@viax.io/uxm 4.47.0 → 4.48.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 CHANGED
@@ -1,3 +1,10 @@
1
+ # [4.48.0](https://github.com/viax-io/uxm/compare/v4.47.0...v4.48.0) (2026-09-22)
2
+
3
+
4
+ ### Features
5
+
6
+ * **list:** add a selectable (multi-select) mode to ListItem ([f8c603a](https://github.com/viax-io/uxm/commit/f8c603a956f60a68a495b51d2738255b30c8440f))
7
+
1
8
  # [4.47.0](https://github.com/viax-io/uxm/compare/v4.46.0...v4.47.0) (2026-09-22)
2
9
 
3
10
 
@@ -88,9 +88,26 @@ const listItemDef = {
88
88
  { key: "mediaSize", label: "Media Size", control: "number", defaultValue: 36, min: 24, max: 64, step: 2, unit: "px", section: "media", showWhen: { leading: "media" } }
89
89
  ],
90
90
  layoutVariants: [
91
+ {
92
+ // Selection is a MODE, not a decoration: a selectable row renders as a
93
+ // <label> around a real checkbox instead of a <button>, because a member
94
+ // of a checkable set is not a toggle button. The atom enforces the same
95
+ // exclusivity — `selected` wins over `interactive`/`href` — so the Mode
96
+ // picker below hides here rather than offering a combination that cannot
97
+ // exist.
98
+ key: "selection",
99
+ label: "Selection",
100
+ options: [
101
+ { value: "none", label: "Off" },
102
+ { value: "selectable", label: "Checkbox" }
103
+ ],
104
+ defaultValue: "none"
105
+ },
91
106
  {
92
107
  key: "mode",
93
108
  label: "Mode",
109
+ // Meaningless once the row is selectable — that mode owns the element.
110
+ showWhen: { selection: "none" },
94
111
  options: [
95
112
  // `static` → renders as `<div>`; no hover / focus / active
96
113
  // styling. `interactive` → renders as `<button>` (or `<a>`
@@ -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 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 // NOT container-gated, despite the label and the section it sits in:\n // `borderColor` maps to `--uxm-list-item-border-color`, whose only read is\n // the ROW divider (`.uxm-list-item` border-bottom). The container's own\n // border is a hardcoded `var(--color-border)` with no knob. Gating this on\n // `card` took the divider colour away from `plain` — the one presentation\n // where the dividers are the only separator left.\n { key: 'borderColor', label: 'Border Color', control: 'color', defaultValue: 'var(--color-border)', section: 'container' },\n // This one IS the container's: `--uxm-list-item-border-radius` is read only\n // by `.uxm-list`'s own radius, which `plain` removes.\n { key: 'borderRadius', label: 'Border Radius', control: 'slider', defaultValue: 8, min: 0, max: 16, step: 1, unit: 'px', section: 'container', showWhen: { container: 'card' } },\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 // The List wrapper's presentation, not the row's — but it lives on this\n // entry because `List` has no registry entry of its own and its two\n // container knobs (Border Color / Border Radius) are already here.\n key: 'container',\n label: 'Container',\n options: [\n { value: 'card', label: 'Card' },\n { value: 'plain', label: 'Plain' },\n ],\n defaultValue: 'card',\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
+ {"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 // NOT container-gated, despite the label and the section it sits in:\n // `borderColor` maps to `--uxm-list-item-border-color`, whose only read is\n // the ROW divider (`.uxm-list-item` border-bottom). The container's own\n // border is a hardcoded `var(--color-border)` with no knob. Gating this on\n // `card` took the divider colour away from `plain` — the one presentation\n // where the dividers are the only separator left.\n { key: 'borderColor', label: 'Border Color', control: 'color', defaultValue: 'var(--color-border)', section: 'container' },\n // This one IS the container's: `--uxm-list-item-border-radius` is read only\n // by `.uxm-list`'s own radius, which `plain` removes.\n { key: 'borderRadius', label: 'Border Radius', control: 'slider', defaultValue: 8, min: 0, max: 16, step: 1, unit: 'px', section: 'container', showWhen: { container: 'card' } },\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 // Selection is a MODE, not a decoration: a selectable row renders as a\n // <label> around a real checkbox instead of a <button>, because a member\n // of a checkable set is not a toggle button. The atom enforces the same\n // exclusivity — `selected` wins over `interactive`/`href` — so the Mode\n // picker below hides here rather than offering a combination that cannot\n // exist.\n key: 'selection',\n label: 'Selection',\n options: [\n { value: 'none', label: 'Off' },\n { value: 'selectable', label: 'Checkbox' },\n ],\n defaultValue: 'none',\n },\n {\n key: 'mode',\n label: 'Mode',\n // Meaningless once the row is selectable — that mode owns the element.\n showWhen: { selection: 'none' },\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 // The List wrapper's presentation, not the row's — but it lives on this\n // entry because `List` has no registry entry of its own and its two\n // container knobs (Border Color / Border Radius) are already here.\n key: 'container',\n label: 'Container',\n options: [\n { value: 'card', label: 'Card' },\n { value: 'plain', label: 'Plain' },\n ],\n defaultValue: 'card',\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,YA8IzB,CAAC"}
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,YA+JzB,CAAC"}
@@ -65,9 +65,26 @@ const listItemDef = {
65
65
  { key: "mediaSize", label: "Media Size", control: "number", defaultValue: 36, min: 24, max: 64, step: 2, unit: "px", section: "media", showWhen: { leading: "media" } }
66
66
  ],
67
67
  layoutVariants: [
68
+ {
69
+ // Selection is a MODE, not a decoration: a selectable row renders as a
70
+ // <label> around a real checkbox instead of a <button>, because a member
71
+ // of a checkable set is not a toggle button. The atom enforces the same
72
+ // exclusivity — `selected` wins over `interactive`/`href` — so the Mode
73
+ // picker below hides here rather than offering a combination that cannot
74
+ // exist.
75
+ key: "selection",
76
+ label: "Selection",
77
+ options: [
78
+ { value: "none", label: "Off" },
79
+ { value: "selectable", label: "Checkbox" }
80
+ ],
81
+ defaultValue: "none"
82
+ },
68
83
  {
69
84
  key: "mode",
70
85
  label: "Mode",
86
+ // Meaningless once the row is selectable — that mode owns the element.
87
+ showWhen: { selection: "none" },
71
88
  options: [
72
89
  // `static` → renders as `<div>`; no hover / focus / active
73
90
  // styling. `interactive` → renders as `<button>` (or `<a>`
@@ -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 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 // NOT container-gated, despite the label and the section it sits in:\n // `borderColor` maps to `--uxm-list-item-border-color`, whose only read is\n // the ROW divider (`.uxm-list-item` border-bottom). The container's own\n // border is a hardcoded `var(--color-border)` with no knob. Gating this on\n // `card` took the divider colour away from `plain` — the one presentation\n // where the dividers are the only separator left.\n { key: 'borderColor', label: 'Border Color', control: 'color', defaultValue: 'var(--color-border)', section: 'container' },\n // This one IS the container's: `--uxm-list-item-border-radius` is read only\n // by `.uxm-list`'s own radius, which `plain` removes.\n { key: 'borderRadius', label: 'Border Radius', control: 'slider', defaultValue: 8, min: 0, max: 16, step: 1, unit: 'px', section: 'container', showWhen: { container: 'card' } },\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 // The List wrapper's presentation, not the row's — but it lives on this\n // entry because `List` has no registry entry of its own and its two\n // container knobs (Border Color / Border Radius) are already here.\n key: 'container',\n label: 'Container',\n options: [\n { value: 'card', label: 'Card' },\n { value: 'plain', label: 'Plain' },\n ],\n defaultValue: 'card',\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
+ {"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 // NOT container-gated, despite the label and the section it sits in:\n // `borderColor` maps to `--uxm-list-item-border-color`, whose only read is\n // the ROW divider (`.uxm-list-item` border-bottom). The container's own\n // border is a hardcoded `var(--color-border)` with no knob. Gating this on\n // `card` took the divider colour away from `plain` — the one presentation\n // where the dividers are the only separator left.\n { key: 'borderColor', label: 'Border Color', control: 'color', defaultValue: 'var(--color-border)', section: 'container' },\n // This one IS the container's: `--uxm-list-item-border-radius` is read only\n // by `.uxm-list`'s own radius, which `plain` removes.\n { key: 'borderRadius', label: 'Border Radius', control: 'slider', defaultValue: 8, min: 0, max: 16, step: 1, unit: 'px', section: 'container', showWhen: { container: 'card' } },\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 // Selection is a MODE, not a decoration: a selectable row renders as a\n // <label> around a real checkbox instead of a <button>, because a member\n // of a checkable set is not a toggle button. The atom enforces the same\n // exclusivity — `selected` wins over `interactive`/`href` — so the Mode\n // picker below hides here rather than offering a combination that cannot\n // exist.\n key: 'selection',\n label: 'Selection',\n options: [\n { value: 'none', label: 'Off' },\n { value: 'selectable', label: 'Checkbox' },\n ],\n defaultValue: 'none',\n },\n {\n key: 'mode',\n label: 'Mode',\n // Meaningless once the row is selectable — that mode owns the element.\n showWhen: { selection: 'none' },\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 // The List wrapper's presentation, not the row's — but it lives on this\n // entry because `List` has no registry entry of its own and its two\n // container knobs (Border Color / Border Radius) are already here.\n key: 'container',\n label: 'Container',\n options: [\n { value: 'card', label: 'Card' },\n { value: 'plain', label: 'Plain' },\n ],\n defaultValue: 'card',\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":[]}
@@ -59,7 +59,10 @@ function buildVars(styles) {
59
59
  };
60
60
  }
61
61
  const DEMO_MEDIA = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_ui4.Thumbnail, { alt: "" });
62
- function trailingForMode(mode, meta, tagType) {
62
+ function trailingForMode(mode, meta, tagType, selectable) {
63
+ if (selectable) {
64
+ return meta;
65
+ }
63
66
  if (mode === "interactive") {
64
67
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_ui.Icon, { glyph: "chevron-right", size: 16 });
65
68
  }
@@ -70,18 +73,32 @@ function StaticShowcase({
70
73
  state,
71
74
  showValue,
72
75
  leading,
73
- container
76
+ container,
77
+ selectable
74
78
  }) {
75
- const isInteractive = mode === "interactive";
79
+ const isInteractive = mode === "interactive" && !selectable;
76
80
  const isActive = isInteractive && state === "active";
77
81
  const isDisabled = state === "disabled";
78
82
  const inner = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
83
+ selectable && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "uxm-list-item__check", children: [
84
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
85
+ "input",
86
+ {
87
+ type: "checkbox",
88
+ className: "uxm-checkbox__input",
89
+ checked: state !== "default",
90
+ disabled: isDisabled,
91
+ readOnly: true
92
+ }
93
+ ),
94
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "uxm-checkbox__box", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_ui.Icon, { glyph: "check", strokeWidth: 3 }) })
95
+ ] }),
79
96
  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" }) }),
80
97
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "uxm-list-item__content", children: [
81
98
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "uxm-list-item__title", children: "Owner" }),
82
99
  showValue && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "uxm-list-item__value", children: "Alex Morgan" })
