@sableui/react 0.17.0 → 0.19.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 (154) hide show
  1. package/AGENTS.md +10 -3
  2. package/CHANGELOG.md +235 -0
  3. package/README.md +11 -4
  4. package/bin/sable.mjs +266 -30
  5. package/dist/{_contract-CDa9-NdB.d.ts → _contract-DlklV0Im.d.ts} +2 -2
  6. package/dist/activity-DT8VK9fx.d.ts +25 -0
  7. package/dist/adapters/github.d.ts +41 -0
  8. package/dist/adapters/github.js +24 -0
  9. package/dist/blocks/index.d.ts +1 -1
  10. package/dist/components/activity-heatmap/activity-heatmap.js +420 -0
  11. package/dist/components/activity-heatmap/activity-heatmap.meta.js +142 -0
  12. package/dist/components/activity-heatmap/index.js +9 -0
  13. package/dist/components/avatar/avatar.js +16 -7
  14. package/dist/components/avatar/avatar.meta.js +43 -10
  15. package/dist/components/avatar/avatar.variants.js +26 -2
  16. package/dist/components/code-block/code-block.js +5 -3
  17. package/dist/components/code-block/code-block.meta.js +6 -0
  18. package/dist/components/collapsible/collapsible.js +6 -5
  19. package/dist/components/collapsible/collapsible.meta.js +14 -5
  20. package/dist/components/data-table/data-table.js +132 -45
  21. package/dist/components/data-table/data-table.meta.js +63 -4
  22. package/dist/components/date-picker/date-picker.js +13 -6
  23. package/dist/components/date-picker/date-picker.meta.js +22 -0
  24. package/dist/components/dialog/dialog.js +25 -5
  25. package/dist/components/dialog/dialog.meta.js +26 -5
  26. package/dist/components/media-image/media-image.js +3 -1
  27. package/dist/components/media-image/media-image.meta.js +15 -1
  28. package/dist/components/media-tile/media-tile.js +13 -1
  29. package/dist/components/media-tile/media-tile.meta.js +10 -0
  30. package/dist/components/menu/menu.js +2 -0
  31. package/dist/components/menu/menu.meta.js +2 -2
  32. package/dist/components/page-header/page-header.js +9 -4
  33. package/dist/components/page-header/page-header.meta.js +26 -1
  34. package/dist/components/panel-group/panel-group.js +17 -1
  35. package/dist/components/panel-group/panel-group.meta.js +43 -3
  36. package/dist/components/pill/pill.meta.js +15 -6
  37. package/dist/components/pill/pill.variants.js +1 -0
  38. package/dist/components/search-input/search-input.js +30 -5
  39. package/dist/components/search-input/search-input.meta.js +29 -4
  40. package/dist/components/section-header/section-header.js +27 -17
  41. package/dist/components/section-header/section-header.meta.js +27 -5
  42. package/dist/components/sheet/sheet.js +15 -3
  43. package/dist/components/sheet/sheet.meta.js +2 -2
  44. package/dist/components/side-nav/side-nav.variants.js +8 -3
  45. package/dist/components/sidebar/index.js +4 -0
  46. package/dist/components/sidebar/sidebar.js +90 -11
  47. package/dist/components/sidebar/sidebar.meta.js +38 -4
  48. package/dist/components/table/table.js +7 -3
  49. package/dist/components/tabs/tabs.js +10 -2
  50. package/dist/components/tabs/tabs.meta.js +4 -0
  51. package/dist/components/workspace/workspace.js +13 -3
  52. package/dist/components/workspace/workspace.meta.js +109 -1
  53. package/dist/file-hashes.json +94 -80
  54. package/dist/foundations/typography.js +13 -0
  55. package/dist/icon/Icon.js +21 -2
  56. package/dist/icon/index.d.ts +36 -1
  57. package/dist/icon/index.js +5 -0
  58. package/dist/icon/names.js +240 -0
  59. package/dist/icon-sets/reicon.js +41 -1
  60. package/dist/index.d.ts +540 -75
  61. package/dist/index.js +18 -1
  62. package/dist/layout/AspectBox.js +2 -5
  63. package/dist/layout/Bleed.js +10 -6
  64. package/dist/layout/Center.js +11 -3
  65. package/dist/layout/Columns.js +6 -3
  66. package/dist/layout/Grid.js +14 -6
  67. package/dist/layout/GridOverlay.js +7 -7
  68. package/dist/layout/Region.js +18 -3
  69. package/dist/layout/Row.js +27 -20
  70. package/dist/layout/Spacer.js +13 -2
  71. package/dist/layout/SplitPane.js +1 -1
  72. package/dist/layout/Stack.js +25 -18
  73. package/dist/layout/index.d.ts +57 -6
  74. package/dist/lib/chart/activity.js +32 -0
  75. package/dist/lib/chart/calendar.js +67 -0
  76. package/dist/lib/chart/chart-tooltip.js +2 -2
  77. package/dist/lib/chart/heat.js +55 -0
  78. package/dist/lib/cn.js +1 -0
  79. package/dist/lib/focus-return.js +53 -0
  80. package/dist/lib/taxonomy.js +1 -0
  81. package/dist/lib/use-media-query.js +41 -0
  82. package/dist/{meta-DX5r5wQL.d.ts → meta-Dfm9AExe.d.ts} +6 -0
  83. package/dist/patterns/dashboard-page.js +23 -17
  84. package/dist/patterns/index.d.ts +7 -2
  85. package/dist/patterns/index.js +5 -1
  86. package/dist/patterns/tracker-page.js +392 -0
  87. package/dist/patterns/tracker-page.meta.js +40 -0
  88. package/dist/presets/index.d.ts +1 -1
  89. package/dist/styles.css +301 -188
  90. package/dist/{theme-provider-BydfM5Z0.d.ts → theme-provider-BUBealk_.d.ts} +1 -1
  91. package/dist/themes/_contract.js +9 -1
  92. package/dist/themes/atelier/index.d.ts +1 -1
  93. package/dist/themes/ember/index.d.ts +1 -1
  94. package/dist/themes/index.d.ts +2 -2
  95. package/dist/themes/meridian/index.d.ts +1 -1
  96. package/dist/themes/poppy/index.d.ts +1 -1
  97. package/dist/themes/prism/index.d.ts +1 -1
  98. package/dist/themes/sable/index.d.ts +1 -1
  99. package/dist/themes/verdant/index.d.ts +1 -1
  100. package/llms/components/activity-heatmap.md +133 -0
  101. package/llms/components/avatar.md +32 -9
  102. package/llms/components/code-block.md +1 -0
  103. package/llms/components/collapsible.md +12 -4
  104. package/llms/components/data-table.md +40 -3
  105. package/llms/components/date-picker.md +15 -1
  106. package/llms/components/dialog.md +34 -3
  107. package/llms/components/media-image.md +12 -0
  108. package/llms/components/media-tile.md +21 -0
  109. package/llms/components/menu.md +1 -1
  110. package/llms/components/page-header.md +16 -1
  111. package/llms/components/panel-group.md +34 -2
  112. package/llms/components/pill.md +24 -5
  113. package/llms/components/search-input.md +37 -4
  114. package/llms/components/section-header.md +28 -4
  115. package/llms/components/sheet.md +2 -2
  116. package/llms/components/sidebar.md +17 -4
  117. package/llms/components/tabs.md +1 -0
  118. package/llms/components/workspace.md +81 -1
  119. package/llms/foundations/typography.md +4 -1
  120. package/llms/layout/aspect-box.md +1 -1
  121. package/llms/layout/bleed.md +1 -1
  122. package/llms/layout/center.md +1 -1
  123. package/llms/layout/columns.md +1 -1
  124. package/llms/layout/grid.md +1 -1
  125. package/llms/layout/region.md +1 -1
  126. package/llms/layout/row.md +9 -1
  127. package/llms/layout/spacer.md +1 -1
  128. package/llms/layout/split-pane.md +1 -1
  129. package/llms/layout/stack.md +1 -1
  130. package/llms/patterns/dashboard-page.md +18 -15
  131. package/llms/patterns/tracker-page.md +450 -0
  132. package/llms/reference/token-intent.md +3 -1
  133. package/llms/reference/utilities.md +4 -1
  134. package/llms/tasks/auditing-an-app.md +35 -1
  135. package/llms/tasks/building-a-page.md +3 -2
  136. package/llms/tasks/building-a-tool.md +101 -5
  137. package/llms/tasks/building-with-sable.md +11 -3
  138. package/llms/tasks/creating-a-preset.md +9 -0
  139. package/llms/tasks/getting-started.md +10 -3
  140. package/llms/tasks/using-presets.md +1 -2
  141. package/llms-full.txt +1215 -89
  142. package/llms.txt +11 -9
  143. package/package.json +7 -1
  144. package/registry.json +1021 -70
  145. package/scripts/adoption.mjs +252 -0
  146. package/scripts/agent-setup.mjs +110 -18
  147. package/scripts/app-css.mjs +129 -0
  148. package/scripts/grade.mjs +178 -2
  149. package/scripts/lint-rules.mjs +126 -0
  150. package/scripts/wiring.mjs +2 -1
  151. package/skills/entry/SKILL.md +1 -1
  152. package/skills/sable/SKILL.md +64 -6
  153. package/skills/sable/agents/openai.yaml +1 -1
  154. package/targets/ios.json +9 -0
