@chiralkit/design-system 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +11 -0
  3. package/dist/chiralkit-ds/chiralkit-ds.css +1 -0
  4. package/dist/chiralkit-ds/chiralkit-ds.esm.js +1 -0
  5. package/dist/chiralkit-ds/index.esm.js +1 -0
  6. package/dist/chiralkit-ds/p-038fa008.entry.js +1 -0
  7. package/dist/chiralkit-ds/p-2fdc1012.entry.js +1 -0
  8. package/dist/chiralkit-ds/p-306ec33b.entry.js +1 -0
  9. package/dist/chiralkit-ds/p-3f704698.entry.js +1 -0
  10. package/dist/chiralkit-ds/p-41081d22.entry.js +1 -0
  11. package/dist/chiralkit-ds/p-7d7e5aac.entry.js +1 -0
  12. package/dist/chiralkit-ds/p-84b801f8.entry.js +1 -0
  13. package/dist/chiralkit-ds/p-a28c71b4.entry.js +1 -0
  14. package/dist/chiralkit-ds/p-be84312f.entry.js +1 -0
  15. package/dist/chiralkit-ds/p-c8015fe6.entry.js +1 -0
  16. package/dist/chiralkit-ds/p-d36a6bea.entry.js +1 -0
  17. package/dist/chiralkit-ds/p-e-glNjK6.js +2 -0
  18. package/dist/chiralkit-ds/p-f0f9ff61.entry.js +1 -0
  19. package/dist/cjs/chiralkit-ds.cjs.js +24 -0
  20. package/dist/cjs/ds-badge.cjs.entry.js +18 -0
  21. package/dist/cjs/ds-button.cjs.entry.js +26 -0
  22. package/dist/cjs/ds-card.cjs.entry.js +17 -0
  23. package/dist/cjs/ds-checkbox.cjs.entry.js +67 -0
  24. package/dist/cjs/ds-code-block.cjs.entry.js +39 -0
  25. package/dist/cjs/ds-dialog.cjs.entry.js +67 -0
  26. package/dist/cjs/ds-input.cjs.entry.js +50 -0
  27. package/dist/cjs/ds-loader.cjs.entry.js +26 -0
  28. package/dist/cjs/ds-select.cjs.entry.js +85 -0
  29. package/dist/cjs/ds-tabs.cjs.entry.js +138 -0
  30. package/dist/cjs/ds-textarea.cjs.entry.js +50 -0
  31. package/dist/cjs/ds-tooltip.cjs.entry.js +233 -0
  32. package/dist/cjs/index-mMvyq7U_.js +2018 -0
  33. package/dist/cjs/index.cjs.js +10 -0
  34. package/dist/cjs/loader.cjs.js +12 -0
  35. package/dist/collection/collection-manifest.json +24 -0
  36. package/dist/collection/components/ds-badge/ds-badge.css +36 -0
  37. package/dist/collection/components/ds-badge/ds-badge.js +93 -0
  38. package/dist/collection/components/ds-button/ds-button.css +66 -0
  39. package/dist/collection/components/ds-button/ds-button.js +197 -0
  40. package/dist/collection/components/ds-card/ds-card.css +12 -0
  41. package/dist/collection/components/ds-card/ds-card.js +68 -0
  42. package/dist/collection/components/ds-checkbox/ds-checkbox.css +22 -0
  43. package/dist/collection/components/ds-checkbox/ds-checkbox.js +339 -0
  44. package/dist/collection/components/ds-code-block/ds-code-block.css +65 -0
  45. package/dist/collection/components/ds-code-block/ds-code-block.js +162 -0
  46. package/dist/collection/components/ds-dialog/ds-dialog.css +30 -0
  47. package/dist/collection/components/ds-dialog/ds-dialog.js +313 -0
  48. package/dist/collection/components/ds-input/ds-input.css +38 -0
  49. package/dist/collection/components/ds-input/ds-input.js +353 -0
  50. package/dist/collection/components/ds-loader/ds-loader.css +94 -0
  51. package/dist/collection/components/ds-loader/ds-loader.js +149 -0
  52. package/dist/collection/components/ds-select/ds-select.css +69 -0
  53. package/dist/collection/components/ds-select/ds-select.js +235 -0
  54. package/dist/collection/components/ds-tabs/ds-tabs.css +59 -0
  55. package/dist/collection/components/ds-tabs/ds-tabs.js +248 -0
  56. package/dist/collection/components/ds-textarea/ds-textarea.css +41 -0
  57. package/dist/collection/components/ds-textarea/ds-textarea.js +342 -0
  58. package/dist/collection/components/ds-tooltip/ds-tooltip.css +31 -0
  59. package/dist/collection/components/ds-tooltip/ds-tooltip.js +318 -0
  60. package/dist/collection/components.js +2 -0
  61. package/dist/collection/index.js +1 -0
  62. package/dist/components/ds-badge.d.ts +11 -0
  63. package/dist/components/ds-badge.js +1 -0
  64. package/dist/components/ds-button.d.ts +11 -0
  65. package/dist/components/ds-button.js +1 -0
  66. package/dist/components/ds-card.d.ts +11 -0
  67. package/dist/components/ds-card.js +1 -0
  68. package/dist/components/ds-checkbox.d.ts +11 -0
  69. package/dist/components/ds-checkbox.js +1 -0
  70. package/dist/components/ds-code-block.d.ts +11 -0
  71. package/dist/components/ds-code-block.js +1 -0
  72. package/dist/components/ds-dialog.d.ts +11 -0
  73. package/dist/components/ds-dialog.js +1 -0
  74. package/dist/components/ds-input.d.ts +11 -0
  75. package/dist/components/ds-input.js +1 -0
  76. package/dist/components/ds-loader.d.ts +11 -0
  77. package/dist/components/ds-loader.js +1 -0
  78. package/dist/components/ds-select.d.ts +11 -0
  79. package/dist/components/ds-select.js +1 -0
  80. package/dist/components/ds-tabs.d.ts +11 -0
  81. package/dist/components/ds-tabs.js +1 -0
  82. package/dist/components/ds-textarea.d.ts +11 -0
  83. package/dist/components/ds-textarea.js +1 -0
  84. package/dist/components/ds-tooltip.d.ts +11 -0
  85. package/dist/components/ds-tooltip.js +1 -0
  86. package/dist/components/index.d.ts +35 -0
  87. package/dist/components/index.js +1 -0
  88. package/dist/components/p-DDUZow8F.js +1 -0
  89. package/dist/components/p-ZsYP0VBq.js +1 -0
  90. package/dist/components/p-k0ywxWpC.js +1 -0
  91. package/dist/esm/chiralkit-ds.js +20 -0
  92. package/dist/esm/ds-badge.entry.js +16 -0
  93. package/dist/esm/ds-button.entry.js +24 -0
  94. package/dist/esm/ds-card.entry.js +15 -0
  95. package/dist/esm/ds-checkbox.entry.js +65 -0
  96. package/dist/esm/ds-code-block.entry.js +37 -0
  97. package/dist/esm/ds-dialog.entry.js +65 -0
  98. package/dist/esm/ds-input.entry.js +48 -0
  99. package/dist/esm/ds-loader.entry.js +24 -0
  100. package/dist/esm/ds-select.entry.js +83 -0
  101. package/dist/esm/ds-tabs.entry.js +136 -0
  102. package/dist/esm/ds-textarea.entry.js +48 -0
  103. package/dist/esm/ds-tooltip.entry.js +231 -0
  104. package/dist/esm/index-e-glNjK6.js +2008 -0
  105. package/dist/esm/index.js +3 -0
  106. package/dist/esm/loader.js +10 -0
  107. package/dist/index.cjs.js +1 -0
  108. package/dist/index.js +1 -0
  109. package/dist/layouts/app-shell.css +84 -0
  110. package/dist/tests/a11y.js +55 -0
  111. package/dist/types/Users/alex/projects/MyGitHub/chiralkit/packages/design-system/.stencil/tests/a11y.d.ts +17 -0
  112. package/dist/types/components/ds-badge/ds-badge.d.ts +54 -0
  113. package/dist/types/components/ds-button/ds-button.d.ts +75 -0
  114. package/dist/types/components/ds-card/ds-card.d.ts +53 -0
  115. package/dist/types/components/ds-checkbox/ds-checkbox.d.ts +94 -0
  116. package/dist/types/components/ds-code-block/ds-code-block.d.ts +83 -0
  117. package/dist/types/components/ds-dialog/ds-dialog.d.ts +148 -0
  118. package/dist/types/components/ds-input/ds-input.d.ts +104 -0
  119. package/dist/types/components/ds-loader/ds-loader.d.ts +64 -0
  120. package/dist/types/components/ds-select/ds-select.d.ts +123 -0
  121. package/dist/types/components/ds-tabs/ds-tabs.d.ts +155 -0
  122. package/dist/types/components/ds-textarea/ds-textarea.d.ts +93 -0
  123. package/dist/types/components/ds-tooltip/ds-tooltip.d.ts +26 -0
  124. package/dist/types/components.d.ts +3068 -0
  125. package/dist/types/index.d.ts +1 -0
  126. package/dist/types/stencil-public-runtime.d.ts +1861 -0
  127. package/docs.json +2112 -0
  128. package/loader/cdn.js +1 -0
  129. package/loader/index.cjs.js +1 -0
  130. package/loader/index.d.ts +24 -0
  131. package/loader/index.es2017.js +1 -0
  132. package/loader/index.js +2 -0
  133. package/package.json +78 -0
  134. package/react/components/ds-badge.tsx +25 -0
  135. package/react/components/ds-button.tsx +25 -0
  136. package/react/components/ds-card.tsx +25 -0
  137. package/react/components/ds-checkbox.tsx +25 -0
  138. package/react/components/ds-code-block.tsx +25 -0
  139. package/react/components/ds-dialog.tsx +31 -0
  140. package/react/components/ds-input.tsx +25 -0
  141. package/react/components/ds-loader.tsx +25 -0
  142. package/react/components/ds-select.tsx +25 -0
  143. package/react/components/ds-tabs.tsx +25 -0
  144. package/react/components/ds-textarea.tsx +25 -0
  145. package/react/components/ds-tooltip.tsx +25 -0
  146. package/react/components.ts +162 -0
