@dillingerstaffing/strand-ui 0.1.0 → 0.2.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/LICENSE +21 -0
- package/dist/components/Alert/Alert.d.ts +16 -0
- package/dist/components/Alert/Alert.d.ts.map +1 -0
- package/dist/components/Alert/index.d.ts +3 -0
- package/dist/components/Alert/index.d.ts.map +1 -0
- package/dist/components/Avatar/Avatar.d.ts +16 -0
- package/dist/components/Avatar/Avatar.d.ts.map +1 -0
- package/dist/components/Avatar/index.d.ts +3 -0
- package/dist/components/Avatar/index.d.ts.map +1 -0
- package/dist/components/Badge/Badge.d.ts +18 -0
- package/dist/components/Badge/Badge.d.ts.map +1 -0
- package/dist/components/Badge/index.d.ts +3 -0
- package/dist/components/Badge/index.d.ts.map +1 -0
- package/dist/components/Breadcrumb/Breadcrumb.d.ts +16 -0
- package/dist/components/Breadcrumb/Breadcrumb.d.ts.map +1 -0
- package/dist/components/Breadcrumb/index.d.ts +3 -0
- package/dist/components/Breadcrumb/index.d.ts.map +1 -0
- package/dist/components/Button/Button.d.ts +22 -0
- package/dist/components/Button/Button.d.ts.map +1 -0
- package/dist/components/Button/index.d.ts +3 -0
- package/dist/components/Button/index.d.ts.map +1 -0
- package/dist/components/Card/Card.d.ts +12 -0
- package/dist/components/Card/Card.d.ts.map +1 -0
- package/dist/components/Card/index.d.ts +3 -0
- package/dist/components/Card/index.d.ts.map +1 -0
- package/dist/components/Checkbox/Checkbox.d.ts +20 -0
- package/dist/components/Checkbox/Checkbox.d.ts.map +1 -0
- package/dist/components/Checkbox/index.d.ts +3 -0
- package/dist/components/Checkbox/index.d.ts.map +1 -0
- package/dist/components/Container/Container.d.ts +10 -0
- package/dist/components/Container/Container.d.ts.map +1 -0
- package/dist/components/Container/index.d.ts +3 -0
- package/dist/components/Container/index.d.ts.map +1 -0
- package/dist/components/DataReadout/DataReadout.d.ts +12 -0
- package/dist/components/DataReadout/DataReadout.d.ts.map +1 -0
- package/dist/components/DataReadout/index.d.ts +3 -0
- package/dist/components/DataReadout/index.d.ts.map +1 -0
- package/dist/components/Dialog/Dialog.d.ts +20 -0
- package/dist/components/Dialog/Dialog.d.ts.map +1 -0
- package/dist/components/Dialog/index.d.ts +3 -0
- package/dist/components/Dialog/index.d.ts.map +1 -0
- package/dist/components/Divider/Divider.d.ts +13 -0
- package/dist/components/Divider/Divider.d.ts.map +1 -0
- package/dist/components/Divider/index.d.ts +3 -0
- package/dist/components/Divider/index.d.ts.map +1 -0
- package/dist/components/FormField/FormField.d.ts +22 -0
- package/dist/components/FormField/FormField.d.ts.map +1 -0
- package/dist/components/FormField/index.d.ts +3 -0
- package/dist/components/FormField/index.d.ts.map +1 -0
- package/dist/components/Grid/Grid.d.ts +12 -0
- package/dist/components/Grid/Grid.d.ts.map +1 -0
- package/dist/components/Grid/index.d.ts +3 -0
- package/dist/components/Grid/index.d.ts.map +1 -0
- package/dist/components/Input/Input.d.ts +18 -0
- package/dist/components/Input/Input.d.ts.map +1 -0
- package/dist/components/Input/index.d.ts +3 -0
- package/dist/components/Input/index.d.ts.map +1 -0
- package/dist/components/Link/Link.d.ts +12 -0
- package/dist/components/Link/Link.d.ts.map +1 -0
- package/dist/components/Link/index.d.ts +3 -0
- package/dist/components/Link/index.d.ts.map +1 -0
- package/dist/components/Nav/Nav.d.ts +19 -0
- package/dist/components/Nav/Nav.d.ts.map +1 -0
- package/dist/components/Nav/index.d.ts +3 -0
- package/dist/components/Nav/index.d.ts.map +1 -0
- package/dist/components/Progress/Progress.d.ts +14 -0
- package/dist/components/Progress/Progress.d.ts.map +1 -0
- package/dist/components/Progress/index.d.ts +3 -0
- package/dist/components/Progress/index.d.ts.map +1 -0
- package/dist/components/Radio/Radio.d.ts +22 -0
- package/dist/components/Radio/Radio.d.ts.map +1 -0
- package/dist/components/Radio/index.d.ts +3 -0
- package/dist/components/Radio/index.d.ts.map +1 -0
- package/dist/components/Section/Section.d.ts +12 -0
- package/dist/components/Section/Section.d.ts.map +1 -0
- package/dist/components/Section/index.d.ts +3 -0
- package/dist/components/Section/index.d.ts.map +1 -0
- package/dist/components/Select/Select.d.ts +24 -0
- package/dist/components/Select/Select.d.ts.map +1 -0
- package/dist/components/Select/index.d.ts +3 -0
- package/dist/components/Select/index.d.ts.map +1 -0
- package/dist/components/Skeleton/Skeleton.d.ts +14 -0
- package/dist/components/Skeleton/Skeleton.d.ts.map +1 -0
- package/dist/components/Skeleton/index.d.ts +3 -0
- package/dist/components/Skeleton/index.d.ts.map +1 -0
- package/dist/components/Slider/Slider.d.ts +20 -0
- package/dist/components/Slider/Slider.d.ts.map +1 -0
- package/dist/components/Slider/index.d.ts +3 -0
- package/dist/components/Slider/index.d.ts.map +1 -0
- package/dist/components/Spinner/Spinner.d.ts +10 -0
- package/dist/components/Spinner/Spinner.d.ts.map +1 -0
- package/dist/components/Spinner/index.d.ts +3 -0
- package/dist/components/Spinner/index.d.ts.map +1 -0
- package/dist/components/Stack/Stack.d.ts +18 -0
- package/dist/components/Stack/Stack.d.ts.map +1 -0
- package/dist/components/Stack/index.d.ts +3 -0
- package/dist/components/Stack/index.d.ts.map +1 -0
- package/dist/components/Switch/Switch.d.ts +18 -0
- package/dist/components/Switch/Switch.d.ts.map +1 -0
- package/dist/components/Switch/index.d.ts +3 -0
- package/dist/components/Switch/index.d.ts.map +1 -0
- package/dist/components/Table/Table.d.ts +24 -0
- package/dist/components/Table/Table.d.ts.map +1 -0
- package/dist/components/Table/index.d.ts +3 -0
- package/dist/components/Table/index.d.ts.map +1 -0
- package/dist/components/Tabs/Tabs.d.ts +19 -0
- package/dist/components/Tabs/Tabs.d.ts.map +1 -0
- package/dist/components/Tabs/index.d.ts +3 -0
- package/dist/components/Tabs/index.d.ts.map +1 -0
- package/dist/components/Tag/Tag.d.ts +18 -0
- package/dist/components/Tag/Tag.d.ts.map +1 -0
- package/dist/components/Tag/index.d.ts +3 -0
- package/dist/components/Tag/index.d.ts.map +1 -0
- package/dist/components/Textarea/Textarea.d.ts +22 -0
- package/dist/components/Textarea/Textarea.d.ts.map +1 -0
- package/dist/components/Textarea/index.d.ts +3 -0
- package/dist/components/Textarea/index.d.ts.map +1 -0
- package/dist/components/Toast/Toast.d.ts +33 -0
- package/dist/components/Toast/Toast.d.ts.map +1 -0
- package/dist/components/Toast/index.d.ts +3 -0
- package/dist/components/Toast/index.d.ts.map +1 -0
- package/dist/components/Tooltip/Tooltip.d.ts +16 -0
- package/dist/components/Tooltip/Tooltip.d.ts.map +1 -0
- package/dist/components/Tooltip/index.d.ts +3 -0
- package/dist/components/Tooltip/index.d.ts.map +1 -0
- package/dist/css/strand-ui.css +2464 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/test-setup.d.ts +2 -0
- package/dist/test-setup.d.ts.map +1 -0
- package/package.json +25 -11
- package/src/__tests__/build-output.test.ts +200 -0
- package/src/__tests__/design-language.test.ts +137 -0
- package/src/__tests__/static.test.tsx +60 -0
- package/src/components/Alert/Alert.css +75 -0
- package/src/components/Alert/Alert.test.tsx +92 -0
- package/src/components/Alert/Alert.tsx +59 -0
- package/src/components/Alert/index.ts +2 -0
- package/src/components/Avatar/Avatar.css +55 -0
- package/src/components/Avatar/Avatar.test.tsx +123 -0
- package/src/components/Avatar/Avatar.tsx +67 -0
- package/src/components/Avatar/index.ts +2 -0
- package/src/components/Badge/Badge.css +72 -0
- package/src/components/Badge/Badge.test.tsx +121 -0
- package/src/components/Badge/Badge.tsx +92 -0
- package/src/components/Badge/index.ts +2 -0
- package/src/components/Breadcrumb/Breadcrumb.css +50 -0
- package/src/components/Breadcrumb/Breadcrumb.test.tsx +107 -0
- package/src/components/Breadcrumb/Breadcrumb.tsx +59 -0
- package/src/components/Breadcrumb/index.ts +2 -0
- package/src/components/Button/Button.css +195 -0
- package/src/components/Button/Button.test.tsx +171 -0
- package/src/components/Button/Button.tsx +78 -0
- package/src/components/Button/index.ts +2 -0
- package/src/components/Card/Card.css +68 -0
- package/src/components/Card/Card.test.tsx +90 -0
- package/src/components/Card/Card.tsx +41 -0
- package/src/components/Card/index.ts +2 -0
- package/src/components/Checkbox/Checkbox.css +97 -0
- package/src/components/Checkbox/Checkbox.test.tsx +92 -0
- package/src/components/Checkbox/Checkbox.tsx +137 -0
- package/src/components/Checkbox/index.ts +2 -0
- package/src/components/Container/Container.css +25 -0
- package/src/components/Container/Container.test.tsx +82 -0
- package/src/components/Container/Container.tsx +37 -0
- package/src/components/Container/index.ts +2 -0
- package/src/components/DataReadout/DataReadout.css +30 -0
- package/src/components/DataReadout/DataReadout.test.tsx +105 -0
- package/src/components/DataReadout/DataReadout.tsx +29 -0
- package/src/components/DataReadout/index.ts +2 -0
- package/src/components/Dialog/Dialog.css +81 -0
- package/src/components/Dialog/Dialog.test.tsx +203 -0
- package/src/components/Dialog/Dialog.tsx +179 -0
- package/src/components/Dialog/index.ts +2 -0
- package/src/components/Divider/Divider.css +44 -0
- package/src/components/Divider/Divider.test.tsx +86 -0
- package/src/components/Divider/Divider.tsx +81 -0
- package/src/components/Divider/index.ts +2 -0
- package/src/components/FormField/FormField.css +47 -0
- package/src/components/FormField/FormField.test.tsx +99 -0
- package/src/components/FormField/FormField.tsx +79 -0
- package/src/components/FormField/index.ts +2 -0
- package/src/components/Grid/Grid.css +27 -0
- package/src/components/Grid/Grid.test.tsx +86 -0
- package/src/components/Grid/Grid.tsx +45 -0
- package/src/components/Grid/index.ts +2 -0
- package/src/components/Input/Input.css +87 -0
- package/src/components/Input/Input.test.tsx +95 -0
- package/src/components/Input/Input.tsx +69 -0
- package/src/components/Input/index.ts +2 -0
- package/src/components/Link/Link.css +30 -0
- package/src/components/Link/Link.test.tsx +88 -0
- package/src/components/Link/Link.tsx +31 -0
- package/src/components/Link/index.ts +2 -0
- package/src/components/Nav/Nav.css +179 -0
- package/src/components/Nav/Nav.test.tsx +174 -0
- package/src/components/Nav/Nav.tsx +101 -0
- package/src/components/Nav/index.ts +2 -0
- package/src/components/Progress/Progress.css +93 -0
- package/src/components/Progress/Progress.test.tsx +93 -0
- package/src/components/Progress/Progress.tsx +104 -0
- package/src/components/Progress/index.ts +2 -0
- package/src/components/Radio/Radio.css +98 -0
- package/src/components/Radio/Radio.test.tsx +80 -0
- package/src/components/Radio/Radio.tsx +72 -0
- package/src/components/Radio/index.ts +2 -0
- package/src/components/Section/Section.css +28 -0
- package/src/components/Section/Section.test.tsx +100 -0
- package/src/components/Section/Section.tsx +41 -0
- package/src/components/Section/index.ts +2 -0
- package/src/components/Select/Select.css +75 -0
- package/src/components/Select/Select.test.tsx +99 -0
- package/src/components/Select/Select.tsx +78 -0
- package/src/components/Select/index.ts +2 -0
- package/src/components/Skeleton/Skeleton.css +52 -0
- package/src/components/Skeleton/Skeleton.test.tsx +96 -0
- package/src/components/Skeleton/Skeleton.tsx +55 -0
- package/src/components/Skeleton/index.ts +2 -0
- package/src/components/Slider/Slider.css +107 -0
- package/src/components/Slider/Slider.test.tsx +85 -0
- package/src/components/Slider/Slider.tsx +66 -0
- package/src/components/Slider/index.ts +2 -0
- package/src/components/Spinner/Spinner.css +61 -0
- package/src/components/Spinner/Spinner.test.tsx +56 -0
- package/src/components/Spinner/Spinner.tsx +38 -0
- package/src/components/Spinner/index.ts +2 -0
- package/src/components/Stack/Stack.css +71 -0
- package/src/components/Stack/Stack.test.tsx +130 -0
- package/src/components/Stack/Stack.tsx +77 -0
- package/src/components/Stack/index.ts +2 -0
- package/src/components/Switch/Switch.css +94 -0
- package/src/components/Switch/Switch.test.tsx +98 -0
- package/src/components/Switch/Switch.tsx +80 -0
- package/src/components/Switch/index.ts +2 -0
- package/src/components/Table/Table.css +83 -0
- package/src/components/Table/Table.test.tsx +134 -0
- package/src/components/Table/Table.tsx +102 -0
- package/src/components/Table/index.ts +2 -0
- package/src/components/Tabs/Tabs.css +51 -0
- package/src/components/Tabs/Tabs.test.tsx +164 -0
- package/src/components/Tabs/Tabs.tsx +126 -0
- package/src/components/Tabs/index.ts +2 -0
- package/src/components/Tag/Tag.css +98 -0
- package/src/components/Tag/Tag.test.tsx +112 -0
- package/src/components/Tag/Tag.tsx +73 -0
- package/src/components/Tag/index.ts +2 -0
- package/src/components/Textarea/Textarea.css +80 -0
- package/src/components/Textarea/Textarea.test.tsx +89 -0
- package/src/components/Textarea/Textarea.tsx +102 -0
- package/src/components/Textarea/index.ts +2 -0
- package/src/components/Toast/Toast.css +103 -0
- package/src/components/Toast/Toast.test.tsx +219 -0
- package/src/components/Toast/Toast.tsx +177 -0
- package/src/components/Toast/index.ts +2 -0
- package/src/components/Tooltip/Tooltip.css +63 -0
- package/src/components/Tooltip/Tooltip.test.tsx +196 -0
- package/src/components/Tooltip/Tooltip.tsx +89 -0
- package/src/components/Tooltip/index.ts +2 -0
- package/src/index.ts +99 -0
- package/src/static.css +47 -0
- package/src/test-setup.ts +7 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { render } from "@testing-library/preact";
|
|
3
|
+
import { Section } from "./Section.js";
|
|
4
|
+
|
|
5
|
+
describe("Section", () => {
|
|
6
|
+
// ── Rendering ──
|
|
7
|
+
|
|
8
|
+
it("renders a section element", () => {
|
|
9
|
+
const { container } = render(<Section>content</Section>);
|
|
10
|
+
expect(container.firstElementChild?.tagName).toBe("SECTION");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("renders children", () => {
|
|
14
|
+
const { getByText } = render(
|
|
15
|
+
<Section>
|
|
16
|
+
<h2>Title</h2>
|
|
17
|
+
<p>Body text</p>
|
|
18
|
+
</Section>,
|
|
19
|
+
);
|
|
20
|
+
expect(getByText("Title")).toBeTruthy();
|
|
21
|
+
expect(getByText("Body text")).toBeTruthy();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// ── Variant ──
|
|
25
|
+
|
|
26
|
+
it("applies standard variant class by default", () => {
|
|
27
|
+
const { container } = render(<Section>content</Section>);
|
|
28
|
+
expect(container.firstElementChild?.className).toContain(
|
|
29
|
+
"strand-section--standard",
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("applies hero variant class", () => {
|
|
34
|
+
const { container } = render(<Section variant="hero">content</Section>);
|
|
35
|
+
expect(container.firstElementChild?.className).toContain(
|
|
36
|
+
"strand-section--hero",
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// ── Background ──
|
|
41
|
+
|
|
42
|
+
it("applies primary background class by default", () => {
|
|
43
|
+
const { container } = render(<Section>content</Section>);
|
|
44
|
+
expect(container.firstElementChild?.className).toContain(
|
|
45
|
+
"strand-section--bg-primary",
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("applies elevated background class", () => {
|
|
50
|
+
const { container } = render(
|
|
51
|
+
<Section background="elevated">content</Section>,
|
|
52
|
+
);
|
|
53
|
+
expect(container.firstElementChild?.className).toContain(
|
|
54
|
+
"strand-section--bg-elevated",
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("applies recessed background class", () => {
|
|
59
|
+
const { container } = render(
|
|
60
|
+
<Section background="recessed">content</Section>,
|
|
61
|
+
);
|
|
62
|
+
expect(container.firstElementChild?.className).toContain(
|
|
63
|
+
"strand-section--bg-recessed",
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// ── Custom className ──
|
|
68
|
+
|
|
69
|
+
it("merges custom className", () => {
|
|
70
|
+
const { container } = render(
|
|
71
|
+
<Section className="custom">content</Section>,
|
|
72
|
+
);
|
|
73
|
+
const el = container.firstElementChild;
|
|
74
|
+
expect(el?.className).toContain("strand-section");
|
|
75
|
+
expect(el?.className).toContain("custom");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// ── Props forwarding ──
|
|
79
|
+
|
|
80
|
+
it("forwards additional props", () => {
|
|
81
|
+
const { container } = render(
|
|
82
|
+
<Section data-testid="my-section" id="s1">
|
|
83
|
+
content
|
|
84
|
+
</Section>,
|
|
85
|
+
);
|
|
86
|
+
expect(container.firstElementChild).toHaveAttribute("id", "s1");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// ── Aria ──
|
|
90
|
+
|
|
91
|
+
it("supports aria-labelledby", () => {
|
|
92
|
+
const { container } = render(
|
|
93
|
+
<Section aria-labelledby="heading-1">content</Section>,
|
|
94
|
+
);
|
|
95
|
+
expect(container.firstElementChild).toHaveAttribute(
|
|
96
|
+
"aria-labelledby",
|
|
97
|
+
"heading-1",
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/*! Strand UI | MIT License | dillingerstaffing.com */
|
|
2
|
+
|
|
3
|
+
import type { JSX } from "preact";
|
|
4
|
+
import { forwardRef } from "preact/compat";
|
|
5
|
+
|
|
6
|
+
export interface SectionProps extends JSX.HTMLAttributes<HTMLElement> {
|
|
7
|
+
/** Padding variant */
|
|
8
|
+
variant?: "standard" | "hero";
|
|
9
|
+
/** Surface background */
|
|
10
|
+
background?: "primary" | "elevated" | "recessed";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const Section = forwardRef<HTMLElement, SectionProps>(
|
|
14
|
+
(
|
|
15
|
+
{
|
|
16
|
+
variant = "standard",
|
|
17
|
+
background = "primary",
|
|
18
|
+
className = "",
|
|
19
|
+
children,
|
|
20
|
+
...rest
|
|
21
|
+
},
|
|
22
|
+
ref,
|
|
23
|
+
) => {
|
|
24
|
+
const classes = [
|
|
25
|
+
"strand-section",
|
|
26
|
+
`strand-section--${variant}`,
|
|
27
|
+
`strand-section--bg-${background}`,
|
|
28
|
+
className,
|
|
29
|
+
]
|
|
30
|
+
.filter(Boolean)
|
|
31
|
+
.join(" ");
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<section ref={ref} className={classes} {...rest}>
|
|
35
|
+
{children}
|
|
36
|
+
</section>
|
|
37
|
+
);
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
Section.displayName = "Section";
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/*! Strand UI | MIT License | dillingerstaffing.com */
|
|
2
|
+
|
|
3
|
+
/* ── Base ── */
|
|
4
|
+
.strand-select {
|
|
5
|
+
position: relative;
|
|
6
|
+
display: inline-flex;
|
|
7
|
+
align-items: center;
|
|
8
|
+
background: var(--strand-surface-elevated);
|
|
9
|
+
border: 1px solid var(--strand-gray-200);
|
|
10
|
+
border-radius: var(--strand-radius-md);
|
|
11
|
+
transition:
|
|
12
|
+
border-color var(--strand-duration-fast) var(--strand-ease-out-quart),
|
|
13
|
+
box-shadow var(--strand-duration-fast) var(--strand-ease-out-quart);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.strand-select:focus-within {
|
|
17
|
+
border-color: var(--strand-blue-primary);
|
|
18
|
+
box-shadow: var(--strand-focus-ring);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/* ── Field ── */
|
|
22
|
+
.strand-select__field {
|
|
23
|
+
flex: 1;
|
|
24
|
+
width: 100%;
|
|
25
|
+
padding: var(--strand-space-3) var(--strand-space-8) var(--strand-space-3) var(--strand-space-4);
|
|
26
|
+
background: transparent;
|
|
27
|
+
border: none;
|
|
28
|
+
font-family: var(--strand-font-sans);
|
|
29
|
+
font-size: var(--strand-text-base);
|
|
30
|
+
color: var(--strand-gray-900);
|
|
31
|
+
outline: none;
|
|
32
|
+
appearance: none;
|
|
33
|
+
cursor: pointer;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* ── Arrow indicator ── */
|
|
37
|
+
.strand-select__arrow {
|
|
38
|
+
position: absolute;
|
|
39
|
+
right: var(--strand-space-3);
|
|
40
|
+
top: 50%;
|
|
41
|
+
transform: translateY(-50%);
|
|
42
|
+
width: 0;
|
|
43
|
+
height: 0;
|
|
44
|
+
border-left: 5px solid transparent;
|
|
45
|
+
border-right: 5px solid transparent;
|
|
46
|
+
border-top: 5px solid var(--strand-gray-500);
|
|
47
|
+
pointer-events: none;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* ── Error ── */
|
|
51
|
+
.strand-select--error {
|
|
52
|
+
border-color: var(--strand-red-alert);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.strand-select--error:focus-within {
|
|
56
|
+
border-color: var(--strand-red-alert);
|
|
57
|
+
box-shadow: var(--strand-focus-ring-error);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* ── Disabled ── */
|
|
61
|
+
.strand-select--disabled {
|
|
62
|
+
opacity: 0.4;
|
|
63
|
+
cursor: not-allowed;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.strand-select--disabled .strand-select__field {
|
|
67
|
+
cursor: not-allowed;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* ── Reduced motion ── */
|
|
71
|
+
@media (prefers-reduced-motion: reduce) {
|
|
72
|
+
.strand-select {
|
|
73
|
+
transition: none;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { render, fireEvent } from "@testing-library/preact";
|
|
3
|
+
import { Select } from "./Select.js";
|
|
4
|
+
|
|
5
|
+
const defaultOptions = [
|
|
6
|
+
{ value: "a", label: "Alpha" },
|
|
7
|
+
{ value: "b", label: "Beta" },
|
|
8
|
+
{ value: "c", label: "Gamma" },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
describe("Select", () => {
|
|
12
|
+
it("renders a select element", () => {
|
|
13
|
+
const { getByRole } = render(
|
|
14
|
+
<Select options={defaultOptions} aria-label="Choice" />
|
|
15
|
+
);
|
|
16
|
+
expect(getByRole("combobox")).toBeTruthy();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("shows placeholder as first disabled option", () => {
|
|
20
|
+
const { getByRole } = render(
|
|
21
|
+
<Select options={defaultOptions} placeholder="Pick one" aria-label="Choice" />
|
|
22
|
+
);
|
|
23
|
+
const select = getByRole("combobox") as HTMLSelectElement;
|
|
24
|
+
const firstOption = select.options[0];
|
|
25
|
+
expect(firstOption.textContent).toBe("Pick one");
|
|
26
|
+
expect(firstOption.disabled).toBe(true);
|
|
27
|
+
expect(firstOption.value).toBe("");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("renders all options", () => {
|
|
31
|
+
const { getByRole } = render(
|
|
32
|
+
<Select options={defaultOptions} aria-label="Choice" />
|
|
33
|
+
);
|
|
34
|
+
const select = getByRole("combobox") as HTMLSelectElement;
|
|
35
|
+
expect(select.options.length).toBe(3);
|
|
36
|
+
expect(select.options[0].textContent).toBe("Alpha");
|
|
37
|
+
expect(select.options[1].textContent).toBe("Beta");
|
|
38
|
+
expect(select.options[2].textContent).toBe("Gamma");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("calls onChange on selection", () => {
|
|
42
|
+
const onChange = vi.fn();
|
|
43
|
+
const { getByRole } = render(
|
|
44
|
+
<Select options={defaultOptions} onChange={onChange} aria-label="Choice" />
|
|
45
|
+
);
|
|
46
|
+
const select = getByRole("combobox") as HTMLSelectElement;
|
|
47
|
+
// Preact onChange maps to native "change" event; dispatch it directly
|
|
48
|
+
select.value = "b";
|
|
49
|
+
select.dispatchEvent(new Event("change", { bubbles: true }));
|
|
50
|
+
expect(onChange).toHaveBeenCalled();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("applies error class when error prop is true", () => {
|
|
54
|
+
const { container } = render(
|
|
55
|
+
<Select options={defaultOptions} error aria-label="Choice" />
|
|
56
|
+
);
|
|
57
|
+
expect(container.querySelector(".strand-select--error")).toBeTruthy();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("sets aria-invalid when error is true", () => {
|
|
61
|
+
const { getByRole } = render(
|
|
62
|
+
<Select options={defaultOptions} error aria-label="Choice" />
|
|
63
|
+
);
|
|
64
|
+
expect(getByRole("combobox")).toHaveAttribute("aria-invalid", "true");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("sets disabled attribute when disabled", () => {
|
|
68
|
+
const { getByRole } = render(
|
|
69
|
+
<Select options={defaultOptions} disabled aria-label="Choice" />
|
|
70
|
+
);
|
|
71
|
+
expect(getByRole("combobox")).toBeDisabled();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("applies disabled class to wrapper when disabled", () => {
|
|
75
|
+
const { container } = render(
|
|
76
|
+
<Select options={defaultOptions} disabled aria-label="Choice" />
|
|
77
|
+
);
|
|
78
|
+
expect(container.querySelector(".strand-select--disabled")).toBeTruthy();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("merges custom className", () => {
|
|
82
|
+
const { container } = render(
|
|
83
|
+
<Select options={defaultOptions} className="custom" aria-label="Choice" />
|
|
84
|
+
);
|
|
85
|
+
expect(container.querySelector(".strand-select")?.className).toContain("custom");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("forwards ref to select element", () => {
|
|
89
|
+
let selectEl: HTMLSelectElement | null = null;
|
|
90
|
+
render(
|
|
91
|
+
<Select
|
|
92
|
+
options={defaultOptions}
|
|
93
|
+
aria-label="Choice"
|
|
94
|
+
ref={(el: HTMLSelectElement | null) => { selectEl = el; }}
|
|
95
|
+
/>
|
|
96
|
+
);
|
|
97
|
+
expect(selectEl).toBeInstanceOf(HTMLSelectElement);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/*! Strand UI | MIT License | dillingerstaffing.com */
|
|
2
|
+
|
|
3
|
+
import type { JSX } from "preact";
|
|
4
|
+
import { forwardRef } from "preact/compat";
|
|
5
|
+
|
|
6
|
+
export interface SelectOption {
|
|
7
|
+
value: string;
|
|
8
|
+
label: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface SelectProps
|
|
12
|
+
extends Omit<JSX.HTMLAttributes<HTMLSelectElement>, "onChange"> {
|
|
13
|
+
/** Array of options to display */
|
|
14
|
+
options: SelectOption[];
|
|
15
|
+
/** Disabled state */
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
/** Currently selected value */
|
|
18
|
+
value?: string;
|
|
19
|
+
/** Change handler */
|
|
20
|
+
onChange?: (e: JSX.TargetedEvent<HTMLSelectElement>) => void;
|
|
21
|
+
/** Show error styling */
|
|
22
|
+
error?: boolean;
|
|
23
|
+
/** Placeholder text shown as first disabled option */
|
|
24
|
+
placeholder?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const Select = forwardRef<HTMLSelectElement, SelectProps>(
|
|
28
|
+
(
|
|
29
|
+
{
|
|
30
|
+
options,
|
|
31
|
+
value,
|
|
32
|
+
onChange,
|
|
33
|
+
disabled,
|
|
34
|
+
error = false,
|
|
35
|
+
placeholder,
|
|
36
|
+
className = "",
|
|
37
|
+
...rest
|
|
38
|
+
},
|
|
39
|
+
ref,
|
|
40
|
+
) => {
|
|
41
|
+
const wrapperClasses = [
|
|
42
|
+
"strand-select",
|
|
43
|
+
error && "strand-select--error",
|
|
44
|
+
disabled && "strand-select--disabled",
|
|
45
|
+
className,
|
|
46
|
+
]
|
|
47
|
+
.filter(Boolean)
|
|
48
|
+
.join(" ");
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div className={wrapperClasses}>
|
|
52
|
+
<select
|
|
53
|
+
ref={ref}
|
|
54
|
+
className="strand-select__field"
|
|
55
|
+
value={value}
|
|
56
|
+
onChange={onChange}
|
|
57
|
+
disabled={disabled}
|
|
58
|
+
aria-invalid={error ? "true" : undefined}
|
|
59
|
+
{...rest}
|
|
60
|
+
>
|
|
61
|
+
{placeholder && (
|
|
62
|
+
<option value="" disabled>
|
|
63
|
+
{placeholder}
|
|
64
|
+
</option>
|
|
65
|
+
)}
|
|
66
|
+
{options.map((opt) => (
|
|
67
|
+
<option key={opt.value} value={opt.value}>
|
|
68
|
+
{opt.label}
|
|
69
|
+
</option>
|
|
70
|
+
))}
|
|
71
|
+
</select>
|
|
72
|
+
<span className="strand-select__arrow" aria-hidden="true" />
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
Select.displayName = "Select";
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/*! Strand UI | MIT License | dillingerstaffing.com */
|
|
2
|
+
|
|
3
|
+
/* ── Base ── */
|
|
4
|
+
.strand-skeleton {
|
|
5
|
+
display: block;
|
|
6
|
+
background: var(--strand-gray-100);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/* ── Shimmer ── */
|
|
10
|
+
.strand-skeleton--shimmer {
|
|
11
|
+
background: linear-gradient(
|
|
12
|
+
90deg,
|
|
13
|
+
var(--strand-gray-100) 25%,
|
|
14
|
+
var(--strand-gray-50) 50%,
|
|
15
|
+
var(--strand-gray-100) 75%
|
|
16
|
+
);
|
|
17
|
+
background-size: 200% 100%;
|
|
18
|
+
animation: strand-skeleton-shimmer 1.8s var(--strand-ease-in-out-sine) infinite;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@keyframes strand-skeleton-shimmer {
|
|
22
|
+
0% {
|
|
23
|
+
background-position: 200% 0;
|
|
24
|
+
}
|
|
25
|
+
100% {
|
|
26
|
+
background-position: -200% 0;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* ── Text variant ── */
|
|
31
|
+
.strand-skeleton--text {
|
|
32
|
+
height: 1em;
|
|
33
|
+
border-radius: var(--strand-radius-sm);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* ── Rectangle variant ── */
|
|
37
|
+
.strand-skeleton--rectangle {
|
|
38
|
+
border-radius: var(--strand-radius-md);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* ── Circle variant ── */
|
|
42
|
+
.strand-skeleton--circle {
|
|
43
|
+
border-radius: var(--strand-radius-full);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* ── Reduced motion ── */
|
|
47
|
+
@media (prefers-reduced-motion: reduce) {
|
|
48
|
+
.strand-skeleton--shimmer {
|
|
49
|
+
animation: none;
|
|
50
|
+
background: var(--strand-gray-100);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { render } from "@testing-library/preact";
|
|
3
|
+
import { Skeleton } from "./Skeleton.js";
|
|
4
|
+
|
|
5
|
+
describe("Skeleton", () => {
|
|
6
|
+
// ── Rendering ──
|
|
7
|
+
|
|
8
|
+
it("renders a div element", () => {
|
|
9
|
+
const { container } = render(<Skeleton />);
|
|
10
|
+
expect(container.firstElementChild?.tagName).toBe("DIV");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("is aria-hidden", () => {
|
|
14
|
+
const { container } = render(<Skeleton />);
|
|
15
|
+
expect(container.firstElementChild?.getAttribute("aria-hidden")).toBe(
|
|
16
|
+
"true",
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// ── Variants ──
|
|
21
|
+
|
|
22
|
+
it("applies text variant class by default", () => {
|
|
23
|
+
const { container } = render(<Skeleton />);
|
|
24
|
+
expect(container.firstElementChild?.className).toContain(
|
|
25
|
+
"strand-skeleton--text",
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("applies rectangle variant class", () => {
|
|
30
|
+
const { container } = render(
|
|
31
|
+
<Skeleton variant="rectangle" width="200px" height="100px" />,
|
|
32
|
+
);
|
|
33
|
+
expect(container.firstElementChild?.className).toContain(
|
|
34
|
+
"strand-skeleton--rectangle",
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("applies circle variant class", () => {
|
|
39
|
+
const { container } = render(
|
|
40
|
+
<Skeleton variant="circle" width="48px" />,
|
|
41
|
+
);
|
|
42
|
+
expect(container.firstElementChild?.className).toContain(
|
|
43
|
+
"strand-skeleton--circle",
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// ── Dimensions ──
|
|
48
|
+
|
|
49
|
+
it("applies width style", () => {
|
|
50
|
+
const { container } = render(<Skeleton width="200px" />);
|
|
51
|
+
expect((container.firstElementChild as HTMLElement)?.style.width).toBe(
|
|
52
|
+
"200px",
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("applies height style", () => {
|
|
57
|
+
const { container } = render(<Skeleton height="40px" />);
|
|
58
|
+
expect((container.firstElementChild as HTMLElement)?.style.height).toBe(
|
|
59
|
+
"40px",
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("circle has equal width and height", () => {
|
|
64
|
+
const { container } = render(
|
|
65
|
+
<Skeleton variant="circle" width="48px" />,
|
|
66
|
+
);
|
|
67
|
+
const el = container.firstElementChild as HTMLElement;
|
|
68
|
+
expect(el.style.width).toBe("48px");
|
|
69
|
+
expect(el.style.height).toBe("48px");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("text variant defaults width to 100%", () => {
|
|
73
|
+
const { container } = render(<Skeleton />);
|
|
74
|
+
expect((container.firstElementChild as HTMLElement)?.style.width).toBe(
|
|
75
|
+
"100%",
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// ── Shimmer ──
|
|
80
|
+
|
|
81
|
+
it("has shimmer animation class", () => {
|
|
82
|
+
const { container } = render(<Skeleton />);
|
|
83
|
+
expect(container.firstElementChild?.className).toContain(
|
|
84
|
+
"strand-skeleton--shimmer",
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// ── Custom className ──
|
|
89
|
+
|
|
90
|
+
it("merges custom className", () => {
|
|
91
|
+
const { container } = render(<Skeleton className="custom" />);
|
|
92
|
+
const el = container.firstElementChild;
|
|
93
|
+
expect(el?.className).toContain("strand-skeleton");
|
|
94
|
+
expect(el?.className).toContain("custom");
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/*! Strand UI | MIT License | dillingerstaffing.com */
|
|
2
|
+
|
|
3
|
+
import type { JSX } from "preact";
|
|
4
|
+
import { forwardRef } from "preact/compat";
|
|
5
|
+
|
|
6
|
+
export interface SkeletonProps
|
|
7
|
+
extends Omit<JSX.HTMLAttributes<HTMLDivElement>, "width" | "height"> {
|
|
8
|
+
/** Shape variant */
|
|
9
|
+
variant?: "text" | "rectangle" | "circle";
|
|
10
|
+
/** CSS width value */
|
|
11
|
+
width?: string;
|
|
12
|
+
/** CSS height value */
|
|
13
|
+
height?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const Skeleton = forwardRef<HTMLDivElement, SkeletonProps>(
|
|
17
|
+
(
|
|
18
|
+
{
|
|
19
|
+
variant = "text",
|
|
20
|
+
width,
|
|
21
|
+
height,
|
|
22
|
+
className = "",
|
|
23
|
+
...rest
|
|
24
|
+
},
|
|
25
|
+
ref,
|
|
26
|
+
) => {
|
|
27
|
+
const effectiveWidth = width ?? (variant === "text" ? "100%" : undefined);
|
|
28
|
+
const effectiveHeight =
|
|
29
|
+
variant === "circle" ? effectiveWidth : height;
|
|
30
|
+
|
|
31
|
+
const classes = [
|
|
32
|
+
"strand-skeleton",
|
|
33
|
+
`strand-skeleton--${variant}`,
|
|
34
|
+
"strand-skeleton--shimmer",
|
|
35
|
+
className,
|
|
36
|
+
]
|
|
37
|
+
.filter(Boolean)
|
|
38
|
+
.join(" ");
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div
|
|
42
|
+
ref={ref}
|
|
43
|
+
className={classes}
|
|
44
|
+
aria-hidden="true"
|
|
45
|
+
style={{
|
|
46
|
+
width: effectiveWidth,
|
|
47
|
+
height: effectiveHeight,
|
|
48
|
+
}}
|
|
49
|
+
{...rest}
|
|
50
|
+
/>
|
|
51
|
+
);
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
Skeleton.displayName = "Skeleton";
|