@agent-native/core 0.137.0 → 0.137.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.
Files changed (47) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/app/components/AgentCompletionSound.tsx +105 -0
  3. package/corpus/templates/analytics/app/components/layout/Layout.tsx +44 -35
  4. package/corpus/templates/analytics/app/i18n-data.ts +38 -0
  5. package/corpus/templates/analytics/app/pages/Settings.tsx +53 -1
  6. package/corpus/templates/analytics/app/pages/settings/settings-search.ts +6 -0
  7. package/corpus/templates/analytics/changelog/2026-08-04-choose-whether-analytics-plays-a-bell-when-the-agent-finishe.md +6 -0
  8. package/corpus/templates/analytics/shared/analytics-user-prefs.ts +2 -0
  9. package/corpus/templates/slides/app/components/editor/AddSlidePopover.tsx +258 -0
  10. package/corpus/templates/slides/app/components/editor/EditorActionCluster.tsx +130 -0
  11. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +25 -337
  12. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +73 -53
  13. package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +321 -0
  14. package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +32 -11
  15. package/corpus/templates/slides/app/components/editor/SlideContextToolbar.tsx +804 -0
  16. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +101 -45
  17. package/corpus/templates/slides/app/components/editor/SlideOverflowWarning.tsx +7 -4
  18. package/corpus/templates/slides/app/components/editor/bullet-editing.ts +11 -3
  19. package/corpus/templates/slides/app/components/editor/commit-active-edit.ts +21 -0
  20. package/corpus/templates/slides/app/components/editor/list-editing.ts +219 -0
  21. package/corpus/templates/slides/app/components/editor/selection-overlay-measurement.ts +0 -3
  22. package/corpus/templates/slides/app/components/editor/slide-style.ts +199 -0
  23. package/corpus/templates/slides/app/global.css +14 -3
  24. package/corpus/templates/slides/app/i18n/en-US.ts +8 -1
  25. package/corpus/templates/slides/app/lib/recent-references.ts +82 -0
  26. package/corpus/templates/slides/app/pages/DeckEditor.tsx +51 -22
  27. package/corpus/templates/slides/app/pages/Index.tsx +236 -181
  28. package/corpus/templates/slides/changelog/2026-08-01-add-slide-moved-to-the-toolbar-and-the-slide-rail-is-now-mor.md +9 -0
  29. package/corpus/templates/slides/changelog/2026-08-01-add-slide-undo-redo-and-the-text-tool-now-lead-the-slide-too.md +9 -0
  30. package/corpus/templates/slides/changelog/2026-08-01-slide-styling-now-appears-in-a-contextual-toolbar-above-the-.md +6 -0
  31. package/corpus/templates/slides/changelog/2026-08-01-the-slide-style-side-panel-is-retired-all-styling-now-lives-.md +6 -0
  32. package/corpus/templates/slides/changelog/2026-08-03-the-slide-toolbar-is-denser-swatch-only-colors-dropdowns-for.md +10 -0
  33. package/corpus/templates/slides/changelog/2026-08-04-bullet-and-numbered-list-buttons-in-the-slide-toolbar-conver.md +6 -0
  34. package/corpus/templates/slides/changelog/2026-08-04-italic-and-underline-are-now-one-click-away-in-the-slide-too.md +6 -0
  35. package/dist/client/DefaultSpinner.d.ts +0 -12
  36. package/dist/client/DefaultSpinner.js +22 -2
  37. package/dist/client/onboarding/use-onboarding.js +3 -0
  38. package/dist/collab/routes.d.ts +1 -1
  39. package/dist/collab/struct-routes.d.ts +1 -1
  40. package/dist/deploy/build.js +2 -1
  41. package/dist/notifications/routes.d.ts +2 -2
  42. package/dist/progress/routes.d.ts +1 -1
  43. package/dist/server/realtime-token.d.ts +1 -1
  44. package/dist/templates/workspace-core/.agents/skills/address-feedback/SKILL.md +25 -9
  45. package/package.json +2 -2
  46. package/src/templates/workspace-core/.agents/skills/address-feedback/SKILL.md +25 -9
  47. package/corpus/templates/slides/app/components/editor/SlideStyleInspector.tsx +0 -763
@@ -74,6 +74,11 @@ import {
74
74
  slidesCanvasInteractionCore,
75
75
  } from "./canvas-interactions";
76
76
  import ImageOverlay from "./ImageOverlay";
