@charcuterie/ui 0.2.0 → 1.0.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 (51) hide show
  1. package/dist/Field/Field.d.ts +41 -6
  2. package/dist/Field/Field.d.ts.map +1 -1
  3. package/dist/Field/Field.js +50 -8
  4. package/dist/Field/Field.js.map +1 -1
  5. package/dist/Field/FieldGroup.d.ts +58 -0
  6. package/dist/Field/FieldGroup.d.ts.map +1 -0
  7. package/dist/Field/FieldGroup.js +65 -0
  8. package/dist/Field/FieldGroup.js.map +1 -0
  9. package/dist/LogViewer/LogViewer.d.ts +3 -1
  10. package/dist/LogViewer/LogViewer.d.ts.map +1 -1
  11. package/dist/LogViewer/LogViewer.js +62 -1
  12. package/dist/LogViewer/LogViewer.js.map +1 -1
  13. package/dist/Select/Select.d.ts +10 -2
  14. package/dist/Select/Select.d.ts.map +1 -1
  15. package/dist/Select/Select.js.map +1 -1
  16. package/dist/Tooltip/Tooltip.d.ts +16 -2
  17. package/dist/Tooltip/Tooltip.d.ts.map +1 -1
  18. package/dist/Tooltip/Tooltip.js +23 -3
  19. package/dist/Tooltip/Tooltip.js.map +1 -1
  20. package/dist/index.d.ts +4 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +2 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/slotProps.d.ts +121 -0
  25. package/dist/slotProps.d.ts.map +1 -0
  26. package/dist/slotProps.js +55 -0
  27. package/dist/slotProps.js.map +1 -0
  28. package/dist/slotWiring.d.ts +32 -0
  29. package/dist/slotWiring.d.ts.map +1 -0
  30. package/dist/slotWiring.js +115 -0
  31. package/dist/slotWiring.js.map +1 -0
  32. package/package.json +3 -3
  33. package/src/Field/Field.mdx +73 -3
  34. package/src/Field/Field.stories.tsx +136 -0
  35. package/src/Field/Field.test.tsx +207 -5
  36. package/src/Field/Field.tsx +70 -21
  37. package/src/Field/FieldGroup.tsx +138 -0
  38. package/src/LogViewer/LogViewer.mdx +36 -1
  39. package/src/LogViewer/LogViewer.stories.tsx +34 -0
  40. package/src/LogViewer/LogViewer.test.tsx +65 -2
  41. package/src/LogViewer/LogViewer.tsx +67 -1
  42. package/src/Menu/Menu.mdx +23 -0
  43. package/src/Menu/Menu.stories.tsx +67 -0
  44. package/src/Menu/Menu.test.tsx +113 -2
  45. package/src/Popover/Popover.mdx +5 -0
  46. package/src/Select/Select.stories.tsx +10 -2
  47. package/src/Select/Select.tsx +10 -2
  48. package/src/Tooltip/Tooltip.tsx +29 -5
  49. package/src/index.ts +4 -0
  50. package/src/slotProps.ts +163 -0
  51. package/src/slotWiring.ts +173 -0
