@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/src/base.css CHANGED
@@ -14,6 +14,21 @@ h1, h2, h3, h4 { font-family: var(--font-display); font-weight: var(--weight-bol
14
14
  a { color: var(--color-primary); }
15
15
  :focus-visible { outline: var(--focus-ring-width) solid var(--focus-ring); outline-offset: var(--focus-ring-width); border-radius: var(--radius-xs); }
16
16
  :where(button, input, select, textarea) { font: inherit; }
17
+ /* Hidden from sight, kept for assistive tech. `clip` hides PAINTING, not LAYOUT: the box still
18
+ * has a position, and because it is position:absolute its containing block is the nearest
19
+ * POSITIONED ancestor — with none, the initial containing block (the document itself).
20
+ *
21
+ * That matters, because `overflow` only clips a descendant whose containing block is inside the
22
+ * overflow box. So a hidden span in a horizontally scrolling table (the kit's own recommended way
23
+ * to name an icon button) parks at its static position — possibly thousands of px to the right —
24
+ * and drags the whole DOCUMENT sideways: invisible, and immune to `overflow-x:hidden` on every
25
+ * wrapper. Measured before the fix: 1351px of phantom page scroll from 11 spans.
26
+ *
27
+ * The fix is not here — it is on the containers: every clipping/scrolling container in the kit
28
+ * that holds consumer markup declares `position:relative` so it becomes the containing block for
29
+ * its own out-of-flow content (see test/css.test.mjs, which will not let a new one skip it).
30
+ * Note the measurement trap: documentElement.scrollWidth sees it, body.scrollWidth does not —
31
+ * the honest check is `window.scrollTo(9999, 0)` then reading `window.scrollX`. */
17
32
  .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; }
18
33
  /* Opt-in list reset. base.css is intentionally a *light* reset — it does NOT strip list/paragraph
19
34
  * margins (see the kit's own list components, which each reset themselves). If you run a utility
@@ -1,5 +1,9 @@
1
1
  /* Freeday — Accordion (native <details>/<summary>, zero-JS) */
2
- .fdy-accordion{border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);overflow:hidden;background:var(--color-surface);}
2
+ /* position:relative — containing block for out-of-flow panel content (see base.css). NOT redundant
3
+ with the panel's reveal animation: that animation gives the panel a transform (which happens to
4
+ make it a containing block too), but it lives behind prefers-reduced-motion:no-preference — so
5
+ without this line the escape bug appears ONLY for readers who asked for reduced motion. */
6
+ .fdy-accordion{position:relative;border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);overflow:hidden;background:var(--color-surface);}
3
7
  .fdy-accordion__item + .fdy-accordion__item{border-top:var(--bw) solid var(--color-border);}
4
8
  .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);}
5
9
  .fdy-accordion__item > summary::-webkit-details-marker{display:none;}
@@ -24,6 +24,15 @@
24
24
  .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;}
25
25
  .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;}
26
26
  .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);}
27
+ /* Horizontal variant — the SAME nav links laid out as a row, for a top-nav application (one that
28
+ * puts its primary navigation in `.fdy-appbar` / `.fdy-app__topbar` and has no sidebar). Deliberately
29
+ * a modifier and not a new block: the item, its states and `aria-current="page"` are unchanged. That
30
+ * is the point — a navigation link is a link marked `aria-current`, never `aria-selected` (invalid
31
+ * ARIA on an anchor) and never a tab role, so borrowing `.fdy-tabs` for routes half-adopts a contract
32
+ * (roving tabindex, a panel per tab) that route links do not honour. position:relative because the
33
+ * row scrolls; see base.css on why a scroller must be a containing block. */
34
+ .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;}
35
+ .fdy-nav--horizontal .fdy-nav__item{white-space:nowrap;}
27
36
  .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);}
28
37
  .fdy-nav__item:hover{background:var(--color-surface-2);color:var(--color-text);}
29
38
  /* Active indicator stays in the primary ramp so it never clashes with a re-themed accent. */
@@ -10,6 +10,15 @@
10
10
  .fdy-appbar__brand svg{display:block;width:1.5rem;height:1.5rem;}
