@krizaka/ui 2.0.0-beta.6 → 2.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 +13 -10
- package/dist/{button-D5aSsmxX.d.ts → button-Cgfkg_3y.d.ts} +5 -1
- package/dist/button.d.ts +5 -1
- package/dist/button.js +4 -2
- package/dist/button.js.map +1 -1
- package/dist/card.d.ts +7 -1
- package/dist/card.js +2 -2
- package/dist/card.js.map +1 -1
- package/dist/{chunk-IUGBKMJC.js → chunk-FHYZSO2S.js} +2 -2
- package/dist/{chunk-6DJ6SQY3.js → chunk-PESA2R4U.js} +5 -3
- package/dist/chunk-PESA2R4U.js.map +1 -0
- package/dist/command.d.ts +11 -2
- package/dist/command.js +17 -6
- package/dist/command.js.map +1 -1
- package/dist/confirm-button.d.ts +1 -1
- package/dist/confirm-button.js +1 -1
- package/dist/countdown.d.ts +7 -6
- package/dist/countdown.js +12 -8
- package/dist/countdown.js.map +1 -1
- package/dist/dialog.js +2 -2
- package/dist/dropdown-menu.js +3 -3
- package/dist/dropdown-menu.js.map +1 -1
- package/dist/field.js +2 -2
- package/dist/field.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +52 -10
- package/dist/index.js.map +1 -1
- package/dist/native.d.ts +20 -8
- package/dist/native.js +170 -184
- package/dist/native.js.map +1 -1
- package/dist/theme.d.ts +19 -9
- package/dist/theme.js +8 -5
- package/dist/theme.js.map +1 -1
- package/package.json +3 -3
- package/registry/avatar.json +20 -5
- package/registry/button.json +36 -15
- package/registry/card.json +17 -2
- package/registry/command.json +30 -8
- package/registry/confirm-button.json +4 -4
- package/registry/countdown.json +53 -12
- package/registry/dropdown-menu.json +1 -1
- package/registry/examples/button/gradient.d.ts +4 -0
- package/registry/examples/button/gradient.js +11 -0
- package/registry/examples/button/gradient.tsx +5 -0
- package/registry/examples/button/native/gradient.d.ts +4 -0
- package/registry/examples/button/native/gradient.dark.png +0 -0
- package/registry/examples/button/native/gradient.js +12 -0
- package/registry/examples/button/native/gradient.light.png +0 -0
- package/registry/examples/button/native/gradient.tsx +5 -0
- package/registry/examples/command/dialog.js +1 -2
- package/registry/examples/command/dialog.tsx +1 -2
- package/registry/examples/command/inline.js +1 -2
- package/registry/examples/command/inline.tsx +1 -2
- package/registry/examples/command/no-results.d.ts +4 -0
- package/registry/examples/command/no-results.js +19 -0
- package/registry/examples/command/no-results.tsx +20 -0
- package/registry/examples/countdown/native/urgent.dark.png +0 -0
- package/registry/examples/countdown/native/urgent.light.png +0 -0
- package/registry/examples/countdown/native/with-label.d.ts +4 -0
- package/registry/examples/countdown/native/with-label.dark.png +0 -0
- package/registry/examples/countdown/native/with-label.js +13 -0
- package/registry/examples/countdown/native/with-label.light.png +0 -0
- package/registry/examples/countdown/native/with-label.tsx +7 -0
- package/registry/examples/countdown/with-label.d.ts +4 -0
- package/registry/examples/countdown/with-label.js +13 -0
- package/registry/examples/countdown/with-label.tsx +10 -0
- package/registry/field.json +1 -1
- package/registry/index.json +8 -8
- package/registry/theme.json +24 -9
- package/dist/chunk-6DJ6SQY3.js.map +0 -1
- /package/dist/{chunk-IUGBKMJC.js.map → chunk-FHYZSO2S.js.map} +0 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// registry/examples/countdown/native/with-label.tsx
|
|
4
|
+
import { Countdown } from "@krizaka/ui/native";
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
import { jsx } from "react/jsx-runtime";
|
|
7
|
+
function NativeCountdownWithLabel() {
|
|
8
|
+
const [target] = useState(() => Date.now() + 3723e3);
|
|
9
|
+
return /* @__PURE__ */ jsx(Countdown, { target, units: { d: "d", h: "h", m: "m", s: "s" }, label: "Ends in", showLabel: true });
|
|
10
|
+
}
|
|
11
|
+
export {
|
|
12
|
+
NativeCountdownWithLabel as default
|
|
13
|
+
};
|
|
Binary file
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Countdown } from "@krizaka/ui/native";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
|
|
4
|
+
export default function NativeCountdownWithLabel() {
|
|
5
|
+
const [target] = useState(() => Date.now() + 3_723_000);
|
|
6
|
+
return <Countdown target={target} units={{ d: "d", h: "h", m: "m", s: "s" }} label="Ends in" showLabel />;
|
|
7
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// registry/examples/countdown/with-label.tsx
|
|
4
|
+
import { Countdown } from "@krizaka/ui/countdown";
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
import { jsx } from "react/jsx-runtime";
|
|
7
|
+
function CountdownWithLabel() {
|
|
8
|
+
const [target] = useState(() => Date.now() + 3723e3);
|
|
9
|
+
return /* @__PURE__ */ jsx(Countdown, { target, units: { d: "d", h: "h", m: "m", s: "s" }, label: "Ends in", showLabel: true });
|
|
10
|
+
}
|
|
11
|
+
export {
|
|
12
|
+
CountdownWithLabel as default
|
|
13
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Countdown } from "@krizaka/ui/countdown";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
|
|
6
|
+
// `showLabel` writes the label before the segments (it stays the accessible name).
|
|
7
|
+
export default function CountdownWithLabel() {
|
|
8
|
+
const [target] = useState(() => Date.now() + 3_723_000);
|
|
9
|
+
return <Countdown target={target} units={{ d: "d", h: "h", m: "m", s: "s" }} label="Ends in" showLabel />;
|
|
10
|
+
}
|
package/registry/field.json
CHANGED
|
@@ -135,7 +135,7 @@
|
|
|
135
135
|
"files": [
|
|
136
136
|
{
|
|
137
137
|
"path": "field/field.tsx",
|
|
138
|
-
"content": "// Server-safe. The label ↔ control wiring is HTML (htmlFor / id, aria-describedby): no context.\nimport type * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport { cn } from \"../cn\";\n\nexport const field = tv({\n slots: {\n root: \"flex flex-col gap-1.5\",\n label: \"text-xs font-semibold text-fg-secondary\",\n control:\n \"flex h-11 w-full rounded-lg border border-border-default bg-surface-2 px-3 text-sm text-fg placeholder:text-fg-muted \" +\n \"transition-[border-color,box-shadow] focus-visible:border-accent focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring \" +\n \"disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-danger aria-invalid:ring-danger/30\",\n hint: \"text-xs text-fg-secondary\",\n // The
|
|
138
|
+
"content": "// Server-safe. The label ↔ control wiring is HTML (htmlFor / id, aria-describedby): no context.\nimport type * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport { cn } from \"../cn\";\n\nexport const field = tv({\n slots: {\n root: \"flex flex-col gap-1.5\",\n label: \"text-xs font-semibold text-fg-secondary\",\n control:\n \"flex h-11 w-full rounded-lg border border-border-default bg-surface-2 px-3 text-sm text-fg placeholder:text-fg-muted \" +\n \"transition-[border-color,box-shadow] focus-visible:border-accent focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring \" +\n \"disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-danger aria-invalid:ring-danger/30\",\n hint: \"text-xs text-fg-secondary\",\n // The message reads in the danger text role (≥ 4.5:1 on every surface, both themes); the dot is the danger fill.\n error: \"flex items-center gap-1.5 text-xs font-medium text-fg-danger before:h-1.5 before:w-1.5 before:shrink-0 before:rounded-full before:bg-danger\",\n selectWrap: \"relative flex w-full\",\n chevron: \"pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-fg-secondary\",\n },\n});\nconst s = field();\n\nexport const Field = {\n Root: ({ className, ...props }: React.ComponentProps<\"div\">) => <div className={s.root({ className })} {...props} />,\n Label: ({ className, ...props }: React.ComponentProps<\"label\">) => <label className={s.label({ className })} {...props} />,\n Hint: ({ className, ...props }: React.ComponentProps<\"p\">) => <p className={s.hint({ className })} {...props} />,\n Error: ({ className, ...props }: React.ComponentProps<\"p\">) => <p role=\"alert\" className={s.error({ className })} {...props} />,\n};\n\ntype ControlProps = {\n /** Marks the control invalid: aria-invalid and data-invalid (pair it with a `Field.Error` in aria-describedby). */\n invalid?: boolean;\n};\n\nconst invalidProps = (invalid?: boolean) => (invalid ? { \"aria-invalid\": true as const, \"data-invalid\": \"\" } : {});\n\nexport function Input({ invalid, className, ...props }: React.ComponentProps<\"input\"> & ControlProps) {\n return <input {...invalidProps(invalid)} className={s.control({ className })} {...props} />;\n}\n\nexport function Textarea({ invalid, className, ...props }: React.ComponentProps<\"textarea\"> & ControlProps) {\n return <textarea {...invalidProps(invalid)} className={s.control({ className: cn(\"h-auto min-h-24 py-2\", className) })} {...props} />;\n}\n\n/** A native <select>, styled like the other controls (the platform's menu: accessible and right on phones). */\nexport function Select({ invalid, className, ...props }: React.ComponentProps<\"select\"> & ControlProps) {\n return (\n <span className={s.selectWrap()}>\n <select {...invalidProps(invalid)} className={s.control({ className: cn(\"appearance-none pr-9\", className) })} {...props} />\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden className={s.chevron()}>\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n </span>\n );\n}\n"
|
|
139
139
|
},
|
|
140
140
|
{
|
|
141
141
|
"path": "field/index.ts",
|
package/registry/index.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@krizaka/ui",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"items": [
|
|
5
5
|
{
|
|
6
6
|
"name": "alert",
|
|
@@ -61,14 +61,14 @@
|
|
|
61
61
|
"name": "button",
|
|
62
62
|
"type": "primitive",
|
|
63
63
|
"title": "Button",
|
|
64
|
-
"summary": "The action: a button in
|
|
65
|
-
"description": "The action: a button in
|
|
64
|
+
"summary": "The action: a button in six looks (primary, gradient, secondary, outline, ghost, danger), three sizes and two shapes.",
|
|
65
|
+
"description": "The action: a button in six looks (primary, gradient, secondary, outline, ghost, danger), three sizes and two shapes.",
|
|
66
66
|
"status": "stable",
|
|
67
67
|
"category": "actions",
|
|
68
68
|
"platforms": "both",
|
|
69
69
|
"examples": {
|
|
70
|
-
"web":
|
|
71
|
-
"native":
|
|
70
|
+
"web": 12,
|
|
71
|
+
"native": 11
|
|
72
72
|
},
|
|
73
73
|
"dependencies": [
|
|
74
74
|
"radix-ui@^1.7.0",
|
|
@@ -164,7 +164,7 @@
|
|
|
164
164
|
"category": "navigation",
|
|
165
165
|
"platforms": "web",
|
|
166
166
|
"examples": {
|
|
167
|
-
"web":
|
|
167
|
+
"web": 3,
|
|
168
168
|
"native": 0
|
|
169
169
|
},
|
|
170
170
|
"dependencies": [
|
|
@@ -208,8 +208,8 @@
|
|
|
208
208
|
"category": "data-display",
|
|
209
209
|
"platforms": "both",
|
|
210
210
|
"examples": {
|
|
211
|
-
"web":
|
|
212
|
-
"native":
|
|
211
|
+
"web": 6,
|
|
212
|
+
"native": 5
|
|
213
213
|
},
|
|
214
214
|
"dependencies": [
|
|
215
215
|
"tailwind-variants@^3.3.1"
|
package/registry/theme.json
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
],
|
|
23
23
|
"bestPractices": [
|
|
24
24
|
"Put `<ThemeScript />` in `<head>` and `suppressHydrationWarning` on `<html>`: the right theme is applied before the first paint.",
|
|
25
|
+
"A product that is dark (or light) first passes the same `defaultMode` to `ThemeScript` and `ThemeProvider`; the visitor's choice still wins.",
|
|
25
26
|
"The choice is persisted as `kz-theme` and applied as `html.light`: every Krizaka app reads the same switch.",
|
|
26
27
|
"Check every screen in both themes."
|
|
27
28
|
],
|
|
@@ -53,15 +54,22 @@
|
|
|
53
54
|
"name": "defaultMode",
|
|
54
55
|
"type": "Mode",
|
|
55
56
|
"default": "system",
|
|
56
|
-
"description": "The mode before
|
|
57
|
+
"description": "The mode before the visitor chooses one (nothing persisted as `kz-theme`) — pass the same value to `ThemeScript`.",
|
|
57
58
|
"required": false
|
|
58
59
|
}
|
|
59
60
|
]
|
|
60
61
|
},
|
|
61
62
|
{
|
|
62
63
|
"component": "ThemeScript",
|
|
63
|
-
"description": "In <head>, before anything renders: applies the persisted mode and named theme, so the page
|
|
64
|
+
"description": "In <head>, before anything renders: applies the persisted mode (else `defaultMode`) and named theme, so the page\nnever flashes. `nonce` for a strict Content-Security-Policy.",
|
|
64
65
|
"props": [
|
|
66
|
+
{
|
|
67
|
+
"name": "defaultMode",
|
|
68
|
+
"type": "Mode",
|
|
69
|
+
"default": "system",
|
|
70
|
+
"description": "The mode before the visitor chooses one — pass the same value as `ThemeProvider`'s `defaultMode`. A persisted\nchoice (`kz-theme`) always wins.",
|
|
71
|
+
"required": false
|
|
72
|
+
},
|
|
65
73
|
{
|
|
66
74
|
"name": "nonce",
|
|
67
75
|
"type": "string",
|
|
@@ -91,7 +99,7 @@
|
|
|
91
99
|
},
|
|
92
100
|
{
|
|
93
101
|
"name": "variant",
|
|
94
|
-
"type": "\"danger\" | \"primary\" | \"secondary\" | \"outline\" | \"ghost\"",
|
|
102
|
+
"type": "\"danger\" | \"primary\" | \"gradient\" | \"secondary\" | \"outline\" | \"ghost\"",
|
|
95
103
|
"default": "ghost",
|
|
96
104
|
"description": "Any `Button` variant.",
|
|
97
105
|
"required": false
|
|
@@ -187,15 +195,15 @@
|
|
|
187
195
|
},
|
|
188
196
|
{
|
|
189
197
|
"path": "theme/index.ts",
|
|
190
|
-
"content": "/** @krizaka/ui/theme — `ThemeScript`, `ThemeProvider`, `useTheme`, `ThemeToggle`. Client (\"use client\"). */\nexport {\n type Mode,\n type ThemeContextValue,\n ThemeProvider,\n type ThemeProviderProps,\n ThemeScript,\n ThemeToggle,\n type ThemeToggleProps,\n useTheme,\n} from \"./theme\";\n"
|
|
198
|
+
"content": "/** @krizaka/ui/theme — `ThemeScript`, `ThemeProvider`, `useTheme`, `ThemeToggle`. Client (\"use client\"). */\nexport {\n type Mode,\n type ThemeContextValue,\n ThemeProvider,\n type ThemeProviderProps,\n ThemeScript,\n type ThemeScriptProps,\n ThemeToggle,\n type ThemeToggleProps,\n useTheme,\n} from \"./theme\";\n"
|
|
191
199
|
},
|
|
192
200
|
{
|
|
193
201
|
"path": "theme/meta.ts",
|
|
194
|
-
"content": "import type { ComponentMeta } from \"../meta\";\n\nexport const meta = {\n title: \"Theme\",\n summary: \"Dark, light or the system's: `ThemeProvider`, `ThemeScript` against the flash, and `ThemeToggle`.\",\n status: \"stable\",\n category: \"foundations\",\n platforms: \"both\",\n whenToUse: [\n \"Once per app, in the root layout: `ThemeScript` in `<head>`, `ThemeProvider` around the app.\",\n \"`ThemeToggle` in the header or the settings, to cycle dark → light → system.\",\n \"`useTheme()` to read or set the mode from your own control.\",\n ],\n whenNotToUse: [\n { when: \"To theme one area differently: put `.theme-light` or `.theme-dark` on the subtree instead.\" },\n { when: \"To change a product's colours: override the `--kz-*` tokens in its design system.\" },\n ],\n bestPractices: [\n \"Put `<ThemeScript />` in `<head>` and `suppressHydrationWarning` on `<html>`: the right theme is applied before the first paint.\",\n \"The choice is persisted as `kz-theme` and applied as `html.light`: every Krizaka app reads the same switch.\",\n \"Check every screen in both themes.\",\n ],\n accessibility: {\n keyboard: [{ keys: \"Enter / Space\", action: \"The toggle cycles to the next mode.\" }],\n notes: [\"`ThemeToggle` is a button named by `label` (translated); its icon shows the current mode.\", \"`color-scheme` follows the theme, so native controls and scrollbars match.\"],\n },\n related: [\"button\", \"cn\"],\n web: {\n imports: [\"ThemeProvider\", \"ThemeScript\", \"ThemeToggle\", \"useTheme\"],\n examples: [\n { name: \"toggle\", title: \"Toggle\", description: \"A ghost icon button in its provider.\" },\n { name: \"secondary\", title: \"Another look\", description: \"Any button variant and shape.\" },\n ],\n },\n native: {\n imports: [\"ThemeProvider\", \"useTheme\"],\n differences: [\n \"`ThemeProvider` takes the product's colour roles (`overrides`) and fonts; there is no `ThemeScript` and no toggle.\",\n \"`useTheme()` returns the resolved `theme` (colour roles), `scheme`, `radius` and `motion` for `StyleSheet`.\",\n \"The mode is not persisted: the app stores it from `onModeChange`.\",\n ],\n examples: [{ name: \"provider\", title: \"Provider\", description: \"Once at the root; `useTheme()` reads the roles in any component.\" }],\n },\n} satisfies ComponentMeta;\n"
|
|
202
|
+
"content": "import type { ComponentMeta } from \"../meta\";\n\nexport const meta = {\n title: \"Theme\",\n summary: \"Dark, light or the system's: `ThemeProvider`, `ThemeScript` against the flash, and `ThemeToggle`.\",\n status: \"stable\",\n category: \"foundations\",\n platforms: \"both\",\n whenToUse: [\n \"Once per app, in the root layout: `ThemeScript` in `<head>`, `ThemeProvider` around the app.\",\n \"`ThemeToggle` in the header or the settings, to cycle dark → light → system.\",\n \"`useTheme()` to read or set the mode from your own control.\",\n ],\n whenNotToUse: [\n { when: \"To theme one area differently: put `.theme-light` or `.theme-dark` on the subtree instead.\" },\n { when: \"To change a product's colours: override the `--kz-*` tokens in its design system.\" },\n ],\n bestPractices: [\n \"Put `<ThemeScript />` in `<head>` and `suppressHydrationWarning` on `<html>`: the right theme is applied before the first paint.\",\n \"A product that is dark (or light) first passes the same `defaultMode` to `ThemeScript` and `ThemeProvider`; the visitor's choice still wins.\",\n \"The choice is persisted as `kz-theme` and applied as `html.light`: every Krizaka app reads the same switch.\",\n \"Check every screen in both themes.\",\n ],\n accessibility: {\n keyboard: [{ keys: \"Enter / Space\", action: \"The toggle cycles to the next mode.\" }],\n notes: [\"`ThemeToggle` is a button named by `label` (translated); its icon shows the current mode.\", \"`color-scheme` follows the theme, so native controls and scrollbars match.\"],\n },\n related: [\"button\", \"cn\"],\n web: {\n imports: [\"ThemeProvider\", \"ThemeScript\", \"ThemeToggle\", \"useTheme\"],\n examples: [\n { name: \"toggle\", title: \"Toggle\", description: \"A ghost icon button in its provider.\" },\n { name: \"secondary\", title: \"Another look\", description: \"Any button variant and shape.\" },\n ],\n },\n native: {\n imports: [\"ThemeProvider\", \"useTheme\"],\n differences: [\n \"`ThemeProvider` takes the product's colour roles (`overrides`) and fonts; there is no `ThemeScript` and no toggle.\",\n \"`useTheme()` returns the resolved `theme` (colour roles), `scheme`, `radius` and `motion` for `StyleSheet`.\",\n \"The mode is not persisted: the app stores it from `onModeChange`.\",\n ],\n examples: [{ name: \"provider\", title: \"Provider\", description: \"Once at the root; `useTheme()` reads the roles in any component.\" }],\n },\n} satisfies ComponentMeta;\n"
|
|
195
203
|
},
|
|
196
204
|
{
|
|
197
205
|
"path": "theme/theme.tsx",
|
|
198
|
-
"content": "// Client. ONE mechanism for the whole organisation: dark by default, `html.light` for light, `html.theme-<name>` for a\n// named theme, persisted in localStorage (`kz-theme` for the mode, `kz-theme-name` for the named theme).\nimport * as React from \"react\";\n\nimport { type ButtonVariants, buttonVariants } from \"../button/button\";\n\nexport type Mode = \"dark\" | \"light\" | \"system\";\n\nconst MODE_KEY = \"kz-theme\";\nconst NAME_KEY = \"kz-theme-name\";\nconst MODES: readonly Mode[] = [\"dark\", \"light\", \"system\"];\nconst DARK_QUERY = \"(prefers-color-scheme: dark)\";\n\nconst isMode = (value: unknown): value is Mode => MODES.includes(value as Mode);\n\n// Storage blocked (private mode, sandbox): the choice lives in memory for the page.\nconst memory = new Map<string, string>();\n\nfunction read(key: string): string | null {\n try {\n return localStorage.getItem(key);\n } catch {\n return memory.get(key) ?? null;\n }\n}\n\nfunction write(key: string, value: string | null) {\n if (value === null) memory.delete(key);\n else memory.set(key, value);\n try {\n if (value === null) localStorage.removeItem(key);\n else localStorage.setItem(key, value);\n } catch {\n // storage blocked: memory holds it\n }\n}\n\n// The persisted choice is an external store: every provider and tab reads the same value, the server reads none.\nconst listeners = new Set<() => void>();\nconst notify = () => listeners.forEach((listener) => listener());\nfunction subscribe(listener: () => void) {\n listeners.add(listener);\n window.addEventListener(\"storage\", listener);\n return () => {\n listeners.delete(listener);\n window.removeEventListener(\"storage\", listener);\n };\n}\n\nfunction applyMode(mode: Mode) {\n const dark = window.matchMedia(DARK_QUERY).matches;\n document.documentElement.classList.toggle(\"light\", mode === \"light\" || (mode === \"system\" && !dark));\n}\n\nfunction applyName(previous: string | null, next: string | null) {\n const classes = document.documentElement.classList;\n if (previous) classes.remove(`theme-${previous}`);\n if (next) classes.add(`theme-${next}`);\n}\n\n/**\n * In <head>, before anything renders: applies the persisted mode and named theme, so the page never flashes
|
|
206
|
+
"content": "// Client. ONE mechanism for the whole organisation: dark by default, `html.light` for light, `html.theme-<name>` for a\n// named theme, persisted in localStorage (`kz-theme` for the mode, `kz-theme-name` for the named theme).\nimport * as React from \"react\";\n\nimport { type ButtonVariants, buttonVariants } from \"../button/button\";\n\nexport type Mode = \"dark\" | \"light\" | \"system\";\n\nconst MODE_KEY = \"kz-theme\";\nconst NAME_KEY = \"kz-theme-name\";\nconst MODES: readonly Mode[] = [\"dark\", \"light\", \"system\"];\nconst DARK_QUERY = \"(prefers-color-scheme: dark)\";\n\nconst isMode = (value: unknown): value is Mode => MODES.includes(value as Mode);\n\n// Storage blocked (private mode, sandbox): the choice lives in memory for the page.\nconst memory = new Map<string, string>();\n\nfunction read(key: string): string | null {\n try {\n return localStorage.getItem(key);\n } catch {\n return memory.get(key) ?? null;\n }\n}\n\nfunction write(key: string, value: string | null) {\n if (value === null) memory.delete(key);\n else memory.set(key, value);\n try {\n if (value === null) localStorage.removeItem(key);\n else localStorage.setItem(key, value);\n } catch {\n // storage blocked: memory holds it\n }\n}\n\n// The persisted choice is an external store: every provider and tab reads the same value, the server reads none.\nconst listeners = new Set<() => void>();\nconst notify = () => listeners.forEach((listener) => listener());\nfunction subscribe(listener: () => void) {\n listeners.add(listener);\n window.addEventListener(\"storage\", listener);\n return () => {\n listeners.delete(listener);\n window.removeEventListener(\"storage\", listener);\n };\n}\n\nfunction applyMode(mode: Mode) {\n const dark = window.matchMedia(DARK_QUERY).matches;\n document.documentElement.classList.toggle(\"light\", mode === \"light\" || (mode === \"system\" && !dark));\n}\n\nfunction applyName(previous: string | null, next: string | null) {\n const classes = document.documentElement.classList;\n if (previous) classes.remove(`theme-${previous}`);\n if (next) classes.add(`theme-${next}`);\n}\n\nexport type ThemeScriptProps = {\n /** The nonce of a strict Content-Security-Policy (`script-src 'nonce-…'`). */\n nonce?: string;\n /**\n * The mode before the visitor chooses one — pass the same value as `ThemeProvider`'s `defaultMode`. A persisted\n * choice (`kz-theme`) always wins.\n * @default \"system\"\n */\n defaultMode?: Mode;\n};\n\n/**\n * In <head>, before anything renders: applies the persisted mode (else `defaultMode`) and named theme, so the page\n * never flashes. `nonce` for a strict Content-Security-Policy.\n */\nexport function ThemeScript({ nonce, defaultMode = \"system\" }: ThemeScriptProps) {\n const fallback = isMode(defaultMode) ? defaultMode : \"system\";\n const js =\n `(function(){try{var m=localStorage.getItem(\"${MODE_KEY}\");if(m!==\"dark\"&&m!==\"light\"&&m!==\"system\")m=\"${fallback}\";` +\n `var n=localStorage.getItem(\"${NAME_KEY}\");var d=matchMedia(\"${DARK_QUERY}\").matches;var c=document.documentElement.classList;` +\n `c.toggle(\"light\",m===\"light\"||(m===\"system\"&&!d));if(n)c.add(\"theme-\"+n)}catch(e){}})()`;\n return <script nonce={nonce} dangerouslySetInnerHTML={{ __html: js }} />;\n}\n\nexport type ThemeContextValue = {\n /** The chosen mode: dark, light or follow the system. */\n mode: Mode;\n setMode: (mode: Mode) => void;\n /** The named theme (`html.theme-<name>`), or null. */\n theme: string | null;\n setTheme: (theme: string | null) => void;\n};\n\nconst ThemeContext = React.createContext<ThemeContextValue | null>(null);\n\nexport type ThemeProviderProps = {\n children: React.ReactNode;\n /**\n * The mode before the visitor chooses one (nothing persisted as `kz-theme`) — pass the same value to `ThemeScript`.\n * @default \"system\"\n */\n defaultMode?: Mode;\n};\n\nexport function ThemeProvider({ children, defaultMode = \"system\" }: ThemeProviderProps) {\n // Before hydration the server knows nothing persisted (ThemeScript has already applied it to <html>).\n const stored = React.useSyncExternalStore(subscribe, () => read(MODE_KEY), () => null);\n const mode: Mode = isMode(stored) ? stored : defaultMode;\n const theme = React.useSyncExternalStore(subscribe, () => read(NAME_KEY), () => null);\n\n // The same resolution as ThemeScript (a no-op after it), for an app that renders the provider without the script.\n React.useEffect(() => {\n const persisted = read(MODE_KEY);\n applyMode(isMode(persisted) ? persisted : defaultMode);\n }, [defaultMode]);\n\n // \"system\" follows the operating system while the page is open.\n React.useEffect(() => {\n if (mode !== \"system\") return;\n const query = window.matchMedia(DARK_QUERY);\n const onChange = () => applyMode(\"system\");\n query.addEventListener?.(\"change\", onChange);\n return () => query.removeEventListener?.(\"change\", onChange);\n }, [mode]);\n\n const setMode = React.useCallback((next: Mode) => {\n write(MODE_KEY, next);\n applyMode(next);\n notify();\n }, []);\n\n const setTheme = React.useCallback((next: string | null) => {\n applyName(read(NAME_KEY), next);\n write(NAME_KEY, next);\n notify();\n }, []);\n\n const value = React.useMemo(() => ({ mode, setMode, theme, setTheme }), [mode, setMode, theme, setTheme]);\n return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;\n}\n\nexport function useTheme(): ThemeContextValue {\n const context = React.useContext(ThemeContext);\n if (!context) throw new Error(\"useTheme: wrap the app in <ThemeProvider>\");\n return context;\n}\n\nconst ICONS: Record<Mode, React.ReactNode> = {\n dark: <path d=\"M20 14.5A8 8 0 0 1 9.5 4a8 8 0 1 0 10.5 10.5Z\" />,\n light: (\n <>\n <circle cx=\"12\" cy=\"12\" r=\"4\" />\n <path d=\"M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4\" />\n </>\n ),\n system: (\n <>\n <rect x=\"3\" y=\"4\" width=\"18\" height=\"12\" rx=\"2\" />\n <path d=\"M8 20h8M12 16v4\" />\n </>\n ),\n};\n\nexport type ThemeToggleProps = Omit<React.ComponentProps<\"button\">, \"onClick\" | \"children\"> &\n Pick<ButtonVariants, \"variant\" | \"shape\"> & {\n /** Any `Button` variant. */\n variant?: ButtonVariants[\"variant\"];\n /**\n * rounded · pill.\n * @default \"rounded\"\n */\n shape?: ButtonVariants[\"shape\"];\n /** The accessible name — a string, or one per current mode (e.g. `(mode) => t(\"theme.\" + mode)`). */\n label: string | ((mode: Mode) => string);\n };\n\n/**\n * An icon button (styled as `IconButton`, without its Slot) that cycles dark → light → system. Needs a\n * <ThemeProvider> above it. State: `data-mode`.\n */\nexport function ThemeToggle({ label, variant = \"ghost\", shape, className, ...props }: ThemeToggleProps) {\n const { mode, setMode } = useTheme();\n const next = MODES[(MODES.indexOf(mode) + 1) % MODES.length] as Mode;\n const name = typeof label === \"function\" ? label(mode) : label;\n return (\n <button\n type=\"button\"\n aria-label={name}\n title={name}\n data-mode={mode}\n onClick={() => setMode(next)}\n className={buttonVariants({ variant, shape, size: \"icon\", className })}\n {...props}\n >\n <svg viewBox=\"0 0 24 24\" className=\"h-5 w-5\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n {ICONS[mode]}\n </svg>\n </button>\n );\n}\n"
|
|
199
207
|
}
|
|
200
208
|
],
|
|
201
209
|
"dependencies": [],
|
|
@@ -215,15 +223,22 @@
|
|
|
215
223
|
"name": "defaultMode",
|
|
216
224
|
"type": "Mode",
|
|
217
225
|
"default": "system",
|
|
218
|
-
"description": "The mode before
|
|
226
|
+
"description": "The mode before the visitor chooses one (nothing persisted as `kz-theme`) — pass the same value to `ThemeScript`.",
|
|
219
227
|
"required": false
|
|
220
228
|
}
|
|
221
229
|
]
|
|
222
230
|
},
|
|
223
231
|
{
|
|
224
232
|
"component": "ThemeScript",
|
|
225
|
-
"description": "In <head>, before anything renders: applies the persisted mode and named theme, so the page
|
|
233
|
+
"description": "In <head>, before anything renders: applies the persisted mode (else `defaultMode`) and named theme, so the page\nnever flashes. `nonce` for a strict Content-Security-Policy.",
|
|
226
234
|
"props": [
|
|
235
|
+
{
|
|
236
|
+
"name": "defaultMode",
|
|
237
|
+
"type": "Mode",
|
|
238
|
+
"default": "system",
|
|
239
|
+
"description": "The mode before the visitor chooses one — pass the same value as `ThemeProvider`'s `defaultMode`. A persisted\nchoice (`kz-theme`) always wins.",
|
|
240
|
+
"required": false
|
|
241
|
+
},
|
|
227
242
|
{
|
|
228
243
|
"name": "nonce",
|
|
229
244
|
"type": "string",
|
|
@@ -253,7 +268,7 @@
|
|
|
253
268
|
},
|
|
254
269
|
{
|
|
255
270
|
"name": "variant",
|
|
256
|
-
"type": "\"danger\" | \"primary\" | \"secondary\" | \"outline\" | \"ghost\"",
|
|
271
|
+
"type": "\"danger\" | \"primary\" | \"gradient\" | \"secondary\" | \"outline\" | \"ghost\"",
|
|
257
272
|
"default": "ghost",
|
|
258
273
|
"description": "Any `Button` variant.",
|
|
259
274
|
"required": false
|
|
@@ -1 +0,0 @@
|
|
|
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-fg-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 /**\n * The look: `primary` (the accent), `secondary` (a raised surface), `outline`, `ghost`, `danger`.\n * @default \"secondary\"\n */\n variant?: ButtonVariants[\"variant\"];\n /**\n * sm · md · lg, or `icon` (a square).\n * @default \"md\"\n */\n size?: ButtonVariants[\"size\"];\n /**\n * rounded · pill.\n * @default \"rounded\"\n */\n shape?: ButtonVariants[\"shape\"];\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 /**\n * sm · md · lg, or `icon` (a square).\n * @default \"icon\"\n */\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;AAmDlC;AAjDG,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;AA2BM,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;AAaO,SAAS,WAAW,EAAE,OAAO,OAAO,QAAQ,GAAG,MAAM,GAAoB;AAC9E,SAAO,oBAAC,UAAO,cAAY,OAAO,OAAO,OAAO,MAAa,GAAG,OAAO;AACzE;","names":[]}
|
|
File without changes
|