@aoctech/ui 0.1.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 +123 -0
- package/dist/components/alert.d.ts +12 -0
- package/dist/components/alert.js +18 -0
- package/dist/components/badge.d.ts +19 -0
- package/dist/components/badge.js +43 -0
- package/dist/components/button.d.ts +9 -0
- package/dist/components/button.js +51 -0
- package/dist/components/checkbox.d.ts +3 -0
- package/dist/components/checkbox.js +11 -0
- package/dist/components/date-picker.d.ts +29 -0
- package/dist/components/date-picker.js +56 -0
- package/dist/components/empty-state.d.ts +16 -0
- package/dist/components/empty-state.js +6 -0
- package/dist/components/field.d.ts +12 -0
- package/dist/components/field.js +7 -0
- package/dist/components/input.d.ts +3 -0
- package/dist/components/input.js +6 -0
- package/dist/components/modal.d.ts +31 -0
- package/dist/components/modal.js +27 -0
- package/dist/components/page-header.d.ts +17 -0
- package/dist/components/page-header.js +6 -0
- package/dist/components/radio.d.ts +5 -0
- package/dist/components/radio.js +12 -0
- package/dist/components/separator.d.ts +3 -0
- package/dist/components/separator.js +8 -0
- package/dist/components/skeleton.d.ts +11 -0
- package/dist/components/skeleton.js +14 -0
- package/dist/components/switch.d.ts +3 -0
- package/dist/components/switch.js +8 -0
- package/dist/components/theme-provider.d.ts +20 -0
- package/dist/components/theme-provider.js +16 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +16 -0
- package/dist/lib/cn.d.ts +7 -0
- package/dist/lib/cn.js +10 -0
- package/dist/styles.css +2 -0
- package/dist/themes.css +65 -0
- package/dist/tokens.css +42 -0
- package/package.json +65 -0
package/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# @aoctech/ui
|
|
2
|
+
|
|
3
|
+
The CTech component vocabulary, in one place. Base UI provides accessible behaviour; this package
|
|
4
|
+
owns the semantic token contract and ships official themes for Account, DFE, Wallet, Poker and
|
|
5
|
+
Billing. Each product keeps its identity without forking interactions or accessibility.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm i @aoctech/ui
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## The consumer supplies two things
|
|
12
|
+
|
|
13
|
+
### 1. The official theme contract
|
|
14
|
+
|
|
15
|
+
Import the shared tokens once and scope an official theme at the product root. The application may
|
|
16
|
+
extend a theme, but it must not redefine semantic status meaning.
|
|
17
|
+
|
|
18
|
+
```css
|
|
19
|
+
@import "tailwindcss";
|
|
20
|
+
@import "@aoctech/ui/styles.css";
|
|
21
|
+
|
|
22
|
+
@theme inline {
|
|
23
|
+
--color-background: var(--background);
|
|
24
|
+
--color-surface: var(--surface);
|
|
25
|
+
--color-foreground: var(--foreground);
|
|
26
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
27
|
+
--color-border: var(--border);
|
|
28
|
+
--color-ring: var(--ring);
|
|
29
|
+
--color-brand-50: var(--brand-50);
|
|
30
|
+
--color-brand-600: var(--brand-600);
|
|
31
|
+
--color-brand-700: var(--brand-700);
|
|
32
|
+
--color-success: var(--success);
|
|
33
|
+
--color-warning: var(--warning);
|
|
34
|
+
--color-danger: var(--danger);
|
|
35
|
+
--color-danger-strong: var(--danger-strong);
|
|
36
|
+
--shadow-modal: var(--shadow-modal);
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
import {ThemeProvider} from "@aoctech/ui"
|
|
42
|
+
|
|
43
|
+
export function App() {
|
|
44
|
+
return <ThemeProvider theme="billing"><BillingApp /></ThemeProvider>
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 2. The `@source` line
|
|
49
|
+
|
|
50
|
+
Tailwind v4 only emits classes it can see. It does not scan `node_modules` by default, so without
|
|
51
|
+
this the components ship with no styles at all — the single most likely thing to go wrong on first
|
|
52
|
+
install.
|
|
53
|
+
|
|
54
|
+
```css
|
|
55
|
+
@source "../node_modules/@aoctech/ui/dist";
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Theme and status are separate contracts
|
|
59
|
+
|
|
60
|
+
`account`, `dfe`, `wallet`, `poker` and `billing` change action, focus, surfaces and brand hue.
|
|
61
|
+
The `success`, `warning` and `danger` roles remain consistent. A new product theme is added only
|
|
62
|
+
after its palette and contrast states are documented in Storybook.
|
|
63
|
+
|
|
64
|
+
## Density is a property of the surface
|
|
65
|
+
|
|
66
|
+
Controls are touch-sized by default (44px). A dense surface opts down by setting one attribute on
|
|
67
|
+
its root, and every control inside follows:
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
<div data-density="compact">…</div> {/* 32px controls */}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
That is why `Button` has no `size="console"`: height is decided by where a button is, not by each
|
|
74
|
+
call site remembering which screen it is on.
|
|
75
|
+
|
|
76
|
+
## Brand colour and status colour never meet
|
|
77
|
+
|
|
78
|
+
`Button variant="brand"` is the only place brand colour appears. `Badge` carries the four status
|
|
79
|
+
tones — `neutral`, `positive`, `attention`, `urgent` — and they are **fixed**: never recoloured to
|
|
80
|
+
match a surface's accent.
|
|
81
|
+
|
|
82
|
+
The reason is legibility across the family. An operator who learns that a green badge means settled
|
|
83
|
+
has to be able to read that on every screen of every CTech app; a tone that shifts with the surface
|
|
84
|
+
is a tone that has to be relearned per app. It also keeps warm-brand apps honest — billing's
|
|
85
|
+
terracotta sits near its own danger red, and the rule is what stops the two ever rendering on one
|
|
86
|
+
component. (Separating them by role was necessary and not sufficient: billing also had to pull its
|
|
87
|
+
brand chroma down until `danger` was the only saturated colour on a screen.)
|
|
88
|
+
|
|
89
|
+
The four tones are the same closed set the CTech APIs emit alongside a human-readable state string,
|
|
90
|
+
so a badge renders what the server said rather than a client-side mapping of an internal enum.
|
|
91
|
+
|
|
92
|
+
## What's here
|
|
93
|
+
|
|
94
|
+
| Component | Notes |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `Button` | Variants `brand` · `outline` · `ghost` · `danger` · `link`. Density-aware. |
|
|
97
|
+
| `Input` + `Field` | Visible labels, hint/error copy and invalid state vocabulary. |
|
|
98
|
+
| `Alert` | Info, success, warning and danger feedback. |
|
|
99
|
+
| `Badge` | The four status tones. Pair with a glyph — never colour alone. |
|
|
100
|
+
| `Modal` | Base UI `Dialog`: real focus trap, scroll lock, restore focus, Escape. |
|
|
101
|
+
| `PageHeader` | One h1, one optional line, one optional action. |
|
|
102
|
+
| `EmptyState` | Title, description, optional action. |
|
|
103
|
+
| `Skeleton` | Honours `prefers-reduced-motion`. |
|
|
104
|
+
| `Separator` | Horizontal and vertical. |
|
|
105
|
+
|
|
106
|
+
Deliberately small. Components arrive when a second app needs one, not in anticipation — the whole
|
|
107
|
+
point of extracting this was to stop maintaining four copies, and a component with one consumer is
|
|
108
|
+
still one copy.
|
|
109
|
+
|
|
110
|
+
The extraction queue and non-domain boundaries live in [docs/component-inventory.md](docs/component-inventory.md). The source palette references and the rules for adding a product live in [docs/theme-registry.md](docs/theme-registry.md).
|
|
111
|
+
|
|
112
|
+
## Storybook
|
|
113
|
+
|
|
114
|
+
```sh
|
|
115
|
+
npm run storybook
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The catalogue includes a theme registry and a state-coverage page. Its toolbar switches all five
|
|
119
|
+
themes and density modes, which makes visual drift visible before a component release.
|
|
120
|
+
|
|
121
|
+
## Peers
|
|
122
|
+
|
|
123
|
+
`react >=18`, `react-dom >=18`, `@base-ui/react >=1.7`, and Tailwind v4 in the consuming app.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { type VariantProps } from "class-variance-authority";
|
|
3
|
+
declare const alertVariants: (props?: ({
|
|
4
|
+
tone?: "danger" | "info" | "success" | "warning" | null | undefined;
|
|
5
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
6
|
+
interface AlertProps extends VariantProps<typeof alertVariants> {
|
|
7
|
+
title: string;
|
|
8
|
+
children?: ReactNode;
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
declare function Alert({ title, children, tone, className }: AlertProps): import("react").JSX.Element;
|
|
12
|
+
export { Alert, alertVariants, type AlertProps };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cva } from "class-variance-authority";
|
|
3
|
+
import { cn } from "../lib/cn";
|
|
4
|
+
const alertVariants = cva("flex gap-3 rounded-lg border px-4 py-3 text-sm", {
|
|
5
|
+
variants: {
|
|
6
|
+
tone: {
|
|
7
|
+
info: "border-border bg-surface text-foreground",
|
|
8
|
+
success: "border-success/25 bg-success/10 text-success",
|
|
9
|
+
warning: "border-warning/25 bg-warning/10 text-warning",
|
|
10
|
+
danger: "border-danger/25 bg-danger/10 text-danger",
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
defaultVariants: { tone: "info" },
|
|
14
|
+
});
|
|
15
|
+
function Alert({ title, children, tone, className }) {
|
|
16
|
+
return (_jsx("div", { "data-slot": "alert", role: tone === "danger" ? "alert" : "status", className: cn(alertVariants({ tone }), className), children: _jsxs("div", { className: "grid gap-1", children: [_jsx("p", { className: "font-medium", children: title }), children && _jsx("div", { children: children })] }) }));
|
|
17
|
+
}
|
|
18
|
+
export { Alert, alertVariants };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
2
|
+
import { type VariantProps } from "class-variance-authority";
|
|
3
|
+
/**
|
|
4
|
+
* Status vocabulary. The four tones are the closed set every CTech API that
|
|
5
|
+
* describes a state emits, so a badge renders a server-supplied tone rather
|
|
6
|
+
* than a client-side mapping of an internal enum.
|
|
7
|
+
*
|
|
8
|
+
* These are fixed. They are never recoloured to match a surface's brand
|
|
9
|
+
* accent: an operator has to read "paid" the same way on every screen of
|
|
10
|
+
* every app, and a tone that shifts with the surface is a tone that has to be
|
|
11
|
+
* relearned. Brand lives on the button; status lives here; they never meet on
|
|
12
|
+
* one component.
|
|
13
|
+
*/
|
|
14
|
+
declare const badgeVariants: (props?: ({
|
|
15
|
+
tone?: "neutral" | "positive" | "attention" | "urgent" | null | undefined;
|
|
16
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
17
|
+
type BadgeTone = NonNullable<VariantProps<typeof badgeVariants>["tone"]>;
|
|
18
|
+
declare function Badge({ className, tone, render, ...props }: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
19
|
+
export { Badge, badgeVariants, type BadgeTone };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { mergeProps } from "@base-ui/react/merge-props";
|
|
3
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
4
|
+
import { cva } from "class-variance-authority";
|
|
5
|
+
import { cn } from "../lib/cn";
|
|
6
|
+
/**
|
|
7
|
+
* Status vocabulary. The four tones are the closed set every CTech API that
|
|
8
|
+
* describes a state emits, so a badge renders a server-supplied tone rather
|
|
9
|
+
* than a client-side mapping of an internal enum.
|
|
10
|
+
*
|
|
11
|
+
* These are fixed. They are never recoloured to match a surface's brand
|
|
12
|
+
* accent: an operator has to read "paid" the same way on every screen of
|
|
13
|
+
* every app, and a tone that shifts with the surface is a tone that has to be
|
|
14
|
+
* relearned. Brand lives on the button; status lives here; they never meet on
|
|
15
|
+
* one component.
|
|
16
|
+
*/
|
|
17
|
+
const badgeVariants = cva([
|
|
18
|
+
"inline-flex w-fit shrink-0 items-center justify-center gap-1.5",
|
|
19
|
+
"rounded-full border px-2.5 py-0.5",
|
|
20
|
+
"text-xs font-medium whitespace-nowrap",
|
|
21
|
+
"[&>svg]:pointer-events-none [&>svg]:size-3",
|
|
22
|
+
], {
|
|
23
|
+
variants: {
|
|
24
|
+
tone: {
|
|
25
|
+
neutral: "border-border bg-surface text-muted-foreground",
|
|
26
|
+
positive: "border-success/20 bg-success/10 text-success",
|
|
27
|
+
attention: "border-warning/20 bg-warning/10 text-warning",
|
|
28
|
+
urgent: "border-danger/20 bg-danger/10 text-danger",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
defaultVariants: {
|
|
32
|
+
tone: "neutral",
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
function Badge({ className, tone, render, ...props }) {
|
|
36
|
+
return useRender({
|
|
37
|
+
defaultTagName: "span",
|
|
38
|
+
props: mergeProps({ className: cn(badgeVariants({ tone }), className) }, props),
|
|
39
|
+
render,
|
|
40
|
+
state: { slot: "badge", tone },
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
export { Badge, badgeVariants };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Button as ButtonPrimitive } from "@base-ui/react/button";
|
|
2
|
+
import { type VariantProps } from "class-variance-authority";
|
|
3
|
+
declare const buttonVariants: (props?: ({
|
|
4
|
+
variant?: "link" | "brand" | "outline" | "ghost" | "danger" | null | undefined;
|
|
5
|
+
size?: "default" | "sm" | "icon" | null | undefined;
|
|
6
|
+
block?: boolean | null | undefined;
|
|
7
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
8
|
+
declare function Button({ className, variant, size, block, nativeButton, ...props }: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>): import("react").JSX.Element;
|
|
9
|
+
export { Button, buttonVariants };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { Button as ButtonPrimitive } from "@base-ui/react/button";
|
|
4
|
+
import { cva } from "class-variance-authority";
|
|
5
|
+
import { cn } from "../lib/cn";
|
|
6
|
+
const buttonVariants = cva([
|
|
7
|
+
"group/button inline-flex shrink-0 items-center justify-center gap-1.5",
|
|
8
|
+
"rounded-lg border border-transparent bg-clip-padding",
|
|
9
|
+
"text-sm font-medium whitespace-nowrap",
|
|
10
|
+
"transition-colors duration-150 outline-none select-none",
|
|
11
|
+
"focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:border-ring",
|
|
12
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
13
|
+
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
14
|
+
], {
|
|
15
|
+
variants: {
|
|
16
|
+
variant: {
|
|
17
|
+
// The brand fill. This is the only place brand colour appears on a
|
|
18
|
+
// button, and status colour never appears here at all — see the
|
|
19
|
+
// package README on why the two vocabularies stay separated.
|
|
20
|
+
brand: "bg-brand-600 text-white hover:bg-brand-700",
|
|
21
|
+
outline: "border-border bg-background text-foreground hover:bg-surface",
|
|
22
|
+
ghost: "text-foreground hover:bg-surface",
|
|
23
|
+
danger: "bg-danger text-white hover:bg-danger-strong",
|
|
24
|
+
link: "text-brand-600 underline-offset-4 hover:underline",
|
|
25
|
+
},
|
|
26
|
+
size: {
|
|
27
|
+
// Height is a property of the surface, not of the call site. The shell
|
|
28
|
+
// sets `data-density` on its root and every button follows; a portal
|
|
29
|
+
// button is touch-sized and the same component in the console is not.
|
|
30
|
+
default: "h-11 px-3.5 in-data-[density=compact]:h-8 in-data-[density=compact]:px-2.5",
|
|
31
|
+
sm: "h-9 px-3 text-xs in-data-[density=compact]:h-7 in-data-[density=compact]:px-2.5",
|
|
32
|
+
icon: "size-11 in-data-[density=compact]:size-8",
|
|
33
|
+
},
|
|
34
|
+
block: {
|
|
35
|
+
true: "w-full",
|
|
36
|
+
false: "",
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
defaultVariants: {
|
|
40
|
+
variant: "brand",
|
|
41
|
+
size: "default",
|
|
42
|
+
block: false,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
function Button({ className, variant, size, block, nativeButton, ...props }) {
|
|
46
|
+
return (_jsx(ButtonPrimitive, { "data-slot": "button",
|
|
47
|
+
// A custom `render` element (a Link, say) is never a native <button>, so
|
|
48
|
+
// default nativeButton to false there instead of warning on every usage.
|
|
49
|
+
nativeButton: nativeButton ?? !props.render, className: cn(buttonVariants({ variant, size, block, className })), ...props }));
|
|
50
|
+
}
|
|
51
|
+
export { Button, buttonVariants };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox";
|
|
4
|
+
import { cn } from "../lib/cn";
|
|
5
|
+
function Checkbox({ className, ...props }) {
|
|
6
|
+
return (_jsx(CheckboxPrimitive.Root, { "data-slot": "checkbox", className: cn("grid size-5 shrink-0 place-items-center rounded-md border border-border bg-background text-white outline-none", "transition-[background-color,border-color,box-shadow] duration-150", "data-[checked]:border-brand-600 data-[checked]:bg-brand-600", "data-[indeterminate]:border-brand-600 data-[indeterminate]:bg-brand-600", "focus-visible:ring-3 focus-visible:ring-ring/35 disabled:cursor-not-allowed disabled:opacity-50", className), ...props, children: _jsx(CheckboxPrimitive.Indicator, { className: "grid place-items-center data-[unchecked]:hidden", keepMounted: true, children: _jsx(CheckGlyph, {}) }) }));
|
|
7
|
+
}
|
|
8
|
+
function CheckGlyph() {
|
|
9
|
+
return _jsx("svg", { viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 2.25, "aria-hidden": true, className: "size-3.5", children: _jsx("path", { d: "m3.25 8.25 2.9 2.9 6.6-6.5", strokeLinecap: "round", strokeLinejoin: "round" }) });
|
|
10
|
+
}
|
|
11
|
+
export { Checkbox };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Matcher } from "react-day-picker";
|
|
2
|
+
interface CalendarProps {
|
|
3
|
+
selected?: Date;
|
|
4
|
+
onSelect?: (date: Date | undefined) => void;
|
|
5
|
+
disabled?: Matcher | Matcher[];
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
/** An inline, keyboard-operable calendar. Use DatePicker for a field trigger. */
|
|
9
|
+
declare function Calendar({ selected, onSelect, disabled, className }: CalendarProps): import("react").JSX.Element;
|
|
10
|
+
interface DatePickerProps {
|
|
11
|
+
value?: Date;
|
|
12
|
+
defaultValue?: Date;
|
|
13
|
+
onValueChange?: (date: Date | undefined) => void;
|
|
14
|
+
placeholder?: string;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
id?: string;
|
|
17
|
+
name?: string;
|
|
18
|
+
className?: string;
|
|
19
|
+
min?: Date;
|
|
20
|
+
max?: Date;
|
|
21
|
+
defaultOpen?: boolean;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A single-date field backed by an accessible Popover + Calendar. Dates remain
|
|
25
|
+
* Date objects at the API boundary so products can apply their own timezone
|
|
26
|
+
* and serialization policies explicitly.
|
|
27
|
+
*/
|
|
28
|
+
declare function DatePicker({ value, defaultValue, onValueChange, placeholder, disabled, id, name, className, min, max, defaultOpen, }: DatePickerProps): import("react").JSX.Element;
|
|
29
|
+
export { Calendar, DatePicker, type CalendarProps, type DatePickerProps };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Popover } from "@base-ui/react/popover";
|
|
4
|
+
import { ptBR } from "date-fns/locale/pt-BR";
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
import { DayPicker } from "react-day-picker";
|
|
7
|
+
import { cn } from "../lib/cn";
|
|
8
|
+
import { useThemeScope } from "./theme-provider";
|
|
9
|
+
/** An inline, keyboard-operable calendar. Use DatePicker for a field trigger. */
|
|
10
|
+
function Calendar({ selected, onSelect, disabled, className }) {
|
|
11
|
+
return (_jsx(DayPicker, { mode: "single", selected: selected, onSelect: onSelect, disabled: disabled, locale: ptBR, classNames: {
|
|
12
|
+
month_caption: "calendar-month-caption",
|
|
13
|
+
caption_label: "calendar-caption-label",
|
|
14
|
+
nav: "calendar-nav",
|
|
15
|
+
button_next: "calendar-button-next",
|
|
16
|
+
button_previous: "calendar-button-previous",
|
|
17
|
+
chevron: "calendar-chevron",
|
|
18
|
+
month_grid: "calendar-month-grid",
|
|
19
|
+
weekday: "calendar-weekday",
|
|
20
|
+
day: "calendar-day",
|
|
21
|
+
day_button: "calendar-day-button",
|
|
22
|
+
}, className: cn("relative min-w-[17.5rem] p-3 text-sm text-foreground", "[&_.calendar-month-caption]:flex [&_.calendar-month-caption]:h-9 [&_.calendar-month-caption]:w-full [&_.calendar-month-caption]:items-center [&_.calendar-month-caption]:justify-center [&_.calendar-month-caption]:px-10 [&_.calendar-caption-label]:truncate [&_.calendar-caption-label]:font-semibold", "[&_.calendar-nav]:pointer-events-none [&_.calendar-nav]:absolute [&_.calendar-nav]:inset-x-3 [&_.calendar-nav]:top-3 [&_.calendar-nav]:flex [&_.calendar-nav]:justify-between", "[&_.calendar-button-next]:pointer-events-auto [&_.calendar-button-next]:grid [&_.calendar-button-next]:size-9 [&_.calendar-button-next]:place-items-center [&_.calendar-button-next]:rounded-md [&_.calendar-button-next]:hover:bg-surface", "[&_.calendar-button-previous]:pointer-events-auto [&_.calendar-button-previous]:grid [&_.calendar-button-previous]:size-9 [&_.calendar-button-previous]:place-items-center [&_.calendar-button-previous]:rounded-md [&_.calendar-button-previous]:hover:bg-surface", "[&_.calendar-chevron]:size-4 [&_.calendar-chevron]:fill-none [&_.calendar-chevron]:stroke-current", "[&_.calendar-month-grid]:w-full [&_.calendar-month-grid]:border-collapse [&_.calendar-weekday]:h-9 [&_.calendar-weekday]:text-xs [&_.calendar-weekday]:font-medium [&_.calendar-weekday]:text-muted-foreground", "[&_.calendar-day]:size-9 [&_.calendar-day-button]:grid [&_.calendar-day-button]:size-9 [&_.calendar-day-button]:place-items-center [&_.calendar-day-button]:rounded-md [&_.calendar-day-button]:outline-none [&_.calendar-day-button]:hover:bg-surface [&_.calendar-day-button]:focus-visible:ring-3 [&_.calendar-day-button]:focus-visible:ring-ring/35", "[&_.calendar-day[data-selected]_button]:bg-brand-600 [&_.calendar-day[data-selected]_button]:text-white [&_.calendar-day[data-today]:not([data-selected])_button]:font-semibold [&_.calendar-day[data-today]:not([data-selected])_button]:text-brand-600", "[&_.calendar-day[data-outside]_button]:text-muted-foreground/60 [&_.calendar-day[data-disabled]_button]:cursor-not-allowed [&_.calendar-day[data-disabled]_button]:opacity-40", className) }));
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A single-date field backed by an accessible Popover + Calendar. Dates remain
|
|
26
|
+
* Date objects at the API boundary so products can apply their own timezone
|
|
27
|
+
* and serialization policies explicitly.
|
|
28
|
+
*/
|
|
29
|
+
function DatePicker({ value, defaultValue, onValueChange, placeholder = "Selecionar data", disabled = false, id, name, className, min, max, defaultOpen = false, }) {
|
|
30
|
+
const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
|
|
31
|
+
const [open, setOpen] = useState(defaultOpen);
|
|
32
|
+
const { theme, density } = useThemeScope();
|
|
33
|
+
const selected = value ?? uncontrolledValue;
|
|
34
|
+
const disabledDays = [
|
|
35
|
+
...(min ? [{ before: min }] : []),
|
|
36
|
+
...(max ? [{ after: max }] : []),
|
|
37
|
+
];
|
|
38
|
+
const label = selected
|
|
39
|
+
? new Intl.DateTimeFormat("pt-BR", { day: "2-digit", month: "long", year: "numeric" }).format(selected)
|
|
40
|
+
: placeholder;
|
|
41
|
+
function selectDate(date) {
|
|
42
|
+
if (value === undefined)
|
|
43
|
+
setUncontrolledValue(date);
|
|
44
|
+
onValueChange?.(date);
|
|
45
|
+
if (date)
|
|
46
|
+
setOpen(false);
|
|
47
|
+
}
|
|
48
|
+
return (_jsxs(Popover.Root, { open: open, onOpenChange: setOpen, children: [_jsxs(Popover.Trigger, { id: id, disabled: disabled, className: cn("flex h-11 w-full items-center justify-between rounded-lg border border-border bg-background px-3 text-left text-sm outline-none", "text-foreground transition-[border-color,box-shadow] duration-150 hover:bg-surface focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/35", "disabled:cursor-not-allowed disabled:opacity-50 in-data-[density=compact]:h-8 in-data-[density=compact]:px-2.5", !selected && "text-muted-foreground", className), children: [_jsx("span", { className: "truncate", children: label }), _jsx(CalendarGlyph, {})] }), name && _jsx("input", { type: "hidden", name: name, value: selected ? formatDateValue(selected) : "" }), _jsx(Popover.Portal, { "data-ctech-theme": theme, "data-density": density, children: _jsx(Popover.Positioner, { side: "bottom", align: "start", sideOffset: 8, className: "z-50", children: _jsx(Popover.Popup, { className: "rounded-xl border border-border bg-background shadow-modal outline-none transition-[opacity,scale] duration-150 data-[ending-style]:scale-[0.98] data-[ending-style]:opacity-0 data-[starting-style]:scale-[0.98] data-[starting-style]:opacity-0 motion-reduce:transition-none", children: _jsx(Calendar, { selected: selected, onSelect: selectDate, disabled: disabled || disabledDays.length > 0 ? (disabled ? true : disabledDays) : undefined }) }) }) })] }));
|
|
49
|
+
}
|
|
50
|
+
function formatDateValue(value) {
|
|
51
|
+
return `${value.getFullYear()}-${String(value.getMonth() + 1).padStart(2, "0")}-${String(value.getDate()).padStart(2, "0")}`;
|
|
52
|
+
}
|
|
53
|
+
function CalendarGlyph() {
|
|
54
|
+
return _jsxs("svg", { viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.5, "aria-hidden": true, className: "size-4 shrink-0", children: [_jsx("rect", { x: "2.5", y: "3.5", width: "11", height: "10", rx: "1.5" }), _jsx("path", { d: "M5 2v3M11 2v3M2.5 6.5h11", strokeLinecap: "round" })] });
|
|
55
|
+
}
|
|
56
|
+
export { Calendar, DatePicker };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* An empty state is a first-run screen, not an error. `title` says what is
|
|
4
|
+
* absent, `description` says what will fill it, and `action` is the way to
|
|
5
|
+
* make that happen when there is one. A screen whose emptiness the reader
|
|
6
|
+
* cannot act on gets no action, rather than a disabled button.
|
|
7
|
+
*/
|
|
8
|
+
interface EmptyStateProps {
|
|
9
|
+
title: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
icon?: ReactNode;
|
|
12
|
+
action?: ReactNode;
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
declare function EmptyState({ title, description, icon, action, className }: EmptyStateProps): import("react").JSX.Element;
|
|
16
|
+
export { EmptyState, type EmptyStateProps };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../lib/cn";
|
|
3
|
+
function EmptyState({ title, description, icon, action, className }) {
|
|
4
|
+
return (_jsxs("div", { "data-slot": "empty-state", className: cn("flex flex-col items-center justify-center gap-3 rounded-xl border border-dashed border-border px-6 py-14 text-center", className), children: [icon && (_jsx("div", { className: "text-muted-foreground [&_svg]:size-6", "aria-hidden": true, children: icon })), _jsx("p", { className: "text-base font-medium text-foreground", children: title }), description && (_jsx("p", { className: "max-w-[46ch] text-pretty text-sm text-muted-foreground", children: description })), action && _jsx("div", { className: "mt-2", children: action })] }));
|
|
5
|
+
}
|
|
6
|
+
export { EmptyState };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
interface FieldProps {
|
|
3
|
+
label: string;
|
|
4
|
+
htmlFor: string;
|
|
5
|
+
hint?: string;
|
|
6
|
+
error?: string;
|
|
7
|
+
required?: boolean;
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
declare function Field({ label, htmlFor, hint, error, required, children, className }: FieldProps): import("react").JSX.Element;
|
|
12
|
+
export { Field, type FieldProps };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../lib/cn";
|
|
3
|
+
function Field({ label, htmlFor, hint, error, required, children, className }) {
|
|
4
|
+
const descriptionId = error ? `${htmlFor}-error` : hint ? `${htmlFor}-hint` : undefined;
|
|
5
|
+
return (_jsxs("div", { "data-slot": "field", className: cn("grid gap-1.5", className), children: [_jsxs("label", { htmlFor: htmlFor, className: "text-sm font-medium text-foreground", children: [label, required && _jsx("span", { className: "ml-1 text-danger", "aria-hidden": true, children: " *" })] }), children, hint && !error && _jsx("p", { id: descriptionId, className: "text-sm text-muted-foreground", children: hint }), error && _jsx("p", { id: descriptionId, className: "text-sm text-danger", role: "alert", children: error })] }));
|
|
6
|
+
}
|
|
7
|
+
export { Field };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../lib/cn";
|
|
3
|
+
function Input({ className, ...props }) {
|
|
4
|
+
return (_jsx("input", { "data-slot": "input", className: cn("h-11 w-full rounded-lg border border-border bg-background px-3 text-sm text-foreground outline-none", "placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/35", "disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-danger aria-invalid:ring-3 aria-invalid:ring-danger/20", "in-data-[density=compact]:h-8 in-data-[density=compact]:px-2.5", className), ...props }));
|
|
5
|
+
}
|
|
6
|
+
export { Input };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Built on Base UI's Dialog rather than a hand-rolled portal. The focus trap,
|
|
4
|
+
* the scroll lock, the Escape handling, the restore-focus-on-close and the
|
|
5
|
+
* inert-background are all behaviours it already gets right, and every one of
|
|
6
|
+
* them is a thing a bespoke implementation gets subtly wrong — most commonly
|
|
7
|
+
* a hardcoded `aria-labelledby` id that collides the moment two dialogs are
|
|
8
|
+
* mounted at once.
|
|
9
|
+
*/
|
|
10
|
+
declare const SIZE: {
|
|
11
|
+
readonly md: "max-w-lg";
|
|
12
|
+
readonly lg: "max-w-2xl";
|
|
13
|
+
readonly xl: "max-w-4xl";
|
|
14
|
+
};
|
|
15
|
+
interface ModalProps {
|
|
16
|
+
open: boolean;
|
|
17
|
+
onClose: () => void;
|
|
18
|
+
title: string;
|
|
19
|
+
/** Optional one-line framing, announced with the title by screen readers. */
|
|
20
|
+
description?: string;
|
|
21
|
+
children?: ReactNode;
|
|
22
|
+
onSubmit?: () => void;
|
|
23
|
+
submitLabel?: string;
|
|
24
|
+
cancelLabel?: string;
|
|
25
|
+
loading?: boolean;
|
|
26
|
+
danger?: boolean;
|
|
27
|
+
submitDisabled?: boolean;
|
|
28
|
+
size?: keyof typeof SIZE;
|
|
29
|
+
}
|
|
30
|
+
declare function Modal({ open, onClose, title, description, children, onSubmit, submitLabel, cancelLabel, loading, danger, submitDisabled, size, }: ModalProps): import("react").JSX.Element;
|
|
31
|
+
export { Modal, type ModalProps };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Dialog } from "@base-ui/react/dialog";
|
|
4
|
+
import { cn } from "../lib/cn";
|
|
5
|
+
import { Button } from "./button";
|
|
6
|
+
import { useThemeScope } from "./theme-provider";
|
|
7
|
+
/**
|
|
8
|
+
* Built on Base UI's Dialog rather than a hand-rolled portal. The focus trap,
|
|
9
|
+
* the scroll lock, the Escape handling, the restore-focus-on-close and the
|
|
10
|
+
* inert-background are all behaviours it already gets right, and every one of
|
|
11
|
+
* them is a thing a bespoke implementation gets subtly wrong — most commonly
|
|
12
|
+
* a hardcoded `aria-labelledby` id that collides the moment two dialogs are
|
|
13
|
+
* mounted at once.
|
|
14
|
+
*/
|
|
15
|
+
const SIZE = {
|
|
16
|
+
md: "max-w-lg",
|
|
17
|
+
lg: "max-w-2xl",
|
|
18
|
+
xl: "max-w-4xl",
|
|
19
|
+
};
|
|
20
|
+
function Modal({ open, onClose, title, description, children, onSubmit, submitLabel = "Salvar", cancelLabel = "Cancelar", loading = false, danger = false, submitDisabled = false, size = "md", }) {
|
|
21
|
+
const { theme, density } = useThemeScope();
|
|
22
|
+
return (_jsx(Dialog.Root, { open: open, onOpenChange: next => !next && onClose(), children: _jsxs(Dialog.Portal, { "data-ctech-theme": theme, "data-density": density, children: [_jsx(Dialog.Backdrop, { className: cn("fixed inset-0 z-40 bg-black/40 backdrop-blur-[1px]", "transition-opacity duration-200 ease-out", "data-[ending-style]:opacity-0 data-[starting-style]:opacity-0", "motion-reduce:transition-none") }), _jsxs(Dialog.Popup, { className: cn("fixed top-1/2 left-1/2 z-50 -translate-x-1/2 -translate-y-1/2", "w-[calc(100vw-2rem)] max-h-[85vh] overflow-y-auto", "rounded-xl border border-border bg-background text-foreground shadow-modal outline-none", "transition-[opacity,translate,scale] duration-200 ease-out", "data-[ending-style]:scale-[0.98] data-[ending-style]:opacity-0", "data-[starting-style]:scale-[0.98] data-[starting-style]:opacity-0", "motion-reduce:transition-opacity motion-reduce:data-[ending-style]:scale-100 motion-reduce:data-[starting-style]:scale-100", SIZE[size]), children: [_jsxs("header", { className: "sticky top-0 z-10 flex items-start justify-between gap-4 border-b border-border bg-background px-6 py-4", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Dialog.Title, { className: "text-base font-semibold text-foreground", children: title }), description && (_jsx(Dialog.Description, { className: "text-sm text-muted-foreground", children: description }))] }), _jsx(Dialog.Close, { render: _jsx(Button, { variant: "ghost", size: "icon", "aria-label": "Fechar", className: "-mr-2 shrink-0 text-muted-foreground" }), children: _jsx(CloseGlyph, {}) })] }), children && _jsx("div", { className: "px-6 py-5", children: children }), _jsxs("footer", { className: "sticky bottom-0 flex justify-end gap-3 border-t border-border bg-surface px-6 py-4", children: [_jsx(Button, { type: "button", variant: "outline", onClick: onClose, disabled: loading, children: cancelLabel }), onSubmit && (_jsx(Button, { type: "button", variant: danger ? "danger" : "brand", onClick: onSubmit, disabled: loading || submitDisabled, children: loading ? "Aguarde…" : submitLabel }))] })] })] }) }));
|
|
23
|
+
}
|
|
24
|
+
function CloseGlyph() {
|
|
25
|
+
return (_jsx("svg", { viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.5, "aria-hidden": true, children: _jsx("path", { d: "m4 4 8 8M12 4l-8 8", strokeLinecap: "round" }) }));
|
|
26
|
+
}
|
|
27
|
+
export { Modal };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* One h1 per screen, optionally with a single supporting line and the screen's
|
|
4
|
+
* primary action. `eyebrow` deliberately does not exist here: a small tracked
|
|
5
|
+
* all-caps label above every heading is scaffolding, not hierarchy.
|
|
6
|
+
*/
|
|
7
|
+
interface PageHeaderProps {
|
|
8
|
+
title: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
/** The screen's primary action. One, not a toolbar. */
|
|
11
|
+
action?: ReactNode;
|
|
12
|
+
/** Rendered above the title — a back link or breadcrumb, not a label. */
|
|
13
|
+
lead?: ReactNode;
|
|
14
|
+
className?: string;
|
|
15
|
+
}
|
|
16
|
+
declare function PageHeader({ title, description, action, lead, className }: PageHeaderProps): import("react").JSX.Element;
|
|
17
|
+
export { PageHeader, type PageHeaderProps };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../lib/cn";
|
|
3
|
+
function PageHeader({ title, description, action, lead, className }) {
|
|
4
|
+
return (_jsxs("header", { "data-slot": "page-header", className: cn("space-y-2", className), children: [lead, _jsxs("div", { className: "flex flex-wrap items-start justify-between gap-x-6 gap-y-3", children: [_jsxs("div", { className: "space-y-1", children: [_jsx("h1", { className: "text-balance text-xl font-semibold tracking-[-0.01em] text-foreground in-data-[density=compact]:text-lg", children: title }), description && (_jsx("p", { className: "max-w-[65ch] text-pretty text-sm text-muted-foreground", children: description }))] }), action && _jsx("div", { className: "shrink-0", children: action })] })] }));
|
|
5
|
+
}
|
|
6
|
+
export { PageHeader };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Radio as RadioPrimitive } from "@base-ui/react/radio";
|
|
2
|
+
import { RadioGroup as RadioGroupPrimitive } from "@base-ui/react/radio-group";
|
|
3
|
+
declare function RadioGroup<Value>({ className, ...props }: RadioGroupPrimitive.Props<Value>): import("react").JSX.Element;
|
|
4
|
+
declare function Radio<Value>({ className, ...props }: RadioPrimitive.Root.Props<Value>): import("react").JSX.Element;
|
|
5
|
+
export { Radio, RadioGroup };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { Radio as RadioPrimitive } from "@base-ui/react/radio";
|
|
4
|
+
import { RadioGroup as RadioGroupPrimitive } from "@base-ui/react/radio-group";
|
|
5
|
+
import { cn } from "../lib/cn";
|
|
6
|
+
function RadioGroup({ className, ...props }) {
|
|
7
|
+
return _jsx(RadioGroupPrimitive, { className: cn("grid gap-3", className), ...props });
|
|
8
|
+
}
|
|
9
|
+
function Radio({ className, ...props }) {
|
|
10
|
+
return (_jsx(RadioPrimitive.Root, { "data-slot": "radio", className: cn("grid size-5 shrink-0 place-items-center rounded-full border border-border bg-background outline-none", "transition-[background-color,border-color,box-shadow] duration-150", "data-[checked]:border-brand-600 focus-visible:ring-3 focus-visible:ring-ring/35", "disabled:cursor-not-allowed disabled:opacity-50", className), ...props, children: _jsx(RadioPrimitive.Indicator, { className: "size-2.5 rounded-full bg-brand-600" }) }));
|
|
11
|
+
}
|
|
12
|
+
export { Radio, RadioGroup };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { Separator as SeparatorPrimitive } from "@base-ui/react/separator";
|
|
4
|
+
import { cn } from "../lib/cn";
|
|
5
|
+
function Separator({ className, orientation = "horizontal", ...props }) {
|
|
6
|
+
return (_jsx(SeparatorPrimitive, { "data-slot": "separator", orientation: orientation, className: cn("shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch", className), ...props }));
|
|
7
|
+
}
|
|
8
|
+
export { Separator };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ComponentProps } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* A skeleton, not a spinner. A spinner says "something is happening"; a
|
|
4
|
+
* skeleton says "this much is coming, and it will land here", which stops the
|
|
5
|
+
* layout jumping when it does.
|
|
6
|
+
*
|
|
7
|
+
* `motion-reduce:animate-none` is not optional: a pulsing block is exactly the
|
|
8
|
+
* kind of ambient motion prefers-reduced-motion exists to switch off.
|
|
9
|
+
*/
|
|
10
|
+
declare function Skeleton({ className, ...props }: ComponentProps<"div">): import("react").JSX.Element;
|
|
11
|
+
export { Skeleton };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../lib/cn";
|
|
3
|
+
/**
|
|
4
|
+
* A skeleton, not a spinner. A spinner says "something is happening"; a
|
|
5
|
+
* skeleton says "this much is coming, and it will land here", which stops the
|
|
6
|
+
* layout jumping when it does.
|
|
7
|
+
*
|
|
8
|
+
* `motion-reduce:animate-none` is not optional: a pulsing block is exactly the
|
|
9
|
+
* kind of ambient motion prefers-reduced-motion exists to switch off.
|
|
10
|
+
*/
|
|
11
|
+
function Skeleton({ className, ...props }) {
|
|
12
|
+
return (_jsx("div", { "data-slot": "skeleton", "aria-hidden": true, className: cn("animate-pulse rounded-md bg-border/60 motion-reduce:animate-none", className), ...props }));
|
|
13
|
+
}
|
|
14
|
+
export { Skeleton };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { Switch as SwitchPrimitive } from "@base-ui/react/switch";
|
|
4
|
+
import { cn } from "../lib/cn";
|
|
5
|
+
function Switch({ className, ...props }) {
|
|
6
|
+
return (_jsx(SwitchPrimitive.Root, { "data-slot": "switch", className: cn("flex h-6 w-11 shrink-0 items-center rounded-full border border-border bg-surface p-0.5 outline-none", "transition-[background-color,border-color,box-shadow] duration-150", "data-[checked]:border-brand-600 data-[checked]:bg-brand-600", "focus-visible:ring-3 focus-visible:ring-ring/35 disabled:cursor-not-allowed disabled:opacity-50", className), ...props, children: _jsx(SwitchPrimitive.Thumb, { className: "size-5 rounded-full bg-background shadow-sm transition-transform duration-150 data-[checked]:translate-x-5 motion-reduce:transition-none" }) }));
|
|
7
|
+
}
|
|
8
|
+
export { Switch };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ComponentProps } from "react";
|
|
2
|
+
type CTechTheme = "account" | "dfe" | "wallet" | "poker" | "billing";
|
|
3
|
+
type Density = "comfortable" | "compact";
|
|
4
|
+
interface ThemeProviderProps extends ComponentProps<"div"> {
|
|
5
|
+
theme: CTechTheme;
|
|
6
|
+
density?: Density;
|
|
7
|
+
}
|
|
8
|
+
interface ThemeScope {
|
|
9
|
+
theme: CTechTheme;
|
|
10
|
+
density: Density;
|
|
11
|
+
}
|
|
12
|
+
/** Scopes a product identity without leaking it through component props. */
|
|
13
|
+
declare function ThemeProvider({ theme, density, children, ...props }: ThemeProviderProps): import("react").JSX.Element;
|
|
14
|
+
/**
|
|
15
|
+
* Portalled primitives need this because CSS variables do not cross a portal
|
|
16
|
+
* boundary. Consumers normally receive it through ThemeProvider; the fallback
|
|
17
|
+
* makes an isolated primitive render with the default CTech theme as well.
|
|
18
|
+
*/
|
|
19
|
+
declare function useThemeScope(): ThemeScope;
|
|
20
|
+
export { ThemeProvider, useThemeScope, type CTechTheme, type ThemeProviderProps, type Density };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext } from "react";
|
|
3
|
+
const ThemeContext = createContext(null);
|
|
4
|
+
/** Scopes a product identity without leaking it through component props. */
|
|
5
|
+
function ThemeProvider({ theme, density = "comfortable", children, ...props }) {
|
|
6
|
+
return (_jsx(ThemeContext.Provider, { value: { theme, density }, children: _jsx("div", { "data-ctech-theme": theme, "data-density": density, ...props, children: children }) }));
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Portalled primitives need this because CSS variables do not cross a portal
|
|
10
|
+
* boundary. Consumers normally receive it through ThemeProvider; the fallback
|
|
11
|
+
* makes an isolated primitive render with the default CTech theme as well.
|
|
12
|
+
*/
|
|
13
|
+
function useThemeScope() {
|
|
14
|
+
return useContext(ThemeContext) ?? { theme: "account", density: "comfortable" };
|
|
15
|
+
}
|
|
16
|
+
export { ThemeProvider, useThemeScope };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { cn } from "./lib/cn";
|
|
2
|
+
export { Badge, badgeVariants, type BadgeTone } from "./components/badge";
|
|
3
|
+
export { Button, buttonVariants } from "./components/button";
|
|
4
|
+
export { Alert, alertVariants, type AlertProps } from "./components/alert";
|
|
5
|
+
export { Calendar, DatePicker, type CalendarProps, type DatePickerProps } from "./components/date-picker";
|
|
6
|
+
export { Checkbox } from "./components/checkbox";
|
|
7
|
+
export { EmptyState, type EmptyStateProps } from "./components/empty-state";
|
|
8
|
+
export { Field, type FieldProps } from "./components/field";
|
|
9
|
+
export { Input } from "./components/input";
|
|
10
|
+
export { Modal, type ModalProps } from "./components/modal";
|
|
11
|
+
export { PageHeader, type PageHeaderProps } from "./components/page-header";
|
|
12
|
+
export { Separator } from "./components/separator";
|
|
13
|
+
export { Skeleton } from "./components/skeleton";
|
|
14
|
+
export { Radio, RadioGroup } from "./components/radio";
|
|
15
|
+
export { Switch } from "./components/switch";
|
|
16
|
+
export { ThemeProvider, type CTechTheme, type Density, type ThemeProviderProps } from "./components/theme-provider";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { cn } from "./lib/cn";
|
|
2
|
+
export { Badge, badgeVariants } from "./components/badge";
|
|
3
|
+
export { Button, buttonVariants } from "./components/button";
|
|
4
|
+
export { Alert, alertVariants } from "./components/alert";
|
|
5
|
+
export { Calendar, DatePicker } from "./components/date-picker";
|
|
6
|
+
export { Checkbox } from "./components/checkbox";
|
|
7
|
+
export { EmptyState } from "./components/empty-state";
|
|
8
|
+
export { Field } from "./components/field";
|
|
9
|
+
export { Input } from "./components/input";
|
|
10
|
+
export { Modal } from "./components/modal";
|
|
11
|
+
export { PageHeader } from "./components/page-header";
|
|
12
|
+
export { Separator } from "./components/separator";
|
|
13
|
+
export { Skeleton } from "./components/skeleton";
|
|
14
|
+
export { Radio, RadioGroup } from "./components/radio";
|
|
15
|
+
export { Switch } from "./components/switch";
|
|
16
|
+
export { ThemeProvider } from "./components/theme-provider";
|
package/dist/lib/cn.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ClassValue } from "clsx";
|
|
2
|
+
/**
|
|
3
|
+
* The one class merger. Every component in this package takes a `className`
|
|
4
|
+
* and merges it here, so a consumer's override always wins over the default
|
|
5
|
+
* rather than depending on stylesheet order.
|
|
6
|
+
*/
|
|
7
|
+
export declare function cn(...inputs: ClassValue[]): string;
|
package/dist/lib/cn.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { clsx } from "clsx";
|
|
2
|
+
import { twMerge } from "tailwind-merge";
|
|
3
|
+
/**
|
|
4
|
+
* The one class merger. Every component in this package takes a `className`
|
|
5
|
+
* and merges it here, so a consumer's override always wins over the default
|
|
6
|
+
* rather than depending on stylesheet order.
|
|
7
|
+
*/
|
|
8
|
+
export function cn(...inputs) {
|
|
9
|
+
return twMerge(clsx(inputs));
|
|
10
|
+
}
|
package/dist/styles.css
ADDED
package/dist/themes.css
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/* Product themes. Only aliases change per product; success, warning and
|
|
2
|
+
danger deliberately remain stable across the CTech ecosystem. */
|
|
3
|
+
:where([data-ctech-theme]) {
|
|
4
|
+
--color-background: oklch(1 0 0);
|
|
5
|
+
--color-surface: oklch(0.97 0.008 255);
|
|
6
|
+
--color-foreground: oklch(0.18 0.02 255);
|
|
7
|
+
--color-muted-foreground: oklch(0.38 0.025 255);
|
|
8
|
+
--color-border: oklch(0.9 0.012 255);
|
|
9
|
+
--color-success: oklch(0.5 0.13 150);
|
|
10
|
+
/* This is text as well as a fill. At this lightness it clears AA on white
|
|
11
|
+
and on its own 10% tint, unlike a decorative amber. */
|
|
12
|
+
--color-warning: oklch(0.46 0.12 70);
|
|
13
|
+
--color-danger: oklch(0.55 0.19 22);
|
|
14
|
+
--color-danger-strong: oklch(0.47 0.175 22);
|
|
15
|
+
--shadow-modal: 0 12px 36px oklch(0.18 0.02 255 / 0.22);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
:where([data-ctech-theme="account"]) {
|
|
19
|
+
--brand-50: oklch(0.96 0.035 258);
|
|
20
|
+
--brand-600: oklch(0.55 0.22 258);
|
|
21
|
+
--brand-700: oklch(0.48 0.2 258);
|
|
22
|
+
--color-ring: var(--brand-600);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
:where([data-ctech-theme="dfe"]) {
|
|
26
|
+
--brand-50: oklch(0.96 0.035 165);
|
|
27
|
+
--brand-600: oklch(0.44 0.1 165);
|
|
28
|
+
--brand-700: oklch(0.37 0.09 165);
|
|
29
|
+
--color-ring: var(--brand-600);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
:where([data-ctech-theme="wallet"]) {
|
|
33
|
+
--brand-50: oklch(0.96 0.03 296);
|
|
34
|
+
--brand-600: oklch(0.54 0.2 296);
|
|
35
|
+
--brand-700: oklch(0.46 0.18 296);
|
|
36
|
+
--color-ring: var(--brand-600);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
:where([data-ctech-theme="poker"]) {
|
|
40
|
+
--color-background: oklch(0.15 0.014 20);
|
|
41
|
+
--color-surface: oklch(0.21 0.02 20);
|
|
42
|
+
--color-foreground: oklch(0.95 0.015 20);
|
|
43
|
+
--color-muted-foreground: oklch(0.78 0.025 20);
|
|
44
|
+
--color-border: oklch(0.32 0.025 20);
|
|
45
|
+
--brand-50: oklch(0.9 0.06 27);
|
|
46
|
+
--brand-600: oklch(0.48 0.16 27);
|
|
47
|
+
--brand-700: oklch(0.4 0.14 27);
|
|
48
|
+
--color-ring: oklch(0.72 0.14 27);
|
|
49
|
+
--color-success: oklch(0.76 0.13 150);
|
|
50
|
+
--color-warning: oklch(0.79 0.12 82);
|
|
51
|
+
--color-danger: oklch(0.74 0.16 22);
|
|
52
|
+
--shadow-modal: 0 16px 40px oklch(0.04 0.01 20 / 0.56);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
:where([data-ctech-theme="billing"]) {
|
|
56
|
+
--brand-50: oklch(0.965 0.014 45);
|
|
57
|
+
--brand-600: oklch(0.44 0.095 45);
|
|
58
|
+
--brand-700: oklch(0.38 0.085 45);
|
|
59
|
+
--color-foreground: oklch(0.22 0.014 45);
|
|
60
|
+
--color-muted-foreground: oklch(0.49 0.018 45);
|
|
61
|
+
--color-border: oklch(0.91 0.006 45);
|
|
62
|
+
--color-surface: oklch(0.976 0.004 45);
|
|
63
|
+
--color-ring: var(--brand-600);
|
|
64
|
+
--shadow-modal: 0 16px 48px -12px oklch(0.22 0.014 45 / 0.28);
|
|
65
|
+
}
|
package/dist/tokens.css
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/* Core primitives never encode a product identity. Product themes map these
|
|
2
|
+
semantic roles to their own palettes in themes.css. */
|
|
3
|
+
:where([data-ctech-theme]) {
|
|
4
|
+
--space-1: 0.25rem;
|
|
5
|
+
--space-2: 0.5rem;
|
|
6
|
+
--space-3: 0.75rem;
|
|
7
|
+
--space-4: 1rem;
|
|
8
|
+
--space-5: 1.5rem;
|
|
9
|
+
--space-6: 2rem;
|
|
10
|
+
--space-7: 3rem;
|
|
11
|
+
--radius-sm: 0.375rem;
|
|
12
|
+
--radius-md: 0.5rem;
|
|
13
|
+
--radius-lg: 0.75rem;
|
|
14
|
+
--duration-fast: 150ms;
|
|
15
|
+
--duration-standard: 200ms;
|
|
16
|
+
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
|
|
17
|
+
--type-caption: 0.75rem/1rem var(--font-sans);
|
|
18
|
+
--type-label: 0.875rem/1.25rem var(--font-sans);
|
|
19
|
+
--type-body: 1rem/1.5rem var(--font-sans);
|
|
20
|
+
--type-page-title: 1.25rem/1.5rem var(--font-sans);
|
|
21
|
+
--z-dropdown: 20;
|
|
22
|
+
--z-sticky: 30;
|
|
23
|
+
--z-backdrop: 40;
|
|
24
|
+
--z-modal: 50;
|
|
25
|
+
--z-toast: 60;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* Tailwind v4 consumers map these raw variables in `@theme inline`. Keeping
|
|
29
|
+
aliases here also lets non-Tailwind surfaces consume the same contract. */
|
|
30
|
+
:where([data-ctech-theme]) {
|
|
31
|
+
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
32
|
+
--background: var(--color-background);
|
|
33
|
+
--surface: var(--color-surface);
|
|
34
|
+
--foreground: var(--color-foreground);
|
|
35
|
+
--muted-foreground: var(--color-muted-foreground);
|
|
36
|
+
--border: var(--color-border);
|
|
37
|
+
--ring: var(--color-ring);
|
|
38
|
+
--success: var(--color-success);
|
|
39
|
+
--warning: var(--color-warning);
|
|
40
|
+
--danger: var(--color-danger);
|
|
41
|
+
--danger-strong: var(--color-danger-strong);
|
|
42
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aoctech/ui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared CTech UI primitives: Base UI behaviour, Tailwind classes, no colours of its own. Themed entirely by the consuming app's CSS variables.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/artur-oliveira/ctech-ui"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./styles.css": "./dist/styles.css",
|
|
18
|
+
"./themes.css": "./dist/themes.css"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"@base-ui/react": ">=1.7.0",
|
|
28
|
+
"react": ">=18",
|
|
29
|
+
"react-dom": ">=18"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"class-variance-authority": "^0.7.1",
|
|
33
|
+
"clsx": "^2.1.1",
|
|
34
|
+
"react-day-picker": "^9.14.0",
|
|
35
|
+
"tailwind-merge": "^3.6.0"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsc -p tsconfig.json && node scripts/copy-styles.mjs",
|
|
39
|
+
"test": "npm run build",
|
|
40
|
+
"storybook": "storybook dev -p 6006",
|
|
41
|
+
"build-storybook": "storybook build"
|
|
42
|
+
},
|
|
43
|
+
"keywords": [
|
|
44
|
+
"react",
|
|
45
|
+
"tailwind",
|
|
46
|
+
"design-system",
|
|
47
|
+
"ctech"
|
|
48
|
+
],
|
|
49
|
+
"author": "Artur Oliveira",
|
|
50
|
+
"license": "MIT",
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@base-ui/react": "^1.7.0",
|
|
53
|
+
"@storybook/react-vite": "^10.5.8",
|
|
54
|
+
"@tailwindcss/vite": "^4.3.3",
|
|
55
|
+
"@types/react": "^19.2.18",
|
|
56
|
+
"@types/react-dom": "^19.2.4",
|
|
57
|
+
"react": "^19.2.8",
|
|
58
|
+
"react-dom": "^19.2.8",
|
|
59
|
+
"typescript": "^6.0.3",
|
|
60
|
+
"vite": "^8.2.1"
|
|
61
|
+
},
|
|
62
|
+
"engines": {
|
|
63
|
+
"node": ">=24"
|
|
64
|
+
}
|
|
65
|
+
}
|