@fest-lib/veela 0.1.1

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 (78) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +533 -0
  3. package/dist/veela.js +878 -0
  4. package/package.json +83 -0
  5. package/src/scss/_layers.scss +16 -0
  6. package/src/scss/advanced/_index.scss +0 -0
  7. package/src/scss/advanced/animation/_motion.scss +317 -0
  8. package/src/scss/advanced/color/_color.scss +328 -0
  9. package/src/scss/advanced/color/_colorize.scss +325 -0
  10. package/src/scss/advanced/design/_design-tokens.scss +118 -0
  11. package/src/scss/advanced/design/_effects.scss +275 -0
  12. package/src/scss/advanced/design/_shadow.scss +359 -0
  13. package/src/scss/advanced/design/_surfaces.scss +76 -0
  14. package/src/scss/advanced/effects/_animations.scss +124 -0
  15. package/src/scss/advanced/effects/_shadows.scss +50 -0
  16. package/src/scss/advanced/font/_typography.scss +600 -0
  17. package/src/scss/advanced/index.scss +11 -0
  18. package/src/scss/advanced/index.ts +35 -0
  19. package/src/scss/advanced/layout/_content.scss +170 -0
  20. package/src/scss/advanced/layout/_index.scss +5 -0
  21. package/src/scss/advanced/layout/_normalize.scss +541 -0
  22. package/src/scss/advanced/layout/_shared-overlays.scss +162 -0
  23. package/src/scss/advanced/md3/_md3.scss +596 -0
  24. package/src/scss/advanced/md3/_utils.scss +114 -0
  25. package/src/scss/advanced/misc/_config.scss +119 -0
  26. package/src/scss/advanced/misc/_initials.scss +298 -0
  27. package/src/scss/advanced/misc/_mixins.scss +179 -0
  28. package/src/scss/advanced/misc/_queries.scss +34 -0
  29. package/src/scss/advanced/misc/_rendering.scss +82 -0
  30. package/src/scss/advanced/misc/_tokens.scss +74 -0
  31. package/src/scss/advanced/theme/_shared.scss +232 -0
  32. package/src/scss/advanced/theme/_states.scss +332 -0
  33. package/src/scss/advanced/theme/_variants.scss +354 -0
  34. package/src/scss/advanced/tokens/_material-color.scss +120 -0
  35. package/src/scss/advanced/tokens/_variables.scss +171 -0
  36. package/src/scss/basic/_layers.scss +0 -0
  37. package/src/scss/basic/design/_index.scss +3 -0
  38. package/src/scss/basic/design/_keyframes.scss +163 -0
  39. package/src/scss/basic/design/_shapes.scss +266 -0
  40. package/src/scss/basic/design/_wavy-runtime.scss +50 -0
  41. package/src/scss/basic/index.scss +19 -0
  42. package/src/scss/basic/index.ts +29 -0
  43. package/src/scss/basic/misc/_core-layout.scss +272 -0
  44. package/src/scss/basic/misc/_index.scss +18 -0
  45. package/src/scss/basic/misc/_layout.scss +1040 -0
  46. package/src/scss/basic/misc/_mixins.scss +179 -0
  47. package/src/scss/basic/misc/_motion.scss +28 -0
  48. package/src/scss/basic/misc/_normalize.scss +765 -0
  49. package/src/scss/basic/misc/_queries.scss +34 -0
  50. package/src/scss/basic/misc/_shared.scss +232 -0
  51. package/src/scss/basic/misc/_states.scss +135 -0
  52. package/src/scss/basic/misc/_tokens.scss +78 -0
  53. package/src/scss/basic/misc/_veela.scss +12 -0
  54. package/src/scss/core/index.scss +1 -0
  55. package/src/scss/core/interact/_draggable.scss +32 -0
  56. package/src/scss/core/interact/_position.scss +45 -0
  57. package/src/scss/core/interact/_ps-anchor.scss +28 -0
  58. package/src/scss/core/interact/_ps-centered.scss +28 -0
  59. package/src/scss/core/interact/_ps-cursor.scss +18 -0
  60. package/src/scss/core/interact/_ps-draggable.scss +25 -0
  61. package/src/scss/core/interact/_ps-mechanic.scss +0 -0
  62. package/src/scss/core/interact/_ps-properties.scss +54 -0
  63. package/src/scss/core/interact/_ps-resizable.scss +61 -0
  64. package/src/scss/core/interact/_resizable.scss +69 -0
  65. package/src/scss/core/interact/_viewport.scss +469 -0
  66. package/src/scss/core/misc/_color-properties.scss +64 -0
  67. package/src/scss/core/misc/_config.scss +110 -0
  68. package/src/scss/core/misc/_functions.scss +530 -0
  69. package/src/scss/core/misc/_icons.scss +109 -0
  70. package/src/scss/core/misc/_index.scss +6 -0
  71. package/src/scss/core/misc/_layout.scss +49 -0
  72. package/src/scss/core/misc/_mixins.scss +744 -0
  73. package/src/scss/core/misc/_properties.scss +245 -0
  74. package/src/scss/core/misc/_tokens.scss +947 -0
  75. package/src/scss/core/misc/_utilities.scss +941 -0
  76. package/src/scss/core/misc/_utils.scss +17 -0
  77. package/src/scss/index.scss +18 -0
  78. package/src/scss/index.ts +105 -0
