@eifi1/ui-kit 0.41.0 → 0.42.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 +19 -6
- package/dist/admin/admin-area-labels.d.ts +55 -0
- package/dist/admin/admin-area-labels.js +33 -0
- package/dist/admin/admin-area-labels.js.map +1 -0
- package/dist/admin/admin-forbidden.d.ts +35 -0
- package/dist/admin/admin-forbidden.js +36 -0
- package/dist/admin/admin-forbidden.js.map +1 -0
- package/dist/admin/admin-overview.d.ts +93 -0
- package/dist/admin/admin-overview.js +69 -0
- package/dist/admin/admin-overview.js.map +1 -0
- package/dist/admin/admin-sections.d.ts +63 -0
- package/dist/admin/admin-sections.js +64 -0
- package/dist/admin/admin-sections.js.map +1 -0
- package/dist/admin/admin-tool-links.d.ts +48 -0
- package/dist/admin/admin-tool-links.js +31 -0
- package/dist/admin/admin-tool-links.js.map +1 -0
- package/dist/billing/billing-banner.d.ts +25 -2
- package/dist/billing/billing-banner.js +5 -2
- package/dist/billing/billing-banner.js.map +1 -1
- package/dist/billing/billing-labels.d.ts +32 -0
- package/dist/billing/billing-labels.js +9 -0
- package/dist/billing/billing-labels.js.map +1 -1
- package/dist/billing/billing-overview.d.ts +33 -0
- package/dist/billing/billing-overview.js +6 -0
- package/dist/billing/billing-overview.js.map +1 -1
- package/dist/billing/billing-standing.d.ts +8 -0
- package/dist/billing/billing-standing.js.map +1 -1
- package/dist/billing/cancel-subscription.d.ts +46 -0
- package/dist/billing/cancel-subscription.js +57 -0
- package/dist/billing/cancel-subscription.js.map +1 -0
- package/dist/billing/plan-card.d.ts +45 -1
- package/dist/billing/plan-card.js +53 -2
- package/dist/billing/plan-card.js.map +1 -1
- package/dist/billing/plan-switch.d.ts +10 -1
- package/dist/billing/plan-switch.js +22 -13
- package/dist/billing/plan-switch.js.map +1 -1
- package/dist/billing/subscription-actions.d.ts +37 -4
- package/dist/billing/subscription-actions.js +10 -3
- package/dist/billing/subscription-actions.js.map +1 -1
- package/dist/components/account-chips.d.ts +42 -1
- package/dist/components/account-chips.js +33 -0
- package/dist/components/account-chips.js.map +1 -1
- package/dist/components/empty-state.d.ts +2 -1
- package/dist/components/empty-state.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/i18n/defaults.js +5 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.js +79 -0
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +9 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/en.js +93 -1
- package/dist/i18n/locales/en.js.map +3 -3
- package/dist/i18n/locales/es.js +76 -0
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +75 -0
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +76 -0
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +76 -0
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +75 -0
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.js +20 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +18 -2
- package/dist/index.js +23 -1
- package/dist/index.js.map +1 -1
- package/dist/settings/settings-layout.d.ts +7 -1
- package/dist/settings/settings-layout.js +35 -27
- package/dist/settings/settings-layout.js.map +1 -1
- package/dist/shell/account-menu-language.d.ts +46 -0
- package/dist/shell/account-menu-language.js +64 -0
- package/dist/shell/account-menu-language.js.map +1 -0
- package/dist/shell/admin-menu.d.ts +46 -0
- package/dist/shell/admin-menu.js +60 -0
- package/dist/shell/admin-menu.js.map +1 -0
- package/dist/shell/app-shell.d.ts +10 -0
- package/dist/shell/app-shell.js +22 -4
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/dev-switchers.d.ts +53 -0
- package/dist/shell/dev-switchers.js +102 -0
- package/dist/shell/dev-switchers.js.map +1 -0
- package/dist/shell/sync-status-indicator.d.ts +86 -0
- package/dist/shell/sync-status-indicator.js +170 -0
- package/dist/shell/sync-status-indicator.js.map +1 -0
- package/dist/shell/top-bar-actions.d.ts +49 -0
- package/dist/shell/top-bar-actions.js +31 -0
- package/dist/shell/top-bar-actions.js.map +1 -0
- package/dist/shell/top-bar-parts-labels.d.ts +84 -0
- package/dist/shell/top-bar-parts-labels.js +48 -0
- package/dist/shell/top-bar-parts-labels.js.map +1 -0
- package/dist/shell/topbar-action-menu.d.ts +13 -1
- package/dist/shell/topbar-action-menu.js +5 -2
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell/topbar-controls.d.ts +16 -3
- package/dist/shell/topbar-controls.js +5 -1
- package/dist/shell/topbar-controls.js.map +1 -1
- package/dist/shell/tour-menu.d.ts +58 -0
- package/dist/shell/tour-menu.js +73 -0
- package/dist/shell/tour-menu.js.map +1 -0
- package/dist/shell.d.ts +14 -0
- package/dist/shell.js +19 -0
- package/dist/shell.js.map +1 -1
- package/package.json +1 -1
- package/src/admin/admin-area-labels.ts +85 -0
- package/src/admin/admin-forbidden.tsx +75 -0
- package/src/admin/admin-overview.tsx +166 -0
- package/src/admin/admin-sections.tsx +137 -0
- package/src/admin/admin-tool-links.tsx +84 -0
- package/src/billing/billing-banner.tsx +32 -4
- package/src/billing/billing-labels.ts +53 -0
- package/src/billing/billing-overview.ts +37 -0
- package/src/billing/billing-standing.ts +8 -0
- package/src/billing/cancel-subscription.tsx +102 -0
- package/src/billing/plan-card.tsx +97 -1
- package/src/billing/plan-switch.tsx +16 -0
- package/src/billing/subscription-actions.tsx +64 -15
- package/src/components/account-chips.tsx +81 -1
- package/src/components/empty-state.tsx +2 -1
- package/src/i18n/defaults.ts +4 -0
- package/src/i18n/german.ts +92 -0
- package/src/i18n/kit-labels.tsx +9 -0
- package/src/i18n/locales/en.ts +19 -0
- package/src/i18n/locales/es.ts +86 -0
- package/src/i18n/locales/fr.ts +86 -0
- package/src/i18n/locales/hu.ts +84 -0
- package/src/i18n/locales/it.ts +87 -0
- package/src/i18n/locales/zh.ts +79 -0
- package/src/i18n/review.ts +19 -0
- package/src/index.ts +30 -2
- package/src/settings/settings-layout.tsx +58 -34
- package/src/shell/account-menu-language.tsx +116 -0
- package/src/shell/admin-menu.tsx +117 -0
- package/src/shell/app-shell.tsx +44 -3
- package/src/shell/dev-switchers.tsx +171 -0
- package/src/shell/sync-status-indicator.tsx +301 -0
- package/src/shell/top-bar-actions.tsx +79 -0
- package/src/shell/top-bar-parts-labels.ts +133 -0
- package/src/shell/topbar-action-menu.tsx +15 -2
- package/src/shell/topbar-controls.tsx +20 -3
- package/src/shell/tour-menu.tsx +144 -0
- package/src/shell.ts +24 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
/** One offered language in the account menu's sub-list. */
|
|
3
|
+
export interface AccountMenuLanguageOption {
|
|
4
|
+
code: string;
|
|
5
|
+
/** Its name, in its own language ("Deutsch", "Français"). */
|
|
6
|
+
label: ReactNode;
|
|
7
|
+
/**
|
|
8
|
+
* A flag before the name, as the app draws it — keksdose's flag-icons span
|
|
9
|
+
* (`<span aria-hidden className="fi fi-ch …" />`). A node rather than a country code, so
|
|
10
|
+
* the kit ships no flag set; decorative, so give it `aria-hidden`.
|
|
11
|
+
*/
|
|
12
|
+
flag?: ReactNode;
|
|
13
|
+
/**
|
|
14
|
+
* Or the ISO 3166-1 alpha-2 country (lowercase) whose flag-icons flag to draw, as
|
|
15
|
+
* `LanguageMenu`'s `LanguageOption` takes it — so one language list feeds both
|
|
16
|
+
* (Kurvenschmiede's `LANGUAGES`). Needs the app's flag-icons CSS, as `LanguageMenu`
|
|
17
|
+
* does. `flag` wins when both are given.
|
|
18
|
+
*/
|
|
19
|
+
country?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface AccountMenuLanguageProps {
|
|
22
|
+
languages: readonly AccountMenuLanguageOption[];
|
|
23
|
+
/** The active language's code. */
|
|
24
|
+
value: string | undefined;
|
|
25
|
+
/** A pick: the app sets the device's language and, signed in, the account's
|
|
26
|
+
* (settings §6.2). */
|
|
27
|
+
onPick: (code: string) => void;
|
|
28
|
+
/** The account menu's `close` (from `TopBarActionMenu`'s `children`): a pick shuts it. */
|
|
29
|
+
close?: () => void;
|
|
30
|
+
/** The row's name. Default: `topBar.language` ("Language"). */
|
|
31
|
+
label?: string;
|
|
32
|
+
/** Open the sub-list at first. Default `false`. */
|
|
33
|
+
defaultOpen?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The language choice INSIDE the account menu (docs/top-bar-harmonization.md §2.3),
|
|
37
|
+
* keksdose's sub-list: a `Disclosure variant="menu"` row — the globe, "Language", the
|
|
38
|
+
* active language's flag and the chevron — that opens in place onto one checked
|
|
39
|
+
* `MenuItem` per language. Inline rather than a nested popover: a popover inside a
|
|
40
|
+
* popover fights the outside click and has nowhere to go on a phone (keksdose #140).
|
|
41
|
+
*
|
|
42
|
+
* It renders its own `<li>`: drop it into `TopBarActionMenu`'s `children` first after the
|
|
43
|
+
* app's context rows (§6.5). Signed out there is no account menu; `LanguageMenu` stays
|
|
44
|
+
* the bar's button (§2.3).
|
|
45
|
+
*/
|
|
46
|
+
export declare function AccountMenuLanguage({ languages, value, onPick, close, label, defaultOpen, }: AccountMenuLanguageProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Globe } from "lucide-react";
|
|
5
|
+
import { Disclosure } from "../components/disclosure.js";
|
|
6
|
+
import { MenuItem } from "../components/menu-item.js";
|
|
7
|
+
import { DEFAULT_TOP_BAR_LABELS, useKitLabels } from "../i18n/kit-labels.js";
|
|
8
|
+
function flagOf(lang) {
|
|
9
|
+
if (lang.flag !== void 0) return lang.flag;
|
|
10
|
+
if (!lang.country) return void 0;
|
|
11
|
+
return /* @__PURE__ */ jsx(
|
|
12
|
+
"span",
|
|
13
|
+
{
|
|
14
|
+
"aria-hidden": true,
|
|
15
|
+
"data-country": lang.country,
|
|
16
|
+
className: `fi fi-${lang.country} inline-block h-[0.9375rem] w-5 shrink-0 rounded-sm shadow-[0_0_0_1px_rgba(0,0,0,0.08)]`
|
|
17
|
+
}
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
function AccountMenuLanguage({
|
|
21
|
+
languages,
|
|
22
|
+
value,
|
|
23
|
+
onPick,
|
|
24
|
+
close,
|
|
25
|
+
label,
|
|
26
|
+
defaultOpen = false
|
|
27
|
+
}) {
|
|
28
|
+
const words = useKitLabels("topBar", DEFAULT_TOP_BAR_LABELS);
|
|
29
|
+
const [open, setOpen] = useState(defaultOpen);
|
|
30
|
+
const active = languages.find((l) => l.code === value);
|
|
31
|
+
return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
|
|
32
|
+
Disclosure,
|
|
33
|
+
{
|
|
34
|
+
variant: "menu",
|
|
35
|
+
open,
|
|
36
|
+
onOpenChange: setOpen,
|
|
37
|
+
title: /* @__PURE__ */ jsxs("span", { className: "flex items-center justify-between gap-3", children: [
|
|
38
|
+
/* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
|
|
39
|
+
/* @__PURE__ */ jsx(Globe, { "aria-hidden": true, className: "size-4" }),
|
|
40
|
+
label ?? words.language
|
|
41
|
+
] }),
|
|
42
|
+
active && flagOf(active)
|
|
43
|
+
] }),
|
|
44
|
+
children: /* @__PURE__ */ jsx("ul", { children: languages.map((lang) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
|
|
45
|
+
MenuItem,
|
|
46
|
+
{
|
|
47
|
+
checked: lang.code === value,
|
|
48
|
+
onClick: () => {
|
|
49
|
+
onPick(lang.code);
|
|
50
|
+
setOpen(false);
|
|
51
|
+
close?.();
|
|
52
|
+
},
|
|
53
|
+
leading: flagOf(lang),
|
|
54
|
+
className: "ps-9",
|
|
55
|
+
children: lang.label
|
|
56
|
+
}
|
|
57
|
+
) }, lang.code)) })
|
|
58
|
+
}
|
|
59
|
+
) });
|
|
60
|
+
}
|
|
61
|
+
export {
|
|
62
|
+
AccountMenuLanguage
|
|
63
|
+
};
|
|
64
|
+
//# sourceMappingURL=account-menu-language.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/shell/account-menu-language.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { Globe } from \"lucide-react\";\nimport { Disclosure } from \"../components/disclosure\";\nimport { MenuItem } from \"../components/menu-item\";\nimport { DEFAULT_TOP_BAR_LABELS, useKitLabels } from \"../i18n/kit-labels\";\n\n/** One offered language in the account menu's sub-list. */\nexport interface AccountMenuLanguageOption {\n code: string;\n /** Its name, in its own language (\"Deutsch\", \"Français\"). */\n label: ReactNode;\n /**\n * A flag before the name, as the app draws it — keksdose's flag-icons span\n * (`<span aria-hidden className=\"fi fi-ch …\" />`). A node rather than a country code, so\n * the kit ships no flag set; decorative, so give it `aria-hidden`.\n */\n flag?: ReactNode;\n /**\n * Or the ISO 3166-1 alpha-2 country (lowercase) whose flag-icons flag to draw, as\n * `LanguageMenu`'s `LanguageOption` takes it — so one language list feeds both\n * (Kurvenschmiede's `LANGUAGES`). Needs the app's flag-icons CSS, as `LanguageMenu`\n * does. `flag` wins when both are given.\n */\n country?: string;\n}\n\n/** The option's flag: its own node, else the flag-icons flag of its country. */\nfunction flagOf(lang: AccountMenuLanguageOption): ReactNode {\n if (lang.flag !== undefined) return lang.flag;\n if (!lang.country) return undefined;\n return (\n <span\n aria-hidden\n data-country={lang.country}\n className={`fi fi-${lang.country} inline-block h-[0.9375rem] w-5 shrink-0 rounded-sm shadow-[0_0_0_1px_rgba(0,0,0,0.08)]`}\n />\n );\n}\n\nexport interface AccountMenuLanguageProps {\n languages: readonly AccountMenuLanguageOption[];\n /** The active language's code. */\n value: string | undefined;\n /** A pick: the app sets the device's language and, signed in, the account's\n * (settings §6.2). */\n onPick: (code: string) => void;\n /** The account menu's `close` (from `TopBarActionMenu`'s `children`): a pick shuts it. */\n close?: () => void;\n /** The row's name. Default: `topBar.language` (\"Language\"). */\n label?: string;\n /** Open the sub-list at first. Default `false`. */\n defaultOpen?: boolean;\n}\n\n/**\n * The language choice INSIDE the account menu (docs/top-bar-harmonization.md §2.3),\n * keksdose's sub-list: a `Disclosure variant=\"menu\"` row — the globe, \"Language\", the\n * active language's flag and the chevron — that opens in place onto one checked\n * `MenuItem` per language. Inline rather than a nested popover: a popover inside a\n * popover fights the outside click and has nowhere to go on a phone (keksdose #140).\n *\n * It renders its own `<li>`: drop it into `TopBarActionMenu`'s `children` first after the\n * app's context rows (§6.5). Signed out there is no account menu; `LanguageMenu` stays\n * the bar's button (§2.3).\n */\nexport function AccountMenuLanguage({\n languages,\n value,\n onPick,\n close,\n label,\n defaultOpen = false,\n}: AccountMenuLanguageProps) {\n const words = useKitLabels(\"topBar\", DEFAULT_TOP_BAR_LABELS);\n const [open, setOpen] = useState(defaultOpen);\n const active = languages.find((l) => l.code === value);\n return (\n <li>\n <Disclosure\n variant=\"menu\"\n open={open}\n onOpenChange={setOpen}\n title={\n <span className=\"flex items-center justify-between gap-3\">\n <span className=\"flex items-center gap-2\">\n <Globe aria-hidden className=\"size-4\" />\n {label ?? words.language}\n </span>\n {active && flagOf(active)}\n </span>\n }\n >\n <ul>\n {languages.map((lang) => (\n <li key={lang.code}>\n <MenuItem\n checked={lang.code === value}\n onClick={() => {\n onPick(lang.code);\n setOpen(false);\n close?.();\n }}\n leading={flagOf(lang)}\n // Under the row's own text, past the globe.\n className=\"ps-9\"\n >\n {lang.label}\n </MenuItem>\n </li>\n ))}\n </ul>\n </Disclosure>\n </li>\n );\n}\n"],"mappings":";AAgCI,cAqDQ,YArDR;AAhCJ,SAAS,gBAAgB;AAEzB,SAAS,aAAa;AACtB,SAAS,kBAAkB;AAC3B,SAAS,gBAAgB;AACzB,SAAS,wBAAwB,oBAAoB;AAuBrD,SAAS,OAAO,MAA4C;AAC1D,MAAI,KAAK,SAAS,OAAW,QAAO,KAAK;AACzC,MAAI,CAAC,KAAK,QAAS,QAAO;AAC1B,SACE;AAAA,IAAC;AAAA;AAAA,MACC,eAAW;AAAA,MACX,gBAAc,KAAK;AAAA,MACnB,WAAW,SAAS,KAAK,OAAO;AAAA;AAAA,EAClC;AAEJ;AA4BO,SAAS,oBAAoB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAChB,GAA6B;AAC3B,QAAM,QAAQ,aAAa,UAAU,sBAAsB;AAC3D,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,WAAW;AAC5C,QAAM,SAAS,UAAU,KAAK,CAAC,MAAM,EAAE,SAAS,KAAK;AACrD,SACE,oBAAC,QACC;AAAA,IAAC;AAAA;AAAA,MACC,SAAQ;AAAA,MACR;AAAA,MACA,cAAc;AAAA,MACd,OACE,qBAAC,UAAK,WAAU,2CACd;AAAA,6BAAC,UAAK,WAAU,2BACd;AAAA,8BAAC,SAAM,eAAW,MAAC,WAAU,UAAS;AAAA,UACrC,SAAS,MAAM;AAAA,WAClB;AAAA,QACC,UAAU,OAAO,MAAM;AAAA,SAC1B;AAAA,MAGF,8BAAC,QACE,oBAAU,IAAI,CAAC,SACd,oBAAC,QACC;AAAA,QAAC;AAAA;AAAA,UACC,SAAS,KAAK,SAAS;AAAA,UACvB,SAAS,MAAM;AACb,mBAAO,KAAK,IAAI;AAChB,oBAAQ,KAAK;AACb,oBAAQ;AAAA,UACV;AAAA,UACA,SAAS,OAAO,IAAI;AAAA,UAEpB,WAAU;AAAA,UAET,eAAK;AAAA;AAAA,MACR,KAbO,KAAK,IAcd,CACD,GACH;AAAA;AAAA,EACF,GACF;AAEJ;","names":[]}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type LabelOverride } from "../i18n/kit-labels.js";
|
|
2
|
+
import type { AdminAreaLabels } from "../admin/admin-area-labels.js";
|
|
3
|
+
import type { TopBarMenuEntry } from "./topbar-action-menu.js";
|
|
4
|
+
export interface AdminMenuEntryOptions {
|
|
5
|
+
/** The admin area. Default `/admin`. */
|
|
6
|
+
href?: string;
|
|
7
|
+
/**
|
|
8
|
+
* The waiting count (§2.4 decision 15, §6.3): only work that waits on this admin —
|
|
9
|
+
* `adminWaitingCount(attention, […])`. `0`, `null` or none shows no badge.
|
|
10
|
+
*/
|
|
11
|
+
badge?: number | null;
|
|
12
|
+
/** The resolved `adminArea` words (`useAdminAreaLabels()`); default English. Builds no
|
|
13
|
+
* hook, so the entry can be made in the list of entries. */
|
|
14
|
+
labels?: AdminAreaLabels;
|
|
15
|
+
/** The entry's `key`. Default `"administration"`. */
|
|
16
|
+
key?: string;
|
|
17
|
+
/** The page already open: `aria-current` and a check mark. */
|
|
18
|
+
current?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The "Administration" {@link TopBarMenuEntry}: a link row with the `ShieldCheck` icon,
|
|
22
|
+
* the kit's word and the waiting count as its badge. Put it in the entries for an admin
|
|
23
|
+
* only — the menu closes on the way, as any link row:
|
|
24
|
+
*
|
|
25
|
+
* entries={[…, ...(isAdmin ? [adminMenuEntry({ badge: waiting, labels })] : []), …]}
|
|
26
|
+
*/
|
|
27
|
+
export declare function adminMenuEntry({ href, badge, labels, key, current, }?: AdminMenuEntryOptions): TopBarMenuEntry;
|
|
28
|
+
export interface AdminMenuRowProps {
|
|
29
|
+
/** The admin area. Default `/admin`. */
|
|
30
|
+
href?: string;
|
|
31
|
+
/** As {@link AdminMenuEntryOptions.badge}. */
|
|
32
|
+
badge?: number | null;
|
|
33
|
+
/** Runs on the way — `HoverMenu`'s `close`. */
|
|
34
|
+
onNavigate?: () => void;
|
|
35
|
+
current?: boolean;
|
|
36
|
+
/** Prop > `<UiKitProvider labels={{ adminArea }}>` > English. */
|
|
37
|
+
labels?: LabelOverride<AdminAreaLabels>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The same row as {@link adminMenuEntry} for a menu drawn from `HoverMenu` children: the
|
|
41
|
+
* `<li>` holding the router-link {@link MenuItem} `TopBarActionMenu` draws for a link
|
|
42
|
+
* entry, so the two menus look alike.
|
|
43
|
+
*
|
|
44
|
+
* {(close) => <ul>… {isAdmin && <AdminMenuRow badge={waiting} onNavigate={close} />} …</ul>}
|
|
45
|
+
*/
|
|
46
|
+
export declare function AdminMenuRow({ href, badge, onNavigate, current, labels: labelsProp }: AdminMenuRowProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Link } from "react-router";
|
|
4
|
+
import { ShieldCheck } from "lucide-react";
|
|
5
|
+
import { Chip } from "../components/chip.js";
|
|
6
|
+
import { MenuItem } from "../components/menu-item.js";
|
|
7
|
+
import { useKitLabels } from "../i18n/kit-labels.js";
|
|
8
|
+
import { DEFAULT_ADMIN_AREA_LABELS } from "../admin/admin-area-labels.js";
|
|
9
|
+
function badgeText(badge) {
|
|
10
|
+
if (typeof badge !== "number" || !(badge > 0)) return null;
|
|
11
|
+
return badge > 99 ? "99+" : String(Math.floor(badge));
|
|
12
|
+
}
|
|
13
|
+
function WaitingBadge({ badge, labels }) {
|
|
14
|
+
const text = badgeText(badge);
|
|
15
|
+
if (text === null) return null;
|
|
16
|
+
return /* @__PURE__ */ jsxs(Chip, { size: "sm", variant: "solid", tone: "warning", "data-slot": "admin-badge", children: [
|
|
17
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": true, children: text }),
|
|
18
|
+
/* @__PURE__ */ jsxs("span", { className: "sr-only-fixed", children: [
|
|
19
|
+
", ",
|
|
20
|
+
labels.waiting(badge)
|
|
21
|
+
] })
|
|
22
|
+
] });
|
|
23
|
+
}
|
|
24
|
+
function adminMenuEntry({
|
|
25
|
+
href = "/admin",
|
|
26
|
+
badge,
|
|
27
|
+
labels = DEFAULT_ADMIN_AREA_LABELS,
|
|
28
|
+
key = "administration",
|
|
29
|
+
current
|
|
30
|
+
} = {}) {
|
|
31
|
+
return {
|
|
32
|
+
kind: "link",
|
|
33
|
+
key,
|
|
34
|
+
to: href,
|
|
35
|
+
icon: /* @__PURE__ */ jsx(ShieldCheck, { className: "size-4", "aria-hidden": true }),
|
|
36
|
+
label: labels.title,
|
|
37
|
+
trailing: badgeText(badge) === null ? void 0 : /* @__PURE__ */ jsx(WaitingBadge, { badge, labels }),
|
|
38
|
+
current
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function AdminMenuRow({ href = "/admin", badge, onNavigate, current, labels: labelsProp }) {
|
|
42
|
+
const labels = useKitLabels("adminArea", DEFAULT_ADMIN_AREA_LABELS, labelsProp);
|
|
43
|
+
return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
|
|
44
|
+
MenuItem,
|
|
45
|
+
{
|
|
46
|
+
href,
|
|
47
|
+
renderLink: ({ href: to, ...p }) => /* @__PURE__ */ jsx(Link, { to, ...p }),
|
|
48
|
+
onClick: onNavigate ? () => onNavigate() : void 0,
|
|
49
|
+
leading: /* @__PURE__ */ jsx(ShieldCheck, { className: "size-4", "aria-hidden": true }),
|
|
50
|
+
trailing: badgeText(badge) === null ? void 0 : /* @__PURE__ */ jsx(WaitingBadge, { badge, labels }),
|
|
51
|
+
current,
|
|
52
|
+
children: labels.title
|
|
53
|
+
}
|
|
54
|
+
) });
|
|
55
|
+
}
|
|
56
|
+
export {
|
|
57
|
+
AdminMenuRow,
|
|
58
|
+
adminMenuEntry
|
|
59
|
+
};
|
|
60
|
+
//# sourceMappingURL=admin-menu.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/shell/admin-menu.tsx"],"sourcesContent":["import { Link } from \"react-router\";\nimport { ShieldCheck } from \"lucide-react\";\nimport { Chip } from \"../components/chip\";\nimport { MenuItem } from \"../components/menu-item\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { DEFAULT_ADMIN_AREA_LABELS } from \"../admin/admin-area-labels\";\nimport type { AdminAreaLabels } from \"../admin/admin-area-labels\";\nimport type { TopBarMenuEntry } from \"./topbar-action-menu\";\n\n/**\n * The account menu's \"Administration\" row (0.42.0, docs/admin-harmonization.md §2.1, §6.6,\n * §7): the way into the admin area everyone finds, in every app, for an admin only. Two\n * forms of one row — {@link adminMenuEntry} for `TopBarActionMenu`'s `entries`, and\n * {@link AdminMenuRow} for a menu built from `HoverMenu` children (Kurvenschmiede's).\n */\n\n/** The badge's text: nothing for none or 0, \"99+\" past 99. */\nfunction badgeText(badge: number | null | undefined): string | null {\n if (typeof badge !== \"number\" || !(badge > 0)) return null;\n return badge > 99 ? \"99+\" : String(Math.floor(badge));\n}\n\n/**\n * The waiting count at the row's end: the `solid` count pill of {@link Chip}, in the\n * pending count's tone, with the number for the eye and `adminArea.waiting` for a reader\n * (\"Administration, 3 waiting\"). `null` for no count.\n */\nfunction WaitingBadge({ badge, labels }: { badge: number | null | undefined; labels: AdminAreaLabels }) {\n const text = badgeText(badge);\n if (text === null) return null;\n return (\n <Chip size=\"sm\" variant=\"solid\" tone=\"warning\" data-slot=\"admin-badge\">\n <span aria-hidden>{text}</span>\n {/* The comma parts it from the row's label in the name: \"Administration, 3 waiting\". */}\n <span className=\"sr-only-fixed\">, {labels.waiting(badge as number)}</span>\n </Chip>\n );\n}\n\nexport interface AdminMenuEntryOptions {\n /** The admin area. Default `/admin`. */\n href?: string;\n /**\n * The waiting count (§2.4 decision 15, §6.3): only work that waits on this admin —\n * `adminWaitingCount(attention, […])`. `0`, `null` or none shows no badge.\n */\n badge?: number | null;\n /** The resolved `adminArea` words (`useAdminAreaLabels()`); default English. Builds no\n * hook, so the entry can be made in the list of entries. */\n labels?: AdminAreaLabels;\n /** The entry's `key`. Default `\"administration\"`. */\n key?: string;\n /** The page already open: `aria-current` and a check mark. */\n current?: boolean;\n}\n\n/**\n * The \"Administration\" {@link TopBarMenuEntry}: a link row with the `ShieldCheck` icon,\n * the kit's word and the waiting count as its badge. Put it in the entries for an admin\n * only — the menu closes on the way, as any link row:\n *\n * entries={[…, ...(isAdmin ? [adminMenuEntry({ badge: waiting, labels })] : []), …]}\n */\nexport function adminMenuEntry({\n href = \"/admin\",\n badge,\n labels = DEFAULT_ADMIN_AREA_LABELS,\n key = \"administration\",\n current,\n}: AdminMenuEntryOptions = {}): TopBarMenuEntry {\n return {\n kind: \"link\",\n key,\n to: href,\n icon: <ShieldCheck className=\"size-4\" aria-hidden />,\n label: labels.title,\n trailing: badgeText(badge) === null ? undefined : <WaitingBadge badge={badge} labels={labels} />,\n current,\n };\n}\n\nexport interface AdminMenuRowProps {\n /** The admin area. Default `/admin`. */\n href?: string;\n /** As {@link AdminMenuEntryOptions.badge}. */\n badge?: number | null;\n /** Runs on the way — `HoverMenu`'s `close`. */\n onNavigate?: () => void;\n current?: boolean;\n /** Prop > `<UiKitProvider labels={{ adminArea }}>` > English. */\n labels?: LabelOverride<AdminAreaLabels>;\n}\n\n/**\n * The same row as {@link adminMenuEntry} for a menu drawn from `HoverMenu` children: the\n * `<li>` holding the router-link {@link MenuItem} `TopBarActionMenu` draws for a link\n * entry, so the two menus look alike.\n *\n * {(close) => <ul>… {isAdmin && <AdminMenuRow badge={waiting} onNavigate={close} />} …</ul>}\n */\nexport function AdminMenuRow({ href = \"/admin\", badge, onNavigate, current, labels: labelsProp }: AdminMenuRowProps) {\n const labels = useKitLabels(\"adminArea\", DEFAULT_ADMIN_AREA_LABELS, labelsProp);\n return (\n <li>\n <MenuItem\n href={href}\n renderLink={({ href: to, ...p }) => <Link to={to} {...p} />}\n onClick={onNavigate ? () => onNavigate() : undefined}\n leading={<ShieldCheck className=\"size-4\" aria-hidden />}\n trailing={badgeText(badge) === null ? undefined : <WaitingBadge badge={badge} labels={labels} />}\n current={current}\n >\n {labels.title}\n </MenuItem>\n </li>\n );\n}\n"],"mappings":";AAgCM,cAEA,YAFA;AAhCN,SAAS,YAAY;AACrB,SAAS,mBAAmB;AAC5B,SAAS,YAAY;AACrB,SAAS,gBAAgB;AACzB,SAAS,oBAAwC;AACjD,SAAS,iCAAiC;AAY1C,SAAS,UAAU,OAAiD;AAClE,MAAI,OAAO,UAAU,YAAY,EAAE,QAAQ,GAAI,QAAO;AACtD,SAAO,QAAQ,KAAK,QAAQ,OAAO,KAAK,MAAM,KAAK,CAAC;AACtD;AAOA,SAAS,aAAa,EAAE,OAAO,OAAO,GAAkE;AACtG,QAAM,OAAO,UAAU,KAAK;AAC5B,MAAI,SAAS,KAAM,QAAO;AAC1B,SACE,qBAAC,QAAK,MAAK,MAAK,SAAQ,SAAQ,MAAK,WAAU,aAAU,eACvD;AAAA,wBAAC,UAAK,eAAW,MAAE,gBAAK;AAAA,IAExB,qBAAC,UAAK,WAAU,iBAAgB;AAAA;AAAA,MAAG,OAAO,QAAQ,KAAe;AAAA,OAAE;AAAA,KACrE;AAEJ;AA0BO,SAAS,eAAe;AAAA,EAC7B,OAAO;AAAA,EACP;AAAA,EACA,SAAS;AAAA,EACT,MAAM;AAAA,EACN;AACF,IAA2B,CAAC,GAAoB;AAC9C,SAAO;AAAA,IACL,MAAM;AAAA,IACN;AAAA,IACA,IAAI;AAAA,IACJ,MAAM,oBAAC,eAAY,WAAU,UAAS,eAAW,MAAC;AAAA,IAClD,OAAO,OAAO;AAAA,IACd,UAAU,UAAU,KAAK,MAAM,OAAO,SAAY,oBAAC,gBAAa,OAAc,QAAgB;AAAA,IAC9F;AAAA,EACF;AACF;AAqBO,SAAS,aAAa,EAAE,OAAO,UAAU,OAAO,YAAY,SAAS,QAAQ,WAAW,GAAsB;AACnH,QAAM,SAAS,aAAa,aAAa,2BAA2B,UAAU;AAC9E,SACE,oBAAC,QACC;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,YAAY,CAAC,EAAE,MAAM,IAAI,GAAG,EAAE,MAAM,oBAAC,QAAK,IAAS,GAAG,GAAG;AAAA,MACzD,SAAS,aAAa,MAAM,WAAW,IAAI;AAAA,MAC3C,SAAS,oBAAC,eAAY,WAAU,UAAS,eAAW,MAAC;AAAA,MACrD,UAAU,UAAU,KAAK,MAAM,OAAO,SAAY,oBAAC,gBAAa,OAAc,QAAgB;AAAA,MAC9F;AAAA,MAEC,iBAAO;AAAA;AAAA,EACV,GACF;AAEJ;","names":[]}
|
|
@@ -67,6 +67,16 @@ export type AppShellSidebarFooterItem = {
|
|
|
67
67
|
hint?: string;
|
|
68
68
|
/** Leaves the app: a plain `<a target="_blank" rel="noopener noreferrer">`. */
|
|
69
69
|
external?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* 0.42.0 (docs/admin-harmonization.md §6.9): a count of what waits behind the link —
|
|
72
|
+
* kastlan's "Administration" with its To-triage count. A pill at the end of the
|
|
73
|
+
* expanded link, a corner dot on the collapsed icon; `0`, `null` or none shows
|
|
74
|
+
* nothing, and above 99 it reads "99+". The count joins the link's name, in
|
|
75
|
+
* `badgeLabel`'s words when given ("Administration, 3 waiting").
|
|
76
|
+
*/
|
|
77
|
+
badge?: number | null;
|
|
78
|
+
/** The badge's words for a reader — `adminArea.waiting(count)`. Default: the number. */
|
|
79
|
+
badgeLabel?: string;
|
|
70
80
|
} | {
|
|
71
81
|
kind: "links";
|
|
72
82
|
key: string;
|
package/dist/shell/app-shell.js
CHANGED
|
@@ -235,6 +235,11 @@ function AppShell({
|
|
|
235
235
|
const routerLink = ({ href, ...p }) => /* @__PURE__ */ jsx(Link, { to: href, ...p });
|
|
236
236
|
const plainLink = ({ children, ...p }) => /* @__PURE__ */ jsx("a", { ...p, children });
|
|
237
237
|
const FOOTER_ICON_LINK = "flex min-h-8 items-center justify-center rounded-md text-muted hover:bg-hover hover:text-secondary";
|
|
238
|
+
function footerBadgeText(badge) {
|
|
239
|
+
if (typeof badge !== "number" || !(badge > 0)) return null;
|
|
240
|
+
return badge > 99 ? "99+" : String(Math.floor(badge));
|
|
241
|
+
}
|
|
242
|
+
const FOOTER_BADGE = "inline-flex h-4 min-w-4 shrink-0 items-center justify-center rounded-full bg-warning px-1 text-micro font-semibold leading-4 tabular-nums text-warning-contrast";
|
|
238
243
|
function RenderedLink({ render, ...props }) {
|
|
239
244
|
return render(props);
|
|
240
245
|
}
|
|
@@ -253,10 +258,15 @@ function SidebarFooterItems({
|
|
|
253
258
|
const to = item.kind === "links" ? item.to ?? item.links[0]?.to : item.to;
|
|
254
259
|
if (!to) return null;
|
|
255
260
|
const external = item.kind === "links" ? item.links.find((l) => l.to === to)?.external : item.external;
|
|
261
|
+
const count2 = item.kind === "links" ? null : footerBadgeText(item.badge);
|
|
262
|
+
const spoken = count2 === null || item.kind === "links" ? null : item.badgeLabel ?? count2;
|
|
256
263
|
return /* @__PURE__ */ jsx(Tooltip, { label: item.label, side: "end", portal: true, className: "block", children: linkTo(to, external, {
|
|
257
|
-
"aria-label": item.label,
|
|
258
|
-
className: FOOTER_ICON_LINK,
|
|
259
|
-
children: /* @__PURE__ */
|
|
264
|
+
"aria-label": spoken === null ? item.label : `${item.label}, ${spoken}`,
|
|
265
|
+
className: cn(FOOTER_ICON_LINK, count2 !== null && "relative"),
|
|
266
|
+
children: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
267
|
+
/* @__PURE__ */ jsx(item.icon, { className: "size-4", "aria-hidden": true }),
|
|
268
|
+
count2 !== null && /* @__PURE__ */ jsx("span", { "aria-hidden": true, "data-badge": "", className: cn(FOOTER_BADGE, "absolute -top-0.5 end-0.5"), children: count2 })
|
|
269
|
+
] })
|
|
260
270
|
}) }, item.key);
|
|
261
271
|
}
|
|
262
272
|
if (item.kind === "links") {
|
|
@@ -273,11 +283,19 @@ function SidebarFooterItems({
|
|
|
273
283
|
item.key
|
|
274
284
|
);
|
|
275
285
|
}
|
|
286
|
+
const count = footerBadgeText(item.badge);
|
|
276
287
|
const link = linkTo(item.to, item.external, {
|
|
277
288
|
className: cn(FOOTER_ICON_LINK, "gap-1.5 text-xs font-medium"),
|
|
278
289
|
children: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
279
290
|
/* @__PURE__ */ jsx(item.icon, { className: "size-3.5 shrink-0", "aria-hidden": true }),
|
|
280
|
-
/* @__PURE__ */ jsx("span", { className: "min-w-0 truncate-until-large", children: item.text ?? item.label })
|
|
291
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 truncate-until-large", children: item.text ?? item.label }),
|
|
292
|
+
count !== null && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
293
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": true, "data-badge": "", className: FOOTER_BADGE, children: count }),
|
|
294
|
+
/* @__PURE__ */ jsxs("span", { className: "sr-only", children: [
|
|
295
|
+
", ",
|
|
296
|
+
item.badgeLabel ?? count
|
|
297
|
+
] })
|
|
298
|
+
] })
|
|
281
299
|
] })
|
|
282
300
|
});
|
|
283
301
|
return item.hint ? /* @__PURE__ */ jsx(Tooltip, { label: item.hint, side: "top", portal: true, className: "block", children: link }, item.key) : /* @__PURE__ */ jsx("div", { children: link }, item.key);
|