@pk-design/react-tailwind 1.13.0 → 1.13.2

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.
@@ -147,27 +147,36 @@ const THEMED_BTN_CLASS = {
147
147
  plain: 'text-[var(--ui-danger-text)] hover:bg-current/10 active:bg-current/15 focus-visible:outline-[var(--ui-danger-ring)]',
148
148
  },
149
149
  secondary: {
150
- 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)]',
151
- 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)]',
150
+ 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)]',
151
+ 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)]',
152
152
  plain: 'text-gray-700 hover:bg-current/10 active:bg-current/15 dark:text-gray-200 focus-visible:outline-[var(--ui-focus-ring)]',
153
153
  },
154
154
  success: {
155
- default: 'text-white bg-emerald-600 hover:bg-emerald-700 focus-visible:outline-emerald-500',
156
- 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',
155
+ default: 'text-white bg-emerald-700 hover:bg-emerald-800 focus-visible:outline-emerald-500',
156
+ 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',
157
157
  plain: 'text-emerald-700 hover:bg-current/10 active:bg-current/15 dark:text-emerald-400 focus-visible:outline-emerald-500',
158
158
  },
159
159
  warning: {
160
- default: 'text-white bg-amber-500 hover:bg-amber-600 focus-visible:outline-amber-500',
161
- 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',
160
+ default: 'text-gray-900 bg-amber-500 hover:bg-amber-400 focus-visible:outline-amber-500',
161
+ 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',
162
162
  plain: 'text-amber-700 hover:bg-current/10 active:bg-current/15 dark:text-amber-400 focus-visible:outline-amber-500',
163
163
  },
164
164
  };
165
165
  const SIZE_CLASSES = {
166
- xs: { normal: 'h-7 px-2.5 rounded-sm gap-1 text-xs', iconOnly: 'h-7 w-7 p-1 rounded-sm text-xs' },
167
- sm: { normal: 'h-8 px-3 rounded gap-1.5 text-sm', iconOnly: 'h-8 w-8 p-1.5 rounded text-sm' },
168
- md: { normal: 'h-9 px-4 rounded-md gap-2 text-sm', iconOnly: 'h-9 w-9 p-2 rounded-md text-sm' },
166
+ xs: {
167
+ normal: 'min-h-7 px-2.5 py-1 rounded-sm gap-1 text-xs',
168
+ iconOnly: 'h-7 w-7 p-1 rounded-sm text-xs',
169
+ },
170
+ sm: {
171
+ normal: 'min-h-8 px-3 py-1 rounded gap-1.5 text-sm',
172
+ iconOnly: 'h-8 w-8 p-1.5 rounded text-sm',
173
+ },
174
+ md: {
175
+ normal: 'min-h-9 px-4 py-1.5 rounded-md gap-2 text-sm',
176
+ iconOnly: 'h-9 w-9 p-2 rounded-md text-sm',
177
+ },
169
178
  lg: {
170
- normal: 'h-10 px-5 rounded-lg gap-2 text-base',
179
+ normal: 'min-h-10 px-5 py-2 rounded-lg gap-2 text-base',
171
180
  iconOnly: 'h-10 w-10 p-2.5 rounded-lg text-base',
172
181
  },
173
182
  };
