@pk-design/react-tailwind 1.13.1 → 1.14.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.
@@ -167,27 +167,36 @@ const THEMED_BTN_CLASS = {
167
167
  plain: 'text-[var(--ui-danger-text)] hover:bg-current/10 active:bg-current/15 focus-visible:outline-[var(--ui-danger-ring)]',
168
168
  },
169
169
  secondary: {
170
- default: 'text-gray-700 bg-white border border-[var(--ui-border)] hover:bg-gray-100 dark:bg-gray-800 dark:text-white dark:hover:bg-gray-700 focus-visible:outline-[var(--ui-focus-ring)]',
171
- outlined: 'text-gray-700 hover:text-white border border-[var(--ui-border)] hover:bg-gray-700 dark:text-gray-200 dark:hover:text-white dark:hover:bg-gray-600 focus-visible:outline-[var(--ui-focus-ring)]',
170
+ default: 'text-gray-800 bg-gray-200 hover:bg-gray-300 active:bg-gray-300 dark:text-white dark:bg-gray-700 dark:hover:bg-gray-600 dark:active:bg-gray-600 focus-visible:outline-[var(--ui-focus-ring)]',
171
+ outlined: 'text-gray-800 bg-white border border-gray-300 shadow-xs hover:bg-gray-100 active:bg-gray-200 dark:text-gray-100 dark:bg-transparent dark:border-gray-600 dark:shadow-none dark:hover:bg-white/10 dark:active:bg-white/15 focus-visible:outline-[var(--ui-focus-ring)]',
172
172
  plain: 'text-gray-700 hover:bg-current/10 active:bg-current/15 dark:text-gray-200 focus-visible:outline-[var(--ui-focus-ring)]',
173
173
  },
174
174
  success: {
175
- default: 'text-white bg-emerald-600 hover:bg-emerald-700 focus-visible:outline-emerald-500',
176
- outlined: 'text-emerald-700 hover:text-white border border-emerald-600 hover:bg-emerald-600 dark:text-emerald-400 dark:border-emerald-500 focus-visible:outline-emerald-500',
175
+ default: 'text-white bg-emerald-700 hover:bg-emerald-800 focus-visible:outline-emerald-500',
176
+ outlined: 'text-emerald-700 hover:text-white border border-emerald-700 hover:bg-emerald-700 dark:text-emerald-400 dark:border-emerald-500 focus-visible:outline-emerald-500',
177
177
  plain: 'text-emerald-700 hover:bg-current/10 active:bg-current/15 dark:text-emerald-400 focus-visible:outline-emerald-500',
178
178
  },
179
179
  warning: {
180
- default: 'text-white bg-amber-500 hover:bg-amber-600 focus-visible:outline-amber-500',
181
- outlined: 'text-amber-700 hover:text-white border border-amber-500 hover:bg-amber-500 dark:text-amber-400 dark:border-amber-500 focus-visible:outline-amber-500',
180
+ default: 'text-gray-900 bg-amber-500 hover:bg-amber-400 focus-visible:outline-amber-500',
181
+ outlined: 'text-amber-700 hover:text-gray-900 border border-amber-500 hover:bg-amber-500 dark:text-amber-400 dark:border-amber-500 focus-visible:outline-amber-500',
182
182
  plain: 'text-amber-700 hover:bg-current/10 active:bg-current/15 dark:text-amber-400 focus-visible:outline-amber-500',
183
183
  },
184
184
  };
