@cahyo-dimas/freeday 1.21.0 → 1.23.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.
@@ -16,19 +16,24 @@
16
16
  });
17
17
  }
18
18
 
19
+ function bindTrigger(btn) {
20
+ if (btn.dataset.fdyDrawerBound === '1') return;
21
+ var dialog = document.getElementById(btn.getAttribute('data-fdy-drawer'));
22
+ if (!dialog) return;
23
+ initDrawer(dialog);
24
+ btn.dataset.fdyDrawerBound = '1';
25
+ btn.addEventListener('click', function () {
26
+ if (typeof dialog.showModal === 'function') dialog.showModal();
27
+ else dialog.setAttribute('open', '');
28
+ });
29
+ }
30
+
19
31
  function initTriggers(context) {
20
32
  var ctx = context || document;
21
- Array.prototype.forEach.call(ctx.querySelectorAll('[data-fdy-drawer]'), function (btn) {
22
- if (btn.dataset.fdyDrawerBound === '1') return;
23
- var dialog = document.getElementById(btn.getAttribute('data-fdy-drawer'));
24
- if (!dialog) return;
25
- initDrawer(dialog);
26
- btn.dataset.fdyDrawerBound = '1';
27
- btn.addEventListener('click', function () {
28
- if (typeof dialog.showModal === 'function') dialog.showModal();
29
- else dialog.setAttribute('open', '');
30
- });
31
- });
33
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
34
+ if (ctx.matches && ctx.matches('[data-fdy-drawer]')) bindTrigger(ctx);
35
+ if (ctx.matches && ctx.matches('.fdy-drawer')) initDrawer(ctx);
36
+ Array.prototype.forEach.call(ctx.querySelectorAll('[data-fdy-drawer]'), bindTrigger);
32
37
  // Init any drawer dialogs even without a wired trigger (backdrop/close still work).
33
38
  Array.prototype.forEach.call(ctx.querySelectorAll('.fdy-drawer'), initDrawer);
34
39
  }
@@ -168,7 +168,10 @@
168
168
  }
169
169
 
170
170
  function initAll(context) {
171
- Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-validate]'), initForm);
171
+ var root = context || document;
172
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
173
+ if (root.matches && root.matches('[data-fdy-validate]')) initForm(root);
174
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-validate]'), initForm);
172
175
  }
173
176
 
174
177
  if (document.readyState === 'loading') {
@@ -100,6 +100,9 @@
100
100
 
101
101
  function initAll(context) {
102
102
  var root = context || document;
103
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
104
+ if (root.matches && root.matches('[data-fdy-mask]')) initMask(root);
105
+ if (root.matches && root.matches('[data-fdy-password]')) initReveal(root);
103
106
  Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-mask]'), initMask);
104
107
  Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-password]'), initReveal);
105
108
  }
@@ -80,7 +80,10 @@
80
80
  }
81
81
 
82
82
  function initAll(ctx) {
83
- Array.prototype.forEach.call((ctx || document).querySelectorAll('[data-fdy-menu]'), initMenu);
83
+ var root = ctx || document;
84
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
85
+ if (root.matches && root.matches('[data-fdy-menu]')) initMenu(root);
86
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-menu]'), initMenu);
84
87
  }
85
88
 
86
89
  if (document.readyState === 'loading') {
@@ -37,7 +37,10 @@
37
37
  }
38
38
 
39
39
  function initAll(context) {
40
- Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-rating]'), initRating);
40
+ var root = context || document;
41
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
42
+ if (root.matches && root.matches('[data-fdy-rating]')) initRating(root);
43
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-rating]'), initRating);
41
44
  }
42
45
 
43
46
  if (document.readyState === 'loading') {
@@ -189,11 +189,10 @@
189
189
  }
190
190
 
191
191
  function initAll(context) {
192
- var scope = context || document;
193
- Array.prototype.forEach.call(
194
- scope.querySelectorAll('[data-fdy-combo]'),
195
- initCombo
196
- );
192
+ var root = context || document;
193
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
194
+ if (root.matches && root.matches('[data-fdy-combo]')) initCombo(root);
195
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-combo]'), initCombo);
197
196
  }
198
197
 
199
198
  // One global handler closes the open combo on an outside click.
@@ -21,7 +21,10 @@
21
21
  }
22
22
 
23
23
  function initAll(context) {
24
- Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-slider]'), initSlider);
24
+ var root = context || document;
25
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
26
+ if (root.matches && root.matches('[data-fdy-slider]')) initSlider(root);
27
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-slider]'), initSlider);
25
28
  }
