diff --git a/SKILL.md b/SKILL.md index b00315f..aea611a 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,42 +1,54 @@ --- name: CSS Utilities description: How to style all HTML websites using @ztimson/css-utils (bootstrap inspired) -modified: 2026-08-01T06:12:28.966Z +modified: 2026-08-29T00:00:00.000Z --- # SCSS Utility Framework -A single-file (`_theme.scss`) utility-class system. No build step config needed beyond Sass with the `sass:map` module. Import once globally. +Two files: `_theme.scss` (utility classes) and `_components.scss` (styled elements). No build step config needed beyond Sass with the `sass:map` module. Import both once globally, theme before components. ## Theme Setup All colors/tokens live in `:root` as CSS custom properties — edit these, not the utility classes: - ```scss --theme-background +--theme-background-contrast --theme-primary +--theme-primary-contrast --theme-accent ---theme-success +--theme-accent-contrast + --theme-info +--theme-success --theme-warn --theme-danger --theme-black --theme-white +--theme-animation +--theme-content-width +--theme-font +--theme-radius + // Light/dark aware variables (auto via `light-dark()`): --theme-surface --theme-border ---theme-text --theme-muted ---theme-lighter (transparent) ---theme-darker (transparent) +--theme-text +--theme-darker (transparent overlay) +--theme-lighter (transparent overlay) +--theme-shade // = darker in light mode, lighter in dark mode +--theme-shade-alt // inverse of --theme-shade // Breakpoints & spacing $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); ``` +Every color in `$variant-colors` (`primary, accent, background, surface, info, success, warn, danger, muted, border, text, black, white`) auto-generates `--theme-{color}-lighter` / `-darker` CSS vars (`color-mix` 80% toward white/black). These back the `-lighter`/`-darker` utility classes below — don't hand-write them. + Force a scheme on any element: `.theme-light` / `.theme-dark`, defaults to system preference. ## "Fix" Classes (reboot/reset helpers) @@ -44,14 +56,14 @@ Force a scheme on any element: `.theme-light` / `.theme-dark`, defaults to syste Apply `.fix` on `body`/root wrapper for all resets, or pick individual ones: | Class | Purpose | -|------------------|-------------------------------------------------------------| +|------------------|---------------------------------------------------------------| | `.fix-anchor` | Styled `` links using `--theme-primary` | | `.fix-button` | `cursor: pointer` on enabled buttons | | `.fix-dom` | 100% height/width, no margin/padding, box-sizing border-box | | `.fix-focus` | Removes outline/tap highlight | | `.fix-font` | Base font sizing/weights for headings & form elements | -| `.fix-scrollbar` | Styled webkit scrollbars using `--theme-muted` | -| `.fix` | All of the above | +| `.fix-scrollbar` | Styled webkit scrollbars using `--theme-muted` | +| `.fix` | All of the above | Other misc: `.center` (absolute centering), `.clamp` (max content width), `.reset` (`all: revert`). @@ -59,11 +71,18 @@ Other misc: `.center` (absolute centering), `.clamp` (max content width), `.rese Pattern: `.b-{color}` (border), `.bg-{color}` (background + auto contrast text), `.fg-{color}` (text color). -Colors: `black, white, background, background-contrast, primary, primary-contrast, accent, accent-contrast, surface, border, text, muted, lighter, darker, info, success, warn, danger` +Base colors: `black, white, background, background-contrast, primary, primary-contrast, accent, accent-contrast, surface, border, text, muted, lighter, darker, info, success, warn, danger` -Variants: `.bg-{color}-lighter` / `.bg-{color}-darker` (filtered overlay) for `primary, accent, background, surface, info, success, warn, danger, muted`. +- `primary` and `accent` auto-apply their `-contrast` color as text/decoration when used via `.bg-primary` / `.bg-accent`. +- `lighter` / `darker` here are the transparent shade overlays, not color variants. -`.bg-transparent` also available. +**Lighter/darker color variants** (`.bg-{color}-lighter`, `.bg-{color}-darker`, `.fg-{color}-lighter`, `.fg-{color}-darker` — no border variant) for every color in `$variant-colors`: `primary, accent, background, surface, info, success, warn, danger, muted, border, text, black, white`. + +`.bg-shade` / `.bg-shade-alt` (scheme-aware shade), `.bg-transparent`. + +### Glow + +`.bg-glow-{primary|accent|info|success|warn|danger}{,-sm,-lg}` — radial glow from top-left corner, spread fixed at 110%, intensity varies by suffix (`-sm`=25%, none=50%, `-lg`=75%). Combine with any background: `.bg-black.bg-glow-primary-lg`. ## Typography (`.fs-*`) @@ -117,10 +136,36 @@ Per breakpoint, uses `$sizes` keys (0–5): - `.z-{0-5}` - `.curs-{name}` — full CSS cursor keyword list (pointer, grab, not-allowed, zoom-in, etc.) - `.hover:hover`, `.curs-hover` — brightness(125%) on hover -- `.align-x, .align-x-start, .align-x-end` (text-align), `.align-y*` (vertical-align) ## Responsive Breakpoints Every layout/spacing/flex utility above is generated per breakpoint suffix: `-sm-, -md-, -lg-, -xl-` (min-width media queries) plus a `-print-` variant for `@media print`. No suffix = base/xs. Example: `w-md-50` → 50% width from `md` breakpoint up. `d-lg-flex` → flex display from `lg` up. + +--- + +# Component Classes + +Styled elements from `_components.scss`. Most support color variants by adding a modifier class matching a theme color: `info, success, warn, danger, primary, accent, black, white` (component-dependent, see below). No background/divider overrides needed — components are seamless with theme surfaces. + +| Class | Element | Notes | +|---|---|---| +| `.banner` | `div` | Alert/notice box. Default `info`. Variants: `success, warn, danger, info, primary, accent, black, white`. | +| `.button` | `button` | Default neutral (`--theme-shade`). Variants: `info, success, danger, warn, primary, accent, white, black`. | +| `.card` | `div` | Surface panel, padding + subtle shadow. | +| `.checkbox` | `label > input[type=checkbox] + span` | Custom check, disabled fades label text. | +| `.radio` | `label > input[type=radio] + span` | Custom radio dot, disabled fades label text. | +| `.chip` | `span`/`div` | Small pill badge, muted text. | +| `.expander` | `details.expander > summary + .expander-body` | Must use native `
/`; marker rotates on `[open]`, body has top border. | +| `.indicator` | `div`/`span` | Round icon/status badge. Default `info`. Variants: `warn, danger, success, primary, accent, white, black`. | +| `.input` | `input`, `textarea.input` | Surface form control. `textarea.input` is vertically resizable. `input[type=color].input` styled swatch. | +| `.input-group` | `div > .input/.button/.select` children | Bootstrap-style joined controls, no inner borders/dividers; colored `.button` children keep their own bg/text. | +| `.select` | `select.select` | Styled dropdown; `[multiple]` gets scrollable option list with hover/checked highlighting. | +| `.range` | `input[type=range].range` | Custom track + thumb, themed via `--theme-accent`. | +| `.table` | `table.table` | Bordered table. `.select` modifier = clickable rows with accent hover highlight. `.striped` modifier = zebra rows. | +| `.toggle` | `label > input[type=checkbox] + span` | Switch-style checkbox. | + +**Disabled state:** form controls (`.input`, `.select`, `.range`, `.checkbox`/`.radio`/`.toggle` inputs) fade to 50% opacity and mute their label `span` automatically via `:disabled`. + +**Color variant convention:** where supported, just add the theme color name as a class alongside the component class, e.g. ` +
+ + - // Misc - --theme-animation: 0.2s linear; - --theme-content-width: 1100px; - --theme-font: Arial, sans-serif; - - // Theme dependant (light/dark) - color-scheme: light dark; - --theme-surface: light-dark(#ffffff, #2e2e2e); - --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-light { color-scheme: light !important; } -.theme-dark { color-scheme: dark !important; } -}
+ +
+
Theme Variables — current :root values
+
+
+
@@ -369,8 +334,8 @@

