@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.
- package/README.md +1 -1
- package/dist/components/cards/NoteCard.svelte +1 -1
- package/dist/components/cards/ProductCard.svelte +1 -1
- package/dist/components/cards/ProjectCard.svelte +1 -1
- package/dist/components/feedback/Alert.stories.svelte +131 -0
- package/dist/components/feedback/Alert.svelte +117 -0
- package/dist/components/{patterns → feedback}/Alert.svelte.d.ts +4 -2
- package/dist/components/feedback/Modal.stories.svelte +49 -117
- package/dist/components/feedback/Modal.svelte +47 -26
- package/dist/components/feedback/Toast.stories.svelte +89 -53
- package/dist/components/feedback/Toast.svelte +5 -64
- package/dist/components/feedback/Toast.svelte.d.ts +2 -0
- package/dist/components/feedback/ToastRegion.stories.svelte +11 -0
- package/dist/components/feedback/ToastRegion.svelte +1 -0
- package/dist/components/feedback/index.d.ts +1 -0
- package/dist/components/feedback/index.js +1 -0
- package/dist/components/forms/Input.stories.svelte +15 -0
- package/dist/components/forms/Input.svelte +11 -0
- package/dist/components/forms/InputWrap.composition.stories.svelte +11 -0
- package/dist/components/forms/InputWrap.stories.svelte +9 -0
- package/dist/components/forms/InputWrap.svelte +13 -11
- package/dist/components/forms/Switch.stories.svelte +55 -47
- package/dist/components/forms/Switch.svelte +4 -3
- package/dist/components/patterns/CtaBlock.svelte +1 -0
- package/dist/components/patterns/PageHero.stories.svelte +39 -0
- package/dist/components/patterns/PageHero.svelte +17 -3
- package/dist/components/patterns/PageHero.svelte.d.ts +5 -1
- package/dist/components/patterns/index.d.ts +0 -1
- package/dist/components/patterns/index.js +0 -1
- package/dist/components/primitives/Heading.stories.svelte +6 -30
- package/dist/components/primitives/Heading.svelte +2 -11
- package/dist/components/primitives/Heading.svelte.d.ts +0 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/stores/toast.d.ts +2 -0
- package/dist/stores/toast.js +1 -0
- package/dist/tokens/tokens.css +2 -0
- package/package.json +1 -1
- package/dist/components/patterns/Alert.stories.svelte +0 -63
- package/dist/components/patterns/Alert.svelte +0 -91
- /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 {
|
|
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
|
|
14
|
+
<!-- AC-13: success variant — title "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
|
|
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
|
|
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
|
|
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
|
|
43
|
-
const
|
|
44
|
-
await expect(
|
|
45
|
-
await expect(
|
|
46
|
-
await expect(
|
|
47
|
-
|
|
48
|
-
// AC
|
|
49
|
-
const
|
|
50
|
-
await expect(
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
62
|
+
<!-- AC-14: warning variant — title "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
|
|
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
|
|
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
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
83
|
-
const
|
|
84
|
-
await expect(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
133
|
+
// AC-12: aria-live="assertive" on Toast wrapper
|
|
110
134
|
await expect(toast.getAttribute("aria-live")).toBe("assertive");
|
|
111
135
|
|
|
112
|
-
// AC
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
118
|
-
const
|
|
119
|
-
await expect(
|
|
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
|
|
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
|
-
//
|
|
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
|
-
<!--
|
|
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
|
|
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
|
|
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
|
|
224
|
+
// AC-12: aria-atomic="true" on Toast wrapper
|
|
197
225
|
await expect(toast.getAttribute("aria-atomic")).toBe("true");
|
|
198
226
|
|
|
199
|
-
// AC
|
|
200
|
-
|
|
201
|
-
|
|
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
|
|
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
|
-
<
|
|
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 }) => {
|
|
@@ -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
|
-
<
|
|
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
|
-
>×</
|
|
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
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
pointer-events: auto;
|
|
122
|
+
.wrap-clear:hover {
|
|
123
|
+
color: var(--amber);
|
|
123
124
|
}
|
|
124
125
|
|
|
125
|
-
|
|
126
|
-
|
|
126
|
+
.wrap-clear.visible {
|
|
127
|
+
opacity: 1;
|
|
128
|
+
pointer-events: auto;
|
|
127
129
|
}
|
|
128
130
|
</style>
|