@gerege-systems/ui 0.11.2
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 +234 -0
- package/dist-lib/__tests__/helpers/color.d.ts +9 -0
- package/dist-lib/__tests__/helpers/theme.d.ts +18 -0
- package/dist-lib/components/ui/Accordion.d.ts +21 -0
- package/dist-lib/components/ui/Accordion.js +71 -0
- package/dist-lib/components/ui/Accordion.js.map +1 -0
- package/dist-lib/components/ui/Alert.d.ts +51 -0
- package/dist-lib/components/ui/Alert.js +79 -0
- package/dist-lib/components/ui/Alert.js.map +1 -0
- package/dist-lib/components/ui/Avatar.d.ts +52 -0
- package/dist-lib/components/ui/Avatar.js +94 -0
- package/dist-lib/components/ui/Avatar.js.map +1 -0
- package/dist-lib/components/ui/Badge.d.ts +26 -0
- package/dist-lib/components/ui/Badge.js +76 -0
- package/dist-lib/components/ui/Badge.js.map +1 -0
- package/dist-lib/components/ui/Breadcrumbs.d.ts +33 -0
- package/dist-lib/components/ui/Breadcrumbs.js +52 -0
- package/dist-lib/components/ui/Breadcrumbs.js.map +1 -0
- package/dist-lib/components/ui/Button.d.ts +52 -0
- package/dist-lib/components/ui/Button.js +131 -0
- package/dist-lib/components/ui/Button.js.map +1 -0
- package/dist-lib/components/ui/Calendar.d.ts +15 -0
- package/dist-lib/components/ui/Calendar.js +139 -0
- package/dist-lib/components/ui/Calendar.js.map +1 -0
- package/dist-lib/components/ui/Card.d.ts +39 -0
- package/dist-lib/components/ui/Card.js +97 -0
- package/dist-lib/components/ui/Card.js.map +1 -0
- package/dist-lib/components/ui/Carousel.d.ts +27 -0
- package/dist-lib/components/ui/Carousel.js +174 -0
- package/dist-lib/components/ui/Carousel.js.map +1 -0
- package/dist-lib/components/ui/Chart.d.ts +87 -0
- package/dist-lib/components/ui/Chart.js +487 -0
- package/dist-lib/components/ui/Chart.js.map +1 -0
- package/dist-lib/components/ui/Checkbox.d.ts +28 -0
- package/dist-lib/components/ui/Checkbox.js +70 -0
- package/dist-lib/components/ui/Checkbox.js.map +1 -0
- package/dist-lib/components/ui/Combobox.d.ts +67 -0
- package/dist-lib/components/ui/Combobox.js +195 -0
- package/dist-lib/components/ui/Combobox.js.map +1 -0
- package/dist-lib/components/ui/CommandPalette.d.ts +112 -0
- package/dist-lib/components/ui/CommandPalette.js +179 -0
- package/dist-lib/components/ui/CommandPalette.js.map +1 -0
- package/dist-lib/components/ui/ContextMenu.d.ts +50 -0
- package/dist-lib/components/ui/ContextMenu.js +165 -0
- package/dist-lib/components/ui/ContextMenu.js.map +1 -0
- package/dist-lib/components/ui/DataGrid.d.ts +79 -0
- package/dist-lib/components/ui/DataGrid.js +99 -0
- package/dist-lib/components/ui/DataGrid.js.map +1 -0
- package/dist-lib/components/ui/DatePicker.d.ts +45 -0
- package/dist-lib/components/ui/DatePicker.js +193 -0
- package/dist-lib/components/ui/DatePicker.js.map +1 -0
- package/dist-lib/components/ui/DesignSystemProvider.d.ts +147 -0
- package/dist-lib/components/ui/DesignSystemProvider.js +135 -0
- package/dist-lib/components/ui/DesignSystemProvider.js.map +1 -0
- package/dist-lib/components/ui/Dialog.d.ts +77 -0
- package/dist-lib/components/ui/Dialog.js +165 -0
- package/dist-lib/components/ui/Dialog.js.map +1 -0
- package/dist-lib/components/ui/Drawer.d.ts +44 -0
- package/dist-lib/components/ui/Drawer.js +132 -0
- package/dist-lib/components/ui/Drawer.js.map +1 -0
- package/dist-lib/components/ui/DropdownMenu.d.ts +47 -0
- package/dist-lib/components/ui/DropdownMenu.js +169 -0
- package/dist-lib/components/ui/DropdownMenu.js.map +1 -0
- package/dist-lib/components/ui/EmptyState.d.ts +48 -0
- package/dist-lib/components/ui/EmptyState.js +46 -0
- package/dist-lib/components/ui/EmptyState.js.map +1 -0
- package/dist-lib/components/ui/ErrorState.d.ts +51 -0
- package/dist-lib/components/ui/ErrorState.js +69 -0
- package/dist-lib/components/ui/ErrorState.js.map +1 -0
- package/dist-lib/components/ui/FileUpload.d.ts +26 -0
- package/dist-lib/components/ui/FileUpload.js +111 -0
- package/dist-lib/components/ui/FileUpload.js.map +1 -0
- package/dist-lib/components/ui/Form.d.ts +36 -0
- package/dist-lib/components/ui/Form.js +131 -0
- package/dist-lib/components/ui/Form.js.map +1 -0
- package/dist-lib/components/ui/Icon.d.ts +34 -0
- package/dist-lib/components/ui/Icon.js +32 -0
- package/dist-lib/components/ui/Icon.js.map +1 -0
- package/dist-lib/components/ui/IconButton.d.ts +34 -0
- package/dist-lib/components/ui/IconButton.js +64 -0
- package/dist-lib/components/ui/IconButton.js.map +1 -0
- package/dist-lib/components/ui/Input.d.ts +54 -0
- package/dist-lib/components/ui/Input.js +158 -0
- package/dist-lib/components/ui/Input.js.map +1 -0
- package/dist-lib/components/ui/Kbd.d.ts +16 -0
- package/dist-lib/components/ui/Kbd.js +24 -0
- package/dist-lib/components/ui/Kbd.js.map +1 -0
- package/dist-lib/components/ui/MultiSelect.d.ts +64 -0
- package/dist-lib/components/ui/MultiSelect.js +210 -0
- package/dist-lib/components/ui/MultiSelect.js.map +1 -0
- package/dist-lib/components/ui/Pagination.d.ts +55 -0
- package/dist-lib/components/ui/Pagination.js +153 -0
- package/dist-lib/components/ui/Pagination.js.map +1 -0
- package/dist-lib/components/ui/Popover.d.ts +16 -0
- package/dist-lib/components/ui/Popover.js +35 -0
- package/dist-lib/components/ui/Popover.js.map +1 -0
- package/dist-lib/components/ui/Progress.d.ts +40 -0
- package/dist-lib/components/ui/Progress.js +100 -0
- package/dist-lib/components/ui/Progress.js.map +1 -0
- package/dist-lib/components/ui/RadioGroup.d.ts +27 -0
- package/dist-lib/components/ui/RadioGroup.js +81 -0
- package/dist-lib/components/ui/RadioGroup.js.map +1 -0
- package/dist-lib/components/ui/RelativeTime.d.ts +19 -0
- package/dist-lib/components/ui/RelativeTime.js +31 -0
- package/dist-lib/components/ui/RelativeTime.js.map +1 -0
- package/dist-lib/components/ui/ScrollArea.d.ts +21 -0
- package/dist-lib/components/ui/ScrollArea.js +60 -0
- package/dist-lib/components/ui/ScrollArea.js.map +1 -0
- package/dist-lib/components/ui/Select.d.ts +61 -0
- package/dist-lib/components/ui/Select.js +139 -0
- package/dist-lib/components/ui/Select.js.map +1 -0
- package/dist-lib/components/ui/Separator.d.ts +13 -0
- package/dist-lib/components/ui/Separator.js +26 -0
- package/dist-lib/components/ui/Separator.js.map +1 -0
- package/dist-lib/components/ui/Sheet.d.ts +55 -0
- package/dist-lib/components/ui/Sheet.js +136 -0
- package/dist-lib/components/ui/Sheet.js.map +1 -0
- package/dist-lib/components/ui/Sidebar.d.ts +91 -0
- package/dist-lib/components/ui/Sidebar.js +212 -0
- package/dist-lib/components/ui/Sidebar.js.map +1 -0
- package/dist-lib/components/ui/Skeleton.d.ts +37 -0
- package/dist-lib/components/ui/Skeleton.js +29 -0
- package/dist-lib/components/ui/Skeleton.js.map +1 -0
- package/dist-lib/components/ui/Slider.d.ts +33 -0
- package/dist-lib/components/ui/Slider.js +64 -0
- package/dist-lib/components/ui/Slider.js.map +1 -0
- package/dist-lib/components/ui/Snackbar.d.ts +21 -0
- package/dist-lib/components/ui/Snackbar.js +57 -0
- package/dist-lib/components/ui/Snackbar.js.map +1 -0
- package/dist-lib/components/ui/Spinner.d.ts +23 -0
- package/dist-lib/components/ui/Spinner.js +54 -0
- package/dist-lib/components/ui/Spinner.js.map +1 -0
- package/dist-lib/components/ui/Stepper.d.ts +40 -0
- package/dist-lib/components/ui/Stepper.js +111 -0
- package/dist-lib/components/ui/Stepper.js.map +1 -0
- package/dist-lib/components/ui/Switch.d.ts +33 -0
- package/dist-lib/components/ui/Switch.js +80 -0
- package/dist-lib/components/ui/Switch.js.map +1 -0
- package/dist-lib/components/ui/Table.d.ts +71 -0
- package/dist-lib/components/ui/Table.js +179 -0
- package/dist-lib/components/ui/Table.js.map +1 -0
- package/dist-lib/components/ui/Tabs.d.ts +32 -0
- package/dist-lib/components/ui/Tabs.js +91 -0
- package/dist-lib/components/ui/Tabs.js.map +1 -0
- package/dist-lib/components/ui/TagInput.d.ts +39 -0
- package/dist-lib/components/ui/TagInput.js +111 -0
- package/dist-lib/components/ui/TagInput.js.map +1 -0
- package/dist-lib/components/ui/Textarea.d.ts +34 -0
- package/dist-lib/components/ui/Textarea.js +88 -0
- package/dist-lib/components/ui/Textarea.js.map +1 -0
- package/dist-lib/components/ui/Timeline.d.ts +33 -0
- package/dist-lib/components/ui/Timeline.js +56 -0
- package/dist-lib/components/ui/Timeline.js.map +1 -0
- package/dist-lib/components/ui/Toast.d.ts +53 -0
- package/dist-lib/components/ui/Toast.js +158 -0
- package/dist-lib/components/ui/Toast.js.map +1 -0
- package/dist-lib/components/ui/Tooltip.d.ts +38 -0
- package/dist-lib/components/ui/Tooltip.js +60 -0
- package/dist-lib/components/ui/Tooltip.js.map +1 -0
- package/dist-lib/components/ui/TopNav.d.ts +37 -0
- package/dist-lib/components/ui/TopNav.js +69 -0
- package/dist-lib/components/ui/TopNav.js.map +1 -0
- package/dist-lib/components/ui/Tree.d.ts +51 -0
- package/dist-lib/components/ui/Tree.js +170 -0
- package/dist-lib/components/ui/Tree.js.map +1 -0
- package/dist-lib/hooks/use-debounce.d.ts +19 -0
- package/dist-lib/hooks/use-debounce.js +36 -0
- package/dist-lib/hooks/use-debounce.js.map +1 -0
- package/dist-lib/hooks/use-delayed-loading.d.ts +21 -0
- package/dist-lib/hooks/use-delayed-loading.js +20 -0
- package/dist-lib/hooks/use-delayed-loading.js.map +1 -0
- package/dist-lib/hooks/use-field-ids.d.ts +30 -0
- package/dist-lib/hooks/use-field-ids.js +18 -0
- package/dist-lib/hooks/use-field-ids.js.map +1 -0
- package/dist-lib/hooks/use-media-query.d.ts +11 -0
- package/dist-lib/hooks/use-media-query.js +22 -0
- package/dist-lib/hooks/use-media-query.js.map +1 -0
- package/dist-lib/hooks/use-modifier-key.d.ts +13 -0
- package/dist-lib/hooks/use-modifier-key.js +19 -0
- package/dist-lib/hooks/use-modifier-key.js.map +1 -0
- package/dist-lib/hooks/use-strings.d.ts +5 -0
- package/dist-lib/hooks/use-strings.js +12 -0
- package/dist-lib/hooks/use-strings.js.map +1 -0
- package/dist-lib/hooks/use-toast.d.ts +48 -0
- package/dist-lib/hooks/use-toast.js +67 -0
- package/dist-lib/hooks/use-toast.js.map +1 -0
- package/dist-lib/icon.d.ts +8 -0
- package/dist-lib/icon.js +7 -0
- package/dist-lib/icon.js.map +1 -0
- package/dist-lib/icons/index.d.ts +80 -0
- package/dist-lib/icons/index.js +163 -0
- package/dist-lib/icons/index.js.map +1 -0
- package/dist-lib/illustrations/index.d.ts +10 -0
- package/dist-lib/illustrations/index.js +114 -0
- package/dist-lib/illustrations/index.js.map +1 -0
- package/dist-lib/index.d.ts +68 -0
- package/dist-lib/index.js +291 -0
- package/dist-lib/index.js.map +1 -0
- package/dist-lib/lib/cva.d.ts +6 -0
- package/dist-lib/lib/format.d.ts +65 -0
- package/dist-lib/lib/format.js +95 -0
- package/dist-lib/lib/format.js.map +1 -0
- package/dist-lib/lib/return-focus.d.ts +11 -0
- package/dist-lib/lib/return-focus.js +12 -0
- package/dist-lib/lib/return-focus.js.map +1 -0
- package/dist-lib/lib/strings.d.ts +190 -0
- package/dist-lib/lib/strings.js +141 -0
- package/dist-lib/lib/strings.js.map +1 -0
- package/dist-lib/lib/strings.mn.d.ts +3 -0
- package/dist-lib/lib/strings.mn.js +115 -0
- package/dist-lib/lib/strings.mn.js.map +1 -0
- package/dist-lib/lib/utils.d.ts +11 -0
- package/dist-lib/lib/utils.js +10 -0
- package/dist-lib/lib/utils.js.map +1 -0
- package/dist-lib/styles.css +1 -0
- package/dist-lib/theme.css +840 -0
- package/package.json +158 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsxs as b, jsx as i } from "react/jsx-runtime";
|
|
3
|
+
import { forwardRef as a, useId as D, useContext as f, createContext as u, Children as N, isValidElement as h } from "react";
|
|
4
|
+
import * as C from "@radix-ui/react-label";
|
|
5
|
+
import { FormProvider as j, Controller as S, useFormContext as p } from "react-hook-form";
|
|
6
|
+
import { Slot as E } from "@radix-ui/react-slot";
|
|
7
|
+
import { cn as l } from "../../lib/utils.js";
|
|
8
|
+
function F(o, e = []) {
|
|
9
|
+
if (!o) return e;
|
|
10
|
+
for (const r of Object.values(o)) {
|
|
11
|
+
if (!r || typeof r != "object") continue;
|
|
12
|
+
const t = r.message;
|
|
13
|
+
typeof t == "string" && t ? e.push(t) : "type" in r || F(r, e);
|
|
14
|
+
}
|
|
15
|
+
return e;
|
|
16
|
+
}
|
|
17
|
+
function L() {
|
|
18
|
+
const { formState: o } = p(), e = F(o.errors);
|
|
19
|
+
return /* @__PURE__ */ i("div", { role: "status", "aria-live": "polite", "aria-atomic": "true", className: "sr-only", children: e.join(". ") });
|
|
20
|
+
}
|
|
21
|
+
function M({ children: o, ...e }) {
|
|
22
|
+
return /* @__PURE__ */ b(j, { ...e, children: [
|
|
23
|
+
o,
|
|
24
|
+
/* @__PURE__ */ i(L, {})
|
|
25
|
+
] });
|
|
26
|
+
}
|
|
27
|
+
M.displayName = "Form";
|
|
28
|
+
const y = u(null);
|
|
29
|
+
function q(o) {
|
|
30
|
+
return /* @__PURE__ */ i(y.Provider, { value: { name: o.name }, children: /* @__PURE__ */ i(S, { ...o }) });
|
|
31
|
+
}
|
|
32
|
+
const x = u(null);
|
|
33
|
+
function d() {
|
|
34
|
+
const o = f(y), e = f(x), { getFieldState: r, formState: t } = p();
|
|
35
|
+
if (!o)
|
|
36
|
+
throw new Error("useFormField must be used inside <FormField>");
|
|
37
|
+
const s = r(o.name, t), n = (e == null ? void 0 : e.id) ?? "";
|
|
38
|
+
return {
|
|
39
|
+
id: n,
|
|
40
|
+
name: o.name,
|
|
41
|
+
formItemId: `${n}-item`,
|
|
42
|
+
formDescriptionId: `${n}-desc`,
|
|
43
|
+
formMessageId: `${n}-error`,
|
|
44
|
+
hasDescription: (e == null ? void 0 : e.hasDescription) ?? !1,
|
|
45
|
+
...s
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function g(o) {
|
|
49
|
+
return N.toArray(o).some((e) => {
|
|
50
|
+
if (!h(e)) return !1;
|
|
51
|
+
if (e.type === I) return !0;
|
|
52
|
+
const r = e.props.children;
|
|
53
|
+
return (typeof e.type == "string" || typeof e.type == "symbol") && r ? g(r) : !1;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
const P = a(
|
|
57
|
+
function({ className: e, children: r, ...t }, s) {
|
|
58
|
+
const n = D(), m = g(r);
|
|
59
|
+
return /* @__PURE__ */ i(x.Provider, { value: { id: n, hasDescription: m }, children: /* @__PURE__ */ i("div", { ref: s, className: l("flex flex-col gap-1.5", e), ...t, children: r }) });
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
P.displayName = "FormItem";
|
|
63
|
+
const w = a(function({ className: e, ...r }, t) {
|
|
64
|
+
const { formItemId: s, error: n } = d();
|
|
65
|
+
return /* @__PURE__ */ i(
|
|
66
|
+
C.Root,
|
|
67
|
+
{
|
|
68
|
+
ref: t,
|
|
69
|
+
htmlFor: s,
|
|
70
|
+
className: l("text-foreground text-sm font-medium", n && "text-danger-text", e),
|
|
71
|
+
...r
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
w.displayName = "FormLabel";
|
|
76
|
+
const R = a(
|
|
77
|
+
function(e, r) {
|
|
78
|
+
const { error: t, formItemId: s, formDescriptionId: n, formMessageId: m, hasDescription: c } = d(), v = [c ? n : null, t ? m : null].filter(Boolean).join(" ") || void 0;
|
|
79
|
+
return /* @__PURE__ */ i(
|
|
80
|
+
E,
|
|
81
|
+
{
|
|
82
|
+
ref: r,
|
|
83
|
+
id: s,
|
|
84
|
+
"aria-describedby": v,
|
|
85
|
+
"aria-invalid": !!t,
|
|
86
|
+
...e
|
|
87
|
+
}
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
);
|
|
91
|
+
R.displayName = "FormControl";
|
|
92
|
+
const I = a(function({ className: e, ...r }, t) {
|
|
93
|
+
const { formDescriptionId: s } = d();
|
|
94
|
+
return /* @__PURE__ */ i(
|
|
95
|
+
"p",
|
|
96
|
+
{
|
|
97
|
+
ref: t,
|
|
98
|
+
id: s,
|
|
99
|
+
className: l("text-foreground-subtle text-xs", e),
|
|
100
|
+
...r
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
I.displayName = "FormDescription";
|
|
105
|
+
const $ = a(
|
|
106
|
+
function({ className: e, children: r, ...t }, s) {
|
|
107
|
+
const { error: n, formMessageId: m } = d(), c = n ? String((n == null ? void 0 : n.message) ?? "") : r;
|
|
108
|
+
return c ? /* @__PURE__ */ i(
|
|
109
|
+
"p",
|
|
110
|
+
{
|
|
111
|
+
ref: s,
|
|
112
|
+
id: m,
|
|
113
|
+
className: l("text-danger-text text-xs", e),
|
|
114
|
+
...t,
|
|
115
|
+
children: c
|
|
116
|
+
}
|
|
117
|
+
) : null;
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
$.displayName = "FormError";
|
|
121
|
+
export {
|
|
122
|
+
M as Form,
|
|
123
|
+
R as FormControl,
|
|
124
|
+
I as FormDescription,
|
|
125
|
+
$ as FormError,
|
|
126
|
+
q as FormField,
|
|
127
|
+
P as FormItem,
|
|
128
|
+
w as FormLabel,
|
|
129
|
+
d as useFormField
|
|
130
|
+
};
|
|
131
|
+
//# sourceMappingURL=Form.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Form.js","sources":["../../../src/components/ui/Form.tsx"],"sourcesContent":["'use client';\n\nimport {\n Children,\n createContext,\n forwardRef,\n isValidElement,\n useContext,\n useId,\n type HTMLAttributes,\n type ReactNode,\n} from 'react';\nimport * as LabelPrimitive from '@radix-ui/react-label';\nimport {\n Controller,\n FormProvider,\n useFormContext,\n type ControllerProps,\n type FieldErrors,\n type FieldPath,\n type FieldValues,\n type FormProviderProps,\n} from 'react-hook-form';\nimport { Slot } from '@radix-ui/react-slot';\nimport { cn } from '@/lib/utils';\n\n/* -----------------------------------------------------------------------------\n * react-hook-form bindings + accessible label / description / error wiring.\n *\n * Usage:\n * const form = useForm<Values>(…);\n * <Form {...form}>\n * <FormField\n * control={form.control}\n * name=\"email\"\n * render={({ field }) => (\n * <FormItem>\n * <FormLabel>Email</FormLabel>\n * <FormControl><Input type=\"email\" {...field} /></FormControl>\n * <FormDescription>We never share this.</FormDescription>\n * <FormError />\n * </FormItem>\n * )}\n * />\n * </Form>\n *\n * Ids: `<item>-item` (control), `<item>-desc`, `<item>-error` — the same\n * `-desc` / `-error` suffixes the standalone fields use (useFieldIds).\n * Invalid styling is driven purely by `aria-invalid` on the control; no\n * `aria-invalid` is set on the control; style from `aria-invalid:` (no `tone` injection).\n * --------------------------------------------------------------------------- */\n\nfunction collectMessages(errors: FieldErrors | undefined, out: string[] = []): string[] {\n if (!errors) return out;\n for (const value of Object.values(errors)) {\n if (!value || typeof value !== 'object') continue;\n const message = (value as { message?: unknown }).message;\n if (typeof message === 'string' && message) out.push(message);\n else if (!('type' in value)) collectMessages(value as FieldErrors, out);\n }\n return out;\n}\n\n/**\n * Single polite live region per form. Field-level `FormError` elements are\n * plain text wired via `aria-describedby`; this region announces the current\n * error set once instead of one `role=\"alert\"` per field firing in parallel.\n */\nfunction FormLiveRegion() {\n const { formState } = useFormContext();\n const messages = collectMessages(formState.errors);\n return (\n <div role=\"status\" aria-live=\"polite\" aria-atomic=\"true\" className=\"sr-only\">\n {messages.join('. ')}\n </div>\n );\n}\n\n/**\n * Form context provider. Spread the `useForm()` return value into it, exactly\n * like react-hook-form's `FormProvider` — plus one shared live region for\n * validation announcements.\n */\nexport function Form<\n TFieldValues extends FieldValues = FieldValues,\n TContext = unknown,\n TTransformedValues = TFieldValues,\n>({ children, ...form }: FormProviderProps<TFieldValues, TContext, TTransformedValues>) {\n return (\n <FormProvider {...form}>\n {children}\n <FormLiveRegion />\n </FormProvider>\n );\n}\nForm.displayName = 'Form';\n\ninterface FormFieldContextValue<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n> {\n name: TName;\n}\n\nconst FormFieldContext = createContext<FormFieldContextValue | null>(null);\n\nexport function FormField<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n>(props: ControllerProps<TFieldValues, TName>) {\n return (\n <FormFieldContext.Provider value={{ name: props.name as string }}>\n <Controller {...props} />\n </FormFieldContext.Provider>\n );\n}\n\ninterface FormItemContextValue {\n id: string;\n /** Whether a <FormDescription> is rendered — drives aria-describedby. */\n hasDescription: boolean;\n}\nconst FormItemContext = createContext<FormItemContextValue | null>(null);\n\nexport function useFormField() {\n const fieldContext = useContext(FormFieldContext);\n const itemContext = useContext(FormItemContext);\n const { getFieldState, formState } = useFormContext();\n if (!fieldContext) {\n throw new Error('useFormField must be used inside <FormField>');\n }\n const fieldState = getFieldState(fieldContext.name, formState);\n const id = itemContext?.id ?? '';\n return {\n id,\n name: fieldContext.name,\n formItemId: `${id}-item`,\n formDescriptionId: `${id}-desc`,\n formMessageId: `${id}-error`,\n hasDescription: itemContext?.hasDescription ?? false,\n ...fieldState,\n };\n}\n\n/** Synchronous scan for a <FormDescription> among the item's children (through fragments / host elements). */\nfunction containsDescription(children: ReactNode): boolean {\n return Children.toArray(children).some((child) => {\n if (!isValidElement(child)) return false;\n if (child.type === FormDescription) return true;\n const nested = (child.props as { children?: ReactNode }).children;\n // Only descend into host elements / fragments — custom components own their subtree.\n return (typeof child.type === 'string' || typeof child.type === 'symbol') && nested\n ? containsDescription(nested)\n : false;\n });\n}\n\nexport const FormItem = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(\n function FormItem({ className, children, ...props }, ref) {\n const id = useId();\n // Derived during render (no effect → no second pass / hydration flip).\n const hasDescription = containsDescription(children);\n return (\n <FormItemContext.Provider value={{ id, hasDescription }}>\n <div ref={ref} className={cn('flex flex-col gap-1.5', className)} {...props}>\n {children}\n </div>\n </FormItemContext.Provider>\n );\n },\n);\nFormItem.displayName = 'FormItem';\n\nexport const FormLabel = forwardRef<\n React.ElementRef<typeof LabelPrimitive.Root>,\n React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>\n>(function FormLabel({ className, ...props }, ref) {\n const { formItemId, error } = useFormField();\n return (\n <LabelPrimitive.Root\n ref={ref}\n htmlFor={formItemId}\n className={cn('text-foreground text-sm font-medium', error && 'text-danger-text', className)}\n {...props}\n />\n );\n});\nFormLabel.displayName = 'FormLabel';\n\nexport const FormControl = forwardRef<HTMLElement, React.ComponentPropsWithoutRef<typeof Slot>>(\n function FormControl(props, ref) {\n const { error, formItemId, formDescriptionId, formMessageId, hasDescription } = useFormField();\n // Only reference ids that actually render: the description when mounted,\n // the message only while there is an error (FormError returns null otherwise).\n const describedBy =\n [hasDescription ? formDescriptionId : null, error ? formMessageId : null]\n .filter(Boolean)\n .join(' ') || undefined;\n return (\n <Slot\n ref={ref}\n id={formItemId}\n aria-describedby={describedBy}\n aria-invalid={!!error}\n {...props}\n />\n );\n },\n);\nFormControl.displayName = 'FormControl';\n\nexport const FormDescription = forwardRef<\n HTMLParagraphElement,\n HTMLAttributes<HTMLParagraphElement>\n>(function FormDescription({ className, ...props }, ref) {\n const { formDescriptionId } = useFormField();\n return (\n <p\n ref={ref}\n id={formDescriptionId}\n className={cn('text-foreground-subtle text-xs', className)}\n {...props}\n />\n );\n});\nFormDescription.displayName = 'FormDescription';\n\n/**\n * Field-level validation message. Not a live region on its own — it is\n * referenced from the control via `aria-describedby`, and the form's single\n * live region announces changes.\n */\nexport const FormError = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLParagraphElement>>(\n function FormError({ className, children, ...props }, ref) {\n const { error, formMessageId } = useFormField();\n const body = error ? String(error?.message ?? '') : children;\n if (!body) return null;\n return (\n <p\n ref={ref}\n id={formMessageId}\n className={cn('text-danger-text text-xs', className)}\n {...props}\n >\n {body}\n </p>\n );\n },\n);\nFormError.displayName = 'FormError';\n"],"names":["collectMessages","errors","out","value","message","FormLiveRegion","formState","useFormContext","messages","jsx","Form","children","form","jsxs","FormProvider","FormFieldContext","createContext","FormField","props","Controller","FormItemContext","useFormField","fieldContext","useContext","itemContext","getFieldState","fieldState","id","containsDescription","Children","child","isValidElement","FormDescription","nested","FormItem","forwardRef","className","ref","useId","hasDescription","cn","FormLabel","formItemId","error","LabelPrimitive","FormControl","formDescriptionId","formMessageId","describedBy","Slot","FormError","body"],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { default as dynamicIconImports } from 'lucide-react/dynamicIconImports';
|
|
3
|
+
import { LucideProps } from 'lucide-react';
|
|
4
|
+
/** Every lucide icon name, kebab-case — e.g. `calendar`, `chevrons-up-down`. */
|
|
5
|
+
export type IconName = keyof typeof dynamicIconImports;
|
|
6
|
+
/** All available icon names (useful for pickers and docs). */
|
|
7
|
+
export declare const iconNames: IconName[];
|
|
8
|
+
export interface IconProps extends Omit<LucideProps, 'ref'> {
|
|
9
|
+
/** Lucide icon name in kebab-case, e.g. `name="calendar"`. Autocompletes. */
|
|
10
|
+
name: IconName;
|
|
11
|
+
/**
|
|
12
|
+
* Shown while the icon chunk loads (first use only — icons are cached).
|
|
13
|
+
* Defaults to an invisible square so layout doesn't shift.
|
|
14
|
+
*/
|
|
15
|
+
fallback?: ReactNode;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Name-addressed lucide icon. Use when the icon is dynamic (data-driven
|
|
19
|
+
* nav items, user-configurable dashboards) or when importing components for
|
|
20
|
+
* every icon is friction.
|
|
21
|
+
*
|
|
22
|
+
* Each icon is code-split and loaded on first use, so referencing the full
|
|
23
|
+
* lucide set costs nothing up front — `<Icon name="rocket" />` ships only
|
|
24
|
+
* the rocket.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* <Icon name="calendar" className="size-4" />
|
|
28
|
+
* <Icon name="chevrons-up-down" size={16} strokeWidth={1.5} />
|
|
29
|
+
*
|
|
30
|
+
* @do Use `size-4` (16px) inline with text, `size-5` (20px) inside buttons.
|
|
31
|
+
* @dont Reach for this in hot lists with hundreds of distinct icons on first
|
|
32
|
+
* paint — statically imported icons from `Icons` skip the lazy hop.
|
|
33
|
+
*/
|
|
34
|
+
export declare function Icon({ name, fallback, ...props }: IconProps): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as o } from "react/jsx-runtime";
|
|
3
|
+
import { useMemo as a, Suspense as u, lazy as d } from "react";
|
|
4
|
+
import r from "lucide-react/dynamicIconImports";
|
|
5
|
+
const p = Object.keys(r), i = /* @__PURE__ */ new Map(), s = /* @__PURE__ */ new Set();
|
|
6
|
+
function f(e) {
|
|
7
|
+
const t = i.get(e);
|
|
8
|
+
if (t) return t;
|
|
9
|
+
const n = r[e];
|
|
10
|
+
if (typeof n != "function")
|
|
11
|
+
return process.env.NODE_ENV !== "production" && !s.has(e) && (s.add(e), console.warn(
|
|
12
|
+
`[@gerege-systems/ui] <Icon name="${e}"> is not a lucide icon; rendering nothing.`
|
|
13
|
+
)), null;
|
|
14
|
+
const c = d(n);
|
|
15
|
+
return i.set(e, c), c;
|
|
16
|
+
}
|
|
17
|
+
function y({ name: e, fallback: t, ...n }) {
|
|
18
|
+
const c = a(() => f(e), [e]), l = t ?? /* @__PURE__ */ o(
|
|
19
|
+
"span",
|
|
20
|
+
{
|
|
21
|
+
"aria-hidden": !0,
|
|
22
|
+
className: n.className,
|
|
23
|
+
style: { display: "inline-block", width: n.size ?? "1em", height: n.size ?? "1em" }
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
return c ? /* @__PURE__ */ o(u, { fallback: l, children: /* @__PURE__ */ o(c, { ...n }) }) : null;
|
|
27
|
+
}
|
|
28
|
+
export {
|
|
29
|
+
y as Icon,
|
|
30
|
+
p as iconNames
|
|
31
|
+
};
|
|
32
|
+
//# sourceMappingURL=Icon.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Icon.js","sources":["../../../src/components/ui/Icon.tsx"],"sourcesContent":["'use client';\n\nimport { lazy, Suspense, useMemo, type LazyExoticComponent, type ReactNode } from 'react';\nimport dynamicIconImports from 'lucide-react/dynamicIconImports';\nimport type { LucideIcon, LucideProps } from 'lucide-react';\n\n/** Every lucide icon name, kebab-case — e.g. `calendar`, `chevrons-up-down`. */\nexport type IconName = keyof typeof dynamicIconImports;\n\n/** All available icon names (useful for pickers and docs). */\nexport const iconNames = Object.keys(dynamicIconImports) as IconName[];\n\n// lazy() must return a stable component per name or React remounts (and\n// re-suspends) on every render.\nconst cache = new Map<IconName, LazyExoticComponent<LucideIcon>>();\nconst warned = new Set<string>();\n\nfunction getIcon(name: IconName): LazyExoticComponent<LucideIcon> | null {\n const cached = cache.get(name);\n if (cached) return cached;\n const loader = dynamicIconImports[name];\n if (typeof loader !== 'function') {\n // Unknown name (data-driven input) → render nothing, warn once in dev.\n if (process.env.NODE_ENV !== 'production' && !warned.has(name)) {\n warned.add(name);\n console.warn(\n `[@gerege-systems/ui] <Icon name=\"${name}\"> is not a lucide icon; rendering nothing.`,\n );\n }\n return null;\n }\n const created = lazy(loader);\n cache.set(name, created);\n return created;\n}\n\nexport interface IconProps extends Omit<LucideProps, 'ref'> {\n /** Lucide icon name in kebab-case, e.g. `name=\"calendar\"`. Autocompletes. */\n name: IconName;\n /**\n * Shown while the icon chunk loads (first use only — icons are cached).\n * Defaults to an invisible square so layout doesn't shift.\n */\n fallback?: ReactNode;\n}\n\n/**\n * Name-addressed lucide icon. Use when the icon is dynamic (data-driven\n * nav items, user-configurable dashboards) or when importing components for\n * every icon is friction.\n *\n * Each icon is code-split and loaded on first use, so referencing the full\n * lucide set costs nothing up front — `<Icon name=\"rocket\" />` ships only\n * the rocket.\n *\n * @example\n * <Icon name=\"calendar\" className=\"size-4\" />\n * <Icon name=\"chevrons-up-down\" size={16} strokeWidth={1.5} />\n *\n * @do Use `size-4` (16px) inline with text, `size-5` (20px) inside buttons.\n * @dont Reach for this in hot lists with hundreds of distinct icons on first\n * paint — statically imported icons from `Icons` skip the lazy hop.\n */\nexport function Icon({ name, fallback, ...props }: IconProps) {\n const LucideIcon = useMemo(() => getIcon(name), [name]);\n\n const placeholder = fallback ?? (\n <span\n aria-hidden\n className={props.className}\n style={{ display: 'inline-block', width: props.size ?? '1em', height: props.size ?? '1em' }}\n />\n );\n\n if (!LucideIcon) return null;\n\n return (\n <Suspense fallback={placeholder}>\n <LucideIcon {...props} />\n </Suspense>\n );\n}\n"],"names":["iconNames","dynamicIconImports","cache","warned","getIcon","name","cached","loader","created","lazy","Icon","fallback","props","LucideIcon","useMemo","placeholder","jsx","Suspense"],"mappings":";;;;AAUO,MAAMA,IAAY,OAAO,KAAKC,CAAkB,GAIjDC,wBAAY,IAAA,GACZC,wBAAa,IAAA;AAEnB,SAASC,EAAQC,GAAwD;AACvE,QAAMC,IAASJ,EAAM,IAAIG,CAAI;AAC7B,MAAIC,EAAQ,QAAOA;AACnB,QAAMC,IAASN,EAAmBI,CAAI;AACtC,MAAI,OAAOE,KAAW;AAEpB,WAAI,QAAQ,IAAI,aAAa,gBAAgB,CAACJ,EAAO,IAAIE,CAAI,MAC3DF,EAAO,IAAIE,CAAI,GACf,QAAQ;AAAA,MACN,oCAAoCA,CAAI;AAAA,IAAA,IAGrC;AAET,QAAMG,IAAUC,EAAKF,CAAM;AAC3B,SAAAL,EAAM,IAAIG,GAAMG,CAAO,GAChBA;AACT;AA6BO,SAASE,EAAK,EAAE,MAAAL,GAAM,UAAAM,GAAU,GAAGC,KAAoB;AAC5D,QAAMC,IAAaC,EAAQ,MAAMV,EAAQC,CAAI,GAAG,CAACA,CAAI,CAAC,GAEhDU,IAAcJ,KAClB,gBAAAK;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,eAAW;AAAA,MACX,WAAWJ,EAAM;AAAA,MACjB,OAAO,EAAE,SAAS,gBAAgB,OAAOA,EAAM,QAAQ,OAAO,QAAQA,EAAM,QAAQ,MAAA;AAAA,IAAM;AAAA,EAAA;AAI9F,SAAKC,IAGH,gBAAAG,EAACC,KAAS,UAAUF,GAClB,4BAACF,GAAA,EAAY,GAAGD,GAAO,EAAA,CACzB,IALsB;AAO1B;"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { ButtonHTMLAttributes, ReactNode } from 'react';
|
|
2
|
+
import { VariantProps } from '../../lib/cva.js';
|
|
3
|
+
declare const iconButton: (props?: ({
|
|
4
|
+
variant?: "outline" | "primary" | "secondary" | "ghost" | "destructive" | null | undefined;
|
|
5
|
+
size?: "md" | "sm" | "lg" | "xl" | null | undefined;
|
|
6
|
+
} & import('class-variance-authority/types').ClassProp) | undefined) => string;
|
|
7
|
+
export interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof iconButton> {
|
|
8
|
+
/**
|
|
9
|
+
* Accessible label for screen readers. **Required** — an icon-only button
|
|
10
|
+
* with no label is invisible to assistive tech.
|
|
11
|
+
*/
|
|
12
|
+
'aria-label': string;
|
|
13
|
+
/** The Lucide icon to render. */
|
|
14
|
+
icon: ReactNode;
|
|
15
|
+
/** Show a spinner instead of the icon; blocks interaction but keeps focus (`aria-disabled`). */
|
|
16
|
+
loading?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Square, icon-only button. Default variant is `ghost` because most
|
|
20
|
+
* IconButtons sit in dense toolbars or tables. Always pass `aria-label`.
|
|
21
|
+
*
|
|
22
|
+
* @example Toolbar action
|
|
23
|
+
* <IconButton aria-label="Edit row" icon={<Edit2 />} onClick={onEdit} />
|
|
24
|
+
*
|
|
25
|
+
* @example Dismissible chip
|
|
26
|
+
* <IconButton aria-label="Remove tag" size="sm" icon={<X />} />
|
|
27
|
+
*
|
|
28
|
+
* @do Use `ghost` for in-row actions; `outline` when next to a label-less
|
|
29
|
+
* primary button you want users to consider as an alternative.
|
|
30
|
+
* @dont Use IconButton when the action's meaning isn't universally
|
|
31
|
+
* recognised. If users need to learn the icon, ship a Button with text.
|
|
32
|
+
*/
|
|
33
|
+
export declare const IconButton: import('react').ForwardRefExoticComponent<IconButtonProps & import('react').RefAttributes<HTMLButtonElement>>;
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as t } from "react/jsx-runtime";
|
|
3
|
+
import { forwardRef as g } from "react";
|
|
4
|
+
import { Loader2 as b } from "../../icons/index.js";
|
|
5
|
+
import { cn as f } from "../../lib/utils.js";
|
|
6
|
+
import { cva as m } from "class-variance-authority";
|
|
7
|
+
const l = m(
|
|
8
|
+
[
|
|
9
|
+
"inline-flex items-center justify-center shrink-0",
|
|
10
|
+
"rounded-md",
|
|
11
|
+
"transition-colors duration-[var(--duration-fast)] ease-[var(--ease-out)]",
|
|
12
|
+
"outline-none",
|
|
13
|
+
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
14
|
+
"focus-visible:ring-offset-background",
|
|
15
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
16
|
+
"aria-busy:pointer-events-none",
|
|
17
|
+
"[&_svg]:pointer-events-none"
|
|
18
|
+
],
|
|
19
|
+
{
|
|
20
|
+
variants: {
|
|
21
|
+
variant: {
|
|
22
|
+
primary: "bg-accent text-on-accent hover:bg-accent-hover active:bg-accent-active",
|
|
23
|
+
secondary: "bg-background-muted text-foreground border border-border-input hover:bg-surface-hover active:bg-surface-active",
|
|
24
|
+
outline: "border border-border-input bg-transparent text-foreground hover:bg-background-muted",
|
|
25
|
+
ghost: "bg-transparent text-foreground-muted hover:bg-background-muted hover:text-foreground",
|
|
26
|
+
destructive: "bg-danger text-on-danger hover:bg-danger-hover active:bg-danger-active"
|
|
27
|
+
},
|
|
28
|
+
size: {
|
|
29
|
+
// Matches Button heights: sm 32 / md 36 / lg 40 / xl 44 (touch / marketing).
|
|
30
|
+
sm: "h-8 w-8 [&_svg]:size-4",
|
|
31
|
+
md: "h-9 w-9 [&_svg]:size-4",
|
|
32
|
+
lg: "h-10 w-10 [&_svg]:size-5",
|
|
33
|
+
xl: "h-11 w-11 [&_svg]:size-5"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
defaultVariants: {
|
|
37
|
+
variant: "ghost",
|
|
38
|
+
size: "md"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
), p = g(function({ className: o, variant: n, size: i, icon: a, loading: e, disabled: s, type: c = "button", onClick: d, ...u }, v) {
|
|
42
|
+
return /* @__PURE__ */ t(
|
|
43
|
+
"button",
|
|
44
|
+
{
|
|
45
|
+
ref: v,
|
|
46
|
+
type: c,
|
|
47
|
+
"aria-busy": e || void 0,
|
|
48
|
+
"aria-disabled": e || void 0,
|
|
49
|
+
disabled: s,
|
|
50
|
+
className: f(l({ variant: n, size: i }), o),
|
|
51
|
+
onClick: e ? h : d,
|
|
52
|
+
...u,
|
|
53
|
+
children: e ? /* @__PURE__ */ t(b, { className: "animate-spin", "aria-hidden": "true" }) : a
|
|
54
|
+
}
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
p.displayName = "IconButton";
|
|
58
|
+
function h(r) {
|
|
59
|
+
r.preventDefault(), r.stopPropagation();
|
|
60
|
+
}
|
|
61
|
+
export {
|
|
62
|
+
p as IconButton
|
|
63
|
+
};
|
|
64
|
+
//# sourceMappingURL=IconButton.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"IconButton.js","sources":["../../../src/components/ui/IconButton.tsx"],"sourcesContent":["'use client';\n\nimport { forwardRef, type ButtonHTMLAttributes, type ReactNode, type SyntheticEvent } from 'react';\nimport { Loader2 } from '@/icons';\nimport { cn } from '@/lib/utils';\nimport { cva, type VariantProps } from '@/lib/cva';\n\nconst iconButton = cva(\n [\n 'inline-flex items-center justify-center shrink-0',\n 'rounded-md',\n 'transition-colors duration-[var(--duration-fast)] ease-[var(--ease-out)]',\n 'outline-none',\n 'focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',\n 'focus-visible:ring-offset-background',\n 'disabled:pointer-events-none disabled:opacity-50',\n 'aria-busy:pointer-events-none',\n '[&_svg]:pointer-events-none',\n ],\n {\n variants: {\n variant: {\n primary: 'bg-accent text-on-accent hover:bg-accent-hover active:bg-accent-active',\n secondary:\n 'bg-background-muted text-foreground border border-border-input hover:bg-surface-hover active:bg-surface-active',\n outline:\n 'border border-border-input bg-transparent text-foreground hover:bg-background-muted',\n ghost:\n 'bg-transparent text-foreground-muted hover:bg-background-muted hover:text-foreground',\n destructive: 'bg-danger text-on-danger hover:bg-danger-hover active:bg-danger-active',\n },\n size: {\n // Matches Button heights: sm 32 / md 36 / lg 40 / xl 44 (touch / marketing).\n sm: 'h-8 w-8 [&_svg]:size-4',\n md: 'h-9 w-9 [&_svg]:size-4',\n lg: 'h-10 w-10 [&_svg]:size-5',\n xl: 'h-11 w-11 [&_svg]:size-5',\n },\n },\n defaultVariants: {\n variant: 'ghost',\n size: 'md',\n },\n },\n);\n\nexport interface IconButtonProps\n extends ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof iconButton> {\n /**\n * Accessible label for screen readers. **Required** — an icon-only button\n * with no label is invisible to assistive tech.\n */\n 'aria-label': string;\n /** The Lucide icon to render. */\n icon: ReactNode;\n /** Show a spinner instead of the icon; blocks interaction but keeps focus (`aria-disabled`). */\n loading?: boolean;\n}\n\n/**\n * Square, icon-only button. Default variant is `ghost` because most\n * IconButtons sit in dense toolbars or tables. Always pass `aria-label`.\n *\n * @example Toolbar action\n * <IconButton aria-label=\"Edit row\" icon={<Edit2 />} onClick={onEdit} />\n *\n * @example Dismissible chip\n * <IconButton aria-label=\"Remove tag\" size=\"sm\" icon={<X />} />\n *\n * @do Use `ghost` for in-row actions; `outline` when next to a label-less\n * primary button you want users to consider as an alternative.\n * @dont Use IconButton when the action's meaning isn't universally\n * recognised. If users need to learn the icon, ship a Button with text.\n */\nexport const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(function IconButton(\n { className, variant, size, icon, loading, disabled, type = 'button', onClick, ...props },\n ref,\n) {\n return (\n <button\n ref={ref}\n type={type}\n aria-busy={loading || undefined}\n aria-disabled={loading || undefined}\n disabled={disabled}\n className={cn(iconButton({ variant, size }), className)}\n onClick={loading ? blockEvent : onClick}\n {...props}\n >\n {loading ? <Loader2 className=\"animate-spin\" aria-hidden=\"true\" /> : icon}\n </button>\n );\n});\n\nIconButton.displayName = 'IconButton';\n\nfunction blockEvent(e: SyntheticEvent) {\n e.preventDefault();\n e.stopPropagation();\n}\n"],"names":["iconButton","cva","IconButton","forwardRef","className","variant","size","icon","loading","disabled","type","onClick","props","ref","jsx","cn","blockEvent","Loader2","e"],"mappings":";;;;;;AAOA,MAAMA,IAAaC;AAAA,EACjB;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAAA,EAEF;AAAA,IACE,UAAU;AAAA,MACR,SAAS;AAAA,QACP,SAAS;AAAA,QACT,WACE;AAAA,QACF,SACE;AAAA,QACF,OACE;AAAA,QACF,aAAa;AAAA,MAAA;AAAA,MAEf,MAAM;AAAA;AAAA,QAEJ,IAAI;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,MAAA;AAAA,IACN;AAAA,IAEF,iBAAiB;AAAA,MACf,SAAS;AAAA,MACT,MAAM;AAAA,IAAA;AAAA,EACR;AAEJ,GA8BaC,IAAaC,EAA+C,SACvE,EAAE,WAAAC,GAAW,SAAAC,GAAS,MAAAC,GAAM,MAAAC,GAAM,SAAAC,GAAS,UAAAC,GAAU,MAAAC,IAAO,UAAU,SAAAC,GAAS,GAAGC,EAAA,GAClFC,GACA;AACA,SACE,gBAAAC;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,KAAAD;AAAA,MACA,MAAAH;AAAA,MACA,aAAWF,KAAW;AAAA,MACtB,iBAAeA,KAAW;AAAA,MAC1B,UAAAC;AAAA,MACA,WAAWM,EAAGf,EAAW,EAAE,SAAAK,GAAS,MAAAC,EAAA,CAAM,GAAGF,CAAS;AAAA,MACtD,SAASI,IAAUQ,IAAaL;AAAA,MAC/B,GAAGC;AAAA,MAEH,cAAU,gBAAAE,EAACG,GAAA,EAAQ,WAAU,gBAAe,eAAY,QAAO,IAAKV;AAAA,IAAA;AAAA,EAAA;AAG3E,CAAC;AAEDL,EAAW,cAAc;AAEzB,SAASc,EAAWE,GAAmB;AACrC,EAAAA,EAAE,eAAA,GACFA,EAAE,gBAAA;AACJ;"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { InputHTMLAttributes, ReactNode } from 'react';
|
|
2
|
+
import { VariantProps } from '../../lib/cva.js';
|
|
3
|
+
declare const field: (props?: ({
|
|
4
|
+
size?: "md" | "sm" | "lg" | null | undefined;
|
|
5
|
+
tone?: "default" | "error" | null | undefined;
|
|
6
|
+
} & import('class-variance-authority/types').ClassProp) | undefined) => string;
|
|
7
|
+
type InputType = NonNullable<InputHTMLAttributes<HTMLInputElement>['type']>;
|
|
8
|
+
export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size' | 'prefix' | 'value'>, VariantProps<typeof field> {
|
|
9
|
+
/** Controlled value. `null` is treated as an empty controlled value (no React warning). */
|
|
10
|
+
value?: InputHTMLAttributes<HTMLInputElement>['value'] | null;
|
|
11
|
+
/** Field type. `password` enables a show/hide toggle; `search` adds a clear button. */
|
|
12
|
+
type?: InputType;
|
|
13
|
+
/** Visible label rendered above the input. */
|
|
14
|
+
label?: ReactNode;
|
|
15
|
+
/** Hint below the input. Hidden while `error` is set. */
|
|
16
|
+
helperText?: ReactNode;
|
|
17
|
+
/** Validation message. Renders in `danger` colour and sets `aria-invalid`. */
|
|
18
|
+
error?: ReactNode;
|
|
19
|
+
/** Content rendered inside the field, before the input — icon or short text. */
|
|
20
|
+
prefix?: ReactNode;
|
|
21
|
+
/** Content rendered inside the field, after the input. */
|
|
22
|
+
suffix?: ReactNode;
|
|
23
|
+
/** Show an inline clear (×) button when the input has a value. */
|
|
24
|
+
clearable?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Fires when the clear button is pressed. Controlled inputs own their state
|
|
27
|
+
* and should reset `value` here; uncontrolled inputs are cleared for you.
|
|
28
|
+
*/
|
|
29
|
+
onClear?: () => void;
|
|
30
|
+
/** Visually conceal the label while keeping it for screen readers. */
|
|
31
|
+
hideLabel?: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Text-style input with optional label, helper / error, prefix / suffix,
|
|
35
|
+
* clear, and password show-hide. Wraps a single native `<input>` so it works
|
|
36
|
+
* with `react-hook-form` and any controlled / uncontrolled pattern.
|
|
37
|
+
*
|
|
38
|
+
* @example Email with helper
|
|
39
|
+
* <Input type="email" label="Work email" helperText="We never share this." />
|
|
40
|
+
*
|
|
41
|
+
* @example Password with show/hide
|
|
42
|
+
* <Input type="password" label="Password" autoComplete="current-password" />
|
|
43
|
+
*
|
|
44
|
+
* @example Search with clear
|
|
45
|
+
* <Input type="search" placeholder="Search…" value={q} onChange={…}
|
|
46
|
+
* clearable onClear={() => setQ('')} />
|
|
47
|
+
*
|
|
48
|
+
* @do Always pair an input with a visible label. If space forces hiding it,
|
|
49
|
+
* use `hideLabel` so the label stays in the accessibility tree.
|
|
50
|
+
* @dont Use placeholder as the only label — placeholders disappear on input
|
|
51
|
+
* and fail accessibility.
|
|
52
|
+
*/
|
|
53
|
+
export declare const Input: import('react').ForwardRefExoticComponent<InputProps & import('react').RefAttributes<HTMLInputElement>>;
|
|
54
|
+
export {};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsxs as B, jsx as e } from "react/jsx-runtime";
|
|
3
|
+
import { forwardRef as Y, useRef as Z, useState as P } from "react";
|
|
4
|
+
import { X as $, EyeOff as ee, Eye as te, Search as re } from "../../icons/index.js";
|
|
5
|
+
import { cn as s } from "../../lib/utils.js";
|
|
6
|
+
import { useStrings as ne } from "../../hooks/use-strings.js";
|
|
7
|
+
import { useFieldIds as se } from "../../hooks/use-field-ids.js";
|
|
8
|
+
import { cva as j } from "class-variance-authority";
|
|
9
|
+
const ie = j(
|
|
10
|
+
[
|
|
11
|
+
"group inline-flex items-center w-full",
|
|
12
|
+
"rounded-md border border-border-input bg-card text-sm",
|
|
13
|
+
"transition-colors duration-[var(--duration-fast)] ease-[var(--ease-out)]",
|
|
14
|
+
"has-disabled:opacity-50 has-disabled:pointer-events-none",
|
|
15
|
+
"focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-offset-background",
|
|
16
|
+
// Invalid state comes from the input's aria-invalid so Form wiring needs no extra prop.
|
|
17
|
+
"has-aria-invalid:border-danger has-aria-invalid:focus-within:border-danger has-aria-invalid:focus-within:ring-danger"
|
|
18
|
+
],
|
|
19
|
+
{
|
|
20
|
+
variants: {
|
|
21
|
+
size: {
|
|
22
|
+
sm: "h-8 px-2.5 gap-1.5",
|
|
23
|
+
md: "h-9 px-3 gap-2",
|
|
24
|
+
lg: "h-10 px-3.5 gap-2"
|
|
25
|
+
},
|
|
26
|
+
tone: {
|
|
27
|
+
default: "focus-within:border-accent focus-within:ring-ring",
|
|
28
|
+
error: "border-danger focus-within:border-danger focus-within:ring-danger"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
defaultVariants: {
|
|
32
|
+
size: "md",
|
|
33
|
+
tone: "default"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
), oe = j([
|
|
37
|
+
"flex-1 min-w-0 bg-transparent outline-none",
|
|
38
|
+
// Fill the field's height so the focusable target is the full control
|
|
39
|
+
// (32/36/40px), not the ~20px text box. Below 24px axe's target-size
|
|
40
|
+
// rule only passes while nothing sits close by, which is a trap for
|
|
41
|
+
// consumers packing a toolbar.
|
|
42
|
+
"self-stretch",
|
|
43
|
+
"placeholder:text-foreground-subtle",
|
|
44
|
+
"text-foreground",
|
|
45
|
+
// 16px below md so iOS Safari does not zoom the page on focus; --text-lg = 1rem.
|
|
46
|
+
"text-lg md:text-sm",
|
|
47
|
+
"disabled:cursor-not-allowed",
|
|
48
|
+
// `type="search"` draws a native clear button in WebKit/Chromium. We render
|
|
49
|
+
// our own via `clearable`, so suppress the native one or both show at once.
|
|
50
|
+
"[&::-webkit-search-cancel-button]:appearance-none",
|
|
51
|
+
"[&::-webkit-search-decoration]:appearance-none"
|
|
52
|
+
]), S = s(
|
|
53
|
+
"text-foreground-subtle hover:text-foreground relative -m-1 flex size-6 shrink-0 items-center justify-center rounded-sm",
|
|
54
|
+
'before:absolute before:-inset-0.5 before:content-[""]',
|
|
55
|
+
"focus-visible:ring-ring focus-visible:ring-offset-background outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
|
|
56
|
+
"disabled:pointer-events-none"
|
|
57
|
+
), ae = Y(function({
|
|
58
|
+
className: R,
|
|
59
|
+
type: i = "text",
|
|
60
|
+
size: O,
|
|
61
|
+
tone: F,
|
|
62
|
+
label: v,
|
|
63
|
+
helperText: l,
|
|
64
|
+
error: o,
|
|
65
|
+
prefix: H,
|
|
66
|
+
suffix: w,
|
|
67
|
+
clearable: L,
|
|
68
|
+
onClear: u,
|
|
69
|
+
hideLabel: T,
|
|
70
|
+
id: _,
|
|
71
|
+
disabled: r,
|
|
72
|
+
value: N,
|
|
73
|
+
defaultValue: a,
|
|
74
|
+
onInput: f,
|
|
75
|
+
"aria-describedby": D,
|
|
76
|
+
"aria-invalid": p,
|
|
77
|
+
...M
|
|
78
|
+
}, c) {
|
|
79
|
+
const h = ne(), y = !!o || p === !0 || p === "true", { fieldId: z, helperId: V, errorId: X, describedBy: q } = se(_, {
|
|
80
|
+
hasHelper: !!l,
|
|
81
|
+
hasError: !!o,
|
|
82
|
+
extra: D
|
|
83
|
+
}), m = N === null ? "" : N, n = Z(null), A = (t) => {
|
|
84
|
+
n.current = t, typeof c == "function" ? c(t) : c && (c.current = t);
|
|
85
|
+
}, [d, C] = P(!1), G = i === "password" ? d ? "text" : "password" : i, J = y ? "error" : F, k = H ?? (i === "search" ? /* @__PURE__ */ e(re, { className: "text-foreground-subtle size-4", "aria-hidden": !0 }) : null), b = m !== void 0, [K, E] = P(
|
|
86
|
+
a == null ? "" : String(a)
|
|
87
|
+
), g = b ? m : K, Q = g !== void 0 && g !== "" && g !== null, U = (t) => {
|
|
88
|
+
b || E(t.currentTarget.value), f == null || f(t);
|
|
89
|
+
}, W = () => {
|
|
90
|
+
var t, I;
|
|
91
|
+
if (!b && n.current) {
|
|
92
|
+
const x = (t = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")) == null ? void 0 : t.set;
|
|
93
|
+
x == null || x.call(n.current, ""), n.current.dispatchEvent(new Event("input", { bubbles: !0 })), E("");
|
|
94
|
+
}
|
|
95
|
+
u == null || u(), (I = n.current) == null || I.focus();
|
|
96
|
+
};
|
|
97
|
+
return /* @__PURE__ */ B("div", { className: s("flex flex-col gap-1.5", R), children: [
|
|
98
|
+
v && /* @__PURE__ */ e(
|
|
99
|
+
"label",
|
|
100
|
+
{
|
|
101
|
+
htmlFor: z,
|
|
102
|
+
className: s("text-foreground text-sm font-medium", T && "sr-only"),
|
|
103
|
+
children: v
|
|
104
|
+
}
|
|
105
|
+
),
|
|
106
|
+
/* @__PURE__ */ B("div", { className: s(ie({ size: O, tone: J })), children: [
|
|
107
|
+
k && /* @__PURE__ */ e("span", { className: "text-foreground-subtle flex items-center [&_svg]:size-4", children: k }),
|
|
108
|
+
/* @__PURE__ */ e(
|
|
109
|
+
"input",
|
|
110
|
+
{
|
|
111
|
+
ref: A,
|
|
112
|
+
id: z,
|
|
113
|
+
type: G,
|
|
114
|
+
disabled: r,
|
|
115
|
+
value: m,
|
|
116
|
+
defaultValue: a,
|
|
117
|
+
onInput: U,
|
|
118
|
+
"aria-invalid": y ? !0 : p,
|
|
119
|
+
"aria-describedby": q,
|
|
120
|
+
className: s(oe()),
|
|
121
|
+
...M
|
|
122
|
+
}
|
|
123
|
+
),
|
|
124
|
+
L && Q && /* @__PURE__ */ e(
|
|
125
|
+
"button",
|
|
126
|
+
{
|
|
127
|
+
type: "button",
|
|
128
|
+
onClick: W,
|
|
129
|
+
disabled: r,
|
|
130
|
+
tabIndex: r ? -1 : void 0,
|
|
131
|
+
"aria-label": h.input.clear,
|
|
132
|
+
className: S,
|
|
133
|
+
children: /* @__PURE__ */ e($, { className: "size-4", "aria-hidden": !0 })
|
|
134
|
+
}
|
|
135
|
+
),
|
|
136
|
+
i === "password" && /* @__PURE__ */ e(
|
|
137
|
+
"button",
|
|
138
|
+
{
|
|
139
|
+
type: "button",
|
|
140
|
+
onClick: () => C((t) => !t),
|
|
141
|
+
disabled: r,
|
|
142
|
+
tabIndex: r ? -1 : void 0,
|
|
143
|
+
"aria-label": d ? h.input.hidePassword : h.input.showPassword,
|
|
144
|
+
"aria-pressed": d,
|
|
145
|
+
className: S,
|
|
146
|
+
children: d ? /* @__PURE__ */ e(ee, { className: "size-4", "aria-hidden": !0 }) : /* @__PURE__ */ e(te, { className: "size-4", "aria-hidden": !0 })
|
|
147
|
+
}
|
|
148
|
+
),
|
|
149
|
+
w && /* @__PURE__ */ e("span", { className: "text-foreground-subtle flex items-center [&_svg]:size-4", children: w })
|
|
150
|
+
] }),
|
|
151
|
+
o ? /* @__PURE__ */ e("p", { id: X, className: "text-danger-text text-xs", children: o }) : l ? /* @__PURE__ */ e("p", { id: V, className: "text-foreground-subtle text-xs", children: l }) : null
|
|
152
|
+
] });
|
|
153
|
+
});
|
|
154
|
+
ae.displayName = "Input";
|
|
155
|
+
export {
|
|
156
|
+
ae as Input
|
|
157
|
+
};
|
|
158
|
+
//# sourceMappingURL=Input.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Input.js","sources":["../../../src/components/ui/Input.tsx"],"sourcesContent":["'use client';\n\nimport {\n forwardRef,\n useRef,\n useState,\n type ComponentProps,\n type InputHTMLAttributes,\n type ReactNode,\n} from 'react';\nimport { Eye, EyeOff, Search, X } from '@/icons';\nimport { cn } from '@/lib/utils';\nimport { useStrings } from '@/hooks/use-strings';\nimport { cva, type VariantProps } from '@/lib/cva';\nimport { useFieldIds } from '@/hooks/use-field-ids';\n\n/* -----------------------------------------------------------------------------\n * Field-shell variants. The inner <input> is unstyled background and gets all\n * its visual treatment from this wrapper so prefix / suffix / clear slots\n * share the same border + focus state with no double rings.\n * --------------------------------------------------------------------------- */\nconst field = cva(\n [\n 'group inline-flex items-center w-full',\n 'rounded-md border border-border-input bg-card text-sm',\n 'transition-colors duration-[var(--duration-fast)] ease-[var(--ease-out)]',\n 'has-disabled:opacity-50 has-disabled:pointer-events-none',\n 'focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-offset-background',\n // Invalid state comes from the input's aria-invalid so Form wiring needs no extra prop.\n 'has-aria-invalid:border-danger has-aria-invalid:focus-within:border-danger has-aria-invalid:focus-within:ring-danger',\n ],\n {\n variants: {\n size: {\n sm: 'h-8 px-2.5 gap-1.5',\n md: 'h-9 px-3 gap-2',\n lg: 'h-10 px-3.5 gap-2',\n },\n tone: {\n default: 'focus-within:border-accent focus-within:ring-ring',\n error: 'border-danger focus-within:border-danger focus-within:ring-danger',\n },\n },\n defaultVariants: {\n size: 'md',\n tone: 'default',\n },\n },\n);\n\nconst innerInput = cva([\n 'flex-1 min-w-0 bg-transparent outline-none',\n // Fill the field's height so the focusable target is the full control\n // (32/36/40px), not the ~20px text box. Below 24px axe's target-size\n // rule only passes while nothing sits close by, which is a trap for\n // consumers packing a toolbar.\n 'self-stretch',\n 'placeholder:text-foreground-subtle',\n 'text-foreground',\n // 16px below md so iOS Safari does not zoom the page on focus; --text-lg = 1rem.\n 'text-lg md:text-sm',\n 'disabled:cursor-not-allowed',\n // `type=\"search\"` draws a native clear button in WebKit/Chromium. We render\n // our own via `clearable`, so suppress the native one or both show at once.\n '[&::-webkit-search-cancel-button]:appearance-none',\n '[&::-webkit-search-decoration]:appearance-none',\n]);\n\ntype InputType = NonNullable<InputHTMLAttributes<HTMLInputElement>['type']>;\n\n// Inline clear / password buttons: 24px box (WCAG 2.5.8 minimum) with a halo\n// that extends the hit area without growing the field.\nconst innerButton = cn(\n 'text-foreground-subtle hover:text-foreground relative -m-1 flex size-6 shrink-0 items-center justify-center rounded-sm',\n 'before:absolute before:-inset-0.5 before:content-[\"\"]',\n 'focus-visible:ring-ring focus-visible:ring-offset-background outline-none focus-visible:ring-2 focus-visible:ring-offset-2',\n 'disabled:pointer-events-none',\n);\n\nexport interface InputProps\n extends\n Omit<InputHTMLAttributes<HTMLInputElement>, 'size' | 'prefix' | 'value'>,\n VariantProps<typeof field> {\n /** Controlled value. `null` is treated as an empty controlled value (no React warning). */\n value?: InputHTMLAttributes<HTMLInputElement>['value'] | null;\n /** Field type. `password` enables a show/hide toggle; `search` adds a clear button. */\n type?: InputType;\n /** Visible label rendered above the input. */\n label?: ReactNode;\n /** Hint below the input. Hidden while `error` is set. */\n helperText?: ReactNode;\n /** Validation message. Renders in `danger` colour and sets `aria-invalid`. */\n error?: ReactNode;\n /** Content rendered inside the field, before the input — icon or short text. */\n prefix?: ReactNode;\n /** Content rendered inside the field, after the input. */\n suffix?: ReactNode;\n /** Show an inline clear (×) button when the input has a value. */\n clearable?: boolean;\n /**\n * Fires when the clear button is pressed. Controlled inputs own their state\n * and should reset `value` here; uncontrolled inputs are cleared for you.\n */\n onClear?: () => void;\n /** Visually conceal the label while keeping it for screen readers. */\n hideLabel?: boolean;\n}\n\n/**\n * Text-style input with optional label, helper / error, prefix / suffix,\n * clear, and password show-hide. Wraps a single native `<input>` so it works\n * with `react-hook-form` and any controlled / uncontrolled pattern.\n *\n * @example Email with helper\n * <Input type=\"email\" label=\"Work email\" helperText=\"We never share this.\" />\n *\n * @example Password with show/hide\n * <Input type=\"password\" label=\"Password\" autoComplete=\"current-password\" />\n *\n * @example Search with clear\n * <Input type=\"search\" placeholder=\"Search…\" value={q} onChange={…}\n * clearable onClear={() => setQ('')} />\n *\n * @do Always pair an input with a visible label. If space forces hiding it,\n * use `hideLabel` so the label stays in the accessibility tree.\n * @dont Use placeholder as the only label — placeholders disappear on input\n * and fail accessibility.\n */\nexport const Input = forwardRef<HTMLInputElement, InputProps>(function Input(\n {\n className,\n type = 'text',\n size,\n tone,\n label,\n helperText,\n error,\n prefix,\n suffix,\n clearable,\n onClear,\n hideLabel,\n id,\n disabled,\n value: valueProp,\n defaultValue,\n onInput,\n 'aria-describedby': ariaDescribedby,\n 'aria-invalid': ariaInvalid,\n ...props\n },\n ref,\n) {\n const strings = useStrings();\n const isError = Boolean(error) || ariaInvalid === true || ariaInvalid === 'true';\n const { fieldId, helperId, errorId, describedBy } = useFieldIds(id, {\n hasHelper: Boolean(helperText),\n hasError: Boolean(error),\n extra: ariaDescribedby,\n });\n // `null` (nullable form state) is a controlled empty value, not \"uncontrolled\".\n const value = valueProp === null ? '' : valueProp;\n const innerRef = useRef<HTMLInputElement | null>(null);\n const setRefs = (el: HTMLInputElement | null) => {\n innerRef.current = el;\n if (typeof ref === 'function') ref(el);\n else if (ref) ref.current = el;\n };\n\n const [showPassword, setShowPassword] = useState(false);\n const effectiveType = type === 'password' ? (showPassword ? 'text' : 'password') : type;\n\n const effectiveTone = isError ? 'error' : tone;\n\n // Pre-fill prefix slot for search type.\n const renderedPrefix =\n prefix ??\n (type === 'search' ? <Search className=\"text-foreground-subtle size-4\" aria-hidden /> : null);\n\n // Track the value of uncontrolled inputs so `clearable` can show/hide the\n // button without the consumer wiring state.\n const isControlled = value !== undefined;\n const [localValue, setLocalValue] = useState<string>(\n defaultValue === undefined || defaultValue === null ? '' : String(defaultValue),\n );\n const current = isControlled ? value : localValue;\n const hasValue = current !== undefined && current !== '' && current !== null;\n\n // React 19 types `onInput` as `InputEventHandler`; `ComponentProps` keeps this portable.\n const handleInput: NonNullable<ComponentProps<'input'>['onInput']> = (e) => {\n if (!isControlled) setLocalValue(e.currentTarget.value);\n onInput?.(e);\n };\n\n const handleClear = () => {\n if (!isControlled && innerRef.current) {\n // Use the native setter so React's onChange/onInput listeners fire.\n const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set;\n setter?.call(innerRef.current, '');\n innerRef.current.dispatchEvent(new Event('input', { bubbles: true }));\n setLocalValue('');\n }\n onClear?.();\n innerRef.current?.focus();\n };\n\n return (\n <div className={cn('flex flex-col gap-1.5', className)}>\n {label && (\n <label\n htmlFor={fieldId}\n className={cn('text-foreground text-sm font-medium', hideLabel && 'sr-only')}\n >\n {label}\n </label>\n )}\n\n <div className={cn(field({ size, tone: effectiveTone }))}>\n {renderedPrefix && (\n <span className=\"text-foreground-subtle flex items-center [&_svg]:size-4\">\n {renderedPrefix}\n </span>\n )}\n\n <input\n ref={setRefs}\n id={fieldId}\n type={effectiveType}\n disabled={disabled}\n value={value}\n defaultValue={defaultValue}\n onInput={handleInput}\n aria-invalid={isError ? true : ariaInvalid}\n aria-describedby={describedBy}\n className={cn(innerInput())}\n {...props}\n />\n\n {clearable && hasValue && (\n <button\n type=\"button\"\n onClick={handleClear}\n disabled={disabled}\n tabIndex={disabled ? -1 : undefined}\n aria-label={strings.input.clear}\n className={innerButton}\n >\n <X className=\"size-4\" aria-hidden />\n </button>\n )}\n\n {type === 'password' && (\n <button\n type=\"button\"\n onClick={() => setShowPassword((p) => !p)}\n disabled={disabled}\n tabIndex={disabled ? -1 : undefined}\n aria-label={showPassword ? strings.input.hidePassword : strings.input.showPassword}\n aria-pressed={showPassword}\n className={innerButton}\n >\n {showPassword ? (\n <EyeOff className=\"size-4\" aria-hidden />\n ) : (\n <Eye className=\"size-4\" aria-hidden />\n )}\n </button>\n )}\n\n {suffix && (\n <span className=\"text-foreground-subtle flex items-center [&_svg]:size-4\">{suffix}</span>\n )}\n </div>\n\n {error ? (\n <p id={errorId} className=\"text-danger-text text-xs\">\n {error}\n </p>\n ) : helperText ? (\n <p id={helperId} className=\"text-foreground-subtle text-xs\">\n {helperText}\n </p>\n ) : null}\n </div>\n );\n});\n\nInput.displayName = 'Input';\n"],"names":["field","cva","innerInput","innerButton","cn","Input","forwardRef","className","type","size","tone","label","helperText","error","prefix","suffix","clearable","onClear","hideLabel","id","disabled","valueProp","defaultValue","onInput","ariaDescribedby","ariaInvalid","props","ref","strings","useStrings","isError","fieldId","helperId","errorId","describedBy","useFieldIds","value","innerRef","useRef","setRefs","el","showPassword","setShowPassword","useState","effectiveType","effectiveTone","renderedPrefix","jsx","Search","isControlled","localValue","setLocalValue","current","hasValue","handleInput","e","handleClear","setter","_a","_b","jsxs","X","p","EyeOff","Eye"],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { HTMLAttributes } from 'react';
|
|
2
|
+
export interface KbdProps extends HTMLAttributes<HTMLElement> {
|
|
3
|
+
/** Visual size — match the surrounding text. */
|
|
4
|
+
size?: 'sm' | 'md';
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Stylised keyboard shortcut indicator. Compose multiple `<Kbd>` for chords.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* Press <Kbd>⌘</Kbd>+<Kbd>K</Kbd> to open the command palette.
|
|
11
|
+
*
|
|
12
|
+
* @do Use OS-conventional symbols (⌘ ⇧ ⌥ ⌃ ⏎) — keep visuals consistent
|
|
13
|
+
* across the app.
|
|
14
|
+
* @dont Use Kbd for clickable buttons — it implies a real keyboard input.
|
|
15
|
+
*/
|
|
16
|
+
export declare const Kbd: import('react').ForwardRefExoticComponent<KbdProps & import('react').RefAttributes<HTMLElement>>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as n } from "react/jsx-runtime";
|
|
3
|
+
import { forwardRef as m } from "react";
|
|
4
|
+
import { cn as s } from "../../lib/utils.js";
|
|
5
|
+
const d = m(function({ size: e = "sm", className: r, ...o }, t) {
|
|
6
|
+
return /* @__PURE__ */ n(
|
|
7
|
+
"kbd",
|
|
8
|
+
{
|
|
9
|
+
ref: t,
|
|
10
|
+
className: s(
|
|
11
|
+
"border-border bg-background-subtle inline-flex items-center justify-center rounded-sm border font-mono",
|
|
12
|
+
"text-foreground-muted shadow-xs",
|
|
13
|
+
e === "sm" ? "h-5 min-w-5 px-1 text-xs" : "h-6 min-w-6 px-1.5 text-xs",
|
|
14
|
+
r
|
|
15
|
+
),
|
|
16
|
+
...o
|
|
17
|
+
}
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
d.displayName = "Kbd";
|
|
21
|
+
export {
|
|
22
|
+
d as Kbd
|
|
23
|
+
};
|
|
24
|
+
//# sourceMappingURL=Kbd.js.map
|