@arcforgelabs/flow 2026.10.43 → 2026.10.44

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.
@@ -7,6 +7,10 @@
7
7
 
8
8
  :root {
9
9
  --fx-line: color-mix(in srgb, var(--t-text) 12%, transparent);
10
+ /* The side space of every page. Pages use the whole width with this slim
11
+ edge, as ClickUp and HubSpot do; the title, toolbar and content start at
12
+ one place on every page. */
13
+ --fx-gutter: clamp(16px, 2vw, 24px);
10
14
  --fx-line-soft: color-mix(in srgb, var(--t-text) 7%, transparent);
11
15
  --fx-hover: color-mix(in srgb, var(--t-text) 5%, transparent);
12
16
  --fx-focus: 0 0 0 3px color-mix(in srgb, var(--t-ring) 40%, transparent);
@@ -90,6 +94,9 @@ body:is([data-state="off"], [data-state="error"], [data-state="no-access"]) .fx-
90
94
  }
91
95
  .fx-input:hover, .fx-select:hover { border-color: var(--t-text); }
92
96
  .fx-input:focus, .fx-select:focus { outline: none; border-color: var(--t-text); box-shadow: var(--fx-focus); }
97
+ .fx-input[aria-invalid="true"], .fx-select[aria-invalid="true"] {
98
+ border-color: var(--t-danger); box-shadow: 0 0 0 3px color-mix(in srgb, var(--t-danger) 28%, transparent);
99
+ }
93
100
  .fx-select {
94
101
  width: auto; min-width: 112px; padding-right: 30px; appearance: none; cursor: pointer;
95
102
  background-image: linear-gradient(45deg, transparent 50%, var(--t-dim) 50%), linear-gradient(135deg, var(--t-dim) 50%, transparent 50%);
@@ -167,7 +174,7 @@ body:is([data-state="off"], [data-state="error"], [data-state="no-access"]) .fx-
167
174
  .fx-shell[data-folded] #user-button .cl-userButtonTrigger { width: auto; padding: 0; border-radius: 50%; background: transparent; box-shadow: none; }
168
175
  .fx-shell[data-folded] #user-button .cl-userButtonOuterIdentifier, .fx-shell[data-folded] #user-button .cl-userButtonBox::before { display: none; }
169
176
  }
170
- .fx-main { flex: 1; min-width: 0; max-width: 1180px; margin: 0 auto; padding: 32px clamp(16px, 4vw, 48px) 64px; display: grid; gap: 20px; align-content: start; }
177
+ .fx-main { flex: 1; min-width: 0; padding: 32px var(--fx-gutter) 64px; display: grid; gap: 20px; align-content: start; }
171
178
  .fx-main:focus { outline: none; box-shadow: none; }
172
179
 
173
180
  /* Page header: the title only (no line under it), then the actions,
@@ -331,6 +338,25 @@ a.fx-link:hover, a.fx-link:focus-visible { border-color: var(--t-strong); }
331
338
  background: #fff; box-shadow: inset 0 0 0 1px var(--fx-line);
332
339
  }
333
340
  .fx-logo--icon { background: var(--t-accent-bg); color: var(--t-accent); box-shadow: none; }
341
+ .fx-tiles { display: grid; gap: 12px; }
342
+ .fx-tiles + .fx-tiles { margin-top: 16px; }
343
+ .fx-tiles__head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
344
+ .fx-tiles__head h2 { color: var(--t-strong); font: 600 15px/1.3 var(--t-body); }
345
+ .fx-tiles__more { display: inline-flex; align-items: center; gap: 4px; color: var(--t-dim); font-size: 13.5px; text-decoration: none; }
346
+ .fx-tiles__more:hover, .fx-tiles__more:focus-visible { color: var(--t-strong); }
347
+ .fx-tiles__more .fx-lucide { width: 15px; height: 15px; }
348
+ .fx-tiles__grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 300px), 1fr)); gap: 12px; }
349
+ .fx-tile {
350
+ display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 14px; align-items: center; padding: 12px 14px 12px 12px;
351
+ border: 1px solid var(--fx-line); border-radius: var(--t-radius-lg); background: var(--t-paper); color: var(--t-text); text-decoration: none;
352
+ transition: border-color 140ms var(--fx-ease), background-color 140ms var(--fx-ease);
353
+ }
354
+ .fx-tile:hover, .fx-tile:focus-visible { border-color: var(--t-control); background: var(--fx-hover); }
355
+ .fx-tile .fx-logo { width: 48px; height: 48px; border-radius: var(--t-radius); }
356
+ .fx-tile .fx-logo--icon { background: var(--fx-hover); color: var(--t-strong); box-shadow: inset 0 0 0 1px var(--fx-line); }
357
+ .fx-tile h3 { overflow: hidden; color: var(--t-strong); font: 600 14.5px/1.35 var(--t-body); text-overflow: ellipsis; white-space: nowrap; }
358
+ .fx-tile__meta { color: var(--t-dim); font-size: 12.5px; white-space: nowrap; }
359
+ .fx-logo--icon .fx-lucide { width: 22px; height: 22px; }
334
360
  .fx-logo img { width: 28px; height: 28px; object-fit: contain; }
335
361
  .fx-logo--lg { width: 52px; height: 52px; }
336
362
  .fx-logo--lg img { width: 34px; height: 34px; }
@@ -407,6 +433,28 @@ a.fx-strong:hover { text-decoration: underline; }
407
433
  .fx-tabs__count { padding: 0 7px; border-radius: 999px; background: var(--fx-hover); color: var(--t-dim); font-size: 12px; line-height: 20px; }
408
434
  .fx-tabs button[data-tone="danger"] .fx-tabs__count { background: var(--t-danger-bg); color: var(--t-danger); }
409
435
 
436
+ /* Saved views (saved-views.js): the row of named view tabs over a list, as
437
+ ClickUp's. The chosen tab is underlined; its ⋯ shows on hover and when
438
+ chosen. Save and Revert sit at the right while the list differs. */
439
+ .fx-views { display: flex; flex-wrap: wrap; align-items: center; gap: 2px; min-height: 32px; margin-bottom: -8px; }
440
+ .fx-views:empty { display: none; }
441
+ .fx-views__item { display: inline-flex; align-items: center; border-bottom: 2px solid transparent; }
442
+ .fx-views__item[data-on="true"] { border-bottom-color: var(--t-brand); }
443
+ .fx-views__tab { display: inline-flex; align-items: center; gap: 6px; max-width: 220px; padding: 6px 4px 6px 8px; border: 0; background: none; color: var(--t-dim); font: 600 13px var(--t-body); cursor: pointer; }
444
+ .fx-views__tab:hover, .fx-views__item[data-on="true"] .fx-views__tab { color: var(--t-strong); }
445
+ .fx-views__tab:focus-visible { outline: 2px solid var(--t-ring, var(--t-accent)); outline-offset: 1px; }
446
+ .fx-views__tab .fx-lucide { width: 14px; height: 14px; flex: none; }
447
+ .fx-views__name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
448
+ .fx-views__dot { flex: none; width: 6px; height: 6px; border-radius: 50%; background: var(--t-accent); }
449
+ .fx-views__item .fx-cfm__more { width: 22px; height: 22px; opacity: 0; }
450
+ .fx-views__item:hover .fx-cfm__more, .fx-views__item[data-on="true"] .fx-cfm__more, .fx-views__item .fx-cfm__more[aria-expanded="true"], .fx-views__item .fx-cfm__more:focus-visible { opacity: 1; }
451
+ .fx-views__item .fx-cfm__more .fx-lucide { width: 14px; height: 14px; }
452
+ .fx-views__add { display: inline-flex; align-items: center; gap: 4px; padding: 6px 8px; border: 0; background: none; color: var(--t-dim); font: 500 13px var(--t-body); cursor: pointer; }
453
+ .fx-views__add:hover { color: var(--t-strong); }
454
+ .fx-views__add .fx-lucide { width: 14px; height: 14px; }
455
+ .fx-views__end { display: inline-flex; gap: 6px; margin-left: auto; }
456
+ @media (hover: none) { .fx-views__item .fx-cfm__more { opacity: 1; } }
457
+
410
458
  /* Quiet alerts (server/ui/alerts.js): one slim line under the page header,
411
459
  only while something on the page needs this person. */