11
11
  .fdy-appbar__spacer{flex:1;}
12
12
  .fdy-appbar__actions{display:inline-flex;align-items:center;gap:var(--space-1);}
13
+ /* Primary navigation inside the bar: `<nav class="fdy-nav fdy-nav--horizontal">` with
14
+ `.fdy-nav__item` links (see app-shell.css). The bar ships no link class of its own — a nav link
15
+ is the same component whether the nav is a sidebar column or a top row. */
13
16
  /* Controls on a coloured app bar go on-colour (icons stay legible, hover is a light wash) */
14
17
  .fdy-appbar--primary .fdy-btn--ghost{background:transparent;border-color:transparent;color:var(--color-on-primary);box-shadow:none;}
15
18
  .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;}
19
+ /* Nav links on a coloured bar: full on-colour ink in every state (a dimmed variant would trade away
20
+ the contrast the bar's own token pair guarantees). The current page is carried by the background
21
+ wash plus the semibold weight the base rule already applies — and by `aria-current` itself. */
22
+ .fdy-appbar--primary .fdy-nav__item{color:var(--color-on-primary);}
23
+ .fdy-appbar--primary .fdy-nav__item:hover{background:color-mix(in srgb,var(--color-on-primary) 16%,transparent);color:var(--color-on-primary);}
24
+ .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);}
@@ -10,6 +10,14 @@
10
10
  .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);}
11
11
  .fdy-btn:active{transform:translateY(1px);box-shadow:0 1px 4px -1px color-mix(in srgb,var(--color-primary) 46%,transparent);}
12
12
  .fdy-btn:disabled{opacity:.5;cursor:not-allowed;transform:none;box-shadow:none;}
13
+ /* Pressed / toggle. `aria-pressed` is already the right attribute for a toggle button, so this adds
14
+ no class and no new markup contract — and it is what makes .fdy-btn-group a complete segmented
15
+ control: the group joins the borders, this says which segment you are on. Each fill variant
16
+ defines its OWN pressed look below; a single shared rule cannot work, because a gradient is a
17
+ background-IMAGE and would blank the ghost variant's background-color. Solid: invert the gradient
18
+ and sink the shadow inward, so a held button reads as depressed, not merely primary-coloured. */
19
+ .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);}
20
+ .fdy-btn[aria-pressed="true"]:hover{transform:none;box-shadow:inset 0 2px 6px color-mix(in srgb,#000 22%,transparent);}
13
21
 
14
22
  .fdy-btn--ghost{background:var(--color-surface);color:var(--color-primary-strong);border-color:var(--color-border-strong);box-shadow:var(--shadow-1);}
15
23
  .fdy-btn--ghost:hover{transform:none;background:var(--color-primary-soft);border-color:var(--color-primary-border);box-shadow:var(--shadow-1);}
@@ -18,6 +26,7 @@
18
26
  .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);}
19
27
  .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);}
20
28
  .fdy-btn--danger:active{transform:translateY(1px);}
29
+ .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);}
21
30
 
22
31
  .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);}
23
32
  .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);}
@@ -30,6 +39,11 @@
30
39
  .fdy-btn--text{background:none;color:var(--color-primary-strong);border-color:transparent;box-shadow:none;}
31
40
  .fdy-btn--text:hover{transform:none;background:var(--color-primary-soft);box-shadow:none;}
32
41
  .fdy-btn--text:active{transform:translateY(1px);box-shadow:none;}
42
+ /* Ghost/text pressed: the kit's selected-surface pair (soft fill + strong ink) — the same pairing
43
+ .fdy-nav__item uses for the current page, and one the AA contrast gate already covers. This is
44
+ the realistic segmented control: a row of --ghost buttons with exactly one aria-pressed. */
45
+ .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;}
46
+ .fdy-btn--ghost[aria-pressed="true"]:hover,.fdy-btn--text[aria-pressed="true"]:hover{background:var(--color-primary-soft);transform:none;box-shadow:none;}
33
47
 
34
48
  /* Icon-only — square; combine with --ghost/--text/--sm/--lg. Requires aria-label. */
