@eifi1/ui-kit 0.51.2 → 0.51.4
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 +5 -5
- package/dist/components/combobox-core.js +4 -5
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/currency-option-list.js +3 -3
- package/dist/components/currency-option-list.js.map +1 -1
- package/dist/components/data-table-fit.js +7 -5
- package/dist/components/data-table-fit.js.map +1 -1
- package/dist/components/data-table-head.js +5 -1
- package/dist/components/data-table-head.js.map +1 -1
- package/dist/components/data-table.d.ts +8 -0
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/grouped-picker.js +5 -4
- package/dist/components/grouped-picker.js.map +1 -1
- package/dist/components/icon-picker.js +5 -4
- package/dist/components/icon-picker.js.map +1 -1
- package/dist/components/inline-entity-combobox.js +4 -4
- package/dist/components/inline-entity-combobox.js.map +1 -1
- package/dist/components/legal-labels.js +9 -2
- package/dist/components/legal-labels.js.map +1 -1
- package/dist/components/legal-page.d.ts +35 -3
- package/dist/components/legal-page.js +22 -0
- package/dist/components/legal-page.js.map +1 -1
- package/dist/components/legal.d.ts +49 -5
- package/dist/components/legal.js +31 -3
- package/dist/components/legal.js.map +1 -1
- package/dist/components/list-labels.js +3 -1
- package/dist/components/list-labels.js.map +1 -1
- package/dist/components/list.d.ts +18 -1
- package/dist/components/list.js +32 -3
- package/dist/components/list.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/feedback/feedback-table.d.ts +3 -1
- package/dist/feedback/feedback-table.js +18 -4
- package/dist/feedback/feedback-table.js.map +1 -1
- package/dist/i18n/german.js +12 -3
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/locales/en.js +16 -3
- package/dist/i18n/locales/en.js.map +2 -2
- package/dist/i18n/locales/es.js +12 -3
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +12 -3
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +12 -3
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +12 -3
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +12 -3
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.js +4 -0
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/text-match.d.ts +16 -0
- package/dist/lib/text-match.js +11 -1
- package/dist/lib/text-match.js.map +1 -1
- package/dist/settings/use-settings-focus.d.ts +7 -0
- package/dist/settings/use-settings-focus.js +2 -0
- package/dist/settings/use-settings-focus.js.map +1 -1
- package/dist/shell/dev-menu.d.ts +5 -0
- package/dist/shell/dev-menu.js +2 -1
- package/dist/shell/dev-menu.js.map +1 -1
- package/dist/shell/topbar-action-menu.d.ts +14 -1
- package/dist/shell/topbar-action-menu.js +85 -34
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell.d.ts +1 -1
- package/dist/shell.js +2 -1
- package/dist/shell.js.map +1 -1
- package/package.json +1 -1
- package/src/components/combobox-core.tsx +5 -5
- package/src/components/currency-option-list.tsx +4 -5
- package/src/components/data-table-fit.ts +7 -5
- package/src/components/data-table-head.tsx +5 -1
- package/src/components/data-table.tsx +8 -0
- package/src/components/grouped-picker.tsx +15 -8
- package/src/components/icon-picker.tsx +6 -4
- package/src/components/inline-entity-combobox.tsx +5 -4
- package/src/components/legal-labels.ts +8 -1
- package/src/components/legal-page.tsx +72 -2
- package/src/components/legal.tsx +80 -5
- package/src/components/list-labels.ts +2 -0
- package/src/components/list.tsx +52 -2
- package/src/feedback/feedback-table.tsx +17 -1
- package/src/i18n/german.ts +10 -1
- package/src/i18n/locales/en.ts +4 -0
- package/src/i18n/locales/es.ts +10 -1
- package/src/i18n/locales/fr.ts +10 -1
- package/src/i18n/locales/hu.ts +10 -1
- package/src/i18n/locales/it.ts +10 -1
- package/src/i18n/locales/zh.ts +10 -1
- package/src/i18n/review.ts +4 -0
- package/src/index.ts +1 -0
- package/src/lib/text-match.ts +30 -0
- package/src/settings/use-settings-focus.ts +8 -0
- package/src/shell/dev-menu.tsx +6 -0
- package/src/shell/topbar-action-menu.tsx +122 -38
- package/src/shell.ts +1 -1
package/dist/shell.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/shell.ts"],"sourcesContent":["// `@eifi1/ui-kit/shell` — the composable app chrome.\n//\n// A re-slicing of the main barrel, not a new API. This entry point requires\n// `react-router`: AppShell renders NavLink and TopBarActionMenu renders Link.\nexport * from \"./shell/topbar-controls\";\nexport * from \"./shell/top-bar\";\nexport * from \"./shell/app-shell\";\nexport * from \"./shell/option-switcher-menu\";\nexport * from \"./shell/role-switcher\";\nexport * from \"./shell/topbar-action-menu\";\nexport * from \"./shell/top-bar-brand\";\nexport * from \"./shell/auth-layout\";\n// 0.40.0: the list page's fill (docs/page-layout-harmonization.md §1.3).\nexport { FillHeight, FILL_HEIGHT_CLASS } from \"./shell/fill-height\";\nexport type { FillHeightProps } from \"./shell/fill-height\";\n// 0.42.0: the account menu's \"Administration\" row, as an entry and as a HoverMenu row\n// (docs/admin-harmonization.md §2.1, §6.6).\nexport { adminMenuEntry, AdminMenuRow } from \"./shell/admin-menu\";\nexport type { AdminMenuEntryOptions, AdminMenuRowProps } from \"./shell/admin-menu\";\n// 0.43.0 (kastlan): the entry with the provider's words.\nexport { useAdminMenuEntry } from \"./shell/admin-menu\";\nexport type { UseAdminMenuEntryOptions } from \"./shell/admin-menu\";\n// 0.45.0 (docs/admin-harmonization.md §9.3.4): the row's waiting pill on its own, for\n// IconButton's `badge` (kastlan's platform button); its name is `adminArea.waitingLabel`.\nexport { AdminWaitingBadge } from \"./shell/admin-menu\";\nexport type { AdminWaitingBadgeProps } from \"./shell/admin-menu\";\n// 0.31.0: the settings and admin pages' shell (docs/settings-harmonization.md §3, §5).\nexport { SettingsLayout, SETTINGS_SEARCH_THRESHOLD } from \"./settings/settings-layout\";\nexport type { SettingsLayoutProps, SettingsLayoutWidth, SettingsDocumentTitleParts } from \"./settings/settings-layout\";\nexport {\n useSettingsRoute,\n resolveSettingsLocation,\n settingsFromListState,\n SETTINGS_FROM_LIST_STATE,\n} from \"./settings/use-settings-route\";\nexport type {\n UseSettingsRouteOptions,\n SettingsRoute,\n SettingsLayoutMode,\n SettingsLocation,\n SettingsLocationRules,\n SettingsLocationResult,\n} from \"./settings/use-settings-route\";\nexport { useSettingsFocus, SETTINGS_FOCUS_RING, SETTINGS_FOCUS_MS } from \"./settings/use-settings-focus\";\nexport { SettingsSection } from \"./settings/settings-section\";\nexport type { SettingsSectionProps } from \"./settings/settings-section\";\nexport { useSettingsLayout } from \"./settings/settings-context\";\nexport type { SettingsLayoutContextValue } from \"./settings/settings-context\";\nexport { SettingsHeadingLevel, useSettingsHeadingLevel } from \"./settings/settings-heading\";\nexport type { SettingsHeadingLevelProps, SettingsHeadingTag } from \"./settings/settings-heading\";\nexport {\n settingsSearchEntries,\n visibleSettingsGroups,\n visibleSettingsEntries,\n settingsHref,\n} from \"./settings/settings-catalogue\";\nexport type { SettingsGroup, SettingsEntry, SettingsSearchEntriesOptions } from \"./settings/settings-catalogue\";\n// 0.31.0: the public pages' header and the resume (docs/landing-demo-harmonization.md §3, §4.1).\nexport {\n RootEntry,\n RedirectIfAuthed,\n useLastVisitedPage,\n readLastVisitedPage,\n clearLastVisitedPage,\n safeNextPath,\n NEXT_PARAM,\n DEFAULT_LAST_VISITED_EXCLUDES,\n} from \"./landing/routing\";\nexport type {\n RootEntryProps,\n RedirectIfAuthedProps,\n RoutingSession,\n LastVisitedPageOptions,\n PathPattern,\n} from \"./landing/routing\";\nexport { PublicHeader } from \"./landing/public-header\";\nexport type { PublicHeaderProps, PublicHeaderBrand } from \"./landing/public-header\";\n// 0.42.0: the top bar's shared parts (docs/top-bar-harmonization.md §3.1) — the slots in\n// §2.1's order, the account menu's language sub-list, the tours menu, the sync status, the\n// local-dev switchers, and their words.\nexport { TopBarActions, TOP_BAR_ACTION_ORDER } from \"./shell/top-bar-actions\";\nexport type { TopBarActionsProps } from \"./shell/top-bar-actions\";\nexport { AccountMenuLanguage } from \"./shell/account-menu-language\";\nexport type { AccountMenuLanguageProps, AccountMenuLanguageOption } from \"./shell/account-menu-language\";\n// 0.46.0 (keksdose live #426): A− / A+ for the text size, beside the language.\nexport { AccountMenuTextSize } from \"./shell/account-menu-text-size\";\nexport type { AccountMenuTextSizeProps } from \"./shell/account-menu-text-size\";\nexport { TourMenu } from \"./shell/tour-menu\";\nexport type { TourMenuProps, TourMenuTour } from \"./shell/tour-menu\";\nexport { SyncStatusIndicator, syncState } from \"./shell/sync-status-indicator\";\nexport type { SyncStatus, SyncState, SyncStatusScope, SyncStatusIndicatorProps } from \"./shell/sync-status-indicator\";\n// 0.51 (§2.7.1, keksdose): one flask, the kit's Account section and the app's own dev\n// sections; `DevSwitchers` is a deprecated wrapper over it until 0.52.\nexport { DevMenu, DEV_ORIGINAL_ROLE_KEY } from \"./shell/dev-menu\";\nexport type { DevMenuProps, DevMenuSection } from \"./shell/dev-menu\";\nexport { DevSwitchers } from \"./shell/dev-switchers\";\nexport type { DevSwitchersProps } from \"./shell/dev-switchers\";\nexport { DEFAULT_TOP_BAR_PARTS_LABELS, useTopBarPartsLabels } from \"./shell/top-bar-parts-labels\";\nexport type {\n TopBarPartsLabels,\n TourMenuLabels,\n SyncStatusLabels,\n DevSwitchersLabels,\n} from \"./shell/top-bar-parts-labels\";\n"],"mappings":";AAIA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAEd,SAAS,YAAY,yBAAyB;AAI9C,SAAS,gBAAgB,oBAAoB;AAG7C,SAAS,yBAAyB;AAIlC,SAAS,yBAAyB;AAGlC,SAAS,gBAAgB,iCAAiC;AAE1D;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AASP,SAAS,kBAAkB,qBAAqB,yBAAyB;
|
|
1
|
+
{"version":3,"sources":["../src/shell.ts"],"sourcesContent":["// `@eifi1/ui-kit/shell` — the composable app chrome.\n//\n// A re-slicing of the main barrel, not a new API. This entry point requires\n// `react-router`: AppShell renders NavLink and TopBarActionMenu renders Link.\nexport * from \"./shell/topbar-controls\";\nexport * from \"./shell/top-bar\";\nexport * from \"./shell/app-shell\";\nexport * from \"./shell/option-switcher-menu\";\nexport * from \"./shell/role-switcher\";\nexport * from \"./shell/topbar-action-menu\";\nexport * from \"./shell/top-bar-brand\";\nexport * from \"./shell/auth-layout\";\n// 0.40.0: the list page's fill (docs/page-layout-harmonization.md §1.3).\nexport { FillHeight, FILL_HEIGHT_CLASS } from \"./shell/fill-height\";\nexport type { FillHeightProps } from \"./shell/fill-height\";\n// 0.42.0: the account menu's \"Administration\" row, as an entry and as a HoverMenu row\n// (docs/admin-harmonization.md §2.1, §6.6).\nexport { adminMenuEntry, AdminMenuRow } from \"./shell/admin-menu\";\nexport type { AdminMenuEntryOptions, AdminMenuRowProps } from \"./shell/admin-menu\";\n// 0.43.0 (kastlan): the entry with the provider's words.\nexport { useAdminMenuEntry } from \"./shell/admin-menu\";\nexport type { UseAdminMenuEntryOptions } from \"./shell/admin-menu\";\n// 0.45.0 (docs/admin-harmonization.md §9.3.4): the row's waiting pill on its own, for\n// IconButton's `badge` (kastlan's platform button); its name is `adminArea.waitingLabel`.\nexport { AdminWaitingBadge } from \"./shell/admin-menu\";\nexport type { AdminWaitingBadgeProps } from \"./shell/admin-menu\";\n// 0.31.0: the settings and admin pages' shell (docs/settings-harmonization.md §3, §5).\nexport { SettingsLayout, SETTINGS_SEARCH_THRESHOLD } from \"./settings/settings-layout\";\nexport type { SettingsLayoutProps, SettingsLayoutWidth, SettingsDocumentTitleParts } from \"./settings/settings-layout\";\nexport {\n useSettingsRoute,\n resolveSettingsLocation,\n settingsFromListState,\n SETTINGS_FROM_LIST_STATE,\n} from \"./settings/use-settings-route\";\nexport type {\n UseSettingsRouteOptions,\n SettingsRoute,\n SettingsLayoutMode,\n SettingsLocation,\n SettingsLocationRules,\n SettingsLocationResult,\n} from \"./settings/use-settings-route\";\nexport { useSettingsFocus, useFocusParam, SETTINGS_FOCUS_RING, SETTINGS_FOCUS_MS } from \"./settings/use-settings-focus\";\nexport { SettingsSection } from \"./settings/settings-section\";\nexport type { SettingsSectionProps } from \"./settings/settings-section\";\nexport { useSettingsLayout } from \"./settings/settings-context\";\nexport type { SettingsLayoutContextValue } from \"./settings/settings-context\";\nexport { SettingsHeadingLevel, useSettingsHeadingLevel } from \"./settings/settings-heading\";\nexport type { SettingsHeadingLevelProps, SettingsHeadingTag } from \"./settings/settings-heading\";\nexport {\n settingsSearchEntries,\n visibleSettingsGroups,\n visibleSettingsEntries,\n settingsHref,\n} from \"./settings/settings-catalogue\";\nexport type { SettingsGroup, SettingsEntry, SettingsSearchEntriesOptions } from \"./settings/settings-catalogue\";\n// 0.31.0: the public pages' header and the resume (docs/landing-demo-harmonization.md §3, §4.1).\nexport {\n RootEntry,\n RedirectIfAuthed,\n useLastVisitedPage,\n readLastVisitedPage,\n clearLastVisitedPage,\n safeNextPath,\n NEXT_PARAM,\n DEFAULT_LAST_VISITED_EXCLUDES,\n} from \"./landing/routing\";\nexport type {\n RootEntryProps,\n RedirectIfAuthedProps,\n RoutingSession,\n LastVisitedPageOptions,\n PathPattern,\n} from \"./landing/routing\";\nexport { PublicHeader } from \"./landing/public-header\";\nexport type { PublicHeaderProps, PublicHeaderBrand } from \"./landing/public-header\";\n// 0.42.0: the top bar's shared parts (docs/top-bar-harmonization.md §3.1) — the slots in\n// §2.1's order, the account menu's language sub-list, the tours menu, the sync status, the\n// local-dev switchers, and their words.\nexport { TopBarActions, TOP_BAR_ACTION_ORDER } from \"./shell/top-bar-actions\";\nexport type { TopBarActionsProps } from \"./shell/top-bar-actions\";\nexport { AccountMenuLanguage } from \"./shell/account-menu-language\";\nexport type { AccountMenuLanguageProps, AccountMenuLanguageOption } from \"./shell/account-menu-language\";\n// 0.46.0 (keksdose live #426): A− / A+ for the text size, beside the language.\nexport { AccountMenuTextSize } from \"./shell/account-menu-text-size\";\nexport type { AccountMenuTextSizeProps } from \"./shell/account-menu-text-size\";\nexport { TourMenu } from \"./shell/tour-menu\";\nexport type { TourMenuProps, TourMenuTour } from \"./shell/tour-menu\";\nexport { SyncStatusIndicator, syncState } from \"./shell/sync-status-indicator\";\nexport type { SyncStatus, SyncState, SyncStatusScope, SyncStatusIndicatorProps } from \"./shell/sync-status-indicator\";\n// 0.51 (§2.7.1, keksdose): one flask, the kit's Account section and the app's own dev\n// sections; `DevSwitchers` is a deprecated wrapper over it until 0.52.\nexport { DevMenu, DEV_ORIGINAL_ROLE_KEY } from \"./shell/dev-menu\";\nexport type { DevMenuProps, DevMenuSection } from \"./shell/dev-menu\";\nexport { DevSwitchers } from \"./shell/dev-switchers\";\nexport type { DevSwitchersProps } from \"./shell/dev-switchers\";\nexport { DEFAULT_TOP_BAR_PARTS_LABELS, useTopBarPartsLabels } from \"./shell/top-bar-parts-labels\";\nexport type {\n TopBarPartsLabels,\n TourMenuLabels,\n SyncStatusLabels,\n DevSwitchersLabels,\n} from \"./shell/top-bar-parts-labels\";\n"],"mappings":";AAIA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAEd,SAAS,YAAY,yBAAyB;AAI9C,SAAS,gBAAgB,oBAAoB;AAG7C,SAAS,yBAAyB;AAIlC,SAAS,yBAAyB;AAGlC,SAAS,gBAAgB,iCAAiC;AAE1D;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AASP,SAAS,kBAAkB,eAAe,qBAAqB,yBAAyB;AACxF,SAAS,uBAAuB;AAEhC,SAAS,yBAAyB;AAElC,SAAS,sBAAsB,+BAA+B;AAE9D;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAQP,SAAS,oBAAoB;AAK7B,SAAS,eAAe,4BAA4B;AAEpD,SAAS,2BAA2B;AAGpC,SAAS,2BAA2B;AAEpC,SAAS,gBAAgB;AAEzB,SAAS,qBAAqB,iBAAiB;AAI/C,SAAS,SAAS,6BAA6B;AAE/C,SAAS,oBAAoB;AAE7B,SAAS,8BAA8B,4BAA4B;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eifi1/ui-kit",
|
|
3
|
-
"version": "0.51.
|
|
3
|
+
"version": "0.51.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "App-agnostic shared design system: domain-free React components, theme and tokens.",
|
|
6
6
|
"_sideEffects": "The barrel (src/index.ts) re-exports EVERY component, including components/chart, which statically imports recharts. Without this field a bundler must assume each of those modules has side effects, so it cannot drop the unused ones: importing a Button from '@eifi1/ui-kit' dragged all 433KB of recharts into the consumer's ENTRY chunk, and Keksdose was preloading it on every page load, login included. Nothing in src/ imports for side effects (no bare imports, no CSS imports from JS, no module-level global writes) — the token stylesheet is its own './tokens.css' export — so this is safe. CSS is still listed so any future stylesheet survives. Re-checked when `flag-icons` left `dependencies` and tsup started emitting a `\"use client\"` banner: neither touches this. The flag stylesheet was never imported from JS in the first place (that was the finding), and a directive prologue is a string literal every bundler lifts out before it decides whether a module is inert.",
|
|
@@ -22,7 +22,7 @@ import { anchoredInline, useAnchoredPanel, viewportSize, type AnchoredPanel } fr
|
|
|
22
22
|
import { pressInside, takeEscape, useDismissLayer, useOutsideClick } from "../hooks/use-dismiss";
|
|
23
23
|
import { useOverlayHistory } from "../hooks/use-overlay-history";
|
|
24
24
|
import { isImeEnter } from "../lib/ime";
|
|
25
|
-
import {
|
|
25
|
+
import { matchesSearch, searchWords } from "../lib/text-match";
|
|
26
26
|
import { DEFAULT_COMBOBOX_LABELS, useKitLabels, useKitLocale } from "../i18n/kit-labels";
|
|
27
27
|
import { hasContent } from "../lib/presence";
|
|
28
28
|
|
|
@@ -283,10 +283,10 @@ export function useOptionSource<V extends string | number>({
|
|
|
283
283
|
const clientResults = useMemo(() => {
|
|
284
284
|
if (isAsync) return [];
|
|
285
285
|
const src = options ?? [];
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
return src.filter((o) =>
|
|
286
|
+
// Every word of the query, in the label or the sublabel (0.51.4): "migros zürich".
|
|
287
|
+
const words = searchWords(query, locale);
|
|
288
|
+
if (!filter || words.length === 0) return src;
|
|
289
|
+
return src.filter((o) => matchesSearch(words, [o.label, o.sublabel], locale));
|
|
290
290
|
}, [isAsync, options, query, filter, locale]);
|
|
291
291
|
|
|
292
292
|
const pending = isAsync && active && !tooShort && settled.query !== query;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
2
|
import type { ComponentPropsWithRef, ReactNode } from "react";
|
|
3
3
|
import { cn } from "../lib/cn";
|
|
4
|
-
import {
|
|
4
|
+
import { matchesSearch, searchWords } from "../lib/text-match";
|
|
5
5
|
import { ComboboxPanel, type ComboboxCore, type ComboOption } from "./combobox-core";
|
|
6
6
|
import { CURRENCIES, type CurrencyOption } from "../currencies";
|
|
7
7
|
|
|
@@ -136,10 +136,9 @@ export function CurrencyOptionList({
|
|
|
136
136
|
// it is written in) and the symbol — folded as every kit list folds (DUP-5).
|
|
137
137
|
const query = core.query;
|
|
138
138
|
const results = useMemo(() => {
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
: named;
|
|
139
|
+
// Every word of the query, in the code, the name or the symbol (0.51.4).
|
|
140
|
+
const words = searchWords(query, locale);
|
|
141
|
+
const hits = words.length > 0 ? named.filter(({ c, name }) => matchesSearch(words, [c.code, name, c.symbol], locale)) : named;
|
|
143
142
|
return hits.map(
|
|
144
143
|
({ c, name }): ComboOption<string> => ({
|
|
145
144
|
value: c.code,
|
|
@@ -119,17 +119,19 @@ export function useColumnFit<T>({
|
|
|
119
119
|
() => (active ? order.filter((c) => hidden.includes(c.key)).map((c) => c.key) : []),
|
|
120
120
|
[active, order, hidden],
|
|
121
121
|
);
|
|
122
|
-
const
|
|
122
|
+
const growCol = columns.find((c) => c.grow && !hiddenCols.has(c.key));
|
|
123
|
+
const growKey = growCol?.key;
|
|
124
|
+
const growMin = growCol?.growMin ?? GROW_MIN_REM;
|
|
123
125
|
|
|
124
126
|
// What `measure` reads, refreshed after each render (before the measure below runs):
|
|
125
127
|
// the ResizeObserver's callback is set up once and must see the current columns.
|
|
126
|
-
const latest = useRef({ order, current, growKey, active, scroller });
|
|
128
|
+
const latest = useRef({ order, current, growKey, growMin, active, scroller });
|
|
127
129
|
useLayoutEffect(() => {
|
|
128
|
-
latest.current = { order, current, growKey, active, scroller };
|
|
130
|
+
latest.current = { order, current, growKey, growMin, active, scroller };
|
|
129
131
|
});
|
|
130
132
|
|
|
131
133
|
const measure = useCallback(() => {
|
|
132
|
-
const { order, current, growKey, active, scroller } = latest.current;
|
|
134
|
+
const { order, current, growKey, growMin, active, scroller } = latest.current;
|
|
133
135
|
// Only the measure right after a bring-back can undo it.
|
|
134
136
|
const flip = lastShown.current;
|
|
135
137
|
lastShown.current = null;
|
|
@@ -143,7 +145,7 @@ export function useColumnFit<T>({
|
|
|
143
145
|
const growTh = growKey ? headRefs.get(growKey) : undefined;
|
|
144
146
|
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
|
|
145
147
|
let need = growTh
|
|
146
|
-
? table.offsetWidth - growTh.offsetWidth +
|
|
148
|
+
? table.offsetWidth - growTh.offsetWidth + growMin * rem
|
|
147
149
|
: table.offsetWidth;
|
|
148
150
|
if (!growTh && need <= avail + 0.5 && current.length > 0) {
|
|
149
151
|
// A `w-full` table is at least the scroller's width, which says nothing about how
|
|
@@ -125,6 +125,7 @@ export function DataTableHead<T>({
|
|
|
125
125
|
const isEndAligned = !!headClass && END_ALIGN.test(headClass);
|
|
126
126
|
const width = widths[col.key];
|
|
127
127
|
const ownHead = splitCellProps(col.headProps);
|
|
128
|
+
const growFloor = col.grow && col.growMin != null ? `${col.growMin}rem` : undefined;
|
|
128
129
|
const pinnedHead = width ? { width, minWidth: width, maxWidth: width } : undefined;
|
|
129
130
|
return (
|
|
130
131
|
<th
|
|
@@ -156,8 +157,11 @@ export function DataTableHead<T>({
|
|
|
156
157
|
: undefined
|
|
157
158
|
}
|
|
158
159
|
style={
|
|
159
|
-
ownHead.style || pinnedHead || lockedWidths?.[col.key] !== undefined
|
|
160
|
+
ownHead.style || pinnedHead || lockedWidths?.[col.key] !== undefined || growFloor
|
|
160
161
|
? {
|
|
162
|
+
// A `grow` column's own floor (0.51.4), over the class's 10rem; a
|
|
163
|
+
// locked width below wins, as it does over the class.
|
|
164
|
+
...(growFloor ? { minWidth: growFloor } : null),
|
|
161
165
|
...ownHead.style,
|
|
162
166
|
...(lockedWidths?.[col.key] !== undefined ? { minWidth: lockedWidths[col.key] } : null),
|
|
163
167
|
...pinnedHead,
|
|
@@ -184,6 +184,14 @@ export interface DataTableColumn<T> {
|
|
|
184
184
|
* broken in two to make room for it.
|
|
185
185
|
*/
|
|
186
186
|
grow?: boolean;
|
|
187
|
+
/**
|
|
188
|
+
* The least this `grow` column keeps, in rem (0.51.4; default 10): the floor below which
|
|
189
|
+
* the table leaves out `fitPriority` columns, and once none is left, scrolls sideways.
|
|
190
|
+
* Lower it for a column whose first words carry it — the feedback inbox's Subject, 8,
|
|
191
|
+
* so the inbox at 768px with a sidebar keeps Subject, Status and the row actions
|
|
192
|
+
* without scrolling (Kurvenschmiede). In rem, so Large text asks for more.
|
|
193
|
+
*/
|
|
194
|
+
growMin?: number;
|
|
187
195
|
/**
|
|
188
196
|
* Leave this column out while the TABLE is too narrow for it (0.48, keksdose live
|
|
189
197
|
* #451) — measured on the table's own scroller, so a sidebar, the window and Large
|
|
@@ -2,7 +2,7 @@ import { useCallback, useId, useMemo } from "react";
|
|
|
2
2
|
import type { ComponentPropsWithoutRef, ReactNode, Ref } from "react";
|
|
3
3
|
import { ChevronDown } from "lucide-react";
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
|
-
import {
|
|
5
|
+
import { matchesSearch, searchWords } from "../lib/text-match";
|
|
6
6
|
import { ComboboxPanel, useComboboxCore, type ComboOption } from "./combobox-core";
|
|
7
7
|
import { assignRef } from "./field-parts";
|
|
8
8
|
import { DEFAULT_COMBOBOX_LABELS, useKitLabels, useKitLocale } from "../i18n/kit-labels";
|
|
@@ -90,13 +90,20 @@ export function GroupedPicker({
|
|
|
90
90
|
// plain `toLowerCase` gets wrong), and now the accents too — "zurich" finds Zürich.
|
|
91
91
|
const locale = useKitLocale();
|
|
92
92
|
const results = useMemo(() => {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
93
|
+
// Every word of the query (0.51.4): all in the group's name keeps the whole group;
|
|
94
|
+
// otherwise an item stays when each word is in its label or its group's name, so
|
|
95
|
+
// "migros 8004" finds Migros' 8004 shop.
|
|
96
|
+
const words = searchWords(query, locale);
|
|
97
|
+
const kept =
|
|
98
|
+
words.length > 0
|
|
99
|
+
? groups
|
|
100
|
+
.map((g) =>
|
|
101
|
+
matchesSearch(words, [g.label], locale)
|
|
102
|
+
? g
|
|
103
|
+
: { ...g, items: g.items.filter((i) => matchesSearch(words, [g.label, i.label], locale)) },
|
|
104
|
+
)
|
|
105
|
+
.filter((g) => g.items.length > 0)
|
|
106
|
+
: groups;
|
|
100
107
|
return kept.flatMap((g) =>
|
|
101
108
|
g.items.map(
|
|
102
109
|
(item): ComboOption<string> => ({
|
|
@@ -6,7 +6,7 @@ import { SearchField } from "./search-field";
|
|
|
6
6
|
import { TILE_SIZE, TileRadioGroup } from "./tile-radio";
|
|
7
7
|
import type { TileItem, TileSize } from "./tile-radio";
|
|
8
8
|
import { hasContent } from "../lib/presence";
|
|
9
|
-
import {
|
|
9
|
+
import { matchesSearch, searchWords } from "../lib/text-match";
|
|
10
10
|
import { LabelStrip, useFieldMessages, type FieldHelpProps } from "./field-parts";
|
|
11
11
|
import { DEFAULT_ICON_PICKER_LABELS } from "./icon-picker-labels";
|
|
12
12
|
|
|
@@ -147,16 +147,18 @@ export function IconPicker<T extends string>({
|
|
|
147
147
|
});
|
|
148
148
|
const [query, setQuery] = useState("");
|
|
149
149
|
// Case- and accent-insensitive, so "cafe" finds "Café" — every kit list's fold (DUP-5).
|
|
150
|
-
|
|
150
|
+
// Every word of the query, in the label or a keyword (0.51.4).
|
|
151
|
+
const words = useMemo(() => searchWords(query, locale), [query, locale]);
|
|
152
|
+
const needle = words.length > 0;
|
|
151
153
|
|
|
152
154
|
const visible = useMemo(
|
|
153
155
|
() =>
|
|
154
156
|
needle
|
|
155
157
|
? options.filter((o) =>
|
|
156
|
-
[o.label, ...(o.keywords ?? [])]
|
|
158
|
+
matchesSearch(words, [o.label, ...(o.keywords ?? [])], locale),
|
|
157
159
|
)
|
|
158
160
|
: options,
|
|
159
|
-
[options, needle, locale],
|
|
161
|
+
[options, needle, words, locale],
|
|
160
162
|
);
|
|
161
163
|
const byValue = useMemo(() => new Map(options.map((o) => [o.value, o])), [options]);
|
|
162
164
|
const items: TileItem<T>[] = [
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
type ComboOption,
|
|
25
25
|
} from "./combobox-core";
|
|
26
26
|
import { DEFAULT_COMBOBOX_LABELS, useKitLabels, useKitLocale } from "../i18n/kit-labels";
|
|
27
|
-
import {
|
|
27
|
+
import { matchesSearch, rankMatch, searchWords } from "../lib/text-match";
|
|
28
28
|
import {
|
|
29
29
|
EndHintRow,
|
|
30
30
|
FieldCaption,
|
|
@@ -311,8 +311,9 @@ export function InlineEntityCombobox<V extends string | number, C extends ComboC
|
|
|
311
311
|
const matches = useMemo(() => {
|
|
312
312
|
let ranked = options;
|
|
313
313
|
if (query) {
|
|
314
|
-
|
|
315
|
-
const
|
|
314
|
+
// Every word of the query, in the label or the sublabel (0.51.4).
|
|
315
|
+
const words = searchWords(query, locale);
|
|
316
|
+
const hit = (o: ComboOption<V>) => matchesSearch(words, [o.label, o.sublabel], locale);
|
|
316
317
|
// The label's whole name or its start first (`rankMatch` 0 and 1), then a match
|
|
317
318
|
// anywhere in the label or the sublabel. Not the group (0.40, kk run 79): with
|
|
318
319
|
// first-letter groups one typed letter matched its whole group.
|
|
@@ -321,7 +322,7 @@ export function InlineEntityCombobox<V extends string | number, C extends ComboC
|
|
|
321
322
|
return rank === 0 || rank === 1;
|
|
322
323
|
};
|
|
323
324
|
const starts = options.filter(leads);
|
|
324
|
-
const rest = options.filter((o) => !leads(o) &&
|
|
325
|
+
const rest = options.filter((o) => !leads(o) && hit(o));
|
|
325
326
|
ranked = [...starts, ...rest];
|
|
326
327
|
}
|
|
327
328
|
if (!ranked.some((o) => o.group)) return ranked;
|
|
@@ -10,6 +10,8 @@ export const DEFAULT_LEGAL_LABELS: LegalLabels = {
|
|
|
10
10
|
impressum: "Imprint",
|
|
11
11
|
privacy: "Privacy Policy",
|
|
12
12
|
terms: "Terms",
|
|
13
|
+
security: "Security",
|
|
14
|
+
accessibility: "Accessibility",
|
|
13
15
|
},
|
|
14
16
|
titles: {
|
|
15
17
|
impressum: "Imprint",
|
|
@@ -28,6 +30,11 @@ export const DEFAULT_LEGAL_LABELS: LegalLabels = {
|
|
|
28
30
|
`This is a translation for your convenience. The ${o.bindingLanguage} version is binding.`,
|
|
29
31
|
show: (o) => `Show the ${o.bindingLanguage} version`,
|
|
30
32
|
},
|
|
33
|
+
updated: (date) => `Last updated: ${date}`,
|
|
34
|
+
summary: {
|
|
35
|
+
title: "In short",
|
|
36
|
+
note: "This summary is for orientation; only the full text is binding.",
|
|
37
|
+
},
|
|
31
38
|
sections: {
|
|
32
39
|
impressum: {
|
|
33
40
|
operator: {
|
|
@@ -82,7 +89,7 @@ export const DEFAULT_LEGAL_LABELS: LegalLabels = {
|
|
|
82
89
|
},
|
|
83
90
|
changes: {
|
|
84
91
|
title: "Changes to these terms",
|
|
85
|
-
body: "
|
|
92
|
+
body: "We may update these terms as the service evolves. We announce a material change at least 30 days before it takes effect, by email and in the app, together with the new text. If you do not agree, you can object or delete your account before that date, and you can export your data until then. If you keep using the service after that date, the changed terms apply. Changes that only benefit you, or that the law requires, apply without this notice and cannot be objected to. If you object to a change, we may end your account on the date it takes effect. If you have paid for a period that runs beyond that date, we refund the unused part.",
|
|
86
93
|
},
|
|
87
94
|
language: {
|
|
88
95
|
title: "Language",
|
|
@@ -6,6 +6,8 @@ import { UiKitProvider, useKitLocale } from "../i18n/kit-labels";
|
|
|
6
6
|
import type { LabelOverride, UiKitLabelOverrides } from "../i18n/kit-labels";
|
|
7
7
|
import { useNoIndex } from "../hooks/use-noindex";
|
|
8
8
|
import { cn } from "../lib/cn";
|
|
9
|
+
import { parseIsoDate } from "../lib/dates";
|
|
10
|
+
import { formatDate } from "../lib/format";
|
|
9
11
|
import { Checkbox } from "./checkbox";
|
|
10
12
|
import type { CheckboxProps } from "./checkbox";
|
|
11
13
|
import {
|
|
@@ -19,6 +21,7 @@ import {
|
|
|
19
21
|
LegalSlotContext,
|
|
20
22
|
LegalText,
|
|
21
23
|
legalOperatorText,
|
|
24
|
+
resolveLegalRefs,
|
|
22
25
|
useLegalLabels,
|
|
23
26
|
warnLegalOnce,
|
|
24
27
|
} from "./legal";
|
|
@@ -339,6 +342,34 @@ export interface LegalPageProps {
|
|
|
339
342
|
/** Default: the kit's beta notice on the imprint and the terms, its privacy notice on
|
|
340
343
|
* the privacy policy (§3.2). `null` for none. */
|
|
341
344
|
notice?: ReactNode;
|
|
345
|
+
/**
|
|
346
|
+
* 0.51.3: the day this page's text last changed, "YYYY-MM-DD" (legal benchmark
|
|
347
|
+
* 2026-10-29, gap 8). Shown under the title as `legal.updated` — "Last updated:
|
|
348
|
+
* 29 October 2026", "Stand: 29. Oktober 2026" — with the date written out in `locale`,
|
|
349
|
+
* so it follows the page's language like every other kit word, the switch to the
|
|
350
|
+
* binding version included. Left out, no line. One date per page: the imprint, the
|
|
351
|
+
* privacy policy and the terms change on their own days.
|
|
352
|
+
*
|
|
353
|
+
* An app that records which terms someone accepted (keksdose) stores the terms page's
|
|
354
|
+
* date: it is the version the reader saw.
|
|
355
|
+
*
|
|
356
|
+
* The kit's sections are part of the page, so the date moves with their words too: a
|
|
357
|
+
* kit release that changes them says so in its adoption note ("Legal wording"), and
|
|
358
|
+
* the app sets the page's date to the day it ships that bump (§2, decision 10).
|
|
359
|
+
*/
|
|
360
|
+
updated?: string;
|
|
361
|
+
/**
|
|
362
|
+
* 0.51.4: a plain-language summary before the first section (legal benchmark, Marcel
|
|
363
|
+
* 2026-10-29) — the app's three to six points, each a sentence. The kit draws them as a
|
|
364
|
+
* list under `legal.summary.title` ("In short", "Das Wichtigste in Kürze") and adds
|
|
365
|
+
* `legal.summary.note`: the summary orients, only the full text binds. Worded without
|
|
366
|
+
* "below", because on a translated page the text below is a translation too.
|
|
367
|
+
*
|
|
368
|
+
* A string point is running text with its `{ref:key}` resolved, as in a section:
|
|
369
|
+
* "You can delete your account at any time (section {ref:ending})." Unnumbered, with
|
|
370
|
+
* the id `summary`; the sections' numbers do not count it. Left out or empty, nothing.
|
|
371
|
+
*/
|
|
372
|
+
summary?: readonly ReactNode[];
|
|
342
373
|
/**
|
|
343
374
|
* 0.28.1: switches the app to the operator's `bindingLanguage` — the button after the
|
|
344
375
|
* translation note on a page read in another language. Without it the note stands
|
|
@@ -456,6 +487,8 @@ function slotSections(
|
|
|
456
487
|
*
|
|
457
488
|
* What it adds to the column:
|
|
458
489
|
* - the title, from `legal.titles`, and "← Back to home" pointing at `homeHref`;
|
|
490
|
+
* - the as-of line under the title, from `updated` (0.51.3);
|
|
491
|
+
* - the plain-language summary before the sections, from `summary` (0.51.4);
|
|
459
492
|
* - the default notice per page — the beta notice, or the privacy notice on the privacy
|
|
460
493
|
* policy, whose technical sections describe running code (§3.2, §7.3);
|
|
461
494
|
* - numbers: the privacy and terms sections are "N. Title" in render order, the
|
|
@@ -476,6 +509,8 @@ export function LegalPage({
|
|
|
476
509
|
footer,
|
|
477
510
|
homeHref = "/",
|
|
478
511
|
notice,
|
|
512
|
+
updated,
|
|
513
|
+
summary,
|
|
479
514
|
onShowBindingLanguage,
|
|
480
515
|
locale: localeProp,
|
|
481
516
|
headingAs = "h1",
|
|
@@ -522,6 +557,37 @@ export function LegalPage({
|
|
|
522
557
|
)}
|
|
523
558
|
</p>
|
|
524
559
|
) : undefined;
|
|
560
|
+
// A calendar day, written out in the page's language; `formatDate` reads a bare
|
|
561
|
+
// "YYYY-MM-DD" as that day in every zone. A malformed date shows no line rather than a
|
|
562
|
+
// made-up one ("2026-02-30" would roll over into March).
|
|
563
|
+
const updatedValid = updated !== undefined && parseIsoDate(updated) !== null;
|
|
564
|
+
if (updated !== undefined && !updatedValid) {
|
|
565
|
+
warnLegalOnce(`LegalPage: updated="${updated}" is not a "YYYY-MM-DD" date; no "Last updated" line is shown.`);
|
|
566
|
+
}
|
|
567
|
+
const updatedLine = updatedValid ? (
|
|
568
|
+
<time dateTime={updated}>
|
|
569
|
+
{(labels.updated ?? DEFAULT_LEGAL_LABELS.updated!)(formatDate(updated, "long", { locale }))}
|
|
570
|
+
</time>
|
|
571
|
+
) : undefined;
|
|
572
|
+
const summaryLabels = labels.summary ?? DEFAULT_LEGAL_LABELS.summary!;
|
|
573
|
+
const SummaryHeading = context.sectionHeading;
|
|
574
|
+
const summaryBox =
|
|
575
|
+
summary !== undefined && summary.length > 0 ? (
|
|
576
|
+
<section id="summary" className="rounded-lg border border-subtle bg-surface p-4 sm:p-5">
|
|
577
|
+
<SummaryHeading className="text-lg font-semibold break-words text-primary">{summaryLabels.title}</SummaryHeading>
|
|
578
|
+
<ul className="mt-3 space-y-2">
|
|
579
|
+
{summary.map((point, i) => (
|
|
580
|
+
<li key={i} className="flex gap-2.5 text-sm leading-relaxed text-secondary">
|
|
581
|
+
<span aria-hidden className="mt-2 size-1.5 shrink-0 rounded-full bg-brand" />
|
|
582
|
+
<span className="min-w-0 break-words">
|
|
583
|
+
{typeof point === "string" ? resolveLegalRefs(point, context) : point}
|
|
584
|
+
</span>
|
|
585
|
+
</li>
|
|
586
|
+
))}
|
|
587
|
+
</ul>
|
|
588
|
+
<p className="mt-3 text-xs text-muted">{summaryLabels.note}</p>
|
|
589
|
+
</section>
|
|
590
|
+
) : undefined;
|
|
525
591
|
|
|
526
592
|
return (
|
|
527
593
|
<UiKitProvider labels={overrides}>
|
|
@@ -537,7 +603,9 @@ export function LegalPage({
|
|
|
537
603
|
}
|
|
538
604
|
title={labels.titles[page]}
|
|
539
605
|
notice={shownNotice}
|
|
606
|
+
updated={updatedLine}
|
|
540
607
|
translationNote={translationNote}
|
|
608
|
+
summary={summaryBox}
|
|
541
609
|
headingAs={headingAs}
|
|
542
610
|
>
|
|
543
611
|
{nodes}
|
|
@@ -596,8 +664,10 @@ function acceptLabel(labels: LegalLabels, termsHref: string, privacyHref: string
|
|
|
596
664
|
*
|
|
597
665
|
* A REQUIRED kit `Checkbox` — not a Switch: a form answer that waits for the submit
|
|
598
666
|
* button. The app keeps its submit disabled while it is unticked, so there is never a
|
|
599
|
-
* failed attempt and no `error` to show.
|
|
600
|
-
*
|
|
667
|
+
* failed attempt and no `error` to show. The checkbox itself sends nothing and asks for
|
|
668
|
+
* no re-acceptance (Marcel, §2.4). An app that records which terms someone accepted
|
|
669
|
+
* stores, with the account, the terms page's `updated` date — the version the reader
|
|
670
|
+
* saw — on its own (keksdose since 2026-10-29; §3.4).
|
|
601
671
|
*
|
|
602
672
|
* The label is ONE template, `legal.accept`, so a language puts the links where its
|
|
603
673
|
* grammar needs them, articles included — fr "J’accepte les {terms} et la {privacy}",
|
package/src/components/legal.tsx
CHANGED
|
@@ -44,6 +44,11 @@ export const LEGAL_HREFS: Readonly<Record<LegalPageKey, string>> = Object.freeze
|
|
|
44
44
|
terms: "/terms",
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
+
/** 0.51.4: the two public pages an app MAY have beside the legal ones, linked after
|
|
48
|
+
* them in the footer (§3.3) — keksdose's /security and /accessibility. Not legal pages:
|
|
49
|
+
* an app draws them with `PublicPage`, in its own words, at routes of its choosing. */
|
|
50
|
+
export type LegalInfoPageKey = "security" | "accessibility";
|
|
51
|
+
|
|
47
52
|
/**
|
|
48
53
|
* Who runs the app — the values the kit's legal sections are filled in with (§5).
|
|
49
54
|
*
|
|
@@ -179,8 +184,11 @@ export interface LegalFramedSectionLabels {
|
|
|
179
184
|
export interface LegalLabels {
|
|
180
185
|
/** The accessible name of {@link LegalLinks}' and {@link LegalFooter}'s `<nav>`. */
|
|
181
186
|
navLabel: string;
|
|
182
|
-
/** The short labels of the three links, everywhere the links appear (§3.3)
|
|
183
|
-
|
|
187
|
+
/** The short labels of the three links, everywhere the links appear (§3.3), and since
|
|
188
|
+
* 0.51.4 of the two optional public pages after them (`securityHref`,
|
|
189
|
+
* `accessibilityHref` on {@link LegalLinks}) — optional keys, as every key added to a
|
|
190
|
+
* namespace an app may annotate: left out, the provider's, then English. */
|
|
191
|
+
links: Record<LegalPageKey, string> & Partial<Record<LegalInfoPageKey, string>>;
|
|
184
192
|
/** The pages' titles — the `<h1>`, and the links of the terms checkbox. */
|
|
185
193
|
titles: Record<LegalPageKey, string>;
|
|
186
194
|
/** The back link above the title: "← Back to home". */
|
|
@@ -208,6 +216,22 @@ export interface LegalLabels {
|
|
|
208
216
|
note: (o: LegalOperatorText) => string;
|
|
209
217
|
show: (o: LegalOperatorText) => string;
|
|
210
218
|
};
|
|
219
|
+
/**
|
|
220
|
+
* 0.51.3: the as-of line under the title (§3.2), "Last updated: 29 October 2026" —
|
|
221
|
+
* {@link LegalPage}'s `updated`, called with the date already written out in the page's
|
|
222
|
+
* locale. OPTIONAL, as every key added to a namespace an app may annotate: left out,
|
|
223
|
+
* the provider's, then English.
|
|
224
|
+
*/
|
|
225
|
+
updated?: (date: string) => string;
|
|
226
|
+
/**
|
|
227
|
+
* 0.51.4: the plain-language summary at the top of a page ({@link LegalPage}'s
|
|
228
|
+
* `summary`, §3.2) — its heading, and the line under the points saying that only the
|
|
229
|
+
* full text binds. Optional, as `updated`.
|
|
230
|
+
*/
|
|
231
|
+
summary?: {
|
|
232
|
+
title: string;
|
|
233
|
+
note: string;
|
|
234
|
+
};
|
|
211
235
|
/** The kit's sections (§4.3), by page and section key. */
|
|
212
236
|
sections: {
|
|
213
237
|
impressum: {
|
|
@@ -356,6 +380,15 @@ export interface LegalLinksProps {
|
|
|
356
380
|
/** Default (0.28): the three routes, `/impressum`, `/privacy` and `/terms`, with the
|
|
357
381
|
* kit's short link labels (`legal.links`), in the contract's order. */
|
|
358
382
|
links?: readonly LegalLink[];
|
|
383
|
+
/**
|
|
384
|
+
* 0.51.4: the app's security page — a "Security" link after the others, with the kit's
|
|
385
|
+
* label (`legal.links.security`). Left out, no link: only an app that has the page
|
|
386
|
+
* names its route.
|
|
387
|
+
*/
|
|
388
|
+
securityHref?: string;
|
|
389
|
+
/** 0.51.4: the app's accessibility statement — "Accessibility", last. As
|
|
390
|
+
* `securityHref`. */
|
|
391
|
+
accessibilityHref?: string;
|
|
359
392
|
/** The page being shown; its link is marked `aria-current="page"`. Usually the
|
|
360
393
|
* router's pathname. */
|
|
361
394
|
currentHref?: string;
|
|
@@ -380,6 +413,14 @@ function defaultLinks(labels: LegalLabels): LegalLink[] {
|
|
|
380
413
|
}));
|
|
381
414
|
}
|
|
382
415
|
|
|
416
|
+
/** The optional pages after them, each only where the app named its route. */
|
|
417
|
+
function infoLinks(labels: LegalLabels, hrefs: Partial<Record<LegalInfoPageKey, string>>): LegalLink[] {
|
|
418
|
+
return (["security", "accessibility"] as const).flatMap((page) => {
|
|
419
|
+
const href = hrefs[page];
|
|
420
|
+
return href ? [{ href, label: labels.links[page] ?? DEFAULT_LEGAL_LABELS.links[page] }] : [];
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
383
424
|
/**
|
|
384
425
|
* The legal pages as a row of quiet links, the one you are on marked (kastlan's
|
|
385
426
|
* `LegalLinks`). Wraps on a phone.
|
|
@@ -391,6 +432,8 @@ function defaultLinks(labels: LegalLabels): LegalLink[] {
|
|
|
391
432
|
*/
|
|
392
433
|
export function LegalLinks({
|
|
393
434
|
links,
|
|
435
|
+
securityHref,
|
|
436
|
+
accessibilityHref,
|
|
394
437
|
currentHref,
|
|
395
438
|
nav = true,
|
|
396
439
|
onLinkClick,
|
|
@@ -398,7 +441,10 @@ export function LegalLinks({
|
|
|
398
441
|
className,
|
|
399
442
|
}: LegalLinksProps) {
|
|
400
443
|
const labels = useLegalLabels(labelsProp);
|
|
401
|
-
const shown =
|
|
444
|
+
const shown = [
|
|
445
|
+
...(links ?? defaultLinks(labels)),
|
|
446
|
+
...infoLinks(labels, { security: securityHref, accessibility: accessibilityHref }),
|
|
447
|
+
];
|
|
402
448
|
const items = shown.flatMap((link, i) => [
|
|
403
449
|
...(i > 0 ? [<span key={`sep-${link.href}`} aria-hidden="true">·</span>] : []),
|
|
404
450
|
<TextLink key={link.href} href={link.href} tone="muted" current={link.href === currentHref} onClick={onLinkClick}>
|
|
@@ -422,6 +468,10 @@ export interface LegalFooterProps {
|
|
|
422
468
|
children?: ReactNode;
|
|
423
469
|
/** Default: the three routes with the kit's link labels — see {@link LegalLinks}. */
|
|
424
470
|
links?: readonly LegalLink[];
|
|
471
|
+
/** 0.51.4: "Security" after the others — see {@link LegalLinksProps.securityHref}. */
|
|
472
|
+
securityHref?: string;
|
|
473
|
+
/** 0.51.4: "Accessibility", last — see {@link LegalLinksProps.accessibilityHref}. */
|
|
474
|
+
accessibilityHref?: string;
|
|
425
475
|
/** Marks that link `aria-current="page"`. {@link LegalPage}'s default footer passes
|
|
426
476
|
* its own page. */
|
|
427
477
|
currentHref?: string;
|
|
@@ -447,14 +497,29 @@ export interface LegalFooterProps {
|
|
|
447
497
|
* landing, auth, legal and 404 pages; kastlan and Kurvenschmiede had them on some pages
|
|
448
498
|
* only. Its palette classes (`border-slate-200 bg-white dark:…`) became the surface and
|
|
449
499
|
* border tokens, so it follows the theme like the rest of the kit.
|
|
500
|
+
*
|
|
501
|
+
* 0.51.4: an app with a security page or an accessibility statement passes their routes
|
|
502
|
+
* (`securityHref`, `accessibilityHref`) and the row goes on with "Security · Accessibility"
|
|
503
|
+
* in the kit's words, so the footer reads the same in every app that has them.
|
|
450
504
|
*/
|
|
451
|
-
export function LegalFooter({
|
|
505
|
+
export function LegalFooter({
|
|
506
|
+
children,
|
|
507
|
+
links,
|
|
508
|
+
securityHref,
|
|
509
|
+
accessibilityHref,
|
|
510
|
+
currentHref,
|
|
511
|
+
landmark = true,
|
|
512
|
+
className,
|
|
513
|
+
labels,
|
|
514
|
+
}: LegalFooterProps) {
|
|
452
515
|
const Root = landmark ? "footer" : "div";
|
|
453
516
|
return (
|
|
454
517
|
<Root className={cn("border-t border-subtle bg-surface", className)}>
|
|
455
518
|
<div className="mx-auto max-w-5xl px-4 py-8 text-sm text-muted">
|
|
456
519
|
<LegalLinks
|
|
457
520
|
links={links}
|
|
521
|
+
securityHref={securityHref}
|
|
522
|
+
accessibilityHref={accessibilityHref}
|
|
458
523
|
currentHref={currentHref}
|
|
459
524
|
labels={labels}
|
|
460
525
|
className="justify-center gap-y-2 text-sm"
|
|
@@ -535,9 +600,13 @@ export interface LegalLayoutProps {
|
|
|
535
600
|
* than the others (its technical sections describe running code).
|
|
536
601
|
*/
|
|
537
602
|
notice?: ReactNode;
|
|
603
|
+
/** 0.51.3: right under the title — {@link LegalPage}'s "Last updated: …" line. */
|
|
604
|
+
updated?: ReactNode;
|
|
538
605
|
/** 0.28.1: under the title, before the notice — {@link LegalPage}'s note that this
|
|
539
606
|
* page is a translation and which language is binding. */
|
|
540
607
|
translationNote?: ReactNode;
|
|
608
|
+
/** 0.51.4: before the sections — {@link LegalPage}'s plain-language summary. */
|
|
609
|
+
summary?: ReactNode;
|
|
541
610
|
/** The sections — {@link LegalSection}s. */
|
|
542
611
|
children: ReactNode;
|
|
543
612
|
/** The three pages, linked at the end of the column with the current one marked.
|
|
@@ -555,7 +624,9 @@ export function LegalLayout({
|
|
|
555
624
|
title,
|
|
556
625
|
back,
|
|
557
626
|
notice,
|
|
627
|
+
updated,
|
|
558
628
|
translationNote,
|
|
629
|
+
summary,
|
|
559
630
|
children,
|
|
560
631
|
links,
|
|
561
632
|
currentHref,
|
|
@@ -567,13 +638,17 @@ export function LegalLayout({
|
|
|
567
638
|
<article className={cn("mx-auto w-full max-w-3xl px-4 py-12", className)}>
|
|
568
639
|
{back !== undefined && <div className="mb-4 text-sm">{back}</div>}
|
|
569
640
|
<Heading className="text-3xl font-bold tracking-tight break-words text-primary">{title}</Heading>
|
|
641
|
+
{updated !== undefined && updated !== null && <p className="mt-2 text-sm text-muted">{updated}</p>}
|
|
570
642
|
{translationNote !== undefined && translationNote !== null && <div className="mt-3">{translationNote}</div>}
|
|
571
643
|
{notice !== undefined && notice !== null && (
|
|
572
644
|
<AlertBanner tone="warning" role="note" className="mt-4">
|
|
573
645
|
{notice}
|
|
574
646
|
</AlertBanner>
|
|
575
647
|
)}
|
|
576
|
-
<div className="mt-8 space-y-8">
|
|
648
|
+
<div className="mt-8 space-y-8">
|
|
649
|
+
{summary}
|
|
650
|
+
{children}
|
|
651
|
+
</div>
|
|
577
652
|
{links !== undefined && links.length > 0 && (
|
|
578
653
|
<LegalLinks links={links} currentHref={currentHref} labels={labels} className="mt-12 border-t border-subtle pt-4" />
|
|
579
654
|
)}
|