@adia-ai/web-components 0.6.47 → 0.6.48

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.
Files changed (62) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/components/badge/badge.d.ts +14 -0
  3. package/components/button/button.a2ui.json +1 -4
  4. package/components/button/button.d.ts +1 -1
  5. package/components/button/button.yaml +0 -3
  6. package/components/calendar-grid/calendar-grid.css +20 -11
  7. package/components/calendar-picker/calendar-picker.css +19 -10
  8. package/components/card/card.a2ui.json +2 -5
  9. package/components/card/card.css +3 -1
  10. package/components/card/card.d.ts +2 -2
  11. package/components/card/card.yaml +2 -5
  12. package/components/date-range-picker/date-range-picker.css +10 -1
  13. package/components/heatmap/heatmap.a2ui.json +2 -0
  14. package/components/heatmap/heatmap.d.ts +1 -1
  15. package/components/heatmap/heatmap.yaml +2 -0
  16. package/components/index.js +1 -0
  17. package/components/preview/preview.a2ui.json +93 -0
  18. package/components/preview/preview.class.js +178 -0
  19. package/components/preview/preview.css +176 -0
  20. package/components/preview/preview.d.ts +24 -0
  21. package/components/preview/preview.js +22 -0
  22. package/components/preview/preview.yaml +100 -0
  23. package/components/progress/progress.a2ui.json +2 -7
  24. package/components/progress/progress.d.ts +2 -2
  25. package/components/progress/progress.yaml +3 -8
  26. package/components/progress-row/progress-row.a2ui.json +1 -3
  27. package/components/progress-row/progress-row.d.ts +1 -1
  28. package/components/progress-row/progress-row.yaml +0 -2
  29. package/components/select/select.a2ui.json +2 -4
  30. package/components/select/select.yaml +2 -2
  31. package/components/tabs/tabs.a2ui.json +1 -4
  32. package/components/tabs/tabs.d.ts +2 -2
  33. package/components/tabs/tabs.yaml +2 -2
  34. package/core/anchor.js +5 -1
  35. package/dist/web-components.min.css +1 -1
  36. package/dist/web-components.min.js +75 -73
  37. package/index.css +6 -6
  38. package/package.json +1 -1
  39. package/styles/README.md +71 -36
  40. package/styles/api/layout.css +19 -0
  41. package/styles/api/sizing.css +225 -0
  42. package/styles/api/text.css +106 -0
  43. package/styles/colors/semantics/aliases.css +32 -0
  44. package/styles/colors/semantics/buckets.css +64 -0
  45. package/styles/colors/semantics/core.css +317 -0
  46. package/styles/colors/semantics/data-viz.css +129 -0
  47. package/styles/colors/semantics/features.css +114 -0
  48. package/styles/colors/semantics.css +10 -619
  49. package/styles/components.css +1 -0
  50. package/styles/foundation/elevation.css +29 -0
  51. package/styles/foundation/index.css +11 -0
  52. package/styles/foundation/motion.css +10 -0
  53. package/styles/foundation/radius.css +27 -0
  54. package/styles/foundation/size.css +33 -0
  55. package/styles/foundation/space.css +47 -0
  56. package/styles/index.css +14 -0
  57. package/styles/resets.css +17 -25
  58. package/styles/tokens.css +16 -384
  59. package/styles/type/elements.css +225 -0
  60. package/styles/type/roles.css +419 -0
  61. package/styles/type/scale.css +89 -0
  62. package/styles/typography.css +11 -809
package/CHANGELOG.md CHANGED
@@ -1,5 +1,47 @@
1
1
  # Changelog — @adia-ai/web-components
2
2
 
