@eifi1/ui-kit 0.29.0 → 0.31.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/README.md +65 -6
- package/dist/account/account-parts.d.ts +21 -0
- package/dist/account/account-parts.js +40 -0
- package/dist/account/account-parts.js.map +1 -0
- package/dist/account/data-export-setting.d.ts +73 -0
- package/dist/account/data-export-setting.js +114 -0
- package/dist/account/data-export-setting.js.map +1 -0
- package/dist/account/delete-account-setting.d.ts +95 -0
- package/dist/account/delete-account-setting.js +200 -0
- package/dist/account/delete-account-setting.js.map +1 -0
- package/dist/account/email-change-setting.d.ts +136 -0
- package/dist/account/email-change-setting.js +278 -0
- package/dist/account/email-change-setting.js.map +1 -0
- package/dist/account/sessions-setting.d.ts +110 -0
- package/dist/account/sessions-setting.js +147 -0
- package/dist/account/sessions-setting.js.map +1 -0
- package/dist/admin/admin-action-confirm.d.ts +95 -0
- package/dist/admin/admin-action-confirm.js +276 -0
- package/dist/admin/admin-action-confirm.js.map +1 -0
- package/dist/admin/admin-action-log.d.ts +105 -0
- package/dist/admin/admin-action-log.js +153 -0
- package/dist/admin/admin-action-log.js.map +1 -0
- package/dist/admin/admin-parts.d.ts +40 -0
- package/dist/admin/admin-parts.js +45 -0
- package/dist/admin/admin-parts.js.map +1 -0
- package/dist/admin/invitations-panel.d.ts +95 -0
- package/dist/admin/invitations-panel.js +290 -0
- package/dist/admin/invitations-panel.js.map +1 -0
- package/dist/admin/reviewer-scope-editor.d.ts +95 -0
- package/dist/admin/reviewer-scope-editor.js +134 -0
- package/dist/admin/reviewer-scope-editor.js.map +1 -0
- package/dist/admin/role-select.d.ts +95 -0
- package/dist/admin/role-select.js +128 -0
- package/dist/admin/role-select.js.map +1 -0
- package/dist/admin/transfer-ownership-dialog.d.ts +95 -0
- package/dist/admin/transfer-ownership-dialog.js +83 -0
- package/dist/admin/transfer-ownership-dialog.js.map +1 -0
- package/dist/admin/user-roster.d.ts +95 -0
- package/dist/admin/user-roster.js +285 -0
- package/dist/admin/user-roster.js.map +1 -0
- package/dist/auth/auth-errors.d.ts +103 -6
- package/dist/auth/auth-errors.js +68 -2
- package/dist/auth/auth-errors.js.map +1 -1
- package/dist/auth/forgot-password-form.d.ts +10 -3
- package/dist/auth/forgot-password-form.js +6 -3
- package/dist/auth/forgot-password-form.js.map +1 -1
- package/dist/auth/register-form.d.ts +21 -12
- package/dist/auth/register-form.js +6 -4
- package/dist/auth/register-form.js.map +1 -1
- package/dist/auth/reset-password-form.d.ts +10 -3
- package/dist/auth/reset-password-form.js +6 -3
- package/dist/auth/reset-password-form.js.map +1 -1
- package/dist/auth/sign-in-form.d.ts +75 -9
- package/dist/auth/sign-in-form.js +105 -15
- package/dist/auth/sign-in-form.js.map +1 -1
- package/dist/auth/verify-email.d.ts +7 -2
- package/dist/auth/verify-email.js +44 -8
- package/dist/auth/verify-email.js.map +1 -1
- package/dist/chart.d.ts +20 -11
- package/dist/components/account-chips.d.ts +47 -7
- package/dist/components/account-chips.js +19 -3
- package/dist/components/account-chips.js.map +1 -1
- package/dist/components/account-settings-labels.d.ts +24 -1
- package/dist/components/account-settings-labels.js +6 -2
- package/dist/components/account-settings-labels.js.map +1 -1
- package/dist/components/account-settings.d.ts +69 -8
- package/dist/components/account-settings.js +133 -25
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/amount-input.d.ts +21 -12
- package/dist/components/button-group.d.ts +21 -12
- package/dist/components/calculator.d.ts +21 -12
- package/dist/components/confirm-dialog.d.ts +21 -12
- package/dist/components/copy-button.d.ts +18 -9
- package/dist/components/danger-confirm.d.ts +21 -12
- package/dist/components/data-table.js +8 -1
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.js +9 -4
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/facing-pair.d.ts +21 -12
- package/dist/components/file-button.d.ts +21 -12
- package/dist/components/file-dropzone.d.ts +21 -12
- package/dist/components/form-actions.d.ts +21 -12
- package/dist/components/iban-input.d.ts +21 -12
- package/dist/components/language-select.d.ts +21 -12
- package/dist/components/legal-page.d.ts +21 -12
- package/dist/components/legal.d.ts +21 -12
- package/dist/components/money-field.d.ts +21 -12
- package/dist/components/number-field.d.ts +21 -12
- package/dist/components/number-input.d.ts +21 -12
- package/dist/components/numpad-sheet.d.ts +21 -12
- package/dist/components/passkeys-setting.js +2 -1
- package/dist/components/passkeys-setting.js.map +1 -1
- package/dist/components/phone-input.d.ts +21 -12
- package/dist/components/series-chart.d.ts +21 -12
- package/dist/components/settings-fields.d.ts +55 -13
- package/dist/components/settings-fields.js +20 -1
- package/dist/components/settings-fields.js.map +1 -1
- package/dist/components/share-card.d.ts +20 -11
- package/dist/components/text-link.d.ts +21 -12
- package/dist/components/time-input.d.ts +21 -12
- package/dist/components/ui.d.ts +17 -8
- package/dist/{register-form-V1OeNlMr.d.ts → delete-account-setting-DCFS5Vof.d.ts} +6612 -5612
- package/dist/demo/demo-banner.d.ts +51 -0
- package/dist/demo/demo-banner.js +97 -0
- package/dist/demo/demo-banner.js.map +1 -0
- package/dist/demo/demo-countdown.d.ts +58 -0
- package/dist/demo/demo-countdown.js +79 -0
- package/dist/demo/demo-countdown.js.map +1 -0
- package/dist/demo/demo-ended.d.ts +45 -0
- package/dist/demo/demo-ended.js +42 -0
- package/dist/demo/demo-ended.js.map +1 -0
- package/dist/demo/demo-labels.d.ts +53 -0
- package/dist/demo/demo-labels.js +29 -0
- package/dist/demo/demo-labels.js.map +1 -0
- package/dist/demo/demo-session.d.ts +30 -0
- package/dist/demo/demo-session.js +8 -0
- package/dist/demo/demo-session.js.map +1 -0
- package/dist/demo/demo-start.d.ts +54 -0
- package/dist/demo/demo-start.js +68 -0
- package/dist/demo/demo-start.js.map +1 -0
- package/dist/feedback/feedback-attachment.d.ts +21 -12
- package/dist/feedback/feedback-context.d.ts +21 -12
- package/dist/feedback/feedback-crash.d.ts +21 -12
- package/dist/feedback/feedback-dialog.d.ts +21 -12
- package/dist/feedback/feedback-inbox.d.ts +21 -12
- package/dist/feedback/feedback-inbox.js +12 -2
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback/feedback-labels.d.ts +21 -12
- package/dist/feedback/feedback-labels.js +1 -0
- package/dist/feedback/feedback-labels.js.map +1 -1
- package/dist/feedback/feedback-menu.d.ts +21 -12
- package/dist/feedback/feedback-record.d.ts +21 -12
- package/dist/feedback/feedback-record.js +2 -0
- package/dist/feedback/feedback-record.js.map +1 -1
- package/dist/feedback/feedback-row-detail.d.ts +21 -12
- package/dist/feedback/feedback-row-detail.js +7 -0
- package/dist/feedback/feedback-row-detail.js.map +1 -1
- package/dist/feedback/feedback-status-undo.d.ts +21 -12
- package/dist/feedback/feedback-submit.d.ts +21 -12
- package/dist/feedback/feedback-swipe.d.ts +22 -13
- package/dist/feedback/feedback-swipe.js.map +1 -1
- package/dist/feedback/feedback-table.d.ts +18 -9
- package/dist/feedback/feedback-thread.d.ts +21 -12
- package/dist/feedback.d.ts +21 -12
- package/dist/hooks/use-anchored-panel.d.ts +6 -2
- package/dist/hooks/use-anchored-panel.js +2 -1
- package/dist/hooks/use-anchored-panel.js.map +1 -1
- package/dist/hooks/use-file-drop.d.ts +21 -12
- package/dist/i18n/defaults.d.ts +21 -12
- package/dist/i18n/defaults.js +28 -0
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.d.ts +21 -12
- package/dist/i18n/german.js +298 -12
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +14 -5
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.d.ts +15 -6
- package/dist/i18n/locales/de-CH.d.ts +21 -12
- package/dist/i18n/locales/en.d.ts +21 -12
- package/dist/i18n/locales/es.d.ts +21 -12
- package/dist/i18n/locales/es.js +287 -10
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +21 -12
- package/dist/i18n/locales/fr.js +290 -7
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +21 -12
- package/dist/i18n/locales/hu.js +288 -12
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +21 -12
- package/dist/i18n/locales/it.js +289 -7
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +21 -12
- package/dist/i18n/locales/zh.js +279 -7
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.d.ts +21 -12
- package/dist/i18n/review.js +50 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +31 -4
- package/dist/index.js +148 -0
- package/dist/index.js.map +1 -1
- package/dist/landing/access.d.ts +48 -0
- package/dist/landing/access.js +38 -0
- package/dist/landing/access.js.map +1 -0
- package/dist/landing/landing-actions.d.ts +138 -0
- package/dist/landing/landing-actions.js +39 -0
- package/dist/landing/landing-actions.js.map +1 -0
- package/dist/landing/landing-labels.d.ts +43 -0
- package/dist/landing/landing-labels.js +23 -0
- package/dist/landing/landing-labels.js.map +1 -0
- package/dist/landing/landing-sections.d.ts +218 -0
- package/dist/landing/landing-sections.js +167 -0
- package/dist/landing/landing-sections.js.map +1 -0
- package/dist/landing/page-seo.d.ts +99 -0
- package/dist/landing/page-seo.js +121 -0
- package/dist/landing/page-seo.js.map +1 -0
- package/dist/landing/public-header.d.ts +148 -0
- package/dist/landing/public-header.js +49 -0
- package/dist/landing/public-header.js.map +1 -0
- package/dist/landing/routing.d.ts +218 -0
- package/dist/landing/routing.js +81 -0
- package/dist/landing/routing.js.map +1 -0
- package/dist/rhf/fields.d.ts +21 -12
- package/dist/rhf/form.d.ts +21 -12
- package/dist/rhf.d.ts +21 -12
- package/dist/settings/settings-catalogue.d.ts +84 -0
- package/dist/settings/settings-catalogue.js +47 -0
- package/dist/settings/settings-catalogue.js.map +1 -0
- package/dist/settings/settings-context.d.ts +31 -0
- package/dist/settings/settings-context.js +11 -0
- package/dist/settings/settings-context.js.map +1 -0
- package/dist/settings/settings-heading.d.ts +50 -0
- package/dist/settings/settings-heading.js +27 -0
- package/dist/settings/settings-heading.js.map +1 -0
- package/dist/settings/settings-labels.d.ts +41 -0
- package/dist/settings/settings-labels.js +38 -0
- package/dist/settings/settings-labels.js.map +1 -0
- package/dist/settings/settings-layout.d.ts +77 -0
- package/dist/settings/settings-layout.js +242 -0
- package/dist/settings/settings-layout.js.map +1 -0
- package/dist/settings/settings-section.d.ts +133 -0
- package/dist/settings/settings-section.js +63 -0
- package/dist/settings/settings-section.js.map +1 -0
- package/dist/settings/use-account-language.d.ts +83 -0
- package/dist/settings/use-account-language.js +62 -0
- package/dist/settings/use-account-language.js.map +1 -0
- package/dist/settings/use-settings-focus.d.ts +34 -0
- package/dist/settings/use-settings-focus.js +77 -0
- package/dist/settings/use-settings-focus.js.map +1 -0
- package/dist/settings/use-settings-route.d.ts +118 -0
- package/dist/settings/use-settings-route.js +120 -0
- package/dist/settings/use-settings-route.js.map +1 -0
- package/dist/shell/app-shell.d.ts +27 -12
- package/dist/shell/app-shell.js +3 -2
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/top-bar-brand.d.ts +21 -12
- package/dist/shell.d.ts +25 -6
- package/dist/shell.js +56 -0
- package/dist/shell.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +21 -12
- package/dist/wizard.d.ts +21 -12
- package/package.json +2 -1
- package/src/account/account-parts.ts +67 -0
- package/src/account/data-export-setting.tsx +219 -0
- package/src/account/delete-account-setting.tsx +363 -0
- package/src/account/email-change-setting.tsx +454 -0
- package/src/account/sessions-setting.tsx +284 -0
- package/src/admin/admin-action-confirm.tsx +495 -0
- package/src/admin/admin-action-log.tsx +297 -0
- package/src/admin/admin-parts.ts +82 -0
- package/src/admin/invitations-panel.tsx +523 -0
- package/src/admin/reviewer-scope-editor.tsx +234 -0
- package/src/admin/role-select.tsx +286 -0
- package/src/admin/transfer-ownership-dialog.tsx +172 -0
- package/src/admin/user-roster.tsx +573 -0
- package/src/auth/auth-errors.ts +196 -8
- package/src/auth/forgot-password-form.tsx +21 -5
- package/src/auth/register-form.tsx +24 -6
- package/src/auth/reset-password-form.tsx +19 -5
- package/src/auth/sign-in-form.tsx +241 -38
- package/src/auth/verify-email.tsx +59 -8
- package/src/components/account-chips.tsx +64 -7
- package/src/components/account-settings-labels.ts +28 -1
- package/src/components/account-settings.tsx +231 -34
- package/src/components/data-table.tsx +8 -0
- package/src/components/date-picker.tsx +9 -4
- package/src/components/passkeys-setting.tsx +2 -1
- package/src/components/settings-fields.tsx +50 -1
- package/src/demo/demo-banner.tsx +159 -0
- package/src/demo/demo-countdown.ts +141 -0
- package/src/demo/demo-ended.tsx +89 -0
- package/src/demo/demo-labels.ts +78 -0
- package/src/demo/demo-session.ts +32 -0
- package/src/demo/demo-start.tsx +150 -0
- package/src/feedback/feedback-inbox.tsx +21 -8
- package/src/feedback/feedback-labels.ts +2 -1
- package/src/feedback/feedback-record.ts +10 -7
- package/src/feedback/feedback-row-detail.tsx +28 -4
- package/src/feedback/feedback-swipe.tsx +1 -1
- package/src/hooks/use-anchored-panel.ts +14 -3
- package/src/i18n/defaults.ts +28 -0
- package/src/i18n/german.ts +327 -5
- package/src/i18n/kit-labels.tsx +42 -0
- package/src/i18n/locales/es.ts +313 -3
- package/src/i18n/locales/fr.ts +316 -0
- package/src/i18n/locales/hu.ts +308 -5
- package/src/i18n/locales/it.ts +313 -0
- package/src/i18n/locales/zh.ts +286 -0
- package/src/i18n/review.ts +49 -0
- package/src/index.ts +184 -0
- package/src/landing/access.ts +97 -0
- package/src/landing/landing-actions.tsx +92 -0
- package/src/landing/landing-labels.ts +59 -0
- package/src/landing/landing-sections.tsx +337 -0
- package/src/landing/page-seo.ts +251 -0
- package/src/landing/public-header.tsx +120 -0
- package/src/landing/routing.tsx +209 -0
- package/src/settings/settings-catalogue.ts +132 -0
- package/src/settings/settings-context.ts +33 -0
- package/src/settings/settings-heading.tsx +65 -0
- package/src/settings/settings-labels.ts +75 -0
- package/src/settings/settings-layout.tsx +379 -0
- package/src/settings/settings-section.tsx +108 -0
- package/src/settings/use-account-language.ts +146 -0
- package/src/settings/use-settings-focus.ts +120 -0
- package/src/settings/use-settings-route.ts +258 -0
- package/src/shell/app-shell.tsx +10 -2
- package/src/shell.ts +45 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { ReactNode, ReactElement } from 'react';
|
|
2
|
+
import { SettingsGroup, SettingsEntry } from './settings-catalogue.js';
|
|
3
|
+
import { SettingsLabels } from './settings-labels.js';
|
|
4
|
+
import { SettingsLayoutMode } from './use-settings-route.js';
|
|
5
|
+
import '../search/search-index.js';
|
|
6
|
+
|
|
7
|
+
/** The page's width cap (§3.2): `6xl` for settings, `7xl` for keksdose's admin, whose
|
|
8
|
+
* user table scrolled sideways at 6xl (dev#488). */
|
|
9
|
+
type SettingsLayoutWidth = "6xl" | "7xl";
|
|
10
|
+
/** More entries than this and the search field shows by itself (§3.4): Kurvenschmiede has
|
|
11
|
+
* about twelve, keksdose 26; a page of six needs none. */
|
|
12
|
+
declare const SETTINGS_SEARCH_THRESHOLD = 8;
|
|
13
|
+
interface SettingsLayoutProps<G extends string> {
|
|
14
|
+
/** The page's `h1`: "Settings", "Administration". Default: `labels.title`. */
|
|
15
|
+
title?: ReactNode;
|
|
16
|
+
/** The groups, in order (§4.2). */
|
|
17
|
+
groups: readonly SettingsGroup<G>[];
|
|
18
|
+
/** One entry per setting (§4.1). Hidden entries leave search; a group left without a
|
|
19
|
+
* visible one is hidden. */
|
|
20
|
+
entries: readonly SettingsEntry<G>[];
|
|
21
|
+
/** `/settings` or `/admin`. The layout owns every path below it. */
|
|
22
|
+
basePath: string;
|
|
23
|
+
/** The group `/settings` opens on a desktop. Default: the first visible one. */
|
|
24
|
+
defaultGroup?: G;
|
|
25
|
+
/** Retired group ids and their successors, replaced in the address (§3.1). */
|
|
26
|
+
aliases?: Readonly<Partial<Record<string, G>>>;
|
|
27
|
+
/**
|
|
28
|
+
* The search field. Left out, it shows once more than {@link SETTINGS_SEARCH_THRESHOLD}
|
|
29
|
+
* entries are visible; `true` or `false` wins over the count — keksdose's admin keeps
|
|
30
|
+
* none (§3.4, §10.3). ⌘K gets the entries either way ({@link settingsSearchEntries}).
|
|
31
|
+
*/
|
|
32
|
+
search?: boolean;
|
|
33
|
+
/** See {@link SettingsLayoutWidth}. Default `"6xl"`. */
|
|
34
|
+
width?: SettingsLayoutWidth;
|
|
35
|
+
/** The group's cards, in one column. `sub` is the path segment after the group, which
|
|
36
|
+
* a card owns (`useSettingsLayout().selectSub`). */
|
|
37
|
+
renderGroup: (group: G, sub: string | null) => ReactNode;
|
|
38
|
+
/**
|
|
39
|
+
* Rows under the phone's group list that LEAVE settings — kastlan's "Administration",
|
|
40
|
+
* its import page (§3.3, §5). `ListItem`s with an `href`; the layout puts them in a list
|
|
41
|
+
* of their own, so they never read as a group. Not shown on a desktop, where the app's
|
|
42
|
+
* own navigation carries them.
|
|
43
|
+
*/
|
|
44
|
+
phoneFooter?: ReactNode;
|
|
45
|
+
/** Prop > `<UiKitProvider labels={{ settings }}>` > English. */
|
|
46
|
+
labels?: Partial<SettingsLabels>;
|
|
47
|
+
/** See {@link SettingsLayoutMode}. Default `"auto"`: the screen decides. */
|
|
48
|
+
layout?: SettingsLayoutMode;
|
|
49
|
+
className?: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The settings page (docs/settings-harmonization.md §3), and the admin page built the
|
|
53
|
+
* same way (§5). keksdose's page is the reference for the desktop; the phone pattern and
|
|
54
|
+
* the path segments are new for all three apps.
|
|
55
|
+
*
|
|
56
|
+
* **Desktop** (§3.2): the page's `PageHeader`, the search field, then a 14rem sidebar —
|
|
57
|
+
* the kit's vertical `Tabs`, a link per group, sticky so a long group keeps the list in
|
|
58
|
+
* view — beside the open group: its heading (`h2`), its help line and its cards in ONE
|
|
59
|
+
* column, never a masonry (keksdose dev#490/#493).
|
|
60
|
+
*
|
|
61
|
+
* **Phone** (§3.3, below `md`): a drill-down. `/settings` is a list of the groups (icon,
|
|
62
|
+
* name, help line, chevron) with `phoneFooter` under it; a group is a page of its own with
|
|
63
|
+
* a back link, its name as the `h1` and the cards under it. It replaces keksdose's chip
|
|
64
|
+
* strip.
|
|
65
|
+
*
|
|
66
|
+
* **Routes, history and `?focus=`** are {@link useSettingsRoute}'s and
|
|
67
|
+
* {@link useSettingsFocus}'s. **Search** (§3.4) runs the kit's matcher over the catalogue:
|
|
68
|
+
* accents folded, ranked, over the title, the group's name and the keywords. On a desktop
|
|
69
|
+
* the sidebar dims the groups without hits and counts the others' while the hits replace
|
|
70
|
+
* the group; on a phone they replace the list. A hit is a link to its card.
|
|
71
|
+
*
|
|
72
|
+
* The cards inside read {@link useSettingsLayout} for `selectSub`, and their titles are
|
|
73
|
+
* headings under the group's ({@link SettingsHeadingLevel}).
|
|
74
|
+
*/
|
|
75
|
+
declare function SettingsLayout<G extends string>({ title, groups, entries, basePath, defaultGroup, aliases, search, width, renderGroup, phoneFooter, labels: labelsProp, layout, className, }: SettingsLayoutProps<G>): ReactElement;
|
|
76
|
+
|
|
77
|
+
export { SETTINGS_SEARCH_THRESHOLD, SettingsLayout, type SettingsLayoutProps, type SettingsLayoutWidth };
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useId, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { Link, useHref, useLocation } from "react-router";
|
|
5
|
+
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
6
|
+
import { Button, Card, EmptyState, Tabs } from "../components/ui.js";
|
|
7
|
+
import { Chip } from "../components/chip.js";
|
|
8
|
+
import { List, ListItem } from "../components/list.js";
|
|
9
|
+
import { PageHeader } from "../components/page-header.js";
|
|
10
|
+
import { SearchField } from "../components/search-field.js";
|
|
11
|
+
import { cn } from "../lib/cn.js";
|
|
12
|
+
import { useKitLabels } from "../i18n/kit-labels.js";
|
|
13
|
+
import { createSearchIndex } from "../search/search-index.js";
|
|
14
|
+
import {
|
|
15
|
+
normalizeSettingsBase,
|
|
16
|
+
settingsHref,
|
|
17
|
+
settingsKeywords,
|
|
18
|
+
visibleSettingsEntries
|
|
19
|
+
} from "./settings-catalogue.js";
|
|
20
|
+
import { SettingsLayoutContext } from "./settings-context.js";
|
|
21
|
+
import { SettingsHeadingLevel } from "./settings-heading.js";
|
|
22
|
+
import { DEFAULT_SETTINGS_LABELS } from "./settings-labels.js";
|
|
23
|
+
import { settingsFromListState, useSettingsRoute } from "./use-settings-route.js";
|
|
24
|
+
import { useSettingsFocus } from "./use-settings-focus.js";
|
|
25
|
+
const SETTINGS_SEARCH_THRESHOLD = 8;
|
|
26
|
+
function SettingsLayout({
|
|
27
|
+
title,
|
|
28
|
+
groups,
|
|
29
|
+
entries,
|
|
30
|
+
basePath,
|
|
31
|
+
defaultGroup,
|
|
32
|
+
aliases,
|
|
33
|
+
search,
|
|
34
|
+
width = "6xl",
|
|
35
|
+
renderGroup,
|
|
36
|
+
phoneFooter,
|
|
37
|
+
labels: labelsProp,
|
|
38
|
+
layout = "auto",
|
|
39
|
+
className
|
|
40
|
+
}) {
|
|
41
|
+
const labels = useKitLabels("settings", DEFAULT_SETTINGS_LABELS, labelsProp);
|
|
42
|
+
const base = normalizeSettingsBase(basePath);
|
|
43
|
+
const route = useSettingsRoute({ groups, entries, basePath: base, defaultGroup, aliases, layout });
|
|
44
|
+
const location = useLocation();
|
|
45
|
+
const root = useHref("/").replace(/\/$/, "");
|
|
46
|
+
const panelId = useId();
|
|
47
|
+
const searchRef = useRef(null);
|
|
48
|
+
const [query, setQuery] = useState("");
|
|
49
|
+
const visibleEntries = useMemo(() => visibleSettingsEntries(groups, entries), [groups, entries]);
|
|
50
|
+
const showSearch = search ?? visibleEntries.length > SETTINGS_SEARCH_THRESHOLD;
|
|
51
|
+
const trimmed = query.trim();
|
|
52
|
+
const searching = showSearch && trimmed !== "";
|
|
53
|
+
const groupById = useMemo(() => new Map(route.groups.map((g) => [g.id, g])), [route.groups]);
|
|
54
|
+
const index = useMemo(
|
|
55
|
+
() => createSearchIndex(
|
|
56
|
+
visibleEntries.map((e) => ({
|
|
57
|
+
id: e.id,
|
|
58
|
+
title: e.title,
|
|
59
|
+
keywords: [groupById.get(e.group)?.title ?? e.group, ...settingsKeywords(e.keywords)],
|
|
60
|
+
group: e.group,
|
|
61
|
+
anchor: e.anchor,
|
|
62
|
+
settingsGroup: e.group
|
|
63
|
+
})),
|
|
64
|
+
// Every hit: the list is the page's whole answer, not a palette's top few.
|
|
65
|
+
{ groupLimit: Infinity }
|
|
66
|
+
),
|
|
67
|
+
[visibleEntries, groupById]
|
|
68
|
+
);
|
|
69
|
+
const hits = useMemo(() => searching ? index.search(trimmed).map((h) => h.entry) : [], [searching, index, trimmed]);
|
|
70
|
+
const hitsByGroup = useMemo(() => {
|
|
71
|
+
const counts = /* @__PURE__ */ new Map();
|
|
72
|
+
for (const hit of hits) counts.set(hit.settingsGroup, (counts.get(hit.settingsGroup) ?? 0) + 1);
|
|
73
|
+
return counts;
|
|
74
|
+
}, [hits]);
|
|
75
|
+
const anchors = useMemo(() => new Set(entries.map((e) => e.anchor)), [entries]);
|
|
76
|
+
const { phone, group, sub, selectSub, select } = route;
|
|
77
|
+
const headingLevel = phone ? "h2" : "h3";
|
|
78
|
+
const context = useMemo(
|
|
79
|
+
() => ({
|
|
80
|
+
basePath: base,
|
|
81
|
+
group,
|
|
82
|
+
sub,
|
|
83
|
+
selectSub,
|
|
84
|
+
headingLevel,
|
|
85
|
+
hasAnchor: (anchor) => anchors.has(anchor)
|
|
86
|
+
}),
|
|
87
|
+
[base, group, sub, selectSub, headingLevel, anchors]
|
|
88
|
+
);
|
|
89
|
+
const hitsShown = searching && (!phone || group === null);
|
|
90
|
+
useSettingsFocus(route.redirecting || group === null || hitsShown ? null : route.focus);
|
|
91
|
+
const { state } = location;
|
|
92
|
+
const renderLink = useCallback(
|
|
93
|
+
({ href, ...props }) => /* @__PURE__ */ jsx(Link, { to: href, replace: !phone, state: phone ? settingsFromListState() : state, ...props }),
|
|
94
|
+
[phone, state]
|
|
95
|
+
);
|
|
96
|
+
const clearSearch = () => {
|
|
97
|
+
setQuery("");
|
|
98
|
+
searchRef.current?.focus();
|
|
99
|
+
};
|
|
100
|
+
const pageTitle = title ?? labels.title;
|
|
101
|
+
const pageClass = cn("mx-auto w-full space-y-4 p-4 md:p-6", width === "7xl" ? "max-w-7xl" : "max-w-6xl", className);
|
|
102
|
+
const searchField = showSearch ? /* @__PURE__ */ jsx(
|
|
103
|
+
SearchField,
|
|
104
|
+
{
|
|
105
|
+
ref: searchRef,
|
|
106
|
+
className: "max-w-md",
|
|
107
|
+
value: query,
|
|
108
|
+
onChange: setQuery,
|
|
109
|
+
"aria-label": labels.search,
|
|
110
|
+
placeholder: labels.searchPlaceholder
|
|
111
|
+
}
|
|
112
|
+
) : null;
|
|
113
|
+
const results = hits.length === 0 ? /* @__PURE__ */ jsx(
|
|
114
|
+
EmptyState,
|
|
115
|
+
{
|
|
116
|
+
title: labels.noMatches(trimmed),
|
|
117
|
+
action: /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "sm", onClick: clearSearch, children: labels.clearSearch })
|
|
118
|
+
}
|
|
119
|
+
) : /* @__PURE__ */ jsx(Card, { className: "p-1", children: /* @__PURE__ */ jsx(List, { separator: "divider", density: "comfortable", "aria-label": labels.results, children: hits.map((hit) => /* @__PURE__ */ jsx(
|
|
120
|
+
ListItem,
|
|
121
|
+
{
|
|
122
|
+
title: hit.title,
|
|
123
|
+
trailing: groupById.get(hit.settingsGroup)?.title,
|
|
124
|
+
href: settingsHref(base, hit.settingsGroup, hit.anchor),
|
|
125
|
+
renderLink,
|
|
126
|
+
onClick: () => setQuery("")
|
|
127
|
+
},
|
|
128
|
+
hit.id
|
|
129
|
+
)) }) });
|
|
130
|
+
const cards = (open) => /* @__PURE__ */ jsx(SettingsHeadingLevel, { level: headingLevel, children: /* @__PURE__ */ jsx("div", { className: "space-y-4", children: renderGroup(open, sub) }) });
|
|
131
|
+
let body;
|
|
132
|
+
if (phone) {
|
|
133
|
+
const open = group === null ? void 0 : groupById.get(group);
|
|
134
|
+
body = open ? /* @__PURE__ */ jsxs("div", { className: pageClass, "data-settings-view": "group", children: [
|
|
135
|
+
/* @__PURE__ */ jsx(
|
|
136
|
+
PageHeader,
|
|
137
|
+
{
|
|
138
|
+
size: "compact",
|
|
139
|
+
title: open.title,
|
|
140
|
+
description: open.help,
|
|
141
|
+
breadcrumbs: /* @__PURE__ */ jsxs(
|
|
142
|
+
Link,
|
|
143
|
+
{
|
|
144
|
+
to: base || "/",
|
|
145
|
+
"aria-label": labels.back,
|
|
146
|
+
onClick: (e) => {
|
|
147
|
+
if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|
148
|
+
e.preventDefault();
|
|
149
|
+
route.back();
|
|
150
|
+
},
|
|
151
|
+
className: "-ms-1 inline-flex items-center gap-0.5 self-start rounded-md pe-1 text-sm font-medium text-[var(--text-muted)] hover:text-[var(--text-primary)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)]",
|
|
152
|
+
children: [
|
|
153
|
+
/* @__PURE__ */ jsx(ChevronLeft, { "aria-hidden": true, className: "size-4 rtl:-scale-x-100" }),
|
|
154
|
+
pageTitle
|
|
155
|
+
]
|
|
156
|
+
}
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
),
|
|
160
|
+
cards(open.id)
|
|
161
|
+
] }) : /* @__PURE__ */ jsxs("div", { className: pageClass, "data-settings-view": "list", children: [
|
|
162
|
+
/* @__PURE__ */ jsx(PageHeader, { size: "compact", title: pageTitle }),
|
|
163
|
+
searchField,
|
|
164
|
+
searching ? results : /* @__PURE__ */ jsx(Card, { className: "p-1", children: /* @__PURE__ */ jsx(List, { separator: "divider", density: "comfortable", "aria-label": labels.sections, children: route.groups.map((g) => /* @__PURE__ */ jsx(
|
|
165
|
+
ListItem,
|
|
166
|
+
{
|
|
167
|
+
href: settingsHref(base, g.id),
|
|
168
|
+
renderLink,
|
|
169
|
+
leading: /* @__PURE__ */ jsx(
|
|
170
|
+
"span",
|
|
171
|
+
{
|
|
172
|
+
"aria-hidden": true,
|
|
173
|
+
className: "flex size-8 shrink-0 items-center justify-center rounded-md bg-[var(--bg-surface-2)] text-[var(--text-secondary)] [&_svg]:size-4",
|
|
174
|
+
children: g.icon
|
|
175
|
+
}
|
|
176
|
+
),
|
|
177
|
+
title: g.title,
|
|
178
|
+
subtitle: g.help,
|
|
179
|
+
trailing: /* @__PURE__ */ jsx(ChevronRight, { "aria-hidden": true, className: "size-4 rtl:-scale-x-100" })
|
|
180
|
+
},
|
|
181
|
+
g.id
|
|
182
|
+
)) }) }),
|
|
183
|
+
!searching && phoneFooter != null && /* @__PURE__ */ jsx(Card, { className: "p-1", children: /* @__PURE__ */ jsx(List, { separator: "divider", density: "comfortable", children: phoneFooter }) })
|
|
184
|
+
] });
|
|
185
|
+
} else {
|
|
186
|
+
const open = group === null ? void 0 : groupById.get(group);
|
|
187
|
+
body = /* @__PURE__ */ jsxs("div", { className: pageClass, "data-settings-view": "desktop", children: [
|
|
188
|
+
/* @__PURE__ */ jsx(PageHeader, { size: "compact", title: pageTitle }),
|
|
189
|
+
searchField,
|
|
190
|
+
/* @__PURE__ */ jsxs("div", { className: "md:grid md:grid-cols-[14rem_minmax(0,1fr)] md:gap-6", children: [
|
|
191
|
+
/* @__PURE__ */ jsx(
|
|
192
|
+
Tabs,
|
|
193
|
+
{
|
|
194
|
+
orientation: "vertical",
|
|
195
|
+
"aria-label": labels.sections,
|
|
196
|
+
className: "mb-4 md:sticky md:top-6 md:mb-0 md:self-start",
|
|
197
|
+
panelId: open && !searching ? panelId : void 0,
|
|
198
|
+
tabs: route.groups.map((g) => {
|
|
199
|
+
const count = hitsByGroup.get(g.id) ?? 0;
|
|
200
|
+
const dim = searching && count === 0;
|
|
201
|
+
return {
|
|
202
|
+
id: g.id,
|
|
203
|
+
href: `${root}${settingsHref(base, g.id)}`,
|
|
204
|
+
name: g.title,
|
|
205
|
+
label: /* @__PURE__ */ jsx("span", { className: cn(dim && "opacity-40"), children: g.title }),
|
|
206
|
+
icon: /* @__PURE__ */ jsx("span", { className: cn("inline-flex [&_svg]:size-4", dim && "opacity-40"), children: g.icon }),
|
|
207
|
+
badge: searching && count > 0 ? /* @__PURE__ */ jsxs(Chip, { size: "sm", tone: "brand", children: [
|
|
208
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": true, children: count }),
|
|
209
|
+
/* @__PURE__ */ jsx("span", { className: "sr-only-fixed", children: labels.matchCount(count) })
|
|
210
|
+
] }) : void 0
|
|
211
|
+
};
|
|
212
|
+
}),
|
|
213
|
+
active: open?.id ?? route.groups[0]?.id,
|
|
214
|
+
onChange: (id) => {
|
|
215
|
+
setQuery("");
|
|
216
|
+
select(id);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
),
|
|
220
|
+
/* @__PURE__ */ jsx("div", { className: "min-w-0 space-y-4", children: searching ? results : open ? /* @__PURE__ */ jsxs(
|
|
221
|
+
"div",
|
|
222
|
+
{
|
|
223
|
+
id: panelId,
|
|
224
|
+
role: "tabpanel",
|
|
225
|
+
"aria-labelledby": `${panelId}-tab`,
|
|
226
|
+
className: "space-y-4",
|
|
227
|
+
children: [
|
|
228
|
+
/* @__PURE__ */ jsx(PageHeader, { as: "h2", size: "sm", title: open.title, description: open.help }),
|
|
229
|
+
cards(open.id)
|
|
230
|
+
]
|
|
231
|
+
}
|
|
232
|
+
) : null })
|
|
233
|
+
] })
|
|
234
|
+
] });
|
|
235
|
+
}
|
|
236
|
+
return /* @__PURE__ */ jsx(SettingsLayoutContext.Provider, { value: context, children: body });
|
|
237
|
+
}
|
|
238
|
+
export {
|
|
239
|
+
SETTINGS_SEARCH_THRESHOLD,
|
|
240
|
+
SettingsLayout
|
|
241
|
+
};
|
|
242
|
+
//# sourceMappingURL=settings-layout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/settings/settings-layout.tsx"],"sourcesContent":["import { useCallback, useId, useMemo, useRef, useState } from \"react\";\nimport type { ReactElement, ReactNode } from \"react\";\nimport { Link, useHref, useLocation } from \"react-router\";\nimport { ChevronLeft, ChevronRight } from \"lucide-react\";\nimport { Button, Card, EmptyState, Tabs } from \"../components/ui\";\nimport { Chip } from \"../components/chip\";\nimport { List, ListItem } from \"../components/list\";\nimport type { ListItemLinkProps } from \"../components/list\";\nimport { PageHeader } from \"../components/page-header\";\nimport { SearchField } from \"../components/search-field\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { createSearchIndex } from \"../search/search-index\";\nimport type { SearchEntry } from \"../search/search-index\";\nimport {\n normalizeSettingsBase,\n settingsHref,\n settingsKeywords,\n visibleSettingsEntries,\n} from \"./settings-catalogue\";\nimport type { SettingsEntry, SettingsGroup } from \"./settings-catalogue\";\nimport { SettingsLayoutContext } from \"./settings-context\";\nimport type { SettingsLayoutContextValue } from \"./settings-context\";\nimport { SettingsHeadingLevel } from \"./settings-heading\";\nimport { DEFAULT_SETTINGS_LABELS } from \"./settings-labels\";\nimport type { SettingsLabels } from \"./settings-labels\";\nimport { settingsFromListState, useSettingsRoute } from \"./use-settings-route\";\nimport type { SettingsLayoutMode } from \"./use-settings-route\";\nimport { useSettingsFocus } from \"./use-settings-focus\";\n\n/** The page's width cap (§3.2): `6xl` for settings, `7xl` for keksdose's admin, whose\n * user table scrolled sideways at 6xl (dev#488). */\nexport type SettingsLayoutWidth = \"6xl\" | \"7xl\";\n\n/** More entries than this and the search field shows by itself (§3.4): Kurvenschmiede has\n * about twelve, keksdose 26; a page of six needs none. */\nexport const SETTINGS_SEARCH_THRESHOLD = 8;\n\nexport interface SettingsLayoutProps<G extends string> {\n /** The page's `h1`: \"Settings\", \"Administration\". Default: `labels.title`. */\n title?: ReactNode;\n /** The groups, in order (§4.2). */\n groups: readonly SettingsGroup<G>[];\n /** One entry per setting (§4.1). Hidden entries leave search; a group left without a\n * visible one is hidden. */\n entries: readonly SettingsEntry<G>[];\n /** `/settings` or `/admin`. The layout owns every path below it. */\n basePath: string;\n /** The group `/settings` opens on a desktop. Default: the first visible one. */\n defaultGroup?: G;\n /** Retired group ids and their successors, replaced in the address (§3.1). */\n aliases?: Readonly<Partial<Record<string, G>>>;\n /**\n * The search field. Left out, it shows once more than {@link SETTINGS_SEARCH_THRESHOLD}\n * entries are visible; `true` or `false` wins over the count — keksdose's admin keeps\n * none (§3.4, §10.3). ⌘K gets the entries either way ({@link settingsSearchEntries}).\n */\n search?: boolean;\n /** See {@link SettingsLayoutWidth}. Default `\"6xl\"`. */\n width?: SettingsLayoutWidth;\n /** The group's cards, in one column. `sub` is the path segment after the group, which\n * a card owns (`useSettingsLayout().selectSub`). */\n renderGroup: (group: G, sub: string | null) => ReactNode;\n /**\n * Rows under the phone's group list that LEAVE settings — kastlan's \"Administration\",\n * its import page (§3.3, §5). `ListItem`s with an `href`; the layout puts them in a list\n * of their own, so they never read as a group. Not shown on a desktop, where the app's\n * own navigation carries them.\n */\n phoneFooter?: ReactNode;\n /** Prop > `<UiKitProvider labels={{ settings }}>` > English. */\n labels?: Partial<SettingsLabels>;\n /** See {@link SettingsLayoutMode}. Default `\"auto\"`: the screen decides. */\n layout?: SettingsLayoutMode;\n className?: string;\n}\n\ninterface Searchable<G extends string> extends SearchEntry {\n anchor: string;\n settingsGroup: G;\n}\n\n/**\n * The settings page (docs/settings-harmonization.md §3), and the admin page built the\n * same way (§5). keksdose's page is the reference for the desktop; the phone pattern and\n * the path segments are new for all three apps.\n *\n * **Desktop** (§3.2): the page's `PageHeader`, the search field, then a 14rem sidebar —\n * the kit's vertical `Tabs`, a link per group, sticky so a long group keeps the list in\n * view — beside the open group: its heading (`h2`), its help line and its cards in ONE\n * column, never a masonry (keksdose dev#490/#493).\n *\n * **Phone** (§3.3, below `md`): a drill-down. `/settings` is a list of the groups (icon,\n * name, help line, chevron) with `phoneFooter` under it; a group is a page of its own with\n * a back link, its name as the `h1` and the cards under it. It replaces keksdose's chip\n * strip.\n *\n * **Routes, history and `?focus=`** are {@link useSettingsRoute}'s and\n * {@link useSettingsFocus}'s. **Search** (§3.4) runs the kit's matcher over the catalogue:\n * accents folded, ranked, over the title, the group's name and the keywords. On a desktop\n * the sidebar dims the groups without hits and counts the others' while the hits replace\n * the group; on a phone they replace the list. A hit is a link to its card.\n *\n * The cards inside read {@link useSettingsLayout} for `selectSub`, and their titles are\n * headings under the group's ({@link SettingsHeadingLevel}).\n */\nexport function SettingsLayout<G extends string>({\n title,\n groups,\n entries,\n basePath,\n defaultGroup,\n aliases,\n search,\n width = \"6xl\",\n renderGroup,\n phoneFooter,\n labels: labelsProp,\n layout = \"auto\",\n className,\n}: SettingsLayoutProps<G>): ReactElement {\n const labels = useKitLabels(\"settings\", DEFAULT_SETTINGS_LABELS, labelsProp);\n const base = normalizeSettingsBase(basePath);\n const route = useSettingsRoute({ groups, entries, basePath: base, defaultGroup, aliases, layout });\n const location = useLocation();\n // The browser URL of an app path, for the sidebar's real links (a middle click opens a\n // tab): `#/settings/…` under a hash router, the basename in front under a browser one.\n const root = useHref(\"/\").replace(/\\/$/, \"\");\n const panelId = useId();\n const searchRef = useRef<HTMLInputElement>(null);\n const [query, setQuery] = useState(\"\");\n\n const visibleEntries = useMemo(() => visibleSettingsEntries(groups, entries), [groups, entries]);\n const showSearch = search ?? visibleEntries.length > SETTINGS_SEARCH_THRESHOLD;\n const trimmed = query.trim();\n const searching = showSearch && trimmed !== \"\";\n\n const groupById = useMemo(() => new Map(route.groups.map((g) => [g.id, g])), [route.groups]);\n const index = useMemo(\n () =>\n createSearchIndex<Searchable<G>>(\n visibleEntries.map((e) => ({\n id: e.id,\n title: e.title,\n keywords: [groupById.get(e.group)?.title ?? e.group, ...settingsKeywords(e.keywords)],\n group: e.group,\n anchor: e.anchor,\n settingsGroup: e.group,\n })),\n // Every hit: the list is the page's whole answer, not a palette's top few.\n { groupLimit: Infinity },\n ),\n [visibleEntries, groupById],\n );\n const hits = useMemo(() => (searching ? index.search(trimmed).map((h) => h.entry) : []), [searching, index, trimmed]);\n const hitsByGroup = useMemo(() => {\n const counts = new Map<G, number>();\n for (const hit of hits) counts.set(hit.settingsGroup, (counts.get(hit.settingsGroup) ?? 0) + 1);\n return counts;\n }, [hits]);\n\n const anchors = useMemo(() => new Set(entries.map((e) => e.anchor)), [entries]);\n const { phone, group, sub, selectSub, select } = route;\n const headingLevel = phone ? \"h2\" : \"h3\";\n const context = useMemo<SettingsLayoutContextValue>(\n () => ({\n basePath: base,\n group,\n sub,\n selectSub,\n headingLevel,\n hasAnchor: (anchor) => anchors.has(anchor),\n }),\n [base, group, sub, selectSub, headingLevel, anchors],\n );\n\n // Not while the address is about to be replaced (its removal of `focus` would undo the\n // replace), not on the list, and not while the hits stand where the cards would be (a\n // phone's group page has no search: a query left from its list does not count there).\n const hitsShown = searching && (!phone || group === null);\n useSettingsFocus(route.redirecting || group === null || hitsShown ? null : route.focus);\n\n // A link inside the page. On a phone it opens a page of its own — a push that marks the\n // list as the entry before it, so the group page's back is a real back. On a desktop\n // it switches the group in place — a replace.\n const { state } = location;\n const renderLink = useCallback(\n ({ href, ...props }: ListItemLinkProps) => (\n <Link to={href} replace={!phone} state={phone ? settingsFromListState() : state} {...props} />\n ),\n [phone, state],\n );\n\n const clearSearch = () => {\n setQuery(\"\");\n // The button that was pressed goes with the empty state; the field is where the\n // next query is typed.\n searchRef.current?.focus();\n };\n\n const pageTitle = title ?? labels.title;\n const pageClass = cn(\"mx-auto w-full space-y-4 p-4 md:p-6\", width === \"7xl\" ? \"max-w-7xl\" : \"max-w-6xl\", className);\n\n const searchField = showSearch ? (\n <SearchField\n ref={searchRef}\n className=\"max-w-md\"\n value={query}\n onChange={setQuery}\n aria-label={labels.search}\n placeholder={labels.searchPlaceholder}\n />\n ) : null;\n\n const results =\n hits.length === 0 ? (\n <EmptyState\n title={labels.noMatches(trimmed)}\n action={\n <Button variant=\"secondary\" size=\"sm\" onClick={clearSearch}>\n {labels.clearSearch}\n </Button>\n }\n />\n ) : (\n <Card className=\"p-1\">\n <List separator=\"divider\" density=\"comfortable\" aria-label={labels.results}>\n {hits.map((hit) => (\n <ListItem\n key={hit.id}\n title={hit.title}\n trailing={groupById.get(hit.settingsGroup)?.title}\n href={settingsHref(base, hit.settingsGroup, hit.anchor)}\n renderLink={renderLink}\n // Leave search on the way: the hit's card is what the user asked for.\n onClick={() => setQuery(\"\")}\n />\n ))}\n </List>\n </Card>\n );\n\n const cards = (open: G) => (\n <SettingsHeadingLevel level={headingLevel}>\n <div className=\"space-y-4\">{renderGroup(open, sub)}</div>\n </SettingsHeadingLevel>\n );\n\n let body: ReactNode;\n if (phone) {\n const open = group === null ? undefined : groupById.get(group);\n body = open ? (\n <div className={pageClass} data-settings-view=\"group\">\n <PageHeader\n size=\"compact\"\n title={open.title}\n description={open.help}\n breadcrumbs={\n <Link\n to={base || \"/\"}\n aria-label={labels.back}\n onClick={(e) => {\n // A real link for a middle click; a plain click is the page's own back,\n // which is `navigate(-1)` when the list is the entry before this one.\n if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;\n e.preventDefault();\n route.back();\n }}\n className=\"-ms-1 inline-flex items-center gap-0.5 self-start rounded-md pe-1 text-sm font-medium text-[var(--text-muted)] hover:text-[var(--text-primary)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)]\"\n >\n <ChevronLeft aria-hidden className=\"size-4 rtl:-scale-x-100\" />\n {pageTitle}\n </Link>\n }\n />\n {cards(open.id)}\n </div>\n ) : (\n <div className={pageClass} data-settings-view=\"list\">\n <PageHeader size=\"compact\" title={pageTitle} />\n {searchField}\n {searching ? (\n results\n ) : (\n <Card className=\"p-1\">\n <List separator=\"divider\" density=\"comfortable\" aria-label={labels.sections}>\n {route.groups.map((g) => (\n <ListItem\n key={g.id}\n href={settingsHref(base, g.id)}\n renderLink={renderLink}\n leading={\n <span\n aria-hidden\n className=\"flex size-8 shrink-0 items-center justify-center rounded-md bg-[var(--bg-surface-2)] text-[var(--text-secondary)] [&_svg]:size-4\"\n >\n {g.icon}\n </span>\n }\n title={g.title}\n subtitle={g.help}\n trailing={<ChevronRight aria-hidden className=\"size-4 rtl:-scale-x-100\" />}\n />\n ))}\n </List>\n </Card>\n )}\n {!searching && phoneFooter != null && (\n <Card className=\"p-1\">\n <List separator=\"divider\" density=\"comfortable\">\n {phoneFooter}\n </List>\n </Card>\n )}\n </div>\n );\n } else {\n const open = group === null ? undefined : groupById.get(group);\n body = (\n <div className={pageClass} data-settings-view=\"desktop\">\n <PageHeader size=\"compact\" title={pageTitle} />\n {searchField}\n <div className=\"md:grid md:grid-cols-[14rem_minmax(0,1fr)] md:gap-6\">\n <Tabs<G>\n orientation=\"vertical\"\n aria-label={labels.sections}\n // Sticky (new in the harmonisation, §3.2): keksdose's notifications group\n // scrolls for screens, and the list should stay in view beside it.\n className=\"mb-4 md:sticky md:top-6 md:mb-0 md:self-start\"\n panelId={open && !searching ? panelId : undefined}\n tabs={route.groups.map((g) => {\n const count = hitsByGroup.get(g.id) ?? 0;\n // While searching, the groups with nothing to show step back (keksdose).\n const dim = searching && count === 0;\n return {\n id: g.id,\n href: `${root}${settingsHref(base, g.id)}`,\n name: g.title,\n label: <span className={cn(dim && \"opacity-40\")}>{g.title}</span>,\n icon: <span className={cn(\"inline-flex [&_svg]:size-4\", dim && \"opacity-40\")}>{g.icon}</span>,\n badge:\n searching && count > 0 ? (\n <Chip size=\"sm\" tone=\"brand\">\n <span aria-hidden>{count}</span>\n <span className=\"sr-only-fixed\">{labels.matchCount(count)}</span>\n </Chip>\n ) : undefined,\n };\n })}\n active={(open?.id ?? route.groups[0]?.id) as G}\n onChange={(id) => {\n // While searching the hits stand where the group would; a click on a group\n // means \"go there\", so the search makes way (keksdose).\n setQuery(\"\");\n select(id);\n }}\n />\n <div className=\"min-w-0 space-y-4\">\n {searching ? (\n results\n ) : open ? (\n <div\n id={panelId}\n role=\"tabpanel\"\n aria-labelledby={`${panelId}-tab`}\n className=\"space-y-4\"\n >\n <PageHeader as=\"h2\" size=\"sm\" title={open.title} description={open.help} />\n {cards(open.id)}\n </div>\n ) : null}\n </div>\n </div>\n </div>\n );\n }\n\n return <SettingsLayoutContext.Provider value={context}>{body}</SettingsLayoutContext.Provider>;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ComponentPropsWithoutRef, ReactNode } from 'react';
|
|
3
|
+
import { bZ as CardTone } from '../delete-account-setting-DCFS5Vof.js';
|
|
4
|
+
import '../components/tooltip.js';
|
|
5
|
+
import '../hooks/use-anchored-rect.js';
|
|
6
|
+
import '../lib/clipping.js';
|
|
7
|
+
import '../data-table-labels-DUELeXpa.js';
|
|
8
|
+
import '../components/data-table-sort.js';
|
|
9
|
+
import '../components/swipeable-row.js';
|
|
10
|
+
import '../components/mini-calendar.js';
|
|
11
|
+
import '../components/calendar-heatmap.js';
|
|
12
|
+
import '../components/popover.js';
|
|
13
|
+
import '../components/chip.js';
|
|
14
|
+
import 'lucide-react';
|
|
15
|
+
import '../components/field-sync.js';
|
|
16
|
+
import '../wizard/types.js';
|
|
17
|
+
import '../tour/tour.js';
|
|
18
|
+
import '../search/command-palette.js';
|
|
19
|
+
import '../search/global-search.js';
|
|
20
|
+
import '../search/search-index.js';
|
|
21
|
+
import '../components/month-picker.js';
|
|
22
|
+
import '../components/page-contents.js';
|
|
23
|
+
import '../components/series-chart-labels.js';
|
|
24
|
+
import '../components/pie-chart-labels.js';
|
|
25
|
+
import '../components/sparkline.js';
|
|
26
|
+
import '../components/stat-tile.js';
|
|
27
|
+
import '../components/signature-pad.js';
|
|
28
|
+
import '../components/password-strength.js';
|
|
29
|
+
import '../components/swatch-picker.js';
|
|
30
|
+
import '../components/tile-radio.js';
|
|
31
|
+
import '../components/icon-picker.js';
|
|
32
|
+
import '../components/dialog-frame.js';
|
|
33
|
+
import '../components/modal.js';
|
|
34
|
+
import '../hooks/use-search-param-state.js';
|
|
35
|
+
import '../components/measured-grid.js';
|
|
36
|
+
import '../components/user-avatar.js';
|
|
37
|
+
import '../lib/person-name.js';
|
|
38
|
+
import '../components/status-dot.js';
|
|
39
|
+
import '../components/progress-bar.js';
|
|
40
|
+
import '../components/signed-amount.js';
|
|
41
|
+
import '../shell/topbar-action-menu.js';
|
|
42
|
+
import '../components/menu-item.js';
|
|
43
|
+
import '../components/floating-panel.js';
|
|
44
|
+
import '../hooks/use-authed-src.js';
|
|
45
|
+
import '../components/account-settings-labels.js';
|
|
46
|
+
import '../components/bulk-action-bar.js';
|
|
47
|
+
import '../components/list.js';
|
|
48
|
+
import '../components/breadcrumbs.js';
|
|
49
|
+
import '../components/toast.js';
|
|
50
|
+
import '../components/description-list.js';
|
|
51
|
+
import '../components/line-items.js';
|
|
52
|
+
import '../components/error-boundary.js';
|
|
53
|
+
import '../components/authed-image.js';
|
|
54
|
+
import '../components/image-grid.js';
|
|
55
|
+
import '../components/lightbox.js';
|
|
56
|
+
import '../components/write-lock.js';
|
|
57
|
+
import '../auth/sign-in-form.js';
|
|
58
|
+
import '../landing/access.js';
|
|
59
|
+
import '../landing/landing-labels.js';
|
|
60
|
+
import '../shell/topbar-controls.js';
|
|
61
|
+
import '../theme/palette-presets.js';
|
|
62
|
+
import '../theme/chart-palette.js';
|
|
63
|
+
import '../theme/theme-store.js';
|
|
64
|
+
import 'zustand/middleware';
|
|
65
|
+
import 'zustand';
|
|
66
|
+
import '../auth/complete-name-dialog.js';
|
|
67
|
+
import '../auth/forgot-password-form.js';
|
|
68
|
+
import '../auth/status-parts.js';
|
|
69
|
+
import '../auth/reset-password-form.js';
|
|
70
|
+
import '../auth/verify-email.js';
|
|
71
|
+
import '../auth/not-found-page.js';
|
|
72
|
+
import '../auth/accept-invitation.js';
|
|
73
|
+
import '../shell/company-switcher.js';
|
|
74
|
+
import '../account/email-change-setting.js';
|
|
75
|
+
import '../account/sessions-setting.js';
|
|
76
|
+
import '../lib/format.js';
|
|
77
|
+
import '../account/data-export-setting.js';
|
|
78
|
+
import '../components/account-chips.js';
|
|
79
|
+
import '../admin/admin-parts.js';
|
|
80
|
+
import '../admin/admin-action-log.js';
|
|
81
|
+
import './settings-labels.js';
|
|
82
|
+
import '../demo/demo-labels.js';
|
|
83
|
+
import '../components/reauth-dialog.js';
|
|
84
|
+
import '../components/server-wake.js';
|
|
85
|
+
import '../lib/server-wake.js';
|
|
86
|
+
import '../components/translation-review-labels.js';
|
|
87
|
+
import '../lib/translation-review.js';
|
|
88
|
+
import '../components/country-select.js';
|
|
89
|
+
import '../components/inline-edit-field.js';
|
|
90
|
+
import '../lib/iban.js';
|
|
91
|
+
import '../lib/phone.js';
|
|
92
|
+
import '../components/sign-chip.js';
|
|
93
|
+
import '../components/column-mapper.js';
|
|
94
|
+
import '../lib/column-mapping.js';
|
|
95
|
+
import '../lib/table-text.js';
|
|
96
|
+
import '../components/field-parts.js';
|
|
97
|
+
|
|
98
|
+
interface SettingsSectionProps extends Omit<ComponentPropsWithoutRef<"div">, "title" | "id"> {
|
|
99
|
+
/** The card's DOM id — the catalogue entry's `anchor`, the `?focus=` target. */
|
|
100
|
+
anchor: string;
|
|
101
|
+
/** The card's name: a heading at the layout's level (`h3` by default). */
|
|
102
|
+
title: ReactNode;
|
|
103
|
+
/**
|
|
104
|
+
* `false` keeps the title for screen readers only — for a card alone in a group named
|
|
105
|
+
* after it, so the page does not print the same heading twice (§3.7, keksdose's admin,
|
|
106
|
+
* dev#490/#493/#494). The heading is still there: the outline and `?focus=` need it.
|
|
107
|
+
* Default `true`.
|
|
108
|
+
*/
|
|
109
|
+
titleVisible?: boolean;
|
|
110
|
+
/** One muted line under the title. */
|
|
111
|
+
description?: ReactNode;
|
|
112
|
+
/** `danger` for the destructive cards at the end of "data" (§3.7). */
|
|
113
|
+
tone?: CardTone;
|
|
114
|
+
/** A control at the title's end — a refresh, a toggle. It drops under the title when
|
|
115
|
+
* the card is too narrow for both. */
|
|
116
|
+
action?: ReactNode;
|
|
117
|
+
children?: ReactNode;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* An app's own settings card (docs/settings-harmonization.md §3.7, §7.1): a kit `Card` at
|
|
121
|
+
* the settings type scale — the title `text-sm font-medium`, the description `text-xs`
|
|
122
|
+
* muted, as the kit's own setting cards — with the catalogue anchor as its id, the title
|
|
123
|
+
* as a heading under the group's, and the `?focus=` ring landing on it.
|
|
124
|
+
*
|
|
125
|
+
* **A card without a catalogue entry throws in development** (keksdose's `section()`
|
|
126
|
+
* guard). A card search cannot find breaks the rule that every setting is findable
|
|
127
|
+
* (§4.1), and the old failure was silent: a hit that scrolled nowhere. The check runs
|
|
128
|
+
* inside a {@link SettingsLayout}, which holds the catalogue; outside one there is
|
|
129
|
+
* nothing to check against.
|
|
130
|
+
*/
|
|
131
|
+
declare function SettingsSection({ anchor, title, titleVisible, description, tone, action, children, className, ...rest }: SettingsSectionProps): react.JSX.Element;
|
|
132
|
+
|
|
133
|
+
export { SettingsSection, type SettingsSectionProps };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Card } from "../components/ui.js";
|
|
4
|
+
import { cn } from "../lib/cn.js";
|
|
5
|
+
import { CARD_DESCRIPTION_CLASS, CARD_TITLE_CLASS } from "../account/account-parts.js";
|
|
6
|
+
import { SettingsHeading, useSettingsHeadingLevel } from "./settings-heading.js";
|
|
7
|
+
import { useSettingsLayout } from "./settings-context.js";
|
|
8
|
+
function devChecks() {
|
|
9
|
+
return Boolean(import.meta.env?.DEV);
|
|
10
|
+
}
|
|
11
|
+
function SettingsSection({
|
|
12
|
+
anchor,
|
|
13
|
+
title,
|
|
14
|
+
titleVisible = true,
|
|
15
|
+
description,
|
|
16
|
+
tone,
|
|
17
|
+
action,
|
|
18
|
+
children,
|
|
19
|
+
className,
|
|
20
|
+
...rest
|
|
21
|
+
}) {
|
|
22
|
+
const layout = useSettingsLayout();
|
|
23
|
+
const level = useSettingsHeadingLevel() ?? "h3";
|
|
24
|
+
if (layout && devChecks() && !layout.hasAnchor(anchor)) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`SettingsSection "${anchor}" has no catalogue entry: add a SettingsEntry with anchor "${anchor}" so search and ?focus= can reach it.`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
const heading = /* @__PURE__ */ jsx(
|
|
30
|
+
SettingsHeading,
|
|
31
|
+
{
|
|
32
|
+
as: level,
|
|
33
|
+
tabIndex: -1,
|
|
34
|
+
"data-settings-heading": "",
|
|
35
|
+
className: cn(CARD_TITLE_CLASS, "outline-none", !titleVisible && "sr-only-fixed"),
|
|
36
|
+
children: title
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
const header = titleVisible || description != null || action != null ? /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-start justify-between gap-x-3 gap-y-2", children: [
|
|
40
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1 basis-48", children: [
|
|
41
|
+
heading,
|
|
42
|
+
description != null && /* @__PURE__ */ jsx("div", { className: CARD_DESCRIPTION_CLASS, children: description })
|
|
43
|
+
] }),
|
|
44
|
+
action != null && /* @__PURE__ */ jsx("div", { className: "flex shrink-0 items-center gap-2", children: action })
|
|
45
|
+
] }) : heading;
|
|
46
|
+
return /* @__PURE__ */ jsxs(
|
|
47
|
+
Card,
|
|
48
|
+
{
|
|
49
|
+
...rest,
|
|
50
|
+
id: anchor,
|
|
51
|
+
tone,
|
|
52
|
+
className: cn("flex flex-col gap-3 p-4 transition-shadow motion-reduce:transition-none", className),
|
|
53
|
+
children: [
|
|
54
|
+
header,
|
|
55
|
+
children
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
export {
|
|
61
|
+
SettingsSection
|
|
62
|
+
};
|
|
63
|
+
//# sourceMappingURL=settings-section.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/settings/settings-section.tsx"],"sourcesContent":["import type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { Card } from \"../components/ui\";\nimport type { CardTone } from \"../components/ui\";\nimport { cn } from \"../lib/cn\";\nimport { CARD_DESCRIPTION_CLASS, CARD_TITLE_CLASS } from \"../account/account-parts\";\nimport { SettingsHeading, useSettingsHeadingLevel } from \"./settings-heading\";\nimport { useSettingsLayout } from \"./settings-context\";\n\nexport interface SettingsSectionProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"title\" | \"id\"> {\n /** The card's DOM id — the catalogue entry's `anchor`, the `?focus=` target. */\n anchor: string;\n /** The card's name: a heading at the layout's level (`h3` by default). */\n title: ReactNode;\n /**\n * `false` keeps the title for screen readers only — for a card alone in a group named\n * after it, so the page does not print the same heading twice (§3.7, keksdose's admin,\n * dev#490/#493/#494). The heading is still there: the outline and `?focus=` need it.\n * Default `true`.\n */\n titleVisible?: boolean;\n /** One muted line under the title. */\n description?: ReactNode;\n /** `danger` for the destructive cards at the end of \"data\" (§3.7). */\n tone?: CardTone;\n /** A control at the title's end — a refresh, a toggle. It drops under the title when\n * the card is too narrow for both. */\n action?: ReactNode;\n children?: ReactNode;\n}\n\n/**\n * Is this a development build? The consumer's bundler makes `import.meta.env.DEV` false in\n * production, so the check below is dead code there. Unlike the kit's warnings it stays\n * on under vitest: a card that cannot be found is a bug a test should fail on.\n * Optional-chained: `import.meta.env` is a Vite injection, absent under plain Node.\n */\nfunction devChecks(): boolean {\n return Boolean(import.meta.env?.DEV);\n}\n\n/**\n * An app's own settings card (docs/settings-harmonization.md §3.7, §7.1): a kit `Card` at\n * the settings type scale — the title `text-sm font-medium`, the description `text-xs`\n * muted, as the kit's own setting cards — with the catalogue anchor as its id, the title\n * as a heading under the group's, and the `?focus=` ring landing on it.\n *\n * **A card without a catalogue entry throws in development** (keksdose's `section()`\n * guard). A card search cannot find breaks the rule that every setting is findable\n * (§4.1), and the old failure was silent: a hit that scrolled nowhere. The check runs\n * inside a {@link SettingsLayout}, which holds the catalogue; outside one there is\n * nothing to check against.\n */\nexport function SettingsSection({\n anchor,\n title,\n titleVisible = true,\n description,\n tone,\n action,\n children,\n className,\n ...rest\n}: SettingsSectionProps) {\n const layout = useSettingsLayout();\n const level = useSettingsHeadingLevel() ?? \"h3\";\n if (layout && devChecks() && !layout.hasAnchor(anchor)) {\n throw new Error(\n `SettingsSection \"${anchor}\" has no catalogue entry: add a SettingsEntry with anchor \"${anchor}\" so search and ?focus= can reach it.`,\n );\n }\n const heading = (\n <SettingsHeading\n as={level}\n // Focusable from script only: `?focus=` moves focus here (§3.5).\n tabIndex={-1}\n data-settings-heading=\"\"\n className={cn(CARD_TITLE_CLASS, \"outline-none\", !titleVisible && \"sr-only-fixed\")}\n >\n {title}\n </SettingsHeading>\n );\n const header =\n titleVisible || description != null || action != null ? (\n <div className=\"flex flex-wrap items-start justify-between gap-x-3 gap-y-2\">\n <div className=\"min-w-0 flex-1 basis-48\">\n {heading}\n {description != null && <div className={CARD_DESCRIPTION_CLASS}>{description}</div>}\n </div>\n {action != null && <div className=\"flex shrink-0 items-center gap-2\">{action}</div>}\n </div>\n ) : (\n heading\n );\n return (\n <Card\n {...rest}\n id={anchor}\n tone={tone}\n // A column with a gap, not `space-y-3`: a hidden title is `position: fixed` and so\n // out of the flow, and a gap is only drawn between items in it — `space-y` would\n // still push the first card part down by a gap for the title nobody sees.\n className={cn(\"flex flex-col gap-3 p-4 transition-shadow motion-reduce:transition-none\", className)}\n >\n {header}\n {children}\n </Card>\n );\n}\n"],"mappings":";AAuEI,cAaI,YAbJ;AAtEJ,SAAS,YAAY;AAErB,SAAS,UAAU;AACnB,SAAS,wBAAwB,wBAAwB;AACzD,SAAS,iBAAiB,+BAA+B;AACzD,SAAS,yBAAyB;AA8BlC,SAAS,YAAqB;AAC5B,SAAO,QAAQ,YAAY,KAAK,GAAG;AACrC;AAcO,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA,eAAe;AAAA,EACf;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAyB;AACvB,QAAM,SAAS,kBAAkB;AACjC,QAAM,QAAQ,wBAAwB,KAAK;AAC3C,MAAI,UAAU,UAAU,KAAK,CAAC,OAAO,UAAU,MAAM,GAAG;AACtD,UAAM,IAAI;AAAA,MACR,oBAAoB,MAAM,8DAA8D,MAAM;AAAA,IAChG;AAAA,EACF;AACA,QAAM,UACJ;AAAA,IAAC;AAAA;AAAA,MACC,IAAI;AAAA,MAEJ,UAAU;AAAA,MACV,yBAAsB;AAAA,MACtB,WAAW,GAAG,kBAAkB,gBAAgB,CAAC,gBAAgB,eAAe;AAAA,MAE/E;AAAA;AAAA,EACH;AAEF,QAAM,SACJ,gBAAgB,eAAe,QAAQ,UAAU,OAC/C,qBAAC,SAAI,WAAU,8DACb;AAAA,yBAAC,SAAI,WAAU,2BACZ;AAAA;AAAA,MACA,eAAe,QAAQ,oBAAC,SAAI,WAAW,wBAAyB,uBAAY;AAAA,OAC/E;AAAA,IACC,UAAU,QAAQ,oBAAC,SAAI,WAAU,oCAAoC,kBAAO;AAAA,KAC/E,IAEA;AAEJ,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,IAAI;AAAA,MACJ;AAAA,MAIA,WAAW,GAAG,2EAA2E,SAAS;AAAA,MAEjG;AAAA;AAAA,QACA;AAAA;AAAA;AAAA,EACH;AAEJ;","names":[]}
|