Port the neumorphic theme to daisyUI themes and Tailwind utilities

Styles/theme.css defines two daisyUI themes, neo-light (default) and neo-dark
(prefersdark). Every colour is an hsl() of three runtime variables:
--neo-hue-light, --neo-hue-dark and --neo-chroma. One formula replaces the six
hard-coded branches CascadingState writes today. Chroma 0 gives the grey themes,
and the fallbacks match PageSettings' defaults.

Styles/neo.css ports neomorph.scss:
- neo-raised, neo-inset and neo-flat, with sizes neo-2xs, neo-xs, neo-sm and
  neo-md. The size variables don't inherit, and Tailwind variants replace the
  old -sm..-2xl classes, some of which were never compiled.
- The pressed and raised gradients, and the rainbow hue-slider track.
- @utility overrides that give the neumorphic look to daisyUI's buttons, fields,
  selectors and surfaces. They sit above daisyUI's own cascade layers, so each
  one leaves out the variants it must not touch: ghost, link, outline and the
  rest.

Styles/base.css carries over base.scss: scrollbars, selection colours, the
focus outline reset, and link underlines everywhere except on buttons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-03 11:28:38 +02:00
1 parent 3551da1b02
commit 5b5fd62233
4 files changed
+448

No files matched your search

+4
View File
@@ -9,3 +9,7 @@
themes: false;
logs: false;
}
@import "./theme.css";
@import "./neo.css";
@import "./base.css";
+40
View File
@@ -0,0 +1,40 @@
/* Port of the old SCSS/base.scss and wwwroot/css/tailwind-override.css. daisyUI's rootcolor module already gives the
page base-100 and base-content. */
@layer base {
:focus-visible {
outline: none;
}
details > summary::-webkit-details-marker {
color: var(--color-base-content);
}
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-button,
::-webkit-scrollbar-track {
background-color: transparent;
}
::-webkit-scrollbar-thumb {
background-color: var(--neo-strong);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background-color: var(--color-base-content);
}
::selection {
color: var(--color-base-100);
background-color: var(--neo-strong);
}
/* Preflight resets link decoration; links keep their underline, links styled as buttons don't. */
a:not(.btn) {
text-decoration-line: underline;
}
}
+289
View File
@@ -0,0 +1,289 @@
/* Neumorphism on top of daisyUI.
Each element gets two shadows: a light one up-left and a dark one down-right, or both inset for a pressed look. The
colours are the theme's --neo-light / --neo-dark (theme.css); the offset and blur are --neo-o / --neo-b.
- neo-raised / neo-inset / neo-flat draw the shadows. neo-2xs, neo-xs (the default), neo-sm and neo-md set the
size, and Tailwind variants work on them (md:neo-sm).
- neo-gradient / neo-gradient-pressed are the 145deg gradients raised and pressed controls use.
- neo-hue-track turns a daisyUI range into the rainbow hue slider.
The daisyUI components are restyled with @utility <component>, as daisyUI recommends. Tailwind emits those rules
straight into the utilities layer, while daisyUI's own rules sit in sublayers of it (daisyui.l1.l2.l3 base,
l1.l2 modifiers, l1 states, daisyui disabled). So these overrides beat every daisyUI rule, variants included, and
each one excludes the variants it must leave alone. :where() keeps those exclusions at specificity (0,1,0).
To drop a neo shadow at a call site use shadow-none! or neo-flat!.
Never write @utility collapse: it brings back Tailwind's own collapse utility (visibility: collapse), which daisyUI
otherwise suppresses, and hides every collapse. DCollapse adds neo-raised in its markup instead. */
/* Not inherited, so a child doesn't pick up a parent's neo-sm size. */
@property --neo-o {
syntax: "<length>";
inherits: false;
initial-value: 3px;
}
@property --neo-b {
syntax: "<length>";
inherits: false;
initial-value: 6px;
}
@utility neo-raised {
box-shadow: calc(-1 * var(--neo-o)) calc(-1 * var(--neo-o)) var(--neo-b) var(--neo-light), var(--neo-o) var(--neo-o) var(--neo-b) var(--neo-dark);
}
@utility neo-inset {
box-shadow: inset var(--neo-o) var(--neo-o) var(--neo-b) var(--neo-dark), inset calc(-1 * var(--neo-o)) calc(-1 * var(--neo-o)) var(--neo-b) var(--neo-light);
}
@utility neo-flat {
box-shadow: none;
}
@utility neo-2xs {
--neo-o: 2px;
--neo-b: 4px;
}
@utility neo-xs {
--neo-o: 3px;
--neo-b: 6px;
}
@utility neo-sm {
--neo-o: 6px;
--neo-b: 12px;
}
@utility neo-md {
--neo-o: 8px;
--neo-b: 16px;
}
@utility neo-gradient {
background-image: linear-gradient(145deg, var(--neo-grad-light), var(--neo-grad-dark));
}
@utility neo-gradient-pressed {
background-image: linear-gradient(145deg, var(--neo-grad-dark), var(--neo-grad-light));
}
@utility neo-hue-track {
--range-fill: 0;
--range-bg: transparent;
&::-webkit-slider-runnable-track {
background-image: linear-gradient(to right, hsl(0 90% 80%), hsl(60 90% 80%), hsl(120 90% 80%), hsl(180 90% 80%), hsl(240 90% 80%), hsl(300 90% 80%), hsl(359 90% 80%));
box-shadow: inset 2px 2px 4px var(--neo-dark), inset -2px -2px 4px var(--neo-light);
}
&::-moz-range-track {
background-image: linear-gradient(to right, hsl(0 90% 80%), hsl(60 90% 80%), hsl(120 90% 80%), hsl(180 90% 80%), hsl(240 90% 80%), hsl(300 90% 80%), hsl(359 90% 80%));
box-shadow: inset 2px 2px 4px var(--neo-dark), inset -2px -2px 4px var(--neo-light);
}
}
/* Buttons: raised, with a gradient built from whatever colour the button has; pressed (active, checked) they sink in
with the gradient reversed. The original neoBtn* classes had no hover state, so daisyUI's hover tint is dropped. */
@utility btn {
&:not(:where(.btn-ghost, .btn-link, .btn-outline, .btn-dash, .btn-soft, .btn-disabled, :disabled, [disabled], [aria-disabled="true"])) {
--btn-bg: var(--btn-color, var(--color-base-100));
--btn-border: transparent;
background-image: linear-gradient(145deg, color-mix(in oklab, var(--btn-bg), white var(--neo-lift)), color-mix(in oklab, var(--btn-bg), black var(--neo-sink)));
box-shadow: -3px -3px 6px var(--neo-light), 3px 3px 6px var(--neo-dark);
&:where(:focus-visible) {
background-image: linear-gradient(-45deg, color-mix(in oklab, var(--btn-bg), white var(--neo-lift)), color-mix(in oklab, var(--btn-bg), black var(--neo-sink)));
}
&:where(.btn-sm, .btn-xs) {
box-shadow: -2px -2px 4px var(--neo-light), 2px 2px 4px var(--neo-dark);
}
&:where(:active, .btn-active, :checked, [aria-pressed="true"], [aria-current="page"]) {
background-image: linear-gradient(145deg, color-mix(in oklab, var(--btn-bg), black var(--neo-sink)), color-mix(in oklab, var(--btn-bg), white var(--neo-lift)));
box-shadow: inset 3px 3px 6px var(--neo-dark), inset -3px -3px 6px var(--neo-light);
translate: none;
}
}
}
/* Text fields: a pressed well, darker while focused, with no focus ring and no border unless a colour or a failed
validation asks for one. */
@utility input {
&:not(:where(.input-ghost)) {
background-color: var(--color-base-100);
background-image: linear-gradient(145deg, var(--neo-grad-dark), var(--neo-grad-light));
box-shadow: inset 2px 2px 4px var(--neo-dark), inset -2px -2px 4px var(--neo-light);
&:focus, &:focus-within {
outline: none;
background-image: linear-gradient(145deg, var(--neo-grad-darker), var(--neo-grad-lighter));
box-shadow: inset 2px 2px 4px var(--neo-dark), inset -2px -2px 4px var(--neo-light);
}
}
&:not(:where(.input-ghost, .input-neutral, .input-primary, .input-secondary, .input-accent, .input-info, .input-success, .input-warning, .input-error, .validator:user-invalid, [aria-invalid="true"])) {
border-color: transparent;
}
}
@utility select {
&:not(:where(.select-ghost)) {
background-color: var(--color-base-100);
background-image: linear-gradient(145deg, var(--neo-grad-dark), var(--neo-grad-light));
box-shadow: inset 2px 2px 4px var(--neo-dark), inset -2px -2px 4px var(--neo-light);
&:focus, &:focus-within {
outline: none;
background-image: linear-gradient(145deg, var(--neo-grad-darker), var(--neo-grad-lighter));
box-shadow: inset 2px 2px 4px var(--neo-dark), inset -2px -2px 4px var(--neo-light);
}
}
&:not(:where(.select-ghost, .select-neutral, .select-primary, .select-secondary, .select-accent, .select-info, .select-success, .select-warning, .select-error, .validator:user-invalid, [aria-invalid="true"])) {
border-color: transparent;
}
}
@utility textarea {
&:not(:where(.textarea-ghost)) {
background-color: var(--color-base-100);
background-image: linear-gradient(145deg, var(--neo-grad-dark), var(--neo-grad-light));
box-shadow: inset 2px 2px 4px var(--neo-dark), inset -2px -2px 4px var(--neo-light);
&:focus, &:focus-within {
outline: none;
background-image: linear-gradient(145deg, var(--neo-grad-darker), var(--neo-grad-lighter));
box-shadow: inset 2px 2px 4px var(--neo-dark), inset -2px -2px 4px var(--neo-light);
}
}
&:not(:where(.textarea-ghost, .textarea-neutral, .textarea-primary, .textarea-secondary, .textarea-accent, .textarea-info, .textarea-success, .textarea-warning, .textarea-error, .validator:user-invalid, [aria-invalid="true"])) {
border-color: transparent;
}
}
@utility file-input {
&:not(:where(.file-input-ghost)) {
background-color: var(--color-base-100);
background-image: linear-gradient(145deg, var(--neo-grad-dark), var(--neo-grad-light));
box-shadow: inset 2px 2px 4px var(--neo-dark), inset -2px -2px 4px var(--neo-light);
}
&:not(:where(.file-input-ghost, .file-input-neutral, .file-input-primary, .file-input-secondary, .file-input-accent, .file-input-info, .file-input-success, .file-input-warning, .file-input-error, .validator:user-invalid, [aria-invalid="true"])) {
border-color: transparent;
}
}
/* Selectors: an inset well while unchecked; once checked they keep daisyUI's filled look. */
@utility checkbox {
&:not(:where(:checked, :indeterminate, [aria-checked="true"], :disabled)) {
border-color: transparent;
background-image: linear-gradient(145deg, var(--neo-grad-dark), var(--neo-grad-light));
box-shadow: inset 2px 2px 4px var(--neo-dark), inset -2px -2px 4px var(--neo-light);
}
}
@utility radio {
&:not(:where(:checked, [aria-checked="true"], :disabled)) {
border-color: transparent;
background-image: linear-gradient(145deg, var(--neo-grad-dark), var(--neo-grad-light));
box-shadow: inset 2px 2px 4px var(--neo-dark), inset -2px -2px 4px var(--neo-light);
}
}
@utility toggle {
&:not(:where(:disabled)) {
border-color: transparent;
background-image: linear-gradient(145deg, var(--neo-grad-dark), var(--neo-grad-light));
box-shadow: inset 2px 2px 4px var(--neo-dark), inset -2px -2px 4px var(--neo-light);
&::before {
box-shadow: -1px -1px 2px var(--neo-light), 1px 1px 2px var(--neo-dark);
}
}
}
@utility range {
&::-webkit-slider-runnable-track {
box-shadow: inset 2px 2px 4px var(--neo-dark), inset -2px -2px 4px var(--neo-light);
}
&::-moz-range-track {
box-shadow: inset 2px 2px 4px var(--neo-dark), inset -2px -2px 4px var(--neo-light);
}
}
/* Surfaces: raised like the old .neomorph panels. */
@utility card {
&:not(:where(.card-border, .card-dash)) {
box-shadow: -3px -3px 6px var(--neo-light), 3px 3px 6px var(--neo-dark);
}
}
@utility modal-box {
box-shadow: -6px -6px 12px var(--neo-light), 6px 6px 12px var(--neo-dark);
}
@utility dropdown-content {
background-color: var(--color-base-100);
box-shadow: -3px -3px 6px var(--neo-light), 3px 3px 6px var(--neo-dark);
}
@utility alert {
&:not(:where(.alert-outline, .alert-dash, .alert-soft)) {
box-shadow: -3px -3px 6px var(--neo-light), 3px 3px 6px var(--neo-dark);
}
}
@utility stats {
box-shadow: -3px -3px 6px var(--neo-light), 3px 3px 6px var(--neo-dark);
}
@utility timeline-box {
border-color: transparent;
box-shadow: -2px -2px 4px var(--neo-light), 2px 2px 4px var(--neo-dark);
}
@utility badge {
&:not(:where(.badge-ghost, .badge-outline, .badge-dash, .badge-soft)) {
box-shadow: -2px -2px 4px var(--neo-light), 2px 2px 4px var(--neo-dark);
}
}
@utility kbd {
box-shadow: -2px -2px 4px var(--neo-light), 2px 2px 4px var(--neo-dark);
}
@utility tabs-box {
background-color: var(--color-base-100);
box-shadow: inset 2px 2px 4px var(--neo-dark), inset -2px -2px 4px var(--neo-light);
}
@utility chat-bubble {
box-shadow: 2px 2px 4px var(--neo-dark);
}
/* Wells: tracks and placeholders sink in. */
@utility progress {
box-shadow: inset 2px 2px 4px var(--neo-dark), inset -2px -2px 4px var(--neo-light);
}
@utility skeleton {
box-shadow: inset 2px 2px 4px var(--neo-dark), inset -2px -2px 4px var(--neo-light);
}
@utility mockup-browser {
box-shadow: -3px -3px 6px var(--neo-light), 3px 3px 6px var(--neo-dark);
}
@utility mockup-window {
box-shadow: -3px -3px 6px var(--neo-light), 3px 3px 6px var(--neo-dark);
}
@utility mockup-code {
box-shadow: -3px -3px 6px var(--neo-light), 3px 3px 6px var(--neo-dark);
}
+115
View File
@@ -0,0 +1,115 @@
/* The two daisyUI themes. Every colour derives from three variables that wwwroot/js/theme.js sets on <html> from
PageSettings: --neo-hue-light / --neo-hue-dark (0-359) and --neo-chroma (1 for the colour themes, 0 for the grey
ones). The fallbacks match PageSettings' defaults, so the page renders as it will before Blazor boots.
theme.js sets data-theme="neo-light" or "neo-dark"; with PreferSystemTheming it removes data-theme, and the
prefersdark rule picks neo-dark when the OS asks for dark.
Plugin gotchas:
- A top-level comma in a value is split and only the last part survives. Commas inside var()/calc() are fine,
which is why multi-layer shadows are built in neo.css rather than stored here.
- neo-light's :where(:root) always matches, so neo-dark must set every token neo-light sets.
--neo-light / --neo-dark are the two neumorphic shadow colours, --neo-grad-* the gradient stops around base-100,
--neo-strong the deeper tone used for scrollbars and selections, and --neo-lift / --neo-sink how far a
gradient built from an arbitrary colour (a coloured .btn) moves towards white and black. */
@plugin "../.tools/tailwind/daisyui-theme.mjs" {
name: "neo-light";
default: true;
prefersdark: false;
color-scheme: light;
--neo-hue: var(--neo-hue-light, 25);
--neo-sat: calc(84% * var(--neo-chroma, 0));
--color-base-100: hsl(var(--neo-hue) var(--neo-sat) 88%);
--color-base-200: hsl(var(--neo-hue) var(--neo-sat) 84%);
--color-base-300: hsl(var(--neo-hue) var(--neo-sat) 80%);
--color-base-content: hsl(var(--neo-hue) var(--neo-sat) 26.4%);
--color-primary: hsl(var(--neo-hue) var(--neo-sat) 66%);
--color-primary-content: hsl(var(--neo-hue) var(--neo-sat) 15%);
--color-secondary: hsl(calc(var(--neo-hue) + 180) var(--neo-sat) 66%);
--color-secondary-content: hsl(calc(var(--neo-hue) + 180) var(--neo-sat) 15%);
--color-accent: hsl(calc(var(--neo-hue) + 90) var(--neo-sat) 66%);
--color-accent-content: hsl(calc(var(--neo-hue) + 90) var(--neo-sat) 15%);
--color-neutral: hsl(var(--neo-hue) calc(30% * var(--neo-chroma, 0)) 30%);
--color-neutral-content: hsl(var(--neo-hue) var(--neo-sat) 88%);
--color-info: hsl(196 82% 42%);
--color-info-content: hsl(196 82% 96%);
--color-success: hsl(140 60% 34%);
--color-success-content: hsl(140 60% 96%);
--color-warning: hsl(40 92% 50%);
--color-warning-content: hsl(40 92% 12%);
--color-error: hsl(0 72% 50%);
--color-error-content: hsl(0 72% 97%);
--neo-light: hsl(var(--neo-hue) var(--neo-sat) 100% / .5);
--neo-dark: hsl(var(--neo-hue) var(--neo-sat) 66% / .5);
--neo-grad-light: hsl(var(--neo-hue) var(--neo-sat) 92%);
--neo-grad-dark: hsl(var(--neo-hue) var(--neo-sat) 84%);
--neo-grad-lighter: hsl(var(--neo-hue) var(--neo-sat) 96%);
--neo-grad-darker: hsl(var(--neo-hue) var(--neo-sat) 80%);
--neo-strong: hsl(var(--neo-hue) var(--neo-sat) 66%);
--neo-lift: 33%;
--neo-sink: 5%;
--radius-selector: 1rem;
--radius-field: 2rem;
--radius-box: .75rem;
--size-selector: .25rem;
--size-field: .25rem;
--border: 1px;
--depth: 0;
--noise: 0;
}
@plugin "../.tools/tailwind/daisyui-theme.mjs" {
name: "neo-dark";
default: false;
prefersdark: true;
color-scheme: dark;
--neo-hue: var(--neo-hue-dark, 215);
--neo-sat: calc(16% * var(--neo-chroma, 0));
--color-base-100: hsl(var(--neo-hue) var(--neo-sat) 12%);
--color-base-200: hsl(var(--neo-hue) var(--neo-sat) 8%);
--color-base-300: hsl(var(--neo-hue) var(--neo-sat) 4%);
--color-base-content: hsl(var(--neo-hue) var(--neo-sat) 73.6%);
--color-primary: hsl(var(--neo-hue) calc(40% * var(--neo-chroma, 0)) 40%);
--color-primary-content: hsl(var(--neo-hue) calc(40% * var(--neo-chroma, 0)) 92%);
--color-secondary: hsl(calc(var(--neo-hue) + 180) calc(40% * var(--neo-chroma, 0)) 40%);
--color-secondary-content: hsl(calc(var(--neo-hue) + 180) calc(40% * var(--neo-chroma, 0)) 92%);
--color-accent: hsl(calc(var(--neo-hue) + 90) calc(40% * var(--neo-chroma, 0)) 40%);
--color-accent-content: hsl(calc(var(--neo-hue) + 90) calc(40% * var(--neo-chroma, 0)) 92%);
--color-neutral: hsl(var(--neo-hue) var(--neo-sat) 78%);
--color-neutral-content: hsl(var(--neo-hue) var(--neo-sat) 12%);
--color-info: hsl(196 62% 58%);
--color-info-content: hsl(196 62% 10%);
--color-success: hsl(140 46% 52%);
--color-success-content: hsl(140 46% 9%);
--color-warning: hsl(40 80% 58%);
--color-warning-content: hsl(40 80% 10%);
--color-error: hsl(0 66% 62%);
--color-error-content: hsl(0 66% 10%);
--neo-light: hsl(var(--neo-hue) calc(84% * var(--neo-chroma, 0)) 66% / .1);
--neo-dark: hsl(var(--neo-hue) var(--neo-sat) 1% / .5);
--neo-grad-light: hsl(var(--neo-hue) var(--neo-sat) 16%);
--neo-grad-dark: hsl(var(--neo-hue) var(--neo-sat) 8%);
--neo-grad-lighter: hsl(var(--neo-hue) var(--neo-sat) 20%);
--neo-grad-darker: hsl(var(--neo-hue) var(--neo-sat) 4%);
--neo-strong: hsl(var(--neo-hue) var(--neo-sat) 33%);
--neo-lift: 5%;
--neo-sink: 33%;
--radius-selector: 1rem;
--radius-field: 2rem;
--radius-box: .75rem;
--size-selector: .25rem;
--size-field: .25rem;
--border: 1px;
--depth: 0;
--noise: 0;
}