@cahyo-dimas/freeday 2.2.0 → 3.1.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 +251 -0
- package/COMPONENTS.md +183 -7
- package/README.id.md +1 -1
- package/README.md +1 -1
- package/USAGE.md +1 -1
- package/adapters/blazor/FdyAppShell.razor +1 -1
- package/adapters/blazor/FdyAppShell.razor.cs +35 -0
- package/adapters/blazor/FdyTable.razor +44 -4
- package/adapters/blazor/FdyTable.razor.cs +110 -0
- package/adapters/blazor/FdyTableFooter.razor +1 -1
- package/adapters/blazor/freeday-blazor.js +8 -0
- package/adapters/react/components/FdyAppShell.tsx +52 -11
- package/adapters/react/components/FdyTable.tsx +123 -2
- package/adapters/react/components/FdyTableFooter.tsx +1 -1
- package/adapters/vue/components/FdyAppShell.vue +41 -11
- package/adapters/vue/components/FdyTable.vue +125 -4
- package/adapters/vue/components/FdyTableFooter.vue +1 -1
- package/dist/freeday-app-shell.js +23 -5
- package/dist/freeday-busy.js +168 -0
- package/dist/freeday-stepper.js +54 -4
- package/dist/freeday-table.js +7 -0
- package/dist/freeday.bundle.css +652 -43
- package/dist/freeday.css +129 -15
- package/dist/freeday.js +253 -9
- package/dist/freeday.tokens.css +523 -28
- package/docs/agent-onboarding.md +4 -0
- package/docs/getting-started.md +1 -1
- package/package.json +4 -3
- package/src/components/app-shell.css +29 -5
- package/src/components/appbar.css +2 -2
- package/src/components/busy.css +42 -0
- package/src/components/card.css +1 -1
- package/src/components/composition.css +15 -2
- package/src/components/drawer.css +1 -1
- package/src/components/filterbar.css +3 -2
- package/src/components/input.css +9 -0
- package/src/components/menu.css +1 -1
- package/src/components/modal.css +1 -1
- package/src/components/stepper.css +11 -0
- package/src/components/table.css +13 -0
- package/tokens/tokens.json +54 -10
package/dist/freeday.css
CHANGED
|
@@ -85,7 +85,7 @@ a { color: var(--color-primary); }
|
|
|
85
85
|
* > (.fdy-app__topbar, .fdy-app__main)). The __content wrapper gives the sticky
|
|
86
86
|
* topbar a tall containing block so it can travel, a bare grid cell can't. */
|
|
87
87
|
.fdy-app{display:flex;align-items:stretch;min-height:100vh;background:var(--color-surface-2);}
|
|
88
|
-
.fdy-app__sidebar{position:sticky;top:0;align-self:flex-start;height:100vh;overflow:hidden;flex:none;width:15.5rem;box-sizing:border-box;background:var(--color-surface);border-right:var(--bw) solid var(--color-border);display:flex;flex-direction:column;transition:width var(--dur-slow) var(--ease-standard),border-right-width var(--dur-slow) var(--ease-standard);}
|
|
88
|
+
.fdy-app__sidebar{position:sticky;top:0;align-self:flex-start;height:100vh;overflow:hidden;flex:none;width:15.5rem;box-sizing:border-box;background:var(--color-surface-raised);backdrop-filter:var(--surface-filter);-webkit-backdrop-filter:var(--surface-filter);border-right:var(--bw) solid var(--color-border);display:flex;flex-direction:column;transition:width var(--dur-slow) var(--ease-standard),border-right-width var(--dur-slow) var(--ease-standard);}
|
|
89
89
|
.fdy-app__content{flex:1;min-width:0;display:flex;flex-direction:column;}
|
|
90
90
|
/* Brand is a fixed header, same height as the topbar so they align, and only
|
|
91
91
|
* the nav below it scrolls. Bare text (no children) renders as one bold
|
|
@@ -158,7 +158,23 @@ a { color: var(--color-primary); }
|
|
|
158
158
|
/* Desktop collapse: animate width -> 0 (content clips via overflow:hidden; the content area
|
|
159
159
|
* flexes in to fill). Not display:none, so the collapse/expand slides instead of snapping. */
|
|
160
160
|
@media (min-width:721px){.fdy-app--nav-collapsed .fdy-app__sidebar{width:0;border-right-width:0;}}
|
|
161
|
-
|
|
161
|
+
/* Desktop OVERLAY: the nav floats over the content instead of displacing it, the arrangement a
|
|
162
|
+
* "menu mode" preference offers on a wide screen. The declarations below are the mobile off-canvas
|
|
163
|
+
* ones repeated, and that repetition is not an oversight: the two conditions are
|
|
164
|
+
* `(max-width:720px)` and `(min-width:721px) and .fdy-app--nav-overlay`, which no single selector
|
|
165
|
+
* can express, and the mobile path has to keep working with no JS and no class at all. What the
|
|
166
|
+
* modifier changes is the LAYOUT; every behaviour (backdrop, Escape, close-on-follow, focus return,
|
|
167
|
+
* inert, the Tab trap) is the same code path the drawer already used. */
|
|
168
|
+
@media (min-width:721px){
|
|
169
|
+
.fdy-app--nav-overlay .fdy-app__sidebar{position:fixed;top:0;left:0;bottom:0;height:100dvh;width:16rem;z-index:60;transform:translateX(-100%);box-shadow:var(--shadow-4);transition:transform var(--dur-base) var(--ease-standard);}
|
|
170
|
+
.fdy-app--nav-overlay.fdy-app--nav-open .fdy-app__sidebar{transform:translateX(0);}
|
|
171
|
+
.fdy-app--nav-overlay .fdy-app__backdrop{display:block;position:fixed;inset:0;z-index:55;background:var(--color-scrim);opacity:0;visibility:hidden;pointer-events:none;transition:opacity var(--dur-base) var(--ease-standard),visibility var(--dur-base) var(--ease-standard);}
|
|
172
|
+
.fdy-app--nav-overlay.fdy-app--nav-open .fdy-app__backdrop{opacity:1;visibility:visible;pointer-events:auto;}
|
|
173
|
+
/* In overlay mode the sidebar is out of flow, so the collapse-to-zero-width rule above must not
|
|
174
|
+
also apply — it would fight the transform for the same panel. */
|
|
175
|
+
.fdy-app--nav-overlay.fdy-app--nav-collapsed .fdy-app__sidebar{width:16rem;border-right-width:var(--bw);}
|
|
176
|
+
}
|
|
177
|
+
.fdy-app__topbar{position:sticky;top:0;z-index:20;min-height:var(--space-16);display:flex;align-items:center;gap:var(--space-3);padding:0 var(--space-8);background:var(--color-surface-raised);backdrop-filter:var(--surface-filter);-webkit-backdrop-filter:var(--surface-filter);border-bottom:var(--bw) solid var(--color-border);}
|
|
162
178
|
.fdy-app__title{font-family:var(--font-display);font-size:var(--text-xl);font-weight:var(--weight-semibold);color:var(--color-text);margin:0 auto 0 0;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
163
179
|
.fdy-app__main{flex:1;padding:var(--space-8);}
|
|
164
180
|
.fdy-skip{position:absolute;left:var(--space-2);top:var(--space-2);background:var(--color-primary);color:var(--color-on-primary);padding:var(--space-2) var(--space-4);border-radius:var(--radius-md);z-index:100;transform:translateY(-250%);transition:transform var(--dur-fast) var(--ease-standard);}
|
|
@@ -167,13 +183,21 @@ a { color: var(--color-primary); }
|
|
|
167
183
|
* drop sticky so nothing escapes the box; sidebar stretches to fill it. */
|
|
168
184
|
.fdy-app--static .fdy-app__sidebar{position:static;height:auto;align-self:stretch;}
|
|
169
185
|
.fdy-app--static .fdy-app__topbar{position:static;}
|
|
170
|
-
/*
|
|
186
|
+
/* `pointer-events` on the backdrop is load-bearing, not a belt-and-braces extra. `visibility` is a
|
|
187
|
+
* TRANSITIONED property here, so a backdrop on its way out stays `visible` for the whole duration
|
|
188
|
+
* and keeps swallowing clicks aimed at the topbar underneath it — and switching INTO overlay mode is
|
|
189
|
+
* worse, because the element goes display:none -> display:block with its visibility animating from
|
|
190
|
+
* the initial `visible`, so a mode switch alone hands the page ~200ms of dead clicks. Measured: with
|
|
191
|
+
* only `visibility:hidden`, elementFromPoint over the nav toggle returned the backdrop.
|
|
192
|
+
* Mobile: the sidebar becomes an off-canvas drawer opened by the topbar toggle. */
|
|
171
193
|
.fdy-app__backdrop{display:none;}
|
|
172
194
|
@media (max-width:720px){
|
|
173
195
|
.fdy-app__sidebar{position:fixed;top:0;left:0;bottom:0;height:100dvh;width:16rem;z-index:60;transform:translateX(-100%);transition:transform var(--dur-base) var(--ease-standard);box-shadow:var(--shadow-4);}
|
|
174
196
|
.fdy-app--nav-open .fdy-app__sidebar{transform:translateX(0);}
|
|
175
|
-
|
|
176
|
-
|
|
197
|
+
/* --color-scrim, not a literal: this was the last raw colour left in a component stylesheet, and
|
|
198
|
+
it also means the drawer behind the nav dims exactly like every other overlay in the kit. */
|
|
199
|
+
.fdy-app__backdrop{display:block;position:fixed;inset:0;z-index:55;background:var(--color-scrim);opacity:0;visibility:hidden;pointer-events:none;transition:opacity var(--dur-base) var(--ease-standard),visibility var(--dur-base) var(--ease-standard);}
|
|
200
|
+
.fdy-app--nav-open .fdy-app__backdrop{opacity:1;visibility:visible;pointer-events:auto;}
|
|
177
201
|
.fdy-app__main{padding:var(--space-5);}
|
|
178
202
|
.fdy-app__topbar{padding:0 var(--space-4);}
|
|
179
203
|
}
|
|
@@ -182,9 +206,9 @@ a { color: var(--color-primary); }
|
|
|
182
206
|
/* Freeday. App bar (standalone top bar: brand · actions · spacer).
|
|
183
207
|
* Distinct from .fdy-app__topbar (which is bound to the shell grid). Use anywhere;
|
|
184
208
|
* sticky / elevated / dense / coloured via modifiers. Pure CSS. */
|
|
185
|
-
.fdy-appbar{display:flex;align-items:center;gap:var(--space-3);height:var(--space-16);padding:0 var(--space-4);background:var(--color-surface);color:var(--color-text);border-bottom:var(--bw) solid var(--color-border);}
|
|
209
|
+
.fdy-appbar{display:flex;align-items:center;gap:var(--space-3);height:var(--space-16);padding:0 var(--space-4);background:var(--color-surface-raised);backdrop-filter:var(--surface-filter);-webkit-backdrop-filter:var(--surface-filter);color:var(--color-text);border-bottom:var(--bw) solid var(--color-border);}
|
|
186
210
|
.fdy-appbar--sticky{position:sticky;top:0;z-index:90;}
|
|
187
|
-
.fdy-appbar--elevated{border-bottom:0;box-shadow:var(--shadow-2);}
|
|
211
|
+
.fdy-appbar--elevated{border-bottom:0;box-shadow:var(--shadow-2),var(--surface-inset);}
|
|
188
212
|
.fdy-appbar--dense{height:var(--space-12);}
|
|
189
213
|
.fdy-appbar--primary{background:var(--color-primary);color:var(--color-on-primary);border-bottom-color:transparent;}
|
|
190
214
|
.fdy-appbar__brand{display:inline-flex;align-items:center;gap:var(--space-2);font-family:var(--font-display);font-weight:var(--weight-bold);font-size:var(--text-lg);color:inherit;white-space:nowrap;}
|
|
@@ -317,6 +341,49 @@ a { color: var(--color-primary); }
|
|
|
317
341
|
@media (min-width:960px){.fdy-hide-above-md{display:none !important;}}
|
|
318
342
|
@media (min-width:1280px){.fdy-hide-above-lg{display:none !important;}}
|
|
319
343
|
|
|
344
|
+
/* Freeday. Busy overlay */
|
|
345
|
+
/* The blocking "the app is working" state, for an operation the reader must not interrupt or
|
|
346
|
+
* double-submit. There is deliberately NO markup contract to hand-write: it is created by
|
|
347
|
+
* `Freeday.busy()` and removed by `Freeday.idle()`, because two of these on screen at once — with
|
|
348
|
+
* two different captions, from two components that each thought they owned it — is the exact bug
|
|
349
|
+
* this component exists to prevent. One owner, enforced by there being only one way in.
|
|
350
|
+
*
|
|
351
|
+
* It is not a dialog. Nothing here asks a question and nothing can be dismissed; interaction is
|
|
352
|
+
* removed with `inert` on everything else, not with a focus trap. */
|
|
353
|
+
|
|
354
|
+
/* `display` is gated on `.is-open` rather than on `:popover-open`, and that is load-bearing. The UA
|
|
355
|
+
* sheet hides a popover with `[popover]:not(:popover-open){display:none}`, but AUTHOR rules beat UA
|
|
356
|
+
* rules whatever the specificity, so a bare `.fdy-busy{display:flex}` would paint the overlay
|
|
357
|
+
* permanently the moment the element existed. Gating on a class instead also means the overlay
|
|
358
|
+
* still works where the Popover API does not: it falls back to a plain fixed layer at this
|
|
359
|
+
* z-index, which cannot clear an open <dialog> but does everything else. */
|
|
360
|
+
.fdy-busy{display:none;position:fixed;inset:0;width:100%;height:100%;max-width:none;max-height:none;margin:0;padding:0;border:0;overflow:hidden;background:var(--color-scrim);z-index:190;}
|
|
361
|
+
.fdy-busy.is-open{display:flex;align-items:center;justify-content:center;}
|
|
362
|
+
/* The popover's own backdrop would double the dim; this element paints its own. */
|
|
363
|
+
.fdy-busy::backdrop{background:transparent;}
|
|
364
|
+
|
|
365
|
+
/* The caption sits on a solid surface, not on the scrim. A scrim is semi-transparent over unknown
|
|
366
|
+
* content, so text on it has no computable contrast ratio and no gate could ever cover it. On
|
|
367
|
+
* --color-surface it is the same pair the contrast suite already checks.
|
|
368
|
+
*
|
|
369
|
+
* THIS IS ALSO WHY THIS PANEL DOES NOT JOIN THE GLASS AXIS, and the omission is a decision rather
|
|
370
|
+
* than an oversight. Every other raised panel reads --color-surface-raised and frosts under
|
|
371
|
+
* [data-style="glass"]; this one stays opaque. The glass gate asserts its ink against the panel
|
|
372
|
+
* composited over the PAGE GROUND (--color-surface-2), which is what sits behind a card or a modal.
|
|
373
|
+
* Behind this one is the scrim, over arbitrary content — the very case that has no computable
|
|
374
|
+
* ratio. A frosted busy panel would be the one surface in the kit whose caption nothing could
|
|
375
|
+
* verify, while it is up precisely because the app is doing something the reader must not
|
|
376
|
+
* interrupt. Left opaque on purpose; do not "fix" it for consistency. */
|
|
377
|
+
.fdy-busy__panel{display:flex;flex-direction:column;align-items:center;gap:var(--space-4);padding:var(--space-6) var(--space-8);border-radius:var(--radius-lg);background:var(--color-surface);box-shadow:var(--shadow-lift);max-width:min(20rem,calc(100vw - var(--space-8)));}
|
|
378
|
+
.fdy-busy__mark{display:flex;align-items:center;justify-content:center;min-height:2.5rem;}
|
|
379
|
+
.fdy-busy__caption{margin:0;font-size:var(--text-sm);font-weight:var(--weight-medium);color:var(--color-text);text-align:center;}
|
|
380
|
+
|
|
381
|
+
/* Nothing travels under reduced motion. The spinner keeps turning (slowed by spinner.css): a
|
|
382
|
+
* loading indicator that stops moving reads as "finished", which is the opposite of the message. */
|
|
383
|
+
@media (prefers-reduced-motion:reduce){
|
|
384
|
+
.fdy-busy{transition:none;}
|
|
385
|
+
}
|
|
386
|
+
|
|
320
387
|
/* Freeday. Button */
|
|
321
388
|
.fdy-btn{display:inline-flex;align-items:center;justify-content:center;gap:var(--space-2);
|
|
322
389
|
height:var(--control-h);padding:0 var(--space-4);
|
|
@@ -420,7 +487,7 @@ a { color: var(--color-primary); }
|
|
|
420
487
|
/* Freeday. Card */
|
|
421
488
|
/* position:relative, containing block for out-of-flow card content (see base.css); it also gives
|
|
422
489
|
consumers the anchor a corner ribbon/badge needs. */
|
|
423
|
-
.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);}
|
|
490
|
+
.fdy-card{position:relative;background:var(--color-surface-raised);backdrop-filter:var(--surface-filter);-webkit-backdrop-filter:var(--surface-filter);border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);overflow:hidden;box-shadow:var(--shadow-lift),var(--surface-inset);transition:box-shadow var(--dur-base) var(--ease-standard),transform var(--dur-base) var(--ease-standard);}
|
|
424
491
|
.fdy-card--elevated{box-shadow:var(--shadow-lift-hover);}
|
|
425
492
|
.fdy-card--interactive{cursor:pointer;}
|
|
426
493
|
.fdy-card--interactive:hover{box-shadow:var(--shadow-lift-hover);transform:translateY(-3px);}
|
|
@@ -734,9 +801,22 @@ a { color: var(--color-primary); }
|
|
|
734
801
|
grid track, `container-type:inline-size` on a standalone .fdy-stat would stop it sizing to its
|
|
735
802
|
own content. The unconditional rule above stays as the fallback wherever @container is missing. */
|
|
736
803
|
@supports (container-type:inline-size){
|
|
737
|
-
.fdy-stats>.fdy-stat{container-type:inline-size;}
|
|
738
|
-
.fdy-stats>.fdy-stat .fdy-stat__value{font-size:clamp(var(--text-xl),11cqw,var(--text-3xl));}
|
|
804
|
+
.fdy-stats:not(.fdy-stats--inline)>.fdy-stat{container-type:inline-size;}
|
|
805
|
+
.fdy-stats:not(.fdy-stats--inline)>.fdy-stat .fdy-stat__value{font-size:clamp(var(--text-xl),11cqw,var(--text-3xl));}
|
|
739
806
|
}
|
|
807
|
+
/* The price of that container, and the reason --inline opts out of it (#050). `container-type`
|
|
808
|
+
applies inline-size CONTAINMENT: the tile's min-content and max-content contributions are both
|
|
809
|
+
ZERO. So the 11rem floor above is not a taste choice, it is the only thing giving these tracks a
|
|
810
|
+
size, and re-tracking the grid on content (`auto`, `max-content`, `minmax(0,…)`) resolves every
|
|
811
|
+
track to 0. `justify-items` is the sharper edge: anything but the default `stretch` sizes the
|
|
812
|
+
tile to that zero contribution even when the track is correctly wide, and three labels paint on
|
|
813
|
+
top of each other. Measured from IDU_EMATE_ACCT_WEB's page header, where a hugging strip came out
|
|
814
|
+
40px wide, all of it gap.
|
|
815
|
+
--inline is the shape that header wanted: a strip that hugs its numbers and sits beside a page
|
|
816
|
+
title, instead of a full-width band whose 11rem floor collapses to one column at 390px. It cannot
|
|
817
|
+
have both, so it drops the container and keeps the flat --text-3xl the base rule already sets;
|
|
818
|
+
a track sized to its own content is never the narrow track the clamp exists for. */
|
|
819
|
+
.fdy-stats--inline{grid-template-columns:none;grid-auto-flow:column;grid-auto-columns:minmax(0,max-content);}
|
|
740
820
|
.fdy-stats--boxed{background:var(--color-surface);border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-1);gap:0;}
|
|
741
821
|
.fdy-stats--boxed .fdy-stat{padding:var(--space-5);border-right:var(--bw) solid var(--color-border-muted);}
|
|
742
822
|
.fdy-stats--boxed .fdy-stat:last-child{border-right:0;}
|
|
@@ -865,7 +945,7 @@ a { color: var(--color-primary); }
|
|
|
865
945
|
/* display is gated behind [open] so the native <dialog> stays hidden until opened.
|
|
866
946
|
* An unconditional display here would beat the UA `dialog:not([open]){display:none}`
|
|
867
947
|
* rule (author origin wins over user-agent) and leave the drawer visible on load. */
|
|
868
|
-
.fdy-drawer{position:fixed;inset:0 auto 0 0;margin:0;padding:0;height:100vh;max-height:none;width:min(20rem,82vw);flex-direction:column;border:0;background:var(--color-surface);color:var(--color-text);box-shadow:var(--shadow-4);overflow:hidden;transform:translateX(-100%);transition:transform var(--dur-slow) var(--ease-standard),overlay var(--dur-slow) allow-discrete,display var(--dur-slow) allow-discrete;}
|
|
948
|
+
.fdy-drawer{position:fixed;inset:0 auto 0 0;margin:0;padding:0;height:100vh;max-height:none;width:min(20rem,82vw);flex-direction:column;border:0;background:var(--color-surface-raised);backdrop-filter:var(--surface-filter);-webkit-backdrop-filter:var(--surface-filter);color:var(--color-text);box-shadow:var(--shadow-4),var(--surface-inset);overflow:hidden;transform:translateX(-100%);transition:transform var(--dur-slow) var(--ease-standard),overlay var(--dur-slow) allow-discrete,display var(--dur-slow) allow-discrete;}
|
|
869
949
|
.fdy-drawer[open]{display:flex;transform:none;}
|
|
870
950
|
.fdy-drawer--right{inset:0 0 0 auto;transform:translateX(100%);}
|
|
871
951
|
.fdy-drawer::backdrop{background:rgba(10,12,25,.5);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);opacity:0;transition:opacity var(--dur-slow) var(--ease-standard),overlay var(--dur-slow) allow-discrete,display var(--dur-slow) allow-discrete;}
|
|
@@ -942,8 +1022,9 @@ a { color: var(--color-primary); }
|
|
|
942
1022
|
/* Fields are direct children. Drop the standalone field's 22rem cap; default to the
|
|
943
1023
|
* medium column width with no unbounded growth (that's what caused the raggedness). */
|
|
944
1024
|
.fdy-filterbar>.fdy-field{max-width:none;flex:0 0 auto;width:12rem;}
|
|
945
|
-
/* Controls fill their field's column
|
|
946
|
-
|
|
1025
|
+
/* Controls fill their field's column: released in input.css for every field, not just this one.
|
|
1026
|
+
The rule used to live here, naming the input group and the combo only, which is why an
|
|
1027
|
+
.fdy-autocomplete or an .fdy-cascade in a --w-2xl field still stopped 48px short of it (#051). */
|
|
947
1028
|
/* Column-width rhythm, pick one per field so bars across the app line up. */
|
|
948
1029
|
.fdy-filterbar>.fdy-field--w-sm{width:9rem;}
|
|
949
1030
|
.fdy-filterbar>.fdy-field--w-lg{width:15rem;}
|
|
@@ -1020,6 +1101,15 @@ a { color: var(--color-primary); }
|
|
|
1020
1101
|
|
|
1021
1102
|
/* Freeday. Field & Input */
|
|
1022
1103
|
.fdy-field{display:flex;flex-direction:column;gap:var(--space-2);max-width:22rem;}
|
|
1104
|
+
/* Inside a labelled field, a control fills the field (#051). The 22rem cap those controls carry is
|
|
1105
|
+
for one standing on its own, with no field to take a width from; #017 already released the date
|
|
1106
|
+
and time pickers here for exactly this reason, and its comment claims a picker then fills its
|
|
1107
|
+
field "like every other control". That was not true of these four: a `--full` row in a
|
|
1108
|
+
.fdy-form-grid held a 352px combo under a 600px label, and a 26rem toolbar search field hid 64px
|
|
1109
|
+
of dead space INSIDE itself, so an author who wrote `gap:var(--space-3)` measured 76px on screen.
|
|
1110
|
+
The field's own max-width above is what governs, and it still caps a field nobody has widened, so
|
|
1111
|
+
this changes nothing wherever the cap was already doing its job. */
|
|
1112
|
+
.fdy-field>.fdy-input-group,.fdy-field>.fdy-combo,.fdy-field>.fdy-autocomplete,.fdy-field>.fdy-cascade{max-width:none;}
|
|
1023
1113
|
/* A grouped field is legitimately a <fieldset> (it needs a <legend>), e.g. a radio/checkbox group.
|
|
1024
1114
|
Neutralise the UA groove border / padding / min-inline-size so it lays out identically to the
|
|
1025
1115
|
<div> form (min-inline-size:min-content would otherwise block the filterbar column widths). */
|
|
@@ -1108,7 +1198,7 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
|
|
|
1108
1198
|
|
|
1109
1199
|
/* Freeday. Menu (action popup) + Split button. Enhanced by freeday-menu.js. */
|
|
1110
1200
|
.fdy-menu-wrap{position:relative;display:inline-flex;}
|
|
1111
|
-
.fdy-menu{position:absolute;top:calc(100% + var(--space-1));left:0;z-index:130;min-width:11rem;margin:0;padding:var(--space-1);list-style:none;background:var(--color-surface);color:var(--color-text);border:var(--bw) solid var(--color-border-strong);border-radius:var(--radius-md);box-shadow:var(--shadow-3);}
|
|
1201
|
+
.fdy-menu{position:absolute;top:calc(100% + var(--space-1));left:0;z-index:130;min-width:11rem;margin:0;padding:var(--space-1);list-style:none;background:var(--color-surface-raised);backdrop-filter:var(--surface-filter);-webkit-backdrop-filter:var(--surface-filter);color:var(--color-text);border:var(--bw) solid var(--color-border-strong);border-radius:var(--radius-md);box-shadow:var(--shadow-3),var(--surface-inset);}
|
|
1112
1202
|
.fdy-menu[hidden]{display:none;}
|
|
1113
1203
|
.fdy-menu--end{left:auto;right:0;}
|
|
1114
1204
|
.fdy-menu__item{display:flex;align-items:center;gap:var(--space-2);width:100%;padding:var(--space-2) var(--space-3);border:0;background:transparent;border-radius:var(--radius-sm);font-family:var(--font-body);font-size:var(--text-sm);color:var(--color-text);text-align:left;cursor:pointer;white-space:nowrap;}
|
|
@@ -1139,7 +1229,7 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
|
|
|
1139
1229
|
.fdy-btn-split__toggle svg{display:block;width:1rem;height:1rem;}
|
|
1140
1230
|
|
|
1141
1231
|
/* Freeday. Modal (native <dialog>) */
|
|
1142
|
-
.fdy-modal{width:min(32rem,calc(100vw - var(--space-8)));max-height:calc(100vh - var(--space-16));padding:0;border:var(--bw) solid var(--color-border);border-radius:var(--radius-xl);background:var(--color-surface);color:var(--color-text);box-shadow:var(--shadow-lift-hover);overflow:hidden;flex-direction:column;opacity:0;transform:translateY(8px) scale(.98);transition:opacity var(--dur-base) var(--ease-spring),transform var(--dur-base) var(--ease-spring),overlay var(--dur-base) allow-discrete,display var(--dur-base) allow-discrete;}
|
|
1232
|
+
.fdy-modal{width:min(32rem,calc(100vw - var(--space-8)));max-height:calc(100vh - var(--space-16));padding:0;border:var(--bw) solid var(--color-border);border-radius:var(--radius-xl);background:var(--color-surface-raised);backdrop-filter:var(--surface-filter);-webkit-backdrop-filter:var(--surface-filter);color:var(--color-text);box-shadow:var(--shadow-lift-hover),var(--surface-inset);overflow:hidden;flex-direction:column;opacity:0;transform:translateY(8px) scale(.98);transition:opacity var(--dur-base) var(--ease-spring),transform var(--dur-base) var(--ease-spring),overlay var(--dur-base) allow-discrete,display var(--dur-base) allow-discrete;}
|
|
1143
1233
|
.fdy-modal--sm{width:min(24rem,calc(100vw - var(--space-8)));}
|
|
1144
1234
|
.fdy-modal--md{width:min(32rem,calc(100vw - var(--space-8)));}
|
|
1145
1235
|
.fdy-modal--lg{width:min(48rem,calc(100vw - var(--space-8)));}
|
|
@@ -1289,7 +1379,18 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
|
|
|
1289
1379
|
.fdy-step.is-active .fdy-step__marker{border-color:var(--color-primary);background:var(--color-primary-soft);color:var(--color-primary-strong);}
|
|
1290
1380
|
.fdy-step.is-complete .fdy-step__marker{border-color:var(--color-primary);background:var(--color-primary);color:var(--color-on-primary);}
|
|
1291
1381
|
.fdy-step.is-error .fdy-step__marker{border-color:var(--color-danger);background:var(--color-danger-btn);color:var(--color-on-danger);}
|
|
1382
|
+
/* The error badge. Colour alone cannot carry "this step has a problem" — the marker already turns
|
|
1383
|
+
red and that is exactly the signal a colour-blind reader loses — so the badge holds a COUNT and
|
|
1384
|
+
the step names it in text. Positioned against `.fdy-step` rather than `__btn`, because the
|
|
1385
|
+
indicator-only variant has no `__btn` and the badge has to land in the same place in both.
|
|
1386
|
+
It is a SIBLING of the marker, never a child: the enhancer rewrites `__marker.innerHTML` on
|
|
1387
|
+
every render, so a badge inside it would be destroyed the first time the reader moved a step. */
|
|
1388
|
+
.fdy-step__badge{position:absolute;top:0;left:50%;z-index:2;transform:translate(.45rem,-.3rem);min-width:1.05rem;height:1.05rem;padding:0 .25rem;display:inline-flex;align-items:center;justify-content:center;border-radius:var(--radius-full);background:var(--color-danger-btn);color:var(--color-on-danger);font-size:var(--text-xs);font-weight:var(--weight-semibold);font-variant-numeric:tabular-nums;line-height:1;}
|
|
1292
1389
|
.fdy-step__label{font-size:var(--text-sm);color:var(--color-text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;}
|
|
1390
|
+
.fdy-step.is-error .fdy-step__label{color:var(--color-danger-strong);}
|
|
1391
|
+
/* While a guard is deciding, the whole control is spoken for: the nav buttons are disabled by the
|
|
1392
|
+
enhancer, and this dims the header so a second click has nothing to aim at. */
|
|
1393
|
+
.fdy-stepper[aria-busy="true"]{opacity:.72;}
|
|
1293
1394
|
.fdy-step.is-active .fdy-step__label{color:var(--color-text);font-weight:var(--weight-semibold);}
|
|
1294
1395
|
.fdy-step__btn:focus-visible{outline:none;}
|
|
1295
1396
|
.fdy-step__btn:focus-visible .fdy-step__marker{box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary) 30%,transparent);}
|
|
@@ -1308,6 +1409,13 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
|
|
|
1308
1409
|
.fdy-table td{padding:var(--space-3) var(--space-4);border-bottom:var(--bw) solid var(--color-border-muted);color:var(--color-text);}
|
|
1309
1410
|
.fdy-table tbody tr:last-child td{border-bottom:0;}
|
|
1310
1411
|
.fdy-table tbody tr{transition:background-color var(--dur-fast) var(--ease-standard);}
|
|
1412
|
+
/* Zebra rows. Sits BEFORE :hover and [aria-selected] on purpose: all three land on the same
|
|
1413
|
+
specificity (0,2,2), so source order is what decides, and a stripe that won would make hover
|
|
1414
|
+
invisible on every second row. The tint is HALF a surface step rather than --color-surface-2
|
|
1415
|
+
itself, for the same reason: hover paints the full step, so the two must not be the same colour.
|
|
1416
|
+
Exposed as a Tier-3 token through the var() FALLBACK, never as a declaration — a custom property
|
|
1417
|
+
set here would outrank a host's own :root override. */
|
|
1418
|
+
.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)));}
|
|
1311
1419
|
.fdy-table tbody tr:hover{background:var(--color-surface-2);}
|
|
1312
1420
|
/* Monospace data, ids, codes, IPs, timestamps. Alignment-neutral so it works in any cell
|
|
1313
1421
|
or inline (`<span class="fdy-mono">`), not just numeric columns. */
|
|
@@ -1372,6 +1480,12 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
|
|
|
1372
1480
|
a hovered row should band unbroken across the freeze line, says so itself. */
|
|
1373
1481
|
.fdy-table--sticky tbody .fdy-table__freeze{z-index:1;}
|
|
1374
1482
|
.fdy-table--sticky tbody td.fdy-table__freeze{background:var(--color-surface);}
|
|
1483
|
+
/* A frozen cell has to paint an OPAQUE background of its own (it scrolls over its neighbours), so
|
|
1484
|
+
on a striped table it would cut a plain column straight through the zebra. Repaint it with the
|
|
1485
|
+
stripe — and deliberately WITHOUT `.fdy-table--sticky` in the selector, which would push this to
|
|
1486
|
+
(0,4,3) and beat the hover rule below. At (0,3,3) it ties, and the later rule wins, so a hovered
|
|
1487
|
+
row still bands unbroken across the freeze line. */
|
|
1488
|
+
.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)));}
|
|
1375
1489
|
.fdy-table--sticky tbody tr:hover td.fdy-table__freeze{background:var(--color-surface-2);}
|
|
1376
1490
|
/* A frozen header cell is stuck on both axes wherever the head is frozen too, so
|
|
1377
1491
|
it has to outrank both. */
|
package/dist/freeday.js
CHANGED
|
@@ -62,7 +62,13 @@
|
|
|
62
62
|
|
|
63
63
|
function isOverlayOpen() { return app.classList.contains('fdy-app--nav-open'); }
|
|
64
64
|
function isCollapsed() { return app.classList.contains('fdy-app--nav-collapsed'); }
|
|
65
|
-
|
|
65
|
+
/* Whether the nav FLOATS. Two ways to be true, and only one of them is the viewport: below the
|
|
66
|
+
breakpoint it is off-canvas by definition, and above it `--nav-overlay` says the app chose to
|
|
67
|
+
float a nav that could have been a column. Everything downstream — which class means visible,
|
|
68
|
+
what the toggle does, whether the content goes inert — asks this instead of the media query,
|
|
69
|
+
so overlay mode reuses the drawer's whole code path rather than growing a second one. */
|
|
70
|
+
function isOverlayMode() { return !mqWide.matches || app.classList.contains('fdy-app--nav-overlay'); }
|
|
71
|
+
function navVisible() { return isOverlayMode() ? isOverlayOpen() : !isCollapsed(); }
|
|
66
72
|
|
|
67
73
|
/* aria-expanded answers "is the nav showing?" in BOTH modes, the two state classes are the
|
|
68
74
|
kit's business, not the reader's. */
|
|
@@ -70,7 +76,7 @@
|
|
|
70
76
|
var visible = navVisible();
|
|
71
77
|
toggle.setAttribute('aria-expanded', String(visible));
|
|
72
78
|
setInert(sidebar, !visible);
|
|
73
|
-
setInert(content,
|
|
79
|
+
setInert(content, isOverlayMode() && visible);
|
|
74
80
|
/* Announce only real changes. The first sync() runs at init to describe the state the markup
|
|
75
81
|
arrived in, which is not something a host asked for and must not look like one. */
|
|
76
82
|
if (lastVisible !== null && visible !== lastVisible) {
|
|
@@ -108,7 +114,7 @@
|
|
|
108
114
|
}
|
|
109
115
|
|
|
110
116
|
toggle.addEventListener('click', function () {
|
|
111
|
-
if (
|
|
117
|
+
if (!isOverlayMode()) {
|
|
112
118
|
app.classList.toggle('fdy-app--nav-collapsed');
|
|
113
119
|
sync();
|
|
114
120
|
} else if (isOverlayOpen()) {
|
|
@@ -156,7 +162,10 @@
|
|
|
156
162
|
content inert forever: the panel becomes a static column again, and the page it is covering
|
|
157
163
|
can no longer be clicked or read. */
|
|
158
164
|
mqWide.addEventListener('change', function () {
|
|
159
|
-
|
|
165
|
+
/* Only when the panel stops floating. In overlay MODE it floats at every width, so widening
|
|
166
|
+
must leave an open panel exactly as it is — closing it there would be the shell overruling
|
|
167
|
+
a reader who never asked for anything. */
|
|
168
|
+
if (mqWide.matches && !isOverlayMode() && isOverlayOpen()) close(false);
|
|
160
169
|
else sync();
|
|
161
170
|
});
|
|
162
171
|
|
|
@@ -166,9 +175,14 @@
|
|
|
166
175
|
own state needs to drive this without reaching for the class names the kit reserves. */
|
|
167
176
|
app._fdyAppShell = {
|
|
168
177
|
isVisible: navVisible,
|
|
178
|
+
/* Re-read the DOM and reconcile `inert` + `aria-expanded`. Needed when something OUTSIDE the
|
|
179
|
+
enhancer changes what the state classes mean — switching `--nav-overlay` on or off does
|
|
180
|
+
exactly that, because it moves the answer to "is the nav visible?" from `--nav-collapsed`
|
|
181
|
+
to `--nav-open`. Without this a mode switch leaves a visible sidebar marked inert. */
|
|
182
|
+
refresh: sync,
|
|
169
183
|
setVisible: function (visible) {
|
|
170
184
|
if (visible === navVisible()) return;
|
|
171
|
-
if (
|
|
185
|
+
if (!isOverlayMode()) {
|
|
172
186
|
app.classList.toggle('fdy-app--nav-collapsed', !visible);
|
|
173
187
|
sync();
|
|
174
188
|
} else if (visible) {
|
|
@@ -205,6 +219,10 @@
|
|
|
205
219
|
isVisible: function (root) {
|
|
206
220
|
return !!(root && root._fdyAppShell && root._fdyAppShell.isVisible());
|
|
207
221
|
},
|
|
222
|
+
/* Call after changing `--nav-overlay` from outside; see the note on the handle above. */
|
|
223
|
+
refresh: function (root) {
|
|
224
|
+
if (root && root._fdyAppShell) root._fdyAppShell.refresh();
|
|
225
|
+
},
|
|
208
226
|
};
|
|
209
227
|
})();
|
|
210
228
|
|
|
@@ -415,6 +433,175 @@
|
|
|
415
433
|
};
|
|
416
434
|
})();
|
|
417
435
|
|
|
436
|
+
/* Freeday, busy overlay (optional, zero-dependency).
|
|
437
|
+
*
|
|
438
|
+
* Freeday.busy({ caption, delay, mark }) block the screen while an operation runs
|
|
439
|
+
* Freeday.idle() release it
|
|
440
|
+
*
|
|
441
|
+
* Imperative on purpose, like Freeday.toast(): a component API invites two instances, and two
|
|
442
|
+
* blocking overlays with two captions is the failure this exists to prevent. A second busy() while
|
|
443
|
+
* one is up REPLACES the caption rather than stacking.
|
|
444
|
+
*
|
|
445
|
+
* caption what is happening. Announced politely, so make it a sentence a reader would want read
|
|
446
|
+
* out, not a spinner label. Omitted, it falls back to the kit default, which a page
|
|
447
|
+
* overrides once with `data-fdy-text-caption` on <html>.
|
|
448
|
+
* delay ms to wait before it appears (default 120; 0 shows immediately). An operation that
|
|
449
|
+
* finishes in 80ms should never flash a scrim — that reads as a glitch, not as progress.
|
|
450
|
+
* mark an Element to use instead of the default spinner. Element only, never an HTML string:
|
|
451
|
+
* a string here would be an injection point in every app that passed user text through.
|
|
452
|
+
*
|
|
453
|
+
* Not a dialog. Interaction is removed with `inert` on everything else, so there is nothing to trap
|
|
454
|
+
* focus against and nothing to dismiss. Focus is parked on the panel and given back on idle(),
|
|
455
|
+
* because the element it was on is inert by then and the browser would otherwise drop it to <body>.
|
|
456
|
+
*/
|
|
457
|
+
(function () {
|
|
458
|
+
'use strict';
|
|
459
|
+
|
|
460
|
+
var DEFAULT_DELAY = 120;
|
|
461
|
+
|
|
462
|
+
var TEXT = {
|
|
463
|
+
caption: 'Working…'
|
|
464
|
+
};
|
|
465
|
+
/* The overlay has no root of its own to carry an override — it is created, not hydrated — so the
|
|
466
|
+
lookup goes to <html>, the one element every page has before this runs. Kebab-cased for the
|
|
467
|
+
same reason as everywhere else: HTML lowercases attribute names, so a camelCase key could only
|
|
468
|
+
ever be written run-together and the override would fail silently. */
|
|
469
|
+
function textOf(key) {
|
|
470
|
+
var root = document.documentElement;
|
|
471
|
+
var kebab = root.getAttribute('data-fdy-text-' + key.replace(/[A-Z]/g, function (c) { return '-' + c.toLowerCase(); }));
|
|
472
|
+
var custom = kebab != null && kebab !== '' ? kebab : root.getAttribute('data-fdy-text-' + key);
|
|
473
|
+
return custom != null && custom !== '' ? custom : TEXT[key];
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
var node = null;
|
|
477
|
+
var showTimer = null;
|
|
478
|
+
var inerted = [];
|
|
479
|
+
var returnFocusTo = null;
|
|
480
|
+
|
|
481
|
+
function build() {
|
|
482
|
+
var el = document.createElement('div');
|
|
483
|
+
el.className = 'fdy-busy';
|
|
484
|
+
el.setAttribute('popover', 'manual');
|
|
485
|
+
el.setAttribute('aria-busy', 'true');
|
|
486
|
+
el.tabIndex = -1;
|
|
487
|
+
|
|
488
|
+
var panel = document.createElement('div');
|
|
489
|
+
panel.className = 'fdy-busy__panel';
|
|
490
|
+
|
|
491
|
+
var mark = document.createElement('div');
|
|
492
|
+
mark.className = 'fdy-busy__mark';
|
|
493
|
+
// aria-hidden: the caption below is the message. A second announcement from the spinner's own
|
|
494
|
+
// role="status" would say "busy" twice and name nothing.
|
|
495
|
+
mark.setAttribute('aria-hidden', 'true');
|
|
496
|
+
mark.appendChild(defaultMark());
|
|
497
|
+
|
|
498
|
+
var caption = document.createElement('p');
|
|
499
|
+
caption.className = 'fdy-busy__caption';
|
|
500
|
+
// role="status" rather than a dialog role: this reports a state, it does not ask a question.
|
|
501
|
+
caption.setAttribute('role', 'status');
|
|
502
|
+
|
|
503
|
+
panel.appendChild(mark);
|
|
504
|
+
panel.appendChild(caption);
|
|
505
|
+
el.appendChild(panel);
|
|
506
|
+
return el;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function defaultMark() {
|
|
510
|
+
var spinner = document.createElement('span');
|
|
511
|
+
spinner.className = 'fdy-spinner fdy-spinner--lg';
|
|
512
|
+
return spinner;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** inert everything else, remembering ONLY what we set so an app's own inert is never cleared. */
|
|
516
|
+
function block(on) {
|
|
517
|
+
var i;
|
|
518
|
+
if (on) {
|
|
519
|
+
var kids = document.body.children;
|
|
520
|
+
for (i = 0; i < kids.length; i++) {
|
|
521
|
+
var child = kids[i];
|
|
522
|
+
if (child === node || child.hasAttribute('inert')) continue;
|
|
523
|
+
child.setAttribute('inert', '');
|
|
524
|
+
inerted.push(child);
|
|
525
|
+
}
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
for (i = 0; i < inerted.length; i++) inerted[i].removeAttribute('inert');
|
|
529
|
+
inerted = [];
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function isOpen() {
|
|
533
|
+
return node !== null && node.classList.contains('is-open');
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function setCaption(text) {
|
|
537
|
+
node.querySelector('.fdy-busy__caption').textContent = text;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function setMark(el) {
|
|
541
|
+
var slot = node.querySelector('.fdy-busy__mark');
|
|
542
|
+
while (slot.firstChild) slot.removeChild(slot.firstChild);
|
|
543
|
+
slot.appendChild(el instanceof Element ? el : defaultMark());
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function show(opts) {
|
|
547
|
+
showTimer = null;
|
|
548
|
+
setCaption(opts.caption == null ? textOf('caption') : String(opts.caption));
|
|
549
|
+
if (opts.mark !== undefined) setMark(opts.mark);
|
|
550
|
+
|
|
551
|
+
returnFocusTo = document.activeElement;
|
|
552
|
+
document.body.appendChild(node);
|
|
553
|
+
node.classList.add('is-open');
|
|
554
|
+
// Top layer, so it also covers an open <dialog>. Where the API is missing the z-index in
|
|
555
|
+
// busy.css is the fallback; it cannot clear a modal, and that is stated in COMPONENTS.md.
|
|
556
|
+
if (typeof node.showPopover === 'function') {
|
|
557
|
+
try { node.showPopover(); } catch (e) { /* already open, or not connected yet */ }
|
|
558
|
+
}
|
|
559
|
+
block(true);
|
|
560
|
+
node.focus();
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function busy(options) {
|
|
564
|
+
var opts = options || {};
|
|
565
|
+
if (node === null) node = build();
|
|
566
|
+
|
|
567
|
+
// Already up: this is a second owner talking. Update what it says, do not stack.
|
|
568
|
+
if (isOpen()) {
|
|
569
|
+
if (opts.caption != null) setCaption(String(opts.caption));
|
|
570
|
+
if (opts.mark !== undefined) setMark(opts.mark);
|
|
571
|
+
return node;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
var delay = opts.delay == null ? DEFAULT_DELAY : Number(opts.delay);
|
|
575
|
+
if (showTimer !== null) clearTimeout(showTimer);
|
|
576
|
+
if (delay > 0) showTimer = setTimeout(function () { show(opts); }, delay);
|
|
577
|
+
else show(opts);
|
|
578
|
+
return node;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function idle() {
|
|
582
|
+
// Cancels a pending show too: an operation that beat the delay must leave nothing behind.
|
|
583
|
+
if (showTimer !== null) { clearTimeout(showTimer); showTimer = null; }
|
|
584
|
+
if (node === null || !isOpen()) return;
|
|
585
|
+
|
|
586
|
+
block(false);
|
|
587
|
+
if (typeof node.hidePopover === 'function') {
|
|
588
|
+
try { node.hidePopover(); } catch (e) { /* was never in the top layer */ }
|
|
589
|
+
}
|
|
590
|
+
node.classList.remove('is-open');
|
|
591
|
+
if (node.parentNode !== null) node.parentNode.removeChild(node);
|
|
592
|
+
|
|
593
|
+
// Give focus back to whatever had it, now that its ancestor is no longer inert.
|
|
594
|
+
if (returnFocusTo !== null && typeof returnFocusTo.focus === 'function' && returnFocusTo.isConnected) {
|
|
595
|
+
returnFocusTo.focus();
|
|
596
|
+
}
|
|
597
|
+
returnFocusTo = null;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
window.Freeday = window.Freeday || {};
|
|
601
|
+
window.Freeday.busy = busy;
|
|
602
|
+
window.Freeday.idle = idle;
|
|
603
|
+
})();
|
|
604
|
+
|
|
418
605
|
/* Freeday, carousel enhancer (optional, zero-dependency).
|
|
419
606
|
* Scroll-snap slider with prev/next arrows, generated dot indicators, keyboard (←/→),
|
|
420
607
|
* and optional autoplay (data-fdy-autoplay="4000", pauses on hover/focus). Auto-inits
|
|
@@ -3474,7 +3661,9 @@
|
|
|
3474
3661
|
* <div class="fdy-step-panel">…</div>… </div>
|
|
3475
3662
|
* <div class="fdy-step-nav"><button data-fdy-step-prev>…</button>
|
|
3476
3663
|
* <button data-fdy-step-next>…</button></div></div>
|
|
3477
|
-
* Emits bubbling "fdy-step-change" {index} and "fdy-step-finish" (Next on the last step)
|
|
3664
|
+
* Emits bubbling "fdy-step-change" {index} and "fdy-step-finish" (Next on the last step), and a
|
|
3665
|
+
* cancelable "fdy-step-before-change" {from, to, waitFor} that a guard refuses with
|
|
3666
|
+
* preventDefault() or defers by assigning a promise to detail.waitFor.
|
|
3478
3667
|
*/
|
|
3479
3668
|
(function () {
|
|
3480
3669
|
'use strict';
|
|
@@ -3553,14 +3742,62 @@
|
|
|
3553
3742
|
render();
|
|
3554
3743
|
}
|
|
3555
3744
|
|
|
3745
|
+
/* Leaving a step is REFUSABLE, because a wizard whose Next cannot be stopped is a wizard that
|
|
3746
|
+
* validates nothing. Two ways to refuse, and the second is why an event alone was not enough:
|
|
3747
|
+
*
|
|
3748
|
+
* sync handler calls ev.preventDefault() — the answer is already known
|
|
3749
|
+
* async handler sets ev.detail.waitFor = promise — the answer is a server round-trip away
|
|
3750
|
+
*
|
|
3751
|
+
* Resolving to `false` refuses; anything else advances, so a handler that forgets to return is
|
|
3752
|
+
* not read as a rejection. HOW validity is decided stays entirely with the app: the kit has no
|
|
3753
|
+
* opinion about form libraries and this is the line that keeps it that way. */
|
|
3754
|
+
var deciding = false;
|
|
3755
|
+
|
|
3756
|
+
function lock(on) {
|
|
3757
|
+
deciding = on;
|
|
3758
|
+
var list = root.querySelector('.fdy-stepper');
|
|
3759
|
+
if (list) { if (on) list.setAttribute('aria-busy', 'true'); else list.removeAttribute('aria-busy'); }
|
|
3760
|
+
if (prevBtn) prevBtn.disabled = on || active === 0;
|
|
3761
|
+
if (nextBtn) nextBtn.disabled = on;
|
|
3762
|
+
}
|
|
3763
|
+
|
|
3764
|
+
function request(to, onAllowed) {
|
|
3765
|
+
if (deciding) return;
|
|
3766
|
+
var ev = new CustomEvent('fdy-step-before-change', {
|
|
3767
|
+
bubbles: true,
|
|
3768
|
+
cancelable: true,
|
|
3769
|
+
detail: { from: active, to: to, waitFor: null },
|
|
3770
|
+
});
|
|
3771
|
+
root.dispatchEvent(ev);
|
|
3772
|
+
if (ev.defaultPrevented) return;
|
|
3773
|
+
|
|
3774
|
+
var pending = ev.detail.waitFor;
|
|
3775
|
+
if (pending === null || typeof pending.then !== 'function') { onAllowed(); return; }
|
|
3776
|
+
|
|
3777
|
+
lock(true);
|
|
3778
|
+
pending.then(
|
|
3779
|
+
function (ok) { lock(false); if (ok !== false) onAllowed(); },
|
|
3780
|
+
// A guard that THREW decided nothing, so it must not advance. Staying put with the nav
|
|
3781
|
+
// released is the only safe reading; the app's own error handling reports the failure.
|
|
3782
|
+
function () { lock(false); },
|
|
3783
|
+
);
|
|
3784
|
+
}
|
|
3785
|
+
|
|
3556
3786
|
if (prevBtn) prevBtn.addEventListener('click', function () { go(active - 1); });
|
|
3557
3787
|
if (nextBtn) nextBtn.addEventListener('click', function () {
|
|
3558
|
-
if (active < steps.length - 1) go(active + 1);
|
|
3559
|
-
else root.dispatchEvent(new CustomEvent('fdy-step-finish', { bubbles: true }));
|
|
3788
|
+
if (active < steps.length - 1) request(active + 1, function () { go(active + 1); });
|
|
3789
|
+
else request(active + 1, function () { root.dispatchEvent(new CustomEvent('fdy-step-finish', { bubbles: true })); });
|
|
3560
3790
|
});
|
|
3561
3791
|
steps.forEach(function (s, i) {
|
|
3562
3792
|
var btn = s.querySelector('.fdy-step__btn');
|
|
3563
|
-
if (btn)
|
|
3793
|
+
if (!btn) return;
|
|
3794
|
+
btn.addEventListener('click', function () {
|
|
3795
|
+
if (i > maxReached) return;
|
|
3796
|
+
// Going BACK is always allowed — nothing is being committed. Jumping forward to a step
|
|
3797
|
+
// already reached still leaves the current one behind, so it asks the guard like Next does.
|
|
3798
|
+
if (i <= active) go(i);
|
|
3799
|
+
else request(i, function () { go(i); });
|
|
3800
|
+
});
|
|
3564
3801
|
});
|
|
3565
3802
|
|
|
3566
3803
|
render();
|
|
@@ -3668,6 +3905,13 @@
|
|
|
3668
3905
|
var countEl = root.querySelector('[data-fdy-table-count]');
|
|
3669
3906
|
var infoEl = root.querySelector('[data-fdy-table-info]');
|
|
3670
3907
|
var pagerEl = root.querySelector('[data-fdy-table-pagination]');
|
|
3908
|
+
/* COMPONENTS.md documents the wrapper as <nav class="fdy-pagination" data-fdy-table-pagination>
|
|
3909
|
+
and nothing in the kit ever wrote that class, here or in the three typed footers (#050 §2).
|
|
3910
|
+
It carries no rule (NEXT-UP #9), so nothing rendered differently, but a consumer selector or
|
|
3911
|
+
an e2e assertion on the block passed against hand-written markup and failed against the kit's
|
|
3912
|
+
own output, and the raw and typed paths are supposed to be interchangeable. Set once at init,
|
|
3913
|
+
not per render: the wrapper outlives every pager we build into it. */
|
|
3914
|
+
if (pagerEl) pagerEl.classList.add('fdy-pagination');
|
|
3671
3915
|
var pageSizeEl = root.querySelector('[data-fdy-table-page-size]');
|
|
3672
3916
|
var selectAll = root.querySelector('[data-fdy-select-all]');
|
|
3673
3917
|
var pageSize = parseInt(root.getAttribute('data-page-size'), 10) || 0; // 0 = no pagination
|