@ntmats/hc-ui 0.5.0 → 0.7.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 CHANGED
@@ -50,6 +50,19 @@ Both open below the trigger, aligned by its end, 4px away (`side="bottom|top"`,
50
50
 
51
51
  `SortMenu` (experimental) is HCUI's desktop sort control: a `Chip` (`size="sm"` by default, `triggerClassName` for a class) showing the current option, named "`label`: option", that opens a `Menu` with a `MenuRadioGroup` of `options` (`value`, `label`, optional `hint` and `disabled`), with the `Menu` keyboard and focus. `applied` marks a choice in effect with the ring; other `Menu` props (`align`, `side`, `portal`, `open`, `onOpenChange`) pass through. On phones, compose instead: `IconButton variant="outline" size="lg"` with `aria-haspopup="dialog"` opens a `Sheet` with `SheetOptions` (its card look, as every sheet list).
52
52
 
53
+ Options for HCUI's remaining local pieces (unreleased, experimental; opt-in, except the visible changes listed in the CHANGELOG):
54
+
55
+ - `Button`: `variant="ghost"` (no fill, only the hover and press veil) and `tone="danger"` (danger text on `secondary`, `outline` and `ghost`), e.g. `variant="ghost" tone="danger"` for a text-only destructive action. As a popup trigger (`aria-haspopup`) that is open (`aria-expanded="true"`), `secondary`, `outline` and `ghost` draw the 1.5px ring in the text color, as `Chip`.
56
+ - `IconButton`: `size="xs"` (28px, keeping a 44px touch target) and `tone="muted"` (text-muted). A `ghost` popup trigger keeps its hover fill while open.
57
+ - `Chip`: `variant="muted"` (gray fill without ring; with `onClear` at `md`/`sm`, the main button fills the height and the × is 26px, 5px from the end), `badgePlacement="corner"` (the badge on the top right corner, brand fill, a 2px ring of `--hc-chip-badge-ring`, default `bg-page`), a chip with an `icon` and no label is a circle, and `informative` (disabled but not dimmed).
58
+ - `SheetChips`: `size`, passed to every `Chip`.
59
+ - `Dialog` and `Sheet`: `bodyClassName` (the scrolling body's class). `Sheet header={false}` drops the header (no title, no close button) for content with its own heading and floating close; `aria-label` names the dialog (required by the type), and the body has no padding. The entry animations no longer leave a transform on the dialog, so fixed popups portaled into it are not clipped; a touch inside a portaled popup (`.hc-ui-portal`) does not drag the sheet.
60
+ - `portalContainerFor(node)`: the open `<dialog>` around `node`, else `document.body` (null on the server). Use it for the app's own tips, so they show above a modal `Dialog` or `Sheet` and receive touches instead of staying behind the top layer; `Menu` and `Popover` with `portal` already do this.
61
+ - `Surface`: `as` (`div`, `section`, `article`, `aside`, `ul`, `ol`, `li`) and `tone="muted"` (the `bg-muted-2` card: 20px corners, `shadow-card`).
62
+ - `FilterBar`: `bleed` (negative margin of `--hc-filter-bar-bleed-start`/`-end`, default `--hc-filter-bar-bleed`, given back as padding) and `rowClassName`; the row is `position: relative`, so an absolute popup placed in it is anchored to it.
63
+ - `Dialog size="sm"` no longer forces 38px buttons: they keep their own size.
64
+ - `SegmentedControl`: `optionMinWidth` (px).
65
+
53
66
  `Table` (experimental) is a dense data table on a semantic `<table>`: HCUI's desktop Meta table. `columns` describe it (`key`, `header`, `cell(row, index)` with free content, `align` `start`/`center`/`end`, `numeric` for end-aligned tabular figures, `width` in px or any CSS length, `tone` `muted` or `secondary` for the header and cells, `className` for the header and every cell, such as a class that hides an optional column in a narrow container, and `rowHeader` for the cell that names the row, rendered as `<th scope="row">`); `rows` and `rowKey` give the data, already filtered, formatted and in order. A `sortable` column puts its header text in a button and carries `aria-sort` (`none` until sorted); `sort` (`{ key, direction }`) marks the sorted column, which shows a 12px arrow, and `onSort(key)` reports a press: the new order (toggle, default direction) and sorting the rows are the caller's. `stickyHeader` sticks the header row to the top of the scrolling ancestor at `--hc-table-sticky-top` (HCUI: under its filter band). `onRowClick` makes the rows react to a press and puts the row header's content in a button, so the keyboard reaches each row once (its press does not also reach the row); give a `rowHeader` column, or rows can only be pressed with a pointer. `selectedKey` fills the row open elsewhere (a side panel) with `bg-muted` and sets `aria-current` on its button. Rows are 4px 8px cells with a line under each and the `bg-muted-2` hover; the header is 11px muted text on the surface fill with a line under it. Focus rings are the shared 2px ring unless `--hc-focus-ring-offset` is set. Empty states, pagination ("show more") and the cell content styles are the caller's. It forwards its ref to the `<table>`.
54
67
 
55
68
  `PageHeader` (experimental) is HCUI's desktop page heading: a row of at least 40px with the `title` (22px semibold on a 27.5px line, pulled up 4px to drop the font's inner space; an `h1` by default, `level` 2 or 3 change only the tag), an optional `count` in muted 13px text inside the heading after a space (hidden at 0) and `actions` at the end, 8px apart. The row wraps when it does not fit. `visuallyHidden` keeps the heading only for assistive technology, for a page whose content already shows its title. Spacing around it is the caller's. As in HCUI, the title's font is set with the `font` shorthand, which drops the `ROND` axis: this reproduces HCUI's look. It forwards its ref to the `<header>`.
package/dist/index.d.ts CHANGED
@@ -1,13 +1,18 @@
1
- import { type ButtonHTMLAttributes, type Dispatch, type HTMLAttributes, type InputHTMLAttributes, type KeyboardEvent, type MouseEvent, type ReactElement, type ReactNode, type Ref, type SetStateAction } from 'react';
2
- export type SurfaceTone = 'page' | 'main' | 'content' | 'elevated' | 'overlay' | 'card';
3
- export interface SurfaceProps extends HTMLAttributes<HTMLDivElement> {
1
+ import { type ButtonHTMLAttributes, type Dispatch, type ForwardRefExoticComponent, type RefAttributes, type HTMLAttributes, type InputHTMLAttributes, type KeyboardEvent, type MouseEvent, type ReactElement, type ReactNode, type Ref, type SetStateAction } from 'react';
2
+ export type SurfaceTone = 'page' | 'main' | 'content' | 'elevated' | 'overlay' | 'card' | 'muted';
3
+ /** Elements a `Surface` may render as. */
4
+ export type SurfaceElement = 'div' | 'section' | 'article' | 'aside' | 'ul' | 'ol' | 'li';
5
+ export interface SurfaceProps extends HTMLAttributes<HTMLElement> {
4
6
  tone?: SurfaceTone;
5
- /** Corner radius of the rounded tones (`content`, `elevated`, `overlay`, `card`): 16, 20 or 24px.
6
- * Defaults to `md` (16px), and to `lg` (20px) for `card`, as HCUI's `surface` mixin. */
7
+ /** Corner radius of the rounded tones (`content`, `elevated`, `overlay`, `card`, `muted`): 16, 20 or 24px.
8
+ * Defaults to `md` (16px), and to `lg` (20px) for `card` and `muted`, as HCUI's `surface` mixin. */
7
9
  radius?: 'md' | 'lg' | 'xl';
10
+ /** Element to render: `div` (default), or `section`, `article`, `aside`, `ul`, `ol`, `li`
11
+ * (a list drawn as a card). Lists keep the browser's margin, padding and markers: reset them in the caller's class. */
12
+ as?: SurfaceElement;
8
13
  }
9
14
  /** Semantic color role only; placement, elevation and overlay behavior stay local. */
10
- export declare function Surface({ tone, radius, className, ...props }: SurfaceProps): import("react/jsx-runtime").JSX.Element;
15
+ export declare function Surface({ tone, radius, as: Element, className, ...props }: SurfaceProps): import("react/jsx-runtime").JSX.Element;
11
16
  export interface SegmentedOption<T extends string> {
12
17
  value: T;
13
18
  label: string;
@@ -31,12 +36,24 @@ export interface SegmentedControlProps<T extends string> extends Omit<HTMLAttrib
31
36
  equalWidth?: boolean;
32
37
  /** Fixed width of each option in px; implies `equalWidth`. */
33
38
  optionWidth?: number;
39
+ /** Minimum width of each option in px (HCUI's 92px Coleções/Unidades toggle); a longer label still widens its option. */
40
+ optionMinWidth?: number;
34
41
  }
35
42
  /** Controlled, single-choice control with radio semantics and roving focus. */
36
- export declare function SegmentedControl<T extends string>({ label, value, options, onChange, density, orientation, hideLabels, indicator, equalWidth, optionWidth, className, style, ...props }: SegmentedControlProps<T>): import("react/jsx-runtime").JSX.Element;
37
- export type ButtonVariant = 'secondary' | 'primary' | 'outline' | 'danger';
43
+ export declare function SegmentedControl<T extends string>({ label, value, options, onChange, density, orientation, hideLabels, indicator, equalWidth, optionWidth, optionMinWidth, className, style, ...props }: SegmentedControlProps<T>): import("react/jsx-runtime").JSX.Element;
44
+ export type ButtonVariant = 'secondary' | 'primary' | 'outline' | 'danger' | 'ghost';
38
45
  export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
46
+ /**
47
+ * `secondary` (default, muted fill), `primary`, `outline`, `danger` (filled) or `ghost`
48
+ * (no fill, only the hover and press veil). A popup trigger (`aria-haspopup`) that is open
49
+ * (`aria-expanded="true"`) draws a 1.5px ring in the text color, as `Chip` (not on `primary` and `danger`).
50
+ */
39
51
  variant?: ButtonVariant;
52
+ /**
53
+ * Text color of the unfilled variants (`secondary`, `outline`, `ghost`): `default` or `danger`
54
+ * (HCUI's "Fechar e descartar alterações": `variant="ghost" tone="danger"`). Ignored by `primary` and `danger`.
55
+ */
56
+ tone?: 'default' | 'danger';
40
57
  /** 36, 44 or 52px. */
41
58
  size?: 'sm' | 'md' | 'lg';
42
59
  /** Decorative icon before the label; pass it already sized (18px in HCUI). */
@@ -53,17 +70,19 @@ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
53
70
  wrap?: boolean;
54
71
  }
55
72
  /** Pill button. The caller supplies the label text and handles the action. */
56
- export declare const Button: import("react").ForwardRefExoticComponent<ButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
73
+ export declare const Button: ForwardRefExoticComponent<ButtonProps & RefAttributes<HTMLButtonElement>>;
57
74
  export interface IconButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'children' | 'aria-label'> {
58
75
  /** Accessible name and tooltip; required because the button has no text. */
59
76
  label: string;
60
77
  icon: ReactNode;
61
- /** 36, 40 or 44px. The 36px size keeps a 44px touch target. */
62
- size?: 'sm' | 'md' | 'lg';
63
- tone?: 'primary' | 'secondary';
78
+ /** 28 (`xs`, dense toolbars and ⋯ triggers), 36, 40 or 44px. `xs` and `sm` keep a 44px touch target. */
79
+ size?: 'xs' | 'sm' | 'md' | 'lg';
80
+ /** Icon color: `primary` (default), `secondary` or `muted` (text-muted, HCUI's ⋯ triggers). */
81
+ tone?: 'primary' | 'secondary' | 'muted';
64
82
  /** Fill: `muted` (default, HCUI `icon-button`), `surface` (surface with the card ring
65
83
  * and shadow, HCUI's mobile theme button), `outline` (surface with a 1px border ring,
66
- * HCUI's mobile sort button) or `ghost` (no fill until hover). */
84
+ * HCUI's mobile sort button) or `ghost` (no fill until hover; an open popup trigger,
85
+ * `aria-haspopup` with `aria-expanded="true"`, keeps the hover look). */
67
86
  variant?: 'muted' | 'surface' | 'outline' | 'ghost';
68
87
  /** `circle` (default) or `rounded` (a 12px-radius square, HCUI's desktop utilities). */
69
88
  shape?: 'circle' | 'rounded';
@@ -74,7 +93,7 @@ export interface IconButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonEle
74
93
  iconKey?: string | number;
75
94
  }