77
+ import {
78
+ detectSlideListKind,
79
+ toggleSlideList,
80
+ type SlideListKind,
81
+ } from "./list-editing";
77
82
  import {
78
83
  applyInlineTextStyle,
79
84
  getInlineTextStyleSnapshot,
@@ -119,13 +124,9 @@ import {
119
124
  type SlidesSelectionTool,
120
125
  } from "./slide-object-interactions";
121
126
  import { getPassiveSlidePresenceUsers } from "./slide-presence";
127
+ import { type SlideStylePatch, type SlideStyleSnapshot } from "./slide-style";
128
+ import { SlideContextToolbar } from "./SlideContextToolbar";
122
129
  import { SlideOverflowWarning } from "./SlideOverflowWarning";
123
- import {
124
- SlideBackgroundInspector,
125
- SlideStyleInspector,
126
- type SlideStylePatch,
127
- type SlideStyleSnapshot,
128
- } from "./SlideStyleInspector";
129
130
  import { SpeakerNotesPanel } from "./SpeakerNotesPanel";
130
131
 
131
132
  let builderIdCounter = 0;
@@ -293,7 +294,15 @@ function stripBuilderIds(html: string): string {
293
294
  cleaned = stripRoot?.innerHTML ?? doc.body.innerHTML;
294
295
  }
295
296
 
296
- return cleaned.replace(/\s*data-builder-id="[^"]*"/g, "");
297
+ return (
298
+ cleaned
299
+ .replace(/\s*data-builder-id="[^"]*"/g, "")
300
+ // An active inline edit marks its host element. Serializing mid-edit —
301
+ // which the list toggles and the draft capture both do — would otherwise
302
+ // bake the editing state into saved slide content.
303
+ .replace(/\s*contenteditable="[^"]*"/gi, "")
304
+ .replace(/\s*data-editing-block="[^"]*"/g, "")
305
+ );
297
306
  }
298
307
 
299
308
  /**
@@ -359,6 +368,8 @@ const INLINE_INSPECTOR_STYLE_KEYS = [
359
368
  "color",
360
369
  "fontSize",
361
370
  "fontWeight",
371
+ "fontStyle",
372
+ "textDecoration",
362
373
  ] as const satisfies readonly (keyof SlideStylePatch)[];
363
374
 
364
375
  function inlineInspectorStylePatch(
@@ -448,6 +459,8 @@ function buildStyleSnapshot(
448
459
  const selectedColor = selectedTextStyle?.values.color;
449
460
  const selectedFontSize = selectedTextStyle?.values.fontSize;
450
461
  const selectedFontWeight = selectedTextStyle?.values.fontWeight;
462
+ const selectedFontStyle = selectedTextStyle?.values.fontStyle;
463
+ const selectedTextDecoration = selectedTextStyle?.values.textDecoration;
451
464
 
452
465
  return {
453
466
  selector,
@@ -471,6 +484,8 @@ function buildStyleSnapshot(
471
484
  selectedFontWeight === null || selectedFontWeight === undefined
472
485
  ? normalizedFontWeight(computed.fontWeight)
473
486
  : normalizedFontWeight(selectedFontWeight),
487
+ fontStyle: selectedFontStyle ?? computed.fontStyle,
488
+ textDecoration: selectedTextDecoration ?? computed.textDecorationLine,
474
489
  lineHeight,
475
490
  textAlign: normalizedTextAlign(computed.textAlign),
476
491
  opacity: Math.round(Number(computed.opacity || 1) * 100),
@@ -480,6 +495,7 @@ function buildStyleSnapshot(
480
495
  paddingX: Math.round((paddingLeft + paddingRight) / 2),
481
496
  paddingY: Math.round((paddingTop + paddingBottom) / 2),
482
497
  zIndex,
498
+ listKind: detectSlideListKind(element),
483
499
  textStyleScope: inlineTextStyle?.scope ?? "block",
484
500
  mixedTextStyles: selectedTextStyle?.mixed ?? [],
485
501
  isAbsolute,
@@ -565,6 +581,12 @@ interface SlideEditorProps {
565
581
  * navigable but contentEditable / image overlays don't activate.
566
582
  * Mirrors Google Slides' viewer experience. */
567
583
  readOnly?: boolean;
