@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.
- package/README.md +16 -1
- package/dist/components/accordion.d.ts +10 -0
- package/dist/components/accordion.d.ts.map +1 -0
- package/dist/components/accordion.js +21 -0
- package/dist/components/accordion.js.map +1 -0
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/button.d.ts +1 -1
- package/dist/components/chip.d.ts +40 -0
- package/dist/components/chip.d.ts.map +1 -0
- package/dist/components/chip.js +23 -0
- package/dist/components/chip.js.map +1 -0
- package/dist/components/dropzone.d.ts +38 -0
- package/dist/components/dropzone.d.ts.map +1 -0
- package/dist/components/dropzone.js +67 -0
- package/dist/components/dropzone.js.map +1 -0
- package/dist/components/empty-state.d.ts +24 -0
- package/dist/components/empty-state.d.ts.map +1 -0
- package/dist/components/empty-state.js +21 -0
- package/dist/components/empty-state.js.map +1 -0
- package/dist/components/multi-select.d.ts +44 -0
- package/dist/components/multi-select.d.ts.map +1 -0
- package/dist/components/multi-select.js +72 -0
- package/dist/components/multi-select.js.map +1 -0
- package/dist/components/popover.d.ts +10 -0
- package/dist/components/popover.d.ts.map +1 -0
- package/dist/components/popover.js +20 -0
- package/dist/components/popover.js.map +1 -0
- package/dist/components/progress.d.ts +18 -0
- package/dist/components/progress.d.ts.map +1 -0
- package/dist/components/progress.js +18 -0
- package/dist/components/progress.js.map +1 -0
- package/dist/components/segmented-control.d.ts +37 -0
- package/dist/components/segmented-control.d.ts.map +1 -0
- package/dist/components/segmented-control.js +26 -0
- package/dist/components/segmented-control.js.map +1 -0
- package/dist/components/stat.d.ts +27 -0
- package/dist/components/stat.d.ts.map +1 -0
- package/dist/components/stat.js +23 -0
- package/dist/components/stat.js.map +1 -0
- package/dist/components/tabs.d.ts +2 -2
- package/package.json +1 -1
- package/src/components/accordion.tsx +55 -0
- package/src/components/chip.tsx +111 -0
- package/src/components/dropzone.tsx +124 -0
- package/src/components/empty-state.tsx +56 -0
- package/src/components/multi-select.tsx +206 -0
- package/src/components/popover.tsx +44 -0
- package/src/components/progress.tsx +50 -0
- package/src/components/segmented-control.tsx +89 -0
- 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?: "
|
|
7
|
-
orientation?: "
|
|
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
|
@@ -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 }
|