1mod bulk_bar;
9mod combobox;
10mod command_palette;
11mod date_picker;
12mod dismissibles;
13mod dropdowns;
14mod filter_bar;
15mod filters;
16mod form_guards;
17mod hero_lazy;
18mod inline_edit;
19mod kanban;
20mod listbox_engine;
21mod live_fragment;
22mod modals;
23mod nav;
24mod notifications;
25mod numpad;
26mod peek;
27mod scroll_preserve;
28mod selection;
29mod sidebar;
30mod sse;
31mod tabs;
32mod tiles;
33mod toast_undo;
34mod toasts;
35
36use std::sync::LazyLock;
37
38pub static FERRO_RUNTIME_JS: LazyLock<String> = LazyLock::new(|| {
42 let mut s = String::with_capacity(8 * 1024);
43 s.push_str("(function() {\n 'use strict';\n");
44 s.push_str(sse::SOURCE);
45 s.push_str(tabs::SOURCE);
46 s.push_str(toasts::SOURCE);
47 s.push_str(dismissibles::SOURCE);
48 s.push_str(notifications::SOURCE);
49 s.push_str(dropdowns::SOURCE);
50 s.push_str(modals::SOURCE);
51 s.push_str(sidebar::SOURCE);
52 s.push_str(form_guards::SOURCE);
53 s.push_str(tiles::SOURCE);
54 s.push_str(numpad::SOURCE);
55 s.push_str(filters::SOURCE);
56 s.push_str(selection::SOURCE);
57 s.push_str(kanban::SOURCE);
58 s.push_str(scroll_preserve::SOURCE);
59 s.push_str(hero_lazy::SOURCE);
60 s.push_str(live_fragment::SOURCE);
61 s.push_str(nav::SOURCE);
62 s.push_str(listbox_engine::SOURCE);
63 s.push_str(command_palette::SOURCE);
64 s.push_str(combobox::SOURCE);
65 s.push_str(date_picker::SOURCE);
66 s.push_str(inline_edit::SOURCE);
67 s.push_str(filter_bar::SOURCE);
68 s.push_str(bulk_bar::SOURCE);
69 s.push_str(peek::SOURCE);
70 s.push_str(toast_undo::SOURCE);
71 s.push_str(
72 "\n function ferroRuntime() {\n\
73 \x20 // Run each setup in isolation: a throw in one concern (e.g. an\n\
74 \x20 // invalid selector built from a malformed attribute) must not\n\
75 \x20 // abort the remaining setups.\n\
76 \x20 var setups = [\n\
77 \x20 setupScrollPreserve,\n\
78 \x20 setupSSE,\n\
79 \x20 setupTabs,\n\
80 \x20 setupDismissibles,\n\
81 \x20 setupNotifications,\n\
82 \x20 setupDropdowns,\n\
83 \x20 setupKanban,\n\
84 \x20 setupSidebar,\n\
85 \x20 setupFormGuards,\n\
86 \x20 setupTiles,\n\
87 \x20 setupNumpad,\n\
88 \x20 setupFilters,\n\
89 \x20 setupSelection,\n\
90 \x20 setupModals,\n\
91 \x20 setupToasts,\n\
92 \x20 setupLazyHeroes,\n\
93 \x20 setupLiveFragments,\n\
94 \x20 setupNav,\n\
95 \x20 setupProgressHairline,\n\
96 \x20 setupCommandPalette,\n\
97 \x20 setupCombobox,\n\
98 \x20 setupDatePicker,\n\
99 \x20 setupInlineEdit,\n\
100 \x20 setupFilterBar,\n\
101 \x20 setupBulkBar,\n\
102 \x20 setupPeek,\n\
103 \x20 setupUndo\n\
104 \x20 ];\n\
105 \x20 for (var i = 0; i < setups.length; i++) {\n\
106 \x20 try { setups[i](); } catch (err) { /* isolated per concern */ }\n\
107 \x20 }\n\
108 \x20 }\n\
109 \x20 document.addEventListener('DOMContentLoaded', ferroRuntime);\n\
110 })();\n",
111 );
112 s
113});
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn variant_classes_use_semantic_tokens() {
121 assert!(FERRO_RUNTIME_JS.contains("fjui-toast--neutral"));
124 assert!(FERRO_RUNTIME_JS.contains("fjui-toast--success"));
125 assert!(FERRO_RUNTIME_JS.contains("fjui-toast--warning"));
126 assert!(FERRO_RUNTIME_JS.contains("fjui-toast--destructive"));
127 assert!(!FERRO_RUNTIME_JS.contains("bg-blue-500"));
128 assert!(!FERRO_RUNTIME_JS.contains("bg-green-500"));
129 assert!(!FERRO_RUNTIME_JS.contains("bg-yellow-500"));
130 assert!(!FERRO_RUNTIME_JS.contains("bg-red-500"));
131 assert!(!FERRO_RUNTIME_JS.contains("data-toast-variant"));
134 assert!(!FERRO_RUNTIME_JS.contains("duration-300"));
137 assert!(!FERRO_RUNTIME_JS.contains("duration-150"));
138 assert!(FERRO_RUNTIME_JS.contains("duration-base"));
139 assert!(FERRO_RUNTIME_JS.contains("transitionend"));
140 }
141
142 #[test]
143 fn tab_switcher_uses_semantic_tokens() {
144 assert!(FERRO_RUNTIME_JS.contains("border-primary"));
145 assert!(FERRO_RUNTIME_JS.contains("text-primary"));
146 assert!(FERRO_RUNTIME_JS.contains("text-text-muted"));
147 assert!(!FERRO_RUNTIME_JS.contains("border-blue-600"));
148 assert!(!FERRO_RUNTIME_JS.contains("text-blue-600"));
149 assert!(!FERRO_RUNTIME_JS.contains("text-gray-500"));
150 }
151
152 #[test]
153 fn toast_uses_semantic_text_color() {
154 assert!(!FERRO_RUNTIME_JS.contains("text-white"));
157 assert!(FERRO_RUNTIME_JS.contains("fjui-toast"));
159 }
160
161 #[test]
165 fn toast_tone_classes_match_ssr() {
166 for tone_class in [
171 "fjui-toast--neutral",
172 "fjui-toast--success",
173 "fjui-toast--warning",
174 "fjui-toast--destructive",
175 ] {
176 assert!(
177 FERRO_RUNTIME_JS.contains(tone_class),
178 "runtime VARIANT_CLASSES drifted from SSR toast tone classes: missing `{tone_class}`"
179 );
180 }
181 assert!(
183 FERRO_RUNTIME_JS.contains("fjui-toast "),
184 "runtime showToast must emit fjui-toast base class"
185 );
186 }
187
188 #[test]
193 fn test_runtime_contains_popover_dropdown_wiring() {
194 assert!(FERRO_RUNTIME_JS.contains("data-popover-menu"));
195 assert!(FERRO_RUNTIME_JS.contains(":popover-open"));
196 assert!(FERRO_RUNTIME_JS.contains("hidePopover"));
197 assert!(FERRO_RUNTIME_JS.contains("positionUnderTrigger"));
198 assert!(FERRO_RUNTIME_JS.contains("getBoundingClientRect"));
199 }
200
201 #[test]
202 fn test_runtime_contains_modal_wiring() {
203 assert!(FERRO_RUNTIME_JS.contains("setupModals"));
204 assert!(FERRO_RUNTIME_JS.contains("data-modal-open"));
205 assert!(FERRO_RUNTIME_JS.contains("showModal"));
206 assert!(FERRO_RUNTIME_JS.contains("data-modal-close"));
207 }
208
209 #[test]
210 fn test_runtime_contains_toast_from_url() {
211 assert!(FERRO_RUNTIME_JS.contains("initToastFromUrl"));
212 assert!(FERRO_RUNTIME_JS.contains("URLSearchParams"));
213 assert!(FERRO_RUNTIME_JS.contains("history.replaceState"));
214 }
215
216 #[test]
217 fn runtime_contains_init_tab_from_url() {
218 assert!(
219 FERRO_RUNTIME_JS.contains("initTabFromUrl"),
220 "FERRO_RUNTIME_JS must include initTabFromUrl for F3 — URL-driven tab init"
221 );
222 assert!(
223 FERRO_RUNTIME_JS.contains("URLSearchParams"),
224 "FERRO_RUNTIME_JS must use URLSearchParams to parse ?tab= for initTabFromUrl"
225 );
226 }
227
228 #[test]
229 fn bundle_contains_dispatcher() {
230 assert!(FERRO_RUNTIME_JS.contains("function ferroRuntime()"));
231 assert!(FERRO_RUNTIME_JS.contains("DOMContentLoaded"));
232 assert!(FERRO_RUNTIME_JS.contains("ferroRuntime"));
233 }
234
235 #[test]
236 fn bundle_contains_all_setup_functions() {
237 for fn_name in [
238 "setupSSE",
239 "setupTabs",
240 "setupToasts",
241 "setupSidebar",
242 "setupDropdowns",
243 "setupModals",
244 "setupDismissibles",
245 "setupNotifications",
246 "setupFormGuards",
247 "setupTiles",
248 "setupNumpad",
249 "setupFilters",
250 "setupSelection",
251 "setupKanban",
252 "setupScrollPreserve",
253 "setupLazyHeroes",
254 "setupLiveFragments",
255 "setupNav",
256 "setupProgressHairline",
257 "setupCommandPalette",
258 "setupCombobox",
259 "setupDatePicker",
260 "setupInlineEdit",
261 "setupFilterBar",
262 "setupBulkBar",
263 "setupPeek",
264 "setupUndo",
265 ] {
266 assert!(
267 FERRO_RUNTIME_JS.contains(fn_name),
268 "bundle missing {fn_name}"
269 );
270 }
271 assert!(
272 FERRO_RUNTIME_JS.contains("createListboxEngine"),
273 "bundle missing createListboxEngine shared keyboard engine"
274 );
275 }
276
277 #[test]
280 fn runtime_contains_nav_setup() {
281 assert!(
284 FERRO_RUNTIME_JS.contains("setupNav"),
285 "bundle missing setupNav"
286 );
287 assert!(
288 FERRO_RUNTIME_JS.contains("setupProgressHairline"),
289 "bundle missing setupProgressHairline"
290 );
291 for token in [
293 "fjui:navigated",
294 "fjui:before-navigate",
295 "scrollRestoration",
296 "pointerdown",
297 "AbortController",
298 "ferro-json-ui",
299 "fjui-nav-progress",
300 "fjui-sidebar__nav-item--active",
301 ] {
302 assert!(
303 FERRO_RUNTIME_JS.contains(token),
304 "nav runtime bundle missing token: {token}"
305 );
306 }
307 }
308
309 #[test]
310 fn bundle_is_single_iife() {
311 assert!(FERRO_RUNTIME_JS.starts_with("(function() {"));
312 assert!(FERRO_RUNTIME_JS.trim_end().ends_with("})();"));
313 }
314
315 #[test]
319 fn dispatcher_invokes_every_setup() {
320 let js: &str = FERRO_RUNTIME_JS.as_str();
321 let dispatcher_start = js.find("function ferroRuntime()").unwrap();
322 let dispatcher = &js[dispatcher_start..];
323 for name in [
324 "setupSSE",
325 "setupTabs",
326 "setupToasts",
327 "setupSidebar",
328 "setupDropdowns",
329 "setupModals",
330 "setupDismissibles",
331 "setupNotifications",
332 "setupFormGuards",
333 "setupTiles",
334 "setupNumpad",
335 "setupFilters",
336 "setupSelection",
337 "setupKanban",
338 "setupScrollPreserve",
339 "setupLazyHeroes",
340 "setupLiveFragments",
341 "setupNav",
342 "setupProgressHairline",
343 "setupCommandPalette",
344 "setupCombobox",
345 "setupDatePicker",
346 "setupInlineEdit",
347 "setupFilterBar",
348 "setupBulkBar",
349 "setupPeek",
350 "setupUndo",
351 ] {
352 assert!(
353 dispatcher.contains(name),
354 "dispatcher setups array missing {name}"
355 );
356 }
357 assert!(
358 dispatcher.contains("try { setups[i](); } catch"),
359 "dispatcher must invoke setups through the per-concern try/catch"
360 );
361 }
362
363 #[test]
364 fn runtime_contains_lazy_hero_setup() {
365 assert!(FERRO_RUNTIME_JS.contains("setupLazyHeroes"));
366 assert!(FERRO_RUNTIME_JS.contains("data-lazy-hero"));
367 assert!(FERRO_RUNTIME_JS.contains("data-lazy-hero-margin"));
368 assert!(FERRO_RUNTIME_JS.contains("data-lazy-hero-promoted"));
369 assert!(FERRO_RUNTIME_JS.contains("IntersectionObserver"));
370 assert!(FERRO_RUNTIME_JS.contains("preload"));
371 assert!(FERRO_RUNTIME_JS.contains("'auto'"));
374 assert!(FERRO_RUNTIME_JS.contains("unobserve"));
375 }
376
377 #[test]
380 fn runtime_wires_disable_on_submit() {
381 assert!(
382 FERRO_RUNTIME_JS.contains("data-disable-on-submit"),
383 "bundle must contain data-disable-on-submit for the double-submit guard (SC-4)"
384 );
385 }
386
387 #[test]
390 fn runtime_exposes_numpad_and_filter_contract() {
391 assert!(
393 FERRO_RUNTIME_JS.contains("data-numpad-key"),
394 "bundle must contain data-numpad-key attribute selector"
395 );
396 assert!(
397 FERRO_RUNTIME_JS.contains("data-numpad-target"),
398 "bundle must contain data-numpad-target attribute"
399 );
400 assert!(
401 FERRO_RUNTIME_JS.contains("data-numpad-input"),
402 "bundle must contain data-numpad-input attribute"
403 );
404 assert!(
405 FERRO_RUNTIME_JS.contains("data-numpad-display"),
406 "bundle must contain data-numpad-display attribute"
407 );
408 assert!(
410 FERRO_RUNTIME_JS.contains("bubbles: true"),
411 "bundle must dispatch bubbling input event on numpad key tap (D-04)"
412 );
413 assert!(
415 FERRO_RUNTIME_JS.contains("data-filter-tokens"),
416 "bundle must contain data-filter-tokens for token matching"
417 );
418 assert!(
419 FERRO_RUNTIME_JS.contains("data-filter-search"),
420 "bundle must contain data-filter-search for text filtering"
421 );
422 assert!(
423 FERRO_RUNTIME_JS.contains("data-filter-text"),
424 "bundle must contain data-filter-text as the universal tile marker"
425 );
426 }
427}