@visns-studio/visns-components 6.4.3 → 6.5.0
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 +16 -1
- package/src/components/Field.jsx +21 -21
- package/src/components/Navigation.jsx +201 -41
- package/src/components/columns/NativeDateFilterEditor.jsx +434 -0
- package/src/components/styles/Field.module.scss +25 -3
- package/src/components/styles/Navigation.module.scss +184 -0
- package/src/components/styles/Notification.module.scss +83 -0
- package/src/components/styles/SwitchAccount.module.scss +22 -0
- package/src/components/styles/global-datagrid.css +141 -0
- package/src/components/styles/global.css +3 -1
- package/src/index.js +2 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
import {
|
|
2
|
+
forwardRef,
|
|
3
|
+
useCallback,
|
|
4
|
+
useEffect,
|
|
5
|
+
useImperativeHandle,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
} from 'react';
|
|
10
|
+
import moment from 'moment';
|
|
11
|
+
import { X } from 'lucide-react';
|
|
12
|
+
import { toNativeInputValue } from '../utils/nativeDateInput';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Native (`<input type="date">`) replacement for the datagrid's popup
|
|
16
|
+
* calendar column filter.
|
|
17
|
+
*
|
|
18
|
+
* The grid ships `DateFilter` from `@visns-studio/visns-datagrid-*`, which
|
|
19
|
+
* renders an Inovua `DateInput` — a text box plus its own floating calendar.
|
|
20
|
+
* That popup is the last remaining non-native date picker in the app: it is
|
|
21
|
+
* clipped inside scrolling grids, its day cells are far below a comfortable
|
|
22
|
+
* tap target, and it duplicates something every browser now does well. This
|
|
23
|
+
* editor renders the platform control instead, on every viewport.
|
|
24
|
+
*
|
|
25
|
+
* ---------------------------------------------------------------------------
|
|
26
|
+
* The filter-editor contract (community-edition
|
|
27
|
+
* Layout/ColumnLayout/Cell/renderFilter/index.tsx -> `GenericFilter.render`):
|
|
28
|
+
*
|
|
29
|
+
* filterValue { name, operator, type, value, ... } — never mutate it
|
|
30
|
+
* onChange called with a WHOLE filterValue object, `value` replaced
|
|
31
|
+
* emptyValue the filter type's "no filter" value (`''` for `date`)
|
|
32
|
+
* filterDelay ms to debounce before calling onChange (grid default 250)
|
|
33
|
+
* render REQUIRED wrapper — the editor must return `render(node)`,
|
|
34
|
+
* which supplies the filter-row chrome and settings icon
|
|
35
|
+
* cellProps the column's props (may carry `dateFormat`)
|
|
36
|
+
* filterEditorProps object, or `(props, { value, index }) => object`
|
|
37
|
+
* ref receives the editor instance; the grid calls `setValue()`
|
|
38
|
+
* on it (e.g. DataGrid's dependent-filter reset) and reads
|
|
39
|
+
* `getInputRef()`
|
|
40
|
+
* theme / rtl / readOnly / disabled / style / i18n / cell / renderInPortal
|
|
41
|
+
*
|
|
42
|
+
* ---------------------------------------------------------------------------
|
|
43
|
+
* Value shape — byte-identical to `DateFilter`.
|
|
44
|
+
*
|
|
45
|
+
* `DateInput` emits `dateMoment.format(dateFormat)` and `''` for a cleared
|
|
46
|
+
* value (packages/Calendar/src/DateInput/index.tsx `format()`), so with the
|
|
47
|
+
* `dateFormat: 'DD-MM-YYYY'` that DataGrid configures, the grid — and
|
|
48
|
+
* therefore the `where[]` entry posted to the server — has always carried a
|
|
49
|
+
* string like `'02-12-2025'`, and `''` once cleared. This editor formats with
|
|
50
|
+
* the same resolved `dateFormat` and clears to the same `emptyValue`, so no
|
|
51
|
+
* server-side filtering changes.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
// Moment tokens that mean "this format carries a time of day". Escaped
|
|
55
|
+
// literals (`[at]`) are stripped first so they cannot produce a false match.
|
|
56
|
+
const TIME_TOKENS = /[HhmsSAa]/;
|
|
57
|
+
|
|
58
|
+
const formatHasTime = (format) =>
|
|
59
|
+
typeof format === 'string' &&
|
|
60
|
+
TIME_TOKENS.test(format.replace(/\[[^\]]*\]/g, ''));
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Resolve the display format the same way `DateFilter` does: an explicit
|
|
64
|
+
* `cellProps.dateFormat` wins, otherwise `filterEditorProps` — which DataGrid
|
|
65
|
+
* passes as a function — is consulted.
|
|
66
|
+
*
|
|
67
|
+
* @returns {string} '' when nothing is configured, which is what
|
|
68
|
+
* `DateInput.format()` falls back on (moment then uses its
|
|
69
|
+
* own ISO default).
|
|
70
|
+
*/
|
|
71
|
+
const resolveDateFormat = (props, value, index) => {
|
|
72
|
+
const { cellProps } = props;
|
|
73
|
+
|
|
74
|
+
if (cellProps && cellProps.dateFormat !== undefined) {
|
|
75
|
+
return cellProps.dateFormat;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return resolveEditorProps(props, value, index).dateFormat || '';
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* `filterEditorProps` may be an object or, as DataGrid passes it for date
|
|
83
|
+
* filters, a `(props, { value, index }) => object` factory. DateFilter also
|
|
84
|
+
* falls back to the copy carried on `filterValue`, so this does too.
|
|
85
|
+
*/
|
|
86
|
+
function resolveEditorProps(props, value, index) {
|
|
87
|
+
let { filterEditorProps } = props;
|
|
88
|
+
|
|
89
|
+
if (filterEditorProps === undefined) {
|
|
90
|
+
filterEditorProps = props.filterValue && props.filterValue.filterEditorProps;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (typeof filterEditorProps === 'function') {
|
|
94
|
+
return filterEditorProps(props, { value, index }) || {};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return filterEditorProps || {};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* A stored filter value (formatted with `dateFormat`) -> the string the
|
|
102
|
+
* native input wants ('YYYY-MM-DD' / 'YYYY-MM-DDTHH:mm').
|
|
103
|
+
*/
|
|
104
|
+
const toInputValue = (value, dateFormat, withTime) => {
|
|
105
|
+
if (value === null || value === undefined || value === '') {
|
|
106
|
+
return '';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Strict against the configured format first — '02-12-2025' parsed
|
|
110
|
+
// loosely would be read as an ISO-ish string and come back wrong.
|
|
111
|
+
let parsed = dateFormat ? moment(value, dateFormat, true) : null;
|
|
112
|
+
|
|
113
|
+
if (!parsed || !parsed.isValid()) {
|
|
114
|
+
parsed = moment(value);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!parsed.isValid()) {
|
|
118
|
+
return '';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return toNativeInputValue(parsed, withTime);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The native input's raw string -> the value the grid and the server expect.
|
|
126
|
+
* Cleared / partially-typed input yields `emptyValue`, which `loadData` in
|
|
127
|
+
* DataGrid.jsx drops from the posted `where[]` so the grid shows every row.
|
|
128
|
+
*/
|
|
129
|
+
const fromInputValue = (raw, dateFormat, withTime, emptyValue) => {
|
|
130
|
+
if (!raw) {
|
|
131
|
+
return emptyValue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// `datetime-local` normally yields 'YYYY-MM-DDTHH:mm', but a non-default
|
|
135
|
+
// `step` makes browsers append seconds — accept both.
|
|
136
|
+
const parsed = withTime
|
|
137
|
+
? moment(raw, ['YYYY-MM-DD[T]HH:mm', 'YYYY-MM-DD[T]HH:mm:ss'], true)
|
|
138
|
+
: moment(raw, 'YYYY-MM-DD', true);
|
|
139
|
+
|
|
140
|
+
if (!parsed.isValid()) {
|
|
141
|
+
return emptyValue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// `format()` with no argument is moment's ISO default — exactly what
|
|
145
|
+
// `DateInput.format()` produces when no dateFormat is configured.
|
|
146
|
+
return dateFormat ? parsed.format(dateFormat) : parsed.format();
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const isRangeOperator = (operator) =>
|
|
150
|
+
operator === 'inrange' || operator === 'notinrange';
|
|
151
|
+
|
|
152
|
+
const WRAPPER_CLASS =
|
|
153
|
+
'InovuaReactDataGrid__column-header__filter ' +
|
|
154
|
+
'InovuaReactDataGrid__column-header__filter--date ' +
|
|
155
|
+
'visns-native-date-filter';
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* One `<input type="date">` (or `datetime-local`) dressed as a grid filter,
|
|
159
|
+
* with the clear affordance the popup calendar used to provide — Chrome's
|
|
160
|
+
* date input has no clear button of its own.
|
|
161
|
+
*/
|
|
162
|
+
const NativeDateInput = ({
|
|
163
|
+
inputValue,
|
|
164
|
+
withTime,
|
|
165
|
+
disabled,
|
|
166
|
+
readOnly,
|
|
167
|
+
placeholder,
|
|
168
|
+
style,
|
|
169
|
+
onValueChange,
|
|
170
|
+
inputRef,
|
|
171
|
+
...rest
|
|
172
|
+
}) => (
|
|
173
|
+
<span
|
|
174
|
+
className={`${WRAPPER_CLASS}${
|
|
175
|
+
inputValue ? '' : ' visns-native-date-filter--empty'
|
|
176
|
+
}${disabled ? ' visns-native-date-filter--disabled' : ''}`}
|
|
177
|
+
style={{ minWidth: 0, ...style }}
|
|
178
|
+
>
|
|
179
|
+
<input
|
|
180
|
+
{...rest}
|
|
181
|
+
ref={inputRef}
|
|
182
|
+
type={withTime ? 'datetime-local' : 'date'}
|
|
183
|
+
className="visns-native-date-filter__input"
|
|
184
|
+
value={inputValue}
|
|
185
|
+
disabled={disabled}
|
|
186
|
+
readOnly={readOnly}
|
|
187
|
+
// `<input type="date">` has no placeholder of its own — the
|
|
188
|
+
// browser always shows its locale mask — so the configured
|
|
189
|
+
// "Select Date" text becomes the accessible name instead.
|
|
190
|
+
aria-label={placeholder || undefined}
|
|
191
|
+
title={placeholder || undefined}
|
|
192
|
+
autoComplete="off"
|
|
193
|
+
onChange={(event) => onValueChange(event.target.value)}
|
|
194
|
+
/>
|
|
195
|
+
{inputValue && !disabled && !readOnly ? (
|
|
196
|
+
<button
|
|
197
|
+
type="button"
|
|
198
|
+
aria-label="Clear filter"
|
|
199
|
+
className="visns-native-date-filter__clear"
|
|
200
|
+
onClick={() => onValueChange('')}
|
|
201
|
+
>
|
|
202
|
+
<X size={14} />
|
|
203
|
+
</button>
|
|
204
|
+
) : null}
|
|
205
|
+
</span>
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const NativeDateFilterEditor = forwardRef((props, ref) => {
|
|
209
|
+
const {
|
|
210
|
+
filterValue,
|
|
211
|
+
onChange,
|
|
212
|
+
filterDelay,
|
|
213
|
+
emptyValue,
|
|
214
|
+
readOnly,
|
|
215
|
+
disabled,
|
|
216
|
+
style,
|
|
217
|
+
render,
|
|
218
|
+
} = props;
|
|
219
|
+
|
|
220
|
+
// `''` is the `date` filter type's emptyValue (community-edition
|
|
221
|
+
// filterTypes.ts) — fall back to it so a grid without the prop still
|
|
222
|
+
// clears to what DateFilter emitted.
|
|
223
|
+
const noValue = emptyValue === undefined ? '' : emptyValue;
|
|
224
|
+
|
|
225
|
+
const range = isRangeOperator(filterValue && filterValue.operator);
|
|
226
|
+
|
|
227
|
+
const [value, setValue] = useState(() =>
|
|
228
|
+
filterValue ? filterValue.value || (range ? {} : '') : ''
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
const inputRef = useRef(null);
|
|
232
|
+
const timerRef = useRef(null);
|
|
233
|
+
const propValueRef = useRef(filterValue && filterValue.value);
|
|
234
|
+
|
|
235
|
+
// Keep the emitter fresh for the debounce timer without re-arming it.
|
|
236
|
+
const emitRef = useRef(null);
|
|
237
|
+
emitRef.current = (nextValue) => {
|
|
238
|
+
if (onChange) {
|
|
239
|
+
onChange({ ...filterValue, value: nextValue });
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* The popup calendar only ever emitted on an explicit pick, so it never
|
|
245
|
+
* needed `filterDelay`. A native input emits on every segment edit — Chrome
|
|
246
|
+
* reports 0002-12-02, 0020-12-02, 0202-12-02 while a year is typed — so the
|
|
247
|
+
* delay the grid already provides is honoured here, exactly as StringFilter
|
|
248
|
+
* does. The posted value is unchanged; only the moment of the request moves.
|
|
249
|
+
*/
|
|
250
|
+
const emit = useCallback(
|
|
251
|
+
(nextValue) => {
|
|
252
|
+
if (timerRef.current) {
|
|
253
|
+
clearTimeout(timerRef.current);
|
|
254
|
+
timerRef.current = null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (filterDelay && filterDelay >= 1) {
|
|
258
|
+
timerRef.current = setTimeout(() => {
|
|
259
|
+
timerRef.current = null;
|
|
260
|
+
emitRef.current(nextValue);
|
|
261
|
+
}, filterDelay);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
emitRef.current(nextValue);
|
|
266
|
+
},
|
|
267
|
+
[filterDelay]
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
useEffect(
|
|
271
|
+
() => () => {
|
|
272
|
+
if (timerRef.current) {
|
|
273
|
+
clearTimeout(timerRef.current);
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
[]
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
// Mirrors DateFilter.componentDidUpdate: adopt the incoming value only
|
|
280
|
+
// when the grid itself changed it (a reset, a saved view, a dependent
|
|
281
|
+
// filter clear), not on every render.
|
|
282
|
+
const incoming = filterValue ? filterValue.value : undefined;
|
|
283
|
+
|
|
284
|
+
useEffect(() => {
|
|
285
|
+
if (incoming !== propValueRef.current) {
|
|
286
|
+
propValueRef.current = incoming;
|
|
287
|
+
setValue(incoming || (range ? {} : ''));
|
|
288
|
+
}
|
|
289
|
+
}, [incoming, range]);
|
|
290
|
+
|
|
291
|
+
useImperativeHandle(
|
|
292
|
+
ref,
|
|
293
|
+
() => ({
|
|
294
|
+
// GenericFilter -> cellInstance.setFilterValue() reaches the
|
|
295
|
+
// editor through this; like StringFilter/DateFilter it updates
|
|
296
|
+
// the displayed value without re-emitting.
|
|
297
|
+
setValue: (next) => {
|
|
298
|
+
propValueRef.current = next;
|
|
299
|
+
setValue(next || (range ? {} : ''));
|
|
300
|
+
},
|
|
301
|
+
getInputRef: () => inputRef.current,
|
|
302
|
+
}),
|
|
303
|
+
[range]
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
const startFormat = useMemo(
|
|
307
|
+
() =>
|
|
308
|
+
resolveDateFormat(
|
|
309
|
+
props,
|
|
310
|
+
range ? value && value.start : value,
|
|
311
|
+
0
|
|
312
|
+
),
|
|
313
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
314
|
+
[props.cellProps, props.filterEditorProps, value, range]
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
const endFormat = useMemo(
|
|
318
|
+
() => (range ? resolveDateFormat(props, value && value.end, 1) : ''),
|
|
319
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
320
|
+
[props.cellProps, props.filterEditorProps, value, range]
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
const startEditorProps = resolveEditorProps(
|
|
324
|
+
props,
|
|
325
|
+
range ? value && value.start : value,
|
|
326
|
+
0
|
|
327
|
+
);
|
|
328
|
+
const endEditorProps = range
|
|
329
|
+
? resolveEditorProps(props, value && value.end, 1)
|
|
330
|
+
: {};
|
|
331
|
+
|
|
332
|
+
// datetime-local only when the configured format actually carries a time
|
|
333
|
+
// — DataGrid's date filters use 'DD-MM-YYYY', which does not, so they stay
|
|
334
|
+
// date-only and keep emitting a date-only string.
|
|
335
|
+
const startWithTime = formatHasTime(startFormat);
|
|
336
|
+
const endWithTime = formatHasTime(endFormat);
|
|
337
|
+
|
|
338
|
+
const handleChange = useCallback(
|
|
339
|
+
(raw, key, dateFormat, withTime) => {
|
|
340
|
+
const next = fromInputValue(raw, dateFormat, withTime, noValue);
|
|
341
|
+
|
|
342
|
+
if (!key) {
|
|
343
|
+
if (next === value) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
setValue(next);
|
|
348
|
+
propValueRef.current = next;
|
|
349
|
+
emit(next);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const base = value && typeof value === 'object' ? { ...value } : {};
|
|
354
|
+
|
|
355
|
+
if (base[key] === next) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
base[key] = next;
|
|
360
|
+
setValue(base);
|
|
361
|
+
propValueRef.current = base;
|
|
362
|
+
emit(base);
|
|
363
|
+
},
|
|
364
|
+
[emit, noValue, value]
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
if (!filterValue) {
|
|
368
|
+
return render ? render(null) : null;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (range) {
|
|
372
|
+
const startValue = value && typeof value === 'object' ? value.start : '';
|
|
373
|
+
const endValue = value && typeof value === 'object' ? value.end : '';
|
|
374
|
+
|
|
375
|
+
const node = (
|
|
376
|
+
<div
|
|
377
|
+
className="visns-native-date-filter__range"
|
|
378
|
+
style={{ display: 'flex', minWidth: 0 }}
|
|
379
|
+
>
|
|
380
|
+
<NativeDateInput
|
|
381
|
+
inputValue={toInputValue(
|
|
382
|
+
startValue,
|
|
383
|
+
startFormat,
|
|
384
|
+
startWithTime
|
|
385
|
+
)}
|
|
386
|
+
withTime={startWithTime}
|
|
387
|
+
disabled={disabled}
|
|
388
|
+
readOnly={readOnly}
|
|
389
|
+
placeholder={startEditorProps.placeholder}
|
|
390
|
+
style={style}
|
|
391
|
+
inputRef={inputRef}
|
|
392
|
+
onValueChange={(raw) =>
|
|
393
|
+
handleChange(raw, 'start', startFormat, startWithTime)
|
|
394
|
+
}
|
|
395
|
+
/>
|
|
396
|
+
<div className="InovuaReactDataGrid__column-header__filter__binary_operator_separator" />
|
|
397
|
+
<NativeDateInput
|
|
398
|
+
inputValue={toInputValue(endValue, endFormat, endWithTime)}
|
|
399
|
+
withTime={endWithTime}
|
|
400
|
+
disabled={disabled}
|
|
401
|
+
readOnly={readOnly}
|
|
402
|
+
placeholder={endEditorProps.placeholder}
|
|
403
|
+
style={style}
|
|
404
|
+
onValueChange={(raw) =>
|
|
405
|
+
handleChange(raw, 'end', endFormat, endWithTime)
|
|
406
|
+
}
|
|
407
|
+
/>
|
|
408
|
+
</div>
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
return render ? render(node) : node;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const node = (
|
|
415
|
+
<NativeDateInput
|
|
416
|
+
inputValue={toInputValue(value, startFormat, startWithTime)}
|
|
417
|
+
withTime={startWithTime}
|
|
418
|
+
disabled={disabled}
|
|
419
|
+
readOnly={readOnly}
|
|
420
|
+
placeholder={startEditorProps.placeholder}
|
|
421
|
+
style={style}
|
|
422
|
+
inputRef={inputRef}
|
|
423
|
+
onValueChange={(raw) =>
|
|
424
|
+
handleChange(raw, null, startFormat, startWithTime)
|
|
425
|
+
}
|
|
426
|
+
/>
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
return render ? render(node) : node;
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
NativeDateFilterEditor.displayName = 'NativeDateFilterEditor';
|
|
433
|
+
|
|
434
|
+
export default NativeDateFilterEditor;
|
|
@@ -1837,9 +1837,19 @@ input.nativeDateInput[type] {
|
|
|
1837
1837
|
input.nativeDateFilled[type] {
|
|
1838
1838
|
padding-right: 2.75rem;
|
|
1839
1839
|
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1840
|
+
/* Phones: tapping the field itself opens the OS sheet, so the browser's
|
|
1841
|
+
calendar glyph is redundant next to our clear button. */
|
|
1842
|
+
@media (pointer: coarse) {
|
|
1843
|
+
&::-webkit-calendar-picker-indicator {
|
|
1844
|
+
-webkit-appearance: none;
|
|
1845
|
+
display: none;
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
/* Desktop: the indicator IS how the picker opens — keep it visible and
|
|
1850
|
+
reserve room for the clear button to sit beside it. */
|
|
1851
|
+
@media (pointer: fine) {
|
|
1852
|
+
padding-right: 4.4rem;
|
|
1843
1853
|
}
|
|
1844
1854
|
}
|
|
1845
1855
|
|
|
@@ -1848,6 +1858,11 @@ input.nativeDateFilled[type] {
|
|
|
1848
1858
|
top: 50%;
|
|
1849
1859
|
right: 0.4rem;
|
|
1850
1860
|
transform: translateY(-50%);
|
|
1861
|
+
|
|
1862
|
+
/* Sit left of the browser's calendar indicator on desktop. */
|
|
1863
|
+
@media (pointer: fine) {
|
|
1864
|
+
right: 2.3rem;
|
|
1865
|
+
}
|
|
1851
1866
|
display: flex;
|
|
1852
1867
|
align-items: center;
|
|
1853
1868
|
justify-content: center;
|
|
@@ -1867,3 +1882,10 @@ input.nativeDateFilled[type] {
|
|
|
1867
1882
|
outline: none;
|
|
1868
1883
|
}
|
|
1869
1884
|
}
|
|
1885
|
+
|
|
1886
|
+
/* Present in the DOM but inert while the field is empty — see the note in
|
|
1887
|
+
Field.jsx's native date markup. */
|
|
1888
|
+
.nativeDateClearHidden {
|
|
1889
|
+
visibility: hidden;
|
|
1890
|
+
pointer-events: none;
|
|
1891
|
+
}
|
|
@@ -870,6 +870,140 @@
|
|
|
870
870
|
}
|
|
871
871
|
}
|
|
872
872
|
|
|
873
|
+
/* ----------------------------------------------------------------------------
|
|
874
|
+
Sidebar-owned account actions — opt-in
|
|
875
|
+
----------------------------------------------------------------------------
|
|
876
|
+
`navConfig.actionsPlacement: 'sidebar'` moves the notification bell, the
|
|
877
|
+
profile link, the logout button and anything else a project mounts as a
|
|
878
|
+
"setting" out of the 50px content header and pins them to the bottom of the
|
|
879
|
+
rail, which was empty space. The page then gets the full height of the
|
|
880
|
+
glass. Nothing below applies unless the consumer publishes that key.
|
|
881
|
+
------------------------------------------------------------------------- */
|
|
882
|
+
.aside--cms--rail {
|
|
883
|
+
/* Three bands: the logo plate, the nav list that takes what's left and
|
|
884
|
+
scrolls, and the actions pinned under it. */
|
|
885
|
+
display: flex;
|
|
886
|
+
flex-direction: column;
|
|
887
|
+
|
|
888
|
+
/* The rail clips itself so a long nav list can't spill; the panels that
|
|
889
|
+
open from the action rows — the notification list, the account
|
|
890
|
+
switcher — would be cut off by that. The nav list keeps its own
|
|
891
|
+
scroll container, so nothing else depended on this. */
|
|
892
|
+
overflow: visible;
|
|
893
|
+
|
|
894
|
+
.awrap {
|
|
895
|
+
/* `min-height: 0` or the nav list refuses to shrink below its
|
|
896
|
+
content and pushes the actions off the bottom of the rail. */
|
|
897
|
+
flex: 1 1 auto;
|
|
898
|
+
min-height: 0;
|
|
899
|
+
display: flex;
|
|
900
|
+
flex-direction: column;
|
|
901
|
+
|
|
902
|
+
.navwrap {
|
|
903
|
+
flex: 1 1 auto;
|
|
904
|
+
min-height: 0;
|
|
905
|
+
/* The fixed `calc(100vh - 50px)` assumed the rail's only other
|
|
906
|
+
band was the logo plate. The column measures it now. */
|
|
907
|
+
max-height: none;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
.rail-actions {
|
|
913
|
+
flex: 0 0 auto;
|
|
914
|
+
position: relative;
|
|
915
|
+
/* Above the content header's sticky 995 so a panel opening from a row
|
|
916
|
+
is never painted underneath it. */
|
|
917
|
+
z-index: 996;
|
|
918
|
+
/* Reads as a footer to the list rather than one more entry in it. */
|
|
919
|
+
border-top: 1px solid rgba(var(--tertiary-color-rgb), 0.14);
|
|
920
|
+
padding: var(--padding) 0;
|
|
921
|
+
|
|
922
|
+
> ul {
|
|
923
|
+
list-style: none;
|
|
924
|
+
margin: 0;
|
|
925
|
+
padding: 0;
|
|
926
|
+
display: flex;
|
|
927
|
+
flex-direction: column;
|
|
928
|
+
width: 100%;
|
|
929
|
+
|
|
930
|
+
> li {
|
|
931
|
+
/* Same gutter as `.app-nav .nav-item`, so the rows line up with
|
|
932
|
+
the navigation above them to the pixel. */
|
|
933
|
+
width: 100%;
|
|
934
|
+
padding: 0.15rem 0.45rem;
|
|
935
|
+
box-sizing: border-box;
|
|
936
|
+
/* The positioning context for the panels a mounted component
|
|
937
|
+
opens — they anchor to the nearest positioned ancestor. */
|
|
938
|
+
position: relative;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// A row wears the rail's own language: the nav item's padding,
|
|
942
|
+
// radius and hover tint. Depth-limited on purpose — the panels these
|
|
943
|
+
// components open live deeper inside the same `li` and must keep
|
|
944
|
+
// their own styling.
|
|
945
|
+
> li > a,
|
|
946
|
+
> li > button,
|
|
947
|
+
> li > div > a,
|
|
948
|
+
> li > div > button {
|
|
949
|
+
width: 100%;
|
|
950
|
+
display: flex;
|
|
951
|
+
flex-wrap: nowrap;
|
|
952
|
+
align-items: center;
|
|
953
|
+
gap: 0.55rem;
|
|
954
|
+
box-sizing: border-box;
|
|
955
|
+
padding: 0.5rem 1rem;
|
|
956
|
+
margin: 0;
|
|
957
|
+
line-height: 1.25;
|
|
958
|
+
text-align: left;
|
|
959
|
+
text-decoration: none;
|
|
960
|
+
color: var(--tertiary-color);
|
|
961
|
+
background: var(--nav-bg);
|
|
962
|
+
border: none;
|
|
963
|
+
outline: none;
|
|
964
|
+
border-radius: var(--br);
|
|
965
|
+
font: inherit;
|
|
966
|
+
cursor: pointer;
|
|
967
|
+
appearance: none;
|
|
968
|
+
transition: all 0.65s cubic-bezier(0.25, 0.8, 0.25, 1);
|
|
969
|
+
|
|
970
|
+
svg {
|
|
971
|
+
flex: 0 0 auto;
|
|
972
|
+
color: var(--secondary-color);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
> li > a:hover,
|
|
977
|
+
> li > button:hover,
|
|
978
|
+
> li > div > a:hover,
|
|
979
|
+
> li > div > button:hover,
|
|
980
|
+
> li > a.active {
|
|
981
|
+
background: rgba(var(--tertiary-color-rgb), 0.05);
|
|
982
|
+
color: var(--secondary-color);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// Components mounted here wrap their trigger in a div of their own
|
|
986
|
+
// (`.notificationContainer`, the switcher's wrapper). Left as an
|
|
987
|
+
// inline-ish block it would collapse the row.
|
|
988
|
+
> li > div {
|
|
989
|
+
width: 100%;
|
|
990
|
+
position: relative;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// A mounted component draws its own trigger and takes no label from
|
|
994
|
+
// us, so the row publishes one as `--nav-action-label` and the
|
|
995
|
+
// trigger wears it. Empty by default, so a component that already
|
|
996
|
+
// labels itself is untouched. (The notification bell sets its own
|
|
997
|
+
// fallback text — see Notification.module.scss.)
|
|
998
|
+
> li > div > a::after {
|
|
999
|
+
content: var(--nav-action-label, '');
|
|
1000
|
+
flex: 1 1 auto;
|
|
1001
|
+
min-width: 0;
|
|
1002
|
+
text-align: left;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
873
1007
|
.content {
|
|
874
1008
|
margin-left: var(--sidebar-width);
|
|
875
1009
|
flex-grow: 1;
|
|
@@ -952,6 +1086,21 @@
|
|
|
952
1086
|
}
|
|
953
1087
|
}
|
|
954
1088
|
|
|
1089
|
+
/* With the actions in the rail and no search field configured, the only
|
|
1090
|
+
thing left in the header is the drawer's hamburger — which exists below
|
|
1091
|
+
1024px and nowhere else. The bar stays in the markup so the drawer keeps
|
|
1092
|
+
the 50px anchor it offsets itself by, and desktop simply drops it. */
|
|
1093
|
+
.content .content-header--mobile {
|
|
1094
|
+
display: none;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
/* Nothing sits above the page any more, so the main container is the
|
|
1098
|
+
viewport. Without this it is only as tall as its content and the page
|
|
1099
|
+
background stops short on a short screen. */
|
|
1100
|
+
.content--full {
|
|
1101
|
+
min-height: 100vh;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
955
1104
|
/* Rendered only while the mobile drawer is open; the phone block below
|
|
956
1105
|
displays it. Desktop never opens the drawer, but the guard costs nothing. */
|
|
957
1106
|
.drawer-backdrop {
|
|
@@ -1016,6 +1165,41 @@
|
|
|
1016
1165
|
}
|
|
1017
1166
|
}
|
|
1018
1167
|
|
|
1168
|
+
/* The account actions sit at the foot of the drawer, above the home
|
|
1169
|
+
indicator, at the same touch size as the nav rows above them. */
|
|
1170
|
+
.rail-actions {
|
|
1171
|
+
padding: 0.35rem 0 calc(0.35rem + env(safe-area-inset-bottom, 0px));
|
|
1172
|
+
|
|
1173
|
+
> ul > li {
|
|
1174
|
+
padding: 0.2rem 0.65rem;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// Depth-limited: the panels these rows open live deeper in the same
|
|
1178
|
+
// `li` and size their own icons.
|
|
1179
|
+
> ul > li > a,
|
|
1180
|
+
> ul > li > button,
|
|
1181
|
+
> ul > li > div > a,
|
|
1182
|
+
> ul > li > div > button {
|
|
1183
|
+
padding: 0.85rem 1rem;
|
|
1184
|
+
font-size: 1rem;
|
|
1185
|
+
line-height: 1.3;
|
|
1186
|
+
|
|
1187
|
+
> svg {
|
|
1188
|
+
width: 20px;
|
|
1189
|
+
height: 20px;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
.content .content-header--mobile {
|
|
1195
|
+
display: flex;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
.content--full {
|
|
1199
|
+
/* The 50px bar is still there below this width. */
|
|
1200
|
+
min-height: calc(100vh - 50px);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1019
1203
|
.drawer-backdrop {
|
|
1020
1204
|
display: block;
|
|
1021
1205
|
position: fixed;
|