@@ -0,0 +1,148 @@
1
+ import { EventEmitter } from "../../stencil-public-runtime";
2
+ /**
3
+ * ## Accessibility
4
+ *
5
+ * **Category:** state-machine
6
+ * **APG pattern:** https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/
7
+ *
8
+ * This component wraps the **native `<dialog>` element** rather than
9
+ * scratch-building the APG pattern on top of a `<div role="dialog">`.
10
+ * `<dialog>` opened via `showModal()` gives us — from the browser,
11
+ * verified by the platform, not by us:
12
+ *
13
+ * - Focus trap: Tab / Shift+Tab cycle only through focusable
14
+ * descendants (including slotted light-DOM descendants; the flattened
15
+ * accessibility tree is what the trap operates on).
16
+ * - Escape: closes the dialog and returns focus to the previously
17
+ * focused element (native, not implemented here).
18
+ * - Inert background: everything outside the dialog is inert while it
19
+ * is open — no mouse, no keyboard, not part of the a11y tree.
20
+ * - Return focus: on close, focus returns to whatever element had it
21
+ * before `showModal()` — native, not implemented here.
22
+ * - Initial focus: browser auto-focuses the first `autofocus` element
23
+ * inside, or the first focusable if none — native.
24
+ * - `role="dialog"` and `aria-modal="true"` semantics: implicit on
25
+ * `<dialog>`, no ARIA needed from us.
26
+ *
27
+ * This is the deliberate answer to the a11y spec §D warning about
28
+ * scratch-building Tier 2 components: the native element removes the
29
+ * "compiles, axe passes, screen-reader user gets trapped" failure
30
+ * mode by delegating the state machine to the platform.
31
+ *
32
+ * **Keyboard interactions (all handled by the native element unless
33
+ * noted otherwise):**
34
+ * - Tab: cycles focus within the dialog's focusable descendants
35
+ * (native focus trap).
36
+ * - Shift+Tab: reverse cycle (native).
37
+ * - Escape: closes the dialog and fires the `close` event (native).
38
+ * Consumers who need to prevent dismissal on Escape (destructive
39
+ * flows: e.g. an unsaved-changes guard) must call
40
+ * `event.preventDefault()` inside their own `keydown` listener on
41
+ * the dialog before it reaches the browser — that is an escape
42
+ * hatch, not a default we impose.
43
+ * - Enter / Space: no special dialog behavior; forwarded to
44
+ * whatever descendant has focus (a button will activate).
45
+ * - Home / End / Arrow keys: no special dialog behavior; forwarded
46
+ * to descendants.
47
+ * - Printable characters: forwarded to descendants (e.g. typing
48
+ * inside a slotted `<ds-input>`).
49
+ *
50
+ * **ARIA attributes and value triggers:**
51
+ * - Implicit `role="dialog"` on the native `<dialog>`.
52
+ * - Implicit `aria-modal="true"` when opened via `showModal()`
53
+ * (which is what this component always uses; `show()` — the
54
+ * non-modal form — is intentionally not exposed in v1).
55
+ * - `aria-label` set from the `label` prop onto the `<dialog>`.
56
+ * Consumers who prefer an in-content title can pass a
57
+ * `labelledBy` prop instead, whose value is the id of a heading
58
+ * element they render inside the slot — `aria-labelledby`
59
+ * resolves that id through the flattened tree.
60
+ * - `aria-describedby` from the `describedBy` prop, resolving a
61
+ * light-DOM id of a descriptive block (e.g. the dialog body copy
62
+ * when the label is short).
63
+ *
64
+ * **Focus lifecycle:**
65
+ * - Opening: `showModal()` moves focus to the first `autofocus`
66
+ * element inside, or the first focusable if none. Consumers who
67
+ * want a specific initial focus target should mark that element
68
+ * with `autofocus`.
69
+ * - While open: focus is trapped within the flattened tree of the
70
+ * `<dialog>`; Tab / Shift+Tab cycle through slotted descendants.
71
+ * Focus cannot escape into the inert background.
72
+ * - Closing: `close()` (either from Escape, from a form-submit with
73
+ * `method="dialog"`, from a light-dismiss backdrop click handler
74
+ * the consumer wires up, or from `open=false`) returns focus to
75
+ * the element that had it immediately before `showModal()` was
76
+ * called. The `dsClose` event fires after focus has been restored.
77
+ *
78
+ * **ARIA labelling contract (choose one):**
79
+ * - **`label`** (string) — sets `aria-label` on the dialog. Simplest
80
+ * for short titles.
81
+ * - **`labelledBy`** (id) — sets `aria-labelledby` to reference a
82
+ * heading rendered inside the slot. Preferred when the dialog has
83
+ * a visible title so the accessible name matches the visible name.
84
+ *
85
+ * If both are omitted, axe reports `dialog-name` — the dialog has no
86
+ * accessible name. The a11y test for this component asserts that at
87
+ * least one of the two is provided.
88
+ *
89
+ * **Motion:**
90
+ * - Animations: none applied by the component. Consumers who want an
91
+ * entrance transition can style `::backdrop` and `dialog[open]`
92
+ * with their own animation, gated on `@media
93
+ * (prefers-reduced-motion: no-preference)`.
94
+ * - Reduced-motion behavior: N/A — no animation to collapse. If a
95
+ * consumer adds one, they own the reduced-motion fallback.
96
+ *
97
+ * **Contrast:**
98
+ * - Text: inherits `color: inherit`; consumer theme owns contrast.
99
+ * - Non-text: subtle bordered surface via
100
+ * `color-mix(in oklch, currentColor N%, transparent)` matching
101
+ * `ds-card`. Backdrop is a semi-transparent black by default.
102
+ *
103
+ * **Screen reader announcement:**
104
+ * - Rendered state (closed): dialog is not in the a11y tree.
105
+ * - Opened: AT announces "dialog" plus the accessible name from
106
+ * `label` / `labelledBy`, then reads the content following focus
107
+ * (or the descriptive block referenced by `describedBy` first,
108
+ * depending on AT).
109
+ * - State change: on close, AT returns to the element that regained
110
+ * focus (the trigger); no `aria-live` announcement is added.
111
+ *
112
+ * **Consumer contract:**
113
+ * - Set `open` to true to show; set to false to close. The component
114
+ * syncs the DOM state via `showModal()` / `close()` in a Watch.
115
+ * - Listen to `dsClose` to know when the dialog was dismissed (by
116
+ * Escape, by `open=false`, or by a form submit with
117
+ * `method="dialog"`). Listen to `dsOpen` for the mirror event.
118
+ * - The default backdrop is opaque and NOT click-to-dismiss. Consumers
119
+ * who want light-dismiss can add a click listener on the dialog
120
+ * that closes when the click target is the dialog itself (not a
121
+ * descendant): `if (e.target === dialog) dialog.close()`.
122
+ * - Put focus targets (form fields, primary action button) inside
123
+ * the slot. Use `autofocus` on the element that should receive
124
+ * focus first.
125
+ *
126
+ * **Pending manual verification (see commit message TODO):**
127
+ * - VoiceOver on macOS: confirm the dialog announces its accessible
128
+ * name + "dialog" on open, focus lands on the correct element,
129
+ * Escape closes and focus returns to the trigger.
130
+ * - Chromium high-contrast / forced-colors: confirm the surface
131
+ * still reads as a bordered container with the native ::backdrop
132
+ * dim.
133
+ */
134
+ export declare class DsDialog {
135
+ private dialogEl?;
136
+ open: boolean;
137
+ label?: string;
138
+ labelledBy?: string;
139
+ describedBy?: string;
140
+ dsOpen: EventEmitter<void>;
141
+ dsClose: EventEmitter<void>;
142
+ watchOpen(newValue: boolean): void;
143
+ componentDidLoad(): void;
144
+ disconnectedCallback(): void;
145
+ private syncOpen;
146
+ private onNativeClose;
147
+ render(): any;
148
+ }
@@ -0,0 +1,104 @@
1
+ import { EventEmitter } from "../../stencil-public-runtime";
2
+ /**
3
+ * ## Accessibility
4
+ *
5
+ * **Category:** form-field
6
+ * **APG pattern:** N/A — native element. This wraps a native `<input>`;
7
+ * there is no WAI-ARIA APG pattern for a plain text input because the
8
+ * browser already implements it natively. Reference:
9
+ * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input
10
+ *
11
+ * **Keyboard interactions:**
12
+ * - Tab: moves focus to the native `<input>` (shadow DOM does not
13
+ * interrupt tab order).
14
+ * - Printable characters: insert at the caret position (native).
15
+ * - Backspace / Delete: native editing.
16
+ * - Arrow keys: move the caret within the value (native); for
17
+ * `type="number"` Up/Down step the numeric value (native).
18
+ * - Enter: on `type="search"` may submit the enclosing form (native);
19
+ * no custom handling.
20
+ * - Escape: no native meaning for a text input.
21
+ *
22
+ * **ARIA:**
23
+ * - Roles used: none explicit — the native `<input>` supplies its
24
+ * implicit role (`textbox`, `searchbox`, etc. depending on `type`).
25
+ * - State attributes: `disabled` (native attribute, not `aria-disabled`),
26
+ * `aria-invalid` (from the `invalid` prop), `aria-required` (from the
27
+ * `required` prop), `aria-describedby` (from the `describedBy` prop,
28
+ * expected to reference a light-DOM node id — typically a hint or
29
+ * error message rendered by the consumer alongside the field).
30
+ * - Labeling strategy: `aria-label` set from the `label` prop onto the
31
+ * internal `<input>`. This is the **canonical, recommended** mode —
32
+ * explicit and independent of DOM structure. Consumers MUST pass
33
+ * `label` (or otherwise give the field an accessible name) — an
34
+ * `<input>` with no accessible name is a WCAG failure (axe rule
35
+ * `label`). Wrapping the host in a light-DOM `<label>` (e.g.
36
+ * `<label>Email <ds-input></ds-input></label>`) also produces an
37
+ * accessible name in Chromium via flattened-tree resolution — the
38
+ * same secondary mode documented on `ds-select`, verified in this
39
+ * component's e2e tests. Setting `aria-label` directly on the
40
+ * `<ds-input>` host does NOT work: shadow-DOM attribute forwarding
41
+ * would be required — use the `label` prop instead.
42
+ *
43
+ * **Focus:**
44
+ * - On mount: nothing auto-focuses.
45
+ * - On value change: focus remains on the native `<input>`.
46
+ * - Focus indicator: the browser default `:focus-visible` ring is kept
47
+ * on purpose — a themed focus ring using `--ds-color-focus` is a
48
+ * future decision once the DS ships a shared focus utility. Not
49
+ * overridden by `ds-input.css`.
50
+ *
51
+ * **Motion:**
52
+ * - Animations: none.
53
+ * - Reduced-motion behavior: N/A — no animation to collapse.
54
+ *
55
+ * **Contrast:**
56
+ * - Text: inherits `color: inherit` / `currentColor`; ratio depends on
57
+ * the consumer's theme, not verified in isolation here.
58
+ * - Border: `color-mix(in oklch, currentColor 22%, transparent)`,
59
+ * matching `ds-select` — inherits whatever contrast the host page
60
+ * sets. WCAG 1.4.11 (3:1 non-text) is the consumer's responsibility
61
+ * in themes that override the surface.
62
+ *
63
+ * **Screen reader announcement:**
64
+ * - Rendered state: announces the `label` prop text as the accessible
65
+ * name, followed by the browser/AT's role announcement for the
66
+ * input type ("edit text", "search text", "password", "phone
67
+ * number", "email"…). When `required=true` the announcement
68
+ * includes "required"; when `invalid=true` it includes "invalid".
69
+ * - State change: on value edit, AT typically echoes the typed
70
+ * character (native input behavior); no `aria-live` added, per the
71
+ * spec's guidance against live-region overuse.
72
+ *
73
+ * **Consumer contract:**
74
+ * - `placeholder` is a hint, not a label. It disappears on focus in
75
+ * most browsers and is not reliably announced by every AT — always
76
+ * pair with `label`.
77
+ * - For `type="password"` in a real form, pair with a light-DOM
78
+ * `<input type="hidden">` or a form-associated ancestor that gives
79
+ * the browser's password manager an `autocomplete` hint. This
80
+ * component intentionally does not expose `autocomplete` in v1;
81
+ * consumers that need it can render a native `<input>` for that
82
+ * field until the prop is added.
83
+ * - When `invalid=true`, pair with a `describedBy` id that points at
84
+ * a visible error message; `aria-invalid` alone gives the AT the
85
+ * status but not the reason.
86
+ */
87
+ export declare class DsInput {
88
+ private inputEl?;
89
+ value?: string;
90
+ name?: string;
91
+ type: "text" | "email" | "password" | "search" | "tel" | "url" | "number";
92
+ placeholder?: string;
93
+ disabled: boolean;
94
+ required: boolean;
95
+ invalid: boolean;
96
+ describedBy?: string;
97
+ label?: string;
98
+ size: "md" | "lg";
99
+ dsInput: EventEmitter<string>;
100
+ watchValue(newValue?: string): void;
101
+ componentDidLoad(): void;
102
+ private onInput;
103
+ render(): any;
104
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * ## Accessibility
3
+ *
4
+ * **Category:** static-display (indeterminate progress indicator)
5
+ * **APG pattern:** N/A — the loader is a passive presence indicator, not a
6
+ * `progressbar` (no measurable value). Uses `role="status"` per WAI-ARIA
7
+ * live-region semantics for indeterminate loading states.
8
+ *
9
+ * **Keyboard interactions:**
10
+ * - Tab: N/A — not focusable.
11
+ * - Enter: N/A.
12
+ * - Escape: N/A.
13
+ * - Arrow keys: N/A.
14
+ * - Printable characters: N/A.
15
+ *
16
+ * **ARIA:**
17
+ * - Roles used: `role="status"` on the host — an implicit polite live
18
+ * region ARIA lists as valid for "advisory information" including
19
+ * in-progress operations. The inner `<svg>` is decorative and carries
20
+ * `aria-hidden="true"` + `focusable="false"` so it never becomes a
21
+ * parallel target for screen readers.
22
+ * - State attributes: none. The loader has no state changes to announce
23
+ * beyond its own presence; the polite live region on mount is the
24
+ * announcement.
25
+ * - Labeling strategy: `aria-label` from the `label` prop (default
26
+ * "Loading"). Consumers pass a more specific string when the context
27
+ * warrants ("Loading ceremonies", "Signing in", …).
28
+ *
29
+ * **Focus:**
30
+ * - On mount: nothing auto-focuses.
31
+ * - On activate/open: N/A — not interactive.
32
+ * - On close/deactivate: N/A.
33
+ * - Focus indicator: N/A — the host is not focusable.
34
+ *
35
+ * **Motion:**
36
+ * - Animations: two mirrored halves of the logo path oscillate
37
+ * symmetrically around the visual center, meeting at the ring midpoint
38
+ * and separating again. Cycle 1.4s by default (`speed="normal"`);
39
+ * `speed="slow"` = 2.2s, `speed="fast"` = 0.9s.
40
+ * - Reduced-motion behavior: `@media (prefers-reduced-motion: reduce)`
41
+ * collapses both halves to a static rendering with a slow opacity
42
+ * pulse (2s ease-in-out). No rotation, no lateral movement.
43
+ *
44
+ * **Contrast:**
45
+ * - Text: N/A — the visible surface is the SVG stroke, not text.
46
+ * - Non-text: stroke inherits `currentColor` from the host. Consumers are
47
+ * responsible for confirming the stroke against its background meets
48
+ * the ≥ 3:1 non-text contrast threshold (WCAG 1.4.11) where the loader
49
+ * conveys state (e.g. distinguishing "still loading" from "idle").
50
+ *
51
+ * **Screen reader announcement:**
52
+ * - Rendered state: the polite live region announces the label
53
+ * (e.g. "Loading") when the loader mounts.
54
+ * - State change: N/A — the loader itself has no state that changes.
55
+ * Consumers unmount the loader when the awaited work completes; that
56
+ * removal is silent, which is the intended behavior (the arriving
57
+ * content is the announcement).
58
+ */
59
+ export declare class DsLoader {
60
+ size: "sm" | "md" | "lg";
61
+ label: string;
62
+ speed: "slow" | "normal" | "fast";
63
+ render(): any;
64
+ }
@@ -0,0 +1,123 @@
1
+ import { EventEmitter } from "../../stencil-public-runtime";
2
+ /**
3
+ * ## Accessibility
4
+ *
5
+ * **Category:** form-field
6
+ * **APG pattern:** N/A — native element. This wraps a native `<select>`;
7
+ * there is no WAI-ARIA APG pattern for it because the browser already
8
+ * implements the full listbox/combobox behavior natively. Reference:
9
+ * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select
10
+ *
11
+ * **Keyboard interactions:**
12
+ * - Tab: moves focus to the native `<select>` (shadow DOM does not
13
+ * interrupt tab order).
14
+ * - Enter: opens the native options popup on most platforms (browser/OS
15
+ * dependent, identical to any bare `<select>`).
16
+ * - Escape: closes the native options popup without changing the value
17
+ * (native behavior, not implemented by us).
18
+ * - Arrow keys: Up/Down move the selected option when closed; Up/Down/Home/End
19
+ * navigate options when the native popup is open (native behavior).
20
+ * - Printable characters: native typeahead jumps to the option starting
21
+ * with the typed character(s) (native behavior).
22
+ *
23
+ * **ARIA:**
24
+ * - Roles used: none explicit — the native `<select>` element supplies the
25
+ * implicit `listbox`/`combobox`-equivalent role itself.
26
+ * - State attributes: `disabled` (native attribute, not `aria-disabled`).
27
+ * - Labeling strategy: `aria-label`, set from the `label` prop onto the
28
+ * internal `<select>`. This is the **canonical, recommended** mode —
29
+ * explicit and independent of DOM structure. Consumers MUST pass `label`
30
+ * (or otherwise give the field an accessible name) — a `<select>` with no
31
+ * accessible name is a WCAG failure (axe rule `select-name`).
32
+ * Wrapping the host in a light-DOM `<label>` (e.g.
33
+ * `<label>Fruit<ds-select>...</ds-select></label>`) is verified (via
34
+ * axe-core/Chrome in this component's e2e tests) to also resolve an
35
+ * accessible name for the shadow-nested `<select>` — Chromium computes it
36
+ * from the flattened/composed tree even though `ds-select` is not a
37
+ * "labelable" element per the strict HTML spec text (it is not a
38
+ * form-associated custom element). Treat this as a secondary, best-effort
39
+ * mode rather than the primary contract, since it depends on
40
+ * browser/AT-specific accessible-name resolution rather than an explicit
41
+ * attribute — prefer the `label` prop when in doubt.
42
+ * Setting `aria-label` directly on the `<ds-select>` host does NOT work:
43
+ * the host's own attributes are not forwarded into its shadow tree, so
44
+ * the internal `<select>` still has no accessible name (confirmed by an
45
+ * `select-name` axe violation) — use the `label` prop instead.
46
+ * Options themselves are **prop-based, not slotted**: a `<slot />` nested
47
+ * inside a shadow-DOM `<select>` does not project light-DOM `<option>`
48
+ * elements into the native option list in Chromium (`select.options` stays
49
+ * empty even though the flattened a11y tree looks correct — the bug this
50
+ * prop API replaces). Pass `options` as a JSON string (SSR-friendly
51
+ * attribute) or as an array (client-side property); the component renders
52
+ * real `<option>` children inside its own shadow `<select>` from that data.
53
+ *
54
+ * **Focus:**
55
+ * - On mount: nothing auto-focuses.
56
+ * - On activate/open: focus stays on the native `<select>`; the browser
57
+ * manages the options popup itself.
58
+ * - On close/deactivate: focus remains on the native `<select>`.
59
+ * - Focus indicator: none added by this component — the native `<select>`
60
+ * keeps the browser's default `:focus-visible` ring, which is not
61
+ * overridden by `ds-select.css`.
62
+ *
63
+ * **Motion:**
64
+ * - Animations: none.
65
+ * - Reduced-motion behavior: N/A — no animation to collapse.
66
+ *
67
+ * **Contrast:**
68
+ * - Text: inherits `color: inherit` / `currentColor`; ratio depends on the
69
+ * consumer's theme, not verified in isolation here (no default text
70
+ * color is imposed by this component).
71
+ * - Non-text (border, caret icon): both use `currentColor`, so they match
72
+ * the text color 1:1 and inherit whatever contrast the host page sets;
73
+ * the caret is decorative-only (no accessible name), applied via
74
+ * `:host::after` mask, `pointer-events: none`.
75
+ *
76
+ * **Screen reader announcement:**
77
+ * - Rendered state: announces the `label` prop text as the accessible
78
+ * name, followed by "combo box" / "pop up button" (announcement wording
79
+ * is browser/AT dependent, standard for any native `<select>`), and the
80
+ * currently selected option's text — sourced from the `label` field of
81
+ * each entry in the `options` prop.
82
+ * - State change: on selection change, AT announces the newly selected
83
+ * option text (native `<select>` behavior; no custom `aria-live` added,
84
+ * per spec guidance to avoid live-region overuse).
85
+ *
86
+ * **Consumer contract:** `value` must correspond to a `value` in `options`.
87
+ * If `value` is set to a string not present in `options`, the native
88
+ * `<select>` will silently set `selectedIndex = -1` and no `dsChange` will
89
+ * fire — the component does not fabricate a fallback selection.
90
+ */
91
+ export type DsSelectOption = {
92
+ value: string;
93
+ label: string;
94
+ };
95
+ export declare class DsSelect {
96
+ private selectEl?;
97
+ value?: string;
98
+ name?: string;
99
+ disabled: boolean;
100
+ size: "md" | "lg";
101
+ label?: string;
102
+ /**
103
+ * Options rendered inside the internal shadow `<select>`. Accepts either:
104
+ * - a JSON string (SSR-friendly — server-rendered HTML can only set
105
+ * string attributes, and Stencil does not JSON-parse array attributes),
106
+ * e.g. `options='[{"value":"en","label":"EN"}]'`
107
+ * - an array property set from client-side JS,
108
+ * e.g. `el.options = [{ value: 'en', label: 'EN' }]`
109
+ *
110
+ * Malformed JSON fails safe to an empty list rather than throwing, so the
111
+ * component still renders a visible (if empty) select.
112
+ */
113
+ options: string | ReadonlyArray<DsSelectOption>;
114
+ dsChange: EventEmitter<string>;
115
+ watchValue(newValue?: string): void;
116
+ watchOptions(): void;
117
+ componentDidLoad(): void;
118
+ componentDidUpdate(): void;
119
+ private syncValue;
120
+ private parseOptions;
121
+ private onChange;
122
+ render(): any;
123
+ }
@@ -0,0 +1,155 @@
1
+ import { EventEmitter } from "../../stencil-public-runtime";
2
+ /**
3
+ * ## Accessibility
4
+ *
5
+ * **Category:** state-machine (Tier 2)
6
+ * **APG pattern:** https://www.w3.org/WAI/ARIA/apg/patterns/tabs/
7
+ *
8
+ * This component is a scratch build of the WAI-ARIA Tabs pattern —
9
+ * there is no native <tabs> element to wrap. Per the a11y spec §D,
10
+ * scratch-building Tier 2 requires citation + full keyboard/ARIA
11
+ * enumeration + focus lifecycle + manual VoiceOver evidence
12
+ * (pending — see commit message TODO).
13
+ *
14
+ * **Scope choice — tablist only, panels owned by the consumer.** The
15
+ * component renders `<button role="tab">` elements based on the
16
+ * `items` prop and manages roving tabindex + selection. It does NOT
17
+ * render tabpanels: the consumer renders their own light-DOM
18
+ * `<div role="tabpanel" aria-labelledby={tabId} hidden={id!==active}>`
19
+ * and toggles visibility based on the `dsChange` event. This keeps
20
+ * the state machine narrow (no reparenting, no slot ambiguity) and
21
+ * matches how ds-select delivers content — declare data, get behavior.
22
+ *
23
+ * **shadow: false intentional.** The tab buttons must have `id`
24
+ * attributes that the consumer's tabpanels reference via
25
+ * `aria-labelledby`. Shadow-DOM ids are not reachable from light-DOM
26
+ * ARIA references. Rendering the tab buttons directly in light DOM
27
+ * makes the ARIA association work correctly, at the cost of style
28
+ * encapsulation. The visual footprint is small (a bordered button
29
+ * strip + selected state); consumers can override with their own CSS.
30
+ *
31
+ * **Activation model — automatic.** APG allows either automatic
32
+ * (focus = select) or manual (focus, then Enter/Space to select).
33
+ * We use automatic because panels are cheap to swap (they are the
34
+ * consumer's DOM, already rendered). Consumers with expensive panels
35
+ * can defer the swap by listening to `dsChange` and rendering lazily.
36
+ *
37
+ * **Keyboard interactions (full APG enumeration):**
38
+ * - Tab: from outside the tablist → focus lands on the currently
39
+ * selected tab (the only one with tabindex=0). From the selected
40
+ * tab → focus moves out of the tablist to the next focusable
41
+ * element in the document (typically the active tabpanel via its
42
+ * own tabindex=0).
43
+ * - Shift+Tab: reverse of Tab.
44
+ * - Arrow Right (horizontal) / Arrow Down (vertical): move focus to
45
+ * the next enabled tab; automatic activation fires dsChange with
46
+ * the new id. Wraps at the last tab back to the first.
47
+ * - Arrow Left (horizontal) / Arrow Up (vertical): move focus to the
48
+ * previous enabled tab; wraps at the first tab to the last.
49
+ * - Home: focus and activate the first enabled tab.
50
+ * - End: focus and activate the last enabled tab.
51
+ * - Enter / Space: no-op under automatic activation (the tab is
52
+ * already selected). We do not preventDefault so consumers can
53
+ * still form-submit if the tab is inside a form for some reason.
54
+ * - Printable characters: no APG-required behavior (typeahead is
55
+ * optional and out of scope for v1).
56
+ *
57
+ * Wrap-around and skip-disabled behavior: arrow keys skip disabled
58
+ * tabs; Home/End land on the first/last *enabled* tab. If every tab
59
+ * is disabled, focus does not move.
60
+ *
61
+ * **ARIA attributes and value triggers:**
62
+ * - Tablist container: `role="tablist"`, `aria-orientation` set from
63
+ * the `orientation` prop ("horizontal" is the ARIA default; we
64
+ * emit "vertical" explicitly when the prop is "vertical" so AT
65
+ * knows which arrow-key axis is meaningful).
66
+ * - Each tab button: `role="tab"`, `id={item.id}` (the consumer's
67
+ * tabpanel references this via `aria-labelledby`),
68
+ * `aria-selected="true"` on the active tab and `"false"` on the
69
+ * others (toggled every dsChange), `aria-controls={item.controls
70
+ * || item.id}` (the id of the tabpanel this tab governs — defaults
71
+ * to the tab id when the consumer uses the same id for both),
72
+ * `tabindex="0"` on the active tab and `-1` on the others (roving
73
+ * tabindex), `disabled` (native) when the item declares
74
+ * `disabled: true`.
75
+ *
76
+ * **Focus lifecycle:**
77
+ * - On mount: no auto-focus. The tablist is not focused by default;
78
+ * Tab from the previous focusable in the document reaches the
79
+ * active tab.
80
+ * - While focus is inside the tablist: arrow keys, Home, and End
81
+ * move focus (and, under automatic activation, selection) among
82
+ * the tabs. Focus is bounded to the tabs; there is no focus trap,
83
+ * Tab / Shift+Tab still leave the tablist.
84
+ * - When the active tab changes via arrow / Home / End: the newly
85
+ * selected tab's `<button>` receives programmatic focus (via
86
+ * `.focus()`) after Stencil re-renders so the visible focus ring
87
+ * tracks the selected state.
88
+ * - On dsChange from a mouse click: focus follows the click to the
89
+ * clicked tab, matching native button behavior.
90
+ *
91
+ * **Motion:**
92
+ * - Animations: none applied by the component. Consumers who want
93
+ * a selection-indicator slide can style the tabs themselves,
94
+ * gated on `prefers-reduced-motion`.
95
+ *
96
+ * **Contrast:**
97
+ * - Text: inherits `color: inherit`.
98
+ * - Selected-tab visual: an underline drawn with `currentColor`
99
+ * (matches the text and inherits whatever contrast the theme
100
+ * provides). WCAG 1.4.11 (3:1 non-text) is the consumer theme's
101
+ * responsibility.
102
+ *
103
+ * **Screen reader announcement:**
104
+ * - Rendered state: AT announces "tab list, N items" (or the
105
+ * equivalent per AT), then each tab as "Label, tab, X of N,
106
+ * selected" or "not selected".
107
+ * - State change: on arrow-key move under automatic activation, AT
108
+ * announces the newly focused tab plus its selection status.
109
+ *
110
+ * **Consumer contract:**
111
+ * - `items` is either a JSON string (SSR-friendly attribute) or an
112
+ * array (client-side property), matching ds-select's dual API.
113
+ * Each item: `{ id, label, controls?, disabled? }`. Malformed
114
+ * JSON falls back to an empty list (visible-but-empty tablist).
115
+ * - `active` is the id of the currently selected tab. If `active`
116
+ * is not in `items` at mount, the first enabled tab is selected
117
+ * and dsChange fires once to reconcile.
118
+ * - The consumer's `<div role="tabpanel" aria-labelledby={tabId}
119
+ * id={item.controls || item.id} tabindex="0" hidden={id !== active}>`
120
+ * is not rendered by this component — it is the consumer's DOM.
121
+ *
122
+ * **Pending manual verification (see commit message TODO):**
123
+ * - VoiceOver on macOS: confirm "tab list" grouping, tab-of-N
124
+ * position, selected state on selection change, and that arrow
125
+ * navigation announces correctly.
126
+ * - Chromium high-contrast / forced-colors: confirm the selected
127
+ * underline remains visible.
128
+ */
129
+ export type DsTabItem = {
130
+ id: string;
131
+ label: string;
132
+ /**
133
+ * Id of the tabpanel this tab controls. Defaults to `id`.
134
+ */
135
+ controls?: string;
136
+ disabled?: boolean;
137
+ };
138
+ export declare class DsTabs {
139
+ private tabRefs;
140
+ private pendingFocusId?;
141
+ items: string | ReadonlyArray<DsTabItem>;
142
+ active?: string;
143
+ orientation: "horizontal" | "vertical";
144
+ label?: string;
145
+ dsChange: EventEmitter<string>;
146
+ watchItems(): void;
147
+ componentWillLoad(): void;
148
+ componentDidRender(): void;
149
+ private parseItems;
150
+ private enabledItems;
151
+ private selectAndFocus;
152
+ private onClick;
153
+ private onKeyDown;
154
+ render(): any;
155
+ }