@glyphui/ui 0.0.0-stage → 0.1.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 (112) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +137 -2
  3. package/dist/data-table.d.ts +110 -0
  4. package/dist/data-table.js +225 -0
  5. package/dist/field-DXZD0l8w.js +107 -0
  6. package/dist/field-context-B8cYheXz.js +321 -0
  7. package/dist/form.d.ts +79 -0
  8. package/dist/form.js +84 -0
  9. package/dist/index-BWK4czIG.d.ts +233 -0
  10. package/dist/index.d.ts +2155 -0
  11. package/dist/index.js +1529 -0
  12. package/dist/registry-BGSJoiLq.d.ts +9 -0
  13. package/dist/states-BxPjCUxz.js +287 -0
  14. package/dist/tv-DuMh7UH2.d.ts +6 -0
  15. package/package.json +113 -3
  16. package/src/components/avatar/avatar.styles.ts +22 -0
  17. package/src/components/avatar/avatar.tsx +62 -0
  18. package/src/components/avatar/index.ts +2 -0
  19. package/src/components/badge/badge.styles.ts +69 -0
  20. package/src/components/badge/badge.tsx +30 -0
  21. package/src/components/badge/index.ts +2 -0
  22. package/src/components/button/button.styles.ts +43 -0
  23. package/src/components/button/button.tsx +77 -0
  24. package/src/components/button/index.ts +2 -0
  25. package/src/components/card/card.styles.ts +32 -0
  26. package/src/components/card/card.tsx +91 -0
  27. package/src/components/card/index.ts +11 -0
  28. package/src/components/checkbox/checkbox.tsx +39 -0
  29. package/src/components/checkbox/index.ts +1 -0
  30. package/src/components/container/container.tsx +35 -0
  31. package/src/components/container/index.ts +1 -0
  32. package/src/components/eyebrow/eyebrow.styles.ts +14 -0
  33. package/src/components/eyebrow/eyebrow.tsx +19 -0
  34. package/src/components/eyebrow/index.ts +2 -0
  35. package/src/components/field/field-context.ts +40 -0
  36. package/src/components/field/field.styles.ts +23 -0
  37. package/src/components/field/field.tsx +109 -0
  38. package/src/components/field/index.ts +3 -0
  39. package/src/components/heading/heading.styles.ts +16 -0
  40. package/src/components/heading/heading.tsx +26 -0
  41. package/src/components/heading/index.ts +2 -0
  42. package/src/components/input/index.ts +2 -0
  43. package/src/components/input/input.styles.ts +36 -0
  44. package/src/components/input/input.tsx +33 -0
  45. package/src/components/kbd/index.ts +1 -0
  46. package/src/components/kbd/kbd.tsx +21 -0
  47. package/src/components/label/index.ts +1 -0
  48. package/src/components/label/label.tsx +21 -0
  49. package/src/components/section/index.ts +1 -0
  50. package/src/components/section/section.tsx +39 -0
  51. package/src/components/segmented-control/index.ts +6 -0
  52. package/src/components/segmented-control/segmented-control.tsx +79 -0
  53. package/src/components/select/index.ts +1 -0
  54. package/src/components/select/select.tsx +133 -0
  55. package/src/components/separator/index.ts +1 -0
  56. package/src/components/separator/separator.tsx +29 -0
  57. package/src/components/skeleton/index.ts +1 -0
  58. package/src/components/skeleton/skeleton.tsx +20 -0
  59. package/src/components/spinner/index.ts +2 -0
  60. package/src/components/spinner/spinner.styles.ts +19 -0
  61. package/src/components/spinner/spinner.tsx +42 -0
  62. package/src/components/stack/index.ts +1 -0
  63. package/src/components/stack/stack.tsx +62 -0
  64. package/src/components/switch/index.ts +1 -0
  65. package/src/components/switch/switch.tsx +48 -0
  66. package/src/components/table/index.ts +10 -0
  67. package/src/components/table/table.tsx +90 -0
  68. package/src/components/tabs/index.ts +1 -0
  69. package/src/components/tabs/tabs.tsx +92 -0
  70. package/src/components/text/index.ts +2 -0
  71. package/src/components/text/text.styles.ts +32 -0
  72. package/src/components/text/text.tsx +17 -0
  73. package/src/components/textarea/index.ts +1 -0
  74. package/src/components/textarea/textarea.tsx +27 -0
  75. package/src/components/tooltip/index.ts +1 -0
  76. package/src/components/tooltip/tooltip.tsx +42 -0
  77. package/src/hooks/index.ts +4 -0
  78. package/src/hooks/use-component-config.ts +42 -0
  79. package/src/hooks/use-controllable-state.ts +27 -0
  80. package/src/hooks/use-media-query.ts +14 -0
  81. package/src/hooks/use-scrolled.ts +14 -0
  82. package/src/index.ts +52 -0
  83. package/src/internal/icons.tsx +99 -0
  84. package/src/patterns/app-shell/app-shell-context.ts +27 -0
  85. package/src/patterns/app-shell/app-shell.styles.ts +127 -0
  86. package/src/patterns/app-shell/app-shell.tsx +81 -0
  87. package/src/patterns/app-shell/index.ts +16 -0
  88. package/src/patterns/app-shell/navbar.tsx +119 -0
  89. package/src/patterns/app-shell/page-header.tsx +30 -0
  90. package/src/patterns/app-shell/sidebar.tsx +163 -0
  91. package/src/patterns/async/async.tsx +83 -0
  92. package/src/patterns/async/index.ts +10 -0
  93. package/src/patterns/async/states.tsx +128 -0
  94. package/src/patterns/data-table/data-table.tsx +367 -0
  95. package/src/patterns/data-table/index.ts +2 -0
  96. package/src/patterns/form/form.tsx +150 -0
  97. package/src/patterns/form/index.ts +13 -0
  98. package/src/provider/color-mode.tsx +133 -0
  99. package/src/provider/glyph-context.ts +42 -0
  100. package/src/provider/glyph-provider.tsx +41 -0
  101. package/src/provider/index.ts +18 -0
  102. package/src/provider/theme-scope.tsx +36 -0
  103. package/src/provider/theme-style.tsx +19 -0
  104. package/src/register.ts +69 -0
  105. package/src/themes.ts +56 -0
  106. package/src/utils/cn.ts +12 -0
  107. package/src/utils/compose-refs.ts +11 -0
  108. package/src/utils/registry.ts +9 -0
  109. package/src/utils/styles.ts +13 -0
  110. package/src/utils/tv.ts +7 -0
  111. package/src/utils/tw-merge-config.ts +14 -0
  112. package/styles.css +178 -0