@@ -0,0 +1,541 @@
1
+ @use "veela-lib" as *;
2
+
3
+ @layer ux-normalize {
4
+
5
+ @keyframes bottom-to-top {
6
+ 0% { opacity: 0; transform: translateY(10%); }
7
+ 100% { opacity: 1; transform: translateY(0); }
8
+ }
9
+
10
+ @layer ux-agate;
11
+
12
+ @media screen {
13
+
14
+ /* -------------------------------------------------------------------------- */
15
+ /* Base */
16
+ /* -------------------------------------------------------------------------- */
17
+
18
+ :where(:root, html) {
19
+ color-scheme: light dark;
20
+ scroll-behavior: smooth;
21
+ tab-size: 4;
22
+ -webkit-text-size-adjust: 100%;
23
+ text-size-adjust: 100%;
24
+ font-size: 16px;
25
+
26
+ min-block-size: 0px;
27
+ min-inline-size: 0px;
28
+ max-inline-size: min(100cqi, 100dvi) !important;
29
+ max-block-size: min(100cqb, 100dvb) !important;
30
+ inline-size: min(100cqi, 100dvi);
31
+ block-size: min(100cqb, 100dvb);
32
+
33
+ contain: strict;
34
+ overflow: hidden;
35
+
36
+ padding: 0px;
37
+ margin: 0px;
38
+ border: none 0px transparent;
39
+ outline: none 0px transparent;
40
+
41
+ @media (prefers-reduced-motion: reduce) {
42
+ scroll-behavior: auto;
43
+ }
44
+
45
+ &, :where(body, button, input, select, textarea) {
46
+ font-family: var(--font-family, inherit);
47
+ }
48
+ }
49
+
50
+ :where(body) {
51
+ margin: 0;
52
+ padding: 0;
53
+
54
+ border: none 0px transparent;
55
+ outline: none 0px transparent;
56
+
57
+ min-block-size: min(100dvb, 100cqb);
58
+ block-size: fit-content;
59
+
60
+ background-color: var(--color-bg);
61
+ color: var(--color-text);
62
+ line-height: var(--line-height);
63
+
64
+ /* Improve text rendering */
65
+ -webkit-font-smoothing: antialiased;
66
+ -moz-osx-font-smoothing: grayscale;
67
+ text-rendering: optimizeLegibility;
68
+
69
+ /* */
70
+ -webkit-text-size-adjust: 100%;
71
+
72
+ @include HQRendering();
73
+ }
74
+
75
+ /* */
76
+ :where(html, body) {
77
+ inset: 0px;
78
+ padding: 0px;
79
+ margin: 0px;
80
+ border: none 0px transparent;
81
+ background-color: transparent;
82
+ color: transparent;
83
+ font-family: Calibri, Carlito, Caliburn, Seravek, 'Gill Sans Nova', Ubuntu, 'DejaVu Sans', Candara, Segoe, "Segoe UI", Optima, source-sans-pro, sans-serif;
84
+ dynamic-range-limit: constrained-high;
85
+ }
86
+
87
+ /* -------------------------------------------------------------------------- */
88
+ /* Global Defaults */
89
+ /* -------------------------------------------------------------------------- */
90
+
91
+ :where(:host, :scope, :root) {
92
+
93
+ /* */
94
+ :where(*), :where(*::before), :where(*::after) {
95
+ box-sizing: border-box;
96
+ font-display: swap;
97
+
98
+ /* don't needs such potential lag-machine */
99
+ /*border-width: 0;
100
+ border-style: solid;
101
+ border-color: var(--color-border, currentColor);
102
+ word-wrap: break-word;*/
103
+ }
104
+
105
+ /* */
106
+ :where(*):not(:defined) { opacity: 0; visibility: collapse; pointer-events: none; display: none; background-color: transparent; color: transparent; };
107
+
108
+ /* */
109
+ :where(*), &::slotted(*) {
110
+ /* Interaction defaults */
111
+ -webkit-tap-highlight-color: transparent;
112
+ }
113
+
114
+ /* */
115
+ :where(input):-webkit-autofill::first-line { font-size: 1em; -webkit-text-size-adjust: 100%; }
116
+ :where(input):autofill::first-line { font-size: 1em; text-size-adjust: 100%; }
117
+ :where(input):auto-fill::first-line { font-size: 1em; text-size-adjust: 100%; }
118
+
119
+ /* */
120
+ :where(input) {
121
+ position: relative;
122
+ text-size-adjust: 100%;
123
+ -webkit-text-size-adjust: 100%;
124
+
125
+ &:-internal-autofill-previewed {
126
+ letter-spacing: #{"calc(1em / 10)"} !important;
127
+ }
128
+ }
129
+
130
+ /* */
131
+ :where(button, a, input, textarea) {
132
+ border: none 0px transparent;
133
+ }
134
+
135
+ /* Media defaults */
136
+ :where(img, video, canvas, svg, picture) {
137
+ display: block;
138
+ max-inline-size: stretch;
139
+ block-size: auto;
140
+ shape-margin: 0.75rem;
141
+ max-block-size: stretch;
142
+ }
143
+
144
+ /* Media defaults */
145
+ :where(img, video) { object-fit: contain; object-position: center; }
146
+ :where(picture) { display: contents; }
147
+
148
+ /* Typography defaults */
149
+ :where(h1, h2, h3, h4, h5, h6) {
150
+ font-weight: bold;
151
+ text-wrap: balance;
152
+ margin-block: 0.5em;
153
+ }
154
+
155
+ :where(p) {
156
+ text-wrap: pretty;
157
+ margin-block: 1em;
158
+ }
159
+
160
+ /* */
161
+ :where(a) {
162
+ color: inherit;
163
+ text-decoration: inherit;
164
+ text-underline-offset: 0.2em;
165
+ pointer-events: auto;
166
+ cursor: pointer;
167
+ user-select: none;
168
+ touch-action: none;
169
+ }
170
+
171
+ /* avoid side effects on labels by default */
172
+ :where(label) {
173
+ user-select: none;
174
+ touch-action: none;
175
+ pointer-events: none;
176
+ }
177
+
178
+ /* button interactions fixtures */
179
+ :where(button) {
180
+ user-select: none;
181
+ touch-action: none;
182
+ pointer-events: auto;
183
+ cursor: pointer;
184
+
185
+ /* if button has only icon */
186
+ &:has(> ui-icon:only-child) {
187
+ place-content: center;
188
+ place-items: center;
189
+ text-align: center;
190
+ font-size: 0;
191
+ line-height: 0;
192
+ letter-spacing: 0;
193
+ aspect-ratio: 1 / 1;
194
+ gap: 0;
195
+ }
196
+
197
+ /* clean up empty elements */
198
+ & :where(span, a, label):empty {
199
+ display: none;
200
+ }
201
+
202
+ /* */
203
+ :where(button) {
204
+ pointer-events: auto;
205
+ }
206
+
207
+ /* interaction defaults */
208
+ :where(span, a, label) {
209
+ user-select: none;
210
+ touch-action: none;
211
+ }
212
+
213
+ /* interaction defaults */
214
+ :where(span, label) {
215
+ pointer-events: none;
216
+ }
217
+
218
+ /* interaction defaults */
219
+ :where(a) {
220
+ pointer-events: auto;
221
+ text-decoration: none;
222
+ }
223
+
224
+ /* interaction defaults */
225
+ &:disabled {
226
+ pointer-events: none;
227
+ user-select: none;
228
+ touch-action: none;
229
+ cursor: not-allowed;
230
+ }
231
+ }
232
+
233
+ /* Form defaults */
234
+ :where(input, button, textarea, select) {
235
+ font: inherit;
236
+ color: inherit;
237
+ letter-spacing: inherit;
238
+ }
239
+
240
+ /* Optimization */
241
+ :where(img, video, audio, picture, canvas, h1, h2, h3, h4, h5, h6, p, form, label, button) {
242
+ content-visibility: auto;
243
+ }
244
+
245
+ :where(div, span, a, h1, h2, h3, h4, h5, h6, p, label, form):empty:not([class]) {
246
+ content-visibility: hidden;
247
+ }
248
+
249
+ /* Hidden Elements */
250
+ :where(link, head, script, style, meta) {
251
+ display: none !important;
252
+ pointer-events: none !important;
253
+ interactivity: inert !important;
254
+ user-select: none !important;
255
+ touch-action: none !important;
256
+ }
257
+
258
+ /* just hidden without important */
259
+ [hidden] { display: none; }
260
+
261
+ /* aria-hidden="true" */
262
+ [aria-hidden="true"] {
263
+ visibility: collapse;
264
+ pointer-events: none;
265
+ touch-action: none;
266
+ user-select: none;
267
+ content-visibility: auto;
268
+ opacity: 0;
269
+ }
270
+
271
+ [data-dragging] {
272
+ will-change: transform;
273
+ cursor: grabbing;
274
+ }
275
+
276
+ /* Reduced Motion */
277
+ @media (prefers-reduced-motion: reduce) {
278
+ :where(*, ::after, ::before) {
279
+ animation-duration: 0.01ms !important;
280
+ animation-iteration-count: 1 !important;
281
+ transition-duration: 0.01ms !important;
282
+ scroll-behavior: auto !important;
283
+ }
284
+ }
285
+
286
+ /* Scrollbars */
287
+ :where(*) {
288
+ scrollbar-width: thin;
289
+ scrollbar-color: var(--color-scrollbar, currentColor) transparent;
290
+
291
+ &::-webkit-scrollbar {
292
+ inline-size: var(--scrollbar-size);
293
+ block-size: var(--scrollbar-size);
294
+ }
295
+
296
+ &::-webkit-scrollbar-track { background: transparent; }
297
+ &::-webkit-scrollbar-thumb {
298
+ background-color: var(--color-scrollbar, currentColor);
299
+ border-radius: var(--border-radius);
300
+ }
301
+ }
302
+ }
303
+
304
+ /* -------------------------------------------------------------------------- */
305
+ /* Layout */
306
+ /* -------------------------------------------------------------------------- */
307
+
308
+ :where(footer, header, main) {
309
+ margin-inline: auto;
310
+ padding: 0px;
311
+ }
312
+
313
+ :where(section) {
314
+ display: flex;
315
+ flex-wrap: wrap;
316
+ justify-content: var(--justify-important, center);
317
+ gap: 1rem;
318
+
319
+ :where(aside) {
320
+ border: 1px solid var(--color-bg-secondary);
321
+ border-radius: var(--border-radius);
322
+ box-shadow: var(--box-shadow);
323
+ padding: 1.25rem;
324
+ inline-size: var(--width-card);
325
+ flex: 1 1 var(--width-card);
326
+ }
327
+ }
328
+
329
+ /* -------------------------------------------------------------------------- */
330
+ /* Components */
331
+ /* -------------------------------------------------------------------------- */
332
+
333
+ /* Header & Nav */
334
+ :where(header) {
335
+ text-align: center;
336
+ }
337
+
338
+ :where(nav) {
339
+ display: flex;
340
+ align-items: center;
341
+ justify-content: space-between;
342
+ flex-wrap: wrap;
343
+ margin-block-end: 0px;
344
+
345
+ ul {
346
+ display: flex;
347
+ list-style: none;
348
+ padding: 0;
349
+ gap: 1rem;
350
+ margin: 0;
351
+
352
+ li { position: relative; }
353
+ }
354
+
355
+ a {
356
+ font-weight: bold;
357
+ color: var(--color-link);
358
+ text-decoration: none;
359
+
360
+ @include when-active { text-decoration: underline; }
361
+ }
362
+ }
363
+
364
+ /* Code blocks */
365
+ :where(pre) {
366
+ background-color: var(--bgColor-muted);
367
+ border-radius: var(--border-radius);
368
+ border: 1px solid var(--borderColor-muted);
369
+ padding: 1rem;
370
+ overflow: auto;
371
+ font-family: var(--font-family-mono);
372
+ max-inline-size: 100%;
373
+
374
+ code {
375
+ background: transparent;
376
+ padding: 0;
377
+ border-radius: 0;
378
+ }
379
+ }
380
+
381
+ :where(code, samp, kbd) {
382
+ font-family: var(--font-family-mono);
383
+ background-color: var(--bgColor-muted);
384
+ border-radius: 0.3em;
385
+ padding: 0.2em 0.4em;
386
+ font-size: 85%;
387
+ }
388
+
389
+ /* Blockquote */
390
+ :where(blockquote) {
391
+ margin-inline: 1rem;
392
+ padding-inline: 1rem;
393
+ border-inline-start: 0.25rem solid var(--color-secondary);
394
+ color: var(--color-text-secondary);
395
+ }
396
+
397
+ /* Tables */
398
+ :where(table) {
399
+ display: block;
400
+ overflow-x: auto;
401
+ border-collapse: collapse;
402
+ border-spacing: 0;
403
+ inline-size: max-content;
404
+ max-inline-size: 100%;
405
+ margin-block: 1rem;
406
+ border: 1px solid var(--color-border);
407
+ border-radius: var(--border-radius);
408
+
409
+ :where(th, td) {
410
+ padding: 0.5rem 1rem;
411
+ border-block-end: 1px solid var(--color-border);
412
+ text-align: start;
413
+ }
414
+
415
+ :where(th) {
416
+ background-color: var(--color-table);
417
+ color: var(--color-text);
418
+ font-weight: bold;
419
+ }
420
+
421
+ :where(tr:last-child td) {
422
+ border-block-end: none;
423
+ }
424
+
425
+ :where(tr:nth-child(even)) {
426
+ background-color: var(--color-bg-secondary);
427
+ }
428
+ }
429
+
430
+ /* Links & Buttons */
431
+ :where(a) {
432
+ color: var(--color-link);
433
+ }
434
+
435
+ :where(button, input[type="submit"], input[type="button"]) {
436
+ display: inline-flex;
437
+ align-items: center;
438
+ justify-content: center;
439
+ background-color: var(--color-link);
440
+ border: 1px solid transparent;
441
+ border-radius: var(--border-radius);
442
+ padding: 0.5rem 1rem;
443
+ font-weight: 600;
444
+ cursor: pointer;
445
+ transition: filter 0.2s ease, transform 0.1s ease;
446
+
447
+ &:disabled {
448
+ opacity: 0.6;
449
+ cursor: not-allowed;
450
+ filter: none;
451
+ background-color: var(--color-secondary);
452
+ }
453
+ }
454
+
455
+ /* Forms */
456
+ :where(input:not([type="submit"]):not([type="button"]), select, textarea) {
457
+ display: block;
458
+ inline-size: 100%;
459
+ padding: 0.5rem;
460
+ background-color: var(--color-bg);
461
+ border: 1px solid var(--color-border);
462
+ border-radius: var(--border-radius);
463
+ color: var(--color-text);
464
+ /*margin-block-end: 1rem;*/
465
+
466
+ @include when-focus-visible {
467
+ outline: 2px solid var(--color-link);
468
+ outline-offset: -1px;
469
+ }
470
+
471
+ &::placeholder {
472
+ color: var(--color-text-secondary);
473
+ opacity: 0.7;
474
+ }
475
+
476
+ &:disabled {
477
+ background-color: var(--color-bg-secondary);
478
+ cursor: not-allowed;
479
+ }
480
+ }
481
+
482
+ :where(label) {
483
+ display: block;
484
+ font-weight: 600;
485
+ margin-block-end: 0.25rem;
486
+ }
487
+
488
+ /* Dialog */
489
+ :where(dialog) {
490
+ margin: auto;
491
+ padding: 1rem;
492
+ background: var(--color-bg);
493
+ color: var(--color-text);
494
+ border: 1px solid var(--color-border);
495
+ border-radius: var(--border-radius);
496
+ box-shadow: var(--box-shadow);
497
+ max-inline-size: min(90vw, 600px);
498
+ max-block-size: 85vh;
499
+
500
+ &::backdrop {
501
+ background-color: rgb(0 0 0 / 0.5);
502
+ backdrop-filter: blur(2px);
503
+ }
504
+
505
+ &[open] {
506
+ animation: bottom-to-top 0.25s ease-out;
507
+ }
508
+ }
509
+
510
+ /* -------------------------------------------------------------------------- */
511
+ /* Visuals */
512
+ /* -------------------------------------------------------------------------- */
513
+
514
+ :where(canvas):is([is="ui-canvas"]) {
515
+ position: absolute;
516
+ inset: 0;
517
+ pointer-events: none;
518
+ z-index: 0;
519
+ padding: 0px;
520
+ margin: 0px;
521
+ border: none 0px transparent;
522
+ outline: none 0px transparent;
523
+
524
+ inline-size: stretch;
525
+ block-size: stretch;
526
+ max-inline-size: min(100%, min(100cqi, 100dvi)) !important;
527
+ max-block-size: min(100%, min(100cqb, 100dvb)) !important;
528
+ box-sizing: border-box !important;
529
+ min-inline-size: 0px;
530
+ min-block-size: 0px;
531
+
532
+ object-fit: cover;
533
+ object-position: center;
534
+ }
535
+ }
536
+
537
+ /* Scheme Overrides */
538
+ [data-scheme="dark"], [data-theme="dark"] { color-scheme: dark only; & * { color-scheme: dark; } }
539
+ [data-scheme="light"], [data-theme="light"] { color-scheme: light only; & * { color-scheme: light; } }
540
+ [data-scheme="system"], [data-theme="system"] { color-scheme: light dark; }
541
+ }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Shared Overlay & State Patterns
3
+ *
4
+ * Consolidates button and form state overlay mixins that were previously
5
+ * duplicated across _buttons.scss, _forms.scss, and _colorize.scss.
6
+ *
7
+ * Provides reusable patterns for:
8
+ * - State overlays (hover, press, active, focus)
9
+ * - Focus rings with consistent styling
10
+ * - Icon button sizing
11
+ */
12
+
13
+ @use "sass:map";
14
+ @use "./utils" as utils;
15
+ @use "./color" as color;
16
+
17
+ // ============================================================================
18
+ // Shared state opacity defaults (used across buttons, forms, inputs)
19
+ // ============================================================================
20
+
21
+ $state-opacity-defaults: (
22
+ hover: 0.08,
23
+ press: 0.12,
24
+ focus: 0.12,
25
+ disabled: 0.38,
26
+ drag: 0.16,
27
+ ) !default;
28
+
29
+ // ============================================================================
30
+ // Base overlay structure (shared placeholder for reusability)
31
+ // ============================================================================
32
+
33
+ %overlay-base {
34
+ content: "";
35
+ position: absolute;
36
+ inset: 0;
37
+ border-radius: inherit;
38
+ pointer-events: none;
39
+ mix-blend-mode: normal;
40
+ }
41
+
42
+ // ============================================================================
43
+ // Mixin: State overlay (for buttons, forms, and interactive elements)
44
+ // ============================================================================
45
+
46
+ ///
47
+ /// Creates state-aware overlays for hover and press states.
48
+ /// Used by buttons, form controls, and other interactive elements.
49
+ ///
50
+ /// @param {color} $fallback-color - Base color for overlay calculation
51
+ /// @param {number} $hover-opacity - Hover state opacity (default: var(--state-opacity-hover, 0.08))
52
+ /// @param {number} $press-opacity - Press/active state opacity (default: var(--state-opacity-press, 0.12))
53
+ ///
54
+ @mixin state-overlay(
55
+ $fallback-color,
56
+ $hover-opacity: var(--state-opacity-hover, 0.08),
57
+ $press-opacity: var(--state-opacity-press, 0.12)
58
+ ) {
59
+ position: relative;
60
+ overflow: hidden;
61
+
62
+ &::after,
63
+ &::before {
64
+ @extend %overlay-base;
65
+ }
66
+
67
+ // Hover overlay
68
+ &::after {
69
+ opacity: 0;
70
+ transition: opacity var(--duration-normal) var(--ease-out);
71
+ background-color: color-mix(
72
+ in oklch,
73
+ $fallback-color calc(#{$hover-opacity} * 100%),
74
+ transparent
75
+ );
76
+ }
77
+
78
+ // Press/active overlay
79
+ &::before {
80
+ opacity: 0;
81
+ transition: opacity var(--duration-fast) var(--ease-out);
82
+ background-color: color-mix(
83
+ in oklch,
84
+ $fallback-color calc(#{$press-opacity} * 100%),
85
+ transparent
86
+ );
87
+ }
88
+
89
+ // Apply overlays on interaction
90
+ @include utils.when-hover { &::after { opacity: 1; } }
91
+ @include utils.when-active { &::before { opacity: 1; } }
92
+ }
93
+
94
+ // ============================================================================
95
+ // Mixin: Focus ring (accessible focus indicator)
96
+ // ============================================================================
97
+
98
+ ///
99
+ /// Creates an accessible focus ring with consistent styling.
100
+ /// Used across buttons, form inputs, and interactive elements.
101
+ ///
102
+ /// @param {color} $state-color - Color for focus ring (default: var(--md-button-state-color, var(--md-primary)))
103
+ /// @param {number} $opacity - Focus opacity (default: var(--state-opacity-focus, 0.12))
104
+ ///
105
+ @mixin focus-ring(
106
+ $state-color: var(--md-button-state-color, var(--md-primary)),
107
+ $opacity: var(--state-opacity-focus, 0.12)
108
+ ) {
109
+ @include utils.when-focus {
110
+ outline: 2px solid color-mix(
111
+ in oklch,
112
+ $state-color calc(#{$opacity} * 100%),
113
+ transparent
114
+ );
115
+ outline-offset: 3px;
116
+ }
117
+ }
118
+
119
+ // ============================================================================
120
+ // Mixin: Icon button sizing (square button for icon-only buttons)
121
+ // ============================================================================
122
+
123
+ ///
124
+ /// Sizes icon-only buttons as squares with centered content.
125
+ /// Commonly used for toolbar buttons, FABs, and icon-only controls.
126
+ ///
127
+ /// @param {length} $size - Button size (default: var(--icon-size-lg))
128
+ ///
129
+ @mixin icon-button(
130
+ $size: var(--icon-size-lg)
131
+ ) {
132
+ min-inline-size: $size;
133
+ min-block-size: $size;
134
+ inline-size: $size;
135
+ block-size: $size;
136
+ padding: 0;
137
+ border-radius: 50%;
138
+ justify-content: center;
139
+
140
+ & > :where(ui-icon, svg) {
141
+ inline-size: 60%;
142
+ block-size: 60%;
143
+ }
144
+ }
145
+
146
+ // ============================================================================
147
+ // Mixin: Export state opacity tokens
148
+ // ============================================================================
149
+
150
+ ///
151
+ /// Exports state opacity values as CSS custom properties.
152
+ /// Centralizes opacity values for buttons, forms, and other interactive elements.
153
+ ///
154
+ /// @param {map} $map - Opacity map (default: $state-opacity-defaults)
155
+ ///
156
+ @mixin export-state-opacity-vars(
157
+ $map: $state-opacity-defaults
158
+ ) {
159
+ @each $state, $value in $map {
160
+ --state-opacity-#{$state}: #{$value};
161
+ }
162
+ }