@krizaka/ui 1.2.0 → 2.0.0-beta.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 CHANGED
@@ -4,9 +4,10 @@
4
4
 
5
5
  # @krizaka/ui
6
6
 
7
- **The Krizaka brand layer, shared by every product.**
8
- The animated marks of Krizaka, Orazaka and Orochia, and the Krizaka motion signature — one easing, one way to enter,
9
- reveal, roll, shine and open.
7
+ **The Krizaka front-end platform's components, shared by every product.**
8
+ The primitives (button, badge, avatar, field, theme, countdown…) on the semantic `--kz-*` tokens, the animated marks
9
+ of Krizaka, Orazaka and Orochia, and the Krizaka motion signature — one easing, one way to enter, reveal, roll, shine
10
+ and open.
10
11
 
11
12
  [![npm](https://img.shields.io/npm/v/@krizaka/ui?color=3b82f6&label=npm)](https://www.npmjs.com/package/@krizaka/ui)
12
13
  [![CI](https://github.com/krizaka/krizaka-ui/actions/workflows/ci.yml/badge.svg)](https://github.com/krizaka/krizaka-ui/actions/workflows/ci.yml)
@@ -20,9 +21,103 @@ reveal, roll, shine and open.
20
21
  npm install @krizaka/ui
21
22
  ```
22
23
 
23
- React 18 or 19. No framework, no CSS library required: the marks carry their own styles, the motion is one CSS file.
24
+ The marks and the motion: React 18 or 19, no framework, no CSS library — the marks carry their own styles, the motion
25
+ is one CSS file. The primitives: **React 19** and Tailwind CSS v4 with the Krizaka preset (below).
24
26
 
25
27
 
28
+ ## Primitives
29
+
30
+ > 2.0 beta: the first layer (atoms). Cards, dialogs, tabs, toasts and menus follow in the next betas.
31
+
32
+ ### Install
33
+
34
+ ```bash
35
+ npm install @krizaka/ui@beta @krizaka/tailwind@beta @krizaka/tokens@beta tailwindcss
36
+ ```
37
+
38
+ ```css
39
+ /* app/globals.css — the preset (tokens as utilities), then the classes the primitives use */
40
+ @import "tailwindcss";
41
+ @import "@krizaka/tailwind";
42
+ @import "@krizaka/ui/tailwind.css";
43
+ ```
44
+
45
+ ```tsx
46
+ import { Button, IconButton } from "@krizaka/ui/button";
47
+ import { Badge } from "@krizaka/ui/badge";
48
+ import { Field, Input } from "@krizaka/ui/field";
49
+ import { ThemeProvider, ThemeScript, ThemeToggle } from "@krizaka/ui/theme";
50
+
51
+ <Button variant="primary" loading={saving}>{t("save")}</Button>
52
+ <Button asChild variant="outline"><Link href="/docs">{t("docs")}</Link></Button>
53
+ <Badge tone="danger" dot pulse>{t("endsSoon")}</Badge>
54
+ <Field.Root>
55
+ <Field.Label htmlFor="email">{t("email")}</Field.Label>
56
+ <Input id="email" invalid={!!error} aria-describedby="email-error" />
57
+ {error && <Field.Error id="email-error">{error}</Field.Error>}
58
+ </Field.Root>
59
+ ```
60
+
61
+ **React 19 is required by the primitives** (`ref` is a prop, no `forwardRef`). The package keeps `react >= 18` as its
62
+ peer range, because the marks and the motion still run on React 18: one `package.json` cannot hold both ranges.
63
+ `tailwindcss` and `@krizaka/tailwind` are optional peers — a native app or a marks-only site never installs them.
64
+
65
+ ### The component contract (12 points)
66
+
67
+ 1. **Composed when there is structure** (`Field.*`, `Avatar.Group`), **flat for an atom** (`Button`, `Badge`, `Input`).
68
+ 2. **Variants exported next to the component** (`buttonVariants`, `badgeVariants`): a `<Link>` or a Server Component
69
+ is styled without importing the component.
70
+ 3. **`asChild`** (Radix `Slot`) rather than `as`: props, `className` and `ref` are merged into the child.
71
+ 4. **`className` is always merged last** (tailwind-merge, through `cn`): the product's override wins.
72
+ 5. **Words are props** (`label`, `closeLabel`, `units`, `title`) — there is no `t()` in the kit. Each app re-exports the
73
+ primitives with its words from its own `components/ui/`.
74
+ 6. **Controlled and uncontrolled**: `value`/`defaultValue` + `onValueChange`, `open`/`defaultOpen` + `onOpenChange`.
75
+ 7. **State is exposed as `data-*`** (`data-tone`, `data-loading`, `data-invalid`, `data-urgent`, `data-mode`): a
76
+ product styles a state without a new variant.
77
+ 8. **Server by default**: `"use client"` only on the entries with a hook or a portal (`avatar`, `theme`, `countdown`).
78
+ One build entry per component; the directive is kept per entry.
79
+ 9. **React 19 minimum for the primitives**; the marks stay `react >= 18`.
80
+ 10. **Accessibility is delegated**: non-trivial interactions on Radix UI (`radix-ui`), the command palette on cmdk,
81
+ toasts on sonner — no hand-written focus trap.
82
+ 11. **Every primitive ships** types, a story (every variant, dark and light, screenshot-tested), an RTL + axe test and
83
+ an entry here.
84
+ 12. **Motion**: `prefers-reduced-motion` is honoured (`motion-safe:`); no component animates a layout property.
85
+
86
+ Colours are roles of the preset (`bg-surface-2`, `text-fg-secondary`, `border-border-default`, `text-accent`,
87
+ `text-danger`), never a palette step nor `light:`: a theme — dark, light or a product's — is a set of tokens.
88
+
89
+ ### Entries
90
+
91
+ | Entry | Exports | Runs |
92
+ | :-- | :-- | :-- |
93
+ | `@krizaka/ui/cn` | `cn(...classes)` — clsx + tailwind-merge (the engine `tailwind-variants` ships) | server |
94
+ | `@krizaka/ui/slot` | `Slot`, `VisuallyHidden` (Radix) | server |
95
+ | `@krizaka/ui/button` | `Button` (`variant` primary·secondary·outline·ghost·danger, `size` sm·md·lg·icon, `shape` rounded·pill, `loading`, `asChild`), `IconButton` (`label` required), `buttonVariants` | server |
96
+ | `@krizaka/ui/badge` | `Badge` (`tone` neutral·accent·success·warning·danger·scrim, `size` sm·md, `dot`, `pulse`), `badgeVariants` | server |
97
+ | `@krizaka/ui/avatar` | `Avatar` (`src`, `alt`, `fallback`, `size` xs·sm·md·lg·xl), `Avatar.Group` (`max`) | client |
98
+ | `@krizaka/ui/field` | `Field.Root/Label/Hint/Error`, `Input`, `Textarea`, `Select` (native, styled) — `invalid` | server |
99
+ | `@krizaka/ui/theme` | `ThemeScript` (in `<head>`, no flash), `ThemeProvider`, `useTheme()` → `{ mode, setMode, theme, setTheme }`, `ThemeToggle` (`label`) | client |
100
+ | `@krizaka/ui/skeleton` | `Skeleton` (`shape` text·circle·rect) | server |
101
+ | `@krizaka/ui/empty-state` | `EmptyState` (`icon`, `title`, `description`, `action`) | server |
102
+ | `@krizaka/ui/spinner` | `Spinner` (`label`, `size`) | server |
103
+ | `@krizaka/ui/countdown` | `Countdown` (`target`, `label`, `units`, `size`, `urgentBelowMs`, `skewMs`), `useCountdown`, `splitDuration` | client |
104
+ | `@krizaka/ui` | the marks, `MotionObserver`, `RotatingWord`, `cx` (unchanged) | client |
105
+ | `@krizaka/ui/native` | the marks for React Native (unchanged) | native |
106
+ | `@krizaka/ui/motion.css`, `@krizaka/ui/tailwind.css` | the motion signature; the `@source` of the primitives' classes | CSS |
107
+
108
+ The theme is one mechanism for every product: dark by default, `html.light` for light, `html.theme-<name>` for a named
109
+ theme, persisted as `kz-theme` (and `kz-theme-name`).
110
+
111
+ ```tsx
112
+ // app/layout.tsx
113
+ <html lang={locale} suppressHydrationWarning>
114
+ <head><ThemeScript /></head>
115
+ <body><ThemeProvider>{children}</ThemeProvider></body>
116
+ </html>
117
+
118
+ <ThemeToggle label={(mode) => t(`theme.${mode}`)} />
119
+ ```
120
+
26
121
  ## React Native
27
122
 
28
123
  The marks have one geometry; `@krizaka/ui/native` draws it with `react-native-svg` for the apps
@@ -84,6 +179,21 @@ With Tailwind CSS v4, give every transition the same easing:
84
179
  }
85
180
  ```
86
181
 
182
+ ## With Tailwind CSS v4
183
+
184
+ The components are styled with the [`@krizaka/tailwind`](https://www.npmjs.com/package/@krizaka/tailwind) utilities.
185
+ Import the preset, then `@krizaka/ui/tailwind.css`: it points Tailwind at the package (`@source "./dist"`), so the
186
+ classes the components use are generated in your stylesheet.
187
+
188
+ ```css
189
+ @import "tailwindcss";
190
+ @import "@krizaka/tailwind";
191
+ @import "@krizaka/ui/tailwind.css";
192
+ ```
193
+
194
+ Every component, in dark and light and under each product identity:
195
+ [krizaka.github.io/krizaka-ui/latest](https://krizaka.github.io/krizaka-ui/latest/).
196
+
87
197
  ## Develop
88
198
 
89
199
  This package lives in the [`krizaka-ui`](https://github.com/krizaka/krizaka-ui) monorepo (pnpm + Turborepo), in
@@ -0,0 +1,98 @@
1
+ import * as tailwind_variants from 'tailwind-variants';
2
+ import { VariantProps } from 'tailwind-variants';
3
+ import * as React from 'react';
4
+
5
+ declare const avatar: tailwind_variants.TVReturnType<{
6
+ size: {
7
+ xs: {
8
+ root: string;
9
+ };
10
+ sm: {
11
+ root: string;
12
+ };
13
+ md: {
14
+ root: string;
15
+ };
16
+ lg: {
17
+ root: string;
18
+ };
19
+ xl: {
20
+ root: string;
21
+ };
22
+ };
23
+ }, {
24
+ root: string;
25
+ image: string;
26
+ fallback: string;
27
+ group: string;
28
+ }, undefined, {
29
+ size: {
30
+ xs: {
31
+ root: string;
32
+ };
33
+ sm: {
34
+ root: string;
35
+ };
36
+ md: {
37
+ root: string;
38
+ };
39
+ lg: {
40
+ root: string;
41
+ };
42
+ xl: {
43
+ root: string;
44
+ };
45
+ };
46
+ }, {
47
+ root: string;
48
+ image: string;
49
+ fallback: string;
50
+ group: string;
51
+ }, tailwind_variants.TVReturnTypeLike<{
52
+ size: {
53
+ xs: {
54
+ root: string;
55
+ };
56
+ sm: {
57
+ root: string;
58
+ };
59
+ md: {
60
+ root: string;
61
+ };
62
+ lg: {
63
+ root: string;
64
+ };
65
+ xl: {
66
+ root: string;
67
+ };
68
+ };
69
+ }, {
70
+ root: string;
71
+ image: string;
72
+ fallback: string;
73
+ group: string;
74
+ }>>;
75
+ type AvatarSize = NonNullable<VariantProps<typeof avatar>["size"]>;
76
+ type AvatarProps = Omit<React.ComponentProps<"span">, "children"> & {
77
+ src?: string | null;
78
+ /** The image's text alternative (the person's name); empty when a visible name sits next to the avatar. */
79
+ alt?: string;
80
+ /** Shown while the image loads, when it fails or when there is none: initials, an icon. */
81
+ fallback?: React.ReactNode;
82
+ size?: AvatarSize;
83
+ };
84
+ declare function AvatarRoot({ src, alt, fallback, size, className, ...props }: AvatarProps): React.JSX.Element;
85
+ type AvatarGroupProps = React.ComponentProps<"div"> & {
86
+ /** How many avatars are shown; the rest becomes a `+n` avatar. */
87
+ max?: number;
88
+ /** The size of the `+n` avatar (match the avatars of the group). */
89
+ size?: AvatarSize;
90
+ };
91
+ /** Overlapping avatars; past `max`, a `+n` avatar counts the rest. */
92
+ declare function AvatarGroup({ max, size, className, children, ...props }: AvatarGroupProps): React.JSX.Element;
93
+ /** `Avatar` (an image with a fallback) and `Avatar.Group`. */
94
+ declare const Avatar: typeof AvatarRoot & {
95
+ Group: typeof AvatarGroup;
96
+ };
97
+
98
+ export { Avatar, AvatarGroup, type AvatarGroupProps, type AvatarProps, AvatarRoot, type AvatarSize };
package/dist/avatar.js ADDED
@@ -0,0 +1,48 @@
1
+ "use client";
2
+
3
+ // src/avatar/avatar.tsx
4
+ import { Avatar as AvatarPrimitive } from "radix-ui";
5
+ import * as React from "react";
6
+ import { tv } from "tailwind-variants";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ var avatar = tv({
9
+ slots: {
10
+ root: "relative inline-flex shrink-0 overflow-hidden rounded-full bg-surface-3 align-middle",
11
+ image: "h-full w-full object-cover",
12
+ fallback: "flex h-full w-full items-center justify-center font-semibold uppercase text-fg-secondary",
13
+ group: "flex items-center -space-x-2 [&>*]:ring-2 [&>*]:ring-surface-0"
14
+ },
15
+ variants: {
16
+ size: {
17
+ xs: { root: "h-5 w-5 text-[9px]" },
18
+ sm: { root: "h-8 w-8 text-xs" },
19
+ md: { root: "h-10 w-10 text-sm" },
20
+ lg: { root: "h-14 w-14 text-base" },
21
+ xl: { root: "h-20 w-20 text-lg" }
22
+ }
23
+ },
24
+ defaultVariants: { size: "md" }
25
+ });
26
+ function AvatarRoot({ src, alt = "", fallback, size, className, ...props }) {
27
+ const s = avatar({ size });
28
+ return /* @__PURE__ */ jsxs(AvatarPrimitive.Root, { "data-size": size ?? "md", className: s.root({ className }), ...props, children: [
29
+ src && /* @__PURE__ */ jsx(AvatarPrimitive.Image, { src, alt, className: s.image() }),
30
+ /* @__PURE__ */ jsx(AvatarPrimitive.Fallback, { delayMs: src ? 300 : 0, className: s.fallback(), children: fallback })
31
+ ] });
32
+ }
33
+ function AvatarGroup({ max, size, className, children, ...props }) {
34
+ const items = React.Children.toArray(children);
35
+ const shown = max === void 0 ? items : items.slice(0, max);
36
+ const rest = items.length - shown.length;
37
+ return /* @__PURE__ */ jsxs("div", { className: avatar().group({ className }), ...props, children: [
38
+ shown,
39
+ rest > 0 && /* @__PURE__ */ jsx(AvatarRoot, { size, fallback: `+${rest}` })
40
+ ] });
41
+ }
42
+ var Avatar = Object.assign(AvatarRoot, { Group: AvatarGroup });
43
+ export {
44
+ Avatar,
45
+ AvatarGroup,
46
+ AvatarRoot
47
+ };
48
+ //# sourceMappingURL=avatar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/avatar/avatar.tsx"],"sourcesContent":["// Client: Radix tracks the image's loading state and shows the fallback until it has loaded (or when it fails).\nimport { Avatar as AvatarPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\nimport { tv, type VariantProps } from \"tailwind-variants\";\n\nconst avatar = tv({\n slots: {\n root: \"relative inline-flex shrink-0 overflow-hidden rounded-full bg-surface-3 align-middle\",\n image: \"h-full w-full object-cover\",\n fallback: \"flex h-full w-full items-center justify-center font-semibold uppercase text-fg-secondary\",\n group: \"flex items-center -space-x-2 [&>*]:ring-2 [&>*]:ring-surface-0\",\n },\n variants: {\n size: {\n xs: { root: \"h-5 w-5 text-[9px]\" },\n sm: { root: \"h-8 w-8 text-xs\" },\n md: { root: \"h-10 w-10 text-sm\" },\n lg: { root: \"h-14 w-14 text-base\" },\n xl: { root: \"h-20 w-20 text-lg\" },\n },\n },\n defaultVariants: { size: \"md\" },\n});\n\nexport type AvatarSize = NonNullable<VariantProps<typeof avatar>[\"size\"]>;\n\nexport type AvatarProps = Omit<React.ComponentProps<\"span\">, \"children\"> & {\n src?: string | null;\n /** The image's text alternative (the person's name); empty when a visible name sits next to the avatar. */\n alt?: string;\n /** Shown while the image loads, when it fails or when there is none: initials, an icon. */\n fallback?: React.ReactNode;\n size?: AvatarSize;\n};\n\nexport function AvatarRoot({ src, alt = \"\", fallback, size, className, ...props }: AvatarProps) {\n const s = avatar({ size });\n return (\n <AvatarPrimitive.Root data-size={size ?? \"md\"} className={s.root({ className })} {...props}>\n {src && <AvatarPrimitive.Image src={src} alt={alt} className={s.image()} />}\n <AvatarPrimitive.Fallback delayMs={src ? 300 : 0} className={s.fallback()}>\n {fallback}\n </AvatarPrimitive.Fallback>\n </AvatarPrimitive.Root>\n );\n}\n\nexport type AvatarGroupProps = React.ComponentProps<\"div\"> & {\n /** How many avatars are shown; the rest becomes a `+n` avatar. */\n max?: number;\n /** The size of the `+n` avatar (match the avatars of the group). */\n size?: AvatarSize;\n};\n\n/** Overlapping avatars; past `max`, a `+n` avatar counts the rest. */\nexport function AvatarGroup({ max, size, className, children, ...props }: AvatarGroupProps) {\n const items = React.Children.toArray(children);\n const shown = max === undefined ? items : items.slice(0, max);\n const rest = items.length - shown.length;\n return (\n <div className={avatar().group({ className })} {...props}>\n {shown}\n {rest > 0 && <AvatarRoot size={size} fallback={`+${rest}`} />}\n </div>\n );\n}\n\n/** `Avatar` (an image with a fallback) and `Avatar.Group`. */\nexport const Avatar = Object.assign(AvatarRoot, { Group: AvatarGroup });\n"],"mappings":";;;AACA,SAAS,UAAU,uBAAuB;AAC1C,YAAY,WAAW;AACvB,SAAS,UAA6B;AAmClC,SACU,KADV;AAjCJ,IAAM,SAAS,GAAG;AAAA,EAChB,OAAO;AAAA,IACL,MAAM;AAAA,IACN,OAAO;AAAA,IACP,UAAU;AAAA,IACV,OAAO;AAAA,EACT;AAAA,EACA,UAAU;AAAA,IACR,MAAM;AAAA,MACJ,IAAI,EAAE,MAAM,qBAAqB;AAAA,MACjC,IAAI,EAAE,MAAM,kBAAkB;AAAA,MAC9B,IAAI,EAAE,MAAM,oBAAoB;AAAA,MAChC,IAAI,EAAE,MAAM,sBAAsB;AAAA,MAClC,IAAI,EAAE,MAAM,oBAAoB;AAAA,IAClC;AAAA,EACF;AAAA,EACA,iBAAiB,EAAE,MAAM,KAAK;AAChC,CAAC;AAaM,SAAS,WAAW,EAAE,KAAK,MAAM,IAAI,UAAU,MAAM,WAAW,GAAG,MAAM,GAAgB;AAC9F,QAAM,IAAI,OAAO,EAAE,KAAK,CAAC;AACzB,SACE,qBAAC,gBAAgB,MAAhB,EAAqB,aAAW,QAAQ,MAAM,WAAW,EAAE,KAAK,EAAE,UAAU,CAAC,GAAI,GAAG,OAClF;AAAA,WAAO,oBAAC,gBAAgB,OAAhB,EAAsB,KAAU,KAAU,WAAW,EAAE,MAAM,GAAG;AAAA,IACzE,oBAAC,gBAAgB,UAAhB,EAAyB,SAAS,MAAM,MAAM,GAAG,WAAW,EAAE,SAAS,GACrE,oBACH;AAAA,KACF;AAEJ;AAUO,SAAS,YAAY,EAAE,KAAK,MAAM,WAAW,UAAU,GAAG,MAAM,GAAqB;AAC1F,QAAM,QAAc,eAAS,QAAQ,QAAQ;AAC7C,QAAM,QAAQ,QAAQ,SAAY,QAAQ,MAAM,MAAM,GAAG,GAAG;AAC5D,QAAM,OAAO,MAAM,SAAS,MAAM;AAClC,SACE,qBAAC,SAAI,WAAW,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,GAAI,GAAG,OAChD;AAAA;AAAA,IACA,OAAO,KAAK,oBAAC,cAAW,MAAY,UAAU,IAAI,IAAI,IAAI;AAAA,KAC7D;AAEJ;AAGO,IAAM,SAAS,OAAO,OAAO,YAAY,EAAE,OAAO,YAAY,CAAC;","names":[]}
@@ -0,0 +1,64 @@
1
+ import * as tailwind_variants from 'tailwind-variants';
2
+ import { VariantProps } from 'tailwind-variants';
3
+ import * as React from 'react';
4
+
5
+ declare const badgeVariants: tailwind_variants.TVReturnType<{
6
+ tone: {
7
+ neutral: "bg-surface-3 text-fg-secondary";
8
+ accent: "bg-accent-soft text-fg ring-1 ring-inset ring-accent/40 [&>[data-dot]]:bg-accent";
9
+ success: "bg-success/15 text-fg ring-1 ring-inset ring-success/40 [&>[data-dot]]:bg-success";
10
+ warning: "bg-warning/15 text-fg ring-1 ring-inset ring-warning/40 [&>[data-dot]]:bg-warning";
11
+ danger: "bg-danger/15 text-fg ring-1 ring-inset ring-danger/40 [&>[data-dot]]:bg-danger";
12
+ /** On a media: veil and text invariant, identical in both themes. */
13
+ scrim: "bg-scrim text-fg-on-media backdrop-blur-md";
14
+ };
15
+ size: {
16
+ sm: "px-2 py-0.5 text-[10px]";
17
+ md: "px-2.5 py-1 text-[11px]";
18
+ };
19
+ pulse: {
20
+ true: "[&>[data-dot]]:motion-safe:animate-pulse";
21
+ };
22
+ }, undefined, "inline-flex shrink-0 items-center gap-1 whitespace-nowrap rounded-full font-semibold uppercase tracking-wider", {
23
+ tone: {
24
+ neutral: "bg-surface-3 text-fg-secondary";
25
+ accent: "bg-accent-soft text-fg ring-1 ring-inset ring-accent/40 [&>[data-dot]]:bg-accent";
26
+ success: "bg-success/15 text-fg ring-1 ring-inset ring-success/40 [&>[data-dot]]:bg-success";
27
+ warning: "bg-warning/15 text-fg ring-1 ring-inset ring-warning/40 [&>[data-dot]]:bg-warning";
28
+ danger: "bg-danger/15 text-fg ring-1 ring-inset ring-danger/40 [&>[data-dot]]:bg-danger";
29
+ /** On a media: veil and text invariant, identical in both themes. */
30
+ scrim: "bg-scrim text-fg-on-media backdrop-blur-md";
31
+ };
32
+ size: {
33
+ sm: "px-2 py-0.5 text-[10px]";
34
+ md: "px-2.5 py-1 text-[11px]";
35
+ };
36
+ pulse: {
37
+ true: "[&>[data-dot]]:motion-safe:animate-pulse";
38
+ };
39
+ }, undefined, tailwind_variants.TVReturnTypeLike<{
40
+ tone: {
41
+ neutral: "bg-surface-3 text-fg-secondary";
42
+ accent: "bg-accent-soft text-fg ring-1 ring-inset ring-accent/40 [&>[data-dot]]:bg-accent";
43
+ success: "bg-success/15 text-fg ring-1 ring-inset ring-success/40 [&>[data-dot]]:bg-success";
44
+ warning: "bg-warning/15 text-fg ring-1 ring-inset ring-warning/40 [&>[data-dot]]:bg-warning";
45
+ danger: "bg-danger/15 text-fg ring-1 ring-inset ring-danger/40 [&>[data-dot]]:bg-danger";
46
+ /** On a media: veil and text invariant, identical in both themes. */
47
+ scrim: "bg-scrim text-fg-on-media backdrop-blur-md";
48
+ };
49
+ size: {
50
+ sm: "px-2 py-0.5 text-[10px]";
51
+ md: "px-2.5 py-1 text-[11px]";
52
+ };
53
+ pulse: {
54
+ true: "[&>[data-dot]]:motion-safe:animate-pulse";
55
+ };
56
+ }, undefined>>;
57
+ type BadgeVariants = VariantProps<typeof badgeVariants>;
58
+ type BadgeProps = React.ComponentProps<"span"> & BadgeVariants & {
59
+ /** A small dot before the text, in the tone's colour (pulsing with `pulse`). */
60
+ dot?: boolean;
61
+ };
62
+ declare function Badge({ tone, size, pulse, dot, className, children, ...props }: BadgeProps): React.JSX.Element;
63
+
64
+ export { Badge, type BadgeProps, type BadgeVariants, badgeVariants };
package/dist/badge.js ADDED
@@ -0,0 +1,31 @@
1
+ // src/badge/badge.tsx
2
+ import { tv } from "tailwind-variants";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ var badgeVariants = tv({
5
+ base: "inline-flex shrink-0 items-center gap-1 whitespace-nowrap rounded-full font-semibold uppercase tracking-wider",
6
+ variants: {
7
+ tone: {
8
+ neutral: "bg-surface-3 text-fg-secondary",
9
+ accent: "bg-accent-soft text-fg ring-1 ring-inset ring-accent/40 [&>[data-dot]]:bg-accent",
10
+ success: "bg-success/15 text-fg ring-1 ring-inset ring-success/40 [&>[data-dot]]:bg-success",
11
+ warning: "bg-warning/15 text-fg ring-1 ring-inset ring-warning/40 [&>[data-dot]]:bg-warning",
12
+ danger: "bg-danger/15 text-fg ring-1 ring-inset ring-danger/40 [&>[data-dot]]:bg-danger",
13
+ /** On a media: veil and text invariant, identical in both themes. */
14
+ scrim: "bg-scrim text-fg-on-media backdrop-blur-md"
15
+ },
16
+ size: { sm: "px-2 py-0.5 text-[10px]", md: "px-2.5 py-1 text-[11px]" },
17
+ pulse: { true: "[&>[data-dot]]:motion-safe:animate-pulse" }
18
+ },
19
+ defaultVariants: { tone: "neutral", size: "sm" }
20
+ });
21
+ function Badge({ tone, size, pulse, dot, className, children, ...props }) {
22
+ return /* @__PURE__ */ jsxs("span", { "data-tone": tone ?? "neutral", className: badgeVariants({ tone, size, pulse, className }), ...props, children: [
23
+ dot && /* @__PURE__ */ jsx("span", { "data-dot": "", "aria-hidden": true, className: "h-1.5 w-1.5 rounded-full bg-current" }),
24
+ children
25
+ ] });
26
+ }
27
+ export {
28
+ Badge,
29
+ badgeVariants
30
+ };
31
+ //# sourceMappingURL=badge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/badge/badge.tsx"],"sourcesContent":["// Server-safe: no hook, no context.\nimport type * as React from \"react\";\nimport { tv, type VariantProps } from \"tailwind-variants\";\n\n// Status tones are soft (a tint, a coloured dot, a text role): the invariant status tokens are too light to carry\n// small text on a light surface, or to sit under white text (WCAG AA).\nexport const badgeVariants = tv({\n base: \"inline-flex shrink-0 items-center gap-1 whitespace-nowrap rounded-full font-semibold uppercase tracking-wider\",\n variants: {\n tone: {\n neutral: \"bg-surface-3 text-fg-secondary\",\n accent: \"bg-accent-soft text-fg ring-1 ring-inset ring-accent/40 [&>[data-dot]]:bg-accent\",\n success: \"bg-success/15 text-fg ring-1 ring-inset ring-success/40 [&>[data-dot]]:bg-success\",\n warning: \"bg-warning/15 text-fg ring-1 ring-inset ring-warning/40 [&>[data-dot]]:bg-warning\",\n danger: \"bg-danger/15 text-fg ring-1 ring-inset ring-danger/40 [&>[data-dot]]:bg-danger\",\n /** On a media: veil and text invariant, identical in both themes. */\n scrim: \"bg-scrim text-fg-on-media backdrop-blur-md\",\n },\n size: { sm: \"px-2 py-0.5 text-[10px]\", md: \"px-2.5 py-1 text-[11px]\" },\n pulse: { true: \"[&>[data-dot]]:motion-safe:animate-pulse\" },\n },\n defaultVariants: { tone: \"neutral\", size: \"sm\" },\n});\n\nexport type BadgeVariants = VariantProps<typeof badgeVariants>;\n\nexport type BadgeProps = React.ComponentProps<\"span\"> &\n BadgeVariants & {\n /** A small dot before the text, in the tone's colour (pulsing with `pulse`). */\n dot?: boolean;\n };\n\nexport function Badge({ tone, size, pulse, dot, className, children, ...props }: BadgeProps) {\n return (\n <span data-tone={tone ?? \"neutral\"} className={badgeVariants({ tone, size, pulse, className })} {...props}>\n {dot && <span data-dot=\"\" aria-hidden className=\"h-1.5 w-1.5 rounded-full bg-current\" />}\n {children}\n </span>\n );\n}\n"],"mappings":";AAEA,SAAS,UAA6B;AAgClC,SACU,KADV;AA5BG,IAAM,gBAAgB,GAAG;AAAA,EAC9B,MAAM;AAAA,EACN,UAAU;AAAA,IACR,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,SAAS;AAAA,MACT,QAAQ;AAAA;AAAA,MAER,OAAO;AAAA,IACT;AAAA,IACA,MAAM,EAAE,IAAI,2BAA2B,IAAI,0BAA0B;AAAA,IACrE,OAAO,EAAE,MAAM,2CAA2C;AAAA,EAC5D;AAAA,EACA,iBAAiB,EAAE,MAAM,WAAW,MAAM,KAAK;AACjD,CAAC;AAUM,SAAS,MAAM,EAAE,MAAM,MAAM,OAAO,KAAK,WAAW,UAAU,GAAG,MAAM,GAAe;AAC3F,SACE,qBAAC,UAAK,aAAW,QAAQ,WAAW,WAAW,cAAc,EAAE,MAAM,MAAM,OAAO,UAAU,CAAC,GAAI,GAAG,OACjG;AAAA,WAAO,oBAAC,UAAK,YAAS,IAAG,eAAW,MAAC,WAAU,uCAAsC;AAAA,IACrF;AAAA,KACH;AAEJ;","names":[]}
@@ -0,0 +1,76 @@
1
+ import * as tailwind_variants from 'tailwind-variants';
2
+ import { VariantProps } from 'tailwind-variants';
3
+ import * as React from 'react';
4
+
5
+ declare const buttonVariants: tailwind_variants.TVReturnType<{
6
+ variant: {
7
+ primary: "bg-accent text-on-accent shadow-sm hover:bg-accent-hover";
8
+ secondary: "border border-border-default bg-surface-2 text-fg hover:border-border-strong hover:bg-surface-3";
9
+ outline: "border border-border-default bg-transparent text-fg hover:border-accent hover:text-accent";
10
+ ghost: "text-fg-secondary hover:bg-surface-2 hover:text-fg";
11
+ danger: "border border-danger/50 bg-danger/10 text-fg hover:border-danger hover:bg-danger/20";
12
+ };
13
+ size: {
14
+ sm: "h-8 px-3 text-xs";
15
+ md: "h-10 px-4 text-sm";
16
+ lg: "h-12 px-6 text-sm";
17
+ icon: "h-10 w-10";
18
+ };
19
+ shape: {
20
+ rounded: "rounded-lg";
21
+ pill: "rounded-full";
22
+ };
23
+ }, undefined, string, {
24
+ variant: {
25
+ primary: "bg-accent text-on-accent shadow-sm hover:bg-accent-hover";
26
+ secondary: "border border-border-default bg-surface-2 text-fg hover:border-border-strong hover:bg-surface-3";
27
+ outline: "border border-border-default bg-transparent text-fg hover:border-accent hover:text-accent";
28
+ ghost: "text-fg-secondary hover:bg-surface-2 hover:text-fg";
29
+ danger: "border border-danger/50 bg-danger/10 text-fg hover:border-danger hover:bg-danger/20";
30
+ };
31
+ size: {
32
+ sm: "h-8 px-3 text-xs";
33
+ md: "h-10 px-4 text-sm";
34
+ lg: "h-12 px-6 text-sm";
35
+ icon: "h-10 w-10";
36
+ };
37
+ shape: {
38
+ rounded: "rounded-lg";
39
+ pill: "rounded-full";
40
+ };
41
+ }, undefined, tailwind_variants.TVReturnTypeLike<{
42
+ variant: {
43
+ primary: "bg-accent text-on-accent shadow-sm hover:bg-accent-hover";
44
+ secondary: "border border-border-default bg-surface-2 text-fg hover:border-border-strong hover:bg-surface-3";
45
+ outline: "border border-border-default bg-transparent text-fg hover:border-accent hover:text-accent";
46
+ ghost: "text-fg-secondary hover:bg-surface-2 hover:text-fg";
47
+ danger: "border border-danger/50 bg-danger/10 text-fg hover:border-danger hover:bg-danger/20";
48
+ };
49
+ size: {
50
+ sm: "h-8 px-3 text-xs";
51
+ md: "h-10 px-4 text-sm";
52
+ lg: "h-12 px-6 text-sm";
53
+ icon: "h-10 w-10";
54
+ };
55
+ shape: {
56
+ rounded: "rounded-lg";
57
+ pill: "rounded-full";
58
+ };
59
+ }, undefined>>;
60
+ type ButtonVariants = VariantProps<typeof buttonVariants>;
61
+ type ButtonProps = React.ComponentProps<"button"> & ButtonVariants & {
62
+ /** Renders the child (e.g. a <Link>) instead of the <button>, with props, classes and ref merged. */
63
+ asChild?: boolean;
64
+ /** Disables and signals the wait (aria-busy, data-loading) — the product adds a spinner if it wants one. */
65
+ loading?: boolean;
66
+ };
67
+ declare function Button({ asChild, loading, variant, size, shape, className, disabled, ...props }: ButtonProps): React.JSX.Element;
68
+ type IconButtonProps = Omit<ButtonProps, "size"> & {
69
+ /** The accessible name (and tooltip) of the button — required, passed translated. */
70
+ label: string;
71
+ size?: ButtonProps["size"];
72
+ };
73
+ /** An icon-only button: `label` is required, it is its accessible name. */
74
+ declare function IconButton({ label, size, ...props }: IconButtonProps): React.JSX.Element;
75
+
76
+ export { Button, type ButtonProps, type ButtonVariants, IconButton, type IconButtonProps, buttonVariants };
package/dist/button.js ADDED
@@ -0,0 +1,46 @@
1
+ // src/button/button.tsx
2
+ import { Slot } from "radix-ui";
3
+ import { tv } from "tailwind-variants";
4
+ import { jsx } from "react/jsx-runtime";
5
+ var buttonVariants = tv({
6
+ base: "inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap font-semibold transition-all focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-surface-0 disabled:pointer-events-none disabled:opacity-40 data-loading:cursor-progress active:scale-[0.98]",
7
+ variants: {
8
+ variant: {
9
+ primary: "bg-accent text-on-accent shadow-sm hover:bg-accent-hover",
10
+ secondary: "border border-border-default bg-surface-2 text-fg hover:border-border-strong hover:bg-surface-3",
11
+ outline: "border border-border-default bg-transparent text-fg hover:border-accent hover:text-accent",
12
+ ghost: "text-fg-secondary hover:bg-surface-2 hover:text-fg",
13
+ // The danger token is invariant and too light for small text on a light surface (3.6:1): the label stays a
14
+ // text role, the border and the tint carry the danger.
15
+ danger: "border border-danger/50 bg-danger/10 text-fg hover:border-danger hover:bg-danger/20"
16
+ },
17
+ size: { sm: "h-8 px-3 text-xs", md: "h-10 px-4 text-sm", lg: "h-12 px-6 text-sm", icon: "h-10 w-10" },
18
+ shape: { rounded: "rounded-lg", pill: "rounded-full" }
19
+ },
20
+ defaultVariants: { variant: "secondary", size: "md", shape: "rounded" }
21
+ });
22
+ function Button({ asChild, loading, variant, size, shape, className, disabled, ...props }) {
23
+ const Comp = asChild ? Slot.Root : "button";
24
+ return /* @__PURE__ */ jsx(
25
+ Comp,
26
+ {
27
+ type: asChild ? void 0 : "button",
28
+ "aria-busy": loading || void 0,
29
+ "data-loading": loading ? "" : void 0,
30
+ "data-variant": variant ?? "secondary",
31
+ disabled: asChild ? void 0 : disabled || loading,
32
+ "aria-disabled": asChild && (disabled || loading) ? true : void 0,
33
+ className: buttonVariants({ variant, size, shape, className }),
34
+ ...props
35
+ }
36
+ );
37
+ }
38
+ function IconButton({ label, size = "icon", ...props }) {
39
+ return /* @__PURE__ */ jsx(Button, { "aria-label": label, title: label, size, ...props });
40
+ }
41
+ export {
42
+ Button,
43
+ IconButton,
44
+ buttonVariants
45
+ };
46
+ //# sourceMappingURL=button.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/button/button.tsx"],"sourcesContent":["// Server-safe: no hook, no context. `buttonVariants` styles a <Link> or a Server Component without the component.\nimport { Slot } from \"radix-ui\";\nimport type * as React from \"react\";\nimport { tv, type VariantProps } from \"tailwind-variants\";\n\nexport const buttonVariants = tv({\n base:\n \"inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap font-semibold transition-all \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-surface-0 \" +\n \"disabled:pointer-events-none disabled:opacity-40 data-loading:cursor-progress active:scale-[0.98]\",\n variants: {\n variant: {\n primary: \"bg-accent text-on-accent shadow-sm hover:bg-accent-hover\",\n secondary: \"border border-border-default bg-surface-2 text-fg hover:border-border-strong hover:bg-surface-3\",\n outline: \"border border-border-default bg-transparent text-fg hover:border-accent hover:text-accent\",\n ghost: \"text-fg-secondary hover:bg-surface-2 hover:text-fg\",\n // The danger token is invariant and too light for small text on a light surface (3.6:1): the label stays a\n // text role, the border and the tint carry the danger.\n danger: \"border border-danger/50 bg-danger/10 text-fg hover:border-danger hover:bg-danger/20\",\n },\n size: { sm: \"h-8 px-3 text-xs\", md: \"h-10 px-4 text-sm\", lg: \"h-12 px-6 text-sm\", icon: \"h-10 w-10\" },\n shape: { rounded: \"rounded-lg\", pill: \"rounded-full\" },\n },\n defaultVariants: { variant: \"secondary\", size: \"md\", shape: \"rounded\" },\n});\n\nexport type ButtonVariants = VariantProps<typeof buttonVariants>;\n\nexport type ButtonProps = React.ComponentProps<\"button\"> &\n ButtonVariants & {\n /** Renders the child (e.g. a <Link>) instead of the <button>, with props, classes and ref merged. */\n asChild?: boolean;\n /** Disables and signals the wait (aria-busy, data-loading) — the product adds a spinner if it wants one. */\n loading?: boolean;\n };\n\nexport function Button({ asChild, loading, variant, size, shape, className, disabled, ...props }: ButtonProps) {\n const Comp = asChild ? Slot.Root : \"button\";\n return (\n <Comp\n type={asChild ? undefined : \"button\"}\n aria-busy={loading || undefined}\n data-loading={loading ? \"\" : undefined}\n data-variant={variant ?? \"secondary\"}\n disabled={asChild ? undefined : disabled || loading}\n aria-disabled={asChild && (disabled || loading) ? true : undefined}\n className={buttonVariants({ variant, size, shape, className })}\n {...props}\n />\n );\n}\n\nexport type IconButtonProps = Omit<ButtonProps, \"size\"> & {\n /** The accessible name (and tooltip) of the button — required, passed translated. */\n label: string;\n size?: ButtonProps[\"size\"];\n};\n\n/** An icon-only button: `label` is required, it is its accessible name. */\nexport function IconButton({ label, size = \"icon\", ...props }: IconButtonProps) {\n return <Button aria-label={label} title={label} size={size} {...props} />;\n}\n"],"mappings":";AACA,SAAS,YAAY;AAErB,SAAS,UAA6B;AAoClC;AAlCG,IAAM,iBAAiB,GAAG;AAAA,EAC/B,MACE;AAAA,EAGF,UAAU;AAAA,IACR,SAAS;AAAA,MACP,SAAS;AAAA,MACT,WAAW;AAAA,MACX,SAAS;AAAA,MACT,OAAO;AAAA;AAAA;AAAA,MAGP,QAAQ;AAAA,IACV;AAAA,IACA,MAAM,EAAE,IAAI,oBAAoB,IAAI,qBAAqB,IAAI,qBAAqB,MAAM,YAAY;AAAA,IACpG,OAAO,EAAE,SAAS,cAAc,MAAM,eAAe;AAAA,EACvD;AAAA,EACA,iBAAiB,EAAE,SAAS,aAAa,MAAM,MAAM,OAAO,UAAU;AACxE,CAAC;AAYM,SAAS,OAAO,EAAE,SAAS,SAAS,SAAS,MAAM,OAAO,WAAW,UAAU,GAAG,MAAM,GAAgB;AAC7G,QAAM,OAAO,UAAU,KAAK,OAAO;AACnC,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAM,UAAU,SAAY;AAAA,MAC5B,aAAW,WAAW;AAAA,MACtB,gBAAc,UAAU,KAAK;AAAA,MAC7B,gBAAc,WAAW;AAAA,MACzB,UAAU,UAAU,SAAY,YAAY;AAAA,MAC5C,iBAAe,YAAY,YAAY,WAAW,OAAO;AAAA,MACzD,WAAW,eAAe,EAAE,SAAS,MAAM,OAAO,UAAU,CAAC;AAAA,MAC5D,GAAG;AAAA;AAAA,EACN;AAEJ;AASO,SAAS,WAAW,EAAE,OAAO,OAAO,QAAQ,GAAG,MAAM,GAAoB;AAC9E,SAAO,oBAAC,UAAO,cAAY,OAAO,OAAO,OAAO,MAAa,GAAG,OAAO;AACzE;","names":[]}
@@ -0,0 +1,11 @@
1
+ // src/cn.ts
2
+ import { clsx } from "clsx";
3
+ import { cn as merge } from "tailwind-variants";
4
+ function cn(...inputs) {
5
+ return merge(clsx(inputs)) ?? "";
6
+ }
7
+
8
+ export {
9
+ cn
10
+ };
11
+ //# sourceMappingURL=chunk-CPMZS4XA.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cn.ts"],"sourcesContent":["import { type ClassValue, clsx } from \"clsx\";\nimport { cn as merge } from \"tailwind-variants\";\n\nexport type { ClassValue };\n\n/**\n * Merges Tailwind classes: the last one wins — a product's `className` overrides the primitive's. The merge engine is\n * tailwind-merge as tailwind-variants ships it (the one `tv` uses), so the platform carries a single copy of it.\n */\nexport function cn(...inputs: ClassValue[]): string {\n return merge(clsx(inputs)) ?? \"\";\n}\n"],"mappings":";AAAA,SAA0B,YAAY;AACtC,SAAS,MAAM,aAAa;AAQrB,SAAS,MAAM,QAA8B;AAClD,SAAO,MAAM,KAAK,MAAM,CAAC,KAAK;AAChC;","names":[]}
package/dist/cn.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { ClassValue } from 'clsx';
2
+ export { ClassValue } from 'clsx';
3
+
4
+ /**
5
+ * Merges Tailwind classes: the last one wins — a product's `className` overrides the primitive's. The merge engine is
6
+ * tailwind-merge as tailwind-variants ships it (the one `tv` uses), so the platform carries a single copy of it.
7
+ */
8
+ declare function cn(...inputs: ClassValue[]): string;
9
+
10
+ export { cn };
package/dist/cn.js ADDED
@@ -0,0 +1,7 @@
1
+ import {
2
+ cn
3
+ } from "./chunk-CPMZS4XA.js";
4
+ export {
5
+ cn
6
+ };
7
+ //# sourceMappingURL=cn.js.map
package/dist/cn.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,46 @@
1
+ import * as React from 'react';
2
+
3
+ /** Milliseconds left until `target` (never negative), ticking every second. `skewMs` = server clock − this clock. */
4
+ declare function useCountdown(target: Date | string | number, skewMs?: number): number;
5
+ /** Splits a duration into days, hours, minutes and seconds. */
6
+ declare function splitDuration(ms: number): {
7
+ d: number;
8
+ h: number;
9
+ m: number;
10
+ s: number;
11
+ };
12
+ /** The short unit labels, passed translated (e.g. `{ d: "d", h: "h", m: "m", s: "s" }`). */
13
+ type CountdownUnits = {
14
+ d: string;
15
+ h: string;
16
+ m: string;
17
+ s: string;
18
+ };
19
+ /** The three segments shown: days, hours, minutes when there are days; else hours, minutes, seconds. */
20
+ declare function countdownParts(ms: number, units: CountdownUnits): [number, string][];
21
+
22
+ type CountdownProps = {
23
+ target: Date | string | number;
24
+ /** Server clock − this clock, in milliseconds. */
25
+ skewMs?: number;
26
+ /** The short unit labels, passed translated. */
27
+ units: CountdownUnits;
28
+ /** Below this many milliseconds it turns urgent (`text-danger`, the last segment pulses). Default 60 s. */
29
+ urgentBelowMs?: number;
30
+ size?: "sm" | "md" | "lg";
31
+ /** Accessible name, e.g. "Ends in". */
32
+ label: string;
33
+ className?: string;
34
+ };
35
+ /**
36
+ * Time left until a moment, as segments (2d 04h 13m 09s — days only when there are some) in tabular figures. Under
37
+ * `urgentBelowMs` it turns to the danger role and its last segment pulses (no motion under prefers-reduced-motion).
38
+ * Words come from the app: `units` are the short unit labels. `role="timer"` without live announcements — the app
39
+ * announces what matters. State: `data-urgent`, `data-ended`.
40
+ *
41
+ * The danger role is large-text safe (≥ 3:1) from `size="md"`; at `size="sm"` an urgent countdown in light mode is
42
+ * under the 4.5:1 of small text.
43
+ */
44
+ declare function Countdown({ target, skewMs, units, urgentBelowMs, size, label, className }: CountdownProps): React.JSX.Element;
45
+
46
+ export { Countdown, type CountdownProps, type CountdownUnits, countdownParts, splitDuration, useCountdown };