83
100
  ] }),
84
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "uxm-list-item__trailing", children: trailingForMode(mode, "Verified", "success") })
101
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "uxm-list-item__trailing", children: trailingForMode(mode, "Verified", "success", selectable) })
85
102
  ] });
86
103
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_ui2.List, { variant: container, children: isInteractive ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
87
104
  "button",
@@ -99,6 +116,16 @@ function StaticShowcase({
99
116
  style: { pointerEvents: "none" },
100
117
  children: inner
101
118
  }
119
+ ) : selectable ? (
120
+ // The atom's selectable element: a <label>, never a <button>.
121
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
122
+ "label",
123
+ {
124
+ className: (0, import_helpers.cn)("uxm-list-item", "uxm-list-item--selectable"),
125
+ ...isDisabled ? { "aria-disabled": true } : {},
126
+ children: inner
127
+ }
128
+ )
102
129
  ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
103
130
  "div",
104
131
  {
@@ -123,7 +150,9 @@ function ListItemPreview({ styles, variants }) {
123
150
  const showValue = (variants.value ?? "shown") === "shown";
124
151
  const leading = variants.leading ?? "icon";
125
152
  const container = variants.container ?? "card";
153
+ const selectable = (variants.selection ?? "none") === "selectable";
126
154
  const [activeKey, setActiveKey] = (0, import_react.useState)("plan");
155
+ const [picked, setPicked] = (0, import_react.useState)(["plan", "owner"]);
127
156
  const cssVars = buildVars(styles);
128
157
  const sectionLabel = {
129
158
  fontSize: 11,
@@ -138,22 +167,29 @@ function ListItemPreview({ styles, variants }) {
138
167
  state,
139
168
  " state"
140
169
  ] }),
141
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StaticShowcase, { mode, state, showValue, leading, container })
170
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StaticShowcase, { mode, state, showValue, leading, container, selectable })
142
171
  ] }),
