@veevarts/design-system 2.1.0-beta.14 → 2.1.0-beta.15
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/dist/components/Select/Select.d.ts +10 -0
- package/dist/index.cjs +8 -8
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4897 -4031
- package/dist/patterns/AdminToolbar/AdminToolbar.d.ts +10 -2
- package/dist/patterns/AdminToolbar/index.d.ts +1 -0
- package/dist/patterns/AdminToolbar/styles.d.ts +49 -0
- package/dist/patterns/AdminToolbar/types.d.ts +2 -0
- package/dist/patterns/DiscardGuard/DiscardGuard.d.ts +23 -0
- package/dist/patterns/DiscardGuard/context.d.ts +25 -0
- package/dist/patterns/DiscardGuard/index.d.ts +6 -0
- package/dist/patterns/DiscardGuard/useDiscardGuard.d.ts +38 -0
- package/dist/patterns/EntityRow/EntityList.d.ts +9 -0
- package/dist/patterns/EntityRow/EntityRow.d.ts +3 -0
- package/dist/patterns/EntityRow/context.d.ts +2 -0
- package/dist/patterns/EntityRow/icons.d.ts +3 -0
- package/dist/patterns/EntityRow/index.d.ts +3 -0
- package/dist/patterns/EntityRow/initials.d.ts +6 -0
- package/dist/patterns/EntityRow/styles.d.ts +59 -0
- package/dist/patterns/EntityRow/types.d.ts +118 -0
- package/dist/patterns/InlineConfirm/InlineConfirm.d.ts +38 -0
- package/dist/patterns/InlineConfirm/index.d.ts +2 -0
- package/dist/patterns/InlineConfirm/styles.d.ts +16 -0
- package/dist/patterns/ModalPanels/ModalPanels.d.ts +35 -0
- package/dist/patterns/ModalPanels/context.d.ts +20 -0
- package/dist/patterns/ModalPanels/focus.d.ts +39 -0
- package/dist/patterns/ModalPanels/index.d.ts +6 -0
- package/dist/patterns/ModalPanels/styles.d.ts +13 -0
- package/dist/patterns/ModalPanels/useModalPanels.d.ts +30 -0
- package/dist/patterns/PaginationSummary/PaginationSummary.d.ts +12 -0
- package/dist/patterns/PaginationSummary/index.d.ts +1 -1
- package/dist/patterns/PillTabs/index.d.ts +1 -1
- package/dist/patterns/PillTabs/styles.d.ts +14 -4
- package/dist/patterns/PillTabs/types.d.ts +8 -0
- package/dist/patterns/RecordCard/index.d.ts +1 -1
- package/dist/patterns/RecordCard/styles.d.ts +3 -2
- package/dist/patterns/RecordCard/types.d.ts +5 -0
- package/dist/patterns/index.d.ts +12 -4
- package/dist/providers/DensityProvider.d.ts +7 -0
- package/dist/providers/density.d.ts +17 -0
- package/dist/providers/index.d.ts +4 -0
- package/package.json +2 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { default as React } from 'react';
|
|
2
|
+
import { AdminToolbarSearchWidth, AdminToolbarSize } from './types';
|
|
2
3
|
export interface AdminToolbarProps {
|
|
3
4
|
searchValue: string;
|
|
4
5
|
onSearchChange: (value: string) => void;
|
|
@@ -10,12 +11,19 @@ export interface AdminToolbarProps {
|
|
|
10
11
|
searchIcon?: React.ReactNode;
|
|
11
12
|
/** Shows a clear button inside the search box while it holds text. Off by default. */
|
|
12
13
|
searchClearable?: boolean;
|
|
14
|
+
/** The clear button's accessible name. `'Clear search'` by default; pass a translation. */
|
|
15
|
+
clearLabel?: string;
|
|
13
16
|
/** Filters, sort pickers, a toggle — laid out after the search box. */
|
|
14
17
|
children?: React.ReactNode;
|
|
15
18
|
/** Pushed to the far right, for a primary action. */
|
|
16
19
|
trailing?: React.ReactNode;
|
|
17
|
-
/**
|
|
18
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Search box width. `'md'` (280px; 250px framed) by default, `'lg'` (360px)
|
|
22
|
+
* for wide tables, `'fill'` to grow into the space the row leaves.
|
|
23
|
+
*/
|
|
24
|
+
searchWidth?: AdminToolbarSearchWidth;
|
|
25
|
+
/** `'lg'` is the 48px touch field with 16px text; `'md'` (default) keeps each variant's own height. */
|
|
26
|
+
size?: AdminToolbarSize;
|
|
19
27
|
variant?: 'default' | 'framed';
|
|
20
28
|
className?: string;
|
|
21
29
|
'data-testid'?: string;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { AdminToolbarSearchWidth, AdminToolbarSize } from './types';
|
|
2
|
+
/** The default variant: a pixel-port of the accounting mockups, so its values stay in px. */
|
|
3
|
+
export declare const DEFAULT_CLASS: {
|
|
4
|
+
readonly base: "flex flex-wrap items-center gap-3 mb-4";
|
|
5
|
+
readonly trailing: "ml-auto";
|
|
6
|
+
readonly icon: "flex text-acct-muted";
|
|
7
|
+
readonly inputWrapper: string;
|
|
8
|
+
readonly input: "text-[13.5px] text-acct-ink";
|
|
9
|
+
};
|
|
10
|
+
export declare const DEFAULT_SEARCH_WIDTH: Record<AdminToolbarSearchWidth, string>;
|
|
11
|
+
export declare const FRAMED_WIDE_REM = 48;
|
|
12
|
+
export declare const FRAMED_CLASS: {
|
|
13
|
+
readonly base: "flex flex-wrap items-center gap-2.5 border-b border-pos-hair px-4 py-[0.875rem]";
|
|
14
|
+
readonly narrow: "[&_[data-slot=filter-select]]:!max-w-none [&_[data-slot=filter-select]]:!min-w-[calc(50%-0.3125rem)] [&_[data-slot=filter-select]]:grow [&_[data-slot=filter-select]]:basis-[calc(50%-0.3125rem)]";
|
|
15
|
+
readonly searchWide: "ml-auto";
|
|
16
|
+
/** Fill wins over the narrow rule: it already takes the row, and keeps a trailing action beside it until 12.5rem. */
|
|
17
|
+
readonly searchFill: "min-w-[12.5rem] flex-1";
|
|
18
|
+
readonly searchNarrow: "basis-full";
|
|
19
|
+
readonly inputBaseNarrow: "w-full";
|
|
20
|
+
readonly inputWrapper: "h-[2.375rem] min-h-[2.375rem] !h-[2.375rem] rounded-[0.625rem] border border-pos-field bg-white pl-3 pr-3 shadow-none data-[hover=true]:border-pos-field group-data-[focus=true]:border-primary";
|
|
21
|
+
readonly innerWrapper: "gap-[0.3125rem]";
|
|
22
|
+
readonly input: "text-ellipsis text-[0.875rem] text-default-900 data-[has-start-content=true]:ps-0.5";
|
|
23
|
+
readonly icon: "flex text-default-400 [&_svg]:size-4";
|
|
24
|
+
};
|
|
25
|
+
export declare const FRAMED_SEARCH_WIDTH: Record<AdminToolbarSearchWidth, string>;
|
|
26
|
+
/**
|
|
27
|
+
* `lg` is the POS register's touch row: a 48px field with 16px text and a 20px
|
|
28
|
+
* glyph, in rem so the text-size setting scales it. Merged after either
|
|
29
|
+
* variant's own classes, so it overrides their height and type only.
|
|
30
|
+
*/
|
|
31
|
+
export declare const LG_CLASS: {
|
|
32
|
+
readonly inputWrapper: "h-12 min-h-12 !h-12";
|
|
33
|
+
readonly input: "text-base";
|
|
34
|
+
readonly icon: "[&_svg]:size-5";
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* The clear control: a DS `Button`, so it is in the tab order and takes a
|
|
38
|
+
* translated name, unlike HeroUI's `isClearable` (tabIndex -1, English label).
|
|
39
|
+
* 32px inside the 38–40px fields, 44px inside the 48px one.
|
|
40
|
+
*/
|
|
41
|
+
export declare const CLEAR_CLASS: {
|
|
42
|
+
readonly button: "shrink-0 text-default-500";
|
|
43
|
+
/** The browser's own search "x" would sit beside ours. */
|
|
44
|
+
readonly input: "[&::-webkit-search-cancel-button]:appearance-none";
|
|
45
|
+
};
|
|
46
|
+
export declare const CLEAR_SIZE_CLASS: Record<AdminToolbarSize, {
|
|
47
|
+
button: string;
|
|
48
|
+
icon: string;
|
|
49
|
+
}>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
import { DiscardGuardController } from './useDiscardGuard';
|
|
3
|
+
/** @internal Used by `ModalPanels` to make each panel its own guard scope. */
|
|
4
|
+
export declare const DiscardGuardScope: React.FC<{
|
|
5
|
+
scope: string;
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
}>;
|
|
8
|
+
export interface DiscardGuardProps {
|
|
9
|
+
/** The controller from `useDiscardGuard()`. */
|
|
10
|
+
guard: DiscardGuardController;
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
/** Confirmation heading. Default `'Discard changes?'`. */
|
|
13
|
+
title?: string;
|
|
14
|
+
/** What is lost by confirming. Default `'What you entered will be lost.'`. */
|
|
15
|
+
description?: React.ReactNode;
|
|
16
|
+
/** Confirm (discard) label. Default `'Discard'`. */
|
|
17
|
+
confirmLabel?: string;
|
|
18
|
+
/** Label of the action that returns to the form. Default `'Keep editing'`. */
|
|
19
|
+
keepEditingLabel?: string;
|
|
20
|
+
/** Root test id of the confirmation; derives `-confirm` and `-dismiss`. Default `'discard-guard'`. */
|
|
21
|
+
'data-testid'?: string;
|
|
22
|
+
}
|
|
23
|
+
export declare const DiscardGuard: React.FC<DiscardGuardProps>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { DirtyReport, Leave } from './useDiscardGuard';
|
|
2
|
+
export interface DiscardGuardContextValue {
|
|
3
|
+
guardScope: (scope: string | null, leave: Leave) => void;
|
|
4
|
+
bypass: (leave: Leave) => void;
|
|
5
|
+
report: (id: string, report: DirtyReport) => void;
|
|
6
|
+
unreport: (id: string) => void;
|
|
7
|
+
}
|
|
8
|
+
export declare const DiscardGuardContext: import('react').Context<DiscardGuardContextValue>;
|
|
9
|
+
/** The scope a reporter or a Back press belongs to. `ModalPanels` sets one per panel. */
|
|
10
|
+
export declare const DiscardScopeContext: import('react').Context<string | null>;
|
|
11
|
+
/**
|
|
12
|
+
* Call from a form while it is mounted. Unmounting withdraws the report, so a
|
|
13
|
+
* form that is gone never holds an exit hostage.
|
|
14
|
+
*/
|
|
15
|
+
export declare function useReportUnsavedChanges(isDirty: boolean): void;
|
|
16
|
+
export interface DiscardGuardActions {
|
|
17
|
+
/** Leave the whole surface: asks when any form is dirty. */
|
|
18
|
+
guard: (leave: Leave) => void;
|
|
19
|
+
/** Leave without asking — the input was saved. */
|
|
20
|
+
bypass: (leave: Leave) => void;
|
|
21
|
+
}
|
|
22
|
+
/** The guard's exits, for components rendered inside `<DiscardGuard>`. */
|
|
23
|
+
export declare function useDiscardGuardActions(): DiscardGuardActions;
|
|
24
|
+
/** @internal Back inside a panel: asks only when that panel holds unsaved input. */
|
|
25
|
+
export declare function useScopedDiscardGuard(): (leave: Leave) => void;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { DiscardGuard } from './DiscardGuard';
|
|
2
|
+
export type { DiscardGuardProps } from './DiscardGuard';
|
|
3
|
+
export { useDiscardGuard } from './useDiscardGuard';
|
|
4
|
+
export type { DiscardGuardController } from './useDiscardGuard';
|
|
5
|
+
export { useDiscardGuardActions, useReportUnsavedChanges } from './context';
|
|
6
|
+
export type { DiscardGuardActions } from './context';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export type Leave = () => void;
|
|
2
|
+
/** One mounted reporter: which scope (panel) it belongs to and whether it holds unsaved input. */
|
|
3
|
+
export interface DirtyReport {
|
|
4
|
+
scope: string | null;
|
|
5
|
+
isDirty: boolean;
|
|
6
|
+
}
|
|
7
|
+
interface PendingLeave {
|
|
8
|
+
leave: Leave;
|
|
9
|
+
}
|
|
10
|
+
/** Wiring `<DiscardGuard>` reads; not part of the consumer contract. */
|
|
11
|
+
interface DiscardGuardInternals {
|
|
12
|
+
pending: PendingLeave | null;
|
|
13
|
+
/** Bumped on confirm so the dialog unmounts in the same commit as the exit (see the file header). */
|
|
14
|
+
confirmKey: number;
|
|
15
|
+
confirm: () => void;
|
|
16
|
+
keepEditing: () => void;
|
|
17
|
+
report: (id: string, report: DirtyReport) => void;
|
|
18
|
+
unreport: (id: string) => void;
|
|
19
|
+
guardScope: (scope: string | null, leave: Leave) => void;
|
|
20
|
+
}
|
|
21
|
+
export interface DiscardGuardController {
|
|
22
|
+
/** True while any mounted form reports unsaved input. */
|
|
23
|
+
readonly isDirty: boolean;
|
|
24
|
+
/** Runs `leave` now, or after the operator confirms when a form is dirty. */
|
|
25
|
+
guard: (leave: Leave) => void;
|
|
26
|
+
/** Runs `leave` without asking — for exits that keep the input, like a successful save. */
|
|
27
|
+
bypass: (leave: Leave) => void;
|
|
28
|
+
/**
|
|
29
|
+
* Wraps a modal's `onOpenChange` so its close button, Escape and outside
|
|
30
|
+
* click go through `guard`. Opening passes straight through.
|
|
31
|
+
*/
|
|
32
|
+
guardOpenChange: (onOpenChange: (open: boolean) => void) => (open: boolean) => void;
|
|
33
|
+
/** @internal Read by `<DiscardGuard>`. */
|
|
34
|
+
readonly internals: DiscardGuardInternals;
|
|
35
|
+
}
|
|
36
|
+
/** Creates the guard. Call it in the component that owns the modal. */
|
|
37
|
+
export declare function useDiscardGuard(): DiscardGuardController;
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
import { EntityListProps, EntityListSkeletonProps } from './types';
|
|
3
|
+
export declare const EntityList: React.FC<EntityListProps>;
|
|
4
|
+
/**
|
|
5
|
+
* The list in skeleton form, at the same row height so nothing shifts when the
|
|
6
|
+
* rows arrive. The placeholder rows are hidden from assistive tech; `label` is
|
|
7
|
+
* announced once through a `role="status"` region instead.
|
|
8
|
+
*/
|
|
9
|
+
export declare const EntityListSkeleton: React.FC<EntityListSkeletonProps>;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { EntityRow } from './EntityRow';
|
|
2
|
+
export { EntityList, EntityListSkeleton } from './EntityList';
|
|
3
|
+
export type { EntityRowProps, EntityRowSize, EntityRowVariant, EntityRowTag, EntityRowTagTone, EntityRowLeading, EntityRowAction, EntityListProps, EntityListSkeletonProps, } from './types';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Chidi Okafor" → "CO", "Ana María de la Cruz" → "AC", "Cher" → "C".
|
|
3
|
+
* First and last word, so a long name does not spell an acronym the way
|
|
4
|
+
* HeroUI's default (every word's initial) does. Empty → "—".
|
|
5
|
+
*/
|
|
6
|
+
export declare function initialsFromName(name: string): string;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { EntityListProps, EntityRowSize, EntityRowTagTone, EntityRowVariant } from './types';
|
|
2
|
+
export declare const entityRowClass: {
|
|
3
|
+
readonly base: "relative flex w-full items-center gap-2 transition-colors";
|
|
4
|
+
readonly content: "flex min-w-0 flex-1 items-center gap-3 text-left";
|
|
5
|
+
/**
|
|
6
|
+
* The press target is a HeroUI `Button` (or `Link`) stretched over the row
|
|
7
|
+
* by its `::after`. Its base classes have to be undone for that to work:
|
|
8
|
+
* `relative` would make the button the overlay's containing block,
|
|
9
|
+
* `transform-gpu` would too, `overflow-hidden` clips the overlay,
|
|
10
|
+
* `min-w-max` and `whitespace-nowrap` defeat truncation, and the focus
|
|
11
|
+
* outline is moved from the button to the overlay so it rings the row.
|
|
12
|
+
*/
|
|
13
|
+
readonly pressTarget: string;
|
|
14
|
+
readonly text: "flex min-w-0 flex-col";
|
|
15
|
+
readonly titleLine: "flex min-w-0 items-center gap-2";
|
|
16
|
+
readonly title: "truncate font-medium text-default-900";
|
|
17
|
+
readonly subtitle: "truncate text-default-600";
|
|
18
|
+
readonly tags: "flex shrink-0 items-center gap-1";
|
|
19
|
+
/** Lifted above the stretched overlay so each stays its own control. */
|
|
20
|
+
readonly side: "relative z-10 flex shrink-0 items-center gap-1";
|
|
21
|
+
readonly chevron: "flex shrink-0 text-default-500";
|
|
22
|
+
readonly menuTrigger: "text-default-600";
|
|
23
|
+
readonly avatar: "shrink-0 bg-default-200 text-default-700";
|
|
24
|
+
readonly dangerItem: "text-danger";
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Text overrides for flat chips whose HeroUI tone measures under 4.5:1 on the
|
|
28
|
+
* plain row's hover tint (default-100): primary 4.39:1 and warning 4.03:1 at
|
|
29
|
+
* their `-700` text. One step darker clears it; the others already do.
|
|
30
|
+
*/
|
|
31
|
+
export declare const entityRowTagToneClass: Record<EntityRowTagTone, string | undefined>;
|
|
32
|
+
export declare const entityRowVariantClass: Record<EntityRowVariant, {
|
|
33
|
+
row: string;
|
|
34
|
+
hover: string;
|
|
35
|
+
overlay: string;
|
|
36
|
+
}>;
|
|
37
|
+
export declare const entityRowSizeClass: Record<EntityRowSize, {
|
|
38
|
+
row: string;
|
|
39
|
+
content: string;
|
|
40
|
+
title: string;
|
|
41
|
+
subtitle: string;
|
|
42
|
+
avatar: 'sm' | 'md';
|
|
43
|
+
control: 'md' | 'lg';
|
|
44
|
+
}>;
|
|
45
|
+
export declare const entityRowMenuItemClass: Record<EntityRowSize, string | undefined>;
|
|
46
|
+
export declare const entityListGapClass: Record<NonNullable<EntityListProps['gap']>, string>;
|
|
47
|
+
export declare const entityListClass = "m-0 flex list-none flex-col p-0";
|
|
48
|
+
export declare const entityListSkeletonClass: {
|
|
49
|
+
readonly row: "flex items-center gap-3 px-2";
|
|
50
|
+
readonly avatar: {
|
|
51
|
+
readonly md: "size-8 shrink-0 rounded-full";
|
|
52
|
+
readonly lg: "size-10 shrink-0 rounded-full";
|
|
53
|
+
};
|
|
54
|
+
readonly lines: "flex flex-col gap-2";
|
|
55
|
+
readonly title: "h-4 rounded-md";
|
|
56
|
+
readonly subtitle: "h-3 w-44 rounded-md";
|
|
57
|
+
/** Varied so the placeholder does not read as a ruled grid. */
|
|
58
|
+
readonly titleWidths: readonly ["w-32", "w-28", "w-36", "w-24", "w-40", "w-28"];
|
|
59
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { ElementType, ReactNode } from 'react';
|
|
2
|
+
/** `md` is a desk row (48px), `lg` a touch row (56px). */
|
|
3
|
+
export type EntityRowSize = 'md' | 'lg';
|
|
4
|
+
/** `plain` sits in a list and tints on hover; `bordered` is a standalone hairline card. */
|
|
5
|
+
export type EntityRowVariant = 'plain' | 'bordered';
|
|
6
|
+
export type EntityRowTagTone = 'default' | 'primary' | 'success' | 'warning' | 'danger';
|
|
7
|
+
export interface EntityRowTag {
|
|
8
|
+
key: string;
|
|
9
|
+
label: ReactNode;
|
|
10
|
+
/** Maps to the HeroUI `Chip` color, always `variant="flat"`. */
|
|
11
|
+
tone?: EntityRowTagTone;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* What the row leads with. Always decorative (`aria-hidden`): the title is the
|
|
15
|
+
* row's name. Precedence: `src`, then `icon`, then initials (`initials`, else
|
|
16
|
+
* derived from `name`, else from a string `title`).
|
|
17
|
+
*/
|
|
18
|
+
export interface EntityRowLeading {
|
|
19
|
+
/** Image URL. */
|
|
20
|
+
src?: string;
|
|
21
|
+
/** An icon tile instead of initials (a household, an organization). */
|
|
22
|
+
icon?: ReactNode;
|
|
23
|
+
/** Explicit initials, used verbatim. */
|
|
24
|
+
initials?: string;
|
|
25
|
+
/** Source for derived initials when the title is not the person's name. */
|
|
26
|
+
name?: string;
|
|
27
|
+
/** `circle` for people (default), `square` for a tile. */
|
|
28
|
+
shape?: 'circle' | 'square';
|
|
29
|
+
}
|
|
30
|
+
export interface EntityRowAction {
|
|
31
|
+
key: string;
|
|
32
|
+
label: string;
|
|
33
|
+
icon?: ReactNode;
|
|
34
|
+
tone?: 'default' | 'danger';
|
|
35
|
+
isDisabled?: boolean;
|
|
36
|
+
onAction: () => void;
|
|
37
|
+
/** Defaults to `<row testid>-menu-<key>`. */
|
|
38
|
+
'data-testid'?: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Actions and their label travel together: an overflow trigger is icon-only,
|
|
42
|
+
* so without a label it is announced as "button" and nothing else.
|
|
43
|
+
*/
|
|
44
|
+
type EntityRowActionsProps = {
|
|
45
|
+
actions?: undefined;
|
|
46
|
+
actionsLabel?: undefined;
|
|
47
|
+
} | {
|
|
48
|
+
/** Overflow menu items. An empty array renders no trigger. */
|
|
49
|
+
actions: EntityRowAction[];
|
|
50
|
+
/**
|
|
51
|
+
* Accessible name of the `⋯` trigger and of the menu. Include the
|
|
52
|
+
* subject: "Actions for Chidi Okafor", not "Actions" — a list of
|
|
53
|
+
* these otherwise reads as a column of identical buttons.
|
|
54
|
+
*/
|
|
55
|
+
actionsLabel: string;
|
|
56
|
+
};
|
|
57
|
+
interface EntityRowBaseProps {
|
|
58
|
+
/** The row's accessible name. */
|
|
59
|
+
title: ReactNode;
|
|
60
|
+
/** Second line (contact details, a count, "View household"). */
|
|
61
|
+
subtitle?: ReactNode;
|
|
62
|
+
leading?: EntityRowLeading;
|
|
63
|
+
/** Chips beside the title. */
|
|
64
|
+
tags?: EntityRowTag[];
|
|
65
|
+
/**
|
|
66
|
+
* Makes the whole row a button. The press target is stretched over the
|
|
67
|
+
* row; `trailing` and the actions menu stay their own controls.
|
|
68
|
+
*/
|
|
69
|
+
onPress?: () => void;
|
|
70
|
+
/** Makes the whole row a link (wins over `onPress` for semantics; both fire). */
|
|
71
|
+
href?: string;
|
|
72
|
+
/** Router link component for `href` (Next.js `Link`, React Router `Link`). */
|
|
73
|
+
linkAs?: ElementType;
|
|
74
|
+
/** e.g. `'dialog'` when the row opens a picker. Only for button rows. */
|
|
75
|
+
'aria-haspopup'?: 'dialog' | 'menu' | 'listbox' | true;
|
|
76
|
+
/** Explicit control between the text and the menu ("Select", "Add"). */
|
|
77
|
+
trailing?: ReactNode;
|
|
78
|
+
/** Trailing chevron: the row leads somewhere. */
|
|
79
|
+
showChevron?: boolean;
|
|
80
|
+
/** Overrides the default chevron glyph. */
|
|
81
|
+
chevronIcon?: ReactNode;
|
|
82
|
+
/** Overrides the default `⋯` glyph. */
|
|
83
|
+
actionsIcon?: ReactNode;
|
|
84
|
+
/** Defaults from `DensityProvider`: `touch` → `lg`, otherwise `md`. */
|
|
85
|
+
size?: EntityRowSize;
|
|
86
|
+
variant?: EntityRowVariant;
|
|
87
|
+
/** Disables the press target and the actions trigger (not `trailing`, which the host owns). */
|
|
88
|
+
isDisabled?: boolean;
|
|
89
|
+
/** Container element. Defaults to `li` inside `EntityList`, `div` elsewhere. */
|
|
90
|
+
as?: 'li' | 'div';
|
|
91
|
+
className?: string;
|
|
92
|
+
/**
|
|
93
|
+
* Row container. Derived: `-trigger` (press target), `-menu-trigger`,
|
|
94
|
+
* `-menu`, `-menu-<key>`.
|
|
95
|
+
*/
|
|
96
|
+
'data-testid'?: string;
|
|
97
|
+
}
|
|
98
|
+
export type EntityRowProps = EntityRowBaseProps & EntityRowActionsProps;
|
|
99
|
+
export interface EntityListProps {
|
|
100
|
+
children: ReactNode;
|
|
101
|
+
/** Name the list: `aria-label` or `aria-labelledby` (one is required by a11y review). */
|
|
102
|
+
'aria-label'?: string;
|
|
103
|
+
'aria-labelledby'?: string;
|
|
104
|
+
/** Spacing between rows; `bordered` rows read better apart. */
|
|
105
|
+
gap?: 'none' | 'sm' | 'md';
|
|
106
|
+
className?: string;
|
|
107
|
+
'data-testid'?: string;
|
|
108
|
+
}
|
|
109
|
+
export interface EntityListSkeletonProps {
|
|
110
|
+
/** Announced to assistive tech while the rows load ("Loading contacts"). */
|
|
111
|
+
label: string;
|
|
112
|
+
rows?: number;
|
|
113
|
+
/** Defaults from `DensityProvider`, like `EntityRow`. */
|
|
114
|
+
size?: EntityRowSize;
|
|
115
|
+
className?: string;
|
|
116
|
+
'data-testid'?: string;
|
|
117
|
+
}
|
|
118
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
export type InlineConfirmVariant = 'danger' | 'warning';
|
|
3
|
+
export type InlineConfirmSize = 'sm' | 'md' | 'lg';
|
|
4
|
+
export interface InlineConfirmProps {
|
|
5
|
+
/** The question. Also the accessible name of the alertdialog. */
|
|
6
|
+
message: React.ReactNode;
|
|
7
|
+
/** Optional consequences. Becomes `aria-describedby`. */
|
|
8
|
+
description?: React.ReactNode;
|
|
9
|
+
/** Label for the confirm action. Carry the verb ("Remove"), not "OK". */
|
|
10
|
+
confirmLabel: string;
|
|
11
|
+
/** Label for the cancel action. Default `'Cancel'`. */
|
|
12
|
+
cancelLabel?: string;
|
|
13
|
+
onConfirm: () => void;
|
|
14
|
+
/** Fires from the Cancel button and from Escape. */
|
|
15
|
+
onCancel: () => void;
|
|
16
|
+
/** Drives the surface tone and the confirm colour. Default `'danger'`. */
|
|
17
|
+
variant?: InlineConfirmVariant;
|
|
18
|
+
/** Spinner on confirm; Cancel and Escape locked. Default `false`. */
|
|
19
|
+
isPending?: boolean;
|
|
20
|
+
/** Which button receives focus on mount. Default `'cancel'`. */
|
|
21
|
+
initialFocus?: 'cancel' | 'confirm';
|
|
22
|
+
/** Stop Escape from reaching ancestors (e.g. an enclosing Modal). Default `true`. */
|
|
23
|
+
containEscape?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Button size: `'sm'` 32px (default), `'md'` 40px, `'lg'` 48px for a touch
|
|
26
|
+
* register.
|
|
27
|
+
*/
|
|
28
|
+
size?: InlineConfirmSize;
|
|
29
|
+
/**
|
|
30
|
+
* `'li'` wraps the box in a list item, for swapping it in place of a row of
|
|
31
|
+
* a `ul`/`ol`. Default `'div'`.
|
|
32
|
+
*/
|
|
33
|
+
as?: 'li' | 'div';
|
|
34
|
+
className?: string;
|
|
35
|
+
/** Root test id; the buttons get `<id>-confirm` and `<id>-cancel`. */
|
|
36
|
+
'data-testid'?: string;
|
|
37
|
+
}
|
|
38
|
+
export declare const InlineConfirm: React.FC<InlineConfirmProps>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { InlineConfirmVariant } from './InlineConfirm';
|
|
2
|
+
export declare const inlineConfirmClass: {
|
|
3
|
+
/** The `li` that keeps list semantics when the box replaces a row. */
|
|
4
|
+
readonly item: "list-none";
|
|
5
|
+
/**
|
|
6
|
+
* The alertdialog itself. It only takes focus programmatically, while pending,
|
|
7
|
+
* and then shows the system focus ring when the user is on the keyboard.
|
|
8
|
+
*/
|
|
9
|
+
readonly box: "outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus";
|
|
10
|
+
readonly body: "gap-3 px-4 py-3";
|
|
11
|
+
readonly text: "flex min-w-0 flex-col gap-1";
|
|
12
|
+
readonly message: "text-sm text-gray-900 [overflow-wrap:anywhere]";
|
|
13
|
+
readonly description: "text-sm text-gray-600 [overflow-wrap:anywhere]";
|
|
14
|
+
readonly actions: "flex flex-wrap items-center gap-2";
|
|
15
|
+
};
|
|
16
|
+
export declare const inlineConfirmSurface: Record<InlineConfirmVariant, string>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
import { ModalPanelEntry, ModalPanelsController } from './useModalPanels';
|
|
3
|
+
export interface ModalPanelsProps<TPanel> {
|
|
4
|
+
/** The stack from `useModalPanels()`. */
|
|
5
|
+
panels: ModalPanelsController<TPanel>;
|
|
6
|
+
/** Renders one panel. Called for every entry on the stack; only the top one is shown. */
|
|
7
|
+
children: (panel: TPanel, entry: ModalPanelEntry<TPanel>) => React.ReactNode;
|
|
8
|
+
}
|
|
9
|
+
export declare function ModalPanels<TPanel>({ panels, children }: ModalPanelsProps<TPanel>): React.ReactElement;
|
|
10
|
+
export interface ModalPanelHeaderProps {
|
|
11
|
+
/** Panel title, rendered as an `h2`. */
|
|
12
|
+
title: React.ReactNode;
|
|
13
|
+
/**
|
|
14
|
+
* Where Back goes. Default: the panel beneath, from the stack. Either way
|
|
15
|
+
* it goes through the discard guard, scoped to this panel.
|
|
16
|
+
*/
|
|
17
|
+
onBack?: () => void;
|
|
18
|
+
/** Back's accessible name. Default `'Back'`. */
|
|
19
|
+
backLabel?: string;
|
|
20
|
+
isBackDisabled?: boolean;
|
|
21
|
+
/** Back button size: `'md'` 40px, `'lg'` 48px (touch registers). Default `'md'`. */
|
|
22
|
+
size?: 'md' | 'lg';
|
|
23
|
+
/** Content at the end of the header, before the modal's close button. */
|
|
24
|
+
actions?: React.ReactNode;
|
|
25
|
+
className?: string;
|
|
26
|
+
/** Root test id; derives `-back` and `-title`. Default `'modal-panel-header'`. */
|
|
27
|
+
'data-testid'?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Back + title for a panel pushed over the first one. Back is navigation, not
|
|
31
|
+
* one of the footer's decisions, so it lives top-left where it never moves.
|
|
32
|
+
* It is shown when there is somewhere to go back to (or `onBack` is given),
|
|
33
|
+
* and it is the element focus lands on when the panel opens.
|
|
34
|
+
*/
|
|
35
|
+
export declare const ModalPanelHeader: React.FC<ModalPanelHeaderProps>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface ModalPanelContextValue {
|
|
2
|
+
isActive: boolean;
|
|
3
|
+
canGoBack: boolean;
|
|
4
|
+
back: () => void;
|
|
5
|
+
setEntryFocus: (node: HTMLElement | null) => void;
|
|
6
|
+
}
|
|
7
|
+
export declare const ModalPanelContext: import('react').Context<ModalPanelContextValue | null>;
|
|
8
|
+
export interface ModalPanelState {
|
|
9
|
+
/** True for the panel on screen. */
|
|
10
|
+
isActive: boolean;
|
|
11
|
+
/** True when a panel lies beneath this one. */
|
|
12
|
+
canGoBack: boolean;
|
|
13
|
+
/** Returns to the panel beneath — asking first if this panel holds unsaved input. */
|
|
14
|
+
back: () => void;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The enclosing panel, for controls other than the header's Back — a footer
|
|
18
|
+
* Cancel, say. Outside `ModalPanels` it reports a lone, active panel.
|
|
19
|
+
*/
|
|
20
|
+
export declare function useModalPanel(): ModalPanelState;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** First element in `container` a Tab press would reach, skipping anything inside a hidden or inert subtree. */
|
|
2
|
+
export declare function firstTabbable(container: HTMLElement): HTMLElement | null;
|
|
3
|
+
/** Still in `container` and still able to take focus — not removed, disabled or hidden since it was recorded. */
|
|
4
|
+
export declare function canStillFocus(element: HTMLElement | null, container: HTMLElement): element is HTMLElement;
|
|
5
|
+
/**
|
|
6
|
+
* A popup (menu, listbox) currently open from a trigger inside `container`,
|
|
7
|
+
* found through the trigger's own ARIA wiring: `aria-expanded="true"` plus
|
|
8
|
+
* `aria-controls` naming the popup.
|
|
9
|
+
*/
|
|
10
|
+
export declare function openPopupFrom(container: HTMLElement): HTMLElement | null;
|
|
11
|
+
/**
|
|
12
|
+
* Fired by react-aria's `FocusScope` on the element it is about to restore
|
|
13
|
+
* focus to when an overlay (a menu, a select, a dialog) unmounts; cancelable,
|
|
14
|
+
* and bubbles from that element. It is react-aria's own seam for redirecting a
|
|
15
|
+
* restore, and the reason no timer is needed here (see the file header).
|
|
16
|
+
*/
|
|
17
|
+
export declare const FOCUS_RESTORE_EVENT = "react-aria-focus-scope-restore";
|
|
18
|
+
/** DOM handles per panel key, and which panel is on screen. Plain bookkeeping; never read during render. */
|
|
19
|
+
export declare class PanelFocusMemory {
|
|
20
|
+
private readonly panels;
|
|
21
|
+
private active;
|
|
22
|
+
setContainer(key: string, node: HTMLElement | null): void;
|
|
23
|
+
setEntry(key: string, node: HTMLElement | null): void;
|
|
24
|
+
remember(key: string, node: HTMLElement): void;
|
|
25
|
+
/** The panel now on screen, and whether the operator came back to it (vs. opened it). */
|
|
26
|
+
setActive(key: string, isReturn: boolean): void;
|
|
27
|
+
/**
|
|
28
|
+
* Puts focus in the panel on screen, unless it is already there. Returning:
|
|
29
|
+
* the element that last had focus in it — the opener of the panel just left.
|
|
30
|
+
* Entering, or when that element is gone: Back, then the first tabbable element.
|
|
31
|
+
*/
|
|
32
|
+
focusActive(): void;
|
|
33
|
+
/**
|
|
34
|
+
* Focus was dropped by an overlay closing over a hidden panel. If the
|
|
35
|
+
* operator has since opened a menu or select in the panel on screen, that
|
|
36
|
+
* is where they are; otherwise it is the panel's own target.
|
|
37
|
+
*/
|
|
38
|
+
recoverFocus(): void;
|
|
39
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { ModalPanels, ModalPanelHeader } from './ModalPanels';
|
|
2
|
+
export type { ModalPanelsProps, ModalPanelHeaderProps } from './ModalPanels';
|
|
3
|
+
export { useModalPanel } from './context';
|
|
4
|
+
export type { ModalPanelState } from './context';
|
|
5
|
+
export { useModalPanels } from './useModalPanels';
|
|
6
|
+
export type { ModalPanelsController, ModalPanelEntry } from './useModalPanels';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ModalPanelHeader slots. The header mirrors HeroUI's `ModalHeader` slot
|
|
3
|
+
* (`flex py-4 px-6 flex-initial text-large font-semibold`) so a panel header
|
|
4
|
+
* and the first panel's `ModalHeader` line up when they swap.
|
|
5
|
+
*
|
|
6
|
+
* `pl-4`, not `px-6`: the Back button's hit area is wider than its 20px glyph,
|
|
7
|
+
* so starting it 16px in puts the arrow on the 24px content edge. `pr-12`
|
|
8
|
+
* leaves the corner to HeroUI's absolutely-positioned close button.
|
|
9
|
+
*/
|
|
10
|
+
export declare const modalPanelHeaderClass = "flex flex-initial items-center gap-2 py-4 pl-4 pr-12 text-large font-semibold";
|
|
11
|
+
export declare const modalPanelTitleClass = "min-w-0 flex-1 truncate text-large font-semibold";
|
|
12
|
+
export declare const modalPanelActionsClass = "flex shrink-0 items-center gap-2";
|
|
13
|
+
export declare const modalPanelBackIconClass = "size-5";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** One panel on the stack. `key` is unique per visit, so pushing the same panel twice mounts it twice. */
|
|
2
|
+
export interface ModalPanelEntry<TPanel> {
|
|
3
|
+
readonly key: string;
|
|
4
|
+
readonly panel: TPanel;
|
|
5
|
+
}
|
|
6
|
+
export interface ModalPanelsController<TPanel> {
|
|
7
|
+
/** The panel on top — the one on screen. */
|
|
8
|
+
readonly current: TPanel;
|
|
9
|
+
/** Bottom to top. Every entry stays mounted; only the last one is shown. */
|
|
10
|
+
readonly entries: readonly ModalPanelEntry<TPanel>[];
|
|
11
|
+
/** True when there is a panel beneath the current one. */
|
|
12
|
+
readonly canGoBack: boolean;
|
|
13
|
+
/** Opens `panel` on top; Back returns to the one showing now. */
|
|
14
|
+
push: (panel: TPanel) => void;
|
|
15
|
+
/** Returns to the panel beneath. No-op on the first panel. */
|
|
16
|
+
back: () => void;
|
|
17
|
+
/** Swaps the top panel for `panel` (e.g. a saved form for its detail); Back skips the old one. */
|
|
18
|
+
replace: (panel: TPanel) => void;
|
|
19
|
+
/** Starts over with a single fresh panel — `initial` unless another is given. */
|
|
20
|
+
reset: (panel?: TPanel) => void;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The back-stack behind `ModalPanels`: which panel is on screen, and where Back
|
|
24
|
+
* goes from it. Back goes where the operator came from because the stack
|
|
25
|
+
* remembers it — no per-panel "return to" flags to keep in step.
|
|
26
|
+
*
|
|
27
|
+
* Owned by the component that owns the modal, so it outlives the modal's
|
|
28
|
+
* content; call `reset()` when the modal closes to start the next visit fresh.
|
|
29
|
+
*/
|
|
30
|
+
export declare function useModalPanels<TPanel>(initial: TPanel): ModalPanelsController<TPanel>;
|
|
@@ -3,6 +3,12 @@ export interface PaginationSummaryClassNames {
|
|
|
3
3
|
base?: string;
|
|
4
4
|
emphasis?: string;
|
|
5
5
|
}
|
|
6
|
+
/** The range `format` receives; `from` and `to` are both 0 when there is nothing to show. */
|
|
7
|
+
export interface PaginationSummaryRange {
|
|
8
|
+
from: number;
|
|
9
|
+
to: number;
|
|
10
|
+
total: number;
|
|
11
|
+
}
|
|
6
12
|
export interface PaginationSummaryProps {
|
|
7
13
|
/** Current page, 1-based. */
|
|
8
14
|
page: number;
|
|
@@ -19,6 +25,12 @@ export interface PaginationSummaryProps {
|
|
|
19
25
|
* avoid reading the change twice.
|
|
20
26
|
*/
|
|
21
27
|
announce?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Writes the sentence instead of the built-in English "Showing {range} of
|
|
30
|
+
* {total} {unit}". Use it to translate; `unit` and `classNames.emphasis`
|
|
31
|
+
* do not apply, since the returned node is the whole sentence.
|
|
32
|
+
*/
|
|
33
|
+
format?: (range: PaginationSummaryRange) => React.ReactNode;
|
|
22
34
|
className?: string;
|
|
23
35
|
classNames?: PaginationSummaryClassNames;
|
|
24
36
|
'data-testid'?: string;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { PaginationSummary } from './PaginationSummary';
|
|
2
|
-
export type { PaginationSummaryProps, PaginationSummaryClassNames } from './PaginationSummary';
|
|
2
|
+
export type { PaginationSummaryProps, PaginationSummaryClassNames, PaginationSummaryRange } from './PaginationSummary';
|
|
3
3
|
export { pageRange } from './page-range';
|