@@ -182,12 +191,18 @@ const LOADER_SIZE = {
182
191
  *
183
192
  * Disabled styling is keyed off both the native `disabled` attribute and
184
193
  * `aria-disabled`, so non-button elements rendered via `as` look disabled too.
194
+ *
195
+ * On touch screens an invisible layer makes the tappable area at least 44px,
196
+ * whatever the button's size, without changing how it looks.
185
197
  */
186
- const BASE_BTN_CLASS = 'font-medium inline-flex items-center justify-center shrink-0 cursor-pointer select-none ' +
198
+ const BASE_BTN_CLASS = 'relative font-medium inline-flex items-center justify-center text-center shrink-0 cursor-pointer select-none ' +
187
199
  'transition-[color,background-color,border-color,box-shadow,opacity,transform] duration-150 ease-out ' +
188
200
  'active:scale-[0.98] focus:outline-none ' +
189
201
  'disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none ' +
190
- 'aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-disabled:shadow-none';
202
+ 'aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-disabled:shadow-none ' +
203
+ 'pointer-coarse:after:absolute pointer-coarse:after:top-1/2 pointer-coarse:after:left-1/2 ' +
204
+ 'pointer-coarse:after:-translate-x-1/2 pointer-coarse:after:-translate-y-1/2 ' +
205
+ 'pointer-coarse:after:w-full pointer-coarse:after:min-w-11 pointer-coarse:after:h-full pointer-coarse:after:min-h-11';
191
206
  const Button = forwardRef(({ as, type, ...props }, ref) => {
192
207
  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;
193
208
  const Component = as || 'button';
@@ -239,7 +254,10 @@ const Button = forwardRef(({ as, type, ...props }, ref) => {
239
254
  const elementProps = isNativeButton
240
255
  ? {
241
256
  type: type !== null && type !== void 0 ? type : 'button',
242
- disabled: resolvedDisabled,
257
+ // Not `disabled` while loading: that would drop keyboard focus from the
258
+ // button just pressed. handleClick ignores presses until it's done.
259
+ disabled: Boolean(disabled),
260
+ 'aria-disabled': loading && !disabled ? true : undefined,
243
261
  role,
244
262
  tabIndex,
245
263
  onClick: handleClick,
@@ -269,7 +287,9 @@ const Button = forwardRef(({ as, type, ...props }, ref) => {
269
287
  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,
270
288
  // An outline (not a ring) so the offset gap stays transparent on any surface/mode.
271
289
  !noOutlineOnFocus &&
272
- '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);
290
+ 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid', rounded && 'rounded-full', fullWidth && 'w-full',
291
+ // Secondary default is a flat grey fill; a shadow would bring back the outlined look.
292
+ variant === 'default' && theme !== 'secondary' && 'shadow-xs hover:shadow-none', active && 'ring-2 ring-current', className);
273
293
  }, [theme, variant, sizeObj, iconOnly, noOutlineOnFocus, rounded, fullWidth, active, className]);
274
294
  // The spinner and loading text inherit the button's text colour.
275
295
  const loaderIcon = (jsx(Loader, { "data-testid": "btn-loader-icon", size: LOADER_SIZE[size] || 'xs', className: "text-current" }));
@@ -372,8 +392,10 @@ const panelAlignmentMap = {
372
392
  start: 'data-[closed]:-translate-x-1/2 rounded-r-2xl',
373
393
  end: 'data-[closed]:translate-x-1/2 rounded-l-2xl',
374
394
  top: 'data-[closed]:-translate-y-1/2 rounded-b-2xl',
375
- // Bottom sheets clear the home indicator on notched phones (0 elsewhere).
376
- bottom: 'data-[closed]:translate-y-full rounded-t-2xl md:rounded-t-3xl max-h-[88vh] max-h-[88dvh] pb-[env(safe-area-inset-bottom,0px)]',
395
+ // Bottom sheets clear the home indicator on notched phones (0 elsewhere). Never
396
+ // taller than the container: with the keyboard up it's shorter than 88dvh, and a
397
+ // bottom-anchored panel past its top is off screen and can't be scrolled to.
398
+ bottom: 'data-[closed]:translate-y-full rounded-t-2xl md:rounded-t-3xl max-h-[min(88dvh,100%)] pb-[env(safe-area-inset-bottom,0px)]',
377
399
  };
378
400
  const panelSizeMap = {
379
401
  xs: 'max-w-xs',
@@ -456,6 +478,31 @@ const Drawer = ({ isOpen, onClose = () => { }, closeOnOutsideClick = true, align
456
478
  : [panelAlignmentMap[align], panelSizeMap[size], panelClass])), "data-layout": fullscreen ? 'fullscreen' : undefined, children: [shouldShowDragHandle && (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 && (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: [jsx(Button, { iconOnly: true, "aria-label": "Close", variant: "plain", theme: "secondary", onClick: onClose, className: "shrink-0", children: jsx(SvgX, { className: "size-5" }) }), title && (jsx(DialogTitle, { as: "h3", className: "min-w-0 flex-1 truncate text-base font-semibold text-gray-900 dark:text-white", children: title })), headerAction && jsx("div", { className: "ml-auto shrink-0", children: headerAction })] })), !fullscreen && showTitleSection && (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: [jsxs("div", { className: "flex items-center justify-between gap-3 min-w-0", children: [jsxs("div", { className: "flex items-center gap-2 min-w-0 flex-1", children: [showBackButton && (jsx(Button, { iconOnly: true, "aria-label": "Back", variant: "plain", theme: "secondary", size: "xs", onClick: onClose, children: jsx(SvgChevronLeft, { className: "size-4" }) })), title && (jsx(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 && (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: jsx(SvgX, { className: "size-4" }) }))] }), description && (jsx("p", { className: buildClassName('text-sm text-gray-500 dark:text-gray-400 font-normal truncate', descriptionClass), children: description }))] })), 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 && (jsx("p", { className: buildClassName('text-sm text-gray-500 dark:text-gray-400', descriptionClass), children: description })), children] }), !fullscreen && footer && (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 }))] }) }) })] }));