143
172
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { borderTop: "1px solid var(--color-border)", paddingTop: 16 }, children: [
144
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: sectionLabel, children: mode === "interactive" ? "Interactive" : "Static" }),
173
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: sectionLabel, children: selectable ? "Selectable" : mode === "interactive" ? "Interactive" : "Static" }),
145
174
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_ui2.List, { variant: container, children: ROWS.map((row, i) => {
146
175
  const isInteractive = mode === "interactive";
147
176
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
148
177
  import_ui2.ListItem,
149
178
  {
150
- interactive: isInteractive,
151
- active: isInteractive && row.key === activeKey,
179
+ ...selectable ? {
180
+ selected: picked.includes(row.key),
181
+ onSelectedChange: (on) => setPicked(
182
+ (prev) => on ? [...prev, row.key] : prev.filter((k) => k !== row.key)
183
+ )
184
+ } : {
185
+ interactive: isInteractive,
186
+ active: isInteractive && row.key === activeKey,
187
+ onClick: isInteractive ? () => setActiveKey(row.key) : void 0
188
+ },
152
189
  disabled: state === "disabled" && i === 1,
153
190
  ...leading === "media" ? { media: DEMO_MEDIA } : { icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_ui.Icon, { glyph: "square" }) },
154
191
  value: showValue ? row.value : void 0,
155
- trailing: trailingForMode(mode, row.meta, row.tagType),
156
- onClick: isInteractive ? () => setActiveKey(row.key) : void 0,
192
+ trailing: trailingForMode(mode, row.meta, row.tagType, selectable),
157
193
  children: row.title
158
194
  },
159
195
  row.key
@@ -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, type ListVariant } 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 container,\n}: {\n mode: Mode;\n state: string;\n showValue: boolean;\n leading: Leading;\n container: ListVariant;\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 variant={container}>\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 container = ((variants.container as string) ?? 'card') as ListVariant;\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} container={container} />\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 variant={container}>\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
+ {"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, type ListVariant } 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 selectable: boolean,\n): ReactNode {\n if (selectable) {\n // NOT the chevron. `mode` keeps its `interactive` default in selectable\n // mode (its picker is hidden, which does not reset the stored value), so\n // deriving trailing from it put a \"tap to drill in\" affordance on a row\n // that ticks a checkbox — two contradictory signals in the one place\n // consumers copy from. A multi-select row's trailing is meta: modo's real\n // rows put a price there.\n return meta;\n }\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 container,\n selectable,\n}: {\n mode: Mode;\n state: string;\n showValue: boolean;\n leading: Leading;\n container: ListVariant;\n selectable: boolean;\n}) {\n // A selectable row is a <label> around a checkbox — it can never be the\n // <button> this showcase renders for `interactive`. Painting a forced state\n // on the wrong element is worse than painting none: the State picker is the\n // only place hover / focus / disabled are shown at all, so a designer would\n // be tuning against a row the atom cannot produce in this mode.\n const isInteractive = mode === 'interactive' && !selectable;\n const isActive = isInteractive && state === 'active';\n const isDisabled = state === 'disabled';\n const inner = (\n <>\n {/* Mirrors the atom's selection affordance — same classes the atom\n renders, so the forced state is shown on the real element. */}\n {selectable && (\n <span className=\"uxm-list-item__check\">\n <input\n type=\"checkbox\"\n className=\"uxm-checkbox__input\"\n checked={state !== 'default'}\n disabled={isDisabled}\n readOnly\n />\n <span className=\"uxm-checkbox__box\" aria-hidden=\"true\">\n <Icon glyph=\"check\" strokeWidth={3} />\n </span>\n </span>\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', selectable)}\n </span>\n </>\n );\n\n return (\n <List variant={container}>\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 ) : selectable ? (\n // The atom's selectable element: a <label>, never a <button>.\n <label\n className={cn('uxm-list-item', 'uxm-list-item--selectable')}\n {...(isDisabled ? { 'aria-disabled': true } : {})}\n >\n {inner}\n </label>\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 container = ((variants.container as string) ?? 'card') as ListVariant;\n const selectable = ((variants.selection as string) ?? 'none') === 'selectable';\n const [activeKey, setActiveKey] = useState<string>('plan');\n // Selectable mode is controlled, so the preview has to hold the set —\n // the same thing a consumer does. Two ticked by default so the checked\n // and unchecked box are both on screen without touching anything.\n const [picked, setPicked] = useState<string[]>(['plan', 'owner']);\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} container={container} selectable={selectable} />\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}>{selectable ? 'Selectable' : mode === 'interactive' ? 'Interactive' : 'Static'}</div>\n <List variant={container}>\n {ROWS.map((row, i) => {\n const isInteractive = mode === 'interactive';\n return (\n <ListItem\n key={row.key}\n {...(selectable\n ? {\n selected: picked.includes(row.key),\n onSelectedChange: (on: boolean) =>\n setPicked((prev) =>\n on ? [...prev, row.key] : prev.filter((k) => k !== row.key),\n ),\n }\n : {\n interactive: isInteractive,\n active: isInteractive && row.key === activeKey,\n onClick: isInteractive ? () => setActiveKey(row.key) : undefined,\n })}\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, selectable)}\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;AAyLrD,wBAAgB,eAAe,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,YAAY,+BAiEjE"}
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;AAkOrD,wBAAgB,eAAe,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,YAAY,+BAgFjE"}
@@ -36,7 +36,10 @@ function buildVars(styles) {
36
36
  };
37
37
  }
38
38
  const DEMO_MEDIA = /* @__PURE__ */ jsx(Thumbnail, { alt: "" });