@@ -0,0 +1,9 @@
1
+ import { ComponentEntry } from "@glyphui/tokens";
2
+ import { VariantProps } from "tailwind-variants";
3
+ //#region src/utils/registry.d.ts
4
+ /** Derives a registry entry (variant props + slot names) from a component's `tv()` styles. */
5
+ type StylesEntry<T extends ((...args: any) => any) & {
6
+ slots: unknown;
7
+ }> = ComponentEntry<VariantProps<T>, Extract<keyof NonNullable<T['slots']>, string>>;
8
+ //#endregion
9
+ export { StylesEntry as t };
@@ -0,0 +1,287 @@
1
+ "use client";
2
+ import { E as tv, O as useComponentConfig, S as focusRing, T as transition, _ as Button, b as Spinner, d as InboxIcon, n as useFieldControl, o as CheckIcon, p as MinusIcon, r as AlertIcon, w as themedBorder } from "./field-context-B8cYheXz.js";
3
+ import { Checkbox } from "radix-ui";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ //#region src/components/checkbox/checkbox.tsx
6
+ const checkboxStyles = tv({ slots: {
7
+ root: [
8
+ "peer mt-0.5 inline-flex size-4 shrink-0 cursor-pointer items-center justify-center rounded-xs border-input bg-surface text-primary-foreground shadow-xs",
9
+ themedBorder,
10
+ transition,
11
+ focusRing,
12
+ "hover:border-border-strong aria-invalid:border-danger",
13
+ "data-[state=checked]:border-primary data-[state=checked]:bg-primary",
14
+ "data-[state=indeterminate]:border-primary data-[state=indeterminate]:bg-primary",
15
+ "disabled:cursor-not-allowed disabled:opacity-50"
16
+ ],
17
+ indicator: "flex items-center justify-center [&>svg]:size-3.5 [&>svg]:stroke-[3]"
18
+ } });
19
+ function Checkbox$1(inProps) {
20
+ const [{ className, ...rest }, slots] = useComponentConfig("Checkbox", useFieldControl(inProps));
21
+ const s = checkboxStyles();
22
+ return /* @__PURE__ */ jsx(Checkbox.Root, {
23
+ "data-slot": "checkbox",
24
+ className: s.root({ class: [slots.root, className] }),
25
+ ...rest,
26
+ children: /* @__PURE__ */ jsx(Checkbox.Indicator, {
27
+ className: s.indicator({ class: slots.indicator }),
28
+ children: rest.checked === "indeterminate" ? /* @__PURE__ */ jsx(MinusIcon, {}) : /* @__PURE__ */ jsx(CheckIcon, {})
29
+ })
30
+ });
31
+ }
32
+ //#endregion
33
+ //#region src/components/input/input.styles.ts
34
+ /** Shared by Input, Textarea and the Select trigger so all fields look alike. */
35
+ const fieldControl = [
36
+ "w-full rounded-md border-input bg-surface text-foreground shadow-xs",
37
+ themedBorder,
38
+ transition,
39
+ "hover:border-border-strong",
40
+ "data-invalid:border-danger aria-invalid:border-danger"
41
+ ];
42
+ const inputStyles = tv({
43
+ slots: {
44
+ root: [
45
+ ...fieldControl,
46
+ "flex items-center gap-2 px-3",
47
+ "focus-within:border-ring focus-within:ring-3 focus-within:ring-ring/20",
48
+ "data-invalid:focus-within:ring-danger/20",
49
+ "has-[input:disabled]:cursor-not-allowed has-[input:disabled]:opacity-50"
50
+ ],
51
+ input: "h-full w-full min-w-0 bg-transparent text-foreground outline-none placeholder:text-faint-foreground disabled:cursor-not-allowed",
52
+ adornment: "flex shrink-0 items-center text-faint-foreground [&>svg]:size-4"
53
+ },
54
+ variants: { size: {
55
+ sm: { root: "h-(--glyph-control-sm) text-small" },
56
+ md: { root: "h-(--glyph-control-md) text-small" },
57
+ lg: { root: "h-(--glyph-control-lg) text-body" }
58
+ } },
59
+ defaultVariants: { size: "md" }
60
+ });
61
+ //#endregion
62
+ //#region src/components/input/input.tsx
63
+ function Input(inProps) {
64
+ const [props, slots] = useComponentConfig("Input", useFieldControl(inProps));
65
+ const { size, prefix, suffix, className, inputClassName, ...rest } = props;
66
+ const s = inputStyles({ size });
67
+ const invalid = rest["aria-invalid"] === true || rest["aria-invalid"] === "true";
68
+ return /* @__PURE__ */ jsxs("div", {
69
+ "data-slot": "input",
70
+ "data-invalid": invalid || void 0,
71
+ className: s.root({ class: [slots.root, className] }),
72
+ children: [
73
+ prefix && /* @__PURE__ */ jsx("span", {
74
+ className: s.adornment({ class: slots.adornment }),
75
+ children: prefix
76
+ }),
77
+ /* @__PURE__ */ jsx("input", {
78
+ className: s.input({ class: [slots.input, inputClassName] }),
79
+ ...rest
80
+ }),
81
+ suffix && /* @__PURE__ */ jsx("span", {
82
+ className: s.adornment({ class: slots.adornment }),
83
+ children: suffix
84
+ })
85
+ ]
86
+ });
87
+ }
88
+ //#endregion
89
+ //#region src/components/skeleton/skeleton.tsx
90
+ const skeletonStyles = tv({ slots: { base: "animate-pulse rounded-sm bg-muted" } });
91
+ /** Placeholder block shown while content loads. Size it with classes: `h-4 w-32`. */
92
+ function Skeleton(inProps) {
93
+ const [{ className, ...rest }, slots] = useComponentConfig("Skeleton", inProps);
94
+ return /* @__PURE__ */ jsx("div", {
95
+ "data-slot": "skeleton",
96
+ "aria-hidden": "true",
97
+ className: skeletonStyles().base({ class: [slots.base, className] }),
98
+ ...rest
99
+ });
100
+ }
101
+ //#endregion
102
+ //#region src/components/table/table.tsx
103
+ const tableStyles = tv({ slots: {
104
+ wrapper: "relative w-full overflow-x-auto",
105
+ table: "w-full caption-bottom border-collapse text-small tabular-nums",
106
+ header: "[&_tr]:border-b [&_tr]:border-border",
107
+ body: "[&_tr:last-child]:border-0",
108
+ row: "border-b border-border transition-colors duration-(--glyph-duration-fast) hover:bg-muted/50 data-[state=selected]:bg-primary/6",
109
+ head: "h-10 whitespace-nowrap px-3 first:pl-4 last:pr-4 text-left align-middle text-caption font-medium text-muted-foreground [&:has([role=checkbox])]:w-10 [&:has([role=checkbox])]:pr-0",
110
+ cell: "px-3 py-2.5 align-middle first:pl-4 last:pr-4 [&:has([role=checkbox])]:pr-0",
111
+ caption: "mt-3 text-small text-faint-foreground"
112
+ } });
113
+ const s = tableStyles();
114
+ function useSlots() {
115
+ return useComponentConfig("Table", {})[1];
116
+ }
117
+ /** Semantic table primitives styled by the theme. For sorting and pagination use DataTable. */
118
+ function Table({ className, ...props }) {
119
+ const slots = useSlots();
120
+ return /* @__PURE__ */ jsx("div", {
121
+ "data-slot": "table-wrapper",
122
+ className: s.wrapper({ class: slots.wrapper }),
123
+ children: /* @__PURE__ */ jsx("table", {
124
+ "data-slot": "table",
125
+ className: s.table({ class: [slots.table, className] }),
126
+ ...props
127
+ })
128
+ });
129
+ }
130
+ function TableHeader({ className, ...props }) {
131
+ const slots = useSlots();
132
+ return /* @__PURE__ */ jsx("thead", {
133
+ "data-slot": "table-header",
134
+ className: s.header({ class: [slots.header, className] }),
135
+ ...props
136
+ });
137
+ }
138
+ function TableBody({ className, ...props }) {
139
+ const slots = useSlots();
140
+ return /* @__PURE__ */ jsx("tbody", {
141
+ "data-slot": "table-body",
142
+ className: s.body({ class: [slots.body, className] }),
143
+ ...props
144
+ });
145
+ }
146
+ function TableRow({ className, ...props }) {
147
+ const slots = useSlots();
148
+ return /* @__PURE__ */ jsx("tr", {
149
+ "data-slot": "table-row",
150
+ className: s.row({ class: [slots.row, className] }),
151
+ ...props
152
+ });
153
+ }
154
+ function TableHead({ className, ...props }) {
155
+ const slots = useSlots();
156
+ return /* @__PURE__ */ jsx("th", {
157
+ "data-slot": "table-head",
158
+ className: s.head({ class: [slots.head, className] }),
159
+ ...props
160
+ });
161
+ }
162
+ function TableCell({ className, ...props }) {
163
+ const slots = useSlots();
164
+ return /* @__PURE__ */ jsx("td", {
165
+ "data-slot": "table-cell",
166
+ className: s.cell({ class: [slots.cell, className] }),
167
+ ...props
168
+ });
169
+ }
170
+ function TableCaption({ className, ...props }) {
171
+ const slots = useSlots();
172
+ return /* @__PURE__ */ jsx("caption", {
173
+ "data-slot": "table-caption",
174
+ className: s.caption({ class: [slots.caption, className] }),
175
+ ...props
176
+ });
177
+ }
178
+ //#endregion
179
+ //#region src/patterns/async/states.tsx
180
+ const stateStyles = tv({
181
+ slots: {
182
+ root: "flex flex-col items-center justify-center gap-3 px-6 py-12 text-center",
183
+ icon: "flex size-10 items-center justify-center rounded-md border border-border bg-surface text-muted-foreground shadow-xs [&>svg]:size-5",
184
+ text: "grid max-w-sm gap-1",
185
+ title: "text-body font-medium text-foreground",
186
+ description: "text-small text-muted-foreground",
187
+ action: "mt-1 flex items-center gap-2"
188
+ },
189
+ variants: { tone: {
190
+ neutral: {},
191
+ danger: { icon: "border-danger/30 bg-danger/10 text-danger" }
192
+ } }
193
+ });
194
+ /** Shown when a list or page has no content yet. Say what will appear and how to add it. */
195
+ function EmptyState(inProps) {
196
+ const [props, slots] = useComponentConfig("EmptyState", inProps);
197
+ const { icon = /* @__PURE__ */ jsx(InboxIcon, {}), title, description, action, className, ...rest } = props;
198
+ const s = stateStyles({ tone: "neutral" });
199
+ return /* @__PURE__ */ jsxs("div", {
200
+ "data-slot": "empty-state",
201
+ className: s.root({ class: [slots.root, className] }),
202
+ ...rest,
203
+ children: [
204
+ /* @__PURE__ */ jsx("div", {
205
+ className: s.icon({ class: slots.icon }),
206
+ "aria-hidden": "true",
207
+ children: icon
208
+ }),
209
+ /* @__PURE__ */ jsxs("div", {
210
+ className: s.text({ class: slots.text }),
211
+ children: [/* @__PURE__ */ jsx("p", {
212
+ className: s.title({ class: slots.title }),
213
+ children: title
214
+ }), description && /* @__PURE__ */ jsx("p", {
215
+ className: s.description({ class: slots.description }),
216
+ children: description
217
+ })]
218
+ }),
219
+ action && /* @__PURE__ */ jsx("div", {
220
+ className: s.action({ class: slots.action }),
221
+ children: action
222
+ })
223
+ ]
224
+ });
225
+ }
226
+ function messageOf(error) {
227
+ if (error instanceof Error) return error.message;
228
+ if (typeof error === "string") return error;
229
+ }
230
+ /** Shown when loading failed. Explains what went wrong and offers a retry. */
231
+ function ErrorState(inProps) {
232
+ const [props, slots] = useComponentConfig("EmptyState", inProps);
233
+ const { icon = /* @__PURE__ */ jsx(AlertIcon, {}), title = "Couldn’t load this", description, error, onRetry, action, className, ...rest } = props;
234
+ const s = stateStyles({ tone: "danger" });
235
+ const detail = description ?? messageOf(error);
236
+ return /* @__PURE__ */ jsxs("div", {
237
+ "data-slot": "error-state",
238
+ role: "alert",
239
+ className: s.root({ class: [slots.root, className] }),
240
+ ...rest,
241
+ children: [
242
+ /* @__PURE__ */ jsx("div", {
243
+ className: s.icon({ class: slots.icon }),
244
+ "aria-hidden": "true",
245
+ children: icon
246
+ }),
247
+ /* @__PURE__ */ jsxs("div", {
248
+ className: s.text({ class: slots.text }),
249
+ children: [/* @__PURE__ */ jsx("p", {
250
+ className: s.title({ class: slots.title }),
251
+ children: title
252
+ }), detail && /* @__PURE__ */ jsx("p", {
253
+ className: s.description({ class: slots.description }),
254
+ children: detail
255
+ })]
256
+ }),
257
+ (action || onRetry) && /* @__PURE__ */ jsxs("div", {
258
+ className: s.action({ class: slots.action }),
259
+ children: [onRetry && /* @__PURE__ */ jsx(Button, {
260
+ variant: "outline",
261
+ size: "sm",
262
+ onClick: () => onRetry(),
263
+ children: "Try again"
264
+ }), action]
265
+ })
266
+ ]
267
+ });
268
+ }
269
+ /** A centered spinner with a label, for content that has no natural skeleton. */
270
+ function LoadingState({ label = "Loading…", className, ...props }) {
271
+ const s = stateStyles();
272
+ return /* @__PURE__ */ jsxs("div", {
273
+ "data-slot": "loading-state",
274
+ role: "status",
275
+ className: s.root({ class: className }),
276
+ ...props,
277
+ children: [/* @__PURE__ */ jsx(Spinner, {
278
+ size: "lg",
279
+ className: "text-faint-foreground"
280
+ }), /* @__PURE__ */ jsx("p", {
281
+ className: s.description(),
282
+ children: label
283
+ })]
284
+ });
285
+ }
286
+ //#endregion
287
+ export { inputStyles as _, Table as a, TableCell as c, TableRow as d, tableStyles as f, fieldControl as g, Input as h, stateStyles as i, TableHead as l, skeletonStyles as m, ErrorState as n, TableBody as o, Skeleton as p, LoadingState as r, TableCaption as s, EmptyState as t, TableHeader as u, Checkbox$1 as v, checkboxStyles as y };
@@ -0,0 +1,6 @@
1
+ import { VariantProps as VariantProps$1 } from "tailwind-variants";
2
+ //#region src/utils/tv.d.ts
3
+ /** `tailwind-variants` preconfigured with Glyph's merge rules. All component styles use this. */
4
+ declare const tv: import("tailwind-variants").TV;
5
+ //#endregion
6
+ export { tv as n, VariantProps$1 as t };
package/package.json CHANGED
@@ -1,6 +1,116 @@
1
1
  {
2
2
  "name": "@glyphui/ui",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.1",
4
+ "description": "React components on Tailwind CSS v4 that take their entire look from one theme file. Forms, data tables, app layouts and async states included.",
5
+ "license": "MIT",
6
+ "author": "Mohak Gupta (https://github.com/khalmohak)",
7
+ "homepage": "https://github.com/khalmohak/glyph#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/khalmohak/glyph.git",
11
+ "directory": "packages/ui"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/khalmohak/glyph/issues"
15
+ },
16
+ "keywords": [
17
+ "react",
18
+ "tailwindcss",
19
+ "tailwind",
20
+ "components",
21
+ "ui",
22
+ "design-system",
23
+ "radix-ui",
24
+ "theme",
25
+ "dark-mode",
26
+ "forms",
27
+ "data-table"
28
+ ],
29
+ "type": "module",
30
+ "sideEffects": [
31
+ "*.css"
32
+ ],
33
+ "exports": {
34
+ ".": {
35
+ "@glyphui/source": "./src/index.ts",
36
+ "types": "./dist/index.d.ts",
37
+ "default": "./dist/index.js"
38
+ },
39
+ "./form": {
40
+ "@glyphui/source": "./src/patterns/form/index.ts",
41
+ "types": "./dist/form.d.ts",
42
+ "default": "./dist/form.js"
43
+ },
44
+ "./data-table": {
45
+ "@glyphui/source": "./src/patterns/data-table/index.ts",
46
+ "types": "./dist/data-table.d.ts",
47
+ "default": "./dist/data-table.js"
48
+ },
49
+ "./styles.css": "./styles.css",
50
+ "./package.json": "./package.json"
51
+ },
52
+ "files": [
53
+ "dist",
54
+ "src",
55
+ "styles.css",
56
+ "!src/**/*.test.tsx",
57
+ "!src/**/*.test-d.tsx",
58
+ "!src/test"
59
+ ],
60
+ "dependencies": {
61
+ "@glyphui/tokens": "^0.1.1",
62
+ "@standard-schema/spec": "^1.1.0",
63
+ "clsx": "^2.1.1",
64
+ "radix-ui": "^1.6.7",
65
+ "tailwind-merge": "^3.7.0",
66
+ "tailwind-variants": "^3.3.1"
67
+ },
68
+ "peerDependencies": {
69
+ "@hookform/resolvers": ">=5.1.0",
70
+ "@tanstack/react-table": "^8.20.0",
71
+ "react": ">=19",
72
+ "react-dom": ">=19",
73
+ "react-hook-form": "^7.55.0",
74
+ "tailwindcss": ">=4.1"
75
+ },
76
+ "peerDependenciesMeta": {
77
+ "@hookform/resolvers": {
78
+ "optional": true
79
+ },
80
+ "@tanstack/react-table": {
81
+ "optional": true
82
+ },
83
+ "react-hook-form": {
84
+ "optional": true
85
+ }
86
+ },
87
+ "devDependencies": {
88
+ "@arethetypeswrong/cli": "^0.18.5",
89
+ "@glyphui/tsconfig": "0.0.0",
90
+ "@hookform/resolvers": "^5.9.1",
91
+ "@tanstack/react-table": "^8.21.3",
92
+ "@testing-library/jest-dom": "^6.10.0",
93
+ "@testing-library/react": "^16.3.3",
94
+ "@testing-library/user-event": "^14.6.7",
95
+ "@types/react": "^19.2.18",
96
+ "@types/react-dom": "^19.2.7",
97
+ "jsdom": "^27.4.0",
98
+ "react": "^19.2.8",
99
+ "react-dom": "^19.2.8",
100
+ "react-hook-form": "^7.89.0",
101
+ "tsdown": "^0.23.0",
102
+ "typescript": "~5.9.3",
103
+ "vitest": "^3.2.7",
104
+ "zod": "^4.6.5"
105
+ },
106
+ "publishConfig": {
107
+ "access": "public",
108
+ "provenance": true
109
+ },
110
+ "scripts": {
111
+ "build": "tsdown",
112
+ "typecheck": "tsc",
113
+ "test": "vitest run",
114
+ "check:exports": "attw --pack . --profile esm-only --exclude-entrypoints ./styles.css"
115
+ }
6
116
  }