3
+ ## [0.6.48] — 2026-05-29
4
+
5
+ ### Demos — library-wide consolidation sweep (HTML-first)
6
+
7
+ - **`components/**/*.examples.html` + `traits/**/*.examples.html`** — Usage-section consolidation across 83 components + 45 traits: every example now shows its own auto-stamped HTML, so the standalone Usage cheat-sheet was redundant. Per-page consolidations (text, card, drawer) + implemented-matrix depth fills. See the root CHANGELOG for the full campaign.
8
+
9
+ ### Changed — foundation styles reorganized by dimension × layer (ADR-0035)
10
+
11
+ - **`styles/`** — the foundation token surface is now organized by **dimension × layer**. `tokens.css`, `typography.css`, and `colors/semantics.css` remain as **compat barrels** at their published paths (the `@adia-ai/web-components/styles/*` export map and the `<link>` surface are unchanged); their content moved to single-responsibility files — `foundation/{space,size,radius,motion,elevation}.css`, `type/{scale,roles,elements}.css`, `colors/semantics/{core,buckets,features,data-viz,aliases}.css` — and the global attribute API to `api/{sizing,text,layout}.css`. New `styles/index.css` is the real barrel; the package-root `index.css` re-exports it. **Behavior-neutral** — identical custom-property declaration multiset in the minified bundle (see ADR-0035). Fixed in passing: the dead `--ui-focus` reference in the global `:focus-visible` fallback, and element-typography defaults consolidated into `type/elements.css`.
12
+ - **`scripts/release/check-foundation-layer-placement.mjs`** — new audit (wired into `npm run check`) enforcing the layout: header tags present, primitives free of attribute selectors, the global attribute API confined to `api/`, no orphan files.
13
+ - **`dist/web-components.min.css`** — bundle rebuild reflecting the reorg (no behavioral change).
14
+
15
+ ### Fixed — `<date-range-picker>` popover clipped its second month
16
+
17
+ - **`core/anchor.js`** — the anchored-popover viewport-safety cap was a hard
18
+ `max-width: min(100vw - 1rem, 32rem)`. It's now `min(100vw - 1rem,
19
+ var(--popover-max-width, 32rem))`, so a wide-content popover can raise the
20
+ 32rem cap by setting `--popover-max-width` on the popover element (default
21
+ unchanged → ordinary popovers like `popover-ui` / `menu-ui` / `select-ui` keep
22
+ the 512px cap). Viewport safety still wins.
23
+ - **`components/date-range-picker/date-range-picker.css`** — the preset rail plus
24
+ two side-by-side month panes need ~656px, but the 32rem (512px) cap clipped the
25
+ second month inside the popover. The popover now opts into
26
+ `--popover-max-width: 48rem` (new `--date-range-picker-popover-max-width` token),
27
+ and `[data-calendar-area]` gains `flex-wrap: wrap` so the second pane wraps below
28
+ the first when the viewport clamps the popover narrower than two months (instead
29
+ of clipping). Fixes `<date-range-selector>` too (it embeds the picker). 32/32
30
+ picker tests pass; no regression on other popovers.
31
+
32
+ ### Changed — `<preview-ui>` (docs primitive)
33
+
34
+ - **Side-by-side is the default + self-correcting.** A bare `<preview-ui>` stamps
35
+ `layout="split"` (render | code); a render cell wider than its half-width cell
36
+ downgrades to stacked after layout (rAF + `ResizeObserver`: `split`→`layout="stack"`,
37
+ or a row gains `[data-stack]`), and `overflow-x:auto` lets a page-scale composite
38
+ scroll rather than clip. New `layout="stack"` enum value + CSS.
39
+ - **`dedent` indentation fix** — measures the common indent from the body (lines
40
+ after the first) so markup captured with the opening tag at column 0 no longer
41
+ renders its children deeply over-indented.
42
+ - **`[data-preview-unwrap]`** — multi-element rows show their children's markup, not
43
+ the throwaway layout wrapper, in the code pane.
44
+
3
45
  ## [0.6.47] — 2026-05-29
4
46
 
5
47
  ### Added — `<card-ui>` `[grow]` section attribute
@@ -29,6 +29,20 @@ export class UIBadge extends UIElement {
29
29
  text: string;
30
30
  /** Badge display text. Renderer routes this to the `text` attribute via CSS attr(text) on ::after. */
31
31
  textContent: string;
32
+ /** Fill style — orthogonal to [variant]. Badge defaults to `muted`
33
+ (quiet metadata is the primitive's identity — counts, IDs, status
34
+ pills in dense rows). Three values:
35
+ - `muted` (default) — tinted bg + scheme-paired text. Same as
36
+ the existing family-variant rules.
37
+ - `solid` — saturated bg + on-strong text. Use for hero badges
38
+ where the badge IS the state (e.g. a single inline error). The
39
+ existing `primary` variant is a shortcut for accent + solid.
40
+ - `outline` — transparent bg + family-colored border + family-
41
+ colored text. Lightest visual weight; good in dense data rows.
42
+ Vocabulary mirrors `<tag-ui>` (which defaults to solid, given its
43
+ different role as filter / autocomplete chip).
44
+ */
45
+ tone: 'muted' | 'solid' | 'outline';
32
46
  /** Semantic color variant. */
33
47
  variant: 'default' | 'accent' | 'info' | 'success' | 'warning' | 'danger' | 'primary' | 'muted' | 'neutral';
34
48
  }
@@ -88,10 +88,7 @@
88
88
  "solid",
89
89
  "outline",
90
90
  "ghost",
91
- "primary",
92
- "secondary",
93
- "soft",
94
- "current"
91
+ "primary"
95
92
  ],
96
93
  "default": "solid"
97
94
  }
@@ -33,7 +33,7 @@ export class UIButton extends UIElement {
33
33
  textContent: string;
34
34
  /** Visual style — `solid` (default fill), `outline`, `ghost`. `default` / `primary` are aliases of `solid`. Style is independent of semantic intent — to express destructive / success / info / warning intent, set [color="…"] alongside.
35
35
  For **inline navigation** (Terms of Service, Privacy Policy, footer links, "Sign in" / "Sign up" cross-page affordances) use `<link-ui>` instead — it carries proper `<a href>` semantics, keyboard handling (Enter only, no Space), middle-click open-new-tab, and screen-reader announces "link" instead of "button". Mixing navigation and action affordances under the same primitive is a category error fixed at this junction. */
36
- variant: 'default' | 'solid' | 'outline' | 'ghost' | 'primary' | 'secondary' | 'soft' | 'current';
36
+ variant: 'default' | 'solid' | 'outline' | 'ghost' | 'primary';
37
37
 
38
38
  addEventListener<K extends keyof HTMLElementEventMap>(
39
39
  type: K,
@@ -80,9 +80,6 @@ props:
80
80
  - outline
81
81
  - ghost
82
82
  - primary
83
- - secondary
84
- - soft
85
- - current
86
83
  color:
87
84
  description: >-
88
85
  Semantic intent — composes with [variant]. `<button-ui variant="solid" color="danger">`
@@ -15,9 +15,12 @@
15
15
  @scope (calendar-grid-ui) {
16
16
  /* ── Block 1 — TOKENS ── */
17
17
  :where(:scope) {
18
- /* Layout */
18
+ /* Layout
19
+ Width derives from the day-cell size so the grid scales with the
20
+ universal [size] system (sm/md/lg) exactly like select/input/button:
21
+ 7 columns at --a-size + 6 inter-cell gaps' worth of breathing room. */
19
22
  --calendar-grid-gap-default: var(--a-space-1);
20
- --calendar-grid-width-default: 16rem;
23
+ --calendar-grid-width-default: calc(7 * var(--a-size) + 6 * var(--a-space-1));
21
24
 
22
25
  /* Header */
23
26
  --calendar-grid-header-gap-default: var(--a-space-1);
@@ -26,26 +29,32 @@
26
29
  --calendar-grid-title-size-default: var(--a-ui-size);
27
30
  --calendar-grid-title-weight-default: var(--a-weight-medium);
28
31
 
29
- /* Nav buttons (prev/next) */
30
- --calendar-grid-nav-size-default: 1.5rem;
32
+ /* Nav buttons (prev/next) — secondary chrome at one notch below the
33
+ control height (mirrors select's `calc(height - gap)` idiom); icon
34
+ is a directional chevron sized from the universal --a-caret-size. */
35
+ --calendar-grid-nav-size-default: calc(var(--a-size) - var(--a-space-2));
31
36
  --calendar-grid-nav-radius-default: var(--a-radius-sm);
32
37
  --calendar-grid-nav-bg-default: transparent;
33
38
  --calendar-grid-nav-bg-hover-default: var(--a-bg-muted);
34
39
  --calendar-grid-nav-fg-default: var(--a-fg-muted);
35
40
  --calendar-grid-nav-fg-hover-default: var(--a-fg);
36
- --calendar-grid-nav-icon-size-default: 0.75rem;
41
+ --calendar-grid-nav-icon-size-default: var(--a-caret-size);
37
42
 
38
- /* Weekday header */
43
+ /* Weekday header — row height tracks the control height (one notch
44
+ below, like nav); label font scales with the tier via --a-ui-size
45
+ (de-emphasis comes from color + weight, not a frozen small size). */
39
46
  --calendar-grid-weekday-fg-default: var(--a-fg-muted);
40
- --calendar-grid-weekday-size-default: var(--a-ui-sm);
47
+ --calendar-grid-weekday-size-default: var(--a-ui-size);
41
48
  --calendar-grid-weekday-weight-default: var(--a-weight-medium);
42
- --calendar-grid-weekday-height-default: 1.5rem;
49
+ --calendar-grid-weekday-height-default: calc(var(--a-size) - var(--a-space-2));
43
50
  --calendar-grid-weekday-mb-default: var(--a-space-1);
44
51
 
45
- /* Day cells */
46
- --calendar-grid-day-size-default: 2rem;
52
+ /* Day cells — the interactive targets size from --a-size (control
53
+ height) and scale their digits with --a-ui-size, exactly like a
54
+ button/select row. */
55
+ --calendar-grid-day-size-default: var(--a-size);
47
56
  --calendar-grid-day-radius-default: var(--a-radius);
48
- --calendar-grid-day-font-size-default: var(--a-ui-sm);
57
+ --calendar-grid-day-font-size-default: var(--a-ui-size);
49
58
  --calendar-grid-day-bg-default: transparent;
50
59
  --calendar-grid-day-fg-default: var(--a-fg-subtle);
51
60
  --calendar-grid-day-bg-hover-default: var(--a-bg-hover);
@@ -30,7 +30,10 @@
30
30
  --calendar-picker-popover-radius-default: var(--a-radius-lg);
31
31
  --calendar-picker-popover-shadow-default: var(--a-shadow-lg);
32
32
  --calendar-picker-popover-padding-default: var(--a-space-2);
33
- --calendar-picker-popover-width-default: 16rem;
33
+ /* Width derives from the day-grid so the popover scales with [size]:
34
+ 7 columns at --a-size + inter-cell breathing + popover padding both
35
+ sides (border-box). Matches calendar-grid-ui's width formula. */
36
+ --calendar-picker-popover-width-default: calc(7 * var(--a-size) + 6 * var(--a-space-1) + 2 * var(--a-space-2));
34
37
  --calendar-picker-popover-fg-default: var(--a-fg);
35
38
 
36
39
  /* Header */
@@ -40,26 +43,32 @@
40
43
  --calendar-picker-title-size-default: var(--a-ui-size);
41
44
  --calendar-picker-title-weight-default: var(--a-weight-medium);
42
45
 
43
- /* Nav buttons (prev/next) */
44
- --calendar-picker-nav-size-default: 1.5rem;
46
+ /* Nav buttons (prev/next) — secondary chrome at one notch below the
47
+ control height (mirrors select's `calc(height - gap)` idiom); icon
48
+ is a directional chevron sized from the universal --a-caret-size. */
49
+ --calendar-picker-nav-size-default: calc(var(--a-size) - var(--a-space-2));
45
50
  --calendar-picker-nav-radius-default: var(--a-radius-sm);
46
51
  --calendar-picker-nav-bg-default: transparent;
47
52
  --calendar-picker-nav-bg-hover-default: var(--a-bg-muted);
48
53
  --calendar-picker-nav-fg-default: var(--a-fg-muted);
49
54
  --calendar-picker-nav-fg-hover-default: var(--a-fg);
50
- --calendar-picker-nav-icon-size-default: 0.75rem;
55
+ --calendar-picker-nav-icon-size-default: var(--a-caret-size);
51
56
 
52
- /* Weekday header */
57
+ /* Weekday header — row height tracks the control height (one notch
58
+ below, like nav); label font scales with the tier via --a-ui-size
59
+ (de-emphasis comes from color + weight, not a frozen small size). */
53
60
  --calendar-picker-weekday-fg-default: var(--a-fg-muted);
54
- --calendar-picker-weekday-size-default: var(--a-ui-sm);
61
+ --calendar-picker-weekday-size-default: var(--a-ui-size);
55
62
  --calendar-picker-weekday-weight-default: var(--a-weight-medium);
56
- --calendar-picker-weekday-height-default: 1.5rem;
63
+ --calendar-picker-weekday-height-default: calc(var(--a-size) - var(--a-space-2));
57
64
  --calendar-picker-weekday-mb-default: var(--a-space-1);
58
65
 
59
- /* Day cells */
60
- --calendar-picker-day-size-default: 2rem;
66
+ /* Day cells — the interactive targets size from --a-size (control
67
+ height) and scale their digits with --a-ui-size, exactly like a
68
+ button/select row. */
69
+ --calendar-picker-day-size-default: var(--a-size);
61
70
  --calendar-picker-day-radius-default: var(--a-radius);
62
- --calendar-picker-day-font-size-default: var(--a-ui-sm);
71
+ --calendar-picker-day-font-size-default: var(--a-ui-size);
63
72
  --calendar-picker-day-bg-default: transparent;
64
73
  --calendar-picker-day-fg-default: var(--a-fg-subtle);
65
74
  --calendar-picker-day-bg-hover-default: var(--a-bg-hover);
@@ -53,17 +53,14 @@
53
53
  "default": ""
54
54
  },
55
55
  "variant": {
56
- "description": "Visual style. `outline` is an alias for `outlined`; `flat` removes shadow; `soft`/`primary` apply tinted surfaces.",
56
+ "description": "Visual style. `outlined` (alias `outline`) draws a border with no shadow; `filled` uses a tinted canvas surface; `ghost` drops border + shadow.",
57
57
  "type": "string",
58
58
  "enum": [
59
59
  "default",
60
60
  "outlined",
61
61
  "outline",
62
62
  "filled",
63
- "ghost",
64
- "flat",
65
- "soft",
66
- "primary"
63
+ "ghost"
67
64
  ],
68
65
  "default": "default"
69
66
  }
@@ -66,7 +66,9 @@
66
66
 
67
67
  /* ═══════ Variants — token-only overrides ═══════ */
68
68
 
69
- :scope[variant="outlined"] {
69
+ /* `outline` is a documented alias for `outlined` (yaml). */
70
+ :scope[variant="outlined"],
71
+ :scope[variant="outline"] {
70
72
  --card-bg-default: transparent;
71
73
  --card-shadow-default: none;
72
74
  --card-border-default: 1px solid var(--a-border);
@@ -23,6 +23,6 @@ export class UICard extends UIElement {
23
23
  raw: boolean;
24
24
  /** Card scale. Controls inset, radius, gap, and font sizes. Default (empty) = md. */
25
25
  size: 'sm' | 'md' | 'lg';
26
- /** Visual style. `outline` is an alias for `outlined`; `flat` removes shadow; `soft`/`primary` apply tinted surfaces. */
27
- variant: 'default' | 'outlined' | 'outline' | 'filled' | 'ghost' | 'flat' | 'soft' | 'primary';
26
+ /** Visual style. `outlined` (alias `outline`) draws a border with no shadow; `filled` uses a tinted canvas surface; `ghost` drops border + shadow. */
27
+ variant: 'default' | 'outlined' | 'outline' | 'filled' | 'ghost';
28
28
  }
@@ -44,8 +44,8 @@ props:
44
44
  - md
45
45
  - lg
46
46
  variant:
47
- description: Visual style. `outline` is an alias for `outlined`; `flat` removes shadow; `soft`/`primary` apply tinted
48
- surfaces.
47
+ description: Visual style. `outlined` (alias `outline`) draws a border with no
48
+ shadow; `filled` uses a tinted canvas surface; `ghost` drops border + shadow.
49
49
  type: string
50
50
  default: default
51
51
  enum:
@@ -54,9 +54,6 @@ props:
54
54
  - outline
55
55
  - filled
56
56
  - ghost
57
- - flat
58
- - soft
59
- - primary
60
57
  events: {}
61
58
  slots:
62
59
  icon:
@@ -21,6 +21,9 @@
21
21
  --date-range-picker-popover-shadow-default: var(--a-shadow-lg);
22
22
  --date-range-picker-popover-padding-default: var(--a-space-3);
23
23
  --date-range-picker-popover-gap-default: var(--a-space-3);
24
+ /* Wide enough for the preset rail + two side-by-side month panes; raises
25
+ anchor.js's default 32rem cap (which clipped the second month). */
26
+ --date-range-picker-popover-max-width-default: 48rem;
24
27
 
25
28
  /* Preset rail */
26
29
  --date-range-picker-preset-bg-default: transparent;
@@ -132,6 +135,9 @@
132
135
  the popover-ui pattern that uses `:not(:popover-open) { display: none }`. */
133
136
  date-range-picker-ui [slot="popover"] {
134
137
  margin: 0;
138
+ /* Opt into a wider anchor.js max-width cap so both month panes fit side-by-side
139
+ (the 32rem default clipped the second month). Viewport safety still applies. */
140
+ --popover-max-width: var(--date-range-picker-popover-max-width, var(--date-range-picker-popover-max-width-default));
135
141
  padding: var(--date-range-picker-py, var(--date-range-picker-py-default)) var(--date-range-picker-px, var(--date-range-picker-px-default));
136
142
  border: 1px solid var(--date-range-picker-popover-border, var(--date-range-picker-popover-border-default));
137
143
  border-radius: var(--date-range-picker-popover-radius, var(--date-range-picker-popover-radius-default));
@@ -188,10 +194,13 @@ date-range-picker-ui [data-preset-rail] button-ui {
188
194
  justify-content: flex-start;
189
195
  }
190
196
 
191
- /* Calendar area — horizontal layout of two calendar panes. */
197
+ /* Calendar area — horizontal layout of two calendar panes. Wraps the second
198
+ pane below the first when the popover is viewport-clamped narrower than two
199
+ months (instead of clipping it past the popover's right edge). */
192
200
  date-range-picker-ui [data-calendar-area] {
193
201
  grid-column: 2;
194
202
  display: flex;
203
+ flex-wrap: wrap;
195
204
  gap: var(--a-space-3);
196
205
  align-items: flex-start;
197
206
  }
@@ -39,6 +39,8 @@
39
39
  "accent",
40
40
  "success",
41
41
  "warning",
42
+ "danger",
43
+ "info",
42
44
  "data-ramp"
43
45
  ],
44
46
  "default": "data-ramp"
@@ -24,7 +24,7 @@ export class UIHeatmap extends UIElement {
24
24
  /** Aspect ratio */
25
25
  aspect: 'square' | 'wide';
26
26
  /** Color ramp */
27
- colorScheme: 'accent' | 'success' | 'warning' | 'data-ramp';
27
+ colorScheme: 'accent' | 'success' | 'warning' | 'danger' | 'info' | 'data-ramp';
28
28
  /** Column count */
29
29
  cols: number;
30
30
  /** Hide the Less/More legend strip */
@@ -33,6 +33,8 @@ props:
33
33
  - accent
34
34
  - success
35
35
  - warning
36
+ - danger
37
+ - info
36
38
  - data-ramp
37
39
  attribute: color-scheme
38
40
  cols:
@@ -87,6 +87,7 @@ export { UITableOfContents } from './toc/toc.js';
87
87
  export { UIQRCode } from './qr-code/qr-code.js';
88
88
  export { UIPagination } from './pagination/pagination.js';
89
89
  export { UICode } from './code/code.js';
90
+ export { UIPreview } from './preview/preview.js';
90
91
  export { UIList, UIListItem } from './list/list.js';
91
92
  export { UIListWindow } from './list-window/list-window.js';
92
93
  export { UIMenu, UIMenuItem, UIMenuDivider } from './menu/menu.js';
@@ -0,0 +1,93 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://adiaui.dev/a2ui/v0_9/components/Preview.json",
4
+ "title": "Preview",
5
+ "description": "Live code + render in one frame, from a single source. Wrap any authored AdiaUI markup: the same HTML renders LIVE in a stage and appears beside (or below) it as escaped, syntax-highlighted, copyable source via a nested <code-ui>. The code can never drift from the render — it IS the render's source. Batteries-included + HTML-first by construction: write one block of HTML, no inline styles, no JS wiring, and both panes appear. Use it for every component/recipe example so the primary snippet is always real, copyable HTML shown next to the working component.",
6
+ "type": "object",
7
+ "allOf": [
8
+ {
9
+ "$ref": "common_types.json#/$defs/ComponentCommon"
10
+ },
11
+ {
12
+ "$ref": "common_types.json#/$defs/CatalogComponentCommon"
13
+ }
14
+ ],
15
+ "properties": {
16
+ "codeFirst": {
17
+ "description": "Show the code pane before (above / left of) the render pane. Default is render-first.",
18
+ "type": "boolean",
19
+ "default": false
20
+ },
21
+ "component": {
22
+ "const": "Preview"
23
+ },
24
+ "language": {
25
+ "description": "Language hint forwarded to the nested <code-ui> for syntax highlighting. Demos are HTML, so this defaults to `html`.",
26
+ "type": "string",
27
+ "default": "html"
28
+ },
29
+ "layout": {
30
+ "description": "Pane arrangement. Defaults to `split` — render and code side-by-side in a responsive 2-column grid that collapses to stacked on narrow widths (a bare `<preview-ui>` stamps `layout=\"split\"` on connect). Use `stack` to force the render stacked above the code; the docs auto-apply `stack` to wide self-framing demos (card / shell / table) that read cramped at half width. The `rows` attribute is a separate gallery mode and overrides this.",
31
+ "type": "string",
32
+ "enum": [
33
+ "split",
34
+ "stack"
35
+ ],
36
+ "default": ""
37
+ },
38
+ "rows": {
39
+ "description": "Gallery mode — treat each direct child as a SEPARATE example and lay each out as its own `[render | code]` row (live sample left, its own source right), stacked with dividers. Without `rows`, the whole slotted markup is one render + one code block. An optional `data-preview-label` on a child surfaces a caption above that row's sample.",
40
+ "type": "boolean",
41
+ "default": false
42
+ }
43
+ },
44
+ "required": [
45
+ "component"
46
+ ],
47
+ "unevaluatedProperties": false,
48
+ "x-adiaui": {
49
+ "anti_patterns": [],
50
+ "category": "display",
51
+ "composes": [],
52
+ "events": {},
53
+ "examples": [],
54
+ "keywords": [
55
+ "preview",
56
+ "example",
57
+ "demo",
58
+ "code-preview",
59
+ "live-preview",
60
+ "playground",
61
+ "codepen"
62
+ ],
63
+ "name": "UIPreview",
64
+ "related": [
65
+ "Code",
66
+ "Card"
67
+ ],
68
+ "slots": {
69
+ "default": {
70
+ "description": "The authored markup to preview. Captured verbatim on connect: re-parsed into the live render stage AND shown literally in the code pane. Any valid AdiaUI HTML — one element or many siblings."
71
+ }
72
+ },
73
+ "states": [
74
+ {
75
+ "description": "Default, the only state.",
76
+ "name": "idle"
77
+ }
78
+ ],
79
+ "status": "stable",
80
+ "synonyms": {
81
+ "preview": [
82
+ "example",
83
+ "demo",
84
+ "sandbox",
85
+ "live-preview"
86
+ ]
87
+ },
88
+ "tag": "preview-ui",
89
+ "tokens": {},
90
+ "traits": [],
91
+ "version": 1
92
+ }
93
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Non-side-effect class export for `<preview-ui>`.
3
+ *
4
+ * Importing this file gives you the class without auto-registering the tag.
5
+ * The auto-register path is `@adia-ai/web-components/components/preview`.
6
+ *
7
+ * @see ../../USAGE.md#registration--auto-vs-explicit
8
+ */
9
+
10
+ /**
11
+ * <preview-ui> — live code + render, single source of truth.
12
+ *
13
+ * Wrap any authored AdiaUI markup; the component keeps it LIVE in a render
14
+ * stage and shows the SAME markup as escaped, syntax-highlighted, copyable
15
+ * HTML beside it — so a demo's code and its rendered output can never drift
16
+ * (the code IS the render's source).
17
+ *
18
+ * <preview-ui>
19
+ * <button-ui text="Save" variant="primary"></button-ui>
20
+ * </preview-ui>
21
+ *
22
+ * Batteries-included + HTML-first by construction: the author writes one
23
+ * block of HTML, no inline styles, no JS wiring — both panes appear.
24
+ *
25
+ * Attributes:
26
+ * [layout="split"] — render + code side-by-side (default: stacked)
27
+ * [code-first] — show the code pane above/before the render
28
+ * [language="…"] — code-ui language hint (default: html)
29
+ *
30
+ * Light-DOM (ADR-0033): the render pane holds real, live custom elements
31
+ * (re-parsed from the captured source on connect), so interactive demos
32
+ * actually work. `slot=` here is decorative; positioning is by CSS.
33
+ */
34
+
35
+ import { UIElement } from '../../core/element.js';
36
+
37
+ /** Prepare captured markup for display: strip shared indentation + surrounding
38
+ * blank lines, and collapse `attr=""` → `attr` (the DOM serializes boolean
39
+ * attributes like `truncate` / `disabled` with an empty value — show the
40
+ * cleaner boolean form a consumer would actually type). */
41
+ function dedent(src) {
42
+ let lines = src
43
+ .replace(/=""(?=[\s/>])/g, '') // boolean attr: truncate="" → truncate
44
+ .replace(/\t/g, ' ')
45
+ .split('\n');
46
+ while (lines.length && lines[0].trim() === '') lines.shift();
47
+ while (lines.length && lines[lines.length - 1].trim() === '') lines.pop();
48
+ const indentOf = (l) => l.match(/^ */)[0].length;
49
+ // Measure the common indent from the BODY (everything after the first line),
50
+ // not from every line: markup captured via outerHTML/innerHTML puts the
51
+ // opening tag at column 0 while its children + closing tag carry the source
52
+ // file's nesting depth. Including that 0-indent first line would make min=0
53
+ // and leave the whole body deeply indented (the original bug). Clamp each
54
+ // slice to the line's own indent so the 0-indent opener is never truncated.
55
+ const body = lines.slice(1).filter((l) => l.trim());
56
+ const measured = (body.length ? body : lines.filter((l) => l.trim())).map(indentOf);
57
+ const min = measured.length ? Math.min(...measured) : 0;
58
+ return lines.map((l) => l.slice(Math.min(min, indentOf(l)))).join('\n');
59
+ }
60
+
61
+ export class UIPreview extends UIElement {
62
+ static properties = {
63
+ layout: { type: String, default: '', reflect: true },
64
+ codeFirst: { type: Boolean, default: false, reflect: true, attribute: 'code-first' },
65
+ language: { type: String, default: 'html', reflect: true },
66
+ rows: { type: Boolean, default: false, reflect: true },
67
+ };
68
+
69
+ static template = () => null;
70
+
71
+ #fitObserver = null;
72
+
73
+ /** A [render-cell | code-cell] pair from one source string. */
74
+ #pane(source, lang, codeFirst) {
75
+ const render = document.createElement('div');
76
+ render.setAttribute('data-preview-render', '');
77
+ render.innerHTML = source; // re-parse → fresh, live custom elements (keeps
78
+ // any data-chunk corpus markers for harvest)
79
+ const codeWrap = document.createElement('div');
80
+ codeWrap.setAttribute('data-preview-code', '');
81
+ const code = document.createElement('code-ui');
82
+ code.setAttribute('language', lang);
83
+ // The CODE pane shows the clean consumer-facing HTML — strip doc-only
84
+ // corpus markers (data-chunk / -kind / -slot) that the render keeps.
85
+ code.textContent = source.replace(/\s+data-chunk(?:-[a-z]+)?="[^"]*"/g, '');
86
+ codeWrap.appendChild(code);
87
+ return codeFirst ? [codeWrap, render] : [render, codeWrap];
88
+ }
89
+
90
+ connected() {
91
+ // Idempotent — never re-stamp (peer mutation / re-connect safe).
92
+ if (this.querySelector(':scope > [data-preview-render], :scope > [data-preview-row]')) return;
93
+
94
+ // Side-by-side (render | code) is the default presentation. `rows` mode lays
95
+ // out its own per-example grid, and an explicit layout="…" (e.g. "stack")
96
+ // opts back into the stacked render-over-code form — so only default when
97
+ // neither is set. Setting the attribute (not the property default) keeps
98
+ // rows-mode previews free of a conflicting layout="split".
99
+ if (!this.hasAttribute('rows') && !this.hasAttribute('layout')) {
100
+ this.setAttribute('layout', 'split');
101
+ }
102
+
103
+ const lang = this.getAttribute('language') || 'html';
104
+ const codeFirst = this.hasAttribute('code-first');
105
+
106
+ // ── rows mode ── each direct child is one example; lay each out as its
107
+ // own [render | code] row so a multi-example gallery reads as paired
108
+ // side-by-side rows instead of all-renders-then-one-code-dump. The code
109
+ // shown is the child's OWN markup (zero drift). An optional
110
+ // [data-preview-label] surfaces a caption per row.
111
+ if (this.hasAttribute('rows')) {
112
+ const children = [...this.children];
113
+ const out = [];
114
+ for (const child of children) {
115
+ const label = child.getAttribute('data-preview-label');
116
+ if (label != null) child.removeAttribute('data-preview-label');
117
+ // A synthetic wrapper (multi-element row grouped by the host) marks
118
+ // itself [data-preview-unwrap] so we show its children's markup, not the
119
+ // throwaway wrapper tag — the copied code stays exactly what the author
120
+ // would paste.
121
+ const unwrap = child.hasAttribute('data-preview-unwrap');
122
+ if (unwrap) child.removeAttribute('data-preview-unwrap');
123
+ const source = dedent(unwrap ? child.innerHTML : child.outerHTML);
124
+ const row = document.createElement('div');
125
+ row.setAttribute('data-preview-row', '');
126
+ const panes = this.#pane(source, lang, codeFirst);
127
+ // The caption pill renders via [data-preview-render]::before, whose
128
+ // attr() can only read its OWN element — so the label goes on the
129
+ // render cell, not the row.
130
+ if (label) {
131
+ const renderEl = panes.find((el) => el.hasAttribute('data-preview-render'));
132
+ renderEl?.setAttribute('data-preview-label', label);
133
+ }
134
+ row.append(...panes);
135
+ out.push(row);
136
+ }
137
+ if (out.length) { this.replaceChildren(...out); this.#observeFit(); return; }
138
+ }
139
+
140
+ // ── single-block mode ── the whole slotted markup → one render + one code.
141
+ const source = dedent(this.innerHTML);
142
+ this.replaceChildren(...this.#pane(source, lang, codeFirst));
143
+ this.#observeFit();
144
+ }
145
+
146
+ // Side-by-side clips content wider than its half-width cell (a full dashboard
147
+ // composite, a wide control). After layout, if a render cell overflows, fall
148
+ // back to the stacked full-width form so the demo is never cut off. Monotonic
149
+ // (split → stack, never back; a row gains [data-stack] once), so it settles in
150
+ // one pass and never oscillates. Runs on a rAF + a ResizeObserver so it also
151
+ // adapts when the viewport narrows. Self-correcting — no per-component "wide"
152
+ // list to maintain.
153
+ #fit = () => {
154
+ if (this.hasAttribute('rows')) {
155
+ for (const row of this.querySelectorAll(':scope > [data-preview-row]')) {
156
+ const r = row.querySelector(':scope > [data-preview-render]');
157
+ if (r && r.scrollWidth > r.clientWidth + 4) row.setAttribute('data-stack', '');
158
+ }
159
+ } else if (this.getAttribute('layout') === 'split') {
160
+ const r = this.querySelector(':scope > [data-preview-render]');
161
+ if (r && r.scrollWidth > r.clientWidth + 4) this.setAttribute('layout', 'stack');
162
+ }
163
+ };
164
+
165
+ #scheduleFit = () => requestAnimationFrame(this.#fit);
166
+
167
+ #observeFit() {
168
+ this.#scheduleFit();
169
+ if (typeof ResizeObserver !== 'undefined') {
170
+ this.#fitObserver = new ResizeObserver(this.#scheduleFit);
171
+ this.#fitObserver.observe(this);
172
+ }
173
+ }
174
+
175
+ disconnected() {
176
+ this.#fitObserver?.disconnect();
177
+ }
178
+ }