@botiverse/raft-ui 0.9.5 → 0.10.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.
package/dist/style.css CHANGED
@@ -148,7 +148,7 @@
148
148
  transform: var(--r-float-button-transform-active);
149
149
  }
150
150
 
151
- &:focus-visible {
151
+ &:focus-visible:not(:where(:root[data-input-modality="pointer"] *)) {
152
152
  outline: var(--r-float-button-outline);
153
153
  outline-offset: var(--r-float-button-outline-offset);
154
154
  }
@@ -269,10 +269,10 @@
269
269
  }
270
270
  .r-timeline {
271
271
  --r-timeline-icon-size: var(--r-timeline-custom-icon-size, 9px);
272
- --r-timeline-title-font-size: 14px;
273
- --r-timeline-content-font-size: 12px;
274
- --r-timeline-time-font-size: 12px;
275
- --r-timeline-line-height: 20px;
272
+ --r-timeline-title-font-size: var(--text-callout);
273
+ --r-timeline-content-font-size: var(--text-mono-meta);
274
+ --r-timeline-time-font-size: var(--text-mono-meta);
275
+ --r-timeline-line-height: var(--text-callout--line-height);
276
276
  --r-timeline-item-inner-gap: 2px;
277
277
  --r-timeline-title-font-family: var(--font-sans);
278
278
  --r-timeline-title-font-weight: 400;
@@ -285,7 +285,7 @@
285
285
  --r-timeline-marker-radius: 2px;
286
286
  --r-timeline-marker-fill: 100%;
287
287
  --r-timeline-marker-border-color: var(--sticker-ink);
288
- --r-timeline-axis-offset: calc((20px - var(--r-timeline-icon-size)) / 2);
288
+ --r-timeline-axis-offset: calc((var(--r-timeline-line-height) - var(--r-timeline-icon-size)) / 2);
289
289
 
290
290
  &.r-timeline--lg {
291
291
  --r-timeline-title-font-size: 16px;
@@ -430,8 +430,8 @@
430
430
  color: var(--foreground-hint);
431
431
  font-family: var(--font-mono);
432
432
  font-size: var(--r-timeline-content-font-size);
433
+ line-height: var(--text-mono-meta--line-height);
433
434
  overflow-wrap: anywhere;
434
- line-height: 16px;
435
435
  }
436
436
 
437
437
  .r-timeline-time {
@@ -501,9 +501,9 @@
501
501
  --r-steps-indicator-size: 20px;
502
502
  --r-steps-indicator-font-size: 12px;
503
503
  --r-steps-icon-size: 14px;
504
- --r-steps-title-font-size: 13px;
505
- --r-steps-description-font-size: 12px;
506
- --r-steps-line-height: calc(18 / 13);
504
+ --r-steps-title-font-size: var(--text-footnote);
505
+ --r-steps-description-font-size: var(--text-caption);
506
+ --r-steps-line-height: var(--text-footnote--line-height);
507
507
  --r-steps-gap: 8px;
508
508
  --r-steps-connector-gap: 8px;
509
509
  --r-steps-connector-min-width: 16px;
@@ -655,8 +655,7 @@
655
655
 
656
656
  .r-step__content {
657
657
  min-width: 0;
658
- padding-top: calc((var(--r-steps-indicator-size) - var(--r-steps-title-font-size) * var(--r-steps-line-height)) /
659
- 2);
658
+ padding-top: calc((var(--r-steps-indicator-size) - var(--r-steps-line-height)) / 2);
660
659
  }
661
660
 
662
661
  .r-step__title {
@@ -672,6 +671,7 @@
672
671
  .r-step__description {
673
672
  color: var(--r-step-description-color);
674
673
  font-size: var(--r-steps-description-font-size);
674
+ line-height: var(--text-caption--line-height);
675
675
  margin-top: 2px;
676
676
  }
677
677
 
@@ -705,7 +705,7 @@
705
705
  outline: none;
706
706
  }
707
707
 
708
- &:focus-visible:after {
708
+ &:focus-visible:not(:where(:root[data-input-modality="pointer"] *)):after {
709
709
  outline: var(--r-steps-focus-outline);
710
710
  }
711
711
 
@@ -1011,27 +1011,22 @@
1011
1011
 
1012
1012
  .r-combobox-select__control {
1013
1013
  box-sizing: border-box;
1014
- border: 1.5px solid var(--line-strong);
1015
- background: var(--layer-card);
1016
1014
  min-width: 0;
1017
- min-height: 44px;
1015
+ min-height: var(--control-height);
1016
+ border: 1px solid var(--line-field);
1017
+ background: var(--layer-card);
1018
1018
  color: var(--foreground);
1019
- box-shadow: none;
1019
+ font-size: var(--field-font-size);
1020
+ line-height: var(--field-line-height);
1020
1021
  border-radius: 8px;
1021
1022
  outline: 0;
1022
1023
  align-items: center;
1023
1024
  gap: .5rem;
1024
1025
  padding: 0 6px 0 12px;
1025
- font-size: 16px;
1026
- line-height: 24px;
1027
1026
  transition: box-shadow 90ms ease-out;
1028
1027
  display: flex;
1029
1028
  }
1030
1029
 
1031
- .r-combobox-select__control:focus-within {
1032
- box-shadow: var(--focus-ring);
1033
- }
1034
-
1035
1030
  .r-combobox-select__input {
1036
1031
  box-sizing: border-box;
1037
1032
  width: 100%;
@@ -1072,13 +1067,13 @@
1072
1067
  max-width: 100%;
1073
1068
  min-height: 28px;
1074
1069
  color: var(--foreground);
1070
+ font-size: var(--text-footnote);
1071
+ line-height: var(--text-footnote--line-height);
1075
1072
  border-radius: 6px;
1076
1073
  align-items: center;
1077
1074
  gap: 6px;
1078
1075
  padding: 0 2px 0 8px;
1079
- font-size: 13px;
1080
1076
  font-weight: 600;
1081
- line-height: 18px;
1082
1077
  display: inline-flex;
1083
1078
  overflow: hidden;
1084
1079
  }
@@ -1187,15 +1182,15 @@
1187
1182
  width: 100%;
1188
1183
  min-height: 36px;
1189
1184
  color: var(--foreground);
1185
+ font-size: var(--text-callout);
1186
+ line-height: var(--text-callout--line-height);
1190
1187
  border-radius: 8px;
1191
1188
  outline: 0;
1192
1189
  justify-content: space-between;
1193
1190
  align-items: center;
1194
1191
  gap: 10px;
1195
1192
  padding: 0 10px;
1196
- font-size: 14px;
1197
1193
  font-weight: 500;
1198
- line-height: 20px;
1199
1194
  transition: background-color .12s, color .12s;
1200
1195
  display: flex;
1201
1196
  overflow: hidden;
@@ -1240,25 +1235,25 @@
1240
1235
 
1241
1236
  .r-combobox-select__group-label {
1242
1237
  color: var(--foreground-hint);
1238
+ font-size: var(--text-caption);
1239
+ line-height: var(--text-caption--line-height);
1243
1240
  padding: 8px 10px 4px;
1244
- font-size: 12px;
1245
1241
  font-weight: 700;
1246
- line-height: 16px;
1247
1242
  }
1248
1243
 
1249
1244
  .r-combobox-select__message {
1250
1245
  color: var(--foreground-hint);
1246
+ font-size: var(--text-callout);
1247
+ line-height: var(--text-callout--line-height);
1251
1248
  text-align: start;
1252
1249
  padding: 14px 16px;
1253
- font-size: 14px;
1254
- line-height: 20px;
1255
1250
  }
1256
1251
 
1257
1252
  .r-combobox-select__overflow {
1258
1253
  color: var(--foreground-hint);
1259
1254
  font-family: var(--mono-font, ui-monospace, monospace);
1260
- font-size: 12px;
1261
- line-height: 16px;
1255
+ font-size: var(--text-caption);
1256
+ line-height: var(--text-caption--line-height);
1262
1257
  }
1263
1258
 
1264
1259
  .r-combobox-select__spinner {
@@ -1269,8 +1264,8 @@
1269
1264
 
1270
1265
  .r-combobox-select--sm.r-combobox-select__control {
1271
1266
  min-height: 1.75rem;
1272
- font-size: 13px;
1273
- line-height: 18px;
1267
+ font-size: var(--text-control);
1268
+ line-height: var(--text-control--line-height);
1274
1269
  }
1275
1270
 
1276
1271
  .r-combobox-select--sm .r-combobox-select__chips {
package/dist/styles.css CHANGED
@@ -23,6 +23,26 @@
23
23
  }
24
24
  }
25
25
 
26
+ /* Focus indicators show only after keyboard navigation
27
+ (design/foundations/states.md#focus). installInputModality, which
28
+ ThemeProvider runs, keeps `data-input-modality` on the root: "pointer" after
29
+ a click or a tap, "keyboard" after a navigation key. While it says "pointer"
30
+ the focus ring is empty; with no attribute at all (no script, or the tracker
31
+ not installed) every ring shows. Each selector outranks a scope that declares
32
+ --focus-ring in foundation.css: a mode class on the root, the system-dark
33
+ fallback (:root:not(.light):not(.dark)), and a light or dark island. */
34
+ :root[data-input-modality="pointer"],
35
+ :root[data-input-modality="pointer"]:not(.light):not(.dark),
36
+ :root[data-input-modality="pointer"] :is(.light, .dark) {
37
+ --focus-ring: 0 0 transparent;
38
+ }
39
+
40
+ /* The same gate for a focus indicator not drawn with --focus-ring (an outline,
41
+ an inset ring): it matches unless the last input was a pointer. Stack it on
42
+ the state, `focus-visible:keyboard:outline-2`. Anchored on the element's own
43
+ class, so it costs WebKit no ancestor dependency on unrelated elements. */
44
+ @custom-variant keyboard (&:not(:where(:root[data-input-modality="pointer"] *)));
45
+
26
46
  @theme {
27
47
  --color-black: oklch(0 0 0);
28
48
  --color-white: oklch(1 0 0);
@@ -55,7 +75,9 @@
55
75
 
56
76
  /* Raft Design System type styles (size / line-height / weight). Grotesk is
57
77
  what people say; pair the mono styles with `font-mono` for what machines
58
- write (ids, times, counts, events, code). */
78
+ write (ids, times, counts, events, code). These are the phone values; the
79
+ desktop block below the theme sets the working sizes again on a desktop
80
+ (from 768px with a mouse or trackpad). */
59
81
  --text-hero: 40px;
60
82
  --text-hero--line-height: 44px;
61
83
  --text-hero--font-weight: 700;
@@ -89,6 +111,11 @@
89
111
  --text-mono-meta--line-height: 16px;
90
112
  --text-mono-code: 13px;
91
113
  --text-mono-code--line-height: 20px;
114
+ /* The label of a 32px control: a Button at `md`, a panel action, a filter
115
+ trigger. Its own style, because on a phone it is footnote's 13px and on a
116
+ desktop it is 14px, where footnote drops to 12. */
117
+ --text-control: 13px;
118
+ --text-control--line-height: 18px;
92
119
 
93
120
  /* Deprecated utility names, kept until raft-ui 1.0 (see codemods/token-names.mjs). */
94
121
  --color-brutal-yellow: var(--palette-yellow-400);
@@ -103,6 +130,43 @@
103
130
  --color-brutal-cream: var(--palette-cream-200);
104
131
  }
105
132
 
133
+ /* Desktop type: from 768px (Tailwind's `md`, where the desktop layout begins)
134
+ with a mouse or trackpad, the same query as the desktop metrics in
135
+ foundation.css. Feishu's PC scale, 14/22 for reading, 12px for notes (on an
136
+ 18px line, see typography.md), 16/24 for titles. A phone, and a touch
137
+ screen at any width, keep the theme's values, Feishu's mobile scale. The
138
+ type utilities read these variables (`font-size: var(--text-body)`), so
139
+ every component follows without a breakpoint of its own. This block comes
140
+ after Tailwind's theme output, so it also wins over an app's own `@theme`
141
+ values for these names: an app that changes one sets it inside this query
142
+ as well. Hero, display, title and label stay as they are: setup pages and
143
+ the phone tab bar. */
144
+ @layer theme {
145
+ @media (width >= 48rem) and (pointer: fine) {
146
+ :root,
147
+ :host {
148
+ --text-headline: 16px;
149
+ --text-headline--line-height: 24px;
150
+ --text-body: 14px;
151
+ --text-body--line-height: 22px;
152
+ --text-subhead: 14px;
153
+ --text-subhead--line-height: 22px;
154
+ --text-callout: 14px;
155
+ --text-callout--line-height: 22px;
156
+ --text-footnote: 12px;
157
+ --text-footnote--line-height: 18px;
158
+ --text-caption: 12px;
159
+ --text-caption--line-height: 18px;
160
+ --text-mono-meta: 12px;
161
+ --text-mono-meta--line-height: 18px;
162
+ --text-mono-code: 13px;
163
+ --text-mono-code--line-height: 20px;
164
+ --text-control: 14px;
165
+ --text-control--line-height: 22px;
166
+ }
167
+ }
168
+ }
169
+
106
170
  @theme inline {
107
171
  --color-foreground: var(--foreground);
108
172
  --color-foreground-muted: var(--foreground-muted);
@@ -287,6 +351,20 @@
287
351
  --text-card-title: var(--card-title-font-size);
288
352
  --text-card-title--line-height: var(--card-title-line-height);
289
353
  --font-weight-card-title: var(--card-title-font-weight);
354
+ /* ─── Control metric axis ───────────────────────────────────────────────────
355
+ The height of a default control: 44px on a phone and on a touch screen,
356
+ 32px from 768px with a mouse or trackpad (Feishu's PC default). The
357
+ tokens switch in foundation.css, so a recipe writes `h-control` /
358
+ `size-control` and never a breakpoint. A button at this height keeps a 44px hit area with
359
+ `after:-inset-y-control-hit`, which is 0 on a phone.
360
+
361
+ Consumers: `Input`, `InputGroup`, `SelectTrigger`,
362
+ `ComboboxControl`, `ComboboxChips`, `ComboboxSelect`, and Button at `lg`,
363
+ `xl` (`h-control-cta`) and `icon-lg` (`size-control-icon` for its icon). */
364
+ --spacing-control: var(--control-height);
365
+ --spacing-control-cta: var(--control-height-cta);
366
+ --spacing-control-icon: var(--control-icon-size);
367
+ --spacing-control-hit: var(--control-hit-inset);
290
368
 
291
369
  /* Deprecated utility names, kept until raft-ui 1.0 (see codemods/token-names.mjs). */
292
370
  --color-foreground-strong: var(--foreground);
package/dist/tokens.json CHANGED
@@ -1588,6 +1588,7 @@
1588
1588
  },
1589
1589
  "field": {
1590
1590
  "$value": "{line.strong}",
1591
+ "$description": "The frame of a field and the composer: the ink of line-strong, drawn 1px.",
1591
1592
  "$extensions": {
1592
1593
  "build.botiverse.raft": {
1593
1594
  "scope": "every-mode"
@@ -2496,6 +2497,7 @@
2496
2497
  "$type": "shadow",
2497
2498
  "ring": {
2498
2499
  "$value": "0 0 0 2px oklch(1 0 0), 0 0 0 4px oklch(0.1813 0.005 17.62)",
2500
+ "$description": "The focus indicator. It shows only after keyboard navigation: styles.css empties it while the root carries data-input-modality=\"pointer\", which installInputModality sets after a click or a tap.",
2499
2501
  "$extensions": {
2500
2502
  "build.botiverse.raft": {
2501
2503
  "css": true,
@@ -2512,7 +2514,15 @@
2512
2514
  "value": 16,
2513
2515
  "unit": "px"
2514
2516
  },
2515
- "$type": "dimension"
2517
+ "$type": "dimension",
2518
+ "$extensions": {
2519
+ "build.botiverse.raft": {
2520
+ "desktop": {
2521
+ "value": 14,
2522
+ "unit": "px"
2523
+ }
2524
+ }
2525
+ }
2516
2526
  },
2517
2527
  "font-weight": {
2518
2528
  "$value": 400,
@@ -2523,7 +2533,15 @@
2523
2533
  "value": 24,
2524
2534
  "unit": "px"
2525
2535
  },
2526
- "$type": "dimension"
2536
+ "$type": "dimension",
2537
+ "$extensions": {
2538
+ "build.botiverse.raft": {
2539
+ "desktop": {
2540
+ "value": 22,
2541
+ "unit": "px"
2542
+ }
2543
+ }
2544
+ }
2527
2545
  }
2528
2546
  },
2529
2547
  "card-title": {
@@ -2532,7 +2550,15 @@
2532
2550
  "value": 16,
2533
2551
  "unit": "px"
2534
2552
  },
2535
- "$type": "dimension"
2553
+ "$type": "dimension",
2554
+ "$extensions": {
2555
+ "build.botiverse.raft": {
2556
+ "desktop": {
2557
+ "value": 14,
2558
+ "unit": "px"
2559
+ }
2560
+ }
2561
+ }
2536
2562
  },
2537
2563
  "font-weight": {
2538
2564
  "$value": 600,
@@ -2543,7 +2569,78 @@
2543
2569
  "value": 24,
2544
2570
  "unit": "px"
2545
2571
  },
2546
- "$type": "dimension"
2572
+ "$type": "dimension",
2573
+ "$extensions": {
2574
+ "build.botiverse.raft": {
2575
+ "desktop": {
2576
+ "value": 22,
2577
+ "unit": "px"
2578
+ }
2579
+ }
2580
+ }
2581
+ }
2582
+ },
2583
+ "control": {
2584
+ "$type": "dimension",
2585
+ "height": {
2586
+ "$value": {
2587
+ "value": 44,
2588
+ "unit": "px"
2589
+ },
2590
+ "$description": "A default control: a field, a Button at lg, an icon button at icon-lg. A finger's 44px on a phone, Feishu's 32px on a desktop.",
2591
+ "$extensions": {
2592
+ "build.botiverse.raft": {
2593
+ "desktop": {
2594
+ "value": 32,
2595
+ "unit": "px"
2596
+ }
2597
+ }
2598
+ }
2599
+ },
2600
+ "height-cta": {
2601
+ "$value": {
2602
+ "value": 52,
2603
+ "unit": "px"
2604
+ },
2605
+ "$description": "The full-width call to action across a sign-in, sign-up or setup form (Button xl).",
2606
+ "$extensions": {
2607
+ "build.botiverse.raft": {
2608
+ "desktop": {
2609
+ "value": 40,
2610
+ "unit": "px"
2611
+ }
2612
+ }
2613
+ }
2614
+ },
2615
+ "icon-size": {
2616
+ "$value": {
2617
+ "value": 22,
2618
+ "unit": "px"
2619
+ },
2620
+ "$description": "The icon in a control-height icon button.",
2621
+ "$extensions": {
2622
+ "build.botiverse.raft": {
2623
+ "desktop": {
2624
+ "value": 18,
2625
+ "unit": "px"
2626
+ }
2627
+ }
2628
+ }
2629
+ },
2630
+ "hit-inset": {
2631
+ "$value": {
2632
+ "value": 0,
2633
+ "unit": "px"
2634
+ },
2635
+ "$description": "How far a control-height button's hit area reaches past its top and bottom, to keep 44px.",
2636
+ "$extensions": {
2637
+ "build.botiverse.raft": {
2638
+ "desktop": {
2639
+ "value": 6,
2640
+ "unit": "px"
2641
+ }
2642
+ }
2643
+ }
2547
2644
  }
2548
2645
  },
2549
2646
  "sticker": {
package/dist/wip.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { O as ButtonProps, T as DialogTrigger, i as DialogClose, s as DialogContentProps, t as Dialog, w as DialogTitleProps } from "./index-DG62I-YM.mjs";
1
+ import { O as ButtonProps, T as DialogTrigger, i as DialogClose, s as DialogContentProps, t as Dialog, w as DialogTitleProps } from "./index-CDHLWJtB.mjs";
2
2
  import { CSSProperties, ComponentPropsWithRef, ReactNode } from "react";
3
3
  import { Combobox } from "@base-ui/react/combobox";
4
4
  //#region src/components/combobox-select/combobox-select.types.d.ts
package/dist/wip.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { S as useTheme, f as DialogTitle, i as DialogContent, m as Button, p as DialogTrigger, r as DialogClose, t as Dialog, w as tv, x as useThemeScopeProps } from "./dialog-BYH5ojYr.mjs";
1
+ import { S as useTheme, f as DialogTitle, i as DialogContent, m as Button, p as DialogTrigger, r as DialogClose, t as Dialog, w as tv, x as useThemeScopeProps } from "./dialog-CDPAT3DH.mjs";
2
2
  import { Check, ChevronDown, LoaderCircle, Plus, X } from "lucide-react";
3
3
  import { useCallback, useId, useMemo, useRef, useState } from "react";
4
4
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
@@ -170,7 +170,7 @@ function ComboboxSelect({ options, multiple, value, defaultValue, onValueChange,
170
170
  const inputClassName = ["r-combobox-select__input", typeof inputProps?.className === "string" ? inputProps.className : null].filter(Boolean).join(" ");
171
171
  const controlClassName = [
172
172
  css,
173
- "r-combobox-select__control",
173
+ "r-combobox-select__control has-focus-visible:shadow-focus",
174
174
  className
175
175
  ].filter(Boolean).join(" ");
176
176
  const positionerClassName = ["r-combobox-select__positioner", typeof positionerProps?.className === "string" ? positionerProps.className : null].filter(Boolean).join(" ");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botiverse/raft-ui",
3
- "version": "0.9.5",
3
+ "version": "0.10.0",
4
4
  "description": "React components and CSS foundations for the Raft Design System.",
5
5
  "keywords": [
6
6
  "agent-skills",
@@ -36,7 +36,7 @@ Judge whether the surface speaks Raft's material language:
36
36
  - Corners follow the shape scale in `styles.css`: sticker, chip, control, container, dialog; fully round only for switches and radios. Flag corners off the scale or mixed within one group.
37
37
  - Space Grotesk is what people say; Space Mono is what machines write (ids, times, counts, events, code). Flag machine output set in sans and prose set in mono.
38
38
  - Yellow and pink must carry semantic roles. Flag decorative yellow/pink fields that compete with primary actions or status.
39
- - Hover and pressed keep a control's resting color under an ink wash; focus-visible is the ink ring. Flag a state that swaps a sticker's color or that is indistinguishable from rest.
39
+ - Hover and pressed keep a control's resting color under an ink wash; focus-visible is the ink ring, shown only after keyboard navigation. Flag a state that swaps a sticker's color or that is indistinguishable from rest, and a ring that appears after a click or a tap.
40
40
  - Stickers, ink edges, and bold type create high baseline intensity. Require a clear entry point and quieter secondary information so the surface does not shout uniformly.
41
41
 
42
42
  Ask: does every sticker, line, and shadow carry a role, or is the surface merely loud?
@@ -63,7 +63,7 @@ Custom fonts use `--heading-font`, `--sans-font`, and `--mono-font`.
63
63
 
64
64
  | Provider | Responsibility |
65
65
  | ----------------- | --------------------------------------------------------------------------------------------------------------------- |
66
- | `ThemeProvider` | Color mode, persistence, and document mode synchronization |
66
+ | `ThemeProvider` | Color mode, persistence, document mode synchronization, and the input modality that keeps focus rings to the keyboard |
67
67
  | `TooltipProvider` | Shared tooltip behavior when using Tooltip |
68
68
  | `ToastProvider` | Toast manager when using the toast API; see the [Toast](https://raft-design.botiverse.build/components/toast.md) page |
69
69
 
@@ -74,6 +74,7 @@ Custom fonts use `--heading-font`, `--sans-font`, and `--mono-font`.
74
74
  ## Mode and portal boundaries
75
75
 
76
76
  - `ThemeProvider` synchronizes `document.documentElement` by default. Nested scopes need `syncDom={false}` and the matching `light`/`dark` class on their root.
77
+ - Focus rings show only after keyboard navigation, not after a click, a tap or typing. `ThemeProvider` installs the tracker (`data-input-modality` on `document.documentElement`); an app without it calls `installInputModality()` once on the client. Draw an indicator with `focus-visible:shadow-focus` (`has-focus-visible:shadow-focus` on a group); one that is not the token takes the `keyboard:` variant (`focus-visible:keyboard:outline-2`). See [States](https://raft-design.botiverse.build/foundations/states.md#focus).
77
78
  - Base UI overlays portal to `document.body`. An app root with `isolation: isolate` keeps its stacking contexts below those surfaces. Embedded previews can use `portalProps.container` (toasts: `ToastProvider portalContainer`) to keep overlays inside their scope.
78
79
  - SSR apps with persisted mode need to restore it before first paint to avoid a theme flash.
79
80