tina4-nodejs 3.13.149 → 3.13.150

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/CLAUDE.md CHANGED
@@ -13,13 +13,13 @@ Even if the skill text is not currently loaded, these are non-negotiable:
13
13
 
14
14
  The full discipline lives in `.claude/skills/tina4-maintainer/SKILL.md`; this block is the always-on floor.
15
15
 
16
- # CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.149)
16
+ # CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.150)
17
17
 
18
18
  > This file helps AI assistants (Claude, Copilot, Cursor, etc.) understand and work on this codebase effectively.
19
19
 
20
20
  ## What This Project Is
21
21
 
22
- Tina4 for Node.js/TypeScript v3.13.149 - The Intelligent Native Application 4ramework. A convention-over-configuration structural paradigm. The developer writes TypeScript; Tina4 is invisible infrastructure.
22
+ Tina4 for Node.js/TypeScript v3.13.150 - The Intelligent Native Application 4ramework. A convention-over-configuration structural paradigm. The developer writes TypeScript; Tina4 is invisible infrastructure.
23
23
 
24
24
  The philosophy: zero ceremony, batteries included, file system as source of truth.
25
25
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tina4-nodejs",
3
- "version": "3.13.149",
3
+ "version": "3.13.150",
4
4
  "type": "module",
5
5
  "description": "Tina4 for Node.js/TypeScript - native TypeScript conventions and shared Tina4 contracts",