Fixes

utility
-

Opt-in opinionated resets. Apply .fix to enable all, or use granular classes.

-
+

Opt-in opinionated resets. Apply .fix to enable all, or use granular classes. This very page is themed with .fix — its background, font and scrollbars all come from these classes rather than bespoke CSS.

+
.fix .fix-anchor .fix-button @@ -380,7 +345,7 @@ .fix-scrollbar .reset
-
+ +

Components

+ + + + + +
+
+

Button

components +
+
.button .info .success .warn .danger .primary .accent .white .black
+
+ + + + + + + + + +
+
+ + +
+
+

Card

components +
+
.card
+
+
+

A .card is just a themed surface: background, border and a soft shadow.

+
+
+
+ + +
+
+

Checkbox

components +
+
.checkbox
+
+ + + +
+
+ + +
+
+

Radio

components +
+
.radio
+
+ + + +
+
+ + +
+
+

Toggle

components +
+
.toggle
+
+ + + +
+

This is the same component powering the light/dark switch in the header.

+
+ + +
+
+

Chip

components +
+
.chip
+
+ Default + Another chip + 42 +
+
+ + +
+
+

Expander

components +
+
<details class="expander">
+
+
+ Click to expand +
Built on native <details>/<summary> — no JS required.
+
+
+
+ + +
+
+

