create-next-imagicma 0.1.11 → 0.1.12
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/README.md +0 -2
- package/package.json +1 -1
- package/template-hono/AGENTS.md +53 -95
- package/template-hono/README.md +3 -31
- package/template-hono/client/src/lib/imagicma-preview-picker.ts +130 -28
- package/template/.env.example +0 -10
- package/template/AGENTS.md +0 -225
- package/template/README.md +0 -34
- package/template/app/_components/DevPreviewShield.tsx +0 -638
- package/template/app/api/greeting/route.ts +0 -27
- package/template/app/error.tsx +0 -93
- package/template/app/favicon.ico +0 -0
- package/template/app/globals.css +0 -767
- package/template/app/hello/_components/HelloClient.tsx +0 -94
- package/template/app/hello/page.tsx +0 -23
- package/template/app/layout.tsx +0 -32
- package/template/app/page.tsx +0 -27
- package/template/app/providers.tsx +0 -25
- package/template/components/ui/accordion.tsx +0 -58
- package/template/components/ui/alert-dialog.tsx +0 -141
- package/template/components/ui/alert.tsx +0 -61
- package/template/components/ui/aspect-ratio.tsx +0 -7
- package/template/components/ui/avatar.tsx +0 -51
- package/template/components/ui/badge.tsx +0 -40
- package/template/components/ui/breadcrumb.tsx +0 -117
- package/template/components/ui/button.tsx +0 -64
- package/template/components/ui/calendar.tsx +0 -72
- package/template/components/ui/card.tsx +0 -87
- package/template/components/ui/carousel.tsx +0 -262
- package/template/components/ui/chart.tsx +0 -365
- package/template/components/ui/checkbox.tsx +0 -30
- package/template/components/ui/collapsible.tsx +0 -11
- package/template/components/ui/command.tsx +0 -153
- package/template/components/ui/context-menu.tsx +0 -200
- package/template/components/ui/dialog.tsx +0 -122
- package/template/components/ui/drawer.tsx +0 -118
- package/template/components/ui/dropdown-menu.tsx +0 -200
- package/template/components/ui/form.tsx +0 -178
- package/template/components/ui/hover-card.tsx +0 -29
- package/template/components/ui/input-otp.tsx +0 -71
- package/template/components/ui/input.tsx +0 -25
- package/template/components/ui/label.tsx +0 -26
- package/template/components/ui/menubar.tsx +0 -256
- package/template/components/ui/navigation-menu.tsx +0 -130
- package/template/components/ui/pagination.tsx +0 -119
- package/template/components/ui/popover.tsx +0 -31
- package/template/components/ui/progress.tsx +0 -28
- package/template/components/ui/radio-group.tsx +0 -44
- package/template/components/ui/resizable.tsx +0 -45
- package/template/components/ui/scroll-area.tsx +0 -48
- package/template/components/ui/select.tsx +0 -160
- package/template/components/ui/separator.tsx +0 -31
- package/template/components/ui/sheet.tsx +0 -140
- package/template/components/ui/sidebar.tsx +0 -732
- package/template/components/ui/skeleton.tsx +0 -17
- package/template/components/ui/slider.tsx +0 -28
- package/template/components/ui/switch.tsx +0 -29
- package/template/components/ui/table.tsx +0 -119
- package/template/components/ui/tabs.tsx +0 -55
- package/template/components/ui/textarea.tsx +0 -24
- package/template/components/ui/toast.tsx +0 -129
- package/template/components/ui/toaster.tsx +0 -35
- package/template/components/ui/toggle-group.tsx +0 -61
- package/template/components/ui/toggle.tsx +0 -45
- package/template/components/ui/tooltip.tsx +0 -30
- package/template/drizzle.config.ts +0 -50
- package/template/eslint.config.mjs +0 -18
- package/template/gitignore +0 -46
- package/template/hooks/use-greeting.ts +0 -15
- package/template/hooks/use-mobile.ts +0 -21
- package/template/hooks/use-toast.ts +0 -194
- package/template/lib/queryClient.ts +0 -59
- package/template/lib/theme/default-theme.ts +0 -11
- package/template/lib/utils.ts +0 -6
- package/template/next.config.ts +0 -8
- package/template/package.json +0 -76
- package/template/pnpm-lock.yaml +0 -6937
- package/template/postcss.config.mjs +0 -7
- package/template/process-compose.yaml +0 -13
- package/template/public/file.svg +0 -1
- package/template/public/globe.svg +0 -1
- package/template/public/imagicma-picker-bridge.js +0 -374
- package/template/public/next.svg +0 -1
- package/template/public/vercel.svg +0 -1
- package/template/public/window.svg +0 -1
- package/template/server/db.ts +0 -24
- package/template/server/storage.ts +0 -41
- package/template/shared/routes.ts +0 -13
- package/template/shared/schema.ts +0 -17
- package/template/tailwind.config.mjs +0 -96
- package/template/tsconfig.json +0 -35
- 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
package/template-hono/AGENTS.md
CHANGED
|
@@ -1,95 +1,53 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
##
|
|
18
|
-
|
|
19
|
-
- `
|
|
20
|
-
- `client
|
|
21
|
-
- `
|
|
22
|
-
- `client
|
|
23
|
-
- `
|
|
24
|
-
- `
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
- `
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
-
|
|
47
|
-
-
|
|
48
|
-
-
|
|
49
|
-
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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.
|
package/template-hono/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# hono-app
|
|
2
2
|
|
|
3
|
-
基于 **Hono + Vite + React**
|
|
3
|
+
基于 **Hono + Vite + React** 的全栈应用。
|
|
4
4
|
|
|
5
5
|
## 技术栈
|
|
6
6
|
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
25
|
pnpm install
|
|
26
|
-
|
|
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`
|
|
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` 编译阶段使用
|
|
@@ -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
|
|
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(
|
|
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", {
|
package/template/.env.example
DELETED