@craftzbay/ui 0.8.1 → 0.10.0
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/README.md +72 -6
- package/dist-lib/components/ui/Accordion.js +60 -0
- package/dist-lib/components/ui/Accordion.js.map +1 -0
- package/dist-lib/components/ui/Alert.d.ts +1 -1
- package/dist-lib/components/ui/Alert.js +66 -0
- package/dist-lib/components/ui/Alert.js.map +1 -0
- package/dist-lib/components/ui/Avatar.d.ts +2 -2
- package/dist-lib/components/ui/Avatar.js +96 -0
- package/dist-lib/components/ui/Avatar.js.map +1 -0
- package/dist-lib/components/ui/Badge.d.ts +1 -1
- package/dist-lib/components/ui/Badge.js +67 -0
- package/dist-lib/components/ui/Badge.js.map +1 -0
- package/dist-lib/components/ui/Breadcrumbs.js +34 -0
- package/dist-lib/components/ui/Breadcrumbs.js.map +1 -0
- package/dist-lib/components/ui/Button.d.ts +1 -1
- package/dist-lib/components/ui/Button.js +117 -0
- package/dist-lib/components/ui/Button.js.map +1 -0
- package/dist-lib/components/ui/Calendar.d.ts +7 -1
- package/dist-lib/components/ui/Calendar.js +111 -0
- package/dist-lib/components/ui/Calendar.js.map +1 -0
- package/dist-lib/components/ui/Card.d.ts +1 -1
- package/dist-lib/components/ui/Card.js +116 -0
- package/dist-lib/components/ui/Card.js.map +1 -0
- package/dist-lib/components/ui/Carousel.d.ts +5 -1
- package/dist-lib/components/ui/Carousel.js +143 -0
- package/dist-lib/components/ui/Carousel.js.map +1 -0
- package/dist-lib/components/ui/Chart.d.ts +26 -4
- package/dist-lib/components/ui/Chart.js +253 -0
- package/dist-lib/components/ui/Chart.js.map +1 -0
- package/dist-lib/components/ui/Checkbox.js +56 -0
- package/dist-lib/components/ui/Checkbox.js.map +1 -0
- package/dist-lib/components/ui/Combobox.d.ts +7 -0
- package/dist-lib/components/ui/Combobox.js +164 -0
- package/dist-lib/components/ui/Combobox.js.map +1 -0
- package/dist-lib/components/ui/CommandPalette.d.ts +3 -3
- package/dist-lib/components/ui/CommandPalette.js +174 -0
- package/dist-lib/components/ui/CommandPalette.js.map +1 -0
- package/dist-lib/components/ui/ContextMenu.js +150 -0
- package/dist-lib/components/ui/ContextMenu.js.map +1 -0
- package/dist-lib/components/ui/DataGrid.js +84 -0
- package/dist-lib/components/ui/DataGrid.js.map +1 -0
- package/dist-lib/components/ui/DatePicker.d.ts +16 -13
- package/dist-lib/components/ui/DatePicker.js +174 -0
- package/dist-lib/components/ui/DatePicker.js.map +1 -0
- package/dist-lib/components/ui/DesignSystemProvider.d.ts +8 -1
- package/dist-lib/components/ui/DesignSystemProvider.js +79 -0
- package/dist-lib/components/ui/DesignSystemProvider.js.map +1 -0
- package/dist-lib/components/ui/Dialog.d.ts +1 -1
- package/dist-lib/components/ui/Dialog.js +140 -0
- package/dist-lib/components/ui/Dialog.js.map +1 -0
- package/dist-lib/components/ui/Drawer.d.ts +6 -0
- package/dist-lib/components/ui/Drawer.js +98 -0
- package/dist-lib/components/ui/Drawer.js.map +1 -0
- package/dist-lib/components/ui/DropdownMenu.js +164 -0
- package/dist-lib/components/ui/DropdownMenu.js.map +1 -0
- package/dist-lib/components/ui/EmptyState.js +32 -0
- package/dist-lib/components/ui/EmptyState.js.map +1 -0
- package/dist-lib/components/ui/ErrorState.js +48 -0
- package/dist-lib/components/ui/ErrorState.js.map +1 -0
- package/dist-lib/components/ui/FileUpload.d.ts +3 -0
- package/dist-lib/components/ui/FileUpload.js +108 -0
- package/dist-lib/components/ui/FileUpload.js.map +1 -0
- package/dist-lib/components/ui/Form.d.ts +1 -0
- package/dist-lib/components/ui/Form.js +105 -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 +26 -0
- package/dist-lib/components/ui/Icon.js.map +1 -0
- package/dist-lib/components/ui/IconButton.d.ts +1 -1
- package/dist-lib/components/ui/IconButton.js +56 -0
- package/dist-lib/components/ui/IconButton.js.map +1 -0
- package/dist-lib/components/ui/Input.d.ts +6 -3
- package/dist-lib/components/ui/Input.js +135 -0
- package/dist-lib/components/ui/Input.js.map +1 -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 +3 -1
- package/dist-lib/components/ui/MultiSelect.js +196 -0
- package/dist-lib/components/ui/MultiSelect.js.map +1 -0
- package/dist-lib/components/ui/Pagination.d.ts +23 -0
- package/dist-lib/components/ui/Pagination.js +148 -0
- package/dist-lib/components/ui/Pagination.js.map +1 -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.js +95 -0
- package/dist-lib/components/ui/Progress.js.map +1 -0
- package/dist-lib/components/ui/RadioGroup.js +64 -0
- package/dist-lib/components/ui/RadioGroup.js.map +1 -0
- package/dist-lib/components/ui/ScrollArea.js +44 -0
- package/dist-lib/components/ui/ScrollArea.js.map +1 -0
- package/dist-lib/components/ui/Select.js +131 -0
- package/dist-lib/components/ui/Select.js.map +1 -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 +1 -1
- package/dist-lib/components/ui/Sheet.js +113 -0
- package/dist-lib/components/ui/Sheet.js.map +1 -0
- package/dist-lib/components/ui/Sidebar.d.ts +18 -6
- package/dist-lib/components/ui/Sidebar.js +169 -0
- package/dist-lib/components/ui/Sidebar.js.map +1 -0
- package/dist-lib/components/ui/Skeleton.js +28 -0
- package/dist-lib/components/ui/Skeleton.js.map +1 -0
- package/dist-lib/components/ui/Slider.js +52 -0
- package/dist-lib/components/ui/Slider.js.map +1 -0
- package/dist-lib/components/ui/Snackbar.d.ts +1 -1
- package/dist-lib/components/ui/Snackbar.js +56 -0
- package/dist-lib/components/ui/Snackbar.js.map +1 -0
- package/dist-lib/components/ui/Spinner.js +42 -0
- package/dist-lib/components/ui/Spinner.js.map +1 -0
- package/dist-lib/components/ui/Stepper.js +88 -0
- package/dist-lib/components/ui/Stepper.js.map +1 -0
- package/dist-lib/components/ui/Switch.js +77 -0
- package/dist-lib/components/ui/Switch.js.map +1 -0
- package/dist-lib/components/ui/Table.d.ts +9 -1
- package/dist-lib/components/ui/Table.js +157 -0
- package/dist-lib/components/ui/Table.js.map +1 -0
- package/dist-lib/components/ui/Tabs.d.ts +1 -1
- package/dist-lib/components/ui/Tabs.js +89 -0
- package/dist-lib/components/ui/Tabs.js.map +1 -0
- package/dist-lib/components/ui/TagInput.d.ts +17 -2
- 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.js +77 -0
- package/dist-lib/components/ui/Textarea.js.map +1 -0
- package/dist-lib/components/ui/Timeline.d.ts +16 -6
- package/dist-lib/components/ui/Timeline.js +52 -0
- package/dist-lib/components/ui/Timeline.js.map +1 -0
- package/dist-lib/components/ui/Toast.d.ts +1 -1
- package/dist-lib/components/ui/Toast.js +171 -0
- package/dist-lib/components/ui/Toast.js.map +1 -0
- package/dist-lib/components/ui/Tooltip.js +58 -0
- package/dist-lib/components/ui/Tooltip.js.map +1 -0
- package/dist-lib/components/ui/TopNav.js +52 -0
- package/dist-lib/components/ui/TopNav.js.map +1 -0
- package/dist-lib/components/ui/Tree.d.ts +27 -5
- package/dist-lib/components/ui/Tree.js +167 -0
- package/dist-lib/components/ui/Tree.js.map +1 -0
- package/dist-lib/hooks/use-field-ids.d.ts +29 -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.js +18 -0
- package/dist-lib/hooks/use-media-query.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.js +36 -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 +71 -11
- package/dist-lib/icons/index.js +145 -0
- package/dist-lib/icons/index.js.map +1 -0
- package/dist-lib/illustrations/index.js +105 -0
- package/dist-lib/illustrations/index.js.map +1 -0
- package/dist-lib/index.d.ts +62 -58
- package/dist-lib/index.js +268 -4386
- package/dist-lib/index.js.map +1 -1
- package/dist-lib/lib/strings.d.ts +129 -0
- package/dist-lib/lib/strings.js +85 -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 +70 -0
- package/dist-lib/lib/strings.mn.js.map +1 -0
- package/dist-lib/lib/utils.d.ts +0 -1
- package/dist-lib/lib/utils.js +10 -0
- package/dist-lib/lib/utils.js.map +1 -0
- package/dist-lib/styles.css +1 -1
- package/dist-lib/theme.css +777 -0
- package/package.json +31 -8
- package/dist-lib/index.cjs +0 -8
- package/dist-lib/index.cjs.map +0 -1
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as n } from "react/jsx-runtime";
|
|
3
|
+
import { useMemo as a, Suspense as l, lazy as d } from "react";
|
|
4
|
+
import i from "lucide-react/dynamicIconImports";
|
|
5
|
+
const p = Object.keys(i), o = /* @__PURE__ */ new Map();
|
|
6
|
+
function u(e) {
|
|
7
|
+
const t = o.get(e);
|
|
8
|
+
if (t) return t;
|
|
9
|
+
const c = d(i[e]);
|
|
10
|
+
return o.set(e, c), c;
|
|
11
|
+
}
|
|
12
|
+
function y({ name: e, fallback: t, ...c }) {
|
|
13
|
+
const s = a(() => u(e), [e]), r = t ?? /* @__PURE__ */ n(
|
|
14
|
+
"span",
|
|
15
|
+
{
|
|
16
|
+
"aria-hidden": !0,
|
|
17
|
+
style: { display: "inline-block", width: c.size ?? "1em", height: c.size ?? "1em" }
|
|
18
|
+
}
|
|
19
|
+
);
|
|
20
|
+
return /* @__PURE__ */ n(l, { fallback: r, children: /* @__PURE__ */ n(s, { ...c }) });
|
|
21
|
+
}
|
|
22
|
+
export {
|
|
23
|
+
y as Icon,
|
|
24
|
+
p as iconNames
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=Icon.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Icon.js","sources":["../../../src/components/ui/Icon.tsx"],"sourcesContent":["'use client';\n\nimport {\n lazy,\n Suspense,\n useMemo,\n type LazyExoticComponent,\n type ReactNode,\n} 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>>();\n\nfunction getIcon(name: IconName): LazyExoticComponent<LucideIcon> {\n const cached = cache.get(name);\n if (cached) return cached;\n const created = lazy(dynamicIconImports[name]);\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 style={{ display: 'inline-block', width: props.size ?? '1em', height: props.size ?? '1em' }}\n />\n );\n\n return (\n <Suspense fallback={placeholder}>\n <LucideIcon {...props} />\n </Suspense>\n );\n}\n"],"names":["iconNames","dynamicIconImports","cache","getIcon","name","cached","created","lazy","Icon","fallback","props","LucideIcon","useMemo","placeholder","jsx","Suspense"],"mappings":";;;;AAgBO,MAAMA,IAAY,OAAO,KAAKC,CAAkB,GAIjDC,wBAAY,IAAA;AAElB,SAASC,EAAQC,GAAiD;AAChE,QAAMC,IAASH,EAAM,IAAIE,CAAI;AAC7B,MAAIC,EAAQ,QAAOA;AACnB,QAAMC,IAAUC,EAAKN,EAAmBG,CAAI,CAAC;AAC7C,SAAAF,EAAM,IAAIE,GAAME,CAAO,GAChBA;AACT;AA6BO,SAASE,EAAK,EAAE,MAAAJ,GAAM,UAAAK,GAAU,GAAGC,KAAoB;AAC5D,QAAMC,IAAaC,EAAQ,MAAMT,EAAQC,CAAI,GAAG,CAACA,CAAI,CAAC,GAEhDS,IAAcJ,KAClB,gBAAAK;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,eAAW;AAAA,MACX,OAAO,EAAE,SAAS,gBAAgB,OAAOJ,EAAM,QAAQ,OAAO,QAAQA,EAAM,QAAQ,MAAA;AAAA,IAAM;AAAA,EAAA;AAI9F,SACE,gBAAAI,EAACC,KAAS,UAAUF,GAClB,4BAACF,GAAA,EAAY,GAAGD,GAAO,EAAA,CACzB;AAEJ;"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ButtonHTMLAttributes, ReactNode } from 'react';
|
|
2
|
-
import { VariantProps } from '../../lib/cva';
|
|
2
|
+
import { VariantProps } from '../../lib/cva.js';
|
|
3
3
|
declare const iconButton: (props?: ({
|
|
4
4
|
variant?: "outline" | "primary" | "secondary" | "ghost" | "destructive" | null | undefined;
|
|
5
5
|
size?: "md" | "sm" | "lg" | null | undefined;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as r } from "react/jsx-runtime";
|
|
3
|
+
import { forwardRef as u } from "react";
|
|
4
|
+
import { Loader2 as b } from "../../icons/index.js";
|
|
5
|
+
import { cn as g } from "../../lib/utils.js";
|
|
6
|
+
import { cva as v } from "class-variance-authority";
|
|
7
|
+
const f = v(
|
|
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
|
+
"[&_svg]:pointer-events-none"
|
|
17
|
+
],
|
|
18
|
+
{
|
|
19
|
+
variants: {
|
|
20
|
+
variant: {
|
|
21
|
+
primary: "bg-accent text-on-accent hover:bg-accent-hover active:bg-accent-active",
|
|
22
|
+
secondary: "bg-background-muted text-foreground border border-border hover:bg-surface-hover active:bg-surface-active",
|
|
23
|
+
outline: "border border-border bg-transparent text-foreground hover:bg-background-muted",
|
|
24
|
+
ghost: "bg-transparent text-foreground-muted hover:bg-background-muted hover:text-foreground",
|
|
25
|
+
destructive: "bg-danger text-on-danger hover:bg-danger-hover active:bg-danger-active"
|
|
26
|
+
},
|
|
27
|
+
size: {
|
|
28
|
+
sm: "h-7 w-7 [&_svg]:size-3.5",
|
|
29
|
+
md: "h-8 w-8 [&_svg]:size-4",
|
|
30
|
+
lg: "h-10 w-10 [&_svg]:size-5"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
defaultVariants: {
|
|
34
|
+
variant: "ghost",
|
|
35
|
+
size: "md"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
), m = u(function({ className: t, variant: o, size: n, icon: i, loading: e, disabled: a, type: s = "button", ...d }, c) {
|
|
39
|
+
return /* @__PURE__ */ r(
|
|
40
|
+
"button",
|
|
41
|
+
{
|
|
42
|
+
ref: c,
|
|
43
|
+
type: s,
|
|
44
|
+
"aria-busy": e || void 0,
|
|
45
|
+
disabled: a || e,
|
|
46
|
+
className: g(f({ variant: o, size: n }), t),
|
|
47
|
+
...d,
|
|
48
|
+
children: e ? /* @__PURE__ */ r(b, { className: "animate-spin", "aria-hidden": "true" }) : i
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
m.displayName = "IconButton";
|
|
53
|
+
export {
|
|
54
|
+
m as IconButton
|
|
55
|
+
};
|
|
56
|
+
//# 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 } 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 '[&_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 hover:bg-surface-hover active:bg-surface-active',\n outline:\n 'border border-border bg-transparent text-foreground hover:bg-background-muted',\n ghost: '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 sm: 'h-7 w-7 [&_svg]:size-3.5',\n md: 'h-8 w-8 [&_svg]:size-4',\n lg: 'h-10 w-10 [&_svg]:size-5',\n },\n },\n defaultVariants: {\n variant: 'ghost',\n size: 'md',\n },\n },\n);\n\nexport interface IconButtonProps\n extends ButtonHTMLAttributes<HTMLButtonElement>,\n 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. */\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', ...props },\n ref,\n) {\n const isDisabled = disabled || loading;\n return (\n <button\n ref={ref}\n type={type}\n aria-busy={loading || undefined}\n disabled={isDisabled}\n className={cn(iconButton({ variant, size }), className)}\n {...props}\n >\n {loading ? <Loader2 className=\"animate-spin\" aria-hidden=\"true\" /> : icon}\n </button>\n );\n});\n\nIconButton.displayName = 'IconButton';\n"],"names":["iconButton","cva","IconButton","forwardRef","className","variant","size","icon","loading","disabled","type","props","ref","jsx","cn","Loader2"],"mappings":";;;;;;AAOA,MAAMA,IAAaC;AAAA,EACjB;AAAA,IACE;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,OAAO;AAAA,QACP,aAAa;AAAA,MAAA;AAAA,MAEf,MAAM;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,GA+BaC,IAAaC,EAA+C,SACvE,EAAE,WAAAC,GAAW,SAAAC,GAAS,MAAAC,GAAM,MAAAC,GAAM,SAAAC,GAAS,UAAAC,GAAU,MAAAC,IAAO,UAAU,GAAGC,EAAA,GACzEC,GACA;AAEA,SACE,gBAAAC;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,KAAAD;AAAA,MACA,MAAAF;AAAA,MACA,aAAWF,KAAW;AAAA,MACtB,UANeC,KAAYD;AAAA,MAO3B,WAAWM,EAAGd,EAAW,EAAE,SAAAK,GAAS,MAAAC,EAAA,CAAM,GAAGF,CAAS;AAAA,MACrD,GAAGO;AAAA,MAEH,cAAU,gBAAAE,EAACE,GAAA,EAAQ,WAAU,gBAAe,eAAY,QAAO,IAAKR;AAAA,IAAA;AAAA,EAAA;AAG3E,CAAC;AAEDL,EAAW,cAAc;"}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { InputHTMLAttributes, ReactNode } from 'react';
|
|
2
|
-
import { VariantProps } from '../../lib/cva';
|
|
2
|
+
import { VariantProps } from '../../lib/cva.js';
|
|
3
3
|
declare const field: (props?: ({
|
|
4
4
|
size?: "md" | "sm" | "lg" | null | undefined;
|
|
5
5
|
tone?: "default" | "error" | null | undefined;
|
|
6
6
|
} & import('class-variance-authority/types').ClassProp) | undefined) => string;
|
|
7
|
-
type InputType = '
|
|
7
|
+
type InputType = NonNullable<InputHTMLAttributes<HTMLInputElement>['type']>;
|
|
8
8
|
export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size' | 'prefix'>, VariantProps<typeof field> {
|
|
9
9
|
/** Field type. `password` enables a show/hide toggle; `search` adds a clear button. */
|
|
10
10
|
type?: InputType;
|
|
@@ -20,7 +20,10 @@ export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>,
|
|
|
20
20
|
suffix?: ReactNode;
|
|
21
21
|
/** Show an inline clear (×) button when the input has a value. */
|
|
22
22
|
clearable?: boolean;
|
|
23
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* Fires when the clear button is pressed. Controlled inputs own their state
|
|
25
|
+
* and should reset `value` here; uncontrolled inputs are cleared for you.
|
|
26
|
+
*/
|
|
24
27
|
onClear?: () => void;
|
|
25
28
|
/** Visually conceal the label while keeping it for screen readers. */
|
|
26
29
|
hideLabel?: boolean;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsxs as E, jsx as e } from "react/jsx-runtime";
|
|
3
|
+
import { forwardRef as K, useRef as Q, useState as I } from "react";
|
|
4
|
+
import { X as U, EyeOff as W, Eye as Y, Search as Z } from "../../icons/index.js";
|
|
5
|
+
import { cn as a } from "../../lib/utils.js";
|
|
6
|
+
import { useStrings as $ } from "../../hooks/use-strings.js";
|
|
7
|
+
import { useFieldIds as ee } from "../../hooks/use-field-ids.js";
|
|
8
|
+
import { cva as P } from "class-variance-authority";
|
|
9
|
+
const te = P(
|
|
10
|
+
[
|
|
11
|
+
"group inline-flex items-center w-full",
|
|
12
|
+
"rounded-md 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
|
+
],
|
|
17
|
+
{
|
|
18
|
+
variants: {
|
|
19
|
+
size: {
|
|
20
|
+
sm: "h-8 px-2.5 gap-1.5",
|
|
21
|
+
md: "h-9 px-3 gap-2",
|
|
22
|
+
lg: "h-10 px-3.5 gap-2"
|
|
23
|
+
},
|
|
24
|
+
tone: {
|
|
25
|
+
default: "focus-within:border-accent focus-within:ring-ring",
|
|
26
|
+
error: "border-danger focus-within:border-danger focus-within:ring-danger"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
defaultVariants: {
|
|
30
|
+
size: "md",
|
|
31
|
+
tone: "default"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
), re = P([
|
|
35
|
+
"flex-1 min-w-0 bg-transparent outline-none",
|
|
36
|
+
"placeholder:text-foreground-subtle",
|
|
37
|
+
"text-foreground",
|
|
38
|
+
// 16px below md so iOS Safari does not zoom the page on focus; --text-lg = 1rem.
|
|
39
|
+
"text-lg md:text-sm",
|
|
40
|
+
"disabled:cursor-not-allowed"
|
|
41
|
+
]), se = K(function({
|
|
42
|
+
className: S,
|
|
43
|
+
type: s = "text",
|
|
44
|
+
size: R,
|
|
45
|
+
tone: j,
|
|
46
|
+
label: x,
|
|
47
|
+
helperText: d,
|
|
48
|
+
error: c,
|
|
49
|
+
prefix: k,
|
|
50
|
+
suffix: v,
|
|
51
|
+
clearable: B,
|
|
52
|
+
onClear: l,
|
|
53
|
+
hideLabel: O,
|
|
54
|
+
id: F,
|
|
55
|
+
disabled: H,
|
|
56
|
+
value: u,
|
|
57
|
+
defaultValue: n,
|
|
58
|
+
onInput: f,
|
|
59
|
+
...L
|
|
60
|
+
}, o) {
|
|
61
|
+
const m = $(), p = !!c, { fieldId: w, helperId: T, errorId: _, describedBy: D } = ee(F, {
|
|
62
|
+
hasHelper: !!d,
|
|
63
|
+
hasError: p
|
|
64
|
+
}), r = Q(null), M = (t) => {
|
|
65
|
+
r.current = t, typeof o == "function" ? o(t) : o && (o.current = t);
|
|
66
|
+
}, [i, V] = I(!1), X = s === "password" ? i ? "text" : "password" : s, q = p ? "error" : j, N = k ?? (s === "search" ? /* @__PURE__ */ e(Z, { className: "text-foreground-subtle size-4", "aria-hidden": !0 }) : null), g = u !== void 0, [A, y] = I(
|
|
67
|
+
n == null ? "" : String(n)
|
|
68
|
+
), h = g ? u : A, C = h !== void 0 && h !== "" && h !== null, G = (t) => {
|
|
69
|
+
g || y(t.currentTarget.value), f == null || f(t);
|
|
70
|
+
}, J = () => {
|
|
71
|
+
var t, z;
|
|
72
|
+
if (!g && r.current) {
|
|
73
|
+
const b = (t = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")) == null ? void 0 : t.set;
|
|
74
|
+
b == null || b.call(r.current, ""), r.current.dispatchEvent(new Event("input", { bubbles: !0 })), y("");
|
|
75
|
+
}
|
|
76
|
+
l == null || l(), (z = r.current) == null || z.focus();
|
|
77
|
+
};
|
|
78
|
+
return /* @__PURE__ */ E("div", { className: a("flex flex-col gap-1.5", S), children: [
|
|
79
|
+
x && /* @__PURE__ */ e(
|
|
80
|
+
"label",
|
|
81
|
+
{
|
|
82
|
+
htmlFor: w,
|
|
83
|
+
className: a("text-foreground text-sm font-medium", O && "sr-only"),
|
|
84
|
+
children: x
|
|
85
|
+
}
|
|
86
|
+
),
|
|
87
|
+
/* @__PURE__ */ E("div", { className: a(te({ size: R, tone: q })), children: [
|
|
88
|
+
N && /* @__PURE__ */ e("span", { className: "text-foreground-subtle flex items-center [&_svg]:size-4", children: N }),
|
|
89
|
+
/* @__PURE__ */ e(
|
|
90
|
+
"input",
|
|
91
|
+
{
|
|
92
|
+
ref: M,
|
|
93
|
+
id: w,
|
|
94
|
+
type: X,
|
|
95
|
+
disabled: H,
|
|
96
|
+
value: u,
|
|
97
|
+
defaultValue: n,
|
|
98
|
+
onInput: G,
|
|
99
|
+
"aria-invalid": p || void 0,
|
|
100
|
+
"aria-describedby": D,
|
|
101
|
+
className: a(re()),
|
|
102
|
+
...L
|
|
103
|
+
}
|
|
104
|
+
),
|
|
105
|
+
B && C && /* @__PURE__ */ e(
|
|
106
|
+
"button",
|
|
107
|
+
{
|
|
108
|
+
type: "button",
|
|
109
|
+
onClick: J,
|
|
110
|
+
"aria-label": m.input.clear,
|
|
111
|
+
className: "text-foreground-subtle hover:text-foreground focus-visible:ring-ring flex items-center rounded-sm outline-none focus-visible:ring-2",
|
|
112
|
+
children: /* @__PURE__ */ e(U, { className: "size-4", "aria-hidden": !0 })
|
|
113
|
+
}
|
|
114
|
+
),
|
|
115
|
+
s === "password" && /* @__PURE__ */ e(
|
|
116
|
+
"button",
|
|
117
|
+
{
|
|
118
|
+
type: "button",
|
|
119
|
+
onClick: () => V((t) => !t),
|
|
120
|
+
"aria-label": i ? m.input.hidePassword : m.input.showPassword,
|
|
121
|
+
"aria-pressed": i,
|
|
122
|
+
className: "text-foreground-subtle hover:text-foreground focus-visible:ring-ring flex items-center rounded-sm outline-none focus-visible:ring-2",
|
|
123
|
+
children: i ? /* @__PURE__ */ e(W, { className: "size-4", "aria-hidden": !0 }) : /* @__PURE__ */ e(Y, { className: "size-4", "aria-hidden": !0 })
|
|
124
|
+
}
|
|
125
|
+
),
|
|
126
|
+
v && /* @__PURE__ */ e("span", { className: "text-foreground-subtle flex items-center [&_svg]:size-4", children: v })
|
|
127
|
+
] }),
|
|
128
|
+
c ? /* @__PURE__ */ e("p", { id: _, role: "alert", className: "text-danger-text text-xs", children: c }) : d ? /* @__PURE__ */ e("p", { id: T, className: "text-foreground-subtle text-xs", children: d }) : null
|
|
129
|
+
] });
|
|
130
|
+
});
|
|
131
|
+
se.displayName = "Input";
|
|
132
|
+
export {
|
|
133
|
+
se as Input
|
|
134
|
+
};
|
|
135
|
+
//# 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 FormEvent,\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-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 ],\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 '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]);\n\ntype InputType = NonNullable<InputHTMLAttributes<HTMLInputElement>['type']>;\n\nexport interface InputProps\n extends\n Omit<InputHTMLAttributes<HTMLInputElement>, 'size' | 'prefix'>,\n VariantProps<typeof field> {\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,\n defaultValue,\n onInput,\n ...props\n },\n ref,\n) {\n const strings = useStrings();\n const isError = Boolean(error);\n const { fieldId, helperId, errorId, describedBy } = useFieldIds(id, {\n hasHelper: Boolean(helperText),\n hasError: isError,\n });\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 const handleInput = (e: FormEvent<HTMLInputElement>) => {\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 || undefined}\n aria-describedby={describedBy}\n className={cn(innerInput())}\n {...props}\n />\n\n {clearable && hasValue && (\n <button\n type=\"button\"\n onClick={handleClear}\n aria-label={strings.input.clear}\n className=\"text-foreground-subtle hover:text-foreground focus-visible:ring-ring flex items-center rounded-sm outline-none focus-visible:ring-2\"\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 aria-label={showPassword ? strings.input.hidePassword : strings.input.showPassword}\n aria-pressed={showPassword}\n className=\"text-foreground-subtle hover:text-foreground focus-visible:ring-ring flex items-center rounded-sm outline-none focus-visible:ring-2\"\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} role=\"alert\" 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","Input","forwardRef","className","type","size","tone","label","helperText","error","prefix","suffix","clearable","onClear","hideLabel","id","disabled","value","defaultValue","onInput","props","ref","strings","useStrings","isError","fieldId","helperId","errorId","describedBy","useFieldIds","innerRef","useRef","setRefs","el","showPassword","setShowPassword","useState","effectiveType","effectiveTone","renderedPrefix","jsx","Search","isControlled","localValue","setLocalValue","current","hasValue","handleInput","e","handleClear","setter","_a","_b","cn","jsxs","X","p","EyeOff","Eye"],"mappings":"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"}
|
|
@@ -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
|
+
"inline-flex items-center justify-center rounded-sm border border-border bg-background-subtle 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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Kbd.js","sources":["../../../src/components/ui/Kbd.tsx"],"sourcesContent":["'use client';\n\nimport { forwardRef, type HTMLAttributes } from 'react';\nimport { cn } from '@/lib/utils';\n\nexport interface KbdProps extends HTMLAttributes<HTMLElement> {\n /** Visual size — match the surrounding text. */\n size?: 'sm' | 'md';\n}\n\n/**\n * Stylised keyboard shortcut indicator. Compose multiple `<Kbd>` for chords.\n *\n * @example\n * Press <Kbd>⌘</Kbd>+<Kbd>K</Kbd> to open the command palette.\n *\n * @do Use OS-conventional symbols (⌘ ⇧ ⌥ ⌃ ⏎) — keep visuals consistent\n * across the app.\n * @dont Use Kbd for clickable buttons — it implies a real keyboard input.\n */\nexport const Kbd = forwardRef<HTMLElement, KbdProps>(function Kbd(\n { size = 'sm', className, ...props },\n ref,\n) {\n return (\n <kbd\n ref={ref}\n className={cn(\n 'inline-flex items-center justify-center rounded-sm border border-border bg-background-subtle font-mono',\n 'text-foreground-muted shadow-xs',\n size === 'sm' ? 'h-5 min-w-5 px-1 text-xs' : 'h-6 min-w-6 px-1.5 text-xs',\n className,\n )}\n {...props}\n />\n );\n});\nKbd.displayName = 'Kbd';\n"],"names":["Kbd","forwardRef","size","className","props","ref","jsx","cn"],"mappings":";;;;AAoBO,MAAMA,IAAMC,EAAkC,SACnD,EAAE,MAAAC,IAAO,MAAM,WAAAC,GAAW,GAAGC,EAAA,GAC7BC,GACA;AACA,SACE,gBAAAC;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,KAAAD;AAAA,MACA,WAAWE;AAAA,QACT;AAAA,QACA;AAAA,QACAL,MAAS,OAAO,6BAA6B;AAAA,QAC7CC;AAAA,MAAA;AAAA,MAED,GAAGC;AAAA,IAAA;AAAA,EAAA;AAGV,CAAC;AACDJ,EAAI,cAAc;"}
|
|
@@ -32,6 +32,8 @@ export interface MultiSelectProps {
|
|
|
32
32
|
clearable?: boolean;
|
|
33
33
|
/** Disable the entire field. */
|
|
34
34
|
disabled?: boolean;
|
|
35
|
+
/** Consumer-supplied id for the search input (label association). */
|
|
36
|
+
id?: string;
|
|
35
37
|
className?: string;
|
|
36
38
|
}
|
|
37
39
|
/**
|
|
@@ -39,7 +41,7 @@ export interface MultiSelectProps {
|
|
|
39
41
|
*
|
|
40
42
|
* Keyboard:
|
|
41
43
|
* - Backspace on the input with empty query removes the last chip
|
|
42
|
-
* -
|
|
44
|
+
* - ArrowUp / ArrowDown move the highlight, Enter toggles the highlighted option
|
|
43
45
|
* - Escape closes the menu
|
|
44
46
|
*
|
|
45
47
|
* @example Tag picker
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsxs as n, jsx as t } from "react/jsx-runtime";
|
|
3
|
+
import { forwardRef as Z, useState as w, useRef as y, useEffect as _, useMemo as F, useCallback as O } from "react";
|
|
4
|
+
import * as f from "@radix-ui/react-popover";
|
|
5
|
+
import { Command as p } from "cmdk";
|
|
6
|
+
import { X as P, ChevronsUpDown as $, Check as ee } from "../../icons/index.js";
|
|
7
|
+
import { cn as i } from "../../lib/utils.js";
|
|
8
|
+
import { useStrings as te } from "../../hooks/use-strings.js";
|
|
9
|
+
import { formatString as re } from "../../lib/strings.js";
|
|
10
|
+
import { useFieldIds as oe } from "../../hooks/use-field-ids.js";
|
|
11
|
+
const ne = Z(function({
|
|
12
|
+
options: l,
|
|
13
|
+
value: r,
|
|
14
|
+
onChange: s,
|
|
15
|
+
label: N,
|
|
16
|
+
helperText: h,
|
|
17
|
+
error: x,
|
|
18
|
+
placeholder: B,
|
|
19
|
+
emptyText: E,
|
|
20
|
+
maxVisibleChips: L = 3,
|
|
21
|
+
clearable: M = !0,
|
|
22
|
+
disabled: d,
|
|
23
|
+
id: j,
|
|
24
|
+
className: K
|
|
25
|
+
}, U) {
|
|
26
|
+
const c = te(), q = B ?? c.multiSelect.placeholder, H = E ?? c.multiSelect.empty, g = !!x, { fieldId: k, helperId: Q, errorId: X, describedBy: G } = oe(j, {
|
|
27
|
+
hasHelper: !!h,
|
|
28
|
+
hasError: g
|
|
29
|
+
}), [a, u] = w(!1), [m, J] = w(""), S = y(null), C = y(null), z = y(null), [T, V] = w(void 0);
|
|
30
|
+
_(() => {
|
|
31
|
+
var e;
|
|
32
|
+
a && V((e = z.current) == null ? void 0 : e.id);
|
|
33
|
+
}, [a]);
|
|
34
|
+
const b = F(() => l.filter((e) => r.includes(e.value)), [l, r]), I = O(
|
|
35
|
+
(e) => {
|
|
36
|
+
r.includes(e) ? s(r.filter((o) => o !== e)) : s([...r, e]);
|
|
37
|
+
},
|
|
38
|
+
[r, s]
|
|
39
|
+
), W = O(() => s([]), [s]), Y = (e) => {
|
|
40
|
+
e.key === "Backspace" && m === "" && r.length > 0 ? s(r.slice(0, -1)) : (e.key === "ArrowDown" || e.key === "ArrowUp") && !a && u(!0);
|
|
41
|
+
}, D = F(
|
|
42
|
+
() => l.filter((e) => e.label.toLowerCase().includes(m.toLowerCase())),
|
|
43
|
+
[l, m]
|
|
44
|
+
), v = b.slice(0, L), R = b.length - v.length;
|
|
45
|
+
return /* @__PURE__ */ n("div", { ref: U, className: i("flex flex-col gap-1.5", K), children: [
|
|
46
|
+
N && /* @__PURE__ */ t("label", { htmlFor: k, className: "text-foreground text-sm font-medium", children: N }),
|
|
47
|
+
/* @__PURE__ */ t(p, { shouldFilter: !1, className: "flex w-full flex-col", children: /* @__PURE__ */ n(f.Root, { open: a, onOpenChange: u, children: [
|
|
48
|
+
/* @__PURE__ */ t(f.Anchor, { asChild: !0, children: /* @__PURE__ */ n(
|
|
49
|
+
"div",
|
|
50
|
+
{
|
|
51
|
+
ref: C,
|
|
52
|
+
"aria-disabled": d || void 0,
|
|
53
|
+
className: i(
|
|
54
|
+
"bg-card flex h-9 w-full cursor-text items-center gap-1.5 overflow-hidden rounded-md border px-2 py-1 text-sm",
|
|
55
|
+
"transition-colors duration-[var(--duration-fast)] ease-[var(--ease-out)]",
|
|
56
|
+
"focus-within:ring-offset-background focus-within:ring-2 focus-within:ring-offset-2",
|
|
57
|
+
g ? "border-danger focus-within:border-danger focus-within:ring-danger" : "border-border focus-within:border-accent focus-within:ring-ring",
|
|
58
|
+
d && "pointer-events-none opacity-50"
|
|
59
|
+
),
|
|
60
|
+
onClick: () => {
|
|
61
|
+
var e;
|
|
62
|
+
(e = S.current) == null || e.focus(), d || u(!0);
|
|
63
|
+
},
|
|
64
|
+
children: [
|
|
65
|
+
/* @__PURE__ */ n("div", { className: "flex min-w-0 flex-1 items-center gap-1.5 overflow-hidden", children: [
|
|
66
|
+
v.map((e) => /* @__PURE__ */ n(
|
|
67
|
+
"span",
|
|
68
|
+
{
|
|
69
|
+
className: "bg-accent-soft text-on-accent-soft inline-flex max-w-[10rem] shrink-0 items-center gap-1 rounded-md px-1.5 py-0.5 text-xs font-medium",
|
|
70
|
+
children: [
|
|
71
|
+
/* @__PURE__ */ t("span", { className: "truncate", title: e.label, children: e.label }),
|
|
72
|
+
/* @__PURE__ */ t(
|
|
73
|
+
"button",
|
|
74
|
+
{
|
|
75
|
+
type: "button",
|
|
76
|
+
"aria-label": re(c.multiSelect.remove, { label: e.label }),
|
|
77
|
+
className: "text-on-accent-soft hover:text-foreground focus-visible:ring-ring inline-flex items-center rounded-sm outline-none focus-visible:ring-2",
|
|
78
|
+
onClick: (o) => {
|
|
79
|
+
o.stopPropagation(), I(e.value);
|
|
80
|
+
},
|
|
81
|
+
children: /* @__PURE__ */ t(P, { className: "size-3", "aria-hidden": !0 })
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
]
|
|
85
|
+
},
|
|
86
|
+
e.value
|
|
87
|
+
)),
|
|
88
|
+
R > 0 && /* @__PURE__ */ n("span", { className: "bg-background-muted text-foreground-muted shrink-0 rounded-md px-1.5 py-0.5 text-xs font-medium", children: [
|
|
89
|
+
"+",
|
|
90
|
+
R
|
|
91
|
+
] }),
|
|
92
|
+
/* @__PURE__ */ t(
|
|
93
|
+
"input",
|
|
94
|
+
{
|
|
95
|
+
ref: S,
|
|
96
|
+
id: k,
|
|
97
|
+
type: "text",
|
|
98
|
+
autoComplete: "off",
|
|
99
|
+
value: m,
|
|
100
|
+
onChange: (e) => J(e.target.value),
|
|
101
|
+
onFocus: () => u(!0),
|
|
102
|
+
onKeyDown: Y,
|
|
103
|
+
placeholder: v.length === 0 ? q : void 0,
|
|
104
|
+
role: "combobox",
|
|
105
|
+
"aria-expanded": a,
|
|
106
|
+
"aria-controls": a ? T : void 0,
|
|
107
|
+
"aria-haspopup": "listbox",
|
|
108
|
+
"aria-autocomplete": "list",
|
|
109
|
+
"aria-invalid": g || void 0,
|
|
110
|
+
"aria-describedby": G,
|
|
111
|
+
className: "placeholder:text-foreground-subtle min-w-[6ch] flex-1 bg-transparent text-sm outline-none",
|
|
112
|
+
disabled: d
|
|
113
|
+
}
|
|
114
|
+
)
|
|
115
|
+
] }),
|
|
116
|
+
/* @__PURE__ */ n("span", { className: "flex shrink-0 items-center gap-1 pl-1", children: [
|
|
117
|
+
M && b.length > 0 && /* @__PURE__ */ t(
|
|
118
|
+
"button",
|
|
119
|
+
{
|
|
120
|
+
type: "button",
|
|
121
|
+
"aria-label": c.multiSelect.clearAll,
|
|
122
|
+
onClick: (e) => {
|
|
123
|
+
e.stopPropagation(), W();
|
|
124
|
+
},
|
|
125
|
+
className: "text-foreground-subtle hover:text-foreground focus-visible:ring-ring rounded-sm outline-none focus-visible:ring-2",
|
|
126
|
+
children: /* @__PURE__ */ t(P, { className: "size-4", "aria-hidden": !0 })
|
|
127
|
+
}
|
|
128
|
+
),
|
|
129
|
+
/* @__PURE__ */ t($, { className: "text-foreground-subtle size-4", "aria-hidden": !0 })
|
|
130
|
+
] })
|
|
131
|
+
]
|
|
132
|
+
}
|
|
133
|
+
) }),
|
|
134
|
+
/* @__PURE__ */ t(f.Portal, { children: /* @__PURE__ */ t(
|
|
135
|
+
f.Content,
|
|
136
|
+
{
|
|
137
|
+
align: "start",
|
|
138
|
+
sideOffset: 4,
|
|
139
|
+
className: i(
|
|
140
|
+
"border-border bg-popover text-popover-foreground z-[var(--z-popover)] w-[var(--radix-popover-trigger-width)] overflow-hidden rounded-lg border shadow-md",
|
|
141
|
+
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
|
142
|
+
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
143
|
+
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95"
|
|
144
|
+
),
|
|
145
|
+
onOpenAutoFocus: (e) => e.preventDefault(),
|
|
146
|
+
onInteractOutside: (e) => {
|
|
147
|
+
var A;
|
|
148
|
+
const o = e.target;
|
|
149
|
+
o && ((A = C.current) != null && A.contains(o)) && e.preventDefault();
|
|
150
|
+
},
|
|
151
|
+
children: /* @__PURE__ */ n(p.List, { ref: z, className: "max-h-64 overflow-y-auto p-1", children: [
|
|
152
|
+
D.map((e) => {
|
|
153
|
+
const o = r.includes(e.value);
|
|
154
|
+
return /* @__PURE__ */ n(
|
|
155
|
+
p.Item,
|
|
156
|
+
{
|
|
157
|
+
value: e.value,
|
|
158
|
+
disabled: e.disabled,
|
|
159
|
+
onSelect: () => I(e.value),
|
|
160
|
+
className: i(
|
|
161
|
+
"text-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm",
|
|
162
|
+
"data-[selected=true]:bg-background-muted",
|
|
163
|
+
"data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50"
|
|
164
|
+
),
|
|
165
|
+
children: [
|
|
166
|
+
/* @__PURE__ */ t(
|
|
167
|
+
"span",
|
|
168
|
+
{
|
|
169
|
+
"aria-hidden": !0,
|
|
170
|
+
className: i(
|
|
171
|
+
"flex size-4 items-center justify-center rounded-sm border",
|
|
172
|
+
o ? "border-accent bg-accent text-on-accent" : "border-border"
|
|
173
|
+
),
|
|
174
|
+
children: o && /* @__PURE__ */ t(ee, { className: "size-3", "aria-hidden": !0 })
|
|
175
|
+
}
|
|
176
|
+
),
|
|
177
|
+
/* @__PURE__ */ t("span", { className: "flex-1", children: e.label }),
|
|
178
|
+
e.description && /* @__PURE__ */ t("span", { className: "text-foreground-subtle text-xs", children: e.description })
|
|
179
|
+
]
|
|
180
|
+
},
|
|
181
|
+
e.value
|
|
182
|
+
);
|
|
183
|
+
}),
|
|
184
|
+
D.length === 0 && /* @__PURE__ */ t(p.Empty, { className: "text-foreground-subtle px-2 py-6 text-center text-sm", children: H })
|
|
185
|
+
] })
|
|
186
|
+
}
|
|
187
|
+
) })
|
|
188
|
+
] }) }),
|
|
189
|
+
x ? /* @__PURE__ */ t("p", { id: X, role: "alert", className: "text-danger-text text-xs", children: x }) : h ? /* @__PURE__ */ t("p", { id: Q, className: "text-foreground-subtle text-xs", children: h }) : null
|
|
190
|
+
] });
|
|
191
|
+
});
|
|
192
|
+
ne.displayName = "MultiSelect";
|
|
193
|
+
export {
|
|
194
|
+
ne as MultiSelect
|
|
195
|
+
};
|
|
196
|
+
//# sourceMappingURL=MultiSelect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MultiSelect.js","sources":["../../../src/components/ui/MultiSelect.tsx"],"sourcesContent":["'use client';\n\nimport {\n forwardRef,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n type KeyboardEvent,\n type ReactNode,\n} from 'react';\nimport * as PopoverPrimitive from '@radix-ui/react-popover';\nimport { Command as CommandPrimitive } from 'cmdk';\nimport { Check, ChevronsUpDown, X } from '@/icons';\nimport { cn } from '@/lib/utils';\nimport { useStrings } from '@/hooks/use-strings';\nimport { formatString } from '@/lib/strings';\nimport { useFieldIds } from '@/hooks/use-field-ids';\n\nexport interface MultiSelectOption {\n /** Stable, unique value submitted in `onChange`. */\n value: string;\n /** Visible label shown both in the menu and in the selected chip. */\n label: string;\n /** Optional secondary text shown after the label in the menu. */\n description?: string;\n /** Disable selecting this option. */\n disabled?: boolean;\n}\n\nexport interface MultiSelectProps {\n /** All possible options. */\n options: MultiSelectOption[];\n /** Currently selected values. Controlled. */\n value: string[];\n /** Called whenever the selection changes. */\n onChange: (next: string[]) => void;\n /** Visible label above the field. */\n label?: ReactNode;\n /** Hint below the field. Hidden when `error` is set. */\n helperText?: ReactNode;\n /** Validation message. */\n error?: ReactNode;\n /** Empty-state placeholder shown when nothing is selected. */\n placeholder?: string;\n /** Text shown when no options match the search. */\n emptyText?: string;\n /** Max number of chips rendered inline; remainder shown as \"+N more\". */\n maxVisibleChips?: number;\n /** Whether the user can clear all selections with a single click. */\n clearable?: boolean;\n /** Disable the entire field. */\n disabled?: boolean;\n /** Consumer-supplied id for the search input (label association). */\n id?: string;\n className?: string;\n}\n\n/**\n * Chip-based multi-select with a searchable menu (cmdk under the hood).\n *\n * Keyboard:\n * - Backspace on the input with empty query removes the last chip\n * - ArrowUp / ArrowDown move the highlight, Enter toggles the highlighted option\n * - Escape closes the menu\n *\n * @example Tag picker\n * <MultiSelect label=\"Tags\"\n * options={tags}\n * value={selected}\n * onChange={setSelected}\n * placeholder=\"Pick tags\" />\n *\n * @example Bounded with overflow chip\n * <MultiSelect options={users}\n * value={value}\n * onChange={setValue}\n * maxVisibleChips={3} />\n *\n * @do Cap `maxVisibleChips` (3–5) on narrow surfaces so the field doesn't\n * reflow as the user adds selections.\n * @dont Use MultiSelect for fewer than ~6 options. CheckboxGroup is clearer.\n */\nexport const MultiSelect = forwardRef<HTMLDivElement, MultiSelectProps>(function MultiSelect(\n {\n options,\n value,\n onChange,\n label,\n helperText,\n error,\n placeholder: placeholderProp,\n emptyText: emptyTextProp,\n maxVisibleChips = 3,\n clearable = true,\n disabled,\n id,\n className,\n },\n ref,\n) {\n const strings = useStrings();\n const placeholder = placeholderProp ?? strings.multiSelect.placeholder;\n const emptyText = emptyTextProp ?? strings.multiSelect.empty;\n const isError = Boolean(error);\n const { fieldId, helperId, errorId, describedBy } = useFieldIds(id, {\n hasHelper: Boolean(helperText),\n hasError: isError,\n });\n\n const [open, setOpen] = useState(false);\n const [query, setQuery] = useState('');\n const inputRef = useRef<HTMLInputElement>(null);\n const fieldRef = useRef<HTMLDivElement>(null);\n // cmdk assigns its own id to the list (ours is ignored), so read it back\n // once the portal mounts to keep aria-controls truthful.\n const listRef = useRef<HTMLDivElement>(null);\n const [listId, setListId] = useState<string | undefined>(undefined);\n useEffect(() => {\n if (open) setListId(listRef.current?.id);\n }, [open]);\n\n const selected = useMemo(() => options.filter((o) => value.includes(o.value)), [options, value]);\n\n const toggle = useCallback(\n (v: string) => {\n if (value.includes(v)) onChange(value.filter((x) => x !== v));\n else onChange([...value, v]);\n },\n [value, onChange],\n );\n\n const clear = useCallback(() => onChange([]), [onChange]);\n\n const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {\n if (e.key === 'Backspace' && query === '' && value.length > 0) {\n onChange(value.slice(0, -1));\n } else if ((e.key === 'ArrowDown' || e.key === 'ArrowUp') && !open) {\n setOpen(true);\n }\n };\n\n const filtered = useMemo(\n () => options.filter((o) => o.label.toLowerCase().includes(query.toLowerCase())),\n [options, query],\n );\n\n const visibleChips = selected.slice(0, maxVisibleChips);\n const overflow = selected.length - visibleChips.length;\n\n return (\n <div ref={ref} className={cn('flex flex-col gap-1.5', className)}>\n {label && (\n <label htmlFor={fieldId} className=\"text-foreground text-sm font-medium\">\n {label}\n </label>\n )}\n\n {/* The Command root wraps the whole field so cmdk's root keydown handler\n (ArrowUp/Down/Enter) sees the input even though the list is portaled.\n A plain <input> is used instead of Command.Input because cmdk forces\n its own id / aria-* on that element, breaking the label association. */}\n <CommandPrimitive shouldFilter={false} className=\"flex w-full flex-col\">\n <PopoverPrimitive.Root open={open} onOpenChange={setOpen}>\n <PopoverPrimitive.Anchor asChild>\n {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -- click-to-focus wrapper; the <input role=\"combobox\"> inside owns keyboard handling */}\n <div\n ref={fieldRef}\n aria-disabled={disabled || undefined}\n className={cn(\n 'bg-card flex h-9 w-full cursor-text items-center gap-1.5 overflow-hidden rounded-md border px-2 py-1 text-sm',\n 'transition-colors duration-[var(--duration-fast)] ease-[var(--ease-out)]',\n 'focus-within:ring-offset-background focus-within:ring-2 focus-within:ring-offset-2',\n isError\n ? 'border-danger focus-within:border-danger focus-within:ring-danger'\n : 'border-border focus-within:border-accent focus-within:ring-ring',\n disabled && 'pointer-events-none opacity-50',\n )}\n onClick={() => {\n inputRef.current?.focus();\n if (!disabled) setOpen(true);\n }}\n >\n <div className=\"flex min-w-0 flex-1 items-center gap-1.5 overflow-hidden\">\n {visibleChips.map((opt) => (\n <span\n key={opt.value}\n className=\"bg-accent-soft text-on-accent-soft inline-flex max-w-[10rem] shrink-0 items-center gap-1 rounded-md px-1.5 py-0.5 text-xs font-medium\"\n >\n <span className=\"truncate\" title={opt.label}>\n {opt.label}\n </span>\n <button\n type=\"button\"\n aria-label={formatString(strings.multiSelect.remove, { label: opt.label })}\n className=\"text-on-accent-soft hover:text-foreground focus-visible:ring-ring inline-flex items-center rounded-sm outline-none focus-visible:ring-2\"\n onClick={(e) => {\n e.stopPropagation();\n toggle(opt.value);\n }}\n >\n <X className=\"size-3\" aria-hidden />\n </button>\n </span>\n ))}\n {overflow > 0 && (\n <span className=\"bg-background-muted text-foreground-muted shrink-0 rounded-md px-1.5 py-0.5 text-xs font-medium\">\n +{overflow}\n </span>\n )}\n <input\n ref={inputRef}\n id={fieldId}\n type=\"text\"\n autoComplete=\"off\"\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n onFocus={() => setOpen(true)}\n onKeyDown={handleKeyDown}\n placeholder={visibleChips.length === 0 ? placeholder : undefined}\n role=\"combobox\"\n aria-expanded={open}\n aria-controls={open ? listId : undefined}\n aria-haspopup=\"listbox\"\n aria-autocomplete=\"list\"\n aria-invalid={isError || undefined}\n aria-describedby={describedBy}\n className=\"placeholder:text-foreground-subtle min-w-[6ch] flex-1 bg-transparent text-sm outline-none\"\n disabled={disabled}\n />\n </div>\n <span className=\"flex shrink-0 items-center gap-1 pl-1\">\n {clearable && selected.length > 0 && (\n <button\n type=\"button\"\n aria-label={strings.multiSelect.clearAll}\n onClick={(e) => {\n e.stopPropagation();\n clear();\n }}\n className=\"text-foreground-subtle hover:text-foreground focus-visible:ring-ring rounded-sm outline-none focus-visible:ring-2\"\n >\n <X className=\"size-4\" aria-hidden />\n </button>\n )}\n <ChevronsUpDown className=\"text-foreground-subtle size-4\" aria-hidden />\n </span>\n </div>\n </PopoverPrimitive.Anchor>\n\n <PopoverPrimitive.Portal>\n <PopoverPrimitive.Content\n align=\"start\"\n sideOffset={4}\n className={cn(\n 'border-border bg-popover text-popover-foreground z-[var(--z-popover)] w-[var(--radix-popover-trigger-width)] overflow-hidden rounded-lg border shadow-md',\n 'data-[state=open]:animate-in data-[state=closed]:animate-out',\n 'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',\n 'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',\n )}\n onOpenAutoFocus={(e) => e.preventDefault()}\n onInteractOutside={(e) => {\n // Clicks inside the anchor (chips, clear) must not close the menu.\n const target = e.target as Node | null;\n if (target && fieldRef.current?.contains(target)) e.preventDefault();\n }}\n >\n <CommandPrimitive.List ref={listRef} className=\"max-h-64 overflow-y-auto p-1\">\n {filtered.map((opt) => {\n const isSelected = value.includes(opt.value);\n return (\n <CommandPrimitive.Item\n key={opt.value}\n value={opt.value}\n disabled={opt.disabled}\n onSelect={() => toggle(opt.value)}\n className={cn(\n 'text-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm',\n 'data-[selected=true]:bg-background-muted',\n 'data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50',\n )}\n >\n <span\n aria-hidden\n className={cn(\n 'flex size-4 items-center justify-center rounded-sm border',\n isSelected ? 'border-accent bg-accent text-on-accent' : 'border-border',\n )}\n >\n {isSelected && <Check className=\"size-3\" aria-hidden />}\n </span>\n <span className=\"flex-1\">{opt.label}</span>\n {opt.description && (\n <span className=\"text-foreground-subtle text-xs\">{opt.description}</span>\n )}\n </CommandPrimitive.Item>\n );\n })}\n {filtered.length === 0 && (\n <CommandPrimitive.Empty className=\"text-foreground-subtle px-2 py-6 text-center text-sm\">\n {emptyText}\n </CommandPrimitive.Empty>\n )}\n </CommandPrimitive.List>\n </PopoverPrimitive.Content>\n </PopoverPrimitive.Portal>\n </PopoverPrimitive.Root>\n </CommandPrimitive>\n\n {error ? (\n <p id={errorId} role=\"alert\" 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\nMultiSelect.displayName = 'MultiSelect';\n"],"names":["MultiSelect","forwardRef","options","value","onChange","label","helperText","error","placeholderProp","emptyTextProp","maxVisibleChips","clearable","disabled","id","className","ref","strings","useStrings","placeholder","emptyText","isError","fieldId","helperId","errorId","describedBy","useFieldIds","open","setOpen","useState","query","setQuery","inputRef","useRef","fieldRef","listRef","listId","setListId","useEffect","_a","selected","useMemo","o","toggle","useCallback","v","x","clear","handleKeyDown","filtered","visibleChips","overflow","cn","jsx","CommandPrimitive","jsxs","PopoverPrimitive","opt","formatString","e","X","ChevronsUpDown","target","isSelected","Check"],"mappings":"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"}
|
|
@@ -1,4 +1,25 @@
|
|
|
1
1
|
import { HTMLAttributes } from 'react';
|
|
2
|
+
import { UiStrings } from '../../lib/strings.js';
|
|
3
|
+
export interface PaginationLabels {
|
|
4
|
+
/** Summary line. Receives the computed range. */
|
|
5
|
+
showing: (from: number, to: number, total: number) => string;
|
|
6
|
+
/** sr-only label for the page-size select. */
|
|
7
|
+
rowsPerPage: string;
|
|
8
|
+
first: string;
|
|
9
|
+
last: string;
|
|
10
|
+
prev: string;
|
|
11
|
+
next: string;
|
|
12
|
+
/** aria-label for a numbered page button. */
|
|
13
|
+
page: (n: number) => string;
|
|
14
|
+
/** Suffix in the page-size options, e.g. "20 / page". */
|
|
15
|
+
perPage: (n: number) => string;
|
|
16
|
+
/** aria-label of the nav landmark. */
|
|
17
|
+
nav: string;
|
|
18
|
+
}
|
|
19
|
+
/** Build the label set from a strings object (context → functions). */
|
|
20
|
+
export declare function paginationLabelsFromStrings(s: UiStrings['pagination']): PaginationLabels;
|
|
21
|
+
/** English defaults — derived from `defaultStrings.pagination`. */
|
|
22
|
+
export declare const defaultPaginationLabels: PaginationLabels;
|
|
2
23
|
export interface PaginationProps extends HTMLAttributes<HTMLElement> {
|
|
3
24
|
/** 1-indexed current page. */
|
|
4
25
|
page: number;
|
|
@@ -16,6 +37,8 @@ export interface PaginationProps extends HTMLAttributes<HTMLElement> {
|
|
|
16
37
|
onPageSizeChange?: (size: number) => void;
|
|
17
38
|
/** Show first/last (« ») jump buttons. Default true. */
|
|
18
39
|
showJump?: boolean;
|
|
40
|
+
/** Override any of the visible / accessible strings (i18n). */
|
|
41
|
+
labels?: Partial<PaginationLabels>;
|
|
19
42
|
}
|
|
20
43
|
/**
|
|
21
44
|
* Numbered pagination with prev/next, first/last jumps, page-size selector,
|