create-next-imagicma 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +0 -2
  2. package/package.json +1 -1
  3. package/template-hono/AGENTS.md +53 -95
  4. package/template-hono/README.md +3 -31
  5. package/template-hono/client/public/imagicma-picker-bridge.js +6 -2
  6. package/template-hono/client/public/imagicma-preview-feedback.js +1 -0
  7. package/template-hono/client/src/lib/imagicma-preview-bridge.ts +1 -1
  8. package/template-hono/client/src/lib/imagicma-preview-picker.ts +130 -28
  9. package/template-hono/vite.config.ts +1 -1
  10. package/template/.env.example +0 -10
  11. package/template/AGENTS.md +0 -225
  12. package/template/README.md +0 -34
  13. package/template/app/_components/DevPreviewShield.tsx +0 -638
  14. package/template/app/api/greeting/route.ts +0 -27
  15. package/template/app/error.tsx +0 -93
  16. package/template/app/favicon.ico +0 -0
  17. package/template/app/globals.css +0 -767
  18. package/template/app/hello/_components/HelloClient.tsx +0 -94
  19. package/template/app/hello/page.tsx +0 -23
  20. package/template/app/layout.tsx +0 -32
  21. package/template/app/page.tsx +0 -27
  22. package/template/app/providers.tsx +0 -25
  23. package/template/components/ui/accordion.tsx +0 -58
  24. package/template/components/ui/alert-dialog.tsx +0 -141
  25. package/template/components/ui/alert.tsx +0 -61
  26. package/template/components/ui/aspect-ratio.tsx +0 -7
  27. package/template/components/ui/avatar.tsx +0 -51
  28. package/template/components/ui/badge.tsx +0 -40
  29. package/template/components/ui/breadcrumb.tsx +0 -117
  30. package/template/components/ui/button.tsx +0 -64
  31. package/template/components/ui/calendar.tsx +0 -72
  32. package/template/components/ui/card.tsx +0 -87
  33. package/template/components/ui/carousel.tsx +0 -262
  34. package/template/components/ui/chart.tsx +0 -365
  35. package/template/components/ui/checkbox.tsx +0 -30
  36. package/template/components/ui/collapsible.tsx +0 -11
  37. package/template/components/ui/command.tsx +0 -153
  38. package/template/components/ui/context-menu.tsx +0 -200
  39. package/template/components/ui/dialog.tsx +0 -122
  40. package/template/components/ui/drawer.tsx +0 -118
  41. package/template/components/ui/dropdown-menu.tsx +0 -200
  42. package/template/components/ui/form.tsx +0 -178
  43. package/template/components/ui/hover-card.tsx +0 -29
  44. package/template/components/ui/input-otp.tsx +0 -71
  45. package/template/components/ui/input.tsx +0 -25
  46. package/template/components/ui/label.tsx +0 -26
  47. package/template/components/ui/menubar.tsx +0 -256
  48. package/template/components/ui/navigation-menu.tsx +0 -130
  49. package/template/components/ui/pagination.tsx +0 -119
  50. package/template/components/ui/popover.tsx +0 -31
  51. package/template/components/ui/progress.tsx +0 -28
  52. package/template/components/ui/radio-group.tsx +0 -44
  53. package/template/components/ui/resizable.tsx +0 -45
  54. package/template/components/ui/scroll-area.tsx +0 -48
  55. package/template/components/ui/select.tsx +0 -160
  56. package/template/components/ui/separator.tsx +0 -31
  57. package/template/components/ui/sheet.tsx +0 -140
  58. package/template/components/ui/sidebar.tsx +0 -732
  59. package/template/components/ui/skeleton.tsx +0 -17
  60. package/template/components/ui/slider.tsx +0 -28
  61. package/template/components/ui/switch.tsx +0 -29
  62. package/template/components/ui/table.tsx +0 -119
  63. package/template/components/ui/tabs.tsx +0 -55
  64. package/template/components/ui/textarea.tsx +0 -24
  65. package/template/components/ui/toast.tsx +0 -129
  66. package/template/components/ui/toaster.tsx +0 -35
  67. package/template/components/ui/toggle-group.tsx +0 -61
  68. package/template/components/ui/toggle.tsx +0 -45
  69. package/template/components/ui/tooltip.tsx +0 -30
  70. package/template/drizzle.config.ts +0 -50
  71. package/template/eslint.config.mjs +0 -18
  72. package/template/gitignore +0 -46
  73. package/template/hooks/use-greeting.ts +0 -15
  74. package/template/hooks/use-mobile.ts +0 -21
  75. package/template/hooks/use-toast.ts +0 -194
  76. package/template/lib/queryClient.ts +0 -59
  77. package/template/lib/theme/default-theme.ts +0 -11
  78. package/template/lib/utils.ts +0 -6
  79. package/template/next.config.ts +0 -8
  80. package/template/package.json +0 -76
  81. package/template/pnpm-lock.yaml +0 -6937
  82. package/template/postcss.config.mjs +0 -7
  83. package/template/process-compose.yaml +0 -13
  84. package/template/public/file.svg +0 -1
  85. package/template/public/globe.svg +0 -1
  86. package/template/public/imagicma-picker-bridge.js +0 -374
  87. package/template/public/next.svg +0 -1
  88. package/template/public/vercel.svg +0 -1
  89. package/template/public/window.svg +0 -1
  90. package/template/server/db.ts +0 -24
  91. package/template/server/storage.ts +0 -41
  92. package/template/shared/routes.ts +0 -13
  93. package/template/shared/schema.ts +0 -17
  94. package/template/tailwind.config.mjs +0 -96
  95. package/template/tsconfig.json +0 -35
  96. package/template/types/pg.d.ts +0 -19
