@adia-ai/web-components 0.2.3 → 0.2.4
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/components/button/button.js +3 -0
- package/components/demo-toggle/demo-toggle.a2ui.json +144 -0
- package/components/demo-toggle/demo-toggle.css +120 -0
- package/components/demo-toggle/demo-toggle.js +144 -0
- package/components/demo-toggle/demo-toggle.test.js +102 -0
- package/components/demo-toggle/demo-toggle.yaml +144 -0
- package/components/index.js +1 -0
- package/components/input/input.js +11 -0
- package/components/list/list.css +21 -0
- package/components/textarea/textarea.js +10 -0
- package/core/icons.js +12 -1
- package/package.json +1 -1
- package/styles/components.css +1 -0
- package/styles/typography.css +1 -1
- package/traits/_catalog.json +257 -4
- package/traits/active-state.test.js +1 -1
- package/traits/anchor-positioning.js +205 -52
- package/traits/anchor-positioning.test.js +77 -4
- package/traits/announcer-stage.js +157 -0
- package/traits/announcer.js +145 -0
- package/traits/announcer.test.js +268 -0
- package/traits/arrow-grid-nav.js +234 -0
- package/traits/arrow-grid-nav.test.js +375 -0
- package/traits/attention-pulse.js +1 -1
- package/traits/attention-pulse.test.js +1 -1
- package/traits/confetti-burst.js +67 -63
- package/traits/confetti-burst.test.js +16 -8
- package/traits/confetti-stage.js +143 -0
- package/traits/confetti.js +44 -47
- package/traits/confetti.test.js +24 -5
- package/traits/count-up.js +31 -6
- package/traits/count-up.test.js +1 -1
- package/traits/declarative.test.js +1 -1
- package/traits/dirty-state.test.js +1 -1
- package/traits/drag-ghost.js +43 -3
- package/traits/drag-ghost.test.js +1 -1
- package/traits/draggable-list-item.js +279 -0
- package/traits/draggable-list-item.test.js +51 -0
- package/traits/draggable.js +14 -4
- package/traits/draggable.test.js +1 -1
- package/traits/drop-target.js +223 -0
- package/traits/drop-target.test.js +241 -0
- package/traits/droppable-collection.js +89 -0
- package/traits/droppable-collection.test.js +99 -0
- package/traits/droppable.js +125 -0
- package/traits/droppable.test.js +54 -0
- package/traits/error-shake.js +157 -0
- package/traits/error-shake.test.js +114 -0
- package/traits/fade-presence.test.js +1 -1
- package/traits/focus-restore.js +135 -0
- package/traits/focus-restore.test.js +202 -0
- package/traits/focus-trap.test.js +1 -1
- package/traits/focusable.test.js +1 -1
- package/traits/glow-focus.js +1 -1
- package/traits/glow-focus.test.js +1 -1
- package/traits/gradient-shift.js +1 -1
- package/traits/gradient-shift.test.js +1 -1
- package/traits/haptic-feedback.test.js +1 -1
- package/traits/hotkey.test.js +1 -1
- package/traits/hoverable.test.js +1 -1
- package/traits/index.js +15 -0
- package/traits/inertia-drag.js +9 -0
- package/traits/inertia-drag.test.js +1 -1
- package/traits/input-mask.js +328 -0
- package/traits/input-mask.test.js +151 -0
- package/traits/intersection-observer.test.js +1 -1
- package/traits/keyboard-nav.test.js +1 -1
- package/traits/keyboard-reorderable.js +254 -0
- package/traits/keyboard-reorderable.test.js +45 -0
- package/traits/layout-animation.js +229 -0
- package/traits/layout-animation.test.js +114 -0
- package/traits/long-press.js +212 -0
- package/traits/long-press.test.js +244 -0
- package/traits/magnetic-hover.js +1 -1
- package/traits/magnetic-hover.test.js +1 -1
- package/traits/noise-texture.js +7 -3
- package/traits/noise-texture.test.js +1 -1
- package/traits/parallax.js +1 -1
- package/traits/parallax.test.js +1 -1
- package/traits/portal.test.js +1 -1
- package/traits/pressable.test.js +1 -1
- package/traits/resettable.js +29 -3
- package/traits/resettable.test.js +34 -1
- package/traits/resizable.test.js +1 -1
- package/traits/resize-observer.test.js +1 -1
- package/traits/ripple.js +1 -1
- package/traits/ripple.test.js +1 -1
- package/traits/roving-tabindex.test.js +1 -1
- package/traits/scale-press.test.js +1 -1
- package/traits/scroll-lock.test.js +1 -1
- package/traits/scroll-progress.js +201 -0
- package/traits/scroll-progress.test.js +182 -0
- package/traits/shimmer-loading.js +1 -1
- package/traits/shimmer-loading.test.js +1 -1
- package/traits/{_smoke.test.js → smoke.test.js} +1 -1
- package/traits/snap-to-grid.test.js +1 -1
- package/traits/sound-feedback.test.js +1 -1
- package/traits/spring-animate.test.js +1 -1
- package/traits/success-checkmark.js +222 -0
- package/traits/success-checkmark.test.js +120 -0
- package/traits/tilt-hover.js +1 -1
- package/traits/tilt-hover.test.js +1 -1
- package/traits/tossable.js +9 -0
- package/traits/tossable.test.js +1 -1
- package/traits/traits-host.test.js +1 -1
- package/traits/typeahead.test.js +1 -1
- package/traits/typewriter.js +1 -1
- package/traits/typewriter.test.js +1 -1
- package/traits/validation.test.js +1 -1
- package/traits/view-transition.js +140 -0
- package/traits/view-transition.test.js +268 -0
- /package/traits/{_motion.js → motion.js} +0 -0
- /package/traits/{_test-helpers.js → test-helpers.js} +0 -0
|
@@ -43,6 +43,9 @@ class UIButton extends UIElement {
|
|
|
43
43
|
if (this.type === 'submit') {
|
|
44
44
|
const form = this.closest('form');
|
|
45
45
|
if (form?.requestSubmit) form.requestSubmit();
|
|
46
|
+
} else if (this.type === 'reset') {
|
|
47
|
+
const form = this.closest('form');
|
|
48
|
+
if (form?.reset) form.reset();
|
|
46
49
|
}
|
|
47
50
|
this.dispatchEvent(new Event('press', { bubbles: true }));
|
|
48
51
|
};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://adiaui.dev/a2ui/v0_9/components/DemoToggle.json",
|
|
4
|
+
"title": "DemoToggle",
|
|
5
|
+
"description": "Side-by-side comparison primitive — header bar with a switch + two child slots (\"on\" / \"off\"); toggling the switch swaps which slot is visible. Used on trait detail pages to show \"with trait\" vs \"without trait\" on the same chrome. data-mode=\"overlay\" stacks the slots on the same coordinates so layout never shifts.",
|
|
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
|
+
"component": {
|
|
17
|
+
"const": "DemoToggle"
|
|
18
|
+
},
|
|
19
|
+
"initial": {
|
|
20
|
+
"description": "Initial state when [state] is not set on connect (\"on\" | \"off\").",
|
|
21
|
+
"type": "string",
|
|
22
|
+
"enum": [
|
|
23
|
+
"on",
|
|
24
|
+
"off"
|
|
25
|
+
],
|
|
26
|
+
"default": "off"
|
|
27
|
+
},
|
|
28
|
+
"labelOff": {
|
|
29
|
+
"description": "Header label rendered when state is \"off\".",
|
|
30
|
+
"type": "string",
|
|
31
|
+
"default": "Off"
|
|
32
|
+
},
|
|
33
|
+
"labelOn": {
|
|
34
|
+
"description": "Header label rendered when state is \"on\".",
|
|
35
|
+
"type": "string",
|
|
36
|
+
"default": "On"
|
|
37
|
+
},
|
|
38
|
+
"state": {
|
|
39
|
+
"description": "Current toggle state (\"on\" | \"off\"). Reflected as data-state on the host.",
|
|
40
|
+
"type": "string",
|
|
41
|
+
"enum": [
|
|
42
|
+
"",
|
|
43
|
+
"on",
|
|
44
|
+
"off"
|
|
45
|
+
],
|
|
46
|
+
"default": ""
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"required": [
|
|
50
|
+
"component"
|
|
51
|
+
],
|
|
52
|
+
"unevaluatedProperties": false,
|
|
53
|
+
"x-adiaui": {
|
|
54
|
+
"anti_patterns": [],
|
|
55
|
+
"category": "container",
|
|
56
|
+
"events": {
|
|
57
|
+
"change": {
|
|
58
|
+
"description": "Fired when the toggle flips. detail contains { state }."
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"examples": [
|
|
62
|
+
{
|
|
63
|
+
"description": "Compare a trait-bearing surface against the bare control on the same chrome.",
|
|
64
|
+
"a2ui": "[\n {\n \"id\": \"root\",\n \"component\": \"DemoToggle\",\n \"labelOn\": \"With shimmer-loading\",\n \"labelOff\": \"Without shimmer-loading\",\n \"initial\": \"off\",\n \"children\": [\"off\", \"on\"]\n },\n {\n \"id\": \"off\",\n \"component\": \"Card\",\n \"slot\": \"off\",\n \"children\": [\"off-section\"]\n },\n {\n \"id\": \"off-section\",\n \"component\": \"Section\",\n \"children\": [\"off-text\"]\n },\n {\n \"id\": \"off-text\",\n \"component\": \"Text\",\n \"textContent\": \"Bare surface — no trait attached.\"\n },\n {\n \"id\": \"on\",\n \"component\": \"Card\",\n \"slot\": \"on\",\n \"children\": [\"on-section\"]\n },\n {\n \"id\": \"on-section\",\n \"component\": \"Section\",\n \"children\": [\"on-text\"]\n },\n {\n \"id\": \"on-text\",\n \"component\": \"Text\",\n \"textContent\": \"Same surface, with trait attached.\"\n }\n]",
|
|
65
|
+
"name": "with-without-trait"
|
|
66
|
+
}
|
|
67
|
+
],
|
|
68
|
+
"keywords": [
|
|
69
|
+
"demo",
|
|
70
|
+
"toggle",
|
|
71
|
+
"compare",
|
|
72
|
+
"comparison",
|
|
73
|
+
"trait",
|
|
74
|
+
"before-after",
|
|
75
|
+
"a-b",
|
|
76
|
+
"switch"
|
|
77
|
+
],
|
|
78
|
+
"name": "UIDemoToggle",
|
|
79
|
+
"related": [
|
|
80
|
+
"Switch",
|
|
81
|
+
"Card"
|
|
82
|
+
],
|
|
83
|
+
"slots": {
|
|
84
|
+
"bar": {
|
|
85
|
+
"description": "Internal header bar (auto-stamped). Hosts the embedded switch + label."
|
|
86
|
+
},
|
|
87
|
+
"off": {
|
|
88
|
+
"description": "Surface rendered when state is \"off\" (the \"without trait\" variant)."
|
|
89
|
+
},
|
|
90
|
+
"on": {
|
|
91
|
+
"description": "Surface rendered when state is \"on\" (the \"with trait\" variant)."
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
"states": [
|
|
95
|
+
{
|
|
96
|
+
"description": "Default, ready for interaction.",
|
|
97
|
+
"name": "idle"
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"description": "On slot is active; bar leans accent.",
|
|
101
|
+
"attribute": "data-state",
|
|
102
|
+
"name": "on"
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"description": "Off slot is active; bar is neutral.",
|
|
106
|
+
"attribute": "data-state",
|
|
107
|
+
"name": "off"
|
|
108
|
+
}
|
|
109
|
+
],
|
|
110
|
+
"synonyms": {
|
|
111
|
+
"compare": [
|
|
112
|
+
"demo-toggle",
|
|
113
|
+
"segmented"
|
|
114
|
+
],
|
|
115
|
+
"switch": [
|
|
116
|
+
"demo-toggle",
|
|
117
|
+
"toggle-group"
|
|
118
|
+
]
|
|
119
|
+
},
|
|
120
|
+
"tag": "demo-toggle-ui",
|
|
121
|
+
"tokens": {
|
|
122
|
+
"--demo-toggle-bar-bg": {
|
|
123
|
+
"description": "Header bar background (accent when state=on)"
|
|
124
|
+
},
|
|
125
|
+
"--demo-toggle-bar-fg": {
|
|
126
|
+
"description": "Header bar foreground"
|
|
127
|
+
},
|
|
128
|
+
"--demo-toggle-bg": {
|
|
129
|
+
"description": "Stage background"
|
|
130
|
+
},
|
|
131
|
+
"--demo-toggle-border": {
|
|
132
|
+
"description": "Border color"
|
|
133
|
+
},
|
|
134
|
+
"--demo-toggle-radius": {
|
|
135
|
+
"description": "Outer border-radius"
|
|
136
|
+
},
|
|
137
|
+
"--demo-toggle-stage-padding": {
|
|
138
|
+
"description": "Inner padding around each slot"
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
"traits": [],
|
|
142
|
+
"version": 1
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
@scope (demo-toggle-ui) {
|
|
2
|
+
:where(:scope) {
|
|
3
|
+
/* ── Layout ── */
|
|
4
|
+
--demo-toggle-radius: var(--a-radius-lg);
|
|
5
|
+
--demo-toggle-gap: var(--a-space-3);
|
|
6
|
+
--demo-toggle-stage-padding: var(--a-space-4);
|
|
7
|
+
--demo-toggle-bar-px: var(--a-space-3);
|
|
8
|
+
--demo-toggle-bar-py: var(--a-space-2);
|
|
9
|
+
|
|
10
|
+
/* ── Colors ── */
|
|
11
|
+
--demo-toggle-bg: var(--a-canvas-1);
|
|
12
|
+
--demo-toggle-border: var(--a-border);
|
|
13
|
+
--demo-toggle-bar-bg: var(--a-bg-muted);
|
|
14
|
+
--demo-toggle-bar-fg: var(--a-fg);
|
|
15
|
+
|
|
16
|
+
/* ── Typography ── */
|
|
17
|
+
--demo-toggle-label-size: var(--a-ui-size);
|
|
18
|
+
--demo-toggle-label-weight: var(--a-weight-medium, 500);
|
|
19
|
+
|
|
20
|
+
/* ── Transition ── */
|
|
21
|
+
--demo-toggle-duration: var(--a-duration-fast);
|
|
22
|
+
--demo-toggle-easing: var(--a-easing);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
:scope {
|
|
26
|
+
/* ── Base ── */
|
|
27
|
+
box-sizing: border-box;
|
|
28
|
+
display: flex;
|
|
29
|
+
flex-direction: column;
|
|
30
|
+
gap: 0;
|
|
31
|
+
width: 100%;
|
|
32
|
+
|
|
33
|
+
background: var(--demo-toggle-bg);
|
|
34
|
+
border: 1px solid var(--demo-toggle-border);
|
|
35
|
+
border-radius: var(--demo-toggle-radius);
|
|
36
|
+
overflow: hidden;
|
|
37
|
+
isolation: isolate;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* -- Header bar -- */
|
|
41
|
+
[slot="bar"] {
|
|
42
|
+
width: 100%;
|
|
43
|
+
padding: var(--demo-toggle-bar-py) var(--demo-toggle-bar-px);
|
|
44
|
+
background: var(--demo-toggle-bar-bg);
|
|
45
|
+
color: var(--demo-toggle-bar-fg);
|
|
46
|
+
border-bottom: 1px solid var(--demo-toggle-border);
|
|
47
|
+
justify-content: space-between;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
[data-demo-toggle-label] {
|
|
51
|
+
font-size: var(--demo-toggle-label-size);
|
|
52
|
+
font-weight: var(--demo-toggle-label-weight);
|
|
53
|
+
color: var(--demo-toggle-bar-fg);
|
|
54
|
+
transition: color var(--demo-toggle-duration) var(--demo-toggle-easing);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* -- Stage area (where the slotted content lives) -- */
|
|
58
|
+
/* Slotted children are anything that isn't the [slot="bar"]. They sit in
|
|
59
|
+
the second flex row; default mode shows only the active slot. */
|
|
60
|
+
:scope > [slot="on"],
|
|
61
|
+
:scope > [slot="off"] {
|
|
62
|
+
box-sizing: border-box;
|
|
63
|
+
width: 100%;
|
|
64
|
+
padding: var(--demo-toggle-stage-padding);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
:scope[data-state="on"] > [slot="off"] { display: none; }
|
|
68
|
+
:scope[data-state="off"] > [slot="on"] { display: none; }
|
|
69
|
+
|
|
70
|
+
/* Overlay mode — both slots stack on the same coordinates so toggling
|
|
71
|
+
never reflows the surrounding layout. The inactive slot is
|
|
72
|
+
visibility-hidden (still occupying space → still measurable for the
|
|
73
|
+
active one). The stack is a 1-cell grid; both slots target row 1
|
|
74
|
+
col 1 via grid-area. */
|
|
75
|
+
:scope[data-mode="overlay"] {
|
|
76
|
+
/* The bar is row 1; the stage cell is row 2. The cell's children all
|
|
77
|
+
claim the same grid-area so they overlap. */
|
|
78
|
+
}
|
|
79
|
+
:scope[data-mode="overlay"] > [slot="on"],
|
|
80
|
+
:scope[data-mode="overlay"] > [slot="off"] {
|
|
81
|
+
display: block;
|
|
82
|
+
}
|
|
83
|
+
:scope[data-mode="overlay"] {
|
|
84
|
+
/* Use a wrapper-less stack: rely on the parent flex column for the
|
|
85
|
+
bar/stage stack and turn the stage region into a relative anchor
|
|
86
|
+
via the second slot's positioning. */
|
|
87
|
+
position: relative;
|
|
88
|
+
}
|
|
89
|
+
:scope[data-mode="overlay"] > [slot="on"] {
|
|
90
|
+
position: absolute;
|
|
91
|
+
inset: 0;
|
|
92
|
+
/* Bar height — the slot starts beneath the bar. We can't query the
|
|
93
|
+
bar's height in CSS, so the stage region uses the larger of the
|
|
94
|
+
two slot intrinsic heights (default flow), and the active
|
|
95
|
+
slot positions on top via inset:0 starting after the bar. The
|
|
96
|
+
enclosing :scope provides padding-top via the bar; the slot's
|
|
97
|
+
own padding still applies. */
|
|
98
|
+
top: auto;
|
|
99
|
+
height: auto;
|
|
100
|
+
}
|
|
101
|
+
:scope[data-mode="overlay"][data-state="on"] > [slot="off"] {
|
|
102
|
+
visibility: hidden;
|
|
103
|
+
}
|
|
104
|
+
:scope[data-mode="overlay"][data-state="off"] > [slot="on"] {
|
|
105
|
+
visibility: hidden;
|
|
106
|
+
}
|
|
107
|
+
/* Reset display:none branches above for overlay — already handled by
|
|
108
|
+
visibility:hidden, but the default-mode display:none rules above
|
|
109
|
+
would otherwise fight overlay. Override here. */
|
|
110
|
+
:scope[data-mode="overlay"][data-state="on"] > [slot="off"],
|
|
111
|
+
:scope[data-mode="overlay"][data-state="off"] > [slot="on"] {
|
|
112
|
+
display: block;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* State-themed bar — when on, lean accent; when off, leave neutral. */
|
|
116
|
+
:scope[data-state="on"] {
|
|
117
|
+
--demo-toggle-bar-bg: var(--a-accent-muted);
|
|
118
|
+
--demo-toggle-bar-fg: var(--a-accent-strong);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <demo-toggle-ui>
|
|
3
|
+
* <section slot="off">…without trait…</section>
|
|
4
|
+
* <section slot="on">…with trait…</section>
|
|
5
|
+
* </demo-toggle-ui>
|
|
6
|
+
*
|
|
7
|
+
* Side-by-side "with trait / without trait" comparison primitive.
|
|
8
|
+
* Renders a header bar with a switch-ui that flips which child slot is
|
|
9
|
+
* visible. Reflects [data-state="on"|"off"] on the host so consumer CSS
|
|
10
|
+
* can theme the bar around the active variant.
|
|
11
|
+
*
|
|
12
|
+
* Two layout modes:
|
|
13
|
+
* default — only the active slot is in the layout (the other is `display: none`).
|
|
14
|
+
* data-mode="overlay" — both slots stack on the same coordinates; inactive
|
|
15
|
+
* slot is `visibility: hidden` so layout never shifts.
|
|
16
|
+
*
|
|
17
|
+
* Authoring API:
|
|
18
|
+
* [label-on] — header label when state=on (default: "On")
|
|
19
|
+
* [label-off] — header label when state=off (default: "Off")
|
|
20
|
+
* [initial] — "on" | "off"; only consulted when [state] is unset on connect
|
|
21
|
+
* [state] — "on" | "off"; reflected; the live state attribute
|
|
22
|
+
*
|
|
23
|
+
* Events:
|
|
24
|
+
* change — { detail: { state: "on" | "off" } }
|
|
25
|
+
*
|
|
26
|
+
* Keyboard:
|
|
27
|
+
* The embedded <switch-ui> handles space + enter via its own keydown wiring.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { UIElement } from '../../core/element.js';
|
|
31
|
+
import '../switch/switch.js';
|
|
32
|
+
import '../row/row.js';
|
|
33
|
+
import '../text/text.js';
|
|
34
|
+
|
|
35
|
+
class UIDemoToggle extends UIElement {
|
|
36
|
+
static properties = {
|
|
37
|
+
labelOn: { type: String, default: 'On', attribute: 'label-on', reflect: true },
|
|
38
|
+
labelOff: { type: String, default: 'Off', attribute: 'label-off', reflect: true },
|
|
39
|
+
initial: { type: String, default: 'off', reflect: true },
|
|
40
|
+
state: { type: String, default: '', reflect: true },
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Header bar with a switch + active label. Stamped once per class via
|
|
44
|
+
// static parts; live label text is updated in render() so the same
|
|
45
|
+
// template covers every (label-on, label-off, state) tuple.
|
|
46
|
+
static parts = {
|
|
47
|
+
bar: `
|
|
48
|
+
<row-ui slot="bar" gap="3" align="center" data-demo-toggle-bar>
|
|
49
|
+
<text-ui slot="label" data-demo-toggle-label></text-ui>
|
|
50
|
+
<switch-ui slot="switch" data-demo-toggle-switch></switch-ui>
|
|
51
|
+
</row-ui>
|
|
52
|
+
`,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
static template = () => null;
|
|
56
|
+
|
|
57
|
+
#bar = null;
|
|
58
|
+
#switch = null;
|
|
59
|
+
#label = null;
|
|
60
|
+
#bound = false;
|
|
61
|
+
|
|
62
|
+
connected() {
|
|
63
|
+
// Resolve the initial state. Honor [state] if explicitly authored;
|
|
64
|
+
// otherwise fall back to [initial] (default "off"). Anything other
|
|
65
|
+
// than the literal "on" coerces to "off" so authors can't poison
|
|
66
|
+
// the data-state attribute with arbitrary values.
|
|
67
|
+
if (!this.state) {
|
|
68
|
+
const seed = (this.initial === 'on') ? 'on' : 'off';
|
|
69
|
+
this.state = seed;
|
|
70
|
+
} else {
|
|
71
|
+
this.state = (this.state === 'on') ? 'on' : 'off';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Stamp the header bar lazily — `this.ensure('bar')` clones the
|
|
75
|
+
// blueprint into the light DOM the first time it's called.
|
|
76
|
+
this.#bar = this.ensure('bar');
|
|
77
|
+
this.#switch = this.#bar.querySelector('[data-demo-toggle-switch]');
|
|
78
|
+
this.#label = this.#bar.querySelector('[data-demo-toggle-label]');
|
|
79
|
+
|
|
80
|
+
if (!this.#bound) {
|
|
81
|
+
this.#bound = true;
|
|
82
|
+
this.#switch.addEventListener('change', this.#onSwitchChange);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
disconnected() {
|
|
87
|
+
if (this.#switch) {
|
|
88
|
+
this.#switch.removeEventListener('change', this.#onSwitchChange);
|
|
89
|
+
}
|
|
90
|
+
this.#bar = null;
|
|
91
|
+
this.#switch = null;
|
|
92
|
+
this.#label = null;
|
|
93
|
+
this.#bound = false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
render() {
|
|
97
|
+
if (!this.#bar) return;
|
|
98
|
+
const on = this.state === 'on';
|
|
99
|
+
|
|
100
|
+
// Sync the embedded switch to the host's state. checked is a
|
|
101
|
+
// reflected property; the assignment fires no event because
|
|
102
|
+
// syncValue() short-circuits when the next state matches.
|
|
103
|
+
if (this.#switch.checked !== on) {
|
|
104
|
+
this.#switch.checked = on;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (this.#label) {
|
|
108
|
+
this.#label.textContent = on ? this.labelOn : this.labelOff;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Public toggle method — flips the host between on and off and emits
|
|
114
|
+
* a `change` event. Mirrors the click-on-switch path so consumers
|
|
115
|
+
* can drive the state from outside without touching the embedded
|
|
116
|
+
* switch directly.
|
|
117
|
+
*/
|
|
118
|
+
toggle() {
|
|
119
|
+
this.state = (this.state === 'on') ? 'off' : 'on';
|
|
120
|
+
this.dispatchEvent(new CustomEvent('change', {
|
|
121
|
+
bubbles: true,
|
|
122
|
+
detail: { state: this.state },
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
#onSwitchChange = (e) => {
|
|
127
|
+
// The embedded <switch-ui> fires a bubbling `change`. Stop it at the
|
|
128
|
+
// host boundary — we re-dispatch our own `change` with detail.state
|
|
129
|
+
// so consumers see exactly one event per toggle, not the inner
|
|
130
|
+
// switch's bare event AND our annotated one.
|
|
131
|
+
e.stopPropagation();
|
|
132
|
+
const next = this.#switch.checked ? 'on' : 'off';
|
|
133
|
+
if (next === this.state) return;
|
|
134
|
+
this.state = next;
|
|
135
|
+
this.dispatchEvent(new CustomEvent('change', {
|
|
136
|
+
bubbles: true,
|
|
137
|
+
detail: { state: this.state },
|
|
138
|
+
}));
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
customElements.define('demo-toggle-ui', UIDemoToggle);
|
|
143
|
+
|
|
144
|
+
export { UIDemoToggle };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import '../../core/element.js';
|
|
3
|
+
import './demo-toggle.js';
|
|
4
|
+
|
|
5
|
+
const tick = () => new Promise((r) => queueMicrotask(r));
|
|
6
|
+
|
|
7
|
+
function mount(html) {
|
|
8
|
+
const wrap = document.createElement('div');
|
|
9
|
+
wrap.innerHTML = html;
|
|
10
|
+
document.body.appendChild(wrap);
|
|
11
|
+
return wrap.firstElementChild;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('demo-toggle-ui', () => {
|
|
15
|
+
beforeEach(() => { document.body.innerHTML = ''; });
|
|
16
|
+
|
|
17
|
+
it('registers demo-toggle-ui as a custom element', () => {
|
|
18
|
+
expect(customElements.get('demo-toggle-ui')).toBeDefined();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('defaults to state="off" when [initial] is not set', async () => {
|
|
22
|
+
const el = mount('<demo-toggle-ui></demo-toggle-ui>');
|
|
23
|
+
await tick();
|
|
24
|
+
expect(el.state).toBe('off');
|
|
25
|
+
expect(el.getAttribute('state')).toBe('off');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('honors [initial="on"] on connect', async () => {
|
|
29
|
+
const el = mount('<demo-toggle-ui initial="on"></demo-toggle-ui>');
|
|
30
|
+
await tick();
|
|
31
|
+
expect(el.state).toBe('on');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('renders the auto-stamped header bar with switch + label', async () => {
|
|
35
|
+
const el = mount('<demo-toggle-ui label-on="Foo" label-off="Bar"></demo-toggle-ui>');
|
|
36
|
+
await tick();
|
|
37
|
+
const bar = el.querySelector('[slot="bar"]');
|
|
38
|
+
const sw = el.querySelector('[data-demo-toggle-switch]');
|
|
39
|
+
const label = el.querySelector('[data-demo-toggle-label]');
|
|
40
|
+
expect(bar).toBeTruthy();
|
|
41
|
+
expect(sw).toBeTruthy();
|
|
42
|
+
expect(label).toBeTruthy();
|
|
43
|
+
// state defaults to "off"; label-off should be active
|
|
44
|
+
expect(label.textContent).toBe('Bar');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('toggle() flips state and emits change with detail.state', async () => {
|
|
48
|
+
const el = mount('<demo-toggle-ui></demo-toggle-ui>');
|
|
49
|
+
await tick();
|
|
50
|
+
const events = [];
|
|
51
|
+
el.addEventListener('change', (e) => events.push(e.detail));
|
|
52
|
+
|
|
53
|
+
el.toggle();
|
|
54
|
+
expect(el.state).toBe('on');
|
|
55
|
+
expect(events).toHaveLength(1);
|
|
56
|
+
expect(events[0]).toEqual({ state: 'on' });
|
|
57
|
+
|
|
58
|
+
el.toggle();
|
|
59
|
+
expect(el.state).toBe('off');
|
|
60
|
+
expect(events).toHaveLength(2);
|
|
61
|
+
expect(events[1]).toEqual({ state: 'off' });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('clicking the embedded switch flips host state and bubbles change', async () => {
|
|
65
|
+
const el = mount('<demo-toggle-ui></demo-toggle-ui>');
|
|
66
|
+
await tick();
|
|
67
|
+
const sw = el.querySelector('[data-demo-toggle-switch]');
|
|
68
|
+
|
|
69
|
+
const events = [];
|
|
70
|
+
el.addEventListener('change', (e) => events.push(e.detail));
|
|
71
|
+
|
|
72
|
+
sw.click();
|
|
73
|
+
await tick();
|
|
74
|
+
expect(el.state).toBe('on');
|
|
75
|
+
expect(events).toHaveLength(1);
|
|
76
|
+
expect(events[0]).toEqual({ state: 'on' });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('reflects state to the [state] attribute', async () => {
|
|
80
|
+
const el = mount('<demo-toggle-ui initial="on"></demo-toggle-ui>');
|
|
81
|
+
await tick();
|
|
82
|
+
expect(el.getAttribute('state')).toBe('on');
|
|
83
|
+
el.toggle();
|
|
84
|
+
expect(el.getAttribute('state')).toBe('off');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('coerces an invalid [state] attribute to "off"', async () => {
|
|
88
|
+
const el = mount('<demo-toggle-ui state="garbage"></demo-toggle-ui>');
|
|
89
|
+
await tick();
|
|
90
|
+
expect(el.state).toBe('off');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('updates the active label when state flips', async () => {
|
|
94
|
+
const el = mount('<demo-toggle-ui label-on="With trait" label-off="Without trait"></demo-toggle-ui>');
|
|
95
|
+
await tick();
|
|
96
|
+
const label = el.querySelector('[data-demo-toggle-label]');
|
|
97
|
+
expect(label.textContent).toBe('Without trait');
|
|
98
|
+
el.toggle();
|
|
99
|
+
await tick();
|
|
100
|
+
expect(label.textContent).toBe('With trait');
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Edit this file; run `node scripts/build/components.mjs` to regenerate a2ui.json.
|
|
2
|
+
$schema: ../../../../scripts/schemas/component.yaml.schema.json
|
|
3
|
+
name: UIDemoToggle
|
|
4
|
+
tag: demo-toggle-ui
|
|
5
|
+
component: DemoToggle
|
|
6
|
+
category: container
|
|
7
|
+
version: 1
|
|
8
|
+
description: >-
|
|
9
|
+
Side-by-side comparison primitive — header bar with a switch + two child
|
|
10
|
+
slots ("on" / "off"); toggling the switch swaps which slot is visible.
|
|
11
|
+
Used on trait detail pages to show "with trait" vs "without trait" on
|
|
12
|
+
the same chrome. data-mode="overlay" stacks the slots on the same
|
|
13
|
+
coordinates so layout never shifts.
|
|
14
|
+
props:
|
|
15
|
+
labelOn:
|
|
16
|
+
description: Header label rendered when state is "on".
|
|
17
|
+
type: string
|
|
18
|
+
default: "On"
|
|
19
|
+
attribute: label-on
|
|
20
|
+
reflect: true
|
|
21
|
+
labelOff:
|
|
22
|
+
description: Header label rendered when state is "off".
|
|
23
|
+
type: string
|
|
24
|
+
default: "Off"
|
|
25
|
+
attribute: label-off
|
|
26
|
+
reflect: true
|
|
27
|
+
initial:
|
|
28
|
+
description: Initial state when [state] is not set on connect ("on" | "off").
|
|
29
|
+
type: string
|
|
30
|
+
default: "off"
|
|
31
|
+
enum:
|
|
32
|
+
- "on"
|
|
33
|
+
- "off"
|
|
34
|
+
reflect: true
|
|
35
|
+
state:
|
|
36
|
+
description: Current toggle state ("on" | "off"). Reflected as data-state on the host.
|
|
37
|
+
type: string
|
|
38
|
+
default: ""
|
|
39
|
+
enum:
|
|
40
|
+
- ""
|
|
41
|
+
- "on"
|
|
42
|
+
- "off"
|
|
43
|
+
reflect: true
|
|
44
|
+
events:
|
|
45
|
+
change:
|
|
46
|
+
description: Fired when the toggle flips. detail contains { state }.
|
|
47
|
+
slots:
|
|
48
|
+
bar:
|
|
49
|
+
description: Internal header bar (auto-stamped). Hosts the embedded switch + label.
|
|
50
|
+
"on":
|
|
51
|
+
description: Surface rendered when state is "on" (the "with trait" variant).
|
|
52
|
+
"off":
|
|
53
|
+
description: Surface rendered when state is "off" (the "without trait" variant).
|
|
54
|
+
states:
|
|
55
|
+
- name: idle
|
|
56
|
+
description: Default, ready for interaction.
|
|
57
|
+
- name: on
|
|
58
|
+
description: On slot is active; bar leans accent.
|
|
59
|
+
attribute: data-state
|
|
60
|
+
- name: off
|
|
61
|
+
description: Off slot is active; bar is neutral.
|
|
62
|
+
attribute: data-state
|
|
63
|
+
traits: []
|
|
64
|
+
tokens:
|
|
65
|
+
--demo-toggle-radius:
|
|
66
|
+
description: Outer border-radius
|
|
67
|
+
--demo-toggle-bg:
|
|
68
|
+
description: Stage background
|
|
69
|
+
--demo-toggle-border:
|
|
70
|
+
description: Border color
|
|
71
|
+
--demo-toggle-bar-bg:
|
|
72
|
+
description: Header bar background (accent when state=on)
|
|
73
|
+
--demo-toggle-bar-fg:
|
|
74
|
+
description: Header bar foreground
|
|
75
|
+
--demo-toggle-stage-padding:
|
|
76
|
+
description: Inner padding around each slot
|
|
77
|
+
a2ui:
|
|
78
|
+
rules: []
|
|
79
|
+
anti_patterns: []
|
|
80
|
+
examples:
|
|
81
|
+
- name: with-without-trait
|
|
82
|
+
description: Compare a trait-bearing surface against the bare control on the same chrome.
|
|
83
|
+
a2ui: >-
|
|
84
|
+
[
|
|
85
|
+
{
|
|
86
|
+
"id": "root",
|
|
87
|
+
"component": "DemoToggle",
|
|
88
|
+
"labelOn": "With shimmer-loading",
|
|
89
|
+
"labelOff": "Without shimmer-loading",
|
|
90
|
+
"initial": "off",
|
|
91
|
+
"children": ["off", "on"]
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"id": "off",
|
|
95
|
+
"component": "Card",
|
|
96
|
+
"slot": "off",
|
|
97
|
+
"children": ["off-section"]
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"id": "off-section",
|
|
101
|
+
"component": "Section",
|
|
102
|
+
"children": ["off-text"]
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"id": "off-text",
|
|
106
|
+
"component": "Text",
|
|
107
|
+
"textContent": "Bare surface — no trait attached."
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"id": "on",
|
|
111
|
+
"component": "Card",
|
|
112
|
+
"slot": "on",
|
|
113
|
+
"children": ["on-section"]
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"id": "on-section",
|
|
117
|
+
"component": "Section",
|
|
118
|
+
"children": ["on-text"]
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"id": "on-text",
|
|
122
|
+
"component": "Text",
|
|
123
|
+
"textContent": "Same surface, with trait attached."
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
keywords:
|
|
127
|
+
- demo
|
|
128
|
+
- toggle
|
|
129
|
+
- compare
|
|
130
|
+
- comparison
|
|
131
|
+
- trait
|
|
132
|
+
- before-after
|
|
133
|
+
- a-b
|
|
134
|
+
- switch
|
|
135
|
+
synonyms:
|
|
136
|
+
compare:
|
|
137
|
+
- demo-toggle
|
|
138
|
+
- segmented
|
|
139
|
+
switch:
|
|
140
|
+
- demo-toggle
|
|
141
|
+
- toggle-group
|
|
142
|
+
related:
|
|
143
|
+
- Switch
|
|
144
|
+
- Card
|
package/components/index.js
CHANGED
|
@@ -85,6 +85,7 @@ export { UIEmbed } from './embed/embed.js';
|
|
|
85
85
|
export { UIBlock } from './block/block.js';
|
|
86
86
|
export { UIText } from './text/text.js';
|
|
87
87
|
export { UIToggleGroup, UIToggleOption } from './toggle-group/toggle-group.js';
|
|
88
|
+
export { UIDemoToggle } from './demo-toggle/demo-toggle.js';
|
|
88
89
|
export { UIRichText } from './richtext/richtext.js';
|
|
89
90
|
export { UIStream } from './stream/stream.js';
|
|
90
91
|
export { UICanvas } from './canvas/canvas.js';
|