@adia-ai/web-components 0.4.8 → 0.5.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.
Files changed (112) hide show
  1. package/USAGE.md +255 -2
  2. package/components/action-list/action-list.a2ui.json +15 -1
  3. package/components/action-list/action-list.d.ts +10 -1
  4. package/components/action-list/action-list.yaml +10 -0
  5. package/components/agent-artifact/agent-artifact.a2ui.json +7 -1
  6. package/components/agent-artifact/agent-artifact.d.ts +6 -1
  7. package/components/agent-artifact/agent-artifact.yaml +4 -0
  8. package/components/agent-feedback-bar/agent-feedback-bar.a2ui.json +7 -1
  9. package/components/agent-feedback-bar/agent-feedback-bar.d.ts +6 -1
  10. package/components/agent-feedback-bar/agent-feedback-bar.yaml +4 -0
  11. package/components/agent-questions/agent-questions.a2ui.json +11 -1
  12. package/components/agent-questions/agent-questions.d.ts +8 -1
  13. package/components/agent-questions/agent-questions.yaml +7 -0
  14. package/components/agent-reasoning/agent-reasoning.a2ui.json +25 -3
  15. package/components/agent-reasoning/agent-reasoning.d.ts +20 -3
  16. package/components/agent-reasoning/agent-reasoning.yaml +15 -0
  17. package/components/agent-suggestions/agent-suggestions.a2ui.json +15 -1
  18. package/components/agent-suggestions/agent-suggestions.d.ts +10 -1
  19. package/components/agent-suggestions/agent-suggestions.yaml +10 -0
  20. package/components/agent-trace/agent-trace.a2ui.json +7 -1
  21. package/components/agent-trace/agent-trace.d.ts +6 -1
  22. package/components/agent-trace/agent-trace.yaml +4 -0
  23. package/components/canvas/canvas.yaml +9 -7
  24. package/components/chart/chart.a2ui.json +3 -0
  25. package/components/chart/chart.d.ts +2 -0
  26. package/components/chart/chart.yaml +5 -0
  27. package/components/chart-legend/chart-legend.a2ui.json +15 -1
  28. package/components/chart-legend/chart-legend.d.ts +10 -1
  29. package/components/chart-legend/chart-legend.yaml +10 -0
  30. package/components/chat-thread/chat-thread.a2ui.json +11 -1
  31. package/components/chat-thread/chat-thread.d.ts +8 -1
  32. package/components/chat-thread/chat-thread.yaml +7 -0
  33. package/components/code/code.a2ui.json +36 -7
  34. package/components/code/code.d.ts +30 -0
  35. package/components/code/code.yaml +29 -6
  36. package/components/color-picker/class.js +59 -1
  37. package/components/color-picker/color-picker.a2ui.json +34 -0
  38. package/components/color-picker/color-picker.d.ts +70 -8
  39. package/components/color-picker/color-picker.yaml +49 -0
  40. package/components/command/command.a2ui.json +11 -1
  41. package/components/command/command.d.ts +8 -1
  42. package/components/command/command.yaml +7 -0
  43. package/components/demo-toggle/demo-toggle.a2ui.json +7 -1
  44. package/components/demo-toggle/demo-toggle.d.ts +6 -1
  45. package/components/demo-toggle/demo-toggle.yaml +4 -0
  46. package/components/heatmap/heatmap.a2ui.json +11 -2
  47. package/components/heatmap/heatmap.d.ts +6 -0
  48. package/components/heatmap/heatmap.yaml +17 -2
  49. package/components/link/link.a2ui.json +11 -1
  50. package/components/link/link.d.ts +8 -1
  51. package/components/link/link.yaml +7 -0
  52. package/components/list/list.a2ui.json +11 -1
  53. package/components/list/list.d.ts +8 -1
  54. package/components/list/list.yaml +7 -0
  55. package/components/menu/menu.a2ui.json +11 -1
  56. package/components/menu/menu.d.ts +8 -1
  57. package/components/menu/menu.yaml +7 -0
  58. package/components/nav/nav.a2ui.json +15 -1
  59. package/components/nav/nav.d.ts +10 -1
  60. package/components/nav/nav.yaml +10 -0
  61. package/components/nav-group/nav-group.a2ui.json +11 -1
  62. package/components/nav-group/nav-group.d.ts +8 -1
  63. package/components/nav-group/nav-group.yaml +7 -0
  64. package/components/nav-item/nav-item.a2ui.json +15 -1
  65. package/components/nav-item/nav-item.d.ts +10 -1
  66. package/components/nav-item/nav-item.yaml +10 -0
  67. package/components/noodles/noodles.a2ui.json +46 -2
  68. package/components/noodles/noodles.d.ts +28 -2
  69. package/components/noodles/noodles.yaml +32 -0
  70. package/components/otp-input/otp-input.a2ui.json +14 -2
  71. package/components/otp-input/otp-input.d.ts +11 -0
  72. package/components/otp-input/otp-input.yaml +10 -2
  73. package/components/pagination/pagination.a2ui.json +7 -1
  74. package/components/pagination/pagination.d.ts +6 -1
  75. package/components/pagination/pagination.yaml +4 -0
  76. package/components/stream/stream.a2ui.json +7 -1
  77. package/components/stream/stream.d.ts +6 -1
  78. package/components/stream/stream.yaml +4 -0
  79. package/components/swatch/class.js +362 -15
  80. package/components/swatch/swatch.a2ui.json +68 -1
  81. package/components/swatch/swatch.css +150 -0
  82. package/components/swatch/swatch.d.ts +43 -0
  83. package/components/swatch/swatch.yaml +67 -1
  84. package/components/swiper/swiper.a2ui.json +18 -2
  85. package/components/swiper/swiper.d.ts +14 -2
  86. package/components/swiper/swiper.yaml +11 -0
  87. package/components/table/table.a2ui.json +80 -5
  88. package/components/table/table.d.ts +58 -5
  89. package/components/table/table.yaml +54 -2
  90. package/components/tabs/tabs.a2ui.json +7 -1
  91. package/components/tabs/tabs.d.ts +6 -1
  92. package/components/tabs/tabs.yaml +4 -0
  93. package/components/tag/tag.a2ui.json +11 -1
  94. package/components/tag/tag.d.ts +8 -1
  95. package/components/tag/tag.yaml +7 -0
  96. package/components/timeline/timeline.a2ui.json +3 -7
  97. package/components/timeline/timeline.d.ts +2 -4
  98. package/components/timeline/timeline.yaml +3 -6
  99. package/components/toggle-group/toggle-group.a2ui.json +7 -1
  100. package/components/toggle-group/toggle-group.d.ts +6 -1
  101. package/components/toggle-group/toggle-group.yaml +4 -0
  102. package/components/toggle-scheme/toggle-scheme.a2ui.json +11 -1
  103. package/components/toggle-scheme/toggle-scheme.d.ts +8 -1
  104. package/components/toggle-scheme/toggle-scheme.yaml +7 -0
  105. package/components/tree/tree.a2ui.json +15 -1
  106. package/components/tree/tree.d.ts +10 -1
  107. package/components/tree/tree.yaml +10 -0
  108. package/core/data-stream.d.ts +56 -0
  109. package/core/element.d.ts +10 -0
  110. package/core/index.d.ts +6 -0
  111. package/index.d.ts +9 -2
  112. package/package.json +2 -2
