@cahyo-dimas/freeday 1.13.0 → 1.14.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/CHANGELOG.md +32 -0
- package/README.id.md +1 -1
- package/README.md +1 -1
- package/adapters/react/components/FdyAutocomplete.tsx +6 -2
- package/adapters/react/components/FdyCascade.tsx +5 -1
- package/adapters/react/components/FdyCfl.tsx +5 -2
- package/adapters/react/components/FdyCombo.tsx +10 -2
- package/adapters/react/components/FdyDateRange.tsx +4 -0
- package/adapters/react/components/FdyDatepicker.tsx +6 -2
- package/adapters/vue/components/FdyAutocomplete.vue +6 -2
- package/adapters/vue/components/FdyCascade.vue +5 -1
- package/adapters/vue/components/FdyCfl.vue +5 -2
- package/adapters/vue/components/FdyCombo.vue +10 -2
- package/adapters/vue/components/FdyDateRange.vue +4 -0
- package/adapters/vue/components/FdyDatepicker.vue +6 -2
- package/dist/freeday-datetime.js +24 -0
- package/dist/freeday.bundle.css +30 -0
- package/dist/freeday.css +30 -0
- package/dist/freeday.js +24 -0
- package/package.json +1 -1
- package/src/components/cascade.css +3 -0
- package/src/components/combo.css +5 -0
- package/src/components/datepicker.css +4 -0
- package/src/components/file-upload.css +1 -0
- package/src/components/input.css +6 -0
- package/src/components/rating.css +6 -0
- package/src/components/tabs.css +2 -0
- package/src/components/timepicker.css +1 -0
- package/src/components/tree.css +2 -0
- package/src/freeday-datetime.js +24 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,38 @@
|
|
|
3
3
|
Semua perubahan penting dicatat di sini. Format longgar mengikuti
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/); tiap versi = git tag.
|
|
5
5
|
|
|
6
|
+
## [1.14.0] — 2026-08-01
|
|
7
|
+
### Added
|
|
8
|
+
- **Read-only state across the form controls (#39).** `input` / `textarea` gain a `[readonly]` rule —
|
|
9
|
+
full-contrast text on a muted surface with a softened border and no focus-ring escalation, distinct
|
|
10
|
+
from `:disabled` (which dims the value to placeholder-grey and drops it from tab order). A new
|
|
11
|
+
`readonly` prop lands on all six controlled select-type adapters — `FdyCombo`, `FdyCascade`,
|
|
12
|
+
`FdyDatepicker`, `FdyDateRange`, `FdyAutocomplete`, `FdyCfl` (Vue + React): the control stays focusable
|
|
13
|
+
and shows its value with `aria-readonly="true"` but can't be opened or changed (`FdyDatepicker`'s clear
|
|
14
|
+
button is suppressed too). Read-only ≠ disabled — it keeps tab order, full contrast, and stays
|
|
15
|
+
selectable / copyable / submittable, so a real value the user may not edit reads as data, not an empty
|
|
16
|
+
placeholder. Verified in-browser (readonly `opacity:1` vs disabled `0.5`; text at full `--color-text`).
|
|
17
|
+
- **Interaction-state coverage completed.** Added the missing `disabled` state to `timepicker`, `rating`
|
|
18
|
+
(`.fdy-rating--disabled`), `tree` (`[aria-disabled]` rows), `tabs`, `file-upload`
|
|
19
|
+
(`[aria-disabled]` / `.is-disabled`), and `datepicker`; and the missing `invalid` state to `rating`
|
|
20
|
+
(`.fdy-rating--error`). The vanilla datetime composer now reflects `data-fdy-disabled` /
|
|
21
|
+
`data-fdy-invalid` from its wrapper onto **both** child pickers so a datetime reads as one control
|
|
22
|
+
(applied on a macrotask, so it lands after both child triggers are built — a microtask would run
|
|
23
|
+
before the later-registered timepicker enhancer). All additive; no breaking changes.
|
|
24
|
+
|
|
25
|
+
## [1.13.1] — 2026-08-01
|
|
26
|
+
### Fixed
|
|
27
|
+
- **`FdyCombo` can be selected with the mouse again (#38).** Clicking an option did nothing — the
|
|
28
|
+
listbox closed and no `update:modelValue` fired (keyboard select still worked, which is why a
|
|
29
|
+
keyboard-only smoke test missed it). The option `<li>` isn't focusable, so a `mousedown` moved focus
|
|
30
|
+
off the combobox button; the root's `focusout` handler then closed the list — removing the `<li>` —
|
|
31
|
+
*before* `mouseup`, so the browser never generated a `click` and `choose()` was never reached. Added
|
|
32
|
+
`@mousedown.prevent` (Vue) / `onMouseDown` `preventDefault` (React) on the option so focus stays on the
|
|
33
|
+
button and the click lands — the same pattern the sibling `FdyDatepicker` / `FdyAutocomplete` (and the
|
|
34
|
+
vanilla `freeday-select.js`, fixed in v1.6.1) already use. Both adapters; no API change; keyboard and
|
|
35
|
+
hover paths untouched. Verified with trusted CDP mouse events (a synthetic `dispatchEvent` won't
|
|
36
|
+
reproduce it — untrusted events run no default action, so focus never moves).
|
|
37
|
+
|
|
6
38
|
## [1.13.0] — 2026-07-30
|
|
7
39
|
### Added
|
|
8
40
|
- **`FdyDatepicker` clear affordance — an optional date can now be unset (#37A).** A new `clearable`
|
package/README.id.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
> **Lebih banyak _free day_ buat dev — UI kit-nya sudah siap pakai.**
|
|
6
6
|
|
|
7
7
|
[](https://cahyo-dimas.github.io/freeday-ui-kit/)
|
|
8
|
-
[](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.14.0)
|
|
9
9
|
|
|
10
10
|
UI KIT yang token-driven & framework-agnostic — satu sumber kebenaran untuk warna, tipografi,
|
|
11
11
|
spasi, dan komponen. Blueprint: `docs/superpowers/specs/2026-07-21-freeday-ui-kit-design.md`.
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
> **More free days for devs — the UI kit is ready to use.**
|
|
6
6
|
|
|
7
7
|
[](https://cahyo-dimas.github.io/freeday-ui-kit/)
|
|
8
|
-
[](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.14.0)
|
|
9
9
|
|
|
10
10
|
A token-driven, framework-agnostic UI kit — one source of truth for color, typography,
|
|
11
11
|
spacing, and components. Blueprint: `docs/superpowers/specs/2026-07-21-freeday-ui-kit-design.md`.
|
|
@@ -21,6 +21,8 @@ export interface FdyAutocompleteProps {
|
|
|
21
21
|
ariaLabel?: string;
|
|
22
22
|
ariaLabelledby?: string;
|
|
23
23
|
disabled?: boolean;
|
|
24
|
+
/** Locked/view mode: the input is not editable and the list won't open, but it stays focusable and shows its value. Unlike `disabled`, it keeps tab order and isn't greyed. */
|
|
25
|
+
readonly?: boolean;
|
|
24
26
|
invalid?: boolean;
|
|
25
27
|
describedby?: string;
|
|
26
28
|
}
|
|
@@ -44,6 +46,7 @@ export function FdyAutocomplete(props: FdyAutocompleteProps): JSX.Element {
|
|
|
44
46
|
}, []);
|
|
45
47
|
|
|
46
48
|
const isDisabled: boolean = props.disabled === true;
|
|
49
|
+
const isReadonly: boolean = props.readonly === true;
|
|
47
50
|
const isInvalid: boolean = props.invalid === true;
|
|
48
51
|
|
|
49
52
|
const filtered: string[] = useMemo((): string[] => {
|
|
@@ -54,7 +57,7 @@ export function FdyAutocomplete(props: FdyAutocompleteProps): JSX.Element {
|
|
|
54
57
|
const activeDescendant: string | undefined =
|
|
55
58
|
open && active >= 0 && active < filtered.length ? optionId(active) : undefined;
|
|
56
59
|
|
|
57
|
-
const openList = (): void => { if (!isDisabled) setOpen(true); };
|
|
60
|
+
const openList = (): void => { if (!isDisabled && !isReadonly) setOpen(true); };
|
|
58
61
|
const closeList = (): void => { setOpen(false); setActive(-1); };
|
|
59
62
|
|
|
60
63
|
const choose = (label: string): void => {
|
|
@@ -65,7 +68,7 @@ export function FdyAutocomplete(props: FdyAutocompleteProps): JSX.Element {
|
|
|
65
68
|
};
|
|
66
69
|
|
|
67
70
|
const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>): void => {
|
|
68
|
-
if (isDisabled) return;
|
|
71
|
+
if (isDisabled || isReadonly) return;
|
|
69
72
|
switch (e.key) {
|
|
70
73
|
case 'ArrowDown':
|
|
71
74
|
e.preventDefault();
|
|
@@ -128,6 +131,7 @@ export function FdyAutocomplete(props: FdyAutocompleteProps): JSX.Element {
|
|
|
128
131
|
autoComplete="off"
|
|
129
132
|
placeholder={props.placeholder}
|
|
130
133
|
disabled={isDisabled}
|
|
134
|
+
readOnly={isReadonly}
|
|
131
135
|
value={props.value}
|
|
132
136
|
onChange={(e: React.ChangeEvent<HTMLInputElement>): void => { props.onChange(e.target.value); setActive(-1); openList(); }}
|
|
133
137
|
onFocus={openList}
|
|
@@ -29,6 +29,8 @@ export interface FdyCascadeProps {
|
|
|
29
29
|
id?: string;
|
|
30
30
|
ariaLabelledby?: string;
|
|
31
31
|
disabled?: boolean;
|
|
32
|
+
/** Locked/view mode: stays focusable and shows its value, but can't be opened or changed. Unlike `disabled`, it keeps tab order and isn't greyed. */
|
|
33
|
+
readonly?: boolean;
|
|
32
34
|
invalid?: boolean;
|
|
33
35
|
describedby?: string;
|
|
34
36
|
}
|
|
@@ -70,6 +72,7 @@ export function FdyCascade(props: FdyCascadeProps): JSX.Element {
|
|
|
70
72
|
}, []);
|
|
71
73
|
|
|
72
74
|
const isDisabled: boolean = props.disabled === true;
|
|
75
|
+
const isReadonly: boolean = props.readonly === true;
|
|
73
76
|
const isInvalid: boolean = props.invalid === true;
|
|
74
77
|
|
|
75
78
|
const current: ReadonlyArray<CascadeNode> =
|
|
@@ -86,7 +89,7 @@ export function FdyCascade(props: FdyCascadeProps): JSX.Element {
|
|
|
86
89
|
const crumb: string = stack.length > 0 ? stack.map((n: CascadeNode): string => n.label).join(sep) : name;
|
|
87
90
|
|
|
88
91
|
const openPanel = (): void => {
|
|
89
|
-
if (isDisabled) return;
|
|
92
|
+
if (isDisabled || isReadonly) return;
|
|
90
93
|
// Re-open at the selected leaf's level for quick re-selection (matches the enhancer).
|
|
91
94
|
setStack(selectedTrail !== null && selectedTrail.length > 1 ? selectedTrail.slice(0, -1) : []);
|
|
92
95
|
setActive(0);
|
|
@@ -174,6 +177,7 @@ export function FdyCascade(props: FdyCascadeProps): JSX.Element {
|
|
|
174
177
|
aria-label={props.ariaLabelledby === undefined ? name : undefined}
|
|
175
178
|
aria-labelledby={props.ariaLabelledby}
|
|
176
179
|
aria-invalid={isInvalid ? 'true' : undefined}
|
|
180
|
+
aria-readonly={isReadonly ? 'true' : undefined}
|
|
177
181
|
aria-describedby={props.describedby}
|
|
178
182
|
disabled={isDisabled}
|
|
179
183
|
onClick={(): void => { if (open) closePanel(true); else openPanel(); }}
|
|
@@ -31,6 +31,8 @@ export interface FdyCflProps<Row extends Record<string, unknown>> {
|
|
|
31
31
|
pageSize?: number;
|
|
32
32
|
placeholder?: string;
|
|
33
33
|
disabled?: boolean;
|
|
34
|
+
/** Locked/view mode: shows the picked value (focusable, copyable), but the search dialog can't be opened. Unlike `disabled`, it keeps tab order and isn't greyed. */
|
|
35
|
+
readonly?: boolean;
|
|
34
36
|
invalid?: boolean;
|
|
35
37
|
describedby?: string;
|
|
36
38
|
id?: string;
|
|
@@ -68,6 +70,7 @@ export function FdyCfl<Row extends Record<string, unknown>>(props: FdyCflProps<R
|
|
|
68
70
|
const searchTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
69
71
|
|
|
70
72
|
const isDisabled: boolean = props.disabled === true;
|
|
73
|
+
const isReadonly: boolean = props.readonly === true;
|
|
71
74
|
const isInvalid: boolean = props.invalid === true;
|
|
72
75
|
const displayValue: string = props.value !== null ? props.display(props.value) : '';
|
|
73
76
|
// The results <table> (owner of `resultsId`) only renders in the rows branch, so gate the
|
|
@@ -187,7 +190,7 @@ export function FdyCfl<Row extends Record<string, unknown>>(props: FdyCflProps<R
|
|
|
187
190
|
|
|
188
191
|
// --- Open / close ------------------------------------------------------------------------
|
|
189
192
|
function openDialog(): void {
|
|
190
|
-
if (isDisabled) return;
|
|
193
|
+
if (isDisabled || isReadonly) return;
|
|
191
194
|
if (searchTimerRef.current !== null) {
|
|
192
195
|
clearTimeout(searchTimerRef.current);
|
|
193
196
|
searchTimerRef.current = null;
|
|
@@ -257,7 +260,7 @@ export function FdyCfl<Row extends Record<string, unknown>>(props: FdyCflProps<R
|
|
|
257
260
|
aria-haspopup="dialog"
|
|
258
261
|
aria-labelledby={props.ariaLabelledby}
|
|
259
262
|
aria-label={props.ariaLabelledby ? undefined : 'Buka pencarian'}
|
|
260
|
-
disabled={isDisabled}
|
|
263
|
+
disabled={isDisabled || isReadonly}
|
|
261
264
|
onClick={openDialog}
|
|
262
265
|
>
|
|
263
266
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
@@ -15,6 +15,8 @@ export interface FdyComboProps<T extends string> {
|
|
|
15
15
|
ariaLabelledby?: string;
|
|
16
16
|
placeholder?: string;
|
|
17
17
|
disabled?: boolean;
|
|
18
|
+
/** Locked/view mode: stays focusable and shows its value, but can't be opened or changed. Unlike `disabled`, it keeps tab order and isn't greyed. */
|
|
19
|
+
readonly?: boolean;
|
|
18
20
|
invalid?: boolean;
|
|
19
21
|
describedby?: string;
|
|
20
22
|
}
|
|
@@ -39,6 +41,7 @@ export function FdyCombo<T extends string>(props: FdyComboProps<T>): JSX.Element
|
|
|
39
41
|
}, []);
|
|
40
42
|
|
|
41
43
|
const isDisabled: boolean = props.disabled === true;
|
|
44
|
+
const isReadonly: boolean = props.readonly === true;
|
|
42
45
|
const isInvalid: boolean = props.invalid === true;
|
|
43
46
|
const selectedIndex: number = useMemo(
|
|
44
47
|
(): number => props.options.findIndex((o: FdyComboOption<T>): boolean => o.value === props.value),
|
|
@@ -53,7 +56,7 @@ export function FdyCombo<T extends string>(props: FdyComboProps<T>): JSX.Element
|
|
|
53
56
|
setHighlighted(len === 0 ? -1 : ((index % len) + len) % len);
|
|
54
57
|
};
|
|
55
58
|
const openList = (): void => {
|
|
56
|
-
if (isDisabled || open) return;
|
|
59
|
+
if (isDisabled || isReadonly || open) return;
|
|
57
60
|
setOpen(true);
|
|
58
61
|
const start: number = selectedIndex >= 0 ? selectedIndex : 0;
|
|
59
62
|
const len: number = props.options.length;
|
|
@@ -85,7 +88,7 @@ export function FdyCombo<T extends string>(props: FdyComboProps<T>): JSX.Element
|
|
|
85
88
|
};
|
|
86
89
|
|
|
87
90
|
const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>): void => {
|
|
88
|
-
if (isDisabled) return;
|
|
91
|
+
if (isDisabled || isReadonly) return;
|
|
89
92
|
switch (e.key) {
|
|
90
93
|
case 'ArrowDown': e.preventDefault(); if (open) setHighlight(highlighted + 1); else openList(); break;
|
|
91
94
|
case 'ArrowUp': e.preventDefault(); if (open) setHighlight(highlighted - 1); else openList(); break;
|
|
@@ -140,6 +143,7 @@ export function FdyCombo<T extends string>(props: FdyComboProps<T>): JSX.Element
|
|
|
140
143
|
aria-activedescendant={activeDescendant}
|
|
141
144
|
aria-labelledby={props.ariaLabelledby}
|
|
142
145
|
aria-invalid={isInvalid ? 'true' : undefined}
|
|
146
|
+
aria-readonly={isReadonly ? 'true' : undefined}
|
|
143
147
|
aria-describedby={props.describedby}
|
|
144
148
|
disabled={isDisabled}
|
|
145
149
|
onClick={toggle}
|
|
@@ -148,12 +152,16 @@ export function FdyCombo<T extends string>(props: FdyComboProps<T>): JSX.Element
|
|
|
148
152
|
</button>
|
|
149
153
|
<ul id={listboxId} ref={listboxRef} className="fdy-combo__listbox" role="listbox" hidden={!open}>
|
|
150
154
|
{props.options.map((opt: FdyComboOption<T>, i: number): JSX.Element => (
|
|
155
|
+
// onMouseDown preventDefault keeps focus on the button: the option <li> isn't focusable, so a
|
|
156
|
+
// plain mousedown moves focus out of the combo, fires onBlur (focusout), and closes the list
|
|
157
|
+
// before the click lands. Same pattern as FdyDatepicker/FdyAutocomplete.
|
|
151
158
|
<li
|
|
152
159
|
id={optionId(i)}
|
|
153
160
|
key={opt.value}
|
|
154
161
|
className={i === highlighted ? 'fdy-combo__option is-highlighted' : 'fdy-combo__option'}
|
|
155
162
|
role="option"
|
|
156
163
|
aria-selected={opt.value === props.value}
|
|
164
|
+
onMouseDown={(e: React.MouseEvent): void => e.preventDefault()}
|
|
157
165
|
onClick={(): void => choose(i)}
|
|
158
166
|
onMouseMove={(): void => setHighlight(i)}
|
|
159
167
|
>
|
|
@@ -19,6 +19,8 @@ export interface FdyDateRangeProps {
|
|
|
19
19
|
max?: string;
|
|
20
20
|
locale?: string;
|
|
21
21
|
disabled?: boolean;
|
|
22
|
+
/** Locked/view mode for both pickers — focusable, values shown, but can't be opened or changed. */
|
|
23
|
+
readonly?: boolean;
|
|
22
24
|
invalid?: boolean;
|
|
23
25
|
describedby?: string;
|
|
24
26
|
startPlaceholder?: string;
|
|
@@ -45,6 +47,7 @@ export function FdyDateRange(props: FdyDateRangeProps): JSX.Element {
|
|
|
45
47
|
max={props.value.end ?? props.max}
|
|
46
48
|
locale={props.locale}
|
|
47
49
|
disabled={props.disabled}
|
|
50
|
+
readonly={props.readonly}
|
|
48
51
|
invalid={props.invalid}
|
|
49
52
|
describedby={props.describedby}
|
|
50
53
|
placeholder={props.startPlaceholder ?? 'Dari'}
|
|
@@ -57,6 +60,7 @@ export function FdyDateRange(props: FdyDateRangeProps): JSX.Element {
|
|
|
57
60
|
max={props.max}
|
|
58
61
|
locale={props.locale}
|
|
59
62
|
disabled={props.disabled}
|
|
63
|
+
readonly={props.readonly}
|
|
60
64
|
invalid={props.invalid}
|
|
61
65
|
describedby={props.describedby}
|
|
62
66
|
placeholder={props.endPlaceholder ?? 'Sampai'}
|
|
@@ -29,6 +29,8 @@ export interface FdyDatepickerProps {
|
|
|
29
29
|
locale?: string;
|
|
30
30
|
placeholder?: string;
|
|
31
31
|
disabled?: boolean;
|
|
32
|
+
/** Locked/view mode: stays focusable and shows its date, but can't be opened, cleared, or changed. Unlike `disabled`, it keeps tab order and isn't greyed. */
|
|
33
|
+
readonly?: boolean;
|
|
32
34
|
invalid?: boolean;
|
|
33
35
|
describedby?: string;
|
|
34
36
|
id?: string;
|
|
@@ -116,9 +118,10 @@ export function FdyDatepicker(props: FdyDatepickerProps): JSX.Element {
|
|
|
116
118
|
const minDate: Date | null = useMemo((): Date | null => parseISO(props.min), [props.min]);
|
|
117
119
|
const maxDate: Date | null = useMemo((): Date | null => parseISO(props.max), [props.max]);
|
|
118
120
|
const isDisabled: boolean = props.disabled === true;
|
|
121
|
+
const isReadonly: boolean = props.readonly === true;
|
|
119
122
|
const isInvalid: boolean = props.invalid === true;
|
|
120
123
|
const displayPlaceholder: string = props.placeholder ?? 'Select date';
|
|
121
|
-
const showClear: boolean = props.clearable === true && selectedDate !== null && !isDisabled;
|
|
124
|
+
const showClear: boolean = props.clearable === true && selectedDate !== null && !isDisabled && !isReadonly;
|
|
122
125
|
const prevMonthLabelText: string = props.prevMonthLabel ?? 'Previous month';
|
|
123
126
|
const nextMonthLabelText: string = props.nextMonthLabel ?? 'Next month';
|
|
124
127
|
const clearLabelText: string = props.clearLabel ?? 'Clear date';
|
|
@@ -223,7 +226,7 @@ export function FdyDatepicker(props: FdyDatepickerProps): JSX.Element {
|
|
|
223
226
|
}
|
|
224
227
|
|
|
225
228
|
function openPanel(): void {
|
|
226
|
-
if (isDisabled || open) return;
|
|
229
|
+
if (isDisabled || isReadonly || open) return;
|
|
227
230
|
const next: Date = selectedDate ?? focusDate ?? new Date();
|
|
228
231
|
setFocusDate(next);
|
|
229
232
|
setViewMonth(new Date(next.getFullYear(), next.getMonth(), 1));
|
|
@@ -319,6 +322,7 @@ export function FdyDatepicker(props: FdyDatepickerProps): JSX.Element {
|
|
|
319
322
|
aria-expanded={open}
|
|
320
323
|
aria-labelledby={props.ariaLabelledby}
|
|
321
324
|
aria-invalid={isInvalid ? 'true' : undefined}
|
|
325
|
+
aria-readonly={isReadonly ? 'true' : undefined}
|
|
322
326
|
aria-describedby={props.describedby}
|
|
323
327
|
disabled={isDisabled}
|
|
324
328
|
onClick={toggle}
|
|
@@ -18,6 +18,8 @@ const props = defineProps<{
|
|
|
18
18
|
ariaLabel?: string;
|
|
19
19
|
ariaLabelledby?: string;
|
|
20
20
|
disabled?: boolean;
|
|
21
|
+
/** Locked/view mode: the input is not editable and the list won't open, but it stays focusable and shows its value. Unlike `disabled`, it keeps tab order and isn't greyed. */
|
|
22
|
+
readonly?: boolean;
|
|
21
23
|
invalid?: boolean;
|
|
22
24
|
describedby?: string;
|
|
23
25
|
}>();
|
|
@@ -44,6 +46,7 @@ const active: Ref<number> = ref(-1);
|
|
|
44
46
|
usePopover(listboxEl, inputEl, open);
|
|
45
47
|
|
|
46
48
|
const isDisabled: ComputedRef<boolean> = computed((): boolean => props.disabled === true);
|
|
49
|
+
const isReadonly: ComputedRef<boolean> = computed((): boolean => props.readonly === true);
|
|
47
50
|
const isInvalid: ComputedRef<boolean> = computed((): boolean => props.invalid === true);
|
|
48
51
|
|
|
49
52
|
const filtered: ComputedRef<string[]> = computed((): string[] => {
|
|
@@ -56,7 +59,7 @@ const activeDescendant: ComputedRef<string | undefined> = computed((): string |
|
|
|
56
59
|
);
|
|
57
60
|
|
|
58
61
|
function openList(): void {
|
|
59
|
-
if (!isDisabled.value) open.value = true;
|
|
62
|
+
if (!isDisabled.value && !isReadonly.value) open.value = true;
|
|
60
63
|
}
|
|
61
64
|
function closeList(): void {
|
|
62
65
|
open.value = false;
|
|
@@ -76,7 +79,7 @@ function onInput(e: Event): void {
|
|
|
76
79
|
}
|
|
77
80
|
|
|
78
81
|
function onKeydown(e: KeyboardEvent): void {
|
|
79
|
-
if (isDisabled.value) return;
|
|
82
|
+
if (isDisabled.value || isReadonly.value) return;
|
|
80
83
|
const len: number = filtered.value.length;
|
|
81
84
|
switch (e.key) {
|
|
82
85
|
case 'ArrowDown':
|
|
@@ -144,6 +147,7 @@ watch(active, async (i: number): Promise<void> => {
|
|
|
144
147
|
autocomplete="off"
|
|
145
148
|
:placeholder="placeholder"
|
|
146
149
|
:disabled="isDisabled"
|
|
150
|
+
:readonly="isReadonly"
|
|
147
151
|
:value="modelValue"
|
|
148
152
|
@input="onInput"
|
|
149
153
|
@focus="openList"
|
|
@@ -42,6 +42,8 @@ const props = defineProps<{
|
|
|
42
42
|
id?: string;
|
|
43
43
|
ariaLabelledby?: string;
|
|
44
44
|
disabled?: boolean;
|
|
45
|
+
/** Locked/view mode: stays focusable and shows its value, but can't be opened or changed. Unlike `disabled`, it keeps tab order and isn't greyed. */
|
|
46
|
+
readonly?: boolean;
|
|
45
47
|
invalid?: boolean;
|
|
46
48
|
describedby?: string;
|
|
47
49
|
}>();
|
|
@@ -73,6 +75,7 @@ usePopover(panelEl, triggerEl, open);
|
|
|
73
75
|
const sep: ComputedRef<string> = computed((): string => props.separator ?? ' / ');
|
|
74
76
|
const name: ComputedRef<string> = computed((): string => props.label ?? 'Pilih');
|
|
75
77
|
const isDisabled: ComputedRef<boolean> = computed((): boolean => props.disabled === true);
|
|
78
|
+
const isReadonly: ComputedRef<boolean> = computed((): boolean => props.readonly === true);
|
|
76
79
|
const isInvalid: ComputedRef<boolean> = computed((): boolean => props.invalid === true);
|
|
77
80
|
|
|
78
81
|
const current: ComputedRef<ReadonlyArray<CascadeNode>> = computed((): ReadonlyArray<CascadeNode> =>
|
|
@@ -95,7 +98,7 @@ const activeDescendant: ComputedRef<string | undefined> = computed((): string |
|
|
|
95
98
|
);
|
|
96
99
|
|
|
97
100
|
function openPanel(): void {
|
|
98
|
-
if (isDisabled.value) return;
|
|
101
|
+
if (isDisabled.value || isReadonly.value) return;
|
|
99
102
|
// Re-open at the selected leaf's level for quick re-selection (matches the enhancer).
|
|
100
103
|
const trail: CascadeNode[] | null = selectedTrail.value;
|
|
101
104
|
stack.value = trail !== null && trail.length > 1 ? trail.slice(0, -1) : [];
|
|
@@ -190,6 +193,7 @@ onBeforeUnmount((): void => {
|
|
|
190
193
|
:aria-label="ariaLabelledby === undefined ? name : undefined"
|
|
191
194
|
:aria-labelledby="ariaLabelledby"
|
|
192
195
|
:aria-invalid="isInvalid ? 'true' : undefined"
|
|
196
|
+
:aria-readonly="isReadonly ? 'true' : undefined"
|
|
193
197
|
:aria-describedby="describedby"
|
|
194
198
|
:disabled="isDisabled"
|
|
195
199
|
@click="open ? closePanel(true) : openPanel()"
|
|
@@ -32,6 +32,8 @@ const props = defineProps<{
|
|
|
32
32
|
pageSize?: number;
|
|
33
33
|
placeholder?: string;
|
|
34
34
|
disabled?: boolean;
|
|
35
|
+
/** Locked/view mode: shows the picked value (focusable, copyable), but the search dialog can't be opened. Unlike `disabled`, it keeps tab order and isn't greyed. */
|
|
36
|
+
readonly?: boolean;
|
|
35
37
|
invalid?: boolean;
|
|
36
38
|
describedby?: string;
|
|
37
39
|
id?: string;
|
|
@@ -64,6 +66,7 @@ const error: Ref<Error | null> = ref(null);
|
|
|
64
66
|
const activeIndex: Ref<number> = ref(-1);
|
|
65
67
|
|
|
66
68
|
const isDisabled: ComputedRef<boolean> = computed((): boolean => props.disabled === true);
|
|
69
|
+
const isReadonly: ComputedRef<boolean> = computed((): boolean => props.readonly === true);
|
|
67
70
|
const isInvalid: ComputedRef<boolean> = computed((): boolean => props.invalid === true);
|
|
68
71
|
const displayValue: ComputedRef<string> = computed((): string =>
|
|
69
72
|
props.modelValue !== null ? props.display(props.modelValue) : '',
|
|
@@ -194,7 +197,7 @@ function onKeydown(e: KeyboardEvent): void {
|
|
|
194
197
|
|
|
195
198
|
// --- Open / close ---------------------------------------------------------
|
|
196
199
|
function openDialog(): void {
|
|
197
|
-
if (isDisabled.value) return;
|
|
200
|
+
if (isDisabled.value || isReadonly.value) return;
|
|
198
201
|
if (searchTimer !== null) {
|
|
199
202
|
clearTimeout(searchTimer);
|
|
200
203
|
searchTimer = null;
|
|
@@ -260,7 +263,7 @@ onBeforeUnmount((): void => {
|
|
|
260
263
|
aria-haspopup="dialog"
|
|
261
264
|
:aria-labelledby="ariaLabelledby"
|
|
262
265
|
:aria-label="ariaLabelledby ? undefined : 'Buka pencarian'"
|
|
263
|
-
:disabled="isDisabled"
|
|
266
|
+
:disabled="isDisabled || isReadonly"
|
|
264
267
|
@click="openDialog"
|
|
265
268
|
>
|
|
266
269
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
@@ -22,6 +22,8 @@ const props = defineProps<{
|
|
|
22
22
|
ariaLabelledby?: string;
|
|
23
23
|
placeholder?: string;
|
|
24
24
|
disabled?: boolean;
|
|
25
|
+
/** Locked/view mode: stays focusable and shows its value, but can't be opened or changed. Unlike `disabled`, it keeps tab order and isn't greyed. */
|
|
26
|
+
readonly?: boolean;
|
|
25
27
|
invalid?: boolean;
|
|
26
28
|
describedby?: string;
|
|
27
29
|
}>();
|
|
@@ -57,6 +59,7 @@ const selectedLabel: ComputedRef<string> = computed((): string => {
|
|
|
57
59
|
});
|
|
58
60
|
const isPlaceholder: ComputedRef<boolean> = computed((): boolean => selectedIndex.value < 0);
|
|
59
61
|
const isDisabled: ComputedRef<boolean> = computed((): boolean => props.disabled === true);
|
|
62
|
+
const isReadonly: ComputedRef<boolean> = computed((): boolean => props.readonly === true);
|
|
60
63
|
const isInvalid: ComputedRef<boolean> = computed((): boolean => props.invalid === true);
|
|
61
64
|
const activeDescendant: ComputedRef<string | undefined> = computed((): string | undefined =>
|
|
62
65
|
open.value && highlighted.value >= 0 ? optionId(highlighted.value) : undefined,
|
|
@@ -68,7 +71,7 @@ function setHighlight(index: number): void {
|
|
|
68
71
|
}
|
|
69
72
|
|
|
70
73
|
function openList(): void {
|
|
71
|
-
if (isDisabled.value || open.value) return;
|
|
74
|
+
if (isDisabled.value || isReadonly.value || open.value) return;
|
|
72
75
|
open.value = true;
|
|
73
76
|
setHighlight(selectedIndex.value >= 0 ? selectedIndex.value : 0);
|
|
74
77
|
}
|
|
@@ -114,7 +117,7 @@ function typeahead(char: string): void {
|
|
|
114
117
|
}
|
|
115
118
|
|
|
116
119
|
function onKeydown(e: KeyboardEvent): void {
|
|
117
|
-
if (isDisabled.value) return;
|
|
120
|
+
if (isDisabled.value || isReadonly.value) return;
|
|
118
121
|
switch (e.key) {
|
|
119
122
|
case 'ArrowDown':
|
|
120
123
|
e.preventDefault();
|
|
@@ -207,6 +210,7 @@ onBeforeUnmount((): void => {
|
|
|
207
210
|
:aria-activedescendant="activeDescendant"
|
|
208
211
|
:aria-labelledby="ariaLabelledby"
|
|
209
212
|
:aria-invalid="isInvalid ? 'true' : undefined"
|
|
213
|
+
:aria-readonly="isReadonly ? 'true' : undefined"
|
|
210
214
|
:aria-describedby="describedby"
|
|
211
215
|
:disabled="isDisabled"
|
|
212
216
|
@click="toggle"
|
|
@@ -214,6 +218,9 @@ onBeforeUnmount((): void => {
|
|
|
214
218
|
<span class="fdy-combo__value" :class="{ 'fdy-combo__value--placeholder': isPlaceholder }">{{ selectedLabel }}</span>
|
|
215
219
|
</button>
|
|
216
220
|
<ul :id="listboxId" ref="listboxEl" class="fdy-combo__listbox" role="listbox" popover="manual" :hidden="!open">
|
|
221
|
+
<!-- @mousedown.prevent keeps focus on the button: the option <li> isn't focusable, so a plain
|
|
222
|
+
mousedown moves focus out of the combo, fires @focusout, and closes the list before the click
|
|
223
|
+
lands — mouse-select would silently do nothing. Same pattern as FdyDatepicker/FdyAutocomplete. -->
|
|
217
224
|
<li
|
|
218
225
|
v-for="(opt, i) in options"
|
|
219
226
|
:id="optionId(i)"
|
|
@@ -222,6 +229,7 @@ onBeforeUnmount((): void => {
|
|
|
222
229
|
:class="{ 'is-highlighted': i === highlighted }"
|
|
223
230
|
role="option"
|
|
224
231
|
:aria-selected="opt.value === modelValue"
|
|
232
|
+
@mousedown.prevent
|
|
225
233
|
@click="choose(i)"
|
|
226
234
|
@mousemove="setHighlight(i)"
|
|
227
235
|
>
|
|
@@ -21,6 +21,8 @@ const props = defineProps<{
|
|
|
21
21
|
max?: string;
|
|
22
22
|
locale?: string;
|
|
23
23
|
disabled?: boolean;
|
|
24
|
+
/** Locked/view mode for both pickers — focusable, values shown, but can't be opened or changed. */
|
|
25
|
+
readonly?: boolean;
|
|
24
26
|
invalid?: boolean;
|
|
25
27
|
describedby?: string;
|
|
26
28
|
startPlaceholder?: string;
|
|
@@ -57,6 +59,7 @@ function setEnd(end: string): void {
|
|
|
57
59
|
:max="startMax"
|
|
58
60
|
:locale="locale"
|
|
59
61
|
:disabled="disabled"
|
|
62
|
+
:readonly="readonly"
|
|
60
63
|
:invalid="invalid"
|
|
61
64
|
:describedby="describedby"
|
|
62
65
|
:placeholder="startPlaceholder ?? 'Dari'"
|
|
@@ -69,6 +72,7 @@ function setEnd(end: string): void {
|
|
|
69
72
|
:max="max"
|
|
70
73
|
:locale="locale"
|
|
71
74
|
:disabled="disabled"
|
|
75
|
+
:readonly="readonly"
|
|
72
76
|
:invalid="invalid"
|
|
73
77
|
:describedby="describedby"
|
|
74
78
|
:placeholder="endPlaceholder ?? 'Sampai'"
|
|
@@ -31,6 +31,8 @@ const props = defineProps<{
|
|
|
31
31
|
locale?: string;
|
|
32
32
|
placeholder?: string;
|
|
33
33
|
disabled?: boolean;
|
|
34
|
+
/** Locked/view mode: stays focusable and shows its date, but can't be opened, cleared, or changed. Unlike `disabled`, it keeps tab order and isn't greyed. */
|
|
35
|
+
readonly?: boolean;
|
|
34
36
|
invalid?: boolean;
|
|
35
37
|
describedby?: string;
|
|
36
38
|
id?: string;
|
|
@@ -105,10 +107,11 @@ const selectedDate: ComputedRef<Date | null> = computed((): Date | null => parse
|
|
|
105
107
|
const minDate: ComputedRef<Date | null> = computed((): Date | null => parseISO(props.min));
|
|
106
108
|
const maxDate: ComputedRef<Date | null> = computed((): Date | null => parseISO(props.max));
|
|
107
109
|
const isDisabled: ComputedRef<boolean> = computed((): boolean => props.disabled === true);
|
|
110
|
+
const isReadonly: ComputedRef<boolean> = computed((): boolean => props.readonly === true);
|
|
108
111
|
const isInvalid: ComputedRef<boolean> = computed((): boolean => props.invalid === true);
|
|
109
112
|
const displayPlaceholder: ComputedRef<string> = computed((): string => props.placeholder ?? 'Select date');
|
|
110
113
|
const showClear: ComputedRef<boolean> = computed(
|
|
111
|
-
(): boolean => props.clearable === true && selectedDate.value !== null && isDisabled.value === false,
|
|
114
|
+
(): boolean => props.clearable === true && selectedDate.value !== null && isDisabled.value === false && isReadonly.value === false,
|
|
112
115
|
);
|
|
113
116
|
const prevMonthLabelText: ComputedRef<string> = computed((): string => props.prevMonthLabel ?? 'Previous month');
|
|
114
117
|
const nextMonthLabelText: ComputedRef<string> = computed((): string => props.nextMonthLabel ?? 'Next month');
|
|
@@ -211,7 +214,7 @@ function clearValue(): void {
|
|
|
211
214
|
}
|
|
212
215
|
|
|
213
216
|
function openPanel(): void {
|
|
214
|
-
if (isDisabled.value || open.value) return;
|
|
217
|
+
if (isDisabled.value || isReadonly.value || open.value) return;
|
|
215
218
|
focusDate.value = selectedDate.value ?? focusDate.value ?? new Date();
|
|
216
219
|
view.value = new Date(focusDate.value.getFullYear(), focusDate.value.getMonth(), 1);
|
|
217
220
|
open.value = true;
|
|
@@ -314,6 +317,7 @@ onBeforeUnmount((): void => {
|
|
|
314
317
|
:aria-expanded="open"
|
|
315
318
|
:aria-labelledby="ariaLabelledby"
|
|
316
319
|
:aria-invalid="isInvalid ? 'true' : undefined"
|
|
320
|
+
:aria-readonly="isReadonly ? 'true' : undefined"
|
|
317
321
|
:aria-describedby="describedby"
|
|
318
322
|
:disabled="isDisabled"
|
|
319
323
|
@click="toggle"
|
package/dist/freeday-datetime.js
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* Markup: <div data-fdy-datetimepicker>
|
|
10
10
|
* <div data-fdy-datepicker data-value="2026-07-21"></div>
|
|
11
11
|
* <div data-fdy-timepicker data-value="14:30"></div></div>
|
|
12
|
+
* State: add data-fdy-disabled or data-fdy-invalid on the wrapper to reflect it onto both
|
|
13
|
+
* child triggers as one control (read-only is adapter-only — a vanilla datetime is interactive).
|
|
12
14
|
*/
|
|
13
15
|
(function () {
|
|
14
16
|
'use strict';
|
|
@@ -22,6 +24,28 @@
|
|
|
22
24
|
var tp = wrap.querySelector('[data-fdy-timepicker]');
|
|
23
25
|
if (!dp || !tp) return;
|
|
24
26
|
|
|
27
|
+
// Reflect one wrapper-level state onto BOTH child triggers so a datetime reads as a single
|
|
28
|
+
// disabled/invalid control. Deferred with setTimeout(0) — NOT a microtask: the timepicker
|
|
29
|
+
// enhancer registers its DOMContentLoaded init AFTER this composer, and microtasks drain
|
|
30
|
+
// *between* listeners (so a microtask would run before the timepicker trigger exists). A
|
|
31
|
+
// macrotask waits until the whole init pass is done and both triggers are built. (readonly is
|
|
32
|
+
// intentionally not supported here: a vanilla datetime is interactive; adapters cover read-only.)
|
|
33
|
+
if (wrap.hasAttribute('data-fdy-disabled') || wrap.hasAttribute('data-fdy-invalid')) {
|
|
34
|
+
var applyState = function () {
|
|
35
|
+
var dpTrig = dp.querySelector('.fdy-datepicker__trigger');
|
|
36
|
+
var tpTrig = tp.querySelector('.fdy-timepicker__trigger');
|
|
37
|
+
if (wrap.hasAttribute('data-fdy-disabled')) {
|
|
38
|
+
if (dpTrig) dpTrig.disabled = true;
|
|
39
|
+
if (tpTrig) tpTrig.disabled = true;
|
|
40
|
+
}
|
|
41
|
+
if (wrap.hasAttribute('data-fdy-invalid')) {
|
|
42
|
+
if (dpTrig) dpTrig.setAttribute('aria-invalid', 'true');
|
|
43
|
+
if (tpTrig) tpTrig.setAttribute('aria-invalid', 'true');
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
setTimeout(applyState, 0);
|
|
47
|
+
}
|
|
48
|
+
|
|
25
49
|
var date = dp.getAttribute('data-value') || '';
|
|
26
50
|
var time = tp.getAttribute('data-value') || '';
|
|
27
51
|
|
package/dist/freeday.bundle.css
CHANGED
|
@@ -678,6 +678,9 @@ a { color: var(--color-primary); }
|
|
|
678
678
|
.fdy-cascade__trigger:focus-visible{outline:none;border-color:var(--color-primary);box-shadow:inset 0 1px 2px rgba(16,14,30,.04),0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
|
|
679
679
|
.fdy-cascade__trigger.is-open{border-color:var(--color-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
|
|
680
680
|
.fdy-cascade__trigger:disabled,.fdy-cascade__trigger[aria-disabled="true"]{opacity:.5;cursor:not-allowed;}
|
|
681
|
+
.fdy-cascade__trigger[aria-readonly="true"]{background:var(--color-surface-2);border-color:var(--color-border);cursor:default;}
|
|
682
|
+
.fdy-cascade__trigger[aria-readonly="true"]:hover{border-color:var(--color-border);}
|
|
683
|
+
.fdy-cascade__trigger[aria-readonly="true"]:focus-visible{border-color:var(--color-border-strong);box-shadow:none;}
|
|
681
684
|
.fdy-cascade--error .fdy-cascade__trigger,.fdy-cascade__trigger[aria-invalid="true"]{border-color:var(--color-danger);}
|
|
682
685
|
.fdy-cascade--error .fdy-cascade__trigger:focus-visible,.fdy-cascade--error .fdy-cascade__trigger.is-open,.fdy-cascade__trigger[aria-invalid="true"]:focus-visible{border-color:var(--color-danger);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-danger) 26%,transparent);}
|
|
683
686
|
.fdy-cascade__value{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
@@ -814,6 +817,11 @@ a { color: var(--color-primary); }
|
|
|
814
817
|
.fdy-combo__button:focus-visible{outline:none;border-color:var(--color-primary);box-shadow:inset 0 1px 2px rgba(16,14,30,.04),0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
|
|
815
818
|
.fdy-combo__button.is-open{border-color:var(--color-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
|
|
816
819
|
.fdy-combo__button:disabled,.fdy-combo__button[aria-disabled="true"]{opacity:.5;cursor:not-allowed;}
|
|
820
|
+
/* Read-only: stays focusable & shows its value at full contrast but can't open — muted surface,
|
|
821
|
+
softened border, default cursor, no focus-ring escalation (matches input[readonly], #39). */
|
|
822
|
+
.fdy-combo__button[aria-readonly="true"]{background:var(--color-surface-2);border-color:var(--color-border);cursor:default;}
|
|
823
|
+
.fdy-combo__button[aria-readonly="true"]:hover{border-color:var(--color-border);}
|
|
824
|
+
.fdy-combo__button[aria-readonly="true"]:focus-visible{border-color:var(--color-border-strong);box-shadow:none;}
|
|
817
825
|
.fdy-combo--error .fdy-combo__button,.fdy-combo__button[aria-invalid="true"]{border-color:var(--color-danger);}
|
|
818
826
|
.fdy-combo--error .fdy-combo__button:focus-visible,.fdy-combo--error .fdy-combo__button.is-open,.fdy-combo__button[aria-invalid="true"]:focus-visible{border-color:var(--color-danger);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-danger) 26%,transparent);}
|
|
819
827
|
.fdy-combo__value{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
@@ -844,6 +852,10 @@ a { color: var(--color-primary); }
|
|
|
844
852
|
.fdy-datepicker__trigger.is-open{border-color:var(--color-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
|
|
845
853
|
.fdy-datepicker--error .fdy-datepicker__trigger,.fdy-datepicker__trigger[aria-invalid="true"]{border-color:var(--color-danger);}
|
|
846
854
|
.fdy-datepicker--error .fdy-datepicker__trigger:focus-visible,.fdy-datepicker--error .fdy-datepicker__trigger.is-open,.fdy-datepicker__trigger[aria-invalid="true"]:focus-visible{border-color:var(--color-danger);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-danger) 26%,transparent);}
|
|
855
|
+
.fdy-datepicker__trigger:disabled,.fdy-datepicker__trigger[aria-disabled="true"]{opacity:.5;cursor:not-allowed;}
|
|
856
|
+
.fdy-datepicker__trigger[aria-readonly="true"]{background:var(--color-surface-2);border-color:var(--color-border);cursor:default;}
|
|
857
|
+
.fdy-datepicker__trigger[aria-readonly="true"]:hover{border-color:var(--color-border);}
|
|
858
|
+
.fdy-datepicker__trigger[aria-readonly="true"]:focus-visible{border-color:var(--color-border-strong);box-shadow:none;}
|
|
847
859
|
.fdy-datepicker__value{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
848
860
|
.fdy-datepicker__value--placeholder{color:var(--color-text-subtle);}
|
|
849
861
|
.fdy-datepicker__icon{flex:none;display:inline-flex;color:var(--color-text-subtle);}
|
|
@@ -943,6 +955,7 @@ a { color: var(--color-primary); }
|
|
|
943
955
|
.fdy-dropzone:hover{border-color:var(--color-text-muted);}
|
|
944
956
|
.fdy-dropzone:focus-visible{outline:none;border-color:var(--color-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
|
|
945
957
|
.fdy-dropzone.is-dragover{border-color:var(--color-primary);background:var(--color-primary-soft);color:var(--color-primary-strong);}
|
|
958
|
+
.fdy-dropzone[aria-disabled="true"],.fdy-dropzone.is-disabled{opacity:.55;cursor:not-allowed;pointer-events:none;}
|
|
946
959
|
.fdy-dropzone__icon{color:var(--color-text-subtle);}
|
|
947
960
|
.fdy-dropzone.is-dragover .fdy-dropzone__icon{color:var(--color-primary);}
|
|
948
961
|
.fdy-dropzone__icon svg{display:block;width:2rem;height:2rem;}
|
|
@@ -1058,6 +1071,12 @@ fieldset.fdy-field>legend{padding:0;float:none;}
|
|
|
1058
1071
|
.fdy-input:hover,.fdy-textarea:hover{border-color:var(--color-text-muted);}
|
|
1059
1072
|
.fdy-input:focus,.fdy-textarea:focus{outline:none;border-color:var(--color-primary);box-shadow:inset 0 1px 2px rgba(16,14,30,.04),0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
|
|
1060
1073
|
.fdy-input:disabled,.fdy-textarea:disabled,.fdy-input[aria-disabled="true"],.fdy-textarea[aria-disabled="true"]{opacity:.5;cursor:not-allowed;}
|
|
1074
|
+
/* Read-only (#39): the value is real and may be read/copied, only not edited. Full contrast (unlike
|
|
1075
|
+
:disabled which dims to placeholder-grey), muted surface + softened border so it still reads as
|
|
1076
|
+
inert, default cursor, no focus-ring escalation. */
|
|
1077
|
+
.fdy-input[readonly],.fdy-textarea[readonly]{background:var(--color-surface-2);border-color:var(--color-border);color:var(--color-text);cursor:default;}
|
|
1078
|
+
.fdy-input[readonly]:hover,.fdy-textarea[readonly]:hover{border-color:var(--color-border);}
|
|
1079
|
+
.fdy-input[readonly]:focus-visible,.fdy-textarea[readonly]:focus-visible{border-color:var(--color-border-strong);box-shadow:none;}
|
|
1061
1080
|
.fdy-input--error,.fdy-input[aria-invalid="true"],.fdy-textarea[aria-invalid="true"]{border-color:var(--color-danger);}
|
|
1062
1081
|
.fdy-input--error:focus,.fdy-input[aria-invalid="true"]:focus,.fdy-textarea[aria-invalid="true"]:focus{border-color:var(--color-danger);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-danger) 26%,transparent);}
|
|
1063
1082
|
.fdy-help{font-size:var(--text-xs);color:var(--color-text-muted);}
|
|
@@ -1144,6 +1163,12 @@ fieldset.fdy-field>legend{padding:0;float:none;}
|
|
|
1144
1163
|
.fdy-rating__star.is-filled svg{fill:var(--color-warning);stroke:var(--color-warning);}
|
|
1145
1164
|
.fdy-rating__star input:focus-visible + svg{outline:2px solid var(--color-primary);outline-offset:2px;border-radius:var(--radius-xs);}
|
|
1146
1165
|
.fdy-rating--readonly .fdy-rating__star{cursor:default;}
|
|
1166
|
+
/* Disabled: not editable and greyed (readonly stays full-strength; disabled dims). */
|
|
1167
|
+
.fdy-rating--disabled .fdy-rating__star{cursor:not-allowed;}
|
|
1168
|
+
.fdy-rating--disabled .fdy-rating__star svg{opacity:.5;}
|
|
1169
|
+
/* Invalid: tint the (outline) empty stars danger so a required-but-empty rating reads as an error,
|
|
1170
|
+
paired with a help/error message — never colour alone. */
|
|
1171
|
+
.fdy-rating--error .fdy-rating__star svg{stroke:var(--color-danger);}
|
|
1147
1172
|
.fdy-rating--sm .fdy-rating__star svg{width:1.1rem;height:1.1rem;}
|
|
1148
1173
|
|
|
1149
1174
|
/* Freeday — Selection controls */
|
|
@@ -1305,6 +1330,8 @@ fieldset.fdy-field>legend{padding:0;float:none;}
|
|
|
1305
1330
|
.fdy-tabs__list{display:flex;gap:var(--space-1);border-bottom:var(--bw) solid var(--color-border);overflow-x:auto;}
|
|
1306
1331
|
.fdy-tabs__tab{appearance:none;border:0;background:transparent;cursor:pointer;white-space:nowrap;font-family:var(--font-body);font-size:var(--text-sm);font-weight:var(--weight-medium);color:var(--color-text-muted);padding:var(--space-3) var(--space-4);border-bottom:2px solid transparent;margin-bottom:-1px;transition:color var(--dur-fast) var(--ease-standard),border-color var(--dur-fast) var(--ease-standard);}
|
|
1307
1332
|
.fdy-tabs__tab:hover{color:var(--color-text);}
|
|
1333
|
+
.fdy-tabs__tab:disabled,.fdy-tabs__tab[aria-disabled="true"]{opacity:.5;cursor:not-allowed;color:var(--color-text-muted);}
|
|
1334
|
+
.fdy-tabs__tab:disabled:hover,.fdy-tabs__tab[aria-disabled="true"]:hover{color:var(--color-text-muted);}
|
|
1308
1335
|
.fdy-tabs__tab[aria-selected="true"]{color:var(--color-primary-strong);border-bottom-color:var(--color-primary);}
|
|
1309
1336
|
.fdy-tabs__tab:focus-visible{outline:none;border-radius:var(--radius-sm);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
|
|
1310
1337
|
.fdy-tabs__panel{padding:var(--space-5) 0;}
|
|
@@ -1335,6 +1362,7 @@ fieldset.fdy-field>legend{padding:0;float:none;}
|
|
|
1335
1362
|
.fdy-timepicker__trigger.is-open{border-color:var(--color-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
|
|
1336
1363
|
.fdy-timepicker--error .fdy-timepicker__trigger,.fdy-timepicker__trigger[aria-invalid="true"]{border-color:var(--color-danger);}
|
|
1337
1364
|
.fdy-timepicker--error .fdy-timepicker__trigger:focus-visible,.fdy-timepicker--error .fdy-timepicker__trigger.is-open,.fdy-timepicker__trigger[aria-invalid="true"]:focus-visible{border-color:var(--color-danger);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-danger) 26%,transparent);}
|
|
1365
|
+
.fdy-timepicker__trigger:disabled,.fdy-timepicker__trigger[aria-disabled="true"]{opacity:.5;cursor:not-allowed;}
|
|
1338
1366
|
.fdy-timepicker__value{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-variant-numeric:tabular-nums;}
|
|
1339
1367
|
.fdy-timepicker__value--placeholder{color:var(--color-text-subtle);}
|
|
1340
1368
|
.fdy-timepicker__icon{flex:none;display:inline-flex;color:var(--color-text-subtle);}
|
|
@@ -1387,6 +1415,8 @@ fieldset.fdy-field>legend{padding:0;float:none;}
|
|
|
1387
1415
|
.fdy-tree__icon{flex:none;width:1rem;height:1rem;color:var(--color-text-muted);}
|
|
1388
1416
|
.fdy-tree__leaf{padding-left:calc(var(--space-2) * 2 + .9rem);}
|
|
1389
1417
|
.fdy-tree__leaf .fdy-tree__icon{color:var(--color-text-subtle);}
|
|
1418
|
+
/* Disabled row (branch summary or leaf): dimmed, no hover, not interactive. */
|
|
1419
|
+
.fdy-tree__leaf[aria-disabled="true"],.fdy-tree__branch[aria-disabled="true"] > summary{opacity:.5;cursor:not-allowed;pointer-events:none;}
|
|
1390
1420
|
|
|
1391
1421
|
/* Checkbox variant — a selectable tree (cascading parent/child via freeday-tree.js).
|
|
1392
1422
|
* A native <input class="fdy-checkbox"> sits at the start of every row; a partially
|
package/dist/freeday.css
CHANGED
|
@@ -333,6 +333,9 @@ a { color: var(--color-primary); }
|
|
|
333
333
|
.fdy-cascade__trigger:focus-visible{outline:none;border-color:var(--color-primary);box-shadow:inset 0 1px 2px rgba(16,14,30,.04),0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
|
|
334
334
|
.fdy-cascade__trigger.is-open{border-color:var(--color-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
|
|
335
335
|
.fdy-cascade__trigger:disabled,.fdy-cascade__trigger[aria-disabled="true"]{opacity:.5;cursor:not-allowed;}
|
|
336
|
+
.fdy-cascade__trigger[aria-readonly="true"]{background:var(--color-surface-2);border-color:var(--color-border);cursor:default;}
|
|
337
|
+
.fdy-cascade__trigger[aria-readonly="true"]:hover{border-color:var(--color-border);}
|
|
338
|
+
.fdy-cascade__trigger[aria-readonly="true"]:focus-visible{border-color:var(--color-border-strong);box-shadow:none;}
|
|
336
339
|
.fdy-cascade--error .fdy-cascade__trigger,.fdy-cascade__trigger[aria-invalid="true"]{border-color:var(--color-danger);}
|
|
337
340
|
.fdy-cascade--error .fdy-cascade__trigger:focus-visible,.fdy-cascade--error .fdy-cascade__trigger.is-open,.fdy-cascade__trigger[aria-invalid="true"]:focus-visible{border-color:var(--color-danger);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-danger) 26%,transparent);}
|
|
338
341
|
.fdy-cascade__value{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
@@ -469,6 +472,11 @@ a { color: var(--color-primary); }
|
|
|
469
472
|
.fdy-combo__button:focus-visible{outline:none;border-color:var(--color-primary);box-shadow:inset 0 1px 2px rgba(16,14,30,.04),0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
|
|
470
473
|
.fdy-combo__button.is-open{border-color:var(--color-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
|
|
471
474
|
.fdy-combo__button:disabled,.fdy-combo__button[aria-disabled="true"]{opacity:.5;cursor:not-allowed;}
|
|
475
|
+
/* Read-only: stays focusable & shows its value at full contrast but can't open — muted surface,
|
|
476
|
+
softened border, default cursor, no focus-ring escalation (matches input[readonly], #39). */
|
|
477
|
+
.fdy-combo__button[aria-readonly="true"]{background:var(--color-surface-2);border-color:var(--color-border);cursor:default;}
|
|
478
|
+
.fdy-combo__button[aria-readonly="true"]:hover{border-color:var(--color-border);}
|
|
479
|
+
.fdy-combo__button[aria-readonly="true"]:focus-visible{border-color:var(--color-border-strong);box-shadow:none;}
|
|
472
480
|
.fdy-combo--error .fdy-combo__button,.fdy-combo__button[aria-invalid="true"]{border-color:var(--color-danger);}
|
|
473
481
|
.fdy-combo--error .fdy-combo__button:focus-visible,.fdy-combo--error .fdy-combo__button.is-open,.fdy-combo__button[aria-invalid="true"]:focus-visible{border-color:var(--color-danger);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-danger) 26%,transparent);}
|
|
474
482
|
.fdy-combo__value{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
@@ -499,6 +507,10 @@ a { color: var(--color-primary); }
|
|
|
499
507
|
.fdy-datepicker__trigger.is-open{border-color:var(--color-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
|
|
500
508
|
.fdy-datepicker--error .fdy-datepicker__trigger,.fdy-datepicker__trigger[aria-invalid="true"]{border-color:var(--color-danger);}
|
|
501
509
|
.fdy-datepicker--error .fdy-datepicker__trigger:focus-visible,.fdy-datepicker--error .fdy-datepicker__trigger.is-open,.fdy-datepicker__trigger[aria-invalid="true"]:focus-visible{border-color:var(--color-danger);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-danger) 26%,transparent);}
|
|
510
|
+
.fdy-datepicker__trigger:disabled,.fdy-datepicker__trigger[aria-disabled="true"]{opacity:.5;cursor:not-allowed;}
|
|
511
|
+
.fdy-datepicker__trigger[aria-readonly="true"]{background:var(--color-surface-2);border-color:var(--color-border);cursor:default;}
|
|
512
|
+
.fdy-datepicker__trigger[aria-readonly="true"]:hover{border-color:var(--color-border);}
|
|
513
|
+
.fdy-datepicker__trigger[aria-readonly="true"]:focus-visible{border-color:var(--color-border-strong);box-shadow:none;}
|
|
502
514
|
.fdy-datepicker__value{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
503
515
|
.fdy-datepicker__value--placeholder{color:var(--color-text-subtle);}
|
|
504
516
|
.fdy-datepicker__icon{flex:none;display:inline-flex;color:var(--color-text-subtle);}
|
|
@@ -598,6 +610,7 @@ a { color: var(--color-primary); }
|
|
|
598
610
|
.fdy-dropzone:hover{border-color:var(--color-text-muted);}
|
|
599
611
|
.fdy-dropzone:focus-visible{outline:none;border-color:var(--color-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
|
|
600
612
|
.fdy-dropzone.is-dragover{border-color:var(--color-primary);background:var(--color-primary-soft);color:var(--color-primary-strong);}
|
|
613
|
+
.fdy-dropzone[aria-disabled="true"],.fdy-dropzone.is-disabled{opacity:.55;cursor:not-allowed;pointer-events:none;}
|
|
601
614
|
.fdy-dropzone__icon{color:var(--color-text-subtle);}
|
|
602
615
|
.fdy-dropzone.is-dragover .fdy-dropzone__icon{color:var(--color-primary);}
|
|
603
616
|
.fdy-dropzone__icon svg{display:block;width:2rem;height:2rem;}
|
|
@@ -713,6 +726,12 @@ fieldset.fdy-field>legend{padding:0;float:none;}
|
|
|
713
726
|
.fdy-input:hover,.fdy-textarea:hover{border-color:var(--color-text-muted);}
|
|
714
727
|
.fdy-input:focus,.fdy-textarea:focus{outline:none;border-color:var(--color-primary);box-shadow:inset 0 1px 2px rgba(16,14,30,.04),0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
|
|
715
728
|
.fdy-input:disabled,.fdy-textarea:disabled,.fdy-input[aria-disabled="true"],.fdy-textarea[aria-disabled="true"]{opacity:.5;cursor:not-allowed;}
|
|
729
|
+
/* Read-only (#39): the value is real and may be read/copied, only not edited. Full contrast (unlike
|
|
730
|
+
:disabled which dims to placeholder-grey), muted surface + softened border so it still reads as
|
|
731
|
+
inert, default cursor, no focus-ring escalation. */
|
|
732
|
+
.fdy-input[readonly],.fdy-textarea[readonly]{background:var(--color-surface-2);border-color:var(--color-border);color:var(--color-text);cursor:default;}
|
|
733
|
+
.fdy-input[readonly]:hover,.fdy-textarea[readonly]:hover{border-color:var(--color-border);}
|
|
734
|
+
.fdy-input[readonly]:focus-visible,.fdy-textarea[readonly]:focus-visible{border-color:var(--color-border-strong);box-shadow:none;}
|
|
716
735
|
.fdy-input--error,.fdy-input[aria-invalid="true"],.fdy-textarea[aria-invalid="true"]{border-color:var(--color-danger);}
|
|
717
736
|
.fdy-input--error:focus,.fdy-input[aria-invalid="true"]:focus,.fdy-textarea[aria-invalid="true"]:focus{border-color:var(--color-danger);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-danger) 26%,transparent);}
|
|
718
737
|
.fdy-help{font-size:var(--text-xs);color:var(--color-text-muted);}
|
|
@@ -799,6 +818,12 @@ fieldset.fdy-field>legend{padding:0;float:none;}
|
|
|
799
818
|
.fdy-rating__star.is-filled svg{fill:var(--color-warning);stroke:var(--color-warning);}
|
|
800
819
|
.fdy-rating__star input:focus-visible + svg{outline:2px solid var(--color-primary);outline-offset:2px;border-radius:var(--radius-xs);}
|
|
801
820
|
.fdy-rating--readonly .fdy-rating__star{cursor:default;}
|
|
821
|
+
/* Disabled: not editable and greyed (readonly stays full-strength; disabled dims). */
|
|
822
|
+
.fdy-rating--disabled .fdy-rating__star{cursor:not-allowed;}
|
|
823
|
+
.fdy-rating--disabled .fdy-rating__star svg{opacity:.5;}
|
|
824
|
+
/* Invalid: tint the (outline) empty stars danger so a required-but-empty rating reads as an error,
|
|
825
|
+
paired with a help/error message — never colour alone. */
|
|
826
|
+
.fdy-rating--error .fdy-rating__star svg{stroke:var(--color-danger);}
|
|
802
827
|
.fdy-rating--sm .fdy-rating__star svg{width:1.1rem;height:1.1rem;}
|
|
803
828
|
|
|
804
829
|
/* Freeday — Selection controls */
|
|
@@ -960,6 +985,8 @@ fieldset.fdy-field>legend{padding:0;float:none;}
|
|
|
960
985
|
.fdy-tabs__list{display:flex;gap:var(--space-1);border-bottom:var(--bw) solid var(--color-border);overflow-x:auto;}
|
|
961
986
|
.fdy-tabs__tab{appearance:none;border:0;background:transparent;cursor:pointer;white-space:nowrap;font-family:var(--font-body);font-size:var(--text-sm);font-weight:var(--weight-medium);color:var(--color-text-muted);padding:var(--space-3) var(--space-4);border-bottom:2px solid transparent;margin-bottom:-1px;transition:color var(--dur-fast) var(--ease-standard),border-color var(--dur-fast) var(--ease-standard);}
|
|
962
987
|
.fdy-tabs__tab:hover{color:var(--color-text);}
|
|
988
|
+
.fdy-tabs__tab:disabled,.fdy-tabs__tab[aria-disabled="true"]{opacity:.5;cursor:not-allowed;color:var(--color-text-muted);}
|
|
989
|
+
.fdy-tabs__tab:disabled:hover,.fdy-tabs__tab[aria-disabled="true"]:hover{color:var(--color-text-muted);}
|
|
963
990
|
.fdy-tabs__tab[aria-selected="true"]{color:var(--color-primary-strong);border-bottom-color:var(--color-primary);}
|
|
964
991
|
.fdy-tabs__tab:focus-visible{outline:none;border-radius:var(--radius-sm);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
|
|
965
992
|
.fdy-tabs__panel{padding:var(--space-5) 0;}
|
|
@@ -990,6 +1017,7 @@ fieldset.fdy-field>legend{padding:0;float:none;}
|
|
|
990
1017
|
.fdy-timepicker__trigger.is-open{border-color:var(--color-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
|
|
991
1018
|
.fdy-timepicker--error .fdy-timepicker__trigger,.fdy-timepicker__trigger[aria-invalid="true"]{border-color:var(--color-danger);}
|
|
992
1019
|
.fdy-timepicker--error .fdy-timepicker__trigger:focus-visible,.fdy-timepicker--error .fdy-timepicker__trigger.is-open,.fdy-timepicker__trigger[aria-invalid="true"]:focus-visible{border-color:var(--color-danger);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-danger) 26%,transparent);}
|
|
1020
|
+
.fdy-timepicker__trigger:disabled,.fdy-timepicker__trigger[aria-disabled="true"]{opacity:.5;cursor:not-allowed;}
|
|
993
1021
|
.fdy-timepicker__value{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-variant-numeric:tabular-nums;}
|
|
994
1022
|
.fdy-timepicker__value--placeholder{color:var(--color-text-subtle);}
|
|
995
1023
|
.fdy-timepicker__icon{flex:none;display:inline-flex;color:var(--color-text-subtle);}
|
|
@@ -1042,6 +1070,8 @@ fieldset.fdy-field>legend{padding:0;float:none;}
|
|
|
1042
1070
|
.fdy-tree__icon{flex:none;width:1rem;height:1rem;color:var(--color-text-muted);}
|
|
1043
1071
|
.fdy-tree__leaf{padding-left:calc(var(--space-2) * 2 + .9rem);}
|
|
1044
1072
|
.fdy-tree__leaf .fdy-tree__icon{color:var(--color-text-subtle);}
|
|
1073
|
+
/* Disabled row (branch summary or leaf): dimmed, no hover, not interactive. */
|
|
1074
|
+
.fdy-tree__leaf[aria-disabled="true"],.fdy-tree__branch[aria-disabled="true"] > summary{opacity:.5;cursor:not-allowed;pointer-events:none;}
|
|
1045
1075
|
|
|
1046
1076
|
/* Checkbox variant — a selectable tree (cascading parent/child via freeday-tree.js).
|
|
1047
1077
|
* A native <input class="fdy-checkbox"> sits at the start of every row; a partially
|
package/dist/freeday.js
CHANGED
|
@@ -1618,6 +1618,8 @@
|
|
|
1618
1618
|
* Markup: <div data-fdy-datetimepicker>
|
|
1619
1619
|
* <div data-fdy-datepicker data-value="2026-07-21"></div>
|
|
1620
1620
|
* <div data-fdy-timepicker data-value="14:30"></div></div>
|
|
1621
|
+
* State: add data-fdy-disabled or data-fdy-invalid on the wrapper to reflect it onto both
|
|
1622
|
+
* child triggers as one control (read-only is adapter-only — a vanilla datetime is interactive).
|
|
1621
1623
|
*/
|
|
1622
1624
|
(function () {
|
|
1623
1625
|
'use strict';
|
|
@@ -1631,6 +1633,28 @@
|
|
|
1631
1633
|
var tp = wrap.querySelector('[data-fdy-timepicker]');
|
|
1632
1634
|
if (!dp || !tp) return;
|
|
1633
1635
|
|
|
1636
|
+
// Reflect one wrapper-level state onto BOTH child triggers so a datetime reads as a single
|
|
1637
|
+
// disabled/invalid control. Deferred with setTimeout(0) — NOT a microtask: the timepicker
|
|
1638
|
+
// enhancer registers its DOMContentLoaded init AFTER this composer, and microtasks drain
|
|
1639
|
+
// *between* listeners (so a microtask would run before the timepicker trigger exists). A
|
|
1640
|
+
// macrotask waits until the whole init pass is done and both triggers are built. (readonly is
|
|
1641
|
+
// intentionally not supported here: a vanilla datetime is interactive; adapters cover read-only.)
|
|
1642
|
+
if (wrap.hasAttribute('data-fdy-disabled') || wrap.hasAttribute('data-fdy-invalid')) {
|
|
1643
|
+
var applyState = function () {
|
|
1644
|
+
var dpTrig = dp.querySelector('.fdy-datepicker__trigger');
|
|
1645
|
+
var tpTrig = tp.querySelector('.fdy-timepicker__trigger');
|
|
1646
|
+
if (wrap.hasAttribute('data-fdy-disabled')) {
|
|
1647
|
+
if (dpTrig) dpTrig.disabled = true;
|
|
1648
|
+
if (tpTrig) tpTrig.disabled = true;
|
|
1649
|
+
}
|
|
1650
|
+
if (wrap.hasAttribute('data-fdy-invalid')) {
|
|
1651
|
+
if (dpTrig) dpTrig.setAttribute('aria-invalid', 'true');
|
|
1652
|
+
if (tpTrig) tpTrig.setAttribute('aria-invalid', 'true');
|
|
1653
|
+
}
|
|
1654
|
+
};
|
|
1655
|
+
setTimeout(applyState, 0);
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1634
1658
|
var date = dp.getAttribute('data-value') || '';
|
|
1635
1659
|
var time = tp.getAttribute('data-value') || '';
|
|
1636
1660
|
|
package/package.json
CHANGED
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
.fdy-cascade__trigger:focus-visible{outline:none;border-color:var(--color-primary);box-shadow:inset 0 1px 2px rgba(16,14,30,.04),0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
|
|
11
11
|
.fdy-cascade__trigger.is-open{border-color:var(--color-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
|
|
12
12
|
.fdy-cascade__trigger:disabled,.fdy-cascade__trigger[aria-disabled="true"]{opacity:.5;cursor:not-allowed;}
|
|
13
|
+
.fdy-cascade__trigger[aria-readonly="true"]{background:var(--color-surface-2);border-color:var(--color-border);cursor:default;}
|
|
14
|
+
.fdy-cascade__trigger[aria-readonly="true"]:hover{border-color:var(--color-border);}
|
|
15
|
+
.fdy-cascade__trigger[aria-readonly="true"]:focus-visible{border-color:var(--color-border-strong);box-shadow:none;}
|
|
13
16
|
.fdy-cascade--error .fdy-cascade__trigger,.fdy-cascade__trigger[aria-invalid="true"]{border-color:var(--color-danger);}
|
|
14
17
|
.fdy-cascade--error .fdy-cascade__trigger:focus-visible,.fdy-cascade--error .fdy-cascade__trigger.is-open,.fdy-cascade__trigger[aria-invalid="true"]:focus-visible{border-color:var(--color-danger);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-danger) 26%,transparent);}
|
|
15
18
|
.fdy-cascade__value{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
package/src/components/combo.css
CHANGED
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
.fdy-combo__button:focus-visible{outline:none;border-color:var(--color-primary);box-shadow:inset 0 1px 2px rgba(16,14,30,.04),0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
|
|
6
6
|
.fdy-combo__button.is-open{border-color:var(--color-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
|
|
7
7
|
.fdy-combo__button:disabled,.fdy-combo__button[aria-disabled="true"]{opacity:.5;cursor:not-allowed;}
|
|
8
|
+
/* Read-only: stays focusable & shows its value at full contrast but can't open — muted surface,
|
|
9
|
+
softened border, default cursor, no focus-ring escalation (matches input[readonly], #39). */
|
|
10
|
+
.fdy-combo__button[aria-readonly="true"]{background:var(--color-surface-2);border-color:var(--color-border);cursor:default;}
|
|
11
|
+
.fdy-combo__button[aria-readonly="true"]:hover{border-color:var(--color-border);}
|
|
12
|
+
.fdy-combo__button[aria-readonly="true"]:focus-visible{border-color:var(--color-border-strong);box-shadow:none;}
|
|
8
13
|
.fdy-combo--error .fdy-combo__button,.fdy-combo__button[aria-invalid="true"]{border-color:var(--color-danger);}
|
|
9
14
|
.fdy-combo--error .fdy-combo__button:focus-visible,.fdy-combo--error .fdy-combo__button.is-open,.fdy-combo__button[aria-invalid="true"]:focus-visible{border-color:var(--color-danger);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-danger) 26%,transparent);}
|
|
10
15
|
.fdy-combo__value{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
.fdy-datepicker__trigger.is-open{border-color:var(--color-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
|
|
8
8
|
.fdy-datepicker--error .fdy-datepicker__trigger,.fdy-datepicker__trigger[aria-invalid="true"]{border-color:var(--color-danger);}
|
|
9
9
|
.fdy-datepicker--error .fdy-datepicker__trigger:focus-visible,.fdy-datepicker--error .fdy-datepicker__trigger.is-open,.fdy-datepicker__trigger[aria-invalid="true"]:focus-visible{border-color:var(--color-danger);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-danger) 26%,transparent);}
|
|
10
|
+
.fdy-datepicker__trigger:disabled,.fdy-datepicker__trigger[aria-disabled="true"]{opacity:.5;cursor:not-allowed;}
|
|
11
|
+
.fdy-datepicker__trigger[aria-readonly="true"]{background:var(--color-surface-2);border-color:var(--color-border);cursor:default;}
|
|
12
|
+
.fdy-datepicker__trigger[aria-readonly="true"]:hover{border-color:var(--color-border);}
|
|
13
|
+
.fdy-datepicker__trigger[aria-readonly="true"]:focus-visible{border-color:var(--color-border-strong);box-shadow:none;}
|
|
10
14
|
.fdy-datepicker__value{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
11
15
|
.fdy-datepicker__value--placeholder{color:var(--color-text-subtle);}
|
|
12
16
|
.fdy-datepicker__icon{flex:none;display:inline-flex;color:var(--color-text-subtle);}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
.fdy-dropzone:hover{border-color:var(--color-text-muted);}
|
|
5
5
|
.fdy-dropzone:focus-visible{outline:none;border-color:var(--color-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
|
|
6
6
|
.fdy-dropzone.is-dragover{border-color:var(--color-primary);background:var(--color-primary-soft);color:var(--color-primary-strong);}
|
|
7
|
+
.fdy-dropzone[aria-disabled="true"],.fdy-dropzone.is-disabled{opacity:.55;cursor:not-allowed;pointer-events:none;}
|
|
7
8
|
.fdy-dropzone__icon{color:var(--color-text-subtle);}
|
|
8
9
|
.fdy-dropzone.is-dragover .fdy-dropzone__icon{color:var(--color-primary);}
|
|
9
10
|
.fdy-dropzone__icon svg{display:block;width:2rem;height:2rem;}
|
package/src/components/input.css
CHANGED
|
@@ -12,6 +12,12 @@ fieldset.fdy-field>legend{padding:0;float:none;}
|
|
|
12
12
|
.fdy-input:hover,.fdy-textarea:hover{border-color:var(--color-text-muted);}
|
|
13
13
|
.fdy-input:focus,.fdy-textarea:focus{outline:none;border-color:var(--color-primary);box-shadow:inset 0 1px 2px rgba(16,14,30,.04),0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
|
|
14
14
|
.fdy-input:disabled,.fdy-textarea:disabled,.fdy-input[aria-disabled="true"],.fdy-textarea[aria-disabled="true"]{opacity:.5;cursor:not-allowed;}
|
|
15
|
+
/* Read-only (#39): the value is real and may be read/copied, only not edited. Full contrast (unlike
|
|
16
|
+
:disabled which dims to placeholder-grey), muted surface + softened border so it still reads as
|
|
17
|
+
inert, default cursor, no focus-ring escalation. */
|
|
18
|
+
.fdy-input[readonly],.fdy-textarea[readonly]{background:var(--color-surface-2);border-color:var(--color-border);color:var(--color-text);cursor:default;}
|
|
19
|
+
.fdy-input[readonly]:hover,.fdy-textarea[readonly]:hover{border-color:var(--color-border);}
|
|
20
|
+
.fdy-input[readonly]:focus-visible,.fdy-textarea[readonly]:focus-visible{border-color:var(--color-border-strong);box-shadow:none;}
|
|
15
21
|
.fdy-input--error,.fdy-input[aria-invalid="true"],.fdy-textarea[aria-invalid="true"]{border-color:var(--color-danger);}
|
|
16
22
|
.fdy-input--error:focus,.fdy-input[aria-invalid="true"]:focus,.fdy-textarea[aria-invalid="true"]:focus{border-color:var(--color-danger);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-danger) 26%,transparent);}
|
|
17
23
|
.fdy-help{font-size:var(--text-xs);color:var(--color-text-muted);}
|
|
@@ -7,4 +7,10 @@
|
|
|
7
7
|
.fdy-rating__star.is-filled svg{fill:var(--color-warning);stroke:var(--color-warning);}
|
|
8
8
|
.fdy-rating__star input:focus-visible + svg{outline:2px solid var(--color-primary);outline-offset:2px;border-radius:var(--radius-xs);}
|
|
9
9
|
.fdy-rating--readonly .fdy-rating__star{cursor:default;}
|
|
10
|
+
/* Disabled: not editable and greyed (readonly stays full-strength; disabled dims). */
|
|
11
|
+
.fdy-rating--disabled .fdy-rating__star{cursor:not-allowed;}
|
|
12
|
+
.fdy-rating--disabled .fdy-rating__star svg{opacity:.5;}
|
|
13
|
+
/* Invalid: tint the (outline) empty stars danger so a required-but-empty rating reads as an error,
|
|
14
|
+
paired with a help/error message — never colour alone. */
|
|
15
|
+
.fdy-rating--error .fdy-rating__star svg{stroke:var(--color-danger);}
|
|
10
16
|
.fdy-rating--sm .fdy-rating__star svg{width:1.1rem;height:1.1rem;}
|
package/src/components/tabs.css
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
.fdy-tabs__list{display:flex;gap:var(--space-1);border-bottom:var(--bw) solid var(--color-border);overflow-x:auto;}
|
|
3
3
|
.fdy-tabs__tab{appearance:none;border:0;background:transparent;cursor:pointer;white-space:nowrap;font-family:var(--font-body);font-size:var(--text-sm);font-weight:var(--weight-medium);color:var(--color-text-muted);padding:var(--space-3) var(--space-4);border-bottom:2px solid transparent;margin-bottom:-1px;transition:color var(--dur-fast) var(--ease-standard),border-color var(--dur-fast) var(--ease-standard);}
|
|
4
4
|
.fdy-tabs__tab:hover{color:var(--color-text);}
|
|
5
|
+
.fdy-tabs__tab:disabled,.fdy-tabs__tab[aria-disabled="true"]{opacity:.5;cursor:not-allowed;color:var(--color-text-muted);}
|
|
6
|
+
.fdy-tabs__tab:disabled:hover,.fdy-tabs__tab[aria-disabled="true"]:hover{color:var(--color-text-muted);}
|
|
5
7
|
.fdy-tabs__tab[aria-selected="true"]{color:var(--color-primary-strong);border-bottom-color:var(--color-primary);}
|
|
6
8
|
.fdy-tabs__tab:focus-visible{outline:none;border-radius:var(--radius-sm);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
|
|
7
9
|
.fdy-tabs__panel{padding:var(--space-5) 0;}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
.fdy-timepicker__trigger.is-open{border-color:var(--color-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
|
|
9
9
|
.fdy-timepicker--error .fdy-timepicker__trigger,.fdy-timepicker__trigger[aria-invalid="true"]{border-color:var(--color-danger);}
|
|
10
10
|
.fdy-timepicker--error .fdy-timepicker__trigger:focus-visible,.fdy-timepicker--error .fdy-timepicker__trigger.is-open,.fdy-timepicker__trigger[aria-invalid="true"]:focus-visible{border-color:var(--color-danger);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-danger) 26%,transparent);}
|
|
11
|
+
.fdy-timepicker__trigger:disabled,.fdy-timepicker__trigger[aria-disabled="true"]{opacity:.5;cursor:not-allowed;}
|
|
11
12
|
.fdy-timepicker__value{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-variant-numeric:tabular-nums;}
|
|
12
13
|
.fdy-timepicker__value--placeholder{color:var(--color-text-subtle);}
|
|
13
14
|
.fdy-timepicker__icon{flex:none;display:inline-flex;color:var(--color-text-subtle);}
|
package/src/components/tree.css
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
.fdy-tree__icon{flex:none;width:1rem;height:1rem;color:var(--color-text-muted);}
|
|
13
13
|
.fdy-tree__leaf{padding-left:calc(var(--space-2) * 2 + .9rem);}
|
|
14
14
|
.fdy-tree__leaf .fdy-tree__icon{color:var(--color-text-subtle);}
|
|
15
|
+
/* Disabled row (branch summary or leaf): dimmed, no hover, not interactive. */
|
|
16
|
+
.fdy-tree__leaf[aria-disabled="true"],.fdy-tree__branch[aria-disabled="true"] > summary{opacity:.5;cursor:not-allowed;pointer-events:none;}
|
|
15
17
|
|
|
16
18
|
/* Checkbox variant — a selectable tree (cascading parent/child via freeday-tree.js).
|
|
17
19
|
* A native <input class="fdy-checkbox"> sits at the start of every row; a partially
|
package/src/freeday-datetime.js
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* Markup: <div data-fdy-datetimepicker>
|
|
10
10
|
* <div data-fdy-datepicker data-value="2026-07-21"></div>
|
|
11
11
|
* <div data-fdy-timepicker data-value="14:30"></div></div>
|
|
12
|
+
* State: add data-fdy-disabled or data-fdy-invalid on the wrapper to reflect it onto both
|
|
13
|
+
* child triggers as one control (read-only is adapter-only — a vanilla datetime is interactive).
|
|
12
14
|
*/
|
|
13
15
|
(function () {
|
|
14
16
|
'use strict';
|
|
@@ -22,6 +24,28 @@
|
|
|
22
24
|
var tp = wrap.querySelector('[data-fdy-timepicker]');
|
|
23
25
|
if (!dp || !tp) return;
|
|
24
26
|
|
|
27
|
+
// Reflect one wrapper-level state onto BOTH child triggers so a datetime reads as a single
|
|
28
|
+
// disabled/invalid control. Deferred with setTimeout(0) — NOT a microtask: the timepicker
|
|
29
|
+
// enhancer registers its DOMContentLoaded init AFTER this composer, and microtasks drain
|
|
30
|
+
// *between* listeners (so a microtask would run before the timepicker trigger exists). A
|
|
31
|
+
// macrotask waits until the whole init pass is done and both triggers are built. (readonly is
|
|
32
|
+
// intentionally not supported here: a vanilla datetime is interactive; adapters cover read-only.)
|
|
33
|
+
if (wrap.hasAttribute('data-fdy-disabled') || wrap.hasAttribute('data-fdy-invalid')) {
|
|
34
|
+
var applyState = function () {
|
|
35
|
+
var dpTrig = dp.querySelector('.fdy-datepicker__trigger');
|
|
36
|
+
var tpTrig = tp.querySelector('.fdy-timepicker__trigger');
|
|
37
|
+
if (wrap.hasAttribute('data-fdy-disabled')) {
|
|
38
|
+
if (dpTrig) dpTrig.disabled = true;
|
|
39
|
+
if (tpTrig) tpTrig.disabled = true;
|
|
40
|
+
}
|
|
41
|
+
if (wrap.hasAttribute('data-fdy-invalid')) {
|
|
42
|
+
if (dpTrig) dpTrig.setAttribute('aria-invalid', 'true');
|
|
43
|
+
if (tpTrig) tpTrig.setAttribute('aria-invalid', 'true');
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
setTimeout(applyState, 0);
|
|
47
|
+
}
|
|
48
|
+
|
|
25
49
|
var date = dp.getAttribute('data-value') || '';
|
|
26
50
|
var time = tp.getAttribute('data-value') || '';
|
|
27
51
|
|