76
95
  /** Round icon-only button (back, close, options). */
77
- export declare const IconButton: import("react").ForwardRefExoticComponent<IconButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
96
+ export declare const IconButton: ForwardRefExoticComponent<IconButtonProps & RefAttributes<HTMLButtonElement>>;
78
97
  interface ChipBaseProps extends ButtonHTMLAttributes<HTMLButtonElement> {
79
98
  /** Toggle state for filters; omit for a plain action chip. */
80
99
  selected?: boolean;
@@ -85,8 +104,12 @@ interface ChipBaseProps extends ButtonHTMLAttributes<HTMLButtonElement> {
85
104
  * desktop quick filters and chosen-value tags); selected turns to the contrast fill.
86
105
  */
87
106
  size?: 'md' | 'sm' | 'xs';
88
- /** `outline` (default), or `quiet`: lighter ring and secondary text, bolder once applied (HCUI's rule filter pills). */
89
- variant?: 'outline' | 'quiet';
107
+ /**
108
+ * `outline` (default); `quiet`: lighter ring and secondary text, bolder once applied (HCUI's rule
109
+ * filter pills); `muted`: gray fill without ring (HCUI's search conditions). With `onClear`, a
110
+ * `muted` chip of `md` or `sm` draws its × at 26px, concentric with the pill's end.
111
+ */
112
+ variant?: 'outline' | 'quiet' | 'muted';
90
113
  /** A filter with values applied: a 1.5px ring in the primary text color (and primary, bolder text when `quiet`). */
91
114
  applied?: boolean;
92
115
  /**
@@ -99,6 +122,17 @@ interface ChipBaseProps extends ButtonHTMLAttributes<HTMLButtonElement> {
99
122
  * `undefined` (e.g. `badge={n || undefined}`) to hide it when nothing is applied.
100
123
  */
101
124
  badge?: ReactNode;
125
+ /**
126
+ * Where the badge goes: `inline` (default, after the label) or `corner` (on the top right
127
+ * corner, in the brand color with a ring of `--hc-chip-badge-ring`, default `bg-page`; HCUI's
128
+ * mobile Meta filter button). A chip with an `icon` and no label is a circle.
129
+ */
130
+ badgePlacement?: 'inline' | 'corner';
131
+ /**
132
+ * Information, not an action: the chip cannot be pressed (the button is disabled) but keeps
133
+ * its full color instead of dimming (HCUI's Meta era chip when only one era fits the format).
134
+ */
135
+ informative?: boolean;
102
136
  /**
103
137
  * Down chevron at the end. On by default when the chip opens a popup (`aria-haspopup`,
104
138
  * which `Menu` and `Popover` give their trigger); `aria-expanded="true"` draws the ring.
@@ -127,7 +161,7 @@ export type ChipProps = ChipBaseProps & ({
127
161
  * trigger it takes a `badge` with the number of values applied, `applied`, and the
128
162
  * chevron and ring of an open popup; `onClear` adds a remove button.
129
163
  */
130
- export declare const Chip: import("react").ForwardRefExoticComponent<ChipProps & import("react").RefAttributes<HTMLButtonElement>>;
164
+ export declare const Chip: ForwardRefExoticComponent<ChipProps & RefAttributes<HTMLButtonElement>>;
131
165
  export interface FilterBarProps extends HTMLAttributes<HTMLDivElement> {
132
166
  /** Accessible name of the group (e.g. "Filtros"). */
133
167
  label: string;
@@ -147,13 +181,25 @@ export interface FilterBarProps extends HTMLAttributes<HTMLDivElement> {
147
181
  density?: 'comfortable' | 'compact';
148
182
  /** A band that sticks to the top of the scrolling panel, with the surface fill and 8px above and below (HCUI's desktop filter bar). The offset is `--hc-filter-bar-top`. */
149
183
  sticky?: boolean;
184
+ /**
185
+ * The bar reaches the panel's edges: a negative margin of `--hc-filter-bar-bleed-start` and
186
+ * `--hc-filter-bar-bleed-end` (both default to `--hc-filter-bar-bleed`, else 0) given back as
187
+ * padding, so the content stays in place and a sticky band covers the full width (HCUI's
188
+ * `.desktop-filter-bar`, which content bleeding to the edge passes under).
189
+ */
190
+ bleed?: boolean;
191
+ /**
192
+ * Class of the row that holds the filters. The row is `position: relative`, so a popup the
193
+ * caller places with `position: absolute` inside it is anchored to the row (HCUI's "Mais filtros").
194
+ */
195
+ rowClassName?: string;
150
196
  }
151
197
  /**
152
198
  * Layout of a row of filters (`Chip`s) with optional `start`, `end` and `footer` slots.
153
199
  * Presentation only: which filters exist, their values, the search rules and persistence
154
200
  * are the caller's. A scrolling row bleeds into the page gutter by `--hc-filter-bar-bleed`.
155
201
  */
156
- export declare const FilterBar: import("react").ForwardRefExoticComponent<FilterBarProps & import("react").RefAttributes<HTMLDivElement>>;
202
+ export declare const FilterBar: ForwardRefExoticComponent<FilterBarProps & RefAttributes<HTMLDivElement>>;
157
203
  export interface FieldProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {
158
204
  /** Accessible name. Visible above the field unless `hideLabel` is set. */
159
205
  label: string;
@@ -169,7 +215,7 @@ export interface FieldProps extends Omit<InputHTMLAttributes<HTMLInputElement>,
169
215
  error?: ReactNode;
170
216
  }
171
217
  /** Filled pill text field. Value, validation and messages stay with the caller. */
172
- export declare const Field: import("react").ForwardRefExoticComponent<FieldProps & import("react").RefAttributes<HTMLInputElement>>;
218
+ export declare const Field: ForwardRefExoticComponent<FieldProps & RefAttributes<HTMLInputElement>>;
173
219
  export interface SkeletonProps extends HTMLAttributes<HTMLSpanElement> {
174
220
  width?: string | number;
175
221
  height?: string | number;
@@ -218,7 +264,7 @@ export type MeterProps = MeterOwnProps & (
218
264
  * Bar of a value against a limit (`role="meter"`), with an over-the-limit tone. It
219
265
  * renders only spans, so it fits inside a button. Placement (absolute, grid) is the caller's.
220
266
  */
221
- export declare const Meter: import("react").ForwardRefExoticComponent<MeterProps & import("react").RefAttributes<HTMLSpanElement>>;
267
+ export declare const Meter: ForwardRefExoticComponent<MeterProps & RefAttributes<HTMLSpanElement>>;
222
268
  export interface CheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size' | 'type'> {
223
269
  /** 20px (touch lists, sheets) or 16px (dense desktop lists). */
224
270
  size?: 'sm' | 'md';
@@ -230,7 +276,7 @@ export interface CheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement
230
276
  * and `<label>` behave as usual. Wrap it in a `<label>` (the whole row becomes the
231
277
  * target) or pass `aria-label`; the caller supplies the text.
232
278
  */
233
- export declare const Checkbox: import("react").ForwardRefExoticComponent<CheckboxProps & import("react").RefAttributes<HTMLInputElement>>;
279
+ export declare const Checkbox: ForwardRefExoticComponent<CheckboxProps & RefAttributes<HTMLInputElement>>;
234
280
  export interface DialogProps extends Omit<HTMLAttributes<HTMLDialogElement>, 'title' | 'open' | 'onClose' | 'onCancel'> {
235
281
  /** Controlled: the dialog shows while `open` is true and plays its exit before unmounting. */
236
282
  open: boolean;
@@ -254,17 +300,17 @@ export interface DialogProps extends Omit<HTMLAttributes<HTMLDialogElement>, 'ti
254
300
  size?: 'sm' | 'md';
255
301
  /** Esc and a backdrop click ask to close. Default true. */
256
302
  dismissible?: boolean;
303
+ /** Class of the body that holds the children (and scrolls in `md`), e.g. to reserve the scrollbar's gutter. */
304
+ bodyClassName?: string;
257
305
  }
258
306
  /**
259
307
  * Centered modal dialog on the native `<dialog>` and `showModal()`: the browser
260
308
  * keeps it in the top layer, makes the rest of the page inert and traps Tab.
261
309
  * The caller owns `open` and the texts; the dialog only asks to close.
262
310
  */
263
- export declare const Dialog: import("react").ForwardRefExoticComponent<DialogProps & import("react").RefAttributes<HTMLDialogElement>>;
311
+ export declare const Dialog: ForwardRefExoticComponent<DialogProps & RefAttributes<HTMLDialogElement>>;
264
312
  export type SheetPresentation = 'bottom' | 'center' | 'auto';
265
- export interface SheetProps extends Omit<DialogProps, 'size' | 'actions' | 'description' | 'closeLabel'> {
266
- /** Accessible name of the close button, which every sheet shows. */
267
- closeLabel: string;
313
+ interface SheetBaseProps extends Omit<DialogProps, 'size' | 'actions' | 'description' | 'closeLabel' | 'title'> {
268
314
  /** Slot under the scrolling body, for the buttons (Clear | Done). */
269
315
  footer?: ReactNode;
270
316
  /**
@@ -276,6 +322,25 @@ export interface SheetProps extends Omit<DialogProps, 'size' | 'actions' | 'desc
276
322
  /** Maximum width of the centered panel, in px. Default 440. */
277
323
  width?: number;
278
324
  }
325
+ /**
326
+ * `Sheet` props. By default the sheet has a header with the title (its accessible name) and a
327
+ * close button. `header={false}` drops both: the content brings its own heading and floating
328
+ * close button (HCUI's unit sheet), and `aria-label` names the dialog. The body then has no
329
+ * padding, and at the bottom the grabber (drawn above the content) covers its top 16px.
330
+ */
331
+ export type SheetProps = SheetBaseProps & ({
332
+ header?: true;
333
+ /** Visible heading; it names the sheet. */
334
+ title: ReactNode;
335
+ /** Accessible name of the close button, which every sheet with a header shows. */
336
+ closeLabel: string;
337
+ } | {
338
+ header: false;
339
+ 'aria-label': string;
340
+ title?: never;
341
+ closeLabel?: never;
342
+ closeIcon?: never;
343
+ });
279
344
  /**
280
345
  * Modal sheet on the `Dialog` base (native `<dialog>`, `showModal()`): the same focus,
281
346
  * Tab trap, Esc, focus return, scroll lock, stacking and controlled `open`/`onClose`.
@@ -283,7 +348,7 @@ export interface SheetProps extends Omit<DialogProps, 'size' | 'actions' | 'desc
283
348
  * flick) and drag up from the grabber or header to full height; content that has
284
349
  * scrolled down scrolls back before the sheet moves.
285
350
  */
286
- export declare const Sheet: import("react").ForwardRefExoticComponent<SheetProps & import("react").RefAttributes<HTMLDialogElement>>;
351
+ export declare const Sheet: ForwardRefExoticComponent<SheetProps & RefAttributes<HTMLDialogElement>>;
287
352
  export interface SheetOption<T extends string> {
288
353
  value: T;
289
354
  label: ReactNode;
@@ -315,9 +380,11 @@ export interface SheetChipsProps<T extends string> extends SheetChoiceProps<T> {
315
380
  label: ReactNode;
316
381
  disabled?: boolean;
317
382
  }[];
383
+ /** Size of every chip, as `Chip`'s: `md` (default), `sm` or `xs`. */
384
+ size?: ChipProps['size'];
318
385
  }
319
386
  /** Single-choice group of `Chip`s that wrap (HCUI's SheetChips), for inside a sheet. */
320
- export declare function SheetChips<T extends string>({ label, options, value, onChange, className, ...props }: SheetChipsProps<T>): import("react/jsx-runtime").JSX.Element;
387
+ export declare function SheetChips<T extends string>({ label, options, value, onChange, size, className, ...props }: SheetChipsProps<T>): import("react/jsx-runtime").JSX.Element;
321
388
  export type PopupSide = 'bottom' | 'top';
322
389
  export type PopupAlign = 'start' | 'end';
323
390
  /** Props a trigger receives: spread them on the element that opens the popup. */
@@ -368,6 +435,14 @@ export declare function placePopup(trigger: Rect, size: {
368
435
  side: PopupSide;
369
436
  align: PopupAlign;
370
437
  };
438
+ /**
439
+ * Where to portal something that floats over `node` (a tip, a tooltip): the open `<dialog>`
440
+ * around it, else `document.body`. A modal dialog (`Dialog`, `Sheet`) is in the top layer and
441
+ * makes the rest of the page inert, so a tip portaled to `body` from inside it would stay
442
+ * behind it and get no touches; inside the dialog it shows above it. `Menu` and `Popover`
443
+ * with `portal` already do this. On the server (no document) it returns null.
444
+ */
445
+ export declare function portalContainerFor(node: Element | null | undefined): HTMLElement | null;
371
446
  export interface MenuProps extends PopupBaseProps, Omit<HTMLAttributes<HTMLDivElement>, 'role' | 'children'> {
372
447
  /** Accessible name of the menu (`aria-label`). */
373
448
  label: string;
@@ -381,7 +456,7 @@ export interface MenuProps extends PopupBaseProps, Omit<HTMLAttributes<HTMLDivEl
381
456
  * typing move between items; Enter/Space choose; Esc closes and returns focus to the
382
457
  * trigger; Tab closes and moves on; a press outside closes.
383
458
  */
384
- export declare const Menu: import("react").ForwardRefExoticComponent<MenuProps & import("react").RefAttributes<HTMLDivElement>>;
459
+ export declare const Menu: ForwardRefExoticComponent<MenuProps & RefAttributes<HTMLDivElement>>;
385
460
  interface MenuItemBaseProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'role' | 'onSelect'> {
386
461
  /** Decorative icon at the end of the row; pass it already sized (15px in HCUI). */
387
462
  icon?: ReactNode;
@@ -395,7 +470,7 @@ export interface MenuItemProps extends MenuItemBaseProps {
395
470
  onSelect?: (event: MouseEvent<HTMLButtonElement>) => void;
396
471
  }
397
472
  /** An action (`menuitem`). It closes the menu; focus returns to the trigger unless the action moved it. */
398
- export declare const MenuItem: import("react").ForwardRefExoticComponent<MenuItemProps & import("react").RefAttributes<HTMLButtonElement>>;
473
+ export declare const MenuItem: ForwardRefExoticComponent<MenuItemProps & RefAttributes<HTMLButtonElement>>;
399
474
  export interface MenuRadioGroupProps<T extends string> extends Omit<HTMLAttributes<HTMLDivElement>, 'role' | 'onChange'> {
400
475
  value: T;
401
476
  onValueChange: (value: NoInfer<T>) => void;
@@ -408,7 +483,7 @@ export interface MenuItemRadioProps extends MenuItemBaseProps {
408
483
  value: string;
409
484
  }
410
485
  /** A `menuitemradio`: the chosen one is bold with a check. Choosing closes the menu and returns focus to the trigger. */
411
- export declare const MenuItemRadio: import("react").ForwardRefExoticComponent<MenuItemRadioProps & import("react").RefAttributes<HTMLButtonElement>>;
486
+ export declare const MenuItemRadio: ForwardRefExoticComponent<MenuItemRadioProps & RefAttributes<HTMLButtonElement>>;
412
487
  export interface MenuItemCheckboxProps extends MenuItemBaseProps {
413
488
  checked: boolean;
414
489
  onCheckedChange: (checked: boolean) => void;
@@ -416,7 +491,7 @@ export interface MenuItemCheckboxProps extends MenuItemBaseProps {
416
491
  closeOnSelect?: boolean;
417
492
  }
418
493
  /** A `menuitemcheckbox`: toggles in place, with a check while on. */
419
- export declare const MenuItemCheckbox: import("react").ForwardRefExoticComponent<MenuItemCheckboxProps & import("react").RefAttributes<HTMLButtonElement>>;
494
+ export declare const MenuItemCheckbox: ForwardRefExoticComponent<MenuItemCheckboxProps & RefAttributes<HTMLButtonElement>>;
420
495
  /** A line between groups of items. */
421
496
  export declare function MenuSeparator({ className, ...props }: Omit<HTMLAttributes<HTMLDivElement>, 'role'>): import("react/jsx-runtime").JSX.Element;
422
497
  /** Plain text inside the menu, such as a question before a destructive choice. Not focusable. */
@@ -465,7 +540,7 @@ export interface PopoverProps extends PopupBaseProps, Omit<HTMLAttributes<HTMLDi
465
540
  * not trapped. The caller supplies the content and its texts. Esc is taken before it
466
541
  * reaches the content (so an enclosing Dialog stays open): a field inside cannot use it.
467
542
  */
468
- export declare const Popover: import("react").ForwardRefExoticComponent<PopoverProps & import("react").RefAttributes<HTMLDivElement>>;
543
+ export declare const Popover: ForwardRefExoticComponent<PopoverProps & RefAttributes<HTMLDivElement>>;
469
544
  export type TableSortDirection = 'ascending' | 'descending';
470
545
  export interface TableColumn<Row> {
471
546
  /** Stable id of the column; it is what `sort.key` and `onSort` name. */
@@ -530,7 +605,7 @@ export interface PageHeaderProps extends Omit<HTMLAttributes<HTMLElement>, 'titl
530
605
  visuallyHidden?: boolean;
531
606
  }
532
607
  /** Desktop page header: one line with the title (22px, semibold) and actions at the end. */
533
- export declare const PageHeader: import("react").ForwardRefExoticComponent<PageHeaderProps & import("react").RefAttributes<HTMLElement>>;
608
+ export declare const PageHeader: ForwardRefExoticComponent<PageHeaderProps & RefAttributes<HTMLElement>>;
534
609
  export interface PanelHeaderStep {
535
610
  label: string;
536
611
  /** Goes back to this step. The last step is the current one and is never a button. */
@@ -569,14 +644,14 @@ export interface PanelHeaderProps extends Omit<HTMLAttributes<HTMLElement>, 'tit
569
644
  closeIcon?: ReactNode;
570
645
  }
571
646
  /** Header of a panel or section: title (or a trail), optional eyebrow, subtitle, icon, actions and close button. */
572
- export declare const PanelHeader: import("react").ForwardRefExoticComponent<PanelHeaderProps & import("react").RefAttributes<HTMLElement>>;
647
+ export declare const PanelHeader: ForwardRefExoticComponent<PanelHeaderProps & RefAttributes<HTMLElement>>;
573
648
  export type ListRowProps = ButtonHTMLAttributes<HTMLButtonElement>;
574
649
  /**
575
650
  * A tappable list row (HCUI's `list-row` mixin): full width, at least 56px, 8px 16px of padding,
576
651
  * a 12px gap between its children, a translucent veil on hover and press and the focus ring inside.
577
652
  * The content and its semantics (`aria-pressed`, `role="option"`) are the caller's.
578
653
  */
579
- export declare const ListRow: import("react").ForwardRefExoticComponent<ListRowProps & import("react").RefAttributes<HTMLButtonElement>>;
654
+ export declare const ListRow: ForwardRefExoticComponent<ListRowProps & RefAttributes<HTMLButtonElement>>;
580
655
  export interface CommandPaletteItem {
581
656
  /** Unique among all items. */
582
657
  key: string;
@@ -640,7 +715,7 @@ export interface CommandPaletteProps extends Omit<HTMLAttributes<HTMLDialogEleme
640
715
  * into view; Enter selects, ⌘/Ctrl+Enter takes the alternative; the pointer activates the
641
716
  * item under it. Texts, results and actions come from the caller.
642
717
  */
643
- export declare const CommandPalette: import("react").ForwardRefExoticComponent<CommandPaletteProps & import("react").RefAttributes<HTMLDialogElement>>;
718
+ export declare const CommandPalette: ForwardRefExoticComponent<CommandPaletteProps & RefAttributes<HTMLDialogElement>>;
644
719
  /**
645
720
  * Walks a list of image addresses: starts at the first and moves to the next on
646
721
  * each load error. Starts over when the list changes. `src` is `undefined` once
@@ -682,7 +757,7 @@ export interface MiniDialViewProps {
682
757
  * The dial summarized in small squares: one column per click, one row per value
683
758
  * (speed, attack, defense, damage). The cell size is `--hc-mini-dial-cell` (7px).
684
759
  */
685
- export declare const MiniDialView: import("react").ForwardRefExoticComponent<MiniDialViewProps & import("react").RefAttributes<HTMLSpanElement>>;
760
+ export declare const MiniDialView: ForwardRefExoticComponent<MiniDialViewProps & RefAttributes<HTMLSpanElement>>;
686
761
  export interface PowerIconViewProps {
687
762
  /** Power color (the circle's fill). */
688
763
  color: string;
@@ -693,7 +768,7 @@ export interface PowerIconViewProps {
693
768
  size?: number;
694
769
  }
695
770
  /** Power icon: a circle in the power color with the value symbol on top. */
696
- export declare const PowerIconView: import("react").ForwardRefExoticComponent<PowerIconViewProps & import("react").RefAttributes<HTMLSpanElement>>;
771
+ export declare const PowerIconView: ForwardRefExoticComponent<PowerIconViewProps & RefAttributes<HTMLSpanElement>>;
697
772
  export interface StatGlyphViewProps {
698
773
  /** Address of the symbol, already built. */
699
774
  src: string;
@@ -702,7 +777,7 @@ export interface StatGlyphViewProps {
702
777
  size?: number;
703
778
  }
704
779
  /** The value symbol alone (no circle), in neutral gray. Used in the dial legend. */
705
- export declare const StatGlyphView: import("react").ForwardRefExoticComponent<StatGlyphViewProps & import("react").RefAttributes<HTMLImageElement>>;
780
+ export declare const StatGlyphView: ForwardRefExoticComponent<StatGlyphViewProps & RefAttributes<HTMLImageElement>>;
706
781
  export interface SpecialIconViewProps {
707
782
  /** Round white background (the image server's `sp/white.svg`), already built. */
708
783
  backgroundSrc: string;
@@ -711,7 +786,7 @@ export interface SpecialIconViewProps {
711
786
  size?: number;
712
787
  }
713
788
  /** Special power and trait icon: a white circle with a symbol in the middle. */
714
- export declare const SpecialIconView: import("react").ForwardRefExoticComponent<SpecialIconViewProps & import("react").RefAttributes<HTMLSpanElement>>;
789
+ export declare const SpecialIconView: ForwardRefExoticComponent<SpecialIconViewProps & RefAttributes<HTMLSpanElement>>;
715
790
  export interface EquipmentIconProps {
716
791
  size?: number;
717
792
  }
@@ -752,7 +827,7 @@ export interface AvatarViewProps {
752
827
  * `fallbackSrc` (with `data-fallback` and the muted fill). The image cannot be
753
828
  * dragged, selected or opened in a context menu.
754
829
  */
755
- export declare const AvatarView: import("react").ForwardRefExoticComponent<AvatarViewProps & import("react").RefAttributes<HTMLImageElement>>;
830
+ export declare const AvatarView: ForwardRefExoticComponent<AvatarViewProps & RefAttributes<HTMLImageElement>>;
756
831
  export interface SetIconViewProps {
757
832
  /** Addresses of the set icon, in the order to try. Empty: renders nothing. */
758
833
  sources: readonly string[];
@@ -761,7 +836,7 @@ export interface SetIconViewProps {
761
836
  title?: string;
762
837
  }
763
838
  /** Set icon. Tries each address in order and disappears when none loads. Decorative: the set name is next to it. */
764
- export declare const SetIconView: import("react").ForwardRefExoticComponent<SetIconViewProps & import("react").RefAttributes<HTMLImageElement>>;
839
+ export declare const SetIconView: ForwardRefExoticComponent<SetIconViewProps & RefAttributes<HTMLImageElement>>;
765
840
  /** Value rows of the dial, top to bottom (the top row is the click number). */
766
841
  export declare const COMBAT_DIAL_STATS: readonly ["speed", "attack", "defense", "damage"];
767
842
  export type CombatDialStat = (typeof COMBAT_DIAL_STATS)[number];
@@ -888,7 +963,7 @@ export interface CombatDialViewProps {
888
963
  * printed card, the header with the point values. Hover, touch and keyboard on
889
964
  * the cells open the tip through `onTipChange`; the caller draws the tip.
890
965
  */
891
- export declare const CombatDialView: import("react").ForwardRefExoticComponent<CombatDialViewProps & import("react").RefAttributes<HTMLElement>>;
966
+ export declare const CombatDialView: ForwardRefExoticComponent<CombatDialViewProps & RefAttributes<HTMLElement>>;
892
967
  export declare const PHYSICAL_CARD_WIDTH = 375;
893
968
  export declare const PHYSICAL_CARD_HEIGHT = 525;
894
969
  export declare const PHYSICAL_CARD_BACK_TABLE_HEIGHT = 440;
@@ -1225,7 +1300,7 @@ export declare function PhysicalCardFocusView({ label, onClose, children }: Phys
1225
1300
  * holding to open the focus view and flipping in it, and opens the tips through
1226
1301
  * `onTipChange`; drawing the tip, translating and copying are the caller's.
1227
1302
  */
1228
- export declare const PhysicalCardView: import("react").ForwardRefExoticComponent<PhysicalCardViewProps & import("react").RefAttributes<HTMLDivElement>>;
1303
+ export declare const PhysicalCardView: ForwardRefExoticComponent<PhysicalCardViewProps & RefAttributes<HTMLDivElement>>;
1229
1304
  /**
1230
1305
  * A faithful port of hcmaps.net's map drawing engine (map.js / tile.js / edge.js /
1231
1306
  * wall.js), the same engine HCUnits uses to draw the map's canvas. It reads the
@@ -1283,6 +1358,21 @@ export interface HCMapJson {
1283
1358
  ramps?: JsonRamp[];
1284
1359
  ziplines?: JsonWall[];
1285
1360
  }
1361
+ /** The grid's line between squares. */
1362
+ export declare const MAP_GRID_COLOR = "#222222";
1363
+ /** `soft` grid: the line between two plain squares (clear, at floor level). */
1364
+ export declare const MAP_GRID_SOFT_COLOR = "#D4D2CC";
1365
+ /** `soft` grid: between two squares of the same terrain, the fill darkened by this fraction. */
1366
+ export declare const MAP_GRID_TERRAIN_SHADE = 0.22;
1367
+ export interface MapDrawOptions {
1368
+ /**
1369
+ * `dark` (default): every square outlined in the dark grid, as printed. `soft`: the line between
1370
+ * two plain squares (clear terrain at floor level) is light gray; between two squares of the same
1371
+ * terrain, a darker tone of that terrain's color; between different kinds and on the map's edge,
1372
+ * the dark grid.
1373
+ */
1374
+ grid?: 'dark' | 'soft';
1375
+ }
1286
1376
  export declare class HCMap {
1287
1377
  width: number;
1288
1378
  height: number;
@@ -1293,7 +1383,13 @@ export declare class HCMap {
1293
1383
  constructor(json: HCMapJson);
1294
1384
  private getTile;
1295
1385
  private tryAddEdge;
1296
- draw(ctx: CanvasRenderingContext2D): void;
1386
+ /**
1387
+ * The `soft` grid's color for the line between two squares: light gray between plain squares
1388
+ * (clear, floor level, no starting zone), a tone of the terrain's own color between two squares
1389
+ * of the same kind, and the dark grid between different kinds or on the map's edge.
1390
+ */
1391
+ private softGridColor;
1392
+ draw(ctx: CanvasRenderingContext2D, { grid }?: MapDrawOptions): void;
1297
1393
  private drawWall;
1298
1394
  }
1299
1395
  export declare function mapSquare(map: HCMapJson, x: number, y: number): {
@@ -1509,6 +1605,8 @@ export interface MapBoardViewProps extends Omit<HTMLAttributes<HTMLDivElement>,
1509
1605
  onLeave?(): void;
1510
1606
  /** Drawing failed (a JSON outside the expected format). */
1511
1607
  onDrawError?(): void;
1608
+ /** The grid between squares: `dark` (default) or `soft` (light between plain squares). */
1609
+ grid?: MapDrawOptions['grid'];
1512
1610
  }
1513
1611
  /**
1514
1612
  * The map's 2D board: the hcmaps drawing in a canvas and, over it, the inspection's
@@ -1516,7 +1614,7 @@ export interface MapBoardViewProps extends Omit<HTMLAttributes<HTMLDivElement>,
1516
1614
  * the tooltip shows are the caller's. Other attributes (role, focus, keyboard,
1517
1615
  * `aria-*`, `data-*`) go to the container.
1518
1616
  */
1519
- export declare const MapBoardView: import("react").ForwardRefExoticComponent<MapBoardViewProps & import("react").RefAttributes<HTMLDivElement>>;
1617
+ export declare const MapBoardView: ForwardRefExoticComponent<MapBoardViewProps & RefAttributes<HTMLDivElement>>;
1520
1618
  export interface MapBoardPlaceholderProps {
1521
1619
  /** Read by screen readers while the map loads (e.g. "Loading map"). */
1522
1620
  label: string;
package/dist/index.js CHANGED
@@ -6,8 +6,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
6
6
  import { cloneElement, createContext, forwardRef, Fragment, useCallback, useContext, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react";
7
7
  import { createPortal } from "react-dom";
8
8
  import { Fragment as Fragment2, jsx, jsxs } from "react/jsx-runtime";
9
- function Surface({ tone = "content", radius, className, ...props }) {
10
- return /* @__PURE__ */ jsx("div", { ...props, className: ["hc-ui-surface", className].filter(Boolean).join(" "), "data-tone": tone, "data-radius": radius });
9
+ function Surface({ tone = "content", radius, as: Element = "div", className, ...props }) {
10
+ return /* @__PURE__ */ jsx(Element, { ...props, className: ["hc-ui-surface", className].filter(Boolean).join(" "), "data-tone": tone, "data-radius": radius });
11
11
  }
12
12
  function SegmentedControl({
13
13
  label,
@@ -20,6 +20,7 @@ function SegmentedControl({
20
20
  indicator = "static",
21
21
  equalWidth = false,
22
22
  optionWidth,
23
+ optionMinWidth,
23
24
  className,
24
25
  style,
25
26
  ...props
@@ -33,7 +34,8 @@ function SegmentedControl({
33
34
  const layout = {
34
35
  ...equal ? { "--hc-segmented-count": options.length } : {},
35
36
  ...slide ? { "--hc-segmented-index": Math.max(knobIndex, 0) } : {},
36
- ...optionWidth !== void 0 ? { "--hc-segmented-option-width": `${optionWidth}px` } : {}
37
+ ...optionWidth !== void 0 ? { "--hc-segmented-option-width": `${optionWidth}px` } : {},
38
+ ...optionMinWidth !== void 0 ? { "--hc-segmented-option-min-width": `${optionMinWidth}px` } : {}
37
39
  };
38
40
  const move = (index, key) => {
39
41
  if (!["ArrowRight", "ArrowLeft", "ArrowDown", "ArrowUp", "Home", "End"].includes(key)) return false;
@@ -103,7 +105,7 @@ function SegmentedControl({
103
105
  );
104
106
  }
105
107
  var classes = (...names) => names.filter(Boolean).join(" ");
106
- var Button = forwardRef(function Button2({ variant = "secondary", size = "md", icon, iconEnd, fullWidth = false, loading = false, wrap = false, type = "button", className, children, onClick, ...props }, ref) {
108
+ var Button = forwardRef(function Button2({ variant = "secondary", tone = "default", size = "md", icon, iconEnd, fullWidth = false, loading = false, wrap = false, type = "button", className, children, onClick, ...props }, ref) {
107
109
  const handleClick = (event) => {
108
110
  if (loading) event.preventDefault();
109
111
  else onClick?.(event);
@@ -120,6 +122,7 @@ var Button = forwardRef(function Button2({ variant = "secondary", size = "md", i
120
122
  "aria-disabled": loading || props["aria-disabled"],
121
123
  "data-variant": variant,
122
124
  "data-size": size,
125
+ "data-tone": tone === "default" ? void 0 : tone,
123
126
  "data-full-width": fullWidth || void 0,
124
127
  "data-wrap": wrap || void 0,
125
128
  children: [
@@ -153,31 +156,33 @@ var glyph = (size, d) => /* @__PURE__ */ jsx("svg", { width: size, height: size,
153
156
  var CHEVRON_DOWN = "M6 9L12 15L18 9";
154
157
  var XMARK = "M6.75827 17.2426L12.0009 12M17.2435 6.75736L12.0009 12M12.0009 12L6.75827 6.75736M12.0009 12L17.2435 17.2426";
155
158
  var visible = (node) => node !== void 0 && node !== null && node !== false && node !== "";
156
- var Chip = forwardRef(function Chip2({ selected, icon, size = "md", variant = "outline", applied = false, count, badge, chevron, onClear, clearLabel, type = "button", className, children, ...props }, ref) {
159
+ var Chip = forwardRef(function Chip2({ selected, icon, size = "md", variant = "outline", applied = false, count, badge, badgePlacement = "inline", informative = false, chevron, onClear, clearLabel, type = "button", className, children, ...props }, ref) {
157
160
  const popup = props["aria-haspopup"];
158
161
  const withChevron = chevron ?? (popup !== void 0 && popup !== false && popup !== "false");
159
162
  const look = {
160
163
  "data-size": size === "md" ? void 0 : size,
161
164
  "data-variant": variant === "outline" ? void 0 : variant,
162
- "data-applied": applied || void 0
165
+ "data-applied": applied || void 0,
166
+ "data-icon-only": icon && !visible(children) || void 0,
167
+ "data-informative": informative || void 0
163
168
  };
164
169
  const content = /* @__PURE__ */ jsxs(Fragment2, { children: [
165
170
  icon && /* @__PURE__ */ jsx("span", { className: "hc-ui-chip__icon", "aria-hidden": "true", children: icon }),
166
171
  children,
167
172
  visible(count) && /* @__PURE__ */ jsx("span", { className: "hc-ui-chip__count", children: count }),
168
- visible(badge) && /* @__PURE__ */ jsx("span", { className: "hc-ui-chip__badge", children: badge }),
173
+ visible(badge) && /* @__PURE__ */ jsx("span", { className: "hc-ui-chip__badge", "data-placement": badgePlacement === "corner" ? "corner" : void 0, children: badge }),
169
174
  withChevron && /* @__PURE__ */ jsx("span", { className: "hc-ui-chip__chevron", "aria-hidden": "true", children: glyph(size === "xs" ? 12 : 14, CHEVRON_DOWN) })
170
175
  ] });
171
176
  if (!onClear) {
172
- return /* @__PURE__ */ jsx("button", { ...props, ref, type, "aria-pressed": selected, className: classes("hc-ui-chip", className), ...look, children: content });
177
+ return /* @__PURE__ */ jsx("button", { ...props, ...informative ? { disabled: true } : {}, ref, type, "aria-pressed": selected, className: classes("hc-ui-chip", className), ...look, children: content });
173
178
  }
174
179
  const { style, id, onClick, disabled, ...rest } = props;
175
180
  return /* @__PURE__ */ jsxs("span", { className: classes("hc-ui-chip", className), style, ...look, "data-clearable": "", "data-selected": selected || void 0, "data-disabled": disabled || void 0, children: [
176
- onClick ? /* @__PURE__ */ jsx("button", { ...rest, id, ref, type, disabled, "aria-pressed": selected, className: "hc-ui-chip__main", onClick, children: content }) : /* @__PURE__ */ jsx("span", { id, className: "hc-ui-chip__main", children: content }),
181
+ onClick ? /* @__PURE__ */ jsx("button", { ...rest, id, ref, type, disabled: informative || disabled, "aria-pressed": selected, className: "hc-ui-chip__main", onClick, children: content }) : /* @__PURE__ */ jsx("span", { id, className: "hc-ui-chip__main", children: content }),
177
182
  /* @__PURE__ */ jsx("button", { type: "button", className: "hc-ui-chip__clear", "aria-label": clearLabel, title: clearLabel, disabled, onClick: onClear, children: glyph(12, XMARK) })
178
183
  ] });
179
184
  });
180
- var FilterBar = forwardRef(function FilterBar2({ label, start, end, footer, layout = "auto", density = "comfortable", sticky = false, className, children, ...props }, ref) {
185
+ var FilterBar = forwardRef(function FilterBar2({ label, start, end, footer, layout = "auto", density = "comfortable", sticky = false, bleed = false, rowClassName, className, children, ...props }, ref) {
181
186
  return /* @__PURE__ */ jsxs(
182
187
  "div",
183
188
  {
@@ -189,10 +194,11 @@ var FilterBar = forwardRef(function FilterBar2({ label, start, end, footer, layo
189
194
  "data-layout": layout,
190
195
  "data-density": density === "comfortable" ? void 0 : density,
191
196
  "data-sticky": sticky || void 0,
197
+ "data-bleed": bleed || void 0,
192
198
  children: [
193
199
  /* @__PURE__ */ jsxs("div", { className: "hc-ui-filter-bar__line", children: [
194
200
  start && /* @__PURE__ */ jsx("div", { className: "hc-ui-filter-bar__start", children: start }),
195
- /* @__PURE__ */ jsx("div", { className: "hc-ui-filter-bar__row", children }),
201
+ /* @__PURE__ */ jsx("div", { className: classes("hc-ui-filter-bar__row", rowClassName), children }),
196
202
  end && /* @__PURE__ */ jsx("div", { className: "hc-ui-filter-bar__end", children: end })
197
203
  ] }),
198
204
  footer && /* @__PURE__ */ jsx("div", { className: "hc-ui-filter-bar__footer", children: footer })
@@ -323,7 +329,7 @@ var outside = (element, x, y) => {
323
329
  const box = element.getBoundingClientRect();
324
330
  return x < box.left || x > box.right || y < box.top || y > box.bottom;
325
331
  };
326
- var Dialog = forwardRef(function Dialog2({
332
+ var DialogBase = forwardRef(function Dialog({
327
333
  open,
328
334
  onClose,
329
335
  title,
@@ -334,6 +340,8 @@ var Dialog = forwardRef(function Dialog2({
334
340
  closeIcon,
335
341
  size = "md",
336
342
  dismissible = true,
343
+ bodyClassName,
344
+ headerless = false,
337
345
  className,
338
346
  onClick,
339
347
  onPointerDown,
@@ -348,6 +356,7 @@ var Dialog = forwardRef(function Dialog2({
348
356
  }, [ref]);
349
357
  const id = useId();
350
358
  const titleId = `${id}-title`;
359
+ const bodyId = `${id}-body`;
351
360
  const descriptionId = description ? `${id}-description` : void 0;
352
361
  const [mounted, setMounted] = useState(open);
353
362
  const [closing, setClosing] = useState(false);
@@ -362,7 +371,7 @@ var Dialog = forwardRef(function Dialog2({
362
371
  });
363
372
  const keepFocus = useRef(null);
364
373
  const handleMount = useCallback(() => {
365
- const dialog = document.getElementById(titleId)?.closest("dialog");
374
+ const dialog = (document.getElementById(titleId) ?? document.getElementById(bodyId))?.closest("dialog");
366
375
  if (!dialog) return () => {
367
376
  };
368
377
  const trigger = document.activeElement instanceof HTMLElement ? document.activeElement : null;
@@ -381,7 +390,7 @@ var Dialog = forwardRef(function Dialog2({
381
390
  if (lost && !dialog.open && trigger?.isConnected) trigger.focus({ preventScroll: true });
382
391
  });
383
392
  };
384
- }, [titleId]);
393
+ }, [titleId, bodyId]);
385
394
  useEffect(() => {
386
395
  const dialog = dialogRef.current;
387
396
  if (!closing || !dialog) return;
@@ -428,7 +437,7 @@ var Dialog = forwardRef(function Dialog2({
428
437
  if (fromBackdrop && latest.current.dismissible && !latest.current.closing) latest.current.onClose();
429
438
  };
430
439
  if (!mounted) return null;
431
- const hasBody = Boolean(description) || children !== void 0 && children !== null && children !== false;
440
+ const hasBody = headerless || Boolean(description) || children !== void 0 && children !== null && children !== false;
432
441
  return /* @__PURE__ */ jsxs(
433
442
  "dialog",
434
443
  {
@@ -437,7 +446,8 @@ var Dialog = forwardRef(function Dialog2({
437
446
  className: classes("hc-ui-dialog", className),
438
447
  "data-size": size,
439
448
  "data-closing": closing || void 0,
440
- "aria-labelledby": titleId,
449
+ "data-headerless": headerless || void 0,
450
+ "aria-labelledby": headerless ? void 0 : titleId,
441
451
  "aria-describedby": classes(descriptionId, describedBy) || void 0,
442
452
  onCancel: handleCancel,
443
453
  onClose: handleNativeClose,
@@ -445,11 +455,11 @@ var Dialog = forwardRef(function Dialog2({
445
455
  onClick: handleClick,
446
456
  children: [
447
457
  /* @__PURE__ */ jsx(DialogMount, { onMount: handleMount }),
448
- /* @__PURE__ */ jsxs("div", { className: "hc-ui-dialog__header", children: [
458
+ !headerless && /* @__PURE__ */ jsxs("div", { className: "hc-ui-dialog__header", children: [
449
459
  /* @__PURE__ */ jsx("h2", { className: "hc-ui-dialog__title", id: titleId, children: title }),
450
460
  closeLabel && /* @__PURE__ */ jsx(IconButton, { className: "hc-ui-dialog__close", label: closeLabel, icon: closeIcon ?? closeGlyph, size: "sm", tone: "secondary", onClick: () => latest.current.onClose() })
451
461
  ] }),
452
- hasBody && /* @__PURE__ */ jsxs("div", { className: "hc-ui-dialog__body", children: [
462
+ hasBody && /* @__PURE__ */ jsxs("div", { className: classes("hc-ui-dialog__body", bodyClassName), id: headerless ? bodyId : void 0, children: [
453
463
  description && /* @__PURE__ */ jsx("p", { className: "hc-ui-dialog__description", id: descriptionId, children: description }),
454
464
  children
455
465
  ] }),
@@ -458,8 +468,9 @@ var Dialog = forwardRef(function Dialog2({
458
468
  }
459
469
  );
460
470
  });
471
+ var Dialog2 = DialogBase;
461
472
  var SHEET_EASE = "cubic-bezier(0.32, 0.72, 0, 1)";
462
- var NO_DRAG = 'input, textarea, select, [contenteditable="true"], [data-hc-sheet-nodrag]';
473
+ var NO_DRAG = 'input, textarea, select, [contenteditable="true"], [data-hc-sheet-nodrag], .hc-ui-portal';
463
474
  function useSheetDrag(sheet, latest) {
464
475
  useEffect(() => {
465
476
  const el = sheet;
@@ -591,7 +602,7 @@ function useSheetDrag(sheet, latest) {
591
602
  };
592
603
  }, [sheet, latest]);
593
604
  }
594
- var Sheet = forwardRef(function Sheet2({ presentation = "auto", width = 440, footer, dismissible = true, onClose, className, style, ...props }, ref) {
605
+ var Sheet = forwardRef(function Sheet2({ presentation = "auto", width = 440, footer, dismissible = true, header = true, onClose, className, style, ...props }, ref) {
595
606
  const [node, setNode] = useState(null);
596
607
  const setRef = useCallback((element) => {
597
608
  setNode(element);
@@ -604,7 +615,7 @@ var Sheet = forwardRef(function Sheet2({ presentation = "auto", width = 440, foo
604
615
  useSheetDrag(node, latest);
605
616
  const sized = width === 440 ? style : { ...style, "--hc-sheet-width": `${width}px` };
606
617
  return /* @__PURE__ */ jsx(
607
- Dialog,
618
+ DialogBase,
608
619
  {
609
620
  ...props,
610
621
  ref: setRef,
@@ -612,6 +623,7 @@ var Sheet = forwardRef(function Sheet2({ presentation = "auto", width = 440, foo
612
623
  actions: footer,
613
624
  dismissible,
614
625
  onClose,
626
+ headerless: !header,
615
627
  className: classes("hc-ui-sheet", className),
616
628
  style: sized,
617
629
  "data-presentation": presentation
@@ -676,7 +688,7 @@ function SheetOptions({ label, options, value, onChange, className, ...props })
676
688
  );
677
689
  }) });
678
690
  }
679
- function SheetChips({ label, options, value, onChange, className, ...props }) {
691
+ function SheetChips({ label, options, value, onChange, size = "md", className, ...props }) {
680
692
  const item = useRovingRadios(options, value);
681
693
  return /* @__PURE__ */ jsx("div", { ...props, className: classes("hc-ui-sheet-chips", className), role: "radiogroup", "aria-label": label, children: options.map((option, index) => /* @__PURE__ */ jsx(
682
694
  "button",
@@ -687,6 +699,7 @@ function SheetChips({ label, options, value, onChange, className, ...props }) {
687
699
  "aria-checked": option.value === value,
688
700
  disabled: option.disabled,
689
701
  className: "hc-ui-chip",
702
+ "data-size": size === "md" ? void 0 : size,
690
703
  onClick: () => onChange(option.value),
691
704
  children: option.label
692
705
  },
@@ -716,6 +729,10 @@ function placePopup(trigger, size, boundary, side, align) {
716
729
  const finalAlign = !fits(align) && fits(align === "end" ? "start" : "end") ? align === "end" ? "start" : "end" : align;
717
730
  return { side: finalSide, align: finalAlign };
718
731
  }
732
+ function portalContainerFor(node) {
733
+ if (typeof document === "undefined") return null;
734
+ return node?.closest("dialog[open]") ?? document.body;
735
+ }
719
736
  var layers = [];
720
737
  function themeOf(element) {
721
738
  const scoped = element?.closest("[data-theme]")?.getAttribute("data-theme");
@@ -761,7 +778,7 @@ function usePopup({ kind, trigger, open: openProp, defaultOpen = false, onOpenCh
761
778
  setContainer(null);
762
779
  return;
763
780
  }
764
- const element = portal instanceof HTMLElement ? portal : triggerRef.current?.closest("dialog[open]") ?? document.body;
781
+ const element = portal instanceof HTMLElement ? portal : portalContainerFor(triggerRef.current) ?? document.body;
765
782
  setContainer({ element, theme: theme ?? themeOf(triggerRef.current) });
766
783
  }, [open, portal, theme]);
767
784
  const place = useCallback(() => {
@@ -2451,6 +2468,16 @@ var Tile = class {
2451
2468
  ctx.restore();
2452
2469
  }
2453
2470
  };
2471
+ var MAP_GRID_COLOR = "#222222";
2472
+ var MAP_GRID_SOFT_COLOR = "#D4D2CC";
2473
+ var MAP_GRID_TERRAIN_SHADE = 0.22;
2474
+ function shadeHex(color, amount) {
2475
+ const m = /^#([0-9a-f]{6})$/i.exec(color);
2476
+ if (!m) return null;
2477
+ const n = parseInt(m[1], 16);
2478
+ const ch = (shift) => Math.round((n >> shift & 255) * (1 - amount)).toString(16).padStart(2, "0");
2479
+ return `#${ch(16)}${ch(8)}${ch(0)}`;
2480
+ }
2454
2481
  var HCMap = class {
2455
2482
  constructor(json) {
2456
2483
  __publicField(this, "width");
@@ -2520,7 +2547,19 @@ var HCMap = class {
2520
2547
  t0.addEdge(edgeType, MAP_TERRAIN_EDGE_COLORS[t0.terrain] ?? "#FFFFFF", dashed);
2521
2548
  }
2522
2549
  }
2523
- draw(ctx) {
2550
+ /**
2551
+ * The `soft` grid's color for the line between two squares: light gray between plain squares
2552
+ * (clear, floor level, no starting zone), a tone of the terrain's own color between two squares
2553
+ * of the same kind, and the dark grid between different kinds or on the map's edge.
2554
+ */
2555
+ softGridColor(a, b) {
2556
+ if (!a || !b) return MAP_GRID_COLOR;
2557
+ const same = a.terrain === b.terrain && a.type === b.type && String(a.elevation) === String(b.elevation) && !!a.isStartingZone === !!b.isStartingZone && !!a.isStartingZone4p === !!b.isStartingZone4p && a.fillStyle === b.fillStyle;
2558
+ if (!same) return MAP_GRID_COLOR;
2559
+ if (a.terrain === "clear" && Number(a.elevation) === 1 && !a.isStartingZone && !a.isStartingZone4p) return MAP_GRID_SOFT_COLOR;
2560
+ return shadeHex(a.fillStyle, MAP_GRID_TERRAIN_SHADE) ?? MAP_GRID_COLOR;
2561
+ }
2562
+ draw(ctx, { grid = "dark" } = {}) {
2524
2563
  ctx.save();
2525
2564
  for (const tile of this.tiles) tile.draw(ctx);
2526
2565
  ctx.restore();
@@ -2528,11 +2567,29 @@ var HCMap = class {
2528
2567
  for (const tile of this.tiles) tile.drawEdges(ctx);
2529
2568
  ctx.restore();
2530
2569
  ctx.save();
2531
- ctx.strokeStyle = "#222222";
2570
+ ctx.strokeStyle = MAP_GRID_COLOR;
2532
2571
  ctx.lineWidth = 1;
2533
- for (let y = 0; y < this.height; y++) {
2534
- for (let x = 0; x < this.width; x++) {
2535
- ctx.strokeRect(x * MAP_TILE_SIZE, y * MAP_TILE_SIZE, MAP_TILE_SIZE, MAP_TILE_SIZE);
2572
+ if (grid === "dark") {
2573
+ for (let y = 0; y < this.height; y++) {
2574
+ for (let x = 0; x < this.width; x++) {
2575
+ ctx.strokeRect(x * MAP_TILE_SIZE, y * MAP_TILE_SIZE, MAP_TILE_SIZE, MAP_TILE_SIZE);
2576
+ }
2577
+ }
2578
+ } else {
2579
+ const lines = [];
2580
+ for (let y = 0; y <= this.height; y++) {
2581
+ for (let x = 0; x < this.width; x++) lines.push({ x0: x, y0: y, x1: x + 1, y1: y, color: this.softGridColor(this.getTile(x, y - 1), this.getTile(x, y)) });
2582
+ }
2583
+ for (let x = 0; x <= this.width; x++) {
2584
+ for (let y = 0; y < this.height; y++) lines.push({ x0: x, y0: y, x1: x, y1: y + 1, color: this.softGridColor(this.getTile(x - 1, y), this.getTile(x, y)) });
2585
+ }
2586
+ lines.sort((a, b) => Number(a.color === MAP_GRID_COLOR) - Number(b.color === MAP_GRID_COLOR));
2587
+ for (const l of lines) {
2588
+ ctx.strokeStyle = l.color;
2589
+ ctx.beginPath();
2590
+ ctx.moveTo(l.x0 * MAP_TILE_SIZE, l.y0 * MAP_TILE_SIZE);
2591
+ ctx.lineTo(l.x1 * MAP_TILE_SIZE, l.y1 * MAP_TILE_SIZE);
2592
+ ctx.stroke();
2536
2593
  }
2537
2594
  }
2538
2595
  ctx.restore();
@@ -3123,7 +3180,7 @@ function renderMapThumbnail(json, height = MAP_THUMBNAIL_HEIGHT) {
3123
3180
  tctx.drawImage(full, 0, 0, thumb.width, thumb.height);
3124
3181
  return thumb.toDataURL("image/png");
3125
3182
  }
3126
- var MapBoardView = forwardRef(function MapBoardView2({ map, canvasLabel, selection, onInspect, onLeave, onDrawError, className, style, ...rest }, ref) {
3183
+ var MapBoardView = forwardRef(function MapBoardView2({ map, canvasLabel, selection, onInspect, onLeave, onDrawError, grid = "dark", className, style, ...rest }, ref) {
3127
3184
  const canvasRef = useRef(null);
3128
3185
  const failed = useRef(onDrawError);
3129
3186
  failed.current = onDrawError;
@@ -3134,11 +3191,11 @@ var MapBoardView = forwardRef(function MapBoardView2({ map, canvasLabel, selecti
3134
3191
  if (!ctx) return;
3135
3192
  try {
3136
3193
  ctx.clearRect(0, 0, canvas.width, canvas.height);
3137
- new HCMap(map).draw(ctx);
3194
+ new HCMap(map).draw(ctx, { grid });
3138
3195
  } catch {
3139
3196
  failed.current?.();
3140
3197
  }
3141
- }, [map]);
3198
+ }, [map, grid]);
3142
3199
  function pointer(event) {
3143
3200
  const bounds = event.currentTarget.getBoundingClientRect();
3144
3201
  onInspect?.((event.clientX - bounds.left) / bounds.width * map.width, (event.clientY - bounds.top) / bounds.height * map.height, event.clientX, event.clientY);
@@ -3233,7 +3290,7 @@ export {
3233
3290
  Chip,
3234
3291
  CombatDialView,
3235
3292
  CommandPalette,
3236
- Dialog,
3293
+ Dialog2 as Dialog,
3237
3294
  EquipmentIcon,
3238
3295
  Field,
3239
3296
  FilterBar,
@@ -3244,6 +3301,9 @@ export {
3244
3301
  MAP_DEFAULT_CAMERA,
3245
3302
  MAP_ELEVATION_EDGE_COLOR,
3246
3303
  MAP_ELEVATION_TILE_COLORS,
3304
+ MAP_GRID_COLOR,
3305
+ MAP_GRID_SOFT_COLOR,
3306
+ MAP_GRID_TERRAIN_SHADE,
3247
3307
  MAP_INDOOR_EDGE_COLOR,
3248
3308
  MAP_SCENE_BASE_Z,
3249
3309
  MAP_SCENE_LEVEL_HEIGHT,
@@ -3313,6 +3373,7 @@ export {
3313
3373
  physicalCardTiltStyles,
3314
3374
  pickMapScene,
3315
3375
  placePopup,
3376
+ portalContainerFor,
3316
3377
  renderMapThumbnail,
3317
3378
  requestPhysicalCardMotionAccess,
3318
3379
  useImageChain,
package/dist/styles.css CHANGED
@@ -29,6 +29,13 @@
29
29
  border-radius: var(--hc-radius-lg);
30
30
  box-shadow: var(--hc-shadow-card);
31
31
  }
32
+ /* Muted: the card with the `bg-muted-2` fill (HCUI's `surface` mixin over `bg-muted-2`, as the
33
+ `SheetOptions` card and HCUI's action lists in sheets). */
34
+ [data-hc-ds] .hc-ui-surface[data-tone='muted'] {
35
+ background: var(--hc-color-bg-muted-2);
36
+ border-radius: var(--hc-radius-lg);
37
+ box-shadow: var(--hc-shadow-card);
38
+ }
32
39
  [data-hc-ds] .hc-ui-surface:not([data-tone='page'], [data-tone='main'])[data-radius='md'] { border-radius: var(--hc-radius-md); }
33
40
  [data-hc-ds] .hc-ui-surface:not([data-tone='page'], [data-tone='main'])[data-radius='lg'] { border-radius: var(--hc-radius-lg); }
34
41
  [data-hc-ds] .hc-ui-surface:not([data-tone='page'], [data-tone='main'])[data-radius='xl'] { border-radius: var(--hc-radius-xl); }
@@ -53,7 +60,8 @@
53
60
  align-items: center;
54
61
  justify-content: center;
55
62
  gap: 6px;
56
- min-width: 0;
63
+ /* `optionMinWidth` sets the custom property; without it, 0 as before. */
64
+ min-width: var(--hc-segmented-option-min-width, 0);
57
65
  min-height: 38px;
58
66
  margin: 0;
59
67
  padding: 0 var(--hc-space-3);
@@ -224,6 +232,11 @@
224
232
  [data-hc-ds] .hc-ui-button[data-variant='primary'] { color: var(--hc-color-text-inverse); background-color: var(--hc-color-accent); }
225
233
  [data-hc-ds] .hc-ui-button[data-variant='outline'] { background-color: transparent; box-shadow: inset 0 0 0 1px var(--hc-color-border-strong); }
226
234
  [data-hc-ds] .hc-ui-button[data-variant='danger'] { color: var(--hc-color-text-on-danger); background-color: var(--hc-color-bg-danger); }
235
+ /* Ghost: no fill, only the veil; `tone="danger"` is HCUI's danger in text only (team-save-menu__btn--danger). */
236
+ [data-hc-ds] .hc-ui-button[data-variant='ghost'] { background-color: transparent; }
237
+ [data-hc-ds] .hc-ui-button:is([data-variant='secondary'], [data-variant='outline'], [data-variant='ghost'])[data-tone='danger'] { color: var(--hc-color-danger); }
238
+ /* An open popup trigger: the 1.5px ring in the text color, as Chip (HCUI's Importar, Entrar and Convidar). */
239
+ [data-hc-ds] .hc-ui-button:is([data-variant='secondary'], [data-variant='outline'], [data-variant='ghost'])[aria-haspopup]:not([aria-haspopup='false'])[aria-expanded='true'] { box-shadow: inset 0 0 0 1.5px var(--hc-color-text-primary); }
227
240
  [data-hc-ds] .hc-ui-button[data-size='sm'] { min-height: var(--hc-control-height-chip); padding: 0 var(--hc-space-4); font-size: var(--hc-font-size-sm); }
228
241
  [data-hc-ds] .hc-ui-button[data-size='lg'] { min-height: var(--hc-control-height-lg); padding: 0 var(--hc-space-6); font-size: var(--hc-font-size-base); }
229
242
  [data-hc-ds] .hc-ui-button[data-full-width] { width: 100%; }
@@ -257,9 +270,12 @@
257
270
  }
258
271
  [data-hc-ds] .hc-ui-icon-button[data-size='sm'] { width: 36px; height: 36px; overflow: visible; }
259
272
  [data-hc-ds] .hc-ui-icon-button[data-size='lg'] { width: 44px; height: 44px; }
273
+ [data-hc-ds] .hc-ui-icon-button[data-size='xs'] { width: 28px; height: 28px; overflow: visible; }
260
274
  [data-hc-ds] .hc-ui-icon-button[data-tone='secondary'] { color: var(--hc-color-text-secondary); }
261
- /* 36px keeps a 44px touch target; the veil stays clipped to the circle. */
275
+ [data-hc-ds] .hc-ui-icon-button[data-tone='muted'] { color: var(--hc-color-text-muted); }
276
+ /* 36 and 28px keep a 44px touch target; the veil stays clipped to the circle. */
262
277
  [data-hc-ds] .hc-ui-icon-button[data-size='sm']::before { content: ''; position: absolute; inset: -4px; }
278
+ [data-hc-ds] .hc-ui-icon-button[data-size='xs']::before { content: ''; position: absolute; inset: -8px; }
263
279
  /* Fills: surface (HCUI's mobile theme button: surface, card ring and shadow) and ghost
264
280
  (HCUI's desktop utilities: no fill, muted fill and primary text on hover). */
265
281
  [data-hc-ds] .hc-ui-icon-button[data-variant='surface'] { background-color: var(--hc-color-bg-surface); box-shadow: var(--hc-shadow-card); }
@@ -268,6 +284,8 @@
268
284
  @media (hover: hover) {
269
285
  [data-hc-ds] .hc-ui-icon-button[data-variant='ghost']:hover:not(:disabled, [aria-pressed='true']) { color: var(--hc-color-text-primary); background-color: var(--hc-color-bg-muted); }
270
286
  }
287
+ /* An open popup trigger keeps the hover look, with a mouse or not (HCUI's ⋯ menus). */
288
+ [data-hc-ds] .hc-ui-icon-button[data-variant='ghost'][aria-haspopup]:not([aria-haspopup='false'])[aria-expanded='true']:not([aria-pressed='true']) { color: var(--hc-color-text-primary); background-color: var(--hc-color-bg-muted); }
271
289
  [data-hc-ds] .hc-ui-icon-button[data-shape='rounded'] { border-radius: var(--hc-radius-sm); }
272
290
  [data-hc-ds] .hc-ui-icon-button[aria-pressed='true'] { color: var(--hc-color-text-inverse); background-color: var(--hc-color-accent); }
273
291
  /* iconKey: the new icon turns in (HCUI's theme sun and moon). */
@@ -381,6 +399,33 @@
381
399
  [data-hc-ds] .hc-ui-chip[data-size='xs']:is([aria-pressed='true'], [aria-checked='true'], [data-selected]):hover { color: var(--hc-color-text-on-contrast); background-color: var(--hc-color-bg-contrast); }
382
400
  [data-hc-ds] .hc-ui-chip:is([aria-pressed='true'], [aria-checked='true'], [data-selected]) .hc-ui-chip__count { color: inherit; }
383
401
  [data-hc-ds] .hc-ui-chip[data-selected] .hc-ui-chip__clear { color: inherit; }
402
+ /* Muted: gray fill without ring (HCUI's search conditions, .search-page__cond). */
403
+ [data-hc-ds] .hc-ui-chip[data-variant='muted'] { background-color: var(--hc-color-bg-muted); box-shadow: none; }
404
+ /* Removable at md and sm: the whole height is the main button, and the 26px × sits 5px from the
405
+ end, concentric with the pill's curve ((36 - 26) / 2), in the muted color. */
406
+ [data-hc-ds] .hc-ui-chip[data-clearable][data-variant='muted']:not([data-size='xs']) { gap: 0; padding: 0 5px 0 0; }
407
+ [data-hc-ds] .hc-ui-chip[data-clearable][data-variant='muted']:not([data-size='xs']) .hc-ui-chip__main { align-self: stretch; padding: 0 var(--hc-space-1) 0 14px; }
408
+ [data-hc-ds] .hc-ui-chip[data-clearable][data-variant='muted'][data-size='sm'] .hc-ui-chip__main { padding-left: var(--hc-space-3); }
409
+ [data-hc-ds] .hc-ui-chip[data-clearable][data-variant='muted']:not([data-size='xs']) .hc-ui-chip__clear { width: 26px; height: 26px; color: var(--hc-color-text-muted); }
410
+ [data-hc-ds] .hc-ui-chip[data-clearable][data-variant='muted']:not([data-size='xs']) .hc-ui-chip__clear svg { width: 14px; height: 14px; }
411
+ /* Only an icon: a circle as tall as the chip. */
412
+ [data-hc-ds] .hc-ui-chip[data-icon-only] { width: var(--hc-control-height-chip); padding: 0; }
413
+ [data-hc-ds] .hc-ui-chip[data-icon-only][data-size='xs'] { width: 26px; }
414
+ /* Corner badge: on the top right corner, brand fill, a ring of the page around it (HCUI's .meta-fchip__count). */
415
+ [data-hc-ds] .hc-ui-chip__badge[data-placement='corner'] {
416
+ position: absolute;
417
+ top: -4px;
418
+ right: -4px;
419
+ padding: 0 var(--hc-space-1);
420
+ color: var(--hc-color-text-inverse);
421
+ background-color: var(--hc-color-brand);
422
+ box-shadow: 0 0 0 2px var(--hc-chip-badge-ring, var(--hc-color-bg-page));
423
+ font-family: var(--hc-font-mono);
424
+ font-weight: var(--hc-font-weight-semibold);
425
+ animation: hc-ui-check-in 260ms var(--hc-ease-pop);
426
+ }
427
+ /* Informative: it cannot be pressed (disabled) but keeps its full color. */
428
+ [data-hc-ds] .hc-ui-chip[data-informative]:disabled { opacity: 1; cursor: default; }
384
429
 
385
430
  /* FilterBar — a row of filters with start, end and footer slots (HCUI's .desktop-filter-bar,
386
431
  .desktop-filter-bar--sheet and .quick-filters). Placement stays with the caller. */
@@ -395,7 +440,13 @@
395
440
  [data-hc-ds] .hc-ui-filter-bar__line { display: flex; align-items: center; gap: var(--hc-space-2); min-width: 0; }
396
441
  [data-hc-ds] :is(.hc-ui-filter-bar__start, .hc-ui-filter-bar__end) { display: flex; flex: 0 0 auto; align-items: center; gap: var(--hc-space-2); }
397
442
  [data-hc-ds] .hc-ui-filter-bar__end { margin-inline-start: auto; }
398
- [data-hc-ds] .hc-ui-filter-bar__row { display: flex; flex: 1 1 auto; flex-wrap: wrap; align-items: center; gap: var(--hc-space-2); min-width: 0; }
443
+ /* Bleed: the bar reaches the panel's edges and gives the space back as padding. */
444
+ [data-hc-ds] .hc-ui-filter-bar[data-bleed] {
445
+ margin-inline: calc(-1 * var(--hc-filter-bar-bleed-start, var(--hc-filter-bar-bleed, 0px))) calc(-1 * var(--hc-filter-bar-bleed-end, var(--hc-filter-bar-bleed, 0px)));
446
+ padding-inline: var(--hc-filter-bar-bleed-start, var(--hc-filter-bar-bleed, 0px)) var(--hc-filter-bar-bleed-end, var(--hc-filter-bar-bleed, 0px));
447
+ }
448
+ /* The row anchors what the caller places in it with position: absolute (a popup). */
449
+ [data-hc-ds] .hc-ui-filter-bar__row { position: relative; display: flex; flex: 1 1 auto; flex-wrap: wrap; align-items: center; gap: var(--hc-space-2); min-width: 0; }
399
450
  [data-hc-ds] .hc-ui-filter-bar[data-density='compact'] .hc-ui-filter-bar__row { gap: var(--hc-space-1); }
400
451
  [data-hc-ds] .hc-ui-filter-bar__footer { margin-top: var(--hc-space-2); }
401
452
  /* One line that scrolls sideways. 4px of padding above and below, taken back by the margin,
@@ -503,6 +554,7 @@
503
554
  [data-hc-ds] :is(.hc-ui-segmented[data-indicator='slide'], .hc-ui-segmented__indicator) { transition: none; }
504
555
  [data-hc-ds] .hc-ui-segmented[data-indicator='slide']:has(.hc-ui-segmented__option:active:not(:disabled)) { transform: none; }
505
556
  [data-hc-ds] .hc-ui-icon-button__icon[data-swap] { animation: none; }
557
+ [data-hc-ds] .hc-ui-chip__badge[data-placement='corner'] { animation: none; }
506
558
  [data-hc-ds] :is(.hc-ui-button, .hc-ui-icon-button, .hc-ui-chip) { transition: none; }
507
559
  [data-hc-ds] .hc-ui-field__control { transition: none; }
508
560
  [data-hc-ds] .hc-ui-button__spinner { animation: none; }
@@ -660,7 +712,9 @@
660
712
  font-variation-settings: 'ROND' var(--hc-rond);
661
713
  line-height: var(--hc-line-height-md);
662
714
  outline: none;
663
- animation: hc-ui-dialog-in var(--hc-dur-medium) var(--hc-ease-standard) both;
715
+ /* `backwards`, not `both`: once in, no transform stays on the dialog. A transform would make
716
+ it the containing block of fixed popups portaled into it, and its overflow would cut them. */
717
+ animation: hc-ui-dialog-in var(--hc-dur-medium) var(--hc-ease-standard) backwards;
664
718
  }
665
719
  [data-hc-ds] .hc-ui-dialog[open] { display: grid; align-content: start; gap: var(--hc-space-2); }
666
720
  [data-hc-ds] .hc-ui-dialog::backdrop {
@@ -702,8 +756,6 @@
702
756
  gap: var(--hc-space-2);
703
757
  margin-top: var(--hc-space-3);
704
758
  }
705
- /* HCUI's confirmation buttons are 38px, between Button sm (36) and md (44). */
706
- [data-hc-ds] .hc-ui-dialog[data-size='sm'] .hc-ui-dialog__actions .hc-ui-button[data-size='sm'] { min-height: 38px; }
707
759
 
708
760
  [data-hc-ds] .hc-ui-dialog[data-size='md'] {
709
761
  width: min(440px, calc(100vw - 2 * var(--hc-space-6)));
@@ -739,6 +791,8 @@
739
791
  box-shadow: 0 -1px 0 var(--hc-color-border);
740
792
  }
741
793
  [data-hc-ds] .hc-ui-dialog[data-size='md'] .hc-ui-dialog__actions > * { flex: 1 1 0; }
794
+ /* Header-less (Sheet header={false}): the content brings its own spacing, heading and close. */
795
+ [data-hc-ds] .hc-ui-dialog[data-size='md'][data-headerless] .hc-ui-dialog__body { gap: 0; padding: 0; }
742
796
 
743
797
  @keyframes hc-ui-dialog-in {
744
798
  from { opacity: 0; transform: translateY(8px) scale(0.97); }
@@ -776,7 +830,7 @@
776
830
  padding-bottom: max(env(safe-area-inset-bottom, 0px), var(--hc-space-3));
777
831
  border-radius: var(--hc-radius-sheet) var(--hc-radius-sheet) 0 0;
778
832
  touch-action: pan-y;
779
- animation: hc-ui-sheet-up var(--hc-dur-large) var(--hc-ease-sheet) both;
833
+ animation: hc-ui-sheet-up var(--hc-dur-large) var(--hc-ease-sheet) backwards;
780
834
  transition: height 320ms var(--hc-ease-sheet), max-height 320ms var(--hc-ease-sheet);
781
835
  }
782
836
  [data-hc-ds] .hc-ui-sheet[data-presentation='bottom']::before {
@@ -811,7 +865,7 @@
811
865
  padding-bottom: max(env(safe-area-inset-bottom, 0px), var(--hc-space-3));
812
866
  border-radius: var(--hc-radius-sheet) var(--hc-radius-sheet) 0 0;
813
867
  touch-action: pan-y;
814
- animation: hc-ui-sheet-up var(--hc-dur-large) var(--hc-ease-sheet) both;
868
+ animation: hc-ui-sheet-up var(--hc-dur-large) var(--hc-ease-sheet) backwards;
815
869
  transition: height 320ms var(--hc-ease-sheet), max-height 320ms var(--hc-ease-sheet);
816
870
  }
817
871
  [data-hc-ds] .hc-ui-sheet[data-presentation='auto']::before {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ntmats/hc-ui",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "description": "Shared HCUI/Arena React presentation primitives.",
6
6
  "license": "UNLICENSED",