@@ -0,0 +1,173 @@
1
+ /**
2
+ * The two props a slot may not overwrite, and how it merges them.
3
+ *
4
+ * `mergeSlotProps` next door settles the five *attributes* two
5
+ * nested slots both write, and last-write-wins is right for four of
6
+ * them. It is not right for the wiring, because neither of these is
7
+ * a value:
8
+ *
9
+ * - **`ref`** is a subscription. `Menu` and `Tooltip` on one trigger
10
+ * both hand it a floating-ui `refs.setReference` — the anchor each
11
+ * of them positions its panel against — and the inner one used to
12
+ * replace the outer one's. Nothing threw. The outer panel simply
13
+ * had no reference element, so floating-ui left it at
14
+ * `left: 0; top: 0` in the corner of the viewport, which reads as
15
+ * a CSS bug and is not one.
16
+ * - **`on*`** is a listener, and listeners are plural.
17
+ *
18
+ * ### Why this is not imported from `@charcuterie/logic`
19
+ *
20
+ * `useClonedChild`'s `mergeClonedProps` applies the same rule one
21
+ * level down — between a slot and the element a caller wrote — and
22
+ * the two are near-identical. Sharing them means a new public export
23
+ * on `@charcuterie/logic`, which is a `minor`; this is a patch, and
24
+ * the duplication is thirty lines against a wrong version number.
25
+ * Queued in `docs/2026-08-02-reported-by-consumers-queued-for-1-1.md`.
26
+ */
27
+
28
+ const isEventHandlerName = (name: string) =>
29
+ /^on[A-Z]/.test(name)
30
+
31
+ type MergeableRef =
32
+ | ((node: unknown) => unknown)
33
+ | { current: unknown }
34
+
35
+ const isMergeableRef = (
36
+ value: unknown,
37
+ ): value is MergeableRef =>
38
+ typeof value === "function" ||
39
+ (typeof value === "object" &&
40
+ value !== null &&
41
+ "current" in value)
42
+
43
+ const setRef = (ref: MergeableRef, node: unknown) => {
44
+ if (typeof ref === "function") {
45
+ return ref(node)
46
+ }
47
+
48
+ ref.current = node
49
+
50
+ return undefined
51
+ }
52
+
53
+ /**
54
+ * Every pair of refs ever merged, so the same two always come back
55
+ * as the **same function** — which is load-bearing rather than
56
+ * frugal.
57
+ *
58
+ * React re-runs a callback ref whenever its identity changes between
59
+ * renders, detaching with the old one and attaching with the new.
60
+ * Both refs here are floating-ui `setReference`s, and calling one
61
+ * sets state; a merged ref rebuilt every render would call both
62
+ * twice per render, each call scheduling the render that rebuilds
63
+ * it. `mergeSlotProps` is a plain function called during render, so
64
+ * it cannot reach for `useMemo` — the memo has to be in the identity
65
+ * of the inputs, which is what a `WeakMap` of `WeakMap`s is.
66
+ */
67
+ const mergedRefs = new WeakMap<
68
+ MergeableRef,
69
+ WeakMap<MergeableRef, (node: unknown) => () => void>
70
+ >()
71
+
72
+ const mergeRefs = (
73
+ first: MergeableRef,
74
+ second: MergeableRef,
75
+ ) => {
76
+ const bySecond =
77
+ mergedRefs.get(first) ??
78
+ new WeakMap<
79
+ MergeableRef,
80
+ (node: unknown) => () => void
81
+ >()
82
+
83
+ mergedRefs.set(first, bySecond)
84
+
85
+ const cached = bySecond.get(second)
86
+
87
+ if (cached) {
88
+ return cached
89
+ }
90
+
91
+ const merged = (node: unknown) => {
92
+ const cleanups = [
93
+ setRef(first, node),
94
+ setRef(second, node),
95
+ ]
96
+
97
+ // Per ref: one that returned a cleanup has opted out of being
98
+ // called with `null`, and one that did not still expects it.
99
+ return () => {
100
+ for (const [index, cleanup] of cleanups.entries()) {
101
+ if (typeof cleanup === "function") {
102
+ cleanup()
103
+ } else {
104
+ setRef(index === 0 ? first : second, null)
105
+ }
106
+ }
107
+ }
108
+ }
109
+
110
+ bySecond.set(second, merged)
111
+
112
+ return merged
113
+ }
114
+
115
+ const chainHandlers =
116
+ (
117
+ first: (...args: unknown[]) => unknown,
118
+ second: (...args: unknown[]) => unknown,
119
+ ) =>
120
+ (...args: unknown[]) => {
121
+ // Outer first, the same order `aria-describedby` is joined in:
122
+ // the merge order is structural — whoever wrapped whom — and the
123
+ // author of the nesting is the one who chose it.
124
+ first(...args)
125
+
126
+ second(...args)
127
+ }
128
+
129
+ /**
130
+ * The keys of `ownProps` that must not simply replace the ones
131
+ * `receivedProps` arrived with. Spread **last**, over both.
132
+ */
133
+ export const mergeSlotWiring = (
134
+ receivedProps: Record<string, unknown>,
135
+ ownProps: Record<string, unknown>,
136
+ ): Record<string, unknown> =>
137
+ Object.fromEntries(
138
+ Object.entries(ownProps).flatMap(
139
+ ([name, ownValue]): [string, unknown][] => {
140
+ const receivedValue = receivedProps[name]
141
+
142
+ if (
143
+ name === "ref" &&
144
+ isMergeableRef(receivedValue) &&
145
+ isMergeableRef(ownValue)
146
+ ) {
147
+ return [
148
+ [name, mergeRefs(receivedValue, ownValue)],
149
+ ]
150
+ }
151
+
152
+ if (
153
+ isEventHandlerName(name) &&
154
+ typeof receivedValue === "function" &&
155
+ typeof ownValue === "function"
156
+ ) {
157
+ return [
158
+ [
159
+ name,
160
+ chainHandlers(
161
+ receivedValue as (
162
+ ...args: unknown[]
163
+ ) => unknown,
164
+ ownValue as (...args: unknown[]) => unknown,
165
+ ),
166
+ ],
167
+ ]
168
+ }
169
+
170
+ return []
171
+ },
172
+ ),
173
+ )