@kennycorrea/ui 0.11.3 → 0.11.4

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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.11.4 — 2026-10-20
4
+
5
+ - **`Card`** — Themes' unlayered `.rt-Card` padding (16px at size 2) beat the layered `p-0`, so every card was padded on top of `CardHeader`/`CardBody` (double padding; header dividers short of the edges). The zero is now `!p-0` plus an unlayered `.ui-card.rt-Card { padding: 0 }` in `tokens.css`. Padding utilities a caller passes (`p-*`, `px-*`, `md:py-*`…) are promoted to `!` so they still win.
6
+ - **`SegmentedControl fullWidth`** — items share the width (`minmax(0, 1fr)`, track `flex: 1 1 0`, inline padding 8px at `md` / 6px at `sm`; labels ellipsize) instead of growing the track past the pill, which pushed the thumb and buttons outside the rounded background and lost the end padding (four labels in 343px). The thumb lines up with each item inside the `--seg-pad` inset. Non-full controls still size to content and scroll. Example: `examples/SegmentedFull.tsx`.
7
+ - **`Drawer`, `Sheet`** — no outer hairline border; the elevation shadow and the rounded top carry the edge. Footer dividers stay.
8
+ - **Menus, popovers, selects** — Themes' `--shadow-5` starts with a `0 0 0 1px` ring that read as a border. `.rt-BaseMenuContent` (DropdownMenu, ContextMenu), `.rt-PopoverContent` and `.rt-SelectContent` get the same elevation without the ring, in light and dark (`--ui-float-shadow`). Covers Themes' own `DropdownMenu` used directly in apps.
9
+
3
10
  ## 0.11.2 — 2026-10-19
4
11
 
5
12
  - **`ChipGroup`** — centring the selected chip no longer calls `scrollIntoView`, whose `block: 'nearest'` scrolled the whole page to a strip below the fold (about 1000 px on a long page). The strip's own scroller is moved with `scrollTo({ left })`, and nothing else; `prefers-reduced-motion` gets an instant scroll.
@@ -21,9 +21,11 @@ export interface CardProps extends HTMLAttributes<HTMLDivElement>, CardStyleProp
21
21
  * Radix Themes' Card, with the padding taken off.
22
22
  *
23
23
  * Themes pads its card for you; the house card is built from a header and a body with a
24
- * rule between them, so the padding belongs to those rather than to the card. `p-0` is the
25
- * whole of the customisation. Variant and density reach the header, title and body through
26
- * context, and each can still override them.
24
+ * rule between them, so the padding belongs to those rather than to the card. Themes' padding
25
+ * is unlayered CSS and beats a layered `p-0`, so the zero is `!p-0` (plus an unlayered rule in
26
+ * `tokens.css`), and any padding utility a caller passes is promoted to `!` so it still wins.
27
+ * Variant and density reach the header, title and body through context, and each can still
28
+ * override them.
27
29
  */
28
30
  export declare function Card({ raised, variant, density, className, ...props }: CardProps): import("react").JSX.Element;
29
31
  export interface CardPartProps extends HTMLAttributes<HTMLDivElement>, CardStyleProps {
@@ -32,17 +32,34 @@ function useStyleAttrs(own) {
32
32
  ...responsiveAttrs('density', own.density ?? inherited.density),
33
33
  };
34
34
  }
