@strategicos/ui 0.5.0 → 0.6.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 (50) hide show
  1. package/README.md +16 -1
  2. package/dist/components/accordion.d.ts +10 -0
  3. package/dist/components/accordion.d.ts.map +1 -0
  4. package/dist/components/accordion.js +21 -0
  5. package/dist/components/accordion.js.map +1 -0
  6. package/dist/components/badge.d.ts +1 -1
  7. package/dist/components/button.d.ts +1 -1
  8. package/dist/components/chip.d.ts +40 -0
  9. package/dist/components/chip.d.ts.map +1 -0
  10. package/dist/components/chip.js +23 -0
  11. package/dist/components/chip.js.map +1 -0
  12. package/dist/components/dropzone.d.ts +38 -0
  13. package/dist/components/dropzone.d.ts.map +1 -0
  14. package/dist/components/dropzone.js +67 -0
  15. package/dist/components/dropzone.js.map +1 -0
  16. package/dist/components/empty-state.d.ts +24 -0
  17. package/dist/components/empty-state.d.ts.map +1 -0
  18. package/dist/components/empty-state.js +21 -0
  19. package/dist/components/empty-state.js.map +1 -0
  20. package/dist/components/multi-select.d.ts +44 -0
  21. package/dist/components/multi-select.d.ts.map +1 -0
  22. package/dist/components/multi-select.js +72 -0
  23. package/dist/components/multi-select.js.map +1 -0
  24. package/dist/components/popover.d.ts +10 -0
  25. package/dist/components/popover.d.ts.map +1 -0
  26. package/dist/components/popover.js +20 -0
  27. package/dist/components/popover.js.map +1 -0
  28. package/dist/components/progress.d.ts +18 -0
  29. package/dist/components/progress.d.ts.map +1 -0
  30. package/dist/components/progress.js +18 -0
  31. package/dist/components/progress.js.map +1 -0
  32. package/dist/components/segmented-control.d.ts +37 -0
  33. package/dist/components/segmented-control.d.ts.map +1 -0
  34. package/dist/components/segmented-control.js +26 -0
  35. package/dist/components/segmented-control.js.map +1 -0
  36. package/dist/components/stat.d.ts +27 -0
  37. package/dist/components/stat.d.ts.map +1 -0
  38. package/dist/components/stat.js +23 -0
  39. package/dist/components/stat.js.map +1 -0
  40. package/dist/components/tabs.d.ts +2 -2
  41. package/package.json +1 -1
  42. package/src/components/accordion.tsx +55 -0
  43. package/src/components/chip.tsx +111 -0
  44. package/src/components/dropzone.tsx +124 -0
  45. package/src/components/empty-state.tsx +56 -0
  46. package/src/components/multi-select.tsx +206 -0
  47. package/src/components/popover.tsx +44 -0
  48. package/src/components/progress.tsx +50 -0
  49. package/src/components/segmented-control.tsx +89 -0
  50. package/src/components/stat.tsx +65 -0