@@ -13,9 +13,10 @@ import {
13
13
  } from "react";
14
14
  import { cn } from "../../lib/cn.js";
15
15
  import { GradientArt } from "../gradient-art/index.js";
16
+ import { isPaletteColor } from "../../lib/palette.js";
16
17
  import { mergeRefs } from "../../lib/refs.js";
17
18
  import { useResizeObserver } from "../../lib/use-resize-observer.js";
18
- import { avatarGroupVariants, avatarVariants } from "./avatar.variants.js";
19
+ import { AVATAR_SQUARE_RADIUS, avatarGroupVariants, avatarVariants } from "./avatar.variants.js";
19
20
  import { jsx, jsxs } from "react/jsx-runtime";
20
21
  var AvatarGroupContext = createContext(null);
21
22
  var STATUS_SCALE = {
@@ -26,12 +27,14 @@ var IMAGE_FADE = {
26
27
  xl: "motion-fade",
27
28
  "2xl": "motion-fade"
28
29
  };
29
- function AvatarRoot({ size, className, ...props }) {
30
+ function AvatarRoot({ size, shape, tone, className, ...props }) {
30
31
  const group = useContext(AvatarGroupContext);
32
+ const palette = isPaletteColor(tone) ? tone : void 0;
31
33
  return /* @__PURE__ */ jsx(
32
34
  Base.Root,
33
35
  {
34
- className: cn(avatarVariants({ size: group?.size ?? size }), className),
36
+ "data-palette": palette,
37
+ className: cn(avatarVariants({ size: group?.size ?? size, shape }), palette && "bg-palette-wash", className),
35
38
  ...props
36
39
  }
37
40
  );
@@ -51,7 +54,7 @@ function AvatarFallback({ className, ...props }) {
51
54
  return /* @__PURE__ */ jsx(
52
55
  Base.Fallback,
53
56
  {
54
- className: cn("text-muted-foreground", className),
57
+ className: cn("text-muted-foreground [[data-palette]>&]:text-palette-text", className),
55
58
  ...props
56
59
  }
57
60
  );
@@ -63,6 +66,8 @@ function Avatar({
63
66
  fallback,
64
67
  fallbackDelay,
65
68
  size,
69
+ shape,
70
+ tone,
66
71
  status,
67
72
  variant = "neutral",
68
73
  seed,
@@ -70,7 +75,7 @@ function Avatar({
70
75
  ...props
71
76
  }) {
72
77
  const named = !src && alt ? { role: "img", "aria-label": alt } : null;
73
- const avatar = /* @__PURE__ */ jsxs(AvatarRoot, { size, className: status == null ? className : void 0, ...named, ...props, children: [
78
+ const avatar = /* @__PURE__ */ jsxs(AvatarRoot, { size, shape, tone: variant === "art" ? void 0 : tone, className: status == null ? className : void 0, ...named, ...props, children: [
74
79
  src ? /* @__PURE__ */ jsx(AvatarImage, { src, alt: alt ?? "", className: size ? IMAGE_FADE[size] : void 0 }) : null,
75
80
  variant === "art" ? /* @__PURE__ */ jsx(AvatarFallback, { delay: fallbackDelay, "aria-hidden": named ? true : void 0, className: "size-full", children: /* @__PURE__ */ jsxs(GradientArt, { seed: seed ?? (typeof fallback === "string" && fallback || alt || void 0), className: "flex size-full items-center justify-center text-overlay-foreground", children: [
76
81
  /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "absolute inset-0 bg-overlay/30" }),
@@ -78,14 +83,16 @@ function Avatar({
78
83
  ] }) }) : /* @__PURE__ */ jsx(AvatarFallback, { delay: fallbackDelay, "aria-hidden": named ? true : void 0, children: fallback })
79
84
  ] });
80
85
  if (status == null) return avatar;
81
- return /* @__PURE__ */ jsxs("span", { "data-slot": "avatar-status-anchor", className: cn("relative inline-flex shrink-0 rounded-full", className), children: [
86
+ const anchorRadius = shape === "square" ? AVATAR_SQUARE_RADIUS[size ?? "md"] : "rounded-full";
87
+ return /* @__PURE__ */ jsxs("span", { "data-slot": "avatar-status-anchor", className: cn("relative inline-flex shrink-0", anchorRadius, className), children: [
82
88
  avatar,
83
89
  /* @__PURE__ */ jsx(
84
90
  "span",
85
91
  {
86
92
  "data-slot": "avatar-status",
87
93
  className: cn(
88
- "absolute end-[14.6%] bottom-[14.6%] flex translate-x-1/2 translate-y-1/2 rounded-full rtl:-translate-x-1/2 *:ring-2 *:ring-[color:var(--avatar-group-ring,var(--background))]",
94
+ "absolute flex translate-x-1/2 translate-y-1/2 rounded-full rtl:-translate-x-1/2 *:ring-2 *:ring-[color:var(--avatar-group-ring,var(--background))]",
95
+ shape === "square" ? "end-[6%] bottom-[6%]" : "end-[14.6%] bottom-[14.6%]",
89
96
  size ? STATUS_SCALE[size] : void 0
90
97
  ),
91
98
  children: status
@@ -110,6 +117,8 @@ function AvatarGroupCount({
110
117
  }
111
118
  AvatarGroupCount.displayName = "AvatarGroupCount";
112
119
  var FIT_MIN_WIDTH = {
120
+ "2xs": "min-w-9",
121
+ xs: "min-w-11",
113
122
  sm: "min-w-15",
114
123
  md: "min-w-[--spacing(18.5)]",
115
124
  lg: "min-w-22"
@@ -1,7 +1,7 @@
1
1
  // src/components/avatar/avatar.meta.ts
2
2
  var avatarMeta = {
3
3
  name: "Avatar",
4
- summary: 'Circular profile image over Base UI Avatar \u2014 automatic image-load tracking with an initials/glyph fallback. Ergonomic Avatar plus low-level AvatarRoot/AvatarImage/AvatarFallback parts, and AvatarGroup for an overlapping stack with a "+N" overflow count.',
4
+ summary: 'Circular (or square) profile image over Base UI Avatar \u2014 automatic image-load tracking with an initials/glyph fallback. Ergonomic Avatar plus low-level AvatarRoot/AvatarImage/AvatarFallback parts, and AvatarGroup for an overlapping stack with a "+N" overflow count.',
5
5
  keywords: [
6
6
  "avatar",
7
7
  "profile",
@@ -14,7 +14,12 @@ var avatarMeta = {
14
14
  "stack",
15
15
  "overflow",
16
16
  "collaborators",
17
- "facepile"
17
+ "facepile",
18
+ "monogram",
19
+ "project mark",
20
+ "workspace icon",
21
+ "org logo",
22
+ "assignee"
18
23
  ],
19
24
  usage: {
20
25
  description: 'Use Avatar to represent a person, team, or account with a compact visual identity. Wrap several in AvatarGroup for collaborators, assignees, or attendees \u2014 it overlaps them, rings each in the surface colour, and collapses the rest into a "+N" count; use the low-level parts only when you need custom image/fallback layout.',
@@ -33,12 +38,16 @@ var avatarMeta = {
33
38
  },
34
39
  {
35
40
  guidance: "do",
36
- description: "Pick the size by the job: sm in dense rows and tables, md (the default) beside one line of text, lg in a row with a title and subtitle, xl (96px) for a profile header beside the person's name, 2xl (160px) for an artist or profile hero and roster portraits. Keep one size within a list or group."
41
+ description: "Pick the size by the job: 2xs (20px) or xs (24px) in a compact workspace row (an assignee in an issue list, a project mark in a nav item), sm in dense rows and tables, md (the default) beside one line of text, lg in a row with a title and subtitle, xl (96px) for a profile header beside the person's name, 2xl (160px) for an artist or profile hero and roster portraits. Keep one size within a list or group."
37
42
  },
38
43
  {
39
44
  guidance: "do",
40
45
  description: 'Use AvatarGroup for a stack of people, and give it an aria-label with the real count ("12 collaborators"). Set `surface` (or `--avatar-group-ring`) to whatever the group sits on, so the separating ring matches a card or popover, not the page. When it shares a row with actions, set `fit` so it gives up avatars to the "+N" count before the row has to wrap.'
41
46
  },
47
+ {
48
+ guidance: "do",
49
+ description: 'Use `shape="square"` for what is not a person (a workspace, a project, an org, a team), with `tone` set to a palette family so each one keeps its own colour: `<Avatar shape="square" tone="teal" fallback="N" alt="Northwind storefront" size="xs" />`. Circles mean people; keep that distinction across the app.'
50
+ },
42
51
  {
43
52
  guidance: "dont",
44
53
  description: 'Hand-build a stack with negative margins and per-avatar ring classes, or fake the overflow as an Avatar with "+4" initials \u2014 screen readers hear "plus 4" with no context. Use AvatarGroup\'s `max`/`total`, or AvatarGroupCount.'
@@ -53,7 +62,7 @@ var avatarMeta = {
53
62
  },
54
63
  {
55
64
  guidance: "dont",
56
- description: 'Build a portrait by stretching an Avatar with size classes (`className="size-40"`) \u2014 the initials, the status indicator and the image fade stay at row scale. Use size="xl" or "2xl", which scale all three. Past 160px, or for a non-circular cover, it is media: an AspectBox, not an avatar.'
65
+ description: 'Resize an Avatar with size classes (`className="size-5"`, `className="size-40"`) \u2014 the initials, the status indicator and the image fade stay at the size they were drawn for. Use size="2xs"/"xs" for dense rows and size="xl"/"2xl" for portraits, which scale all three. Don\'t hand-build a tinted initials tile for a project or workspace mark either: that is Avatar shape="square" with a tone. Past 160px, or for a cover, it is media: an AspectBox, not an avatar.'
57
66
  }
58
67
  ],
59
68
  anatomy: [
@@ -118,9 +127,21 @@ var avatarMeta = {
118
127
  },
119
128
  {
120
129
  name: "size",
121
- type: '"sm" | "md" | "lg" | "xl" | "2xl"',
130
+ type: '"2xs" | "xs" | "sm" | "md" | "lg" | "xl" | "2xl"',
122
131
  default: '"md"',
123
- description: "Square footprint: 32px / 40px / 48px for rows; 96px / 160px for portraits (a profile header, an artist hero, a roster tile). The initials, a status indicator and its cut-out ring scale with it, and at the portrait sizes the photo fades in over the resting surface."
132
+ description: "Square footprint: 20px / 24px for compact workspace rows; 32px / 40px / 48px for rows; 96px / 160px for portraits (a profile header, an artist hero, a roster tile). The initials, a status indicator and its cut-out ring scale with it, and at the portrait sizes the photo fades in over the resting surface."
133
+ },
134
+ {
135
+ name: "shape",
136
+ type: '"circle" | "square"',
137
+ default: '"circle"',
138
+ description: "The frame's corner. circle is a person. square is what is not a face \u2014 a workspace, a project, an org, a team \u2014 with a radius from the theme's steps that grows with the size (4px at 20\u201324px to 20px at 160px), so a square theme stays square. A status indicator sits on the square's corner."
139
+ },
140
+ {
141
+ name: "tone",
142
+ type: '"neutral" | "red" | "orange" | "yellow" | "green" | "teal" | "blue" | "purple" | "pink"',
143
+ default: '"neutral"',
144
+ description: `The fallback's colour. neutral is the muted surface; a palette family paints its wash behind the initials and its deep ink on them (a soft Badge's pair, held to contrast in every theme), for a category colour such as one per project. Ignored by variant="art", which colours by seed.`
124
145
  },
125
146
  {
126
147
  name: "variant",
@@ -137,7 +158,7 @@ var avatarMeta = {
137
158
  subcomponents: [
138
159
  {
139
160
  name: "AvatarRoot",
140
- type: "Base UI Avatar.Root props + size + className",
161
+ type: "Base UI Avatar.Root props + size + shape + tone + className",
141
162
  description: "Low-level circular frame for full composition control \u2014 clips the image and holds the `size` axis. Wraps AvatarImage and AvatarFallback."
142
163
  },
143
164
  {
@@ -152,17 +173,18 @@ var avatarMeta = {
152
173
  },
153
174
  {
154
175
  name: "AvatarGroup",
155
- type: '{ size?: "sm" | "md" | "lg"; surface?: "background" | "card" | "popover" | "muted"; max?: number; fit?: boolean; total?: number; countLabel?: (count: number) => string; "aria-label"?: string; className?: string; children: ReactNode }',
176
+ type: '{ size?: "2xs" | "xs" | "sm" | "md" | "lg"; surface?: "background" | "card" | "popover" | "muted"; max?: number; fit?: boolean; total?: number; countLabel?: (count: number) => string; "aria-label"?: string; className?: string; children: ReactNode }',
156
177
  description: 'Overlapping stack (role=group) \u2014 name it with aria-label ("12 collaborators"). `size` applies to every avatar inside. `max` shows that many and collapses the rest into a "+N" count; `total` is the real head-count when you only pass some people. `fit` shows only as many avatars as fit the width the group is given (it fills the space left in a flex row) and folds the rest into the count, so a tight row reads "MO SR +5" instead of wrapping. The separating ring reads the inherited `--avatar-group-ring` (default: the page background); `surface` sets it to card, popover, or muted.'
157
178
  },
158
179
  {
159
180
  name: "AvatarGroupCount",
160
- type: '{ count: number; label?: string; size?: "sm" | "md" | "lg"; className?: string }',
181
+ type: '{ count: number; label?: string; size?: "2xs" | "xs" | "sm" | "md" | "lg"; className?: string }',
161
182
  description: 'The "+N" avatar. Shows "+4" and reads "and 4 more" (override with label). AvatarGroup renders it for you from max/total; use it directly only when composing the stack yourself.'
162
183
  }
163
184
  ],
164
185
  variants: {
165
- size: ["sm", "md", "lg", "xl", "2xl"],
186
+ size: ["2xs", "xs", "sm", "md", "lg", "xl", "2xl"],
187
+ shape: ["circle", "square"],
166
188
  variant: ["neutral", "art"]
167
189
  },
168
190
  tokensUsed: ["muted", "muted-foreground", "background", "card", "popover", "overlay", "overlay-foreground"],
@@ -205,6 +227,17 @@ var avatarMeta = {
205
227
  <p className="text-body-sm text-muted-foreground">1.2M monthly listeners</p>
206
228
  </div>
207
229
  </div>`
230
+ },
231
+ {
232
+ label: "Dense row sizes",
233
+ code: `<Avatar fallback="EL" alt="Emma Lee" size="2xs" />
234
+ <Avatar fallback="EL" alt="Emma Lee" size="xs" />`
235
+ },
236
+ {
237
+ label: "Project marks",
238
+ code: `<Avatar shape="square" tone="teal" size="xs" fallback="N" alt="Northwind storefront" />
239
+ <Avatar shape="square" tone="orange" size="xs" fallback="H" alt="Harbor brand refresh" />
240
+ <Avatar shape="square" tone="purple" size="xs" fallback="L" alt="Lumen editorial" />`
208
241
  },
209
242
  {
210
243
  label: "Low-level parts",
@@ -1,19 +1,40 @@
1
1
  // src/components/avatar/avatar.variants.ts
2
2
  import { cva } from "class-variance-authority";
3
+ var AVATAR_SQUARE_RADIUS = {
4
+ "2xs": "rounded-xs",
5
+ xs: "rounded-xs",
6
+ sm: "rounded-sm",
7
+ md: "rounded-sm",
8
+ lg: "rounded-md",
9
+ xl: "rounded-lg",
10
+ "2xl": "rounded-xl"
11
+ };
3
12
  var avatarVariants = cva(
4
- "relative inline-flex shrink-0 select-none items-center justify-center overflow-hidden rounded-full bg-muted",
13
+ "relative inline-flex shrink-0 select-none items-center justify-center overflow-hidden bg-muted",
5
14
  {
6
15
  variants: {
7
16
  size: {
17
+ "2xs": "size-5 text-ui-micro",
18
+ xs: "size-6 text-label-xs",
8
19
  sm: "size-8 text-label-sm",
9
20
  md: "size-10 text-label-md",
10
21
  lg: "size-12 text-label-lg",
11
22
  xl: "size-24 text-heading-2",
12
23
  "2xl": "size-40 text-display-lg"
24
+ },
25
+ shape: {
26
+ circle: "rounded-full",
27
+ square: ""
13
28
  }
14
29
  },
30
+ compoundVariants: Object.entries(AVATAR_SQUARE_RADIUS).map(([size, className]) => ({
31
+ shape: "square",
32
+ size,
33
+ className
34
+ })),
15
35
  defaultVariants: {
16
- size: "md"
36
+ size: "md",
37
+ shape: "circle"
17
38
  }
18
39
  }
19
40
  );
@@ -22,6 +43,8 @@ var avatarGroupVariants = cva(
22
43
  {
23
44
  variants: {
24
45
  size: {
46
+ "2xs": "*:not-first:-ms-1",
47
+ xs: "*:not-first:-ms-1",
25
48
  sm: "*:not-first:-ms-1",
26
49
  md: "*:not-first:-ms-1.5",
27
50
  lg: "*:not-first:-ms-2"
@@ -39,6 +62,7 @@ var avatarGroupVariants = cva(
39
62
  }
40
63
  );
41
64
  export {
65
+ AVATAR_SQUARE_RADIUS,
42
66
  avatarGroupVariants,
43
67
  avatarVariants
44
68
  };
@@ -25,6 +25,7 @@ function CodeBlock({
25
25
  showCopy = true,
26
26
  highlight,
27
27
  prompt,
28
+ wrap = false,
28
29
  className,
29
30
  ...props
30
31
  }) {
@@ -73,10 +74,11 @@ function CodeBlock({
73
74
  "pre",
74
75
  {
75
76
  ref: preRef,
76
- onScroll: fade.sync,
77
- style: fade.style,
77
+ onScroll: wrap ? void 0 : fade.sync,
78
+ style: wrap ? void 0 : fade.style,
78
79
  className: cn(
79
- "overflow-x-auto overscroll-x-contain p-4 text-mono-md text-foreground scrollbar-quiet",
80
+ "p-4 text-mono-md text-foreground",
81
+ wrap ? "whitespace-pre-wrap break-words" : "overflow-x-auto overscroll-x-contain scrollbar-quiet",
80
82
  copyColumn && "min-w-0 flex-1 pe-2"
81
83
  ),
82
84
  children: /* @__PURE__ */ jsx("code", { children: highlighter ? highlighter(code) : code })
@@ -69,6 +69,12 @@ var codeBlockMeta = {
69
69
  name: "prompt",
70
70
  type: "string",
71
71
  description: 'A shell prompt ("$", ">"). Makes it a one-line command: the prompt is dimmed and unselectable, the line truncates, and copy takes only code.'
72
+ },
73
+ {
74
+ name: "wrap",
75
+ type: "boolean",
76
+ default: "false",
77
+ description: "Wrap long lines instead of scrolling them. For prose in a code face (a prompt to paste into an agent, a log line); code keeps scrolling, since wrapping breaks its indentation."
72
78
  }
73
79
  ],
74
80
  subcomponents: [
@@ -5,15 +5,16 @@ import { cn } from "../../lib/cn.js";
5
5
  import { jsx } from "react/jsx-runtime";
6
6
  var Collapsible = Base.Root;
7
7
  var CollapsibleTrigger = Base.Trigger;
8
- function CollapsibleIndicator({ className }) {
8
+ var DIRECTION = {
9
+ down: "[[data-panel-open]_&]:rotate-180",
10
+ end: "-rotate-90 rtl:rotate-90 [[data-panel-open]_&]:rotate-0"
11
+ };
12
+ function CollapsibleIndicator({ direction = "down", className }) {
9
13
  return /* @__PURE__ */ jsx(
10
14
  Icon,
11
15
  {
12
16
  name: "chevron-down",
13
- className: cn(
14
- "text-xs text-muted-foreground motion-interactive group-data-[panel-open]/collapsible:rotate-180",
15
- className
16
- )
17
+ className: cn("text-xs text-muted-foreground motion-interactive", DIRECTION[direction], className)
17
18
  }
18
19
  );
19
20
  }
@@ -7,7 +7,12 @@ var collapsibleMeta = {
7
7
  "collapsible",
8
8
  "disclosure",
9
9
  "expand",
10
- "collapse"
10
+ "collapse",
11
+ "chevron",
12
+ "caret",
13
+ "rotate",
14
+ "group header",
15
+ "section toggle"
11
16
  ],
12
17
  usage: {
13
18
  description: "Use Collapsible for a single piece of optional content that can expand in place.",
@@ -87,8 +92,8 @@ var collapsibleMeta = {
87
92
  },
88
93
  {
89
94
  name: "CollapsibleIndicator",
90
- type: "{ className? }",
91
- description: "Auto-rotating disclosure chevron. Place it inside the trigger content and add the `group/collapsible` class to the trigger so it flips 180\xB0 on open."
95
+ type: '{ direction?: "down" | "end"; className? }',
96
+ description: `Auto-rotating disclosure chevron. Place it anywhere inside the trigger content; it reads the trigger's open state itself, no class to add. direction="down" (default) flips 180\xB0 on open; direction="end" points to the inline end while closed and turns down on open \u2014 the caret for a grouped list's section header or a tree row. Use it instead of rotating your own chevron with CSS, which snaps with no transition.`
92
97
  }
93
98
  ],
94
99
  tokensUsed: ["foreground", "muted-foreground"],
@@ -115,7 +120,7 @@ var collapsibleMeta = {
115
120
  slot: "transition",
116
121
  recipe: "motion-interactive",
117
122
  part: "CollapsibleIndicator",
118
- why: "The chevron turns with the panel rather than flipping in one frame: motion-interactive lists `rotate`, the property rotate-180 writes."
123
+ why: "The chevron turns with the panel rather than flipping in one frame: motion-interactive lists `rotate`, the property rotate-180 and rotate-0 write."
119
124
  },
120
125
  {
121
126
  slot: "activity",
@@ -141,7 +146,11 @@ var collapsibleMeta = {
141
146
  },
142
147
  {
143
148
  label: "With rotating chevron",
144
- code: '<Collapsible><CollapsibleTrigger className="group/collapsible" render={<Button variant="ghost">Details <CollapsibleIndicator /></Button>} /><CollapsiblePanel><p className="pt-3">Revealed on toggle.</p></CollapsiblePanel></Collapsible>'
149
+ code: '<Collapsible><CollapsibleTrigger render={<Button variant="ghost">Details <CollapsibleIndicator /></Button>} /><CollapsiblePanel><p className="pt-3">Revealed on toggle.</p></CollapsiblePanel></Collapsible>'
150
+ },
151
+ {
152
+ label: "Group header with a side caret",
153
+ code: '<Collapsible defaultOpen><CollapsibleTrigger render={<Button variant="ghost" size="xs"><CollapsibleIndicator direction="end" />In progress <span className="text-ui-num text-muted-foreground">5</span></Button>} /><CollapsiblePanel><ul className="text-ui-body"><li>Refine product detail page</li><li>Build the ingredient library</li></ul></CollapsiblePanel></Collapsible>'
145
154
  }
146
155
  ]
147
156
  };
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  // src/components/data-table/data-table.tsx
4
- import { useMemo, useState } from "react";
4
+ import { useId, useMemo, useState } from "react";
5
5
  import { Icon } from "../../icon/index.js";
6
6
  import { cn } from "../../lib/cn.js";
7
7
  import { Button } from "../button/button.js";
@@ -15,7 +15,7 @@ import {
15
15
  TableHeader,
16
16
  TableRow
17
17
  } from "../table/index.js";
18
- import { jsx, jsxs } from "react/jsx-runtime";
18
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
19
19
  var ALIGN = {
20
20
  start: "text-start",
21
21
  center: "text-center",
@@ -36,9 +36,18 @@ function DataTable({
36
36
  defaultSort,
37
37
  empty = "No results.",
38
38
  caption,
39
+ groupBy,
40
+ groups,
41
+ collapsible = true,
42
+ expandedGroups,
43
+ onExpandedGroupsChange,
39
44
  className
40
45
  }) {
41
46
  const [sort, setSort] = useState(defaultSort ?? null);
47
+ const [closedGroups, setClosedGroups] = useState(
48
+ () => new Set((groups ?? []).filter((g) => g.defaultExpanded === false).map((g) => g.key))
49
+ );
50
+ const uid = useId();
42
51
  function toggleSort(key) {
43
52
  setSort((prev) => {
44
53
  if (prev?.key !== key) return { key, direction: "asc" };
@@ -63,13 +72,44 @@ function DataTable({
63
72
  return sort.direction === "asc" ? c : -c;
64
73
  });
65
74
  }, [data, sort, columns]);
75
+ const sections = useMemo(() => {
76
+ if (!groupBy) return null;
77
+ const byKey = /* @__PURE__ */ new Map();
78
+ for (const row of data) {
79
+ const key = groupBy(row);
80
+ if (!byKey.has(key)) byKey.set(key, []);
81
+ }
82
+ rows.forEach((row, index) => byKey.get(groupBy(row))?.push({ row, index }));
83
+ const listed = new Map((groups ?? []).map((g) => [g.key, g]));
84
+ const order = [...listed.keys(), ...[...byKey.keys()].filter((k) => !listed.has(k))];
85
+ return order.filter((key) => (byKey.get(key)?.length ?? 0) > 0).map((key) => ({ key, group: listed.get(key), items: byKey.get(key) ?? [] }));
86
+ }, [groupBy, groups, data, rows]);
87
+ const isExpanded = (key) => !collapsible || (expandedGroups ? expandedGroups.has(key) : !closedGroups.has(key));
88
+ function toggleGroup(key) {
89
+ const open = isExpanded(key);
90
+ if (!expandedGroups) {
91
+ setClosedGroups((prev) => {
92
+ const next = new Set(prev);
93
+ if (open) next.add(key);
94
+ else next.delete(key);
95
+ return next;
96
+ });
97
+ }
98
+ if (onExpandedGroupsChange) {
99
+ const next = new Set((sections ?? []).map((s) => s.key).filter(isExpanded));
100
+ if (open) next.delete(key);
101
+ else next.add(key);
102
+ onExpandedGroupsChange(next);
103
+ }
104
+ }
105
+ const shownRows = sections ? sections.flatMap((s) => isExpanded(s.key) ? s.items.map((it) => it.row) : []) : rows;
66
106
  const selectable = Boolean(onSelectedKeysChange);
67
107
  const dataIndex = selectable ? new Map(data.map((row, i) => [row, i])) : null;
68
108
  const keyOf = (row) => {
69
109
  const i = dataIndex?.get(row) ?? 0;
70
110
  return rowKey ? rowKey(row, i) : i;
71
111
  };
72
- const shownKeys = selectable ? rows.map(keyOf) : [];
112
+ const shownKeys = selectable ? shownRows.map(keyOf) : [];
73
113
  const checkedCount = selectable ? shownKeys.filter((k) => selectedKeys?.has(k)).length : 0;
74
114
  const allChecked = shownKeys.length > 0 && checkedCount === shownKeys.length;
75
115
  function setChecked(keys, checked) {
@@ -80,6 +120,50 @@ function DataTable({
80
120
  }
81
121
  onSelectedKeysChange?.(next);
82
122
  }
123
+ const span = columns.length + (selectable ? 1 : 0);
124
+ function renderRow(row, i, hidden = false) {
125
+ const key = selectable ? keyOf(row) : i;
126
+ const checked = selectable && Boolean(selectedKeys?.has(key));
127
+ return /* @__PURE__ */ jsxs(
128
+ TableRow,
129
+ {
130
+ hidden: hidden || void 0,
131
+ "data-selected": checked || isRowSelected?.(row) ? "" : void 0,
132
+ onClick: onRowClick ? () => onRowClick(row) : void 0,
133
+ tabIndex: onRowClick ? 0 : void 0,
134
+ onKeyDown: onRowClick ? (e) => {
135
+ if (e.target !== e.currentTarget) return;
136
+ if (e.key !== "Enter" && e.key !== " ") return;
137
+ e.preventDefault();
138
+ onRowClick(row);
139
+ } : void 0,
140
+ className: onRowClick ? "cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring" : void 0,
141
+ children: [
142
+ selectable ? (
143
+ // A click on the box checks it; it never opens the row.
144
+ /* @__PURE__ */ jsx(TableCell, { className: "w-0", onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsx(
145
+ Checkbox,
146
+ {
147
+ "aria-label": rowSelectionLabel(row),
148
+ checked,
149
+ onCheckedChange: (next) => setChecked([key], next),
150
+ className: "align-middle"
151
+ }
152
+ ) })
153
+ ) : null,
154
+ columns.map((col) => /* @__PURE__ */ jsx(
155
+ TableCell,
156
+ {
157
+ className: cn(col.align && ALIGN[col.align], col.className),
158
+ children: col.cell ? col.cell(row) : row[col.key]
159
+ },
160
+ col.key
161
+ ))
162
+ ]
163
+ },
164
+ rowKey ? rowKey(row, i) : i
165
+ );
166
+ }
83
167
  return /* @__PURE__ */ jsxs(Table, { className, children: [
84
168
  caption ? /* @__PURE__ */ jsx(TableCaption, { children: caption }) : null,
85
169
  /* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsxs(TableRow, { children: [
@@ -139,58 +223,61 @@ function DataTable({
139
223
  );
140
224
  })
141
225
  ] }) }),
142
- /* @__PURE__ */ jsx(TableBody, { children: rows.length === 0 ? /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(
226
+ rows.length === 0 ? /* @__PURE__ */ jsx(TableBody, { children: /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(
143
227
  TableCell,
144
228
  {
145
- colSpan: columns.length + (selectable ? 1 : 0),
229
+ colSpan: span,
146
230
  className: cn(
147
231
  "text-center text-muted-foreground",
148
232
  typeof empty === "string" && "py-8"
149
233
  ),
150
234
  children: empty
151
235
  }
152
- ) }) : rows.map((row, i) => {
153
- const key = selectable ? keyOf(row) : i;
154
- const checked = selectable && Boolean(selectedKeys?.has(key));
155
- return /* @__PURE__ */ jsxs(
156
- TableRow,
157
- {
158
- "data-selected": checked || isRowSelected?.(row) ? "" : void 0,
159
- onClick: onRowClick ? () => onRowClick(row) : void 0,
160
- tabIndex: onRowClick ? 0 : void 0,
161
- onKeyDown: onRowClick ? (e) => {
162
- if (e.target !== e.currentTarget) return;
163
- if (e.key !== "Enter" && e.key !== " ") return;
164
- e.preventDefault();
165
- onRowClick(row);
166
- } : void 0,
167
- className: onRowClick ? "cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring" : void 0,
168
- children: [
169
- selectable ? (
170
- // A click on the box checks it; it never opens the row.
171
- /* @__PURE__ */ jsx(TableCell, { className: "w-0", onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsx(
172
- Checkbox,
173
- {
174
- "aria-label": rowSelectionLabel(row),
175
- checked,
176
- onCheckedChange: (next) => setChecked([key], next),
177
- className: "align-middle"
178
- }
179
- ) })
180
- ) : null,
181
- columns.map((col) => /* @__PURE__ */ jsx(
182
- TableCell,
236
+ ) }) }) : sections ? sections.map(({ key, group, items }, n) => {
237
+ const expanded = isExpanded(key);
238
+ const bodyId = `${uid}-group-${n}`;
239
+ const name = /* @__PURE__ */ jsxs(Fragment, { children: [
240
+ group?.icon,
241
+ /* @__PURE__ */ jsx("span", { children: group?.label ?? key }),
242
+ /* @__PURE__ */ jsx("span", { className: "tabular-nums text-muted-foreground", children: items.length })
243
+ ] });
244
+ return (
245
+ // One tbody per group: a row group to assistive tech, headed by its
246
+ // own header row, so the group rows are never read as data rows.
247
+ /* @__PURE__ */ jsxs(TableBody, { id: bodyId, "data-group": key, children: [
248
+ /* @__PURE__ */ jsx(TableRow, { "data-group-header": "", className: "border-t [tbody>&:hover]:bg-transparent", children: /* @__PURE__ */ jsx(TableHead, { scope: "rowgroup", colSpan: span, className: "h-auto bg-muted/50 py-1 text-foreground", children: /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center justify-between gap-2", children: [
249
+ collapsible ? /* @__PURE__ */ jsxs(
250
+ Button,
183
251
  {
184
- className: cn(col.align && ALIGN[col.align], col.className),
185
- children: col.cell ? col.cell(row) : row[col.key]
186
- },
187
- col.key
188
- ))
189
- ]
190
- },
191
- rowKey ? rowKey(row, i) : i
252
+ variant: "ghost",
253
+ size: "xs",
254
+ bleed: "start",
255
+ "aria-expanded": expanded,
256
+ "aria-controls": bodyId,
257
+ onClick: () => toggleGroup(key),
258
+ className: "gap-2",
259
+ children: [
260
+ /* @__PURE__ */ jsx(
261
+ Icon,
262
+ {
263
+ name: "chevron-down",
264
+ className: cn(
265
+ "text-xs text-muted-foreground motion-interactive",
266
+ // Closed, it points along the reading direction.
267
+ !expanded && "-rotate-90 rtl:rotate-90"
268
+ )
269
+ }
270
+ ),
271
+ name
272
+ ]
273
+ }
274
+ ) : /* @__PURE__ */ jsx("span", { className: "flex min-w-0 items-center gap-2", children: name }),
275
+ group?.action ? /* @__PURE__ */ jsx("div", { className: "flex shrink-0 items-center gap-1", children: group.action }) : null
276
+ ] }) }) }),
277
+ items.map(({ row, index }) => renderRow(row, index, !expanded))
278
+ ] }, key)
192
279
  );
193
- }) })
280
+ }) : /* @__PURE__ */ jsx(TableBody, { children: rows.map((row, i) => renderRow(row, i)) })
194
281
  ] });
195
282
  }
196
283
  DataTable.displayName = "DataTable";