@eifi1/ui-kit 0.6.1 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -33
- package/dist/chart.d.ts +1 -0
- package/dist/components/account-settings.js +3 -2
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/amount-input.d.ts +5 -0
- package/dist/components/amount-input.js +6 -4
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.d.ts +33 -6
- package/dist/components/autocomplete.js +62 -16
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/calculator.d.ts +5 -0
- package/dist/components/calculator.js +2 -1
- package/dist/components/calculator.js.map +1 -1
- package/dist/components/chart-zoom.js +2 -1
- package/dist/components/chart-zoom.js.map +1 -1
- package/dist/components/chart.d.ts +33 -2
- package/dist/components/chart.js +20 -8
- package/dist/components/chart.js.map +1 -1
- package/dist/components/chip.d.ts +34 -14
- package/dist/components/chip.js +63 -28
- package/dist/components/chip.js.map +1 -1
- package/dist/components/choice-card.d.ts +18 -1
- package/dist/components/choice-card.js +21 -8
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/combobox-core.d.ts +61 -2
- package/dist/components/combobox-core.js +68 -17
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +11 -7
- package/dist/components/combobox.js +51 -28
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/currency-select.js +3 -3
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/danger-confirm.d.ts +30 -6
- package/dist/components/danger-confirm.js +51 -32
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/data-table-filter-popover.d.ts +1 -1
- package/dist/components/data-table-filters.d.ts +1 -1
- package/dist/components/data-table-pagination.d.ts +6 -3
- package/dist/components/data-table-pagination.js +6 -5
- package/dist/components/data-table-pagination.js.map +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +332 -248
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.js +17 -7
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/dialog-frame.d.ts +28 -3
- package/dist/components/dialog-frame.js +47 -11
- package/dist/components/dialog-frame.js.map +1 -1
- package/dist/components/disclosure.d.ts +22 -2
- package/dist/components/disclosure.js +41 -8
- package/dist/components/disclosure.js.map +1 -1
- package/dist/components/dropdown.d.ts +7 -3
- package/dist/components/dropdown.js +6 -2
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/entity-combobox.d.ts +17 -8
- package/dist/components/entity-combobox.js +7 -5
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/file-button.d.ts +38 -5
- package/dist/components/file-button.js +23 -2
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.d.ts +11 -5
- package/dist/components/file-dropzone.js +14 -19
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/grouped-picker.js +2 -2
- package/dist/components/grouped-picker.js.map +1 -1
- package/dist/components/hover-menu.d.ts +8 -3
- package/dist/components/hover-menu.js +88 -4
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/icon-picker.d.ts +10 -1
- package/dist/components/icon-picker.js +5 -1
- package/dist/components/icon-picker.js.map +1 -1
- package/dist/components/measured-grid.d.ts +165 -0
- package/dist/components/measured-grid.js +559 -0
- package/dist/components/measured-grid.js.map +1 -0
- package/dist/components/mini-calendar.js +7 -8
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/month-picker.js +13 -9
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.js +2 -2
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/multi-select.d.ts +9 -1
- package/dist/components/multi-select.js +22 -8
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/number-field.d.ts +5 -0
- package/dist/components/number-input.d.ts +5 -0
- package/dist/components/number-input.js +3 -3
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +5 -0
- package/dist/components/numpad-sheet.js +3 -3
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/picker-sheet.js +1 -1
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/popover.js +11 -2
- package/dist/components/popover.js.map +1 -1
- package/dist/components/series-chart-ticks.d.ts +13 -0
- package/dist/components/series-chart-ticks.js +29 -0
- package/dist/components/series-chart-ticks.js.map +1 -0
- package/dist/components/series-chart.js +33 -27
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/settings-fields.d.ts +2 -0
- package/dist/components/swatch-picker.d.ts +10 -1
- package/dist/components/swatch-picker.js +2 -0
- package/dist/components/swatch-picker.js.map +1 -1
- package/dist/components/swipeable-row.d.ts +11 -2
- package/dist/components/swipeable-row.js +5 -3
- package/dist/components/swipeable-row.js.map +1 -1
- package/dist/components/tile-radio.js +5 -7
- package/dist/components/tile-radio.js.map +1 -1
- package/dist/components/time-input.d.ts +2 -0
- package/dist/components/toggle-group.d.ts +13 -3
- package/dist/components/toggle-group.js +21 -1
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.js +1 -2
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/tooltip.d.ts +10 -4
- package/dist/components/tooltip.js +23 -6
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/treemap.d.ts +6 -1
- package/dist/components/treemap.js +16 -6
- package/dist/components/treemap.js.map +1 -1
- package/dist/components/ui.d.ts +36 -8
- package/dist/components/ui.js +27 -19
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-anchor-dir.d.ts +15 -0
- package/dist/components/use-anchor-dir.js +14 -0
- package/dist/components/use-anchor-dir.js.map +1 -0
- package/dist/components/use-mobile-reveal.d.ts +8 -1
- package/dist/components/use-mobile-reveal.js +3 -2
- package/dist/components/use-mobile-reveal.js.map +1 -1
- package/dist/components/use-table-state.d.ts +4 -4
- package/dist/components/use-table-state.js +12 -5
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/{data-table-filters-DM1auUfH.d.ts → data-table-filters-noy0Abvi.d.ts} +40 -7
- package/dist/data-table.d.ts +1 -1
- package/dist/data-table.js.map +1 -1
- package/dist/feedback/feedback-attachment.d.ts +22 -3
- package/dist/feedback/feedback-attachment.js +15 -7
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +3 -2
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback.d.ts +1 -1
- package/dist/hooks/use-windowed-rows.d.ts +29 -0
- package/dist/hooks/use-windowed-rows.js +31 -0
- package/dist/hooks/use-windowed-rows.js.map +1 -0
- package/dist/i18n/defaults.d.ts +5 -0
- package/dist/i18n/defaults.js +5 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +7 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH.d.ts +45 -0
- package/dist/i18n/locales/de-CH.js +21 -0
- package/dist/i18n/locales/de-CH.js.map +1 -0
- package/dist/i18n/locales/de.d.ts +45 -0
- package/dist/i18n/locales/de.js +344 -0
- package/dist/i18n/locales/de.js.map +1 -0
- package/dist/i18n/locales/es.d.ts +45 -0
- package/dist/i18n/locales/es.js +339 -0
- package/dist/i18n/locales/es.js.map +1 -0
- package/dist/i18n/locales/fr.d.ts +45 -0
- package/dist/i18n/locales/fr.js +344 -0
- package/dist/i18n/locales/fr.js.map +1 -0
- package/dist/i18n/locales/hu.d.ts +45 -0
- package/dist/i18n/locales/hu.js +343 -0
- package/dist/i18n/locales/hu.js.map +1 -0
- package/dist/i18n/locales/it.d.ts +45 -0
- package/dist/i18n/locales/it.js +342 -0
- package/dist/i18n/locales/it.js.map +1 -0
- package/dist/i18n/locales/zh.d.ts +45 -0
- package/dist/i18n/locales/zh.js +338 -0
- package/dist/i18n/locales/zh.js.map +1 -0
- package/dist/index.d.ts +7 -4
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/direction.d.ts +24 -0
- package/dist/lib/direction.js +19 -0
- package/dist/lib/direction.js.map +1 -0
- package/dist/rhf/form.d.ts +2 -0
- package/dist/rhf/form.js.map +1 -1
- package/dist/rhf.d.ts +2 -0
- package/dist/search/command-palette.d.ts +5 -0
- package/dist/search/command-palette.js +80 -50
- package/dist/search/command-palette.js.map +1 -1
- package/dist/shell/app-shell.d.ts +13 -1
- package/dist/shell/app-shell.js +134 -85
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/topbar-action-menu.js +1 -1
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell/topbar-controls.d.ts +7 -3
- package/dist/shell/topbar-controls.js +38 -31
- package/dist/shell/topbar-controls.js.map +1 -1
- package/dist/theme/chart-palette.d.ts +7 -1
- package/dist/theme/chart-palette.js +2 -1
- package/dist/theme/chart-palette.js.map +1 -1
- package/dist/tour/tour.d.ts +11 -5
- package/dist/tour/tour.js +124 -58
- package/dist/tour/tour.js.map +1 -1
- package/dist/wizard/use-wizard.js +30 -13
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/wizard-summary.js +1 -1
- package/dist/wizard/wizard-summary.js.map +1 -1
- package/package.json +12 -2
- package/src/components/account-settings.tsx +6 -2
- package/src/components/amount-input.tsx +10 -8
- package/src/components/autocomplete.tsx +110 -24
- package/src/components/calculator.tsx +5 -1
- package/src/components/chart-zoom.tsx +8 -1
- package/src/components/chart.tsx +68 -7
- package/src/components/chip.tsx +131 -49
- package/src/components/choice-card.tsx +77 -36
- package/src/components/combobox-core.tsx +147 -20
- package/src/components/combobox.tsx +100 -44
- package/src/components/currency-select.tsx +6 -6
- package/src/components/danger-confirm.tsx +90 -36
- package/src/components/data-table-pagination.tsx +32 -19
- package/src/components/data-table.tsx +198 -36
- package/src/components/date-picker.tsx +24 -7
- package/src/components/dialog-frame.tsx +97 -22
- package/src/components/disclosure.tsx +75 -13
- package/src/components/dropdown.tsx +17 -5
- package/src/components/entity-combobox.tsx +26 -13
- package/src/components/file-button.tsx +74 -5
- package/src/components/file-dropzone.tsx +44 -35
- package/src/components/grouped-picker.tsx +2 -2
- package/src/components/hover-menu.tsx +139 -10
- package/src/components/icon-picker.tsx +14 -1
- package/src/components/measured-grid.tsx +852 -0
- package/src/components/mini-calendar.tsx +11 -8
- package/src/components/month-picker.tsx +18 -11
- package/src/components/multi-entity-combobox.tsx +2 -2
- package/src/components/multi-select.tsx +34 -7
- package/src/components/number-input.tsx +4 -4
- package/src/components/numpad-sheet.tsx +5 -3
- package/src/components/picker-sheet.tsx +1 -1
- package/src/components/popover.tsx +18 -3
- package/src/components/series-chart-ticks.ts +52 -0
- package/src/components/series-chart.tsx +39 -31
- package/src/components/swatch-picker.tsx +11 -0
- package/src/components/swipeable-row.tsx +21 -6
- package/src/components/tile-radio.tsx +5 -7
- package/src/components/toggle-group.tsx +39 -3
- package/src/components/toggle-legend.tsx +5 -2
- package/src/components/tooltip.tsx +40 -12
- package/src/components/treemap.tsx +28 -5
- package/src/components/ui.tsx +79 -33
- package/src/components/use-anchor-dir.ts +20 -0
- package/src/components/use-mobile-reveal.ts +10 -2
- package/src/components/use-table-state.ts +44 -7
- package/src/data-table.ts +1 -0
- package/src/feedback/feedback-attachment.tsx +36 -9
- package/src/feedback/feedback-dialog.tsx +3 -2
- package/src/hooks/use-windowed-rows.ts +69 -0
- package/src/i18n/defaults.ts +4 -0
- package/src/i18n/kit-labels.tsx +4 -0
- package/src/i18n/locales/de-CH.ts +30 -0
- package/src/i18n/locales/de.ts +365 -0
- package/src/i18n/locales/es.ts +358 -0
- package/src/i18n/locales/fr.ts +371 -0
- package/src/i18n/locales/hu.ts +359 -0
- package/src/i18n/locales/it.ts +360 -0
- package/src/i18n/locales/zh.ts +348 -0
- package/src/index.ts +10 -1
- package/src/lib/direction.ts +37 -0
- package/src/rhf/form.tsx +21 -0
- package/src/search/command-palette.tsx +77 -19
- package/src/shell/app-shell.tsx +201 -130
- package/src/shell/topbar-action-menu.tsx +1 -1
- package/src/shell/topbar-controls.tsx +19 -8
- package/src/theme/chart-palette.ts +9 -2
- package/src/tour/tour.tsx +119 -18
- package/src/wizard/use-wizard.ts +55 -17
- package/src/wizard/wizard-summary.tsx +1 -1
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useLayoutEffect, useState } from "react";
|
|
2
|
+
import type { RefObject } from "react";
|
|
3
|
+
import { dirOf, type Direction } from "../lib/direction";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The reading direction at a portalled panel's anchor — internal, not exported from
|
|
7
|
+
* the barrel. The field pickers' panels are portalled to `<body>`, out of the subtree
|
|
8
|
+
* their `dir` came from, so they carry it across on their own root.
|
|
9
|
+
*
|
|
10
|
+
* Read in a layout effect while `open`, rather than off the ref during render: the
|
|
11
|
+
* anchored rect these panels also wait for is measured the same way, so the panel's
|
|
12
|
+
* first painted frame already has both, and a render never reads a ref.
|
|
13
|
+
*/
|
|
14
|
+
export function useAnchorDir(ref: RefObject<Element | null>, open: boolean): Direction {
|
|
15
|
+
const [dir, setDir] = useState<Direction>("ltr");
|
|
16
|
+
useLayoutEffect(() => {
|
|
17
|
+
if (open) setDir(dirOf(ref.current));
|
|
18
|
+
}, [open, ref]);
|
|
19
|
+
return dir;
|
|
20
|
+
}
|
|
@@ -11,6 +11,11 @@ import { useEffect, useRef, useState } from "react";
|
|
|
11
11
|
* is why nothing resets it: narrowing a filter cannot leave the limit pointing past the
|
|
12
12
|
* end, and widening one does not have to re-reveal what the user already scrolled past.
|
|
13
13
|
*
|
|
14
|
+
* `paginated={false}` opts out entirely: a table that is short by construction (an
|
|
15
|
+
* invoice's line items) renders every row on a phone too. It used to be chunked here
|
|
16
|
+
* regardless, so a twelve-line invoice showed its first ten lines and a "Loading…"
|
|
17
|
+
* sentinel for rows that were already in memory.
|
|
18
|
+
*
|
|
14
19
|
* Lifted out of `DataTable` with the rest of its state; the sentinel ref belongs to the
|
|
15
20
|
* caller's `<li>`.
|
|
16
21
|
*/
|
|
@@ -19,6 +24,7 @@ export function useMobileReveal<T>({
|
|
|
19
24
|
slice,
|
|
20
25
|
isServer,
|
|
21
26
|
isMdUp,
|
|
27
|
+
unpaged = false,
|
|
22
28
|
defaultPageSize,
|
|
23
29
|
}: {
|
|
24
30
|
/** The whole sorted list — what there is to reveal. */
|
|
@@ -27,11 +33,13 @@ export function useMobileReveal<T>({
|
|
|
27
33
|
slice: T[];
|
|
28
34
|
isServer: boolean;
|
|
29
35
|
isMdUp: boolean;
|
|
36
|
+
/** `paginated={false}` on a client-side table: show every row, reveal nothing. */
|
|
37
|
+
unpaged?: boolean;
|
|
30
38
|
defaultPageSize: number;
|
|
31
39
|
}) {
|
|
32
40
|
const [mobileLimit, setMobileLimit] = useState(defaultPageSize);
|
|
33
|
-
const mobileSlice = isServer ? slice : rows.slice(0, mobileLimit);
|
|
34
|
-
const canRevealMoreMobile = !isServer && !isMdUp && mobileLimit < rows.length;
|
|
41
|
+
const mobileSlice = isServer ? slice : unpaged ? rows : rows.slice(0, mobileLimit);
|
|
42
|
+
const canRevealMoreMobile = !isServer && !unpaged && !isMdUp && mobileLimit < rows.length;
|
|
35
43
|
const loadMoreRef = useRef<HTMLLIElement | null>(null);
|
|
36
44
|
|
|
37
45
|
useEffect(() => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
-
import
|
|
2
|
+
import { useSearchParams } from "react-router";
|
|
3
3
|
import { readStored, writeStored } from "../lib/safe-storage";
|
|
4
4
|
import {
|
|
5
5
|
decodeFilterValue,
|
|
@@ -59,9 +59,17 @@ interface UrlState {
|
|
|
59
59
|
page?: number; // 0-based
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
/**
|
|
63
|
+
* The managed keys out of the ROUTER's query string — the one `setSearchParams` writes.
|
|
64
|
+
*
|
|
65
|
+
* This read `window.location.search` until 0.7.0, which is the same string only under a
|
|
66
|
+
* BrowserRouter. Under a HashRouter the router's query lives after the `#`
|
|
67
|
+
* (`/#/tx?f.payee=Rewe`) and `window.location.search` is empty: a shared link opened
|
|
68
|
+
* unfiltered, and the table's first write then replaced the link's filter with its own
|
|
69
|
+
* empty state. Reading and writing through `useSearchParams` is one source for both
|
|
70
|
+
* directions under every router, MemoryRouter included.
|
|
71
|
+
*/
|
|
72
|
+
function readUrlState<T>(sp: URLSearchParams, columns: DataTableColumn<T>[]): UrlState {
|
|
65
73
|
const out: UrlState = {};
|
|
66
74
|
|
|
67
75
|
const filters: FilterState = {};
|
|
@@ -179,13 +187,14 @@ function writeUrlState<T>(
|
|
|
179
187
|
* * **Persistence writes on every change, the URL only when asked.** `storageKey` is the
|
|
180
188
|
* per-table identity; `urlSync` is opt-in because only some tables want their view in
|
|
181
189
|
* a shareable address.
|
|
190
|
+
* * **A controlled filter or sort that changes sends the table back to page 1.** See
|
|
191
|
+
* the note at the reset below.
|
|
182
192
|
*/
|
|
183
193
|
export function useTableState<T>({
|
|
184
194
|
columns,
|
|
185
195
|
storageKey,
|
|
186
196
|
storageKeyPrefix,
|
|
187
197
|
urlSync,
|
|
188
|
-
setSearchParams,
|
|
189
198
|
defaultPageSize,
|
|
190
199
|
sortsProp,
|
|
191
200
|
filtersProp,
|
|
@@ -194,14 +203,18 @@ export function useTableState<T>({
|
|
|
194
203
|
storageKey?: string;
|
|
195
204
|
storageKeyPrefix: string;
|
|
196
205
|
urlSync?: boolean;
|
|
197
|
-
setSearchParams: ReturnType<typeof useSearchParams>[1];
|
|
198
206
|
defaultPageSize: number;
|
|
199
207
|
sortsProp?: SortState[];
|
|
200
208
|
filtersProp?: FilterState;
|
|
201
209
|
}) {
|
|
210
|
+
// Called unconditionally (a Router is required either way — see src/data-table.ts),
|
|
211
|
+
// and the SAME pair is used to read and to write; see `readUrlState`.
|
|
212
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
202
213
|
// Read URL once on mount so URL-encoded views (e.g. shared links, deep links from
|
|
203
214
|
// other pages) populate initial state. After mount, state is local.
|
|
204
|
-
const [urlInitial] = useState<UrlState>(() =>
|
|
215
|
+
const [urlInitial] = useState<UrlState>(() =>
|
|
216
|
+
urlSync ? readUrlState(searchParams, columns) : {},
|
|
217
|
+
);
|
|
205
218
|
const initial = useMemo(
|
|
206
219
|
() => loadPersisted(storageKey, storageKeyPrefix),
|
|
207
220
|
[storageKey, storageKeyPrefix],
|
|
@@ -217,6 +230,30 @@ export function useTableState<T>({
|
|
|
217
230
|
const sorts = sortsProp ?? internalSorts;
|
|
218
231
|
const filters = filtersProp ?? internalFilters;
|
|
219
232
|
const [page, setPage] = useState(() => urlInitial.page ?? 0);
|
|
233
|
+
|
|
234
|
+
// ---- Controlled filters/sorts reset the page ----
|
|
235
|
+
//
|
|
236
|
+
// A self-owned table resets to page 1 in the filter handlers. A CONTROLLED one could
|
|
237
|
+
// not: the handlers hand the change to the owner and return, and `page` lives here,
|
|
238
|
+
// where the owner has no way to reach it — so narrowing a filter from outside (a
|
|
239
|
+
// saved view, a chip dismissed above the table) left a client-side table on page 4
|
|
240
|
+
// of what was now two pages, clamped to the last one instead of the first.
|
|
241
|
+
//
|
|
242
|
+
// So the table does it: whenever the controlled VALUE changes, back to the first
|
|
243
|
+
// page. Compared by value, because a controlled prop is a fresh object on every
|
|
244
|
+
// parent render. Adjusted during render rather than in an effect, so the stale page
|
|
245
|
+
// is never painted and never written to the URL. Not on mount: a deep link's `p=3`
|
|
246
|
+
// must survive the owner's first render. (In `serverPagination` mode this state is
|
|
247
|
+
// unused — the owner's `page` is the page, and resetting it stays the owner's job.)
|
|
248
|
+
const controlledSig =
|
|
249
|
+
sortsProp !== undefined || filtersProp !== undefined
|
|
250
|
+
? JSON.stringify([sortsProp ?? null, filtersProp ?? null])
|
|
251
|
+
: null;
|
|
252
|
+
const [seenControlledSig, setSeenControlledSig] = useState(controlledSig);
|
|
253
|
+
if (seenControlledSig !== controlledSig) {
|
|
254
|
+
setSeenControlledSig(controlledSig);
|
|
255
|
+
setPage(0);
|
|
256
|
+
}
|
|
220
257
|
const [pageSize, setPageSize] = useState<number>(() => {
|
|
221
258
|
if (urlInitial.pageSize !== undefined) return urlInitial.pageSize;
|
|
222
259
|
if (initial.pageSize === "all") return Infinity;
|
package/src/data-table.ts
CHANGED
|
@@ -2,12 +2,36 @@ import { cn } from "../lib/cn";
|
|
|
2
2
|
import { useEffect, useRef, useState, type RefObject } from "react";
|
|
3
3
|
import { Camera, FileText, Paperclip, X } from "lucide-react";
|
|
4
4
|
import { Button } from "../components/ui";
|
|
5
|
-
import { useKitFileLabels } from "../i18n/kit-labels";
|
|
5
|
+
import { useKitFileLabels, useKitLabels } from "../i18n/kit-labels";
|
|
6
6
|
import type { FeedbackAttachmentLabels } from "./feedback-dialog";
|
|
7
7
|
|
|
8
8
|
export const DEFAULT_ATTACHMENT_ACCEPT = ["image/png", "image/jpeg", "image/webp", "image/gif"];
|
|
9
9
|
export const DEFAULT_MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* The field's own strings — the `feedbackAttachment` namespace of `<UiKitProvider
|
|
13
|
+
* labels>`. The keys are {@link FeedbackAttachmentLabels}' own, so the `labels` prop
|
|
14
|
+
* (and a whole `FeedbackDialogLabels` handed down by the dialog) merges over them
|
|
15
|
+
* one to one. The optional heading, `attachment`, stays a prop: a note editor puts
|
|
16
|
+
* the buttons straight under its textarea and wants none.
|
|
17
|
+
*
|
|
18
|
+
* Before 0.7.0 the two optional keys fell back to hard-coded English, so a German app
|
|
19
|
+
* that relied on its provider got "Capture screenshot" under a German form.
|
|
20
|
+
*/
|
|
21
|
+
export interface FeedbackAttachmentFieldLabels {
|
|
22
|
+
attachmentAdd: string;
|
|
23
|
+
attachmentCapture: string;
|
|
24
|
+
attachmentPaste: string;
|
|
25
|
+
attachmentRemove: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const DEFAULT_FEEDBACK_ATTACHMENT_LABELS: FeedbackAttachmentFieldLabels = {
|
|
29
|
+
attachmentAdd: "Attach image",
|
|
30
|
+
attachmentCapture: "Capture screenshot",
|
|
31
|
+
attachmentPaste: "…or paste a screenshot from the clipboard.",
|
|
32
|
+
attachmentRemove: "Remove attachment",
|
|
33
|
+
};
|
|
34
|
+
|
|
11
35
|
/**
|
|
12
36
|
* Picking one picture: the file dialog, a capture of the app view, or a paste.
|
|
13
37
|
*
|
|
@@ -40,7 +64,7 @@ export const DEFAULT_MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;
|
|
|
40
64
|
export function FeedbackAttachmentField({
|
|
41
65
|
value,
|
|
42
66
|
onChange,
|
|
43
|
-
labels,
|
|
67
|
+
labels: labelsProp,
|
|
44
68
|
accept = DEFAULT_ATTACHMENT_ACCEPT,
|
|
45
69
|
maxBytes = DEFAULT_MAX_ATTACHMENT_BYTES,
|
|
46
70
|
onError,
|
|
@@ -51,7 +75,9 @@ export function FeedbackAttachmentField({
|
|
|
51
75
|
}: {
|
|
52
76
|
value: File | null;
|
|
53
77
|
onChange: (file: File | null) => void;
|
|
54
|
-
labels
|
|
78
|
+
/** Prop > `<UiKitProvider labels={{ feedbackAttachment }}>` > English. Optional
|
|
79
|
+
* since 0.7.0; `attachment` (the heading) is only ever read from here. */
|
|
80
|
+
labels?: Partial<FeedbackAttachmentLabels>;
|
|
55
81
|
accept?: string[];
|
|
56
82
|
maxBytes?: number;
|
|
57
83
|
onError?: (kind: "type" | "size") => void;
|
|
@@ -73,6 +99,7 @@ export function FeedbackAttachmentField({
|
|
|
73
99
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
74
100
|
// `file.size` from `<UiKitProvider labels>`, formatted in its locale — see FileDropzone.
|
|
75
101
|
const fileText = useKitFileLabels();
|
|
102
|
+
const text = useKitLabels("feedbackAttachment", DEFAULT_FEEDBACK_ATTACHMENT_LABELS, labelsProp);
|
|
76
103
|
|
|
77
104
|
// Object-URL preview lifecycle (create on change, revoke on cleanup).
|
|
78
105
|
useEffect(() => {
|
|
@@ -151,9 +178,9 @@ export function FeedbackAttachmentField({
|
|
|
151
178
|
: (event) => takeImage(event.clipboardData, () => event.preventDefault())
|
|
152
179
|
}
|
|
153
180
|
>
|
|
154
|
-
{
|
|
181
|
+
{labelsProp?.attachment && (
|
|
155
182
|
<div className="mb-1 text-xs font-medium uppercase tracking-wide text-[var(--text-muted)]">
|
|
156
|
-
{
|
|
183
|
+
{labelsProp.attachment}
|
|
157
184
|
</div>
|
|
158
185
|
)}
|
|
159
186
|
{value ? (
|
|
@@ -180,7 +207,7 @@ export function FeedbackAttachmentField({
|
|
|
180
207
|
<button
|
|
181
208
|
type="button"
|
|
182
209
|
onClick={() => onChange(null)}
|
|
183
|
-
aria-label={
|
|
210
|
+
aria-label={text.attachmentRemove}
|
|
184
211
|
className="rounded p-1.5 text-[var(--text-placeholder)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-secondary)]"
|
|
185
212
|
>
|
|
186
213
|
<X className="size-4" />
|
|
@@ -190,7 +217,7 @@ export function FeedbackAttachmentField({
|
|
|
190
217
|
<div className="space-y-1.5">
|
|
191
218
|
<div className="flex flex-wrap gap-2">
|
|
192
219
|
<Button type="button" variant="secondary" onClick={() => fileInputRef.current?.click()}>
|
|
193
|
-
<Paperclip className="size-4" /> {
|
|
220
|
+
<Paperclip className="size-4" /> {text.attachmentAdd}
|
|
194
221
|
</Button>
|
|
195
222
|
{onCaptureScreenshot && (
|
|
196
223
|
<Button
|
|
@@ -208,14 +235,14 @@ export function FeedbackAttachmentField({
|
|
|
208
235
|
}}
|
|
209
236
|
>
|
|
210
237
|
<Camera className="size-4" />{" "}
|
|
211
|
-
{capturing ? "…" :
|
|
238
|
+
{capturing ? "…" : text.attachmentCapture}
|
|
212
239
|
</Button>
|
|
213
240
|
)}
|
|
214
241
|
</div>
|
|
215
242
|
{/* Said out loud, because a gesture with no affordance is a gesture
|
|
216
243
|
nobody finds. */}
|
|
217
244
|
<p className="text-xs text-[var(--text-muted)]">
|
|
218
|
-
{
|
|
245
|
+
{text.attachmentPaste}
|
|
219
246
|
</p>
|
|
220
247
|
</div>
|
|
221
248
|
)}
|
|
@@ -19,10 +19,11 @@ export interface FeedbackAttachmentLabels {
|
|
|
19
19
|
* straight under its textarea, where a second heading is noise. */
|
|
20
20
|
attachment?: string;
|
|
21
21
|
attachmentAdd: string;
|
|
22
|
-
/** Label for the "capture screenshot" button. Optional — falls back to
|
|
22
|
+
/** Label for the "capture screenshot" button. Optional — falls back to the provider's
|
|
23
|
+
* `feedbackAttachment.attachmentCapture`, then English. */
|
|
23
24
|
attachmentCapture?: string;
|
|
24
25
|
/** The line under the attachment buttons saying a screenshot can be pasted
|
|
25
|
-
* straight in. Optional — falls back to
|
|
26
|
+
* straight in. Optional — falls back to the provider's `feedbackAttachment`, then English. */
|
|
26
27
|
attachmentPaste?: string;
|
|
27
28
|
attachmentRemove: string;
|
|
28
29
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import type { RefObject } from "react";
|
|
3
|
+
|
|
4
|
+
/** Rows kept mounted beyond each edge, so a flick-scroll has something to show
|
|
5
|
+
* before the next render lands. */
|
|
6
|
+
const OVERSCAN = 6;
|
|
7
|
+
|
|
8
|
+
/** What {@link useWindowedRows} answers: render rows `first` up to (not including)
|
|
9
|
+
* `last`, inside a scroll body `totalHeight` pixels tall. */
|
|
10
|
+
export interface WindowedRows {
|
|
11
|
+
first: number;
|
|
12
|
+
last: number;
|
|
13
|
+
totalHeight: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Which rows of a fixed-height list are worth rendering.
|
|
18
|
+
*
|
|
19
|
+
* Hand-rolled rather than pulled from a virtualization library, for two reasons. With
|
|
20
|
+
* a fixed row height the whole problem is two divisions — a library earns its keep on
|
|
21
|
+
* measured, variable-height rows, and there are none here. And the obvious library for
|
|
22
|
+
* it (`@tanstack/react-virtual`) returns functions that cannot be memoized safely, so
|
|
23
|
+
* the React Compiler skips optimising any component that calls it: a list that exists
|
|
24
|
+
* to stay fast would opt out of the consumer's main source of speed.
|
|
25
|
+
*
|
|
26
|
+
* `scrollRef` is the element that scrolls. The rows are assumed to start at its top;
|
|
27
|
+
* a sticky header above them costs a row or two of the overscan, not correctness.
|
|
28
|
+
*
|
|
29
|
+
* Moved from Lenkbank, where `MeasuredGrid` and a segment list window the same way over
|
|
30
|
+
* rows of different heights — two copies of this would be two places to fix the day
|
|
31
|
+
* the overscan turns out to be wrong.
|
|
32
|
+
*/
|
|
33
|
+
export function useWindowedRows(
|
|
34
|
+
count: number,
|
|
35
|
+
rowHeight: number,
|
|
36
|
+
scrollRef: RefObject<HTMLElement | null>,
|
|
37
|
+
): WindowedRows {
|
|
38
|
+
const [scrollTop, setScrollTop] = useState(0);
|
|
39
|
+
const [viewport, setViewport] = useState(0);
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
const element = scrollRef.current;
|
|
43
|
+
if (!element) return;
|
|
44
|
+
const measure = () => {
|
|
45
|
+
setScrollTop(element.scrollTop);
|
|
46
|
+
setViewport(element.clientHeight);
|
|
47
|
+
};
|
|
48
|
+
measure();
|
|
49
|
+
element.addEventListener("scroll", measure, { passive: true });
|
|
50
|
+
// The pane usually sits in a responsive layout, so its height changes without the
|
|
51
|
+
// window's — a resize listener alone would miss every one of those. Guarded:
|
|
52
|
+
// jsdom and older embedded webviews have no ResizeObserver, and the list still
|
|
53
|
+
// has to render there.
|
|
54
|
+
const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(measure);
|
|
55
|
+
observer?.observe(element);
|
|
56
|
+
return () => {
|
|
57
|
+
element.removeEventListener("scroll", measure);
|
|
58
|
+
observer?.disconnect();
|
|
59
|
+
};
|
|
60
|
+
}, [scrollRef]);
|
|
61
|
+
|
|
62
|
+
const first = Math.max(0, Math.floor(scrollTop / rowHeight) - OVERSCAN);
|
|
63
|
+
// A viewport of 0 is the first render, before the pane has been laid out (and every
|
|
64
|
+
// render under jsdom). Rendering nothing then would leave the list blank until
|
|
65
|
+
// something scrolled it, so fall back to a screenful.
|
|
66
|
+
const visible = Math.ceil((viewport || 600) / rowHeight) + OVERSCAN * 2;
|
|
67
|
+
const last = Math.min(count, first + visible);
|
|
68
|
+
return { first, last, totalHeight: count * rowHeight };
|
|
69
|
+
}
|
package/src/i18n/defaults.ts
CHANGED
|
@@ -19,6 +19,8 @@ import { DEFAULT_SWATCH_PICKER_LABELS } from "../components/swatch-picker";
|
|
|
19
19
|
import { DEFAULT_ICON_PICKER_LABELS } from "../components/icon-picker";
|
|
20
20
|
import { DEFAULT_DIALOG_FRAME_LABELS } from "../components/dialog-frame";
|
|
21
21
|
import { DEFAULT_FILE_PICKER_LABELS } from "../components/file-button";
|
|
22
|
+
import { DEFAULT_MEASURED_GRID_LABELS } from "../components/measured-grid";
|
|
23
|
+
import { DEFAULT_FEEDBACK_ATTACHMENT_LABELS } from "../feedback/feedback-attachment";
|
|
22
24
|
import {
|
|
23
25
|
DEFAULT_APP_SHELL_LABELS,
|
|
24
26
|
DEFAULT_CALCULATOR_LABELS,
|
|
@@ -76,4 +78,6 @@ export const DEFAULT_UI_KIT_LABELS: UiKitLabels = {
|
|
|
76
78
|
iconPicker: DEFAULT_ICON_PICKER_LABELS,
|
|
77
79
|
dialogFrame: DEFAULT_DIALOG_FRAME_LABELS,
|
|
78
80
|
filePicker: DEFAULT_FILE_PICKER_LABELS,
|
|
81
|
+
measuredGrid: DEFAULT_MEASURED_GRID_LABELS,
|
|
82
|
+
feedbackAttachment: DEFAULT_FEEDBACK_ATTACHMENT_LABELS,
|
|
79
83
|
};
|
package/src/i18n/kit-labels.tsx
CHANGED
|
@@ -21,6 +21,8 @@ import type { SwatchPickerLabels } from "../components/swatch-picker";
|
|
|
21
21
|
import type { IconPickerLabels } from "../components/icon-picker";
|
|
22
22
|
import type { DialogFrameLabels } from "../components/dialog-frame";
|
|
23
23
|
import type { FilePickerLabels } from "../components/file-button";
|
|
24
|
+
import type { MeasuredGridLabels } from "../components/measured-grid";
|
|
25
|
+
import type { FeedbackAttachmentFieldLabels } from "../feedback/feedback-attachment";
|
|
24
26
|
|
|
25
27
|
/**
|
|
26
28
|
* EVERY string the kit renders, as one typed tree — and an optional provider that
|
|
@@ -208,6 +210,8 @@ export interface UiKitLabels {
|
|
|
208
210
|
iconPicker: IconPickerLabels;
|
|
209
211
|
dialogFrame: DialogFrameLabels;
|
|
210
212
|
filePicker: FilePickerLabels;
|
|
213
|
+
measuredGrid: MeasuredGridLabels;
|
|
214
|
+
feedbackAttachment: FeedbackAttachmentFieldLabels;
|
|
211
215
|
}
|
|
212
216
|
|
|
213
217
|
/** Any subset of the tree, one level deep — each namespace may be partial, and a
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { UiKitLabels } from "../kit-labels";
|
|
2
|
+
import { uiKitLabelsDe } from "./de";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Swiss Standard German: the German labels with Swiss spelling — "ss" for every "ß"
|
|
6
|
+
* (Schliessen, Grösse), which is the one systematic difference in UI text — and
|
|
7
|
+
* `de-CH` number formatting (1’234.5).
|
|
8
|
+
*
|
|
9
|
+
* Derived rather than duplicated, so a key added to the German labels is Swiss the
|
|
10
|
+
* moment it lands. Function labels are wrapped: their RESULT is respelled, so an
|
|
11
|
+
* argument the app passes in (a file name, a phrase to type) is respelled too — which
|
|
12
|
+
* is what a Swiss app writing "ss" everywhere wants.
|
|
13
|
+
*/
|
|
14
|
+
export const UI_KIT_LABELS_DE_CH: UiKitLabels = swiss(uiKitLabelsDe("de-CH"));
|
|
15
|
+
|
|
16
|
+
function respell(text: string): string {
|
|
17
|
+
return text.replace(/ß/g, "ss").replace(/ẞ/g, "SS");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function swiss<T>(value: T): T {
|
|
21
|
+
if (typeof value === "string") return respell(value) as T;
|
|
22
|
+
if (typeof value === "function") {
|
|
23
|
+
const fn = value as (...args: unknown[]) => unknown;
|
|
24
|
+
return ((...args: unknown[]) => swiss(fn(...args))) as T;
|
|
25
|
+
}
|
|
26
|
+
if (value && typeof value === "object") {
|
|
27
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, swiss(v)])) as T;
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
}
|