39
- function trailingForMode(mode, meta, tagType) {
39
+ function trailingForMode(mode, meta, tagType, selectable) {
40
+ if (selectable) {
41
+ return meta;
42
+ }
40
43
  if (mode === "interactive") {
41
44
  return /* @__PURE__ */ jsx(Icon, { glyph: "chevron-right", size: 16 });
42
45
  }
@@ -47,18 +50,32 @@ function StaticShowcase({
47
50
  state,
48
51
  showValue,
49
52
  leading,
50
- container
53
+ container,
54
+ selectable
51
55
  }) {
52
- const isInteractive = mode === "interactive";
56
+ const isInteractive = mode === "interactive" && !selectable;
53
57
  const isActive = isInteractive && state === "active";
54
58
  const isDisabled = state === "disabled";
55
59
  const inner = /* @__PURE__ */ jsxs(Fragment, { children: [
60
+ selectable && /* @__PURE__ */ jsxs("span", { className: "uxm-list-item__check", children: [
61
+ /* @__PURE__ */ jsx(
62
+ "input",
63
+ {
64
+ type: "checkbox",
65
+ className: "uxm-checkbox__input",
66
+ checked: state !== "default",
67
+ disabled: isDisabled,
68
+ readOnly: true
69
+ }
70
+ ),
71
+ /* @__PURE__ */ jsx("span", { className: "uxm-checkbox__box", "aria-hidden": "true", children: /* @__PURE__ */ jsx(Icon, { glyph: "check", strokeWidth: 3 }) })
72
+ ] }),
56
73
  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" }) }),
57
74
  /* @__PURE__ */ jsxs("span", { className: "uxm-list-item__content", children: [
58
75
  /* @__PURE__ */ jsx("span", { className: "uxm-list-item__title", children: "Owner" }),
59
76
  showValue && /* @__PURE__ */ jsx("span", { className: "uxm-list-item__value", children: "Alex Morgan" })
60
77
  ] }),
61
- /* @__PURE__ */ jsx("span", { className: "uxm-list-item__trailing", children: trailingForMode(mode, "Verified", "success") })
78
+ /* @__PURE__ */ jsx("span", { className: "uxm-list-item__trailing", children: trailingForMode(mode, "Verified", "success", selectable) })
62
79
  ] });
63
80
  return /* @__PURE__ */ jsx(List, { variant: container, children: isInteractive ? /* @__PURE__ */ jsx(
64
81
  "button",
@@ -76,6 +93,16 @@ function StaticShowcase({
76
93
  style: { pointerEvents: "none" },
77
94
  children: inner
78
95
  }
96
+ ) : selectable ? (
97
+ // The atom's selectable element: a <label>, never a <button>.
98
+ /* @__PURE__ */ jsx(
99
+ "label",
100
+ {
101
+ className: cn("uxm-list-item", "uxm-list-item--selectable"),
102
+ ...isDisabled ? { "aria-disabled": true } : {},
103
+ children: inner
104
+ }
105
+ )
79
106
  ) : /* @__PURE__ */ jsx(
80
107
  "div",
81
108
  {
@@ -100,7 +127,9 @@ function ListItemPreview({ styles, variants }) {
100
127
  const showValue = (variants.value ?? "shown") === "shown";
101
128
  const leading = variants.leading ?? "icon";
102
129
  const container = variants.container ?? "card";
130
+ const selectable = (variants.selection ?? "none") === "selectable";
103
131
  const [activeKey, setActiveKey] = useState("plan");
132
+ const [picked, setPicked] = useState(["plan", "owner"]);
104
133
  const cssVars = buildVars(styles);
105
134
  const sectionLabel = {
106
135
  fontSize: 11,
@@ -115,22 +144,29 @@ function ListItemPreview({ styles, variants }) {
115
144
  state,
116
145
  " state"
117
146
  ] }),
118
- /* @__PURE__ */ jsx(StaticShowcase, { mode, state, showValue, leading, container })
147
+ /* @__PURE__ */ jsx(StaticShowcase, { mode, state, showValue, leading, container, selectable })
119
148
  ] }),
120
149
  /* @__PURE__ */ jsxs("div", { style: { borderTop: "1px solid var(--color-border)", paddingTop: 16 }, children: [
121
- /* @__PURE__ */ jsx("div", { style: sectionLabel, children: mode === "interactive" ? "Interactive" : "Static" }),
150
+ /* @__PURE__ */ jsx("div", { style: sectionLabel, children: selectable ? "Selectable" : mode === "interactive" ? "Interactive" : "Static" }),
122
151
  /* @__PURE__ */ jsx(List, { variant: container, children: ROWS.map((row, i) => {
123
152
  const isInteractive = mode === "interactive";
124
153
  return /* @__PURE__ */ jsx(
125
154
  ListItem,
126
155
  {
127
- interactive: isInteractive,
128
- active: isInteractive && row.key === activeKey,
156
+ ...selectable ? {
157
+ selected: picked.includes(row.key),
158
+ onSelectedChange: (on) => setPicked(
159
+ (prev) => on ? [...prev, row.key] : prev.filter((k) => k !== row.key)
160
+ )
161
+ } : {
162
+ interactive: isInteractive,
163
+ active: isInteractive && row.key === activeKey,
164
+ onClick: isInteractive ? () => setActiveKey(row.key) : void 0
165
+ },
129
166
  disabled: state === "disabled" && i === 1,
130
167
  ...leading === "media" ? { media: DEMO_MEDIA } : { icon: /* @__PURE__ */ jsx(Icon, { glyph: "square" }) },
131
168
  value: showValue ? row.value : void 0,
132
- trailing: trailingForMode(mode, row.meta, row.tagType),
133
- onClick: isInteractive ? () => setActiveKey(row.key) : void 0,
169
+ trailing: trailingForMode(mode, row.meta, row.tagType, selectable),
134
170
  children: row.title
135
171
  },
136
172
  row.key
@@ -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, type ListVariant } 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 container,\n}: {\n mode: Mode;\n state: string;\n showValue: boolean;\n leading: Leading;\n container: ListVariant;\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 variant={container}>\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 container = ((variants.container as string) ?? 'card') as ListVariant;\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} container={container} />\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 variant={container}>\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":[]}
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, type ListVariant } 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 selectable: boolean,\n): ReactNode {\n if (selectable) {\n // NOT the chevron. `mode` keeps its `interactive` default in selectable\n // mode (its picker is hidden, which does not reset the stored value), so\n // deriving trailing from it put a \"tap to drill in\" affordance on a row\n // that ticks a checkbox — two contradictory signals in the one place\n // consumers copy from. A multi-select row's trailing is meta: modo's real\n // rows put a price there.\n return meta;\n }\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 container,\n selectable,\n}: {\n mode: Mode;\n state: string;\n showValue: boolean;\n leading: Leading;\n container: ListVariant;\n selectable: boolean;\n}) {\n // A selectable row is a <label> around a checkbox — it can never be the\n // <button> this showcase renders for `interactive`. Painting a forced state\n // on the wrong element is worse than painting none: the State picker is the\n // only place hover / focus / disabled are shown at all, so a designer would\n // be tuning against a row the atom cannot produce in this mode.\n const isInteractive = mode === 'interactive' && !selectable;\n const isActive = isInteractive && state === 'active';\n const isDisabled = state === 'disabled';\n const inner = (\n <>\n {/* Mirrors the atom's selection affordance — same classes the atom\n renders, so the forced state is shown on the real element. */}\n {selectable && (\n <span className=\"uxm-list-item__check\">\n <input\n type=\"checkbox\"\n className=\"uxm-checkbox__input\"\n checked={state !== 'default'}\n disabled={isDisabled}\n readOnly\n />\n <span className=\"uxm-checkbox__box\" aria-hidden=\"true\">\n <Icon glyph=\"check\" strokeWidth={3} />\n </span>\n </span>\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', selectable)}\n </span>\n </>\n );\n\n return (\n <List variant={container}>\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 ) : selectable ? (\n // The atom's selectable element: a <label>, never a <button>.\n <label\n className={cn('uxm-list-item', 'uxm-list-item--selectable')}\n {...(isDisabled ? { 'aria-disabled': true } : {})}\n >\n {inner}\n </label>\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 container = ((variants.container as string) ?? 'card') as ListVariant;\n const selectable = ((variants.selection as string) ?? 'none') === 'selectable';\n const [activeKey, setActiveKey] = useState<string>('plan');\n // Selectable mode is controlled, so the preview has to hold the set —\n // the same thing a consumer does. Two ticked by default so the checked\n // and unchecked box are both on screen without touching anything.\n const [picked, setPicked] = useState<string[]>(['plan', 'owner']);\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} container={container} selectable={selectable} />\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}>{selectable ? 'Selectable' : mode === 'interactive' ? 'Interactive' : 'Static'}</div>\n <List variant={container}>\n {ROWS.map((row, i) => {\n const isInteractive = mode === 'interactive';\n return (\n <ListItem\n key={row.key}\n {...(selectable\n ? {\n selected: picked.includes(row.key),\n onSelectedChange: (on: boolean) =>\n setPicked((prev) =>\n on ? [...prev, row.key] : prev.filter((k) => k !== row.key),\n ),\n }\n : {\n interactive: isInteractive,\n active: isInteractive && row.key === activeKey,\n onClick: isInteractive ? () => setActiveKey(row.key) : undefined,\n })}\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, selectable)}\n >\n {row.title}\n </ListItem>\n );\n })}\n </List>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -26,6 +26,7 @@ __export(list_exports, {
26
26
  module.exports = __toCommonJS(list_exports);
27
27
  var import_jsx_runtime = require("react/jsx-runtime");
28
28
  var import_helpers = require("../../helpers/index.cjs");
29
+ var import_icon = require("../icon/index.cjs");
29
30
  var import_icon_tile = require("../icon-tile/index.cjs");
30
31
  const LIST_ITEM_ICON_TILE_STYLE = {
31
32
  "--uxm-icon-tile-bg": "var(--uxm-list-item-icon-bg, var(--color-accent))",
@@ -59,16 +60,38 @@ function ListItem({
59
60
  active,
60
61
  disabled,
61
62
  href,
63
+ selected,
64
+ onSelectedChange,
62
65
  className,
63
66
  ...rest
64
67
  }) {
68
+ const isSelectable = selected !== void 0 || onSelectedChange !== void 0;
65
69
  const classes = (0, import_helpers.cn)(
66
70
  "uxm-list-item",
67
71
  active && "uxm-list-item--active",
72
+ isSelectable && "uxm-list-item--selectable",
68
73
  className
69
74
  );
70
- const isInteractive = interactive || href !== void 0;
75
+ const isInteractive = (interactive || href !== void 0) && !isSelectable;
76
+ if (process.env.NODE_ENV !== "production" && isSelectable && (interactive || href !== void 0)) {
77
+ console.warn(
78
+ "[uxm] ListItem: `selected`/`onSelectedChange` cannot be combined with `interactive`/`href` \u2014 a row is either a navigate/activate row or a member of a checkable set. Selectable mode wins."
79
+ );
80
+ }
71
81
  const inner = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
82
+ isSelectable && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "uxm-list-item__check", children: [
83
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
84
+ "input",
85
+ {
86
+ type: "checkbox",
87
+ className: "uxm-checkbox__input",
88
+ checked: selected ?? false,
89
+ disabled,
90
+ onChange: (e) => onSelectedChange?.(e.target.checked, e)
91
+ }
92
+ ),
93
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "uxm-checkbox__box", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icon.Icon, { glyph: "check", strokeWidth: 3 }) })
94
+ ] }),
72
95
  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 }),
73
96
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "uxm-list-item__content", children: [
74
97
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "uxm-list-item__title", children }),
@@ -76,6 +99,17 @@ function ListItem({
76
99
  ] }),
77
100
  trailing && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "uxm-list-item__trailing", children: trailing })
78
101
  ] });
