@vvedantb/ui 0.6.0 → 0.6.1

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
@@ -215,7 +215,7 @@ import { ConfirmDialog, SlideOverPanel, ToolbarSearchInput } from "@vvedantb/ui"
215
215
  - `SlideOverPanel`: a right-side panel, absolutely positioned inside its nearest positioned parent. It is not a portal or dialog. It slides in with a spring and is hidden below `md`. Only the close button closes it (no Escape, no outside click). `width` is `"w-96"` (default) or `"w-80"`. `closeLabel` defaults to `"Close panel"`. `className` merges last.
216
216
  - `ToolbarSearchInput`: a compact `h-8` search field for toolbars, with a search icon and a clear button. The clear button stays mounted and is hidden while the value is blank. Clearing calls `onChange("")` and does not move focus. Defaults: `placeholder="Search..."`, `label="Search"` (the `aria-label`), `clearLabel="Clear search"`. The ref goes to the `<input>`.
217
217
  - `ConfirmDialog` `size`: `"default"` keeps the alert dialog. `"sm"` renders a compact, dismissible dialog (outside click, Escape and the close button close it) with small Cancel and confirm buttons. Cancel calls `onCancel` and then `onOpenChange(false)`. Confirm calls `onConfirm` only, so the caller closes it. `"md"` (0.5.2) is dismissible like `"sm"`, with the title in a header (`pb-4`), the description in its own section (`py-6`) and a footer (`pt-4`) with default-size buttons. It has the same Cancel and confirm behaviour as `"sm"`.
