@charcuterie/ui 2.7.0 → 2.8.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 (96) hide show
  1. package/README.md +11 -0
  2. package/dist/AdaptiveGrid/AdaptiveGrid.d.ts +94 -0
  3. package/dist/AdaptiveGrid/AdaptiveGrid.d.ts.map +1 -0
  4. package/dist/AdaptiveGrid/AdaptiveGrid.js +73 -0
  5. package/dist/AdaptiveGrid/AdaptiveGrid.js.map +1 -0
  6. package/dist/AdaptiveGrid/chooseColumns.d.ts +206 -0
  7. package/dist/AdaptiveGrid/chooseColumns.d.ts.map +1 -0
  8. package/dist/AdaptiveGrid/chooseColumns.js +156 -0
  9. package/dist/AdaptiveGrid/chooseColumns.js.map +1 -0
  10. package/dist/AdaptiveGrid/useAdaptiveColumns.d.ts +155 -0
  11. package/dist/AdaptiveGrid/useAdaptiveColumns.d.ts.map +1 -0
  12. package/dist/AdaptiveGrid/useAdaptiveColumns.js +153 -0
  13. package/dist/AdaptiveGrid/useAdaptiveColumns.js.map +1 -0
  14. package/dist/Button/Button.d.ts.map +1 -1
  15. package/dist/Button/Button.js +11 -6
  16. package/dist/Button/Button.js.map +1 -1
  17. package/dist/ButtonLink/ButtonLink.d.ts +59 -0
  18. package/dist/ButtonLink/ButtonLink.d.ts.map +1 -0
  19. package/dist/ButtonLink/ButtonLink.js +69 -0
  20. package/dist/ButtonLink/ButtonLink.js.map +1 -0
  21. package/dist/Combobox/Combobox.d.ts.map +1 -1
  22. package/dist/Combobox/Combobox.js +1 -1
  23. package/dist/Combobox/Combobox.js.map +1 -1
  24. package/dist/Dialog/Dialog.js +1 -1
  25. package/dist/Dialog/Dialog.js.map +1 -1
  26. package/dist/LogViewer/LogViewer.d.ts.map +1 -1
  27. package/dist/LogViewer/LogViewer.js +33 -1
  28. package/dist/LogViewer/LogViewer.js.map +1 -1
  29. package/dist/RouterLink/AnchorLink.d.ts +17 -0
  30. package/dist/RouterLink/AnchorLink.d.ts.map +1 -0
  31. package/dist/RouterLink/AnchorLink.js +16 -0
  32. package/dist/RouterLink/AnchorLink.js.map +1 -0
  33. package/dist/RouterLink/RouterLinkProvider.d.ts +13 -0
  34. package/dist/RouterLink/RouterLinkProvider.d.ts.map +1 -0
  35. package/dist/RouterLink/RouterLinkProvider.js +47 -0
  36. package/dist/RouterLink/RouterLinkProvider.js.map +1 -0
  37. package/dist/RouterLink/routerLink.d.ts +45 -0
  38. package/dist/RouterLink/routerLink.d.ts.map +1 -0
  39. package/dist/RouterLink/routerLink.js +31 -0
  40. package/dist/RouterLink/routerLink.js.map +1 -0
  41. package/dist/TextLink/TextLink.d.ts +69 -0
  42. package/dist/TextLink/TextLink.d.ts.map +1 -0
  43. package/dist/TextLink/TextLink.js +79 -0
  44. package/dist/TextLink/TextLink.js.map +1 -0
  45. package/dist/controlStyles.d.ts +36 -1
  46. package/dist/controlStyles.d.ts.map +1 -1
  47. package/dist/controlStyles.js +30 -0
  48. package/dist/controlStyles.js.map +1 -1
  49. package/dist/index.d.ts +16 -2
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +10 -2
  52. package/dist/index.js.map +1 -1
  53. package/dist/intentStyles.d.ts +17 -0
  54. package/dist/intentStyles.d.ts.map +1 -1
  55. package/dist/intentStyles.js +17 -0
  56. package/dist/intentStyles.js.map +1 -1
  57. package/dist/reactRouter/ReactRouterLink.d.ts +23 -0
  58. package/dist/reactRouter/ReactRouterLink.d.ts.map +1 -0
  59. package/dist/reactRouter/ReactRouterLink.js +23 -0
  60. package/dist/reactRouter/ReactRouterLink.js.map +1 -0
  61. package/dist/reactRouter/index.d.ts +29 -0
  62. package/dist/reactRouter/index.d.ts.map +1 -0
  63. package/dist/reactRouter/index.js +29 -0
  64. package/dist/reactRouter/index.js.map +1 -0
  65. package/package.json +15 -3
  66. package/src/AdaptiveGrid/AdaptiveGrid.mdx +169 -0
  67. package/src/AdaptiveGrid/AdaptiveGrid.stories.tsx +240 -0
  68. package/src/AdaptiveGrid/AdaptiveGrid.test.tsx +243 -0
  69. package/src/AdaptiveGrid/AdaptiveGrid.tsx +150 -0
  70. package/src/AdaptiveGrid/chooseColumns.test.ts +253 -0
  71. package/src/AdaptiveGrid/chooseColumns.ts +270 -0
  72. package/src/AdaptiveGrid/useAdaptiveColumns.ts +355 -0
  73. package/src/Button/Button.tsx +10 -22
  74. package/src/ButtonLink/ButtonLink.mdx +75 -0
  75. package/src/ButtonLink/ButtonLink.stories.tsx +281 -0
  76. package/src/ButtonLink/ButtonLink.test.tsx +203 -0
  77. package/src/ButtonLink/ButtonLink.tsx +149 -0
  78. package/src/Combobox/Combobox.tsx +4 -1
  79. package/src/Dialog/Dialog.tsx +1 -1
  80. package/src/LogViewer/LogViewer.test.tsx +47 -0
  81. package/src/LogViewer/LogViewer.tsx +35 -1
  82. package/src/RouterLink/AnchorLink.tsx +23 -0
  83. package/src/RouterLink/RouterLink.mdx +178 -0
  84. package/src/RouterLink/RouterLinkProvider.tsx +68 -0
  85. package/src/RouterLink/routerLink.ts +56 -0
  86. package/src/TextLink/TextLink.mdx +93 -0
  87. package/src/TextLink/TextLink.stories.tsx +260 -0
  88. package/src/TextLink/TextLink.test.tsx +230 -0
  89. package/src/TextLink/TextLink.tsx +194 -0
  90. package/src/controlStyles.ts +69 -1
  91. package/src/icons.storyHelpers.tsx +32 -0
  92. package/src/index.ts +41 -0
  93. package/src/intentStyles.ts +19 -0
  94. package/src/reactRouter/ReactRouterLink.tsx +30 -0
  95. package/src/reactRouter/index.ts +29 -0
  96. package/src/sourceRules.test.ts +41 -6
