@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/src/components/ui.tsx
CHANGED
|
@@ -254,12 +254,15 @@ export function Button({
|
|
|
254
254
|
const ICON_BUTTON_BASE =
|
|
255
255
|
"inline-flex items-center justify-center rounded-md transition-colors focus:outline-none focus:ring-2 disabled:opacity-50 disabled:cursor-not-allowed";
|
|
256
256
|
|
|
257
|
-
export type IconButtonSize = "lg" | "md" | "sm" | "xs" | "2xs";
|
|
257
|
+
export type IconButtonSize = "xl" | "lg" | "md" | "sm" | "xs" | "2xs";
|
|
258
258
|
|
|
259
259
|
// Box and glyph together, so a 24px chip action cannot end up holding a 20px icon
|
|
260
260
|
// that touches its edges. The two small steps are lenkbank's list-row (28px) and
|
|
261
261
|
// chip (24px) actions, which it had hand-rolled beside the kit's 32/36px ones.
|
|
262
262
|
const ICON_BUTTON_SIZES: Record<IconButtonSize, string> = {
|
|
263
|
+
// 48px with a 24px glyph: the one primary action of a phone screen or sheet, a step
|
|
264
|
+
// past the 44px minimum rather than on it.
|
|
265
|
+
xl: "size-12 [&_svg]:size-6",
|
|
263
266
|
// The 44px touch target (WCAG 2.5.5's size) with the same 20px glyph — keksdose's
|
|
264
267
|
// bulk-action bars write `size-11` by hand over an `md` button to get it on phones.
|
|
265
268
|
lg: "size-11 [&_svg]:size-5",
|
|
@@ -269,6 +272,18 @@ const ICON_BUTTON_SIZES: Record<IconButtonSize, string> = {
|
|
|
269
272
|
"2xs": "size-6 rounded [&_svg]:size-3.5",
|
|
270
273
|
};
|
|
271
274
|
|
|
275
|
+
// `stretch`: the same WIDTH and glyph, and a height taken from the row instead of
|
|
276
|
+
// stated — `size-*` sets a height that `self-stretch` cannot override, so the box is
|
|
277
|
+
// written as a width plus a minimum height (the square it would otherwise be).
|
|
278
|
+
const ICON_BUTTON_STRETCH_SIZES: Record<IconButtonSize, string> = {
|
|
279
|
+
xl: "w-12 min-h-12 self-stretch [&_svg]:size-6",
|
|
280
|
+
lg: "w-11 min-h-11 self-stretch [&_svg]:size-5",
|
|
281
|
+
md: "w-9 min-h-9 self-stretch [&_svg]:size-5",
|
|
282
|
+
sm: "w-8 min-h-8 self-stretch [&_svg]:size-5",
|
|
283
|
+
xs: "w-7 min-h-7 self-stretch rounded [&_svg]:size-4",
|
|
284
|
+
"2xs": "w-6 min-h-6 self-stretch rounded [&_svg]:size-3.5",
|
|
285
|
+
};
|
|
286
|
+
|
|
272
287
|
// A tone re-colours the glyph without changing what the variant draws around it.
|
|
273
288
|
// Each coloured tone comes in two resting looks, picked by `quiet` (see the prop):
|
|
274
289
|
// QUIET is placeholder grey until the pointer or focus arrives, then the tone's
|
|
@@ -279,9 +294,14 @@ const ICON_BUTTON_SIZES: Record<IconButtonSize, string> = {
|
|
|
279
294
|
// on the row to notice (keksdose's "needs review" flag, its reconcile action at
|
|
280
295
|
// accounts-page:867), and quiet grey would hide the very thing they point out.
|
|
281
296
|
// `muted` is quiet by definition and has no toned look; `default` has no tone.
|
|
282
|
-
|
|
297
|
+
//
|
|
298
|
+
// `success` is toned at rest too: the green is the news (keksdose's "in sync" chip).
|
|
299
|
+
// `custom` paints from `--icon-button-tone`, which `toneColor` sets — for a glyph whose
|
|
300
|
+
// colour FOLLOWS STATE across tones (keksdose's sync chip: rose, amber, sky, green as
|
|
301
|
+
// the sync moves), where swapping `tone` would do but the colour is one variable.
|
|
302
|
+
export type IconButtonTone = "default" | "muted" | "danger" | "warning" | "info" | "success" | "custom";
|
|
283
303
|
|
|
284
|
-
type ColouredTone = "danger" | "warning" | "info";
|
|
304
|
+
type ColouredTone = "danger" | "warning" | "info" | "success" | "custom";
|
|
285
305
|
|
|
286
306
|
const ICON_BUTTON_TONES: Record<ColouredTone, { quiet: string; toned: string }> = {
|
|
287
307
|
danger: {
|
|
@@ -299,13 +319,33 @@ const ICON_BUTTON_TONES: Record<ColouredTone, { quiet: string; toned: string }>
|
|
|
299
319
|
"text-[var(--text-placeholder)] hover:bg-[var(--info-bg)] hover:text-[var(--info)] focus:ring-[var(--info-border)]",
|
|
300
320
|
toned: "text-[var(--info)] hover:bg-[var(--info-bg)] focus:ring-[var(--info-border)]",
|
|
301
321
|
},
|
|
322
|
+
success: {
|
|
323
|
+
quiet:
|
|
324
|
+
"text-[var(--text-placeholder)] hover:bg-[var(--success-bg)] hover:text-[var(--success)] focus:ring-[var(--success-border)]",
|
|
325
|
+
toned: "text-[var(--success)] hover:bg-[var(--success-bg)] focus:ring-[var(--success-border)]",
|
|
326
|
+
},
|
|
327
|
+
// The hover fill and the ring are MIXED from the one colour, since a custom tone
|
|
328
|
+
// brings no `-bg` / `-border` pair of its own. The fallback is the body colour, so a
|
|
329
|
+
// `custom` tone with no colour set is the default look rather than invisible.
|
|
330
|
+
custom: {
|
|
331
|
+
quiet:
|
|
332
|
+
"text-[var(--text-placeholder)] hover:bg-[color-mix(in_srgb,var(--icon-button-tone,var(--text-primary))_12%,transparent)] hover:text-[var(--icon-button-tone,var(--text-primary))] focus:ring-[color-mix(in_srgb,var(--icon-button-tone,var(--text-primary))_40%,transparent)]",
|
|
333
|
+
toned:
|
|
334
|
+
"text-[var(--icon-button-tone,var(--text-primary))] hover:bg-[color-mix(in_srgb,var(--icon-button-tone,var(--text-primary))_12%,transparent)] focus:ring-[color-mix(in_srgb,var(--icon-button-tone,var(--text-primary))_40%,transparent)]",
|
|
335
|
+
},
|
|
302
336
|
};
|
|
303
337
|
|
|
304
338
|
const ICON_BUTTON_MUTED =
|
|
305
339
|
"text-[var(--text-placeholder)] hover:bg-[var(--bg-surface-2)] hover:text-[var(--text-primary)]";
|
|
306
340
|
|
|
307
341
|
/** Which tones sit quiet at rest when `quiet` is left out. */
|
|
308
|
-
const QUIET_BY_DEFAULT: Record<ColouredTone, boolean> = {
|
|
342
|
+
const QUIET_BY_DEFAULT: Record<ColouredTone, boolean> = {
|
|
343
|
+
danger: true,
|
|
344
|
+
warning: false,
|
|
345
|
+
info: false,
|
|
346
|
+
success: false,
|
|
347
|
+
custom: false,
|
|
348
|
+
};
|
|
309
349
|
|
|
310
350
|
function iconButtonToneClass(tone: IconButtonTone, quiet: boolean | undefined): string {
|
|
311
351
|
if (tone === "default") return "";
|
|
@@ -362,11 +402,31 @@ export interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>
|
|
|
362
402
|
* xs = 28px with a 16px icon (an action in a list row), 2xs = 24px with a 14px
|
|
363
403
|
* icon (an action on a chip or a tab). */
|
|
364
404
|
size?: IconButtonSize;
|
|
405
|
+
/**
|
|
406
|
+
* Fill the height of the flex row it stands in, keeping its width — the delete at
|
|
407
|
+
* the end of keksdose's split line (transaction-fields:162), level with the labelled
|
|
408
|
+
* fields beside it rather than floating at half their height. {@link Button}'s
|
|
409
|
+
* `stretch`; no effect outside a flex row. With a `label`, the tooltip's wrapper is
|
|
410
|
+
* what stretches.
|
|
411
|
+
*/
|
|
412
|
+
stretch?: boolean;
|
|
413
|
+
/** `round`: a circle instead of the rounded square — a compact status chip in a top
|
|
414
|
+
* bar (keksdose's sync indicator). `overlay` is always round. */
|
|
415
|
+
shape?: "square" | "round";
|
|
416
|
+
/**
|
|
417
|
+
* The colour of `tone="custom"`, as any CSS colour — a token (`var(--warning)`),
|
|
418
|
+
* ideally. Sets `--icon-button-tone` on the button, so passing a different one per
|
|
419
|
+
* state re-colours the glyph, its hover fill and its focus ring together. Passing it
|
|
420
|
+
* implies `tone="custom"`. Or leave it out and set the variable yourself from a class
|
|
421
|
+
* (`className="[--icon-button-tone:var(--success)]"`).
|
|
422
|
+
*/
|
|
423
|
+
toneColor?: string;
|
|
365
424
|
/** Glyph colour over the variant. `muted`: placeholder grey, full text colour on
|
|
366
425
|
* hover. `danger`: the same grey at rest, `--danger` on hover and focus — for a
|
|
367
426
|
* remove/delete that repeats down a list. `warning`: amber at rest — a flag that
|
|
368
427
|
* wants attention. `info`: sky at rest — a notice-worthy but harmless action
|
|
369
|
-
* (keksdose's reconcile).
|
|
428
|
+
* (keksdose's reconcile). `success`: green at rest — a good state worth seeing.
|
|
429
|
+
* `custom`: the colour of `toneColor` (see there). Default: the variant's own colours. See `quiet` for
|
|
370
430
|
* turning a coloured tone's resting look the other way. */
|
|
371
431
|
tone?: IconButtonTone;
|
|
372
432
|
/**
|
|
@@ -448,35 +508,73 @@ export interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>
|
|
|
448
508
|
tooltipSide?: TooltipSide;
|
|
449
509
|
/** Passed to the `label` tooltip's `portal`. Left out, Tooltip decides (see there). */
|
|
450
510
|
tooltipPortal?: boolean;
|
|
511
|
+
/**
|
|
512
|
+
* Why the action is not available — {@link Button}'s `disabledReason`, on the icon
|
|
513
|
+
* button: keksdose's accounts page cannot hide an account with a balance or delete
|
|
514
|
+
* one with bookings (accounts-page:879/913 — "hide requires zero", "delete blocked"),
|
|
515
|
+
* nor its budgets page delete the only budget (budgets-page:330), and each wrapped a
|
|
516
|
+
* natively disabled IconButton in a Tooltip that swaps its label for the reason — a
|
|
517
|
+
* bubble a keyboard never opens, since `disabled` leaves the tab order, and a reason
|
|
518
|
+
* a screen reader never hears.
|
|
519
|
+
*
|
|
520
|
+
* The same contract as Button's: `aria-disabled` instead of `disabled`, so it stays
|
|
521
|
+
* focusable and hoverable; clicks (and the Enter/Space they stand for) are swallowed
|
|
522
|
+
* — `stopPropagation` still applies, so a locked action in a clickable row does not
|
|
523
|
+
* open the row either; the reason shows in the kit {@link Tooltip} and is attached
|
|
524
|
+
* through `aria-describedby` as a `hidden` copy. The NAME stays `aria-label` /
|
|
525
|
+
* `label` — "Delete", described by "The active budget cannot be deleted" — and the
|
|
526
|
+
* bubble shows the reason in place of the label, since the glyph already says what
|
|
527
|
+
* the button is and the reason is the news. Shown even with `tooltip={false}`: a
|
|
528
|
+
* reason nobody can see is not one. Wins over `disabled`, as on Button.
|
|
529
|
+
*/
|
|
530
|
+
disabledReason?: ReactNode;
|
|
451
531
|
}
|
|
452
532
|
|
|
453
533
|
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(function IconButton(
|
|
454
534
|
{
|
|
455
535
|
variant = "ghost",
|
|
456
536
|
size = "md",
|
|
457
|
-
tone
|
|
537
|
+
tone: toneProp,
|
|
458
538
|
quiet,
|
|
459
539
|
pressed,
|
|
460
540
|
stopPropagation,
|
|
541
|
+
stretch,
|
|
542
|
+
shape = "square",
|
|
543
|
+
toneColor,
|
|
461
544
|
label,
|
|
462
545
|
tooltip = true,
|
|
463
546
|
tooltipSide,
|
|
464
547
|
tooltipPortal,
|
|
548
|
+
disabledReason,
|
|
465
549
|
className,
|
|
550
|
+
style,
|
|
466
551
|
onClick,
|
|
467
552
|
onKeyDown,
|
|
468
553
|
...rest
|
|
469
554
|
},
|
|
470
555
|
ref,
|
|
471
556
|
) {
|
|
557
|
+
const tone = toneProp ?? (toneColor !== undefined ? "custom" : "default");
|
|
558
|
+
const reasonId = useId();
|
|
559
|
+
const locked = hasContent(disabledReason);
|
|
560
|
+
const ownDescribedBy = rest["aria-describedby"];
|
|
472
561
|
const button = (
|
|
473
562
|
<button
|
|
474
563
|
ref={ref}
|
|
475
564
|
{...rest}
|
|
565
|
+
disabled={locked ? undefined : rest.disabled}
|
|
566
|
+
aria-disabled={locked || rest["aria-disabled"]}
|
|
567
|
+
aria-describedby={locked ? (ownDescribedBy ? `${ownDescribedBy} ${reasonId}` : reasonId) : ownDescribedBy}
|
|
476
568
|
aria-label={rest["aria-label"] ?? label}
|
|
477
569
|
aria-pressed={pressed ?? rest["aria-pressed"]}
|
|
570
|
+
style={toneColor !== undefined ? { ...style, ["--icon-button-tone" as string]: toneColor } : style}
|
|
478
571
|
onClick={(e) => {
|
|
479
572
|
if (stopPropagation) e.stopPropagation();
|
|
573
|
+
// `preventDefault` too: a locked submit must not submit its form.
|
|
574
|
+
if (locked) {
|
|
575
|
+
e.preventDefault();
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
480
578
|
onClick?.(e);
|
|
481
579
|
}}
|
|
482
580
|
onKeyDown={(e) => {
|
|
@@ -487,21 +585,43 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(functio
|
|
|
487
585
|
}}
|
|
488
586
|
className={cn(
|
|
489
587
|
ICON_BUTTON_BASE,
|
|
490
|
-
ICON_BUTTON_SIZES[size],
|
|
588
|
+
stretch ? ICON_BUTTON_STRETCH_SIZES[size] : ICON_BUTTON_SIZES[size],
|
|
491
589
|
// After the size, so the overlay's `rounded-full` beats the small sizes' `rounded`.
|
|
492
590
|
variant === "overlay" ? ICON_BUTTON_OVERLAY : buttonVariantClasses[variant],
|
|
591
|
+
shape === "round" && "rounded-full",
|
|
493
592
|
ICON_BUTTON_DISABLED_REST[variant],
|
|
494
593
|
iconButtonToneClass(tone, quiet),
|
|
495
594
|
pressed && ICON_BUTTON_PRESSED,
|
|
595
|
+
// The disabled look for the focusable kind of disabled, as on Button.
|
|
596
|
+
locked && "cursor-not-allowed opacity-50",
|
|
496
597
|
className,
|
|
497
598
|
)}
|
|
498
599
|
/>
|
|
499
600
|
);
|
|
601
|
+
if (locked) {
|
|
602
|
+
return (
|
|
603
|
+
// Button's shape: the button and the hidden reason in a FRAGMENT, so Tooltip does
|
|
604
|
+
// not add its bubble to the description as well.
|
|
605
|
+
<Tooltip
|
|
606
|
+
label={disabledReason}
|
|
607
|
+
side={tooltipSide}
|
|
608
|
+
portal={tooltipPortal}
|
|
609
|
+
className={stretch ? "self-stretch" : undefined}
|
|
610
|
+
>
|
|
611
|
+
<>
|
|
612
|
+
{button}
|
|
613
|
+
<span id={reasonId} hidden>
|
|
614
|
+
{disabledReason}
|
|
615
|
+
</span>
|
|
616
|
+
</>
|
|
617
|
+
</Tooltip>
|
|
618
|
+
);
|
|
619
|
+
}
|
|
500
620
|
if (!tooltip || label === undefined || label === "") return button;
|
|
501
621
|
return (
|
|
502
622
|
// A fragment, so Tooltip leaves the button's description alone: the bubble says
|
|
503
623
|
// exactly what `aria-label` already does.
|
|
504
|
-
<Tooltip label={label} side={tooltipSide} portal={tooltipPortal}>
|
|
624
|
+
<Tooltip label={label} side={tooltipSide} portal={tooltipPortal} className={stretch ? "self-stretch" : undefined}>
|
|
505
625
|
<>{button}</>
|
|
506
626
|
</Tooltip>
|
|
507
627
|
);
|
|
@@ -1590,6 +1710,16 @@ export interface EmptyStateProps extends Omit<ComponentPropsWithoutRef<"div">, "
|
|
|
1590
1710
|
* verdict.
|
|
1591
1711
|
*/
|
|
1592
1712
|
tone?: "danger" | "success";
|
|
1713
|
+
/**
|
|
1714
|
+
* `inline` only (0.11.0). `sm`: 12px, start-aligned and tight (`py-1`) — the empty
|
|
1715
|
+
* line INSIDE a detail panel, under a panel heading and among `text-xs` rows, where
|
|
1716
|
+
* the default's 14px centred line with `py-4` read as a section of its own.
|
|
1717
|
+
* keksdose's holdings panel (holdings-panel:563), asset-loan panel
|
|
1718
|
+
* (asset-loan-panel:108) and loan-payment panel (loan-payment-panel:191) each wrote
|
|
1719
|
+
* `className="justify-start py-1"` over it and still got body-size text. Default `md`,
|
|
1720
|
+
* the look it had. Ignored by the box.
|
|
1721
|
+
*/
|
|
1722
|
+
size?: "sm" | "md";
|
|
1593
1723
|
}
|
|
1594
1724
|
|
|
1595
1725
|
const EMPTY_STATE_TONE: Record<"danger" | "success", string> = {
|
|
@@ -1605,6 +1735,7 @@ export function EmptyState({
|
|
|
1605
1735
|
headingAs,
|
|
1606
1736
|
variant = "box",
|
|
1607
1737
|
tone,
|
|
1738
|
+
size = "md",
|
|
1608
1739
|
className,
|
|
1609
1740
|
...rest
|
|
1610
1741
|
}: EmptyStateProps) {
|
|
@@ -1612,22 +1743,29 @@ export function EmptyState({
|
|
|
1612
1743
|
const toneClass = tone ? EMPTY_STATE_TONE[tone] : undefined;
|
|
1613
1744
|
const hasHint = hint != null && hint !== false && hint !== "";
|
|
1614
1745
|
if (variant === "inline") {
|
|
1746
|
+
const sm = size === "sm";
|
|
1615
1747
|
return (
|
|
1616
1748
|
<div
|
|
1617
1749
|
{...rest}
|
|
1618
1750
|
className={cn(
|
|
1619
|
-
"flex flex-wrap items-center
|
|
1751
|
+
"flex flex-wrap items-center text-[var(--text-muted)]",
|
|
1752
|
+
sm
|
|
1753
|
+
? "justify-start gap-x-1.5 gap-y-0.5 py-1 text-start text-xs"
|
|
1754
|
+
: "justify-center gap-x-2 gap-y-1 py-4 text-center text-sm",
|
|
1620
1755
|
className,
|
|
1621
1756
|
)}
|
|
1622
1757
|
>
|
|
1623
1758
|
{icon != null && (
|
|
1624
|
-
<span
|
|
1759
|
+
<span
|
|
1760
|
+
aria-hidden
|
|
1761
|
+
className={cn("flex text-[var(--text-placeholder)]", sm ? "[&_svg]:size-3.5" : "[&_svg]:size-4", toneClass)}
|
|
1762
|
+
>
|
|
1625
1763
|
{icon}
|
|
1626
1764
|
</span>
|
|
1627
1765
|
)}
|
|
1628
1766
|
{/* No weight of its own: one muted line is what these sites were, and a
|
|
1629
1767
|
bold title would turn a quiet "nothing here" into a heading. */}
|
|
1630
|
-
<Title className={cn("text-sm", toneClass)}>{title}</Title>
|
|
1768
|
+
<Title className={cn(sm ? "text-xs" : "text-sm", toneClass)}>{title}</Title>
|
|
1631
1769
|
{hasHint && <span className="text-xs">{hint}</span>}
|
|
1632
1770
|
{action != null && <span className="flex flex-wrap items-center gap-2">{action}</span>}
|
|
1633
1771
|
</div>
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import type { ComponentPropsWithoutRef } from "react";
|
|
1
|
+
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
2
2
|
import { cn } from "../lib/cn";
|
|
3
|
+
import { StatusDot } from "./status-dot";
|
|
4
|
+
import type { StatusDotSize, StatusDotTone } from "./status-dot";
|
|
3
5
|
|
|
4
6
|
/** Initials from a display name (first + last) or, failing that, an email —
|
|
5
7
|
* e.g. "Marcel Eifert" → "ME", "marcel@x.com" → "MA". App-agnostic.
|
|
@@ -25,19 +27,71 @@ const AVATAR_SIZES = {
|
|
|
25
27
|
lg: "size-12 text-base",
|
|
26
28
|
} as const;
|
|
27
29
|
|
|
30
|
+
const CHIP =
|
|
31
|
+
"inline-flex shrink-0 items-center justify-center rounded-full bg-[var(--bg-inverse)] font-semibold text-[var(--text-inverse)]";
|
|
32
|
+
|
|
33
|
+
/** The dot grows with the avatar: 8px on `sm`, 10px on `md` (keksdose's), 12px on `lg`. */
|
|
34
|
+
const BADGE_SIZES: Record<keyof typeof AVATAR_SIZES, StatusDotSize> = { sm: "sm", md: "md", lg: "lg" };
|
|
35
|
+
|
|
36
|
+
/** A status dot in the avatar's top corner — an unread count, presence. */
|
|
37
|
+
export interface UserAvatarBadge {
|
|
38
|
+
/** What the dot MEANS, for a screen reader ("3 unread"). Required: a coloured circle
|
|
39
|
+
* says nothing to someone who cannot see it. */
|
|
40
|
+
label: ReactNode;
|
|
41
|
+
/** Default `danger`, the unread dot both apps draw by hand. */
|
|
42
|
+
tone?: StatusDotTone;
|
|
43
|
+
}
|
|
44
|
+
|
|
28
45
|
/** A `<span>`'s props, minus `children`: the content is the initials this computes
|
|
29
46
|
* from `name`/`email`, so there is nothing for a caller to put inside. */
|
|
30
47
|
export interface UserAvatarProps extends Omit<ComponentPropsWithoutRef<"span">, "children"> {
|
|
31
48
|
name?: string | null;
|
|
32
49
|
email?: string | null;
|
|
33
50
|
size?: keyof typeof AVATAR_SIZES;
|
|
51
|
+
/**
|
|
52
|
+
* A status dot in the top-end corner (the top-right in LTR, top-left in RTL), ringed
|
|
53
|
+
* in the surface colour so it reads as its own mark (keksdose account-menu ~385,
|
|
54
|
+
* top-bar :152). Its `label` is read as text where the dot sits — inside a button
|
|
55
|
+
* named by its content it joins that name. In `TopBarActionMenu`'s `trigger` the
|
|
56
|
+
* button is named by the menu's `ariaLabel` followed by this label: "Account menu
|
|
57
|
+
* 3 unread". The initials stay hidden either way, so the person's name is NOT part
|
|
58
|
+
* of it — put it in `ariaLabel` ("Marcel Eifert, account menu") if it should be.
|
|
59
|
+
*/
|
|
60
|
+
badge?: UserAvatarBadge | null;
|
|
34
61
|
}
|
|
35
62
|
|
|
36
63
|
/**
|
|
37
64
|
* A round initials avatar — the common user chip shared across apps (feedback
|
|
38
65
|
* #333). Purely presentational; wrap it in a button for the account menu trigger.
|
|
39
66
|
*/
|
|
40
|
-
export function UserAvatar({ name, email, size = "md", className, ...rest }: UserAvatarProps) {
|
|
67
|
+
export function UserAvatar({ name, email, size = "md", badge, className, ...rest }: UserAvatarProps) {
|
|
68
|
+
if (badge) {
|
|
69
|
+
// A wrapper, because the chip itself is `aria-hidden` and the badge label must not
|
|
70
|
+
// be. `className` stays on the chip (a size override still sizes the circle); the
|
|
71
|
+
// other attributes go on the wrapper, which is what a caller addresses. `relative`
|
|
72
|
+
// also contains the sr-only text.
|
|
73
|
+
return (
|
|
74
|
+
<span {...rest} className="relative inline-flex shrink-0">
|
|
75
|
+
<span
|
|
76
|
+
aria-hidden
|
|
77
|
+
className={cn(
|
|
78
|
+
CHIP,
|
|
79
|
+
AVATAR_SIZES[size],
|
|
80
|
+
className,
|
|
81
|
+
)}
|
|
82
|
+
>
|
|
83
|
+
{avatarInitials(name, email)}
|
|
84
|
+
</span>
|
|
85
|
+
<StatusDot
|
|
86
|
+
ring
|
|
87
|
+
tone={badge.tone ?? "danger"}
|
|
88
|
+
size={BADGE_SIZES[size]}
|
|
89
|
+
className="absolute -end-0.5 -top-0.5"
|
|
90
|
+
/>
|
|
91
|
+
<span className="sr-only">{badge.label}</span>
|
|
92
|
+
</span>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
41
95
|
return (
|
|
42
96
|
<span
|
|
43
97
|
// `aria-hidden` sits BEFORE the spread, unlike the structural attributes on the
|
|
@@ -50,7 +104,7 @@ export function UserAvatar({ name, email, size = "md", className, ...rest }: Use
|
|
|
50
104
|
aria-hidden
|
|
51
105
|
{...rest}
|
|
52
106
|
className={cn(
|
|
53
|
-
|
|
107
|
+
CHIP,
|
|
54
108
|
AVATAR_SIZES[size],
|
|
55
109
|
className,
|
|
56
110
|
)}
|
package/src/i18n/defaults.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DEFAULT_DATA_TABLE_LABELS } from "../components/data-table-labels";
|
|
2
2
|
import { DEFAULT_MINI_CALENDAR_LABELS } from "../components/mini-calendar";
|
|
3
|
+
import { DEFAULT_CALENDAR_HEATMAP_LABELS } from "../components/calendar-heatmap";
|
|
3
4
|
import { DEFAULT_MONTH_PICKER_LABELS } from "../components/month-picker";
|
|
4
5
|
import { DEFAULT_PAGE_CONTENTS_LABELS } from "../components/page-contents";
|
|
5
6
|
import { DEFAULT_POPOVER_LABELS } from "../components/popover";
|
|
@@ -56,6 +57,7 @@ export const DEFAULT_UI_KIT_LABELS: UiKitLabels = {
|
|
|
56
57
|
common: DEFAULT_COMMON_LABELS,
|
|
57
58
|
dataTable: DEFAULT_DATA_TABLE_LABELS,
|
|
58
59
|
miniCalendar: DEFAULT_MINI_CALENDAR_LABELS,
|
|
60
|
+
calendarHeatmap: DEFAULT_CALENDAR_HEATMAP_LABELS,
|
|
59
61
|
datePicker: DEFAULT_DATE_PICKER_LABELS,
|
|
60
62
|
monthPicker: DEFAULT_MONTH_PICKER_LABELS,
|
|
61
63
|
popover: DEFAULT_POPOVER_LABELS,
|
package/src/i18n/kit-labels.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { createContext, useContext, useMemo } from "react";
|
|
|
2
2
|
import type { ReactNode } from "react";
|
|
3
3
|
import type { DataTableLabels } from "../components/data-table-labels";
|
|
4
4
|
import type { MiniCalendarLabels, WeekDay } from "../components/mini-calendar";
|
|
5
|
+
import type { CalendarHeatmapLabels } from "../components/calendar-heatmap";
|
|
5
6
|
import type { PopoverLabels } from "../components/popover";
|
|
6
7
|
import type { ChipInputLabels } from "../components/chip";
|
|
7
8
|
import type { FieldSyncLabels } from "../components/field-sync";
|
|
@@ -197,6 +198,7 @@ export interface UiKitLabels {
|
|
|
197
198
|
common: CommonLabels;
|
|
198
199
|
dataTable: DataTableLabels;
|
|
199
200
|
miniCalendar: MiniCalendarLabels;
|
|
201
|
+
calendarHeatmap: CalendarHeatmapLabels;
|
|
200
202
|
datePicker: DatePickerLabels;
|
|
201
203
|
monthPicker: MonthPickerLabels;
|
|
202
204
|
popover: PopoverLabels;
|
package/src/i18n/locales/de.ts
CHANGED
|
@@ -118,6 +118,14 @@ export function uiKitLabelsDe(numberLocale = "de-DE"): UiKitLabels {
|
|
|
118
118
|
rangeSelected: (from, to) =>
|
|
119
119
|
`Zeitraum ${from} bis ${to} gewählt. Wählen Sie ein Startdatum, um neu zu beginnen.`,
|
|
120
120
|
},
|
|
121
|
+
calendarHeatmap: {
|
|
122
|
+
grid: "Tageswerte",
|
|
123
|
+
day: (date, value) => `${date}: ${value}`,
|
|
124
|
+
less: "Weniger",
|
|
125
|
+
more: "Mehr",
|
|
126
|
+
truncated: (count) =>
|
|
127
|
+
`Die neuesten Tage werden angezeigt; ${n(count)} ${count === 1 ? "früherer Tag wird" : "frühere Tage werden"} nicht angezeigt.`,
|
|
128
|
+
},
|
|
121
129
|
datePicker: {
|
|
122
130
|
apply: "Übernehmen",
|
|
123
131
|
cancel: "Abbrechen",
|
|
@@ -392,6 +400,7 @@ export function uiKitLabelsDe(numberLocale = "de-DE"): UiKitLabels {
|
|
|
392
400
|
},
|
|
393
401
|
floatingPanel: {
|
|
394
402
|
close: "Schließen",
|
|
403
|
+
badge: (count) => `${n(count)} neu`,
|
|
395
404
|
},
|
|
396
405
|
copyButton: {
|
|
397
406
|
copy: "Kopieren",
|
package/src/i18n/locales/es.ts
CHANGED
|
@@ -125,6 +125,14 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
|
|
|
125
125
|
panel: "Elegir un mes",
|
|
126
126
|
month: (monthYear) => monthYear,
|
|
127
127
|
},
|
|
128
|
+
calendarHeatmap: {
|
|
129
|
+
grid: "Valores diarios",
|
|
130
|
+
day: (date, value) => `${date}: ${value}`,
|
|
131
|
+
less: "Menos",
|
|
132
|
+
more: "Más",
|
|
133
|
+
truncated: (count) =>
|
|
134
|
+
`Se muestran los días más recientes; ${count === 1 ? "no se muestra 1 día anterior" : `no se muestran ${n(count)} días anteriores`}.`,
|
|
135
|
+
},
|
|
128
136
|
datePicker: {
|
|
129
137
|
apply: "Aplicar",
|
|
130
138
|
cancel: "Cancelar",
|
|
@@ -385,6 +393,7 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
|
|
|
385
393
|
},
|
|
386
394
|
floatingPanel: {
|
|
387
395
|
close: "Cerrar",
|
|
396
|
+
badge: (count) => `${n(count)} ${plural(count, "nuevo", "nuevos")}`,
|
|
388
397
|
},
|
|
389
398
|
copyButton: {
|
|
390
399
|
copy: "Copiar",
|
package/src/i18n/locales/fr.ts
CHANGED
|
@@ -122,6 +122,14 @@ export function uiKitLabelsFr(numberLocale = "fr-FR"): UiKitLabels {
|
|
|
122
122
|
rangeSelected: (from, to) =>
|
|
123
123
|
`Période du ${from} au ${to} sélectionnée. Choisissez une date de début pour recommencer.`,
|
|
124
124
|
},
|
|
125
|
+
calendarHeatmap: {
|
|
126
|
+
grid: "Valeurs quotidiennes",
|
|
127
|
+
day: (date, value) => `${date} : ${value}`,
|
|
128
|
+
less: "Moins",
|
|
129
|
+
more: "Plus",
|
|
130
|
+
truncated: (count) =>
|
|
131
|
+
`Affichage des jours les plus récents ; ${n(count)} ${count === 1 ? "jour antérieur n’est pas affiché" : "jours antérieurs ne sont pas affichés"}.`,
|
|
132
|
+
},
|
|
125
133
|
datePicker: {
|
|
126
134
|
apply: "Appliquer",
|
|
127
135
|
cancel: "Annuler",
|
|
@@ -398,6 +406,7 @@ export function uiKitLabelsFr(numberLocale = "fr-FR"): UiKitLabels {
|
|
|
398
406
|
},
|
|
399
407
|
floatingPanel: {
|
|
400
408
|
close: "Fermer",
|
|
409
|
+
badge: (count) => `${n(count)} ${plural(count, "nouveau", "nouveaux")}`,
|
|
401
410
|
},
|
|
402
411
|
copyButton: {
|
|
403
412
|
copy: "Copier",
|
package/src/i18n/locales/hu.ts
CHANGED
|
@@ -121,6 +121,14 @@ export function uiKitLabelsHu(numberLocale = "hu-HU"): UiKitLabels {
|
|
|
121
121
|
panel: "Hónap kiválasztása",
|
|
122
122
|
month: (monthYear) => monthYear,
|
|
123
123
|
},
|
|
124
|
+
calendarHeatmap: {
|
|
125
|
+
grid: "Napi értékek",
|
|
126
|
+
day: (date, value) => `${date}: ${value}`,
|
|
127
|
+
less: "Kevesebb",
|
|
128
|
+
more: "Több",
|
|
129
|
+
truncated: (count) =>
|
|
130
|
+
`A legutóbbi napok láthatók; ${n(count)} korábbi nap nem látható.`,
|
|
131
|
+
},
|
|
124
132
|
datePicker: {
|
|
125
133
|
apply: "Alkalmaz",
|
|
126
134
|
cancel: "Mégse",
|
|
@@ -386,6 +394,7 @@ export function uiKitLabelsHu(numberLocale = "hu-HU"): UiKitLabels {
|
|
|
386
394
|
},
|
|
387
395
|
floatingPanel: {
|
|
388
396
|
close: "Bezárás",
|
|
397
|
+
badge: (count) => `${n(count)} új`,
|
|
389
398
|
},
|
|
390
399
|
copyButton: {
|
|
391
400
|
copy: "Másolás",
|
package/src/i18n/locales/it.ts
CHANGED
|
@@ -125,6 +125,14 @@ export function uiKitLabelsIt(numberLocale = "it-IT"): UiKitLabels {
|
|
|
125
125
|
panel: "Scegli un mese",
|
|
126
126
|
month: (monthYear) => monthYear,
|
|
127
127
|
},
|
|
128
|
+
calendarHeatmap: {
|
|
129
|
+
grid: "Valori giornalieri",
|
|
130
|
+
day: (date, value) => `${date}: ${value}`,
|
|
131
|
+
less: "Meno",
|
|
132
|
+
more: "Più",
|
|
133
|
+
truncated: (count) =>
|
|
134
|
+
`Vengono mostrati i giorni più recenti; ${count === 1 ? "1 giorno precedente non è mostrato" : `${n(count)} giorni precedenti non sono mostrati`}.`,
|
|
135
|
+
},
|
|
128
136
|
datePicker: {
|
|
129
137
|
apply: "Applica",
|
|
130
138
|
cancel: "Annulla",
|
|
@@ -387,6 +395,7 @@ export function uiKitLabelsIt(numberLocale = "it-IT"): UiKitLabels {
|
|
|
387
395
|
},
|
|
388
396
|
floatingPanel: {
|
|
389
397
|
close: "Chiudi",
|
|
398
|
+
badge: (count) => `${n(count)} ${plural(count, "nuovo", "nuovi")}`,
|
|
390
399
|
},
|
|
391
400
|
copyButton: {
|
|
392
401
|
copy: "Copia",
|
package/src/i18n/locales/zh.ts
CHANGED
|
@@ -120,6 +120,14 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
|
|
|
120
120
|
panel: "选择月份",
|
|
121
121
|
month: (monthYear) => monthYear,
|
|
122
122
|
},
|
|
123
|
+
calendarHeatmap: {
|
|
124
|
+
grid: "每日数值",
|
|
125
|
+
day: (date, value) => `${date}:${value}`,
|
|
126
|
+
less: "较少",
|
|
127
|
+
more: "较多",
|
|
128
|
+
truncated: (count) =>
|
|
129
|
+
`仅显示最近的日期;另有 ${n(count)} 天未显示。`,
|
|
130
|
+
},
|
|
123
131
|
datePicker: {
|
|
124
132
|
apply: "应用",
|
|
125
133
|
cancel: "取消",
|
|
@@ -375,6 +383,7 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
|
|
|
375
383
|
},
|
|
376
384
|
floatingPanel: {
|
|
377
385
|
close: "关闭",
|
|
386
|
+
badge: (count) => `${n(count)} 条新内容`,
|
|
378
387
|
},
|
|
379
388
|
copyButton: {
|
|
380
389
|
copy: "复制",
|
package/src/index.ts
CHANGED
|
@@ -56,6 +56,8 @@ export * from "./theme/palette-store";
|
|
|
56
56
|
|
|
57
57
|
// ── components ───────────────────────────────────────────────────────────────
|
|
58
58
|
export * from "./components/ui";
|
|
59
|
+
// The label-ABOVE field group (label, hint, error, required), inside wizards or out.
|
|
60
|
+
export * from "./components/field";
|
|
59
61
|
export * from "./components/search-field";
|
|
60
62
|
export * from "./components/dropdown";
|
|
61
63
|
export * from "./components/popover";
|
|
@@ -126,6 +128,8 @@ export * from "./components/series-chart-labels";
|
|
|
126
128
|
export * from "./components/account-settings";
|
|
127
129
|
export * from "./components/alert-banner";
|
|
128
130
|
export * from "./components/toggle-group";
|
|
131
|
+
// A wrapping set of links/buttons marked `aria-current` — Tabs-like looks, not tabs.
|
|
132
|
+
export * from "./components/nav-pills";
|
|
129
133
|
// A pill carrying one VALUE — inert, a link, or a toggle, depending on which prop it is
|
|
130
134
|
// given — and the list field built from it. Distinct from Button on purpose: a row of
|
|
131
135
|
// buttons reads as "choose an action", a row of chips as "here are the things".
|
|
@@ -137,6 +141,7 @@ export * from "./components/full-bleed-dialog";
|
|
|
137
141
|
export * from "./components/grouped-picker";
|
|
138
142
|
export * from "./components/file-dropzone";
|
|
139
143
|
export * from "./components/mini-calendar";
|
|
144
|
+
export * from "./components/calendar-heatmap";
|
|
140
145
|
export * from "./components/date-picker";
|
|
141
146
|
export * from "./components/tree-view";
|
|
142
147
|
export * from "./components/chart";
|
package/src/lib/clipping.ts
CHANGED
|
@@ -1,14 +1,32 @@
|
|
|
1
|
-
/** Internal (not exported from the package): the Tooltip's auto-portal check.
|
|
1
|
+
/** Internal (not exported from the package): the Tooltip's auto-portal check. The
|
|
2
|
+
* attribute name itself IS exported, as `CLIPS_ATTRIBUTE` from the tooltip module. */
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The explicit "this element clips or scrolls" marker. A container carrying it counts
|
|
6
|
+
* as clipping whatever its computed `overflow` says.
|
|
7
|
+
*
|
|
8
|
+
* Why a marker as well as the computed style: the kit's containers get their
|
|
9
|
+
* `overflow` from Tailwind classes, and jsdom computes no stylesheet — so under test
|
|
10
|
+
* every scroller reads `overflow: visible`, the Tooltip stays in place, and its always-
|
|
11
|
+
* mounted bubble shows up in the cell's text (keksdose 0.10: "CheckingChecking"). The
|
|
12
|
+
* browser and the test then disagreed about where the bubble lives, and apps pinned
|
|
13
|
+
* `portal` on every tooltip in a table to make them agree. The kit's own scrollers
|
|
14
|
+
* (DataTable's body, Table's wrapper) carry the marker, so the answer is the same in
|
|
15
|
+
* both; an app marks its own with `{ [CLIPS_ATTRIBUTE]: "" }` or a literal `data-clips`.
|
|
16
|
+
*/
|
|
17
|
+
export const CLIPS_ATTRIBUTE = "data-clips";
|
|
2
18
|
|
|
3
19
|
/** Whether anything between `el` and `<body>` clips or scrolls its content — the
|
|
4
20
|
* containers an absolutely positioned bubble would be cut off by, or would widen.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
21
|
+
* Either the computed `overflow` says so or the element carries
|
|
22
|
+
* {@link CLIPS_ATTRIBUTE}. `<body>` and `<html>` are not counted: their `overflow`
|
|
23
|
+
* belongs to the viewport (a dialog's scroll lock sets it), and the viewport clips a
|
|
24
|
+
* portalled bubble just the same. */
|
|
8
25
|
export function hasClippingAncestor(el: Element | null): boolean {
|
|
9
26
|
if (typeof window === "undefined" || !el) return false;
|
|
10
27
|
for (let node = el.parentElement; node; node = node.parentElement) {
|
|
11
28
|
if (node === document.body || node === document.documentElement) return false;
|
|
29
|
+
if (node.hasAttribute(CLIPS_ATTRIBUTE)) return true;
|
|
12
30
|
const style = window.getComputedStyle(node);
|
|
13
31
|
if (style.overflowX !== "visible" || style.overflowY !== "visible") return true;
|
|
14
32
|
}
|