584
+ /** Full-width host for the contextual style toolbar, rendered by the editor
585
+ * shell above the slide rail. Selection state lives here, so the toolbar is
586
+ * portaled out rather than lifted. Falls back to rendering in place. */
587
+ contextToolbarSlot?: HTMLElement | null;
588
+ /** Selection-independent actions for the head of the contextual toolbar. */
589
+ contextToolbarLeading?: ReactNode;
568
590
  onGenerateImage: () => void;
569
591
  onOpenAssetLibrary: (replaceSrc: string) => void;
570
592
  onUploadImage: (replaceSrc: string) => void;
@@ -601,10 +623,6 @@ interface SlideEditorProps {
601
623
  designSystem?: DesignSystemData;
602
624
  /** Deck aspect ratio (defaults to 16:9 when omitted) */
603
625
  aspectRatio?: AspectRatio;
604
- /** Whether the right-side style inspector is visible */
605
- stylePanelOpen?: boolean;
606
- /** Close the right-side style inspector */
607
- onCloseStylePanel?: () => void;
608
626
  /** Whether the draw-to-prompt overlay is visible */
609
627
  drawMode?: boolean;
610
628
  /** Called when the draw overlay should exit (Esc, Send, close button) */
@@ -1027,6 +1045,8 @@ export default function SlideEditor({
1027
1045
  slide,
1028
1046
  onUpdateSlide,
1029
1047
  readOnly = false,
1048
+ contextToolbarSlot,
1049
+ contextToolbarLeading,
1030
1050
  onGenerateImage,
1031
1051
  onOpenAssetLibrary,
1032
1052
  onUploadImage,
@@ -1040,8 +1060,6 @@ export default function SlideEditor({
1040
1060
  slideCount = 1,
1041
1061
  designSystem,
1042
1062
  aspectRatio,
1043
- stylePanelOpen = false,
1044
- onCloseStylePanel,
1045
1063
  drawMode,
1046
1064
  onExitDrawMode,
1047
1065
  pinMode,
@@ -1141,7 +1159,6 @@ export default function SlideEditor({
1141
1159
  selector: selectedElementSelector,
1142
1160
  path: selectedElementPath,
1143
1161
  canvasZoom,
1144
- stylePanelOpen,
1145
1162
  revision: selectionMeasurementRevision,
1146
1163
  });
1147
1164
  const selectedElementRect = currentSelectionOverlayRect(
@@ -3986,6 +4003,45 @@ export default function SlideEditor({
3986
4003
  ],
3987
4004
  );
3988
4005
 
4006
+ /** Bullet / numbered list toggle for the selected text object. */
4007
+ const handleToggleList = useCallback(
4008
+ (kind: SlideListKind) => {
4009
+ const target = editingElRef.current ?? resolveSelectedElement();
4010
+ if (!target) return;
4011
+ // Editing one item still means "this list". Converting the LI itself
4012
+ // would build a second list inside it instead of toggling the one it
4013
+ // already belongs to.
4014
+ const parentList =
4015
+ target.tagName === "LI" ? target.closest("ul, ol") : null;
4016
+ const element = (parentList as HTMLElement | null) ?? target;
4017
+
4018
+ const editing = editingElRef.current;
4019
+ const converted = toggleSlideList(element, kind);
4020
+ if (!converted) return;
4021
+
4022
+ // Conversions retag and replace nodes, so an active edit can be left
4023
+ // pointing at a node that is no longer in the page. Move it onto the
4024
+ // element that replaced it rather than silently writing into nothing.
4025
+ if (editing && !editing.isConnected) {
4026
+ converted.contentEditable = "true";
4027
+ converted.setAttribute("data-editing-block", "true");
4028
+ editingElRef.current = converted;
4029
+ setEditingEl(converted);
4030
+ converted.focus({ preventScroll: true });
4031
+ }
4032
+
4033
+ const html = readCurrentSlideContentHtml();
4034
+ if (html !== null) onUpdateSlideRef.current({ content: html });
4035
+ const selector = getBuilderSelector(converted);
4036
+ if (selector) selectElementForStyling(converted, selector);
4037
+ },
4038
+ [
4039
+ readCurrentSlideContentHtml,
4040
+ resolveSelectedElement,
4041
+ selectElementForStyling,
4042
+ ],
4043
+ );
4044
+
3989
4045
  // --- Pending visual updates ---
3990
4046
  const [pendingUpdateCount, setPendingUpdateCount] = useState(0);
3991
4047
 
@@ -4050,11 +4106,42 @@ export default function SlideEditor({
4050
4106
  const isSelectedElementDraggable = selectedForDrag
4051
4107
  ? getComputedStyle(selectedForDrag).position === "absolute"
4052
4108
  : false;
4109
+ // Excalidraw slides have no selectable slide content, so the row collapses
4110
+ // to its slide-level state — but that state owns the background picker, and
4111
+ // SlideRenderer paints `slide.background` behind the drawing, so the row has
4112
+ // to stay mounted or that background becomes uneditable.
4113
+ const contextToolbar = !readOnly ? (
4114
+ <div
4115
+ className="hidden shrink-0 lg:block"
4116
+ // Snapshotting the range is only half the job: without this marker the
4117
+ // click-outside handler exits the edit and clears the snapshot before
4118
+ // the button's onClick runs, so partial-text formatting would silently
4119
+ // apply to the whole object.
4120
+ data-slide-inline-edit-surface="true"
4121
+ onPointerDownCapture={preserveRichTextSelection}
4122
+ >
4123
+ <SlideContextToolbar
4124
+ snapshot={selectedStyleSnapshot}
4125
+ background={slide.background}
4126
+ designSystem={designSystem}
4127
+ leading={contextToolbarLeading}
4128
+ onChange={applySelectedStylePatch}
4129
+ onBackgroundChange={applySlideBackground}
4130
+ onArrange={handleArrangeSelected}
4131
+ onToggleList={handleToggleList}
4132
+ />
4133
+ </div>
4134
+ ) : null;
4135
+
4053
4136
  return (
4054
4137
  <div
4055
4138
  className="relative flex h-full min-h-0 flex-1 flex-col overflow-hidden"
4056
4139
  data-slide-element-selected={slideElementSelected ? "true" : undefined}
4057
4140
  >
4141
+ {contextToolbarSlot
4142
+ ? createPortal(contextToolbar, contextToolbarSlot)
4143
+ : contextToolbar}
4144
+
4058
4145
  <div className="flex min-h-0 flex-1 overflow-hidden">
4059
4146
  <div className="min-w-0 flex-1 overflow-hidden">
4060
4147
  {slide.excalidrawData ? (
@@ -4197,37 +4284,6 @@ export default function SlideEditor({
4197
4284
  </div>
4198
4285
  )}
4199
4286
  </div>
4200
-
4201
- {!readOnly && stylePanelOpen && (
4202
- <div
4203
- className="relative z-[70] hidden h-full w-[17rem] shrink-0 border-l border-border/70 bg-background/95 lg:block"
4204
- data-slide-style-dock="true"
4205
- onPointerDownCapture={preserveRichTextSelection}
4206
- >
4207
- {selectedStyleSnapshot ? (
4208
- <SlideStyleInspector
4209
- snapshot={selectedStyleSnapshot}
4210
- designSystem={designSystem}
4211
- className="h-full w-full rounded-none border-0 bg-transparent shadow-none"
4212
- onChange={applySelectedStylePatch}
4213
- onArrange={handleArrangeSelected}
4214
- onClose={() => {
4215
- clearSelectedElement();
4216
- syncSelectionToAppState(null);
4217
- onCloseStylePanel?.();
4218
- }}
4219
- />
4220
- ) : (
4221
- <SlideBackgroundInspector
4222
- background={slide.background}
4223
- designSystem={designSystem}
4224
- className="h-full w-full rounded-none border-0 bg-transparent shadow-none"
4225
- onChange={applySlideBackground}
4226
- onClose={onCloseStylePanel}
4227
- />
4228
- )}
4229
- </div>
4230
- )}
4231
4287
  </div>
4232
4288
 
4233
4289
  <SpeakerNotesPanel
@@ -31,18 +31,21 @@ export function SlideOverflowWarning({
31
31
  <div
32
32
  role="status"
33
33
  aria-live="polite"
34
- className="absolute top-3 left-3 z-20 flex items-center gap-2 rounded-md border border-amber-400/70 bg-amber-950/95 px-2.5 py-1.5 text-xs text-amber-50 shadow-lg"
34
+ className="absolute -top-12 left-0 z-20 flex items-center gap-2 rounded-md border border-foreground/40 px-2 py-1 text-xs text-foreground"
35
35
  onPointerDown={(event) => event.stopPropagation()}
36
36
  onClick={(event) => event.stopPropagation()}
37
37
  >
38
- <IconAlertTriangle className="h-3.5 w-3.5 flex-shrink-0" stroke={2} />
38
+ <IconAlertTriangle
39
+ className="h-3.5 w-3.5 flex-shrink-0 text-foreground"
40
+ stroke={2}
41
+ />
39
42
  <span className="leading-tight">
40
43
  Layout overflows by {visibleOverflowLabel}
41
44
  </span>
42
45
  <Button
43
46
  size="sm"
44
47
  variant="ghost"
45
- className="ml-1 h-6 cursor-pointer px-2 text-[11px] font-medium text-amber-50 hover:bg-amber-400/20 hover:text-white"
48
+ className="h-6 cursor-pointer px-1.5 text-[11px] font-medium text-foreground hover:bg-transparent hover:underline"
46
49
  onClick={onFix}
47
50
  disabled={isAskingAgentToFix}
48
51
  >
@@ -51,7 +54,7 @@ export function SlideOverflowWarning({
51
54
  <Button
52
55
  size="icon"
53
56
  variant="ghost"
54
- className="size-6 cursor-pointer text-amber-50 hover:bg-amber-400/20 hover:text-white"
57
+ className="size-6 cursor-pointer text-foreground hover:bg-transparent hover:text-foreground/70"
55
58
  onClick={onDismiss}
56
59
  aria-label={dismissLabel}
57
60
  title={dismissLabel}
@@ -200,9 +200,9 @@ export function convertMarkdownPrefixToBullet(el: HTMLElement): boolean {
200
200
  }
201
201
 
202
202
  /**
203
- * Walk up from a text leaf to the nearest enclosing styled bullet-row
204
- * container, so Enter can add a new item to the whole list instead of being
205
- * trapped inside one item.
203
+ * Walk up from a text leaf to the nearest enclosing list — a native UL/OL or
204
+ * a styled bullet-row container so Enter can add a new item to the whole
205
+ * list instead of being trapped inside one item.
206
206
  */
207
207
  export function findEnclosingList(
208
208
  el: HTMLElement,
@@ -212,6 +212,14 @@ export function findEnclosingList(
212
212
  while (node && root.contains(node)) {
213
213
  const parentEl: HTMLElement | null = node.parentElement;
214
214
  if (!parentEl) break;
215
+ // A native item can only gain a sibling when the list is the editing
216
+ // host; with the LI itself as host the browser splits inside the item.
217
+ if (
218
+ node.tagName === "LI" &&
219
+ (parentEl.tagName === "UL" || parentEl.tagName === "OL")
220
+ ) {
221
+ return parentEl;
222
+ }
215
223
  if (isBulletRow(node) && isBulletList(parentEl)) return parentEl;
216
224
  // Even from a bullet row whose siblings aren't all bullets, treat the
217
225
  // parent as a list once it holds two or more bullet rows.
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The Cmd+Z handler in DeckContext deliberately ignores keystrokes while the
3
+ * user is typing, so the text editor keeps its own history. A menu item or
4
+ * button has no such escape hatch: without committing first, invoking undo
5
+ * mid-edit would skip the in-progress text and undo the deck op behind it,
6
+ * silently discarding what was just typed.
7
+ */
8
+ export function commitActiveEditThenRun(run: () => void) {
9
+ const active = document.activeElement as HTMLElement | null;
10
+ if (
11
+ active &&
12
+ (active.isContentEditable ||
13
+ active.tagName === "INPUT" ||
14
+ active.tagName === "TEXTAREA")
15
+ ) {
16
+ active.blur();
17
+ requestAnimationFrame(run);
18
+ return;
19
+ }
20
+ run();
21
+ }
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Convert a slide text object between plain lines and real UL/OL markup.
3
+ *
4
+ * Decks generated by the agent represent bullets as styled flex rows (see
5
+ * `bullet-editing.ts`). Toolbar-created lists deliberately use semantic markup
6
+ * instead, because an ordered list must renumber itself when items are added,
7
+ * removed, or reordered, and styled rows cannot. Both shapes stay editable:
8
+ * `RICH_BLOCK_TAGS` in SlideEditor already treats UL/OL/LI as text blocks.
9
+ *
10
+ * No React exports, so this stays Fast-Refresh friendly and unit-testable.
11
+ */
12
+
13
+ import { isBulletMarker } from "./bullet-editing";
14
+
15
+ export type SlideListKind = "bullet" | "ordered";
16
+
17
+ const LIST_TAG: Record<SlideListKind, "UL" | "OL"> = {
18
+ bullet: "UL",
19
+ ordered: "OL",
20
+ };
21
+
22
+ /**
23
+ * Tailwind's preflight sets `list-style: none` on every UL and OL, so the type
24
+ * has to be restated inline or the list renders with no markers at all. The
25
+ * marker sits outside the content box so a wrapped line aligns under its own
26
+ * text rather than under the marker, matching how styled rows already read.
27
+ */
28
+ const LIST_STYLE: Record<SlideListKind, string> = {
29
+ bullet:
30
+ "margin:0;padding-left:1.25em;list-style-position:outside;list-style-type:disc;",
31
+ ordered:
32
+ "margin:0;padding-left:1.25em;list-style-position:outside;list-style-type:decimal;",
33
+ };
34
+
35
+ function isListTag(element: Element): boolean {
36
+ return element.tagName === "UL" || element.tagName === "OL";
37
+ }
38
+
39
+ /**
40
+ * The list this object is, or the single list it wholly contains. A list
41
+ * sitting beside other content is not something a whole-object toggle owns.
42
+ */
43
+ function listElement(element: HTMLElement): HTMLElement | null {
44
+ if (isListTag(element)) return element;
45
+ const children = Array.from(element.children);
46
+ return children.length === 1 && isListTag(children[0])
47
+ ? (children[0] as HTMLElement)
48
+ : null;
49
+ }
50
+
51
+ export function detectSlideListKind(
52
+ element: HTMLElement | null,
53
+ ): SlideListKind | null {
54
+ const list = element ? listElement(element) : null;
55
+ if (!list) return null;
56
+ return list.tagName === "OL" ? "ordered" : "bullet";
57
+ }
58
+
59
+ /**
60
+ * Swap an element's tag while keeping its attributes and children. UL and OL
61
+ * differ only by tag, so switching list kind in place would otherwise leave an
62
+ * ordered list still rendering bullets.
63
+ */
64
+ function retag(element: HTMLElement, tagName: string): HTMLElement {
65
+ const replacement = element.ownerDocument.createElement(tagName);
66
+ for (const attribute of Array.from(element.attributes)) {
67
+ replacement.setAttribute(attribute.name, attribute.value);
68
+ }
69
+ replacement.append(...Array.from(element.childNodes));
70
+ element.replaceWith(replacement);
71
+ return replacement;
72
+ }
73
+
74
+ const BLOCK_TAGS = new Set([
75
+ "ARTICLE",
76
+ "BLOCKQUOTE",
77
+ "DIV",
78
+ "H1",
79
+ "H2",
80
+ "H3",
81
+ "H4",
82
+ "H5",
83
+ "H6",
84
+ "LI",
85
+ "OL",
86
+ "P",
87
+ "PRE",
88
+ "SECTION",
89
+ "UL",
90
+ ]);
91
+
92
+ /**
93
+ * The object's children when it is built out of block elements, or null when
94
+ * it holds a single run of text. Bare text or an inline tag at the top level
95
+ * means the content is one line: treating `<strong>` as a block would keep
96
+ * only its text and silently drop everything around it.
97
+ */
98
+ function blockChildren(source: HTMLElement): HTMLElement[] | null {
99
+ const blocks: HTMLElement[] = [];
100
+ for (const node of Array.from(source.childNodes)) {
101
+ if (node.nodeType === Node.TEXT_NODE) {
102
+ if ((node.textContent ?? "").trim()) return null;
103
+ continue;
104
+ }
105
+ if (!(node instanceof HTMLElement)) continue;
106
+ // A break separates lines within a block run; it never makes one.
107
+ if (node.tagName === "BR") continue;
108
+ if (!BLOCK_TAGS.has(node.tagName)) return null;
109
+ blocks.push(node);
110
+ }
111
+ return blocks.length > 0 ? blocks : null;
112
+ }
113
+
114
+ /**
115
+ * The inner HTML of each line the object currently holds. A styled bullet row
116
+ * contributes only its text, so converting agent-generated bullets to a real
117
+ * list drops the now-duplicated marker instead of rendering two markers.
118
+ */
119
+ function readLines(source: HTMLElement): string[] {
120
+ const blocks = blockChildren(source);
121
+
122
+ if (blocks) {
123
+ return blocks.flatMap((block) => {
124
+ const stripped = block.cloneNode(true) as HTMLElement;
125
+ for (const child of Array.from(stripped.children)) {
126
+ if (isBulletMarker(child)) child.remove();
127
+ }
128
+ const html = stripped.innerHTML.trim();
129
+ return html ? [html] : [];
130
+ });
131
+ }
132
+
133
+ return source.innerHTML
134
+ .split(/<br\s*\/?>/i)
135
+ .map((line) => line.trim())
136
+ .filter(Boolean);
137
+ }
138
+
139
+ function itemHtml(list: HTMLElement): string[] {
140
+ return Array.from(list.children)
141
+ .filter((child): child is HTMLElement => child instanceof HTMLElement)
142
+ .map((item) => item.innerHTML);
143
+ }
144
+
145
+ function buildList(
146
+ doc: Document,
147
+ kind: SlideListKind,
148
+ lines: string[],
149
+ ): HTMLElement {
150
+ const list = doc.createElement(LIST_TAG[kind]);
151
+ list.setAttribute("style", LIST_STYLE[kind]);
152
+ for (const line of lines) {
153
+ const item = doc.createElement("li");
154
+ item.innerHTML = line;
155
+ list.appendChild(item);
156
+ }
157
+ return list;
158
+ }
159
+
160
+ function buildLines(doc: Document, lines: string[]): DocumentFragment {
161
+ const fragment = doc.createDocumentFragment();
162
+ for (const line of lines) {
163
+ const block = doc.createElement("div");
164
+ block.innerHTML = line;
165
+ fragment.appendChild(block);
166
+ }
167
+ return fragment;
168
+ }
169
+
170
+ /**
171
+ * Toggle `element` into a list of `kind`: switching kind when it is already
172
+ * the other one, and back to plain lines when it already matches. Returns the
173
+ * element now holding the content, or null when there was nothing to convert
174
+ * so the caller can skip a pointless slide write.
175
+ */
176
+ export function toggleSlideList(
177
+ element: HTMLElement,
178
+ kind: SlideListKind,
179
+ ): HTMLElement | null {
180
+ const doc = element.ownerDocument;
181
+ const existing = listElement(element);
182
+
183
+ if (!existing) {
184
+ const lines = readLines(element);
185
+ if (lines.length === 0) return null;
186
+ element.replaceChildren(buildList(doc, kind, lines));
187
+ return element;
188
+ }
189
+
190
+ const lines = itemHtml(existing);
191
+
192
+ if (existing.tagName !== LIST_TAG[kind]) {
193
+ // Same content, different marker. The tag alone is not enough: an inline
194
+ // `list-style-type` from the other kind would keep painting the old
195
+ // marker, so restate it. When the object itself was the list, the retagged
196
+ // node replaces it and is what the caller must go on using, since
197
+ // `element` is now detached.
198
+ const switched = retag(existing, LIST_TAG[kind]);
199
+ switched.style.setProperty(
200
+ "list-style-type",
201
+ kind === "ordered" ? "decimal" : "disc",
202
+ );
203
+ return existing === element ? switched : element;
204
+ }
205
+
206
+ if (existing === element) {
207
+ // The object itself is the list, so unwrapping means becoming a plain
208
+ // block; its attributes (object id, position, styling) must survive.
209
+ const unwrapped = retag(element, "DIV");
210
+ unwrapped.replaceChildren(buildLines(doc, lines));
211
+ unwrapped.style.removeProperty("padding-left");
212
+ unwrapped.style.removeProperty("list-style-position");
213
+ unwrapped.style.removeProperty("list-style-type");
214
+ return unwrapped;
215
+ }
216
+
217
+ existing.replaceWith(buildLines(doc, lines));
218
+ return element;
219
+ }
@@ -10,7 +10,6 @@ export interface SelectionOverlayMeasurementIdentity {
10
10
  selector: string | null;
11
11
  path: number[] | null;
12
12
  canvasZoom: number;
13
- stylePanelOpen: boolean;
14
13
  revision: number;
15
14
  }
16
15
 
@@ -25,7 +24,6 @@ export function createSelectionOverlayMeasurementKey({
25
24
  selector,
26
25
  path,
27
26
  canvasZoom,
28
- stylePanelOpen,
29
27
  revision,
30
28
  }: SelectionOverlayMeasurementIdentity): string {
31
29
  return JSON.stringify([
@@ -35,7 +33,6 @@ export function createSelectionOverlayMeasurementKey({
35
33
  selector,
36
34
  path,
37
35
  canvasZoom,
38
- stylePanelOpen,
39
36
  revision,
40
37
  ]);
41
38
  }