@agent-native/core 0.136.5 → 0.137.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/corpus/README.md +1 -1
- package/corpus/templates/clips/app/components/import-menu.tsx +109 -0
- package/corpus/templates/clips/app/components/library/library-grid.tsx +8 -18
- package/corpus/templates/clips/app/components/library/library-layout.tsx +14 -50
- package/corpus/templates/clips/app/components/library/sort-menu.tsx +20 -6
- package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +10 -34
- package/corpus/templates/clips/app/routes/_app.library._index.tsx +0 -2
- package/corpus/templates/clips/changelog/2026-08-04-cleaner-library-controls-with-an-icon-only-sort-button.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-04-upload-and-loom-imports-now-live-under-one-import-menu.md +6 -0
- package/corpus/templates/content/actions/_database-utils.ts +6 -0
- package/corpus/templates/content/actions/bind-content-database-source-field.ts +273 -97
- package/corpus/templates/content/actions/change-content-database-source-role.ts +65 -29
- package/corpus/templates/content/actions/configure-document-property.ts +113 -32
- package/corpus/templates/content/actions/delete-content-database.ts +5 -7
- package/corpus/templates/content/actions/delete-document-property.ts +109 -62
- package/corpus/templates/content/actions/delete-document.ts +178 -36
- package/corpus/templates/content/actions/duplicate-database-item.ts +27 -0
- package/corpus/templates/content/actions/duplicate-database-items.ts +31 -1
- package/corpus/templates/content/actions/permanently-delete-document.ts +4 -6
- package/corpus/templates/content/actions/remove-database-items.ts +13 -0
- package/corpus/templates/content/actions/set-document-property.ts +69 -1
- package/corpus/templates/content/actions/upsert-database-item-by-key.ts +814 -0
- package/corpus/templates/content/parity/matrix.md +1 -1
- package/corpus/templates/content/parity/matrix.ts +1 -0
- package/corpus/templates/content/server/db/schema.ts +27 -0
- package/corpus/templates/content/server/plugins/db.ts +24 -0
- package/corpus/templates/design/app/components/design/BreakpointBar.tsx +122 -114
- package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +2 -2
- package/corpus/templates/design/app/components/design/EditPanel.tsx +68 -34
- package/corpus/templates/design/app/components/design/KeyboardShortcutsPanel.tsx +78 -4
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +8 -0
- package/corpus/templates/design/app/components/design/QuestionFlow.tsx +5 -2
- package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +10 -9
- package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +31 -69
- package/corpus/templates/design/app/components/design/inspector/design-icons.tsx +80 -106
- package/corpus/templates/design/app/components/design/multi-screen/types.ts +4 -0
- package/corpus/templates/design/app/hooks/use-editor-preferences.ts +75 -0
- package/corpus/templates/design/app/i18n-data.ts +8 -0
- package/corpus/templates/design/app/i18n-keyboard-shortcuts.ts +61 -0
- package/corpus/templates/design/app/pages/design-editor/editor-preferences.ts +98 -0
- package/corpus/templates/design/app/pages/design-editor/nudge-intent.ts +491 -0
- package/corpus/templates/design/app/pages/design-editor/paste-placement.ts +133 -0
- package/corpus/templates/design/app/pages/design-editor/png-export-render.ts +8 -0
- package/corpus/templates/design/app/pages/design-editor/selection-state.ts +5 -12
- package/corpus/templates/design/changelog/2026-08-03-arrow-keys-now-reorder-a-layer-inside-an-auto-layout-frame.md +6 -0
- package/corpus/templates/design/changelog/2026-08-03-auto-layout-flow-icons-show-the-direction-at-a-glance-instea.md +6 -0
- package/corpus/templates/design/changelog/2026-08-03-auto-layout-icons-share-one-outlined-square-style-so-flow-ga.md +6 -0
- package/corpus/templates/design/changelog/2026-08-03-export-has-a-named-preview-section-you-can-expand-replacing-.md +6 -0
- package/corpus/templates/design/changelog/2026-08-03-export-preview-shows-the-selected-element-instead-of-the-who.md +6 -0
- package/corpus/templates/design/changelog/2026-08-03-inspector-sections-show-an-expand-indicator-so-it-is-clear-w.md +6 -0
- package/corpus/templates/design/changelog/2026-08-03-paste-lands-inside-a-selected-frame-instead-of-beside-it.md +6 -0
- package/corpus/templates/design/changelog/2026-08-03-the-add-breakpoint-button-sits-outside-the-breakpoint-select.md +6 -0
- package/corpus/templates/design/changelog/2026-08-03-the-agent-s-follow-up-questions-keep-space-below-the-continu.md +6 -0
- package/corpus/templates/design/changelog/2026-08-03-the-agent-s-follow-up-questions-share-the-chat-panel-s-backg.md +6 -0
- package/corpus/templates/design/changelog/2026-08-03-the-export-panel-no-longer-stacks-a-duplicate-export-block-o.md +6 -0
- package/corpus/templates/design/changelog/2026-08-03-the-text-tool-uses-a-t-icon-matching-the-letter-it-inserts.md +6 -0
- package/corpus/templates/factory/.env.example +17 -0
- package/corpus/templates/factory/AGENTS.md +27 -17
- package/corpus/templates/factory/README.md +19 -9
- package/corpus/templates/factory/actions/get-slack-feedback-context.ts +90 -0
- package/corpus/templates/factory/actions/get-triage-config.ts +14 -3
- package/corpus/templates/factory/actions/govern-agent-native-pull-request.ts +386 -0
- package/corpus/templates/factory/actions/list-factory-automations.ts +53 -0
- package/corpus/templates/factory/actions/list-triage-items.ts +7 -2
- package/corpus/templates/factory/actions/poll-github-sources.ts +203 -0
- package/corpus/templates/factory/actions/poll-sentry-errors.ts +161 -0
- package/corpus/templates/factory/actions/poll-slack-channel.ts +4 -2
- package/corpus/templates/factory/actions/run-factory-automation.ts +42 -0
- package/corpus/templates/factory/actions/save-factory-automation.ts +69 -0
- package/corpus/templates/factory/actions/save-triage-config.ts +28 -2
- package/corpus/templates/factory/actions/start-builder-for-item.ts +411 -0
- package/corpus/templates/factory/app/components/factory/FactoryCanvas.tsx +14 -3
- package/corpus/templates/factory/app/components/ui/checkbox.tsx +1 -0
- package/corpus/templates/factory/app/hooks/use-navigation-state.ts +7 -0
- package/corpus/templates/factory/app/i18n/en-US.ts +35 -1
- package/corpus/templates/factory/app/routes/factory.tsx +462 -9
- package/corpus/templates/factory/changelog/2026-08-04-added-organization-owned-factory-automations-for-slack-bug-t.md +6 -0
- package/corpus/templates/factory/changelog/2026-08-04-automation-editor-controls-and-model-picker.md +6 -0
- package/corpus/templates/factory/netlify.toml +2 -2
- package/corpus/templates/factory/server/connectors/credentials.ts +6 -5
- package/corpus/templates/factory/server/connectors/slack.ts +133 -5
- package/corpus/templates/factory/server/db/schema.ts +6 -0
- package/corpus/templates/factory/server/lib/require-factory-automation.ts +65 -0
- package/corpus/templates/factory/server/plugins/agent-chat.ts +29 -11
- package/corpus/templates/factory/server/plugins/auth.ts +1 -0
- package/corpus/templates/factory/server/plugins/factory-migrations.ts +12 -0
- package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +218 -67
- package/corpus/templates/factory/server/triage/builder-executor.ts +4 -0
- package/corpus/templates/factory/server/triage/contracts.ts +14 -1
- package/corpus/templates/factory/server/triage/github-client.ts +404 -0
- package/corpus/templates/factory/server/triage/metadata.ts +44 -0
- package/corpus/templates/factory/server/triage/pr-babysit.ts +5 -5
- package/corpus/templates/factory/server/triage/pr-policy.ts +144 -0
- package/corpus/templates/factory/server/triage/sentry-client.ts +180 -0
- package/corpus/templates/factory/server/triage/slack-client.ts +43 -0
- package/corpus/templates/factory/server/triage/slack-poller.ts +9 -2
- package/corpus/templates/plan/.agents/skills/visual-plan/SKILL.md +27 -0
- package/corpus/templates/plan/.agents/skills/visual-plan/references/canvas.md +14 -4
- package/corpus/templates/plan/.agents/skills/visual-plan/references/wireframe.md +7 -0
- package/corpus/templates/plan/.agents/skills/visual-recap/references/wireframe.md +7 -0
- package/corpus/templates/plan/actions/create-plan-design.ts +8 -6
- package/corpus/templates/plan/actions/create-prototype-plan.ts +8 -6
- package/corpus/templates/plan/actions/create-ui-plan.ts +7 -7
- package/corpus/templates/plan/actions/get-plan-blocks.ts +7 -1
- package/corpus/templates/plan/actions/validate-plan-input.ts +39 -0
- package/corpus/templates/plan/app/components/plan/CanvasArea.tsx +167 -11
- package/corpus/templates/plan/app/i18n/en-US.ts +1 -0
- package/corpus/templates/plan/changelog/2026-08-04-canvas-zoom-controls-now-explain-the-command-ctrl-plus-scrol.md +6 -0
- package/corpus/templates/plan/changelog/2026-08-04-pinch-to-zoom-is-now-supported-on-touchscreens-in-visual-pla.md +6 -0
- package/corpus/templates/plan/shared/plan-content.ts +17 -2
- package/dist/cli/skills-content/canvas.d.ts +3 -3
- package/dist/cli/skills-content/canvas.js +16 -5
- package/dist/cli/skills-content/visual-plan-skill.d.ts +1 -1
- package/dist/cli/skills-content/visual-plan-skill.js +27 -0
- package/dist/cli/skills-content/wireframe.d.ts +2 -2
- package/dist/cli/skills-content/wireframe.js +7 -0
- package/dist/client/AgentPanel.js +23 -8
- package/dist/client/api-path.d.ts +6 -0
- package/dist/client/api-path.js +55 -0
- package/dist/client/onboarding/FirstRunOnboarding.d.ts +3 -0
- package/dist/client/onboarding/FirstRunOnboarding.js +93 -0
- package/dist/client/onboarding/first-run-enabled.d.ts +2 -0
- package/dist/client/onboarding/first-run-enabled.js +9 -0
- package/dist/client/onboarding/index.d.ts +2 -1
- package/dist/client/onboarding/index.js +1 -0
- package/dist/client/onboarding/use-onboarding.d.ts +6 -1
- package/dist/client/onboarding/use-onboarding.js +32 -1
- package/dist/client/route-state.js +10 -0
- package/dist/connections/catalog.d.ts +4 -4
- package/dist/connections/catalog.js +3 -3
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.js +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/notifications/routes.d.ts +5 -5
- package/dist/observability/routes.d.ts +3 -3
- package/dist/onboarding/app-profile.d.ts +4 -0
- package/dist/onboarding/app-profile.js +378 -0
- package/dist/onboarding/index.d.ts +2 -1
- package/dist/onboarding/index.js +1 -0
- package/dist/onboarding/plugin.d.ts +2 -0
- package/dist/onboarding/plugin.js +21 -1
- package/dist/onboarding/types.d.ts +19 -0
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +9 -9
- package/dist/provider-api/actions/provider-api.d.ts +12 -12
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/auth.js +21 -1
- package/dist/server/better-auth-instance.d.ts +3 -2
- package/dist/server/better-auth-instance.js +9 -7
- package/dist/server/email-markdown.js +3 -8
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/shared/first-run-onboarding.d.ts +3 -0
- package/dist/shared/first-run-onboarding.js +3 -0
- package/dist/triggers/index.d.ts +3 -0
- package/dist/triggers/index.js +5 -0
- package/docs/content/cross-app-sso.mdx +26 -0
- package/package.json +1 -1
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
import { buildCodeLayerProjection } from "@shared/code-layer";
|
|
2
|
+
|
|
3
|
+
import type { ElementInfo } from "@/components/design/types";
|
|
4
|
+
|
|
5
|
+
import { resolveCodeLayerNodeFromElementInfo } from "./code-layer-state";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Figma parity — an arrow key reorders a flow child and translates everything
|
|
9
|
+
* else. Writing left/top on a flow child is the wrong operation: under
|
|
10
|
+
* `position: static` it does nothing, and under `relative` it detaches the
|
|
11
|
+
* element from its neighbours instead of moving through them.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type NudgeDirection = "up" | "right" | "down" | "left";
|
|
15
|
+
|
|
16
|
+
export interface NudgeAmounts {
|
|
17
|
+
small: number;
|
|
18
|
+
big: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const DEFAULT_NUDGE_AMOUNTS: NudgeAmounts = { small: 1, big: 10 };
|
|
22
|
+
|
|
23
|
+
export type FlowAxis = "horizontal" | "vertical";
|
|
24
|
+
|
|
25
|
+
export interface FlowContainerInfo {
|
|
26
|
+
kind: "flex" | "grid" | "none";
|
|
27
|
+
/** The axis DOM order advances along. */
|
|
28
|
+
axis: FlowAxis;
|
|
29
|
+
/** Visual order runs opposite to DOM order (`*-reverse`). */
|
|
30
|
+
reversed: boolean;
|
|
31
|
+
wraps: boolean;
|
|
32
|
+
/** Items per line when statically knowable (grid tracks); null otherwise. */
|
|
33
|
+
lineLength: number | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const NO_FLOW_CONTAINER: FlowContainerInfo = {
|
|
37
|
+
kind: "none",
|
|
38
|
+
axis: "horizontal",
|
|
39
|
+
reversed: false,
|
|
40
|
+
wraps: false,
|
|
41
|
+
lineLength: null,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type NudgeIntent =
|
|
45
|
+
| { kind: "translate"; dx: number; dy: number }
|
|
46
|
+
| { kind: "reorder"; fromIndex: number; toIndex: number }
|
|
47
|
+
| { kind: "none" };
|
|
48
|
+
|
|
49
|
+
export interface FlowContainerStyleSource {
|
|
50
|
+
style: Partial<Record<string, string>>;
|
|
51
|
+
classes?: readonly string[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function toPositiveInteger(value: string | undefined): number | null {
|
|
55
|
+
if (!value) return null;
|
|
56
|
+
const parsed = Number.parseInt(value, 10);
|
|
57
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Tailwind utilities only count unprefixed: `md:flex` is conditional on a
|
|
61
|
+
* breakpoint we are not resolving here, so treating it as the current layout
|
|
62
|
+
* would reorder against a container shape the user cannot see. */
|
|
63
|
+
function utilitySet(classes: readonly string[] | undefined): Set<string> {
|
|
64
|
+
const set = new Set<string>();
|
|
65
|
+
for (const raw of classes ?? []) {
|
|
66
|
+
const token = raw.trim();
|
|
67
|
+
if (!token || token.includes(":")) continue;
|
|
68
|
+
set.add(token);
|
|
69
|
+
}
|
|
70
|
+
return set;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* `grid-template-columns: repeat(4, minmax(0,1fr))` and
|
|
75
|
+
* `grid-template-columns: 1fr 1fr 1fr` both describe three-or-four column
|
|
76
|
+
* tracks; counting top-level tokens (with `repeat(n, …)` expanded to n) is
|
|
77
|
+
* enough for reorder arithmetic and never needs layout.
|
|
78
|
+
*/
|
|
79
|
+
export function countGridTracks(value: string | undefined): number | null {
|
|
80
|
+
if (!value) return null;
|
|
81
|
+
const trimmed = value.trim();
|
|
82
|
+
if (!trimmed || trimmed === "none") return null;
|
|
83
|
+
let count = 0;
|
|
84
|
+
let depth = 0;
|
|
85
|
+
let token = "";
|
|
86
|
+
let unresolved = false;
|
|
87
|
+
const flush = () => {
|
|
88
|
+
const item = token.trim();
|
|
89
|
+
token = "";
|
|
90
|
+
if (!item) return;
|
|
91
|
+
const repeat = /^repeat\(\s*([^,]+?)\s*,(.*)\)$/is.exec(item);
|
|
92
|
+
if (repeat) {
|
|
93
|
+
const times = Number.parseInt(repeat[1]!, 10);
|
|
94
|
+
// `auto-fit`/`auto-fill` resolve against the container's width, so the
|
|
95
|
+
// track count is only knowable from layout. Counting the repeat as one
|
|
96
|
+
// track would make a cross-axis arrow move one sibling, not one row.
|
|
97
|
+
if (!Number.isSafeInteger(times) || times <= 0) {
|
|
98
|
+
unresolved = true;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
count += times * (countGridTracks(repeat[2]) ?? 1);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
count += 1;
|
|
105
|
+
};
|
|
106
|
+
for (const char of trimmed) {
|
|
107
|
+
if (char === "(") depth += 1;
|
|
108
|
+
if (char === ")") depth -= 1;
|
|
109
|
+
if (depth === 0 && /\s/.test(char)) {
|
|
110
|
+
flush();
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
token += char;
|
|
114
|
+
}
|
|
115
|
+
flush();
|
|
116
|
+
if (unresolved) return null;
|
|
117
|
+
return count > 0 ? count : null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Read the flow shape of a would-be parent from its authored styles and
|
|
121
|
+
* Tailwind utilities. Accepts anything shaped like a `CodeLayerNode` or an
|
|
122
|
+
* `ElementInfo`'s computed styles. */
|
|
123
|
+
export function describeFlowContainer(
|
|
124
|
+
source: FlowContainerStyleSource | null | undefined,
|
|
125
|
+
): FlowContainerInfo {
|
|
126
|
+
if (!source) return NO_FLOW_CONTAINER;
|
|
127
|
+
const style = source.style ?? {};
|
|
128
|
+
const classes = utilitySet(source.classes);
|
|
129
|
+
|
|
130
|
+
const display =
|
|
131
|
+
style.display ??
|
|
132
|
+
(classes.has("flex")
|
|
133
|
+
? "flex"
|
|
134
|
+
: classes.has("inline-flex")
|
|
135
|
+
? "inline-flex"
|
|
136
|
+
: classes.has("grid")
|
|
137
|
+
? "grid"
|
|
138
|
+
: classes.has("inline-grid")
|
|
139
|
+
? "inline-grid"
|
|
140
|
+
: undefined);
|
|
141
|
+
|
|
142
|
+
if (display === "flex" || display === "inline-flex") {
|
|
143
|
+
const direction =
|
|
144
|
+
style["flex-direction"] ??
|
|
145
|
+
(classes.has("flex-row-reverse")
|
|
146
|
+
? "row-reverse"
|
|
147
|
+
: classes.has("flex-col-reverse")
|
|
148
|
+
? "column-reverse"
|
|
149
|
+
: classes.has("flex-col")
|
|
150
|
+
? "column"
|
|
151
|
+
: classes.has("flex-row")
|
|
152
|
+
? "row"
|
|
153
|
+
: "row");
|
|
154
|
+
const wrap =
|
|
155
|
+
style["flex-wrap"] ??
|
|
156
|
+
(classes.has("flex-wrap-reverse")
|
|
157
|
+
? "wrap-reverse"
|
|
158
|
+
: classes.has("flex-wrap")
|
|
159
|
+
? "wrap"
|
|
160
|
+
: classes.has("flex-nowrap")
|
|
161
|
+
? "nowrap"
|
|
162
|
+
: "nowrap");
|
|
163
|
+
return {
|
|
164
|
+
kind: "flex",
|
|
165
|
+
axis: direction.startsWith("column") ? "vertical" : "horizontal",
|
|
166
|
+
reversed: direction.endsWith("-reverse"),
|
|
167
|
+
wraps: wrap === "wrap" || wrap === "wrap-reverse",
|
|
168
|
+
lineLength: null,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (display === "grid" || display === "inline-grid") {
|
|
173
|
+
const autoFlow = style["grid-auto-flow"] ?? "";
|
|
174
|
+
const columnFlow =
|
|
175
|
+
autoFlow.includes("column") || classes.has("grid-flow-col");
|
|
176
|
+
const templateColumns =
|
|
177
|
+
style["grid-template-columns"] ??
|
|
178
|
+
gridTemplateFromUtilities(classes, "grid-cols-");
|
|
179
|
+
const templateRows =
|
|
180
|
+
style["grid-template-rows"] ??
|
|
181
|
+
gridTemplateFromUtilities(classes, "grid-rows-");
|
|
182
|
+
return {
|
|
183
|
+
kind: "grid",
|
|
184
|
+
axis: columnFlow ? "vertical" : "horizontal",
|
|
185
|
+
reversed: false,
|
|
186
|
+
// A grid always continues onto the next track line; there is no
|
|
187
|
+
// grid equivalent of `flex-wrap: nowrap`.
|
|
188
|
+
wraps: true,
|
|
189
|
+
lineLength: columnFlow
|
|
190
|
+
? countGridTracks(templateRows)
|
|
191
|
+
: countGridTracks(templateColumns),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return NO_FLOW_CONTAINER;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function gridTemplateFromUtilities(
|
|
199
|
+
classes: Set<string>,
|
|
200
|
+
prefix: string,
|
|
201
|
+
): string | undefined {
|
|
202
|
+
for (const token of classes) {
|
|
203
|
+
if (!token.startsWith(prefix)) continue;
|
|
204
|
+
const count = toPositiveInteger(token.slice(prefix.length));
|
|
205
|
+
if (count) return `repeat(${count}, 1fr)`;
|
|
206
|
+
}
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** A declared flex/grid `order`, or null when the child leaves it at the
|
|
211
|
+
* default. Tailwind's `order-first`/`order-last` map to the sentinels the
|
|
212
|
+
* utility generates. */
|
|
213
|
+
const GRID_PLACEMENT_PROPERTIES = [
|
|
214
|
+
"grid-row",
|
|
215
|
+
"grid-column",
|
|
216
|
+
"grid-area",
|
|
217
|
+
"grid-row-start",
|
|
218
|
+
"grid-column-start",
|
|
219
|
+
];
|
|
220
|
+
|
|
221
|
+
const GRID_PLACEMENT_UTILITY_PREFIXES = [
|
|
222
|
+
"col-span-",
|
|
223
|
+
"row-span-",
|
|
224
|
+
"col-start-",
|
|
225
|
+
"row-start-",
|
|
226
|
+
"col-end-",
|
|
227
|
+
"row-end-",
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
/** Explicit grid placement opts a child out of auto-placement, so moving it in
|
|
231
|
+
* the DOM leaves it in the same cell. */
|
|
232
|
+
export function hasExplicitGridPlacement(
|
|
233
|
+
source: FlowContainerStyleSource,
|
|
234
|
+
): boolean {
|
|
235
|
+
if (GRID_PLACEMENT_PROPERTIES.some((property) => source.style?.[property])) {
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
for (const token of utilitySet(source.classes)) {
|
|
239
|
+
if (GRID_PLACEMENT_UTILITY_PREFIXES.some((p) => token.startsWith(p))) {
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function declaredFlexOrder(
|
|
247
|
+
source: FlowContainerStyleSource,
|
|
248
|
+
): number | null {
|
|
249
|
+
const inline = source.style?.order;
|
|
250
|
+
if (inline !== undefined && inline !== "") {
|
|
251
|
+
const parsed = Number.parseInt(inline, 10);
|
|
252
|
+
return Number.isSafeInteger(parsed) ? parsed : null;
|
|
253
|
+
}
|
|
254
|
+
for (const token of utilitySet(source.classes)) {
|
|
255
|
+
if (token === "order-first") return -9999;
|
|
256
|
+
if (token === "order-last") return 9999;
|
|
257
|
+
if (token === "order-none") return 0;
|
|
258
|
+
if (!token.startsWith("order-")) continue;
|
|
259
|
+
const parsed = Number.parseInt(token.slice("order-".length), 10);
|
|
260
|
+
if (Number.isSafeInteger(parsed)) return parsed;
|
|
261
|
+
}
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** `position: absolute|fixed` takes a child out of its parent's flow — Figma
|
|
266
|
+
* calls the same escape hatch "Ignore auto layout". `sticky` and `relative`
|
|
267
|
+
* still occupy a flow slot, so they keep reordering. */
|
|
268
|
+
export function escapesFlow(
|
|
269
|
+
position: string | null | undefined,
|
|
270
|
+
classes?: readonly string[],
|
|
271
|
+
): boolean {
|
|
272
|
+
const value =
|
|
273
|
+
position ??
|
|
274
|
+
(utilitySet(classes).has("absolute")
|
|
275
|
+
? "absolute"
|
|
276
|
+
: utilitySet(classes).has("fixed")
|
|
277
|
+
? "fixed"
|
|
278
|
+
: undefined);
|
|
279
|
+
return value === "absolute" || value === "fixed";
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export interface ResolveNudgeIntentArgs {
|
|
283
|
+
direction: NudgeDirection;
|
|
284
|
+
largeStep: boolean;
|
|
285
|
+
amounts?: NudgeAmounts;
|
|
286
|
+
container?: FlowContainerInfo;
|
|
287
|
+
/** The selected node's own `position`. */
|
|
288
|
+
position?: string | null;
|
|
289
|
+
/** Its 0-based index among its siblings in DOM order. */
|
|
290
|
+
siblingIndex?: number;
|
|
291
|
+
/** How many siblings share the container, including the selection. */
|
|
292
|
+
siblingCount?: number;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function resolveNudgeIntent(args: ResolveNudgeIntentArgs): NudgeIntent {
|
|
296
|
+
const amounts = args.amounts ?? DEFAULT_NUDGE_AMOUNTS;
|
|
297
|
+
const step = args.largeStep ? amounts.big : amounts.small;
|
|
298
|
+
const forward = args.direction === "right" || args.direction === "down";
|
|
299
|
+
const arrowAxis: FlowAxis =
|
|
300
|
+
args.direction === "left" || args.direction === "right"
|
|
301
|
+
? "horizontal"
|
|
302
|
+
: "vertical";
|
|
303
|
+
|
|
304
|
+
const translate = (): NudgeIntent => ({
|
|
305
|
+
kind: "translate",
|
|
306
|
+
dx: arrowAxis === "horizontal" ? (forward ? step : -step) : 0,
|
|
307
|
+
dy: arrowAxis === "vertical" ? (forward ? step : -step) : 0,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
const container = args.container ?? NO_FLOW_CONTAINER;
|
|
311
|
+
if (container.kind === "none" || escapesFlow(args.position)) {
|
|
312
|
+
return translate();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const fromIndex = args.siblingIndex;
|
|
316
|
+
const siblingCount = args.siblingCount;
|
|
317
|
+
if (
|
|
318
|
+
fromIndex === undefined ||
|
|
319
|
+
siblingCount === undefined ||
|
|
320
|
+
fromIndex < 0 ||
|
|
321
|
+
siblingCount < 2
|
|
322
|
+
) {
|
|
323
|
+
return { kind: "none" };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
let delta: number;
|
|
327
|
+
if (arrowAxis === container.axis) {
|
|
328
|
+
delta = (forward ? 1 : -1) * (container.reversed ? -1 : 1);
|
|
329
|
+
} else {
|
|
330
|
+
const lineLength = container.lineLength;
|
|
331
|
+
if (!container.wraps || !lineLength || lineLength < 1) {
|
|
332
|
+
return { kind: "none" };
|
|
333
|
+
}
|
|
334
|
+
delta = (forward ? 1 : -1) * lineLength;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const toIndex = Math.min(Math.max(fromIndex + delta, 0), siblingCount - 1);
|
|
338
|
+
if (toIndex === fromIndex) return { kind: "none" };
|
|
339
|
+
return { kind: "reorder", fromIndex, toIndex };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export interface ReorderAnchor {
|
|
343
|
+
anchorIndex: number;
|
|
344
|
+
placement: "before" | "after";
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** Translate a `reorder` intent into the anchor sibling + placement the
|
|
348
|
+
* `moveNode` visual edit takes. Moving later must anchor AFTER the sibling
|
|
349
|
+
* currently at the destination index, and moving earlier BEFORE it — the
|
|
350
|
+
* moved node vacates its own slot, so anchoring on the wrong side lands it
|
|
351
|
+
* one position short. */
|
|
352
|
+
export function reorderAnchorFor(intent: {
|
|
353
|
+
fromIndex: number;
|
|
354
|
+
toIndex: number;
|
|
355
|
+
}): ReorderAnchor {
|
|
356
|
+
return {
|
|
357
|
+
anchorIndex: intent.toIndex,
|
|
358
|
+
placement: intent.toIndex > intent.fromIndex ? "after" : "before",
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export type ElementNudgeIntent =
|
|
363
|
+
| { kind: "translate"; dx: number; dy: number }
|
|
364
|
+
| {
|
|
365
|
+
kind: "reorder";
|
|
366
|
+
/** The snapshot the node ids below were resolved against — the caller
|
|
367
|
+
* must apply its `moveNode` edit to THIS string, not re-read the file,
|
|
368
|
+
* or the ids can address a document that has since changed. */
|
|
369
|
+
content: string;
|
|
370
|
+
targetNodeId: string;
|
|
371
|
+
anchorNodeId: string;
|
|
372
|
+
placement: "before" | "after";
|
|
373
|
+
}
|
|
374
|
+
| { kind: "none" };
|
|
375
|
+
|
|
376
|
+
/** Rendered `display` from the canvas bridge (`getComputedStyle`), which sees
|
|
377
|
+
* stylesheet rules and the active breakpoint that authored-style parsing
|
|
378
|
+
* cannot. */
|
|
379
|
+
function isRenderedFlowDisplay(display: string | null | undefined): boolean {
|
|
380
|
+
return (
|
|
381
|
+
display === "flex" ||
|
|
382
|
+
display === "inline-flex" ||
|
|
383
|
+
display === "grid" ||
|
|
384
|
+
display === "inline-grid"
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export interface ResolveElementNudgeIntentArgs {
|
|
389
|
+
content: string;
|
|
390
|
+
selectedElement: ElementInfo;
|
|
391
|
+
direction: NudgeDirection;
|
|
392
|
+
largeStep: boolean;
|
|
393
|
+
amounts?: NudgeAmounts;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** Resolve an arrow key against a selected element's real position in its
|
|
397
|
+
* source document. Falls back to a plain translate whenever the document or
|
|
398
|
+
* the node cannot be resolved (live/localhost screens have no authored source
|
|
399
|
+
* here), so an unreadable projection never silently swallows the keypress. */
|
|
400
|
+
export function resolveElementNudgeIntent(
|
|
401
|
+
args: ResolveElementNudgeIntentArgs,
|
|
402
|
+
): ElementNudgeIntent {
|
|
403
|
+
const translate = resolveNudgeIntent({
|
|
404
|
+
direction: args.direction,
|
|
405
|
+
largeStep: args.largeStep,
|
|
406
|
+
amounts: args.amounts,
|
|
407
|
+
}) as { kind: "translate"; dx: number; dy: number };
|
|
408
|
+
|
|
409
|
+
if (!args.content) return translate;
|
|
410
|
+
const projection = buildCodeLayerProjection(args.content);
|
|
411
|
+
const node = resolveCodeLayerNodeFromElementInfo(
|
|
412
|
+
projection,
|
|
413
|
+
args.selectedElement,
|
|
414
|
+
);
|
|
415
|
+
if (!node) return translate;
|
|
416
|
+
|
|
417
|
+
const parent = node.parentId
|
|
418
|
+
? (projection.nodes.find((candidate) => candidate.id === node.parentId) ??
|
|
419
|
+
null)
|
|
420
|
+
: null;
|
|
421
|
+
const siblingIds = parent?.children ?? projection.rootNodeIds;
|
|
422
|
+
const siblingIndex = siblingIds.indexOf(node.id);
|
|
423
|
+
if (siblingIndex < 0) return translate;
|
|
424
|
+
|
|
425
|
+
const position =
|
|
426
|
+
node.style.position ??
|
|
427
|
+
(escapesFlow(undefined, node.classes) ? "absolute" : undefined) ??
|
|
428
|
+
args.selectedElement.computedStyles?.position;
|
|
429
|
+
|
|
430
|
+
const container = describeFlowContainer(parent);
|
|
431
|
+
// Flex/grid paint children by `order` and explicit grid placement, not DOM
|
|
432
|
+
// position, so moving the node would write a source change that produces no
|
|
433
|
+
// visible movement.
|
|
434
|
+
if (
|
|
435
|
+
container.kind !== "none" &&
|
|
436
|
+
siblingIds.some((id) => {
|
|
437
|
+
const sibling = projection.nodes.find((candidate) => candidate.id === id);
|
|
438
|
+
if (!sibling) return false;
|
|
439
|
+
const order = declaredFlexOrder(sibling);
|
|
440
|
+
if (order !== null && order !== 0) return true;
|
|
441
|
+
return container.kind === "grid" && hasExplicitGridPlacement(sibling);
|
|
442
|
+
})
|
|
443
|
+
) {
|
|
444
|
+
return { kind: "none" };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// A `.row { display: flex }` parent parses as no container. Translating
|
|
448
|
+
// would write left/top onto a flex child and detach it from its
|
|
449
|
+
// neighbours, so do nothing rather than corrupt the layout.
|
|
450
|
+
if (
|
|
451
|
+
container.kind === "none" &&
|
|
452
|
+
!escapesFlow(position) &&
|
|
453
|
+
isRenderedFlowDisplay(args.selectedElement.parentDisplay)
|
|
454
|
+
) {
|
|
455
|
+
return { kind: "none" };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Rendered `order` from the bridge sees stylesheet rules that the authored
|
|
459
|
+
// styles above cannot.
|
|
460
|
+
const renderedOrder = args.selectedElement.computedStyles?.order;
|
|
461
|
+
if (
|
|
462
|
+
container.kind !== "none" &&
|
|
463
|
+
renderedOrder !== undefined &&
|
|
464
|
+
renderedOrder !== "" &&
|
|
465
|
+
renderedOrder !== "0"
|
|
466
|
+
) {
|
|
467
|
+
return { kind: "none" };
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const intent = resolveNudgeIntent({
|
|
471
|
+
direction: args.direction,
|
|
472
|
+
largeStep: args.largeStep,
|
|
473
|
+
amounts: args.amounts,
|
|
474
|
+
container,
|
|
475
|
+
position,
|
|
476
|
+
siblingIndex,
|
|
477
|
+
siblingCount: siblingIds.length,
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
if (intent.kind !== "reorder") return intent;
|
|
481
|
+
const anchor = reorderAnchorFor(intent);
|
|
482
|
+
const anchorNodeId = siblingIds[anchor.anchorIndex];
|
|
483
|
+
if (!anchorNodeId) return { kind: "none" };
|
|
484
|
+
return {
|
|
485
|
+
kind: "reorder",
|
|
486
|
+
content: args.content,
|
|
487
|
+
targetNodeId: node.id,
|
|
488
|
+
anchorNodeId,
|
|
489
|
+
placement: anchor.placement,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildCodeLayerProjection,
|
|
3
|
+
type CodeLayerNode,
|
|
4
|
+
} from "@shared/code-layer";
|
|
5
|
+
|
|
6
|
+
import type { ElementInfo } from "@/components/design/types";
|
|
7
|
+
|
|
8
|
+
import { resolveCodeLayerNodeFromElementInfo } from "./code-layer-state";
|
|
9
|
+
import { describeFlowContainer, type FlowContainerInfo } from "./nudge-intent";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Figma parity — paste goes INSIDE a selected frame and AFTER a selected
|
|
13
|
+
* object. Treating every selection as an object is the difference between
|
|
14
|
+
* "paste into this card" and "paste a second card beside it".
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Elements that render their own content and can never host a pasted layer. */
|
|
18
|
+
const REPLACED_TAGS = new Set([
|
|
19
|
+
"area",
|
|
20
|
+
"audio",
|
|
21
|
+
"br",
|
|
22
|
+
"canvas",
|
|
23
|
+
"circle",
|
|
24
|
+
"embed",
|
|
25
|
+
"hr",
|
|
26
|
+
"iframe",
|
|
27
|
+
"img",
|
|
28
|
+
"input",
|
|
29
|
+
"object",
|
|
30
|
+
"path",
|
|
31
|
+
"polygon",
|
|
32
|
+
"rect",
|
|
33
|
+
"select",
|
|
34
|
+
"source",
|
|
35
|
+
"svg",
|
|
36
|
+
"textarea",
|
|
37
|
+
"track",
|
|
38
|
+
"video",
|
|
39
|
+
"wbr",
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
/** Elements a designer reads as a text object rather than a frame, even when
|
|
43
|
+
* markup nests inline children inside them. */
|
|
44
|
+
const TEXT_LEAF_TAGS = new Set([
|
|
45
|
+
"a",
|
|
46
|
+
"b",
|
|
47
|
+
"blockquote",
|
|
48
|
+
"button",
|
|
49
|
+
"caption",
|
|
50
|
+
"code",
|
|
51
|
+
"em",
|
|
52
|
+
"figcaption",
|
|
53
|
+
"h1",
|
|
54
|
+
"h2",
|
|
55
|
+
"h3",
|
|
56
|
+
"h4",
|
|
57
|
+
"h5",
|
|
58
|
+
"h6",
|
|
59
|
+
"i",
|
|
60
|
+
"label",
|
|
61
|
+
"legend",
|
|
62
|
+
"li",
|
|
63
|
+
"option",
|
|
64
|
+
"p",
|
|
65
|
+
"pre",
|
|
66
|
+
"small",
|
|
67
|
+
"span",
|
|
68
|
+
"strong",
|
|
69
|
+
"summary",
|
|
70
|
+
"td",
|
|
71
|
+
"th",
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
export interface PasteTargetNode {
|
|
75
|
+
tag: string;
|
|
76
|
+
hasElementChildren: boolean;
|
|
77
|
+
hasText: boolean;
|
|
78
|
+
container: FlowContainerInfo;
|
|
79
|
+
primitiveKind?: string | null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function isPasteContainer(node: PasteTargetNode): boolean {
|
|
83
|
+
const tag = node.tag.toLowerCase();
|
|
84
|
+
if (REPLACED_TAGS.has(tag)) return false;
|
|
85
|
+
if (TEXT_LEAF_TAGS.has(tag)) return false;
|
|
86
|
+
if (node.primitiveKind && node.primitiveKind !== "frame") return false;
|
|
87
|
+
if (node.container.kind !== "none") return true;
|
|
88
|
+
if (node.hasElementChildren) return true;
|
|
89
|
+
return !node.hasText;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export type PastePlacement = "inside" | "after";
|
|
93
|
+
|
|
94
|
+
export interface PastePlacementDecision {
|
|
95
|
+
placement: PastePlacement;
|
|
96
|
+
targetNodeId: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function resolvePastePlacement(
|
|
100
|
+
node: PasteTargetNode & { targetNodeId: string },
|
|
101
|
+
): PastePlacementDecision {
|
|
102
|
+
return {
|
|
103
|
+
placement: isPasteContainer(node) ? "inside" : "after",
|
|
104
|
+
targetNodeId: node.targetNodeId,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function pasteTargetFromCodeLayerNode(
|
|
109
|
+
node: CodeLayerNode,
|
|
110
|
+
): PasteTargetNode & { targetNodeId: string } {
|
|
111
|
+
return {
|
|
112
|
+
tag: node.tag,
|
|
113
|
+
hasElementChildren: node.children.length > 0,
|
|
114
|
+
hasText: Boolean(node.textSnippet && node.textSnippet.trim()),
|
|
115
|
+
container: describeFlowContainer(node),
|
|
116
|
+
primitiveKind: node.dataAttributes["data-an-primitive"] ?? null,
|
|
117
|
+
targetNodeId: node.id,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function resolvePastePlacementForSelection(args: {
|
|
122
|
+
content: string;
|
|
123
|
+
selectedElement: ElementInfo | null | undefined;
|
|
124
|
+
}): PastePlacementDecision | null {
|
|
125
|
+
if (!args.content || !args.selectedElement) return null;
|
|
126
|
+
const projection = buildCodeLayerProjection(args.content);
|
|
127
|
+
const node = resolveCodeLayerNodeFromElementInfo(
|
|
128
|
+
projection,
|
|
129
|
+
args.selectedElement,
|
|
130
|
+
);
|
|
131
|
+
if (!node) return null;
|
|
132
|
+
return resolvePastePlacement(pasteTargetFromCodeLayerNode(node));
|
|
133
|
+
}
|
|
@@ -331,6 +331,14 @@ export async function renderExportDocumentCanvas({
|
|
|
331
331
|
}
|
|
332
332
|
}
|
|
333
333
|
|
|
334
|
+
/**
|
|
335
|
+
* What a raster capture is of. Overview mode resolves a different iframe and a
|
|
336
|
+
* different crop per scope, so "the current selection" is not one thing: Copy
|
|
337
|
+
* as PNG wants the selected screen, the export preview wants the selected
|
|
338
|
+
* element inside it.
|
|
339
|
+
*/
|
|
340
|
+
export type PngCaptureScope = "document" | "screens" | "element";
|
|
341
|
+
|
|
334
342
|
export type PngCaptureErrorCode =
|
|
335
343
|
| "no-preview"
|
|
336
344
|
| "external-preview"
|
|
@@ -149,19 +149,12 @@ export function isScreenRootElementInfo(info: ElementInfo | null | undefined) {
|
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
/**
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
* "delete these screens" even when the real selection is one node. The more
|
|
157
|
-
* specific target wins — the same precedence shouldSuppressFrameSelectionBox
|
|
158
|
-
* already applies to the selection chrome.
|
|
159
|
-
*
|
|
160
|
-
* Layer ids are filtered the way getSelectedLayerSnapshots filters its
|
|
161
|
-
* candidates: a screen's own file-id row and the `__`-prefixed pseudo rows are
|
|
162
|
-
* screen/frame selections, not element selections.
|
|
152
|
+
* MultiScreenCanvas keeps the owning screen in `selectedIds` while an element
|
|
153
|
+
* inside it is selected, so every overview command reading that array (Delete,
|
|
154
|
+
* arrow-key nudge) sees "the screens" when the real target is one node. The
|
|
155
|
+
* more specific target wins. `__`-prefixed and file-id rows are frames.
|
|
163
156
|
*/
|
|
164
|
-
export function
|
|
157
|
+
export function overviewSelectionTargetsElement(args: {
|
|
165
158
|
selectedElement: ElementInfo | null | undefined;
|
|
166
159
|
selectedLayerIds: readonly string[];
|
|
167
160
|
fileIds: readonly string[];
|