@moldable-ai/ui 0.2.31 → 0.2.32

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.
Files changed (54) hide show
  1. package/dist/components/app-error-boundary.d.ts +1 -1
  2. package/dist/components/chat/chat-image-lightbox.d.ts +8 -0
  3. package/dist/components/chat/chat-image-lightbox.d.ts.map +1 -1
  4. package/dist/components/chat/chat-image-lightbox.js +24 -2
  5. package/dist/components/chat/chat-input.d.ts +3 -2
  6. package/dist/components/chat/chat-input.d.ts.map +1 -1
  7. package/dist/components/chat/chat-input.js +3 -3
  8. package/dist/components/chat/chat-message.d.ts.map +1 -1
  9. package/dist/components/chat/chat-message.js +3 -1
  10. package/dist/components/chat/chat-panel.d.ts +3 -1
  11. package/dist/components/chat/chat-panel.d.ts.map +1 -1
  12. package/dist/components/chat/chat-panel.js +53 -39
  13. package/dist/components/chat/conversation-history.d.ts +4 -2
  14. package/dist/components/chat/conversation-history.d.ts.map +1 -1
  15. package/dist/components/chat/conversation-history.js +20 -15
  16. package/dist/components/ui/alert.d.ts +1 -1
  17. package/dist/components/ui/button.d.ts +2 -2
  18. package/dist/components/ui/button.js +1 -1
  19. package/dist/components/ui/context-menu.d.ts.map +1 -1
  20. package/dist/components/ui/context-menu.js +1 -0
  21. package/dist/components/ui/date-field.d.ts +1 -1
  22. package/dist/components/ui/dropdown-menu.d.ts +1 -0
  23. package/dist/components/ui/dropdown-menu.d.ts.map +1 -1
  24. package/dist/components/ui/dropdown-menu.js +3 -2
  25. package/dist/components/ui/menu-items.d.ts +2 -1
  26. package/dist/components/ui/menu-items.d.ts.map +1 -1
  27. package/dist/components/ui/menu-items.js +3 -2
  28. package/dist/components/ui/message-scroller-controller.d.ts +15 -17
  29. package/dist/components/ui/message-scroller-controller.d.ts.map +1 -1
  30. package/dist/components/ui/message-scroller-controller.js +106 -91
  31. package/dist/components/ui/message-scroller-primitive.d.ts +2 -0
  32. package/dist/components/ui/message-scroller-primitive.d.ts.map +1 -1
  33. package/dist/components/ui/message-scroller-primitive.js +2 -0
  34. package/dist/components/ui/message-scroller.d.ts.map +1 -1
  35. package/dist/components/ui/message-scroller.js +2 -2
  36. package/dist/components/ui/number-input.d.ts +1 -1
  37. package/dist/components/ui/popover.js +1 -1
  38. package/dist/components/ui/search-field.d.ts +1 -1
  39. package/dist/components/ui/sidebar.d.ts +6 -2
  40. package/dist/components/ui/sidebar.d.ts.map +1 -1
  41. package/dist/components/ui/sidebar.js +26 -7
  42. package/dist/components/ui/toolbar-controls.d.ts +10 -2
  43. package/dist/components/ui/toolbar-controls.d.ts.map +1 -1
  44. package/dist/components/ui/toolbar-controls.js +25 -7
  45. package/package.json +1 -1
  46. package/src/components/ui/button.md +3 -2
  47. package/src/components/ui/dropdown-menu.md +16 -14
  48. package/src/components/ui/menu-items.md +2 -0
  49. package/src/components/ui/message-scroller.md +6 -3
  50. package/src/components/ui/sidebar.md +11 -5
  51. package/src/components/ui/toolbar-controls.md +11 -7
  52. package/src/styles/index.css +128 -1
  53. package/src/tokens/moldable/brand.css +124 -7
  54. package/src/tokens/platform-contract.test.ts +5 -1
@@ -6,16 +6,34 @@ import { cn } from '../../lib/utils.js';
6
6
  import { Button } from './button.js';
7
7
  import { DropdownMenu, DropdownMenuTrigger } from './dropdown-menu.js';
8
8
  import { IconButton, } from './icon-button.js';
9
+ import { Material } from './material.js';
9
10
  import { Separator } from './separator.js';
