@questpie/admin 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +108 -0
- package/CHANGELOG.md +10 -0
- package/README.md +556 -0
- package/STATUS.md +917 -0
- package/VALIDATION.md +602 -0
- package/components.json +24 -0
- package/dist/__tests__/setup.mjs +38 -0
- package/dist/__tests__/test-utils.mjs +45 -0
- package/dist/__tests__/vitest.d.mjs +3 -0
- package/dist/components/admin-app.mjs +69 -0
- package/dist/components/fields/array-field.mjs +190 -0
- package/dist/components/fields/checkbox-field.mjs +34 -0
- package/dist/components/fields/custom-field.mjs +32 -0
- package/dist/components/fields/date-field.mjs +41 -0
- package/dist/components/fields/datetime-field.mjs +42 -0
- package/dist/components/fields/email-field.mjs +37 -0
- package/dist/components/fields/embedded-collection.mjs +253 -0
- package/dist/components/fields/field-types.mjs +1 -0
- package/dist/components/fields/field-utils.mjs +10 -0
- package/dist/components/fields/field-wrapper.mjs +34 -0
- package/dist/components/fields/index.mjs +23 -0
- package/dist/components/fields/json-field.mjs +243 -0
- package/dist/components/fields/locale-badge.mjs +16 -0
- package/dist/components/fields/number-field.mjs +39 -0
- package/dist/components/fields/password-field.mjs +37 -0
- package/dist/components/fields/relation-field.mjs +104 -0
- package/dist/components/fields/relation-picker.mjs +229 -0
- package/dist/components/fields/relation-select.mjs +188 -0
- package/dist/components/fields/rich-text-editor/index.mjs +897 -0
- package/dist/components/fields/select-field.mjs +41 -0
- package/dist/components/fields/switch-field.mjs +34 -0
- package/dist/components/fields/text-field.mjs +38 -0
- package/dist/components/fields/textarea-field.mjs +38 -0
- package/dist/components/index.mjs +59 -0
- package/dist/components/primitives/checkbox-input.mjs +127 -0
- package/dist/components/primitives/date-input.mjs +303 -0
- package/dist/components/primitives/index.mjs +12 -0
- package/dist/components/primitives/number-input.mjs +104 -0
- package/dist/components/primitives/select-input.mjs +177 -0
- package/dist/components/primitives/tag-input.mjs +135 -0
- package/dist/components/primitives/text-input.mjs +39 -0
- package/dist/components/primitives/textarea-input.mjs +37 -0
- package/dist/components/primitives/toggle-input.mjs +31 -0
- package/dist/components/primitives/types.mjs +12 -0
- package/dist/components/ui/accordion.mjs +55 -0
- package/dist/components/ui/avatar.mjs +54 -0
- package/dist/components/ui/badge.mjs +34 -0
- package/dist/components/ui/button.mjs +48 -0
- package/dist/components/ui/card.mjs +58 -0
- package/dist/components/ui/checkbox.mjs +21 -0
- package/dist/components/ui/combobox.mjs +163 -0
- package/dist/components/ui/dialog.mjs +95 -0
- package/dist/components/ui/dropdown-menu.mjs +138 -0
- package/dist/components/ui/field.mjs +113 -0
- package/dist/components/ui/input-group.mjs +82 -0
- package/dist/components/ui/input.mjs +17 -0
- package/dist/components/ui/label.mjs +15 -0
- package/dist/components/ui/popover.mjs +56 -0
- package/dist/components/ui/scroll-area.mjs +38 -0
- package/dist/components/ui/select.mjs +100 -0
- package/dist/components/ui/separator.mjs +16 -0
- package/dist/components/ui/sheet.mjs +90 -0
- package/dist/components/ui/sidebar.mjs +387 -0
- package/dist/components/ui/skeleton.mjs +14 -0
- package/dist/components/ui/spinner.mjs +16 -0
- package/dist/components/ui/switch.mjs +22 -0
- package/dist/components/ui/table.mjs +68 -0
- package/dist/components/ui/tabs.mjs +48 -0
- package/dist/components/ui/textarea.mjs +15 -0
- package/dist/components/ui/tooltip.mjs +44 -0
- package/dist/config/component-registry.mjs +38 -0
- package/dist/config/index.mjs +129 -0
- package/dist/hooks/admin-provider.mjs +70 -0
- package/dist/hooks/index.mjs +7 -0
- package/dist/hooks/store.mjs +178 -0
- package/dist/hooks/use-auth.mjs +76 -0
- package/dist/hooks/use-collection-db.mjs +146 -0
- package/dist/hooks/use-collection.mjs +112 -0
- package/dist/hooks/use-global.mjs +46 -0
- package/dist/hooks/use-mobile.mjs +20 -0
- package/dist/lib/utils.mjs +10 -0
- package/dist/styles/index.css +336 -0
- package/dist/styles/index.mjs +1 -0
- package/dist/utils/index.mjs +9 -0
- package/dist/views/auth/auth-layout.mjs +52 -0
- package/dist/views/auth/forgot-password-form.mjs +148 -0
- package/dist/views/auth/index.mjs +6 -0
- package/dist/views/auth/login-form.mjs +156 -0
- package/dist/views/auth/reset-password-form.mjs +184 -0
- package/dist/views/collection/auto-form-fields.mjs +525 -0
- package/dist/views/collection/collection-form.mjs +91 -0
- package/dist/views/collection/collection-list.mjs +76 -0
- package/dist/views/collection/form-field.mjs +42 -0
- package/dist/views/collection/index.mjs +6 -0
- package/dist/views/common/index.mjs +4 -0
- package/dist/views/common/locale-switcher.mjs +39 -0
- package/dist/views/common/version-history.mjs +272 -0
- package/dist/views/index.mjs +9 -0
- package/dist/views/layout/admin-layout.mjs +40 -0
- package/dist/views/layout/admin-router.mjs +95 -0
- package/dist/views/layout/admin-sidebar.mjs +63 -0
- package/dist/views/layout/index.mjs +5 -0
- package/package.json +276 -0
- package/src/__tests__/setup.ts +44 -0
- package/src/__tests__/test-utils.tsx +49 -0
- package/src/__tests__/vitest.d.ts +9 -0
- package/src/components/admin-app.tsx +221 -0
- package/src/components/fields/array-field.tsx +237 -0
- package/src/components/fields/checkbox-field.tsx +47 -0
- package/src/components/fields/custom-field.tsx +50 -0
- package/src/components/fields/date-field.tsx +65 -0
- package/src/components/fields/datetime-field.tsx +67 -0
- package/src/components/fields/email-field.tsx +51 -0
- package/src/components/fields/embedded-collection.tsx +315 -0
- package/src/components/fields/field-types.ts +162 -0
- package/src/components/fields/field-utils.ts +6 -0
- package/src/components/fields/field-wrapper.tsx +52 -0
- package/src/components/fields/index.ts +66 -0
- package/src/components/fields/json-field.tsx +440 -0
- package/src/components/fields/locale-badge.tsx +15 -0
- package/src/components/fields/number-field.tsx +57 -0
- package/src/components/fields/password-field.tsx +51 -0
- package/src/components/fields/relation-field.tsx +243 -0
- package/src/components/fields/relation-picker.tsx +402 -0
- package/src/components/fields/relation-select.tsx +327 -0
- package/src/components/fields/rich-text-editor/index.tsx +1337 -0
- package/src/components/fields/select-field.tsx +61 -0
- package/src/components/fields/switch-field.tsx +47 -0
- package/src/components/fields/text-field.tsx +55 -0
- package/src/components/fields/textarea-field.tsx +55 -0
- package/src/components/index.ts +40 -0
- package/src/components/primitives/checkbox-input.tsx +193 -0
- package/src/components/primitives/date-input.tsx +401 -0
- package/src/components/primitives/index.ts +24 -0
- package/src/components/primitives/number-input.tsx +132 -0
- package/src/components/primitives/select-input.tsx +296 -0
- package/src/components/primitives/tag-input.tsx +200 -0
- package/src/components/primitives/text-input.tsx +49 -0
- package/src/components/primitives/textarea-input.tsx +46 -0
- package/src/components/primitives/toggle-input.tsx +36 -0
- package/src/components/primitives/types.ts +235 -0
- package/src/components/ui/accordion.tsx +72 -0
- package/src/components/ui/avatar.tsx +106 -0
- package/src/components/ui/badge.tsx +48 -0
- package/src/components/ui/button.tsx +53 -0
- package/src/components/ui/card.tsx +94 -0
- package/src/components/ui/checkbox.tsx +27 -0
- package/src/components/ui/combobox.tsx +290 -0
- package/src/components/ui/dialog.tsx +151 -0
- package/src/components/ui/dropdown-menu.tsx +254 -0
- package/src/components/ui/field.tsx +227 -0
- package/src/components/ui/input-group.tsx +149 -0
- package/src/components/ui/input.tsx +20 -0
- package/src/components/ui/label.tsx +18 -0
- package/src/components/ui/popover.tsx +88 -0
- package/src/components/ui/scroll-area.tsx +53 -0
- package/src/components/ui/select.tsx +192 -0
- package/src/components/ui/separator.tsx +23 -0
- package/src/components/ui/sheet.tsx +127 -0
- package/src/components/ui/sidebar.tsx +723 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/spinner.tsx +10 -0
- package/src/components/ui/switch.tsx +32 -0
- package/src/components/ui/table.tsx +99 -0
- package/src/components/ui/tabs.tsx +82 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +70 -0
- package/src/config/component-registry.ts +190 -0
- package/src/config/index.ts +1099 -0
- package/src/hooks/README.md +269 -0
- package/src/hooks/admin-provider.tsx +110 -0
- package/src/hooks/index.ts +41 -0
- package/src/hooks/store.ts +248 -0
- package/src/hooks/use-auth.ts +168 -0
- package/src/hooks/use-collection-db.ts +209 -0
- package/src/hooks/use-collection.ts +156 -0
- package/src/hooks/use-global.ts +69 -0
- package/src/hooks/use-mobile.ts +21 -0
- package/src/lib/utils.ts +6 -0
- package/src/styles/index.css +340 -0
- package/src/utils/index.ts +6 -0
- package/src/views/auth/auth-layout.tsx +77 -0
- package/src/views/auth/forgot-password-form.tsx +192 -0
- package/src/views/auth/index.ts +21 -0
- package/src/views/auth/login-form.tsx +229 -0
- package/src/views/auth/reset-password-form.tsx +232 -0
- package/src/views/collection/auto-form-fields.tsx +982 -0
- package/src/views/collection/collection-form.tsx +186 -0
- package/src/views/collection/collection-list.tsx +223 -0
- package/src/views/collection/form-field.tsx +52 -0
- package/src/views/collection/index.ts +15 -0
- package/src/views/common/index.ts +8 -0
- package/src/views/common/locale-switcher.tsx +45 -0
- package/src/views/common/version-history.tsx +406 -0
- package/src/views/index.ts +25 -0
- package/src/views/layout/admin-layout.tsx +117 -0
- package/src/views/layout/admin-router.tsx +206 -0
- package/src/views/layout/admin-sidebar.tsx +185 -0
- package/src/views/layout/index.ts +12 -0
- package/tsconfig.json +13 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/tsdown.config.ts +13 -0
- package/vitest.config.ts +29 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Button } from "../ui/button";
|
|
3
|
+
import { Input } from "../ui/input";
|
|
4
|
+
import { cn } from "../../lib/utils";
|
|
5
|
+
import { Minus, Plus } from "@phosphor-icons/react";
|
|
6
|
+
|
|
7
|
+
//#region src/components/primitives/number-input.tsx
|
|
8
|
+
/**
|
|
9
|
+
* Number Input Primitive
|
|
10
|
+
*
|
|
11
|
+
* A number input with optional increment/decrement buttons.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* <NumberInput
|
|
16
|
+
* value={count}
|
|
17
|
+
* onChange={setCount}
|
|
18
|
+
* min={0}
|
|
19
|
+
* max={100}
|
|
20
|
+
* step={1}
|
|
21
|
+
* showButtons
|
|
22
|
+
* />
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
function NumberInput({ value, onChange, min, max, step = 1, showButtons = false, placeholder, disabled, readOnly, className, id, "aria-invalid": ariaInvalid }) {
|
|
26
|
+
const handleChange = (newValue) => {
|
|
27
|
+
if (newValue === null) {
|
|
28
|
+
onChange(null);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
let clampedValue = newValue;
|
|
32
|
+
if (min !== void 0 && clampedValue < min) clampedValue = min;
|
|
33
|
+
if (max !== void 0 && clampedValue > max) clampedValue = max;
|
|
34
|
+
onChange(clampedValue);
|
|
35
|
+
};
|
|
36
|
+
const increment = () => {
|
|
37
|
+
handleChange((value ?? 0) + step);
|
|
38
|
+
};
|
|
39
|
+
const decrement = () => {
|
|
40
|
+
handleChange((value ?? 0) - step);
|
|
41
|
+
};
|
|
42
|
+
if (showButtons) return /* @__PURE__ */ jsxs("div", {
|
|
43
|
+
className: cn("flex items-center gap-1", className),
|
|
44
|
+
children: [
|
|
45
|
+
/* @__PURE__ */ jsx(Button, {
|
|
46
|
+
type: "button",
|
|
47
|
+
variant: "outline",
|
|
48
|
+
size: "icon-sm",
|
|
49
|
+
onClick: decrement,
|
|
50
|
+
disabled: disabled || min !== void 0 && (value ?? 0) <= min,
|
|
51
|
+
tabIndex: -1,
|
|
52
|
+
children: /* @__PURE__ */ jsx(Minus, { className: "size-3" })
|
|
53
|
+
}),
|
|
54
|
+
/* @__PURE__ */ jsx(Input, {
|
|
55
|
+
id,
|
|
56
|
+
type: "number",
|
|
57
|
+
value: value ?? "",
|
|
58
|
+
onChange: (e) => {
|
|
59
|
+
const val = e.target.value;
|
|
60
|
+
if (val === "") handleChange(null);
|
|
61
|
+
else handleChange(Number(val));
|
|
62
|
+
},
|
|
63
|
+
placeholder,
|
|
64
|
+
disabled,
|
|
65
|
+
readOnly,
|
|
66
|
+
min,
|
|
67
|
+
max,
|
|
68
|
+
step,
|
|
69
|
+
"aria-invalid": ariaInvalid,
|
|
70
|
+
className: "text-center [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
|
|
71
|
+
}),
|
|
72
|
+
/* @__PURE__ */ jsx(Button, {
|
|
73
|
+
type: "button",
|
|
74
|
+
variant: "outline",
|
|
75
|
+
size: "icon-sm",
|
|
76
|
+
onClick: increment,
|
|
77
|
+
disabled: disabled || max !== void 0 && (value ?? 0) >= max,
|
|
78
|
+
tabIndex: -1,
|
|
79
|
+
children: /* @__PURE__ */ jsx(Plus, { className: "size-3" })
|
|
80
|
+
})
|
|
81
|
+
]
|
|
82
|
+
});
|
|
83
|
+
return /* @__PURE__ */ jsx(Input, {
|
|
84
|
+
id,
|
|
85
|
+
type: "number",
|
|
86
|
+
value: value ?? "",
|
|
87
|
+
onChange: (e) => {
|
|
88
|
+
const val = e.target.value;
|
|
89
|
+
if (val === "") handleChange(null);
|
|
90
|
+
else handleChange(Number(val));
|
|
91
|
+
},
|
|
92
|
+
placeholder,
|
|
93
|
+
disabled,
|
|
94
|
+
readOnly,
|
|
95
|
+
min,
|
|
96
|
+
max,
|
|
97
|
+
step,
|
|
98
|
+
"aria-invalid": ariaInvalid,
|
|
99
|
+
className: cn(className)
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
//#endregion
|
|
104
|
+
export { NumberInput };
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
import { Combobox, ComboboxChip, ComboboxChips, ComboboxChipsInput, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, useComboboxAnchor } from "../ui/combobox";
|
|
6
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select";
|
|
7
|
+
import { cn } from "../../lib/utils";
|
|
8
|
+
import { Check, CircleNotch } from "@phosphor-icons/react";
|
|
9
|
+
import { flattenOptions } from "./types";
|
|
10
|
+
|
|
11
|
+
//#region src/components/primitives/select-input.tsx
|
|
12
|
+
/**
|
|
13
|
+
* Select Input Primitive
|
|
14
|
+
*
|
|
15
|
+
* A unified select component that supports:
|
|
16
|
+
* - Single select (dropdown)
|
|
17
|
+
* - Multi-select (chips)
|
|
18
|
+
* - Static options
|
|
19
|
+
* - Dynamic loading via loadOptions
|
|
20
|
+
* - Search/filtering
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```tsx
|
|
24
|
+
* // Single select with static options
|
|
25
|
+
* <SelectInput
|
|
26
|
+
* value={status}
|
|
27
|
+
* onChange={setStatus}
|
|
28
|
+
* options={[
|
|
29
|
+
* { value: "active", label: "Active" },
|
|
30
|
+
* { value: "inactive", label: "Inactive" },
|
|
31
|
+
* ]}
|
|
32
|
+
* />
|
|
33
|
+
*
|
|
34
|
+
* // Multi-select with dynamic loading
|
|
35
|
+
* <SelectInput
|
|
36
|
+
* value={selectedTags}
|
|
37
|
+
* onChange={setSelectedTags}
|
|
38
|
+
* multiple
|
|
39
|
+
* loadOptions={async (search) => {
|
|
40
|
+
* const res = await fetch(`/api/tags?search=${search}`)
|
|
41
|
+
* return res.json()
|
|
42
|
+
* }}
|
|
43
|
+
* />
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
function SelectInput({ value, onChange, options: staticOptions = [], loadOptions, multiple = false, clearable = true, maxSelections, debounceMs = 300, loading: externalLoading = false, emptyMessage = "No options found", placeholder = "Select...", disabled, className, id, "aria-invalid": ariaInvalid }) {
|
|
47
|
+
const [search, setSearch] = useState("");
|
|
48
|
+
const [dynamicOptions, setDynamicOptions] = useState([]);
|
|
49
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
50
|
+
const debounceRef = useRef(null);
|
|
51
|
+
const anchorRef = useComboboxAnchor();
|
|
52
|
+
const flatStaticOptions = useMemo(() => flattenOptions(staticOptions), [staticOptions]);
|
|
53
|
+
const allOptions = useMemo(() => {
|
|
54
|
+
if (loadOptions) return dynamicOptions;
|
|
55
|
+
return flatStaticOptions;
|
|
56
|
+
}, [
|
|
57
|
+
loadOptions,
|
|
58
|
+
dynamicOptions,
|
|
59
|
+
flatStaticOptions
|
|
60
|
+
]);
|
|
61
|
+
const filteredOptions = useMemo(() => {
|
|
62
|
+
if (loadOptions) return allOptions;
|
|
63
|
+
if (!search) return allOptions;
|
|
64
|
+
return allOptions.filter((opt) => opt.label.toLowerCase().includes(search.toLowerCase()));
|
|
65
|
+
}, [
|
|
66
|
+
allOptions,
|
|
67
|
+
search,
|
|
68
|
+
loadOptions
|
|
69
|
+
]);
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (!loadOptions) return;
|
|
72
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
73
|
+
debounceRef.current = setTimeout(async () => {
|
|
74
|
+
setIsLoading(true);
|
|
75
|
+
try {
|
|
76
|
+
setDynamicOptions(await loadOptions(search));
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error("Failed to load options:", error);
|
|
79
|
+
setDynamicOptions([]);
|
|
80
|
+
} finally {
|
|
81
|
+
setIsLoading(false);
|
|
82
|
+
}
|
|
83
|
+
}, debounceMs);
|
|
84
|
+
return () => {
|
|
85
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
86
|
+
};
|
|
87
|
+
}, [
|
|
88
|
+
search,
|
|
89
|
+
loadOptions,
|
|
90
|
+
debounceMs
|
|
91
|
+
]);
|
|
92
|
+
const getLabel = useCallback((val) => {
|
|
93
|
+
return allOptions.find((opt) => opt.value === val)?.label ?? String(val);
|
|
94
|
+
}, [allOptions]);
|
|
95
|
+
const selectedValues = useMemo(() => {
|
|
96
|
+
if (value === null || value === void 0) return [];
|
|
97
|
+
return Array.isArray(value) ? value : [value];
|
|
98
|
+
}, [value]);
|
|
99
|
+
const handleMultiSelect = useCallback((selectedValue) => {
|
|
100
|
+
const currentValues = selectedValues;
|
|
101
|
+
if (currentValues.includes(selectedValue)) onChange(currentValues.filter((v) => v !== selectedValue));
|
|
102
|
+
else {
|
|
103
|
+
if (maxSelections && currentValues.length >= maxSelections) return;
|
|
104
|
+
onChange([...currentValues, selectedValue]);
|
|
105
|
+
}
|
|
106
|
+
}, [
|
|
107
|
+
selectedValues,
|
|
108
|
+
onChange,
|
|
109
|
+
maxSelections
|
|
110
|
+
]);
|
|
111
|
+
useCallback((removedValue) => {
|
|
112
|
+
onChange(selectedValues.filter((v) => v !== removedValue));
|
|
113
|
+
}, [selectedValues, onChange]);
|
|
114
|
+
const showLoading = isLoading || externalLoading;
|
|
115
|
+
if (!multiple && !loadOptions) return /* @__PURE__ */ jsxs(Select, {
|
|
116
|
+
value: selectedValues[0] ?? "",
|
|
117
|
+
onValueChange: (val) => onChange(val),
|
|
118
|
+
disabled,
|
|
119
|
+
children: [/* @__PURE__ */ jsx(SelectTrigger, {
|
|
120
|
+
id,
|
|
121
|
+
className: cn("w-full", className),
|
|
122
|
+
"aria-invalid": ariaInvalid,
|
|
123
|
+
children: /* @__PURE__ */ jsx(SelectValue, { children: selectedValues[0] ? getLabel(selectedValues[0]) : placeholder })
|
|
124
|
+
}), /* @__PURE__ */ jsx(SelectContent, { children: filteredOptions.map((option) => /* @__PURE__ */ jsxs(SelectItem, {
|
|
125
|
+
value: String(option.value),
|
|
126
|
+
disabled: option.disabled,
|
|
127
|
+
children: [option.icon, option.label]
|
|
128
|
+
}, String(option.value))) })]
|
|
129
|
+
});
|
|
130
|
+
return /* @__PURE__ */ jsxs(Combobox, {
|
|
131
|
+
value: multiple ? void 0 : selectedValues[0] ?? null,
|
|
132
|
+
onValueChange: (val) => {
|
|
133
|
+
if (val !== null && val !== void 0) if (multiple) handleMultiSelect(val);
|
|
134
|
+
else onChange(val);
|
|
135
|
+
},
|
|
136
|
+
disabled,
|
|
137
|
+
children: [multiple ? /* @__PURE__ */ jsxs(ComboboxChips, {
|
|
138
|
+
ref: anchorRef,
|
|
139
|
+
className: cn(className),
|
|
140
|
+
"aria-invalid": ariaInvalid,
|
|
141
|
+
children: [selectedValues.map((val) => /* @__PURE__ */ jsx(ComboboxChip, { children: getLabel(val) }, String(val))), /* @__PURE__ */ jsx(ComboboxChipsInput, {
|
|
142
|
+
placeholder: selectedValues.length === 0 ? placeholder : void 0,
|
|
143
|
+
value: search,
|
|
144
|
+
onChange: (e) => setSearch(e.target.value)
|
|
145
|
+
})]
|
|
146
|
+
}) : /* @__PURE__ */ jsx(ComboboxInput, {
|
|
147
|
+
id,
|
|
148
|
+
placeholder,
|
|
149
|
+
value: search,
|
|
150
|
+
onChange: (e) => setSearch(e.target.value),
|
|
151
|
+
className: cn(className),
|
|
152
|
+
"aria-invalid": ariaInvalid,
|
|
153
|
+
showClear: clearable && selectedValues.length > 0
|
|
154
|
+
}), /* @__PURE__ */ jsxs(ComboboxContent, {
|
|
155
|
+
anchor: multiple ? anchorRef : void 0,
|
|
156
|
+
children: [
|
|
157
|
+
showLoading && /* @__PURE__ */ jsx("div", {
|
|
158
|
+
className: "flex items-center justify-center py-4",
|
|
159
|
+
children: /* @__PURE__ */ jsx(CircleNotch, { className: "size-4 animate-spin text-muted-foreground" })
|
|
160
|
+
}),
|
|
161
|
+
/* @__PURE__ */ jsx(ComboboxList, { children: filteredOptions.map((option) => /* @__PURE__ */ jsxs(ComboboxItem, {
|
|
162
|
+
value: option.value,
|
|
163
|
+
disabled: option.disabled,
|
|
164
|
+
children: [
|
|
165
|
+
option.icon,
|
|
166
|
+
option.label,
|
|
167
|
+
multiple && selectedValues.includes(option.value) && /* @__PURE__ */ jsx(Check, { className: "ml-auto size-3.5" })
|
|
168
|
+
]
|
|
169
|
+
}, String(option.value))) }),
|
|
170
|
+
/* @__PURE__ */ jsx(ComboboxEmpty, { children: emptyMessage })
|
|
171
|
+
]
|
|
172
|
+
})]
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
//#endregion
|
|
177
|
+
export { SelectInput };
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useState } from "react";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
import { Badge } from "../ui/badge";
|
|
6
|
+
import { cn } from "../../lib/utils";
|
|
7
|
+
import { X } from "@phosphor-icons/react";
|
|
8
|
+
|
|
9
|
+
//#region src/components/primitives/tag-input.tsx
|
|
10
|
+
/**
|
|
11
|
+
* Tag Input Primitive
|
|
12
|
+
*
|
|
13
|
+
* An input for creating and managing tags/chips.
|
|
14
|
+
* Supports validation patterns, max tags, and autocomplete suggestions.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```tsx
|
|
18
|
+
* // Basic usage
|
|
19
|
+
* <TagInput
|
|
20
|
+
* value={tags}
|
|
21
|
+
* onChange={setTags}
|
|
22
|
+
* placeholder="Add tags..."
|
|
23
|
+
* />
|
|
24
|
+
*
|
|
25
|
+
* // With suggestions and validation
|
|
26
|
+
* <TagInput
|
|
27
|
+
* value={emails}
|
|
28
|
+
* onChange={setEmails}
|
|
29
|
+
* suggestions={["user@example.com", "admin@example.com"]}
|
|
30
|
+
* pattern={/^[^\s@]+@[^\s@]+\.[^\s@]+$/}
|
|
31
|
+
* maxTags={5}
|
|
32
|
+
* />
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
function TagInput({ value, onChange, suggestions = [], maxTags, allowDuplicates = false, pattern, placeholder = "Type and press Enter...", disabled, className, id, "aria-invalid": ariaInvalid }) {
|
|
36
|
+
const [inputValue, setInputValue] = useState("");
|
|
37
|
+
const [showSuggestions, setShowSuggestions] = useState(false);
|
|
38
|
+
const addTag = useCallback((tag) => {
|
|
39
|
+
const trimmed = tag.trim();
|
|
40
|
+
if (!trimmed) return;
|
|
41
|
+
if (maxTags && value.length >= maxTags) return;
|
|
42
|
+
if (!allowDuplicates && value.includes(trimmed)) return;
|
|
43
|
+
if (pattern && !pattern.test(trimmed)) return;
|
|
44
|
+
onChange([...value, trimmed]);
|
|
45
|
+
setInputValue("");
|
|
46
|
+
setShowSuggestions(false);
|
|
47
|
+
}, [
|
|
48
|
+
value,
|
|
49
|
+
onChange,
|
|
50
|
+
maxTags,
|
|
51
|
+
allowDuplicates,
|
|
52
|
+
pattern
|
|
53
|
+
]);
|
|
54
|
+
const removeTag = useCallback((index) => {
|
|
55
|
+
if (disabled) return;
|
|
56
|
+
onChange(value.filter((_, i) => i !== index));
|
|
57
|
+
}, [
|
|
58
|
+
value,
|
|
59
|
+
onChange,
|
|
60
|
+
disabled
|
|
61
|
+
]);
|
|
62
|
+
const handleKeyDown = useCallback((e) => {
|
|
63
|
+
if (e.key === "Enter") {
|
|
64
|
+
e.preventDefault();
|
|
65
|
+
addTag(inputValue);
|
|
66
|
+
} else if (e.key === "Backspace" && !inputValue && value.length > 0) removeTag(value.length - 1);
|
|
67
|
+
else if (e.key === "Escape") setShowSuggestions(false);
|
|
68
|
+
}, [
|
|
69
|
+
inputValue,
|
|
70
|
+
value,
|
|
71
|
+
addTag,
|
|
72
|
+
removeTag
|
|
73
|
+
]);
|
|
74
|
+
const handleInputChange = useCallback((e) => {
|
|
75
|
+
const newValue = e.target.value;
|
|
76
|
+
setInputValue(newValue);
|
|
77
|
+
setShowSuggestions(newValue.length > 0 && suggestions.length > 0);
|
|
78
|
+
}, [suggestions]);
|
|
79
|
+
const filteredSuggestions = suggestions.filter((suggestion) => suggestion.toLowerCase().includes(inputValue.toLowerCase()) && (allowDuplicates || !value.includes(suggestion)));
|
|
80
|
+
const canAddMore = !maxTags || value.length < maxTags;
|
|
81
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
82
|
+
className: cn("relative", className),
|
|
83
|
+
children: [
|
|
84
|
+
/* @__PURE__ */ jsxs("div", {
|
|
85
|
+
className: cn("flex flex-wrap gap-1.5 min-h-9 w-full border border-input bg-background px-3 py-2", "focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2", ariaInvalid && "border-destructive ring-destructive/20", disabled && "cursor-not-allowed opacity-50"),
|
|
86
|
+
children: [value.map((tag, index) => /* @__PURE__ */ jsxs(Badge, {
|
|
87
|
+
variant: "secondary",
|
|
88
|
+
className: "gap-1 pr-1",
|
|
89
|
+
children: [tag, !disabled && /* @__PURE__ */ jsx("button", {
|
|
90
|
+
type: "button",
|
|
91
|
+
onClick: () => removeTag(index),
|
|
92
|
+
className: "ml-1 rounded-full p-0.5 hover:bg-muted-foreground/20",
|
|
93
|
+
"aria-label": `Remove ${tag}`,
|
|
94
|
+
children: /* @__PURE__ */ jsx(X, { className: "size-3" })
|
|
95
|
+
})]
|
|
96
|
+
}, `${tag}-${index}`)), canAddMore && /* @__PURE__ */ jsx("input", {
|
|
97
|
+
id,
|
|
98
|
+
type: "text",
|
|
99
|
+
value: inputValue,
|
|
100
|
+
onChange: handleInputChange,
|
|
101
|
+
onKeyDown: handleKeyDown,
|
|
102
|
+
onFocus: () => setShowSuggestions(inputValue.length > 0 && suggestions.length > 0),
|
|
103
|
+
onBlur: () => {
|
|
104
|
+
setTimeout(() => setShowSuggestions(false), 150);
|
|
105
|
+
},
|
|
106
|
+
placeholder: value.length === 0 ? placeholder : void 0,
|
|
107
|
+
disabled,
|
|
108
|
+
"aria-invalid": ariaInvalid,
|
|
109
|
+
className: cn("flex-1 min-w-[120px] bg-transparent text-sm outline-none placeholder:text-muted-foreground", "disabled:cursor-not-allowed")
|
|
110
|
+
})]
|
|
111
|
+
}),
|
|
112
|
+
showSuggestions && filteredSuggestions.length > 0 && /* @__PURE__ */ jsx("div", {
|
|
113
|
+
className: cn("absolute z-50 mt-1 w-full border border-input bg-popover shadow-md", "max-h-[200px] overflow-auto"),
|
|
114
|
+
children: filteredSuggestions.map((suggestion, index) => /* @__PURE__ */ jsx("button", {
|
|
115
|
+
type: "button",
|
|
116
|
+
onClick: () => addTag(suggestion),
|
|
117
|
+
className: cn("w-full px-3 py-2 text-left text-sm", "hover:bg-accent hover:text-accent-foreground", "focus:bg-accent focus:text-accent-foreground focus:outline-none"),
|
|
118
|
+
children: suggestion
|
|
119
|
+
}, suggestion))
|
|
120
|
+
}),
|
|
121
|
+
maxTags && /* @__PURE__ */ jsxs("p", {
|
|
122
|
+
className: "mt-1 text-xs text-muted-foreground",
|
|
123
|
+
children: [
|
|
124
|
+
value.length,
|
|
125
|
+
" / ",
|
|
126
|
+
maxTags,
|
|
127
|
+
" tags"
|
|
128
|
+
]
|
|
129
|
+
})
|
|
130
|
+
]
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
//#endregion
|
|
135
|
+
export { TagInput };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Input } from "../ui/input";
|
|
3
|
+
import { cn } from "../../lib/utils";
|
|
4
|
+
|
|
5
|
+
//#region src/components/primitives/text-input.tsx
|
|
6
|
+
/**
|
|
7
|
+
* Text Input Primitive
|
|
8
|
+
*
|
|
9
|
+
* A basic text input with value/onChange pattern.
|
|
10
|
+
* Supports different input types: text, email, password, url, tel, search.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* <TextInput
|
|
15
|
+
* value={email}
|
|
16
|
+
* onChange={setEmail}
|
|
17
|
+
* type="email"
|
|
18
|
+
* placeholder="Enter email"
|
|
19
|
+
* />
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
function TextInput({ value, onChange, type = "text", placeholder, disabled, readOnly, maxLength, autoComplete, className, id, "aria-invalid": ariaInvalid }) {
|
|
23
|
+
return /* @__PURE__ */ jsx(Input, {
|
|
24
|
+
id,
|
|
25
|
+
type,
|
|
26
|
+
value,
|
|
27
|
+
onChange: (e) => onChange(e.target.value),
|
|
28
|
+
placeholder,
|
|
29
|
+
disabled,
|
|
30
|
+
readOnly,
|
|
31
|
+
maxLength,
|
|
32
|
+
autoComplete,
|
|
33
|
+
"aria-invalid": ariaInvalid,
|
|
34
|
+
className: cn(className)
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
//#endregion
|
|
39
|
+
export { TextInput };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Textarea } from "../ui/textarea";
|
|
3
|
+
import { cn } from "../../lib/utils";
|
|
4
|
+
|
|
5
|
+
//#region src/components/primitives/textarea-input.tsx
|
|
6
|
+
/**
|
|
7
|
+
* Textarea Input Primitive
|
|
8
|
+
*
|
|
9
|
+
* A multi-line text input.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* <TextareaInput
|
|
14
|
+
* value={description}
|
|
15
|
+
* onChange={setDescription}
|
|
16
|
+
* rows={4}
|
|
17
|
+
* placeholder="Enter description..."
|
|
18
|
+
* />
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
function TextareaInput({ value, onChange, rows = 3, maxLength, placeholder, disabled, readOnly, className, id, "aria-invalid": ariaInvalid }) {
|
|
22
|
+
return /* @__PURE__ */ jsx(Textarea, {
|
|
23
|
+
id,
|
|
24
|
+
value,
|
|
25
|
+
onChange: (e) => onChange(e.target.value),
|
|
26
|
+
rows,
|
|
27
|
+
maxLength,
|
|
28
|
+
placeholder,
|
|
29
|
+
disabled,
|
|
30
|
+
readOnly,
|
|
31
|
+
"aria-invalid": ariaInvalid,
|
|
32
|
+
className: cn(className)
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
//#endregion
|
|
37
|
+
export { TextareaInput };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Switch } from "../ui/switch";
|
|
3
|
+
import { cn } from "../../lib/utils";
|
|
4
|
+
|
|
5
|
+
//#region src/components/primitives/toggle-input.tsx
|
|
6
|
+
/**
|
|
7
|
+
* Toggle Input Primitive
|
|
8
|
+
*
|
|
9
|
+
* A switch/toggle for boolean values.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* <ToggleInput
|
|
14
|
+
* value={isActive}
|
|
15
|
+
* onChange={setIsActive}
|
|
16
|
+
* />
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
function ToggleInput({ value, onChange, disabled, className, id, "aria-invalid": ariaInvalid }) {
|
|
20
|
+
return /* @__PURE__ */ jsx(Switch, {
|
|
21
|
+
id,
|
|
22
|
+
checked: value,
|
|
23
|
+
onCheckedChange: onChange,
|
|
24
|
+
disabled,
|
|
25
|
+
"aria-invalid": ariaInvalid,
|
|
26
|
+
className: cn(className)
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
//#endregion
|
|
31
|
+
export { ToggleInput };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/components/primitives/types.ts
|
|
2
|
+
/** Check if options are grouped */
|
|
3
|
+
function isOptionGroup(option) {
|
|
4
|
+
return "options" in option;
|
|
5
|
+
}
|
|
6
|
+
/** Flatten grouped options */
|
|
7
|
+
function flattenOptions(options) {
|
|
8
|
+
return options.flatMap((opt) => isOptionGroup(opt) ? opt.options : [opt]);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
//#endregion
|
|
12
|
+
export { flattenOptions, isOptionGroup };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../../lib/utils";
|
|
3
|
+
import { CaretDownIcon, CaretUpIcon } from "@phosphor-icons/react";
|
|
4
|
+
import { Accordion as Accordion$1 } from "@base-ui/react/accordion";
|
|
5
|
+
|
|
6
|
+
//#region src/components/ui/accordion.tsx
|
|
7
|
+
function Accordion({ className, ...props }) {
|
|
8
|
+
return /* @__PURE__ */ jsx(Accordion$1.Root, {
|
|
9
|
+
"data-slot": "accordion",
|
|
10
|
+
className: cn("overflow-hidden rounded-md border flex w-full flex-col", className),
|
|
11
|
+
...props
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
function AccordionItem({ className, ...props }) {
|
|
15
|
+
return /* @__PURE__ */ jsx(Accordion$1.Item, {
|
|
16
|
+
"data-slot": "accordion-item",
|
|
17
|
+
className: cn("data-open:bg-muted/50 not-last:border-b", className),
|
|
18
|
+
...props
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function AccordionTrigger({ className, children, ...props }) {
|
|
22
|
+
return /* @__PURE__ */ jsx(Accordion$1.Header, {
|
|
23
|
+
className: "flex",
|
|
24
|
+
children: /* @__PURE__ */ jsxs(Accordion$1.Trigger, {
|
|
25
|
+
"data-slot": "accordion-trigger",
|
|
26
|
+
className: cn("**:data-[slot=accordion-trigger-icon]:text-muted-foreground gap-6 p-2 text-left text-xs/relaxed font-medium hover:underline **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4 group/accordion-trigger relative flex flex-1 items-start justify-between border border-transparent transition-all outline-none disabled:pointer-events-none disabled:opacity-50", className),
|
|
27
|
+
...props,
|
|
28
|
+
children: [
|
|
29
|
+
children,
|
|
30
|
+
/* @__PURE__ */ jsx(CaretDownIcon, {
|
|
31
|
+
"data-slot": "accordion-trigger-icon",
|
|
32
|
+
className: "pointer-events-none shrink-0 group-aria-expanded/accordion-trigger:hidden"
|
|
33
|
+
}),
|
|
34
|
+
/* @__PURE__ */ jsx(CaretUpIcon, {
|
|
35
|
+
"data-slot": "accordion-trigger-icon",
|
|
36
|
+
className: "pointer-events-none hidden shrink-0 group-aria-expanded/accordion-trigger:inline"
|
|
37
|
+
})
|
|
38
|
+
]
|
|
39
|
+
})
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function AccordionContent({ className, children, ...props }) {
|
|
43
|
+
return /* @__PURE__ */ jsx(Accordion$1.Panel, {
|
|
44
|
+
"data-slot": "accordion-content",
|
|
45
|
+
className: "data-open:animate-accordion-down data-closed:animate-accordion-up px-2 text-xs/relaxed overflow-hidden",
|
|
46
|
+
...props,
|
|
47
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
48
|
+
className: cn("pt-0 pb-4 [&_a]:hover:text-foreground h-(--accordion-panel-height) data-ending-style:h-0 data-starting-style:h-0 [&_a]:underline [&_a]:underline-offset-3 [&_p:not(:last-child)]:mb-4", className),
|
|
49
|
+
children
|
|
50
|
+
})
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
//#endregion
|
|
55
|
+
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import "react";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
import { cn } from "../../lib/utils";
|
|
6
|
+
import { Avatar as Avatar$1 } from "@base-ui/react/avatar";
|
|
7
|
+
|
|
8
|
+
//#region src/components/ui/avatar.tsx
|
|
9
|
+
function Avatar({ className, size = "default", ...props }) {
|
|
10
|
+
return /* @__PURE__ */ jsx(Avatar$1.Root, {
|
|
11
|
+
"data-slot": "avatar",
|
|
12
|
+
"data-size": size,
|
|
13
|
+
className: cn("size-8 rounded-full after:rounded-full data-[size=lg]:size-10 data-[size=sm]:size-6 after:border-border group/avatar relative flex shrink-0 select-none after:absolute after:inset-0 after:border after:mix-blend-darken dark:after:mix-blend-lighten", className),
|
|
14
|
+
...props
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
function AvatarImage({ className, ...props }) {
|
|
18
|
+
return /* @__PURE__ */ jsx(Avatar$1.Image, {
|
|
19
|
+
"data-slot": "avatar-image",
|
|
20
|
+
className: cn("rounded-full aspect-square size-full object-cover", className),
|
|
21
|
+
...props
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function AvatarFallback({ className, ...props }) {
|
|
25
|
+
return /* @__PURE__ */ jsx(Avatar$1.Fallback, {
|
|
26
|
+
"data-slot": "avatar-fallback",
|
|
27
|
+
className: cn("bg-muted text-muted-foreground rounded-full flex size-full items-center justify-center text-sm group-data-[size=sm]/avatar:text-xs", className),
|
|
28
|
+
...props
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function AvatarBadge({ className, ...props }) {
|
|
32
|
+
return /* @__PURE__ */ jsx("span", {
|
|
33
|
+
"data-slot": "avatar-badge",
|
|
34
|
+
className: cn("bg-primary text-primary-foreground ring-background absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-blend-color ring-2 select-none", "group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden", "group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2", "group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2", className),
|
|
35
|
+
...props
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function AvatarGroup({ className, ...props }) {
|
|
39
|
+
return /* @__PURE__ */ jsx("div", {
|
|
40
|
+
"data-slot": "avatar-group",
|
|
41
|
+
className: cn("*:data-[slot=avatar]:ring-background group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2", className),
|
|
42
|
+
...props
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function AvatarGroupCount({ className, ...props }) {
|
|
46
|
+
return /* @__PURE__ */ jsx("div", {
|
|
47
|
+
"data-slot": "avatar-group-count",
|
|
48
|
+
className: cn("bg-muted text-muted-foreground size-8 rounded-full text-xs/relaxed group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3 ring-background relative flex shrink-0 items-center justify-center ring-2", className),
|
|
49
|
+
...props
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
//#endregion
|
|
54
|
+
export { Avatar, AvatarBadge, AvatarFallback, AvatarGroup, AvatarGroupCount, AvatarImage };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { cn } from "../../lib/utils";
|
|
2
|
+
import { mergeProps } from "@base-ui/react/merge-props";
|
|
3
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
4
|
+
import { cva } from "class-variance-authority";
|
|
5
|
+
|
|
6
|
+
//#region src/components/ui/badge.tsx
|
|
7
|
+
const badgeVariants = cva("h-5 gap-1 rounded-full border border-transparent px-2 py-0.5 text-[0.625rem] font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-2.5! inline-flex items-center justify-center w-fit whitespace-nowrap shrink-0 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-colors overflow-hidden group/badge", {
|
|
8
|
+
variants: { variant: {
|
|
9
|
+
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
|
10
|
+
secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
|
11
|
+
destructive: "bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20",
|
|
12
|
+
outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground bg-input/20 dark:bg-input/30",
|
|
13
|
+
ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
|
|
14
|
+
link: "text-primary underline-offset-4 hover:underline"
|
|
15
|
+
} },
|
|
16
|
+
defaultVariants: { variant: "default" }
|
|
17
|
+
});
|
|
18
|
+
function Badge({ className, variant = "default", render, ...props }) {
|
|
19
|
+
return useRender({
|
|
20
|
+
defaultTagName: "span",
|
|
21
|
+
props: mergeProps({ className: cn(badgeVariants({
|
|
22
|
+
className,
|
|
23
|
+
variant
|
|
24
|
+
})) }, props),
|
|
25
|
+
render,
|
|
26
|
+
state: {
|
|
27
|
+
slot: "badge",
|
|
28
|
+
variant
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
//#endregion
|
|
34
|
+
export { Badge, badgeVariants };
|