185
185
  const SIZE_CLASSES = {
186
- xs: { normal: 'h-7 px-2.5 rounded-sm gap-1 text-xs', iconOnly: 'h-7 w-7 p-1 rounded-sm text-xs' },
187
- sm: { normal: 'h-8 px-3 rounded gap-1.5 text-sm', iconOnly: 'h-8 w-8 p-1.5 rounded text-sm' },
188
- md: { normal: 'h-9 px-4 rounded-md gap-2 text-sm', iconOnly: 'h-9 w-9 p-2 rounded-md text-sm' },
186
+ xs: {
187
+ normal: 'min-h-7 px-2.5 py-1 rounded-sm gap-1 text-xs',
188
+ iconOnly: 'h-7 w-7 p-1 rounded-sm text-xs',
189
+ },
190
+ sm: {
191
+ normal: 'min-h-8 px-3 py-1 rounded gap-1.5 text-sm',
192
+ iconOnly: 'h-8 w-8 p-1.5 rounded text-sm',
193
+ },
194
+ md: {
195
+ normal: 'min-h-9 px-4 py-1.5 rounded-md gap-2 text-sm',
196
+ iconOnly: 'h-9 w-9 p-2 rounded-md text-sm',
197
+ },
189
198
  lg: {
190
- normal: 'h-10 px-5 rounded-lg gap-2 text-base',
199
+ normal: 'min-h-10 px-5 py-2 rounded-lg gap-2 text-base',
191
200
  iconOnly: 'h-10 w-10 p-2.5 rounded-lg text-base',
192
201
  },
193
202
  };
