Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "harmonia",
"name": "codbex",
"owner": {
"name": "codbex"
},
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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"
},
Expand Down
2 changes: 1 addition & 1 deletion .claude/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"enabledPlugins": {
"harmonia@harmonia": true
"harmonia@codbex": true
}
}
70 changes: 70 additions & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,7 @@ docs/public/theming/sections.json

# VitePress
**/.vitepress/cache

# Playwright
playwright-report
test-results
4 changes: 4 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
9 changes: 9 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`. `<body>` 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).
Expand Down
123 changes: 123 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

28 changes: 26 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Harmonia ships an agent-readable skill so coding agents (Hermes, Claude Code and

```
/plugin marketplace add codbex/harmonia
/plugin install harmonia@harmonia
/plugin install harmonia@codbex
```

**Manual (any agent that reads `.claude/skills`, `.hermes/skills`, etc.).**
Expand Down Expand Up @@ -83,6 +83,8 @@ The build writes to `dist/`: four JS bundles (`harmonia.js` / `.min.js` for the
| `npm test` | Run the test suite once (vitest). |
| `npm run test:watch` | Run the tests in watch mode. |
| `npm run test:coverage` | Run the tests with a coverage report. |
| `npm run test:e2e` | Run the Playwright end-to-end suite against the built `dist/` (build first). |
| `npm run test:e2e:ui` | Run the Playwright suite in the interactive UI mode. |
| `npm run lint` | Lint `src/` and `scripts/` with ESLint. |
| `npm run lint:fix` | Lint and auto-fix. |
| `npm run format` | Format the repo with Prettier. |
Expand All @@ -108,7 +110,7 @@ harmonia/
module.js ESM entry point (consumers register components manually)
icons/ The single source of truth for the icon registry
scripts/ Build and code-generation scripts (build.cjs, generate-icons.cjs, ...)
tests/ Vitest suite, mirroring the src/ layout
tests/ Vitest suite mirroring the src/ layout, plus the Playwright e2e suite in tests/e2e/
dist/ Build output (generated)
skills/ Agent skill (generated from docs/**)
docs/ VitePress documentation site
Expand Down Expand Up @@ -149,7 +151,9 @@ Full, self-contained example apps live under `docs/public/templates/`. They show

- **granite-erp** - a multi-page ERP-style admin app (`index.html` + `pages/` + `js/`).
- **onyx-chat** - a multi-page chat app (`index.html` + `pages/` + `js/`).
- **quartz-docs** - a multi-page documentation and blog site (`index.html` + `pages/` + `js/`).
- **slate** - a single-page dashboard.
- **ember** - a single-page, mobile-first habit tracker.

They are served by the docs site, so with `npm run docs:dev` running you can open them under the site's `/templates/` path (or open the HTML files directly in a browser).

Expand Down Expand Up @@ -204,6 +208,26 @@ npx vitest run -t "name substring" # a single test by name
npm run test:coverage # with coverage
```

### End-to-end tests (Playwright)