412
460
  .fx-alerts { display: flex; align-items: center; gap: 12px; margin: 0 0 12px; padding: 6px 6px 6px 14px; border: 1px solid var(--fx-line); border-left: 3px solid var(--t-warn); border-radius: var(--t-radius); background: var(--t-paper); font-size: 14px; }
@@ -492,8 +540,6 @@ a.fx-strong:hover { text-decoration: underline; }
492
540
  }
493
541
 
494
542
  /* ── Jobs board ─────────────────────────────────────────────────────────── */
495
- /* Every page shares .fx-main's width; only the board's lanes use the whole window. */
496
- .fx-main--board { max-width: none; }
497
543
  .fx-main > * { min-width: 0; }
498
544
  .fx-toolbar--plain { border-bottom: 0; align-items: center; }
499
545
  .fx-toolbar--plain .fx-search { margin-bottom: 0; }
@@ -544,14 +590,16 @@ a.fx-strong:hover { text-decoration: underline; }
544
590
  .fx-chipset--on .fx-chip__clear { border-color: var(--t-accent-brd, var(--t-accent)); background: var(--t-accent-bg); color: var(--t-accent); }
545
591
  /* Group by, Sort and Filters: rows of selects with a bin at the end, as
546
592
  ClickUp lays them out, in Flow's own look. */
547
- .fx-pop.fx-pop--rows { width: max-content; min-width: 360px; max-width: min(680px, calc(100vw - 16px)); max-height: min(520px, calc(100vh - 32px)); padding: 12px; }
593
+ .fx-pop.fx-pop--rows { width: max-content; min-width: 360px; max-width: min(760px, calc(100vw - 16px)); max-height: min(520px, calc(100vh - 32px)); padding: 12px; }
548
594
  .fx-rows__title { margin: 0 0 10px; color: var(--t-strong); font: 600 14px/1.3 var(--t-body); }
549
595
  .fx-rows { display: grid; gap: 8px; }
550
- .fx-rows__row { display: grid; grid-template-columns: 78px minmax(130px, 1fr) minmax(120px, auto) minmax(150px, 1.2fr) 32px; gap: 8px; align-items: center; }
596
+ .fx-rows__row { display: grid; grid-template-columns: 64px minmax(140px, 1fr) minmax(120px, auto) minmax(170px, 1.3fr) 32px; gap: 8px; align-items: start; }
551
597
  .fx-rows__row--sort { grid-template-columns: 78px minmax(150px, 1fr) minmax(140px, auto) 32px; }
552
598
  .fx-rows__row--group { grid-template-columns: minmax(170px, 1fr) minmax(130px, auto) 32px; }
553
599
  .fx-rows .fx-select, .fx-rows .fx-input { width: 100%; min-width: 0; min-height: 32px; font-size: 13px; }
554
- .fx-rows__lead { color: var(--t-dim); font: 500 13px/1.2 var(--t-body); }
600
+ /* Rows align at the top, so a value on two lines (a date range) keeps the
601
+ bin beside its first line. */
602
+ .fx-rows__lead { display: flex; align-items: center; min-height: 32px; color: var(--t-dim); font: 500 13px/1.2 var(--t-body); }
555
603
  .fx-rows__value { overflow: hidden; text-align: left; text-overflow: ellipsis; white-space: nowrap; }
556
604
  .fx-rows__gone { grid-column: span 3; color: var(--t-dim); font-size: 13px; font-style: italic; }