@@ -202,12 +211,18 @@ const LOADER_SIZE = {
202
211
  *
203
212
  * Disabled styling is keyed off both the native `disabled` attribute and
204
213
  * `aria-disabled`, so non-button elements rendered via `as` look disabled too.
214
+ *
215
+ * On touch screens an invisible layer makes the tappable area at least 44px,
216
+ * whatever the button's size, without changing how it looks.
205
217
  */
206
- const BASE_BTN_CLASS = 'font-medium inline-flex items-center justify-center shrink-0 cursor-pointer select-none ' +
218
+ const BASE_BTN_CLASS = 'relative font-medium inline-flex items-center justify-center text-center shrink-0 cursor-pointer select-none ' +
207
219
  'transition-[color,background-color,border-color,box-shadow,opacity,transform] duration-150 ease-out ' +
208
220
  'active:scale-[0.98] focus:outline-none ' +
209
221
  'disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none ' +
210
- 'aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-disabled:shadow-none';
222
+ 'aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-disabled:shadow-none ' +
223
+ 'pointer-coarse:after:absolute pointer-coarse:after:top-1/2 pointer-coarse:after:left-1/2 ' +
224
+ 'pointer-coarse:after:-translate-x-1/2 pointer-coarse:after:-translate-y-1/2 ' +
225
+ 'pointer-coarse:after:w-full pointer-coarse:after:min-w-11 pointer-coarse:after:h-full pointer-coarse:after:min-h-11';
211
226
  const Button = React.forwardRef(({ as, type, ...props }, ref) => {
212
227
  const { className = '', children, theme = 'primary', variant = 'default', size = 'md', leftIcon, rightIcon, fullWidth = false, loading = false, loadingText = '', rounded = false, disabled, iconOnly = false, noOutlineOnFocus = false, active, onClick, ...restProps } = props;
213
228
  const Component = as || 'button';
@@ -259,7 +274,10 @@ const Button = React.forwardRef(({ as, type, ...props }, ref) => {
259
274
  const elementProps = isNativeButton
260
275
  ? {
261
276
  type: type !== null && type !== void 0 ? type : 'button',
262
- disabled: resolvedDisabled,
277
+ // Not `disabled` while loading: that would drop keyboard focus from the
278
+ // button just pressed. handleClick ignores presses until it's done.
279
+ disabled: Boolean(disabled),
280
+ 'aria-disabled': loading && !disabled ? true : undefined,
263
281
  role,
264
282
  tabIndex,
265
283
  onClick: handleClick,
@@ -289,7 +307,9 @@ const Button = React.forwardRef(({ as, type, ...props }, ref) => {
289
307
  return buildClassName(BASE_BTN_CLASS, ((_a = THEMED_BTN_CLASS[theme]) === null || _a === void 0 ? void 0 : _a[variant]) || THEMED_BTN_CLASS.primary.default, iconOnly ? sizeObj.iconOnly : sizeObj.normal,
290
308
  // An outline (not a ring) so the offset gap stays transparent on any surface/mode.
291
309
  !noOutlineOnFocus &&
292
- 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid', rounded && 'rounded-full', fullWidth && 'w-full', variant === 'default' && 'shadow-xs hover:shadow-none', active && 'ring-2 ring-current', className);
310
+ 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid', rounded && 'rounded-full', fullWidth && 'w-full',
311
+ // Secondary default is a flat grey fill; a shadow would bring back the outlined look.
312
+ variant === 'default' && theme !== 'secondary' && 'shadow-xs hover:shadow-none', active && 'ring-2 ring-current', className);
293
313
  }, [theme, variant, sizeObj, iconOnly, noOutlineOnFocus, rounded, fullWidth, active, className]);
294
314
  // The spinner and loading text inherit the button's text colour.
295
315
  const loaderIcon = (jsxRuntime.jsx(Loader, { "data-testid": "btn-loader-icon", size: LOADER_SIZE[size] || 'xs', className: "text-current" }));
@@ -380,6 +400,86 @@ const useResponsiveValue = (value) => {
380
400
  return value.base;
381
401
  };
382
402
 
403
+ // history.back() lands later, so a drawer opening meanwhile (one replacing another,
404
+ // or React re-running effects under StrictMode) would have its new entry removed
405
+ // instead, and close at once. Removals are counted, and an opening drawer waits for them.
406
+ let pendingBacks = 0;
407
+ const afterBacks = [];
408
+ function settleBack() {
409
+ pendingBacks = Math.max(0, pendingBacks - 1);
410
+ if (pendingBacks === 0)
411
+ afterBacks.splice(0).forEach((run) => run());
412
+ }
413
+ function removeEntry() {
414
+ pendingBacks += 1;
415
+ let settled = false;
416
+ const settle = () => {
417
+ if (settled)
418
+ return;
419
+ settled = true;
420
+ window.removeEventListener('popstate', settle);
421
+ settleBack();
422
+ };
423
+ window.addEventListener('popstate', settle);
424
+ // A back that never reports (nothing to go back to) mustn't hold others up.
425
+ window.setTimeout(settle, 500);
426
+ window.history.back();
427
+ }
428
+ /**
429
+ * While `active`, the browser's back button (and Android's back gesture)
430
+ * closes the drawer instead of leaving the page: opening adds a history entry
431
+ * (keeping the current state, so routers see the same location), and back
432
+ * removes it. Closed any other way, the entry is removed too, but only if it's
433
+ * still the latest one, so a form that saves and then navigates isn't undone.
434
+ */
435
+ function useCloseOnBack(active, onClose) {
436
+ const onCloseRef = React.useRef(onClose);
437
+ React.useEffect(() => {
438
+ onCloseRef.current = onClose;
439
+ });
440
+ const token = React.useId();
441
+ React.useEffect(() => {
442
+ if (!active || typeof window === 'undefined')
443
+ return;
444
+ const pushEntry = () => { var _a; return window.history.pushState({ ...((_a = window.history.state) !== null && _a !== void 0 ? _a : {}), drawer: token }, ''); };
445
+ let entryPresent = false;
446
+ let disposed = false;
447
+ const handlePop = () => {
448
+ var _a;
449
+ if (((_a = window.history.state) === null || _a === void 0 ? void 0 : _a.drawer) === token)
450
+ return;
451
+ entryPresent = false;
452
+ onCloseRef.current();
453
+ // onClose may step back within the drawer instead of closing it; if it's
454
+ // still open afterwards, take back its entry so the next back works too.
455
+ window.setTimeout(() => {
456
+ if (disposed)
457
+ return;
458
+ pushEntry();
459
+ entryPresent = true;
460
+ }, 0);
461
+ };
462
+ const start = () => {
463
+ if (disposed)
464
+ return;
465
+ pushEntry();
466
+ entryPresent = true;
467
+ window.addEventListener('popstate', handlePop);
468
+ };
469
+ if (pendingBacks > 0)
470
+ afterBacks.push(start);
471
+ else
472
+ start();
473
+ return () => {
474
+ var _a;
475
+ disposed = true;
476
+ window.removeEventListener('popstate', handlePop);
477
+ if (entryPresent && ((_a = window.history.state) === null || _a === void 0 ? void 0 : _a.drawer) === token)
478
+ removeEntry();
479
+ };
480
+ }, [active, token]);
481
+ }
482
+
383
483
  const alignmentMap = {
384
484
  center: 'items-center justify-center h-full',
385
485
  start: 'justify-start h-full',
@@ -478,85 +578,6 @@ const Drawer = ({ isOpen, onClose = () => { }, closeOnOutsideClick = true, align
478
578
  : [panelAlignmentMap[align], panelSizeMap[size], panelClass])), "data-layout": fullscreen ? 'fullscreen' : undefined, children: [shouldShowDragHandle && (jsxRuntime.jsx("div", { className: "w-12 h-1.5 rounded-full bg-gray-300 dark:bg-gray-600 mx-auto my-2.5 shrink-0 cursor-grab", "data-testid": "drawer-drag-handle" })), fullscreen && (jsxRuntime.jsxs("div", { className: "sticky top-0 z-10 flex h-14 shrink-0 items-center gap-1 border-b border-[var(--ui-border)] bg-white pl-1 pr-3 pt-[env(safe-area-inset-top,0px)] box-content dark:bg-gray-800", children: [jsxRuntime.jsx(Button, { iconOnly: true, "aria-label": "Close", variant: "plain", theme: "secondary", onClick: onClose, className: "shrink-0", children: jsxRuntime.jsx(SvgX, { className: "size-5" }) }), title && (jsxRuntime.jsx(react.DialogTitle, { as: "h3", className: "min-w-0 flex-1 truncate text-base font-semibold text-gray-900 dark:text-white", children: title })), headerAction && jsxRuntime.jsx("div", { className: "ml-auto shrink-0", children: headerAction })] })), !fullscreen && showTitleSection && (jsxRuntime.jsxs("div", { className: buildClassName('space-y-1 py-3 px-4 md:px-6 flex flex-col border-b border-[var(--ui-border)] shrink-0', titleSticky && 'sticky top-0 bg-white dark:bg-gray-800 z-10'), children: [jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-3 min-w-0", children: [jsxRuntime.jsxs("div", { className: "flex items-center gap-2 min-w-0 flex-1", children: [showBackButton && (jsxRuntime.jsx(Button, { iconOnly: true, "aria-label": "Back", variant: "plain", theme: "secondary", size: "xs", onClick: onClose, children: jsxRuntime.jsx(SvgChevronLeft, { className: "size-4" }) })), title && (jsxRuntime.jsx(react.DialogTitle, { as: "h3", className: buildClassName('text-lg font-semibold text-gray-900 dark:text-white truncate tracking-tight min-w-0', titleClass), children: title }))] }), showCloseButton && (jsxRuntime.jsx(Button, { iconOnly: true, "aria-label": "Close drawer", variant: "plain", theme: "secondary", size: "xs", onClick: onClose, className: "rounded-full text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 shrink-0", children: jsxRuntime.jsx(SvgX, { className: "size-4" }) }))] }), description && (jsxRuntime.jsx("p", { className: buildClassName('text-sm text-gray-500 dark:text-gray-400 font-normal truncate', descriptionClass), children: description }))] })), jsxRuntime.jsxs("div", { className: buildClassName('flex-1 p-4 md:p-6 space-y-4 min-w-0', contentClass, fullscreen && 'pb-[calc(1rem+env(safe-area-inset-bottom,0px))]'), children: [fullscreen && description && (jsxRuntime.jsx("p", { className: buildClassName('text-sm text-gray-500 dark:text-gray-400', descriptionClass), children: description })), children] }), !fullscreen && footer && (jsxRuntime.jsx("div", { className: buildClassName('p-4 border-t border-[var(--ui-border-muted)] bg-white dark:bg-gray-800 shrink-0 min-w-0', footerSticky && 'sticky bottom-0 z-10'), children: footer }))] }) }) })] }));
479
579
  };
480
580
  Drawer.displayName = 'Drawer';
481
- // history.back() lands later, so a drawer opening meanwhile (one replacing another,
482
- // or React re-running effects under StrictMode) would have its new entry removed
483
- // instead, and close at once. Removals are counted, and an opening drawer waits for them.
484
- let pendingBacks = 0;
485
- const afterBacks = [];
486
- function settleBack() {
487
- pendingBacks = Math.max(0, pendingBacks - 1);
488
- if (pendingBacks === 0)
489
- afterBacks.splice(0).forEach((run) => run());
490
- }
491
- function removeEntry() {
492
- pendingBacks += 1;
493
- let settled = false;
494
- const settle = () => {
495
- if (settled)
496
- return;
497
- settled = true;
498
- window.removeEventListener('popstate', settle);
499
- settleBack();
500
- };
501
- window.addEventListener('popstate', settle);
502
- // A back that never reports (nothing to go back to) mustn't hold others up.
503
- window.setTimeout(settle, 500);
504
- window.history.back();
505
- }
506
- /**
507
- * While `active`, the browser's back button (and Android's back gesture)
508
- * closes the drawer instead of leaving the page: opening adds a history entry
509
- * (keeping the current state, so routers see the same location), and back
510
- * removes it. Closed any other way, the entry is removed too, but only if it's
511
- * still the latest one, so a form that saves and then navigates isn't undone.
512
- */
513
- function useCloseOnBack(active, onClose) {
514
- const onCloseRef = React.useRef(onClose);
515
- React.useEffect(() => {
516
- onCloseRef.current = onClose;
517
- });
518
- const token = React.useId();
519
- React.useEffect(() => {
520
- if (!active || typeof window === 'undefined')
521
- return;
522
- const pushEntry = () => { var _a; return window.history.pushState({ ...((_a = window.history.state) !== null && _a !== void 0 ? _a : {}), drawer: token }, ''); };
523
- let entryPresent = false;
524
- let disposed = false;
525
- const handlePop = () => {
526
- var _a;
527
- if (((_a = window.history.state) === null || _a === void 0 ? void 0 : _a.drawer) === token)
528
- return;
529
- entryPresent = false;
530
- onCloseRef.current();
531
- // onClose may step back within the drawer instead of closing it; if it's
532
- // still open afterwards, take back its entry so the next back works too.
533
- window.setTimeout(() => {
534
- if (disposed)
535
- return;
536
- pushEntry();
537
- entryPresent = true;
538
- }, 0);
539
- };
540
- const start = () => {
541
- if (disposed)
542
- return;
543
- pushEntry();
544
- entryPresent = true;
545
- window.addEventListener('popstate', handlePop);
546
- };
547
- if (pendingBacks > 0)
548
- afterBacks.push(start);
549
- else
550
- start();
551
- return () => {
552
- var _a;
553
- disposed = true;
554
- window.removeEventListener('popstate', handlePop);
555
- if (entryPresent && ((_a = window.history.state) === null || _a === void 0 ? void 0 : _a.drawer) === token)
556
- removeEntry();
557
- };
558
- }, [active, token]);
559
- }
560
581
 
561
582
  function ActionSheet({ isOpen, onClose, title, description, actions, showCancelButton = true, cancelLabel = 'Cancel', onCancel, closeOnBackdropClick = true, dragHandle = true, panelClass = '', containerClass = '', }) {
562
583
  const handleItemClick = (action) => {
@@ -5332,6 +5353,7 @@ exports.isEmpty = isEmpty;
5332
5353
  exports.nextSortDirection = nextSortDirection;
5333
5354
  exports.updateSortQuery = updateSortQuery;
5334
5355
  exports.useBreakpoint = useBreakpoint;
5356
+ exports.useCloseOnBack = useCloseOnBack;
5335
5357
  exports.useDebounce = useDebounce;
5336
5358
  exports.useDebounceFn = useDebounceFn;
5337
5359
  exports.useIsMobile = useIsMobile;