The [Playwright](https://playwright.dev/) suite in `tests/e2e/` covers what the unit tests cannot - real Alpine reactivity, real CSS and layout, floating-ui positioning, focus and keyboard traversal, CSS transitions, pointer drags and theme switching. It runs against the built `dist/` bundles, so build first.

```sh
# 1. Install the Playwright browsers (once)
npx playwright install

# 2. Build the library so dist/ is fresh
npm run build

# 3. Run the suite (Chromium)
npm run test:e2e

# Or with the interactive UI
npm run test:e2e:ui
```

Locally the suite runs on Chromium only. In CI (GitHub Actions) it also runs on Firefox and WebKit. You can try all three locally with `CI=1 npx playwright test`, but the WebKit tests will probably fail on an up-to-date system due to an upstream issue - Playwright's bundled WebKit build is not compatible with the newer system libraries many distributions ship. They should run fine in GitHub Actions (though not guaranteed).

## Contributing

Conventions live in [AGENTS.md](AGENTS.md). The split component has its own guide in [SPLIT.md](SPLIT.md). Formatting is enforced by ESLint (flat config) and Prettier, so run `npm run lint` and `npm run format:check` before committing. After editing anything under `src/**`, run `npm run build` so the generated `dist/`, CSS and skill stay in sync.
Expand Down
2 changes: 2 additions & 0 deletions docs/.vitepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export default defineConfig({
{ text: 'Carousel', link: '/components/carousel' },
{ text: 'Checkbox', link: '/components/checkbox' },
{ text: 'Chip', link: '/components/chip' },
{ text: 'Combobox', link: '/components/combobox' },
// Date & time components grouped together
{ text: 'Calendar', link: '/components/calendar' },
{ text: 'Calendar (Inline)', link: '/components/inline-calendar' },
Expand Down Expand Up @@ -130,6 +131,7 @@ export default defineConfig({
{ text: 'Date Format', link: '/utilities/date-format' },
{ text: 'Focus', link: '/utilities/focus' },
{ text: 'Include', link: '/utilities/include' },
{ text: 'Native Notifications', link: '/utilities/native-notifications' },
{ text: 'Responsive', link: '/utilities/responsive' },
{ text: 'Template', link: '/utilities/template' },
{ text: 'Theme', link: '/utilities/theme' },
Expand Down
3 changes: 2 additions & 1 deletion docs/.vitepress/theme/TemplateShowcase.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ defineProps({
title: { type: String, required: true },
src: { type: String, required: true },
desc: { type: String, default: '' },
mobile: { type: Boolean, default: false },
});

const tab = ref('preview');
Expand Down Expand Up @@ -63,7 +64,7 @@ onMounted(() => {
</div>
</header>

<div v-show="tab === 'preview'">
<div v-show="tab === 'preview'" :class="{ 'template-mobile': mobile }">
<iframe class="template-frame" :src="src" :title="`${title} template`" loading="lazy"></iframe>
</div>

Expand Down
10 changes: 10 additions & 0 deletions docs/.vitepress/theme/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,16 @@ h6:has(+ component-container) {
gap: 2rem;
}

.template-card > .template-mobile {
display: flex;
flex-direction: column;
align-items: center;
}

.template-mobile > iframe {
width: 377px;
}

.template-card {
overflow: hidden;
}
Expand Down
2 changes: 1 addition & 1 deletion docs/agent-skill.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Install the plugin so the skill is discovered automatically:

```
/plugin marketplace add codbex/harmonia
/plugin install harmonia@harmonia
/plugin install harmonia@codbex
```

### Manual (any agent that reads `.claude/skills`, `.hermes/skills`, etc.)
Expand Down
14 changes: 14 additions & 0 deletions docs/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ outline: deep
<p class="text">Visual representation of a user or entity, displayed as an image, icon, or fallback initials.</p>
</div>
</a>
<a href="./components/backdrop" class="card">
<svg-icon class="card-media" src="./images/backdrop.svg"></svg-icon>
<div class="card-content">
<h3>Backdrop</h3>
<p class="text">Full-screen scrim that dims the page and animates transient surfaces like command palettes and custom modals in and out.</p>
</div>
</a>
<a href="./components/badge" class="card">
<svg-icon class="card-media" src="./images/badge.svg"></svg-icon>
<div class="card-content">
Expand Down Expand Up @@ -98,6 +105,13 @@ outline: deep
<p class="text">Compact, interactive element representing a filter or selection, with an optional dismiss button.</p>
</div>
</a>
<a href="./components/combobox" class="card">
<svg-icon class="card-media" src="./images/combobox.svg"></svg-icon>
<div class="card-content">
<h3>Combobox</h3>
<p class="text">A list of options belonging to a text field, filtered as the user types.</p>
</div>
</a>
<a href="./components/calendar" class="card">
<svg-icon class="card-media" src="./images/calendar.svg"></svg-icon>
<div class="card-content">
Expand Down
46 changes: 37 additions & 9 deletions docs/components/accordion.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ x-h-accordion-content

#### x-h-accordion-item

| Attribute | Type | Required | Description |
| --------- | ------ | -------- | ----------------------------------------------------------------------- |
| `self` | string | false | Sets the ID of the item. Useful when setting the default expanded item. |
| Attribute | Type | Required | Description |
| --------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `self` | string | false | Sets the ID of the item. Expects a string literal or a reference to a variable. Useful when setting the default expanded item. |

#### x-h-accordion-trigger

Expand All @@ -41,9 +41,9 @@ x-h-accordion-content

#### x-h-accordion

| Modifier | Type | Required | Description |
| -------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| single | string | false | Used when the accordion must show only one section at a time. Optionally, the id of the item that should be expanded by default can be set. |
| Modifier | Type | Required | Description |
| -------- | ------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| single | string | false | Used when the accordion must show only one section at a time. Optionally, the id of the item that should be expanded by default can be set. Expects a string literal or a reference to a variable. |

#### x-h-accordion-item

Expand All @@ -58,15 +58,15 @@ x-h-accordion-content
<LiveExample>

```html
<div x-h-accordion.single="itemId2">
<div x-h-accordion-item="itemId1">
<div x-h-accordion.single="'itemId2'">
<div x-h-accordion-item="'itemId1'">
<h3 x-h-accordion-trigger="'Accordion Item 1'"></h3>
<div x-h-accordion-content>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
</div>
</div>
<div x-h-accordion-item="itemId2">
<div x-h-accordion-item="'itemId2'">
<h3 x-h-accordion-trigger="'Accordion Item 2'"></h3>
<div x-h-accordion-content>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Expand All @@ -78,6 +78,34 @@ x-h-accordion-content

</LiveExample>

### Dynamic items

Because the item id is evaluated, items rendered with `x-for` can take their id from the iterated data, which keeps single mode working.

<LiveExample>

```html
<div
x-data="{
faq: [
{ id: 'faq-1', question: 'What is Harmonia?', answer: 'A UI component library for Alpine.js, built with Tailwind CSS.' },
{ id: 'faq-2', question: 'How do I install it?', answer: 'Add the harmonia.js script and the harmonia.css stylesheet to your page, after Alpine.js.' },
{ id: 'faq-3', question: 'Does it support dark mode?', answer: 'Yes, every component adapts to light and dark mode automatically.' }
]
}"
x-h-accordion.single
>
<template x-for="entry in faq" :key="entry.id">
<div x-h-accordion-item="entry.id">
<h3 x-h-accordion-trigger="entry.question"></h3>
<div x-h-accordion-content x-text="entry.answer"></div>
</div>
</template>
</div>
```

</LiveExample>

### Default section

<LiveExample data-exclude="generator">
Expand Down
12 changes: 10 additions & 2 deletions docs/components/avatar.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ Represents a person, entity, or object using an image, icon, or text, such as a

Use avatars to visually identify users or related entities in lists, profiles, or collaborative features. Choose the appropriate variant based on available data - images for personal recognition, initials or icons as fallbacks.

## Behavior

An avatar is a control only when it is written as a `button` or an `a` element, so the element itself carries the role, the tab stop and the keyboard behavior. On any other tag it stays a plain avatar and is never given a role or a `tabindex` it cannot honor.

## API Reference

### Component attribute(s)
Expand Down Expand Up @@ -107,6 +111,8 @@ You can change the avatar shape by using the `rounded-` classes.

### Variants

Inside an active [sidebar menu button or menu nav](/components/sidebar), a variant avatar switches to the button's own foreground color so it stays legible.

<LiveExample data-class="grid grid-cols-5 justify-items-center gap-4">

```html
Expand Down Expand Up @@ -168,12 +174,14 @@ Use `data-color` to fill the avatar with one of Harmonia's standard palette colo

### Interactive

To make an avatar interactive, use the `button` HTML element instead of a `span`.
To make an avatar interactive, write it as a `button` or an `a` element instead of a `div`. Use a `button` when the avatar acts on the page, and an `a` with an `href` when it leads somewhere.

<LiveExample data-class="flex flex-col items-center">
<LiveExample data-class="flex items-center justify-center gap-4">

```html
<button x-h-avatar>HM</button>

<a x-h-avatar href="#interactive">HM</a>
```

</LiveExample>
Loading
Loading