@lfrprazeres/ui 0.1.0 → 0.3.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.
Files changed (93) hide show
  1. package/README.md +46 -6
  2. package/dist/charts/chart-data-table.d.ts +30 -0
  3. package/dist/charts/chart-data-table.d.ts.map +1 -0
  4. package/dist/charts/chart-data-table.js +34 -0
  5. package/dist/charts/chart-data-table.js.map +1 -0
  6. package/dist/charts/chart-frame.d.ts +33 -0
  7. package/dist/charts/chart-frame.d.ts.map +1 -0
  8. package/dist/charts/chart-frame.js +35 -0
  9. package/dist/charts/chart-frame.js.map +1 -0
  10. package/dist/charts/chart-legend-list.d.ts +20 -0
  11. package/dist/charts/chart-legend-list.d.ts.map +1 -0
  12. package/dist/charts/chart-legend-list.js +38 -0
  13. package/dist/charts/chart-legend-list.js.map +1 -0
  14. package/dist/charts/chart-ramp.d.ts +7 -0
  15. package/dist/charts/chart-ramp.d.ts.map +1 -0
  16. package/dist/charts/chart-ramp.js +16 -0
  17. package/dist/charts/chart-ramp.js.map +1 -0
  18. package/dist/charts/donut-chart.d.ts +16 -0
  19. package/dist/charts/donut-chart.d.ts.map +1 -0
  20. package/dist/charts/donut-chart.js +78 -0
  21. package/dist/charts/donut-chart.js.map +1 -0
  22. package/dist/charts/index.d.ts +10 -0
  23. package/dist/charts/index.js +9 -0
  24. package/dist/charts/line-chart.d.ts +38 -0
  25. package/dist/charts/line-chart.d.ts.map +1 -0
  26. package/dist/charts/line-chart.js +89 -0
  27. package/dist/charts/line-chart.js.map +1 -0
  28. package/dist/charts/types.d.ts +35 -0
  29. package/dist/charts/types.d.ts.map +1 -0
  30. package/dist/charts/use-cartesian-chart.d.ts +35 -0
  31. package/dist/charts/use-cartesian-chart.d.ts.map +1 -0
  32. package/dist/charts/use-cartesian-chart.js +48 -0
  33. package/dist/charts/use-cartesian-chart.js.map +1 -0
  34. package/dist/components/chip.d.ts +7 -4
  35. package/dist/components/chip.d.ts.map +1 -1
  36. package/dist/components/chip.js +15 -11
  37. package/dist/components/chip.js.map +1 -1
  38. package/dist/components/file-dropzone.d.ts +24 -0
  39. package/dist/components/file-dropzone.d.ts.map +1 -0
  40. package/dist/components/file-dropzone.js +70 -0
  41. package/dist/components/file-dropzone.js.map +1 -0
  42. package/dist/components/index.d.ts +4 -2
  43. package/dist/components/index.js +3 -1
  44. package/dist/components/marquee.d.ts +7 -1
  45. package/dist/components/marquee.d.ts.map +1 -1
  46. package/dist/components/marquee.js +33 -9
  47. package/dist/components/marquee.js.map +1 -1
  48. package/dist/components/search-combobox-option.js +24 -0
  49. package/dist/components/search-combobox-option.js.map +1 -0
  50. package/dist/components/search-combobox.d.ts +35 -0
  51. package/dist/components/search-combobox.d.ts.map +1 -0
  52. package/dist/components/search-combobox.js +128 -0
  53. package/dist/components/search-combobox.js.map +1 -0
  54. package/dist/components/theme-toggle.d.ts +13 -2
  55. package/dist/components/theme-toggle.d.ts.map +1 -1
  56. package/dist/components/theme-toggle.js +6 -3
  57. package/dist/components/theme-toggle.js.map +1 -1
  58. package/dist/elements/avatar.d.ts +1 -1
  59. package/dist/elements/badge.d.ts +1 -1
  60. package/dist/elements/button.d.ts +1 -1
  61. package/dist/elements/chart.d.ts +47 -0
  62. package/dist/elements/chart.d.ts.map +1 -0
  63. package/dist/elements/chart.js +151 -0
  64. package/dist/elements/chart.js.map +1 -0
  65. package/dist/elements/checkbox.d.ts +1 -1
  66. package/dist/elements/dialog.d.ts +1 -1
  67. package/dist/elements/dropdown-menu.d.ts +1 -1
  68. package/dist/elements/index.d.ts +4 -1
  69. package/dist/elements/index.js +4 -1
  70. package/dist/elements/label.d.ts +1 -1
  71. package/dist/elements/popover.d.ts +1 -1
  72. package/dist/elements/select.d.ts +1 -1
  73. package/dist/elements/separator.d.ts +1 -1
  74. package/dist/elements/switch.d.ts +1 -1
  75. package/dist/elements/table.d.ts +13 -0
  76. package/dist/elements/table.d.ts.map +1 -0
  77. package/dist/elements/table.js +68 -0
  78. package/dist/elements/table.js.map +1 -0
  79. package/dist/elements/toggle-group.d.ts +12 -0
  80. package/dist/elements/toggle-group.d.ts.map +1 -0
  81. package/dist/elements/toggle-group.js +50 -0
  82. package/dist/elements/toggle-group.js.map +1 -0
  83. package/dist/elements/toggle.d.ts +12 -0
  84. package/dist/elements/toggle.d.ts.map +1 -0
  85. package/dist/elements/toggle.js +38 -0
  86. package/dist/elements/toggle.js.map +1 -0
  87. package/dist/index.d.ts +7 -2
  88. package/dist/index.js +6 -1
  89. package/dist/tokens/palettes/base.css +3 -3
  90. package/dist/tokens/palettes/cyberpunk.css +4 -4
  91. package/dist/tokens/palettes/gold.css +2 -2
  92. package/dist/tokens/palettes/minimal.css +1 -1
  93. package/package.json +11 -2
