@shadow-js/ui 0.0.0-stage → 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 (190) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/LICENSE +21 -0
  3. package/README.md +598 -2
  4. package/dist/cli.mjs +2281 -0
  5. package/dist/dev/cli/schema.d.ts +71 -0
  6. package/dist/dev/index.d.ts +6 -0
  7. package/dist/dev/index.js +5 -0
  8. package/dist/dev/locales/ar.d.ts +139 -0
  9. package/dist/dev/locales/ar.js +127 -0
  10. package/dist/dev/locales/ar.js.map +1 -0
  11. package/dist/dev/locales/en.d.ts +139 -0
  12. package/dist/dev/locales/en.js +127 -0
  13. package/dist/dev/locales/en.js.map +1 -0
  14. package/dist/dev/registry/utils/utils.d.ts +27 -0
  15. package/dist/dev/registry/utils/utils.js +26 -0
  16. package/dist/dev/registry/utils/utils.js.map +1 -0
  17. package/dist/dev/theme/theme-logic.d.ts +185 -0
  18. package/dist/dev/theme/theme-logic.js +183 -0
  19. package/dist/dev/theme/theme-logic.js.map +1 -0
  20. package/dist/dev/theme/theme-provider.d.ts +88 -0
  21. package/dist/dev/theme/theme-provider.js +149 -0
  22. package/dist/dev/theme/theme-provider.js.map +1 -0
  23. package/dist/prod/index.js +1 -0
  24. package/dist/prod/locales/ar.js +2 -0
  25. package/dist/prod/locales/ar.js.map +1 -0
  26. package/dist/prod/locales/en.js +2 -0
  27. package/dist/prod/locales/en.js.map +1 -0
  28. package/dist/prod/registry/utils/utils.js +2 -0
  29. package/dist/prod/registry/utils/utils.js.map +1 -0
  30. package/dist/prod/theme/theme-logic.js +2 -0
  31. package/dist/prod/theme/theme-logic.js.map +1 -0
  32. package/dist/prod/theme/theme-provider.js +2 -0
  33. package/dist/prod/theme/theme-provider.js.map +1 -0
  34. package/package.json +102 -4
  35. package/registry/accordion/accordion.tsx +210 -0
  36. package/registry/accordion/item.json +4 -0
  37. package/registry/alert/alert.tsx +200 -0
  38. package/registry/alert/item.json +4 -0
  39. package/registry/alert-dialog/alert-dialog.tsx +530 -0
  40. package/registry/alert-dialog/item.json +4 -0
  41. package/registry/aspect-ratio/aspect-ratio.tsx +71 -0
  42. package/registry/aspect-ratio/item.json +4 -0
  43. package/registry/attachment/attachment.tsx +459 -0
  44. package/registry/attachment/item.json +4 -0
  45. package/registry/avatar/avatar.tsx +385 -0
  46. package/registry/avatar/item.json +4 -0
  47. package/registry/badge/badge.tsx +108 -0
  48. package/registry/badge/item.json +4 -0
  49. package/registry/breadcrumb/breadcrumb.tsx +354 -0
  50. package/registry/breadcrumb/item.json +4 -0
  51. package/registry/bubble/bubble.tsx +260 -0
  52. package/registry/bubble/item.json +4 -0
  53. package/registry/button/button.tsx +125 -0
  54. package/registry/button/item.json +4 -0
  55. package/registry/button-group/button-group.tsx +189 -0
  56. package/registry/button-group/item.json +4 -0
  57. package/registry/calendar/calendar.tsx +348 -0
  58. package/registry/calendar/item.json +4 -0
  59. package/registry/card/card.tsx +311 -0
  60. package/registry/card/item.json +4 -0
  61. package/registry/carousel/carousel.tsx +851 -0
  62. package/registry/carousel/item.json +4 -0
  63. package/registry/chart/chart.tsx +1956 -0
  64. package/registry/chart/item.json +4 -0
  65. package/registry/chart-scale/chart-scale.ts +670 -0
  66. package/registry/chart-scale/item.json +5 -0
  67. package/registry/checkbox/checkbox.tsx +79 -0
  68. package/registry/checkbox/item.json +4 -0
  69. package/registry/code-block/code-block.tsx +470 -0
  70. package/registry/code-block/item.json +4 -0
  71. package/registry/collapsible/collapsible.tsx +117 -0
  72. package/registry/collapsible/item.json +4 -0
  73. package/registry/combobox/combobox.tsx +977 -0
  74. package/registry/combobox/item.json +4 -0
  75. package/registry/command/command.tsx +757 -0
  76. package/registry/command/item.json +4 -0
  77. package/registry/context-menu/context-menu.tsx +697 -0
  78. package/registry/context-menu/item.json +4 -0
  79. package/registry/data-table/data-table.tsx +437 -0
  80. package/registry/data-table/item.json +4 -0
  81. package/registry/date-picker/date-picker.tsx +349 -0
  82. package/registry/date-picker/item.json +4 -0
  83. package/registry/dialog/dialog.tsx +412 -0
  84. package/registry/dialog/item.json +4 -0
  85. package/registry/direction/direction.tsx +88 -0
  86. package/registry/direction/item.json +4 -0
  87. package/registry/drawer/drawer.tsx +787 -0
  88. package/registry/drawer/item.json +4 -0
  89. package/registry/dropdown-menu/dropdown-menu.tsx +692 -0
  90. package/registry/dropdown-menu/item.json +4 -0
  91. package/registry/empty/empty.tsx +240 -0
  92. package/registry/empty/item.json +4 -0
  93. package/registry/field/field.tsx +624 -0
  94. package/registry/field/item.json +4 -0
  95. package/registry/form/form.tsx +1112 -0
  96. package/registry/form/item.json +4 -0
  97. package/registry/hover-card/hover-card.tsx +144 -0
  98. package/registry/hover-card/item.json +4 -0
  99. package/registry/icons/icons.tsx +1691 -0
  100. package/registry/icons/item.json +4 -0
  101. package/registry/input/input.tsx +43 -0
  102. package/registry/input/item.json +4 -0
  103. package/registry/input-group/input-group.tsx +315 -0
  104. package/registry/input-group/item.json +4 -0
  105. package/registry/input-otp/input-otp.tsx +329 -0
  106. package/registry/input-otp/item.json +4 -0
  107. package/registry/item/item.json +4 -0
  108. package/registry/item/item.tsx +439 -0
  109. package/registry/kbd/item.json +4 -0
  110. package/registry/kbd/kbd.tsx +65 -0
  111. package/registry/label/item.json +4 -0
  112. package/registry/label/label.tsx +60 -0
  113. package/registry/marker/item.json +4 -0
  114. package/registry/marker/marker.tsx +175 -0
  115. package/registry/menubar/item.json +4 -0
  116. package/registry/menubar/menubar.tsx +694 -0
  117. package/registry/message/item.json +4 -0
  118. package/registry/message/message.tsx +280 -0
  119. package/registry/message-scroller/item.json +4 -0
  120. package/registry/message-scroller/message-scroller.tsx +262 -0
  121. package/registry/native-select/item.json +4 -0
  122. package/registry/native-select/native-select.tsx +113 -0
  123. package/registry/navigation-menu/item.json +4 -0
  124. package/registry/navigation-menu/navigation-menu.tsx +362 -0
  125. package/registry/pagination/item.json +4 -0
  126. package/registry/pagination/pagination.tsx +460 -0
  127. package/registry/password-input/item.json +5 -0
  128. package/registry/password-input/password-input.tsx +494 -0
  129. package/registry/popover/item.json +4 -0
  130. package/registry/popover/popover.tsx +284 -0
  131. package/registry/progress/item.json +4 -0
  132. package/registry/progress/progress.tsx +190 -0
  133. package/registry/questionnaire/item.json +4 -0
  134. package/registry/questionnaire/questionnaire.tsx +569 -0
  135. package/registry/radio-group/item.json +4 -0
  136. package/registry/radio-group/radio-group.tsx +101 -0
  137. package/registry/resizable/item.json +4 -0
  138. package/registry/resizable/resizable.tsx +455 -0
  139. package/registry/scroll-area/item.json +4 -0
  140. package/registry/scroll-area/scroll-area.tsx +149 -0
  141. package/registry/select/item.json +4 -0
  142. package/registry/select/select.tsx +571 -0
  143. package/registry/separator/item.json +4 -0
  144. package/registry/separator/separator.tsx +52 -0
  145. package/registry/sheet/item.json +4 -0
  146. package/registry/sheet/sheet.tsx +476 -0
  147. package/registry/sidebar/item.json +4 -0
  148. package/registry/sidebar/sidebar.tsx +1870 -0
  149. package/registry/skeleton/item.json +4 -0
  150. package/registry/skeleton/skeleton.tsx +47 -0
  151. package/registry/slider/item.json +4 -0
  152. package/registry/slider/slider.tsx +149 -0
  153. package/registry/spinner/item.json +4 -0
  154. package/registry/spinner/spinner.tsx +51 -0
  155. package/registry/switch/item.json +4 -0
  156. package/registry/switch/switch.tsx +93 -0
  157. package/registry/table/item.json +4 -0
  158. package/registry/table/table.tsx +373 -0
  159. package/registry/tabs/item.json +4 -0
  160. package/registry/tabs/tabs.tsx +209 -0
  161. package/registry/textarea/item.json +4 -0
  162. package/registry/textarea/textarea.tsx +51 -0
  163. package/registry/toast/item.json +4 -0
  164. package/registry/toast/toast.tsx +949 -0
  165. package/registry/toggle/item.json +4 -0
  166. package/registry/toggle/toggle.tsx +110 -0
  167. package/registry/toggle-group/item.json +4 -0
  168. package/registry/toggle-group/toggle-group.tsx +189 -0
  169. package/registry/tooltip/item.json +4 -0
  170. package/registry/tooltip/tooltip.tsx +169 -0
  171. package/registry/trace/item.json +4 -0
  172. package/registry/trace/trace.tsx +356 -0
  173. package/registry/ui-strings/item.json +5 -0
  174. package/registry/ui-strings/ui-strings.tsx +454 -0
  175. package/registry/use-highlighted-code/item.json +5 -0
  176. package/registry/use-highlighted-code/use-highlighted-code.tsx +145 -0
  177. package/registry/use-mobile/item.json +5 -0
  178. package/registry/use-mobile/use-mobile.ts +46 -0
  179. package/registry/utils/item.json +5 -0
  180. package/registry/utils/utils.ts +38 -0
  181. package/registry.json +3274 -0
  182. package/styles/animations.css +321 -0
  183. package/styles/theme-core.css +55 -0
  184. package/styles/theme.css +12 -0
  185. package/styles/themes/gray.css +69 -0
  186. package/styles/themes/neutral.css +69 -0
  187. package/styles/themes/slate.css +69 -0
  188. package/styles/themes/stone.css +69 -0
  189. package/styles/themes/zinc.css +69 -0
  190. package/styles/typeset.css +251 -0
