@vialiq/flux-ui 0.19.0 → 0.21.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.
@@ -0,0 +1,574 @@
1
+ @use '@vialiq/flux-ui/styles/variables' as tokens;
2
+ @use '../styles/functions' as func;
3
+
4
+ @layer components {
5
+
6
+ // ─────────────────────────────────────────────────────────────────────────────
7
+ // vi-tabs — Container, tablist, SLIDING CURSOR, indicator, all variants
8
+ //
9
+ // Architecture:
10
+ // • .vi-tabs__cursor → absolutely positioned, slides between tabs (JS-positioned)
11
+ // Used by: pill, card, enclosed (the "selected surface")
12
+ // • .vi-tabs__indicator → absolutely positioned thin line at bottom/side (JS-positioned)
13
+ // Used by: line, secondary (the "underline signal")
14
+ //
15
+ // The cursor element ALWAYS exists in the DOM. CSS on each variant controls
16
+ // whether it's visible (opacity, border-radius, shadow etc.)
17
+ // ─────────────────────────────────────────────────────────────────────────────
18
+
19
+
20
+ .vi-tabs {
21
+ display: grid;
22
+ grid-template-columns: minmax(0, 1fr);
23
+ position: relative;
24
+ }
25
+
26
+ .vi-tabs__scroll-btn {
27
+ grid-area: 1 / 1;
28
+ align-self: stretch;
29
+ display: flex;
30
+ align-items: center;
31
+ justify-content: center;
32
+ width: 48px; // Wider to accommodate the gradient safely
33
+ z-index: 10;
34
+ border: none;
35
+ color: var(--vi-tabs-text-color, #{tokens.$text-primary});
36
+ cursor: pointer;
37
+ transition: opacity 0.2s;
38
+
39
+ &[disabled] {
40
+ opacity: 0;
41
+ pointer-events: none;
42
+ }
43
+
44
+ &:hover, &:focus-visible {
45
+ color: var(--vi-tabs-primary-color, #{tokens.$color-primary});
46
+ outline: none;
47
+ }
48
+
49
+ &--left {
50
+ justify-self: start;
51
+ background: linear-gradient(to right, var(--vi-tabs-surface-primary, #{tokens.$layer-01}) 50%, transparent 100%);
52
+ padding-right: 12px; // Push icon slightly left
53
+ }
54
+
55
+ &--right {
56
+ justify-self: end;
57
+ background: linear-gradient(to left, var(--vi-tabs-surface-primary, #{tokens.$layer-01}) 50%, transparent 100%);
58
+ padding-left: 12px; // Push icon slightly right
59
+ }
60
+ }
61
+
62
+
63
+
64
+ // ─────────────────────────────────────────────────────────────────────────────
65
+ // Add Button
66
+ // ─────────────────────────────────────────────────────────────────────────────
67
+
68
+ .vi-tabs__add-btn {
69
+ display: flex;
70
+ align-items: center;
71
+ justify-content: center;
72
+ width: 48px;
73
+ height: 48px;
74
+ flex-shrink: 0;
75
+ background: transparent;
76
+ border: none;
77
+ border-radius: 50%;
78
+ color: var(--vi-tabs-add-btn-color, #{tokens.$text-secondary});
79
+ cursor: pointer;
80
+ margin-left: 4px;
81
+ outline: none;
82
+ position: relative;
83
+ transition: background-color 0.2s, color 0.2s;
84
+ order: 9998; // Ensure it stays right before the More menu
85
+
86
+ &::before {
87
+ content: '';
88
+ position: absolute;
89
+ inset: 4px;
90
+ border-radius: 50%;
91
+ background-color: currentColor;
92
+ opacity: 0;
93
+ transition: opacity 0.2s;
94
+ }
95
+
96
+ &:hover::before,
97
+ &:focus-visible::before {
98
+ opacity: 0.08;
99
+ }
100
+
101
+ &:active::before {
102
+ opacity: 0.12;
103
+ }
104
+
105
+ &:focus-visible {
106
+ border-radius: #{func.to-rem(4px)}; // Square focus ring for accessibility
107
+ outline: 2px solid var(--vi-tabs-focus-ring, #{tokens.$color-primary});
108
+ outline-offset: 2px;
109
+ }
110
+
111
+ svg {
112
+ position: relative; // Above the hover state
113
+ }
114
+ }
115
+
116
+ // ─────────────────────────────────────────────────────────────────────────────
117
+ // Shared tablist base
118
+ // ─────────────────────────────────────────────────────────────────────────────
119
+
120
+ .vi-tabs__tablist {
121
+ grid-area: 1 / 1;
122
+ position: relative;
123
+ display: flex;
124
+ flex-direction: row;
125
+ align-items: stretch;
126
+ gap: 0;
127
+ // overflow: visible so overflow-menu dropdown is not clipped.
128
+ // Each variant overrides this as needed.
129
+ overflow: visible;
130
+ min-width: 0;
131
+ }
132
+
133
+ // ─────────────────────────────────────────────────────────────────────────────
134
+ // SLIDING CURSOR — the magic that makes this feel premium
135
+ //
136
+ // Positioned absolutely by JS. Each variant styles it differently.
137
+ // ─────────────────────────────────────────────────────────────────────────────
138
+
139
+ .vi-tabs__cursor {
140
+ position: absolute;
141
+ top: 0;
142
+ left: 0;
143
+ pointer-events: none;
144
+ opacity: 0;
145
+ z-index: 0;
146
+
147
+ // Transitions — fast out slow in, matching motion system
148
+ transition:
149
+ left 220ms cubic-bezier(0.4, 0, 0.2, 1),
150
+ top 220ms cubic-bezier(0.4, 0, 0.2, 1),
151
+ width 220ms cubic-bezier(0.4, 0, 0.2, 1),
152
+ height 220ms cubic-bezier(0.4, 0, 0.2, 1),
153
+ opacity 120ms ease;
154
+ }
155
+
156
+ // ─────────────────────────────────────────────────────────────────────────────
157
+ // BOTTOM-LINE INDICATOR — for line / secondary variants
158
+ // ─────────────────────────────────────────────────────────────────────────────
159
+
160
+ .vi-tabs__indicator {
161
+ position: absolute;
162
+ bottom: 0;
163
+ left: 0;
164
+ height: var(--vi-tabs-indicator-thickness, 2px);
165
+ width: 0;
166
+ background: var(--vi-tabs-indicator-color, #{tokens.$color-primary});
167
+ border-radius: #{func.to-rem(2px)} #{func.to-rem(2px)} 0 0;
168
+ opacity: 0;
169
+ pointer-events: none;
170
+ z-index: 2;
171
+
172
+ transition:
173
+ left 220ms cubic-bezier(0.4, 0, 0.2, 1),
174
+ width 220ms cubic-bezier(0.4, 0, 0.2, 1),
175
+ top 220ms cubic-bezier(0.4, 0, 0.2, 1),
176
+ height 220ms cubic-bezier(0.4, 0, 0.2, 1),
177
+ opacity 120ms ease;
178
+ }
179
+
180
+ // ─────────────────────────────────────────────────────────────────────────────
181
+ // VARIANT: line (default) — clean underline, minimal strip
182
+ //
183
+ // Design reference: Vercel, Linear, GitHub navigation
184
+ // Strip: barely-there border, no bg. Typography + indicator do the work.
185
+ // ─────────────────────────────────────────────────────────────────────────────
186
+
187
+ .vi-tabs--line {
188
+ .vi-tabs__tablist {
189
+ border-bottom: 1px solid var(--vi-tabs-border-color, #{tokens.$color-grey-200});
190
+ background: transparent;
191
+ padding: 0;
192
+ gap: 0;
193
+ }
194
+
195
+ // Cursor hidden — line variant uses indicator only
196
+ .vi-tabs__cursor {
197
+ display: none;
198
+ }
199
+
200
+ // Indicator: solid 2px line
201
+ .vi-tabs__indicator {
202
+ bottom: -1px;
203
+ height: 2px;
204
+ background: var(--vi-tabs-primary-color, #{tokens.$color-primary});
205
+ border-radius: #{func.to-rem(2px)} #{func.to-rem(2px)} 0 0;
206
+ box-shadow: 0 0 0 0 transparent; // no glow on line variant — keep clean
207
+ }
208
+ }
209
+
210
+ // ─────────────────────────────────────────────────────────────────────────────
211
+ // VARIANT: pill — segmented control / HeroUI / shadcn style
212
+ //
213
+ // Design reference: Apple iOS, shadcn/ui, HeroUI
214
+ // The cursor IS the active pill — white card that slides between tabs.
215
+ // Container: inset-shadow tray. Active: white card with shadow.
216
+ // ─────────────────────────────────────────────────────────────────────────────
217
+
218
+ .vi-tabs--pill {
219
+ .vi-tabs__tablist {
220
+ background: var(--vi-tabs-pill-bg, #{tokens.$color-grey-100});
221
+ border: 1px solid var(--vi-tabs-border-color, #{tokens.$color-border});
222
+ border-radius: #{func.to-rem(10px)};
223
+ padding: var(--vi-tabs-spacing-xs, 4px);
224
+ gap: var(--vi-tabs-gap-xs, 2px);
225
+ // Inset shadow creates a "recessed tray" feel
226
+ box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.07), inset 0 1px 2px rgba(0, 0, 0, 0.04);
227
+ }
228
+
229
+ // The sliding white pill cursor
230
+ .vi-tabs__cursor {
231
+ display: block;
232
+ inset: 4px auto; // matches tablist padding
233
+ border-radius: #{func.to-rem(7px)};
234
+ background: var(--vi-tabs-surface-primary, #{tokens.$layer-01});
235
+ // Multi-layer shadow for floating card feel
236
+ box-shadow:
237
+ 0 1px 3px rgba(0, 0, 0, 0.10),
238
+ 0 1px 2px rgba(0, 0, 0, 0.06),
239
+ 0 0 0 1px rgba(0, 0, 0, 0.04);
240
+ }
241
+
242
+ // Indicator hidden — cursor does selection work
243
+ .vi-tabs__indicator {
244
+ display: none;
245
+ }
246
+ }
247
+
248
+ // ─────────────────────────────────────────────────────────────────────────────
249
+ // VARIANT: secondary (M3 — sub-hierarchy, thinner indicator)
250
+ // ─────────────────────────────────────────────────────────────────────────────
251
+
252
+ .vi-tabs--secondary {
253
+ .vi-tabs__tablist {
254
+ border-bottom: 1px solid var(--vi-tabs-border-color, #{tokens.$color-border});
255
+ background: transparent;
256
+ padding: 0;
257
+ }
258
+
259
+ .vi-tabs__cursor { display: none; }
260
+
261
+ .vi-tabs__indicator {
262
+ bottom: -1px;
263
+ height: 2px;
264
+ background: var(--vi-tabs-primary-color, #{tokens.$color-primary});
265
+ border-radius: #{func.to-rem(2px)} #{func.to-rem(2px)} 0 0;
266
+ }
267
+ }
268
+
269
+ // ─────────────────────────────────────────────────────────────────────────────
270
+ // VARIANT: card — browser-tab file-folder aesthetic
271
+ //
272
+ // Design reference: browser tabs, VS Code editor tabs
273
+ // Active tab: white surface merges with content area below.
274
+ // The cursor element becomes the white card + side borders.
275
+ // ─────────────────────────────────────────────────────────────────────────────
276
+
277
+ .vi-tabs--card {
278
+ .vi-tabs__tablist {
279
+ background: var(--vi-tabs-surface-secondary, #{tokens.$layer-02});
280
+ border-bottom: 1px solid var(--vi-tabs-border-color, #{tokens.$color-border});
281
+ padding: var(--vi-tabs-spacing-md-md-none, 8px 8px 0);
282
+ gap: var(--vi-tabs-gap-sm, 4px);
283
+ align-items: flex-end;
284
+ }
285
+
286
+ // The cursor is the active card surface
287
+ .vi-tabs__cursor {
288
+ $surface-color: var(--vi-tabs-surface-primary, #{tokens.$layer-01});
289
+ display: block;
290
+ border-radius: #{func.to-rem(8px)} #{func.to-rem(8px)} 0 0;
291
+ background: $surface-color;
292
+ border: 1px solid var(--vi-tabs-border-color, #{tokens.$color-border});
293
+ border-bottom-color: $surface-color; // Merges with content below
294
+ margin-top: 8px;
295
+ box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);
296
+ }
297
+
298
+ .vi-tabs__indicator { display: none; }
299
+ }
300
+
301
+ // ─────────────────────────────────────────────────────────────────────────────
302
+ // VARIANT: enclosed — bordered box with shared header/content border
303
+ //
304
+ // Design reference: Mantine, Chakra UI enclosed tabs
305
+ // The entire component lives in a border box. Active tab has indicator only.
306
+ // ─────────────────────────────────────────────────────────────────────────────
307
+
308
+ .vi-tabs--enclosed {
309
+ border: 1px solid var(--vi-tabs-border-color, #{tokens.$color-border});
310
+ border-radius: #{func.to-rem(10px)};
311
+ overflow: clip; // Enclosed: clip cursor inside rounded border box
312
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
313
+
314
+ .vi-tabs__tablist {
315
+ background: var(--vi-tabs-surface-secondary, #{tokens.$layer-02});
316
+ border-bottom: 1px solid var(--vi-tabs-border-color, #{tokens.$color-border});
317
+ padding: 0;
318
+ gap: 0;
319
+ }
320
+
321
+ // Cursor: subtle active tab highlight within the enclosed strip
322
+ .vi-tabs__cursor {
323
+ display: block;
324
+ top: 0;
325
+ border-radius: 0;
326
+ background: rgba(#{tokens.$color-primary}, 0.06);
327
+ border-right: 1px solid var(--vi-tabs-border-color, #{tokens.$color-border});
328
+ border-left: 1px solid var(--vi-tabs-border-color, #{tokens.$color-border});
329
+ }
330
+
331
+ // Thin indicator at bottom of strip header
332
+ .vi-tabs__indicator {
333
+ bottom: 0;
334
+ height: 3px;
335
+ background: var(--vi-tabs-primary-color, #{tokens.$color-primary});
336
+ box-shadow: 0 0 8px color-mix(in srgb, #{tokens.$color-primary} 50%, transparent);
337
+ border-radius: 0;
338
+ }
339
+
340
+ ::slotted(vi-tab) {
341
+ border-right: 1px solid var(--vi-tabs-border-color, #{tokens.$color-border}) !important;
342
+ min-height: 44px !important;
343
+ }
344
+ }
345
+
346
+ // ─────────────────────────────────────────────────────────────────────────────
347
+ // ORIENTATION: vertical (sidebar settings pattern)
348
+ // ─────────────────────────────────────────────────────────────────────────────
349
+
350
+ .vi-tabs--vertical {
351
+ grid-template-columns: auto minmax(0, 1fr);
352
+ align-items: stretch;
353
+
354
+ .vi-tabs__tablist {
355
+ flex-direction: column;
356
+ align-items: stretch;
357
+ background: var(--vi-tabs-surface-secondary, #{tokens.$layer-02});
358
+ border-right: 1px solid var(--vi-tabs-border-color, #{tokens.$color-border});
359
+ border-bottom: none;
360
+ padding: var(--vi-tabs-spacing-sm-xs, 6px 4px);
361
+ min-width: 168px;
362
+ gap: var(--vi-tabs-gap-xs, 2px);
363
+ }
364
+
365
+ // Cursor becomes the sliding bg highlight in vertical mode
366
+ .vi-tabs__cursor {
367
+ display: block;
368
+ left: 4px !important;
369
+ right: 4px;
370
+ width: calc(100% - 8px) !important; // Override JS width — fills sidebar
371
+ border-radius: #{func.to-rem(8px)};
372
+ background: var(--vi-tabs-surface-primary, #{tokens.$layer-01});
373
+ box-shadow:
374
+ 0 1px 3px rgba(0, 0, 0, 0.08),
375
+ 0 0 0 1px rgba(0, 0, 0, 0.04);
376
+ }
377
+
378
+ // Vertical indicator — right edge accent line
379
+ .vi-tabs__indicator {
380
+ bottom: auto;
381
+ top: 0;
382
+ left: auto;
383
+ right: 0;
384
+ width: 3px !important;
385
+ height: 0;
386
+ border-radius: #{func.to-rem(3px)} 0 0 #{func.to-rem(3px)};
387
+ background: var(--vi-tabs-primary-color, #{tokens.$color-primary});
388
+ box-shadow: 0 0 6px color-mix(in srgb, #{tokens.$color-primary} 40%, transparent);
389
+ }
390
+
391
+ ::slotted(vi-tab) {
392
+ justify-content: flex-start !important;
393
+ min-height: 40px !important;
394
+ text-align: left !important;
395
+ }
396
+
397
+ &.vi-tabs--line ::slotted(vi-tab[active]) {
398
+ background: var(--vi-tabs-surface-primary, #{tokens.$layer-01});
399
+ border-radius: #{func.to-rem(8px)};
400
+ }
401
+ }
402
+
403
+ // ─────────────────────────────────────────────────────────────────────────────
404
+ // Reduced motion — respect system preferences
405
+ // ─────────────────────────────────────────────────────────────────────────────
406
+
407
+ @media (prefers-reduced-motion: reduce) {
408
+ .vi-tabs__cursor,
409
+ .vi-tabs__indicator {
410
+ transition: opacity 100ms ease;
411
+ }
412
+ }
413
+
414
+ // ─────────────────────────────────────────────────────────────────────────────
415
+ // OVERFLOW: scroll
416
+ //
417
+ // Tablist scrolls horizontally. Fade gradients hint at hidden content.
418
+ // ─────────────────────────────────────────────────────────────────────────────
419
+
420
+ .vi-tabs--overflow-scroll .vi-tabs__tablist {
421
+ overflow-x: auto;
422
+ overflow-y: hidden;
423
+ scrollbar-width: none;
424
+ &::-webkit-scrollbar { display: none; }
425
+
426
+ // Premium visual indicator using mask-image to fade tabs at the scroll edges
427
+ mask-image: linear-gradient(to right, transparent 0%, black 24px, black calc(100% - 24px), transparent 100%);
428
+ -webkit-mask-image: linear-gradient(to right, transparent 0%, black 24px, black calc(100% - 24px), transparent 100%);
429
+
430
+ // When at scroll start, hide left fade
431
+ &[data-scroll-start] {
432
+ mask-image: linear-gradient(to right, black 0%, black calc(100% - 24px), transparent 100%);
433
+ -webkit-mask-image: linear-gradient(to right, black 0%, black calc(100% - 24px), transparent 100%);
434
+ }
435
+
436
+ // When at scroll end, hide right fade
437
+ &[data-scroll-end] {
438
+ mask-image: linear-gradient(to right, transparent 0%, black 24px, black 100%);
439
+ -webkit-mask-image: linear-gradient(to right, transparent 0%, black 24px, black 100%);
440
+ }
441
+
442
+ // When no scroll possible, no fades
443
+ &[data-scroll-none] {
444
+ mask-image: none;
445
+ -webkit-mask-image: none;
446
+ }
447
+ }
448
+
449
+ // ─────────────────────────────────────────────────────────────────────────────
450
+ // OVERFLOW: wrap
451
+ //
452
+ // Tabs wrap to the next line.
453
+ // ─────────────────────────────────────────────────────────────────────────────
454
+
455
+ .vi-tabs--overflow-wrap .vi-tabs__tablist {
456
+ flex-wrap: wrap;
457
+ overflow: visible;
458
+ height: auto;
459
+ }
460
+
461
+ // ─────────────────────────────────────────────────────────────────────────────
462
+ // OVERFLOW: menu — "More" button + dropdown
463
+ // ─────────────────────────────────────────────────────────────────────────────
464
+
465
+ // More wrapper sits at end of tablist
466
+ .vi-tabs__more-wrapper {
467
+ position: relative;
468
+ display: inline-flex;
469
+ align-items: stretch;
470
+ flex-shrink: 0;
471
+ margin-left: auto;
472
+ order: 9999; // ensure it sits after visually reordered tabs
473
+ }
474
+
475
+ // More trigger button
476
+ .vi-tabs__more-btn {
477
+ display: inline-flex;
478
+ align-items: center;
479
+ gap: var(--vi-tabs-gap-sm, 4px);
480
+ padding: var(--vi-tabs-spacing-none-lg, 0 14px);
481
+ min-height: 44px;
482
+ border: none;
483
+ background: transparent;
484
+ cursor: pointer;
485
+ font-family: inherit;
486
+ font-size: #{func.to-rem(13px)};
487
+ font-weight: 500;
488
+ color: var(--vi-tabs-text-secondary, #{tokens.$text-secondary});
489
+ border-radius: #{func.to-rem(6px)} #{func.to-rem(6px)} 0 0;
490
+ transition: color 150ms ease, background-color 150ms ease;
491
+
492
+ &:hover {
493
+ color: var(--vi-tabs-text-primary, #{tokens.$text-primary});
494
+ background: rgba(0, 0, 0, 0.04);
495
+ }
496
+
497
+ &--open {
498
+ color: var(--vi-tabs-primary-color, #{tokens.$color-primary});
499
+ }
500
+ }
501
+
502
+ .vi-tabs__more-chevron {
503
+ flex-shrink: 0;
504
+ transition: transform 200ms ease;
505
+
506
+ .vi-tabs__more-btn--open & {
507
+ transform: rotate(180deg);
508
+ }
509
+ }
510
+
511
+ // More dropdown menu
512
+ .vi-tabs__more-menu {
513
+ position: absolute;
514
+ top: calc(100% + 4px);
515
+ right: 0;
516
+ z-index: 100;
517
+ min-width: 180px;
518
+ background: var(--vi-tabs-surface-primary, #{tokens.$layer-01});
519
+ border: 1px solid var(--vi-tabs-border-color, #{tokens.$color-border});
520
+ border-radius: #{func.to-rem(8px)};
521
+ box-shadow:
522
+ 0 4px 6px -1px rgba(0, 0, 0, 0.10),
523
+ 0 2px 4px -1px rgba(0, 0, 0, 0.06),
524
+ 0 0 0 1px rgba(0, 0, 0, 0.04);
525
+ padding: var(--vi-tabs-spacing-xs, 4px);
526
+ animation: vi-menu-appear 150ms cubic-bezier(0.4, 0, 0.2, 1);
527
+ transform-origin: top right;
528
+ }
529
+
530
+ @keyframes vi-menu-appear {
531
+ from { opacity: 0; transform: scale(0.95) translateY(-4px); }
532
+ to { opacity: 1; transform: scale(1) translateY(0); }
533
+ }
534
+
535
+ // Individual menu item
536
+ .vi-tabs__more-item {
537
+ display: flex;
538
+ align-items: center;
539
+ width: 100%;
540
+ padding: var(--vi-tabs-spacing-md-lg, 8px 12px);
541
+ border: none;
542
+ border-radius: #{func.to-rem(6px)};
543
+ background: transparent;
544
+ font-family: inherit;
545
+ font-size: #{func.to-rem(13.5px)};
546
+ font-weight: 500;
547
+ color: var(--vi-tabs-text-secondary, #{tokens.$text-secondary});
548
+ cursor: pointer;
549
+ text-align: left;
550
+ transition: background-color 100ms ease, color 100ms ease;
551
+
552
+ &:hover:not(:disabled) {
553
+ background: var(--vi-tabs-surface-secondary, #{tokens.$layer-02});
554
+ color: var(--vi-tabs-text-primary, #{tokens.$text-primary});
555
+ }
556
+
557
+ &:disabled {
558
+ color: var(--vi-tabs-text-disabled, #{tokens.$text-disabled});
559
+ cursor: not-allowed;
560
+ }
561
+
562
+ &--active {
563
+ color: var(--vi-tabs-primary-color, #{tokens.$color-primary});
564
+ font-weight: 600;
565
+ background: color-mix(in srgb, #{tokens.$color-primary} 6%, transparent);
566
+
567
+ &:hover {
568
+ background: color-mix(in srgb, #{tokens.$color-primary} 10%, transparent);
569
+ }
570
+ }
571
+ }
572
+
573
+
574
+ }