@adia-ai/web-components 0.0.33 → 0.0.34

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.
@@ -48,6 +48,7 @@ export { UIUpload } from './upload/upload.js';
48
48
  export { UICard } from './card/card.js';
49
49
  export { UIAvatar, UIAvatarGroup } from './avatar/avatar.js';
50
50
  export { UIProgress } from './progress/progress.js';
51
+ export { UIStepProgress } from './step-progress/step-progress.js';
51
52
  export { UISkeleton } from './skeleton/skeleton.js';
52
53
  export { UIAlert } from './alert/alert.js';
53
54
  export { UIKbd } from './kbd/kbd.js';
@@ -0,0 +1,111 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://adiaui.dev/a2ui/v0_9/components/StepProgress.json",
4
+ "title": "StepProgress",
5
+ "description": "Discrete-dash step-progress indicator. One <span> dash per step in\n[total]; the first [value] dashes are filled with --a-accent. Used\nin multi-step flows (registration, onboarding, wizards). Replaces\nthe bespoke `[data-onb-progress]` + `[data-reg-progress]` markup\nthat lived in onboarding.css + registration.css with a single\nprimitive.\n",
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
+ "caption": {
17
+ "description": "Optional caption text rendered above the track (e.g. 'Step 3 of 10', 'Step 3 of 10 · Optional', 'All steps complete'). Hidden when empty.",
18
+ "type": "string",
19
+ "default": ""
20
+ },
21
+ "component": {
22
+ "const": "StepProgress"
23
+ },
24
+ "total": {
25
+ "description": "Total number of steps. The track renders this many dashes.",
26
+ "type": "number",
27
+ "default": 0
28
+ },
29
+ "value": {
30
+ "description": "Number of steps completed (1-indexed). Clamped to [0, total].",
31
+ "type": "number",
32
+ "default": 0
33
+ }
34
+ },
35
+ "required": [
36
+ "component"
37
+ ],
38
+ "unevaluatedProperties": false,
39
+ "x-adiaui": {
40
+ "anti_patterns": [],
41
+ "category": "feedback",
42
+ "events": {},
43
+ "examples": [
44
+ {
45
+ "description": "3 of 10 steps complete with caption.",
46
+ "a2ui": "[\n {\n \"id\": \"root\",\n \"component\": \"StepProgress\",\n \"value\": 3,\n \"total\": 10,\n \"caption\": \"Step 3 of 10\"\n }\n]",
47
+ "name": "in-progress"
48
+ },
49
+ {
50
+ "description": "All steps complete.",
51
+ "a2ui": "[\n {\n \"id\": \"root\",\n \"component\": \"StepProgress\",\n \"value\": 10,\n \"total\": 10,\n \"caption\": \"All steps complete\"\n }\n]",
52
+ "name": "complete"
53
+ },
54
+ {
55
+ "description": "A 9-of-10 step that's optional.",
56
+ "a2ui": "[\n {\n \"id\": \"root\",\n \"component\": \"StepProgress\",\n \"value\": 9,\n \"total\": 10,\n \"caption\": \"Step 9 of 10 · Optional\"\n }\n]",
57
+ "name": "optional-step"
58
+ }
59
+ ],
60
+ "keywords": [
61
+ "progress",
62
+ "step",
63
+ "wizard",
64
+ "onboarding",
65
+ "registration",
66
+ "multi-step",
67
+ "indicator"
68
+ ],
69
+ "name": "UIStepProgress",
70
+ "related": [
71
+ "progress-ui",
72
+ "stepper-ui"
73
+ ],
74
+ "slots": {
75
+ "caption": {
76
+ "description": "Optional override of the auto-minted caption (use for rich content like icon + text). When present, the [caption] attribute is ignored."
77
+ },
78
+ "track": {
79
+ "description": "Optional override of the auto-minted track (rare — use only when the dash structure needs to differ)."
80
+ }
81
+ },
82
+ "states": [
83
+ {
84
+ "description": "value=0; no dashes filled.",
85
+ "name": "empty"
86
+ },
87
+ {
88
+ "description": "0 < value < total; some dashes filled.",
89
+ "name": "in-progress"
90
+ },
91
+ {
92
+ "description": "value=total; all dashes filled.",
93
+ "name": "complete"
94
+ }
95
+ ],
96
+ "synonyms": {
97
+ "step": [
98
+ "stage",
99
+ "phase"
100
+ ],
101
+ "total": [
102
+ "count",
103
+ "length"
104
+ ]
105
+ },
106
+ "tag": "step-progress-ui",
107
+ "tokens": {},
108
+ "traits": [],
109
+ "version": 1
110
+ }
111
+ }
@@ -0,0 +1,61 @@
1
+ /* ═══════════════════════════════════════════
2
+ step-progress-ui — Discrete-dash step progress indicator
3
+ Replaces bespoke `[data-onb-progress]` + `[data-reg-progress]` markup.
4
+ ═══════════════════════════════════════════ */
5
+
6
+ @scope (step-progress-ui) {
7
+ :where(:scope) {
8
+ --step-progress-gap: var(--a-space-2);
9
+ --step-progress-track-gap: var(--a-space-1);
10
+ --step-progress-dash-h: 4px;
11
+ --step-progress-dash-radius: var(--a-radius-full);
12
+ --step-progress-dash-bg: var(--a-border-subtle);
13
+ --step-progress-dash-bg-active: var(--a-accent);
14
+ --step-progress-caption-fg: var(--a-fg-muted);
15
+ --step-progress-caption-size: var(--a-ui-sm);
16
+
17
+ /* Sizing — match the bespoke flex shape in registration + onboarding
18
+ (flex:0 1 18rem; min-width:10rem). Consumers can override. */
19
+ --step-progress-flex: 0 1 18rem;
20
+ --step-progress-min-width: 10rem;
21
+ }
22
+
23
+ :scope {
24
+ display: flex;
25
+ flex-direction: column;
26
+ gap: var(--step-progress-gap);
27
+ flex: var(--step-progress-flex);
28
+ min-width: var(--step-progress-min-width);
29
+ }
30
+
31
+ /* Caption row — small, muted text. Hidden when no caption provided
32
+ (the [hidden] attribute is set by the JS). */
33
+ [slot="caption"] {
34
+ color: var(--step-progress-caption-fg);
35
+ font-size: var(--step-progress-caption-size);
36
+ line-height: 1.4;
37
+ }
38
+
39
+ [slot="caption"][hidden] {
40
+ display: none;
41
+ }
42
+
43
+ /* Discrete-dash track — one <span> per step. Each dash is a flex
44
+ item so the track scales with the column width. */
45
+ [slot="track"] {
46
+ display: flex;
47
+ gap: var(--step-progress-track-gap);
48
+ }
49
+
50
+ [slot="track"] > span {
51
+ flex: 1;
52
+ height: var(--step-progress-dash-h);
53
+ border-radius: var(--step-progress-dash-radius);
54
+ background: var(--step-progress-dash-bg);
55
+ transition: background var(--a-duration-fast, 150ms) var(--a-easing, ease);
56
+ }
57
+
58
+ [slot="track"] > span[data-active] {
59
+ background: var(--step-progress-dash-bg-active);
60
+ }
61
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * <step-progress-ui value="3" total="10" caption="Step 3 of 10"></step-progress-ui>
3
+ * <step-progress-ui value="3" total="10"></step-progress-ui> <!-- no caption -->
4
+ * <step-progress-ui value="10" total="10" caption="All steps complete"></step-progress-ui>
5
+ *
6
+ * Discrete-dash step progress indicator. One dash per step in the
7
+ * total; the first `value` dashes carry [data-active] and paint with
8
+ * --a-accent. Used in registration + onboarding flows; replaces the
9
+ * bespoke `[data-onb-progress]` / `[data-reg-progress]` markup.
10
+ *
11
+ * Primitive shape (per ROADMAP § Done):
12
+ * `<step-progress-ui value="N" total="M" caption="...">`
13
+ *
14
+ * Caption is optional; pass via attribute. Free-form text passes
15
+ * straight through; `<text-ui>` styling can be replicated with
16
+ * default-slot HTML if needed (the auto-mint uses plain text).
17
+ */
18
+
19
+ import { UIElement } from '../../core/element.js';
20
+
21
+ class UIStepProgress extends UIElement {
22
+ static properties = {
23
+ value: { type: Number, default: 0, reflect: true },
24
+ total: { type: Number, default: 0, reflect: true },
25
+ caption: { type: String, default: '', reflect: true },
26
+ };
27
+
28
+ static template = () => null;
29
+
30
+ #captionEl = null;
31
+ #trackEl = null;
32
+
33
+ connected() {
34
+ this.setAttribute('role', 'progressbar');
35
+ this.setAttribute('aria-valuemin', '0');
36
+
37
+ // Auto-mint internal structure if the consumer didn't provide custom slots.
38
+ if (!this.querySelector(':scope > [slot="caption"]') && !this.querySelector(':scope > [slot="track"]')) {
39
+ this.#captionEl = document.createElement('span');
40
+ this.#captionEl.setAttribute('slot', 'caption');
41
+ this.appendChild(this.#captionEl);
42
+
43
+ this.#trackEl = document.createElement('div');
44
+ this.#trackEl.setAttribute('slot', 'track');
45
+ this.appendChild(this.#trackEl);
46
+ } else {
47
+ this.#captionEl = this.querySelector(':scope > [slot="caption"]');
48
+ this.#trackEl = this.querySelector(':scope > [slot="track"]');
49
+ }
50
+ }
51
+
52
+ render() {
53
+ const total = Math.max(0, this.total | 0);
54
+ const clamped = Math.max(0, Math.min(total, this.value | 0));
55
+
56
+ this.setAttribute('aria-valuemax', String(total));
57
+ this.setAttribute('aria-valuenow', String(clamped));
58
+
59
+ // Caption — text content if attribute set; cleared otherwise.
60
+ // Skip if the consumer passed in a custom caption slot (preserve their content).
61
+ if (this.#captionEl && !this.#captionEl.dataset.custom) {
62
+ this.#captionEl.textContent = this.caption || '';
63
+ this.#captionEl.toggleAttribute('hidden', !this.caption);
64
+ }
65
+
66
+ // Track — sync span count with `total`; mark first `value` as active.
67
+ if (this.#trackEl) {
68
+ const existing = this.#trackEl.children.length;
69
+ while (this.#trackEl.children.length < total) {
70
+ this.#trackEl.appendChild(document.createElement('span'));
71
+ }
72
+ while (this.#trackEl.children.length > total) {
73
+ this.#trackEl.lastElementChild.remove();
74
+ }
75
+ for (let i = 0; i < this.#trackEl.children.length; i++) {
76
+ this.#trackEl.children[i].toggleAttribute('data-active', i < clamped);
77
+ }
78
+ }
79
+ }
80
+
81
+ disconnected() {
82
+ this.#captionEl = null;
83
+ this.#trackEl = null;
84
+ }
85
+ }
86
+
87
+ customElements.define('step-progress-ui', UIStepProgress);
88
+ export { UIStepProgress };
@@ -0,0 +1,118 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import '../../core/element.js';
3
+ import './step-progress.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('step-progress-ui', () => {
15
+ beforeEach(() => { document.body.innerHTML = ''; });
16
+
17
+ it('registers as a custom element', () => {
18
+ expect(customElements.get('step-progress-ui')).toBeDefined();
19
+ });
20
+
21
+ it('sets role=progressbar + aria-value{min,max,now}', () => {
22
+ const el = mount('<step-progress-ui value="3" total="10"></step-progress-ui>');
23
+ expect(el.getAttribute('role')).toBe('progressbar');
24
+ expect(el.getAttribute('aria-valuemin')).toBe('0');
25
+ expect(el.getAttribute('aria-valuemax')).toBe('10');
26
+ expect(el.getAttribute('aria-valuenow')).toBe('3');
27
+ });
28
+
29
+ it('auto-mints a caption span + track div', () => {
30
+ const el = mount('<step-progress-ui value="2" total="5" caption="Step 2 of 5"></step-progress-ui>');
31
+ expect(el.querySelector(':scope > [slot="caption"]')).not.toBeNull();
32
+ expect(el.querySelector(':scope > [slot="track"]')).not.toBeNull();
33
+ });
34
+
35
+ it('renders one <span> per step in the track', () => {
36
+ const el = mount('<step-progress-ui value="2" total="5"></step-progress-ui>');
37
+ const dashes = el.querySelectorAll(':scope > [slot="track"] > span');
38
+ expect(dashes.length).toBe(5);
39
+ });
40
+
41
+ it('marks the first `value` dashes as [data-active]', () => {
42
+ const el = mount('<step-progress-ui value="3" total="5"></step-progress-ui>');
43
+ const dashes = el.querySelectorAll(':scope > [slot="track"] > span');
44
+ expect(dashes[0].hasAttribute('data-active')).toBe(true);
45
+ expect(dashes[1].hasAttribute('data-active')).toBe(true);
46
+ expect(dashes[2].hasAttribute('data-active')).toBe(true);
47
+ expect(dashes[3].hasAttribute('data-active')).toBe(false);
48
+ expect(dashes[4].hasAttribute('data-active')).toBe(false);
49
+ });
50
+
51
+ it('renders the caption text in the caption slot', () => {
52
+ const el = mount('<step-progress-ui value="3" total="10" caption="Step 3 of 10"></step-progress-ui>');
53
+ expect(el.querySelector('[slot="caption"]').textContent).toBe('Step 3 of 10');
54
+ });
55
+
56
+ it('hides the caption span when caption is empty', () => {
57
+ const el = mount('<step-progress-ui value="2" total="5"></step-progress-ui>');
58
+ expect(el.querySelector('[slot="caption"]').hasAttribute('hidden')).toBe(true);
59
+ });
60
+
61
+ it('updates the dash + aria when value changes', async () => {
62
+ const el = mount('<step-progress-ui value="2" total="5"></step-progress-ui>');
63
+ el.value = 4;
64
+ await tick();
65
+ const dashes = el.querySelectorAll(':scope > [slot="track"] > span');
66
+ expect(el.getAttribute('aria-valuenow')).toBe('4');
67
+ expect(dashes[3].hasAttribute('data-active')).toBe(true);
68
+ expect(dashes[4].hasAttribute('data-active')).toBe(false);
69
+ });
70
+
71
+ it('grows the dash count when total increases', async () => {
72
+ const el = mount('<step-progress-ui value="2" total="5"></step-progress-ui>');
73
+ expect(el.querySelectorAll('[slot="track"] > span').length).toBe(5);
74
+ el.total = 8;
75
+ await tick();
76
+ expect(el.querySelectorAll('[slot="track"] > span').length).toBe(8);
77
+ });
78
+
79
+ it('shrinks the dash count when total decreases', async () => {
80
+ const el = mount('<step-progress-ui value="2" total="8"></step-progress-ui>');
81
+ expect(el.querySelectorAll('[slot="track"] > span').length).toBe(8);
82
+ el.total = 4;
83
+ await tick();
84
+ expect(el.querySelectorAll('[slot="track"] > span').length).toBe(4);
85
+ });
86
+
87
+ it('clamps value to [0, total]', () => {
88
+ const el = mount('<step-progress-ui value="20" total="5"></step-progress-ui>');
89
+ const dashes = el.querySelectorAll(':scope > [slot="track"] > span');
90
+ expect(el.getAttribute('aria-valuenow')).toBe('5');
91
+ for (const dash of dashes) {
92
+ expect(dash.hasAttribute('data-active')).toBe(true);
93
+ }
94
+ });
95
+
96
+ it('handles value=0 (empty state) — no dashes active', () => {
97
+ const el = mount('<step-progress-ui value="0" total="5"></step-progress-ui>');
98
+ const dashes = el.querySelectorAll(':scope > [slot="track"] > span');
99
+ expect(el.getAttribute('aria-valuenow')).toBe('0');
100
+ for (const dash of dashes) {
101
+ expect(dash.hasAttribute('data-active')).toBe(false);
102
+ }
103
+ });
104
+
105
+ it('handles value=total (complete state) — all dashes active', () => {
106
+ const el = mount('<step-progress-ui value="5" total="5" caption="All steps complete"></step-progress-ui>');
107
+ const dashes = el.querySelectorAll(':scope > [slot="track"] > span');
108
+ for (const dash of dashes) {
109
+ expect(dash.hasAttribute('data-active')).toBe(true);
110
+ }
111
+ expect(el.querySelector('[slot="caption"]').textContent).toBe('All steps complete');
112
+ });
113
+
114
+ it('survives disconnect without throwing', () => {
115
+ const el = mount('<step-progress-ui value="3" total="10"></step-progress-ui>');
116
+ expect(() => el.remove()).not.toThrow();
117
+ });
118
+ });
@@ -0,0 +1,93 @@
1
+ $schema: ../../../../scripts/schemas/component.yaml.schema.json
2
+ name: UIStepProgress
3
+ tag: step-progress-ui
4
+ component: StepProgress
5
+ category: feedback
6
+ version: 1
7
+ description: |
8
+ Discrete-dash step-progress indicator. One <span> dash per step in
9
+ [total]; the first [value] dashes are filled with --a-accent. Used
10
+ in multi-step flows (registration, onboarding, wizards). Replaces
11
+ the bespoke `[data-onb-progress]` + `[data-reg-progress]` markup
12
+ that lived in onboarding.css + registration.css with a single
13
+ primitive.
14
+
15
+ props:
16
+ value:
17
+ type: number
18
+ default: 0
19
+ description: "Number of steps completed (1-indexed). Clamped to [0, total]."
20
+ total:
21
+ type: number
22
+ default: 0
23
+ description: "Total number of steps. The track renders this many dashes."
24
+ caption:
25
+ type: string
26
+ default: ''
27
+ description: "Optional caption text rendered above the track (e.g. 'Step 3 of 10', 'Step 3 of 10 · Optional', 'All steps complete'). Hidden when empty."
28
+
29
+ events: {}
30
+
31
+ slots:
32
+ caption:
33
+ description: "Optional override of the auto-minted caption (use for rich content like icon + text). When present, the [caption] attribute is ignored."
34
+ track:
35
+ description: "Optional override of the auto-minted track (rare — use only when the dash structure needs to differ)."
36
+
37
+ states:
38
+ - name: empty
39
+ description: "value=0; no dashes filled."
40
+ - name: in-progress
41
+ description: "0 < value < total; some dashes filled."
42
+ - name: complete
43
+ description: "value=total; all dashes filled."
44
+
45
+ traits: []
46
+ tokens: {}
47
+ a2ui:
48
+ rules: []
49
+ anti_patterns: []
50
+
51
+ examples:
52
+ - name: in-progress
53
+ description: 3 of 10 steps complete with caption.
54
+ a2ui: >-
55
+ [
56
+ {
57
+ "id": "root",
58
+ "component": "StepProgress",
59
+ "value": 3,
60
+ "total": 10,
61
+ "caption": "Step 3 of 10"
62
+ }
63
+ ]
64
+ - name: complete
65
+ description: All steps complete.
66
+ a2ui: >-
67
+ [
68
+ {
69
+ "id": "root",
70
+ "component": "StepProgress",
71
+ "value": 10,
72
+ "total": 10,
73
+ "caption": "All steps complete"
74
+ }
75
+ ]
76
+ - name: optional-step
77
+ description: A 9-of-10 step that's optional.
78
+ a2ui: >-
79
+ [
80
+ {
81
+ "id": "root",
82
+ "component": "StepProgress",
83
+ "value": 9,
84
+ "total": 10,
85
+ "caption": "Step 9 of 10 · Optional"
86
+ }
87
+ ]
88
+
89
+ keywords: [progress, step, wizard, onboarding, registration, multi-step, indicator]
90
+ synonyms:
91
+ step: [stage, phase]
92
+ total: [count, length]
93
+ related: [progress-ui, stepper-ui]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adia-ai/web-components",
3
- "version": "0.0.33",
3
+ "version": "0.0.34",
4
4
  "description": "AdiaUI web components — vanilla custom elements. A2UI runtime (renderer, registry, streams, wiring) lives in @adia-ai/a2ui-utils.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -70,6 +70,7 @@
70
70
  @import "../components/nav/nav.css";
71
71
  @import "../components/nav-group/nav-group.css";
72
72
  @import "../components/nav-item/nav-item.css";
73
+ @import "../components/step-progress/step-progress.css";
73
74
  @import "../components/otp-input/otp-input.css";
74
75
  @import "../components/image/image.css";
75
76
  @import "../components/search/search.css";