102
+ if (isSelectable) {
103
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
104
+ "label",
105
+ {
106
+ className: classes,
107
+ ...disabled ? { "aria-disabled": true } : {},
108
+ ...rest,
109
+ children: inner
110
+ }
111
+ );
112
+ }
79
113
  if (href !== void 0) {
80
114
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
81
115
  "a",
@@ -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\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\n/** Container presentation. See `ListProps.variant`. */\nexport type ListVariant = 'card' | 'plain';\n\nexport interface ListProps extends HTMLAttributes<HTMLDivElement> {\n children: ReactNode;\n /**\n * Container presentation. `card` (default) keeps the bordered, rounded\n * surface — right for a free-standing list on a page ground. `plain` drops\n * the background, border and radius for a list that is ALREADY inside a\n * surface (a flexpane body, a card body, a disclosure section), where a card\n * container reads as a card inside a card.\n *\n * `plain` also releases `overflow` to `visible`. The card has to clip so rows\n * cannot spill past its radius; with no radius there is nothing to clip to,\n * and `hidden` would only crop what a row deliberately renders OUTSIDE the\n * container box — an overhanging corner control, a focus ring with a positive\n * offset. Clipped, those are neither painted nor hit-testable.\n *\n * Row styling is untouched — every `--uxm-list-item-*` knob behaves\n * identically in both.\n */\n variant?: ListVariant;\n}\n\nexport function List({ children, className, variant = 'card', ...rest }: ListProps) {\n return (\n <div\n className={cn('uxm-list', variant === 'plain' && 'uxm-list--plain', className)}\n {...rest}\n >\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;AA0EI;AA1EJ,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;AA0BO,SAAS,KAAK,EAAE,UAAU,WAAW,UAAU,QAAQ,GAAG,KAAK,GAAc;AAClF,SACE;AAAA,IAAC;AAAA;AAAA,MACC,eAAW,mBAAG,YAAY,YAAY,WAAW,mBAAmB,SAAS;AAAA,MAC5E,GAAG;AAAA,MAEH;AAAA;AAAA,EACH;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":[]}
1
+ {"version":3,"sources":["../../../src/ui/list/list.tsx"],"sourcesContent":["import { cn } from '@/helpers';\n\nimport { Icon } from '../icon';\nimport { IconTile } from '../icon-tile';\n\nimport type {\n AnchorHTMLAttributes,\n ChangeEvent,\n ButtonHTMLAttributes,\n CSSProperties,\n HTMLAttributes,\n LabelHTMLAttributes,\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\n/** Container presentation. See `ListProps.variant`. */\nexport type ListVariant = 'card' | 'plain';\n\nexport interface ListProps extends HTMLAttributes<HTMLDivElement> {\n children: ReactNode;\n /**\n * Container presentation. `card` (default) keeps the bordered, rounded\n * surface — right for a free-standing list on a page ground. `plain` drops\n * the background, border and radius for a list that is ALREADY inside a\n * surface (a flexpane body, a card body, a disclosure section), where a card\n * container reads as a card inside a card.\n *\n * `plain` also releases `overflow` to `visible`. The card has to clip so rows\n * cannot spill past its radius; with no radius there is nothing to clip to,\n * and `hidden` would only crop what a row deliberately renders OUTSIDE the\n * container box — an overhanging corner control, a focus ring with a positive\n * offset. Clipped, those are neither painted nor hit-testable.\n *\n * Row styling is untouched — every `--uxm-list-item-*` knob behaves\n * identically in both.\n */\n variant?: ListVariant;\n}\n\nexport function List({ children, className, variant = 'card', ...rest }: ListProps) {\n return (\n <div\n className={cn('uxm-list', variant === 'plain' && 'uxm-list--plain', className)}\n {...rest}\n >\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 /**\n * Controlled selection state. Passing `selected` OR `onSelectedChange` puts\n * the row in **selectable** mode: it renders as a `<label>` around a real\n * checkbox, so activating anywhere on the row toggles it natively — the\n * shape for \"tick several, then commit\" lists (a product picker, recipients,\n * file selection).\n *\n * Distinct from `active`, which is a *toggle button* (`aria-pressed`) —\n * the wrong semantic for a member of a checkable set. The two can still\n * combine if a consumer wants the row-fill as well as the tick.\n *\n * Mutually exclusive with `interactive` / `href`: a row either navigates or\n * is ticked, and selectable mode wins if both are passed (dev warning).\n */\n selected?: boolean;\n /** Toggle handler for selectable mode; `next` is the new checked state. */\n onSelectedChange?: (next: boolean, e: ChangeEvent<HTMLInputElement>) => void;\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// Selectable rows render a <label>, whose handlers are typed to\n// HTMLLabelElement — a DivRest cast does not fit it.\ntype LabelRest = Omit<LabelHTMLAttributes<HTMLLabelElement>, 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 selected,\n onSelectedChange,\n className,\n ...rest\n}: ListItemProps & (AnchorRest | ButtonRest | DivRest | LabelRest)) {\n const isSelectable = selected !== undefined || onSelectedChange !== undefined;\n const classes = cn(\n 'uxm-list-item',\n active && 'uxm-list-item--active',\n isSelectable && 'uxm-list-item--selectable',\n className,\n );\n const isInteractive = (interactive || href !== undefined) && !isSelectable;\n\n if (\n process.env.NODE_ENV !== 'production' &&\n isSelectable &&\n (interactive || href !== undefined)\n ) {\n \n console.warn(\n '[uxm] ListItem: `selected`/`onSelectedChange` cannot be combined with `interactive`/`href` — ' +\n 'a row is either a navigate/activate row or a member of a checkable set. Selectable mode wins.',\n );\n }\n const inner = (\n <>\n {/* Selection affordance, before the leading slot. It renders `Checkbox`'s\n OWN classes rather than nesting the component: `Checkbox` is itself a\n `<label>`, and a `<label>` inside the row `<label>` is invalid HTML\n (Chrome tolerates it — measured, one change event either way — but the\n spec does not, and assistive tech is not guaranteed to). Reusing the\n classes keeps the box pixel-identical and re-tints with the checkbox's\n own knobs, at the cost of a coupling to its internals: `list-item.test`\n asserts these classes still match what `Checkbox` renders, so a change\n there fails here rather than shipping an unstyled box. */}\n {isSelectable && (\n <span className=\"uxm-list-item__check\">\n <input\n type=\"checkbox\"\n className=\"uxm-checkbox__input\"\n // `?? false`, never a bare `undefined`: selectable mode is\n // documented as controlled, and a consumer deriving it from async\n // data (`selected={data?.picked.includes(id)}`) would otherwise\n // hand React an uncontrolled input that turns controlled the\n // moment the data lands — React warns, and until it does the DOM\n // owns the checked state, so a tick made while loading is taken\n // over instead of reported. `RadioGroup` pins its own mode for the\n // component's lifetime for exactly this reason.\n checked={selected ?? false}\n disabled={disabled}\n onChange={(e) => onSelectedChange?.(e.target.checked, e)}\n />\n <span className=\"uxm-checkbox__box\" aria-hidden=\"true\">\n <Icon glyph=\"check\" strokeWidth={3} />\n </span>\n </span>\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 (isSelectable) {\n // A real `<label>`: whole-row activation is the browser's, not a click\n // handler's, so it works for pointer and keyboard and needs no ARIA of its\n // own. The row is NOT `aria-pressed` — that is a toggle button, the wrong\n // semantic for one member of a checkable set.\n return (\n <label\n className={classes}\n {...(disabled ? { 'aria-disabled': true as const } : {})}\n {...(rest as LabelRest)}\n >\n {inner}\n </label>\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":"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","names":[]}
@@ -71,6 +71,21 @@
71
71
  which owns the accent-tile treatment. The studio icon knobs are forwarded
72
72
  into IconTile's var namespace via `LIST_ITEM_ICON_TILE_STYLE`, so the tile
73
73
  pattern is defined once instead of being re-implemented here. */
74
+ /* Selection affordance. The box itself is `Checkbox`'s — same classes, so
75
+ it paints from `--uxm-checkbox-*` and re-tints with it; this slot only
76
+ places it. Sized by the checkbox's own knob rather than a second one, so
77
+ a themed checkbox stays one size everywhere. */
78
+ }
79
+ .uxm-list-item__check {
80
+ align-items: center;
81
+ display: inline-flex;
82
+ flex-shrink: 0;
83
+ /* Containing block for the visually-hidden input, which `checkbox.scss`
84
+ positions absolutely — without this it would resolve against the row
85
+ and drag the focus ring with it. */
86
+ position: relative;
87
+ }
88
+ .uxm-list-item {
74
89
  /* Media slot — the alternative leading position for a thumbnail / avatar /
75
90
  bare <img>, rendered as-is (no IconTile, no accent fill, no colour of its
76
91
  own: the media brings its own frame). The row still owns the geometry, so
@@ -111,6 +126,16 @@
111
126
  font-size: calc(12px * var(--type-scale, 1));
112
127
  }
113
128
 
129
+ /* A selectable row is a <label>, so it wants the pointer a button row gets —
130
+ the base block is a <div> by default and declares none. */
131
+ .uxm-list-item--selectable {
132
+ cursor: pointer;
133
+ }
134
+
135
+ .uxm-list-item--selectable:has(.uxm-checkbox__input:disabled) {
136
+ cursor: not-allowed;
137
+ }
138
+
114
139
  /* Media fallback stretch, at (0,2,0) on purpose — same reasoning as
115
140
  `avatar.scss`'s double-class size presets: a single-class selector here
116
141
  (`.uxm-list-item__media > *`) TIES with a child atom's own size rule
@@ -1,4 +1,4 @@
1
- import type { AnchorHTMLAttributes, ButtonHTMLAttributes, CSSProperties, HTMLAttributes, ReactNode } from 'react';
1
+ import type { AnchorHTMLAttributes, ChangeEvent, ButtonHTMLAttributes, CSSProperties, HTMLAttributes, LabelHTMLAttributes, ReactNode } from 'react';
2
2
  /**
3
3
  * Forward the studio icon knobs (projected as `--uxm-list-item-icon-*` on the
4
4
  * row) into the `IconTile` atom's own var namespace, so the accent-tile
@@ -94,11 +94,29 @@ export interface ListItemProps {
94
94
  disabled?: boolean;
95
95
  /** Navigate to this URL on click. Implies `interactive`. */
96
96
  href?: string;
97
+ /**
98
+ * Controlled selection state. Passing `selected` OR `onSelectedChange` puts
99
+ * the row in **selectable** mode: it renders as a `<label>` around a real
100
+ * checkbox, so activating anywhere on the row toggles it natively — the
101
+ * shape for "tick several, then commit" lists (a product picker, recipients,
102
+ * file selection).
103
+ *
104
+ * Distinct from `active`, which is a *toggle button* (`aria-pressed`) —
105
+ * the wrong semantic for a member of a checkable set. The two can still
106
+ * combine if a consumer wants the row-fill as well as the tick.
107
+ *
108
+ * Mutually exclusive with `interactive` / `href`: a row either navigates or
109
+ * is ticked, and selectable mode wins if both are passed (dev warning).
110
+ */
111
+ selected?: boolean;
112
+ /** Toggle handler for selectable mode; `next` is the new checked state. */
113
+ onSelectedChange?: (next: boolean, e: ChangeEvent<HTMLInputElement>) => void;
97
114
  className?: string;
98
115
  }
99
116
  type AnchorRest = Omit<AnchorHTMLAttributes<HTMLAnchorElement>, keyof ListItemProps>;
100
117
  type ButtonRest = Omit<ButtonHTMLAttributes<HTMLButtonElement>, keyof ListItemProps>;
101
118
  type DivRest = Omit<HTMLAttributes<HTMLDivElement>, keyof ListItemProps>;
102
- export declare function ListItem({ icon, media, value, trailing, children, interactive, active, disabled, href, className, ...rest }: ListItemProps & (AnchorRest | ButtonRest | DivRest)): import("react").JSX.Element;
119
+ type LabelRest = Omit<LabelHTMLAttributes<HTMLLabelElement>, keyof ListItemProps>;
120
+ export declare function ListItem({ icon, media, value, trailing, children, interactive, active, disabled, href, selected, onSelectedChange, className, ...rest }: ListItemProps & (AnchorRest | ButtonRest | DivRest | LabelRest)): import("react").JSX.Element;
103
121
  export {};
104
122
  //# 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;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,qBAAqB,EAI7B,aAAa,CAAC;AAEnB,uDAAuD;AACvD,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,OAAO,CAAC;AAE3C,MAAM,WAAW,SAAU,SAAQ,cAAc,CAAC,cAAc,CAAC;IAC/D,QAAQ,EAAE,SAAS,CAAC;IACpB;;;;;;;;;;;;;;;OAeG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC;CACvB;AAED,wBAAgB,IAAI,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAgB,EAAE,GAAG,IAAI,EAAE,EAAE,SAAS,+BASjF;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"}
1
+ {"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../../src/ui/list/list.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,oBAAoB,EACpB,WAAW,EACX,oBAAoB,EACpB,aAAa,EACb,cAAc,EACd,mBAAmB,EACnB,SAAS,EACV,MAAM,OAAO,CAAC;AAEf;;;;;;;GAOG;AACH,eAAO,MAAM,yBAAyB,EAMjC,aAAa,CAAC;AAEnB;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,qBAAqB,EAI7B,aAAa,CAAC;AAEnB,uDAAuD;AACvD,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,OAAO,CAAC;AAE3C,MAAM,WAAW,SAAU,SAAQ,cAAc,CAAC,cAAc,CAAC;IAC/D,QAAQ,EAAE,SAAS,CAAC;IACpB;;;;;;;;;;;;;;;OAeG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC;CACvB;AAED,wBAAgB,IAAI,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAgB,EAAE,GAAG,IAAI,EAAE,EAAE,SAAS,+BASjF;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;;;;;;;;;;;;;OAaG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,2EAA2E;IAC3E,gBAAgB,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,CAAC,EAAE,WAAW,CAAC,gBAAgB,CAAC,KAAK,IAAI,CAAC;IAC7E,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;AAGzE,KAAK,SAAS,GAAG,IAAI,CAAC,mBAAmB,CAAC,gBAAgB,CAAC,EAAE,MAAM,aAAa,CAAC,CAAC;AAElF,wBAAgB,QAAQ,CAAC,EACvB,IAAI,EACJ,KAAK,EACL,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,WAAW,EACX,MAAM,EACN,QAAQ,EACR,IAAI,EACJ,QAAQ,EACR,gBAAgB,EAChB,SAAS,EACT,GAAG,IAAI,EACR,EAAE,aAAa,GAAG,CAAC,UAAU,GAAG,UAAU,GAAG,OAAO,GAAG,SAAS,CAAC,+BAiIjE"}
@@ -1,5 +1,6 @@
1
1
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
2
  import { cn } from "../../helpers/index.js";
3
+ import { Icon } from "../icon/index.js";
3
4
  import { IconTile } from "../icon-tile/index.js";
4
5
  const LIST_ITEM_ICON_TILE_STYLE = {
5
6
  "--uxm-icon-tile-bg": "var(--uxm-list-item-icon-bg, var(--color-accent))",
@@ -33,16 +34,38 @@ function ListItem({
33
34
  active,
34
35
  disabled,
35
36
  href,
37
+ selected,
38
+ onSelectedChange,
36
39
  className,
37
40
  ...rest
38
41
  }) {
42
+ const isSelectable = selected !== void 0 || onSelectedChange !== void 0;
39
43
  const classes = cn(
40
44
  "uxm-list-item",
41
45
  active && "uxm-list-item--active",
46
+ isSelectable && "uxm-list-item--selectable",
42
47
  className
43
48
  );
44
- const isInteractive = interactive || href !== void 0;
49
+ const isInteractive = (interactive || href !== void 0) && !isSelectable;
50
+ if (process.env.NODE_ENV !== "production" && isSelectable && (interactive || href !== void 0)) {
51
+ console.warn(
52
+ "[uxm] ListItem: `selected`/`onSelectedChange` cannot be combined with `interactive`/`href` \u2014 a row is either a navigate/activate row or a member of a checkable set. Selectable mode wins."
53
+ );
54
+ }
45
55
  const inner = /* @__PURE__ */ jsxs(Fragment, { children: [
56
+ isSelectable && /* @__PURE__ */ jsxs("span", { className: "uxm-list-item__check", children: [
57
+ /* @__PURE__ */ jsx(
58
+ "input",
59
+ {
60
+ type: "checkbox",
61
+ className: "uxm-checkbox__input",
62
+ checked: selected ?? false,
63
+ disabled,
64
+ onChange: (e) => onSelectedChange?.(e.target.checked, e)
65
+ }
66
+ ),
67
+ /* @__PURE__ */ jsx("span", { className: "uxm-checkbox__box", "aria-hidden": "true", children: /* @__PURE__ */ jsx(Icon, { glyph: "check", strokeWidth: 3 }) })
68
+ ] }),
46
69
  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 }),
47
70
  /* @__PURE__ */ jsxs("span", { className: "uxm-list-item__content", children: [
48
71
  /* @__PURE__ */ jsx("span", { className: "uxm-list-item__title", children }),
@@ -50,6 +73,17 @@ function ListItem({
50
73
  ] }),
51
74
  trailing && /* @__PURE__ */ jsx("span", { className: "uxm-list-item__trailing", children: trailing })
52
75
  ] });
76
+ if (isSelectable) {
77
+ return /* @__PURE__ */ jsx(
78
+ "label",
79
+ {
80
+ className: classes,
81
+ ...disabled ? { "aria-disabled": true } : {},
82
+ ...rest,
83
+ children: inner
84
+ }
85
+ );
86
+ }
53
87
  if (href !== void 0) {
54
88
  return /* @__PURE__ */ jsx(
55
89
  "a",
@@ -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\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\n/** Container presentation. See `ListProps.variant`. */\nexport type ListVariant = 'card' | 'plain';\n\nexport interface ListProps extends HTMLAttributes<HTMLDivElement> {\n children: ReactNode;\n /**\n * Container presentation. `card` (default) keeps the bordered, rounded\n * surface — right for a free-standing list on a page ground. `plain` drops\n * the background, border and radius for a list that is ALREADY inside a\n * surface (a flexpane body, a card body, a disclosure section), where a card\n * container reads as a card inside a card.\n *\n * `plain` also releases `overflow` to `visible`. The card has to clip so rows\n * cannot spill past its radius; with no radius there is nothing to clip to,\n * and `hidden` would only crop what a row deliberately renders OUTSIDE the\n * container box — an overhanging corner control, a focus ring with a positive\n * offset. Clipped, those are neither painted nor hit-testable.\n *\n * Row styling is untouched — every `--uxm-list-item-*` knob behaves\n * identically in both.\n */\n variant?: ListVariant;\n}\n\nexport function List({ children, className, variant = 'card', ...rest }: ListProps) {\n return (\n <div\n className={cn('uxm-list', variant === 'plain' && 'uxm-list--plain', className)}\n {...rest}\n >\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":"AA0EI,SA0FA,UA1FA,KA0GE,YA1GF;AA1EJ,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;AA0BO,SAAS,KAAK,EAAE,UAAU,WAAW,UAAU,QAAQ,GAAG,KAAK,GAAc;AAClF,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,GAAG,YAAY,YAAY,WAAW,mBAAmB,SAAS;AAAA,MAC5E,GAAG;AAAA,MAEH;AAAA;AAAA,EACH;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":[]}
1
+ {"version":3,"sources":["../../../src/ui/list/list.tsx"],"sourcesContent":["import { cn } from '@/helpers';\n\nimport { Icon } from '../icon';\nimport { IconTile } from '../icon-tile';\n\nimport type {\n AnchorHTMLAttributes,\n ChangeEvent,\n ButtonHTMLAttributes,\n CSSProperties,\n HTMLAttributes,\n LabelHTMLAttributes,\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\n/** Container presentation. See `ListProps.variant`. */\nexport type ListVariant = 'card' | 'plain';\n\nexport interface ListProps extends HTMLAttributes<HTMLDivElement> {\n children: ReactNode;\n /**\n * Container presentation. `card` (default) keeps the bordered, rounded\n * surface — right for a free-standing list on a page ground. `plain` drops\n * the background, border and radius for a list that is ALREADY inside a\n * surface (a flexpane body, a card body, a disclosure section), where a card\n * container reads as a card inside a card.\n *\n * `plain` also releases `overflow` to `visible`. The card has to clip so rows\n * cannot spill past its radius; with no radius there is nothing to clip to,\n * and `hidden` would only crop what a row deliberately renders OUTSIDE the\n * container box — an overhanging corner control, a focus ring with a positive\n * offset. Clipped, those are neither painted nor hit-testable.\n *\n * Row styling is untouched — every `--uxm-list-item-*` knob behaves\n * identically in both.\n */\n variant?: ListVariant;\n}\n\nexport function List({ children, className, variant = 'card', ...rest }: ListProps) {\n return (\n <div\n className={cn('uxm-list', variant === 'plain' && 'uxm-list--plain', className)}\n {...rest}\n >\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 /**\n * Controlled selection state. Passing `selected` OR `onSelectedChange` puts\n * the row in **selectable** mode: it renders as a `<label>` around a real\n * checkbox, so activating anywhere on the row toggles it natively — the\n * shape for \"tick several, then commit\" lists (a product picker, recipients,\n * file selection).\n *\n * Distinct from `active`, which is a *toggle button* (`aria-pressed`) —\n * the wrong semantic for a member of a checkable set. The two can still\n * combine if a consumer wants the row-fill as well as the tick.\n *\n * Mutually exclusive with `interactive` / `href`: a row either navigates or\n * is ticked, and selectable mode wins if both are passed (dev warning).\n */\n selected?: boolean;\n /** Toggle handler for selectable mode; `next` is the new checked state. */\n onSelectedChange?: (next: boolean, e: ChangeEvent<HTMLInputElement>) => void;\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// Selectable rows render a <label>, whose handlers are typed to\n// HTMLLabelElement — a DivRest cast does not fit it.\ntype LabelRest = Omit<LabelHTMLAttributes<HTMLLabelElement>, 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 selected,\n onSelectedChange,\n className,\n ...rest\n}: ListItemProps & (AnchorRest | ButtonRest | DivRest | LabelRest)) {\n const isSelectable = selected !== undefined || onSelectedChange !== undefined;\n const classes = cn(\n 'uxm-list-item',\n active && 'uxm-list-item--active',\n isSelectable && 'uxm-list-item--selectable',\n className,\n );\n const isInteractive = (interactive || href !== undefined) && !isSelectable;\n\n if (\n process.env.NODE_ENV !== 'production' &&\n isSelectable &&\n (interactive || href !== undefined)\n ) {\n \n console.warn(\n '[uxm] ListItem: `selected`/`onSelectedChange` cannot be combined with `interactive`/`href` — ' +\n 'a row is either a navigate/activate row or a member of a checkable set. Selectable mode wins.',\n );\n }\n const inner = (\n <>\n {/* Selection affordance, before the leading slot. It renders `Checkbox`'s\n OWN classes rather than nesting the component: `Checkbox` is itself a\n `<label>`, and a `<label>` inside the row `<label>` is invalid HTML\n (Chrome tolerates it — measured, one change event either way — but the\n spec does not, and assistive tech is not guaranteed to). Reusing the\n classes keeps the box pixel-identical and re-tints with the checkbox's\n own knobs, at the cost of a coupling to its internals: `list-item.test`\n asserts these classes still match what `Checkbox` renders, so a change\n there fails here rather than shipping an unstyled box. */}\n {isSelectable && (\n <span className=\"uxm-list-item__check\">\n <input\n type=\"checkbox\"\n className=\"uxm-checkbox__input\"\n // `?? false`, never a bare `undefined`: selectable mode is\n // documented as controlled, and a consumer deriving it from async\n // data (`selected={data?.picked.includes(id)}`) would otherwise\n // hand React an uncontrolled input that turns controlled the\n // moment the data lands — React warns, and until it does the DOM\n // owns the checked state, so a tick made while loading is taken\n // over instead of reported. `RadioGroup` pins its own mode for the\n // component's lifetime for exactly this reason.\n checked={selected ?? false}\n disabled={disabled}\n onChange={(e) => onSelectedChange?.(e.target.checked, e)}\n />\n <span className=\"uxm-checkbox__box\" aria-hidden=\"true\">\n <Icon glyph=\"check\" strokeWidth={3} />\n </span>\n </span>\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 (isSelectable) {\n // A real `<label>`: whole-row activation is the browser's, not a click\n // handler's, so it works for pointer and keyboard and needs no ARIA of its\n // own. The row is NOT `aria-pressed` — that is a toggle button, the wrong\n // semantic for one member of a checkable set.\n return (\n <label\n className={classes}\n {...(disabled ? { 'aria-disabled': true as const } : {})}\n {...(rest as LabelRest)}\n >\n {inner}\n </label>\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":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viax.io/uxm",
3
- "version": "4.47.0",
3
+ "version": "4.48.0",
4
4
  "description": "Viax UXM — React 19 UI primitives and design tokens.",
5
5
  "license": "MIT",
6
6
  "private": false,