Indicator

components +
+
.indicator .success .warn .danger .primary .accent .white .black
+
+ i + + ! + + P + A + W + B +
+
+ + +
+
+

Input

components +
+
.input
+
+ + + +
+
+
Color — input[type="color"].input
+
+ + + + Same surface & radius treatment as text inputs +
+
+
+ + +
+
+

Input Group

components +
+
.input-group
+
+
+
+ $ + + +
+
+
+
+ + +
+
+

Select

components +
+
.select
+
+
+ +
+ +
+
+ + +
+
+

Range

components +
+
.range
+
+ + +
+
+ + +
+
+

Table

components +
+
.table .striped .select
+
+ + + + + + + +
NameRoleStatus
Jane DoeAdminActive
John SmithEditorPending
Alex KimViewerInvited
+
+
+
Striped rows — .table.striped darkens every other row
+ + + + + + + +
NameRoleStatus
Jane DoeAdminActive
John SmithEditorPending
Alex KimViewerInvited
+
+
+
Selectable rows — .table.select shows a pointer cursor per-row
+ + + + + + + +
NameRoleStatus
Jane DoeAdminActive
John SmithEditorPending
Alex KimViewerInvited
+
+
+ +

Utilities

+
@@ -399,7 +618,7 @@
Center — .center
-
+
.center
@@ -408,7 +627,7 @@
Clamp — .clamp
-
+
.clamp — restricted to --theme-content-width
@@ -417,7 +636,7 @@
Anchor — .anchor-c .anchor-e .anchor-s
-
+
.anchor-s
.anchor-c
.anchor-e
@@ -433,7 +652,7 @@
.animate-[color|none|opacity|pos|trans]

Controlled by --theme-animation. Each variant limits which CSS properties transition.