457
479
  };
458
480
  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
+ }
459
506
  /**
460
507
  * While `active`, the browser's back button (and Android's back gesture)
461
508
  * closes the drawer instead of leaving the page: opening adds a history entry
@@ -473,8 +520,7 @@ function useCloseOnBack(active, onClose) {
473
520
  if (!active || typeof window === 'undefined')
474
521
  return;
475
522
  const pushEntry = () => { var _a; return window.history.pushState({ ...((_a = window.history.state) !== null && _a !== void 0 ? _a : {}), drawer: token }, ''); };
476
- pushEntry();
477
- let entryPresent = true;
523
+ let entryPresent = false;
478
524
  let disposed = false;
479
525
  const handlePop = () => {
480
526
  var _a;
@@ -491,13 +537,23 @@ function useCloseOnBack(active, onClose) {
491
537
  entryPresent = true;
492
538
  }, 0);
493
539
  };
494
- window.addEventListener('popstate', handlePop);
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();
495
551
  return () => {
496
552
  var _a;
497
553
  disposed = true;
498
554
  window.removeEventListener('popstate', handlePop);
499
555
  if (entryPresent && ((_a = window.history.state) === null || _a === void 0 ? void 0 : _a.drawer) === token)
500
- window.history.back();
556
+ removeEntry();
501
557
  };
502
558
  }, [active, token]);
503
559
  }
@@ -4339,7 +4395,7 @@ const SelectBox = forwardRef((props, forwardedRef) => {
4339
4395
  const isOptSelected = selectedValueSet.has(optVal);
4340
4396
  return (jsx(ComboboxOption, { disabled: option.disabled, value: option, className: "group rounded-lg py-1.5 px-3 select-none data-[focus]:bg-gray-100 dark:data-[focus]:bg-white/10 data-disabled:opacity-50 cursor-pointer data-disabled:pointer-events-none", children: renderOption ? (renderOption(option, Boolean(isOptSelected))) : (jsxs("div", { className: "flex items-center justify-between gap-2 text-gray-900 dark:text-white group-data-[selected]:text-blue-600", children: [jsx("span", { className: "text-sm/6", children: getDisplayValue(option) }), jsx(SvgCheck, { className: "invisible size-5 dark:stroke-gray-300 group-data-[selected]:visible" })] })) }, optVal));
4341
4397
  };
4342
- return (jsxs("div", { className: buildClassName('group space-y-2', hasError && 'has-error', containerClass), ref: containerRef, children: [label && (jsxs("div", { className: buildClassName('flex items-center justify-between', labelWrapperClass), children: [jsx(Label, { className: labelClass, htmlFor: id, "aria-required": required, children: label }), labelHint && jsx(TextContent, { xsmall: true, children: labelHint })] })), jsxs(Combobox, { multiple: multiple, immediate: immediate, name: name, value: normalizedSelected, onChange: comboboxOnChangeWrapper, onClose: () => setQuery(''), disabled: disabled, by: (a, b) => (a && b ? getValue(a) === getValue(b) : a === b), children: [multiple ? (jsxs("div", { className: buildClassName(baseInputClass, 'flex flex-wrap gap-2 peer relative', rightPaddingClass, hasLeftGroup ? (size === 'lg' ? 'ps-12' : size === 'sm' ? 'ps-8' : 'ps-10') : ''), ref: forwardedRef, children: [hasLeftGroup && (jsx("span", { className: buildClassName('absolute left-0 top-0 h-full flex items-center justify-center text-gray-400 dark:text-gray-500', currentSize.group), children: leftGroup })), isArray(normalizedSelected) &&
4398
+ return (jsxs("div", { className: buildClassName('group', hasError && 'has-error', containerClass), ref: containerRef, children: [label && (jsxs("div", { className: buildClassName('flex items-center justify-between mb-1.5', labelWrapperClass), children: [jsx(Label, { className: labelClass, htmlFor: id, "aria-required": required, children: label }), labelHint && jsx(TextContent, { xsmall: true, children: labelHint })] })), jsxs(Combobox, { multiple: multiple, immediate: immediate, name: name, value: normalizedSelected, onChange: comboboxOnChangeWrapper, onClose: () => setQuery(''), disabled: disabled, by: (a, b) => (a && b ? getValue(a) === getValue(b) : a === b), children: [multiple ? (jsxs("div", { className: buildClassName(baseInputClass, 'flex flex-wrap gap-2 peer relative', rightPaddingClass, hasLeftGroup ? (size === 'lg' ? 'ps-12' : size === 'sm' ? 'ps-8' : 'ps-10') : ''), ref: forwardedRef, children: [hasLeftGroup && (jsx("span", { className: buildClassName('absolute left-0 top-0 h-full flex items-center justify-center text-gray-400 dark:text-gray-500', currentSize.group), children: leftGroup })), isArray(normalizedSelected) &&
4343
4399
  (normalizedSelected === null || normalizedSelected === void 0 ? void 0 : normalizedSelected.length) > 0 &&
4344
4400
  normalizedSelected.map((option, idx) => (jsx(Badge, { theme: "secondary", size: currentSize.badgeSize, removable: !disabled, onRemove: () => removeSelection(idx), children: getDisplayValue(option) }, `${getValue(option)}-${idx}`))), jsx(ComboboxInput, { className: "outline-none flex-1 dark:placeholder-gray-400 placeholder-gray-400 bg-transparent", "aria-label": name, value: query, autoComplete: "off", placeholder: (normalizedSelected === null || normalizedSelected === void 0 ? void 0 : normalizedSelected.length) ? '' : placeholder, onChange: (e) => {
4345
4401
  const val = e.target.value;
@@ -4470,7 +4526,7 @@ const QuantityStepper = forwardRef((props, ref) => {
4470
4526
  };
4471
4527
  const isAtMin = disabled || value <= min;
4472
4528
  const isAtMax = disabled || value >= max;
4473
- return (jsxs("div", { className: buildClassName('flex flex-col gap-1 inline-flex', className), children: [label && (jsxs("div", { className: "flex items-center justify-between gap-2", children: [jsx(Label, { htmlFor: stepperId, className: disabled ? 'opacity-70 cursor-not-allowed' : '', children: label }), labelHint && jsx(TextContent, { xsmall: true, children: labelHint })] })), jsxs("div", { className: buildClassName('inline-flex items-center rounded-lg shadow-2xs transition-all duration-150', 'outline outline-1 -outline-offset-1', CONTAINER_SIZE_CLASS[size], hasError
4529
+ return (jsxs("div", { className: buildClassName('flex flex-col gap-1.5 inline-flex', className), children: [label && (jsxs("div", { className: "flex items-center justify-between gap-2", children: [jsx(Label, { htmlFor: stepperId, className: disabled ? 'opacity-70 cursor-not-allowed' : '', children: label }), labelHint && jsx(TextContent, { xsmall: true, children: labelHint })] })), jsxs("div", { className: buildClassName('inline-flex items-center rounded-lg shadow-2xs transition-all duration-150', 'outline outline-1 -outline-offset-1', CONTAINER_SIZE_CLASS[size], hasError
4474
4530
  ? '!outline-red-500 !ring-1 !ring-red-500/50 dark:!outline-red-500'
4475
4531
  : 'outline-[var(--ui-border)]', disabled
4476
4532
  ? 'pointer-events-none bg-gray-100 dark:bg-gray-800 outline-[var(--ui-border-muted)]'