css-is-awesome 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +343 -0
- package/CHANGELOG.md +1036 -0
- package/CLAUDE.md +9 -0
- package/CONTRACT.md +580 -0
- package/GEMINI.md +9 -0
- package/LICENSE +21 -0
- package/LICENSE-third-party +88 -0
- package/MIGRATION.md +312 -0
- package/README.md +380 -0
- package/ROADMAP.md +714 -0
- package/THEMING.md +290 -0
- package/THREE-TIERS.md +158 -0
- package/VERSIONING.md +200 -0
- package/_index.scss +34 -0
- package/api.scss +16 -0
- package/bin/README.md +57 -0
- package/bin/cia.cjs +114 -0
- package/bin/migrate-bootstrap.cjs +427 -0
- package/bin/migrate-tailwind.cjs +832 -0
- package/css-is-awesome.instructions.md +533 -0
- package/dist/css-is-awesome.core.css +598 -0
- package/dist/css-is-awesome.core.min.css +1 -0
- package/dist/css-is-awesome.css +3733 -0
- package/dist/css-is-awesome.min.css +1 -0
- package/dist/css-is-awesome.utilities.css +2445 -0
- package/dist/css-is-awesome.utilities.min.css +1 -0
- package/dist/tokens.css +508 -0
- package/dist/tokens.d.ts +266 -0
- package/dist/tokens.min.css +1 -0
- package/figma-tokens/README.md +57 -0
- package/figma-tokens/primitives-brand.scss +121 -0
- package/figma-tokens/semantic-dark.scss +61 -0
- package/figma-tokens/semantic-light.scss +61 -0
- package/figma-tokens/tokens.json +144 -0
- package/llm.txt +183 -0
- package/mcp/server.cjs +1495 -0
- package/package.json +199 -0
- package/public/icons/README.md +278 -0
- package/public/icons/arrow-right.svg +4 -0
- package/public/icons/check.svg +4 -0
- package/public/icons/chevron-down.svg +4 -0
- package/public/icons/close.svg +4 -0
- package/public/icons/core/arrow-down.svg +13 -0
- package/public/icons/core/arrow-left.svg +13 -0
- package/public/icons/core/arrow-right.svg +13 -0
- package/public/icons/core/arrow-up.svg +13 -0
- package/public/icons/core/bell.svg +13 -0
- package/public/icons/core/calendar.svg +15 -0
- package/public/icons/core/check.svg +12 -0
- package/public/icons/core/chevron-down.svg +12 -0
- package/public/icons/core/chevron-left.svg +12 -0
- package/public/icons/core/chevron-right.svg +12 -0
- package/public/icons/core/chevron-up.svg +12 -0
- package/public/icons/core/clock.svg +13 -0
- package/public/icons/core/close.svg +13 -0
- package/public/icons/core/copy.svg +13 -0
- package/public/icons/core/download.svg +14 -0
- package/public/icons/core/edit.svg +13 -0
- package/public/icons/core/error.svg +14 -0
- package/public/icons/core/external-link.svg +14 -0
- package/public/icons/core/eye-off.svg +15 -0
- package/public/icons/core/eye.svg +13 -0
- package/public/icons/core/filter.svg +12 -0
- package/public/icons/core/heart.svg +12 -0
- package/public/icons/core/help.svg +14 -0
- package/public/icons/core/home.svg +13 -0
- package/public/icons/core/info.svg +14 -0
- package/public/icons/core/loading.svg +12 -0
- package/public/icons/core/lock.svg +13 -0
- package/public/icons/core/mail.svg +13 -0
- package/public/icons/core/menu.svg +14 -0
- package/public/icons/core/message.svg +12 -0
- package/public/icons/core/minus.svg +12 -0
- package/public/icons/core/more-horizontal.svg +14 -0
- package/public/icons/core/pause.svg +13 -0
- package/public/icons/core/play.svg +12 -0
- package/public/icons/core/plus.svg +13 -0
- package/public/icons/core/refresh.svg +15 -0
- package/public/icons/core/save.svg +14 -0
- package/public/icons/core/search.svg +13 -0
- package/public/icons/core/settings.svg +13 -0
- package/public/icons/core/share.svg +14 -0
- package/public/icons/core/sort.svg +15 -0
- package/public/icons/core/star.svg +12 -0
- package/public/icons/core/success.svg +13 -0
- package/public/icons/core/trash.svg +16 -0
- package/public/icons/core/unlock.svg +13 -0
- package/public/icons/core/upload.svg +14 -0
- package/public/icons/core/user.svg +13 -0
- package/public/icons/core/users.svg +15 -0
- package/public/icons/core/warning.svg +14 -0
- package/public/icons/download.svg +4 -0
- package/public/icons/edit.svg +4 -0
- package/public/icons/menu.svg +4 -0
- package/public/icons/search.svg +4 -0
- package/public/theme.css +4179 -0
- package/public/themes/README.md +102 -0
- package/public/themes/boilerplate/theme.css +154 -0
- package/public/themes/boilerplate-dark/theme.css +171 -0
- package/public/themes/boilerplate-light/theme.css +171 -0
- package/public/themes/cupertino/theme.css +165 -0
- package/public/themes/cupertino-dark/theme.css +200 -0
- package/public/themes/cupertino-light/theme.css +227 -0
- package/public/themes/glass/theme.css +190 -0
- package/public/themes/glass-dark/theme.css +201 -0
- package/public/themes/glass-light/theme.css +229 -0
- package/public/themes/graphite/theme.css +155 -0
- package/public/themes/graphite-dark/theme.css +223 -0
- package/public/themes/graphite-light/theme.css +191 -0
- package/public/themes/press/theme.css +189 -0
- package/public/themes/press-dark/theme.css +199 -0
- package/public/themes/press-light/theme.css +235 -0
- package/public/themes/prism/theme.css +159 -0
- package/public/themes/prism-dark/theme.css +185 -0
- package/public/themes/prism-light/theme.css +188 -0
- package/public/themes/sketchbook/theme.css +152 -0
- package/public/themes/sketchbook-dark/theme.css +194 -0
- package/public/themes/sketchbook-light/theme.css +188 -0
- package/public/themes/terminal/theme.css +159 -0
- package/public/themes/terminal-dark/theme.css +229 -0
- package/public/themes/terminal-light/theme.css +152 -0
- package/scripts/README.md +70 -0
- package/scripts/icon-contract.json +67 -0
- package/scripts/icon-validator.js +205 -0
- package/scripts/prepare-dist.mjs +65 -0
- package/scripts/theme-a11y.js +415 -0
- package/scripts/theme-contract.json +171 -0
- package/scripts/theme-validator.js +674 -0
- package/scss/README.md +55 -0
- package/scss/_animations-utilities.scss +107 -0
- package/scss/_animations.scss +248 -0
- package/scss/_app-styles.scss +15 -0
- package/scss/_generator.scss +174 -0
- package/scss/_icons.scss +475 -0
- package/scss/_index.scss +17 -0
- package/scss/_layout.scss +597 -0
- package/scss/_mixins.scss +1036 -0
- package/scss/_system.scss +291 -0
- package/scss/_utilities.scss +377 -0
- package/scss/api.scss +61 -0
- package/scss/components/_accordion.scss +72 -0
- package/scss/components/_buttons.scss +171 -0
- package/scss/components/_copy-button.scss +69 -0
- package/scss/components/_data.scss +190 -0
- package/scss/components/_feedback.scss +197 -0
- package/scss/components/_forms.scss +272 -0
- package/scss/components/_index.scss +17 -0
- package/scss/components/_navigation.scss +186 -0
- package/scss/components/_overlay.scss +259 -0
- package/scss/components/_stepper.scss +159 -0
- package/scss/components/_tabs.scss +72 -0
- package/scss/core.scss +54 -0
- package/scss/examples/_theming.scss +107 -0
- package/scss/examples/_usage.scss +317 -0
- package/scss/main.scss +64 -0
- package/scss/recipes/README.md +110 -0
- package/scss/recipes/_bare-tags.scss +232 -0
- package/scss/recipes/_recipe-template.md +119 -0
- package/scss/recipes/combobox.md +509 -0
- package/scss/recipes/dialog.md +265 -0
- package/scss/recipes/print-to-pdf.md +263 -0
- package/scss/theme/README.md +67 -0
- package/scss/theme/_brand.scss +18 -0
- package/scss/theme/_colors-dark.scss +68 -0
- package/scss/theme/_colors-light.scss +70 -0
- package/scss/theme/_components.scss +94 -0
- package/scss/theme/_icons.scss +104 -0
- package/scss/theme/_index.scss +62 -0
- package/scss/theme/_shadows.scss +65 -0
- package/scss/theme/_shape.scss +13 -0
- package/scss/theme/_typography.scss +9 -0
- package/scss/theme/registry.json +447 -0
- package/scss/themes/boilerplate-dark.scss +212 -0
- package/scss/themes/boilerplate-light.scss +209 -0
- package/scss/themes/boilerplate.scss +181 -0
- package/scss/themes/cupertino-dark.scss +238 -0
- package/scss/themes/cupertino-light.scss +266 -0
- package/scss/themes/cupertino.scss +194 -0
- package/scss/themes/glass-dark.scss +238 -0
- package/scss/themes/glass-light.scss +266 -0
- package/scss/themes/glass.scss +220 -0
- package/scss/themes/graphite-dark.scss +260 -0
- package/scss/themes/graphite-light.scss +228 -0
- package/scss/themes/graphite.scss +182 -0
- package/scss/themes/press-dark.scss +237 -0
- package/scss/themes/press-light.scss +273 -0
- package/scss/themes/press.scss +241 -0
- package/scss/themes/prism-dark.scss +217 -0
- package/scss/themes/prism-light.scss +220 -0
- package/scss/themes/prism.scss +190 -0
- package/scss/themes/sketchbook-dark.scss +231 -0
- package/scss/themes/sketchbook-light.scss +231 -0
- package/scss/themes/sketchbook.scss +178 -0
- package/scss/themes/terminal-dark.scss +267 -0
- package/scss/themes/terminal-light.scss +175 -0
- package/scss/themes/terminal.scss +185 -0
- package/scss/tokens.scss +23 -0
- package/scss/utilities-only.scss +4 -0
|
@@ -0,0 +1,1036 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// MIXINS - Core Utilities
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Icons are in _icons.scss (separate file)
|
|
5
|
+
// Every mixin has parameters. Override anything.
|
|
6
|
+
// ============================================================================
|
|
7
|
+
@use 'sass:map';
|
|
8
|
+
@use 'sass:list';
|
|
9
|
+
@use 'sass:math';
|
|
10
|
+
@use 'sass:meta';
|
|
11
|
+
@use 'sass:string';
|
|
12
|
+
@use './theme' as t;
|
|
13
|
+
@use './system' as s;
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// HELPER: Component token runtime override (Feature 1.3)
|
|
17
|
+
// ----------------------------------------------------------------------------
|
|
18
|
+
// `comp(key, fallback)` returns `var(--<key>, <fallback>)`. The fallback is a
|
|
19
|
+
// fully-resolved CSS value — typically the result of another mixin helper
|
|
20
|
+
// like `radius(md)` or `space(4)`, or a literal like `1px`. Themes can then
|
|
21
|
+
// override `--<key>` on their :root block without rebuilding the library.
|
|
22
|
+
//
|
|
23
|
+
// Example:
|
|
24
|
+
// border-radius: comp(btn-radius, radius(md));
|
|
25
|
+
// // → border-radius: var(--btn-radius, var(--radius-md, 0.25rem));
|
|
26
|
+
//
|
|
27
|
+
// The legacy private `_comp()` helper is retained so existing callers that
|
|
28
|
+
// looked up a map key (to forward into space()/radius()/etc.) still work.
|
|
29
|
+
// New call sites should prefer `comp()` at the CSS-property level.
|
|
30
|
+
// ============================================================================
|
|
31
|
+
@function _comp($key, $fallback) {
|
|
32
|
+
$val: map.get(t.$theme-components, $key);
|
|
33
|
+
@if $val {
|
|
34
|
+
@return $val;
|
|
35
|
+
}
|
|
36
|
+
@return $fallback;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@function comp($key, $fallback) {
|
|
40
|
+
@return var(--#{$key}, #{$fallback});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ============================================================================
|
|
44
|
+
// COLOR FUNCTIONS
|
|
45
|
+
// ============================================================================
|
|
46
|
+
|
|
47
|
+
@function color($name) {
|
|
48
|
+
$fallback: map.get(t.$theme-light, $name);
|
|
49
|
+
@if not $fallback {
|
|
50
|
+
$fallback: map.get(t.$theme-brand, $name);
|
|
51
|
+
}
|
|
52
|
+
@if $fallback {
|
|
53
|
+
@return var(--#{$name}, #{$fallback});
|
|
54
|
+
}
|
|
55
|
+
@return var(--#{$name}, currentColor);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@function brand($name) {
|
|
59
|
+
$fallback: map.get(t.$theme-brand, $name);
|
|
60
|
+
@return var(--brand-#{$name}, #{$fallback or currentColor});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@function color-static($name) {
|
|
64
|
+
$value: map.get(t.$theme-light, $name);
|
|
65
|
+
@if not $value {
|
|
66
|
+
$value: map.get(t.$theme-brand, $name);
|
|
67
|
+
}
|
|
68
|
+
@return $value or currentColor;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ============================================================================
|
|
72
|
+
// SPACING FUNCTIONS (2026 System)
|
|
73
|
+
// ============================================================================
|
|
74
|
+
// UPDATED: 2026-04 - Numbered scale (1..9) is source of truth; t-shirt keys
|
|
75
|
+
// (xs/sm/md/lg/xl/2xl/3xl/4xl) layer on top as aliases.
|
|
76
|
+
// All values in rem for accessibility.
|
|
77
|
+
//
|
|
78
|
+
// Usage:
|
|
79
|
+
// padding: space(4); // Returns var(--space-4, 1rem)
|
|
80
|
+
// padding: space(md); // Returns var(--space-md, 1rem) (alias)
|
|
81
|
+
// Both styles continue to work, but they are no longer independent: themes
|
|
82
|
+
// declare only the NUMBERED set, and the library emits each t-shirt name as a
|
|
83
|
+
// reference to it (--space-md: var(--space-4)). Previously both sets were
|
|
84
|
+
// emitted as separate literals holding equal values, so a theme could set
|
|
85
|
+
// --space-md while every component read var(--space-4) and nothing moved.
|
|
86
|
+
// Theme the numbered step; the alias follows.
|
|
87
|
+
// ============================================================================
|
|
88
|
+
|
|
89
|
+
@function space($key) {
|
|
90
|
+
$fallback: map.get(t.$space, $key);
|
|
91
|
+
// Not a scale key — a raw length like `12px` or `2rem`. Pass it through
|
|
92
|
+
// instead of inventing `var(--space-12px, 1rem)`, which silently resolved to
|
|
93
|
+
// the fallback and threw the caller's value away. This is what lets the
|
|
94
|
+
// layout mixins accept either a scale step or a literal.
|
|
95
|
+
@if not $fallback {
|
|
96
|
+
@return $key;
|
|
97
|
+
}
|
|
98
|
+
@return var(--space-#{$key}, #{$fallback});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@function space-raw($key) {
|
|
102
|
+
@return map.get(t.$space, $key) or 1rem;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Semantic spacing aliases for better readability
|
|
106
|
+
@function gap($type: normal) {
|
|
107
|
+
$fallback: map.get(t.$gap, $type);
|
|
108
|
+
@return var(--gap-#{$type}, #{$fallback or 1rem});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@function padding($type: normal) {
|
|
112
|
+
$fallback: map.get(t.$padding, $type);
|
|
113
|
+
@return var(--padding-#{$type}, #{$fallback or 1rem});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ============================================================================
|
|
117
|
+
// GEOMETRIC GRID — explicit sizes on cia's 4px coordinate system
|
|
118
|
+
// ============================================================================
|
|
119
|
+
// Pure functions for sizes that aren't a design choice — they're geometric
|
|
120
|
+
// truth shared across Figma, the design system, and the codebase. NOT
|
|
121
|
+
// themeable; consumers don't tune the 4px grid. Distinct from space() which
|
|
122
|
+
// IS themeable (consumer can re-tune the spacing scale per theme).
|
|
123
|
+
//
|
|
124
|
+
// Use grid() for icon widths, control heights, fixed dimensions that must
|
|
125
|
+
// align with the design grid. Use px() for the rare off-grid pixel value
|
|
126
|
+
// that doesn't fit any system (a 17px hero margin, a 33px badge offset).
|
|
127
|
+
// Both return rem (for a11y zoom + browser-default-font-size respect).
|
|
128
|
+
// Both inline at call site — no custom properties emitted.
|
|
129
|
+
//
|
|
130
|
+
// Decision tree:
|
|
131
|
+
// color/type/radius/shadow/motion → m.color() / m.font-size() / m.radius() / m.shadow() / m.duration() (themed tokens)
|
|
132
|
+
// margin / padding / gap → m.space(n) (themeable spacing scale)
|
|
133
|
+
// explicit geometric size on grid → m.grid(n) (this section — 4px grid, not themed)
|
|
134
|
+
// off-grid pixel value (rare) → m.px(value) (this section — raw rem conversion)
|
|
135
|
+
// component → m.btn() / m.modal() / etc. + parameter-power overrides
|
|
136
|
+
//
|
|
137
|
+
// MCP / Figma → Code: AI agents resolve a design's px value via
|
|
138
|
+
// `step = math.round(px / 4)` and emit `m.grid(step)`. See the
|
|
139
|
+
// `resolve_size` MCP tool for the canonical mapping.
|
|
140
|
+
// ============================================================================
|
|
141
|
+
|
|
142
|
+
// Geometric value on the 4px grid. n is the step count (n × 0.25rem).
|
|
143
|
+
// m.grid(4) → 1rem (16px)
|
|
144
|
+
// m.grid(6) → 1.5rem (24px)
|
|
145
|
+
// m.grid(10) → 2.5rem (40px)
|
|
146
|
+
// m.grid(20) → 5rem (80px)
|
|
147
|
+
@function grid($n, $base: 0.25rem) {
|
|
148
|
+
@return $n * $base;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Raw pixel value converted to rem (off-grid, intentional). For values
|
|
152
|
+
// that genuinely don't fit any cia scale. Prefer m.grid() / m.space() /
|
|
153
|
+
// the typed token functions when they fit — m.px() is the escape hatch,
|
|
154
|
+
// not the default. Returns rem so user zoom + browser font-size scaling
|
|
155
|
+
// still work.
|
|
156
|
+
// m.px(17) → 1.0625rem
|
|
157
|
+
// m.px(33) → 2.0625rem
|
|
158
|
+
@function px($value) {
|
|
159
|
+
@return math.div($value, 16) * 1rem;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Snap a px value to the nearest cia grid step. For tooling
|
|
163
|
+
// (Figma → code, MCP, codegen) — pure math, no rendering output. AI
|
|
164
|
+
// agents call this to map a design's px value to the cia grid step,
|
|
165
|
+
// then emit m.grid(step) in generated code.
|
|
166
|
+
// m.grid-from-px(24) → 6
|
|
167
|
+
// m.grid-from-px(17) → 4 (rounds to nearest grid step)
|
|
168
|
+
@function grid-from-px($px, $base-px: 4) {
|
|
169
|
+
@return math.round(math.div($px, $base-px));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ============================================================================
|
|
173
|
+
// TYPOGRAPHY
|
|
174
|
+
// ============================================================================
|
|
175
|
+
|
|
176
|
+
@function font-family($type: primary) {
|
|
177
|
+
$stack: map.get(t.$theme-fonts, $type);
|
|
178
|
+
@if $stack {
|
|
179
|
+
$fallback: list.join($stack, (), comma);
|
|
180
|
+
@return var(--font-#{$type}, #{$fallback});
|
|
181
|
+
}
|
|
182
|
+
@return var(--font-#{$type}, sans-serif);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/// Letter-spacing from the tracking scale: tighter | tight | normal | wide |
|
|
186
|
+
/// wider | widest. Emits a custom property with the scale value as fallback,
|
|
187
|
+
/// so a theme can retune tracking without a rebuild — the same contract as
|
|
188
|
+
/// `space()` and `font-size()`. A raw value (`0.03em`) passes straight through.
|
|
189
|
+
@function letter-spacing($key) {
|
|
190
|
+
$fallback: map.get(s.$letter-spacings, $key);
|
|
191
|
+
@if not $fallback {
|
|
192
|
+
@return $key;
|
|
193
|
+
}
|
|
194
|
+
@return var(--letter-spacing-#{$key}, #{$fallback});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
@function font-size($size) {
|
|
198
|
+
$fallback: map.get(s.$font-sizes, $size) or 1rem;
|
|
199
|
+
@return var(--font-size-#{$size}, #{$fallback});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
@function font-size-raw($size) {
|
|
203
|
+
@return map.get(s.$font-sizes, $size) or 1rem;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
@function line-height($size) {
|
|
207
|
+
$fallback: map.get(s.$line-heights, $size) or 1.5;
|
|
208
|
+
@return var(--line-height-#{$size}, #{$fallback});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
@function line-height-raw($size) {
|
|
212
|
+
@return map.get(s.$line-heights, $size) or 1.5;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
@function font-weight($weight) {
|
|
216
|
+
$fallback: map.get(s.$font-weights, $weight) or 400;
|
|
217
|
+
@return var(--font-weight-#{$weight}, #{$fallback});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
@function font-weight-raw($weight) {
|
|
221
|
+
@return map.get(s.$font-weights, $weight) or 400;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Font type map
|
|
225
|
+
$_font-types: (
|
|
226
|
+
reg: (weight: normal, style: normal),
|
|
227
|
+
regular: (weight: normal, style: normal),
|
|
228
|
+
italic: (weight: normal, style: italic),
|
|
229
|
+
light: (weight: light, style: normal),
|
|
230
|
+
light-it: (weight: light, style: italic),
|
|
231
|
+
medium: (weight: medium, style: normal),
|
|
232
|
+
medium-it: (weight: medium, style: italic),
|
|
233
|
+
semibold: (weight: semibold, style: normal),
|
|
234
|
+
semibold-it: (weight: semibold, style: italic),
|
|
235
|
+
bold: (weight: bold, style: normal),
|
|
236
|
+
bold-it: (weight: bold, style: italic),
|
|
237
|
+
black: (weight: black, style: normal),
|
|
238
|
+
black-it: (weight: black, style: italic),
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
// Font mixin - the complete text-style block in one call.
|
|
242
|
+
// $family handling:
|
|
243
|
+
// - Bare slug (e.g. `meme`, `pacifico`) → emits font-family: var(--font-<slug>).
|
|
244
|
+
// Consumer declares the variable in their global stylesheet (globals.css).
|
|
245
|
+
// - Sass list / pre-formed string with commas or var() → passes through.
|
|
246
|
+
// $color handling: pass a contract color name (e.g. `text-primary`, `ink`) and
|
|
247
|
+
// the mixin emits `color: m.color($color)` at the end of the block.
|
|
248
|
+
@mixin font(
|
|
249
|
+
$type: reg,
|
|
250
|
+
$size: null,
|
|
251
|
+
$lh: null,
|
|
252
|
+
$ls: null,
|
|
253
|
+
$family: null,
|
|
254
|
+
$color: null
|
|
255
|
+
) {
|
|
256
|
+
$cfg: map.get($_font-types, $type) or map.get($_font-types, reg);
|
|
257
|
+
font-weight: font-weight(map.get($cfg, weight));
|
|
258
|
+
font-style: map.get($cfg, style);
|
|
259
|
+
|
|
260
|
+
@if $family {
|
|
261
|
+
$is-string: meta.type-of($family) == 'string';
|
|
262
|
+
$is-bare-name: $is-string
|
|
263
|
+
and not string.index('#{$family}', ',')
|
|
264
|
+
and not string.index('#{$family}', 'var(');
|
|
265
|
+
@if $is-bare-name {
|
|
266
|
+
// Slugs are CSS variable names — emit var(--font-<slug>) and trust
|
|
267
|
+
// the consumer to declare the variable somewhere global
|
|
268
|
+
// (typically `:root { --font-<slug>: ...; }` in globals.css).
|
|
269
|
+
font-family: var(--font-#{$family});
|
|
270
|
+
} @else {
|
|
271
|
+
font-family: $family;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Route scale keys through font-size()/line-height() so they emit
|
|
276
|
+
// `var(--font-size-N)` rather than a baked literal — otherwise a consumer
|
|
277
|
+
// overriding the token at runtime silently has no effect here, while the
|
|
278
|
+
// same override works on mixins that use the functions. Raw values
|
|
279
|
+
// (`0.8rem`, `1.4`) still pass straight through.
|
|
280
|
+
@if $size {
|
|
281
|
+
@if map.get(s.$font-sizes, $size) {
|
|
282
|
+
font-size: font-size($size);
|
|
283
|
+
} @else {
|
|
284
|
+
font-size: $size;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
@if $lh {
|
|
289
|
+
@if map.get(s.$line-heights, $lh) {
|
|
290
|
+
line-height: line-height($lh);
|
|
291
|
+
} @else {
|
|
292
|
+
line-height: $lh;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
@if $ls {
|
|
297
|
+
@if map.get(s.$letter-spacings, $ls) {
|
|
298
|
+
letter-spacing: letter-spacing($ls);
|
|
299
|
+
} @else {
|
|
300
|
+
letter-spacing: $ls;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
@if $color {
|
|
305
|
+
color: color($color);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ============================================================================
|
|
310
|
+
// FONTS — two-line model
|
|
311
|
+
// ----------------------------------------------------------------------------
|
|
312
|
+
// Two lines of work for any custom font, period:
|
|
313
|
+
//
|
|
314
|
+
// 1) Declare the CSS variable on :root in your global stylesheet:
|
|
315
|
+
// :root { --font-meme: 'Helvetica Neue', Helvetica, Arial, sans-serif; }
|
|
316
|
+
//
|
|
317
|
+
// 2) Use it in any component:
|
|
318
|
+
// @include m.font($lh: 0.95, $family: meme); // mixin form
|
|
319
|
+
// font-family: var(--font-meme); // raw form
|
|
320
|
+
//
|
|
321
|
+
// Override anywhere CSS variables work — root, theme, page, block, element.
|
|
322
|
+
//
|
|
323
|
+
// HOSTED FONTS (Google Fonts / CDN):
|
|
324
|
+
// `font-load` registers a URL and emits `@import url(...)`. Call it from a
|
|
325
|
+
// global Sass file (NOT a .module.scss — CSS Modules pure mode forbids the
|
|
326
|
+
// :root selector the auto-emit would need). The CSS variable still gets
|
|
327
|
+
// declared via the consumer's global stylesheet, same as system fonts.
|
|
328
|
+
//
|
|
329
|
+
// // src/styles/fonts.scss (a global .scss imported from layout.tsx):
|
|
330
|
+
// @include m.font-face('Pacifico', 'https://fonts.googleapis.com/css2?family=Pacifico');
|
|
331
|
+
//
|
|
332
|
+
// // src/app/globals.css:
|
|
333
|
+
// :root { --font-pacifico: 'Pacifico', cursive; }
|
|
334
|
+
//
|
|
335
|
+
// // Any component:
|
|
336
|
+
// .heading { @include m.font($family: pacifico); }
|
|
337
|
+
//
|
|
338
|
+
// Sister `font-face-local()` covers self-hosted woff2/ttf/etc. via @font-face.
|
|
339
|
+
// ============================================================================
|
|
340
|
+
$_loaded-fonts: () !default;
|
|
341
|
+
|
|
342
|
+
@mixin font-face($name, $url, $alias: null, $fallback: sans-serif) {
|
|
343
|
+
$existing: map.get($_loaded-fonts, $name);
|
|
344
|
+
@if $existing {
|
|
345
|
+
@if map.get($existing, url) != $url {
|
|
346
|
+
@error "font-face(): '#{$name}' is already registered with a different URL. Existing: #{map.get($existing, url)}. New: #{$url}. One name, one URL.";
|
|
347
|
+
}
|
|
348
|
+
// Same name + URL = no-op. @import already emitted on the first call.
|
|
349
|
+
} @else {
|
|
350
|
+
$_loaded-fonts: map.set($_loaded-fonts, $name, (url: $url, fallback: $fallback)) !global;
|
|
351
|
+
@at-root {
|
|
352
|
+
/* stylelint-disable-next-line no-invalid-position-at-import-rule, import-notation -- @at-root lifts this @import to the top of the compiled stylesheet (valid position); url() notation is required because Sass rejects the bare-string form inside @at-root. */
|
|
353
|
+
@import url(#{$url});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Optional: emit a theme-slot alias at the call site. Best called from a
|
|
358
|
+
// selector context that owns the override (e.g. inside :root in a global
|
|
359
|
+
// Sass file, or inside [data-theme="x"] for a per-theme override).
|
|
360
|
+
@if $alias {
|
|
361
|
+
--font-#{$alias}: '#{$name}', #{$fallback};
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
@mixin font-face-local(
|
|
366
|
+
$name,
|
|
367
|
+
$src,
|
|
368
|
+
$weight: 400,
|
|
369
|
+
$style: normal,
|
|
370
|
+
$display: swap,
|
|
371
|
+
$alias: null,
|
|
372
|
+
$fallback: sans-serif
|
|
373
|
+
) {
|
|
374
|
+
$existing: map.get($_loaded-fonts, $name);
|
|
375
|
+
@if $existing {
|
|
376
|
+
@if map.get($existing, src) != $src {
|
|
377
|
+
@error "font-face-local(): '#{$name}' is already registered with a different src. Existing: #{map.get($existing, src)}. New: #{$src}. One name, one src.";
|
|
378
|
+
}
|
|
379
|
+
// Same name + src = no-op.
|
|
380
|
+
} @else {
|
|
381
|
+
$_loaded-fonts: map.set($_loaded-fonts, $name, (src: $src, fallback: $fallback)) !global;
|
|
382
|
+
@at-root {
|
|
383
|
+
@font-face {
|
|
384
|
+
font-family: '#{$name}';
|
|
385
|
+
src: url(#{$src}) format('woff2');
|
|
386
|
+
font-weight: $weight;
|
|
387
|
+
font-style: $style;
|
|
388
|
+
font-display: $display;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
@if $alias {
|
|
394
|
+
--font-#{$alias}: '#{$name}', #{$fallback};
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// ============================================================================
|
|
399
|
+
// TYPE SCALE PRESETS
|
|
400
|
+
// ============================================================================
|
|
401
|
+
// Semantic typography compositions for consistent hierarchy.
|
|
402
|
+
// Usage: @include m.type(heading-1); or @include m.type(caption);
|
|
403
|
+
// ============================================================================
|
|
404
|
+
|
|
405
|
+
$_type-scale: (
|
|
406
|
+
display: (size: 8, weight: bold, lh: 2, ls: tight),
|
|
407
|
+
heading-1: (size: 7, weight: bold, lh: 2, ls: null),
|
|
408
|
+
heading-2: (size: 6, weight: semibold, lh: 2, ls: null),
|
|
409
|
+
heading-3: (size: 5, weight: semibold, lh: 3, ls: null),
|
|
410
|
+
heading-4: (size: 4, weight: medium, lh: 4, ls: null),
|
|
411
|
+
body: (size: 3, weight: normal, lh: 4, ls: null),
|
|
412
|
+
body-sm: (size: 2, weight: normal, lh: 4, ls: null),
|
|
413
|
+
caption: (size: 1, weight: normal, lh: 4, ls: null),
|
|
414
|
+
overline: (size: 1, weight: semibold, lh: 4, ls: wider, tt: uppercase),
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
@mixin type($preset) {
|
|
418
|
+
$cfg: map.get($_type-scale, $preset);
|
|
419
|
+
@if not $cfg {
|
|
420
|
+
@error "Unknown type preset: #{$preset}. Available: #{map.keys($_type-scale)}";
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
$s: map.get($cfg, size);
|
|
424
|
+
$w: map.get($cfg, weight);
|
|
425
|
+
$lh-key: map.get($cfg, lh);
|
|
426
|
+
$ls-key: map.get($cfg, ls);
|
|
427
|
+
$tt: map.get($cfg, tt);
|
|
428
|
+
|
|
429
|
+
font-size: font-size($s);
|
|
430
|
+
font-weight: font-weight($w);
|
|
431
|
+
|
|
432
|
+
@if $lh-key {
|
|
433
|
+
$lh-val: map.get(s.$line-heights, $lh-key);
|
|
434
|
+
@if $lh-val { line-height: $lh-val; }
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
@if $ls-key {
|
|
438
|
+
$ls-val: map.get(s.$letter-spacings, $ls-key);
|
|
439
|
+
@if $ls-val and $ls-val != 0 { letter-spacing: $ls-val; }
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
@if $tt { text-transform: $tt; }
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ============================================================================
|
|
446
|
+
// UI TEXT PRESETS — type() + a default color, packaged for the common cases
|
|
447
|
+
// ----------------------------------------------------------------------------
|
|
448
|
+
// When to reach for these: the lockup is a recognisable UI pattern (eyebrow
|
|
449
|
+
// kicker, figure caption, footer note) rather than raw typography. They sit
|
|
450
|
+
// on top of type() and add a default color token; pass $color to override.
|
|
451
|
+
//
|
|
452
|
+
// When NOT to reach for these: you just need a size/weight without a
|
|
453
|
+
// semantic role — use type(<preset>) directly, no color, you keep currentColor.
|
|
454
|
+
//
|
|
455
|
+
// Adding a new one: name it for what the pattern IS (eyebrow, caption-text),
|
|
456
|
+
// not what it does (small-caps-tracked-muted). Default $color to the slot
|
|
457
|
+
// the pattern uses 80% of the time. Wrap an existing type() preset whenever
|
|
458
|
+
// you can — don't redeclare font-size/weight/ls/lh inline.
|
|
459
|
+
// ============================================================================
|
|
460
|
+
|
|
461
|
+
// Eyebrow / kicker — small caps-tracked label that tags a section above
|
|
462
|
+
// its heading. One-liner replacement for the
|
|
463
|
+
// font-size + color + text-transform + letter-spacing
|
|
464
|
+
// four-property lockup that shows up wherever a chip-style label sits over
|
|
465
|
+
// a title.
|
|
466
|
+
@mixin eyebrow($color: text-secondary) {
|
|
467
|
+
@include type(overline);
|
|
468
|
+
color: color($color);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// ============================================================================
|
|
472
|
+
// BORDER & RADIUS
|
|
473
|
+
// ============================================================================
|
|
474
|
+
|
|
475
|
+
@function radius($size) {
|
|
476
|
+
$fallback: map.get(t.$theme-radius, $size);
|
|
477
|
+
@return var(--radius-#{$size}, #{$fallback or 0.25rem});
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
@function radius-raw($size) {
|
|
481
|
+
@return map.get(t.$theme-radius, $size) or 0.25rem;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
@mixin border(
|
|
485
|
+
$width: 1px,
|
|
486
|
+
$style: solid,
|
|
487
|
+
$color: border-default,
|
|
488
|
+
$sides: all
|
|
489
|
+
) {
|
|
490
|
+
@if $sides == all {
|
|
491
|
+
border: $width $style color($color);
|
|
492
|
+
} @else if meta.type-of($sides) == list {
|
|
493
|
+
@each $side in $sides {
|
|
494
|
+
border-#{$side}: $width $style color($color);
|
|
495
|
+
}
|
|
496
|
+
} @else {
|
|
497
|
+
border-#{$sides}: $width $style color($color);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// ============================================================================
|
|
502
|
+
// SHADOWS
|
|
503
|
+
// ============================================================================
|
|
504
|
+
|
|
505
|
+
@function shadow($level) {
|
|
506
|
+
$fallback: map.get(t.$theme-shadows-light, $level);
|
|
507
|
+
@return var(--shadow-#{$level}, #{$fallback or none});
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
@function shadow-raw($level) {
|
|
511
|
+
@return map.get(t.$theme-shadows-light, $level) or none;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
@mixin elevation($level: 2) {
|
|
515
|
+
box-shadow: shadow($level);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// ============================================================================
|
|
519
|
+
// Z-INDEX
|
|
520
|
+
// ============================================================================
|
|
521
|
+
|
|
522
|
+
@function z($layer) {
|
|
523
|
+
$fallback: map.get(s.$z-layers, $layer);
|
|
524
|
+
@return var(--z-#{$layer}, #{$fallback or 0});
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
@function z-raw($layer) {
|
|
528
|
+
@return map.get(s.$z-layers, $layer) or 0;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// ============================================================================
|
|
532
|
+
// BREAKPOINTS
|
|
533
|
+
// ============================================================================
|
|
534
|
+
|
|
535
|
+
@mixin media($size) {
|
|
536
|
+
$width: $size;
|
|
537
|
+
@if meta.type-of($size) != number {
|
|
538
|
+
$width: map.get(s.$breakpoints, $size);
|
|
539
|
+
}
|
|
540
|
+
@media screen and (min-width: $width) { @content; }
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
@mixin media-down($size) {
|
|
544
|
+
$width: $size;
|
|
545
|
+
@if meta.type-of($size) != number {
|
|
546
|
+
$width: map.get(s.$breakpoints, $size);
|
|
547
|
+
}
|
|
548
|
+
@media screen and (max-width: calc(#{$width} - 1px)) { @content; }
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
@mixin media-between($min, $max) {
|
|
552
|
+
$min-w: $min;
|
|
553
|
+
$max-w: $max;
|
|
554
|
+
@if meta.type-of($min) != number {
|
|
555
|
+
$min-w: map.get(s.$breakpoints, $min);
|
|
556
|
+
}
|
|
557
|
+
@if meta.type-of($max) != number {
|
|
558
|
+
$max-w: map.get(s.$breakpoints, $max);
|
|
559
|
+
}
|
|
560
|
+
@media screen and (min-width: $min-w) and (max-width: calc(#{$max-w} - 1px)) { @content; }
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Convenience aliases
|
|
564
|
+
@mixin mobile-only { @include media-down(md) { @content; } }
|
|
565
|
+
@mixin tablet { @include media(md) { @content; } }
|
|
566
|
+
@mixin tablet-only { @include media-between(md, lg) { @content; } }
|
|
567
|
+
@mixin desktop { @include media(lg) { @content; } }
|
|
568
|
+
@mixin wide { @include media(xl) { @content; } }
|
|
569
|
+
|
|
570
|
+
// ============================================================================
|
|
571
|
+
// PRINT
|
|
572
|
+
// ============================================================================
|
|
573
|
+
// Print/PDF control. Every browser already ships a layout engine and a PDF
|
|
574
|
+
// writer wired together behind Ctrl+P — the page IS the PDF source. These own the `@media print` layer: `print-base` ships
|
|
575
|
+
// sensible page-level defaults ON; the rest are per-element toggles. Nothing
|
|
576
|
+
// emits until called, and nothing here needs a button or a server — Ctrl+P
|
|
577
|
+
// (or the print dialog's "Save as PDF") drives it all.
|
|
578
|
+
// ============================================================================
|
|
579
|
+
|
|
580
|
+
// Primitive: bare print media wrapper. Co-locate inside the selector it changes
|
|
581
|
+
// so the override lives next to the rule it overrides.
|
|
582
|
+
// .subtitle { color: #ccc; @include m.print { color: #555; } }
|
|
583
|
+
@mixin print {
|
|
584
|
+
@media print { @content; }
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Page-level defaults, ON by default. Include ONCE at the stylesheet ROOT —
|
|
588
|
+
// NOT inside a selector. It emits `@page`, which is invalid nested under a rule
|
|
589
|
+
// (`@page { body { … } }` does nothing). Toggle pieces off via args.
|
|
590
|
+
// @include m.print-base; // at root
|
|
591
|
+
// @include m.print-base($size: A4, $freeze-animations: false);
|
|
592
|
+
//
|
|
593
|
+
// Emits the print VARIABLE control plane — the whole filter flips from here:
|
|
594
|
+
// --is-print 0 on screen, 1 on paper. Read it in calc()/opacity/
|
|
595
|
+
// @container style(--is-print: 1) for custom print effects.
|
|
596
|
+
// --print-hide display applied to elements carrying `print-hidden`.
|
|
597
|
+
// --print-show display applied to elements carrying `print-only`.
|
|
598
|
+
// Override either locally for a per-element exception — no rule rewrite:
|
|
599
|
+
// .keep-this { --print-hide: revert; } // stays on paper despite print-hidden
|
|
600
|
+
// .pdf-row { --print-show: flex; } // print-only block laid out as flex
|
|
601
|
+
@mixin print-base($freeze-animations: true, $size: letter, $margin: 0.5in) {
|
|
602
|
+
:root {
|
|
603
|
+
--is-print: 0;
|
|
604
|
+
--print-hide: none;
|
|
605
|
+
--print-show: revert;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
@media print {
|
|
609
|
+
:root { --is-print: 1; }
|
|
610
|
+
|
|
611
|
+
@page { size: $size; margin: $margin; }
|
|
612
|
+
|
|
613
|
+
@if $freeze-animations {
|
|
614
|
+
// The opacity:0 entrance-fade trap: a page snapshotted mid-animation
|
|
615
|
+
// prints invisible text.
|
|
616
|
+
//
|
|
617
|
+
// We do NOT solve this by forcing `opacity: 1` and `transform: none`.
|
|
618
|
+
// That fixes the fade but destroys every DELIBERATE use of the same
|
|
619
|
+
// properties — a 0.15 watermark, a 0.4 disabled control, a stamp rotated
|
|
620
|
+
// -4deg all get flattened on paper.
|
|
621
|
+
//
|
|
622
|
+
// Instead, collapse animations to zero duration and pin them to their
|
|
623
|
+
// final frame. An entrance fade lands on its end state (visible), while
|
|
624
|
+
// an element that was never animating is left completely alone.
|
|
625
|
+
//
|
|
626
|
+
// `!important` stays, and is load-bearing: @media adds no specificity, so
|
|
627
|
+
// any author `animation:` shorthand of equal-or-higher specificity would
|
|
628
|
+
// otherwise win and re-introduce the trap.
|
|
629
|
+
/* stylelint-disable declaration-no-important -- must beat author animation/transition declarations; @media contributes no specificity of its own. */
|
|
630
|
+
*,
|
|
631
|
+
*::before,
|
|
632
|
+
*::after {
|
|
633
|
+
animation-delay: 0s !important;
|
|
634
|
+
animation-duration: 0s !important;
|
|
635
|
+
animation-fill-mode: forwards !important;
|
|
636
|
+
transition-delay: 0s !important;
|
|
637
|
+
transition-duration: 0s !important;
|
|
638
|
+
}
|
|
639
|
+
/* stylelint-enable declaration-no-important */
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Hide on paper — the "hide the nav" case. The display VALUE is variable-driven
|
|
645
|
+
// (--print-hide), so consumers re-aim it per element without a rule rewrite;
|
|
646
|
+
// !important on the declaration keeps the hide reliable against component
|
|
647
|
+
// `display` rules. Requires `print-base` at the root for the variable default.
|
|
648
|
+
@mixin print-hidden {
|
|
649
|
+
/* stylelint-disable-next-line declaration-no-important -- hide-on-print must beat any component display rule; the value stays variable-driven via --print-hide. */
|
|
650
|
+
@media print { display: var(--print-hide, none) !important; }
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Show ONLY on paper (e.g. an inline URL footer). Hidden on screen; the paper
|
|
654
|
+
// display is variable-driven (--print-show) so a print-only block can be flex.
|
|
655
|
+
@mixin print-only {
|
|
656
|
+
/* stylelint-disable declaration-no-important -- show/hide-on-print must beat component display rules; values stay variable-driven. */
|
|
657
|
+
@media screen { display: none !important; }
|
|
658
|
+
@media print { display: var(--print-show, revert) !important; }
|
|
659
|
+
/* stylelint-enable declaration-no-important */
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// ============================================================================
|
|
663
|
+
// CONTAINER QUERIES
|
|
664
|
+
// ============================================================================
|
|
665
|
+
// Component-level responsive design. Use alongside viewport breakpoints.
|
|
666
|
+
// Usage: @include m.container('card'); then @include m.cq(md) { ... }
|
|
667
|
+
// ============================================================================
|
|
668
|
+
|
|
669
|
+
@mixin container($name: null, $type: inline-size) {
|
|
670
|
+
container-type: $type;
|
|
671
|
+
@if $name {
|
|
672
|
+
container-name: $name;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
@mixin contain($size, $name: null) {
|
|
677
|
+
$width: $size;
|
|
678
|
+
@if meta.type-of($size) != number {
|
|
679
|
+
$width: map.get(s.$breakpoints, $size);
|
|
680
|
+
}
|
|
681
|
+
@if $name {
|
|
682
|
+
@container #{$name} (min-width: #{$width}) { @content; }
|
|
683
|
+
} @else {
|
|
684
|
+
@container (min-width: #{$width}) { @content; }
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
@mixin contain-down($size, $name: null) {
|
|
689
|
+
$width: $size;
|
|
690
|
+
@if meta.type-of($size) != number {
|
|
691
|
+
$width: map.get(s.$breakpoints, $size);
|
|
692
|
+
}
|
|
693
|
+
@if $name {
|
|
694
|
+
@container #{$name} (max-width: calc(#{$width} - 1px)) { @content; }
|
|
695
|
+
} @else {
|
|
696
|
+
@container (max-width: calc(#{$width} - 1px)) { @content; }
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
@mixin contain-between($min, $max, $name: null) {
|
|
701
|
+
$min-w: $min;
|
|
702
|
+
$max-w: $max;
|
|
703
|
+
@if meta.type-of($min) != number {
|
|
704
|
+
$min-w: map.get(s.$breakpoints, $min);
|
|
705
|
+
}
|
|
706
|
+
@if meta.type-of($max) != number {
|
|
707
|
+
$max-w: map.get(s.$breakpoints, $max);
|
|
708
|
+
}
|
|
709
|
+
@if $name {
|
|
710
|
+
@container #{$name} (min-width: #{$min-w}) and (max-width: calc(#{$max-w} - 1px)) { @content; }
|
|
711
|
+
} @else {
|
|
712
|
+
@container (min-width: #{$min-w}) and (max-width: calc(#{$max-w} - 1px)) { @content; }
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// ============================================================================
|
|
717
|
+
// TRANSITIONS
|
|
718
|
+
// ============================================================================
|
|
719
|
+
$_speeds: (
|
|
720
|
+
instant: 0ms,
|
|
721
|
+
fast: 150ms,
|
|
722
|
+
normal: 250ms,
|
|
723
|
+
slow: 400ms,
|
|
724
|
+
slower: 600ms,
|
|
725
|
+
);
|
|
726
|
+
|
|
727
|
+
$_easings: (
|
|
728
|
+
linear: linear,
|
|
729
|
+
ease: ease,
|
|
730
|
+
ease-in: ease-in,
|
|
731
|
+
ease-out: ease-out,
|
|
732
|
+
ease-in-out: ease-in-out,
|
|
733
|
+
bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55),
|
|
734
|
+
smooth: cubic-bezier(0.4, 0, 0.2, 1),
|
|
735
|
+
);
|
|
736
|
+
|
|
737
|
+
// Speed and easing can be given two ways:
|
|
738
|
+
// positionally @include transition(color, slow, bounce)
|
|
739
|
+
// by name @include transition(color, $duration: slow, $easing: bounce)
|
|
740
|
+
//
|
|
741
|
+
// The named form exists because the positional one is undiscoverable — the
|
|
742
|
+
// signature is `$props...`, so nothing tells you a speed keyword is even
|
|
743
|
+
// accepted, and an unrecognised word silently becomes a transitioned property.
|
|
744
|
+
// A raw duration (`220ms`) is passed through as-is.
|
|
745
|
+
@mixin transition($props...) {
|
|
746
|
+
$kw: meta.keywords($props);
|
|
747
|
+
$speed: map.get($kw, duration) or fast;
|
|
748
|
+
$easing: map.get($kw, easing) or smooth;
|
|
749
|
+
$result: ();
|
|
750
|
+
|
|
751
|
+
// Pass 1 — pick up positional speed/easing keywords wherever they appear.
|
|
752
|
+
// Previously this happened in the same loop that built the list, so
|
|
753
|
+
// `transition(color, slow)` silently used the default speed: `color` was
|
|
754
|
+
// appended before `slow` had been seen. Only `transition(slow, color)` worked.
|
|
755
|
+
@each $prop in $props {
|
|
756
|
+
@if map.has-key($_speeds, $prop) {
|
|
757
|
+
$speed: $prop;
|
|
758
|
+
} @else if map.has-key($_easings, $prop) {
|
|
759
|
+
$easing: $prop;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// Pass 2 — build the list, now that speed and easing are settled.
|
|
764
|
+
// `or $speed` / `or $easing` lets a raw value through — `220ms` or a literal
|
|
765
|
+
// cubic-bezier() — instead of resolving to null and emitting `color ;`.
|
|
766
|
+
@each $prop in $props {
|
|
767
|
+
@if not map.has-key($_speeds, $prop) and not map.has-key($_easings, $prop) {
|
|
768
|
+
$result: list.append(
|
|
769
|
+
$result,
|
|
770
|
+
$prop (map.get($_speeds, $speed) or $speed) (map.get($_easings, $easing) or $easing),
|
|
771
|
+
comma
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
transition: $result;
|
|
776
|
+
@media (prefers-reduced-motion: reduce) {
|
|
777
|
+
transition: none;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// ============================================================================
|
|
782
|
+
// LAYOUT
|
|
783
|
+
// ============================================================================
|
|
784
|
+
// One flex primitive. Pass only what differs from the defaults.
|
|
785
|
+
//
|
|
786
|
+
// @include m.flex; // flex row, centered cross-axis
|
|
787
|
+
// @include m.flex($direction: column, $gap: 3); // vertical stack with gap
|
|
788
|
+
// @include m.flex($justify: between, $gap: 3); // header bar / accordion trigger
|
|
789
|
+
// @include m.flex($justify: center); // perfectly centered children
|
|
790
|
+
// @include m.flex($inline: true, $gap: 2); // inline-flex chip lockup
|
|
791
|
+
//
|
|
792
|
+
// $justify shorthand maps to CSS: start/end/center/between/around/evenly →
|
|
793
|
+
// flex-start/flex-end/center/space-between/space-around/space-evenly. Same
|
|
794
|
+
// idea for $align (start/end → flex-start/flex-end). Full CSS values pass
|
|
795
|
+
// through unchanged, so $justify: space-between still works.
|
|
796
|
+
|
|
797
|
+
$_flex-justify: (
|
|
798
|
+
start: flex-start,
|
|
799
|
+
end: flex-end,
|
|
800
|
+
center: center,
|
|
801
|
+
between: space-between,
|
|
802
|
+
around: space-around,
|
|
803
|
+
evenly: space-evenly,
|
|
804
|
+
);
|
|
805
|
+
|
|
806
|
+
$_flex-align: (
|
|
807
|
+
start: flex-start,
|
|
808
|
+
end: flex-end,
|
|
809
|
+
center: center,
|
|
810
|
+
baseline: baseline,
|
|
811
|
+
stretch: stretch,
|
|
812
|
+
);
|
|
813
|
+
|
|
814
|
+
@mixin flex(
|
|
815
|
+
$direction: row,
|
|
816
|
+
$gap: null,
|
|
817
|
+
$align: center,
|
|
818
|
+
$justify: start,
|
|
819
|
+
$wrap: nowrap,
|
|
820
|
+
$inline: false
|
|
821
|
+
) {
|
|
822
|
+
$j: map.get($_flex-justify, $justify) or $justify;
|
|
823
|
+
$a: map.get($_flex-align, $align) or $align;
|
|
824
|
+
|
|
825
|
+
display: if(sass($inline): inline-flex; else: flex);
|
|
826
|
+
flex-direction: $direction;
|
|
827
|
+
flex-wrap: $wrap;
|
|
828
|
+
align-items: $a;
|
|
829
|
+
justify-content: $j;
|
|
830
|
+
|
|
831
|
+
@if $gap { gap: space($gap); }
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
@mixin pad($size: 4) {
|
|
835
|
+
padding: space($size);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
@mixin pad-x($size: 4) {
|
|
839
|
+
padding-inline: space($size);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
@mixin pad-y($size: 4) {
|
|
843
|
+
padding-block: space($size);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
@mixin pad-asym($y: 2, $x: 4) {
|
|
847
|
+
padding: space($y) space($x);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// ============================================================================
|
|
851
|
+
// RESETS
|
|
852
|
+
// ============================================================================
|
|
853
|
+
|
|
854
|
+
@mixin button-reset {
|
|
855
|
+
appearance: none;
|
|
856
|
+
background: none;
|
|
857
|
+
border: none;
|
|
858
|
+
padding: 0;
|
|
859
|
+
margin: 0;
|
|
860
|
+
font: inherit;
|
|
861
|
+
color: inherit;
|
|
862
|
+
cursor: pointer;
|
|
863
|
+
text-decoration: none;
|
|
864
|
+
&:focus { outline: none; }
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
@mixin list-reset {
|
|
868
|
+
list-style: none;
|
|
869
|
+
margin: 0;
|
|
870
|
+
padding: 0;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
@mixin header-reset {
|
|
874
|
+
@for $i from 1 through 6 {
|
|
875
|
+
h#{$i} { margin: 0; padding: 0; font: inherit; }
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
@mixin form-reset {
|
|
880
|
+
input, button, select, textarea {
|
|
881
|
+
font: inherit;
|
|
882
|
+
color: inherit;
|
|
883
|
+
}
|
|
884
|
+
input:not([type="checkbox"]):not([type="radio"]),
|
|
885
|
+
select,
|
|
886
|
+
textarea {
|
|
887
|
+
width: 100%;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// ============================================================================
|
|
892
|
+
// UTILITIES
|
|
893
|
+
// ============================================================================
|
|
894
|
+
|
|
895
|
+
@mixin truncate($lines: 1) {
|
|
896
|
+
@if $lines == 1 {
|
|
897
|
+
overflow: hidden;
|
|
898
|
+
text-overflow: ellipsis;
|
|
899
|
+
white-space: nowrap;
|
|
900
|
+
} @else {
|
|
901
|
+
display: -webkit-box;
|
|
902
|
+
-webkit-line-clamp: $lines;
|
|
903
|
+
-webkit-box-orient: vertical;
|
|
904
|
+
overflow: hidden;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
@mixin sr-only {
|
|
909
|
+
position: absolute;
|
|
910
|
+
width: 1px;
|
|
911
|
+
height: 1px;
|
|
912
|
+
padding: 0;
|
|
913
|
+
margin: -1px;
|
|
914
|
+
overflow: hidden;
|
|
915
|
+
clip: rect(0, 0, 0, 0);
|
|
916
|
+
border: 0;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
@mixin disabled($opacity: 0.5) {
|
|
920
|
+
opacity: $opacity;
|
|
921
|
+
cursor: not-allowed;
|
|
922
|
+
pointer-events: none;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
@mixin focus-ring($color: border-focus, $width: 3px, $offset: 0) {
|
|
926
|
+
&:focus-visible {
|
|
927
|
+
outline: none;
|
|
928
|
+
box-shadow: 0 0 0 $width color($color);
|
|
929
|
+
@if $offset > 0 {
|
|
930
|
+
outline: $width solid color($color);
|
|
931
|
+
outline-offset: $offset;
|
|
932
|
+
box-shadow: none;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
@mixin hover($prop, $value, $speed: fast) {
|
|
938
|
+
@include transition($prop, $speed);
|
|
939
|
+
&:hover { #{$prop}: $value; }
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
@mixin interactive($bg-hover: interactive-hover, $bg-active: interactive-active) {
|
|
943
|
+
@include transition(background-color, fast);
|
|
944
|
+
&:hover { background-color: color($bg-hover); }
|
|
945
|
+
&:active { background-color: color($bg-active); }
|
|
946
|
+
&:disabled, &[disabled] { @include disabled; }
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// ============================================================================
|
|
950
|
+
// THEME
|
|
951
|
+
// ============================================================================
|
|
952
|
+
// Authoring helpers for theme files. Keep boilerplate out of per-theme source;
|
|
953
|
+
// theme authors write only the token values.
|
|
954
|
+
|
|
955
|
+
// Wraps a theme's :root[data-theme] selector + color-scheme around its body.
|
|
956
|
+
//
|
|
957
|
+
// Usage:
|
|
958
|
+
// @include theme('press') {
|
|
959
|
+
// --background-default: light-dark(#f7f2e8, #0a0a08);
|
|
960
|
+
// --ink: light-dark(#1a1a1a, #f5f5f0);
|
|
961
|
+
// }
|
|
962
|
+
//
|
|
963
|
+
// // Mode-stable themes pass an explicit scheme:
|
|
964
|
+
// @include theme('sketchbook', light) {
|
|
965
|
+
// --background-default: #faf8f1;
|
|
966
|
+
// }
|
|
967
|
+
//
|
|
968
|
+
// SELECTOR MODEL
|
|
969
|
+
// ----------------------------------------------------------------------------
|
|
970
|
+
// Emits `:root, :root[data-theme="<name>"]` by default, so ONE theme file
|
|
971
|
+
// dropped in as theme.css restyles the page with no markup change, AND the same
|
|
972
|
+
// file still responds to data-theme when several themes are loaded together.
|
|
973
|
+
//
|
|
974
|
+
// The bare `:root` is the whole point of the product promise: install cia once,
|
|
975
|
+
// then reskin by swapping a single file. Requiring the consumer to also edit
|
|
976
|
+
// `<html data-theme=...>` to match the file they just swapped makes a mismatch
|
|
977
|
+
// silently render an untokenised page — the failure is invisible and the fix is
|
|
978
|
+
// non-obvious. Before this, the shipped themes disagreed three ways: 9 emitted
|
|
979
|
+
// `:root[data-theme=x]`, 7 emitted `[data-theme=x]`, and 5 emitted a bare
|
|
980
|
+
// `:root`. Only the last group actually worked when dropped in alone.
|
|
981
|
+
//
|
|
982
|
+
// Pass $standalone: false for the multi-theme bundle, where every theme shares
|
|
983
|
+
// one file and a bare `:root` would make them collide — there, the data-theme
|
|
984
|
+
// attribute is the only thing distinguishing them.
|
|
985
|
+
@mixin theme($name, $scheme: light dark, $standalone: true) {
|
|
986
|
+
$scoped: ':root[data-theme="#{$name}"]';
|
|
987
|
+
|
|
988
|
+
// @if/@else rather than the if() FUNCTION: Sass deprecated `if()` in favour
|
|
989
|
+
// of the CSS if() syntax, and every theme build emitted a deprecation
|
|
990
|
+
// warning through it — 24 themes' worth of noise in the release log.
|
|
991
|
+
$selector: $scoped;
|
|
992
|
+
@if $standalone {
|
|
993
|
+
$selector: ':root, #{$scoped}';
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
#{$selector} {
|
|
997
|
+
color-scheme: #{$scheme};
|
|
998
|
+
@content;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// Derives hover/active states from a base color via color-mix in oklch.
|
|
1003
|
+
// Mixes BLACK in light mode, WHITE in dark mode — so light hover = darker,
|
|
1004
|
+
// dark hover = lighter, both feel right without separate per-mode declarations.
|
|
1005
|
+
//
|
|
1006
|
+
// Usage:
|
|
1007
|
+
// --action-primary-default: light-dark(#3A5FCD, #60a5fa);
|
|
1008
|
+
// @include states(action-primary);
|
|
1009
|
+
// /* emits --action-primary-hover and --action-primary-active */
|
|
1010
|
+
@mixin states($group, $hover-amount: 12%, $active-amount: 20%) {
|
|
1011
|
+
--#{$group}-hover: color-mix(in oklch, var(--#{$group}-default), light-dark(black, white) #{$hover-amount});
|
|
1012
|
+
--#{$group}-active: color-mix(in oklch, var(--#{$group}-default), light-dark(black, white) #{$active-amount});
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// Registers a list of color tokens with @property so theme swaps animate
|
|
1016
|
+
// smoothly via `transition: --token 240ms`. Only emits when called.
|
|
1017
|
+
//
|
|
1018
|
+
// Usage:
|
|
1019
|
+
// @include theme-properties((
|
|
1020
|
+
// action-primary-default,
|
|
1021
|
+
// action-primary-hover,
|
|
1022
|
+
// action-primary-active,
|
|
1023
|
+
// background-default,
|
|
1024
|
+
// ink,
|
|
1025
|
+
// ));
|
|
1026
|
+
@mixin theme-properties($tokens) {
|
|
1027
|
+
@at-root {
|
|
1028
|
+
@each $token in $tokens {
|
|
1029
|
+
@property --#{$token} {
|
|
1030
|
+
syntax: '<color>';
|
|
1031
|
+
inherits: true;
|
|
1032
|
+
initial-value: transparent;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
}
|