@dxlbnl/ui 0.1.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 (208) hide show
  1. package/README.md +94 -0
  2. package/dist/components/cards/Card.stories.svelte +82 -0
  3. package/dist/components/cards/Card.stories.svelte.d.ts +19 -0
  4. package/dist/components/cards/Card.svelte +28 -0
  5. package/dist/components/cards/Card.svelte.d.ts +12 -0
  6. package/dist/components/cards/NoteCard.stories.svelte +94 -0
  7. package/dist/components/cards/NoteCard.stories.svelte.d.ts +19 -0
  8. package/dist/components/cards/NoteCard.svelte +89 -0
  9. package/dist/components/cards/NoteCard.svelte.d.ts +18 -0
  10. package/dist/components/cards/ProductCard.stories.svelte +98 -0
  11. package/dist/components/cards/ProductCard.stories.svelte.d.ts +19 -0
  12. package/dist/components/cards/ProductCard.svelte +150 -0
  13. package/dist/components/cards/ProductCard.svelte.d.ts +22 -0
  14. package/dist/components/cards/ProjectCard.stories.svelte +88 -0
  15. package/dist/components/cards/ProjectCard.stories.svelte.d.ts +19 -0
  16. package/dist/components/cards/ProjectCard.svelte +109 -0
  17. package/dist/components/cards/ProjectCard.svelte.d.ts +20 -0
  18. package/dist/components/cards/index.d.ts +4 -0
  19. package/dist/components/cards/index.js +4 -0
  20. package/dist/components/data/Accordion.stories.svelte +316 -0
  21. package/dist/components/data/Accordion.stories.svelte.d.ts +19 -0
  22. package/dist/components/data/Accordion.svelte +23 -0
  23. package/dist/components/data/Accordion.svelte.d.ts +9 -0
  24. package/dist/components/data/AccordionItem.svelte +112 -0
  25. package/dist/components/data/AccordionItem.svelte.d.ts +11 -0
  26. package/dist/components/data/Table.composition.stories.svelte +67 -0
  27. package/dist/components/data/Table.composition.stories.svelte.d.ts +19 -0
  28. package/dist/components/data/Table.stories.svelte +137 -0
  29. package/dist/components/data/Table.stories.svelte.d.ts +19 -0
  30. package/dist/components/data/Table.svelte +83 -0
  31. package/dist/components/data/Table.svelte.d.ts +14 -0
  32. package/dist/components/data/Tabs.stories.svelte +386 -0
  33. package/dist/components/data/Tabs.stories.svelte.d.ts +19 -0
  34. package/dist/components/data/Tabs.svelte +142 -0
  35. package/dist/components/data/Tabs.svelte.d.ts +19 -0
  36. package/dist/components/data/index.d.ts +4 -0
  37. package/dist/components/data/index.js +4 -0
  38. package/dist/components/feedback/Modal.stories.svelte +192 -0
  39. package/dist/components/feedback/Modal.stories.svelte.d.ts +4 -0
  40. package/dist/components/feedback/Modal.svelte +185 -0
  41. package/dist/components/feedback/Modal.svelte.d.ts +19 -0
  42. package/dist/components/feedback/Toast.stories.svelte +203 -0
  43. package/dist/components/feedback/Toast.stories.svelte.d.ts +19 -0
  44. package/dist/components/feedback/Toast.svelte +109 -0
  45. package/dist/components/feedback/Toast.svelte.d.ts +15 -0
  46. package/dist/components/feedback/ToastRegion.stories.svelte +193 -0
  47. package/dist/components/feedback/ToastRegion.stories.svelte.d.ts +19 -0
  48. package/dist/components/feedback/ToastRegion.svelte +102 -0
  49. package/dist/components/feedback/ToastRegion.svelte.d.ts +9 -0
  50. package/dist/components/feedback/index.d.ts +3 -0
  51. package/dist/components/feedback/index.js +3 -0
  52. package/dist/components/forms/Checkbox.stories.svelte +103 -0
  53. package/dist/components/forms/Checkbox.stories.svelte.d.ts +19 -0
  54. package/dist/components/forms/Checkbox.svelte +150 -0
  55. package/dist/components/forms/Checkbox.svelte.d.ts +11 -0
  56. package/dist/components/forms/Field.stories.svelte +113 -0
  57. package/dist/components/forms/Field.stories.svelte.d.ts +19 -0
  58. package/dist/components/forms/Field.svelte +77 -0
  59. package/dist/components/forms/Field.svelte.d.ts +17 -0
  60. package/dist/components/forms/Input.stories.svelte +58 -0
  61. package/dist/components/forms/Input.stories.svelte.d.ts +19 -0
  62. package/dist/components/forms/Input.svelte +64 -0
  63. package/dist/components/forms/Input.svelte.d.ts +9 -0
  64. package/dist/components/forms/InputWrap.composition.stories.svelte +32 -0
  65. package/dist/components/forms/InputWrap.composition.stories.svelte.d.ts +19 -0
  66. package/dist/components/forms/InputWrap.stories.svelte +53 -0
  67. package/dist/components/forms/InputWrap.stories.svelte.d.ts +19 -0
  68. package/dist/components/forms/InputWrap.svelte +128 -0
  69. package/dist/components/forms/InputWrap.svelte.d.ts +21 -0
  70. package/dist/components/forms/Radio.stories.svelte +70 -0
  71. package/dist/components/forms/Radio.stories.svelte.d.ts +19 -0
  72. package/dist/components/forms/Radio.svelte +109 -0
  73. package/dist/components/forms/Radio.svelte.d.ts +9 -0
  74. package/dist/components/forms/RadioGroup.stories.svelte +115 -0
  75. package/dist/components/forms/RadioGroup.stories.svelte.d.ts +19 -0
  76. package/dist/components/forms/RadioGroup.svelte +116 -0
  77. package/dist/components/forms/RadioGroup.svelte.d.ts +24 -0
  78. package/dist/components/forms/Select.stories.svelte +168 -0
  79. package/dist/components/forms/Select.stories.svelte.d.ts +19 -0
  80. package/dist/components/forms/Select.svelte +262 -0
  81. package/dist/components/forms/Select.svelte.d.ts +23 -0
  82. package/dist/components/forms/Switch.stories.svelte +86 -0
  83. package/dist/components/forms/Switch.stories.svelte.d.ts +19 -0
  84. package/dist/components/forms/Switch.svelte +113 -0
  85. package/dist/components/forms/Switch.svelte.d.ts +11 -0
  86. package/dist/components/forms/Textarea.stories.svelte +40 -0
  87. package/dist/components/forms/Textarea.stories.svelte.d.ts +19 -0
  88. package/dist/components/forms/Textarea.svelte +66 -0
  89. package/dist/components/forms/Textarea.svelte.d.ts +9 -0
  90. package/dist/components/forms/field-context.d.ts +7 -0
  91. package/dist/components/forms/field-context.js +1 -0
  92. package/dist/components/forms/index.d.ts +9 -0
  93. package/dist/components/forms/index.js +9 -0
  94. package/dist/components/layout/Container.stories.svelte +67 -0
  95. package/dist/components/layout/Container.stories.svelte.d.ts +19 -0
  96. package/dist/components/layout/Container.svelte +52 -0
  97. package/dist/components/layout/Container.svelte.d.ts +14 -0
  98. package/dist/components/layout/Grid.stories.svelte +109 -0
  99. package/dist/components/layout/Grid.stories.svelte.d.ts +19 -0
  100. package/dist/components/layout/Grid.svelte +54 -0
  101. package/dist/components/layout/Grid.svelte.d.ts +19 -0
  102. package/dist/components/layout/Inline.stories.svelte +136 -0
  103. package/dist/components/layout/Inline.stories.svelte.d.ts +19 -0
  104. package/dist/components/layout/Inline.svelte +46 -0
  105. package/dist/components/layout/Inline.svelte.d.ts +19 -0
  106. package/dist/components/layout/Prose.stories.svelte +423 -0
  107. package/dist/components/layout/Prose.stories.svelte.d.ts +19 -0
  108. package/dist/components/layout/Prose.svelte +176 -0
  109. package/dist/components/layout/Prose.svelte.d.ts +12 -0
  110. package/dist/components/layout/Rule.stories.svelte +80 -0
  111. package/dist/components/layout/Rule.stories.svelte.d.ts +19 -0
  112. package/dist/components/layout/Rule.svelte +33 -0
  113. package/dist/components/layout/Rule.svelte.d.ts +9 -0
  114. package/dist/components/layout/Spread.stories.svelte +118 -0
  115. package/dist/components/layout/Spread.stories.svelte.d.ts +19 -0
  116. package/dist/components/layout/Spread.svelte +38 -0
  117. package/dist/components/layout/Spread.svelte.d.ts +16 -0
  118. package/dist/components/layout/Stack.stories.svelte +90 -0
  119. package/dist/components/layout/Stack.stories.svelte.d.ts +19 -0
  120. package/dist/components/layout/Stack.svelte +37 -0
  121. package/dist/components/layout/Stack.svelte.d.ts +16 -0
  122. package/dist/components/layout/index.d.ts +7 -0
  123. package/dist/components/layout/index.js +7 -0
  124. package/dist/components/navigation/Breadcrumb.stories.svelte +122 -0
  125. package/dist/components/navigation/Breadcrumb.stories.svelte.d.ts +19 -0
  126. package/dist/components/navigation/Breadcrumb.svelte +70 -0
  127. package/dist/components/navigation/Breadcrumb.svelte.d.ts +13 -0
  128. package/dist/components/navigation/Nav.stories.svelte +323 -0
  129. package/dist/components/navigation/Nav.stories.svelte.d.ts +19 -0
  130. package/dist/components/navigation/Nav.svelte +257 -0
  131. package/dist/components/navigation/Nav.svelte.d.ts +21 -0
  132. package/dist/components/navigation/index.d.ts +2 -0
  133. package/dist/components/navigation/index.js +2 -0
  134. package/dist/components/patterns/ActivityRow.stories.svelte +45 -0
  135. package/dist/components/patterns/ActivityRow.stories.svelte.d.ts +19 -0
  136. package/dist/components/patterns/ActivityRow.svelte +69 -0
  137. package/dist/components/patterns/ActivityRow.svelte.d.ts +16 -0
  138. package/dist/components/patterns/Alert.stories.svelte +63 -0
  139. package/dist/components/patterns/Alert.stories.svelte.d.ts +19 -0
  140. package/dist/components/patterns/Alert.svelte +91 -0
  141. package/dist/components/patterns/Alert.svelte.d.ts +16 -0
  142. package/dist/components/patterns/CtaBlock.stories.svelte +62 -0
  143. package/dist/components/patterns/CtaBlock.stories.svelte.d.ts +19 -0
  144. package/dist/components/patterns/CtaBlock.svelte +80 -0
  145. package/dist/components/patterns/CtaBlock.svelte.d.ts +16 -0
  146. package/dist/components/patterns/KvList.stories.svelte +48 -0
  147. package/dist/components/patterns/KvList.stories.svelte.d.ts +19 -0
  148. package/dist/components/patterns/KvList.svelte +65 -0
  149. package/dist/components/patterns/KvList.svelte.d.ts +15 -0
  150. package/dist/components/patterns/PageHero.stories.svelte +62 -0
  151. package/dist/components/patterns/PageHero.stories.svelte.d.ts +19 -0
  152. package/dist/components/patterns/PageHero.svelte +62 -0
  153. package/dist/components/patterns/PageHero.svelte.d.ts +14 -0
  154. package/dist/components/patterns/ProgressBar.stories.svelte +83 -0
  155. package/dist/components/patterns/ProgressBar.stories.svelte.d.ts +19 -0
  156. package/dist/components/patterns/ProgressBar.svelte +71 -0
  157. package/dist/components/patterns/ProgressBar.svelte.d.ts +13 -0
  158. package/dist/components/patterns/SectionFoot.stories.svelte +37 -0
  159. package/dist/components/patterns/SectionFoot.stories.svelte.d.ts +19 -0
  160. package/dist/components/patterns/SectionFoot.svelte +70 -0
  161. package/dist/components/patterns/SectionFoot.svelte.d.ts +15 -0
  162. package/dist/components/patterns/SectionHead.stories.svelte +67 -0
  163. package/dist/components/patterns/SectionHead.stories.svelte.d.ts +19 -0
  164. package/dist/components/patterns/SectionHead.svelte +54 -0
  165. package/dist/components/patterns/SectionHead.svelte.d.ts +14 -0
  166. package/dist/components/patterns/StatCard.stories.svelte +59 -0
  167. package/dist/components/patterns/StatCard.stories.svelte.d.ts +19 -0
  168. package/dist/components/patterns/StatCard.svelte +57 -0
  169. package/dist/components/patterns/StatCard.svelte.d.ts +15 -0
  170. package/dist/components/patterns/index.d.ts +9 -0
  171. package/dist/components/patterns/index.js +9 -0
  172. package/dist/components/primitives/Button.stories.svelte +132 -0
  173. package/dist/components/primitives/Button.stories.svelte.d.ts +19 -0
  174. package/dist/components/primitives/Button.svelte +142 -0
  175. package/dist/components/primitives/Button.svelte.d.ts +16 -0
  176. package/dist/components/primitives/Heading.stories.svelte +137 -0
  177. package/dist/components/primitives/Heading.stories.svelte.d.ts +19 -0
  178. package/dist/components/primitives/Heading.svelte +107 -0
  179. package/dist/components/primitives/Heading.svelte.d.ts +23 -0
  180. package/dist/components/primitives/Led.stories.svelte +63 -0
  181. package/dist/components/primitives/Led.stories.svelte.d.ts +19 -0
  182. package/dist/components/primitives/Led.svelte +65 -0
  183. package/dist/components/primitives/Led.svelte.d.ts +11 -0
  184. package/dist/components/primitives/TagPill.stories.svelte +90 -0
  185. package/dist/components/primitives/TagPill.stories.svelte.d.ts +19 -0
  186. package/dist/components/primitives/TagPill.svelte +44 -0
  187. package/dist/components/primitives/TagPill.svelte.d.ts +9 -0
  188. package/dist/components/primitives/Text.stories.svelte +252 -0
  189. package/dist/components/primitives/Text.stories.svelte.d.ts +19 -0
  190. package/dist/components/primitives/Text.svelte +101 -0
  191. package/dist/components/primitives/Text.svelte.d.ts +25 -0
  192. package/dist/components/primitives/index.d.ts +5 -0
  193. package/dist/components/primitives/index.js +5 -0
  194. package/dist/index.d.ts +10 -0
  195. package/dist/index.js +10 -0
  196. package/dist/stores/toast.d.ts +19 -0
  197. package/dist/stores/toast.js +22 -0
  198. package/dist/storybook-utils.d.ts +11 -0
  199. package/dist/storybook-utils.js +29 -0
  200. package/dist/tokens/ColorSwatch.svelte +73 -0
  201. package/dist/tokens/ColorSwatch.svelte.d.ts +10 -0
  202. package/dist/tokens/layout.css +144 -0
  203. package/dist/tokens/patterns.css +281 -0
  204. package/dist/tokens/tokens.css +96 -0
  205. package/dist/tokens/tokens.stories.svelte +107 -0
  206. package/dist/tokens/tokens.stories.svelte.d.ts +18 -0
  207. package/dist/tokens/typography.css +159 -0
  208. package/package.json +62 -0
