@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
package/src/shell/app-shell.tsx
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { useCallback, useEffect, useId, useRef, useState } from "react";
|
|
1
|
+
import { createContext, useCallback, useContext, useEffect, useId, useRef, useState } from "react";
|
|
2
2
|
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
3
3
|
import { createPortal } from "react-dom";
|
|
4
4
|
import { NavLink, matchPath, useLocation } from "react-router";
|
|
5
5
|
import { ChevronRight, PanelLeftClose, PanelLeftOpen } from "lucide-react";
|
|
6
6
|
import type { LucideIcon } from "lucide-react";
|
|
7
7
|
import { cn } from "../lib/cn";
|
|
8
|
+
import { dirOf, type Direction } from "../lib/direction";
|
|
8
9
|
import { readStored, writeStored } from "../lib/safe-storage";
|
|
9
10
|
import { Tooltip } from "../components/tooltip";
|
|
10
11
|
import { useAnchoredRect } from "../hooks/use-anchored-rect";
|
|
@@ -92,8 +93,23 @@ export interface AppShellProps extends ComponentPropsWithoutRef<"div"> {
|
|
|
92
93
|
* bottom bar — the phone's counterpart of the sidebar's second level. Default true;
|
|
93
94
|
* `false` keeps the single bar, where a group entry only links to its own `to`. */
|
|
94
95
|
mobileSubNav?: boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Lay the shell out inside its PARENT'S box instead of the viewport: `h-full` rather
|
|
98
|
+
* than `h-dvh`, its own scroll container at every width, a bottom bar pinned to the
|
|
99
|
+
* shell rather than the window, `--app-nav-h` published on the shell alone, and no
|
|
100
|
+
* persisted collapse state unless `collapseStorageKey` is passed. For an app preview,
|
|
101
|
+
* a device frame, a docs example.
|
|
102
|
+
*
|
|
103
|
+
* Implied for a shell rendered inside another AppShell, which is the case this
|
|
104
|
+
* exists for: the inner one used to publish its own nav height on `<html>`, fight
|
|
105
|
+
* the outer one over it, and remove it on unmount from under the outer app.
|
|
106
|
+
*/
|
|
107
|
+
embedded?: boolean;
|
|
95
108
|
}
|
|
96
109
|
|
|
110
|
+
/** True below an AppShell — how a nested shell knows it is one. */
|
|
111
|
+
const AppShellNesting = createContext(false);
|
|
112
|
+
|
|
97
113
|
/**
|
|
98
114
|
* The bottom nav's own height, published as `--app-nav-h` so anything that has to sit
|
|
99
115
|
* ON it can stop guessing.
|
|
@@ -114,7 +130,11 @@ export interface AppShellProps extends ComponentPropsWithoutRef<"div"> {
|
|
|
114
130
|
* as a zero box — so a consumer can write `bottom-[var(--app-nav-h,0px)]` once and get
|
|
115
131
|
* the right answer at both widths without a breakpoint of its own.
|
|
116
132
|
*/
|
|
117
|
-
function useNavHeightVar(
|
|
133
|
+
function useNavHeightVar(
|
|
134
|
+
ref: React.RefObject<HTMLElement | null>,
|
|
135
|
+
shellRef: React.RefObject<HTMLElement | null>,
|
|
136
|
+
embedded: boolean,
|
|
137
|
+
): void {
|
|
118
138
|
// Re-run when the breakpoint flips. A ResizeObserver SKIPS an element that is not
|
|
119
139
|
// being rendered, so the nav going `display:none` above `md` fires no callback and
|
|
120
140
|
// would leave the last phone height published — a desktop footer would then sit 56px
|
|
@@ -123,7 +143,15 @@ function useNavHeightVar(ref: React.RefObject<HTMLElement | null>): void {
|
|
|
123
143
|
const isMdUp = useMediaQuery("(min-width: 768px)", false);
|
|
124
144
|
useEffect(() => {
|
|
125
145
|
const node = ref.current;
|
|
126
|
-
const
|
|
146
|
+
const shell = shellRef.current;
|
|
147
|
+
// Always on the shell's own root, so everything inside it — a nested shell's
|
|
148
|
+
// content included — reads the nearest bar's height. On <html> as well only for
|
|
149
|
+
// the OUTERMOST shell, for what it portals to <body> (a sheet docking on the bar).
|
|
150
|
+
// The DOM check backs up the context for a shell mounted in a separate React root.
|
|
151
|
+
const global = !embedded && !shell?.parentElement?.closest("[data-app-shell]");
|
|
152
|
+
const targets = [shell, global ? document.documentElement : null].filter(
|
|
153
|
+
(el): el is HTMLElement => el !== null,
|
|
154
|
+
);
|
|
127
155
|
// `getBoundingClientRect().height`, rounded DOWN — not `offsetHeight`, and not
|
|
128
156
|
// ceil. Measured on the running app at 406x816: the bar is **55.5px** tall, and
|
|
129
157
|
// both `offsetHeight` and `Math.ceil` answer 56. That is why live #314 round four
|
|
@@ -141,11 +169,10 @@ function useNavHeightVar(ref: React.RefObject<HTMLElement | null>): void {
|
|
|
141
169
|
// it (`z-30` against `z-10`): invisible. Too LARGE opens the seam. So floor, and
|
|
142
170
|
// consumers subtract a further pixel (see `invoice-line-totals.tsx`) for the case
|
|
143
171
|
// where the height lands on a whole pixel and floor leaves no overlap at all.
|
|
144
|
-
const publish = () =>
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
);
|
|
172
|
+
const publish = () => {
|
|
173
|
+
const value = `${node ? Math.floor(node.getBoundingClientRect().height) : 0}px`;
|
|
174
|
+
for (const el of targets) el.style.setProperty("--app-nav-h", value);
|
|
175
|
+
};
|
|
149
176
|
publish();
|
|
150
177
|
// The observer is the refinement, not the mechanism: `publish()` above is already
|
|
151
178
|
// right for a static nav, and jsdom has no ResizeObserver unless a test stubs one.
|
|
@@ -156,9 +183,9 @@ function useNavHeightVar(ref: React.RefObject<HTMLElement | null>): void {
|
|
|
156
183
|
if (ro && node) ro.observe(node);
|
|
157
184
|
return () => {
|
|
158
185
|
ro?.disconnect();
|
|
159
|
-
|
|
186
|
+
for (const el of targets) el.style.removeProperty("--app-nav-h");
|
|
160
187
|
};
|
|
161
|
-
}, [ref, isMdUp]);
|
|
188
|
+
}, [ref, shellRef, embedded, isMdUp]);
|
|
162
189
|
}
|
|
163
190
|
|
|
164
191
|
/**
|
|
@@ -174,15 +201,21 @@ export function AppShell({
|
|
|
174
201
|
children,
|
|
175
202
|
footer,
|
|
176
203
|
sidebarFooter,
|
|
177
|
-
collapseStorageKey
|
|
204
|
+
collapseStorageKey: collapseStorageKeyProp,
|
|
178
205
|
collapseLabel,
|
|
179
206
|
expandLabel,
|
|
180
207
|
subNav = "flyout",
|
|
181
208
|
mobileSubNav = true,
|
|
182
209
|
toggleGroupLabel,
|
|
210
|
+
embedded: embeddedProp,
|
|
183
211
|
className,
|
|
184
212
|
...rest
|
|
185
213
|
}: AppShellProps) {
|
|
214
|
+
const embedded = useContext(AppShellNesting) || !!embeddedProp;
|
|
215
|
+
// An embedded shell sharing the default key would overwrite the outer app's sidebar
|
|
216
|
+
// preference with its own — so it persists only when given a key of its own.
|
|
217
|
+
const collapseStorageKey =
|
|
218
|
+
collapseStorageKeyProp ?? (embedded ? null : "appLayout.sidebarCollapsed");
|
|
186
219
|
// The three props are per-shell overrides of the provider's `appShell`; one left
|
|
187
220
|
// undefined falls through to the provider rather than to English.
|
|
188
221
|
const labels = useKitLabels("appShell", DEFAULT_APP_SHELL_LABELS, {
|
|
@@ -195,16 +228,19 @@ export function AppShell({
|
|
|
195
228
|
// `localStorage` THROWS where site data is blocked (Safari private browsing, a
|
|
196
229
|
// partitioned webview). Unguarded, that was not a lost sidebar preference: nothing
|
|
197
230
|
// in the application mounted at all.
|
|
198
|
-
const [collapsed, setCollapsed] = useState(
|
|
231
|
+
const [collapsed, setCollapsed] = useState(
|
|
232
|
+
() => collapseStorageKey !== null && readStored(collapseStorageKey) === "1",
|
|
233
|
+
);
|
|
199
234
|
// The inline sidebar is an ACCORDION: one group open at a time, keyed by its `to`.
|
|
200
235
|
// Held here rather than per group so opening one can close the other — two lists
|
|
201
236
|
// open at once pushed every entry below them off a laptop screen.
|
|
202
237
|
const [openGroup, setOpenGroup] = useState<string | null>(null);
|
|
203
238
|
// Publishes `--app-nav-h` for anything that has to sit on the bottom bar.
|
|
204
239
|
const navRef = useRef<HTMLDivElement>(null);
|
|
205
|
-
|
|
240
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
|
241
|
+
useNavHeightVar(navRef, rootRef, embedded);
|
|
206
242
|
useEffect(() => {
|
|
207
|
-
writeStored(collapseStorageKey, collapsed ? "1" : "0");
|
|
243
|
+
if (collapseStorageKey !== null) writeStored(collapseStorageKey, collapsed ? "1" : "0");
|
|
208
244
|
}, [collapsed, collapseStorageKey]);
|
|
209
245
|
|
|
210
246
|
return (
|
|
@@ -212,126 +248,152 @@ export function AppShell({
|
|
|
212
248
|
// wrappers below — those are the kit's anchors for its own guided tour (#313/#322),
|
|
213
249
|
// and a second element answering to the same selector is a tour that highlights the
|
|
214
250
|
// wrong thing or nothing at all.
|
|
215
|
-
<
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
251
|
+
<AppShellNesting.Provider value={true}>
|
|
252
|
+
<div
|
|
253
|
+
{...rest}
|
|
254
|
+
ref={rootRef}
|
|
255
|
+
data-app-shell=""
|
|
256
|
+
className={cn(
|
|
257
|
+
"relative flex flex-col",
|
|
258
|
+
embedded
|
|
259
|
+
? // `contain: layout` makes this box the containing block for the `fixed`
|
|
260
|
+
// bottom bar below, so it pins to the shell rather than to the window.
|
|
261
|
+
"h-full min-h-0 overflow-hidden [contain:layout]"
|
|
262
|
+
: "min-h-screen md:h-dvh md:overflow-hidden",
|
|
263
|
+
className,
|
|
264
|
+
)}
|
|
265
|
+
>
|
|
266
|
+
{topBar}
|
|
267
|
+
<div className="flex flex-1 min-h-0">
|
|
268
|
+
<aside
|
|
269
|
+
className={cn(
|
|
270
|
+
// `border-e`: the sidebar sits at the START, which is the right in RTL.
|
|
271
|
+
"hidden md:flex md:flex-col border-e border-[var(--border)] bg-[var(--bg-surface)] transition-[width] duration-200 ease-out overflow-hidden",
|
|
272
|
+
// Embedded, the row it sits in is already the shell's height; the sticky
|
|
273
|
+
// offsets below are measured against the window's top bar.
|
|
274
|
+
embedded ? "md:self-stretch" : "md:sticky md:top-12 md:self-start md:h-[calc(100vh-3rem)]",
|
|
275
|
+
collapsed ? "md:w-14" : "md:w-60",
|
|
276
|
+
)}
|
|
277
|
+
>
|
|
278
|
+
{/* The `<nav>` is flex-1 so it fills the sidebar's height (with empty
|
|
279
|
+
space below the items); the data-tour marker goes on the INNER,
|
|
280
|
+
fit-content wrapper so the guided-tour spotlight hugs the actual nav
|
|
281
|
+
items instead of the whole tall column (feedback #322). The marker
|
|
282
|
+
also tags the mobile bottom bar below; the tour targets
|
|
283
|
+
`[data-tour="nav"]` and resolves to whichever is visible (#313). */}
|
|
284
|
+
<nav className="flex-1 px-2 py-3 overflow-y-auto overflow-x-hidden">
|
|
285
|
+
<div data-tour="nav" className="space-y-1">
|
|
286
|
+
{nav.map((item) => (
|
|
287
|
+
<SidebarNavItem
|
|
288
|
+
key={item.to}
|
|
289
|
+
item={item}
|
|
290
|
+
collapsed={collapsed}
|
|
291
|
+
inline={subNav === "inline"}
|
|
292
|
+
toggleGroupLabel={labels.toggleGroup}
|
|
293
|
+
openGroup={openGroup}
|
|
294
|
+
setOpenGroup={setOpenGroup}
|
|
295
|
+
/>
|
|
296
|
+
))}
|
|
297
|
+
</div>
|
|
298
|
+
</nav>
|
|
299
|
+
{sidebarFooter?.(collapsed)}
|
|
300
|
+
<div className="border-t border-[var(--border)] p-2">
|
|
301
|
+
{collapsed ? (
|
|
302
|
+
<Tooltip label={labels.expand} side="end" portal className="block">
|
|
303
|
+
<button
|
|
304
|
+
type="button"
|
|
305
|
+
onClick={() => setCollapsed(false)}
|
|
306
|
+
aria-label={labels.expand}
|
|
307
|
+
className="flex w-full items-center justify-center min-h-9 rounded-md text-[var(--text-muted)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-secondary)]"
|
|
308
|
+
>
|
|
309
|
+
<PanelLeftOpen className="size-4 shrink-0 rtl:-scale-x-100" />
|
|
310
|
+
</button>
|
|
311
|
+
</Tooltip>
|
|
312
|
+
) : (
|
|
248
313
|
<button
|
|
249
314
|
type="button"
|
|
250
|
-
onClick={() => setCollapsed(
|
|
251
|
-
aria-label={labels.
|
|
252
|
-
className="flex w-full items-center
|
|
315
|
+
onClick={() => setCollapsed(true)}
|
|
316
|
+
aria-label={labels.collapse}
|
|
317
|
+
className="flex w-full items-center gap-3 min-h-9 px-3 py-2 rounded-md text-sm text-[var(--text-muted)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-secondary)]"
|
|
253
318
|
>
|
|
254
|
-
<
|
|
319
|
+
<PanelLeftClose className="size-4 shrink-0 rtl:-scale-x-100" />
|
|
320
|
+
<span className="truncate">{labels.collapse}</span>
|
|
255
321
|
</button>
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
type="button"
|
|
260
|
-
onClick={() => setCollapsed(true)}
|
|
261
|
-
aria-label={labels.collapse}
|
|
262
|
-
className="flex w-full items-center gap-3 min-h-9 px-3 py-2 rounded-md text-sm text-[var(--text-muted)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-secondary)]"
|
|
263
|
-
>
|
|
264
|
-
<PanelLeftClose className="size-4 shrink-0" />
|
|
265
|
-
<span className="truncate">{labels.collapse}</span>
|
|
266
|
-
</button>
|
|
267
|
-
)}
|
|
268
|
-
</div>
|
|
269
|
-
</aside>
|
|
322
|
+
)}
|
|
323
|
+
</div>
|
|
324
|
+
</aside>
|
|
270
325
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
326
|
+
{/* Content column beside the sidebar. main grows to fill so the footer
|
|
327
|
+
sits at the bottom — at the viewport edge when content is short,
|
|
328
|
+
beside the sidebar rather than under it. */}
|
|
329
|
+
<div className="flex flex-1 flex-col min-w-0 md:min-h-0">
|
|
330
|
+
{/* `scrollbar-gutter: stable` reserves the scrollbar's track whether or
|
|
331
|
+
not it is currently needed. Without it this element — the app's only
|
|
332
|
+
scroll container from md up — narrows its own client box by the
|
|
333
|
+
scrollbar width the moment a page's content outgrows it, and widens
|
|
334
|
+
it again when the next page fits. Every centred `mx-auto` container
|
|
335
|
+
inside then jumps sideways by half a scrollbar, and any fluid-width
|
|
336
|
+
content changes width outright. Keksdose feedback #403 caught it as
|
|
337
|
+
"slight width increase/decrease" when switching to the notifications
|
|
338
|
+
settings section and back: that section is the only one tall enough
|
|
339
|
+
to scroll. Platform-dependent, which is why it is easy to miss —
|
|
340
|
+
overlay scrollbars (macOS, most Linux builds) take no layout space,
|
|
341
|
+
classic ones (Windows) take ~15px.
|
|
287
342
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
343
|
+
`both-edges`, not the bare `stable`: a one-sided reservation keeps the
|
|
344
|
+
width stable but moves the middle. On a page short enough not to
|
|
345
|
+
scroll, nothing is painted into the reserved track, so every centred
|
|
346
|
+
`mx-auto` container sits half a scrollbar left of the optical centre
|
|
347
|
+
and every full-bleed child stops ~15px short on the right. Keksdose
|
|
348
|
+
feedback #491 (/settings#data) and #498 (/budgets) both reported it as
|
|
349
|
+
"the left boundary looks smaller / cut off compared to the right" on
|
|
350
|
+
the one element per page whose frame is a saturated colour — the rose
|
|
351
|
+
destructive-action cards. Reserving the track on both edges keeps the
|
|
352
|
+
content centred whether or not the scrollbar is showing. */}
|
|
353
|
+
{/* `relative` makes <main> the containing block for everything absolutely
|
|
354
|
+
positioned inside it. Tailwind's `sr-only` is `position:absolute`; with no
|
|
355
|
+
positioned ancestor it resolves against the INITIAL containing block, is
|
|
356
|
+
laid out at its offset from the top of the DOCUMENT, and grows
|
|
357
|
+
`<html>`'s scroll height past the viewport — a second scrollbar beside this
|
|
358
|
+
one that scrolls into empty space. Guarding it per component (see
|
|
359
|
+
sr-only-containment.test) could never be complete: any page's own
|
|
360
|
+
`sr-only` span re-opened it. Here it is closed for every child at once. */}
|
|
361
|
+
<main
|
|
362
|
+
className={cn(
|
|
363
|
+
"relative flex-1 max-w-full overflow-x-clip pb-[calc(var(--app-nav-h,4rem)+1.5rem)] md:pb-0 md:min-h-0 md:overflow-y-auto md:[scrollbar-gutter:stable_both-edges]",
|
|
364
|
+
// Embedded there is no document scroll to lean on below `md` either.
|
|
365
|
+
embedded && "min-h-0 overflow-y-auto",
|
|
366
|
+
)}
|
|
367
|
+
>
|
|
368
|
+
{children}
|
|
369
|
+
</main>
|
|
370
|
+
{footer}
|
|
371
|
+
</div>
|
|
310
372
|
</div>
|
|
311
|
-
</div>
|
|
312
373
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
>
|
|
323
|
-
{mobileSubNav && <MobileSubNav nav={nav} />}
|
|
324
|
-
<nav
|
|
325
|
-
data-tour="nav"
|
|
326
|
-
className="grid"
|
|
327
|
-
style={{ gridTemplateColumns: `repeat(${nav.length}, minmax(0, 1fr))` }}
|
|
374
|
+
{/* The phone navigation: the group bar, and above it — when the current page
|
|
375
|
+
belongs to a group with pages of its own — the row of those pages. The pair
|
|
376
|
+
is the phone's version of the sidebar's two levels; without the upper row a
|
|
377
|
+
group's `items` were simply unreachable below `md`. ONE measured box, so
|
|
378
|
+
`--app-nav-h` covers both rows and whatever docks on the bar sits on the
|
|
379
|
+
whole of it. */}
|
|
380
|
+
<div
|
|
381
|
+
ref={navRef}
|
|
382
|
+
className="md:hidden fixed bottom-0 inset-x-0 z-30 border-t border-[var(--border)] bg-[var(--bg-surface)]"
|
|
328
383
|
>
|
|
329
|
-
{nav
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
384
|
+
{mobileSubNav && <MobileSubNav nav={nav} />}
|
|
385
|
+
<nav
|
|
386
|
+
data-tour="nav"
|
|
387
|
+
className="grid"
|
|
388
|
+
style={{ gridTemplateColumns: `repeat(${nav.length}, minmax(0, 1fr))` }}
|
|
389
|
+
>
|
|
390
|
+
{nav.map((item) => (
|
|
391
|
+
<MobileNavItem key={item.to} item={item} />
|
|
392
|
+
))}
|
|
393
|
+
</nav>
|
|
394
|
+
</div>
|
|
333
395
|
</div>
|
|
334
|
-
</
|
|
396
|
+
</AppShellNesting.Provider>
|
|
335
397
|
);
|
|
336
398
|
}
|
|
337
399
|
|
|
@@ -501,13 +563,13 @@ function SidebarNavItem({
|
|
|
501
563
|
& sl…" hides the one word that tells two pages apart, and there is height
|
|
502
564
|
to spare. (The phone bar keeps `truncate` — its cells must stay one row.) */}
|
|
503
565
|
{!collapsed && <span className="min-w-0 flex-1 break-words leading-snug">{item.label}</span>}
|
|
504
|
-
{!collapsed && hasSub && <ChevronRight className="size-3.5 shrink-0 opacity-60" />}
|
|
566
|
+
{!collapsed && hasSub && <ChevronRight className="size-3.5 shrink-0 opacity-60 rtl:-scale-x-100" />}
|
|
505
567
|
</NavLink>
|
|
506
568
|
);
|
|
507
569
|
|
|
508
570
|
if (hasSub) return <SidebarFlyout item={item}>{link}</SidebarFlyout>;
|
|
509
571
|
return collapsed ? (
|
|
510
|
-
<Tooltip label={item.label} side="
|
|
572
|
+
<Tooltip label={item.label} side="end" portal className="block">
|
|
511
573
|
{link}
|
|
512
574
|
</Tooltip>
|
|
513
575
|
) : (
|
|
@@ -672,11 +734,18 @@ function SidebarFlyout({ item, children }: { item: AppShellNavItem; children: Re
|
|
|
672
734
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
673
735
|
const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
674
736
|
const [open, setOpen] = useState(false);
|
|
737
|
+
// The flyout is portalled out of the subtree whose `dir` it inherited; read on open.
|
|
738
|
+
const [dir, setDir] = useState<Direction>("ltr");
|
|
675
739
|
|
|
676
|
-
// Anchor the flyout to the item's
|
|
740
|
+
// Anchor the flyout to the item's END edge — its right, or its left in RTL, where
|
|
741
|
+
// the sidebar sits on the right of the window. The hook re-measures on
|
|
677
742
|
// scroll/resize (the nav list can scroll).
|
|
678
743
|
const rect = useAnchoredRect(wrapperRef, open);
|
|
679
|
-
const pos = rect
|
|
744
|
+
const pos = rect
|
|
745
|
+
? dir === "rtl"
|
|
746
|
+
? { top: rect.top, right: window.innerWidth - rect.left }
|
|
747
|
+
: { top: rect.top, left: rect.right }
|
|
748
|
+
: null;
|
|
680
749
|
|
|
681
750
|
const cancelClose = useCallback(() => {
|
|
682
751
|
if (closeTimer.current) {
|
|
@@ -694,6 +763,7 @@ function SidebarFlyout({ item, children }: { item: AppShellNavItem; children: Re
|
|
|
694
763
|
cancelClose();
|
|
695
764
|
if (activeFlyoutClose && activeFlyoutClose !== closeNow) activeFlyoutClose();
|
|
696
765
|
activeFlyoutClose = closeNow;
|
|
766
|
+
setDir(dirOf(wrapperRef.current));
|
|
697
767
|
setOpen(true);
|
|
698
768
|
}, [cancelClose, closeNow]);
|
|
699
769
|
const scheduleClose = useCallback(() => {
|
|
@@ -729,8 +799,9 @@ function SidebarFlyout({ item, children }: { item: AppShellNavItem; children: Re
|
|
|
729
799
|
pos &&
|
|
730
800
|
createPortal(
|
|
731
801
|
<div
|
|
732
|
-
|
|
733
|
-
|
|
802
|
+
dir={dir}
|
|
803
|
+
style={{ position: "fixed", ...pos }}
|
|
804
|
+
className="z-40 ps-1"
|
|
734
805
|
onMouseEnter={cancelClose}
|
|
735
806
|
onMouseLeave={scheduleClose}
|
|
736
807
|
>
|
|
@@ -60,7 +60,7 @@ export function TopBarActionMenu({
|
|
|
60
60
|
<span className="flex w-full items-center gap-2">
|
|
61
61
|
{entry.icon}
|
|
62
62
|
{entry.label}
|
|
63
|
-
{entry.trailing && <span className="
|
|
63
|
+
{entry.trailing && <span className="ms-auto ps-2">{entry.trailing}</span>}
|
|
64
64
|
</span>
|
|
65
65
|
);
|
|
66
66
|
return (
|
|
@@ -11,9 +11,19 @@ import { DEFAULT_TOP_BAR_LABELS, useKitLabels } from "../i18n/kit-labels";
|
|
|
11
11
|
export const TOPBAR_TRIGGER_CLASS =
|
|
12
12
|
"size-9 rounded-md flex items-center justify-center text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-hover)]";
|
|
13
13
|
|
|
14
|
-
/** Shared row styling for the top-bar hover-menu items.
|
|
14
|
+
/** Shared row styling for the top-bar hover-menu items. `text-start`, so a label that
|
|
15
|
+
* wraps aligns to the reading edge in RTL too. */
|
|
15
16
|
export const TOPBAR_MENU_ITEM_CLASS =
|
|
16
|
-
"flex w-full items-center justify-between gap-3 px-3 py-2 text-sm text-
|
|
17
|
+
"flex w-full items-center justify-between gap-3 px-3 py-2 text-sm text-start text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]";
|
|
18
|
+
|
|
19
|
+
/** The small uppercase heading row the top-bar menus share (`heading`). */
|
|
20
|
+
function MenuHeading({ children }: { children: string }) {
|
|
21
|
+
return (
|
|
22
|
+
<li className="px-3 py-1 text-[10px] font-semibold uppercase tracking-wide text-[var(--text-placeholder)]">
|
|
23
|
+
{children}
|
|
24
|
+
</li>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
17
27
|
|
|
18
28
|
/** Sun/Moon toggle for the light/dark theme. Controlled: the app owns the mode. */
|
|
19
29
|
export function ThemeToggle({
|
|
@@ -92,11 +102,7 @@ export function PaletteMenu({
|
|
|
92
102
|
>
|
|
93
103
|
{(close) => (
|
|
94
104
|
<ul className="w-60 py-1">
|
|
95
|
-
{heading &&
|
|
96
|
-
<li className="px-3 py-1 text-[10px] font-semibold uppercase tracking-wide text-[var(--text-placeholder)]">
|
|
97
|
-
{heading}
|
|
98
|
-
</li>
|
|
99
|
-
)}
|
|
105
|
+
{heading && <MenuHeading>{heading}</MenuHeading>}
|
|
100
106
|
{palettes.map((p) => {
|
|
101
107
|
const ts = p[mode];
|
|
102
108
|
const swatches = [ts.bgPage, ts.brand, ts.moneyIncome, ts.moneyExpense, ts.moneyNet];
|
|
@@ -123,7 +129,7 @@ export function PaletteMenu({
|
|
|
123
129
|
/>
|
|
124
130
|
))}
|
|
125
131
|
</span>
|
|
126
|
-
<span className="flex min-w-0 flex-col text-
|
|
132
|
+
<span className="flex min-w-0 flex-col text-start">
|
|
127
133
|
<span className="truncate">{p.name}</span>
|
|
128
134
|
<span className="truncate text-[11px] text-[var(--text-placeholder)]">
|
|
129
135
|
{p.blurb}
|
|
@@ -164,12 +170,16 @@ export function LanguageMenu({
|
|
|
164
170
|
current,
|
|
165
171
|
onChange,
|
|
166
172
|
ariaLabel,
|
|
173
|
+
heading,
|
|
167
174
|
}: {
|
|
168
175
|
options: LanguageOption[];
|
|
169
176
|
current: string | undefined;
|
|
170
177
|
onChange: (code: string) => void;
|
|
171
178
|
/** Default: `topBar.language` from the {@link UiKitProvider}, else English. */
|
|
172
179
|
ariaLabel?: string;
|
|
180
|
+
/** Optional small heading row above the list — as on `TopBarActionMenu`,
|
|
181
|
+
* `OptionSwitcherMenu` and `PaletteMenu`. */
|
|
182
|
+
heading?: string;
|
|
173
183
|
}) {
|
|
174
184
|
const labels = useKitLabels("topBar", DEFAULT_TOP_BAR_LABELS, { language: ariaLabel });
|
|
175
185
|
const active = options.find((o) => o.code === current);
|
|
@@ -198,6 +208,7 @@ export function LanguageMenu({
|
|
|
198
208
|
>
|
|
199
209
|
{(close) => (
|
|
200
210
|
<ul className="py-1">
|
|
211
|
+
{heading && <MenuHeading>{heading}</MenuHeading>}
|
|
201
212
|
{options.map((lang) => {
|
|
202
213
|
const isActive = lang.code === current;
|
|
203
214
|
return (
|
|
@@ -8,9 +8,16 @@ import { contrast, parseHex as parseRgbHex } from "./color";
|
|
|
8
8
|
|
|
9
9
|
const PALETTE_SIZE = 9;
|
|
10
10
|
|
|
11
|
-
/** CVD-safe categorical colour for series `index`, as a theme-aware CSS var.
|
|
11
|
+
/** CVD-safe categorical colour for series `index`, as a theme-aware CSS var.
|
|
12
|
+
*
|
|
13
|
+
* Wraps in both directions, the way `strokeDash` does: JS `%` keeps the sign,
|
|
14
|
+
* so `-1 % 9` is `-1` and the plain formula named `--chart-0`, a token that does not
|
|
15
|
+
* exist — a series with no stroke at all. A fractional index is truncated, and one
|
|
16
|
+
* that is not a number (NaN, ±Infinity — an `indexOf` miss divided by something)
|
|
17
|
+
* takes the first colour rather than `var(--chart-NaN)`. */
|
|
12
18
|
export function paletteFor(index: number): string {
|
|
13
|
-
|
|
19
|
+
const i = Number.isFinite(index) ? Math.trunc(index) : 0;
|
|
20
|
+
return `var(--chart-${(((i % PALETTE_SIZE) + PALETTE_SIZE) % PALETTE_SIZE) + 1})`;
|
|
14
21
|
}
|
|
15
22
|
|
|
16
23
|
/** Semantic money colours, theme-aware via tokens. Pair with +/- and arrow
|