@eifi1/ui-kit 0.10.0 → 0.11.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 +20 -16
- package/dist/components/alert-banner.d.ts +26 -1
- package/dist/components/alert-banner.js +10 -7
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +2 -0
- package/dist/components/bulk-action-bar.d.ts +51 -5
- package/dist/components/bulk-action-bar.js +88 -41
- package/dist/components/bulk-action-bar.js.map +1 -1
- package/dist/components/button-group.d.ts +58 -3
- package/dist/components/button-group.js +56 -5
- package/dist/components/button-group.js.map +1 -1
- package/dist/components/calculator.d.ts +2 -0
- package/dist/components/calendar-heatmap.d.ts +124 -0
- package/dist/components/calendar-heatmap.js +295 -0
- package/dist/components/calendar-heatmap.js.map +1 -0
- package/dist/components/choice-card.d.ts +34 -2
- package/dist/components/choice-card.js +49 -0
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/copy-button.d.ts +1 -0
- package/dist/components/data-table.js +2 -0
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/field.d.ts +43 -0
- package/dist/components/field.js +52 -0
- package/dist/components/field.js.map +1 -0
- package/dist/components/file-button.d.ts +1 -0
- package/dist/components/file-dropzone.d.ts +1 -0
- package/dist/components/floating-panel.d.ts +147 -4
- package/dist/components/floating-panel.js +188 -28
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/list.d.ts +14 -1
- package/dist/components/list.js +7 -1
- package/dist/components/list.js.map +1 -1
- package/dist/components/menu-item.d.ts +11 -1
- package/dist/components/menu-item.js +6 -1
- package/dist/components/menu-item.js.map +1 -1
- package/dist/components/mini-calendar.d.ts +70 -5
- package/dist/components/mini-calendar.js +166 -62
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/nav-pills.d.ts +66 -0
- package/dist/components/nav-pills.js +65 -0
- package/dist/components/nav-pills.js.map +1 -0
- package/dist/components/number-field.d.ts +2 -0
- package/dist/components/number-input.d.ts +2 -0
- package/dist/components/numpad-sheet.d.ts +2 -0
- package/dist/components/progress-bar.d.ts +31 -2
- package/dist/components/progress-bar.js +73 -7
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/series-chart.d.ts +16 -0
- package/dist/components/series-chart.js +4 -1
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/settings-fields.d.ts +1 -0
- package/dist/components/status-dot.d.ts +6 -2
- package/dist/components/status-dot.js +6 -1
- package/dist/components/status-dot.js.map +1 -1
- package/dist/components/table.d.ts +40 -8
- package/dist/components/table.js +41 -7
- package/dist/components/table.js.map +1 -1
- package/dist/components/text.d.ts +8 -1
- package/dist/components/text.js +1 -0
- package/dist/components/text.js.map +1 -1
- package/dist/components/time-input.d.ts +1 -0
- package/dist/components/toggle-group.d.ts +15 -0
- package/dist/components/toggle-group.js +23 -3
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/tooltip.d.ts +11 -3
- package/dist/components/tooltip.js +2 -0
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/ui.d.ts +55 -4
- package/dist/components/ui.js +84 -7
- package/dist/components/ui.js.map +1 -1
- package/dist/components/user-avatar.d.ts +25 -3
- package/dist/components/user-avatar.js +32 -3
- package/dist/components/user-avatar.js.map +1 -1
- package/dist/hooks/use-file-drop.d.ts +1 -0
- package/dist/i18n/defaults.d.ts +2 -0
- package/dist/i18n/defaults.js +2 -0
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +3 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH-informal.d.ts +2 -0
- package/dist/i18n/locales/de-CH.d.ts +2 -0
- package/dist/i18n/locales/de-informal.d.ts +2 -0
- package/dist/i18n/locales/de.d.ts +2 -0
- package/dist/i18n/locales/de.js +9 -1
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +2 -0
- package/dist/i18n/locales/es.js +9 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +2 -0
- package/dist/i18n/locales/fr.js +9 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +2 -0
- package/dist/i18n/locales/hu.js +9 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +2 -0
- package/dist/i18n/locales/it.js +9 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +2 -0
- package/dist/i18n/locales/zh.js +9 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +14 -10
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/clipping.d.ts +21 -5
- package/dist/lib/clipping.js +3 -0
- package/dist/lib/clipping.js.map +1 -1
- package/dist/lib/dates.d.ts +17 -1
- package/dist/lib/dates.js +19 -0
- package/dist/lib/dates.js.map +1 -1
- package/dist/rhf/form.d.ts +1 -0
- package/dist/rhf/use-rhf-wizard-step.d.ts +22 -0
- package/dist/rhf/use-rhf-wizard-step.js +38 -0
- package/dist/rhf/use-rhf-wizard-step.js.map +1 -0
- package/dist/rhf.d.ts +2 -0
- package/dist/rhf.js +1 -0
- package/dist/rhf.js.map +1 -1
- package/dist/shell/topbar-action-menu.d.ts +73 -3
- package/dist/shell/topbar-action-menu.js +100 -27
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell.d.ts +2 -1
- package/dist/wizard/stepper-nav.d.ts +1 -0
- package/dist/wizard/wizard-context.d.ts +7 -1
- package/dist/wizard/wizard-context.js +4 -0
- package/dist/wizard/wizard-context.js.map +1 -1
- package/dist/wizard.d.ts +2 -1
- package/package.json +1 -1
- package/src/components/alert-banner.tsx +36 -7
- package/src/components/bulk-action-bar.tsx +163 -57
- package/src/components/button-group.tsx +124 -4
- package/src/components/calendar-heatmap.tsx +504 -0
- package/src/components/choice-card.tsx +97 -1
- package/src/components/data-table.tsx +7 -0
- package/src/components/field.tsx +137 -0
- package/src/components/floating-panel.tsx +366 -15
- package/src/components/list.tsx +28 -2
- package/src/components/menu-item.tsx +22 -1
- package/src/components/mini-calendar.tsx +245 -54
- package/src/components/nav-pills.tsx +145 -0
- package/src/components/progress-bar.tsx +129 -11
- package/src/components/series-chart.tsx +21 -0
- package/src/components/status-dot.tsx +10 -2
- package/src/components/table.tsx +85 -11
- package/src/components/text.tsx +9 -1
- package/src/components/toggle-group.tsx +48 -3
- package/src/components/tooltip.tsx +17 -3
- package/src/components/ui.tsx +149 -11
- package/src/components/user-avatar.tsx +57 -3
- package/src/i18n/defaults.ts +2 -0
- package/src/i18n/kit-labels.tsx +2 -0
- package/src/i18n/locales/de.ts +9 -0
- package/src/i18n/locales/es.ts +9 -0
- package/src/i18n/locales/fr.ts +9 -0
- package/src/i18n/locales/hu.ts +9 -0
- package/src/i18n/locales/it.ts +9 -0
- package/src/i18n/locales/zh.ts +9 -0
- package/src/index.ts +5 -0
- package/src/lib/clipping.ts +22 -4
- package/src/lib/dates.ts +37 -0
- package/src/rhf/use-rhf-wizard-step.ts +113 -0
- package/src/rhf.ts +2 -0
- package/src/shell/topbar-action-menu.tsx +239 -49
- package/src/wizard/wizard-context.tsx +9 -0
package/dist/lib/dates.js
CHANGED
|
@@ -70,6 +70,23 @@ function addDaysIso(iso, days) {
|
|
|
70
70
|
d.setDate(d.getDate() + days);
|
|
71
71
|
return toLocalIso(d);
|
|
72
72
|
}
|
|
73
|
+
function addMonthsClamped(d, months) {
|
|
74
|
+
const day = d.getDate();
|
|
75
|
+
const out = new Date(d.getFullYear(), d.getMonth() + months, 1);
|
|
76
|
+
const lastOfTarget = new Date(out.getFullYear(), out.getMonth() + 1, 0).getDate();
|
|
77
|
+
out.setDate(Math.min(day, lastOfTarget));
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
function localeWeekStart(locale) {
|
|
81
|
+
try {
|
|
82
|
+
const tag = locale ?? new Intl.DateTimeFormat().resolvedOptions().locale;
|
|
83
|
+
const loc = new Intl.Locale(tag);
|
|
84
|
+
const firstDay = (loc.getWeekInfo?.() ?? loc.weekInfo)?.firstDay;
|
|
85
|
+
if (typeof firstDay === "number") return firstDay % 7;
|
|
86
|
+
} catch {
|
|
87
|
+
}
|
|
88
|
+
return 1;
|
|
89
|
+
}
|
|
73
90
|
function sameYmd(a, b) {
|
|
74
91
|
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
75
92
|
}
|
|
@@ -107,6 +124,7 @@ function calendarMonthPresets(options = {}) {
|
|
|
107
124
|
}
|
|
108
125
|
export {
|
|
109
126
|
addDaysIso,
|
|
127
|
+
addMonthsClamped,
|
|
110
128
|
calendarMonthPresets,
|
|
111
129
|
dateRangePresets,
|
|
112
130
|
endOfMonthIso,
|
|
@@ -114,6 +132,7 @@ export {
|
|
|
114
132
|
formatIsoDate,
|
|
115
133
|
lastFullMonthsRange,
|
|
116
134
|
lastFullYearsRange,
|
|
135
|
+
localeWeekStart,
|
|
117
136
|
monthKey,
|
|
118
137
|
pad,
|
|
119
138
|
parseIsoDate,
|
package/dist/lib/dates.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/lib/dates.ts"],"sourcesContent":["/**\n * ISO date helpers, collected from copies that were scattered across the pages.\n *\n * **Every \"what day is it\" helper here reads the LOCAL calendar** — one family, not\n * two. It used to be two: these all built a local Date and then read it back through\n * `toISOString()`, which is UTC, so east of Greenwich they all returned YESTERDAY\n * between local midnight and the UTC rollover. The old module note called that\n * deliberate and warned against changing it. It was not defensible, and dev#471 is\n * what it cost:\n *\n * > *\"Just entered a tx now from scheduled. But it still shows as upcoming. Is there\n * > a problem with timezones? It is currently 1 o clock middle european summer time.\"*\n *\n * There was: at 01:00 CEST `todayIso()` answered with the 14th, the row the scheduler\n * had just booked was dated the 15th, and `isUpcoming` compares the two strings — so a\n * transaction entered a second ago read as one still to come. The same slip moved\n * `startOfMonthIso()` onto the last day of the PREVIOUS month for an hour every first\n * of the month, which is a report range nobody would have questioned.\n *\n * A calendar day only means anything to the human reading it, and every caller here —\n * the register's today, the date-range presets, the budget cursor, a new\n * transaction's default date — is asking that question. There is no caller for whom\n * \"the UTC day\" is the right answer; the server compares plain dates with no zone at\n * all, so sending it the user's local day is also what it wants.\n */\n\n/** Zero-pad a number to two digits (\"3\" -> \"03\"). */\nexport function pad(n: number): string {\n return String(n).padStart(2, \"0\");\n}\n\n/** Today as \"YYYY-MM-DD\" on the LOCAL calendar (see the module note). */\nexport function todayIso(): string {\n return toLocalIso(new Date());\n}\n\n/** A Date as a local-time \"YYYY-MM-DD\" string. */\nexport function toLocalIso(d: Date): string {\n return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;\n}\n\n/** A Date as a local-time \"YYYY-MM\" month key. */\nexport function monthKey(d: Date): string {\n return `${d.getFullYear()}-${pad(d.getMonth() + 1)}`;\n}\n\n// ---------- Date-range presets + calendar helpers ----------\n// Local-calendar like todayIso (see the module note); used by the data-table date\n// filter and its MiniCalendar. Each one mutates a local Date and must therefore READ\n// it back locally — going out through toISOString() is what shifted every one of\n// these by a day near midnight (dev#471).\n\n/** Today shifted by `days`, as \"YYYY-MM-DD\". */\nexport function shiftIso(days: number): string {\n const d = new Date();\n d.setDate(d.getDate() + days);\n return toLocalIso(d);\n}\n\n/** First day (Monday) of the current week, as \"YYYY-MM-DD\". */\nexport function startOfWeekIso(): string {\n const d = new Date();\n const day = d.getDay();\n const diff = (day + 6) % 7; // monday = 0\n d.setDate(d.getDate() - diff);\n return toLocalIso(d);\n}\n\n/** First day of the month `offsetMonths` from now, as \"YYYY-MM-DD\". */\nexport function startOfMonthIso(offsetMonths = 0): string {\n const d = new Date();\n d.setDate(1);\n d.setMonth(d.getMonth() + offsetMonths);\n return toLocalIso(d);\n}\n\n/** Last day of the month `offsetMonths` from now, as \"YYYY-MM-DD\". */\nexport function endOfMonthIso(offsetMonths = 0): string {\n const d = new Date();\n d.setDate(1);\n d.setMonth(d.getMonth() + offsetMonths + 1);\n d.setDate(0);\n return toLocalIso(d);\n}\n\n/** First day of the year `offsetYears` from now, as \"YYYY-MM-DD\". */\nexport function startOfYearIso(offsetYears = 0): string {\n const d = new Date();\n d.setMonth(0);\n d.setDate(1);\n d.setFullYear(d.getFullYear() + offsetYears);\n return toLocalIso(d);\n}\n\n/** Last day of the year `offsetYears` from now, as \"YYYY-MM-DD\". */\nexport function endOfYearIso(offsetYears = 0): string {\n const d = new Date();\n d.setMonth(11);\n d.setDate(31);\n d.setFullYear(d.getFullYear() + offsetYears);\n return toLocalIso(d);\n}\n\n/** Parse a \"YYYY-MM-DD\" string to a local-midnight Date, or null if malformed —\n * where \"malformed\" includes a date that does not exist.\n *\n * The falsy check alone was not enough. `new Date(y, m - 1, d)` ROLLS OVER out-of-range\n * components rather than refusing them, so \"2026-13-45\" came back as 2027-02-14 and\n * \"2026-02-30\" as 2026-03-02: a valid-looking Date for a day that never happened. No\n * caller could tell — {@link formatIsoDate} blanks only on `null`, so the invented\n * date formatted and displayed exactly like a real one.\n *\n * The round-trip is the check: build the Date, then read the components back off it.\n * A value that rolled over cannot answer with the numbers it was given. That is the\n * same shape {@link sameYmd} already expresses, applied to a Date and its source. */\nexport function parseIsoDate(s: string): Date | null {\n if (!s) return null;\n const [y, m, d] = s.split(\"-\").map(Number);\n if (!y || !m || !d) return null;\n if (!Number.isInteger(y) || !Number.isInteger(m) || !Number.isInteger(d)) return null;\n const dt = new Date(y, m - 1, d);\n if (Number.isNaN(dt.getTime())) return null;\n if (dt.getFullYear() !== y || dt.getMonth() !== m - 1 || dt.getDate() !== d) return null;\n return dt;\n}\n\n/** Format a \"YYYY-MM-DD\" string for display in `locale` (empty string when\n * unparseable). Defaults to the locale's short numeric date; pass `options` to\n * override (e.g. `{ dateStyle: \"medium\" }`). */\nexport function formatIsoDate(\n iso: string,\n locale: string,\n options?: Intl.DateTimeFormatOptions,\n): string {\n const d = parseIsoDate(iso);\n return d ? d.toLocaleDateString(locale, options) : \"\";\n}\n\n/** A \"YYYY-MM-DD\" string shifted by `days` (negative shifts back), or the input\n * unchanged when it isn't a parseable date.\n *\n * Local-time throughout — {@link parseIsoDate} builds a local-midnight Date and\n * {@link toLocalIso} reads local components back, so this never slides a day the way\n * a `toISOString()` round-trip does west of UTC. */\nexport function addDaysIso(iso: string, days: number): string {\n const d = parseIsoDate(iso);\n if (!d) return iso;\n d.setDate(d.getDate() + days);\n return toLocalIso(d);\n}\n\n/** Whether two Dates fall on the same local calendar day. */\nexport function sameYmd(a: Date, b: Date): boolean {\n return (\n a.getFullYear() === b.getFullYear() &&\n a.getMonth() === b.getMonth() &&\n a.getDate() === b.getDate()\n );\n}\n\nexport interface DateRangePreset {\n key: string;\n from: string;\n to: string;\n}\n\n/**\n * The named ranges offered by the data-table date filter (local-calendar, like\n * {@link todayIso} — see the module note). `key` maps to the consumer's\n * `table.preset_*` labels.\n */\nexport function dateRangePresets(): DateRangePreset[] {\n const mondayOffset = (new Date().getDay() + 6) % 7; // monday = 0\n return [\n { key: \"today\", from: todayIso(), to: todayIso() },\n { key: \"yesterday\", from: shiftIso(-1), to: shiftIso(-1) },\n { key: \"this_week\", from: startOfWeekIso(), to: todayIso() },\n { key: \"last_week\", from: shiftIso(-7 - mondayOffset), to: shiftIso(-1 - mondayOffset) },\n { key: \"last_7_days\", from: shiftIso(-6), to: todayIso() },\n { key: \"last_30_days\", from: shiftIso(-29), to: todayIso() },\n { key: \"this_month\", from: startOfMonthIso(0), to: todayIso() },\n { key: \"last_month\", from: startOfMonthIso(-1), to: endOfMonthIso(-1) },\n { key: \"last_3_months\", from: shiftIso(-90), to: todayIso() },\n { key: \"ytd\", from: startOfYearIso(0), to: todayIso() },\n { key: \"last_year\", from: startOfYearIso(-1), to: endOfYearIso(-1) },\n ];\n}\n\n/** The last `n` WHOLE calendar months before the current one, as an inclusive\n * `{ from, to }` — on 15 May, `n = 3` is 1 February … 30 April. Local-calendar, like\n * everything here; `now` is for tests and for a host with its own clock. */\nexport function lastFullMonthsRange(n: number, now: Date = new Date()): { from: string; to: string } {\n const from = new Date(now.getFullYear(), now.getMonth() - n, 1);\n // Day 0 of the current month is the last day of the previous one.\n const to = new Date(now.getFullYear(), now.getMonth(), 0);\n return { from: toLocalIso(from), to: toLocalIso(to) };\n}\n\n/** The last `n` WHOLE calendar years before the current one — in 2026, `n = 2` is\n * 1 January 2024 … 31 December 2025. */\nexport function lastFullYearsRange(n: number, now: Date = new Date()): { from: string; to: string } {\n const year = now.getFullYear();\n return { from: `${year - n}-01-01`, to: `${year - 1}-12-31` };\n}\n\nexport interface CalendarMonthPresetOptions {\n /** How many whole months each month preset spans. Default `[3, 6]`. */\n months?: readonly number[];\n /** How many whole years each year preset spans. Default `[2]`. */\n years?: readonly number[];\n /** The day to count back from. Default: now, on the local calendar. */\n now?: Date;\n}\n\n/**\n * Month-ALIGNED presets, beside the rolling-day ones of {@link dateRangePresets}.\n *\n * kastlan's accounting reports are closed by the month: \"the last three months\" there\n * means February, March and April, never \"the 90 days up to this morning\", which\n * starts mid-month and ends with a day that is still being booked. `last_3_months`\n * above is the rolling kind and stays that way for the data table's filter, so these\n * are separate keys rather than a changed meaning under an old one.\n *\n * Keys are `last_<n>_full_months` and `last_<n>_full_years`; the host labels them (the\n * same arrangement as `dateRangePresets`' keys and its `table.preset_*` strings). With\n * `DateRangePicker`, pass the key as the preset's `id` so the choice keeps its\n * identity while the dates are recomputed on every render.\n */\nexport function calendarMonthPresets(options: CalendarMonthPresetOptions = {}): DateRangePreset[] {\n const { months = [3, 6], years = [2], now = new Date() } = options;\n return [\n ...months.map((n) => ({ key: `last_${n}_full_months`, ...lastFullMonthsRange(n, now) })),\n ...years.map((n) => ({ key: `last_${n}_full_years`, ...lastFullYearsRange(n, now) })),\n ];\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/lib/dates.ts"],"sourcesContent":["/**\n * ISO date helpers, collected from copies that were scattered across the pages.\n *\n * **Every \"what day is it\" helper here reads the LOCAL calendar** — one family, not\n * two. It used to be two: these all built a local Date and then read it back through\n * `toISOString()`, which is UTC, so east of Greenwich they all returned YESTERDAY\n * between local midnight and the UTC rollover. The old module note called that\n * deliberate and warned against changing it. It was not defensible, and dev#471 is\n * what it cost:\n *\n * > *\"Just entered a tx now from scheduled. But it still shows as upcoming. Is there\n * > a problem with timezones? It is currently 1 o clock middle european summer time.\"*\n *\n * There was: at 01:00 CEST `todayIso()` answered with the 14th, the row the scheduler\n * had just booked was dated the 15th, and `isUpcoming` compares the two strings — so a\n * transaction entered a second ago read as one still to come. The same slip moved\n * `startOfMonthIso()` onto the last day of the PREVIOUS month for an hour every first\n * of the month, which is a report range nobody would have questioned.\n *\n * A calendar day only means anything to the human reading it, and every caller here —\n * the register's today, the date-range presets, the budget cursor, a new\n * transaction's default date — is asking that question. There is no caller for whom\n * \"the UTC day\" is the right answer; the server compares plain dates with no zone at\n * all, so sending it the user's local day is also what it wants.\n */\n\n/** Zero-pad a number to two digits (\"3\" -> \"03\"). */\nexport function pad(n: number): string {\n return String(n).padStart(2, \"0\");\n}\n\n/** Today as \"YYYY-MM-DD\" on the LOCAL calendar (see the module note). */\nexport function todayIso(): string {\n return toLocalIso(new Date());\n}\n\n/** A Date as a local-time \"YYYY-MM-DD\" string. */\nexport function toLocalIso(d: Date): string {\n return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;\n}\n\n/** A Date as a local-time \"YYYY-MM\" month key. */\nexport function monthKey(d: Date): string {\n return `${d.getFullYear()}-${pad(d.getMonth() + 1)}`;\n}\n\n// ---------- Date-range presets + calendar helpers ----------\n// Local-calendar like todayIso (see the module note); used by the data-table date\n// filter and its MiniCalendar. Each one mutates a local Date and must therefore READ\n// it back locally — going out through toISOString() is what shifted every one of\n// these by a day near midnight (dev#471).\n\n/** Today shifted by `days`, as \"YYYY-MM-DD\". */\nexport function shiftIso(days: number): string {\n const d = new Date();\n d.setDate(d.getDate() + days);\n return toLocalIso(d);\n}\n\n/** First day (Monday) of the current week, as \"YYYY-MM-DD\". */\nexport function startOfWeekIso(): string {\n const d = new Date();\n const day = d.getDay();\n const diff = (day + 6) % 7; // monday = 0\n d.setDate(d.getDate() - diff);\n return toLocalIso(d);\n}\n\n/** First day of the month `offsetMonths` from now, as \"YYYY-MM-DD\". */\nexport function startOfMonthIso(offsetMonths = 0): string {\n const d = new Date();\n d.setDate(1);\n d.setMonth(d.getMonth() + offsetMonths);\n return toLocalIso(d);\n}\n\n/** Last day of the month `offsetMonths` from now, as \"YYYY-MM-DD\". */\nexport function endOfMonthIso(offsetMonths = 0): string {\n const d = new Date();\n d.setDate(1);\n d.setMonth(d.getMonth() + offsetMonths + 1);\n d.setDate(0);\n return toLocalIso(d);\n}\n\n/** First day of the year `offsetYears` from now, as \"YYYY-MM-DD\". */\nexport function startOfYearIso(offsetYears = 0): string {\n const d = new Date();\n d.setMonth(0);\n d.setDate(1);\n d.setFullYear(d.getFullYear() + offsetYears);\n return toLocalIso(d);\n}\n\n/** Last day of the year `offsetYears` from now, as \"YYYY-MM-DD\". */\nexport function endOfYearIso(offsetYears = 0): string {\n const d = new Date();\n d.setMonth(11);\n d.setDate(31);\n d.setFullYear(d.getFullYear() + offsetYears);\n return toLocalIso(d);\n}\n\n/** Parse a \"YYYY-MM-DD\" string to a local-midnight Date, or null if malformed —\n * where \"malformed\" includes a date that does not exist.\n *\n * The falsy check alone was not enough. `new Date(y, m - 1, d)` ROLLS OVER out-of-range\n * components rather than refusing them, so \"2026-13-45\" came back as 2027-02-14 and\n * \"2026-02-30\" as 2026-03-02: a valid-looking Date for a day that never happened. No\n * caller could tell — {@link formatIsoDate} blanks only on `null`, so the invented\n * date formatted and displayed exactly like a real one.\n *\n * The round-trip is the check: build the Date, then read the components back off it.\n * A value that rolled over cannot answer with the numbers it was given. That is the\n * same shape {@link sameYmd} already expresses, applied to a Date and its source. */\nexport function parseIsoDate(s: string): Date | null {\n if (!s) return null;\n const [y, m, d] = s.split(\"-\").map(Number);\n if (!y || !m || !d) return null;\n if (!Number.isInteger(y) || !Number.isInteger(m) || !Number.isInteger(d)) return null;\n const dt = new Date(y, m - 1, d);\n if (Number.isNaN(dt.getTime())) return null;\n if (dt.getFullYear() !== y || dt.getMonth() !== m - 1 || dt.getDate() !== d) return null;\n return dt;\n}\n\n/** Format a \"YYYY-MM-DD\" string for display in `locale` (empty string when\n * unparseable). Defaults to the locale's short numeric date; pass `options` to\n * override (e.g. `{ dateStyle: \"medium\" }`). */\nexport function formatIsoDate(\n iso: string,\n locale: string,\n options?: Intl.DateTimeFormatOptions,\n): string {\n const d = parseIsoDate(iso);\n return d ? d.toLocaleDateString(locale, options) : \"\";\n}\n\n/** A \"YYYY-MM-DD\" string shifted by `days` (negative shifts back), or the input\n * unchanged when it isn't a parseable date.\n *\n * Local-time throughout — {@link parseIsoDate} builds a local-midnight Date and\n * {@link toLocalIso} reads local components back, so this never slides a day the way\n * a `toISOString()` round-trip does west of UTC. */\nexport function addDaysIso(iso: string, days: number): string {\n const d = parseIsoDate(iso);\n if (!d) return iso;\n d.setDate(d.getDate() + days);\n return toLocalIso(d);\n}\n\n/**\n * `d` moved by whole months, keeping the day of the month where the target has one.\n * `setMonth` alone ROLLS OVER — 31 January + 1 month is 3 March — which would skip\n * February entirely for anyone paging through the year with PageDown. Returns a new\n * Date; `d` is not touched.\n */\nexport function addMonthsClamped(d: Date, months: number): Date {\n const day = d.getDate();\n const out = new Date(d.getFullYear(), d.getMonth() + months, 1);\n const lastOfTarget = new Date(out.getFullYear(), out.getMonth() + 1, 0).getDate();\n out.setDate(Math.min(day, lastOfTarget));\n return out;\n}\n\n/**\n * The first day of the week in `locale`, as a `Date#getDay` index (0 = Sunday).\n *\n * `Intl.Locale#getWeekInfo()` is the standard spelling; V8 shipped it first as the\n * `weekInfo` accessor, and Firefox has neither yet — hence both reads and a Monday\n * fallback, which is ISO 8601. Its `firstDay` counts 1 = Monday … 7 = Sunday, so `% 7`\n * maps it onto `getDay`. `undefined` asks for the runtime's default locale.\n */\nexport function localeWeekStart(locale: string | undefined): 0 | 1 | 2 | 3 | 4 | 5 | 6 {\n try {\n const tag = locale ?? new Intl.DateTimeFormat().resolvedOptions().locale;\n const loc = new Intl.Locale(tag) as Intl.Locale & {\n getWeekInfo?: () => { firstDay: number };\n weekInfo?: { firstDay: number };\n };\n const firstDay = (loc.getWeekInfo?.() ?? loc.weekInfo)?.firstDay;\n if (typeof firstDay === \"number\") return (firstDay % 7) as 0 | 1 | 2 | 3 | 4 | 5 | 6;\n } catch {\n // A malformed tag throws from `Intl.Locale`; a calendar still has to render.\n }\n return 1;\n}\n\n/** Whether two Dates fall on the same local calendar day. */\nexport function sameYmd(a: Date, b: Date): boolean {\n return (\n a.getFullYear() === b.getFullYear() &&\n a.getMonth() === b.getMonth() &&\n a.getDate() === b.getDate()\n );\n}\n\nexport interface DateRangePreset {\n key: string;\n from: string;\n to: string;\n}\n\n/**\n * The named ranges offered by the data-table date filter (local-calendar, like\n * {@link todayIso} — see the module note). `key` maps to the consumer's\n * `table.preset_*` labels.\n */\nexport function dateRangePresets(): DateRangePreset[] {\n const mondayOffset = (new Date().getDay() + 6) % 7; // monday = 0\n return [\n { key: \"today\", from: todayIso(), to: todayIso() },\n { key: \"yesterday\", from: shiftIso(-1), to: shiftIso(-1) },\n { key: \"this_week\", from: startOfWeekIso(), to: todayIso() },\n { key: \"last_week\", from: shiftIso(-7 - mondayOffset), to: shiftIso(-1 - mondayOffset) },\n { key: \"last_7_days\", from: shiftIso(-6), to: todayIso() },\n { key: \"last_30_days\", from: shiftIso(-29), to: todayIso() },\n { key: \"this_month\", from: startOfMonthIso(0), to: todayIso() },\n { key: \"last_month\", from: startOfMonthIso(-1), to: endOfMonthIso(-1) },\n { key: \"last_3_months\", from: shiftIso(-90), to: todayIso() },\n { key: \"ytd\", from: startOfYearIso(0), to: todayIso() },\n { key: \"last_year\", from: startOfYearIso(-1), to: endOfYearIso(-1) },\n ];\n}\n\n/** The last `n` WHOLE calendar months before the current one, as an inclusive\n * `{ from, to }` — on 15 May, `n = 3` is 1 February … 30 April. Local-calendar, like\n * everything here; `now` is for tests and for a host with its own clock. */\nexport function lastFullMonthsRange(n: number, now: Date = new Date()): { from: string; to: string } {\n const from = new Date(now.getFullYear(), now.getMonth() - n, 1);\n // Day 0 of the current month is the last day of the previous one.\n const to = new Date(now.getFullYear(), now.getMonth(), 0);\n return { from: toLocalIso(from), to: toLocalIso(to) };\n}\n\n/** The last `n` WHOLE calendar years before the current one — in 2026, `n = 2` is\n * 1 January 2024 … 31 December 2025. */\nexport function lastFullYearsRange(n: number, now: Date = new Date()): { from: string; to: string } {\n const year = now.getFullYear();\n return { from: `${year - n}-01-01`, to: `${year - 1}-12-31` };\n}\n\nexport interface CalendarMonthPresetOptions {\n /** How many whole months each month preset spans. Default `[3, 6]`. */\n months?: readonly number[];\n /** How many whole years each year preset spans. Default `[2]`. */\n years?: readonly number[];\n /** The day to count back from. Default: now, on the local calendar. */\n now?: Date;\n}\n\n/**\n * Month-ALIGNED presets, beside the rolling-day ones of {@link dateRangePresets}.\n *\n * kastlan's accounting reports are closed by the month: \"the last three months\" there\n * means February, March and April, never \"the 90 days up to this morning\", which\n * starts mid-month and ends with a day that is still being booked. `last_3_months`\n * above is the rolling kind and stays that way for the data table's filter, so these\n * are separate keys rather than a changed meaning under an old one.\n *\n * Keys are `last_<n>_full_months` and `last_<n>_full_years`; the host labels them (the\n * same arrangement as `dateRangePresets`' keys and its `table.preset_*` strings). With\n * `DateRangePicker`, pass the key as the preset's `id` so the choice keeps its\n * identity while the dates are recomputed on every render.\n */\nexport function calendarMonthPresets(options: CalendarMonthPresetOptions = {}): DateRangePreset[] {\n const { months = [3, 6], years = [2], now = new Date() } = options;\n return [\n ...months.map((n) => ({ key: `last_${n}_full_months`, ...lastFullMonthsRange(n, now) })),\n ...years.map((n) => ({ key: `last_${n}_full_years`, ...lastFullYearsRange(n, now) })),\n ];\n}\n"],"mappings":"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","names":[]}
|
package/dist/rhf/form.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { FieldValues, FieldPath, ControllerProps } from 'react-hook-form';
|
|
|
5
5
|
import { LabelProps } from '../components/ui.js';
|
|
6
6
|
import '../components/tooltip.js';
|
|
7
7
|
import '../hooks/use-anchored-rect.js';
|
|
8
|
+
import '../lib/clipping.js';
|
|
8
9
|
|
|
9
10
|
/** The form's `useForm()` return, spread in: `<Form {...form}>`. react-hook-form's own
|
|
10
11
|
* `FormProvider`, under the name the shadcn file gave it. */
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { FieldValues, FieldPath, UseFormReturn } from 'react-hook-form';
|
|
2
|
+
|
|
3
|
+
type RhfWizardStepOptions<TFieldValues extends FieldValues, TTransformed = TFieldValues> = {
|
|
4
|
+
/** Validate only these fields — the step's share of a form that spans steps. */
|
|
5
|
+
fields: readonly FieldPath<TFieldValues>[];
|
|
6
|
+
/** Called with `form.getValues()` when the fields pass, before the wizard
|
|
7
|
+
* advances. */
|
|
8
|
+
onValid?: (values: TFieldValues) => void;
|
|
9
|
+
} | {
|
|
10
|
+
/** Omitted: validate the whole form through `handleSubmit`. */
|
|
11
|
+
fields?: undefined;
|
|
12
|
+
/** Called with the form's (transformed) values when it passes, before the
|
|
13
|
+
* wizard advances. */
|
|
14
|
+
onValid?: (values: TTransformed) => void;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Connects `form` to the wizard's step validation and returns the step's validator.
|
|
18
|
+
* See the module note for the two modes.
|
|
19
|
+
*/
|
|
20
|
+
declare function useRhfWizardStep<TFieldValues extends FieldValues, TContext = unknown, TTransformed = TFieldValues>(form: UseFormReturn<TFieldValues, TContext, TTransformed>, options?: RhfWizardStepOptions<TFieldValues, TTransformed> | ((values: TTransformed) => void)): () => Promise<boolean>;
|
|
21
|
+
|
|
22
|
+
export { type RhfWizardStepOptions, useRhfWizardStep };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
3
|
+
import { useOptionalWizardContext } from "../wizard/wizard-context.js";
|
|
4
|
+
function useRhfWizardStep(form, options) {
|
|
5
|
+
const opts = typeof options === "function" ? { onValid: options } : options ?? {};
|
|
6
|
+
const optsRef = useRef(opts);
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
optsRef.current = opts;
|
|
9
|
+
});
|
|
10
|
+
const validate = useCallback(async () => {
|
|
11
|
+
const current = optsRef.current;
|
|
12
|
+
if (current.fields) {
|
|
13
|
+
const ok2 = await form.trigger([...current.fields], { shouldFocus: true });
|
|
14
|
+
if (ok2) current.onValid?.(form.getValues());
|
|
15
|
+
return ok2;
|
|
16
|
+
}
|
|
17
|
+
const onValid = current.onValid;
|
|
18
|
+
let ok = false;
|
|
19
|
+
await form.handleSubmit(
|
|
20
|
+
(values) => {
|
|
21
|
+
ok = true;
|
|
22
|
+
onValid?.(values);
|
|
23
|
+
},
|
|
24
|
+
() => {
|
|
25
|
+
ok = false;
|
|
26
|
+
}
|
|
27
|
+
)();
|
|
28
|
+
return ok;
|
|
29
|
+
}, [form]);
|
|
30
|
+
const wizard = useOptionalWizardContext();
|
|
31
|
+
const register = wizard?.registerStepValidate;
|
|
32
|
+
useEffect(() => register?.(validate), [register, validate]);
|
|
33
|
+
return validate;
|
|
34
|
+
}
|
|
35
|
+
export {
|
|
36
|
+
useRhfWizardStep
|
|
37
|
+
};
|
|
38
|
+
//# sourceMappingURL=use-rhf-wizard-step.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/rhf/use-rhf-wizard-step.ts"],"sourcesContent":["/**\n * The bridge between a react-hook-form step and the wizard's Next gate.\n * `@eifi1/ui-kit/rhf`.\n *\n * ```tsx\n * function AddressStep({ onUpdate }: { onUpdate: (v: Address) => void }) {\n * const form = useForm<Address>({ defaultValues });\n * useRhfWizardStep(form, { onValid: onUpdate });\n * return <Form {...form}>…</Form>;\n * }\n * ```\n *\n * Inside a `<StepperNav>` the hook registers the step's validator with the wizard, so\n * Next (and Finish on a form-bearing last step) runs it: on success the values go to\n * `onValid` and the wizard advances; on failure react-hook-form shows its own inline\n * `<FormMessage>`s, focuses the first field in error, and the wizard stays put — and,\n * since the result carries no per-field errors, says its \"fill in the required\n * fields\" line so the button did not seem to do nothing. It also RETURNS the\n * validator, for a nav of your own outside `<StepperNav>` (where there is no wizard\n * context to register with, so nothing is registered).\n *\n * Two modes:\n *\n * - **The whole form** (no `fields`): `form.handleSubmit`, exactly as kastlan's\n * `use-rhf-wizard-step.ts` — every rule and the resolver run, `onValid` receives\n * the TRANSFORMED values (a zod `.transform()` included), and focus follows the\n * form's own `shouldFocusError` (on by default).\n * - **Some fields** (`fields: [\"iban\", \"bic\"]`): `form.trigger(fields)`, for a wizard\n * that keeps one form across several steps, each of which validates only what it\n * shows. The first of those fields in error is focused (`shouldFocus`; the control\n * has to take `field.ref`), errors on fields of later steps are not raised, and\n * `onValid` receives `form.getValues()` — the untransformed values, because a\n * resolver's output is only produced for the whole form.\n *\n * Both callbacks are read through refs, so the validator is registered once per form\n * however the caller writes them (an inline `(v) => onUpdate(v)` is the usual case).\n *\n * The positional `useRhfWizardStep(form, onValid)` of the hook kastlan kept is the\n * whole-form mode, and still accepted.\n *\n * Why it lives here and not in `@eifi1/ui-kit/wizard`: it imports react-hook-form,\n * which only this entry may (see packaging-contract).\n */\nimport { useCallback, useEffect, useRef } from \"react\";\nimport type { FieldPath, FieldValues, UseFormReturn } from \"react-hook-form\";\nimport { useOptionalWizardContext } from \"../wizard/wizard-context\";\n\nexport type RhfWizardStepOptions<\n TFieldValues extends FieldValues,\n TTransformed = TFieldValues,\n> =\n | {\n /** Validate only these fields — the step's share of a form that spans steps. */\n fields: readonly FieldPath<TFieldValues>[];\n /** Called with `form.getValues()` when the fields pass, before the wizard\n * advances. */\n onValid?: (values: TFieldValues) => void;\n }\n | {\n /** Omitted: validate the whole form through `handleSubmit`. */\n fields?: undefined;\n /** Called with the form's (transformed) values when it passes, before the\n * wizard advances. */\n onValid?: (values: TTransformed) => void;\n };\n\n/**\n * Connects `form` to the wizard's step validation and returns the step's validator.\n * See the module note for the two modes.\n */\nexport function useRhfWizardStep<\n TFieldValues extends FieldValues,\n TContext = unknown,\n TTransformed = TFieldValues,\n>(\n form: UseFormReturn<TFieldValues, TContext, TTransformed>,\n options?:\n | RhfWizardStepOptions<TFieldValues, TTransformed>\n | ((values: TTransformed) => void),\n): () => Promise<boolean> {\n const opts = typeof options === \"function\" ? { onValid: options } : (options ?? {});\n const optsRef = useRef(opts);\n useEffect(() => {\n optsRef.current = opts;\n });\n\n const validate = useCallback(async (): Promise<boolean> => {\n const current = optsRef.current;\n if (current.fields) {\n const ok = await form.trigger([...current.fields], { shouldFocus: true });\n if (ok) current.onValid?.(form.getValues());\n return ok;\n }\n const onValid = current.onValid as ((values: TTransformed) => void) | undefined;\n let ok = false;\n await form.handleSubmit(\n (values) => {\n ok = true;\n onValid?.(values);\n },\n () => {\n ok = false;\n },\n )();\n return ok;\n }, [form]);\n\n const wizard = useOptionalWizardContext();\n const register = wizard?.registerStepValidate;\n useEffect(() => register?.(validate), [register, validate]);\n\n return validate;\n}\n"],"mappings":";AA2CA,SAAS,aAAa,WAAW,cAAc;AAE/C,SAAS,gCAAgC;AAyBlC,SAAS,iBAKd,MACA,SAGwB;AACxB,QAAM,OAAO,OAAO,YAAY,aAAa,EAAE,SAAS,QAAQ,IAAK,WAAW,CAAC;AACjF,QAAM,UAAU,OAAO,IAAI;AAC3B,YAAU,MAAM;AACd,YAAQ,UAAU;AAAA,EACpB,CAAC;AAED,QAAM,WAAW,YAAY,YAA8B;AACzD,UAAM,UAAU,QAAQ;AACxB,QAAI,QAAQ,QAAQ;AAClB,YAAMA,MAAK,MAAM,KAAK,QAAQ,CAAC,GAAG,QAAQ,MAAM,GAAG,EAAE,aAAa,KAAK,CAAC;AACxE,UAAIA,IAAI,SAAQ,UAAU,KAAK,UAAU,CAAC;AAC1C,aAAOA;AAAA,IACT;AACA,UAAM,UAAU,QAAQ;AACxB,QAAI,KAAK;AACT,UAAM,KAAK;AAAA,MACT,CAAC,WAAW;AACV,aAAK;AACL,kBAAU,MAAM;AAAA,MAClB;AAAA,MACA,MAAM;AACJ,aAAK;AAAA,MACP;AAAA,IACF,EAAE;AACF,WAAO;AAAA,EACT,GAAG,CAAC,IAAI,CAAC;AAET,QAAM,SAAS,yBAAyB;AACxC,QAAM,WAAW,QAAQ;AACzB,YAAU,MAAM,WAAW,QAAQ,GAAG,CAAC,UAAU,QAAQ,CAAC;AAE1D,SAAO;AACT;","names":["ok"]}
|
package/dist/rhf.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { Form, FormControl, FormControlProps, FormDescription, FormDescriptionProps, FormField, FormItem, FormItemProps, FormLabel, FormLabelProps, FormMessage, FormMessageProps, useFormField } from './rhf/form.js';
|
|
2
|
+
export { RhfWizardStepOptions, useRhfWizardStep } from './rhf/use-rhf-wizard-step.js';
|
|
2
3
|
import 'react';
|
|
3
4
|
import 'react-hook-form';
|
|
4
5
|
import './components/ui.js';
|
|
5
6
|
import './components/tooltip.js';
|
|
6
7
|
import './hooks/use-anchored-rect.js';
|
|
8
|
+
import './lib/clipping.js';
|
package/dist/rhf.js
CHANGED
package/dist/rhf.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/rhf.ts"],"sourcesContent":["// `@eifi1/ui-kit/rhf` — the react-hook-form adapter: Form, FormField, FormItem,\n// FormLabel, FormControl, FormDescription, FormMessage and useFormField.\n//\n// The only entry point in the package that requires `react-hook-form`, and an optional\n// peer for that reason: an app without a form library installs nothing for it. NOT\n// re-exported from the main barrel, which would make the peer everyone's — the reason\n// the barrel dropped its last react-hook-form import (see the wizard section of\n// src/index.ts). The packaging contract test holds that line.\nexport * from \"./rhf/form\";\n"],"mappings":";AAQA,cAAc;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/rhf.ts"],"sourcesContent":["// `@eifi1/ui-kit/rhf` — the react-hook-form adapter: Form, FormField, FormItem,\n// FormLabel, FormControl, FormDescription, FormMessage and useFormField.\n//\n// The only entry point in the package that requires `react-hook-form`, and an optional\n// peer for that reason: an app without a form library installs nothing for it. NOT\n// re-exported from the main barrel, which would make the peer everyone's — the reason\n// the barrel dropped its last react-hook-form import (see the wizard section of\n// src/index.ts). The packaging contract test holds that line.\nexport * from \"./rhf/form\";\n// The react-hook-form bridge to the wizard's Next gate (useWizard lives in /wizard).\nexport * from \"./rhf/use-rhf-wizard-step\";\n"],"mappings":";AAQA,cAAc;AAEd,cAAc;","names":[]}
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
|
+
import { MenuItemTone } from '../components/menu-item.js';
|
|
4
|
+
import 'lucide-react';
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* One row of a {@link TopBarActionMenu}. An `action` (default) runs `onSelect`, a
|
|
6
8
|
* `link` navigates via react-router, and a `divider` draws a rule. Non-divider
|
|
7
9
|
* rows close the menu automatically when chosen. `kind` may be omitted for
|
|
8
10
|
* actions, keeping the common case terse.
|
|
11
|
+
*
|
|
12
|
+
* The rows are {@link MenuItem}s, so they take its options: `tone: "danger"` for the
|
|
13
|
+
* log-out row, `checked` (+ `checkable`) for a choice, `current` for the link to the
|
|
14
|
+
* page already open, `disabled`.
|
|
9
15
|
*/
|
|
10
16
|
type TopBarMenuEntry = {
|
|
11
17
|
kind?: "action";
|
|
@@ -14,6 +20,12 @@ type TopBarMenuEntry = {
|
|
|
14
20
|
label: ReactNode;
|
|
15
21
|
trailing?: ReactNode;
|
|
16
22
|
onSelect: () => void;
|
|
23
|
+
tone?: MenuItemTone;
|
|
24
|
+
/** Makes the row a choice (`menuitemradio`, or `menuitemcheckbox` with
|
|
25
|
+
* `checkable: "checkbox"`) and says whether it is chosen. */
|
|
26
|
+
checked?: boolean;
|
|
27
|
+
checkable?: "radio" | "checkbox";
|
|
28
|
+
disabled?: boolean;
|
|
17
29
|
} | {
|
|
18
30
|
kind: "link";
|
|
19
31
|
key: string;
|
|
@@ -21,10 +33,28 @@ type TopBarMenuEntry = {
|
|
|
21
33
|
label: ReactNode;
|
|
22
34
|
trailing?: ReactNode;
|
|
23
35
|
to: string;
|
|
36
|
+
tone?: MenuItemTone;
|
|
37
|
+
/** The link to the page already open: `aria-current="page"` and a check mark. */
|
|
38
|
+
current?: boolean;
|
|
39
|
+
disabled?: boolean;
|
|
24
40
|
} | {
|
|
25
41
|
kind: "divider";
|
|
26
42
|
key: string;
|
|
27
43
|
};
|
|
44
|
+
/**
|
|
45
|
+
* The non-interactive identity block at the top of an account menu — kastlan's name +
|
|
46
|
+
* role, keksdose's display name + email + role chip. It is text, not a `menuitem`: the
|
|
47
|
+
* arrow keys pass over it.
|
|
48
|
+
*/
|
|
49
|
+
interface TopBarMenuHeader {
|
|
50
|
+
/** The first line — the user's name. */
|
|
51
|
+
title: ReactNode;
|
|
52
|
+
/** The muted second line — an email, a role. */
|
|
53
|
+
subtitle?: ReactNode;
|
|
54
|
+
/** Anything below — a role chip. A link or button here stays out of the arrow keys
|
|
55
|
+
* and in the Tab order, like the footer's. */
|
|
56
|
+
extra?: ReactNode;
|
|
57
|
+
}
|
|
28
58
|
/**
|
|
29
59
|
* A top-bar icon button that opens a {@link HoverMenu} of actions and/or links,
|
|
30
60
|
* with an optional uppercase heading and divider rules. Generic glue over the
|
|
@@ -32,13 +62,53 @@ type TopBarMenuEntry = {
|
|
|
32
62
|
* so those call sites describe their rows as data instead of rebuilding the
|
|
33
63
|
* trigger + list markup. For a checkmarked value-switcher use
|
|
34
64
|
* {@link OptionSwitcherMenu} instead.
|
|
65
|
+
*
|
|
66
|
+
* It is also the ACCOUNT menu kastlan and keksdose each hand-build on `HoverMenu`: a
|
|
67
|
+
* custom `trigger` (the avatar), a `header` (name + role / email), the rows, and a
|
|
68
|
+
* free-content `footer` (the legal links).
|
|
35
69
|
*/
|
|
36
|
-
declare function TopBarActionMenu({ icon, ariaLabel, heading, entries, panelClassName, }: {
|
|
37
|
-
icon
|
|
70
|
+
declare function TopBarActionMenu({ icon, trigger, triggerClassName, ariaLabel, heading, header, entries, children, footer, footerLabel, align, panelClassName, }: {
|
|
71
|
+
/** The trigger's face for the usual icon button. Ignored when `trigger` is given. */
|
|
72
|
+
icon?: ReactNode;
|
|
73
|
+
/**
|
|
74
|
+
* A face of your own for the trigger — the account menu's `UserAvatar`. It is drawn
|
|
75
|
+
* INSIDE the kit's button, which keeps the wiring: `aria-haspopup`, `aria-expanded`,
|
|
76
|
+
* the toggle, the keyboard. `open` is there for a chevron that turns.
|
|
77
|
+
*
|
|
78
|
+
* The button is named by `ariaLabel` followed by any readable text in the face — a
|
|
79
|
+
* `UserAvatar` `badge` label reaches a screen reader that way ("Account menu 3
|
|
80
|
+
* unread"), where an `aria-label` on the button would have hidden it. A
|
|
81
|
+
* `UserAvatar`'s initials are `aria-hidden`, so the person's name is only in the
|
|
82
|
+
* name if `ariaLabel` carries it ("Marcel Eifert, account menu").
|
|
83
|
+
*/
|
|
84
|
+
trigger?: (state: {
|
|
85
|
+
open: boolean;
|
|
86
|
+
}) => ReactNode;
|
|
87
|
+
/** Extra classes for the trigger button — `rounded-full` round an avatar. */
|
|
88
|
+
triggerClassName?: string;
|
|
89
|
+
/** Names the trigger and the menu. */
|
|
38
90
|
ariaLabel: string;
|
|
39
91
|
heading?: string;
|
|
92
|
+
/** A non-interactive identity block above the rows. */
|
|
93
|
+
header?: TopBarMenuHeader;
|
|
40
94
|
entries: TopBarMenuEntry[];
|
|
95
|
+
/**
|
|
96
|
+
* Rows the entry data cannot describe (a `Disclosure` sub-list, an app's own
|
|
97
|
+
* section), after `entries`. Render `<li>`s holding `MenuItem`s or plain controls;
|
|
98
|
+
* `close` shuts the menu.
|
|
99
|
+
*/
|
|
100
|
+
children?: (close: () => void) => ReactNode;
|
|
101
|
+
/**
|
|
102
|
+
* Free content below the rows, behind a rule — the legal links. Its links and
|
|
103
|
+
* buttons are NOT menu rows: the arrow keys skip them and Tab reaches them. Pass a
|
|
104
|
+
* function to get `close` for the links' `onClick`.
|
|
105
|
+
*/
|
|
106
|
+
footer?: ReactNode | ((close: () => void) => ReactNode);
|
|
107
|
+
/** Wraps the footer in a `<nav>` with this name ("Legal"). Without it, a plain block. */
|
|
108
|
+
footerLabel?: string;
|
|
109
|
+
/** Which edge of the trigger the panel lines up with (see `HoverMenu`). Default `end`. */
|
|
110
|
+
align?: "start" | "end" | "left" | "right";
|
|
41
111
|
panelClassName?: string;
|
|
42
112
|
}): react.JSX.Element;
|
|
43
113
|
|
|
44
|
-
export { TopBarActionMenu, type TopBarMenuEntry };
|
|
114
|
+
export { TopBarActionMenu, type TopBarMenuEntry, type TopBarMenuHeader };
|
|
@@ -1,46 +1,119 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useLayoutEffect, useRef } from "react";
|
|
3
4
|
import { Link } from "react-router";
|
|
4
5
|
import { cn } from "../lib/cn.js";
|
|
5
6
|
import { HoverMenu } from "../components/hover-menu.js";
|
|
6
|
-
import {
|
|
7
|
+
import { MenuItem } from "../components/menu-item.js";
|
|
8
|
+
import { TOPBAR_TRIGGER_CLASS } from "./topbar-controls.js";
|
|
9
|
+
const SECTION_CONTROLS = "a[href]:not([role]),button:not([role])";
|
|
10
|
+
const TABBABLE = "a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]";
|
|
11
|
+
function MenuSection({ className, children }) {
|
|
12
|
+
const ref = useRef(null);
|
|
13
|
+
useLayoutEffect(() => {
|
|
14
|
+
const el = ref.current;
|
|
15
|
+
if (!el) return;
|
|
16
|
+
for (const c of el.querySelectorAll(SECTION_CONTROLS)) {
|
|
17
|
+
c.setAttribute("role", c.tagName === "A" ? "link" : "button");
|
|
18
|
+
}
|
|
19
|
+
for (const l of el.querySelectorAll("ul:not([role]),ol:not([role])")) l.setAttribute("role", "list");
|
|
20
|
+
for (const l of el.querySelectorAll("li:not([role])")) l.setAttribute("role", "listitem");
|
|
21
|
+
});
|
|
22
|
+
return /* @__PURE__ */ jsx("li", { ref, role: "none", className, children });
|
|
23
|
+
}
|
|
24
|
+
function keepTabInsidePanel(e) {
|
|
25
|
+
if (e.key !== "Tab") return;
|
|
26
|
+
const from = e.target;
|
|
27
|
+
const stops = Array.from(e.currentTarget.querySelectorAll(TABBABLE)).filter(
|
|
28
|
+
(el) => el.getAttribute("tabindex") !== "-1"
|
|
29
|
+
);
|
|
30
|
+
const way = e.shiftKey ? Node.DOCUMENT_POSITION_PRECEDING : Node.DOCUMENT_POSITION_FOLLOWING;
|
|
31
|
+
if (stops.some((el) => el !== from && from.compareDocumentPosition(el) & way)) e.stopPropagation();
|
|
32
|
+
}
|
|
7
33
|
function TopBarActionMenu({
|
|
8
34
|
icon,
|
|
35
|
+
trigger,
|
|
36
|
+
triggerClassName,
|
|
9
37
|
ariaLabel,
|
|
10
38
|
heading,
|
|
39
|
+
header,
|
|
11
40
|
entries,
|
|
41
|
+
children,
|
|
42
|
+
footer,
|
|
43
|
+
footerLabel,
|
|
44
|
+
align,
|
|
12
45
|
panelClassName
|
|
13
46
|
}) {
|
|
14
47
|
return /* @__PURE__ */ jsx(
|
|
15
48
|
HoverMenu,
|
|
16
49
|
{
|
|
17
50
|
ariaLabel,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
51
|
+
align,
|
|
52
|
+
trigger: ({ open, toggle }) => trigger ? /* @__PURE__ */ jsxs("button", { type: "button", onClick: toggle, className: cn(TOPBAR_TRIGGER_CLASS, "relative", triggerClassName), children: [
|
|
53
|
+
/* @__PURE__ */ jsx("span", { className: "sr-only", children: ariaLabel }),
|
|
54
|
+
" ",
|
|
55
|
+
trigger({ open })
|
|
56
|
+
] }) : /* @__PURE__ */ jsx(
|
|
57
|
+
"button",
|
|
58
|
+
{
|
|
59
|
+
type: "button",
|
|
60
|
+
onClick: toggle,
|
|
61
|
+
"aria-label": ariaLabel,
|
|
62
|
+
className: cn(TOPBAR_TRIGGER_CLASS, triggerClassName),
|
|
63
|
+
children: icon
|
|
64
|
+
}
|
|
65
|
+
),
|
|
66
|
+
children: (close) => {
|
|
67
|
+
const footerBody = typeof footer === "function" ? footer(close) : footer;
|
|
68
|
+
const footerClass = "flex flex-wrap gap-x-3 gap-y-1 text-xs text-[var(--text-muted)] [&_a:hover]:text-[var(--text-primary)] [&_a]:rounded-sm [&_a:focus-visible]:outline-none [&_a:focus-visible]:ring-2 [&_a:focus-visible]:ring-[var(--brand)]";
|
|
69
|
+
return (
|
|
70
|
+
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- a Tab bubbling up from a row or footer link, see keepTabInsidePanel
|
|
71
|
+
/* @__PURE__ */ jsxs("ul", { className: cn("py-1", panelClassName), onKeyDown: keepTabInsidePanel, children: [
|
|
72
|
+
header && /* @__PURE__ */ jsxs(MenuSection, { className: "mb-1 border-b border-[var(--border)] px-3 py-2", children: [
|
|
73
|
+
/* @__PURE__ */ jsx("div", { className: "truncate text-sm font-medium text-[var(--text-primary)]", children: header.title }),
|
|
74
|
+
header.subtitle != null && /* @__PURE__ */ jsx("div", { className: "truncate text-xs text-[var(--text-muted)]", children: header.subtitle }),
|
|
75
|
+
header.extra != null && /* @__PURE__ */ jsx("div", { className: "mt-1", children: header.extra })
|
|
76
|
+
] }),
|
|
77
|
+
heading && /* @__PURE__ */ jsx("li", { className: "px-3 py-1 text-[10px] font-semibold uppercase tracking-wide text-[var(--text-placeholder)]", children: heading }),
|
|
78
|
+
entries.map((entry) => {
|
|
79
|
+
if (entry.kind === "divider") {
|
|
80
|
+
return /* @__PURE__ */ jsx("li", { className: "my-1 border-t border-[var(--border)]" }, entry.key);
|
|
81
|
+
}
|
|
82
|
+
return /* @__PURE__ */ jsx("li", { children: entry.kind === "link" ? /* @__PURE__ */ jsx(
|
|
83
|
+
MenuItem,
|
|
84
|
+
{
|
|
85
|
+
href: entry.to,
|
|
86
|
+
renderLink: ({ href, ...p }) => /* @__PURE__ */ jsx(Link, { to: href, ...p }),
|
|
87
|
+
onClick: close,
|
|
88
|
+
leading: entry.icon,
|
|
89
|
+
trailing: entry.trailing,
|
|
90
|
+
tone: entry.tone,
|
|
91
|
+
current: entry.current,
|
|
92
|
+
disabled: entry.disabled,
|
|
93
|
+
children: entry.label
|
|
94
|
+
}
|
|
95
|
+
) : /* @__PURE__ */ jsx(
|
|
96
|
+
MenuItem,
|
|
97
|
+
{
|
|
98
|
+
onClick: () => {
|
|
99
|
+
entry.onSelect();
|
|
100
|
+
close();
|
|
101
|
+
},
|
|
102
|
+
leading: entry.icon,
|
|
103
|
+
trailing: entry.trailing,
|
|
104
|
+
tone: entry.tone,
|
|
105
|
+
checked: entry.checked,
|
|
106
|
+
checkable: entry.checkable,
|
|
107
|
+
disabled: entry.disabled,
|
|
108
|
+
children: entry.label
|
|
109
|
+
}
|
|
110
|
+
) }, entry.key);
|
|
111
|
+
}),
|
|
112
|
+
children?.(close),
|
|
113
|
+
footerBody != null && footerBody !== false && /* @__PURE__ */ jsx(MenuSection, { className: "mt-1 border-t border-[var(--border)] px-3 py-2", children: footerLabel ? /* @__PURE__ */ jsx("nav", { "aria-label": footerLabel, className: footerClass, children: footerBody }) : /* @__PURE__ */ jsx("div", { className: footerClass, children: footerBody }) })
|
|
114
|
+
] })
|
|
115
|
+
);
|
|
116
|
+
}
|
|
44
117
|
}
|
|
45
118
|
);
|
|
46
119
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/shell/topbar-action-menu.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { Link } from \"react-router\";\nimport { cn } from \"../lib/cn\";\nimport { HoverMenu } from \"../components/hover-menu\";\nimport { TOPBAR_MENU_ITEM_CLASS, TOPBAR_TRIGGER_CLASS } from \"./topbar-controls\";\n\n/**\n * One row of a {@link TopBarActionMenu}. An `action` (default) runs `onSelect`, a\n * `link` navigates via react-router, and a `divider` draws a rule. Non-divider\n * rows close the menu automatically when chosen. `kind` may be omitted for\n * actions, keeping the common case terse.\n */\nexport type TopBarMenuEntry =\n | { kind?: \"action\"; key: string; icon?: ReactNode; label: ReactNode; trailing?: ReactNode; onSelect: () => void }\n | { kind: \"link\"; key: string; icon?: ReactNode; label: ReactNode; trailing?: ReactNode; to: string }\n | { kind: \"divider\"; key: string };\n\n/**\n * A top-bar icon button that opens a {@link HoverMenu} of actions and/or links,\n * with an optional uppercase heading and divider rules. Generic glue over the\n * shared top-bar chrome — e.g. the feedback launcher or the guided-tours menu —\n * so those call sites describe their rows as data instead of rebuilding the\n * trigger + list markup. For a checkmarked value-switcher use\n * {@link OptionSwitcherMenu} instead.\n */\nexport function TopBarActionMenu({\n icon,\n ariaLabel,\n heading,\n entries,\n panelClassName,\n}: {\n icon: ReactNode;\n ariaLabel: string;\n heading?: string;\n entries: TopBarMenuEntry[];\n panelClassName?: string;\n}) {\n return (\n <HoverMenu\n ariaLabel={ariaLabel}\n trigger={({ toggle }) => (\n <button type=\"button\" onClick={toggle} aria-label={ariaLabel} className={TOPBAR_TRIGGER_CLASS}>\n {icon}\n </button>\n )}\n >\n {(close) => (\n <ul className={cn(\"py-1\", panelClassName)}>\n {heading && (\n <li className=\"px-3 py-1 text-[10px] font-semibold uppercase tracking-wide text-[var(--text-placeholder)]\">\n {heading}\n </li>\n )}\n {entries.map((entry) => {\n if (entry.kind === \"divider\") {\n return <li key={entry.key} className=\"my-1 border-t border-[var(--border)]\" />;\n }\n const content = (\n <span className=\"flex w-full items-center gap-2\">\n {entry.icon}\n {entry.label}\n {entry.trailing && <span className=\"ms-auto ps-2\">{entry.trailing}</span>}\n </span>\n );\n return (\n <li key={entry.key}>\n {entry.kind === \"link\" ? (\n <Link to={entry.to} onClick={close} className={TOPBAR_MENU_ITEM_CLASS}>\n {content}\n </Link>\n ) : (\n <button\n type=\"button\"\n onClick={() => {\n entry.onSelect();\n close();\n }}\n className={TOPBAR_MENU_ITEM_CLASS}\n >\n {content}\n </button>\n )}\n </li>\n );\n })}\n </ul>\n )}\n </HoverMenu>\n );\n}\n"],"mappings":";AA0CQ,cAiBM,YAjBN;AAzCR,SAAS,YAAY;AACrB,SAAS,UAAU;AACnB,SAAS,iBAAiB;AAC1B,SAAS,wBAAwB,4BAA4B;AAqBtD,SAAS,iBAAiB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAMG;AACD,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,SAAS,CAAC,EAAE,OAAO,MACjB,oBAAC,YAAO,MAAK,UAAS,SAAS,QAAQ,cAAY,WAAW,WAAW,sBACtE,gBACH;AAAA,MAGD,WAAC,UACA,qBAAC,QAAG,WAAW,GAAG,QAAQ,cAAc,GACrC;AAAA,mBACC,oBAAC,QAAG,WAAU,8FACX,mBACH;AAAA,QAED,QAAQ,IAAI,CAAC,UAAU;AACtB,cAAI,MAAM,SAAS,WAAW;AAC5B,mBAAO,oBAAC,QAAmB,WAAU,0CAArB,MAAM,GAAsD;AAAA,UAC9E;AACA,gBAAM,UACJ,qBAAC,UAAK,WAAU,kCACb;AAAA,kBAAM;AAAA,YACN,MAAM;AAAA,YACN,MAAM,YAAY,oBAAC,UAAK,WAAU,gBAAgB,gBAAM,UAAS;AAAA,aACpE;AAEF,iBACE,oBAAC,QACE,gBAAM,SAAS,SACd,oBAAC,QAAK,IAAI,MAAM,IAAI,SAAS,OAAO,WAAW,wBAC5C,mBACH,IAEA;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAS,MAAM;AACb,sBAAM,SAAS;AACf,sBAAM;AAAA,cACR;AAAA,cACA,WAAW;AAAA,cAEV;AAAA;AAAA,UACH,KAfK,MAAM,GAiBf;AAAA,QAEJ,CAAC;AAAA,SACH;AAAA;AAAA,EAEJ;AAEJ;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/shell/topbar-action-menu.tsx"],"sourcesContent":["import { useLayoutEffect, useRef } from \"react\";\nimport type { KeyboardEvent as ReactKeyboardEvent, ReactNode } from \"react\";\nimport { Link } from \"react-router\";\nimport { cn } from \"../lib/cn\";\nimport { HoverMenu } from \"../components/hover-menu\";\nimport { MenuItem } from \"../components/menu-item\";\nimport type { MenuItemTone } from \"../components/menu-item\";\nimport { TOPBAR_TRIGGER_CLASS } from \"./topbar-controls\";\n\n/**\n * One row of a {@link TopBarActionMenu}. An `action` (default) runs `onSelect`, a\n * `link` navigates via react-router, and a `divider` draws a rule. Non-divider\n * rows close the menu automatically when chosen. `kind` may be omitted for\n * actions, keeping the common case terse.\n *\n * The rows are {@link MenuItem}s, so they take its options: `tone: \"danger\"` for the\n * log-out row, `checked` (+ `checkable`) for a choice, `current` for the link to the\n * page already open, `disabled`.\n */\nexport type TopBarMenuEntry =\n | {\n kind?: \"action\";\n key: string;\n icon?: ReactNode;\n label: ReactNode;\n trailing?: ReactNode;\n onSelect: () => void;\n tone?: MenuItemTone;\n /** Makes the row a choice (`menuitemradio`, or `menuitemcheckbox` with\n * `checkable: \"checkbox\"`) and says whether it is chosen. */\n checked?: boolean;\n checkable?: \"radio\" | \"checkbox\";\n disabled?: boolean;\n }\n | {\n kind: \"link\";\n key: string;\n icon?: ReactNode;\n label: ReactNode;\n trailing?: ReactNode;\n to: string;\n tone?: MenuItemTone;\n /** The link to the page already open: `aria-current=\"page\"` and a check mark. */\n current?: boolean;\n disabled?: boolean;\n }\n | { kind: \"divider\"; key: string };\n\n/**\n * The non-interactive identity block at the top of an account menu — kastlan's name +\n * role, keksdose's display name + email + role chip. It is text, not a `menuitem`: the\n * arrow keys pass over it.\n */\nexport interface TopBarMenuHeader {\n /** The first line — the user's name. */\n title: ReactNode;\n /** The muted second line — an email, a role. */\n subtitle?: ReactNode;\n /** Anything below — a role chip. A link or button here stays out of the arrow keys\n * and in the Tab order, like the footer's. */\n extra?: ReactNode;\n}\n\n/** What a control is, as far as `HoverMenu` is concerned, when it lives in the header\n * or the footer: not a menu row. */\nconst SECTION_CONTROLS = \"a[href]:not([role]),button:not([role])\";\nconst TABBABLE = 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]';\n\n/**\n * A header or footer inside the menu panel.\n *\n * `HoverMenu` promotes EVERY role-less button and link in its panel to a `menuitem` out\n * of the Tab order, and turns every list into `role=\"none\"` — right for the rows, wrong\n * for a footer of legal links, which would join the arrow-key cycle and drop out of Tab.\n * So before it looks (a child's layout effect runs before its parent's), the controls\n * in here get their own role spelled out, which `HoverMenu` leaves alone, and any list\n * keeps its list semantics.\n */\nfunction MenuSection({ className, children }: { className: string; children: ReactNode }) {\n const ref = useRef<HTMLLIElement>(null);\n useLayoutEffect(() => {\n const el = ref.current;\n if (!el) return;\n for (const c of el.querySelectorAll<HTMLElement>(SECTION_CONTROLS)) {\n c.setAttribute(\"role\", c.tagName === \"A\" ? \"link\" : \"button\");\n }\n for (const l of el.querySelectorAll<HTMLElement>(\"ul:not([role]),ol:not([role])\")) l.setAttribute(\"role\", \"list\");\n for (const l of el.querySelectorAll<HTMLElement>(\"li:not([role])\")) l.setAttribute(\"role\", \"listitem\");\n });\n return (\n <li ref={ref} role=\"none\" className={className}>\n {children}\n </li>\n );\n}\n\n/**\n * `HoverMenu` closes on any Tab inside the panel — right when Tab leaves it, wrong when\n * Tab is on its way from a row to a footer link, or from one footer link to the next:\n * the panel would unmount under the focus. So a Tab whose next stop is still inside the\n * panel stops here and the browser moves focus; one that leaves reaches `HoverMenu`,\n * which closes.\n */\nfunction keepTabInsidePanel(e: ReactKeyboardEvent<HTMLUListElement>) {\n if (e.key !== \"Tab\") return;\n const from = e.target as HTMLElement;\n const stops = Array.from(e.currentTarget.querySelectorAll<HTMLElement>(TABBABLE)).filter(\n (el) => el.getAttribute(\"tabindex\") !== \"-1\",\n );\n const way = e.shiftKey ? Node.DOCUMENT_POSITION_PRECEDING : Node.DOCUMENT_POSITION_FOLLOWING;\n if (stops.some((el) => el !== from && from.compareDocumentPosition(el) & way)) e.stopPropagation();\n}\n\n/**\n * A top-bar icon button that opens a {@link HoverMenu} of actions and/or links,\n * with an optional uppercase heading and divider rules. Generic glue over the\n * shared top-bar chrome — e.g. the feedback launcher or the guided-tours menu —\n * so those call sites describe their rows as data instead of rebuilding the\n * trigger + list markup. For a checkmarked value-switcher use\n * {@link OptionSwitcherMenu} instead.\n *\n * It is also the ACCOUNT menu kastlan and keksdose each hand-build on `HoverMenu`: a\n * custom `trigger` (the avatar), a `header` (name + role / email), the rows, and a\n * free-content `footer` (the legal links).\n */\nexport function TopBarActionMenu({\n icon,\n trigger,\n triggerClassName,\n ariaLabel,\n heading,\n header,\n entries,\n children,\n footer,\n footerLabel,\n align,\n panelClassName,\n}: {\n /** The trigger's face for the usual icon button. Ignored when `trigger` is given. */\n icon?: ReactNode;\n /**\n * A face of your own for the trigger — the account menu's `UserAvatar`. It is drawn\n * INSIDE the kit's button, which keeps the wiring: `aria-haspopup`, `aria-expanded`,\n * the toggle, the keyboard. `open` is there for a chevron that turns.\n *\n * The button is named by `ariaLabel` followed by any readable text in the face — a\n * `UserAvatar` `badge` label reaches a screen reader that way (\"Account menu 3\n * unread\"), where an `aria-label` on the button would have hidden it. A\n * `UserAvatar`'s initials are `aria-hidden`, so the person's name is only in the\n * name if `ariaLabel` carries it (\"Marcel Eifert, account menu\").\n */\n trigger?: (state: { open: boolean }) => ReactNode;\n /** Extra classes for the trigger button — `rounded-full` round an avatar. */\n triggerClassName?: string;\n /** Names the trigger and the menu. */\n ariaLabel: string;\n heading?: string;\n /** A non-interactive identity block above the rows. */\n header?: TopBarMenuHeader;\n entries: TopBarMenuEntry[];\n /**\n * Rows the entry data cannot describe (a `Disclosure` sub-list, an app's own\n * section), after `entries`. Render `<li>`s holding `MenuItem`s or plain controls;\n * `close` shuts the menu.\n */\n children?: (close: () => void) => ReactNode;\n /**\n * Free content below the rows, behind a rule — the legal links. Its links and\n * buttons are NOT menu rows: the arrow keys skip them and Tab reaches them. Pass a\n * function to get `close` for the links' `onClick`.\n */\n footer?: ReactNode | ((close: () => void) => ReactNode);\n /** Wraps the footer in a `<nav>` with this name (\"Legal\"). Without it, a plain block. */\n footerLabel?: string;\n /** Which edge of the trigger the panel lines up with (see `HoverMenu`). Default `end`. */\n align?: \"start\" | \"end\" | \"left\" | \"right\";\n panelClassName?: string;\n}) {\n return (\n <HoverMenu\n ariaLabel={ariaLabel}\n align={align}\n trigger={({ open, toggle }) =>\n trigger ? (\n <button type=\"button\" onClick={toggle} className={cn(TOPBAR_TRIGGER_CLASS, \"relative\", triggerClassName)}>\n <span className=\"sr-only\">{ariaLabel}</span>\n {/* The space keeps the name from running into the face's text (\"Account\n menu3 unread\"); as a flex item it takes no room. */}\n {\" \"}\n {trigger({ open })}\n </button>\n ) : (\n <button\n type=\"button\"\n onClick={toggle}\n aria-label={ariaLabel}\n className={cn(TOPBAR_TRIGGER_CLASS, triggerClassName)}\n >\n {icon}\n </button>\n )\n }\n >\n {(close) => {\n const footerBody = typeof footer === \"function\" ? footer(close) : footer;\n const footerClass =\n \"flex flex-wrap gap-x-3 gap-y-1 text-xs text-[var(--text-muted)] [&_a:hover]:text-[var(--text-primary)] [&_a]:rounded-sm [&_a:focus-visible]:outline-none [&_a:focus-visible]:ring-2 [&_a:focus-visible]:ring-[var(--brand)]\";\n return (\n // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- a Tab bubbling up from a row or footer link, see keepTabInsidePanel\n <ul className={cn(\"py-1\", panelClassName)} onKeyDown={keepTabInsidePanel}>\n {header && (\n <MenuSection className=\"mb-1 border-b border-[var(--border)] px-3 py-2\">\n <div className=\"truncate text-sm font-medium text-[var(--text-primary)]\">{header.title}</div>\n {header.subtitle != null && (\n <div className=\"truncate text-xs text-[var(--text-muted)]\">{header.subtitle}</div>\n )}\n {header.extra != null && <div className=\"mt-1\">{header.extra}</div>}\n </MenuSection>\n )}\n {heading && (\n <li className=\"px-3 py-1 text-[10px] font-semibold uppercase tracking-wide text-[var(--text-placeholder)]\">\n {heading}\n </li>\n )}\n {entries.map((entry) => {\n if (entry.kind === \"divider\") {\n return <li key={entry.key} className=\"my-1 border-t border-[var(--border)]\" />;\n }\n return (\n <li key={entry.key}>\n {entry.kind === \"link\" ? (\n <MenuItem\n href={entry.to}\n renderLink={({ href, ...p }) => <Link to={href} {...p} />}\n onClick={close}\n leading={entry.icon}\n trailing={entry.trailing}\n tone={entry.tone}\n current={entry.current}\n disabled={entry.disabled}\n >\n {entry.label}\n </MenuItem>\n ) : (\n <MenuItem\n onClick={() => {\n entry.onSelect();\n close();\n }}\n leading={entry.icon}\n trailing={entry.trailing}\n tone={entry.tone}\n checked={entry.checked}\n checkable={entry.checkable}\n disabled={entry.disabled}\n >\n {entry.label}\n </MenuItem>\n )}\n </li>\n );\n })}\n {children?.(close)}\n {footerBody != null && footerBody !== false && (\n <MenuSection className=\"mt-1 border-t border-[var(--border)] px-3 py-2\">\n {footerLabel ? (\n <nav aria-label={footerLabel} className={footerClass}>\n {footerBody}\n </nav>\n ) : (\n <div className={footerClass}>{footerBody}</div>\n )}\n </MenuSection>\n )}\n </ul>\n );\n }}\n </HoverMenu>\n );\n}\n"],"mappings":"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","names":[]}
|
package/dist/shell.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { TopBar, TopBarProps } from './shell/top-bar.js';
|
|
|
3
3
|
export { AppShell, AppShellNavItem, AppShellProps, AppShellSubItem } from './shell/app-shell.js';
|
|
4
4
|
export { OptionSwitcherMenu, OptionSwitcherOption } from './shell/option-switcher-menu.js';
|
|
5
5
|
export { RoleSwitcher, RoleSwitcherProps } from './shell/role-switcher.js';
|
|
6
|
-
export { TopBarActionMenu, TopBarMenuEntry } from './shell/topbar-action-menu.js';
|
|
6
|
+
export { TopBarActionMenu, TopBarMenuEntry, TopBarMenuHeader } from './shell/topbar-action-menu.js';
|
|
7
7
|
import 'react';
|
|
8
8
|
import './theme/palette-presets.js';
|
|
9
9
|
import './theme/chart-palette.js';
|
|
@@ -11,3 +11,4 @@ import './theme/theme-store.js';
|
|
|
11
11
|
import 'zustand/middleware';
|
|
12
12
|
import 'zustand';
|
|
13
13
|
import 'lucide-react';
|
|
14
|
+
import './components/menu-item.js';
|
|
@@ -4,6 +4,7 @@ import { ButtonVariant } from '../components/ui.js';
|
|
|
4
4
|
import { UseWizardReturn, WizardLabels } from './types.js';
|
|
5
5
|
import '../components/tooltip.js';
|
|
6
6
|
import '../hooks/use-anchored-rect.js';
|
|
7
|
+
import '../lib/clipping.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* The chrome around a wizard: the step indicator, the step content, and the
|
|
@@ -15,5 +15,11 @@ interface WizardContextValue {
|
|
|
15
15
|
}
|
|
16
16
|
declare const WizardContextProvider: react.Provider<WizardContextValue | null>;
|
|
17
17
|
declare function useWizardContext(): WizardContextValue;
|
|
18
|
+
/**
|
|
19
|
+
* The wizard context, or `null` outside a `<StepperNav>`. For a hook that registers
|
|
20
|
+
* with the wizard when there is one and works without it — `useRhfWizardStep` in
|
|
21
|
+
* `@eifi1/ui-kit/rhf`, whose returned `validate` a custom nav can call itself.
|
|
22
|
+
*/
|
|
23
|
+
declare function useOptionalWizardContext(): WizardContextValue | null;
|
|
18
24
|
|
|
19
|
-
export { WizardContextProvider, type WizardContextValue, useWizardContext };
|
|
25
|
+
export { WizardContextProvider, type WizardContextValue, useOptionalWizardContext, useWizardContext };
|
|
@@ -9,8 +9,12 @@ function useWizardContext() {
|
|
|
9
9
|
}
|
|
10
10
|
return ctx;
|
|
11
11
|
}
|
|
12
|
+
function useOptionalWizardContext() {
|
|
13
|
+
return useContext(WizardContext);
|
|
14
|
+
}
|
|
12
15
|
export {
|
|
13
16
|
WizardContextProvider,
|
|
17
|
+
useOptionalWizardContext,
|
|
14
18
|
useWizardContext
|
|
15
19
|
};
|
|
16
20
|
//# sourceMappingURL=wizard-context.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/wizard/wizard-context.tsx"],"sourcesContent":["import { createContext, useContext } from \"react\";\nimport type { ValidateResult } from \"./types\";\n\nexport interface WizardContextValue {\n /**\n * Register a validator for the currently-mounted step. Returns an unregister\n * fn (call it in a useEffect cleanup). `goNext` runs every registered\n * validator for the active step (plus the step config's own `validate`),\n * AND-combining the results — this replaces the per-page `useRef` +\n * `validate: () => ref.current?.()` bridge each RHF step used to wire.\n */\n registerStepValidate: (\n fn: () => ValidateResult | Promise<ValidateResult>,\n ) => () => void;\n /** Lets the mounted step disable Next/Skip (e.g. invalid tenant shares). */\n setNextBlocked: (blocked: boolean) => void;\n}\n\nconst WizardContext = createContext<WizardContextValue | null>(null);\n\nexport const WizardContextProvider = WizardContext.Provider;\n\nexport function useWizardContext(): WizardContextValue {\n const ctx = useContext(WizardContext);\n if (!ctx) {\n throw new Error(\"useWizardContext must be used within a <StepperNav>\");\n }\n return ctx;\n}\n"],"mappings":";AAAA,SAAS,eAAe,kBAAkB;AAkB1C,MAAM,gBAAgB,cAAyC,IAAI;AAE5D,MAAM,wBAAwB,cAAc;AAE5C,SAAS,mBAAuC;AACrD,QAAM,MAAM,WAAW,aAAa;AACpC,MAAI,CAAC,KAAK;AACR,UAAM,IAAI,MAAM,qDAAqD;AAAA,EACvE;AACA,SAAO;AACT;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/wizard/wizard-context.tsx"],"sourcesContent":["import { createContext, useContext } from \"react\";\nimport type { ValidateResult } from \"./types\";\n\nexport interface WizardContextValue {\n /**\n * Register a validator for the currently-mounted step. Returns an unregister\n * fn (call it in a useEffect cleanup). `goNext` runs every registered\n * validator for the active step (plus the step config's own `validate`),\n * AND-combining the results — this replaces the per-page `useRef` +\n * `validate: () => ref.current?.()` bridge each RHF step used to wire.\n */\n registerStepValidate: (\n fn: () => ValidateResult | Promise<ValidateResult>,\n ) => () => void;\n /** Lets the mounted step disable Next/Skip (e.g. invalid tenant shares). */\n setNextBlocked: (blocked: boolean) => void;\n}\n\nconst WizardContext = createContext<WizardContextValue | null>(null);\n\nexport const WizardContextProvider = WizardContext.Provider;\n\nexport function useWizardContext(): WizardContextValue {\n const ctx = useContext(WizardContext);\n if (!ctx) {\n throw new Error(\"useWizardContext must be used within a <StepperNav>\");\n }\n return ctx;\n}\n\n/**\n * The wizard context, or `null` outside a `<StepperNav>`. For a hook that registers\n * with the wizard when there is one and works without it — `useRhfWizardStep` in\n * `@eifi1/ui-kit/rhf`, whose returned `validate` a custom nav can call itself.\n */\nexport function useOptionalWizardContext(): WizardContextValue | null {\n return useContext(WizardContext);\n}\n"],"mappings":";AAAA,SAAS,eAAe,kBAAkB;AAkB1C,MAAM,gBAAgB,cAAyC,IAAI;AAE5D,MAAM,wBAAwB,cAAc;AAE5C,SAAS,mBAAuC;AACrD,QAAM,MAAM,WAAW,aAAa;AACpC,MAAI,CAAC,KAAK;AACR,UAAM,IAAI,MAAM,qDAAqD;AAAA,EACvE;AACA,SAAO;AACT;AAOO,SAAS,2BAAsD;AACpE,SAAO,WAAW,aAAa;AACjC;","names":[]}
|
package/dist/wizard.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { DEFAULT_WIZARD_LABELS, FieldErrors, StepStatus, SummaryItem, SummarySection, UseWizardOptions, UseWizardReturn, ValidateResult, WizardLabels, WizardStepConfig, WizardUrlSync, resolveWizardLabels } from './wizard/types.js';
|
|
2
2
|
export { useWizard } from './wizard/use-wizard.js';
|
|
3
|
-
export { WizardContextProvider, WizardContextValue, useWizardContext } from './wizard/wizard-context.js';
|
|
3
|
+
export { WizardContextProvider, WizardContextValue, useOptionalWizardContext, useWizardContext } from './wizard/wizard-context.js';
|
|
4
4
|
export { RequiredFieldSpec, requiredFieldsValidator } from './wizard/validation.js';
|
|
5
5
|
export { WizardStep } from './wizard/wizard-step.js';
|
|
6
6
|
export { WizardSummary } from './wizard/wizard-summary.js';
|
|
@@ -9,3 +9,4 @@ import 'react';
|
|
|
9
9
|
import './components/ui.js';
|
|
10
10
|
import './components/tooltip.js';
|
|
11
11
|
import './hooks/use-anchored-rect.js';
|
|
12
|
+
import './lib/clipping.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eifi1/ui-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
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.",
|