@cahyo-dimas/freeday 1.20.0 → 1.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +133 -0
- package/COMPONENTS.md +91 -2
- package/README.id.md +25 -1
- package/README.md +24 -1
- package/USAGE.md +42 -0
- package/dist/freeday.bundle.css +96 -10
- package/dist/freeday.css +85 -8
- package/dist/freeday.tokens.css +11 -2
- package/docs/agent-onboarding.md +39 -6
- package/docs/getting-started.md +15 -8
- package/package.json +2 -2
- package/src/base.css +15 -0
- package/src/components/accordion.css +5 -1
- package/src/components/app-shell.css +9 -0
- package/src/components/appbar.css +9 -0
- package/src/components/button.css +14 -0
- package/src/components/card.css +7 -1
- package/src/components/carousel.css +4 -1
- package/src/components/list.css +8 -1
- package/src/components/table.css +5 -2
- package/src/components/tabs.css +9 -2
package/dist/freeday.bundle.css
CHANGED
|
@@ -177,6 +177,9 @@
|
|
|
177
177
|
--focus-ring: var(--azure-600);
|
|
178
178
|
--focus-ring-width: 2px;
|
|
179
179
|
}
|
|
180
|
+
/* The SYSTEM default stays root-scoped: it is a statement about the document, and dropping :root
|
|
181
|
+
* here would match every element that is not itself [data-theme="light"] — which would re-darken
|
|
182
|
+
* the children of a light panel, since they carry no attribute of their own. */
|
|
180
183
|
@media (prefers-color-scheme: dark) {
|
|
181
184
|
:root:not([data-theme="light"]) {
|
|
182
185
|
--shadow-1: 0 1px 2px rgba(0,0,0,.4);
|
|
@@ -235,7 +238,13 @@
|
|
|
235
238
|
--focus-ring: var(--azure-400);
|
|
236
239
|
}
|
|
237
240
|
}
|
|
238
|
-
:root
|
|
241
|
+
/* The two EXPLICIT opt-ins are deliberately NOT scoped to :root, for the same reason as density
|
|
242
|
+
* below: these are inheriting custom properties, so data-theme on any ancestor re-themes just that
|
|
243
|
+
* subtree — a dark brand panel beside a light form is an ordinary layout, and it should not require
|
|
244
|
+
* re-colouring each component by hand. The root still matches, so data-theme on <html> is unchanged.
|
|
245
|
+
* Both keep the same specificity (0,1,0) as :root and come after it, so they still win there; and
|
|
246
|
+
* --light after --dark means a light island inside a dark region wins in turn. */
|
|
247
|
+
[data-theme="dark"] {
|
|
239
248
|
--shadow-1: 0 1px 2px rgba(0,0,0,.4);
|
|
240
249
|
--shadow-2: 0 2px 6px rgba(0,0,0,.45);
|
|
241
250
|
--shadow-3: 0 6px 20px -2px rgba(0,0,0,.6),0 1px 3px rgba(0,0,0,.5);
|
|
@@ -291,7 +300,7 @@
|
|
|
291
300
|
--chart-tick: var(--slate-600);
|
|
292
301
|
--focus-ring: var(--azure-400);
|
|
293
302
|
}
|
|
294
|
-
|
|
303
|
+
[data-theme="light"] {
|
|
295
304
|
--shadow-1: 0 1px 2px rgba(16,14,30,.06);
|
|
296
305
|
--shadow-2: 0 2px 6px rgba(16,14,30,.07),0 1px 2px rgba(16,14,30,.05);
|
|
297
306
|
--shadow-3: 0 4px 16px -2px rgba(16,14,30,.14),0 1px 3px rgba(16,14,30,.10);
|
|
@@ -376,6 +385,21 @@ h1, h2, h3, h4 { font-family: var(--font-display); font-weight: var(--weight-bol
|
|
|
376
385
|
a { color: var(--color-primary); }
|
|
377
386
|
:focus-visible { outline: var(--focus-ring-width) solid var(--focus-ring); outline-offset: var(--focus-ring-width); border-radius: var(--radius-xs); }
|
|
378
387
|
:where(button, input, select, textarea) { font: inherit; }
|
|
388
|
+
/* Hidden from sight, kept for assistive tech. `clip` hides PAINTING, not LAYOUT: the box still
|
|
389
|
+
* has a position, and because it is position:absolute its containing block is the nearest
|
|
390
|
+
* POSITIONED ancestor — with none, the initial containing block (the document itself).
|
|
391
|
+
*
|
|
392
|
+
* That matters, because `overflow` only clips a descendant whose containing block is inside the
|
|
393
|
+
* overflow box. So a hidden span in a horizontally scrolling table (the kit's own recommended way
|
|
394
|
+
* to name an icon button) parks at its static position — possibly thousands of px to the right —
|
|
395
|
+
* and drags the whole DOCUMENT sideways: invisible, and immune to `overflow-x:hidden` on every
|
|
396
|
+
* wrapper. Measured before the fix: 1351px of phantom page scroll from 11 spans.
|
|
397
|
+
*
|
|
398
|
+
* The fix is not here — it is on the containers: every clipping/scrolling container in the kit
|
|
399
|
+
* that holds consumer markup declares `position:relative` so it becomes the containing block for
|
|
400
|
+
* its own out-of-flow content (see test/css.test.mjs, which will not let a new one skip it).
|
|
401
|
+
* Note the measurement trap: documentElement.scrollWidth sees it, body.scrollWidth does not —
|
|
402
|
+
* the honest check is `window.scrollTo(9999, 0)` then reading `window.scrollX`. */
|
|
379
403
|
.fdy-visually-hidden { position: absolute !important; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
|
|
380
404
|
/* Opt-in list reset. base.css is intentionally a *light* reset — it does NOT strip list/paragraph
|
|
381
405
|
* margins (see the kit's own list components, which each reset themselves). If you run a utility
|
|
@@ -387,7 +411,11 @@ a { color: var(--color-primary); }
|
|
|
387
411
|
}
|
|
388
412
|
|
|
389
413
|
/* Freeday — Accordion (native <details>/<summary>, zero-JS) */
|
|
390
|
-
|
|
414
|
+
/* position:relative — containing block for out-of-flow panel content (see base.css). NOT redundant
|
|
415
|
+
with the panel's reveal animation: that animation gives the panel a transform (which happens to
|
|
416
|
+
make it a containing block too), but it lives behind prefers-reduced-motion:no-preference — so
|
|
417
|
+
without this line the escape bug appears ONLY for readers who asked for reduced motion. */
|
|
418
|
+
.fdy-accordion{position:relative;border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);overflow:hidden;background:var(--color-surface);}
|
|
391
419
|
.fdy-accordion__item + .fdy-accordion__item{border-top:var(--bw) solid var(--color-border);}
|
|
392
420
|
.fdy-accordion__item > summary{list-style:none;cursor:pointer;display:flex;align-items:center;gap:var(--space-3);padding:var(--space-3) var(--space-4);font-weight:var(--weight-medium);color:var(--color-text);}
|
|
393
421
|
.fdy-accordion__item > summary::-webkit-details-marker{display:none;}
|
|
@@ -445,6 +473,15 @@ a { color: var(--color-primary); }
|
|
|
445
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;}
|
|
446
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;}
|
|
447
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;}
|
|
448
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);}
|
|
449
486
|
.fdy-nav__item:hover{background:var(--color-surface-2);color:var(--color-text);}
|
|
450
487
|
/* Active indicator stays in the primary ramp so it never clashes with a re-themed accent. */
|
|
@@ -520,9 +557,18 @@ a { color: var(--color-primary); }
|
|
|
520
557
|
.fdy-appbar__brand svg{display:block;width:1.5rem;height:1.5rem;}
|
|
521
558
|
.fdy-appbar__spacer{flex:1;}
|
|
522
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. */
|
|
523
563
|
/* Controls on a coloured app bar go on-colour (icons stay legible, hover is a light wash) */
|
|
524
564
|
.fdy-appbar--primary .fdy-btn--ghost{background:transparent;border-color:transparent;color:var(--color-on-primary);box-shadow:none;}
|
|
525
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);}
|
|
526
572
|
|
|
527
573
|
/* Freeday — Autocomplete (text input + filtered suggestion listbox, APG editable combobox).
|
|
528
574
|
* Enhanced by freeday-autocomplete.js. */
|
|
@@ -625,6 +671,14 @@ a { color: var(--color-primary); }
|
|
|
625
671
|
.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);}
|
|
626
672
|
.fdy-btn:active{transform:translateY(1px);box-shadow:0 1px 4px -1px color-mix(in srgb,var(--color-primary) 46%,transparent);}
|
|
627
673
|
.fdy-btn:disabled{opacity:.5;cursor:not-allowed;transform:none;box-shadow:none;}
|
|
674
|
+
/* Pressed / toggle. `aria-pressed` is already the right attribute for a toggle button, so this adds
|
|
675
|
+
no class and no new markup contract — and it is what makes .fdy-btn-group a complete segmented
|
|
676
|
+
control: the group joins the borders, this says which segment you are on. Each fill variant
|
|
677
|
+
defines its OWN pressed look below; a single shared rule cannot work, because a gradient is a
|
|
678
|
+
background-IMAGE and would blank the ghost variant's background-color. Solid: invert the gradient
|
|
679
|
+
and sink the shadow inward, so a held button reads as depressed, not merely primary-coloured. */
|
|
680
|
+
.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);}
|
|
681
|
+
.fdy-btn[aria-pressed="true"]:hover{transform:none;box-shadow:inset 0 2px 6px color-mix(in srgb,#000 22%,transparent);}
|
|
628
682
|
|
|
629
683
|
.fdy-btn--ghost{background:var(--color-surface);color:var(--color-primary-strong);border-color:var(--color-border-strong);box-shadow:var(--shadow-1);}
|
|
630
684
|
.fdy-btn--ghost:hover{transform:none;background:var(--color-primary-soft);border-color:var(--color-primary-border);box-shadow:var(--shadow-1);}
|
|
@@ -633,6 +687,7 @@ a { color: var(--color-primary); }
|
|
|
633
687
|
.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);}
|
|
634
688
|
.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);}
|
|
635
689
|
.fdy-btn--danger:active{transform:translateY(1px);}
|
|
690
|
+
.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);}
|
|
636
691
|
|
|
637
692
|
.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);}
|
|
638
693
|
.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);}
|
|
@@ -645,6 +700,11 @@ a { color: var(--color-primary); }
|
|
|
645
700
|
.fdy-btn--text{background:none;color:var(--color-primary-strong);border-color:transparent;box-shadow:none;}
|
|
646
701
|
.fdy-btn--text:hover{transform:none;background:var(--color-primary-soft);box-shadow:none;}
|
|
647
702
|
.fdy-btn--text:active{transform:translateY(1px);box-shadow:none;}
|
|
703
|
+
/* Ghost/text pressed: the kit's selected-surface pair (soft fill + strong ink) — the same pairing
|
|
704
|
+
.fdy-nav__item uses for the current page, and one the AA contrast gate already covers. This is
|
|
705
|
+
the realistic segmented control: a row of --ghost buttons with exactly one aria-pressed. */
|
|
706
|
+
.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;}
|
|
707
|
+
.fdy-btn--ghost[aria-pressed="true"]:hover,.fdy-btn--text[aria-pressed="true"]:hover{background:var(--color-primary-soft);transform:none;box-shadow:none;}
|
|
648
708
|
|
|
649
709
|
/* Icon-only — square; combine with --ghost/--text/--sm/--lg. Requires aria-label. */
|
|
650
710
|
.fdy-btn--icon{width:var(--control-h);padding:0;}
|
|
@@ -673,7 +733,9 @@ a { color: var(--color-primary); }
|
|
|
673
733
|
.fdy-fab--accent{background:linear-gradient(180deg,var(--color-accent),color-mix(in srgb,#000 10%,var(--color-accent)));color:var(--color-on-accent);}
|
|
674
734
|
|
|
675
735
|
/* Freeday — Card */
|
|
676
|
-
|
|
736
|
+
/* position:relative — containing block for out-of-flow card content (see base.css); it also gives
|
|
737
|
+
consumers the anchor a corner ribbon/badge needs. */
|
|
738
|
+
.fdy-card{position:relative;background:var(--color-surface);border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);overflow:hidden;box-shadow:var(--shadow-lift);transition:box-shadow var(--dur-base) var(--ease-standard),transform var(--dur-base) var(--ease-standard);}
|
|
677
739
|
.fdy-card--elevated{box-shadow:var(--shadow-lift-hover);}
|
|
678
740
|
.fdy-card--interactive{cursor:pointer;}
|
|
679
741
|
.fdy-card--interactive:hover{box-shadow:var(--shadow-lift-hover);transform:translateY(-3px);}
|
|
@@ -684,6 +746,10 @@ a { color: var(--color-primary); }
|
|
|
684
746
|
* announced — reset the UA button box. Note: it does NOT reset background/border, because
|
|
685
747
|
* .fdy-card already sets its own surface + border (resetting them here would strip the card). */
|
|
686
748
|
.fdy-card--button{display:block;width:100%;text-align:inherit;color:inherit;appearance:none;-webkit-appearance:none;}
|
|
749
|
+
/* Disabled card-as-control — same contract as .fdy-list__row--button: dim + not-allowed, and the
|
|
750
|
+
--interactive lift is withdrawn so a refusing control stops answering the pointer. */
|
|
751
|
+
.fdy-card--button:disabled,.fdy-card--button[aria-disabled="true"],.fdy-card--interactive[aria-disabled="true"]{opacity:.5;cursor:not-allowed;}
|
|
752
|
+
.fdy-card--interactive:disabled:hover,.fdy-card--interactive[aria-disabled="true"]:hover{box-shadow:var(--shadow-lift);transform:none;}
|
|
687
753
|
.fdy-card__body{padding:var(--space-5);}
|
|
688
754
|
.fdy-card__title{font-family:var(--font-display);font-size:var(--text-lg);font-weight:var(--weight-semibold);letter-spacing:var(--tracking-tight);color:var(--color-text);margin:0 0 var(--space-1);}
|
|
689
755
|
.fdy-card__desc{font-size:var(--text-sm);color:var(--color-text-muted);margin:0;line-height:var(--leading-normal);}
|
|
@@ -691,7 +757,10 @@ a { color: var(--color-primary); }
|
|
|
691
757
|
|
|
692
758
|
/* Freeday — Carousel (scroll-snap track + arrows + dots). Enhanced by freeday-carousel.js. */
|
|
693
759
|
.fdy-carousel{position:relative;}
|
|
694
|
-
.fdy-
|
|
760
|
+
/* position:relative — the arrows are positioned against .fdy-carousel (the parent), so this only
|
|
761
|
+
catches out-of-flow content inside the SLIDES, which would otherwise escape the viewport
|
|
762
|
+
entirely and scroll the page by one slide-offset per slide (see base.css). */
|
|
763
|
+
.fdy-carousel__viewport{position:relative;display:flex;overflow-x:auto;scroll-snap-type:x mandatory;scroll-behavior:smooth;border-radius:var(--radius-lg);scrollbar-width:none;-ms-overflow-style:none;}
|
|
695
764
|
.fdy-carousel__viewport::-webkit-scrollbar{display:none;}
|
|
696
765
|
.fdy-carousel__slide{flex:0 0 100%;min-width:0;scroll-snap-align:center;}
|
|
697
766
|
.fdy-carousel__arrow{position:absolute;top:50%;transform:translateY(-50%);z-index:2;display:inline-flex;align-items:center;justify-content:center;width:2.25rem;height:2.25rem;border:0;border-radius:var(--radius-full);background:color-mix(in srgb,var(--color-surface) 86%,transparent);color:var(--color-text);box-shadow:var(--shadow-2);cursor:pointer;transition:background var(--dur-fast) var(--ease-standard);}
|
|
@@ -1198,7 +1267,8 @@ fieldset.fdy-field>legend{padding:0;float:none;}
|
|
|
1198
1267
|
*
|
|
1199
1268
|
* Not to be confused with .fdy-list-reset (base.css), which only strips UA bullets/indent.
|
|
1200
1269
|
* Works on <ul>/<ol> (list-style is reset here) or on plain <div>s. */
|
|
1201
|
-
|
|
1270
|
+
/* position:relative — containing block for out-of-flow row content (see base.css). */
|
|
1271
|
+
.fdy-list{position:relative;list-style:none;margin:0;padding:0;background:var(--color-surface);border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);overflow:hidden;}
|
|
1202
1272
|
.fdy-list__row{display:flex;align-items:center;gap:var(--space-3);padding:var(--space-4) var(--space-5);min-width:0;}
|
|
1203
1273
|
/* Divider between rows. Two shapes are supported and both are load-bearing: rows as direct children
|
|
1204
1274
|
* of the list, and rows wrapped in <li> (the semantic shape, where the ADJACENT siblings are the
|
|
@@ -1211,6 +1281,12 @@ fieldset.fdy-field>legend{padding:0;float:none;}
|
|
|
1211
1281
|
.fdy-list__row--button:hover,.fdy-list__row--interactive:hover{background:var(--color-surface-2);}
|
|
1212
1282
|
.fdy-list__row--button:focus-visible{outline:none;box-shadow:inset 0 0 0 2px var(--color-primary);}
|
|
1213
1283
|
.fdy-list__row--interactive{cursor:pointer;}
|
|
1284
|
+
/* Disabled row. The reset above adopts the UA button box, but not its disabled state — so a row
|
|
1285
|
+
disabled mid-flight (a redirect in progress, say) kept lighting up and kept a pointer cursor:
|
|
1286
|
+
a control answering the pointer while refusing input. Same contract as every other disabled
|
|
1287
|
+
control in the kit (dim + not-allowed), hover withdrawn, both the native and the aria- form. */
|
|
1288
|
+
.fdy-list__row--button:disabled,.fdy-list__row--button[aria-disabled="true"],.fdy-list__row--interactive[aria-disabled="true"]{opacity:.5;cursor:not-allowed;}
|
|
1289
|
+
.fdy-list__row--button:disabled:hover,.fdy-list__row--button[aria-disabled="true"]:hover,.fdy-list__row--interactive[aria-disabled="true"]:hover{background:none;}
|
|
1214
1290
|
/* Row internals: a title/meta stack that truncates, and a trailing slot pinned right. */
|
|
1215
1291
|
.fdy-list__main{display:flex;flex-direction:column;gap:var(--space-1);min-width:0;flex:1;}
|
|
1216
1292
|
.fdy-list__title{font-weight:var(--weight-medium);color:var(--color-text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
@@ -1395,7 +1471,10 @@ fieldset.fdy-field>legend{padding:0;float:none;}
|
|
|
1395
1471
|
.fdy-step-nav{display:flex;justify-content:space-between;gap:var(--space-2);margin-top:var(--space-5);}
|
|
1396
1472
|
|
|
1397
1473
|
/* Freeday — Table */
|
|
1398
|
-
|
|
1474
|
+
/* position:relative on every scroller/clipper — it makes the box the containing block for its own
|
|
1475
|
+
absolutely positioned content (.fdy-visually-hidden above all), which `overflow` alone does NOT
|
|
1476
|
+
clip. Without it a hidden label in a wide table scrolls the whole page. See base.css. */
|
|
1477
|
+
.fdy-table-wrap{position:relative;overflow-x:auto;border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-1);background:var(--color-surface);}
|
|
1399
1478
|
.fdy-table{width:100%;border-collapse:collapse;font-size:var(--text-sm);}
|
|
1400
1479
|
.fdy-table caption{text-align:left;padding:var(--space-3) var(--space-4);font-weight:var(--weight-semibold);color:var(--color-text);}
|
|
1401
1480
|
.fdy-table th{text-align:left;padding:var(--space-3) var(--space-4);font-size:var(--text-xs);font-weight:var(--weight-semibold);text-transform:uppercase;letter-spacing:var(--tracking-wide);color:var(--color-text-muted);background:var(--color-surface-2);border-bottom:var(--bw) solid var(--color-border);white-space:nowrap;}
|
|
@@ -1432,7 +1511,7 @@ fieldset.fdy-field>legend{padding:0;float:none;}
|
|
|
1432
1511
|
|
|
1433
1512
|
/* Data-table shell (toolbar + scroll + footer stay put while the table scrolls) */
|
|
1434
1513
|
.fdy-datatable{border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-1);background:var(--color-surface);overflow:hidden;}
|
|
1435
|
-
.fdy-table-scroll{overflow-x:auto;}
|
|
1514
|
+
.fdy-table-scroll{position:relative;overflow-x:auto;}
|
|
1436
1515
|
.fdy-table-toolbar{display:flex;align-items:center;gap:var(--space-3);flex-wrap:wrap;padding:var(--space-3) var(--space-4);border-bottom:var(--bw) solid var(--color-border);}
|
|
1437
1516
|
.fdy-table-toolbar__search{max-width:18rem;}
|
|
1438
1517
|
.fdy-table-toolbar__spacer{flex:1;}
|
|
@@ -1465,12 +1544,19 @@ fieldset.fdy-field>legend{padding:0;float:none;}
|
|
|
1465
1544
|
.fdy-table-bulkbar__actions{display:flex;gap:var(--space-2);}
|
|
1466
1545
|
|
|
1467
1546
|
/* Freeday — Tabs (WAI-ARIA APG) */
|
|
1468
|
-
|
|
1547
|
+
/* position:relative — containing block for out-of-flow content in the tabs (see base.css). */
|
|
1548
|
+
.fdy-tabs__list{position:relative;display:flex;gap:var(--space-1);border-bottom:var(--bw) solid var(--color-border);overflow-x:auto;}
|
|
1469
1549
|
.fdy-tabs__tab{appearance:none;border:0;background:transparent;cursor:pointer;white-space:nowrap;font-family:var(--font-body);font-size:var(--text-sm);font-weight:var(--weight-medium);color:var(--color-text-muted);padding:var(--space-3) var(--space-4);border-bottom:2px solid transparent;margin-bottom:-1px;transition:color var(--dur-fast) var(--ease-standard),border-color var(--dur-fast) var(--ease-standard);}
|
|
1470
1550
|
.fdy-tabs__tab:hover{color:var(--color-text);}
|
|
1471
1551
|
.fdy-tabs__tab:disabled,.fdy-tabs__tab[aria-disabled="true"]{opacity:.5;cursor:not-allowed;color:var(--color-text-muted);}
|
|
1472
1552
|
.fdy-tabs__tab:disabled:hover,.fdy-tabs__tab[aria-disabled="true"]:hover{color:var(--color-text-muted);}
|
|
1473
|
-
|
|
1553
|
+
/* `aria-current="page"` is honoured alongside `aria-selected="true"` so the tab look can be used for
|
|
1554
|
+
ROUTE-driven sub-navigation (/settings/profile · /settings/billing), where the tabs are real links.
|
|
1555
|
+
A link cannot be `aria-selected` — that is invalid ARIA on an anchor; `aria-current` is the only
|
|
1556
|
+
correct marker, and it is what a router sets. Use `.fdy-tabs` + `freeday-tabs.js` (roving tabindex,
|
|
1557
|
+
a panel per tab) for in-page tabs; use these classes on plain links for routed ones, and
|
|
1558
|
+
`.fdy-nav--horizontal` for an application's PRIMARY navigation. */
|
|
1559
|
+
.fdy-tabs__tab[aria-selected="true"],.fdy-tabs__tab[aria-current="page"]{color:var(--color-primary-strong);border-bottom-color:var(--color-primary);}
|
|
1474
1560
|
.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);}
|
|
1475
1561
|
.fdy-tabs__panel{padding:var(--space-5) 0;}
|
|
1476
1562
|
.fdy-tabs__panel[hidden]{display:none;}
|
package/dist/freeday.css
CHANGED
|
@@ -15,6 +15,21 @@ h1, h2, h3, h4 { font-family: var(--font-display); font-weight: var(--weight-bol
|
|
|
15
15
|
a { color: var(--color-primary); }
|
|
16
16
|
:focus-visible { outline: var(--focus-ring-width) solid var(--focus-ring); outline-offset: var(--focus-ring-width); border-radius: var(--radius-xs); }
|
|
17
17
|
:where(button, input, select, textarea) { font: inherit; }
|
|
18
|
+
/* Hidden from sight, kept for assistive tech. `clip` hides PAINTING, not LAYOUT: the box still
|
|
19
|
+
* has a position, and because it is position:absolute its containing block is the nearest
|
|
20
|
+
* POSITIONED ancestor — with none, the initial containing block (the document itself).
|
|
21
|
+
*
|
|
22
|
+
* That matters, because `overflow` only clips a descendant whose containing block is inside the
|
|
23
|
+
* overflow box. So a hidden span in a horizontally scrolling table (the kit's own recommended way
|
|
24
|
+
* to name an icon button) parks at its static position — possibly thousands of px to the right —
|
|
25
|
+
* and drags the whole DOCUMENT sideways: invisible, and immune to `overflow-x:hidden` on every
|
|
26
|
+
* wrapper. Measured before the fix: 1351px of phantom page scroll from 11 spans.
|
|
27
|
+
*
|
|
28
|
+
* The fix is not here — it is on the containers: every clipping/scrolling container in the kit
|
|
29
|
+
* that holds consumer markup declares `position:relative` so it becomes the containing block for
|
|
30
|
+
* its own out-of-flow content (see test/css.test.mjs, which will not let a new one skip it).
|
|
31
|
+
* Note the measurement trap: documentElement.scrollWidth sees it, body.scrollWidth does not —
|
|
32
|
+
* the honest check is `window.scrollTo(9999, 0)` then reading `window.scrollX`. */
|
|
18
33
|
.fdy-visually-hidden { position: absolute !important; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
|
|
19
34
|
/* Opt-in list reset. base.css is intentionally a *light* reset — it does NOT strip list/paragraph
|
|
20
35
|
* margins (see the kit's own list components, which each reset themselves). If you run a utility
|
|
@@ -26,7 +41,11 @@ a { color: var(--color-primary); }
|
|
|
26
41
|
}
|
|
27
42
|
|
|
28
43
|
/* Freeday — Accordion (native <details>/<summary>, zero-JS) */
|
|
29
|
-
|
|
44
|
+
/* position:relative — containing block for out-of-flow panel content (see base.css). NOT redundant
|
|
45
|
+
with the panel's reveal animation: that animation gives the panel a transform (which happens to
|
|
46
|
+
make it a containing block too), but it lives behind prefers-reduced-motion:no-preference — so
|
|
47
|
+
without this line the escape bug appears ONLY for readers who asked for reduced motion. */
|
|
48
|
+
.fdy-accordion{position:relative;border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);overflow:hidden;background:var(--color-surface);}
|
|
30
49
|
.fdy-accordion__item + .fdy-accordion__item{border-top:var(--bw) solid var(--color-border);}
|
|
31
50
|
.fdy-accordion__item > summary{list-style:none;cursor:pointer;display:flex;align-items:center;gap:var(--space-3);padding:var(--space-3) var(--space-4);font-weight:var(--weight-medium);color:var(--color-text);}
|
|
32
51
|
.fdy-accordion__item > summary::-webkit-details-marker{display:none;}
|
|
@@ -84,6 +103,15 @@ a { color: var(--color-primary); }
|
|
|
84
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;}
|
|
85
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;}
|
|
86
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;}
|
|
87
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);}
|
|
88
116
|
.fdy-nav__item:hover{background:var(--color-surface-2);color:var(--color-text);}
|
|
89
117
|
/* Active indicator stays in the primary ramp so it never clashes with a re-themed accent. */
|
|
@@ -159,9 +187,18 @@ a { color: var(--color-primary); }
|
|
|
159
187
|
.fdy-appbar__brand svg{display:block;width:1.5rem;height:1.5rem;}
|
|
160
188
|
.fdy-appbar__spacer{flex:1;}
|
|
161
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. */
|
|
162
193
|
/* Controls on a coloured app bar go on-colour (icons stay legible, hover is a light wash) */
|
|
163
194
|
.fdy-appbar--primary .fdy-btn--ghost{background:transparent;border-color:transparent;color:var(--color-on-primary);box-shadow:none;}
|
|
164
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);}
|
|
165
202
|
|
|
166
203
|
/* Freeday — Autocomplete (text input + filtered suggestion listbox, APG editable combobox).
|
|
167
204
|
* Enhanced by freeday-autocomplete.js. */
|
|
@@ -264,6 +301,14 @@ a { color: var(--color-primary); }
|
|
|
264
301
|
.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);}
|
|
265
302
|
.fdy-btn:active{transform:translateY(1px);box-shadow:0 1px 4px -1px color-mix(in srgb,var(--color-primary) 46%,transparent);}
|
|
266
303
|
.fdy-btn:disabled{opacity:.5;cursor:not-allowed;transform:none;box-shadow:none;}
|
|
304
|
+
/* Pressed / toggle. `aria-pressed` is already the right attribute for a toggle button, so this adds
|
|
305
|
+
no class and no new markup contract — and it is what makes .fdy-btn-group a complete segmented
|
|
306
|
+
control: the group joins the borders, this says which segment you are on. Each fill variant
|
|
307
|
+
defines its OWN pressed look below; a single shared rule cannot work, because a gradient is a
|
|
308
|
+
background-IMAGE and would blank the ghost variant's background-color. Solid: invert the gradient
|
|
309
|
+
and sink the shadow inward, so a held button reads as depressed, not merely primary-coloured. */
|
|
310
|
+
.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);}
|
|
311
|
+
.fdy-btn[aria-pressed="true"]:hover{transform:none;box-shadow:inset 0 2px 6px color-mix(in srgb,#000 22%,transparent);}
|
|
267
312
|
|
|
268
313
|
.fdy-btn--ghost{background:var(--color-surface);color:var(--color-primary-strong);border-color:var(--color-border-strong);box-shadow:var(--shadow-1);}
|
|
269
314
|
.fdy-btn--ghost:hover{transform:none;background:var(--color-primary-soft);border-color:var(--color-primary-border);box-shadow:var(--shadow-1);}
|
|
@@ -272,6 +317,7 @@ a { color: var(--color-primary); }
|
|
|
272
317
|
.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);}
|
|
273
318
|
.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);}
|
|
274
319
|
.fdy-btn--danger:active{transform:translateY(1px);}
|
|
320
|
+
.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);}
|
|
275
321
|
|
|
276
322
|
.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);}
|
|
277
323
|
.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);}
|
|
@@ -284,6 +330,11 @@ a { color: var(--color-primary); }
|
|
|
284
330
|
.fdy-btn--text{background:none;color:var(--color-primary-strong);border-color:transparent;box-shadow:none;}
|
|
285
331
|
.fdy-btn--text:hover{transform:none;background:var(--color-primary-soft);box-shadow:none;}
|
|
286
332
|
.fdy-btn--text:active{transform:translateY(1px);box-shadow:none;}
|
|
333
|
+
/* Ghost/text pressed: the kit's selected-surface pair (soft fill + strong ink) — the same pairing
|
|
334
|
+
.fdy-nav__item uses for the current page, and one the AA contrast gate already covers. This is
|
|
335
|
+
the realistic segmented control: a row of --ghost buttons with exactly one aria-pressed. */
|
|
336
|
+
.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;}
|
|
337
|
+
.fdy-btn--ghost[aria-pressed="true"]:hover,.fdy-btn--text[aria-pressed="true"]:hover{background:var(--color-primary-soft);transform:none;box-shadow:none;}
|
|
287
338
|
|
|
288
339
|
/* Icon-only — square; combine with --ghost/--text/--sm/--lg. Requires aria-label. */
|
|
289
340
|
.fdy-btn--icon{width:var(--control-h);padding:0;}
|
|
@@ -312,7 +363,9 @@ a { color: var(--color-primary); }
|
|
|
312
363
|
.fdy-fab--accent{background:linear-gradient(180deg,var(--color-accent),color-mix(in srgb,#000 10%,var(--color-accent)));color:var(--color-on-accent);}
|
|
313
364
|
|
|
314
365
|
/* Freeday — Card */
|
|
315
|
-
|
|
366
|
+
/* position:relative — containing block for out-of-flow card content (see base.css); it also gives
|
|
367
|
+
consumers the anchor a corner ribbon/badge needs. */
|
|
368
|
+
.fdy-card{position:relative;background:var(--color-surface);border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);overflow:hidden;box-shadow:var(--shadow-lift);transition:box-shadow var(--dur-base) var(--ease-standard),transform var(--dur-base) var(--ease-standard);}
|
|
316
369
|
.fdy-card--elevated{box-shadow:var(--shadow-lift-hover);}
|
|
317
370
|
.fdy-card--interactive{cursor:pointer;}
|
|
318
371
|
.fdy-card--interactive:hover{box-shadow:var(--shadow-lift-hover);transform:translateY(-3px);}
|
|
@@ -323,6 +376,10 @@ a { color: var(--color-primary); }
|
|
|
323
376
|
* announced — reset the UA button box. Note: it does NOT reset background/border, because
|
|
324
377
|
* .fdy-card already sets its own surface + border (resetting them here would strip the card). */
|
|
325
378
|
.fdy-card--button{display:block;width:100%;text-align:inherit;color:inherit;appearance:none;-webkit-appearance:none;}
|
|
379
|
+
/* Disabled card-as-control — same contract as .fdy-list__row--button: dim + not-allowed, and the
|
|
380
|
+
--interactive lift is withdrawn so a refusing control stops answering the pointer. */
|
|
381
|
+
.fdy-card--button:disabled,.fdy-card--button[aria-disabled="true"],.fdy-card--interactive[aria-disabled="true"]{opacity:.5;cursor:not-allowed;}
|
|
382
|
+
.fdy-card--interactive:disabled:hover,.fdy-card--interactive[aria-disabled="true"]:hover{box-shadow:var(--shadow-lift);transform:none;}
|
|
326
383
|
.fdy-card__body{padding:var(--space-5);}
|
|
327
384
|
.fdy-card__title{font-family:var(--font-display);font-size:var(--text-lg);font-weight:var(--weight-semibold);letter-spacing:var(--tracking-tight);color:var(--color-text);margin:0 0 var(--space-1);}
|
|
328
385
|
.fdy-card__desc{font-size:var(--text-sm);color:var(--color-text-muted);margin:0;line-height:var(--leading-normal);}
|
|
@@ -330,7 +387,10 @@ a { color: var(--color-primary); }
|
|
|
330
387
|
|
|
331
388
|
/* Freeday — Carousel (scroll-snap track + arrows + dots). Enhanced by freeday-carousel.js. */
|
|
332
389
|
.fdy-carousel{position:relative;}
|
|
333
|
-
.fdy-
|
|
390
|
+
/* position:relative — the arrows are positioned against .fdy-carousel (the parent), so this only
|
|
391
|
+
catches out-of-flow content inside the SLIDES, which would otherwise escape the viewport
|
|
392
|
+
entirely and scroll the page by one slide-offset per slide (see base.css). */
|
|
393
|
+
.fdy-carousel__viewport{position:relative;display:flex;overflow-x:auto;scroll-snap-type:x mandatory;scroll-behavior:smooth;border-radius:var(--radius-lg);scrollbar-width:none;-ms-overflow-style:none;}
|
|
334
394
|
.fdy-carousel__viewport::-webkit-scrollbar{display:none;}
|
|
335
395
|
.fdy-carousel__slide{flex:0 0 100%;min-width:0;scroll-snap-align:center;}
|
|
336
396
|
.fdy-carousel__arrow{position:absolute;top:50%;transform:translateY(-50%);z-index:2;display:inline-flex;align-items:center;justify-content:center;width:2.25rem;height:2.25rem;border:0;border-radius:var(--radius-full);background:color-mix(in srgb,var(--color-surface) 86%,transparent);color:var(--color-text);box-shadow:var(--shadow-2);cursor:pointer;transition:background var(--dur-fast) var(--ease-standard);}
|
|
@@ -837,7 +897,8 @@ fieldset.fdy-field>legend{padding:0;float:none;}
|
|
|
837
897
|
*
|
|
838
898
|
* Not to be confused with .fdy-list-reset (base.css), which only strips UA bullets/indent.
|
|
839
899
|
* Works on <ul>/<ol> (list-style is reset here) or on plain <div>s. */
|
|
840
|
-
|
|
900
|
+
/* position:relative — containing block for out-of-flow row content (see base.css). */
|
|
901
|
+
.fdy-list{position:relative;list-style:none;margin:0;padding:0;background:var(--color-surface);border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);overflow:hidden;}
|
|
841
902
|
.fdy-list__row{display:flex;align-items:center;gap:var(--space-3);padding:var(--space-4) var(--space-5);min-width:0;}
|
|
842
903
|
/* Divider between rows. Two shapes are supported and both are load-bearing: rows as direct children
|
|
843
904
|
* of the list, and rows wrapped in <li> (the semantic shape, where the ADJACENT siblings are the
|
|
@@ -850,6 +911,12 @@ fieldset.fdy-field>legend{padding:0;float:none;}
|
|
|
850
911
|
.fdy-list__row--button:hover,.fdy-list__row--interactive:hover{background:var(--color-surface-2);}
|
|
851
912
|
.fdy-list__row--button:focus-visible{outline:none;box-shadow:inset 0 0 0 2px var(--color-primary);}
|
|
852
913
|
.fdy-list__row--interactive{cursor:pointer;}
|
|
914
|
+
/* Disabled row. The reset above adopts the UA button box, but not its disabled state — so a row
|
|
915
|
+
disabled mid-flight (a redirect in progress, say) kept lighting up and kept a pointer cursor:
|
|
916
|
+
a control answering the pointer while refusing input. Same contract as every other disabled
|
|
917
|
+
control in the kit (dim + not-allowed), hover withdrawn, both the native and the aria- form. */
|
|
918
|
+
.fdy-list__row--button:disabled,.fdy-list__row--button[aria-disabled="true"],.fdy-list__row--interactive[aria-disabled="true"]{opacity:.5;cursor:not-allowed;}
|
|
919
|
+
.fdy-list__row--button:disabled:hover,.fdy-list__row--button[aria-disabled="true"]:hover,.fdy-list__row--interactive[aria-disabled="true"]:hover{background:none;}
|
|
853
920
|
/* Row internals: a title/meta stack that truncates, and a trailing slot pinned right. */
|
|
854
921
|
.fdy-list__main{display:flex;flex-direction:column;gap:var(--space-1);min-width:0;flex:1;}
|
|
855
922
|
.fdy-list__title{font-weight:var(--weight-medium);color:var(--color-text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
@@ -1034,7 +1101,10 @@ fieldset.fdy-field>legend{padding:0;float:none;}
|
|
|
1034
1101
|
.fdy-step-nav{display:flex;justify-content:space-between;gap:var(--space-2);margin-top:var(--space-5);}
|
|
1035
1102
|
|
|
1036
1103
|
/* Freeday — Table */
|
|
1037
|
-
|
|
1104
|
+
/* position:relative on every scroller/clipper — it makes the box the containing block for its own
|
|
1105
|
+
absolutely positioned content (.fdy-visually-hidden above all), which `overflow` alone does NOT
|
|
1106
|
+
clip. Without it a hidden label in a wide table scrolls the whole page. See base.css. */
|
|
1107
|
+
.fdy-table-wrap{position:relative;overflow-x:auto;border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-1);background:var(--color-surface);}
|
|
1038
1108
|
.fdy-table{width:100%;border-collapse:collapse;font-size:var(--text-sm);}
|
|
1039
1109
|
.fdy-table caption{text-align:left;padding:var(--space-3) var(--space-4);font-weight:var(--weight-semibold);color:var(--color-text);}
|
|
1040
1110
|
.fdy-table th{text-align:left;padding:var(--space-3) var(--space-4);font-size:var(--text-xs);font-weight:var(--weight-semibold);text-transform:uppercase;letter-spacing:var(--tracking-wide);color:var(--color-text-muted);background:var(--color-surface-2);border-bottom:var(--bw) solid var(--color-border);white-space:nowrap;}
|
|
@@ -1071,7 +1141,7 @@ fieldset.fdy-field>legend{padding:0;float:none;}
|
|
|
1071
1141
|
|
|
1072
1142
|
/* Data-table shell (toolbar + scroll + footer stay put while the table scrolls) */
|
|
1073
1143
|
.fdy-datatable{border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-1);background:var(--color-surface);overflow:hidden;}
|
|
1074
|
-
.fdy-table-scroll{overflow-x:auto;}
|
|
1144
|
+
.fdy-table-scroll{position:relative;overflow-x:auto;}
|
|
1075
1145
|
.fdy-table-toolbar{display:flex;align-items:center;gap:var(--space-3);flex-wrap:wrap;padding:var(--space-3) var(--space-4);border-bottom:var(--bw) solid var(--color-border);}
|
|
1076
1146
|
.fdy-table-toolbar__search{max-width:18rem;}
|
|
1077
1147
|
.fdy-table-toolbar__spacer{flex:1;}
|
|
@@ -1104,12 +1174,19 @@ fieldset.fdy-field>legend{padding:0;float:none;}
|
|
|
1104
1174
|
.fdy-table-bulkbar__actions{display:flex;gap:var(--space-2);}
|
|
1105
1175
|
|
|
1106
1176
|
/* Freeday — Tabs (WAI-ARIA APG) */
|
|
1107
|
-
|
|
1177
|
+
/* position:relative — containing block for out-of-flow content in the tabs (see base.css). */
|
|
1178
|
+
.fdy-tabs__list{position:relative;display:flex;gap:var(--space-1);border-bottom:var(--bw) solid var(--color-border);overflow-x:auto;}
|
|
1108
1179
|
.fdy-tabs__tab{appearance:none;border:0;background:transparent;cursor:pointer;white-space:nowrap;font-family:var(--font-body);font-size:var(--text-sm);font-weight:var(--weight-medium);color:var(--color-text-muted);padding:var(--space-3) var(--space-4);border-bottom:2px solid transparent;margin-bottom:-1px;transition:color var(--dur-fast) var(--ease-standard),border-color var(--dur-fast) var(--ease-standard);}
|
|
1109
1180
|
.fdy-tabs__tab:hover{color:var(--color-text);}
|
|
1110
1181
|
.fdy-tabs__tab:disabled,.fdy-tabs__tab[aria-disabled="true"]{opacity:.5;cursor:not-allowed;color:var(--color-text-muted);}
|
|
1111
1182
|
.fdy-tabs__tab:disabled:hover,.fdy-tabs__tab[aria-disabled="true"]:hover{color:var(--color-text-muted);}
|
|
1112
|
-
|
|
1183
|
+
/* `aria-current="page"` is honoured alongside `aria-selected="true"` so the tab look can be used for
|
|
1184
|
+
ROUTE-driven sub-navigation (/settings/profile · /settings/billing), where the tabs are real links.
|
|
1185
|
+
A link cannot be `aria-selected` — that is invalid ARIA on an anchor; `aria-current` is the only
|
|
1186
|
+
correct marker, and it is what a router sets. Use `.fdy-tabs` + `freeday-tabs.js` (roving tabindex,
|
|
1187
|
+
a panel per tab) for in-page tabs; use these classes on plain links for routed ones, and
|
|
1188
|
+
`.fdy-nav--horizontal` for an application's PRIMARY navigation. */
|
|
1189
|
+
.fdy-tabs__tab[aria-selected="true"],.fdy-tabs__tab[aria-current="page"]{color:var(--color-primary-strong);border-bottom-color:var(--color-primary);}
|
|
1113
1190
|
.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);}
|
|
1114
1191
|
.fdy-tabs__panel{padding:var(--space-5) 0;}
|
|
1115
1192
|
.fdy-tabs__panel[hidden]{display:none;}
|
package/dist/freeday.tokens.css
CHANGED
|
@@ -176,6 +176,9 @@
|
|
|
176
176
|
--focus-ring: var(--azure-600);
|
|
177
177
|
--focus-ring-width: 2px;
|
|
178
178
|
}
|
|
179
|
+
/* The SYSTEM default stays root-scoped: it is a statement about the document, and dropping :root
|
|
180
|
+
* here would match every element that is not itself [data-theme="light"] — which would re-darken
|
|
181
|
+
* the children of a light panel, since they carry no attribute of their own. */
|
|
179
182
|
@media (prefers-color-scheme: dark) {
|
|
180
183
|
:root:not([data-theme="light"]) {
|
|
181
184
|
--shadow-1: 0 1px 2px rgba(0,0,0,.4);
|
|
@@ -234,7 +237,13 @@
|
|
|
234
237
|
--focus-ring: var(--azure-400);
|
|
235
238
|
}
|
|
236
239
|
}
|
|
237
|
-
:root
|
|
240
|
+
/* The two EXPLICIT opt-ins are deliberately NOT scoped to :root, for the same reason as density
|
|
241
|
+
* below: these are inheriting custom properties, so data-theme on any ancestor re-themes just that
|
|
242
|
+
* subtree — a dark brand panel beside a light form is an ordinary layout, and it should not require
|
|
243
|
+
* re-colouring each component by hand. The root still matches, so data-theme on <html> is unchanged.
|
|
244
|
+
* Both keep the same specificity (0,1,0) as :root and come after it, so they still win there; and
|
|
245
|
+
* --light after --dark means a light island inside a dark region wins in turn. */
|
|
246
|
+
[data-theme="dark"] {
|
|
238
247
|
--shadow-1: 0 1px 2px rgba(0,0,0,.4);
|
|
239
248
|
--shadow-2: 0 2px 6px rgba(0,0,0,.45);
|
|
240
249
|
--shadow-3: 0 6px 20px -2px rgba(0,0,0,.6),0 1px 3px rgba(0,0,0,.5);
|
|
@@ -290,7 +299,7 @@
|
|
|
290
299
|
--chart-tick: var(--slate-600);
|
|
291
300
|
--focus-ring: var(--azure-400);
|
|
292
301
|
}
|
|
293
|
-
|
|
302
|
+
[data-theme="light"] {
|
|
294
303
|
--shadow-1: 0 1px 2px rgba(16,14,30,.06);
|
|
295
304
|
--shadow-2: 0 2px 6px rgba(16,14,30,.07),0 1px 2px rgba(16,14,30,.05);
|
|
296
305
|
--shadow-3: 0 4px 16px -2px rgba(16,14,30,.14),0 1px 3px rgba(16,14,30,.10);
|
package/docs/agent-onboarding.md
CHANGED
|
@@ -18,8 +18,38 @@ the **root of the consuming project**:
|
|
|
18
18
|
```markdown
|
|
19
19
|
## UI: Freeday design system (@cahyo-dimas/freeday)
|
|
20
20
|
|
|
21
|
-
All UI in this project is built from Freeday
|
|
22
|
-
|
|
21
|
+
All UI in this project is built from Freeday: a **token-driven CSS kit** (`fdy-*` classes on plain
|
|
22
|
+
markup) with **typed components for Vue, React and Blazor** layered on top. Most of the kit is
|
|
23
|
+
markup + classes; ten interactive components also ship a typed wrapper, and in those three stacks
|
|
24
|
+
the wrapper is the correct way to use them.
|
|
25
|
+
|
|
26
|
+
**0. First decide which entry point this project uses. This is not an optimisation — get it wrong
|
|
27
|
+
and the code looks correct and fails later.**
|
|
28
|
+
|
|
29
|
+
| This project's stack | Import the ten components from | Binding |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| Vue 3 | `@cahyo-dimas/freeday/vue` | `v-model` |
|
|
32
|
+
| React 18/19 | `@cahyo-dimas/freeday/react` | `value` + `onChange` |
|
|
33
|
+
| Blazor (net8.0) | `@using Freeday.Blazor` (RCL) | `@bind-Value` |
|
|
34
|
+
| Static HTML, Svelte, server-rendered templates… | no wrapper — raw markup + the enhancer script | `fdy-*` DOM events |
|
|
35
|
+
|
|
36
|
+
The ten: **FdyCombo · FdyDatepicker · FdyDateRange · FdyAutocomplete · FdyCascade · FdyCfl ·
|
|
37
|
+
FdyChart · FdyTable · FdyModal · FdyDrawer**. In Vue/React/Blazor, **never hand-write the raw
|
|
38
|
+
markup + enhancer for these ten.** The raw path *appears* to work — the enhancer auto-initialises
|
|
39
|
+
once on `DOMContentLoaded` and the first render is correct — then fails quietly: DOM your framework
|
|
40
|
+
renders later is never hydrated, and the widget's state lives in the DOM instead of in your
|
|
41
|
+
framework's state.
|
|
42
|
+
|
|
43
|
+
Everything else is the same in every stack: plain `fdy-*` markup (button, card, badge, alert,
|
|
44
|
+
table markup, layout…). For the *other* interactive components (chips, stepper, input mask, file
|
|
45
|
+
upload, tree, tabs, menu, rating, slider, form validation, carousel, timepicker) there is no
|
|
46
|
+
wrapper — use the raw markup and hydrate it:
|
|
47
|
+
|
|
48
|
+
- **Vue / React** — `useFreeday(rootRef)` from the same import path, plus `import '@cahyo-dimas/freeday'`
|
|
49
|
+
once at app entry to register the enhancers. (The ten typed components do **not** need this: they
|
|
50
|
+
are native Vue/React implementations of the same markup, not wrappers over the enhancer.)
|
|
51
|
+
- **Blazor** — `FreedayBlazor.initAll` interop. (Here the typed components *are* thin wrappers over
|
|
52
|
+
the enhancers, so the enhancer script is always required.)
|
|
23
53
|
|
|
24
54
|
**Before writing or editing any markup/CSS, read these (they ship inside the package):**
|
|
25
55
|
- `node_modules/@cahyo-dimas/freeday/COMPONENTS.md` — every class that exists, with minimal markup
|
|
@@ -40,8 +70,9 @@ enhancers**, not a component framework — components are plain markup with `fdy
|
|
|
40
70
|
5. Assemble from the frame down: `.fdy-app` → `.fdy-page` → `.fdy-page-section` → components.
|
|
41
71
|
6. Form errors: `aria-invalid="true"` + `aria-describedby` → a `.fdy-help.fdy-help--error`.
|
|
42
72
|
Icon-only buttons need `aria-label`. Status is never colour-only.
|
|
43
|
-
7. Interactive components need their enhancer script loaded (see the table in COMPONENTS.md);
|
|
44
|
-
|
|
73
|
+
7. Interactive components need their enhancer script loaded (see the table in COMPONENTS.md); in an
|
|
74
|
+
SPA, re-hydrate dynamic DOM with `useFreeday` / `FreedayBlazor.initAll`. This applies to the
|
|
75
|
+
components **without** a typed wrapper — for the ten in step 0, use the wrapper instead.
|
|
45
76
|
8. Freeday owns components + tokens, **not layout**. Grids/stacks/one-off gaps come from our own
|
|
46
77
|
layout layer — build its theme on `var(--space-N)` so both systems stay in step.
|
|
47
78
|
```
|
|
@@ -82,8 +113,10 @@ The order matters; skipping to components is what produces flat, identical-card
|
|
|
82
113
|
you build the frame instead of inventing a class. Not in the npm package — read it on GitHub.
|
|
83
114
|
1. **Shell** — is `.fdy-app` already in place (usually once, in the app layout)? If not, copy it
|
|
84
115
|
from `docs/getting-started.md` §The app shell.
|
|
85
|
-
2. **Theme** — `data-theme="light|dark"` + `data-density="comfortable|compact"
|
|
86
|
-
once at the root. Use `compact` for table-heavy back-office screens.
|
|
116
|
+
2. **Theme** — `data-theme="light|dark"` + `data-density="comfortable|compact"`, normally on
|
|
117
|
+
`<html>`, set once at the root. Use `compact` for table-heavy back-office screens. Both attributes
|
|
118
|
+
also work on **any ancestor**: `<section data-theme="dark">` inverts that region and every
|
|
119
|
+
component inside it follows, so never hand-colour an inverted panel.
|
|
87
120
|
3. **Fonts** — the package ships **no** `@font-face`. Load Sora / IBM Plex Sans / JetBrains Mono
|
|
88
121
|
yourself, or override `--font-display`/`--font-body`/`--font-mono`. Skipping this reads as
|
|
89
122
|
"unfinished design", not "missing dependency".
|
package/docs/getting-started.md
CHANGED
|
@@ -17,16 +17,23 @@ Freeday = **CSS** (semantic tokens + `fdy-*` classes) + **zero-dependency JS enh
|
|
|
17
17
|
1. **Static vs interactive.** Static components (button, card, badge, plain input, layout) need
|
|
18
18
|
only the **`fdy-*` classes** — no JS. Interactive components (select/combo, cascade, date/time
|
|
19
19
|
picker, table, dropzone, form validation, input mask, chip) need the **JS enhancers**.
|
|
20
|
-
2. **The enhancer is the source of truth
|
|
21
|
-
the widget's DOM. You **listen for `fdy-*` events** (all bubbling
|
|
22
|
-
`event.detail`) → store them in your framework state. Event/API contract
|
|
23
|
-
[`integrations.md` §Event & API contract](integrations.md).
|
|
20
|
+
2. **The enhancer is the source of truth — *on the raw path*.** You don't re-implement components;
|
|
21
|
+
the enhancer owns the widget's DOM. You **listen for `fdy-*` events** (all bubbling
|
|
22
|
+
`CustomEvent`s, data in `event.detail`) → store them in your framework state. Event/API contract
|
|
23
|
+
table: [`integrations.md` §Event & API contract](integrations.md).
|
|
24
|
+
**On Vue, React or Blazor this is not the path to take for ten of the components** — `FdyCombo`,
|
|
25
|
+
`FdyDatepicker`, `FdyDateRange`, `FdyAutocomplete`, `FdyCascade`, `FdyCfl`, `FdyChart`,
|
|
26
|
+
`FdyTable`, `FdyModal`, `FdyDrawer` ship typed wrappers that own the state properly (Vue and
|
|
27
|
+
React re-implement the interaction natively; Blazor wraps the enhancer over interop). Use them;
|
|
28
|
+
the raw path is for the components without a wrapper, and for stacks without an adapter.
|
|
24
29
|
3. **Hydrate dynamic DOM.** Enhancers auto-init once on `DOMContentLoaded`. DOM an SPA renders
|
|
25
30
|
**after** that must be re-hydrated: `window.Freeday<X>.initAll(el)` (idempotent, safe to repeat).
|
|
26
31
|
Each framework's adapter wraps this — you don't call it manually.
|
|
27
|
-
4. **Theme via `data
|
|
28
|
-
`data-density="comfortable|compact"` (control height, for data-dense screens).
|
|
29
|
-
`document.documentElement.dataset.theme = 'dark'`.
|
|
32
|
+
4. **Theme via `data-*`.** `data-theme="light|dark"` (all semantic tokens switch) +
|
|
33
|
+
`data-density="comfortable|compact"` (control height, for data-dense screens). Normally on
|
|
34
|
+
`<html>`; change at runtime with `document.documentElement.dataset.theme = 'dark'`. Both also
|
|
35
|
+
work on **any ancestor** — these are inheriting custom properties, so `<section data-theme="dark">`
|
|
36
|
+
inverts just that region and every component inside it follows. See [`USAGE.md`](../USAGE.md) §5b.
|
|
30
37
|
5. **3-tier token rule.** Components only touch **Tier 2/3** (`var(--color-primary)`,
|
|
31
38
|
`var(--space-4)`, `var(--radius-md)`…). **Never** write raw hex/px.
|
|
32
39
|
6. **Scope: components + tokens, deliberately *not* layout.** Freeday ships components and tokens;
|
|
@@ -177,7 +184,7 @@ live docs also have a copy button per component.
|
|
|
177
184
|
```bash
|
|
178
185
|
npm i @cahyo-dimas/freeday
|
|
179
186
|
```
|
|
180
|
-
Lands in `package.json` as `"@cahyo-dimas/freeday": "^1.
|
|
187
|
+
Lands in `package.json` as `"@cahyo-dimas/freeday": "^1.22.0"` (public npm package). `dist/` is
|
|
181
188
|
committed and published → no build step; `npm ci` runs without auth.
|
|
182
189
|
|
|
183
190
|
### 2. Import the CSS + enhancers **once** in your entry (`src/main.ts`)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cahyo-dimas/freeday",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.22.0",
|
|
4
4
|
"description": "Freeday — token-driven, framework-agnostic UI KIT (design source-of-truth).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"scripts": {
|
|
72
72
|
"build": "node tokens/build.mjs",
|
|
73
73
|
"test": "node --test",
|
|
74
|
-
"test:browser": "node --test browser/vanilla.mjs browser/adapter.mjs",
|
|
74
|
+
"test:browser": "node --test browser/vanilla.mjs browser/adapter.mjs browser/layout.mjs browser/theme.mjs browser/state.mjs",
|
|
75
75
|
"prepack": "node tokens/build.mjs",
|
|
76
76
|
"version": "node tokens/build.mjs && git add dist",
|
|
77
77
|
"typecheck:react": "tsc -p adapters/react/tsconfig.json --noEmit"
|