@djangocfg/ui-core 2.1.426 → 2.1.427
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/package.json +4 -4
- package/src/components/data/badge/index.tsx +1 -1
- package/src/components/data/calendar/calendar.tsx +2 -2
- package/src/components/data/stat/index.tsx +5 -5
- package/src/components/data/status/index.tsx +3 -3
- package/src/components/data/table/index.tsx +30 -11
- package/src/components/feedback/banner/index.tsx +5 -4
- package/src/components/forms/button/index.tsx +15 -5
- package/src/components/forms/button-download/index.tsx +2 -2
- package/src/components/forms/checkbox/index.tsx +1 -1
- package/src/components/forms/editable/index.tsx +19 -19
- package/src/components/forms/input/index.tsx +44 -9
- package/src/components/forms/otp/index.tsx +1 -1
- package/src/components/forms/setting-row/index.tsx +359 -0
- package/src/components/forms/switch/index.tsx +1 -1
- package/src/components/forms/tags-input/index.tsx +1 -1
- package/src/components/forms/textarea/index.tsx +3 -8
- package/src/components/index.ts +2 -0
- package/src/components/navigation/dropdown-menu/index.tsx +3 -1
- package/src/components/navigation/menu/menu-builder.tsx +7 -2
- package/src/components/navigation/stepper/index.tsx +1 -1
- package/src/components/navigation/tabs/index.tsx +3 -3
- package/src/components/overlay/dialog/index.tsx +8 -3
- package/src/components/overlay/sheet/index.tsx +1 -1
- package/src/components/overlay/tooltip/index.tsx +4 -1
- package/src/components/select/multi-select-pro-async.tsx +2 -2
- package/src/components/select/multi-select-pro.tsx +2 -2
- package/src/components/specialized/copy/index.tsx +2 -2
- package/src/components/specialized/item/index.tsx +1 -1
- package/src/styles/README.md +49 -10
- package/src/styles/base.css +18 -1
- package/src/styles/theme/dark.css +40 -26
- package/src/styles/theme/light.css +13 -7
- package/src/styles/theme/tokens.css +1 -0
- package/src/styles/utilities/controls.css +12 -0
- package/src/styles/utilities/divider.css +23 -0
- package/src/styles/utilities.css +2 -0
- package/src/theme/ThemeSegmented.tsx +73 -0
- package/src/theme/index.ts +2 -0
- package/src/types/index.ts +0 -0
package/src/styles/README.md
CHANGED
|
@@ -9,8 +9,9 @@ styles/
|
|
|
9
9
|
├── full.css # Golden path (recommended) — Tailwind + tokens + base + utilities, cascade-layer-safe
|
|
10
10
|
├── index.css # Plain entry (no Tailwind, unlayered) — you own layer ordering
|
|
11
11
|
├── theme.css # Imports tokens.css → animations → light → dark
|
|
12
|
-
├── base.css # Resets + `*` border-color + body bg/color
|
|
13
|
-
├── utilities.css # Custom utilities
|
|
12
|
+
├── base.css # Resets + `*` border-color + body bg/color + radius scale + native focus-outline reset
|
|
13
|
+
├── utilities.css # Custom utilities entry — imports utilities/*
|
|
14
|
+
│ └── utilities/ # display · divider · controls · step · animations · glass · marquee
|
|
14
15
|
├── sources.css # @source directives for monorepo class detection
|
|
15
16
|
├── palette/ # JS-readable color access (Canvas/SVG/Mermaid)
|
|
16
17
|
└── theme/
|
|
@@ -53,17 +54,18 @@ This makes opacity modifiers (`bg-card/40`, `border-foreground/20`) resolve thro
|
|
|
53
54
|
| `bg-card` / `text-card-foreground` | `--card` | Card surface (elevated over background) |
|
|
54
55
|
| `bg-popover` / `text-popover-foreground` | `--popover` | Floating menus, tooltips, dropdowns |
|
|
55
56
|
| `bg-muted` / `text-muted-foreground` | `--muted` | Subtle surface (input rest, chips, secondary text) |
|
|
56
|
-
| `bg-accent` / `text-accent-foreground` | `--accent` | Hover surface |
|
|
57
|
-
| `bg-primary` / `text-primary-foreground` | `--primary` | Brand CTA (filled buttons, links) |
|
|
57
|
+
| `bg-accent` / `text-accent-foreground` | `--accent` | Hover + selected surface — **neutral gray** (no brand tint), quiet like macOS/Claude |
|
|
58
|
+
| `bg-primary` / `text-primary-foreground` | `--primary` | Brand CTA (filled buttons, links) — cyan |
|
|
58
59
|
| `bg-secondary` / `text-secondary-foreground` | `--secondary` | Neutral filled controls |
|
|
59
60
|
| `bg-destructive` / `text-destructive-foreground` | `--destructive` | Error / delete filled controls |
|
|
60
|
-
| `border-border` | `--border` |
|
|
61
|
-
| `
|
|
62
|
-
| `
|
|
61
|
+
| `border-border` | `--border` | Card outlines, control borders |
|
|
62
|
+
| `border-divider` / `.divider-b` | `--divider` | **Hairline between rows** — deliberately *lighter than `--card`* so it stays visible on elevated surfaces (a `--border` line vanishes on a card) |
|
|
63
|
+
| `bg-input` | `--input` | Input **fill** — a notch off the panel so fields read as real controls (not flush holes). The input *border* uses `--border`, not `--input` |
|
|
64
|
+
| `ring-ring` | `--ring` | Focus rings, selected outlines — **blue** (system-accent feel), independent of the cyan brand |
|
|
63
65
|
|
|
64
|
-
### Status surface tokens (
|
|
66
|
+
### Status surface tokens (light + dark)
|
|
65
67
|
|
|
66
|
-
Each status has 4
|
|
68
|
+
Each status has the full 4-token set in **both** themes for banners and alerts:
|
|
67
69
|
|
|
68
70
|
| Role | Class | Token |
|
|
69
71
|
|---|---|---|
|
|
@@ -72,7 +74,7 @@ Each status has 4 tokens for banners and alerts:
|
|
|
72
74
|
| Readable text | `text-warning-foreground` | `--warning-foreground` |
|
|
73
75
|
| Border ring | `border-warning-border` | `--warning-border` |
|
|
74
76
|
|
|
75
|
-
Available statuses: **`warning`** · **`success`** · **`destructive`** · **`info`**.
|
|
77
|
+
Available statuses: **`warning`** · **`success`** · **`destructive`** · **`info`**. Reference consumers: `feedback/banner`, `specialized/copy`, `forms/button-download`, `data/status`, `data/stat`.
|
|
76
78
|
|
|
77
79
|
```tsx
|
|
78
80
|
<div className="flex items-center gap-3 rounded-md border border-warning-border/40
|
|
@@ -97,6 +99,43 @@ Most presets don't override `*-background` / `*-border` / `*-foreground`. For pr
|
|
|
97
99
|
| `bg-sidebar` / `text-sidebar-foreground` / `border-sidebar-border` | App sidebar chrome |
|
|
98
100
|
| `bg-sidebar-accent` / `text-sidebar-accent-foreground` | Sidebar hover state |
|
|
99
101
|
|
|
102
|
+
## Radius tokens
|
|
103
|
+
|
|
104
|
+
The radius **scale** (`--radius`, `--radius-sm`, …) is theme-independent and lives in `base.css`; presets that set a semantic `radius` regenerate the scale via `presets/build.ts`. A few named radii are fixed defaults (default preset only):
|
|
105
|
+
|
|
106
|
+
| Token | Value | Used by |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| `--radius-control` | `0.625rem` | **Interactive controls** (inputs, nav items, search, value chips) — one shared corner so they round consistently. Class: `.rounded-control` |
|
|
109
|
+
| `--radius-dialog` | `1rem` | Dialog panels. Applied at **all** sizes (was `sm:`-gated, which left phones square) |
|
|
110
|
+
| `--radius-popover` | `0.75rem` | Popovers / menus |
|
|
111
|
+
|
|
112
|
+
## Focus rings (Vercel / Linear pattern)
|
|
113
|
+
|
|
114
|
+
Inputs use a **crisp thin accent outline**, not a blurry halo: the border turns `--ring` (blue) plus a tight `ring-1 ring-ring`, `:focus-visible` only.
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
focus-visible:border-ring focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The native browser focus outline (often white/auto, which used to pierce through on click) is reset app-wide in `base.css`:
|
|
121
|
+
|
|
122
|
+
```css
|
|
123
|
+
:where(input, textarea, select, button, a, [tabindex], [role="button"], [contenteditable]):focus { outline: none; }
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
So every interactive control relies on its own `focus-visible:ring-*` — keyboard a11y is preserved, the stray native outline is gone. The single source for input styling is `INPUT_CLASS` / `inputClass(size)` exported from `components/forms/input` and **reused by `Editable`**, so standalone inputs and inline-edits focus identically.
|
|
127
|
+
|
|
128
|
+
## Scan-independent utility classes
|
|
129
|
+
|
|
130
|
+
Some classes are authored as **plain CSS** in `utilities/*.css` (not Tailwind utilities):
|
|
131
|
+
|
|
132
|
+
| Class | File | Why plain CSS |
|
|
133
|
+
|---|---|---|
|
|
134
|
+
| `.divider-b` / `.divider-t` | `utilities/divider.css` | Hairline via `--divider` |
|
|
135
|
+
| `.rounded-control` | `utilities/controls.css` | Shared control radius |
|
|
136
|
+
|
|
137
|
+
> **JIT-scan gotcha.** Tailwind's content scan covers `ui-core/src` but **not always every consumer package** (e.g. the `layouts` package source isn't always scanned by Storybook/apps). A *new* arbitrary Tailwind class used **only** in a consumer (`border-zinc-500/25`, `border-foreground/[0.12]`, `rounded-[var(--x)]`) then produces **no CSS rule** and silently falls back to the global `* { border-color: var(--border) }` — the class is in the DOM but the computed color is wrong. **Fix pattern:** for any token-driven visual that consumers need, add a plain `.class` in `ui-core/styles/utilities/*` and `@import` it, then use that class downstream. Don't invent new Tailwind classes in the `layouts` package.
|
|
138
|
+
|
|
100
139
|
## Theme presets — 8 production-ready
|
|
101
140
|
|
|
102
141
|
| ID | Use case |
|
package/src/styles/base.css
CHANGED
|
@@ -8,8 +8,15 @@
|
|
|
8
8
|
:root {
|
|
9
9
|
--radius: 0.625rem;
|
|
10
10
|
--radius-sm: 0.375rem;
|
|
11
|
+
/* Control corner — shared by interactive controls (nav items, inputs,
|
|
12
|
+
* search, value chips) so they round consistently. ~10px reads like
|
|
13
|
+
* Claude/macOS. Override per preset for a tighter/softer feel. */
|
|
14
|
+
--radius-control: 0.625rem;
|
|
11
15
|
--radius-popover: 0.75rem;
|
|
12
|
-
|
|
16
|
+
/* Dialog corner — tightened from 1.5rem to read like Claude/macOS panels
|
|
17
|
+
* (round, but not pill-soft). Default preset only; preset themes that set
|
|
18
|
+
* their own `radius` are unaffected. */
|
|
19
|
+
--radius-dialog: 1rem;
|
|
13
20
|
--radius-tooltip: 0.375rem;
|
|
14
21
|
|
|
15
22
|
/* Typography tokens — overridable per preset */
|
|
@@ -29,6 +36,16 @@
|
|
|
29
36
|
border-color: var(--border);
|
|
30
37
|
}
|
|
31
38
|
|
|
39
|
+
/* Suppress the native (often white/auto) focus outline app-wide — every
|
|
40
|
+
* interactive ui-core component provides its own `focus-visible:ring-*` in the
|
|
41
|
+
* blue `--ring` token. Without this, clicking certain inputs (where the browser
|
|
42
|
+
* applies `:focus` but not `:focus-visible`) shows an ugly white outline ON TOP
|
|
43
|
+
* of our ring. Keyboard accessibility is preserved via the ring utilities.
|
|
44
|
+
*/
|
|
45
|
+
:where(input, textarea, select, button, a, [tabindex], [role="button"], [contenteditable]):focus {
|
|
46
|
+
outline: none;
|
|
47
|
+
}
|
|
48
|
+
|
|
32
49
|
html {
|
|
33
50
|
font-weight: 400;
|
|
34
51
|
}
|
|
@@ -13,30 +13,42 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
.dark {
|
|
16
|
-
/*
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
--
|
|
22
|
-
--
|
|
16
|
+
/* Warm-gray surface ladder mapped from Claude's dark theme (hue ~48–60, a
|
|
17
|
+
* faint warm tint instead of pure neutral). The VISIBLE page background is
|
|
18
|
+
* Claude's bg-100 (~14.5%) — NOT near-black; card/popover lift above it so
|
|
19
|
+
* panels and menus read as raised. muted recesses below the page.
|
|
20
|
+
* page ≈ bg-100 card ≈ bg-000(18.4%) popover ≈ slightly above card */
|
|
21
|
+
--background: hsl(60 2.7% 14.5%);
|
|
22
|
+
--foreground: hsl(48 33% 97%);
|
|
23
|
+
--card: hsl(60 2.1% 18.4%);
|
|
24
|
+
--card-foreground: hsl(48 33% 97%);
|
|
25
|
+
--popover: hsl(48 2.5% 21%);
|
|
26
|
+
--popover-foreground: hsl(48 33% 97%);
|
|
23
27
|
/* Exact brand: #00d9ff (same HSL as :root light primary) */
|
|
24
28
|
--primary: hsl(189 100% 50%);
|
|
25
29
|
/* Dark label on bright cyan — stronger contrast than white on #00d9ff */
|
|
26
30
|
--primary-foreground: hsl(0 0% 9%);
|
|
27
|
-
/* Secondary:
|
|
28
|
-
--secondary: hsl(
|
|
29
|
-
--secondary-foreground: hsl(
|
|
30
|
-
|
|
31
|
-
--muted
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
--accent
|
|
35
|
-
--
|
|
31
|
+
/* Secondary: elevated warm-gray control surface + light label */
|
|
32
|
+
--secondary: hsl(60 2.1% 18.4%);
|
|
33
|
+
--secondary-foreground: hsl(48 33% 97%);
|
|
34
|
+
/* Recessed surface — below the page (Claude bg-200), for input rest / chips. */
|
|
35
|
+
--muted: hsl(30 3.3% 11.8%);
|
|
36
|
+
--muted-foreground: hsl(48 5% 59%);
|
|
37
|
+
/* Hover/active surface (rails, menus, tabs) — a quiet warm lift above the page. */
|
|
38
|
+
--accent: hsl(48 3% 24%);
|
|
39
|
+
--accent-foreground: hsl(48 33% 97%);
|
|
40
|
+
--destructive: hsl(0 67% 60%);
|
|
36
41
|
--destructive-foreground: hsl(0 0% 98%);
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
--
|
|
42
|
+
/* Soft warm border — Claude draws hairlines from a light border at low
|
|
43
|
+
* contrast, so a mid warm-gray keeps lines visible but never harsh. */
|
|
44
|
+
--border: hsl(48 3% 28%);
|
|
45
|
+
/* Input surface — a notch above card so fields read as raised controls. */
|
|
46
|
+
--input: hsl(48 2.5% 24%);
|
|
47
|
+
/* Divider — hairline; slightly softer than --border so rows don't read as a
|
|
48
|
+
* hard rule (Claude menus/rows). */
|
|
49
|
+
--divider: hsl(48 3% 24%);
|
|
50
|
+
/* Focus ring — blue (system-accent feel), independent of the cyan brand. */
|
|
51
|
+
--ring: hsl(217 91% 60%);
|
|
40
52
|
--shadow-card: none;
|
|
41
53
|
|
|
42
54
|
/* Code surface — see light.css for the rationale. On dark we want
|
|
@@ -49,15 +61,17 @@
|
|
|
49
61
|
--code-border: hsl(0 0% 18%);
|
|
50
62
|
--code-inline: hsl(0 0% 14%);
|
|
51
63
|
--code-inline-foreground: hsl(0 0% 92%);
|
|
52
|
-
/* Sidebar
|
|
53
|
-
|
|
54
|
-
|
|
64
|
+
/* Sidebar — same warm-gray family as the page (Claude has NO black sidebar
|
|
65
|
+
* block; rail and page share one tone). A hair darker than --background for
|
|
66
|
+
* subtle separation, not a hard black slab. */
|
|
67
|
+
--sidebar-background: hsl(30 3% 11.8%);
|
|
68
|
+
--sidebar-foreground: hsl(48 33% 97%);
|
|
55
69
|
--sidebar-primary: hsl(189 100% 50%);
|
|
56
70
|
--sidebar-primary-foreground: hsl(0 0% 9%);
|
|
57
|
-
--sidebar-accent: hsl(
|
|
58
|
-
--sidebar-accent-foreground: hsl(
|
|
59
|
-
--sidebar-border: hsl(
|
|
60
|
-
--sidebar-ring: hsl(
|
|
71
|
+
--sidebar-accent: hsl(48 3% 24%);
|
|
72
|
+
--sidebar-accent-foreground: hsl(48 33% 97%);
|
|
73
|
+
--sidebar-border: hsl(48 3% 28%);
|
|
74
|
+
--sidebar-ring: hsl(217 91% 60%);
|
|
61
75
|
/* ── Status surfaces (dark) ──────────────────────────────────────
|
|
62
76
|
* Dark variants: dim backgrounds (~8-12% lightness) so banners
|
|
63
77
|
* don't blow out against the near-black page. Foreground raised
|
|
@@ -24,14 +24,20 @@
|
|
|
24
24
|
--secondary-foreground: hsl(0 0% 98%);
|
|
25
25
|
--muted: hsl(0 0% 96%);
|
|
26
26
|
--muted-foreground: hsl(0 0% 45%);
|
|
27
|
-
/*
|
|
28
|
-
|
|
27
|
+
/* Neutral hover/active surface — quiet gray lift, no hue tint
|
|
28
|
+
* (Claude/macOS settings convention). */
|
|
29
|
+
--accent: hsl(0 0% 94%);
|
|
29
30
|
--accent-foreground: hsl(0 0% 9%);
|
|
30
31
|
--destructive: hsl(0 84% 60%);
|
|
31
32
|
--destructive-foreground: hsl(0 0% 98%);
|
|
32
|
-
--border: hsl(0 0%
|
|
33
|
-
|
|
34
|
-
--
|
|
33
|
+
--border: hsl(0 0% 87%);
|
|
34
|
+
/* Input surface — faint gray fill so fields are visible on white cards. */
|
|
35
|
+
--input: hsl(0 0% 97%);
|
|
36
|
+
/* Divider — hairline between rows; slightly darker than --border so it reads
|
|
37
|
+
* on white cards. */
|
|
38
|
+
--divider: hsl(0 0% 88%);
|
|
39
|
+
/* Focus ring — blue (system-accent feel), independent of the cyan brand. */
|
|
40
|
+
--ring: hsl(217 91% 55%);
|
|
35
41
|
--shadow-card: 0 1px 3px 0 rgb(0 0 0 / 0.06), 0 1px 2px -1px rgb(0 0 0 / 0.04);
|
|
36
42
|
|
|
37
43
|
/* Code surface — used by markdown code fences, terminal blocks,
|
|
@@ -54,10 +60,10 @@
|
|
|
54
60
|
--sidebar-foreground: hsl(0 0% 9%);
|
|
55
61
|
--sidebar-primary: hsl(192 90% 35%);
|
|
56
62
|
--sidebar-primary-foreground: hsl(0 0% 100%);
|
|
57
|
-
--sidebar-accent: hsl(
|
|
63
|
+
--sidebar-accent: hsl(0 0% 95%);
|
|
58
64
|
--sidebar-accent-foreground: hsl(0 0% 9%);
|
|
59
65
|
--sidebar-border: hsl(0 0% 90%);
|
|
60
|
-
--sidebar-ring: hsl(
|
|
66
|
+
--sidebar-ring: hsl(217 91% 55%);
|
|
61
67
|
/* ── Status surfaces ─────────────────────────────────────────────
|
|
62
68
|
* Each status has 4 tokens: icon/accent color, page background,
|
|
63
69
|
* readable foreground text, and border ring.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Control utilities
|
|
3
|
+
*
|
|
4
|
+
* Shared corner radius for interactive controls (nav items, inputs, search,
|
|
5
|
+
* value chips) via the `--radius-control` token, so they round consistently
|
|
6
|
+
* and stay themeable per preset. Plain CSS class — compiles regardless of the
|
|
7
|
+
* Tailwind content scan.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
.rounded-control {
|
|
11
|
+
border-radius: var(--radius-control);
|
|
12
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Divider utilities
|
|
3
|
+
*
|
|
4
|
+
* Hairline separators that use the dedicated `--divider` token (lighter than
|
|
5
|
+
* `--border` on elevated surfaces, so rows inside cards/dialogs stay visible).
|
|
6
|
+
*
|
|
7
|
+
* Plain CSS classes — NOT JIT-scanned Tailwind utilities — so they compile
|
|
8
|
+
* regardless of which package's source files the Tailwind content scan covers.
|
|
9
|
+
* Use these for settings rows, list items, and any inset divider.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
.divider-b {
|
|
13
|
+
border-bottom: 1px solid var(--divider);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.divider-t {
|
|
17
|
+
border-top: 1px solid var(--divider);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/* Suppress the divider on the last child in a list of `.divider-b` rows. */
|
|
21
|
+
.divider-b:last-child {
|
|
22
|
+
border-bottom: 0;
|
|
23
|
+
}
|
package/src/styles/utilities.css
CHANGED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ThemeSegmented — a 3-way System / Light / Dark segmented control
|
|
5
|
+
* (Claude / macOS settings style), as opposed to the binary `ThemeToggle`.
|
|
6
|
+
*
|
|
7
|
+
* Reads/writes the full theme via `useThemeContext` (`theme` + `setTheme`,
|
|
8
|
+
* which support `'system'`). Must be used within ThemeProvider. SSR-safe:
|
|
9
|
+
* renders a stable placeholder until mounted to avoid hydration mismatch.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Monitor, Moon, Sun } from 'lucide-react';
|
|
13
|
+
import { useEffect, useState } from 'react';
|
|
14
|
+
|
|
15
|
+
import { cn } from '../lib/utils';
|
|
16
|
+
import { useThemeContext, type Theme } from './ThemeProvider';
|
|
17
|
+
|
|
18
|
+
export interface ThemeSegmentedProps {
|
|
19
|
+
className?: string;
|
|
20
|
+
/** Compact paddings for tight rows. */
|
|
21
|
+
size?: 'default' | 'compact';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const OPTIONS: Array<{ value: Theme; icon: React.ComponentType<{ className?: string }>; label: string }> = [
|
|
25
|
+
{ value: 'system', icon: Monitor, label: 'System' },
|
|
26
|
+
{ value: 'light', icon: Sun, label: 'Light' },
|
|
27
|
+
{ value: 'dark', icon: Moon, label: 'Dark' },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
export function ThemeSegmented({ className, size = 'default' }: ThemeSegmentedProps) {
|
|
31
|
+
const { theme, setTheme } = useThemeContext();
|
|
32
|
+
const [mounted, setMounted] = useState(false);
|
|
33
|
+
useEffect(() => setMounted(true), []);
|
|
34
|
+
|
|
35
|
+
const active: Theme = mounted ? (theme ?? 'system') : 'system';
|
|
36
|
+
const btn = size === 'compact' ? 'h-7 w-7' : 'h-8 w-8';
|
|
37
|
+
const icon = size === 'compact' ? 'size-3.5' : 'size-4';
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div
|
|
41
|
+
role="radiogroup"
|
|
42
|
+
aria-label="Theme"
|
|
43
|
+
className={cn(
|
|
44
|
+
'rounded-control inline-flex items-center gap-0.5 border border-border bg-muted p-0.5',
|
|
45
|
+
className,
|
|
46
|
+
)}
|
|
47
|
+
>
|
|
48
|
+
{OPTIONS.map(({ value, icon: Icon, label }) => {
|
|
49
|
+
const selected = active === value;
|
|
50
|
+
return (
|
|
51
|
+
<button
|
|
52
|
+
key={value}
|
|
53
|
+
type="button"
|
|
54
|
+
role="radio"
|
|
55
|
+
aria-checked={selected}
|
|
56
|
+
aria-label={label}
|
|
57
|
+
title={label}
|
|
58
|
+
onClick={() => setTheme(value)}
|
|
59
|
+
className={cn(
|
|
60
|
+
'inline-flex items-center justify-center rounded-[calc(var(--radius-control)-0.25rem)] transition-colors',
|
|
61
|
+
btn,
|
|
62
|
+
selected
|
|
63
|
+
? 'bg-background text-foreground shadow-sm'
|
|
64
|
+
: 'text-muted-foreground hover:text-foreground',
|
|
65
|
+
)}
|
|
66
|
+
>
|
|
67
|
+
<Icon className={icon} />
|
|
68
|
+
</button>
|
|
69
|
+
);
|
|
70
|
+
})}
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
package/src/theme/index.ts
CHANGED
|
@@ -4,6 +4,8 @@ export { ThemeProvider, useThemeContext } from './ThemeProvider';
|
|
|
4
4
|
export type { Theme, ThemeProviderProps } from './ThemeProvider';
|
|
5
5
|
export { ThemeToggle } from './ThemeToggle';
|
|
6
6
|
export type { ThemeToggleProps, ThemeLockBehavior } from './ThemeToggle';
|
|
7
|
+
export { ThemeSegmented } from './ThemeSegmented';
|
|
8
|
+
export type { ThemeSegmentedProps } from './ThemeSegmented';
|
|
7
9
|
export { ForceTheme } from './ForceTheme';
|
|
8
10
|
export { ThemeOverride, resolveForcedTheme } from './ThemeOverride';
|
|
9
11
|
export type { ThemeOverrideRule, ThemeOverrideProps, ForcedTheme } from './ThemeOverride';
|
package/src/types/index.ts
DELETED
|
File without changes
|