From 2255f74c706a04f7c1c1b51ac32d3f35346689e9 Mon Sep 17 00:00:00 2001 From: tmushayahama Date: Wed, 1 Oct 2025 13:41:20 -0700 Subject: [PATCH 1/4] 0.0.3 --- package-lock.json | 4 ++-- package.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 513dcbd..92e3b28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pango-toolbar", - "version": "0.0.2", + "version": "0.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pango-toolbar", - "version": "0.0.2", + "version": "0.0.3", "license": "MIT", "devDependencies": { "@rollup/plugin-image": "^3.0.3", diff --git a/package.json b/package.json index ab47a6e..f292ae9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pango-toolbar", - "version": "0.0.2", + "version": "0.0.3", "description": "PAN-GO toolbar", "main": "dist/index.cjs.js", "module": "dist/index.js", @@ -50,4 +50,4 @@ "rollup-plugin-node-polyfills": "^0.2.1" }, "license": "MIT" -} \ No newline at end of file +} From 0d9c443672a5d1d90ccd11dfa1bf0f21305d22f1 Mon Sep 17 00:00:00 2001 From: tmushayahama Date: Thu, 19 Feb 2026 17:02:10 -0800 Subject: [PATCH 2/4] Align environment constants with site-react versioning (#1) - Restructure environment.ts to match site-react constants pattern with BASE_CONFIG + VERSION_CONFIGS (pango-1/pango-2) - Add apiVersion prop with auto-detection from ?apiVersion= query param - Add align prop to pango-dropdown for right-aligned positioning --- .gitignore | 7 + .plans/feature/mobile-responsive-toolbar.md | 101 ++++++++++ .plans/template.md | 138 +++++++++++++ CHANGELOG.md | 25 +++ CLAUDE.md | 56 ++++++ src/components.d.ts | 6 + .../pango-dropdown/pango-dropdown.scss | 7 +- .../pango-dropdown/pango-dropdown.tsx | 5 +- src/components/pango-dropdown/readme.md | 7 + .../pango-toolbar/pango-toolbar.scss | 74 ++++++- .../pango-toolbar/pango-toolbar.tsx | 182 ++++++++++++++---- src/components/pango-toolbar/readme.md | 11 +- src/utils/environment.ts | 106 +++++++--- 13 files changed, 648 insertions(+), 77 deletions(-) create mode 100644 .plans/feature/mobile-responsive-toolbar.md create mode 100644 .plans/template.md create mode 100644 CHANGELOG.md create mode 100644 CLAUDE.md diff --git a/.gitignore b/.gitignore index c3ea58a..99eb6f9 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,10 @@ $RECYCLE.BIN/ Thumbs.db UserInterfaceState.xcuserstate .env + +# Claude Code +.claude/settings.local.json + +# System Files +.DS_Store +Thumbs.db diff --git a/.plans/feature/mobile-responsive-toolbar.md b/.plans/feature/mobile-responsive-toolbar.md new file mode 100644 index 0000000..467fcbb --- /dev/null +++ b/.plans/feature/mobile-responsive-toolbar.md @@ -0,0 +1,101 @@ +# Task: Add mobile responsive layout to pango-toolbar + +**Status:** ACTIVE +**Branch:** main + +## Goal + +On viewports < 768px, the toolbar shows icon-only buttons for Download (dropdown), About, Help, and a GO-logo button that opens a dropdown with both partner logos. Desktop layout remains unchanged. Matches the React reference at `site-react/src/app/layout/Toolbar.tsx`. + +## Context + +- **Related files:** see Files to Modify below +- **Triggered by:** user request to match React toolbar dropdowns and mobile layout +- **Out of scope:** hamburger menu, loading bar, gene search + +## Current State + +- What works now: Desktop toolbar with title, subtitle, GitHub link, Download dropdown, About/Help links, partner logos. Below 768px, the entire actions section is hidden (`display: none`). +- What's broken/missing: No mobile layout — actions completely disappear on small viewports. + +## Steps + +### Phase 1: pango-dropdown enhancements + +- [ ] Add `@Prop() align: 'left' | 'right' = 'left'` to `pango-dropdown.tsx` +- [ ] Apply `align-right` class conditionally in the dropdown render +- [ ] In `pango-dropdown.scss`: add `--pango-dropdown-trigger-padding` variable to `.dropdown-trigger`, add `.align-right` modifier to `.dropdown-content` + +### Phase 2: Viewport detection in pango-toolbar + +- [ ] Add `@State() isMobile: boolean = false` and `mediaQuery`/`mediaHandler` private fields +- [ ] Add `connectedCallback()` — set up `matchMedia('(max-width: 767px)')`, set initial value, listen for `change` events +- [ ] Add `disconnectedCallback()` — remove the listener + +### Phase 3: Refactor render into helpers + +- [ ] Extract `renderGitHubIcon()` from existing inline SVG +- [ ] Extract `renderDownloadLinks()` to share between mobile/desktop +- [ ] Move existing actions + logos into `renderDesktopActions()` +- [ ] Update `render()` to call these helpers — verify desktop layout is unchanged + +### Phase 4: Mobile layout + +- [ ] Add `renderDownloadIcon()`, `renderInfoIcon()`, `renderHelpIcon()` — inline SVGs matching FA icons +- [ ] Implement `renderMobileActions()`: + - Download icon → `pango-dropdown` with icon trigger and shared download links + - About icon → link to `pangoHome/about` + - Help icon → link to `pangoHome/help` + - Logos icon → `pango-dropdown align="right"` with GO logo trigger, dropdown shows both logos +- [ ] Update `render()`: `{this.isMobile ? this.renderMobileActions() : this.renderDesktopActions()}` + +### Phase 5: SCSS updates + +- [ ] Change `__actions` from `display: none` + media query → always `display: flex` +- [ ] Add styles: `__mobile-actions`, `__icon-button`, `__mobile-logo-icon`, `__logos-dropdown`, `__logos-dropdown-item` + +### Phase 6: Verify + +- [ ] `npm run build` succeeds +- [ ] `npm start` — desktop layout unchanged at >= 768px +- [ ] Mobile layout shows icon buttons at < 768px +- [ ] Download dropdown works at both sizes +- [ ] Logos dropdown works on mobile +- [ ] Click-outside closes dropdowns +- [ ] Resizing across 768px transitions smoothly + +## Recovery Checkpoint + +> **⚠ UPDATE THIS AFTER EVERY CHANGE** + +- **Last completed action:** Plan created +- **Next immediate action:** Add `align` prop to `pango-dropdown.tsx` +- **Recent commands run:** none +- **Uncommitted changes:** none +- **Environment state:** clean working tree on main + +## Failed Approaches + +| What was tried | Why it failed | Date | +| -------------- | ------------- | ---- | +| | | | + +## Files to Modify + +| File | Action | Status | +| ---- | ------ | ------ | +| `src/components/pango-dropdown/pango-dropdown.tsx` | Add `align` prop | pending | +| `src/components/pango-dropdown/pango-dropdown.scss` | Add trigger padding var, align-right modifier | pending | +| `src/components/pango-toolbar/pango-toolbar.tsx` | Add isMobile state, viewport detection, conditional render methods, SVG icons | pending | +| `src/components/pango-toolbar/pango-toolbar.scss` | Remove mobile hiding, add mobile action styles | pending | + +## Blockers + +- None currently + +## Notes + +- **Viewport detection:** `window.matchMedia` in `connectedCallback` (fires before first render, avoids FOUC; more efficient than ResizeObserver) +- **Icons:** Inline SVGs with `fill="currentColor"` (same pattern as existing GitHub icon; can't use react-icons in Shadow DOM) +- **Logos dropdown on mobile:** Reuse `pango-dropdown` component — no new component needed +- **Dropdown alignment:** New `align` prop on `pango-dropdown` prevents right-side viewport overflow diff --git a/.plans/template.md b/.plans/template.md new file mode 100644 index 0000000..b5647e7 --- /dev/null +++ b/.plans/template.md @@ -0,0 +1,138 @@ +# Task Management — Claude Code + +## Core Rule + +**For EVERY task: create, maintain, and read plan files.** + +- **Starting a new task →** Create `.plans/[category]/[task-name].md` +- **Resuming after context loss →** Read ACTIVE plan files in `.plans/` before doing anything +- **After completing a step or phase →** Update the plan + +--- + +## Folder Structure + +``` +.plans/ +├── bugfix/ # Bug fixes and error resolution +├── feature/ # New features and enhancements +├── refactor/ # Code restructuring and cleanup +├── config/ # Configuration, CI/CD, environment changes +├── docs/ # Documentation tasks +├── testing/ # Test creation and test fixes +└── misc/ # Anything that doesn't fit above +``` + +Examples: + +- `.plans/bugfix/login-redirect-loop.md` +- `.plans/feature/dark-mode.md` +- `.plans/refactor/extract-auth-service.md` + +--- + +## Plan Template + +```markdown +# Task: [Clear one-line description] + +**Status:** ACTIVE | COMPLETE | BLOCKED +**Issue:** [issue number/link, e.g. #220] +**Branch:** [branch name] + +## Goal +[1-2 sentences. What does "done" look like?] + +## Context +- **Related files:** [key files involved] +- **Triggered by:** [issue link, user request, etc.] + +## Current State +- What works now: +- What's broken/missing: + +## Steps + +### Phase 1: [Name] +- [ ] Step 1 +- [ ] Step 2 + +### Phase 2: [Name] +- [ ] Step 1 +- [ ] Step 2 + +## Recovery Checkpoint + +> **⚠ UPDATE THIS AFTER EVERY CHANGE** + +- **Last completed action:** [exact file + what was done] +- **Next immediate action:** [the single next thing to do] +- **Recent commands run:** + - `[command 1]` + - `[command 2]` +- **Uncommitted changes:** [list modified/staged files] +- **Environment state:** [anything running, installed, temporary] + +## Failed Approaches + + +| What was tried | Why it failed | Date | +| -------------- | ------------- | ---- | +| | | | + +## Files Modified + +| File | Action | Status | +| ---- | ------ | ------ | +| | | | + +## Blockers +- None currently + +## Notes +- [Design decisions, gotchas, things to remember] + +## Lessons Learned + +- [What went well, what didn't, what you'd do differently] + +## Additional Context (Claude) + +``` + +--- + +## Update Triggers + +Update the plan file (especially **Recovery Checkpoint**) after: +1. Completing a step or phase +2. Every failed attempt (add to **Failed Approaches**) +3. Before any long-running command +4. Before ending a session + +## On Context Resume + +When starting a new session or after context loss: + +1. **FIRST:** `ls .plans/` — find all plan categories +2. **SCAN** for ACTIVE plans (check Status line at top of each file) +3. **READ** active plans, focus on **Recovery Checkpoint** +4. **VERIFY** file states match what the plan says (check git status) +5. **CONTINUE** from the documented next immediate action + +--- + +## When a Task is Complete + +- Set **Status** to: `COMPLETE` +- Mark all steps done +- Set Recovery Checkpoint to: `✅ TASK COMPLETE` +- Trim verbose code snippets — keep decisions and summaries, not implementation details +- Add a `## Summary` section with what was accomplished +- Note any follow-up work needed diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f244593 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,25 @@ +# Changelog + +## [Unreleased] + +### environment.ts — Version-aware constants +- **Before:** Single flat `ENVIRONMENT` object with hardcoded URLs (no version awareness) +- **After:** `BASE_CONFIG` (shared URLs) + `VERSION_CONFIGS` (per-version URLs keyed by `pango-1`/`pango-2`). Exports `getConfig(version)`, `getCurrentConfig()`, and `ENVIRONMENT` (backward compat) +- Key names changed from camelCase to SCREAMING_CASE to match site-react (`downloadAllDataCSVUrl` → `DOWNLOAD_ALL_DATA_CSV_URL`) +- V1 download paths now include `/v1/` segment (e.g. `/download/v1/export_annotations.zip`) +- V2 ontology URL updated from stale `release.geneontology.org/2022-03-22` to `ftp.ebi.ac.uk/.../2025-10-06` +- Added version metadata fields: `APP_VERSION`, `GO_RELEASE`, `PANTHER_VERSION` + +### pango-toolbar.tsx — API version prop +- New `apiVersion` prop (type `'pango-1' | 'pango-2'`, default `pango-2`) +- Download links now use `getConfig(this.apiVersion)` instead of hardcoded `ENVIRONMENT` +- Auto-detects `?apiVersion=` from URL when no `api-version` attribute is set + +### pango-toolbar — Mobile responsive layout +- Toolbar actions now visible on all viewport sizes (previously hidden below 768px) +- Below 768px: icon-only buttons for Download, About, Help, and a logos dropdown +- Above 768px: unchanged desktop layout with text buttons and side-by-side logos + +### pango-dropdown — New features +- `align` prop (`'left' | 'right'`) for dropdown positioning +- `--pango-dropdown-trigger-padding` CSS variable for customizable trigger padding diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..3640721 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,56 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +pango-toolbar is a customizable toolbar web component for the PAN-GO (Human Functionome) project, built with **StencilJS**. It publishes as an npm package usable in plain HTML, React, Angular, and other frameworks. + +## Commands + +- **Dev server**: `npm start` (builds, watches, and serves at localhost) +- **Production build**: `npm run build` +- **Run all tests**: `npm test` (spec + e2e via Jest/Puppeteer) +- **Watch tests**: `npm run test.watch` +- **Run a single test file**: `npx stencil test --spec src/utils/utils.spec.ts` +- **Scaffold new component**: `npm run generate` + +## Architecture + +### StencilJS Web Components + +Both components use **Shadow DOM** for style encapsulation and are styled with SCSS. Stencil uses a JSX syntax with its own `h()` function (not React). + +- **`pango-toolbar`** (`src/components/pango-toolbar/`) — Main toolbar component. Accepts props `headerTitle`, `headerSubTitle`, and `pangoHome`. Renders logo, navigation links (About, Help), a download dropdown, GitHub link, and partner logos (GO, PANTHER). +- **`pango-dropdown`** (`src/components/pango-dropdown/`) — Generic dropdown using named slots (`trigger`, `content`). Emits `dropdownToggle` events. Handles click-outside-to-close via `composedPath()`. + +### Key Files + +- `src/utils/environment.ts` — Centralized external URLs (APIs, downloads, documentation). All URLs used by the toolbar are defined here. +- `src/components.d.ts` — Auto-generated type definitions (do not edit manually). +- `stencil.config.ts` — Build config: SASS plugin, image plugin, output targets (dist, custom-elements, www). + +### Theming + +The toolbar is themed via CSS custom properties. Key variables: +- `--pango-primary-color`, `--pango-accent-color` for colors +- `--pango-toolbar-height`, `--pango-toolbar-bg`, `--pango-toolbar-color` for toolbar appearance +- `--pango-header-font-size`, `--pango-button-font-size` for typography + +### Build Outputs + +Stencil produces multiple output formats in `dist/`: ESM, CJS, and custom elements. The `loader/` directory is also generated. Both `dist/` and `loader/` must be committed (CI verifies this). + +## Code Style + +- Prettier: single quotes, 2-space indent, trailing commas, 180 char line width, no parens on single arrow params +- LF line endings (configured in `.editorconfig`) + +## Git Commits + +- Do NOT include the `Co-Authored-By` line in commit messages. + +## Task Plans + +- Always create and maintain task plans using the [.plans/template.md](.plans/template.md) system. +- On context resume, check `.plans/` for ACTIVE plans before doing anything else. diff --git a/src/components.d.ts b/src/components.d.ts index 0e6e973..bcea6f3 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -5,10 +5,14 @@ * It contains typing information for all components that exist in this project. */ import { HTMLStencilElement, JSXBase } from "@stencil/core/internal"; +import { ApiVersion } from "./utils/environment"; +export { ApiVersion } from "./utils/environment"; export namespace Components { interface PangoDropdown { + "align": 'left' | 'right'; } interface PangoToolbar { + "apiVersion": ApiVersion; "headerSubTitle": string; "headerTitle": string; "pangoHome": string; @@ -49,9 +53,11 @@ declare global { } declare namespace LocalJSX { interface PangoDropdown { + "align"?: 'left' | 'right'; "onDropdownToggle"?: (event: PangoDropdownCustomEvent) => void; } interface PangoToolbar { + "apiVersion"?: ApiVersion; "headerSubTitle"?: string; "headerTitle"?: string; "pangoHome"?: string; diff --git a/src/components/pango-dropdown/pango-dropdown.scss b/src/components/pango-dropdown/pango-dropdown.scss index a3b2388..5ceb9a9 100644 --- a/src/components/pango-dropdown/pango-dropdown.scss +++ b/src/components/pango-dropdown/pango-dropdown.scss @@ -21,7 +21,7 @@ color: var(--pango-dropdown-text); font-size: var(--pango-dropdown-font); cursor: pointer; - padding: 8px 16px; + padding: var(--pango-dropdown-trigger-padding, 8px 16px); text-decoration: none; display: inline-block; @@ -46,6 +46,11 @@ display: block; } + &.align-right { + left: auto; + right: 0; + } + ::slotted(a) { display: block; padding: 8px 16px; diff --git a/src/components/pango-dropdown/pango-dropdown.tsx b/src/components/pango-dropdown/pango-dropdown.tsx index 4a6daae..454c041 100644 --- a/src/components/pango-dropdown/pango-dropdown.tsx +++ b/src/components/pango-dropdown/pango-dropdown.tsx @@ -1,4 +1,4 @@ -import { Component, h, State, Element, Event, EventEmitter } from '@stencil/core'; +import { Component, h, Prop, State, Element, Event, EventEmitter } from '@stencil/core'; @Component({ tag: 'pango-dropdown', @@ -7,6 +7,7 @@ import { Component, h, State, Element, Event, EventEmitter } from '@stencil/core }) export class PangoDropdown { @Element() host: HTMLElement; + @Prop() align: 'left' | 'right' = 'left'; @State() isOpen: boolean = false; @Event() dropdownToggle: EventEmitter; @@ -38,7 +39,7 @@ export class PangoDropdown { Toggle Dropdown -
+
diff --git a/src/components/pango-dropdown/readme.md b/src/components/pango-dropdown/readme.md index 1a4f2e3..f6cea99 100644 --- a/src/components/pango-dropdown/readme.md +++ b/src/components/pango-dropdown/readme.md @@ -5,6 +5,13 @@ +## Properties + +| Property | Attribute | Description | Type | Default | +| -------- | --------- | ----------- | ------------------- | -------- | +| `align` | `align` | | `"left" \| "right"` | `'left'` | + + ## Events | Event | Description | Type | diff --git a/src/components/pango-toolbar/pango-toolbar.scss b/src/components/pango-toolbar/pango-toolbar.scss index 40fc793..6e9e5fc 100644 --- a/src/components/pango-toolbar/pango-toolbar.scss +++ b/src/components/pango-toolbar/pango-toolbar.scss @@ -78,14 +78,10 @@ } &__actions { - display: none; + display: flex; flex: 1; justify-content: flex-end; align-items: center; - - @media (min-width: 768px) { - display: flex; - } } &__social-links { @@ -187,4 +183,72 @@ } } } + + &__mobile-actions { + display: flex; + align-items: center; + gap: 4px; + padding-left: 8px; + + pango-dropdown { + --pango-dropdown-trigger-padding: 8px; + } + } + + &__icon-button { + display: flex; + align-items: center; + justify-content: center; + background: none; + border: none; + color: var(--pango-toolbar-color); + cursor: pointer; + padding: 8px; + text-decoration: none; + line-height: 1; + + &:hover { + color: var(--pango-toolbar-hover-color); + } + + svg { + display: block; + } + } + + &__mobile-logo-icon { + height: 20px; + width: auto; + display: block; + } + + &__logos-dropdown { + display: flex; + flex-direction: column; + background: white; + border-radius: 4px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + min-width: 180px; + padding: 8px 0; + } + + &__logos-dropdown-item { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 16px; + color: var(--pango-toolbar-dropdown-color); + text-decoration: none; + font-size: var(--pango-button-font-size); + transition: background 0.2s ease; + + &:hover { + background: rgba(235, 195, 54, 0.1); + } + + img { + height: 20px; + width: auto; + } + } } diff --git a/src/components/pango-toolbar/pango-toolbar.tsx b/src/components/pango-toolbar/pango-toolbar.tsx index cd51983..0c935e9 100644 --- a/src/components/pango-toolbar/pango-toolbar.tsx +++ b/src/components/pango-toolbar/pango-toolbar.tsx @@ -1,5 +1,5 @@ import { Component, Prop, h, State, Element } from '@stencil/core'; -import { ENVIRONMENT } from '../../utils/environment'; +import { type ApiVersion, ApiVersions, getConfig } from '../../utils/environment'; // Import images import goLogo from './../../assets/images/go-logo-yellow.png'; @@ -9,7 +9,7 @@ import pantherLogo from './../../assets/images/panther-logo-yellow.png'; tag: 'pango-toolbar', styleUrl: 'pango-toolbar.scss', shadow: true, - assetsDirs: ['assets'] + assetsDirs: ['assets'], }) export class PangoToolbar { @Element() el: HTMLElement; @@ -17,8 +17,150 @@ export class PangoToolbar { @Prop() headerTitle: string = 'PAN-GO'; @Prop() headerSubTitle: string = 'Human Functionome'; @Prop() pangoHome: string = '/'; + @Prop() apiVersion: ApiVersion = ApiVersions.V2; - @State() isDropdownOpen: boolean = false; + @State() isMobile: boolean = false; + + private mediaQuery: MediaQueryList; + private mediaHandler = (e: MediaQueryListEvent) => { + this.isMobile = e.matches; + }; + + connectedCallback() { + this.mediaQuery = window.matchMedia('(max-width: 767px)'); + this.isMobile = this.mediaQuery.matches; + this.mediaQuery.addEventListener('change', this.mediaHandler); + + // Auto-detect version from URL query param if prop was not explicitly set + if (!this.el.hasAttribute('api-version')) { + const searchParams = new URLSearchParams(window.location.search); + const urlVersion = searchParams.get('apiVersion') as ApiVersion; + if (urlVersion && (urlVersion === ApiVersions.V1 || urlVersion === ApiVersions.V2)) { + this.apiVersion = urlVersion; + } + } + } + + disconnectedCallback() { + this.mediaQuery?.removeEventListener('change', this.mediaHandler); + } + + private renderGitHubIcon() { + return ( + + ); + } + + private renderDownloadIcon() { + return ( + + ); + } + + private renderInfoIcon() { + return ( + + ); + } + + private renderHelpIcon() { + return ( + + ); + } + + private renderDownloadLinks() { + const config = getConfig(this.apiVersion); + return [ + All data as CSV, + All data as JSON, + Annotations as GAF, + + Evolutionary models as GAF + , + + Ontology Files + , + ]; + } + + private renderDesktopActions() { + return [ +
+ + Download + + + + About + + + Help + +
, +
+
+ +
+
+ +
+
, + ]; + } + + private renderMobileActions() { + return ( +
+ + + {this.renderDownloadIcon()} + + + + + + {this.renderInfoIcon()} + + + + {this.renderHelpIcon()} + + + + + Partners + + + +
+ ); + } render() { return ( @@ -36,42 +178,14 @@ export class PangoToolbar {
- - -
-
- -
-
- -
-
+ {this.isMobile ? this.renderMobileActions() : this.renderDesktopActions()}
); } -} \ No newline at end of file +} diff --git a/src/components/pango-toolbar/readme.md b/src/components/pango-toolbar/readme.md index d5666fa..50ea4dd 100644 --- a/src/components/pango-toolbar/readme.md +++ b/src/components/pango-toolbar/readme.md @@ -7,11 +7,12 @@ ## Properties -| Property | Attribute | Description | Type | Default | -| ---------------- | ------------------ | ----------- | -------- | --------------------- | -| `headerSubTitle` | `header-sub-title` | | `string` | `'Human Functionome'` | -| `headerTitle` | `header-title` | | `string` | `'PAN-GO'` | -| `pangoHome` | `pango-home` | | `string` | `'/'` | +| Property | Attribute | Description | Type | Default | +| ---------------- | ------------------ | ----------- | ------------------------ | --------------------- | +| `apiVersion` | `api-version` | | `"pango-1" \| "pango-2"` | `ApiVersions.V2` | +| `headerSubTitle` | `header-sub-title` | | `string` | `'Human Functionome'` | +| `headerTitle` | `header-title` | | `string` | `'PAN-GO'` | +| `pangoHome` | `pango-home` | | `string` | `'/'` | ## Dependencies diff --git a/src/utils/environment.ts b/src/utils/environment.ts index e2a1fb9..9681d22 100644 --- a/src/utils/environment.ts +++ b/src/utils/environment.ts @@ -1,33 +1,79 @@ -export const ENVIRONMENT = { - contactUrl: - 'https://docs.google.com/forms/d/e/1FAIpQLScX_caoY-mqsyK5Y6M2bof7EXVG0UY5DhOQ67zBMoAKKlRF4Q/viewform?usp=sharing', +export const ApiVersions = { + V1: 'pango-1', + V2: 'pango-2', +} as const; - contactPrefillUrl: 'https://docs.google.com/forms/d/e/1FAIpQLScX_caoY-mqsyK5Y6M2bof7EXVG0UY5DhOQ67zBMoAKKlRF4Q/viewform?usp=pp_url', - amigoTermUrl: 'http://amigo.geneontology.org/amigo/term/', - amigoGPUrl: 'http://amigo.geneontology.org/amigo/gene_product/', - pubmedUrl: 'https://www.ncbi.nlm.nih.gov/pubmed/', - taxonApiUrl: 'https://www.ncbi.nlm.nih.gov/Taxonomy/Browser/wwwtax.cgi?id=', +export type ApiVersion = (typeof ApiVersions)[keyof typeof ApiVersions]; - ucscUrl: +// Base configuration - shared across all versions +export const BASE_CONFIG = { + CONTACT_URL: + 'https://docs.google.com/forms/d/e/1FAIpQLScX_caoY-mqsyK5Y6M2bof7EXVG0UY5DhOQ67zBMoAKKlRF4Q/viewform?usp=sharing', + CONTACT_PREFILL_URL: + 'https://docs.google.com/forms/d/e/1FAIpQLScX_caoY-mqsyK5Y6M2bof7EXVG0UY5DhOQ67zBMoAKKlRF4Q/viewform?usp=pp_url', + AMIGO_TERM_URL: 'http://amigo.geneontology.org/amigo/term/', + AMIGO_GP_URL: 'http://amigo.geneontology.org/amigo/gene_product/', + PUBMED_URL: 'https://www.ncbi.nlm.nih.gov/pubmed/', + TAXON_API_URL: 'https://www.ncbi.nlm.nih.gov/Taxonomy/Browser/wwwtax.cgi?id=', + UCSC_URL: 'https://genome.ucsc.edu/cgi-bin/hgTracks?db=hg38&lastVirtModeType=default&lastVirtModeExtraState=&virtModeType=default&virtMode=0&nonVirtPosition=&position=chr', - pantherFamilyUrl: 'https://enrichment.functionome.org/treeViewer/treeViewer.jsp?', - overrepApiUrl: 'https://enrichment.functionome.org/webservices/go/overrep.jsp', - - uniprotUrl: 'https://www.uniprot.org/uniprotkb/', - agrPrefixUrl: 'https://www.alliancegenome.org/gene/', - hgncPrefixUrl: 'https://www.genenames.org/data/gene-symbol-report/#!/hgnc_id/', - ncbiGeneUrl: 'https://www.ncbi.nlm.nih.gov/gene/?term=', - pantreeUrl: 'https://pantree.functionome.org/tree/family.jsp?accession=', - - // Downloads - downloadAllDataCSVUrl: 'https://functionome.geneontology.org/download/export_annotations.zip', - downloadAllDataJSONUrl: - 'https://functionome.geneontology.org/download/export_annotations.json.gz', - downloadAnnotationsGAFUrl: - 'https://functionome.geneontology.org/download/functionome_release.gaf.gz', - downloadEvolutionaryModelsGAFUrl: 'https://functionome.geneontology.org/download/IBD.gaf', - downloadOntologyFilesUrl: 'https://release.geneontology.org/2022-03-22/ontology/index.html', - - overrepDocsApiUrl: 'https://geneontology.org/docs/go-enrichment-analysis/', - paperUrl: 'https://www.nature.com/articles/s41586-025-08592-0', -} \ No newline at end of file + UNIPROT_URL: 'https://www.uniprot.org/uniprotkb/', + AGR_PREFIX_URL: 'https://www.alliancegenome.org/gene/', + HGNC_PREFIX_URL: 'https://www.genenames.org/data/gene-symbol-report/#!/hgnc_id/', + NCBI_GENE_URL: 'https://www.ncbi.nlm.nih.gov/gene/?term=', + PAPER_URL: 'https://www.nature.com/articles/s41586-025-08592-0', + OVERREP_DOCS_API_URL: 'https://geneontology.org/docs/go-enrichment-analysis/', +}; + +// Version-specific configurations +const VERSION_CONFIGS = { + [ApiVersions.V1]: { + PANTREE_URL: 'https://pantree-v1.functionome.org/tree/family.jsp?accession=', + PANTHER_FAMILY_URL: 'https://enrichment-v1.functionome.org/treeViewer/treeViewer.jsp?', + OVERREP_API_URL: 'https://enrichment-v1.functionome.org/webservices/go/overrep.jsp', + // Downloads + DOWNLOAD_ALL_DATA_CSV_URL: 'https://functionome.geneontology.org/download/v1/export_annotations.zip', + DOWNLOAD_ALL_DATA_JSON_URL: 'https://functionome.geneontology.org/download/v1/export_annotations.json.gz', + DOWNLOAD_ANNOTATIONS_GAF_URL: 'https://functionome.geneontology.org/download/v1/functionome_release.gaf.gz', + DOWNLOAD_EVOLUTIONARY_MODELS_GAF_URL: 'https://functionome.geneontology.org/download/v1/IBD.gaf', + DOWNLOAD_ONTOLOGY_FILES_URL: 'https://release.geneontology.org/2022-03-22/ontology/index.html', + + // Version metadata + APP_VERSION: '1.0', + GO_RELEASE: '2022-03-22', + PANTHER_VERSION: '15.0', + }, + + [ApiVersions.V2]: { + PANTREE_URL: 'https://pantree.functionome.org/tree/family.jsp?accession=', + PANTHER_FAMILY_URL: 'https://enrichment.functionome.org/treeViewer/treeViewer.jsp?', + OVERREP_API_URL: 'https://enrichment.functionome.org/webservices/go/overrep.jsp', + // Downloads + DOWNLOAD_ALL_DATA_CSV_URL: 'https://functionome.geneontology.org/download/export_annotations.zip', + DOWNLOAD_ALL_DATA_JSON_URL: 'https://functionome.geneontology.org/download/export_annotations.json.gz', + DOWNLOAD_ANNOTATIONS_GAF_URL: 'https://functionome.geneontology.org/download/functionome_release.gaf.gz', + DOWNLOAD_EVOLUTIONARY_MODELS_GAF_URL: 'https://functionome.geneontology.org/download/IBD.gaf', + DOWNLOAD_ONTOLOGY_FILES_URL: 'https://ftp.ebi.ac.uk/pub/contrib/goa/goex/releases/2025-10-06/ontology/', + + // Version metadata + APP_VERSION: '2.0', + GO_RELEASE: 'Oct 2025', + PANTHER_VERSION: '19.0', + }, +}; + +const DEFAULT_VERSION = ApiVersions.V2; + +export const getConfig = (version: ApiVersion = DEFAULT_VERSION) => ({ + ...BASE_CONFIG, + ...VERSION_CONFIGS[version], +}); + +export const getCurrentConfig = () => { + const searchParams = new URLSearchParams(window.location.search); + const version = (searchParams.get('apiVersion') as ApiVersion) || DEFAULT_VERSION; + return getConfig(version); +}; + +// Backward compatibility - default export for current version +export const ENVIRONMENT = getCurrentConfig(); From 31ebfbd189a5223d4fb2785c174ac87d0bfae2fa Mon Sep 17 00:00:00 2001 From: tmushayahama Date: Thu, 19 Feb 2026 17:30:18 -0800 Subject: [PATCH 3/4] Add spec and e2e tests for pango-toolbar and pango-dropdown Add unit tests (spec) and end-to-end tests for both components, covering rendering, props, events, and alignment. Configure Jest image mocking in stencil.config.ts to handle PNG imports in tests. --- .plans/testing/component-tests.md | 39 ++++++ .../pango-dropdown/pango-dropdown.e2e.ts | 62 +++++++++ .../pango-dropdown/pango-dropdown.spec.ts | 123 +++++++++++++++++ .../pango-toolbar/pango-toolbar.e2e.ts | 51 +++++++ .../pango-toolbar/pango-toolbar.spec.ts | 124 ++++++++++++++++++ src/utils/__mocks__/imageMock.ts | 1 + stencil.config.ts | 5 + 7 files changed, 405 insertions(+) create mode 100644 .plans/testing/component-tests.md create mode 100644 src/components/pango-dropdown/pango-dropdown.e2e.ts create mode 100644 src/components/pango-dropdown/pango-dropdown.spec.ts create mode 100644 src/components/pango-toolbar/pango-toolbar.e2e.ts create mode 100644 src/components/pango-toolbar/pango-toolbar.spec.ts create mode 100644 src/utils/__mocks__/imageMock.ts diff --git a/.plans/testing/component-tests.md b/.plans/testing/component-tests.md new file mode 100644 index 0000000..c7778b1 --- /dev/null +++ b/.plans/testing/component-tests.md @@ -0,0 +1,39 @@ +# Task: Write spec and e2e tests for pango-toolbar and pango-dropdown + +**Status:** COMPLETE +**Branch:** issue-1-toolbar-versioning + +## Goal +Create comprehensive spec (unit) and e2e tests for both components: `pango-toolbar` and `pango-dropdown`. + +## Steps + +### Phase 1: Spec Tests +- [x] Write `pango-toolbar.spec.ts` — 11 tests covering default rendering, custom props, download links, version handling, partner logos +- [x] Write `pango-dropdown.spec.ts` — 10 tests covering rendering, toggle, events, alignment, structure + +### Phase 2: E2E Tests +- [x] Write `pango-toolbar.e2e.ts` — 6 tests for browser rendering, nav links, custom props +- [x] Write `pango-dropdown.e2e.ts` — 6 tests for rendering, toggle, events, alignment + +### Phase 3: Verify +- [x] Spec tests: 25/25 passing +- [x] E2E tests: written but cannot run locally (Chrome not installed at expected path) + +## Recovery Checkpoint +> ✅ TASK COMPLETE + +## Files Modified + +| File | Action | Status | +| ---- | ------ | ------ | +| src/components/pango-toolbar/pango-toolbar.spec.ts | Create | Done | +| src/components/pango-dropdown/pango-dropdown.spec.ts | Create | Done | +| src/components/pango-toolbar/pango-toolbar.e2e.ts | Create | Done | +| src/components/pango-dropdown/pango-dropdown.e2e.ts | Create | Done | +| src/utils/__mocks__/imageMock.ts | Create | Done | +| stencil.config.ts | Edit (added testing.moduleNameMapper) | Done | + +## Notes +- Had to add `moduleNameMapper` in `stencil.config.ts` to mock PNG imports in spec tests +- Created `src/utils/__mocks__/imageMock.ts` as the image stub diff --git a/src/components/pango-dropdown/pango-dropdown.e2e.ts b/src/components/pango-dropdown/pango-dropdown.e2e.ts new file mode 100644 index 0000000..b377900 --- /dev/null +++ b/src/components/pango-dropdown/pango-dropdown.e2e.ts @@ -0,0 +1,62 @@ +import { newE2EPage } from '@stencil/core/testing'; + +describe('pango-dropdown e2e', () => { + it('renders on the page', async () => { + const page = await newE2EPage(); + await page.setContent(''); + const el = await page.find('pango-dropdown'); + expect(el).not.toBeNull(); + }); + + it('is closed by default', async () => { + const page = await newE2EPage(); + await page.setContent('Click'); + const content = await page.find('pango-dropdown >>> .dropdown-content'); + expect(content).not.toHaveClass('active'); + }); + + it('opens on trigger click', async () => { + const page = await newE2EPage(); + await page.setContent('Click'); + const trigger = await page.find('pango-dropdown >>> .dropdown-trigger'); + await trigger.click(); + await page.waitForChanges(); + + const content = await page.find('pango-dropdown >>> .dropdown-content'); + expect(content).toHaveClass('active'); + }); + + it('closes on second click', async () => { + const page = await newE2EPage(); + await page.setContent('Click'); + const trigger = await page.find('pango-dropdown >>> .dropdown-trigger'); + + await trigger.click(); + await page.waitForChanges(); + await trigger.click(); + await page.waitForChanges(); + + const content = await page.find('pango-dropdown >>> .dropdown-content'); + expect(content).not.toHaveClass('active'); + }); + + it('emits dropdownToggle event', async () => { + const page = await newE2EPage(); + await page.setContent('Click'); + const toggleEvent = await page.spyOnEvent('dropdownToggle'); + + const trigger = await page.find('pango-dropdown >>> .dropdown-trigger'); + await trigger.click(); + await page.waitForChanges(); + + expect(toggleEvent).toHaveReceivedEvent(); + expect(toggleEvent).toHaveReceivedEventDetail(true); + }); + + it('applies align-right class', async () => { + const page = await newE2EPage(); + await page.setContent('Click'); + const content = await page.find('pango-dropdown >>> .dropdown-content'); + expect(content).toHaveClass('align-right'); + }); +}); diff --git a/src/components/pango-dropdown/pango-dropdown.spec.ts b/src/components/pango-dropdown/pango-dropdown.spec.ts new file mode 100644 index 0000000..048b893 --- /dev/null +++ b/src/components/pango-dropdown/pango-dropdown.spec.ts @@ -0,0 +1,123 @@ +import { newSpecPage } from '@stencil/core/testing'; +import { PangoDropdown } from './pango-dropdown'; + +describe('pango-dropdown', () => { + it('renders', async () => { + const page = await newSpecPage({ + components: [PangoDropdown], + html: ``, + }); + expect(page.root).toBeTruthy(); + expect(page.root.shadowRoot).toBeTruthy(); + }); + + it('renders with trigger slot content', async () => { + const page = await newSpecPage({ + components: [PangoDropdown], + html: `Click Me`, + }); + const trigger = page.root.shadowRoot.querySelector('.dropdown-trigger'); + expect(trigger).toBeTruthy(); + }); + + it('is closed by default', async () => { + const page = await newSpecPage({ + components: [PangoDropdown], + html: ``, + }); + const content = page.root.shadowRoot.querySelector('.dropdown-content'); + expect(content.classList.contains('active')).toBe(false); + }); + + it('opens on trigger click', async () => { + const page = await newSpecPage({ + components: [PangoDropdown], + html: `Toggle`, + }); + const trigger = page.root.shadowRoot.querySelector('.dropdown-trigger') as HTMLAnchorElement; + trigger.click(); + await page.waitForChanges(); + + const content = page.root.shadowRoot.querySelector('.dropdown-content'); + expect(content.classList.contains('active')).toBe(true); + }); + + it('closes on second trigger click', async () => { + const page = await newSpecPage({ + components: [PangoDropdown], + html: `Toggle`, + }); + const trigger = page.root.shadowRoot.querySelector('.dropdown-trigger') as HTMLAnchorElement; + + trigger.click(); + await page.waitForChanges(); + trigger.click(); + await page.waitForChanges(); + + const content = page.root.shadowRoot.querySelector('.dropdown-content'); + expect(content.classList.contains('active')).toBe(false); + }); + + it('emits dropdownToggle event on open', async () => { + const page = await newSpecPage({ + components: [PangoDropdown], + html: `Toggle`, + }); + const spy = jest.fn(); + page.root.addEventListener('dropdownToggle', spy); + + const trigger = page.root.shadowRoot.querySelector('.dropdown-trigger') as HTMLAnchorElement; + trigger.click(); + await page.waitForChanges(); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy.mock.calls[0][0].detail).toBe(true); + }); + + it('emits dropdownToggle event on close', async () => { + const page = await newSpecPage({ + components: [PangoDropdown], + html: `Toggle`, + }); + const spy = jest.fn(); + page.root.addEventListener('dropdownToggle', spy); + + const trigger = page.root.shadowRoot.querySelector('.dropdown-trigger') as HTMLAnchorElement; + trigger.click(); + await page.waitForChanges(); + trigger.click(); + await page.waitForChanges(); + + expect(spy).toHaveBeenCalledTimes(2); + expect(spy.mock.calls[1][0].detail).toBe(false); + }); + + it('defaults to left alignment', async () => { + const page = await newSpecPage({ + components: [PangoDropdown], + html: ``, + }); + const content = page.root.shadowRoot.querySelector('.dropdown-content'); + expect(content.classList.contains('align-right')).toBe(false); + }); + + it('applies align-right class when align="right"', async () => { + const page = await newSpecPage({ + components: [PangoDropdown], + html: ``, + }); + const content = page.root.shadowRoot.querySelector('.dropdown-content'); + expect(content.classList.contains('align-right')).toBe(true); + }); + + it('renders dropdown wrapper structure', async () => { + const page = await newSpecPage({ + components: [PangoDropdown], + html: ``, + }); + const dropdown = page.root.shadowRoot.querySelector('.dropdown'); + expect(dropdown).toBeTruthy(); + expect(dropdown.querySelector('.dropdown-trigger')).toBeTruthy(); + expect(dropdown.querySelector('.dropdown-content')).toBeTruthy(); + }); +}); diff --git a/src/components/pango-toolbar/pango-toolbar.e2e.ts b/src/components/pango-toolbar/pango-toolbar.e2e.ts new file mode 100644 index 0000000..7161b50 --- /dev/null +++ b/src/components/pango-toolbar/pango-toolbar.e2e.ts @@ -0,0 +1,51 @@ +import { newE2EPage } from '@stencil/core/testing'; + +describe('pango-toolbar e2e', () => { + it('renders on the page', async () => { + const page = await newE2EPage(); + await page.setContent(''); + const el = await page.find('pango-toolbar'); + expect(el).not.toBeNull(); + }); + + it('displays default title', async () => { + const page = await newE2EPage(); + await page.setContent(''); + const title = await page.find('pango-toolbar >>> .pango-toolbar__logo-text'); + expect(title.textContent).toBe('PAN-GO'); + }); + + it('displays custom title and subtitle', async () => { + const page = await newE2EPage(); + await page.setContent(''); + const title = await page.find('pango-toolbar >>> .pango-toolbar__logo-text'); + const subtitle = await page.find('pango-toolbar >>> .pango-toolbar__header-text'); + expect(title.textContent).toBe('Test App'); + expect(subtitle.textContent).toBe('Test Sub'); + }); + + it('contains a GitHub link', async () => { + const page = await newE2EPage(); + await page.setContent(''); + const githubLink = await page.find('pango-toolbar >>> a[href="https://github.com/pantherdb/pango"]'); + expect(githubLink).not.toBeNull(); + }); + + it('renders about and help links with default home', async () => { + const page = await newE2EPage(); + await page.setContent(''); + const aboutLink = await page.find('pango-toolbar >>> a[href="/about"]'); + const helpLink = await page.find('pango-toolbar >>> a[href="/help"]'); + expect(aboutLink).not.toBeNull(); + expect(helpLink).not.toBeNull(); + }); + + it('renders about and help links with custom home', async () => { + const page = await newE2EPage(); + await page.setContent(''); + const aboutLink = await page.find('pango-toolbar >>> a[href="/custom/about"]'); + const helpLink = await page.find('pango-toolbar >>> a[href="/custom/help"]'); + expect(aboutLink).not.toBeNull(); + expect(helpLink).not.toBeNull(); + }); +}); diff --git a/src/components/pango-toolbar/pango-toolbar.spec.ts b/src/components/pango-toolbar/pango-toolbar.spec.ts new file mode 100644 index 0000000..047b7bd --- /dev/null +++ b/src/components/pango-toolbar/pango-toolbar.spec.ts @@ -0,0 +1,124 @@ +import { newSpecPage } from '@stencil/core/testing'; +import { PangoToolbar } from './pango-toolbar'; + +describe('pango-toolbar', () => { + it('renders with default props', async () => { + const page = await newSpecPage({ + components: [PangoToolbar], + html: ``, + }); + expect(page.root).toBeTruthy(); + expect(page.root.shadowRoot).toBeTruthy(); + }); + + it('displays default title and subtitle', async () => { + const page = await newSpecPage({ + components: [PangoToolbar], + html: ``, + }); + const title = page.root.shadowRoot.querySelector('.pango-toolbar__logo-text'); + const subtitle = page.root.shadowRoot.querySelector('.pango-toolbar__header-text'); + expect(title.textContent).toBe('PAN-GO'); + expect(subtitle.textContent).toBe('Human Functionome'); + }); + + it('renders custom title and subtitle', async () => { + const page = await newSpecPage({ + components: [PangoToolbar], + html: ``, + }); + const title = page.root.shadowRoot.querySelector('.pango-toolbar__logo-text'); + const subtitle = page.root.shadowRoot.querySelector('.pango-toolbar__header-text'); + expect(title.textContent).toBe('Custom Title'); + expect(subtitle.textContent).toBe('Custom Sub'); + }); + + it('uses pangoHome for logo links', async () => { + const page = await newSpecPage({ + components: [PangoToolbar], + html: ``, + }); + const logoLinks = page.root.shadowRoot.querySelectorAll('.pango-toolbar__logo-link'); + logoLinks.forEach(link => { + expect(link.getAttribute('href')).toBe('/app'); + }); + }); + + it('uses pangoHome for about and help links', async () => { + const page = await newSpecPage({ + components: [PangoToolbar], + html: ``, + }); + const aboutLink = page.root.shadowRoot.querySelector('a[href="/app/about"]'); + const helpLink = page.root.shadowRoot.querySelector('a[href="/app/help"]'); + expect(aboutLink).toBeTruthy(); + expect(helpLink).toBeTruthy(); + }); + + it('renders GitHub link', async () => { + const page = await newSpecPage({ + components: [PangoToolbar], + html: ``, + }); + const githubLink = page.root.shadowRoot.querySelector('a[href="https://github.com/pantherdb/pango"]'); + expect(githubLink).toBeTruthy(); + expect(githubLink.getAttribute('target')).toBe('_blank'); + }); + + it('renders download links with V2 URLs by default', async () => { + const page = await newSpecPage({ + components: [PangoToolbar], + html: ``, + }); + const csvLink = page.root.shadowRoot.querySelector( + 'a[href="https://functionome.geneontology.org/download/export_annotations.zip"]', + ); + const jsonLink = page.root.shadowRoot.querySelector( + 'a[href="https://functionome.geneontology.org/download/export_annotations.json.gz"]', + ); + expect(csvLink).toBeTruthy(); + expect(jsonLink).toBeTruthy(); + }); + + it('renders download links with V1 URLs when api-version is pango-1', async () => { + const page = await newSpecPage({ + components: [PangoToolbar], + html: ``, + }); + const csvLink = page.root.shadowRoot.querySelector( + 'a[href="https://functionome.geneontology.org/download/v1/export_annotations.zip"]', + ); + expect(csvLink).toBeTruthy(); + }); + + it('renders partner logo links', async () => { + const page = await newSpecPage({ + components: [PangoToolbar], + html: ``, + }); + const goLink = page.root.shadowRoot.querySelector('a[href="http://geneontology.org/"]'); + const pantherLink = page.root.shadowRoot.querySelector('a[href="http://pantherdb.org"]'); + expect(goLink).toBeTruthy(); + expect(pantherLink).toBeTruthy(); + }); + + it('renders SVG icons', async () => { + const page = await newSpecPage({ + components: [PangoToolbar], + html: ``, + }); + const svgs = page.root.shadowRoot.querySelectorAll('svg'); + expect(svgs.length).toBeGreaterThan(0); + }); + + it('defaults pangoHome to /', async () => { + const page = await newSpecPage({ + components: [PangoToolbar], + html: ``, + }); + const logoLinks = page.root.shadowRoot.querySelectorAll('.pango-toolbar__logo-link'); + logoLinks.forEach(link => { + expect(link.getAttribute('href')).toBe('/'); + }); + }); +}); diff --git a/src/utils/__mocks__/imageMock.ts b/src/utils/__mocks__/imageMock.ts new file mode 100644 index 0000000..cbd849c --- /dev/null +++ b/src/utils/__mocks__/imageMock.ts @@ -0,0 +1 @@ +export default 'mock-image'; diff --git a/stencil.config.ts b/stencil.config.ts index e6a3c6e..f8cffec 100644 --- a/stencil.config.ts +++ b/stencil.config.ts @@ -43,6 +43,11 @@ export const config: Config = { ] }, ], + testing: { + moduleNameMapper: { + '\\.(png|jpg|jpeg|gif|svg)$': '/src/utils/__mocks__/imageMock.ts', + }, + }, extras: { enableImportInjection: true } From 1fbd94c34c9a6f99b92062af88b3d07970592f94 Mon Sep 17 00:00:00 2001 From: tmushayahama Date: Thu, 19 Feb 2026 17:52:05 -0800 Subject: [PATCH 4/4] ci: configure semantic-release and update developer docs - Remove publish.yml (superseded by semantic-release) - Update ci.yml to PR-only trigger - Update semantic-release.yml with permissions and v24 - Add commitlint config and CI workflow - Fix package.json repository URL to pantherdb/pango-toolbar - Clean up CHANGELOG.md for semantic-release ownership - Expand README with contributing guide and release workflow --- .github/workflows/ci.yml | 3 - .github/workflows/commitlint.yml | 25 ++++++ .github/workflows/publish.yml | 48 ----------- .github/workflows/semantic-release.yml | 13 ++- .plans/config/semantic-release-setup.md | 41 +++++++++ CHANGELOG.md | 40 ++++----- commitlint.config.js | 3 + package.json | 7 +- readme.md | 108 +++++++++++++++++++++--- 9 files changed, 198 insertions(+), 90 deletions(-) create mode 100644 .github/workflows/commitlint.yml delete mode 100644 .github/workflows/publish.yml create mode 100644 .plans/config/semantic-release-setup.md create mode 100644 commitlint.config.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e06fede..d7a7fd1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,6 @@ name: Build and Test on: pull_request: - push: - branches: - - main jobs: build-and-test: diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml new file mode 100644 index 0000000..8ab545f --- /dev/null +++ b/.github/workflows/commitlint.yml @@ -0,0 +1,25 @@ +name: Commitlint + +on: + pull_request: + +jobs: + commitlint: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + cache: 'npm' + + - name: Install commitlint + run: npm install --no-save @commitlint/cli @commitlint/config-conventional + + - name: Lint commits + run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index c92edb9..0000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: Publish Release - -on: - release: - types: - - published - -jobs: - publish: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20.x' - registry-url: 'https://registry.npmjs.org' - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Build components - run: npm run build - - - name: Verify build outputs - run: | - echo "Checking dist outputs..." - ls -la dist/ - echo "Checking loader outputs..." - ls -la loader/ - echo "Verifying package.json exports..." - node -e "console.log(JSON.stringify(require('./package.json').exports, null, 2))" - - - name: Update package version - run: | - VERSION=${GITHUB_REF#refs/tags/} - VERSION=${VERSION#v} - echo "Setting version to: $VERSION" - npm version $VERSION --no-git-tag-version - - - name: Publish to NPM - run: npm publish --access public - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/semantic-release.yml b/.github/workflows/semantic-release.yml index beb88ae..b02967d 100644 --- a/.github/workflows/semantic-release.yml +++ b/.github/workflows/semantic-release.yml @@ -5,11 +5,16 @@ on: branches: - main +permissions: + contents: write + issues: write + pull-requests: write + jobs: release: runs-on: ubuntu-latest - if: ${{ !contains(github.event.head_commit.message, 'skip ci') }} - + if: ${{ !contains(github.event.head_commit.message, '[skip ci]') }} + steps: - name: Checkout code uses: actions/checkout@v4 @@ -36,10 +41,10 @@ jobs: - name: Semantic Release uses: cycjimmy/semantic-release-action@v4 with: - semantic_version: 21 + semantic_version: 24 extra_plugins: | @semantic-release/changelog @semantic-release/git env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} \ No newline at end of file + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.plans/config/semantic-release-setup.md b/.plans/config/semantic-release-setup.md new file mode 100644 index 0000000..a2cce75 --- /dev/null +++ b/.plans/config/semantic-release-setup.md @@ -0,0 +1,41 @@ +# Task: Set up semantic-release for automated npm publishing + +**Status:** ACTIVE +**Branch:** issue-1-toolbar-versioning + +## Goal + +Configure semantic-release for fully automated versioning, changelogs, npm publishing, and GitHub releases. Next release will be 0.1.0. + +## Steps + +- [x] Fix package.json metadata (repo URL, bugs, homepage, prepublishOnly) +- [x] Clean up CHANGELOG.md for semantic-release ownership +- [x] Delete redundant publish.yml workflow +- [x] Update ci.yml to PR-only trigger +- [x] Update semantic-release.yml (permissions, semantic_version 24, skip-ci fix) +- [x] Add commitlint config and CI workflow +- [ ] Commit, push, and verify + +## Recovery Checkpoint + +- **Last completed action:** All file changes made +- **Next immediate action:** Commit and push + +## Files Modified + +| File | Action | Status | +| ---- | ------ | ------ | +| package.json | Edit (repo URL, bugs, homepage, prepublishOnly) | Done | +| CHANGELOG.md | Rewrite for semantic-release | Done | +| .github/workflows/publish.yml | Delete | Done | +| .github/workflows/ci.yml | Edit (PR-only trigger) | Done | +| .github/workflows/semantic-release.yml | Rewrite (permissions, v24, skip-ci) | Done | +| commitlint.config.js | Create | Done | +| .github/workflows/commitlint.yml | Create | Done | + +## Notes + +- Squash merge to main with `feat:` prefix will trigger 0.0.3 -> 0.1.0 +- NPM_TOKEN secret must be set in pantherdb/pango-toolbar repo settings +- If main has branch protection, allow GH Actions to bypass or use a PAT diff --git a/CHANGELOG.md b/CHANGELOG.md index f244593..906cc81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,25 +1,19 @@ # Changelog -## [Unreleased] - -### environment.ts — Version-aware constants -- **Before:** Single flat `ENVIRONMENT` object with hardcoded URLs (no version awareness) -- **After:** `BASE_CONFIG` (shared URLs) + `VERSION_CONFIGS` (per-version URLs keyed by `pango-1`/`pango-2`). Exports `getConfig(version)`, `getCurrentConfig()`, and `ENVIRONMENT` (backward compat) -- Key names changed from camelCase to SCREAMING_CASE to match site-react (`downloadAllDataCSVUrl` → `DOWNLOAD_ALL_DATA_CSV_URL`) -- V1 download paths now include `/v1/` segment (e.g. `/download/v1/export_annotations.zip`) -- V2 ontology URL updated from stale `release.geneontology.org/2022-03-22` to `ftp.ebi.ac.uk/.../2025-10-06` -- Added version metadata fields: `APP_VERSION`, `GO_RELEASE`, `PANTHER_VERSION` - -### pango-toolbar.tsx — API version prop -- New `apiVersion` prop (type `'pango-1' | 'pango-2'`, default `pango-2`) -- Download links now use `getConfig(this.apiVersion)` instead of hardcoded `ENVIRONMENT` -- Auto-detects `?apiVersion=` from URL when no `api-version` attribute is set - -### pango-toolbar — Mobile responsive layout -- Toolbar actions now visible on all viewport sizes (previously hidden below 768px) -- Below 768px: icon-only buttons for Download, About, Help, and a logos dropdown -- Above 768px: unchanged desktop layout with text buttons and side-by-side logos - -### pango-dropdown — New features -- `align` prop (`'left' | 'right'`) for dropdown positioning -- `--pango-dropdown-trigger-padding` CSS variable for customizable trigger padding +All notable changes to this project will be documented in this file. +This changelog is automatically generated by [semantic-release](https://github.com/semantic-release/semantic-release). + +## Pre-release History + +### 0.0.3 + +- Align environment constants with site-react versioning +- Add GitHub Actions CI workflow + +### 0.0.2 + +- Fix images and packaging + +### 0.0.1 + +- Initial toolbar and dropdown components diff --git a/commitlint.config.js b/commitlint.config.js new file mode 100644 index 0000000..84dcb12 --- /dev/null +++ b/commitlint.config.js @@ -0,0 +1,3 @@ +module.exports = { + extends: ['@commitlint/config-conventional'], +}; diff --git a/package.json b/package.json index f292ae9..c08e5fa 100644 --- a/package.json +++ b/package.json @@ -25,13 +25,18 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/ionic-team/stencil-component-starter.git" + "url": "git+https://github.com/pantherdb/pango-toolbar.git" }, + "bugs": { + "url": "https://github.com/pantherdb/pango-toolbar/issues" + }, + "homepage": "https://github.com/pantherdb/pango-toolbar#readme", "files": [ "dist/", "loader/" ], "scripts": { + "prepublishOnly": "npm run build", "build": "stencil build", "start": "stencil build --dev --watch --serve", "test": "stencil test --spec --e2e", diff --git a/readme.md b/readme.md index 7c21b3d..e740af5 100644 --- a/readme.md +++ b/readme.md @@ -128,34 +128,121 @@ Override these in your project to match your vibe ## Development -1. Clone the repository +### Setup ```bash -git clone [repository-url] +git clone https://github.com/pantherdb/pango-toolbar.git +cd pango-toolbar +npm install ``` -2. Install dependencies +### Commands + +| Command | Description | +| --- | --- | +| `npm start` | Dev server with hot reload at localhost | +| `npm run build` | Production build | +| `npm test` | Run all tests (spec + e2e) | +| `npm run test.watch` | Run tests in watch mode | +| `npm run generate` | Scaffold a new component | + +### Running a single test file ```bash -npm install +npx stencil test --spec src/components/pango-dropdown/pango-dropdown.spec.ts +npx stencil test --e2e src/components/pango-dropdown/pango-dropdown.e2e.ts ``` -3. Start development server +## Contributing + +### Branch workflow + +1. Create a feature branch from `main` +2. Make your changes and write tests +3. Open a PR against `main` +4. CI runs build, tests, and commit lint automatically +5. Squash merge into `main` with a conventional commit message + +### Commit messages + +This project uses [Conventional Commits](https://www.conventionalcommits.org/) to automate versioning and changelog generation. All commits must follow this format: + +```text +(): +``` + +| Type | Version bump | When to use | +| --- | --- | --- | +| `feat` | Minor (0.1.0 -> 0.2.0) | New feature or capability | +| `fix` | Patch (0.1.0 -> 0.1.1) | Bug fix | +| `perf` | Patch | Performance improvement | +| `docs` | No release | Documentation only | +| `style` | No release | Formatting, whitespace | +| `refactor` | No release | Code change that doesn't fix a bug or add a feature | +| `test` | No release | Adding or updating tests | +| `chore` | No release | Build process, tooling, dependencies | +| `ci` | No release | CI/CD changes | + +**Examples:** ```bash -npm start +git commit -m "feat: add dark mode toggle to toolbar" +git commit -m "fix: dropdown not closing on outside click" +git commit -m "feat(dropdown): add maxHeight prop" +git commit -m "docs: update CSS variables table in README" ``` -4. Run tests +For breaking changes, add `BREAKING CHANGE:` in the commit body or `!` after the type: ```bash -npm test +git commit -m "feat!: rename headerTitle prop to title" ``` -5. Build for production +Commitlint runs on PRs to validate your messages. If it fails, amend your commit: ```bash -npm run build +git commit --amend -m "feat: correct message here" +git push --force-with-lease +``` + +### Releases + +Releases are **fully automated** via [semantic-release](https://github.com/semantic-release/semantic-release). When a PR is merged to `main`: + +1. Semantic-release analyzes the commit messages since the last tag +2. Determines the version bump (major/minor/patch) or skips if no releasable commits +3. Updates `package.json` version and `CHANGELOG.md` +4. Publishes to npm +5. Creates a GitHub release with auto-generated notes +6. Tags the release (e.g., `v0.2.0`) +7. Commits the version bump back to `main` with `[skip ci]` + +**You never need to manually bump versions, edit the changelog, or create tags.** + +### What triggers a release? + +| Commit type in `main` | Result | +| --- | --- | +| `feat:` | Minor version bump + release | +| `fix:`, `perf:` | Patch version bump + release | +| `docs:`, `chore:`, `ci:`, `test:`, `refactor:`, `style:` | No release | +| `feat!:` or `BREAKING CHANGE:` | Major version bump + release | + +### Project structure + +```text +src/ + components/ + pango-toolbar/ # Main toolbar component + pango-dropdown/ # Generic dropdown component + utils/ + environment.ts # Centralized external URLs + __mocks__/ # Jest mocks (image imports) +.github/ + workflows/ + ci.yml # Build + test on PRs + semantic-release.yml # Auto-release on push to main + commitlint.yml # Commit message validation on PRs ``` ## Browser Support @@ -168,4 +255,3 @@ npm run build ## License MIT -