@dillingerstaffing/strand-ui 0.6.0 → 0.7.1

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 (32) hide show
  1. package/HTML_REFERENCE.md +64 -1
  2. package/README.md +1 -1
  3. package/dist/components/InstrumentViewport/InstrumentViewport.d.ts +10 -0
  4. package/dist/components/InstrumentViewport/InstrumentViewport.d.ts.map +1 -0
  5. package/dist/components/InstrumentViewport/index.d.ts +3 -0
  6. package/dist/components/InstrumentViewport/index.d.ts.map +1 -0
  7. package/dist/components/ScrollReveal/ScrollReveal.d.ts +12 -0
  8. package/dist/components/ScrollReveal/ScrollReveal.d.ts.map +1 -0
  9. package/dist/components/ScrollReveal/index.d.ts +3 -0
  10. package/dist/components/ScrollReveal/index.d.ts.map +1 -0
  11. package/dist/css/strand-ui.css +104 -24
  12. package/dist/index.d.ts +4 -0
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +568 -527
  15. package/package.json +2 -2
  16. package/src/__tests__/build-output.test.ts +1 -0
  17. package/src/components/Avatar/Avatar.css +2 -2
  18. package/src/components/Button/Button.css +12 -12
  19. package/src/components/Checkbox/Checkbox.css +3 -2
  20. package/src/components/InstrumentViewport/InstrumentViewport.css +36 -0
  21. package/src/components/InstrumentViewport/InstrumentViewport.test.tsx +70 -0
  22. package/src/components/InstrumentViewport/InstrumentViewport.tsx +31 -0
  23. package/src/components/InstrumentViewport/index.ts +2 -0
  24. package/src/components/Nav/Nav.css +2 -2
  25. package/src/components/Radio/Radio.css +3 -2
  26. package/src/components/ScrollReveal/ScrollReveal.css +29 -0
  27. package/src/components/ScrollReveal/ScrollReveal.test.tsx +68 -0
  28. package/src/components/ScrollReveal/ScrollReveal.tsx +64 -0
  29. package/src/components/ScrollReveal/index.ts +2 -0
  30. package/src/components/Slider/Slider.css +10 -4
  31. package/src/components/Switch/Switch.css +1 -0
  32. package/src/index.ts +8 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dillingerstaffing/strand-ui",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "description": "Strand UI - Preact/React component library built on the Strand Design Language",
5
5
  "author": "Dillinger Staffing <engineering@dillingerstaffing.com> (https://dillingerstaffing.com)",
6
6
  "license": "MIT",
@@ -60,7 +60,7 @@
60
60
  }
61
61
  },
62
62
  "dependencies": {
63
- "@dillingerstaffing/strand": "^0.6.0"
63
+ "@dillingerstaffing/strand": "^0.7.1"
64
64
  },
