@cahyo-dimas/freeday 3.4.0 → 3.6.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/COMPONENTS.md +278 -12
  3. package/README.id.md +9 -5
  4. package/README.md +9 -5
  5. package/adapters/blazor/FdyDateRange.razor.cs +10 -0
  6. package/adapters/blazor/FdyTable.razor +6 -2
  7. package/adapters/blazor/FdyTable.razor.cs +21 -0
  8. package/adapters/blazor/TableTypes.cs +4 -0
  9. package/adapters/react/components/FdyDateRange.tsx +8 -2
  10. package/adapters/react/components/FdyTable.tsx +24 -3
  11. package/adapters/react/index.d.ts +1 -1
  12. package/adapters/vue/components/FdyDateRange.vue +11 -5
  13. package/adapters/vue/components/FdyTable.vue +18 -2
  14. package/dist/freeday-confirm.js +101 -0
  15. package/dist/freeday-copy.js +58 -0
  16. package/dist/freeday-datepicker.js +17 -2
  17. package/dist/freeday-form.js +30 -1
  18. package/dist/freeday-search.js +77 -0
  19. package/dist/freeday.bundle.css +98 -8
  20. package/dist/freeday.css +98 -8
  21. package/dist/freeday.d.ts +17 -2
  22. package/dist/freeday.js +286 -3
  23. package/dist/freeday.media.css +27 -0
  24. package/docs/agent-onboarding.md +10 -0
  25. package/docs/getting-started.md +1 -1
  26. package/package.json +6 -2
  27. package/src/components/avatar.css +2 -0
  28. package/src/components/breakpoints.css +7 -1
  29. package/src/components/card.css +7 -0
  30. package/src/components/composition.css +36 -0
  31. package/src/components/description-list.css +11 -1
  32. package/src/components/filterbar.css +8 -1
  33. package/src/components/selection.css +6 -0
  34. package/src/components/states.css +7 -0
  35. package/src/components/table.css +14 -5
  36. package/tokens/breakpoints.d.ts +4 -0
  37. package/tokens/breakpoints.mjs +22 -8
@@ -0,0 +1,77 @@
1
+ /* Freeday, list search enhancer (optional, zero-dependency). Note #061.
2
+ * Turns an .fdy-input-group into a search box that searches while the reader pauses, at once on
3
+ * Enter, and has a clear button. All three paths end in ONE bubbling event on the group,
4
+ * "fdy-search" {term}, so an app listens once instead of wiring keyup, a timer and a click.
5
+ *
6
+ * Markup (the group, not a new block; the clear button is yours, so is its aria-label):
7
+ * <div class="fdy-input-group" data-fdy-search [data-fdy-search-delay="400"]>
8
+ * <span class="fdy-input-group__addon fdy-input-group__addon--icon">…svg…</span>
9
+ * <input class="fdy-input" type="text" aria-label="Search orders">
10
+ * <button type="button" class="fdy-input-group__btn" data-fdy-search-clear aria-label="Clear search">×</button>
11
+ * </div>
12
+ *
13
+ * type="text", not "search": WebKit's own × is unthemed and fires no Enter, which is what made
14
+ * clearing a field leave the old rows on screen until somebody pressed Enter on an empty box.
15
+ * The clear button is hidden while the box is empty. Clearing writes through the native value
16
+ * setter and fires a native `input`, so v-model, a React onChange and Blazor @bind all follow.
17
+ * `term` is trimmed; "" means "show everything again". Exposes window.FreedaySearch.
18
+ */
19
+ (function () {
20
+ 'use strict';
21
+
22
+ var DEFAULT_DELAY = 400;
23
+ var nativeValue = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set;
24
+
25
+ function initSearch(root) {
26
+ if (root.dataset.fdySearchReady === '1') return;
27
+ var input = root.querySelector('input');
28
+ if (!input) return;
29
+ root.dataset.fdySearchReady = '1';
30
+ var clearBtn = root.querySelector('[data-fdy-search-clear]');
31
+ var delayAttr = parseInt(root.getAttribute('data-fdy-search-delay'), 10);
32
+ var delay = isNaN(delayAttr) ? DEFAULT_DELAY : delayAttr;
33
+ var timer = null;
34
+
35
+ function cancel() { if (timer !== null) { clearTimeout(timer); timer = null; } }
36
+ function emit() {
37
+ cancel();
38
+ root.dispatchEvent(new CustomEvent('fdy-search', { bubbles: true, detail: { term: input.value.trim() } }));
39
+ }
40
+ function syncClear() { if (clearBtn) clearBtn.hidden = input.value === ''; }
41
+
42
+ input.addEventListener('input', function () {
43
+ syncClear();
44
+ cancel();
45
+ timer = setTimeout(emit, delay);
46
+ });
47
+ input.addEventListener('keydown', function (e) {
48
+ if (e.key !== 'Enter') return;
49
+ e.preventDefault(); // inside a <form>, Enter would submit it as well
50
+ emit();
51
+ });
52
+ if (clearBtn) {
53
+ clearBtn.addEventListener('click', function () {
54
+ nativeValue.call(input, '');
55
+ input.dispatchEvent(new Event('input', { bubbles: true }));
56
+ emit(); // at once, and it cancels the pause the input event just started
57
+ input.focus();
58
+ });
59
+ }
60
+ syncClear();
61
+ }
62
+
63
+ function initAll(context) {
64
+ var root = context || document;
65
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
66
+ if (root.matches && root.matches('[data-fdy-search]')) initSearch(root);
67
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-search]'), initSearch);
68
+ }
69
+
70
+ if (document.readyState === 'loading') {
71
+ document.addEventListener('DOMContentLoaded', function () { initAll(); });
72
+ } else {
73
+ initAll();
74
+ }
75
+
76
+ window.FreedaySearch = { init: initSearch, initAll: initAll };
77
+ })();
@@ -1151,6 +1151,8 @@ a { color: var(--color-primary); }
1151
1151
  .fdy-avatar--xs{width:1.5rem;height:1.5rem;font-size:var(--text-xs);}
