@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.
Files changed (261) hide show
  1. package/LICENSE +21 -0
  2. package/dist/components/Alert/Alert.d.ts +16 -0
  3. package/dist/components/Alert/Alert.d.ts.map +1 -0
  4. package/dist/components/Alert/index.d.ts +3 -0
  5. package/dist/components/Alert/index.d.ts.map +1 -0
  6. package/dist/components/Avatar/Avatar.d.ts +16 -0
  7. package/dist/components/Avatar/Avatar.d.ts.map +1 -0
  8. package/dist/components/Avatar/index.d.ts +3 -0
  9. package/dist/components/Avatar/index.d.ts.map +1 -0
  10. package/dist/components/Badge/Badge.d.ts +18 -0
  11. package/dist/components/Badge/Badge.d.ts.map +1 -0
  12. package/dist/components/Badge/index.d.ts +3 -0
  13. package/dist/components/Badge/index.d.ts.map +1 -0
  14. package/dist/components/Breadcrumb/Breadcrumb.d.ts +16 -0
  15. package/dist/components/Breadcrumb/Breadcrumb.d.ts.map +1 -0
  16. package/dist/components/Breadcrumb/index.d.ts +3 -0
  17. package/dist/components/Breadcrumb/index.d.ts.map +1 -0
  18. package/dist/components/Button/Button.d.ts +22 -0
  19. package/dist/components/Button/Button.d.ts.map +1 -0
  20. package/dist/components/Button/index.d.ts +3 -0
  21. package/dist/components/Button/index.d.ts.map +1 -0
  22. package/dist/components/Card/Card.d.ts +12 -0
  23. package/dist/components/Card/Card.d.ts.map +1 -0
  24. package/dist/components/Card/index.d.ts +3 -0
  25. package/dist/components/Card/index.d.ts.map +1 -0
  26. package/dist/components/Checkbox/Checkbox.d.ts +20 -0
  27. package/dist/components/Checkbox/Checkbox.d.ts.map +1 -0
  28. package/dist/components/Checkbox/index.d.ts +3 -0
  29. package/dist/components/Checkbox/index.d.ts.map +1 -0
  30. package/dist/components/Container/Container.d.ts +10 -0
  31. package/dist/components/Container/Container.d.ts.map +1 -0
  32. package/dist/components/Container/index.d.ts +3 -0
  33. package/dist/components/Container/index.d.ts.map +1 -0
  34. package/dist/components/DataReadout/DataReadout.d.ts +12 -0
  35. package/dist/components/DataReadout/DataReadout.d.ts.map +1 -0
  36. package/dist/components/DataReadout/index.d.ts +3 -0
  37. package/dist/components/DataReadout/index.d.ts.map +1 -0
  38. package/dist/components/Dialog/Dialog.d.ts +20 -0
  39. package/dist/components/Dialog/Dialog.d.ts.map +1 -0
  40. package/dist/components/Dialog/index.d.ts +3 -0
  41. package/dist/components/Dialog/index.d.ts.map +1 -0
  42. package/dist/components/Divider/Divider.d.ts +13 -0
  43. package/dist/components/Divider/Divider.d.ts.map +1 -0
  44. package/dist/components/Divider/index.d.ts +3 -0
  45. package/dist/components/Divider/index.d.ts.map +1 -0
  46. package/dist/components/FormField/FormField.d.ts +22 -0
  47. package/dist/components/FormField/FormField.d.ts.map +1 -0
  48. package/dist/components/FormField/index.d.ts +3 -0
  49. package/dist/components/FormField/index.d.ts.map +1 -0
  50. package/dist/components/Grid/Grid.d.ts +12 -0
  51. package/dist/components/Grid/Grid.d.ts.map +1 -0
  52. package/dist/components/Grid/index.d.ts +3 -0
  53. package/dist/components/Grid/index.d.ts.map +1 -0
  54. package/dist/components/Input/Input.d.ts +18 -0
  55. package/dist/components/Input/Input.d.ts.map +1 -0
  56. package/dist/components/Input/index.d.ts +3 -0
  57. package/dist/components/Input/index.d.ts.map +1 -0
  58. package/dist/components/Link/Link.d.ts +12 -0
  59. package/dist/components/Link/Link.d.ts.map +1 -0
  60. package/dist/components/Link/index.d.ts +3 -0
  61. package/dist/components/Link/index.d.ts.map +1 -0
  62. package/dist/components/Nav/Nav.d.ts +19 -0
  63. package/dist/components/Nav/Nav.d.ts.map +1 -0
  64. package/dist/components/Nav/index.d.ts +3 -0
  65. package/dist/components/Nav/index.d.ts.map +1 -0
  66. package/dist/components/Progress/Progress.d.ts +14 -0
  67. package/dist/components/Progress/Progress.d.ts.map +1 -0
  68. package/dist/components/Progress/index.d.ts +3 -0
  69. package/dist/components/Progress/index.d.ts.map +1 -0
  70. package/dist/components/Radio/Radio.d.ts +22 -0
  71. package/dist/components/Radio/Radio.d.ts.map +1 -0
  72. package/dist/components/Radio/index.d.ts +3 -0
  73. package/dist/components/Radio/index.d.ts.map +1 -0
  74. package/dist/components/Section/Section.d.ts +12 -0
  75. package/dist/components/Section/Section.d.ts.map +1 -0
  76. package/dist/components/Section/index.d.ts +3 -0
  77. package/dist/components/Section/index.d.ts.map +1 -0
  78. package/dist/components/Select/Select.d.ts +24 -0
  79. package/dist/components/Select/Select.d.ts.map +1 -0
  80. package/dist/components/Select/index.d.ts +3 -0
  81. package/dist/components/Select/index.d.ts.map +1 -0
  82. package/dist/components/Skeleton/Skeleton.d.ts +14 -0
  83. package/dist/components/Skeleton/Skeleton.d.ts.map +1 -0
  84. package/dist/components/Skeleton/index.d.ts +3 -0
  85. package/dist/components/Skeleton/index.d.ts.map +1 -0
  86. package/dist/components/Slider/Slider.d.ts +20 -0
  87. package/dist/components/Slider/Slider.d.ts.map +1 -0
  88. package/dist/components/Slider/index.d.ts +3 -0
  89. package/dist/components/Slider/index.d.ts.map +1 -0
  90. package/dist/components/Spinner/Spinner.d.ts +10 -0
  91. package/dist/components/Spinner/Spinner.d.ts.map +1 -0
  92. package/dist/components/Spinner/index.d.ts +3 -0
  93. package/dist/components/Spinner/index.d.ts.map +1 -0
  94. package/dist/components/Stack/Stack.d.ts +18 -0
  95. package/dist/components/Stack/Stack.d.ts.map +1 -0
  96. package/dist/components/Stack/index.d.ts +3 -0
  97. package/dist/components/Stack/index.d.ts.map +1 -0
  98. package/dist/components/Switch/Switch.d.ts +18 -0
  99. package/dist/components/Switch/Switch.d.ts.map +1 -0
  100. package/dist/components/Switch/index.d.ts +3 -0
  101. package/dist/components/Switch/index.d.ts.map +1 -0
  102. package/dist/components/Table/Table.d.ts +24 -0
  103. package/dist/components/Table/Table.d.ts.map +1 -0
  104. package/dist/components/Table/index.d.ts +3 -0
  105. package/dist/components/Table/index.d.ts.map +1 -0
  106. package/dist/components/Tabs/Tabs.d.ts +19 -0
  107. package/dist/components/Tabs/Tabs.d.ts.map +1 -0
  108. package/dist/components/Tabs/index.d.ts +3 -0
  109. package/dist/components/Tabs/index.d.ts.map +1 -0
  110. package/dist/components/Tag/Tag.d.ts +18 -0
  111. package/dist/components/Tag/Tag.d.ts.map +1 -0
  112. package/dist/components/Tag/index.d.ts +3 -0
  113. package/dist/components/Tag/index.d.ts.map +1 -0
  114. package/dist/components/Textarea/Textarea.d.ts +22 -0
  115. package/dist/components/Textarea/Textarea.d.ts.map +1 -0
  116. package/dist/components/Textarea/index.d.ts +3 -0
  117. package/dist/components/Textarea/index.d.ts.map +1 -0
  118. package/dist/components/Toast/Toast.d.ts +33 -0
  119. package/dist/components/Toast/Toast.d.ts.map +1 -0
  120. package/dist/components/Toast/index.d.ts +3 -0
  121. package/dist/components/Toast/index.d.ts.map +1 -0
  122. package/dist/components/Tooltip/Tooltip.d.ts +16 -0
  123. package/dist/components/Tooltip/Tooltip.d.ts.map +1 -0
  124. package/dist/components/Tooltip/index.d.ts +3 -0
  125. package/dist/components/Tooltip/index.d.ts.map +1 -0
  126. package/dist/css/strand-ui.css +2464 -0
  127. package/dist/index.d.ts +64 -0
  128. package/dist/index.d.ts.map +1 -0
  129. package/dist/test-setup.d.ts +2 -0
  130. package/dist/test-setup.d.ts.map +1 -0
  131. package/package.json +25 -11
  132. package/src/__tests__/build-output.test.ts +200 -0
  133. package/src/__tests__/design-language.test.ts +137 -0
  134. package/src/__tests__/static.test.tsx +60 -0
  135. package/src/components/Alert/Alert.css +75 -0
  136. package/src/components/Alert/Alert.test.tsx +92 -0
  137. package/src/components/Alert/Alert.tsx +59 -0
  138. package/src/components/Alert/index.ts +2 -0
  139. package/src/components/Avatar/Avatar.css +55 -0
  140. package/src/components/Avatar/Avatar.test.tsx +123 -0
  141. package/src/components/Avatar/Avatar.tsx +67 -0
  142. package/src/components/Avatar/index.ts +2 -0
  143. package/src/components/Badge/Badge.css +72 -0
  144. package/src/components/Badge/Badge.test.tsx +121 -0
  145. package/src/components/Badge/Badge.tsx +92 -0
  146. package/src/components/Badge/index.ts +2 -0
  147. package/src/components/Breadcrumb/Breadcrumb.css +50 -0
  148. package/src/components/Breadcrumb/Breadcrumb.test.tsx +107 -0
  149. package/src/components/Breadcrumb/Breadcrumb.tsx +59 -0
  150. package/src/components/Breadcrumb/index.ts +2 -0
  151. package/src/components/Button/Button.css +195 -0
  152. package/src/components/Button/Button.test.tsx +171 -0
  153. package/src/components/Button/Button.tsx +78 -0
  154. package/src/components/Button/index.ts +2 -0
  155. package/src/components/Card/Card.css +68 -0
  156. package/src/components/Card/Card.test.tsx +90 -0
  157. package/src/components/Card/Card.tsx +41 -0
  158. package/src/components/Card/index.ts +2 -0
  159. package/src/components/Checkbox/Checkbox.css +97 -0
  160. package/src/components/Checkbox/Checkbox.test.tsx +92 -0
  161. package/src/components/Checkbox/Checkbox.tsx +137 -0
  162. package/src/components/Checkbox/index.ts +2 -0
  163. package/src/components/Container/Container.css +25 -0
  164. package/src/components/Container/Container.test.tsx +82 -0
  165. package/src/components/Container/Container.tsx +37 -0
  166. package/src/components/Container/index.ts +2 -0
  167. package/src/components/DataReadout/DataReadout.css +30 -0
  168. package/src/components/DataReadout/DataReadout.test.tsx +105 -0
  169. package/src/components/DataReadout/DataReadout.tsx +29 -0
  170. package/src/components/DataReadout/index.ts +2 -0
  171. package/src/components/Dialog/Dialog.css +81 -0
  172. package/src/components/Dialog/Dialog.test.tsx +203 -0
  173. package/src/components/Dialog/Dialog.tsx +179 -0
  174. package/src/components/Dialog/index.ts +2 -0
  175. package/src/components/Divider/Divider.css +44 -0
  176. package/src/components/Divider/Divider.test.tsx +86 -0
  177. package/src/components/Divider/Divider.tsx +81 -0
  178. package/src/components/Divider/index.ts +2 -0
  179. package/src/components/FormField/FormField.css +47 -0
  180. package/src/components/FormField/FormField.test.tsx +99 -0
  181. package/src/components/FormField/FormField.tsx +79 -0
  182. package/src/components/FormField/index.ts +2 -0
  183. package/src/components/Grid/Grid.css +27 -0
  184. package/src/components/Grid/Grid.test.tsx +86 -0
  185. package/src/components/Grid/Grid.tsx +45 -0
  186. package/src/components/Grid/index.ts +2 -0
  187. package/src/components/Input/Input.css +87 -0
  188. package/src/components/Input/Input.test.tsx +95 -0
  189. package/src/components/Input/Input.tsx +69 -0
  190. package/src/components/Input/index.ts +2 -0
  191. package/src/components/Link/Link.css +30 -0
  192. package/src/components/Link/Link.test.tsx +88 -0
  193. package/src/components/Link/Link.tsx +31 -0
  194. package/src/components/Link/index.ts +2 -0
  195. package/src/components/Nav/Nav.css +179 -0
  196. package/src/components/Nav/Nav.test.tsx +174 -0
  197. package/src/components/Nav/Nav.tsx +101 -0
  198. package/src/components/Nav/index.ts +2 -0
  199. package/src/components/Progress/Progress.css +93 -0
  200. package/src/components/Progress/Progress.test.tsx +93 -0
  201. package/src/components/Progress/Progress.tsx +104 -0
  202. package/src/components/Progress/index.ts +2 -0
  203. package/src/components/Radio/Radio.css +98 -0
  204. package/src/components/Radio/Radio.test.tsx +80 -0
  205. package/src/components/Radio/Radio.tsx +72 -0
  206. package/src/components/Radio/index.ts +2 -0
  207. package/src/components/Section/Section.css +28 -0
  208. package/src/components/Section/Section.test.tsx +100 -0
  209. package/src/components/Section/Section.tsx +41 -0
  210. package/src/components/Section/index.ts +2 -0
  211. package/src/components/Select/Select.css +75 -0
  212. package/src/components/Select/Select.test.tsx +99 -0
  213. package/src/components/Select/Select.tsx +78 -0
  214. package/src/components/Select/index.ts +2 -0
  215. package/src/components/Skeleton/Skeleton.css +52 -0
  216. package/src/components/Skeleton/Skeleton.test.tsx +96 -0
  217. package/src/components/Skeleton/Skeleton.tsx +55 -0
  218. package/src/components/Skeleton/index.ts +2 -0
  219. package/src/components/Slider/Slider.css +107 -0
  220. package/src/components/Slider/Slider.test.tsx +85 -0
  221. package/src/components/Slider/Slider.tsx +66 -0
  222. package/src/components/Slider/index.ts +2 -0
  223. package/src/components/Spinner/Spinner.css +61 -0
  224. package/src/components/Spinner/Spinner.test.tsx +56 -0
  225. package/src/components/Spinner/Spinner.tsx +38 -0
  226. package/src/components/Spinner/index.ts +2 -0
  227. package/src/components/Stack/Stack.css +71 -0
  228. package/src/components/Stack/Stack.test.tsx +130 -0
  229. package/src/components/Stack/Stack.tsx +77 -0
  230. package/src/components/Stack/index.ts +2 -0
  231. package/src/components/Switch/Switch.css +94 -0
  232. package/src/components/Switch/Switch.test.tsx +98 -0
  233. package/src/components/Switch/Switch.tsx +80 -0
  234. package/src/components/Switch/index.ts +2 -0
  235. package/src/components/Table/Table.css +83 -0
  236. package/src/components/Table/Table.test.tsx +134 -0
  237. package/src/components/Table/Table.tsx +102 -0
  238. package/src/components/Table/index.ts +2 -0
  239. package/src/components/Tabs/Tabs.css +51 -0
  240. package/src/components/Tabs/Tabs.test.tsx +164 -0
  241. package/src/components/Tabs/Tabs.tsx +126 -0
  242. package/src/components/Tabs/index.ts +2 -0
  243. package/src/components/Tag/Tag.css +98 -0
  244. package/src/components/Tag/Tag.test.tsx +112 -0
  245. package/src/components/Tag/Tag.tsx +73 -0
  246. package/src/components/Tag/index.ts +2 -0
  247. package/src/components/Textarea/Textarea.css +80 -0
  248. package/src/components/Textarea/Textarea.test.tsx +89 -0
  249. package/src/components/Textarea/Textarea.tsx +102 -0
  250. package/src/components/Textarea/index.ts +2 -0
  251. package/src/components/Toast/Toast.css +103 -0
  252. package/src/components/Toast/Toast.test.tsx +219 -0
  253. package/src/components/Toast/Toast.tsx +177 -0
  254. package/src/components/Toast/index.ts +2 -0
  255. package/src/components/Tooltip/Tooltip.css +63 -0
  256. package/src/components/Tooltip/Tooltip.test.tsx +196 -0
  257. package/src/components/Tooltip/Tooltip.tsx +89 -0
  258. package/src/components/Tooltip/index.ts +2 -0
  259. package/src/index.ts +99 -0
  260. package/src/static.css +47 -0
  261. package/src/test-setup.ts +7 -0