35
+ const PADDING_CLASS = /^(?:[\w-]+:)*-?p[xytrblse]?-/;
36
+ /** `md:px-4` → `md:!px-4`; other classes untouched. Already-important classes are left alone. */
37
+ function importantPadding(className) {
38
+ if (!className)
39
+ return className;
40
+ return className
41
+ .split(/\s+/)
42
+ .map((c) => {
43
+ if (!c || c.includes('!') || !PADDING_CLASS.test(c))
44
+ return c;
45
+ const i = c.lastIndexOf(':') + 1;
46
+ return `${c.slice(0, i)}!${c.slice(i)}`;
47
+ })
48
+ .join(' ');
49
+ }
35
50
  /**
36
51
  * Radix Themes' Card, with the padding taken off.
37
52
  *
38
53
  * Themes pads its card for you; the house card is built from a header and a body with a
39
- * rule between them, so the padding belongs to those rather than to the card. `p-0` is the
40
- * whole of the customisation. Variant and density reach the header, title and body through
41
- * context, and each can still override them.
54
+ * rule between them, so the padding belongs to those rather than to the card. Themes' padding
55
+ * is unlayered CSS and beats a layered `p-0`, so the zero is `!p-0` (plus an unlayered rule in
56
+ * `tokens.css`), and any padding utility a caller passes is promoted to `!` so it still wins.
57
+ * Variant and density reach the header, title and body through context, and each can still
58
+ * override them.
42
59
  */
43
60
  export function Card({ raised = false, variant = 'outlined', density = 'comfortable', className, ...props }) {
44
61
  const attrs = useStyleAttrs({ variant, density });
45
- return (_jsx(CardContext.Provider, { value: { variant, density }, children: _jsx(ThemedCard, { size: "2", variant: raised ? 'classic' : 'surface', className: cn('ui-card p-0', className), ...attrs, ...props }) }));
62
+ return (_jsx(CardContext.Provider, { value: { variant, density }, children: _jsx(ThemedCard, { size: "2", variant: raised ? 'classic' : 'surface', className: cn('ui-card !p-0', importantPadding(className)), ...attrs, ...props }) }));
46
63
  }