@@ -0,0 +1,37 @@
1
+ import * as React from "react";
2
+ export type SegmentedControlItem = {
3
+ value: string;
4
+ label: React.ReactNode;
5
+ /** Ícone antes do rótulo. */
6
+ icon?: React.ReactNode;
7
+ disabled?: boolean;
8
+ /** Rótulo acessível quando `label` não é texto. */
9
+ "aria-label"?: string;
10
+ };
11
+ /**
12
+ * Escolha exclusiva entre poucas opções, como o SegmentedControl do Mantine:
13
+ * trilho cinza com a opção ativa destacada. Para trocar o MODO de ver o mesmo
14
+ * conteúdo (Lista | Quadro, 30d | 60d | 90d). Para navegar entre conteúdos
15
+ * diferentes, use Tabs.
16
+ *
17
+ * <SegmentedControl
18
+ * value={view}
19
+ * onValueChange={setView}
20
+ * data={[{ value: "lista", label: "Lista" }, { value: "quadro", label: "Quadro" }]}
21
+ * />
22
+ *
23
+ * Sempre há uma opção ativa: clicar na ativa não a desmarca.
24
+ */
25
+ declare function SegmentedControl({ data, value, onValueChange, size, fullWidth, disabled, className, "aria-label": ariaLabel, }: {
26
+ data: SegmentedControlItem[];
27
+ value: string;
28
+ onValueChange: (value: string) => void;
29
+ size?: "sm" | "default";
30
+ /** Ocupa a largura disponível, com as opções em partes iguais. */
31
+ fullWidth?: boolean;
32
+ disabled?: boolean;
33
+ className?: string;
34
+ "aria-label"?: string;
35
+ }): React.JSX.Element;
36
+ export { SegmentedControl };
37
+ //# sourceMappingURL=segmented-control.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"segmented-control.d.ts","sourceRoot":"","sources":["../../src/components/segmented-control.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAK9B,MAAM,MAAM,oBAAoB,GAAG;IACjC,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,KAAK,CAAC,SAAS,CAAA;IACtB,6BAA6B;IAC7B,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,mDAAmD;IACnD,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB,CAAA;AAED;;;;;;;;;;;;;GAaG;AACH,iBAAS,gBAAgB,CAAC,EACxB,IAAI,EACJ,KAAK,EACL,aAAa,EACb,IAAgB,EAChB,SAAiB,EACjB,QAAQ,EACR,SAAS,EACT,YAAY,EAAE,SAAS,GACxB,EAAE;IACD,IAAI,EAAE,oBAAoB,EAAE,CAAA;IAC5B,KAAK,EAAE,MAAM,CAAA;IACb,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACtC,IAAI,CAAC,EAAE,IAAI,GAAG,SAAS,CAAA;IACvB,kEAAkE;IAClE,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB,qBAoCA;AAED,OAAO,EAAE,gBAAgB,EAAE,CAAA"}
@@ -0,0 +1,26 @@
1
+ "use client";
2
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
3
+ import { ToggleGroup as ToggleGroupPrimitive } from "radix-ui";
4
+ import { cn } from "../lib/utils.js";
5
+ /**
6
+ * Escolha exclusiva entre poucas opções, como o SegmentedControl do Mantine:
7
+ * trilho cinza com a opção ativa destacada. Para trocar o MODO de ver o mesmo
8
+ * conteúdo (Lista | Quadro, 30d | 60d | 90d). Para navegar entre conteúdos
9
+ * diferentes, use Tabs.
10
+ *
11
+ * <SegmentedControl
12
+ * value={view}
13
+ * onValueChange={setView}
14
+ * data={[{ value: "lista", label: "Lista" }, { value: "quadro", label: "Quadro" }]}
15
+ * />
16
+ *
17
+ * Sempre há uma opção ativa: clicar na ativa não a desmarca.
18
+ */
19
+ function SegmentedControl({ data, value, onValueChange, size = "default", fullWidth = false, disabled, className, "aria-label": ariaLabel, }) {
20
+ return (_jsx(ToggleGroupPrimitive.Root, { type: "single", value: value,
21
+ // O Radix desmarca ao clicar na opção ativa (valor vazio): ignora.
22
+ onValueChange: v => { if (v)
23
+ onValueChange(v); }, disabled: disabled, "aria-label": ariaLabel, "data-slot": "segmented-control", className: cn("inline-flex items-center gap-0.5 rounded-lg bg-muted p-0.5 text-muted-foreground", size === "sm" ? "h-7.5" : "h-9", fullWidth ? "flex w-full" : "w-fit", className), children: data.map(item => (_jsxs(ToggleGroupPrimitive.Item, { value: item.value, disabled: item.disabled, "aria-label": item["aria-label"], "data-slot": "segmented-control-item", className: cn("inline-flex h-full cursor-pointer items-center justify-center gap-1.5 rounded-md px-3 font-medium whitespace-nowrap transition-colors outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[state=on]:bg-background data-[state=on]:text-foreground data-[state=on]:shadow-sm dark:data-[state=on]:bg-input/60 [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", size === "sm" ? "text-xs" : "text-sm", fullWidth && "flex-1"), children: [item.icon, item.label] }, item.value))) }));
24
+ }
25
+ export { SegmentedControl };
26
+ //# sourceMappingURL=segmented-control.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"segmented-control.js","sourceRoot":"","sources":["../../src/components/segmented-control.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAGZ,OAAO,EAAE,WAAW,IAAI,oBAAoB,EAAE,MAAM,UAAU,CAAA;AAE9D,OAAO,EAAE,EAAE,EAAE,MAAM,iBAAiB,CAAA;AAYpC;;;;;;;;;;;;;GAaG;AACH,SAAS,gBAAgB,CAAC,EACxB,IAAI,EACJ,KAAK,EACL,aAAa,EACb,IAAI,GAAG,SAAS,EAChB,SAAS,GAAG,KAAK,EACjB,QAAQ,EACR,SAAS,EACT,YAAY,EAAE,SAAS,GAWxB;IACC,OAAO,CACL,KAAC,oBAAoB,CAAC,IAAI,IACxB,IAAI,EAAC,QAAQ,EACb,KAAK,EAAE,KAAK;QACZ,mEAAmE;QACnE,aAAa,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC;YAAE,aAAa,CAAC,CAAC,CAAC,CAAA,CAAC,CAAC,EAC/C,QAAQ,EAAE,QAAQ,gBACN,SAAS,eACX,mBAAmB,EAC7B,SAAS,EAAE,EAAE,CACX,kFAAkF,EAClF,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,EAC/B,SAAS,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,OAAO,EACnC,SAAS,CACV,YAEA,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAChB,MAAC,oBAAoB,CAAC,IAAI,IAExB,KAAK,EAAE,IAAI,CAAC,KAAK,EACjB,QAAQ,EAAE,IAAI,CAAC,QAAQ,gBACX,IAAI,CAAC,YAAY,CAAC,eACpB,wBAAwB,EAClC,SAAS,EAAE,EAAE,CACX,ybAAyb,EACzb,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,EACrC,SAAS,IAAI,QAAQ,CACtB,aAEA,IAAI,CAAC,IAAI,EACT,IAAI,CAAC,KAAK,KAZN,IAAI,CAAC,KAAK,CAaW,CAC7B,CAAC,GACwB,CAC7B,CAAA;AACH,CAAC;AAED,OAAO,EAAE,gBAAgB,EAAE,CAAA"}
@@ -0,0 +1,27 @@
1
+ import * as React from "react";
2
+ /**
3
+ * Indicador numérico, como os "StatsGrid" do Mantine: rótulo, valor em
4
+ * destaque, contexto e, opcionalmente, um selo de variação e um ícone.
5
+ *
6
+ * <Stat
7
+ * label="Total após a reforma"
8
+ * value="R$ 1.890.000"
9
+ * description="2024 — primeira fase"
10
+ * delta="−R$ 560k"
11
+ * deltaTone="success"
12
+ * icon={<TrendingDown />}
13
+ * />
14
+ *
15
+ * Use num grid (ex.: `grid gap-4 sm:grid-cols-2 xl:grid-cols-4`).
16
+ */
17
+ declare function Stat({ label, value, description, delta, deltaTone, icon, className, ...props }: Omit<React.ComponentProps<"div">, "children"> & {
18
+ label: React.ReactNode;
19
+ value: React.ReactNode;
20
+ description?: React.ReactNode;
21
+ /** Variação ou etiqueta curta, num selo. */
22
+ delta?: React.ReactNode;
23
+ deltaTone?: "secondary" | "success" | "warning" | "destructive" | "info";
24
+ icon?: React.ReactNode;
25
+ }): React.JSX.Element;
26
+ export { Stat };
27
+ //# sourceMappingURL=stat.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stat.d.ts","sourceRoot":"","sources":["../../src/components/stat.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAK9B;;;;;;;;;;;;;;GAcG;AACH,iBAAS,IAAI,CAAC,EACZ,KAAK,EACL,KAAK,EACL,WAAW,EACX,KAAK,EACL,SAAuB,EACvB,IAAI,EACJ,SAAS,EACT,GAAG,KAAK,EACT,EAAE,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,EAAE,UAAU,CAAC,GAAG;IACjD,KAAK,EAAE,KAAK,CAAC,SAAS,CAAA;IACtB,KAAK,EAAE,KAAK,CAAC,SAAS,CAAA;IACtB,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC7B,4CAA4C;IAC5C,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACvB,SAAS,CAAC,EAAE,WAAW,GAAG,SAAS,GAAG,SAAS,GAAG,aAAa,GAAG,MAAM,CAAA;IACxE,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CACvB,qBAyBA;AAED,OAAO,EAAE,IAAI,EAAE,CAAA"}
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cn } from "../lib/utils.js";
3
+ import { Badge } from "./badge.js";
4
+ /**
5
+ * Indicador numérico, como os "StatsGrid" do Mantine: rótulo, valor em
6
+ * destaque, contexto e, opcionalmente, um selo de variação e um ícone.
7
+ *
8
+ * <Stat
9
+ * label="Total após a reforma"
10
+ * value="R$ 1.890.000"
11
+ * description="2024 — primeira fase"
12
+ * delta="−R$ 560k"
13
+ * deltaTone="success"
14
+ * icon={<TrendingDown />}
15
+ * />
16
+ *
17
+ * Use num grid (ex.: `grid gap-4 sm:grid-cols-2 xl:grid-cols-4`).
18
+ */
19
+ function Stat({ label, value, description, delta, deltaTone = "secondary", icon, className, ...props }) {
20
+ return (_jsxs("div", { "data-slot": "stat", className: cn("flex flex-col gap-1 rounded-lg border border-border bg-card p-4 shadow-card", className), ...props, children: [_jsxs("div", { className: "flex items-start justify-between gap-3", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: label }), icon && (_jsx("span", { className: "flex size-8 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground [&_svg]:size-4", children: icon }))] }), _jsx("p", { className: "text-2xl leading-tight font-semibold text-foreground tabular-nums", children: value }), (description || delta) && (_jsxs("div", { className: "mt-1 flex flex-wrap items-center gap-2", children: [delta && _jsx(Badge, { variant: deltaTone, className: "normal-case", children: delta }), description && _jsx("span", { className: "text-xs text-muted-foreground", children: description })] }))] }));
21
+ }
22
+ export { Stat };
23
+ //# sourceMappingURL=stat.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stat.js","sourceRoot":"","sources":["../../src/components/stat.tsx"],"names":[],"mappings":";AAEA,OAAO,EAAE,EAAE,EAAE,MAAM,iBAAiB,CAAA;AACpC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAElC;;;;;;;;;;;;;;GAcG;AACH,SAAS,IAAI,CAAC,EACZ,KAAK,EACL,KAAK,EACL,WAAW,EACX,KAAK,EACL,SAAS,GAAG,WAAW,EACvB,IAAI,EACJ,SAAS,EACT,GAAG,KAAK,EAST;IACC,OAAO,CACL,4BACY,MAAM,EAChB,SAAS,EAAE,EAAE,CAAC,6EAA6E,EAAE,SAAS,CAAC,KACnG,KAAK,aAET,eAAK,SAAS,EAAC,wCAAwC,aACrD,YAAG,SAAS,EAAC,+BAA+B,YAAE,KAAK,GAAK,EACvD,IAAI,IAAI,CACP,eAAM,SAAS,EAAC,2GAA2G,YACxH,IAAI,GACA,CACR,IACG,EACN,YAAG,SAAS,EAAC,mEAAmE,YAAE,KAAK,GAAK,EAC3F,CAAC,WAAW,IAAI,KAAK,CAAC,IAAI,CACzB,eAAK,SAAS,EAAC,wCAAwC,aAEpD,KAAK,IAAI,KAAC,KAAK,IAAC,OAAO,EAAE,SAAS,EAAE,SAAS,EAAC,aAAa,YAAE,KAAK,GAAS,EAC3E,WAAW,IAAI,eAAM,SAAS,EAAC,+BAA+B,YAAE,WAAW,GAAQ,IAChF,CACP,IACG,CACP,CAAA;AACH,CAAC;AAED,OAAO,EAAE,IAAI,EAAE,CAAA"}
@@ -3,8 +3,8 @@ import { type VariantProps } from "class-variance-authority";
3
3
  import { Tabs as TabsPrimitive } from "radix-ui";