@@ -0,0 +1,107 @@
1
+ /*! Strand UI | MIT License | dillingerstaffing.com */
2
+
3
+ /* ── Base ── */
4
+ .strand-slider {
5
+ position: relative;
6
+ display: flex;
7
+ align-items: center;
8
+ width: 100%;
9
+ }
10
+
11
+ /* ── Field ── */
12
+ .strand-slider__field {
13
+ width: 100%;
14
+ height: 6px;
15
+ appearance: none;
16
+ background: var(--strand-gray-200);
17
+ border-radius: var(--strand-radius-full);
18
+ outline: none;
19
+ cursor: pointer;
20
+ transition: background var(--strand-duration-fast) var(--strand-ease-out-quart);
21
+ }
22
+
23
+ /* ── Thumb: Webkit ── */
24
+ .strand-slider__field::-webkit-slider-thumb {
25
+ appearance: none;
26
+ width: 20px;
27
+ height: 20px;
28
+ background: var(--strand-blue-primary);
29
+ border: 2px solid var(--strand-surface-elevated);
30
+ border-radius: var(--strand-radius-full);
31
+ cursor: pointer;
32
+ box-shadow: var(--strand-elevation-1);
33
+ transition:
34
+ background var(--strand-duration-fast) var(--strand-ease-out-quart),
35
+ transform var(--strand-duration-fast) var(--strand-ease-out-expo);
36
+ }
37
+
38
+ .strand-slider__field:hover:not(:disabled)::-webkit-slider-thumb {
39
+ background: var(--strand-blue-vivid);
40
+ transform: scale(1.15);
41
+ }
42
+
43
+ .strand-slider__field:active:not(:disabled)::-webkit-slider-thumb {
44
+ background: var(--strand-blue-deep);
45
+ transform: scale(1.05);
46
+ }
47
+
48
+ /* ── Thumb: Firefox ── */
49
+ .strand-slider__field::-moz-range-thumb {
50
+ width: 20px;
51
+ height: 20px;
52
+ background: var(--strand-blue-primary);
53
+ border: 2px solid var(--strand-surface-elevated);
54
+ border-radius: var(--strand-radius-full);
55
+ cursor: pointer;
56
+ box-shadow: var(--strand-elevation-1);
57
+ transition:
58
+ background var(--strand-duration-fast) var(--strand-ease-out-quart),
59
+ transform var(--strand-duration-fast) var(--strand-ease-out-expo);
60
+ }
61
+
62
+ .strand-slider__field:hover:not(:disabled)::-moz-range-thumb {
63
+ background: var(--strand-blue-vivid);
64
+ transform: scale(1.15);
65
+ }
66
+
67
+ .strand-slider__field:active:not(:disabled)::-moz-range-thumb {
68
+ background: var(--strand-blue-deep);
69
+ transform: scale(1.05);
70
+ }
71
+
72
+ /* ── Track: Firefox ── */
73
+ .strand-slider__field::-moz-range-track {
74
+ height: 6px;
75
+ background: var(--strand-gray-200);
76
+ border-radius: var(--strand-radius-full);
77
+ }
78
+
79
+ /* ── Focus ── */
80
+ .strand-slider__field:focus-visible::-webkit-slider-thumb {
81
+ box-shadow: var(--strand-focus-ring);
82
+ }
83
+
84
+ .strand-slider__field:focus-visible::-moz-range-thumb {
85
+ box-shadow: var(--strand-focus-ring);
86
+ }
87
+
88
+ /* ── Disabled ── */
89
+ .strand-slider--disabled {
90
+ opacity: 0.4;
91
+ cursor: not-allowed;
92
+ }
93
+
94
+ .strand-slider--disabled .strand-slider__field {
95
+ cursor: not-allowed;
96
+ }
97
+
98
+ /* ── Reduced motion ── */
99
+ @media (prefers-reduced-motion: reduce) {
100
+ .strand-slider__field::-webkit-slider-thumb {
101
+ transition: none;
102
+ }
103
+
104
+ .strand-slider__field::-moz-range-thumb {
105
+ transition: none;
106
+ }
107
+ }
@@ -0,0 +1,85 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { render, fireEvent } from "@testing-library/preact";
3
+ import { Slider } from "./Slider.js";
4
+
5
+ describe("Slider", () => {
6
+ it("renders a range input", () => {
7
+ const { getByRole } = render(<Slider aria-label="Volume" />);
8
+ expect(getByRole("slider")).toBeTruthy();
9
+ });
10
+
11
+ it("has correct default min/max/step", () => {
12
+ const { getByRole } = render(<Slider aria-label="Volume" />);
13
+ const slider = getByRole("slider");
14
+ expect(slider).toHaveAttribute("min", "0");
15
+ expect(slider).toHaveAttribute("max", "100");
16
+ expect(slider).toHaveAttribute("step", "1");
17
+ });
18
+
19
+ it("accepts custom min/max/step", () => {
20
+ const { getByRole } = render(
21
+ <Slider min={10} max={50} step={5} aria-label="Volume" />
22
+ );
23
+ const slider = getByRole("slider");
24
+ expect(slider).toHaveAttribute("min", "10");
25
+ expect(slider).toHaveAttribute("max", "50");
26
+ expect(slider).toHaveAttribute("step", "5");
27
+ });
28
+
29
+ it("calls onChange on input", () => {
30
+ const onChange = vi.fn();
31
+ const { getByRole } = render(
32
+ <Slider onChange={onChange} aria-label="Volume" />
33
+ );
34
+ fireEvent.change(getByRole("slider"), { target: { value: "50" } });
35
+ expect(onChange).toHaveBeenCalled();
36
+ });
37
+
38
+ it("sets disabled attribute when disabled", () => {
39
+ const { getByRole } = render(<Slider disabled aria-label="Volume" />);
40
+ expect(getByRole("slider")).toBeDisabled();
41
+ });
42
+
43
+ it("applies disabled class to wrapper when disabled", () => {
44
+ const { container } = render(<Slider disabled aria-label="Volume" />);
45
+ expect(container.querySelector(".strand-slider--disabled")).toBeTruthy();
46
+ });
47
+
48
+ it("sets value on the input", () => {
49
+ const { getByRole } = render(<Slider value={42} aria-label="Volume" />);
50
+ expect((getByRole("slider") as HTMLInputElement).value).toBe("42");
51
+ });
52
+
53
+ it("merges custom className", () => {
54
+ const { container } = render(
55
+ <Slider className="custom" aria-label="Volume" />
56
+ );
57
+ expect(container.querySelector(".strand-slider")?.className).toContain("custom");
58
+ });
59
+
60
+ it("has aria-valuemin attribute", () => {
61
+ const { getByRole } = render(<Slider min={5} aria-label="Volume" />);
62
+ expect(getByRole("slider")).toHaveAttribute("aria-valuemin", "5");
63
+ });
64
+
65
+ it("has aria-valuemax attribute", () => {
66
+ const { getByRole } = render(<Slider max={200} aria-label="Volume" />);
67
+ expect(getByRole("slider")).toHaveAttribute("aria-valuemax", "200");
68
+ });
69
+
70
+ it("has aria-valuenow attribute", () => {
71
+ const { getByRole } = render(<Slider value={75} aria-label="Volume" />);
72
+ expect(getByRole("slider")).toHaveAttribute("aria-valuenow", "75");
73
+ });
74
+
75
+ it("forwards ref to input element", () => {
76
+ let inputEl: HTMLInputElement | null = null;
77
+ render(
78
+ <Slider
79
+ aria-label="Volume"
80
+ ref={(el: HTMLInputElement | null) => { inputEl = el; }}
81
+ />
82
+ );
83
+ expect(inputEl).toBeInstanceOf(HTMLInputElement);
84
+ });
85
+ });
@@ -0,0 +1,66 @@
1
+ /*! Strand UI | MIT License | dillingerstaffing.com */
2
+
3
+ import type { JSX } from "preact";
4
+ import { forwardRef } from "preact/compat";
5
+
6
+ export interface SliderProps
7
+ extends Omit<JSX.HTMLAttributes<HTMLInputElement>, "type" | "onChange" | "value"> {
8
+ /** Minimum value */
9
+ min?: number;
10
+ /** Maximum value */
11
+ max?: number;
12
+ /** Step increment */
13
+ step?: number;
14
+ /** Current value */
15
+ value?: number;
16
+ /** Change handler */
17
+ onChange?: (e: JSX.TargetedEvent<HTMLInputElement>) => void;
18
+ /** Disabled state */
19
+ disabled?: boolean;
20
+ }
21
+
22
+ export const Slider = forwardRef<HTMLInputElement, SliderProps>(
23
+ (
24
+ {
25
+ min = 0,
26
+ max = 100,
27
+ step = 1,
28
+ value,
29
+ onChange,
30
+ disabled,
31
+ className = "",
32
+ ...rest
33
+ },
34
+ ref,
35
+ ) => {
36
+ const wrapperClasses = [
37
+ "strand-slider",
38
+ disabled && "strand-slider--disabled",
39
+ className,
40
+ ]
41
+ .filter(Boolean)
42
+ .join(" ");
43
+
44
+ return (
45
+ <div className={wrapperClasses}>
46
+ <input
47
+ ref={ref}
48
+ type="range"
49
+ className="strand-slider__field"
50
+ min={min}
51
+ max={max}
52
+ step={step}
53
+ value={value}
54
+ onChange={onChange}
55
+ disabled={disabled}
56
+ aria-valuemin={min}
57
+ aria-valuemax={max}
58
+ aria-valuenow={value}
59
+ {...rest}
60
+ />
61
+ </div>
62
+ );
63
+ },
64
+ );
65
+
66
+ Slider.displayName = "Slider";
@@ -0,0 +1,2 @@
1
+ export { Slider } from "./Slider.js";
2
+ export type { SliderProps } from "./Slider.js";
@@ -0,0 +1,61 @@
1
+ /*! Strand UI | MIT License | dillingerstaffing.com */
2
+
3
+ /* ── Base ── */
4
+ .strand-spinner {
5
+ display: inline-flex;
6
+ align-items: center;
7
+ justify-content: center;
8
+ }
9
+
10
+ /* ── Ring ── */
11
+ .strand-spinner__ring {
12
+ display: block;
13
+ border: 2px solid var(--strand-gray-200);
14
+ border-top-color: var(--strand-blue-primary);
15
+ border-radius: var(--strand-radius-full);
16
+ animation: strand-spinner-rotate 0.8s linear infinite;
17
+ }
18
+
19
+ /* ── Sizes ── */
20
+ .strand-spinner--sm .strand-spinner__ring {
21
+ width: 16px;
22
+ height: 16px;
23
+ }
24
+
25
+ .strand-spinner--md .strand-spinner__ring {
26
+ width: 20px;
27
+ height: 20px;
28
+ }
29
+
30
+ .strand-spinner--lg .strand-spinner__ring {
31
+ width: 32px;
32
+ height: 32px;
33
+ }
34
+
35
+ /* ── Animation ── */
36
+ @keyframes strand-spinner-rotate {
37
+ to {
38
+ transform: rotate(360deg);
39
+ }
40
+ }
41
+
42
+ /* ── Screen reader only text ── */
43
+ .strand-spinner__sr-only {
44
+ position: absolute;
45
+ width: 1px;
46
+ height: 1px;
47
+ padding: 0;
48
+ margin: -1px;
49
+ overflow: hidden;
50
+ clip: rect(0, 0, 0, 0);
51
+ white-space: nowrap;
52
+ border: 0;
53
+ }
54
+
55
+ /* ── Reduced motion ── */
56
+ @media (prefers-reduced-motion: reduce) {
57
+ .strand-spinner__ring {
58
+ animation: none;
59
+ border-style: dotted;
60
+ }
61
+ }
@@ -0,0 +1,56 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { render } from "@testing-library/preact";
3
+ import { Spinner } from "./Spinner.js";
4
+
5
+ describe("Spinner", () => {
6
+ // ── ARIA ──
7
+
8
+ it("renders with status role", () => {
9
+ const { getByRole } = render(<Spinner />);
10
+ expect(getByRole("status")).toBeTruthy();
11
+ });
12
+
13
+ it("has loading text for screen readers", () => {
14
+ const { getByRole } = render(<Spinner />);
15
+ expect(getByRole("status")).toHaveTextContent("Loading");
16
+ });
17
+
18
+ // ── Sizes ──
19
+
20
+ it("applies md size class by default", () => {
21
+ const { getByRole } = render(<Spinner />);
22
+ expect(getByRole("status").className).toContain("strand-spinner--md");
23
+ });
24
+
25
+ it("applies sm size class", () => {
26
+ const { getByRole } = render(<Spinner size="sm" />);
27
+ expect(getByRole("status").className).toContain("strand-spinner--sm");
28
+ });
29
+
30
+ it("applies lg size class", () => {
31
+ const { getByRole } = render(<Spinner size="lg" />);
32
+ expect(getByRole("status").className).toContain("strand-spinner--lg");
33
+ });
34
+
35
+ // ── Animation ──
36
+
37
+ it("has spinning ring element", () => {
38
+ const { container } = render(<Spinner />);
39
+ expect(container.querySelector(".strand-spinner__ring")).toBeTruthy();
40
+ });
41
+
42
+ it("ring element is aria-hidden", () => {
43
+ const { container } = render(<Spinner />);
44
+ const ring = container.querySelector(".strand-spinner__ring");
45
+ expect(ring?.getAttribute("aria-hidden")).toBe("true");
46
+ });
47
+
48
+ // ── Custom className ──
49
+
50
+ it("merges custom className", () => {
51
+ const { getByRole } = render(<Spinner className="custom" />);
52
+ const el = getByRole("status");
53
+ expect(el.className).toContain("strand-spinner");
54
+ expect(el.className).toContain("custom");
55
+ });
56
+ });
@@ -0,0 +1,38 @@
1
+ /*! Strand UI | MIT License | dillingerstaffing.com */
2
+
3
+ import type { JSX } from "preact";
4
+ import { forwardRef } from "preact/compat";
5
+
6
+ export interface SpinnerProps
7
+ extends Omit<JSX.HTMLAttributes<HTMLSpanElement>, "size"> {
8
+ /** Size of the spinner */
9
+ size?: "sm" | "md" | "lg";
10
+ }
11
+
12
+ export const Spinner = forwardRef<HTMLSpanElement, SpinnerProps>(
13
+ (
14
+ {
15
+ size = "md",
16
+ className = "",
17
+ ...rest
18
+ },
19
+ ref,
20
+ ) => {
21
+ const classes = [
22
+ "strand-spinner",
23
+ `strand-spinner--${size}`,
24
+ className,
25
+ ]
26
+ .filter(Boolean)
27
+ .join(" ");
28
+
29
+ return (
30
+ <span ref={ref} className={classes} role="status" {...rest}>
31
+ <span className="strand-spinner__ring" aria-hidden="true" />
32
+ <span className="strand-spinner__sr-only">Loading</span>
33
+ </span>
34
+ );
35
+ },
36
+ );
37
+
38
+ Spinner.displayName = "Spinner";
@@ -0,0 +1,2 @@
1
+ export { Spinner } from "./Spinner.js";
2
+ export type { SpinnerProps } from "./Spinner.js";
@@ -0,0 +1,71 @@
1
+ /*! Strand UI | MIT License | dillingerstaffing.com */
2
+
3
+ /* ── Base ── */
4
+ .strand-stack {
5
+ display: flex;
6
+ max-width: 100%;
7
+ box-sizing: border-box;
8
+ }
9
+
10
+ .strand-stack > * {
11
+ min-width: 0;
12
+ }
13
+
14
+ /* ── Direction ── */
15
+ .strand-stack--vertical {
16
+ flex-direction: column;
17
+ }
18
+
19
+ .strand-stack--horizontal {
20
+ flex-direction: row;
21
+ }
22
+
23
+ /* ── Alignment ── */
24
+ .strand-stack--align-start {
25
+ align-items: flex-start;
26
+ }
27
+
28
+ .strand-stack--align-center {
29
+ align-items: center;
30
+ }
31
+
32
+ .strand-stack--align-end {
33
+ align-items: flex-end;
34
+ }
35
+
36
+ /* Default stretch handled by flexbox default */
37
+
38
+ /* ── Justification ── */
39
+ .strand-stack--justify-start {
40
+ justify-content: flex-start;
41
+ }
42
+
43
+ .strand-stack--justify-center {
44
+ justify-content: center;
45
+ }
46
+
47
+ .strand-stack--justify-end {
48
+ justify-content: flex-end;
49
+ }
50
+
51
+ .strand-stack--justify-between {
52
+ justify-content: space-between;
53
+ }
54
+
55
+ .strand-stack--justify-around {
56
+ justify-content: space-around;
57
+ }
58
+
59
+ /* ── Wrap ── */
60
+ .strand-stack--wrap {
61
+ flex-wrap: wrap;
62
+ }
63
+
64
+ /* ── Gap utilities ── */
65
+ .strand-stack--gap-1 { gap: var(--strand-space-1); }
66
+ .strand-stack--gap-2 { gap: var(--strand-space-2); }
67
+ .strand-stack--gap-3 { gap: var(--strand-space-3); }
68
+ .strand-stack--gap-4 { gap: var(--strand-space-4); }
69
+ .strand-stack--gap-5 { gap: var(--strand-space-5); }
70
+ .strand-stack--gap-6 { gap: var(--strand-space-6); }
71
+ .strand-stack--gap-8 { gap: var(--strand-space-8); }
@@ -0,0 +1,130 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { render } from "@testing-library/preact";
3
+ import { Stack } from "./Stack.js";
4
+
5
+ describe("Stack", () => {
6
+ // ── Rendering ──
7
+
8
+ it("renders a div element", () => {
9
+ const { container } = render(<Stack>content</Stack>);
10
+ expect(container.firstElementChild?.tagName).toBe("DIV");
11
+ });
12
+
13
+ it("renders children", () => {
14
+ const { getByText } = render(
15
+ <Stack>
16
+ <span>Child A</span>
17
+ <span>Child B</span>
18
+ </Stack>,
19
+ );
20
+ expect(getByText("Child A")).toBeTruthy();
21
+ expect(getByText("Child B")).toBeTruthy();
22
+ });
23
+
24
+ // ── Direction ──
25
+
26
+ it("applies vertical direction class by default", () => {
27
+ const { container } = render(<Stack>content</Stack>);
28
+ expect(container.firstElementChild?.className).toContain(
29
+ "strand-stack--vertical",
30
+ );
31
+ });
32
+
33
+ it("applies horizontal direction class", () => {
34
+ const { container } = render(
35
+ <Stack direction="horizontal">content</Stack>,
36
+ );
37
+ expect(container.firstElementChild?.className).toContain(
38
+ "strand-stack--horizontal",
39
+ );
40
+ });
41
+
42
+ // ── Gap ──
43
+
44
+ it("applies default gap as inline style", () => {
45
+ const { container } = render(<Stack>content</Stack>);
46
+ const el = container.firstElementChild as HTMLElement;
47
+ expect(el.style.gap).toBe("var(--strand-space-4)");
48
+ });
49
+
50
+ it("applies custom gap as inline style", () => {
51
+ const { container } = render(<Stack gap={8}>content</Stack>);
52
+ const el = container.firstElementChild as HTMLElement;
53
+ expect(el.style.gap).toBe("var(--strand-space-8)");
54
+ });
55
+
56
+ // ── Alignment ──
57
+
58
+ it("applies align-center class", () => {
59
+ const { container } = render(<Stack align="center">content</Stack>);
60
+ expect(container.firstElementChild?.className).toContain(
61
+ "strand-stack--align-center",
62
+ );
63
+ });
64
+
65
+ it("applies align-start class", () => {
66
+ const { container } = render(<Stack align="start">content</Stack>);
67
+ expect(container.firstElementChild?.className).toContain(
68
+ "strand-stack--align-start",
69
+ );
70
+ });
71
+
72
+ it("does not apply alignment class for default stretch", () => {
73
+ const { container } = render(<Stack>content</Stack>);
74
+ expect(container.firstElementChild?.className).not.toContain(
75
+ "strand-stack--align-",
76
+ );
77
+ });
78
+
79
+ // ── Justification ──
80
+
81
+ it("applies justify-between class", () => {
82
+ const { container } = render(<Stack justify="between">content</Stack>);
83
+ expect(container.firstElementChild?.className).toContain(
84
+ "strand-stack--justify-between",
85
+ );
86
+ });
87
+
88
+ it("applies justify-center class", () => {
89
+ const { container } = render(<Stack justify="center">content</Stack>);
90
+ expect(container.firstElementChild?.className).toContain(
91
+ "strand-stack--justify-center",
92
+ );
93
+ });
94
+
95
+ // ── Wrap ──
96
+
97
+ it("applies wrap class when wrap is true", () => {
98
+ const { container } = render(<Stack wrap>content</Stack>);
99
+ expect(container.firstElementChild?.className).toContain(
100
+ "strand-stack--wrap",
101
+ );
102
+ });
103
+
104
+ it("does not apply wrap class by default", () => {
105
+ const { container } = render(<Stack>content</Stack>);
106
+ expect(container.firstElementChild?.className).not.toContain(
107
+ "strand-stack--wrap",
108
+ );
109
+ });
110
+
111
+ // ── Custom className ──
112
+
113
+ it("merges custom className", () => {
114
+ const { container } = render(<Stack className="custom">content</Stack>);
115
+ const el = container.firstElementChild;
116
+ expect(el?.className).toContain("strand-stack");
117
+ expect(el?.className).toContain("custom");
118
+ });
119
+
120
+ // ── Props forwarding ──
121
+
122
+ it("forwards additional props", () => {
123
+ const { container } = render(
124
+ <Stack data-testid="my-stack" id="s1">
125
+ content
126
+ </Stack>,
127
+ );
128
+ expect(container.firstElementChild).toHaveAttribute("id", "s1");
129
+ });
130
+ });
@@ -0,0 +1,77 @@
1
+ /*! Strand UI | MIT License | dillingerstaffing.com */
2
+
3
+ import type { JSX } from "preact";
4
+ import { forwardRef } from "preact/compat";
5
+
6
+ export interface StackProps extends JSX.HTMLAttributes<HTMLDivElement> {
7
+ /** Flex direction */
8
+ direction?: "vertical" | "horizontal";
9
+ /** Gap between items, maps to --strand-space-{n} */
10
+ gap?: number;
11
+ /** Cross-axis alignment */
12
+ align?: "start" | "center" | "end" | "stretch";
13
+ /** Main-axis alignment */
14
+ justify?: "start" | "center" | "end" | "between" | "around";
15
+ /** Enable flex-wrap */
16
+ wrap?: boolean;
17
+ }
18
+
19
+ const ALIGN_MAP: Record<string, string> = {
20
+ start: "flex-start",
21
+ center: "center",
22
+ end: "flex-end",
23
+ stretch: "stretch",
24
+ };
25
+
26
+ const JUSTIFY_MAP: Record<string, string> = {
27
+ start: "flex-start",
28
+ center: "center",
29
+ end: "flex-end",
30
+ between: "space-between",
31
+ around: "space-around",
32
+ };
33
+
34
+ export const Stack = forwardRef<HTMLDivElement, StackProps>(
35
+ (
36
+ {
37
+ direction = "vertical",
38
+ gap = 4,
39
+ align = "stretch",
40
+ wrap = false,
41
+ justify,
42
+ className = "",
43
+ style,
44
+ children,
45
+ ...rest
46
+ },
47
+ ref,
48
+ ) => {
49
+ const classes = [
50
+ "strand-stack",
51
+ `strand-stack--${direction}`,
52
+ align !== "stretch" && `strand-stack--align-${align}`,
53
+ justify && `strand-stack--justify-${justify}`,
54
+ wrap && "strand-stack--wrap",
55
+ className,
56
+ ]
57
+ .filter(Boolean)
58
+ .join(" ");
59
+
60
+ const inlineStyle: Record<string, string> = {
61
+ gap: `var(--strand-space-${gap})`,
62
+ };
63
+
64
+ return (
65
+ <div
66
+ ref={ref}
67
+ className={classes}
68
+ style={{ ...inlineStyle, ...(style as Record<string, string>) }}
69
+ {...rest}
70
+ >
71
+ {children}
72
+ </div>
73
+ );
74
+ },
75
+ );
76
+
77
+ Stack.displayName = "Stack";