Better color support, glow and simple component styling
Some checks failed
Publish Library / Build NPM Project (push) Failing after 23s
Publish Library / Tag Version (push) Has been skipped
Publish Library / Build Container (push) Successful in 24s

This commit is contained in:
2026-08-29 21:10:01 -04:00
parent 3b209f3a98
commit 0582970858
5 changed files with 1349 additions and 321 deletions

429
src/components.scss Normal file
View File

@@ -0,0 +1,429 @@
@use 'sass:map';
// Shared bits ================================================================
%surface {
background: var(--theme-darker);
border: 0.5px solid var(--theme-border);
border-radius: var(--theme-radius);
color: var(--theme-text);
}
%disabled-control {
&:disabled {
cursor: not-allowed;
opacity: .5;
& ~ span { color: var(--theme-muted); cursor: not-allowed; }
}
}
// $text lets black/white variants flip contrast; everything else defaults to white
@mixin color-variant($color, $text: white) {
&.#{$color} {
--color: var(--theme-#{$color});
--text: #{$text};
}
}
// $mix is the color the tint blends toward; black variant needs to blend toward
// white instead of black or the background goes fully black (invisible text)
@mixin indicator-variant($color, $mix: black) {
&.#{$color} {
--background: color-mix(in srgb, var(--theme-#{$color}) 20%, #{$mix});
--color: var(--theme-#{$color});
}
}
// Checkbox/radio share hover-halo + checked/disabled behavior, differ only in shape
@mixin control-input($radius) {
appearance: none;
width: 20px;
height: 20px;
flex-shrink: 0;
position: relative;
border: 2px solid var(--theme-border);
border-radius: $radius;
cursor: pointer;
transition: border-color 0.2s;
&::before {
content: '';
position: absolute;
inset: -10px;
border-radius: $radius;
background: transparent;
transition: background 0.2s;
}
&:hover::before { background: var(--theme-shade); }
&:checked { border-color: var(--theme-accent); }
@include disableable-input;
}
@mixin disableable-input {
&:disabled {
opacity: .5;
cursor: not-allowed;
& ~ span { color: var(--theme-muted); cursor: not-allowed; }
}
}
// Components ==================================================================
.banner {
--color: var(--theme-info);
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
border-radius: var(--theme-radius);
border: 0.5px solid var(--color);
background: color-mix(in srgb, var(--color) 12%, var(--theme-surface));
color: var(--theme-text);
@each $c in (success, warn, danger, info, primary, accent, "black", "white") {
&.#{$c} { --color: var(--theme-#{$c}); }
}
}
.button {
--color: var(--theme-shade);
--text: var(--theme-text);
font-size: 13px;
padding: 6px 15px;
border-radius: var(--theme-radius);
border: 0.5px solid var(--theme-border);
background: var(--color);
color: var(--text);
cursor: pointer;
transition: background .15s, transform .1s;
white-space: nowrap;
@include color-variant('info');
@include color-variant('success');
@include color-variant('danger');
@include color-variant('warn');
@include color-variant('primary');
@include color-variant('accent');
@include color-variant('white', black);
@include color-variant('black', white);
&:hover { filter: brightness(1.2); }
}
.card {
@extend %surface;
padding: 18px 22px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
background: var(--theme-surface);
}
.checkbox, .radio {
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
font-size: 14px;
font-family: var(--theme-font), sans-serif;
color: var(--theme-text);
user-select: none;
}
.checkbox input[type="checkbox"] {
@include control-input(4px);
&:checked {
background: var(--theme-accent);
&::after {
content: '';
position: absolute;
left: 4.5px; top: 0;
width: 5px; height: 10px;
border: 2px solid white;
border-top: none; border-left: none;
transform: rotate(45deg);
}
}
}
.chip {
display: flex;
align-items: center;
justify-content: center;
color: var(--theme-muted);
background: var(--theme-darker);
border: 0.5px solid var(--theme-border);
padding: 3px 10px;
border-radius: 999px;
font-size: 0.75em;
* { margin: 0; }
}
.expander {
@extend %surface;
padding: 0;
overflow: hidden;
summary {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 10px 14px;
cursor: pointer;
list-style: none;
&::-webkit-details-marker { display: none; }
&::after {
content: '';
color: var(--theme-muted);
transition: transform .2s;
}
}
&[open] summary::after { transform: rotate(90deg); }
.expander-body {
padding: 0 14px 14px;
border-top: 0.5px solid var(--theme-border);
padding-top: 14px;
}
}
.indicator {
--background: color-mix(in srgb, var(--theme-info) 20%, black);
--color: var(--theme-info);
width: 26px;
height: 26px;
display: flex;
align-items: center;
justify-content: center;
background: var(--background);
border: 0.5px solid var(--color);
border-radius: 50%;
color: var(--color);
font-size: 16px;
@include indicator-variant('warn');
@include indicator-variant('danger');
@include indicator-variant('success');
@include indicator-variant('primary');
@include indicator-variant('accent');
@include indicator-variant('white');
@include indicator-variant('black', white);
}
.input, textarea.input {
@extend %surface;
padding: 8px 10px;
cursor: text;
font-family: var(--theme-font), sans-serif;
@include disableable-input;
&:focus-within { border-color: var(--theme-accent); }
}
textarea.input {
resize: vertical;
min-height: 60px;
}
input[type="color"].input {
padding: 0;
width: 40px;
height: 28px;
border-radius: var(--theme-radius);
overflow: hidden;
&::-webkit-color-swatch-wrapper { padding: 0; }
&::-webkit-color-swatch { border: none; border-radius: var(--theme-radius); }
&::-moz-color-swatch { border: none; border-radius: var(--theme-radius); }
}
.input-group {
display: inline-flex;
align-items: stretch;
@extend %surface;
overflow: hidden;
background: var(--theme-darker);
> * {
border: none;
border-radius: 0;
background: transparent;
}
// let colored buttons keep their own bg/text, everything else blends in
> .button:is(.info, .success, .warn, .danger, .primary, .accent, .white, .black) {
background: var(--color);
color: var(--text);
}
&:focus-within { border-color: var(--theme-accent); }
}
select.select {
@extend %surface;
padding: 8px 10px;
cursor: pointer;
@include disableable-input;
&:focus-within { border-color: var(--theme-accent); }
option {
color: var(--theme-text);
background: var(--theme-surface);
}
&[multiple] {
padding: 0;
max-height: 200px;
overflow-y: auto;
option {
padding: 5px 10px;
background: var(--theme-darker);
&:checked {
background: color-mix(in srgb, var(--theme-primary) 10%, transparent);
color: var(--theme-primary);
font-weight: 500;
}
&:hover { background: color-mix(in srgb, var(--theme-primary) 10%, transparent); }
}
}
}
.radio {
padding: 6px 0;
input[type="radio"] {
@include control-input(50%);
&:checked::after {
content: '';
position: absolute;
inset: 3px;
background: var(--theme-accent);
border-radius: 50%;
}
}
}
.range {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
width: 100%;
height: 16px;
background: transparent;
cursor: pointer;
@extend %disabled-control;
&::-webkit-slider-runnable-track {
width: 100%;
height: 4px;
border-radius: 999px;
background: var(--theme-muted);
}
&::-moz-range-track {
width: 100%;
height: 4px;
border-radius: 999px;
background: var(--theme-muted);
}
&::-webkit-slider-thumb {
appearance: none;
-webkit-appearance: none;
width: 16px; height: 16px;
margin-top: -6px;
border-radius: 50%;
background: var(--theme-accent);
border: 2px solid var(--theme-surface);
transition: transform .15s;
&:hover { transform: scale(1.15); }
}
&::-moz-range-thumb {
width: 16px; height: 16px;
border-radius: 50%;
background: var(--theme-accent);
border: 2px solid var(--theme-surface);
}
}
.table {
display: table;
width: 100%;
border-collapse: separate;
border-spacing: 0;
border: 1px solid var(--theme-border);
border-radius: var(--theme-radius);
color: var(--theme-text);
font-size: .875rem;
&.select tbody {
& > * { cursor: pointer; }
tr:hover {
background-color: var(--theme-accent);
color: var(--theme-white);
}
}
thead {
background-color: var(--theme-shade);
border-bottom: 2px solid var(--theme-border);
}
tbody tr { transition: background-color 0.2s; }
&.striped {
tbody tr:nth-child(even) { background-color: var(--theme-shade-alt); }
}
th { padding: 1rem; text-align: left; font-weight: bold; color: var(--theme-primary); }
td { padding: 1rem; vertical-align: middle; }
}
.toggle {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-size: 14px;
font-family: var(--theme-font), sans-serif;
color: var(--theme-text);
user-select: none;
input[type="checkbox"] {
appearance: none;
width: 36px;
height: 20px;
border: none;
border-radius: 999px;
background: var(--theme-border);
position: relative;
cursor: pointer;
flex-shrink: 0;
transition: background .2s;
&::after {
content: '';
position: absolute;
top: 2px; left: 2px;
width: 16px; height: 16px;
border-radius: 50%;
background: var(--theme-white);
transition: transform .2s;
}
&:checked {
background: var(--theme-accent);
&::after { transform: translateX(16px); }
}
@extend %disabled-control;
}
}

View File

@@ -11,7 +11,7 @@
--theme-accent: #2caec0;
--theme-accent-contrast: black;
--theme-info: #6157cc;
--theme-info: #5786cc;
--theme-success: #65c147;
--theme-warn: #d88750;
--theme-danger: #bf4f4f;
@@ -22,6 +22,7 @@
--theme-animation: 0.2s linear;
--theme-content-width: 1100px;
--theme-font: Arial, sans-serif;
--theme-radius: 0.5rem;
// Theme dependant (light/dark)
color-scheme: light dark;
@@ -29,8 +30,13 @@
--theme-border: light-dark(#E5E7EB, #374151);
--theme-muted: light-dark(#6c757d, #cccccc);
--theme-text: light-dark(#000000, #ffffff);
--theme-lighter: light-dark(rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.1));
--theme-darker: light-dark(rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.1));
--theme-lighter: light-dark(rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.1));
--theme-shade: light-dark(var(--theme-darker), var(--theme-lighter));
--theme-shade-alt: light-dark(var(--theme-lighter), var(--theme-darker));
scrollbar-width: thin;
scrollbar-color: var(--theme-border) transparent;
}
.theme-light { color-scheme: light !important; }
@@ -39,6 +45,17 @@
$breakpoints: (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px);
$sizes: (0: 0, 1: 0.25rem, 2: 0.5rem, 3: 1rem, 4: 1.5rem, 5: 2.5rem);
// Colors that get generated lighter/darker variants (computed once as CSS vars below)
$variant-colors: ('primary', 'accent', 'background', 'surface', 'info', 'success', 'warn', 'danger', 'muted', 'border', 'text', 'black', 'white');
// Generate lighter/darker CSS custom properties once, so all utilities can reuse them
:root {
@each $c in $variant-colors {
--theme-#{$c}-lighter: color-mix(in srgb, var(--theme-#{$c}) 80%, white);
--theme-#{$c}-darker: color-mix(in srgb, var(--theme-#{$c}) 80%, black);
}
}
// Don't edit past this point ===============================================================================
$direction: ('s': 'left', 't': 'top', 'e': 'right', 'b': 'bottom');
@@ -171,15 +188,17 @@ $divisible: (0, 10, 20, 25, 30, 33, 34, 40, 50, 60, 66, 70, 75, 80, 90, 100);
}
// Border - Style
.b-dash { border-style: dashed !important; }
.b-dot { border-style: dotted !important; }
.b-double { border-style: double !important; }
.b-none { border-style: none !important; }
.b-solid { border-style: solid !important; }
.b-dash { border-style: dashed !important; border-color: var(--theme-border); }
.b-dot { border-style: dotted !important; border-color: var(--theme-border); }
.b-double { border-style: double !important; border-color: var(--theme-border); }
.b-none { border-style: none !important; border-color: var(--theme-border); }
.b-solid { border-style: solid !important; border-color: var(--theme-border); }
// Colors - Shared mixin for efficiency
@mixin color-variant($name, $bg-var, $fg-var: null) {
.b-#{$name} { border-color: var(--theme-#{$bg-var}) !important; }
// Colors - Shared mixin, generates border/background/foreground variants for a color
@mixin color-variant($name, $bg-var, $fg-var: null, $border: true) {
@if $border {
.b-#{$name} { border-color: var(--theme-#{$bg-var}) !important; }
}
.bg-#{$name} {
background-color: var(--theme-#{$bg-var}) !important;
@if $fg-var {
@@ -212,27 +231,24 @@ $divisible: (0, 10, 20, 25, 30, 33, 34, 40, 50, 60, 66, 70, 75, 80, 90, 100);
@include color-variant('warn', 'warn');
@include color-variant('danger', 'danger');
// Lighter/darker variants (bg-, fg- only) for every color in $variant-colors, using the CSS vars generated above
@each $c in $variant-colors {
@include color-variant('#{$c}-lighter', '#{$c}-lighter', $border: false);
@include color-variant('#{$c}-darker', '#{$c}-darker', $border: false);
}
.bg-shade { background-color: var(--theme-shade); }
.bg-shade-alt { background-color: var(--theme-shade-alt); }
.bg-transparent { background-color: transparent !important; }
// Lighter/Darker pseudo variants
@each $color in ('primary', 'accent', 'background', 'surface', 'info', 'success', 'warn', 'danger', 'muted') {
.bg-#{$color}-lighter, .bg-#{$color}-darker {
position: relative;
z-index: 1;
color: var(--theme-#{$color}-contrast);
text-decoration-color: var(--theme-#{$color}-contrast);
&::before {
content: '';
position: absolute;
inset: 0;
background-color: var(--theme-#{$color});
border-radius: inherit;
z-index: -1;
$glow-intensity: ('': 50%, '-sm': 25%, '-lg': 75%);
@each $name in ('primary', 'accent', 'info', 'success', 'warn', 'danger') {
@each $suffix, $intensity in $glow-intensity {
.bg-glow-#{$name}#{$suffix} {
background-image: radial-gradient(circle at top left, color-mix(in srgb, var(--theme-#{$name}) #{$intensity}, transparent) 0%, transparent 110%);
background-repeat: no-repeat;
}
}
.bg-#{$color}-lighter::before { filter: saturate(.5) brightness(150%); }
.bg-#{$color}-darker::before { filter: saturate(.5) brightness(50%); }
}
// Cursors