35
49
  .fdy-btn--icon{width:var(--control-h);padding:0;}
@@ -1,5 +1,7 @@
1
1
  /* Freeday — Card */
2
- .fdy-card{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);}
2
+ /* position:relative — containing block for out-of-flow card content (see base.css); it also gives
3
+ consumers the anchor a corner ribbon/badge needs. */
4
+ .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);}
3
5
  .fdy-card--elevated{box-shadow:var(--shadow-lift-hover);}
4
6
  .fdy-card--interactive{cursor:pointer;}
5
7
  .fdy-card--interactive:hover{box-shadow:var(--shadow-lift-hover);transform:translateY(-3px);}
@@ -10,6 +12,10 @@
10
12
  * announced — reset the UA button box. Note: it does NOT reset background/border, because
11
13
  * .fdy-card already sets its own surface + border (resetting them here would strip the card). */
12
14
  .fdy-card--button{display:block;width:100%;text-align:inherit;color:inherit;appearance:none;-webkit-appearance:none;}
15
+ /* Disabled card-as-control — same contract as .fdy-list__row--button: dim + not-allowed, and the
16
+ --interactive lift is withdrawn so a refusing control stops answering the pointer. */
17
+ .fdy-card--button:disabled,.fdy-card--button[aria-disabled="true"],.fdy-card--interactive[aria-disabled="true"]{opacity:.5;cursor:not-allowed;}
18
+ .fdy-card--interactive:disabled:hover,.fdy-card--interactive[aria-disabled="true"]:hover{box-shadow:var(--shadow-lift);transform:none;}
13
19
  .fdy-card__body{padding:var(--space-5);}
14
20
  .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);}
15
21
  .fdy-card__desc{font-size:var(--text-sm);color:var(--color-text-muted);margin:0;line-height:var(--leading-normal);}
@@ -1,6 +1,9 @@
1
1
  /* Freeday — Carousel (scroll-snap track + arrows + dots). Enhanced by freeday-carousel.js. */
2
2
  .fdy-carousel{position:relative;}
3
- .fdy-carousel__viewport{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;}
3
+ /* position:relative — the arrows are positioned against .fdy-carousel (the parent), so this only
4
+ catches out-of-flow content inside the SLIDES, which would otherwise escape the viewport
5
+ entirely and scroll the page by one slide-offset per slide (see base.css). */
6
+ .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;}
4
7
  .fdy-carousel__viewport::-webkit-scrollbar{display:none;}
5
8
  .fdy-carousel__slide{flex:0 0 100%;min-width:0;scroll-snap-align:center;}
6
9
  .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);}
@@ -8,7 +8,8 @@
8
8
  *
9
9
  * Not to be confused with .fdy-list-reset (base.css), which only strips UA bullets/indent.
10
10
  * Works on <ul>/<ol> (list-style is reset here) or on plain <div>s. */
11
- .fdy-list{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;}
11
+ /* position:relative — containing block for out-of-flow row content (see base.css). */
12
+ .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;}
12
13
  .fdy-list__row{display:flex;align-items:center;gap:var(--space-3);padding:var(--space-4) var(--space-5);min-width:0;}
13
14
  /* Divider between rows. Two shapes are supported and both are load-bearing: rows as direct children
14
15
  * of the list, and rows wrapped in <li> (the semantic shape, where the ADJACENT siblings are the
@@ -21,6 +22,12 @@
21
22
  .fdy-list__row--button:hover,.fdy-list__row--interactive:hover{background:var(--color-surface-2);}
22
23
  .fdy-list__row--button:focus-visible{outline:none;box-shadow:inset 0 0 0 2px var(--color-primary);}
23
24
  .fdy-list__row--interactive{cursor:pointer;}
25
+ /* Disabled row. The reset above adopts the UA button box, but not its disabled state — so a row
26
+ disabled mid-flight (a redirect in progress, say) kept lighting up and kept a pointer cursor:
27
+ a control answering the pointer while refusing input. Same contract as every other disabled
28
+ control in the kit (dim + not-allowed), hover withdrawn, both the native and the aria- form. */
29
+ .fdy-list__row--button:disabled,.fdy-list__row--button[aria-disabled="true"],.fdy-list__row--interactive[aria-disabled="true"]{opacity:.5;cursor:not-allowed;}
30
+ .fdy-list__row--button:disabled:hover,.fdy-list__row--button[aria-disabled="true"]:hover,.fdy-list__row--interactive[aria-disabled="true"]:hover{background:none;}
24
31
  /* Row internals: a title/meta stack that truncates, and a trailing slot pinned right. */
