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.
Files changed (198) hide show
  1. package/AGENTS.md +343 -0
  2. package/CHANGELOG.md +1036 -0
  3. package/CLAUDE.md +9 -0
  4. package/CONTRACT.md +580 -0
  5. package/GEMINI.md +9 -0
  6. package/LICENSE +21 -0
  7. package/LICENSE-third-party +88 -0
  8. package/MIGRATION.md +312 -0
  9. package/README.md +380 -0
  10. package/ROADMAP.md +714 -0
  11. package/THEMING.md +290 -0
  12. package/THREE-TIERS.md +158 -0
  13. package/VERSIONING.md +200 -0
  14. package/_index.scss +34 -0
  15. package/api.scss +16 -0
  16. package/bin/README.md +57 -0
  17. package/bin/cia.cjs +114 -0
  18. package/bin/migrate-bootstrap.cjs +427 -0
  19. package/bin/migrate-tailwind.cjs +832 -0
  20. package/css-is-awesome.instructions.md +533 -0
  21. package/dist/css-is-awesome.core.css +598 -0
  22. package/dist/css-is-awesome.core.min.css +1 -0
  23. package/dist/css-is-awesome.css +3733 -0
  24. package/dist/css-is-awesome.min.css +1 -0
  25. package/dist/css-is-awesome.utilities.css +2445 -0
  26. package/dist/css-is-awesome.utilities.min.css +1 -0
  27. package/dist/tokens.css +508 -0
  28. package/dist/tokens.d.ts +266 -0
  29. package/dist/tokens.min.css +1 -0
  30. package/figma-tokens/README.md +57 -0
  31. package/figma-tokens/primitives-brand.scss +121 -0
  32. package/figma-tokens/semantic-dark.scss +61 -0
  33. package/figma-tokens/semantic-light.scss +61 -0
  34. package/figma-tokens/tokens.json +144 -0
  35. package/llm.txt +183 -0
  36. package/mcp/server.cjs +1495 -0
  37. package/package.json +199 -0
  38. package/public/icons/README.md +278 -0
  39. package/public/icons/arrow-right.svg +4 -0
  40. package/public/icons/check.svg +4 -0
  41. package/public/icons/chevron-down.svg +4 -0
  42. package/public/icons/close.svg +4 -0
  43. package/public/icons/core/arrow-down.svg +13 -0
  44. package/public/icons/core/arrow-left.svg +13 -0
  45. package/public/icons/core/arrow-right.svg +13 -0
  46. package/public/icons/core/arrow-up.svg +13 -0
  47. package/public/icons/core/bell.svg +13 -0
  48. package/public/icons/core/calendar.svg +15 -0
  49. package/public/icons/core/check.svg +12 -0
  50. package/public/icons/core/chevron-down.svg +12 -0
  51. package/public/icons/core/chevron-left.svg +12 -0
  52. package/public/icons/core/chevron-right.svg +12 -0
  53. package/public/icons/core/chevron-up.svg +12 -0
  54. package/public/icons/core/clock.svg +13 -0
  55. package/public/icons/core/close.svg +13 -0
  56. package/public/icons/core/copy.svg +13 -0
  57. package/public/icons/core/download.svg +14 -0
  58. package/public/icons/core/edit.svg +13 -0
  59. package/public/icons/core/error.svg +14 -0
  60. package/public/icons/core/external-link.svg +14 -0
  61. package/public/icons/core/eye-off.svg +15 -0
  62. package/public/icons/core/eye.svg +13 -0
  63. package/public/icons/core/filter.svg +12 -0
  64. package/public/icons/core/heart.svg +12 -0
  65. package/public/icons/core/help.svg +14 -0
  66. package/public/icons/core/home.svg +13 -0
  67. package/public/icons/core/info.svg +14 -0
  68. package/public/icons/core/loading.svg +12 -0
  69. package/public/icons/core/lock.svg +13 -0
  70. package/public/icons/core/mail.svg +13 -0
  71. package/public/icons/core/menu.svg +14 -0
  72. package/public/icons/core/message.svg +12 -0
  73. package/public/icons/core/minus.svg +12 -0
  74. package/public/icons/core/more-horizontal.svg +14 -0
  75. package/public/icons/core/pause.svg +13 -0
  76. package/public/icons/core/play.svg +12 -0
  77. package/public/icons/core/plus.svg +13 -0
  78. package/public/icons/core/refresh.svg +15 -0
  79. package/public/icons/core/save.svg +14 -0
  80. package/public/icons/core/search.svg +13 -0
  81. package/public/icons/core/settings.svg +13 -0
  82. package/public/icons/core/share.svg +14 -0
  83. package/public/icons/core/sort.svg +15 -0
  84. package/public/icons/core/star.svg +12 -0
  85. package/public/icons/core/success.svg +13 -0
  86. package/public/icons/core/trash.svg +16 -0
  87. package/public/icons/core/unlock.svg +13 -0
  88. package/public/icons/core/upload.svg +14 -0
  89. package/public/icons/core/user.svg +13 -0
  90. package/public/icons/core/users.svg +15 -0
  91. package/public/icons/core/warning.svg +14 -0
  92. package/public/icons/download.svg +4 -0
  93. package/public/icons/edit.svg +4 -0
  94. package/public/icons/menu.svg +4 -0
  95. package/public/icons/search.svg +4 -0
  96. package/public/theme.css +4179 -0
  97. package/public/themes/README.md +102 -0
  98. package/public/themes/boilerplate/theme.css +154 -0
  99. package/public/themes/boilerplate-dark/theme.css +171 -0
  100. package/public/themes/boilerplate-light/theme.css +171 -0
  101. package/public/themes/cupertino/theme.css +165 -0
  102. package/public/themes/cupertino-dark/theme.css +200 -0
  103. package/public/themes/cupertino-light/theme.css +227 -0
  104. package/public/themes/glass/theme.css +190 -0
  105. package/public/themes/glass-dark/theme.css +201 -0
  106. package/public/themes/glass-light/theme.css +229 -0
  107. package/public/themes/graphite/theme.css +155 -0
  108. package/public/themes/graphite-dark/theme.css +223 -0
  109. package/public/themes/graphite-light/theme.css +191 -0
  110. package/public/themes/press/theme.css +189 -0
  111. package/public/themes/press-dark/theme.css +199 -0
  112. package/public/themes/press-light/theme.css +235 -0
  113. package/public/themes/prism/theme.css +159 -0
  114. package/public/themes/prism-dark/theme.css +185 -0
  115. package/public/themes/prism-light/theme.css +188 -0
  116. package/public/themes/sketchbook/theme.css +152 -0
  117. package/public/themes/sketchbook-dark/theme.css +194 -0
  118. package/public/themes/sketchbook-light/theme.css +188 -0
  119. package/public/themes/terminal/theme.css +159 -0
  120. package/public/themes/terminal-dark/theme.css +229 -0
  121. package/public/themes/terminal-light/theme.css +152 -0
  122. package/scripts/README.md +70 -0
  123. package/scripts/icon-contract.json +67 -0
  124. package/scripts/icon-validator.js +205 -0
  125. package/scripts/prepare-dist.mjs +65 -0
  126. package/scripts/theme-a11y.js +415 -0
  127. package/scripts/theme-contract.json +171 -0
  128. package/scripts/theme-validator.js +674 -0
  129. package/scss/README.md +55 -0
  130. package/scss/_animations-utilities.scss +107 -0
  131. package/scss/_animations.scss +248 -0
  132. package/scss/_app-styles.scss +15 -0
  133. package/scss/_generator.scss +174 -0
  134. package/scss/_icons.scss +475 -0
  135. package/scss/_index.scss +17 -0
  136. package/scss/_layout.scss +597 -0
  137. package/scss/_mixins.scss +1036 -0
  138. package/scss/_system.scss +291 -0
  139. package/scss/_utilities.scss +377 -0
  140. package/scss/api.scss +61 -0
  141. package/scss/components/_accordion.scss +72 -0
  142. package/scss/components/_buttons.scss +171 -0
  143. package/scss/components/_copy-button.scss +69 -0
  144. package/scss/components/_data.scss +190 -0
  145. package/scss/components/_feedback.scss +197 -0
  146. package/scss/components/_forms.scss +272 -0
  147. package/scss/components/_index.scss +17 -0
  148. package/scss/components/_navigation.scss +186 -0
  149. package/scss/components/_overlay.scss +259 -0
  150. package/scss/components/_stepper.scss +159 -0
  151. package/scss/components/_tabs.scss +72 -0
  152. package/scss/core.scss +54 -0
  153. package/scss/examples/_theming.scss +107 -0
  154. package/scss/examples/_usage.scss +317 -0
  155. package/scss/main.scss +64 -0
  156. package/scss/recipes/README.md +110 -0
  157. package/scss/recipes/_bare-tags.scss +232 -0
  158. package/scss/recipes/_recipe-template.md +119 -0
  159. package/scss/recipes/combobox.md +509 -0
  160. package/scss/recipes/dialog.md +265 -0
  161. package/scss/recipes/print-to-pdf.md +263 -0
  162. package/scss/theme/README.md +67 -0
  163. package/scss/theme/_brand.scss +18 -0
  164. package/scss/theme/_colors-dark.scss +68 -0
  165. package/scss/theme/_colors-light.scss +70 -0
  166. package/scss/theme/_components.scss +94 -0
  167. package/scss/theme/_icons.scss +104 -0
  168. package/scss/theme/_index.scss +62 -0
  169. package/scss/theme/_shadows.scss +65 -0
  170. package/scss/theme/_shape.scss +13 -0
  171. package/scss/theme/_typography.scss +9 -0
  172. package/scss/theme/registry.json +447 -0
  173. package/scss/themes/boilerplate-dark.scss +212 -0
  174. package/scss/themes/boilerplate-light.scss +209 -0
  175. package/scss/themes/boilerplate.scss +181 -0
  176. package/scss/themes/cupertino-dark.scss +238 -0
  177. package/scss/themes/cupertino-light.scss +266 -0
  178. package/scss/themes/cupertino.scss +194 -0
  179. package/scss/themes/glass-dark.scss +238 -0
  180. package/scss/themes/glass-light.scss +266 -0
  181. package/scss/themes/glass.scss +220 -0
  182. package/scss/themes/graphite-dark.scss +260 -0
  183. package/scss/themes/graphite-light.scss +228 -0
  184. package/scss/themes/graphite.scss +182 -0
  185. package/scss/themes/press-dark.scss +237 -0
  186. package/scss/themes/press-light.scss +273 -0
  187. package/scss/themes/press.scss +241 -0
  188. package/scss/themes/prism-dark.scss +217 -0
  189. package/scss/themes/prism-light.scss +220 -0
  190. package/scss/themes/prism.scss +190 -0
  191. package/scss/themes/sketchbook-dark.scss +231 -0
  192. package/scss/themes/sketchbook-light.scss +231 -0
  193. package/scss/themes/sketchbook.scss +178 -0
  194. package/scss/themes/terminal-dark.scss +267 -0
  195. package/scss/themes/terminal-light.scss +175 -0
  196. package/scss/themes/terminal.scss +185 -0
  197. package/scss/tokens.scss +23 -0
  198. package/scss/utilities-only.scss +4 -0
