@adia-ai/web-components 0.4.6 → 0.4.7
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/USAGE.md +29 -9
- package/components/accordion/accordion.d.ts +17 -0
- package/components/accordion/accordion.js +10 -117
- package/components/accordion/class.js +132 -0
- package/components/action-list/action-list.d.ts +15 -0
- package/components/action-list/action-list.js +9 -140
- package/components/action-list/class.js +156 -0
- package/components/agent-artifact/agent-artifact.d.ts +25 -0
- package/components/agent-artifact/agent-artifact.js +8 -181
- package/components/agent-artifact/class.js +200 -0
- package/components/agent-feedback-bar/agent-feedback-bar.d.ts +21 -0
- package/components/agent-feedback-bar/agent-feedback-bar.js +8 -143
- package/components/agent-feedback-bar/class.js +162 -0
- package/components/agent-questions/agent-questions.d.ts +23 -0
- package/components/agent-questions/agent-questions.js +8 -180
- package/components/agent-questions/class.js +199 -0
- package/components/agent-reasoning/agent-reasoning.d.ts +23 -0
- package/components/agent-reasoning/agent-reasoning.js +8 -494
- package/components/agent-reasoning/class.js +513 -0
- package/components/agent-suggestions/agent-suggestions.d.ts +21 -0
- package/components/agent-suggestions/agent-suggestions.js +8 -78
- package/components/agent-suggestions/class.js +97 -0
- package/components/agent-trace/agent-trace.d.ts +19 -0
- package/components/alert/alert.d.ts +29 -0
- package/components/alert/alert.js +8 -175
- package/components/alert/class.js +194 -0
- package/components/avatar/avatar.d.ts +27 -0
- package/components/avatar/avatar.js +9 -159
- package/components/avatar/class.js +173 -0
- package/components/badge/badge.d.ts +27 -0
- package/components/badge/badge.js +9 -75
- package/components/badge/class.js +93 -0
- package/components/block/block.d.ts +19 -0
- package/components/block/block.js +9 -15
- package/components/block/class.js +33 -0
- package/components/breadcrumb/breadcrumb.d.ts +23 -0
- package/components/breadcrumb/breadcrumb.js +8 -113
- package/components/breadcrumb/class.js +132 -0
- package/components/button/button.d.ts +34 -0
- package/components/button/button.js +15 -66
- package/components/button/class.js +80 -0
- package/components/calendar-picker/calendar-picker.a2ui.json +6 -1
- package/components/calendar-picker/calendar-picker.js +8 -332
- package/components/calendar-picker/calendar-picker.yaml +51 -177
- package/components/calendar-picker/class.js +351 -0
- package/components/canvas/canvas.a2ui.json +6 -1
- package/components/canvas/canvas.d.ts +17 -0
- package/components/canvas/canvas.yaml +19 -36
- package/components/card/card.a2ui.json +3 -0
- package/components/card/card.d.ts +27 -0
- package/components/card/card.js +9 -50
- package/components/card/card.yaml +171 -433
- package/components/card/class.js +68 -0
- package/components/chart/chart.d.ts +41 -0
- package/components/chart/chart.js +8 -2131
- package/components/chart/class.js +2150 -0
- package/components/chart-legend/chart-legend.d.ts +27 -0
- package/components/chart-legend/chart-legend.js +8 -197
- package/components/chart-legend/class.js +215 -0
- package/components/chat-thread/chat-thread.d.ts +17 -0
- package/components/chat-thread/chat-thread.js +8 -157
- package/components/chat-thread/class.js +176 -0
- package/components/check/check.js +11 -52
- package/components/check/class.js +68 -0
- package/components/code/class.js +501 -0
- package/components/code/code.js +8 -482
- package/components/col/class.js +30 -0
- package/components/col/col.d.ts +23 -0
- package/components/col/col.js +10 -13
- package/components/color-picker/class.js +550 -0
- package/components/color-picker/color-picker.js +8 -531
- package/components/command/class.js +364 -0
- package/components/command/command.a2ui.json +3 -0
- package/components/command/command.d.ts +19 -0
- package/components/command/command.js +8 -345
- package/components/command/command.yaml +105 -124
- package/components/demo-toggle/class.js +153 -0
- package/components/demo-toggle/demo-toggle.d.ts +23 -0
- package/components/demo-toggle/demo-toggle.js +8 -135
- package/components/description-list/class.js +86 -0
- package/components/description-list/description-list.d.ts +21 -0
- package/components/description-list/description-list.js +8 -67
- package/components/divider/class.js +57 -0
- package/components/divider/divider.d.ts +19 -0
- package/components/divider/divider.js +10 -40
- package/components/drawer/class.js +306 -0
- package/components/drawer/drawer.d.ts +25 -0
- package/components/drawer/drawer.js +8 -287
- package/components/embed/class.js +73 -0
- package/components/embed/embed.d.ts +23 -0
- package/components/embed/embed.js +9 -55
- package/components/empty-state/class.js +108 -0
- package/components/empty-state/empty-state.d.ts +21 -0
- package/components/empty-state/empty-state.js +9 -90
- package/components/feed/class.js +381 -0
- package/components/feed/feed.d.ts +19 -0
- package/components/feed/feed.js +9 -367
- package/components/field/class.js +266 -0
- package/components/field/field.d.ts +23 -0
- package/components/field/field.js +8 -247
- package/components/fields/class.js +106 -0
- package/components/fields/fields.d.ts +19 -0
- package/components/fields/fields.js +8 -87
- package/components/grid/class.js +31 -0
- package/components/grid/grid.d.ts +23 -0
- package/components/grid/grid.js +10 -14
- package/components/heatmap/class.js +305 -0
- package/components/heatmap/heatmap.d.ts +31 -0
- package/components/heatmap/heatmap.js +8 -286
- package/components/icon/class.js +54 -0
- package/components/icon/icon.d.ts +23 -0
- package/components/icon/icon.js +13 -40
- package/components/image/class.js +112 -0
- package/components/image/image.d.ts +33 -0
- package/components/image/image.js +9 -94
- package/components/input/class.js +773 -0
- package/components/input/input.a2ui.json +3 -0
- package/components/input/input.js +8 -755
- package/components/input/input.yaml +171 -442
- package/components/inspector/class.js +142 -0
- package/components/inspector/inspector.a2ui.json +8 -1
- package/components/inspector/inspector.d.ts +17 -0
- package/components/inspector/inspector.js +8 -124
- package/components/inspector/inspector.yaml +15 -30
- package/components/kbd/class.js +34 -0
- package/components/kbd/kbd.a2ui.json +3 -0
- package/components/kbd/kbd.d.ts +17 -0
- package/components/kbd/kbd.js +10 -17
- package/components/kbd/kbd.yaml +54 -185
- package/components/link/class.js +187 -0
- package/components/link/link.d.ts +55 -0
- package/components/link/link.js +8 -168
- package/components/list/class.js +249 -0
- package/components/list/list.d.ts +23 -0
- package/components/list/list.js +9 -231
- package/components/menu/class.js +332 -0
- package/components/menu/menu.d.ts +21 -0
- package/components/menu/menu.js +11 -316
- package/components/modal/class.js +231 -0
- package/components/modal/modal.a2ui.json +5 -1
- package/components/modal/modal.d.ts +23 -0
- package/components/modal/modal.js +8 -212
- package/components/modal/modal.yaml +19 -39
- package/components/nav/class.js +150 -0
- package/components/nav/nav.d.ts +31 -0
- package/components/nav/nav.js +8 -131
- package/components/nav-group/class.js +152 -0
- package/components/nav-group/nav-group.d.ts +35 -0
- package/components/nav-group/nav-group.js +9 -134
- package/components/nav-item/class.js +86 -0
- package/components/nav-item/nav-item.d.ts +37 -0
- package/components/nav-item/nav-item.js +10 -69
- package/components/noodles/class.js +510 -0
- package/components/noodles/noodles.d.ts +33 -0
- package/components/noodles/noodles.js +9 -493
- package/components/option-card/class.js +167 -0
- package/components/option-card/option-card.js +8 -149
- package/components/otp-input/class.js +180 -0
- package/components/otp-input/otp-input.a2ui.json +5 -1
- package/components/otp-input/otp-input.js +9 -162
- package/components/otp-input/otp-input.yaml +45 -174
- package/components/page/class.js +97 -0
- package/components/page/page.d.ts +46 -0
- package/components/page/page.js +8 -79
- package/components/pagination/class.js +195 -0
- package/components/pagination/pagination.d.ts +23 -0
- package/components/pagination/pagination.js +9 -177
- package/components/pane/class.js +186 -0
- package/components/pane/pane.a2ui.json +12 -1
- package/components/pane/pane.d.ts +31 -0
- package/components/pane/pane.js +8 -167
- package/components/pane/pane.yaml +57 -157
- package/components/pipeline-status/class.js +189 -0
- package/components/pipeline-status/pipeline-status.a2ui.json +7 -1
- package/components/pipeline-status/pipeline-status.d.ts +21 -0
- package/components/pipeline-status/pipeline-status.js +9 -172
- package/components/pipeline-status/pipeline-status.yaml +34 -72
- package/components/popover/class.js +194 -0
- package/components/popover/popover.d.ts +23 -0
- package/components/popover/popover.js +9 -176
- package/components/progress/class.js +74 -0
- package/components/progress/progress.a2ui.json +3 -0
- package/components/progress/progress.d.ts +19 -0
- package/components/progress/progress.js +10 -57
- package/components/progress/progress.yaml +124 -287
- package/components/progress-row/class.js +110 -0
- package/components/progress-row/progress-row.d.ts +23 -0
- package/components/progress-row/progress-row.js +8 -92
- package/components/radio/class.js +83 -0
- package/components/radio/radio.js +11 -67
- package/components/range/class.js +194 -0
- package/components/range/range.js +9 -176
- package/components/rating/class.js +148 -0
- package/components/rating/rating.js +9 -130
- package/components/richtext/class.js +87 -0
- package/components/richtext/richtext.a2ui.json +7 -1
- package/components/richtext/richtext.d.ts +19 -0
- package/components/richtext/richtext.js +8 -68
- package/components/richtext/richtext.yaml +30 -65
- package/components/row/class.js +50 -0
- package/components/row/row.d.ts +27 -0
- package/components/row/row.js +10 -33
- package/components/search/class.js +134 -0
- package/components/search/search.js +10 -117
- package/components/segment/class.js +62 -0
- package/components/segment/segment.d.ts +25 -0
- package/components/segment/segment.js +10 -45
- package/components/segmented/class.js +165 -0
- package/components/segmented/segmented.a2ui.json +4 -0
- package/components/segmented/segmented.js +10 -148
- package/components/segmented/segmented.yaml +41 -59
- package/components/select/class.js +408 -0
- package/components/select/select.js +15 -396
- package/components/skeleton/class.js +52 -0
- package/components/skeleton/skeleton.d.ts +23 -0
- package/components/skeleton/skeleton.js +8 -34
- package/components/slider/class.js +184 -0
- package/components/slider/slider.js +9 -166
- package/components/stack/class.js +28 -0
- package/components/stack/stack.d.ts +17 -0
- package/components/stack/stack.js +10 -11
- package/components/step-progress/class.js +98 -0
- package/components/step-progress/step-progress.d.ts +27 -0
- package/components/step-progress/step-progress.js +8 -79
- package/components/stepper/class.js +126 -0
- package/components/stepper/stepper.d.ts +19 -0
- package/components/stepper/stepper.js +9 -112
- package/components/stream/class.js +109 -0
- package/components/stream/stream.d.ts +19 -0
- package/components/stream/stream.js +8 -90
- package/components/swatch/class.js +131 -0
- package/components/swatch/swatch.d.ts +28 -0
- package/components/swatch/swatch.js +8 -112
- package/components/swiper/class.js +373 -0
- package/components/swiper/swiper.a2ui.json +4 -0
- package/components/swiper/swiper.d.ts +31 -0
- package/components/swiper/swiper.js +8 -354
- package/components/swiper/swiper.yaml +68 -212
- package/components/switch/class.js +63 -0
- package/components/switch/switch.a2ui.json +6 -1
- package/components/switch/switch.js +11 -47
- package/components/switch/switch.yaml +70 -265
- package/components/table/class.js +1453 -0
- package/components/table/table.d.ts +37 -0
- package/components/table/table.js +8 -1435
- package/components/table-toolbar/class.js +680 -0
- package/components/table-toolbar/table-toolbar.d.ts +33 -0
- package/components/table-toolbar/table-toolbar.js +8 -689
- package/components/tabs/class.js +242 -0
- package/components/tabs/tabs.d.ts +21 -0
- package/components/tabs/tabs.js +8 -223
- package/components/tag/class.js +99 -0
- package/components/tag/tag.d.ts +27 -0
- package/components/tag/tag.js +8 -80
- package/components/text/class.js +46 -0
- package/components/text/text.d.ts +25 -0
- package/components/text/text.js +9 -28
- package/components/textarea/class.js +134 -0
- package/components/textarea/textarea.js +11 -118
- package/components/timeline/class.js +176 -0
- package/components/timeline/timeline.d.ts +19 -0
- package/components/timeline/timeline.js +9 -162
- package/components/toast/class.js +92 -0
- package/components/toast/toast.d.ts +23 -0
- package/components/toast/toast.js +9 -76
- package/components/toggle-group/class.js +154 -0
- package/components/toggle-group/toggle-group.d.ts +19 -0
- package/components/toggle-group/toggle-group.js +11 -140
- package/components/toggle-scheme/class.js +286 -0
- package/components/toggle-scheme/toggle-scheme.d.ts +41 -0
- package/components/toggle-scheme/toggle-scheme.js +8 -268
- package/components/toolbar/class.js +388 -0
- package/components/toolbar/toolbar.d.ts +23 -0
- package/components/toolbar/toolbar.js +10 -376
- package/components/tooltip/class.js +299 -0
- package/components/tooltip/tooltip.d.ts +27 -0
- package/components/tooltip/tooltip.js +8 -280
- package/components/tree/class.js +245 -0
- package/components/tree/tree.d.ts +15 -0
- package/components/tree/tree.js +9 -244
- package/components/upload/class.js +199 -0
- package/components/upload/upload.js +11 -183
- package/index.d.ts +159 -5
- package/package.json +5 -1
|
@@ -1,501 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* <div data-noodle-port="left right">Node A</div>
|
|
4
|
-
* <div data-noodle-port="top bottom">Node B</div>
|
|
5
|
-
* </noodles-ui>
|
|
2
|
+
* `<noodles-ui>` — auto-registers the tag on import.
|
|
6
3
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* For non-side-effect class import (test isolation, tag override), use
|
|
5
|
+
* the `class` subpath:
|
|
9
6
|
*
|
|
10
|
-
*
|
|
7
|
+
* import { UINoodles } from '@adia-ai/web-components/components/noodles/class';
|
|
8
|
+
*
|
|
9
|
+
* @see ../../USAGE.md#registration--auto-vs-explicit
|
|
11
10
|
*/
|
|
12
11
|
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
let nextId = 0;
|
|
16
|
-
|
|
17
|
-
class UINoodles extends UIElement {
|
|
18
|
-
static properties = {
|
|
19
|
-
editable: { type: Boolean, default: false, reflect: true },
|
|
20
|
-
color: { type: String, default: '', reflect: true },
|
|
21
|
-
strokeWidth: { type: Number, default: 2, reflect: true, attribute: 'stroke-width' },
|
|
22
|
-
tension: { type: Number, default: 0.5, reflect: true },
|
|
23
|
-
showPorts: { type: Boolean, default: false, reflect: true, attribute: 'show-ports' },
|
|
24
|
-
portSize: { type: Number, default: 10, attribute: 'port-size', reflect: true },
|
|
25
|
-
curve: { type: String, default: 'bezier', reflect: true },
|
|
26
|
-
animated: { type: Boolean, default: false, reflect: true },
|
|
27
|
-
readonly: { type: Boolean, default: false, reflect: true },
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
static template = () => null;
|
|
31
|
-
|
|
32
|
-
#svg = null;
|
|
33
|
-
#connections = [];
|
|
34
|
-
#portIndicators = new Map(); // key: "elIndex:side" -> DOM element
|
|
35
|
-
#resizeObs = null;
|
|
36
|
-
#mutationObs = null;
|
|
37
|
-
#rafId = null;
|
|
38
|
-
#bound = false;
|
|
39
|
-
#dragState = null;
|
|
40
|
-
#dragPath = null;
|
|
41
|
-
#dragCancel = null;
|
|
42
|
-
|
|
43
|
-
// ── Public API ──────────────────────────────────────────────
|
|
44
|
-
|
|
45
|
-
get connections() {
|
|
46
|
-
return this.#connections.map(c => ({ ...c }));
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
connect(from, to, fromPort = 'right', toPort = 'left') {
|
|
50
|
-
const id = `noodle-${++nextId}`;
|
|
51
|
-
const conn = { id, from, to, fromPort, toPort };
|
|
52
|
-
this.#connections.push(conn);
|
|
53
|
-
this.#scheduleUpdate();
|
|
54
|
-
this.dispatchEvent(new CustomEvent('noodle-connect', {
|
|
55
|
-
bubbles: true, detail: { ...conn },
|
|
56
|
-
}));
|
|
57
|
-
return id;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
disconnect(id) {
|
|
61
|
-
const idx = this.#connections.findIndex(c => c.id === id);
|
|
62
|
-
if (idx === -1) return;
|
|
63
|
-
const [conn] = this.#connections.splice(idx, 1);
|
|
64
|
-
this.#scheduleUpdate();
|
|
65
|
-
this.dispatchEvent(new CustomEvent('noodle-disconnect', {
|
|
66
|
-
bubbles: true, detail: { ...conn },
|
|
67
|
-
}));
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
setConnections(list) {
|
|
71
|
-
this.#connections = list.map(c => ({
|
|
72
|
-
id: c.id || `noodle-${++nextId}`,
|
|
73
|
-
from: c.from,
|
|
74
|
-
to: c.to,
|
|
75
|
-
fromPort: c.fromPort || 'right',
|
|
76
|
-
toPort: c.toPort || 'left',
|
|
77
|
-
}));
|
|
78
|
-
this.#scheduleUpdate();
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
clear() {
|
|
82
|
-
this.#connections = [];
|
|
83
|
-
this.#scheduleUpdate();
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
update() {
|
|
87
|
-
this.#performUpdate();
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// ── Lifecycle ───────────────────────────────────────────────
|
|
91
|
-
|
|
92
|
-
connected() {
|
|
93
|
-
// Create the SVG overlay
|
|
94
|
-
this.#svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
95
|
-
this.#svg.setAttribute('data-noodle-svg', '');
|
|
96
|
-
this.prepend(this.#svg);
|
|
97
|
-
|
|
98
|
-
// Observers for child layout changes
|
|
99
|
-
this.#resizeObs = new ResizeObserver(() => this.#scheduleUpdate());
|
|
100
|
-
this.#mutationObs = new MutationObserver(() => {
|
|
101
|
-
this.#rebuildPortIndicators();
|
|
102
|
-
this.#scheduleUpdate();
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
this.#resizeObs.observe(this);
|
|
106
|
-
this.#observeMutations();
|
|
107
|
-
|
|
108
|
-
// Observe each port-bearing child
|
|
109
|
-
for (const child of this.#getPortChildren()) {
|
|
110
|
-
this.#resizeObs.observe(child);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
this.#rebuildPortIndicators();
|
|
114
|
-
this.#scheduleUpdate();
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
render() {
|
|
118
|
-
if (!this.#svg) return;
|
|
119
|
-
// Update SVG attributes from properties
|
|
120
|
-
if (this.color) {
|
|
121
|
-
this.#svg.style.setProperty('--noodles-stroke', this.color);
|
|
122
|
-
} else {
|
|
123
|
-
this.#svg.style.removeProperty('--noodles-stroke');
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
disconnected() {
|
|
128
|
-
// Flush any in-flight drag so per-port pointermove/up listeners + their
|
|
129
|
-
// closure references are released before the host detaches.
|
|
130
|
-
this.#dragCancel?.();
|
|
131
|
-
this.#resizeObs?.disconnect();
|
|
132
|
-
this.#mutationObs?.disconnect();
|
|
133
|
-
if (this.#rafId) cancelAnimationFrame(this.#rafId);
|
|
134
|
-
this.#rafId = null;
|
|
135
|
-
this.#clearPortIndicators();
|
|
136
|
-
this.#svg?.remove();
|
|
137
|
-
this.#svg = null;
|
|
138
|
-
this.#bound = false;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// ── Update scheduling ───────────────────────────────────────
|
|
142
|
-
|
|
143
|
-
#scheduleUpdate() {
|
|
144
|
-
if (this.#rafId) return;
|
|
145
|
-
this.#rafId = requestAnimationFrame(() => {
|
|
146
|
-
this.#rafId = null;
|
|
147
|
-
this.#performUpdate();
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
#observeMutations() {
|
|
152
|
-
this.#mutationObs?.observe(this, {
|
|
153
|
-
childList: true,
|
|
154
|
-
subtree: true,
|
|
155
|
-
attributes: true,
|
|
156
|
-
attributeFilter: ['data-noodle-port', 'style'],
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
#pauseMutations() {
|
|
161
|
-
this.#mutationObs?.disconnect();
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
#performUpdate() {
|
|
165
|
-
if (!this.#svg) return;
|
|
166
|
-
this.#pauseMutations();
|
|
167
|
-
|
|
168
|
-
// Sync resize observer with current port children
|
|
169
|
-
const portChildren = this.#getPortChildren();
|
|
170
|
-
for (const child of portChildren) {
|
|
171
|
-
this.#resizeObs.observe(child);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Update port indicator positions
|
|
175
|
-
this.#updatePortIndicatorPositions();
|
|
176
|
-
|
|
177
|
-
// Render paths
|
|
178
|
-
const pathData = this.#connections.map(conn => {
|
|
179
|
-
const fromEl = this.#resolveElement(conn.from);
|
|
180
|
-
const toEl = this.#resolveElement(conn.to);
|
|
181
|
-
if (!fromEl || !toEl) return null;
|
|
182
|
-
|
|
183
|
-
const p1 = this.#getPortPosition(fromEl, conn.fromPort);
|
|
184
|
-
const p2 = this.#getPortPosition(toEl, conn.toPort);
|
|
185
|
-
if (!p1 || !p2) return null;
|
|
186
|
-
|
|
187
|
-
return { id: conn.id, d: this.#computePath(p1, p2, conn.fromPort, conn.toPort) };
|
|
188
|
-
}).filter(Boolean);
|
|
189
|
-
|
|
190
|
-
// Reconcile SVG path elements
|
|
191
|
-
const existing = new Map();
|
|
192
|
-
for (const path of this.#svg.querySelectorAll('path[data-noodle-id]')) {
|
|
193
|
-
existing.set(path.getAttribute('data-noodle-id'), path);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const activeIds = new Set(pathData.map(p => p.id));
|
|
197
|
-
|
|
198
|
-
// Remove stale paths
|
|
199
|
-
for (const [id, path] of existing) {
|
|
200
|
-
if (!activeIds.has(id)) path.remove();
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Add/update paths
|
|
204
|
-
for (const { id, d } of pathData) {
|
|
205
|
-
let path = existing.get(id);
|
|
206
|
-
if (!path) {
|
|
207
|
-
path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
208
|
-
path.setAttribute('data-noodle-id', id);
|
|
209
|
-
this.#svg.appendChild(path);
|
|
210
|
-
}
|
|
211
|
-
path.setAttribute('d', d);
|
|
212
|
-
path.setAttribute('stroke-width', this.strokeWidth);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// Render drag preview path
|
|
216
|
-
if (this.#dragState && this.#dragPath) {
|
|
217
|
-
let preview = this.#svg.querySelector('path[data-noodle-preview]');
|
|
218
|
-
if (!preview) {
|
|
219
|
-
preview = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
220
|
-
preview.setAttribute('data-noodle-preview', '');
|
|
221
|
-
preview.style.opacity = '0.5';
|
|
222
|
-
preview.style.strokeDasharray = '6 3';
|
|
223
|
-
this.#svg.appendChild(preview);
|
|
224
|
-
}
|
|
225
|
-
preview.setAttribute('d', this.#dragPath);
|
|
226
|
-
preview.setAttribute('stroke-width', this.strokeWidth);
|
|
227
|
-
} else {
|
|
228
|
-
this.#svg.querySelector('path[data-noodle-preview]')?.remove();
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
this.#observeMutations();
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// ── Element resolution ──────────────────────────────────────
|
|
235
|
-
|
|
236
|
-
#resolveElement(ref) {
|
|
237
|
-
if (ref instanceof HTMLElement) return ref;
|
|
238
|
-
if (typeof ref === 'string') return this.querySelector(`#${CSS.escape(ref)}`);
|
|
239
|
-
if (typeof ref === 'number') return this.#getPortChildren()[ref] ?? null;
|
|
240
|
-
return null;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
#getPortChildren() {
|
|
244
|
-
return [...this.querySelectorAll('[data-noodle-port]')];
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// ── Port position computation ──────────────────────────────
|
|
248
|
-
// Uses bounding client rects so nested port-bearing descendants
|
|
249
|
-
// (e.g. inside an absolutely-positioned card wrapper) project
|
|
250
|
-
// correctly into the noodles-ui coordinate system.
|
|
251
|
-
//
|
|
252
|
-
// When an ancestor applies a CSS transform (zoom/pan in a graph
|
|
253
|
-
// editor), bounding rects are in *screen* pixels — but SVG paths
|
|
254
|
-
// and port-indicator dots are positioned in *local* (untransformed)
|
|
255
|
-
// pixels. We detect any ancestor scale by comparing this element's
|
|
256
|
-
// visible width to its layout width and divide deltas accordingly.
|
|
257
|
-
// No transform → ratio is 1, no-op.
|
|
258
|
-
#getPortPosition(el, side) {
|
|
259
|
-
const elRect = el.getBoundingClientRect();
|
|
260
|
-
const myRect = this.getBoundingClientRect();
|
|
261
|
-
const localScale = (this.offsetWidth && myRect.width)
|
|
262
|
-
? (myRect.width / this.offsetWidth)
|
|
263
|
-
: 1;
|
|
264
|
-
const left = (elRect.left - myRect.left) / localScale;
|
|
265
|
-
const top = (elRect.top - myRect.top) / localScale;
|
|
266
|
-
const w = elRect.width / localScale;
|
|
267
|
-
const h = elRect.height / localScale;
|
|
268
|
-
|
|
269
|
-
switch (side) {
|
|
270
|
-
case 'left': return { x: left, y: top + h / 2 };
|
|
271
|
-
case 'right': return { x: left + w, y: top + h / 2 };
|
|
272
|
-
case 'top': return { x: left + w / 2, y: top };
|
|
273
|
-
case 'bottom': return { x: left + w / 2, y: top + h };
|
|
274
|
-
default: return { x: left + w / 2, y: top + h / 2 };
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// ── Path computation ───────────────────────────────────────
|
|
279
|
-
|
|
280
|
-
#computePath(p1, p2, fromSide, toSide) {
|
|
281
|
-
switch (this.curve) {
|
|
282
|
-
case 'step': return this.#stepPath(p1, p2);
|
|
283
|
-
case 'straight': return this.#straightPath(p1, p2);
|
|
284
|
-
case 'bezier':
|
|
285
|
-
default: return this.#bezierPath(p1, p2, fromSide, toSide);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
#bezierPath(p1, p2, fromSide, toSide) {
|
|
290
|
-
const dx = Math.abs(p2.x - p1.x);
|
|
291
|
-
const dy = Math.abs(p2.y - p1.y);
|
|
292
|
-
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
293
|
-
const offset = Math.max(50, dist * this.tension);
|
|
294
|
-
|
|
295
|
-
const c1 = this.#controlPoint(p1, fromSide, offset);
|
|
296
|
-
const c2 = this.#controlPoint(p2, toSide, offset);
|
|
297
|
-
|
|
298
|
-
return `M ${p1.x} ${p1.y} C ${c1.x} ${c1.y}, ${c2.x} ${c2.y}, ${p2.x} ${p2.y}`;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
#controlPoint(p, side, offset) {
|
|
302
|
-
switch (side) {
|
|
303
|
-
case 'left': return { x: p.x - offset, y: p.y };
|
|
304
|
-
case 'right': return { x: p.x + offset, y: p.y };
|
|
305
|
-
case 'top': return { x: p.x, y: p.y - offset };
|
|
306
|
-
case 'bottom': return { x: p.x, y: p.y + offset };
|
|
307
|
-
default: return { x: p.x + offset, y: p.y };
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
#stepPath(p1, p2) {
|
|
312
|
-
const midX = (p1.x + p2.x) / 2;
|
|
313
|
-
return `M ${p1.x} ${p1.y} L ${midX} ${p1.y} L ${midX} ${p2.y} L ${p2.x} ${p2.y}`;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
#straightPath(p1, p2) {
|
|
317
|
-
return `M ${p1.x} ${p1.y} L ${p2.x} ${p2.y}`;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// ── Port indicators ────────────────────────────────────────
|
|
321
|
-
|
|
322
|
-
#rebuildPortIndicators() {
|
|
323
|
-
this.#pauseMutations();
|
|
324
|
-
this.#clearPortIndicators();
|
|
325
|
-
|
|
326
|
-
const children = this.#getPortChildren();
|
|
327
|
-
for (const child of children) {
|
|
328
|
-
const sides = (child.getAttribute('data-noodle-port') || '').split(/\s+/).filter(Boolean);
|
|
329
|
-
for (const side of sides) {
|
|
330
|
-
const key = this.#portKey(child, side);
|
|
331
|
-
const dot = document.createElement('div');
|
|
332
|
-
dot.setAttribute('data-noodle-port-indicator', '');
|
|
333
|
-
dot.setAttribute('data-noodle-side', side);
|
|
334
|
-
dot.dataset.portElement = child.id || '';
|
|
335
|
-
|
|
336
|
-
if (this.editable) {
|
|
337
|
-
dot.addEventListener('pointerdown', (e) => this.#onPortPointerDown(e, child, side));
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
this.appendChild(dot);
|
|
341
|
-
this.#portIndicators.set(key, dot);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
this.#updatePortIndicatorPositions();
|
|
346
|
-
this.#observeMutations();
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
#clearPortIndicators() {
|
|
350
|
-
for (const [, dot] of this.#portIndicators) {
|
|
351
|
-
dot.remove();
|
|
352
|
-
}
|
|
353
|
-
this.#portIndicators.clear();
|
|
354
|
-
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
#updatePortIndicatorPositions() {
|
|
358
|
-
const children = this.#getPortChildren();
|
|
359
|
-
for (const child of children) {
|
|
360
|
-
const sides = (child.getAttribute('data-noodle-port') || '').split(/\s+/).filter(Boolean);
|
|
361
|
-
for (const side of sides) {
|
|
362
|
-
const key = this.#portKey(child, side);
|
|
363
|
-
const dot = this.#portIndicators.get(key);
|
|
364
|
-
if (!dot) continue;
|
|
365
|
-
|
|
366
|
-
const pos = this.#getPortPosition(child, side);
|
|
367
|
-
dot.style.left = `${pos.x}px`;
|
|
368
|
-
dot.style.top = `${pos.y}px`;
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
#portKey(el, side) {
|
|
374
|
-
// Use a stable identifier
|
|
375
|
-
if (!el._noodleId) el._noodleId = `_n${++nextId}`;
|
|
376
|
-
return `${el._noodleId}:${side}`;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// ── Drag to connect ────────────────────────────────────────
|
|
380
|
-
|
|
381
|
-
#onPortPointerDown = (e, fromEl, fromSide) => {
|
|
382
|
-
if (!this.editable || this.readonly) return;
|
|
383
|
-
e.preventDefault();
|
|
384
|
-
e.stopPropagation();
|
|
385
|
-
|
|
386
|
-
const dot = e.currentTarget;
|
|
387
|
-
dot.setPointerCapture(e.pointerId);
|
|
388
|
-
dot.setAttribute('data-noodle-dragging', '');
|
|
389
|
-
|
|
390
|
-
const startPos = this.#getPortPosition(fromEl, fromSide);
|
|
391
|
-
|
|
392
|
-
const onMove = (ev) => {
|
|
393
|
-
if (!this.#dragState) return;
|
|
394
|
-
const rect = this.getBoundingClientRect();
|
|
395
|
-
const mx = ev.clientX - rect.left;
|
|
396
|
-
const my = ev.clientY - rect.top;
|
|
397
|
-
|
|
398
|
-
this.#dragPath = this.#computePath(
|
|
399
|
-
startPos, { x: mx, y: my }, fromSide, this.#inferSide(startPos, { x: mx, y: my })
|
|
400
|
-
);
|
|
401
|
-
this.#scheduleUpdate();
|
|
402
|
-
|
|
403
|
-
// Highlight potential drop targets
|
|
404
|
-
this.#updateDropTargets(ev.clientX, ev.clientY, fromEl);
|
|
405
|
-
|
|
406
|
-
this.dispatchEvent(new CustomEvent('noodle-drag', {
|
|
407
|
-
bubbles: true,
|
|
408
|
-
detail: { from: fromEl, fromPort: fromSide, x: mx, y: my },
|
|
409
|
-
}));
|
|
410
|
-
};
|
|
411
|
-
|
|
412
|
-
const onUp = (ev) => {
|
|
413
|
-
try { dot.releasePointerCapture(ev.pointerId); } catch (_) { /* already released */ }
|
|
414
|
-
dot.removeAttribute('data-noodle-dragging');
|
|
415
|
-
dot.removeEventListener('pointermove', onMove);
|
|
416
|
-
dot.removeEventListener('pointerup', onUp);
|
|
417
|
-
|
|
418
|
-
// Check if dropped on a target port (skip on synthetic disconnect cancel).
|
|
419
|
-
if (ev.type === 'pointerup' && ev.clientX !== undefined) {
|
|
420
|
-
const target = this.#findDropTarget(ev.clientX, ev.clientY, fromEl);
|
|
421
|
-
if (target) this.connect(fromEl, target.el, fromSide, target.side);
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
this.#clearDropTargets();
|
|
425
|
-
this.#dragState = null;
|
|
426
|
-
this.#dragPath = null;
|
|
427
|
-
this.#dragCancel = null;
|
|
428
|
-
this.#scheduleUpdate();
|
|
429
|
-
};
|
|
430
|
-
|
|
431
|
-
// Capture the handlers so `disconnected()` can flush a mid-drag drag
|
|
432
|
-
// by calling the same onUp tear-down without a synthetic event.
|
|
433
|
-
this.#dragCancel = () => {
|
|
434
|
-
this.#dragState = null;
|
|
435
|
-
dot.removeAttribute('data-noodle-dragging');
|
|
436
|
-
dot.removeEventListener('pointermove', onMove);
|
|
437
|
-
dot.removeEventListener('pointerup', onUp);
|
|
438
|
-
this.#clearDropTargets();
|
|
439
|
-
this.#dragPath = null;
|
|
440
|
-
this.#dragCancel = null;
|
|
441
|
-
};
|
|
442
|
-
|
|
443
|
-
this.#dragState = { fromEl, fromSide, startPos, pointerId: e.pointerId };
|
|
444
|
-
this.#dragPath = null;
|
|
445
|
-
|
|
446
|
-
dot.addEventListener('pointermove', onMove);
|
|
447
|
-
dot.addEventListener('pointerup', onUp);
|
|
448
|
-
};
|
|
449
|
-
|
|
450
|
-
#inferSide(from, to) {
|
|
451
|
-
const dx = to.x - from.x;
|
|
452
|
-
const dy = to.y - from.y;
|
|
453
|
-
if (Math.abs(dx) > Math.abs(dy)) {
|
|
454
|
-
return dx > 0 ? 'left' : 'right';
|
|
455
|
-
}
|
|
456
|
-
return dy > 0 ? 'top' : 'bottom';
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
#updateDropTargets(clientX, clientY, excludeEl) {
|
|
460
|
-
this.#clearDropTargets();
|
|
461
|
-
const target = this.#findDropTarget(clientX, clientY, excludeEl);
|
|
462
|
-
if (target?.dot) {
|
|
463
|
-
target.dot.setAttribute('data-noodle-drop-ready', '');
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
#findDropTarget(clientX, clientY, excludeEl) {
|
|
468
|
-
const hitRadius = this.portSize * 2;
|
|
469
|
-
let best = null;
|
|
470
|
-
let bestDist = Infinity;
|
|
471
|
-
|
|
472
|
-
for (const [key, dot] of this.#portIndicators) {
|
|
473
|
-
const rect = dot.getBoundingClientRect();
|
|
474
|
-
const cx = rect.left + rect.width / 2;
|
|
475
|
-
const cy = rect.top + rect.height / 2;
|
|
476
|
-
const dist = Math.sqrt((clientX - cx) ** 2 + (clientY - cy) ** 2);
|
|
477
|
-
|
|
478
|
-
// Parse the key to find the element
|
|
479
|
-
const [noodleId, side] = key.split(':');
|
|
480
|
-
const el = this.#getPortChildren().find(c => c._noodleId === noodleId);
|
|
481
|
-
if (!el || el === excludeEl) continue;
|
|
482
|
-
|
|
483
|
-
if (dist < hitRadius && dist < bestDist) {
|
|
484
|
-
bestDist = dist;
|
|
485
|
-
best = { el, side, dot };
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
return best;
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
#clearDropTargets() {
|
|
493
|
-
for (const [, dot] of this.#portIndicators) {
|
|
494
|
-
dot.removeAttribute('data-noodle-drop-ready');
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
}
|
|
12
|
+
import { defineIfFree } from '../../core/register.js';
|
|
13
|
+
import { UINoodles } from './class.js';
|
|
498
14
|
|
|
499
|
-
|
|
15
|
+
defineIfFree('noodles-ui', UINoodles);
|
|
500
16
|
|
|
501
17
|
export { UINoodles };
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Non-side-effect class export for `<option-card-ui>`.
|
|
3
|
+
*
|
|
4
|
+
* Importing this file gives you the class(es) without auto-registering the tag.
|
|
5
|
+
* Useful for test isolation, subclassing with tag-name override, or selective
|
|
6
|
+
* composition.
|
|
7
|
+
*
|
|
8
|
+
* The auto-register path stays at `@adia-ai/web-components/components/option-card`
|
|
9
|
+
* (which imports this file + calls `defineIfFree()`).
|
|
10
|
+
*
|
|
11
|
+
* @see ../../USAGE.md#registration--auto-vs-explicit
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* <option-card-ui> — Selectable card with radio semantics.
|
|
16
|
+
*
|
|
17
|
+
* A "rich radio" — single-select-of-N where each option carries a
|
|
18
|
+
* heading, optional description, and optional leading icon. Siblings
|
|
19
|
+
* with the same `name` form a radiogroup. The whole card is the click
|
|
20
|
+
* target; a CSS-rendered radio circle in the top-left signals state.
|
|
21
|
+
*
|
|
22
|
+
* <option-card-ui name="use-case" value="build" checked
|
|
23
|
+
* heading="I'm building a product"
|
|
24
|
+
* description="Spinning up a new project — design, ship, iterate.">
|
|
25
|
+
* </option-card-ui>
|
|
26
|
+
*
|
|
27
|
+
* Rich content via slots:
|
|
28
|
+
*
|
|
29
|
+
* <option-card-ui name="plan" value="pro">
|
|
30
|
+
* <span slot="heading">Pro <badge-ui text="14-day trial"></badge-ui></span>
|
|
31
|
+
* <span slot="description">Unlimited members · advanced charts</span>
|
|
32
|
+
* </option-card-ui>
|
|
33
|
+
*
|
|
34
|
+
* Spillover content via the default slot — any unslotted child shows
|
|
35
|
+
* only when the card is `[checked]`. Used for "Other"-style fields:
|
|
36
|
+
*
|
|
37
|
+
* <option-card-ui name="reason" value="other"
|
|
38
|
+
* heading="Something else">
|
|
39
|
+
* <textarea-ui name="reason-detail" rows="3"></textarea-ui>
|
|
40
|
+
* </option-card-ui>
|
|
41
|
+
*
|
|
42
|
+
* Sibling navigation: arrow keys move focus and selection; Space/Enter
|
|
43
|
+
* select. Form-associated via UIFormElement, so `name=value` submits
|
|
44
|
+
* with the parent form when the card is checked.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
import { UIFormElement } from '../../core/form.js';
|
|
48
|
+
|
|
49
|
+
export class UIOptionCard extends UIFormElement {
|
|
50
|
+
static properties = {
|
|
51
|
+
...UIFormElement.properties,
|
|
52
|
+
checked: { type: Boolean, default: false, reflect: true },
|
|
53
|
+
heading: { type: String, default: '', reflect: true },
|
|
54
|
+
description: { type: String, default: '', reflect: true },
|
|
55
|
+
icon: { type: String, default: '', reflect: true },
|
|
56
|
+
layout: { type: String, default: 'default', reflect: true },
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
static template = () => null;
|
|
60
|
+
|
|
61
|
+
connected() {
|
|
62
|
+
super.connected();
|
|
63
|
+
this.setAttribute('role', 'radio');
|
|
64
|
+
this.setAttribute('tabindex', '0');
|
|
65
|
+
this.#ensureLayout();
|
|
66
|
+
this.addEventListener('click', this.#select);
|
|
67
|
+
this.addEventListener('keydown', this.#onKey);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
disconnected() {
|
|
71
|
+
super.disconnected();
|
|
72
|
+
this.removeEventListener('click', this.#select);
|
|
73
|
+
this.removeEventListener('keydown', this.#onKey);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
render() {
|
|
77
|
+
this.setAttribute('aria-checked', String(this.checked));
|
|
78
|
+
if (this.disabled) this.setAttribute('aria-disabled', 'true');
|
|
79
|
+
else this.removeAttribute('aria-disabled');
|
|
80
|
+
|
|
81
|
+
// Keep slotted content in sync with attrs (only when slot was
|
|
82
|
+
// auto-stamped from the attr — don't clobber consumer-authored
|
|
83
|
+
// rich content).
|
|
84
|
+
const h = this.querySelector(':scope > [slot="heading"]');
|
|
85
|
+
if (h && h.dataset.fromAttr === 'true') h.textContent = this.heading || '';
|
|
86
|
+
const d = this.querySelector(':scope > [slot="description"]');
|
|
87
|
+
if (d && d.dataset.fromAttr === 'true') d.textContent = this.description || '';
|
|
88
|
+
const i = this.querySelector(':scope > [slot="icon"]');
|
|
89
|
+
if (i && i.dataset.fromAttr === 'true') i.setAttribute('name', this.icon || '');
|
|
90
|
+
|
|
91
|
+
if (this.checked) this.syncValue(this.value || 'on');
|
|
92
|
+
else this.syncValue('');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Private ───────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
/** Stamp heading / description / icon from attributes when the
|
|
98
|
+
* consumer hasn't already provided slotted content. */
|
|
99
|
+
#ensureLayout() {
|
|
100
|
+
if (this.heading && !this.querySelector(':scope > [slot="heading"]')) {
|
|
101
|
+
const el = document.createElement('span');
|
|
102
|
+
el.setAttribute('slot', 'heading');
|
|
103
|
+
el.dataset.fromAttr = 'true';
|
|
104
|
+
el.textContent = this.heading;
|
|
105
|
+
this.appendChild(el);
|
|
106
|
+
}
|
|
107
|
+
if (this.description && !this.querySelector(':scope > [slot="description"]')) {
|
|
108
|
+
const el = document.createElement('span');
|
|
109
|
+
el.setAttribute('slot', 'description');
|
|
110
|
+
el.dataset.fromAttr = 'true';
|
|
111
|
+
el.textContent = this.description;
|
|
112
|
+
this.appendChild(el);
|
|
113
|
+
}
|
|
114
|
+
if (this.icon && !this.querySelector(':scope > [slot="icon"]')) {
|
|
115
|
+
const el = document.createElement('icon-ui');
|
|
116
|
+
el.setAttribute('slot', 'icon');
|
|
117
|
+
el.dataset.fromAttr = 'true';
|
|
118
|
+
el.setAttribute('name', this.icon);
|
|
119
|
+
this.appendChild(el);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#select = () => {
|
|
124
|
+
if (this.disabled || this.readonly || this.checked) return;
|
|
125
|
+
const group = this.#group();
|
|
126
|
+
if (group) {
|
|
127
|
+
for (const el of group.querySelectorAll(`option-card-ui[name="${this.name}"]`)) {
|
|
128
|
+
if (el !== this && el.checked) el.checked = false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
this.checked = true;
|
|
132
|
+
this.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: this.value, checked: this.checked } }));
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
#onKey = (e) => {
|
|
136
|
+
// Don't intercept keys when focus is in a nested form control
|
|
137
|
+
// (textarea, input, contenteditable). Otherwise Space/Enter would
|
|
138
|
+
// block typing and arrow keys would navigate siblings instead of
|
|
139
|
+
// moving the cursor.
|
|
140
|
+
if (e.target !== this) return;
|
|
141
|
+
if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); this.#select(); return; }
|
|
142
|
+
if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
|
|
143
|
+
e.preventDefault();
|
|
144
|
+
const next = this.#sibling(1);
|
|
145
|
+
if (next) { next.focus(); next.click(); }
|
|
146
|
+
}
|
|
147
|
+
if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') {
|
|
148
|
+
e.preventDefault();
|
|
149
|
+
const prev = this.#sibling(-1);
|
|
150
|
+
if (prev) { prev.focus(); prev.click(); }
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/** The radiogroup root — explicit `<fieldset>` / `[role="radiogroup"]`
|
|
155
|
+
* if present, else the immediate parent. */
|
|
156
|
+
#group() {
|
|
157
|
+
return this.closest('fieldset, [role="radiogroup"]') || this.parentElement;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
#sibling(dir) {
|
|
161
|
+
const group = this.#group();
|
|
162
|
+
const items = [...(group?.querySelectorAll(`option-card-ui[name="${this.name}"]`) || [])];
|
|
163
|
+
const i = items.indexOf(this);
|
|
164
|
+
if (i < 0) return null;
|
|
165
|
+
return items[(i + dir + items.length) % items.length] || null;
|
|
166
|
+
}
|
|
167
|
+
}
|