package/README.md CHANGED
@@ -4,7 +4,6 @@
4
4
 
5
5
  ## 模版源
6
6
 
7
- - `../nextjs-app`
8
7
  - `../hono-app`
9
8
  如果要修改模版,请修改源头,然后执行 `pnpm run sync-template` 同步到当前模版目录,切记不可直接修改当前模版目录。
10
9
  ## 使用
@@ -50,7 +49,6 @@ pnpm run sync-template
50
49
 
51
50
  默认同步:
52
51
 
53
- - `../nextjs-app` -> `template/`
54
52
  - `../hono-app` -> `template-hono/`
55
53
 
56
54
  可通过环境变量覆盖:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-next-imagicma",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "create-next-imagicma": "./bin/create-next-imagicma.mjs"
@@ -1,95 +1,53 @@
1
- # 项目 Agent 指南(Hono + Vite + React + Drizzle)
2
-
3
- 默认使用中文沟通与输出(除非用户明确要求其他语言)。
4
-
5
- ## 目标
6
-
7
- 你需要一次性交付可运行、可预览、可验证、可演示的完整应用,而不是半成品。
8
-
9
- ## 技术栈与约束
10
-
11
- - 框架:Hono(Node runtime)+ Vite(单进程开发)
12
- - 前端:React 19 + React Router + Tailwind v4 + shadcn/ui
13
- - 请求层:React Query + fetch
14
- - 契约:Zod(`shared/routes.ts`)
15
- - 数据库:SQLite + Drizzle(默认 `./.data/app.db`,无需 `DATABASE_URL`;可用 `DATABASE_FILE` 覆盖)
16
-
17
- ## 目录约定
18
-
19
- - `client/index.html`:Vite 前端入口 HTML
20
- - `client/public/`:前端静态资源
21
- - `client/src/`:前端应用入口、页面、Provider、错误边界
22
- - `client/src/components/ui/`:shadcn/ui 组件
23
- - `client/src/hooks/`:客户端 hooks
24
- - `client/src/lib/`:通用工具
25
- - `server/`:Hono 入口、路由、存储、DB
26
- - `shared/`:前后端共享 schema/契约
27
-
28
- ## 常用命令
29
-
30
- - `pnpm dev`:单进程开发(Vite + Hono dev middleware)
31
- - `pnpm build`:构建前端并编译 server
32
- - `pnpm start`:生产启动
33
- - `pnpm check`:类型检查
34
- - `pnpm lint`:ESLint
35
- - `pnpm db:push`:同步数据库结构
36
-
37
- ## 执行硬规则
38
-
39
- - 不阻塞就不要问用户,直接推进到完成。
40
- - 不要要求用户发送“继续执行/不要停在总结”之类控制语句,把持续推进作为默认行为。
41
- - 进度反馈用自然里程碑播报,不使用突兀命令式话术。
42
- - 仅在真实阻塞时才提问,并附已尝试步骤与日志证据。
43
- - 未达到完成标准前,不得用“文档总结/状态总结”代替执行。
44
- - 不得只做前端占位,必须打通真实 API 与数据读写链路。
45
- - 每次代码改动后必须重启服务并验证页面/接口。
46
- - 关键改动后必须跑端到端验证(如 `run_test`)并检查日志。
47
- - 禁止递归调用测试代理或无限循环调用同一工具。
48
- - 如果 `run_test` 结果不是 `[run_test] ok`,必须视为未通过,继续修复。
49
- - `run_test` 卡住或无进展时要快速失败并给出证据,禁止长时间空转重试。
50
- - 不允许“口头完成”。完成声明必须有工具证据支撑。
51
- - SQLite 路径必须保持单一 schema 真相源,禁止手写临时 SQL 与 ORM schema 并行漂移。
52
- - 禁止修改 `scripts/` 下的受保护启动文件:`imagicma-common.mjs`、`imagicma-guard.mjs`、`imagicma-dev.mjs`、`imagicma-start.mjs`、`imagicma-runtime-logs.mjs`。
53
- - 禁止修改 `package.json` `scripts.dev` `scripts.start`(以及对应 `predev`、`prestart`)命令。
54
- - 禁止直接执行 `vite` 或 `node dist/server/index.js` 或 `pnpm dev` 或 `pnpm start` 启动项目;只能通过 `restart_workflow` 启动。
55
- - 禁止主动注入环境变量到 `process.env`。
56
- - 端口契约只允许使用大写 `PORT`。
57
- - 运行时端口读取顺序固定为:外部环境变量 `PORT` -> 项目内 `.imagicma/runtime.env`。
58
- - 禁止写入 `/.imagicma/port.json`,禁止在 `process-compose.yaml` 中固化端口真相源。
59
-
60
- ## 状态文件写入规则
61
-
62
- - 若维护 `docs/project_state.json`,每次写入前先读取最新版本。
63
- - 若写入报冲突(文件已变更),必须重新读取后重试,不得忽略。
64
- - `quality_gates.typecheck=true` 仅在构建命令真实通过后设置。
65
-
66
- ## UI 质量门禁(必须全部满足)
67
-
68
- - `/` 路由必须可访问并落到首页组件。
69
- - `client/src/pages/home.tsx` 不允许为空文件、空组件,或仅返回 `null` / 空白占位;首页必须渲染可见内容。
70
- - 接入真实业务时,应将 `/` 首页替换为真实业务内容,不要长期保留模板默认文案或空白首页。
71
- - 视觉风格必须明确:字体、颜色、间距、动效要统一。
72
- - 必须定义可复用设计 token(颜色、圆角、阴影、间距等级)。
73
- - 至少覆盖桌面与移动端关键断点,不允许内容溢出。
74
- - 交互状态完整:hover/focus/disabled/loading/error/success。
75
- - 在 `<Button>` 上使用自定义背景色类(如 `bg-*`)时,必须同时显式声明文本色类(如 `text-*`);禁止只改背景不改文字色。
76
- - 优先复用 `Input/Button/Card/Checkbox/Select` 的默认视觉语言,必要时只做局部增强,不要重写整套样式。
77
- - 列表增删改状态建议加入轻量动效(如 `framer-motion` 的入场/退出),并提供失败反馈(toast 或 inline alert)。
78
-
79
- ## 数据与安全
80
-
81
- - 服务端数据库逻辑放在 `server/`,不要泄漏到客户端。
82
- - API 响应必须经过 `shared/routes.ts` 的 Zod schema 校验。
83
- - 不提交 `.env.local`、数据库密钥。
84
- - 优先复用 `client/src/components/ui` 与 `client/src/hooks`,避免重复造轮子。
85
-
86
- ## 完成标准
87
-
88
- 只有以下全部满足才允许结束:
89
-
90
- - `pnpm build` 通过(无 pnpm 时 `npm run build` 通过)
91
- - 页面可在本地端口访问并完成核心流程
92
- - `/` 首页路由可访问(允许临时隐藏默认模板 DOM)
93
- - `run_test` 返回 `[run_test] ok`
94
- - 关键日志无阻塞级错误
95
- - UI 达到可演示级(非“功能可用但观感粗糙”)
1
+ # Repository Guidelines
2
+
3
+ ## Project Structure & Module Organization
4
+
5
+ This is a `Hono + Vite + React` app. Keep browser code in `client/src`, server entrypoints in `server/`, and shared contracts in `shared/`.
6
+
7
+ - `client/src/pages/`: route-level pages such as `home.tsx`
8
+ - `client/src/components/`: reusable UI, with shadcn/ui primitives under `components/ui/`
9
+ - `client/src/lib/` and `client/src/hooks/`: client utilities and hooks
10
+ - `server/routes/`: Hono route modules
11
+ - `shared/routes.ts` and `shared/schema.ts`: Zod-backed API contracts and shared schema
12
+ - `scripts/`: guarded dev/start scripts; do not bypass them
13
+ - `.data/`: local SQLite data; treat as runtime state, not source
14
+
15
+ The homepage must never ship blank. `client/src/pages/home.tsx` should render visible, meaningful content on first load, not hidden placeholders or empty shells.
16
+
17
+ ## Build, Test, and Development Commands
18
+
19
+ - `restart_workflow`: required agent entrypoint for restarting dev services; do not call `pnpm dev` directly
20
+ - `pnpm build`: builds the client with Vite and compiles the server with `tsc`
21
+ - `pnpm start`: runs the production server through the guarded startup script
22
+ - `pnpm check`: TypeScript type-check for both client and server configs
23
+ - `pnpm lint`: runs ESLint across the repository
24
+ - `pnpm db:push`: applies Drizzle schema changes to the local SQLite database
25
+
26
+ ## Coding Style & Naming Conventions
27
+
28
+ Use TypeScript and ESM throughout. Follow the existing style: 2-space indentation, double quotes, semicolons, and trailing commas. Prefer:
29
+
30
+ - PascalCase for React components, for example `HelloClient.tsx`
31
+ - kebab-case for route/page files, for example `not-found.tsx`
32
+ - colocated logic in `hooks/`, `lib/`, and `routes/` before creating new top-level folders
33
+
34
+ Reuse the `@/` import alias for client code where it improves readability.
35
+
36
+ ## Testing Guidelines
37
+
38
+ This template does not currently ship with a committed automated test runner or `pnpm test` script. Before opening a PR, run `pnpm check` and `pnpm lint`, then manually verify the main page and `GET /api/greeting`.
39
+
40
+ If you add tests, keep them as `*.test.ts` or `*.test.tsx`, or add a `tests/` folder and document the new command in `package.json`.
41
+
42
+ ## Commit & Pull Request Guidelines
43
+
44
+ Recent history uses Conventional Commit prefixes such as `feat:`, `fix:`, and `chore:`; keep that format and write concise subjects. PRs should include:
45
+
46
+ - a short description of behavior changes
47
+ - linked issue or task when available
48
+ - screenshots or recordings for UI changes
49
+ - notes for env, port, or database-impacting changes
50
+
51
+ ## Runtime & Configuration Notes
52
+
53
+ Agents must use `restart_workflow` instead of invoking `pnpm dev` manually. Do not pass a port on the command line or by prefixing `pnpm dev` with `PORT=...`; keep runtime port configuration in `.imagicma/runtime.env`. Database defaults to `./.data/app.db`, and overrides belong in `.env.local`, not committed secrets.
@@ -1,6 +1,6 @@
1
1
  # hono-app