557
605
  .fx-rows__bin {
@@ -565,13 +613,51 @@ a.fx-strong:hover { text-decoration: underline; }
565
613
  .fx-rows__foot { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 12px; }
566
614
  .fx-rows__add { display: inline-flex; align-items: center; gap: 6px; }
567
615
  .fx-rows__add .fx-lucide { width: 14px; height: 14px; }
616
+ /* The filter panel's groups, as ClickUp's: each group a shaded box with its
617
+ own rows and "Add nested filter"; groups stack with And / Or at the left. */
618
+ .fx-fgroup { display: grid; gap: 8px; padding: 10px; border-radius: var(--t-radius-lg); background: var(--fx-hover); }
619
+ .fx-fgroups { display: grid; gap: 8px; }
620
+ .fx-fgroups__row { display: grid; grid-template-columns: 64px minmax(0, 1fr); gap: 8px; align-items: start; }
621
+ .fx-fgroups__row > .fx-rows__lead, .fx-fgroups__row > span { padding-top: 16px; }
622
+ .fx-fgroup__add { justify-self: start; padding: 2px 4px; border: 0; background: none; color: var(--t-dim); font: 500 12.5px/1.3 var(--t-body); cursor: pointer; }
623
+ .fx-fgroup__add:hover, .fx-fgroup__add:focus-visible { color: var(--t-strong); text-decoration: underline; outline: none; }
624
+ .fx-choice { display: inline-flex; align-items: center; gap: 6px; min-width: 0; text-align: left; }
625
+ .fx-choice .fx-lucide { flex: none; width: 14px; height: 14px; color: var(--t-dim); }
626
+ .fx-choice__text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
627
+ .fx-rows .fx-rows__join { min-width: 0; width: 100%; padding-left: 8px; font: 600 12px/1 var(--t-data, var(--t-body)); letter-spacing: .04em; }
628
+ .fx-rows__word { font: 600 12px/1 var(--t-data, var(--t-body)); letter-spacing: .04em; padding-left: 9px; }
629
+ .fx-fgroups__row .fx-rows__join { min-width: 0; width: 64px; padding-left: 8px; font: 600 12px/1 var(--t-data, var(--t-body)); letter-spacing: .04em; min-height: 32px; }
630
+ .fx-rows__date { display: flex; flex-wrap: wrap; gap: 6px; overflow: visible; }
631
+ .fx-rows__date > .fx-choice { flex: 1 1 130px; }
632
+ .fx-rows__date .fx-input { width: auto; flex: 1 1 130px; }
633
+ .fx-rows__days { display: inline-flex; flex: 1 1 130px; align-items: center; gap: 6px; }
634
+ .fx-rows__days .fx-input[type="number"] { width: 72px; flex: none; }
635
+ .fx-pop__bar { display: flex; align-items: center; justify-content: space-between; }
636
+ .fx-pop__link { padding: 4px 8px; border: 0; background: none; color: var(--t-accent); font: 500 12.5px/1.3 var(--t-body); cursor: pointer; }
637
+ .fx-pop__link:hover, .fx-pop__link:focus-visible { text-decoration: underline; outline: none; }
568
638
  .fx-pop.fx-pop--choices { min-width: 200px; max-width: 300px; padding: 6px; }
639
+ /* A value list row, as ClickUp's: mark (when there is one), name, tick box
640
+ at the right. No empty space at the left. */
641
+ .fx-pop__item--check { gap: 8px; }
642
+ .fx-pop__item--check .fx-pop__text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
643
+ .fx-pop__box { display: inline-grid; place-items: center; flex: none; width: 16px; height: 16px; margin-left: auto; border: 1px solid var(--t-control); border-radius: var(--t-radius); color: var(--t-on-primary); }
644
+ .fx-pop__item--check[aria-checked="true"] .fx-pop__box { border-color: var(--t-primary); background: var(--t-primary); }
645
+ .fx-pop__box svg { width: 12px; height: 12px; }
646
+ .fx-dot { flex: none; width: 9px; height: 9px; border-radius: 50%; background: var(--cf, var(--t-dim)); }
647
+ .fx-dot--stage { --stage: var(--t-accent); background: var(--stage); }
648
+ .fx-dot--stage[data-rank="0"] { --stage: var(--t-mute); }
649
+ .fx-dot--stage[data-rank="3"], .fx-dot--stage[data-rank="4"] { --stage: color-mix(in srgb, var(--t-accent) 45%, var(--t-success)); }
650
+ .fx-dot--stage[data-rank="5"], .fx-dot--stage[data-rank="6"] { --stage: var(--t-success); }
651
+ .fx-dot--stage[data-rank="7"] { --stage: var(--t-dim); }
569
652
  .fx-pop--choices .fx-input { min-height: 32px; margin-bottom: 4px; font-size: 13px; }
570
653
  .fx-pop__end { display: inline-grid; margin-left: auto; color: var(--t-accent); }
571
654
  @media (max-width: 640px) {
572
655
  .fx-pop.fx-pop--rows { min-width: 0; width: calc(100vw - 16px); }
573
656
  .fx-rows__row, .fx-rows__row--sort { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 32px; }
574
- .fx-rows__row > .fx-rows__lead, .fx-rows__row > .fx-select:first-child { grid-column: 1 / -1; }
657
+ .fx-rows__row > .fx-rows__lead, .fx-rows__row > .fx-select:first-child, .fx-rows__row > .fx-rows__join { grid-column: 1 / -1; }
658
+ .fx-rows__row > .fx-rows__join { width: 72px; }
659
+ .fx-fgroups__row { grid-template-columns: minmax(0, 1fr); }
660
+ .fx-fgroups__row > .fx-rows__lead, .fx-fgroups__row > span { padding-top: 0; }
575
661
  .fx-rows__row > .fx-rows__value { grid-column: 1 / 3; }
576
662
  .fx-rows__row > .fx-rows__value--none { display: none; }
577
663
  }
@@ -610,6 +696,42 @@ th:hover .fx-th-sort, .fx-th-sort:focus-visible, .fx-th-sort--on { opacity: 1; }
610
696
  .fx-th-sort .fx-lucide { width: 13px; height: 13px; }
611
697
  .fx-th-sort__n { font: 700 10px/1 var(--t-body); }
612
698
  @media (hover: none) { .fx-th-sort { opacity: 1; } }
699
+ /* A sized list (columns.js): each column has its width, the table is as
700
+ wide as its columns (never stretched; space left over stays empty) and
701
+ scrolls sideways past the page. A head's right
702
+ edge is a grip that resizes the column; a head drags to move it, and a
703
+ line shows where it lands. */
704
+ .fx-table-scroll { overflow-x: auto; scrollbar-width: thin; }
705
+ .fx-table.fx-table--sized { table-layout: fixed; }
706
+ .fx-table--sized th, .fx-table--sized td { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
707
+ .fx-table--sized th { position: relative; }
708
+ /* Every cell is one line: a long value ends in "…" and the row keeps its
709
+ height, as HubSpot's. A wrapper in a cell (a client with its warning
710
+ mark, a stage pill) shrinks its text, never its mark. */
711
+ .fx-table--sized td > * { max-width: 100%; }
712
+ .fx-table--sized .fx-job__cell { display: flex; min-width: 0; }
713
+ .fx-table--sized .fx-job__cell > * { flex: none; }
714
+ .fx-table--sized .fx-job__cell > .fx-strong,
715
+ .fx-table--sized .fx-status { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
716
+ .fx-table--sized .fx-job__cell > .fx-strong { flex: 0 1 auto; }
717
+ .fx-table--sized .fx-th-cell { max-width: 100%; }
718
+ .fx-table--sized .fx-th { overflow: hidden; text-overflow: ellipsis; }
719
+ .fx-th--drag .fx-th { cursor: grab; touch-action: none; }
720
+ .fx-th--drag[data-dragging] { opacity: .5; }
721
+ .fx-th--drag[data-dragging] .fx-th { cursor: grabbing; }
722
+ .fx-table--sized th[data-drop]::after { content: ""; position: absolute; top: 6px; bottom: 6px; width: 2px; border-radius: 1px; background: var(--t-accent); }
723
+ .fx-table--sized th[data-drop="before"]::after { left: 0; }
724
+ .fx-table--sized th[data-drop="after"]::after { right: 0; }
725
+ .fx-th-grip { position: absolute; top: 0; right: 0; bottom: 0; width: 8px; cursor: col-resize; touch-action: none; }
726
+ .fx-th-grip::after { content: ""; position: absolute; top: 25%; bottom: 25%; right: 3px; width: 2px; border-radius: 1px; background: transparent; }
727
+ th:hover > .fx-th-grip::after { background: var(--t-brd-hi); }
728
+ .fx-th-grip:hover::after, .fx-th-grip:focus-visible::after, .fx-table--sized[data-resizing] .fx-th-grip:active::after { background: var(--t-accent); }
729
+ .fx-th-grip:focus-visible { outline: none; }
730
+ .fx-table--sized[data-resizing] { cursor: col-resize; user-select: none; }
731
+ .fx-table--sized .fx-tick-cell, .fx-table--sized .fx-th-add, .fx-table--sized .fx-c-end { overflow: visible; text-overflow: clip; }
732
+ .fx-table--sized .fx-tick-cell { padding-right: 0; }
733
+ /* + stays at the right edge while the table scrolls sideways. */
734
+ .fx-table--sized .fx-th-add { position: sticky; right: 0; z-index: 1; padding-left: 0; padding-right: 12px; }
613
735
  .fx-avatar--sm { width: 24px; height: 24px; font-size: 10px; }
614
736
  .fx-c-date time { font-variant-numeric: tabular-nums; }
615
737
  .fx-stage[data-rank="plain"] { --stage: var(--t-dim); }
@@ -624,9 +746,41 @@ th:hover .fx-th-sort, .fx-th-sort:focus-visible, .fx-th-sort--on { opacity: 1; }
624
746
  .fx-board__head { display: flex; flex: none; align-items: center; gap: 8px; min-width: 0; padding: 2px 4px 4px 2px; }
625
747
  .fx-board__sum { flex: none; margin-left: auto; color: var(--t-dim); font-size: 12px; font-variant-numeric: tabular-nums; }
626
748
  .fx-board__cards { display: grid; gap: 8px; align-content: start; min-height: 0; max-height: 70vh; padding-right: 2px; overflow-y: auto; scrollbar-width: thin; }
749
+ /* listFrame() in kit.js: the window of a list or board (Jobs, Invoices,
750
+ Reconcile). Its body scrolls both ways; the foot holds the count. A table
751
+ or panel inside drops its own frame. On a wide, tall screen the page is
752
+ the window's height and does not scroll itself: the frame fills it, and
753
+ on the board each lane scrolls inside itself. */
754
+ .fx-frame { display: flex; flex-direction: column; min-width: 0; border: 1px solid var(--fx-line); border-radius: var(--t-radius-lg); background: var(--t-paper); box-shadow: var(--t-shadow); overflow: hidden; }
755
+ .fx-frame__view { display: flex; flex-direction: column; min-height: 0; }
756
+ .fx-frame__body { min-height: 0; overflow: auto; scrollbar-width: thin; }
757
+ /* edgeFade() in kit.js: a soft fade over each edge with more content past
758
+ it, in the colour under the rows (--fx-fade). */
759
+ .fx-fades { position: relative; --fx-fade: var(--t-paper); }
760
+ .fx-fade { position: absolute; z-index: 3; pointer-events: none; opacity: 0; transition: opacity .15s; }
761
+ .fx-fade[data-edge="top"] { inset: 0 0 auto; height: 24px; background: linear-gradient(to bottom, var(--fx-fade), transparent); }
762
+ .fx-fade[data-edge="bottom"] { inset: auto 0 0; height: 24px; background: linear-gradient(to top, var(--fx-fade), transparent); }
763
+ .fx-fade[data-edge="left"] { inset: 0 auto 0 0; width: 24px; background: linear-gradient(to right, var(--fx-fade), transparent); }
764
+ .fx-fade[data-edge="right"] { inset: 0 0 0 auto; width: 24px; background: linear-gradient(to left, var(--fx-fade), transparent); }
765
+ .fx-fades[data-fade~="top"] > .fx-fade[data-edge="top"], .fx-fades[data-fade~="bottom"] > .fx-fade[data-edge="bottom"],
766
+ .fx-fades[data-fade~="left"] > .fx-fade[data-edge="left"], .fx-fades[data-fade~="right"] > .fx-fade[data-edge="right"] { opacity: 1; }
767
+ .fx-main--board .fx-frame__view { --fx-fade: var(--t-bg); }
768
+ .fx-board__col.fx-fades { --fx-fade: var(--t-s1); }
769
+ .fx-board__col > .fx-fade[data-edge="bottom"] { border-radius: 0 0 var(--t-radius-lg) var(--t-radius-lg); }
770
+ .fx-frame__body .fx-panel { border: 0; border-radius: 0; box-shadow: none; overflow: visible; }
771
+ .fx-frame__body .fx-empty { padding: 24px; }
772
+ .fx-frame .fx-table-scroll { overflow: visible; }
773
+ .fx-frame thead th { position: sticky; top: 0; z-index: 2; }
774
+ .fx-frame__foot { display: flex; flex: none; align-items: center; justify-content: space-between; gap: 12px; min-height: 44px; padding: 6px 12px; border-top: 1px solid var(--fx-line); color: var(--t-dim); font-size: 13px; font-variant-numeric: tabular-nums; }
775
+ .fx-frame__tools { display: flex; align-items: center; gap: 8px; }
776
+ .fx-main--board .fx-frame__body { padding: 12px; background: var(--t-bg); }
627
777
  @media (min-width: 821px) and (min-height: 560px) {
628
- .fx-main--board { height: 100vh; height: 100dvh; padding: 24px 24px 16px; grid-template-rows: auto auto minmax(0, 1fr); align-content: stretch; }
629
- .fx-main--board > .fx-jobs { min-height: 0; }
778
+ .fx-main--frame { height: 100vh; height: 100dvh; padding-bottom: 16px; display: flex; flex-direction: column; }
779
+ .fx-main--frame > * { flex: none; }
780
+ .fx-main--frame > .fx-frame { flex: 1; min-height: 0; }
781
+ .fx-main--frame .fx-frame__view, .fx-main--frame .fx-frame__body { flex: 1; }
782
+ .fx-main--board .fx-frame__body { display: flex; overflow: hidden; }
783
+ .fx-main--board .fx-jobs { flex: 1; min-width: 0; min-height: 0; }
630
784
  .fx-main--board .fx-board { height: 100%; overflow-y: hidden; align-items: stretch; }
631
785
  .fx-main--board .fx-board__cards { flex: 1; max-height: none; }
632
786
  }
@@ -695,9 +849,9 @@ th:hover .fx-th-sort, .fx-th-sort:focus-visible, .fx-th-sort--on { opacity: 1; }
695
849
  .fx-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; color: var(--t-dim); overflow-wrap: anywhere; }
696
850
  .fx-dialog.fx-task { width: min(1040px, calc(100vw - 32px)); max-height: calc(100vh - 48px); overflow-y: auto; }
697
851
  .fx-task__grid { display: grid; grid-template-columns: minmax(0, 1fr) 300px; }
698
- /* No sidecar: the main area takes the room. Review is wider for its table. */
852
+ /* No sidecar: the main area takes the room. Fix is wider for the change table. */
699
853
  .fx-task__grid:has(> .fx-sidecar[hidden]) { grid-template-columns: minmax(0, 1fr); }
700
- .fx-task__grid:has(> .fx-sidecar [data-tab="review"][aria-selected="true"]) { grid-template-columns: minmax(0, 1fr) 400px; }
854
+ .fx-task__grid:has(> .fx-sidecar [data-tab="fix"][aria-selected="true"]) { grid-template-columns: minmax(0, 1fr) 400px; }
701
855
  .fx-task__side { background: var(--t-s1); }
702
856
  .fx-task__tools { display: flex; align-items: center; gap: 4px; }
703
857
  .fx-task__tools .fx-lucide { width: 16px; height: 16px; }
@@ -715,16 +869,22 @@ th:hover .fx-th-sort, .fx-th-sort:focus-visible, .fx-th-sort--on { opacity: 1; }
715
869
  .fx-task__late { color: var(--t-danger) !important; }
716
870
  .fx-task__link { color: var(--t-accent); text-decoration: none; border-bottom: 1px solid color-mix(in srgb, var(--t-accent) 40%, transparent); }
717
871
  .fx-task__link:hover { border-bottom-color: currentColor; }
718
- .fx-task__fix { scroll-margin-top: 16px; margin-top: 22px; border: 1px solid var(--t-warn-brd, var(--t-warn)); border-radius: var(--t-radius); background: var(--t-warn-bg); }
719
- .fx-task__fix h3, .fx-task__lines h3 { margin: 0; color: var(--t-dim); font-size: 12px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; }
720
- .fx-task__fix h3 { padding: 12px 14px 4px; color: var(--t-warn); }
721
- .fx-task__fixrow { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; padding: 9px 14px; border-top: 1px solid var(--fx-line-soft); color: var(--t-strong); font-size: 13.5px; }
722
- .fx-task__fixrow > strong { font-weight: 600; }
723
- .fx-task__fixrow > span { flex: 1 1 160px; color: var(--t-text); }
724
- .fx-task__linkbtn { all: unset; cursor: pointer; color: var(--t-accent); border-bottom: 1px solid color-mix(in srgb, var(--t-accent) 40%, transparent); }
725
- .fx-task__linkbtn:hover { border-bottom-color: currentColor; }
726
- .fx-task__linkbtn:focus-visible { box-shadow: var(--fx-focus); }
727
- .fx-task__fixrow:first-of-type { border-top: 0; }
872
+ .fx-task__lines h3 { margin: 0; color: var(--t-dim); font-size: 12px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; }
873
+ /* What to fix: one toggle chip a problem beside the stage; its problem opens
874
+ in the sidecar's Fix tab (jobs.js openJob). One layout with or without. */
875
+ .fx-task__stage { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
876
+ .fx-task__fixchip { all: unset; cursor: pointer; display: inline-flex; align-items: center; gap: 6px; height: 24px; padding: 0 10px; border: 1px solid var(--t-warn-brd, var(--t-warn)); border-radius: 999px; background: var(--t-warn-bg); color: var(--t-warn); font-size: 12.5px; font-weight: 600; }
877
+ .fx-task__fixchip .fx-lucide { width: 14px; height: 14px; }
878
+ .fx-task__fixchip:hover { border-color: var(--t-warn); }
879
+ .fx-task__fixchip:focus-visible { box-shadow: var(--fx-focus); }
880
+ /* Pressed: its problem is open in the sidecar's Fix tab. */
881
+ .fx-task__fixchip[aria-pressed="true"] { border-color: var(--t-warn); background: var(--t-warn); color: var(--t-paper); }
882
+ .fx-task__fixitem { display: grid; gap: 8px; justify-items: start; font-size: 13.5px; }
883
+ .fx-task__fixitem > strong { color: var(--t-strong); }
884
+ .fx-task__fixitem > p { margin: 0; }
885
+ .fx-task__fixitem > .fx-task__review { justify-self: stretch; }
886
+ .fx-task__fixgo .fx-lucide { width: 14px; height: 14px; }
887
+ .fx-task__fixgo { display: inline-flex; align-items: center; gap: 6px; }
728
888
  .fx-task__choose { scroll-margin-top: 16px; margin-top: 22px; display: flex; flex-wrap: wrap; align-items: center; gap: 10px; padding: 12px 14px; border: 1px solid var(--t-warn-brd, var(--t-warn)); border-radius: var(--t-radius); background: var(--t-warn-bg); color: var(--t-strong); font-size: 13.5px; }
729
889
  .fx-task__choose > svg { flex: none; width: 20px; height: 20px; color: var(--t-warn); }
730
890
  .fx-task__choose > span { flex: 1 1 200px; }
@@ -736,7 +896,6 @@ th:hover .fx-th-sort, .fx-th-sort:focus-visible, .fx-th-sort--on { opacity: 1; }
736
896
  invoice opened from Invoices (the pager on top). */
737
897
  .fx-rec { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
738
898
  .fx-rec .fx-lucide { width: 16px; height: 16px; }
739
- .fx-rec__gap { max-width: 1180px; }
740
899
  .fx-rec__gap .fx-toolbar .fx-rows__title { margin: 0; }
741
900
  .fx-rec__gap .fx-ph__actions .fx-btn, .fx-rec__acts .fx-btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; }
742
901
  .fx-rec__results { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
@@ -804,7 +963,7 @@ th:hover .fx-th-sort, .fx-th-sort:focus-visible, .fx-th-sort--on { opacity: 1; }
804
963
  .fx-task__grid { grid-template-columns: minmax(0, 1fr); }
805
964
  .fx-task__main { padding: 16px; }
806
965
  .fx-task__fields { grid-template-columns: minmax(0, 1fr); }
807
- .fx-task__grid:has(> .fx-sidecar [data-tab="review"][aria-selected="true"]) { grid-template-columns: minmax(0, 1fr); }
966
+ .fx-task__grid:has(> .fx-sidecar [data-tab="fix"][aria-selected="true"]) { grid-template-columns: minmax(0, 1fr); }
808
967
  .fx-task__side { border-top: 1px solid var(--fx-line); border-left: 0; }
809
968
  }
810
969
 
@@ -945,6 +1104,8 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
945
1104
  /* ── Narrow screens: the sidebar becomes a bar across the top. ──────────── */
946
1105
  @media (max-width: 820px) {
947
1106
  .fx-shell { flex-direction: column; }
1107
+ /* In a column, the page takes the full width, not its content's. */
1108
+ .fx-main { width: 100%; }
948
1109
  .fx-side { flex-direction: row; align-items: center; width: auto; height: 60px; padding: 0 12px; border-right: 0; border-bottom: 1px solid var(--fx-line); }
949
1110
  .fx-side__head { flex-direction: row-reverse; gap: 6px; padding: 0; }
950
1111
  .fx-side__head .fx-brand { display: none; }
@@ -987,6 +1148,9 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
987
1148
  .fx-main { padding-top: 20px; }
988
1149
  .fx-title { font-size: 22px; }
989
1150
  .fx-table thead { display: none; }
1151
+ .fx-table.fx-table--sized { width: 100% !important; min-width: 0; table-layout: auto; }
1152
+ .fx-table--sized td { white-space: normal; }
1153
+ .fx-table--sized colgroup { display: none; }
990
1154
  .fx-table, .fx-table tbody, .fx-table tr, .fx-table td { display: block; width: 100%; }
991
1155
  .fx-table tr { padding: 12px 14px; border-bottom: 1px solid var(--fx-line-soft); }
992
1156
  .fx-table td { padding: 4px 0; border: 0; }
@@ -1014,7 +1178,7 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
1014
1178
  .fx-table--jobs tr.fx-row-link > td.fx-c-client { grid-area: 1 / 2; }
1015
1179
  .fx-table--jobs tr.fx-row-link > td.fx-c-total { grid-area: 1 / 3; }
1016
1180
  .fx-table--jobs td.fx-c-end { display: none; }
1017
- .fx-table--jobs tr.fx-row-link > td[data-label]:not(.fx-c-total, .fx-c-stage)::before { content: attr(data-label) " "; color: var(--t-dim); font-size: 12px; }
1181
+ .fx-table--jobs tr.fx-row-link > td[data-label]:not(.fx-c-client, .fx-c-total, .fx-c-stage)::before { content: attr(data-label) " "; color: var(--t-dim); font-size: 12px; }
1018
1182
  .fx-table--jobs tr.fx-table__group { grid-template-columns: 28px minmax(0, 1fr); background: var(--t-s1); }
1019
1183
  .fx-table--jobs tr.fx-table__group > td.fx-tick-cell { grid-row: 1; align-self: center; padding: 0; }
1020
1184
  .fx-table--jobs tr.fx-table__group > td { background: none; }
@@ -1217,7 +1381,7 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
1217
1381
  .fx-cfs__where .fx-lucide { width: 16px; height: 16px; color: var(--t-dim); }
1218
1382
  .fx-cfs__foot { display: flex; align-items: center; gap: 8px; padding: 12px 16px; border-top: 1px solid var(--fx-line-soft); }
1219
1383
  .fx-cfs__delete { color: var(--t-danger); background: color-mix(in srgb, var(--t-danger) 12%, transparent); }
1220
- .fx-cfs__delete:hover { background: color-mix(in srgb, var(--t-danger) 22%, transparent); }
1384
+ .fx-cfs__delete:hover { background: color-mix(in srgb, var(--t-danger) 28%, transparent); }
1221
1385
  .fx-cfs__delete .fx-lucide { width: 16px; height: 16px; }
1222
1386
  @media (max-width: 720px) {
1223
1387
  .fx-cfm { grid-template-columns: minmax(0, 1fr) 36px 32px; column-gap: 8px; }
@@ -1314,12 +1478,21 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
1314
1478
  /* The Invoices list head (invoicing.js): tabs, search and the send bar stay
1315
1479
  at the top while rows scroll up under them; a shadow shows once they do.
1316
1480
  Narrow, the app bar (60px) sits above it. */
1317
- .fx-listhead { position: sticky; top: 0; z-index: 5; margin: -12px calc(-1 * clamp(16px, 4vw, 48px)) 0; padding: 12px clamp(16px, 4vw, 48px) 0; background: var(--t-bg); transition: box-shadow .15s; }
1481
+ /* listHead() in kit.js: the toolbar row of every list. One height, with
1482
+ chips, tabs or a Board/List switch in it, so the rows start at one place. */
1483
+ .fx-listhead > .fx-toolbar { min-height: 40px; }
1484
+ .fx-listhead .fx-toolbar__end { flex: 1 1 auto; }
1485
+ .fx-listhead > .fx-toolbar--plain { align-items: center; }
1486
+ .fx-listhead[data-sticky] { position: sticky; top: 0; z-index: 5; margin: -12px calc(-1 * var(--fx-gutter)) 0; padding: 12px var(--fx-gutter) 0; background: var(--t-bg); transition: box-shadow .15s; }
1318
1487
  .fx-listhead[data-stuck] { box-shadow: 0 10px 12px -12px rgba(0, 0, 0, .45); }
1319
- .fx-listhead__edge { height: 0; margin-bottom: -20px; }
1320
- @media (max-width: 820px) { .fx-listhead { top: 60px; } }
1488
+ /* The edge tells listHead when the rows reach the toolbar. In the page's
1489
+ grid it takes a row and a gap of its own, so the toolbar moves up by both. */
1490
+ .fx-listhead__edge { height: 0; }
1491
+ .fx-listhead__edge + .fx-listhead[data-sticky] { margin-top: -32px; }
1492
+ .fx-listhead > .fx-toolbar > .fx-tabs button { padding-block: 9px; }
1493
+ @media (max-width: 820px) { .fx-listhead[data-sticky] { top: 60px; } }
1321
1494
  /* On a phone the head would hide too many rows: it scrolls away. */
1322
- @media (max-width: 560px) { .fx-listhead { position: static; } }
1495
+ @media (max-width: 560px) { .fx-listhead[data-sticky] { position: static; } }
1323
1496
 
1324
1497
  /* The bulk action bar (bulk-bar.js): floats at the foot while rows are
1325
1498
  ticked. "N selected ×", then icon actions in groups, then More. It takes
@@ -1364,7 +1537,7 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
1364
1537
  /* Xero › Import (server/ui/xero-import.js): Review compares one fact per row
1365
1538
  with ✓ or ≠ between the sides; the list and history tick rows for the bulk
1366
1539
  bar. */
1367
- .fx-xi { display: flex; flex-direction: column; gap: 16px; min-width: 0; max-width: 980px; }
1540
+ .fx-xi { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
1368
1541
  .fx-xi .fx-lucide { width: 16px; height: 16px; }
1369
1542
  .fx-xi__views { display: inline-flex; border: 1px solid var(--fx-line); }
1370
1543
  .fx-xi__view { display: inline-grid; place-items: center; width: 40px; height: 36px; border: 0; background: transparent; color: var(--t-dim); cursor: pointer; }
@@ -12,10 +12,10 @@ const TEST_LABELS = {
12
12
  more_than: 'is more than', at_most: 'is at most', is: 'is', is_not: 'is not', is_one_of: 'is one of',
13
13
  is_empty: 'is empty', is_not_empty: 'is not empty', has: 'has', has_not: 'does not have',
14
14
  is_not_one_of: 'is not one of', contains: 'contains', does_not_contain: 'does not contain',
15
- starts_with: 'starts with', does_not_start_with: 'does not start with', has_number: 'has a number', one_word: 'is one word',
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
17
  const ACTION_LABELS = { wait: 'Wait for a person', skip: 'Never draft', pending: 'Pending, with a note' };
18
- const NO_VALUE = ['is_empty', 'is_not_empty', 'has_number', 'one_word'];
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'];
21
21
 
@@ -158,7 +158,7 @@ export function rulesSection(ui, title = 'Rules') {
158
158
  // A pending rule's note: what a person must fix. It shows on the draft.
159
159
  const note = h('input', {
160
160
  class: 'fx-input', maxlength: '200', value: draft?.action.note ?? '', 'aria-label': 'Note',
161
- placeholder: 'For example: Put the practitioner in the job, then check the Reference.',
161
+ placeholder: 'Type a note',
162
162
  });
163
163
  const noteField = h('div', { class: 'fx-field' }, h('span', { class: 'fx-label' }, 'Note on the draft'), note);
164
164
  const showNote = () => { noteField.hidden = action.value !== 'pending'; };
@@ -1,5 +1,6 @@
1
- // Settings, Rules (/invoicing-settings): a card for each part, like the
2
- // Integrations page, and one page for each (/invoicing-settings/<key>).
1
+ // Settings, Rules (/invoicing-settings): a section of cards for each part,
2
+ // like Claude's Customize page, and one page for each
3
+ // (/invoicing-settings/<key>).
3
4
  // Drafts holds payment terms and the Reference; Price codes the lines with no
4
5
  // price item (line-mappings.js); Invoices the checks a ready job passes
5
6
  // before Flow drafts its invoice (invoice-rules.js, docs/INVOICE_RULES.md).
@@ -19,43 +20,65 @@ const SEND_MODES = [
19
20
  { value: 'approve_and_email', label: 'Approved and emailed' },
20
21
  ];
21
22
 
22
- const onCount = (list) => ({ label: `${list.filter((x) => x.enabled).length} on`, tone: list.some((x) => x.enabled) ? 'success' : 'mute' });
23
-
24
- // Each card: its page, its mark, and its state once the data is in. No
25
- // blurb: the name and the state say enough.
23
+ // The parts of Rules, each on its own page (/invoicing-settings/<key>).
26
24
  const PARTS = [
27
- { key: 'drafts', title: 'Drafts', mark: 'draft', mount: (ui) => draftsSection(ui) },
28
- { key: 'price-codes', title: 'Price codes', mark: 'tag', mount: (ui) => lineMappingSection(ui),
29
- summary: async (flow) => {
30
- const { labels } = await flow('/api/invoicing/line-labels');
31
- const waiting = labels.filter((l) => l.unmapped > 0).length;
32
- return waiting ? { status: { label: `${waiting} wait`, tone: 'warn' }, action: 'Fix' } : { status: { label: 'All mapped', tone: 'success' } };
33
- } },
34
- { key: 'invoices', title: 'Invoices', mark: 'rules', mount: (ui) => rulesSection(ui, 'Invoices'),
35
- summary: async (flow) => ({ status: onCount((await flow('/api/invoicing/rules')).rules) }) },
25
+ { key: 'invoices', title: 'Invoices', mount: (ui) => rulesSection(ui, 'Invoices') },
26
+ { key: 'drafts', title: 'Drafts', mount: (ui) => draftsSection(ui) },
27
+ { key: 'price-codes', title: 'Price codes', mount: (ui) => lineMappingSection(ui) },
36
28
  ];
37
29
 
30
+ // What a rule does, as its card's mark: Pending, Waits, Never drafted.
31
+ const KIND = {
32
+ pending: { mark: 'flag' },
33
+ wait: { mark: 'clock' },
34
+ skip: { mark: 'ban' },
35
+ };
36
+ const SHOWN = 6;
37
+
38
38
  export function renderInvoicingSettings(main, ui, id) {
39
- const { h, flow, notice, pageHeader, tileCard, crumbs } = ui;
39
+ const { h, flow, notice, pageHeader, tileCard, tileSection, crumbs } = ui;
40
40
  if (id) {
41
41
  const part = PARTS.find((p) => p.key === id);
42
42
  main.append(crumbs(['/invoicing-settings', 'Rules'], part?.title ?? 'Not found'),
43
43
  part ? h('div', { class: 'fx-card', style: { padding: '8px 20px 16px' } }, part.mount(ui)) : notice('No such part of Rules.', 'danger'));
44
44
  return;
45
45
  }
46
- const grid = h('div', { class: 'fx-int-grid' });
47
- main.append(pageHeader('Rules'), grid);
48
- // Each card shows at once and fills in its state when its data comes back.
49
- for (const part of PARTS) {
50
- const href = `/invoicing-settings/${part.key}`;
51
- const slot = tileCard({ href, mark: part.mark, title: part.title });
52
- grid.append(slot);
53
- if (part.summary) {
54
- part.summary(flow)
55
- .then((more) => slot.replaceWith(tileCard({ href, mark: part.mark, title: part.title, ...more })))
56
- .catch(() => {});
46
+ // The landing is Claude's Customize page: a section for each part, one-line
47
+ // cards, and "Show all" to the part's own page, a list.
48
+ const host = h('div', { style: { display: 'grid', gap: '20px' } }, h('div', { class: 'fx-skeleton' }));
49
+ main.append(pageHeader('Rules'), host);
50
+ const page = (key) => `/invoicing-settings/${key}`;
51
+ (async () => {
52
+ let settings;
53
+ let labels;
54
+ let rules;
55
+ try {
56
+ [{ settings }, { labels }, { rules }] = await Promise.all([
57
+ flow('/api/invoicing/settings'), flow('/api/invoicing/line-labels'), flow('/api/invoicing/rules')]);
58
+ } catch (error) {
59
+ host.replaceChildren(notice(error.message, 'danger'));
60
+ return;
57
61
  }
58
- }
62
+ const ruleCard = (rule) => tileCard({
63
+ row: true, href: page('invoices'), mark: (KIND[rule.action.kind] ?? { mark: 'list-checks' }).mark, title: rule.name,
64
+ meta: rule.enabled ? 'On' : 'Off',
65
+ });
66
+ const waiting = labels.filter((l) => l.unmapped > 0).length;
67
+ host.replaceChildren(
68
+ tileSection('Invoices', rules.length ? rules.slice(0, SHOWN).map(ruleCard) : [h('p', { class: 'fx-dim' }, 'No rules yet.')],
69
+ { href: page('invoices'), more: rules.length > SHOWN ? `Show all ${rules.length}` : 'Show all' }),
70
+ tileSection('Drafts', [
71
+ tileCard({ row: true, href: page('drafts'), mark: 'calendar-clock', title: 'Payment terms', meta: `${settings.paymentTermsDays} days` }),
72
+ tileCard({ row: true, href: page('drafts'), mark: 'text-cursor-input', title: 'Reference' }),
73
+ ], { href: page('drafts') }),
74
+ tileSection('Price codes', [
75
+ tileCard({
76
+ row: true, href: page('price-codes'), mark: 'tag', title: 'Line labels',
77
+ status: waiting ? { label: `${waiting} wait`, tone: 'warn' } : { label: 'All mapped', tone: 'success' },
78
+ }),
79
+ ], { href: page('price-codes') }),
80
+ );
81
+ })();
59
82
  }
60
83
 
61
84
  // Payment terms and the Reference template.
@@ -104,7 +127,7 @@ function draftsSection(ui) {
104
127
  disabled ? notice('Only an owner or admin can change these.', 'info') : null,
105
128
  row('Payment terms', 'Days from invoice to due date.', box('paymentTermsDays', 'Payment terms in days', { type: 'number', read: Number })),
106
129
  row('Reference', 'The Reference on each invoice. Use {practitioner} and {patient}.',
107
- box('referenceTemplate', 'Reference rule', { placeholder: '{practitioner} - {patient}' })),
130
+ box('referenceTemplate', 'Reference rule', { placeholder: 'Type a reference rule' })),
108
131
  ].filter(Boolean));
109
132
  })();
110
133
  return section;