@@ -12,7 +12,12 @@
12
12
 
13
13
  import { UIElement } from '../../core/element.js';
14
14
 
15
- export type AgentTraceTraceToggleEvent = CustomEvent<unknown>;
15
+ export interface AgentTraceTraceToggleEventDetail {
16
+ /** Whether the trace is now collapsed. */
17
+ collapsed: boolean;
18
+ }
19
+
20
+ export type AgentTraceTraceToggleEvent = CustomEvent<AgentTraceTraceToggleEventDetail>;
16
21
 
17
22
  export class UIAgentTrace extends UIElement {
18
23
  /** Hide the trace body. Default-visible disclosure (matches the agent-* family — agent-reasoning, agent-artifact). Set to opt out. */
@@ -20,6 +20,10 @@ props:
20
20
  events:
21
21
  trace-toggle:
22
22
  description: "Fired on trace-toggle."
23
+ detail:
24
+ collapsed:
25
+ type: boolean
26
+ description: Whether the trace is now collapsed.
23
27
  slots:
24
28
  default:
25
29
  description: "Default slot — primary child content."
@@ -7,14 +7,16 @@ version: 1
7
7
  description: A2UI rendering surface.
8
8
  # Per ADR-0027 — primitives that programmatically create other primitives
9
9
  # do NOT auto-import them. Consumer (or demo shell) must explicitly import.
10
- # NOTE: <canvas-ui> mounts <a2ui-root> (web-modules/runtime) and optionally
11
- # <theme-ui> (web-modules/theme). These are CROSS-PACKAGE compositions that
12
- # the current composes: field can't express as same-package siblings. Cross-
13
- # package composition tracking is queued for v0.4.9; for now consumers
14
- # loading <canvas-ui> with a2ui-root rendering must import:
15
- # @adia-ai/web-modules/runtime/a2ui-root
16
- # @adia-ai/web-modules/theme/theme (only if [theme-wrapper] is used)
10
+ # <canvas-ui> composes only cross-package primitives (a2ui-root + theme-ui);
11
+ # they're declared in cross_package_composes: below per v0.4.9 §92 G1.
17
12
  composes: []
13
+ cross_package_composes:
14
+ - tag: a2ui-root
15
+ from: '@adia-ai/web-modules/runtime'
16
+ note: Hosts A2UI rendering. Required whenever canvas mounts content.
17
+ - tag: theme-ui
18
+ from: '@adia-ai/web-modules/theme'
19
+ note: Optional — only read when [theme-wrapper] is set.
18
20
  props:
19
21
  theme:
20
22
  description: 'Component property: theme.'
@@ -143,6 +143,9 @@
143
143
  },