25
32
  .fdy-list__main{display:flex;flex-direction:column;gap:var(--space-1);min-width:0;flex:1;}
26
33
  .fdy-list__title{font-weight:var(--weight-medium);color:var(--color-text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
@@ -1,5 +1,8 @@
1
1
  /* Freeday — Table */
2
- .fdy-table-wrap{overflow-x:auto;border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-1);background:var(--color-surface);}
2
+ /* position:relative on every scroller/clipper — it makes the box the containing block for its own
3
+ absolutely positioned content (.fdy-visually-hidden above all), which `overflow` alone does NOT
4
+ clip. Without it a hidden label in a wide table scrolls the whole page. See base.css. */
5
+ .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);}
3
6
  .fdy-table{width:100%;border-collapse:collapse;font-size:var(--text-sm);}
4
7
  .fdy-table caption{text-align:left;padding:var(--space-3) var(--space-4);font-weight:var(--weight-semibold);color:var(--color-text);}
5
8
  .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;}
@@ -36,7 +39,7 @@
36
39
 
37
40
  /* Data-table shell (toolbar + scroll + footer stay put while the table scrolls) */
38
41
  .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;}
39
- .fdy-table-scroll{overflow-x:auto;}
42
+ .fdy-table-scroll{position:relative;overflow-x:auto;}
40
43
  .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);}
41
44
  .fdy-table-toolbar__search{max-width:18rem;}
42
45
  .fdy-table-toolbar__spacer{flex:1;}
@@ -1,10 +1,17 @@
1
1
  /* Freeday — Tabs (WAI-ARIA APG) */
2
- .fdy-tabs__list{display:flex;gap:var(--space-1);border-bottom:var(--bw) solid var(--color-border);overflow-x:auto;}
2
+ /* position:relative — containing block for out-of-flow content in the tabs (see base.css). */
3
+ .fdy-tabs__list{position:relative;display:flex;gap:var(--space-1);border-bottom:var(--bw) solid var(--color-border);overflow-x:auto;}
3
4
  .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);}
4
5
  .fdy-tabs__tab:hover{color:var(--color-text);}
5
6
  .fdy-tabs__tab:disabled,.fdy-tabs__tab[aria-disabled="true"]{opacity:.5;cursor:not-allowed;color:var(--color-text-muted);}
6
7
  .fdy-tabs__tab:disabled:hover,.fdy-tabs__tab[aria-disabled="true"]:hover{color:var(--color-text-muted);}
7
- .fdy-tabs__tab[aria-selected="true"]{color:var(--color-primary-strong);border-bottom-color:var(--color-primary);}
8
+ /* `aria-current="page"` is honoured alongside `aria-selected="true"` so the tab look can be used for
9
+ ROUTE-driven sub-navigation (/settings/profile · /settings/billing), where the tabs are real links.
10
+ A link cannot be `aria-selected` — that is invalid ARIA on an anchor; `aria-current` is the only
11
+ correct marker, and it is what a router sets. Use `.fdy-tabs` + `freeday-tabs.js` (roving tabindex,
12
+ a panel per tab) for in-page tabs; use these classes on plain links for routed ones, and
13
+ `.fdy-nav--horizontal` for an application's PRIMARY navigation. */
14
+ .fdy-tabs__tab[aria-selected="true"],.fdy-tabs__tab[aria-current="page"]{color:var(--color-primary-strong);border-bottom-color:var(--color-primary);}
8
15
  .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);}
9
16
  .fdy-tabs__panel{padding:var(--space-5) 0;}
10
17
  .fdy-tabs__panel[hidden]{display:none;}