@arcforgelabs/flow 2026.10.43 → 2026.10.45
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/README.md +1 -0
- package/npm-shrinkwrap.json +4 -3
- package/package.json +5 -3
- package/server/api/invoicing.js +53 -3
- package/server/auth/people.js +41 -2
- package/server/auth/routes.js +11 -0
- package/server/auth/session.js +22 -3
- package/server/core/custom-fields.js +4 -3
- package/server/core/invoice-rules.js +8 -3
- package/server/core/job-match.js +6 -2
- package/server/core/sending.js +4 -3
- package/server/release-channel.js +5 -2
- package/server/ui/app.js +53 -197
- package/server/ui/columns.js +286 -0
- package/server/ui/flow.css +230 -43
- package/server/ui/invoice-rules.js +3 -3
- package/server/ui/invoicing-settings.js +52 -29
- package/server/ui/invoicing.js +219 -72
- package/server/ui/job-view.js +276 -60
- package/server/ui/jobs.js +133 -137
- package/server/ui/kit.js +279 -0
- package/server/ui/list-views.js +115 -0
- package/server/ui/monitoring.js +51 -8
- package/server/ui/pager.js +67 -0
- package/server/ui/person.js +29 -0
- package/server/ui/price-list.js +27 -7
- package/server/ui/reconcile.js +114 -61
- package/server/ui/routes.js +1 -1
- package/server/ui/saved-views.js +141 -0
- package/server/ui/search.js +8 -0
- package/server/ui/view-bar.js +176 -70
package/server/ui/flow.css
CHANGED
|
@@ -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%);
|
|
@@ -134,17 +141,16 @@ body:is([data-state="off"], [data-state="error"], [data-state="no-access"]) .fx-
|
|
|
134
141
|
.fx-toggle__mark { display: none; }
|
|
135
142
|
.fx-toggle__mark .fx-mark { display: block; }
|
|
136
143
|
.fx-nav { display: flex; flex-direction: column; gap: 2px; flex: 1; min-height: 0; overflow-y: auto; }
|
|
137
|
-
.fx-
|
|
138
|
-
.fx-nav__group:first-child { padding-top: 4px; }
|
|
139
|
-
.fx-nav a, .fx-nav__report {
|
|
144
|
+
.fx-nav a, .fx-nav__report, .fx-nav__more {
|
|
140
145
|
display: flex; align-items: center; gap: 10px;
|
|
141
146
|
padding: 8px 10px; border-radius: var(--t-radius);
|
|
142
147
|
color: var(--t-text); font-weight: 500; font-size: 14px; text-decoration: none; white-space: nowrap;
|
|
143
148
|
}
|
|
144
|
-
.fx-nav a:hover, .fx-nav__report:hover { color: var(--t-strong); background: var(--fx-hover); }
|
|
145
|
-
.fx-nav__report
|
|
149
|
+
.fx-nav a:hover, .fx-nav__report:hover, .fx-nav__more:hover { color: var(--t-strong); background: var(--fx-hover); }
|
|
150
|
+
.fx-nav__report, .fx-nav__more { border: 0; background: none; font: inherit; font-weight: 500; font-size: 14px; text-align: left; cursor: pointer; }
|
|
151
|
+
.fx-nav__report { margin-top: auto; }
|
|
146
152
|
.fx-nav__report[hidden] { display: none; }
|
|
147
|
-
.fx-nav a[aria-current="page"] { color: var(--t-accent); background: var(--t-accent-bg); font-weight: 650; box-shadow: inset 2px 0 0 var(--t-brand); }
|
|
153
|
+
.fx-nav :is(a, .fx-nav__more)[aria-current="page"] { color: var(--t-accent); background: var(--t-accent-bg); font-weight: 650; box-shadow: inset 2px 0 0 var(--t-brand); }
|
|
148
154
|
.fx-nav__count { margin-left: auto; color: var(--t-dim); font-size: 12px; font-weight: 700; }
|
|
149
155
|
.fx-ico { display: inline-flex; flex: none; }
|
|
150
156
|
.fx-ico svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
|
|
@@ -159,15 +165,14 @@ body:is([data-state="off"], [data-state="error"], [data-state="no-access"]) .fx-
|
|
|
159
165
|
.fx-shell[data-folded] .fx-side :is(a, button, #user-button) { cursor: pointer; }
|
|
160
166
|
.fx-shell[data-folded] .fx-side__head { flex-direction: column; gap: 10px; padding: 0 0 14px; }
|
|
161
167
|
.fx-shell[data-folded] .fx-side__head .fx-brand { display: none; }
|
|
162
|
-
.fx-shell[data-folded] .fx-
|
|
163
|
-
.fx-shell[data-folded] :is(.fx-nav a, .fx-nav__report) { justify-content: center; padding: 9px 0; }
|
|
168
|
+
.fx-shell[data-folded] :is(.fx-nav a, .fx-nav__report, .fx-nav__more) { justify-content: center; padding: 9px 0; }
|
|
164
169
|
.fx-shell[data-folded] .fx-nav__label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
|
|
165
170
|
.fx-shell[data-folded] .fx-side__end { flex-direction: column; padding: 12px 0 0; }
|
|
166
171
|
.fx-shell[data-folded] #user-button { flex: none; }
|
|
167
172
|
.fx-shell[data-folded] #user-button .cl-userButtonTrigger { width: auto; padding: 0; border-radius: 50%; background: transparent; box-shadow: none; }
|
|
168
173
|
.fx-shell[data-folded] #user-button .cl-userButtonOuterIdentifier, .fx-shell[data-folded] #user-button .cl-userButtonBox::before { display: none; }
|
|
169
174
|
}
|
|
170
|
-
.fx-main { flex: 1; min-width: 0;
|
|
175
|
+
.fx-main { flex: 1; min-width: 0; padding: 32px var(--fx-gutter) 64px; display: grid; gap: 20px; align-content: start; }
|
|
171
176
|
.fx-main:focus { outline: none; box-shadow: none; }
|
|
172
177
|
|
|
173
178
|
/* Page header: the title only (no line under it), then the actions,
|
|
@@ -331,6 +336,25 @@ a.fx-link:hover, a.fx-link:focus-visible { border-color: var(--t-strong); }
|
|
|
331
336
|
background: #fff; box-shadow: inset 0 0 0 1px var(--fx-line);
|
|
332
337
|
}
|
|
333
338
|
.fx-logo--icon { background: var(--t-accent-bg); color: var(--t-accent); box-shadow: none; }
|
|
339
|
+
.fx-tiles { display: grid; gap: 12px; }
|
|
340
|
+
.fx-tiles + .fx-tiles { margin-top: 16px; }
|
|
341
|
+
.fx-tiles__head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
|
342
|
+
.fx-tiles__head h2 { color: var(--t-strong); font: 600 15px/1.3 var(--t-body); }
|
|
343
|
+
.fx-tiles__more { display: inline-flex; align-items: center; gap: 4px; color: var(--t-dim); font-size: 13.5px; text-decoration: none; }
|
|
344
|
+
.fx-tiles__more:hover, .fx-tiles__more:focus-visible { color: var(--t-strong); }
|
|
345
|
+
.fx-tiles__more .fx-lucide { width: 15px; height: 15px; }
|
|
346
|
+
.fx-tiles__grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 300px), 1fr)); gap: 12px; }
|
|
347
|
+
.fx-tile {
|
|
348
|
+
display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 14px; align-items: center; padding: 12px 14px 12px 12px;
|
|
349
|
+
border: 1px solid var(--fx-line); border-radius: var(--t-radius-lg); background: var(--t-paper); color: var(--t-text); text-decoration: none;
|
|
350
|
+
transition: border-color 140ms var(--fx-ease), background-color 140ms var(--fx-ease);
|
|
351
|
+
}
|
|
352
|
+
.fx-tile:hover, .fx-tile:focus-visible { border-color: var(--t-control); background: var(--fx-hover); }
|
|
353
|
+
.fx-tile .fx-logo { width: 48px; height: 48px; border-radius: var(--t-radius); }
|
|
354
|
+
.fx-tile .fx-logo--icon { background: var(--fx-hover); color: var(--t-strong); box-shadow: inset 0 0 0 1px var(--fx-line); }
|
|
355
|
+
.fx-tile h3 { overflow: hidden; color: var(--t-strong); font: 600 14.5px/1.35 var(--t-body); text-overflow: ellipsis; white-space: nowrap; }
|
|
356
|
+
.fx-tile__meta { color: var(--t-dim); font-size: 12.5px; white-space: nowrap; }
|
|
357
|
+
.fx-logo--icon .fx-lucide { width: 22px; height: 22px; }
|
|
334
358
|
.fx-logo img { width: 28px; height: 28px; object-fit: contain; }
|
|
335
359
|
.fx-logo--lg { width: 52px; height: 52px; }
|
|
336
360
|
.fx-logo--lg img { width: 34px; height: 34px; }
|
|
@@ -407,6 +431,28 @@ a.fx-strong:hover { text-decoration: underline; }
|
|
|
407
431
|
.fx-tabs__count { padding: 0 7px; border-radius: 999px; background: var(--fx-hover); color: var(--t-dim); font-size: 12px; line-height: 20px; }
|
|
408
432
|
.fx-tabs button[data-tone="danger"] .fx-tabs__count { background: var(--t-danger-bg); color: var(--t-danger); }
|
|
409
433
|
|
|
434
|
+
/* Saved views (saved-views.js): the row of named view tabs over a list, as
|
|
435
|
+
ClickUp's. The chosen tab is underlined; its ⋯ shows on hover and when
|
|
436
|
+
chosen. Save and Revert sit at the right while the list differs. */
|
|
437
|
+
.fx-views { display: flex; flex-wrap: wrap; align-items: center; gap: 2px; min-height: 32px; margin-bottom: -8px; }
|
|
438
|
+
.fx-views:empty { display: none; }
|
|
439
|
+
.fx-views__item { display: inline-flex; align-items: center; border-bottom: 2px solid transparent; }
|
|
440
|
+
.fx-views__item[data-on="true"] { border-bottom-color: var(--t-brand); }
|
|
441
|
+
.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; }
|
|
442
|
+
.fx-views__tab:hover, .fx-views__item[data-on="true"] .fx-views__tab { color: var(--t-strong); }
|
|
443
|
+
.fx-views__tab:focus-visible { outline: 2px solid var(--t-ring, var(--t-accent)); outline-offset: 1px; }
|
|
444
|
+
.fx-views__tab .fx-lucide { width: 14px; height: 14px; flex: none; }
|
|
445
|
+
.fx-views__name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
446
|
+
.fx-views__dot { flex: none; width: 6px; height: 6px; border-radius: 50%; background: var(--t-accent); }
|
|
447
|
+
.fx-views__item .fx-cfm__more { width: 22px; height: 22px; opacity: 0; }
|
|
448
|
+
.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; }
|
|
449
|
+
.fx-views__item .fx-cfm__more .fx-lucide { width: 14px; height: 14px; }
|
|
450
|
+
.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; }
|
|
451
|
+
.fx-views__add:hover { color: var(--t-strong); }
|
|
452
|
+
.fx-views__add .fx-lucide { width: 14px; height: 14px; }
|
|
453
|
+
.fx-views__end { display: inline-flex; gap: 6px; margin-left: auto; }
|
|
454
|
+
@media (hover: none) { .fx-views__item .fx-cfm__more { opacity: 1; } }
|
|
455
|
+
|
|
410
456
|
/* Quiet alerts (server/ui/alerts.js): one slim line under the page header,
|
|
411
457
|
only while something on the page needs this person. */
|
|
412
458
|
.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 +538,6 @@ a.fx-strong:hover { text-decoration: underline; }
|
|
|
492
538
|
}
|
|
493
539
|
|
|
494
540
|
/* ── 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
541
|
.fx-main > * { min-width: 0; }
|
|
498
542
|
.fx-toolbar--plain { border-bottom: 0; align-items: center; }
|
|
499
543
|
.fx-toolbar--plain .fx-search { margin-bottom: 0; }
|
|
@@ -544,14 +588,16 @@ a.fx-strong:hover { text-decoration: underline; }
|
|
|
544
588
|
.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
589
|
/* Group by, Sort and Filters: rows of selects with a bin at the end, as
|
|
546
590
|
ClickUp lays them out, in Flow's own look. */
|
|
547
|
-
.fx-pop.fx-pop--rows { width: max-content; min-width: 360px; max-width: min(
|
|
591
|
+
.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
592
|
.fx-rows__title { margin: 0 0 10px; color: var(--t-strong); font: 600 14px/1.3 var(--t-body); }
|
|
549
593
|
.fx-rows { display: grid; gap: 8px; }
|
|
550
|
-
.fx-rows__row { display: grid; grid-template-columns:
|
|
594
|
+
.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
595
|
.fx-rows__row--sort { grid-template-columns: 78px minmax(150px, 1fr) minmax(140px, auto) 32px; }
|
|
552
596
|
.fx-rows__row--group { grid-template-columns: minmax(170px, 1fr) minmax(130px, auto) 32px; }
|
|
553
597
|
.fx-rows .fx-select, .fx-rows .fx-input { width: 100%; min-width: 0; min-height: 32px; font-size: 13px; }
|
|
554
|
-
|
|
598
|
+
/* Rows align at the top, so a value on two lines (a date range) keeps the
|
|
599
|
+
bin beside its first line. */
|
|
600
|
+
.fx-rows__lead { display: flex; align-items: center; min-height: 32px; color: var(--t-dim); font: 500 13px/1.2 var(--t-body); }
|
|
555
601
|
.fx-rows__value { overflow: hidden; text-align: left; text-overflow: ellipsis; white-space: nowrap; }
|
|
556
602
|
.fx-rows__gone { grid-column: span 3; color: var(--t-dim); font-size: 13px; font-style: italic; }
|
|
557
603
|
.fx-rows__bin {
|
|
@@ -565,13 +611,51 @@ a.fx-strong:hover { text-decoration: underline; }
|
|
|
565
611
|
.fx-rows__foot { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 12px; }
|
|
566
612
|
.fx-rows__add { display: inline-flex; align-items: center; gap: 6px; }
|
|
567
613
|
.fx-rows__add .fx-lucide { width: 14px; height: 14px; }
|
|
614
|
+
/* The filter panel's groups, as ClickUp's: each group a shaded box with its
|
|
615
|
+
own rows and "Add nested filter"; groups stack with And / Or at the left. */
|
|
616
|
+
.fx-fgroup { display: grid; gap: 8px; padding: 10px; border-radius: var(--t-radius-lg); background: var(--fx-hover); }
|
|
617
|
+
.fx-fgroups { display: grid; gap: 8px; }
|
|
618
|
+
.fx-fgroups__row { display: grid; grid-template-columns: 64px minmax(0, 1fr); gap: 8px; align-items: start; }
|
|
619
|
+
.fx-fgroups__row > .fx-rows__lead, .fx-fgroups__row > span { padding-top: 16px; }
|
|
620
|
+
.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; }
|
|
621
|
+
.fx-fgroup__add:hover, .fx-fgroup__add:focus-visible { color: var(--t-strong); text-decoration: underline; outline: none; }
|
|
622
|
+
.fx-choice { display: inline-flex; align-items: center; gap: 6px; min-width: 0; text-align: left; }
|
|
623
|
+
.fx-choice .fx-lucide { flex: none; width: 14px; height: 14px; color: var(--t-dim); }
|
|
624
|
+
.fx-choice__text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
625
|
+
.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; }
|
|
626
|
+
.fx-rows__word { font: 600 12px/1 var(--t-data, var(--t-body)); letter-spacing: .04em; padding-left: 9px; }
|
|
627
|
+
.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; }
|
|
628
|
+
.fx-rows__date { display: flex; flex-wrap: wrap; gap: 6px; overflow: visible; }
|
|
629
|
+
.fx-rows__date > .fx-choice { flex: 1 1 130px; }
|
|
630
|
+
.fx-rows__date .fx-input { width: auto; flex: 1 1 130px; }
|
|
631
|
+
.fx-rows__days { display: inline-flex; flex: 1 1 130px; align-items: center; gap: 6px; }
|
|
632
|
+
.fx-rows__days .fx-input[type="number"] { width: 72px; flex: none; }
|
|
633
|
+
.fx-pop__bar { display: flex; align-items: center; justify-content: space-between; }
|
|
634
|
+
.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; }
|
|
635
|
+
.fx-pop__link:hover, .fx-pop__link:focus-visible { text-decoration: underline; outline: none; }
|
|
568
636
|
.fx-pop.fx-pop--choices { min-width: 200px; max-width: 300px; padding: 6px; }
|
|
637
|
+
/* A value list row, as ClickUp's: mark (when there is one), name, tick box
|
|
638
|
+
at the right. No empty space at the left. */
|
|
639
|
+
.fx-pop__item--check { gap: 8px; }
|
|
640
|
+
.fx-pop__item--check .fx-pop__text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
641
|
+
.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); }
|
|
642
|
+
.fx-pop__item--check[aria-checked="true"] .fx-pop__box { border-color: var(--t-primary); background: var(--t-primary); }
|
|
643
|
+
.fx-pop__box svg { width: 12px; height: 12px; }
|
|
644
|
+
.fx-dot { flex: none; width: 9px; height: 9px; border-radius: 50%; background: var(--cf, var(--t-dim)); }
|
|
645
|
+
.fx-dot--stage { --stage: var(--t-accent); background: var(--stage); }
|
|
646
|
+
.fx-dot--stage[data-rank="0"] { --stage: var(--t-mute); }
|
|
647
|
+
.fx-dot--stage[data-rank="3"], .fx-dot--stage[data-rank="4"] { --stage: color-mix(in srgb, var(--t-accent) 45%, var(--t-success)); }
|
|
648
|
+
.fx-dot--stage[data-rank="5"], .fx-dot--stage[data-rank="6"] { --stage: var(--t-success); }
|
|
649
|
+
.fx-dot--stage[data-rank="7"] { --stage: var(--t-dim); }
|
|
569
650
|
.fx-pop--choices .fx-input { min-height: 32px; margin-bottom: 4px; font-size: 13px; }
|
|
570
651
|
.fx-pop__end { display: inline-grid; margin-left: auto; color: var(--t-accent); }
|
|
571
652
|
@media (max-width: 640px) {
|
|
572
653
|
.fx-pop.fx-pop--rows { min-width: 0; width: calc(100vw - 16px); }
|
|
573
654
|
.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; }
|
|
655
|
+
.fx-rows__row > .fx-rows__lead, .fx-rows__row > .fx-select:first-child, .fx-rows__row > .fx-rows__join { grid-column: 1 / -1; }
|
|
656
|
+
.fx-rows__row > .fx-rows__join { width: 72px; }
|
|
657
|
+
.fx-fgroups__row { grid-template-columns: minmax(0, 1fr); }
|
|
658
|
+
.fx-fgroups__row > .fx-rows__lead, .fx-fgroups__row > span { padding-top: 0; }
|
|
575
659
|
.fx-rows__row > .fx-rows__value { grid-column: 1 / 3; }
|
|
576
660
|
.fx-rows__row > .fx-rows__value--none { display: none; }
|
|
577
661
|
}
|
|
@@ -610,23 +694,94 @@ th:hover .fx-th-sort, .fx-th-sort:focus-visible, .fx-th-sort--on { opacity: 1; }
|
|
|
610
694
|
.fx-th-sort .fx-lucide { width: 13px; height: 13px; }
|
|
611
695
|
.fx-th-sort__n { font: 700 10px/1 var(--t-body); }
|
|
612
696
|
@media (hover: none) { .fx-th-sort { opacity: 1; } }
|
|
697
|
+
/* A sized list (columns.js): each column has its width, the table is as
|
|
698
|
+
wide as its columns (never stretched; space left over stays empty) and
|
|
699
|
+
scrolls sideways past the page. A head's right
|
|
700
|
+
edge is a grip that resizes the column; a head drags to move it, and a
|
|
701
|
+
line shows where it lands. */
|
|
702
|
+
.fx-table-scroll { overflow-x: auto; scrollbar-width: thin; }
|
|
703
|
+
.fx-table.fx-table--sized { table-layout: fixed; }
|
|
704
|
+
.fx-table--sized th, .fx-table--sized td { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
705
|
+
.fx-table--sized th { position: relative; }
|
|
706
|
+
/* Every cell is one line: a long value ends in "…" and the row keeps its
|
|
707
|
+
height, as HubSpot's. A wrapper in a cell (a client with its warning
|
|
708
|
+
mark, a stage pill) shrinks its text, never its mark. */
|
|
709
|
+
.fx-table--sized td > * { max-width: 100%; }
|
|
710
|
+
.fx-table--sized .fx-job__cell { display: flex; min-width: 0; }
|
|
711
|
+
.fx-table--sized .fx-job__cell > * { flex: none; }
|
|
712
|
+
.fx-table--sized .fx-job__cell > .fx-strong,
|
|
713
|
+
.fx-table--sized .fx-status { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
714
|
+
.fx-table--sized .fx-job__cell > .fx-strong { flex: 0 1 auto; }
|
|
715
|
+
.fx-table--sized .fx-th-cell { max-width: 100%; }
|
|
716
|
+
.fx-table--sized .fx-th { overflow: hidden; text-overflow: ellipsis; }
|
|
717
|
+
.fx-th--drag .fx-th { cursor: grab; touch-action: none; }
|
|
718
|
+
.fx-th--drag[data-dragging] { opacity: .5; }
|
|
719
|
+
.fx-th--drag[data-dragging] .fx-th { cursor: grabbing; }
|
|
720
|
+
.fx-table--sized th[data-drop]::after { content: ""; position: absolute; top: 6px; bottom: 6px; width: 2px; border-radius: 1px; background: var(--t-accent); }
|
|
721
|
+
.fx-table--sized th[data-drop="before"]::after { left: 0; }
|
|
722
|
+
.fx-table--sized th[data-drop="after"]::after { right: 0; }
|
|
723
|
+
.fx-th-grip { position: absolute; top: 0; right: 0; bottom: 0; width: 8px; cursor: col-resize; touch-action: none; }
|
|
724
|
+
.fx-th-grip::after { content: ""; position: absolute; top: 25%; bottom: 25%; right: 3px; width: 2px; border-radius: 1px; background: transparent; }
|
|
725
|
+
th:hover > .fx-th-grip::after { background: var(--t-brd-hi); }
|
|
726
|
+
.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); }
|
|
727
|
+
.fx-th-grip:focus-visible { outline: none; }
|
|
728
|
+
.fx-table--sized[data-resizing] { cursor: col-resize; user-select: none; }
|
|
729
|
+
.fx-table--sized .fx-tick-cell, .fx-table--sized .fx-th-add, .fx-table--sized .fx-c-end { overflow: visible; text-overflow: clip; }
|
|
730
|
+
.fx-table--sized .fx-tick-cell { padding-right: 0; }
|
|
731
|
+
/* The end column fills the row after the last column (columns.js), as
|
|
732
|
+
HubSpot's; + sits at its right edge and stays there while the table
|
|
733
|
+
scrolls sideways. */
|
|
734
|
+
.fx-table--sized .fx-th-add { position: sticky; right: 0; z-index: 1; padding-left: 0; padding-right: 12px; text-align: right; }
|
|
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); }
|
|
616
|
-
/* The board is one row of lanes (one per group), each
|
|
617
|
-
|
|
618
|
-
the
|
|
738
|
+
/* The board is one row of lanes (one per group), each a fixed phone width
|
|
739
|
+
(--fx-lane), as HubSpot's: lanes never stretch to fill a wide screen, so
|
|
740
|
+
the space past the last lane stays empty. More lanes scroll sideways
|
|
741
|
+
inside the board, never as a second row. On a wide, tall screen the board fills
|
|
619
742
|
the page down to its bottom edge, the page itself does not scroll, and
|
|
620
743
|
each lane scrolls inside itself (jobs.js sets fx-main--board while the
|
|
621
744
|
board shows). */
|
|
622
|
-
.fx-board { display: grid; grid-auto-flow: column; grid-auto-columns:
|
|
745
|
+
.fx-board { --fx-lane: min(85vw, 272px); display: grid; grid-auto-flow: column; grid-auto-columns: var(--fx-lane); justify-content: start; gap: 12px; align-items: start; overflow-x: auto; overscroll-behavior-x: contain; scrollbar-width: thin; padding-bottom: 4px; }
|
|
623
746
|
.fx-board__col { display: flex; flex-direction: column; gap: 8px; min-width: 0; min-height: 0; padding: 10px 6px 6px 8px; border-radius: var(--t-radius-lg); background: var(--t-s1); }
|
|
624
747
|
.fx-board__head { display: flex; flex: none; align-items: center; gap: 8px; min-width: 0; padding: 2px 4px 4px 2px; }
|
|
625
748
|
.fx-board__sum { flex: none; margin-left: auto; color: var(--t-dim); font-size: 12px; font-variant-numeric: tabular-nums; }
|
|
626
749
|
.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; }
|
|
750
|
+
/* listFrame() in kit.js: the window of a list or board (Jobs, Invoices,
|
|
751
|
+
Reconcile). Its body scrolls both ways; the foot holds the count. A table
|
|
752
|
+
or panel inside drops its own frame. On a wide, tall screen the page is
|
|
753
|
+
the window's height and does not scroll itself: the frame fills it, and
|
|
754
|
+
on the board each lane scrolls inside itself. */
|
|
755
|
+
.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; }
|
|
756
|
+
.fx-frame__view { display: flex; flex-direction: column; min-height: 0; }
|
|
757
|
+
.fx-frame__body { min-height: 0; overflow: auto; scrollbar-width: thin; }
|
|
758
|
+
/* edgeFade() in kit.js: a soft fade over each edge with more content past
|
|
759
|
+
it, in the colour under the rows (--fx-fade). */
|
|
760
|
+
.fx-fades { position: relative; --fx-fade: var(--t-paper); }
|
|
761
|
+
.fx-fade { position: absolute; z-index: 3; pointer-events: none; opacity: 0; transition: opacity .15s; }
|
|
762
|
+
.fx-fade[data-edge="top"] { inset: 0 0 auto; height: 24px; background: linear-gradient(to bottom, var(--fx-fade), transparent); }
|
|
763
|
+
.fx-fade[data-edge="bottom"] { inset: auto 0 0; height: 24px; background: linear-gradient(to top, var(--fx-fade), transparent); }
|
|
764
|
+
.fx-fade[data-edge="left"] { inset: 0 auto 0 0; width: 24px; background: linear-gradient(to right, var(--fx-fade), transparent); }
|
|
765
|
+
.fx-fade[data-edge="right"] { inset: 0 0 0 auto; width: 24px; background: linear-gradient(to left, var(--fx-fade), transparent); }
|
|
766
|
+
.fx-fades[data-fade~="top"] > .fx-fade[data-edge="top"], .fx-fades[data-fade~="bottom"] > .fx-fade[data-edge="bottom"],
|
|
767
|
+
.fx-fades[data-fade~="left"] > .fx-fade[data-edge="left"], .fx-fades[data-fade~="right"] > .fx-fade[data-edge="right"] { opacity: 1; }
|
|
768
|
+
.fx-main--board .fx-frame__view { --fx-fade: var(--t-bg); }
|
|
769
|
+
.fx-board__col.fx-fades { --fx-fade: var(--t-s1); }
|
|
770
|
+
.fx-board__col > .fx-fade[data-edge="bottom"] { border-radius: 0 0 var(--t-radius-lg) var(--t-radius-lg); }
|
|
771
|
+
.fx-frame__body .fx-panel { border: 0; border-radius: 0; box-shadow: none; overflow: visible; }
|
|
772
|
+
.fx-frame__body .fx-empty { padding: 24px; }
|
|
773
|
+
.fx-frame .fx-table-scroll { overflow: visible; }
|
|
774
|
+
.fx-frame thead th { position: sticky; top: 0; z-index: 2; }
|
|
775
|
+
.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; }
|
|
776
|
+
.fx-frame__tools { display: flex; align-items: center; gap: 8px; }
|
|
777
|
+
.fx-main--board .fx-frame__body { padding: 12px; background: var(--t-bg); }
|
|
627
778
|
@media (min-width: 821px) and (min-height: 560px) {
|
|
628
|
-
.fx-main--
|
|
629
|
-
.fx-main--
|
|
779
|
+
.fx-main--frame { height: 100vh; height: 100dvh; padding-bottom: 16px; display: flex; flex-direction: column; }
|
|
780
|
+
.fx-main--frame > * { flex: none; }
|
|
781
|
+
.fx-main--frame > .fx-frame { flex: 1; min-height: 0; }
|
|
782
|
+
.fx-main--frame .fx-frame__view, .fx-main--frame .fx-frame__body { flex: 1; }
|
|
783
|
+
.fx-main--board .fx-frame__body { display: flex; overflow: hidden; }
|
|
784
|
+
.fx-main--board .fx-jobs { flex: 1; min-width: 0; min-height: 0; }
|
|
630
785
|
.fx-main--board .fx-board { height: 100%; overflow-y: hidden; align-items: stretch; }
|
|
631
786
|
.fx-main--board .fx-board__cards { flex: 1; max-height: none; }
|
|
632
787
|
}
|
|
@@ -695,9 +850,9 @@ th:hover .fx-th-sort, .fx-th-sort:focus-visible, .fx-th-sort--on { opacity: 1; }
|
|
|
695
850
|
.fx-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; color: var(--t-dim); overflow-wrap: anywhere; }
|
|
696
851
|
.fx-dialog.fx-task { width: min(1040px, calc(100vw - 32px)); max-height: calc(100vh - 48px); overflow-y: auto; }
|
|
697
852
|
.fx-task__grid { display: grid; grid-template-columns: minmax(0, 1fr) 300px; }
|
|
698
|
-
/* No sidecar: the main area takes the room.
|
|
853
|
+
/* No sidecar: the main area takes the room. Fix is wider for the change table. */
|
|
699
854
|
.fx-task__grid:has(> .fx-sidecar[hidden]) { grid-template-columns: minmax(0, 1fr); }
|
|
700
|
-
.fx-task__grid:has(> .fx-sidecar [data-tab="
|
|
855
|
+
.fx-task__grid:has(> .fx-sidecar [data-tab="fix"][aria-selected="true"]) { grid-template-columns: minmax(0, 1fr) 400px; }
|
|
701
856
|
.fx-task__side { background: var(--t-s1); }
|
|
702
857
|
.fx-task__tools { display: flex; align-items: center; gap: 4px; }
|
|
703
858
|
.fx-task__tools .fx-lucide { width: 16px; height: 16px; }
|
|
@@ -715,16 +870,22 @@ th:hover .fx-th-sort, .fx-th-sort:focus-visible, .fx-th-sort--on { opacity: 1; }
|
|
|
715
870
|
.fx-task__late { color: var(--t-danger) !important; }
|
|
716
871
|
.fx-task__link { color: var(--t-accent); text-decoration: none; border-bottom: 1px solid color-mix(in srgb, var(--t-accent) 40%, transparent); }
|
|
717
872
|
.fx-task__link:hover { border-bottom-color: currentColor; }
|
|
718
|
-
.fx-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
.fx-
|
|
722
|
-
.fx-
|
|
723
|
-
.fx-
|
|
724
|
-
.fx-
|
|
725
|
-
.fx-
|
|
726
|
-
|
|
727
|
-
.fx-
|
|
873
|
+
.fx-task__lines h3 { margin: 0; color: var(--t-dim); font-size: 12px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; }
|
|
874
|
+
/* What to fix: one toggle chip a problem beside the stage; its problem opens
|
|
875
|
+
in the sidecar's Fix tab (jobs.js openJob). One layout with or without. */
|
|
876
|
+
.fx-task__stage { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
|
|
877
|
+
.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; }
|
|
878
|
+
.fx-task__fixchip .fx-lucide { width: 14px; height: 14px; }
|
|
879
|
+
.fx-task__fixchip:hover { border-color: var(--t-warn); }
|
|
880
|
+
.fx-task__fixchip:focus-visible { box-shadow: var(--fx-focus); }
|
|
881
|
+
/* Pressed: its problem is open in the sidecar's Fix tab. */
|
|
882
|
+
.fx-task__fixchip[aria-pressed="true"] { border-color: var(--t-warn); background: var(--t-warn); color: var(--t-paper); }
|
|
883
|
+
.fx-task__fixitem { display: grid; gap: 8px; justify-items: start; font-size: 13.5px; }
|
|
884
|
+
.fx-task__fixitem > strong { color: var(--t-strong); }
|
|
885
|
+
.fx-task__fixitem > p { margin: 0; }
|
|
886
|
+
.fx-task__fixitem > .fx-task__review { justify-self: stretch; }
|
|
887
|
+
.fx-task__fixgo .fx-lucide { width: 14px; height: 14px; }
|
|
888
|
+
.fx-task__fixgo { display: inline-flex; align-items: center; gap: 6px; }
|
|
728
889
|
.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
890
|
.fx-task__choose > svg { flex: none; width: 20px; height: 20px; color: var(--t-warn); }
|
|
730
891
|
.fx-task__choose > span { flex: 1 1 200px; }
|
|
@@ -736,7 +897,6 @@ th:hover .fx-th-sort, .fx-th-sort:focus-visible, .fx-th-sort--on { opacity: 1; }
|
|
|
736
897
|
invoice opened from Invoices (the pager on top). */
|
|
737
898
|
.fx-rec { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
|
|
738
899
|
.fx-rec .fx-lucide { width: 16px; height: 16px; }
|
|
739
|
-
.fx-rec__gap { max-width: 1180px; }
|
|
740
900
|
.fx-rec__gap .fx-toolbar .fx-rows__title { margin: 0; }
|
|
741
901
|
.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
902
|
.fx-rec__results { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
|
|
@@ -804,7 +964,7 @@ th:hover .fx-th-sort, .fx-th-sort:focus-visible, .fx-th-sort--on { opacity: 1; }
|
|
|
804
964
|
.fx-task__grid { grid-template-columns: minmax(0, 1fr); }
|
|
805
965
|
.fx-task__main { padding: 16px; }
|
|
806
966
|
.fx-task__fields { grid-template-columns: minmax(0, 1fr); }
|
|
807
|
-
.fx-task__grid:has(> .fx-sidecar [data-tab="
|
|
967
|
+
.fx-task__grid:has(> .fx-sidecar [data-tab="fix"][aria-selected="true"]) { grid-template-columns: minmax(0, 1fr); }
|
|
808
968
|
.fx-task__side { border-top: 1px solid var(--fx-line); border-left: 0; }
|
|
809
969
|
}
|
|
810
970
|
|
|
@@ -945,6 +1105,8 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
|
|
|
945
1105
|
/* ── Narrow screens: the sidebar becomes a bar across the top. ──────────── */
|
|
946
1106
|
@media (max-width: 820px) {
|
|
947
1107
|
.fx-shell { flex-direction: column; }
|
|
1108
|
+
/* In a column, the page takes the full width, not its content's. */
|
|
1109
|
+
.fx-main { width: 100%; }
|
|
948
1110
|
.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
1111
|
.fx-side__head { flex-direction: row-reverse; gap: 6px; padding: 0; }
|
|
950
1112
|
.fx-side__head .fx-brand { display: none; }
|
|
@@ -987,6 +1149,10 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
|
|
|
987
1149
|
.fx-main { padding-top: 20px; }
|
|
988
1150
|
.fx-title { font-size: 22px; }
|
|
989
1151
|
.fx-table thead { display: none; }
|
|
1152
|
+
.fx-table.fx-table--sized { width: 100% !important; min-width: 0; table-layout: auto; }
|
|
1153
|
+
.fx-table--sized td { white-space: normal; }
|
|
1154
|
+
.fx-table--sized colgroup { display: none; }
|
|
1155
|
+
.fx-table--sized td.fx-c-end { display: none; }
|
|
990
1156
|
.fx-table, .fx-table tbody, .fx-table tr, .fx-table td { display: block; width: 100%; }
|
|
991
1157
|
.fx-table tr { padding: 12px 14px; border-bottom: 1px solid var(--fx-line-soft); }
|
|
992
1158
|
.fx-table td { padding: 4px 0; border: 0; }
|
|
@@ -1014,7 +1180,7 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
|
|
|
1014
1180
|
.fx-table--jobs tr.fx-row-link > td.fx-c-client { grid-area: 1 / 2; }
|
|
1015
1181
|
.fx-table--jobs tr.fx-row-link > td.fx-c-total { grid-area: 1 / 3; }
|
|
1016
1182
|
.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; }
|
|
1183
|
+
.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
1184
|
.fx-table--jobs tr.fx-table__group { grid-template-columns: 28px minmax(0, 1fr); background: var(--t-s1); }
|
|
1019
1185
|
.fx-table--jobs tr.fx-table__group > td.fx-tick-cell { grid-row: 1; align-self: center; padding: 0; }
|
|
1020
1186
|
.fx-table--jobs tr.fx-table__group > td { background: none; }
|
|
@@ -1047,7 +1213,6 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
|
|
|
1047
1213
|
.fx-price__head { display: flex; flex-wrap: wrap; align-items: end; justify-content: space-between; gap: 8px 16px; }
|
|
1048
1214
|
.fx-price__head .fx-search { margin: 0 0 6px; width: 260px; }
|
|
1049
1215
|
@media (max-width: 560px) { .fx-price__head .fx-search { width: 100%; } }
|
|
1050
|
-
.fx-price__scroll { overflow-x: auto; border: 1px solid var(--fx-line); background: var(--t-paper); }
|
|
1051
1216
|
.fx-price__table th { position: sticky; top: 0; z-index: 1; }
|
|
1052
1217
|
.fx-price__table td { padding-top: 9px; padding-bottom: 9px; }
|
|
1053
1218
|
.fx-price__table td.fx-num, .fx-price__table th.fx-num { text-align: right; white-space: nowrap; }
|
|
@@ -1217,7 +1382,7 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
|
|
|
1217
1382
|
.fx-cfs__where .fx-lucide { width: 16px; height: 16px; color: var(--t-dim); }
|
|
1218
1383
|
.fx-cfs__foot { display: flex; align-items: center; gap: 8px; padding: 12px 16px; border-top: 1px solid var(--fx-line-soft); }
|
|
1219
1384
|
.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)
|
|
1385
|
+
.fx-cfs__delete:hover { background: color-mix(in srgb, var(--t-danger) 28%, transparent); }
|
|
1221
1386
|
.fx-cfs__delete .fx-lucide { width: 16px; height: 16px; }
|
|
1222
1387
|
@media (max-width: 720px) {
|
|
1223
1388
|
.fx-cfm { grid-template-columns: minmax(0, 1fr) 36px 32px; column-gap: 8px; }
|
|
@@ -1300,6 +1465,19 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
|
|
|
1300
1465
|
.fx-pager__step:hover:not(:disabled) { background: var(--fx-hover); border-color: var(--fx-line); }
|
|
1301
1466
|
.fx-pager__step:disabled { opacity: 0.35; cursor: default; }
|
|
1302
1467
|
|
|
1468
|
+
/* The pages of a long list, in the list window's foot bar (pager.js):
|
|
1469
|
+
rows per page, the range, numbered pages. The page shown is filled. */
|
|
1470
|
+
.fx-pages { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 16px; color: var(--t-dim); font-size: 13px; font-variant-numeric: tabular-nums; }
|
|
1471
|
+
.fx-pages__rows { display: inline-flex; align-items: center; gap: 6px; }
|
|
1472
|
+
.fx-pages__size { width: 76px; min-height: 28px; padding-block: 2px; font-size: 13px; }
|
|
1473
|
+
.fx-pages__range { color: var(--t-text); }
|
|
1474
|
+
.fx-pages__steps { display: inline-flex; align-items: center; gap: 4px; }
|
|
1475
|
+
.fx-pages__step, .fx-pages__num { display: inline-grid; place-items: center; min-width: 28px; height: 28px; padding: 0 6px; border: 1px solid var(--fx-line-soft); border-radius: 999px; background: transparent; color: var(--t-text); font: inherit; cursor: pointer; transition: background 120ms var(--fx-ease), border-color 120ms var(--fx-ease); }
|
|
1476
|
+
.fx-pages__step:hover:not(:disabled), .fx-pages__num:hover:not([aria-current]) { background: var(--fx-hover); border-color: var(--fx-line); }
|
|
1477
|
+
.fx-pages__step:disabled { opacity: 0.35; cursor: default; }
|
|
1478
|
+
.fx-pages__num[aria-current="page"] { background: var(--t-primary); border-color: var(--t-primary); color: var(--t-on-primary); cursor: default; }
|
|
1479
|
+
.fx-pages__gap { min-width: 16px; text-align: center; color: var(--t-mute); }
|
|
1480
|
+
|
|
1303
1481
|
/* New invoice from a job: a searchable list of jobs with no invoice. */
|
|
1304
1482
|
.fx-dialog:has(.fx-pick__body) { width: min(620px, calc(100vw - 32px)); }
|
|
1305
1483
|
.fx-pick__body { display: grid; gap: 12px; }
|
|
@@ -1314,12 +1492,21 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
|
|
|
1314
1492
|
/* The Invoices list head (invoicing.js): tabs, search and the send bar stay
|
|
1315
1493
|
at the top while rows scroll up under them; a shadow shows once they do.
|
|
1316
1494
|
Narrow, the app bar (60px) sits above it. */
|
|
1317
|
-
|
|
1495
|
+
/* listHead() in kit.js: the toolbar row of every list. One height, with
|
|
1496
|
+
chips, tabs or a Board/List switch in it, so the rows start at one place. */
|
|
1497
|
+
.fx-listhead > .fx-toolbar { min-height: 40px; }
|
|
1498
|
+
.fx-listhead .fx-toolbar__end { flex: 1 1 auto; }
|
|
1499
|
+
.fx-listhead > .fx-toolbar--plain { align-items: center; }
|
|
1500
|
+
.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
1501
|
.fx-listhead[data-stuck] { box-shadow: 0 10px 12px -12px rgba(0, 0, 0, .45); }
|
|
1319
|
-
|
|
1320
|
-
|
|
1502
|
+
/* The edge tells listHead when the rows reach the toolbar. In the page's
|
|
1503
|
+
grid it takes a row and a gap of its own, so the toolbar moves up by both. */
|
|
1504
|
+
.fx-listhead__edge { height: 0; }
|
|
1505
|
+
.fx-listhead__edge + .fx-listhead[data-sticky] { margin-top: -32px; }
|
|
1506
|
+
.fx-listhead > .fx-toolbar > .fx-tabs button { padding-block: 9px; }
|
|
1507
|
+
@media (max-width: 820px) { .fx-listhead[data-sticky] { top: 60px; } }
|
|
1321
1508
|
/* On a phone the head would hide too many rows: it scrolls away. */
|
|
1322
|
-
@media (max-width: 560px) { .fx-listhead { position: static; } }
|
|
1509
|
+
@media (max-width: 560px) { .fx-listhead[data-sticky] { position: static; } }
|
|
1323
1510
|
|
|
1324
1511
|
/* The bulk action bar (bulk-bar.js): floats at the foot while rows are
|
|
1325
1512
|
ticked. "N selected ×", then icon actions in groups, then More. It takes
|
|
@@ -1364,7 +1551,7 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
|
|
|
1364
1551
|
/* Xero › Import (server/ui/xero-import.js): Review compares one fact per row
|
|
1365
1552
|
with ✓ or ≠ between the sides; the list and history tick rows for the bulk
|
|
1366
1553
|
bar. */
|
|
1367
|
-
.fx-xi { display: flex; flex-direction: column; gap: 16px; min-width: 0;
|
|
1554
|
+
.fx-xi { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
|
|
1368
1555
|
.fx-xi .fx-lucide { width: 16px; height: 16px; }
|
|
1369
1556
|
.fx-xi__views { display: inline-flex; border: 1px solid var(--fx-line); }
|
|
1370
1557
|
.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: '
|
|
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
|
|
2
|
-
//
|
|
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
|
-
|
|
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: '
|
|
28
|
-
{ key: '
|
|
29
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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: '
|
|
130
|
+
box('referenceTemplate', 'Reference rule', { placeholder: 'Type a reference rule' })),
|
|
108
131
|
].filter(Boolean));
|
|
109
132
|
})();
|
|
110
133
|
return section;
|