(feat): more improvements
All checks were successful
/ deploy-and-package (push) Successful in 1m4s

This commit is contained in:
Alois 2026-07-26 16:14:16 +02:00
commit bb065692b0
Signed by: alois
SSH key fingerprint: SHA256:GBzT2DXvAuGV9XIV5W3WrzVpjU54FThmxHXdbz95J24
8 changed files with 92 additions and 132 deletions

View file

@ -46,7 +46,7 @@ export default function App() {
return (
<main className="mx-auto flex max-w-[1500px] flex-col gap-7 p-5 md:p-10">
<div>
<Button variant="outline" onClick={() => setShowOnboarding(true)}>
<Button variant="subtleDefault" onClick={() => setShowOnboarding(true)}>
Open onboarding
</Button>
</div>

View file

@ -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)));

View file

@ -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

View file

@ -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",

View file

@ -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>
);
}

View file

@ -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"

View file

@ -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"] {

View file

@ -29,7 +29,6 @@ export type ThemeDesign = {
density: number;
borderWidth: number;
shadowStrength: number;
surfaceContrast: number;
fontScale: number;
headingWeight: number;
motion: number;