@cahyo-dimas/freeday 1.52.1 → 1.54.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 +105 -0
- package/COMPONENTS.md +30 -2
- package/README.id.md +2 -2
- package/README.md +2 -2
- package/USAGE.md +6 -2
- package/adapters/blazor/FdyAppShell.razor +50 -0
- package/adapters/blazor/FdyAppShell.razor.cs +114 -0
- package/adapters/core/app-shell.d.ts +30 -0
- package/adapters/core/app-shell.js +134 -0
- package/adapters/react/components/FdyAppShell.tsx +159 -0
- package/adapters/react/index.d.ts +1 -0
- package/adapters/react/index.js +1 -0
- package/adapters/vue/components/FdyAppShell.vue +160 -0
- package/adapters/vue/index.d.ts +1 -0
- package/adapters/vue/index.js +1 -0
- package/dist/freeday-app-shell.js +207 -0
- package/dist/freeday.js +208 -0
- package/docs/agent-onboarding.md +1 -1
- package/docs/reference-screen.html +5 -16
- package/package.json +2 -2
package/dist/freeday.js
CHANGED
|
@@ -1,5 +1,213 @@
|
|
|
1
1
|
/* Freeday enhancers — GENERATED by tokens/build.mjs. Do not edit by hand.
|
|
2
2
|
* Concatenation of every src/*.js enhancer (each a self-contained IIFE). */
|
|
3
|
+
/* Freeday — app shell enhancer (optional, zero-dependency).
|
|
4
|
+
* Opt in with <div class="fdy-app" data-fdy-app>; the markup is otherwise unchanged.
|
|
5
|
+
*
|
|
6
|
+
* The shell has always shipped the state classes and no behaviour, which left every consumer to
|
|
7
|
+
* assemble the toggle, Escape, focus trap, focus restore and `inert` themselves — and the two
|
|
8
|
+
* hand-rolled copies in this repo's own docs already disagreed about which of those exist. Below
|
|
9
|
+
* 721px the sidebar is an OVERLAY (app-shell.css: position:fixed + translateX(-100%)), so it needs
|
|
10
|
+
* the same treatment a modal drawer gets; at or above 721px it is a column that merely collapses.
|
|
11
|
+
*
|
|
12
|
+
* One rule covers both a hidden nav and a hidden page:
|
|
13
|
+
* - `__sidebar` is inert whenever the nav is NOT visible. Off-canvas and collapsed panels stay in
|
|
14
|
+
* the tab order otherwise — translateX(-100%) and width:0 hide a thing from the eye, not from
|
|
15
|
+
* the keyboard, which is how a nav nobody can see still swallows Tab.
|
|
16
|
+
* - `__content` is inert only while the nav is an OPEN OVERLAY, so Tab cannot wander behind the
|
|
17
|
+
* backdrop.
|
|
18
|
+
*
|
|
19
|
+
* Emits a bubbling `fdy-app-nav` CustomEvent (detail {visible}) whenever the nav's visibility
|
|
20
|
+
* changes, and takes `FreedayAppShell.setVisible(root, visible)` from outside. Those two exist for
|
|
21
|
+
* the same reason the other enhancers have them: a host that keeps its own state — the Blazor
|
|
22
|
+
* wrapper binding @bind-NavOpen, an app persisting the collapsed preference — has to be able to
|
|
23
|
+
* hear the change and to drive it, without owning the behaviour twice.
|
|
24
|
+
* FreedayAppShell.init(root) for late-mounted markup.
|
|
25
|
+
*/
|
|
26
|
+
(function () {
|
|
27
|
+
'use strict';
|
|
28
|
+
|
|
29
|
+
/* Must match app-shell.css, which switches at (min-width:721px) / (max-width:720px). */
|
|
30
|
+
var WIDE = '(min-width: 721px)';
|
|
31
|
+
|
|
32
|
+
var FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]),'
|
|
33
|
+
+ ' textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
34
|
+
|
|
35
|
+
/* getClientRects(), not offsetParent: an overlay sidebar is position:fixed, and a fixed element
|
|
36
|
+
reports no offsetParent at all — filtering on it would call every nav item invisible. */
|
|
37
|
+
function focusables(root) {
|
|
38
|
+
return Array.prototype.filter.call(root.querySelectorAll(FOCUSABLE), function (el) {
|
|
39
|
+
return el.getClientRects().length > 0;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function setInert(el, on) {
|
|
44
|
+
if (!el) return;
|
|
45
|
+
if (on) el.setAttribute('inert', '');
|
|
46
|
+
else el.removeAttribute('inert');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function initShell(app) {
|
|
50
|
+
if (app.dataset.fdyAppShellReady === '1') return;
|
|
51
|
+
app.dataset.fdyAppShellReady = '1';
|
|
52
|
+
|
|
53
|
+
var toggle = app.querySelector('.fdy-app__navtoggle');
|
|
54
|
+
var sidebar = app.querySelector('.fdy-app__sidebar');
|
|
55
|
+
if (!toggle || !sidebar) return;
|
|
56
|
+
var content = app.querySelector('.fdy-app__content');
|
|
57
|
+
var backdrop = app.querySelector('.fdy-app__backdrop');
|
|
58
|
+
|
|
59
|
+
var mqWide = window.matchMedia(WIDE);
|
|
60
|
+
var restoreTo = null;
|
|
61
|
+
var lastVisible = null;
|
|
62
|
+
|
|
63
|
+
function isOverlayOpen() { return app.classList.contains('fdy-app--nav-open'); }
|
|
64
|
+
function isCollapsed() { return app.classList.contains('fdy-app--nav-collapsed'); }
|
|
65
|
+
function navVisible() { return mqWide.matches ? !isCollapsed() : isOverlayOpen(); }
|
|
66
|
+
|
|
67
|
+
/* aria-expanded answers "is the nav showing?" in BOTH modes — the two state classes are the
|
|
68
|
+
kit's business, not the reader's. */
|
|
69
|
+
function sync() {
|
|
70
|
+
var visible = navVisible();
|
|
71
|
+
toggle.setAttribute('aria-expanded', String(visible));
|
|
72
|
+
setInert(sidebar, !visible);
|
|
73
|
+
setInert(content, !mqWide.matches && visible);
|
|
74
|
+
/* Announce only real changes. The first sync() runs at init to describe the state the markup
|
|
75
|
+
arrived in, which is not something a host asked for and must not look like one. */
|
|
76
|
+
if (lastVisible !== null && visible !== lastVisible) {
|
|
77
|
+
app.dispatchEvent(new CustomEvent('fdy-app-nav', { bubbles: true, detail: { visible: visible } }));
|
|
78
|
+
}
|
|
79
|
+
lastVisible = visible;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function open() {
|
|
83
|
+
if (isOverlayOpen()) return;
|
|
84
|
+
restoreTo = document.activeElement;
|
|
85
|
+
app.classList.add('fdy-app--nav-open');
|
|
86
|
+
sync();
|
|
87
|
+
var first = focusables(sidebar)[0];
|
|
88
|
+
if (first) {
|
|
89
|
+
first.focus();
|
|
90
|
+
} else {
|
|
91
|
+
sidebar.setAttribute('tabindex', '-1');
|
|
92
|
+
sidebar.focus();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* restoreFocus is false when the viewport closed it rather than the user — a resize must not
|
|
97
|
+
yank focus out from under whatever the reader was doing. */
|
|
98
|
+
function close(restoreFocus) {
|
|
99
|
+
if (!isOverlayOpen()) return;
|
|
100
|
+
app.classList.remove('fdy-app--nav-open');
|
|
101
|
+
sync();
|
|
102
|
+
if (restoreFocus === false) return;
|
|
103
|
+
/* Back to the control that opened it. document.body is where a keyboard user gets stranded,
|
|
104
|
+
so anything that is gone, detached or the body itself falls back to the toggle. */
|
|
105
|
+
var target = (restoreTo && restoreTo !== document.body && document.contains(restoreTo))
|
|
106
|
+
? restoreTo : toggle;
|
|
107
|
+
if (typeof target.focus === 'function') target.focus();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
toggle.addEventListener('click', function () {
|
|
111
|
+
if (mqWide.matches) {
|
|
112
|
+
app.classList.toggle('fdy-app--nav-collapsed');
|
|
113
|
+
sync();
|
|
114
|
+
} else if (isOverlayOpen()) {
|
|
115
|
+
close();
|
|
116
|
+
} else {
|
|
117
|
+
open();
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
if (backdrop) backdrop.addEventListener('click', function () { close(); });
|
|
122
|
+
|
|
123
|
+
/* Following a link inside an overlay nav means "take me there" — leaving the panel open over
|
|
124
|
+
the page you just asked for is the one thing every hand-rolled copy in this repo disagreed on. */
|
|
125
|
+
sidebar.addEventListener('click', function (e) {
|
|
126
|
+
if (!isOverlayOpen()) return;
|
|
127
|
+
if (e.target.closest && e.target.closest('.fdy-nav__item')) close();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
document.addEventListener('keydown', function (e) {
|
|
131
|
+
if (!isOverlayOpen()) return;
|
|
132
|
+
if (e.key === 'Escape') {
|
|
133
|
+
e.preventDefault();
|
|
134
|
+
close();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (e.key !== 'Tab') return;
|
|
138
|
+
/* The trap. `inert` on the content stops Tab reaching the page, but without this the focus
|
|
139
|
+
would walk out of the document into the browser's own chrome instead of cycling. */
|
|
140
|
+
var items = focusables(sidebar);
|
|
141
|
+
if (items.length === 0) {
|
|
142
|
+
e.preventDefault();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
var first = items[0], last = items[items.length - 1];
|
|
146
|
+
if (e.shiftKey && document.activeElement === first) {
|
|
147
|
+
e.preventDefault();
|
|
148
|
+
last.focus();
|
|
149
|
+
} else if (!e.shiftKey && document.activeElement === last) {
|
|
150
|
+
e.preventDefault();
|
|
151
|
+
first.focus();
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
/* Crossing the breakpoint with the overlay open would otherwise leave --nav-open set and the
|
|
156
|
+
content inert forever: the panel becomes a static column again, and the page it is covering
|
|
157
|
+
can no longer be clicked or read. */
|
|
158
|
+
mqWide.addEventListener('change', function () {
|
|
159
|
+
if (mqWide.matches && isOverlayOpen()) close(false);
|
|
160
|
+
else sync();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
sync();
|
|
164
|
+
|
|
165
|
+
/* The same handle the other enhancers expose (`_fdyCombo` and friends): a host that binds its
|
|
166
|
+
own state needs to drive this without reaching for the class names the kit reserves. */
|
|
167
|
+
app._fdyAppShell = {
|
|
168
|
+
isVisible: navVisible,
|
|
169
|
+
setVisible: function (visible) {
|
|
170
|
+
if (visible === navVisible()) return;
|
|
171
|
+
if (mqWide.matches) {
|
|
172
|
+
app.classList.toggle('fdy-app--nav-collapsed', !visible);
|
|
173
|
+
sync();
|
|
174
|
+
} else if (visible) {
|
|
175
|
+
open();
|
|
176
|
+
} else {
|
|
177
|
+
close();
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function initShells(context) {
|
|
184
|
+
var ctx = context || document;
|
|
185
|
+
/* root included: querySelectorAll never matches its own root, and a framework ref often sits ON
|
|
186
|
+
the widget. */
|
|
187
|
+
if (ctx.matches && ctx.matches('[data-fdy-app]')) initShell(ctx);
|
|
188
|
+
Array.prototype.forEach.call(ctx.querySelectorAll('[data-fdy-app]'), initShell);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (document.readyState === 'loading') {
|
|
192
|
+
document.addEventListener('DOMContentLoaded', function () { initShells(); });
|
|
193
|
+
} else {
|
|
194
|
+
initShells();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
window.FreedayAppShell = {
|
|
198
|
+
init: initShells,
|
|
199
|
+
initAll: initShells,
|
|
200
|
+
/* Both take the shell root. A missing or un-initialised root is a no-op rather than a throw:
|
|
201
|
+
a host may race the enhancer on first render, and a crash there is worse than a late sync. */
|
|
202
|
+
setVisible: function (root, visible) {
|
|
203
|
+
if (root && root._fdyAppShell) root._fdyAppShell.setVisible(visible === true);
|
|
204
|
+
},
|
|
205
|
+
isVisible: function (root) {
|
|
206
|
+
return !!(root && root._fdyAppShell && root._fdyAppShell.isVisible());
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
})();
|
|
210
|
+
|
|
3
211
|
/* Freeday — autocomplete enhancer (optional, zero-dependency).
|
|
4
212
|
* Editable combobox (WAI-ARIA APG): a text input filters a role="listbox" of role="option"
|
|
5
213
|
* items as you type; select fills the input. Auto-inits [data-fdy-autocomplete]. Options are
|
package/docs/agent-onboarding.md
CHANGED
|
@@ -46,7 +46,7 @@ upload, tree, tabs, menu, rating, slider, form validation, carousel, timepicker)
|
|
|
46
46
|
wrapper — use the raw markup and hydrate it:
|
|
47
47
|
|
|
48
48
|
- **Vue / React** — `useFreeday(rootRef)` from the same import path, plus `import '@cahyo-dimas/freeday'`
|
|
49
|
-
once at app entry to register the enhancers. (The
|
|
49
|
+
once at app entry to register the enhancers. (The eleven typed components do **not** need this: they
|
|
50
50
|
are native Vue/React implementations of the same markup, not wrappers over the enhancer.)
|
|
51
51
|
- **Blazor** — `FreedayBlazor.initAll` interop. (Here the typed components *are* thin wrappers over
|
|
52
52
|
the enhancers, so the enhancer script is always required.)
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
Doctrine behind the choices: USAGE.md.
|
|
34
34
|
══════════════════════════════════════════════════════════════════════════════════════════ -->
|
|
35
35
|
|
|
36
|
-
<div class="fdy-app" id="app">
|
|
36
|
+
<div class="fdy-app" id="app" data-fdy-app>
|
|
37
37
|
<a class="fdy-skip" href="#main">Skip to main content</a>
|
|
38
38
|
|
|
39
39
|
<!-- Sidebar comes FIRST: .fdy-app is a flex row of [sidebar | content]. The brand lives in the
|
|
@@ -407,6 +407,7 @@
|
|
|
407
407
|
</dialog>
|
|
408
408
|
|
|
409
409
|
<!-- Enhancers: only the ones this screen actually uses. Zero dependencies, load order free. -->
|
|
410
|
+
<script src="../dist/freeday-app-shell.js"></script>
|
|
410
411
|
<script src="../dist/freeday-popover.js"></script>
|
|
411
412
|
<script src="../dist/freeday-select.js"></script>
|
|
412
413
|
<script src="../dist/freeday-datepicker.js"></script>
|
|
@@ -417,21 +418,9 @@
|
|
|
417
418
|
<script src="../dist/freeday-toast.js"></script>
|
|
418
419
|
|
|
419
420
|
<script>
|
|
420
|
-
// The
|
|
421
|
-
//
|
|
422
|
-
//
|
|
423
|
-
const app = document.getElementById('app');
|
|
424
|
-
const toggle = app.querySelector('.fdy-app__navtoggle');
|
|
425
|
-
const wide = () => window.matchMedia('(min-width: 721px)').matches;
|
|
426
|
-
toggle.addEventListener('click', () => {
|
|
427
|
-
const cls = wide() ? 'fdy-app--nav-collapsed' : 'fdy-app--nav-open';
|
|
428
|
-
const open = app.classList.toggle(cls);
|
|
429
|
-
toggle.setAttribute('aria-expanded', String(wide() ? !open : open));
|
|
430
|
-
});
|
|
431
|
-
app.querySelector('.fdy-app__backdrop').addEventListener('click', () => {
|
|
432
|
-
app.classList.remove('fdy-app--nav-open');
|
|
433
|
-
toggle.setAttribute('aria-expanded', 'false');
|
|
434
|
-
});
|
|
421
|
+
// The nav toggle is the kit's now: `data-fdy-app` on the shell is the whole wiring. What used to
|
|
422
|
+
// live here handled the two state classes and nothing else — no Escape, no focus trap, no inert
|
|
423
|
+
// background, no focus restore — which is exactly why the behaviour moved into the kit.
|
|
435
424
|
|
|
436
425
|
// Theme + density live on <html>, set once — never per component.
|
|
437
426
|
const root = document.documentElement;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cahyo-dimas/freeday",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.54.0",
|
|
4
4
|
"description": "Freeday — token-driven, framework-agnostic UI KIT (design source-of-truth).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
"scripts": {
|
|
93
93
|
"build": "node tokens/build.mjs",
|
|
94
94
|
"test": "node --test",
|
|
95
|
-
"test:browser": "node --test browser/vanilla.mjs browser/adapter.mjs browser/layout.mjs browser/theme.mjs browser/state.mjs browser/root-init.mjs browser/upload-states.mjs browser/number.mjs browser/card-stretch.mjs browser/text-override.mjs browser/control-heights.mjs browser/cfl-multi.mjs browser/crowding.mjs browser/over-dialog.mjs browser/chart-scale.mjs browser/overlay-stack.mjs browser/chart-a11y.mjs",
|
|
95
|
+
"test:browser": "node --test --test-concurrency=3 browser/vanilla.mjs browser/adapter.mjs browser/layout.mjs browser/theme.mjs browser/state.mjs browser/root-init.mjs browser/upload-states.mjs browser/number.mjs browser/card-stretch.mjs browser/text-override.mjs browser/control-heights.mjs browser/cfl-multi.mjs browser/crowding.mjs browser/over-dialog.mjs browser/chart-scale.mjs browser/overlay-stack.mjs browser/chart-a11y.mjs browser/app-shell.mjs",
|
|
96
96
|
"prepack": "node tokens/build.mjs",
|
|
97
97
|
"version": "node tokens/build.mjs && git add dist",
|
|
98
98
|
"typecheck:react": "tsc -p adapters/react/tsconfig.json --noEmit"
|