@@ -0,0 +1,22 @@
1
+ import { tv, type VariantProps } from '../../utils/tv'
2
+
3
+ export const avatarStyles = tv({
4
+ slots: {
5
+ root: 'relative inline-flex shrink-0 select-none items-center justify-center overflow-hidden rounded-pill bg-muted align-middle',
6
+ image: 'size-full object-cover',
7
+ fallback: 'flex size-full items-center justify-center font-medium text-muted-foreground',
8
+ group:
9
+ 'flex items-center -space-x-2 [&>[data-slot=avatar]]:ring-2 [&>[data-slot=avatar]]:ring-background',
10
+ },
11
+ variants: {
12
+ size: {
13
+ xs: { root: 'size-5', fallback: 'text-[0.5625rem]' },
14
+ sm: { root: 'size-6', fallback: 'text-[0.625rem]' },
15
+ md: { root: 'size-8', fallback: 'text-caption' },
16
+ lg: { root: 'size-11', fallback: 'text-small' },
17
+ },
18
+ },
19
+ defaultVariants: { size: 'md' },
20
+ })
21
+
22
+ export type AvatarVariants = VariantProps<typeof avatarStyles>
@@ -0,0 +1,62 @@
1
+ import { Avatar as AvatarPrimitive } from 'radix-ui'
2
+ import type { ComponentProps, CSSProperties } from 'react'
3
+ import { useComponentConfig } from '../../hooks/use-component-config'
4
+ import { type AvatarVariants, avatarStyles } from './avatar.styles'
5
+
6
+ export interface AvatarProps extends ComponentProps<typeof AvatarPrimitive.Root>, AvatarVariants {
7
+ /** Used for the image alt text and the initials fallback. */
8
+ name: string
9
+ src?: string | undefined
10
+ }
11
+
12
+ function initials(name: string): string {
13
+ const parts = name.trim().split(/\s+/)
14
+ const first = parts[0]?.[0] ?? ''
15
+ const last = parts.length > 1 ? (parts.at(-1)?.[0] ?? '') : ''
16
+ return (first + last).toUpperCase()
17
+ }
18
+
19
+ /** A stable hue per name, so fallback avatars are distinguishable but not random. */
20
+ function hue(name: string): number {
21
+ let h = 0
22
+ for (const char of name) h = (h * 31 + char.charCodeAt(0)) % 360
23
+ return h
24
+ }
25
+
26
+ export function Avatar(inProps: AvatarProps) {
27
+ const [props, slots] = useComponentConfig('Avatar', inProps)
28
+ const { name, src, size, className, style, ...rest } = props
29
+ const s = avatarStyles({ size })
30
+ const tint = { '--avatar-hue': hue(name), ...style } as CSSProperties
31
+ return (
32
+ <AvatarPrimitive.Root
33
+ data-slot="avatar"
34
+ className={s.root({ class: [slots.root, className] })}
35
+ style={tint}
36
+ {...rest}
37
+ >
38
+ {src && (
39
+ <AvatarPrimitive.Image src={src} alt={name} className={s.image({ class: slots.image })} />
40
+ )}
41
+ <AvatarPrimitive.Fallback
42
+ className={s.fallback({
43
+ class: ['bg-[oklch(68%_0.11_var(--avatar-hue)/0.22)] text-foreground', slots.fallback],
44
+ })}
45
+ aria-label={src ? undefined : name}
46
+ >
47
+ {initials(name)}
48
+ </AvatarPrimitive.Fallback>
49
+ </AvatarPrimitive.Root>
50
+ )
51
+ }
52
+
53
+ export function AvatarGroup({ className, ...props }: ComponentProps<'div'>) {
54
+ const [, slots] = useComponentConfig('Avatar', {})
55
+ return (
56
+ <div
57
+ data-slot="avatar-group"
58
+ className={avatarStyles().group({ class: [slots.group, className] })}
59
+ {...props}
60
+ />
61
+ )
62
+ }
@@ -0,0 +1,2 @@
1
+ export { Avatar, AvatarGroup, type AvatarProps } from './avatar'
2
+ export { type AvatarVariants, avatarStyles } from './avatar.styles'
@@ -0,0 +1,69 @@
1
+ import { themedBorder } from '../../utils/styles'
2
+ import { tv, type VariantProps } from '../../utils/tv'
3
+
4
+ // Tinted text is mixed toward the foreground so it stays readable on light and dark surfaces.
5
+ const tones = {
6
+ neutral: {
7
+ soft: 'bg-muted text-muted-foreground',
8
+ solid: 'bg-secondary text-secondary-foreground',
9
+ outline: 'border-border-strong text-foreground',
10
+ },
11
+ primary: {
12
+ soft: 'bg-primary/14 text-[color-mix(in_oklab,var(--color-primary)_62%,var(--color-foreground))]',
13
+ solid: 'bg-primary text-primary-foreground',
14
+ outline:
15
+ 'border-primary/50 text-[color-mix(in_oklab,var(--color-primary)_62%,var(--color-foreground))]',
16
+ },
17
+ success: {
18
+ soft: 'bg-success/14 text-[color-mix(in_oklab,var(--color-success)_62%,var(--color-foreground))]',
19
+ solid: 'bg-success text-success-foreground',
20
+ outline:
21
+ 'border-success/50 text-[color-mix(in_oklab,var(--color-success)_62%,var(--color-foreground))]',
22
+ },
23
+ warning: {
24
+ soft: 'bg-warning/16 text-[color-mix(in_oklab,var(--color-warning)_62%,var(--color-foreground))]',
25
+ solid: 'bg-warning text-warning-foreground',
26
+ outline:
27
+ 'border-warning/50 text-[color-mix(in_oklab,var(--color-warning)_62%,var(--color-foreground))]',
28
+ },
29
+ danger: {
30
+ soft: 'bg-danger/14 text-[color-mix(in_oklab,var(--color-danger)_62%,var(--color-foreground))]',
31
+ solid: 'bg-danger text-danger-foreground',
32
+ outline:
33
+ 'border-danger/50 text-[color-mix(in_oklab,var(--color-danger)_62%,var(--color-foreground))]',
34
+ },
35
+ info: {
36
+ soft: 'bg-info/14 text-[color-mix(in_oklab,var(--color-info)_62%,var(--color-foreground))]',
37
+ solid: 'bg-info text-info-foreground',
38
+ outline:
39
+ 'border-info/50 text-[color-mix(in_oklab,var(--color-info)_62%,var(--color-foreground))]',
40
+ },
41
+ } as const
42
+
43
+ type Tone = keyof typeof tones
44
+ type Variant = keyof (typeof tones)[Tone]
45
+
46
+ export const badgeStyles = tv({
47
+ slots: {
48
+ base: [
49
+ 'inline-flex h-5.5 shrink-0 items-center gap-1.5 whitespace-nowrap rounded-pill border-transparent px-2',
50
+ 'text-caption tabular-nums',
51
+ themedBorder,
52
+ ],
53
+ dot: 'size-1.5 shrink-0 rounded-pill bg-current',
54
+ },
55
+ variants: {
56
+ tone: { neutral: {}, primary: {}, success: {}, warning: {}, danger: {}, info: {} },
57
+ variant: { soft: {}, solid: {}, outline: { base: 'bg-transparent' } },
58
+ },
59
+ compoundVariants: (Object.keys(tones) as Tone[]).flatMap((tone) =>
60
+ (Object.keys(tones[tone]) as Variant[]).map((variant) => ({
61
+ tone,
62
+ variant,
63
+ class: { base: tones[tone][variant] },
64
+ })),
65
+ ),
66
+ defaultVariants: { tone: 'neutral', variant: 'soft' },
67
+ })
68
+
69
+ export type BadgeVariants = VariantProps<typeof badgeStyles>
@@ -0,0 +1,30 @@
1
+ import { Slot } from 'radix-ui'
2
+ import type { ComponentProps } from 'react'
3
+ import { useComponentConfig } from '../../hooks/use-component-config'
4
+ import { type BadgeVariants, badgeStyles } from './badge.styles'
5
+
6
+ export interface BadgeProps extends ComponentProps<'span'>, BadgeVariants {
7
+ /** Leading status dot in the badge's color. */
8
+ dot?: boolean
9
+ asChild?: boolean
10
+ }
11
+
12
+ export function Badge(inProps: BadgeProps) {
13
+ const [props, slots] = useComponentConfig('Badge', inProps)
14
+ const { tone, variant, dot, asChild, className, children, ...rest } = props
15
+ const s = badgeStyles({ tone, variant })
16
+ const Comp = asChild ? Slot.Root : 'span'
17
+ return (
18
+ <Comp
19
+ data-slot="badge"
20
+ data-tone={tone ?? 'neutral'}
21
+ className={s.base({ class: [slots.base, className] })}
22
+ {...rest}
23
+ >
24
+ {dot && (
25
+ <span data-slot="badge-dot" className={s.dot({ class: slots.dot })} aria-hidden="true" />
26
+ )}
27
+ <Slot.Slottable>{children}</Slot.Slottable>
28
+ </Comp>
29
+ )
30
+ }
@@ -0,0 +1,2 @@
1
+ export { Badge, type BadgeProps } from './badge'
2
+ export { type BadgeVariants, badgeStyles } from './badge.styles'
@@ -0,0 +1,43 @@
1
+ import { focusRing, themedBorder, transition } from '../../utils/styles'
2
+ import { tv, type VariantProps } from '../../utils/tv'
3
+
4
+ export const buttonStyles = tv({
5
+ slots: {
6
+ base: [
7
+ 'relative inline-flex shrink-0 cursor-pointer select-none items-center justify-center gap-2 whitespace-nowrap',
8
+ 'rounded-md border-transparent font-medium',
9
+ themedBorder,
10
+ transition,
11
+ focusRing,
12
+ 'disabled:pointer-events-none disabled:opacity-50 aria-busy:cursor-progress',
13
+ ],
14
+ icon: 'inline-flex size-4 shrink-0 items-center justify-center [&>svg]:size-full',
15
+ },
16
+ variants: {
17
+ variant: {
18
+ primary: { base: 'bg-primary text-primary-foreground shadow-sm hover:bg-primary/88' },
19
+ secondary: { base: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/88' },
20
+ outline: {
21
+ base: 'border-border bg-surface text-foreground shadow-xs hover:border-border-strong hover:bg-muted',
22
+ },
23
+ ghost: { base: 'text-muted-foreground hover:bg-muted hover:text-foreground' },
24
+ danger: { base: 'bg-danger text-danger-foreground shadow-sm hover:bg-danger/88' },
25
+ link: { base: 'h-auto px-0 text-primary underline-offset-4 shadow-none hover:underline' },
26
+ },
27
+ size: {
28
+ sm: { base: 'h-(--glyph-control-sm) px-3 text-small', icon: 'size-3.5' },
29
+ md: { base: 'h-(--glyph-control-md) px-4 text-small' },
30
+ lg: { base: 'h-(--glyph-control-lg) px-5 text-body', icon: 'size-[1.125rem]' },
31
+ },
32
+ iconOnly: { true: { base: 'px-0' } },
33
+ fullWidth: { true: { base: 'w-full' } },
34
+ },
35
+ compoundVariants: [
36
+ { iconOnly: true, size: 'sm', class: { base: 'w-(--glyph-control-sm)' } },
37
+ { iconOnly: true, size: 'md', class: { base: 'w-(--glyph-control-md)' } },
38
+ { iconOnly: true, size: 'lg', class: { base: 'w-(--glyph-control-lg)' } },
39
+ ],
40
+ defaultVariants: { variant: 'primary', size: 'md' },
41
+ })
42
+
43
+ export type ButtonVariants = VariantProps<typeof buttonStyles>