@charcuterie/ui 4.5.1 → 4.7.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 (58) hide show
  1. package/README.md +12 -0
  2. package/dist/LaneTimeline/LaneTimeline.d.ts +132 -0
  3. package/dist/LaneTimeline/LaneTimeline.d.ts.map +1 -0
  4. package/dist/LaneTimeline/LaneTimeline.js +165 -0
  5. package/dist/LaneTimeline/LaneTimeline.js.map +1 -0
  6. package/dist/LaneTimeline/LaneTimelineBar.d.ts +97 -0
  7. package/dist/LaneTimeline/LaneTimelineBar.d.ts.map +1 -0
  8. package/dist/LaneTimeline/LaneTimelineBar.js +69 -0
  9. package/dist/LaneTimeline/LaneTimelineBar.js.map +1 -0
  10. package/dist/LaneTimeline/LaneTimelineLane.d.ts +84 -0
  11. package/dist/LaneTimeline/LaneTimelineLane.d.ts.map +1 -0
  12. package/dist/LaneTimeline/LaneTimelineLane.js +109 -0
  13. package/dist/LaneTimeline/LaneTimelineLane.js.map +1 -0
  14. package/dist/LaneTimeline/laneTimelineGeometry.d.ts +268 -0
  15. package/dist/LaneTimeline/laneTimelineGeometry.d.ts.map +1 -0
  16. package/dist/LaneTimeline/laneTimelineGeometry.js +277 -0
  17. package/dist/LaneTimeline/laneTimelineGeometry.js.map +1 -0
  18. package/dist/LaneTimeline/useTrackMeasurements.d.ts +69 -0
  19. package/dist/LaneTimeline/useTrackMeasurements.d.ts.map +1 -0
  20. package/dist/LaneTimeline/useTrackMeasurements.js +134 -0
  21. package/dist/LaneTimeline/useTrackMeasurements.js.map +1 -0
  22. package/dist/Overlay/OverlayPanel.d.ts.map +1 -1
  23. package/dist/Overlay/OverlayPanel.js +54 -2
  24. package/dist/Overlay/OverlayPanel.js.map +1 -1
  25. package/dist/Overlay/OverlayStack.d.ts +5 -0
  26. package/dist/Overlay/OverlayStack.d.ts.map +1 -1
  27. package/dist/Overlay/OverlayStack.js +2 -0
  28. package/dist/Overlay/OverlayStack.js.map +1 -1
  29. package/dist/Toast/ToastRegion.d.ts +61 -0
  30. package/dist/Toast/ToastRegion.d.ts.map +1 -1
  31. package/dist/Toast/ToastRegion.js +80 -3
  32. package/dist/Toast/ToastRegion.js.map +1 -1
  33. package/dist/Toolbar/useToolbarOverflow.d.ts.map +1 -1
  34. package/dist/Toolbar/useToolbarOverflow.js +39 -0
  35. package/dist/Toolbar/useToolbarOverflow.js.map +1 -1
  36. package/dist/index.d.ts +6 -0
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +2 -0
  39. package/dist/index.js.map +1 -1
  40. package/package.json +3 -3
  41. package/src/LaneTimeline/LaneTimeline.mdx +191 -0
  42. package/src/LaneTimeline/LaneTimeline.stories.tsx +561 -0
  43. package/src/LaneTimeline/LaneTimeline.test.tsx +330 -0
  44. package/src/LaneTimeline/LaneTimeline.tsx +381 -0
  45. package/src/LaneTimeline/LaneTimelineBar.tsx +237 -0
  46. package/src/LaneTimeline/LaneTimelineLane.tsx +332 -0
  47. package/src/LaneTimeline/laneTimelineGeometry.test.ts +429 -0
  48. package/src/LaneTimeline/laneTimelineGeometry.ts +476 -0
  49. package/src/LaneTimeline/useTrackMeasurements.ts +165 -0
  50. package/src/Overlay/OverlayPanel.tsx +63 -2
  51. package/src/Overlay/OverlayStack.tsx +7 -1
  52. package/src/Toast/Toast.stories.tsx +154 -0
  53. package/src/Toast/ToastRegion.test.tsx +172 -0
  54. package/src/Toast/ToastRegion.tsx +120 -32
  55. package/src/Toolbar/Toolbar.test.tsx +84 -0
  56. package/src/Toolbar/useToolbarOverflow.ts +43 -0
  57. package/src/index.ts +13 -0
  58. package/src/sourceRules.test.ts +22 -1
