@eifi1/ui-kit 0.14.0 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -84,7 +84,9 @@ interface PageHeaderProps extends Omit<ComponentPropsWithoutRef<"div">, "title">
|
|
|
84
84
|
* `CurrencySelect` as its action: a field with its label on top is taller than the
|
|
85
85
|
* title, and `stacked`'s default top alignment hangs it from the title's cap height
|
|
86
86
|
* with its control well below the title's line. `center` sets it on the title's
|
|
87
|
-
* middle, as that page's hand-written `items-center` row did.
|
|
87
|
+
* middle, as that page's hand-written `items-center` row did. It aligns the whole row
|
|
88
|
+
* (title block and actions against each other), since 0.14.1: 0.14.0 moved only the
|
|
89
|
+
* actions box, which did nothing when the actions were the taller item (keksdose H2).
|
|
88
90
|
*/
|
|
89
91
|
actionsAlign?: PageHeaderActionsAlign;
|
|
90
92
|
/**
|
|
@@ -11,9 +11,9 @@ const ROW = {
|
|
|
11
11
|
stacked: "flex-col gap-3 sm:flex-row sm:items-start sm:justify-between sm:gap-4",
|
|
12
12
|
inline: "flex-row items-center justify-between gap-2 sm:gap-4"
|
|
13
13
|
};
|
|
14
|
-
const
|
|
15
|
-
stacked: { start: "sm:
|
|
16
|
-
inline: { start: "
|
|
14
|
+
const ROW_ALIGN = {
|
|
15
|
+
stacked: { start: "sm:items-start", center: "sm:items-center", end: "sm:items-end" },
|
|
16
|
+
inline: { start: "items-start", center: "items-center", end: "items-end" }
|
|
17
17
|
};
|
|
18
18
|
function PageHeader({
|
|
19
19
|
title,
|
|
@@ -33,38 +33,49 @@ function PageHeader({
|
|
|
33
33
|
const Heading = as;
|
|
34
34
|
const hasSecondary = secondaryActions != null;
|
|
35
35
|
const wrapsSecondary = hasSecondary && mobileLayout === "inline";
|
|
36
|
-
const align = actionsAlign != null ?
|
|
36
|
+
const align = actionsAlign != null ? ROW_ALIGN[mobileLayout][actionsAlign] : void 0;
|
|
37
|
+
const keepsWords = mobileLayout === "inline" && !truncateTitle;
|
|
37
38
|
return /* @__PURE__ */ jsxs("div", { ...rest, className: cn("flex min-w-0 flex-col gap-2", className), children: [
|
|
38
39
|
breadcrumbs,
|
|
39
|
-
/* @__PURE__ */ jsxs(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"text-[var(--text-primary)]",
|
|
48
|
-
TITLE[size]
|
|
49
|
-
),
|
|
50
|
-
children: title
|
|
51
|
-
}
|
|
40
|
+
/* @__PURE__ */ jsxs(
|
|
41
|
+
"div",
|
|
42
|
+
{
|
|
43
|
+
className: cn(
|
|
44
|
+
"flex min-w-0",
|
|
45
|
+
ROW[mobileLayout],
|
|
46
|
+
keepsWords ? "flex-wrap" : wrapsSecondary && "flex-wrap sm:flex-nowrap",
|
|
47
|
+
align
|
|
52
48
|
),
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
49
|
+
children: [
|
|
50
|
+
/* @__PURE__ */ jsxs("div", { className: cn("flex-1", keepsWords ? "min-w-min" : "min-w-0"), children: [
|
|
51
|
+
eyebrow != null && /* @__PURE__ */ jsx("p", { className: cn(SECTION_LABEL_CLASS.xs, "mb-1"), children: eyebrow }),
|
|
52
|
+
/* @__PURE__ */ jsx(
|
|
53
|
+
Heading,
|
|
54
|
+
{
|
|
55
|
+
className: cn(
|
|
56
|
+
truncateTitle ? "truncate" : "break-words",
|
|
57
|
+
"text-[var(--text-primary)]",
|
|
58
|
+
TITLE[size]
|
|
59
|
+
),
|
|
60
|
+
children: title
|
|
61
|
+
}
|
|
62
|
+
),
|
|
63
|
+
description != null && /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-[var(--text-muted)]", children: description })
|
|
64
|
+
] }),
|
|
65
|
+
actions != null && /* @__PURE__ */ jsx("div", { className: cn("flex shrink-0 flex-wrap items-center gap-2", hasSecondary && "sm:order-2"), children: actions }),
|
|
66
|
+
hasSecondary && /* @__PURE__ */ jsx(
|
|
67
|
+
"div",
|
|
68
|
+
{
|
|
69
|
+
className: cn(
|
|
70
|
+
"flex flex-wrap items-center gap-2 sm:order-1 sm:shrink-0",
|
|
71
|
+
wrapsSecondary && "basis-full sm:basis-auto"
|
|
72
|
+
),
|
|
73
|
+
children: secondaryActions
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
)
|
|
68
79
|
] });
|
|
69
80
|
}
|
|
70
81
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/page-header.tsx"],"sourcesContent":["import type { ComponentPropsWithoutRef, ElementType, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { SECTION_LABEL_CLASS } from \"./text\";\n\n/**\n * `md` (default) is kastlan's `text-2xl` page title from `sm` up, a step smaller on a\n * phone; `sm` is lenkbank's `text-lg` title on a dense tool page (projects-page.tsx).\n *\n * `compact` (0.13) is keksdose's page title (keksdose F4): `text-xl font-semibold` at\n * EVERY width — budgets, rules, accounts, transactions and budget pages all write\n * exactly that `<h1>` by hand. It is `md`'s phone size without `md`'s step up to\n * `text-2xl` on a wider screen, and without its `tracking-tight`, which keksdose's\n * titles never had. A word and not a letter for the reason `ProgressBar`'s `slim` is\n * one: the letters are taken in order, and there is none between `sm` and `md` — an\n * `lg` or `xl` at `text-xl` would read as LARGER than `md`, which from `sm` up it is\n * not.\n */\nexport type PageHeaderSize = \"sm\" | \"compact\" | \"md\";\n\nconst TITLE: Record<PageHeaderSize, string> = {\n sm: \"text-lg font-semibold\",\n compact: \"text-xl font-semibold\",\n md: \"text-xl font-semibold tracking-tight sm:text-2xl\",\n};\n\n/**\n * How the title block and the actions sit below the `sm` breakpoint. From `sm` up both\n * layouts are one row: title block at the start, actions at the end.\n */\nexport type PageHeaderMobileLayout = \"stacked\" | \"inline\";\n\nconst ROW: Record<PageHeaderMobileLayout, string> = {\n stacked: \"flex-col gap-3 sm:flex-row sm:items-start sm:justify-between sm:gap-4\",\n inline: \"flex-row items-center justify-between gap-2 sm:gap-4\",\n};\n\n/**\n * Where the actions sit on the title block's height, in the one row the header is from\n * `sm` up (and on a phone too when {@link PageHeaderMobileLayout} is `inline`, the only\n * layout that has a row there). Unset keeps each layout's own: `start` for `stacked`,\n * `center` for `inline`.\n */\nexport type PageHeaderActionsAlign = \"start\" | \"center\" | \"end\";\n\n/** Per layout, because `stacked` is a column on a phone — a `self-center` there would\n * centre the actions HORIZONTALLY — so it only aligns from `sm` up. */\nconst ACTIONS_ALIGN: Record<PageHeaderMobileLayout, Record<PageHeaderActionsAlign, string>> = {\n stacked: { start: \"sm:self-start\", center: \"sm:self-center\", end: \"sm:self-end\" },\n inline: { start: \"self-start\", center: \"self-center\", end: \"self-end\" },\n};\n\nexport interface PageHeaderProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"title\"> {\n title: ReactNode;\n /** A sentence under the title. */\n description?: ReactNode;\n /** Buttons for the page — \"New\", \"Export\". At the title's end from `sm` up; under it,\n * wrapping, on a phone — unless {@link mobileLayout} is `inline`. */\n actions?: ReactNode;\n /** A `Breadcrumbs` trail, drawn above everything else. */\n breadcrumbs?: ReactNode;\n /** A small uppercase line above the title — the section or the kind of record\n * (\"Building\", \"Settings\"). The section-label type. */\n eyebrow?: ReactNode;\n /** The title's element. Default `h1`: a page has one, and this is it. `h2` for a\n * header inside a page that already has its `h1` (a tab's own header). */\n as?: \"h1\" | \"h2\" | \"h3\";\n size?: PageHeaderSize;\n /**\n * `stacked` (default): on a phone the actions drop under the title and wrap, so a\n * row of three buttons never pushes the title off the screen.\n *\n * `inline`: the actions stay beside the title on a phone too, vertically centred on\n * it — keksdose's headers (transactions-page.tsx:515, `flex items-center\n * justify-between gap-2`), which carry one or two icon-sized controls and were tuned\n * for the phone in live #263. Adopting `PageHeader` must not undo that tuning, so the\n * row stays a row (keksdose F4). The actions do NOT wrap or shrink here: `inline` is\n * for a header whose actions are known to fit beside a phone-width title (an icon\n * button, a month picker); a long title wraps to make room for them instead.\n */\n mobileLayout?: PageHeaderMobileLayout;\n /**\n * A second group of actions that gets its OWN full-width row under the header on a\n * phone, and joins the actions' row from `sm` up, between the title and\n * {@link actions}.\n *\n * keksdose's budget page (budget-page.tsx, feedback #60) is the case: title and month\n * navigation on row 1, the fold/unfold toggles on row 2 on a phone — \"two tidy rows\n * instead of the old ragged justify-between overflow\" — and one row, title | toggles |\n * month nav, on a wider screen. It builds that by hand with a wrapping flex row and\n * swapped order utilities; adopting `PageHeader` (keksdose G6b) must not lose it.\n * There: `actions` is the month navigation, this is the toggles.\n *\n * The DOM order is title, `actions`, `secondaryActions` — the phone's visual order,\n * and the one a screen reader and the Tab key follow at every width. From `sm` up the\n * two groups swap places visually only, which keeps the page's primary control (the\n * month) at the row's end, where keksdose had it, and first in reading order. The\n * breakpoint is `sm`, the one {@link mobileLayout} switches at, not keksdose's `md`:\n * one header, one breakpoint.\n */\n secondaryActions?: ReactNode;\n /**\n * The actions' vertical alignment against the title block — see\n * {@link PageHeaderActionsAlign}. Unset keeps the layout's own.\n *\n * keksdose's reports page (reports-page.tsx, keksdose G8) carries a LABELLED\n * `CurrencySelect` as its action: a field with its label on top is taller than the\n * title, and `stacked`'s default top alignment hangs it from the title's cap height\n * with its control well below the title's line. `center` sets it on the title's\n * middle, as that page's hand-written `items-center` row did.\n */\n actionsAlign?: PageHeaderActionsAlign;\n /**\n * One line, cut with an ellipsis, instead of the default breaking of a long word onto\n * as many lines as it needs.\n *\n * keksdose's payees page (payees-page.tsx, live #263, keksdose G6a) wraps its title in\n * its own truncating span so a long name gives way to the `inline` header's actions\n * instead of pushing them. This is that span's behaviour on the heading itself; the\n * title block is already allowed to shrink below its content, which is what lets the\n * cut happen inside a flex row.\n *\n * The full title stays in the DOM as the heading's text, so a screen reader and the\n * document outline read all of it. There is deliberately no native `title` tooltip:\n * the kit does not use them (not on touch, not on keyboard focus, not styled).\n */\n truncateTitle?: boolean;\n}\n\n/**\n * A page's head: optional breadcrumbs, an eyebrow, the title, a description, and the\n * page's actions.\n *\n * kastlan's (shared/components/display/page-header.tsx) is the model — title and\n * description on one side, actions on the other, stacked on a phone — and this adds\n * what its pages kept writing around it: a trail above, an eyebrow, a `ReactNode`\n * title (a status chip beside the name), and actions that WRAP when there are more\n * than fit a phone's width, instead of running off it. No outer margin: the page's\n * own rhythm (`space-y-*`, a grid gap) places it.\n */\nexport function PageHeader({\n title,\n description,\n actions,\n breadcrumbs,\n eyebrow,\n as = \"h1\",\n size = \"md\",\n mobileLayout = \"stacked\",\n secondaryActions,\n actionsAlign,\n truncateTitle = false,\n className,\n ...rest\n}: PageHeaderProps) {\n const Heading = as as ElementType;\n const hasSecondary = secondaryActions != null;\n const wrapsSecondary = hasSecondary && mobileLayout === \"inline\";\n const align = actionsAlign != null ? ACTIONS_ALIGN[mobileLayout][actionsAlign] : undefined;\n return (\n <div {...rest} className={cn(\"flex min-w-0 flex-col gap-2\", className)}>\n {breadcrumbs}\n {/* With a second group, `inline` wraps so that group's full-width basis puts it\n on a row of its own on a phone (`stacked` is a column there already, and a\n basis would size a column item's HEIGHT); from `sm` up the row stops wrapping\n and the order utilities seat it between the title and the actions. Without\n one, nothing changes. */}\n <div className={cn(\"flex min-w-0\", ROW[mobileLayout], wrapsSecondary && \"flex-wrap sm:flex-nowrap\")}>\n <div className=\"min-w-0 flex-1\">\n {eyebrow != null && <p className={cn(SECTION_LABEL_CLASS.xs, \"mb-1\")}>{eyebrow}</p>}\n <Heading\n className={cn(\n truncateTitle ? \"truncate\" : \"break-words\",\n \"text-[var(--text-primary)]\",\n TITLE[size],\n )}\n >\n {title}\n </Heading>\n {description != null && <p className=\"mt-1 text-sm text-[var(--text-muted)]\">{description}</p>}\n </div>\n {actions != null && (\n <div className={cn(\"flex shrink-0 flex-wrap items-center gap-2\", hasSecondary && \"sm:order-2\", align)}>\n {actions}\n </div>\n )}\n {hasSecondary && (\n <div\n className={cn(\n \"flex flex-wrap items-center gap-2 sm:order-1 sm:shrink-0\",\n wrapsSecondary && \"basis-full sm:basis-auto\",\n align,\n )}\n >\n {secondaryActions}\n </div>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":";AAuKQ,SACsB,KADtB;AAtKR,SAAS,UAAU;AACnB,SAAS,2BAA2B;AAiBpC,MAAM,QAAwC;AAAA,EAC5C,IAAI;AAAA,EACJ,SAAS;AAAA,EACT,IAAI;AACN;AAQA,MAAM,MAA8C;AAAA,EAClD,SAAS;AAAA,EACT,QAAQ;AACV;AAYA,MAAM,gBAAwF;AAAA,EAC5F,SAAS,EAAE,OAAO,iBAAiB,QAAQ,kBAAkB,KAAK,cAAc;AAAA,EAChF,QAAQ,EAAE,OAAO,cAAc,QAAQ,eAAe,KAAK,WAAW;AACxE;AA0FO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,KAAK;AAAA,EACL,OAAO;AAAA,EACP,eAAe;AAAA,EACf;AAAA,EACA;AAAA,EACA,gBAAgB;AAAA,EAChB;AAAA,EACA,GAAG;AACL,GAAoB;AAClB,QAAM,UAAU;AAChB,QAAM,eAAe,oBAAoB;AACzC,QAAM,iBAAiB,gBAAgB,iBAAiB;AACxD,QAAM,QAAQ,gBAAgB,OAAO,cAAc,YAAY,EAAE,YAAY,IAAI;AACjF,SACE,qBAAC,SAAK,GAAG,MAAM,WAAW,GAAG,+BAA+B,SAAS,GAClE;AAAA;AAAA,IAMD,qBAAC,SAAI,WAAW,GAAG,gBAAgB,IAAI,YAAY,GAAG,kBAAkB,0BAA0B,GAChG;AAAA,2BAAC,SAAI,WAAU,kBACZ;AAAA,mBAAW,QAAQ,oBAAC,OAAE,WAAW,GAAG,oBAAoB,IAAI,MAAM,GAAI,mBAAQ;AAAA,QAC/E;AAAA,UAAC;AAAA;AAAA,YACC,WAAW;AAAA,cACT,gBAAgB,aAAa;AAAA,cAC7B;AAAA,cACA,MAAM,IAAI;AAAA,YACZ;AAAA,YAEC;AAAA;AAAA,QACH;AAAA,QACC,eAAe,QAAQ,oBAAC,OAAE,WAAU,yCAAyC,uBAAY;AAAA,SAC5F;AAAA,MACC,WAAW,QACV,oBAAC,SAAI,WAAW,GAAG,8CAA8C,gBAAgB,cAAc,KAAK,GACjG,mBACH;AAAA,MAED,gBACC;AAAA,QAAC;AAAA;AAAA,UACC,WAAW;AAAA,YACT;AAAA,YACA,kBAAkB;AAAA,YAClB;AAAA,UACF;AAAA,UAEC;AAAA;AAAA,MACH;AAAA,OAEJ;AAAA,KACF;AAEJ;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/page-header.tsx"],"sourcesContent":["import type { ComponentPropsWithoutRef, ElementType, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { SECTION_LABEL_CLASS } from \"./text\";\n\n/**\n * `md` (default) is kastlan's `text-2xl` page title from `sm` up, a step smaller on a\n * phone; `sm` is lenkbank's `text-lg` title on a dense tool page (projects-page.tsx).\n *\n * `compact` (0.13) is keksdose's page title (keksdose F4): `text-xl font-semibold` at\n * EVERY width — budgets, rules, accounts, transactions and budget pages all write\n * exactly that `<h1>` by hand. It is `md`'s phone size without `md`'s step up to\n * `text-2xl` on a wider screen, and without its `tracking-tight`, which keksdose's\n * titles never had. A word and not a letter for the reason `ProgressBar`'s `slim` is\n * one: the letters are taken in order, and there is none between `sm` and `md` — an\n * `lg` or `xl` at `text-xl` would read as LARGER than `md`, which from `sm` up it is\n * not.\n */\nexport type PageHeaderSize = \"sm\" | \"compact\" | \"md\";\n\nconst TITLE: Record<PageHeaderSize, string> = {\n sm: \"text-lg font-semibold\",\n compact: \"text-xl font-semibold\",\n md: \"text-xl font-semibold tracking-tight sm:text-2xl\",\n};\n\n/**\n * How the title block and the actions sit below the `sm` breakpoint. From `sm` up both\n * layouts are one row: title block at the start, actions at the end.\n */\nexport type PageHeaderMobileLayout = \"stacked\" | \"inline\";\n\nconst ROW: Record<PageHeaderMobileLayout, string> = {\n stacked: \"flex-col gap-3 sm:flex-row sm:items-start sm:justify-between sm:gap-4\",\n inline: \"flex-row items-center justify-between gap-2 sm:gap-4\",\n};\n\n/**\n * Where the actions sit on the title block's height, in the one row the header is from\n * `sm` up (and on a phone too when {@link PageHeaderMobileLayout} is `inline`, the only\n * layout that has a row there). Unset keeps each layout's own: `start` for `stacked`,\n * `center` for `inline`.\n */\nexport type PageHeaderActionsAlign = \"start\" | \"center\" | \"end\";\n\n/** Applied to the ROW, so the title block and the actions are aligned against each\n * other on one cross axis. 0.14.0 put `self-*` on the actions box alone, which moved\n * nothing when the actions were the taller item — they already filled the row, and\n * the title stayed at the top (keksdose H2, reports-page's two-line labelled\n * CurrencySelect). Per layout, because `stacked` is a column on a phone, where\n * `items-center` would centre everything HORIZONTALLY — so it aligns from `sm` up. */\nconst ROW_ALIGN: Record<PageHeaderMobileLayout, Record<PageHeaderActionsAlign, string>> = {\n stacked: { start: \"sm:items-start\", center: \"sm:items-center\", end: \"sm:items-end\" },\n inline: { start: \"items-start\", center: \"items-center\", end: \"items-end\" },\n};\n\nexport interface PageHeaderProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"title\"> {\n title: ReactNode;\n /** A sentence under the title. */\n description?: ReactNode;\n /** Buttons for the page — \"New\", \"Export\". At the title's end from `sm` up; under it,\n * wrapping, on a phone — unless {@link mobileLayout} is `inline`. */\n actions?: ReactNode;\n /** A `Breadcrumbs` trail, drawn above everything else. */\n breadcrumbs?: ReactNode;\n /** A small uppercase line above the title — the section or the kind of record\n * (\"Building\", \"Settings\"). The section-label type. */\n eyebrow?: ReactNode;\n /** The title's element. Default `h1`: a page has one, and this is it. `h2` for a\n * header inside a page that already has its `h1` (a tab's own header). */\n as?: \"h1\" | \"h2\" | \"h3\";\n size?: PageHeaderSize;\n /**\n * `stacked` (default): on a phone the actions drop under the title and wrap, so a\n * row of three buttons never pushes the title off the screen.\n *\n * `inline`: the actions stay beside the title on a phone too, vertically centred on\n * it — keksdose's headers (transactions-page.tsx:515, `flex items-center\n * justify-between gap-2`), which carry one or two icon-sized controls and were tuned\n * for the phone in live #263. Adopting `PageHeader` must not undo that tuning, so the\n * row stays a row (keksdose F4). The actions do NOT wrap or shrink here: `inline` is\n * for a header whose actions are known to fit beside a phone-width title (an icon\n * button, a month picker); a long title wraps to make room for them instead.\n */\n mobileLayout?: PageHeaderMobileLayout;\n /**\n * A second group of actions that gets its OWN full-width row under the header on a\n * phone, and joins the actions' row from `sm` up, between the title and\n * {@link actions}.\n *\n * keksdose's budget page (budget-page.tsx, feedback #60) is the case: title and month\n * navigation on row 1, the fold/unfold toggles on row 2 on a phone — \"two tidy rows\n * instead of the old ragged justify-between overflow\" — and one row, title | toggles |\n * month nav, on a wider screen. It builds that by hand with a wrapping flex row and\n * swapped order utilities; adopting `PageHeader` (keksdose G6b) must not lose it.\n * There: `actions` is the month navigation, this is the toggles.\n *\n * The DOM order is title, `actions`, `secondaryActions` — the phone's visual order,\n * and the one a screen reader and the Tab key follow at every width. From `sm` up the\n * two groups swap places visually only, which keeps the page's primary control (the\n * month) at the row's end, where keksdose had it, and first in reading order. The\n * breakpoint is `sm`, the one {@link mobileLayout} switches at, not keksdose's `md`:\n * one header, one breakpoint.\n */\n secondaryActions?: ReactNode;\n /**\n * The actions' vertical alignment against the title block — see\n * {@link PageHeaderActionsAlign}. Unset keeps the layout's own.\n *\n * keksdose's reports page (reports-page.tsx, keksdose G8) carries a LABELLED\n * `CurrencySelect` as its action: a field with its label on top is taller than the\n * title, and `stacked`'s default top alignment hangs it from the title's cap height\n * with its control well below the title's line. `center` sets it on the title's\n * middle, as that page's hand-written `items-center` row did. It aligns the whole row\n * (title block and actions against each other), since 0.14.1: 0.14.0 moved only the\n * actions box, which did nothing when the actions were the taller item (keksdose H2).\n */\n actionsAlign?: PageHeaderActionsAlign;\n /**\n * One line, cut with an ellipsis, instead of the default breaking of a long word onto\n * as many lines as it needs.\n *\n * keksdose's payees page (payees-page.tsx, live #263, keksdose G6a) wraps its title in\n * its own truncating span so a long name gives way to the `inline` header's actions\n * instead of pushing them. This is that span's behaviour on the heading itself; the\n * title block is already allowed to shrink below its content, which is what lets the\n * cut happen inside a flex row.\n *\n * The full title stays in the DOM as the heading's text, so a screen reader and the\n * document outline read all of it. There is deliberately no native `title` tooltip:\n * the kit does not use them (not on touch, not on keyboard focus, not styled).\n */\n truncateTitle?: boolean;\n}\n\n/**\n * A page's head: optional breadcrumbs, an eyebrow, the title, a description, and the\n * page's actions.\n *\n * kastlan's (shared/components/display/page-header.tsx) is the model — title and\n * description on one side, actions on the other, stacked on a phone — and this adds\n * what its pages kept writing around it: a trail above, an eyebrow, a `ReactNode`\n * title (a status chip beside the name), and actions that WRAP when there are more\n * than fit a phone's width, instead of running off it. No outer margin: the page's\n * own rhythm (`space-y-*`, a grid gap) places it.\n */\nexport function PageHeader({\n title,\n description,\n actions,\n breadcrumbs,\n eyebrow,\n as = \"h1\",\n size = \"md\",\n mobileLayout = \"stacked\",\n secondaryActions,\n actionsAlign,\n truncateTitle = false,\n className,\n ...rest\n}: PageHeaderProps) {\n const Heading = as as ElementType;\n const hasSecondary = secondaryActions != null;\n const wrapsSecondary = hasSecondary && mobileLayout === \"inline\";\n const align = actionsAlign != null ? ROW_ALIGN[mobileLayout][actionsAlign] : undefined;\n // keksdose H1: in `inline` the title block could shrink below its longest word, and\n // `break-words` then split \"Monatsbudget\" into \"Monatsbudg\" / \"et\" beside the actions.\n // Unless it truncates, the title keeps its longest word (min-content) and the ROW\n // wraps instead: actions that do not fit go to a line of their own, as the app's old\n // `flex-wrap` header did.\n const keepsWords = mobileLayout === \"inline\" && !truncateTitle;\n return (\n <div {...rest} className={cn(\"flex min-w-0 flex-col gap-2\", className)}>\n {breadcrumbs}\n {/* With a second group, `inline` wraps so that group's full-width basis puts it\n on a row of its own on a phone (`stacked` is a column there already, and a\n basis would size a column item's HEIGHT); from `sm` up the row stops wrapping\n and the order utilities seat it between the title and the actions. Without\n one, nothing changes. */}\n <div\n className={cn(\n \"flex min-w-0\",\n ROW[mobileLayout],\n keepsWords ? \"flex-wrap\" : wrapsSecondary && \"flex-wrap sm:flex-nowrap\",\n align,\n )}\n >\n <div className={cn(\"flex-1\", keepsWords ? \"min-w-min\" : \"min-w-0\")}>\n {eyebrow != null && <p className={cn(SECTION_LABEL_CLASS.xs, \"mb-1\")}>{eyebrow}</p>}\n <Heading\n className={cn(\n truncateTitle ? \"truncate\" : \"break-words\",\n \"text-[var(--text-primary)]\",\n TITLE[size],\n )}\n >\n {title}\n </Heading>\n {description != null && <p className=\"mt-1 text-sm text-[var(--text-muted)]\">{description}</p>}\n </div>\n {actions != null && (\n <div className={cn(\"flex shrink-0 flex-wrap items-center gap-2\", hasSecondary && \"sm:order-2\")}>\n {actions}\n </div>\n )}\n {hasSecondary && (\n <div\n className={cn(\n \"flex flex-wrap items-center gap-2 sm:order-1 sm:shrink-0\",\n wrapsSecondary && \"basis-full sm:basis-auto\",\n )}\n >\n {secondaryActions}\n </div>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":";AA0LQ,SACsB,KADtB;AAzLR,SAAS,UAAU;AACnB,SAAS,2BAA2B;AAiBpC,MAAM,QAAwC;AAAA,EAC5C,IAAI;AAAA,EACJ,SAAS;AAAA,EACT,IAAI;AACN;AAQA,MAAM,MAA8C;AAAA,EAClD,SAAS;AAAA,EACT,QAAQ;AACV;AAgBA,MAAM,YAAoF;AAAA,EACxF,SAAS,EAAE,OAAO,kBAAkB,QAAQ,mBAAmB,KAAK,eAAe;AAAA,EACnF,QAAQ,EAAE,OAAO,eAAe,QAAQ,gBAAgB,KAAK,YAAY;AAC3E;AA4FO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,KAAK;AAAA,EACL,OAAO;AAAA,EACP,eAAe;AAAA,EACf;AAAA,EACA;AAAA,EACA,gBAAgB;AAAA,EAChB;AAAA,EACA,GAAG;AACL,GAAoB;AAClB,QAAM,UAAU;AAChB,QAAM,eAAe,oBAAoB;AACzC,QAAM,iBAAiB,gBAAgB,iBAAiB;AACxD,QAAM,QAAQ,gBAAgB,OAAO,UAAU,YAAY,EAAE,YAAY,IAAI;AAM7E,QAAM,aAAa,iBAAiB,YAAY,CAAC;AACjD,SACE,qBAAC,SAAK,GAAG,MAAM,WAAW,GAAG,+BAA+B,SAAS,GAClE;AAAA;AAAA,IAMD;AAAA,MAAC;AAAA;AAAA,QACC,WAAW;AAAA,UACT;AAAA,UACA,IAAI,YAAY;AAAA,UAChB,aAAa,cAAc,kBAAkB;AAAA,UAC7C;AAAA,QACF;AAAA,QAEA;AAAA,+BAAC,SAAI,WAAW,GAAG,UAAU,aAAa,cAAc,SAAS,GAC9D;AAAA,uBAAW,QAAQ,oBAAC,OAAE,WAAW,GAAG,oBAAoB,IAAI,MAAM,GAAI,mBAAQ;AAAA,YAC/E;AAAA,cAAC;AAAA;AAAA,gBACC,WAAW;AAAA,kBACT,gBAAgB,aAAa;AAAA,kBAC7B;AAAA,kBACA,MAAM,IAAI;AAAA,gBACZ;AAAA,gBAEC;AAAA;AAAA,YACH;AAAA,YACC,eAAe,QAAQ,oBAAC,OAAE,WAAU,yCAAyC,uBAAY;AAAA,aAC5F;AAAA,UACC,WAAW,QACV,oBAAC,SAAI,WAAW,GAAG,8CAA8C,gBAAgB,YAAY,GAC1F,mBACH;AAAA,UAED,gBACC;AAAA,YAAC;AAAA;AAAA,cACC,WAAW;AAAA,gBACT;AAAA,gBACA,kBAAkB;AAAA,cACpB;AAAA,cAEC;AAAA;AAAA,UACH;AAAA;AAAA;AAAA,IAEJ;AAAA,KACF;AAEJ;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eifi1/ui-kit",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "App-agnostic shared design system: domain-free React components, theme and tokens.",
|
|
6
6
|
"_sideEffects": "The barrel (src/index.ts) re-exports EVERY component, including components/chart, which statically imports recharts. Without this field a bundler must assume each of those modules has side effects, so it cannot drop the unused ones: importing a Button from '@eifi1/ui-kit' dragged all 433KB of recharts into the consumer's ENTRY chunk, and Keksdose was preloading it on every page load, login included. Nothing in src/ imports for side effects (no bare imports, no CSS imports from JS, no module-level global writes) — the token stylesheet is its own './tokens.css' export — so this is safe. CSS is still listed so any future stylesheet survives. Re-checked when `flag-icons` left `dependencies` and tsup started emitting a `\"use client\"` banner: neither touches this. The flag stylesheet was never imported from JS in the first place (that was the finding), and a directive prologue is a string literal every bundler lifts out before it decides whether a module is inert.",
|
|
@@ -42,11 +42,15 @@ const ROW: Record<PageHeaderMobileLayout, string> = {
|
|
|
42
42
|
*/
|
|
43
43
|
export type PageHeaderActionsAlign = "start" | "center" | "end";
|
|
44
44
|
|
|
45
|
-
/**
|
|
46
|
-
*
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
/** Applied to the ROW, so the title block and the actions are aligned against each
|
|
46
|
+
* other on one cross axis. 0.14.0 put `self-*` on the actions box alone, which moved
|
|
47
|
+
* nothing when the actions were the taller item — they already filled the row, and
|
|
48
|
+
* the title stayed at the top (keksdose H2, reports-page's two-line labelled
|
|
49
|
+
* CurrencySelect). Per layout, because `stacked` is a column on a phone, where
|
|
50
|
+
* `items-center` would centre everything HORIZONTALLY — so it aligns from `sm` up. */
|
|
51
|
+
const ROW_ALIGN: Record<PageHeaderMobileLayout, Record<PageHeaderActionsAlign, string>> = {
|
|
52
|
+
stacked: { start: "sm:items-start", center: "sm:items-center", end: "sm:items-end" },
|
|
53
|
+
inline: { start: "items-start", center: "items-center", end: "items-end" },
|
|
50
54
|
};
|
|
51
55
|
|
|
52
56
|
export interface PageHeaderProps extends Omit<ComponentPropsWithoutRef<"div">, "title"> {
|
|
@@ -106,7 +110,9 @@ export interface PageHeaderProps extends Omit<ComponentPropsWithoutRef<"div">, "
|
|
|
106
110
|
* `CurrencySelect` as its action: a field with its label on top is taller than the
|
|
107
111
|
* title, and `stacked`'s default top alignment hangs it from the title's cap height
|
|
108
112
|
* with its control well below the title's line. `center` sets it on the title's
|
|
109
|
-
* middle, as that page's hand-written `items-center` row did.
|
|
113
|
+
* middle, as that page's hand-written `items-center` row did. It aligns the whole row
|
|
114
|
+
* (title block and actions against each other), since 0.14.1: 0.14.0 moved only the
|
|
115
|
+
* actions box, which did nothing when the actions were the taller item (keksdose H2).
|
|
110
116
|
*/
|
|
111
117
|
actionsAlign?: PageHeaderActionsAlign;
|
|
112
118
|
/**
|
|
@@ -155,7 +161,13 @@ export function PageHeader({
|
|
|
155
161
|
const Heading = as as ElementType;
|
|
156
162
|
const hasSecondary = secondaryActions != null;
|
|
157
163
|
const wrapsSecondary = hasSecondary && mobileLayout === "inline";
|
|
158
|
-
const align = actionsAlign != null ?
|
|
164
|
+
const align = actionsAlign != null ? ROW_ALIGN[mobileLayout][actionsAlign] : undefined;
|
|
165
|
+
// keksdose H1: in `inline` the title block could shrink below its longest word, and
|
|
166
|
+
// `break-words` then split "Monatsbudget" into "Monatsbudg" / "et" beside the actions.
|
|
167
|
+
// Unless it truncates, the title keeps its longest word (min-content) and the ROW
|
|
168
|
+
// wraps instead: actions that do not fit go to a line of their own, as the app's old
|
|
169
|
+
// `flex-wrap` header did.
|
|
170
|
+
const keepsWords = mobileLayout === "inline" && !truncateTitle;
|
|
159
171
|
return (
|
|
160
172
|
<div {...rest} className={cn("flex min-w-0 flex-col gap-2", className)}>
|
|
161
173
|
{breadcrumbs}
|
|
@@ -164,8 +176,15 @@ export function PageHeader({
|
|
|
164
176
|
basis would size a column item's HEIGHT); from `sm` up the row stops wrapping
|
|
165
177
|
and the order utilities seat it between the title and the actions. Without
|
|
166
178
|
one, nothing changes. */}
|
|
167
|
-
<div
|
|
168
|
-
|
|
179
|
+
<div
|
|
180
|
+
className={cn(
|
|
181
|
+
"flex min-w-0",
|
|
182
|
+
ROW[mobileLayout],
|
|
183
|
+
keepsWords ? "flex-wrap" : wrapsSecondary && "flex-wrap sm:flex-nowrap",
|
|
184
|
+
align,
|
|
185
|
+
)}
|
|
186
|
+
>
|
|
187
|
+
<div className={cn("flex-1", keepsWords ? "min-w-min" : "min-w-0")}>
|
|
169
188
|
{eyebrow != null && <p className={cn(SECTION_LABEL_CLASS.xs, "mb-1")}>{eyebrow}</p>}
|
|
170
189
|
<Heading
|
|
171
190
|
className={cn(
|
|
@@ -179,7 +198,7 @@ export function PageHeader({
|
|
|
179
198
|
{description != null && <p className="mt-1 text-sm text-[var(--text-muted)]">{description}</p>}
|
|
180
199
|
</div>
|
|
181
200
|
{actions != null && (
|
|
182
|
-
<div className={cn("flex shrink-0 flex-wrap items-center gap-2", hasSecondary && "sm:order-2"
|
|
201
|
+
<div className={cn("flex shrink-0 flex-wrap items-center gap-2", hasSecondary && "sm:order-2")}>
|
|
183
202
|
{actions}
|
|
184
203
|
</div>
|
|
185
204
|
)}
|
|
@@ -188,7 +207,6 @@ export function PageHeader({
|
|
|
188
207
|
className={cn(
|
|
189
208
|
"flex flex-wrap items-center gap-2 sm:order-1 sm:shrink-0",
|
|
190
209
|
wrapsSecondary && "basis-full sm:basis-auto",
|
|
191
|
-
align,
|
|
192
210
|
)}
|
|
193
211
|
>
|
|
194
212
|
{secondaryActions}
|