26
29
 
27
30
  if (document.readyState === 'loading') {
@@ -77,7 +77,10 @@
77
77
  }
78
78
 
79
79
  function initAll(context) {
80
- Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-stepper]'), initStepper);
80
+ var root = context || document;
81
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
82
+ if (root.matches && root.matches('[data-fdy-stepper]')) initStepper(root);
83
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-stepper]'), initStepper);
81
84
  }
82
85
 
83
86
  if (document.readyState === 'loading') {
@@ -459,10 +459,10 @@
459
459
  }
460
460
 
461
461
  function initAll(context) {
462
- Array.prototype.forEach.call(
463
- (context || document).querySelectorAll('[data-fdy-table]'),
464
- initTable
465
- );
462
+ var root = context || document;
463
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
464
+ if (root.matches && root.matches('[data-fdy-table]')) initTable(root);
465
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-table]'), initTable);
466
466
  }
467
467
 
468
468
  if (document.readyState === 'loading') {
@@ -52,10 +52,10 @@
52
52
  }
53
53
 
54
54
  function initAll(context) {
55
- Array.prototype.forEach.call(
56
- (context || document).querySelectorAll('[data-fdy-tabs]'),
57
- initTabs
58
- );
55
+ var root = context || document;
56
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
57
+ if (root.matches && root.matches('[data-fdy-tabs]')) initTabs(root);
58
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-tabs]'), initTabs);
59
59
  }
60
60
 
61
61
  if (document.readyState === 'loading') {
@@ -167,7 +167,10 @@
167
167
  }
168
168
 
169
169
  function initAll(context) {
170
- Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-timepicker]'), initTime);
170
+ var root = context || document;
171
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
172
+ if (root.matches && root.matches('[data-fdy-timepicker]')) initTime(root);
173
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-timepicker]'), initTime);
171
174
  }
172
175
 
173
176
  if (document.readyState === 'loading') {
@@ -81,7 +81,10 @@
81
81
  }
82
82
 
83
83
  function initAll(context) {
84
- Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-tree]'), initTree);
84
+ var root = context || document;
85
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
86
+ if (root.matches && root.matches('[data-fdy-tree]')) initTree(root);
87
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-tree]'), initTree);
85
88
  }
86
89
 
87
90
  if (document.readyState === 'loading') {
@@ -193,7 +193,10 @@
193
193
  }
194
194
 
195
195
  function initAll(context) {
196
- Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-dropzone]'), initDropzone);
196
+ var root = context || document;
197
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
198
+ if (root.matches && root.matches('[data-fdy-dropzone]')) initDropzone(root);
199
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-dropzone]'), initDropzone);
197
200
  }
198
201
 