65
65
  "devDependencies": {
66
66
  "@testing-library/preact": "^3.2.0",
@@ -44,6 +44,7 @@ describe("Build output", () => {
44
44
  ".strand-nav", ".strand-toast", ".strand-alert",
45
45
  ".strand-dialog", ".strand-tooltip", ".strand-progress",
46
46
  ".strand-spinner", ".strand-skeleton",
47
+ ".strand-instrument-viewport", ".strand-reveal",
47
48
  ];
48
49
  for (const cls of expectedClasses) {
49
50
  expect(css, `Missing CSS class: ${cls}`).toContain(cls);
@@ -49,7 +49,7 @@
49
49
  }
50
50
 
51
51
  .strand-avatar--xl {
52
- width: 64px;
53
- height: 64px;
52
+ width: var(--strand-space-16);
53
+ height: var(--strand-space-16);
54
54
  font-size: var(--strand-text-lg);
55
55
  }
@@ -35,32 +35,32 @@
35
35
 
36
36
  /* ── Sizes ── */
37
37
  .strand-btn--sm {
38
- padding: var(--strand-space-1) var(--strand-space-3);
38
+ padding: var(--strand-space-2) var(--strand-space-5);
39
39
  font-size: var(--strand-text-sm);
40
- min-height: 32px;
40
+ min-height: var(--strand-touch-target);
41
41
  }
42
42
 
43
43
  .strand-btn--md {
44
- padding: calc(var(--strand-space-2) + var(--strand-space-1) / 2) var(--strand-space-5);
44
+ padding: var(--strand-space-3) var(--strand-space-8);
45
45
  font-size: var(--strand-text-sm);
46
- min-height: 40px;
46
+ min-height: var(--strand-touch-target);
47
47
  }
48
48
 
49
49
  .strand-btn--lg {
50
- padding: var(--strand-space-3) var(--strand-space-6);
50
+ padding: var(--strand-space-3) var(--strand-space-10);
51
51
  font-size: var(--strand-text-base);
52
- min-height: 48px;
52
+ min-height: var(--strand-touch-target);
53
53
  }
54
54
 
55
55
  /* ── Icon-only ── */
56
56
  .strand-btn--icon-only.strand-btn--sm {
57
57
  padding: var(--strand-space-1);
58
- min-width: 32px;
58
+ min-width: var(--strand-touch-target);
59
59
  }
60
60
 
61
61
  .strand-btn--icon-only.strand-btn--md {
62
62
  padding: var(--strand-space-2);
63
- min-width: 40px;
63
+ min-width: var(--strand-touch-target);
64
64
  }
65
65
 
66
66
  .strand-btn--icon-only.strand-btn--lg {
@@ -75,18 +75,18 @@
75
75
 
76
76
  /* ── Primary variant ── */
77
77
  .strand-btn--primary {
78
- background: var(--strand-blue-primary);
79
- color: var(--strand-on-blue-primary);
78
+ background: var(--strand-blue-deep);
79
+ color: var(--strand-on-blue-deep);
80
80
  }
81
81
 
82
82
  .strand-btn--primary:hover:not(:disabled) {
83
- background: var(--strand-blue-vivid);
83
+ background: var(--strand-blue-midnight);
84
84
  transform: translateY(-1px);
85
85
  box-shadow: var(--strand-hover-shadow-primary);
86
86
  }
87
87
 
88
88
  .strand-btn--primary:active:not(:disabled) {
89
- background: var(--strand-blue-deep);
89
+ background: var(--strand-blue-abyss);
90
90
  }
91
91
 
92
92
  /* ── Secondary variant ── */
@@ -5,6 +5,7 @@
5
5
  display: inline-flex;
6
6
  align-items: center;
7
7
  gap: var(--strand-space-2);
8
+ min-height: var(--strand-touch-target);
8
9
  cursor: pointer;
9
10
  user-select: none;
10
11
  font-family: var(--strand-font-sans);
@@ -31,8 +32,8 @@
31
32
  display: flex;
32
33
  align-items: center;
33
34
  justify-content: center;
34
- width: 18px;
35
- height: 18px;
35
+ width: var(--strand-control-size);
36
+ height: var(--strand-control-size);
36
37
  border: 1px solid var(--strand-gray-200);
37
38
  border-radius: var(--strand-radius-sm);
38
39
  background: var(--strand-surface-elevated);
@@ -0,0 +1,36 @@
1
+ /*! Strand UI | MIT License | dillingerstaffing.com */
2
+
3
+ .strand-instrument-viewport {
4
+ background: var(--strand-blue-abyss);
5
+ color: var(--strand-gray-100);
6
+ border-radius: var(--strand-radius-lg);
7
+ overflow: hidden;
8
+ position: relative;
9
+ }
10
+
11
+ .strand-instrument-viewport__label {
12
+ color: var(--strand-gray-400);
13
+ font-family: var(--strand-font-mono);
14
+ font-size: var(--strand-text-xs);
15
+ font-weight: var(--strand-weight-medium);
16
+ letter-spacing: var(--strand-tracking-widest);
17
+ text-transform: uppercase;
18
+ }
19
+
20
+ .strand-instrument-viewport__value {
21
+ color: var(--strand-on-blue-primary);
22
+ font-family: var(--strand-font-mono);
23
+ font-variant-numeric: tabular-nums;
24
+ }
25
+
26
+ /* Optional grid overlay */
27
+ .strand-instrument-viewport--grid::before {
28
+ content: '';
29
+ position: absolute;
30
+ inset: 0;
31
+ background:
32
+ linear-gradient(rgba(59, 142, 246, 0.04) 1px, transparent 1px),
33
+ linear-gradient(90deg, rgba(59, 142, 246, 0.04) 1px, transparent 1px);
34
+ background-size: var(--strand-viewport-grid-size) var(--strand-viewport-grid-size);
35
+ pointer-events: none;
36
+ }
@@ -0,0 +1,70 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { render } from "@testing-library/preact";
3
+ import { InstrumentViewport } from "./InstrumentViewport.js";
4
+
5
+ describe("InstrumentViewport", () => {
6
+ // ── Rendering ──
7
+
8
+ it("renders a div element", () => {
9
+ const { container } = render(
10
+ <InstrumentViewport>Content</InstrumentViewport>,
11
+ );
12
+ const el = container.firstElementChild;
13
+ expect(el?.tagName).toBe("DIV");
14
+ });
15
+
16
+ it("applies base class", () => {
17
+ const { container } = render(
18
+ <InstrumentViewport>Content</InstrumentViewport>,
19
+ );
20
+ const el = container.firstElementChild;
21
+ expect(el?.className).toContain("strand-instrument-viewport");
22
+ });
23
+
24
+ it("renders children", () => {
25
+ const { getByText } = render(
26
+ <InstrumentViewport>Hello viewport</InstrumentViewport>,
27
+ );
28
+ expect(getByText("Hello viewport")).toBeTruthy();
29
+ });
30
+
31
+ // ── Grid modifier ──
32
+
33
+ it("does not apply grid class by default", () => {
34
+ const { container } = render(
35
+ <InstrumentViewport>Content</InstrumentViewport>,
36
+ );
37
+ const el = container.firstElementChild;
38
+ expect(el?.className).not.toContain("strand-instrument-viewport--grid");
39
+ });
40
+
41
+ it("applies grid modifier class when grid prop is true", () => {
42
+ const { container } = render(
43
+ <InstrumentViewport grid>Content</InstrumentViewport>,
44
+ );
45
+ const el = container.firstElementChild;
46
+ expect(el?.className).toContain("strand-instrument-viewport--grid");
47
+ });
48
+
49
+ // ── Custom className ──
50
+
51
+ it("merges custom className with component classes", () => {
52
+ const { container } = render(
53
+ <InstrumentViewport className="custom">Content</InstrumentViewport>,
54
+ );
55
+ const el = container.firstElementChild;
56
+ expect(el?.className).toContain("strand-instrument-viewport");
57
+ expect(el?.className).toContain("custom");
58
+ });
59
+
60
+ // ── Props forwarding ──
61
+
62
+ it("forwards additional props", () => {
63
+ const { container } = render(
64
+ <InstrumentViewport id="vp-1" data-testid="viewport">
65
+ Content
66
+ </InstrumentViewport>,
67
+ );
68
+ expect(container.firstElementChild?.getAttribute("id")).toBe("vp-1");
69
+ });
70
+ });
@@ -0,0 +1,31 @@
1
+ /*! Strand UI | MIT License | dillingerstaffing.com */
2
+
3
+ import type { JSX } from "preact";
4
+ import { forwardRef } from "preact/compat";
5
+
6
+ export interface InstrumentViewportProps
7
+ extends JSX.HTMLAttributes<HTMLDivElement> {
8
+ /** Show subtle grid overlay */
9
+ grid?: boolean;
10
+ }
11
+
12
+ export const InstrumentViewport = forwardRef<
13
+ HTMLDivElement,
14
+ InstrumentViewportProps
15
+ >(({ grid = false, className = "", children, ...rest }, ref) => {
16
+ const classes = [
17
+ "strand-instrument-viewport",
18
+ grid ? "strand-instrument-viewport--grid" : "",
19
+ className,
20
+ ]
21
+ .filter(Boolean)
22
+ .join(" ");
23
+
24
+ return (
25
+ <div ref={ref} className={classes} {...rest}>
26
+ {children}
27
+ </div>
28
+ );
29
+ });
30
+
31
+ InstrumentViewport.displayName = "InstrumentViewport";
@@ -0,0 +1,2 @@
1
+ export { InstrumentViewport } from "./InstrumentViewport.js";
2
+ export type { InstrumentViewportProps } from "./InstrumentViewport.js";
@@ -4,7 +4,7 @@
4
4
  .strand-nav {
5
5
  position: relative;
6
6
  width: 100%;
7
- height: 64px;
7
+ height: var(--strand-nav-height);
8
8
  background: var(--strand-surface-elevated);
9
9
  border-bottom: 1px solid var(--strand-gray-200);
10
10
  font-family: var(--strand-font-sans);
@@ -165,7 +165,7 @@
165
165
 
166
166
  .strand-nav {
167
167
  height: auto;
168
- min-height: 64px;
168
+ min-height: var(--strand-nav-height);
169
169
  }
170
170
  }
171
171
 
@@ -5,6 +5,7 @@
5
5
  display: inline-flex;
6
6
  align-items: center;
7
7
  gap: var(--strand-space-2);
8
+ min-height: var(--strand-touch-target);
8
9
  cursor: pointer;
9
10
  user-select: none;
10
11
  font-family: var(--strand-font-sans);
@@ -31,8 +32,8 @@
31
32
  display: flex;
32
33
  align-items: center;
33
34
  justify-content: center;
34
- width: 18px;
35
- height: 18px;
35
+ width: var(--strand-control-size);
36
+ height: var(--strand-control-size);
36
37
  border: 1px solid var(--strand-gray-200);
37
38
  border-radius: var(--strand-radius-full);
38
39
  background: var(--strand-surface-elevated);
@@ -0,0 +1,29 @@
1
+ /*! Strand UI | MIT License | dillingerstaffing.com */
2
+
3
+ .strand-reveal {
4
+ opacity: 0;
5
+ transform: translateY(24px);
6
+ transition: opacity var(--strand-duration-glacial) var(--strand-ease-out-expo),
7
+ transform var(--strand-duration-glacial) var(--strand-ease-out-expo);
8
+ }
9
+
10
+ .strand-reveal--visible {
11
+ opacity: 1;
12
+ transform: translateY(0);
13
+ }
14
+
15
+ /* Stagger children */
16
+ .strand-reveal-group > .strand-reveal:nth-child(1) { transition-delay: 0ms; }
17
+ .strand-reveal-group > .strand-reveal:nth-child(2) { transition-delay: var(--strand-stagger-delay); }
18
+ .strand-reveal-group > .strand-reveal:nth-child(3) { transition-delay: calc(var(--strand-stagger-delay) * 2); }
19
+ .strand-reveal-group > .strand-reveal:nth-child(4) { transition-delay: calc(var(--strand-stagger-delay) * 3); }
20
+ .strand-reveal-group > .strand-reveal:nth-child(5) { transition-delay: calc(var(--strand-stagger-delay) * 4); }
21
+ .strand-reveal-group > .strand-reveal:nth-child(6) { transition-delay: var(--strand-duration-slow); }
22
+
23
+ @media (prefers-reduced-motion: reduce) {
24
+ .strand-reveal {
25
+ opacity: 1;
26
+ transform: none;
27
+ transition: none;
28
+ }
29
+ }
@@ -0,0 +1,68 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { render } from "@testing-library/preact";
3
+ import { ScrollReveal } from "./ScrollReveal.js";
4
+
5
+ describe("ScrollReveal", () => {
6
+ // ── Rendering ──
7
+
8
+ it("renders a div element", () => {
9
+ const { container } = render(<ScrollReveal>Content</ScrollReveal>);
10
+ const el = container.firstElementChild;
11
+ expect(el?.tagName).toBe("DIV");
12
+ });
13
+
14
+ it("applies strand-reveal class", () => {
15
+ const { container } = render(<ScrollReveal>Content</ScrollReveal>);
16
+ const el = container.firstElementChild;
17
+ expect(el?.className).toContain("strand-reveal");
18
+ });
19
+
20
+ it("renders children", () => {
21
+ const { getByText } = render(<ScrollReveal>Hello reveal</ScrollReveal>);
22
+ expect(getByText("Hello reveal")).toBeTruthy();
23
+ });
24
+
25
+ it("does not apply visible class initially", () => {
26
+ const { container } = render(<ScrollReveal>Content</ScrollReveal>);
27
+ const el = container.firstElementChild;
28
+ expect(el?.className).not.toContain("strand-reveal--visible");
29
+ });
30
+
31
+ // ── Custom className ──
32
+
33
+ it("merges custom className with component classes", () => {
34
+ const { container } = render(
35
+ <ScrollReveal className="custom">Content</ScrollReveal>,
36
+ );
37
+ const el = container.firstElementChild;
38
+ expect(el?.className).toContain("strand-reveal");
39
+ expect(el?.className).toContain("custom");
40
+ });
41
+
42
+ // ── Props forwarding ──
43
+
44
+ it("forwards additional props", () => {
45
+ const { container } = render(
46
+ <ScrollReveal id="reveal-1" data-testid="reveal">
47
+ Content
48
+ </ScrollReveal>,
49
+ );
50
+ expect(container.firstElementChild?.getAttribute("id")).toBe("reveal-1");
51
+ });
52
+
53
+ // ── Props acceptance ──
54
+
55
+ it("accepts threshold prop without error", () => {
56
+ const { container } = render(
57
+ <ScrollReveal threshold={0.5}>Content</ScrollReveal>,
58
+ );
59
+ expect(container.firstElementChild).toBeTruthy();
60
+ });
61
+
62
+ it("accepts once prop without error", () => {
63
+ const { container } = render(
64
+ <ScrollReveal once={false}>Content</ScrollReveal>,
65
+ );
66
+ expect(container.firstElementChild).toBeTruthy();
67
+ });
68
+ });
@@ -0,0 +1,64 @@
1
+ /*! Strand UI | MIT License | dillingerstaffing.com */
2
+
3
+ import type { JSX } from "preact";
4
+ import { useEffect, useRef } from "preact/hooks";
5
+ import { forwardRef } from "preact/compat";
6
+
7
+ export interface ScrollRevealProps
8
+ extends JSX.HTMLAttributes<HTMLDivElement> {
9
+ /** IntersectionObserver visibility threshold (0-1) */
10
+ threshold?: number;
11
+ /** Only trigger reveal once */
12
+ once?: boolean;
13
+ }
14
+
15
+ export const ScrollReveal = forwardRef<HTMLDivElement, ScrollRevealProps>(
16
+ ({ threshold = 0.1, once = true, className = "", children, ...rest }, ref) => {
17
+ const innerRef = useRef<HTMLDivElement>(null);
18
+
19
+ const classes = ["strand-reveal", className].filter(Boolean).join(" ");
20
+
21
+ useEffect(() => {
22
+ const el = innerRef.current;
23
+ if (!el || typeof IntersectionObserver === "undefined") return;
24
+
25
+ const observer = new IntersectionObserver(
26
+ (entries) => {
27
+ for (const entry of entries) {
28
+ if (entry.isIntersecting) {
29
+ entry.target.classList.add("strand-reveal--visible");
30
+ if (once) {
31
+ observer.unobserve(entry.target);
32
+ }
33
+ } else if (!once) {
34
+ entry.target.classList.remove("strand-reveal--visible");
35
+ }
36
+ }
37
+ },
38
+ { threshold },
39
+ );
40
+
41
+ observer.observe(el);
42
+
43
+ return () => {
44
+ observer.disconnect();
45
+ };
46
+ }, [threshold, once]);
47
+
48
+ return (
49
+ <div
50
+ ref={(node) => {
51
+ (innerRef as { current: HTMLDivElement | null }).current = node;
52
+ if (typeof ref === "function") ref(node);
53
+ else if (ref) (ref as { current: HTMLDivElement | null }).current = node;
54
+ }}
55
+ className={classes}
56
+ {...rest}
57
+ >
58
+ {children}
59
+ </div>
60
+ );
61
+ },
62
+ );
63
+
64
+ ScrollReveal.displayName = "ScrollReveal";
@@ -0,0 +1,2 @@
1
+ export { ScrollReveal } from "./ScrollReveal.js";
2
+ export type { ScrollRevealProps } from "./ScrollReveal.js";
@@ -20,14 +20,15 @@
20
20
  transition: background var(--strand-duration-fast) var(--strand-ease-out-quart);
21
21
  }
22
22
 
23
- /* ── Thumb: Webkit ── */
23
+ /* ── Thumb: Webkit (44px touch target, 20px visual) ── */
24
24
  .strand-slider__field::-webkit-slider-thumb {
25
25
  appearance: none;
26
26
  width: 20px;
27
27
  height: 20px;
28
28
  background: var(--strand-blue-primary);
29
- border: 2px solid var(--strand-surface-elevated);
29
+ border: 12px solid transparent;
30
30
  border-radius: var(--strand-radius-full);
31
+ background-clip: padding-box;
31
32
  cursor: pointer;
32
33
  box-shadow: var(--strand-elevation-1);
33
34
  transition:
@@ -37,21 +38,24 @@
37
38
 
38
39
  .strand-slider__field:hover:not(:disabled)::-webkit-slider-thumb {
39
40
  background: var(--strand-blue-vivid);
41
+ background-clip: padding-box;
40
42
  transform: scale(1.15);
41
43
  }
42
44
 
43
45
  .strand-slider__field:active:not(:disabled)::-webkit-slider-thumb {
44
46
  background: var(--strand-blue-deep);
47
+ background-clip: padding-box;
45
48
  transform: scale(1.05);
46
49
  }
47
50
 
48
- /* ── Thumb: Firefox ── */
51
+ /* ── Thumb: Firefox (44px touch target, 20px visual) ── */
49
52
  .strand-slider__field::-moz-range-thumb {
50
53
  width: 20px;
51
54
  height: 20px;
52
55
  background: var(--strand-blue-primary);
53
- border: 2px solid var(--strand-surface-elevated);
56
+ border: 12px solid transparent;
54
57
  border-radius: var(--strand-radius-full);
58
+ background-clip: padding-box;
55
59
  cursor: pointer;
56
60
  box-shadow: var(--strand-elevation-1);
57
61
  transition:
@@ -61,11 +65,13 @@
61
65
 
62
66
  .strand-slider__field:hover:not(:disabled)::-moz-range-thumb {
63
67
  background: var(--strand-blue-vivid);
68
+ background-clip: padding-box;
64
69
  transform: scale(1.15);
65
70
  }
66
71
 
67
72
  .strand-slider__field:active:not(:disabled)::-moz-range-thumb {
68
73
  background: var(--strand-blue-deep);
74
+ background-clip: padding-box;
69
75
  transform: scale(1.05);
70
76
  }
71
77
 
@@ -5,6 +5,7 @@
5
5
  display: inline-flex;
6
6
  align-items: center;
7
7
  gap: var(--strand-space-2);
8
+ min-height: var(--strand-touch-target);
8
9
  cursor: pointer;
9
10
  user-select: none;
10
11
  font-family: var(--strand-font-sans);
package/src/index.ts CHANGED
@@ -100,3 +100,11 @@ export type { SpinnerProps } from "./components/Spinner/index.js";
100
100
 
101
101
  export { Skeleton } from "./components/Skeleton/index.js";
102
102
  export type { SkeletonProps } from "./components/Skeleton/index.js";
103
+
104
+ // Surfaces
105
+ export { InstrumentViewport } from "./components/InstrumentViewport/index.js";
106
+ export type { InstrumentViewportProps } from "./components/InstrumentViewport/index.js";
107
+
108
+ // Animation
109
+ export { ScrollReveal } from "./components/ScrollReveal/index.js";
110
+ export type { ScrollRevealProps } from "./components/ScrollReveal/index.js";