218
- - Legacy (still supported): the `ConfirmDialog` `size`, `variant`, `cancelLabel` and `confirmLabel` props. For anything they do not cover, compose the parts (see [Composable parts](#composable-parts-060)).
218
+ - Legacy (still supported): the `ConfirmDialog` `size`, `variant`, `cancelLabel` and `confirmLabel` props. For anything they do not cover, compose the parts (see [Composable parts](#composable-parts-060-options-061)).
219
219
 
220
220
  ## Buttons, menus and fields (0.4.4)
221
221
 
@@ -381,7 +381,7 @@ function PanelWidth() {
381
381
  </Tabs>
382
382
  ```
383
383
 
384
- ## Composable parts (0.6.0)
384
+ ## Composable parts (0.6.0, options 0.6.1)
385
385
 
386
386
  Shadcn-style building blocks. The prop-driven components above are thin compositions of them and render the same DOM.
387
387
 
@@ -391,18 +391,18 @@ Convention: compound parts are static members of a namespace object, `<Namespace
391
391
 
392
392
  | Part | Purpose |
393
393
  | --- | --- |
394
- | `Root` | Radix dialog root. `size` (`"default"`, `"sm"`, `"md"`: layout slot for the other parts), `alert` (blocking alert dialog; default `size === "default"`), `pending` (swallows close requests, disables Cancel, Action shows its pending state), `confirmPhrase` (type-to-confirm; the typed text resets each time the dialog opens). |
394
+ | `Root` | Radix dialog root. `size` (`"default"`, `"sm"`, `"md"`: layout slot for the other parts), `alert` (blocking alert dialog; default `size === "default"`), `pending` (swallows close requests, disables Cancel, Action shows its pending state), `confirmPhrase` (type-to-confirm; the typed text resets each time the dialog opens). 0.6.1: `dismissibleWhilePending` (Escape, outside click and the close button still close while `pending`; Cancel stays disabled), `focusCancel` (with `alert={false}`, Cancel gets `data-alert-cancel` so a `role="alertdialog"` Content focuses it on open). |
395
395
  | `Content` | `AlertDialogContent` with `alert`, else `DialogContent`. `sm` and `md` add `sm:max-w-96`. Takes every `DialogContent` prop (`role`, `hideCloseButton`, ...). |
396
396
  | `Header` | `DialogHeader`; `md` adds `pb-4`. |
397
397
  | `Title` | `DialogTitle`; `sm` adds `mb-2 text-sm font-medium`. |
398
398
  | `Description` | `DialogDescription`; `sm` and `md` add their spacing. |
399
399
  | `Body` | Plain `<div>` for extra content. No default classes. |
400
- | `Footer` | `DialogFooter` (`md` adds `pt-4`); `sm` renders a `flex justify-end gap-2` row. |
401
- | `Cancel` | Alert: the outline `AlertDialogCancel` (closes through Radix). Otherwise a ghost `Button` that runs `onClick`, then (unless it called `preventDefault()`) the Root `onOpenChange(false)`. Disabled while pending. Default text `"Cancel"`. |
402
- | `Action` | Confirm button. Never closes the dialog. Disabled while `disabled`, pending, or until `confirmPhrase` matches. `pendingLabel` replaces the children with a spinner (`spinnerSize`, default 14) and the label while pending. |
400
+ | `Footer` | `DialogFooter` (`md` adds `pt-4`); `sm` renders a `flex justify-end gap-2` row. 0.6.1: `row` keeps a right-aligned row at every width, compact sheet included (`justify-end flex-row!`); no-op for `sm`. |
401
+ | `Cancel` | Alert: the outline `AlertDialogCancel` (closes through Radix). Otherwise a ghost `Button` that runs `onClick`, then (unless it called `preventDefault()`) the Root `onOpenChange(false)`. In both modes `preventDefault()` in `onClick` skips the close, and `variant` overrides the default. Disabled while pending. Default text `"Cancel"`. |
402
+ | `Action` | Confirm button. Never closes the dialog. Disabled while `disabled`, pending, or until `confirmPhrase` matches. `pendingLabel` replaces the children with a spinner (`spinnerSize`, default 14) and the label while pending. `pending` on Action alone shows the pending state without blocking close. 0.6.1: `spinnerClassName` merges after `animate-spin` (for example `"mr-2"`). |
403
403
  | `Field` | Type-to-confirm input bound to `confirmPhrase` (trimmed, case-insensitive). `label` replaces the label content, `onEnter` runs on Enter while the phrase matches, `inputProps` go to the `Input`. |
404
404
 
405
- `useConfirmDialog()` returns `{ size, alert, pending, phraseMatches, typed, setTyped, requestClose }` for custom parts.
405
+ `useConfirmDialog()` returns `{ size, alert, pending, focusCancel, phraseMatches, typed, setTyped, requestClose }` for custom parts.
406
406
 
407
407
  ### `AppShell.*`
408
408
 
@@ -483,6 +483,27 @@ A dismissible rename dialog:
483
483
  </ConfirmDialog.Root>
484
484
  ```
485
485
 
486
+ A dismissible alert (0.6.1 options) that focuses Cancel, keeps a button row on mobile, and can still be closed while busy:
487
+
488
+ ```tsx
489
+ <ConfirmDialog.Root open={open} onOpenChange={setOpen} alert={false} focusCancel pending={busy} dismissibleWhilePending>
490
+ <ConfirmDialog.Content role="alertdialog">
491
+ <ConfirmDialog.Header>
492
+ <ConfirmDialog.Title>Delete item?</ConfirmDialog.Title>
493
+ <ConfirmDialog.Description>This cannot be undone.</ConfirmDialog.Description>
494
+ </ConfirmDialog.Header>
495
+ <ConfirmDialog.Footer row>
496
+ <ConfirmDialog.Cancel variant="outline" />
497
+ <ConfirmDialog.Action variant="destructive" pendingLabel="Delete" spinnerSize={16} spinnerClassName="mr-2" onClick={remove}>
498
+ Delete
499
+ </ConfirmDialog.Action>
500
+ </ConfirmDialog.Footer>
501
+ </ConfirmDialog.Content>
502
+ </ConfirmDialog.Root>
503
+ ```
504
+
505
+ Parts read the nearest Root through context, so `Footer`, `Cancel` and `Action` work inside a child component or a `<form>` rendered in `Content`. To show another step, render a different footer (and Content `role`) conditionally inside the same Root and Content.
506
+
486
507
  A full shell:
487
508
 
488
509
  ```tsx
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vvedantb/ui",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "license": "MIT",
5
5
  "description": "Shared React 19 UI for web apps: one JavaScript entry (primitives, shell pieces, app helpers, native tokens), design-token CSS, shared Tailwind v4 CSS and a Tailwind preset.",
6
6
  "type": "module",
@@ -24,11 +24,12 @@ type ConfirmDialogContextValue = {
24
24
  size: ConfirmDialogSize;
25
25
  alert: boolean;
26
26
  pending: boolean;
27
+ focusCancel: boolean;
27
28
  confirmPhrase: string | undefined;
28
29
  typed: string;
29
30
  setTyped: (value: string) => void;
30
31
  phraseMatches: boolean;
31
- /** Calls the Root's `onOpenChange(false)` unless `pending`. */
32
+ /** Calls the Root's `onOpenChange(false)` unless `pending` (and not `dismissibleWhilePending`). */
32
33
  requestClose: () => void;
33
34
  };
34
35
 
@@ -36,6 +37,7 @@ const ConfirmDialogContext = React.createContext<ConfirmDialogContextValue>({
36
37
  size: "default",
37
38
  alert: true,
38
39
  pending: false,
40
+ focusCancel: false,
39
41
  confirmPhrase: undefined,
40
42
  typed: "",
41
43
  setTyped: () => {},
@@ -59,6 +61,18 @@ export type ConfirmDialogRootProps = React.ComponentProps<typeof Dialog> & {
59
61
  * Needs a controlled `open`.
60
62
  */
61
63
  pending?: boolean;
64
+ /**
65
+ * While `pending`, Escape, outside click and the close button still close
66
+ * (Root `onOpenChange(false)` runs). Cancel stays disabled and Action stays
67
+ * pending. Default `false`.
68
+ */
69
+ dismissibleWhilePending?: boolean;
70
+ /**
71
+ * Non-alert only: Cancel renders `data-alert-cancel=""`, so a Content with
72
+ * `role="alertdialog"` focuses it on open. An explicit `data-alert-cancel`
73
+ * on Cancel wins. No-op with `alert` (its Cancel already has it). Default `false`.
74
+ */
75
+ focusCancel?: boolean;
62
76
  /** Type-to-confirm phrase for `ConfirmDialog.Field`. Action stays disabled until it matches (trimmed, case-insensitive). */
63
77
  confirmPhrase?: string;
64
78
  };
@@ -67,6 +81,8 @@ function Root({
67
81
  size = "default",
68
82
  alert = size === "default",
69
83
  pending = false,
84
+ dismissibleWhilePending = false,
85
+ focusCancel = false,
70
86
  confirmPhrase,
71
87
  open,
72
88
  onOpenChange,
@@ -81,7 +97,7 @@ function Root({
81
97
  }
82
98
 
83
99
  const handleOpenChange = (next: boolean) => {
84
- if (!next && pending) return;
100
+ if (!next && pending && !dismissibleWhilePending) return;
85
101
  if (next) setTyped("");
86
102
  onOpenChange?.(next);
87
103
  };
@@ -96,6 +112,7 @@ function Root({
96
112
  size,
97
113
  alert,
98
114
  pending,
115
+ focusCancel,
99
116
  confirmPhrase,
100
117
  typed,
101
118
  setTyped,
@@ -163,12 +180,27 @@ function Body(props: ConfirmDialogBodyProps) {
163
180
  }
164
181
  Body.displayName = "ConfirmDialog.Body";
165
182
 
166
- export type ConfirmDialogFooterProps = React.HTMLAttributes<HTMLDivElement>;
183
+ export type ConfirmDialogFooterProps = React.HTMLAttributes<HTMLDivElement> & {
184
+ /**
185
+ * Right-aligned row of buttons at every width, laid out like a
186
+ * `flex justify-end gap-2` div inside the footer. Adds `justify-end flex-row!`:
187
+ * the `!important` beats both the footer's own `flex-col-reverse` below `sm`
188
+ * and the compact sheet's descendant `flex-col-reverse` rule, which has higher
189
+ * specificity than a plain class. Keeps `data-slot` and every other footer
190
+ * class. No-op for `size="sm"` (already a row). Default `false`.
191
+ */
192
+ row?: boolean;
193
+ };
167
194
 
168
- function Footer({ className, ...props }: ConfirmDialogFooterProps) {
195
+ function Footer({ row = false, className, ...props }: ConfirmDialogFooterProps) {
169
196
  const { size } = useConfirmDialog();
170
197
  if (size === "sm") return <div {...props} className={cn("flex justify-end gap-2", className)} />;
171
- return <DialogFooter {...props} className={cn(size === "md" && "pt-4", className)} />;
198
+ return (
199
+ <DialogFooter
200
+ {...props}
201
+ className={cn(size === "md" && "pt-4", row && "justify-end flex-row!", className)}
202
+ />
203
+ );
172
204
  }
173
205
  Footer.displayName = "ConfirmDialog.Footer";
174
206
 
@@ -177,7 +209,8 @@ export type ConfirmDialogCancelProps = ButtonProps;
177
209
  /**
178
210
  * Alert: a `DialogClose` outline button. Otherwise a ghost button that runs
179
211
  * `onClick` and then, unless it called `preventDefault()`, the Root's
180
- * `onOpenChange(false)`. Disabled while pending.
212
+ * `onOpenChange(false)`. Disabled while pending. With Root `focusCancel`
213
+ * it also gets `data-alert-cancel`.
181
214
  */
182
215
  function Cancel({ children = "Cancel", variant, size, disabled, onClick, ...props }: ConfirmDialogCancelProps) {
183
216
  const dialog = useConfirmDialog();
@@ -198,6 +231,7 @@ function Cancel({ children = "Cancel", variant, size, disabled, onClick, ...prop
198
231
  <Button
199
232
  variant={variant ?? "ghost"}
200
233
  size={size ?? (dialog.size === "sm" ? "sm" : undefined)}
234
+ data-alert-cancel={dialog.focusCancel ? "" : undefined}
201
235
  {...props}
202
236
  onClick={(event) => {
203
237
  onClick?.(event);
@@ -218,6 +252,8 @@ export type ConfirmDialogActionProps = ButtonProps & {
218
252
  pendingLabel?: React.ReactNode;
219
253
  /** Spinner size in px. Default `14`. */
220
254
  spinnerSize?: number;
255
+ /** Merged into the spinner className after `animate-spin`, e.g. `"mr-2"`. */
256
+ spinnerClassName?: string;
221
257
  };
222
258
 
223
259
  /** Confirm button. Does not close the dialog. Disabled while pending or until the confirm phrase matches. */
@@ -225,6 +261,7 @@ function Action({
225
261
  pending: pendingProp,
226
262
  pendingLabel,
227
263
  spinnerSize = 14,
264
+ spinnerClassName,
228
265
  size,
229
266
  disabled,
230
267
  children,
@@ -235,7 +272,7 @@ function Action({
235
272
  const content =
236
273
  pending && pendingLabel !== undefined ? (
237
274
  <>
238
- <IconLoader2 size={spinnerSize} className="animate-spin" />
275
+ <IconLoader2 size={spinnerSize} className={cn("animate-spin", spinnerClassName)} />
239
276
  {pendingLabel}
240
277
  </>
241
278
  ) : (