@dxlbnl/ui 0.1.0 → 1.0.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 (41) hide show
  1. package/README.md +1 -1
  2. package/dist/components/cards/NoteCard.svelte +1 -1
  3. package/dist/components/cards/ProductCard.svelte +1 -1
  4. package/dist/components/cards/ProjectCard.svelte +1 -1
  5. package/dist/components/feedback/Alert.stories.svelte +131 -0
  6. package/dist/components/feedback/Alert.svelte +117 -0
  7. package/dist/components/{patterns → feedback}/Alert.svelte.d.ts +4 -2
  8. package/dist/components/feedback/Modal.stories.svelte +49 -117
  9. package/dist/components/feedback/Modal.svelte +47 -26
  10. package/dist/components/feedback/Toast.stories.svelte +89 -53
  11. package/dist/components/feedback/Toast.svelte +5 -64
  12. package/dist/components/feedback/Toast.svelte.d.ts +2 -0
  13. package/dist/components/feedback/ToastRegion.stories.svelte +11 -0
  14. package/dist/components/feedback/ToastRegion.svelte +1 -0
  15. package/dist/components/feedback/index.d.ts +1 -0
  16. package/dist/components/feedback/index.js +1 -0
  17. package/dist/components/forms/Input.stories.svelte +15 -0
  18. package/dist/components/forms/Input.svelte +11 -0
  19. package/dist/components/forms/InputWrap.composition.stories.svelte +11 -0
  20. package/dist/components/forms/InputWrap.stories.svelte +9 -0
  21. package/dist/components/forms/InputWrap.svelte +13 -11
  22. package/dist/components/forms/Switch.stories.svelte +55 -47
  23. package/dist/components/forms/Switch.svelte +4 -3
  24. package/dist/components/patterns/CtaBlock.svelte +1 -0
  25. package/dist/components/patterns/PageHero.stories.svelte +39 -0
  26. package/dist/components/patterns/PageHero.svelte +17 -3
  27. package/dist/components/patterns/PageHero.svelte.d.ts +5 -1
  28. package/dist/components/patterns/index.d.ts +0 -1
  29. package/dist/components/patterns/index.js +0 -1
  30. package/dist/components/primitives/Heading.stories.svelte +6 -30
  31. package/dist/components/primitives/Heading.svelte +2 -11
  32. package/dist/components/primitives/Heading.svelte.d.ts +0 -3
  33. package/dist/index.d.ts +2 -2
  34. package/dist/index.js +2 -2
  35. package/dist/stores/toast.d.ts +2 -0
  36. package/dist/stores/toast.js +1 -0
  37. package/dist/tokens/tokens.css +2 -0
  38. package/package.json +1 -1
  39. package/dist/components/patterns/Alert.stories.svelte +0 -63
  40. package/dist/components/patterns/Alert.svelte +0 -91
  41. /package/dist/components/{patterns → feedback}/Alert.stories.svelte.d.ts +0 -0
@@ -1,7 +1,7 @@
1
1
  <script module lang="ts">
2
2
  import { defineMeta } from "@storybook/addon-svelte-csf";
3
3
  import { expect, within } from "storybook/test";
4
- import { resolveTokenColor } from "../../storybook-utils.js";
4
+ import { resolveTokenFgColor } from "../../storybook-utils.js";
5
5
  import Toast from "./Toast.svelte";
6
6
 
7
7
  const { Story } = defineMeta({
@@ -11,11 +11,12 @@
11
11
  });
12
12
  </script>
13
13
 
14
- <!-- AC 16, 23, 26, 27, 29 success variant: role=status, icon "ok", border-color=--ok -->
14
+ <!-- AC-13: success varianttitle "Build complete", .alert-tag "ok", borderLeftColor on .alert -->
15
15
  <Story
16
16
  name="Success"