2
2
 
3
- 基于 **Hono + Vite + React** 的全栈模板,目标是保留原 `nextjs-app` 的前端体验,同时降低服务端运行开销。
3
+ 基于 **Hono + Vite + React** 的全栈应用。
4
4
 
5
5
  ## 技术栈
6
6
 
@@ -23,7 +23,7 @@
23
23
 
24
24
  ```bash
25
25
  pnpm install
26
- pnpm dev
26
+ start_app.sh
27
27
  ```
28
28
 
29
29
  启动时按以下顺序解析端口:
@@ -38,14 +38,7 @@ pnpm dev
38
38
  PORT=6424
39
39
  ```
40
40
 
41
- 请勿直接执行 `vite` 启动,必须使用 `pnpm dev`。
42
-
43
- ## 构建与运行
44
-
45
- ```bash
46
- pnpm build
47
- pnpm start
48
- ```
41
+ 请勿直接执行 `vite` `pnpm dev` 启动,必须使用 `restart_workflow`。
49
42
 
50
43
  - `build` 产出:
51
44
  - 前端:`dist/client`
@@ -67,29 +60,8 @@ pnpm db:push
67
60
  ## 运行时文件
68
61
 
69
62
  - `.imagicma/runtime.env`:项目运行端口契约,只使用大写 `PORT`
70
- - `/.imagicma/port.json`:已废弃,不再使用
71
63
 
72
64
  ## 验证路径
73
65
 
74
66
  - API:`GET /api/greeting`
75
67
  - 页面:`/hello`
76
-
77
- ## E2E 与 `run_test`
78
-
79
- - Playwright 配置位于 [`playwright.config.ts`](/Users/alexliu/Project/imagicma-all/imagicma-template/hono-app/playwright.config.ts)
80
- - 测试契约位于 [`hono-app/.imagicma/testing-manifest.json`](/Users/alexliu/Project/imagicma-all/imagicma-template/hono-app/.imagicma/testing-manifest.json)
81
- - 本地回归命令:
82
-
83
- ```bash
84
- pnpm test:e2e
85
- ```
86
-
87
- - `run_test` 驱动 Playwright 时会注入:
88
- - `IMAGICMA_RUN_ID`
89
- - `IMAGICMA_RUN_TEST_RUNTIME_ROOT`
90
- - `IMAGICMA_RUN_TEST_SKIP_WEBSERVER`
91
- - `BASE_URL`
92
-
93
- fixtures 约定:
94
- - `tests/e2e/fixtures/imagicma.ts` 必须把浏览器 warning/error、page errors、失败网络请求与 current URL 写入 `IMAGICMA_RUN_TEST_RUNTIME_ROOT`
95
- - `testing-manifest.json` 负责声明稳定页面、selectors、fixtures 与 auth profile,供 `run_test` 编译阶段使用
@@ -3,7 +3,11 @@
3
3
  var VERSION = 1;
4
4
  var MAX_TEXT_LENGTH = 240;
5
5
  var MAX_SELECTOR_LENGTH = 512;
6
- var PROD_PARENT_ORIGIN = 'https://imagicma.cn';
6
+ var PROD_PARENT_ORIGINS = {
7
+ 'https://agentma.cn': true,
8
+ 'https://imagicma.cn': true,
9
+ 'https://nanocoda.com': true,
10
+ };
7
11
  var LOCAL_PARENT_RE = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/i;
8
12
  var LOCAL_IMAGICMA_PARENT_RE = /^https?:\/\/([a-z0-9-]+\.)?local\.imagicma\.cn(:\d+)?$/i;
9
13
 
@@ -21,7 +25,7 @@
21
25
 
22
26
  function isAllowedParentOrigin(origin) {
23
27
  if (!origin) return false;
24
- return origin === PROD_PARENT_ORIGIN || LOCAL_PARENT_RE.test(origin) || LOCAL_IMAGICMA_PARENT_RE.test(origin);
28
+ return !!PROD_PARENT_ORIGINS[origin] || LOCAL_PARENT_RE.test(origin) || LOCAL_IMAGICMA_PARENT_RE.test(origin);
25
29
  }
26
30
 
27
31
  function isRecord(value) {
@@ -4,6 +4,7 @@
4
4
  var PROD_PARENT_ORIGINS = {
5
5
  'https://agentma.cn': true,
6
6
  'https://imagicma.cn': true,
7
+ 'https://nanocoda.com': true
7
8
  };
8
9
  var LOCAL_PARENT_RE = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/i;
9
10
  var LOCAL_IMAGICMA_PARENT_RE = /^https?:\/\/([a-z0-9-]+\.)?local\.(agentma\.cn|imagicma\.cn)(:\d+)?$/i;
@@ -1,4 +1,4 @@
1
- const PROD_PARENT_ORIGINS = new Set(["https://agentma.cn", "https://imagicma.cn"]);
1
+ const PROD_PARENT_ORIGINS = new Set(["https://agentma.cn", "https://imagicma.cn", "https://nanocoda.com"]);
2
2
  const LOCAL_PARENT_RE = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/i;
3
3
  const LOCAL_IMAGICMA_PARENT_RE = /^https?:\/\/([a-z0-9-]+\.)?local\.(agentma\.cn|imagicma\.cn)(:\d+)?$/i;
4
4
 
@@ -142,6 +142,8 @@ type PreviewPickerStateSyncPayload = {
142
142
  pageKey: string;
143
143
  overrides: PreviewOverridePageEntry | null;
144
144
  selectedNodeId: string | null;
145
+ selectedTextEditable: boolean;
146
+ draftTargetScope: PreviewDraftTargetScope;
145
147
  draft: PreviewDraftApplyPayload | null;
146
148
  drafts: PreviewDraftApplyPayload[];
147
149
  pendingSort: PreviewSortPreviewPayload | null;
@@ -167,6 +169,19 @@ type PickerMessage =
167
169
  sessionId: string | null;
168
170
  payload: PreviewPickerStateSyncPayload;
169
171
  }
172
+ | {
173
+ channel: typeof PREVIEW_PICKER_CHANNEL;
174
+ version: typeof PREVIEW_PICKER_VERSION;
175
+ type: "IMAGICMA_PICKER_DRAFT_PATCH";
176
+ frameInstanceId: string;
177
+ sessionId: string | null;
178
+ payload?: {
179
+ nodeId?: string;
180
+ patch?: {
181
+ textContent?: string;
182
+ };
183
+ };
184
+ }
170
185
  | {
171
186
  channel: typeof PREVIEW_PICKER_CHANNEL;
172
187
  version: typeof PREVIEW_PICKER_VERSION;
@@ -193,6 +208,8 @@ type RuntimeState = {
193
208
  selectedSiblingBoxEls: HTMLDivElement[];
194
209
  draftStyleEl: HTMLStyleElement | null;
195
210
  persistedOverrides: PreviewOverridePageEntry | null;
211
+ selectedTextEditable: boolean;
212
+ draftTargetScope: PreviewDraftTargetScope;
196
213
  draftPayload: PreviewDraftApplyPayload | null;
197
214
  draftPayloads: PreviewDraftApplyPayload[];
198
215
  pendingSort: { groupKey: string; orderedSortKeys: readonly string[] } | null;
@@ -215,6 +232,10 @@ type RuntimeState = {
215
232
  throttledRecalculate: (() => void) | null;
216
233
  mutationObserver: MutationObserver | null;
217
234
  suppressMutationObserver: number;
235
+ inlineEditingElement: HTMLElement | null;
236
+ inlineEditingAbortController: AbortController | null;
237
+ inlineEditingOriginalContentEditable: string | null;
238
+ inlineEditingOriginalSpellcheck: boolean;
218
239
  };
219
240
 
220
241
  type SortableMetadata = {
@@ -503,14 +524,6 @@ function getElementNodeId(element: HTMLElement | null | undefined): string {
503
524
  return trimText(element?.getAttribute("data-imagicma-node-id"));
504
525
  }
505
526
 
506
- function getRepeatItemScopeKey(element: HTMLElement | null | undefined): string | null {
507
- const repeatRoot = element ? getRepeatItemRoot(element) : null;
508
- const groupKey = trimText(repeatRoot?.getAttribute("data-imagicma-repeat-group"));
509
- const itemKey = trimText(repeatRoot?.getAttribute("data-imagicma-sort-key"));
510
- if (!groupKey || !itemKey) return null;
511
- return `${groupKey}::${itemKey}`;
512
- }
513
-
514
527
  function getIndexedElementsByNodeKey(state: RuntimeState, nodeKey: string): HTMLElement[] {
515
528
  const cached = state.nodeIdIndex.get(nodeKey)?.filter((element) => element.isConnected) ?? [];
516
529
  if (cached.length > 0) return cached;
@@ -522,26 +535,13 @@ function getIndexedElementsByNodeKey(state: RuntimeState, nodeKey: string): HTML
522
535
  );
523
536
  }
524
537
 
525
- function isRepeatScopedNodeIdentity(peers: HTMLElement[]): boolean {
526
- if (peers.length <= 1) return false;
527
-
528
- const seenScopes = new Set<string>();
529
- for (const peer of peers) {
530
- const scopeKey = getRepeatItemScopeKey(peer);
531
- if (!scopeKey || seenScopes.has(scopeKey)) {
532
- return false;
533
- }
534
- seenScopes.add(scopeKey);
535
- }
536
-
537
- return seenScopes.size === peers.length;
538
- }
539
-
540
538
  function getDirectSemanticSiblingItems(parent: HTMLElement | null): HTMLElement[] {
541
539
  if (!parent) return [];
542
540
 
543
541
  const items = Array.from(parent.children).filter((child): child is HTMLElement => {
544
- return child instanceof HTMLElement && Boolean(getElementNodeId(child));
542
+ return child instanceof HTMLElement
543
+ && Boolean(getElementNodeId(child))
544
+ && trimText(child.getAttribute("data-imagicma-kind")) === "repeat-item";
545
545
  });
546
546
 
547
547
  if (items.length < 2) return [];
@@ -559,6 +559,10 @@ function getSyntheticSortableParentId(parent: HTMLElement | null): string | null
559
559
  }
560
560
 
561
561
  function getSiblingSortableMetadata(element: HTMLElement): SortableMetadata | null {
562
+ if (trimText(element.getAttribute("data-imagicma-kind")) !== "repeat-item") {
563
+ return null;
564
+ }
565
+
562
566
  const nodeId = getElementNodeId(element);
563
567
  if (!nodeId) return null;
564
568
 
@@ -1690,6 +1694,92 @@ function applyCurrentDraftToSelectedElement(state: RuntimeState) {
1690
1694
  });
1691
1695
  }
1692
1696
 
1697
+ function disableInlineTextEditing(state: RuntimeState) {
1698
+ const element = state.inlineEditingElement;
1699
+ state.inlineEditingAbortController?.abort();
1700
+ state.inlineEditingAbortController = null;
1701
+
1702
+ if (element?.isConnected) {
1703
+ if (state.inlineEditingOriginalContentEditable === null) {
1704
+ element.removeAttribute("contenteditable");
1705
+ } else {
1706
+ element.setAttribute("contenteditable", state.inlineEditingOriginalContentEditable);
1707
+ }
1708
+ element.spellcheck = state.inlineEditingOriginalSpellcheck;
1709
+ element.removeAttribute("data-imagicma-inline-editing");
1710
+ }
1711
+
1712
+ state.inlineEditingElement = null;
1713
+ state.inlineEditingOriginalContentEditable = null;
1714
+ state.inlineEditingOriginalSpellcheck = false;
1715
+ }
1716
+
1717
+ function moveCaretToEnd(element: HTMLElement) {
1718
+ const selection = window.getSelection();
1719
+ if (!selection) return;
1720
+ const range = document.createRange();
1721
+ range.selectNodeContents(element);
1722
+ range.collapse(false);
1723
+ selection.removeAllRanges();
1724
+ selection.addRange(range);
1725
+ }
1726
+
1727
+ function shouldEnableInlineTextEditing(state: RuntimeState, element: HTMLElement | null): element is HTMLElement {
1728
+ if (!state.enabled || !element || element !== state.selectedElement) return false;
1729
+ if (state.draftTargetScope !== "single") return false;
1730
+ if (!state.selectedTextEditable) return false;
1731
+ if (!isSimpleTextElement(element)) return false;
1732
+ return true;
1733
+ }
1734
+
1735
+ function enableInlineTextEditing(state: RuntimeState, element: HTMLElement) {
1736
+ if (state.inlineEditingElement === element) return;
1737
+ disableInlineTextEditing(state);
1738
+
1739
+ state.inlineEditingElement = element;
1740
+ state.inlineEditingOriginalContentEditable = element.getAttribute("contenteditable");
1741
+ state.inlineEditingOriginalSpellcheck = element.spellcheck;
1742
+
1743
+ const abortController = new AbortController();
1744
+ state.inlineEditingAbortController = abortController;
1745
+
1746
+ element.setAttribute("contenteditable", "plaintext-only");
1747
+ element.setAttribute("data-imagicma-inline-editing", "true");
1748
+ element.spellcheck = false;
1749
+
1750
+ element.addEventListener("input", () => {
1751
+ postToParent(state, {
1752
+ channel: PREVIEW_PICKER_CHANNEL,
1753
+ version: PREVIEW_PICKER_VERSION,
1754
+ type: "IMAGICMA_PICKER_DRAFT_PATCH",
1755
+ frameInstanceId: state.frameInstanceId,
1756
+ sessionId: state.activeSessionId,
1757
+ payload: {
1758
+ nodeId: getElementNodeId(element),
1759
+ patch: {
1760
+ textContent: element.textContent || "",
1761
+ },
1762
+ },
1763
+ });
1764
+ }, { signal: abortController.signal });
1765
+
1766
+ window.requestAnimationFrame(() => {
1767
+ if (state.inlineEditingElement !== element) return;
1768
+ if (document.activeElement !== element) {
1769
+ element.focus({ preventScroll: true });
1770
+ moveCaretToEnd(element);
1771
+ }
1772
+ });
1773
+ }
1774
+
1775
+ function syncInlineTextEditing(state: RuntimeState) {
1776
+ if (shouldEnableInlineTextEditing(state, state.selectedElement)) {
1777
+ enableInlineTextEditing(state, state.selectedElement);
1778
+ return;
1779
+ }
1780
+ disableInlineTextEditing(state);
1781
+ }
1782
+
1693
1783
  function applySortPreview(state: RuntimeState, items: HTMLElement[], orderedKeys: readonly string[]) {
1694
1784
  if (items.length < 2 || orderedKeys.length === 0) return;
1695
1785
 
@@ -1771,6 +1861,7 @@ function reapplyVisualState(state: RuntimeState) {
1771
1861
  applyTextOverrides(state, state.persistedOverrides, state.draftPayloads);
1772
1862
  applyCurrentDraftToSelectedElement(state);
1773
1863
  applySortOverrides(state, state.persistedOverrides, state.pendingSort);
1864
+ syncInlineTextEditing(state);
1774
1865
 
1775
1866
  const hoverTarget = state.hoveredElement && state.hoveredElement !== state.selectedElement
1776
1867
  ? state.hoveredElement
@@ -1792,12 +1883,10 @@ function reapplyVisualState(state: RuntimeState) {
1792
1883
  }
1793
1884
  }
1794
1885
 
1795
- function isNodeSelectableElement(state: RuntimeState, element: HTMLElement): boolean {
1886
+ function isNodeSelectableElement(_state: RuntimeState, element: HTMLElement): boolean {
1796
1887
  const nodeId = getElementNodeId(element);
1797
1888
  if (!nodeId) return false;
1798
-
1799
- const peers = getIndexedElementsByNodeKey(state, nodeId);
1800
- return peers.length <= 1 || isRepeatScopedNodeIdentity(peers);
1889
+ return true;
1801
1890
  }
1802
1891
 
1803
1892
  function isOverlayElement(state: RuntimeState, element: HTMLElement): boolean {
@@ -1879,6 +1968,8 @@ function createRuntimeState(): RuntimeState {
1879
1968
  selectedSiblingBoxEls: [],
1880
1969
  draftStyleEl: null,
1881
1970
  persistedOverrides: null,
1971
+ selectedTextEditable: false,
1972
+ draftTargetScope: "single",
1882
1973
  draftPayload: null,
1883
1974
  draftPayloads: [],
1884
1975
  pendingSort: null,
@@ -1897,6 +1988,10 @@ function createRuntimeState(): RuntimeState {
1897
1988
  throttledRecalculate: null,
1898
1989
  mutationObserver: null,
1899
1990
  suppressMutationObserver: 0,
1991
+ inlineEditingElement: null,
1992
+ inlineEditingAbortController: null,
1993
+ inlineEditingOriginalContentEditable: null,
1994
+ inlineEditingOriginalSpellcheck: false,
1900
1995
  };
1901
1996
  }
1902
1997
 
@@ -2104,6 +2199,8 @@ export function installPreviewPickerRuntime() {
2104
2199
  state.activeSessionId = event.data.sessionId;
2105
2200
  state.enabled = event.data.payload.mode === "picking";
2106
2201
  state.persistedOverrides = event.data.payload.overrides;
2202
+ state.selectedTextEditable = event.data.payload.selectedTextEditable === true;
2203
+ state.draftTargetScope = event.data.payload.draftTargetScope;
2107
2204
  state.draftPayload = event.data.payload.draft;
2108
2205
  state.draftPayloads = event.data.payload.drafts ?? (event.data.payload.draft ? [event.data.payload.draft] : []);
2109
2206
  state.pendingSort = event.data.payload.pendingSort
@@ -2121,6 +2218,8 @@ export function installPreviewPickerRuntime() {
2121
2218
  sessionId: state.activeSessionId,
2122
2219
  mode: event.data.payload.mode,
2123
2220
  selectedNodeId: event.data.payload.selectedNodeId,
2221
+ selectedTextEditable: event.data.payload.selectedTextEditable,
2222
+ draftTargetScope: event.data.payload.draftTargetScope,
2124
2223
  hasDraft: Boolean(event.data.payload.draft),
2125
2224
  hasPendingSort: Boolean(event.data.payload.pendingSort),
2126
2225
  pageKey: event.data.payload.pageKey,
@@ -2134,9 +2233,12 @@ export function installPreviewPickerRuntime() {
2134
2233
  state.activeSessionId = null;
2135
2234
  state.selectedElement = null;
2136
2235
  state.hoveredElement = null;
2236
+ state.selectedTextEditable = false;
2237
+ state.draftTargetScope = "single";
2137
2238
  state.draftPayload = null;
2138
2239
  state.draftPayloads = [];
2139
2240
  state.pendingSort = null;
2241
+ disableInlineTextEditing(state);
2140
2242
  clearOverlay(state);
2141
2243
  applySortableAffordance(state, false);
2142
2244
  logPreviewPickerRuntime("stop-session", {
@@ -126,7 +126,7 @@ export default defineConfig(async ({ command }) => {
126
126
  : {
127
127
  host: "0.0.0.0",
128
128
  port: runtimePort,
129
- allowedHosts: ["localhost", "127.0.0.1", ".preview.imagicma.cn", ".preview.agentma.cn", ".preview.agentma.com"],
129
+ allowedHosts: ["localhost", "127.0.0.1", ".preview.imagicma.cn", ".preview.agentma.cn", ".preview.agentma.com", ".preview.nanocoda.com"],
130
130
  strictPort: true,
131
131
  hmr: { overlay: false },
132
132
  },
@@ -1,10 +0,0 @@
1
- # 复制为 .env.local 并填入真实值(.env.local 不会被提交)
2
- #
3
- # Postgres 连接串(Drizzle + pg 使用)
4
- # 示例:
5
- # DATABASE_URL="postgres://user:pass@localhost:5432/mydb"
6
- DATABASE_URL=""
7
-
8
- # 可选:运行端口(某些平台/容器会要求读取 PORT,例如 Replit 默认 5000)
9
- # PORT=5000
10
-