199
202
  if (document.readyState === 'loading') {
@@ -473,6 +473,15 @@ a { color: var(--color-primary); }
473
473
  .fdy-app__brand-title{display:block;font-family:var(--font-display);font-weight:var(--weight-bold);font-size:var(--text-lg);color:var(--color-text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
474
474
  .fdy-app__brand-subtitle{display:block;font-size:var(--text-xs);font-weight:var(--weight-regular);color:var(--color-text-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
475
475
  .fdy-nav{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;display:flex;flex-direction:column;gap:2px;padding:var(--space-3) var(--space-3) var(--space-6);}
476
+ /* Horizontal variant — the SAME nav links laid out as a row, for a top-nav application (one that
477
+ * puts its primary navigation in `.fdy-appbar` / `.fdy-app__topbar` and has no sidebar). Deliberately
478
+ * a modifier and not a new block: the item, its states and `aria-current="page"` are unchanged. That
479
+ * is the point — a navigation link is a link marked `aria-current`, never `aria-selected` (invalid
480
+ * ARIA on an anchor) and never a tab role, so borrowing `.fdy-tabs` for routes half-adopts a contract
481
+ * (roving tabindex, a panel per tab) that route links do not honour. position:relative because the
482
+ * row scrolls; see base.css on why a scroller must be a containing block. */
483
+ .fdy-nav--horizontal{position:relative;flex:0 1 auto;flex-direction:row;align-items:center;gap:var(--space-1);padding:0;min-height:0;overflow-x:auto;}
484
+ .fdy-nav--horizontal .fdy-nav__item{white-space:nowrap;}
476
485
  .fdy-nav__item{display:flex;align-items:center;gap:var(--space-3);padding:var(--space-2) var(--space-3);border-radius:var(--radius-md);font-size:var(--text-sm);font-weight:var(--weight-medium);color:var(--color-text-muted);text-decoration:none;cursor:pointer;transition:background var(--dur-fast) var(--ease-standard),color var(--dur-fast) var(--ease-standard);}
477
486
  .fdy-nav__item:hover{background:var(--color-surface-2);color:var(--color-text);}
478
487
  /* Active indicator stays in the primary ramp so it never clashes with a re-themed accent. */
@@ -548,9 +557,18 @@ a { color: var(--color-primary); }
548
557
  .fdy-appbar__brand svg{display:block;width:1.5rem;height:1.5rem;}
549
558
  .fdy-appbar__spacer{flex:1;}
550
559
  .fdy-appbar__actions{display:inline-flex;align-items:center;gap:var(--space-1);}
560
+ /* Primary navigation inside the bar: `<nav class="fdy-nav fdy-nav--horizontal">` with
561
+ `.fdy-nav__item` links (see app-shell.css). The bar ships no link class of its own — a nav link
562
+ is the same component whether the nav is a sidebar column or a top row. */
551
563
  /* Controls on a coloured app bar go on-colour (icons stay legible, hover is a light wash) */
552
564
  .fdy-appbar--primary .fdy-btn--ghost{background:transparent;border-color:transparent;color:var(--color-on-primary);box-shadow:none;}
553
565
  .fdy-appbar--primary .fdy-btn--ghost:hover{background:color-mix(in srgb,var(--color-on-primary) 16%,transparent);color:var(--color-on-primary);transform:none;}
566
+ /* Nav links on a coloured bar: full on-colour ink in every state (a dimmed variant would trade away
567
+ the contrast the bar's own token pair guarantees). The current page is carried by the background
568
+ wash plus the semibold weight the base rule already applies — and by `aria-current` itself. */
569
+ .fdy-appbar--primary .fdy-nav__item{color:var(--color-on-primary);}
570
+ .fdy-appbar--primary .fdy-nav__item:hover{background:color-mix(in srgb,var(--color-on-primary) 16%,transparent);color:var(--color-on-primary);}
571
+ .fdy-appbar--primary .fdy-nav__item[aria-current="page"]{background:color-mix(in srgb,var(--color-on-primary) 24%,transparent);color:var(--color-on-primary);}
554
572
 
555
573
  /* Freeday — Autocomplete (text input + filtered suggestion listbox, APG editable combobox).
556
574
  * Enhanced by freeday-autocomplete.js. */
@@ -568,6 +586,11 @@ a { color: var(--color-primary); }
568
586
  /* Freeday — Avatar */
569
587
  .fdy-avatar{display:inline-flex;align-items:center;justify-content:center;flex:none;width:2.5rem;height:2.5rem;border-radius:var(--radius-full);background:var(--color-primary-soft);color:var(--color-primary-strong);font-family:var(--font-body);font-weight:var(--weight-semibold);font-size:var(--text-sm);line-height:1;overflow:hidden;user-select:none;}
570
588
  .fdy-avatar img{width:100%;height:100%;object-fit:cover;}
589
+ /* --xs exists for an avatar INSIDE a control: `.fdy-btn--sm` is exactly 2rem tall, which is also
590
+ `--sm`'s size, so a small avatar in a small button fills it edge to edge and the ghost border
591
+ crosses the circle. 1.5rem leaves the same ~4px breathing room the kit's controls give their
592
+ icons. Uses `--text-xs` (the smallest type token the kit has — there is no `2xs`). */
593
+ .fdy-avatar--xs{width:1.5rem;height:1.5rem;font-size:var(--text-xs);}
571
594
  .fdy-avatar--sm{width:2rem;height:2rem;font-size:var(--text-xs);}
572
595
  .fdy-avatar--lg{width:3.5rem;height:3.5rem;font-size:var(--text-lg);}
573
596
  .fdy-avatar-group{display:inline-flex;}
@@ -653,6 +676,14 @@ a { color: var(--color-primary); }
653
676
  .fdy-btn:hover{transform:translateY(-1px);box-shadow:0 6px 18px -2px color-mix(in srgb,var(--color-primary) 60%,transparent),inset 0 1px 0 rgba(255,255,255,.26);}
654
677
  .fdy-btn:active{transform:translateY(1px);box-shadow:0 1px 4px -1px color-mix(in srgb,var(--color-primary) 46%,transparent);}
655
678
  .fdy-btn:disabled{opacity:.5;cursor:not-allowed;transform:none;box-shadow:none;}
679
+ /* Pressed / toggle. `aria-pressed` is already the right attribute for a toggle button, so this adds
680
+ no class and no new markup contract — and it is what makes .fdy-btn-group a complete segmented
681
+ control: the group joins the borders, this says which segment you are on. Each fill variant
682
+ defines its OWN pressed look below; a single shared rule cannot work, because a gradient is a
683
+ background-IMAGE and would blank the ghost variant's background-color. Solid: invert the gradient
684
+ and sink the shadow inward, so a held button reads as depressed, not merely primary-coloured. */
685
+ .fdy-btn[aria-pressed="true"]{background:linear-gradient(180deg,color-mix(in srgb,#000 12%,var(--color-primary)),var(--color-primary));box-shadow:inset 0 2px 6px color-mix(in srgb,#000 22%,transparent);}
686
+ .fdy-btn[aria-pressed="true"]:hover{transform:none;box-shadow:inset 0 2px 6px color-mix(in srgb,#000 22%,transparent);}
656
687
 
657
688
  .fdy-btn--ghost{background:var(--color-surface);color:var(--color-primary-strong);border-color:var(--color-border-strong);box-shadow:var(--shadow-1);}
658
689
  .fdy-btn--ghost:hover{transform:none;background:var(--color-primary-soft);border-color:var(--color-primary-border);box-shadow:var(--shadow-1);}
@@ -661,6 +692,7 @@ a { color: var(--color-primary); }
661
692
  .fdy-btn--danger{background:linear-gradient(180deg,var(--color-danger-btn),color-mix(in srgb,#000 12%,var(--color-danger-btn)));color:var(--color-on-danger);box-shadow:0 2px 9px -1px color-mix(in srgb,var(--color-danger-btn) 46%,transparent),inset 0 1px 0 rgba(255,255,255,.22);}
662
693
  .fdy-btn--danger:hover{transform:translateY(-1px);box-shadow:0 6px 18px -2px color-mix(in srgb,var(--color-danger-btn) 54%,transparent),inset 0 1px 0 rgba(255,255,255,.22);}
663
694
  .fdy-btn--danger:active{transform:translateY(1px);}
695
+ .fdy-btn--danger[aria-pressed="true"]{background:linear-gradient(180deg,color-mix(in srgb,#000 14%,var(--color-danger-btn)),var(--color-danger-btn));box-shadow:inset 0 2px 6px color-mix(in srgb,#000 24%,transparent);}
664
696
 
665
697
  .fdy-btn--sm{height:calc(var(--control-h) - var(--space-2));padding:0 var(--space-3);font-size:var(--text-xs);border-radius:var(--radius-sm);}
666
698
  .fdy-btn--lg{height:calc(var(--control-h) + var(--space-2));padding:0 var(--space-6);font-size:var(--text-base);border-radius:var(--radius-lg);}
@@ -673,6 +705,11 @@ a { color: var(--color-primary); }
673
705
  .fdy-btn--text{background:none;color:var(--color-primary-strong);border-color:transparent;box-shadow:none;}
674
706
  .fdy-btn--text:hover{transform:none;background:var(--color-primary-soft);box-shadow:none;}
675
707
  .fdy-btn--text:active{transform:translateY(1px);box-shadow:none;}
708
+ /* Ghost/text pressed: the kit's selected-surface pair (soft fill + strong ink) — the same pairing
709
+ .fdy-nav__item uses for the current page, and one the AA contrast gate already covers. This is
710
+ the realistic segmented control: a row of --ghost buttons with exactly one aria-pressed. */
711
+ .fdy-btn--ghost[aria-pressed="true"],.fdy-btn--text[aria-pressed="true"]{background:var(--color-primary-soft);color:var(--color-primary-strong);border-color:var(--color-primary-border);box-shadow:none;}
712
+ .fdy-btn--ghost[aria-pressed="true"]:hover,.fdy-btn--text[aria-pressed="true"]:hover{background:var(--color-primary-soft);transform:none;box-shadow:none;}
676
713
 
677
714
  /* Icon-only — square; combine with --ghost/--text/--sm/--lg. Requires aria-label. */
678
715
  .fdy-btn--icon{width:var(--control-h);padding:0;}
@@ -1425,7 +1462,12 @@ fieldset.fdy-field>legend{padding:0;float:none;}
1425
1462
  .fdy-step.is-active::before,.fdy-step.is-complete::before{background:var(--color-primary);}
1426
1463
  .fdy-step__btn{position:relative;z-index:1;display:flex;flex-direction:column;align-items:center;gap:var(--space-2);padding:0 var(--space-2);background:none;border:0;font:inherit;color:inherit;cursor:pointer;max-width:100%;}
1427
1464
  .fdy-step__btn:disabled{cursor:default;}
1428
- .fdy-step__marker{display:inline-flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;flex:none;border-radius:var(--radius-full);background:var(--color-surface);border:2px solid var(--color-border-strong);color:var(--color-text-muted);font-size:var(--text-sm);font-weight:var(--weight-semibold);font-variant-numeric:tabular-nums;}
1465
+ /* The marker carries the lift itself, NOT `__btn`. The connector above is position:absolute with
1466
+ z-index:0, and a positioned box paints after in-flow inline content in the same stacking context —
1467
+ so a marker that is merely inline (an indicator-only stepper with no `__btn`) gets painted THROUGH
1468
+ by the connector line. Lifting the part that must always exist makes that assembly impossible;
1469
+ `__btn`'s own z-index stays, harmlessly, for the navigable case. */
1470
+ .fdy-step__marker{position:relative;z-index:1;display:inline-flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;flex:none;border-radius:var(--radius-full);background:var(--color-surface);border:2px solid var(--color-border-strong);color:var(--color-text-muted);font-size:var(--text-sm);font-weight:var(--weight-semibold);font-variant-numeric:tabular-nums;}
1429
1471
  .fdy-step__marker svg{display:block;width:1rem;height:1rem;}
1430
1472
  .fdy-step.is-active .fdy-step__marker{border-color:var(--color-primary);background:var(--color-primary-soft);color:var(--color-primary-strong);}
1431
1473
  .fdy-step.is-complete .fdy-step__marker{border-color:var(--color-primary);background:var(--color-primary);color:var(--color-on-primary);}
@@ -1518,7 +1560,13 @@ fieldset.fdy-field>legend{padding:0;float:none;}
1518
1560
  .fdy-tabs__tab:hover{color:var(--color-text);}
1519
1561
  .fdy-tabs__tab:disabled,.fdy-tabs__tab[aria-disabled="true"]{opacity:.5;cursor:not-allowed;color:var(--color-text-muted);}
1520
1562
  .fdy-tabs__tab:disabled:hover,.fdy-tabs__tab[aria-disabled="true"]:hover{color:var(--color-text-muted);}
1521
- .fdy-tabs__tab[aria-selected="true"]{color:var(--color-primary-strong);border-bottom-color:var(--color-primary);}
1563
+ /* `aria-current="page"` is honoured alongside `aria-selected="true"` so the tab look can be used for
1564
+ ROUTE-driven sub-navigation (/settings/profile · /settings/billing), where the tabs are real links.
1565
+ A link cannot be `aria-selected` — that is invalid ARIA on an anchor; `aria-current` is the only
1566
+ correct marker, and it is what a router sets. Use `.fdy-tabs` + `freeday-tabs.js` (roving tabindex,
1567
+ a panel per tab) for in-page tabs; use these classes on plain links for routed ones, and
1568
+ `.fdy-nav--horizontal` for an application's PRIMARY navigation. */
1569
+ .fdy-tabs__tab[aria-selected="true"],.fdy-tabs__tab[aria-current="page"]{color:var(--color-primary-strong);border-bottom-color:var(--color-primary);}
1522
1570
  .fdy-tabs__tab:focus-visible{outline:none;border-radius:var(--radius-sm);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
1523
1571
  .fdy-tabs__panel{padding:var(--space-5) 0;}
1524
1572
  .fdy-tabs__panel[hidden]{display:none;}
package/dist/freeday.css CHANGED
@@ -103,6 +103,15 @@ a { color: var(--color-primary); }
103
103
  .fdy-app__brand-title{display:block;font-family:var(--font-display);font-weight:var(--weight-bold);font-size:var(--text-lg);color:var(--color-text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
104
104
  .fdy-app__brand-subtitle{display:block;font-size:var(--text-xs);font-weight:var(--weight-regular);color:var(--color-text-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
105
105
  .fdy-nav{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;display:flex;flex-direction:column;gap:2px;padding:var(--space-3) var(--space-3) var(--space-6);}
106
+ /* Horizontal variant — the SAME nav links laid out as a row, for a top-nav application (one that
107
+ * puts its primary navigation in `.fdy-appbar` / `.fdy-app__topbar` and has no sidebar). Deliberately
108
+ * a modifier and not a new block: the item, its states and `aria-current="page"` are unchanged. That
109
+ * is the point — a navigation link is a link marked `aria-current`, never `aria-selected` (invalid
110
+ * ARIA on an anchor) and never a tab role, so borrowing `.fdy-tabs` for routes half-adopts a contract
111
+ * (roving tabindex, a panel per tab) that route links do not honour. position:relative because the
112
+ * row scrolls; see base.css on why a scroller must be a containing block. */
113
+ .fdy-nav--horizontal{position:relative;flex:0 1 auto;flex-direction:row;align-items:center;gap:var(--space-1);padding:0;min-height:0;overflow-x:auto;}
114
+ .fdy-nav--horizontal .fdy-nav__item{white-space:nowrap;}
106
115
  .fdy-nav__item{display:flex;align-items:center;gap:var(--space-3);padding:var(--space-2) var(--space-3);border-radius:var(--radius-md);font-size:var(--text-sm);font-weight:var(--weight-medium);color:var(--color-text-muted);text-decoration:none;cursor:pointer;transition:background var(--dur-fast) var(--ease-standard),color var(--dur-fast) var(--ease-standard);}
107
116
  .fdy-nav__item:hover{background:var(--color-surface-2);color:var(--color-text);}
108
117
  /* Active indicator stays in the primary ramp so it never clashes with a re-themed accent. */
@@ -178,9 +187,18 @@ a { color: var(--color-primary); }
178
187
  .fdy-appbar__brand svg{display:block;width:1.5rem;height:1.5rem;}
179
188
  .fdy-appbar__spacer{flex:1;}
180
189
  .fdy-appbar__actions{display:inline-flex;align-items:center;gap:var(--space-1);}
190
+ /* Primary navigation inside the bar: `<nav class="fdy-nav fdy-nav--horizontal">` with
191
+ `.fdy-nav__item` links (see app-shell.css). The bar ships no link class of its own — a nav link
192
+ is the same component whether the nav is a sidebar column or a top row. */
181
193
  /* Controls on a coloured app bar go on-colour (icons stay legible, hover is a light wash) */
182
194
  .fdy-appbar--primary .fdy-btn--ghost{background:transparent;border-color:transparent;color:var(--color-on-primary);box-shadow:none;}
183
195
  .fdy-appbar--primary .fdy-btn--ghost:hover{background:color-mix(in srgb,var(--color-on-primary) 16%,transparent);color:var(--color-on-primary);transform:none;}
196
+ /* Nav links on a coloured bar: full on-colour ink in every state (a dimmed variant would trade away
197
+ the contrast the bar's own token pair guarantees). The current page is carried by the background
198
+ wash plus the semibold weight the base rule already applies — and by `aria-current` itself. */
199
+ .fdy-appbar--primary .fdy-nav__item{color:var(--color-on-primary);}
200
+ .fdy-appbar--primary .fdy-nav__item:hover{background:color-mix(in srgb,var(--color-on-primary) 16%,transparent);color:var(--color-on-primary);}
201
+ .fdy-appbar--primary .fdy-nav__item[aria-current="page"]{background:color-mix(in srgb,var(--color-on-primary) 24%,transparent);color:var(--color-on-primary);}
184
202
 
185
203
  /* Freeday — Autocomplete (text input + filtered suggestion listbox, APG editable combobox).
186
204
  * Enhanced by freeday-autocomplete.js. */
@@ -198,6 +216,11 @@ a { color: var(--color-primary); }
198
216
  /* Freeday — Avatar */
199
217
  .fdy-avatar{display:inline-flex;align-items:center;justify-content:center;flex:none;width:2.5rem;height:2.5rem;border-radius:var(--radius-full);background:var(--color-primary-soft);color:var(--color-primary-strong);font-family:var(--font-body);font-weight:var(--weight-semibold);font-size:var(--text-sm);line-height:1;overflow:hidden;user-select:none;}
200
218
  .fdy-avatar img{width:100%;height:100%;object-fit:cover;}
219
+ /* --xs exists for an avatar INSIDE a control: `.fdy-btn--sm` is exactly 2rem tall, which is also
220
+ `--sm`'s size, so a small avatar in a small button fills it edge to edge and the ghost border
221
+ crosses the circle. 1.5rem leaves the same ~4px breathing room the kit's controls give their
222
+ icons. Uses `--text-xs` (the smallest type token the kit has — there is no `2xs`). */
223
+ .fdy-avatar--xs{width:1.5rem;height:1.5rem;font-size:var(--text-xs);}
201
224
  .fdy-avatar--sm{width:2rem;height:2rem;font-size:var(--text-xs);}
202
225
  .fdy-avatar--lg{width:3.5rem;height:3.5rem;font-size:var(--text-lg);}
203
226
  .fdy-avatar-group{display:inline-flex;}
@@ -283,6 +306,14 @@ a { color: var(--color-primary); }
283
306
  .fdy-btn:hover{transform:translateY(-1px);box-shadow:0 6px 18px -2px color-mix(in srgb,var(--color-primary) 60%,transparent),inset 0 1px 0 rgba(255,255,255,.26);}
284
307
  .fdy-btn:active{transform:translateY(1px);box-shadow:0 1px 4px -1px color-mix(in srgb,var(--color-primary) 46%,transparent);}
285
308
  .fdy-btn:disabled{opacity:.5;cursor:not-allowed;transform:none;box-shadow:none;}
309
+ /* Pressed / toggle. `aria-pressed` is already the right attribute for a toggle button, so this adds
310
+ no class and no new markup contract — and it is what makes .fdy-btn-group a complete segmented
311
+ control: the group joins the borders, this says which segment you are on. Each fill variant
312
+ defines its OWN pressed look below; a single shared rule cannot work, because a gradient is a
313
+ background-IMAGE and would blank the ghost variant's background-color. Solid: invert the gradient
314
+ and sink the shadow inward, so a held button reads as depressed, not merely primary-coloured. */
315
+ .fdy-btn[aria-pressed="true"]{background:linear-gradient(180deg,color-mix(in srgb,#000 12%,var(--color-primary)),var(--color-primary));box-shadow:inset 0 2px 6px color-mix(in srgb,#000 22%,transparent);}
316
+ .fdy-btn[aria-pressed="true"]:hover{transform:none;box-shadow:inset 0 2px 6px color-mix(in srgb,#000 22%,transparent);}
286
317
 
287
318
  .fdy-btn--ghost{background:var(--color-surface);color:var(--color-primary-strong);border-color:var(--color-border-strong);box-shadow:var(--shadow-1);}
288
319
  .fdy-btn--ghost:hover{transform:none;background:var(--color-primary-soft);border-color:var(--color-primary-border);box-shadow:var(--shadow-1);}
@@ -291,6 +322,7 @@ a { color: var(--color-primary); }
291
322
  .fdy-btn--danger{background:linear-gradient(180deg,var(--color-danger-btn),color-mix(in srgb,#000 12%,var(--color-danger-btn)));color:var(--color-on-danger);box-shadow:0 2px 9px -1px color-mix(in srgb,var(--color-danger-btn) 46%,transparent),inset 0 1px 0 rgba(255,255,255,.22);}
292
323
  .fdy-btn--danger:hover{transform:translateY(-1px);box-shadow:0 6px 18px -2px color-mix(in srgb,var(--color-danger-btn) 54%,transparent),inset 0 1px 0 rgba(255,255,255,.22);}
293
324
  .fdy-btn--danger:active{transform:translateY(1px);}
325
+ .fdy-btn--danger[aria-pressed="true"]{background:linear-gradient(180deg,color-mix(in srgb,#000 14%,var(--color-danger-btn)),var(--color-danger-btn));box-shadow:inset 0 2px 6px color-mix(in srgb,#000 24%,transparent);}
294
326
 
295
327
  .fdy-btn--sm{height:calc(var(--control-h) - var(--space-2));padding:0 var(--space-3);font-size:var(--text-xs);border-radius:var(--radius-sm);}
296
328
  .fdy-btn--lg{height:calc(var(--control-h) + var(--space-2));padding:0 var(--space-6);font-size:var(--text-base);border-radius:var(--radius-lg);}
@@ -303,6 +335,11 @@ a { color: var(--color-primary); }
303
335
  .fdy-btn--text{background:none;color:var(--color-primary-strong);border-color:transparent;box-shadow:none;}
304
336
  .fdy-btn--text:hover{transform:none;background:var(--color-primary-soft);box-shadow:none;}
305
337
  .fdy-btn--text:active{transform:translateY(1px);box-shadow:none;}
338
+ /* Ghost/text pressed: the kit's selected-surface pair (soft fill + strong ink) — the same pairing
339
+ .fdy-nav__item uses for the current page, and one the AA contrast gate already covers. This is
340
+ the realistic segmented control: a row of --ghost buttons with exactly one aria-pressed. */
341
+ .fdy-btn--ghost[aria-pressed="true"],.fdy-btn--text[aria-pressed="true"]{background:var(--color-primary-soft);color:var(--color-primary-strong);border-color:var(--color-primary-border);box-shadow:none;}
342
+ .fdy-btn--ghost[aria-pressed="true"]:hover,.fdy-btn--text[aria-pressed="true"]:hover{background:var(--color-primary-soft);transform:none;box-shadow:none;}
306
343
 
307
344
  /* Icon-only — square; combine with --ghost/--text/--sm/--lg. Requires aria-label. */
308
345
  .fdy-btn--icon{width:var(--control-h);padding:0;}
@@ -1055,7 +1092,12 @@ fieldset.fdy-field>legend{padding:0;float:none;}
1055
1092
  .fdy-step.is-active::before,.fdy-step.is-complete::before{background:var(--color-primary);}
1056
1093
  .fdy-step__btn{position:relative;z-index:1;display:flex;flex-direction:column;align-items:center;gap:var(--space-2);padding:0 var(--space-2);background:none;border:0;font:inherit;color:inherit;cursor:pointer;max-width:100%;}
1057
1094
  .fdy-step__btn:disabled{cursor:default;}
1058
- .fdy-step__marker{display:inline-flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;flex:none;border-radius:var(--radius-full);background:var(--color-surface);border:2px solid var(--color-border-strong);color:var(--color-text-muted);font-size:var(--text-sm);font-weight:var(--weight-semibold);font-variant-numeric:tabular-nums;}
1095
+ /* The marker carries the lift itself, NOT `__btn`. The connector above is position:absolute with
1096
+ z-index:0, and a positioned box paints after in-flow inline content in the same stacking context —
1097
+ so a marker that is merely inline (an indicator-only stepper with no `__btn`) gets painted THROUGH
1098
+ by the connector line. Lifting the part that must always exist makes that assembly impossible;
1099
+ `__btn`'s own z-index stays, harmlessly, for the navigable case. */
1100
+ .fdy-step__marker{position:relative;z-index:1;display:inline-flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;flex:none;border-radius:var(--radius-full);background:var(--color-surface);border:2px solid var(--color-border-strong);color:var(--color-text-muted);font-size:var(--text-sm);font-weight:var(--weight-semibold);font-variant-numeric:tabular-nums;}
1059
1101
  .fdy-step__marker svg{display:block;width:1rem;height:1rem;}
1060
1102
  .fdy-step.is-active .fdy-step__marker{border-color:var(--color-primary);background:var(--color-primary-soft);color:var(--color-primary-strong);}
1061
1103
  .fdy-step.is-complete .fdy-step__marker{border-color:var(--color-primary);background:var(--color-primary);color:var(--color-on-primary);}
@@ -1148,7 +1190,13 @@ fieldset.fdy-field>legend{padding:0;float:none;}
1148
1190
  .fdy-tabs__tab:hover{color:var(--color-text);}
1149
1191
  .fdy-tabs__tab:disabled,.fdy-tabs__tab[aria-disabled="true"]{opacity:.5;cursor:not-allowed;color:var(--color-text-muted);}
1150
1192
  .fdy-tabs__tab:disabled:hover,.fdy-tabs__tab[aria-disabled="true"]:hover{color:var(--color-text-muted);}
1151
- .fdy-tabs__tab[aria-selected="true"]{color:var(--color-primary-strong);border-bottom-color:var(--color-primary);}
1193
+ /* `aria-current="page"` is honoured alongside `aria-selected="true"` so the tab look can be used for
1194
+ ROUTE-driven sub-navigation (/settings/profile · /settings/billing), where the tabs are real links.
1195
+ A link cannot be `aria-selected` — that is invalid ARIA on an anchor; `aria-current` is the only
1196
+ correct marker, and it is what a router sets. Use `.fdy-tabs` + `freeday-tabs.js` (roving tabindex,
1197
+ a panel per tab) for in-page tabs; use these classes on plain links for routed ones, and
1198
+ `.fdy-nav--horizontal` for an application's PRIMARY navigation. */
1199
+ .fdy-tabs__tab[aria-selected="true"],.fdy-tabs__tab[aria-current="page"]{color:var(--color-primary-strong);border-bottom-color:var(--color-primary);}
1152
1200
  .fdy-tabs__tab:focus-visible{outline:none;border-radius:var(--radius-sm);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
1153
1201
  .fdy-tabs__panel{padding:var(--space-5) 0;}
1154
1202
  .fdy-tabs__panel[hidden]{display:none;}