@eifi1/ui-kit 0.40.1 → 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/components/nav-pills.d.ts +2 -2
- package/dist/components/nav-pills.js.map +1 -1
- package/dist/components/tabs.d.ts +4 -32
- package/dist/components/tabs.js +30 -66
- package/dist/components/tabs.js.map +1 -1
- package/dist/components/translation-review-parts.d.ts +2 -2
- package/dist/components/translation-review-parts.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/feedback/feedback-menu-labels.js +0 -2
- package/dist/feedback/feedback-menu-labels.js.map +1 -1
- package/dist/feedback/feedback-menu.d.ts +0 -6
- package/dist/feedback/feedback-menu.js.map +1 -1
- package/dist/i18n/defaults.js +5 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.js +79 -2
- 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 -3
- package/dist/i18n/locales/en.js.map +3 -3
- package/dist/i18n/locales/es.js +76 -2
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +75 -2
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +76 -2
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +76 -2
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +75 -2
- 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/components/nav-pills.tsx +2 -2
- package/src/components/tabs.tsx +19 -110
- package/src/components/translation-review-parts.tsx +2 -2
- package/src/feedback/feedback-menu-labels.ts +0 -2
- package/src/feedback/feedback-menu.tsx +0 -6
- package/src/i18n/defaults.ts +4 -0
- package/src/i18n/german.ts +92 -2
- package/src/i18n/kit-labels.tsx +9 -0
- package/src/i18n/locales/en.ts +19 -0
- package/src/i18n/locales/es.ts +86 -2
- package/src/i18n/locales/fr.ts +86 -2
- package/src/i18n/locales/hu.ts +84 -2
- package/src/i18n/locales/it.ts +87 -2
- package/src/i18n/locales/zh.ts +79 -2
- 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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/billing/subscription-actions.tsx"],"sourcesContent":["import type { ComponentPropsWithRef } from \"react\";\n\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport { Button } from \"../components/button\";\nimport { useBillingLabels } from \"./billing-labels\";\nimport type { BillingLabels } from \"./billing-labels\";\nimport type { PortalTarget } from \"./billing-overview\";\n\nexport interface SubscriptionActionsProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\"> {\n /** \"Payment and invoices\", as a link — or {@link onManage}. Without either, none. */\n manageHref?: string;\n /** \"Payment and invoices\", as the app's call: `POST /billing/portal` with\n * `{target: \"overview\"}`, then off to its `url` (§4, §14.5). The kit sends no\n * request. */\n onManage?: () => void;\n /** \"Cancel subscription\", as a link — or {@link onCancel}. Without either, none. */\n cancelHref?: string;\n /** \"Cancel subscription\", as the app's call: the portal again, with `{target:\n * \"cancel\"}` — its cancel link, where the provider's cancel and withdrawal functions\n * are (§12.26, §14.5). Not the overview. */\n onCancel?: () => void;\n /**\n * Both, as one call that says where to ({@link PortalTarget}, the body of\n * `POST /billing/portal`): `\"overview\"` for \"Payment and invoices\",\n * `\"cancel\"` for \"Cancel subscription\" — so the target can't be mixed up (kastlan's\n * cancel opened the overview). {@link onManage} and {@link onCancel} win over it.\n */\n onPortal?: (target: PortalTarget) => void;\n /**\n * The overview's `at_provider` (§14.5): whether the payer has reached the provider.\n * `false`: no customer there yet, so nothing to open — a caption (`notAtProvider`)\n * stands in place of both actions. `true`: both show, WHATEVER the status or source —\n * a payer who bought under a grant keeps the way to the invoices and to \"Cancel\"\n * (§12.26). Default true (0.32's behaviour).\n */\n atProvider?: boolean;\n /**
|
|
1
|
+
{"version":3,"sources":["../../src/billing/subscription-actions.tsx"],"sourcesContent":["import type { ComponentPropsWithRef } from \"react\";\n\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport { Button } from \"../components/button\";\nimport { useBillingLabels } from \"./billing-labels\";\nimport type { BillingLabels } from \"./billing-labels\";\nimport type { BillingPortalAccess, PortalTarget } from \"./billing-overview\";\n\nexport interface SubscriptionActionsProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\"> {\n /** \"Payment and invoices\", as a link — or {@link onManage}. Without either, none. */\n manageHref?: string;\n /** \"Payment and invoices\", as the app's call: `POST /billing/portal` with\n * `{target: \"overview\"}`, then off to its `url` (§4, §14.5). The kit sends no\n * request. */\n onManage?: () => void;\n /** \"Cancel subscription\", as a link — or {@link onCancel}. Without either, none. */\n cancelHref?: string;\n /** \"Cancel subscription\", as the app's call: the portal again, with `{target:\n * \"cancel\"}` — its cancel link, where the provider's cancel and withdrawal functions\n * are (§12.26, §14.5). Not the overview. */\n onCancel?: () => void;\n /**\n * Both, as one call that says where to ({@link PortalTarget}, the body of\n * `POST /billing/portal`): `\"overview\"` for \"Payment and invoices\",\n * `\"cancel\"` for \"Cancel subscription\" — so the target can't be mixed up (kastlan's\n * cancel opened the overview). {@link onManage} and {@link onCancel} win over it.\n */\n onPortal?: (target: PortalTarget) => void;\n /**\n * The overview's `at_provider` (§14.5): whether the payer has reached the provider.\n * `false`: no customer there yet, so nothing to open — a caption (`notAtProvider`)\n * stands in place of both actions. `true`: both show, WHATEVER the status or source —\n * a payer who bought under a grant keeps the way to the invoices and to \"Cancel\"\n * (§12.26). Default true (0.32's behaviour).\n */\n atProvider?: boolean;\n /**\n * 0.42 (§16.2, server-kit 0.14): the overview's `portal`. `\"buyer_only\"` — the row has\n * a buyer and the viewer isn't them, so the portal would answer `403\n * billing_portal_buyer_only`: \"Payment and invoices\" becomes a caption naming the buyer\n * (`buyerHolds`, no button), and \"Cancel subscription\" calls {@link onCancelInApp}\n * (no portal at all — `cancelHref`, `onCancel` and `onPortal` are not used). `\"open\"`\n * (default): as before.\n */\n portal?: BillingPortalAccess;\n /** 0.42: the overview's `buyer_name`, for the caption under `\"buyer_only\"`. Empty: \"…with\n * the admin who bought the subscription\". */\n buyerName?: string | null;\n /**\n * 0.42 (§16.2 decision 2): \"Cancel subscription\" in the app — open\n * `CancelSubscriptionConfirm`, whose Confirm is `POST /billing/cancel`. The cancel under\n * `\"buyer_only\"`. Under `\"open\"` it is the last resort, used only when no portal cancel\n * (`cancelHref`, `onCancel`, `onPortal`) is given — a person-payer app that mounts the\n * route and not the portal's cancel.\n */\n onCancelInApp?: () => void;\n /** 0.42 (§16.7 C): the overview's `cancel_at_period_end` — a cancel is scheduled. With\n * {@link onResume}, \"Keep subscription\" takes the cancel's place. */\n cancelAtPeriodEnd?: boolean;\n /** 0.42 (§16.7 C): \"Keep subscription\", the app's `POST /billing/resume` — shown in\n * the cancel's place while {@link cancelAtPeriodEnd} is true. Without it, the cancel\n * stays as before. */\n onResume?: () => void;\n /** Which one is under way: its spinner, no second press. `\"resume\"` since 0.42. */\n pending?: \"manage\" | \"cancel\" | \"resume\" | null;\n /** Prop > `<UiKitProvider labels={{ billing }}>` > English. */\n labels?: LabelOverride<BillingLabels>;\n}\n\n/**\n * The two ways to the provider's hosted portal on the subscription page\n * (docs/billing-harmonization.md §2.8, §7, §12.26): \"Payment and invoices\" — the payment\n * method and the invoices live there, never in the app — and a VISIBLE \"Cancel\n * subscription\". Consumer law wants the second in plain sight: Germany's cancellation\n * button (§312k BGB) and the EU withdrawal function (Directive (EU) 2023/2673, from\n * 19 June 2026) are in the portal, and the app's page must not hide the way there behind\n * \"manage\".\n *\n * Neither takes the write lock: a read-only payer must be able to pay, leave or cancel\n * (§3.3).\n *\n * Before the payer has reached the provider (`atProvider={false}`, a trial without a\n * card, a beta) there is no portal to open: the part says so in a caption instead of\n * offering two buttons that could only end in `409 billing_not_at_provider`.\n *\n * 0.42 (§16): where the company's buyer is another admin (`portal=\"buyer_only\"`), the\n * portal stays the buyer's — \"Payment and invoices\" is a caption that names them, and\n * \"Cancel subscription\" is the in-app cancel (`onCancelInApp`). While a cancel is\n * scheduled (`cancelAtPeriodEnd`) and `onResume` is given, \"Keep subscription\" stands in\n * the cancel's place, whoever the viewer is.\n */\nexport function SubscriptionActions({\n manageHref,\n onManage,\n cancelHref,\n onCancel,\n onPortal,\n atProvider = true,\n portal = \"open\",\n buyerName,\n onCancelInApp,\n cancelAtPeriodEnd = false,\n onResume,\n pending,\n labels: labelsProp,\n className,\n ...rest\n}: SubscriptionActionsProps) {\n const labels = useBillingLabels(labelsProp);\n if (!atProvider) {\n return (\n <div {...rest} data-at-provider=\"false\" className={cn(\"text-sm text-muted\", className)}>\n <p>{labels.notAtProvider}</p>\n </div>\n );\n }\n const buyerOnly = portal === \"buyer_only\";\n const manageCall = onManage ?? (onPortal ? () => onPortal(\"overview\") : undefined);\n const portalCancel = onCancel ?? (onPortal ? () => onPortal(\"cancel\") : undefined);\n // Under \"buyer_only\" the in-app cancel only; under \"open\" the portal's first, the\n // in-app cancel only where no way to the portal's is given.\n const cancelCall = buyerOnly ? onCancelInApp : (portalCancel ?? (cancelHref === undefined ? onCancelInApp : undefined));\n const manage = buyerOnly ? (\n <p className=\"basis-full text-sm text-muted\" data-buyer-holds=\"\">\n {labels.buyerHolds(buyerName || undefined)}\n </p>\n ) : manageHref !== undefined ? (\n <Button href={manageHref} variant=\"secondary\">\n {labels.manage}\n </Button>\n ) : manageCall !== undefined ? (\n <Button type=\"button\" variant=\"secondary\" onClick={manageCall} pending={pending === \"manage\"}>\n {labels.manage}\n </Button>\n ) : null;\n const cancel =\n cancelAtPeriodEnd && onResume !== undefined ? (\n <Button type=\"button\" variant=\"link\" onClick={onResume} pending={pending === \"resume\"} data-keep-subscription=\"\">\n {labels.keepSubscription}\n </Button>\n ) : cancelHref !== undefined && !buyerOnly ? (\n <Button href={cancelHref} variant=\"link\" tone=\"danger\">\n {labels.cancel}\n </Button>\n ) : cancelCall !== undefined ? (\n <Button type=\"button\" variant=\"link\" tone=\"danger\" onClick={cancelCall} pending={pending === \"cancel\"}>\n {labels.cancel}\n </Button>\n ) : null;\n if (!manage && !cancel) return null;\n return (\n <div {...rest} className={cn(\"flex flex-wrap items-center gap-x-4 gap-y-2\", className)}>\n {manage}\n {cancel}\n </div>\n );\n}\n"],"mappings":";AAiHQ,cAuCJ,YAvCI;AA9GR,SAAS,UAAU;AACnB,SAAS,cAAc;AACvB,SAAS,wBAAwB;AAuF1B,SAAS,oBAAoB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb,SAAS;AAAA,EACT;AAAA,EACA;AAAA,EACA,oBAAoB;AAAA,EACpB;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA,GAAG;AACL,GAA6B;AAC3B,QAAM,SAAS,iBAAiB,UAAU;AAC1C,MAAI,CAAC,YAAY;AACf,WACE,oBAAC,SAAK,GAAG,MAAM,oBAAiB,SAAQ,WAAW,GAAG,sBAAsB,SAAS,GACnF,8BAAC,OAAG,iBAAO,eAAc,GAC3B;AAAA,EAEJ;AACA,QAAM,YAAY,WAAW;AAC7B,QAAM,aAAa,aAAa,WAAW,MAAM,SAAS,UAAU,IAAI;AACxE,QAAM,eAAe,aAAa,WAAW,MAAM,SAAS,QAAQ,IAAI;AAGxE,QAAM,aAAa,YAAY,gBAAiB,iBAAiB,eAAe,SAAY,gBAAgB;AAC5G,QAAM,SAAS,YACb,oBAAC,OAAE,WAAU,iCAAgC,oBAAiB,IAC3D,iBAAO,WAAW,aAAa,MAAS,GAC3C,IACE,eAAe,SACjB,oBAAC,UAAO,MAAM,YAAY,SAAQ,aAC/B,iBAAO,QACV,IACE,eAAe,SACjB,oBAAC,UAAO,MAAK,UAAS,SAAQ,aAAY,SAAS,YAAY,SAAS,YAAY,UACjF,iBAAO,QACV,IACE;AACJ,QAAM,SACJ,qBAAqB,aAAa,SAChC,oBAAC,UAAO,MAAK,UAAS,SAAQ,QAAO,SAAS,UAAU,SAAS,YAAY,UAAU,0BAAuB,IAC3G,iBAAO,kBACV,IACE,eAAe,UAAa,CAAC,YAC/B,oBAAC,UAAO,MAAM,YAAY,SAAQ,QAAO,MAAK,UAC3C,iBAAO,QACV,IACE,eAAe,SACjB,oBAAC,UAAO,MAAK,UAAS,SAAQ,QAAO,MAAK,UAAS,SAAS,YAAY,SAAS,YAAY,UAC1F,iBAAO,QACV,IACE;AACN,MAAI,CAAC,UAAU,CAAC,OAAQ,QAAO;AAC/B,SACE,qBAAC,SAAK,GAAG,MAAM,WAAW,GAAG,+CAA+C,SAAS,GAClF;AAAA;AAAA,IACA;AAAA,KACH;AAEJ;","names":[]}
|
|
@@ -2,7 +2,7 @@ import type { ReactElement, ReactNode } from "react";
|
|
|
2
2
|
import type { LucideIcon } from "lucide-react";
|
|
3
3
|
import type { DateInput, FormatDateStyle, FormatRelativeTimeOptions } from "../lib/format.js";
|
|
4
4
|
import { type LabelOverride } from "../i18n/kit-labels.js";
|
|
5
|
-
import type { ChipShape, ChipSize, ChipTone, ChipVariant } from "./chip.js";
|
|
5
|
+
import type { ChipLinkProps, ChipShape, ChipSize, ChipTone, ChipVariant } from "./chip.js";
|
|
6
6
|
import type { DataTableColumn } from "./data-table.js";
|
|
7
7
|
import { DEFAULT_ACCOUNT_STATE_LABELS } from "./account-chips-labels.js";
|
|
8
8
|
/**
|
|
@@ -40,6 +40,20 @@ export interface RoleDefinition {
|
|
|
40
40
|
tone?: ChipTone;
|
|
41
41
|
/** A lucide component or an element — Kurvenschmiede marks its admins with a shield. */
|
|
42
42
|
icon?: LucideIcon | ReactElement;
|
|
43
|
+
/**
|
|
44
|
+
* 0.42.0 (docs/admin-harmonization.md §2.1, settings §12.3): the role's tool — an
|
|
45
|
+
* admin's `/admin`, a reviewer's `/translations`. {@link AccountRoleChip} is a link to
|
|
46
|
+
* it; a role without one gets a plain chip. {@link RoleChip} ignores it: a roster's
|
|
47
|
+
* chips name a role, they are not a way in.
|
|
48
|
+
*/
|
|
49
|
+
href?: string;
|
|
50
|
+
/**
|
|
51
|
+
* 0.42.0 (keksdose build review): this role's look in {@link AccountRoleChip} — keksdose
|
|
52
|
+
* draws only ADMIN `solid` and its REVIEWER and MEMBER `soft`. Default `solid`, the
|
|
53
|
+
* header's look; the chip's own `variant` prop wins over it. {@link RoleChip} ignores
|
|
54
|
+
* it: a roster's column keeps one look, set on the column.
|
|
55
|
+
*/
|
|
56
|
+
variant?: ChipVariant;
|
|
43
57
|
}
|
|
44
58
|
/**
|
|
45
59
|
* Every role an app has, keyed by the value its API sends. Annotate the app's table
|
|
@@ -67,6 +81,33 @@ export interface RoleChipProps<R extends string = string> extends AccountChipLoo
|
|
|
67
81
|
* style hook.
|
|
68
82
|
*/
|
|
69
83
|
export declare function RoleChip<R extends string = string>({ value: role, roles, size, variant, shape, caps, className, ...rest }: RoleChipProps<R>): import("react").JSX.Element | null;
|
|
84
|
+
export interface AccountRoleChipProps<R extends string = string> extends RoleChipProps<R> {
|
|
85
|
+
/**
|
|
86
|
+
* Runs on the way to the role's tool — the account menu's `close`, so the panel does
|
|
87
|
+
* not stay open over the page it just navigated to (keksdose F7). Only called on a
|
|
88
|
+
* link chip.
|
|
89
|
+
*/
|
|
90
|
+
onNavigate?: () => void;
|
|
91
|
+
/** Your router's link for the role's `href`. Default: the `<UiKitProvider
|
|
92
|
+
* linkComponent>`, then `<a>` — {@link Chip}'s `renderLink`. */
|
|
93
|
+
renderLink?: (props: ChipLinkProps) => ReactElement;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The role in the account menu's header (0.42.0, docs/admin-harmonization.md §2.1, §7):
|
|
97
|
+
* {@link RoleChip} in keksdose's look — `solid`, `sm`, `square`, `caps`, the role's icon
|
|
98
|
+
* and tone — made for `TopBarActionMenu`'s `header.extra`:
|
|
99
|
+
*
|
|
100
|
+
* extra: (close) => <AccountRoleChip value={me.role} roles={ROLES} onNavigate={close} />
|
|
101
|
+
*
|
|
102
|
+
* A role whose {@link RoleDefinition.href} is set is a shortcut to its tool: the chip is a
|
|
103
|
+
* link (through `renderLink`, else the provider's link, as {@link Chip} draws links) and
|
|
104
|
+
* calls `onNavigate` on the click. Any other role is a plain chip — every account shows
|
|
105
|
+
* its role (§7, "Always a badge"). The words and tones are the app's vocabulary; the kit
|
|
106
|
+
* adds none. `solid` stays the default, so a role the app gives no look still reads as
|
|
107
|
+
* the header's tag; a role that should sit quieter (keksdose's MEMBER and REVIEWER) sets
|
|
108
|
+
* {@link RoleDefinition.variant} `soft` in the vocabulary. Carries `data-role` with the key.
|
|
109
|
+
*/
|
|
110
|
+
export declare function AccountRoleChip<R extends string = string>({ value: role, roles, onNavigate, renderLink, size, variant, shape, caps, className, ...rest }: AccountRoleChipProps<R>): import("react").JSX.Element | null;
|
|
70
111
|
/**
|
|
71
112
|
* The states both rosters show, each as the app's own flag maps onto it:
|
|
72
113
|
*
|
|
@@ -34,6 +34,38 @@ function RoleChip({
|
|
|
34
34
|
}
|
|
35
35
|
);
|
|
36
36
|
}
|
|
37
|
+
function AccountRoleChip({
|
|
38
|
+
value: role,
|
|
39
|
+
roles,
|
|
40
|
+
onNavigate,
|
|
41
|
+
renderLink,
|
|
42
|
+
size = "sm",
|
|
43
|
+
variant,
|
|
44
|
+
shape = "square",
|
|
45
|
+
caps = true,
|
|
46
|
+
className,
|
|
47
|
+
...rest
|
|
48
|
+
}) {
|
|
49
|
+
if (role === null || role === void 0 || role === "") return null;
|
|
50
|
+
const def = roles[role];
|
|
51
|
+
const look = {
|
|
52
|
+
...rest,
|
|
53
|
+
"data-role": role,
|
|
54
|
+
tone: def?.tone ?? "neutral",
|
|
55
|
+
icon: def?.icon,
|
|
56
|
+
size,
|
|
57
|
+
// The prop, then the role's own look, then the header's `solid`.
|
|
58
|
+
variant: variant ?? def?.variant ?? "solid",
|
|
59
|
+
shape,
|
|
60
|
+
caps,
|
|
61
|
+
className
|
|
62
|
+
};
|
|
63
|
+
const label = def?.label ?? role;
|
|
64
|
+
if (def?.href) {
|
|
65
|
+
return /* @__PURE__ */ jsx(Chip, { ...look, href: def.href, renderLink, onClick: onNavigate ? () => onNavigate() : void 0, children: label });
|
|
66
|
+
}
|
|
67
|
+
return /* @__PURE__ */ jsx(Chip, { ...look, children: label });
|
|
68
|
+
}
|
|
37
69
|
const ACCOUNT_STATE_TONES = {
|
|
38
70
|
active: "success",
|
|
39
71
|
inactive: "danger",
|
|
@@ -158,6 +190,7 @@ function dateColumn({
|
|
|
158
190
|
}
|
|
159
191
|
export {
|
|
160
192
|
ACCOUNT_STATE_TONES,
|
|
193
|
+
AccountRoleChip,
|
|
161
194
|
AccountStateChip,
|
|
162
195
|
DEFAULT_ACCOUNT_STATE_LABELS,
|
|
163
196
|
DateMark,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/account-chips.tsx"],"sourcesContent":["import type { ReactElement, ReactNode } from \"react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { formatDate, formatRelativeTime, toDate, EMPTY_FORMATTED_VALUE } from \"../lib/format\";\nimport type { DateInput, FormatDateStyle, FormatRelativeTimeOptions } from \"../lib/format\";\nimport { useKitDateFormatter, useKitLabels, useKitLocale, type LabelOverride } from \"../i18n/kit-labels\";\nimport { toLocalIso } from \"../lib/dates\";\nimport { Chip } from \"./chip\";\nimport type { ChipShape, ChipSize, ChipTone, ChipVariant } from \"./chip\";\nimport type { DataTableColumn } from \"./data-table\";\nimport { DEFAULT_ACCOUNT_STATE_LABELS } from \"./account-chips-labels\";\n\n/**\n * The parts an admin roster repeats (0.18.0) — and only those.\n *\n * Kurvenschmiede (features/admin/users-panel.tsx, allowlist-panel.tsx) and keksdose\n * (features/admin/users-panel.tsx, the allowlist card in admin-page.tsx) each built the\n * same table: name over email, a role, whether the account is active, created, last\n * login, a row of actions, and an allowlist with a note. What they share is not the\n * table — the columns around it (keksdose's plan and privacy mode, Kurvenschmiede's\n * owned-work counts and its role SELECT), the actions and every rule about who may do\n * what are each app's own. What they share is the vocabulary: both spelled the same\n * square, uppercase status chip by hand (keksdose's `Badge` with its own tone map,\n * Kurvenschmiede's inline `<Chip size=\"xs\" shape=\"square\" caps>`), and both formatted\n * their two date columns by hand (`toLocaleDateString` in one, a \"3 days ago\" helper\n * in the other). So: {@link RoleChip}, {@link AccountStateChip} and\n * {@link dateColumn}, and the roster stays the app's `DataTable`.\n */\n\n/** The chip look both rosters use: a square, uppercase status tag. Each can be changed. */\ninterface AccountChipLook {\n /** Default `sm` — keksdose's roster; Kurvenschmiede's denser lists pass `xs`. */\n size?: ChipSize;\n /** Default `soft`. `dot` for a column of them that should not outshout the names. */\n variant?: ChipVariant;\n /** Default `square`: a chip in a table cell, beside square controls. */\n shape?: ChipShape;\n /** Default true — the status-badge type. */\n caps?: boolean;\n className?: string;\n [key: `data-${string}`]: string | number | boolean | undefined;\n}\n\n/* ── RoleChip ──────────────────────────────────────────────────────────────── */\n\n/** How one role is drawn. `label` is the app's, translated. */\nexport interface RoleDefinition {\n label: string;\n tone?: ChipTone;\n /** A lucide component or an element — Kurvenschmiede marks its admins with a shield. */\n icon?: LucideIcon | ReactElement;\n}\n\n/**\n * Every role an app has, keyed by the value its API sends. Annotate the app's table\n * with it (`const ROLES: RoleVocabulary<UserRole> = {…}`) and TypeScript asks for a\n * label when a role is added.\n */\nexport type RoleVocabulary<R extends string = string> = Readonly<Record<R, RoleDefinition>>;\n\nexport interface RoleChipProps<R extends string = string> extends AccountChipLook {\n /**\n * The role key as the API sends it. `null` / `undefined` renders nothing. Named\n * `value`, not `role`, because `role` on a JSX element is the ARIA role — the lint\n * rule and every reader would take it for one.\n */\n value: R | null | undefined;\n /** What each key is called and how it is toned. A key missing here is shown as the\n * key itself, in `neutral`: an unknown role is still a fact the admin should see. */\n roles: Readonly<Partial<Record<R, RoleDefinition>>>;\n}\n\n/**\n * A role as a chip: the API's key looked up in the app's {@link RoleVocabulary}.\n *\n * The vocabulary is a prop and not kit English, because the roles are the app's —\n * Kurvenschmiede has admin, member and customer, keksdose admin and user, kastlan its\n * own — and so are their names. Carries `data-role` with the key, for a test or a\n * style hook.\n */\nexport function RoleChip<R extends string = string>({\n value: role,\n roles,\n size = \"sm\",\n variant,\n shape = \"square\",\n caps = true,\n className,\n ...rest\n}: RoleChipProps<R>) {\n if (role === null || role === undefined || role === \"\") return null;\n const def = roles[role];\n return (\n <Chip\n {...rest}\n data-role={role}\n tone={def?.tone ?? \"neutral\"}\n icon={def?.icon}\n size={size}\n variant={variant}\n shape={shape}\n caps={caps}\n className={className}\n >\n {def?.label ?? role}\n </Chip>\n );\n}\n\n/* ── AccountStateChip ──────────────────────────────────────────────────────── */\n\n/**\n * The states both rosters show, each as the app's own flag maps onto it:\n *\n * - `active` / `inactive` — the account may sign in, or an admin switched it off\n * (both apps' `is_active`).\n * - `invited` — on the allowlist and mailed, not registered yet (an allowlist row).\n * - `registered` — an allowlist row whose address has an account now (Kurvenschmiede).\n * - `unverified` — the email address is not confirmed (keksdose).\n * - `passwordChange` — must set a new password at the next sign-in (keksdose).\n * - `deletion` — 0.30.0: the user asked for the account to be deleted\n * (docs/user-admin-harmonization.md §2.1, §6.4). It is deactivated at once and erased\n * on a date (`after_days`: keksdose, Kurvenschmiede) or by an operator (kastlan). The\n * chip says the date when there is one ({@link AccountStateChipProps.date}), and\n * \"Deletion requested\" when there is none. It stands IN PLACE of `inactive`: an\n * account marked for deletion is always deactivated, and two chips would say it twice.\n *\n * An account can be several at once (inactive AND unverified): render one chip per\n * state, as keksdose's state column does.\n */\nexport type AccountState =\n | \"active\"\n | \"inactive\"\n | \"invited\"\n | \"registered\"\n | \"unverified\"\n | \"passwordChange\"\n | \"deletion\";\n\n/**\n * The `accountState` namespace. One word per state, plus the two forms of `deletion`.\n *\n * The 0.30 keys are OPTIONAL, like every key added to a namespace an app annotates\n * (`const L: AccountStateLabels = {…}`): a new required key would be a compile error in\n * each app on its next update. Left out, they fall back to the provider's, then English.\n */\nexport interface AccountStateLabels extends Record<Exclude<AccountState, \"deletion\">, string> {\n /** 0.30.0: an account marked for deletion with no date — kastlan's `operator` mode,\n * where an operator erases it (§6.4). \"Deletion requested\". */\n deletion?: string;\n /** 0.30.0: an account that will be erased on `date`, already formatted in the\n * reader's locale — the `after_days` mode. \"Deletion on 6 Nov 2026\". */\n deletionOn?: (date: string) => string;\n}\n\n// Defined in ./account-chips-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_ACCOUNT_STATE_LABELS };\n\n/** The default tone of each state: green for the normal case, red for switched off\n * (and for going, `deletion`), amber for something the user still has to do, blue for\n * an open invitation. */\nexport const ACCOUNT_STATE_TONES: Readonly<Record<AccountState, ChipTone>> = {\n active: \"success\",\n inactive: \"danger\",\n invited: \"info\",\n registered: \"neutral\",\n unverified: \"warning\",\n passwordChange: \"warning\",\n // Red like `inactive`, which it replaces: the account cannot sign in, and it is going.\n deletion: \"danger\",\n};\n\nexport interface AccountStateChipProps extends AccountChipLook {\n state: AccountState;\n /**\n * 0.30.0, `state=\"deletion\"` only: the day the account will be erased — the row's\n * `deletion_scheduled_at` (§3.2, §6.4). Formatted as a date in the reader's locale\n * (\"Deletion on 6 Nov 2026\"), through the provider's `formatDate` when the app set\n * one, as {@link DateMark} does. Missing or `null` — an operator erases it, so there\n * is no date — the chip says \"Deletion requested\". Ignored for every other state.\n */\n date?: DateInput;\n /** Over the provider's locale, for the `deletion` date. */\n locale?: string;\n /** Over {@link ACCOUNT_STATE_TONES} — Kurvenschmiede draws `inactive` neutral. */\n tone?: ChipTone;\n /** Over the label, for an app whose word for the state is its own (\"Deactivated\"). */\n children?: ReactNode;\n /** Prop > `<UiKitProvider labels={{ accountState }}>` > English. */\n labels?: LabelOverride<AccountStateLabels>;\n}\n\n/**\n * An account's state as a chip, in the kit's words and tones. Carries `data-state`\n * with the key.\n */\nexport function AccountStateChip({\n state,\n date,\n locale: localeProp,\n tone,\n children,\n labels: labelsProp,\n size = \"sm\",\n variant,\n shape = \"square\",\n caps = true,\n className,\n ...rest\n}: AccountStateChipProps) {\n // An `undefined` in the prop is skipped (the provider's merge does that), so an\n // optional value cannot blank a default.\n const labels = useKitLabels(\"accountState\", DEFAULT_ACCOUNT_STATE_LABELS, labelsProp);\n const locale = useKitLocale(localeProp);\n const fromProvider = useKitDateFormatter();\n const when = state === \"deletion\" ? toDate(date ?? null) : null;\n let text: ReactNode;\n if (state !== \"deletion\") text = labels[state];\n else if (when) {\n // A date in a SENTENCE, so no weekday (keksdose's rule, KitDateFormatContext).\n const day =\n (fromProvider && fromProvider(toLocalIso(when), { unit: \"day\", source: \"dateMark\", locale, weekday: false })) ||\n formatDate(when, \"medium\", { locale });\n text = (labels.deletionOn ?? DEFAULT_ACCOUNT_STATE_LABELS.deletionOn)(day);\n } else text = labels.deletion ?? DEFAULT_ACCOUNT_STATE_LABELS.deletion;\n return (\n <Chip\n {...rest}\n data-state={state}\n data-date={when ? toLocalIso(when) : undefined}\n tone={tone ?? ACCOUNT_STATE_TONES[state]}\n size={size}\n variant={variant}\n shape={shape}\n caps={caps}\n className={className}\n >\n {children ?? text}\n </Chip>\n );\n}\n\n/* ── Date columns ──────────────────────────────────────────────────────────── */\n\nexport interface DateMarkProps {\n value: DateInput;\n /**\n * `\"date\"` (default): the date in `dateStyle` — \"Created\". `\"relative\"`: how long\n * ago — \"Last login\", where the question is \"recently?\" — with the full date and\n * time beside it (keksdose's last-seen column; see `absolute`).\n */\n display?: \"date\" | \"relative\";\n /**\n * Where a `\"relative\"` mark puts the exact date and time (0.32,\n * docs/text-size-harmonization.md §4: \"relative dates show the absolute one beside\n * them\", at every size — a touch screen never opens the tooltip it used to live in):\n * `\"below\"` (default), a muted second line under the words, so a table column keeps\n * its width; `\"inline\"`, after them on the same line, for a sentence or a wide cell.\n */\n absolute?: \"below\" | \"inline\";\n /** Default `medium`. Under `\"relative\"`, the style of the exact date beside it (default\n * `dateTime`) and of the date `relative.absoluteAfterDays` switches to. Given, it wins\n * over the provider's `formatDate`. */\n dateStyle?: FormatDateStyle | Intl.DateTimeFormatOptions;\n /** `formatRelativeTime` options: `numeric`, `style`, `absoluteAfterDays`, `now`. */\n relative?: Omit<FormatRelativeTimeOptions, \"locale\" | \"empty\" | \"absoluteStyle\">;\n /** What a missing value reads as. Default \"—\"; a last-login column passes its\n * translated \"Never\". */\n empty?: ReactNode;\n /** Over the provider's locale. */\n locale?: string;\n className?: string;\n}\n\n/**\n * A date in a table cell: a `<time>` with a machine-readable `dateTime`, nowrap and\n * tabular figures, in the provider's locale — or `empty` when there is none.\n *\n * `display=\"date\"` without a `dateStyle` is written by `<UiKitProvider formatDate>` when\n * the app set one (keksdose K12: its `DateCell` exists to put every column's date in the\n * app's date-format preference, weekday and all — dev#546), called with the LOCAL\n * calendar day of the value as `\"YYYY-MM-DD\"` and `weekday: true`. Without one, or for\n * an empty answer, it is `Intl`'s `medium` date as before. The relative display is not\n * a calendar day and keeps `formatRelativeTime`.\n */\nexport function DateMark({\n value,\n display = \"date\",\n dateStyle,\n relative,\n absolute = \"below\",\n empty = EMPTY_FORMATTED_VALUE,\n locale: localeProp,\n className,\n}: DateMarkProps) {\n const locale = useKitLocale(localeProp);\n const fromProvider = useKitDateFormatter();\n const date = toDate(value);\n const cls = cn(\"whitespace-nowrap tabular-nums\", className);\n if (!date) return <span className={cn(cls, \"text-muted\")}>{empty}</span>;\n if (display === \"date\") {\n const own =\n dateStyle === undefined && fromProvider\n ? fromProvider(toLocalIso(date), { unit: \"day\", source: \"dateMark\", locale, weekday: true })\n : \"\";\n return (\n <time dateTime={date.toISOString()} className={cls}>\n {own || formatDate(date, dateStyle ?? \"medium\", { locale })}\n </time>\n );\n }\n const text = formatRelativeTime(date, { ...relative, locale, absoluteStyle: dateStyle ?? \"medium\" });\n const exact = formatDate(date, dateStyle ?? \"dateTime\", { locale });\n // Past `absoluteAfterDays` the relative words already ARE the date; then the exact\n // line alone, rather than the same day twice.\n const sameDay = text === formatDate(date, dateStyle ?? \"medium\", { locale });\n const iso = date.toISOString();\n if (sameDay) {\n return (\n <time dateTime={iso} className={cls}>\n {exact}\n </time>\n );\n }\n // The exact date and time in the layout, never only in a tooltip (§4). Still no tab\n // stop: a roster of fifty rows would be fifty more of them.\n return (\n <time\n dateTime={iso}\n data-slot=\"date-mark\"\n className={cn(absolute === \"below\" ? \"inline-flex flex-col leading-tight\" : \"inline\", className)}\n >\n <span className=\"whitespace-nowrap tabular-nums\">{text}</span>\n {absolute === \"inline\" && \" · \"}\n <span className={cn(\"whitespace-nowrap tabular-nums text-muted\", absolute === \"below\" && \"text-xs\")}>\n {exact}\n </span>\n </time>\n );\n}\n\nexport interface DateColumnOptions<T> extends Omit<DateMarkProps, \"value\" | \"className\"> {\n key: string;\n header: ReactNode;\n /** The value of this row: an ISO string, epoch ms, a Date, or null for none. */\n value: (row: T) => DateInput;\n /** Sortable by the instant; rows with no date sort last either way. Default true. */\n sortable?: boolean;\n /** Default `desc` — a date column is sorted to find the newest. */\n firstSort?: \"asc\" | \"desc\";\n /** Add the table's from–to date filter over the column. Default false. */\n filter?: boolean;\n /** Further column settings (`mobileHidden`, `headClassName`, …), merged over. */\n column?: Partial<DataTableColumn<T>>;\n}\n\n/**\n * A {@link DataTableColumn} for a date: {@link DateMark} cells, sorted by the instant\n * (newest first on the first click), optionally the date filter. The roster's\n * \"Created\" is `dateColumn({ key, header, value })`; its \"Last login\" adds\n * `display: \"relative\", empty: t(\"never\")`.\n */\nexport function dateColumn<T>({\n key,\n header,\n value,\n sortable = true,\n firstSort = \"desc\",\n filter = false,\n column,\n ...mark\n}: DateColumnOptions<T>): DataTableColumn<T> {\n return {\n key,\n header,\n cell: (row) => <DateMark value={value(row)} {...mark} />,\n sortBy: sortable ? (row) => toDate(value(row))?.getTime() ?? null : undefined,\n firstSort: sortable ? firstSort : undefined,\n filter: filter\n ? {\n type: \"date\",\n // A string goes as it is: the filter reads a timestamp's local day itself.\n // A Date or epoch value goes as its LOCAL day, the one the cell shows —\n // `toISOString()` handed over the UTC day, and in Berlin a 00:30 row was\n // filtered under the day before (round 0.34 BUG-2).\n getValue: (row) => {\n const raw = value(row);\n if (typeof raw === \"string\") return raw;\n const date = toDate(raw);\n return date ? toLocalIso(date) : null;\n },\n }\n : undefined,\n ...column,\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/account-chips.tsx"],"sourcesContent":["import type { ReactElement, ReactNode } from \"react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { formatDate, formatRelativeTime, toDate, EMPTY_FORMATTED_VALUE } from \"../lib/format\";\nimport type { DateInput, FormatDateStyle, FormatRelativeTimeOptions } from \"../lib/format\";\nimport { useKitDateFormatter, useKitLabels, useKitLocale, type LabelOverride } from \"../i18n/kit-labels\";\nimport { toLocalIso } from \"../lib/dates\";\nimport { Chip } from \"./chip\";\nimport type { ChipLinkProps, ChipShape, ChipSize, ChipTone, ChipVariant } from \"./chip\";\nimport type { DataTableColumn } from \"./data-table\";\nimport { DEFAULT_ACCOUNT_STATE_LABELS } from \"./account-chips-labels\";\n\n/**\n * The parts an admin roster repeats (0.18.0) — and only those.\n *\n * Kurvenschmiede (features/admin/users-panel.tsx, allowlist-panel.tsx) and keksdose\n * (features/admin/users-panel.tsx, the allowlist card in admin-page.tsx) each built the\n * same table: name over email, a role, whether the account is active, created, last\n * login, a row of actions, and an allowlist with a note. What they share is not the\n * table — the columns around it (keksdose's plan and privacy mode, Kurvenschmiede's\n * owned-work counts and its role SELECT), the actions and every rule about who may do\n * what are each app's own. What they share is the vocabulary: both spelled the same\n * square, uppercase status chip by hand (keksdose's `Badge` with its own tone map,\n * Kurvenschmiede's inline `<Chip size=\"xs\" shape=\"square\" caps>`), and both formatted\n * their two date columns by hand (`toLocaleDateString` in one, a \"3 days ago\" helper\n * in the other). So: {@link RoleChip}, {@link AccountStateChip} and\n * {@link dateColumn}, and the roster stays the app's `DataTable`.\n */\n\n/** The chip look both rosters use: a square, uppercase status tag. Each can be changed. */\ninterface AccountChipLook {\n /** Default `sm` — keksdose's roster; Kurvenschmiede's denser lists pass `xs`. */\n size?: ChipSize;\n /** Default `soft`. `dot` for a column of them that should not outshout the names. */\n variant?: ChipVariant;\n /** Default `square`: a chip in a table cell, beside square controls. */\n shape?: ChipShape;\n /** Default true — the status-badge type. */\n caps?: boolean;\n className?: string;\n [key: `data-${string}`]: string | number | boolean | undefined;\n}\n\n/* ── RoleChip ──────────────────────────────────────────────────────────────── */\n\n/** How one role is drawn. `label` is the app's, translated. */\nexport interface RoleDefinition {\n label: string;\n tone?: ChipTone;\n /** A lucide component or an element — Kurvenschmiede marks its admins with a shield. */\n icon?: LucideIcon | ReactElement;\n /**\n * 0.42.0 (docs/admin-harmonization.md §2.1, settings §12.3): the role's tool — an\n * admin's `/admin`, a reviewer's `/translations`. {@link AccountRoleChip} is a link to\n * it; a role without one gets a plain chip. {@link RoleChip} ignores it: a roster's\n * chips name a role, they are not a way in.\n */\n href?: string;\n /**\n * 0.42.0 (keksdose build review): this role's look in {@link AccountRoleChip} — keksdose\n * draws only ADMIN `solid` and its REVIEWER and MEMBER `soft`. Default `solid`, the\n * header's look; the chip's own `variant` prop wins over it. {@link RoleChip} ignores\n * it: a roster's column keeps one look, set on the column.\n */\n variant?: ChipVariant;\n}\n\n/**\n * Every role an app has, keyed by the value its API sends. Annotate the app's table\n * with it (`const ROLES: RoleVocabulary<UserRole> = {…}`) and TypeScript asks for a\n * label when a role is added.\n */\nexport type RoleVocabulary<R extends string = string> = Readonly<Record<R, RoleDefinition>>;\n\nexport interface RoleChipProps<R extends string = string> extends AccountChipLook {\n /**\n * The role key as the API sends it. `null` / `undefined` renders nothing. Named\n * `value`, not `role`, because `role` on a JSX element is the ARIA role — the lint\n * rule and every reader would take it for one.\n */\n value: R | null | undefined;\n /** What each key is called and how it is toned. A key missing here is shown as the\n * key itself, in `neutral`: an unknown role is still a fact the admin should see. */\n roles: Readonly<Partial<Record<R, RoleDefinition>>>;\n}\n\n/**\n * A role as a chip: the API's key looked up in the app's {@link RoleVocabulary}.\n *\n * The vocabulary is a prop and not kit English, because the roles are the app's —\n * Kurvenschmiede has admin, member and customer, keksdose admin and user, kastlan its\n * own — and so are their names. Carries `data-role` with the key, for a test or a\n * style hook.\n */\nexport function RoleChip<R extends string = string>({\n value: role,\n roles,\n size = \"sm\",\n variant,\n shape = \"square\",\n caps = true,\n className,\n ...rest\n}: RoleChipProps<R>) {\n if (role === null || role === undefined || role === \"\") return null;\n const def = roles[role];\n return (\n <Chip\n {...rest}\n data-role={role}\n tone={def?.tone ?? \"neutral\"}\n icon={def?.icon}\n size={size}\n variant={variant}\n shape={shape}\n caps={caps}\n className={className}\n >\n {def?.label ?? role}\n </Chip>\n );\n}\n\n/* ── AccountRoleChip ───────────────────────────────────────────────────────── */\n\nexport interface AccountRoleChipProps<R extends string = string> extends RoleChipProps<R> {\n /**\n * Runs on the way to the role's tool — the account menu's `close`, so the panel does\n * not stay open over the page it just navigated to (keksdose F7). Only called on a\n * link chip.\n */\n onNavigate?: () => void;\n /** Your router's link for the role's `href`. Default: the `<UiKitProvider\n * linkComponent>`, then `<a>` — {@link Chip}'s `renderLink`. */\n renderLink?: (props: ChipLinkProps) => ReactElement;\n}\n\n/**\n * The role in the account menu's header (0.42.0, docs/admin-harmonization.md §2.1, §7):\n * {@link RoleChip} in keksdose's look — `solid`, `sm`, `square`, `caps`, the role's icon\n * and tone — made for `TopBarActionMenu`'s `header.extra`:\n *\n * extra: (close) => <AccountRoleChip value={me.role} roles={ROLES} onNavigate={close} />\n *\n * A role whose {@link RoleDefinition.href} is set is a shortcut to its tool: the chip is a\n * link (through `renderLink`, else the provider's link, as {@link Chip} draws links) and\n * calls `onNavigate` on the click. Any other role is a plain chip — every account shows\n * its role (§7, \"Always a badge\"). The words and tones are the app's vocabulary; the kit\n * adds none. `solid` stays the default, so a role the app gives no look still reads as\n * the header's tag; a role that should sit quieter (keksdose's MEMBER and REVIEWER) sets\n * {@link RoleDefinition.variant} `soft` in the vocabulary. Carries `data-role` with the key.\n */\nexport function AccountRoleChip<R extends string = string>({\n value: role,\n roles,\n onNavigate,\n renderLink,\n size = \"sm\",\n variant,\n shape = \"square\",\n caps = true,\n className,\n ...rest\n}: AccountRoleChipProps<R>) {\n if (role === null || role === undefined || role === \"\") return null;\n const def = roles[role];\n const look = {\n ...rest,\n \"data-role\": role,\n tone: def?.tone ?? \"neutral\",\n icon: def?.icon,\n size,\n // The prop, then the role's own look, then the header's `solid`.\n variant: variant ?? def?.variant ?? \"solid\",\n shape,\n caps,\n className,\n } as const;\n const label = def?.label ?? role;\n if (def?.href) {\n return (\n <Chip {...look} href={def.href} renderLink={renderLink} onClick={onNavigate ? () => onNavigate() : undefined}>\n {label}\n </Chip>\n );\n }\n return <Chip {...look}>{label}</Chip>;\n}\n\n/* ── AccountStateChip ──────────────────────────────────────────────────────── */\n\n/**\n * The states both rosters show, each as the app's own flag maps onto it:\n *\n * - `active` / `inactive` — the account may sign in, or an admin switched it off\n * (both apps' `is_active`).\n * - `invited` — on the allowlist and mailed, not registered yet (an allowlist row).\n * - `registered` — an allowlist row whose address has an account now (Kurvenschmiede).\n * - `unverified` — the email address is not confirmed (keksdose).\n * - `passwordChange` — must set a new password at the next sign-in (keksdose).\n * - `deletion` — 0.30.0: the user asked for the account to be deleted\n * (docs/user-admin-harmonization.md §2.1, §6.4). It is deactivated at once and erased\n * on a date (`after_days`: keksdose, Kurvenschmiede) or by an operator (kastlan). The\n * chip says the date when there is one ({@link AccountStateChipProps.date}), and\n * \"Deletion requested\" when there is none. It stands IN PLACE of `inactive`: an\n * account marked for deletion is always deactivated, and two chips would say it twice.\n *\n * An account can be several at once (inactive AND unverified): render one chip per\n * state, as keksdose's state column does.\n */\nexport type AccountState =\n | \"active\"\n | \"inactive\"\n | \"invited\"\n | \"registered\"\n | \"unverified\"\n | \"passwordChange\"\n | \"deletion\";\n\n/**\n * The `accountState` namespace. One word per state, plus the two forms of `deletion`.\n *\n * The 0.30 keys are OPTIONAL, like every key added to a namespace an app annotates\n * (`const L: AccountStateLabels = {…}`): a new required key would be a compile error in\n * each app on its next update. Left out, they fall back to the provider's, then English.\n */\nexport interface AccountStateLabels extends Record<Exclude<AccountState, \"deletion\">, string> {\n /** 0.30.0: an account marked for deletion with no date — kastlan's `operator` mode,\n * where an operator erases it (§6.4). \"Deletion requested\". */\n deletion?: string;\n /** 0.30.0: an account that will be erased on `date`, already formatted in the\n * reader's locale — the `after_days` mode. \"Deletion on 6 Nov 2026\". */\n deletionOn?: (date: string) => string;\n}\n\n// Defined in ./account-chips-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_ACCOUNT_STATE_LABELS };\n\n/** The default tone of each state: green for the normal case, red for switched off\n * (and for going, `deletion`), amber for something the user still has to do, blue for\n * an open invitation. */\nexport const ACCOUNT_STATE_TONES: Readonly<Record<AccountState, ChipTone>> = {\n active: \"success\",\n inactive: \"danger\",\n invited: \"info\",\n registered: \"neutral\",\n unverified: \"warning\",\n passwordChange: \"warning\",\n // Red like `inactive`, which it replaces: the account cannot sign in, and it is going.\n deletion: \"danger\",\n};\n\nexport interface AccountStateChipProps extends AccountChipLook {\n state: AccountState;\n /**\n * 0.30.0, `state=\"deletion\"` only: the day the account will be erased — the row's\n * `deletion_scheduled_at` (§3.2, §6.4). Formatted as a date in the reader's locale\n * (\"Deletion on 6 Nov 2026\"), through the provider's `formatDate` when the app set\n * one, as {@link DateMark} does. Missing or `null` — an operator erases it, so there\n * is no date — the chip says \"Deletion requested\". Ignored for every other state.\n */\n date?: DateInput;\n /** Over the provider's locale, for the `deletion` date. */\n locale?: string;\n /** Over {@link ACCOUNT_STATE_TONES} — Kurvenschmiede draws `inactive` neutral. */\n tone?: ChipTone;\n /** Over the label, for an app whose word for the state is its own (\"Deactivated\"). */\n children?: ReactNode;\n /** Prop > `<UiKitProvider labels={{ accountState }}>` > English. */\n labels?: LabelOverride<AccountStateLabels>;\n}\n\n/**\n * An account's state as a chip, in the kit's words and tones. Carries `data-state`\n * with the key.\n */\nexport function AccountStateChip({\n state,\n date,\n locale: localeProp,\n tone,\n children,\n labels: labelsProp,\n size = \"sm\",\n variant,\n shape = \"square\",\n caps = true,\n className,\n ...rest\n}: AccountStateChipProps) {\n // An `undefined` in the prop is skipped (the provider's merge does that), so an\n // optional value cannot blank a default.\n const labels = useKitLabels(\"accountState\", DEFAULT_ACCOUNT_STATE_LABELS, labelsProp);\n const locale = useKitLocale(localeProp);\n const fromProvider = useKitDateFormatter();\n const when = state === \"deletion\" ? toDate(date ?? null) : null;\n let text: ReactNode;\n if (state !== \"deletion\") text = labels[state];\n else if (when) {\n // A date in a SENTENCE, so no weekday (keksdose's rule, KitDateFormatContext).\n const day =\n (fromProvider && fromProvider(toLocalIso(when), { unit: \"day\", source: \"dateMark\", locale, weekday: false })) ||\n formatDate(when, \"medium\", { locale });\n text = (labels.deletionOn ?? DEFAULT_ACCOUNT_STATE_LABELS.deletionOn)(day);\n } else text = labels.deletion ?? DEFAULT_ACCOUNT_STATE_LABELS.deletion;\n return (\n <Chip\n {...rest}\n data-state={state}\n data-date={when ? toLocalIso(when) : undefined}\n tone={tone ?? ACCOUNT_STATE_TONES[state]}\n size={size}\n variant={variant}\n shape={shape}\n caps={caps}\n className={className}\n >\n {children ?? text}\n </Chip>\n );\n}\n\n/* ── Date columns ──────────────────────────────────────────────────────────── */\n\nexport interface DateMarkProps {\n value: DateInput;\n /**\n * `\"date\"` (default): the date in `dateStyle` — \"Created\". `\"relative\"`: how long\n * ago — \"Last login\", where the question is \"recently?\" — with the full date and\n * time beside it (keksdose's last-seen column; see `absolute`).\n */\n display?: \"date\" | \"relative\";\n /**\n * Where a `\"relative\"` mark puts the exact date and time (0.32,\n * docs/text-size-harmonization.md §4: \"relative dates show the absolute one beside\n * them\", at every size — a touch screen never opens the tooltip it used to live in):\n * `\"below\"` (default), a muted second line under the words, so a table column keeps\n * its width; `\"inline\"`, after them on the same line, for a sentence or a wide cell.\n */\n absolute?: \"below\" | \"inline\";\n /** Default `medium`. Under `\"relative\"`, the style of the exact date beside it (default\n * `dateTime`) and of the date `relative.absoluteAfterDays` switches to. Given, it wins\n * over the provider's `formatDate`. */\n dateStyle?: FormatDateStyle | Intl.DateTimeFormatOptions;\n /** `formatRelativeTime` options: `numeric`, `style`, `absoluteAfterDays`, `now`. */\n relative?: Omit<FormatRelativeTimeOptions, \"locale\" | \"empty\" | \"absoluteStyle\">;\n /** What a missing value reads as. Default \"—\"; a last-login column passes its\n * translated \"Never\". */\n empty?: ReactNode;\n /** Over the provider's locale. */\n locale?: string;\n className?: string;\n}\n\n/**\n * A date in a table cell: a `<time>` with a machine-readable `dateTime`, nowrap and\n * tabular figures, in the provider's locale — or `empty` when there is none.\n *\n * `display=\"date\"` without a `dateStyle` is written by `<UiKitProvider formatDate>` when\n * the app set one (keksdose K12: its `DateCell` exists to put every column's date in the\n * app's date-format preference, weekday and all — dev#546), called with the LOCAL\n * calendar day of the value as `\"YYYY-MM-DD\"` and `weekday: true`. Without one, or for\n * an empty answer, it is `Intl`'s `medium` date as before. The relative display is not\n * a calendar day and keeps `formatRelativeTime`.\n */\nexport function DateMark({\n value,\n display = \"date\",\n dateStyle,\n relative,\n absolute = \"below\",\n empty = EMPTY_FORMATTED_VALUE,\n locale: localeProp,\n className,\n}: DateMarkProps) {\n const locale = useKitLocale(localeProp);\n const fromProvider = useKitDateFormatter();\n const date = toDate(value);\n const cls = cn(\"whitespace-nowrap tabular-nums\", className);\n if (!date) return <span className={cn(cls, \"text-muted\")}>{empty}</span>;\n if (display === \"date\") {\n const own =\n dateStyle === undefined && fromProvider\n ? fromProvider(toLocalIso(date), { unit: \"day\", source: \"dateMark\", locale, weekday: true })\n : \"\";\n return (\n <time dateTime={date.toISOString()} className={cls}>\n {own || formatDate(date, dateStyle ?? \"medium\", { locale })}\n </time>\n );\n }\n const text = formatRelativeTime(date, { ...relative, locale, absoluteStyle: dateStyle ?? \"medium\" });\n const exact = formatDate(date, dateStyle ?? \"dateTime\", { locale });\n // Past `absoluteAfterDays` the relative words already ARE the date; then the exact\n // line alone, rather than the same day twice.\n const sameDay = text === formatDate(date, dateStyle ?? \"medium\", { locale });\n const iso = date.toISOString();\n if (sameDay) {\n return (\n <time dateTime={iso} className={cls}>\n {exact}\n </time>\n );\n }\n // The exact date and time in the layout, never only in a tooltip (§4). Still no tab\n // stop: a roster of fifty rows would be fifty more of them.\n return (\n <time\n dateTime={iso}\n data-slot=\"date-mark\"\n className={cn(absolute === \"below\" ? \"inline-flex flex-col leading-tight\" : \"inline\", className)}\n >\n <span className=\"whitespace-nowrap tabular-nums\">{text}</span>\n {absolute === \"inline\" && \" · \"}\n <span className={cn(\"whitespace-nowrap tabular-nums text-muted\", absolute === \"below\" && \"text-xs\")}>\n {exact}\n </span>\n </time>\n );\n}\n\nexport interface DateColumnOptions<T> extends Omit<DateMarkProps, \"value\" | \"className\"> {\n key: string;\n header: ReactNode;\n /** The value of this row: an ISO string, epoch ms, a Date, or null for none. */\n value: (row: T) => DateInput;\n /** Sortable by the instant; rows with no date sort last either way. Default true. */\n sortable?: boolean;\n /** Default `desc` — a date column is sorted to find the newest. */\n firstSort?: \"asc\" | \"desc\";\n /** Add the table's from–to date filter over the column. Default false. */\n filter?: boolean;\n /** Further column settings (`mobileHidden`, `headClassName`, …), merged over. */\n column?: Partial<DataTableColumn<T>>;\n}\n\n/**\n * A {@link DataTableColumn} for a date: {@link DateMark} cells, sorted by the instant\n * (newest first on the first click), optionally the date filter. The roster's\n * \"Created\" is `dateColumn({ key, header, value })`; its \"Last login\" adds\n * `display: \"relative\", empty: t(\"never\")`.\n */\nexport function dateColumn<T>({\n key,\n header,\n value,\n sortable = true,\n firstSort = \"desc\",\n filter = false,\n column,\n ...mark\n}: DateColumnOptions<T>): DataTableColumn<T> {\n return {\n key,\n header,\n cell: (row) => <DateMark value={value(row)} {...mark} />,\n sortBy: sortable ? (row) => toDate(value(row))?.getTime() ?? null : undefined,\n firstSort: sortable ? firstSort : undefined,\n filter: filter\n ? {\n type: \"date\",\n // A string goes as it is: the filter reads a timestamp's local day itself.\n // A Date or epoch value goes as its LOCAL day, the one the cell shows —\n // `toISOString()` handed over the UTC day, and in Berlin a 00:30 row was\n // filtered under the day before (round 0.34 BUG-2).\n getValue: (row) => {\n const raw = value(row);\n if (typeof raw === \"string\") return raw;\n const date = toDate(raw);\n return date ? toLocalIso(date) : null;\n },\n }\n : undefined,\n ...column,\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -15,8 +15,9 @@ export interface EmptyStateProps extends Omit<ComponentPropsWithRef<"div">, "chi
|
|
|
15
15
|
* keksdose's error boundary IS the page when it trips, and its `<h2>` went missing
|
|
16
16
|
* when it moved onto EmptyState — so the page's heading outline lost the one line
|
|
17
17
|
* that says what happened. Only the element changes; the look is the box's.
|
|
18
|
+
* `h1` (0.42.0) for an empty state that is the whole page — `AdminForbidden`.
|
|
18
19
|
*/
|
|
19
|
-
headingAs?: "h2" | "h3" | "h4" | "h5" | "h6";
|
|
20
|
+
headingAs?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
|
|
20
21
|
/** A glyph ABOVE the title — kastlan's InboxEmptyState (an inbox), keksdose's
|
|
21
22
|
* offline card (a cloud with a slash). Sized by the box (`[&_svg]:size-8`) and
|
|
22
23
|
* muted, so five call sites cannot pick five sizes; hidden from assistive tech,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/empty-state.tsx"],"sourcesContent":["import type { ComponentPropsWithRef, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\n\nexport interface EmptyStateProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\" | \"title\"> {\n /** The box renders `title` and `hint` in its own two-line rhythm, which is what makes\n * every empty state in three apps look like the same thing — so there is no\n * `children` slot to put arbitrary content in. The two slots below are the only\n * other things an empty state has turned out to need, and each has a fixed place. */\n /** Nodes rather than strings, so a title can carry a code, a link or an emphasised\n * word — keksdose's error boundary shows the error's name in `<code>` — while the\n * box still sets the type. */\n title: ReactNode;\n hint?: ReactNode;\n /**\n * Render the title as a heading of this level. Left out it is a `<div>`, as before:\n * most empty states are a message inside a section that already has its heading.\n * keksdose's error boundary IS the page when it trips, and its `<h2>` went missing\n * when it moved onto EmptyState — so the page's heading outline lost the one line\n * that says what happened. Only the element changes; the look is the box's.\n */\n headingAs?: \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\";\n /** A glyph ABOVE the title — kastlan's InboxEmptyState (an inbox), keksdose's\n * offline card (a cloud with a slash). Sized by the box (`[&_svg]:size-8`) and\n * muted, so five call sites cannot pick five sizes; hidden from assistive tech,\n * because the title already says what it shows. */\n icon?: ReactNode;\n /** What to do about it, BELOW the hint: keksdose's query-state cards (Retry), its\n * error boundary (Retry / Reload / Go home — pass all three in a fragment, they\n * wrap and centre as one row), kastlan's \"Create a rule\". Buttons or links the\n * caller renders; the box only places them. */\n action?: ReactNode;\n /**\n * `box` (default): the dashed, padded card that stands in for a whole section.\n * `inline`: one quiet line with no box — for the empty state INSIDE something that\n * already has its own frame, where a dashed card would be a box in a box. keksdose\n * writes that line by hand five times as a muted `<p className=\"py-6 text-center\">`:\n * the notification inbox (notification-inbox:140), a support thread\n * (support-thread:144), the assistant (assistant-page:321), the admin support panel\n * (support-panel:265) and the funding dialog (funding-dialog:89). The icon shrinks\n * to the text's size and sits before the title, the hint follows on the same line,\n * and the row wraps only when it must. Centred like the box; `className=\"justify-start\"`\n * for a list that reads from the start edge (funding-dialog).\n */\n variant?: \"box\" | \"inline\";\n /**\n * Colour the icon and the title: `danger` for an error standing where the content\n * should be — kastlan's ErrorState (shared/components/feedback/error-state.tsx) and\n * its error boundary's fallback (error-boundary.tsx:16), both a red triangle over a\n * message — and `success` for an empty state that is GOOD news, kastlan's \"No defects\n * recorded for this room\" (handover defects-step.tsx:142, a green check). Left out,\n * both stay muted. The hint stays muted either way: it is the explanation, not the\n * verdict.\n */\n tone?: \"danger\" | \"success\";\n /**\n * `inline` only (0.11.0). `sm`: 12px, start-aligned and tight (`py-1`) — the empty\n * line INSIDE a detail panel, under a panel heading and among `text-xs` rows, where\n * the default's 14px centred line with `py-4` read as a section of its own.\n * keksdose's holdings panel (holdings-panel:563), asset-loan panel\n * (asset-loan-panel:108) and loan-payment panel (loan-payment-panel:191) each wrote\n * `className=\"justify-start py-1\"` over it and still got body-size text. Default `md`,\n * the look it had. Ignored by the box.\n */\n size?: \"sm\" | \"md\";\n}\n\nconst EMPTY_STATE_TONE: Record<\"danger\" | \"success\", string> = {\n danger: \"text-danger\",\n success: \"text-success\",\n};\n\nexport function EmptyState({\n title,\n hint,\n icon,\n action,\n headingAs,\n variant = \"box\",\n tone,\n size = \"md\",\n className,\n ...rest\n}: EmptyStateProps) {\n const Title = headingAs ?? \"div\";\n const toneClass = tone ? EMPTY_STATE_TONE[tone] : undefined;\n const hasHint = hint != null && hint !== false && hint !== \"\";\n if (variant === \"inline\") {\n const sm = size === \"sm\";\n return (\n <div\n {...rest}\n className={cn(\n \"flex flex-wrap items-center text-muted\",\n sm\n ? \"justify-start gap-x-1.5 gap-y-0.5 py-1 text-start text-xs\"\n : \"justify-center gap-x-2 gap-y-1 py-4 text-center text-sm\",\n className,\n )}\n >\n {icon != null && (\n <span\n aria-hidden\n className={cn(\"flex text-placeholder\", sm ? \"[&_svg]:size-3.5\" : \"[&_svg]:size-4\", toneClass)}\n >\n {icon}\n </span>\n )}\n {/* No weight of its own: one muted line is what these sites were, and a\n bold title would turn a quiet \"nothing here\" into a heading. */}\n <Title className={cn(sm ? \"text-xs\" : \"text-sm\", toneClass)}>{title}</Title>\n {hasHint && <span className=\"text-xs\">{hint}</span>}\n {action != null && <span className=\"flex flex-wrap items-center gap-2\">{action}</span>}\n </div>\n );\n }\n return (\n <div\n {...rest}\n className={cn(\n \"flex flex-col items-center justify-center rounded-lg border border-dashed border-strong bg-surface-2 px-4 py-10 text-center text-sm text-muted\",\n className,\n )}\n >\n {icon != null && (\n <div aria-hidden className={cn(\"mb-3 text-placeholder [&_svg]:size-8\", toneClass)}>\n {icon}\n </div>\n )}\n {/* `text-sm`: a heading keeps the box's size, not whatever a stylesheet gives h2. */}\n <Title className={cn(\"text-sm font-medium text-secondary\", toneClass)}>{title}</Title>\n {hasHint && <div className=\"mt-1 text-xs\">{hint}</div>}\n {action != null && (\n <div className=\"mt-4 flex flex-wrap items-center justify-center gap-2\">{action}</div>\n )}\n </div>\n );\n}\n"],"mappings":";
|
|
1
|
+
{"version":3,"sources":["../../src/components/empty-state.tsx"],"sourcesContent":["import type { ComponentPropsWithRef, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\n\nexport interface EmptyStateProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\" | \"title\"> {\n /** The box renders `title` and `hint` in its own two-line rhythm, which is what makes\n * every empty state in three apps look like the same thing — so there is no\n * `children` slot to put arbitrary content in. The two slots below are the only\n * other things an empty state has turned out to need, and each has a fixed place. */\n /** Nodes rather than strings, so a title can carry a code, a link or an emphasised\n * word — keksdose's error boundary shows the error's name in `<code>` — while the\n * box still sets the type. */\n title: ReactNode;\n hint?: ReactNode;\n /**\n * Render the title as a heading of this level. Left out it is a `<div>`, as before:\n * most empty states are a message inside a section that already has its heading.\n * keksdose's error boundary IS the page when it trips, and its `<h2>` went missing\n * when it moved onto EmptyState — so the page's heading outline lost the one line\n * that says what happened. Only the element changes; the look is the box's.\n * `h1` (0.42.0) for an empty state that is the whole page — `AdminForbidden`.\n */\n headingAs?: \"h1\" | \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\";\n /** A glyph ABOVE the title — kastlan's InboxEmptyState (an inbox), keksdose's\n * offline card (a cloud with a slash). Sized by the box (`[&_svg]:size-8`) and\n * muted, so five call sites cannot pick five sizes; hidden from assistive tech,\n * because the title already says what it shows. */\n icon?: ReactNode;\n /** What to do about it, BELOW the hint: keksdose's query-state cards (Retry), its\n * error boundary (Retry / Reload / Go home — pass all three in a fragment, they\n * wrap and centre as one row), kastlan's \"Create a rule\". Buttons or links the\n * caller renders; the box only places them. */\n action?: ReactNode;\n /**\n * `box` (default): the dashed, padded card that stands in for a whole section.\n * `inline`: one quiet line with no box — for the empty state INSIDE something that\n * already has its own frame, where a dashed card would be a box in a box. keksdose\n * writes that line by hand five times as a muted `<p className=\"py-6 text-center\">`:\n * the notification inbox (notification-inbox:140), a support thread\n * (support-thread:144), the assistant (assistant-page:321), the admin support panel\n * (support-panel:265) and the funding dialog (funding-dialog:89). The icon shrinks\n * to the text's size and sits before the title, the hint follows on the same line,\n * and the row wraps only when it must. Centred like the box; `className=\"justify-start\"`\n * for a list that reads from the start edge (funding-dialog).\n */\n variant?: \"box\" | \"inline\";\n /**\n * Colour the icon and the title: `danger` for an error standing where the content\n * should be — kastlan's ErrorState (shared/components/feedback/error-state.tsx) and\n * its error boundary's fallback (error-boundary.tsx:16), both a red triangle over a\n * message — and `success` for an empty state that is GOOD news, kastlan's \"No defects\n * recorded for this room\" (handover defects-step.tsx:142, a green check). Left out,\n * both stay muted. The hint stays muted either way: it is the explanation, not the\n * verdict.\n */\n tone?: \"danger\" | \"success\";\n /**\n * `inline` only (0.11.0). `sm`: 12px, start-aligned and tight (`py-1`) — the empty\n * line INSIDE a detail panel, under a panel heading and among `text-xs` rows, where\n * the default's 14px centred line with `py-4` read as a section of its own.\n * keksdose's holdings panel (holdings-panel:563), asset-loan panel\n * (asset-loan-panel:108) and loan-payment panel (loan-payment-panel:191) each wrote\n * `className=\"justify-start py-1\"` over it and still got body-size text. Default `md`,\n * the look it had. Ignored by the box.\n */\n size?: \"sm\" | \"md\";\n}\n\nconst EMPTY_STATE_TONE: Record<\"danger\" | \"success\", string> = {\n danger: \"text-danger\",\n success: \"text-success\",\n};\n\nexport function EmptyState({\n title,\n hint,\n icon,\n action,\n headingAs,\n variant = \"box\",\n tone,\n size = \"md\",\n className,\n ...rest\n}: EmptyStateProps) {\n const Title = headingAs ?? \"div\";\n const toneClass = tone ? EMPTY_STATE_TONE[tone] : undefined;\n const hasHint = hint != null && hint !== false && hint !== \"\";\n if (variant === \"inline\") {\n const sm = size === \"sm\";\n return (\n <div\n {...rest}\n className={cn(\n \"flex flex-wrap items-center text-muted\",\n sm\n ? \"justify-start gap-x-1.5 gap-y-0.5 py-1 text-start text-xs\"\n : \"justify-center gap-x-2 gap-y-1 py-4 text-center text-sm\",\n className,\n )}\n >\n {icon != null && (\n <span\n aria-hidden\n className={cn(\"flex text-placeholder\", sm ? \"[&_svg]:size-3.5\" : \"[&_svg]:size-4\", toneClass)}\n >\n {icon}\n </span>\n )}\n {/* No weight of its own: one muted line is what these sites were, and a\n bold title would turn a quiet \"nothing here\" into a heading. */}\n <Title className={cn(sm ? \"text-xs\" : \"text-sm\", toneClass)}>{title}</Title>\n {hasHint && <span className=\"text-xs\">{hint}</span>}\n {action != null && <span className=\"flex flex-wrap items-center gap-2\">{action}</span>}\n </div>\n );\n }\n return (\n <div\n {...rest}\n className={cn(\n \"flex flex-col items-center justify-center rounded-lg border border-dashed border-strong bg-surface-2 px-4 py-10 text-center text-sm text-muted\",\n className,\n )}\n >\n {icon != null && (\n <div aria-hidden className={cn(\"mb-3 text-placeholder [&_svg]:size-8\", toneClass)}>\n {icon}\n </div>\n )}\n {/* `text-sm`: a heading keeps the box's size, not whatever a stylesheet gives h2. */}\n <Title className={cn(\"text-sm font-medium text-secondary\", toneClass)}>{title}</Title>\n {hasHint && <div className=\"mt-1 text-xs\">{hint}</div>}\n {action != null && (\n <div className=\"mt-4 flex flex-wrap items-center justify-center gap-2\">{action}</div>\n )}\n </div>\n );\n}\n"],"mappings":";AA0FM,SAWI,KAXJ;AAzFN,SAAS,UAAU;AAkEnB,MAAM,mBAAyD;AAAA,EAC7D,QAAQ;AAAA,EACR,SAAS;AACX;AAEO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA,GAAG;AACL,GAAoB;AAClB,QAAM,QAAQ,aAAa;AAC3B,QAAM,YAAY,OAAO,iBAAiB,IAAI,IAAI;AAClD,QAAM,UAAU,QAAQ,QAAQ,SAAS,SAAS,SAAS;AAC3D,MAAI,YAAY,UAAU;AACxB,UAAM,KAAK,SAAS;AACpB,WACE;AAAA,MAAC;AAAA;AAAA,QACE,GAAG;AAAA,QACJ,WAAW;AAAA,UACT;AAAA,UACA,KACI,8DACA;AAAA,UACJ;AAAA,QACF;AAAA,QAEC;AAAA,kBAAQ,QACP;AAAA,YAAC;AAAA;AAAA,cACC,eAAW;AAAA,cACX,WAAW,GAAG,yBAAyB,KAAK,qBAAqB,kBAAkB,SAAS;AAAA,cAE3F;AAAA;AAAA,UACH;AAAA,UAIF,oBAAC,SAAM,WAAW,GAAG,KAAK,YAAY,WAAW,SAAS,GAAI,iBAAM;AAAA,UACnE,WAAW,oBAAC,UAAK,WAAU,WAAW,gBAAK;AAAA,UAC3C,UAAU,QAAQ,oBAAC,UAAK,WAAU,qCAAqC,kBAAO;AAAA;AAAA;AAAA,IACjF;AAAA,EAEJ;AACA,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MAEC;AAAA,gBAAQ,QACP,oBAAC,SAAI,eAAW,MAAC,WAAW,GAAG,wCAAwC,SAAS,GAC7E,gBACH;AAAA,QAGF,oBAAC,SAAM,WAAW,GAAG,sCAAsC,SAAS,GAAI,iBAAM;AAAA,QAC7E,WAAW,oBAAC,SAAI,WAAU,gBAAgB,gBAAK;AAAA,QAC/C,UAAU,QACT,oBAAC,SAAI,WAAU,yDAAyD,kBAAO;AAAA;AAAA;AAAA,EAEnF;AAEJ;","names":[]}
|
|
@@ -56,8 +56,8 @@ export interface NavPillsProps<T extends string> extends Omit<ComponentPropsWith
|
|
|
56
56
|
* keksdose's swipe-settings card (swipe-settings-card:152) hand-rolled this: four
|
|
57
57
|
* surface names that do not fit a 406px phone, and a sideways scroll would hide the
|
|
58
58
|
* ones that do not fit behind a gesture nothing announces (live #332: *"Have the tabs
|
|
59
|
-
* in more rows, rather than scrolling sideways"*). It is not {@link Tabs}
|
|
60
|
-
* because its items are not tabs: a `role="tab"` promises a tabpanel, a roving tab
|
|
59
|
+
* in more rows, rather than scrolling sideways"*). It is not {@link Tabs} (which
|
|
60
|
+
* scrolls; its multirow `wrap` went in 0.41) because its items are not tabs: a `role="tab"` promises a tabpanel, a roving tab
|
|
61
61
|
* stop and arrow keys, and a set of links to pages (or of buttons that switch the rest
|
|
62
62
|
* of a card) promises none of that. So every pill is its own tab stop, the set is a
|
|
63
63
|
* list (a screen reader hears "list, 4 items"), and "current" is `aria-current` — the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/nav-pills.tsx"],"sourcesContent":["import type { ComponentPropsWithoutRef, ComponentType, MouseEvent, ReactElement, ReactNode, Ref } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLink } from \"../i18n/kit-labels\";\nimport { FOCUS_RING } from \"./focus-ring\";\nimport { pickLinkRenderer, RenderedKitLink } from \"./text-link\";\n\nexport interface NavPillItem<T extends string> {\n value: T;\n label: ReactNode;\n /** Where the pill leads. With one it is a link (see `renderLink`); without, a button\n * that calls `onSelect`. */\n href?: string;\n /** A Lucide icon (or any component taking a `className`), before the label. */\n icon?: ComponentType<{ className?: string; \"aria-hidden\"?: boolean }>;\n /** Buttons only — a link cannot be disabled. */\n disabled?: boolean;\n}\n\n/** What {@link NavPillsProps.renderLink} is handed. Spread it onto your router's link\n * — `({ href, ...p }) => <Link to={href} {...p} />`. */\nexport interface NavPillLinkProps {\n href: string;\n className: string;\n children: ReactNode;\n \"aria-current\": NavPillsCurrent | undefined;\n onClick: (e: MouseEvent<HTMLAnchorElement>) => void;\n}\n\n/** The `aria-current` token the current pill carries. */\nexport type NavPillsCurrent = \"page\" | \"step\" | \"location\" | \"true\";\n\nexport interface NavPillsProps<T extends string> extends Omit<ComponentPropsWithoutRef<\"nav\">, \"children\" | \"onSelect\"> {\n items: NavPillItem<T>[];\n /** The pill that is current. `null` or a value not in `items`: none is. */\n current: T | null;\n /** Called with the pill's value on a click — of a button, and of a link too (before\n * the router follows it), for a caller that keeps the choice in state as well. */\n onSelect?: (value: T) => void;\n /** Your router's link. Default: the `<UiKitProvider linkComponent>` (for an in-app\n * `href`), then `<a>`. The API `Breadcrumbs`, `Chip` and `ListItem` share. */\n renderLink?: (props: NavPillLinkProps) => ReactElement;\n /** What `aria-current` says on the current pill. Default `page` where the items\n * are links and `true` where they are buttons (keksdose's swipe-surface picker\n * switches a panel on the same page, which is not a page). */\n currentType?: NavPillsCurrent;\n /** `sm`: 12px pills — the compact size of a {@link ToggleGroup}. Default `md`. */\n size?: \"sm\" | \"md\";\n /** Render a `<nav>` landmark (default) or a plain `<div role=\"group\">` for a picker\n * that is not the page's navigation. Name either with `aria-label`. */\n landmark?: boolean;\n /** Classes for every pill. */\n itemClassName?: string;\n /** The `<nav>` (or the `<div role=\"group\">`) — hence `HTMLElement` (0.34, API-2,\n * refactor audit 2026-10). */\n ref?: Ref<HTMLElement>;\n}\n\n// The pill is a ToggleGroup segment's (`rounded`, `px-3 py-1.5`, the inverse fill for\n// the current one), so a strip of these beside a segmented control reads as the same\n// family. Unlike the segments, a pill never truncates: the strip WRAPS instead. Its focus\n// ring is the kit's one focus frame (text-size §5), in brand; until 0.34 it copied the\n// segment's `ring-strong` too (refactor audit 2026-10, DOC-7).\nconst PILL = `inline-flex items-center gap-1.5 whitespace-nowrap rounded-md px-3 py-1.5 text-sm font-medium transition-colors focus:outline-none ${FOCUS_RING} [&_svg]:size-4 [&_svg]:shrink-0`;\nconst PILL_SM = \"gap-1 px-2 py-1 text-xs [&_svg]:size-3.5\";\nconst PILL_CURRENT = \"bg-inverse text-inverse\";\nconst PILL_IDLE = \"text-secondary hover:bg-hover hover:text-primary\";\nconst PILL_DISABLED = \"cursor-not-allowed opacity-50 hover:bg-transparent hover:text-secondary\";\n\n/**\n * A set of links or buttons that WRAPS onto as many rows as it needs, the current one\n * filled and marked `aria-current`.\n *\n * keksdose's swipe-settings card (swipe-settings-card:152) hand-rolled this: four\n * surface names that do not fit a 406px phone, and a sideways scroll would hide the\n * ones that do not fit behind a gesture nothing announces (live #332: *\"Have the tabs\n * in more rows, rather than scrolling sideways\"*). It is not {@link Tabs}
|
|
1
|
+
{"version":3,"sources":["../../src/components/nav-pills.tsx"],"sourcesContent":["import type { ComponentPropsWithoutRef, ComponentType, MouseEvent, ReactElement, ReactNode, Ref } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLink } from \"../i18n/kit-labels\";\nimport { FOCUS_RING } from \"./focus-ring\";\nimport { pickLinkRenderer, RenderedKitLink } from \"./text-link\";\n\nexport interface NavPillItem<T extends string> {\n value: T;\n label: ReactNode;\n /** Where the pill leads. With one it is a link (see `renderLink`); without, a button\n * that calls `onSelect`. */\n href?: string;\n /** A Lucide icon (or any component taking a `className`), before the label. */\n icon?: ComponentType<{ className?: string; \"aria-hidden\"?: boolean }>;\n /** Buttons only — a link cannot be disabled. */\n disabled?: boolean;\n}\n\n/** What {@link NavPillsProps.renderLink} is handed. Spread it onto your router's link\n * — `({ href, ...p }) => <Link to={href} {...p} />`. */\nexport interface NavPillLinkProps {\n href: string;\n className: string;\n children: ReactNode;\n \"aria-current\": NavPillsCurrent | undefined;\n onClick: (e: MouseEvent<HTMLAnchorElement>) => void;\n}\n\n/** The `aria-current` token the current pill carries. */\nexport type NavPillsCurrent = \"page\" | \"step\" | \"location\" | \"true\";\n\nexport interface NavPillsProps<T extends string> extends Omit<ComponentPropsWithoutRef<\"nav\">, \"children\" | \"onSelect\"> {\n items: NavPillItem<T>[];\n /** The pill that is current. `null` or a value not in `items`: none is. */\n current: T | null;\n /** Called with the pill's value on a click — of a button, and of a link too (before\n * the router follows it), for a caller that keeps the choice in state as well. */\n onSelect?: (value: T) => void;\n /** Your router's link. Default: the `<UiKitProvider linkComponent>` (for an in-app\n * `href`), then `<a>`. The API `Breadcrumbs`, `Chip` and `ListItem` share. */\n renderLink?: (props: NavPillLinkProps) => ReactElement;\n /** What `aria-current` says on the current pill. Default `page` where the items\n * are links and `true` where they are buttons (keksdose's swipe-surface picker\n * switches a panel on the same page, which is not a page). */\n currentType?: NavPillsCurrent;\n /** `sm`: 12px pills — the compact size of a {@link ToggleGroup}. Default `md`. */\n size?: \"sm\" | \"md\";\n /** Render a `<nav>` landmark (default) or a plain `<div role=\"group\">` for a picker\n * that is not the page's navigation. Name either with `aria-label`. */\n landmark?: boolean;\n /** Classes for every pill. */\n itemClassName?: string;\n /** The `<nav>` (or the `<div role=\"group\">`) — hence `HTMLElement` (0.34, API-2,\n * refactor audit 2026-10). */\n ref?: Ref<HTMLElement>;\n}\n\n// The pill is a ToggleGroup segment's (`rounded`, `px-3 py-1.5`, the inverse fill for\n// the current one), so a strip of these beside a segmented control reads as the same\n// family. Unlike the segments, a pill never truncates: the strip WRAPS instead. Its focus\n// ring is the kit's one focus frame (text-size §5), in brand; until 0.34 it copied the\n// segment's `ring-strong` too (refactor audit 2026-10, DOC-7).\nconst PILL = `inline-flex items-center gap-1.5 whitespace-nowrap rounded-md px-3 py-1.5 text-sm font-medium transition-colors focus:outline-none ${FOCUS_RING} [&_svg]:size-4 [&_svg]:shrink-0`;\nconst PILL_SM = \"gap-1 px-2 py-1 text-xs [&_svg]:size-3.5\";\nconst PILL_CURRENT = \"bg-inverse text-inverse\";\nconst PILL_IDLE = \"text-secondary hover:bg-hover hover:text-primary\";\nconst PILL_DISABLED = \"cursor-not-allowed opacity-50 hover:bg-transparent hover:text-secondary\";\n\n/**\n * A set of links or buttons that WRAPS onto as many rows as it needs, the current one\n * filled and marked `aria-current`.\n *\n * keksdose's swipe-settings card (swipe-settings-card:152) hand-rolled this: four\n * surface names that do not fit a 406px phone, and a sideways scroll would hide the\n * ones that do not fit behind a gesture nothing announces (live #332: *\"Have the tabs\n * in more rows, rather than scrolling sideways\"*). It is not {@link Tabs} (which\n * scrolls; its multirow `wrap` went in 0.41) because its items are not tabs: a `role=\"tab\"` promises a tabpanel, a roving tab\n * stop and arrow keys, and a set of links to pages (or of buttons that switch the rest\n * of a card) promises none of that. So every pill is its own tab stop, the set is a\n * list (a screen reader hears \"list, 4 items\"), and \"current\" is `aria-current` — the\n * attribute a link to where you already are carries.\n */\nexport function NavPills<T extends string>({\n items,\n current,\n onSelect,\n renderLink,\n currentType,\n size = \"md\",\n landmark = true,\n itemClassName,\n className,\n ref,\n ...rest\n}: NavPillsProps<T>) {\n const kitLink = useKitLink();\n const Root = landmark ? \"nav\" : \"div\";\n return (\n <Root\n {...rest}\n ref={ref as Ref<HTMLDivElement> | undefined}\n role={landmark ? undefined : \"group\"}\n className={cn(\"min-w-0\", className)}\n >\n <ul className=\"m-0 flex list-none flex-wrap gap-1 p-0\">\n {items.map((item) => {\n const isCurrent = item.value === current;\n const Icon = item.icon;\n const pillClass = cn(\n PILL,\n size === \"sm\" && PILL_SM,\n isCurrent ? PILL_CURRENT : PILL_IDLE,\n item.disabled && item.href === undefined && PILL_DISABLED,\n itemClassName,\n );\n const children = (\n <>\n {Icon && <Icon aria-hidden />}\n {item.label}\n </>\n );\n let pill: ReactNode;\n if (item.href !== undefined) {\n const props: NavPillLinkProps = {\n href: item.href,\n className: pillClass,\n children,\n \"aria-current\": isCurrent ? (currentType ?? \"page\") : undefined,\n onClick: () => onSelect?.(item.value),\n };\n const kit = pickLinkRenderer<NavPillLinkProps>(undefined, kitLink, item.href);\n pill = renderLink ? (\n renderLink(props)\n ) : kit ? (\n <RenderedKitLink render={kit} props={props} />\n ) : (\n <a href={props.href} className={props.className} aria-current={props[\"aria-current\"]} onClick={props.onClick}>\n {props.children}\n </a>\n );\n } else {\n pill = (\n <button\n type=\"button\"\n disabled={item.disabled}\n aria-current={isCurrent ? (currentType ?? \"true\") : undefined}\n onClick={() => onSelect?.(item.value)}\n className={pillClass}\n >\n {children}\n </button>\n );\n }\n return (\n <li key={item.value} className=\"flex\">\n {pill}\n </li>\n );\n })}\n </ul>\n </Root>\n );\n}\n"],"mappings":";AAoHY,mBACW,KADX;AAnHZ,SAAS,UAAU;AACnB,SAAS,kBAAkB;AAC3B,SAAS,kBAAkB;AAC3B,SAAS,kBAAkB,uBAAuB;AA0DlD,MAAM,OAAO,sIAAsI,UAAU;AAC7J,MAAM,UAAU;AAChB,MAAM,eAAe;AACrB,MAAM,YAAY;AAClB,MAAM,gBAAgB;AAgBf,SAAS,SAA2B;AAAA,EACzC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAqB;AACnB,QAAM,UAAU,WAAW;AAC3B,QAAM,OAAO,WAAW,QAAQ;AAChC,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ;AAAA,MACA,MAAM,WAAW,SAAY;AAAA,MAC7B,WAAW,GAAG,WAAW,SAAS;AAAA,MAElC,8BAAC,QAAG,WAAU,0CACX,gBAAM,IAAI,CAAC,SAAS;AACnB,cAAM,YAAY,KAAK,UAAU;AACjC,cAAM,OAAO,KAAK;AAClB,cAAM,YAAY;AAAA,UAChB;AAAA,UACA,SAAS,QAAQ;AAAA,UACjB,YAAY,eAAe;AAAA,UAC3B,KAAK,YAAY,KAAK,SAAS,UAAa;AAAA,UAC5C;AAAA,QACF;AACA,cAAM,WACJ,iCACG;AAAA,kBAAQ,oBAAC,QAAK,eAAW,MAAC;AAAA,UAC1B,KAAK;AAAA,WACR;AAEF,YAAI;AACJ,YAAI,KAAK,SAAS,QAAW;AAC3B,gBAAM,QAA0B;AAAA,YAC9B,MAAM,KAAK;AAAA,YACX,WAAW;AAAA,YACX;AAAA,YACA,gBAAgB,YAAa,eAAe,SAAU;AAAA,YACtD,SAAS,MAAM,WAAW,KAAK,KAAK;AAAA,UACtC;AACA,gBAAM,MAAM,iBAAmC,QAAW,SAAS,KAAK,IAAI;AAC5E,iBAAO,aACL,WAAW,KAAK,IACd,MACF,oBAAC,mBAAgB,QAAQ,KAAK,OAAc,IAE5C,oBAAC,OAAE,MAAM,MAAM,MAAM,WAAW,MAAM,WAAW,gBAAc,MAAM,cAAc,GAAG,SAAS,MAAM,SAClG,gBAAM,UACT;AAAA,QAEJ,OAAO;AACL,iBACE;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,UAAU,KAAK;AAAA,cACf,gBAAc,YAAa,eAAe,SAAU;AAAA,cACpD,SAAS,MAAM,WAAW,KAAK,KAAK;AAAA,cACpC,WAAW;AAAA,cAEV;AAAA;AAAA,UACH;AAAA,QAEJ;AACA,eACE,oBAAC,QAAoB,WAAU,QAC5B,kBADM,KAAK,KAEd;AAAA,MAEJ,CAAC,GACH;AAAA;AAAA,EACF;AAEJ;","names":[]}
|
|
@@ -61,34 +61,6 @@ export interface TabsProps<T extends string> extends Omit<ComponentPropsWithoutR
|
|
|
61
61
|
active: T;
|
|
62
62
|
onChange: (id: T) => void;
|
|
63
63
|
className?: string;
|
|
64
|
-
/** Let the strip WRAP onto as many rows as it needs below `md`, instead of
|
|
65
|
-
* scrolling sideways — for a strip carrying more tabs than fit on a phone row.
|
|
66
|
-
* Keksdose live #262: ten reports on a 406px screen showed about three, and the
|
|
67
|
-
* round-2 answer (a <select> below `md`) cost a tap to open and a tap to choose.
|
|
68
|
-
* *"I would rather keep the tabs but have multirow tabs depending on the screen
|
|
69
|
-
* size, I will not loose the function to see all reports at once and navigate by
|
|
70
|
-
* single click rather by double click."*
|
|
71
|
-
*
|
|
72
|
-
* The active MARKER has to change with the layout, which is why this cannot be
|
|
73
|
-
* done from a call site with a `className`: the default marker is a `border-b-2`
|
|
74
|
-
* underline riding the container's own bottom rule, and a tab sitting in a row
|
|
75
|
-
* that does not touch that rule cannot wear it — every row but the last would
|
|
76
|
-
* show a stray line floating mid-strip, and `-mb-px` would pull each chip a
|
|
77
|
-
* pixel into the row beneath it. A wrapped strip marks the active tab with a
|
|
78
|
-
* filled brand chip instead, which is also the only thing findable at a glance
|
|
79
|
-
* among ten same-weight labels on three rows.
|
|
80
|
-
*
|
|
81
|
-
* From `md` up NOTHING changes: same single-line underline strip, same paddings,
|
|
82
|
-
* same colours as every other strip in the app.
|
|
83
|
-
*
|
|
84
|
-
* Opt-in on purpose. The three-tab strips (invoices, statements, bank imports)
|
|
85
|
-
* already fit a phone row, and turning those into chips would be an unrequested
|
|
86
|
-
* redesign of three other pages.
|
|
87
|
-
*
|
|
88
|
-
* @deprecated 0.40 — use the scrolling strip with `overflowCue`; removed in 0.41. No
|
|
89
|
-
* consumer is left (keksdose went back to one scrolling row, live #410); the caret
|
|
90
|
-
* says a phone's row goes on. */
|
|
91
|
-
wrap?: boolean;
|
|
92
64
|
/**
|
|
93
65
|
* The caret beside a SCROLLING strip below `md` (0.40, kk run 79 gap 1): a › while
|
|
94
66
|
* tabs lie past the strip's end — a tap scrolls it on by three quarters of its width —
|
|
@@ -101,7 +73,7 @@ export interface TabsProps<T extends string> extends Omit<ComponentPropsWithoutR
|
|
|
101
73
|
* turns round rather than two that come and go, so the strip's width never changes
|
|
102
74
|
* under the thumb. Out of the tab order — the keyboard walks the strip with the arrow
|
|
103
75
|
* keys, which bring the focused tab into view. From `md` up nothing changes. Ignored on
|
|
104
|
-
* a
|
|
76
|
+
* a vertical strip (a side nav is a column, not a row). Names:
|
|
105
77
|
* `tabs.more` / `tabs.backToFirst`.
|
|
106
78
|
*/
|
|
107
79
|
overflowCue?: boolean;
|
|
@@ -187,8 +159,8 @@ export interface TabsProps<T extends string> extends Omit<ComponentPropsWithoutR
|
|
|
187
159
|
* `onRemove`, `onAdd` — means exactly what it means on the horizontal strip; the
|
|
188
160
|
* badge moves to the row's end.
|
|
189
161
|
*
|
|
190
|
-
* **On a phone (`usePhoneLayout()`) it becomes the horizontal strip**,
|
|
191
|
-
*
|
|
162
|
+
* **On a phone (`usePhoneLayout()`) it becomes the horizontal strip**, rather than
|
|
163
|
+
* staying a column. A side nav only works beside its content; on
|
|
192
164
|
* a phone it has to go ABOVE it, and eight full-width rows there push the panel
|
|
193
165
|
* the user picked below the fold on every visit — the two keksdose pages both
|
|
194
166
|
* collapsed theirs into a scrolling row by hand for exactly that reason. The
|
|
@@ -203,4 +175,4 @@ export interface TabsProps<T extends string> extends Omit<ComponentPropsWithoutR
|
|
|
203
175
|
* view and measures the fade, so neither loses the element. */
|
|
204
176
|
ref?: Ref<HTMLDivElement>;
|
|
205
177
|
}
|
|
206
|
-
export declare function Tabs<T extends string>({ tabs, active, onChange, className,
|
|
178
|
+
export declare function Tabs<T extends string>({ tabs, active, onChange, className, overflowCue, panelId, onRemove, removeOn, onAdd, addLabel, labels, busy, orientation, "aria-label": ariaLabel, ref, ...rest }: TabsProps<T>): import("react").JSX.Element;
|
package/dist/components/tabs.js
CHANGED
|
@@ -17,24 +17,18 @@ const TABLIST_VERTICAL_CLASSES = "flex flex-col gap-0.5";
|
|
|
17
17
|
const TAB_VERTICAL_CLASSES = FOCUS_RING_WIDTH + " flex w-full items-center rounded-md px-3 py-2 text-start text-sm font-medium transition-colors focus:outline-none focus-visible:ring-strong";
|
|
18
18
|
const TAB_VERTICAL_ACTIVE_CLASSES = "bg-active text-primary";
|
|
19
19
|
const TAB_VERTICAL_INACTIVE_CLASSES = "text-muted hover:bg-hover hover:text-primary";
|
|
20
|
-
const TABLIST_WRAP_CLASSES = "flex flex-wrap gap-1.5 md:flex-nowrap md:gap-1 md:overflow-x-auto md:overflow-y-hidden md:border-b md:border-subtle";
|
|
21
20
|
const TAB_CLASSES = FOCUS_RING_WIDTH + " max-w-full whitespace-nowrap px-3 py-2 text-sm font-medium transition-colors border-b-2 -mb-px focus:outline-none focus-visible:ring-strong";
|
|
22
21
|
const TAB_ACTIVE_CLASSES = "border-[var(--text-primary)] text-primary";
|
|
23
22
|
const TAB_INACTIVE_CLASSES = "border-transparent text-muted hover:text-secondary hover:border-strong";
|
|
24
|
-
const
|
|
25
|
-
const TAB_WRAP_ACTIVE_CLASSES = "bg-brand text-brand-contrast md:border-[var(--text-primary)] md:text-primary";
|
|
26
|
-
const TAB_WRAP_INACTIVE_CLASSES = "bg-surface text-primary md:border-transparent md:text-muted md:hover:border-strong md:hover:text-secondary";
|
|
27
|
-
const TAB_REMOVABLE_CLASSES = "pe-8 md:pe-8";
|
|
23
|
+
const TAB_REMOVABLE_CLASSES = "pe-8";
|
|
28
24
|
const TAB_EMPTY_CLASSES = "-mx-1.5 rounded border border-dashed border-strong px-1.5";
|
|
29
25
|
const TAB_ADD_CLASSES = FOCUS_RING_WIDTH + " inline-flex items-center gap-1 whitespace-nowrap rounded-md px-3 py-2 text-sm font-medium text-muted transition-colors hover:text-primary focus:outline-none focus-visible:ring-strong disabled:cursor-not-allowed disabled:opacity-50 [&_svg]:size-4";
|
|
30
|
-
const TAB_ADD_WRAP_CLASSES = "px-2.5 text-xs md:px-3 md:text-sm";
|
|
31
26
|
const TAB_ADD_PHONE_CLASSES = "relative max-md:px-2";
|
|
32
27
|
function Tabs({
|
|
33
28
|
tabs,
|
|
34
29
|
active,
|
|
35
30
|
onChange,
|
|
36
31
|
className,
|
|
37
|
-
wrap = false,
|
|
38
32
|
overflowCue = false,
|
|
39
33
|
panelId,
|
|
40
34
|
onRemove,
|
|
@@ -54,7 +48,7 @@ function Tabs({
|
|
|
54
48
|
const stripRef = useRef(null);
|
|
55
49
|
const setStrip = useMemo(() => mergeRefs(stripRef, ref), [ref]);
|
|
56
50
|
const fade = useStripFade(stripRef);
|
|
57
|
-
const cue = overflowCue && !
|
|
51
|
+
const cue = overflowCue && !vertical && fade.overflow !== void 0;
|
|
58
52
|
const cueAtEnd = fade.overflow === "start";
|
|
59
53
|
const onCue = () => {
|
|
60
54
|
const strip = stripRef.current;
|
|
@@ -142,8 +136,8 @@ function Tabs({
|
|
|
142
136
|
style: fade.mask ? { ...rest.style, maskImage: fade.mask, WebkitMaskImage: fade.mask } : rest.style,
|
|
143
137
|
"data-overflow": fade.overflow,
|
|
144
138
|
className: cn(
|
|
145
|
-
vertical ? TABLIST_VERTICAL_CLASSES :
|
|
146
|
-
onAdd ? "min-w-0" : overflowCue && !
|
|
139
|
+
vertical ? TABLIST_VERTICAL_CLASSES : TABLIST_CLASSES,
|
|
140
|
+
onAdd ? "min-w-0" : overflowCue && !vertical ? "min-w-0 flex-1" : className
|
|
147
141
|
),
|
|
148
142
|
children: tabs.map((tab) => {
|
|
149
143
|
const isActive = tab.id === active;
|
|
@@ -174,8 +168,8 @@ function Tabs({
|
|
|
174
168
|
isActive ? TAB_VERTICAL_ACTIVE_CLASSES : TAB_VERTICAL_INACTIVE_CLASSES,
|
|
175
169
|
removable && "pe-8"
|
|
176
170
|
) : cn(
|
|
177
|
-
|
|
178
|
-
|
|
171
|
+
TAB_CLASSES,
|
|
172
|
+
isActive ? TAB_ACTIVE_CLASSES : TAB_INACTIVE_CLASSES,
|
|
179
173
|
removable && TAB_REMOVABLE_CLASSES
|
|
180
174
|
)
|
|
181
175
|
};
|
|
@@ -198,20 +192,7 @@ function Tabs({
|
|
|
198
192
|
/* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-col items-start text-start", children: [
|
|
199
193
|
/* @__PURE__ */ jsx("span", { className: "max-w-full truncate-until-large", children: tab.label }),
|
|
200
194
|
" ",
|
|
201
|
-
/* @__PURE__ */ jsx(
|
|
202
|
-
"span",
|
|
203
|
-
{
|
|
204
|
-
className: cn(
|
|
205
|
-
"max-w-full truncate-until-large text-caption font-normal leading-tight text-muted",
|
|
206
|
-
// On a wrapped strip's filled chip (below `md`) the muted grey
|
|
207
|
-
// was dark text on the brand fill — "loop 2" all but vanished
|
|
208
|
-
// under the one label that most needed reading. It takes the
|
|
209
|
-
// chip's own ink instead, as the label and the × already do.
|
|
210
|
-
wrap && isActive && !vertical && "text-brand-contrast md:text-muted"
|
|
211
|
-
),
|
|
212
|
-
children: tab.detail
|
|
213
|
-
}
|
|
214
|
-
)
|
|
195
|
+
/* @__PURE__ */ jsx("span", { className: "max-w-full truncate-until-large text-caption font-normal leading-tight text-muted", children: tab.detail })
|
|
215
196
|
] })
|
|
216
197
|
) : tab.label,
|
|
217
198
|
tab.badge != null ? vertical ? /* @__PURE__ */ jsx("span", { className: "ms-auto flex shrink-0 items-center", children: tab.badge }) : tab.badge : null
|
|
@@ -257,12 +238,7 @@ function Tabs({
|
|
|
257
238
|
type: "button",
|
|
258
239
|
disabled: busy,
|
|
259
240
|
onClick: () => requestRemove(tab.id),
|
|
260
|
-
className:
|
|
261
|
-
"absolute inset-y-0 end-1 my-auto",
|
|
262
|
-
// On a wrapped strip's filled chip the quiet grey would vanish into
|
|
263
|
-
// the brand fill below `md`.
|
|
264
|
-
wrap && "text-brand-contrast md:text-placeholder"
|
|
265
|
-
),
|
|
241
|
+
className: "absolute inset-y-0 end-1 my-auto",
|
|
266
242
|
children: /* @__PURE__ */ jsx(X, {})
|
|
267
243
|
}
|
|
268
244
|
)
|
|
@@ -284,7 +260,7 @@ function Tabs({
|
|
|
284
260
|
children: text.remove(nameOf(activeTab))
|
|
285
261
|
}
|
|
286
262
|
) : null;
|
|
287
|
-
if (!onAdd && overflowCue && !
|
|
263
|
+
if (!onAdd && overflowCue && !vertical) {
|
|
288
264
|
return /* @__PURE__ */ jsxs("div", { className: cn("flex items-end", className), children: [
|
|
289
265
|
tablist,
|
|
290
266
|
removeButton,
|
|
@@ -307,42 +283,30 @@ function Tabs({
|
|
|
307
283
|
] })
|
|
308
284
|
] });
|
|
309
285
|
}
|
|
310
|
-
return /* @__PURE__ */ jsxs(
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
cueButton,
|
|
321
|
-
/* @__PURE__ */ jsx(
|
|
322
|
-
"div",
|
|
286
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("flex items-end", className), children: [
|
|
287
|
+
tablist,
|
|
288
|
+
removeButton,
|
|
289
|
+
cueButton,
|
|
290
|
+
/* @__PURE__ */ jsx(
|
|
291
|
+
"div",
|
|
292
|
+
{
|
|
293
|
+
className: "flex flex-1 items-center self-stretch border-b border-subtle",
|
|
294
|
+
children: /* @__PURE__ */ jsxs(
|
|
295
|
+
"button",
|
|
323
296
|
{
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
),
|
|
328
|
-
children:
|
|
329
|
-
"
|
|
330
|
-
{
|
|
331
|
-
|
|
332
|
-
onClick: onAdd,
|
|
333
|
-
disabled: busy,
|
|
334
|
-
className: cn(TAB_ADD_CLASSES, wrap ? TAB_ADD_WRAP_CLASSES : TAB_ADD_PHONE_CLASSES),
|
|
335
|
-
children: [
|
|
336
|
-
/* @__PURE__ */ jsx(Plus, { "aria-hidden": true }),
|
|
337
|
-
/* @__PURE__ */ jsx("span", { className: wrap ? void 0 : "max-md:sr-only", children: addLabel ?? text.add })
|
|
338
|
-
]
|
|
339
|
-
}
|
|
340
|
-
)
|
|
297
|
+
type: "button",
|
|
298
|
+
onClick: onAdd,
|
|
299
|
+
disabled: busy,
|
|
300
|
+
className: cn(TAB_ADD_CLASSES, TAB_ADD_PHONE_CLASSES),
|
|
301
|
+
children: [
|
|
302
|
+
/* @__PURE__ */ jsx(Plus, { "aria-hidden": true }),
|
|
303
|
+
/* @__PURE__ */ jsx("span", { className: "max-md:sr-only", children: addLabel ?? text.add })
|
|
304
|
+
]
|
|
341
305
|
}
|
|
342
306
|
)
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
);
|
|
307
|
+
}
|
|
308
|
+
)
|
|
309
|
+
] });
|
|
346
310
|
}
|
|
347
311
|
export {
|
|
348
312
|
DEFAULT_TABS_LABELS,
|