@@ -16,7 +16,18 @@
16
16
  * `packages/tokens/src/tailwindCollisions.test.ts`.
17
17
  */
18
18
 
19
- import type { ControlSize } from "@charcuterie/tokens"
19
+ import type {
20
+ ControlSize,
21
+ IntentName,
22
+ } from "@charcuterie/tokens"
23
+
24
+ import type { IntentAppearance } from "./intentStyles.ts"
25
+ import {
26
+ FOCUS_RING_CLASS,
27
+ INTENT_APPEARANCE_CLASS,
28
+ INTENT_HOVER_CLASS,
29
+ } from "./intentStyles.ts"
30
+ import { toClassName } from "./toClassName.ts"
20
31
 
21
32
  export const CONTROL_SIZE_CLASS: Record<
22
33
  ControlSize,
@@ -58,6 +69,63 @@ export const ICON_CONTROL_SIZE_CLASS: Record<
58
69
  export const MIN_TOUCH_TARGET_CLASS =
59
70
  "min-h-(--control-min-touch-target) min-w-(--control-min-touch-target)"
60
71
 
72
+ /**
73
+ * Everything a pressable control looks like before an intent, a size,
74
+ * or a state is applied to it.
75
+ *
76
+ * Pulled out of `Button` the day `ButtonLink` shipped. Those two must
77
+ * paint **identically** — the whole point of `ButtonLink` is that a
78
+ * navigation can read as a primary button — and "identically" spelled
79
+ * as two copies of the same string is a promise that survives exactly
80
+ * one edit.
81
+ */
82
+ export const CONTROL_BASE_CLASS =
83
+ "inline-flex cursor-pointer items-center justify-center rounded-md border font-medium whitespace-nowrap transition-colors duration-(--duration-fast) ease-standard select-none"
84
+
85
+ /**
86
+ * The one class list a button-shaped control wears, assembled once.
87
+ *
88
+ * `Button` and `ButtonLink` call this with the same arguments and get
89
+ * the same string back, so a new appearance, a changed radius, or a
90
+ * different hover ramp reaches both or neither. `ButtonLink.test.tsx`
91
+ * asserts the resulting *computed styles* match, which is the version
92
+ * of this claim a class-name refactor cannot quietly break.
93
+ *
94
+ * The disabled treatment is the caller's, because it is the one thing
95
+ * that genuinely differs: a `<button>` has a real `disabled`
96
+ * attribute and gets `DISABLED_CLASS`, while an `<a>` has no such
97
+ * concept and gets `ARIA_DISABLED_CLASS` keyed off `aria-disabled`.
98
+ */
99
+ export const getControlClassName = ({
100
+ appearance,
101
+ className,
102
+ disabledClass,
103
+ intent,
104
+ isFullWidth,
105
+ size,
106
+ sizing,
107
+ }: {
108
+ appearance: IntentAppearance
109
+ className?: string
110
+ disabledClass: string
111
+ intent: IntentName
112
+ isFullWidth: boolean
113
+ size: ControlSize
114
+ sizing: "control" | "icon"
115
+ }): string =>
116
+ toClassName(
117
+ CONTROL_BASE_CLASS,
118
+ sizing === "icon"
119
+ ? ICON_CONTROL_SIZE_CLASS[size]
120
+ : CONTROL_SIZE_CLASS[size],
121
+ INTENT_APPEARANCE_CLASS[intent][appearance],
122
+ INTENT_HOVER_CLASS[intent][appearance],
123
+ FOCUS_RING_CLASS,
124
+ disabledClass,
125
+ isFullWidth && "w-full",
126
+ className,
127
+ )
128
+
61
129
  /**
62
130
  * Non-control sizing: badges, indicator dots, spinners. These are
63
131
  * type-relative on purpose — a badge beside `text-sm` prose that
@@ -48,6 +48,38 @@ export const SettingsIcon = (): ReactNode => (
48
48
  </svg>
49
49
  )
50
50
 
51
+ /**
52
+ * The back-arrow seven repos spell as a literal `←`, drawn instead —
53
+ * so the `TextLink` boards screenshot as an arrow rather than as the
54
+ * blank box that character measures in this Chromium.
55
+ */
56
+ export const ArrowLeftIcon = (): ReactNode => (
57
+ <svg {...iconProps} aria-hidden="true">
58
+ <path d="M19 12H5" />
59
+
60
+ <path d="m11 18-6-6 6-6" />
61
+ </svg>
62
+ )
63
+
64
+ export const ArrowRightIcon = (): ReactNode => (
65
+ <svg {...iconProps} aria-hidden="true">
66
+ <path d="M5 12h14" />
67
+
68
+ <path d="m13 6 6 6-6 6" />
69
+ </svg>
70
+ )
71
+
72
+ /** The `↗` an app puts after an external link, drawn. */
73
+ export const ExternalIcon = (): ReactNode => (
74
+ <svg {...iconProps} aria-hidden="true">
75
+ <path d="M14 5h5v5" />
76
+
77
+ <path d="M19 5 10 14" />
78
+
79
+ <path d="M18 14v4a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h4" />
80
+ </svg>
81
+ )
82
+
51
83
  export const UndoIcon = (): ReactNode => (
52
84
  <svg {...iconProps} aria-hidden="true">
53
85
  <path d="M9 14 4 9l5-5" />
package/src/index.ts CHANGED
@@ -37,6 +37,27 @@ export type {
37
37
  AccordionProps,
38
38
  } from "./Accordion/Accordion.tsx"
39
39
  export { Accordion } from "./Accordion/Accordion.tsx"
40
+ export type { AdaptiveGridProps } from "./AdaptiveGrid/AdaptiveGrid.tsx"
41
+ export { AdaptiveGrid } from "./AdaptiveGrid/AdaptiveGrid.tsx"
42
+ export type { ColumnChoice } from "./AdaptiveGrid/chooseColumns.ts"
43
+ export {
44
+ chooseColumns,
45
+ DEFAULT_MAX_AUTO_COLUMNS,
46
+ DEFAULT_MAX_MANUAL_COLUMNS,
47
+ DEFAULT_MIN_COLUMN_INLINE_SIZE_PX,
48
+ getColumnChoices,
49
+ getContentMaxInlineSize,
50
+ } from "./AdaptiveGrid/chooseColumns.ts"
51
+ export type {
52
+ BlockSizeResolver,
53
+ ColumnPersistence,
54
+ } from "./AdaptiveGrid/useAdaptiveColumns.ts"
55
+ export {
56
+ localStorageColumnPersistence,
57
+ readStoredChoice,
58
+ useAdaptiveColumns,
59
+ viewportBlockSizeResolver,
60
+ } from "./AdaptiveGrid/useAdaptiveColumns.ts"
40
61
  export type {
41
62
  AlertProps,
42
63
  AlertSize,
@@ -46,6 +67,8 @@ export type { BadgeProps } from "./Badge/Badge.tsx"
46
67
  export { Badge } from "./Badge/Badge.tsx"
47
68
  export type { ButtonProps } from "./Button/Button.tsx"
48
69
  export { Button } from "./Button/Button.tsx"
70
+ export type { ButtonLinkProps } from "./ButtonLink/ButtonLink.tsx"
71
+ export { ButtonLink } from "./ButtonLink/ButtonLink.tsx"
49
72
  export type {
50
73
  CardPadding,
51
74
  CardProps,
@@ -66,8 +89,10 @@ export { Combobox } from "./Combobox/Combobox.tsx"
66
89
  export type { BadgeSize } from "./controlStyles.ts"
67
90
  export {
68
91
  BADGE_SIZE_CLASS,
92
+ CONTROL_BASE_CLASS,
69
93
  CONTROL_SIZE_CLASS,
70
94
  DOT_SIZE_CLASS,
95
+ getControlClassName,
71
96
  ICON_CONTROL_SIZE_CLASS,
72
97
  MIN_TOUCH_TARGET_CLASS,
73
98
  SPINNER_SIZE_CLASS,
@@ -92,6 +117,7 @@ export type { IconButtonProps } from "./IconButton/IconButton.tsx"
92
117
  export { IconButton } from "./IconButton/IconButton.tsx"
93
118
  export type { IntentAppearance } from "./intentStyles.ts"
94
119
  export {
120
+ ARIA_DISABLED_CLASS,
95
121
  DISABLED_CLASS,
96
122
  FOCUS_RING_CLASS,
97
123
  INTENT_APPEARANCE_CLASS,
@@ -148,6 +174,16 @@ export type {
148
174
  RadioItem,
149
175
  } from "./RadioGroup/RadioGroup.tsx"
150
176
  export { RadioGroup } from "./RadioGroup/RadioGroup.tsx"
177
+ export { AnchorLink } from "./RouterLink/AnchorLink.tsx"
178
+ export {
179
+ RouterLinkProvider,
180
+ useRouterLink,
181
+ } from "./RouterLink/RouterLinkProvider.tsx"
182
+ export type {
183
+ RouterLinkComponent,
184
+ RouterLinkProps,
185
+ } from "./RouterLink/routerLink.ts"
186
+ export { getIsRoutedHref } from "./RouterLink/routerLink.ts"
151
187
  export type {
152
188
  SegmentedControlProps,
153
189
  SegmentedItem,
@@ -196,6 +232,11 @@ export type {
196
232
  TabsProps,
197
233
  } from "./Tabs/Tabs.tsx"
198
234
  export { Tabs } from "./Tabs/Tabs.tsx"
235
+ export type {
236
+ TextLinkAppearance,
237
+ TextLinkProps,
238
+ } from "./TextLink/TextLink.tsx"
239
+ export { TextLink } from "./TextLink/TextLink.tsx"
199
240
  export type { ToastProps } from "./Toast/Toast.tsx"
200
241
  export { Toast } from "./Toast/Toast.tsx"
201
242
  export type {
@@ -205,3 +205,22 @@ export const FOCUS_RING_CLASS =
205
205
  */
206
206
  export const DISABLED_CLASS =
207
207
  "disabled:cursor-not-allowed disabled:border-border-subtle disabled:bg-surface-sunken disabled:text-content-disabled disabled:shadow-none"
208
+
209
+ /**
210
+ * The same treatment for something that cannot *be* disabled.
211
+ *
212
+ * **`disabled` is not a thing an `<a>` has.** There is no attribute,
213
+ * no `:disabled` match, and no browser behaviour to inherit — so a
214
+ * "disabled link" is entirely something a component has to build, and
215
+ * the usual build of it is a focusable `<a href>` that silently does
216
+ * nothing when pressed. `ButtonLink` instead drops `href` (the
217
+ * platform's own way to make an anchor inert and unfocusable) and
218
+ * says so with `aria-disabled`, which is what this paints against.
219
+ *
220
+ * `pointer-events-none` rather than `cursor-not-allowed`: without a
221
+ * real `:disabled`, the `hover:` classes still match — CSS `:hover`
222
+ * fires on disabled elements too — so the hover fill has to be taken
223
+ * off the table rather than merely overridden.
224
+ */
225
+ export const ARIA_DISABLED_CLASS =
226
+ "aria-disabled:pointer-events-none aria-disabled:border-border-subtle aria-disabled:bg-surface-sunken aria-disabled:text-content-disabled aria-disabled:shadow-none"
@@ -0,0 +1,30 @@
1
+ import type { ReactNode } from "react"
2
+ import { Link } from "react-router"
3
+
4
+ import type { RouterLinkProps } from "../RouterLink/routerLink.ts"
5
+
6
+ /**
7
+ * react-router's `<Link>`, wearing the seam's `href`.
8
+ *
9
+ * The entire adapter, and that is the point: the seam's contract is
10
+ * "a component that takes `href`", so an adapter is a rename. wouter,
11
+ * TanStack Router, and a hand-rolled router each get the same four
12
+ * lines, written in the app rather than here.
13
+ *
14
+ * `to` versus `href` is the only mismatch worth naming. Everything
15
+ * else — `className`, `rel`, `target`, `aria-*`, the ref — passes
16
+ * through untouched, because react-router renders a real `<a>` and
17
+ * forwards what it does not understand.
18
+ *
19
+ * Imported from `react-router`, not `react-router-dom`. v7 and v8
20
+ * both export `Link` from the former, and `react-router-dom` v7 (what
21
+ * bambuddy has) is a re-export shim over exactly that module — so one
22
+ * import covers both installs, while naming `react-router-dom` would
23
+ * cover only one.
24
+ */
25
+ export const ReactRouterLink = ({
26
+ href,
27
+ ...linkProps
28
+ }: RouterLinkProps): ReactNode => (
29
+ <Link {...linkProps} to={href} />
30
+ )
@@ -0,0 +1,29 @@
1
+ /**
2
+ * `@charcuterie/ui/react-router` — the react-router adapter for the
3
+ * link seam.
4
+ *
5
+ * **A subpath export with an optional peer dependency**, and both
6
+ * halves of that matter. The base package must stay router-free: the
7
+ * fleet runs react-router v8, react-router-dom v7, wouter-preact, two
8
+ * hand-rolled routers, and five apps with no router at all, so a
9
+ * `react-router` in `@charcuterie/ui`'s dependencies would be a
10
+ * package eleven consumers install and six of them use. Optional
11
+ * peer + separate entry point means an app that never imports this
12
+ * path never resolves `react-router`, and `sourceRules.test.ts`
13
+ * asserts exactly that — the main entry is measured for what it
14
+ * reaches, so this cannot leak into it by accident.
15
+ *
16
+ * The wiring is one provider at the app root; `Guides/Routing` in
17
+ * Storybook is the copy-pasteable version.
18
+ *
19
+ * ```tsx
20
+ * import { RouterLinkProvider } from "@charcuterie/ui"
21
+ * import { ReactRouterLink } from "@charcuterie/ui/react-router"
22
+ *
23
+ * <RouterLinkProvider link={ReactRouterLink}>
24
+ * <RouterProvider router={router} />
25
+ * </RouterLinkProvider>
26
+ * ```
27
+ */
28
+
29
+ export { ReactRouterLink } from "./ReactRouterLink.tsx"
@@ -240,13 +240,22 @@ test("the barrel is the only place components are re-exported", async () => {
240
240
  // sibling *through* `index.ts` turns every component into a
241
241
  // dependency of every other one, which is how a 3 KB `Spinner`
242
242
  // starts pulling in `MediaTile`.
243
+ //
244
+ // Comments are stripped first, for the same reason the colour rule
245
+ // strips them: a docstring showing a *consumer* how to wire
246
+ // `RouterLinkProvider` has to spell `from "@charcuterie/ui"`, and a
247
+ // rule that cannot tell a citation from an import is a rule that
248
+ // gets switched off. An actual import is never inside a comment.
243
249
  const offenders = componentFiles
244
250
  .filter((one) => one.file !== "index.ts")
245
- .filter(
246
- (one) =>
247
- /from "\.\.?\/index\.ts"/.test(one.contents) ||
248
- /from "@charcuterie\/ui"/.test(one.contents),
249
- )
251
+ .filter((one) => {
252
+ const code = stripComments(one.contents)
253
+
254
+ return (
255
+ /from "\.\.?\/index\.ts"/.test(code) ||
256
+ /from "@charcuterie\/ui"/.test(code)
257
+ )
258
+ })
250
259
  .map((one) => one.file)
251
260
 
252
261
  expect(offenders).toEqual([])
@@ -313,7 +322,27 @@ test("the barrel is the only place components are re-exported", async () => {
313
322
  // because both registrations are effects — and stays out of this
314
323
  // count and the barrel by the `<Name>/<Name>.tsx` rule, exactly as
315
324
  // `SegmentedOption` does.
316
- expect(componentNames.length).toBe(35)
325
+ //
326
+ // `AdaptiveGrid` is the library's first *layout* component, and the
327
+ // first one whose reason to exist is a rule rather than a widget:
328
+ // the wrapping grids in this fleet are `auto-fill, minmax()`, which
329
+ // takes every column the window allows and lands on seven items
330
+ // strung across an ultrawide. It spends height first instead, and
331
+ // it is a component rather than only a hook because three things
332
+ // about the markup are silent when they are wrong — the
333
+ // measured box must not be the capped box, `min-w-0` has to reach
334
+ // children the caller owns, and the track list must be an inline
335
+ // style because Tailwind cannot scan an interpolated class: +1 -> 36.
336
+ // The link family closes the gap that made seven repos hand-roll a
337
+ // back-link and made `Button` get used for navigation: `TextLink`
338
+ // and `ButtonLink`, both a real `<a href>`, differing in paint
339
+ // rather than semantics: +2 -> 38, on top of `AdaptiveGrid`'s 36.
340
+ // `AnchorLink`,
341
+ // `RouterLinkProvider`, and `ReactRouterLink` are the seam rather
342
+ // than components anybody stories, and live in `RouterLink/` and
343
+ // `reactRouter/` — outside this count by the `<Name>/<Name>.tsx`
344
+ // rule, exactly as `Overlay/`'s parts are.
345
+ expect(componentNames.length).toBe(38)
317
346
 
318
347
  for (const name of componentNames) {
319
348
  expect(barrel).toContain(`export { ${name} }`)
@@ -418,6 +447,12 @@ const ENTRY_POINT_RUNTIMES: Record<
418
447
  "@tanstack/react-virtual",
419
448
  "react",
420
449
  ],
450
+ // The router seam's one shipped adapter, and the reason it is a
451
+ // subpath rather than part of the barrel: `react-router` appears
452
+ // **only** here, as an optional peer, so the eleven consumers of
453
+ // the main entry never resolve it. Six of them do not have a
454
+ // react-router at all.
455
+ "./react-router": ["react", "react-router"],
421
456
  "./testing": [],
422
457
  "./tokens": ["@charcuterie/tokens"],
423
458
  },