@@ -0,0 +1,79 @@
1
+ import { splitProps } from "@shadow-js/core";
2
+ import {
3
+ Checkbox as CheckboxPrimitive,
4
+ type CheckboxProps as CheckboxPrimitiveProps,
5
+ type PolymorphicTarget,
6
+ } from "@shadow-js/primitives";
7
+ import type { JSX } from "@shadow-js/web";
8
+ import { CheckIcon, MinusIcon } from "@/components/ui/icons";
9
+ import { type ClassValue, cn } from "@/lib/utils";
10
+
11
+ /**
12
+ * Props of {@link Checkbox}: `checked` (`true`, `false` or `"indeterminate"`), `defaultChecked`,
13
+ * `onCheckedChange`, `disabled`, `required`, `name`, `value` and `form`. Every other prop is an
14
+ * attribute of the rendered `button`, such as `id` or `aria-label`.
15
+ * @see {@link Checkbox}
16
+ * @since 1.0.0
17
+ */
18
+ export type CheckboxProps<T extends PolymorphicTarget = "button"> = CheckboxPrimitiveProps<T>;
19
+
20
+ /** The props as the implementation sees them. */
21
+ type Loose = { readonly class?: ClassValue; readonly [prop: string]: unknown };
22
+
23
+ /**
24
+ * A checkbox that checks, unchecks or marks a choice as mixed with `checked="indeterminate"`. Use it
25
+ * for independent yes/no choices, including a parent of a group of choices.
26
+ *
27
+ * It is controlled with `checked` and `onCheckedChange`, or uncontrolled with `defaultChecked`. With
28
+ * a `name` it takes part in native forms: a checked box submits its `value` (`"on"` by default),
29
+ * `required` blocks submission while unchecked, and a form reset restores `defaultChecked`.
30
+ *
31
+ * Keyboard and ARIA: Space toggles the checkbox. Enter does nothing, as the WAI-ARIA checkbox pattern
32
+ * requires, so it never submits a surrounding form. The element is a `button` with
33
+ * `role="checkbox"`, `aria-checked` (`"true"`, `"false"` or `"mixed"` when indeterminate) and
34
+ * `data-state` (`"checked"`, `"unchecked"` or `"indeterminate"`); `required` sets `aria-required`
35
+ * and `disabled` sets the native `disabled` attribute and `data-disabled`. Clicking an indeterminate
36
+ * checkbox checks it. Name it with a `Label` (`id` on the checkbox, `htmlFor` on the label), which
37
+ * also toggles it when clicked, or with `aria-label`. The indicator is decorative.
38
+ *
39
+ * shadcn/ui differences: `as` replaces `asChild`. The prop is `class`, not `className`, and every element
40
+ * has a `data-slot` (`checkbox` and `checkbox-indicator`).
41
+ *
42
+ * @example
43
+ * import { Checkbox } from "@/components/ui/checkbox";
44
+ * import { Label } from "@/components/ui/label";
45
+ *
46
+ * export function Terms() {
47
+ * return (
48
+ * <>
49
+ * <Checkbox id="terms" defaultChecked />
50
+ * <Label htmlFor="terms">Accept terms and conditions</Label>
51
+ * </>
52
+ * );
53
+ * }
54
+ * @see {@link CheckboxProps}
55
+ * @since 1.0.0
56
+ */
57
+ function Checkbox<T extends PolymorphicTarget = "button">(props: CheckboxProps<T>): JSX.Element {
58
+ const [local, rest] = splitProps(props as unknown as Loose, ["class"]);
59
+ return (
60
+ <CheckboxPrimitive
61
+ data-slot="checkbox"
62
+ class={cn(
63
+ "peer size-4 shrink-0 rounded-[4px] border border-input shadow-xs transition-shadow outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 motion-reduce:transition-none data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=indeterminate]:border-primary data-[state=indeterminate]:bg-primary data-[state=indeterminate]:text-primary-foreground dark:bg-input/30 dark:aria-invalid:ring-destructive/40 dark:data-[state=checked]:bg-primary dark:data-[state=indeterminate]:bg-primary forced-colors:border-[ButtonText] forced-colors:data-[state=checked]:bg-[Highlight] forced-colors:data-[state=checked]:text-[HighlightText] forced-colors:data-[state=indeterminate]:bg-[Highlight] forced-colors:data-[state=indeterminate]:text-[HighlightText] focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-transparent",
64
+ local.class,
65
+ )}
66
+ {...rest}
67
+ >
68
+ <CheckboxPrimitive.Indicator
69
+ data-slot="checkbox-indicator"
70
+ class="grid place-content-center text-current transition-none"
71
+ >
72
+ <CheckIcon class="size-3.5 in-data-[state=indeterminate]:hidden" />
73
+ <MinusIcon class="hidden size-3.5 in-data-[state=indeterminate]:block" />
74
+ </CheckboxPrimitive.Indicator>
75
+ </CheckboxPrimitive>
76
+ );
77
+ }
78
+
79
+ export { Checkbox };
@@ -0,0 +1,4 @@
1
+ {
2
+ "title": "Checkbox",
3
+ "description": "A control that allows the user to toggle between checked, unchecked and indeterminate."
4
+ }
@@ -0,0 +1,470 @@
1
+ import { onCleanup, splitProps, useSignal } from "@shadow-js/core";
2
+ import type { ComponentProps, JSX, JsxChild } from "@shadow-js/web";
3
+ import { Index } from "@shadow-js/web";
4
+ import { cva, type VariantProps } from "class-variance-authority";
5
+ import { Button, type ButtonProps } from "@/components/ui/button";
6
+ import { CheckIcon, CopyIcon } from "@/components/ui/icons";
7
+ import { useUiStrings } from "@/lib/ui-strings";
8
+ import { cn } from "@/lib/utils";
9
+
10
+ /**
11
+ * The class list of a code block for each `variant` and `size`. Use it to style any element like a code block.
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * import { codeBlockVariants } from "@/components/ui/code-block";
16
+ *
17
+ * export const Box = <pre class={codeBlockVariants({ variant: "destructive" })}>boom</pre>;
18
+ * ```
19
+ * @since 1.0.0
20
+ * @see {@link CodeBlock}
21
+ */
22
+ const codeBlockVariants = cva(
23
+ "group/code-block relative flex min-w-0 flex-col overflow-hidden rounded-lg border bg-muted/40 text-foreground",
24
+ {
25
+ variants: {
26
+ variant: {
27
+ default: "",
28
+ destructive: "border-destructive/40 bg-destructive/5 text-destructive",
29
+ },
30
+ size: {
31
+ default: "text-sm",
32
+ sm: "text-xs",
33
+ },
34
+ },
35
+ defaultVariants: { variant: "default", size: "default" },
36
+ },
37
+ );
38
+
39
+ /**
40
+ * Props of {@link CodeBlock}: the attributes of a `div` plus `variant` (`"default"` or `"destructive"`, for an
41
+ * error) and `size` (`"default"` or `"sm"`, for tool and trace output).
42
+ * @since 1.0.0
43
+ * @see {@link CodeBlock}
44
+ */
45
+ export type CodeBlockProps = ComponentProps<"div"> & VariantProps<typeof codeBlockVariants>;
46
+
47
+ /**
48
+ * A block of code, terminal output, a log or a diff. Put an optional {@link CodeBlockHeader} (a title and actions), a
49
+ * {@link CodeBlockContent} and an optional {@link CodeBlockFooter} inside. Without a header, {@link CodeBlockActions}
50
+ * float at the top end and show on hover or focus. The text always reads left to right, even on a right-to-left page, because code does. Streaming is
51
+ * cheap: the content keeps one row for each line, so a growing reply rewrites only the last line.
52
+ *
53
+ * Keyboard and ARIA: the block is a plain `div`. The content is focusable by default (`tabindex="0"`), so a
54
+ * keyboard user can scroll it. Floating actions appear on focus as well as on hover. Line numbers are hidden from assistive technology, and
55
+ * diff lines carry `+` and `-` markers, so color is not the only cue.
56
+ *
57
+ * Compared with shadcn/ui: `class` replaces `className`, and every part has a `data-slot`. Highlighting is `useHighlightedCode`
58
+ * (`@/hooks/use-highlighted-code`).
59
+ *
60
+ * @param props - The block props: `variant`, `size` and the `div` attributes.
61
+ * @returns The block.
62
+ * @example
63
+ * ```tsx
64
+ * import { CodeBlock, CodeBlockContent } from "@/components/ui/code-block";
65
+ *
66
+ * export function Install() {
67
+ * return (
68
+ * <CodeBlock>
69
+ * <CodeBlockContent>{"npm install @shadow-js/ui"}</CodeBlockContent>
70
+ * </CodeBlock>
71
+ * );
72
+ * }
73
+ * ```
74
+ * @since 1.0.0
75
+ * @see {@link CodeBlockContent}
76
+ * @see {@link CodeBlockHeader}
77
+ */
78
+ function CodeBlock(props: CodeBlockProps): JSX.Element {
79
+ const [local, rest] = splitProps(props, ["class", "variant", "size"]);
80
+ return (
81
+ <div
82
+ data-slot="code-block"
83
+ data-variant={local.variant ?? "default"}
84
+ data-size={local.size ?? "default"}
85
+ dir="ltr"
86
+ class={cn(codeBlockVariants({ variant: local.variant, size: local.size }), local.class)}
87
+ {...rest}
88
+ />
89
+ );
90
+ }
91
+
92
+ /**
93
+ * The bar above the code: a {@link CodeBlockTitle} at the start and {@link CodeBlockActions} at the end.
94
+ *
95
+ * Keyboard and ARIA: a plain `div`; no role and no keys.
96
+ *
97
+ * @param props - The `div` attributes and `class`.
98
+ * @returns The header.
99
+ * @example
100
+ * ```tsx
101
+ * import { CodeBlockHeader, CodeBlockTitle } from "@/components/ui/code-block";
102
+ *
103
+ * export const Header = (
104
+ * <CodeBlockHeader>
105
+ * <CodeBlockTitle>src/app.tsx</CodeBlockTitle>
106
+ * </CodeBlockHeader>
107
+ * );
108
+ * ```
109
+ * @since 1.0.0
110
+ * @see {@link CodeBlock}
111
+ */
112
+ function CodeBlockHeader(props: ComponentProps<"div">): JSX.Element {
113
+ const [local, rest] = splitProps(props, ["class"]);
114
+ return (
115
+ <div
116
+ data-slot="code-block-header"
117
+ class={cn(
118
+ "flex min-h-10 items-center justify-between gap-2 border-b px-3 py-1.5",
119
+ local.class,
120
+ )}
121
+ {...rest}
122
+ />
123
+ );
124
+ }
125
+
126
+ /**
127
+ * The title in the header: a file name or a command. It truncates.
128
+ *
129
+ * Keyboard and ARIA: a plain `div`; no role and no keys.
130
+ *
131
+ * @param props - The `div` attributes and `class`.
132
+ * @returns The title.
133
+ * @example
134
+ * ```tsx
135
+ * import { CodeBlockTitle } from "@/components/ui/code-block";
136
+ *
137
+ * export const Title = <CodeBlockTitle>src/app.tsx</CodeBlockTitle>;
138
+ * ```
139
+ * @since 1.0.0
140
+ * @see {@link CodeBlockHeader}
141
+ */
142
+ function CodeBlockTitle(props: ComponentProps<"div">): JSX.Element {
143
+ const [local, rest] = splitProps(props, ["class"]);
144
+ return (
145
+ <div
146
+ data-slot="code-block-title"
147
+ class={cn("min-w-0 truncate font-mono text-xs text-muted-foreground", local.class)}
148
+ {...rest}
149
+ />
150
+ );
151
+ }
152
+
153
+ /**
154
+ * Holds the actions (a copy button, a wrap toggle). In a header it sits at the end; directly in the block, without a header, it floats
155
+ * at the top end and shows on hover or focus.
156
+ *
157
+ * Keyboard and ARIA: a plain `div`; the buttons inside are the focus stops. A toggle button should carry `aria-pressed` or
158
+ * `aria-expanded`, with `aria-controls` pointing at the content's `id`.
159
+ *
160
+ * @param props - The `div` attributes and `class`.
161
+ * @returns The actions container.
162
+ * @example
163
+ * ```tsx
164
+ * import { CodeBlockActions, CodeBlockCopyButton } from "@/components/ui/code-block";
165
+ *
166
+ * export const Actions = (
167
+ * <CodeBlockActions>
168
+ * <CodeBlockCopyButton value="npm install @shadow-js/ui" />
169
+ * </CodeBlockActions>
170
+ * );
171
+ * ```
172
+ * @since 1.0.0
173
+ * @see {@link CodeBlockCopyButton}
174
+ */
175
+ function CodeBlockActions(props: ComponentProps<"div">): JSX.Element {
176
+ const [local, rest] = splitProps(props, ["class"]);
177
+ return (
178
+ <div
179
+ data-slot="code-block-actions"
180
+ class={cn(
181
+ "absolute end-2 top-2 z-10 flex items-center gap-1 opacity-0 transition-opacity group-focus-within/code-block:opacity-100 group-hover/code-block:opacity-100 motion-reduce:transition-none group-has-data-[slot=code-block-header]/code-block:static group-has-data-[slot=code-block-header]/code-block:opacity-100",
182
+ local.class,
183
+ )}
184
+ {...rest}
185
+ />
186
+ );
187
+ }
188
+
189
+ /**
190
+ * Props of {@link CodeBlockCopyButton}: every prop of {@link Button} plus the `value` to copy.
191
+ * @since 1.0.0
192
+ * @see {@link CodeBlockCopyButton}
193
+ */
194
+ export type CodeBlockCopyButtonProps = Omit<ButtonProps, "children" | "onClick"> & {
195
+ /** The text to copy. It is the source, so it never holds line numbers or diff markers. */
196
+ readonly value: string;
197
+ /** Called after the text was copied. @default none */
198
+ readonly onCopy?: (() => void) | undefined;
199
+ /** How long the check mark stays, in milliseconds. @default 2000 */
200
+ readonly feedbackDuration?: number | undefined;
201
+ };
202
+
203
+ /**
204
+ * A ghost icon button that copies `value` to the clipboard, then shows a check mark for two seconds and says "Copied" through a
205
+ * polite live region. It copies the source text you pass, not what is on screen, so line numbers and diff markers are never in it.
206
+ * If the browser refuses (no permission, an insecure page) nothing is shown as copied and the error goes to the page's error reporter.
207
+ *
208
+ * Keyboard and ARIA: a native button (Enter and Space), named "Copy code" (the kit's string `codeBlock.copy`). The text "Copied" is in a visually
209
+ * hidden `role="status"` region that is always present, so its change is announced.
210
+ *
211
+ * @param props - The button props, `value` and `onCopy`.
212
+ * @returns The button.
213
+ * @example
214
+ * ```tsx
215
+ * import { CodeBlockCopyButton } from "@/components/ui/code-block";
216
+ *
217
+ * export const Copy = <CodeBlockCopyButton value="npm install @shadow-js/ui" />;
218
+ * ```
219
+ * @since 1.0.0
220
+ * @see {@link CodeBlockActions}
221
+ */
222
+ function CodeBlockCopyButton(props: CodeBlockCopyButtonProps): JSX.Element {
223
+ const [local, rest] = splitProps(props, [
224
+ "class",
225
+ "value",
226
+ "onCopy",
227
+ "feedbackDuration",
228
+ "variant",
229
+ "size",
230
+ ]);
231
+ const t = useUiStrings();
232
+ const [copied, setCopied] = useSignal(false);
233
+ let timer: ReturnType<typeof setTimeout> | undefined;
234
+ onCleanup(() => clearTimeout(timer));
235
+ const copy = (): void => {
236
+ navigator.clipboard.writeText(local.value).then(
237
+ () => {
238
+ setCopied(true);
239
+ local.onCopy?.();
240
+ clearTimeout(timer);
241
+ timer = setTimeout(() => setCopied(false), local.feedbackDuration ?? 2000);
242
+ },
243
+ (error: unknown) => {
244
+ globalThis.reportError?.(error);
245
+ },
246
+ );
247
+ };
248
+ return (
249
+ <>
250
+ <Button
251
+ data-slot="code-block-copy-button"
252
+ type="button"
253
+ variant={local.variant ?? "ghost"}
254
+ size={local.size ?? "icon-sm"}
255
+ aria-label={t("codeBlock.copy", "Copy code")}
256
+ class={cn("text-muted-foreground", local.class)}
257
+ onClick={copy}
258
+ {...rest}
259
+ >
260
+ {copied() ? <CheckIcon /> : <CopyIcon />}
261
+ </Button>
262
+ <span role="status" class="sr-only">
263
+ {copied() ? t("codeBlock.copied", "Copied") : ""}
264
+ </span>
265
+ </>
266
+ );
267
+ }
268
+
269
+ /**
270
+ * Props of {@link CodeBlockContent}: the attributes of a `pre` plus the options below. `children` is the code as a
271
+ * string, or {@link CodeBlockLine} elements (what `useHighlightedCode` returns).
272
+ * @since 1.0.0
273
+ * @see {@link CodeBlockContent}
274
+ */
275
+ export interface CodeBlockContentProps extends Omit<ComponentProps<"pre">, "children"> {
276
+ /** The code: a string (split into lines), or line elements. @default none */
277
+ readonly children?: string | JsxChild;
278
+ /** Wraps long lines instead of scrolling sideways. Use it for error output. @default false */
279
+ readonly wrap?: boolean | undefined;
280
+ /** Numbers the lines. The numbers are drawn by CSS, so copying the text never includes them. @default false */
281
+ readonly lineNumbers?: boolean | undefined;
282
+ /** The number of the first line. @default 1 */
283
+ readonly startLine?: number | undefined;
284
+ }
285
+
286
+ /** The text of a string as lines, with a final line break not counted as an empty line. */
287
+ function linesOf(text: string): string[] {
288
+ const lines = text.split("\n");
289
+ if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
290
+ return lines;
291
+ }
292
+
293
+ /**
294
+ * The code itself, in a `pre` that scrolls. A string becomes one {@link CodeBlockLine} for each line; pass your own line elements for
295
+ * highlighted tokens or line variants. `lineNumbers` and `startLine` number the lines, `wrap` wraps them, and a `max-h-*` class caps the height
296
+ * (the content scrolls inside).
297
+ *
298
+ * Keyboard and ARIA: the `pre` is focusable (`tabindex="0"`, set `tabindex="-1"` to remove it) so its scrolling works from the keyboard; give it
299
+ * an `aria-label` when the page has several blocks. Line numbers are generated by CSS, so they are not in the text a screen reader reads.
300
+ *
301
+ * @param props - The content props.
302
+ * @returns The content.
303
+ * @example
304
+ * ```tsx
305
+ * import { CodeBlock, CodeBlockContent } from "@/components/ui/code-block";
306
+ *
307
+ * export function Numbered() {
308
+ * return (
309
+ * <CodeBlock>
310
+ * <CodeBlockContent lineNumbers startLine={10} class="max-h-48">
311
+ * {"const a = 1;\nconst b = 2;"}
312
+ * </CodeBlockContent>
313
+ * </CodeBlock>
314
+ * );
315
+ * }
316
+ * ```
317
+ * @since 1.0.0
318
+ * @see {@link CodeBlockLine}
319
+ */
320
+ function CodeBlockContent(props: CodeBlockContentProps): JSX.Element {
321
+ const [local, rest] = splitProps(props, [
322
+ "class",
323
+ "children",
324
+ "wrap",
325
+ "lineNumbers",
326
+ "startLine",
327
+ "tabindex",
328
+ "style",
329
+ ]);
330
+ const text = (): string | undefined =>
331
+ typeof local.children === "string" ? local.children : undefined;
332
+ // The counter starts one before the first line, because each numbered line counts itself up first. Your own style is kept.
333
+ const style = (): ComponentProps<"pre">["style"] => {
334
+ if (local.lineNumbers !== true) return local.style;
335
+ const reset = `line ${(local.startLine ?? 1) - 1}`;
336
+ return typeof local.style === "string"
337
+ ? `counter-reset: ${reset}; ${local.style}`
338
+ : { ...local.style, "counter-reset": reset };
339
+ };
340
+ return (
341
+ <pre
342
+ data-slot="code-block-content"
343
+ data-line-numbers={local.lineNumbers === true ? "" : undefined}
344
+ tabindex={local.tabindex ?? 0}
345
+ style={style()}
346
+ class={cn(
347
+ "group/code-content m-0 min-w-0 overflow-auto p-3 font-mono leading-relaxed outline-none focus-visible:ring-[3px] focus-visible:ring-inset focus-visible:ring-ring/50 focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-transparent",
348
+ local.wrap === true ? "whitespace-pre-wrap break-words" : "whitespace-pre",
349
+ "[&_[data-token]]:text-(--shiki-light) dark:[&_[data-token]]:text-(--shiki-dark)",
350
+ local.class,
351
+ )}
352
+ {...rest}
353
+ >
354
+ <code class="block min-w-fit">
355
+ {text() === undefined ? (
356
+ local.children
357
+ ) : (
358
+ <Index each={linesOf(text() ?? "")}>
359
+ {(line) => <CodeBlockLine>{line()}</CodeBlockLine>}
360
+ </Index>
361
+ )}
362
+ </code>
363
+ </pre>
364
+ );
365
+ }
366
+
367
+ const lineVariants = cva(
368
+ "relative block min-h-[1lh] group-data-[line-numbers]/code-content:before:me-4 group-data-[line-numbers]/code-content:before:inline-block group-data-[line-numbers]/code-content:before:w-8 group-data-[line-numbers]/code-content:before:text-end group-data-[line-numbers]/code-content:before:text-muted-foreground group-data-[line-numbers]/code-content:before:content-[counter(line)] group-data-[line-numbers]/code-content:[counter-increment:line] group-data-[line-numbers]/code-content:before:select-none",
369
+ {
370
+ variants: {
371
+ variant: {
372
+ default: "",
373
+ added:
374
+ "-mx-3 bg-green-500/10 px-3 after:absolute after:start-0.5 after:top-0 after:text-green-600 after:content-['+'] dark:after:text-green-400",
375
+ removed:
376
+ "-mx-3 bg-red-500/10 px-3 after:absolute after:start-0.5 after:top-0 after:text-red-600 after:content-['-'] dark:after:text-red-400",
377
+ highlighted: "-mx-3 bg-accent px-3",
378
+ },
379
+ },
380
+ defaultVariants: { variant: "default" },
381
+ },
382
+ );
383
+
384
+ /**
385
+ * Props of {@link CodeBlockLine}: the attributes of a `span` plus `variant`, which is `"default"`, `"added"`, `"removed"` or `"highlighted"`.
386
+ * @since 1.0.0
387
+ * @see {@link CodeBlockLine}
388
+ */
389
+ export type CodeBlockLineProps = ComponentProps<"span"> & VariantProps<typeof lineVariants>;
390
+
391
+ /**
392
+ * One line of code. `added` and `removed` tint the line and put a `+` or `-` in the margin, drawn by CSS so it is not part of the
393
+ * copied text and not read as code; `highlighted` marks a line to look at.
394
+ *
395
+ * Keyboard and ARIA: a plain `span` with no role and no keys. The diff markers are visual: say what a diff changed in the text around the block.
396
+ *
397
+ * @param props - The line props: `variant` and the `span` attributes.
398
+ * @returns The line.
399
+ * @example
400
+ * ```tsx
401
+ * import { CodeBlock, CodeBlockContent, CodeBlockLine } from "@/components/ui/code-block";
402
+ *
403
+ * export function Diff() {
404
+ * return (
405
+ * <CodeBlock>
406
+ * <CodeBlockContent>
407
+ * <CodeBlockLine variant="removed">const a = 1;</CodeBlockLine>
408
+ * <CodeBlockLine variant="added">const a = 2;</CodeBlockLine>
409
+ * </CodeBlockContent>
410
+ * </CodeBlock>
411
+ * );
412
+ * }
413
+ * ```
414
+ * @since 1.0.0
415
+ * @see {@link CodeBlockContent}
416
+ */
417
+ function CodeBlockLine(props: CodeBlockLineProps): JSX.Element {
418
+ const [local, rest] = splitProps(props, ["class", "variant"]);
419
+ return (
420
+ <span
421
+ data-slot="code-block-line"
422
+ data-variant={local.variant ?? "default"}
423
+ class={cn(lineVariants({ variant: local.variant }), local.class)}
424
+ {...rest}
425
+ />
426
+ );
427
+ }
428
+
429
+ /**
430
+ * A row under the code, for a "Show more" toggle or a note. A toggle button in it should carry `aria-expanded` and `aria-controls` that
431
+ * names the content's `id`.
432
+ *
433
+ * Keyboard and ARIA: a plain `div`; no role and no keys.
434
+ *
435
+ * @param props - The `div` attributes and `class`.
436
+ * @returns The footer.
437
+ * @example
438
+ * ```tsx
439
+ * import { CodeBlockFooter } from "@/components/ui/code-block";
440
+ *
441
+ * export const Footer = <CodeBlockFooter>42 lines</CodeBlockFooter>;
442
+ * ```
443
+ * @since 1.0.0
444
+ * @see {@link CodeBlock}
445
+ */
446
+ function CodeBlockFooter(props: ComponentProps<"div">): JSX.Element {
447
+ const [local, rest] = splitProps(props, ["class"]);
448
+ return (
449
+ <div
450
+ data-slot="code-block-footer"
451
+ class={cn(
452
+ "flex items-center justify-between gap-2 border-t px-3 py-1.5 text-xs text-muted-foreground",
453
+ local.class,
454
+ )}
455
+ {...rest}
456
+ />
457
+ );
458
+ }
459
+
460
+ export {
461
+ CodeBlock,
462
+ CodeBlockActions,
463
+ CodeBlockContent,
464
+ CodeBlockCopyButton,
465
+ CodeBlockFooter,
466
+ CodeBlockHeader,
467
+ CodeBlockLine,
468
+ CodeBlockTitle,
469
+ codeBlockVariants,
470
+ };
@@ -0,0 +1,4 @@
1
+ {
2
+ "title": "Code Block",
3
+ "description": "Code, terminal output, logs and diffs, with an optional header, line numbers, wrapping and a copy button."
4
+ }
@@ -0,0 +1,117 @@
1
+ import { splitProps } from "@shadow-js/core";
2
+ import {
3
+ type CollapsibleContentProps,
4
+ Collapsible as CollapsiblePrimitive,
5
+ type CollapsibleProps,
6
+ type CollapsibleTriggerProps,
7
+ type PolymorphicTarget,
8
+ } from "@shadow-js/primitives";
9
+ import type { JSX } from "@shadow-js/web";
10
+ import { cn } from "@/lib/utils";
11
+
12
+ /**
13
+ * A panel that expands and collapses from a trigger, for one disclosure such as a "show more"
14
+ * section. It is controlled with `open` and `onOpenChange`, or uncontrolled with `defaultOpen`.
15
+ * For a stack of such panels use the accordion.
16
+ *
17
+ * Keyboard and ARIA: Enter and Space on the trigger toggle the panel, because the trigger is a
18
+ * native `button`. The trigger gets `aria-expanded`, and `aria-controls` naming the panel while the
19
+ * panel is mounted. The root and the panel have no role, so the trigger's text (or an `aria-label`)
20
+ * is its name. `data-state` (`"open"` or `"closed"`) is set on the root, the trigger and the panel.
21
+ * A `disabled` collapsible makes the trigger natively `disabled`.
22
+ *
23
+ * shadcn/ui differences: `as` replaces `asChild` on `CollapsibleTrigger`. The prop is `class`, not `className`, and every part has a `data-slot` (`collapsible`, `collapsible-trigger` and
24
+ * `collapsible-content`).
25
+ *
26
+ * @example
27
+ * import { Button } from "@/components/ui/button";
28
+ * import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
29
+ *
30
+ * export function MoreDetails() {
31
+ * return (
32
+ * <Collapsible>
33
+ * <CollapsibleTrigger as={Button} variant="ghost">
34
+ * Show more
35
+ * </CollapsibleTrigger>
36
+ * <CollapsibleContent>Hidden until opened.</CollapsibleContent>
37
+ * </Collapsible>
38
+ * );
39
+ * }
40
+ * @see {@link CollapsibleTrigger}
41
+ * @see {@link CollapsibleContent}
42
+ * @since 1.0.0
43
+ */
44
+ function Collapsible(props: CollapsibleProps): JSX.Element {
45
+ return <CollapsiblePrimitive data-slot="collapsible" {...props} />;
46
+ }
47
+
48
+ /**
49
+ * The button that expands and collapses the panel. It takes the attributes of the element it
50
+ * renders (`as` picks the element, such as a shadcn `Button`).
51
+ *
52
+ * @param props The trigger props: the attributes of the rendered element, with `as` to choose it.
53
+ * @returns The rendered trigger.
54
+ * @throws Error when rendered outside a {@link Collapsible}.
55
+ * @example
56
+ * import { Button } from "@/components/ui/button";
57
+ * import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
58
+ *
59
+ * export function Details() {
60
+ * return (
61
+ * <Collapsible>
62
+ * <CollapsibleTrigger as={Button} variant="ghost" size="sm">
63
+ * Toggle
64
+ * </CollapsibleTrigger>
65
+ * <CollapsibleContent>Details.</CollapsibleContent>
66
+ * </Collapsible>
67
+ * );
68
+ * }
69
+ * @see {@link Collapsible}
70
+ * @since 1.0.0
71
+ */
72
+ function CollapsibleTrigger<T extends PolymorphicTarget = "button">(
73
+ props: CollapsibleTriggerProps<T>,
74
+ ): JSX.Element {
75
+ return <CollapsiblePrimitive.Trigger data-slot="collapsible-trigger" {...props} />;
76
+ }
77
+
78
+ /**
79
+ * The panel that expands and collapses. Its height animates with the `animate-collapsible-down`
80
+ * and `animate-collapsible-up` utilities, which read the measured height from the CSS variable
81
+ * `--primitives-collapsible-content-height`. Content open on the first render does not animate in.
82
+ *
83
+ * @param props The panel props: `class` is merged with the animation classes, and `forceMount`
84
+ * keeps the panel mounted while closed.
85
+ * @returns The rendered panel.
86
+ * @throws Error when rendered outside a {@link Collapsible}.
87
+ * @example
88
+ * import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
89
+ *
90
+ * export function Notes() {
91
+ * return (
92
+ * <Collapsible defaultOpen>
93
+ * <CollapsibleTrigger>Notes</CollapsibleTrigger>
94
+ * <CollapsibleContent class="space-y-2">
95
+ * <p>First note.</p>
96
+ * </CollapsibleContent>
97
+ * </Collapsible>
98
+ * );
99
+ * }
100
+ * @see {@link Collapsible}
101
+ * @since 1.0.0
102
+ */
103
+ function CollapsibleContent(props: CollapsibleContentProps): JSX.Element {
104
+ const [local, rest] = splitProps(props, ["class"]);
105
+ return (
106
+ <CollapsiblePrimitive.Content
107
+ data-slot="collapsible-content"
108
+ class={cn(
109
+ "overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down",
110
+ local.class,
111
+ )}
112
+ {...rest}
113
+ />
114
+ );
115
+ }
116
+
117
+ export { Collapsible, CollapsibleContent, CollapsibleTrigger };
@@ -0,0 +1,4 @@
1
+ {
2
+ "title": "Collapsible",
3
+ "description": "An interactive component which expands/collapses a panel."
4
+ }