@@ -0,0 +1,597 @@
1
+ // ============================================================================
2
+ // LAYOUT - Grid, Container, and Page Structure Mixins
3
+ // ============================================================================
4
+ // Smart defaults that work for 80% of cases. Override with parameters.
5
+ // Usage: @include m.wrap; @include m.grid(3); @include m.page-layout;
6
+ // ============================================================================
7
+ @use 'sass:list';
8
+ @use 'sass:map';
9
+ @use 'sass:math';
10
+ @use 'sass:meta';
11
+ @use 'sass:string';
12
+ @use './system' as s;
13
+ @use './theme' as t;
14
+ @use './mixins' as m;
15
+
16
+ // ============================================================================
17
+ // CONTAINER
18
+ // ============================================================================
19
+ // Centered, max-width wrapper with responsive padding.
20
+ // Usage:
21
+ // @include container; → 1280px max, responsive padding
22
+ // @include container(sm); → 640px max
23
+ // @include container(full); → no max-width, just padding
24
+ // @include wrap($px: 6); → custom horizontal padding (space key 6)
25
+
26
+ $_container-widths: (
27
+ sm: 40rem,
28
+ md: 48rem,
29
+ lg: 64rem,
30
+ xl: 80rem,
31
+ 2xl: 96rem,
32
+ full: none,
33
+ );
34
+
35
+ @mixin wrap($size: xl, $px: null) {
36
+ width: 100%;
37
+ margin-inline: auto;
38
+
39
+ $max: map.get($_container-widths, $size);
40
+ @if $max and $max != none {
41
+ max-width: $max;
42
+ }
43
+
44
+ $pad: $px or 4;
45
+ $pad-val: m.space($pad);
46
+ padding-inline: $pad-val;
47
+
48
+ @media screen and (min-width: map.get(s.$breakpoints, md)) {
49
+ $pad-up: if(sass($px): $pad-val; else: m.space(5));
50
+ padding-inline: $pad-up;
51
+ }
52
+ }
53
+
54
+ // ============================================================================
55
+ // GRID
56
+ // ============================================================================
57
+ // Responsive CSS grid. Sensible defaults: N columns on desktop, 1 on mobile.
58
+ // Usage:
59
+ // @include grid(3); → 3 cols desktop, 1 col mobile
60
+ // @include grid(4, $gap: 5); → 4 cols, large gap (space key 5)
61
+ // @include grid(auto, $min: 250px);→ auto-fit, 250px minimum per item
62
+ // @include grid(2, $bp: sm); → 2 cols from sm breakpoint
63
+
64
+ @mixin grid($cols: 1, $gap: 4, $bp: md, $min: null) {
65
+ display: grid;
66
+ gap: m.space($gap);
67
+
68
+ @if $cols == auto {
69
+ $min-val: $min or 16rem;
70
+ grid-template-columns: repeat(auto-fit, minmax(#{$min-val}, 1fr));
71
+ } @else if $cols == 1 {
72
+ grid-template-columns: 1fr;
73
+ } @else {
74
+ grid-template-columns: 1fr;
75
+
76
+ $bp-val: $bp;
77
+ @if meta.type-of($bp) != number {
78
+ $bp-val: map.get(s.$breakpoints, $bp);
79
+ }
80
+
81
+ @media screen and (min-width: $bp-val) {
82
+ grid-template-columns: repeat(#{$cols}, 1fr);
83
+ }
84
+ }
85
+ }
86
+
87
+ // ============================================================================
88
+ // SUBGRID
89
+ // ============================================================================
90
+ // Makes a grid child inherit the parent's column/row tracks.
91
+ // Usage:
92
+ // .parent { @include grid(3); }
93
+ // .child { @include subgrid; grid-column: span 3; }
94
+
95
+ @mixin subgrid($direction: columns) {
96
+ display: grid;
97
+ @if $direction == both {
98
+ grid-template-columns: subgrid;
99
+ grid-template-rows: subgrid;
100
+ } @else if $direction == rows {
101
+ grid-template-rows: subgrid;
102
+ } @else {
103
+ grid-template-columns: subgrid;
104
+ }
105
+ }
106
+
107
+ // ============================================================================
108
+ // PAGE LAYOUT — Named-Region Engine
109
+ // ============================================================================
110
+ // One engine, FOUR ways to use it — simplest → most powerful. Users pick the
111
+ // tier they're comfortable with; every tier compiles to plain
112
+ // grid-template-areas with zero runtime cost.
113
+ //
114
+ // Tier 0 @include m.page-layout(sidebar-left); // preset keyword
115
+ // Tier 1 @include m.named-layout(app, $wire: true); // reusable map + auto-place
116
+ // Tier 2 @include m.layout((nav),(side body),(foot)); // build from YOUR names
117
+ // Tier 3 body { @include m.page-layout-switchable; } // runtime data-layout switch
118
+ // ============================================================================
119
+
120
+ // ----------------------------------------------------------------------------
121
+ // ENGINE — turn a list of region rows into a grid-template-areas spec.
122
+ // Returns (areas: <space-list of quoted strings>, cols: <int>, regions: <rows>).
123
+ //
124
+ // Fill rule: a row with fewer names than the widest row stretches its LAST
125
+ // region to fill the width — so a single-name row becomes a full-width band,
126
+ // and every generated template is a valid rectangle no matter what's passed.
127
+ // ----------------------------------------------------------------------------
128
+ @function _layout-spec($rows) {
129
+ $max: 1;
130
+ $norm: ();
131
+ @each $row in $rows {
132
+ $cells: if(sass(meta.type-of($row) != list): ($row,); else: $row);
133
+ $norm: list.append($norm, $cells, comma);
134
+ $max: math.max($max, list.length($cells));
135
+ }
136
+
137
+ $template: ();
138
+ @each $cells in $norm {
139
+ $n: list.length($cells);
140
+ $line: list.nth($cells, 1);
141
+ @if $max > 1 { // guard: Sass @for counts DOWN if from > through
142
+ @for $i from 2 through $max {
143
+ $next: if(sass($i <= $n): list.nth($cells, $i); else: list.nth($cells, $n));
144
+ $line: "#{$line} #{$next}";
145
+ }
146
+ }
147
+ $template: list.append($template, string.quote($line), space);
148
+ }
149
+
150
+ @return (areas: $template, cols: $max, regions: $norm);
151
+ }
152
+
153
+ // Flatten regions → ordered, de-duped single-column stack (used for mobile collapse).
154
+ @function _layout-flatten($regions) {
155
+ $flat: ();
156
+ @each $cells in $regions {
157
+ @each $name in $cells {
158
+ @if not list.index($flat, $name) { $flat: list.append($flat, $name); }
159
+ }
160
+ }
161
+ @return $flat;
162
+ }
163
+
164
+ // ----------------------------------------------------------------------------
165
+ // TIER 2 — generic builder from region names YOU choose.
166
+ // @include m.layout(nav, body, foot); // single-column stack
167
+ // @include m.layout((header),(sidebar main),(footer)); // rows of columns
168
+ // @include m.layout((a),(b c), $tracks: 16rem 1fr); // custom column tracks
169
+ // @include m.layout((a),(b c), $wire: true); // auto-place by data-area
170
+ // ----------------------------------------------------------------------------
171
+ @mixin layout($rows...) {
172
+ $opts: meta.keywords($rows);
173
+ $gap: map.get($opts, gap) or 4;
174
+ $tracks: map.get($opts, tracks) or null;
175
+ $wire: map.get($opts, wire);
176
+
177
+ $spec: _layout-spec($rows);
178
+ display: grid;
179
+ gap: m.space($gap);
180
+ grid-template-columns: if(sass($tracks): $tracks; else: repeat(#{map.get($spec, cols)}, 1fr));
181
+ grid-template-areas: map.get($spec, areas);
182
+
183
+ @if $wire {
184
+ @each $cells in map.get($spec, regions) {
185
+ @each $name in $cells {
186
+ > [data-area="#{$name}"] { grid-area: $name; }
187
+ }
188
+ }
189
+ }
190
+ }
191
+
192
+ // Place a child into a named region (when not using $wire). Or just write
193
+ // `grid-area: <name>` yourself.
194
+ @mixin area($name) { grid-area: $name; }
195
+
196
+ // ----------------------------------------------------------------------------
197
+ // TIER 0 / 1 — named presets. One shared registry feeds page-layout,
198
+ // named-layout, AND the runtime switcher. Add a layout = ONE map entry.
199
+ // ----------------------------------------------------------------------------
200
+ $_layouts: (
201
+ default: ((header), (main), (footer)),
202
+ sidebar-left: ((header), (sidebar main), (footer)),
203
+ sidebar-right: ((header), (main sidebar), (footer)),
204
+ holy-grail: ((header), (nav main aside), (footer)),
205
+ );
206
+
207
+ // Tier 1: reusable named layout. Forwards $gap / $tracks / $wire to layout().
208
+ // (Named args BEFORE the rest arg — required by Dart Sass 2.0.)
209
+ @mixin named-layout($name, $gap: 4, $tracks: null, $wire: null) {
210
+ $rows: map.get($_layouts, $name);
211
+ @if not $rows {
212
+ @error "Unknown layout '#{$name}'. Available: #{map.keys($_layouts)}";
213
+ }
214
+ @include layout($gap: $gap, $tracks: $tracks, $wire: $wire, $rows...);
215
+ }
216
+
217
+ // Tier 0: full page shell — viewport height, sticky footer, auto mobile collapse.
218
+ // body { @include m.page-layout; }
219
+ // body { @include m.page-layout(holy-grail); }
220
+ @mixin page-layout($variant: default, $collapse-at: md) {
221
+ $rows: map.get($_layouts, $variant);
222
+ @if not $rows {
223
+ @error "Unknown page-layout '#{$variant}'. Available: #{map.keys($_layouts)}";
224
+ }
225
+ $spec: _layout-spec($rows);
226
+
227
+ display: grid;
228
+ min-block-size: 100dvh;
229
+ grid-template-rows: auto 1fr auto;
230
+ grid-template-columns: repeat(#{map.get($spec, cols)}, 1fr);
231
+ grid-template-areas: map.get($spec, areas);
232
+
233
+ @if map.get($spec, cols) > 1 {
234
+ @include m.media-down($collapse-at) {
235
+ $stacked: _layout-spec(_layout-flatten(map.get($spec, regions)));
236
+ grid-template-columns: 1fr;
237
+ grid-template-areas: map.get($stacked, areas);
238
+ }
239
+ }
240
+ }
241
+
242
+ // Region placement helpers (unchanged from the original API).
243
+ @mixin page-header { grid-area: header; }
244
+ @mixin page-main { grid-area: main; }
245
+ @mixin page-footer { grid-area: footer; }
246
+ @mixin page-sidebar { grid-area: sidebar; }
247
+ @mixin page-nav { grid-area: nav; }
248
+ @mixin page-aside { grid-area: aside; }
249
+
250
+ // ----------------------------------------------------------------------------
251
+ // TIER 3 — runtime switching, ZERO recompile. Selector-based (NOT custom
252
+ // properties — Sass strips the quotes off grid-template-areas when
253
+ // interpolated into a custom prop, producing invalid CSS).
254
+ //
255
+ // body { @include m.page-layout-switchable; }
256
+ // <body data-layout="sidebar-left"> // flip live: el.dataset.layout = 'holy-grail'
257
+ // ----------------------------------------------------------------------------
258
+ @mixin page-layout-switchable($collapse-at: md) {
259
+ display: grid;
260
+ min-block-size: 100dvh;
261
+ grid-template-rows: auto 1fr auto;
262
+
263
+ @each $name, $rows in $_layouts {
264
+ &[data-layout="#{$name}"] {
265
+ $spec: _layout-spec($rows);
266
+ grid-template-columns: repeat(#{map.get($spec, cols)}, 1fr);
267
+ grid-template-areas: map.get($spec, areas);
268
+
269
+ @if map.get($spec, cols) > 1 {
270
+ @include m.media-down($collapse-at) {
271
+ $stacked: _layout-spec(_layout-flatten(map.get($spec, regions)));
272
+ grid-template-columns: 1fr;
273
+ grid-template-areas: map.get($stacked, areas);
274
+ }
275
+ }
276
+ }
277
+ }
278
+ }
279
+
280
+ // ============================================================================
281
+ // SECTION
282
+ // ============================================================================
283
+ // Vertical page section with consistent spacing.
284
+ // Usage:
285
+ // section { @include section; }
286
+ // section { @include section($py: 9); }
287
+
288
+ @mixin section($py: 8, $px: null) {
289
+ $py-val: m.space($py);
290
+ padding-block: $py-val;
291
+
292
+ @if $px {
293
+ $px-val: m.space($px);
294
+ padding-inline: $px-val;
295
+ }
296
+ }
297
+
298
+ // ============================================================================
299
+ // DIVIDER
300
+ // ============================================================================
301
+
302
+ @mixin divider($color: border-default, $spacing: 4) {
303
+ $space-val: m.space($spacing);
304
+ border: none;
305
+ border-block-start: 1px solid var(--#{$color});
306
+ margin-block: $space-val;
307
+ }
308
+
309
+ @mixin divider-vertical($color: border-default, $spacing: 4) {
310
+ $space-val: m.space($spacing);
311
+ border: none;
312
+ border-inline-start: 1px solid var(--#{$color});
313
+ margin-inline: $space-val;
314
+ align-self: stretch;
315
+ }
316
+
317
+ // ============================================================================
318
+ // INTRINSIC LAYOUT — Every Layout vocabulary, cia-native
319
+ // ============================================================================
320
+
321
+ // STACK — vertical rhythm via flex column + gap.
322
+ // Usage:
323
+ // .field-group { @include stack; }
324
+ // .form { @include stack($gap: 5); }
325
+ @mixin stack($gap: 4) {
326
+ display: flex;
327
+ flex-direction: column;
328
+ gap: m.space($gap);
329
+ }
330
+
331
+ // CLUSTER — wrapping inline group (pills, tags, button rows).
332
+ // Usage:
333
+ // .toolbar { @include cluster; }
334
+ // .tag-row { @include cluster($gap: 1, $align: center); }
335
+ @mixin cluster($gap: 2, $align: center, $justify: start) {
336
+ display: flex;
337
+ flex-wrap: wrap;
338
+ gap: m.space($gap);
339
+ align-items: $align;
340
+ justify-content: $justify;
341
+ }
342
+
343
+ // SWITCHER — auto switches between row and column based on container width.
344
+ // When container is wider than $threshold, items flow as a row. Below, stack.
345
+ // Container-query aware: works inside any sized parent.
346
+ // Usage:
347
+ // .form-row { @include switcher($threshold: 30rem); }
348
+ @mixin switcher($threshold: 30rem, $gap: 4, $limit: 4) {
349
+ display: flex;
350
+ flex-wrap: wrap;
351
+ gap: m.space($gap);
352
+
353
+ > * {
354
+ flex-grow: 1;
355
+ flex-basis: calc((#{$threshold} - 100%) * 999);
356
+ }
357
+
358
+ > :nth-last-child(n+#{$limit + 1}),
359
+ > :nth-last-child(n+#{$limit + 1}) ~ * {
360
+ flex-basis: 100%;
361
+ }
362
+ }
363
+
364
+ // COVER — hero / full-height section with vertically-centered content.
365
+ // Designated centered child gets margin-block: auto.
366
+ // Usage:
367
+ // <section class="hero">
368
+ // <h1>Title</h1>
369
+ // <div class="cover-center">Centered content</div>
370
+ // <p>Footer note</p>
371
+ // </section>
372
+ //
373
+ // .hero { @include cover; }
374
+ @mixin cover($min-height: 100vh, $center-selector: '.cover-center', $padding: 5) {
375
+ display: flex;
376
+ flex-direction: column;
377
+ min-block-size: $min-height;
378
+ padding: m.space($padding);
379
+
380
+ > * { margin-block: 1rem; }
381
+ > :first-child:not(#{$center-selector}) { margin-block-start: 0; }
382
+ > :last-child:not(#{$center-selector}) { margin-block-end: 0; }
383
+ > #{$center-selector} { margin-block: auto; }
384
+ }
385
+
386
+ // FRAME — aspect-ratio wrapper for media (images, video, iframes).
387
+ // Usage:
388
+ // .video-wrap { @include frame; } → 16:9 default
389
+ // .square { @include frame($ratio: 1); }
390
+ // .portrait { @include frame($ratio: math.div(3, 4)); }
391
+ @mixin frame($ratio: math.div(16, 9)) {
392
+ aspect-ratio: $ratio;
393
+ overflow: hidden;
394
+ display: block;
395
+
396
+ > img,
397
+ > video,
398
+ > iframe {
399
+ inline-size: 100%;
400
+ block-size: 100%;
401
+ object-fit: cover;
402
+ }
403
+ }
404
+
405
+ // ============================================================================
406
+ // FLOW — preset flexbox layer over the existing m.flex() engine
407
+ // ============================================================================
408
+ // One-word switchable flex default + responsive axis switch. Same "presets
409
+ // over one engine" pattern as page-layout for grid — no duplicated property
410
+ // logic; flow() delegates to m.flex().
411
+ //
412
+ // Tier 0 @include m.flow; // sensible flex row
413
+ // Tier 0 @include m.flow(col); // switch row → col → wrap...
414
+ // Tier 0 @include m.flow(row, $at: md, $then: col);// responsive axis switch
415
+ // Tier 1 @include m.stack / m.cluster / m.toolbar // existing semantic names (keep)
416
+ // Tier 2 @include m.flex($direction:.., $justify:..) // existing full-control primitive
417
+ // Tier 3 @include m.flow-switchable; <el data-flow="col"> // runtime switch
418
+ // ============================================================================
419
+
420
+ // Preset registry. Each entry is a complete flex config bundle. Add a mode =
421
+ // one map entry. justify/align use flex()'s shorthand vocabulary.
422
+ $_flow: (
423
+ row: (direction: row, wrap: nowrap, justify: start, align: center),
424
+ col: (direction: column, wrap: nowrap, justify: start, align: stretch),
425
+ wrap: (direction: row, wrap: wrap, justify: start, align: center),
426
+ center: (direction: row, wrap: nowrap, justify: center, align: center),
427
+ between: (direction: row, wrap: nowrap, justify: space-between, align: center),
428
+ around: (direction: row, wrap: nowrap, justify: space-around, align: center),
429
+ );
430
+
431
+ // One-word flex default with an optional breakpoint axis-switch.
432
+ // $preset one of $_flow keys (default: row)
433
+ // $gap space-scale key (default: 4)
434
+ // $at breakpoint key/value — below it, swap to $then
435
+ // $then preset to switch to below $at (e.g. row → col on mobile)
436
+ @mixin flow($preset: row, $gap: 4, $at: null, $then: null) {
437
+ $cfg: map.get($_flow, $preset);
438
+ @if not $cfg {
439
+ @error "Unknown flow '#{$preset}'. Available: #{map.keys($_flow)}";
440
+ }
441
+ @include m.flex(
442
+ $direction: map.get($cfg, direction),
443
+ $wrap: map.get($cfg, wrap),
444
+ $justify: map.get($cfg, justify),
445
+ $align: map.get($cfg, align),
446
+ $gap: $gap
447
+ );
448
+
449
+ @if $at and $then {
450
+ $alt: map.get($_flow, $then);
451
+ @if not $alt {
452
+ @error "Unknown flow '#{$then}'. Available: #{map.keys($_flow)}";
453
+ }
454
+ @include m.media-down($at) {
455
+ @include m.flex(
456
+ $direction: map.get($alt, direction),
457
+ $wrap: map.get($alt, wrap),
458
+ $justify: map.get($alt, justify),
459
+ $align: map.get($alt, align),
460
+ $gap: $gap
461
+ );
462
+ }
463
+ }
464
+ }
465
+
466
+ // Tier 3 (optional) — runtime switch with zero recompile.
467
+ // .bar { @include m.flow-switchable; }
468
+ // <div class="bar" data-flow="between">…</div> // el.dataset.flow = 'col'
469
+ @mixin flow-switchable($gap: 4) {
470
+ @each $name, $cfg in $_flow {
471
+ &[data-flow="#{$name}"] {
472
+ @include m.flex(
473
+ $direction: map.get($cfg, direction),
474
+ $wrap: map.get($cfg, wrap),
475
+ $justify: map.get($cfg, justify),
476
+ $align: map.get($cfg, align),
477
+ $gap: $gap
478
+ );
479
+ }
480
+ }
481
+ }
482
+
483
+ // ============================================================================
484
+ // APP-SHELL FAMILY — wizard-shell + sidebar + toolbar
485
+ // ============================================================================
486
+
487
+ // WIZARD-SHELL — 3-row layout with shared horizontal rhythm.
488
+ // Stepper at top + body in middle + controls at bottom all use the same
489
+ // padding-inline. Body scrolls; header and footer rails are fixed-height.
490
+ //
491
+ // Usage:
492
+ // <div class="my-wizard">
493
+ // <header data-slot="header"><Stepper /></header>
494
+ // <main data-slot="body">{stepContent}</main>
495
+ // <footer data-slot="footer"><BackButton/><NextButton/></footer>
496
+ // </div>
497
+ // .my-wizard { @include wizard-shell; }
498
+ @mixin wizard-shell(
499
+ $min-height: 100dvh,
500
+ $gutter: 6,
501
+ $body-padding: 5,
502
+ $rail-padding: 3,
503
+ $max-width: lg,
504
+ $footer-border: true,
505
+ ) {
506
+ min-block-size: $min-height;
507
+ display: grid;
508
+ grid-template-rows: auto 1fr auto;
509
+
510
+ > [data-slot='header'],
511
+ > [data-slot='body'],
512
+ > [data-slot='footer'] {
513
+ padding-inline: m.space($gutter);
514
+ }
515
+ > [data-slot='header'] {
516
+ padding-block: m.space($rail-padding);
517
+ }
518
+ > [data-slot='body'] {
519
+ padding-block: m.space($body-padding);
520
+ overflow-y: auto;
521
+
522
+ @if $max-width != none {
523
+ $bp: map.get(s.$breakpoints, $max-width) or $max-width;
524
+ display: grid;
525
+ justify-content: center;
526
+ > * {
527
+ max-inline-size: $bp;
528
+ inline-size: 100%;
529
+ }
530
+ }
531
+ }
532
+ > [data-slot='footer'] {
533
+ padding-block: m.space($rail-padding);
534
+ @if $footer-border {
535
+ border-block-start: 1px solid var(--border-subtle, #{map.get(t.$theme-light, border-subtle)});
536
+ }
537
+ }
538
+ }
539
+
540
+ // SIDEBAR — Every-Layout primitive: fixed-width nav + flexible content.
541
+ // Wraps to stack when content falls below $content-min of the container.
542
+ //
543
+ // Usage:
544
+ // <div class="layout">
545
+ // <aside data-slot="side">…</aside>
546
+ // <main data-slot="content">…</main>
547
+ // </div>
548
+ // .layout { @include sidebar; }
549
+ //
550
+ // // Side on the right:
551
+ // .layout { @include sidebar($side: end); }
552
+ @mixin sidebar(
553
+ $side: start,
554
+ $side-width: 16rem,
555
+ $content-min: 50%,
556
+ $gap: 4,
557
+ ) {
558
+ display: flex;
559
+ flex-wrap: wrap;
560
+ gap: m.space($gap);
561
+
562
+ > [data-slot='side'] {
563
+ flex-basis: $side-width;
564
+ flex-grow: 1;
565
+ }
566
+
567
+ > [data-slot='content'] {
568
+ flex-basis: 0;
569
+ flex-grow: 999;
570
+ min-inline-size: $content-min;
571
+ }
572
+
573
+ @if $side == end {
574
+ > [data-slot='side'] { order: 2; }
575
+ }
576
+ }
577
+
578
+ // TOOLBAR — cluster with auto-margin trailing slot.
579
+ // Left group + right group on the same row (e.g. DataTable filters + actions,
580
+ // modal footer with Cancel left / Confirm right).
581
+ //
582
+ // Usage:
583
+ // <div class="my-toolbar">
584
+ // <span>Filters: …</span>
585
+ // <span data-slot="trailing"><Button>Save</Button></span>
586
+ // </div>
587
+ // .my-toolbar { @include toolbar; }
588
+ @mixin toolbar($gap: 3, $align: center) {
589
+ display: flex;
590
+ align-items: $align;
591
+ gap: m.space($gap);
592
+ flex-wrap: wrap;
593
+
594
+ > [data-slot='trailing'] {
595
+ margin-inline-start: auto;
596
+ }
597
+ }