17
17
  args={{
18
18
  id: "toast-1",
19
+ title: "Build complete",
19
20
  message: "Build completed successfully.",
20
21
  variant: "success",
21
22
  ondismiss: () => {},
@@ -23,43 +24,47 @@
23
24
  play={async ({ canvasElement }) => {
24
25
  const canvas = within(canvasElement);
25
26
 
26
- // AC 16: ok variant renders role="status"
27
+ // AC-12: Toast wrapper keeps role="status"
27
28
  const toast = canvas.getByRole("status");
28
29
  await expect(toast).toBeVisible();
29
30
 
30
- // AC 26: message text visible
31
+ // AC-11: .alert-title visible with the title text
32
+ const titleEl = canvasElement.querySelector(".alert-title");
33
+ await expect(titleEl).not.toBeNull();
34
+ await expect(titleEl!.textContent!.trim()).toBe("Build complete");
35
+
36
+ // message text visible
31
37
  await expect(
32
38
  canvas.getByText("Build completed successfully."),
33
39
  ).toBeVisible();
34
40
 
35
- // AC 27: close button present, enabled
41
+ // AC-16: dismiss button still present (rendered by Toast, not Alert)
36
42
  const closeBtn = canvas.getByRole("button", {
37
43
  name: /Dismiss notification/i,
38
44
  });
39
45
  await expect(closeBtn).toBeVisible();
40
46
  await expect(closeBtn).toBeEnabled();
41
47
 
42
- // AC 22, 23: .toast-icon with aria-hidden="true" and text "ok"
43
- const icon = canvasElement.querySelector(".toast-icon");
44
- await expect(icon).not.toBeNull();
45
- await expect(icon!.getAttribute("aria-hidden")).toBe("true");
46
- await expect(icon!.textContent!.trim()).toBe("ok");
47
-
48
- // AC 29: border-color matches var(--ok)
49
- const okColor = resolveTokenColor("--ok");
50
- await expect(getComputedStyle(toast).borderColor).toBe(okColor);
51
-
52
- // AC 32: background-color matches var(--bg-elev)
53
- const bgElev = resolveTokenColor("--bg-elev");
54
- await expect(getComputedStyle(toast).backgroundColor).toBe(bgElev);
48
+ // AC-13: .alert-tag (not .toast-icon) with text "ok"
49
+ const tag = canvasElement.querySelector(".alert-tag");
50
+ await expect(tag).not.toBeNull();
51
+ await expect(tag!.getAttribute("aria-hidden")).toBe("true");
52
+ await expect(tag!.textContent!.trim()).toBe("ok");
53
+
54
+ // AC-17: borderLeftColor on .alert child element, not Toast root
55
+ const alertEl = canvasElement.querySelector(".alert");
56
+ await expect(alertEl).not.toBeNull();
57
+ const okColor = resolveTokenFgColor("--ok");
58
+ await expect(getComputedStyle(alertEl!).borderLeftColor).toBe(okColor);
55
59
  }}
56
60
  />
57
61
 
58
- <!-- AC 17, 24, 30 warning variant: role=status, icon "!!", border-color=--amber -->
62
+ <!-- AC-14: warning varianttitle "High load", .alert-tag "!!", borderLeftColor on .alert -->
59
63
  <Story
60
64
  name="Warning"
61
65
  args={{
62
66
  id: "toast-2",
67
+ title: "High load",
63
68
  message: "+12V rail at 88% capacity.",
64
69
  variant: "warning",
65
70
  ondismiss: () => {},
@@ -67,29 +72,43 @@
67
72
  play={async ({ canvasElement }) => {
68
73
  const canvas = within(canvasElement);
69
74
 
70
- // AC 17: amber variant renders role="status"
75
+ // AC-12: Toast wrapper keeps role="status"
71
76
  const toast = canvas.getByRole("status");
72
77
  await expect(toast).toBeVisible();
73
78
 
74
- // AC 26: message text visible
79
+ // AC-11: .alert-title visible
80
+ const titleEl = canvasElement.querySelector(".alert-title");
81
+ await expect(titleEl).not.toBeNull();
82
+ await expect(titleEl!.textContent!.trim()).toBe("High load");
83
+
84
+ // message text visible
75
85
  await expect(canvas.getByText(/12V rail at 88%/i)).toBeVisible();
76
86
 
77
- // AC 24: .toast-icon text is "!!"
78
- const icon = canvasElement.querySelector(".toast-icon");
79
- await expect(icon).not.toBeNull();
80
- await expect(icon!.textContent!.trim()).toBe("!!");
87
+ // AC-16: dismiss button still present
88
+ const closeBtn = canvas.getByRole("button", {
89
+ name: /Dismiss notification/i,
90
+ });
91
+ await expect(closeBtn).toBeVisible();
81
92
 
82
- // AC 30: border-color matches var(--amber)
83
- const amberColor = resolveTokenColor("--amber");
84
- await expect(getComputedStyle(toast).borderColor).toBe(amberColor);
93
+ // AC-14: .alert-tag with text "!!"
94
+ const tag = canvasElement.querySelector(".alert-tag");
95
+ await expect(tag).not.toBeNull();
96
+ await expect(tag!.textContent!.trim()).toBe("!!");
97
+
98
+ // AC-17: borderLeftColor on .alert child element
99
+ const alertEl = canvasElement.querySelector(".alert");
100
+ await expect(alertEl).not.toBeNull();
101
+ const amberColor = resolveTokenFgColor("--amber");
102
+ await expect(getComputedStyle(alertEl!).borderLeftColor).toBe(amberColor);
85
103
  }}
86
104
  />
87
105
 
88
- <!-- AC 18, 20, 25, 31 error variant: role=alert, aria-live=assertive, icon "err", border-color=--danger -->
106
+ <!-- AC-15: error variant title "Fault", .alert-tag "err", role="alert", borderLeftColor on .alert -->
89
107
  <Story
90
108
  name="Error"
91
109
  args={{
92
110
  id: "toast-3",
111
+ title: "Fault",
93
112
  message: "Thermal protection triggered.",
94
113
  variant: "error",
95
114
  ondismiss: () => {},
@@ -97,30 +116,43 @@
97
116
  play={async ({ canvasElement }) => {
98
117
  const canvas = within(canvasElement);
99
118
 
100
- // AC 18: danger variant renders role="alert"
119
+ // AC-12: error variant keeps role="alert" on Toast wrapper
101
120
  const toast = canvas.getByRole("alert");
102
121
  await expect(toast).toBeVisible();
103
122
 
104
- // AC 26: message text visible
123
+ // AC-11: .alert-title visible
124
+ const titleEl = canvasElement.querySelector(".alert-title");
125
+ await expect(titleEl).not.toBeNull();
126
+ await expect(titleEl!.textContent!.trim()).toBe("Fault");
127
+
128
+ // message text visible
105
129
  await expect(
106
130
  canvas.getByText("Thermal protection triggered."),
107
131
  ).toBeVisible();
108
132
 
109
- // AC 20: danger has aria-live="assertive"
133
+ // AC-12: aria-live="assertive" on Toast wrapper
110
134
  await expect(toast.getAttribute("aria-live")).toBe("assertive");
111
135
 
112
- // AC 25: .toast-icon text is "err"
113
- const icon = canvasElement.querySelector(".toast-icon");
114
- await expect(icon).not.toBeNull();
115
- await expect(icon!.textContent!.trim()).toBe("err");
136
+ // AC-16: dismiss button still present
137
+ const closeBtn = canvas.getByRole("button", {
138
+ name: /Dismiss notification/i,
139
+ });
140
+ await expect(closeBtn).toBeVisible();
141
+
142
+ // AC-15: .alert-tag with text "err"
143
+ const tag = canvasElement.querySelector(".alert-tag");
144
+ await expect(tag).not.toBeNull();
145
+ await expect(tag!.textContent!.trim()).toBe("err");
116
146
 
117
- // AC 31: border-color matches var(--danger)
118
- const dangerColor = resolveTokenColor("--danger");
119
- await expect(getComputedStyle(toast).borderColor).toBe(dangerColor);
147
+ // AC-17: borderLeftColor on .alert child element
148
+ const alertEl = canvasElement.querySelector(".alert");
149
+ await expect(alertEl).not.toBeNull();
150
+ const dangerColor = resolveTokenFgColor("--danger");
151
+ await expect(getComputedStyle(alertEl!).borderLeftColor).toBe(dangerColor);
120
152
  }}
121
153
  />
122
154
 
123
- <!-- AC 27, 28 — clicking the close button calls ondismiss (no-op in story; no throw) -->
155
+ <!-- AC-16: clicking dismiss button calls ondismiss without error -->
124
156
  <Story
125
157
  name="Manual Close"
126
158
  args={{
@@ -132,23 +164,22 @@
132
164
  play={async ({ canvasElement, userEvent }) => {
133
165
  const canvas = within(canvasElement);
134
166
 
135
- // AC 27: close button present
136
167
  const toast = canvas.getByRole("status");
137
168
  await expect(toast).toBeVisible();
138
169
 
170
+ // AC-16: dismiss button rendered by Toast
139
171
  const closeBtn = canvas.getByRole("button", {
140
172
  name: /Dismiss notification/i,
141
173
  });
142
174
  await expect(closeBtn).toBeVisible();
143
175
 
144
- // AC 28: clicking button calls ondismiss — the no-op callback should not throw
176
+ // clicking should not throw
145
177
  await userEvent.click(closeBtn);
146
- // If ondismiss threw, userEvent.click would have rejected — reaching here means success
147
178
  await expect(closeBtn).toBeInTheDocument();
148
179
  }}
149
180
  />
150
181
 
151
- <!-- AC 33 — long message: toast visible and max-width 400px -->
182
+ <!-- max-width constraint still 400px after refactor -->
152
183
  <Story
153
184
  name="Long Message"
154
185
  args={{
@@ -161,23 +192,20 @@
161
192
  play={async ({ canvasElement }) => {
162
193
  const canvas = within(canvasElement);
163
194
 
164
- // AC 17: still role=status for warning
165
195
  const toast = canvas.getByRole("status");
166
196
  await expect(toast).toBeVisible();
167
197
 
168
- // AC 26: full message present in DOM
169
198
  await expect(
170
199
  canvas.getByText(
171
200
  "This is a longer notification message that tests how the toast handles wrapping text within its maximum width constraint.",
172
201
  ),
173
202
  ).toBeVisible();
174
203
 
175
- // AC 33: max-width is 400px
176
204
  await expect(getComputedStyle(toast).maxWidth).toBe("400px");
177
205
  }}
178
206
  />
179
207
 
180
- <!-- AC 19, 21, 22 ARIA attributes on success variant -->
208
+ <!-- AC-12: ARIA attributes (role, aria-live, aria-atomic) stay on Toast wrapper -->
181
209
  <Story
182
210
  name="Aria Attributes"
183
211
  args={{
@@ -189,15 +217,23 @@
189
217
  play={async ({ canvasElement }) => {
190
218
  const canvas = within(canvasElement);
191
219
 
192
- // AC 19: ok/amber has aria-live="polite"
220
+ // AC-12: aria-live="polite" on Toast wrapper (success/warning)
193
221
  const toast = canvas.getByRole("status");
194
222
  await expect(toast.getAttribute("aria-live")).toBe("polite");
195
223
 
196
- // AC 21: aria-atomic="true"
224
+ // AC-12: aria-atomic="true" on Toast wrapper
197
225
  await expect(toast.getAttribute("aria-atomic")).toBe("true");
198
226
 
199
- // AC 22: .toast-icon has aria-hidden="true"
200
- const icon = canvasElement.querySelector(".toast-icon");
201
- await expect(icon!.getAttribute("aria-hidden")).toBe("true");
227
+ // AC-12: role is on Toast wrapper, NOT inside Alert
228
+ // Verify the .alert child element has no role attribute
229
+ const alertEl = canvasElement.querySelector(".alert");
230
+ await expect(alertEl).not.toBeNull();
231
+ await expect(alertEl!.getAttribute("role")).toBeNull();
232
+
233
+ // AC-11: when no title passed, .alert-title has no visible text
234
+ const titleEl = canvasElement.querySelector(".alert-title");
235
+ if (titleEl) {
236
+ await expect(titleEl.textContent?.trim()).toBeFalsy();
237
+ }
202
238
  }}
203
239
  />
@@ -1,13 +1,15 @@
1
1
  <script lang="ts">
2
2
  import type { HTMLAttributes } from "svelte/elements";
3
3
  import type { ToastVariant } from "../../stores/toast.js";
4
- import Button from "../primitives/Button.svelte";
4
+ import Alert from "./Alert.svelte";
5
5
 
6
6
  interface Props extends HTMLAttributes<HTMLDivElement> {
7
7
  /** Unique toast ID (used by the dismiss callback). */
8
8
  id: string;
9
9
  /** Text content of the notification. */
10
10
  message: string;
11
+ /** Optional title text shown above the message. */
12
+ title?: string;
11
13
  /** Colour variant — also sets the ARIA live region type. @default 'success' */
12
14
  variant?: ToastVariant;
13
15
  /** Called with the toast `id` when the dismiss button is clicked. */
@@ -17,22 +19,16 @@
17
19
  let {
18
20
  id,
19
21
  message,
22
+ title = '',
20
23
  variant = "success",
21
24
  ondismiss,
22
25
  ...rest
23
26
  }: Props = $props();
24
27
 
25
- const ICONS: Record<ToastVariant, string> = {
26
- success: "ok",
27
- warning: "!!",
28
- error: "err",
29
- };
30
-
31
28
  let role = $derived(variant === "error" ? "alert" : "status");
32
29
  let ariaLive: "assertive" | "polite" = $derived(
33
30
  variant === "error" ? "assertive" : "polite",
34
31
  );
35
- let icon = $derived(ICONS[variant]);
36
32
  </script>
37
33
 
38
34
  <div
@@ -42,68 +38,13 @@
42
38
  aria-atomic="true"
43
39
  {...rest}
44
40
  >
45
- <span class="toast-icon" aria-hidden="true">{icon}</span>
46
- <span class="toast-message">{message}</span>
47
- <div class="toast-close-wrap">
48
- <Button
49
- variant="ghost"
50
- type="button"
51
- aria-label="Dismiss notification"
52
- onclick={() => ondismiss(id)}>×</Button
53
- >
54
- </div>
41
+ <Alert variant={variant} {title} {message} ondismiss={() => ondismiss(id)} />
55
42
  </div>
56
43
 
57
44
  <style>
58
45
  .toast {
59
- display: flex;
60
- align-items: center;
61
- gap: 10px;
62
- padding: 12px 14px;
63
46
  min-width: 260px;
64
47
  max-width: 400px;
65
- border: 1px solid;
66
- background: var(--bg-elev);
67
- font-size: var(--t-body);
68
- line-height: 1.4;
69
48
  pointer-events: all;
70
49
  }
71
-
72
- .toast--success {
73
- border-color: var(--ok);
74
- }
75
- .toast--warning {
76
- border-color: var(--amber);
77
- }
78
- .toast--error {
79
- border-color: var(--danger);
80
- }
81
-
82
- .toast-icon {
83
- font-family: var(--mono);
84
- font-size: var(--t-micro);
85
- letter-spacing: 0.08em;
86
- text-transform: uppercase;
87
- flex-shrink: 0;
88
- }
89
-
90
- .toast--success .toast-icon {
91
- color: var(--ok);
92
- }
93
- .toast--warning .toast-icon {
94
- color: var(--amber);
95
- }
96
- .toast--error .toast-icon {
97
- color: var(--danger);
98
- }
99
-
100
- .toast-message {
101
- flex: 1;
102
- color: var(--ink-dim);
103
- }
104
-
105
- .toast-close-wrap {
106
- flex-shrink: 0;
107
- margin-left: auto;
108
- }
109
50
  </style>
@@ -5,6 +5,8 @@ interface Props extends HTMLAttributes<HTMLDivElement> {
5
5
  id: string;
6
6
  /** Text content of the notification. */
7
7
  message: string;
8
+ /** Optional title text shown above the message. */
9
+ title?: string;
8
10
  /** Colour variant — also sets the ARIA live region type. @default 'success' */
9
11
  variant?: ToastVariant;
10
12
  /** Called with the toast `id` when the dismiss button is clicked. */
@@ -2,6 +2,7 @@
2
2
  import { defineMeta } from "@storybook/addon-svelte-csf";
3
3
  import { expect, within, waitFor } from "storybook/test";
4
4
  import ToastRegion from "./ToastRegion.svelte";
5
+ import Button from "../primitives/Button.svelte";
5
6
  import { toast } from "../../stores/toast.js";
6
7
 
7
8
  // AC 69: no component: in defineMeta — ToastRegion is a singleton portal (composition-only)
@@ -11,6 +12,16 @@
11
12
  });
12
13
  </script>
13
14
 
15
+ <!-- Interactive demo — trigger buttons with duration:0 so toasts persist -->
16
+ <Story name="Interactive">
17
+ <div style="display: flex; gap: 8px; padding: 1rem;">
18
+ <Button onclick={() => toast.push('Build complete.', { title: 'Success', variant: 'success', duration: 0 })}>Success</Button>
19
+ <Button onclick={() => toast.push('+12V rail at 88%.', { title: 'Warning', variant: 'warning', duration: 0 })}>Warning</Button>
20
+ <Button onclick={() => toast.push('Thermal fault detected.', { title: 'Error', variant: 'error', duration: 0 })}>Error</Button>
21
+ </div>
22
+ <ToastRegion position="bottom-right" />
23
+ </Story>
24
+
14
25
  <!-- AC 39, 40, 66 — push one toast and verify it renders in the region -->
15
26
  <Story name="Single Toast"
16
27
  play={async ({ canvasElement, userEvent }) => {
@@ -55,6 +55,7 @@
55
55
  <Toast
56
56
  id={item.id}
57
57
  message={item.message}
58
+ title={item.title}
58
59
  variant={item.variant}
59
60
  ondismiss={handleDismiss}
60
61
  />
@@ -1,3 +1,4 @@
1
+ export { default as Alert } from './Alert.svelte';
1
2
  export { default as Modal } from './Modal.svelte';
2
3
  export { default as Toast } from './Toast.svelte';
3
4
  export { default as ToastRegion } from './ToastRegion.svelte';
@@ -1,3 +1,4 @@
1
+ export { default as Alert } from './Alert.svelte';
1
2
  export { default as Modal } from './Modal.svelte';
2
3
  export { default as Toast } from './Toast.svelte';
3
4
  export { default as ToastRegion } from './ToastRegion.svelte';
@@ -56,3 +56,18 @@
56
56
  const input = canvas.getByRole("textbox");
57
57
  await expect(input.getAttribute("type")).toBe("email");
58
58
  }} />
59
+
60
+ <Story name="Number Input" args={{ type: "number", value: 42 }}
61
+ play={async ({ canvasElement }) => {
62
+ const canvas = within(canvasElement);
63
+ // AC-3: number input baseline — input is visible and enabled
64
+ const input = canvas.getByRole("spinbutton");
65
+ await expect(input).toBeVisible();
66
+ await expect(input).toBeEnabled();
67
+ // AC-1/AC-2: type is number (spin buttons suppressed via CSS — code-level reviewer check)
68
+ await expect(input.getAttribute("type")).toBe("number");
69
+ // Baseline token checks inherited from Default story
70
+ const bgSunken = resolveTokenColor("--bg-sunken");
71
+ await expect(getComputedStyle(input).backgroundColor).toBe(bgSunken);
72
+ await expect(getComputedStyle(input).fontFamily).toContain("JetBrains Mono");
73
+ }} />
@@ -61,4 +61,15 @@
61
61
  opacity: 0.4;
62
62
  cursor: not-allowed;
63
63
  }
64
+
65
+ /* Hide WebKit/Blink spin buttons */
66
+ .input::-webkit-inner-spin-button,
67
+ .input::-webkit-outer-spin-button {
68
+ display: none;
69
+ }
70
+
71
+ /* Hide Firefox spin buttons */
72
+ .input[type='number'] {
73
+ -moz-appearance: textfield;
74
+ }
64
75
  </style>
@@ -3,6 +3,7 @@
3
3
  import { expect, within } from "storybook/test";
4
4
  import InputWrap from "./InputWrap.svelte";
5
5
  import Input from "./Input.svelte";
6
+ import { resolveTokenColor } from "../../storybook-utils.js";
6
7
 
7
8
  const { Story } = defineMeta({
8
9
  title: "Forms/InputWrap/Composition",
@@ -25,6 +26,16 @@
25
26
  const iconPre = canvasElement.querySelector(".icon-pre");
26
27
  await expect(iconPre).not.toBeNull();
27
28
  await expect(iconPre!.getAttribute("aria-hidden")).toBe("true");
29
+
30
+ // AC-7: SVG fill must resolve to --ink-faint (via fill: currentColor inheriting from .icon-pre)
31
+ const svgEl = canvasElement.querySelector(".icon-pre svg");
32
+ await expect(svgEl).not.toBeNull();
33
+ const probe = document.createElement("div");
34
+ probe.style.cssText = "color:var(--ink-faint);position:absolute;opacity:0";
35
+ document.body.appendChild(probe);
36
+ const inkFaintColor = getComputedStyle(probe).color;
37
+ document.body.removeChild(probe);
38
+ await expect(getComputedStyle(svgEl!).fill).toBe(inkFaintColor);
28
39
  }}>
29
40
  <InputWrap iconPre={mailIcon}>
30
41
  <Input type="email" placeholder="you@domain.com" />
@@ -48,6 +48,15 @@
48
48
  const canvas = within(canvasElement);
49
49
  const clearBtn = canvas.getByRole("button", { name: "Clear" });
50
50
  await expect(clearBtn).toBeVisible();
51
+
52
+ // AC-4: clear button resting color must be --ink-dim
53
+ const restProbe = document.createElement("div");
54
+ restProbe.style.cssText = "color:var(--ink-dim);position:absolute;opacity:0";
55
+ document.body.appendChild(restProbe);
56
+ const inkDimColor = getComputedStyle(restProbe).color;
57
+ document.body.removeChild(restProbe);
58
+ await expect(getComputedStyle(clearBtn).color).toBe(inkDimColor);
59
+
51
60
  }}>
52
61
  <Input type="text" value="DISTRANS-AR1" />
53
62
  </Story>
@@ -1,7 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { HTMLAttributes } from 'svelte/elements'
3
3
  import type { Snippet } from 'svelte'
4
- import Button from '../primitives/Button.svelte'
5
4
 
6
5
  interface Props extends HTMLAttributes<HTMLDivElement> {
7
6
  /** Snippet rendered as an icon before the input (aria-hidden). */
@@ -48,14 +47,13 @@
48
47
  <span class="addon addon-suf">{addonSuf}</span>
49
48
  {/if}
50
49
  {#if clearable}
51
- <Button
52
- variant="ghost"
50
+ <button
53
51
  type="button"
54
52
  class={showClear ? 'wrap-clear visible' : 'wrap-clear'}
55
53
  aria-label="Clear"
56
54
  tabindex={showClear ? 0 : -1}
57
55
  onclick={onclear}
58
- >×</Button>
56
+ >×</button>
59
57
  {/if}
60
58
  </div>
61
59
 
@@ -78,6 +76,10 @@
78
76
  color: var(--ink-faint);
79
77
  }
80
78
 
79
+ .icon-pre :global(svg) {
80
+ fill: currentColor;
81
+ }
82
+
81
83
  .addon {
82
84
  display: flex;
83
85
  align-items: center;
@@ -99,7 +101,7 @@
99
101
  border-left: none;
100
102
  }
101
103
 
102
- :global(.wrap-clear) {
104
+ .wrap-clear {
103
105
  position: absolute;
104
106
  right: 8px;
105
107
  top: 50%;
@@ -107,7 +109,6 @@
107
109
  font-family: var(--mono);
108
110
  font-size: 14px;
109
111
  line-height: 1;
110
- color: var(--ink-faint);
111
112
  cursor: pointer;
112
113
  background: none;
113
114
  border: none;
@@ -115,14 +116,15 @@
115
116
  transition: color var(--transition);
116
117
  opacity: 0;
117
118
  pointer-events: none;
119
+ color: var(--ink-dim);
118
120
  }
119
121
 
120
- :global(.wrap-clear.visible) {
121
- opacity: 1;
122
- pointer-events: auto;
122
+ .wrap-clear:hover {
123
+ color: var(--amber);
123
124
  }
124
125
 
125
- :global(.wrap-clear:hover) {
126
- color: var(--ink);
126
+ .wrap-clear.visible {
127
+ opacity: 1;
128
+ pointer-events: auto;
127
129
  }
128
130
  </style>