1152
1152
  .fdy-avatar--sm{width:2rem;height:2rem;font-size:var(--text-xs);}
1153
1153
  .fdy-avatar--lg{width:3.5rem;height:3.5rem;font-size:var(--text-lg);}
1154
+ /* Square: a mark that is not a person (a company logo, a card's icon chip, #070). */
1155
+ .fdy-avatar--square{border-radius:var(--radius-md);}
1154
1156
  .fdy-avatar-group{display:inline-flex;}
1155
1157
  .fdy-avatar-group .fdy-avatar{border:2px solid var(--color-surface);margin-left:calc(var(--space-2)*-1);}
1156
1158
  .fdy-avatar-group .fdy-avatar:first-child{margin-left:0;}
@@ -1290,8 +1292,14 @@ a { color: var(--color-primary); }
1290
1292
 
1291
1293
  /* Freeday. Responsive display utilities (pairs with freeday-breakpoint.js).
1292
1294
  * Breakpoint scale (min-width, px): sm 600 · md 960 · lg 1280 · xl 1920.
1295
+ * Two further widths exist and are NOT ramp steps: `nav` (app-shell.css, off-canvas drawer vs
1296
+ * static sidebar) and `filterbar` (filterbar.css, the width its fields stack at). Both are named
1297
+ * in tokens/breakpoints.mjs; a rule that has to fire with one of them aligns to it, not to the
1298
+ * nearest ramp step, which is a band of broken widths in both directions.
1293
1299
  * NOTE: media-query breakpoints must be literal. CSS can't read custom properties
1294
- * inside @media, so these px are structural, not stray magic numbers. */
1300
+ * inside @media, so these px are structural, not stray magic numbers. An app that runs PostCSS
1301
+ * need not retype them: `@cahyo-dimas/freeday/media` declares every width above as a
1302
+ * @custom-media, so it can write `@media (--fdy-below-sm)` and never drift (#058 §2). */
1295
1303
  .fdy-hidden{display:none !important;}
1296
1304
 
1297
1305
  /* hide-below-X, hidden when the viewport is narrower than X (content shows at X and up) */
@@ -1481,6 +1489,13 @@ a { color: var(--color-primary); }
1481
1489
  absolutely positioned dismiss button back into the flow. Do not merge these two rules. */
1482
1490
  :where(:is(.fdy-card,.fdy-list__row):has(.fdy-btn--stretch) :is(a[href],button,input,select,textarea,[tabindex]):not(.fdy-btn--stretch)){position:relative;}
1483
1491
  .fdy-card__body{padding:var(--space-5);}
1492
+ /* Header band (#070): an identity mark, the title, and the card's way out pinned right. Beside the
1493
+ title the action sits in one place on every card; under the content it moved each time the
1494
+ content grew. Outside __body's padding, so its rule spans the card edge to edge. The mark is an
1495
+ .fdy-avatar (--sm --square --tone-N), so its tints are the ones the contrast gate already covers. */
1496
+ .fdy-card__head{display:flex;align-items:center;gap:var(--space-3);padding:var(--space-4) var(--space-5);border-bottom:var(--bw) solid var(--color-border-muted);}
1497
+ .fdy-card__head .fdy-card__title{flex:1 1 auto;min-width:0;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
1498
+ .fdy-card__action{flex:none;display:inline-flex;align-items:center;gap:var(--space-2);}
1484
1499
  .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);}
1485
1500
  .fdy-card__desc{font-size:var(--text-sm);color:var(--color-text-muted);margin:0;line-height:var(--leading-normal);}
1486
1501
  .fdy-card__footer{padding:var(--space-3) var(--space-5);border-top:var(--bw) solid var(--color-border-muted);background:linear-gradient(180deg,var(--color-surface),var(--color-surface-2));display:flex;gap:var(--space-2);justify-content:flex-end;}
@@ -1863,6 +1878,42 @@ a { color: var(--color-primary); }
1863
1878
  .fdy-text-success{color:var(--color-success-strong);}
1864
1879
  .fdy-text-warning{color:var(--color-warning-strong);}
1865
1880
  .fdy-text-danger{color:var(--color-danger-strong);}