10
11
  const ToolbarControlGroupContext = React.createContext(null);
11
- function ToolbarButton({ appearance = 'chrome', className, size, variant, ...props }) {
12
+ function ToolbarButton({ appearance = 'chrome', className, material, size, variant, ...props }) {
12
13
  const group = React.useContext(ToolbarControlGroupContext);
13
14
  const groupedSize = group?.size === 'xl' ? 'lg' : 'sm';
14
15
  const emphasis = variant === 'prominent' ? 'primary' : 'standard';
15
- const resolvedVariant = variant === 'prominent'
16
- ? 'toolbar'
17
- : (variant ?? (group || appearance === 'quiet' ? 'ghost' : 'toolbar'));
18
- return (_jsx(Button, { "data-slot": "toolbar-button", "data-emphasis": emphasis, size: size ?? (group ? groupedSize : 'xl'), variant: resolvedVariant, className: cn('rounded-pill gap-[var(--m-ui-toolbar-action-gap,var(--density-control-gap))] px-[var(--m-ui-toolbar-action-padding-inline,var(--density-control-padding-inline))] [&_svg:not([class*="size-"])]:size-[var(--m-ui-toolbar-action-icon-size,13px)]', emphasis === 'primary' && 'font-semibold', group && 'shadow-none', className), ...props }));
16
+ const usesToolbarChrome = variant === undefined || variant === 'prominent' || variant === 'toolbar';
17
+ const resolvedMaterial = group || appearance === 'quiet'
18
+ ? 'none'
19
+ : (material ??
20
+ (usesToolbarChrome
21
+ ? variant === 'prominent'
22
+ ? 'thick'
23
+ : 'regular'
24
+ : 'none'));
25
+ const resolvedVariant = resolvedMaterial !== 'none'
26
+ ? 'ghost'
27
+ : usesToolbarChrome &&
28
+ (group || appearance === 'quiet' || material === 'none')
29
+ ? 'ghost'
30
+ : variant === 'prominent'
31
+ ? 'toolbar'
32
+ : (variant ?? 'toolbar');
33
+ const button = (_jsx(Button, { "data-slot": "toolbar-button", "data-emphasis": emphasis, size: size ?? (group ? groupedSize : 'xl'), variant: resolvedVariant, className: cn('rounded-pill gap-[var(--m-ui-toolbar-action-gap,var(--density-control-gap))] px-[var(--m-ui-toolbar-action-padding-inline,var(--density-control-padding-inline))] [&_svg:not([class*="size-"])]:size-[var(--m-ui-toolbar-action-icon-size,13px)]', emphasis === 'primary' && 'font-semibold', group && 'shadow-none', className), ...props }));
34
+ if (resolvedMaterial === 'none')
35
+ return button;
36
+ return (_jsx(Material, { asChild: true, variant: resolvedMaterial, shape: "capsule", elevation: "panel", interactive: true, children: button }));
19
37
  }
20
38
  function ToolbarIconButton({ appearance = 'chrome', className, size, variant, ...props }) {
21
39
  const group = React.useContext(ToolbarControlGroupContext);
@@ -38,8 +56,8 @@ function ToolbarSidebarToggle({ expanded, hideLabel = 'Hide sidebar', onExpanded
38
56
  const label = expanded ? hideLabel : showLabel;
39
57
  return (_jsx(ToolbarIconButton, { "aria-pressed": expanded, "data-state": expanded ? 'on' : 'off', label: label, tooltip: tooltip, onClick: () => onExpandedChange(!expanded), ...props, children: _jsx(PanelLeftIcon, {}) }));
40
58
  }
41
- function ToolbarOverflowMenu({ items, label = 'More actions', onSelectItem, tooltip = true, ...props }) {
42
- return (_jsx(DropdownMenu, { items: items, onSelectItem: onSelectItem, children: _jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(ToolbarIconButton, { label: label, tooltip: tooltip, ...props, children: _jsx(EllipsisIcon, {}) }) }) }));
59
+ function ToolbarOverflowMenu({ items, label = 'More actions', nativeMenu = false, onSelectItem, tooltip = true, ...props }) {
60
+ return (_jsx(DropdownMenu, { items: items, nativeMenu: nativeMenu, onSelectItem: onSelectItem, children: _jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(ToolbarIconButton, { label: label, tooltip: tooltip, ...props, children: _jsx(EllipsisIcon, {}) }) }) }));
43
61
  }
