@arcforgelabs/flow 2026.10.48 → 2026.10.50
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/VISION.md +18 -9
- package/npm-shrinkwrap.json +11 -11
- package/package.json +1 -1
- package/server/api/invoicing.js +7 -3
- package/server/core/automations.js +34 -0
- package/server/core/invoice-rules.js +17 -3
- package/server/core/job-board.js +1 -1
- package/server/core/pipeline.js +46 -6
- package/server/core/sending.js +27 -9
- package/server/integrations/port.js +1 -1
- package/server/integrations/seazona/feed.js +41 -4
- package/server/integrations/seazona/sweep.js +11 -8
- package/server/integrations/sentry/index.js +2 -2
- package/server/integrations/xero/client.js +23 -0
- package/server/integrations/xero/connections.js +2 -0
- package/server/integrations/xero/contact-sync.js +289 -0
- package/server/integrations/xero/create-contact.js +42 -2
- package/server/integrations/xero/drafts.js +14 -6
- package/server/integrations/xero/import.js +5 -1
- package/server/integrations/xero/index.js +16 -2
- package/server/integrations/xero/lookup.js +15 -2
- package/server/integrations/xero/migrations/0013_xero_contact_sync.sql +15 -0
- package/server/integrations/xero/migrations/meta/_journal.json +7 -0
- package/server/integrations/xero/oauth.js +10 -3
- package/server/integrations/xero/routes.js +74 -42
- package/server/integrations/xero/runtime.js +7 -4
- package/server/integrations/xero/settings.js +18 -1
- package/server/ui/app-page.js +1 -1
- package/server/ui/app.js +48 -40
- package/server/ui/automations.js +2 -4
- package/server/ui/contacts.js +20 -27
- package/server/ui/flow.css +125 -41
- package/server/ui/invoice-rules.js +3 -5
- package/server/ui/invoicing-settings.js +3 -7
- package/server/ui/invoicing.js +69 -84
- package/server/ui/job-board.js +3 -1
- package/server/ui/jobs.js +1 -1
- package/server/ui/kit.js +101 -10
- package/server/ui/line-mappings.js +2 -4
- package/server/ui/mobile.js +49 -4
- package/server/ui/price-list.js +3 -5
- package/server/ui/routes.js +1 -1
- package/server/ui/xero-contact.js +101 -0
package/server/ui/flow.css
CHANGED
|
@@ -205,13 +205,20 @@ body:is([data-state="off"], [data-state="error"], [data-state="no-access"]) .fx-
|
|
|
205
205
|
|
|
206
206
|
/* ── Notices, pills, tags, toasts ──────────────────────────────────────── */
|
|
207
207
|
.fx-notice {
|
|
208
|
-
display:
|
|
208
|
+
display: block; padding: 12px 14px;
|
|
209
209
|
border: 1px solid var(--t-accent-brd); border-radius: var(--t-radius);
|
|
210
210
|
background: var(--t-accent-bg); color: var(--t-text); font-size: 14px;
|
|
211
211
|
}
|
|
212
212
|
.fx-notice[data-tone="warn"] { border-color: var(--t-warn-brd); background: var(--t-warn-bg); }
|
|
213
213
|
.fx-notice[data-tone="danger"] { border-color: var(--t-danger-brd); background: var(--t-danger-bg); color: var(--t-danger); }
|
|
214
214
|
.fx-panel > .fx-notice { margin: 12px; }
|
|
215
|
+
/* The title leads the text on the same line ("Problem. No Xero contact…"). */
|
|
216
|
+
.fx-notice > strong { margin-right: 6px; }
|
|
217
|
+
.fx-notice__short { display: none; }
|
|
218
|
+
@media (max-width: 639px) {
|
|
219
|
+
.fx-notice__short { display: inline; }
|
|
220
|
+
.fx-notice .fx-notice__long { display: none; }
|
|
221
|
+
}
|
|
215
222
|
|
|
216
223
|
/* The job changed after its invoice was made (job-change.js): was / now per
|
|
217
224
|
changed fact. Was is struck through; Now is the work system's value. */
|
|
@@ -371,24 +378,27 @@ a.fx-link:hover, a.fx-link:focus-visible { border-color: var(--t-strong); }
|
|
|
371
378
|
.fx-logo--lg { width: 52px; height: 52px; }
|
|
372
379
|
.fx-logo--lg img { width: 34px; height: 34px; }
|
|
373
380
|
|
|
374
|
-
.fx-logo--xl { width: 112px; height: 112px; border-radius: 22px; }
|
|
375
|
-
.fx-logo--xl img { width: 68px; height: 68px; }
|
|
376
381
|
.fx-chip--static { display: inline-flex; align-items: center; min-height: 26px; padding: 0 10px; font-size: 12.5px; cursor: default; }
|
|
377
382
|
|
|
378
383
|
/* One integration's page: hero, then the setup beside its facts. */
|
|
379
384
|
.fx-int-page { display: grid; gap: 24px; }
|
|
380
|
-
.fx-int-hero { display: flex;
|
|
381
|
-
.fx-int-
|
|
382
|
-
.fx-int-
|
|
383
|
-
.fx-int-
|
|
384
|
-
.fx-int-hero__blurb { max-width: 64ch; color: var(--t-text); font-size: 15.5px; }
|
|
385
|
-
.fx-int-hero__actions { position: relative; display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-top: 6px; }
|
|
385
|
+
.fx-int-hero { display: flex; align-items: center; gap: 14px; min-width: 0; }
|
|
386
|
+
.fx-int-hero .fx-title { min-width: 0; font-size: 28px; overflow-wrap: anywhere; }
|
|
387
|
+
.fx-int-hero__actions { position: relative; display: flex; align-items: center; gap: 10px; margin-left: auto; }
|
|
388
|
+
.fx-int-hero__actions .fx-progress { left: auto; right: 0; }
|
|
386
389
|
.fx-int-page__grid { display: grid; grid-template-columns: minmax(0, 1fr) 240px; gap: 32px; align-items: start; }
|
|
387
390
|
.fx-int-page__main { display: grid; gap: 28px; }
|
|
388
391
|
.fx-int-page__main > .fx-alerts { margin: 0; }
|
|
389
392
|
.fx-int-section { display: grid; gap: 14px; padding-top: 20px; border-top: 1px solid var(--fx-line); }
|
|
390
393
|
.fx-int-section__title { color: var(--t-strong); font: 650 17px/1.3 var(--t-body); }
|
|
391
394
|
.fx-int-section .fx-btn { justify-self: start; }
|
|
395
|
+
/* A folded section (Set up, Settings): a person opens it. Its chevron
|
|
396
|
+
points right when closed and down when open, as the table folds do. */
|
|
397
|
+
.fx-int-fold > summary { display: flex; align-items: center; gap: 8px; min-height: 44px; margin: -10px 0; cursor: pointer; list-style: none; }
|
|
398
|
+
.fx-int-fold > summary::-webkit-details-marker { display: none; }
|
|
399
|
+
.fx-int-fold > summary::after { content: ''; width: 8px; height: 8px; margin-left: 2px; border: solid currentColor; border-width: 0 2px 2px 0; transform: rotate(-45deg); transition: transform 120ms var(--fx-ease); }
|
|
400
|
+
.fx-int-fold[open] > summary::after { transform: rotate(45deg) translateY(-2px); }
|
|
401
|
+
.fx-int-fold:not([open]) { gap: 0; }
|
|
392
402
|
.fx-steps { display: grid; gap: 10px; margin: 0; padding: 0; list-style: none; counter-reset: step; }
|
|
393
403
|
.fx-steps li { position: relative; padding-left: 38px; min-height: 26px; counter-increment: step; font-size: 14.5px; line-height: 1.55; }
|
|
394
404
|
.fx-steps li::before {
|
|
@@ -419,8 +429,6 @@ a.fx-link:hover, a.fx-link:focus-visible { border-color: var(--t-strong); }
|
|
|
419
429
|
@media (max-width: 900px) {
|
|
420
430
|
.fx-int-page__grid { grid-template-columns: minmax(0, 1fr); }
|
|
421
431
|
.fx-int-page__meta { position: static; }
|
|
422
|
-
.fx-logo--xl { width: 72px; height: 72px; border-radius: 16px; }
|
|
423
|
-
.fx-logo--xl img { width: 44px; height: 44px; }
|
|
424
432
|
}
|
|
425
433
|
.fx-drawer .fx-select { width: 100%; }
|
|
426
434
|
|
|
@@ -432,7 +440,12 @@ a.fx-strong:hover { text-decoration: underline; }
|
|
|
432
440
|
.fx-toolbar { display: flex; flex-wrap: wrap; align-items: end; justify-content: space-between; gap: 12px; border-bottom: 1px solid var(--fx-line); }
|
|
433
441
|
.fx-tabs { display: flex; gap: 2px; }
|
|
434
442
|
.fx-tabs button { flex: none; }
|
|
435
|
-
|
|
443
|
+
/* Phones and tablets: the tabs scroll in one row and each is 44px tall, so
|
|
444
|
+
seven tabs never run past the page and a thumb hits each one. */
|
|
445
|
+
@media (max-width: 1023px) {
|
|
446
|
+
.fx-tabs { max-width: 100%; overflow-x: auto; scrollbar-width: none; }
|
|
447
|
+
.fx-tabs button { min-height: 44px; }
|
|
448
|
+
}
|
|
436
449
|
.fx-tabs button {
|
|
437
450
|
display: inline-flex; align-items: center; gap: 8px; padding: 10px 14px; margin-bottom: -1px;
|
|
438
451
|
border: 0; border-bottom: 2px solid transparent; background: transparent; cursor: pointer;
|
|
@@ -503,7 +516,7 @@ a.fx-strong:hover { text-decoration: underline; }
|
|
|
503
516
|
.fx-crumbs { display: flex; gap: 8px; color: var(--t-dim); font-size: 13.5px; }
|
|
504
517
|
.fx-crumbs a { color: var(--t-dim); text-decoration: none; }
|
|
505
518
|
.fx-crumbs a:hover { color: var(--t-strong); text-decoration: underline; }
|
|
506
|
-
.fx-invoice { display: grid; gap: 20px; }
|
|
519
|
+
.fx-invoice { display: grid; grid-template-columns: minmax(0, 1fr); gap: 20px; }
|
|
507
520
|
.fx-invoice__grid { display: grid; grid-template-columns: minmax(0, 1fr) 260px; gap: 20px; align-items: start; }
|
|
508
521
|
.fx-invoice__main, .fx-invoice__aside { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; min-width: 0; }
|
|
509
522
|
.fx-invoice__aside:empty { display: none; }
|
|
@@ -558,6 +571,29 @@ a.fx-strong:hover { text-decoration: underline; }
|
|
|
558
571
|
.fx-lines { overflow-x: auto; }
|
|
559
572
|
.fx-lines__table { min-width: 680px; }
|
|
560
573
|
}
|
|
574
|
+
/* CardRows (cardRows in kit.js): on a phone a table is a list of small
|
|
575
|
+
cards, never a sideways scroll. Each cell sits where the table's areas
|
|
576
|
+
place it, under its column name. */
|
|
577
|
+
@media (max-width: 639px) {
|
|
578
|
+
.fx-lines { overflow-x: visible; }
|
|
579
|
+
.fx-cardrows, .fx-cardrows tbody { display: block; width: 100%; min-width: 0; }
|
|
580
|
+
.fx-cardrows thead { display: none; }
|
|
581
|
+
.fx-cardrows tr {
|
|
582
|
+
display: grid; grid-template-columns: var(--fx-cardrows-columns); grid-template-areas: var(--fx-cardrows-areas);
|
|
583
|
+
gap: 10px 8px; padding: 14px 16px; border-bottom: 1px solid var(--fx-line-soft);
|
|
584
|
+
}
|
|
585
|
+
.fx-cardrows td,
|
|
586
|
+
.fx-cardrows td:first-child,
|
|
587
|
+
.fx-cardrows td:last-child,
|
|
588
|
+
.fx-lines__table--read td { display: grid; align-content: end; gap: 4px; width: auto; min-width: 0; padding: 0; border: 0; text-align: left; }
|
|
589
|
+
.fx-cardrows td[data-label]::before {
|
|
590
|
+
content: attr(data-label); color: var(--t-dim);
|
|
591
|
+
font: var(--t-label-weight) var(--t-label-size)/1.3 var(--t-body); letter-spacing: var(--t-label-spacing); text-transform: var(--t-label-case);
|
|
592
|
+
}
|
|
593
|
+
.fx-cardrows td[data-end] { justify-items: end; text-align: right; }
|
|
594
|
+
.fx-cardrows :is(.fx-input, .fx-select, .fx-btn) { min-height: 44px; }
|
|
595
|
+
.fx-cardrows .fx-btn--icon { width: 44px; }
|
|
596
|
+
}
|
|
561
597
|
|
|
562
598
|
/* ── Jobs board ─────────────────────────────────────────────────────────── */
|
|
563
599
|
.fx-main > * { min-width: 0; }
|
|
@@ -1086,6 +1122,9 @@ th:hover > .fx-th-grip::after { background: var(--t-brd-hi); }
|
|
|
1086
1122
|
}
|
|
1087
1123
|
.fx-record .fx-ph { flex: 1; min-width: 0; min-height: 0; gap: 8px; background: transparent; }
|
|
1088
1124
|
.fx-record .fx-ph__text { min-width: 0; }
|
|
1125
|
+
/* A short number (FL-0001) shows whole; the status pill is compact beside it. */
|
|
1126
|
+
.fx-record .fx-ph__text .fx-status { flex: none; padding-inline: 8px; font-size: 12px; }
|
|
1127
|
+
.fx-record .fx-ph__text .fx-status::before { display: none; }
|
|
1089
1128
|
.fx-record__end { display: flex; flex: none; align-items: center; }
|
|
1090
1129
|
.fx-record__foot:not([hidden]) {
|
|
1091
1130
|
display: flex; position: fixed; left: 0; right: 0; bottom: 0; z-index: 5; gap: 8px;
|
|
@@ -1204,18 +1243,25 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
|
|
|
1204
1243
|
.fx-setting + .fx-setting { border-top: 1px solid var(--fx-line-soft); }
|
|
1205
1244
|
.fx-setting__text { display: grid; gap: 3px; min-width: 0; }
|
|
1206
1245
|
.fx-setting__name { color: var(--t-strong); font: 500 14.5px/1.35 var(--t-body); }
|
|
1246
|
+
.fx-setting__head { display: flex; align-items: center; gap: 2px; min-width: 0; }
|
|
1247
|
+
.fx-setting__head .fx-help { margin: -4px 0; border: 0; background: none; cursor: pointer; }
|
|
1207
1248
|
.fx-setting__hint { margin: 0; color: var(--t-dim); font-size: 13px; line-height: 1.45; }
|
|
1208
1249
|
.fx-setting[data-tone="danger"] .fx-setting__hint { color: var(--t-danger); }
|
|
1209
|
-
.fx-setting__control { display: flex; justify-content: flex-end; min-width: 0; }
|
|
1250
|
+
.fx-setting__control { display: flex; align-items: center; justify-content: flex-end; gap: 10px; min-width: 0; }
|
|
1210
1251
|
.fx-setting__control > .fx-input, .fx-setting__control > .fx-select, .fx-setting__stack { width: 320px; max-width: 100%; }
|
|
1252
|
+
.fx-setting__control > .fx-input[type="number"] { width: 96px; }
|
|
1211
1253
|
.fx-setting__control .fx-swatches { flex-wrap: nowrap; }
|
|
1212
1254
|
.fx-setting__stack { display: grid; gap: 6px; justify-items: end; }
|
|
1213
1255
|
.fx-setting__value { font-size: 12.5px; color: var(--t-strong); overflow-wrap: anywhere; text-align: right; }
|
|
1214
1256
|
.fx-setting--wide { grid-template-columns: minmax(0, 1fr); }
|
|
1215
1257
|
.fx-setting--wide .fx-setting__control { display: block; }
|
|
1216
1258
|
@media (max-width: 639px) {
|
|
1217
|
-
|
|
1218
|
-
|
|
1259
|
+
/* A wide control (a box, a menu, a choice) goes under its name. Switches
|
|
1260
|
+
and buttons stay at the right. */
|
|
1261
|
+
.fx-setting:has(> .fx-setting__control :is(.fx-select, .fx-multi, .fx-input:not([type="number"]), textarea, .fx-seg, .fx-swatches, .fx-setting__stack)),
|
|
1262
|
+
.fx-setting--wide { grid-template-columns: minmax(0, 1fr); }
|
|
1263
|
+
.fx-setting:has(> .fx-setting__control :is(.fx-select, .fx-multi, .fx-input:not([type="number"]), textarea, .fx-seg, .fx-swatches, .fx-setting__stack)) > .fx-setting__control { justify-content: flex-start; }
|
|
1264
|
+
.fx-setting__control > :is(.fx-select, .fx-multi, .fx-input:not([type="number"])) { width: 100%; }
|
|
1219
1265
|
.fx-setting__stack { justify-items: start; }
|
|
1220
1266
|
.fx-setting__control .fx-swatches { flex-wrap: wrap; }
|
|
1221
1267
|
}
|
|
@@ -1302,6 +1348,13 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
|
|
|
1302
1348
|
|
|
1303
1349
|
/* ── Phones ─────────────────────────────────────────────────────────────── */
|
|
1304
1350
|
@media (max-width: 639px) {
|
|
1351
|
+
/* Touch: every control a thumb uses is 44px (docs/MOBILE_TESTS.md). The
|
|
1352
|
+
icons and text keep their size; the box around them grows. */
|
|
1353
|
+
#menu-toggle, #search { width: 44px; height: 44px; }
|
|
1354
|
+
/* The notice's buttons grow to 44px into its padding, so the notice
|
|
1355
|
+
stays one short line. */
|
|
1356
|
+
.fx-alerts__close { width: 44px; height: 44px; margin: -12px -8px; }
|
|
1357
|
+
.fx-alerts__action { display: inline-flex; align-items: center; min-height: 44px; margin-block: -12px; }
|
|
1305
1358
|
.fx-main { padding-top: 20px; }
|
|
1306
1359
|
/* A two-line list needs the rows, not the gaps, so seven fit at 375×667.
|
|
1307
1360
|
The alert stays one line. The toolbar stays one scrolling row. */
|
|
@@ -1355,7 +1408,7 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
|
|
|
1355
1408
|
[data-phone-layout="two-line"] .fx-table tbody tr:not(.fx-table__group) {
|
|
1356
1409
|
display: flex; flex-wrap: wrap; align-items: center; position: relative;
|
|
1357
1410
|
box-sizing: border-box; height: 56px; min-height: 56px; max-height: 56px;
|
|
1358
|
-
margin: 0; padding: 6px 12px 6px 44px; overflow: hidden;
|
|
1411
|
+
margin: 0; padding: 6px 12px 6px 44px; overflow: hidden; column-gap: 8px;
|
|
1359
1412
|
border-bottom: 1px solid var(--fx-line-soft);
|
|
1360
1413
|
}
|
|
1361
1414
|
[data-phone-layout="two-line"] .fx-table tbody tr:not(.fx-table__group)::after {
|
|
@@ -1367,29 +1420,47 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
|
|
|
1367
1420
|
display: block; width: auto; margin: 0; padding: 0; border: 0;
|
|
1368
1421
|
white-space: nowrap; background: none;
|
|
1369
1422
|
}
|
|
1370
|
-
[data-phone-layout="two-line"] .fx-tick-cell {
|
|
1423
|
+
[data-phone-layout="two-line"] .fx-table td.fx-tick-cell {
|
|
1371
1424
|
position: absolute; top: 0; left: 0; display: flex; align-items: center; justify-content: center;
|
|
1372
1425
|
width: 44px; height: 56px; padding: 0;
|
|
1373
1426
|
}
|
|
1374
|
-
|
|
1427
|
+
/* The tick keeps a 44px hit area, but shows an 18px box, as on a desk. */
|
|
1428
|
+
[data-phone-layout="two-line"] .fx-tick-cell .fx-tick {
|
|
1429
|
+
appearance: none; -webkit-appearance: none; display: grid; place-items: center;
|
|
1430
|
+
width: 44px; height: 44px; margin: 0; border: 0; background: none;
|
|
1431
|
+
}
|
|
1432
|
+
[data-phone-layout="two-line"] .fx-tick::before,
|
|
1433
|
+
[data-phone-layout="two-line"] .fx-tick::after { content: ""; grid-area: 1 / 1; box-sizing: border-box; }
|
|
1434
|
+
[data-phone-layout="two-line"] .fx-tick::before {
|
|
1435
|
+
width: 18px; height: 18px; border: 1.5px solid var(--t-brd-hi); border-radius: calc(var(--t-radius) / 2); background: var(--t-paper);
|
|
1436
|
+
}
|
|
1437
|
+
[data-phone-layout="two-line"] .fx-tick:is(:checked, :indeterminate)::before { border-color: var(--t-brand); background: var(--t-brand); }
|
|
1438
|
+
[data-phone-layout="two-line"] .fx-tick:checked::after {
|
|
1439
|
+
width: 10px; height: 6px; margin-top: -2px; border: solid var(--t-on-primary); border-width: 0 0 2px 2px; transform: rotate(-45deg);
|
|
1440
|
+
}
|
|
1441
|
+
[data-phone-layout="two-line"] .fx-tick:indeterminate::after { width: 10px; height: 2px; background: var(--t-on-primary); }
|
|
1375
1442
|
[data-phone-layout="two-line"] [data-phone-slot="title"] {
|
|
1376
1443
|
order: 1; flex: 1 1 0; min-width: 0; overflow: hidden; text-overflow: ellipsis;
|
|
1377
1444
|
color: var(--t-strong); font-weight: 600;
|
|
1378
1445
|
}
|
|
1446
|
+
/* The title link keeps a 44px hit area but takes 24px of the line, so
|
|
1447
|
+
line 2 still fits in the 56px row. */
|
|
1379
1448
|
[data-phone-layout="two-line"] [data-phone-slot="title"] :is(a, button) {
|
|
1380
|
-
display: inline-flex; align-items: center; max-width: 100%; min-height: 44px;
|
|
1449
|
+
display: inline-flex; align-items: center; max-width: 100%; min-height: 44px; margin-block: -10px;
|
|
1381
1450
|
}
|
|
1382
1451
|
[data-phone-layout="two-line"] [data-phone-slot="end"] {
|
|
1383
1452
|
order: 2; flex: none; margin-left: 8px; font-variant-numeric: tabular-nums;
|
|
1384
1453
|
}
|
|
1385
1454
|
[data-phone-layout="two-line"] [data-phone-slot="meta"] {
|
|
1386
1455
|
order: 5; flex: 0 1 auto; min-width: 0; max-width: 42%; overflow: hidden; text-overflow: ellipsis;
|
|
1387
|
-
color: var(--t-dim); font-size: 12px; line-height: 16px;
|
|
1456
|
+
color: var(--t-dim); font-size: 12px; line-height: 16px; height: 20px;
|
|
1388
1457
|
}
|
|
1458
|
+
/* Line 2 is 20px; its cells centre on it. */
|
|
1459
|
+
[data-phone-layout="two-line"] .fx-table td[data-phone-slot="meta"] { display: flex; align-items: center; overflow: visible; }
|
|
1389
1460
|
[data-phone-layout="two-line"] td.fx-c-stage,
|
|
1390
1461
|
[data-phone-layout="two-line"] td.fx-c-status { order: 4; color: inherit; }
|
|
1391
1462
|
[data-phone-layout="two-line"] .fx-stage { padding-block: 0; line-height: 16px; }
|
|
1392
|
-
[data-phone-layout="two-line"] [data-phone-slot="hide"],
|
|
1463
|
+
[data-phone-layout="two-line"] .fx-table td[data-phone-slot="hide"],
|
|
1393
1464
|
[data-phone-layout="two-line"] td.fx-c-end { display: none; }
|
|
1394
1465
|
[data-phone-layout="two-line"] tr.fx-table__group {
|
|
1395
1466
|
display: flex; align-items: center; box-sizing: border-box; height: 32px; min-height: 32px; max-height: 32px;
|
|
@@ -1397,7 +1468,7 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
|
|
|
1397
1468
|
border-bottom: 1px solid var(--fx-line-soft);
|
|
1398
1469
|
}
|
|
1399
1470
|
[data-phone-layout="two-line"] tr.fx-table__group td { height: 32px; padding: 0; background: none; }
|
|
1400
|
-
[data-phone-layout="two-line"] tr.fx-table__group .fx-tick-cell { height: 32px; }
|
|
1471
|
+
[data-phone-layout="two-line"] .fx-table tr.fx-table__group td.fx-tick-cell { height: 32px; }
|
|
1401
1472
|
[data-phone-layout="two-line"] tr.fx-table__group .fx-tick-cell .fx-tick { width: 44px; height: 44px; }
|
|
1402
1473
|
/* Tiles: every card is one 56px row, as a list row: the mark, the name,
|
|
1403
1474
|
the status and ›. The blurb and the button stay on the page it opens. */
|
|
@@ -1433,15 +1504,7 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
|
|
|
1433
1504
|
control on the right. */
|
|
1434
1505
|
.fx-set { display: grid; gap: 4px; }
|
|
1435
1506
|
.fx-set__title { margin: 8px 0 4px; color: var(--t-strong); font: 600 15px/1.3 var(--t-body); }
|
|
1436
|
-
.fx-set__row { display: flex; align-items: center; justify-content: space-between; gap: 24px; padding: 12px 0; }
|
|
1437
|
-
.fx-set__row + .fx-set__row { border-top: 1px solid var(--fx-line); }
|
|
1438
|
-
.fx-set__text { display: grid; gap: 2px; min-width: 0; }
|
|
1439
|
-
.fx-set__name { color: var(--t-text); font: 500 14px/1.35 var(--t-body); }
|
|
1440
1507
|
.fx-set__hint { color: var(--t-dim); font-size: 13px; line-height: 1.35; }
|
|
1441
|
-
.fx-set__control { flex: none; display: flex; justify-content: flex-end; }
|
|
1442
|
-
.fx-set__control .fx-input { width: 220px; }
|
|
1443
|
-
.fx-set__control .fx-input[type="number"] { width: 96px; }
|
|
1444
|
-
.fx-set__control .fx-select { min-width: 220px; }
|
|
1445
1508
|
.fx-set + .fx-set { margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--fx-line); }
|
|
1446
1509
|
/* Price list (price-list.js): one plain table of Flow's codes. */
|
|
1447
1510
|
.fx-price { display: flex; flex-direction: column; gap: 12px; min-width: 0; }
|
|
@@ -1633,7 +1696,7 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
|
|
|
1633
1696
|
menu on line 1, up to three facts on line 2, 56px. */
|
|
1634
1697
|
.fx-cfm__row {
|
|
1635
1698
|
display: flex; flex-wrap: wrap; align-items: center; box-sizing: border-box;
|
|
1636
|
-
height: 56px; min-height: 56px; max-height: 56px; padding:
|
|
1699
|
+
height: 56px; min-height: 56px; max-height: 56px; padding: 4px 8px 4px 16px; overflow: hidden; column-gap: 8px;
|
|
1637
1700
|
}
|
|
1638
1701
|
.fx-cfm__row::after { content: ""; order: 3; flex-basis: 100%; height: 0; }
|
|
1639
1702
|
.fx-cfm__row > * { display: none; }
|
|
@@ -1643,10 +1706,11 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
|
|
|
1643
1706
|
display: block; order: 5; flex: 0 1 auto; min-width: 0; max-width: 40%; overflow: hidden;
|
|
1644
1707
|
text-overflow: ellipsis; white-space: nowrap; color: var(--t-dim); font-size: 12px; line-height: 16px;
|
|
1645
1708
|
}
|
|
1709
|
+
/* Line 2 is 20px: the type picker keeps its size, but takes no more. */
|
|
1710
|
+
.fx-cfm__row > .fx-cfm__type { display: flex; align-items: center; height: 20px; overflow: visible; }
|
|
1646
1711
|
}
|
|
1647
1712
|
/* Invoice rules (invoice-rules.js): a row's Edit button and switch, and the
|
|
1648
1713
|
rule dialog's condition lines. */
|
|
1649
|
-
.fx-rule__controls { align-items: center; gap: 10px; }
|
|
1650
1714
|
.fx-rule__footer { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 8px; }
|
|
1651
1715
|
.fx-dialog:has(.fx-rule__dialog) { width: min(680px, calc(100vw - 32px)); }
|
|
1652
1716
|
.fx-rule__dialog .fx-btn--sm { justify-self: start; }
|
|
@@ -1660,11 +1724,6 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
|
|
|
1660
1724
|
@media (max-width: 639px) {
|
|
1661
1725
|
.fx-rule__condition { grid-template-columns: minmax(0, 1fr); }
|
|
1662
1726
|
}
|
|
1663
|
-
@media (max-width: 639px) {
|
|
1664
|
-
.fx-set__row { gap: 12px; }
|
|
1665
|
-
.fx-set__control .fx-input { width: 140px; }
|
|
1666
|
-
.fx-set__control .fx-input[type="number"] { width: 72px; }
|
|
1667
|
-
.fx-set__control .fx-select { min-width: 140px; }
|
|
1668
1727
|
}
|
|
1669
1728
|
.fx-multi { position: relative; width: 220px; max-width: 100%; }
|
|
1670
1729
|
.fx-multi__summary { display: flex; align-items: center; list-style: none; cursor: pointer; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
@@ -1685,6 +1744,19 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
|
|
|
1685
1744
|
.fx-switch[aria-checked="true"]::after { transform: translateX(18px); }
|
|
1686
1745
|
.fx-switch:focus-visible { outline: none; box-shadow: var(--fx-focus); }
|
|
1687
1746
|
.fx-switch:disabled { cursor: default; opacity: 0.6; }
|
|
1747
|
+
/* On a touch screen the switch keeps its look and gets a 44px target: a
|
|
1748
|
+
clear border round it that only the finger sees. */
|
|
1749
|
+
@media (max-width: 1023px) {
|
|
1750
|
+
.fx-switch { box-sizing: content-box; border: 11px solid transparent; background-clip: padding-box; margin: -11px; }
|
|
1751
|
+
/* A section title's help link: a 44px target that does not grow the title. */
|
|
1752
|
+
.fx-set__title .fx-help { min-width: 44px; min-height: 44px; justify-content: center; align-items: center; margin-block: -12px; }
|
|
1753
|
+
/* A setting's ? gets the same 44px target. */
|
|
1754
|
+
.fx-setting__head .fx-help { min-width: 44px; min-height: 44px; justify-content: center; align-items: center; margin-block: -12px; }
|
|
1755
|
+
/* An integration page's buttons, boxes and menus, and every crumb link. */
|
|
1756
|
+
.fx-int-page :is(.fx-btn, .fx-input, .fx-select, .fx-multi__summary, .fx-multi__option, .fx-meta a) { min-height: 44px; }
|
|
1757
|
+
.fx-int-page .fx-meta a { display: inline-flex; align-items: center; margin-block: -10px; }
|
|
1758
|
+
.fx-crumbs a { display: inline-flex; align-items: center; min-height: 44px; margin-block: -12px; }
|
|
1759
|
+
}
|
|
1688
1760
|
|
|
1689
1761
|
/* ── Sentry's report form (monitoring.js) ──────────────────────────────────
|
|
1690
1762
|
The form lives in a shadow root and reads these variables from its host.
|
|
@@ -1734,7 +1806,7 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
|
|
|
1734
1806
|
.fx-dialog:has(.fx-pick__body) { width: min(620px, calc(100vw - 32px)); }
|
|
1735
1807
|
.fx-pick__body { display: grid; gap: 12px; }
|
|
1736
1808
|
.fx-pick { display: grid; max-height: min(420px, 55vh); overflow-y: auto; border: 1px solid var(--fx-line); border-radius: var(--t-radius); }
|
|
1737
|
-
.fx-pick__row { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 12px; padding: 10px 14px; border-bottom: 1px solid var(--fx-line); cursor: pointer; }
|
|
1809
|
+
.fx-pick__row { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 12px; min-height: 44px; padding: 10px 14px; border-bottom: 1px solid var(--fx-line); cursor: pointer; }
|
|
1738
1810
|
.fx-pick__row:last-child { border-bottom: 0; }
|
|
1739
1811
|
.fx-pick__row:hover, .fx-pick__row:has(input:checked) { background: var(--fx-hover); }
|
|
1740
1812
|
.fx-pick__row[hidden] { display: none; }
|
|
@@ -1768,7 +1840,13 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
|
|
|
1768
1840
|
.fx-listhead > .fx-toolbar:not(.fx-toolbar--plain) > :not(.fx-tabs) { margin-bottom: 8px; }
|
|
1769
1841
|
@media (max-width: 820px) { .fx-listhead[data-sticky] { top: 60px; } }
|
|
1770
1842
|
/* On a phone the head would hide too many rows: it scrolls away. */
|
|
1771
|
-
@media (max-width: 639px) {
|
|
1843
|
+
@media (max-width: 639px) {
|
|
1844
|
+
/* The head does not stick on a phone, so it needs no top padding. It
|
|
1845
|
+
takes back the gap of the edge marker, and never covers the notice. */
|
|
1846
|
+
.fx-listhead[data-sticky] { position: static; margin-top: 0; padding-top: 0; }
|
|
1847
|
+
.fx-listhead__edge + .fx-listhead[data-sticky] { margin-top: -8px; }
|
|
1848
|
+
.fx-alerts { margin-bottom: 4px; }
|
|
1849
|
+
}
|
|
1772
1850
|
|
|
1773
1851
|
/* The bulk action bar (bulk-bar.js): floats at the foot while rows are
|
|
1774
1852
|
ticked. "N selected ×", then icon actions in groups, then More. It takes
|
|
@@ -1932,7 +2010,7 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
|
|
|
1932
2010
|
.fx-xu__fold { padding: 12px; }
|
|
1933
2011
|
}
|
|
1934
2012
|
|
|
1935
|
-
/* Contacts (server/ui/contacts.js) and the pending-invoice Create offer.
|
|
2013
|
+
/* Contacts (server/ui/contacts.js) and the pending-invoice Link and Create offer.
|
|
1936
2014
|
Phone targets stay on this page only, so other screenshots stay valid. */
|
|
1937
2015
|
.fx-offer { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
|
|
1938
2016
|
.fx-offer > .fx-notice { flex: 1 1 220px; }
|
|
@@ -1946,6 +2024,12 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
|
|
|
1946
2024
|
.fx-contacts .fx-person { color: inherit; text-decoration: none; }
|
|
1947
2025
|
@media (max-width: 639px) {
|
|
1948
2026
|
.fx-contacts__grid { grid-template-columns: minmax(0, 1fr); }
|
|
2027
|
+
/* The note on its own line, then Link and Create side by side. */
|
|
2028
|
+
.fx-offer > .fx-notice { flex-basis: 100%; }
|
|
2029
|
+
.fx-offer > .fx-btn { flex: 1 1 0; }
|
|
2030
|
+
/* A short note fits on one row with Link and Create. */
|
|
2031
|
+
.fx-offer > .fx-notice:has(.fx-notice__short) { display: flex; flex: 1 1 auto; align-self: stretch; align-items: center; }
|
|
2032
|
+
.fx-offer:has(.fx-notice__short) > .fx-btn { flex: 0 1 auto; }
|
|
1949
2033
|
.fx-offer .fx-btn,
|
|
1950
2034
|
.fx-contacts :is(.fx-btn, .fx-input, .fx-select, .fx-check) { min-height: 44px; }
|
|
1951
2035
|
.fx-contacts .fx-help { min-width: 44px; min-height: 44px; }
|
|
@@ -14,7 +14,7 @@ const TEST_LABELS = {
|
|
|
14
14
|
is_not_one_of: 'is not one of', contains: 'contains', does_not_contain: 'does not contain',
|
|
15
15
|
starts_with: 'starts with', does_not_start_with: 'does not start with', has_number: 'has a number', number_in_words: 'has a number beside words', one_word: 'is one word',
|
|
16
16
|
};
|
|
17
|
-
const ACTION_LABELS = { wait: 'Wait for a person', skip: 'Never draft', pending: 'Pending, with a note' };
|
|
17
|
+
const ACTION_LABELS = { wait: 'Wait for a person', skip: 'Never draft', pending: 'Pending, with a note', non_chargeable: 'Non-chargeable' };
|
|
18
18
|
const NO_VALUE = ['is_empty', 'is_not_empty', 'has_number', 'number_in_words', 'one_word'];
|
|
19
19
|
const MANY = ['is_one_of', 'is_not_one_of'];
|
|
20
20
|
const TYPED = ['contains', 'does_not_contain', 'starts_with', 'does_not_start_with'];
|
|
@@ -42,15 +42,13 @@ export function ruleSummary(rule, meta) {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
export function rulesSection(ui, title = 'Rules') {
|
|
45
|
-
const { h, flow, toast, notice, dialog, me } = ui;
|
|
45
|
+
const { h, flow, toast, notice, dialog, me, settingRow } = ui;
|
|
46
46
|
const disabled = !['owner', 'admin'].includes(me?.person.role);
|
|
47
47
|
const section = h('section', { class: 'fx-set' }, h('h2', { class: 'fx-set__title' }, title), h('div', { class: 'fx-skeleton' }));
|
|
48
48
|
let meta;
|
|
49
49
|
let rules = [];
|
|
50
50
|
|
|
51
|
-
const row = (
|
|
52
|
-
h('div', { class: 'fx-set__text' }, h('span', { class: 'fx-set__name' }, name), h('span', { class: 'fx-set__hint' }, hint)),
|
|
53
|
-
h('div', { class: 'fx-set__control fx-rule__controls' }, control));
|
|
51
|
+
const row = (label, hint, control) => settingRow({ label, hint, control });
|
|
54
52
|
|
|
55
53
|
async function send(path, method, payload) {
|
|
56
54
|
try {
|
|
@@ -83,7 +83,7 @@ export function renderInvoicingSettings(main, ui, id) {
|
|
|
83
83
|
|
|
84
84
|
// Payment terms and the Reference template.
|
|
85
85
|
function draftsSection(ui) {
|
|
86
|
-
const { h, flow, toast, notice, me } = ui;
|
|
86
|
+
const { h, flow, toast, notice, me, settingRow } = ui;
|
|
87
87
|
const disabled = !['owner', 'admin'].includes(me?.person.role);
|
|
88
88
|
const section = h('section', { class: 'fx-set' }, h('h2', { class: 'fx-set__title' }, 'Drafts'), h('div', { class: 'fx-skeleton' }));
|
|
89
89
|
let saved;
|
|
@@ -102,9 +102,7 @@ function draftsSection(ui) {
|
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
const row = (
|
|
106
|
-
h('div', { class: 'fx-set__text' }, h('span', { class: 'fx-set__name' }, name), h('span', { class: 'fx-set__hint' }, hint)),
|
|
107
|
-
h('div', { class: 'fx-set__control' }, control));
|
|
105
|
+
const row = (label, help, control) => settingRow({ label, help, control });
|
|
108
106
|
|
|
109
107
|
// A text or number box saves when it loses focus with a new value.
|
|
110
108
|
const box = (key, label, { type = 'text', read = (v) => v, show = (v) => v ?? '', placeholder } = {}) => {
|
|
@@ -141,9 +139,7 @@ export function sendModeSection(ui) {
|
|
|
141
139
|
const select = h('select', { class: 'fx-select', 'aria-label': 'Send to Xero as', disabled },
|
|
142
140
|
SEND_MODES.map((m) => h('option', { value: m.value }, m.label)));
|
|
143
141
|
const section = h('section', { class: 'fx-set' }, ui.sectionTitle('Sending', { href: DOCS, label: 'How sending works' }),
|
|
144
|
-
|
|
145
|
-
h('div', { class: 'fx-set__text' }, h('span', { class: 'fx-set__name' }, 'Send to Xero as'), h('span', { class: 'fx-set__hint' }, 'Draft: approve in Xero. Approved: Xero emails it.')),
|
|
146
|
-
h('div', { class: 'fx-set__control' }, select)));
|
|
142
|
+
ui.settingRow({ label: 'Send to Xero as', help: 'Draft: approve in Xero. Approved: Xero emails it.', control: select }));
|
|
147
143
|
let saved = null;
|
|
148
144
|
(async () => {
|
|
149
145
|
try {
|