47
64
  export function CardHeader({ variant, density, className, ...props }) {
48
65
  const attrs = useStyleAttrs({ variant, density });
@@ -192,6 +192,6 @@ export function Drawer({ mode = 'modal', open: openProp, defaultOpen = true, onO
192
192
  onPointerUp: end,
193
193
  onPointerCancel: cancel,
194
194
  };
195
- return (_jsxs(_Fragment, { children: [anchor, _jsx(ThemePortal, { target: target, children: _jsxs("div", { ref: wrapperRef, "data-drawer-root": "", children: [modal ? (_jsx("div", { "data-testid": "drawer-scrim", "aria-hidden": "true", className: "fixed inset-0 z-40 bg-scrim", onClick: dismissible ? close : undefined })) : null, _jsxs("div", { ref: sheetRef, role: modal ? 'dialog' : 'region', "aria-modal": modal ? true : undefined, "aria-labelledby": titleId, "aria-describedby": description ? descId : undefined, tabIndex: -1, "data-snap": snap, "data-mode": mode, onKeyDown: trapTab, style: { transform: `translateY(${Math.max(offset, -40)}px)` }, className: cn('fixed inset-x-0 bottom-0 z-50 mx-auto flex w-full flex-col', 'rounded-t-2xl border border-b-0 border-hairline bg-sheet text-ink shadow-2xl', 'pb-[env(safe-area-inset-bottom)] outline-none', !dragging &&
195
+ return (_jsxs(_Fragment, { children: [anchor, _jsx(ThemePortal, { target: target, children: _jsxs("div", { ref: wrapperRef, "data-drawer-root": "", children: [modal ? (_jsx("div", { "data-testid": "drawer-scrim", "aria-hidden": "true", className: "fixed inset-0 z-40 bg-scrim", onClick: dismissible ? close : undefined })) : null, _jsxs("div", { ref: sheetRef, role: modal ? 'dialog' : 'region', "aria-modal": modal ? true : undefined, "aria-labelledby": titleId, "aria-describedby": description ? descId : undefined, tabIndex: -1, "data-snap": snap, "data-mode": mode, onKeyDown: trapTab, style: { transform: `translateY(${Math.max(offset, -40)}px)` }, className: cn('fixed inset-x-0 bottom-0 z-50 mx-auto flex w-full flex-col', 'rounded-t-2xl bg-sheet text-ink shadow-2xl', 'pb-[env(safe-area-inset-bottom)] outline-none', !dragging &&
196
196
  'transition-transform duration-200 ease-out motion-reduce:transition-none', maxWidthClassName, className), children: [_jsxs("div", { className: "touch-none select-none px-5 pt-2", onPointerDown: (e) => begin(e, false), ...dragHandlers, children: [_jsx("button", { type: "button", "aria-expanded": expanded, "aria-controls": bodyId, "aria-label": expanded ? (collapseLabel ?? strings.collapse) : (expandLabel ?? strings.expand), onClick: onHandleClick, className: "mx-auto flex h-6 w-16 cursor-grab items-center justify-center rounded-full focus-visible:outline-2 focus-visible:outline-brand-700 active:cursor-grabbing", children: _jsx("span", { "aria-hidden": "true", className: "h-1.5 w-10 rounded-full bg-hairline" }) }), _jsxs("div", { className: "flex items-start gap-3 pb-3 pt-1", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("h2", { id: titleId, className: "m-0 text-base font-medium text-brand-900", children: title }), description ? (_jsx("p", { id: descId, className: "m-0 mt-1 text-sm text-ink-muted", children: description })) : null] }), showCloseButton && dismissible ? (_jsx("button", { type: "button", "aria-label": closeLabel ?? strings.close, onClick: close, className: "-mr-1 flex size-8 shrink-0 items-center justify-center rounded-full text-ink-muted hover:bg-brand-700/8 focus-visible:outline-2 focus-visible:outline-brand-700", children: _jsx("svg", { "aria-hidden": "true", width: "15", height: "15", viewBox: "0 0 15 15", fill: "none", children: _jsx("path", { d: "M3.5 3.5l8 8m0-8l-8 8", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" }) }) })) : null] })] }), !expanded && peek ? _jsx("div", { className: "px-5 pb-4", children: peek }) : null, _jsx("div", { ref: bodyRef, id: bodyId, hidden: !expanded, className: "max-h-[80vh] overflow-y-auto overscroll-contain px-5 pb-4", onPointerDown: (e) => begin(e, true), ...dragHandlers, children: children }), footer ? (_jsx("div", { className: "flex justify-end gap-2 border-t border-hairline px-5 py-3", children: footer })) : null] })] }) })] }));
197
197
  }
@@ -248,7 +248,7 @@ export function Sheet({ open, onOpenChange, title, description, children, footer
248
248
  const content = (_jsxs("div", { ref: rootRef, "data-sheet-root": "", className: "contents", children: [_jsx(RadixDialog.Overlay, { "data-testid": "sheet-scrim", className: "ui-sheet-scrim fixed inset-0 z-40 bg-scrim" }), _jsxs(RadixDialog.Content, { ref: contentRef, "data-detent": detent, "data-dragging": dragging ? '' : undefined, ...(description ? {} : { 'aria-describedby': undefined }), onEscapeKeyDown: dismissGuard, onPointerDownOutside: dismissGuard, onInteractOutside: dismissGuard,
249
249
  // The sheet moves focus to its first control; for a sheet that is the handle or close
250
250
  // button, which is correct, so Radix's default is kept.
251
- style: style, className: cn('ui-sheet fixed inset-x-0 bottom-0 z-50 mx-auto flex w-full flex-col overflow-hidden', 'rounded-t-2xl border border-b-0 border-hairline bg-sheet text-ink shadow-2xl outline-none', maxWidthClassName, className), children: [_jsxs("div", { className: "touch-none select-none px-5 pt-2", onPointerDown: (e) => begin(e, false), ...dragHandlers, children: [cycles ? (_jsx("button", { type: "button", "aria-label": raising ? (expandLabel ?? strings.expand) : (collapseLabel ?? strings.collapse), onClick: onHandleClick, className: "mx-auto flex h-6 w-16 cursor-grab items-center justify-center rounded-full focus-visible:outline-2 focus-visible:outline-brand-700 active:cursor-grabbing", children: _jsx("span", { "aria-hidden": "true", className: "h-1.5 w-10 rounded-full bg-hairline" }) })) : (_jsx("div", { "aria-hidden": "true", className: "mx-auto flex h-6 w-16 items-center justify-center", children: _jsx("span", { className: "h-1.5 w-10 rounded-full bg-hairline" }) })), _jsxs("div", { className: "flex items-start gap-3 pb-3 pt-1", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsx(RadixDialog.Title, { className: "m-0 text-base font-medium text-brand-900", children: title }), description ? (_jsx(RadixDialog.Description, { className: "m-0 mt-1 text-sm text-ink-muted", children: description })) : null] }), showCloseButton && dismissible ? (_jsx(RadixDialog.Close, { "aria-label": closeLabel ?? strings.close, className: "-mr-1 flex size-8 shrink-0 items-center justify-center rounded-full text-ink-muted hover:bg-brand-700/8 focus-visible:outline-2 focus-visible:outline-brand-700", children: _jsx("svg", { "aria-hidden": "true", width: "15", height: "15", viewBox: "0 0 15 15", fill: "none", children: _jsx("path", { d: "M3.5 3.5l8 8m0-8l-8 8", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" }) }) })) : null] })] }), _jsx("div", { ref: bodyRef, "data-sheet-body": "", className: "min-h-0 flex-1 overflow-y-auto overscroll-contain px-5 pb-4", onPointerDown: (e) => begin(e, true), ...dragHandlers, children: children }), footer ? (_jsx("div", { "data-sheet-footer": "", className: "flex shrink-0 justify-end gap-2 border-t border-hairline px-5 pt-3",
251
+ style: style, className: cn('ui-sheet fixed inset-x-0 bottom-0 z-50 mx-auto flex w-full flex-col overflow-hidden', 'rounded-t-2xl bg-sheet text-ink shadow-2xl outline-none', maxWidthClassName, className), children: [_jsxs("div", { className: "touch-none select-none px-5 pt-2", onPointerDown: (e) => begin(e, false), ...dragHandlers, children: [cycles ? (_jsx("button", { type: "button", "aria-label": raising ? (expandLabel ?? strings.expand) : (collapseLabel ?? strings.collapse), onClick: onHandleClick, className: "mx-auto flex h-6 w-16 cursor-grab items-center justify-center rounded-full focus-visible:outline-2 focus-visible:outline-brand-700 active:cursor-grabbing", children: _jsx("span", { "aria-hidden": "true", className: "h-1.5 w-10 rounded-full bg-hairline" }) })) : (_jsx("div", { "aria-hidden": "true", className: "mx-auto flex h-6 w-16 items-center justify-center", children: _jsx("span", { className: "h-1.5 w-10 rounded-full bg-hairline" }) })), _jsxs("div", { className: "flex items-start gap-3 pb-3 pt-1", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsx(RadixDialog.Title, { className: "m-0 text-base font-medium text-brand-900", children: title }), description ? (_jsx(RadixDialog.Description, { className: "m-0 mt-1 text-sm text-ink-muted", children: description })) : null] }), showCloseButton && dismissible ? (_jsx(RadixDialog.Close, { "aria-label": closeLabel ?? strings.close, className: "-mr-1 flex size-8 shrink-0 items-center justify-center rounded-full text-ink-muted hover:bg-brand-700/8 focus-visible:outline-2 focus-visible:outline-brand-700", children: _jsx("svg", { "aria-hidden": "true", width: "15", height: "15", viewBox: "0 0 15 15", fill: "none", children: _jsx("path", { d: "M3.5 3.5l8 8m0-8l-8 8", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" }) }) })) : null] })] }), _jsx("div", { ref: bodyRef, "data-sheet-body": "", className: "min-h-0 flex-1 overflow-y-auto overscroll-contain px-5 pb-4", onPointerDown: (e) => begin(e, true), ...dragHandlers, children: children }), footer ? (_jsx("div", { "data-sheet-footer": "", className: "flex shrink-0 justify-end gap-2 border-t border-hairline px-5 pt-3",
252
252
  // Over the keyboard there is no home indicator to clear, only a gap to leave.
253
253
  style: {
254
254
  paddingBottom: keyboard ? '0.75rem' : 'max(0.75rem, var(--safe-bottom, 0px))',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kennycorrea/ui",
3
- "version": "0.11.3",
3
+ "version": "0.11.4",
4
4
  "description": "A React + TypeScript component library built on Radix Themes: atoms, a Tailwind token bridge and theming.",
5
5
  "private": false,
6
6
  "type": "module",
package/tokens.css CHANGED
@@ -592,6 +592,9 @@
592
592
  * `md` is 768. Ascending media queries of equal specificity, so the widest match wins. The card's
593
593
  * pseudo-elements carry the surface and ring, so grouped/plain switch them off with `content`.
594
594
  */
595
+ /* Unlayered, like Themes' `.rt-Card` padding, and more specific: the header and body pad, the card
596
+ does not. A caller's padding class reaches the card as `!p-*` (see Card.tsx), which still wins. */
597
+ .ui-card.rt-Card { padding: 0; }
595
598
  .ui-card-header, .ui-card-body { padding: 16px 20px; }
596
599
  .ui-card-title { font-size: 1rem; font-weight: 500; }
597
600
  .ui-card.rt-Card[data-variant="outlined"] { background: transparent; --base-card-border-radius: var(--card-border-radius);}
@@ -841,6 +844,12 @@
841
844
  .ui-segmented__label { display: block; overflow: hidden; text-overflow: ellipsis; }
842
845
  .ui-segmented[data-size="md"] .ui-segmented__item { min-height: 32px; padding: 0 16px; font-size: 14px; }
843
846
  .ui-segmented[data-size="sm"] .ui-segmented__item { min-height: 24px; padding: 0 12px; font-size: 12px; }
847
+ /* Full width: items share the track instead of growing it past the pill. Equal 1fr columns from a
848
+ zero basis keep the thumb (100% / count) on each item exactly; labels ellipsize. */
849
+ .ui-segmented[data-full="true"] { overflow-x: hidden; }
850
+ .ui-segmented[data-full="true"] .ui-segmented__track { grid-auto-columns: minmax(0, 1fr); flex: 1 1 0; }
851
+ .ui-segmented[data-full="true"][data-size="md"] .ui-segmented__item { padding: 0 8px; }
852
+ .ui-segmented[data-full="true"][data-size="sm"] .ui-segmented__item { padding: 0 6px; }
844
853
  .ui-segmented__item[aria-checked="true"] { color: var(--gray-12); }
845
854
  .ui-segmented__item:disabled { opacity: 0.4; cursor: not-allowed; }
846
855
  .ui-segmented__item:focus-visible { outline: 2px solid var(--focus-8); outline-offset: 1px; }
@@ -917,3 +926,13 @@
917
926
  .ui-button, .ui-list-row[data-interactive], .ui-tabbar__item, .ui-stat[data-interactive] { transition: none; }
918
927
  .ui-button:active, .ui-list-row[data-interactive]:active, .ui-tabbar__item:active, .ui-stat[data-interactive]:active { transform: none; }
919
928
  }
929
+
930
+ /* Floating content: Themes' --shadow-5 opens with a `0 0 0 1px` ring that reads as a border on
931
+ menus, popovers and selects. Same elevation, without the ring. Unlayered to beat Themes. */
932
+ .radix-themes {
933
+ --ui-float-shadow: 0 12px 60px var(--black-a3), 0 12px 32px -16px var(--gray-a5);
934
+ }
935
+ .radix-themes:is(.dark, .dark-theme), :is(.dark, .dark-theme) .radix-themes:not(.light, .light-theme) {
936
+ --ui-float-shadow: 0 12px 60px var(--black-a5), 0 12px 32px -16px var(--black-a7);
937
+ }
938
+ .rt-BaseMenuContent, .rt-SelectContent, .rt-PopoverContent { box-shadow: var(--ui-float-shadow); }