-
+
.animate
@@ -470,7 +689,7 @@
Width — .b[b|e|s|t]-[bp]-[0–5]
-
+
.b-0
.b-1
.b-2
@@ -478,7 +697,7 @@
.b-4
.b-5
-
+
.bt-3
.bb-3
.bs-3
@@ -489,7 +708,7 @@
Radius — .br-[0–5|circle|pill] + corners .brt- .brb- .brs- .bre-
-
+
.br-0
.br-1
.br-2
@@ -499,7 +718,7 @@
.br-pill
.br-circle
-
+
.brt-3
.brb-3
.brs-3
@@ -514,7 +733,7 @@
Style — .b-[dash|dot|double|none|solid]
-
+
.b-dash
.b-dot
.b-double
@@ -523,28 +742,28 @@
- +
-
Color — .b-[color]
-
-
.b-primary
-
.b-accent
-
.b-info
-
.b-success
-
.b-warn
-
.b-danger
-
.b-muted
-
.b-text
-
.b-surface
-
.b-border
-
.b-background
-
.b-background-contrast
-
.b-primary-contrast
-
.b-accent-contrast
-
.b-lighter
-
.b-darker
-
.b-black
-
.b-white
+
Border — .b-[color]
+
+
+
.b-primary
+
.b-primary-contrast
+
.b-accent
+
.b-accent-contrast
+
.b-info
+
.b-success
+
.b-warn
+
.b-danger
+
.b-muted
+
.b-surface
+
.b-background
+
.b-background-contrast
+
.b-border
+
.b-text
+
.b-black
+
.b-white
+
@@ -558,87 +777,196 @@
Background — .bg-[color]
-
-
.bg-primary
-
.bg-primary-contrast
-
.bg-accent
-
.bg-accent-contrast
-
.bg-info
-
.bg-success
-
.bg-warn
-
.bg-danger
-
.bg-surface
-
.bg-background
-
.bg-background-contrast
-
.bg-muted
-
.bg-text
-
.bg-border
-
.bg-lighter
-
.bg-darker
-
.bg-black
-
.bg-white
-
.bg-transparent
-
.bg-container
-
- -
Lighter / Darker variants — .bg-[color]-lighter .bg-[color]-darker
-
-
.bg-primary-darker
-
.bg-primary
-
.bg-primary-lighter
-
.bg-success-darker
-
.bg-success
-
.bg-success-lighter
-
.bg-danger-darker
-
.bg-danger
-
.bg-danger-lighter
-
.bg-warn-darker
-
.bg-warn
-
.bg-warn-lighter
-
.bg-accent-darker
-
.bg-accent
-
.bg-accent-lighter
-
.bg-info-darker
-
.bg-info
-
.bg-info-lighter
-
.bg-muted-darker
-
.bg-muted
-
.bg-muted-lighter
-
.bg-surface-darker
-
.bg-surface
-
.bg-surface-lighter
+
+
+ primary +
.bg-primary-darker
+
.bg-primary
+
.bg-primary-lighter
+
.bg-primary-contrast
+
+
+ accent +
.bg-accent-darker
+
.bg-accent
+
.bg-accent-lighter
+
.bg-accent-contrast
+
+
+ info +
.bg-info-darker
+
.bg-info
+
.bg-info-lighter
+
+
+ success +
.bg-success-darker
+
.bg-success
+
.bg-success-lighter
+
+
+ warn +
.bg-warn-darker
+
.bg-warn
+
.bg-warn-lighter
+
+
+ danger +
.bg-danger-darker
+
.bg-danger
+
.bg-danger-lighter
+
+
+ muted +
.bg-muted-darker
+
.bg-muted
+
.bg-muted-lighter
+
+
+ surface +
.bg-surface-darker
+
.bg-surface
+
.bg-surface-lighter
+
+
+ background +
.bg-background-darker
+
.bg-background
+
.bg-background-lighter
+
.bg-background-contrast
+
+
+ border +
.bg-border-darker
+
.bg-border
+
.bg-border-lighter
+
+
+ text +
.bg-text-darker
+
.bg-text
+
.bg-text-lighter
+
+
+ black +
.bg-black-darker
+
.bg-black
+
.bg-black-lighter
+
+
+ white +
.bg-white-darker
+
.bg-white
+
.bg-white-lighter
+
+
+ Transparency +
.bg-shade
+
.bg-shade-alt
+
.bg-lighter
+
.bg-darker
+
.bg-transparent
+
Foreground — .fg-[color]
-
- .fg-primary - .fg-primary-contrast - .fg-accent - .fg-accent-contrast - .fg-info - .fg-success - .fg-warn - .fg-danger - .fg-muted - .fg-text - .fg-surface - .fg-border - .fg-background - .fg-background-contrast - .fg-lighter - .fg-darker - .fg-black - .fg-white +
+
+ primary + .fg-primary-darker + .fg-primary + .fg-primary-lighter + .fg-primary-contrast +
+
+ accent + .fg-accent-darker + .fg-accent + .fg-accent-lighter + .fg-accent-contrast +
+
+ info + .fg-info-darker + .fg-info + .fg-info-lighter +
+
+ success + .fg-success-darker + .fg-success + .fg-success-lighter +
+
+ warn + .fg-warn-darker + .fg-warn + .fg-warn-lighter +
+
+ danger + .fg-danger-darker + .fg-danger + .fg-danger-lighter +
+
+ muted + .fg-muted-darker + .fg-muted + .fg-muted-lighter +
+
+ surface + .fg-surface-darker + .fg-surface + .fg-surface-lighter +
+
+ background + .fg-background-darker + .fg-background + .fg-background-lighter + .fg-background-contrast +
+
+ border + .fg-border-darker + .fg-border + .fg-border-lighter +
+
+ text + .fg-text-darker + .fg-text + .fg-text-lighter +
+
+ black + .fg-black-darker + .fg-black + .fg-black-lighter +
+
+ white + .fg-white-darker + .fg-white + .fg-white-lighter +
+
+ Transparency + .fg-lighter + .fg-darker +
Alignment — .align-x[-start|-end] .align-y[-start|-end]
-
+
.align-x-start
.align-x (center)
.align-x-end
@@ -653,13 +981,83 @@
+ +
+
+

Glow

utility +
+
.bg-glow-[primary|accent][-sm|-lg]
+

