@visns-studio/visns-components 6.0.5 → 6.1.1
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/package.json +1 -1
- package/src/components/DataGrid.jsx +40 -0
- package/src/components/Navigation.jsx +59 -3
- package/src/components/auth/Login.jsx +2 -2
- package/src/components/generic/GenericDashboard.jsx +181 -1
- package/src/components/generic/GenericFormBuilder.jsx +374 -14
- package/src/components/generic/OutstandingRuleEditor.jsx +313 -0
- package/src/components/styles/GenericDashboard.module.scss +155 -0
- package/src/components/styles/GenericFormBuilder.module.scss +60 -0
- package/src/components/styles/Login.module.scss +29 -3
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import styles from '../styles/GenericFormBuilder.module.scss';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Per-question "outstanding item" rules.
|
|
6
|
+
*
|
|
7
|
+
* OPT-IN for consuming apps that model outstanding/defect follow-up items.
|
|
8
|
+
* Everything here is inert unless the app's builder config carries
|
|
9
|
+
* `outstandingItems: { enabled: true, ... }`, so no other project is affected.
|
|
10
|
+
*
|
|
11
|
+
* The stored schema is one optional per-field key, `outstanding_when`:
|
|
12
|
+
*
|
|
13
|
+
* key absent the app's own legacy convention applies (see legacyTriggerId)
|
|
14
|
+
* [] this question can never raise an outstanding item
|
|
15
|
+
* ['no-'] outstanding when the answer is this OPTION ID
|
|
16
|
+
*
|
|
17
|
+
* Option ids are stored, never labels, because editing an option label
|
|
18
|
+
* regenerates its id — exactly as `conditional_value` does.
|
|
19
|
+
*
|
|
20
|
+
* The UI is a single "Outstanding" switch on each row of the field's existing
|
|
21
|
+
* options editor, with radio semantics: at most one answer may be marked.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** Field types the editor offers when the config does not name any. */
|
|
25
|
+
export const DEFAULT_OUTSTANDING_TYPES = ['checkbox'];
|
|
26
|
+
|
|
27
|
+
/** Types the legacy (unconfigured) convention applies to. */
|
|
28
|
+
export const LEGACY_OUTSTANDING_TYPES = ['checkbox'];
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The one label whose sense is inverted under the legacy convention: a "Yes"
|
|
32
|
+
* here means there IS something else affecting the build.
|
|
33
|
+
*
|
|
34
|
+
* Mirrors App\Support\OutstandingRule::SPECIAL_LABEL / ::SPECIAL_SIMILARITY.
|
|
35
|
+
*/
|
|
36
|
+
export const SPECIAL_LABEL =
|
|
37
|
+
'Other features affecting the build (provide details and photos)';
|
|
38
|
+
export const SPECIAL_SIMILARITY = 85;
|
|
39
|
+
|
|
40
|
+
// --------------------------------------------------------------- similar_text
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Longest common substring, PHP's php_similar_str() — first-found wins on ties,
|
|
44
|
+
* scanning the first string outermost.
|
|
45
|
+
*/
|
|
46
|
+
const similarStr = (s1, s2) => {
|
|
47
|
+
let max = 0;
|
|
48
|
+
let count = 0;
|
|
49
|
+
let pos1 = 0;
|
|
50
|
+
let pos2 = 0;
|
|
51
|
+
|
|
52
|
+
for (let p = 0; p < s1.length; p++) {
|
|
53
|
+
for (let q = 0; q < s2.length; q++) {
|
|
54
|
+
let l = 0;
|
|
55
|
+
while (
|
|
56
|
+
p + l < s1.length &&
|
|
57
|
+
q + l < s2.length &&
|
|
58
|
+
s1[p + l] === s2[q + l]
|
|
59
|
+
) {
|
|
60
|
+
l++;
|
|
61
|
+
}
|
|
62
|
+
if (l > max) {
|
|
63
|
+
max = l;
|
|
64
|
+
count++;
|
|
65
|
+
pos1 = p;
|
|
66
|
+
pos2 = q;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { pos1, pos2, max, count };
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** PHP's php_similar_char(): the common substring, then recurse either side. */
|
|
75
|
+
const similarChar = (s1, s2) => {
|
|
76
|
+
const { pos1, pos2, max, count } = similarStr(s1, s2);
|
|
77
|
+
let sum = max;
|
|
78
|
+
|
|
79
|
+
if (sum) {
|
|
80
|
+
if (pos1 && pos2 && count > 1) {
|
|
81
|
+
sum += similarChar(s1.slice(0, pos1), s2.slice(0, pos2));
|
|
82
|
+
}
|
|
83
|
+
if (pos1 + max < s1.length && pos2 + max < s2.length) {
|
|
84
|
+
sum += similarChar(s1.slice(pos1 + max), s2.slice(pos2 + max));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return sum;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* PHP's similar_text($first, $second, $percent) percentage.
|
|
93
|
+
*
|
|
94
|
+
* Verified byte-for-byte against PHP 8.5 over the real label corpus plus 400
|
|
95
|
+
* random strings. The argument order is load-bearing — the function is not
|
|
96
|
+
* symmetric — and must match the backend's call exactly.
|
|
97
|
+
*
|
|
98
|
+
* (PHP compares bytes; this compares UTF-16 code units. Identical for the
|
|
99
|
+
* ASCII labels in use, and only ever used against an ASCII constant.)
|
|
100
|
+
*/
|
|
101
|
+
export const similarTextPercent = (first, second) => {
|
|
102
|
+
const s1 = typeof first === 'string' ? first : '';
|
|
103
|
+
const s2 = typeof second === 'string' ? second : '';
|
|
104
|
+
const total = s1.length + s2.length;
|
|
105
|
+
|
|
106
|
+
if (total === 0) return 0;
|
|
107
|
+
|
|
108
|
+
return (similarChar(s1, s2) * 2 * 100) / total;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------- the config
|
|
112
|
+
|
|
113
|
+
export const outstandingTypesOf = (config) => {
|
|
114
|
+
const types = config?.types;
|
|
115
|
+
return Array.isArray(types) ? types : DEFAULT_OUTSTANDING_TYPES;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/** The single gate: config must enable it AND the type must store one option id. */
|
|
119
|
+
export const isOutstandingConfigurable = (config, fieldType) =>
|
|
120
|
+
!!(config && config.enabled) &&
|
|
121
|
+
outstandingTypesOf(config).includes(fieldType);
|
|
122
|
+
|
|
123
|
+
// ------------------------------------------------------------- the rule state
|
|
124
|
+
|
|
125
|
+
const optionsOf = (field) =>
|
|
126
|
+
(Array.isArray(field?.options) ? field.options : []).filter(Boolean);
|
|
127
|
+
|
|
128
|
+
const storedIds = (field) =>
|
|
129
|
+
Array.isArray(field?.outstanding_when) ? field.outstanding_when : null;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The legacy trigger for a field: 'yes-' for the special "Other features
|
|
133
|
+
* affecting the build" question, 'no-' for any other checkbox, and null for
|
|
134
|
+
* every other type (the legacy convention never reached them).
|
|
135
|
+
*
|
|
136
|
+
* Mirrors App\Support\OutstandingRule::legacyTrigger().
|
|
137
|
+
*/
|
|
138
|
+
export const legacyTriggerId = (field) => {
|
|
139
|
+
if (!LEGACY_OUTSTANDING_TYPES.includes(field?.type)) return null;
|
|
140
|
+
|
|
141
|
+
return similarTextPercent(field?.label, SPECIAL_LABEL) >=
|
|
142
|
+
SPECIAL_SIMILARITY
|
|
143
|
+
? 'yes-'
|
|
144
|
+
: 'no-';
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* What the legacy convention would show as toggled, which is only meaningful
|
|
149
|
+
* when the field actually offers that answer. A checkbox with no 'no-' option
|
|
150
|
+
* (or no options yet) has no legacy toggle, and the backend agrees: its trigger
|
|
151
|
+
* can never match a stored value.
|
|
152
|
+
*/
|
|
153
|
+
export const legacyDefaultOptionId = (field) => {
|
|
154
|
+
const trigger = legacyTriggerId(field);
|
|
155
|
+
if (!trigger) return null;
|
|
156
|
+
|
|
157
|
+
return optionsOf(field).some((option) => option.id === trigger)
|
|
158
|
+
? trigger
|
|
159
|
+
: null;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Which option should show toggled when the modal opens.
|
|
164
|
+
*
|
|
165
|
+
* Configured → the first stored id the field still offers. (No template today
|
|
166
|
+
* carries more than one; a multi-entry array shows its first and is normalised
|
|
167
|
+
* to a single id on save.) An explicit `[]` shows nothing toggled.
|
|
168
|
+
* Unconfigured → whatever the legacy convention would flag.
|
|
169
|
+
*/
|
|
170
|
+
export const deriveOutstandingSelection = (field) => {
|
|
171
|
+
const stored = storedIds(field);
|
|
172
|
+
|
|
173
|
+
if (stored) {
|
|
174
|
+
const options = optionsOf(field);
|
|
175
|
+
return (
|
|
176
|
+
stored.find((id) => options.some((option) => option.id === id)) ??
|
|
177
|
+
null
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return legacyDefaultOptionId(field);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/** Stored ids the field no longer offers — the rename/delete hazard. */
|
|
185
|
+
export const staleOutstandingIds = (field) => {
|
|
186
|
+
const stored = storedIds(field);
|
|
187
|
+
if (!stored) return [];
|
|
188
|
+
|
|
189
|
+
const options = optionsOf(field);
|
|
190
|
+
return stored.filter((id) => !options.some((option) => option.id === id));
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Fold the toggle state back into the field.
|
|
195
|
+
*
|
|
196
|
+
* The key is written only when the outcome differs from what the legacy
|
|
197
|
+
* convention would produce, so a field nobody configured never gains it just
|
|
198
|
+
* because its modal was opened and saved:
|
|
199
|
+
*
|
|
200
|
+
* selection === legacy default → key deleted (including both being null)
|
|
201
|
+
* an answer toggled → ['<id>']
|
|
202
|
+
* nothing toggled, legacy would → [] (an explicit "never")
|
|
203
|
+
*
|
|
204
|
+
* A stale stored id is not toggleable, so saving normalises it to [] (checkbox)
|
|
205
|
+
* or an absent key (dropdown) — both meaning "never flags", which is precisely
|
|
206
|
+
* what an id no answer can produce already meant.
|
|
207
|
+
*/
|
|
208
|
+
export const applyOutstandingRule = (field, selectedId, config) => {
|
|
209
|
+
if (!isOutstandingConfigurable(config, field?.type)) return field;
|
|
210
|
+
|
|
211
|
+
const selection = selectedId || null;
|
|
212
|
+
const next = { ...field };
|
|
213
|
+
|
|
214
|
+
if (selection === legacyDefaultOptionId(field)) {
|
|
215
|
+
delete next.outstanding_when;
|
|
216
|
+
} else {
|
|
217
|
+
next.outstanding_when = selection ? [selection] : [];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return next;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* One-line summary for field-list chips and tooltips. Returns null when the
|
|
225
|
+
* field carries no rule, so callers can skip falsy results.
|
|
226
|
+
*/
|
|
227
|
+
export const summariseOutstandingRule = (field) => {
|
|
228
|
+
const stored = storedIds(field);
|
|
229
|
+
if (!stored) return null;
|
|
230
|
+
if (stored.length === 0) return 'Never outstanding';
|
|
231
|
+
|
|
232
|
+
const options = optionsOf(field);
|
|
233
|
+
const labels = stored.map((id) => {
|
|
234
|
+
const match = options.find((option) => option.id === id);
|
|
235
|
+
return match ? match.label || match.id : `${id} (missing)`;
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
return `Outstanding: ${labels.join(', ')}`;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Remap a stored id after an option's id was regenerated from an edited label.
|
|
243
|
+
* Returns the field untouched (same reference) when there is nothing to remap.
|
|
244
|
+
*/
|
|
245
|
+
export const remapOutstandingWhen = (field, previousId, nextId) => {
|
|
246
|
+
const stored = storedIds(field);
|
|
247
|
+
|
|
248
|
+
if (!stored || previousId === nextId) return field;
|
|
249
|
+
if (!stored.includes(previousId)) return field;
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
...field,
|
|
253
|
+
outstanding_when: stored.map((id) => (id === previousId ? nextId : id)),
|
|
254
|
+
};
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// ------------------------------------------------------------------------ UI
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* The per-row switch. Radio semantics live in the caller: `onToggle` receives
|
|
261
|
+
* the id to select, or null when the row was switched off.
|
|
262
|
+
*/
|
|
263
|
+
export function OutstandingOptionToggle({ optionId, label, checked, onToggle }) {
|
|
264
|
+
return (
|
|
265
|
+
<label
|
|
266
|
+
className={`${styles.outstandingFlag} ${
|
|
267
|
+
checked ? styles.outstandingFlagOn : ''
|
|
268
|
+
}`}
|
|
269
|
+
title={
|
|
270
|
+
checked
|
|
271
|
+
? `An answer of "${label}" raises an outstanding item`
|
|
272
|
+
: `Mark "${label}" as the answer that raises an outstanding item`
|
|
273
|
+
}
|
|
274
|
+
>
|
|
275
|
+
<input
|
|
276
|
+
type="checkbox"
|
|
277
|
+
checked={checked}
|
|
278
|
+
onChange={() => onToggle(checked ? null : optionId)}
|
|
279
|
+
/>
|
|
280
|
+
<span>Outstanding</span>
|
|
281
|
+
</label>
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* The hint shown once above the option rows, plus the amber stale-reference
|
|
287
|
+
* warning. Renders nothing when there is nothing to say.
|
|
288
|
+
*/
|
|
289
|
+
export function OutstandingRuleHint({ config, staleIds = [] }) {
|
|
290
|
+
const help = config?.help;
|
|
291
|
+
|
|
292
|
+
if (!help && staleIds.length === 0) return null;
|
|
293
|
+
|
|
294
|
+
return (
|
|
295
|
+
<>
|
|
296
|
+
{help ? (
|
|
297
|
+
<div className={styles.outstandingHint}>{help}</div>
|
|
298
|
+
) : null}
|
|
299
|
+
|
|
300
|
+
{staleIds.length > 0 ? (
|
|
301
|
+
<div className={styles.outstandingStale}>
|
|
302
|
+
{staleIds.length === 1
|
|
303
|
+
? 'The answer this question’s outstanding rule refers to no longer exists'
|
|
304
|
+
: 'Answers this question’s outstanding rule refers to no longer exist'}{' '}
|
|
305
|
+
({staleIds.join(', ')}) — editing an answer’s label gives it
|
|
306
|
+
a new id. Set the switch on the right answer, or leave every
|
|
307
|
+
switch off to mean “never outstanding”. Saving tidies this
|
|
308
|
+
up.
|
|
309
|
+
</div>
|
|
310
|
+
) : null}
|
|
311
|
+
</>
|
|
312
|
+
);
|
|
313
|
+
}
|
|
@@ -814,3 +814,158 @@
|
|
|
814
814
|
color: #888;
|
|
815
815
|
padding: 20px;
|
|
816
816
|
}
|
|
817
|
+
|
|
818
|
+
/* ---------------------------------------------------------------------------
|
|
819
|
+
Loading skeletons
|
|
820
|
+
Neutral greys only (no brand colours) so every tenant gets a safe default.
|
|
821
|
+
Keyframes are uniquely prefixed to avoid colliding with host app globals.
|
|
822
|
+
--------------------------------------------------------------------------- */
|
|
823
|
+
@keyframes visns-dash-skeleton-sweep {
|
|
824
|
+
0% {
|
|
825
|
+
background-position: 150% 0;
|
|
826
|
+
}
|
|
827
|
+
100% {
|
|
828
|
+
background-position: -150% 0;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
.skeleton {
|
|
833
|
+
width: 100%;
|
|
834
|
+
box-sizing: border-box;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
.skeletonBlock {
|
|
838
|
+
display: block;
|
|
839
|
+
box-sizing: border-box;
|
|
840
|
+
border-radius: var(--br, 5px);
|
|
841
|
+
background-color: rgba(0, 0, 0, 0.06);
|
|
842
|
+
background-image: linear-gradient(
|
|
843
|
+
90deg,
|
|
844
|
+
rgba(0, 0, 0, 0) 0%,
|
|
845
|
+
rgba(0, 0, 0, 0.05) 50%,
|
|
846
|
+
rgba(0, 0, 0, 0) 100%
|
|
847
|
+
);
|
|
848
|
+
background-repeat: no-repeat;
|
|
849
|
+
background-size: 150% 100%;
|
|
850
|
+
animation: visns-dash-skeleton-sweep 1.4s ease-in-out infinite;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/* Counter: matches .widgetNo (2rem bottom padding, 3em value) */
|
|
854
|
+
.skeletonCounter {
|
|
855
|
+
width: 100%;
|
|
856
|
+
padding: 0 0 2rem 0;
|
|
857
|
+
box-sizing: border-box;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
.skeletonCounterValue {
|
|
861
|
+
width: 55%;
|
|
862
|
+
max-width: 220px;
|
|
863
|
+
height: 3.6rem;
|
|
864
|
+
margin: 0 auto;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/* Counter button: matches .widgetLink .btn */
|
|
868
|
+
.skeletonButtonRow {
|
|
869
|
+
width: 100%;
|
|
870
|
+
display: block;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
.skeletonButton {
|
|
874
|
+
width: 180px;
|
|
875
|
+
max-width: 100%;
|
|
876
|
+
height: 34px;
|
|
877
|
+
margin: 0 auto;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/* Charts: the wrapper takes widget.height (or the 600px chart default) inline */
|
|
881
|
+
.skeletonChart {
|
|
882
|
+
width: 100%;
|
|
883
|
+
display: flex;
|
|
884
|
+
flex-direction: column;
|
|
885
|
+
align-items: center;
|
|
886
|
+
justify-content: center;
|
|
887
|
+
box-sizing: border-box;
|
|
888
|
+
padding: 20px 0;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
.skeletonChartStrips {
|
|
892
|
+
align-items: flex-start;
|
|
893
|
+
gap: 14px;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
.skeletonStrip {
|
|
897
|
+
height: 16px;
|
|
898
|
+
min-height: 16px;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
.skeletonCircle {
|
|
902
|
+
width: 60%;
|
|
903
|
+
max-width: 260px;
|
|
904
|
+
aspect-ratio: 1 / 1;
|
|
905
|
+
border-radius: 50%;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/* Table: header 44px / rows 38px, matching .table-container th and td */
|
|
909
|
+
.skeletonTable {
|
|
910
|
+
width: 100%;
|
|
911
|
+
display: flex;
|
|
912
|
+
flex-direction: column;
|
|
913
|
+
gap: 1px;
|
|
914
|
+
box-sizing: border-box;
|
|
915
|
+
border: 1px solid #dadce0;
|
|
916
|
+
border-radius: var(--br, 5px);
|
|
917
|
+
overflow: hidden;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
.skeletonTableHeader {
|
|
921
|
+
width: 100%;
|
|
922
|
+
height: 44px;
|
|
923
|
+
border-radius: 0;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
.skeletonTableRow {
|
|
927
|
+
width: 100%;
|
|
928
|
+
height: 38px;
|
|
929
|
+
border-radius: 0;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/* List: rows sized like .dashList li button */
|
|
933
|
+
.skeletonList {
|
|
934
|
+
width: 100%;
|
|
935
|
+
display: flex;
|
|
936
|
+
flex-direction: column;
|
|
937
|
+
gap: 1px;
|
|
938
|
+
font-size: 0.875rem;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
.skeletonListRow {
|
|
942
|
+
width: 100%;
|
|
943
|
+
height: 30px;
|
|
944
|
+
border-radius: 0;
|
|
945
|
+
|
|
946
|
+
&:first-child {
|
|
947
|
+
border-top-left-radius: var(--br, 5px);
|
|
948
|
+
border-top-right-radius: var(--br, 5px);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
&:last-child {
|
|
952
|
+
border-bottom-left-radius: var(--br, 5px);
|
|
953
|
+
border-bottom-right-radius: var(--br, 5px);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
.skeletonTimeline {
|
|
958
|
+
width: 100%;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
.skeletonGeneric {
|
|
962
|
+
width: 100%;
|
|
963
|
+
height: 120px;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
/* Keep the shape, drop the motion */
|
|
967
|
+
@media (prefers-reduced-motion: reduce) {
|
|
968
|
+
.skeletonBlock {
|
|
969
|
+
animation: none;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
@@ -2275,6 +2275,66 @@ select:not(:placeholder-shown) + .fi__span {
|
|
|
2275
2275
|
}
|
|
2276
2276
|
}
|
|
2277
2277
|
|
|
2278
|
+
// Outstanding item rule — a per-answer switch inside the options editor, plus
|
|
2279
|
+
// its hint and stale-reference warning. All of it renders only when the
|
|
2280
|
+
// consuming app enables `outstandingItems` in its builder config.
|
|
2281
|
+
.outstandingHint {
|
|
2282
|
+
font-size: 0.78rem;
|
|
2283
|
+
line-height: 1.45;
|
|
2284
|
+
color: rgba(var(--paragraph-color-rgb), 0.65);
|
|
2285
|
+
padding: 0 0 6px;
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
.outstandingStale {
|
|
2289
|
+
background: #fff8e1;
|
|
2290
|
+
border: 1px solid #f4c544;
|
|
2291
|
+
border-radius: 6px;
|
|
2292
|
+
padding: 8px 12px;
|
|
2293
|
+
margin-bottom: 6px;
|
|
2294
|
+
color: #7a5c00;
|
|
2295
|
+
font-size: 0.8rem;
|
|
2296
|
+
line-height: 1.45;
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
.outstandingFlag {
|
|
2300
|
+
flex-shrink: 0;
|
|
2301
|
+
display: inline-flex;
|
|
2302
|
+
align-items: center;
|
|
2303
|
+
gap: 5px;
|
|
2304
|
+
padding: 4px 9px;
|
|
2305
|
+
border: 1px solid rgba(var(--primary-rgb), 0.15);
|
|
2306
|
+
border-radius: 6px;
|
|
2307
|
+
background: white;
|
|
2308
|
+
font-size: 0.72rem;
|
|
2309
|
+
font-weight: 600;
|
|
2310
|
+
text-transform: uppercase;
|
|
2311
|
+
letter-spacing: 0.04em;
|
|
2312
|
+
color: rgba(var(--paragraph-color-rgb), 0.45);
|
|
2313
|
+
cursor: pointer;
|
|
2314
|
+
user-select: none;
|
|
2315
|
+
transition: all 0.2s ease;
|
|
2316
|
+
|
|
2317
|
+
&:hover {
|
|
2318
|
+
border-color: var(--primary-color);
|
|
2319
|
+
color: var(--paragraph-color);
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
// The modal forces a 44px min-height on every input; a tick box must opt out.
|
|
2323
|
+
input[type='checkbox'] {
|
|
2324
|
+
margin: 0;
|
|
2325
|
+
width: auto !important;
|
|
2326
|
+
height: auto !important;
|
|
2327
|
+
min-height: 0 !important;
|
|
2328
|
+
cursor: pointer;
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
.outstandingFlagOn {
|
|
2333
|
+
border-color: #e0a800;
|
|
2334
|
+
background: #fff8e1;
|
|
2335
|
+
color: #7a5c00;
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2278
2338
|
.btnCancel {
|
|
2279
2339
|
background: rgba(var(--paragraph-color-rgb), 0.08) !important;
|
|
2280
2340
|
color: var(--paragraph-color) !important;
|
|
@@ -52,6 +52,17 @@
|
|
|
52
52
|
// A scrim anchored to the bottom-left, where the copy sits — not a flat
|
|
53
53
|
// wash over the whole image, which would dull the dusk light that makes
|
|
54
54
|
// the photograph worth using.
|
|
55
|
+
//
|
|
56
|
+
// The foot is a near-solid plate rather than a fade. The brand line has to
|
|
57
|
+
// hold against whatever the photograph is doing behind it, and this one is
|
|
58
|
+
// bright exactly where the type sits, which left "Prime Builders" washing
|
|
59
|
+
// into the render. Held high — 0.88 still at 18%, which is where the ink
|
|
60
|
+
// block ends — so the whole block reads on one ground, then released
|
|
61
|
+
// quickly to 0.06 at the top, so the sky above is still a photograph and
|
|
62
|
+
// not a tint.
|
|
63
|
+
//
|
|
64
|
+
// Shared verbatim with the supervisor PWA
|
|
65
|
+
// (prime-web-nextjs/app/login/Login.module.scss). If one moves, move both.
|
|
55
66
|
&::after {
|
|
56
67
|
content: '';
|
|
57
68
|
position: absolute;
|
|
@@ -59,9 +70,11 @@
|
|
|
59
70
|
background:
|
|
60
71
|
linear-gradient(
|
|
61
72
|
to top,
|
|
62
|
-
rgba(16, 25, 42, 0.
|
|
63
|
-
rgba(16, 25, 42, 0.
|
|
64
|
-
rgba(16, 25, 42, 0.
|
|
73
|
+
rgba(16, 25, 42, 0.94) 0%,
|
|
74
|
+
rgba(16, 25, 42, 0.88) 18%,
|
|
75
|
+
rgba(16, 25, 42, 0.62) 42%,
|
|
76
|
+
rgba(16, 25, 42, 0.22) 70%,
|
|
77
|
+
rgba(16, 25, 42, 0.06) 100%
|
|
65
78
|
),
|
|
66
79
|
linear-gradient(
|
|
67
80
|
to right,
|
|
@@ -90,6 +103,11 @@
|
|
|
90
103
|
font-size: 0.72rem;
|
|
91
104
|
font-weight: 700;
|
|
92
105
|
letter-spacing: 0.22em;
|
|
106
|
+
// Stated, not left to the cascade: global.css sets
|
|
107
|
+
// `p { line-height: var(--para-height) }`, which is 1.6 here. That put the
|
|
108
|
+
// kicker on a taller line than the same kicker in the PWA, so the red dash
|
|
109
|
+
// sat at a different height and the gap beneath it read as wider.
|
|
110
|
+
line-height: 1.4;
|
|
93
111
|
text-transform: uppercase;
|
|
94
112
|
color: rgba(255, 255, 255, 0.75);
|
|
95
113
|
// The single use of the logo's red in the whole screen.
|
|
@@ -114,6 +132,10 @@
|
|
|
114
132
|
line-height: 1;
|
|
115
133
|
// Tight tracking at display size — Barlow opens up as it scales.
|
|
116
134
|
letter-spacing: -0.02em;
|
|
135
|
+
// Stated rather than inherited from .brandInk: this is the one line on the
|
|
136
|
+
// screen that has to be white, and finding that out should not mean
|
|
137
|
+
// reading the parent.
|
|
138
|
+
color: #fff;
|
|
117
139
|
text-wrap: balance;
|
|
118
140
|
}
|
|
119
141
|
|
|
@@ -123,6 +145,10 @@
|
|
|
123
145
|
font-size: 1rem;
|
|
124
146
|
font-weight: 300;
|
|
125
147
|
line-height: 1.5;
|
|
148
|
+
// No-op against this app's globals, but the PWA's own `p` rule tracks body
|
|
149
|
+
// copy at 0.015rem, so the value is pinned in both files rather than left
|
|
150
|
+
// to whichever global layer the screen happens to be rendered under.
|
|
151
|
+
letter-spacing: 0;
|
|
126
152
|
color: rgba(255, 255, 255, 0.82);
|
|
127
153
|
}
|
|
128
154
|
|