@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.
- package/LICENSE +21 -0
- package/README.md +137 -2
- package/dist/data-table.d.ts +110 -0
- package/dist/data-table.js +225 -0
- package/dist/field-DXZD0l8w.js +107 -0
- package/dist/field-context-B8cYheXz.js +321 -0
- package/dist/form.d.ts +79 -0
- package/dist/form.js +84 -0
- package/dist/index-BWK4czIG.d.ts +233 -0
- package/dist/index.d.ts +2155 -0
- package/dist/index.js +1529 -0
- package/dist/registry-BGSJoiLq.d.ts +9 -0
- package/dist/states-BxPjCUxz.js +287 -0
- package/dist/tv-DuMh7UH2.d.ts +6 -0
- package/package.json +113 -3
- package/src/components/avatar/avatar.styles.ts +22 -0
- package/src/components/avatar/avatar.tsx +62 -0
- package/src/components/avatar/index.ts +2 -0
- package/src/components/badge/badge.styles.ts +69 -0
- package/src/components/badge/badge.tsx +30 -0
- package/src/components/badge/index.ts +2 -0
- package/src/components/button/button.styles.ts +43 -0
- package/src/components/button/button.tsx +77 -0
- package/src/components/button/index.ts +2 -0
- package/src/components/card/card.styles.ts +32 -0
- package/src/components/card/card.tsx +91 -0
- package/src/components/card/index.ts +11 -0
- package/src/components/checkbox/checkbox.tsx +39 -0
- package/src/components/checkbox/index.ts +1 -0
- package/src/components/container/container.tsx +35 -0
- package/src/components/container/index.ts +1 -0
- package/src/components/eyebrow/eyebrow.styles.ts +14 -0
- package/src/components/eyebrow/eyebrow.tsx +19 -0
- package/src/components/eyebrow/index.ts +2 -0
- package/src/components/field/field-context.ts +40 -0
- package/src/components/field/field.styles.ts +23 -0
- package/src/components/field/field.tsx +109 -0
- package/src/components/field/index.ts +3 -0
- package/src/components/heading/heading.styles.ts +16 -0
- package/src/components/heading/heading.tsx +26 -0
- package/src/components/heading/index.ts +2 -0
- package/src/components/input/index.ts +2 -0
- package/src/components/input/input.styles.ts +36 -0
- package/src/components/input/input.tsx +33 -0
- package/src/components/kbd/index.ts +1 -0
- package/src/components/kbd/kbd.tsx +21 -0
- package/src/components/label/index.ts +1 -0
- package/src/components/label/label.tsx +21 -0
- package/src/components/section/index.ts +1 -0
- package/src/components/section/section.tsx +39 -0
- package/src/components/segmented-control/index.ts +6 -0
- package/src/components/segmented-control/segmented-control.tsx +79 -0
- package/src/components/select/index.ts +1 -0
- package/src/components/select/select.tsx +133 -0
- package/src/components/separator/index.ts +1 -0
- package/src/components/separator/separator.tsx +29 -0
- package/src/components/skeleton/index.ts +1 -0
- package/src/components/skeleton/skeleton.tsx +20 -0
- package/src/components/spinner/index.ts +2 -0
- package/src/components/spinner/spinner.styles.ts +19 -0
- package/src/components/spinner/spinner.tsx +42 -0
- package/src/components/stack/index.ts +1 -0
- package/src/components/stack/stack.tsx +62 -0
- package/src/components/switch/index.ts +1 -0
- package/src/components/switch/switch.tsx +48 -0
- package/src/components/table/index.ts +10 -0
- package/src/components/table/table.tsx +90 -0
- package/src/components/tabs/index.ts +1 -0
- package/src/components/tabs/tabs.tsx +92 -0
- package/src/components/text/index.ts +2 -0
- package/src/components/text/text.styles.ts +32 -0
- package/src/components/text/text.tsx +17 -0
- package/src/components/textarea/index.ts +1 -0
- package/src/components/textarea/textarea.tsx +27 -0
- package/src/components/tooltip/index.ts +1 -0
- package/src/components/tooltip/tooltip.tsx +42 -0
- package/src/hooks/index.ts +4 -0
- package/src/hooks/use-component-config.ts +42 -0
- package/src/hooks/use-controllable-state.ts +27 -0
- package/src/hooks/use-media-query.ts +14 -0
- package/src/hooks/use-scrolled.ts +14 -0
- package/src/index.ts +52 -0
- package/src/internal/icons.tsx +99 -0
- package/src/patterns/app-shell/app-shell-context.ts +27 -0
- package/src/patterns/app-shell/app-shell.styles.ts +127 -0
- package/src/patterns/app-shell/app-shell.tsx +81 -0
- package/src/patterns/app-shell/index.ts +16 -0
- package/src/patterns/app-shell/navbar.tsx +119 -0
- package/src/patterns/app-shell/page-header.tsx +30 -0
- package/src/patterns/app-shell/sidebar.tsx +163 -0
- package/src/patterns/async/async.tsx +83 -0
- package/src/patterns/async/index.ts +10 -0
- package/src/patterns/async/states.tsx +128 -0
- package/src/patterns/data-table/data-table.tsx +367 -0
- package/src/patterns/data-table/index.ts +2 -0
- package/src/patterns/form/form.tsx +150 -0
- package/src/patterns/form/index.ts +13 -0
- package/src/provider/color-mode.tsx +133 -0
- package/src/provider/glyph-context.ts +42 -0
- package/src/provider/glyph-provider.tsx +41 -0
- package/src/provider/index.ts +18 -0
- package/src/provider/theme-scope.tsx +36 -0
- package/src/provider/theme-style.tsx +19 -0
- package/src/register.ts +69 -0
- package/src/themes.ts +56 -0
- package/src/utils/cn.ts +12 -0
- package/src/utils/compose-refs.ts +11 -0
- package/src/utils/registry.ts +9 -0
- package/src/utils/styles.ts +13 -0
- package/src/utils/tv.ts +7 -0
- package/src/utils/tw-merge-config.ts +14 -0
- 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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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,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,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>
|