@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
|
@@ -11,9 +11,11 @@
|
|
|
11
11
|
});
|
|
12
12
|
</script>
|
|
13
13
|
|
|
14
|
-
<!-- AC-44, AC-45, AC-46, AC-47: Off state -->
|
|
15
|
-
<Story
|
|
16
|
-
|
|
14
|
+
<!-- AC-44, AC-45, AC-46, AC-47, AC-50: Off state + Space key toggle -->
|
|
15
|
+
<Story
|
|
16
|
+
name="Off (Default)"
|
|
17
|
+
args={{ label: "Dark mode", checked: false }}
|
|
18
|
+
play={async ({ canvasElement, userEvent }) => {
|
|
17
19
|
const canvas = within(canvasElement);
|
|
18
20
|
// AC-44: renders a button with role="switch"
|
|
19
21
|
// AC-46: accessible name provided by aria-label={label}
|
|
@@ -24,11 +26,32 @@
|
|
|
24
26
|
// AC-47: track background matches var(--bg-sunken) when off
|
|
25
27
|
const bgSunken = resolveTokenColor("--bg-sunken");
|
|
26
28
|
await expect(getComputedStyle(sw).backgroundColor).toBe(bgSunken);
|
|
27
|
-
|
|
29
|
+
// AC-1: off-state border resolves to --rule
|
|
30
|
+
const ruleColor = resolveTokenColor("--rule");
|
|
31
|
+
await expect(getComputedStyle(sw).borderColor).toBe(ruleColor);
|
|
32
|
+
// AC-2: border is NOT invisible (not the same as --bg)
|
|
33
|
+
const bgColor = resolveTokenColor("--bg");
|
|
34
|
+
await expect(getComputedStyle(sw).borderColor).not.toBe(bgColor);
|
|
35
|
+
// AC-5: clicking the label text toggles the switch on
|
|
36
|
+
await userEvent.click(canvas.getByText("Dark mode"));
|
|
37
|
+
await expect(sw.getAttribute("aria-checked")).toBe("true");
|
|
38
|
+
// AC-8: clicking the button itself flips it back to off
|
|
39
|
+
await userEvent.click(sw);
|
|
40
|
+
await expect(sw.getAttribute("aria-checked")).toBe("false");
|
|
41
|
+
// AC-50: Space key toggles switch from off to on
|
|
42
|
+
sw.focus();
|
|
43
|
+
await expect(sw).toHaveFocus();
|
|
44
|
+
await userEvent.keyboard(" ");
|
|
45
|
+
await expect(sw.getAttribute("aria-checked")).toBe("true");
|
|
46
|
+
await userEvent.keyboard(" ");
|
|
47
|
+
}}
|
|
48
|
+
/>
|
|
28
49
|
|
|
29
|
-
<!-- AC-45, AC-48: On state -->
|
|
30
|
-
<Story
|
|
31
|
-
|
|
50
|
+
<!-- AC-45, AC-48, AC-52: On state + amber track + pill shape dimensions -->
|
|
51
|
+
<Story
|
|
52
|
+
name="On"
|
|
53
|
+
args={{ label: "Dark mode", checked: true }}
|
|
54
|
+
play={async ({ canvasElement, userEvent }) => {
|
|
32
55
|
const canvas = within(canvasElement);
|
|
33
56
|
const sw = canvas.getByRole("switch", { name: "Dark mode" });
|
|
34
57
|
// AC-45: aria-checked is "true" when on
|
|
@@ -36,51 +59,36 @@
|
|
|
36
59
|
// AC-48: track background matches var(--amber) when on
|
|
37
60
|
const amberColor = resolveTokenColor("--amber");
|
|
38
61
|
await expect(getComputedStyle(sw).backgroundColor).toBe(amberColor);
|
|
39
|
-
|
|
62
|
+
// AC-3: on-state border resolves to --amber
|
|
63
|
+
await expect(getComputedStyle(sw).borderColor).toBe(amberColor);
|
|
64
|
+
// AC-52: pill shape — borderRadius is 11px, width 40px, height 22px
|
|
65
|
+
await expect(getComputedStyle(sw).borderRadius).toBe("11px");
|
|
66
|
+
await expect(getComputedStyle(sw).width).toBe("40px");
|
|
67
|
+
await expect(getComputedStyle(sw).height).toBe("22px");
|
|
68
|
+
// AC-6: clicking the label text toggles the switch off
|
|
69
|
+
await userEvent.click(canvas.getByText("Dark mode"));
|
|
70
|
+
await expect(sw.getAttribute("aria-checked")).toBe("false");
|
|
71
|
+
await userEvent.click(canvas.getByText("Dark mode"));
|
|
72
|
+
}}
|
|
73
|
+
/>
|
|
40
74
|
|
|
41
|
-
<!-- AC-49: disabled
|
|
42
|
-
<Story
|
|
43
|
-
|
|
75
|
+
<!-- AC-49: disabled — toBeDisabled, wrap opacity 0.4, no-toggle, and disabled-on aria-checked -->
|
|
76
|
+
<Story
|
|
77
|
+
name="Disabled"
|
|
78
|
+
args={{ label: "Feature flag", disabled: true, checked: false }}
|
|
79
|
+
play={async ({ canvasElement, userEvent }) => {
|
|
44
80
|
const canvas = within(canvasElement);
|
|
45
81
|
const sw = canvas.getByRole("switch", { name: "Feature flag" });
|
|
46
82
|
await expect(sw).toBeDisabled();
|
|
47
83
|
// AC-49: wrap opacity is 0.4
|
|
48
84
|
const wrap = canvasElement.querySelector(".switch-wrap");
|
|
49
85
|
await expect(getComputedStyle(wrap!).opacity).toBe("0.4");
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const canvas = within(canvasElement);
|
|
56
|
-
const sw = canvas.getByRole("switch", { name: "Feature flag" });
|
|
57
|
-
await expect(sw).toBeDisabled();
|
|
58
|
-
await expect(sw.getAttribute("aria-checked")).toBe("true");
|
|
59
|
-
}} />
|
|
60
|
-
|
|
61
|
-
<!-- AC-50: Space key toggles switch from off to on -->
|
|
62
|
-
<Story name="Space to Toggle" args={{ label: "Toggle", checked: false }}
|
|
63
|
-
play={async ({ canvasElement, userEvent }) => {
|
|
64
|
-
const canvas = within(canvasElement);
|
|
65
|
-
const sw = canvas.getByRole("switch", { name: "Toggle" });
|
|
86
|
+
// AC-4: disabled off-state border still resolves to --rule
|
|
87
|
+
const ruleColor = resolveTokenColor("--rule");
|
|
88
|
+
await expect(getComputedStyle(sw).borderColor).toBe(ruleColor);
|
|
89
|
+
// AC-7: clicking the label on a disabled switch must NOT toggle it
|
|
90
|
+
await userEvent.click(canvas.getByText("Feature flag"));
|
|
66
91
|
await expect(sw.getAttribute("aria-checked")).toBe("false");
|
|
67
|
-
await userEvent.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
// AC-50: after Space on an off switch, aria-checked becomes "true"
|
|
71
|
-
await expect(sw.getAttribute("aria-checked")).toBe("true");
|
|
72
|
-
}} />
|
|
73
|
-
|
|
74
|
-
<!-- AC-48, AC-52: amber track when on + pill shape dimensions -->
|
|
75
|
-
<Story name="Amber Track When On" args={{ label: "Power", checked: true }}
|
|
76
|
-
play={async ({ canvasElement }) => {
|
|
77
|
-
const canvas = within(canvasElement);
|
|
78
|
-
const sw = canvas.getByRole("switch", { name: "Power" });
|
|
79
|
-
// AC-48: track background matches var(--amber)
|
|
80
|
-
const amberColor = resolveTokenColor("--amber");
|
|
81
|
-
await expect(getComputedStyle(sw).backgroundColor).toBe(amberColor);
|
|
82
|
-
// AC-52: pill shape — borderRadius is 11px, width 40px, height 22px
|
|
83
|
-
await expect(getComputedStyle(sw).borderRadius).toBe("11px");
|
|
84
|
-
await expect(getComputedStyle(sw).width).toBe("40px");
|
|
85
|
-
await expect(getComputedStyle(sw).height).toBe("22px");
|
|
86
|
-
}} />
|
|
92
|
+
await userEvent.click(canvas.getByText("Feature flag"));
|
|
93
|
+
}}
|
|
94
|
+
/>
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
>
|
|
47
47
|
<span class="switch-knob" aria-hidden="true"></span>
|
|
48
48
|
</button>
|
|
49
|
-
<span class="switch-label">{label}</span>
|
|
49
|
+
<span class="switch-label" onclick={() => { if (!disabled) checked = !checked }}>{label}</span>
|
|
50
50
|
</span>
|
|
51
51
|
|
|
52
52
|
<style>
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
width: 40px;
|
|
71
71
|
height: 22px;
|
|
72
72
|
border-radius: 11px;
|
|
73
|
-
border: 1px solid var(--
|
|
73
|
+
border: 1px solid var(--rule);
|
|
74
74
|
background: var(--bg-sunken);
|
|
75
75
|
cursor: pointer;
|
|
76
76
|
padding: 0;
|
|
@@ -98,12 +98,13 @@
|
|
|
98
98
|
width: 16px;
|
|
99
99
|
height: 16px;
|
|
100
100
|
border-radius: 50%;
|
|
101
|
-
background: var(--
|
|
101
|
+
background: var(--ink-faint);
|
|
102
102
|
transform: translateX(2px);
|
|
103
103
|
transition: transform var(--transition);
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
.switch.on .switch-knob {
|
|
107
|
+
background: var(--bg);
|
|
107
108
|
transform: translateX(20px);
|
|
108
109
|
}
|
|
109
110
|
|
|
@@ -60,3 +60,42 @@
|
|
|
60
60
|
<Button variant="primary">View Catalogue</Button>
|
|
61
61
|
<Button variant="ghost">View Projects →</Button>
|
|
62
62
|
</Story>
|
|
63
|
+
|
|
64
|
+
<!-- B36 AC-1, AC-2 branch B, AC-3, AC-4, AC-8: snippet-based heading with mixed ink colors -->
|
|
65
|
+
<Story name="SnippetHeading"
|
|
66
|
+
play={async ({ canvasElement }) => {
|
|
67
|
+
// AC-3a: h1 is present and visible
|
|
68
|
+
const h1 = canvasElement.querySelector("h1");
|
|
69
|
+
await expect(h1).not.toBeNull();
|
|
70
|
+
await expect(h1).toBeVisible();
|
|
71
|
+
|
|
72
|
+
// AC-3b: h1 contains an <em> child
|
|
73
|
+
const em = h1!.querySelector("em");
|
|
74
|
+
await expect(em).not.toBeNull();
|
|
75
|
+
|
|
76
|
+
// AC-3d: computed fontStyle of <em> is "normal" (proves :global rule fired)
|
|
77
|
+
await expect(getComputedStyle(em!).fontStyle).toBe("normal");
|
|
78
|
+
}}>
|
|
79
|
+
{#snippet template(args)}
|
|
80
|
+
{#snippet headingContent()}
|
|
81
|
+
Dexter.<br /><em>Things built</em><br />in the lab.
|
|
82
|
+
{/snippet}
|
|
83
|
+
<PageHero eyebrow="// DEXTERLABS" headingContent={headingContent} />
|
|
84
|
+
{/snippet}
|
|
85
|
+
</Story>
|
|
86
|
+
|
|
87
|
+
<!-- B36 AC-5, AC-6: border prop — suppresses bottom rule -->
|
|
88
|
+
<Story name="NoBorder" args={{ eyebrow: "// DEXTERLABS", heading: "Homepage", border: false }}
|
|
89
|
+
play={async ({ canvasElement }) => {
|
|
90
|
+
const canvas = within(canvasElement);
|
|
91
|
+
|
|
92
|
+
// AC-6a: heading is visible
|
|
93
|
+
await expect(canvas.getByRole("heading", { level: 1, name: "Homepage" })).toBeVisible();
|
|
94
|
+
|
|
95
|
+
// AC-6b: <header> element is present
|
|
96
|
+
const header = canvasElement.querySelector("header");
|
|
97
|
+
await expect(header).not.toBeNull();
|
|
98
|
+
|
|
99
|
+
// AC-6c: border-bottom is suppressed
|
|
100
|
+
await expect(getComputedStyle(header!).borderBottomStyle).toBe("none");
|
|
101
|
+
}} />
|
|
@@ -8,9 +8,13 @@
|
|
|
8
8
|
/** Small mono label shown above the heading. */
|
|
9
9
|
eyebrow?: string
|
|
10
10
|
/** Hero heading text. */
|
|
11
|
-
heading
|
|
11
|
+
heading?: string
|
|
12
|
+
/** Snippet-based hero heading — takes precedence over `heading` string when provided. */
|
|
13
|
+
headingContent?: Snippet
|
|
12
14
|
/** Subtitle / lede text shown below the heading. */
|
|
13
15
|
lede?: string
|
|
16
|
+
/** Show bottom border rule. @default true */
|
|
17
|
+
border?: boolean
|
|
14
18
|
children?: Snippet
|
|
15
19
|
[key: string]: unknown
|
|
16
20
|
}
|
|
@@ -18,17 +22,19 @@
|
|
|
18
22
|
let {
|
|
19
23
|
eyebrow,
|
|
20
24
|
heading,
|
|
25
|
+
headingContent,
|
|
21
26
|
lede,
|
|
27
|
+
border = true,
|
|
22
28
|
children,
|
|
23
29
|
...rest
|
|
24
30
|
}: Props = $props()
|
|
25
31
|
</script>
|
|
26
32
|
|
|
27
|
-
<header class="page-hero" {...rest}>
|
|
33
|
+
<header class="page-hero" class:page-hero--bordered={border} {...rest}>
|
|
28
34
|
{#if eyebrow}
|
|
29
35
|
<div class="page-hero-eyebrow"><Text variant="eyebrow">{eyebrow}</Text></div>
|
|
30
36
|
{/if}
|
|
31
|
-
<Heading level={1} variant="hero">{heading}</Heading>
|
|
37
|
+
<Heading level={1} variant="hero">{#if headingContent}{@render headingContent()}{:else}{heading}{/if}</Heading>
|
|
32
38
|
{#if lede}
|
|
33
39
|
<div class="page-hero-lede"><Text variant="lede">{lede}</Text></div>
|
|
34
40
|
{/if}
|
|
@@ -44,6 +50,9 @@
|
|
|
44
50
|
<style>
|
|
45
51
|
.page-hero {
|
|
46
52
|
padding: 48px 0 40px;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.page-hero--bordered {
|
|
47
56
|
border-bottom: 1px solid var(--rule);
|
|
48
57
|
}
|
|
49
58
|
|
|
@@ -59,4 +68,9 @@
|
|
|
59
68
|
.page-hero-actions {
|
|
60
69
|
margin-top: 24px;
|
|
61
70
|
}
|
|
71
|
+
|
|
72
|
+
.page-hero :global(.hero-heading em) {
|
|
73
|
+
font-style: normal;
|
|
74
|
+
color: var(--ink-faint);
|
|
75
|
+
}
|
|
62
76
|
</style>
|
|
@@ -3,9 +3,13 @@ interface Props {
|
|
|
3
3
|
/** Small mono label shown above the heading. */
|
|
4
4
|
eyebrow?: string;
|
|
5
5
|
/** Hero heading text. */
|
|
6
|
-
heading
|
|
6
|
+
heading?: string;
|
|
7
|
+
/** Snippet-based hero heading — takes precedence over `heading` string when provided. */
|
|
8
|
+
headingContent?: Snippet;
|
|
7
9
|
/** Subtitle / lede text shown below the heading. */
|
|
8
10
|
lede?: string;
|
|
11
|
+
/** Show bottom border rule. @default true */
|
|
12
|
+
border?: boolean;
|
|
9
13
|
children?: Snippet;
|
|
10
14
|
[key: string]: unknown;
|
|
11
15
|
}
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
|
|
13
13
|
<!-- AC-17, AC-18, AC-19, AC-21, AC-22, AC-23: levels 1/2/3 render h1/h2/h3 with default variant classes -->
|
|
14
14
|
<!-- AC-20: default level is 2 -->
|
|
15
|
+
<!-- AC-12: H1 font-size === 72px (var(--t-h1)) -->
|
|
16
|
+
<!-- AC-10: H2 font-size === 36px (var(--t-h2)) -->
|
|
17
|
+
<!-- AC-11: H3 font-size === 24px (var(--t-h3)) -->
|
|
15
18
|
<Story name="Levels"
|
|
16
19
|
play={async ({ canvasElement }) => {
|
|
17
20
|
const canvas = within(canvasElement);
|
|
@@ -19,14 +22,17 @@
|
|
|
19
22
|
const h1 = canvas.getByTestId("level-1");
|
|
20
23
|
await expect(h1.tagName).toBe("H1");
|
|
21
24
|
await expect(h1.classList.contains("h1")).toBe(true);
|
|
25
|
+
await expect(getComputedStyle(h1).fontSize).toBe("72px");
|
|
22
26
|
|
|
23
27
|
const h2 = canvas.getByTestId("level-2");
|
|
24
28
|
await expect(h2.tagName).toBe("H2");
|
|
25
29
|
await expect(h2.classList.contains("h2")).toBe(true);
|
|
30
|
+
await expect(getComputedStyle(h2).fontSize).toBe("36px");
|
|
26
31
|
|
|
27
32
|
const h3 = canvas.getByTestId("level-3");
|
|
28
33
|
await expect(h3.tagName).toBe("H3");
|
|
29
34
|
await expect(h3.classList.contains("h3")).toBe(true);
|
|
35
|
+
await expect(getComputedStyle(h3).fontSize).toBe("24px");
|
|
30
36
|
|
|
31
37
|
const defaultLevel = canvas.getByTestId("level-default");
|
|
32
38
|
await expect(defaultLevel.tagName).toBe("H2");
|
|
@@ -105,33 +111,3 @@
|
|
|
105
111
|
Section Title
|
|
106
112
|
</Story>
|
|
107
113
|
|
|
108
|
-
<!-- B26 AC-12, AC-43: size="xs" on h3 → 12px -->
|
|
109
|
-
<Story name="SizeXs" args={{ level: 3, size: "xs" }}
|
|
110
|
-
play={async ({ canvasElement }) => {
|
|
111
|
-
const el = canvasElement.firstElementChild!;
|
|
112
|
-
await expect(el.getAttribute("data-size")).toBe("xs");
|
|
113
|
-
await expect(getComputedStyle(el).fontSize).toBe("12px");
|
|
114
|
-
}}>
|
|
115
|
-
Micro heading
|
|
116
|
-
</Story>
|
|
117
|
-
|
|
118
|
-
<!-- B26 AC-13, AC-15, AC-43: size="lg" on h3 → 19px, letterSpacing stays at h3 default -0.01em -->
|
|
119
|
-
<Story name="SizeLg" args={{ level: 3, size: "lg" }}
|
|
120
|
-
play={async ({ canvasElement }) => {
|
|
121
|
-
const el = canvasElement.firstElementChild!;
|
|
122
|
-
await expect(el.getAttribute("data-size")).toBe("lg");
|
|
123
|
-
await expect(getComputedStyle(el).fontSize).toBe("19px");
|
|
124
|
-
await expect(getComputedStyle(el).letterSpacing).toBe("-0.19px");
|
|
125
|
-
}}>
|
|
126
|
-
Lede-size heading
|
|
127
|
-
</Story>
|
|
128
|
-
|
|
129
|
-
<!-- B26 AC-14: size="xl" on h3 → 24px -->
|
|
130
|
-
<Story name="SizeXl" args={{ level: 3, size: "xl" }}
|
|
131
|
-
play={async ({ canvasElement }) => {
|
|
132
|
-
const el = canvasElement.firstElementChild!;
|
|
133
|
-
await expect(el.getAttribute("data-size")).toBe("xl");
|
|
134
|
-
await expect(getComputedStyle(el).fontSize).toBe("24px");
|
|
135
|
-
}}>
|
|
136
|
-
XL heading
|
|
137
|
-
</Story>
|
|
@@ -5,8 +5,6 @@
|
|
|
5
5
|
type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6
|
|
6
6
|
type HeadingVariant = 'display' | 'hero' | 'h1' | 'h2' | 'h3'
|
|
7
7
|
type TextColor = 'ink' | 'dim' | 'faint' | 'amber' | 'cyan' | 'ok' | 'danger'
|
|
8
|
-
type SizeVariant = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
|
9
|
-
|
|
10
8
|
interface Props {
|
|
11
9
|
/** HTML heading level (h1–h6). @default 2 */
|
|
12
10
|
level?: HeadingLevel
|
|
@@ -14,8 +12,6 @@
|
|
|
14
12
|
variant?: HeadingVariant
|
|
15
13
|
/** Text colour mapped to a design token. */
|
|
16
14
|
color?: TextColor
|
|
17
|
-
/** Font size override mapped to a type-scale token. */
|
|
18
|
-
size?: SizeVariant
|
|
19
15
|
children?: Snippet
|
|
20
16
|
class?: ClassValue | null
|
|
21
17
|
style?: string | null
|
|
@@ -46,7 +42,7 @@
|
|
|
46
42
|
danger: 'danger',
|
|
47
43
|
}
|
|
48
44
|
|
|
49
|
-
let { level = 2, variant, color,
|
|
45
|
+
let { level = 2, variant, color, children, class: klass = '', style, ...rest }: Props = $props()
|
|
50
46
|
|
|
51
47
|
const resolvedVariant = $derived(variant ?? DEFAULT_VARIANT[level])
|
|
52
48
|
const variantClass = $derived(resolvedVariant ? VARIANT_CLASS[resolvedVariant] : undefined)
|
|
@@ -54,7 +50,7 @@
|
|
|
54
50
|
const mergedStyle = $derived([colorStyle, style].filter(Boolean).join(' ') || undefined)
|
|
55
51
|
</script>
|
|
56
52
|
|
|
57
|
-
<svelte:element this={"h" + level} class={[variantClass, klass]} style={mergedStyle}
|
|
53
|
+
<svelte:element this={"h" + level} class={[variantClass, klass]} style={mergedStyle} {...rest}>
|
|
58
54
|
{@render children?.()}
|
|
59
55
|
</svelte:element>
|
|
60
56
|
|
|
@@ -99,9 +95,4 @@
|
|
|
99
95
|
line-height: 0.9;
|
|
100
96
|
}
|
|
101
97
|
|
|
102
|
-
[data-size="xs"] { font-size: var(--t-micro) }
|
|
103
|
-
[data-size="sm"] { font-size: var(--t-mono) }
|
|
104
|
-
[data-size="md"] { font-size: var(--t-body) }
|
|
105
|
-
[data-size="lg"] { font-size: var(--t-lede) }
|
|
106
|
-
[data-size="xl"] { font-size: var(--t-h3) }
|
|
107
98
|
</style>
|
|
@@ -3,7 +3,6 @@ import type { Snippet } from 'svelte';
|
|
|
3
3
|
type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
|
|
4
4
|
type HeadingVariant = 'display' | 'hero' | 'h1' | 'h2' | 'h3';
|
|
5
5
|
type TextColor = 'ink' | 'dim' | 'faint' | 'amber' | 'cyan' | 'ok' | 'danger';
|
|
6
|
-
type SizeVariant = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
7
6
|
interface Props {
|
|
8
7
|
/** HTML heading level (h1–h6). @default 2 */
|
|
9
8
|
level?: HeadingLevel;
|
|
@@ -11,8 +10,6 @@ interface Props {
|
|
|
11
10
|
variant?: HeadingVariant;
|
|
12
11
|
/** Text colour mapped to a design token. */
|
|
13
12
|
color?: TextColor;
|
|
14
|
-
/** Font size override mapped to a type-scale token. */
|
|
15
|
-
size?: SizeVariant;
|
|
16
13
|
children?: Snippet;
|
|
17
14
|
class?: ClassValue | null;
|
|
18
15
|
style?: string | null;
|
package/dist/index.d.ts
CHANGED
|
@@ -3,8 +3,8 @@ export { Stack, Inline, Spread, Grid, Container, Rule, Prose } from './component
|
|
|
3
3
|
export { Card, ProductCard, ProjectCard, NoteCard } from './components/cards/index.js';
|
|
4
4
|
export { Nav, Breadcrumb } from './components/navigation/index.js';
|
|
5
5
|
export { Input, Textarea, Select, InputWrap, Field, Checkbox, Radio, RadioGroup, Switch } from './components/forms/index.js';
|
|
6
|
-
export { Modal, Toast, ToastRegion } from './components/feedback/index.js';
|
|
6
|
+
export { Alert, Modal, Toast, ToastRegion } from './components/feedback/index.js';
|
|
7
7
|
export { toast } from './stores/toast.js';
|
|
8
8
|
export type { ToastItem, ToastVariant, ToastOptions } from './stores/toast.js';
|
|
9
|
-
export {
|
|
9
|
+
export { CtaBlock, StatCard, KvList, ProgressBar, ActivityRow, SectionHead, SectionFoot, PageHero } from './components/patterns/index.js';
|
|
10
10
|
export { Accordion, AccordionItem, Tabs, Table } from './components/data/index.js';
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ export { Stack, Inline, Spread, Grid, Container, Rule, Prose } from './component
|
|
|
4
4
|
export { Card, ProductCard, ProjectCard, NoteCard } from './components/cards/index.js';
|
|
5
5
|
export { Nav, Breadcrumb } from './components/navigation/index.js';
|
|
6
6
|
export { Input, Textarea, Select, InputWrap, Field, Checkbox, Radio, RadioGroup, Switch } from './components/forms/index.js';
|
|
7
|
-
export { Modal, Toast, ToastRegion } from './components/feedback/index.js';
|
|
7
|
+
export { Alert, Modal, Toast, ToastRegion } from './components/feedback/index.js';
|
|
8
8
|
export { toast } from './stores/toast.js';
|
|
9
|
-
export {
|
|
9
|
+
export { CtaBlock, StatCard, KvList, ProgressBar, ActivityRow, SectionHead, SectionFoot, PageHero } from './components/patterns/index.js';
|
|
10
10
|
export { Accordion, AccordionItem, Tabs, Table } from './components/data/index.js';
|
package/dist/stores/toast.d.ts
CHANGED
|
@@ -2,12 +2,14 @@ export type ToastVariant = "success" | "warning" | "error";
|
|
|
2
2
|
export interface ToastItem {
|
|
3
3
|
id: string;
|
|
4
4
|
message: string;
|
|
5
|
+
title: string;
|
|
5
6
|
variant: ToastVariant;
|
|
6
7
|
duration: number;
|
|
7
8
|
}
|
|
8
9
|
export interface ToastOptions {
|
|
9
10
|
variant?: ToastVariant;
|
|
10
11
|
duration?: number;
|
|
12
|
+
title?: string;
|
|
11
13
|
}
|
|
12
14
|
declare function push(message: string, options?: ToastOptions): string;
|
|
13
15
|
declare function dismiss(id: string): void;
|
package/dist/stores/toast.js
CHANGED
package/dist/tokens/tokens.css
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
--bg-rail: #0f1211; /* cards, nav, sidebars */
|
|
13
13
|
--bg-elev: #141817; /* elevated surfaces, inline-code bg */
|
|
14
14
|
--bg-sunken: #070908; /* code blocks, status bar, sunken wells */
|
|
15
|
+
--overlay: rgba(7, 9, 8, 0.85);
|
|
15
16
|
|
|
16
17
|
/* Ink */
|
|
17
18
|
--ink: #d6e2dc; /* primary text */
|
|
@@ -93,4 +94,5 @@
|
|
|
93
94
|
--ok: #356b31;
|
|
94
95
|
--shiki-token-string: #2d7a50;
|
|
95
96
|
--shiki-token-string-expression: #2d7a50;
|
|
97
|
+
--overlay: rgba(7, 9, 8, 0.85);
|
|
96
98
|
}
|
package/package.json
CHANGED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
<script module lang="ts">
|
|
2
|
-
import { defineMeta } from "@storybook/addon-svelte-csf";
|
|
3
|
-
import { expect, within } from "storybook/test";
|
|
4
|
-
import Alert from "./Alert.svelte";
|
|
5
|
-
import { resolveTokenFgColor } from "../../storybook-utils.js";
|
|
6
|
-
|
|
7
|
-
const { Story } = defineMeta({
|
|
8
|
-
title: "Patterns/Alert",
|
|
9
|
-
component: Alert,
|
|
10
|
-
tags: ["autodocs"],
|
|
11
|
-
});
|
|
12
|
-
</script>
|
|
13
|
-
|
|
14
|
-
<Story name="Ok" args={{ variant: "ok", title: "Build successful", message: "All rails nominal." }}
|
|
15
|
-
play={async ({ canvasElement }) => {
|
|
16
|
-
const canvas = within(canvasElement);
|
|
17
|
-
const root = canvas.getByRole("status");
|
|
18
|
-
await expect(root).toBeVisible();
|
|
19
|
-
await expect(canvas.getByText("Build successful")).toBeVisible();
|
|
20
|
-
await expect(canvas.getByText("All rails nominal.")).toBeVisible();
|
|
21
|
-
const tag = canvas.getByText("ok");
|
|
22
|
-
await expect(tag).toHaveAttribute("aria-hidden", "true");
|
|
23
|
-
const okColor = resolveTokenFgColor("--ok");
|
|
24
|
-
await expect(getComputedStyle(root).borderLeftColor).toBe(okColor);
|
|
25
|
-
}} />
|
|
26
|
-
|
|
27
|
-
<Story name="Amber" args={{ variant: "amber", title: "High load", message: "+12V rail at 88%." }}
|
|
28
|
-
play={async ({ canvasElement }) => {
|
|
29
|
-
const canvas = within(canvasElement);
|
|
30
|
-
const root = canvas.getByRole("status");
|
|
31
|
-
await expect(root).toBeVisible();
|
|
32
|
-
await expect(canvas.getByText("High load")).toBeVisible();
|
|
33
|
-
const amberColor = resolveTokenFgColor("--amber");
|
|
34
|
-
await expect(getComputedStyle(root).borderLeftColor).toBe(amberColor);
|
|
35
|
-
}} />
|
|
36
|
-
|
|
37
|
-
<Story name="Danger" args={{ variant: "danger", title: "Thermal fault", message: "Over-temperature protection triggered." }}
|
|
38
|
-
play={async ({ canvasElement }) => {
|
|
39
|
-
const canvas = within(canvasElement);
|
|
40
|
-
const root = canvas.getByRole("status");
|
|
41
|
-
await expect(root).toBeVisible();
|
|
42
|
-
await expect(canvas.getByText("Thermal fault")).toBeVisible();
|
|
43
|
-
const dangerColor = resolveTokenFgColor("--danger");
|
|
44
|
-
await expect(getComputedStyle(root).borderLeftColor).toBe(dangerColor);
|
|
45
|
-
}} />
|
|
46
|
-
|
|
47
|
-
<Story name="Info" args={{ variant: "info", title: "Firmware update available", message: "v2.1.0 → v2.2.0." }}
|
|
48
|
-
play={async ({ canvasElement }) => {
|
|
49
|
-
const canvas = within(canvasElement);
|
|
50
|
-
const root = canvas.getByRole("status");
|
|
51
|
-
await expect(root).toBeVisible();
|
|
52
|
-
const cyanColor = resolveTokenFgColor("--cyan");
|
|
53
|
-
await expect(getComputedStyle(root).borderLeftColor).toBe(cyanColor);
|
|
54
|
-
}} />
|
|
55
|
-
|
|
56
|
-
<Story name="No Message" args={{ variant: "ok", title: "Status OK" }}
|
|
57
|
-
play={async ({ canvasElement }) => {
|
|
58
|
-
const canvas = within(canvasElement);
|
|
59
|
-
const root = canvas.getByRole("status");
|
|
60
|
-
await expect(root).toBeVisible();
|
|
61
|
-
await expect(canvas.getByText("Status OK")).toBeVisible();
|
|
62
|
-
await expect(canvasElement.querySelector(".alert-msg")).toBeNull();
|
|
63
|
-
}} />
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import type { HTMLAttributes } from 'svelte/elements'
|
|
3
|
-
import type { Snippet } from 'svelte'
|
|
4
|
-
import Stack from '../layout/Stack.svelte'
|
|
5
|
-
|
|
6
|
-
type AlertVariant = 'ok' | 'amber' | 'danger' | 'info'
|
|
7
|
-
|
|
8
|
-
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
9
|
-
/** Colour variant — drives the tag glyph and border accent. @default 'info' */
|
|
10
|
-
variant?: AlertVariant
|
|
11
|
-
/** Bold title text shown in the alert header. */
|
|
12
|
-
title: string
|
|
13
|
-
/** Body message text. */
|
|
14
|
-
message?: string
|
|
15
|
-
children?: Snippet
|
|
16
|
-
[key: string]: unknown
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
let {
|
|
20
|
-
variant = 'info',
|
|
21
|
-
title,
|
|
22
|
-
message,
|
|
23
|
-
children,
|
|
24
|
-
...rest
|
|
25
|
-
}: Props = $props()
|
|
26
|
-
|
|
27
|
-
const TAG_GLYPHS: Record<AlertVariant, string> = {
|
|
28
|
-
ok: 'ok',
|
|
29
|
-
amber: '!!',
|
|
30
|
-
danger: 'err',
|
|
31
|
-
info: 'inf',
|
|
32
|
-
}
|
|
33
|
-
</script>
|
|
34
|
-
|
|
35
|
-
<div class="alert alert--{variant}" role="status" {...rest}>
|
|
36
|
-
<span class="alert-tag" aria-hidden="true">{TAG_GLYPHS[variant]}</span>
|
|
37
|
-
<Stack gap="sm">
|
|
38
|
-
<span class="alert-title">{title}</span>
|
|
39
|
-
{#if message}
|
|
40
|
-
<span class="alert-msg">{message}</span>
|
|
41
|
-
{/if}
|
|
42
|
-
{@render children?.()}
|
|
43
|
-
</Stack>
|
|
44
|
-
</div>
|
|
45
|
-
|
|
46
|
-
<style>
|
|
47
|
-
.alert {
|
|
48
|
-
display: flex;
|
|
49
|
-
align-items: flex-start;
|
|
50
|
-
gap: 12px;
|
|
51
|
-
padding: 12px 16px;
|
|
52
|
-
border-left: 2px solid;
|
|
53
|
-
font-size: var(--t-body);
|
|
54
|
-
line-height: 1.5;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
.alert--ok { border-color: var(--ok); background: color-mix(in srgb, var(--ok) 8%, transparent); }
|
|
58
|
-
.alert--amber { border-color: var(--amber); background: color-mix(in srgb, var(--amber) 8%, transparent); }
|
|
59
|
-
.alert--danger { border-color: var(--danger); background: color-mix(in srgb, var(--danger) 8%, transparent); }
|
|
60
|
-
.alert--info { border-color: var(--cyan); background: color-mix(in srgb, var(--cyan) 8%, transparent); }
|
|
61
|
-
|
|
62
|
-
.alert-tag {
|
|
63
|
-
font-family: var(--mono);
|
|
64
|
-
font-size: var(--t-micro);
|
|
65
|
-
letter-spacing: 0.08em;
|
|
66
|
-
text-transform: uppercase;
|
|
67
|
-
flex-shrink: 0;
|
|
68
|
-
margin-top: 2px;
|
|
69
|
-
}
|
|
70
|
-
.alert--ok .alert-tag { color: var(--ok); }
|
|
71
|
-
.alert--amber .alert-tag { color: var(--amber); }
|
|
72
|
-
.alert--danger .alert-tag { color: var(--danger); }
|
|
73
|
-
.alert--info .alert-tag { color: var(--cyan); }
|
|
74
|
-
|
|
75
|
-
.alert-title {
|
|
76
|
-
font-family: var(--mono);
|
|
77
|
-
font-size: var(--t-micro);
|
|
78
|
-
letter-spacing: 0.08em;
|
|
79
|
-
text-transform: uppercase;
|
|
80
|
-
}
|
|
81
|
-
.alert--ok .alert-title { color: var(--ok); }
|
|
82
|
-
.alert--amber .alert-title { color: var(--amber); }
|
|
83
|
-
.alert--danger .alert-title { color: var(--danger); }
|
|
84
|
-
.alert--info .alert-title { color: var(--cyan); }
|
|
85
|
-
|
|
86
|
-
.alert-msg {
|
|
87
|
-
font-size: var(--t-mono);
|
|
88
|
-
color: var(--ink-dim);
|
|
89
|
-
line-height: 1.5;
|
|
90
|
-
}
|
|
91
|
-
</style>
|
|
File without changes
|