144
144
  "chart-select": {
145
145
  "description": "Fires on click of a datum with the same detail shape as chart-hover."
146
+ },
147
+ "legend-update": {
148
+ "description": "Fires when the chart's internal legend payload regenerates (datum\nlabel/value re-aggregate). Signal-only — no detail payload.\nUsed by chart-legend-ui to mirror state when wired via [for].\n"
146
149
  }
147
150
  },
148
151
  "examples": [
@@ -15,6 +15,7 @@ import { UIElement } from '../../core/element.js';
15
15
  export type ChartHoverEvent = CustomEvent<unknown>;
16
16
  export type ChartLeaveEvent = CustomEvent<unknown>;
17
17
  export type ChartSelectEvent = CustomEvent<unknown>;
18
+ export type ChartLegendUpdateEvent = CustomEvent<unknown>;
18
19
 
19
20
  export class UIChart extends UIElement {
20
21
  /** Chart type. All 18 enum values have dedicated render paths in chart.js. */
@@ -52,4 +53,5 @@ export class UIChart extends UIElement {
52
53
  addEventListener(type: 'chart-hover', listener: (ev: ChartHoverEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
53
54
  addEventListener(type: 'chart-leave', listener: (ev: ChartLeaveEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
54
55
  addEventListener(type: 'chart-select', listener: (ev: ChartSelectEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
56
+ addEventListener(type: 'legend-update', listener: (ev: ChartLegendUpdateEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
55
57
  }
@@ -132,6 +132,11 @@ events:
132
132
  description: Fires when the pointer leaves the plot area or a previously-hovered datum with no new one entering.
133
133
  chart-select:
134
134
  description: Fires on click of a datum with the same detail shape as chart-hover.
135
+ legend-update:
136
+ description: |
137
+ Fires when the chart's internal legend payload regenerates (datum
138
+ label/value re-aggregate). Signal-only — no detail payload.
139
+ Used by chart-legend-ui to mirror state when wired via [for].
135
140
  slots:
136
141
  canvas:
137
142
  description: Chart rendering area
@@ -76,7 +76,21 @@
76
76
  ],
77
77
  "events": {
78
78
  "toggle": {
79
- "description": "Fires on row click (non-static). Detail: {key, active, mode}. `active` is the new state (true=visible). Consumers (or chart-ui via [for]) wire this to series visibility."
79
+ "description": "Fires on row click (non-static). Detail: {key, active, mode}. `active` is the new state (true=visible). Consumers (or chart-ui via [for]) wire this to series visibility.",
80
+ "detail": {
81
+ "active": {
82
+ "description": "New visibility state (true = visible).",
83
+ "type": "boolean"
84
+ },
85
+ "key": {
86
+ "description": "Series key identifier.",
87
+ "type": "string"
88
+ },
89
+ "mode": {
90
+ "description": "Visibility mode (default \"hide\").",
91
+ "type": "string"
92
+ }
93
+ }
80
94
  }
81
95
  },
82
96
  "examples": [
@@ -12,7 +12,16 @@
12
12
 
13
13
  import { UIElement } from '../../core/element.js';
14
14
 
15
- export type ChartLegendToggleEvent = CustomEvent<unknown>;
15
+ export interface ChartLegendToggleEventDetail {
16
+ /** New visibility state (true = visible). */
17
+ active: boolean;
18
+ /** Series key identifier. */
19
+ key: string;
20
+ /** Visibility mode (default "hide"). */
21
+ mode: string;
22
+ }
23
+
24
+ export type ChartLegendToggleEvent = CustomEvent<ChartLegendToggleEventDetail>;
16
25
 
17
26
  export class UIChartLegend extends UIElement {
18
27
  /** JSON array of {key, label, slot?, pct?} legend items. Takes precedence over [for] when both are provided. */
@@ -68,6 +68,16 @@ events:
68
68
  Fires on row click (non-static). Detail: {key, active, mode}. `active` is
69
69
  the new state (true=visible). Consumers (or chart-ui via [for]) wire
70
70
  this to series visibility.
71
+ detail:
72
+ key:
73
+ type: string
74
+ description: Series key identifier.
75
+ active:
76
+ type: boolean
77
+ description: New visibility state (true = visible).
78
+ mode:
79
+ type: string
80
+ description: Visibility mode (default "hide").
71
81
  slots:
72
82
  default:
73
83
  description: >-
@@ -32,7 +32,17 @@
32
32
  "composes": [],
33
33
  "events": {
34
34
  "submit": {
35
- "description": "Fired when the user submits a chat message via Enter or send button. Detail: { text, model }."
35
+ "description": "Fired when the user submits a chat message via Enter or send button. Detail: { text, model }.",
36
+ "detail": {
37
+ "model": {
38
+ "description": "Selected model identifier.",
39
+ "type": "string"
40
+ },
41
+ "text": {
42
+ "description": "Submitted message text.",
43
+ "type": "string"
44
+ }
45
+ }
36
46
  }
37
47
  },
38
48
  "examples": [
@@ -12,7 +12,14 @@
12
12
 
13
13
  import { UIElement } from '../../core/element.js';
14
14
 
15
- export type ChatThreadSubmitEvent = CustomEvent<unknown>;
15
+ export interface ChatThreadSubmitEventDetail {
16
+ /** Selected model identifier. */
17
+ model: string;
18
+ /** Submitted message text. */
19
+ text: string;
20
+ }
21
+
22
+ export type ChatThreadSubmitEvent = CustomEvent<ChatThreadSubmitEventDetail>;
16
23
 
17
24
  export class UIChatThread extends UIElement {
18
25
  /** Component property: streaming. */
@@ -15,6 +15,13 @@ props:
15
15
  events:
16
16
  submit:
17
17
  description: "Fired when the user submits a chat message via Enter or send button. Detail: { text, model }."
18
+ detail:
19
+ text:
20
+ type: string
21
+ description: Submitted message text.
22
+ model:
23
+ type: string
24
+ description: Selected model identifier.
18
25
  slots:
19
26
  default:
20
27
  description: "Default slot — primary child content."
@@ -86,19 +86,48 @@
86
86
  "composes": [],
87
87
  "events": {
88
88
  "change": {
89
- "description": "Fired on blur if the buffer changed since focus (editable mode); detail.value is the final buffer"
90
- },
91
- "copy": {
92
- "description": "Fired when text is copied to clipboard"
89
+ "description": "Fired on blur if the buffer changed since focus (editable mode); detail.value is the final buffer.",
90
+ "detail": {
91
+ "value": {
92
+ "description": "Editor contents at blur.",
93
+ "type": "string"
94
+ }
95
+ }
93
96
  },
94
97
  "input": {
95
- "description": "Fired on every doc change (editable mode); detail.value is the current buffer"
98
+ "description": "Fired on every doc change (editable mode); detail.value is the current buffer.",
99
+ "detail": {
100
+ "value": {
101
+ "description": "Current editor contents.",
102
+ "type": "string"
103
+ }
104
+ }
96
105
  },
97
106
  "language-load-error": {
98
- "description": "Fired when the language pack or CodeMirror bundle fails to load; detail.phase is 'core' or 'language'"
107
+ "description": "Fired when CodeMirror's dynamic language-mode or core bundle fails to load.\ndetail.phase is \"core\" (fatal — component falls back to static pre+code) or\n\"language\" (recoverable — editor mounts in plain-text mode).\n",
108
+ "detail": {
109
+ "error": {
110
+ "description": "The underlying load failure.",
111
+ "type": "object"
112
+ },
113
+ "language": {
114
+ "description": "Present when phase is \"language\" — the language slug that failed.",
115
+ "type": "string"
116
+ },
117
+ "phase": {
118
+ "description": "Either \"core\" or \"language\".",
119
+ "type": "string"
120
+ }
121
+ }
99
122
  },
100
123
  "save": {
101
- "description": "Fired on Mod+S keybind (editable mode); detail.value is the current buffer. Cancelable."
124
+ "description": "Fired on Mod+S keybind (editable mode); detail.value is the current buffer. Cancelable — call event.preventDefault() to suppress the host page's save handling.",
125
+ "detail": {
126
+ "value": {
127
+ "description": "Editor contents at the moment of save.",
128
+ "type": "string"
129
+ }
130
+ }
102
131
  }
103
132
  },
104
133
  "examples": [
@@ -12,6 +12,34 @@ export interface CodeChangeEventDetail {
12
12
  export type CodeChangeEvent = CustomEvent<CodeChangeEventDetail>;
13
13
  export type CodeInputEvent = CustomEvent<CodeChangeEventDetail>;
14
14
 
15
+ /**
16
+ * Detail payload for the `save` event — fired when the user invokes the
17
+ * CodeMirror save keybinding (Cmd/Ctrl+S). The event is `cancelable: true`;
18
+ * call `event.preventDefault()` to suppress the host page's save handling.
19
+ */
20
+ export interface CodeSaveEventDetail {
21
+ /** Current editor contents at the moment of save. */
22
+ value: string;
23
+ }
24
+ export type CodeSaveEvent = CustomEvent<CodeSaveEventDetail>;
25
+
26
+ /**
27
+ * Detail payload for the `language-load-error` event — fired when
28
+ * CodeMirror's dynamic language-mode import fails. `phase: "core"`
29
+ * indicates the CodeMirror core bundle itself failed (rare, fatal —
30
+ * the component falls back to a static `<pre><code>`). `phase: "language"`
31
+ * indicates the requested language extension failed (recoverable —
32
+ * editor still mounts in plain-text mode).
33
+ */
34
+ export interface CodeLanguageLoadErrorEventDetail {
35
+ phase: 'core' | 'language';
36
+ /** Present when `phase === 'language'`; the language slug that failed. */
37
+ language?: string;
38
+ /** The underlying load failure. */
39
+ error: unknown;
40
+ }
41
+ export type CodeLanguageLoadErrorEvent = CustomEvent<CodeLanguageLoadErrorEventDetail>;
42
+
15
43
  export class UICode extends UIFormElement {
16
44
  /** CodeMirror language slug — `javascript` / `typescript` / `html` / `css` / `json` / `markdown` / `python` etc. */
17
45
  language: string;
@@ -36,4 +64,6 @@ export class UICode extends UIFormElement {
36
64
  ): void;
37
65
  addEventListener(type: 'change', listener: (ev: CodeChangeEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
38
66
  addEventListener(type: 'input', listener: (ev: CodeInputEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
67
+ addEventListener(type: 'save', listener: (ev: CodeSaveEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
68
+ addEventListener(type: 'language-load-error', listener: (ev: CodeLanguageLoadErrorEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
39
69
  }
@@ -58,16 +58,39 @@ props:
58
58
  type: boolean
59
59
  default: false
60
60
  events:
61
- copy:
62
- description: Fired when text is copied to clipboard
63
61
  input:
64
- description: Fired on every doc change (editable mode); detail.value is the current buffer
62
+ description: Fired on every doc change (editable mode); detail.value is the current buffer.
63
+ detail:
64
+ value:
65
+ type: string
66
+ description: Current editor contents.
65
67
  change:
66
- description: Fired on blur if the buffer changed since focus (editable mode); detail.value is the final buffer
68
+ description: Fired on blur if the buffer changed since focus (editable mode); detail.value is the final buffer.
69
+ detail:
70
+ value:
71
+ type: string
72
+ description: Editor contents at blur.
67
73
  save:
68
- description: Fired on Mod+S keybind (editable mode); detail.value is the current buffer. Cancelable.
74
+ description: Fired on Mod+S keybind (editable mode); detail.value is the current buffer. Cancelable — call event.preventDefault() to suppress the host page's save handling.
75
+ detail:
76
+ value:
77
+ type: string
78
+ description: Editor contents at the moment of save.
69
79
  language-load-error:
70
- description: Fired when the language pack or CodeMirror bundle fails to load; detail.phase is 'core' or 'language'
80
+ description: |
81
+ Fired when CodeMirror's dynamic language-mode or core bundle fails to load.
82
+ detail.phase is "core" (fatal — component falls back to static pre+code) or
83
+ "language" (recoverable — editor mounts in plain-text mode).
84
+ detail:
85
+ phase:
86
+ type: string
87
+ description: Either "core" or "language".
88
+ language:
89
+ type: string
90
+ description: Present when phase is "language" — the language slug that failed.
91
+ error:
92
+ type: object
93
+ description: The underlying load failure.
71
94
  slots:
72
95
  default:
73
96
  description: Raw text fallback when the text property is not set
@@ -128,9 +128,27 @@ export class UIColorPicker extends UIFormElement {
128
128
  value: { type: String, default: '#3b82f6', reflect: true },
129
129
  format: { type: String, default: 'hex', reflect: true },
130
130
  disabled: { type: Boolean, default: false, reflect: true },
131
+ /**
132
+ * v0.4.9 §99h — generation-constraint props (FEEDBACK-02 #7).
133
+ * When set, the picker clamps OKLCH channels to the consumer's
134
+ * declared bounds before committing. Out-of-bound mutations
135
+ * round-trip to the nearest in-bound equivalent + fire a
136
+ * `constraint-clamp` event with `{ axis, requested, clamped, reason }`
137
+ * so the consumer can surface UX feedback (toast, validity warning,
138
+ * scope-drift indicator). Default values disable the constraint.
139
+ */
140
+ maxChroma: { type: Number, default: Infinity, reflect: true, attribute: 'max-chroma' },
141
+ maxL: { type: Number, default: 1, reflect: true, attribute: 'max-l' },
142
+ minL: { type: Number, default: 0, reflect: true, attribute: 'min-l' },
143
+ /** Maximum allowed hue deviation in degrees from the picker's `baseHue`. NaN = no constraint. */
144
+ hueDriftMax: { type: Number, default: NaN, reflect: true, attribute: 'hue-drift-max' },
145
+ /** Reference hue (degrees) for [hue-drift-max]. NaN = use the value parsed at first commit. */
146
+ baseHue: { type: Number, default: NaN, reflect: true, attribute: 'base-hue' },
131
147
  };
132
148
 
133
149
  #L = 0.6; #C = 0.15; #H = 230;
150
+ /** Cached reference hue for hue-drift constraint. Set on first commit if [base-hue] isn't authored. */
151
+ #resolvedBaseHue = NaN;
134
152
  #bound = false;
135
153
  #dragging = null; // 'area' | 'hue' | null
136
154
  #internalUpdate = false;
@@ -400,9 +418,45 @@ export class UIColorPicker extends UIFormElement {
400
418
  }
401
419
  }
402
420
 
403
- // ── Commit: update value + fire events ──
421
+ // ── Commit: clamp to consumer constraints + gamut-map + update value + fire events ──
404
422
 
405
423
  #commit(eventType) {
424
+ // Apply consumer-declared constraints BEFORE gamut mapping. Each constraint
425
+ // axis records a `constraint-clamp` event-payload entry when it actually
426
+ // moved a value; we dispatch one event per commit so the consumer can
427
+ // surface a single UX response (toast / validity warning / etc.).
428
+ const clamps = [];
429
+ if (this.#L > this.maxL) {
430
+ clamps.push({ axis: 'l', requested: this.#L, clamped: this.maxL, reason: 'max-l' });
431
+ this.#L = this.maxL;
432
+ }
433
+ if (this.#L < this.minL) {
434
+ clamps.push({ axis: 'l', requested: this.#L, clamped: this.minL, reason: 'min-l' });
435
+ this.#L = this.minL;
436
+ }
437
+ if (this.#C > this.maxChroma) {
438
+ clamps.push({ axis: 'c', requested: this.#C, clamped: this.maxChroma, reason: 'max-chroma' });
439
+ this.#C = this.maxChroma;
440
+ }
441
+ if (Number.isFinite(this.hueDriftMax)) {
442
+ const base = Number.isFinite(this.baseHue)
443
+ ? this.baseHue
444
+ : (Number.isFinite(this.#resolvedBaseHue) ? this.#resolvedBaseHue : this.#H);
445
+ if (!Number.isFinite(this.#resolvedBaseHue)) this.#resolvedBaseHue = base;
446
+ // Hue is circular — find the signed shortest-path drift in [-180, 180].
447
+ let drift = ((this.#H - base + 540) % 360) - 180;
448
+ const limit = this.hueDriftMax;
449
+ if (drift > limit) {
450
+ const requestedH = this.#H;
451
+ this.#H = (base + limit + 360) % 360;
452
+ clamps.push({ axis: 'h', requested: requestedH, clamped: this.#H, reason: 'hue-drift-max' });
453
+ } else if (drift < -limit) {
454
+ const requestedH = this.#H;
455
+ this.#H = (base - limit + 360) % 360;
456
+ clamps.push({ axis: 'h', requested: requestedH, clamped: this.#H, reason: 'hue-drift-max' });
457
+ }
458
+ }
459
+
406
460
  const clampedC = gamutMapChroma(this.#L, this.#C, this.#H);
407
461
  const hexVal = oklchToHex(this.#L, clampedC, this.#H);
408
462
  const oklchStr = this.#oklchStr();
@@ -411,6 +465,10 @@ export class UIColorPicker extends UIFormElement {
411
465
  this.value = this.format === 'oklch' ? oklchStr : hexVal;
412
466
  this.syncValue();
413
467
 
468
+ if (clamps.length) {
469
+ this.dispatchEvent(new CustomEvent('constraint-clamp', { bubbles: true, detail: { clamps } }));
470
+ }
471
+
414
472
  const detail = { value: this.value, l: this.#L, c: this.#C, h: this.#H, hex: hexVal, oklch: oklchStr };
415
473
  this.dispatchEvent(new CustomEvent(eventType, { bubbles: true, detail }));
416
474
  }
@@ -13,6 +13,11 @@
13
13
  }
14
14
  ],
15
15
  "properties": {
16
+ "baseHue": {
17
+ "description": "Reference hue (degrees) for the [hue-drift-max] constraint. Default\nNaN — falls back to the picker's hue at first commit so the consumer\ncan pre-seed the picker and constrain drift from that initial value.\n",
18
+ "type": "number",
19
+ "default": "NaN"
20
+ },
16
21
  "component": {
17
22
  "const": "ColorPicker"
18
23
  },
@@ -30,6 +35,26 @@
30
35
  ],
31
36
  "default": "hex"
32
37
  },
38
+ "hueDriftMax": {
39
+ "description": "Generation constraint — maximum allowed signed-shortest-path hue\ndeviation (degrees) from [base-hue] (or the first-committed hue\nif [base-hue] is unset). Default NaN (no constraint). Wrap-aware\nso a drift of 350 degrees resolves as -10.\n",
40
+ "type": "number",
41
+ "default": "NaN"
42
+ },
43
+ "maxChroma": {
44
+ "description": "Generation constraint (v0.4.9 §99h, FEEDBACK-02 #7) — clamp the\nOKLCH chroma channel to at most this value before commit. Out-of-\nbound mutations round-trip to the nearest in-bound equivalent +\nfire `constraint-clamp`. Default Infinity (no constraint).\n",
45
+ "type": "number",
46
+ "default": "Infinity"
47
+ },
48
+ "maxL": {
49
+ "description": "Generation constraint — clamp OKLCH lightness to at most this value (0..1). Default 1 (no constraint).",
50
+ "type": "number",
51
+ "default": 1
52
+ },
53
+ "minL": {
54
+ "description": "Generation constraint — clamp OKLCH lightness to at least this value (0..1). Default 0 (no constraint).",
55
+ "type": "number",
56
+ "default": 0
57
+ },
33
58
  "name": {
34
59
  "description": "Form field name",
35
60
  "type": "string",
@@ -55,6 +80,15 @@
55
80
  "change": {
56
81
  "description": "Fired on every color change"
57
82
  },
83
+ "constraint-clamp": {
84
+ "description": "Fired immediately before `change` / `input` when one or more\nconsumer-declared constraints (max-chroma / max-l / min-l /\nhue-drift-max) clamped a channel away from the user-requested\nvalue. detail.clamps is an array of axis-specific clamp records.\n",
85
+ "detail": {
86
+ "clamps": {
87
+ "description": "Array of `{ axis, requested, clamped, reason }` objects. axis is\none of \"l\" / \"c\" / \"h\". reason names the triggering constraint\nprop. Empty arrays are never emitted.\n",
88
+ "type": "array"
89
+ }
90
+ }
91
+ },
58
92
  "input": {
59
93
  "description": "Fired during continuous interaction (drag)"
60
94
  }
@@ -1,30 +1,92 @@
1
1
  /**
2
- * `<color-picker-ui>` — Color picker with hex / rgb / hsl / oklch formats.
2
+ * `<color-picker-ui>` — OKLCH-native color picker with 2D area + H/C/L sliders.
3
+ *
4
+ * Form-associated (extends UIFormElement). The `[format]` attribute controls
5
+ * whether `value` is read/written as a `#rrggbb` hex string or an `oklch(L C H)`
6
+ * string, but the event detail always carries BOTH forms plus the parsed
7
+ * OKLCH channel scalars — consumers don't need to parse the string.
3
8
  *
4
9
  * @see https://ui-kit.exe.xyz/site/components/color-picker
5
10
  */
6
11
 
7
12
  import { UIFormElement } from '../../core/form.js';
8
13
 
9
- export type ColorFormat = 'hex' | 'rgb' | 'hsl' | 'oklch';
14
+ /**
15
+ * Output format selected by the `[format]` attribute. Drives the shape of
16
+ * `value` (and the matching `detail.value` field). Both `hex` and `oklch`
17
+ * forms are always present on the event detail regardless of `[format]`.
18
+ */
19
+ export type ColorFormat = 'hex' | 'oklch';
10
20
 
21
+ /**
22
+ * Detail payload for both `change` (commit) and `input` (continuous drag)
23
+ * events. Carries parsed channel scalars + both string forms — `value`
24
+ * mirrors the picker's current `[format]`, while `hex` and `oklch` are
25
+ * always populated parallel views.
26
+ */
11
27
  export interface ColorPickerChangeEventDetail {
28
+ /** Format-respecting string. Equals `hex` when `[format="hex"]`; equals `oklch` when `[format="oklch"]`. */
12
29
  value: string;
13
- format: ColorFormat;
30
+ /** OKLCH lightness (0–1). */
31
+ l: number;
32
+ /** OKLCH chroma (0–~0.4). */
33
+ c: number;
34
+ /** OKLCH hue (0–360, degrees). */
35
+ h: number;
36
+ /** Hex string (`#rrggbb`). Gamut-mapped to sRGB. */
37
+ hex: string;
38
+ /** OKLCH string (`oklch(L C H)`, fixed precision). */
39
+ oklch: string;
14
40
  }
15
41
  export type ColorPickerChangeEvent = CustomEvent<ColorPickerChangeEventDetail>;
16
42
  export type ColorPickerInputEvent = CustomEvent<ColorPickerChangeEventDetail>;
17
43
 
18
- export interface ColorPickerFormatChangeEventDetail {
19
- format: ColorFormat;
44
+ /**
45
+ * Axis-specific clamp record. Fires when a consumer-declared constraint
46
+ * (max-chroma / max-l / min-l / hue-drift-max) clamped a user-requested
47
+ * channel value to its in-bound equivalent.
48
+ */
49
+ export interface ColorPickerClampRecord {
50
+ /** OKLCH axis that was clamped. */
51
+ axis: 'l' | 'c' | 'h';
52
+ /** Channel value the user requested before the clamp. */
53
+ requested: number;
54
+ /** Channel value the picker actually committed after the clamp. */
55
+ clamped: number;
56
+ /** Constraint prop that triggered the clamp. */
57
+ reason: 'max-l' | 'min-l' | 'max-chroma' | 'hue-drift-max';
58
+ }
59
+
60
+ /**
61
+ * Detail payload for the `constraint-clamp` event — fired immediately
62
+ * before the corresponding `change` / `input` when one or more axes
63
+ * were clamped to fit the consumer's declared constraints. `clamps`
64
+ * is never empty.
65
+ */
66
+ export interface ColorPickerConstraintClampEventDetail {
67
+ clamps: ColorPickerClampRecord[];
20
68
  }
21
- export type ColorPickerFormatChangeEvent = CustomEvent<ColorPickerFormatChangeEventDetail>;
69
+ export type ColorPickerConstraintClampEvent = CustomEvent<ColorPickerConstraintClampEventDetail>;
22
70
 
23
71
  export class UIColorPicker extends UIFormElement {
72
+ /** Form field name. */
73
+ name: string;
74
+ /** Disables all interaction. */
75
+ disabled: boolean;
24
76
  /** Current color as a string in the active `format`. */
25
77
  value: string;
26
- /** Output format. */
78
+ /** Output format — drives the shape of `value`. Detail always carries both `hex` and `oklch`. */
27
79
  format: ColorFormat;
80
+ /** Clamp the OKLCH chroma channel to at most this value. `Infinity` = unconstrained. */
81
+ maxChroma: number;
82
+ /** Clamp the OKLCH lightness channel to at most this value (0..1). `1` = unconstrained. */
83
+ maxL: number;
84
+ /** Clamp the OKLCH lightness channel to at least this value (0..1). `0` = unconstrained. */
85
+ minL: number;
86
+ /** Maximum allowed signed-shortest-path hue drift in degrees from `baseHue`. `NaN` = unconstrained. */
87
+ hueDriftMax: number;
88
+ /** Reference hue (degrees) for the `hueDriftMax` constraint. `NaN` = use the picker's hue at first commit. */
89
+ baseHue: number;
28
90
 
29
91
  addEventListener<K extends keyof HTMLElementEventMap>(
30
92
  type: K,
@@ -33,5 +95,5 @@ export class UIColorPicker extends UIFormElement {
33
95
  ): void;
34
96
  addEventListener(type: 'change', listener: (ev: ColorPickerChangeEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
35
97
  addEventListener(type: 'input', listener: (ev: ColorPickerInputEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
36
- addEventListener(type: 'format-change', listener: (ev: ColorPickerFormatChangeEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
98
+ addEventListener(type: 'constraint-clamp', listener: (ev: ColorPickerConstraintClampEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
37
99
  }
@@ -32,11 +32,60 @@ props:
32
32
  description: Current color as hex string
33
33
  type: string
34
34
  default: "#3b82f6"
35
+ maxChroma:
36
+ description: |
37
+ Generation constraint (v0.4.9 §99h, FEEDBACK-02 #7) — clamp the
38
+ OKLCH chroma channel to at most this value before commit. Out-of-
39
+ bound mutations round-trip to the nearest in-bound equivalent +
40
+ fire `constraint-clamp`. Default Infinity (no constraint).
41
+ type: number
42
+ default: Infinity
43
+ reflect: true
44
+ maxL:
45
+ description: Generation constraint — clamp OKLCH lightness to at most this value (0..1). Default 1 (no constraint).
46
+ type: number
47
+ default: 1
48
+ reflect: true
49
+ minL:
50
+ description: Generation constraint — clamp OKLCH lightness to at least this value (0..1). Default 0 (no constraint).
51
+ type: number
52
+ default: 0
53
+ reflect: true
54
+ hueDriftMax:
55
+ description: |
56
+ Generation constraint — maximum allowed signed-shortest-path hue
57
+ deviation (degrees) from [base-hue] (or the first-committed hue
58
+ if [base-hue] is unset). Default NaN (no constraint). Wrap-aware
59
+ so a drift of 350 degrees resolves as -10.
60
+ type: number
61
+ default: NaN
62
+ reflect: true
63
+ baseHue:
64
+ description: |
65
+ Reference hue (degrees) for the [hue-drift-max] constraint. Default
66
+ NaN — falls back to the picker's hue at first commit so the consumer
67
+ can pre-seed the picker and constrain drift from that initial value.
68
+ type: number
69
+ default: NaN
70
+ reflect: true
35
71
  events:
36
72
  change:
37
73
  description: Fired on every color change
38
74
  input:
39
75
  description: Fired during continuous interaction (drag)
76
+ constraint-clamp:
77
+ description: |
78
+ Fired immediately before `change` / `input` when one or more
79
+ consumer-declared constraints (max-chroma / max-l / min-l /
80
+ hue-drift-max) clamped a channel away from the user-requested
81
+ value. detail.clamps is an array of axis-specific clamp records.
82
+ detail:
83
+ clamps:
84
+ type: array
85
+ description: |
86
+ Array of `{ axis, requested, clamped, reason }` objects. axis is
87
+ one of "l" / "c" / "h". reason names the triggering constraint
88
+ prop. Empty arrays are never emitted.
40
89
  slots: {}
41
90
  states:
42
91
  - name: idle