@boyernick/standard-ui-react 0.1.0 → 0.1.1-canary.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.
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@boyernick/standard-ui-react",
3
- "version": "0.1.0",
3
+ "version": "0.1.1-canary.1",
4
4
  "description": "React components for StandardUI",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "sideEffects": false,
8
8
  "files": [
9
- "src"
9
+ "src",
10
+ "!src/**/*.test.ts"
10
11
  ],
11
12
  "exports": {
12
13
  ".": {
@@ -15,6 +16,10 @@
15
16
  },
16
17
  "./package.json": "./package.json"
17
18
  },
19
+ "scripts": {
20
+ "typecheck": "tsc -p tsconfig.json --noEmit",
21
+ "test": "node --test --experimental-strip-types \"src/**/*.test.ts\""
22
+ },
18
23
  "peerDependencies": {
19
24
  "react": "^18.0.0 || ^19.0.0",
20
25
  "react-dom": "^18.0.0 || ^19.0.0"
@@ -28,7 +33,8 @@
28
33
  "embla-carousel-react": "^8.6.0",
29
34
  "react-day-picker": "^10.0.1",
30
35
  "recharts": "^3.10.1",
31
- "sugar-high": "^2.0.1"
36
+ "sugar-high": "^2.0.1",
37
+ "tailwind-merge": "^3.6.0"
32
38
  },
33
39
  "devDependencies": {
34
40
  "@types/react": "^19",
package/src/carousel.tsx CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  useCallback,
9
9
  useContext,
10
10
  useEffect,
11
- useState,
11
+ useSyncExternalStore,
12
12
  type ComponentProps,
13
13
  type KeyboardEvent,
14
14
  } from "react"
@@ -70,14 +70,34 @@ export const Carousel = ({
70
70
  },
71
71
  plugins,
72
72
  )
73
- const [canScrollPrev, setCanScrollPrev] = useState(false)
74
- const [canScrollNext, setCanScrollNext] = useState(false)
73
+ // Embla is an external store, so read from it directly rather than mirroring
74
+ // it into state. Seeding mirrored state meant calling setState synchronously
75
+ // inside an effect, costing an extra render on mount and on every slide
76
+ // change. Both snapshots return booleans, so there is no object identity to
77
+ // keep stable between reads.
78
+ const subscribe = useCallback(
79
+ (onStoreChange: () => void) => {
80
+ if (!api) return () => {}
81
+ api.on("reInit", onStoreChange)
82
+ api.on("select", onStoreChange)
83
+ return () => {
84
+ api.off("reInit", onStoreChange)
85
+ api.off("select", onStoreChange)
86
+ }
87
+ },
88
+ [api],
89
+ )
75
90
 
76
- const handleSelect = useCallback((instance: CarouselApi) => {
77
- if (!instance) return
78
- setCanScrollPrev(instance.canScrollPrev())
79
- setCanScrollNext(instance.canScrollNext())
80
- }, [])
91
+ const canScrollPrev = useSyncExternalStore(
92
+ subscribe,
93
+ () => api?.canScrollPrev() ?? false,
94
+ () => false,
95
+ )
96
+ const canScrollNext = useSyncExternalStore(
97
+ subscribe,
98
+ () => api?.canScrollNext() ?? false,
99
+ () => false,
100
+ )
81
101
 
82
102
  const scrollPrev = useCallback(() => {
83
103
  api?.scrollPrev()
@@ -107,17 +127,6 @@ export const Carousel = ({
107
127
  setApi(api)
108
128
  }, [api, setApi])
109
129
 
110
- useEffect(() => {
111
- if (!api) return
112
- handleSelect(api)
113
- api.on("reInit", handleSelect)
114
- api.on("select", handleSelect)
115
- return () => {
116
- api.off("reInit", handleSelect)
117
- api.off("select", handleSelect)
118
- }
119
- }, [api, handleSelect])
120
-
121
130
  return (
122
131
  <CarouselContext.Provider
123
132
  value={{
package/src/lib/cn.ts CHANGED
@@ -1,3 +1,45 @@
1
1
  import { clsx, type ClassValue } from "clsx"
2
+ import { extendTailwindMerge } from "tailwind-merge"
2
3
 
3
- export const cn = (...inputs: ClassValue[]) => clsx(inputs)
4
+ /**
5
+ * StandardUI defines typography composites as custom `@utility` rules
6
+ * (`text-sm-strong`, `text-md`, …). Tailwind Merge cannot know those are font
7
+ * sizes, so it files them under text *color* — and then drops them whenever a
8
+ * real color follows, as in `cn("text-sm-strong", "text-fg-primary")`. Grouping
9
+ * them with font sizes keeps them conflicting with each other and with
10
+ * `text-sm`, while leaving colors alone.
11
+ *
12
+ * `shadow-hairline` has the same problem in the shadow group. Custom font sizes
13
+ * (`text-2xs`) and radii (`rounded-2xs`, `rounded-4xl`) already resolve
14
+ * correctly and need no help.
15
+ *
16
+ * Keep this in step with the `@utility` rules in
17
+ * @boyernick/standard-ui-tokens/css/tokens.css.
18
+ */
19
+ const twMerge = extendTailwindMerge({
20
+ extend: {
21
+ classGroups: {
22
+ "font-size": [
23
+ {
24
+ text: [
25
+ "2xs-strong",
26
+ "xs-strong",
27
+ "sm-strong",
28
+ "md",
29
+ "md-strong",
30
+ "lg-strong",
31
+ ],
32
+ },
33
+ ],
34
+ shadow: [{ shadow: ["hairline"] }],
35
+ },
36
+ },
37
+ })
38
+
39
+ /**
40
+ * Combines class names and resolves Tailwind conflicts so the last value wins.
41
+ * Without the merge step a caller's `className` cannot override a built-in
42
+ * utility: both classes land in the DOM and the winner is whichever rule sits
43
+ * later in the stylesheet, which the caller does not control.
44
+ */
45
+ export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs))
@@ -108,7 +108,7 @@ type Format = {
108
108
  }
109
109
 
110
110
  const formats: Format[] = [
111
- { label: "Bold", marker: "**", className: "font-semibold" },
111
+ { label: "Bold", marker: "**", className: "font-medium" },
112
112
  { label: "Italic", marker: "*", className: "italic" },
113
113
  { label: "Code", marker: "`", className: "font-mono" },
114
114
  ]
@@ -45,7 +45,6 @@ export const TextAnimate = ({
45
45
 
46
46
  useEffect(() => {
47
47
  let cancelled = false
48
- let frame = 0
49
48
  let timeoutId: ReturnType<typeof setTimeout> | undefined
50
49
 
51
50
  const start = () => {
@@ -114,7 +113,6 @@ export const TextAnimate = ({
114
113
  return () => {
115
114
  cancelled = true
116
115
  if (timeoutId) clearTimeout(timeoutId)
117
- cancelAnimationFrame(frame)
118
116
  }
119
117
  }, [chars, delay, effect, replay, speed, text])
120
118