4
4
  declare function Tabs({ className, orientation, ...props }: React.ComponentProps<typeof TabsPrimitive.Root>): React.JSX.Element;
5
5
  declare const tabsListVariants: (props?: ({
6
- variant?: "default" | "line" | null | undefined;
7
- orientation?: "horizontal" | "vertical" | null | undefined;
6
+ variant?: "line" | "default" | null | undefined;
7
+ orientation?: "vertical" | "horizontal" | null | undefined;
8
8
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
9
9
  declare function TabsList({ className, variant, ...props }: React.ComponentProps<typeof TabsPrimitive.List> & Omit<VariantProps<typeof tabsListVariants>, "orientation">): React.JSX.Element;
10
10
  declare function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>): React.JSX.Element;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@strategicos/ui",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Componentes React da Strategicos (shadcn + Radix + Tailwind v4).",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -0,0 +1,55 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Accordion as AccordionPrimitive } from "radix-ui"
5
+ import { ChevronDownIcon } from "lucide-react"
6
+
7
+ import { cn } from "../lib/utils.js"
8
+
9
+ /** Seções que abrem e fecham (Radix Accordion), no estilo do Accordion do
10
+ * Mantine: itens separados por fio, chevron à direita. */
11
+ function Accordion(props: React.ComponentProps<typeof AccordionPrimitive.Root>) {
12
+ return <AccordionPrimitive.Root data-slot="accordion" {...props} />
13
+ }
14
+
15
+ function AccordionItem({ className, ...props }: React.ComponentProps<typeof AccordionPrimitive.Item>) {
16
+ return (
17
+ <AccordionPrimitive.Item
18
+ data-slot="accordion-item"
19
+ className={cn("border-b border-border last:border-b-0", className)}
20
+ {...props}
21
+ />
22
+ )
23
+ }
24
+
25
+ function AccordionTrigger({ className, children, ...props }: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
26
+ return (
27
+ <AccordionPrimitive.Header className="flex">
28
+ <AccordionPrimitive.Trigger
29
+ data-slot="accordion-trigger"
30
+ className={cn(
31
+ "group/accordion flex flex-1 cursor-pointer items-center justify-between gap-4 py-3 text-left text-sm font-medium text-foreground transition-colors outline-none hover:text-foreground/80 focus-visible:ring-2 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 [&_svg]:shrink-0",
32
+ className
33
+ )}
34
+ {...props}
35
+ >
36
+ {children}
37
+ <ChevronDownIcon className="size-4 text-muted-foreground transition-transform duration-200 group-data-[state=open]/accordion:rotate-180" />
38
+ </AccordionPrimitive.Trigger>
39
+ </AccordionPrimitive.Header>
40
+ )
41
+ }
42
+
43
+ function AccordionContent({ className, children, ...props }: React.ComponentProps<typeof AccordionPrimitive.Content>) {
44
+ return (
45
+ <AccordionPrimitive.Content
46
+ data-slot="accordion-content"
47
+ className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
48
+ {...props}
49
+ >
50
+ <div className={cn("pb-4", className)}>{children}</div>
51
+ </AccordionPrimitive.Content>
52
+ )
53
+ }
54
+
55
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
@@ -0,0 +1,111 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { ToggleGroup as ToggleGroupPrimitive } from "radix-ui"
5
+ import { CheckIcon } from "lucide-react"
6
+
7
+ import { cn } from "../lib/utils.js"
8
+
9
+ /**
10
+ * Pílulas selecionáveis, como o Chip do Mantine: filtros por categoria, status
11
+ * e afins. Selecionada, a pílula ganha o fundo claro da cor primária e um ✓.
12
+ *
13
+ * <ChipGroup type="single" value={categoria} onValueChange={setCategoria}>
14
+ * <Chip value="todas">Todas</Chip>
15
+ * <Chip value="legislacao">Legislação</Chip>
16
+ * </ChipGroup>
17
+ *
18
+ * <ChipGroup type="multiple" value={status} onValueChange={setStatus}>
19
+ * <Chip value="todo" count={8}>A fazer</Chip>
20
+ * </ChipGroup>
21
+ *
22
+ * Em `single`, clicar na selecionada não a desmarca (sempre há uma ativa).
23
+ * Os valores não podem ser vazios: o Radix trata "" como "nada selecionado".
24
+ * `scrollable` deixa o grupo numa linha só, rolando na horizontal no celular.
25
+ */
26
+
27
+ type ChipGroupProps =
28
+ | {
29
+ type: "single"
30
+ value: string
31
+ onValueChange: (value: string) => void
32
+ }
33
+ | {
34
+ type: "multiple"
35
+ value: string[]
36
+ onValueChange: (value: string[]) => void
37
+ }
38
+
39
+ function ChipGroup({
40
+ className,
41
+ children,
42
+ scrollable = false,
43
+ "aria-label": ariaLabel,
44
+ ...props
45
+ }: ChipGroupProps & {
46
+ className?: string
47
+ children: React.ReactNode
48
+ scrollable?: boolean
49
+ "aria-label"?: string
50
+ }) {
51
+ const shared = {
52
+ "aria-label": ariaLabel,
53
+ "data-slot": "chip-group",
54
+ className: cn(
55
+ "flex gap-1.5",
56
+ scrollable ? "no-scrollbar -mx-1 overflow-x-auto px-1 py-0.5" : "flex-wrap",
57
+ className
58
+ ),
59
+ }
60
+ if (props.type === "single") {
61
+ const { value, onValueChange } = props
62
+ return (
63
+ <ToggleGroupPrimitive.Root
64
+ type="single"
65
+ value={value}
66
+ onValueChange={v => { if (v) onValueChange(v) }}
67
+ {...shared}
68
+ >
69
+ {children}
70
+ </ToggleGroupPrimitive.Root>
71
+ )
72
+ }
73
+ return (
74
+ <ToggleGroupPrimitive.Root type="multiple" value={props.value} onValueChange={props.onValueChange} {...shared}>
75
+ {children}
76
+ </ToggleGroupPrimitive.Root>
77
+ )
78
+ }
79
+
80
+ function Chip({
81
+ value,
82
+ count,
83
+ className,
84
+ children,
85
+ ...props
86
+ }: React.ComponentProps<typeof ToggleGroupPrimitive.Item> & {
87
+ /** Número exibido depois do rótulo (ex.: quantidade de itens). */
88
+ count?: number
89
+ }) {
90
+ return (
91
+ <ToggleGroupPrimitive.Item
92
+ value={value}
93
+ data-slot="chip"
94
+ className={cn(
95
+ "group/chip inline-flex h-7.5 shrink-0 cursor-pointer items-center gap-1.5 rounded-full border border-border bg-background px-3 text-sm whitespace-nowrap text-foreground transition-colors outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[state=on]:border-primary/50 data-[state=on]:bg-primary/10 data-[state=on]:text-primary-strong dark:bg-transparent",
96
+ className
97
+ )}
98
+ {...props}
99
+ >
100
+ <CheckIcon className="hidden size-3.5 shrink-0 stroke-3 group-data-[state=on]/chip:block" />
101
+ {children}
102
+ {count !== undefined && (
103
+ <span className="text-xs text-muted-foreground tabular-nums group-data-[state=on]/chip:text-primary-strong/80">
104
+ {count}
105
+ </span>
106
+ )}
107
+ </ToggleGroupPrimitive.Item>
108
+ )
109
+ }
110
+
111
+ export { ChipGroup, Chip }
@@ -0,0 +1,124 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { UploadIcon } from "lucide-react"
5
+
6
+ import { cn } from "../lib/utils.js"
7
+
8
+ /**
9
+ * Área de envio de arquivos, como o Dropzone do Mantine: arrastar e soltar, ou
10
+ * clicar (e Enter/Espaço) para abrir o seletor do sistema.
11
+ *
12
+ * <Dropzone
13
+ * accept=".txt,.xml,.zip"
14
+ * maxSize={50 * 1024 * 1024}
15
+ * onDrop={files => enviar(files)}
16
+ * onReject={msg => toast.error(msg)}
17
+ * title="Importar arquivos fiscais"
18
+ * description="SPED (.txt), NF-e (.xml) ou pacote .zip, até 50 MB"
19
+ * />
20
+ *
21
+ * `children` substitui o conteúdo padrão (ícone, título e descrição).
22
+ * A validação de tipo e tamanho é só uma conveniência: o servidor valida de
23
+ * novo.
24
+ */
25
+ function Dropzone({
26
+ onDrop,
27
+ onReject,
28
+ accept,
29
+ multiple = false,
30
+ maxSize,
31
+ disabled = false,
32
+ loading = false,
33
+ title = "Arraste arquivos ou clique para escolher",
34
+ description,
35
+ icon,
36
+ className,
37
+ children,
38
+ }: {
39
+ onDrop: (files: File[]) => void
40
+ /** Chamado com uma mensagem quando algum arquivo é recusado (tipo/tamanho). */
41
+ onReject?: (message: string) => void
42
+ /** Mesmo formato do atributo `accept` do input (ex.: ".pdf,image/*"). */
43
+ accept?: string
44
+ multiple?: boolean
45
+ /** Tamanho máximo por arquivo, em bytes. */
46
+ maxSize?: number
47
+ disabled?: boolean
48
+ /** Enquanto envia: bloqueia novos envios e mostra o estado ocupado. */
49
+ loading?: boolean
50
+ title?: React.ReactNode
51
+ description?: React.ReactNode
52
+ icon?: React.ReactNode
53
+ className?: string
54
+ children?: React.ReactNode
55
+ }) {
56
+ const inputRef = React.useRef<HTMLInputElement>(null)
57
+ const [dragging, setDragging] = React.useState(false)
58
+ const blocked = disabled || loading
59
+
60
+ function matchesAccept(file: File) {
61
+ if (!accept) return true
62
+ const name = file.name.toLowerCase()
63
+ return accept.split(",").map(s => s.trim().toLowerCase()).some(rule => {
64
+ if (rule.startsWith(".")) return name.endsWith(rule)
65
+ if (rule.endsWith("/*")) return file.type.startsWith(rule.slice(0, -1))
66
+ return file.type === rule
67
+ })
68
+ }
69
+
70
+ function handleFiles(list: FileList | null) {
71
+ if (!list || list.length === 0 || blocked) return
72
+ const files = Array.from(list).slice(0, multiple ? undefined : 1)
73
+ const ok: File[] = []
74
+ for (const f of files) {
75
+ if (!matchesAccept(f)) onReject?.(`"${f.name}" não é de um tipo aceito.`)
76
+ else if (maxSize && f.size > maxSize) onReject?.(`"${f.name}" passa do limite de ${Math.round(maxSize / 1024 / 1024)} MB.`)
77
+ else ok.push(f)
78
+ }
79
+ if (ok.length) onDrop(ok)
80
+ }
81
+
82
+ return (
83
+ <div
84
+ role="button"
85
+ tabIndex={blocked ? -1 : 0}
86
+ aria-disabled={blocked || undefined}
87
+ aria-busy={loading || undefined}
88
+ data-slot="dropzone"
89
+ data-dragging={dragging || undefined}
90
+ onClick={() => !blocked && inputRef.current?.click()}
91
+ onKeyDown={e => {
92
+ if (blocked) return
93
+ if (e.key === "Enter" || e.key === " ") { e.preventDefault(); inputRef.current?.click() }
94
+ }}
95
+ onDragOver={e => { e.preventDefault(); if (!blocked) setDragging(true) }}
96
+ onDragLeave={e => { if (!e.currentTarget.contains(e.relatedTarget as Node)) setDragging(false) }}
97
+ onDrop={e => { e.preventDefault(); setDragging(false); handleFiles(e.dataTransfer.files) }}
98
+ className={cn(
99
+ "flex cursor-pointer flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed border-border px-6 py-8 text-center transition-colors outline-none hover:border-primary/50 hover:bg-muted/40 focus-visible:ring-2 focus-visible:ring-ring/50 data-dragging:border-primary data-dragging:bg-primary/5 aria-disabled:cursor-not-allowed aria-disabled:opacity-60 aria-disabled:hover:border-border aria-disabled:hover:bg-transparent",
100
+ className
101
+ )}
102
+ >
103
+ <input
104
+ ref={inputRef}
105
+ type="file"
106
+ accept={accept}
107
+ multiple={multiple}
108
+ className="hidden"
109
+ onChange={e => { handleFiles(e.target.files); e.target.value = "" }}
110
+ />
111
+ {children ?? (
112
+ <>
113
+ <span className="flex size-10 items-center justify-center rounded-full bg-muted text-muted-foreground [&_svg]:size-5">
114
+ {icon ?? <UploadIcon />}
115
+ </span>
116
+ <p className="text-sm font-medium text-foreground">{title}</p>
117
+ {description && <p className="text-xs text-muted-foreground">{description}</p>}
118
+ </>
119
+ )}
120
+ </div>
121
+ )
122
+ }
123
+
124
+ export { Dropzone }
@@ -0,0 +1,56 @@
1
+ import * as React from "react"
2
+
3
+ import { cn } from "../lib/utils.js"
4
+
5
+ /**
6
+ * Estado vazio: ícone, título, explicação e uma ação. Para listas sem itens,
7
+ * buscas sem resultado e telas que ainda não têm conteúdo.
8
+ *
9
+ * <EmptyState
10
+ * icon={<FileText />}
11
+ * title="Nenhum documento ainda"
12
+ * description="Crie um documento ou importe uma apresentação para começar."
13
+ * action={<Button><Plus /> Novo documento</Button>}
14
+ * />
15
+ *
16
+ * `bordered` desenha a moldura tracejada, para quando o vazio ocupa o lugar de
17
+ * uma área (lista, quadro) e precisa mostrar o contorno dela.
18
+ */
19
+ function EmptyState({
20
+ icon,
21
+ title,
22
+ description,
23
+ action,
24
+ bordered = false,
25
+ className,
26
+ ...props
27
+ }: Omit<React.ComponentProps<"div">, "title"> & {
28
+ icon?: React.ReactNode
29
+ title: React.ReactNode
30
+ description?: React.ReactNode
31
+ action?: React.ReactNode
32
+ bordered?: boolean
33
+ }) {
34
+ return (
35
+ <div
36
+ data-slot="empty-state"
37
+ className={cn(
38
+ "flex flex-col items-center justify-center gap-2 px-6 py-12 text-center",
39
+ bordered && "rounded-lg border-2 border-dashed border-border",
40
+ className
41
+ )}
42
+ {...props}
43
+ >
44
+ {icon && (
45
+ <span className="mb-1 flex size-12 items-center justify-center rounded-full bg-muted text-muted-foreground [&_svg]:size-6">
46
+ {icon}
47
+ </span>
48
+ )}
49
+ <p className="text-base font-semibold text-foreground">{title}</p>
50
+ {description && <p className="max-w-sm text-sm text-muted-foreground">{description}</p>}
51
+ {action && <div className="mt-3 flex flex-wrap items-center justify-center gap-2">{action}</div>}
52
+ </div>
53
+ )
54
+ }
55
+
56
+ export { EmptyState }