@@ -284,3 +284,87 @@ test("the shell mounts exactly one toolbar", async () => {
284
284
  }),
285
285
  ).toHaveLength(1)
286
286
  })
287
+
288
+ /**
289
+ * The font race, which is latent rather than observed: it needs the
290
+ * two faces to disagree across a collapse boundary, and no story in
291
+ * the library crosses one today. The test builds that boundary on
292
+ * purpose, so the mechanism is checked rather than waited for.
293
+ *
294
+ * Every width the bar measures is a **text** width. On a cold cache
295
+ * the first measurement is taken in the fallback face, and when the
296
+ * real face swaps in the items change size — but the container does
297
+ * not, so the `ResizeObserver` stays quiet, and nothing re-renders,
298
+ * so the layout effect does not run either. The bar keeps an answer
299
+ * computed against a font that is no longer on screen.
300
+ *
301
+ * The stub below is that swap with the timing made explicit: the bar
302
+ * mounts while `document.fonts.ready` is still pending, the items
303
+ * are widened with no change to the container's box, and the
304
+ * promise is then resolved. A bar that only measures on mount and
305
+ * on resize sees neither event and stays wrong.
306
+ */
307
+ test("the bar re-measures once the webfont has loaded", async () => {
308
+ const realFonts = document.fonts
309
+
310
+ let markFontsReady = () => {}
311
+
312
+ const fontsReady = new Promise<FontFaceSet>((resolve) => {
313
+ markFontsReady = () => {
314
+ resolve(realFonts)
315
+ }
316
+ })
317
+
318
+ // Layered over the real set rather than replacing it, so anything
319
+ // else that reads `document.fonts` during the mount still works.
320
+ Object.defineProperty(document, "fonts", {
321
+ configurable: true,
322
+ value: Object.create(realFonts, {
323
+ ready: {
324
+ get: () => fontsReady,
325
+ },
326
+ }),
327
+ })
328
+
329
+ // Widens every action without touching the bar's own box — the
330
+ // `Frame` around `Playground` is a fixed `34rem`. That is exactly
331
+ // what a font swap does, and it is why the `ResizeObserver` is
332
+ // not a substitute for this effect.
333
+ const widenActions = document.createElement("style")
334
+
335
+ widenActions.textContent = `[role="toolbar"] button { padding-inline: 4rem; }`
336
+
337
+ try {
338
+ const { canvas } = await mountStory(Playground)
339
+
340
+ const toolbar = expectAgentDrivable(canvas, {
341
+ name: "Deck actions",
342
+ role: "toolbar",
343
+ })
344
+
345
+ // It fits in the fallback face.
346
+ await expect(
347
+ within(toolbar).queryByRole("button", {
348
+ name: "More actions",
349
+ }),
350
+ ).toBeNull()
351
+
352
+ document.head.append(widenActions)
353
+
354
+ markFontsReady()
355
+
356
+ await waitFor(() => {
357
+ expect(
358
+ within(toolbar).getByRole("button", {
359
+ name: "More actions",
360
+ }),
361
+ ).toBeInTheDocument()
362
+ })
363
+ } finally {
364
+ widenActions.remove()
365
+
366
+ // Back to the prototype getter — the stub was an own property
367
+ // shadowing it.
368
+ Reflect.deleteProperty(document, "fonts")
369
+ }
370
+ })
@@ -159,6 +159,49 @@ export const useToolbarOverflow = ({
159
159
  */
160
160
  useLayoutEffect(measure)
161
161
 
162
+ /**
163
+ * Measure again once the webfont has actually arrived.
164
+ *
165
+ * Every width above is a **text** width, so it is only as correct
166
+ * as the font that was loaded when it was read. On a cold cache
167
+ * the first measurement happens in the fallback face, the items
168
+ * come out a few pixels off, and `chooseVisibleCount` keeps or
169
+ * drops one action it should not have — then never revisits it,
170
+ * because swapping a font changes no box the `ResizeObserver`
171
+ * watches and triggers no render.
172
+ *
173
+ * The user-visible symptom is a bar that settles one button wrong
174
+ * and stays there. It needs the two faces to disagree across a
175
+ * collapse boundary, so it is latent rather than observed: no
176
+ * story in the library crosses one today, which is why `vrt`
177
+ * reports nothing either way. That makes it a gap to close, not a
178
+ * regression to chase — the bar is supposed to measure what is on
179
+ * screen, and until now it could not.
180
+ *
181
+ * `document.fonts.ready` resolves immediately when the fonts are
182
+ * already in, so a warm load pays one microtask and
183
+ * `setVisibleCount` bails on the unchanged answer.
184
+ */
185
+ useLayoutEffect(() => {
186
+ // jsdom has no `document.fonts`, and neither does a very old
187
+ // browser. Nothing to wait for is not an error.
188
+ if (!document.fonts) {
189
+ return
190
+ }
191
+
192
+ let isActive = true
193
+
194
+ document.fonts.ready.then(() => {
195
+ if (isActive) {
196
+ measure()
197
+ }
198
+ })
199
+
200
+ return () => {
201
+ isActive = false
202
+ }
203
+ }, [measure])
204
+
162
205
  useLayoutEffect(() => {
163
206
  const container = containerRef.current
164
207
 
package/src/index.ts CHANGED
@@ -237,6 +237,19 @@ export {
237
237
  INTENT_HOVER_CLASS,
238
238
  INTENT_SOLID_FILL_CLASS,
239
239
  } from "./intentStyles.ts"
240
+ export type { LaneTimelineProps } from "./LaneTimeline/LaneTimeline.tsx"
241
+ export { LaneTimeline } from "./LaneTimeline/LaneTimeline.tsx"
242
+ export type { TimelineItem } from "./LaneTimeline/LaneTimelineBar.tsx"
243
+ export type { TimelineLane } from "./LaneTimeline/LaneTimelineLane.tsx"
244
+ export type {
245
+ BarPlacement,
246
+ TimelineAxis,
247
+ TimelineSpan,
248
+ } from "./LaneTimeline/laneTimelineGeometry.ts"
249
+ export {
250
+ toBarPlacement,
251
+ toTimelineAxis,
252
+ } from "./LaneTimeline/laneTimelineGeometry.ts"
240
253
  export type { LightboxProps } from "./Lightbox/Lightbox.tsx"
241
254
  export { Lightbox } from "./Lightbox/Lightbox.tsx"
242
255
  export type {
@@ -233,6 +233,13 @@ test("a container-query component is never storied in a shrink-to-fit cell", ()
233
233
  // owner is in whenever he browses zoomed in.
234
234
  "DatePicker",
235
235
  "EmptyState",
236
+ // `LaneTimeline` declares one and **never queries it**, which is
237
+ // the `Main` case rather than a mistake: it wants the
238
+ // `contain: inline-size` that rides along, because the component
239
+ // measures its own box to decide whether to draw a date axis at
240
+ // all. Without the containment a shrink-wrapping parent makes
241
+ // that answer its own input.
242
+ "LaneTimeline",
236
243
  "Main",
237
244
  "MarkdownEditor",
238
245
  "MediaTile",
@@ -591,7 +598,21 @@ test("the barrel is the only place components are re-exported", async () => {
591
598
  // from `ActionTiles`' "what do you want to do" and would have
592
599
  // meant one component whose icon is sometimes a 20px glyph beside
593
600
  // a title and sometimes a 144px circle above it.
594
- expect(componentNames.length).toBe(65)
601
+ //
602
+ // `LaneTimeline` — one horizontal lane per group across a date
603
+ // axis, with a multi-day thing drawn as a bar — is +1 -> 66. It is
604
+ // the first component that folds on a **measurement of its own
605
+ // box** rather than on a container query, and the reason is in the
606
+ // arithmetic: the question is not how wide the box is but how wide
607
+ // one day is, which is the width divided by a number that lives in
608
+ // the data. `laneTimelineGeometry.ts`, `LaneTimelineBar.tsx`,
609
+ // `LaneTimelineLane.tsx` and `useTrackMeasurements.ts` are its
610
+ // members and stay out of this count by the `<Name>/<Name>.tsx`
611
+ // rule. The geometry module is still exported from the barrel — a
612
+ // consumer deciding whether an item is on screen must ask the
613
+ // question the same way the bars are placed, or the two answers
614
+ // drift by a day and nothing says so.
615
+ expect(componentNames.length).toBe(66)
595
616
 
596
617
  for (const name of componentNames) {
597
618
  expect(barrel).toContain(`export { ${name} }`)