6
6
  "keywords": [
@@ -26381,7 +26381,7 @@ function enginePath() {
26381
26381
  function runEngine(target) {
26382
26382
  const binary = enginePath();
26383
26383
  if (!binary) throw new MetricsEngineError(`tina4 not found on PATH - ${INSTALL_HINT}`);
26384
- const processResult = spawnSync(binary, ["metrics", "--path", target, "--json"], {
26384
+ const processResult = spawnSync(binary, ["metrics", "--path", target, "--json", "--no-history"], {
26385
26385
  encoding: "utf8",
26386
26386
  timeout: 6e4,
26387
26387
  maxBuffer: 64 * 1024 * 1024
@@ -35582,7 +35582,7 @@ var init_plan = __esm({
35582
35582
  const existing = (currentPlan.steps || []).map((s) => s.text || "");
35583
35583
  const title = currentPlan.title || target;
35584
35584
  const goal = currentPlan.goal || "";
35585
- const aiUrl = process.env.TINA4_AI_URL || "http://localhost:11437/api/chat";
35585
+ const aiUrl = process.env.TINA4_AI_URL || "http://localhost:11435/api/chat";
35586
35586
  const aiModel = process.env.TINA4_AI_MODEL || "qwen2.5-coder:14b";
35587
35587
  const prompts = fleshPrompt(title, goal, existing, prompt);
35588
35588
  const fetched = await fetchFleshReply(aiUrl, aiModel, prompts);
@@ -38019,7 +38019,7 @@ function toolbarJs() {
38019
38019
  connect();
38020
38020
  })();`;
38021
38021
  }
38022
- var cpuCount, DEV_MCP_PREFIXES, DEV_PUBLIC_ASSETS, DEV_SECRET_BASENAMES, DEV_SECRET_SUFFIXES, MessageLog, RequestInspector, ErrorTracker, DevMailboxStore, DevQueue, WsTracker, DevAdmin, handleDashboard, _reloadMtime, _reloadFile, handleMtime, handleReload, handleMessages, handleMessagesClear, handleRequests, handleRequestsClear, handleSystem, handleMessagesSearch, handleQueue, handleQueueTopics, handleQueueDeadLetters, handleQueueRetry, handleQueuePurge, handleQueueReplay, handleMailbox, handleMailboxRead, handleMailboxSeed, handleMailboxClear, handleTable, handleTables, handleSeed, handleQuery, handleBroken, handleBrokenResolve, handleBrokenClear, handleWebsockets, handleWebsocketsDisconnect, handleTool, DEFAULT_MCP_URL, handleGroundingStatus, handleGroundingToken, handleMigrate, handleSeedRun, handleTest, handleThreads, handleThreadsSub, handleConnections, handleConnectionsTest, handleConnectionsSave, __devAdminFilename, __devAdminDirname, handleGalleryList, handleVersionCheck, handleThoughts, handleSuperviseStub, DEV_FILES_IGNORED, handleFiles, DEV_ADMIN_LANG_MAP, handleFileRead, handleFileSave, handleFileRaw, handleFileRename, handleFileDelete, handleDepsSearch, handleDepsInstall, handleGitStatus, handleMcpTools, handleMcpCall, handleMcpStreamable, handleMcpDelete, handleMcpGet405, handleMcpLegacyMessage, handleMcpSse, handleScaffoldList, handleScaffoldRun, handlePlanCurrent, handlePlanList, handlePlanCreate, handlePlanSwitch, handlePlanCompleteStep, handlePlanAddStep, handlePlanNote, handlePlanArchive, handlePlanRead, handlePlanFlesh, handleIndexRebuild, handleIndexSearch, handleIndexFile, handleIndexOverview, handleDocsSearch, handleDocsClass, handleDocsMethod, handleDocsIndex, handleDocsWellKnown, handleDevAdminJs, handleToolbarCss, handleToolbarJs;
38022
+ var cpuCount, DEV_MCP_PREFIXES, DEV_PUBLIC_ASSETS, DEV_SECRET_BASENAMES, DEV_SECRET_SUFFIXES, MessageLog, RequestInspector, ErrorTracker, DevMailboxStore, DevQueue, WsTracker, DevAdmin, handleDashboard, _reloadMtime, _reloadFile, handleMtime, handleReload, handleMessages, handleMessagesClear, handleRequests, handleRequestsClear, handleSystem, handleMessagesSearch, handleQueue, handleQueueTopics, handleQueueDeadLetters, handleQueueRetry, handleQueuePurge, handleQueueReplay, handleMailbox, handleMailboxRead, handleMailboxSeed, handleMailboxClear, handleTable, handleTables, handleSeed, handleQuery, handleBroken, handleBrokenResolve, handleBrokenClear, handleWebsockets, handleWebsocketsDisconnect, handleTool, DEFAULT_MCP_URL, handleGroundingStatus, handleGroundingToken, handleMigrate, handleSeedRun, handleTest, handleThreads, handleThreadsSub, handleConnections, handleConnectionsTest, handleConnectionsSave, __devAdminFilename, __devAdminDirname, handleGalleryList, handleVersionCheck, handleThoughts, handleSuperviseStub, DEV_FILES_IGNORED, handleFiles, DEV_ADMIN_LANG_MAP, handleFileRead, handleFileSave, handleFileRaw, handleFileRename, handleFileDelete, handleDepsSearch, handleDepsInstall, handleGitStatus, handleMcpTools, handleMcpCall, handleMcpStreamable, handleMcpDelete, handleMcpGet405, handleMcpLegacyMessage, handleMcpSse, handleScaffoldList, handleScaffoldRun, handlePlanCurrent, handlePlanList, handlePlanCreate, handlePlanSwitch, handlePlanCompleteStep, handlePlanAddStep, handlePlanNote, handlePlanArchive, handlePlanRead, handlePlanFlesh, handleIndexRebuild, handleIndexSearch, handleIndexFile, handleIndexOverview, handleDocsSearch, handleDocsClass, handleDocsMethod, handleDocsIndex, handleDocsWellKnown, handleDevAdminJs, handleDevAdminSplit, handleToolbarCss, handleToolbarJs;
38023
38023
  var init_devAdmin = __esm({
38024
38024
  "../core/src/devAdmin.ts"() {
38025
38025
  "use strict";
@@ -38489,6 +38489,7 @@ var init_devAdmin = __esm({
38489
38489
  { method: "GET", pattern: "/__dev/api/docs/.well-known.json", handler: handleDocsWellKnown },
38490
38490
  // JS asset
38491
38491
  { method: "GET", pattern: "/__dev/js/tina4-dev-admin.min.js", handler: handleDevAdminJs },
38492
+ { method: "GET", pattern: "/__dev/split", handler: handleDevAdminSplit },
38492
38493
  // Dev-toolbar assets — served as external CSS/JS so the injected toolbar is
38493
38494
  // CSP-clean (no inline style=, onclick=, or <script>) under a strict
38494
38495
  // `default-src 'self'`. Parity with PHP's /__dev/toolbar.css + toolbar.js.
@@ -39927,6 +39928,31 @@ var init_devAdmin = __esm({
39927
39928
  res.raw.writeHead(404, { "Content-Type": "text/plain" });
39928
39929
  res.raw.end("tina4-dev-admin.min.js not found");
39929
39930
  };
39931
+ handleDevAdminSplit = async (_req, res) => {
39932
+ const { readFileSync: readFileSync30, existsSync: existsSync38 } = await import("node:fs");
39933
+ const { dirname: dirname20, join: join41, resolve: resolve29 } = await import("node:path");
39934
+ const { fileURLToPath: fileURLToPath12 } = await import("node:url");
39935
+ const dir = dirname20(fileURLToPath12(import.meta.url));
39936
+ const candidates = [
39937
+ join41(dir, "..", "public", "js", "tina4-dev-admin-split.html"),
39938
+ join41(dir, "..", "..", "public", "js", "tina4-dev-admin-split.html"),
39939
+ resolve29(process.cwd(), "node_modules", "tina4-nodejs", "packages", "core", "public", "js", "tina4-dev-admin-split.html"),
39940
+ resolve29(process.cwd(), "public", "js", "tina4-dev-admin-split.html")
39941
+ ];
39942
+ for (const path10 of candidates) {
39943
+ if (existsSync38(path10)) {
39944
+ try {
39945
+ const content = readFileSync30(path10, "utf-8");
39946
+ res.raw.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-cache" });
39947
+ res.raw.end(content);
39948
+ return;
39949
+ } catch {
39950
+ }
39951
+ }
39952
+ }
39953
+ res.raw.writeHead(404, { "Content-Type": "text/plain" });
39954
+ res.raw.end("tina4-dev-admin-split.html not found");
39955
+ };
39930
39956
  handleToolbarCss = (_req, res) => {
39931
39957
  res.raw.writeHead(200, { "Content-Type": "text/css; charset=utf-8", "Cache-Control": "no-cache" });
39932
39958
  res.raw.end(toolbarCss());
@@ -26360,7 +26360,7 @@ function enginePath() {
26360
26360
  function runEngine(target) {
26361
26361
  const binary = enginePath();
26362
26362
  if (!binary) throw new MetricsEngineError(`tina4 not found on PATH - ${INSTALL_HINT}`);
26363
- const processResult = spawnSync(binary, ["metrics", "--path", target, "--json"], {
26363
+ const processResult = spawnSync(binary, ["metrics", "--path", target, "--json", "--no-history"], {
26364
26364
  encoding: "utf8",
26365
26365
  timeout: 6e4,
26366
26366
  maxBuffer: 64 * 1024 * 1024
@@ -35538,7 +35538,7 @@ var init_plan = __esm({
35538
35538
  const existing = (currentPlan.steps || []).map((s) => s.text || "");
35539
35539
  const title = currentPlan.title || target;
35540
35540
  const goal = currentPlan.goal || "";
35541
- const aiUrl = process.env.TINA4_AI_URL || "http://localhost:11437/api/chat";
35541
+ const aiUrl = process.env.TINA4_AI_URL || "http://localhost:11435/api/chat";
35542
35542
  const aiModel = process.env.TINA4_AI_MODEL || "qwen2.5-coder:14b";
35543
35543
  const prompts = fleshPrompt(title, goal, existing, prompt);
35544
35544
  const fetched = await fetchFleshReply(aiUrl, aiModel, prompts);
@@ -37975,7 +37975,7 @@ function toolbarJs() {
37975
37975
  connect();
37976
37976
  })();`;
37977
37977
  }
37978
- var cpuCount, DEV_MCP_PREFIXES, DEV_PUBLIC_ASSETS, DEV_SECRET_BASENAMES, DEV_SECRET_SUFFIXES, MessageLog, RequestInspector, ErrorTracker, DevMailboxStore, DevQueue, WsTracker, DevAdmin, handleDashboard, _reloadMtime, _reloadFile, handleMtime, handleReload, handleMessages, handleMessagesClear, handleRequests, handleRequestsClear, handleSystem, handleMessagesSearch, handleQueue, handleQueueTopics, handleQueueDeadLetters, handleQueueRetry, handleQueuePurge, handleQueueReplay, handleMailbox, handleMailboxRead, handleMailboxSeed, handleMailboxClear, handleTable, handleTables, handleSeed, handleQuery, handleBroken, handleBrokenResolve, handleBrokenClear, handleWebsockets, handleWebsocketsDisconnect, handleTool, DEFAULT_MCP_URL, handleGroundingStatus, handleGroundingToken, handleMigrate, handleSeedRun, handleTest, handleThreads, handleThreadsSub, handleConnections, handleConnectionsTest, handleConnectionsSave, __devAdminFilename, __devAdminDirname, handleGalleryList, handleVersionCheck, handleThoughts, handleSuperviseStub, DEV_FILES_IGNORED, handleFiles, DEV_ADMIN_LANG_MAP, handleFileRead, handleFileSave, handleFileRaw, handleFileRename, handleFileDelete, handleDepsSearch, handleDepsInstall, handleGitStatus, handleMcpTools, handleMcpCall, handleMcpStreamable, handleMcpDelete, handleMcpGet405, handleMcpLegacyMessage, handleMcpSse, handleScaffoldList, handleScaffoldRun, handlePlanCurrent, handlePlanList, handlePlanCreate, handlePlanSwitch, handlePlanCompleteStep, handlePlanAddStep, handlePlanNote, handlePlanArchive, handlePlanRead, handlePlanFlesh, handleIndexRebuild, handleIndexSearch, handleIndexFile, handleIndexOverview, handleDocsSearch, handleDocsClass, handleDocsMethod, handleDocsIndex, handleDocsWellKnown, handleDevAdminJs, handleToolbarCss, handleToolbarJs;
37978
+ var cpuCount, DEV_MCP_PREFIXES, DEV_PUBLIC_ASSETS, DEV_SECRET_BASENAMES, DEV_SECRET_SUFFIXES, MessageLog, RequestInspector, ErrorTracker, DevMailboxStore, DevQueue, WsTracker, DevAdmin, handleDashboard, _reloadMtime, _reloadFile, handleMtime, handleReload, handleMessages, handleMessagesClear, handleRequests, handleRequestsClear, handleSystem, handleMessagesSearch, handleQueue, handleQueueTopics, handleQueueDeadLetters, handleQueueRetry, handleQueuePurge, handleQueueReplay, handleMailbox, handleMailboxRead, handleMailboxSeed, handleMailboxClear, handleTable, handleTables, handleSeed, handleQuery, handleBroken, handleBrokenResolve, handleBrokenClear, handleWebsockets, handleWebsocketsDisconnect, handleTool, DEFAULT_MCP_URL, handleGroundingStatus, handleGroundingToken, handleMigrate, handleSeedRun, handleTest, handleThreads, handleThreadsSub, handleConnections, handleConnectionsTest, handleConnectionsSave, __devAdminFilename, __devAdminDirname, handleGalleryList, handleVersionCheck, handleThoughts, handleSuperviseStub, DEV_FILES_IGNORED, handleFiles, DEV_ADMIN_LANG_MAP, handleFileRead, handleFileSave, handleFileRaw, handleFileRename, handleFileDelete, handleDepsSearch, handleDepsInstall, handleGitStatus, handleMcpTools, handleMcpCall, handleMcpStreamable, handleMcpDelete, handleMcpGet405, handleMcpLegacyMessage, handleMcpSse, handleScaffoldList, handleScaffoldRun, handlePlanCurrent, handlePlanList, handlePlanCreate, handlePlanSwitch, handlePlanCompleteStep, handlePlanAddStep, handlePlanNote, handlePlanArchive, handlePlanRead, handlePlanFlesh, handleIndexRebuild, handleIndexSearch, handleIndexFile, handleIndexOverview, handleDocsSearch, handleDocsClass, handleDocsMethod, handleDocsIndex, handleDocsWellKnown, handleDevAdminJs, handleDevAdminSplit, handleToolbarCss, handleToolbarJs;
37979
37979
  var init_devAdmin = __esm({
37980
37980
  "src/devAdmin.ts"() {
37981
37981
  "use strict";
@@ -38445,6 +38445,7 @@ var init_devAdmin = __esm({
38445
38445
  { method: "GET", pattern: "/__dev/api/docs/.well-known.json", handler: handleDocsWellKnown },
38446
38446
  // JS asset
38447
38447
  { method: "GET", pattern: "/__dev/js/tina4-dev-admin.min.js", handler: handleDevAdminJs },
38448
+ { method: "GET", pattern: "/__dev/split", handler: handleDevAdminSplit },
38448
38449
  // Dev-toolbar assets — served as external CSS/JS so the injected toolbar is
38449
38450
  // CSP-clean (no inline style=, onclick=, or <script>) under a strict
38450
38451
  // `default-src 'self'`. Parity with PHP's /__dev/toolbar.css + toolbar.js.
@@ -39883,6 +39884,31 @@ var init_devAdmin = __esm({
39883
39884
  res.raw.writeHead(404, { "Content-Type": "text/plain" });
39884
39885
  res.raw.end("tina4-dev-admin.min.js not found");
39885
39886
  };
39887
+ handleDevAdminSplit = async (_req, res) => {
39888
+ const { readFileSync: readFileSync27, existsSync: existsSync29 } = await import("node:fs");
39889
+ const { dirname: dirname17, join: join33, resolve: resolve21 } = await import("node:path");
39890
+ const { fileURLToPath: fileURLToPath10 } = await import("node:url");
39891
+ const dir = dirname17(fileURLToPath10(import.meta.url));
39892
+ const candidates = [
39893
+ join33(dir, "..", "public", "js", "tina4-dev-admin-split.html"),
39894
+ join33(dir, "..", "..", "public", "js", "tina4-dev-admin-split.html"),
39895
+ resolve21(process.cwd(), "node_modules", "tina4-nodejs", "packages", "core", "public", "js", "tina4-dev-admin-split.html"),
39896
+ resolve21(process.cwd(), "public", "js", "tina4-dev-admin-split.html")
39897
+ ];
39898
+ for (const path10 of candidates) {
39899
+ if (existsSync29(path10)) {
39900
+ try {
39901
+ const content = readFileSync27(path10, "utf-8");
39902
+ res.raw.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-cache" });
39903
+ res.raw.end(content);
39904
+ return;
39905
+ } catch {
39906
+ }
39907
+ }
39908
+ }
39909
+ res.raw.writeHead(404, { "Content-Type": "text/plain" });
39910
+ res.raw.end("tina4-dev-admin-split.html not found");
39911
+ };
39886
39912
  handleToolbarCss = (_req, res) => {
39887
39913
  res.raw.writeHead(200, { "Content-Type": "text/css; charset=utf-8", "Cache-Control": "no-cache" });
39888
39914
  res.raw.end(toolbarCss());
@@ -41,8 +41,8 @@
41
41
  },
42
42
  {
43
43
  "path": "js/tina4-dev-admin.min.js",
44
- "component": "tina4-dev-admin-21700e0",
45
- "sha256": "0ca84246c26afb6a3243aafd1c65fa9486c076039e5d721b6f44661b3a59d7c2"
44
+ "component": "tina4-dev-admin-8c4617d",
45
+ "sha256": "6b05be06b5d76ee4126a11f019d104d3920c1e3234250cfe1966a33f34c1a234"
46
46
  }
47
47
  ]
48
48
  }
@@ -0,0 +1,202 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>Tina4 Dev - Split</title>
3
+ <style>
4
+ html,body{margin:0;height:100%;background:#0f172a;font-family:system-ui,-apple-system,sans-serif;overflow:hidden}
5
+ /* One top toolbar controls both panels and the preview device. */
6
+ .t4-topbar{height:40px;box-sizing:border-box;display:flex;align-items:center;gap:6px;padding:0 10px;background:#0b1220;border-bottom:1px solid #1f2937}
7
+ .t4-brand{color:#e2e8f0;font:600 12px system-ui;margin-right:6px}
8
+ .t4-spacer{flex:1 1 auto}
9
+ .t4-devbtn{background:#1e293b;color:#cbd5e1;border:1px solid #334155;border-radius:6px;padding:3px 9px;font:12px system-ui;cursor:pointer}
10
+ .t4-devbtn.active{background:#2563eb;border-color:#2563eb;color:#fff}
11
+ /* Icon toggle buttons: full opacity + highlight when the panel is shown,
12
+ dimmed when hidden. */
13
+ .t4-tg{display:inline-flex;align-items:center;justify-content:center;width:28px;height:26px;background:#1e293b;color:#94a3b8;border:1px solid #334155;border-radius:6px;cursor:pointer;opacity:.5}
14
+ .t4-tg svg{width:15px;height:15px}
15
+ .t4-tg.active{background:#334155;color:#fff;opacity:1}
16
+ .t4-dim{color:#64748b;font:11px/1 ui-monospace,monospace;margin:0 6px}
17
+ .t4-popout{background:#0f766e;color:#fff;border:1px solid #0f766e;border-radius:6px;padding:3px 10px;font:12px system-ui;cursor:pointer;margin-left:6px}
18
+ .t4-split{display:flex;height:calc(100vh - 41px);width:100vw}
19
+ .t4-left{flex:0 0 auto;width:58%;min-width:0;height:100%;overflow:hidden}
20
+ .t4-left iframe{width:100%;height:100%;border:0;display:block}
21
+ .t4-gutter{flex:0 0 6px;cursor:col-resize;background:#1f2937}
22
+ .t4-gutter:hover,.t4-gutter.drag{background:#2563eb}
23
+ .t4-right{flex:1 1 auto;min-width:0;height:100%;overflow:hidden;display:flex;flex-direction:column}
24
+ /* A LIGHT backdrop so the dark device frame stands out. */
25
+ .t4-stage{flex:1 1 auto;min-width:0;min-height:0;position:relative;overflow:hidden;display:flex;align-items:center;justify-content:center;background:radial-gradient(circle at 50% 32%,#f8fafc,#c3ccd9)}
26
+ /* The device frame - a phone/tablet bezel around the live app. overflow:hidden
27
+ + border-radius CLIP the app to the rounded frame, so content is contained.
28
+ No width/height transition: fit() measures right after a toggle, and an
29
+ animated size would be read mid-transition (the "clicking does nothing" bug). */
30
+ .t4-device{position:relative;background:#111;box-shadow:0 16px 50px rgba(15,23,42,.45);transform-origin:center center;overflow:hidden}
31
+ .t4-device iframe{width:100%;height:100%;border:0;display:block;background:#fff}
32
+ .t4-device.mobile{border:14px solid #111;border-radius:46px}
33
+ .t4-device.tablet{border:16px solid #111;border-radius:26px}
34
+ /* Desktop fills the whole preview block - no bezel, no scaling. */
35
+ .t4-device.desktop{border:0;border-radius:0;box-shadow:none;width:100%;height:100%}
36
+ .t4-hidden{display:none !important}
37
+ /* While dragging the gutter, let mouse events pass over the iframes. */
38
+ body.t4-dragging iframe{pointer-events:none}
39
+ </style></head>
40
+ <body>
41
+ <div class="t4-topbar">
42
+ <span class="t4-brand">Tina4 Dev</span>
43
+ <button class="t4-tg active" id="t4-tg-files" title="Show/hide the file navigator"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4"><rect x="1.5" y="2.5" width="13" height="11" rx="1.5"/><line x1="6" y1="2.5" x2="6" y2="13.5"/></svg></button>
44
+ <button class="t4-tg active" id="t4-tg-editor" title="Show/hide the code editor"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><polyline points="5.5,4.5 2,8 5.5,11.5"/><polyline points="10.5,4.5 14,8 10.5,11.5"/></svg></button>
45
+ <button class="t4-tg active" id="t4-tg-preview" title="Show/hide the preview"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"><rect x="1.5" y="2.5" width="13" height="9" rx="1.5"/><line x1="5.5" y1="14" x2="10.5" y2="14"/><line x1="8" y1="11.5" x2="8" y2="14"/></svg></button>
46
+ <span class="t4-spacer"></span>
47
+ <span class="t4-dim" id="t4-dim">1440 x 900</span>
48
+ <button class="t4-devbtn" data-mode="mobile">Mobile</button>
49
+ <button class="t4-devbtn" data-mode="tablet">Tablet</button>
50
+ <button class="t4-devbtn active" data-mode="desktop">Desktop</button>
51
+ <button class="t4-popout" id="t4-popout" title="Open this view in its own window - drag it to another screen">&#8689; Pop out</button>
52
+ </div>
53
+ <div class="t4-split" id="t4-split">
54
+ <div class="t4-left" id="t4-left"><iframe src="/__dev" title="Tina4 Dev Admin"></iframe></div>
55
+ <div class="t4-gutter" id="t4-gutter"></div>
56
+ <div class="t4-right" id="t4-right">
57
+ <div class="t4-stage" id="t4-stage">
58
+ <div class="t4-device desktop" id="t4-device">
59
+ <iframe id="t4-preview" src="/" title="Live preview"></iframe>
60
+ </div>
61
+ </div>
62
+ </div>
63
+ </div>
64
+ <script>
65
+ (function () {
66
+ var SIZES = { mobile: {w:390,h:844}, tablet: {w:820,h:1180}, desktop: {w:1440,h:900} };
67
+ var KEY = 'tina4_split_device';
68
+ var device = document.getElementById('t4-device');
69
+ var stage = document.getElementById('t4-stage');
70
+ var dim = document.getElementById('t4-dim');
71
+ var left = document.getElementById('t4-left');
72
+ var right = document.getElementById('t4-right');
73
+ var gutter = document.getElementById('t4-gutter');
74
+ var split = document.getElementById('t4-split');
75
+ var mode = 'desktop';
76
+ try { var saved = localStorage.getItem(KEY); if (saved && SIZES[saved]) mode = saved; } catch (_) {}
77
+ var leftWidth = '58%';
78
+
79
+ // ── device preview ──
80
+ function fit() {
81
+ if (right.classList.contains('t4-hidden')) return;
82
+ if (mode === 'desktop') {
83
+ // Desktop just fills the preview block - responsive, no frame, no scale.
84
+ device.style.transform = 'none';
85
+ device.style.width = '100%';
86
+ device.style.height = '100%';
87
+ var r0 = stage.getBoundingClientRect();
88
+ if (dim) dim.textContent = Math.round(r0.width) + ' x ' + Math.round(r0.height);
89
+ return;
90
+ }
91
+ var s = SIZES[mode];
92
+ device.style.transform = 'none';
93
+ device.style.width = s.w + 'px';
94
+ device.style.height = s.h + 'px';
95
+ var rect = stage.getBoundingClientRect();
96
+ var pad = 28;
97
+ var ow = device.offsetWidth, oh = device.offsetHeight;
98
+ var scale = Math.min((rect.width - pad) / ow, (rect.height - pad) / oh, 1);
99
+ if (!isFinite(scale) || scale <= 0) scale = 1;
100
+ device.style.transform = 'scale(' + scale + ')';
101
+ }
102
+ function setMode(m) {
103
+ if (!SIZES[m]) return;
104
+ mode = m;
105
+ device.className = 't4-device ' + m;
106
+ if (dim) dim.textContent = SIZES[m].w + ' x ' + SIZES[m].h;
107
+ document.querySelectorAll('.t4-devbtn').forEach(function (b) {
108
+ b.classList.toggle('active', b.getAttribute('data-mode') === m);
109
+ });
110
+ try { localStorage.setItem(KEY, m); } catch (_) {}
111
+ fit();
112
+ }
113
+ document.querySelectorAll('.t4-devbtn').forEach(function (b) {
114
+ b.addEventListener('click', function () { setMode(b.getAttribute('data-mode')); });
115
+ });
116
+ var pop = document.getElementById('t4-popout');
117
+ if (pop) pop.addEventListener('click', function () {
118
+ var s = SIZES[mode];
119
+ var w = Math.min(s.w, (screen.availWidth || 1920) - 40);
120
+ var h = Math.min(s.h, (screen.availHeight || 1080) - 120);
121
+ window.open('/', 'tina4preview_' + mode, 'width=' + w + ',height=' + h + ',menubar=no,toolbar=no,location=no');
122
+ });
123
+
124
+ // ── show / hide each panel (Files, Editor, Preview) ──
125
+ // Files (the nav) and Editor (the code area) are SEPARATE sections living
126
+ // inside the left /__dev SPA iframe, so hiding the editor must not take the
127
+ // file nav with it. They are same-origin, so we reach into the iframe and
128
+ // toggle its own `#editor-sidebar` (nav) and `.editor-main` (editor). The
129
+ // Preview is the right pane of this page.
130
+ var vis = { files: true, editor: true, preview: true };
131
+ function frameDoc() {
132
+ try { return document.querySelector('#t4-left iframe').contentDocument; } catch (_) { return null; }
133
+ }
134
+ function setFrameEl(sel, show) {
135
+ var d = frameDoc(); if (!d) return;
136
+ var el = d.querySelector(sel); if (el) el.style.display = show ? '' : 'none';
137
+ }
138
+ function applyPanels() {
139
+ setFrameEl('#editor-sidebar', vis.files);
140
+ setFrameEl('#editor-splitter-left', vis.files && vis.editor);
141
+ setFrameEl('.editor-main', vis.editor);
142
+ var leftEmpty = !vis.files && !vis.editor;
143
+ left.classList.toggle('t4-hidden', leftEmpty);
144
+ right.classList.toggle('t4-hidden', !vis.preview);
145
+ gutter.classList.toggle('t4-hidden', leftEmpty || !vis.preview);
146
+ if (!vis.preview) {
147
+ // Preview hidden: the editor/nav fills the whole width.
148
+ left.style.flex = '1 1 auto'; left.style.width = 'auto';
149
+ } else if (vis.files && !vis.editor) {
150
+ // Only the file nav is shown: shrink the left pane to the nav width so
151
+ // there is no empty gap where the editor used to be.
152
+ left.style.flex = '0 0 auto'; left.style.width = '250px';
153
+ } else {
154
+ left.style.flex = '0 0 auto'; left.style.width = leftWidth;
155
+ }
156
+ document.getElementById('t4-tg-files').classList.toggle('active', vis.files);
157
+ document.getElementById('t4-tg-editor').classList.toggle('active', vis.editor);
158
+ document.getElementById('t4-tg-preview').classList.toggle('active', vis.preview);
159
+ fit();
160
+ }
161
+ function toggle(name) {
162
+ // Never hide all three - at least one stays visible.
163
+ var n = { files: vis.files, editor: vis.editor, preview: vis.preview };
164
+ n[name] = !n[name];
165
+ if (!n.files && !n.editor && !n.preview) return;
166
+ vis[name] = !vis[name];
167
+ applyPanels();
168
+ }
169
+ document.getElementById('t4-tg-files').addEventListener('click', function () { toggle('files'); });
170
+ document.getElementById('t4-tg-editor').addEventListener('click', function () { toggle('editor'); });
171
+ document.getElementById('t4-tg-preview').addEventListener('click', function () { toggle('preview'); });
172
+
173
+ // ── drag the gutter to resize ──
174
+ var dragging = false;
175
+ gutter.addEventListener('mousedown', function (e) {
176
+ dragging = true;
177
+ document.body.classList.add('t4-dragging');
178
+ gutter.classList.add('drag');
179
+ e.preventDefault();
180
+ });
181
+ window.addEventListener('mousemove', function (e) {
182
+ if (!dragging) return;
183
+ var r = split.getBoundingClientRect();
184
+ var w = e.clientX - r.left;
185
+ w = Math.max(220, Math.min(w, r.width - 260));
186
+ leftWidth = w + 'px';
187
+ left.style.width = leftWidth;
188
+ fit();
189
+ });
190
+ window.addEventListener('mouseup', function () {
191
+ if (!dragging) return;
192
+ dragging = false;
193
+ document.body.classList.remove('t4-dragging');
194
+ gutter.classList.remove('drag');
195
+ });
196
+
197
+ window.addEventListener('resize', fit);
198
+ setMode(mode);
199
+ applyPanels();
200
+ })();
201
+ </script>
202
+ </body></html>