1881
+ /* Document columns (#065): a form or record page with an optional identity rail on the left
1882
+ * (__lead) and a summary rail on the right (__aside). The widths follow which rails are PRESENT,
1883
+ * so the layout is decided once here and not again by every screen:
1884
+ * __main alone one column
1885
+ * __main + __aside 8 / 4 (from md)
1886
+ * __lead + __main 3 / 9 (from md)
1887
+ * all three 3 / 6 / 3 (from lg; between md and lg both rails share the left)
1888
+ * Stacked, the summary rail comes FIRST: last, it sits under every section, and its button with it.
1889
+ * From md each rail is sticky and scrolls on its own, so the total and the status stay answerable
1890
+ * while the main column scrolls with the page, and a rail taller than the screen never hangs its
1891
+ * end off it. Screen only: a printed page is not held to a window. */
1892
+ .fdy-page__columns{--_fdy-rail-top:var(--fdy-page-sticky-top,var(--space-4));display:grid;grid-template-columns:minmax(0,1fr);gap:var(--space-6);align-items:start;}
1893
+ /* Under the shell the sticky topbar is in the way; --fdy-page-sticky-top overrides either default. */
1894
+ .fdy-app .fdy-page__columns{--_fdy-rail-top:var(--fdy-page-sticky-top,calc(var(--space-16) + var(--space-4)));}
1895
+ .fdy-page__columns > *{min-width:0;}
1896
+ @media (max-width:959.98px){.fdy-page__aside{order:-1;}}
1897
+ @media (min-width:960px){
1898
+ .fdy-page__columns:has(> .fdy-page__aside){grid-template-columns:minmax(0,2fr) minmax(0,1fr);}
1899
+ .fdy-page__columns:has(> .fdy-page__lead){grid-template-columns:minmax(0,1fr) minmax(0,3fr);}
1900
+ /* Both rails, two columns: they stack on the left while __main spans both rows on the right. */
1901
+ .fdy-page__columns:has(> .fdy-page__lead):has(> .fdy-page__aside) > .fdy-page__main{grid-row:span 2;}
1902
+ }
1903
+ @media (min-width:1280px){
1904
+ .fdy-page__columns:has(> .fdy-page__lead):has(> .fdy-page__aside){grid-template-columns:minmax(0,1fr) minmax(0,2fr) minmax(0,1fr);}
1905
+ .fdy-page__columns:has(> .fdy-page__lead):has(> .fdy-page__aside) > .fdy-page__main{grid-row:auto;}
1906
+ }
1907
+ /* The padding, taken back by the margin, is room for a focus ring at a card's edge: a scrolling box
1908
+ clips whatever draws outside it. */
1909
+ @media screen and (min-width:960px){
1910
+ .fdy-page__lead,.fdy-page__aside{position:sticky;top:var(--_fdy-rail-top);max-height:calc(100dvh - var(--_fdy-rail-top) - var(--space-4));overflow-y:auto;overscroll-behavior:contain;padding:var(--space-1);margin:calc(var(--space-1) * -1);}
1911
+ }
1912
+ /* Two sticky rails in ONE column would slide over each other: between md and lg, with both rails
1913
+ present, they scroll with the page instead. */
1914
+ @media screen and (min-width:960px) and (max-width:1279.98px){
1915
+ .fdy-page__columns:has(> .fdy-page__lead):has(> .fdy-page__aside) > :is(.fdy-page__lead,.fdy-page__aside){position:static;max-height:none;overflow:visible;}
1916
+ }
1866
1917
 
1867
1918
  /* Freeday. Date picker (input-styled trigger + calendar popover).
1868
1919
  * Built by freeday-datepicker.js from an empty [data-fdy-datepicker] wrapper. */
@@ -1958,9 +2009,19 @@ a { color: var(--color-primary); }
1958
2009
  /* Freeday. Description list (key-value detail view) */
1959
2010
  .fdy-dl{display:grid;grid-template-columns:auto 1fr;gap:var(--space-2) var(--space-5);margin:0;}
1960
2011
  .fdy-dl dt{color:var(--color-text-muted);font-size:var(--text-sm);}
1961
- .fdy-dl dd{margin:0;color:var(--color-text);font-size:var(--text-sm);font-weight:var(--weight-medium);}
2012
+ /* overflow-wrap: an email, an order code or a 13-digit phone number is one unbreakable token, and
2013
+ without it the value prints over whatever sits beside it (#066). */
2014
+ .fdy-dl dd{margin:0;min-width:0;color:var(--color-text);font-size:var(--text-sm);font-weight:var(--weight-medium);overflow-wrap:anywhere;}
1962
2015
  .fdy-dl--rows{grid-template-columns:1fr;gap:var(--space-1);}
1963
2016
  .fdy-dl--rows dt{margin-top:var(--space-2);}
2017
+ /* Facts grid (#066): each pair a <div> (allowed inside <dl>), caption above value, as many columns
2018
+ as the list's OWN width holds, never more than three. Sized by the element rather than the
2019
+ window, so a grid in a narrow side rail stays one column with no modifier: a window breakpoint
2020
+ gave a rail two columns of about 110px. min(100%, …) keeps a single column from overflowing a
2021
+ rail narrower than the column floor. */
2022
+ .fdy-dl--grid{grid-template-columns:repeat(auto-fill,minmax(min(100%,max(var(--fdy-dl-col-min,14rem),calc((100% - 2 * var(--space-6)) / 3))),1fr));gap:var(--space-5) var(--space-6);}
2023
+ .fdy-dl--grid > div{min-width:0;}
2024
+ .fdy-dl--grid dd{margin-top:var(--space-1);}
1964
2025
 
1965
2026
  /* Freeday. Divider */
1966
2027
  .fdy-divider{border:0;border-top:var(--bw) solid var(--color-border);margin:var(--space-4) 0;}
@@ -2077,7 +2138,14 @@ a { color: var(--color-primary); }
2077
2138
  /* Opt-in: sit the actions inline right after the last field instead of at the far edge, so when the
2078
2139
  * bar wraps they read as attached to the filters rather than shoved to the right of a lonely row. */
2079
2140
  .fdy-filterbar--actions-inline .fdy-filterbar__actions{margin-left:0;}
