@lfrprazeres/ui 0.1.0 → 0.2.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 +46 -6
- package/dist/charts/chart-data-table.d.ts +30 -0
- package/dist/charts/chart-data-table.d.ts.map +1 -0
- package/dist/charts/chart-data-table.js +34 -0
- package/dist/charts/chart-data-table.js.map +1 -0
- package/dist/charts/chart-frame.d.ts +33 -0
- package/dist/charts/chart-frame.d.ts.map +1 -0
- package/dist/charts/chart-frame.js +35 -0
- package/dist/charts/chart-frame.js.map +1 -0
- package/dist/charts/chart-legend-list.d.ts +20 -0
- package/dist/charts/chart-legend-list.d.ts.map +1 -0
- package/dist/charts/chart-legend-list.js +38 -0
- package/dist/charts/chart-legend-list.js.map +1 -0
- package/dist/charts/chart-ramp.d.ts +7 -0
- package/dist/charts/chart-ramp.d.ts.map +1 -0
- package/dist/charts/chart-ramp.js +16 -0
- package/dist/charts/chart-ramp.js.map +1 -0
- package/dist/charts/donut-chart.d.ts +16 -0
- package/dist/charts/donut-chart.d.ts.map +1 -0
- package/dist/charts/donut-chart.js +78 -0
- package/dist/charts/donut-chart.js.map +1 -0
- package/dist/charts/index.d.ts +10 -0
- package/dist/charts/index.js +9 -0
- package/dist/charts/line-chart.d.ts +22 -0
- package/dist/charts/line-chart.d.ts.map +1 -0
- package/dist/charts/line-chart.js +84 -0
- package/dist/charts/line-chart.js.map +1 -0
- package/dist/charts/types.d.ts +35 -0
- package/dist/charts/types.d.ts.map +1 -0
- package/dist/charts/use-cartesian-chart.d.ts +35 -0
- package/dist/charts/use-cartesian-chart.d.ts.map +1 -0
- package/dist/charts/use-cartesian-chart.js +48 -0
- package/dist/charts/use-cartesian-chart.js.map +1 -0
- package/dist/components/chip.d.ts +7 -4
- package/dist/components/chip.d.ts.map +1 -1
- package/dist/components/chip.js +15 -11
- package/dist/components/chip.js.map +1 -1
- package/dist/components/file-dropzone.d.ts +24 -0
- package/dist/components/file-dropzone.d.ts.map +1 -0
- package/dist/components/file-dropzone.js +70 -0
- package/dist/components/file-dropzone.js.map +1 -0
- package/dist/components/index.d.ts +4 -2
- package/dist/components/index.js +3 -1
- package/dist/components/marquee.d.ts +7 -1
- package/dist/components/marquee.d.ts.map +1 -1
- package/dist/components/marquee.js +33 -9
- package/dist/components/marquee.js.map +1 -1
- package/dist/components/search-combobox-option.js +24 -0
- package/dist/components/search-combobox-option.js.map +1 -0
- package/dist/components/search-combobox.d.ts +35 -0
- package/dist/components/search-combobox.d.ts.map +1 -0
- package/dist/components/search-combobox.js +128 -0
- package/dist/components/search-combobox.js.map +1 -0
- package/dist/components/theme-toggle.d.ts +13 -2
- package/dist/components/theme-toggle.d.ts.map +1 -1
- package/dist/components/theme-toggle.js +6 -3
- package/dist/components/theme-toggle.js.map +1 -1
- package/dist/elements/avatar.d.ts +1 -1
- package/dist/elements/badge.d.ts +1 -1
- package/dist/elements/button.d.ts +1 -1
- package/dist/elements/chart.d.ts +47 -0
- package/dist/elements/chart.d.ts.map +1 -0
- package/dist/elements/chart.js +151 -0
- package/dist/elements/chart.js.map +1 -0
- package/dist/elements/checkbox.d.ts +1 -1
- package/dist/elements/dialog.d.ts +1 -1
- package/dist/elements/dropdown-menu.d.ts +1 -1
- package/dist/elements/index.d.ts +2 -1
- package/dist/elements/index.js +2 -1
- package/dist/elements/label.d.ts +1 -1
- package/dist/elements/popover.d.ts +1 -1
- package/dist/elements/select.d.ts +1 -1
- package/dist/elements/separator.d.ts +1 -1
- package/dist/elements/switch.d.ts +1 -1
- package/dist/elements/table.d.ts +13 -0
- package/dist/elements/table.d.ts.map +1 -0
- package/dist/elements/table.js +68 -0
- package/dist/elements/table.js.map +1 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +4 -1
- package/dist/tokens/palettes/base.css +3 -3
- package/dist/tokens/palettes/cyberpunk.css +4 -4
- package/dist/tokens/palettes/gold.css +2 -2
- package/dist/tokens/palettes/minimal.css +1 -1
- package/package.json +11 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","names":[],"sources":["../../src/charts/types.ts"],"mappings":";;UAEiB;;EAEf;;EAEA;;EAEA;EACA;;;UAIe;EACf;EACA;EACA;;KAGU,WAAW;;UAGN;EACf;;EAEA;;EAEA,eAAe;;;;;EAKf;EACA"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ChartConfig } from "../elements/chart.js";
|
|
2
|
+
import { ChartRow, ChartSeries } from "./types.js";
|
|
3
|
+
//#region src/charts/use-cartesian-chart.d.ts
|
|
4
|
+
interface CartesianChartInput {
|
|
5
|
+
data: ChartRow[];
|
|
6
|
+
formatValue?: (value: number) => string;
|
|
7
|
+
series: ChartSeries[];
|
|
8
|
+
xKey: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Derives everything the cartesian charts share: the recharts config, the
|
|
12
|
+
* resolved ramp colours, the legend entries and the rows of the text
|
|
13
|
+
* alternative.
|
|
14
|
+
*
|
|
15
|
+
* The axes and grid are deliberately *not* abstracted into a component.
|
|
16
|
+
* recharts inspects its direct children by type to work out what to render, so
|
|
17
|
+
* wrapping `XAxis` and friends in a helper component makes them invisible to
|
|
18
|
+
* it. They stay inline in each chart on purpose.
|
|
19
|
+
*/
|
|
20
|
+
declare function useCartesianChart({ data, formatValue, series, xKey }: CartesianChartInput): {
|
|
21
|
+
colors: string[];
|
|
22
|
+
columns: string[];
|
|
23
|
+
config: ChartConfig;
|
|
24
|
+
legendItems: {
|
|
25
|
+
color: string;
|
|
26
|
+
label: string;
|
|
27
|
+
}[];
|
|
28
|
+
rows: {
|
|
29
|
+
cells: string[];
|
|
30
|
+
header: string;
|
|
31
|
+
}[];
|
|
32
|
+
};
|
|
33
|
+
//#endregion
|
|
34
|
+
export { CartesianChartInput, useCartesianChart };
|
|
35
|
+
//# sourceMappingURL=use-cartesian-chart.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-cartesian-chart.d.ts","names":[],"sources":["../../src/charts/use-cartesian-chart.ts"],"mappings":";;;UAOiB;EACf,MAAM;EACN,eAAe;EACf,QAAQ;EACR;;;;;;;;;;;;iBAac,oBACd,MACA,aACA,QACA,QACC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { rampColor } from "./chart-ramp.js";
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
//#region src/charts/use-cartesian-chart.ts
|
|
5
|
+
/**
|
|
6
|
+
* Derives everything the cartesian charts share: the recharts config, the
|
|
7
|
+
* resolved ramp colours, the legend entries and the rows of the text
|
|
8
|
+
* alternative.
|
|
9
|
+
*
|
|
10
|
+
* The axes and grid are deliberately *not* abstracted into a component.
|
|
11
|
+
* recharts inspects its direct children by type to work out what to render, so
|
|
12
|
+
* wrapping `XAxis` and friends in a helper component makes them invisible to
|
|
13
|
+
* it. They stay inline in each chart on purpose.
|
|
14
|
+
*/
|
|
15
|
+
function useCartesianChart({ data, formatValue, series, xKey }) {
|
|
16
|
+
const format = useMemo(() => formatValue ?? ((value) => value.toLocaleString()), [formatValue]);
|
|
17
|
+
const colors = useMemo(() => series.map((entry, index) => rampColor(index, entry.color)), [series]);
|
|
18
|
+
const config = useMemo(() => Object.fromEntries(series.map((entry, index) => [entry.key, {
|
|
19
|
+
color: rampColor(index, entry.color),
|
|
20
|
+
label: entry.label ?? entry.key
|
|
21
|
+
}])), [series]);
|
|
22
|
+
const legendItems = useMemo(() => series.map((entry, index) => ({
|
|
23
|
+
color: rampColor(index, entry.color),
|
|
24
|
+
label: entry.label ?? entry.key
|
|
25
|
+
})), [series]);
|
|
26
|
+
return {
|
|
27
|
+
colors,
|
|
28
|
+
columns: useMemo(() => [xKey, ...series.map((entry) => entry.label ?? entry.key)], [series, xKey]),
|
|
29
|
+
config,
|
|
30
|
+
legendItems,
|
|
31
|
+
rows: useMemo(() => data.map((row) => ({
|
|
32
|
+
cells: series.map((entry) => {
|
|
33
|
+
const value = row[entry.key];
|
|
34
|
+
return typeof value === "number" ? format(value) : String(value ?? "");
|
|
35
|
+
}),
|
|
36
|
+
header: String(row[xKey] ?? "")
|
|
37
|
+
})), [
|
|
38
|
+
data,
|
|
39
|
+
format,
|
|
40
|
+
series,
|
|
41
|
+
xKey
|
|
42
|
+
])
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
export { useCartesianChart };
|
|
47
|
+
|
|
48
|
+
//# sourceMappingURL=use-cartesian-chart.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-cartesian-chart.js","names":[],"sources":["../../src/charts/use-cartesian-chart.ts"],"sourcesContent":["\"use client\";\n\nimport { useMemo } from \"react\";\nimport { rampColor } from \"@/charts/chart-ramp\";\nimport type { ChartRow, ChartSeries } from \"@/charts/types\";\nimport type { ChartConfig } from \"@/elements/chart\";\n\nexport interface CartesianChartInput {\n data: ChartRow[];\n formatValue?: (value: number) => string;\n series: ChartSeries[];\n xKey: string;\n}\n\n/**\n * Derives everything the cartesian charts share: the recharts config, the\n * resolved ramp colours, the legend entries and the rows of the text\n * alternative.\n *\n * The axes and grid are deliberately *not* abstracted into a component.\n * recharts inspects its direct children by type to work out what to render, so\n * wrapping `XAxis` and friends in a helper component makes them invisible to\n * it. They stay inline in each chart on purpose.\n */\nexport function useCartesianChart({\n data,\n formatValue,\n series,\n xKey,\n}: CartesianChartInput) {\n const format = useMemo(\n () => formatValue ?? ((value: number) => value.toLocaleString()),\n [formatValue]\n );\n\n const colors = useMemo(\n () => series.map((entry, index) => rampColor(index, entry.color)),\n [series]\n );\n\n const config = useMemo<ChartConfig>(\n () =>\n Object.fromEntries(\n series.map((entry, index) => [\n entry.key,\n {\n color: rampColor(index, entry.color),\n label: entry.label ?? entry.key,\n },\n ])\n ),\n [series]\n );\n\n const legendItems = useMemo(\n () =>\n series.map((entry, index) => ({\n color: rampColor(index, entry.color),\n label: entry.label ?? entry.key,\n })),\n [series]\n );\n\n const columns = useMemo(\n () => [xKey, ...series.map((entry) => entry.label ?? entry.key)],\n [series, xKey]\n );\n\n const rows = useMemo(\n () =>\n data.map((row) => ({\n cells: series.map((entry) => {\n const value = row[entry.key];\n return typeof value === \"number\"\n ? format(value)\n : String(value ?? \"\");\n }),\n header: String(row[xKey] ?? \"\"),\n })),\n [data, format, series, xKey]\n );\n\n return { colors, columns, config, legendItems, rows };\n}\n"],"mappings":";;;;;;;;;;;;;;AAwBA,SAAgB,kBAAkB,EAChC,MACA,aACA,QACA,QACsB;CACtB,MAAM,SAAS,cACP,iBAAiB,UAAkB,MAAM,eAAe,IAC9D,CAAC,WAAW,CACd;CAEA,MAAM,SAAS,cACP,OAAO,KAAK,OAAO,UAAU,UAAU,OAAO,MAAM,KAAK,CAAC,GAChE,CAAC,MAAM,CACT;CAEA,MAAM,SAAS,cAEX,OAAO,YACL,OAAO,KAAK,OAAO,UAAU,CAC3B,MAAM,KACN;EACE,OAAO,UAAU,OAAO,MAAM,KAAK;EACnC,OAAO,MAAM,SAAS,MAAM;CAC9B,CACF,CAAC,CACH,GACF,CAAC,MAAM,CACT;CAEA,MAAM,cAAc,cAEhB,OAAO,KAAK,OAAO,WAAW;EAC5B,OAAO,UAAU,OAAO,MAAM,KAAK;EACnC,OAAO,MAAM,SAAS,MAAM;CAC9B,EAAE,GACJ,CAAC,MAAM,CACT;CAqBA,OAAO;EAAE;EAAQ,SAnBD,cACR,CAAC,MAAM,GAAG,OAAO,KAAK,UAAU,MAAM,SAAS,MAAM,GAAG,CAAC,GAC/D,CAAC,QAAQ,IAAI,CAiBQ;EAAG;EAAQ;EAAa,MAdlC,cAET,KAAK,KAAK,SAAS;GACjB,OAAO,OAAO,KAAK,UAAU;IAC3B,MAAM,QAAQ,IAAI,MAAM;IACxB,OAAO,OAAO,UAAU,WACpB,OAAO,KAAK,IACZ,OAAO,SAAS,EAAE;GACxB,CAAC;GACD,QAAQ,OAAO,IAAI,SAAS,EAAE;EAChC,EAAE,GACJ;GAAC;GAAM;GAAQ;GAAQ;EAAI,CAGqB;CAAE;AACtD"}
|
|
@@ -5,12 +5,15 @@ declare const chipVariants: (props?: ({
|
|
|
5
5
|
size?: "default" | "sm" | null | undefined;
|
|
6
6
|
variant?: "default" | "accent" | "muted" | "outline" | null | undefined;
|
|
7
7
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
8
|
-
type
|
|
8
|
+
type ChipOwnProps = VariantProps<typeof chipVariants> & {
|
|
9
9
|
icon?: ReactNode;
|
|
10
|
-
/** Renders as an anchor when set, keeping the same visual treatment. */
|
|
11
|
-
href?: string;
|
|
12
10
|
};
|
|
13
|
-
|
|
11
|
+
type ChipProps = ChipOwnProps & (({
|
|
12
|
+
href: string;
|
|
13
|
+
} & Omit<ComponentProps<"a">, "href">) | ({
|
|
14
|
+
href?: never;
|
|
15
|
+
} & ComponentProps<"span">));
|
|
16
|
+
declare function Chip({ className, variant, size, icon, children, ...rest }: ChipProps): import("react").JSX.Element;
|
|
14
17
|
//#endregion
|
|
15
18
|
export { Chip, ChipProps, chipVariants };
|
|
16
19
|
//# sourceMappingURL=chip.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chip.d.ts","names":[],"sources":["../../src/components/chip.tsx"],"mappings":";;;cAIM,eAAY;;;6CAoBjB;
|
|
1
|
+
{"version":3,"file":"chip.d.ts","names":[],"sources":["../../src/components/chip.tsx"],"mappings":";;;cAIM,eAAY;;;6CAoBjB;KAEI,eAAe,oBAAoB;EACtC,OAAO;;KAQG,YAAY;EAEf;IAAiB,KAAK;EACtB;IAAiB;iBAGV,OACd,WACA,SACA,MACA,MACA,aACG,QACF,4BAAS,IAAA"}
|
package/dist/components/chip.js
CHANGED
|
@@ -20,26 +20,30 @@ const chipVariants = cva("inline-flex max-w-full items-center gap-1.5 rounded-fu
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
});
|
|
23
|
-
function Chip({ className, variant, size, icon,
|
|
23
|
+
function Chip({ className, variant, size, icon, children, ...rest }) {
|
|
24
24
|
const content = /* @__PURE__ */ jsxs(Fragment, { children: [icon, /* @__PURE__ */ jsx("span", {
|
|
25
25
|
className: "truncate",
|
|
26
26
|
children
|
|
27
27
|
})] });
|
|
28
|
-
if (href
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
28
|
+
if (rest.href !== void 0) {
|
|
29
|
+
const { href, ...anchorProps } = rest;
|
|
30
|
+
return /* @__PURE__ */ jsx("a", {
|
|
31
|
+
className: cn(chipVariants({
|
|
32
|
+
size,
|
|
33
|
+
variant
|
|
34
|
+
}), "hover:bg-accent hover:text-accent-foreground", className),
|
|
35
|
+
href,
|
|
36
|
+
...anchorProps,
|
|
37
|
+
children: content
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
const { href: _omitted, ...spanProps } = rest;
|
|
37
41
|
return /* @__PURE__ */ jsx("span", {
|
|
38
42
|
className: cn(chipVariants({
|
|
39
43
|
size,
|
|
40
44
|
variant
|
|
41
45
|
}), className),
|
|
42
|
-
...
|
|
46
|
+
...spanProps,
|
|
43
47
|
children: content
|
|
44
48
|
});
|
|
45
49
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chip.js","names":[],"sources":["../../src/components/chip.tsx"],"sourcesContent":["import { cva, type VariantProps } from \"class-variance-authority\";\nimport type { ComponentProps, ReactNode } from \"react\";\nimport { cn } from \"@/lib/cn\";\n\nconst chipVariants = cva(\n \"inline-flex max-w-full items-center gap-1.5 rounded-full border font-medium text-xs transition-colors [&_svg]:size-3 [&_svg]:shrink-0\",\n {\n defaultVariants: {\n size: \"default\",\n variant: \"default\",\n },\n variants: {\n size: {\n default: \"px-2.5 py-1\",\n sm: \"px-2 py-0.5 text-[11px]\",\n },\n variant: {\n accent: \"border-transparent bg-accent text-accent-foreground\",\n default: \"border-border bg-card text-card-foreground\",\n muted: \"border-transparent bg-muted text-muted-foreground\",\n outline: \"border-border bg-transparent text-foreground\",\n },\n },\n }\n);\n\
|
|
1
|
+
{"version":3,"file":"chip.js","names":[],"sources":["../../src/components/chip.tsx"],"sourcesContent":["import { cva, type VariantProps } from \"class-variance-authority\";\nimport type { ComponentProps, ReactNode } from \"react\";\nimport { cn } from \"@/lib/cn\";\n\nconst chipVariants = cva(\n \"inline-flex max-w-full items-center gap-1.5 rounded-full border font-medium text-xs transition-colors [&_svg]:size-3 [&_svg]:shrink-0\",\n {\n defaultVariants: {\n size: \"default\",\n variant: \"default\",\n },\n variants: {\n size: {\n default: \"px-2.5 py-1\",\n sm: \"px-2 py-0.5 text-[11px]\",\n },\n variant: {\n accent: \"border-transparent bg-accent text-accent-foreground\",\n default: \"border-border bg-card text-card-foreground\",\n muted: \"border-transparent bg-muted text-muted-foreground\",\n outline: \"border-border bg-transparent text-foreground\",\n },\n },\n }\n);\n\ntype ChipOwnProps = VariantProps<typeof chipVariants> & {\n icon?: ReactNode;\n};\n\n/*\n * Discriminated on `href`, so the anchor branch accepts anchor attributes such\n * as `target` and `rel`. Typing the whole thing as a span made those a type\n * error on the very element that renders as a link.\n */\nexport type ChipProps = ChipOwnProps &\n (\n | ({ href: string } & Omit<ComponentProps<\"a\">, \"href\">)\n | ({ href?: never } & ComponentProps<\"span\">)\n );\n\nexport function Chip({\n className,\n variant,\n size,\n icon,\n children,\n ...rest\n}: ChipProps) {\n const content = (\n <>\n {icon}\n <span className=\"truncate\">{children}</span>\n </>\n );\n\n if (rest.href !== undefined) {\n const { href, ...anchorProps } = rest;\n return (\n <a\n className={cn(\n chipVariants({ size, variant }),\n \"hover:bg-accent hover:text-accent-foreground\",\n className\n )}\n href={href}\n {...anchorProps}\n >\n {content}\n </a>\n );\n }\n\n const { href: _omitted, ...spanProps } = rest;\n return (\n <span\n className={cn(chipVariants({ size, variant }), className)}\n {...spanProps}\n >\n {content}\n </span>\n );\n}\n\nexport { chipVariants };\n"],"mappings":";;;;AAIA,MAAM,eAAe,IACnB,yIACA;CACE,iBAAiB;EACf,MAAM;EACN,SAAS;CACX;CACA,UAAU;EACR,MAAM;GACJ,SAAS;GACT,IAAI;EACN;EACA,SAAS;GACP,QAAQ;GACR,SAAS;GACT,OAAO;GACP,SAAS;EACX;CACF;AACF,CACF;AAiBA,SAAgB,KAAK,EACnB,WACA,SACA,MACA,MACA,UACA,GAAG,QACS;CACZ,MAAM,UACJ,qBAAA,UAAA,EAAA,UAAA,CACG,MACD,oBAAC,QAAD;EAAM,WAAU;EAAY;CAAe,CAAA,CAC3C,EAAA,CAAA;CAGJ,IAAI,KAAK,SAAS,KAAA,GAAW;EAC3B,MAAM,EAAE,MAAM,GAAG,gBAAgB;EACjC,OACE,oBAAC,KAAD;GACE,WAAW,GACT,aAAa;IAAE;IAAM;GAAQ,CAAC,GAC9B,gDACA,SACF;GACM;GACN,GAAI;GAEH,UAAA;EACA,CAAA;CAEP;CAEA,MAAM,EAAE,MAAM,UAAU,GAAG,cAAc;CACzC,OACE,oBAAC,QAAD;EACE,WAAW,GAAG,aAAa;GAAE;GAAM;EAAQ,CAAC,GAAG,SAAS;EACxD,GAAI;EAEH,UAAA;CACG,CAAA;AAEV"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
//#region src/components/file-dropzone.d.ts
|
|
3
|
+
interface FileDropzoneProps {
|
|
4
|
+
/** Mirrors the file input's accept attribute, e.g. ".csv,.pdf". */
|
|
5
|
+
accept?: string;
|
|
6
|
+
className?: string;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
/** Secondary line, typically the accepted formats or a size limit. */
|
|
9
|
+
hint?: ReactNode;
|
|
10
|
+
icon?: ReactNode;
|
|
11
|
+
/** Primary line. Swap it for a progress message while work is in flight. */
|
|
12
|
+
label: ReactNode;
|
|
13
|
+
multiple?: boolean;
|
|
14
|
+
onFiles: (files: File[]) => void;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A drop target that is also a button, so pointer, touch and keyboard all
|
|
18
|
+
* reach the same file picker. It owns no upload logic: the caller receives the
|
|
19
|
+
* chosen files and decides what happens next.
|
|
20
|
+
*/
|
|
21
|
+
declare function FileDropzone({ accept, className, disabled, hint, icon, label, multiple, onFiles }: FileDropzoneProps): import("react").JSX.Element;
|
|
22
|
+
//#endregion
|
|
23
|
+
export { FileDropzone, FileDropzoneProps };
|
|
24
|
+
//# sourceMappingURL=file-dropzone.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-dropzone.d.ts","names":[],"sources":["../../src/components/file-dropzone.tsx"],"mappings":";;UAWiB;;EAEf;EACA;EACA;;EAEA,OAAO;EACP,OAAO;;EAEP,OAAO;EACP;EACA,UAAU,OAAO;;;;;;;iBAQH,eACd,QACA,WACA,UACA,MACA,MACA,OACA,UACA,WACC,oCAAiB,IAAA"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../lib/cn.js";
|
|
3
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { UploadIcon } from "lucide-react";
|
|
5
|
+
import { useCallback, useRef, useState } from "react";
|
|
6
|
+
//#region src/components/file-dropzone.tsx
|
|
7
|
+
/**
|
|
8
|
+
* A drop target that is also a button, so pointer, touch and keyboard all
|
|
9
|
+
* reach the same file picker. It owns no upload logic: the caller receives the
|
|
10
|
+
* chosen files and decides what happens next.
|
|
11
|
+
*/
|
|
12
|
+
function FileDropzone({ accept, className, disabled = false, hint, icon, label, multiple = false, onFiles }) {
|
|
13
|
+
const inputRef = useRef(null);
|
|
14
|
+
const [dragOver, setDragOver] = useState(false);
|
|
15
|
+
const emit = useCallback((list) => {
|
|
16
|
+
if (!list || list.length === 0) return;
|
|
17
|
+
onFiles(Array.from(list));
|
|
18
|
+
}, [onFiles]);
|
|
19
|
+
const handlePick = useCallback((event) => {
|
|
20
|
+
emit(event.target.files);
|
|
21
|
+
event.target.value = "";
|
|
22
|
+
}, [emit]);
|
|
23
|
+
const handleDrop = useCallback((event) => {
|
|
24
|
+
event.preventDefault();
|
|
25
|
+
setDragOver(false);
|
|
26
|
+
if (!disabled) emit(event.dataTransfer.files);
|
|
27
|
+
}, [disabled, emit]);
|
|
28
|
+
const handleDragOver = useCallback((event) => {
|
|
29
|
+
event.preventDefault();
|
|
30
|
+
setDragOver(true);
|
|
31
|
+
}, []);
|
|
32
|
+
const handleDragLeave = useCallback(() => setDragOver(false), []);
|
|
33
|
+
const openPicker = useCallback(() => inputRef.current?.click(), []);
|
|
34
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("button", {
|
|
35
|
+
className: cn("flex w-full flex-col items-center justify-center gap-2 rounded-md border-2 border-border border-dashed bg-card px-6 py-8 text-center transition-colors", "hover:border-primary/50 hover:bg-accent/30", "focus-visible:border-ring focus-visible:outline-hidden focus-visible:ring-[3px] focus-visible:ring-ring/50", "disabled:pointer-events-none disabled:opacity-50", dragOver && "border-primary bg-primary/5", className),
|
|
36
|
+
disabled,
|
|
37
|
+
onClick: openPicker,
|
|
38
|
+
onDragLeave: handleDragLeave,
|
|
39
|
+
onDragOver: handleDragOver,
|
|
40
|
+
onDrop: handleDrop,
|
|
41
|
+
type: "button",
|
|
42
|
+
children: [
|
|
43
|
+
icon ?? /* @__PURE__ */ jsx(UploadIcon, {
|
|
44
|
+
"aria-hidden": "true",
|
|
45
|
+
className: "size-6 text-muted-foreground"
|
|
46
|
+
}),
|
|
47
|
+
/* @__PURE__ */ jsx("span", {
|
|
48
|
+
className: "font-medium text-foreground text-sm",
|
|
49
|
+
children: label
|
|
50
|
+
}),
|
|
51
|
+
hint ? /* @__PURE__ */ jsx("span", {
|
|
52
|
+
className: "text-muted-foreground text-xs",
|
|
53
|
+
children: hint
|
|
54
|
+
}) : null
|
|
55
|
+
]
|
|
56
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
57
|
+
accept,
|
|
58
|
+
"aria-hidden": "true",
|
|
59
|
+
className: "hidden",
|
|
60
|
+
multiple,
|
|
61
|
+
onChange: handlePick,
|
|
62
|
+
ref: inputRef,
|
|
63
|
+
tabIndex: -1,
|
|
64
|
+
type: "file"
|
|
65
|
+
})] });
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
export { FileDropzone };
|
|
69
|
+
|
|
70
|
+
//# sourceMappingURL=file-dropzone.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-dropzone.js","names":[],"sources":["../../src/components/file-dropzone.tsx"],"sourcesContent":["\"use client\";\n\nimport { UploadIcon } from \"lucide-react\";\nimport type {\n DragEvent,\n ChangeEvent as ReactChangeEvent,\n ReactNode,\n} from \"react\";\nimport { useCallback, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface FileDropzoneProps {\n /** Mirrors the file input's accept attribute, e.g. \".csv,.pdf\". */\n accept?: string;\n className?: string;\n disabled?: boolean;\n /** Secondary line, typically the accepted formats or a size limit. */\n hint?: ReactNode;\n icon?: ReactNode;\n /** Primary line. Swap it for a progress message while work is in flight. */\n label: ReactNode;\n multiple?: boolean;\n onFiles: (files: File[]) => void;\n}\n\n/**\n * A drop target that is also a button, so pointer, touch and keyboard all\n * reach the same file picker. It owns no upload logic: the caller receives the\n * chosen files and decides what happens next.\n */\nexport function FileDropzone({\n accept,\n className,\n disabled = false,\n hint,\n icon,\n label,\n multiple = false,\n onFiles,\n}: FileDropzoneProps) {\n const inputRef = useRef<HTMLInputElement>(null);\n const [dragOver, setDragOver] = useState(false);\n\n const emit = useCallback(\n (list: FileList | null) => {\n if (!list || list.length === 0) {\n return;\n }\n onFiles(Array.from(list));\n },\n [onFiles]\n );\n\n const handlePick = useCallback(\n (event: ReactChangeEvent<HTMLInputElement>) => {\n emit(event.target.files);\n // Re-picking the same file fires no change event unless the value resets.\n event.target.value = \"\";\n },\n [emit]\n );\n\n const handleDrop = useCallback(\n (event: DragEvent<HTMLButtonElement>) => {\n event.preventDefault();\n setDragOver(false);\n if (!disabled) {\n emit(event.dataTransfer.files);\n }\n },\n [disabled, emit]\n );\n\n const handleDragOver = useCallback((event: DragEvent<HTMLButtonElement>) => {\n event.preventDefault();\n setDragOver(true);\n }, []);\n\n const handleDragLeave = useCallback(() => setDragOver(false), []);\n\n const openPicker = useCallback(() => inputRef.current?.click(), []);\n\n return (\n <>\n <button\n className={cn(\n \"flex w-full flex-col items-center justify-center gap-2 rounded-md border-2 border-border border-dashed bg-card px-6 py-8 text-center transition-colors\",\n \"hover:border-primary/50 hover:bg-accent/30\",\n \"focus-visible:border-ring focus-visible:outline-hidden focus-visible:ring-[3px] focus-visible:ring-ring/50\",\n \"disabled:pointer-events-none disabled:opacity-50\",\n dragOver && \"border-primary bg-primary/5\",\n className\n )}\n disabled={disabled}\n onClick={openPicker}\n onDragLeave={handleDragLeave}\n onDragOver={handleDragOver}\n onDrop={handleDrop}\n type=\"button\"\n >\n {icon ?? (\n <UploadIcon\n aria-hidden=\"true\"\n className=\"size-6 text-muted-foreground\"\n />\n )}\n <span className=\"font-medium text-foreground text-sm\">{label}</span>\n {hint ? (\n <span className=\"text-muted-foreground text-xs\">{hint}</span>\n ) : null}\n </button>\n {/*\n * The button above is the control users see and operate. This input is\n * only the mechanism that opens the picker, so it stays out of the\n * accessibility tree rather than carrying a duplicate label.\n */}\n <input\n accept={accept}\n aria-hidden=\"true\"\n className=\"hidden\"\n multiple={multiple}\n onChange={handlePick}\n ref={inputRef}\n tabIndex={-1}\n type=\"file\"\n />\n </>\n );\n}\n"],"mappings":";;;;;;;;;;;AA8BA,SAAgB,aAAa,EAC3B,QACA,WACA,WAAW,OACX,MACA,MACA,OACA,WAAW,OACX,WACoB;CACpB,MAAM,WAAW,OAAyB,IAAI;CAC9C,MAAM,CAAC,UAAU,eAAe,SAAS,KAAK;CAE9C,MAAM,OAAO,aACV,SAA0B;EACzB,IAAI,CAAC,QAAQ,KAAK,WAAW,GAC3B;EAEF,QAAQ,MAAM,KAAK,IAAI,CAAC;CAC1B,GACA,CAAC,OAAO,CACV;CAEA,MAAM,aAAa,aAChB,UAA8C;EAC7C,KAAK,MAAM,OAAO,KAAK;EAEvB,MAAM,OAAO,QAAQ;CACvB,GACA,CAAC,IAAI,CACP;CAEA,MAAM,aAAa,aAChB,UAAwC;EACvC,MAAM,eAAe;EACrB,YAAY,KAAK;EACjB,IAAI,CAAC,UACH,KAAK,MAAM,aAAa,KAAK;CAEjC,GACA,CAAC,UAAU,IAAI,CACjB;CAEA,MAAM,iBAAiB,aAAa,UAAwC;EAC1E,MAAM,eAAe;EACrB,YAAY,IAAI;CAClB,GAAG,CAAC,CAAC;CAEL,MAAM,kBAAkB,kBAAkB,YAAY,KAAK,GAAG,CAAC,CAAC;CAEhE,MAAM,aAAa,kBAAkB,SAAS,SAAS,MAAM,GAAG,CAAC,CAAC;CAElE,OACE,qBAAA,UAAA,EAAA,UAAA,CACE,qBAAC,UAAD;EACE,WAAW,GACT,0JACA,8CACA,8GACA,oDACA,YAAY,+BACZ,SACF;EACU;EACV,SAAS;EACT,aAAa;EACb,YAAY;EACZ,QAAQ;EACR,MAAK;EAdP,UAAA;GAgBG,QACC,oBAAC,YAAD;IACE,eAAY;IACZ,WAAU;GACX,CAAA;GAEH,oBAAC,QAAD;IAAM,WAAU;IAAuC,UAAA;GAAY,CAAA;GAClE,OACC,oBAAC,QAAD;IAAM,WAAU;IAAiC,UAAA;GAAW,CAAA,IAC1D;EACE;CAMR,CAAA,GAAA,oBAAC,SAAD;EACU;EACR,eAAY;EACZ,WAAU;EACA;EACV,UAAU;EACV,KAAK;EACL,UAAU;EACV,MAAK;CACN,CAAA,CACD,EAAA,CAAA;AAEN"}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Chip, ChipProps, chipVariants } from "./chip.js";
|
|
2
|
+
import { FileDropzone, FileDropzoneProps } from "./file-dropzone.js";
|
|
2
3
|
import { LanguageOption, LanguageSwitcher, LanguageSwitcherProps } from "./language-switcher.js";
|
|
3
4
|
import { Marquee, MarqueeProps } from "./marquee.js";
|
|
4
5
|
import { MessageBubble, MessageBubbleProps, MessageRole } from "./message-bubble.js";
|
|
6
|
+
import { SearchCombobox, SearchComboboxProps } from "./search-combobox.js";
|
|
5
7
|
import { StatDirection, StatTile, StatTileProps } from "./stat-tile.js";
|
|
6
8
|
import { StreamingDots, StreamingDotsProps } from "./streaming-dots.js";
|
|
7
|
-
import { ThemeMode, ThemeToggle, ThemeToggleProps } from "./theme-toggle.js";
|
|
8
|
-
export { Chip, ChipProps, LanguageOption, LanguageSwitcher, LanguageSwitcherProps, Marquee, MarqueeProps, MessageBubble, MessageBubbleProps, MessageRole, StatDirection, StatTile, StatTileProps, StreamingDots, StreamingDotsProps, ThemeMode, ThemeToggle, ThemeToggleProps, chipVariants };
|
|
9
|
+
import { ThemeMode, ThemeToggle, ThemeToggleLabels, ThemeToggleProps } from "./theme-toggle.js";
|
|
10
|
+
export { Chip, ChipProps, FileDropzone, FileDropzoneProps, LanguageOption, LanguageSwitcher, LanguageSwitcherProps, Marquee, MarqueeProps, MessageBubble, MessageBubbleProps, MessageRole, SearchCombobox, SearchComboboxProps, StatDirection, StatTile, StatTileProps, StreamingDots, StreamingDotsProps, ThemeMode, ThemeToggle, ThemeToggleLabels, ThemeToggleProps, chipVariants };
|
package/dist/components/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Chip, chipVariants } from "./chip.js";
|
|
2
|
+
import { FileDropzone } from "./file-dropzone.js";
|
|
2
3
|
import { LanguageSwitcher } from "./language-switcher.js";
|
|
3
4
|
import { Marquee } from "./marquee.js";
|
|
4
5
|
import { MessageBubble } from "./message-bubble.js";
|
|
6
|
+
import { SearchCombobox } from "./search-combobox.js";
|
|
5
7
|
import { StatTile } from "./stat-tile.js";
|
|
6
8
|
import { StreamingDots } from "./streaming-dots.js";
|
|
7
9
|
import { ThemeToggle } from "./theme-toggle.js";
|
|
8
|
-
export { Chip, LanguageSwitcher, Marquee, MessageBubble, StatTile, StreamingDots, ThemeToggle, chipVariants };
|
|
10
|
+
export { Chip, FileDropzone, LanguageSwitcher, Marquee, MessageBubble, SearchCombobox, StatTile, StreamingDots, ThemeToggle, chipVariants };
|
|
@@ -7,17 +7,23 @@ interface MarqueeProps {
|
|
|
7
7
|
duration?: number;
|
|
8
8
|
/** Fade the leading and trailing edges into the background. */
|
|
9
9
|
fade?: boolean;
|
|
10
|
+
/** Hold the strip still while the pointer is over it. */
|
|
11
|
+
pauseOnHover?: boolean;
|
|
10
12
|
/** Scroll direction. */
|
|
11
13
|
reverse?: boolean;
|
|
12
14
|
}
|
|
13
15
|
/**
|
|
14
16
|
* A continuously scrolling strip.
|
|
15
17
|
*
|
|
18
|
+
* Driven frame by frame rather than by a keyframe animation, because that is
|
|
19
|
+
* what makes `pauseOnHover` able to stop and resume mid-travel instead of
|
|
20
|
+
* snapping back to the start.
|
|
21
|
+
*
|
|
16
22
|
* Under reduced motion this suppresses the animation and becomes a scrollable
|
|
17
23
|
* region, rather than using the duration tokens. A loop has no final state to
|
|
18
24
|
* land on, so collapsing its duration to zero would be an infinite speed.
|
|
19
25
|
*/
|
|
20
|
-
declare function Marquee({ children,
|
|
26
|
+
declare function Marquee({ children, className, duration, fade, pauseOnHover, reverse }: MarqueeProps): import("react").JSX.Element;
|
|
21
27
|
//#endregion
|
|
22
28
|
export { Marquee, MarqueeProps };
|
|
23
29
|
//# sourceMappingURL=marquee.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"marquee.d.ts","names":[],"sources":["../../src/components/marquee.tsx"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"marquee.d.ts","names":[],"sources":["../../src/components/marquee.tsx"],"mappings":";;UAaiB;EACf,UAAU;EACV;;EAEA;;EAEA;;EAEA;;EAEA;;;;;;;;;;;;;iBAoBc,UACd,UACA,WACA,UACA,MACA,cACA,WACC,+BAAY,IAAA"}
|
|
@@ -1,20 +1,45 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { cn } from "../lib/cn.js";
|
|
3
3
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
-
import {
|
|
4
|
+
import { useCallback, useState } from "react";
|
|
5
|
+
import { motion, useAnimationFrame, useMotionValue, useReducedMotion, useTransform } from "motion/react";
|
|
5
6
|
//#region src/components/marquee.tsx
|
|
6
7
|
const FADE_MASK = "[mask-image:linear-gradient(to_right,transparent,black_10%,black_90%,transparent)]";
|
|
8
|
+
/** The track is duplicated, so one pass is half the total width. */
|
|
9
|
+
const PASS_PERCENT = 50;
|
|
7
10
|
/**
|
|
8
11
|
* A continuously scrolling strip.
|
|
9
12
|
*
|
|
13
|
+
* Driven frame by frame rather than by a keyframe animation, because that is
|
|
14
|
+
* what makes `pauseOnHover` able to stop and resume mid-travel instead of
|
|
15
|
+
* snapping back to the start.
|
|
16
|
+
*
|
|
10
17
|
* Under reduced motion this suppresses the animation and becomes a scrollable
|
|
11
18
|
* region, rather than using the duration tokens. A loop has no final state to
|
|
12
19
|
* land on, so collapsing its duration to zero would be an infinite speed.
|
|
13
20
|
*/
|
|
14
|
-
function Marquee({ children, duration = 30,
|
|
21
|
+
function Marquee({ children, className, duration = 30, fade = false, pauseOnHover = false, reverse = false }) {
|
|
15
22
|
const reduced = useReducedMotion();
|
|
23
|
+
const [paused, setPaused] = useState(false);
|
|
24
|
+
const offset = useMotionValue(0);
|
|
25
|
+
const translate = useTransform(offset, (value) => `${value}%`);
|
|
26
|
+
const pause = useCallback(() => setPaused(true), []);
|
|
27
|
+
const resume = useCallback(() => setPaused(false), []);
|
|
28
|
+
useAnimationFrame((_time, delta) => {
|
|
29
|
+
if (paused || reduced) return;
|
|
30
|
+
const travelled = PASS_PERCENT / (duration * 1e3) * delta;
|
|
31
|
+
const next = offset.get() + (reverse ? travelled : -travelled);
|
|
32
|
+
if (next <= -50) {
|
|
33
|
+
offset.set(next + PASS_PERCENT);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (next >= 0 && reverse) {
|
|
37
|
+
offset.set(next - PASS_PERCENT);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
offset.set(next);
|
|
41
|
+
});
|
|
16
42
|
const track = /* @__PURE__ */ jsx("div", {
|
|
17
|
-
"aria-hidden": void 0,
|
|
18
43
|
className: "flex shrink-0 items-center gap-6 pr-6",
|
|
19
44
|
children
|
|
20
45
|
});
|
|
@@ -27,14 +52,13 @@ function Marquee({ children, duration = 30, reverse = false, fade = false, class
|
|
|
27
52
|
});
|
|
28
53
|
return /* @__PURE__ */ jsx("div", {
|
|
29
54
|
className: cn("overflow-hidden", fade && FADE_MASK, className),
|
|
55
|
+
onBlurCapture: pauseOnHover ? resume : void 0,
|
|
56
|
+
onFocusCapture: pauseOnHover ? pause : void 0,
|
|
57
|
+
onPointerEnter: pauseOnHover ? pause : void 0,
|
|
58
|
+
onPointerLeave: pauseOnHover ? resume : void 0,
|
|
30
59
|
children: /* @__PURE__ */ jsxs(motion.div, {
|
|
31
|
-
animate: { x: reverse ? ["-50%", "0%"] : ["0%", "-50%"] },
|
|
32
60
|
className: "flex w-max items-center",
|
|
33
|
-
|
|
34
|
-
duration,
|
|
35
|
-
ease: "linear",
|
|
36
|
-
repeat: Number.POSITIVE_INFINITY
|
|
37
|
-
},
|
|
61
|
+
style: { x: translate },
|
|
38
62
|
children: [track, /* @__PURE__ */ jsx("div", {
|
|
39
63
|
"aria-hidden": "true",
|
|
40
64
|
className: "flex shrink-0 items-center gap-6 pr-6",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"marquee.js","names":[],"sources":["../../src/components/marquee.tsx"],"sourcesContent":["\"use client\";\n\nimport {
|
|
1
|
+
{"version":3,"file":"marquee.js","names":[],"sources":["../../src/components/marquee.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n motion,\n useAnimationFrame,\n useMotionValue,\n useReducedMotion,\n useTransform,\n} from \"motion/react\";\nimport type { ReactNode } from \"react\";\nimport { useCallback, useState } from \"react\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface MarqueeProps {\n children: ReactNode;\n className?: string;\n /** Seconds for one full pass. Larger is slower. */\n duration?: number;\n /** Fade the leading and trailing edges into the background. */\n fade?: boolean;\n /** Hold the strip still while the pointer is over it. */\n pauseOnHover?: boolean;\n /** Scroll direction. */\n reverse?: boolean;\n}\n\nconst FADE_MASK =\n \"[mask-image:linear-gradient(to_right,transparent,black_10%,black_90%,transparent)]\";\n\n/** The track is duplicated, so one pass is half the total width. */\nconst PASS_PERCENT = 50;\n\n/**\n * A continuously scrolling strip.\n *\n * Driven frame by frame rather than by a keyframe animation, because that is\n * what makes `pauseOnHover` able to stop and resume mid-travel instead of\n * snapping back to the start.\n *\n * Under reduced motion this suppresses the animation and becomes a scrollable\n * region, rather than using the duration tokens. A loop has no final state to\n * land on, so collapsing its duration to zero would be an infinite speed.\n */\nexport function Marquee({\n children,\n className,\n duration = 30,\n fade = false,\n pauseOnHover = false,\n reverse = false,\n}: MarqueeProps) {\n const reduced = useReducedMotion();\n const [paused, setPaused] = useState(false);\n const offset = useMotionValue(0);\n const translate = useTransform(offset, (value) => `${value}%`);\n\n const pause = useCallback(() => setPaused(true), []);\n const resume = useCallback(() => setPaused(false), []);\n\n useAnimationFrame((_time, delta) => {\n if (paused || reduced) {\n return;\n }\n const travelled = (PASS_PERCENT / (duration * 1000)) * delta;\n const next = offset.get() + (reverse ? travelled : -travelled);\n if (next <= -PASS_PERCENT) {\n offset.set(next + PASS_PERCENT);\n return;\n }\n if (next >= 0 && reverse) {\n offset.set(next - PASS_PERCENT);\n return;\n }\n offset.set(next);\n });\n\n const track = (\n <div className=\"flex shrink-0 items-center gap-6 pr-6\">{children}</div>\n );\n\n if (reduced) {\n return (\n <div className={cn(\"overflow-x-auto\", className)}>\n <div className=\"flex w-max items-center\">{track}</div>\n </div>\n );\n }\n\n return (\n <div\n className={cn(\"overflow-hidden\", fade && FADE_MASK, className)}\n onBlurCapture={pauseOnHover ? resume : undefined}\n onFocusCapture={pauseOnHover ? pause : undefined}\n onPointerEnter={pauseOnHover ? pause : undefined}\n onPointerLeave={pauseOnHover ? resume : undefined}\n >\n <motion.div className=\"flex w-max items-center\" style={{ x: translate }}>\n {track}\n <div\n aria-hidden=\"true\"\n className=\"flex shrink-0 items-center gap-6 pr-6\"\n >\n {children}\n </div>\n </motion.div>\n </div>\n );\n}\n"],"mappings":";;;;;;AA0BA,MAAM,YACJ;;AAGF,MAAM,eAAe;;;;;;;;;;;;AAarB,SAAgB,QAAQ,EACtB,UACA,WACA,WAAW,IACX,OAAO,OACP,eAAe,OACf,UAAU,SACK;CACf,MAAM,UAAU,iBAAiB;CACjC,MAAM,CAAC,QAAQ,aAAa,SAAS,KAAK;CAC1C,MAAM,SAAS,eAAe,CAAC;CAC/B,MAAM,YAAY,aAAa,SAAS,UAAU,GAAG,MAAM,EAAE;CAE7D,MAAM,QAAQ,kBAAkB,UAAU,IAAI,GAAG,CAAC,CAAC;CACnD,MAAM,SAAS,kBAAkB,UAAU,KAAK,GAAG,CAAC,CAAC;CAErD,mBAAmB,OAAO,UAAU;EAClC,IAAI,UAAU,SACZ;EAEF,MAAM,YAAa,gBAAgB,WAAW,OAAS;EACvD,MAAM,OAAO,OAAO,IAAI,KAAK,UAAU,YAAY,CAAC;EACpD,IAAI,QAAQ,KAAe;GACzB,OAAO,IAAI,OAAO,YAAY;GAC9B;EACF;EACA,IAAI,QAAQ,KAAK,SAAS;GACxB,OAAO,IAAI,OAAO,YAAY;GAC9B;EACF;EACA,OAAO,IAAI,IAAI;CACjB,CAAC;CAED,MAAM,QACJ,oBAAC,OAAD;EAAK,WAAU;EAAyC;CAAc,CAAA;CAGxE,IAAI,SACF,OACE,oBAAC,OAAD;EAAK,WAAW,GAAG,mBAAmB,SAAS;EAC7C,UAAA,oBAAC,OAAD;GAAK,WAAU;GAA2B,UAAA;EAAW,CAAA;CAClD,CAAA;CAIT,OACE,oBAAC,OAAD;EACE,WAAW,GAAG,mBAAmB,QAAQ,WAAW,SAAS;EAC7D,eAAe,eAAe,SAAS,KAAA;EACvC,gBAAgB,eAAe,QAAQ,KAAA;EACvC,gBAAgB,eAAe,QAAQ,KAAA;EACvC,gBAAgB,eAAe,SAAS,KAAA;EAExC,UAAA,qBAAC,OAAO,KAAR;GAAY,WAAU;GAA0B,OAAO,EAAE,GAAG,UAAU;GAAtE,UAAA,CACG,OACD,oBAAC,OAAD;IACE,eAAY;IACZ,WAAU;IAET;GACE,CAAA,CACK;;CACT,CAAA;AAET"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../lib/cn.js";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
import { useCallback } from "react";
|
|
5
|
+
//#region src/components/search-combobox-option.tsx
|
|
6
|
+
/** Internal to SearchCombobox: holds the per-option handlers so they are stable. */
|
|
7
|
+
function SearchComboboxOption({ active, id, index, onActivate, onChoose, option, render }) {
|
|
8
|
+
const handleClick = useCallback(() => onChoose(option), [onChoose, option]);
|
|
9
|
+
const handleMouseEnter = useCallback(() => onActivate(index), [index, onActivate]);
|
|
10
|
+
return /* @__PURE__ */ jsx("button", {
|
|
11
|
+
"aria-selected": active,
|
|
12
|
+
className: cn("flex w-full items-center gap-3 px-3 py-2 text-left transition-colors hover:bg-accent hover:text-accent-foreground", active && "bg-accent text-accent-foreground"),
|
|
13
|
+
id,
|
|
14
|
+
onClick: handleClick,
|
|
15
|
+
onMouseEnter: handleMouseEnter,
|
|
16
|
+
role: "option",
|
|
17
|
+
type: "button",
|
|
18
|
+
children: render(option, active)
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
22
|
+
export { SearchComboboxOption };
|
|
23
|
+
|
|
24
|
+
//# sourceMappingURL=search-combobox-option.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search-combobox-option.js","names":[],"sources":["../../src/components/search-combobox-option.tsx"],"sourcesContent":["\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { useCallback } from \"react\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface SearchComboboxOptionProps<T> {\n active: boolean;\n id: string;\n index: number;\n onActivate: (index: number) => void;\n onChoose: (option: T) => void;\n option: T;\n render: (option: T, active: boolean) => ReactNode;\n}\n\n/** Internal to SearchCombobox: holds the per-option handlers so they are stable. */\nexport function SearchComboboxOption<T>({\n active,\n id,\n index,\n onActivate,\n onChoose,\n option,\n render,\n}: SearchComboboxOptionProps<T>) {\n const handleClick = useCallback(() => onChoose(option), [onChoose, option]);\n const handleMouseEnter = useCallback(\n () => onActivate(index),\n [index, onActivate]\n );\n\n return (\n <button\n aria-selected={active}\n className={cn(\n \"flex w-full items-center gap-3 px-3 py-2 text-left transition-colors hover:bg-accent hover:text-accent-foreground\",\n active && \"bg-accent text-accent-foreground\"\n )}\n id={id}\n onClick={handleClick}\n onMouseEnter={handleMouseEnter}\n role=\"option\"\n type=\"button\"\n >\n {render(option, active)}\n </button>\n );\n}\n"],"mappings":";;;;;;AAiBA,SAAgB,qBAAwB,EACtC,QACA,IACA,OACA,YACA,UACA,QACA,UAC+B;CAC/B,MAAM,cAAc,kBAAkB,SAAS,MAAM,GAAG,CAAC,UAAU,MAAM,CAAC;CAC1E,MAAM,mBAAmB,kBACjB,WAAW,KAAK,GACtB,CAAC,OAAO,UAAU,CACpB;CAEA,OACE,oBAAC,UAAD;EACE,iBAAe;EACf,WAAW,GACT,qHACA,UAAU,kCACZ;EACI;EACJ,SAAS;EACT,cAAc;EACd,MAAK;EACL,MAAK;EAEJ,UAAA,OAAO,QAAQ,MAAM;CAChB,CAAA;AAEZ"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
//#region src/components/search-combobox.d.ts
|
|
3
|
+
interface SearchComboboxProps<T> {
|
|
4
|
+
className?: string;
|
|
5
|
+
/** Shown when the query returned nothing. */
|
|
6
|
+
emptyLabel: ReactNode;
|
|
7
|
+
/** Stable identity for each option, used for keys and aria wiring. */
|
|
8
|
+
getOptionKey: (option: T) => string;
|
|
9
|
+
icon?: ReactNode;
|
|
10
|
+
/** Accessible name for the input. A placeholder is not a label. */
|
|
11
|
+
label: string;
|
|
12
|
+
/** Replaces the icon and list with a pending message. */
|
|
13
|
+
loading?: boolean;
|
|
14
|
+
loadingLabel?: ReactNode;
|
|
15
|
+
onOpenChange?: (open: boolean) => void;
|
|
16
|
+
onQueryChange: (query: string) => void;
|
|
17
|
+
onSelect: (option: T) => void;
|
|
18
|
+
/** Already filtered. This component matches nothing on its own. */
|
|
19
|
+
options: T[];
|
|
20
|
+
placeholder?: string;
|
|
21
|
+
query: string;
|
|
22
|
+
renderOption: (option: T, active: boolean) => ReactNode;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A combobox with a listbox popup, following the ARIA authoring practices:
|
|
26
|
+
* focus stays in the input and the active option is pointed at with
|
|
27
|
+
* `aria-activedescendant` rather than moved.
|
|
28
|
+
*
|
|
29
|
+
* Controlled and source-agnostic. Filtering, fetching and what a selection
|
|
30
|
+
* does all belong to the caller.
|
|
31
|
+
*/
|
|
32
|
+
declare function SearchCombobox<T>({ className, emptyLabel, getOptionKey, icon, label, loading, loadingLabel, onOpenChange, onQueryChange, onSelect, options, placeholder, query, renderOption }: SearchComboboxProps<T>): import("react").JSX.Element;
|
|
33
|
+
//#endregion
|
|
34
|
+
export { SearchCombobox, SearchComboboxProps };
|
|
35
|
+
//# sourceMappingURL=search-combobox.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search-combobox.d.ts","names":[],"sources":["../../src/components/search-combobox.tsx"],"mappings":";;UAQiB,oBAAoB;EACnC;;EAEA,YAAY;;EAEZ,eAAe,QAAQ;EACvB,OAAO;;EAEP;;EAEA;EACA,eAAe;EACf,gBAAgB;EAChB,gBAAgB;EAChB,WAAW,QAAQ;;EAEnB,SAAS;EACT;EACA;EACA,eAAe,QAAQ,GAAG,oBAAoB;;;;;;;;;;iBAWhC,eAAe,KAC7B,WACA,YACA,cACA,MACA,OACA,SACA,cACA,cACA,eACA,UACA,SACA,aACA,OACA,gBACC,oBAAoB,qBAAE,IAAA"}
|