@arcforgelabs/flow 2026.10.46 → 2026.10.47

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.
@@ -0,0 +1,726 @@
1
+ // Phone rules for every shared component. The source is each card's
2
+ // mobile.json (design-system/components/<Card>/mobile.json). This module is
3
+ // what the browser reads, so a component and its card cannot drift.
4
+ // Regenerate with: node scripts/design-system/mobile-rules.mjs
5
+ // docs/DESIGN_SYSTEM.md, docs/MOBILE_TESTS.md.
6
+
7
+ // The three widths (docs/MOBILE_TESTS.md): phone up to 639px, tablet
8
+ // 640–1023px, desk from 1024px.
9
+ export const WIDTHS = Object.freeze({
10
+ phone: '(max-width: 639px)',
11
+ tabletMin: '(min-width: 640px)',
12
+ tabletMax: '(max-width: 1023px)',
13
+ desk: '(min-width: 1024px)',
14
+ });
15
+
16
+ export const RULES = {
17
+ "bulkBar": {
18
+ "component": "bulkBar",
19
+ "direction": "foot bar",
20
+ "phone": {
21
+ "layout": "foot"
22
+ },
23
+ "tablet": {
24
+ "layout": "foot"
25
+ },
26
+ "desk": {
27
+ "layout": "foot"
28
+ },
29
+ "touch": 44,
30
+ "pageOverflow": "none",
31
+ "screens": [
32
+ "invoices-ticked"
33
+ ],
34
+ "card": "BulkBar"
35
+ },
36
+ "fx-btn": {
37
+ "component": "fx-btn",
38
+ "direction": "44px control",
39
+ "phone": {
40
+ "layout": "control"
41
+ },
42
+ "tablet": {
43
+ "layout": "control"
44
+ },
45
+ "desk": {
46
+ "layout": "control"
47
+ },
48
+ "touch": 44,
49
+ "pageOverflow": "none",
50
+ "screens": [
51
+ "jobs-board",
52
+ "jobs-list",
53
+ "job-panel",
54
+ "invoices-ready",
55
+ "invoices-pending",
56
+ "invoices-approved",
57
+ "invoices-ticked",
58
+ "invoices-filter",
59
+ "invoice",
60
+ "reconcile",
61
+ "price-list",
62
+ "rules",
63
+ "automations",
64
+ "integrations",
65
+ "custom-fields",
66
+ "appearance"
67
+ ],
68
+ "mark": false,
69
+ "card": "Button"
70
+ },
71
+ "columnTable": {
72
+ "component": "columnTable",
73
+ "direction": "two-line row",
74
+ "phone": {
75
+ "layout": "two-line",
76
+ "rowHeight": 56,
77
+ "slots": {
78
+ "title": 2,
79
+ "end": 1,
80
+ "meta": 3
81
+ },
82
+ "columnFlag": "phone",
83
+ "flags": [
84
+ "title",
85
+ "meta",
86
+ "end",
87
+ "hide"
88
+ ]
89
+ },
90
+ "tablet": {
91
+ "layout": "table",
92
+ "sticky": "first"
93
+ },
94
+ "desk": {
95
+ "layout": "table"
96
+ },
97
+ "touch": 44,
98
+ "pageOverflow": "none",
99
+ "screens": [
100
+ "jobs-list",
101
+ "invoices-ready",
102
+ "invoices-pending",
103
+ "invoices-approved",
104
+ "invoices-ticked",
105
+ "invoices-filter",
106
+ "reconcile",
107
+ "price-list"
108
+ ],
109
+ "minRowsAt375": 7,
110
+ "card": "Columns"
111
+ },
112
+ "crumbs": {
113
+ "component": "crumbs",
114
+ "direction": "trail",
115
+ "phone": {
116
+ "layout": "trail"
117
+ },
118
+ "tablet": {
119
+ "layout": "trail"
120
+ },
121
+ "desk": {
122
+ "layout": "trail"
123
+ },
124
+ "touch": 44,
125
+ "pageOverflow": "none",
126
+ "screens": [],
127
+ "card": "Crumbs"
128
+ },
129
+ "dialog": {
130
+ "component": "dialog",
131
+ "direction": "sheet",
132
+ "phone": {
133
+ "layout": "sheet"
134
+ },
135
+ "tablet": {
136
+ "layout": "dialog"
137
+ },
138
+ "desk": {
139
+ "layout": "dialog"
140
+ },
141
+ "touch": 44,
142
+ "pageOverflow": "none",
143
+ "screens": [],
144
+ "card": "Dialog"
145
+ },
146
+ "fieldError": {
147
+ "component": "fieldError",
148
+ "direction": "field",
149
+ "phone": {
150
+ "layout": "field"
151
+ },
152
+ "tablet": {
153
+ "layout": "field"
154
+ },
155
+ "desk": {
156
+ "layout": "field"
157
+ },
158
+ "touch": 44,
159
+ "pageOverflow": "none",
160
+ "screens": [],
161
+ "card": "FieldError"
162
+ },
163
+ "viewBar": {
164
+ "component": "viewBar",
165
+ "direction": "sheet",
166
+ "phone": {
167
+ "layout": "sheet"
168
+ },
169
+ "tablet": {
170
+ "layout": "panel"
171
+ },
172
+ "desk": {
173
+ "layout": "panel"
174
+ },
175
+ "touch": 44,
176
+ "pageOverflow": "none",
177
+ "screens": [
178
+ "jobs-board",
179
+ "jobs-list",
180
+ "job-panel",
181
+ "invoices-ready",
182
+ "invoices-pending",
183
+ "invoices-approved",
184
+ "invoices-ticked",
185
+ "invoices-filter",
186
+ "reconcile"
187
+ ],
188
+ "card": "Filter"
189
+ },
190
+ "findBox": {
191
+ "component": "findBox",
192
+ "direction": "icon",
193
+ "phone": {
194
+ "layout": "icon"
195
+ },
196
+ "tablet": {
197
+ "layout": "icon"
198
+ },
199
+ "desk": {
200
+ "layout": "icon"
201
+ },
202
+ "touch": 44,
203
+ "pageOverflow": "none",
204
+ "screens": [
205
+ "jobs-board",
206
+ "jobs-list",
207
+ "job-panel",
208
+ "invoices-ready",
209
+ "invoices-pending",
210
+ "invoices-approved",
211
+ "invoices-ticked",
212
+ "invoices-filter",
213
+ "reconcile"
214
+ ],
215
+ "card": "FindBox"
216
+ },
217
+ "holdButton": {
218
+ "component": "holdButton",
219
+ "direction": "wait",
220
+ "phone": {
221
+ "layout": "wait"
222
+ },
223
+ "tablet": {
224
+ "layout": "wait"
225
+ },
226
+ "desk": {
227
+ "layout": "wait"
228
+ },
229
+ "touch": 44,
230
+ "pageOverflow": "none",
231
+ "screens": [],
232
+ "mark": false,
233
+ "card": "HoldButton"
234
+ },
235
+ "lucide": {
236
+ "component": "lucide",
237
+ "direction": "icon",
238
+ "phone": {
239
+ "layout": "icon"
240
+ },
241
+ "tablet": {
242
+ "layout": "icon"
243
+ },
244
+ "desk": {
245
+ "layout": "icon"
246
+ },
247
+ "touch": 44,
248
+ "pageOverflow": "none",
249
+ "screens": [],
250
+ "mark": false,
251
+ "card": "Icon"
252
+ },
253
+ "jobChangeBanner": {
254
+ "component": "jobChangeBanner",
255
+ "direction": "banner",
256
+ "phone": {
257
+ "layout": "banner"
258
+ },
259
+ "tablet": {
260
+ "layout": "banner"
261
+ },
262
+ "desk": {
263
+ "layout": "banner"
264
+ },
265
+ "touch": 44,
266
+ "pageOverflow": "none",
267
+ "screens": [],
268
+ "card": "JobChangeBanner"
269
+ },
270
+ "listFrame": {
271
+ "component": "listFrame",
272
+ "direction": "frame",
273
+ "phone": {
274
+ "layout": "page"
275
+ },
276
+ "tablet": {
277
+ "layout": "frame"
278
+ },
279
+ "desk": {
280
+ "layout": "frame"
281
+ },
282
+ "touch": 44,
283
+ "pageOverflow": "none",
284
+ "screens": [
285
+ "jobs-board",
286
+ "jobs-list",
287
+ "job-panel",
288
+ "invoices-ready",
289
+ "invoices-pending",
290
+ "invoices-approved",
291
+ "invoices-ticked",
292
+ "invoices-filter",
293
+ "reconcile"
294
+ ],
295
+ "card": "ListFrame"
296
+ },
297
+ "listHead": {
298
+ "component": "listHead",
299
+ "direction": "one row",
300
+ "phone": {
301
+ "layout": "one-row"
302
+ },
303
+ "tablet": {
304
+ "layout": "toolbar"
305
+ },
306
+ "desk": {
307
+ "layout": "toolbar"
308
+ },
309
+ "touch": 44,
310
+ "pageOverflow": "none",
311
+ "screens": [
312
+ "jobs-board",
313
+ "jobs-list",
314
+ "job-panel",
315
+ "invoices-ready",
316
+ "invoices-pending",
317
+ "invoices-approved",
318
+ "invoices-ticked",
319
+ "invoices-filter",
320
+ "reconcile"
321
+ ],
322
+ "card": "ListHead"
323
+ },
324
+ "listPager": {
325
+ "component": "listPager",
326
+ "direction": "slim foot",
327
+ "phone": {
328
+ "layout": "slim"
329
+ },
330
+ "tablet": {
331
+ "layout": "full"
332
+ },
333
+ "desk": {
334
+ "layout": "full"
335
+ },
336
+ "touch": 44,
337
+ "pageOverflow": "none",
338
+ "screens": [
339
+ "invoices-ready",
340
+ "invoices-pending",
341
+ "invoices-approved",
342
+ "invoices-ticked",
343
+ "invoices-filter"
344
+ ],
345
+ "card": "ListPager"
346
+ },
347
+ "messagePage": {
348
+ "component": "messagePage",
349
+ "direction": "card",
350
+ "phone": {
351
+ "layout": "card"
352
+ },
353
+ "tablet": {
354
+ "layout": "card"
355
+ },
356
+ "desk": {
357
+ "layout": "card"
358
+ },
359
+ "touch": 44,
360
+ "pageOverflow": "none",
361
+ "mark": false,
362
+ "screens": [],
363
+ "card": "MessagePage"
364
+ },
365
+ "notice": {
366
+ "component": "notice",
367
+ "direction": "note",
368
+ "phone": {
369
+ "layout": "note"
370
+ },
371
+ "tablet": {
372
+ "layout": "note"
373
+ },
374
+ "desk": {
375
+ "layout": "note"
376
+ },
377
+ "touch": 44,
378
+ "pageOverflow": "none",
379
+ "screens": [],
380
+ "card": "Notice"
381
+ },
382
+ "optionPill": {
383
+ "component": "optionPill",
384
+ "direction": "pill",
385
+ "phone": {
386
+ "layout": "pill"
387
+ },
388
+ "tablet": {
389
+ "layout": "pill"
390
+ },
391
+ "desk": {
392
+ "layout": "pill"
393
+ },
394
+ "touch": 44,
395
+ "pageOverflow": "none",
396
+ "screens": [],
397
+ "mark": false,
398
+ "card": "OptionPill"
399
+ },
400
+ "pageHeader": {
401
+ "component": "pageHeader",
402
+ "direction": "title",
403
+ "phone": {
404
+ "layout": "title"
405
+ },
406
+ "tablet": {
407
+ "layout": "title"
408
+ },
409
+ "desk": {
410
+ "layout": "title"
411
+ },
412
+ "touch": 44,
413
+ "pageOverflow": "none",
414
+ "screens": [
415
+ "jobs-board",
416
+ "jobs-list",
417
+ "job-panel",
418
+ "invoices-ready",
419
+ "invoices-pending",
420
+ "invoices-approved",
421
+ "invoices-ticked",
422
+ "invoices-filter",
423
+ "invoice",
424
+ "reconcile",
425
+ "price-list",
426
+ "rules",
427
+ "automations",
428
+ "integrations",
429
+ "custom-fields",
430
+ "appearance"
431
+ ],
432
+ "card": "PageHeader"
433
+ },
434
+ "fx-pager": {
435
+ "component": "fx-pager",
436
+ "direction": "record bar",
437
+ "phone": {
438
+ "layout": "bar"
439
+ },
440
+ "tablet": {
441
+ "layout": "bar"
442
+ },
443
+ "desk": {
444
+ "layout": "bar"
445
+ },
446
+ "touch": 44,
447
+ "pageOverflow": "none",
448
+ "screens": [
449
+ "invoice"
450
+ ],
451
+ "mark": false,
452
+ "selector": ".fx-pager",
453
+ "card": "Pager"
454
+ },
455
+ "rowMenu": {
456
+ "component": "rowMenu",
457
+ "direction": "menu",
458
+ "phone": {
459
+ "layout": "menu"
460
+ },
461
+ "tablet": {
462
+ "layout": "menu"
463
+ },
464
+ "desk": {
465
+ "layout": "menu"
466
+ },
467
+ "touch": 44,
468
+ "pageOverflow": "none",
469
+ "screens": [],
470
+ "card": "RowMenu"
471
+ },
472
+ "savedViews": {
473
+ "component": "savedViews",
474
+ "direction": "scrolling tabs",
475
+ "phone": {
476
+ "layout": "scroll"
477
+ },
478
+ "tablet": {
479
+ "layout": "tabs"
480
+ },
481
+ "desk": {
482
+ "layout": "tabs"
483
+ },
484
+ "touch": 44,
485
+ "pageOverflow": "none",
486
+ "screens": [
487
+ "jobs-board",
488
+ "jobs-list",
489
+ "job-panel",
490
+ "invoices-ready",
491
+ "invoices-pending",
492
+ "invoices-approved",
493
+ "invoices-ticked",
494
+ "invoices-filter",
495
+ "reconcile"
496
+ ],
497
+ "card": "SavedViews"
498
+ },
499
+ "settingRow": {
500
+ "component": "settingRow",
501
+ "direction": "sheet",
502
+ "phone": {
503
+ "layout": "sheet"
504
+ },
505
+ "tablet": {
506
+ "layout": "row"
507
+ },
508
+ "desk": {
509
+ "layout": "row"
510
+ },
511
+ "touch": 44,
512
+ "pageOverflow": "none",
513
+ "screens": [
514
+ "appearance"
515
+ ],
516
+ "card": "SettingRow"
517
+ },
518
+ "sidecar": {
519
+ "component": "sidecar",
520
+ "direction": "full page",
521
+ "phone": {
522
+ "layout": "page"
523
+ },
524
+ "tablet": {
525
+ "layout": "panel"
526
+ },
527
+ "desk": {
528
+ "layout": "panel"
529
+ },
530
+ "touch": 44,
531
+ "pageOverflow": "none",
532
+ "screens": [
533
+ "job-panel"
534
+ ],
535
+ "card": "Sidecar"
536
+ },
537
+ "fx-tabs": {
538
+ "component": "fx-tabs",
539
+ "direction": "scrolling tabs",
540
+ "phone": {
541
+ "layout": "scroll"
542
+ },
543
+ "tablet": {
544
+ "layout": "tabs"
545
+ },
546
+ "desk": {
547
+ "layout": "tabs"
548
+ },
549
+ "touch": 44,
550
+ "pageOverflow": "none",
551
+ "screens": [
552
+ "invoices-ready",
553
+ "invoices-pending",
554
+ "invoices-approved",
555
+ "invoices-ticked",
556
+ "invoices-filter",
557
+ "price-list"
558
+ ],
559
+ "mark": false,
560
+ "selector": ".fx-tabs",
561
+ "card": "Tabs"
562
+ },
563
+ "tileCard": {
564
+ "component": "tileCard",
565
+ "direction": "one-line rows",
566
+ "phone": {
567
+ "layout": "one-line"
568
+ },
569
+ "tablet": {
570
+ "layout": "cards"
571
+ },
572
+ "desk": {
573
+ "layout": "cards"
574
+ },
575
+ "touch": 44,
576
+ "pageOverflow": "none",
577
+ "screens": [
578
+ "rules"
579
+ ],
580
+ "card": "Tiles"
581
+ },
582
+ "tileSection": {
583
+ "component": "tileSection",
584
+ "direction": "one-line rows",
585
+ "phone": {
586
+ "layout": "one-line"
587
+ },
588
+ "tablet": {
589
+ "layout": "cards"
590
+ },
591
+ "desk": {
592
+ "layout": "cards"
593
+ },
594
+ "touch": 44,
595
+ "pageOverflow": "none",
596
+ "screens": [
597
+ "rules"
598
+ ],
599
+ "card": "Tiles"
600
+ },
601
+ "named": {
602
+ "component": "named",
603
+ "direction": "tip",
604
+ "phone": {
605
+ "layout": "tip"
606
+ },
607
+ "tablet": {
608
+ "layout": "tip"
609
+ },
610
+ "desk": {
611
+ "layout": "tip"
612
+ },
613
+ "touch": 44,
614
+ "pageOverflow": "none",
615
+ "screens": [],
616
+ "mark": false,
617
+ "card": "Tip"
618
+ },
619
+ "toast": {
620
+ "component": "toast",
621
+ "direction": "toast",
622
+ "phone": {
623
+ "layout": "toast"
624
+ },
625
+ "tablet": {
626
+ "layout": "toast"
627
+ },
628
+ "desk": {
629
+ "layout": "toast"
630
+ },
631
+ "touch": 44,
632
+ "pageOverflow": "none",
633
+ "screens": [],
634
+ "card": "Toast"
635
+ }
636
+ };
637
+
638
+ // The card's rules for one component (columnTable, listHead, sidecar, …).
639
+ export function rule(component) {
640
+ const found = RULES[component];
641
+ if (!found) throw new Error(`No phone rules for ${component}`);
642
+ return found;
643
+ }
644
+
645
+ export function isPhone() {
646
+ return typeof matchMedia === 'function' && matchMedia(WIDTHS.phone).matches;
647
+ }
648
+
649
+ export function isTablet() {
650
+ return typeof matchMedia === 'function' && matchMedia(WIDTHS.tabletMin).matches && matchMedia(WIDTHS.tabletMax).matches;
651
+ }
652
+
653
+ // The layout the card names for the width the page is at now.
654
+ export function layoutFor(component) {
655
+ const found = rule(component);
656
+ if (isPhone()) return found.phone.layout;
657
+ if (isTablet()) return found.tablet.layout;
658
+ return found.desk?.layout ?? found.tablet.layout;
659
+ }
660
+
661
+ // The rules that list this phone-suite screen.
662
+ export function rulesFor(screenId) {
663
+ return Object.values(RULES).filter((found) => found.screens?.includes(screenId));
664
+ }
665
+
666
+ // Mark an element with the layout, touch size and row height from its card,
667
+ // and keep the mark current when the window crosses a width. The phone suite
668
+ // reads these marks.
669
+ export function applyPhoneRule(el, component) {
670
+ const found = rule(component);
671
+ if (!el?.dataset) return el;
672
+ const paint = () => {
673
+ el.dataset.phoneRule = component;
674
+ el.dataset.phoneLayout = layoutFor(component);
675
+ el.dataset.phoneTouch = String(found.touch);
676
+ if (isPhone() && found.phone.rowHeight) el.dataset.phoneRowHeight = String(found.phone.rowHeight);
677
+ else delete el.dataset.phoneRowHeight;
678
+ };
679
+ paint();
680
+ if (typeof matchMedia === 'function' && !el.dataset.phoneWatch) {
681
+ el.dataset.phoneWatch = '1';
682
+ for (const query of [WIDTHS.phone, WIDTHS.tabletMin, WIDTHS.desk]) matchMedia(query).addEventListener?.('change', paint);
683
+ }
684
+ return el;
685
+ }
686
+
687
+ // A list column's phone slot, from the flag the Columns card names
688
+ // (phone: 'title' | 'meta' | 'end' | 'hide'). Anything else is unset.
689
+ export function phoneSlot(column) {
690
+ const found = rule('columnTable');
691
+ const value = column?.[found.phone.columnFlag];
692
+ return found.phone.flags?.includes(value) ? value : null;
693
+ }
694
+
695
+ // Defaults for one list's columns. An explicit phone flag wins, up to the
696
+ // card's slot counts. Otherwise the first column is the title, the money
697
+ // column (kind total, or num) is the end, a stage or status leads the meta
698
+ // line, and further columns are meta in order. Past the slot counts, hide.
699
+ export function assignPhoneSlots(columns) {
700
+ const { slots } = rule('columnTable').phone;
701
+ const counts = { title: 0, end: 0, meta: 0 };
702
+ const room = (slot) => slot === 'hide' || counts[slot] < slots[slot];
703
+ const claim = (slot) => {
704
+ if (slot !== 'hide') counts[slot] += 1;
705
+ return slot;
706
+ };
707
+ const chosen = columns.map((column) => {
708
+ const explicit = phoneSlot(column);
709
+ if (!explicit) return null;
710
+ return room(explicit) ? claim(explicit) : 'hide';
711
+ });
712
+ const open = () => chosen.findIndex((slot) => slot == null);
713
+ const give = (index, slot) => {
714
+ if (index < 0 || !room(slot)) return;
715
+ chosen[index] = claim(slot);
716
+ };
717
+ give(open(), 'title');
718
+ give(columns.findIndex((column, index) => chosen[index] == null && (column.kind === 'total' || column.num)), 'end');
719
+ give(columns.findIndex((column, index) => chosen[index] == null && (column.kind === 'stage' || column.kind === 'status')), 'meta');
720
+ let index = open();
721
+ while (index >= 0) {
722
+ chosen[index] = room('meta') ? claim('meta') : 'hide';
723
+ index = open();
724
+ }
725
+ return columns.map((column, i) => (column.phone === chosen[i] ? column : { ...column, phone: chosen[i] }));
726
+ }