@eifi1/ui-kit 0.50.1 → 0.51.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 +11 -9
- package/dist/admin/user-roster.js +31 -12
- package/dist/admin/user-roster.js.map +1 -1
- package/dist/auth/verify-email.d.ts +4 -0
- package/dist/auth/verify-email.js.map +1 -1
- package/dist/components/alert-banner.d.ts +9 -0
- package/dist/components/alert-banner.js +9 -5
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/button-look.d.ts +4 -19
- package/dist/components/button-look.js +3 -30
- package/dist/components/button-look.js.map +1 -1
- package/dist/components/data-table-fit.js.map +1 -1
- package/dist/components/data-table-virtual.js +1 -0
- package/dist/components/data-table-virtual.js.map +1 -1
- package/dist/components/field-base.d.ts +31 -1
- package/dist/components/field-base.js +28 -0
- package/dist/components/field-base.js.map +1 -1
- package/dist/components/floating-panel.d.ts +6 -13
- package/dist/components/floating-panel.js +2 -9
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/icon-button.js +3 -5
- package/dist/components/icon-button.js.map +1 -1
- package/dist/components/select.js +3 -2
- package/dist/components/select.js.map +1 -1
- package/dist/components/stat-tile.d.ts +8 -1
- package/dist/components/stat-tile.js +7 -3
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/i18n/german.js +2 -0
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +2 -2
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/en.js +2 -0
- package/dist/i18n/locales/en.js.map +2 -2
- package/dist/i18n/locales/es.js +2 -0
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +2 -0
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +2 -0
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +2 -0
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +2 -0
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/landing/page-seo.d.ts +19 -11
- package/dist/landing/page-seo.js +11 -7
- package/dist/landing/page-seo.js.map +1 -1
- package/dist/landing/public-page.d.ts +57 -0
- package/dist/landing/public-page.js +45 -0
- package/dist/landing/public-page.js.map +1 -0
- package/dist/lib/server-wake.d.ts +3 -2
- package/dist/lib/server-wake.js +19 -13
- package/dist/lib/server-wake.js.map +1 -1
- package/dist/shell/app-shell.d.ts +2 -15
- package/dist/shell/app-shell.js +7 -26
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/dev-menu.d.ts +98 -0
- package/dist/shell/dev-menu.js +133 -0
- package/dist/shell/dev-menu.js.map +1 -0
- package/dist/shell/dev-switchers.d.ts +6 -53
- package/dist/shell/dev-switchers.js +5 -100
- package/dist/shell/dev-switchers.js.map +1 -1
- package/dist/shell/top-bar-actions.d.ts +4 -1
- package/dist/shell/top-bar-actions.js.map +1 -1
- package/dist/shell/top-bar-parts-labels.d.ts +18 -9
- package/dist/shell/top-bar-parts-labels.js +2 -0
- package/dist/shell/top-bar-parts-labels.js.map +1 -1
- package/dist/shell/topbar-action-menu.d.ts +17 -2
- package/dist/shell/topbar-action-menu.js +3 -1
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell.d.ts +3 -1
- package/dist/shell.js +3 -1
- package/dist/shell.js.map +1 -1
- package/dist/theme/palette-presets.d.ts +0 -3
- package/dist/theme/palette-presets.js +0 -2
- package/dist/theme/palette-presets.js.map +1 -1
- package/package.json +1 -1
- package/src/admin/user-roster.tsx +30 -10
- package/src/auth/verify-email.tsx +4 -0
- package/src/components/alert-banner.tsx +37 -10
- package/src/components/button-look.ts +7 -57
- package/src/components/data-table-fit.ts +4 -1
- package/src/components/data-table-virtual.ts +14 -1
- package/src/components/field-base.tsx +53 -1
- package/src/components/floating-panel.tsx +6 -28
- package/src/components/icon-button.tsx +6 -8
- package/src/components/select.tsx +3 -6
- package/src/components/stat-tile.tsx +27 -9
- package/src/i18n/german.ts +2 -0
- package/src/i18n/kit-labels.tsx +2 -2
- package/src/i18n/locales/es.ts +2 -0
- package/src/i18n/locales/fr.ts +2 -0
- package/src/i18n/locales/hu.ts +2 -0
- package/src/i18n/locales/it.ts +2 -0
- package/src/i18n/locales/zh.ts +2 -0
- package/src/index.ts +4 -1
- package/src/landing/page-seo.ts +28 -16
- package/src/landing/public-page.tsx +106 -0
- package/src/lib/server-wake.ts +41 -17
- package/src/shell/app-shell.tsx +11 -48
- package/src/shell/dev-menu.tsx +259 -0
- package/src/shell/dev-switchers.tsx +13 -174
- package/src/shell/top-bar-actions.tsx +4 -1
- package/src/shell/top-bar-parts-labels.ts +20 -9
- package/src/shell/topbar-action-menu.tsx +28 -2
- package/src/shell.ts +5 -1
- package/src/theme/palette-presets.ts +0 -4
|
@@ -34,54 +34,7 @@ export type ButtonVariant =
|
|
|
34
34
|
| "secondary"
|
|
35
35
|
| "ghost"
|
|
36
36
|
| "danger"
|
|
37
|
-
| "link"
|
|
38
|
-
/** @deprecated 0.49 — use `default` (same look); removed in 0.51. */
|
|
39
|
-
| "primary"
|
|
40
|
-
/** @deprecated 0.49 — use `accent` (same look); removed in 0.51. */
|
|
41
|
-
| "brand";
|
|
42
|
-
|
|
43
|
-
/** @internal A {@link ButtonVariant} without the deprecated aliases. */
|
|
44
|
-
export type CanonicalButtonVariant = Exclude<ButtonVariant, "primary" | "brand">;
|
|
45
|
-
|
|
46
|
-
/** The deprecated names and what they stand for — see {@link ButtonVariant}. */
|
|
47
|
-
const DEPRECATED_BUTTON_VARIANTS: Readonly<Record<string, CanonicalButtonVariant>> = {
|
|
48
|
-
primary: "default",
|
|
49
|
-
brand: "accent",
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
/** The kit's dev switch, as `lib/logger.ts` reads it: a development build, never a
|
|
53
|
-
* production one (the consumer's bundler makes `import.meta.env.DEV` false) and never
|
|
54
|
-
* under vitest. Read per call so a test can switch it on with `vi.stubEnv`. */
|
|
55
|
-
function devWarnings(): boolean {
|
|
56
|
-
return Boolean(import.meta.env?.DEV) && !import.meta.env?.VITEST;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const warnedVariants = new Set<string>();
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* @internal A deprecated variant name warns once (per `component` and name) in a
|
|
63
|
-
* development build, then reads as the name it stands for. `component` is the prop's
|
|
64
|
-
* owner in the message ("Button variant", "FormActions submitVariant", …).
|
|
65
|
-
*/
|
|
66
|
-
export function warnDeprecatedVariant(component: string, name: string, replacement: string): void {
|
|
67
|
-
if (!devWarnings()) return;
|
|
68
|
-
const key = `${component}|${name}`;
|
|
69
|
-
if (warnedVariants.has(key)) return;
|
|
70
|
-
warnedVariants.add(key);
|
|
71
|
-
console.warn(
|
|
72
|
-
`[ui-kit] ${component}="${name}" is deprecated: use "${replacement}" (same look). ` +
|
|
73
|
-
`"${name}" is removed in 0.51.`,
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** @internal `primary` → `default`, `brand` → `accent` (warning once in development);
|
|
78
|
-
* every other variant as it is. Runs before every lookup keyed by the variant. */
|
|
79
|
-
export function resolveButtonVariant(variant: ButtonVariant, component = "Button variant"): CanonicalButtonVariant {
|
|
80
|
-
const replacement = DEPRECATED_BUTTON_VARIANTS[variant];
|
|
81
|
-
if (replacement === undefined) return variant as CanonicalButtonVariant;
|
|
82
|
-
warnDeprecatedVariant(component, variant, replacement);
|
|
83
|
-
return replacement;
|
|
84
|
-
}
|
|
37
|
+
| "link";
|
|
85
38
|
|
|
86
39
|
// Shared base ring for every button-styled element. Kept as a named const so the
|
|
87
40
|
// <Button> component and the {@link buttonClasses} helper draw from one source and
|
|
@@ -155,9 +108,8 @@ export const HOVER_INK = "hover:bg-[color-mix(in_srgb,var(--text-primary)_7%,tra
|
|
|
155
108
|
// outline); `accent` is the solid brand fill for the rare strong CTA; `danger` takes the
|
|
156
109
|
// semantic `--danger` family (destructive semantics), so a consumer can re-point the
|
|
157
110
|
// destructive hue instead of inheriting a hard-coded red. Focus rings follow each
|
|
158
|
-
// variant's own accent: brand for the four neutral ones, danger for `danger`.
|
|
159
|
-
|
|
160
|
-
export const buttonVariantClasses: Record<CanonicalButtonVariant, string> = {
|
|
111
|
+
// variant's own accent: brand for the four neutral ones, danger for `danger`.
|
|
112
|
+
export const buttonVariantClasses: Record<ButtonVariant, string> = {
|
|
161
113
|
default:
|
|
162
114
|
// Hovers onto `--bg-active`, not `--border` (0.33, §12.9): More contrast steps the
|
|
163
115
|
// border until it is a 3:1 LINE, and as a fill under the label that left 2.7:1.
|
|
@@ -223,12 +175,12 @@ const BUTTON_TONES: Record<Exclude<ButtonTone, "default">, string> = {
|
|
|
223
175
|
};
|
|
224
176
|
|
|
225
177
|
/** Only the two transparent variants take every tone; see {@link ButtonTone}. */
|
|
226
|
-
const TONED_VARIANTS = new Set<
|
|
178
|
+
const TONED_VARIANTS = new Set<ButtonVariant>(["link", "ghost"]);
|
|
227
179
|
|
|
228
180
|
// `tone="danger"` on the neutral boxed variants — see {@link ButtonTone}. Each string
|
|
229
181
|
// restates every colour the variant sets (border, fill, text, hover, ring) so none of
|
|
230
182
|
// the neutral ones survives the merge.
|
|
231
|
-
const BUTTON_BOXED_DANGER: Partial<Record<
|
|
183
|
+
const BUTTON_BOXED_DANGER: Partial<Record<ButtonVariant, string>> = {
|
|
232
184
|
// Not boxed, but its hover is a fill: the danger wash under the danger text, as
|
|
233
185
|
// IconButton's danger tone (0.33). On B′'s translucent ink `--danger` fell to 4.2:1
|
|
234
186
|
// over a light page.
|
|
@@ -266,12 +218,11 @@ export interface ButtonClassesOptions {
|
|
|
266
218
|
* compact toolbar links.
|
|
267
219
|
*/
|
|
268
220
|
export function buttonClasses(
|
|
269
|
-
|
|
221
|
+
variant: ButtonVariant = "default",
|
|
270
222
|
classNameOrOptions?: string | ButtonClassesOptions,
|
|
271
223
|
): string {
|
|
272
224
|
const { size = "md", className } =
|
|
273
225
|
typeof classNameOrOptions === "object" ? classNameOrOptions : { className: classNameOrOptions };
|
|
274
|
-
const variant = resolveButtonVariant(given, "buttonClasses variant");
|
|
275
226
|
return cn(
|
|
276
227
|
BUTTON_BASE,
|
|
277
228
|
BUTTON_SIZES[size],
|
|
@@ -291,8 +242,7 @@ export const ICON_BUTTON_PRESSED =
|
|
|
291
242
|
"bg-brand-soft text-brand hover:bg-brand-soft-hover hover:text-brand-muted disabled:hover:bg-brand-soft disabled:hover:text-brand";
|
|
292
243
|
|
|
293
244
|
/** @internal The button look shared by the `<button>` and the `<a>` form. */
|
|
294
|
-
export function buttonLook(
|
|
295
|
-
const variant = resolveButtonVariant(given);
|
|
245
|
+
export function buttonLook(variant: ButtonVariant, size: ButtonSize, stretch: boolean | undefined, tone: ButtonTone) {
|
|
296
246
|
return cn(
|
|
297
247
|
BUTTON_BASE,
|
|
298
248
|
BUTTON_SIZES[size],
|
|
@@ -183,7 +183,10 @@ export function useColumnFit<T>({
|
|
|
183
183
|
// update limit (#185). So a column whose return the next measure undid, at the same
|
|
184
184
|
// scroller width, waits for more room than it had then: a wider scroller or narrower
|
|
185
185
|
// content. Whatever the error in its width, that is one invisible flip per room it is
|
|
186
|
-
// tried at.
|
|
186
|
+
// tried at. (0.51, keksdose N2: under `virtualize` that excess was the table coming
|
|
187
|
+
// back at an OLD lock's ratcheted widths, not the ones measured here; the lock now goes
|
|
188
|
+
// when the columns change, data-table-virtual.ts. The guard stays for the rest: a
|
|
189
|
+
// column whose drawn cells are wider than its header.)
|
|
187
190
|
let n = current.length;
|
|
188
191
|
const rooms = new Map<string, number>();
|
|
189
192
|
while (n > 0) {
|
|
@@ -30,7 +30,9 @@ import type { RefObject } from "react";
|
|
|
30
30
|
// nothing). Re-measured from scratch when the data, the columns, a user width, the text
|
|
31
31
|
// size or the scroller's width change — and when a grown column would push the table
|
|
32
32
|
// past its scroller, which it fit before. A `grow` column and a user-resized one are not
|
|
33
|
-
// locked (the first takes the rest, the second is pinned already).
|
|
33
|
+
// locked (the first takes the rest, the second is pinned already). A lock replaced is
|
|
34
|
+
// gone: changing back (a column the fit leaves out and brings back) measures again
|
|
35
|
+
// (0.51, keksdose N2).
|
|
34
36
|
|
|
35
37
|
/** {@link DataTableProps.virtualize}'s options. */
|
|
36
38
|
export interface DataTableVirtualize {
|
|
@@ -302,6 +304,17 @@ export function useRowWindow<T>({
|
|
|
302
304
|
);
|
|
303
305
|
|
|
304
306
|
const lockSig = `${resetKey}|${scrollerWidth}|${colKeys.join(",")}`;
|
|
307
|
+
// A lock is the table it was measured on, so it goes as soon as the signature moves on
|
|
308
|
+
// — not merely set aside until the signature comes BACK (0.51, keksdose N2). The fit
|
|
309
|
+
// leaves a column out and, measuring the table afresh without it, brings it back in the
|
|
310
|
+
// same frame, before this hook measures anything: the signature was then the old one
|
|
311
|
+
// again, and the column came back at the widths that had ratcheted up while rows
|
|
312
|
+
// scrolled (a wider cell widens its column once), 80–110px more than the fit had just
|
|
313
|
+
// measured (606 + 93 estimated, 778–812 drawn, at 757), and B2's guard kept it out
|
|
314
|
+
// although it fitted. Dropped here, the table it comes back to is drawn as measured,
|
|
315
|
+
// and the next frame locks that. Adjusted during render, as React's "storing
|
|
316
|
+
// information from previous renders": an effect would draw the stale widths first.
|
|
317
|
+
if (lock !== null && lock.sig !== lockSig) setLock(null);
|
|
305
318
|
const colWidths = lock && lock.sig === lockSig ? lock.widths : null;
|
|
306
319
|
|
|
307
320
|
// What the scroll handler and the frame's measure read: this render's model and lock.
|
|
@@ -21,7 +21,8 @@ import { DEFAULT_CHARACTER_COUNT_LABELS, DEFAULT_PASSWORD_REVEAL_LABELS } from "
|
|
|
21
21
|
* `useFieldError`, every field's.
|
|
22
22
|
*
|
|
23
23
|
* Public through ./ui by name. `FieldGroup`, the counter and the placeholder helpers are
|
|
24
|
-
* the three fields' shared parts (`@internal`)
|
|
24
|
+
* the three fields' shared parts (`@internal`), as are the Select's own classes and its
|
|
25
|
+
* `SelectWidthFloor` (0.51).
|
|
25
26
|
*/
|
|
26
27
|
|
|
27
28
|
/**
|
|
@@ -197,6 +198,57 @@ export function FieldChevron({ className, ...rest }: FieldChevronProps) {
|
|
|
197
198
|
);
|
|
198
199
|
}
|
|
199
200
|
|
|
201
|
+
/** A native dropdown Select's dress: no native arrow, and `pe-9`, the room the
|
|
202
|
+
* {@link FieldChevron} takes. Shared with {@link SelectWidthFloor}. `@internal` */
|
|
203
|
+
export const SELECT_MENU_DRESS = "appearance-none pe-9";
|
|
204
|
+
|
|
205
|
+
/** The compact Select (`size="sm"`): the field's colours, a toolbar button's height.
|
|
206
|
+
* `py-0` and a fixed height rather than a smaller padding, so the box is 28px whatever
|
|
207
|
+
* line height the caller's type brings with it. Shared with {@link SelectWidthFloor}.
|
|
208
|
+
* `@internal` */
|
|
209
|
+
export const SELECT_SM = "h-7 py-0 ps-2 pe-7 text-xs";
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* The width an UNLABELLED Select needs to show the widest of `values` whole: an invisible,
|
|
213
|
+
* zero-height line under it, in the select's own type, padding, chevron room and border
|
|
214
|
+
* (the same classes), one value per line. Placed beside the Select in the box that sizes
|
|
215
|
+
* it, it holds that box to the widest value, and the `w-full` select fills it. `@internal`
|
|
216
|
+
*
|
|
217
|
+
* Why it takes a sizer (0.51, Kurvenschmiede N5). A `<select>` with a percentage width
|
|
218
|
+
* is a "compressible" element: its min-content contribution is ZERO (CSS Sizing 3
|
|
219
|
+
* §5.2.2), so the auto table layout squeezes its column to the header, and beside a
|
|
220
|
+
* `grow` column every other column IS squeezed to its min-content (data-table-fit.ts).
|
|
221
|
+
* The user roster's role select stood 92px and read "Meml…" and "Admi…" (KS review,
|
|
222
|
+
* 0.49 and 0.50). `min-width: max-content` on the select would hold it instead — but to
|
|
223
|
+
* its widest OPTION, and an unavailable role's option carries its reason ("Customer (owns
|
|
224
|
+
* work)", RoleSelect's `optionDisabledReason`): the column would widen for a text that is
|
|
225
|
+
* never the selected value. So the caller says which values can be selected, in the
|
|
226
|
+
* current language.
|
|
227
|
+
*/
|
|
228
|
+
export function SelectWidthFloor({ values, size = "md" }: { values: readonly string[]; size?: "sm" | "md" }) {
|
|
229
|
+
if (values.length === 0) return null;
|
|
230
|
+
return (
|
|
231
|
+
<span
|
|
232
|
+
aria-hidden
|
|
233
|
+
data-select-floor=""
|
|
234
|
+
className={cn(
|
|
235
|
+
FIELD_BASE,
|
|
236
|
+
SELECT_MENU_DRESS,
|
|
237
|
+
size === "sm" && SELECT_SM,
|
|
238
|
+
// The horizontal box only: no height, no vertical padding or border, not painted.
|
|
239
|
+
"invisible h-0 overflow-hidden border-y-0 py-0 whitespace-nowrap shadow-none",
|
|
240
|
+
)}
|
|
241
|
+
>
|
|
242
|
+
{values.map((value, i) => (
|
|
243
|
+
// The value as generated content, not as text: it is laid out (and so measured)
|
|
244
|
+
// the same, but a test's `getByText("Member")` still finds the one option, and a
|
|
245
|
+
// find-in-page or a copied row does not meet a word nobody sees.
|
|
246
|
+
<span key={i} data-value={value} className="block before:content-[attr(data-value)]" />
|
|
247
|
+
))}
|
|
248
|
+
</span>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
200
252
|
// Animated label that starts centred (as a placeholder) in an empty field and
|
|
201
253
|
// floats up INSIDE the top strip on focus or once the field has a value. Sits on
|
|
202
254
|
// the field's own surface, so no background chip and nothing to mismatch the card.
|
|
@@ -15,7 +15,6 @@ import { X } from "lucide-react";
|
|
|
15
15
|
|
|
16
16
|
import { cn } from "../lib/cn";
|
|
17
17
|
import { FOCUS_RING } from "./focus-ring";
|
|
18
|
-
import { warnDeprecatedVariant } from "./button-look";
|
|
19
18
|
import { dirOf, type Direction } from "../lib/direction";
|
|
20
19
|
import { registerFloating } from "../lib/floating-stack";
|
|
21
20
|
import { useFloatClearance } from "../lib/float-clearance";
|
|
@@ -142,14 +141,9 @@ export interface FloatingActionButtonProps extends Omit<ButtonHTMLAttributes<HTM
|
|
|
142
141
|
* `accent`. `surface`: the page's surface with a hairline border — for a FAB that is a
|
|
143
142
|
* status rather than a call to action, as kastlan's offline pill is (a brand pill would
|
|
144
143
|
* shout "press me" at a user who is merely offline). Ignored while `pressed` is set; a
|
|
145
|
-
* toggle has its own two looks.
|
|
146
|
-
* development build warns once), removed in 0.51.
|
|
144
|
+
* toggle has its own two looks.
|
|
147
145
|
*/
|
|
148
|
-
variant?:
|
|
149
|
-
| "accent"
|
|
150
|
-
| "surface"
|
|
151
|
-
/** @deprecated 0.49 — use `accent` (same look); removed in 0.51. */
|
|
152
|
-
| "primary";
|
|
146
|
+
variant?: "accent" | "surface";
|
|
153
147
|
/**
|
|
154
148
|
* Hover and focus label in the kit {@link Tooltip}, instead of the native `title` —
|
|
155
149
|
* keksdose has ONE tooltip (dev#523) and wrapped its assistant launcher by hand,
|
|
@@ -173,14 +167,6 @@ export interface FloatingActionButtonProps extends Omit<ButtonHTMLAttributes<HTM
|
|
|
173
167
|
ref?: Ref<HTMLButtonElement>;
|
|
174
168
|
}
|
|
175
169
|
|
|
176
|
-
/** FloatingActionButton's and FloatingAction's `primary` was always the solid brand
|
|
177
|
-
* fill: it reads as `accent` (warning once in development), never as Button's
|
|
178
|
-
* `default`. */
|
|
179
|
-
function deprecatedPrimary(component: string): "accent" {
|
|
180
|
-
warnDeprecatedVariant(component, "primary", "accent");
|
|
181
|
-
return "accent";
|
|
182
|
-
}
|
|
183
|
-
|
|
184
170
|
/**
|
|
185
171
|
* A round, icon-only button pinned to a corner of the viewport — the trigger of a
|
|
186
172
|
* {@link FloatingPanel}, and usable alone for a page's one primary action.
|
|
@@ -215,7 +201,7 @@ export function FloatingActionButton({
|
|
|
215
201
|
pressed,
|
|
216
202
|
extended = false,
|
|
217
203
|
live = false,
|
|
218
|
-
variant
|
|
204
|
+
variant = "accent",
|
|
219
205
|
tooltip,
|
|
220
206
|
tooltipSide = "top",
|
|
221
207
|
hideWhenOverlay = true,
|
|
@@ -230,7 +216,6 @@ export function FloatingActionButton({
|
|
|
230
216
|
const overlay = useOverlayOpen(hideWhenOverlay);
|
|
231
217
|
const hidden = hiddenProp || overlay || undefined;
|
|
232
218
|
const [marker, dir] = usePortalDir();
|
|
233
|
-
const variant = givenVariant === "primary" ? deprecatedPrimary("FloatingActionButton variant") : givenVariant;
|
|
234
219
|
const toggle = pressed !== undefined;
|
|
235
220
|
const tip = tooltip === true ? label : tooltip === false || tooltip === "" ? undefined : tooltip;
|
|
236
221
|
const wrapped = tip !== undefined && tip !== null;
|
|
@@ -700,15 +685,9 @@ export interface FloatingActionProps extends Omit<ButtonHTMLAttributes<HTMLButto
|
|
|
700
685
|
pressedStyle?: "tint" | "plain";
|
|
701
686
|
/**
|
|
702
687
|
* `accent`: the brand-filled member, for the group's main action (the /transactions
|
|
703
|
-
* "+"), as Button's `accent`. `default` (default): the quiet one.
|
|
704
|
-
* deprecated alias of `accent` (0.49; a development build warns once), removed no
|
|
705
|
-
* earlier than 0.51.
|
|
688
|
+
* "+"), as Button's `accent`. `default` (default): the quiet one.
|
|
706
689
|
*/
|
|
707
|
-
variant?:
|
|
708
|
-
| "default"
|
|
709
|
-
| "accent"
|
|
710
|
-
/** @deprecated 0.49 — use `accent` (same look); removed in 0.51. */
|
|
711
|
-
| "primary";
|
|
690
|
+
variant?: "default" | "accent";
|
|
712
691
|
/**
|
|
713
692
|
* A count in a dot at the member's top end corner — the /transactions invoices link's
|
|
714
693
|
* pending count. Nothing is drawn for `0` or less; above 99 it reads `99+`. The count
|
|
@@ -754,7 +733,7 @@ export function FloatingAction({
|
|
|
754
733
|
icon,
|
|
755
734
|
pressed,
|
|
756
735
|
pressedStyle = "tint",
|
|
757
|
-
variant
|
|
736
|
+
variant = "default",
|
|
758
737
|
badge,
|
|
759
738
|
badgeLabel,
|
|
760
739
|
badgeTone = "warning",
|
|
@@ -773,7 +752,6 @@ export function FloatingAction({
|
|
|
773
752
|
const counted = badge !== undefined && badge > 0;
|
|
774
753
|
const name = counted ? `${label}, ${badgeLabel ?? labels.badge(badge)}` : label;
|
|
775
754
|
const isLink = href !== undefined;
|
|
776
|
-
const variant = givenVariant === "primary" ? deprecatedPrimary("FloatingAction variant") : givenVariant;
|
|
777
755
|
const look = cn(
|
|
778
756
|
"relative flex size-12 items-center justify-center transition-colors",
|
|
779
757
|
"outline-none focus-visible:ring-inset",
|
|
@@ -28,9 +28,8 @@ import {
|
|
|
28
28
|
HIT_AREA_LARGE,
|
|
29
29
|
TOUCH_TARGET_LARGE,
|
|
30
30
|
buttonVariantClasses,
|
|
31
|
-
resolveButtonVariant,
|
|
32
31
|
} from "./button-look";
|
|
33
|
-
import type { ButtonVariant
|
|
32
|
+
import type { ButtonVariant } from "./button-look";
|
|
34
33
|
|
|
35
34
|
// Canonical square icon-only button. <Button> carries TEXT geometry — px-3 py-2
|
|
36
35
|
// gap-2 — so rendering a bare glyph through it gives a small icon in a wide,
|
|
@@ -200,7 +199,7 @@ function iconButtonToneClass(tone: IconButtonTone, quiet: boolean | undefined):
|
|
|
200
199
|
// and would out-rank a caller's plain `hover:` override), each variant pins its
|
|
201
200
|
// RESTING look under `disabled:hover:`, which out-ranks any `hover:` by specificity
|
|
202
201
|
// and only ever matches a disabled button.
|
|
203
|
-
const ICON_BUTTON_DISABLED_REST: Record<
|
|
202
|
+
const ICON_BUTTON_DISABLED_REST: Record<ButtonVariant | "overlay" | "shutter", string> = {
|
|
204
203
|
default: "disabled:hover:bg-surface-2",
|
|
205
204
|
secondary: "disabled:hover:bg-transparent",
|
|
206
205
|
ghost: "disabled:hover:bg-transparent",
|
|
@@ -634,7 +633,6 @@ function iconButtonLook({
|
|
|
634
633
|
/** The label is drawn beside the glyph (see `labelVisible`). */
|
|
635
634
|
withText?: boolean;
|
|
636
635
|
}) {
|
|
637
|
-
const look = variant === "overlay" || variant === "shutter" ? variant : resolveButtonVariant(variant, "IconButton variant");
|
|
638
636
|
return cn(
|
|
639
637
|
ICON_BUTTON_BASE,
|
|
640
638
|
withText
|
|
@@ -646,13 +644,13 @@ function iconButtonLook({
|
|
|
646
644
|
withText ? TOUCH_TARGET_LARGE : ICON_BUTTON_TOUCH_LARGE[size],
|
|
647
645
|
glyphSize !== undefined && ICON_BUTTON_GLYPH_SIZES[glyphSize],
|
|
648
646
|
// After the size, so the overlay's `rounded-full` beats the small sizes' `rounded`.
|
|
649
|
-
|
|
647
|
+
variant === "overlay"
|
|
650
648
|
? ICON_BUTTON_OVERLAY
|
|
651
|
-
:
|
|
649
|
+
: variant === "shutter"
|
|
652
650
|
? ICON_BUTTON_SHUTTER
|
|
653
|
-
: buttonVariantClasses[
|
|
651
|
+
: buttonVariantClasses[variant],
|
|
654
652
|
shape === "round" && "rounded-full",
|
|
655
|
-
ICON_BUTTON_DISABLED_REST[
|
|
653
|
+
ICON_BUTTON_DISABLED_REST[variant],
|
|
656
654
|
iconButtonToneClass(tone, quiet),
|
|
657
655
|
toneColor !== undefined && typeof toneColor !== "string" && ICON_BUTTON_TONE_PAIR,
|
|
658
656
|
);
|
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
FieldChevron,
|
|
19
19
|
FieldGroup,
|
|
20
20
|
FloatingField,
|
|
21
|
+
SELECT_MENU_DRESS,
|
|
22
|
+
SELECT_SM,
|
|
21
23
|
fieldBelow,
|
|
22
24
|
} from "./field-base";
|
|
23
25
|
|
|
@@ -88,11 +90,6 @@ export interface SelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement
|
|
|
88
90
|
ref?: Ref<HTMLSelectElement>;
|
|
89
91
|
}
|
|
90
92
|
|
|
91
|
-
// The compact select: the field's colours, a toolbar button's height. `py-0` and a
|
|
92
|
-
// fixed height rather than a smaller padding, so the box is 28px whatever line
|
|
93
|
-
// height the caller's type brings with it.
|
|
94
|
-
const SELECT_SM = "h-7 py-0 ps-2 pe-7 text-xs";
|
|
95
|
-
|
|
96
93
|
/** Keys a locked select still answers: leaving it, and dismissing its tooltip. Every
|
|
97
94
|
* other key would open the list or step the value. */
|
|
98
95
|
const LOCKED_SELECT_KEYS = new Set(["Tab", "Escape", "Shift"]);
|
|
@@ -171,7 +168,7 @@ export function Select(props: SelectProps) {
|
|
|
171
168
|
// moves onto the rows instead (option padding counts toward the row height the
|
|
172
169
|
// browser sizes the box by), and the box keeps no bottom padding: exactly `size`
|
|
173
170
|
// whole rows, at any width.
|
|
174
|
-
const dress = listBox ? "overflow-y-auto [&_option]:py-1" :
|
|
171
|
+
const dress = listBox ? "overflow-y-auto [&_option]:py-1" : SELECT_MENU_DRESS;
|
|
175
172
|
// Locked: focusable, but nothing opens the list or moves the value, and nothing is
|
|
176
173
|
// reported. The no-op `onChange` keeps a controlled select's value where React put
|
|
177
174
|
// it (and keeps React from warning about a `value` with no handler).
|
|
@@ -209,8 +209,15 @@ export interface StatTileProps
|
|
|
209
209
|
* the whole list up to bring one row with it would put them between the headline
|
|
210
210
|
* and its delta. Same shape as a sub-value, so the `tone` rules are the same.
|
|
211
211
|
* Withheld while `loading`, like the sub-values.
|
|
212
|
+
*
|
|
213
|
+
* 0.51: or several, in order (keksdose #633). Its figure tiles carry two forward rows,
|
|
214
|
+
* "Committed <date>" (today plus what is scheduled) and "Projected <date>" (that plus the
|
|
215
|
+
* usual variable money), both of which belong with the headline and neither with the
|
|
216
|
+
* history below. The tile draws what it is given; which rows to give is the app's rule
|
|
217
|
+
* (keksdose shows both on every tile, live #455, even where Committed equals the figure
|
|
218
|
+
* so far).
|
|
212
219
|
*/
|
|
213
|
-
projection?: StatTileSubValue;
|
|
220
|
+
projection?: StatTileSubValue | readonly StatTileSubValue[];
|
|
214
221
|
/**
|
|
215
222
|
* Smaller figures under the headline, label left and figure right.
|
|
216
223
|
*
|
|
@@ -349,6 +356,9 @@ export function StatTile({
|
|
|
349
356
|
const valueId = `${id}-value`;
|
|
350
357
|
const deltaId = `${id}-delta`;
|
|
351
358
|
const priv = sensitive ? "" : undefined;
|
|
359
|
+
// One projection or several (0.51), as a list either way.
|
|
360
|
+
const projections: readonly StatTileSubValue[] =
|
|
361
|
+
projection === undefined ? [] : isFigureList(projection) ? projection : [projection];
|
|
352
362
|
|
|
353
363
|
const fmt =
|
|
354
364
|
format ??
|
|
@@ -543,9 +553,11 @@ export function StatTile({
|
|
|
543
553
|
|
|
544
554
|
{/* At Large the delta and description follow the figure at once, and the
|
|
545
555
|
projection moves down with the other figures (see `subValues`). */}
|
|
546
|
-
{!large && !loading &&
|
|
547
|
-
<dl className="mt-0.5">
|
|
548
|
-
|
|
556
|
+
{!large && !loading && projections.length > 0 && (
|
|
557
|
+
<dl className="mt-0.5 space-y-0.5">
|
|
558
|
+
{projections.map((p, i) => (
|
|
559
|
+
<FigureRow key={i} figure={p} tileTone={tone} show={show} priv={priv} />
|
|
560
|
+
))}
|
|
549
561
|
</dl>
|
|
550
562
|
)}
|
|
551
563
|
|
|
@@ -553,11 +565,12 @@ export function StatTile({
|
|
|
553
565
|
{description != null && <div className="mt-1 text-xs text-muted">{description}</div>}
|
|
554
566
|
{!large && graphic && <div className="mt-2">{graphic}</div>}
|
|
555
567
|
|
|
556
|
-
{!loading && ((large &&
|
|
568
|
+
{!loading && ((large && projections.length > 0) || (subValues && subValues.length > 0)) && (
|
|
557
569
|
<dl data-slot="stat-tile-figures" className={cn("mt-1.5", large ? "space-y-1.5" : "space-y-0.5")}>
|
|
558
|
-
{large &&
|
|
559
|
-
|
|
560
|
-
|
|
570
|
+
{large &&
|
|
571
|
+
projections.map((p, i) => (
|
|
572
|
+
<FigureRow key={`projection-${i}`} figure={p} tileTone={tone} show={show} priv={priv} stacked />
|
|
573
|
+
))}
|
|
561
574
|
{subValues?.map((sv, i) => (
|
|
562
575
|
<FigureRow key={i} figure={sv} tileTone={tone} show={show} priv={priv} stacked={large} />
|
|
563
576
|
))}
|
|
@@ -575,7 +588,12 @@ export function StatTile({
|
|
|
575
588
|
);
|
|
576
589
|
}
|
|
577
590
|
|
|
578
|
-
/**
|
|
591
|
+
/** `Array.isArray` narrows a readonly array to `any[]`; this keeps the element type. */
|
|
592
|
+
function isFigureList(p: StatTileSubValue | readonly StatTileSubValue[]): p is readonly StatTileSubValue[] {
|
|
593
|
+
return Array.isArray(p);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/** One label/value row of a `<dl>`: a sub-value, or a projection. */
|
|
579
597
|
function FigureRow({
|
|
580
598
|
figure,
|
|
581
599
|
tileTone,
|
package/src/i18n/german.ts
CHANGED
|
@@ -2036,6 +2036,8 @@ export function germanLabels(numberLocale: string): UiKitLabels {
|
|
|
2036
2036
|
saving: "Wird gespeichert…",
|
|
2037
2037
|
},
|
|
2038
2038
|
dev: {
|
|
2039
|
+
menu: "Dev-Werkzeuge",
|
|
2040
|
+
overridesOn: "Überschreibungen aktiv",
|
|
2039
2041
|
roleHeading: "Dev · Rolle",
|
|
2040
2042
|
switchPlan: "Tarif wechseln",
|
|
2041
2043
|
plan: (value) => `Tarif: ${value}`,
|
package/src/i18n/kit-labels.tsx
CHANGED
|
@@ -498,8 +498,8 @@ export interface UiKitLabels {
|
|
|
498
498
|
* 403 page, the shared sections, the overview's tiles and the Tools list
|
|
499
499
|
* (docs/admin-harmonization.md §3.1). */
|
|
500
500
|
adminArea: AdminAreaLabels;
|
|
501
|
-
/** 0.42.0: the top bar's shared parts — `TourMenu`, `SyncStatusIndicator`, `
|
|
502
|
-
* (docs/top-bar-harmonization.md §3.1). */
|
|
501
|
+
/** 0.42.0: the top bar's shared parts — `TourMenu`, `SyncStatusIndicator`, `DevMenu`
|
|
502
|
+
* (0.42's `DevSwitchers`; docs/top-bar-harmonization.md §3.1, §2.7.1). */
|
|
503
503
|
topBarParts: TopBarPartsLabels;
|
|
504
504
|
/** 0.45.0: "Send a test mail to me" (docs/admin-harmonization.md §9.2.4). */
|
|
505
505
|
mailTestCard: MailTestCardLabels;
|
package/src/i18n/locales/es.ts
CHANGED
|
@@ -1972,6 +1972,8 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
|
|
|
1972
1972
|
saving: "Guardando…",
|
|
1973
1973
|
},
|
|
1974
1974
|
dev: {
|
|
1975
|
+
menu: "Herramientas de desarrollo",
|
|
1976
|
+
overridesOn: "Anulaciones activas",
|
|
1975
1977
|
roleHeading: "Dev · rol",
|
|
1976
1978
|
switchPlan: "Cambiar de plan",
|
|
1977
1979
|
plan: (value) => `Plan: ${value}`,
|
package/src/i18n/locales/fr.ts
CHANGED
|
@@ -2011,6 +2011,8 @@ export function uiKitLabelsFr(numberLocale = "fr-FR"): UiKitLabels {
|
|
|
2011
2011
|
saving: "Enregistrement…",
|
|
2012
2012
|
},
|
|
2013
2013
|
dev: {
|
|
2014
|
+
menu: "Outils de dev",
|
|
2015
|
+
overridesOn: "Forçages actifs",
|
|
2014
2016
|
roleHeading: "Dev · rôle",
|
|
2015
2017
|
switchPlan: "Changer de formule",
|
|
2016
2018
|
plan: (value) => `Formule\u00a0: ${value}`,
|
package/src/i18n/locales/hu.ts
CHANGED
|
@@ -1976,6 +1976,8 @@ export function uiKitLabelsHu(numberLocale = "hu-HU"): UiKitLabels {
|
|
|
1976
1976
|
saving: "Mentés…",
|
|
1977
1977
|
},
|
|
1978
1978
|
dev: {
|
|
1979
|
+
menu: "Fejlesztői eszközök",
|
|
1980
|
+
overridesOn: "Felülbírálások bekapcsolva",
|
|
1979
1981
|
roleHeading: "Dev · szerepkör",
|
|
1980
1982
|
switchPlan: "Csomag váltása",
|
|
1981
1983
|
plan: (value) => `Csomag: ${value}`,
|
package/src/i18n/locales/it.ts
CHANGED
|
@@ -1991,6 +1991,8 @@ export function uiKitLabelsIt(numberLocale = "it-IT"): UiKitLabels {
|
|
|
1991
1991
|
saving: "Salvataggio…",
|
|
1992
1992
|
},
|
|
1993
1993
|
dev: {
|
|
1994
|
+
menu: "Strumenti di sviluppo",
|
|
1995
|
+
overridesOn: "Override attivi",
|
|
1994
1996
|
roleHeading: "Dev · ruolo",
|
|
1995
1997
|
switchPlan: "Cambia piano",
|
|
1996
1998
|
plan: (value) => `Piano: ${value}`,
|
package/src/i18n/locales/zh.ts
CHANGED
|
@@ -1894,6 +1894,8 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
|
|
|
1894
1894
|
saving: "正在保存…",
|
|
1895
1895
|
},
|
|
1896
1896
|
dev: {
|
|
1897
|
+
menu: "开发工具",
|
|
1898
|
+
overridesOn: "覆盖已开启",
|
|
1897
1899
|
roleHeading: "开发 · 角色",
|
|
1898
1900
|
switchPlan: "切换套餐",
|
|
1899
1901
|
plan: (value) => `套餐:${value}`,
|
package/src/index.ts
CHANGED
|
@@ -1075,7 +1075,10 @@ export type {
|
|
|
1075
1075
|
PublicFooterProps,
|
|
1076
1076
|
} from "./landing/landing-sections";
|
|
1077
1077
|
export { usePageSeo, seoCopyProblems, metaContent } from "./landing/page-seo";
|
|
1078
|
-
export type { SeoCopy, SeoCopyLimits, SeoCopyProblem, SeoCopyProblemCode } from "./landing/page-seo";
|
|
1078
|
+
export type { PageSeo, SeoCopy, SeoCopyLimits, SeoCopyProblem, SeoCopyProblemCode } from "./landing/page-seo";
|
|
1079
|
+
// 0.51.0 (Marcel 2026-10-26): the frame of a public page that is not the landing or a legal page.
|
|
1080
|
+
export { PublicPage } from "./landing/public-page";
|
|
1081
|
+
export type { PublicPageProps } from "./landing/public-page";
|
|
1079
1082
|
|
|
1080
1083
|
// ── demo ─────────────────────────────────────────────────────────────────────
|
|
1081
1084
|
// 0.31.0 (docs/landing-demo-harmonization.md): the demo's start, banner, countdown and end.
|
package/src/landing/page-seo.ts
CHANGED
|
@@ -6,6 +6,19 @@ import { useEffect } from "react";
|
|
|
6
6
|
* and the two pure helpers each app's SEO test is built from.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
/** What {@link usePageSeo} declares for a page. */
|
|
10
|
+
export interface PageSeo {
|
|
11
|
+
canonicalPath: string;
|
|
12
|
+
title?: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
/** Structured data for this page, as the object (not a string). Injected as an
|
|
15
|
+
* `application/ld+json` DATA BLOCK — a script element the browser never executes,
|
|
16
|
+
* which is also why it can be added here rather than in `index.html`: the CSP
|
|
17
|
+
* carries a HASH of the one inline script that file is allowed to have, and
|
|
18
|
+
* `csp-inline-script.test.ts` fails the build on a second one (live #250). */
|
|
19
|
+
jsonLd?: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
|
|
9
22
|
/**
|
|
10
23
|
* Declares the current page's canonical URL — and optionally its title and meta
|
|
11
24
|
* description — while the calling component is mounted (feedback #129).
|
|
@@ -28,23 +41,22 @@ import { useEffect } from "react";
|
|
|
28
41
|
* landing calls it with `canonicalPath: "/"` on both `/` and `/welcome`, so the two URLs
|
|
29
42
|
* of one page are one result; there are no per-language URLs and no hreflang.
|
|
30
43
|
*/
|
|
31
|
-
export function usePageSeo({
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
jsonLd?: Record<string, unknown>;
|
|
46
|
-
}): void {
|
|
44
|
+
export function usePageSeo(seo: PageSeo): void {
|
|
45
|
+
usePageSeoIf(seo);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @internal {@link usePageSeo}, or nothing for `null` — for a component that declares a
|
|
50
|
+
* page only sometimes (`PublicPage`'s `seo`): hooks cannot be called conditionally, so the
|
|
51
|
+
* switch is the argument, as `useNoIndex(active)` has it.
|
|
52
|
+
*/
|
|
53
|
+
export function usePageSeoIf(seo: PageSeo | null): void {
|
|
54
|
+
const canonicalPath = seo?.canonicalPath;
|
|
55
|
+
const title = seo?.title;
|
|
56
|
+
const description = seo?.description;
|
|
57
|
+
const jsonLd = seo?.jsonLd;
|
|
47
58
|
useEffect(() => {
|
|
59
|
+
if (canonicalPath === undefined) return;
|
|
48
60
|
const link = document.createElement("link");
|
|
49
61
|
link.rel = "canonical";
|
|
50
62
|
link.href = new URL(canonicalPath, window.location.origin).toString();
|