@cahyo-dimas/freeday 1.41.0 → 1.43.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 +65 -0
- package/COMPONENTS.md +6 -0
- package/adapters/blazor/FdyCfl.razor +24 -1
- package/adapters/blazor/FdyCfl.razor.cs +65 -0
- package/adapters/react/components/FdyCfl.tsx +82 -10
- package/adapters/vue/components/FdyCfl.vue +73 -11
- package/dist/freeday.bundle.css +25 -1
- package/dist/freeday.css +25 -1
- package/package.json +2 -2
- package/src/components/composition.css +12 -0
- package/src/components/file-upload.css +6 -1
- package/src/components/table.css +7 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,71 @@
|
|
|
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.43.0] — 2026-08-19
|
|
7
|
+
Three crowding reports from one settlement screen (IDU_EMATE_APPL_WEB, #020). All three measured in
|
|
8
|
+
Chromium rather than read off the CSS — each is a layout outcome, invisible in the stylesheet.
|
|
9
|
+
### Fixed
|
|
10
|
+
- **A control in a table column collapsed** (#020). `.fdy-input` is `width:100%`, which in an
|
|
11
|
+
auto-layout table contributes **no intrinsic width**: the column shrank to its header text and took
|
|
12
|
+
the control with it. A NOTE box measured **36px** — too narrow to read what you typed into it.
|
|
13
|
+
Controls that are direct children of a `td` now keep a `7rem` floor (112px measured). Per-control
|
|
14
|
+
rather than per-column, because the table cannot know which columns hold controls. `width:100%` was
|
|
15
|
+
never wrong; it is only in a table that 100% of nothing is nothing.
|
|
16
|
+
- **A stat value wrapped between the currency and the number** (#020). `.fdy-stat__value` is
|
|
17
|
+
`--text-3xl`; `IDR 300,000.00` needs **224px** at that size and `.fdy-stats` tracks are
|
|
18
|
+
`minmax(11rem,1fr)`, so a three-across row at 660px gave it ~205px and it broke onto two lines.
|
|
19
|
+
It now shrinks **only when its own column cannot hold it** — a 205px track renders 22.7px on one
|
|
20
|
+
line, a 420px track still renders exactly 31px. Shrinking the token instead would have taken 31px
|
|
21
|
+
from every consumer to fix a column that was 19px short. Scoped to a stat inside `.fdy-stats`,
|
|
22
|
+
where the width comes from the grid track: `container-type:inline-size` on a standalone
|
|
23
|
+
`.fdy-stat` would stop it sizing to its own content, and the unconditional rule stays as the
|
|
24
|
+
fallback where `@container` is missing.
|
|
25
|
+
- **`.fdy-filelist` claimed space above itself and none below** (#020), so the "Add files" button
|
|
26
|
+
every upload UI puts after it sat flush against the last row — a measured **0px**. Symmetric now,
|
|
27
|
+
with `:last-child` dropping the bottom margin so nothing pays for room it does not need.
|
|
28
|
+
### Added — guards
|
|
29
|
+
- `browser/crowding.mjs` measures all three. The stat case asserts **both** directions — one line
|
|
30
|
+
when narrow, still exactly `31px` when wide — because a fix that quietly shrank every dashboard
|
|
31
|
+
would pass a "does it wrap?" test.
|
|
32
|
+
|
|
33
|
+
## [1.42.0] — 2026-08-19
|
|
34
|
+
One note from the back-office app (IDU_EMATE_APPL_WEB, #019), and a string the #009 guard could not see.
|
|
35
|
+
### Added
|
|
36
|
+
- **`multiple` on the typed `FdyCfl`** (Vue · React · Blazor). The enhancer has offered
|
|
37
|
+
`data-fdy-cfl-multiple` since it shipped and `COMPONENTS.md` documented it; all three wrappers were
|
|
38
|
+
single-valued **at the type level**, so there was nothing to widen from the outside. An app that
|
|
39
|
+
started on the enhancer and later adopted the wrapper silently lost a capability — which is the
|
|
40
|
+
actual defect, more than the missing feature. The settlement screen that reported it gathers six or
|
|
41
|
+
eight approved claims onto one document and was doing six open→search→click cycles.
|
|
42
|
+
Rows tick instead of committing, the footer counts them, and **Confirm** commits the set. Vue and
|
|
43
|
+
React widen their model to `Row[] | null`; Blazor takes `Values` / `ValuesChanged` instead, because
|
|
44
|
+
a nullable union is not a C# shape. New strings: `selectedText` (`{n} selected`), `confirmText`,
|
|
45
|
+
`hintText` — the last one making the single-mode footer overridable too, which it never was.
|
|
46
|
+
### Notes on the shape of the fix
|
|
47
|
+
- **The ticks are component state, not the model.** Closing without Confirm has to leave the caller's
|
|
48
|
+
value exactly as it was, so the dialog seeds its ticks from the bound value on every open. Binding
|
|
49
|
+
them straight to the model would make Cancel a lie.
|
|
50
|
+
- **The field states `{n} selected`.** `display()` takes ONE row: naming one of six would be wrong and
|
|
51
|
+
naming all six does not fit a 22rem control.
|
|
52
|
+
- **Blazor gained a modal footer**, which it never had — Vue and React both commit from one.
|
|
53
|
+
- The tick checkbox is `aria-hidden` and inert. `.fdy-cfl__check input` is already
|
|
54
|
+
`pointer-events:none` in the kit's CSS, so the row owns the click and a handler on the box would
|
|
55
|
+
have been unreachable code; `aria-selected` on the row is what carries the state.
|
|
56
|
+
### Fixed
|
|
57
|
+
- **`FdyCfl`'s second retry button said `Coba lagi`** (Vue · React). The error state inside the
|
|
58
|
+
results branch had a hard-coded Indonesian literal while the button two states above it correctly
|
|
59
|
+
read `retryText ?? 'Try again'` — so the component both had the prop and ignored it. #009's guard
|
|
60
|
+
missed it because **neither `coba` nor `lagi` is in its word list**, which is #015's lesson landing
|
|
61
|
+
a third time: a word list is a floor, not a ceiling. Both now read `retryText`.
|
|
62
|
+
### Added — guards
|
|
63
|
+
- **The multi flow is driven by real clicks, in both adapters** (#019). `browser/cfl-multi.mjs`
|
|
64
|
+
asserts that a click ticks rather than commits, that the dialog stays open, that `aria-selected`
|
|
65
|
+
and the footer count follow, that a second click unticks, that Confirm hands back an **array** in
|
|
66
|
+
order, and that closing without Confirm discards the ticks *and* does not remember them on re-open.
|
|
67
|
+
Vue and React run the same script, so a divergence between the two fails here instead of in an app.
|
|
68
|
+
A typecheck cannot tell you whether a mode ever renders. Mutation-tested — committing
|
|
69
|
+
`picked[0]` instead of the array fails it by name.
|
|
70
|
+
|
|
6
71
|
## [1.41.0] — 2026-08-19
|
|
7
72
|
One note from the account app (IDU_EMATE_ACCT_WEB, #018) — the first raised from that repo.
|
|
8
73
|
### Fixed
|
package/COMPONENTS.md
CHANGED
|
@@ -463,6 +463,12 @@ the field. The value is always **chosen, never typed**. Needs `freeday-cfl.js`.
|
|
|
463
463
|
- Hooks: `data-fdy-cfl-search` (the search input), `data-fdy-cfl-empty`, `data-fdy-cfl-count`,
|
|
464
464
|
`data-fdy-cfl-confirm` (multi only), `data-fdy-cfl-trigger`, `data-fdy-cfl-open`
|
|
465
465
|
- Rows are server state — in a real app drive them from a controlled fetch, not a global store.
|
|
466
|
+
- **Multi-select in the typed wrappers** (Vue · React · Blazor): `multiple` ticks rows and commits
|
|
467
|
+
them together on **Confirm** instead of committing the row that was clicked. Vue and React widen
|
|
468
|
+
their model to an array (`Row[] | null`); Blazor takes a second pair, `Values` /
|
|
469
|
+
`ValuesChanged`, because a nullable union is not a C# shape. Closing without Confirm leaves the
|
|
470
|
+
bound value untouched, and re-opening seeds the ticks from it. The field states `{n} selected`
|
|
471
|
+
rather than one row's `display()`. Strings: `selectedText`, `confirmText`, `hintText`.
|
|
466
472
|
|
|
467
473
|
```html
|
|
468
474
|
<div class="fdy-field">
|
|
@@ -63,6 +63,10 @@
|
|
|
63
63
|
<table class="fdy-table" aria-label="@Title">
|
|
64
64
|
<thead>
|
|
65
65
|
<tr>
|
|
66
|
+
@if (Multiple)
|
|
67
|
+
{
|
|
68
|
+
<th scope="col"><span class="fdy-visually-hidden">@SelectedLabel</span></th>
|
|
69
|
+
}
|
|
66
70
|
@foreach (CflColumn<TRow> col in Columns)
|
|
67
71
|
{
|
|
68
72
|
<th scope="col">@col.Header</th>
|
|
@@ -72,7 +76,15 @@
|
|
|
72
76
|
<tbody>
|
|
73
77
|
@foreach (TRow row in _rows)
|
|
74
78
|
{
|
|
75
|
-
<tr class="fdy-cfl__row" tabindex="0" @key="RowKey(row)"
|
|
79
|
+
<tr class="fdy-cfl__row" tabindex="0" @key="RowKey(row)"
|
|
80
|
+
aria-selected="@(Multiple ? IsPicked(row).ToString().ToLowerInvariant() : null)"
|
|
81
|
+
@onclick="() => RowClickAsync(row)">
|
|
82
|
+
@if (Multiple)
|
|
83
|
+
{
|
|
84
|
+
<td class="fdy-cfl__check">
|
|
85
|
+
<input class="fdy-check" type="checkbox" tabindex="-1" checked="@IsPicked(row)" aria-hidden="true" />
|
|
86
|
+
</td>
|
|
87
|
+
}
|
|
76
88
|
@foreach (CflColumn<TRow> col in Columns)
|
|
77
89
|
{
|
|
78
90
|
<td>@col.Cell(row)</td>
|
|
@@ -99,4 +111,15 @@
|
|
|
99
111
|
}
|
|
100
112
|
</div>
|
|
101
113
|
</div>
|
|
114
|
+
|
|
115
|
+
<div class="fdy-modal__footer">
|
|
116
|
+
<span class="fdy-cfl__count" aria-live="polite">@(Multiple ? SelectedLabel : HintText)</span>
|
|
117
|
+
<div class="fdy-cfl__actions">
|
|
118
|
+
<button class="fdy-btn fdy-btn--ghost" type="button" @onclick="CloseAsync">@CloseLabel</button>
|
|
119
|
+
@if (Multiple)
|
|
120
|
+
{
|
|
121
|
+
<button class="fdy-btn" type="button" @onclick="ConfirmAsync">@ConfirmText</button>
|
|
122
|
+
}
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
102
125
|
</dialog>
|
|
@@ -35,6 +35,26 @@ public partial class FdyCfl<TRow>
|
|
|
35
35
|
/// <summary>aria-label for the button that opens the picker.</summary>
|
|
36
36
|
[Parameter] public string OpenLabel { get; set; } = "Open search";
|
|
37
37
|
|
|
38
|
+
/// <summary>Tick rows and commit them together, instead of committing the row that was clicked.
|
|
39
|
+
/// The kit's own enhancer offers this (<c>data-fdy-cfl-multiple</c>); a screen that gathers six
|
|
40
|
+
/// expense claims onto one document wants one dialog, not six. Binds <see cref="Values"/>.</summary>
|
|
41
|
+
[Parameter] public bool Multiple { get; set; }
|
|
42
|
+
|
|
43
|
+
/// <summary>The committed rows when <see cref="Multiple"/> is set. Vue and React widen their
|
|
44
|
+
/// single model to an array; C# takes a second pair instead, so neither is nullable-of-union.</summary>
|
|
45
|
+
[Parameter] public IReadOnlyList<TRow>? Values { get; set; }
|
|
46
|
+
|
|
47
|
+
[Parameter] public EventCallback<IReadOnlyList<TRow>?> ValuesChanged { get; set; }
|
|
48
|
+
|
|
49
|
+
/// <summary>Footer label while picking; <c>{n}</c> is replaced by the tick count.</summary>
|
|
50
|
+
[Parameter] public string SelectedText { get; set; } = "{n} selected";
|
|
51
|
+
|
|
52
|
+
/// <summary>The multi-select commit button.</summary>
|
|
53
|
+
[Parameter] public string ConfirmText { get; set; } = "Confirm";
|
|
54
|
+
|
|
55
|
+
/// <summary>Footer hint in single mode.</summary>
|
|
56
|
+
[Parameter] public string HintText { get; set; } = "Click a row to choose it";
|
|
57
|
+
|
|
38
58
|
[Parameter] public bool Disabled { get; set; }
|
|
39
59
|
|
|
40
60
|
/// <summary>Locked/view mode: shows the value but the search dialog can't be opened.</summary>
|
|
@@ -78,6 +98,9 @@ public partial class FdyCfl<TRow>
|
|
|
78
98
|
if (Disabled || Readonly || _open) return;
|
|
79
99
|
_open = true;
|
|
80
100
|
_query = string.Empty;
|
|
101
|
+
/* Re-seeded per open, so Cancel really is a cancel: the ticks start as whatever the caller holds. */
|
|
102
|
+
_picked.Clear();
|
|
103
|
+
if (Multiple && Values is not null) _picked.AddRange(Values);
|
|
81
104
|
await JS.InvokeVoidAsync("FreedayBlazor.dialogShow", Root);
|
|
82
105
|
await LoadAsync(reset: true);
|
|
83
106
|
}
|
|
@@ -141,10 +164,52 @@ public partial class FdyCfl<TRow>
|
|
|
141
164
|
/// opens nor closes it.</summary>
|
|
142
165
|
private async Task ClearAsync()
|
|
143
166
|
{
|
|
167
|
+
_picked.Clear();
|
|
168
|
+
if (Multiple)
|
|
169
|
+
{
|
|
170
|
+
Values = null;
|
|
171
|
+
await ValuesChanged.InvokeAsync(null);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
144
174
|
Value = default;
|
|
145
175
|
await ValueChanged.InvokeAsync(default);
|
|
146
176
|
}
|
|
147
177
|
|
|
178
|
+
/* The ticks live here, not in Values, because a multi dialog is only committed at Confirm:
|
|
179
|
+
closing it must leave the caller's value exactly as it was. Seeded from Values on open. */
|
|
180
|
+
private readonly List<TRow> _picked = new();
|
|
181
|
+
|
|
182
|
+
private string SelectedLabel => SelectedText.Replace("{n}", _picked.Count.ToString());
|
|
183
|
+
|
|
184
|
+
private bool IsPicked(TRow row)
|
|
185
|
+
{
|
|
186
|
+
string key = RowKey(row);
|
|
187
|
+
return _picked.Exists(r => RowKey(r) == key);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private void TogglePick(TRow row)
|
|
191
|
+
{
|
|
192
|
+
string key = RowKey(row);
|
|
193
|
+
int at = _picked.FindIndex(r => RowKey(r) == key);
|
|
194
|
+
if (at == -1) _picked.Add(row);
|
|
195
|
+
else _picked.RemoveAt(at);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/* A click means "tick this" in multi and "this is my answer" in single — the whole difference. */
|
|
199
|
+
private async Task RowClickAsync(TRow row)
|
|
200
|
+
{
|
|
201
|
+
if (Multiple) TogglePick(row);
|
|
202
|
+
else await ChooseAsync(row);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private async Task ConfirmAsync()
|
|
206
|
+
{
|
|
207
|
+
IReadOnlyList<TRow> picks = _picked.ToArray();
|
|
208
|
+
Values = picks;
|
|
209
|
+
await ValuesChanged.InvokeAsync(picks);
|
|
210
|
+
await CloseAsync();
|
|
211
|
+
}
|
|
212
|
+
|
|
148
213
|
private async Task ChooseAsync(TRow row)
|
|
149
214
|
{
|
|
150
215
|
Value = row;
|
|
@@ -21,8 +21,11 @@ export interface CflPage<Row> {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export interface FdyCflProps<Row extends Record<string, unknown>> {
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
/** Single: `Row | null`. With `multiple`, an array — `Row[] | null`, where null and [] both mean
|
|
25
|
+
* nothing picked. The enhancer has had `data-fdy-cfl-multiple` all along; this is the typed
|
|
26
|
+
* wrappers catching up (#019). */
|
|
27
|
+
value: Row | Row[] | null;
|
|
28
|
+
onChange: (value: Row | Row[] | null) => void;
|
|
26
29
|
fetchPage: (query: string, page: number) => Promise<CflPage<Row>>;
|
|
27
30
|
columns: ReadonlyArray<CflColumn<Row>>;
|
|
28
31
|
display: (row: Row) => string;
|
|
@@ -45,6 +48,16 @@ export interface FdyCflProps<Row extends Record<string, unknown>> {
|
|
|
45
48
|
closeLabel?: string;
|
|
46
49
|
/** aria-label for the button that opens the picker. Default 'Open search'. */
|
|
47
50
|
openLabel?: string;
|
|
51
|
+
/** Tick rows and commit them together, instead of committing the row that was clicked. The kit's
|
|
52
|
+
* own enhancer offers this (`data-fdy-cfl-multiple`); a screen that gathers six expense claims
|
|
53
|
+
* onto one document wants one dialog, not six. */
|
|
54
|
+
multiple?: boolean;
|
|
55
|
+
/** Footer label while picking, `{n}` replaced by the tick count. Default '{n} selected'. */
|
|
56
|
+
selectedText?: string;
|
|
57
|
+
/** The multi-select commit button. Default 'Confirm'. */
|
|
58
|
+
confirmText?: string;
|
|
59
|
+
/** Footer hint in single mode. Default 'Click a row to choose it'. */
|
|
60
|
+
hintText?: string;
|
|
48
61
|
placeholder?: string;
|
|
49
62
|
disabled?: boolean;
|
|
50
63
|
/** 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. */
|
|
@@ -71,6 +84,7 @@ export function FdyCfl<Row extends Record<string, unknown>>(props: FdyCflProps<R
|
|
|
71
84
|
control that still exists — the trigger beside it, since this button disappears with the value. */
|
|
72
85
|
const clearLabelText: string = props.clearLabel ?? 'Clear selection';
|
|
73
86
|
const clearValue = (): void => {
|
|
87
|
+
setPicked([]);
|
|
74
88
|
props.onChange(null);
|
|
75
89
|
triggerRef.current?.focus();
|
|
76
90
|
};
|
|
@@ -86,6 +100,9 @@ export function FdyCfl<Row extends Record<string, unknown>>(props: FdyCflProps<R
|
|
|
86
100
|
const [loading, setLoading] = useState<boolean>(false);
|
|
87
101
|
const [error, setError] = useState<Error | null>(null);
|
|
88
102
|
const [activeIndex, setActiveIndex] = useState<number>(-1);
|
|
103
|
+
/* The ticks live here, not in `value`, because a multi dialog is only committed at Confirm:
|
|
104
|
+
closing it must leave the caller's value exactly as it was. Seeded from `value` on open. */
|
|
105
|
+
const [picked, setPicked] = useState<Row[]>([]);
|
|
89
106
|
|
|
90
107
|
// Out-of-order guard: every fetch takes a monotonically increasing token; a resolved or
|
|
91
108
|
// rejected page is applied only if it still owns the latest token. A newer search, a
|
|
@@ -100,10 +117,25 @@ export function FdyCfl<Row extends Record<string, unknown>>(props: FdyCflProps<R
|
|
|
100
117
|
|
|
101
118
|
const isDisabled: boolean = props.disabled === true;
|
|
102
119
|
const isReadonly: boolean = props.readonly === true;
|
|
120
|
+
/* `display()` takes one row, so in multi the field states HOW MANY — naming one of six would be a
|
|
121
|
+
lie, and naming all six does not fit a control that is 22rem wide. */
|
|
122
|
+
const currentRows: Row[] = Array.isArray(props.value)
|
|
123
|
+
? props.value
|
|
124
|
+
: props.value !== null
|
|
125
|
+
? [props.value as Row]
|
|
126
|
+
: [];
|
|
127
|
+
const selectedTextFor = (n: number): string => (props.selectedText ?? '{n} selected').replace('{n}', String(n));
|
|
128
|
+
const isMultiple: boolean = props.multiple === true;
|
|
103
129
|
const showClear: boolean =
|
|
104
|
-
props.clearable === true &&
|
|
130
|
+
props.clearable === true && currentRows.length > 0 && isDisabled === false && isReadonly === false;
|
|
105
131
|
const isInvalid: boolean = props.invalid === true;
|
|
106
|
-
const displayValue: string =
|
|
132
|
+
const displayValue: string = isMultiple
|
|
133
|
+
? currentRows.length === 0
|
|
134
|
+
? ''
|
|
135
|
+
: selectedTextFor(currentRows.length)
|
|
136
|
+
: props.value !== null
|
|
137
|
+
? props.display(props.value as Row)
|
|
138
|
+
: '';
|
|
107
139
|
// The results <table> (owner of `resultsId`) only renders in the rows branch, so gate the
|
|
108
140
|
// search input's aria refs on rows existing — otherwise they'd dangle during loading/empty/error.
|
|
109
141
|
const hasRows: boolean = rows.length > 0;
|
|
@@ -179,6 +211,31 @@ export function FdyCfl<Row extends Record<string, unknown>>(props: FdyCflProps<R
|
|
|
179
211
|
document.getElementById(rowId(activeIndex))?.scrollIntoView({ block: 'nearest' });
|
|
180
212
|
}, [activeIndex]);
|
|
181
213
|
|
|
214
|
+
function isPicked(row: Row): boolean {
|
|
215
|
+
const key: string = props.rowKey(row);
|
|
216
|
+
return picked.some((r: Row): boolean => props.rowKey(r) === key);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function togglePick(row: Row): void {
|
|
220
|
+
const key: string = props.rowKey(row);
|
|
221
|
+
setPicked((prev: Row[]): Row[] =>
|
|
222
|
+
prev.some((r: Row): boolean => props.rowKey(r) === key)
|
|
223
|
+
? prev.filter((r: Row): boolean => props.rowKey(r) !== key)
|
|
224
|
+
: [...prev, row],
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/* A click means "tick this" in multi and "this is my answer" in single — the whole difference. */
|
|
229
|
+
function onRowClick(row: Row): void {
|
|
230
|
+
if (isMultiple) togglePick(row);
|
|
231
|
+
else commit(row);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function confirmPicks(): void {
|
|
235
|
+
props.onChange(picked);
|
|
236
|
+
closeDialog();
|
|
237
|
+
}
|
|
238
|
+
|
|
182
239
|
function commit(row: Row): void {
|
|
183
240
|
props.onChange(row);
|
|
184
241
|
closeDialog();
|
|
@@ -210,7 +267,7 @@ export function FdyCfl<Row extends Record<string, unknown>>(props: FdyCflProps<R
|
|
|
210
267
|
const row: Row | undefined = activeIndex >= 0 ? rows[activeIndex] : undefined;
|
|
211
268
|
if (row !== undefined) {
|
|
212
269
|
e.preventDefault();
|
|
213
|
-
|
|
270
|
+
onRowClick(row);
|
|
214
271
|
}
|
|
215
272
|
break;
|
|
216
273
|
}
|
|
@@ -233,6 +290,8 @@ export function FdyCfl<Row extends Record<string, unknown>>(props: FdyCflProps<R
|
|
|
233
290
|
setHasMore(false);
|
|
234
291
|
setError(null);
|
|
235
292
|
setActiveIndex(-1);
|
|
293
|
+
/* Re-seeded per open, so Cancel really is a cancel: the ticks start as whatever the caller holds. */
|
|
294
|
+
setPicked(isMultiple ? [...currentRows] : []);
|
|
236
295
|
dialogRef.current?.showModal();
|
|
237
296
|
searchRef.current?.focus();
|
|
238
297
|
void load('', 0, false);
|
|
@@ -356,6 +415,9 @@ export function FdyCfl<Row extends Record<string, unknown>>(props: FdyCflProps<R
|
|
|
356
415
|
<table id={resultsId} className="fdy-table" aria-label="Search results">
|
|
357
416
|
<thead>
|
|
358
417
|
<tr>
|
|
418
|
+
{isMultiple ? (
|
|
419
|
+
<th scope="col"><span className="fdy-visually-hidden">{selectedTextFor(picked.length)}</span></th>
|
|
420
|
+
) : null}
|
|
359
421
|
{props.columns.map((col: CflColumn<Row>): JSX.Element => (
|
|
360
422
|
<th key={col.key} scope="col">{col.label}</th>
|
|
361
423
|
))}
|
|
@@ -366,11 +428,16 @@ export function FdyCfl<Row extends Record<string, unknown>>(props: FdyCflProps<R
|
|
|
366
428
|
<tr
|
|
367
429
|
id={rowId(i)}
|
|
368
430
|
key={props.rowKey(row)}
|
|
369
|
-
className=
|
|
370
|
-
aria-selected={i === activeIndex ?
|
|
371
|
-
onClick={(): void =>
|
|
431
|
+
className={i === activeIndex ? 'fdy-cfl__row is-active' : 'fdy-cfl__row'}
|
|
432
|
+
aria-selected={isMultiple ? isPicked(row) : i === activeIndex ? true : undefined}
|
|
433
|
+
onClick={(): void => onRowClick(row)}
|
|
372
434
|
onMouseMove={(): void => setActive(i)}
|
|
373
435
|
>
|
|
436
|
+
{isMultiple ? (
|
|
437
|
+
<td className="fdy-cfl__check">
|
|
438
|
+
<input className="fdy-check" type="checkbox" tabIndex={-1} checked={isPicked(row)} readOnly aria-hidden="true" />
|
|
439
|
+
</td>
|
|
440
|
+
) : null}
|
|
374
441
|
{props.columns.map((col: CflColumn<Row>): JSX.Element => (
|
|
375
442
|
<td key={col.key}>{cellText(row, col.key)}</td>
|
|
376
443
|
))}
|
|
@@ -382,7 +449,7 @@ export function FdyCfl<Row extends Record<string, unknown>>(props: FdyCflProps<R
|
|
|
382
449
|
{error !== null ? (
|
|
383
450
|
<div className="fdy-cfl__empty" role="alert" style={{ padding: 'var(--space-4) var(--space-5)' }}>
|
|
384
451
|
<p style={{ margin: '0 0 var(--space-3)' }}>{error.message}</p>
|
|
385
|
-
<button className="fdy-btn fdy-btn--sm" type="button" onClick={retry}>
|
|
452
|
+
<button className="fdy-btn fdy-btn--sm" type="button" onClick={retry}>{props.retryText ?? 'Try again'}</button>
|
|
386
453
|
</div>
|
|
387
454
|
) : hasMore ? (
|
|
388
455
|
<div style={{ padding: 'var(--space-3) var(--space-4)', textAlign: 'center' }}>
|
|
@@ -397,9 +464,14 @@ export function FdyCfl<Row extends Record<string, unknown>>(props: FdyCflProps<R
|
|
|
397
464
|
</div>
|
|
398
465
|
|
|
399
466
|
<div className="fdy-modal__footer">
|
|
400
|
-
<span className="fdy-cfl__count"
|
|
467
|
+
<span className="fdy-cfl__count" aria-live="polite">
|
|
468
|
+
{isMultiple ? selectedTextFor(picked.length) : (props.hintText ?? 'Click a row to choose it')}
|
|
469
|
+
</span>
|
|
401
470
|
<div className="fdy-cfl__actions">
|
|
402
471
|
<button className="fdy-btn fdy-btn--ghost" type="button" onClick={closeDialog}>{props.closeLabel ?? 'Close'}</button>
|
|
472
|
+
{isMultiple ? (
|
|
473
|
+
<button className="fdy-btn" type="button" onClick={confirmPicks}>{props.confirmText ?? 'Confirm'}</button>
|
|
474
|
+
) : null}
|
|
403
475
|
</div>
|
|
404
476
|
</div>
|
|
405
477
|
</dialog>
|
|
@@ -23,7 +23,10 @@ interface CflPage {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
const props = defineProps<{
|
|
26
|
-
|
|
26
|
+
/** Single: `Row | null`. With `multiple`, an array — `Row[] | null`, where null and [] both mean
|
|
27
|
+
* nothing picked. The enhancer has had `data-fdy-cfl-multiple` all along; this is the typed
|
|
28
|
+
* wrappers catching up (#019). */
|
|
29
|
+
modelValue: Row | Row[] | null;
|
|
27
30
|
fetchPage: (query: string, page: number) => Promise<CflPage>;
|
|
28
31
|
columns: ReadonlyArray<CflColumn>;
|
|
29
32
|
display: (row: Row) => string;
|
|
@@ -46,6 +49,16 @@ const props = defineProps<{
|
|
|
46
49
|
closeLabel?: string;
|
|
47
50
|
/** aria-label for the button that opens the picker. Default 'Open search'. */
|
|
48
51
|
openLabel?: string;
|
|
52
|
+
/** Tick rows and commit them together, instead of committing the row that was clicked. The kit's
|
|
53
|
+
* own enhancer offers this (`data-fdy-cfl-multiple`); a screen that gathers six expense claims
|
|
54
|
+
* onto one document wants one dialog, not six. */
|
|
55
|
+
multiple?: boolean;
|
|
56
|
+
/** Footer label while picking, `{n}` replaced by the tick count. Default '{n} selected'. */
|
|
57
|
+
selectedText?: string;
|
|
58
|
+
/** The multi-select commit button. Default 'Confirm'. */
|
|
59
|
+
confirmText?: string;
|
|
60
|
+
/** Footer hint in single mode. Default 'Click a row to choose it'. */
|
|
61
|
+
hintText?: string;
|
|
49
62
|
placeholder?: string;
|
|
50
63
|
disabled?: boolean;
|
|
51
64
|
/** 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. */
|
|
@@ -62,8 +75,8 @@ const props = defineProps<{
|
|
|
62
75
|
}>();
|
|
63
76
|
|
|
64
77
|
const emit = defineEmits<{
|
|
65
|
-
'update:modelValue': [value: Row | null];
|
|
66
|
-
change: [value: Row | null];
|
|
78
|
+
'update:modelValue': [value: Row | Row[] | null];
|
|
79
|
+
change: [value: Row | Row[] | null];
|
|
67
80
|
}>();
|
|
68
81
|
|
|
69
82
|
const baseId: string = useId();
|
|
@@ -91,13 +104,20 @@ const isReadonly: ComputedRef<boolean> = computed((): boolean => props.readonly
|
|
|
91
104
|
|
|
92
105
|
const showClear: ComputedRef<boolean> = computed(
|
|
93
106
|
(): boolean =>
|
|
94
|
-
props.clearable === true &&
|
|
107
|
+
props.clearable === true && currentRows.value.length > 0 && isDisabled.value === false && isReadonly.value === false,
|
|
95
108
|
);
|
|
96
109
|
const clearLabelText: ComputedRef<string> = computed((): string => props.clearLabel ?? 'Clear selection');
|
|
97
110
|
const isInvalid: ComputedRef<boolean> = computed((): boolean => props.invalid === true);
|
|
98
|
-
|
|
99
|
-
|
|
111
|
+
/* `display()` takes one row, so in multi the field states HOW MANY — naming one of six would be a
|
|
112
|
+
lie, and naming all six does not fit a control that is 22rem wide. */
|
|
113
|
+
const currentRows: ComputedRef<Row[]> = computed((): Row[] =>
|
|
114
|
+
Array.isArray(props.modelValue) ? props.modelValue : props.modelValue !== null ? [props.modelValue as Row] : [],
|
|
100
115
|
);
|
|
116
|
+
const selectedTextFor = (n: number): string => (props.selectedText ?? '{n} selected').replace('{n}', String(n));
|
|
117
|
+
const displayValue: ComputedRef<string> = computed((): string => {
|
|
118
|
+
if (props.multiple === true) return currentRows.value.length === 0 ? '' : selectedTextFor(currentRows.value.length);
|
|
119
|
+
return props.modelValue !== null ? props.display(props.modelValue as Row) : '';
|
|
120
|
+
});
|
|
101
121
|
// The results <table> (owner of `resultsId`) only renders in the rows branch, so gate the
|
|
102
122
|
// search input's aria refs on rows existing — otherwise they'd dangle during loading/empty/error.
|
|
103
123
|
const hasRows: ComputedRef<boolean> = computed((): boolean => rows.value.length > 0);
|
|
@@ -181,6 +201,37 @@ function setActive(index: number): void {
|
|
|
181
201
|
});
|
|
182
202
|
}
|
|
183
203
|
|
|
204
|
+
/* The ticks live here, not in `modelValue`, because a multi dialog is only committed at Confirm:
|
|
205
|
+
closing it must leave the caller's value exactly as it was. Seeded from `modelValue` on open. */
|
|
206
|
+
const picked: Ref<Row[]> = ref([]) as Ref<Row[]>;
|
|
207
|
+
|
|
208
|
+
const pickedKeys: ComputedRef<Set<string>> = computed(
|
|
209
|
+
(): Set<string> => new Set(picked.value.map((r: Row): string => props.rowKey(r)))
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
function isPicked(row: Row): boolean {
|
|
213
|
+
return pickedKeys.value.has(props.rowKey(row));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function togglePick(row: Row): void {
|
|
217
|
+
const key: string = props.rowKey(row);
|
|
218
|
+
const at: number = picked.value.findIndex((r: Row): boolean => props.rowKey(r) === key);
|
|
219
|
+
if (at === -1) picked.value = [...picked.value, row];
|
|
220
|
+
else picked.value = picked.value.filter((_: Row, i: number): boolean => i !== at);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/* A click means "tick this" in multi and "this is my answer" in single — the whole difference. */
|
|
224
|
+
function onRowClick(row: Row): void {
|
|
225
|
+
if (props.multiple === true) togglePick(row);
|
|
226
|
+
else commit(row);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function confirmPicks(): void {
|
|
230
|
+
emit('update:modelValue', picked.value);
|
|
231
|
+
emit('change', picked.value);
|
|
232
|
+
closeDialog();
|
|
233
|
+
}
|
|
234
|
+
|
|
184
235
|
function commit(row: Row | null): void {
|
|
185
236
|
emit('update:modelValue', row);
|
|
186
237
|
emit('change', row);
|
|
@@ -190,6 +241,7 @@ function commit(row: Row | null): void {
|
|
|
190
241
|
/* Unsetting is not "picking nothing" — it must not open or close the dialog, and it must leave focus
|
|
191
242
|
on a control that still exists, so focus returns to the trigger beside it. */
|
|
192
243
|
function clearValue(): void {
|
|
244
|
+
picked.value = [];
|
|
193
245
|
emit('update:modelValue', null);
|
|
194
246
|
emit('change', null);
|
|
195
247
|
triggerEl.value?.focus();
|
|
@@ -221,7 +273,7 @@ function onKeydown(e: KeyboardEvent): void {
|
|
|
221
273
|
const row: Row | undefined = activeIndex.value >= 0 ? rows.value[activeIndex.value] : undefined;
|
|
222
274
|
if (row !== undefined) {
|
|
223
275
|
e.preventDefault();
|
|
224
|
-
|
|
276
|
+
onRowClick(row);
|
|
225
277
|
}
|
|
226
278
|
break;
|
|
227
279
|
}
|
|
@@ -244,6 +296,8 @@ function openDialog(): void {
|
|
|
244
296
|
hasMore.value = false;
|
|
245
297
|
error.value = null;
|
|
246
298
|
activeIndex.value = -1;
|
|
299
|
+
/* Re-seeded per open, so Cancel really is a cancel: the ticks start as whatever the caller holds. */
|
|
300
|
+
picked.value = props.multiple === true ? [...currentRows.value] : [];
|
|
247
301
|
dialogEl.value?.showModal();
|
|
248
302
|
void nextTick((): void => searchEl.value?.focus());
|
|
249
303
|
void loadPage(0, false);
|
|
@@ -361,6 +415,7 @@ onBeforeUnmount((): void => {
|
|
|
361
415
|
<table :id="resultsId" class="fdy-table" aria-label="Search results">
|
|
362
416
|
<thead>
|
|
363
417
|
<tr>
|
|
418
|
+
<th v-if="multiple === true" scope="col"><span class="fdy-visually-hidden">{{ selectedTextFor(picked.length) }}</span></th>
|
|
364
419
|
<th v-for="col in columns" :key="col.key" scope="col">{{ col.label }}</th>
|
|
365
420
|
</tr>
|
|
366
421
|
</thead>
|
|
@@ -370,10 +425,14 @@ onBeforeUnmount((): void => {
|
|
|
370
425
|
:id="rowId(i)"
|
|
371
426
|
:key="rowKey(row)"
|
|
372
427
|
class="fdy-cfl__row"
|
|
373
|
-
:
|
|
374
|
-
|
|
428
|
+
:class="{ 'is-active': i === activeIndex }"
|
|
429
|
+
:aria-selected="multiple === true ? String(isPicked(row)) : i === activeIndex ? 'true' : undefined"
|
|
430
|
+
@click="onRowClick(row)"
|
|
375
431
|
@mousemove="setActive(i)"
|
|
376
432
|
>
|
|
433
|
+
<td v-if="multiple === true" class="fdy-cfl__check">
|
|
434
|
+
<input class="fdy-check" type="checkbox" tabindex="-1" :checked="isPicked(row)" aria-hidden="true" />
|
|
435
|
+
</td>
|
|
377
436
|
<td v-for="col in columns" :key="col.key">{{ cellText(row, col.key) }}</td>
|
|
378
437
|
</tr>
|
|
379
438
|
</tbody>
|
|
@@ -381,7 +440,7 @@ onBeforeUnmount((): void => {
|
|
|
381
440
|
|
|
382
441
|
<div v-if="error !== null" class="fdy-cfl__empty" role="alert" style="padding:var(--space-4) var(--space-5)">
|
|
383
442
|
<p style="margin:0 0 var(--space-3)">{{ error.message }}</p>
|
|
384
|
-
<button class="fdy-btn fdy-btn--sm" type="button" @click="retry">
|
|
443
|
+
<button class="fdy-btn fdy-btn--sm" type="button" @click="retry">{{ retryText ?? 'Try again' }}</button>
|
|
385
444
|
</div>
|
|
386
445
|
<div v-else-if="hasMore" style="padding:var(--space-3) var(--space-4);text-align:center">
|
|
387
446
|
<button class="fdy-btn fdy-btn--ghost fdy-btn--sm" type="button" :disabled="loading" @click="loadMore">
|
|
@@ -393,9 +452,12 @@ onBeforeUnmount((): void => {
|
|
|
393
452
|
</div>
|
|
394
453
|
|
|
395
454
|
<div class="fdy-modal__footer">
|
|
396
|
-
<span class="fdy-cfl__count"
|
|
455
|
+
<span class="fdy-cfl__count" aria-live="polite">
|
|
456
|
+
{{ multiple === true ? selectedTextFor(picked.length) : (hintText ?? 'Click a row to choose it') }}
|
|
457
|
+
</span>
|
|
397
458
|
<div class="fdy-cfl__actions">
|
|
398
459
|
<button class="fdy-btn fdy-btn--ghost" type="button" @click="closeDialog">{{ closeLabel ?? 'Close' }}</button>
|
|
460
|
+
<button v-if="multiple === true" class="fdy-btn" type="button" @click="confirmPicks">{{ confirmText ?? 'Confirm' }}</button>
|
|
399
461
|
</div>
|
|
400
462
|
</div>
|
|
401
463
|
</dialog>
|
package/dist/freeday.bundle.css
CHANGED
|
@@ -1089,6 +1089,18 @@ a { color: var(--color-primary); }
|
|
|
1089
1089
|
.fdy-stat__value{font-family:var(--font-display);font-size:var(--text-3xl);font-weight:var(--weight-bold);letter-spacing:var(--tracking-tighter);line-height:1;color:var(--color-text);font-variant-numeric:tabular-nums;}
|
|
1090
1090
|
.fdy-stat__value small{font-size:var(--text-lg);font-weight:var(--weight-semibold);color:var(--color-text-muted);}
|
|
1091
1091
|
.fdy-stat__meta{font-size:var(--text-sm);color:var(--color-text-muted);display:flex;align-items:center;gap:var(--space-2);}
|
|
1092
|
+
/* A stat value is display type sized for a COUNT — "1,284" — and a back office puts money in it.
|
|
1093
|
+
"IDR 300,000.00" needs 224px at --text-3xl and the grid's own track is 11rem, so it wrapped
|
|
1094
|
+
between the currency and the number: two lines of 31px where the design says one. Measured, not
|
|
1095
|
+
guessed (#020).
|
|
1096
|
+
The value shrinks only when its own column is too narrow to hold it, so a wide dashboard keeps
|
|
1097
|
+
exactly the type it has today. Scoped to a stat INSIDE .fdy-stats, where the width comes from the
|
|
1098
|
+
grid track — `container-type:inline-size` on a standalone .fdy-stat would stop it sizing to its
|
|
1099
|
+
own content. The unconditional rule above stays as the fallback wherever @container is missing. */
|
|
1100
|
+
@supports (container-type:inline-size){
|
|
1101
|
+
.fdy-stats>.fdy-stat{container-type:inline-size;}
|
|
1102
|
+
.fdy-stats>.fdy-stat .fdy-stat__value{font-size:clamp(var(--text-xl),11cqw,var(--text-3xl));}
|
|
1103
|
+
}
|
|
1092
1104
|
.fdy-stats--boxed{background:var(--color-surface);border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-1);gap:0;}
|
|
1093
1105
|
.fdy-stats--boxed .fdy-stat{padding:var(--space-5);border-right:var(--bw) solid var(--color-border-muted);}
|
|
1094
1106
|
.fdy-stats--boxed .fdy-stat:last-child{border-right:0;}
|
|
@@ -1256,7 +1268,12 @@ a { color: var(--color-primary); }
|
|
|
1256
1268
|
.fdy-dropzone__text{display:flex;flex-direction:column;gap:var(--space-1);}
|
|
1257
1269
|
|
|
1258
1270
|
/* File rows (list of added/uploaded/rejected files) */
|
|
1259
|
-
.
|
|
1271
|
+
/* Symmetric on purpose. The list carried margin-top and nothing below, so whatever follows it —
|
|
1272
|
+
an "Add files" button, in every upload UI there is — sat flush against the last row: measured
|
|
1273
|
+
0px. A block that claims space on one side only is not spacing, it is a lean. */
|
|
1274
|
+
.fdy-filelist{display:flex;flex-direction:column;gap:var(--space-2);margin-top:var(--space-3);margin-bottom:var(--space-3);}
|
|
1275
|
+
/* Nothing after it, nothing to separate from — so the room is not spent. */
|
|
1276
|
+
.fdy-filelist:last-child{margin-bottom:0;}
|
|
1260
1277
|
/* Side-by-side rows on wide screens (reflow to one column when narrow). */
|
|
1261
1278
|
.fdy-filelist--grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(16rem,1fr));}
|
|
1262
1279
|
.fdy-file{display:flex;align-items:center;gap:var(--space-3);padding:var(--space-3);border:var(--bw) solid var(--color-border);border-radius:var(--radius-md);background:var(--color-surface);}
|
|
@@ -1768,6 +1785,13 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
|
|
|
1768
1785
|
.fdy-table-bulkbar__spacer{flex:1;}
|
|
1769
1786
|
.fdy-table-bulkbar__actions{display:flex;gap:var(--space-2);}
|
|
1770
1787
|
|
|
1788
|
+
/* Controls inside a data row. `.fdy-input` and friends are width:100%, which in an auto-layout
|
|
1789
|
+
table means they contribute NO intrinsic width — the column shrinks to whatever the header text
|
|
1790
|
+
needs and the control collapses with it. A NOTE column ends up a box too narrow to read what you
|
|
1791
|
+
typed into it. The floor is per-control rather than on the column, because the table does not
|
|
1792
|
+
know which of its columns hold controls. */
|
|
1793
|
+
.fdy-table td>.fdy-input,.fdy-table td>.fdy-textarea,.fdy-table td>.fdy-combo,.fdy-table td>.fdy-input-group,.fdy-table td>.fdy-datepicker,.fdy-table td>.fdy-timepicker{min-width:7rem;}
|
|
1794
|
+
|
|
1771
1795
|
/* Freeday — Tabs (WAI-ARIA APG) */
|
|
1772
1796
|
/* position:relative — containing block for out-of-flow content in the tabs (see base.css). */
|
|
1773
1797
|
.fdy-tabs__list{position:relative;display:flex;gap:var(--space-1);border-bottom:var(--bw) solid var(--color-border);overflow-x:auto;}
|
package/dist/freeday.css
CHANGED
|
@@ -706,6 +706,18 @@ a { color: var(--color-primary); }
|
|
|
706
706
|
.fdy-stat__value{font-family:var(--font-display);font-size:var(--text-3xl);font-weight:var(--weight-bold);letter-spacing:var(--tracking-tighter);line-height:1;color:var(--color-text);font-variant-numeric:tabular-nums;}
|
|
707
707
|
.fdy-stat__value small{font-size:var(--text-lg);font-weight:var(--weight-semibold);color:var(--color-text-muted);}
|
|
708
708
|
.fdy-stat__meta{font-size:var(--text-sm);color:var(--color-text-muted);display:flex;align-items:center;gap:var(--space-2);}
|
|
709
|
+
/* A stat value is display type sized for a COUNT — "1,284" — and a back office puts money in it.
|
|
710
|
+
"IDR 300,000.00" needs 224px at --text-3xl and the grid's own track is 11rem, so it wrapped
|
|
711
|
+
between the currency and the number: two lines of 31px where the design says one. Measured, not
|
|
712
|
+
guessed (#020).
|
|
713
|
+
The value shrinks only when its own column is too narrow to hold it, so a wide dashboard keeps
|
|
714
|
+
exactly the type it has today. Scoped to a stat INSIDE .fdy-stats, where the width comes from the
|
|
715
|
+
grid track — `container-type:inline-size` on a standalone .fdy-stat would stop it sizing to its
|
|
716
|
+
own content. The unconditional rule above stays as the fallback wherever @container is missing. */
|
|
717
|
+
@supports (container-type:inline-size){
|
|
718
|
+
.fdy-stats>.fdy-stat{container-type:inline-size;}
|
|
719
|
+
.fdy-stats>.fdy-stat .fdy-stat__value{font-size:clamp(var(--text-xl),11cqw,var(--text-3xl));}
|
|
720
|
+
}
|
|
709
721
|
.fdy-stats--boxed{background:var(--color-surface);border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-1);gap:0;}
|
|
710
722
|
.fdy-stats--boxed .fdy-stat{padding:var(--space-5);border-right:var(--bw) solid var(--color-border-muted);}
|
|
711
723
|
.fdy-stats--boxed .fdy-stat:last-child{border-right:0;}
|
|
@@ -873,7 +885,12 @@ a { color: var(--color-primary); }
|
|
|
873
885
|
.fdy-dropzone__text{display:flex;flex-direction:column;gap:var(--space-1);}
|
|
874
886
|
|
|
875
887
|
/* File rows (list of added/uploaded/rejected files) */
|
|
876
|
-
.
|
|
888
|
+
/* Symmetric on purpose. The list carried margin-top and nothing below, so whatever follows it —
|
|
889
|
+
an "Add files" button, in every upload UI there is — sat flush against the last row: measured
|
|
890
|
+
0px. A block that claims space on one side only is not spacing, it is a lean. */
|
|
891
|
+
.fdy-filelist{display:flex;flex-direction:column;gap:var(--space-2);margin-top:var(--space-3);margin-bottom:var(--space-3);}
|
|
892
|
+
/* Nothing after it, nothing to separate from — so the room is not spent. */
|
|
893
|
+
.fdy-filelist:last-child{margin-bottom:0;}
|
|
877
894
|
/* Side-by-side rows on wide screens (reflow to one column when narrow). */
|
|
878
895
|
.fdy-filelist--grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(16rem,1fr));}
|
|
879
896
|
.fdy-file{display:flex;align-items:center;gap:var(--space-3);padding:var(--space-3);border:var(--bw) solid var(--color-border);border-radius:var(--radius-md);background:var(--color-surface);}
|
|
@@ -1385,6 +1402,13 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
|
|
|
1385
1402
|
.fdy-table-bulkbar__spacer{flex:1;}
|
|
1386
1403
|
.fdy-table-bulkbar__actions{display:flex;gap:var(--space-2);}
|
|
1387
1404
|
|
|
1405
|
+
/* Controls inside a data row. `.fdy-input` and friends are width:100%, which in an auto-layout
|
|
1406
|
+
table means they contribute NO intrinsic width — the column shrinks to whatever the header text
|
|
1407
|
+
needs and the control collapses with it. A NOTE column ends up a box too narrow to read what you
|
|
1408
|
+
typed into it. The floor is per-control rather than on the column, because the table does not
|
|
1409
|
+
know which of its columns hold controls. */
|
|
1410
|
+
.fdy-table td>.fdy-input,.fdy-table td>.fdy-textarea,.fdy-table td>.fdy-combo,.fdy-table td>.fdy-input-group,.fdy-table td>.fdy-datepicker,.fdy-table td>.fdy-timepicker{min-width:7rem;}
|
|
1411
|
+
|
|
1388
1412
|
/* Freeday — Tabs (WAI-ARIA APG) */
|
|
1389
1413
|
/* position:relative — containing block for out-of-flow content in the tabs (see base.css). */
|
|
1390
1414
|
.fdy-tabs__list{position:relative;display:flex;gap:var(--space-1);border-bottom:var(--bw) solid var(--color-border);overflow-x:auto;}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cahyo-dimas/freeday",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.43.0",
|
|
4
4
|
"description": "Freeday — token-driven, framework-agnostic UI KIT (design source-of-truth).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
"scripts": {
|
|
93
93
|
"build": "node tokens/build.mjs",
|
|
94
94
|
"test": "node --test",
|
|
95
|
-
"test:browser": "node --test browser/vanilla.mjs browser/adapter.mjs browser/layout.mjs browser/theme.mjs browser/state.mjs browser/root-init.mjs browser/upload-states.mjs browser/number.mjs browser/card-stretch.mjs browser/text-override.mjs browser/control-heights.mjs",
|
|
95
|
+
"test:browser": "node --test browser/vanilla.mjs browser/adapter.mjs browser/layout.mjs browser/theme.mjs browser/state.mjs browser/root-init.mjs browser/upload-states.mjs browser/number.mjs browser/card-stretch.mjs browser/text-override.mjs browser/control-heights.mjs browser/cfl-multi.mjs browser/crowding.mjs",
|
|
96
96
|
"prepack": "node tokens/build.mjs",
|
|
97
97
|
"version": "node tokens/build.mjs && git add dist",
|
|
98
98
|
"typecheck:react": "tsc -p adapters/react/tsconfig.json --noEmit"
|
|
@@ -51,6 +51,18 @@
|
|
|
51
51
|
.fdy-stat__value{font-family:var(--font-display);font-size:var(--text-3xl);font-weight:var(--weight-bold);letter-spacing:var(--tracking-tighter);line-height:1;color:var(--color-text);font-variant-numeric:tabular-nums;}
|
|
52
52
|
.fdy-stat__value small{font-size:var(--text-lg);font-weight:var(--weight-semibold);color:var(--color-text-muted);}
|
|
53
53
|
.fdy-stat__meta{font-size:var(--text-sm);color:var(--color-text-muted);display:flex;align-items:center;gap:var(--space-2);}
|
|
54
|
+
/* A stat value is display type sized for a COUNT — "1,284" — and a back office puts money in it.
|
|
55
|
+
"IDR 300,000.00" needs 224px at --text-3xl and the grid's own track is 11rem, so it wrapped
|
|
56
|
+
between the currency and the number: two lines of 31px where the design says one. Measured, not
|
|
57
|
+
guessed (#020).
|
|
58
|
+
The value shrinks only when its own column is too narrow to hold it, so a wide dashboard keeps
|
|
59
|
+
exactly the type it has today. Scoped to a stat INSIDE .fdy-stats, where the width comes from the
|
|
60
|
+
grid track — `container-type:inline-size` on a standalone .fdy-stat would stop it sizing to its
|
|
61
|
+
own content. The unconditional rule above stays as the fallback wherever @container is missing. */
|
|
62
|
+
@supports (container-type:inline-size){
|
|
63
|
+
.fdy-stats>.fdy-stat{container-type:inline-size;}
|
|
64
|
+
.fdy-stats>.fdy-stat .fdy-stat__value{font-size:clamp(var(--text-xl),11cqw,var(--text-3xl));}
|
|
65
|
+
}
|
|
54
66
|
.fdy-stats--boxed{background:var(--color-surface);border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-1);gap:0;}
|
|
55
67
|
.fdy-stats--boxed .fdy-stat{padding:var(--space-5);border-right:var(--bw) solid var(--color-border-muted);}
|
|
56
68
|
.fdy-stats--boxed .fdy-stat:last-child{border-right:0;}
|
|
@@ -16,7 +16,12 @@
|
|
|
16
16
|
.fdy-dropzone__text{display:flex;flex-direction:column;gap:var(--space-1);}
|
|
17
17
|
|
|
18
18
|
/* File rows (list of added/uploaded/rejected files) */
|
|
19
|
-
.
|
|
19
|
+
/* Symmetric on purpose. The list carried margin-top and nothing below, so whatever follows it —
|
|
20
|
+
an "Add files" button, in every upload UI there is — sat flush against the last row: measured
|
|
21
|
+
0px. A block that claims space on one side only is not spacing, it is a lean. */
|
|
22
|
+
.fdy-filelist{display:flex;flex-direction:column;gap:var(--space-2);margin-top:var(--space-3);margin-bottom:var(--space-3);}
|
|
23
|
+
/* Nothing after it, nothing to separate from — so the room is not spent. */
|
|
24
|
+
.fdy-filelist:last-child{margin-bottom:0;}
|
|
20
25
|
/* Side-by-side rows on wide screens (reflow to one column when narrow). */
|
|
21
26
|
.fdy-filelist--grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(16rem,1fr));}
|
|
22
27
|
.fdy-file{display:flex;align-items:center;gap:var(--space-3);padding:var(--space-3);border:var(--bw) solid var(--color-border);border-radius:var(--radius-md);background:var(--color-surface);}
|
package/src/components/table.css
CHANGED
|
@@ -128,3 +128,10 @@
|
|
|
128
128
|
.fdy-table-bulkbar__count{font-weight:var(--weight-semibold);white-space:nowrap;}
|
|
129
129
|
.fdy-table-bulkbar__spacer{flex:1;}
|
|
130
130
|
.fdy-table-bulkbar__actions{display:flex;gap:var(--space-2);}
|
|
131
|
+
|
|
132
|
+
/* Controls inside a data row. `.fdy-input` and friends are width:100%, which in an auto-layout
|
|
133
|
+
table means they contribute NO intrinsic width — the column shrinks to whatever the header text
|
|
134
|
+
needs and the control collapses with it. A NOTE column ends up a box too narrow to read what you
|
|
135
|
+
typed into it. The floor is per-control rather than on the column, because the table does not
|
|
136
|
+
know which of its columns hold controls. */
|
|
137
|
+
.fdy-table td>.fdy-input,.fdy-table td>.fdy-textarea,.fdy-table td>.fdy-combo,.fdy-table td>.fdy-input-group,.fdy-table td>.fdy-datepicker,.fdy-table td>.fdy-timepicker{min-width:7rem;}
|