This commit is contained in:
parent
9faf66399c
commit
bb065692b0
8 changed files with 92 additions and 132 deletions
|
|
@ -54,7 +54,6 @@
|
|||
--ui-density: 1;
|
||||
--ui-border-width: 1px;
|
||||
--ui-shadow-strength: 0;
|
||||
--ui-surface-contrast: 0;
|
||||
--ui-font-scale: 1;
|
||||
--ui-heading-weight: 600;
|
||||
--ui-motion: 1;
|
||||
|
|
@ -248,11 +247,6 @@
|
|||
padding: calc(0.625rem * var(--ui-density)) calc(0.75rem * var(--ui-density));
|
||||
}
|
||||
[data-slot="card"] {
|
||||
background: color-mix(
|
||||
in oklch,
|
||||
var(--card),
|
||||
var(--foreground) calc(var(--ui-surface-contrast) * 1%)
|
||||
);
|
||||
box-shadow: 0 calc(0.4rem * var(--ui-shadow-strength))
|
||||
calc(1.8rem * var(--ui-shadow-strength))
|
||||
rgb(0 0 0 / calc(0.12 * var(--ui-shadow-strength)));
|
||||
|
|
|
|||
|
|
@ -45,7 +45,12 @@ export function ThemeSelector({
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className={cn("flex flex-wrap gap-3", className)}>
|
||||
<div
|
||||
className={cn(
|
||||
"grid w-full grid-cols-[repeat(auto-fit,minmax(5rem,1fr))] gap-3 sm:flex sm:flex-wrap",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{themes.map((theme) => (
|
||||
<div key={theme.id} className="flex flex-col items-center gap-1.5">
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -50,7 +50,6 @@ const DESIGN_NUMBER_FIELDS = [
|
|||
"density",
|
||||
"borderWidth",
|
||||
"shadowStrength",
|
||||
"surfaceContrast",
|
||||
"fontScale",
|
||||
"headingWeight",
|
||||
"motion",
|
||||
|
|
@ -83,7 +82,16 @@ function parseDesign(value: string | null, fallback: ThemeDesign) {
|
|||
if (!value) return fallback;
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(value);
|
||||
return isThemeDesign(parsed) ? parsed : fallback;
|
||||
if (!isThemeDesign(parsed)) return fallback;
|
||||
return {
|
||||
density: parsed.density,
|
||||
borderWidth: parsed.borderWidth,
|
||||
shadowStrength: parsed.shadowStrength,
|
||||
fontScale: parsed.fontScale,
|
||||
headingWeight: parsed.headingWeight,
|
||||
motion: parsed.motion,
|
||||
fontFamily: parsed.fontFamily,
|
||||
};
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
|
|
@ -332,10 +340,6 @@ export function ThemeProvider({
|
|||
"--ui-shadow-strength",
|
||||
String(themeDesign.shadowStrength),
|
||||
);
|
||||
root.style.setProperty(
|
||||
"--ui-surface-contrast",
|
||||
String(themeDesign.surfaceContrast),
|
||||
);
|
||||
root.style.setProperty("--ui-font-scale", String(themeDesign.fontScale));
|
||||
root.style.setProperty(
|
||||
"--ui-heading-weight",
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
import { useId } from "react";
|
||||
import { Trash } from "lucide-react";
|
||||
|
||||
import { Button } from "../../cmp/button";
|
||||
import { Label } from "../../cmp/label";
|
||||
import { Slider } from "../../cmp/slider";
|
||||
import { useTheme } from "../Provider";
|
||||
import {
|
||||
DEFAULT_BORDER_RADIUS,
|
||||
MAX_BORDER_RADIUS,
|
||||
MIN_BORDER_RADIUS,
|
||||
} from "../utils";
|
||||
|
||||
export function BorderRadiusPicker() {
|
||||
const id = useId();
|
||||
const { themeBorderRadius, setThemeBorderRadius } = useTheme();
|
||||
const updateBorderRadius = (value: number | readonly number[]) => {
|
||||
setThemeBorderRadius(Array.isArray(value) ? (value[0] ?? 0) : value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex w-65 flex-col gap-2">
|
||||
<div className="flex justify-between gap-4">
|
||||
<Label htmlFor={id}>Border radius</Label>
|
||||
<output
|
||||
htmlFor={id}
|
||||
className="font-mono text-xs text-muted-foreground"
|
||||
>
|
||||
{themeBorderRadius.toFixed(3)}rem
|
||||
</output>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex flex-1 flex-col justify-start gap-1">
|
||||
<Slider
|
||||
id={id}
|
||||
className="w-full"
|
||||
min={MIN_BORDER_RADIUS}
|
||||
max={MAX_BORDER_RADIUS}
|
||||
step={0.025}
|
||||
value={[themeBorderRadius]}
|
||||
onValueChange={updateBorderRadius}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
className="h-8 w-8"
|
||||
variant="destructive"
|
||||
disabled={themeBorderRadius === DEFAULT_BORDER_RADIUS}
|
||||
onClick={() => updateBorderRadius(DEFAULT_BORDER_RADIUS)}
|
||||
>
|
||||
<Trash />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { useId, useState, type ReactNode } from "react";
|
||||
import { useId, useState, type ComponentProps, type ReactNode } from "react";
|
||||
import { ChevronDown, RotateCcw } from "lucide-react";
|
||||
|
||||
import { Button } from "../../cmp/button";
|
||||
|
|
@ -14,8 +14,8 @@ import { Slider } from "../../cmp/slider";
|
|||
import { ThemeSelector } from "../BaseThemeSelector";
|
||||
import { useTheme } from "../Provider";
|
||||
import type { ThemeDesign, ThemeFontFamily } from "../types";
|
||||
import { MAX_BORDER_RADIUS, MIN_BORDER_RADIUS } from "../utils";
|
||||
import { Base16Picker } from "./Base16Picker";
|
||||
import { BorderRadiusPicker } from "./BorderRadiusPicker";
|
||||
import { CustomCssPicker } from "./CustomCssPicker";
|
||||
import { PolarityPicker } from "./PolarityPicker";
|
||||
import { PrimaryPicker } from "./PrimaryPicker";
|
||||
|
|
@ -30,32 +30,23 @@ function Section({ title, children }: { title: string; children: ReactNode }) {
|
|||
);
|
||||
}
|
||||
|
||||
function DesignSlider({
|
||||
function ThemeSlider({
|
||||
label,
|
||||
field,
|
||||
value,
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
onValueChange,
|
||||
format = (value) => String(value),
|
||||
}: {
|
||||
label: string;
|
||||
field: keyof Pick<
|
||||
ThemeDesign,
|
||||
| "density"
|
||||
| "borderWidth"
|
||||
| "shadowStrength"
|
||||
| "surfaceContrast"
|
||||
| "fontScale"
|
||||
| "headingWeight"
|
||||
| "motion"
|
||||
>;
|
||||
value: number;
|
||||
min: number;
|
||||
max: number;
|
||||
step: number;
|
||||
onValueChange: (value: number) => void;
|
||||
format?: (value: number) => string;
|
||||
}) {
|
||||
const { themeDesign, setThemeDesign } = useTheme();
|
||||
const value = themeDesign[field];
|
||||
const id = useId();
|
||||
return (
|
||||
<div className="grid gap-2 text-sm">
|
||||
|
|
@ -75,16 +66,39 @@ function DesignSlider({
|
|||
step={step}
|
||||
value={[value]}
|
||||
onValueChange={(next) =>
|
||||
setThemeDesign((current) => ({
|
||||
...current,
|
||||
[field]: Array.isArray(next) ? (next[0] ?? value) : next,
|
||||
}))
|
||||
onValueChange(Array.isArray(next) ? (next[0] ?? value) : next)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DesignSlider({
|
||||
field,
|
||||
...props
|
||||
}: Omit<ComponentProps<typeof ThemeSlider>, "value" | "onValueChange"> & {
|
||||
field: keyof Pick<
|
||||
ThemeDesign,
|
||||
| "density"
|
||||
| "borderWidth"
|
||||
| "shadowStrength"
|
||||
| "fontScale"
|
||||
| "headingWeight"
|
||||
| "motion"
|
||||
>;
|
||||
}) {
|
||||
const { themeDesign, setThemeDesign } = useTheme();
|
||||
return (
|
||||
<ThemeSlider
|
||||
{...props}
|
||||
value={themeDesign[field]}
|
||||
onValueChange={(value) =>
|
||||
setThemeDesign((current) => ({ ...current, [field]: value }))
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function ThemeCustomizer({
|
||||
value,
|
||||
onValueChange,
|
||||
|
|
@ -97,6 +111,8 @@ export function ThemeCustomizer({
|
|||
const {
|
||||
themeDesign,
|
||||
setThemeDesign,
|
||||
themeBorderRadius,
|
||||
setThemeBorderRadius,
|
||||
parentTheme,
|
||||
isThemeCustomized,
|
||||
resetThemePreset,
|
||||
|
|
@ -113,7 +129,7 @@ export function ThemeCustomizer({
|
|||
</Section>
|
||||
<div className="space-y-7">
|
||||
<Section title="Shape and spacing">
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div className="grid gap-6 xl:grid-cols-2">
|
||||
<DesignSlider
|
||||
label="Density"
|
||||
field="density"
|
||||
|
|
@ -130,18 +146,19 @@ export function ThemeCustomizer({
|
|||
step={0.25}
|
||||
format={(v) => `${v}px`}
|
||||
/>
|
||||
<BorderRadiusPicker />
|
||||
<DesignSlider
|
||||
label="Surface contrast"
|
||||
field="surfaceContrast"
|
||||
min={0}
|
||||
max={8}
|
||||
step={0.5}
|
||||
<ThemeSlider
|
||||
label="Border radius"
|
||||
value={themeBorderRadius}
|
||||
min={MIN_BORDER_RADIUS}
|
||||
max={MAX_BORDER_RADIUS}
|
||||
step={0.025}
|
||||
format={(value) => `${value.toFixed(3)}rem`}
|
||||
onValueChange={setThemeBorderRadius}
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
<Section title="Depth and motion">
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div className="grid gap-6 xl:grid-cols-2">
|
||||
<DesignSlider
|
||||
label="Shadow strength"
|
||||
field="shadowStrength"
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ export const DEFAULT_THEME_DESIGN: ThemeDesign = {
|
|||
density: 1,
|
||||
borderWidth: 1,
|
||||
shadowStrength: 0,
|
||||
surfaceContrast: 0,
|
||||
fontScale: 1,
|
||||
headingWeight: 600,
|
||||
motion: 1,
|
||||
|
|
@ -87,14 +86,12 @@ export const BUILT_IN_THEMES = defineThemes([
|
|||
--tensamin-edge: var(--primary-foreground-alt);
|
||||
}
|
||||
:root[data-theme="tensamin"] [data-slot="button"][data-variant="subtleDefault"] {
|
||||
background-color: color-mix(in oklch, var(--background) 88%, var(--primary-foreground-alt));
|
||||
border-top-color: color-mix(in oklch, color-mix(in oklch, var(--background) 88%, var(--primary-foreground-alt)) 98%, white);
|
||||
border-left-color: color-mix(in oklch, color-mix(in oklch, var(--background) 88%, var(--primary-foreground-alt)) 89%, black);
|
||||
border-right-color: color-mix(in oklch, color-mix(in oklch, var(--background) 88%, var(--primary-foreground-alt)) 89%, black);
|
||||
border-bottom-color: color-mix(in oklch, color-mix(in oklch, var(--background) 88%, var(--primary-foreground-alt)) 83%, black);
|
||||
background-color: color-mix(in oklch, var(--primary) 10%, transparent);
|
||||
border-color: transparent;
|
||||
color: var(--primary-foreground-alt);
|
||||
}
|
||||
:root[data-theme="tensamin"] [data-slot="button"][data-variant="subtleDefault"]:hover {
|
||||
background-color: color-mix(in oklch, var(--background) 80%, var(--primary-foreground-alt));
|
||||
background-color: color-mix(in oklch, var(--primary) 20%, transparent);
|
||||
}
|
||||
:root[data-theme="tensamin"] :where([data-slot="button"], [data-slot="badge"], [data-slot="toggle"])[data-variant="secondary"] {
|
||||
--tensamin-edge: var(--secondary);
|
||||
|
|
@ -349,22 +346,14 @@ export const BUILT_IN_THEMES = defineThemes([
|
|||
:root[data-theme="methanium"] [data-slot="card"] {
|
||||
padding-block: calc(1rem * var(--ui-density));
|
||||
}
|
||||
:root[data-theme="methanium"] [data-slot="button"]:where([data-variant="default"], [data-variant="outline"]) {
|
||||
:root[data-theme="methanium"] [data-slot="button"][data-variant="default"] {
|
||||
background-color: var(--primary);
|
||||
color: var(--primary-foreground);
|
||||
}
|
||||
:root[data-theme="methanium"] [data-slot="button"][data-variant="default"] {
|
||||
border-color: transparent;
|
||||
}
|
||||
:root[data-theme="methanium"] [data-slot="button"][data-variant="outline"] {
|
||||
border-color: var(--input);
|
||||
}
|
||||
:root[data-theme="methanium"] [data-slot="button"]:where([data-variant="default"], [data-variant="outline"]):hover {
|
||||
:root[data-theme="methanium"] [data-slot="button"][data-variant="default"]:hover {
|
||||
background-color: #bb6fd6;
|
||||
}
|
||||
:root[data-theme="methanium"] [data-slot="button"][data-variant="outline"]:is(:hover, :focus-visible) {
|
||||
border-color: color-mix(in srgb, var(--input) 75%, var(--foreground) 25%);
|
||||
}
|
||||
:root[data-theme="methanium"] [data-slot="button"][data-variant="secondary"] {
|
||||
background-color: var(--secondary);
|
||||
border-color: color-mix(in srgb, white 16%, transparent);
|
||||
|
|
@ -373,12 +362,13 @@ export const BUILT_IN_THEMES = defineThemes([
|
|||
:root[data-theme="methanium"] [data-slot="button"][data-variant="secondary"]:hover {
|
||||
background-color: color-mix(in srgb, var(--secondary) 80%, var(--foreground) 20%);
|
||||
}
|
||||
:root[data-theme="methanium"] :where([data-slot="input"], [data-slot="textarea"], [data-slot="native-select"], [data-slot="select-trigger"], [data-slot="input-group"]) {
|
||||
:root[data-theme="methanium"] :where([data-slot="input"], [data-slot="textarea"], [data-slot="native-select"], [data-slot="select-trigger"], [data-slot="input-group"], [data-slot="button"][data-variant="outline"]) {
|
||||
background-color: color-mix(in srgb, var(--input) 55%, var(--background));
|
||||
border-color: var(--input);
|
||||
color: var(--foreground);
|
||||
transition: background-color 100ms ease, border-color 100ms ease;
|
||||
}
|
||||
:root[data-theme="methanium"] :where([data-slot="input"], [data-slot="textarea"], [data-slot="native-select"], [data-slot="select-trigger"], [data-slot="input-group"]):not(:disabled):not([data-disabled]):not(:has(:disabled)):hover {
|
||||
:root[data-theme="methanium"] :where([data-slot="input"], [data-slot="textarea"], [data-slot="native-select"], [data-slot="select-trigger"], [data-slot="input-group"], [data-slot="button"][data-variant="outline"]):not(:disabled):not([data-disabled]):not(:has(:disabled)):hover {
|
||||
background-color: color-mix(in srgb, var(--input) 72%, var(--background));
|
||||
border-color: color-mix(in srgb, var(--input) 75%, var(--foreground) 25%);
|
||||
}`,
|
||||
|
|
@ -392,6 +382,8 @@ export const BUILT_IN_THEMES = defineThemes([
|
|||
design: DEFAULT_THEME_DESIGN,
|
||||
},
|
||||
css: `:root[data-theme="secret"] {
|
||||
--secret-border: color-mix(in srgb, #ffe8b4 15%, #24120c 85%);
|
||||
--secret-control: #5e244e;
|
||||
--background: #2f1b13;
|
||||
--foreground: #fffaf2;
|
||||
--card: #f9b637;
|
||||
|
|
@ -408,8 +400,8 @@ export const BUILT_IN_THEMES = defineThemes([
|
|||
--accent: #fb6c00;
|
||||
--accent-foreground: #24120c;
|
||||
--destructive: #e73f1e;
|
||||
--border: #f9b637;
|
||||
--input: #5e244e;
|
||||
--border: var(--secret-border);
|
||||
--input: var(--secret-border);
|
||||
--ring: #fb6c00;
|
||||
--sidebar: #24120c;
|
||||
--sidebar-foreground: #fffaf2;
|
||||
|
|
@ -417,7 +409,7 @@ export const BUILT_IN_THEMES = defineThemes([
|
|||
--sidebar-primary-foreground: #24120c;
|
||||
--sidebar-accent: #aa1c41;
|
||||
--sidebar-accent-foreground: #fffaf2;
|
||||
--sidebar-border: #e68457;
|
||||
--sidebar-border: var(--secret-border);
|
||||
}
|
||||
:root.dark[data-theme="secret"] {
|
||||
--background: #24120c;
|
||||
|
|
@ -436,8 +428,8 @@ export const BUILT_IN_THEMES = defineThemes([
|
|||
--accent: #e73f1e;
|
||||
--accent-foreground: #fffaf2;
|
||||
--destructive: #e73f1e;
|
||||
--border: #e68457;
|
||||
--input: #5e244e;
|
||||
--border: var(--secret-border);
|
||||
--input: var(--secret-border);
|
||||
--ring: #fb6c00;
|
||||
--sidebar: #24120c;
|
||||
--sidebar-foreground: #ffe8b4;
|
||||
|
|
@ -445,7 +437,7 @@ export const BUILT_IN_THEMES = defineThemes([
|
|||
--sidebar-primary-foreground: #24120c;
|
||||
--sidebar-accent: #aa1c41;
|
||||
--sidebar-accent-foreground: #fffaf2;
|
||||
--sidebar-border: #5e244e;
|
||||
--sidebar-border: var(--secret-border);
|
||||
}
|
||||
:root[data-theme="secret"] body {
|
||||
background-color: var(--background);
|
||||
|
|
@ -459,29 +451,33 @@ export const BUILT_IN_THEMES = defineThemes([
|
|||
background-image: linear-gradient(to top, rgb(255 255 255 / 12%), rgb(255 255 255 / 2%));
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
:root[data-theme="secret"] [data-slot="button"][data-variant="subtleDefault"] {
|
||||
background-image: linear-gradient(to top, rgb(255 232 180 / 12%), rgb(255 232 180 / 2%));
|
||||
color: var(--foreground);
|
||||
}
|
||||
:root[data-theme="secret"] :where([data-slot="input"], [data-slot="textarea"], [data-slot="native-select"], [data-slot="select-trigger"], [data-slot="input-group"]) {
|
||||
background-color: var(--input);
|
||||
background-color: var(--secret-control);
|
||||
color: var(--foreground);
|
||||
}
|
||||
:root[data-theme="secret"] :where([data-slot="input"], [data-slot="textarea"], [data-slot="native-select"], [data-slot="select-trigger"], [data-slot="input-group"]):hover {
|
||||
background-color: color-mix(in srgb, var(--input) 84%, #24120c 16%);
|
||||
background-color: color-mix(in srgb, var(--secret-control) 84%, #24120c 16%);
|
||||
border-color: var(--input);
|
||||
}
|
||||
:root[data-theme="secret"] :where([data-slot="input"], [data-slot="textarea"], [data-slot="native-select"], [data-slot="select-trigger"], [data-slot="input-group"]):is(:focus-visible, :focus-within) {
|
||||
background-color: color-mix(in srgb, var(--input) 84%, #24120c 16%);
|
||||
background-color: color-mix(in srgb, var(--secret-control) 84%, #24120c 16%);
|
||||
border-color: var(--input);
|
||||
}
|
||||
:root[data-theme="secret"] [data-slot="select-trigger"]:is([data-popup-open], [data-open], [aria-expanded="true"]) {
|
||||
background-color: color-mix(in srgb, var(--input) 84%, #24120c 16%);
|
||||
background-color: color-mix(in srgb, var(--secret-control) 84%, #24120c 16%);
|
||||
border-color: var(--input);
|
||||
}
|
||||
:root[data-theme="secret"] [data-slot="select-content"] {
|
||||
background-color: var(--popover);
|
||||
border-color: color-mix(in srgb, #ffe8b4 15%, #24120c 85%);
|
||||
border-color: var(--secret-border);
|
||||
color: var(--popover-foreground);
|
||||
}
|
||||
:root.dark[data-theme="secret"] [data-slot="select-content"] {
|
||||
border-color: color-mix(in srgb, #ffe8b4 15%, #24120c 85%);
|
||||
border-color: var(--secret-border);
|
||||
}
|
||||
:root[data-theme="secret"] [data-slot="select-item"]:is(:hover, :focus, [data-highlighted], [data-selected]) {
|
||||
background-color: #24120c;
|
||||
|
|
@ -490,13 +486,13 @@ export const BUILT_IN_THEMES = defineThemes([
|
|||
:root[data-theme="secret"] [data-slot="button"][data-variant="default"] {
|
||||
background-color: var(--primary);
|
||||
background-image: linear-gradient(to top, rgb(255 255 255 / 12%), rgb(255 255 255 / 2%)), linear-gradient(115deg, #e73f1e 0%, #fb6c00 30%, #ffdd9c 47%, #f9b637 58%, #fb6c00 100%);
|
||||
border-color: #e68457;
|
||||
border-color: var(--secret-border);
|
||||
color: var(--primary-foreground);
|
||||
text-shadow: 0 1px rgb(255 255 255 / 35%);
|
||||
box-shadow: inset 0 1px rgb(255 255 255 / 45%), 0 0.45rem 1.2rem rgb(36 18 12 / 28%);
|
||||
}
|
||||
:root[data-theme="secret"] :where([data-slot="card"], [data-slot="dialog-content"], [data-slot="alert-dialog-content"]) {
|
||||
border-color: color-mix(in oklch, var(--border) 72%, var(--primary));
|
||||
border-color: var(--secret-border);
|
||||
box-shadow: inset 0 1px color-mix(in oklch, white 8%, transparent), 0 1rem 3rem rgb(97 37 82 / 12%);
|
||||
}
|
||||
:root[data-theme="secret"] [data-slot="card"] {
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ export type ThemeDesign = {
|
|||
density: number;
|
||||
borderWidth: number;
|
||||
shadowStrength: number;
|
||||
surfaceContrast: number;
|
||||
fontScale: number;
|
||||
headingWeight: number;
|
||||
motion: number;
|
||||
|
|
|
|||
Loading…
Reference in a new issue