@agent-native/core 0.136.5 → 0.137.1
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/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/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/lib/recent-references.ts +82 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +3 -0
- package/corpus/templates/slides/app/pages/Index.tsx +236 -181
- 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/DefaultSpinner.d.ts +0 -12
- package/dist/client/DefaultSpinner.js +22 -2
- 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 +35 -1
- package/dist/client/route-state.js +10 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/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/deploy/build.js +2 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +1 -1
- 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 +3 -3
- 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/shared/first-run-onboarding.d.ts +3 -0
- package/dist/shared/first-run-onboarding.js +3 -0
- package/dist/templates/workspace-core/.agents/skills/address-feedback/SKILL.md +25 -9
- 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
- package/src/templates/workspace-core/.agents/skills/address-feedback/SKILL.md +25 -9
|
@@ -77,6 +77,18 @@ type WorldPoint = {
|
|
|
77
77
|
y: number;
|
|
78
78
|
};
|
|
79
79
|
|
|
80
|
+
type ClientPoint = {
|
|
81
|
+
x: number;
|
|
82
|
+
y: number;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
type PinchGesture = {
|
|
86
|
+
pointerIds: [number, number];
|
|
87
|
+
startDistance: number;
|
|
88
|
+
startMidpoint: ClientPoint;
|
|
89
|
+
startView: CanvasView;
|
|
90
|
+
};
|
|
91
|
+
|
|
80
92
|
type PendingMarkup = {
|
|
81
93
|
mode: "text" | "callout";
|
|
82
94
|
origin: WorldPoint;
|
|
@@ -140,6 +152,10 @@ export function CanvasArea({
|
|
|
140
152
|
[canvas.viewport?.pan?.x, canvas.viewport?.pan?.y, canvas.viewport?.zoom],
|
|
141
153
|
);
|
|
142
154
|
const [view, setView] = useState<CanvasView>(initialView);
|
|
155
|
+
const viewRef = useRef(view);
|
|
156
|
+
viewRef.current = view;
|
|
157
|
+
const activePointersRef = useRef(new Map<number, ClientPoint>());
|
|
158
|
+
const pinchGestureRef = useRef<PinchGesture | null>(null);
|
|
143
159
|
const [drag, setDrag] = useState<{
|
|
144
160
|
pointerId: number;
|
|
145
161
|
startX: number;
|
|
@@ -581,8 +597,48 @@ export function CanvasArea({
|
|
|
581
597
|
const onPointerDown = (event: ReactPointerEvent<HTMLDivElement>) => {
|
|
582
598
|
if (event.button !== 0 && event.button !== 1) return;
|
|
583
599
|
const target = event.target as HTMLElement;
|
|
600
|
+
const activePointers = activePointersRef.current;
|
|
584
601
|
// Don't start a pan when grabbing interactive chrome (zoom controls etc.).
|
|
585
|
-
if (
|
|
602
|
+
if (
|
|
603
|
+
event.button === 0 &&
|
|
604
|
+
target.closest("[data-plan-interactive]") &&
|
|
605
|
+
activePointers.size === 0
|
|
606
|
+
) {
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
activePointers.set(event.pointerId, {
|
|
610
|
+
x: event.clientX,
|
|
611
|
+
y: event.clientY,
|
|
612
|
+
});
|
|
613
|
+
if (activePointers.size >= 2) {
|
|
614
|
+
const pointerIds = Array.from(activePointers.keys()).slice(0, 2) as [
|
|
615
|
+
number,
|
|
616
|
+
number,
|
|
617
|
+
];
|
|
618
|
+
const first = activePointers.get(pointerIds[0]);
|
|
619
|
+
const second = activePointers.get(pointerIds[1]);
|
|
620
|
+
if (!first || !second) return;
|
|
621
|
+
const rect = viewportRef.current?.getBoundingClientRect();
|
|
622
|
+
const startMidpoint = getMidpoint(first, second);
|
|
623
|
+
pinchGestureRef.current = {
|
|
624
|
+
pointerIds,
|
|
625
|
+
startDistance: getDistance(first, second),
|
|
626
|
+
startMidpoint: rect
|
|
627
|
+
? {
|
|
628
|
+
x: startMidpoint.x - rect.left,
|
|
629
|
+
y: startMidpoint.y - rect.top,
|
|
630
|
+
}
|
|
631
|
+
: startMidpoint,
|
|
632
|
+
startView: viewRef.current,
|
|
633
|
+
};
|
|
634
|
+
setDrag(null);
|
|
635
|
+
setDraftCallout(null);
|
|
636
|
+
setPendingMarkup(null);
|
|
637
|
+
event.preventDefault();
|
|
638
|
+
event.stopPropagation();
|
|
639
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
586
642
|
if (!isEditableShortcutTarget(target)) {
|
|
587
643
|
event.currentTarget.focus({ preventScroll: true });
|
|
588
644
|
}
|
|
@@ -674,6 +730,37 @@ export function CanvasArea({
|
|
|
674
730
|
onPointerDown={onPointerDown}
|
|
675
731
|
onKeyDown={onViewportKeyDown}
|
|
676
732
|
onPointerMove={(event) => {
|
|
733
|
+
const activePointer = activePointersRef.current.get(event.pointerId);
|
|
734
|
+
if (activePointer) {
|
|
735
|
+
activePointer.x = event.clientX;
|
|
736
|
+
activePointer.y = event.clientY;
|
|
737
|
+
}
|
|
738
|
+
const pinchGesture = pinchGestureRef.current;
|
|
739
|
+
if (pinchGesture) {
|
|
740
|
+
const first = activePointersRef.current.get(
|
|
741
|
+
pinchGesture.pointerIds[0],
|
|
742
|
+
);
|
|
743
|
+
const second = activePointersRef.current.get(
|
|
744
|
+
pinchGesture.pointerIds[1],
|
|
745
|
+
);
|
|
746
|
+
const rect = viewportRef.current?.getBoundingClientRect();
|
|
747
|
+
if (!first || !second || !rect) return;
|
|
748
|
+
const midpoint = getMidpoint(first, second);
|
|
749
|
+
const currentMidpoint = {
|
|
750
|
+
x: midpoint.x - rect.left,
|
|
751
|
+
y: midpoint.y - rect.top,
|
|
752
|
+
};
|
|
753
|
+
updateView(() =>
|
|
754
|
+
viewForPinchGesture({
|
|
755
|
+
gesture: pinchGesture,
|
|
756
|
+
currentDistance: getDistance(first, second),
|
|
757
|
+
currentMidpoint,
|
|
758
|
+
}),
|
|
759
|
+
);
|
|
760
|
+
event.preventDefault();
|
|
761
|
+
event.stopPropagation();
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
677
764
|
if (draftCallout?.pointerId === event.pointerId) {
|
|
678
765
|
const point = clientPointToWorld(event);
|
|
679
766
|
if (!point) return;
|
|
@@ -697,6 +784,28 @@ export function CanvasArea({
|
|
|
697
784
|
}));
|
|
698
785
|
}}
|
|
699
786
|
onPointerUp={(event) => {
|
|
787
|
+
activePointersRef.current.delete(event.pointerId);
|
|
788
|
+
if (pinchGestureRef.current) {
|
|
789
|
+
event.preventDefault();
|
|
790
|
+
event.stopPropagation();
|
|
791
|
+
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
|
792
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
793
|
+
}
|
|
794
|
+
const remaining = Array.from(
|
|
795
|
+
activePointersRef.current.entries(),
|
|
796
|
+
)[0];
|
|
797
|
+
pinchGestureRef.current = null;
|
|
798
|
+
if (remaining) {
|
|
799
|
+
setDrag({
|
|
800
|
+
pointerId: remaining[0],
|
|
801
|
+
startX: remaining[1].x,
|
|
802
|
+
startY: remaining[1].y,
|
|
803
|
+
panX: viewRef.current.pan.x,
|
|
804
|
+
panY: viewRef.current.pan.y,
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
700
809
|
if (draftCallout?.pointerId === event.pointerId) {
|
|
701
810
|
const point = clientPointToWorld(event) ?? draftCallout.current;
|
|
702
811
|
event.preventDefault();
|
|
@@ -722,7 +831,9 @@ export function CanvasArea({
|
|
|
722
831
|
setDrag(null);
|
|
723
832
|
}
|
|
724
833
|
}}
|
|
725
|
-
onPointerCancel={() => {
|
|
834
|
+
onPointerCancel={(event) => {
|
|
835
|
+
activePointersRef.current.delete(event.pointerId);
|
|
836
|
+
pinchGestureRef.current = null;
|
|
726
837
|
setDraftCallout(null);
|
|
727
838
|
setDrag(null);
|
|
728
839
|
}}
|
|
@@ -838,6 +949,7 @@ export function CanvasArea({
|
|
|
838
949
|
<div
|
|
839
950
|
className="plan-canvas-zoom absolute bottom-3 left-3 z-10 flex items-center gap-0.5 rounded-lg border border-plan-line bg-plan-chrome p-0.5 shadow-md backdrop-blur"
|
|
840
951
|
data-plan-interactive
|
|
952
|
+
title={t("raw.canvas.zoomHint")}
|
|
841
953
|
>
|
|
842
954
|
<Button
|
|
843
955
|
type="button"
|
|
@@ -912,6 +1024,18 @@ function surfaceOf(frame: PlanArtboard): PlanWireframeSurface {
|
|
|
912
1024
|
return frame.surface ?? frame.wireframe?.surface ?? "desktop";
|
|
913
1025
|
}
|
|
914
1026
|
|
|
1027
|
+
/**
|
|
1028
|
+
* Keep the surface width stable while allowing long screens to reserve a
|
|
1029
|
+
* taller, non-scrolling artboard in canvas mode.
|
|
1030
|
+
*/
|
|
1031
|
+
export function canvasFrameSize(frame: PlanArtboard) {
|
|
1032
|
+
const preset = SURFACE_SIZE[surfaceOf(frame)];
|
|
1033
|
+
return {
|
|
1034
|
+
width: preset.width,
|
|
1035
|
+
height: Math.max(preset.height, frame.height ?? preset.height),
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
|
|
915
1039
|
function sameCanvasView(a: CanvasView, b: CanvasView) {
|
|
916
1040
|
return (
|
|
917
1041
|
Math.abs(a.zoom - b.zoom) < 0.0001 &&
|
|
@@ -980,11 +1104,9 @@ function layoutArtboards(frames: PlanArtboard[]): PlanArtboard[] {
|
|
|
980
1104
|
|
|
981
1105
|
return frames.map((frame) => {
|
|
982
1106
|
const surface = surfaceOf(frame);
|
|
983
|
-
|
|
984
|
-
//
|
|
985
|
-
|
|
986
|
-
const width = preset.width;
|
|
987
|
-
const height = preset.height;
|
|
1107
|
+
// Surface owns the width/aspect. A larger explicit height is the
|
|
1108
|
+
// non-scrolling escape hatch for long HTML screens on the canvas.
|
|
1109
|
+
const { width, height } = canvasFrameSize(frame);
|
|
988
1110
|
|
|
989
1111
|
if (frame.x !== undefined || frame.y !== undefined) {
|
|
990
1112
|
return {
|
|
@@ -1038,10 +1160,7 @@ function CanvasArtboard({
|
|
|
1038
1160
|
onDesignElementSelect?: (selection: DesignElementSelection) => void;
|
|
1039
1161
|
}) {
|
|
1040
1162
|
const surface = surfaceOf(frame);
|
|
1041
|
-
const
|
|
1042
|
-
// SURFACE-locked footprint (see layoutArtboards) — model width/height ignored.
|
|
1043
|
-
const width = preset.width;
|
|
1044
|
-
const height = preset.height;
|
|
1163
|
+
const { width, height } = canvasFrameSize(frame);
|
|
1045
1164
|
const label = frame.label ?? block?.title;
|
|
1046
1165
|
// Report the frame's real rendered height so board connectors can anchor to
|
|
1047
1166
|
// the content box (frames are capped at `height` but usually shorter).
|
|
@@ -2280,6 +2399,43 @@ function connectorRoute(
|
|
|
2280
2399
|
};
|
|
2281
2400
|
}
|
|
2282
2401
|
|
|
2402
|
+
export function viewForPinchGesture(input: {
|
|
2403
|
+
gesture: PinchGesture;
|
|
2404
|
+
currentDistance: number;
|
|
2405
|
+
currentMidpoint: ClientPoint;
|
|
2406
|
+
}): CanvasView {
|
|
2407
|
+
const { gesture, currentDistance, currentMidpoint } = input;
|
|
2408
|
+
const nextZoom = clamp(
|
|
2409
|
+
gesture.startView.zoom * (currentDistance / gesture.startDistance),
|
|
2410
|
+
MIN_ZOOM,
|
|
2411
|
+
MAX_ZOOM,
|
|
2412
|
+
);
|
|
2413
|
+
const worldX =
|
|
2414
|
+
(gesture.startMidpoint.x - gesture.startView.pan.x) /
|
|
2415
|
+
gesture.startView.zoom;
|
|
2416
|
+
const worldY =
|
|
2417
|
+
(gesture.startMidpoint.y - gesture.startView.pan.y) /
|
|
2418
|
+
gesture.startView.zoom;
|
|
2419
|
+
return {
|
|
2420
|
+
zoom: nextZoom,
|
|
2421
|
+
pan: {
|
|
2422
|
+
x: currentMidpoint.x - worldX * nextZoom,
|
|
2423
|
+
y: currentMidpoint.y - worldY * nextZoom,
|
|
2424
|
+
},
|
|
2425
|
+
};
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
function getMidpoint(first: ClientPoint, second: ClientPoint): ClientPoint {
|
|
2429
|
+
return {
|
|
2430
|
+
x: (first.x + second.x) / 2,
|
|
2431
|
+
y: (first.y + second.y) / 2,
|
|
2432
|
+
};
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
function getDistance(first: ClientPoint, second: ClientPoint) {
|
|
2436
|
+
return Math.max(Math.hypot(first.x - second.x, first.y - second.y), 1);
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2283
2439
|
function distance(a: WorldPoint, b: WorldPoint) {
|
|
2284
2440
|
return Math.hypot(a.x - b.x, a.y - b.y);
|
|
2285
2441
|
}
|
|
@@ -1495,12 +1495,27 @@ export const questionFormDataSchema: z.ZodType<PlanQuestionFormBlock["data"]> =
|
|
|
1495
1495
|
submitLabel: z.string().trim().max(400).optional(),
|
|
1496
1496
|
});
|
|
1497
1497
|
|
|
1498
|
+
/**
|
|
1499
|
+
* Rich-text stores Markdown as runtime text. A fully escaped one-line payload
|
|
1500
|
+
* ("### Heading\\n\\nBody") renders as one giant heading, so reject that shape at
|
|
1501
|
+
* the shared schema instead of persisting a document that only looks valid.
|
|
1502
|
+
* Escapes are still fine when they occur alongside real line breaks, such as
|
|
1503
|
+
* an intentional `\\n` inside a code example.
|
|
1504
|
+
*/
|
|
1505
|
+
const planMarkdownSchema = z
|
|
1506
|
+
.string()
|
|
1507
|
+
.max(100_000)
|
|
1508
|
+
.refine(
|
|
1509
|
+
(value) => value.includes("\n") || !value.includes("\\n"),
|
|
1510
|
+
"Rich-text Markdown must use actual line breaks, not a fully escaped `\\n` string.",
|
|
1511
|
+
);
|
|
1512
|
+
|
|
1498
1513
|
export const planBlockSchema: z.ZodType<PlanBlock> = z.lazy(() =>
|
|
1499
1514
|
z.discriminatedUnion("type", [
|
|
1500
1515
|
baseBlockSchema.extend({
|
|
1501
1516
|
type: z.literal("rich-text"),
|
|
1502
1517
|
data: z.object({
|
|
1503
|
-
markdown:
|
|
1518
|
+
markdown: planMarkdownSchema,
|
|
1504
1519
|
}),
|
|
1505
1520
|
}),
|
|
1506
1521
|
baseBlockSchema.extend({
|
|
@@ -2758,7 +2773,7 @@ const planContentPatchUnion = z.discriminatedUnion("op", [
|
|
|
2758
2773
|
op: z.literal("update-rich-text"),
|
|
2759
2774
|
blockId: idSchema,
|
|
2760
2775
|
title: z.string().trim().min(1).max(180).optional(),
|
|
2761
|
-
markdown:
|
|
2776
|
+
markdown: planMarkdownSchema.optional(),
|
|
2762
2777
|
}),
|
|
2763
2778
|
z.object({
|
|
2764
2779
|
op: z.literal("update-custom-html"),
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IconArrowRight,
|
|
3
|
+
IconPalette,
|
|
4
|
+
IconPresentation,
|
|
5
|
+
IconUpload,
|
|
6
|
+
} from "@tabler/icons-react";
|
|
7
|
+
import type { ChangeEvent, ReactNode } from "react";
|
|
8
|
+
|
|
9
|
+
import { Button } from "@/components/ui/button";
|
|
10
|
+
import {
|
|
11
|
+
Dialog,
|
|
12
|
+
DialogContent,
|
|
13
|
+
DialogDescription,
|
|
14
|
+
DialogHeader,
|
|
15
|
+
DialogTitle,
|
|
16
|
+
} from "@/components/ui/dialog";
|
|
17
|
+
import {
|
|
18
|
+
Select,
|
|
19
|
+
SelectContent,
|
|
20
|
+
SelectGroup,
|
|
21
|
+
SelectItem,
|
|
22
|
+
SelectLabel,
|
|
23
|
+
SelectTrigger,
|
|
24
|
+
SelectValue,
|
|
25
|
+
} from "@/components/ui/select";
|
|
26
|
+
import type { Deck } from "@/context/DeckContext";
|
|
27
|
+
import type { RecentReference } from "@/lib/recent-references";
|
|
28
|
+
import { cn } from "@/lib/utils";
|
|
29
|
+
|
|
30
|
+
export interface NewDeckReferenceSelection {
|
|
31
|
+
designSystemId?: string | null;
|
|
32
|
+
referenceDeckId?: string | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface DesignSystemOption {
|
|
36
|
+
id: string;
|
|
37
|
+
title: string;
|
|
38
|
+
isDefault?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface NewDeckReferenceStepProps {
|
|
42
|
+
open: boolean;
|
|
43
|
+
designSystems: DesignSystemOption[];
|
|
44
|
+
decks: Deck[];
|
|
45
|
+
defaultDesignSystemId: string | null;
|
|
46
|
+
defaultReferenceDeckId: string | null;
|
|
47
|
+
recentReferences: RecentReference[];
|
|
48
|
+
onSelect: (selection: NewDeckReferenceSelection) => void;
|
|
49
|
+
onImport: (files: File[]) => Promise<void>;
|
|
50
|
+
onSkip: () => void;
|
|
51
|
+
onOpenChange: (open: boolean) => void;
|
|
52
|
+
importing?: boolean;
|
|
53
|
+
title: string;
|
|
54
|
+
designSystemLabel: string;
|
|
55
|
+
referenceDeckLabel: string;
|
|
56
|
+
chooseDeckLabel: string;
|
|
57
|
+
importFileLabel: string;
|
|
58
|
+
importingLabel: string;
|
|
59
|
+
skipLabel: string;
|
|
60
|
+
defaultSuffix: string;
|
|
61
|
+
starredLabel: string;
|
|
62
|
+
otherDecksLabel: string;
|
|
63
|
+
description: string;
|
|
64
|
+
promptNote?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function NewDeckReferenceStep({
|
|
68
|
+
open,
|
|
69
|
+
designSystems,
|
|
70
|
+
decks,
|
|
71
|
+
defaultDesignSystemId,
|
|
72
|
+
defaultReferenceDeckId,
|
|
73
|
+
recentReferences,
|
|
74
|
+
onSelect,
|
|
75
|
+
onImport,
|
|
76
|
+
onSkip,
|
|
77
|
+
onOpenChange,
|
|
78
|
+
importing = false,
|
|
79
|
+
title,
|
|
80
|
+
designSystemLabel,
|
|
81
|
+
referenceDeckLabel,
|
|
82
|
+
chooseDeckLabel,
|
|
83
|
+
importFileLabel,
|
|
84
|
+
importingLabel,
|
|
85
|
+
skipLabel,
|
|
86
|
+
defaultSuffix,
|
|
87
|
+
starredLabel,
|
|
88
|
+
otherDecksLabel,
|
|
89
|
+
description,
|
|
90
|
+
promptNote,
|
|
91
|
+
}: NewDeckReferenceStepProps) {
|
|
92
|
+
const designSystemById = new Map(
|
|
93
|
+
designSystems.map((designSystem) => [designSystem.id, designSystem]),
|
|
94
|
+
);
|
|
95
|
+
const deckById = new Map(decks.map((deck) => [deck.id, deck]));
|
|
96
|
+
const recentOptions = recentReferences
|
|
97
|
+
.map((reference) => {
|
|
98
|
+
const item =
|
|
99
|
+
reference.kind === "design-system"
|
|
100
|
+
? designSystemById.get(reference.id)
|
|
101
|
+
: deckById.get(reference.id);
|
|
102
|
+
return item ? { reference, item } : null;
|
|
103
|
+
})
|
|
104
|
+
.filter(
|
|
105
|
+
(
|
|
106
|
+
value,
|
|
107
|
+
): value is {
|
|
108
|
+
reference: RecentReference;
|
|
109
|
+
item: DesignSystemOption | Deck;
|
|
110
|
+
} => value !== null,
|
|
111
|
+
);
|
|
112
|
+
const defaultReferenceKeys = new Set(
|
|
113
|
+
[
|
|
114
|
+
defaultDesignSystemId ? `design-system:${defaultDesignSystemId}` : null,
|
|
115
|
+
defaultReferenceDeckId ? `deck:${defaultReferenceDeckId}` : null,
|
|
116
|
+
].filter((value): value is string => value !== null),
|
|
117
|
+
);
|
|
118
|
+
const visibleRecentOptions = recentOptions.filter(
|
|
119
|
+
({ reference }) =>
|
|
120
|
+
!defaultReferenceKeys.has(`${reference.kind}:${reference.id}`),
|
|
121
|
+
);
|
|
122
|
+
const starredDecks = decks.filter((deck) => deck.starred);
|
|
123
|
+
const otherDecks = decks.filter((deck) => !deck.starred);
|
|
124
|
+
|
|
125
|
+
const handleImport = async (event: ChangeEvent<HTMLInputElement>) => {
|
|
126
|
+
const files = Array.from(event.target.files ?? []);
|
|
127
|
+
event.target.value = "";
|
|
128
|
+
if (files.length === 0) return;
|
|
129
|
+
await onImport(files);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
134
|
+
<DialogContent className="max-h-[min(720px,calc(100vh-32px))] overflow-y-auto sm:max-w-2xl">
|
|
135
|
+
<DialogHeader>
|
|
136
|
+
<DialogTitle>{title}</DialogTitle>
|
|
137
|
+
<DialogDescription>{description}</DialogDescription>
|
|
138
|
+
</DialogHeader>
|
|
139
|
+
|
|
140
|
+
<div className="grid gap-4">
|
|
141
|
+
{(defaultDesignSystemId || defaultReferenceDeckId) && (
|
|
142
|
+
<div className="grid gap-2 sm:grid-cols-2">
|
|
143
|
+
{defaultDesignSystemId &&
|
|
144
|
+
designSystemById.has(defaultDesignSystemId) && (
|
|
145
|
+
<ReferenceCard
|
|
146
|
+
icon={<IconPalette className="size-4" />}
|
|
147
|
+
label={
|
|
148
|
+
designSystemById.get(defaultDesignSystemId)?.title ??
|
|
149
|
+
designSystemLabel
|
|
150
|
+
}
|
|
151
|
+
meta={`${designSystemLabel}${defaultSuffix}`}
|
|
152
|
+
onClick={() =>
|
|
153
|
+
onSelect({
|
|
154
|
+
designSystemId: defaultDesignSystemId,
|
|
155
|
+
referenceDeckId: null,
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
/>
|
|
159
|
+
)}
|
|
160
|
+
{defaultReferenceDeckId &&
|
|
161
|
+
deckById.has(defaultReferenceDeckId) && (
|
|
162
|
+
<ReferenceCard
|
|
163
|
+
icon={<IconPresentation className="size-4" />}
|
|
164
|
+
label={deckById.get(defaultReferenceDeckId)?.title ?? ""}
|
|
165
|
+
meta={`${referenceDeckLabel}${defaultSuffix}`}
|
|
166
|
+
onClick={() =>
|
|
167
|
+
onSelect({
|
|
168
|
+
designSystemId: null,
|
|
169
|
+
referenceDeckId: defaultReferenceDeckId,
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
/>
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
)}
|
|
176
|
+
|
|
177
|
+
{visibleRecentOptions.length > 0 && (
|
|
178
|
+
<div className="grid gap-2">
|
|
179
|
+
<div className="grid gap-2 sm:grid-cols-2">
|
|
180
|
+
{visibleRecentOptions.map(({ reference, item }) => (
|
|
181
|
+
<ReferenceCard
|
|
182
|
+
key={`${reference.kind}:${reference.id}`}
|
|
183
|
+
icon={
|
|
184
|
+
reference.kind === "design-system" ? (
|
|
185
|
+
<IconPalette className="size-4" />
|
|
186
|
+
) : (
|
|
187
|
+
<IconPresentation className="size-4" />
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
label={item.title}
|
|
191
|
+
meta={
|
|
192
|
+
reference.kind === "design-system"
|
|
193
|
+
? designSystemLabel
|
|
194
|
+
: referenceDeckLabel
|
|
195
|
+
}
|
|
196
|
+
onClick={() =>
|
|
197
|
+
onSelect(
|
|
198
|
+
reference.kind === "design-system"
|
|
199
|
+
? {
|
|
200
|
+
designSystemId: reference.id,
|
|
201
|
+
referenceDeckId: null,
|
|
202
|
+
}
|
|
203
|
+
: {
|
|
204
|
+
designSystemId: null,
|
|
205
|
+
referenceDeckId: reference.id,
|
|
206
|
+
},
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
/>
|
|
210
|
+
))}
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
)}
|
|
214
|
+
|
|
215
|
+
{decks.length > 0 && (
|
|
216
|
+
<div className="grid gap-2">
|
|
217
|
+
<span className="text-xs font-medium text-muted-foreground">
|
|
218
|
+
{referenceDeckLabel}
|
|
219
|
+
</span>
|
|
220
|
+
<Select
|
|
221
|
+
onValueChange={(value) =>
|
|
222
|
+
onSelect({ designSystemId: null, referenceDeckId: value })
|
|
223
|
+
}
|
|
224
|
+
>
|
|
225
|
+
<SelectTrigger className="w-full">
|
|
226
|
+
<SelectValue placeholder={chooseDeckLabel} />
|
|
227
|
+
</SelectTrigger>
|
|
228
|
+
<SelectContent>
|
|
229
|
+
{starredDecks.length > 0 && (
|
|
230
|
+
<SelectGroup>
|
|
231
|
+
<SelectLabel>{starredLabel}</SelectLabel>
|
|
232
|
+
{starredDecks.map((deck) => (
|
|
233
|
+
<SelectItem key={deck.id} value={deck.id}>
|
|
234
|
+
{deck.title}
|
|
235
|
+
</SelectItem>
|
|
236
|
+
))}
|
|
237
|
+
</SelectGroup>
|
|
238
|
+
)}
|
|
239
|
+
{otherDecks.length > 0 && (
|
|
240
|
+
<SelectGroup>
|
|
241
|
+
<SelectLabel>{otherDecksLabel}</SelectLabel>
|
|
242
|
+
{otherDecks.map((deck) => (
|
|
243
|
+
<SelectItem key={deck.id} value={deck.id}>
|
|
244
|
+
{deck.title}
|
|
245
|
+
</SelectItem>
|
|
246
|
+
))}
|
|
247
|
+
</SelectGroup>
|
|
248
|
+
)}
|
|
249
|
+
</SelectContent>
|
|
250
|
+
</Select>
|
|
251
|
+
</div>
|
|
252
|
+
)}
|
|
253
|
+
|
|
254
|
+
<div className="flex flex-wrap items-center gap-2 border-t border-border pt-3">
|
|
255
|
+
<label
|
|
256
|
+
className={cn(
|
|
257
|
+
"inline-flex cursor-pointer items-center gap-2 rounded-md border border-border px-3 py-2 text-sm font-medium transition-colors hover:bg-accent",
|
|
258
|
+
importing && "pointer-events-none opacity-60",
|
|
259
|
+
)}
|
|
260
|
+
>
|
|
261
|
+
<IconUpload className="size-4" />
|
|
262
|
+
{importing ? importingLabel : importFileLabel}
|
|
263
|
+
<input
|
|
264
|
+
type="file"
|
|
265
|
+
className="sr-only"
|
|
266
|
+
accept=".pptx,.pdf,.docx"
|
|
267
|
+
multiple
|
|
268
|
+
disabled={importing}
|
|
269
|
+
onChange={(event) => void handleImport(event)}
|
|
270
|
+
/>
|
|
271
|
+
</label>
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
|
|
275
|
+
{promptNote && (
|
|
276
|
+
<p className="truncate border-t border-border pt-3 text-xs text-muted-foreground">
|
|
277
|
+
{promptNote}
|
|
278
|
+
</p>
|
|
279
|
+
)}
|
|
280
|
+
|
|
281
|
+
<div className="flex items-center justify-between border-t border-border pt-3">
|
|
282
|
+
<Button type="button" variant="ghost" onClick={onSkip}>
|
|
283
|
+
{skipLabel}
|
|
284
|
+
</Button>
|
|
285
|
+
<IconArrowRight className="size-4 text-muted-foreground" />
|
|
286
|
+
</div>
|
|
287
|
+
</DialogContent>
|
|
288
|
+
</Dialog>
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function ReferenceCard({
|
|
293
|
+
icon,
|
|
294
|
+
label,
|
|
295
|
+
meta,
|
|
296
|
+
onClick,
|
|
297
|
+
}: {
|
|
298
|
+
icon: ReactNode;
|
|
299
|
+
label: string;
|
|
300
|
+
meta: string;
|
|
301
|
+
onClick: () => void;
|
|
302
|
+
}) {
|
|
303
|
+
return (
|
|
304
|
+
<button
|
|
305
|
+
type="button"
|
|
306
|
+
onClick={onClick}
|
|
307
|
+
className="flex min-w-0 items-center gap-3 rounded-lg border border-border bg-card px-3 py-3 text-start transition-colors hover:border-foreground/30 hover:bg-accent/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
308
|
+
>
|
|
309
|
+
<span className="flex size-8 shrink-0 items-center justify-center rounded-md bg-accent text-muted-foreground">
|
|
310
|
+
{icon}
|
|
311
|
+
</span>
|
|
312
|
+
<span className="min-w-0">
|
|
313
|
+
<span className="block truncate text-sm font-medium">{label}</span>
|
|
314
|
+
<span className="block truncate text-xs text-muted-foreground">
|
|
315
|
+
{meta}
|
|
316
|
+
</span>
|
|
317
|
+
</span>
|
|
318
|
+
<IconArrowRight className="ms-auto size-4 shrink-0 text-muted-foreground" />
|
|
319
|
+
</button>
|
|
320
|
+
);
|
|
321
|
+
}
|
|
@@ -16,6 +16,37 @@ export interface UploadedFile {
|
|
|
16
16
|
size: number;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
export async function uploadPromptFiles(
|
|
20
|
+
files: File[],
|
|
21
|
+
): Promise<UploadedFile[]> {
|
|
22
|
+
if (files.length === 0) return [];
|
|
23
|
+
const formData = new FormData();
|
|
24
|
+
files.forEach((file) => formData.append("files", file));
|
|
25
|
+
const response = await fetch(`${appBasePath()}/api/uploads`, {
|
|
26
|
+
method: "POST",
|
|
27
|
+
body: formData,
|
|
28
|
+
});
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
let message = "Upload failed";
|
|
31
|
+
try {
|
|
32
|
+
const data: unknown = await response.json();
|
|
33
|
+
if (
|
|
34
|
+
data &&
|
|
35
|
+
typeof data === "object" &&
|
|
36
|
+
"error" in data &&
|
|
37
|
+
typeof data.error === "string" &&
|
|
38
|
+
data.error.trim()
|
|
39
|
+
) {
|
|
40
|
+
message = data.error;
|
|
41
|
+
}
|
|
42
|
+
} catch (error) {
|
|
43
|
+
throw new Error(`Upload failed (${response.status})`, { cause: error });
|
|
44
|
+
}
|
|
45
|
+
throw new Error(message);
|
|
46
|
+
}
|
|
47
|
+
return (await response.json()) as UploadedFile[];
|
|
48
|
+
}
|
|
49
|
+
|
|
19
50
|
/**
|
|
20
51
|
* Radix popovers portal to `document.body`, so a mousedown inside the model
|
|
21
52
|
* picker or attachment menu reads as "outside" any panel that hosts a composer.
|
|
@@ -137,17 +168,7 @@ export default function PromptPopover({
|
|
|
137
168
|
if (files.length === 0) return [];
|
|
138
169
|
setUploading(true);
|
|
139
170
|
try {
|
|
140
|
-
|
|
141
|
-
files.forEach((f) => formData.append("files", f));
|
|
142
|
-
const res = await fetch(`${appBasePath()}/api/uploads`, {
|
|
143
|
-
method: "POST",
|
|
144
|
-
body: formData,
|
|
145
|
-
});
|
|
146
|
-
if (!res.ok) {
|
|
147
|
-
const data = await res.json().catch(() => null);
|
|
148
|
-
throw new Error(data?.error || "Upload failed");
|
|
149
|
-
}
|
|
150
|
-
return (await res.json()) as UploadedFile[];
|
|
171
|
+
return await uploadPromptFiles(files);
|
|
151
172
|
} finally {
|
|
152
173
|
setUploading(false);
|
|
153
174
|
}
|