diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 4d66fe6..a524bab 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -1,5 +1,5 @@ { - "name": "harmonia", + "name": "codbex", "owner": { "name": "codbex" }, diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index b87eece..4e6635c 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "harmonia", "description": "Usage docs for the Harmonia Alpine.js UI component library (@codbex/harmonia): how to add, wire, and style x-h-* components.", - "version": "2.14.2", + "version": "3.0.0", "author": { "name": "codbex" }, diff --git a/.claude/settings.json b/.claude/settings.json index b9907ca..6fefa4a 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -1,5 +1,5 @@ { "enabledPlugins": { - "harmonia@harmonia": true + "harmonia@codbex": true } } diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 0000000..d7e39c8 --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,70 @@ +name: Tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +concurrency: + group: tests-${{ github.ref }} + cancel-in-progress: true + +jobs: + unit: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: '22.x' + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Unit tests + run: npm run test + + e2e: + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: '22.x' + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Build dist + run: node scripts/build.cjs && npm run tailwind + + - name: Install Playwright browsers + run: npx playwright install --with-deps + + - name: E2E tests + run: npx playwright test + + - name: Upload report on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: | + playwright-report + test-results + retention-days: 7 diff --git a/.gitignore b/.gitignore index 3401ee7..51e6a74 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,7 @@ docs/public/theming/sections.json # VitePress **/.vitepress/cache + +# Playwright +playwright-report +test-results diff --git a/.prettierignore b/.prettierignore index c1f0878..c3dd653 100644 --- a/.prettierignore +++ b/.prettierignore @@ -10,3 +10,7 @@ src/common/icon-data.js # Generated from docs/** by scripts/generate-agent-docs.cjs skills/ + +# Playwright output +playwright-report +test-results diff --git a/AGENTS.md b/AGENTS.md index 58a76e1..9385e35 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -89,6 +89,15 @@ Strong success criteria let you loop independently. Weak criteria ("make it work - **happy-dom** environment; `tests/setup.js` patches `innerText`. - Directives are tested **without real Alpine**. `tests/test-utils.js` provides `mountDirective(plugin, 'h-name', el, bindings, ctxOverrides)` plus `createMockAlpine` / `createMockContext`, a minimal Proxy-based `reactive`/`effect` implementation. Use these helpers and assert on the DOM, attributes, and `el._h_*` state the directive produces. Mirror the `src/` layout under `tests/` (`tests/components`, `tests/common`, `tests/utils`). +### End-to-end tests (Playwright) + +- `npm run test:e2e` runs `tests/e2e/**/*.spec.js` (Chromium locally; CI adds Firefox and WebKit). `npm run test:e2e:ui` opens the Playwright UI. +- Build first: `node scripts/build.cjs && npm run tailwind`. The suite tests `dist/harmonia.js` and `dist/harmonia.css`, not `src/`. The fixture server (`scripts/e2e-server.cjs`) exits with instructions when `dist/` is missing. +- Write an e2e test only for what the unit suite cannot see: real Alpine init and `x-model`, real CSS and layout, floating-ui positioning, focus and Tab order, CSS transitions, pointer drag, theme switching. Everything else stays a vitest unit test. +- Fixtures are plain HTML files in `tests/e2e/fixtures/`, served over http from the repo root (never `file://` - the bundle reads localStorage at eval time and an opaque origin aborts it). Load `/dist/harmonia.css`, `/dist/harmonia.js`, then `/node_modules/alpinejs/dist/cdn.min.js` with `defer`. `
` must carry `x-data` or Alpine initializes nothing, silently. +- Use `tests/e2e/helpers.js`: `gotoFixture` waits for Alpine and a rendered directive; `settle(page)` waits out the 100-200ms transitions before reading styles or visibility; seed a theme with `seedColorScheme` before navigation; `drag` for real pointer drags. +- Popover-like content is attached but hidden until opened, so locate it with `state: 'attached'`. Use `page.clock.setFixedTime()` for date components, never `clock.install()` (it freezes the setTimeout-driven transitions). Assert on `data-slot`, `data-state`, `aria-*` and settled computed styles; `el.matches(':hover')` is false under automation even when hover styles apply. No screenshot baselines. + ## Conventions - ESLint flat config (`eslint.config.js`): browser globals + `Alpine` readonly for `src`; unused vars allowed only with a `_` prefix; `console` limited to `warn`/`error`. Prettier enforces formatting (with organize-imports + tailwindcss plugins). diff --git a/CHANGELOG.md b/CHANGELOG.md index b15066f..499dbfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,128 @@ # Changelog +## v3.0.0 + +A release that adds the Combobox component and brings drag and drop to the Calendar and the Slot Picker. Calendar events can be rescheduled by dragging them to another time or day, and slot picker slots can be reordered within a day or moved to another day, in both cases with the change proposed through an event and applied by the consumer. Notifications can now play a sound, a built-in chime or an audio file, per notification or as an overlay-wide default, and the new Native Notifications utility, also available as `$notifications.native`, wraps the browser's Notification API for messages shown by the operating system. The Dialog gains a fullscreen mode that fills the viewport, together with a content slot that scrolls the body while the header and the footer stay in place, and moves its padding onto that slot and its neighbours, which is a breaking change for dialog bodies that are not wrapped in the new slot. It also keeps focus inside itself while it is open, like the Backdrop now does. The Card moves its padding the same way, onto its header, content and footer, which is a breaking change for content placed straight in a card, and gains a modifier that lets a table or a list span the card from edge to edge. The Button Group can now hold a single choice, which turns a row of buttons into a segmented control that is announced as a set of options and navigated with the arrow keys. It also draws the dividers between its buttons itself rather than relying on the `outline` variant's border, which retires `x-h-button-group-separator` as a breaking change, gains a borderless mode for a group that fills a card, and no longer overwrites a `role` set on the group. It also completes the viewport height utilities, repairs keyboard navigation in a listbox whose options change, and stops a long line of code from escaping its block. The Sidebar now separates navigation from everything else it can hold, a breaking change for navigation sidebars. A destination is written as `x-h-sidebar-menu-nav`, which announces the active entry with `aria-current="page"`, while `x-h-sidebar-menu-button` makes no navigation claim and leaves `aria-current` to the author. The Chip becomes a container holding its own buttons, a breaking change that retires the button it used to be applied to: a dismissible chip was a button with another control inside it, which is invalid markup and left its close button reachable by `Tab` but impossible to activate. The Tabs take the same shape as a breaking change for every tab. Each tab now lives in an `x-h-tab-item` wrapper that draws its surface, and a tab action is a real button beside the tab instead of a span inside it, which also lifts the one-action limit, so a tab can pair a close button with a menu trigger. The Avatar becomes a control on an `a` element as well as on a `button`, and a button avatar no longer submits the form around it. The Backdrop now keeps focus inside itself while it is open, and the Expansion Panel's generated triggers no longer share a single id. The release fixes single mode in the Accordion and makes accordion item ids dynamic: items written without an explicit id all shared the same empty id, so an accordion in single mode never collapsed the previously open section. The item id is now evaluated as an Alpine expression, which is a breaking change for hard-coded ids but lets items rendered with `x-for` take their id from the iterated data. The Range's `input` and `change` events now carry their value in `event.detail.value` rather than as the whole detail, a breaking change that aligns them with every other component's change event. And components that hold their bound value themselves, among them the Rating, the single choice Button Group, the Inline Calendar and the Menu's checkbox and radio items, now reject `x-model`'s event modifiers with a console error, since `.lazy` used to silently corrupt the bound value. And the date grid shared by the Date Picker, the Datetime Picker, the Inline Calendar and the Slot Picker now starts keyboard navigation from the visibly focused day instead of the 1st of the month. + +### Combobox + +- **New component.** A list of options belonging to a text field, filtered as the user types. It is the pattern behind a search field, an autocomplete and a command palette. + +### Calendar + +- **New: events can be rescheduled by drag and drop.** Opt in to let users drag a timed event to a new time or day in the week and day views (the start time snaps to a configurable minute step and the duration is kept), move all-day pills between days, and change an event's day in the month view. Dropping never changes the calendar's data directly: the proposed change is dispatched as an event for the consumer to apply. Individual events can opt out, and every event stays reachable by keyboard, since dragging is a pointer-only convenience. +- **Fixed: `x-model.lazy` corrupted the bound value of an inline calendar.** The modifier makes Alpine listen for `change`, and the calendar's own `change` event made that listener write the event's detail object over the date string the calendar had just stored. The modifier also silently broke the model-to-view sync, since the model expression was read from the literal `x-model` attribute name. The event modifiers (`.lazy`, `.change`, `.blur`, `.enter`) are now rejected with a console error, the model always updates immediately, and a model bound with any other modifier stays in sync. + +### Slot Picker + +- **New: slots can be reordered and moved between days by drag and drop.** Opt in to let users drag a slot within its day to reorder it or onto another visible day to move it. While dragging, a half-transparent copy of the slot follows the pointer and the other slots part to show where it will land. Dropping never changes the picker's data directly: the proposed change is dispatched as an event for the consumer to apply. Individual slots can opt out, and unavailable slots never drag. + +### Notifications + +- **New: a notification can play a sound.** The `sound` argument on `$notifications.add` plays a built-in chime, a custom audio file or nothing, and the overlay's new `data-sound` attribute sets the default for every notification. +- **New: native notifications.** New utility functions, also exposed as `$notifications.native`, wrap the browser's Notification API with an availability check, permission inspection and request, and a show function that passes the standard options through. + +### Dialog + +- **New: fullscreen mode.** A dialog can now fill the entire viewport instead of sitting centered at a capped width, which suits long forms and multi-step tasks, especially on small screens. It is switched on with `data-fullscreen` and can be bound to an expression, so the same dialog can change modes at runtime. The new `x-h-dialog-content` slot marks the body of a dialog as the only scrolling part, keeping the header and the footer in place while the content scrolls between them. +- **New: focus stays inside an open dialog and returns to the opener when it closes.** `Tab` and `Shift+Tab` used to walk straight out of the dialog into the page behind it, which is invisible to the eye but fully reachable by keyboard, and closing dropped focus at the top of the document instead of returning it to the control that opened it. Where focus lands when a dialog opens is unchanged, and dismissal, including `Esc`, is still wired up by the consumer. +- **Breaking: the dialog surface no longer has padding of its own.** The header, the body and the footer now pad themselves, so a body that is not wrapped in `x-h-dialog-content` reaches the edges of the dialog. To migrate, wrap it: `