@eifi1/ui-kit 0.46.1 → 0.48.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 +8 -6
- package/dist/admin/admin-sections.d.ts +8 -0
- package/dist/admin/admin-sections.js +2 -1
- package/dist/admin/admin-sections.js.map +1 -1
- package/dist/auth/verify-email.d.ts +9 -1
- package/dist/auth/verify-email.js +22 -13
- package/dist/auth/verify-email.js.map +1 -1
- package/dist/components/alert-banner.d.ts +29 -1
- package/dist/components/alert-banner.js +84 -7
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/button-look.d.ts +4 -0
- package/dist/components/button-look.js +2 -0
- package/dist/components/button-look.js.map +1 -1
- package/dist/components/combobox-core.js +5 -1
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/data-table-column-settings.d.ts +6 -1
- package/dist/components/data-table-column-settings.js +12 -2
- package/dist/components/data-table-column-settings.js.map +1 -1
- package/dist/components/data-table-fit.d.ts +44 -0
- package/dist/components/data-table-fit.js +103 -0
- package/dist/components/data-table-fit.js.map +1 -0
- package/dist/components/data-table-groups.d.ts +95 -0
- package/dist/components/data-table-groups.js +165 -0
- package/dist/components/data-table-groups.js.map +1 -0
- package/dist/components/data-table-head.js +3 -1
- package/dist/components/data-table-head.js.map +1 -1
- package/dist/components/data-table-labels.d.ts +7 -0
- package/dist/components/data-table-labels.js +3 -0
- package/dist/components/data-table-labels.js.map +1 -1
- package/dist/components/data-table-leading-row.d.ts +17 -0
- package/dist/components/data-table-leading-row.js +41 -0
- package/dist/components/data-table-leading-row.js.map +1 -0
- package/dist/components/data-table-resize.js +1 -1
- package/dist/components/data-table-resize.js.map +1 -1
- package/dist/components/data-table.d.ts +78 -2
- package/dist/components/data-table.js +355 -247
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/disclosure.d.ts +3 -1
- package/dist/components/disclosure.js +10 -4
- package/dist/components/disclosure.js.map +1 -1
- package/dist/components/dropdown.js +8 -1
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/floating-panel.d.ts +14 -1
- package/dist/components/floating-panel.js +7 -2
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/hover-menu.js +5 -1
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/icon-button.js +2 -1
- package/dist/components/icon-button.js.map +1 -1
- package/dist/components/popover.js +5 -1
- package/dist/components/popover.js.map +1 -1
- package/dist/components/row-actions.d.ts +25 -0
- package/dist/components/row-actions.js +13 -3
- package/dist/components/row-actions.js.map +1 -1
- package/dist/components/table-skeleton.d.ts +35 -0
- package/dist/components/table-skeleton.js +75 -0
- package/dist/components/table-skeleton.js.map +1 -0
- package/dist/components/tabs.d.ts +22 -16
- package/dist/components/tabs.js +47 -31
- package/dist/components/tabs.js.map +1 -1
- package/dist/components/ui-labels.js +1 -1
- package/dist/components/ui-labels.js.map +1 -1
- package/dist/components/use-table-state.d.ts +8 -7
- package/dist/components/use-table-state.js +37 -3
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/data-table.d.ts +1 -0
- package/dist/data-table.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/feedback/feedback-attachment-download.d.ts +19 -0
- package/dist/feedback/feedback-attachment-download.js +59 -0
- package/dist/feedback/feedback-attachment-download.js.map +1 -0
- package/dist/feedback/feedback-comment-thread.d.ts +17 -6
- package/dist/feedback/feedback-comment-thread.js +21 -2
- package/dist/feedback/feedback-comment-thread.js.map +1 -1
- package/dist/feedback/feedback-entry-file.d.ts +61 -0
- package/dist/feedback/feedback-entry-file.js +76 -0
- package/dist/feedback/feedback-entry-file.js.map +1 -0
- package/dist/feedback/feedback-public-list.d.ts +18 -5
- package/dist/feedback/feedback-public-list.js +8 -0
- package/dist/feedback/feedback-public-list.js.map +1 -1
- package/dist/feedback/feedback-public-types.d.ts +7 -2
- package/dist/feedback/feedback-replies.d.ts +26 -9
- package/dist/feedback/feedback-replies.js +20 -10
- package/dist/feedback/feedback-replies.js.map +1 -1
- package/dist/feedback/feedback-row-detail-labels.js +0 -1
- package/dist/feedback/feedback-row-detail-labels.js.map +1 -1
- package/dist/feedback/feedback-row-detail.d.ts +23 -10
- package/dist/feedback/feedback-row-detail.js +10 -54
- package/dist/feedback/feedback-row-detail.js.map +1 -1
- package/dist/feedback/feedback-table.d.ts +5 -0
- package/dist/feedback/feedback-table.js +30 -7
- package/dist/feedback/feedback-table.js.map +1 -1
- package/dist/feedback.d.ts +1 -0
- package/dist/feedback.js +1 -0
- package/dist/feedback.js.map +1 -1
- package/dist/hooks/use-dismiss.d.ts +30 -1
- package/dist/hooks/use-dismiss.js +30 -2
- package/dist/hooks/use-dismiss.js.map +1 -1
- package/dist/i18n/german.js +7 -7
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +4 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/en.js +7 -7
- package/dist/i18n/locales/en.js.map +2 -2
- package/dist/i18n/locales/es.js +7 -7
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +7 -7
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +7 -7
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +7 -7
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +7 -7
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.js +0 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/i18n/shared-labels.js +3 -1
- package/dist/i18n/shared-labels.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +13 -4
- package/dist/index.js.map +1 -1
- package/dist/lib/float-clearance.d.ts +8 -0
- package/dist/lib/float-clearance.js +29 -0
- package/dist/lib/float-clearance.js.map +1 -0
- package/dist/notifications/device-zone-report.d.ts +1 -1
- package/dist/notifications/device-zone-report.js.map +1 -1
- package/dist/notifications/notification-types.d.ts +0 -3
- package/dist/notifications/notifications-labels.d.ts +2 -15
- package/dist/notifications/notifications-labels.js +0 -4
- package/dist/notifications/notifications-labels.js.map +1 -1
- package/dist/notifications/quiet-hours-setting.d.ts +3 -8
- package/dist/notifications/quiet-hours-setting.js +8 -3
- package/dist/notifications/quiet-hours-setting.js.map +1 -1
- package/dist/settings/settings-catalogue.d.ts +10 -0
- package/dist/settings/settings-catalogue.js.map +1 -1
- package/dist/settings/settings-layout.js +10 -3
- package/dist/settings/settings-layout.js.map +1 -1
- package/dist/shell/app-shell.d.ts +5 -0
- package/dist/shell/app-shell.js +5 -3
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/option-switcher-menu.d.ts +2 -0
- package/dist/shell/option-switcher-menu.js +6 -2
- package/dist/shell/option-switcher-menu.js.map +1 -1
- package/dist/shell/topbar-action-menu.js +2 -1
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell/topbar-controls.js +5 -3
- package/dist/shell/topbar-controls.js.map +1 -1
- package/dist/theme/theme-store.d.ts +4 -4
- package/dist/theme/theme-store.js +1 -0
- package/dist/theme/theme-store.js.map +1 -1
- package/dist/tour/tour.js +1 -0
- package/dist/tour/tour.js.map +1 -1
- package/package.json +2 -2
- package/src/admin/admin-sections.tsx +8 -0
- package/src/auth/verify-email.tsx +34 -9
- package/src/components/alert-banner.tsx +131 -7
- package/src/components/button-look.ts +6 -0
- package/src/components/combobox-core.tsx +7 -1
- package/src/components/data-table-column-settings.tsx +23 -2
- package/src/components/data-table-fit.ts +187 -0
- package/src/components/data-table-groups.tsx +275 -0
- package/src/components/data-table-head.tsx +4 -1
- package/src/components/data-table-labels.ts +13 -0
- package/src/components/data-table-leading-row.tsx +55 -0
- package/src/components/data-table-resize.ts +2 -1
- package/src/components/data-table.tsx +274 -45
- package/src/components/disclosure.tsx +22 -5
- package/src/components/dropdown.tsx +11 -1
- package/src/components/floating-panel.tsx +24 -1
- package/src/components/hover-menu.tsx +6 -1
- package/src/components/icon-button.tsx +3 -3
- package/src/components/popover.tsx +7 -1
- package/src/components/row-actions.tsx +42 -3
- package/src/components/table-skeleton.tsx +133 -0
- package/src/components/tabs.tsx +102 -50
- package/src/components/ui-labels.ts +1 -1
- package/src/components/use-table-state.ts +54 -3
- package/src/data-table.ts +2 -0
- package/src/feedback/feedback-attachment-download.tsx +77 -0
- package/src/feedback/feedback-comment-thread.tsx +42 -7
- package/src/feedback/feedback-entry-file.tsx +150 -0
- package/src/feedback/feedback-public-list.tsx +25 -4
- package/src/feedback/feedback-public-types.ts +7 -2
- package/src/feedback/feedback-replies.tsx +46 -19
- package/src/feedback/feedback-row-detail-labels.ts +0 -1
- package/src/feedback/feedback-row-detail.tsx +35 -86
- package/src/feedback/feedback-table.tsx +33 -1
- package/src/feedback.ts +1 -0
- package/src/hooks/use-dismiss.ts +65 -3
- package/src/i18n/german.ts +6 -6
- package/src/i18n/kit-labels.tsx +4 -0
- package/src/i18n/locales/es.ts +6 -6
- package/src/i18n/locales/fr.ts +6 -6
- package/src/i18n/locales/hu.ts +6 -6
- package/src/i18n/locales/it.ts +6 -6
- package/src/i18n/locales/zh.ts +6 -6
- package/src/i18n/review.ts +0 -1
- package/src/i18n/shared-labels.ts +2 -0
- package/src/index.ts +13 -3
- package/src/lib/float-clearance.ts +54 -0
- package/src/notifications/device-zone-report.ts +1 -1
- package/src/notifications/notification-types.ts +0 -3
- package/src/notifications/notifications-labels.ts +2 -19
- package/src/notifications/quiet-hours-setting.tsx +16 -11
- package/src/settings/settings-catalogue.ts +10 -0
- package/src/settings/settings-layout.tsx +18 -4
- package/src/shell/app-shell.tsx +17 -3
- package/src/shell/option-switcher-menu.tsx +16 -4
- package/src/shell/topbar-action-menu.tsx +22 -10
- package/src/shell/topbar-controls.tsx +18 -12
- package/src/theme/theme-store.ts +13 -5
- package/src/tour/tour.tsx +5 -0
- package/dist/notifications/time-zone-select.d.ts +0 -54
- package/dist/notifications/time-zone-select.js +0 -71
- package/dist/notifications/time-zone-select.js.map +0 -1
- package/src/notifications/time-zone-select.tsx +0 -125
|
@@ -68,6 +68,13 @@ export interface AdminSectionsOptions<L extends AdminSharedSection = AdminShared
|
|
|
68
68
|
* whose path then acts as an unknown one.
|
|
69
69
|
*/
|
|
70
70
|
visible?: Partial<Record<AdminSharedSection, boolean>>;
|
|
71
|
+
/**
|
|
72
|
+
* 0.48 (keksdose live #450): a shared section's {@link SettingsGroup.badge} — the
|
|
73
|
+
* waiting work the account menu's badge counts, on the row of the section that holds
|
|
74
|
+
* it: `{ overview: <AdminWaitingBadge count={waiting} /> }`. An app's own section
|
|
75
|
+
* carries its `badge` itself.
|
|
76
|
+
*/
|
|
77
|
+
badges?: Partial<Record<AdminSharedSection, ReactNode>>;
|
|
71
78
|
}
|
|
72
79
|
|
|
73
80
|
/** The shared sections an app keeps when it leaves out `L`. A `leaveOut` typed as the
|
|
@@ -193,6 +200,7 @@ export function adminSections<G extends string = never, L extends AdminSharedSec
|
|
|
193
200
|
// with no entry of its own.
|
|
194
201
|
...(id === "overview" ? { standalone: true } : null),
|
|
195
202
|
...(visible === undefined ? null : { visible }),
|
|
203
|
+
...(options.badges?.[id] == null ? null : { badge: options.badges[id] }),
|
|
196
204
|
};
|
|
197
205
|
});
|
|
198
206
|
}
|
|
@@ -358,6 +358,14 @@ export interface EmailVerificationBannerProps {
|
|
|
358
358
|
children?: ReactNode;
|
|
359
359
|
className?: string;
|
|
360
360
|
labels?: LabelOverride<VerifyEmailLabels>;
|
|
361
|
+
/**
|
|
362
|
+
* 0.48: on a phone hold the message to one line with a More / Less toggle —
|
|
363
|
+
* {@link AlertBanner}'s `clampMobile`, passed through (kastlan S2: its banner took ~110px
|
|
364
|
+
* of a 390px phone). The resend's outcome ("Sent", the error) then moves out of the
|
|
365
|
+
* message to sit after "Send again", at every width, so the line that answers the press
|
|
366
|
+
* is never hidden behind "More". Default `false`: the outcome stays in the message.
|
|
367
|
+
*/
|
|
368
|
+
clampMobile?: boolean;
|
|
361
369
|
}
|
|
362
370
|
|
|
363
371
|
/**
|
|
@@ -379,28 +387,45 @@ export function EmailVerificationBanner({
|
|
|
379
387
|
children,
|
|
380
388
|
className,
|
|
381
389
|
labels: labelsProp,
|
|
390
|
+
clampMobile = false,
|
|
382
391
|
}: EmailVerificationBannerProps) {
|
|
383
392
|
const labels = useKitLabels("verifyEmail", DEFAULT_VERIFY_EMAIL_LABELS, labelsProp);
|
|
384
393
|
const resend = useResend(onResend, cooldown);
|
|
394
|
+
// One live region either way, mounted from the start (a region added later is often not
|
|
395
|
+
// read out): in the message, or — clamped — after the button, outside the clamp.
|
|
396
|
+
const status = (
|
|
397
|
+
<ResendStatus
|
|
398
|
+
phase={resend.phase}
|
|
399
|
+
error={resend.error}
|
|
400
|
+
describeError={describeError}
|
|
401
|
+
labels={labels}
|
|
402
|
+
className="font-medium"
|
|
403
|
+
/>
|
|
404
|
+
);
|
|
405
|
+
const button = <ResendButton state={resend} labels={labels} variant="secondary" />;
|
|
385
406
|
return (
|
|
386
407
|
<AlertBanner
|
|
387
408
|
tone="warning"
|
|
388
409
|
variant={variant}
|
|
389
410
|
icon={<MailWarning />}
|
|
390
|
-
action={
|
|
411
|
+
action={
|
|
412
|
+
clampMobile ? (
|
|
413
|
+
<>
|
|
414
|
+
{button}
|
|
415
|
+
{status}
|
|
416
|
+
</>
|
|
417
|
+
) : (
|
|
418
|
+
button
|
|
419
|
+
)
|
|
420
|
+
}
|
|
391
421
|
onDismiss={onDismiss}
|
|
392
422
|
dismissLabel={onDismiss ? labels.dismiss : undefined}
|
|
423
|
+
clampMobile={clampMobile}
|
|
393
424
|
className={className}
|
|
394
425
|
>
|
|
395
426
|
{labels.banner}
|
|
396
|
-
{children != null && children !== false && <> {children}</>}
|
|
397
|
-
|
|
398
|
-
phase={resend.phase}
|
|
399
|
-
error={resend.error}
|
|
400
|
-
describeError={describeError}
|
|
401
|
-
labels={labels}
|
|
402
|
-
className="font-medium"
|
|
403
|
-
/>
|
|
427
|
+
{children != null && children !== false && <> {children}</>}
|
|
428
|
+
{!clampMobile && <> {status}</>}
|
|
404
429
|
</AlertBanner>
|
|
405
430
|
);
|
|
406
431
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useId, useLayoutEffect, useRef, useState } from "react";
|
|
1
2
|
import type { ComponentPropsWithoutRef, MouseEvent, ReactNode, Ref } from "react";
|
|
2
3
|
import { AlertTriangle, CheckCircle2, ChevronRight, Info, X } from "lucide-react";
|
|
3
4
|
import { cn } from "../lib/cn";
|
|
@@ -195,6 +196,34 @@ interface AlertBannerBaseProps extends Omit<ComponentPropsWithoutRef<"div">, "on
|
|
|
195
196
|
/** Accessible name of the ×. Default: `common.dismiss` from the
|
|
196
197
|
* {@link UiKitProvider}, else "Dismiss". */
|
|
197
198
|
dismissLabel?: string;
|
|
199
|
+
/**
|
|
200
|
+
* 0.48: on a phone (below the kit's text-size-aware `md`) hold the message to ONE line,
|
|
201
|
+
* with a "More" / "Less" toggle after it (`aria-expanded`, `aria-controls` the message;
|
|
202
|
+
* `common.showMore` / `common.showLess`). From `md` up nothing changes. The toggle only
|
|
203
|
+
* shows while the line actually cuts the message off.
|
|
204
|
+
*
|
|
205
|
+
* keksdose dev#611 (UX-1.2): its beta notice took three to four lines (~100px) above
|
|
206
|
+
* every phone screen, and the app built the clamp and the button around the banner's
|
|
207
|
+
* children by hand. Ignored on a whole-row banner (`href` / `onClick`): a toggle cannot
|
|
208
|
+
* sit inside the row's own link or button.
|
|
209
|
+
*/
|
|
210
|
+
clampMobile?: boolean;
|
|
211
|
+
/**
|
|
212
|
+
* Where a box's {@link action} goes on a phone (below `sm`). 0.48.
|
|
213
|
+
*
|
|
214
|
+
* `own-line` (default): a line of its own under the message, lined up with it
|
|
215
|
+
* (kastlan 53 — a long message beside a long button squeezed into a column).
|
|
216
|
+
*
|
|
217
|
+
* `flow`: on the message's line while the message (with its glyph) keeps 10rem, else at the END of the
|
|
218
|
+
* next line — for a short message and a short action, which a line of their own each
|
|
219
|
+
* would stack for nothing. keksdose dev #623: the budget card's "5 underfunded
|
|
220
|
+
* categories" + "Assign" built this with descendant overrides on the kit's classes.
|
|
221
|
+
* The basis is in rem, so it grows with the text size: at Large the action drops a
|
|
222
|
+
* line sooner, by itself. The × stays at the top end either way; `clampMobile` works
|
|
223
|
+
* alongside. From `sm` up nothing changes. Ignored by `strip` (which flows like this
|
|
224
|
+
* already) and `inline`.
|
|
225
|
+
*/
|
|
226
|
+
actionPlacement?: "own-line" | "flow";
|
|
198
227
|
children: ReactNode;
|
|
199
228
|
/**
|
|
200
229
|
* The banner's frame — where the rest of the props go (0.34, API-2, refactor audit
|
|
@@ -245,6 +274,8 @@ export function AlertBanner({
|
|
|
245
274
|
icon,
|
|
246
275
|
onDismiss,
|
|
247
276
|
dismissLabel,
|
|
277
|
+
clampMobile = false,
|
|
278
|
+
actionPlacement = "own-line",
|
|
248
279
|
href,
|
|
249
280
|
onClick,
|
|
250
281
|
className,
|
|
@@ -262,6 +293,26 @@ export function AlertBanner({
|
|
|
262
293
|
const inlineRow = inline && !block;
|
|
263
294
|
const resolvedRole = role ?? (inline && live ? (tone === "danger" ? "alert" : "status") : undefined);
|
|
264
295
|
|
|
296
|
+
// `clampMobile`: the message's one-line clamp below `md`, and whether that line holds
|
|
297
|
+
// it all (then there is nothing to show "More" of). Measured while collapsed — the
|
|
298
|
+
// clamp is what makes `scrollHeight` exceed the box — and again whenever its box
|
|
299
|
+
// changes size, which a rotation or crossing `md` does. With no layout (a test, the
|
|
300
|
+
// server) the box is 0 tall, which reads as "may not fit": the toggle stays.
|
|
301
|
+
const clamp = clampMobile && !interactive;
|
|
302
|
+
const bodyId = useId();
|
|
303
|
+
const bodyRef = useRef<HTMLSpanElement>(null);
|
|
304
|
+
const [expanded, setExpanded] = useState(false);
|
|
305
|
+
const [fits, setFits] = useState(false);
|
|
306
|
+
useLayoutEffect(() => {
|
|
307
|
+
const el = bodyRef.current;
|
|
308
|
+
if (!clamp || expanded || !el) return;
|
|
309
|
+
const check = () => setFits(el.clientHeight > 0 && el.scrollHeight <= el.clientHeight + 1);
|
|
310
|
+
check();
|
|
311
|
+
const ro = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(check);
|
|
312
|
+
ro?.observe(el);
|
|
313
|
+
return () => ro?.disconnect();
|
|
314
|
+
}, [clamp, expanded, children]);
|
|
315
|
+
|
|
265
316
|
const Glyph = TONE_GLYPH[tone];
|
|
266
317
|
// Onto the first line of a box's text: 2px at 14px/20px, 1px at 12px/16px.
|
|
267
318
|
const nudge = inlineRow || strip ? undefined : sm ? "mt-px" : "mt-0.5";
|
|
@@ -285,7 +336,14 @@ export function AlertBanner({
|
|
|
285
336
|
// squeezed "Please confirm your email address…" into a ~100px column beside a
|
|
286
337
|
// "Resend verification email" button. There the action takes a line of its own under
|
|
287
338
|
// the message, and the × stays at the top end; from sm up the box is one row as before.
|
|
288
|
-
const
|
|
339
|
+
const boxAction = !inline && !strip && action !== undefined && action !== null;
|
|
340
|
+
// `flow` (0.48): glyph + message and the action in a row of their own that wraps
|
|
341
|
+
// below sm; the × stays outside it, at the top end.
|
|
342
|
+
const flow = boxAction && actionPlacement === "flow";
|
|
343
|
+
const boxWraps = boxAction && !flow;
|
|
344
|
+
const flowRow = cn("flex min-w-0 flex-1 items-center max-sm:flex-wrap", sm ? "gap-1.5" : "gap-2");
|
|
345
|
+
// The 10rem the message keeps beside the action before the action wraps.
|
|
346
|
+
const flowBasis = flow ? "max-sm:basis-40" : undefined;
|
|
289
347
|
const row = cn(
|
|
290
348
|
inlineRow
|
|
291
349
|
? "inline-flex items-center"
|
|
@@ -309,10 +367,49 @@ export function AlertBanner({
|
|
|
309
367
|
);
|
|
310
368
|
const focusRing = cn(FOCUS_RING, "focus-visible:ring-offset-1 focus-visible:ring-offset-surface");
|
|
311
369
|
|
|
370
|
+
const contentBody = clamp ? (
|
|
371
|
+
// Below `md` a row: the message, clamped, then the toggle on its baseline. From
|
|
372
|
+
// `md` up a plain span with the message in it, as without the prop.
|
|
373
|
+
<span
|
|
374
|
+
className={cn(
|
|
375
|
+
!inlineRow && grow,
|
|
376
|
+
"min-w-0 text-start max-md:flex max-md:items-baseline max-md:gap-2",
|
|
377
|
+
)}
|
|
378
|
+
>
|
|
379
|
+
{/* The clamp is deliberate: the rest is one tap away behind More, so the size sweep's
|
|
380
|
+
truncation gate (scripts/screenshot-sizes.mjs) leaves it alone. */}
|
|
381
|
+
<span
|
|
382
|
+
id={bodyId}
|
|
383
|
+
ref={bodyRef}
|
|
384
|
+
data-truncate-ok=""
|
|
385
|
+
className={cn("max-md:min-w-0", !expanded && "max-md:line-clamp-1")}
|
|
386
|
+
>
|
|
387
|
+
{children}
|
|
388
|
+
</span>
|
|
389
|
+
{(expanded || !fits) && (
|
|
390
|
+
<button
|
|
391
|
+
type="button"
|
|
392
|
+
aria-expanded={expanded}
|
|
393
|
+
aria-controls={bodyId}
|
|
394
|
+
onClick={() => setExpanded((v) => !v)}
|
|
395
|
+
className={cn(
|
|
396
|
+
"relative z-10 shrink-0 rounded-sm font-medium underline underline-offset-2 md:hidden",
|
|
397
|
+
focusRing,
|
|
398
|
+
)}
|
|
399
|
+
>
|
|
400
|
+
{expanded ? common.showLess : common.showMore}
|
|
401
|
+
</button>
|
|
402
|
+
)}
|
|
403
|
+
</span>
|
|
404
|
+
) : (
|
|
405
|
+
<span className={cn(!inlineRow && (nested ? "flex-1" : grow), "min-w-0 text-start")}>
|
|
406
|
+
{children}
|
|
407
|
+
</span>
|
|
408
|
+
);
|
|
312
409
|
const content = (
|
|
313
410
|
<>
|
|
314
411
|
{glyph}
|
|
315
|
-
|
|
412
|
+
{contentBody}
|
|
316
413
|
{interactive && (
|
|
317
414
|
<ChevronRight
|
|
318
415
|
aria-hidden
|
|
@@ -354,7 +451,7 @@ export function AlertBanner({
|
|
|
354
451
|
className={cn(
|
|
355
452
|
"relative z-10 flex max-w-full shrink-0 flex-wrap items-center gap-2",
|
|
356
453
|
!strip && !inlineRow && "self-center",
|
|
357
|
-
strip && "ms-auto",
|
|
454
|
+
(strip || flow) && "ms-auto",
|
|
358
455
|
// Last, on a line of its own, lined up with the message (past the glyph).
|
|
359
456
|
boxWraps && cn("max-sm:order-last max-sm:basis-full", glyph && (sm ? "max-sm:ps-5" : "max-sm:ps-6")),
|
|
360
457
|
)}
|
|
@@ -378,8 +475,24 @@ export function AlertBanner({
|
|
|
378
475
|
role={resolvedRole}
|
|
379
476
|
className={cn(row, frame, TONE_TEXT[tone], className)}
|
|
380
477
|
>
|
|
381
|
-
{
|
|
382
|
-
|
|
478
|
+
{flow ? (
|
|
479
|
+
<>
|
|
480
|
+
<div className={flowRow} data-action-flow="">
|
|
481
|
+
{/* The glyph rides with the message, on its first line; the two are
|
|
482
|
+
centred against the action as one, as the whole-row button is. */}
|
|
483
|
+
<span className={cn("flex min-w-0 flex-1 items-start", flowBasis, sm ? "gap-1.5" : "gap-2")}>
|
|
484
|
+
{glyph}
|
|
485
|
+
{contentBody}
|
|
486
|
+
</span>
|
|
487
|
+
{trailing}
|
|
488
|
+
</div>
|
|
489
|
+
</>
|
|
490
|
+
) : (
|
|
491
|
+
<>
|
|
492
|
+
{content}
|
|
493
|
+
{trailing}
|
|
494
|
+
</>
|
|
495
|
+
)}
|
|
383
496
|
{dismiss}
|
|
384
497
|
</div>
|
|
385
498
|
);
|
|
@@ -393,6 +506,7 @@ export function AlertBanner({
|
|
|
393
506
|
? cn(
|
|
394
507
|
"flex min-w-0 text-start",
|
|
395
508
|
grow,
|
|
509
|
+
flowBasis,
|
|
396
510
|
sm ? "gap-1.5" : "gap-2",
|
|
397
511
|
inlineRow || strip ? "items-center" : "items-start",
|
|
398
512
|
"outline-none after:absolute after:inset-0 after:content-['']",
|
|
@@ -440,8 +554,18 @@ export function AlertBanner({
|
|
|
440
554
|
role={resolvedRole}
|
|
441
555
|
className={cn("relative", row, frame, TONE_TEXT[tone], hover, className)}
|
|
442
556
|
>
|
|
443
|
-
{
|
|
444
|
-
|
|
557
|
+
{flow ? (
|
|
558
|
+
// Not positioned: the row's stretched `after:` target must still cover the frame.
|
|
559
|
+
<div className={flowRow} data-action-flow="">
|
|
560
|
+
{rowAction}
|
|
561
|
+
{trailing}
|
|
562
|
+
</div>
|
|
563
|
+
) : (
|
|
564
|
+
<>
|
|
565
|
+
{rowAction}
|
|
566
|
+
{trailing}
|
|
567
|
+
</>
|
|
568
|
+
)}
|
|
445
569
|
{dismiss}
|
|
446
570
|
</div>
|
|
447
571
|
);
|
|
@@ -41,6 +41,12 @@ const BUTTON_BASE = cn(
|
|
|
41
41
|
*/
|
|
42
42
|
export const TOUCH_TARGET_LARGE = "large:min-h-[48px] large:min-w-[48px]";
|
|
43
43
|
|
|
44
|
+
/** The 48 px target at Large for a control whose BOX cannot grow (a chip's or a tab's
|
|
45
|
+
* action, IconButton `2xs`; Tabs' overflow cues, 0.48 KS N3): an invisible `::after`
|
|
46
|
+
* centred on it takes the taps a finger lands round the glyph. Not public. */
|
|
47
|
+
export const HIT_AREA_LARGE =
|
|
48
|
+
"relative large:after:absolute large:after:inset-[calc((100%-48px)/2)] large:after:content-['']";
|
|
49
|
+
|
|
44
50
|
export type ButtonSize = "sm" | "md";
|
|
45
51
|
|
|
46
52
|
// Box geometry per size, split out of the base so the two cannot be merged into one
|
|
@@ -19,7 +19,7 @@ import { Spinner } from "./spinner";
|
|
|
19
19
|
import { useAnchorDir } from "./use-anchor-dir";
|
|
20
20
|
import { type AnchorRect } from "../hooks/use-anchored-rect";
|
|
21
21
|
import { anchoredInline, useAnchoredPanel, viewportSize, type AnchoredPanel } from "../hooks/use-anchored-panel";
|
|
22
|
-
import { takeEscape, useDismissLayer, useOutsideClick } from "../hooks/use-dismiss";
|
|
22
|
+
import { pressInside, takeEscape, useDismissLayer, useOutsideClick } from "../hooks/use-dismiss";
|
|
23
23
|
import { useOverlayHistory } from "../hooks/use-overlay-history";
|
|
24
24
|
import { isImeEnter } from "../lib/ime";
|
|
25
25
|
import { foldForSearch } from "../lib/text-match";
|
|
@@ -1066,6 +1066,12 @@ export function ComboboxPanel<V extends string | number>({
|
|
|
1066
1066
|
ref={panelRef}
|
|
1067
1067
|
dir={dir}
|
|
1068
1068
|
onKeyDown={onKeyDown}
|
|
1069
|
+
// A press in a panel opened from a row is inside this list for the core's outside
|
|
1070
|
+
// press, though portalled elsewhere (dev#623).
|
|
1071
|
+
onPointerDownCapture={(e) => {
|
|
1072
|
+
rest.onPointerDownCapture?.(e);
|
|
1073
|
+
pressInside(panelRef)?.(e);
|
|
1074
|
+
}}
|
|
1069
1075
|
// `placement` keeps the panel inside the region actually on screen: on a phone
|
|
1070
1076
|
// the search box below pulls up the keyboard, and a panel pinned under a
|
|
1071
1077
|
// low trigger would otherwise sit entirely behind it (feedback #135). Flipped
|
|
@@ -17,6 +17,8 @@ export function ColumnSettingsPanel<T>({
|
|
|
17
17
|
columns,
|
|
18
18
|
hiddenCols,
|
|
19
19
|
setHiddenCols,
|
|
20
|
+
autoHidden,
|
|
21
|
+
keepColumn,
|
|
20
22
|
showSettings,
|
|
21
23
|
setShowSettings,
|
|
22
24
|
columnsCountLabel,
|
|
@@ -27,6 +29,11 @@ export function ColumnSettingsPanel<T>({
|
|
|
27
29
|
columns: DataTableColumn<T>[];
|
|
28
30
|
hiddenCols: Set<string>;
|
|
29
31
|
setHiddenCols: Dispatch<SetStateAction<Set<string>>>;
|
|
32
|
+
/** The columns the table's width leaves out (`fitPriority`, 0.48): unticked, and
|
|
33
|
+
* marked "hidden for width". */
|
|
34
|
+
autoHidden: Set<string>;
|
|
35
|
+
/** A tick keeps a column in whatever the width; an untick drops that (data-table-fit.ts). */
|
|
36
|
+
keepColumn: (key: string, on: boolean) => void;
|
|
30
37
|
showSettings: boolean;
|
|
31
38
|
setShowSettings: (open: boolean) => void;
|
|
32
39
|
/** "Columns (n/m)", the rail's text and name. */
|
|
@@ -102,7 +109,10 @@ export function ColumnSettingsPanel<T>({
|
|
|
102
109
|
<div className="flex flex-col gap-0.5">
|
|
103
110
|
{columns.map((col) => {
|
|
104
111
|
if (isTableOwnColumn(col.key)) return null;
|
|
105
|
-
|
|
112
|
+
// Ticked = drawn: a column left out for width is unticked, so the boxes
|
|
113
|
+
// and "Columns (8/10)" say the same (keksdose live #451).
|
|
114
|
+
const forWidth = !hiddenCols.has(col.key) && autoHidden.has(col.key);
|
|
115
|
+
const checked = !hiddenCols.has(col.key) && !forWidth;
|
|
106
116
|
const headerLabel = columnLabel(col);
|
|
107
117
|
return (
|
|
108
118
|
<label
|
|
@@ -114,6 +124,7 @@ export function ColumnSettingsPanel<T>({
|
|
|
114
124
|
className="size-3.5 shrink-0"
|
|
115
125
|
checked={checked}
|
|
116
126
|
onChange={(e) => {
|
|
127
|
+
keepColumn(col.key, e.target.checked);
|
|
117
128
|
setHiddenCols((s) => {
|
|
118
129
|
const next = new Set(s);
|
|
119
130
|
if (e.target.checked) next.delete(col.key);
|
|
@@ -125,7 +136,17 @@ export function ColumnSettingsPanel<T>({
|
|
|
125
136
|
{/* The panel is `w-56`: a long column name wraps from Large up rather
|
|
126
137
|
than reading "Accoun…" beside its box (text size §10.17, round
|
|
127
138
|
0.34 T1). */}
|
|
128
|
-
|
|
139
|
+
{forWidth ? (
|
|
140
|
+
// Under the name, as part of the box's name: "Email, hidden for width".
|
|
141
|
+
<span className="flex min-w-0 flex-col">
|
|
142
|
+
<span className="truncate-until-large">{headerLabel}</span>
|
|
143
|
+
<span className="text-micro text-muted" data-hidden-for-width="">
|
|
144
|
+
{labels.hiddenForWidth}
|
|
145
|
+
</span>
|
|
146
|
+
</span>
|
|
147
|
+
) : (
|
|
148
|
+
<span className="min-w-0 truncate-until-large">{headerLabel}</span>
|
|
149
|
+
)}
|
|
129
150
|
</label>
|
|
130
151
|
);
|
|
131
152
|
})}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import type { DataTableColumn } from "./data-table";
|
|
3
|
+
|
|
4
|
+
// DataTable's fit to its own width (0.48, keksdose live #451: "On expanded sidebar for
|
|
5
|
+
// desktop the table scrolls horizontally. On collapsed sidebar it fits […] Compute the
|
|
6
|
+
// width selectively"). Two column options, both opt-in:
|
|
7
|
+
//
|
|
8
|
+
// - `grow`: the column takes what the others leave and ellipsises (`w-full max-w-0` on
|
|
9
|
+
// its cells: in an auto table layout that is "the rest of the row", never its content's
|
|
10
|
+
// width), so one long title can no longer widen the table.
|
|
11
|
+
// - `fitPriority`: while the table is wider than its scroller — or, with a `grow` column,
|
|
12
|
+
// while that column would get less than {@link GROW_MIN_REM} — the table leaves out
|
|
13
|
+
// its `fitPriority` columns, the LOWEST number first (ties: the later column first),
|
|
14
|
+
// and brings them back in the reverse order as soon as the table, as it is NOW, has
|
|
15
|
+
// room for them (its need re-measured, plus the column's own header width from when
|
|
16
|
+
// it was left out), so narrower content (a filter, another page) counts as much as a
|
|
17
|
+
// wider scroller. The scroller's own width, through a ResizeObserver: a sidebar, a
|
|
18
|
+
// narrower window, the Columns drawer and Large text all count the same.
|
|
19
|
+
|
|
20
|
+
/** The least a `grow` column keeps before the table starts leaving columns out: about
|
|
21
|
+
* a sentence of a title. In rem, so Large text asks for more. */
|
|
22
|
+
export const GROW_MIN_REM = 10;
|
|
23
|
+
|
|
24
|
+
/** The `<th>`/`<td>` classes of a `grow` column. `truncate-until-large`: one line and an
|
|
25
|
+
* ellipsis at Normal, a wrap from Large up (text size §10.17), as every kit title. The
|
|
26
|
+
* header's `min-w-[10rem]` ({@link GROW_MIN_REM}) is the floor once no `fitPriority`
|
|
27
|
+
* column is left to leave out: the table then scrolls sideways rather than crushing the
|
|
28
|
+
* column it exists for (Kurvenschmiede S1, 0.48: Subject was 24px at 1100px). */
|
|
29
|
+
export const GROW_HEAD_CLASS = "w-full max-w-0 min-w-[10rem]";
|
|
30
|
+
export const GROW_CELL_CLASS = "w-full max-w-0 truncate-until-large";
|
|
31
|
+
|
|
32
|
+
/** Every OTHER cell of a table with a `grow` column (kastlan S2, 0.48). A `w-full` column
|
|
33
|
+
* squeezes the rest to their min-content width, so any text with a space broke in two
|
|
34
|
+
* even at 1920px ("CHF 2'568.38", "Peter Mueller", a date with its time). One line at
|
|
35
|
+
* Normal, so a column is never narrower than its text and `fitPriority` measures what
|
|
36
|
+
* the columns really need; from Large up the cell wraps as before (§10.17). The headers
|
|
37
|
+
* are `whitespace-nowrap` already. A column whose prose should wrap anyway passes
|
|
38
|
+
* `not-large:whitespace-normal` in its `className`. */
|
|
39
|
+
export const GROW_PEER_CELL_CLASS = "not-large:whitespace-nowrap";
|
|
40
|
+
|
|
41
|
+
// A cell's own flex-wrap row (chips) is left to wrap beside a `grow` column: a row of
|
|
42
|
+
// badges reads as well stacked, and held to one line it widened the user roster's Status
|
|
43
|
+
// until kastlan's admin pane scrolled 21px at 1280px (0.48 build review 2, kastlan S1).
|
|
44
|
+
|
|
45
|
+
/** The auto-hide order: lowest `fitPriority` first, the later column first on a tie. */
|
|
46
|
+
export function fitHideOrder<T>(columns: DataTableColumn<T>[]): DataTableColumn<T>[] {
|
|
47
|
+
return columns
|
|
48
|
+
.map((col, index) => ({ col, index }))
|
|
49
|
+
.filter(({ col }) => col.fitPriority != null)
|
|
50
|
+
.sort((a, b) => a.col.fitPriority! - b.col.fitPriority! || b.index - a.index)
|
|
51
|
+
.map(({ col }) => col);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Which columns the table leaves out for width, and the Columns panel's "keep it".
|
|
56
|
+
*
|
|
57
|
+
* A column the user TICKS in the Columns panel is theirs: it is never left out for width
|
|
58
|
+
* again while the table is mounted (`keep`): the table makes room by leaving out the next
|
|
59
|
+
* candidate, and scrolls sideways once none is left — what they asked for. Unticking it hides it as before (the persisted `hiddenCols`) and
|
|
60
|
+
* drops the keep. Keeps are not persisted: on the next visit the table fits again.
|
|
61
|
+
*/
|
|
62
|
+
export function useColumnFit<T>({
|
|
63
|
+
columns,
|
|
64
|
+
hiddenCols,
|
|
65
|
+
headRefs,
|
|
66
|
+
scroller,
|
|
67
|
+
enabled,
|
|
68
|
+
}: {
|
|
69
|
+
columns: DataTableColumn<T>[];
|
|
70
|
+
/** The user's hidden columns: never candidates (they are out already). */
|
|
71
|
+
hiddenCols: Set<string>;
|
|
72
|
+
headRefs: Map<string, HTMLTableCellElement>;
|
|
73
|
+
/** The desktop scroller, read after render (it is declared below the call). */
|
|
74
|
+
scroller: () => HTMLElement | null;
|
|
75
|
+
/** The wide layout. On a phone (the card list) nothing is left out for width. */
|
|
76
|
+
enabled: boolean;
|
|
77
|
+
}): {
|
|
78
|
+
/** The columns left out for width now — empty while off. */
|
|
79
|
+
autoHidden: Set<string>;
|
|
80
|
+
/** The user's choice in the Columns panel: `true` keeps the column in whatever the width. */
|
|
81
|
+
keep: (key: string, on: boolean) => void;
|
|
82
|
+
} {
|
|
83
|
+
const [kept, setKept] = useState<ReadonlySet<string>>(() => new Set());
|
|
84
|
+
const [hidden, setHidden] = useState<string[]>([]);
|
|
85
|
+
// A column's own header width, written when it is left out: it comes back once the
|
|
86
|
+
// table's need as it is now, plus this, fits the scroller (keksdose S3, 0.48: not the
|
|
87
|
+
// whole table's need at hide time, which went stale when the content narrowed).
|
|
88
|
+
const ownWidth = useRef(new Map<string, number>());
|
|
89
|
+
|
|
90
|
+
const hasFit = columns.some((c) => c.fitPriority != null);
|
|
91
|
+
const active = enabled && hasFit;
|
|
92
|
+
const order = useMemo(
|
|
93
|
+
() => fitHideOrder(columns).filter((c) => !hiddenCols.has(c.key) && !kept.has(c.key)),
|
|
94
|
+
[columns, hiddenCols, kept],
|
|
95
|
+
);
|
|
96
|
+
// Only the candidates still in play, in hide order — a column the user hid or kept
|
|
97
|
+
// since, or one the app dropped, is no longer "hidden for width".
|
|
98
|
+
const current = useMemo(
|
|
99
|
+
() => (active ? order.filter((c) => hidden.includes(c.key)).map((c) => c.key) : []),
|
|
100
|
+
[active, order, hidden],
|
|
101
|
+
);
|
|
102
|
+
const growKey = columns.find((c) => c.grow && !hiddenCols.has(c.key))?.key;
|
|
103
|
+
|
|
104
|
+
// What `measure` reads, refreshed after each render (before the measure below runs):
|
|
105
|
+
// the ResizeObserver's callback is set up once and must see the current columns.
|
|
106
|
+
const latest = useRef({ order, current, growKey, active, scroller });
|
|
107
|
+
useLayoutEffect(() => {
|
|
108
|
+
latest.current = { order, current, growKey, active, scroller };
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const measure = useCallback(() => {
|
|
112
|
+
const { order, current, growKey, active, scroller } = latest.current;
|
|
113
|
+
const el = scroller();
|
|
114
|
+
if (!active || !el) return;
|
|
115
|
+
const table = el.querySelector("table");
|
|
116
|
+
const avail = el.clientWidth;
|
|
117
|
+
// Not laid out (display:none, jsdom): nothing to fit to.
|
|
118
|
+
if (!table || avail <= 0) return;
|
|
119
|
+
const growTh = growKey ? headRefs.get(growKey) : undefined;
|
|
120
|
+
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
|
|
121
|
+
let need = growTh
|
|
122
|
+
? table.offsetWidth - growTh.offsetWidth + GROW_MIN_REM * rem
|
|
123
|
+
: table.offsetWidth;
|
|
124
|
+
if (!growTh && need <= avail + 0.5 && current.length > 0) {
|
|
125
|
+
// A `w-full` table is at least the scroller's width, which says nothing about how
|
|
126
|
+
// much room its content leaves: probe its content's width (what it takes once it
|
|
127
|
+
// overflows) for the bring-back below. Read and put back in one layout pass.
|
|
128
|
+
const before = table.style.width;
|
|
129
|
+
table.style.width = "min-content";
|
|
130
|
+
need = table.offsetWidth;
|
|
131
|
+
table.style.width = before;
|
|
132
|
+
}
|
|
133
|
+
if (need > avail + 0.5) {
|
|
134
|
+
// Leave out, in order, until the rest fits: each column saves its header's width
|
|
135
|
+
// (the others are at their content's width while a `grow` column takes the rest).
|
|
136
|
+
const next = [...current];
|
|
137
|
+
for (const col of order) {
|
|
138
|
+
if (need <= avail + 0.5) break;
|
|
139
|
+
if (next.includes(col.key)) continue;
|
|
140
|
+
const w = headRefs.get(col.key)?.offsetWidth ?? 0;
|
|
141
|
+
ownWidth.current.set(col.key, w);
|
|
142
|
+
need -= w;
|
|
143
|
+
next.push(col.key);
|
|
144
|
+
}
|
|
145
|
+
if (next.length !== current.length) setHidden(next);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
// Bring back, the last left out first, while the table as it is now has room for
|
|
149
|
+
// them. Should the column have grown since (wider content), the next measure leaves
|
|
150
|
+
// it out again before paint and records its new width, so it cannot flicker.
|
|
151
|
+
let n = current.length;
|
|
152
|
+
while (n > 0) {
|
|
153
|
+
const w = ownWidth.current.get(current[n - 1]);
|
|
154
|
+
if (w == null || need + w > avail + 0.5) break;
|
|
155
|
+
need += w;
|
|
156
|
+
n--;
|
|
157
|
+
}
|
|
158
|
+
if (n !== current.length) setHidden(current.slice(0, n));
|
|
159
|
+
}, [headRefs]);
|
|
160
|
+
|
|
161
|
+
// After every render: rows, a filter or a page can widen a column as much as a resize.
|
|
162
|
+
// Before paint, so a column the table cannot hold is never drawn first.
|
|
163
|
+
useLayoutEffect(() => {
|
|
164
|
+
measure();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
const el = latest.current.scroller();
|
|
169
|
+
if (!active || !el || typeof ResizeObserver === "undefined") return;
|
|
170
|
+
const ro = new ResizeObserver(() => measure());
|
|
171
|
+
ro.observe(el);
|
|
172
|
+
return () => ro.disconnect();
|
|
173
|
+
}, [active, measure]);
|
|
174
|
+
|
|
175
|
+
const keep = useCallback((key: string, on: boolean) => {
|
|
176
|
+
setKept((s) => {
|
|
177
|
+
if (s.has(key) === on) return s;
|
|
178
|
+
const next = new Set(s);
|
|
179
|
+
if (on) next.add(key);
|
|
180
|
+
else next.delete(key);
|
|
181
|
+
return next;
|
|
182
|
+
});
|
|
183
|
+
}, []);
|
|
184
|
+
|
|
185
|
+
const autoHidden = useMemo(() => new Set(current), [current]);
|
|
186
|
+
return { autoHidden, keep };
|
|
187
|
+
}
|