44
62
  function ToolbarTitleField({ 'aria-label': ariaLabel, className, label = 'Document title', type = 'text', ...props }) {
45
63
  return (_jsx("input", { "data-slot": "toolbar-title-field", "aria-label": ariaLabel ?? label, type: type, className: cn('text-foreground hover:bg-control-hover focus:bg-control-hover focus-visible:ring-ring h-control-md rounded-control min-w-0 max-w-[min(24rem,32vw)] bg-transparent px-2 text-[13px] font-bold outline-none transition-colors focus-visible:ring-1', className), ...props }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moldable-ai/ui",
3
- "version": "0.2.31",
3
+ "version": "0.2.32",
4
4
  "description": "Shared UI components for Moldable applications",
5
5
  "author": "Desiderata LLC",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -56,8 +56,9 @@ For links that should look like buttons:
56
56
  - `variant`: `default`, `prominent`, `bordered`, `borderless`, `tinted`,
57
57
  `toolbar`, `destructive`, `destructive-tinted`, `outline`, `secondary`,
58
58
  `ghost`, or `link`. Defaults to `default`; `default` and `prominent` share
59
- the prominent recipe. Prefer the toolbar-control wrappers instead of
60
- applying `toolbar` directly.
59
+ the prominent recipe. `borderless` uses the neutral foreground color without
60
+ a border or fill, then dims slightly on hover and press. Prefer the
61
+ toolbar-control wrappers instead of applying `toolbar` directly.
61
62
  - `size`: `default`, `xs`, `sm`, `lg`, `xl`, `mini`, `small`, `medium`,
62
63
  `large`, `icon`, `icon-xs`, `icon-sm`, `icon-lg`, or `icon-xl`. Defaults to
63
64
  `default`.
@@ -4,7 +4,8 @@
4
4
 
5
5
  `DropdownMenu` presents a compact list of commands from a visible trigger. Use
6
6
  it for overflow actions, view commands, and small command groups. Its
7
- declarative item API automatically adopts the host-native menu when available.
7
+ declarative item API uses the portable menu by default and can opt into the
8
+ host-native menu when needed.
8
9
 
9
10
  Use `Select` for a form value, `Combobox` for searchable selection,
10
11
  `ContextMenu` for commands attached to the pointer target, and `Popover` when
@@ -91,9 +92,9 @@ shortcuts, and submenus are available when the command set needs structure.
91
92
 
92
93
  ## API summary
93
94
 
94
- - `DropdownMenu` supports `items?: MenuItems[]`,
95
- `onSelectItem?: (id: string) => void`, `open`, `defaultOpen`,
96
- `onOpenChange`, `dir`, and `modal`.
95
+ - `DropdownMenu` supports `items?: MenuItems[]`, `nativeMenu?: boolean`,
96
+ `onSelectItem?: (id: string) => void`, `open`, `defaultOpen`, `onOpenChange`,
97
+ `dir`, and `modal`.
97
98
  - A `MenuItems` value supports `id`, `kind`, `label`, portable `icon`, reserved
98
99
  `sfSymbol`, `enabled`, `checked`, `shortcut`, `destructive`, and `submenu`.
99
100
  - Do not provide `DropdownMenuContent` together with `items`. In development
@@ -128,16 +129,17 @@ menu item; use `Popover` or `Dialog`.
128
129
 
129
130
  ## Platform behavior
130
131
 
131
- With `items`, pointer and keyboard trigger activation automatically opens the
132
- host-native menu when `nativeMenu` is advertised. The trigger rectangle is its
133
- anchor, `selectedId` is delivered to `onSelectItem`, and dismissal is a no-op.
134
- No portable menu is rendered while that native request is open.
135
-
136
- Without the advertisement, including web and iOS, the same items automatically
137
- render through the existing Radix anatomy. A stale advertisement that returns
138
- fallback opens the portable menu as well. Icons and destructive styling are
139
- portable in v1; `sfSymbol` is reserved and is not posted to or rendered by the
140
- native service.
132
+ With `items`, pointer and keyboard trigger activation opens the portable Radix
133
+ menu by default, including when the host advertises `nativeMenu`.
134
+
135
+ Set `nativeMenu` explicitly on `DropdownMenu` to opt into the host-native menu.
136
+ The trigger rectangle is its anchor, `selectedId` is delivered to
137
+ `onSelectItem`, and dismissal is a no-op. No portable menu is rendered while
138
+ that native request is open. Without the host advertisement, including web and
139
+ iOS, the same items render through the existing Radix anatomy. A stale
140
+ advertisement that returns fallback opens the portable menu as well. Icons and
141
+ destructive styling are portable in v1; `sfSymbol` is reserved and is not
142
+ posted to or rendered by the native service.
141
143
 
142
144
  Shortcuts are display metadata only. They do not register keyboard handlers;
143
145
  keep command execution keyed by `onSelectItem`. See
@@ -45,6 +45,8 @@ import { Button, DropdownMenu, DropdownMenuTrigger } from '@moldable-ai/ui'
45
45
  - `useDeclarativeMenuState` supports controlled or uncontrolled portable-open
46
46
  state. While a native request is active, `nativeOpen` is true and
47
47
  `portableOpen` is forced false.
48
+ - Native requests are gated by an explicit component policy. `DropdownMenu`
49
+ exposes that policy as its opt-in `nativeMenu` prop.
48
50
  - A completed native request passes `selectedId` to `onSelectItem`.
49
51
  Cancellation does nothing. A fallback result or request failure opens the
50
52
  portable menu unless the caller supplies its own `onFallback`.
@@ -62,9 +62,12 @@ import {
62
62
 
63
63
  ## Hooks and behavior
64
64
 
65
- - `useMessageScroller()` returns `scrollToStart`, `scrollToEnd`, and
66
- `scrollToMessage`. Targeted scrolls accept `align`, `behavior`, and
67
- `scrollMargin`; alignment defaults to `start` and behavior to `auto`.
65
+ - `useMessageScroller()` returns `scrollToStart`, `scrollToEnd`,
66
+ `scrollToMessage`, `beginHistoryLoad`, and `finishHistoryLoad`. Bracket an
67
+ asynchronous older-history request with the last two methods so prepends and
68
+ stale-page replacements preserve distance from the bottom before paint.
69
+ Targeted scrolls accept `align`, `behavior`, and `scrollMargin`; alignment
70
+ defaults to `start` and behavior to `auto`.
68
71
  - `useMessageScrollerScrollable()` returns `{ start, end }`.
69
72
  - `useMessageScrollerVisibility()` returns `currentAnchorId` and ordered
70
73
  `visibleMessageIds`.
@@ -25,8 +25,11 @@ import {
25
25
  } from '@moldable-ai/ui'
26
26
 
27
27
  ;<SidebarProvider className="h-full min-h-0">
28
- <Sidebar collapsible="icon">
29
- <SidebarContent className="pb-[calc(var(--chat-safe-padding,0px)+1rem)]">
28
+ <Sidebar collapsible="icon" material="medium">
29
+ <SidebarContent
30
+ edgeFade
31
+ className="scrollbar-hide pb-[calc(var(--chat-safe-padding,0px)+1rem)]"
32
+ >
30
33
  <SidebarGroup>
31
34
  <SidebarGroupLabel>Mailboxes</SidebarGroupLabel>
32
35
  <SidebarGroupContent>
@@ -65,14 +68,14 @@ icon tint, and text truncation.
65
68
 
66
69
  - `SidebarProvider`: required context. Supports uncontrolled `defaultOpen`, controlled `open`/`onOpenChange`, and CSS variable overrides through `style`.
67
70
  - `useSidebar()`: returns `state`, `open`, `setOpen`, `openMobile`, `setOpenMobile`, `isMobile`, and `toggleSidebar`.
68
- - `Sidebar`: `side="left" | "right"`, `variant="sidebar" | "floating" | "inset"`, `sidebarStyle="platform" | "floating" | "edge-to-edge"`, and `collapsible="offcanvas" | "icon" | "none"`.
71
+ - `Sidebar`: `side="left" | "right"`, `variant="sidebar" | "floating" | "inset"`, `sidebarStyle="platform" | "floating" | "edge-to-edge"`, `material="ultra-thick" | "thick" | "medium" | "thin" | "ultra-thin"` (plus the compatibility aliases `regular` and `clear`), and `collapsible="offcanvas" | "icon" | "none"`.
69
72
  - `SidebarInset`: main content surface paired with the sidebar.
70
73
  - `SidebarTrigger`: compact toggle button.
71
74
  - `SidebarRail`: pointer target along the sidebar edge.
72
75
 
73
76
  ### Structure
74
77
 
75
- - `SidebarHeader`, `SidebarFooter`, `SidebarContent`, and `SidebarSeparator` divide the vertical shell.
78
+ - `SidebarHeader`, `SidebarFooter`, `SidebarContent`, and `SidebarSeparator` divide the vertical shell. `SidebarContent edgeFade` composes the shared top and bottom `EdgeFade` cues around its scroll viewport; it defaults off.
76
79
  - `SidebarInput` is a compact search/filter input.
77
80
  - `SidebarGroup`, `SidebarGroupLabel`, `SidebarGroupAction`, and `SidebarGroupContent` organize sections.
78
81
 
@@ -120,7 +123,10 @@ Choose the visual anatomy independently from the application's base platform:
120
123
 
121
124
  - `edge-to-edge` is the default.
122
125
  - `floating` uses the macOS inset, material-backed shell and forces the
123
- component's resolved variant to `floating`.
126
+ component's resolved variant to `floating`. Its shell clips scrolling
127
+ content to the rounded material edge. Set `material` when the surface needs
128
+ an explicit adaptive-material thickness; thinner values are more
129
+ transparent without changing the standard blur.
124
130
  - `edge-to-edge` uses the macOS joined window/sidebar anatomy and forces the
125
131
  resolved variant to `sidebar`.
126
132
  It has no independent fill, blur, shadow, or internal divider and reveals
@@ -39,15 +39,17 @@ keeping app toolbars visually consistent.
39
39
 
40
40
  ## Components
41
41
 
42
- - `ToolbarButton` — text or mixed-content action. Defaults to 36px native
43
- chrome; use `variant="prominent"` for the one primary action.
42
+ - `ToolbarButton` — text or mixed-content action. Standalone chrome is a real
43
+ adaptive material surface: `material="regular"` by default and
44
+ `material="thick"` for `variant="prominent"`. Set `material="none"` or
45
+ `appearance="quiet"` to remove it.
44
46
  - `ToolbarIconButton` — required accessible label and optional tooltip.
45
47
  - `ToolbarBackButton` — single chevron Back affordance for root-to-detail
46
48
  navigation.
47
49
  - `ToolbarSidebarToggle` — controlled sidebar visibility button with accurate
48
50
  pressed state and dynamic label.
49
51
  - `ToolbarOverflowMenu` — an ellipsis button backed by Moldable's declarative
50
- menu API, including the native host menu when available.
52
+ portable menu API. Set `nativeMenu` to opt into the native host menu.
51
53
  - `ToolbarControlGroup` — a shared surface for closely related actions such as
52
54
  Undo/Redo. It defaults to `size="xl"`, supports `size="medium"`, and makes
53
55
  children compact quiet controls.
@@ -55,10 +57,12 @@ keeping app toolbars visually consistent.
55
57
  - `ToolbarTitleField` — an accessible, inline-editable contextual title.
56
58
 
57
59
  `ToolbarButton` and `ToolbarIconButton` accept `appearance="quiet"` when a
58
- standalone action should not have raised chrome. The default `chrome`
59
- appearance uses the `toolbar` Button variant. `ToolbarControlGroup` also
60
- accepts `appearance="quiet"` when related controls should not share a raised
61
- surface. Raised controls use these semantic tokens:
60
+ standalone action should not have raised chrome. `ToolbarButton` uses the
61
+ shared adaptive `Material` layer; `ToolbarIconButton` uses the compact toolbar
62
+ control treatment. `ToolbarControlGroup` also accepts `appearance="quiet"`
63
+ when related controls should not share a raised surface. Grouped buttons do
64
+ not add a nested material layer. Raised compact controls use these semantic
65
+ tokens:
62
66
 
63
67
  - `--toolbar-control-background`
64
68
  - `--toolbar-control-hover`
@@ -1037,7 +1037,7 @@
1037
1037
  }
1038
1038
 
1039
1039
  .dark {
1040
- --background: #1d1f1f;
1040
+ --background: #181818;
1041
1041
  --foreground: rgb(255 255 255 / 85%);
1042
1042
  --card: #242424;
1043
1043
  --card-foreground: rgb(255 255 255 / 85%);
@@ -1437,6 +1437,133 @@
1437
1437
  saturate(var(--material-active-saturation));
1438
1438
  }
1439
1439
  }
1440
+
1441
+ /*
1442
+ * Popovers keep the quieter material treatment used before the general
1443
+ * surface-material expansion. They are compact menus, not floating windows.
1444
+ */
1445
+ .moldable-popover {
1446
+ --shape-overlay: 0.75rem;
1447
+ --density-pane-padding: 0.75rem;
1448
+ --popover-material-fill: color-mix(
1449
+ in oklch,
1450
+ var(--background) 76%,
1451
+ transparent
1452
+ );
1453
+ --popover-material-fallback: color-mix(
1454
+ in oklch,
1455
+ var(--background) 94%,
1456
+ var(--foreground) 6%
1457
+ );
1458
+ --popover-material-border: color-mix(
1459
+ in oklch,
1460
+ var(--foreground) 10%,
1461
+ transparent
1462
+ );
1463
+ --popover-material-highlight: oklch(1 0 0 / 42%);
1464
+ --popover-material-blur: 20px;
1465
+ --popover-material-saturation: 1.18;
1466
+ --popover-material-shadow:
1467
+ 0 8px 24px oklch(0.15 0.005 285 / 12%),
1468
+ 0 2px 6px oklch(0.15 0.005 285 / 8%);
1469
+
1470
+ border: 1px solid var(--popover-material-border);
1471
+ background-color: var(--popover-material-fallback);
1472
+ box-shadow:
1473
+ inset 0 1px 0 var(--popover-material-highlight),
1474
+ var(--popover-material-shadow);
1475
+ }
1476
+
1477
+ .moldable-popover[data-material='clear'] {
1478
+ --popover-material-fill: color-mix(
1479
+ in oklch,
1480
+ var(--background) 24%,
1481
+ transparent
1482
+ );
1483
+ --popover-material-fallback: color-mix(
1484
+ in oklch,
1485
+ var(--background) 88%,
1486
+ var(--foreground) 12%
1487
+ );
1488
+ --popover-material-border: color-mix(
1489
+ in oklch,
1490
+ var(--foreground) 14%,
1491
+ transparent
1492
+ );
1493
+ --popover-material-highlight: oklch(1 0 0 / 28%);
1494
+ --popover-material-blur: 12px;
1495
+ --popover-material-saturation: 1.24;
1496
+ }
1497
+
1498
+ .dark .moldable-popover {
1499
+ --popover-material-fill: color-mix(
1500
+ in oklch,
1501
+ var(--background) 68%,
1502
+ transparent
1503
+ );
1504
+ --popover-material-fallback: color-mix(
1505
+ in oklch,
1506
+ var(--background) 88%,
1507
+ var(--foreground) 12%
1508
+ );
1509
+ --popover-material-border: oklch(1 0 0 / 14%);
1510
+ --popover-material-highlight: oklch(1 0 0 / 12%);
1511
+ --popover-material-shadow:
1512
+ 0 12px 32px oklch(0 0 0 / 34%), 0 2px 8px oklch(0 0 0 / 24%);
1513
+ }
1514
+
1515
+ .dark .moldable-popover[data-material='clear'] {
1516
+ --popover-material-fill: color-mix(
1517
+ in oklch,
1518
+ var(--background) 20%,
1519
+ transparent
1520
+ );
1521
+ --popover-material-fallback: color-mix(
1522
+ in oklch,
1523
+ var(--background) 78%,
1524
+ var(--foreground) 22%
1525
+ );
1526
+ --popover-material-border: oklch(1 0 0 / 18%);
1527
+ --popover-material-highlight: oklch(1 0 0 / 10%);
1528
+ }
1529
+
1530
+ :where([data-transparency='reduced']) .moldable-popover {
1531
+ --popover-material-fill: var(--popover-material-fallback);
1532
+ --popover-material-blur: 0px;
1533
+ --popover-material-saturation: 1;
1534
+ }
1535
+
1536
+ @media (prefers-reduced-transparency: reduce) {
1537
+ .moldable-popover {
1538
+ --popover-material-fill: var(--popover-material-fallback);
1539
+ --popover-material-blur: 0px;
1540
+ --popover-material-saturation: 1;
1541
+ }
1542
+ }
1543
+
1544
+ @media (forced-colors: active) {
1545
+ .moldable-popover {
1546
+ --popover-material-fill: Canvas;
1547
+ --popover-material-fallback: Canvas;
1548
+ --popover-material-border: ButtonBorder;
1549
+ --popover-material-highlight: transparent;
1550
+ --popover-material-shadow: none;
1551
+ --popover-material-blur: 0px;
1552
+ --popover-material-saturation: 1;
1553
+ }
1554
+ }
1555
+
1556
+ @supports (
1557
+ (-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))
1558
+ ) {
1559
+ .moldable-popover {
1560
+ background-color: var(--popover-material-fill);
1561
+ -webkit-backdrop-filter: blur(var(--popover-material-blur))
1562
+ saturate(var(--popover-material-saturation));
1563
+ backdrop-filter: blur(var(--popover-material-blur))
1564
+ saturate(var(--popover-material-saturation));
1565
+ }
1566
+ }
1440
1567
  }