A soft radial gradient anchored to the top-left corner — it's a plain background-image, so it layers behind any content. This page's own hero header uses .bg-glow-primary.

+
+
+ .bg-glow-primary-sm +
+
+ .bg-glow-primary +
+
+ .bg-glow-primary-lg +
+ +
+ .bg-glow-accent-sm +
+
+ .bg-glow-accent +
+
+ .bg-glow-accent-lg +
+ +
+ .bg-glow-info-sm +
+
+ .bg-glow-info +
+
+ .bg-glow-info-lg +
+ +
+ .bg-glow-success-sm +
+
+ .bg-glow-success +
+
+ .bg-glow-success-lg +
+ +
+ .bg-glow-warn-sm +
+
+ .bg-glow-warn +
+
+ .bg-glow-warn-lg +
+ +
+ .bg-glow-danger-sm +
+
+ .bg-glow-danger +
+
+ .bg-glow-danger-lg +
+
+
+

Cursors

utility
.curs-[alias|auto|cell|col-resize|copy|crosshair|default|grab|grabbing|help|move|not-allowed|pointer|progress|row-resize|text|none|wait|vertical-text|zoom-in|zoom-out]
-
+
.curs-alias
.curs-auto
.curs-cell
@@ -690,7 +1088,7 @@

Display

utility
.d-[bp]-[block|inline|inline-block|flex|inline-flex|grid|inline-grid|table|table-row|table-cell|none]
-
+
.d-block .d-inline .d-inline-block @@ -705,7 +1103,7 @@ .d-md-none .d-lg-flex
-
+
.d-inline-block
.d-inline
@@ -723,7 +1121,7 @@
Direction — .flex-[bp]-[c|cr|r|rr] .flex-inline-[bp]-[c|cr|r|rr]
-
+
.flex-c
1
@@ -753,7 +1151,7 @@
Justify — .justify-[bp]-[start|center|end|between|around|evenly]
-
+
.justify-start
A
B
C
@@ -782,8 +1180,40 @@
-
Align Items / Self / Content
-
+
Align Content — .align-content-[bp]-[start|end|center|baseline|stretch]
+
+
+
A
B
+ .align-content-start +
+
+
A
B
+ .align-content-center +
+
+
A
B
+ .align-content-end +
+
+
A
B
+ .align-content-stretch +
+
+
+ +
+
Align Items — .align-items-[bp]-[start|end|center|baseline|stretch]
+
+
.align-items-start
+
.align-items-center
+
.align-items-end
+
.align-items-stretch
+
+
+ +
+
Align Self — .align-self-[bp]-[start|end|center|baseline|stretch]
+
.align-self-[bp]-start
.align-self-[bp]-center
.align-self-[bp]-end
@@ -793,7 +1223,7 @@
Grow / Shrink / Fill
-
+
.flex-[bp]-grow-1
.flex-[bp]-grow-2
@@ -814,7 +1244,7 @@
Wrap
-
+
.flex-[bp]-nowrap
@@ -836,7 +1266,7 @@
Order — .order-[bp]-[0–5]
-
+
#1 / .order-5
#2 / .order-1
#3 / .order-3
@@ -846,7 +1276,7 @@
Gap — .gap-[bp]-[0–5]
-
+
.gap-0
@@ -893,7 +1323,7 @@

Float

utility
.float-[bp]-[start|end|none]
-
+
.float-end
.float-start
.float-none — stays in flow
@@ -908,7 +1338,7 @@
Size — .fs-[1–7]
-
+
.fs-1 — 1rem .fs-2 — 1.25rem .fs-3 — 1.5rem @@ -921,7 +1351,7 @@
Weight
-
+
.fs-lighter .fs-normal .fs-bold @@ -931,7 +1361,7 @@
Style & Decoration
-
+
.fs-italic .fs-norm (normal) .fs-underline @@ -943,7 +1373,7 @@
Transform
-
+
.fs-lowercase EXAMPLE .fs-uppercase example .fs-capitalize example text @@ -952,7 +1382,7 @@
Wrapping
-
+
.fs-truncate
Lorem ipsum dolor sit amet consetetur sadipscing elitr sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat
@@ -979,7 +1409,7 @@

Hover

utility
.hover   .curs-hover
-
+
.hover — hover me (brightness)
.curs-hover — pointer + brightness
@@ -991,7 +1421,7 @@

