@agent-native/core 0.136.4 → 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/.agents/skills/recording/SKILL.md +53 -0
- package/corpus/templates/clips/actions/import-loom-recording.ts +7 -0
- package/corpus/templates/clips/actions/lib/loom-import-job.ts +24 -4
- 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-03-restart-during-a-recording-now-immediately-starts-a-fresh-ta.md +6 -0
- 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/clips/desktop/src/app.tsx +81 -30
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +436 -158
- package/corpus/templates/clips/server/lib/post-finalize-dispatch.ts +14 -1
- package/corpus/templates/clips/server/plugins/auth.ts +9 -0
- package/corpus/templates/clips/server/routes/api/_agent-native-background/post-finalize-worker.post.ts +13 -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/client/settings/SecretsSection.js +32 -8
- package/dist/collab/struct-routes.d.ts +1 -1
- 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/localization/default-messages.d.ts +5 -0
- package/dist/localization/default-messages.js +5 -0
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- 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/secrets/routes.d.ts +2 -2
- package/dist/secrets/routes.js +41 -17
- 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
|
@@ -9,7 +9,7 @@ This generated matrix tracks whether high-value Content UI operations use the sa
|
|
|
9
9
|
| database.lifecycle-and-trash | database | Create, soft-delete, restore, list, and inspect content databases | action-backed | `create-content-database`, `create-inline-content-database`, `delete-content-database`, `get-content-database`, `list-content-databases`, `list-trashed-content-databases`, `restore-content-database` | `app/components/editor/SlashCommandMenu.tsx`, `app/hooks/use-content-database.ts`, `app/hooks/use-documents.ts` | Database pages and database records are created, read, soft-deleted, restored, and listed. | - | - | P0 | covered | `actions/content-database-lifecycle.db.test.ts` | `database-source-scope` | - |
|
|
10
10
|
| database.private-preview-drafts | database | Persist and reconcile a user's private database-page preview draft | action-backed | `get-preview-document-draft`, `update-preview-document-draft` | `app/components/editor/database/DatabaseView.tsx`, `app/hooks/use-documents.ts` | A user's private preview draft is read, saved, conflict-checked, or deleted without changing the shared database page until the normal save flow applies it. | These per-user editor-state actions are intentionally hidden from agent tools because preview drafts are a private UI recovery mechanism. | - | P1 | covered | `actions/preview-document-draft.db.test.ts` | - | - |
|
|
11
11
|
| database.properties-and-view-config | database | Configure properties, values, ordering, and saved views | action-backed | `configure-document-property`, `delete-document-property`, `duplicate-document-property`, `get-content-database-personal-view`, `list-document-properties`, `reorder-document-property`, `set-document-property`, `update-content-database-personal-view`, `update-content-database-view` | `app/components/editor/DocumentProperties.tsx`, `app/components/editor/DocumentDatabase.tsx`, `app/components/editor/database/DatabaseView.tsx`, `app/hooks/use-document-properties.ts` | Property schemas, property values, property order, filters, sorts, grouping, hidden columns, view type, and view settings are stored. | - | - | P0 | covered | `actions/bind-content-database-source-field.db.test.ts`, `actions/content-database-source-actions.test.ts`, `actions/resync-content-database-source.db.test.ts` | `database-source-scope` | - |
|
|
12
|
-
| database.rows | database | Add, duplicate, move, open, edit, and remove database rows | action-backed | `add-database-item`, `remove-database-items`, `duplicate-database-items`, `duplicate-database-item`, `move-database-item`, `set-document-property`
|
|
12
|
+
| database.rows | database | Add, duplicate, move, open, edit, and remove database rows | action-backed | `add-database-item`, `upsert-database-item-by-key`, `remove-database-items`, `duplicate-database-items`, `duplicate-database-item`, `move-database-item`, `set-document-property` | `app/components/editor/DocumentDatabase.tsx`, `app/components/editor/database/DatabaseView.tsx` | Database row memberships and ordering are created, duplicated, moved, edited, and removed without deleting the backing page. | - | - | P0 | covered | `actions/database-row-batch-actions.db.test.ts`, `parity/__tests__/database-row-batch-reliability.test.ts` | `database-bulk-row-reliability` | - |
|
|
13
13
|
| database.table-query-page | database | Query one constrained page while retaining database metadata | action-backed | `query-content-database-items` | `app/components/editor/database/DatabaseView.tsx`, `app/hooks/use-content-database.ts` | - | This UI-only bounded projection is intentionally hidden with agentTool: false; agents use get-content-database for the complete database contract. | - | P0 | covered | `actions/content-database-lifecycle.db.test.ts`, `app/hooks/use-content-database.test.ts` | - | - |
|
|
14
14
|
| editor.agent-assist-prompts | editor | Ask AI from slash generation or comment context | client-assist | - | `app/components/editor/SlashCommandMenu.tsx`, `app/components/editor/CommentsSidebar.tsx` | No direct durable mutation; the prompt asks the agent to use document actions when it decides to write. | - | - | P1 | none | - | - | - |
|
|
15
15
|
| editor.client-formatting-and-insertions | editor | Rich text formatting, selection state, slash block insertion, and copy actions | client-only-ephemeral | - | `app/components/editor/BubbleToolbar.tsx`, `app/components/editor/SlashCommandMenu.tsx`, `app/components/editor/DocumentToolbar.tsx` | - | - | - | P1 | none | - | - | - |
|
|
@@ -287,6 +287,33 @@ export const contentDatabaseItems = table(
|
|
|
287
287
|
],
|
|
288
288
|
);
|
|
289
289
|
|
|
290
|
+
// Opt-in stable-key claims are the durable concurrency fence for the generic
|
|
291
|
+
// database-row upsert action. They intentionally do not constrain ordinary
|
|
292
|
+
// property editing or change add-database-item behavior.
|
|
293
|
+
export const contentDatabaseItemKeyClaims = table(
|
|
294
|
+
"content_database_item_key_claims",
|
|
295
|
+
{
|
|
296
|
+
id: text("id").primaryKey(),
|
|
297
|
+
ownerEmail: text("owner_email").notNull().default("local@localhost"),
|
|
298
|
+
orgId: text("org_id"),
|
|
299
|
+
databaseId: text("database_id").notNull(),
|
|
300
|
+
propertyId: text("property_id").notNull(),
|
|
301
|
+
keyValueJson: text("key_value_json").notNull(),
|
|
302
|
+
itemId: text("item_id").notNull(),
|
|
303
|
+
documentId: text("document_id").notNull(),
|
|
304
|
+
createdAt: text("created_at").notNull().default(now()),
|
|
305
|
+
updatedAt: text("updated_at").notNull().default(now()),
|
|
306
|
+
},
|
|
307
|
+
(claim) => [
|
|
308
|
+
uniqueIndex(
|
|
309
|
+
"content_database_item_key_claims_database_property_value_unique",
|
|
310
|
+
).on(claim.databaseId, claim.propertyId, claim.keyValueJson),
|
|
311
|
+
uniqueIndex(
|
|
312
|
+
"content_database_item_key_claims_database_property_document_unique",
|
|
313
|
+
).on(claim.databaseId, claim.propertyId, claim.documentId),
|
|
314
|
+
],
|
|
315
|
+
);
|
|
316
|
+
|
|
290
317
|
export const contentDatabaseBodyHydrationQueue = table(
|
|
291
318
|
"content_database_body_hydration_queue",
|
|
292
319
|
{
|
|
@@ -908,6 +908,30 @@ const runContentMigrations = runMigrations(
|
|
|
908
908
|
WHERE legacy_database_trash.document_id = documents.id
|
|
909
909
|
)`,
|
|
910
910
|
},
|
|
911
|
+
{
|
|
912
|
+
version: 78,
|
|
913
|
+
name: "content-database-item-stable-key-claims",
|
|
914
|
+
sql: `CREATE TABLE IF NOT EXISTS content_database_item_key_claims (
|
|
915
|
+
id TEXT PRIMARY KEY,
|
|
916
|
+
owner_email TEXT NOT NULL DEFAULT 'local@localhost',
|
|
917
|
+
org_id TEXT,
|
|
918
|
+
database_id TEXT NOT NULL,
|
|
919
|
+
property_id TEXT NOT NULL,
|
|
920
|
+
key_value_json TEXT NOT NULL,
|
|
921
|
+
item_id TEXT NOT NULL,
|
|
922
|
+
document_id TEXT NOT NULL,
|
|
923
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
924
|
+
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
925
|
+
);
|
|
926
|
+
CREATE UNIQUE INDEX IF NOT EXISTS content_database_item_key_claims_database_property_value_unique ON content_database_item_key_claims (database_id, property_id, key_value_json);
|
|
927
|
+
CREATE INDEX IF NOT EXISTS content_database_item_key_claims_item_idx ON content_database_item_key_claims (item_id);
|
|
928
|
+
CREATE INDEX IF NOT EXISTS content_database_item_key_claims_document_idx ON content_database_item_key_claims (document_id)`,
|
|
929
|
+
},
|
|
930
|
+
{
|
|
931
|
+
version: 79,
|
|
932
|
+
name: "content-database-item-stable-key-single-active-claim",
|
|
933
|
+
sql: `CREATE UNIQUE INDEX IF NOT EXISTS content_database_item_key_claims_database_property_document_unique ON content_database_item_key_claims (database_id, property_id, document_id)`,
|
|
934
|
+
},
|
|
911
935
|
],
|
|
912
936
|
{ table: "content_migrations" },
|
|
913
937
|
);
|
|
@@ -181,142 +181,150 @@ export function BreakpointDeviceControl({
|
|
|
181
181
|
return (
|
|
182
182
|
<div
|
|
183
183
|
data-breakpoint-device-control
|
|
184
|
-
className={cn(
|
|
185
|
-
"flex shrink-0 items-center gap-0.5 rounded-md bg-[var(--design-editor-control-bg)] p-0.5",
|
|
186
|
-
className,
|
|
187
|
-
)}
|
|
184
|
+
className={cn("flex shrink-0 items-center gap-1", className)}
|
|
188
185
|
>
|
|
189
|
-
{/*
|
|
186
|
+
{/* Only the selectable segments share the tinted, inset group. "+" adds a
|
|
187
|
+
breakpoint rather than targeting one, so it must stay outside this
|
|
188
|
+
surface — inside it, it reads as one more segment to select. */}
|
|
189
|
+
<div className="flex items-center gap-0.5 rounded-md bg-[var(--design-editor-control-bg)] p-0.5">
|
|
190
|
+
{/* Base segment — the primary/widest editing context. Icon-only with
|
|
190
191
|
the label in the tooltip: this control shares one cramped
|
|
191
192
|
inspector-header row with the collaborators menu and play/share
|
|
192
193
|
actions (~300px total), so every segment stays as narrow as it
|
|
193
194
|
can. */}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
195
|
+
<button
|
|
196
|
+
type="button"
|
|
197
|
+
className={segmentClass(baseActive)}
|
|
198
|
+
aria-pressed={baseActive}
|
|
199
|
+
aria-label={t("designEditor.breakpointBar.base")}
|
|
200
|
+
onClick={() => onSelect(undefined)}
|
|
201
|
+
title={
|
|
202
|
+
baseWidthPx != null
|
|
203
|
+
? `${t("designEditor.breakpointBar.base")} · ${Math.round(baseWidthPx)}px`
|
|
204
|
+
: t("designEditor.breakpointBar.base")
|
|
205
|
+
}
|
|
206
|
+
>
|
|
207
|
+
<IconViewportWide className="size-3.5" />
|
|
208
|
+
</button>
|
|
208
209
|
|
|
209
|
-
|
|
210
|
+
{/* One segment per breakpoint, widest → narrowest. Clicking always
|
|
210
211
|
SELECTS (never toggles off) — returning to Base is the Base
|
|
211
212
|
segment, clicking the base frame/empty canvas, or Escape (the
|
|
212
213
|
Framer click-to-target model). */}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
214
|
+
{ordered.map((breakpoint) => {
|
|
215
|
+
const active = activeWidthPx === breakpoint.widthPx;
|
|
216
|
+
const menuOpen = menuOpenFor === breakpoint.id;
|
|
217
|
+
const showMenuAffordance = Boolean(
|
|
218
|
+
canEdit && (onRemove || onChangeWidth) && (active || menuOpen),
|
|
219
|
+
);
|
|
220
|
+
return (
|
|
221
|
+
<div key={breakpoint.id} className="relative flex items-center">
|
|
222
|
+
<button
|
|
223
|
+
type="button"
|
|
224
|
+
className={segmentClass(active)}
|
|
225
|
+
aria-pressed={active}
|
|
226
|
+
onClick={() => onSelect(breakpoint.widthPx)}
|
|
227
|
+
title={`${breakpoint.label} · ${breakpoint.widthPx}px`}
|
|
228
|
+
>
|
|
229
|
+
{/* ITEM 8a — device icon (by width bucket) + width number.
|
|
229
230
|
Kept compact (size-3, one notch smaller than Base's
|
|
230
231
|
size-3.5) so the segment still fits this ~300px
|
|
231
232
|
inspector-header row next to play/share; the full label
|
|
232
233
|
stays in the tooltip. */}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
234
|
+
<DeviceIcon widthPx={breakpoint.widthPx} />
|
|
235
|
+
<span>{breakpoint.widthPx}</span>
|
|
236
|
+
</button>
|
|
237
|
+
{/* "…" — per-breakpoint options (Change width / Remove). Shown
|
|
237
238
|
for the ACTIVE segment (and while its menu is open) so idle
|
|
238
239
|
segments stay clean; replaces the old hover-"X" that both
|
|
239
240
|
shifted layout and, being buried in a floating bar, was easy
|
|
240
241
|
to miss entirely. */}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
>
|
|
249
|
-
<DropdownMenuTrigger asChild>
|
|
250
|
-
<button
|
|
251
|
-
type="button"
|
|
252
|
-
aria-label={t("designEditor.breakpointBar.options")}
|
|
253
|
-
className="flex h-6 w-4 shrink-0 cursor-pointer items-center justify-center rounded-[5px] text-muted-foreground hover:text-foreground"
|
|
254
|
-
onClick={(event) => event.stopPropagation()}
|
|
255
|
-
>
|
|
256
|
-
<IconDots className="size-3" />
|
|
257
|
-
</button>
|
|
258
|
-
</DropdownMenuTrigger>
|
|
259
|
-
<DropdownMenuContent
|
|
260
|
-
align="end"
|
|
261
|
-
className="design-editor-app-menu-content w-52 rounded-lg bg-[var(--design-editor-panel-bg)] p-1"
|
|
242
|
+
{showMenuAffordance ? (
|
|
243
|
+
<DropdownMenu
|
|
244
|
+
open={menuOpen}
|
|
245
|
+
onOpenChange={(open) => {
|
|
246
|
+
setMenuOpenFor(open ? breakpoint.id : null);
|
|
247
|
+
setWidthDraft(open ? String(breakpoint.widthPx) : "");
|
|
248
|
+
}}
|
|
262
249
|
>
|
|
263
|
-
|
|
250
|
+
<DropdownMenuTrigger asChild>
|
|
251
|
+
<button
|
|
252
|
+
type="button"
|
|
253
|
+
aria-label={t("designEditor.breakpointBar.options")}
|
|
254
|
+
className="flex h-6 w-4 shrink-0 cursor-pointer items-center justify-center rounded-[5px] text-muted-foreground hover:text-foreground"
|
|
255
|
+
onClick={(event) => event.stopPropagation()}
|
|
256
|
+
>
|
|
257
|
+
<IconDots className="size-3" />
|
|
258
|
+
</button>
|
|
259
|
+
</DropdownMenuTrigger>
|
|
260
|
+
<DropdownMenuContent
|
|
261
|
+
align="end"
|
|
262
|
+
className="design-editor-app-menu-content w-52 rounded-lg bg-[var(--design-editor-panel-bg)] p-1"
|
|
263
|
+
>
|
|
264
|
+
{/* Change width — inline input row; Enter commits.
|
|
264
265
|
Key events stay local (stopPropagation) so the menu's
|
|
265
266
|
typeahead/arrow navigation doesn't steal keystrokes
|
|
266
267
|
from the input. */}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
event.stopPropagation();
|
|
281
|
-
if (event.key !== "Enter") return;
|
|
282
|
-
event.preventDefault();
|
|
283
|
-
const widthPx = parseBreakpointWidthInput(
|
|
284
|
-
widthDraft,
|
|
285
|
-
existingWidths.filter(
|
|
286
|
-
(width) => width !== breakpoint.widthPx,
|
|
287
|
-
),
|
|
288
|
-
);
|
|
289
|
-
setMenuOpenFor(null);
|
|
290
|
-
if (
|
|
291
|
-
widthPx !== null &&
|
|
292
|
-
widthPx !== breakpoint.widthPx
|
|
293
|
-
) {
|
|
294
|
-
onChangeWidth(breakpoint.id, widthPx);
|
|
268
|
+
{onChangeWidth ? (
|
|
269
|
+
<div className="flex items-center gap-1.5 px-1.5 py-1">
|
|
270
|
+
<span className="shrink-0 !text-[11px] text-muted-foreground">
|
|
271
|
+
{t("designEditor.breakpointBar.changeWidth")}
|
|
272
|
+
</span>
|
|
273
|
+
<Input
|
|
274
|
+
type="number"
|
|
275
|
+
min={320}
|
|
276
|
+
max={3840}
|
|
277
|
+
value={widthDraft}
|
|
278
|
+
autoFocus
|
|
279
|
+
onChange={(event) =>
|
|
280
|
+
setWidthDraft(event.target.value)
|
|
295
281
|
}
|
|
282
|
+
onKeyDown={(event) => {
|
|
283
|
+
event.stopPropagation();
|
|
284
|
+
if (event.key !== "Enter") return;
|
|
285
|
+
event.preventDefault();
|
|
286
|
+
const widthPx = parseBreakpointWidthInput(
|
|
287
|
+
widthDraft,
|
|
288
|
+
existingWidths.filter(
|
|
289
|
+
(width) => width !== breakpoint.widthPx,
|
|
290
|
+
),
|
|
291
|
+
);
|
|
292
|
+
setMenuOpenFor(null);
|
|
293
|
+
if (
|
|
294
|
+
widthPx !== null &&
|
|
295
|
+
widthPx !== breakpoint.widthPx
|
|
296
|
+
) {
|
|
297
|
+
onChangeWidth(breakpoint.id, widthPx);
|
|
298
|
+
}
|
|
299
|
+
}}
|
|
300
|
+
className="h-6 px-1.5 !text-[11px] tabular-nums"
|
|
301
|
+
aria-label={t(
|
|
302
|
+
"designEditor.breakpointBar.changeWidth",
|
|
303
|
+
)}
|
|
304
|
+
/>
|
|
305
|
+
</div>
|
|
306
|
+
) : null}
|
|
307
|
+
{onChangeWidth && onRemove ? (
|
|
308
|
+
<DropdownMenuSeparator />
|
|
309
|
+
) : null}
|
|
310
|
+
{onRemove ? (
|
|
311
|
+
<DropdownMenuItem
|
|
312
|
+
className="h-7 px-2 py-0 !text-[12px] text-destructive focus:text-destructive"
|
|
313
|
+
onSelect={() => {
|
|
314
|
+
setMenuOpenFor(null);
|
|
315
|
+
onRemove(breakpoint.id);
|
|
296
316
|
}}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
onRemove(breakpoint.id);
|
|
309
|
-
}}
|
|
310
|
-
>
|
|
311
|
-
{t("designEditor.breakpointBar.remove")}
|
|
312
|
-
</DropdownMenuItem>
|
|
313
|
-
) : null}
|
|
314
|
-
</DropdownMenuContent>
|
|
315
|
-
</DropdownMenu>
|
|
316
|
-
) : null}
|
|
317
|
-
</div>
|
|
318
|
-
);
|
|
319
|
-
})}
|
|
317
|
+
>
|
|
318
|
+
{t("designEditor.breakpointBar.remove")}
|
|
319
|
+
</DropdownMenuItem>
|
|
320
|
+
) : null}
|
|
321
|
+
</DropdownMenuContent>
|
|
322
|
+
</DropdownMenu>
|
|
323
|
+
) : null}
|
|
324
|
+
</div>
|
|
325
|
+
);
|
|
326
|
+
})}
|
|
327
|
+
</div>
|
|
320
328
|
|
|
321
329
|
{/* "+" — Framer default widths or a custom width. */}
|
|
322
330
|
{canEdit && onAdd ? (
|
|
@@ -325,7 +333,7 @@ export function BreakpointDeviceControl({
|
|
|
325
333
|
<Button
|
|
326
334
|
variant="ghost"
|
|
327
335
|
size="icon"
|
|
328
|
-
className="size-6 cursor-pointer rounded-
|
|
336
|
+
className="size-6 shrink-0 cursor-pointer rounded-md text-muted-foreground hover:bg-[var(--design-editor-control-bg)] hover:text-foreground"
|
|
329
337
|
title={t("designEditor.breakpointBar.addBreakpoint")}
|
|
330
338
|
>
|
|
331
339
|
<IconPlus className="size-3.5" />
|
|
@@ -2,7 +2,6 @@ import {
|
|
|
2
2
|
IconComponents,
|
|
3
3
|
IconFrame,
|
|
4
4
|
IconPhoto,
|
|
5
|
-
IconTypography,
|
|
6
5
|
IconVector,
|
|
7
6
|
} from "@tabler/icons-react";
|
|
8
7
|
import {
|
|
@@ -29,6 +28,7 @@ import {
|
|
|
29
28
|
} from "@/components/ui/context-menu";
|
|
30
29
|
import { cn } from "@/lib/utils";
|
|
31
30
|
|
|
31
|
+
import { IconText } from "./inspector/design-icons";
|
|
32
32
|
import type { CanvasLayerHitCandidate } from "./types";
|
|
33
33
|
|
|
34
34
|
// LIVE-VERIFIED (real Figma, UI3) canvas context menus:
|
|
@@ -1177,7 +1177,7 @@ function CanvasLayerCandidateItem({
|
|
|
1177
1177
|
const Icon = candidate.info.componentName
|
|
1178
1178
|
? IconComponents
|
|
1179
1179
|
: /^(h[1-6]|p|span|label|input|textarea)$/.test(tag)
|
|
1180
|
-
?
|
|
1180
|
+
? IconText
|
|
1181
1181
|
: /^(img|picture|video)$/.test(tag)
|
|
1182
1182
|
? IconPhoto
|
|
1183
1183
|
: /^(svg|path|circle|ellipse|polygon|line)$/.test(tag)
|
|
@@ -84,7 +84,6 @@ import {
|
|
|
84
84
|
IconRotate3d,
|
|
85
85
|
IconShadow,
|
|
86
86
|
IconSquare,
|
|
87
|
-
IconTypography,
|
|
88
87
|
IconUnlink,
|
|
89
88
|
IconVector,
|
|
90
89
|
IconWaveSine,
|
|
@@ -224,7 +223,6 @@ import {
|
|
|
224
223
|
InspectorSegment,
|
|
225
224
|
RowDragHandle,
|
|
226
225
|
SectionIconButton,
|
|
227
|
-
SectionIconToggle,
|
|
228
226
|
useRowDragReorder,
|
|
229
227
|
} from "./edit-panel/inspector-controls";
|
|
230
228
|
import {
|
|
@@ -331,7 +329,7 @@ import {
|
|
|
331
329
|
type MotionKeyframeCssProperty,
|
|
332
330
|
type ScrubInputChangeMeta,
|
|
333
331
|
} from "./inspector";
|
|
334
|
-
import { IconLayoutSettings } from "./inspector/design-icons";
|
|
332
|
+
import { IconLayoutSettings, IconText } from "./inspector/design-icons";
|
|
335
333
|
import type { DesignPaintType } from "./inspector/DesignColorPicker";
|
|
336
334
|
import {
|
|
337
335
|
GlslShaderEffectSection,
|
|
@@ -1156,7 +1154,7 @@ function CodeInspectPanel({
|
|
|
1156
1154
|
function elementTypeIcon(element: ElementInfo) {
|
|
1157
1155
|
if (elementIsComponentSelection(element)) return IconComponents;
|
|
1158
1156
|
const tag = normalizedElementTagName(element.tagName);
|
|
1159
|
-
if (TEXT_TAGS.has(tag)) return
|
|
1157
|
+
if (TEXT_TAGS.has(tag)) return IconText;
|
|
1160
1158
|
if (tag === "img" || tag === "video" || tag === "picture") return IconPhoto;
|
|
1161
1159
|
if (tag === "svg" || tag === "path") return IconVector;
|
|
1162
1160
|
if (tag === "button" || tag === "a") return IconComponents;
|
|
@@ -1573,9 +1571,19 @@ function ExportPreview({
|
|
|
1573
1571
|
}) {
|
|
1574
1572
|
const t = useT();
|
|
1575
1573
|
const rect = element?.boundingRect;
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1574
|
+
// Selection sources disagree on whether boundingRect is populated (layer-tree
|
|
1575
|
+
// picks in overview report nothing), so the rendered bitmap — which exists
|
|
1576
|
+
// only once a real capture succeeded — is the honest size to caption with.
|
|
1577
|
+
const [rendered, setRendered] = useState<{
|
|
1578
|
+
width: number;
|
|
1579
|
+
height: number;
|
|
1580
|
+
} | null>(null);
|
|
1581
|
+
const width = rendered?.width ?? rect?.width ?? null;
|
|
1582
|
+
const height = rendered?.height ?? rect?.height ?? null;
|
|
1583
|
+
const aspect =
|
|
1584
|
+
width != null && height != null && width > 0 && height > 0
|
|
1585
|
+
? width / height
|
|
1586
|
+
: 1;
|
|
1579
1587
|
const [state, setState] = useState<
|
|
1580
1588
|
| { status: "idle" }
|
|
1581
1589
|
| { status: "loading" }
|
|
@@ -1591,6 +1599,7 @@ function ExportPreview({
|
|
|
1591
1599
|
let cancelled = false;
|
|
1592
1600
|
let objectUrl: string | null = null;
|
|
1593
1601
|
setState({ status: "loading" });
|
|
1602
|
+
setRendered(null);
|
|
1594
1603
|
void onRender()
|
|
1595
1604
|
.then((blob) => {
|
|
1596
1605
|
if (cancelled) return;
|
|
@@ -1624,6 +1633,12 @@ function ExportPreview({
|
|
|
1624
1633
|
style={{
|
|
1625
1634
|
imageRendering: "auto",
|
|
1626
1635
|
}}
|
|
1636
|
+
onLoad={(event) =>
|
|
1637
|
+
setRendered({
|
|
1638
|
+
width: event.currentTarget.naturalWidth,
|
|
1639
|
+
height: event.currentTarget.naturalHeight,
|
|
1640
|
+
})
|
|
1641
|
+
}
|
|
1627
1642
|
/>
|
|
1628
1643
|
) : (
|
|
1629
1644
|
<div className="flex size-full items-center justify-center rounded border border-[var(--design-editor-control-border)] bg-[var(--design-editor-panel-bg)] px-2 text-center !text-[10px] text-muted-foreground">
|
|
@@ -1633,9 +1648,43 @@ function ExportPreview({
|
|
|
1633
1648
|
</div>
|
|
1634
1649
|
)}
|
|
1635
1650
|
</div>
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1651
|
+
{width != null && height != null ? (
|
|
1652
|
+
<p className="mt-2 text-center text-[10px] tabular-nums text-muted-foreground">
|
|
1653
|
+
{Math.round(width)} × {Math.round(height)}
|
|
1654
|
+
</p>
|
|
1655
|
+
) : null}
|
|
1656
|
+
</div>
|
|
1657
|
+
);
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
/** Named, collapsed-by-default "Preview" row. An icon-only toggle in the
|
|
1661
|
+
* section header reads as "export an image", not "reveal what will export". */
|
|
1662
|
+
function ExportPreviewDisclosure({
|
|
1663
|
+
element,
|
|
1664
|
+
onRender,
|
|
1665
|
+
}: {
|
|
1666
|
+
element: ElementInfo | null;
|
|
1667
|
+
onRender?: () => Promise<Blob>;
|
|
1668
|
+
}) {
|
|
1669
|
+
const [open, setOpen] = useState(false);
|
|
1670
|
+
const label = "Preview"; // i18n-ignore design inspector label
|
|
1671
|
+
|
|
1672
|
+
return (
|
|
1673
|
+
<div>
|
|
1674
|
+
<button
|
|
1675
|
+
type="button"
|
|
1676
|
+
onClick={() => setOpen((shown) => !shown)}
|
|
1677
|
+
aria-expanded={open}
|
|
1678
|
+
className="flex cursor-pointer items-center gap-1 !text-[11px] font-medium text-muted-foreground hover:text-foreground"
|
|
1679
|
+
>
|
|
1680
|
+
{open ? (
|
|
1681
|
+
<IconChevronDown className="size-3 shrink-0" />
|
|
1682
|
+
) : (
|
|
1683
|
+
<IconChevronRight className="size-3 shrink-0 rtl:-scale-x-100" />
|
|
1684
|
+
)}
|
|
1685
|
+
{label}
|
|
1686
|
+
</button>
|
|
1687
|
+
{open ? <ExportPreview element={element} onRender={onRender} /> : null}
|
|
1639
1688
|
</div>
|
|
1640
1689
|
);
|
|
1641
1690
|
}
|
|
@@ -1800,7 +1849,6 @@ export const EditPanel = memo(function EditPanel({
|
|
|
1800
1849
|
const [exportSettings, setExportSettings] = useState<ExportSettingsValue>(
|
|
1801
1850
|
DEFAULT_EXPORT_SETTINGS,
|
|
1802
1851
|
);
|
|
1803
|
-
const [showExportPreview, setShowExportPreview] = useState(false);
|
|
1804
1852
|
// Element interaction-state selector (Default / Hover / Focus / …). Owned
|
|
1805
1853
|
// here (not lifted to the parent) per the mission contract — DesignEditor
|
|
1806
1854
|
// only needs to react to changes via onInteractionStateChange, it doesn't
|
|
@@ -1927,7 +1975,6 @@ export const EditPanel = memo(function EditPanel({
|
|
|
1927
1975
|
|
|
1928
1976
|
useEffect(() => {
|
|
1929
1977
|
setExportSettings(DEFAULT_EXPORT_SETTINGS);
|
|
1930
|
-
setShowExportPreview(false);
|
|
1931
1978
|
}, [selectedElementKey]);
|
|
1932
1979
|
|
|
1933
1980
|
useEffect(() => {
|
|
@@ -2368,22 +2415,7 @@ export const EditPanel = memo(function EditPanel({
|
|
|
2368
2415
|
</>
|
|
2369
2416
|
)}
|
|
2370
2417
|
{onExport ? (
|
|
2371
|
-
<PanelSection
|
|
2372
|
-
title={t("editPanel.sections.export")}
|
|
2373
|
-
actions={
|
|
2374
|
-
<SectionIconToggle
|
|
2375
|
-
label={
|
|
2376
|
-
showExportPreview
|
|
2377
|
-
? "Hide preview" /* i18n-ignore design inspector action */
|
|
2378
|
-
: "Show preview" /* i18n-ignore design inspector action */
|
|
2379
|
-
}
|
|
2380
|
-
active={showExportPreview}
|
|
2381
|
-
onClick={() => setShowExportPreview((shown) => !shown)}
|
|
2382
|
-
>
|
|
2383
|
-
<IconPhoto className="size-3.5" />
|
|
2384
|
-
</SectionIconToggle>
|
|
2385
|
-
}
|
|
2386
|
-
>
|
|
2418
|
+
<PanelSection title={t("editPanel.sections.export")}>
|
|
2387
2419
|
<ExportSettingsPanel
|
|
2388
2420
|
key={selectedElementKey}
|
|
2389
2421
|
value={exportSettings}
|
|
@@ -2402,12 +2434,14 @@ export const EditPanel = memo(function EditPanel({
|
|
|
2402
2434
|
}
|
|
2403
2435
|
onExport={onExport}
|
|
2404
2436
|
/>
|
|
2405
|
-
{
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2437
|
+
{/* Distinct from the settings panel's key: two siblings
|
|
2438
|
+
sharing one key makes React duplicate the panel on every
|
|
2439
|
+
re-render, so the section grows an Export block per tick. */}
|
|
2440
|
+
<ExportPreviewDisclosure
|
|
2441
|
+
key={`${selectedElementKey}:preview`}
|
|
2442
|
+
element={inspectorElement}
|
|
2443
|
+
onRender={onRenderExportPreview}
|
|
2444
|
+
/>
|
|
2411
2445
|
</PanelSection>
|
|
2412
2446
|
) : null}
|
|
2413
2447
|
|