1441
1568
 
1442
1569
  @layer utilities {
@@ -8,15 +8,132 @@
8
8
  }
9
9
 
10
10
  /*
11
- * Moldable's dark canvas stays close to the macOS window surface while
12
- * carrying a subtle cool cast. Keep these product colors in the brand layer
13
- * instead of changing the immutable platform source values.
11
+ * Moldable's dark canvas stays close to the macOS window surface with a
12
+ * neutral tone. Keep these product colors in the brand layer instead of
13
+ * changing the immutable platform source values.
14
14
  */
15
15
  [data-ui-theme='moldable'][data-ui-appearance='dark'] {
16
- --m-ui-surface-window: #1d1f1f;
17
- --m-ui-surface-content: #1d1f1f;
18
- --background: #1d1f1f;
19
- --surface-canvas: #1d1f1f;
16
+ --m-ui-surface-window: #181818;
17
+ --m-ui-surface-content: #181818;
18
+ --background: #181818;
19
+ --surface-canvas: #181818;
20
+ }
21
+
22
+ /*
23
+ * Renderer-owned selects and dropdowns keep Moldable's compact web styling.
24
+ * Native menu geometry belongs only to the explicit host-native menu path.
25
+ * These values preserve the component treatment from 94f11aa without
26
+ * changing the platform source pack used by native integrations.
27
+ */
28
+ [data-ui-theme='moldable'] [data-slot='select-trigger'] {
29
+ --shape-control: calc(0.65rem - 2px);
30
+ --density-control-height-sm: 2rem;
31
+ --density-control-height-md: 2.25rem;
32
+ --density-control-gap: 0.5rem;
33
+ --density-control-padding-inline: 0.75rem;
34
+ --type-body-size: 0.875rem;
35
+ --type-body-line-height: 1.25rem;
36
+ }
37
+
38
+ [data-ui-theme='moldable']
39
+ :is(
40
+ [data-slot='select-content'],
41
+ [data-slot='dropdown-menu-content'],
42
+ [data-slot='dropdown-menu-sub-content']
43
+ ) {
44
+ --shape-overlay: calc(0.65rem - 2px);
45
+ --m-ui-menu-padding-block: 0.25rem;
46
+ --m-ui-menu-item-radius: calc(0.65rem - 4px);
47
+ --type-body-size: 0.875rem;
48
+ --type-body-line-height: 1.25rem;
49
+ --type-caption-size: 0.75rem;
50
+ --type-caption-line-height: 1rem;
51
+ --material-shadow-overlay: var(--shadow-md);
52
+ }
53
+
54
+ [data-ui-theme='moldable'] [data-slot='dropdown-menu-sub-content'] {
55
+ --material-shadow-overlay: var(--shadow-lg);
56
+ }
57
+
58
+ [data-ui-theme='moldable'][data-ui-appearance='light']
59
+ :is(
60
+ [data-slot='select-content'],
61
+ [data-slot='dropdown-menu-content'],
62
+ [data-slot='dropdown-menu-sub-content']
63
+ ) {
64
+ --popover: oklch(1 0 0);
65
+ --popover-foreground: oklch(0.141 0.005 285.823);
66
+ --accent: oklch(0.967 0.001 286.375);
67
+ --accent-foreground: oklch(0.21 0.006 285.885);
68
+ --muted-foreground: oklch(0.552 0.016 285.938);
69
+ --border: oklch(0.92 0.004 286.32);
70
+ }
71
+
72
+ [data-ui-theme='moldable'][data-ui-appearance='dark']
73
+ :is(
74
+ [data-slot='select-content'],
75
+ [data-slot='dropdown-menu-content'],
76
+ [data-slot='dropdown-menu-sub-content']
77
+ ) {
78
+ --popover: oklch(0.269 0.0018 286.28);
79
+ --popover-foreground: oklch(0.985 0 0);
80
+ --accent: oklch(0.3375 0.0035 286.21);
81
+ --accent-foreground: oklch(0.985 0 0);
82
+ --muted-foreground: oklch(0.705 0.015 286.067);
83
+ --border: oklch(1 0 0 / 10%);
84
+ }
85
+
86
+ /*
87
+ * Keep Moldable tooltips on the compact material recipe from 94f11aa. The
88
+ * selected platform kit now maps generic overlays to its larger popover
89
+ * geometry, so the product theme reasserts the tooltip's own smaller recipe.
90
+ */
91
+ [data-ui-theme='moldable'] [data-slot='tooltip-content'] {
92
+ --shape-overlay: 0.75rem;
93
+ --type-caption-size: 0.75rem;
94
+ --type-caption-line-height: 1rem;
95
+ --surface-overlay: color-mix(in oklch, var(--popover) 92%, transparent);
96
+ --material-regular-fill: color-mix(
97
+ in oklch,
98
+ var(--background) 76%,
99
+ transparent
100
+ );
101
+ --material-regular-fallback: color-mix(
102
+ in oklch,
103
+ var(--background) 94%,
104
+ var(--foreground) 6%
105
+ );
106
+ --material-regular-blur: 20px;
107
+ --material-regular-saturation: 1.18;
108
+ --material-shadow-overlay:
109
+ 0 8px 24px oklch(0.15 0.005 285 / 12%),
110
+ 0 2px 6px oklch(0.15 0.005 285 / 8%);
111
+ box-shadow:
112
+ inset 0 1px 0 oklch(1 0 0 / 42%),
113
+ var(--material-shadow-overlay);
114
+ }
115
+
116
+ [data-ui-theme='moldable'][data-ui-appearance='dark']
117
+ [data-slot='tooltip-content'] {
118
+ --material-regular-fill: color-mix(
119
+ in oklch,
120
+ var(--background) 68%,
121
+ transparent
122
+ );
123
+ --material-regular-fallback: color-mix(
124
+ in oklch,
125
+ var(--background) 88%,
126
+ var(--foreground) 12%
127
+ );
128
+ --material-shadow-overlay:
129
+ 0 12px 32px oklch(0 0 0 / 34%), 0 2px 8px oklch(0 0 0 / 24%);
130
+ box-shadow:
131
+ inset 0 1px 0 oklch(1 0 0 / 12%),
132
+ var(--material-shadow-overlay);
133
+ }
134
+
135
+ [data-ui-theme='moldable'] [data-slot='tooltip-shortcut'] {
136
+ font-size: 0.625rem;
20
137
  }
21
138
 
22
139
  /*
@@ -278,7 +278,11 @@ describe('platform token contract', () => {
278
278
  expect(brand).not.toMatch(
279
279
  /\[data-ui-sidebar-style='floating'\]\s+\[data-ui-appearance='dark'\]/,
280
280
  )
281
- expect(brand).toContain('--m-ui-surface-content: #1d1f1f;')
281
+ expect(brand).toContain('--m-ui-surface-content: #181818;')
282
+ expect(brand).toContain("[data-slot='select-trigger']")
283
+ expect(brand).toContain("[data-slot='select-content']")
284
+ expect(brand).toContain("[data-slot='dropdown-menu-content']")
285
+ expect(brand).toContain('--material-shadow-overlay: var(--shadow-md);')
282
286
  expect(brand).toContain('--m-ui-sidebar-material-fill: #1b1d1e;')
283
287
  expect(brand).toContain('--m-ui-sidebar-border-color: #394043;')
284
288
  expect(brand).toContain(