@repraze/kernelkit 0.1.0-alpha.0 → 0.1.0-alpha.2
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/dist/components/calendar.js +1 -1
- package/dist/components/calendar.js.map +1 -1
- package/dist/components/carousel.d.ts +6 -1
- package/dist/components/carousel.d.ts.map +1 -1
- package/dist/components/carousel.js +30 -2
- package/dist/components/carousel.js.map +1 -1
- package/dist/components/command.d.ts +1 -1
- package/dist/components/command.d.ts.map +1 -1
- package/dist/components/command.js +2 -2
- package/dist/components/command.js.map +1 -1
- package/dist/components/dropdown-menu.js +1 -1
- package/dist/components/dropdown-menu.js.map +1 -1
- package/dist/components/hero.d.ts +14 -2
- package/dist/components/hero.d.ts.map +1 -1
- package/dist/components/hero.js +41 -8
- package/dist/components/hero.js.map +1 -1
- package/dist/components/navigation-menu.d.ts.map +1 -1
- package/dist/components/navigation-menu.js +3 -1
- package/dist/components/navigation-menu.js.map +1 -1
- package/dist/components/sidebar.d.ts.map +1 -1
- package/dist/components/sidebar.js +3 -2
- package/dist/components/sidebar.js.map +1 -1
- package/dist/components/site-header.d.ts +3 -3
- package/dist/components/site-header.d.ts.map +1 -1
- package/dist/components/site-header.js +5 -7
- package/dist/components/site-header.js.map +1 -1
- package/dist/components/theme-menu.d.ts +49 -0
- package/dist/components/theme-menu.d.ts.map +1 -0
- package/dist/components/theme-menu.js +75 -0
- package/dist/components/theme-menu.js.map +1 -0
- package/dist/lib/recipes.d.ts +7 -1
- package/dist/lib/recipes.d.ts.map +1 -1
- package/dist/lib/recipes.js +7 -1
- package/dist/lib/recipes.js.map +1 -1
- package/dist/provider/theme-provider.d.ts +4 -0
- package/dist/provider/theme-provider.d.ts.map +1 -1
- package/dist/provider/theme-provider.js +14 -1
- package/dist/provider/theme-provider.js.map +1 -1
- package/package.json +1 -1
- package/src/components/calendar.tsx +1 -1
- package/src/components/carousel.tsx +35 -0
- package/src/components/command.tsx +2 -3
- package/src/components/dropdown-menu.tsx +1 -1
- package/src/components/hero.tsx +77 -7
- package/src/components/navigation-menu.tsx +4 -0
- package/src/components/sidebar.tsx +3 -2
- package/src/components/site-header.tsx +6 -7
- package/src/components/theme-menu.tsx +243 -0
- package/src/lib/recipes.ts +9 -2
- package/src/provider/theme-provider.tsx +18 -1
|
@@ -95,9 +95,9 @@ function SiteNav({className, render, ...props}: useRender.ComponentProps<"nav">)
|
|
|
95
95
|
/**
|
|
96
96
|
* A nav link, styled like the suite's other navigation (GUIDELINES §7c): muted at rest, a hover wash,
|
|
97
97
|
* and when current (`aria-current="page"`, `data-status="active"` from TanStack Router, or `data-active`)
|
|
98
|
-
* foreground text
|
|
99
|
-
*
|
|
100
|
-
*
|
|
98
|
+
* the sidebar's current-row look: accent tint, foreground text and a 2px accent pill. In SiteNav the pill
|
|
99
|
+
* runs along the link's bottom edge (like NavigationMenu); elsewhere (the phone menu) it sits on the
|
|
100
|
+
* left edge, like a sidebar row.
|
|
101
101
|
*/
|
|
102
102
|
function SiteNavLink({className, render, ...props}: useRender.ComponentProps<"a">) {
|
|
103
103
|
return useRender({
|
|
@@ -107,12 +107,11 @@ function SiteNavLink({className, render, ...props}: useRender.ComponentProps<"a"
|
|
|
107
107
|
className: cn(
|
|
108
108
|
"relative flex h-8 items-center rounded-md px-2.5 font-mono text-sm text-muted-foreground transition-colors hover:bg-hover hover:text-foreground motion-reduce:transition-none",
|
|
109
109
|
focusRing,
|
|
110
|
-
// indicator:
|
|
110
|
+
// indicator: left edge by default, bottom edge inside SiteNav
|
|
111
111
|
"after:pointer-events-none after:absolute after:top-1.5 after:bottom-1.5 after:left-0 after:w-0.5 after:rounded-pill after:bg-primary-text after:opacity-0",
|
|
112
|
-
"in-data-[slot=site-nav]:after:top-auto in-data-[slot=site-nav]:after:right-2.5 in-data-[slot=site-nav]:after
|
|
112
|
+
"in-data-[slot=site-nav]:after:top-auto in-data-[slot=site-nav]:after:right-2.5 in-data-[slot=site-nav]:after:bottom-0 in-data-[slot=site-nav]:after:left-2.5 in-data-[slot=site-nav]:after:h-0.5 in-data-[slot=site-nav]:after:w-auto",
|
|
113
113
|
"aria-[current=page]:text-foreground aria-[current=page]:after:opacity-100 data-[status=active]:text-foreground data-[status=active]:after:opacity-100 data-active:text-foreground data-active:after:opacity-100",
|
|
114
|
-
|
|
115
|
-
"not-in-data-[slot=site-nav]:aria-[current=page]:bg-primary-tint not-in-data-[slot=site-nav]:data-[status=active]:bg-primary-tint not-in-data-[slot=site-nav]:data-active:bg-primary-tint",
|
|
114
|
+
"aria-[current=page]:bg-primary-tint data-[status=active]:bg-primary-tint data-active:bg-primary-tint",
|
|
116
115
|
className,
|
|
117
116
|
),
|
|
118
117
|
},
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {Button} from "@repraze/kernelkit/components/button";
|
|
4
|
+
import {Label} from "@repraze/kernelkit/components/label";
|
|
5
|
+
import {
|
|
6
|
+
Popover,
|
|
7
|
+
PopoverContent,
|
|
8
|
+
PopoverHeader,
|
|
9
|
+
PopoverTitle,
|
|
10
|
+
PopoverTrigger,
|
|
11
|
+
} from "@repraze/kernelkit/components/popover";
|
|
12
|
+
import {Switch} from "@repraze/kernelkit/components/switch";
|
|
13
|
+
import {ToggleGroup, ToggleGroupItem} from "@repraze/kernelkit/components/toggle-group";
|
|
14
|
+
import {Tooltip, TooltipContent, TooltipTrigger} from "@repraze/kernelkit/components/tooltip";
|
|
15
|
+
import {type Accent, ACCENTS, type Mode, RADII, type Radius, useTheme} from "@repraze/kernelkit/provider";
|
|
16
|
+
import {cn} from "cn";
|
|
17
|
+
import {CheckIcon, type LucideIcon, MonitorIcon, MoonIcon, RotateCcwIcon, SunIcon} from "lucide-react";
|
|
18
|
+
import * as React from "react";
|
|
19
|
+
|
|
20
|
+
/** Mode choices in display order, with the icon the trigger shows for each. */
|
|
21
|
+
const THEME_MODES: readonly {value: Mode; label: string; icon: LucideIcon}[] = [
|
|
22
|
+
{value: "light", label: "Light", icon: SunIcon},
|
|
23
|
+
{value: "dark", label: "Dark", icon: MoonIcon},
|
|
24
|
+
{value: "system", label: "System", icon: MonitorIcon},
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const modeOption = (mode: Mode) => THEME_MODES.find((m) => m.value === mode) ?? THEME_MODES[2]!;
|
|
28
|
+
|
|
29
|
+
/** The icon for the chosen mode (not the resolved one): sun, moon, or monitor for "system". */
|
|
30
|
+
function ThemeModeIcon({mode, ...props}: {mode?: Mode} & React.ComponentProps<LucideIcon>) {
|
|
31
|
+
const theme = useTheme();
|
|
32
|
+
const Icon = modeOption(mode ?? theme.mode).icon;
|
|
33
|
+
return <Icon data-slot="theme-mode-icon" {...props} />;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Light / dark / system as a segmented control. */
|
|
37
|
+
function ThemeModePicker({className}: {className?: string}) {
|
|
38
|
+
const theme = useTheme();
|
|
39
|
+
return (
|
|
40
|
+
<ToggleGroup
|
|
41
|
+
data-slot="theme-mode-picker"
|
|
42
|
+
aria-label="Mode"
|
|
43
|
+
variant="outline"
|
|
44
|
+
size="sm"
|
|
45
|
+
value={[theme.mode]}
|
|
46
|
+
onValueChange={(v) => v[0] && theme.setMode(v[0] as Mode)}
|
|
47
|
+
className={cn("w-full", className)}
|
|
48
|
+
>
|
|
49
|
+
{THEME_MODES.map(({value, label, icon: Icon}) => (
|
|
50
|
+
<ToggleGroupItem key={value} value={value} className="flex-1">
|
|
51
|
+
<Icon /> {label}
|
|
52
|
+
</ToggleGroupItem>
|
|
53
|
+
))}
|
|
54
|
+
</ToggleGroup>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Swatch per accent: the vivid fill tier, with that accent's own on-fill color for the check mark. */
|
|
59
|
+
const swatch = (accent: Accent) => ({
|
|
60
|
+
background: `var(--tc-accent-${accent}-accent-fill)`,
|
|
61
|
+
color: `var(--tc-accent-${accent}-on-accent-fill)`,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
/** The eight TermiColor accents as swatches; sets the theme's accent. */
|
|
65
|
+
function ThemeAccentPicker({className}: {className?: string}) {
|
|
66
|
+
const theme = useTheme();
|
|
67
|
+
return (
|
|
68
|
+
<div
|
|
69
|
+
data-slot="theme-accent-picker"
|
|
70
|
+
role="group"
|
|
71
|
+
aria-label="Accent"
|
|
72
|
+
className={cn("grid grid-cols-8 gap-1.5", className)}
|
|
73
|
+
>
|
|
74
|
+
{ACCENTS.map((accent) => {
|
|
75
|
+
const on = theme.accent === accent;
|
|
76
|
+
return (
|
|
77
|
+
<Tooltip key={accent}>
|
|
78
|
+
<TooltipTrigger
|
|
79
|
+
render={
|
|
80
|
+
<button
|
|
81
|
+
type="button"
|
|
82
|
+
aria-label={accent}
|
|
83
|
+
aria-pressed={on}
|
|
84
|
+
onClick={() => theme.setAccent(accent)}
|
|
85
|
+
style={swatch(accent)}
|
|
86
|
+
className={cn(
|
|
87
|
+
"flex aspect-square items-center justify-center rounded-md outline-none",
|
|
88
|
+
"ring-offset-2 ring-offset-transparent transition-shadow duration-100 motion-reduce:transition-none",
|
|
89
|
+
"hover:ring-2 hover:ring-input focus-visible:ring-2 focus-visible:ring-ring",
|
|
90
|
+
on && "ring-2 ring-foreground hover:ring-foreground",
|
|
91
|
+
)}
|
|
92
|
+
/>
|
|
93
|
+
}
|
|
94
|
+
>
|
|
95
|
+
{on ? <CheckIcon className="size-3.5" strokeWidth={3} /> : null}
|
|
96
|
+
</TooltipTrigger>
|
|
97
|
+
<TooltipContent className="capitalize">{accent}</TooltipContent>
|
|
98
|
+
</Tooltip>
|
|
99
|
+
);
|
|
100
|
+
})}
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The radius scale as a segmented control; sets the theme's radius. */
|
|
106
|
+
function ThemeRadiusPicker({className}: {className?: string}) {
|
|
107
|
+
const theme = useTheme();
|
|
108
|
+
return (
|
|
109
|
+
<ToggleGroup
|
|
110
|
+
data-slot="theme-radius-picker"
|
|
111
|
+
aria-label="Radius"
|
|
112
|
+
variant="outline"
|
|
113
|
+
size="sm"
|
|
114
|
+
value={[theme.radius]}
|
|
115
|
+
onValueChange={(v) => v[0] && theme.setRadius(v[0] as Radius)}
|
|
116
|
+
className={cn("w-full", className)}
|
|
117
|
+
>
|
|
118
|
+
{RADII.map((radius) => (
|
|
119
|
+
<ToggleGroupItem key={radius} value={radius} className="flex-1 font-mono">
|
|
120
|
+
{radius}
|
|
121
|
+
</ToggleGroupItem>
|
|
122
|
+
))}
|
|
123
|
+
</ToggleGroup>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** High contrast on/off, with its label. */
|
|
128
|
+
function ThemeContrastSwitch({className}: {className?: string}) {
|
|
129
|
+
const theme = useTheme();
|
|
130
|
+
const id = React.useId();
|
|
131
|
+
return (
|
|
132
|
+
<div data-slot="theme-contrast-switch" className={cn("flex items-center justify-between gap-2", className)}>
|
|
133
|
+
<Label htmlFor={id}>High contrast</Label>
|
|
134
|
+
<Switch
|
|
135
|
+
id={id}
|
|
136
|
+
checked={theme.contrast === "high"}
|
|
137
|
+
onCheckedChange={(on) => theme.setContrast(on ? "high" : "normal")}
|
|
138
|
+
/>
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** A labelled block inside the menu. */
|
|
144
|
+
function ThemeMenuSection({
|
|
145
|
+
label,
|
|
146
|
+
children,
|
|
147
|
+
className,
|
|
148
|
+
}: {
|
|
149
|
+
label: string;
|
|
150
|
+
children: React.ReactNode;
|
|
151
|
+
className?: string;
|
|
152
|
+
}) {
|
|
153
|
+
return (
|
|
154
|
+
<div data-slot="theme-menu-section" className={cn("flex flex-col gap-2", className)}>
|
|
155
|
+
<span className="text-xs font-medium text-muted-foreground">{label}</span>
|
|
156
|
+
{children}
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
interface ThemeMenuProps extends Omit<React.ComponentProps<typeof Button>, "children" | "title"> {
|
|
162
|
+
/** Popover title. */
|
|
163
|
+
title?: React.ReactNode;
|
|
164
|
+
/** Extra controls, after the built-in ones and before Reset. */
|
|
165
|
+
children?: React.ReactNode;
|
|
166
|
+
/** Popover alignment against the trigger. */
|
|
167
|
+
align?: "start" | "center" | "end";
|
|
168
|
+
contentClassName?: string;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* The suite's theme control (GUIDELINES §7b): one ghost icon button in `SiteHeaderActions` showing the
|
|
173
|
+
* chosen mode (sun, moon, monitor), opening a popover with mode, accent, high contrast, radius and a
|
|
174
|
+
* Reset to the provider's defaults. Reads and writes `useTheme()`, so it needs a `ThemeProvider`.
|
|
175
|
+
* Trigger props (`aria-label`, `className`, `data-*`) pass through to the button.
|
|
176
|
+
*/
|
|
177
|
+
function ThemeMenu({title = "Theme", children, align = "end", contentClassName, className, ...props}: ThemeMenuProps) {
|
|
178
|
+
const theme = useTheme();
|
|
179
|
+
const {defaults} = theme;
|
|
180
|
+
const isDefault =
|
|
181
|
+
theme.mode === defaults.mode &&
|
|
182
|
+
theme.accent === defaults.accent &&
|
|
183
|
+
theme.contrast === defaults.contrast &&
|
|
184
|
+
theme.radius === defaults.radius;
|
|
185
|
+
return (
|
|
186
|
+
<Popover>
|
|
187
|
+
<PopoverTrigger
|
|
188
|
+
render={
|
|
189
|
+
<Button
|
|
190
|
+
variant="ghost"
|
|
191
|
+
size="icon"
|
|
192
|
+
aria-label={`Theme: ${modeOption(theme.mode).label}`}
|
|
193
|
+
data-slot="theme-menu-trigger"
|
|
194
|
+
data-mode={theme.mode}
|
|
195
|
+
className={className}
|
|
196
|
+
{...props}
|
|
197
|
+
/>
|
|
198
|
+
}
|
|
199
|
+
>
|
|
200
|
+
<ThemeModeIcon />
|
|
201
|
+
</PopoverTrigger>
|
|
202
|
+
<PopoverContent data-slot="theme-menu-content" align={align} className={cn("w-72 gap-4", contentClassName)}>
|
|
203
|
+
<PopoverHeader>
|
|
204
|
+
<PopoverTitle>{title}</PopoverTitle>
|
|
205
|
+
</PopoverHeader>
|
|
206
|
+
<ThemeMenuSection label="Mode">
|
|
207
|
+
<ThemeModePicker />
|
|
208
|
+
</ThemeMenuSection>
|
|
209
|
+
<ThemeMenuSection label="Accent">
|
|
210
|
+
<ThemeAccentPicker />
|
|
211
|
+
</ThemeMenuSection>
|
|
212
|
+
<ThemeContrastSwitch />
|
|
213
|
+
<ThemeMenuSection label="Radius">
|
|
214
|
+
<ThemeRadiusPicker />
|
|
215
|
+
</ThemeMenuSection>
|
|
216
|
+
{children}
|
|
217
|
+
<div className="-mx-3 -mb-3 border-t border-border p-3">
|
|
218
|
+
<Button
|
|
219
|
+
variant="outline"
|
|
220
|
+
size="sm"
|
|
221
|
+
data-slot="theme-menu-reset"
|
|
222
|
+
className="w-full"
|
|
223
|
+
disabled={isDefault}
|
|
224
|
+
onClick={theme.resetTheme}
|
|
225
|
+
>
|
|
226
|
+
<RotateCcwIcon data-icon="inline-start" /> Reset to defaults
|
|
227
|
+
</Button>
|
|
228
|
+
</div>
|
|
229
|
+
</PopoverContent>
|
|
230
|
+
</Popover>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export {
|
|
235
|
+
ThemeAccentPicker,
|
|
236
|
+
ThemeContrastSwitch,
|
|
237
|
+
ThemeMenu,
|
|
238
|
+
ThemeMenuSection,
|
|
239
|
+
ThemeModeIcon,
|
|
240
|
+
ThemeModePicker,
|
|
241
|
+
ThemeRadiusPicker,
|
|
242
|
+
};
|
|
243
|
+
export type {ThemeMenuProps};
|
package/src/lib/recipes.ts
CHANGED
|
@@ -23,9 +23,16 @@ export const popupSurface =
|
|
|
23
23
|
export const popupMotion =
|
|
24
24
|
"origin-(--transform-origin) duration-100 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 motion-reduce:animate-none";
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* The sidebar's current-row indicator (§7c): a 2px accent pill inset on the row's left edge, hidden until
|
|
28
|
+
* the row's state shows it (`data-highlighted:after:opacity-100`, `data-active:after:opacity-100`...).
|
|
29
|
+
* The row keeps its own rounding on every corner.
|
|
30
|
+
*/
|
|
31
|
+
export const rowIndicator =
|
|
32
|
+
"after:pointer-events-none after:absolute after:top-1.5 after:bottom-1.5 after:left-0 after:w-0.5 after:rounded-pill after:bg-primary-text after:opacity-0";
|
|
33
|
+
|
|
26
34
|
/** Row inside a popup or list: menu item, select item, command item (§4, §6). */
|
|
27
|
-
export const listItem =
|
|
28
|
-
"relative flex min-h-8 cursor-default items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-hidden select-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-highlighted:[&_svg:not([class*='text-'])]:text-accent-foreground";
|
|
35
|
+
export const listItem = `${rowIndicator} data-highlighted:after:opacity-100 relative flex min-h-8 cursor-default items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-hidden select-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-highlighted:[&_svg:not([class*='text-'])]:text-accent-foreground`;
|
|
29
36
|
|
|
30
37
|
/** Group label inside a popup or list. */
|
|
31
38
|
export const listLabel = "px-2 py-1.5 text-xs font-medium text-muted-foreground";
|
|
@@ -32,6 +32,10 @@ export interface ThemeContextValue extends ThemeState {
|
|
|
32
32
|
setContrast: (contrast: ThemeState["contrast"]) => void;
|
|
33
33
|
setAccent: (accent: ThemeState["accent"]) => void;
|
|
34
34
|
setRadius: (radius: ThemeState["radius"]) => void;
|
|
35
|
+
/** The values `resetTheme` restores: the provider's `defaultTheme` over the kit defaults. */
|
|
36
|
+
defaults: ThemeState;
|
|
37
|
+
/** Forget the stored choice and go back to `defaults`. */
|
|
38
|
+
resetTheme: () => void;
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
const ThemeContext = createContext<ThemeContextValue | null>(null);
|
|
@@ -139,6 +143,17 @@ export function ThemeProvider({
|
|
|
139
143
|
[storageKey, fallback],
|
|
140
144
|
);
|
|
141
145
|
|
|
146
|
+
const resetTheme = useCallback(() => {
|
|
147
|
+
if (storageKey) {
|
|
148
|
+
try {
|
|
149
|
+
window.localStorage.removeItem(storageKey);
|
|
150
|
+
} catch {
|
|
151
|
+
// storage blocked: the in-memory reset still applies
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
setStored(fallback);
|
|
155
|
+
}, [storageKey, fallback]);
|
|
156
|
+
|
|
142
157
|
const value = useMemo<ThemeContextValue>(
|
|
143
158
|
() => ({
|
|
144
159
|
...theme,
|
|
@@ -148,8 +163,10 @@ export function ThemeProvider({
|
|
|
148
163
|
setContrast: (contrast) => setTheme({contrast}),
|
|
149
164
|
setAccent: (accent) => setTheme({accent}),
|
|
150
165
|
setRadius: (radius) => setTheme({radius}),
|
|
166
|
+
defaults: fallback,
|
|
167
|
+
resetTheme,
|
|
151
168
|
}),
|
|
152
|
-
[theme, resolvedMode, setTheme],
|
|
169
|
+
[theme, resolvedMode, setTheme, fallback, resetTheme],
|
|
153
170
|
);
|
|
154
171
|
|
|
155
172
|
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
|