@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.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/app/components/AgentCompletionSound.tsx +105 -0
- package/corpus/templates/analytics/app/components/layout/Layout.tsx +44 -35
- package/corpus/templates/analytics/app/i18n-data.ts +38 -0
- package/corpus/templates/analytics/app/pages/Settings.tsx +53 -1
- package/corpus/templates/analytics/app/pages/settings/settings-search.ts +6 -0
- package/corpus/templates/analytics/changelog/2026-08-04-choose-whether-analytics-plays-a-bell-when-the-agent-finishe.md +6 -0
- package/corpus/templates/analytics/shared/analytics-user-prefs.ts +2 -0
- package/corpus/templates/slides/app/components/editor/AddSlidePopover.tsx +258 -0
- package/corpus/templates/slides/app/components/editor/EditorActionCluster.tsx +130 -0
- package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +25 -337
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +73 -53
- package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +321 -0
- package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +32 -11
- package/corpus/templates/slides/app/components/editor/SlideContextToolbar.tsx +804 -0
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +101 -45
- package/corpus/templates/slides/app/components/editor/SlideOverflowWarning.tsx +7 -4
- package/corpus/templates/slides/app/components/editor/bullet-editing.ts +11 -3
- package/corpus/templates/slides/app/components/editor/commit-active-edit.ts +21 -0
- package/corpus/templates/slides/app/components/editor/list-editing.ts +219 -0
- package/corpus/templates/slides/app/components/editor/selection-overlay-measurement.ts +0 -3
- package/corpus/templates/slides/app/components/editor/slide-style.ts +199 -0
- package/corpus/templates/slides/app/global.css +14 -3
- package/corpus/templates/slides/app/i18n/en-US.ts +8 -1
- package/corpus/templates/slides/app/lib/recent-references.ts +82 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +51 -22
- package/corpus/templates/slides/app/pages/Index.tsx +236 -181
- package/corpus/templates/slides/changelog/2026-08-01-add-slide-moved-to-the-toolbar-and-the-slide-rail-is-now-mor.md +9 -0
- package/corpus/templates/slides/changelog/2026-08-01-add-slide-undo-redo-and-the-text-tool-now-lead-the-slide-too.md +9 -0
- package/corpus/templates/slides/changelog/2026-08-01-slide-styling-now-appears-in-a-contextual-toolbar-above-the-.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-01-the-slide-style-side-panel-is-retired-all-styling-now-lives-.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-03-the-slide-toolbar-is-denser-swatch-only-colors-dropdowns-for.md +10 -0
- package/corpus/templates/slides/changelog/2026-08-04-bullet-and-numbered-list-buttons-in-the-slide-toolbar-conver.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-04-italic-and-underline-are-now-one-click-away-in-the-slide-too.md +6 -0
- package/dist/client/DefaultSpinner.d.ts +0 -12
- package/dist/client/DefaultSpinner.js +22 -2
- package/dist/client/onboarding/use-onboarding.js +3 -0
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/deploy/build.js +2 -1
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/progress/routes.d.ts +1 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/templates/workspace-core/.agents/skills/address-feedback/SKILL.md +25 -9
- package/package.json +2 -2
- package/src/templates/workspace-core/.agents/skills/address-feedback/SKILL.md +25 -9
- 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
|
|
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-
|
|
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
|
|
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="
|
|
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-
|
|
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
|
|
204
|
-
* container
|
|
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
|
}
|