diff --git a/Styles/app.css b/Styles/app.css index 91725c1..0980ba1 100644 --- a/Styles/app.css +++ b/Styles/app.css @@ -9,3 +9,7 @@ themes: false; logs: false; } + +@import "./theme.css"; +@import "./neo.css"; +@import "./base.css"; diff --git a/Styles/base.css b/Styles/base.css new file mode 100644 index 0000000..a998602 --- /dev/null +++ b/Styles/base.css @@ -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; + } +} diff --git a/Styles/neo.css b/Styles/neo.css new file mode 100644 index 0000000..3f1bbf4 --- /dev/null +++ b/Styles/neo.css @@ -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 , 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: ""; + inherits: false; + initial-value: 3px; +} + +@property --neo-b { + syntax: ""; + 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); +} diff --git a/Styles/theme.css b/Styles/theme.css new file mode 100644 index 0000000..4fc16bb --- /dev/null +++ b/Styles/theme.css @@ -0,0 +1,115 @@ +/* The two daisyUI themes. Every colour derives from three variables that wwwroot/js/theme.js sets on 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; +}