Object Fit

utility
.fit-[bp]-[contain|cover|fill|scale|none]
-
+
wave @@ -1031,7 +1461,7 @@

Opacity

utility
.o-[0|10|20|25|30|33|34|40|50|60|66|70|75|80|90|100]
-
+
0
10
20
@@ -1057,7 +1487,7 @@

Overflow

utility
.overflow-[x|y]-[auto|hidden|scroll|visible]
-
+
.overflow-auto
@@ -1099,7 +1529,7 @@
Type — .pos-[abs|fix|rel|static|stick]
-
+
.pos-abs (absolute) .pos-fix (fixed) .pos-rel (relative) @@ -1110,7 +1540,7 @@
Coordinates — .[top|bottom|start|end]-[0–100]
-
+
.top-0 .start-0
.top-0 .end-0
.center
@@ -1121,7 +1551,7 @@
Anchor — .anchor-[c|e|s]
-
+
.anchor-s
.anchor-c
.anchor-e
@@ -1137,7 +1567,7 @@
Elevation — .shadow .shadow-[1–5] .shadow-none
-
+
.shadow-1 @@ -1172,7 +1602,7 @@
Directional — .shadow-[top|bottom|start|end]
-
+
.shadow-top
.shadow-bottom
.shadow-start
@@ -1182,7 +1612,7 @@
Colored — .shadow-[primary|accent|info|success|warn|danger]
-
+
.shadow-primary @@ -1212,7 +1642,7 @@
Inset — .shadow-inset-[1|2]
-
+
.shadow-inset-1
.shadow-inset-2
@@ -1220,7 +1650,7 @@
Drop (filter-based, respects SVG/PNG shape) — .shadow-drop-sm .shadow-drop .shadow-drop-lg
-
+

Unlike box-shadow, filter drop-shadow respects transparency

@@ -1247,12 +1677,12 @@
-
+

User Select

utility
.select-[all|auto|none|text]
-
+
.select-all — click to select all
.select-auto — default behavior
.select-none — cannot select this
@@ -1267,13 +1697,13 @@
.w-[0|10|20|25|30|33|34|40|50|60|66|70|75|80|90|100|auto]
.h-[0|10|20|25|30|33|34|40|50|60|66|70|75|80|90|100|auto]
-
+
.w-25
.w-50
.w-75
.w-100
-
+
.h-25
.h-50
.h-75
@@ -1288,7 +1718,7 @@
Margin: .m[b|e|s|t|x|y]-[bp]-[0–5|auto]
Padding: .p[b|e|s|t|x|y]-[bp]-[0–5]
-
+
.mx-auto
@@ -1311,12 +1741,12 @@
-
.m-0 .p-0
-
.m-1 .p-1
-
.m-2 .p-2
-
.m-3 .p-3
-
.m-4 .p-4
-
.m-5 .p-5
+
.m-0 .p-0
+
.m-1 .p-1
+
.m-2 .p-2
+
.m-3 .p-3
+
.m-4 .p-4
+
.m-5 .p-5
@@ -1327,7 +1757,7 @@

Visibility

utility
.[hidden|visible]-[bp]
-
+

.hidden preserves layout space. Use .d-none to remove from flow entirely.

@@ -1359,26 +1789,27 @@
diff --git a/package.json b/package.json index ad7d3c9..b919364 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,12 @@ { "name": "@ztimson/css-utils", - "version": "2.1.0", + "version": "2.2.0", "description": "CSS Utility Classes", "readme": "README.md", "scripts": { - "build": "npx sass src/main.scss > dist/css-utils.css && npx sass src/main.scss --style compressed > dist/css-utils.min.css" + "build": "npm run build:utils && npm run build:comp", + "build:comp": "npx sass src/components.scss > dist/components.css && npx sass src/components.scss --style compressed > dist/components.min.css", + "build:utils": "npx sass src/utils.scss > dist/css-utils.css && npx sass src/utils.scss --style compressed > dist/css-utils.min.css" }, "repository": { "type": "git", diff --git a/src/components.scss b/src/components.scss new file mode 100644 index 0000000..61a99c0 --- /dev/null +++ b/src/components.scss @@ -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; + } +} diff --git a/src/main.scss b/src/utils.scss similarity index 87% rename from src/main.scss rename to src/utils.scss index d8bd22a..46d7e03 100644 --- a/src/main.scss +++ b/src/utils.scss @@ -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