@viax.io/uxm 4.40.2 → 4.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +89 -20
- package/dist/studio/index.cjs.map +1 -1
- package/dist/studio/index.d.ts +3 -2
- package/dist/studio/index.d.ts.map +1 -1
- package/dist/studio/index.js.map +1 -1
- package/dist/studio/lib/registry/composite/list-item.cjs +22 -5
- package/dist/studio/lib/registry/composite/list-item.cjs.map +1 -1
- package/dist/studio/lib/registry/composite/list-item.d.ts.map +1 -1
- package/dist/studio/lib/registry/composite/list-item.js +22 -5
- package/dist/studio/lib/registry/composite/list-item.js.map +1 -1
- package/dist/ui/list/list-item-preview.cjs +9 -4
- package/dist/ui/list/list-item-preview.cjs.map +1 -1
- package/dist/ui/list/list-item-preview.d.ts.map +1 -1
- package/dist/ui/list/list-item-preview.js +10 -5
- package/dist/ui/list/list-item-preview.js.map +1 -1
- package/dist/ui/list/list.cjs +9 -1
- package/dist/ui/list/list.cjs.map +1 -1
- package/dist/ui/list/list.css +39 -0
- package/dist/ui/list/list.d.ts +32 -1
- package/dist/ui/list/list.d.ts.map +1 -1
- package/dist/ui/list/list.js +8 -1
- package/dist/ui/list/list.js.map +1 -1
- package/package.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [4.42.0](https://github.com/viax-io/uxm/compare/v4.41.0...v4.42.0) (2026-09-15)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **cdn:** add build:cdn pipeline for self-contained script-tag bundles ([77a48be](https://github.com/viax-io/uxm/commit/77a48bedddb4b2a6c25f2d8f65ff6d7a67a29eab))
|
|
7
|
+
|
|
8
|
+
# [4.41.0](https://github.com/viax-io/uxm/compare/v4.40.2...v4.41.0) (2026-09-11)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **list:** add a media leading slot to ListItem ([f346cd4](https://github.com/viax-io/uxm/commit/f346cd43acf89df9c194d733ffe0b218cfdb1f51))
|
|
14
|
+
|
|
1
15
|
## [4.40.2](https://github.com/viax-io/uxm/compare/v4.40.1...v4.40.2) (2026-09-09)
|
|
2
16
|
|
|
3
17
|
|
package/README.md
CHANGED
|
@@ -11,19 +11,44 @@ The package ships **no `"use client"` / `"use server"` directives** by design: c
|
|
|
11
11
|
- **UI primitives** (`@viax.io/uxm/ui`) — BEM-classed React 19 components.
|
|
12
12
|
- **Icon registry** (`@viax.io/uxm/ui`) — `ICONS`, `ICON_OPTIONS`, `getIcon`, `IconDef` for tooling that enumerates the bundled icon set.
|
|
13
13
|
- **Design tokens** (`@viax.io/uxm/tokens`) — the canonical `themeTokens` array plus helpers (`findToken`, `resolveHex`, `isTokenValue`) and the `ThemeToken` type.
|
|
14
|
+
- **Behaviour hooks** (`@viax.io/uxm/hooks`) — `useDismiss`, `useFocusTrap`, `useFocusOnMount`, `useRovingTabIndex`, `useScrollLock`, `usePortal`, `useToastStore` for hosts composing their own floating layers or keyboard widgets.
|
|
14
15
|
- **Themable previews** (`@viax.io/uxm/previews`) — preview components (one per atom, plus composite previews) used by host shells like MODO's brand-settings editor to render live, knob-driven theme exploration.
|
|
16
|
+
- **Studio** (`@viax.io/uxm/studio`) — `UxmApp`, the full design workbench MODO serves at `/uxm`, backend-decoupled through the `StudioPersistence` contract; see [Studio](#studio).
|
|
15
17
|
- **WCAG / contrast helpers** (`@viax.io/uxm`) — `contrastRatio`, `parseColor`, `rgbToHex`, `suggestAccessibleColor`, `suggestAccessibleToken`, `wcagLevel`, plus `RGB` and `TokenCandidate` types.
|
|
16
18
|
- **Default stylesheets** — `@viax.io/uxm/ui.css` (component primitive defaults) and `@viax.io/uxm/tokens.css` (token declarations).
|
|
17
19
|
|
|
18
20
|
## Install
|
|
19
21
|
|
|
20
22
|
```bash
|
|
21
|
-
npm
|
|
23
|
+
npm i @viax.io/uxm
|
|
22
24
|
```
|
|
23
25
|
|
|
26
|
+
Every example in this README imports from `@viax.io/uxm` and its subpaths.
|
|
27
|
+
|
|
28
|
+
> **Tip: install under an alias.** viax apps install the package as `uxm` so the real name
|
|
29
|
+
> appears once, in `package.json`, and imports stay short and rename-proof:
|
|
30
|
+
>
|
|
31
|
+
> ```bash
|
|
32
|
+
> npm i uxm@npm:@viax.io/uxm@latest
|
|
33
|
+
> ```
|
|
34
|
+
>
|
|
35
|
+
> That writes `"uxm": "npm:@viax.io/uxm@^4.40.1"` and lets you write `import { Button } from 'uxm/ui'`
|
|
36
|
+
> or `import 'uxm/tokens.css'`. The subpath exports are identical under either name — read `@viax.io/uxm`
|
|
37
|
+
> as `uxm` throughout.
|
|
38
|
+
|
|
24
39
|
Peer deps: `react@^19`, `react-dom@^19`. Node `>=20` for local dev.
|
|
25
40
|
|
|
26
|
-
Published publicly on npm — no registry configuration, credentials or VPN required
|
|
41
|
+
Published publicly on npm — no registry configuration, credentials or VPN required for a plain
|
|
42
|
+
`npm i @viax.io/uxm` against the default npm registry.
|
|
43
|
+
|
|
44
|
+
> **If your project's default registry points at Nexus** (`registry=https://nexus.viax.tech/repository/viax-npm/`
|
|
45
|
+
> in `.npmrc` — the common viax-internal setup), add a scope override so this package still
|
|
46
|
+
> resolves straight from npm:
|
|
47
|
+
> ```
|
|
48
|
+
> @viax.io:registry=https://registry.npmjs.org/
|
|
49
|
+
> ```
|
|
50
|
+
> Nexus mirrors `registry.npmjs.org` with a delay, so installing right after a fresh release can
|
|
51
|
+
> 404 or silently resolve a stale version through Nexus without this override.
|
|
27
52
|
|
|
28
53
|
## Quick start
|
|
29
54
|
|
|
@@ -75,6 +100,9 @@ import { ButtonPrimary, themeTokens } from '@viax.io/uxm';
|
|
|
75
100
|
| `@viax.io/uxm/tokens.css` | `--color-*` declarations on `:root`. |
|
|
76
101
|
| `@viax.io/uxm/hooks` | The behaviour hooks the atoms are built on — `useDismiss`, `useFocusTrap`, `useFocusOnMount`, `useRovingTabIndex`, `useScrollLock`, `usePortal`, `useToastStore` — for hosts composing their own floating layers or keyboard widgets. Pure React, no atom imports. |
|
|
77
102
|
| `@viax.io/uxm/previews` | Preview components for host shells building theme editors. **No preview symbol leaks into `/ui`** — see the tree-shake guarantee below. |
|
|
103
|
+
| `@viax.io/uxm/studio` | `UxmApp` + `UxmProvider` / `useUxm`, the component `registry`, the `StudioPersistence` contract and its three adapters (`createHttpPersistence`, `createClientPersistence`, `createReadOnlyPersistence`). |
|
|
104
|
+
| `@viax.io/uxm/studio/generate-css` | `generateOverridesCss` + the CSS sanitizers — turn saved studio overrides and a `BrandConfig` into a stylesheet on the server, without pulling in the workbench UI. |
|
|
105
|
+
| `@viax.io/uxm/studio.css` | Tailwind utilities for the studio shell + token declarations. Does **not** bundle the atom CSS — a studio host imports `ui.css` alongside it. |
|
|
78
106
|
|
|
79
107
|
## Component catalog
|
|
80
108
|
|
|
@@ -83,13 +111,13 @@ Every component folder ships a `README.md` documenting props, CSS variables, MOD
|
|
|
83
111
|
> The catalog below covers every folder in `src/ui/`. If you add a component, add it here too — and keep `src/ui/` the authoritative source if the two ever drift.
|
|
84
112
|
|
|
85
113
|
### Forms & inputs
|
|
86
|
-
[`calendar`](src/ui/calendar/README.md) · [`checkbox`](src/ui/checkbox/README.md) · [`color-input`](src/ui/color-input/README.md) (ColorInput + ColorInputPopover) · [`currency-input`](src/ui/currency-input/README.md) · [`date-input`](src/ui/date-input/README.md) · [`editable-cell`](src/ui/editable-cell/README.md) · [`field-error`](src/ui/field-error/README.md) · [`file-upload`](src/ui/file-upload/README.md) · [`form-field`](src/ui/form-field/README.md) · [`input`](src/ui/input/README.md) (TextInput + Select + Textarea) · [`input-with-icon`](src/ui/input-with-icon/README.md) · [`number-input`](src/ui/number-input/README.md) · [`number-stepper`](src/ui/number-stepper/README.md) · [`password-input`](src/ui/password-input/README.md) · [`phone-input`](src/ui/phone-input/README.md) · [`pill-select`](src/ui/pill-select/README.md) · [`radio-group`](src/ui/radio-group/README.md) · [`range-slider`](src/ui/range-slider/README.md) · [`search-dropdown`](src/ui/search-dropdown/README.md) · [`slider`](src/ui/slider/README.md) · [`time-input`](src/ui/time-input/README.md) · [`toggle-switch`](src/ui/toggle-switch/README.md)
|
|
114
|
+
[`calendar`](src/ui/calendar/README.md) · [`checkbox`](src/ui/checkbox/README.md) · [`code-editor`](src/ui/code-editor/README.md) (CodeEditor + CodeBlock) · [`color-input`](src/ui/color-input/README.md) (ColorInput + ColorInputPopover) · [`currency-input`](src/ui/currency-input/README.md) · [`date-input`](src/ui/date-input/README.md) · [`editable-cell`](src/ui/editable-cell/README.md) · [`field-error`](src/ui/field-error/README.md) · [`file-upload`](src/ui/file-upload/README.md) · [`form-field`](src/ui/form-field/README.md) · [`input`](src/ui/input/README.md) (TextInput + Select + Textarea) · [`input-with-icon`](src/ui/input-with-icon/README.md) · [`number-input`](src/ui/number-input/README.md) · [`number-stepper`](src/ui/number-stepper/README.md) · [`password-input`](src/ui/password-input/README.md) · [`phone-input`](src/ui/phone-input/README.md) · [`pill-select`](src/ui/pill-select/README.md) · [`radio-group`](src/ui/radio-group/README.md) · [`range-slider`](src/ui/range-slider/README.md) · [`search-dropdown`](src/ui/search-dropdown/README.md) · [`slider`](src/ui/slider/README.md) · [`time-input`](src/ui/time-input/README.md) · [`toggle-switch`](src/ui/toggle-switch/README.md)
|
|
87
115
|
|
|
88
116
|
### Buttons & actions
|
|
89
117
|
[`back-link`](src/ui/back-link/README.md) · [`bulk-action-bar`](src/ui/bulk-action-bar/README.md) · [`button`](src/ui/button/README.md) (Primary/Secondary/Tertiary/Ghost) · [`button-group`](src/ui/button-group/README.md) · [`button-icon`](src/ui/button-icon/README.md) (deprecated → `icon-button` `variant="filled"`) · [`button-with-icon`](src/ui/button-with-icon/README.md) · [`icon-button`](src/ui/icon-button/README.md) · [`inline-action`](src/ui/inline-action/README.md) · [`link`](src/ui/link/README.md)
|
|
90
118
|
|
|
91
119
|
### Navigation
|
|
92
|
-
[`app-sidebar`](src/ui/app-sidebar/README.md) · [`app-top-bar`](src/ui/app-top-bar/README.md) · [`breadcrumb`](src/ui/breadcrumb/README.md) · [`filter-tabs`](src/ui/filter-tabs/README.md) · [`menu`](src/ui/menu/README.md) · [`sidebar-nav-item`](src/ui/sidebar-nav-item/README.md) · [`tabs`](src/ui/tabs/README.md) · [`tabs-underline`](src/ui/tabs-underline/README.md) · [`view-switcher`](src/ui/view-switcher/README.md)
|
|
120
|
+
[`app-sidebar`](src/ui/app-sidebar/README.md) · [`app-top-bar`](src/ui/app-top-bar/README.md) · [`breadcrumb`](src/ui/breadcrumb/README.md) · [`filter-tabs`](src/ui/filter-tabs/README.md) · [`menu`](src/ui/menu/README.md) · [`sidebar-nav-item`](src/ui/sidebar-nav-item/README.md) · [`sidebar-nav-trigger`](src/ui/sidebar-nav-trigger/README.md) · [`tabs`](src/ui/tabs/README.md) · [`tabs-underline`](src/ui/tabs-underline/README.md) · [`view-switcher`](src/ui/view-switcher/README.md)
|
|
93
121
|
|
|
94
122
|
### Overlays & floating layers
|
|
95
123
|
[`dialog`](src/ui/dialog/README.md) · [`hover-tooltip`](src/ui/hover-tooltip/README.md) · [`listbox`](src/ui/listbox/README.md) (Listbox + MultiListbox) · [`modal`](src/ui/modal/README.md) · [`option-list`](src/ui/option-list/README.md) · [`popover`](src/ui/popover/README.md) · [`toast`](src/ui/toast/README.md) (Toast + Toaster + the `toast.*` API)
|
|
@@ -125,6 +153,9 @@ The canonical token catalogue lives in `src/tokens/index.ts` as the `themeTokens
|
|
|
125
153
|
- `cssVar` — the `--color-*` identifier consumed by components
|
|
126
154
|
- `hex` / `darkHex` — light/dark defaults
|
|
127
155
|
- `group` — one of `surfaces` · `text` · `borders` · `accent` · `highlights` · `categories` · `semantic`
|
|
156
|
+
- `identity` (optional) — promotes the token into the brand editor's Identity section as a top-level knob; reserved for tokens that fan out (the accent base drives the whole ramp)
|
|
157
|
+
|
|
158
|
+
`tokens.css` and `themeTokens` must stay in lockstep — `npm run check:drift` fails when a `--color-*` variable exists on one side only.
|
|
128
159
|
|
|
129
160
|
```ts
|
|
130
161
|
import { themeTokens, findToken } from '@viax.io/uxm/tokens';
|
|
@@ -195,47 +226,79 @@ import { ButtonPreview, type PreviewShellContext } from '@viax.io/uxm/previews';
|
|
|
195
226
|
/>;
|
|
196
227
|
```
|
|
197
228
|
|
|
198
|
-
**Tree-shake guarantee**: no preview symbols leak into `@viax.io/uxm/ui`, so consumers that only import primitives never pay for preview code.
|
|
229
|
+
**Tree-shake guarantee**: no preview symbols leak into `@viax.io/uxm/ui`, so consumers that only import primitives never pay for preview code. **ESLint enforces it** (`eslint.config.mjs`): `no-restricted-imports` rejects any `*-preview` re-export from `src/ui/index.ts` or a `src/ui/*/index.ts`, and the `import/no-restricted-paths` zones pin the layer direction — `ui` may not import `studio` or the previews barrel, `tokens` imports nothing, and `lib` / `hooks` / `helpers` may not import components. After a build, `grep "Preview" dist/ui/index.js` still returns nothing.
|
|
230
|
+
|
|
231
|
+
## Studio
|
|
232
|
+
|
|
233
|
+
`@viax.io/uxm/studio` ships `UxmApp`, the design workbench MODO serves at `/uxm`: sidebar, canvas, properties panel and WCAG panel over the same previews. It is decoupled from any backend through the `StudioPersistence` contract — pick an adapter per host:
|
|
234
|
+
|
|
235
|
+
- `createHttpPersistence()` — reads and writes against a Hono API.
|
|
236
|
+
- `createClientPersistence()` — live preview plus client-side asset uploads (what the `portal/` dev shell uses).
|
|
237
|
+
- `createReadOnlyPersistence()` — a static, view-only portal.
|
|
238
|
+
|
|
239
|
+
```tsx
|
|
240
|
+
import '@viax.io/uxm/tokens.css';
|
|
241
|
+
import '@viax.io/uxm/ui.css';
|
|
242
|
+
import '@viax.io/uxm/studio.css';
|
|
243
|
+
import { UxmApp, createHttpPersistence } from '@viax.io/uxm/studio';
|
|
244
|
+
|
|
245
|
+
<UxmApp persistence={createHttpPersistence('/api/uxm')} />;
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
`@viax.io/uxm/studio/generate-css` exposes `generateOverridesCss` and the CSS sanitizers on their own, so a server can render the saved overrides into a stylesheet without loading the workbench. The studio is the only layer styled with Tailwind; `studio.css` bundles those utilities and the token declarations but not the atom CSS.
|
|
199
249
|
|
|
200
250
|
## Architecture
|
|
201
251
|
|
|
202
252
|
```
|
|
203
253
|
┌─────────────────────────────────────────────────┐
|
|
204
|
-
│ @viax.io/uxm/
|
|
254
|
+
│ @viax.io/uxm/studio (+ studio.css) │ ← the design workbench (MODO /uxm)
|
|
255
|
+
│ UxmApp, registry, StudioPersistence adapters │
|
|
256
|
+
└────────────────┬────────────────────────────────┘
|
|
257
|
+
│ renders
|
|
258
|
+
▼
|
|
259
|
+
┌─────────────────────────────────────────────────┐
|
|
260
|
+
│ @viax.io/uxm/previews │ ← host editors (MODO brand-settings)
|
|
205
261
|
│ themable, shell-aware preview components │
|
|
206
262
|
└────────────────┬────────────────────────────────┘
|
|
207
263
|
│ consumes
|
|
208
264
|
▼
|
|
209
265
|
┌─────────────────────────────────────────────────┐
|
|
210
|
-
│ @viax.io/uxm/ui
|
|
266
|
+
│ @viax.io/uxm/ui (+ ui.css) │ ← application code
|
|
211
267
|
│ BEM-classed React components │
|
|
212
268
|
│ + Icon registry (ICONS, getIcon, …) │
|
|
269
|
+
│ built on @viax.io/uxm/hooks │
|
|
213
270
|
└────────────────┬────────────────────────────────┘
|
|
214
271
|
│ reads fallbacks
|
|
215
272
|
▼
|
|
216
273
|
┌─────────────────────────────────────────────────┐
|
|
217
|
-
│ @viax.io/uxm/tokens (+ tokens.css)
|
|
274
|
+
│ @viax.io/uxm/tokens (+ tokens.css) │ ← MODO brand-settings edits this
|
|
218
275
|
│ themeTokens array + --color-* declarations │
|
|
219
276
|
└─────────────────────────────────────────────────┘
|
|
220
277
|
```
|
|
221
278
|
|
|
222
|
-
Every layer is independently importable; every layer has its own type declarations and own CSS bundle.
|
|
279
|
+
The dependency direction is `studio / previews → ui → tokens`, enforced by the ESLint layer zones. Every layer is independently importable; every layer has its own type declarations and own CSS bundle.
|
|
223
280
|
|
|
224
281
|
## Build & develop
|
|
225
282
|
|
|
226
283
|
```bash
|
|
227
284
|
npm install
|
|
228
|
-
npm run dev:modo
|
|
229
|
-
npm run build
|
|
230
|
-
npm run
|
|
231
|
-
npm run
|
|
232
|
-
npm run lint
|
|
233
|
-
npm run
|
|
285
|
+
npm run dev:modo # Vite dev server for the studio portal — fastest loop, no build needed
|
|
286
|
+
npm run build:modo # Vite production build of the portal
|
|
287
|
+
npm run build # full dist/ (see the pipeline below)
|
|
288
|
+
npm run typecheck # tsc --noEmit for src + portal
|
|
289
|
+
npm run lint # eslint .
|
|
290
|
+
npm run lint:fix # eslint . --fix
|
|
291
|
+
npm run check:drift # state-var drift, type-scale and tokens.css ↔ themeTokens parity gates
|
|
292
|
+
npm test # Vitest smoke suite (tests/), jsdom — `npm run test:watch` while iterating
|
|
293
|
+
npm run test:coverage # coverage summary for orientation only — no thresholds, not a gate
|
|
294
|
+
npm run dev # tsup --watch — only for local linked dev against a consumer
|
|
234
295
|
```
|
|
235
296
|
|
|
236
297
|
**`npm run dev:modo` is the fastest way to see a UI change.** The portal pulls `src/ui/**/*.scss` through `import.meta.glob`, so components render with real CSS over HMR without building `dist/` first.
|
|
237
298
|
|
|
238
|
-
**
|
|
299
|
+
**Tests are a small Vitest smoke suite, not coverage.** `npm test` (`tests/*.test.tsx`, jsdom + Testing Library + axe-core) pins the contracts nothing else can see: focus trap / Escape / outside-click on the floating layers, ARIA wiring on the pickers, the CSS sanitizers and the overrides generator, and an axe pass over a form and a dialog. It also pins the public surface: the export names of every subpath, every `--uxm-*` variable the stylesheets read, and that `dist/` loads as ESM and CJS (snapshots in `tests/__snapshots__/`; a minus line in the diff is a removed export or knob, i.e. a MAJOR). See `tests/README.md`. Add a test when you touch one of those contracts or fix a behavioural bug; do not chase coverage on presentational atoms. For a visual change "tests pass" proves nothing — the verification is `typecheck`, `lint`, `build` and the portal.
|
|
300
|
+
|
|
301
|
+
CI (`.github/workflows/ci.yml`) runs `lint`, `typecheck`, `check:drift`, `build` and `test:ci` on every pull request, plus an `npm audit` job that fails at critical.
|
|
239
302
|
|
|
240
303
|
`npm run build` chains five steps, and the order is deliberate:
|
|
241
304
|
|
|
@@ -259,9 +322,15 @@ dist/
|
|
|
259
322
|
├── tokens/
|
|
260
323
|
│ ├── index.{js,cjs,d.ts}
|
|
261
324
|
│ └── index.css
|
|
262
|
-
|
|
325
|
+
├── hooks/
|
|
326
|
+
│ └── index.{js,cjs,d.ts}
|
|
327
|
+
├── previews/
|
|
328
|
+
│ ├── index.{js,cjs,d.ts}
|
|
329
|
+
│ └── …per-preview files…
|
|
330
|
+
└── studio/
|
|
263
331
|
├── index.{js,cjs,d.ts}
|
|
264
|
-
|
|
332
|
+
├── studio.css
|
|
333
|
+
└── persistence/generate-css.{js,cjs,d.ts}
|
|
265
334
|
```
|
|
266
335
|
|
|
267
336
|
## Local linked development (e.g. modo)
|
|
@@ -287,9 +356,9 @@ This makes commit types carry semver meaning: `fix` → PATCH, `feat` → MINOR,
|
|
|
287
356
|
- **Add a component**: create `src/ui/{name}/` with `{name}.tsx`, `{name}.scss`, `index.ts`. Re-export from **both** the folder `index.ts` and `src/ui/index.ts`. Add the compiled stylesheet to `src/ui/styles.css` as `@import "./{name}/{name}.css";` in cascade order — the aggregator is hand-written, and a component whose `@import` is missing ships with no CSS. Add a `README.md` mirroring [`button/README.md`](src/ui/button/README.md) (simple) or [`data-table/README.md`](src/ui/data-table/README.md) (complex).
|
|
288
357
|
- **Update the AI skill in the same change**: `skills/viax-uxm/` needs a catalog row, a cheatsheet row, and a bullet under `### Unreleased` in `SKILL.md`. Leave the version and component-count markers alone — CI stamps those at release via `scripts/stamp-skill-version.mjs`.
|
|
289
358
|
- **Add a preview**: create `{name}-preview.tsx` next to the component — previews live beside their component, not in `src/previews/`. Use `PreviewProps` and project knob values as inline CSS vars so production CSS rules paint them. Never re-export a preview from a `src/ui/` barrel (see the tree-shake guarantee above).
|
|
290
|
-
- **Add a token**: add an entry to `themeTokens` in `src/tokens/index.ts` and declare the `--color-*` variable in `src/tokens/index.css
|
|
359
|
+
- **Add a token**: add an entry to `themeTokens` in `src/tokens/index.ts` and declare the `--color-*` variable in `src/tokens/index.css` — `npm run check:drift` fails if only one side changes. Reference it from component SCSS via `var(--uxm-foo-bar, var(--color-new-token))` — never hardcode a colour, spacing, or radius.
|
|
291
360
|
- Commits follow conventional-commit format (`commitizen` + `commitlint` enforced via `husky`); `npm run commit` walks you through it. Commit types drive the released version — see [Releases](#releases).
|
|
292
|
-
- Run `npm run lint && npm run typecheck && npm test && npm run build` before opening a pull request.
|
|
361
|
+
- Run `npm run lint && npm run typecheck && npm run check:drift && npm test && npm run build` before opening a pull request — the same gates CI runs.
|
|
293
362
|
|
|
294
363
|
## License
|
|
295
364
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/studio/index.ts"],"sourcesContent":["/**\n * @viax.io/uxm/studio — the UXM design workbench (sidebar + canvas + properties\n * panel + previews + editors), the same portal shipped at modo's `/uxm` route.\n *\n * Decoupled from any specific backend via `StudioPersistence`: inject\n * `createHttpPersistence()` to read/write against a Hono API, or\n * `createReadOnlyPersistence()` for a static portal (live-preview only).\n *\n * Styles ship separately as `@viax.io/uxm/studio.css` — import it once in the\n * host
|
|
1
|
+
{"version":3,"sources":["../../src/studio/index.ts"],"sourcesContent":["/**\n * @viax.io/uxm/studio — the UXM design workbench (sidebar + canvas + properties\n * panel + previews + editors), the same portal shipped at modo's `/uxm` route.\n *\n * Decoupled from any specific backend via `StudioPersistence`: inject\n * `createHttpPersistence()` to read/write against a Hono API, or\n * `createReadOnlyPersistence()` for a static portal (live-preview only).\n *\n * Styles ship separately as `@viax.io/uxm/studio.css` — import it once in the\n * host next to `tokens.css` + `ui.css`; it bundles the token declarations and\n * the Tailwind utilities the shell uses (NOT the atom CSS, so atoms are never\n * duplicated), so no host Tailwind config is required.\n */\nexport { UxmApp } from './uxm-app';\nexport type { UxmAppProps } from './uxm-app';\nexport { UxmProvider, useUxm } from './lib/context';\nexport { registry, getComponentDef, categoryColors } from './lib/registry';\nexport type {\n BrandConfig,\n BrandTokens,\n Category,\n ComponentDef,\n EditorControlType,\n LayoutVariant,\n PreviewProps,\n StyleOverrides,\n StyleProperty,\n} from './lib/types';\n\n// Persistence contract + adapters + the single CSS generator (R5/R7).\nexport {\n createReadOnlyPersistence,\n createClientPersistence,\n createHttpPersistence,\n generateOverridesCss,\n} from './persistence';\nexport type {\n StudioPersistence,\n StudioState,\n AllOverrides,\n} from './persistence';\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAaA,qBAAuB;AAEvB,qBAAoC;AACpC,sBAA0D;AAc1D,yBAKO;","names":[]}
|
package/dist/studio/index.d.ts
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* `createReadOnlyPersistence()` for a static portal (live-preview only).
|
|
8
8
|
*
|
|
9
9
|
* Styles ship separately as `@viax.io/uxm/studio.css` — import it once in the
|
|
10
|
-
* host
|
|
11
|
-
* uses
|
|
10
|
+
* host next to `tokens.css` + `ui.css`; it bundles the token declarations and
|
|
11
|
+
* the Tailwind utilities the shell uses (NOT the atom CSS, so atoms are never
|
|
12
|
+
* duplicated), so no host Tailwind config is required.
|
|
12
13
|
*/
|
|
13
14
|
export { UxmApp } from './uxm-app.js';
|
|
14
15
|
export type { UxmAppProps } from './uxm-app.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/studio/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/studio/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACnC,YAAY,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAC3E,YAAY,EACV,WAAW,EACX,WAAW,EACX,QAAQ,EACR,YAAY,EACZ,iBAAiB,EACjB,aAAa,EACb,YAAY,EACZ,cAAc,EACd,aAAa,GACd,MAAM,aAAa,CAAC;AAGrB,OAAO,EACL,yBAAyB,EACzB,uBAAuB,EACvB,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,eAAe,CAAC;AACvB,YAAY,EACV,iBAAiB,EACjB,WAAW,EACX,YAAY,GACb,MAAM,eAAe,CAAC"}
|
package/dist/studio/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/studio/index.ts"],"sourcesContent":["/**\n * @viax.io/uxm/studio — the UXM design workbench (sidebar + canvas + properties\n * panel + previews + editors), the same portal shipped at modo's `/uxm` route.\n *\n * Decoupled from any specific backend via `StudioPersistence`: inject\n * `createHttpPersistence()` to read/write against a Hono API, or\n * `createReadOnlyPersistence()` for a static portal (live-preview only).\n *\n * Styles ship separately as `@viax.io/uxm/studio.css` — import it once in the\n * host
|
|
1
|
+
{"version":3,"sources":["../../src/studio/index.ts"],"sourcesContent":["/**\n * @viax.io/uxm/studio — the UXM design workbench (sidebar + canvas + properties\n * panel + previews + editors), the same portal shipped at modo's `/uxm` route.\n *\n * Decoupled from any specific backend via `StudioPersistence`: inject\n * `createHttpPersistence()` to read/write against a Hono API, or\n * `createReadOnlyPersistence()` for a static portal (live-preview only).\n *\n * Styles ship separately as `@viax.io/uxm/studio.css` — import it once in the\n * host next to `tokens.css` + `ui.css`; it bundles the token declarations and\n * the Tailwind utilities the shell uses (NOT the atom CSS, so atoms are never\n * duplicated), so no host Tailwind config is required.\n */\nexport { UxmApp } from './uxm-app';\nexport type { UxmAppProps } from './uxm-app';\nexport { UxmProvider, useUxm } from './lib/context';\nexport { registry, getComponentDef, categoryColors } from './lib/registry';\nexport type {\n BrandConfig,\n BrandTokens,\n Category,\n ComponentDef,\n EditorControlType,\n LayoutVariant,\n PreviewProps,\n StyleOverrides,\n StyleProperty,\n} from './lib/types';\n\n// Persistence contract + adapters + the single CSS generator (R5/R7).\nexport {\n createReadOnlyPersistence,\n createClientPersistence,\n createHttpPersistence,\n generateOverridesCss,\n} from './persistence';\nexport type {\n StudioPersistence,\n StudioState,\n AllOverrides,\n} from './persistence';\n"],"mappings":"AAaA,SAAS,cAAc;AAEvB,SAAS,aAAa,cAAc;AACpC,SAAS,UAAU,iBAAiB,sBAAsB;AAc1D;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;","names":[]}
|
|
@@ -25,7 +25,7 @@ const listItemDef = {
|
|
|
25
25
|
id: "list-item",
|
|
26
26
|
name: "List Item",
|
|
27
27
|
category: "Composite",
|
|
28
|
-
description: "Row with icon, title, and optional trailing slot \u2014 used in stacked lists. Renders as `<div>` (static) or `<button>` / `<a>` (interactive) depending on the atom's `interactive` / `href` props. State knobs gate on `mode` rather than trailing \u2014 trailing is purely a visual choice and the two are orthogonal: an interactive row can have any trailing, a static row can have a chevron decoration without being tappable.",
|
|
28
|
+
description: "Row with a leading slot (accent icon tile, or media rendered as-is for a thumbnail / avatar), title, and optional trailing slot \u2014 used in stacked lists. Renders as `<div>` (static) or `<button>` / `<a>` (interactive) depending on the atom's `interactive` / `href` props. State knobs gate on `mode` rather than trailing \u2014 trailing is purely a visual choice and the two are orthogonal: an interactive row can have any trailing, a static row can have a chevron decoration without being tappable.",
|
|
29
29
|
styleProperties: [
|
|
30
30
|
// ── Per-state row colors. Filtered into the "States" section by
|
|
31
31
|
// the State variant. Default-state knobs (`inactiveBg`,
|
|
@@ -70,10 +70,14 @@ const listItemDef = {
|
|
|
70
70
|
// glyph inside. Keeps the icon visually distinct from the row bg in
|
|
71
71
|
// every state (default / hover / active / disabled), so the shared
|
|
72
72
|
// icon knobs don't need to vary per-state.
|
|
73
|
-
{ key: "iconBg", label: "Icon Bg", control: "color", defaultValue: "var(--color-accent)", section: "icon" },
|
|
74
|
-
{ key: "iconColor", label: "Icon Color", control: "color", defaultValue: "var(--color-card)", section: "icon" },
|
|
75
|
-
{ key: "iconSize", label: "Icon Size", control: "number", defaultValue: 24, min: 16, max: 40, step: 2, unit: "px", section: "icon" },
|
|
76
|
-
{ key: "iconRadius", label: "Icon Radius", control: "slider", defaultValue: 6, min: 0, max: 16, step: 1, unit: "px", section: "icon" }
|
|
73
|
+
{ key: "iconBg", label: "Icon Bg", control: "color", defaultValue: "var(--color-accent)", section: "icon", showWhen: { leading: "icon" } },
|
|
74
|
+
{ key: "iconColor", label: "Icon Color", control: "color", defaultValue: "var(--color-card)", section: "icon", showWhen: { leading: "icon" } },
|
|
75
|
+
{ key: "iconSize", label: "Icon Size", control: "number", defaultValue: 24, min: 16, max: 40, step: 2, unit: "px", section: "icon", showWhen: { leading: "icon" } },
|
|
76
|
+
{ key: "iconRadius", label: "Icon Radius", control: "slider", defaultValue: 6, min: 0, max: 16, step: 1, unit: "px", section: "icon", showWhen: { leading: "icon" } },
|
|
77
|
+
// Media slot is the un-tiled leading alternative: the passed node
|
|
78
|
+
// (Thumbnail / avatar / <img>) brings its own surface and radius, so
|
|
79
|
+
// the row only owns the box it sits in — one size knob, no colours.
|
|
80
|
+
{ key: "mediaSize", label: "Media Size", control: "number", defaultValue: 36, min: 24, max: 64, step: 2, unit: "px", section: "media", showWhen: { leading: "media" } }
|
|
77
81
|
],
|
|
78
82
|
layoutVariants: [
|
|
79
83
|
{
|
|
@@ -111,6 +115,19 @@ const listItemDef = {
|
|
|
111
115
|
],
|
|
112
116
|
defaultValue: "default"
|
|
113
117
|
},
|
|
118
|
+
{
|
|
119
|
+
key: "leading",
|
|
120
|
+
label: "Leading",
|
|
121
|
+
options: [
|
|
122
|
+
// `icon` → the accent `IconTile` (the long-standing default, so an
|
|
123
|
+
// existing saved theme opens on the same row it was saved against).
|
|
124
|
+
// `media` → the node renders as-is; the preview passes a `Thumbnail`,
|
|
125
|
+
// which is the shape this slot exists for (product / people rows).
|
|
126
|
+
{ value: "icon", label: "Icon" },
|
|
127
|
+
{ value: "media", label: "Media" }
|
|
128
|
+
],
|
|
129
|
+
defaultValue: "icon"
|
|
130
|
+
},
|
|
114
131
|
{
|
|
115
132
|
key: "value",
|
|
116
133
|
label: "Value Text",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../src/studio/lib/registry/composite/list-item.ts"],"sourcesContent":["import type { ComponentDef } from '../../types';\n\nexport const listItemDef: ComponentDef = {\n id: 'list-item',\n name: 'List Item',\n category: 'Composite',\n description: \"Row with icon, title, and optional trailing slot — used in stacked lists. Renders as `<div>` (static) or `<button>` / `<a>` (interactive) depending on the atom's `interactive` / `href` props. State knobs gate on `mode` rather than trailing — trailing is purely a visual choice and the two are orthogonal: an interactive row can have any trailing, a static row can have a chevron decoration without being tappable.\",\n styleProperties: [\n // ── Per-state row colors. Filtered into the \"States\" section by\n // the State variant. Default-state knobs (`inactiveBg`,\n // `inactiveText`) gate on `state: \"default\"` only — so the bg /\n // text are editable in both static and interactive modes. The\n // interactive-only state knobs (hover / focus / active)\n // additionally gate on `mode: \"interactive\"` so they hide\n // entirely when the row is rendered as a display-only `<div>`.\n // `disabled` is a visual treatment, not an interaction state, so\n // its knobs are NOT mode-gated.\n { key: 'inactiveBg', label: 'Background', control: 'color', defaultValue: 'var(--color-card)', section: 'states', showWhen: { state: 'default' } },\n { key: 'inactiveText', label: 'Text Color', control: 'color', defaultValue: 'var(--color-text)', section: 'states', showWhen: { state: 'default' } },\n { key: 'hoverBg', label: 'Background', control: 'color', defaultValue: 'var(--color-surface-alt)', section: 'states', showWhen: { state: 'hover', mode: 'interactive' } },\n { key: 'hoverText', label: 'Text Color', control: 'color', defaultValue: 'var(--color-text)', section: 'states', showWhen: { state: 'hover', mode: 'interactive' } },\n { key: 'focusText', label: 'Text Color', control: 'color', defaultValue: 'var(--color-text)', section: 'states', showWhen: { state: 'focus', mode: 'interactive' } },\n { key: 'activeBg', label: 'Background', control: 'color', defaultValue: 'var(--color-accent-subtle)', section: 'states', showWhen: { state: 'active', mode: 'interactive' } },\n { key: 'activeText', label: 'Text Color', control: 'color', defaultValue: 'var(--color-accent-bold)', section: 'states', showWhen: { state: 'active', mode: 'interactive' } },\n { key: 'disabledBg', label: 'Background', control: 'color', defaultValue: 'var(--color-card)', section: 'states', showWhen: { state: 'disabled' } },\n { key: 'disabledText', label: 'Text Color', control: 'color', defaultValue: 'var(--color-text-muted)', section: 'states', showWhen: { state: 'disabled' } },\n // Focus ring + disabled opacity in their own sections.\n { key: 'focusRing', label: 'Ring Color', control: 'color', defaultValue: 'var(--color-accent)', section: 'focusState', showWhen: { state: 'focus', mode: 'interactive' } },\n { key: 'disabledOpacity', label: 'Opacity', control: 'slider', defaultValue: 0.5, min: 0.1, max: 1, step: 0.05, section: 'disabledState', showWhen: { state: 'disabled' } },\n // ── Container (List wrapper)\n { key: 'borderColor', label: 'Border Color', control: 'color', defaultValue: 'var(--color-border)', section: 'container' },\n { key: 'borderRadius', label: 'Border Radius', control: 'slider', defaultValue: 8, min: 0, max: 16, step: 1, unit: 'px', section: 'container' },\n // ── Shared sizing (applies to every state)\n { key: 'paddingX', label: 'Padding X', control: 'number', defaultValue: 16, min: 8, max: 32, step: 2, unit: 'px' },\n { key: 'paddingY', label: 'Padding Y', control: 'number', defaultValue: 12, min: 4, max: 24, step: 2, unit: 'px' },\n { key: 'gap', label: 'Icon Gap', control: 'number', defaultValue: 12, min: 4, max: 24, step: 2, unit: 'px' },\n { key: 'fontSize', label: 'Font Size', control: 'number', defaultValue: 14, min: 11, max: 18, step: 1, unit: 'px' },\n // ── Sub-elements (shared across every state)\n { key: 'valueColor', label: 'Value Color', control: 'color', defaultValue: 'var(--color-text-muted)', section: 'value', showWhen: { value: 'shown' } },\n { key: 'valueSize', label: 'Value Size', control: 'number', defaultValue: 12, min: 10, max: 16, step: 1, unit: 'px', section: 'value', showWhen: { value: 'shown' } },\n // Trailing slot is purely composable — the consumer passes whatever\n // ReactNode they want. The preview demos the canonical pattern per\n // mode (chevron for interactive nav rows, Tag for static status\n // rows), so a single Chevron Color knob covers the interactive\n // example. Other trailing decorations (Tag, Badge, Avatar, etc.)\n // bring their own styling via their respective atoms.\n { key: 'chevronColor', label: 'Chevron Color', control: 'color', defaultValue: 'var(--color-text-muted)', section: 'trailing', showWhen: { mode: 'interactive' } },\n // High-contrast icon \"stamp\" — bright accent tile with a card-coloured\n // glyph inside. Keeps the icon visually distinct from the row bg in\n // every state (default / hover / active / disabled), so the shared\n // icon knobs don't need to vary per-state.\n { key: 'iconBg', label: 'Icon Bg', control: 'color', defaultValue: 'var(--color-accent)', section: 'icon' },\n { key: 'iconColor', label: 'Icon Color', control: 'color', defaultValue: 'var(--color-card)', section: 'icon' },\n { key: 'iconSize', label: 'Icon Size', control: 'number', defaultValue: 24, min: 16, max: 40, step: 2, unit: 'px', section: 'icon' },\n { key: 'iconRadius', label: 'Icon Radius', control: 'slider', defaultValue: 6, min: 0, max: 16, step: 1, unit: 'px', section: 'icon' },\n ],\n layoutVariants: [\n {\n key: 'mode',\n label: 'Mode',\n options: [\n // `static` → renders as `<div>`; no hover / focus / active\n // styling. `interactive` → renders as `<button>` (or `<a>`\n // when href is passed); full state styling. Mode is\n // orthogonal to trailing: any trailing can sit in either mode.\n { value: 'static', label: 'Static' },\n { value: 'interactive', label: 'Interactive' },\n ],\n defaultValue: 'interactive',\n },\n {\n key: 'state',\n label: 'State',\n options: [\n // `default` and `disabled` are always available — every row\n // has a resting state, and `disabled` is a visual treatment\n // (dimmed, muted) that applies regardless of mode. The CSS\n // rule includes a bare `.uxm-list-item[aria-disabled]`\n // selector that fires on `<div>` as well as `<button>` /\n // `<a>`. The interactive-only states (hover / focus / active)\n // are option-level-gated to `mode: \"interactive\"`: when the\n // row is static, the picker collapses to \"Default\" +\n // \"Disabled\", which self-explains as \"this row only has\n // visual states\" without the picker disappearing.\n { value: 'default', label: 'Default' },\n { value: 'hover', label: 'Hover', showWhen: { mode: 'interactive' } },\n { value: 'focus', label: 'Focus', showWhen: { mode: 'interactive' } },\n { value: 'active', label: 'Active', showWhen: { mode: 'interactive' } },\n { value: 'disabled', label: 'Disabled' },\n ],\n defaultValue: 'default',\n },\n {\n key: 'value',\n label: 'Value Text',\n options: [\n { value: 'shown', label: 'Shown' },\n { value: 'hidden', label: 'Hidden' },\n ],\n defaultValue: 'shown',\n },\n // No `trailing` variant — the preview chooses what to show in\n // the trailing slot based on `mode` (chevron for interactive,\n // Tag for static). Real consumers pass any ReactNode via the\n // atom's `trailing` prop; the editor preview demos the\n // canonical patterns only.\n ],\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAEO,MAAM,cAA4B;AAAA,EACvC,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,UAAU;AAAA,EACV,aAAa;AAAA,EACb,iBAAiB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAUf,EAAE,KAAK,cAAc,OAAO,cAAc,SAAS,SAAS,cAAc,qBAAqB,SAAS,UAAU,UAAU,EAAE,OAAO,UAAU,EAAE;AAAA,IACjJ,EAAE,KAAK,gBAAgB,OAAO,cAAc,SAAS,SAAS,cAAc,qBAAqB,SAAS,UAAU,UAAU,EAAE,OAAO,UAAU,EAAE;AAAA,IACnJ,EAAE,KAAK,WAAW,OAAO,cAAc,SAAS,SAAS,cAAc,4BAA4B,SAAS,UAAU,UAAU,EAAE,OAAO,SAAS,MAAM,cAAc,EAAE;AAAA,IACxK,EAAE,KAAK,aAAa,OAAO,cAAc,SAAS,SAAS,cAAc,qBAAqB,SAAS,UAAU,UAAU,EAAE,OAAO,SAAS,MAAM,cAAc,EAAE;AAAA,IACnK,EAAE,KAAK,aAAa,OAAO,cAAc,SAAS,SAAS,cAAc,qBAAqB,SAAS,UAAU,UAAU,EAAE,OAAO,SAAS,MAAM,cAAc,EAAE;AAAA,IACnK,EAAE,KAAK,YAAY,OAAO,cAAc,SAAS,SAAS,cAAc,8BAA8B,SAAS,UAAU,UAAU,EAAE,OAAO,UAAU,MAAM,cAAc,EAAE;AAAA,IAC5K,EAAE,KAAK,cAAc,OAAO,cAAc,SAAS,SAAS,cAAc,4BAA4B,SAAS,UAAU,UAAU,EAAE,OAAO,UAAU,MAAM,cAAc,EAAE;AAAA,IAC5K,EAAE,KAAK,cAAc,OAAO,cAAc,SAAS,SAAS,cAAc,qBAAqB,SAAS,UAAU,UAAU,EAAE,OAAO,WAAW,EAAE;AAAA,IAClJ,EAAE,KAAK,gBAAgB,OAAO,cAAc,SAAS,SAAS,cAAc,2BAA2B,SAAS,UAAU,UAAU,EAAE,OAAO,WAAW,EAAE;AAAA;AAAA,IAE1J,EAAE,KAAK,aAAa,OAAO,cAAc,SAAS,SAAS,cAAc,uBAAuB,SAAS,cAAc,UAAU,EAAE,OAAO,SAAS,MAAM,cAAc,EAAE;AAAA,IACzK,EAAE,KAAK,mBAAmB,OAAO,WAAW,SAAS,UAAU,cAAc,KAAK,KAAK,KAAK,KAAK,GAAG,MAAM,MAAM,SAAS,iBAAiB,UAAU,EAAE,OAAO,WAAW,EAAE;AAAA;AAAA,IAE1K,EAAE,KAAK,eAAe,OAAO,gBAAgB,SAAS,SAAS,cAAc,uBAAuB,SAAS,YAAY;AAAA,IACzH,EAAE,KAAK,gBAAgB,OAAO,iBAAiB,SAAS,UAAU,cAAc,GAAG,KAAK,GAAG,KAAK,IAAI,MAAM,GAAG,MAAM,MAAM,SAAS,YAAY;AAAA;AAAA,IAE9I,EAAE,KAAK,YAAY,OAAO,aAAa,SAAS,UAAU,cAAc,IAAI,KAAK,GAAG,KAAK,IAAI,MAAM,GAAG,MAAM,KAAK;AAAA,IACjH,EAAE,KAAK,YAAY,OAAO,aAAa,SAAS,UAAU,cAAc,IAAI,KAAK,GAAG,KAAK,IAAI,MAAM,GAAG,MAAM,KAAK;AAAA,IACjH,EAAE,KAAK,OAAO,OAAO,YAAY,SAAS,UAAU,cAAc,IAAI,KAAK,GAAG,KAAK,IAAI,MAAM,GAAG,MAAM,KAAK;AAAA,IAC3G,EAAE,KAAK,YAAY,OAAO,aAAa,SAAS,UAAU,cAAc,IAAI,KAAK,IAAI,KAAK,IAAI,MAAM,GAAG,MAAM,KAAK;AAAA;AAAA,IAElH,EAAE,KAAK,cAAc,OAAO,eAAe,SAAS,SAAS,cAAc,2BAA2B,SAAS,SAAS,UAAU,EAAE,OAAO,QAAQ,EAAE;AAAA,IACrJ,EAAE,KAAK,aAAa,OAAO,cAAc,SAAS,UAAU,cAAc,IAAI,KAAK,IAAI,KAAK,IAAI,MAAM,GAAG,MAAM,MAAM,SAAS,SAAS,UAAU,EAAE,OAAO,QAAQ,EAAE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOpK,EAAE,KAAK,gBAAgB,OAAO,iBAAiB,SAAS,SAAS,cAAc,2BAA2B,SAAS,YAAY,UAAU,EAAE,MAAM,cAAc,EAAE;AAAA;AAAA;AAAA;AAAA;AAAA,IAKjK,EAAE,KAAK,UAAU,OAAO,WAAW,SAAS,SAAS,cAAc,uBAAuB,SAAS,OAAO;AAAA,IAC1G,EAAE,KAAK,aAAa,OAAO,cAAc,SAAS,SAAS,cAAc,qBAAqB,SAAS,OAAO;AAAA,IAC9G,EAAE,KAAK,YAAY,OAAO,aAAa,SAAS,UAAU,cAAc,IAAI,KAAK,IAAI,KAAK,IAAI,MAAM,GAAG,MAAM,MAAM,SAAS,OAAO;AAAA,IACnI,EAAE,KAAK,cAAc,OAAO,eAAe,SAAS,UAAU,cAAc,GAAG,KAAK,GAAG,KAAK,IAAI,MAAM,GAAG,MAAM,MAAM,SAAS,OAAO;AAAA,EACvI;AAAA,EACA,gBAAgB;AAAA,IACd;AAAA,MACE,KAAK;AAAA,MACL,OAAO;AAAA,MACP,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA,QAKP,EAAE,OAAO,UAAU,OAAO,SAAS;AAAA,QACnC,EAAE,OAAO,eAAe,OAAO,cAAc;AAAA,MAC/C;AAAA,MACA,cAAc;AAAA,IAChB;AAAA,IACA;AAAA,MACE,KAAK;AAAA,MACL,OAAO;AAAA,MACP,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAWP,EAAE,OAAO,WAAW,OAAO,UAAU;AAAA,QACrC,EAAE,OAAO,SAAS,OAAO,SAAS,UAAU,EAAE,MAAM,cAAc,EAAE;AAAA,QACpE,EAAE,OAAO,SAAS,OAAO,SAAS,UAAU,EAAE,MAAM,cAAc,EAAE;AAAA,QACpE,EAAE,OAAO,UAAU,OAAO,UAAU,UAAU,EAAE,MAAM,cAAc,EAAE;AAAA,QACtE,EAAE,OAAO,YAAY,OAAO,WAAW;AAAA,MACzC;AAAA,MACA,cAAc;AAAA,IAChB;AAAA,IACA;AAAA,MACE,KAAK;AAAA,MACL,OAAO;AAAA,MACP,SAAS;AAAA,QACP,EAAE,OAAO,SAAS,OAAO,QAAQ;AAAA,QACjC,EAAE,OAAO,UAAU,OAAO,SAAS;AAAA,MACrC;AAAA,MACA,cAAc;AAAA,IAChB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMF;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../../../src/studio/lib/registry/composite/list-item.ts"],"sourcesContent":["import type { ComponentDef } from '../../types';\n\nexport const listItemDef: ComponentDef = {\n id: 'list-item',\n name: 'List Item',\n category: 'Composite',\n description: \"Row with a leading slot (accent icon tile, or media rendered as-is for a thumbnail / avatar), title, and optional trailing slot — used in stacked lists. Renders as `<div>` (static) or `<button>` / `<a>` (interactive) depending on the atom's `interactive` / `href` props. State knobs gate on `mode` rather than trailing — trailing is purely a visual choice and the two are orthogonal: an interactive row can have any trailing, a static row can have a chevron decoration without being tappable.\",\n styleProperties: [\n // ── Per-state row colors. Filtered into the \"States\" section by\n // the State variant. Default-state knobs (`inactiveBg`,\n // `inactiveText`) gate on `state: \"default\"` only — so the bg /\n // text are editable in both static and interactive modes. The\n // interactive-only state knobs (hover / focus / active)\n // additionally gate on `mode: \"interactive\"` so they hide\n // entirely when the row is rendered as a display-only `<div>`.\n // `disabled` is a visual treatment, not an interaction state, so\n // its knobs are NOT mode-gated.\n { key: 'inactiveBg', label: 'Background', control: 'color', defaultValue: 'var(--color-card)', section: 'states', showWhen: { state: 'default' } },\n { key: 'inactiveText', label: 'Text Color', control: 'color', defaultValue: 'var(--color-text)', section: 'states', showWhen: { state: 'default' } },\n { key: 'hoverBg', label: 'Background', control: 'color', defaultValue: 'var(--color-surface-alt)', section: 'states', showWhen: { state: 'hover', mode: 'interactive' } },\n { key: 'hoverText', label: 'Text Color', control: 'color', defaultValue: 'var(--color-text)', section: 'states', showWhen: { state: 'hover', mode: 'interactive' } },\n { key: 'focusText', label: 'Text Color', control: 'color', defaultValue: 'var(--color-text)', section: 'states', showWhen: { state: 'focus', mode: 'interactive' } },\n { key: 'activeBg', label: 'Background', control: 'color', defaultValue: 'var(--color-accent-subtle)', section: 'states', showWhen: { state: 'active', mode: 'interactive' } },\n { key: 'activeText', label: 'Text Color', control: 'color', defaultValue: 'var(--color-accent-bold)', section: 'states', showWhen: { state: 'active', mode: 'interactive' } },\n { key: 'disabledBg', label: 'Background', control: 'color', defaultValue: 'var(--color-card)', section: 'states', showWhen: { state: 'disabled' } },\n { key: 'disabledText', label: 'Text Color', control: 'color', defaultValue: 'var(--color-text-muted)', section: 'states', showWhen: { state: 'disabled' } },\n // Focus ring + disabled opacity in their own sections.\n { key: 'focusRing', label: 'Ring Color', control: 'color', defaultValue: 'var(--color-accent)', section: 'focusState', showWhen: { state: 'focus', mode: 'interactive' } },\n { key: 'disabledOpacity', label: 'Opacity', control: 'slider', defaultValue: 0.5, min: 0.1, max: 1, step: 0.05, section: 'disabledState', showWhen: { state: 'disabled' } },\n // ── Container (List wrapper)\n { key: 'borderColor', label: 'Border Color', control: 'color', defaultValue: 'var(--color-border)', section: 'container' },\n { key: 'borderRadius', label: 'Border Radius', control: 'slider', defaultValue: 8, min: 0, max: 16, step: 1, unit: 'px', section: 'container' },\n // ── Shared sizing (applies to every state)\n { key: 'paddingX', label: 'Padding X', control: 'number', defaultValue: 16, min: 8, max: 32, step: 2, unit: 'px' },\n { key: 'paddingY', label: 'Padding Y', control: 'number', defaultValue: 12, min: 4, max: 24, step: 2, unit: 'px' },\n { key: 'gap', label: 'Icon Gap', control: 'number', defaultValue: 12, min: 4, max: 24, step: 2, unit: 'px' },\n { key: 'fontSize', label: 'Font Size', control: 'number', defaultValue: 14, min: 11, max: 18, step: 1, unit: 'px' },\n // ── Sub-elements (shared across every state)\n { key: 'valueColor', label: 'Value Color', control: 'color', defaultValue: 'var(--color-text-muted)', section: 'value', showWhen: { value: 'shown' } },\n { key: 'valueSize', label: 'Value Size', control: 'number', defaultValue: 12, min: 10, max: 16, step: 1, unit: 'px', section: 'value', showWhen: { value: 'shown' } },\n // Trailing slot is purely composable — the consumer passes whatever\n // ReactNode they want. The preview demos the canonical pattern per\n // mode (chevron for interactive nav rows, Tag for static status\n // rows), so a single Chevron Color knob covers the interactive\n // example. Other trailing decorations (Tag, Badge, Avatar, etc.)\n // bring their own styling via their respective atoms.\n { key: 'chevronColor', label: 'Chevron Color', control: 'color', defaultValue: 'var(--color-text-muted)', section: 'trailing', showWhen: { mode: 'interactive' } },\n // High-contrast icon \"stamp\" — bright accent tile with a card-coloured\n // glyph inside. Keeps the icon visually distinct from the row bg in\n // every state (default / hover / active / disabled), so the shared\n // icon knobs don't need to vary per-state.\n { key: 'iconBg', label: 'Icon Bg', control: 'color', defaultValue: 'var(--color-accent)', section: 'icon', showWhen: { leading: 'icon' } },\n { key: 'iconColor', label: 'Icon Color', control: 'color', defaultValue: 'var(--color-card)', section: 'icon', showWhen: { leading: 'icon' } },\n { key: 'iconSize', label: 'Icon Size', control: 'number', defaultValue: 24, min: 16, max: 40, step: 2, unit: 'px', section: 'icon', showWhen: { leading: 'icon' } },\n { key: 'iconRadius', label: 'Icon Radius', control: 'slider', defaultValue: 6, min: 0, max: 16, step: 1, unit: 'px', section: 'icon', showWhen: { leading: 'icon' } },\n // Media slot is the un-tiled leading alternative: the passed node\n // (Thumbnail / avatar / <img>) brings its own surface and radius, so\n // the row only owns the box it sits in — one size knob, no colours.\n { key: 'mediaSize', label: 'Media Size', control: 'number', defaultValue: 36, min: 24, max: 64, step: 2, unit: 'px', section: 'media', showWhen: { leading: 'media' } },\n ],\n layoutVariants: [\n {\n key: 'mode',\n label: 'Mode',\n options: [\n // `static` → renders as `<div>`; no hover / focus / active\n // styling. `interactive` → renders as `<button>` (or `<a>`\n // when href is passed); full state styling. Mode is\n // orthogonal to trailing: any trailing can sit in either mode.\n { value: 'static', label: 'Static' },\n { value: 'interactive', label: 'Interactive' },\n ],\n defaultValue: 'interactive',\n },\n {\n key: 'state',\n label: 'State',\n options: [\n // `default` and `disabled` are always available — every row\n // has a resting state, and `disabled` is a visual treatment\n // (dimmed, muted) that applies regardless of mode. The CSS\n // rule includes a bare `.uxm-list-item[aria-disabled]`\n // selector that fires on `<div>` as well as `<button>` /\n // `<a>`. The interactive-only states (hover / focus / active)\n // are option-level-gated to `mode: \"interactive\"`: when the\n // row is static, the picker collapses to \"Default\" +\n // \"Disabled\", which self-explains as \"this row only has\n // visual states\" without the picker disappearing.\n { value: 'default', label: 'Default' },\n { value: 'hover', label: 'Hover', showWhen: { mode: 'interactive' } },\n { value: 'focus', label: 'Focus', showWhen: { mode: 'interactive' } },\n { value: 'active', label: 'Active', showWhen: { mode: 'interactive' } },\n { value: 'disabled', label: 'Disabled' },\n ],\n defaultValue: 'default',\n },\n {\n key: 'leading',\n label: 'Leading',\n options: [\n // `icon` → the accent `IconTile` (the long-standing default, so an\n // existing saved theme opens on the same row it was saved against).\n // `media` → the node renders as-is; the preview passes a `Thumbnail`,\n // which is the shape this slot exists for (product / people rows).\n { value: 'icon', label: 'Icon' },\n { value: 'media', label: 'Media' },\n ],\n defaultValue: 'icon',\n },\n {\n key: 'value',\n label: 'Value Text',\n options: [\n { value: 'shown', label: 'Shown' },\n { value: 'hidden', label: 'Hidden' },\n ],\n defaultValue: 'shown',\n },\n // No `trailing` variant — the preview chooses what to show in\n // the trailing slot based on `mode` (chevron for interactive,\n // Tag for static). Real consumers pass any ReactNode via the\n // atom's `trailing` prop; the editor preview demos the\n // canonical patterns only.\n ],\n};\n"],"mappings":"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","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"list-item.d.ts","sourceRoot":"","sources":["../../../../../src/studio/lib/registry/composite/list-item.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,eAAO,MAAM,WAAW,EAAE,
|
|
1
|
+
{"version":3,"file":"list-item.d.ts","sourceRoot":"","sources":["../../../../../src/studio/lib/registry/composite/list-item.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,eAAO,MAAM,WAAW,EAAE,YA0HzB,CAAC"}
|
|
@@ -2,7 +2,7 @@ const listItemDef = {
|
|
|
2
2
|
id: "list-item",
|
|
3
3
|
name: "List Item",
|
|
4
4
|
category: "Composite",
|
|
5
|
-
description: "Row with icon, title, and optional trailing slot \u2014 used in stacked lists. Renders as `<div>` (static) or `<button>` / `<a>` (interactive) depending on the atom's `interactive` / `href` props. State knobs gate on `mode` rather than trailing \u2014 trailing is purely a visual choice and the two are orthogonal: an interactive row can have any trailing, a static row can have a chevron decoration without being tappable.",
|
|
5
|
+
description: "Row with a leading slot (accent icon tile, or media rendered as-is for a thumbnail / avatar), title, and optional trailing slot \u2014 used in stacked lists. Renders as `<div>` (static) or `<button>` / `<a>` (interactive) depending on the atom's `interactive` / `href` props. State knobs gate on `mode` rather than trailing \u2014 trailing is purely a visual choice and the two are orthogonal: an interactive row can have any trailing, a static row can have a chevron decoration without being tappable.",
|
|
6
6
|
styleProperties: [
|
|
7
7
|
// ── Per-state row colors. Filtered into the "States" section by
|
|
8
8
|
// the State variant. Default-state knobs (`inactiveBg`,
|
|
@@ -47,10 +47,14 @@ const listItemDef = {
|
|
|
47
47
|
// glyph inside. Keeps the icon visually distinct from the row bg in
|
|
48
48
|
// every state (default / hover / active / disabled), so the shared
|
|
49
49
|
// icon knobs don't need to vary per-state.
|
|
50
|
-
{ key: "iconBg", label: "Icon Bg", control: "color", defaultValue: "var(--color-accent)", section: "icon" },
|
|
51
|
-
{ key: "iconColor", label: "Icon Color", control: "color", defaultValue: "var(--color-card)", section: "icon" },
|
|
52
|
-
{ key: "iconSize", label: "Icon Size", control: "number", defaultValue: 24, min: 16, max: 40, step: 2, unit: "px", section: "icon" },
|
|
53
|
-
{ key: "iconRadius", label: "Icon Radius", control: "slider", defaultValue: 6, min: 0, max: 16, step: 1, unit: "px", section: "icon" }
|
|
50
|
+
{ key: "iconBg", label: "Icon Bg", control: "color", defaultValue: "var(--color-accent)", section: "icon", showWhen: { leading: "icon" } },
|
|
51
|
+
{ key: "iconColor", label: "Icon Color", control: "color", defaultValue: "var(--color-card)", section: "icon", showWhen: { leading: "icon" } },
|
|
52
|
+
{ key: "iconSize", label: "Icon Size", control: "number", defaultValue: 24, min: 16, max: 40, step: 2, unit: "px", section: "icon", showWhen: { leading: "icon" } },
|
|
53
|
+
{ key: "iconRadius", label: "Icon Radius", control: "slider", defaultValue: 6, min: 0, max: 16, step: 1, unit: "px", section: "icon", showWhen: { leading: "icon" } },
|
|
54
|
+
// Media slot is the un-tiled leading alternative: the passed node
|
|
55
|
+
// (Thumbnail / avatar / <img>) brings its own surface and radius, so
|
|
56
|
+
// the row only owns the box it sits in — one size knob, no colours.
|
|
57
|
+
{ key: "mediaSize", label: "Media Size", control: "number", defaultValue: 36, min: 24, max: 64, step: 2, unit: "px", section: "media", showWhen: { leading: "media" } }
|
|
54
58
|
],
|
|
55
59
|
layoutVariants: [
|
|
56
60
|
{
|
|
@@ -88,6 +92,19 @@ const listItemDef = {
|
|
|
88
92
|
],
|
|
89
93
|
defaultValue: "default"
|
|
90
94
|
},
|
|
95
|
+
{
|
|
96
|
+
key: "leading",
|
|
97
|
+
label: "Leading",
|
|
98
|
+
options: [
|
|
99
|
+
// `icon` → the accent `IconTile` (the long-standing default, so an
|
|
100
|
+
// existing saved theme opens on the same row it was saved against).
|
|
101
|
+
// `media` → the node renders as-is; the preview passes a `Thumbnail`,
|
|
102
|
+
// which is the shape this slot exists for (product / people rows).
|
|
103
|
+
{ value: "icon", label: "Icon" },
|
|
104
|
+
{ value: "media", label: "Media" }
|
|
105
|
+
],
|
|
106
|
+
defaultValue: "icon"
|
|
107
|
+
},
|
|
91
108
|
{
|
|
92
109
|
key: "value",
|
|
93
110
|
label: "Value Text",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../src/studio/lib/registry/composite/list-item.ts"],"sourcesContent":["import type { ComponentDef } from '../../types';\n\nexport const listItemDef: ComponentDef = {\n id: 'list-item',\n name: 'List Item',\n category: 'Composite',\n description: \"Row with icon, title, and optional trailing slot — used in stacked lists. Renders as `<div>` (static) or `<button>` / `<a>` (interactive) depending on the atom's `interactive` / `href` props. State knobs gate on `mode` rather than trailing — trailing is purely a visual choice and the two are orthogonal: an interactive row can have any trailing, a static row can have a chevron decoration without being tappable.\",\n styleProperties: [\n // ── Per-state row colors. Filtered into the \"States\" section by\n // the State variant. Default-state knobs (`inactiveBg`,\n // `inactiveText`) gate on `state: \"default\"` only — so the bg /\n // text are editable in both static and interactive modes. The\n // interactive-only state knobs (hover / focus / active)\n // additionally gate on `mode: \"interactive\"` so they hide\n // entirely when the row is rendered as a display-only `<div>`.\n // `disabled` is a visual treatment, not an interaction state, so\n // its knobs are NOT mode-gated.\n { key: 'inactiveBg', label: 'Background', control: 'color', defaultValue: 'var(--color-card)', section: 'states', showWhen: { state: 'default' } },\n { key: 'inactiveText', label: 'Text Color', control: 'color', defaultValue: 'var(--color-text)', section: 'states', showWhen: { state: 'default' } },\n { key: 'hoverBg', label: 'Background', control: 'color', defaultValue: 'var(--color-surface-alt)', section: 'states', showWhen: { state: 'hover', mode: 'interactive' } },\n { key: 'hoverText', label: 'Text Color', control: 'color', defaultValue: 'var(--color-text)', section: 'states', showWhen: { state: 'hover', mode: 'interactive' } },\n { key: 'focusText', label: 'Text Color', control: 'color', defaultValue: 'var(--color-text)', section: 'states', showWhen: { state: 'focus', mode: 'interactive' } },\n { key: 'activeBg', label: 'Background', control: 'color', defaultValue: 'var(--color-accent-subtle)', section: 'states', showWhen: { state: 'active', mode: 'interactive' } },\n { key: 'activeText', label: 'Text Color', control: 'color', defaultValue: 'var(--color-accent-bold)', section: 'states', showWhen: { state: 'active', mode: 'interactive' } },\n { key: 'disabledBg', label: 'Background', control: 'color', defaultValue: 'var(--color-card)', section: 'states', showWhen: { state: 'disabled' } },\n { key: 'disabledText', label: 'Text Color', control: 'color', defaultValue: 'var(--color-text-muted)', section: 'states', showWhen: { state: 'disabled' } },\n // Focus ring + disabled opacity in their own sections.\n { key: 'focusRing', label: 'Ring Color', control: 'color', defaultValue: 'var(--color-accent)', section: 'focusState', showWhen: { state: 'focus', mode: 'interactive' } },\n { key: 'disabledOpacity', label: 'Opacity', control: 'slider', defaultValue: 0.5, min: 0.1, max: 1, step: 0.05, section: 'disabledState', showWhen: { state: 'disabled' } },\n // ── Container (List wrapper)\n { key: 'borderColor', label: 'Border Color', control: 'color', defaultValue: 'var(--color-border)', section: 'container' },\n { key: 'borderRadius', label: 'Border Radius', control: 'slider', defaultValue: 8, min: 0, max: 16, step: 1, unit: 'px', section: 'container' },\n // ── Shared sizing (applies to every state)\n { key: 'paddingX', label: 'Padding X', control: 'number', defaultValue: 16, min: 8, max: 32, step: 2, unit: 'px' },\n { key: 'paddingY', label: 'Padding Y', control: 'number', defaultValue: 12, min: 4, max: 24, step: 2, unit: 'px' },\n { key: 'gap', label: 'Icon Gap', control: 'number', defaultValue: 12, min: 4, max: 24, step: 2, unit: 'px' },\n { key: 'fontSize', label: 'Font Size', control: 'number', defaultValue: 14, min: 11, max: 18, step: 1, unit: 'px' },\n // ── Sub-elements (shared across every state)\n { key: 'valueColor', label: 'Value Color', control: 'color', defaultValue: 'var(--color-text-muted)', section: 'value', showWhen: { value: 'shown' } },\n { key: 'valueSize', label: 'Value Size', control: 'number', defaultValue: 12, min: 10, max: 16, step: 1, unit: 'px', section: 'value', showWhen: { value: 'shown' } },\n // Trailing slot is purely composable — the consumer passes whatever\n // ReactNode they want. The preview demos the canonical pattern per\n // mode (chevron for interactive nav rows, Tag for static status\n // rows), so a single Chevron Color knob covers the interactive\n // example. Other trailing decorations (Tag, Badge, Avatar, etc.)\n // bring their own styling via their respective atoms.\n { key: 'chevronColor', label: 'Chevron Color', control: 'color', defaultValue: 'var(--color-text-muted)', section: 'trailing', showWhen: { mode: 'interactive' } },\n // High-contrast icon \"stamp\" — bright accent tile with a card-coloured\n // glyph inside. Keeps the icon visually distinct from the row bg in\n // every state (default / hover / active / disabled), so the shared\n // icon knobs don't need to vary per-state.\n { key: 'iconBg', label: 'Icon Bg', control: 'color', defaultValue: 'var(--color-accent)', section: 'icon' },\n { key: 'iconColor', label: 'Icon Color', control: 'color', defaultValue: 'var(--color-card)', section: 'icon' },\n { key: 'iconSize', label: 'Icon Size', control: 'number', defaultValue: 24, min: 16, max: 40, step: 2, unit: 'px', section: 'icon' },\n { key: 'iconRadius', label: 'Icon Radius', control: 'slider', defaultValue: 6, min: 0, max: 16, step: 1, unit: 'px', section: 'icon' },\n ],\n layoutVariants: [\n {\n key: 'mode',\n label: 'Mode',\n options: [\n // `static` → renders as `<div>`; no hover / focus / active\n // styling. `interactive` → renders as `<button>` (or `<a>`\n // when href is passed); full state styling. Mode is\n // orthogonal to trailing: any trailing can sit in either mode.\n { value: 'static', label: 'Static' },\n { value: 'interactive', label: 'Interactive' },\n ],\n defaultValue: 'interactive',\n },\n {\n key: 'state',\n label: 'State',\n options: [\n // `default` and `disabled` are always available — every row\n // has a resting state, and `disabled` is a visual treatment\n // (dimmed, muted) that applies regardless of mode. The CSS\n // rule includes a bare `.uxm-list-item[aria-disabled]`\n // selector that fires on `<div>` as well as `<button>` /\n // `<a>`. The interactive-only states (hover / focus / active)\n // are option-level-gated to `mode: \"interactive\"`: when the\n // row is static, the picker collapses to \"Default\" +\n // \"Disabled\", which self-explains as \"this row only has\n // visual states\" without the picker disappearing.\n { value: 'default', label: 'Default' },\n { value: 'hover', label: 'Hover', showWhen: { mode: 'interactive' } },\n { value: 'focus', label: 'Focus', showWhen: { mode: 'interactive' } },\n { value: 'active', label: 'Active', showWhen: { mode: 'interactive' } },\n { value: 'disabled', label: 'Disabled' },\n ],\n defaultValue: 'default',\n },\n {\n key: 'value',\n label: 'Value Text',\n options: [\n { value: 'shown', label: 'Shown' },\n { value: 'hidden', label: 'Hidden' },\n ],\n defaultValue: 'shown',\n },\n // No `trailing` variant — the preview chooses what to show in\n // the trailing slot based on `mode` (chevron for interactive,\n // Tag for static). Real consumers pass any ReactNode via the\n // atom's `trailing` prop; the editor preview demos the\n // canonical patterns only.\n ],\n};\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../../../src/studio/lib/registry/composite/list-item.ts"],"sourcesContent":["import type { ComponentDef } from '../../types';\n\nexport const listItemDef: ComponentDef = {\n id: 'list-item',\n name: 'List Item',\n category: 'Composite',\n description: \"Row with a leading slot (accent icon tile, or media rendered as-is for a thumbnail / avatar), title, and optional trailing slot — used in stacked lists. Renders as `<div>` (static) or `<button>` / `<a>` (interactive) depending on the atom's `interactive` / `href` props. State knobs gate on `mode` rather than trailing — trailing is purely a visual choice and the two are orthogonal: an interactive row can have any trailing, a static row can have a chevron decoration without being tappable.\",\n styleProperties: [\n // ── Per-state row colors. Filtered into the \"States\" section by\n // the State variant. Default-state knobs (`inactiveBg`,\n // `inactiveText`) gate on `state: \"default\"` only — so the bg /\n // text are editable in both static and interactive modes. The\n // interactive-only state knobs (hover / focus / active)\n // additionally gate on `mode: \"interactive\"` so they hide\n // entirely when the row is rendered as a display-only `<div>`.\n // `disabled` is a visual treatment, not an interaction state, so\n // its knobs are NOT mode-gated.\n { key: 'inactiveBg', label: 'Background', control: 'color', defaultValue: 'var(--color-card)', section: 'states', showWhen: { state: 'default' } },\n { key: 'inactiveText', label: 'Text Color', control: 'color', defaultValue: 'var(--color-text)', section: 'states', showWhen: { state: 'default' } },\n { key: 'hoverBg', label: 'Background', control: 'color', defaultValue: 'var(--color-surface-alt)', section: 'states', showWhen: { state: 'hover', mode: 'interactive' } },\n { key: 'hoverText', label: 'Text Color', control: 'color', defaultValue: 'var(--color-text)', section: 'states', showWhen: { state: 'hover', mode: 'interactive' } },\n { key: 'focusText', label: 'Text Color', control: 'color', defaultValue: 'var(--color-text)', section: 'states', showWhen: { state: 'focus', mode: 'interactive' } },\n { key: 'activeBg', label: 'Background', control: 'color', defaultValue: 'var(--color-accent-subtle)', section: 'states', showWhen: { state: 'active', mode: 'interactive' } },\n { key: 'activeText', label: 'Text Color', control: 'color', defaultValue: 'var(--color-accent-bold)', section: 'states', showWhen: { state: 'active', mode: 'interactive' } },\n { key: 'disabledBg', label: 'Background', control: 'color', defaultValue: 'var(--color-card)', section: 'states', showWhen: { state: 'disabled' } },\n { key: 'disabledText', label: 'Text Color', control: 'color', defaultValue: 'var(--color-text-muted)', section: 'states', showWhen: { state: 'disabled' } },\n // Focus ring + disabled opacity in their own sections.\n { key: 'focusRing', label: 'Ring Color', control: 'color', defaultValue: 'var(--color-accent)', section: 'focusState', showWhen: { state: 'focus', mode: 'interactive' } },\n { key: 'disabledOpacity', label: 'Opacity', control: 'slider', defaultValue: 0.5, min: 0.1, max: 1, step: 0.05, section: 'disabledState', showWhen: { state: 'disabled' } },\n // ── Container (List wrapper)\n { key: 'borderColor', label: 'Border Color', control: 'color', defaultValue: 'var(--color-border)', section: 'container' },\n { key: 'borderRadius', label: 'Border Radius', control: 'slider', defaultValue: 8, min: 0, max: 16, step: 1, unit: 'px', section: 'container' },\n // ── Shared sizing (applies to every state)\n { key: 'paddingX', label: 'Padding X', control: 'number', defaultValue: 16, min: 8, max: 32, step: 2, unit: 'px' },\n { key: 'paddingY', label: 'Padding Y', control: 'number', defaultValue: 12, min: 4, max: 24, step: 2, unit: 'px' },\n { key: 'gap', label: 'Icon Gap', control: 'number', defaultValue: 12, min: 4, max: 24, step: 2, unit: 'px' },\n { key: 'fontSize', label: 'Font Size', control: 'number', defaultValue: 14, min: 11, max: 18, step: 1, unit: 'px' },\n // ── Sub-elements (shared across every state)\n { key: 'valueColor', label: 'Value Color', control: 'color', defaultValue: 'var(--color-text-muted)', section: 'value', showWhen: { value: 'shown' } },\n { key: 'valueSize', label: 'Value Size', control: 'number', defaultValue: 12, min: 10, max: 16, step: 1, unit: 'px', section: 'value', showWhen: { value: 'shown' } },\n // Trailing slot is purely composable — the consumer passes whatever\n // ReactNode they want. The preview demos the canonical pattern per\n // mode (chevron for interactive nav rows, Tag for static status\n // rows), so a single Chevron Color knob covers the interactive\n // example. Other trailing decorations (Tag, Badge, Avatar, etc.)\n // bring their own styling via their respective atoms.\n { key: 'chevronColor', label: 'Chevron Color', control: 'color', defaultValue: 'var(--color-text-muted)', section: 'trailing', showWhen: { mode: 'interactive' } },\n // High-contrast icon \"stamp\" — bright accent tile with a card-coloured\n // glyph inside. Keeps the icon visually distinct from the row bg in\n // every state (default / hover / active / disabled), so the shared\n // icon knobs don't need to vary per-state.\n { key: 'iconBg', label: 'Icon Bg', control: 'color', defaultValue: 'var(--color-accent)', section: 'icon', showWhen: { leading: 'icon' } },\n { key: 'iconColor', label: 'Icon Color', control: 'color', defaultValue: 'var(--color-card)', section: 'icon', showWhen: { leading: 'icon' } },\n { key: 'iconSize', label: 'Icon Size', control: 'number', defaultValue: 24, min: 16, max: 40, step: 2, unit: 'px', section: 'icon', showWhen: { leading: 'icon' } },\n { key: 'iconRadius', label: 'Icon Radius', control: 'slider', defaultValue: 6, min: 0, max: 16, step: 1, unit: 'px', section: 'icon', showWhen: { leading: 'icon' } },\n // Media slot is the un-tiled leading alternative: the passed node\n // (Thumbnail / avatar / <img>) brings its own surface and radius, so\n // the row only owns the box it sits in — one size knob, no colours.\n { key: 'mediaSize', label: 'Media Size', control: 'number', defaultValue: 36, min: 24, max: 64, step: 2, unit: 'px', section: 'media', showWhen: { leading: 'media' } },\n ],\n layoutVariants: [\n {\n key: 'mode',\n label: 'Mode',\n options: [\n // `static` → renders as `<div>`; no hover / focus / active\n // styling. `interactive` → renders as `<button>` (or `<a>`\n // when href is passed); full state styling. Mode is\n // orthogonal to trailing: any trailing can sit in either mode.\n { value: 'static', label: 'Static' },\n { value: 'interactive', label: 'Interactive' },\n ],\n defaultValue: 'interactive',\n },\n {\n key: 'state',\n label: 'State',\n options: [\n // `default` and `disabled` are always available — every row\n // has a resting state, and `disabled` is a visual treatment\n // (dimmed, muted) that applies regardless of mode. The CSS\n // rule includes a bare `.uxm-list-item[aria-disabled]`\n // selector that fires on `<div>` as well as `<button>` /\n // `<a>`. The interactive-only states (hover / focus / active)\n // are option-level-gated to `mode: \"interactive\"`: when the\n // row is static, the picker collapses to \"Default\" +\n // \"Disabled\", which self-explains as \"this row only has\n // visual states\" without the picker disappearing.\n { value: 'default', label: 'Default' },\n { value: 'hover', label: 'Hover', showWhen: { mode: 'interactive' } },\n { value: 'focus', label: 'Focus', showWhen: { mode: 'interactive' } },\n { value: 'active', label: 'Active', showWhen: { mode: 'interactive' } },\n { value: 'disabled', label: 'Disabled' },\n ],\n defaultValue: 'default',\n },\n {\n key: 'leading',\n label: 'Leading',\n options: [\n // `icon` → the accent `IconTile` (the long-standing default, so an\n // existing saved theme opens on the same row it was saved against).\n // `media` → the node renders as-is; the preview passes a `Thumbnail`,\n // which is the shape this slot exists for (product / people rows).\n { value: 'icon', label: 'Icon' },\n { value: 'media', label: 'Media' },\n ],\n defaultValue: 'icon',\n },\n {\n key: 'value',\n label: 'Value Text',\n options: [\n { value: 'shown', label: 'Shown' },\n { value: 'hidden', label: 'Hidden' },\n ],\n defaultValue: 'shown',\n },\n // No `trailing` variant — the preview chooses what to show in\n // the trailing slot based on `mode` (chevron for interactive,\n // Tag for static). Real consumers pass any ReactNode via the\n // atom's `trailing` prop; the editor preview demos the\n // canonical patterns only.\n ],\n};\n"],"mappings":"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","names":[]}
|
|
@@ -27,6 +27,7 @@ var import_helpers = require("../../helpers/index.cjs");
|
|
|
27
27
|
var import_ui = require("../index.cjs");
|
|
28
28
|
var import_ui2 = require("../index.cjs");
|
|
29
29
|
var import_ui3 = require("../index.cjs");
|
|
30
|
+
var import_ui4 = require("../index.cjs");
|
|
30
31
|
var import_list = require("./list.cjs");
|
|
31
32
|
function buildVars(styles) {
|
|
32
33
|
return {
|
|
@@ -53,9 +54,11 @@ function buildVars(styles) {
|
|
|
53
54
|
"--uxm-list-item-icon-color": styles.iconColor,
|
|
54
55
|
"--uxm-list-item-icon-size": `${styles.iconSize}px`,
|
|
55
56
|
"--uxm-list-item-icon-radius": `${styles.iconRadius}px`,
|
|
57
|
+
"--uxm-list-item-media-size": `${styles.mediaSize}px`,
|
|
56
58
|
"--uxm-list-item-chevron-color": styles.chevronColor
|
|
57
59
|
};
|
|
58
60
|
}
|
|
61
|
+
const DEMO_MEDIA = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_ui4.Thumbnail, { alt: "" });
|
|
59
62
|
function trailingForMode(mode, meta, tagType) {
|
|
60
63
|
if (mode === "interactive") {
|
|
61
64
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_ui.Icon, { glyph: "chevron-right", size: 16 });
|
|
@@ -65,13 +68,14 @@ function trailingForMode(mode, meta, tagType) {
|
|
|
65
68
|
function StaticShowcase({
|
|
66
69
|
mode,
|
|
67
70
|
state,
|
|
68
|
-
showValue
|
|
71
|
+
showValue,
|
|
72
|
+
leading
|
|
69
73
|
}) {
|
|
70
74
|
const isInteractive = mode === "interactive";
|
|
71
75
|
const isActive = isInteractive && state === "active";
|
|
72
76
|
const isDisabled = state === "disabled";
|
|
73
77
|
const inner = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
74
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_ui.IconTile, { className: "uxm-list-item__icon", style: import_list.LIST_ITEM_ICON_TILE_STYLE, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_ui.Icon, { glyph: "square" }) }),
|
|
78
|
+
leading === "media" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "uxm-list-item__media", style: import_list.LIST_ITEM_MEDIA_STYLE, children: DEMO_MEDIA }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_ui.IconTile, { className: "uxm-list-item__icon", style: import_list.LIST_ITEM_ICON_TILE_STYLE, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_ui.Icon, { glyph: "square" }) }),
|
|
75
79
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "uxm-list-item__content", children: [
|
|
76
80
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "uxm-list-item__title", children: "Owner" }),
|
|
77
81
|
showValue && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "uxm-list-item__value", children: "Alex Morgan" })
|
|
@@ -116,6 +120,7 @@ function ListItemPreview({ styles, variants }) {
|
|
|
116
120
|
const mode = variants.mode ?? "interactive";
|
|
117
121
|
const state = variants.state ?? "default";
|
|
118
122
|
const showValue = (variants.value ?? "shown") === "shown";
|
|
123
|
+
const leading = variants.leading ?? "icon";
|
|
119
124
|
const [activeKey, setActiveKey] = (0, import_react.useState)("plan");
|
|
120
125
|
const cssVars = buildVars(styles);
|
|
121
126
|
const sectionLabel = {
|
|
@@ -131,7 +136,7 @@ function ListItemPreview({ styles, variants }) {
|
|
|
131
136
|
state,
|
|
132
137
|
" state"
|
|
133
138
|
] }),
|
|
134
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(StaticShowcase, { mode, state, showValue })
|
|
139
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(StaticShowcase, { mode, state, showValue, leading })
|
|
135
140
|
] }),
|
|
136
141
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { borderTop: "1px solid var(--color-border)", paddingTop: 16 }, children: [
|
|
137
142
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: sectionLabel, children: mode === "interactive" ? "Interactive" : "Static" }),
|
|
@@ -143,7 +148,7 @@ function ListItemPreview({ styles, variants }) {
|
|
|
143
148
|
interactive: isInteractive,
|
|
144
149
|
active: isInteractive && row.key === activeKey,
|
|
145
150
|
disabled: state === "disabled" && i === 1,
|
|
146
|
-
icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_ui.Icon, { glyph: "square" }),
|
|
151
|
+
...leading === "media" ? { media: DEMO_MEDIA } : { icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_ui.Icon, { glyph: "square" }) },
|
|
147
152
|
value: showValue ? row.value : void 0,
|
|
148
153
|
trailing: trailingForMode(mode, row.meta, row.tagType),
|
|
149
154
|
onClick: isInteractive ? () => setActiveKey(row.key) : void 0,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/ui/list/list-item-preview.tsx"],"sourcesContent":["import { useState, type CSSProperties, type ReactNode } from 'react';\n\nimport { cn } from '@/helpers';\nimport type { PreviewProps } from '@/previews/types';\nimport { Icon, IconTile } from '@/ui';\nimport { List, ListItem } from '@/ui';\nimport { Tag, type TagType } from '@/ui';\n\nimport { LIST_ITEM_ICON_TILE_STYLE } from './list';\n\ntype Styles = PreviewProps['styles'];\ntype Mode = 'static' | 'interactive';\n\n/**\n * Project every registry knob as a `--uxm-list-item-*` custom property\n * on the wrapper. Both the static showcase and the interactive list\n * below read from the same vars; the showcase forces a state via the\n * `--state-*` modifier class while the interactive list exercises the\n * production `:hover` / `:focus-visible` / `:disabled` rules on real\n * pointer + keyboard.\n */\nfunction buildVars(styles: Styles): CSSProperties {\n return {\n '--uxm-list-item-inactive-bg': styles.inactiveBg as string,\n '--uxm-list-item-inactive-text': styles.inactiveText as string,\n '--uxm-list-item-hover-bg': styles.hoverBg as string,\n '--uxm-list-item-hover-text': styles.hoverText as string,\n '--uxm-list-item-focus-text': styles.focusText as string,\n '--uxm-list-item-focus-ring': styles.focusRing as string,\n '--uxm-list-item-active-bg': styles.activeBg as string,\n '--uxm-list-item-active-text': styles.activeText as string,\n '--uxm-list-item-disabled-bg': styles.disabledBg as string,\n '--uxm-list-item-disabled-text': styles.disabledText as string,\n '--uxm-list-item-disabled-opacity':\n styles.disabledOpacity != null ? String(styles.disabledOpacity) : undefined,\n '--uxm-list-item-border-color': styles.borderColor as string,\n '--uxm-list-item-border-radius': `${styles.borderRadius}px`,\n '--uxm-list-item-padding-x': `${styles.paddingX}px`,\n '--uxm-list-item-padding-y': `${styles.paddingY}px`,\n '--uxm-list-item-gap': `${styles.gap}px`,\n '--uxm-list-item-font-size': `${styles.fontSize}px`,\n '--uxm-list-item-value-color': styles.valueColor as string,\n '--uxm-list-item-value-size': `${styles.valueSize}px`,\n '--uxm-list-item-icon-bg': styles.iconBg as string,\n '--uxm-list-item-icon-color': styles.iconColor as string,\n '--uxm-list-item-icon-size': `${styles.iconSize}px`,\n '--uxm-list-item-icon-radius': `${styles.iconRadius}px`,\n '--uxm-list-item-chevron-color': styles.chevronColor as string,\n } as CSSProperties;\n}\n\n/**\n * Pick the canonical trailing element for the given mode. The trailing\n * slot is `ReactNode` on the atom — consumers can pass anything (Tag,\n * Badge, Avatar, custom node). The preview demos the canonical patterns:\n * - interactive → chevron icon (the \"tap to drill in\" affordance)\n * - static → Tag (status indicator for an info row)\n */\nfunction trailingForMode(\n mode: Mode,\n meta: string,\n tagType: TagType,\n): ReactNode {\n if (mode === 'interactive') {\n // Color inherits via `currentColor` from `.uxm-list-item__trailing`,\n // which reads `--uxm-list-item-chevron-color`. Keeps saves working\n // (the var has a single source of truth instead of an inline JS\n // override that would shadow it after save).\n return <Icon glyph=\"chevron-right\" size={16} />;\n }\n return <Tag type={tagType}>{meta}</Tag>;\n}\n\n/**\n * Static showcase — ONE row, rendering matches the `mode` variant.\n * - `interactive` → `<button>` with the forced-state class so all\n * state CSS rules can apply (inert via pointer-events: none and\n * tabIndex={-1}, so the forced state isn't overridden by real\n * input).\n * - `static` → `<div>` with no state class. State CSS rules are\n * scoped to button/anchor element selectors and don't fire on\n * divs, so the static row stays truly static.\n *\n * Hand-rendered because the atom doesn't expose a way to set the\n * showcase-only `--state-*` modifier class.\n */\nfunction StaticShowcase({\n mode,\n state,\n showValue,\n}: {\n mode: Mode;\n state: string;\n showValue: boolean;\n}) {\n const isInteractive = mode === 'interactive';\n const isActive = isInteractive && state === 'active';\n const isDisabled = state === 'disabled';\n const inner = (\n <>\n {/* Mirrors the atom: the icon slot is an IconTile, sized/coloured from\n the same forwarded knob vars (glyph size comes from the tile var). */}\n <IconTile className=\"uxm-list-item__icon\" style={LIST_ITEM_ICON_TILE_STYLE}>\n <Icon glyph=\"square\" />\n </IconTile>\n <span className=\"uxm-list-item__content\">\n <span className=\"uxm-list-item__title\">Owner</span>\n {showValue && <span className=\"uxm-list-item__value\">Alex Morgan</span>}\n </span>\n <span className=\"uxm-list-item__trailing\">\n {trailingForMode(mode, 'Verified', 'success')}\n </span>\n </>\n );\n\n return (\n <List>\n {isInteractive ? (\n <button\n type=\"button\"\n tabIndex={-1}\n disabled={isDisabled}\n aria-pressed={isActive}\n className={cn(\n 'uxm-list-item',\n isActive && 'uxm-list-item--active',\n state === 'hover' && 'uxm-list-item--state-hover',\n state === 'focus' && 'uxm-list-item--state-focus',\n )}\n style={{ pointerEvents: 'none' }}\n >\n {inner}\n </button>\n ) : (\n <div\n className=\"uxm-list-item\"\n {...(isDisabled ? { 'aria-disabled': true } : {})}\n >\n {inner}\n </div>\n )}\n </List>\n );\n}\n\nconst ROWS: Array<{\n key: string;\n title: string;\n value: string;\n meta: string;\n tagType: TagType;\n}> = [\n // One row per Tag type, with a label that matches the type's\n // semantic meaning — so two tags never read the same word with\n // different visual treatments.\n { key: 'plan', title: 'Plan', value: 'Enterprise', meta: 'Pro', tagType: 'accent' },\n { key: 'billing', title: 'Billing', value: 'Visa •• 4242', meta: 'Action needed', tagType: 'warning' },\n { key: 'owner', title: 'Owner', value: 'Alex Morgan', meta: 'Verified', tagType: 'success' },\n { key: 'activity', title: 'Last activity', value: '2 hours ago', meta: 'Recent', tagType: 'neutral' },\n];\n\nexport function ListItemPreview({ styles, variants }: PreviewProps) {\n const mode = ((variants.mode as string) ?? 'interactive') as Mode;\n const state = (variants.state as string) ?? 'default';\n const showValue = ((variants.value as string) ?? 'shown') === 'shown';\n const [activeKey, setActiveKey] = useState<string>('plan');\n const cssVars = buildVars(styles);\n\n const sectionLabel = {\n fontSize: 11,\n color: 'var(--color-text-muted)',\n marginBottom: 12,\n letterSpacing: 0.5,\n textTransform: 'uppercase' as const,\n };\n\n return (\n <div style={{ display: 'flex', flexDirection: 'column', gap: 32, minWidth: 380, ...cssVars } as CSSProperties}>\n <div>\n <div style={sectionLabel}>{state} state</div>\n <StaticShowcase mode={mode} state={state} showValue={showValue} />\n </div>\n\n {/* List — interactivity is driven by the `mode` variant. Trailing\n slot is set by the preview to the canonical example for each\n mode: chevron for interactive (the \"tap to drill in\" cue),\n Tag for static (status indicator).\n interactive → all rows render as <button>; the State dropdown\n carries through to `disabled` (applied to the second row),\n and real `:hover` / `:focus-visible` exercise the production\n CSS via the projected vars above.\n static → all rows render as <div>; the interactive states\n (hover / focus / active) are filtered out of the State picker\n via the registry's showWhen. `disabled` still applies to divs\n via `[aria-disabled]`. */}\n <div style={{ borderTop: '1px solid var(--color-border)', paddingTop: 16 }}>\n <div style={sectionLabel}>{mode === 'interactive' ? 'Interactive' : 'Static'}</div>\n <List>\n {ROWS.map((row, i) => {\n const isInteractive = mode === 'interactive';\n return (\n <ListItem\n key={row.key}\n interactive={isInteractive}\n active={isInteractive && row.key === activeKey}\n // Disabled is a visual treatment that applies to both\n // interactive rows (CSS `:disabled` / `[aria-disabled]`)\n // and static rows (`[aria-disabled]` on `<div>`).\n disabled={state === 'disabled' && i === 1}\n icon={<Icon glyph=\"square\" />}\n value={showValue ? row.value : undefined}\n trailing={trailingForMode(mode, row.meta, row.tagType)}\n onClick={isInteractive ? () => setActiveKey(row.key) : undefined}\n >\n {row.title}\n </ListItem>\n );\n })}\n </List>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":["import_ui"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/ui/list/list-item-preview.tsx"],"sourcesContent":["import { useState, type CSSProperties, type ReactNode } from 'react';\n\nimport { cn } from '@/helpers';\nimport type { PreviewProps } from '@/previews/types';\nimport { Icon, IconTile } from '@/ui';\nimport { List, ListItem } from '@/ui';\nimport { Tag, type TagType } from '@/ui';\nimport { Thumbnail } from '@/ui';\n\nimport { LIST_ITEM_ICON_TILE_STYLE, LIST_ITEM_MEDIA_STYLE } from './list';\n\ntype Styles = PreviewProps['styles'];\ntype Mode = 'static' | 'interactive';\ntype Leading = 'icon' | 'media';\n\n/**\n * Project every registry knob as a `--uxm-list-item-*` custom property\n * on the wrapper. Both the static showcase and the interactive list\n * below read from the same vars; the showcase forces a state via the\n * `--state-*` modifier class while the interactive list exercises the\n * production `:hover` / `:focus-visible` / `:disabled` rules on real\n * pointer + keyboard.\n */\nfunction buildVars(styles: Styles): CSSProperties {\n return {\n '--uxm-list-item-inactive-bg': styles.inactiveBg as string,\n '--uxm-list-item-inactive-text': styles.inactiveText as string,\n '--uxm-list-item-hover-bg': styles.hoverBg as string,\n '--uxm-list-item-hover-text': styles.hoverText as string,\n '--uxm-list-item-focus-text': styles.focusText as string,\n '--uxm-list-item-focus-ring': styles.focusRing as string,\n '--uxm-list-item-active-bg': styles.activeBg as string,\n '--uxm-list-item-active-text': styles.activeText as string,\n '--uxm-list-item-disabled-bg': styles.disabledBg as string,\n '--uxm-list-item-disabled-text': styles.disabledText as string,\n '--uxm-list-item-disabled-opacity':\n styles.disabledOpacity != null ? String(styles.disabledOpacity) : undefined,\n '--uxm-list-item-border-color': styles.borderColor as string,\n '--uxm-list-item-border-radius': `${styles.borderRadius}px`,\n '--uxm-list-item-padding-x': `${styles.paddingX}px`,\n '--uxm-list-item-padding-y': `${styles.paddingY}px`,\n '--uxm-list-item-gap': `${styles.gap}px`,\n '--uxm-list-item-font-size': `${styles.fontSize}px`,\n '--uxm-list-item-value-color': styles.valueColor as string,\n '--uxm-list-item-value-size': `${styles.valueSize}px`,\n '--uxm-list-item-icon-bg': styles.iconBg as string,\n '--uxm-list-item-icon-color': styles.iconColor as string,\n '--uxm-list-item-icon-size': `${styles.iconSize}px`,\n '--uxm-list-item-icon-radius': `${styles.iconRadius}px`,\n '--uxm-list-item-media-size': `${styles.mediaSize}px`,\n '--uxm-list-item-chevron-color': styles.chevronColor as string,\n } as CSSProperties;\n}\n\n/**\n * The demo media node for the `leading: \"media\"` variant — a `Thumbnail`\n * with no `src`, so it renders the atom's own placeholder instead of\n * reaching for a network image the canvas may not be able to load. It\n * tracks the Media Size knob because the slot forwards\n * `--uxm-list-item-media-size` into `--uxm-thumbnail-size`\n * (`LIST_ITEM_MEDIA_STYLE`), so the frame AND its placeholder glyph scale\n * together — the row owns the leading geometry, the media owns its surface.\n * `alt=\"\"` because the row's title is the accessible name; a described\n * image here would be concatenated into it.\n */\nconst DEMO_MEDIA = <Thumbnail alt=\"\" />;\n\n/**\n * Pick the canonical trailing element for the given mode. The trailing\n * slot is `ReactNode` on the atom — consumers can pass anything (Tag,\n * Badge, Avatar, custom node). The preview demos the canonical patterns:\n * - interactive → chevron icon (the \"tap to drill in\" affordance)\n * - static → Tag (status indicator for an info row)\n */\nfunction trailingForMode(\n mode: Mode,\n meta: string,\n tagType: TagType,\n): ReactNode {\n if (mode === 'interactive') {\n // Color inherits via `currentColor` from `.uxm-list-item__trailing`,\n // which reads `--uxm-list-item-chevron-color`. Keeps saves working\n // (the var has a single source of truth instead of an inline JS\n // override that would shadow it after save).\n return <Icon glyph=\"chevron-right\" size={16} />;\n }\n return <Tag type={tagType}>{meta}</Tag>;\n}\n\n/**\n * Static showcase — ONE row, rendering matches the `mode` variant.\n * - `interactive` → `<button>` with the forced-state class so all\n * state CSS rules can apply (inert via pointer-events: none and\n * tabIndex={-1}, so the forced state isn't overridden by real\n * input).\n * - `static` → `<div>` with no state class. State CSS rules are\n * scoped to button/anchor element selectors and don't fire on\n * divs, so the static row stays truly static.\n *\n * Hand-rendered because the atom doesn't expose a way to set the\n * showcase-only `--state-*` modifier class.\n */\nfunction StaticShowcase({\n mode,\n state,\n showValue,\n leading,\n}: {\n mode: Mode;\n state: string;\n showValue: boolean;\n leading: Leading;\n}) {\n const isInteractive = mode === 'interactive';\n const isActive = isInteractive && state === 'active';\n const isDisabled = state === 'disabled';\n const inner = (\n <>\n {/* Mirrors the atom's leading slot: an IconTile sized/coloured from the\n same forwarded knob vars (glyph size comes from the tile var), or the\n un-tiled media span when the Leading variant selects it. */}\n {leading === 'media' ? (\n <span className=\"uxm-list-item__media\" style={LIST_ITEM_MEDIA_STYLE}>\n {DEMO_MEDIA}\n </span>\n ) : (\n <IconTile className=\"uxm-list-item__icon\" style={LIST_ITEM_ICON_TILE_STYLE}>\n <Icon glyph=\"square\" />\n </IconTile>\n )}\n <span className=\"uxm-list-item__content\">\n <span className=\"uxm-list-item__title\">Owner</span>\n {showValue && <span className=\"uxm-list-item__value\">Alex Morgan</span>}\n </span>\n <span className=\"uxm-list-item__trailing\">\n {trailingForMode(mode, 'Verified', 'success')}\n </span>\n </>\n );\n\n return (\n <List>\n {isInteractive ? (\n <button\n type=\"button\"\n tabIndex={-1}\n disabled={isDisabled}\n aria-pressed={isActive}\n className={cn(\n 'uxm-list-item',\n isActive && 'uxm-list-item--active',\n state === 'hover' && 'uxm-list-item--state-hover',\n state === 'focus' && 'uxm-list-item--state-focus',\n )}\n style={{ pointerEvents: 'none' }}\n >\n {inner}\n </button>\n ) : (\n <div\n className=\"uxm-list-item\"\n {...(isDisabled ? { 'aria-disabled': true } : {})}\n >\n {inner}\n </div>\n )}\n </List>\n );\n}\n\nconst ROWS: Array<{\n key: string;\n title: string;\n value: string;\n meta: string;\n tagType: TagType;\n}> = [\n // One row per Tag type, with a label that matches the type's\n // semantic meaning — so two tags never read the same word with\n // different visual treatments.\n { key: 'plan', title: 'Plan', value: 'Enterprise', meta: 'Pro', tagType: 'accent' },\n { key: 'billing', title: 'Billing', value: 'Visa •• 4242', meta: 'Action needed', tagType: 'warning' },\n { key: 'owner', title: 'Owner', value: 'Alex Morgan', meta: 'Verified', tagType: 'success' },\n { key: 'activity', title: 'Last activity', value: '2 hours ago', meta: 'Recent', tagType: 'neutral' },\n];\n\nexport function ListItemPreview({ styles, variants }: PreviewProps) {\n const mode = ((variants.mode as string) ?? 'interactive') as Mode;\n const state = (variants.state as string) ?? 'default';\n const showValue = ((variants.value as string) ?? 'shown') === 'shown';\n const leading = ((variants.leading as string) ?? 'icon') as Leading;\n const [activeKey, setActiveKey] = useState<string>('plan');\n const cssVars = buildVars(styles);\n\n const sectionLabel = {\n fontSize: 11,\n color: 'var(--color-text-muted)',\n marginBottom: 12,\n letterSpacing: 0.5,\n textTransform: 'uppercase' as const,\n };\n\n return (\n <div style={{ display: 'flex', flexDirection: 'column', gap: 32, minWidth: 380, ...cssVars } as CSSProperties}>\n <div>\n <div style={sectionLabel}>{state} state</div>\n <StaticShowcase mode={mode} state={state} showValue={showValue} leading={leading} />\n </div>\n\n {/* List — interactivity is driven by the `mode` variant. Trailing\n slot is set by the preview to the canonical example for each\n mode: chevron for interactive (the \"tap to drill in\" cue),\n Tag for static (status indicator).\n interactive → all rows render as <button>; the State dropdown\n carries through to `disabled` (applied to the second row),\n and real `:hover` / `:focus-visible` exercise the production\n CSS via the projected vars above.\n static → all rows render as <div>; the interactive states\n (hover / focus / active) are filtered out of the State picker\n via the registry's showWhen. `disabled` still applies to divs\n via `[aria-disabled]`. */}\n <div style={{ borderTop: '1px solid var(--color-border)', paddingTop: 16 }}>\n <div style={sectionLabel}>{mode === 'interactive' ? 'Interactive' : 'Static'}</div>\n <List>\n {ROWS.map((row, i) => {\n const isInteractive = mode === 'interactive';\n return (\n <ListItem\n key={row.key}\n interactive={isInteractive}\n active={isInteractive && row.key === activeKey}\n // Disabled is a visual treatment that applies to both\n // interactive rows (CSS `:disabled` / `[aria-disabled]`)\n // and static rows (`[aria-disabled]` on `<div>`).\n disabled={state === 'disabled' && i === 1}\n {...(leading === 'media'\n ? { media: DEMO_MEDIA }\n : { icon: <Icon glyph=\"square\" /> })}\n value={showValue ? row.value : undefined}\n trailing={trailingForMode(mode, row.meta, row.tagType)}\n onClick={isInteractive ? () => setActiveKey(row.key) : undefined}\n >\n {row.title}\n </ListItem>\n );\n })}\n </List>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":["import_ui"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"list-item-preview.d.ts","sourceRoot":"","sources":["../../../src/ui/list/list-item-preview.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"list-item-preview.d.ts","sourceRoot":"","sources":["../../../src/ui/list/list-item-preview.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAuLrD,wBAAgB,eAAe,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,YAAY,+BAgEjE"}
|
|
@@ -4,7 +4,8 @@ import { cn } from "../../helpers/index.js";
|
|
|
4
4
|
import { Icon, IconTile } from "../index.js";
|
|
5
5
|
import { List, ListItem } from "../index.js";
|
|
6
6
|
import { Tag } from "../index.js";
|
|
7
|
-
import {
|
|
7
|
+
import { Thumbnail } from "../index.js";
|
|
8
|
+
import { LIST_ITEM_ICON_TILE_STYLE, LIST_ITEM_MEDIA_STYLE } from "./list.js";
|
|
8
9
|
function buildVars(styles) {
|
|
9
10
|
return {
|
|
10
11
|
"--uxm-list-item-inactive-bg": styles.inactiveBg,
|
|
@@ -30,9 +31,11 @@ function buildVars(styles) {
|
|
|
30
31
|
"--uxm-list-item-icon-color": styles.iconColor,
|
|
31
32
|
"--uxm-list-item-icon-size": `${styles.iconSize}px`,
|
|
32
33
|
"--uxm-list-item-icon-radius": `${styles.iconRadius}px`,
|
|
34
|
+
"--uxm-list-item-media-size": `${styles.mediaSize}px`,
|
|
33
35
|
"--uxm-list-item-chevron-color": styles.chevronColor
|
|
34
36
|
};
|
|
35
37
|
}
|
|
38
|
+
const DEMO_MEDIA = /* @__PURE__ */ jsx(Thumbnail, { alt: "" });
|
|
36
39
|
function trailingForMode(mode, meta, tagType) {
|
|
37
40
|
if (mode === "interactive") {
|
|
38
41
|
return /* @__PURE__ */ jsx(Icon, { glyph: "chevron-right", size: 16 });
|
|
@@ -42,13 +45,14 @@ function trailingForMode(mode, meta, tagType) {
|
|
|
42
45
|
function StaticShowcase({
|
|
43
46
|
mode,
|
|
44
47
|
state,
|
|
45
|
-
showValue
|
|
48
|
+
showValue,
|
|
49
|
+
leading
|
|
46
50
|
}) {
|
|
47
51
|
const isInteractive = mode === "interactive";
|
|
48
52
|
const isActive = isInteractive && state === "active";
|
|
49
53
|
const isDisabled = state === "disabled";
|
|
50
54
|
const inner = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
51
|
-
/* @__PURE__ */ jsx(IconTile, { className: "uxm-list-item__icon", style: LIST_ITEM_ICON_TILE_STYLE, children: /* @__PURE__ */ jsx(Icon, { glyph: "square" }) }),
|
|
55
|
+
leading === "media" ? /* @__PURE__ */ jsx("span", { className: "uxm-list-item__media", style: LIST_ITEM_MEDIA_STYLE, children: DEMO_MEDIA }) : /* @__PURE__ */ jsx(IconTile, { className: "uxm-list-item__icon", style: LIST_ITEM_ICON_TILE_STYLE, children: /* @__PURE__ */ jsx(Icon, { glyph: "square" }) }),
|
|
52
56
|
/* @__PURE__ */ jsxs("span", { className: "uxm-list-item__content", children: [
|
|
53
57
|
/* @__PURE__ */ jsx("span", { className: "uxm-list-item__title", children: "Owner" }),
|
|
54
58
|
showValue && /* @__PURE__ */ jsx("span", { className: "uxm-list-item__value", children: "Alex Morgan" })
|
|
@@ -93,6 +97,7 @@ function ListItemPreview({ styles, variants }) {
|
|
|
93
97
|
const mode = variants.mode ?? "interactive";
|
|
94
98
|
const state = variants.state ?? "default";
|
|
95
99
|
const showValue = (variants.value ?? "shown") === "shown";
|
|
100
|
+
const leading = variants.leading ?? "icon";
|
|
96
101
|
const [activeKey, setActiveKey] = useState("plan");
|
|
97
102
|
const cssVars = buildVars(styles);
|
|
98
103
|
const sectionLabel = {
|
|
@@ -108,7 +113,7 @@ function ListItemPreview({ styles, variants }) {
|
|
|
108
113
|
state,
|
|
109
114
|
" state"
|
|
110
115
|
] }),
|
|
111
|
-
/* @__PURE__ */ jsx(StaticShowcase, { mode, state, showValue })
|
|
116
|
+
/* @__PURE__ */ jsx(StaticShowcase, { mode, state, showValue, leading })
|
|
112
117
|
] }),
|
|
113
118
|
/* @__PURE__ */ jsxs("div", { style: { borderTop: "1px solid var(--color-border)", paddingTop: 16 }, children: [
|
|
114
119
|
/* @__PURE__ */ jsx("div", { style: sectionLabel, children: mode === "interactive" ? "Interactive" : "Static" }),
|
|
@@ -120,7 +125,7 @@ function ListItemPreview({ styles, variants }) {
|
|
|
120
125
|
interactive: isInteractive,
|
|
121
126
|
active: isInteractive && row.key === activeKey,
|
|
122
127
|
disabled: state === "disabled" && i === 1,
|
|
123
|
-
icon: /* @__PURE__ */ jsx(Icon, { glyph: "square" }),
|
|
128
|
+
...leading === "media" ? { media: DEMO_MEDIA } : { icon: /* @__PURE__ */ jsx(Icon, { glyph: "square" }) },
|
|
124
129
|
value: showValue ? row.value : void 0,
|
|
125
130
|
trailing: trailingForMode(mode, row.meta, row.tagType),
|
|
126
131
|
onClick: isInteractive ? () => setActiveKey(row.key) : void 0,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/ui/list/list-item-preview.tsx"],"sourcesContent":["import { useState, type CSSProperties, type ReactNode } from 'react';\n\nimport { cn } from '@/helpers';\nimport type { PreviewProps } from '@/previews/types';\nimport { Icon, IconTile } from '@/ui';\nimport { List, ListItem } from '@/ui';\nimport { Tag, type TagType } from '@/ui';\n\nimport { LIST_ITEM_ICON_TILE_STYLE } from './list';\n\ntype Styles = PreviewProps['styles'];\ntype Mode = 'static' | 'interactive';\n\n/**\n * Project every registry knob as a `--uxm-list-item-*` custom property\n * on the wrapper. Both the static showcase and the interactive list\n * below read from the same vars; the showcase forces a state via the\n * `--state-*` modifier class while the interactive list exercises the\n * production `:hover` / `:focus-visible` / `:disabled` rules on real\n * pointer + keyboard.\n */\nfunction buildVars(styles: Styles): CSSProperties {\n return {\n '--uxm-list-item-inactive-bg': styles.inactiveBg as string,\n '--uxm-list-item-inactive-text': styles.inactiveText as string,\n '--uxm-list-item-hover-bg': styles.hoverBg as string,\n '--uxm-list-item-hover-text': styles.hoverText as string,\n '--uxm-list-item-focus-text': styles.focusText as string,\n '--uxm-list-item-focus-ring': styles.focusRing as string,\n '--uxm-list-item-active-bg': styles.activeBg as string,\n '--uxm-list-item-active-text': styles.activeText as string,\n '--uxm-list-item-disabled-bg': styles.disabledBg as string,\n '--uxm-list-item-disabled-text': styles.disabledText as string,\n '--uxm-list-item-disabled-opacity':\n styles.disabledOpacity != null ? String(styles.disabledOpacity) : undefined,\n '--uxm-list-item-border-color': styles.borderColor as string,\n '--uxm-list-item-border-radius': `${styles.borderRadius}px`,\n '--uxm-list-item-padding-x': `${styles.paddingX}px`,\n '--uxm-list-item-padding-y': `${styles.paddingY}px`,\n '--uxm-list-item-gap': `${styles.gap}px`,\n '--uxm-list-item-font-size': `${styles.fontSize}px`,\n '--uxm-list-item-value-color': styles.valueColor as string,\n '--uxm-list-item-value-size': `${styles.valueSize}px`,\n '--uxm-list-item-icon-bg': styles.iconBg as string,\n '--uxm-list-item-icon-color': styles.iconColor as string,\n '--uxm-list-item-icon-size': `${styles.iconSize}px`,\n '--uxm-list-item-icon-radius': `${styles.iconRadius}px`,\n '--uxm-list-item-chevron-color': styles.chevronColor as string,\n } as CSSProperties;\n}\n\n/**\n * Pick the canonical trailing element for the given mode. The trailing\n * slot is `ReactNode` on the atom — consumers can pass anything (Tag,\n * Badge, Avatar, custom node). The preview demos the canonical patterns:\n * - interactive → chevron icon (the \"tap to drill in\" affordance)\n * - static → Tag (status indicator for an info row)\n */\nfunction trailingForMode(\n mode: Mode,\n meta: string,\n tagType: TagType,\n): ReactNode {\n if (mode === 'interactive') {\n // Color inherits via `currentColor` from `.uxm-list-item__trailing`,\n // which reads `--uxm-list-item-chevron-color`. Keeps saves working\n // (the var has a single source of truth instead of an inline JS\n // override that would shadow it after save).\n return <Icon glyph=\"chevron-right\" size={16} />;\n }\n return <Tag type={tagType}>{meta}</Tag>;\n}\n\n/**\n * Static showcase — ONE row, rendering matches the `mode` variant.\n * - `interactive` → `<button>` with the forced-state class so all\n * state CSS rules can apply (inert via pointer-events: none and\n * tabIndex={-1}, so the forced state isn't overridden by real\n * input).\n * - `static` → `<div>` with no state class. State CSS rules are\n * scoped to button/anchor element selectors and don't fire on\n * divs, so the static row stays truly static.\n *\n * Hand-rendered because the atom doesn't expose a way to set the\n * showcase-only `--state-*` modifier class.\n */\nfunction StaticShowcase({\n mode,\n state,\n showValue,\n}: {\n mode: Mode;\n state: string;\n showValue: boolean;\n}) {\n const isInteractive = mode === 'interactive';\n const isActive = isInteractive && state === 'active';\n const isDisabled = state === 'disabled';\n const inner = (\n <>\n {/* Mirrors the atom: the icon slot is an IconTile, sized/coloured from\n the same forwarded knob vars (glyph size comes from the tile var). */}\n <IconTile className=\"uxm-list-item__icon\" style={LIST_ITEM_ICON_TILE_STYLE}>\n <Icon glyph=\"square\" />\n </IconTile>\n <span className=\"uxm-list-item__content\">\n <span className=\"uxm-list-item__title\">Owner</span>\n {showValue && <span className=\"uxm-list-item__value\">Alex Morgan</span>}\n </span>\n <span className=\"uxm-list-item__trailing\">\n {trailingForMode(mode, 'Verified', 'success')}\n </span>\n </>\n );\n\n return (\n <List>\n {isInteractive ? (\n <button\n type=\"button\"\n tabIndex={-1}\n disabled={isDisabled}\n aria-pressed={isActive}\n className={cn(\n 'uxm-list-item',\n isActive && 'uxm-list-item--active',\n state === 'hover' && 'uxm-list-item--state-hover',\n state === 'focus' && 'uxm-list-item--state-focus',\n )}\n style={{ pointerEvents: 'none' }}\n >\n {inner}\n </button>\n ) : (\n <div\n className=\"uxm-list-item\"\n {...(isDisabled ? { 'aria-disabled': true } : {})}\n >\n {inner}\n </div>\n )}\n </List>\n );\n}\n\nconst ROWS: Array<{\n key: string;\n title: string;\n value: string;\n meta: string;\n tagType: TagType;\n}> = [\n // One row per Tag type, with a label that matches the type's\n // semantic meaning — so two tags never read the same word with\n // different visual treatments.\n { key: 'plan', title: 'Plan', value: 'Enterprise', meta: 'Pro', tagType: 'accent' },\n { key: 'billing', title: 'Billing', value: 'Visa •• 4242', meta: 'Action needed', tagType: 'warning' },\n { key: 'owner', title: 'Owner', value: 'Alex Morgan', meta: 'Verified', tagType: 'success' },\n { key: 'activity', title: 'Last activity', value: '2 hours ago', meta: 'Recent', tagType: 'neutral' },\n];\n\nexport function ListItemPreview({ styles, variants }: PreviewProps) {\n const mode = ((variants.mode as string) ?? 'interactive') as Mode;\n const state = (variants.state as string) ?? 'default';\n const showValue = ((variants.value as string) ?? 'shown') === 'shown';\n const [activeKey, setActiveKey] = useState<string>('plan');\n const cssVars = buildVars(styles);\n\n const sectionLabel = {\n fontSize: 11,\n color: 'var(--color-text-muted)',\n marginBottom: 12,\n letterSpacing: 0.5,\n textTransform: 'uppercase' as const,\n };\n\n return (\n <div style={{ display: 'flex', flexDirection: 'column', gap: 32, minWidth: 380, ...cssVars } as CSSProperties}>\n <div>\n <div style={sectionLabel}>{state} state</div>\n <StaticShowcase mode={mode} state={state} showValue={showValue} />\n </div>\n\n {/* List — interactivity is driven by the `mode` variant. Trailing\n slot is set by the preview to the canonical example for each\n mode: chevron for interactive (the \"tap to drill in\" cue),\n Tag for static (status indicator).\n interactive → all rows render as <button>; the State dropdown\n carries through to `disabled` (applied to the second row),\n and real `:hover` / `:focus-visible` exercise the production\n CSS via the projected vars above.\n static → all rows render as <div>; the interactive states\n (hover / focus / active) are filtered out of the State picker\n via the registry's showWhen. `disabled` still applies to divs\n via `[aria-disabled]`. */}\n <div style={{ borderTop: '1px solid var(--color-border)', paddingTop: 16 }}>\n <div style={sectionLabel}>{mode === 'interactive' ? 'Interactive' : 'Static'}</div>\n <List>\n {ROWS.map((row, i) => {\n const isInteractive = mode === 'interactive';\n return (\n <ListItem\n key={row.key}\n interactive={isInteractive}\n active={isInteractive && row.key === activeKey}\n // Disabled is a visual treatment that applies to both\n // interactive rows (CSS `:disabled` / `[aria-disabled]`)\n // and static rows (`[aria-disabled]` on `<div>`).\n disabled={state === 'disabled' && i === 1}\n icon={<Icon glyph=\"square\" />}\n value={showValue ? row.value : undefined}\n trailing={trailingForMode(mode, row.meta, row.tagType)}\n onClick={isInteractive ? () => setActiveKey(row.key) : undefined}\n >\n {row.title}\n </ListItem>\n );\n })}\n </List>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/ui/list/list-item-preview.tsx"],"sourcesContent":["import { useState, type CSSProperties, type ReactNode } from 'react';\n\nimport { cn } from '@/helpers';\nimport type { PreviewProps } from '@/previews/types';\nimport { Icon, IconTile } from '@/ui';\nimport { List, ListItem } from '@/ui';\nimport { Tag, type TagType } from '@/ui';\nimport { Thumbnail } from '@/ui';\n\nimport { LIST_ITEM_ICON_TILE_STYLE, LIST_ITEM_MEDIA_STYLE } from './list';\n\ntype Styles = PreviewProps['styles'];\ntype Mode = 'static' | 'interactive';\ntype Leading = 'icon' | 'media';\n\n/**\n * Project every registry knob as a `--uxm-list-item-*` custom property\n * on the wrapper. Both the static showcase and the interactive list\n * below read from the same vars; the showcase forces a state via the\n * `--state-*` modifier class while the interactive list exercises the\n * production `:hover` / `:focus-visible` / `:disabled` rules on real\n * pointer + keyboard.\n */\nfunction buildVars(styles: Styles): CSSProperties {\n return {\n '--uxm-list-item-inactive-bg': styles.inactiveBg as string,\n '--uxm-list-item-inactive-text': styles.inactiveText as string,\n '--uxm-list-item-hover-bg': styles.hoverBg as string,\n '--uxm-list-item-hover-text': styles.hoverText as string,\n '--uxm-list-item-focus-text': styles.focusText as string,\n '--uxm-list-item-focus-ring': styles.focusRing as string,\n '--uxm-list-item-active-bg': styles.activeBg as string,\n '--uxm-list-item-active-text': styles.activeText as string,\n '--uxm-list-item-disabled-bg': styles.disabledBg as string,\n '--uxm-list-item-disabled-text': styles.disabledText as string,\n '--uxm-list-item-disabled-opacity':\n styles.disabledOpacity != null ? String(styles.disabledOpacity) : undefined,\n '--uxm-list-item-border-color': styles.borderColor as string,\n '--uxm-list-item-border-radius': `${styles.borderRadius}px`,\n '--uxm-list-item-padding-x': `${styles.paddingX}px`,\n '--uxm-list-item-padding-y': `${styles.paddingY}px`,\n '--uxm-list-item-gap': `${styles.gap}px`,\n '--uxm-list-item-font-size': `${styles.fontSize}px`,\n '--uxm-list-item-value-color': styles.valueColor as string,\n '--uxm-list-item-value-size': `${styles.valueSize}px`,\n '--uxm-list-item-icon-bg': styles.iconBg as string,\n '--uxm-list-item-icon-color': styles.iconColor as string,\n '--uxm-list-item-icon-size': `${styles.iconSize}px`,\n '--uxm-list-item-icon-radius': `${styles.iconRadius}px`,\n '--uxm-list-item-media-size': `${styles.mediaSize}px`,\n '--uxm-list-item-chevron-color': styles.chevronColor as string,\n } as CSSProperties;\n}\n\n/**\n * The demo media node for the `leading: \"media\"` variant — a `Thumbnail`\n * with no `src`, so it renders the atom's own placeholder instead of\n * reaching for a network image the canvas may not be able to load. It\n * tracks the Media Size knob because the slot forwards\n * `--uxm-list-item-media-size` into `--uxm-thumbnail-size`\n * (`LIST_ITEM_MEDIA_STYLE`), so the frame AND its placeholder glyph scale\n * together — the row owns the leading geometry, the media owns its surface.\n * `alt=\"\"` because the row's title is the accessible name; a described\n * image here would be concatenated into it.\n */\nconst DEMO_MEDIA = <Thumbnail alt=\"\" />;\n\n/**\n * Pick the canonical trailing element for the given mode. The trailing\n * slot is `ReactNode` on the atom — consumers can pass anything (Tag,\n * Badge, Avatar, custom node). The preview demos the canonical patterns:\n * - interactive → chevron icon (the \"tap to drill in\" affordance)\n * - static → Tag (status indicator for an info row)\n */\nfunction trailingForMode(\n mode: Mode,\n meta: string,\n tagType: TagType,\n): ReactNode {\n if (mode === 'interactive') {\n // Color inherits via `currentColor` from `.uxm-list-item__trailing`,\n // which reads `--uxm-list-item-chevron-color`. Keeps saves working\n // (the var has a single source of truth instead of an inline JS\n // override that would shadow it after save).\n return <Icon glyph=\"chevron-right\" size={16} />;\n }\n return <Tag type={tagType}>{meta}</Tag>;\n}\n\n/**\n * Static showcase — ONE row, rendering matches the `mode` variant.\n * - `interactive` → `<button>` with the forced-state class so all\n * state CSS rules can apply (inert via pointer-events: none and\n * tabIndex={-1}, so the forced state isn't overridden by real\n * input).\n * - `static` → `<div>` with no state class. State CSS rules are\n * scoped to button/anchor element selectors and don't fire on\n * divs, so the static row stays truly static.\n *\n * Hand-rendered because the atom doesn't expose a way to set the\n * showcase-only `--state-*` modifier class.\n */\nfunction StaticShowcase({\n mode,\n state,\n showValue,\n leading,\n}: {\n mode: Mode;\n state: string;\n showValue: boolean;\n leading: Leading;\n}) {\n const isInteractive = mode === 'interactive';\n const isActive = isInteractive && state === 'active';\n const isDisabled = state === 'disabled';\n const inner = (\n <>\n {/* Mirrors the atom's leading slot: an IconTile sized/coloured from the\n same forwarded knob vars (glyph size comes from the tile var), or the\n un-tiled media span when the Leading variant selects it. */}\n {leading === 'media' ? (\n <span className=\"uxm-list-item__media\" style={LIST_ITEM_MEDIA_STYLE}>\n {DEMO_MEDIA}\n </span>\n ) : (\n <IconTile className=\"uxm-list-item__icon\" style={LIST_ITEM_ICON_TILE_STYLE}>\n <Icon glyph=\"square\" />\n </IconTile>\n )}\n <span className=\"uxm-list-item__content\">\n <span className=\"uxm-list-item__title\">Owner</span>\n {showValue && <span className=\"uxm-list-item__value\">Alex Morgan</span>}\n </span>\n <span className=\"uxm-list-item__trailing\">\n {trailingForMode(mode, 'Verified', 'success')}\n </span>\n </>\n );\n\n return (\n <List>\n {isInteractive ? (\n <button\n type=\"button\"\n tabIndex={-1}\n disabled={isDisabled}\n aria-pressed={isActive}\n className={cn(\n 'uxm-list-item',\n isActive && 'uxm-list-item--active',\n state === 'hover' && 'uxm-list-item--state-hover',\n state === 'focus' && 'uxm-list-item--state-focus',\n )}\n style={{ pointerEvents: 'none' }}\n >\n {inner}\n </button>\n ) : (\n <div\n className=\"uxm-list-item\"\n {...(isDisabled ? { 'aria-disabled': true } : {})}\n >\n {inner}\n </div>\n )}\n </List>\n );\n}\n\nconst ROWS: Array<{\n key: string;\n title: string;\n value: string;\n meta: string;\n tagType: TagType;\n}> = [\n // One row per Tag type, with a label that matches the type's\n // semantic meaning — so two tags never read the same word with\n // different visual treatments.\n { key: 'plan', title: 'Plan', value: 'Enterprise', meta: 'Pro', tagType: 'accent' },\n { key: 'billing', title: 'Billing', value: 'Visa •• 4242', meta: 'Action needed', tagType: 'warning' },\n { key: 'owner', title: 'Owner', value: 'Alex Morgan', meta: 'Verified', tagType: 'success' },\n { key: 'activity', title: 'Last activity', value: '2 hours ago', meta: 'Recent', tagType: 'neutral' },\n];\n\nexport function ListItemPreview({ styles, variants }: PreviewProps) {\n const mode = ((variants.mode as string) ?? 'interactive') as Mode;\n const state = (variants.state as string) ?? 'default';\n const showValue = ((variants.value as string) ?? 'shown') === 'shown';\n const leading = ((variants.leading as string) ?? 'icon') as Leading;\n const [activeKey, setActiveKey] = useState<string>('plan');\n const cssVars = buildVars(styles);\n\n const sectionLabel = {\n fontSize: 11,\n color: 'var(--color-text-muted)',\n marginBottom: 12,\n letterSpacing: 0.5,\n textTransform: 'uppercase' as const,\n };\n\n return (\n <div style={{ display: 'flex', flexDirection: 'column', gap: 32, minWidth: 380, ...cssVars } as CSSProperties}>\n <div>\n <div style={sectionLabel}>{state} state</div>\n <StaticShowcase mode={mode} state={state} showValue={showValue} leading={leading} />\n </div>\n\n {/* List — interactivity is driven by the `mode` variant. Trailing\n slot is set by the preview to the canonical example for each\n mode: chevron for interactive (the \"tap to drill in\" cue),\n Tag for static (status indicator).\n interactive → all rows render as <button>; the State dropdown\n carries through to `disabled` (applied to the second row),\n and real `:hover` / `:focus-visible` exercise the production\n CSS via the projected vars above.\n static → all rows render as <div>; the interactive states\n (hover / focus / active) are filtered out of the State picker\n via the registry's showWhen. `disabled` still applies to divs\n via `[aria-disabled]`. */}\n <div style={{ borderTop: '1px solid var(--color-border)', paddingTop: 16 }}>\n <div style={sectionLabel}>{mode === 'interactive' ? 'Interactive' : 'Static'}</div>\n <List>\n {ROWS.map((row, i) => {\n const isInteractive = mode === 'interactive';\n return (\n <ListItem\n key={row.key}\n interactive={isInteractive}\n active={isInteractive && row.key === activeKey}\n // Disabled is a visual treatment that applies to both\n // interactive rows (CSS `:disabled` / `[aria-disabled]`)\n // and static rows (`[aria-disabled]` on `<div>`).\n disabled={state === 'disabled' && i === 1}\n {...(leading === 'media'\n ? { media: DEMO_MEDIA }\n : { icon: <Icon glyph=\"square\" /> })}\n value={showValue ? row.value : undefined}\n trailing={trailingForMode(mode, row.meta, row.tagType)}\n onClick={isInteractive ? () => setActiveKey(row.key) : undefined}\n >\n {row.title}\n </ListItem>\n );\n })}\n </List>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
package/dist/ui/list/list.cjs
CHANGED
|
@@ -19,6 +19,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
19
19
|
var list_exports = {};
|
|
20
20
|
__export(list_exports, {
|
|
21
21
|
LIST_ITEM_ICON_TILE_STYLE: () => LIST_ITEM_ICON_TILE_STYLE,
|
|
22
|
+
LIST_ITEM_MEDIA_STYLE: () => LIST_ITEM_MEDIA_STYLE,
|
|
22
23
|
List: () => List,
|
|
23
24
|
ListItem: () => ListItem
|
|
24
25
|
});
|
|
@@ -33,11 +34,17 @@ const LIST_ITEM_ICON_TILE_STYLE = {
|
|
|
33
34
|
"--uxm-icon-tile-radius": "var(--uxm-list-item-icon-radius, 6px)",
|
|
34
35
|
"--uxm-icon-tile-icon-size": "calc(var(--uxm-list-item-icon-size, 24px) * 0.6)"
|
|
35
36
|
};
|
|
37
|
+
const LIST_ITEM_MEDIA_STYLE = {
|
|
38
|
+
"--uxm-thumbnail-size": "var(--uxm-list-item-media-size, 36px)",
|
|
39
|
+
"--uxm-avatar-size": "var(--uxm-list-item-media-size, 36px)",
|
|
40
|
+
"--uxm-avatar-small-size": "var(--uxm-list-item-media-size, 36px)"
|
|
41
|
+
};
|
|
36
42
|
function List({ children, className, ...rest }) {
|
|
37
43
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: (0, import_helpers.cn)("uxm-list", className), ...rest, children });
|
|
38
44
|
}
|
|
39
45
|
function ListItem({
|
|
40
46
|
icon,
|
|
47
|
+
media,
|
|
41
48
|
value,
|
|
42
49
|
trailing,
|
|
43
50
|
children,
|
|
@@ -55,7 +62,7 @@ function ListItem({
|
|
|
55
62
|
);
|
|
56
63
|
const isInteractive = interactive || href !== void 0;
|
|
57
64
|
const inner = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
58
|
-
icon && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icon_tile.IconTile, { className: "uxm-list-item__icon", style: LIST_ITEM_ICON_TILE_STYLE, children: icon }),
|
|
65
|
+
media ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "uxm-list-item__media", style: LIST_ITEM_MEDIA_STYLE, children: media }) : icon && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icon_tile.IconTile, { className: "uxm-list-item__icon", style: LIST_ITEM_ICON_TILE_STYLE, children: icon }),
|
|
59
66
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "uxm-list-item__content", children: [
|
|
60
67
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "uxm-list-item__title", children }),
|
|
61
68
|
value !== void 0 && value !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "uxm-list-item__value", children: value })
|
|
@@ -100,6 +107,7 @@ function ListItem({
|
|
|
100
107
|
// Annotate the CommonJS export names for ESM import in node:
|
|
101
108
|
0 && (module.exports = {
|
|
102
109
|
LIST_ITEM_ICON_TILE_STYLE,
|
|
110
|
+
LIST_ITEM_MEDIA_STYLE,
|
|
103
111
|
List,
|
|
104
112
|
ListItem
|
|
105
113
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/ui/list/list.tsx"],"sourcesContent":["import { cn } from '@/helpers';\n\nimport { IconTile } from '../icon-tile';\n\nimport type {\n AnchorHTMLAttributes,\n ButtonHTMLAttributes,\n CSSProperties,\n HTMLAttributes,\n ReactNode,\n} from 'react';\n\n/**\n * Forward the studio icon knobs (projected as `--uxm-list-item-icon-*` on the\n * row) into the `IconTile` atom's own var namespace, so the accent-tile\n * pattern lives in one place — the `IconTile` atom — instead of being\n * re-implemented in `list.scss`. The glyph tracks the tile at 0.6× (the\n * row-icon proportion used across the design system). Kept as a module\n * constant so the studio preview can reuse the exact same forwarding.\n */\nexport const LIST_ITEM_ICON_TILE_STYLE = {\n '--uxm-icon-tile-bg': 'var(--uxm-list-item-icon-bg, var(--color-accent))',\n '--uxm-icon-tile-color': 'var(--uxm-list-item-icon-color, var(--color-card))',\n '--uxm-icon-tile-size': 'var(--uxm-list-item-icon-size, 24px)',\n '--uxm-icon-tile-radius': 'var(--uxm-list-item-icon-radius, 6px)',\n '--uxm-icon-tile-icon-size': 'calc(var(--uxm-list-item-icon-size, 24px) * 0.6)',\n} as CSSProperties;\n\nexport interface ListProps extends HTMLAttributes<HTMLDivElement> {\n children: ReactNode;\n}\n\nexport function List({ children, className, ...rest }: ListProps) {\n return (\n <div className={cn('uxm-list', className)} {...rest}>\n {children}\n </div>\n );\n}\n\n/**\n * ListItem renders as one of three element types depending on `interactive`:\n * - `<div>` (default) — display row, no state styling, not focusable\n * - `<button>` — interactive without navigation; emits click events\n * - `<a>` — interactive with navigation; pass `href`\n *\n * The state knobs (hover / focus / active / disabled) in the `list-item`\n * registry only paint when the row renders as button or anchor — the CSS\n * scopes them to element-type selectors so display-only divs stay inert.\n */\nexport interface ListItemProps {\n /** Leading icon — typically <Icon ... /> from @modo/uxm/ui. */\n icon?: ReactNode;\n /** Secondary text shown under the title (e.g. a value). */\n value?: ReactNode;\n /** Trailing content — meta text, chevron icon, or a custom node. */\n trailing?: ReactNode;\n /** Primary label. */\n children: ReactNode;\n /** Render as a clickable element. When false (default), renders as a\n * static `<div>` with no state styling. When true, renders as\n * `<button>` (or `<a>` if `href` is also passed). */\n interactive?: boolean;\n /** Show the selected/active treatment (only meaningful when\n * `interactive` is true). Adds the `--active` modifier class. */\n active?: boolean;\n /** Disable interaction — only meaningful when `interactive`.\n * Forwards to the native `disabled` attribute on `<button>`, or\n * sets `aria-disabled` on `<a>`. */\n disabled?: boolean;\n /** Navigate to this URL on click. Implies `interactive`. */\n href?: string;\n className?: string;\n}\n\ntype AnchorRest = Omit<\n AnchorHTMLAttributes<HTMLAnchorElement>,\n keyof ListItemProps\n>;\ntype ButtonRest = Omit<\n ButtonHTMLAttributes<HTMLButtonElement>,\n keyof ListItemProps\n>;\ntype DivRest = Omit<HTMLAttributes<HTMLDivElement>, keyof ListItemProps>;\n\nexport function ListItem({\n icon,\n value,\n trailing,\n children,\n interactive,\n active,\n disabled,\n href,\n className,\n ...rest\n}: ListItemProps & (AnchorRest | ButtonRest | DivRest)) {\n const classes = cn(\n 'uxm-list-item',\n active && 'uxm-list-item--active',\n className,\n );\n const isInteractive = interactive || href !== undefined;\n const inner = (\n <>\n {icon
|
|
1
|
+
{"version":3,"sources":["../../../src/ui/list/list.tsx"],"sourcesContent":["import { cn } from '@/helpers';\n\nimport { IconTile } from '../icon-tile';\n\nimport type {\n AnchorHTMLAttributes,\n ButtonHTMLAttributes,\n CSSProperties,\n HTMLAttributes,\n ReactNode,\n} from 'react';\n\n/**\n * Forward the studio icon knobs (projected as `--uxm-list-item-icon-*` on the\n * row) into the `IconTile` atom's own var namespace, so the accent-tile\n * pattern lives in one place — the `IconTile` atom — instead of being\n * re-implemented in `list.scss`. The glyph tracks the tile at 0.6× (the\n * row-icon proportion used across the design system). Kept as a module\n * constant so the studio preview can reuse the exact same forwarding.\n */\nexport const LIST_ITEM_ICON_TILE_STYLE = {\n '--uxm-icon-tile-bg': 'var(--uxm-list-item-icon-bg, var(--color-accent))',\n '--uxm-icon-tile-color': 'var(--uxm-list-item-icon-color, var(--color-card))',\n '--uxm-icon-tile-size': 'var(--uxm-list-item-icon-size, 24px)',\n '--uxm-icon-tile-radius': 'var(--uxm-list-item-icon-radius, 6px)',\n '--uxm-icon-tile-icon-size': 'calc(var(--uxm-list-item-icon-size, 24px) * 0.6)',\n} as CSSProperties;\n\n/**\n * Forward the row's media-size knob into the size vars of the atoms that\n * plausibly fill the `media` slot, so each one sizes itself through its OWN\n * machinery — frame, border, radius and inner glyph / initials all scale with\n * it — instead of being stretched from the outside. Mirrors\n * `LIST_ITEM_ICON_TILE_STYLE` above, and is the only order-independent fix:\n * both `Thumbnail` (`.uxm-thumbnail`, 0,1,0 but imported AFTER list in\n * `styles.css`) and `Avatar` (double-class size presets at 0,2,0, deliberately\n * so per its own note) would otherwise win or lose the cascade against a slot\n * rule purely on stylesheet order. The generic `> *` stretch in `list.scss`\n * stays as the fallback for a bare `<img>` / `<svg>` or any atom with no size\n * var of its own; both mechanisms resolve to the same number. Kept as a module\n * constant so the studio preview can reuse the exact same forwarding.\n */\nexport const LIST_ITEM_MEDIA_STYLE = {\n '--uxm-thumbnail-size': 'var(--uxm-list-item-media-size, 36px)',\n '--uxm-avatar-size': 'var(--uxm-list-item-media-size, 36px)',\n '--uxm-avatar-small-size': 'var(--uxm-list-item-media-size, 36px)',\n} as CSSProperties;\n\nexport interface ListProps extends HTMLAttributes<HTMLDivElement> {\n children: ReactNode;\n}\n\nexport function List({ children, className, ...rest }: ListProps) {\n return (\n <div className={cn('uxm-list', className)} {...rest}>\n {children}\n </div>\n );\n}\n\n/**\n * ListItem renders as one of three element types depending on `interactive`:\n * - `<div>` (default) — display row, no state styling, not focusable\n * - `<button>` — interactive without navigation; emits click events\n * - `<a>` — interactive with navigation; pass `href`\n *\n * The state knobs (hover / focus / active / disabled) in the `list-item`\n * registry only paint when the row renders as button or anchor — the CSS\n * scopes them to element-type selectors so display-only divs stay inert.\n */\nexport interface ListItemProps {\n /** Leading icon — typically <Icon ... /> from @modo/uxm/ui. */\n icon?: ReactNode;\n /** Leading media rendered as-is (a <Thumbnail>, <img>, avatar…) — NOT\n * wrapped in the accent `IconTile` the `icon` slot uses. Mutually\n * exclusive with `icon`: when both are passed, `media` wins. The slot\n * box is `--uxm-list-item-media-size` (36px) square; a `Thumbnail` or\n * `Avatar` gets that size forwarded into its own size var (see\n * `LIST_ITEM_MEDIA_STYLE`) and anything else is stretched to fill, so\n * rows stay on one leading grid either way.\n *\n * Prefer decorative media in a row whose title already names the thing\n * (`alt=\"\"`): the slot sits INSIDE the row's `<button>`/`<a>`, so a\n * described image is concatenated into the row's accessible name.\n *\n * Note: this shadows the native `media` attribute of `<a>` for the\n * anchor form of the row — pass it through `className`/a wrapper if\n * you genuinely need the HTML attribute. */\n media?: ReactNode;\n /** Secondary text shown under the title (e.g. a value). */\n value?: ReactNode;\n /** Trailing content — meta text, chevron icon, or a custom node. */\n trailing?: ReactNode;\n /** Primary label. */\n children: ReactNode;\n /** Render as a clickable element. When false (default), renders as a\n * static `<div>` with no state styling. When true, renders as\n * `<button>` (or `<a>` if `href` is also passed). */\n interactive?: boolean;\n /** Show the selected/active treatment (only meaningful when\n * `interactive` is true). Adds the `--active` modifier class. */\n active?: boolean;\n /** Disable interaction — only meaningful when `interactive`.\n * Forwards to the native `disabled` attribute on `<button>`, or\n * sets `aria-disabled` on `<a>`. */\n disabled?: boolean;\n /** Navigate to this URL on click. Implies `interactive`. */\n href?: string;\n className?: string;\n}\n\ntype AnchorRest = Omit<\n AnchorHTMLAttributes<HTMLAnchorElement>,\n keyof ListItemProps\n>;\ntype ButtonRest = Omit<\n ButtonHTMLAttributes<HTMLButtonElement>,\n keyof ListItemProps\n>;\ntype DivRest = Omit<HTMLAttributes<HTMLDivElement>, keyof ListItemProps>;\n\nexport function ListItem({\n icon,\n media,\n value,\n trailing,\n children,\n interactive,\n active,\n disabled,\n href,\n className,\n ...rest\n}: ListItemProps & (AnchorRest | ButtonRest | DivRest)) {\n const classes = cn(\n 'uxm-list-item',\n active && 'uxm-list-item--active',\n className,\n );\n const isInteractive = interactive || href !== undefined;\n const inner = (\n <>\n {/* Leading slot. `media` renders as-is so a product photo / avatar\n keeps its own frame; `icon` keeps the accent IconTile treatment.\n `media` wins when both are passed — a row has one leading grid\n position, and the as-is rendering is the more specific intent. */}\n {media ? (\n <span className=\"uxm-list-item__media\" style={LIST_ITEM_MEDIA_STYLE}>\n {media}\n </span>\n ) : (\n icon && (\n <IconTile className=\"uxm-list-item__icon\" style={LIST_ITEM_ICON_TILE_STYLE}>\n {icon}\n </IconTile>\n )\n )}\n <span className=\"uxm-list-item__content\">\n <span className=\"uxm-list-item__title\">{children}</span>\n {value !== undefined && value !== null && (\n <span className=\"uxm-list-item__value\">{value}</span>\n )}\n </span>\n {trailing && <span className=\"uxm-list-item__trailing\">{trailing}</span>}\n </>\n );\n\n if (href !== undefined) {\n return (\n <a\n href={href}\n className={classes}\n {...(disabled ? { 'aria-disabled': true as const, tabIndex: -1 } : {})}\n {...(rest as AnchorRest)}\n >\n {inner}\n </a>\n );\n }\n if (isInteractive) {\n return (\n <button\n type=\"button\"\n className={classes}\n disabled={disabled}\n aria-pressed={active}\n {...(rest as ButtonRest)}\n >\n {inner}\n </button>\n );\n }\n return (\n <div\n className={classes}\n {...(disabled ? { 'aria-disabled': true as const } : {})}\n {...(rest as DivRest)}\n >\n {inner}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAsDI;AAtDJ,qBAAmB;AAEnB,uBAAyB;AAkBlB,MAAM,4BAA4B;AAAA,EACvC,sBAAsB;AAAA,EACtB,yBAAyB;AAAA,EACzB,wBAAwB;AAAA,EACxB,0BAA0B;AAAA,EAC1B,6BAA6B;AAC/B;AAgBO,MAAM,wBAAwB;AAAA,EACnC,wBAAwB;AAAA,EACxB,qBAAqB;AAAA,EACrB,2BAA2B;AAC7B;AAMO,SAAS,KAAK,EAAE,UAAU,WAAW,GAAG,KAAK,GAAc;AAChE,SACE,4CAAC,SAAI,eAAW,mBAAG,YAAY,SAAS,GAAI,GAAG,MAC5C,UACH;AAEJ;AA+DO,SAAS,SAAS;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAwD;AACtD,QAAM,cAAU;AAAA,IACd;AAAA,IACA,UAAU;AAAA,IACV;AAAA,EACF;AACA,QAAM,gBAAgB,eAAe,SAAS;AAC9C,QAAM,QACJ,4EAKG;AAAA,YACC,4CAAC,UAAK,WAAU,wBAAuB,OAAO,uBAC3C,iBACH,IAEA,QACE,4CAAC,6BAAS,WAAU,uBAAsB,OAAO,2BAC9C,gBACH;AAAA,IAGJ,6CAAC,UAAK,WAAU,0BACd;AAAA,kDAAC,UAAK,WAAU,wBAAwB,UAAS;AAAA,MAChD,UAAU,UAAa,UAAU,QAChC,4CAAC,UAAK,WAAU,wBAAwB,iBAAM;AAAA,OAElD;AAAA,IACC,YAAY,4CAAC,UAAK,WAAU,2BAA2B,oBAAS;AAAA,KACnE;AAGF,MAAI,SAAS,QAAW;AACtB,WACE;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,WAAW;AAAA,QACV,GAAI,WAAW,EAAE,iBAAiB,MAAe,UAAU,GAAG,IAAI,CAAC;AAAA,QACnE,GAAI;AAAA,QAEJ;AAAA;AAAA,IACH;AAAA,EAEJ;AACA,MAAI,eAAe;AACjB,WACE;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,WAAW;AAAA,QACX;AAAA,QACA,gBAAc;AAAA,QACb,GAAI;AAAA,QAEJ;AAAA;AAAA,IACH;AAAA,EAEJ;AACA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,MACV,GAAI,WAAW,EAAE,iBAAiB,KAAc,IAAI,CAAC;AAAA,MACrD,GAAI;AAAA,MAEJ;AAAA;AAAA,EACH;AAEJ;","names":[]}
|
package/dist/ui/list/list.css
CHANGED
|
@@ -55,6 +55,26 @@
|
|
|
55
55
|
which owns the accent-tile treatment. The studio icon knobs are forwarded
|
|
56
56
|
into IconTile's var namespace via `LIST_ITEM_ICON_TILE_STYLE`, so the tile
|
|
57
57
|
pattern is defined once instead of being re-implemented here. */
|
|
58
|
+
/* Media slot — the alternative leading position for a thumbnail / avatar /
|
|
59
|
+
bare <img>, rendered as-is (no IconTile, no accent fill, no colour of its
|
|
60
|
+
own: the media brings its own frame). The row still owns the geometry, so
|
|
61
|
+
every leading element lands on the same grid whichever slot filled it. */
|
|
62
|
+
}
|
|
63
|
+
.uxm-list-item__media {
|
|
64
|
+
align-items: center;
|
|
65
|
+
box-sizing: border-box;
|
|
66
|
+
display: inline-flex;
|
|
67
|
+
/* Never let a long title squeeze the media out of square. */
|
|
68
|
+
flex-shrink: 0;
|
|
69
|
+
height: var(--uxm-list-item-media-size, 36px);
|
|
70
|
+
justify-content: center;
|
|
71
|
+
width: var(--uxm-list-item-media-size, 36px);
|
|
72
|
+
/* Only for a bare <img> passed straight in — without this it would
|
|
73
|
+
stretch to the box. `Thumbnail`'s own `--cover` / `--contain` rule is
|
|
74
|
+
more specific, so its fit choice still wins. */
|
|
75
|
+
}
|
|
76
|
+
.uxm-list-item__media img {
|
|
77
|
+
object-fit: cover;
|
|
58
78
|
}
|
|
59
79
|
.uxm-list-item__content {
|
|
60
80
|
display: flex;
|
|
@@ -73,4 +93,23 @@
|
|
|
73
93
|
.uxm-list-item__trailing {
|
|
74
94
|
color: var(--uxm-list-item-chevron-color, var(--color-text-muted));
|
|
75
95
|
font-size: calc(12px * var(--type-scale, 1));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* Media fallback stretch, at (0,2,0) on purpose — same reasoning as
|
|
99
|
+
`avatar.scss`'s double-class size presets: a single-class selector here
|
|
100
|
+
(`.uxm-list-item__media > *`) TIES with a child atom's own size rule
|
|
101
|
+
(`.uxm-thumbnail`, also 0,1,0) and then loses or wins on nothing but the
|
|
102
|
+
`styles.css` import order, which is alphabetical — `Avatar` (line 7) would
|
|
103
|
+
obey the slot and `Thumbnail` (line 94) would ignore it. Two classes settle
|
|
104
|
+
it for every child regardless of order. The slot is always rendered inside a
|
|
105
|
+
row, so the descendant selector costs nothing.
|
|
106
|
+
|
|
107
|
+
Atoms with a size var of their own are ALSO handled by the forwarding in
|
|
108
|
+
`LIST_ITEM_MEDIA_STYLE` (list.tsx) — that path is what makes their border /
|
|
109
|
+
radius / inner glyph scale with the row instead of merely being clamped to
|
|
110
|
+
its box. Both resolve to the same size; this rule is the floor for a bare
|
|
111
|
+
<img>, <svg>, or a consumer's own node. */
|
|
112
|
+
.uxm-list-item .uxm-list-item__media > * {
|
|
113
|
+
height: 100%;
|
|
114
|
+
width: 100%;
|
|
76
115
|
}
|
package/dist/ui/list/list.d.ts
CHANGED
|
@@ -8,6 +8,21 @@ import type { AnchorHTMLAttributes, ButtonHTMLAttributes, CSSProperties, HTMLAtt
|
|
|
8
8
|
* constant so the studio preview can reuse the exact same forwarding.
|
|
9
9
|
*/
|
|
10
10
|
export declare const LIST_ITEM_ICON_TILE_STYLE: CSSProperties;
|
|
11
|
+
/**
|
|
12
|
+
* Forward the row's media-size knob into the size vars of the atoms that
|
|
13
|
+
* plausibly fill the `media` slot, so each one sizes itself through its OWN
|
|
14
|
+
* machinery — frame, border, radius and inner glyph / initials all scale with
|
|
15
|
+
* it — instead of being stretched from the outside. Mirrors
|
|
16
|
+
* `LIST_ITEM_ICON_TILE_STYLE` above, and is the only order-independent fix:
|
|
17
|
+
* both `Thumbnail` (`.uxm-thumbnail`, 0,1,0 but imported AFTER list in
|
|
18
|
+
* `styles.css`) and `Avatar` (double-class size presets at 0,2,0, deliberately
|
|
19
|
+
* so per its own note) would otherwise win or lose the cascade against a slot
|
|
20
|
+
* rule purely on stylesheet order. The generic `> *` stretch in `list.scss`
|
|
21
|
+
* stays as the fallback for a bare `<img>` / `<svg>` or any atom with no size
|
|
22
|
+
* var of its own; both mechanisms resolve to the same number. Kept as a module
|
|
23
|
+
* constant so the studio preview can reuse the exact same forwarding.
|
|
24
|
+
*/
|
|
25
|
+
export declare const LIST_ITEM_MEDIA_STYLE: CSSProperties;
|
|
11
26
|
export interface ListProps extends HTMLAttributes<HTMLDivElement> {
|
|
12
27
|
children: ReactNode;
|
|
13
28
|
}
|
|
@@ -25,6 +40,22 @@ export declare function List({ children, className, ...rest }: ListProps): impor
|
|
|
25
40
|
export interface ListItemProps {
|
|
26
41
|
/** Leading icon — typically <Icon ... /> from @modo/uxm/ui. */
|
|
27
42
|
icon?: ReactNode;
|
|
43
|
+
/** Leading media rendered as-is (a <Thumbnail>, <img>, avatar…) — NOT
|
|
44
|
+
* wrapped in the accent `IconTile` the `icon` slot uses. Mutually
|
|
45
|
+
* exclusive with `icon`: when both are passed, `media` wins. The slot
|
|
46
|
+
* box is `--uxm-list-item-media-size` (36px) square; a `Thumbnail` or
|
|
47
|
+
* `Avatar` gets that size forwarded into its own size var (see
|
|
48
|
+
* `LIST_ITEM_MEDIA_STYLE`) and anything else is stretched to fill, so
|
|
49
|
+
* rows stay on one leading grid either way.
|
|
50
|
+
*
|
|
51
|
+
* Prefer decorative media in a row whose title already names the thing
|
|
52
|
+
* (`alt=""`): the slot sits INSIDE the row's `<button>`/`<a>`, so a
|
|
53
|
+
* described image is concatenated into the row's accessible name.
|
|
54
|
+
*
|
|
55
|
+
* Note: this shadows the native `media` attribute of `<a>` for the
|
|
56
|
+
* anchor form of the row — pass it through `className`/a wrapper if
|
|
57
|
+
* you genuinely need the HTML attribute. */
|
|
58
|
+
media?: ReactNode;
|
|
28
59
|
/** Secondary text shown under the title (e.g. a value). */
|
|
29
60
|
value?: ReactNode;
|
|
30
61
|
/** Trailing content — meta text, chevron icon, or a custom node. */
|
|
@@ -49,6 +80,6 @@ export interface ListItemProps {
|
|
|
49
80
|
type AnchorRest = Omit<AnchorHTMLAttributes<HTMLAnchorElement>, keyof ListItemProps>;
|
|
50
81
|
type ButtonRest = Omit<ButtonHTMLAttributes<HTMLButtonElement>, keyof ListItemProps>;
|
|
51
82
|
type DivRest = Omit<HTMLAttributes<HTMLDivElement>, keyof ListItemProps>;
|
|
52
|
-
export declare function ListItem({ icon, value, trailing, children, interactive, active, disabled, href, className, ...rest }: ListItemProps & (AnchorRest | ButtonRest | DivRest)): import("react").JSX.Element;
|
|
83
|
+
export declare function ListItem({ icon, media, value, trailing, children, interactive, active, disabled, href, className, ...rest }: ListItemProps & (AnchorRest | ButtonRest | DivRest)): import("react").JSX.Element;
|
|
53
84
|
export {};
|
|
54
85
|
//# sourceMappingURL=list.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../../src/ui/list/list.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,oBAAoB,EACpB,oBAAoB,EACpB,aAAa,EACb,cAAc,EACd,SAAS,EACV,MAAM,OAAO,CAAC;AAEf;;;;;;;GAOG;AACH,eAAO,MAAM,yBAAyB,EAMjC,aAAa,CAAC;AAEnB,MAAM,WAAW,SAAU,SAAQ,cAAc,CAAC,cAAc,CAAC;IAC/D,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED,wBAAgB,IAAI,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,IAAI,EAAE,EAAE,SAAS,+BAM/D;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,aAAa;IAC5B,+DAA+D;IAC/D,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,2DAA2D;IAC3D,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,oEAAoE;IACpE,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,qBAAqB;IACrB,QAAQ,EAAE,SAAS,CAAC;IACpB;;0DAEsD;IACtD,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;sEACkE;IAClE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;yCAEqC;IACrC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,4DAA4D;IAC5D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,KAAK,UAAU,GAAG,IAAI,CACpB,oBAAoB,CAAC,iBAAiB,CAAC,EACvC,MAAM,aAAa,CACpB,CAAC;AACF,KAAK,UAAU,GAAG,IAAI,CACpB,oBAAoB,CAAC,iBAAiB,CAAC,EACvC,MAAM,aAAa,CACpB,CAAC;AACF,KAAK,OAAO,GAAG,IAAI,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,MAAM,aAAa,CAAC,CAAC;AAEzE,wBAAgB,QAAQ,CAAC,EACvB,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,WAAW,EACX,MAAM,EACN,QAAQ,EACR,IAAI,EACJ,SAAS,EACT,GAAG,IAAI,EACR,EAAE,aAAa,GAAG,CAAC,UAAU,GAAG,UAAU,GAAG,OAAO,CAAC,+
|
|
1
|
+
{"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../../src/ui/list/list.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,oBAAoB,EACpB,oBAAoB,EACpB,aAAa,EACb,cAAc,EACd,SAAS,EACV,MAAM,OAAO,CAAC;AAEf;;;;;;;GAOG;AACH,eAAO,MAAM,yBAAyB,EAMjC,aAAa,CAAC;AAEnB;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,qBAAqB,EAI7B,aAAa,CAAC;AAEnB,MAAM,WAAW,SAAU,SAAQ,cAAc,CAAC,cAAc,CAAC;IAC/D,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED,wBAAgB,IAAI,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,IAAI,EAAE,EAAE,SAAS,+BAM/D;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,aAAa;IAC5B,+DAA+D;IAC/D,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB;;;;;;;;;;;;;;iDAc6C;IAC7C,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,2DAA2D;IAC3D,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,oEAAoE;IACpE,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,qBAAqB;IACrB,QAAQ,EAAE,SAAS,CAAC;IACpB;;0DAEsD;IACtD,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;sEACkE;IAClE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;yCAEqC;IACrC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,4DAA4D;IAC5D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,KAAK,UAAU,GAAG,IAAI,CACpB,oBAAoB,CAAC,iBAAiB,CAAC,EACvC,MAAM,aAAa,CACpB,CAAC;AACF,KAAK,UAAU,GAAG,IAAI,CACpB,oBAAoB,CAAC,iBAAiB,CAAC,EACvC,MAAM,aAAa,CACpB,CAAC;AACF,KAAK,OAAO,GAAG,IAAI,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,MAAM,aAAa,CAAC,CAAC;AAEzE,wBAAgB,QAAQ,CAAC,EACvB,IAAI,EACJ,KAAK,EACL,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,WAAW,EACX,MAAM,EACN,QAAQ,EACR,IAAI,EACJ,SAAS,EACT,GAAG,IAAI,EACR,EAAE,aAAa,GAAG,CAAC,UAAU,GAAG,UAAU,GAAG,OAAO,CAAC,+BAoErD"}
|
package/dist/ui/list/list.js
CHANGED
|
@@ -8,11 +8,17 @@ const LIST_ITEM_ICON_TILE_STYLE = {
|
|
|
8
8
|
"--uxm-icon-tile-radius": "var(--uxm-list-item-icon-radius, 6px)",
|
|
9
9
|
"--uxm-icon-tile-icon-size": "calc(var(--uxm-list-item-icon-size, 24px) * 0.6)"
|
|
10
10
|
};
|
|
11
|
+
const LIST_ITEM_MEDIA_STYLE = {
|
|
12
|
+
"--uxm-thumbnail-size": "var(--uxm-list-item-media-size, 36px)",
|
|
13
|
+
"--uxm-avatar-size": "var(--uxm-list-item-media-size, 36px)",
|
|
14
|
+
"--uxm-avatar-small-size": "var(--uxm-list-item-media-size, 36px)"
|
|
15
|
+
};
|
|
11
16
|
function List({ children, className, ...rest }) {
|
|
12
17
|
return /* @__PURE__ */ jsx("div", { className: cn("uxm-list", className), ...rest, children });
|
|
13
18
|
}
|
|
14
19
|
function ListItem({
|
|
15
20
|
icon,
|
|
21
|
+
media,
|
|
16
22
|
value,
|
|
17
23
|
trailing,
|
|
18
24
|
children,
|
|
@@ -30,7 +36,7 @@ function ListItem({
|
|
|
30
36
|
);
|
|
31
37
|
const isInteractive = interactive || href !== void 0;
|
|
32
38
|
const inner = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
33
|
-
icon && /* @__PURE__ */ jsx(IconTile, { className: "uxm-list-item__icon", style: LIST_ITEM_ICON_TILE_STYLE, children: icon }),
|
|
39
|
+
media ? /* @__PURE__ */ jsx("span", { className: "uxm-list-item__media", style: LIST_ITEM_MEDIA_STYLE, children: media }) : icon && /* @__PURE__ */ jsx(IconTile, { className: "uxm-list-item__icon", style: LIST_ITEM_ICON_TILE_STYLE, children: icon }),
|
|
34
40
|
/* @__PURE__ */ jsxs("span", { className: "uxm-list-item__content", children: [
|
|
35
41
|
/* @__PURE__ */ jsx("span", { className: "uxm-list-item__title", children }),
|
|
36
42
|
value !== void 0 && value !== null && /* @__PURE__ */ jsx("span", { className: "uxm-list-item__value", children: value })
|
|
@@ -74,6 +80,7 @@ function ListItem({
|
|
|
74
80
|
}
|
|
75
81
|
export {
|
|
76
82
|
LIST_ITEM_ICON_TILE_STYLE,
|
|
83
|
+
LIST_ITEM_MEDIA_STYLE,
|
|
77
84
|
List,
|
|
78
85
|
ListItem
|
|
79
86
|
};
|
package/dist/ui/list/list.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/ui/list/list.tsx"],"sourcesContent":["import { cn } from '@/helpers';\n\nimport { IconTile } from '../icon-tile';\n\nimport type {\n AnchorHTMLAttributes,\n ButtonHTMLAttributes,\n CSSProperties,\n HTMLAttributes,\n ReactNode,\n} from 'react';\n\n/**\n * Forward the studio icon knobs (projected as `--uxm-list-item-icon-*` on the\n * row) into the `IconTile` atom's own var namespace, so the accent-tile\n * pattern lives in one place — the `IconTile` atom — instead of being\n * re-implemented in `list.scss`. The glyph tracks the tile at 0.6× (the\n * row-icon proportion used across the design system). Kept as a module\n * constant so the studio preview can reuse the exact same forwarding.\n */\nexport const LIST_ITEM_ICON_TILE_STYLE = {\n '--uxm-icon-tile-bg': 'var(--uxm-list-item-icon-bg, var(--color-accent))',\n '--uxm-icon-tile-color': 'var(--uxm-list-item-icon-color, var(--color-card))',\n '--uxm-icon-tile-size': 'var(--uxm-list-item-icon-size, 24px)',\n '--uxm-icon-tile-radius': 'var(--uxm-list-item-icon-radius, 6px)',\n '--uxm-icon-tile-icon-size': 'calc(var(--uxm-list-item-icon-size, 24px) * 0.6)',\n} as CSSProperties;\n\nexport interface ListProps extends HTMLAttributes<HTMLDivElement> {\n children: ReactNode;\n}\n\nexport function List({ children, className, ...rest }: ListProps) {\n return (\n <div className={cn('uxm-list', className)} {...rest}>\n {children}\n </div>\n );\n}\n\n/**\n * ListItem renders as one of three element types depending on `interactive`:\n * - `<div>` (default) — display row, no state styling, not focusable\n * - `<button>` — interactive without navigation; emits click events\n * - `<a>` — interactive with navigation; pass `href`\n *\n * The state knobs (hover / focus / active / disabled) in the `list-item`\n * registry only paint when the row renders as button or anchor — the CSS\n * scopes them to element-type selectors so display-only divs stay inert.\n */\nexport interface ListItemProps {\n /** Leading icon — typically <Icon ... /> from @modo/uxm/ui. */\n icon?: ReactNode;\n /** Secondary text shown under the title (e.g. a value). */\n value?: ReactNode;\n /** Trailing content — meta text, chevron icon, or a custom node. */\n trailing?: ReactNode;\n /** Primary label. */\n children: ReactNode;\n /** Render as a clickable element. When false (default), renders as a\n * static `<div>` with no state styling. When true, renders as\n * `<button>` (or `<a>` if `href` is also passed). */\n interactive?: boolean;\n /** Show the selected/active treatment (only meaningful when\n * `interactive` is true). Adds the `--active` modifier class. */\n active?: boolean;\n /** Disable interaction — only meaningful when `interactive`.\n * Forwards to the native `disabled` attribute on `<button>`, or\n * sets `aria-disabled` on `<a>`. */\n disabled?: boolean;\n /** Navigate to this URL on click. Implies `interactive`. */\n href?: string;\n className?: string;\n}\n\ntype AnchorRest = Omit<\n AnchorHTMLAttributes<HTMLAnchorElement>,\n keyof ListItemProps\n>;\ntype ButtonRest = Omit<\n ButtonHTMLAttributes<HTMLButtonElement>,\n keyof ListItemProps\n>;\ntype DivRest = Omit<HTMLAttributes<HTMLDivElement>, keyof ListItemProps>;\n\nexport function ListItem({\n icon,\n value,\n trailing,\n children,\n interactive,\n active,\n disabled,\n href,\n className,\n ...rest\n}: ListItemProps & (AnchorRest | ButtonRest | DivRest)) {\n const classes = cn(\n 'uxm-list-item',\n active && 'uxm-list-item--active',\n className,\n );\n const isInteractive = interactive || href !== undefined;\n const inner = (\n <>\n {icon
|
|
1
|
+
{"version":3,"sources":["../../../src/ui/list/list.tsx"],"sourcesContent":["import { cn } from '@/helpers';\n\nimport { IconTile } from '../icon-tile';\n\nimport type {\n AnchorHTMLAttributes,\n ButtonHTMLAttributes,\n CSSProperties,\n HTMLAttributes,\n ReactNode,\n} from 'react';\n\n/**\n * Forward the studio icon knobs (projected as `--uxm-list-item-icon-*` on the\n * row) into the `IconTile` atom's own var namespace, so the accent-tile\n * pattern lives in one place — the `IconTile` atom — instead of being\n * re-implemented in `list.scss`. The glyph tracks the tile at 0.6× (the\n * row-icon proportion used across the design system). Kept as a module\n * constant so the studio preview can reuse the exact same forwarding.\n */\nexport const LIST_ITEM_ICON_TILE_STYLE = {\n '--uxm-icon-tile-bg': 'var(--uxm-list-item-icon-bg, var(--color-accent))',\n '--uxm-icon-tile-color': 'var(--uxm-list-item-icon-color, var(--color-card))',\n '--uxm-icon-tile-size': 'var(--uxm-list-item-icon-size, 24px)',\n '--uxm-icon-tile-radius': 'var(--uxm-list-item-icon-radius, 6px)',\n '--uxm-icon-tile-icon-size': 'calc(var(--uxm-list-item-icon-size, 24px) * 0.6)',\n} as CSSProperties;\n\n/**\n * Forward the row's media-size knob into the size vars of the atoms that\n * plausibly fill the `media` slot, so each one sizes itself through its OWN\n * machinery — frame, border, radius and inner glyph / initials all scale with\n * it — instead of being stretched from the outside. Mirrors\n * `LIST_ITEM_ICON_TILE_STYLE` above, and is the only order-independent fix:\n * both `Thumbnail` (`.uxm-thumbnail`, 0,1,0 but imported AFTER list in\n * `styles.css`) and `Avatar` (double-class size presets at 0,2,0, deliberately\n * so per its own note) would otherwise win or lose the cascade against a slot\n * rule purely on stylesheet order. The generic `> *` stretch in `list.scss`\n * stays as the fallback for a bare `<img>` / `<svg>` or any atom with no size\n * var of its own; both mechanisms resolve to the same number. Kept as a module\n * constant so the studio preview can reuse the exact same forwarding.\n */\nexport const LIST_ITEM_MEDIA_STYLE = {\n '--uxm-thumbnail-size': 'var(--uxm-list-item-media-size, 36px)',\n '--uxm-avatar-size': 'var(--uxm-list-item-media-size, 36px)',\n '--uxm-avatar-small-size': 'var(--uxm-list-item-media-size, 36px)',\n} as CSSProperties;\n\nexport interface ListProps extends HTMLAttributes<HTMLDivElement> {\n children: ReactNode;\n}\n\nexport function List({ children, className, ...rest }: ListProps) {\n return (\n <div className={cn('uxm-list', className)} {...rest}>\n {children}\n </div>\n );\n}\n\n/**\n * ListItem renders as one of three element types depending on `interactive`:\n * - `<div>` (default) — display row, no state styling, not focusable\n * - `<button>` — interactive without navigation; emits click events\n * - `<a>` — interactive with navigation; pass `href`\n *\n * The state knobs (hover / focus / active / disabled) in the `list-item`\n * registry only paint when the row renders as button or anchor — the CSS\n * scopes them to element-type selectors so display-only divs stay inert.\n */\nexport interface ListItemProps {\n /** Leading icon — typically <Icon ... /> from @modo/uxm/ui. */\n icon?: ReactNode;\n /** Leading media rendered as-is (a <Thumbnail>, <img>, avatar…) — NOT\n * wrapped in the accent `IconTile` the `icon` slot uses. Mutually\n * exclusive with `icon`: when both are passed, `media` wins. The slot\n * box is `--uxm-list-item-media-size` (36px) square; a `Thumbnail` or\n * `Avatar` gets that size forwarded into its own size var (see\n * `LIST_ITEM_MEDIA_STYLE`) and anything else is stretched to fill, so\n * rows stay on one leading grid either way.\n *\n * Prefer decorative media in a row whose title already names the thing\n * (`alt=\"\"`): the slot sits INSIDE the row's `<button>`/`<a>`, so a\n * described image is concatenated into the row's accessible name.\n *\n * Note: this shadows the native `media` attribute of `<a>` for the\n * anchor form of the row — pass it through `className`/a wrapper if\n * you genuinely need the HTML attribute. */\n media?: ReactNode;\n /** Secondary text shown under the title (e.g. a value). */\n value?: ReactNode;\n /** Trailing content — meta text, chevron icon, or a custom node. */\n trailing?: ReactNode;\n /** Primary label. */\n children: ReactNode;\n /** Render as a clickable element. When false (default), renders as a\n * static `<div>` with no state styling. When true, renders as\n * `<button>` (or `<a>` if `href` is also passed). */\n interactive?: boolean;\n /** Show the selected/active treatment (only meaningful when\n * `interactive` is true). Adds the `--active` modifier class. */\n active?: boolean;\n /** Disable interaction — only meaningful when `interactive`.\n * Forwards to the native `disabled` attribute on `<button>`, or\n * sets `aria-disabled` on `<a>`. */\n disabled?: boolean;\n /** Navigate to this URL on click. Implies `interactive`. */\n href?: string;\n className?: string;\n}\n\ntype AnchorRest = Omit<\n AnchorHTMLAttributes<HTMLAnchorElement>,\n keyof ListItemProps\n>;\ntype ButtonRest = Omit<\n ButtonHTMLAttributes<HTMLButtonElement>,\n keyof ListItemProps\n>;\ntype DivRest = Omit<HTMLAttributes<HTMLDivElement>, keyof ListItemProps>;\n\nexport function ListItem({\n icon,\n media,\n value,\n trailing,\n children,\n interactive,\n active,\n disabled,\n href,\n className,\n ...rest\n}: ListItemProps & (AnchorRest | ButtonRest | DivRest)) {\n const classes = cn(\n 'uxm-list-item',\n active && 'uxm-list-item--active',\n className,\n );\n const isInteractive = interactive || href !== undefined;\n const inner = (\n <>\n {/* Leading slot. `media` renders as-is so a product photo / avatar\n keeps its own frame; `icon` keeps the accent IconTile treatment.\n `media` wins when both are passed — a row has one leading grid\n position, and the as-is rendering is the more specific intent. */}\n {media ? (\n <span className=\"uxm-list-item__media\" style={LIST_ITEM_MEDIA_STYLE}>\n {media}\n </span>\n ) : (\n icon && (\n <IconTile className=\"uxm-list-item__icon\" style={LIST_ITEM_ICON_TILE_STYLE}>\n {icon}\n </IconTile>\n )\n )}\n <span className=\"uxm-list-item__content\">\n <span className=\"uxm-list-item__title\">{children}</span>\n {value !== undefined && value !== null && (\n <span className=\"uxm-list-item__value\">{value}</span>\n )}\n </span>\n {trailing && <span className=\"uxm-list-item__trailing\">{trailing}</span>}\n </>\n );\n\n if (href !== undefined) {\n return (\n <a\n href={href}\n className={classes}\n {...(disabled ? { 'aria-disabled': true as const, tabIndex: -1 } : {})}\n {...(rest as AnchorRest)}\n >\n {inner}\n </a>\n );\n }\n if (isInteractive) {\n return (\n <button\n type=\"button\"\n className={classes}\n disabled={disabled}\n aria-pressed={active}\n {...(rest as ButtonRest)}\n >\n {inner}\n </button>\n );\n }\n return (\n <div\n className={classes}\n {...(disabled ? { 'aria-disabled': true as const } : {})}\n {...(rest as DivRest)}\n >\n {inner}\n </div>\n );\n}\n"],"mappings":"AAsDI,SAuFA,UAvFA,KAuGE,YAvGF;AAtDJ,SAAS,UAAU;AAEnB,SAAS,gBAAgB;AAkBlB,MAAM,4BAA4B;AAAA,EACvC,sBAAsB;AAAA,EACtB,yBAAyB;AAAA,EACzB,wBAAwB;AAAA,EACxB,0BAA0B;AAAA,EAC1B,6BAA6B;AAC/B;AAgBO,MAAM,wBAAwB;AAAA,EACnC,wBAAwB;AAAA,EACxB,qBAAqB;AAAA,EACrB,2BAA2B;AAC7B;AAMO,SAAS,KAAK,EAAE,UAAU,WAAW,GAAG,KAAK,GAAc;AAChE,SACE,oBAAC,SAAI,WAAW,GAAG,YAAY,SAAS,GAAI,GAAG,MAC5C,UACH;AAEJ;AA+DO,SAAS,SAAS;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAwD;AACtD,QAAM,UAAU;AAAA,IACd;AAAA,IACA,UAAU;AAAA,IACV;AAAA,EACF;AACA,QAAM,gBAAgB,eAAe,SAAS;AAC9C,QAAM,QACJ,iCAKG;AAAA,YACC,oBAAC,UAAK,WAAU,wBAAuB,OAAO,uBAC3C,iBACH,IAEA,QACE,oBAAC,YAAS,WAAU,uBAAsB,OAAO,2BAC9C,gBACH;AAAA,IAGJ,qBAAC,UAAK,WAAU,0BACd;AAAA,0BAAC,UAAK,WAAU,wBAAwB,UAAS;AAAA,MAChD,UAAU,UAAa,UAAU,QAChC,oBAAC,UAAK,WAAU,wBAAwB,iBAAM;AAAA,OAElD;AAAA,IACC,YAAY,oBAAC,UAAK,WAAU,2BAA2B,oBAAS;AAAA,KACnE;AAGF,MAAI,SAAS,QAAW;AACtB,WACE;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,WAAW;AAAA,QACV,GAAI,WAAW,EAAE,iBAAiB,MAAe,UAAU,GAAG,IAAI,CAAC;AAAA,QACnE,GAAI;AAAA,QAEJ;AAAA;AAAA,IACH;AAAA,EAEJ;AACA,MAAI,eAAe;AACjB,WACE;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,WAAW;AAAA,QACX;AAAA,QACA,gBAAc;AAAA,QACb,GAAI;AAAA,QAEJ;AAAA;AAAA,IACH;AAAA,EAEJ;AACA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,MACV,GAAI,WAAW,EAAE,iBAAiB,KAAc,IAAI,CAAC;AAAA,MACrD,GAAI;AAAA,MAEJ;AAAA;AAAA,EACH;AAEJ;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@viax.io/uxm",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.42.0",
|
|
4
4
|
"description": "Viax UXM — React 19 UI primitives and design tokens.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
"build": "tsup && npm run build:studio-css && tsc -p tsconfig.build.json && tsc-alias -p tsconfig.json --outDir dist --resolve-full-paths && node scripts/fix-cjs-requires.mjs",
|
|
61
61
|
"build:studio-css": "tailwindcss -i ./src/studio/studio.css -o ./dist/studio/studio.css --minify",
|
|
62
62
|
"build:modo": "vite build -c portal/vite.config.ts",
|
|
63
|
+
"build:cdn": "node scripts/build-cdn.mjs",
|
|
63
64
|
"dev": "tsup --watch",
|
|
64
65
|
"dev:modo": "vite -c portal/vite.config.ts",
|
|
65
66
|
"clean": "rm -rf dist",
|
|
@@ -69,6 +70,7 @@
|
|
|
69
70
|
"check:drift": "node scripts/check-state-var-drift.mjs && npm run check:type-scale && npm run check:tokens",
|
|
70
71
|
"test": "vitest run",
|
|
71
72
|
"test:watch": "vitest",
|
|
73
|
+
"test:coverage": "vitest run --coverage",
|
|
72
74
|
"test:ci": "vitest run --reporter=default --reporter=junit --outputFile=junit-report.xml",
|
|
73
75
|
"audit:report": "npm audit --registry=https://registry.npmjs.org",
|
|
74
76
|
"audit:ci": "npm audit --registry=https://registry.npmjs.org --audit-level=critical",
|
|
@@ -112,9 +114,11 @@
|
|
|
112
114
|
"@types/react": "^19.0.0",
|
|
113
115
|
"@types/react-dom": "^19.0.0",
|
|
114
116
|
"@vitejs/plugin-react": "^6.1.1",
|
|
117
|
+
"@vitest/coverage-v8": "^5.0.0",
|
|
115
118
|
"axe-core": "^4.13.0",
|
|
116
119
|
"commitizen": "^4.3.1",
|
|
117
120
|
"cz-conventional-changelog": "^3.3.0",
|
|
121
|
+
"esbuild": "^0.27.7",
|
|
118
122
|
"eslint": "^9.39.4",
|
|
119
123
|
"eslint-import-resolver-typescript": "^4.4.4",
|
|
120
124
|
"eslint-plugin-import": "^2.32.0",
|