@@ -0,0 +1,35 @@
1
+ //#region src/charts/types.d.ts
2
+ /** Shared prop shapes for the chart layer. */
3
+ interface ChartSlice {
4
+ /** Overrides this slice's position in the chart ramp. */
5
+ color?: string;
6
+ /** Display text, when it differs from the identity. */
7
+ label?: string;
8
+ /** Stable identity, and the default display text. */
9
+ name: string;
10
+ value: number;
11
+ }
12
+ /** One plotted series in a cartesian chart, keyed into each row. */
13
+ interface ChartSeries {
14
+ color?: string;
15
+ key: string;
16
+ label?: string;
17
+ }
18
+ type ChartRow = Record<string, number | string | null | undefined>;
19
+ /** Every chart takes these. `label` is required on purpose. */
20
+ interface ChartBaseProps {
21
+ className?: string;
22
+ /** Message shown instead of the chart when there is nothing to draw. */
23
+ emptyLabel?: string;
24
+ /** Formats values in the tooltip, legend and data table. */
25
+ formatValue?: (value: number) => string;
26
+ /**
27
+ * Accessible name for the chart. Required because a chart with no name is
28
+ * announced as "image" and nothing else.
29
+ */
30
+ label: string;
31
+ legend?: boolean;
32
+ }
33
+ //#endregion
34
+ export { ChartBaseProps, ChartRow, ChartSeries, ChartSlice };
35
+ //# sourceMappingURL=types.d.ts.map
@@ -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 ChipProps = ComponentProps<"span"> & VariantProps<typeof chipVariants> & {
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
- declare function Chip({ className, variant, size, icon, href, children, ...props }: ChipProps): import("react").JSX.Element;
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;KAEW,YAAY,yBACtB,oBAAoB;EAClB,OAAO;;EAEP;;iBAGY,OACd,WACA,SACA,MACA,MACA,MACA,aACG,SACF,4BAAS,IAAA"}
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"}
@@ -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, href, children, ...props }) {
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) return /* @__PURE__ */ jsx("a", {
29
- className: cn(chipVariants({
30
- size,
31
- variant
32
- }), "hover:bg-accent hover:text-accent-foreground", className),
33
- href,
34
- ...props,
35
- children: content
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
- ...props,
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\nexport type ChipProps = ComponentProps<\"span\"> &\n VariantProps<typeof chipVariants> & {\n icon?: ReactNode;\n /** Renders as an anchor when set, keeping the same visual treatment. */\n href?: string;\n };\n\nexport function Chip({\n className,\n variant,\n size,\n icon,\n href,\n children,\n ...props\n}: ChipProps) {\n const content = (\n <>\n {icon}\n <span className=\"truncate\">{children}</span>\n </>\n );\n\n if (href) {\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 {...(props as ComponentProps<\"a\">)}\n >\n {content}\n </a>\n );\n }\n\n return (\n <span className={cn(chipVariants({ size, variant }), className)} {...props}>\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;AASA,SAAgB,KAAK,EACnB,WACA,SACA,MACA,MACA,MACA,UACA,GAAG,SACS;CACZ,MAAM,UACJ,qBAAA,UAAA,EAAA,UAAA,CACG,MACD,oBAAC,QAAD;EAAM,WAAU;EAAY;CAAe,CAAA,CAC3C,EAAA,CAAA;CAGJ,IAAI,MACF,OACE,oBAAC,KAAD;EACE,WAAW,GACT,aAAa;GAAE;GAAM;EAAQ,CAAC,GAC9B,gDACA,SACF;EACM;EACN,GAAK;EAEJ,UAAA;CACA,CAAA;CAIP,OACE,oBAAC,QAAD;EAAM,WAAW,GAAG,aAAa;GAAE;GAAM;EAAQ,CAAC,GAAG,SAAS;EAAG,GAAI;EAClE,UAAA;CACG,CAAA;AAEV"}
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 };
@@ -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, duration, reverse, fade, className }: MarqueeProps): import("react").JSX.Element;
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":";;UAMiB;EACf,UAAU;EACV;;EAEA;;EAEA;;EAEA;;;;;;;;;iBAac,UACd,UACA,UACA,SACA,MACA,aACC,+BAAY,IAAA"}
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 { motion, useReducedMotion } from "motion/react";
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, reverse = false, fade = false, className }) {
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
- transition: {
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 { motion, useReducedMotion } from \"motion/react\";\nimport type { ReactNode } 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 /** 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/**\n * A continuously scrolling strip.\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 duration = 30,\n reverse = false,\n fade = false,\n className,\n}: MarqueeProps) {\n const reduced = useReducedMotion();\n\n const track = (\n <div\n aria-hidden={undefined}\n className=\"flex shrink-0 items-center gap-6 pr-6\"\n >\n {children}\n </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 className={cn(\"overflow-hidden\", fade && FADE_MASK, className)}>\n <motion.div\n animate={{ x: reverse ? [\"-50%\", \"0%\"] : [\"0%\", \"-50%\"] }}\n className=\"flex w-max items-center\"\n transition={{\n duration,\n ease: \"linear\",\n repeat: Number.POSITIVE_INFINITY,\n }}\n >\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":";;;;;AAiBA,MAAM,YACJ;;;;;;;;AASF,SAAgB,QAAQ,EACtB,UACA,WAAW,IACX,UAAU,OACV,OAAO,OACP,aACe;CACf,MAAM,UAAU,iBAAiB;CAEjC,MAAM,QACJ,oBAAC,OAAD;EACE,eAAa,KAAA;EACb,WAAU;EAET;CACE,CAAA;CAGP,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;EAAK,WAAW,GAAG,mBAAmB,QAAQ,WAAW,SAAS;EAChE,UAAA,qBAAC,OAAO,KAAR;GACE,SAAS,EAAE,GAAG,UAAU,CAAC,QAAQ,IAAI,IAAI,CAAC,MAAM,MAAM,EAAE;GACxD,WAAU;GACV,YAAY;IACV;IACA,MAAM;IACN,QAAQ,OAAO;GACjB;GAPF,UAAA,CASG,OACD,oBAAC,OAAD;IACE,eAAY;IACZ,WAAU;IAET;GACE,CAAA,CACK;;CACT,CAAA;AAET"}
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"}