2080
- /* Stack on narrow screens so nothing overflows the row. */
2141
+ /* Stack on narrow screens so nothing overflows the row.
2142
+ * 640 is deliberately not a ramp step (sm is 600): it is a real phone-landscape width, and moving
2143
+ * it now would reflow every filter bar in every consuming app. So it is NAMED instead — `filterbar`
2144
+ * in tokens/breakpoints.mjs, `--fdy-filterbar-stacked` in the media sheet — because an app rule
2145
+ * that must fire with the stack (releasing a --w-grow field's max-width, say) has to match this
2146
+ * exact condition, and aligning it to sm from the docs alone leaves 600–640px half-stacked: the
2147
+ * fields are already full width while the app still caps the growing one. Raised from
2148
+ * IDU_AI_DOC_SAPB1_CLIENT and IDU_AI_DOC_SAAS (#058 §1). */
2081
2149
  @media (max-width:640px){
2082
2150
  .fdy-filterbar>.fdy-field,.fdy-filterbar>.fdy-field--w-sm,.fdy-filterbar>.fdy-field--w-lg,.fdy-filterbar>.fdy-field--w-xl,.fdy-filterbar>.fdy-field--w-2xl,.fdy-filterbar>.fdy-field--w-grow{width:100%;flex:1 1 100%;}
2083
2151
  .fdy-filterbar__actions{width:100%;margin-left:0;}
@@ -2345,6 +2413,12 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
2345
2413
  .fdy-switch input::before{content:"";position:absolute;top:2px;left:2px;width:calc(var(--control-switch-h) - 4px);height:calc(var(--control-switch-h) - 4px);border-radius:50%;background:var(--color-switch-thumb);box-shadow:var(--shadow-1);transition:transform var(--dur-base) var(--ease-spring);}
2346
2414
  .fdy-switch input:checked{background:var(--color-primary);}
2347
2415
  .fdy-switch input:checked::before{transform:translateX(calc(var(--control-switch-w) - var(--control-switch-h)));}
2416
+ /* State hint (#068): one line saying what the CURRENT state means, never "on means X, off means Y",
2417
+ which leaves the reader to work out which half applies. The control's aria-describedby points at
2418
+ the .fdy-help WRAPPING both lines: a hidden child of a referenced node is skipped, so a screen
2419
+ reader hears only the line on screen. Point it at the two children and both are read. */
2420
+ .fdy-field:has(:is(.fdy-switch,.fdy-check) input:checked) [data-fdy-when="off"],
2421
+ .fdy-field:not(:has(:is(.fdy-switch,.fdy-check) input:checked)) [data-fdy-when="on"]{display:none;}
2348
2422
 
2349
2423
  /* Standalone checkbox (e.g. table row/select-all), supports :indeterminate */
2350
2424
  .fdy-checkbox{appearance:none;-webkit-appearance:none;flex:none;width:var(--control-box);height:var(--control-box);margin:0;border:1.5px solid var(--color-control-border);border-radius:var(--radius-sm);background:var(--color-surface);box-shadow:inset 0 1px 2px var(--color-shadow-inset);cursor:pointer;display:grid;place-content:center;transition:background var(--dur-fast) var(--ease-standard),border-color var(--dur-fast) var(--ease-standard);}
@@ -2396,6 +2470,13 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
2396
2470
  .fdy-state__text{font-size:var(--text-sm);color:var(--color-text-muted);max-width:24rem;line-height:var(--leading-normal);}
2397
2471
  .fdy-state__actions{margin-top:var(--space-2);display:flex;gap:var(--space-2);flex-wrap:wrap;justify-content:center;}
2398
2472
  .fdy-state--danger .fdy-state__icon{color:var(--color-danger-strong);}
2473
+ /* Compact (#069): the same state inside a small card, one line of icon and text instead of a
2474
+ page-sized block. Danger matters most here: a widget that renders zero for a failed request
2475
+ reads as "nothing to do", the opposite of what happened. */
2476
+ .fdy-state--compact{flex-direction:row;align-items:flex-start;text-align:left;gap:var(--space-2);padding:var(--space-2) 0;}
2477
+ .fdy-state--compact .fdy-state__icon{width:1rem;height:1rem;flex:none;}
2478
+ .fdy-state--compact .fdy-state__text{font-size:var(--text-xs);max-width:none;}
2479
+ .fdy-state--compact .fdy-state__actions{margin:0 0 0 auto;}
2399
2480
 
2400
2481
  /* Freeday. Stepper / Wizard (progress header + optional panels).
2401
2482
  * Enhanced by freeday-stepper.js for prev/next navigation. */
@@ -2452,8 +2533,17 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
2452
2533
  itself, for the same reason: hover paints the full step, so the two must not be the same colour.
2453
2534
  Exposed as a Tier-3 token through the var() FALLBACK, never as a declaration — a custom property
2454
2535
  set here would outrank a host's own :root override. */
2455
- .fdy-table--striped tbody tr:nth-child(even){background:var(--fdy-table-stripe,color-mix(in srgb,var(--color-surface-2) 55%,var(--color-surface)));}
2456
- .fdy-table tbody tr:hover{background:var(--color-surface-2);}
2536
+ /* The resting colour is also published as --_fdy-row-rest, so a row that must not answer the
2537
+ pointer can hover back to exactly what it was (see --hover-actionable). */
2538
+ .fdy-table--striped tbody tr:nth-child(even){--_fdy-row-rest:var(--fdy-table-stripe,color-mix(in srgb,var(--color-surface-2) 55%,var(--color-surface)));background:var(--_fdy-row-rest);}
2539
+ .fdy-table tbody tr:hover{background:var(--_fdy-row-hover,var(--color-surface-2));}
2540
+ /* Opt-in (#064): only a row that DOES something answers the pointer. A tint on a row with no click
2541
+ handler promises a target that is not there: people click the row, nothing happens, and the
2542
+ button they had to aim at was elsewhere in it. Striping, not hover, is what keeps the eye on a
2543
+ line. The inert row's hover resolves to its own resting colour, so a stripe survives it; a frozen
2544
+ cell falls back to its opaque surface, because it scrolls over its neighbours. */
2545
+ .fdy-table--hover-actionable tbody tr:not(.fdy-table__row--activatable){--_fdy-row-hover:var(--_fdy-row-rest,transparent);}
2546
+ .fdy-table--hover-actionable tbody tr:not(.fdy-table__row--activatable) td.fdy-table__freeze{--_fdy-row-hover:var(--_fdy-row-rest,var(--color-surface));}
2457
2547
  /* Monospace data, ids, codes, IPs, timestamps. Alignment-neutral so it works in any cell
2458
2548
  or inline (`<span class="fdy-mono">`), not just numeric columns. */
2459
2549
  .fdy-mono{font-family:var(--font-mono);font-variant-numeric:tabular-nums;}
@@ -2462,8 +2552,8 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
2462
2552
  .fdy-table__num{text-align:right;font-variant-numeric:tabular-nums;font-family:var(--font-mono);}
2463
2553
  /* Loading / empty state cell (FdyTable spans it across all columns). */
2464
2554
  .fdy-table__state{text-align:center;color:var(--color-text-muted);padding:var(--space-6) var(--space-4);}
2465
- /* Activatable row (FdyTable rowActivatable): pointer + keyboard focus ring. The hover tint already
2466
- comes from the base `.fdy-table tbody tr:hover` rule, so this only adds the affordance + focus. */
2555
+ /* Activatable row (FdyTable rowActivatable): pointer + keyboard focus ring. The hover tint comes from
2556
+ the base `.fdy-table tbody tr:hover` rule, which --hover-actionable keeps for these rows only. */
2467
2557
  .fdy-table__row--activatable{cursor:pointer;}
2468
2558
  .fdy-table__row--activatable:focus-visible{outline:2px solid var(--color-primary);outline-offset:-2px;}
2469
2559
  /* Expandable detail row (FdyTable row-detail slot): a muted panel under its row, no hover tint. */
@@ -2523,7 +2613,7 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
2523
2613
  (0,4,3) and beat the hover rule below. At (0,3,3) it ties, and the later rule wins, so a hovered
2524
2614
  row still bands unbroken across the freeze line. */
2525
2615
  .fdy-table--striped tbody tr:nth-child(even) td.fdy-table__freeze{background:var(--fdy-table-stripe,color-mix(in srgb,var(--color-surface-2) 55%,var(--color-surface)));}
2526
- .fdy-table--sticky tbody tr:hover td.fdy-table__freeze{background:var(--color-surface-2);}
2616
+ .fdy-table--sticky tbody tr:hover td.fdy-table__freeze{background:var(--_fdy-row-hover,var(--color-surface-2));}
2527
2617
  /* A frozen header cell is stuck on both axes wherever the head is frozen too, so
2528
2618
  it has to outrank both. */
2529
2619
  .fdy-table--sticky thead .fdy-table__freeze{z-index:3;}
package/dist/freeday.css CHANGED
@@ -273,6 +273,8 @@ a { color: var(--color-primary); }
273
273
  .fdy-avatar--xs{width:1.5rem;height:1.5rem;font-size:var(--text-xs);}
274
274
  .fdy-avatar--sm{width:2rem;height:2rem;font-size:var(--text-xs);}
275
275
  .fdy-avatar--lg{width:3.5rem;height:3.5rem;font-size:var(--text-lg);}
276
+ /* Square: a mark that is not a person (a company logo, a card's icon chip, #070). */
277
+ .fdy-avatar--square{border-radius:var(--radius-md);}
276
278
  .fdy-avatar-group{display:inline-flex;}
277
279
  .fdy-avatar-group .fdy-avatar{border:2px solid var(--color-surface);margin-left:calc(var(--space-2)*-1);}
278
280
  .fdy-avatar-group .fdy-avatar:first-child{margin-left:0;}
@@ -412,8 +414,14 @@ a { color: var(--color-primary); }
412
414
 
413
415
  /* Freeday. Responsive display utilities (pairs with freeday-breakpoint.js).
414
416
  * Breakpoint scale (min-width, px): sm 600 · md 960 · lg 1280 · xl 1920.
417
+ * Two further widths exist and are NOT ramp steps: `nav` (app-shell.css, off-canvas drawer vs
418
+ * static sidebar) and `filterbar` (filterbar.css, the width its fields stack at). Both are named
419
+ * in tokens/breakpoints.mjs; a rule that has to fire with one of them aligns to it, not to the
420
+ * nearest ramp step, which is a band of broken widths in both directions.
415
421
  * NOTE: media-query breakpoints must be literal. CSS can't read custom properties
416
- * inside @media, so these px are structural, not stray magic numbers. */
422
+ * inside @media, so these px are structural, not stray magic numbers. An app that runs PostCSS
423
+ * need not retype them: `@cahyo-dimas/freeday/media` declares every width above as a
424
+ * @custom-media, so it can write `@media (--fdy-below-sm)` and never drift (#058 §2). */
417
425
  .fdy-hidden{display:none !important;}
418
426
 
419
427
  /* hide-below-X, hidden when the viewport is narrower than X (content shows at X and up) */
@@ -603,6 +611,13 @@ a { color: var(--color-primary); }
603
611
  absolutely positioned dismiss button back into the flow. Do not merge these two rules. */
604
612
  :where(:is(.fdy-card,.fdy-list__row):has(.fdy-btn--stretch) :is(a[href],button,input,select,textarea,[tabindex]):not(.fdy-btn--stretch)){position:relative;}
605
613
  .fdy-card__body{padding:var(--space-5);}
614
+ /* Header band (#070): an identity mark, the title, and the card's way out pinned right. Beside the
615
+ title the action sits in one place on every card; under the content it moved each time the
616
+ content grew. Outside __body's padding, so its rule spans the card edge to edge. The mark is an
617
+ .fdy-avatar (--sm --square --tone-N), so its tints are the ones the contrast gate already covers. */
618
+ .fdy-card__head{display:flex;align-items:center;gap:var(--space-3);padding:var(--space-4) var(--space-5);border-bottom:var(--bw) solid var(--color-border-muted);}
619
+ .fdy-card__head .fdy-card__title{flex:1 1 auto;min-width:0;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
620
+ .fdy-card__action{flex:none;display:inline-flex;align-items:center;gap:var(--space-2);}
606
621
  .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);}
607
622
  .fdy-card__desc{font-size:var(--text-sm);color:var(--color-text-muted);margin:0;line-height:var(--leading-normal);}
608
623
  .fdy-card__footer{padding:var(--space-3) var(--space-5);border-top:var(--bw) solid var(--color-border-muted);background:linear-gradient(180deg,var(--color-surface),var(--color-surface-2));display:flex;gap:var(--space-2);justify-content:flex-end;}
@@ -985,6 +1000,42 @@ a { color: var(--color-primary); }
985
1000
  .fdy-text-success{color:var(--color-success-strong);}
986
1001
  .fdy-text-warning{color:var(--color-warning-strong);}
987
1002
  .fdy-text-danger{color:var(--color-danger-strong);}
1003
+ /* Document columns (#065): a form or record page with an optional identity rail on the left
1004
+ * (__lead) and a summary rail on the right (__aside). The widths follow which rails are PRESENT,
1005
+ * so the layout is decided once here and not again by every screen:
1006
+ * __main alone one column
1007
+ * __main + __aside 8 / 4 (from md)
1008
+ * __lead + __main 3 / 9 (from md)
1009
+ * all three 3 / 6 / 3 (from lg; between md and lg both rails share the left)
1010
+ * Stacked, the summary rail comes FIRST: last, it sits under every section, and its button with it.
1011
+ * From md each rail is sticky and scrolls on its own, so the total and the status stay answerable
1012
+ * while the main column scrolls with the page, and a rail taller than the screen never hangs its
1013
+ * end off it. Screen only: a printed page is not held to a window. */
1014
+ .fdy-page__columns{--_fdy-rail-top:var(--fdy-page-sticky-top,var(--space-4));display:grid;grid-template-columns:minmax(0,1fr);gap:var(--space-6);align-items:start;}
1015
+ /* Under the shell the sticky topbar is in the way; --fdy-page-sticky-top overrides either default. */
1016
+ .fdy-app .fdy-page__columns{--_fdy-rail-top:var(--fdy-page-sticky-top,calc(var(--space-16) + var(--space-4)));}
1017
+ .fdy-page__columns > *{min-width:0;}
1018
+ @media (max-width:959.98px){.fdy-page__aside{order:-1;}}
1019
+ @media (min-width:960px){
1020
+ .fdy-page__columns:has(> .fdy-page__aside){grid-template-columns:minmax(0,2fr) minmax(0,1fr);}
1021
+ .fdy-page__columns:has(> .fdy-page__lead){grid-template-columns:minmax(0,1fr) minmax(0,3fr);}
1022
+ /* Both rails, two columns: they stack on the left while __main spans both rows on the right. */
1023
+ .fdy-page__columns:has(> .fdy-page__lead):has(> .fdy-page__aside) > .fdy-page__main{grid-row:span 2;}
1024
+ }
1025
+ @media (min-width:1280px){
1026
+ .fdy-page__columns:has(> .fdy-page__lead):has(> .fdy-page__aside){grid-template-columns:minmax(0,1fr) minmax(0,2fr) minmax(0,1fr);}
1027
+ .fdy-page__columns:has(> .fdy-page__lead):has(> .fdy-page__aside) > .fdy-page__main{grid-row:auto;}
1028
+ }
1029
+ /* The padding, taken back by the margin, is room for a focus ring at a card's edge: a scrolling box
1030
+ clips whatever draws outside it. */
1031
+ @media screen and (min-width:960px){
1032
+ .fdy-page__lead,.fdy-page__aside{position:sticky;top:var(--_fdy-rail-top);max-height:calc(100dvh - var(--_fdy-rail-top) - var(--space-4));overflow-y:auto;overscroll-behavior:contain;padding:var(--space-1);margin:calc(var(--space-1) * -1);}
1033
+ }
1034
+ /* Two sticky rails in ONE column would slide over each other: between md and lg, with both rails
1035
+ present, they scroll with the page instead. */
1036
+ @media screen and (min-width:960px) and (max-width:1279.98px){
1037
+ .fdy-page__columns:has(> .fdy-page__lead):has(> .fdy-page__aside) > :is(.fdy-page__lead,.fdy-page__aside){position:static;max-height:none;overflow:visible;}
1038
+ }
988
1039
 
989
1040
  /* Freeday. Date picker (input-styled trigger + calendar popover).
990
1041
  * Built by freeday-datepicker.js from an empty [data-fdy-datepicker] wrapper. */
@@ -1080,9 +1131,19 @@ a { color: var(--color-primary); }
1080
1131
  /* Freeday. Description list (key-value detail view) */
1081
1132
  .fdy-dl{display:grid;grid-template-columns:auto 1fr;gap:var(--space-2) var(--space-5);margin:0;}
1082
1133
  .fdy-dl dt{color:var(--color-text-muted);font-size:var(--text-sm);}
1083
- .fdy-dl dd{margin:0;color:var(--color-text);font-size:var(--text-sm);font-weight:var(--weight-medium);}
1134
+ /* overflow-wrap: an email, an order code or a 13-digit phone number is one unbreakable token, and
1135
+ without it the value prints over whatever sits beside it (#066). */
1136
+ .fdy-dl dd{margin:0;min-width:0;color:var(--color-text);font-size:var(--text-sm);font-weight:var(--weight-medium);overflow-wrap:anywhere;}
1084
1137
  .fdy-dl--rows{grid-template-columns:1fr;gap:var(--space-1);}
1085
1138
  .fdy-dl--rows dt{margin-top:var(--space-2);}
1139
+ /* Facts grid (#066): each pair a <div> (allowed inside <dl>), caption above value, as many columns
1140
+ as the list's OWN width holds, never more than three. Sized by the element rather than the
1141
+ window, so a grid in a narrow side rail stays one column with no modifier: a window breakpoint
1142
+ gave a rail two columns of about 110px. min(100%, …) keeps a single column from overflowing a
1143
+ rail narrower than the column floor. */
1144
+ .fdy-dl--grid{grid-template-columns:repeat(auto-fill,minmax(min(100%,max(var(--fdy-dl-col-min,14rem),calc((100% - 2 * var(--space-6)) / 3))),1fr));gap:var(--space-5) var(--space-6);}
1145
+ .fdy-dl--grid > div{min-width:0;}
1146
+ .fdy-dl--grid dd{margin-top:var(--space-1);}
1086
1147
 
1087
1148
  /* Freeday. Divider */
1088
1149
  .fdy-divider{border:0;border-top:var(--bw) solid var(--color-border);margin:var(--space-4) 0;}
@@ -1199,7 +1260,14 @@ a { color: var(--color-primary); }
1199
1260
  /* Opt-in: sit the actions inline right after the last field instead of at the far edge, so when the
1200
1261
  * bar wraps they read as attached to the filters rather than shoved to the right of a lonely row. */
1201
1262
  .fdy-filterbar--actions-inline .fdy-filterbar__actions{margin-left:0;}
1202
- /* Stack on narrow screens so nothing overflows the row. */
1263
+ /* Stack on narrow screens so nothing overflows the row.
1264
+ * 640 is deliberately not a ramp step (sm is 600): it is a real phone-landscape width, and moving
1265
+ * it now would reflow every filter bar in every consuming app. So it is NAMED instead — `filterbar`
1266
+ * in tokens/breakpoints.mjs, `--fdy-filterbar-stacked` in the media sheet — because an app rule
1267
+ * that must fire with the stack (releasing a --w-grow field's max-width, say) has to match this
1268
+ * exact condition, and aligning it to sm from the docs alone leaves 600–640px half-stacked: the
1269
+ * fields are already full width while the app still caps the growing one. Raised from
1270
+ * IDU_AI_DOC_SAPB1_CLIENT and IDU_AI_DOC_SAAS (#058 §1). */
1203
1271
  @media (max-width:640px){
1204
1272
  .fdy-filterbar>.fdy-field,.fdy-filterbar>.fdy-field--w-sm,.fdy-filterbar>.fdy-field--w-lg,.fdy-filterbar>.fdy-field--w-xl,.fdy-filterbar>.fdy-field--w-2xl,.fdy-filterbar>.fdy-field--w-grow{width:100%;flex:1 1 100%;}
1205
1273
  .fdy-filterbar__actions{width:100%;margin-left:0;}
@@ -1467,6 +1535,12 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
1467
1535
  .fdy-switch input::before{content:"";position:absolute;top:2px;left:2px;width:calc(var(--control-switch-h) - 4px);height:calc(var(--control-switch-h) - 4px);border-radius:50%;background:var(--color-switch-thumb);box-shadow:var(--shadow-1);transition:transform var(--dur-base) var(--ease-spring);}
1468
1536
  .fdy-switch input:checked{background:var(--color-primary);}
1469
1537
  .fdy-switch input:checked::before{transform:translateX(calc(var(--control-switch-w) - var(--control-switch-h)));}
1538
+ /* State hint (#068): one line saying what the CURRENT state means, never "on means X, off means Y",
1539
+ which leaves the reader to work out which half applies. The control's aria-describedby points at
1540
+ the .fdy-help WRAPPING both lines: a hidden child of a referenced node is skipped, so a screen
1541
+ reader hears only the line on screen. Point it at the two children and both are read. */
1542
+ .fdy-field:has(:is(.fdy-switch,.fdy-check) input:checked) [data-fdy-when="off"],
1543
+ .fdy-field:not(:has(:is(.fdy-switch,.fdy-check) input:checked)) [data-fdy-when="on"]{display:none;}
1470
1544
 
1471
1545
  /* Standalone checkbox (e.g. table row/select-all), supports :indeterminate */
1472
1546
  .fdy-checkbox{appearance:none;-webkit-appearance:none;flex:none;width:var(--control-box);height:var(--control-box);margin:0;border:1.5px solid var(--color-control-border);border-radius:var(--radius-sm);background:var(--color-surface);box-shadow:inset 0 1px 2px var(--color-shadow-inset);cursor:pointer;display:grid;place-content:center;transition:background var(--dur-fast) var(--ease-standard),border-color var(--dur-fast) var(--ease-standard);}
@@ -1518,6 +1592,13 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
1518
1592
  .fdy-state__text{font-size:var(--text-sm);color:var(--color-text-muted);max-width:24rem;line-height:var(--leading-normal);}
1519
1593
  .fdy-state__actions{margin-top:var(--space-2);display:flex;gap:var(--space-2);flex-wrap:wrap;justify-content:center;}
1520
1594
  .fdy-state--danger .fdy-state__icon{color:var(--color-danger-strong);}
1595
+ /* Compact (#069): the same state inside a small card, one line of icon and text instead of a
1596
+ page-sized block. Danger matters most here: a widget that renders zero for a failed request
1597
+ reads as "nothing to do", the opposite of what happened. */
1598
+ .fdy-state--compact{flex-direction:row;align-items:flex-start;text-align:left;gap:var(--space-2);padding:var(--space-2) 0;}
1599
+ .fdy-state--compact .fdy-state__icon{width:1rem;height:1rem;flex:none;}
1600
+ .fdy-state--compact .fdy-state__text{font-size:var(--text-xs);max-width:none;}
1601
+ .fdy-state--compact .fdy-state__actions{margin:0 0 0 auto;}
1521
1602
 
1522
1603
  /* Freeday. Stepper / Wizard (progress header + optional panels).
1523
1604
  * Enhanced by freeday-stepper.js for prev/next navigation. */
@@ -1574,8 +1655,17 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
1574
1655
  itself, for the same reason: hover paints the full step, so the two must not be the same colour.
1575
1656
  Exposed as a Tier-3 token through the var() FALLBACK, never as a declaration — a custom property
1576
1657
  set here would outrank a host's own :root override. */
1577
- .fdy-table--striped tbody tr:nth-child(even){background:var(--fdy-table-stripe,color-mix(in srgb,var(--color-surface-2) 55%,var(--color-surface)));}
1578
- .fdy-table tbody tr:hover{background:var(--color-surface-2);}
1658
+ /* The resting colour is also published as --_fdy-row-rest, so a row that must not answer the
1659
+ pointer can hover back to exactly what it was (see --hover-actionable). */
1660
+ .fdy-table--striped tbody tr:nth-child(even){--_fdy-row-rest:var(--fdy-table-stripe,color-mix(in srgb,var(--color-surface-2) 55%,var(--color-surface)));background:var(--_fdy-row-rest);}
1661
+ .fdy-table tbody tr:hover{background:var(--_fdy-row-hover,var(--color-surface-2));}
1662
+ /* Opt-in (#064): only a row that DOES something answers the pointer. A tint on a row with no click
1663
+ handler promises a target that is not there: people click the row, nothing happens, and the
1664
+ button they had to aim at was elsewhere in it. Striping, not hover, is what keeps the eye on a
1665
+ line. The inert row's hover resolves to its own resting colour, so a stripe survives it; a frozen
1666
+ cell falls back to its opaque surface, because it scrolls over its neighbours. */
1667
+ .fdy-table--hover-actionable tbody tr:not(.fdy-table__row--activatable){--_fdy-row-hover:var(--_fdy-row-rest,transparent);}
1668
+ .fdy-table--hover-actionable tbody tr:not(.fdy-table__row--activatable) td.fdy-table__freeze{--_fdy-row-hover:var(--_fdy-row-rest,var(--color-surface));}
1579
1669
  /* Monospace data, ids, codes, IPs, timestamps. Alignment-neutral so it works in any cell
1580
1670
  or inline (`<span class="fdy-mono">`), not just numeric columns. */
1581
1671
  .fdy-mono{font-family:var(--font-mono);font-variant-numeric:tabular-nums;}
@@ -1584,8 +1674,8 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
1584
1674
  .fdy-table__num{text-align:right;font-variant-numeric:tabular-nums;font-family:var(--font-mono);}
1585
1675
  /* Loading / empty state cell (FdyTable spans it across all columns). */
1586
1676
  .fdy-table__state{text-align:center;color:var(--color-text-muted);padding:var(--space-6) var(--space-4);}
1587
- /* Activatable row (FdyTable rowActivatable): pointer + keyboard focus ring. The hover tint already
1588
- comes from the base `.fdy-table tbody tr:hover` rule, so this only adds the affordance + focus. */
1677
+ /* Activatable row (FdyTable rowActivatable): pointer + keyboard focus ring. The hover tint comes from
1678
+ the base `.fdy-table tbody tr:hover` rule, which --hover-actionable keeps for these rows only. */
1589
1679
  .fdy-table__row--activatable{cursor:pointer;}
1590
1680
  .fdy-table__row--activatable:focus-visible{outline:2px solid var(--color-primary);outline-offset:-2px;}
1591
1681
  /* Expandable detail row (FdyTable row-detail slot): a muted panel under its row, no hover tint. */
@@ -1645,7 +1735,7 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
1645
1735
  (0,4,3) and beat the hover rule below. At (0,3,3) it ties, and the later rule wins, so a hovered
1646
1736
  row still bands unbroken across the freeze line. */
1647
1737
  .fdy-table--striped tbody tr:nth-child(even) td.fdy-table__freeze{background:var(--fdy-table-stripe,color-mix(in srgb,var(--color-surface-2) 55%,var(--color-surface)));}
1648
- .fdy-table--sticky tbody tr:hover td.fdy-table__freeze{background:var(--color-surface-2);}
1738
+ .fdy-table--sticky tbody tr:hover td.fdy-table__freeze{background:var(--_fdy-row-hover,var(--color-surface-2));}
1649
1739
  /* A frozen header cell is stuck on both axes wherever the head is frozen too, so
1650
1740
  it has to outrank both. */
1651
1741
  .fdy-table--sticky thead .fdy-table__freeze{z-index:3;}
package/dist/freeday.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  // Type surface for the root side-effect import `@cahyo-dimas/freeday`, which loads every
2
- // enhancer and registers the `window.Freeday` global. Authored from src/freeday-toast.js
3
- // Keep it in sync with that file. (The per-component window.Freeday<Name> enhancer namespaces are
2
+ // enhancer and registers the `window.Freeday` global. Authored from src/freeday-toast.js and
3
+ // src/freeday-confirm.js. Keep it in sync with those files. (The per-component window.Freeday<Name> enhancer namespaces are
4
4
  // intentionally not typed here; no consumer reconstructs those.)
5
5
 
6
6
  export interface FreedayToastOptions {
@@ -15,11 +15,26 @@ export interface FreedayToastOptions {
15
15
  key?: string;
16
16
  }
17
17
 
18
+ export interface FreedayConfirmOptions {
19
+ title?: string;
20
+ message?: string;
21
+ /** Findings shown as a list under the message. */
22
+ details?: string[];
23
+ /** Accept label. Default 'Confirm'; name the action instead ('Delete order'). */
24
+ acceptText?: string;
25
+ /** Reject label. Default 'Cancel'. */
26
+ rejectText?: string;
27
+ /** The accept button's variant. Default 'primary'. */
28
+ tone?: 'primary' | 'danger';
29
+ }
30
+
18
31
  export interface FreedayGlobal {
19
32
  /** Show a transient toast in a live region (created on first use). Returns the toast element. */
20
33
  toast(opts?: FreedayToastOptions): HTMLElement;
21
34
  /** Dismiss a toast early: pass the element returned by toast(), or a key string. */
22
35
  dismiss(target: HTMLElement | string): void;
36
+ /** Ask a yes/no question in a modal. true on accept; false on reject, Escape or a backdrop click. */
37
+ confirm(opts?: FreedayConfirmOptions): Promise<boolean>;
23
38
  }
24
39
 
25
40
  declare global {