@@ -0,0 +1,103 @@
1
+ <script module lang="ts">
2
+ import { defineMeta } from "@storybook/addon-svelte-csf";
3
+ import { expect, within } from "storybook/test";
4
+ import { resolveTokenColor } from "../../storybook-utils.js";
5
+ import Checkbox from "./Checkbox.svelte";
6
+ // resolveTokenColor resolves via backgroundColor; use for bg-token assertions
7
+
8
+ const { Story } = defineMeta({
9
+ title: "Forms/Checkbox",
10
+ component: Checkbox,
11
+ tags: ["autodocs"],
12
+ });
13
+ </script>
14
+
15
+ <!-- AC-14: unchecked by default -->
16
+ <Story name="Default (Unchecked)" args={{ label: "Enable notifications", checked: false }}
17
+ play={async ({ canvasElement }) => {
18
+ const canvas = within(canvasElement);
19
+ const input = canvas.getByRole("checkbox");
20
+ await expect(input).toBeVisible();
21
+ await expect(input).not.toBeChecked();
22
+ // AC-20: indicator background is transparent when unchecked
23
+ const indicator = canvasElement.querySelector(".checkbox-indicator");
24
+ await expect(getComputedStyle(indicator!).backgroundColor).toBe("rgba(0, 0, 0, 0)");
25
+ }} />
26
+
27
+ <!-- AC-15: checked state -->
28
+ <Story name="Checked" args={{ label: "Enable notifications", checked: true }}
29
+ play={async ({ canvasElement }) => {
30
+ const canvas = within(canvasElement);
31
+ const input = canvas.getByRole("checkbox");
32
+ await expect(input).toBeChecked();
33
+ // AC-19: indicator background matches var(--amber) when checked
34
+ const amberColor = resolveTokenColor("--amber");
35
+ const indicator = canvasElement.querySelector(".checkbox-indicator");
36
+ await expect(getComputedStyle(indicator!).backgroundColor).toBe(amberColor);
37
+ }} />
38
+
39
+ <!-- AC-16: indeterminate state sets DOM property and aria-checked="mixed" -->
40
+ <Story name="Indeterminate" args={{ label: "Select all", indeterminate: true, checked: false }}
41
+ play={async ({ canvasElement }) => {
42
+ const canvas = within(canvasElement);
43
+ const input = canvas.getByRole("checkbox");
44
+ await expect(input).toBeVisible();
45
+ // AC-16: DOM .indeterminate property must be true after mount (set via $effect)
46
+ await expect((input as HTMLInputElement).indeterminate).toBe(true);
47
+ }} />
48
+
49
+ <!-- AC-17: disabled unchecked — toBeDisabled and wrap opacity 0.4 -->
50
+ <Story name="Disabled Unchecked" args={{ label: "Locked option", disabled: true, checked: false }}
51
+ play={async ({ canvasElement }) => {
52
+ const canvas = within(canvasElement);
53
+ const input = canvas.getByRole("checkbox");
54
+ await expect(input).toBeDisabled();
55
+ // AC-17: wrap opacity 0.4 and cursor not-allowed
56
+ const wrap = canvasElement.querySelector(".checkbox-wrap");
57
+ await expect(getComputedStyle(wrap!).opacity).toBe("0.4");
58
+ await expect(getComputedStyle(wrap!).cursor).toBe("not-allowed");
59
+ }} />
60
+
61
+ <!-- AC-17: disabled checked — both disabled and checked -->
62
+ <Story name="Disabled Checked" args={{ label: "Locked option", disabled: true, checked: true }}
63
+ play={async ({ canvasElement }) => {
64
+ const canvas = within(canvasElement);
65
+ const input = canvas.getByRole("checkbox");
66
+ await expect(input).toBeDisabled();
67
+ await expect(input).toBeChecked();
68
+ }} />
69
+
70
+ <!-- AC-18: Space key toggles checked state; AC-1/AC-2/AC-4/AC-8 regression guards -->
71
+ <Story name="Space to Toggle" args={{ label: "Toggle me", checked: false }}
72
+ play={async ({ canvasElement, userEvent }) => {
73
+ const canvas = within(canvasElement);
74
+ const input = canvas.getByRole("checkbox");
75
+ const indicator = canvasElement.querySelector(".checkbox-indicator");
76
+ const wrap = canvasElement.querySelector(".checkbox-wrap");
77
+
78
+ // AC-1: indicator is exactly 16×16 before toggle
79
+ const indicatorRectBefore = indicator!.getBoundingClientRect();
80
+ await expect(indicatorRectBefore.width).toBe(16);
81
+ await expect(indicatorRectBefore.height).toBe(16);
82
+
83
+ // Capture wrap row height before toggle
84
+ const wrapHeightBefore = wrap!.getBoundingClientRect().height;
85
+
86
+ await expect(input).not.toBeChecked();
87
+ await input.focus();
88
+ await userEvent.keyboard(" ");
89
+ await expect(input).toBeChecked();
90
+
91
+ // AC-2 / AC-4: indicator dimensions must not change after toggle
92
+ const indicatorRectAfter = indicator!.getBoundingClientRect();
93
+ await expect(indicatorRectAfter.width).toBe(16);
94
+ await expect(indicatorRectAfter.height).toBe(16);
95
+
96
+ // AC-4: wrap row height must not change (the layout shift symptom)
97
+ const wrapHeightAfter = wrap!.getBoundingClientRect().height;
98
+ await expect(wrapHeightAfter).toBe(wrapHeightBefore);
99
+
100
+ // AC-8: ::after must be position:absolute so it is out of flow
101
+ const afterPosition = getComputedStyle(indicator!, "::after").position;
102
+ await expect(afterPosition).toBe("absolute");
103
+ }} />
@@ -0,0 +1,19 @@
1
+ import Checkbox from "./Checkbox.svelte";
2
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
3
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
4
+ $$bindings?: Bindings;
5
+ } & Exports;
6
+ (internal: unknown, props: {
7
+ $$events?: Events;
8
+ $$slots?: Slots;
9
+ }): Exports & {
10
+ $set?: any;
11
+ $on?: any;
12
+ };
13
+ z_$$bindings?: Bindings;
14
+ }
15
+ declare const Checkbox: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
16
+ [evt: string]: CustomEvent<any>;
17
+ }, {}, {}, string>;
18
+ type Checkbox = InstanceType<typeof Checkbox>;
19
+ export default Checkbox;
@@ -0,0 +1,150 @@
1
+ <script lang="ts">
2
+ import type { HTMLInputAttributes } from "svelte/elements";
3
+ import { getContext } from "svelte";
4
+ import type { FieldContext } from "./field-context.js";
5
+ import { FIELD_CONTEXT_KEY } from "./field-context.js";
6
+
7
+ interface Props extends HTMLInputAttributes {
8
+ /** Visible label text rendered next to the checkbox. */
9
+ label: string;
10
+ /** Show the indeterminate (−) state. @default false */
11
+ indeterminate?: boolean;
12
+ [key: string]: unknown;
13
+ }
14
+
15
+ let {
16
+ label,
17
+ indeterminate = false,
18
+ checked = $bindable(false),
19
+ disabled,
20
+ ...rest
21
+ }: Props = $props();
22
+
23
+ let inputEl: HTMLInputElement | undefined = $state();
24
+
25
+ const fieldCtx = getContext<FieldContext | undefined>(FIELD_CONTEXT_KEY);
26
+
27
+ let resolvedId = $derived(
28
+ fieldCtx ? fieldCtx.inputId : (rest.id as string | undefined),
29
+ );
30
+ let resolvedAriaInvalid:
31
+ | boolean
32
+ | "true"
33
+ | "false"
34
+ | "grammar"
35
+ | "spelling"
36
+ | null
37
+ | undefined = $derived(fieldCtx?.hasError ? "true" : undefined);
38
+ let resolvedAriaDescribedby = $derived(
39
+ fieldCtx?.hasHint ? fieldCtx.hintId : undefined,
40
+ );
41
+
42
+ $effect(() => {
43
+ if (inputEl) inputEl.indeterminate = indeterminate;
44
+ });
45
+ </script>
46
+
47
+ <label class="checkbox-wrap" class:disabled>
48
+ <input
49
+ type="checkbox"
50
+ class="checkbox-input"
51
+ bind:checked
52
+ {disabled}
53
+ bind:this={inputEl}
54
+ id={resolvedId}
55
+ aria-invalid={resolvedAriaInvalid}
56
+ aria-describedby={resolvedAriaDescribedby}
57
+ {...rest}
58
+ />
59
+ <span class="checkbox-indicator" aria-hidden="true"></span>
60
+ <span class="checkbox-label">{label}</span>
61
+ </label>
62
+
63
+ <style>
64
+ .checkbox-wrap {
65
+ display: inline-flex;
66
+ align-items: center;
67
+ gap: 8px;
68
+ cursor: pointer;
69
+ user-select: none;
70
+ font-family: var(--mono);
71
+ font-size: 13px;
72
+ color: var(--ink);
73
+ position: relative;
74
+
75
+ &.disabled {
76
+ opacity: 0.4;
77
+ cursor: not-allowed;
78
+ }
79
+ }
80
+
81
+ .checkbox-input {
82
+ position: absolute;
83
+ width: 1px;
84
+ height: 1px;
85
+ margin: -1px;
86
+ padding: 0;
87
+ border: 0;
88
+ overflow: hidden;
89
+ clip: rect(0, 0, 0, 0);
90
+ white-space: nowrap;
91
+
92
+ &:checked + .checkbox-indicator {
93
+ background: var(--amber);
94
+ border-color: var(--amber);
95
+
96
+ &::after {
97
+ content: "✔";
98
+ color: var(--bg);
99
+ font-size: 20px;
100
+ line-height: 1;
101
+ position: absolute;
102
+ top: 50%;
103
+ left: 50%;
104
+ transform: translate(-50%, -50%);
105
+ }
106
+ }
107
+
108
+ &:indeterminate + .checkbox-indicator {
109
+ background: var(--amber);
110
+ border-color: var(--amber);
111
+ opacity: 0.6;
112
+
113
+ &::after {
114
+ content: "–";
115
+ color: var(--bg);
116
+ font-size: 20px;
117
+ line-height: 1;
118
+ position: absolute;
119
+ top: 50%;
120
+ left: 50%;
121
+ transform: translate(-50%, -50%);
122
+ }
123
+ }
124
+
125
+ &:focus-visible + .checkbox-indicator {
126
+ outline: 2px solid var(--amber);
127
+ outline-offset: 2px;
128
+ }
129
+ }
130
+
131
+ .checkbox-indicator {
132
+ display: inline-flex;
133
+ align-items: center;
134
+ justify-content: center;
135
+ width: 16px;
136
+ height: 16px;
137
+ flex-shrink: 0;
138
+ border: 1px solid var(--rule-strong);
139
+ background: transparent;
140
+ border-radius: 0;
141
+ transition:
142
+ background var(--transition),
143
+ border-color var(--transition);
144
+ position: relative;
145
+ }
146
+
147
+ .checkbox-label {
148
+ color: var(--ink);
149
+ }
150
+ </style>
@@ -0,0 +1,11 @@
1
+ import type { HTMLInputAttributes } from "svelte/elements";
2
+ interface Props extends HTMLInputAttributes {
3
+ /** Visible label text rendered next to the checkbox. */
4
+ label: string;
5
+ /** Show the indeterminate (−) state. @default false */
6
+ indeterminate?: boolean;
7
+ [key: string]: unknown;
8
+ }
9
+ declare const Checkbox: import("svelte").Component<Props, {}, "checked">;
10
+ type Checkbox = ReturnType<typeof Checkbox>;
11
+ export default Checkbox;
@@ -0,0 +1,113 @@
1
+ <script module lang="ts">
2
+ import { defineMeta } from "@storybook/addon-svelte-csf";
3
+ import { expect, within } from "storybook/test";
4
+ import Field from "./Field.svelte";
5
+ import Input from "./Input.svelte";
6
+ import Textarea from "./Textarea.svelte";
7
+ import Radio from "./Radio.svelte";
8
+ import { resolveTokenColor } from "../../storybook-utils.js";
9
+
10
+ const { Story } = defineMeta({
11
+ title: "Forms/Field",
12
+ component: Field,
13
+ tags: ["autodocs"],
14
+ });
15
+ </script>
16
+
17
+ <Story name="Default — Hint"
18
+ args={{ label: "Bench Note", inputId: "bench-note", hint: "Describe the work session." }}
19
+ play={async ({ canvasElement }) => {
20
+ const canvas = within(canvasElement);
21
+ const input = canvas.getByLabelText("Bench Note");
22
+ await expect(input).toBeVisible();
23
+ await expect(input.tagName).toBe("INPUT");
24
+ const hint = canvas.getByText("Describe the work session.");
25
+ await expect(hint).toBeVisible();
26
+ const inkFaint = resolveTokenColor("--ink-faint");
27
+ await expect(getComputedStyle(hint).color).toBe(inkFaint);
28
+ }}>
29
+ <Input id="bench-note" aria-describedby="bench-note-hint" type="text" placeholder="Placeholder…" />
30
+ </Story>
31
+
32
+ <Story name="Error State"
33
+ args={{ label: "Email", inputId: "email-field", error: "Invalid email address." }}
34
+ play={async ({ canvasElement }) => {
35
+ const canvas = within(canvasElement);
36
+ const input = canvas.getByLabelText("Email");
37
+ await expect(input).toBeVisible();
38
+ await expect(input.getAttribute("aria-invalid")).toBe("true");
39
+ const errorText = canvas.getByText("Invalid email address.");
40
+ await expect(errorText).toBeVisible();
41
+ const dangerColor = resolveTokenColor("--danger");
42
+ await expect(getComputedStyle(errorText).color).toBe(dangerColor);
43
+ const textbox = canvas.getByRole("textbox", { name: "Email" });
44
+ await expect(textbox.getAttribute("aria-describedby")).toBe("email-field-hint");
45
+ }}>
46
+ <Input id="email-field" aria-describedby="email-field-hint" aria-invalid="true" error={true} type="email" value="bad@" />
47
+ </Story>
48
+
49
+ <Story name="Disabled"
50
+ args={{ label: "Locked", inputId: "locked-field", hint: "Field is locked." }}
51
+ play={async ({ canvasElement }) => {
52
+ const canvas = within(canvasElement);
53
+ const input = canvas.getByLabelText("Locked");
54
+ await expect(input).toBeDisabled();
55
+ await expect(canvas.getByText("Field is locked.")).toBeVisible();
56
+ }}>
57
+ <Input id="locked-field" type="text" value="read-only" disabled />
58
+ </Story>
59
+
60
+ <Story name="With Textarea"
61
+ args={{ label: "Notes", inputId: "notes-field", hint: "Up to 500 characters." }}
62
+ play={async ({ canvasElement }) => {
63
+ const canvas = within(canvasElement);
64
+ const control = canvas.getByLabelText("Notes");
65
+ await expect(control.tagName).toBe("TEXTAREA");
66
+ }}>
67
+ <Textarea id="notes-field" aria-describedby="notes-field-hint" />
68
+ </Story>
69
+
70
+ <!-- AC-54 through AC-62: Field auto-injects aria-invalid + aria-describedby via context -->
71
+ <Story name="Auto ARIA Wiring"
72
+ args={{ label: "Email", inputId: "auto-email", error: "Invalid email address." }}
73
+ play={async ({ canvasElement }) => {
74
+ const canvas = within(canvasElement);
75
+ const input = canvas.getByRole("textbox");
76
+ // AC-55: id is set from Field context (no manual id on the Input)
77
+ await expect(input.getAttribute("id")).toBe("auto-email");
78
+ // AC-56: aria-invalid is "true" because Field has an error prop
79
+ await expect(input.getAttribute("aria-invalid")).toBe("true");
80
+ // AC-57: aria-describedby is "{inputId}-hint" because error is set
81
+ await expect(input.getAttribute("aria-describedby")).toBe("auto-email-hint");
82
+ // AC-61: the hint span with the error text is visible
83
+ const hint = canvas.getByText("Invalid email address.");
84
+ await expect(hint).toBeVisible();
85
+ await expect(hint.getAttribute("id")).toBe("auto-email-hint");
86
+ }}>
87
+ <Input />
88
+ </Story>
89
+
90
+ <Story name="Required Field"
91
+ args={{ label: "Project Name", inputId: "project-name" }}
92
+ play={async ({ canvasElement }) => {
93
+ const canvas = within(canvasElement);
94
+ const input = canvas.getByRole("textbox", { name: "Project Name" });
95
+ await expect(input).toBeRequired();
96
+ await expect(canvasElement.querySelector(".field-hint")).toBeNull();
97
+ }}>
98
+ <Input id="project-name" type="text" required />
99
+ </Story>
100
+
101
+ <!-- AC-60: Radio inside Field receives aria-invalid + aria-describedby from context -->
102
+ <Story name="Auto ARIA Wiring — Radio"
103
+ args={{ label: "Module type", inputId: "radio-ac60", error: "Select one option." }}
104
+ play={async ({ canvasElement }) => {
105
+ const canvas = within(canvasElement);
106
+ const radio = canvas.getByRole("radio");
107
+ await expect(radio.getAttribute("aria-invalid")).toBe("true");
108
+ await expect(radio.getAttribute("aria-describedby")).toBe("radio-ac60-hint");
109
+ const hint = canvas.getByText("Select one option.");
110
+ await expect(hint).toBeVisible();
111
+ }}>
112
+ <Radio name="mod" value="osc" label="Oscillator" />
113
+ </Story>
@@ -0,0 +1,19 @@
1
+ import Field from "./Field.svelte";
2
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
3
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
4
+ $$bindings?: Bindings;
5
+ } & Exports;
6
+ (internal: unknown, props: {
7
+ $$events?: Events;
8
+ $$slots?: Slots;
9
+ }): Exports & {
10
+ $set?: any;
11
+ $on?: any;
12
+ };
13
+ z_$$bindings?: Bindings;
14
+ }
15
+ declare const Field: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
16
+ [evt: string]: CustomEvent<any>;
17
+ }, {}, {}, string>;
18
+ type Field = InstanceType<typeof Field>;
19
+ export default Field;
@@ -0,0 +1,77 @@
1
+ <script lang="ts">
2
+ import type { HTMLAttributes } from 'svelte/elements'
3
+ import type { Snippet } from 'svelte'
4
+ import { setContext } from 'svelte'
5
+ import Stack from '../layout/Stack.svelte'
6
+ import { FIELD_CONTEXT_KEY } from './field-context.js'
7
+
8
+ interface Props extends HTMLAttributes<HTMLDivElement> {
9
+ /** Visible label text rendered above the input. */
10
+ label: string
11
+ /** `id` of the associated input — wires the `<label for>` and context. */
12
+ inputId: string
13
+ /** Helper text shown below the input. */
14
+ hint?: string
15
+ /** Error message — overrides hint and sets error state on nested inputs. */
16
+ error?: string
17
+ children: Snippet
18
+ [key: string]: unknown
19
+ }
20
+
21
+ let {
22
+ label,
23
+ inputId,
24
+ hint,
25
+ error,
26
+ children,
27
+ ...rest
28
+ }: Props = $props()
29
+
30
+ let hintId = $derived(`${inputId}-hint`)
31
+ let hasError = $derived(!!error)
32
+ let hasHint = $derived(!!(hint || error))
33
+ let hintText = $derived(error ?? hint)
34
+
35
+ // Provide context so nested Input/Textarea/Checkbox/Radio/Switch
36
+ // can auto-wire aria-invalid, aria-describedby, and id.
37
+ // Use a getter object so children re-read reactive values on each render.
38
+ const ctx = {
39
+ get inputId() { return inputId },
40
+ get hintId() { return hintId },
41
+ get hasHint() { return hasHint },
42
+ get hasError() { return hasError },
43
+ }
44
+ setContext(FIELD_CONTEXT_KEY, ctx)
45
+ </script>
46
+
47
+ <Stack gap="xs" style="min-width: 0;" {...rest}>
48
+ <label for={inputId} class="field-label">{label}</label>
49
+ {@render children()}
50
+ {#if hintText}
51
+ <span id={hintId} class="field-hint" class:err={hasError} aria-live="polite">
52
+ {hintText}
53
+ </span>
54
+ {/if}
55
+ </Stack>
56
+
57
+ <style>
58
+ .field-label {
59
+ font-family: var(--mono);
60
+ font-size: 10px;
61
+ letter-spacing: 0.1em;
62
+ text-transform: uppercase;
63
+ color: var(--ink-faint);
64
+ display: block;
65
+ }
66
+
67
+ .field-hint {
68
+ font-family: var(--mono);
69
+ font-size: 10px;
70
+ letter-spacing: 0.04em;
71
+ color: var(--ink-faint);
72
+ }
73
+
74
+ .field-hint.err {
75
+ color: var(--danger);
76
+ }
77
+ </style>
@@ -0,0 +1,17 @@
1
+ import type { HTMLAttributes } from 'svelte/elements';
2
+ import type { Snippet } from 'svelte';
3
+ interface Props extends HTMLAttributes<HTMLDivElement> {
4
+ /** Visible label text rendered above the input. */
5
+ label: string;
6
+ /** `id` of the associated input — wires the `<label for>` and context. */
7
+ inputId: string;
8
+ /** Helper text shown below the input. */
9
+ hint?: string;
10
+ /** Error message — overrides hint and sets error state on nested inputs. */
11
+ error?: string;
12
+ children: Snippet;
13
+ [key: string]: unknown;
14
+ }
15
+ declare const Field: import("svelte").Component<Props, {}, "">;
16
+ type Field = ReturnType<typeof Field>;
17
+ export default Field;
@@ -0,0 +1,58 @@
1
+ <script module lang="ts">
2
+ import { defineMeta } from "@storybook/addon-svelte-csf";
3
+ import { expect, within } from "storybook/test";
4
+ import Input from "./Input.svelte";
5
+ import { resolveTokenColor } from "../../storybook-utils.js";
6
+
7
+ const { Story } = defineMeta({
8
+ title: "Forms/Input",
9
+ component: Input,
10
+ tags: ["autodocs"],
11
+ });
12
+ </script>
13
+
14
+ <Story name="Default" args={{ type: "text", placeholder: "Placeholder…" }}
15
+ play={async ({ canvasElement }) => {
16
+ const canvas = within(canvasElement);
17
+ const input = canvas.getByRole("textbox");
18
+ await expect(input).toBeVisible();
19
+ await expect(input).toBeEnabled();
20
+ const bgSunken = resolveTokenColor("--bg-sunken");
21
+ await expect(getComputedStyle(input).backgroundColor).toBe(bgSunken);
22
+ await expect(getComputedStyle(input).borderTopStyle).toBe("solid");
23
+ await expect(getComputedStyle(input).borderTopWidth).toBe("1px");
24
+ await expect(getComputedStyle(input).fontFamily).toContain("JetBrains Mono");
25
+ }} />
26
+
27
+ <Story name="With Value" args={{ type: "text", value: "CONDUIT-PDX2" }}
28
+ play={async ({ canvasElement }) => {
29
+ const canvas = within(canvasElement);
30
+ const input = canvas.getByRole("textbox");
31
+ await expect(input).toHaveValue("CONDUIT-PDX2");
32
+ }} />
33
+
34
+ <Story name="Error State" args={{ type: "text", value: "bad@input", error: true }}
35
+ play={async ({ canvasElement }) => {
36
+ const canvas = within(canvasElement);
37
+ const input = canvas.getByRole("textbox");
38
+ await expect(input).toBeVisible();
39
+ await expect(input).toBeEnabled();
40
+ const dangerColor = resolveTokenColor("--danger");
41
+ await expect(getComputedStyle(input).borderColor).toBe(dangerColor);
42
+ }} />
43
+
44
+ <Story name="Disabled" args={{ type: "text", value: "read-only", disabled: true }}
45
+ play={async ({ canvasElement }) => {
46
+ const canvas = within(canvasElement);
47
+ const input = canvas.getByRole("textbox");
48
+ await expect(input).toBeDisabled();
49
+ await expect(getComputedStyle(input).opacity).toBe("0.4");
50
+ await expect(getComputedStyle(input).cursor).toBe("not-allowed");
51
+ }} />
52
+
53
+ <Story name="Email" args={{ type: "email", placeholder: "you@domain.com" }}
54
+ play={async ({ canvasElement }) => {
55
+ const canvas = within(canvasElement);
56
+ const input = canvas.getByRole("textbox");
57
+ await expect(input.getAttribute("type")).toBe("email");
58
+ }} />
@@ -0,0 +1,19 @@
1
+ import Input from "./Input.svelte";
2
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
3
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
4
+ $$bindings?: Bindings;
5
+ } & Exports;
6
+ (internal: unknown, props: {
7
+ $$events?: Events;
8
+ $$slots?: Slots;
9
+ }): Exports & {
10
+ $set?: any;
11
+ $on?: any;
12
+ };
13
+ z_$$bindings?: Bindings;
14
+ }
15
+ declare const Input: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
16
+ [evt: string]: CustomEvent<any>;
17
+ }, {}, {}, string>;
18
+ type Input = InstanceType<typeof Input>;
19
+ export default Input;
@@ -0,0 +1,64 @@
1
+ <script lang="ts">
2
+ import type { HTMLInputAttributes } from 'svelte/elements'
3
+ import { getContext } from 'svelte'
4
+ import type { FieldContext } from './field-context.js'
5
+ import { FIELD_CONTEXT_KEY } from './field-context.js'
6
+
7
+ interface Props extends HTMLInputAttributes {
8
+ /** Show the error (danger-border) state. Auto-set when inside a Field with an error prop. @default false */
9
+ error?: boolean
10
+ [key: string]: unknown
11
+ }
12
+
13
+ let { error = false, ...rest }: Props = $props()
14
+
15
+ const fieldCtx = getContext<FieldContext | undefined>(FIELD_CONTEXT_KEY)
16
+
17
+ let resolvedId = $derived(fieldCtx ? fieldCtx.inputId : (rest.id as string | undefined))
18
+ // 'true' is a valid Booleanish value for aria-invalid
19
+ let resolvedAriaInvalid: boolean | 'true' | 'false' | 'grammar' | 'spelling' | null | undefined = $derived(
20
+ fieldCtx?.hasError ? 'true' : undefined
21
+ )
22
+ let resolvedAriaDescribedby = $derived(
23
+ fieldCtx?.hasHint ? fieldCtx.hintId : (rest['aria-describedby'] as string | undefined)
24
+ )
25
+ let resolvedError = $derived(error || (fieldCtx?.hasError ?? false))
26
+ </script>
27
+
28
+ <input
29
+ class="input"
30
+ class:err={resolvedError}
31
+ id={resolvedId}
32
+ aria-invalid={resolvedAriaInvalid}
33
+ aria-describedby={resolvedAriaDescribedby}
34
+ {...rest}
35
+ />
36
+
37
+ <style>
38
+ .input {
39
+ font-family: var(--mono);
40
+ font-size: 13px;
41
+ letter-spacing: 0.02em;
42
+ background: var(--bg-sunken);
43
+ color: var(--ink);
44
+ border: 1px solid var(--rule-strong);
45
+ padding: 7px 10px;
46
+ outline: none;
47
+ width: 100%;
48
+ transition: border-color var(--transition);
49
+ border-radius: 0;
50
+ }
51
+
52
+ .input:focus {
53
+ border-color: var(--amber);
54
+ }
55
+
56
+ .input.err {
57
+ border-color: var(--danger);
58
+ }
59
+
60
+ .input:disabled {
61
+ opacity: 0.4;
62
+ cursor: not-allowed;
63
+ }
64
+ </style>
@@ -0,0 +1,9 @@
1
+ import type { HTMLInputAttributes } from 'svelte/elements';
2
+ interface Props extends HTMLInputAttributes {
3
+ /** Show the error (danger-border) state. Auto-set when inside a Field with an error prop. @default false */
4
+ error?: boolean;
5
+ [key: string]: unknown;
6
+ }
7
+ declare const Input: import("svelte").Component<Props, {}, "">;
8
+ type Input = ReturnType<typeof Input>;
9
+ export default Input;