@bongos/core 1.19.1081 → 1.20.2
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/.bongos-core.json +528 -193
- package/.claude/skills/planning-session/SKILL.md +6 -2
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +32 -0
- package/clients/bongos-client/index.cjs +32 -0
- package/clients/bongos-client/index.d.ts +51 -6
- package/clients/bongos-client/index.mjs +32 -0
- package/docs/adr/0099-delayed-redacted-mirror-export.md +5 -1
- package/docs/adr/0111-instance-hosting-provisioning-module.md +1 -0
- package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +17 -0
- package/docs/adr/0176-private-repo-deploy-keys.md +2 -0
- package/docs/adr/0310-a-speciality-offers-skills-and-the-adopter-chooses-them.md +1 -1
- package/docs/adr/0348-the-web-tier-may-look-read-only-at-what-an-owners-render-key-can-see.md +53 -0
- package/docs/adr/0349-a-version-preview-is-a-sandboxed-child-the-web-tier-launches.md +106 -0
- package/docs/adr/0350-the-hub-holds-a-per-project-write-deploy-key-so-a-hosted-upgrade-reaches-github.md +103 -0
- package/docs/adr/README.md +3 -0
- package/docs/api/openapi.json +851 -19
- package/docs/api-reference.md +33 -9
- package/docs/architecture.md +6 -2
- package/docs/copy-inventory.md +620 -553
- package/docs/copy-registry.json +1467 -820
- package/docs/file-map.md +2 -0
- package/docs/module-api-changelog.md +8 -0
- package/docs/modules-contract.md +1 -0
- package/docs/page-inventory.json +38 -4
- package/docs/page-readings.json +1666 -1558
- package/migrations/core_260_goals_working_area.sql +63 -0
- package/migrations/core_261_grade_attempts_verdicts.sql +24 -0
- package/migrations/core_262_drop_builder_box_blocked.sql +33 -0
- package/modules/agents/lib/validate.js +43 -0
- package/modules/autonomy/gauge.js +38 -2
- package/modules/grading/grader-subagent.js +37 -2
- package/modules/grading/grader-workers/reader.js +65 -5
- package/modules/hall-ui/public/approval-queue.css +33 -7
- package/modules/hall-ui/public/approval-queue.js +9 -2
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/brand-holes.js +121 -0
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/collab.js +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +6 -1
- package/modules/hall-ui/public/deploy.js +22 -8
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/dom-utils.js +20 -0
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/fleet.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/goal-inbox.js +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/idea.js +28 -0
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/ideas.js +19 -3
- package/modules/hall-ui/public/index.html +5 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile-nudge.js +21 -4
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/profile.js +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings.html +19 -30
- package/modules/hall-ui/public/settings.js +24 -177
- package/modules/hall-ui/public/settings.states.json +1 -1
- package/modules/hall-ui/public/shell.js +4 -2
- package/modules/hall-ui/public/studio.css +14 -8
- package/modules/hall-ui/public/studio.html +5 -4
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.css +6 -3
- package/modules/hall-ui/public/thinking.html +1 -1
- package/modules/hall-ui/public/tweak-editor-lib.js +82 -1
- package/modules/hall-ui/public/tweak-editor.css +142 -11
- package/modules/hall-ui/public/tweak-editor.html +25 -9
- package/modules/hall-ui/public/tweak-editor.js +243 -10
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/hall-ui/records/approval-queue.md +6 -0
- package/modules/hall-ui/records/tweak-editor.md +6 -0
- package/modules/ideas/ratify.js +36 -8
- package/modules/ideas/routes/ratify-goal.js +6 -0
- package/modules/lifecycle/db-analytics.js +79 -10
- package/modules/lifecycle/db-claims.js +48 -1
- package/modules/lifecycle/db-deps-criteria.js +94 -40
- package/modules/lifecycle/db-goals.js +4 -1
- package/modules/lifecycle/db-grade.js +9 -1
- package/modules/lifecycle/db-relevance-flags.js +183 -0
- package/modules/lifecycle/db-versions.js +4 -3
- package/modules/lifecycle/db.js +19 -0
- package/modules/lifecycle/done-when.js +5 -3
- package/modules/lifecycle/est-advisory.js +31 -4
- package/modules/lifecycle/goal-task-relevance-advisory.js +153 -0
- package/modules/lifecycle/goal-task-relevance-judge.js +226 -0
- package/modules/lifecycle/migrations/lifecycle_014_goal_task_relevance_flags.sql +82 -0
- package/modules/lifecycle/module.json +2 -1
- package/modules/lifecycle/routes/claims.js +8 -1
- package/modules/lifecycle/routes/goal-task-relevance.js +72 -0
- package/modules/lifecycle/routes/goals.js +1 -1
- package/modules/lifecycle/routes/tasks.js +27 -24
- package/modules/lifecycle/routes/visuals.js +9 -0
- package/modules/lifecycle/ship-preflight.js +7 -11
- package/modules/lifecycle/task-visuals.js +109 -5
- package/modules/npm-release/module.json +2 -1
- package/modules/npm-release/preview/commands.js +81 -0
- package/modules/npm-release/preview/divert.js +95 -0
- package/modules/npm-release/preview/env.js +78 -0
- package/modules/npm-release/preview/proxy.js +107 -0
- package/modules/npm-release/preview/runtime.js +83 -0
- package/modules/npm-release/preview/supervisor.js +175 -0
- package/modules/npm-release/public/work.js +65 -1
- package/modules/npm-release/routes/preview.js +133 -0
- package/modules/provisioning/app-status.js +83 -0
- package/modules/provisioning/module.json +2 -1
- package/modules/provisioning/pollers/app-liveness.js +110 -0
- package/modules/provisioning/render-lookup.js +68 -0
- package/modules/provisioning/render-standup.js +44 -1
- package/modules/provisioning/routes/render-standup.js +81 -6
- package/modules/provisioning/starter-bundles.js +4 -1
- package/modules/public-landing/public/account-private.states.json +14 -0
- package/modules/public-landing/public/account.html +513 -0
- package/modules/public-landing/public/account.probes.json +37 -0
- package/modules/public-landing/public/account.states.json +16 -0
- package/modules/public-landing/public/index.html +5 -2
- package/modules/public-landing/public/projects.html +661 -18
- package/modules/public-landing/public/projects.states.json +8 -1
- package/modules/render-deploy/deploys.js +203 -0
- package/modules/render-deploy/migrations/render_deploy_001_app.sql +22 -0
- package/modules/render-deploy/module.json +25 -0
- package/modules/render-deploy/public/deploy.css +10 -0
- package/modules/render-deploy/public/deploy.js +249 -0
- package/modules/render-deploy/render.js +56 -0
- package/modules/render-deploy/routes/act.js +88 -0
- package/modules/render-deploy/routes/app.js +65 -0
- package/modules/render-deploy/routes/history.js +38 -0
- package/modules/specialities/db.js +27 -8
- package/modules/specialities/migrations/specialities_003_skills.sql +46 -0
- package/modules/specialities/routes/specialities.js +51 -3
- package/modules/specialities/skills.js +94 -0
- package/modules/specialities/specialities.js +56 -9
- package/modules/ui-design/kit/fixtures/me-cross-project-private.json +12 -0
- package/modules/ui-design/kit/fixtures/me__cross-project.json +15 -0
- package/modules/ui-design/kit/fixtures/provisioning-instance-render.json +39 -0
- package/modules/ui-design/kit/lib.js +3 -1
- package/modules/ui-design/kit/serve.js +96 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +169 -0
- package/scripts/gds/agents-sync.js +13 -3
- package/scripts/gds/autobongos-run.js +80 -2
- package/scripts/gds/autobongos-service.cmd +12 -0
- package/scripts/gds/copy-apply.js +14 -0
- package/scripts/gds/dev-box-guard.js +3 -2
- package/scripts/gds/fitness.js +9 -0
- package/scripts/gds/grade-correlation-audit.js +42 -12
- package/scripts/gds/grade-replay.js +1 -1
- package/scripts/gds/provision-core-upgrade.js +38 -7
- package/scripts/gds/provision-pin-key.js +176 -0
- package/scripts/gds/provision-render.js +62 -31
- package/scripts/gds/provision-teardown.js +7 -2
- package/scripts/gds/provision-units.js +31 -0
- package/scripts/gds/provision.js +10 -10
- package/scripts/gds/publish-manifest.js +1 -0
- package/scripts/gds/render-api.js +19 -0
- package/scripts/gds/render-payload.js +39 -65
- package/scripts/gds/run-unit-tests.js +10 -0
- package/scripts/gds/ship-finish.js +19 -20
- package/scripts/gds/smoke-dependencies.sh +37 -8
- package/scripts/gds/status.js +12 -4
- package/scripts/gds/upgrade.js +2 -2
- package/scripts/public-mirror-export.js +7 -1
- package/src/bongos/module-scope-map.js +13 -0
- package/src/bongos/serve-internal.js +3 -1
- package/src/module-api.js +10 -1
- package/src/platform-server.js +39 -0
- package/tests/account_privacy_flags.mjs +10 -6
- package/tests/agents_authoring.mjs +15 -1
- package/tests/agents_sync.mjs +106 -5
- package/tests/agents_validate.mjs +45 -0
- package/tests/api_path_404.mjs +6 -0
- package/tests/autobongos_cadence.mjs +7 -0
- package/tests/autobongos_loop.mjs +136 -1
- package/tests/autonomy_gauge.mjs +62 -0
- package/tests/blocker_hall_live_proof.mjs +275 -0
- package/tests/claim_gate_ci_unblock.mjs +116 -0
- package/tests/claim_gate_rebase.mjs +15 -3
- package/tests/collab_page.mjs +10 -0
- package/tests/conductor_main_e2e.mjs +97 -0
- package/tests/core_262_drop_box_blocked_db.mjs +143 -0
- package/tests/core_upgrade_runner.mjs +1 -0
- package/tests/dependency_writes_atomic.mjs +191 -0
- package/tests/deploy_page_projects.mjs +2 -1
- package/tests/effective_visibility_predicate.mjs +15 -0
- package/tests/est_advisory.mjs +27 -0
- package/tests/goal_map_page.mjs +26 -0
- package/tests/goal_task_relevance.mjs +518 -0
- package/tests/goal_working_area.mjs +146 -0
- package/tests/grade_attempts.mjs +61 -0
- package/tests/grade_attribution.mjs +23 -0
- package/tests/grade_correlation_audit.mjs +54 -1
- package/tests/grader_reader_lens.mjs +55 -2
- package/tests/grader_root_outage.mjs +6 -1
- package/tests/grader_subagent_tools_arg.mjs +102 -0
- package/tests/hall_approval_queue.mjs +54 -2
- package/tests/hall_settings_world.mjs +3 -1
- package/tests/hall_tweak_editor.mjs +316 -11
- package/tests/hub_account_page.mjs +394 -0
- package/tests/idea_detail_page.mjs +64 -0
- package/tests/idea_goal_ratification.mjs +64 -0
- package/tests/idea_spark_hall.mjs +60 -9
- package/tests/ideator_full_idea_shapes_space_proof.mjs +1 -1
- package/tests/landing_page.mjs +6 -3
- package/tests/module-scope-map.mjs +31 -8
- package/tests/module_api.mjs +1 -0
- package/tests/module_loader.mjs +1 -1
- package/tests/nav_permission_atoms.mjs +1 -1
- package/tests/no_phantom_mirror_workflow.mjs +24 -0
- package/tests/npm_release_preview_commands.mjs +89 -0
- package/tests/npm_release_preview_divert.mjs +164 -0
- package/tests/npm_release_preview_env.mjs +100 -0
- package/tests/npm_release_preview_proxy.mjs +162 -0
- package/tests/npm_release_preview_routes.mjs +208 -0
- package/tests/npm_release_preview_supervisor.mjs +203 -0
- package/tests/pin_write_key.mjs +383 -0
- package/tests/planning_session_skill.mjs +27 -0
- package/tests/platform_boot.mjs +71 -0
- package/tests/profile_nudge_links.mjs +86 -0
- package/tests/profile_ui_cross_project.mjs +1 -1
- package/tests/projects_hub.mjs +2 -2
- package/tests/projects_hub_app_status.mjs +249 -0
- package/tests/projects_hub_app_step.mjs +282 -52
- package/tests/projects_hub_render_connect.mjs +295 -0
- package/tests/provision_render.mjs +121 -28
- package/tests/provision_settings_apply.mjs +62 -0
- package/tests/provisioning_app_status.mjs +204 -0
- package/tests/provisioning_render_route.mjs +126 -6
- package/tests/render_check.mjs +2 -1
- package/tests/render_deploy.mjs +495 -0
- package/tests/rot_inbox.mjs +8 -0
- package/tests/ship_preflight.mjs +61 -8
- package/tests/smoke_dependencies_witness.mjs +46 -0
- package/tests/speciality_skills.mjs +214 -0
- package/tests/studio_room_height.mjs +70 -0
- package/tests/task_detail_includes.mjs +10 -0
- package/tests/task_visuals_instance_root.mjs +150 -0
- package/tests/tweak_batch_apply.mjs +21 -0
- package/tests/ui_design_kit.mjs +36 -4
- package/tests/upgrade.mjs +1 -1
- package/tests/upgrade_persist_pin.mjs +24 -0
|
@@ -50,6 +50,9 @@ body[data-page="tweak-editor"] .tw-stage {
|
|
|
50
50
|
--tw-paper-faint: #65656a;
|
|
51
51
|
--tw-shared: #4a5bb8;
|
|
52
52
|
--tw-engineer: #a3481a;
|
|
53
|
+
--tw-ok: #1f7a44;
|
|
54
|
+
--tw-ok-wash: rgba(31, 122, 68, .11);
|
|
55
|
+
--tw-on-ok: #ffffff;
|
|
53
56
|
--tw-line-hover: rgba(23, 24, 26, .035);
|
|
54
57
|
--tw-line-focus: rgba(251, 149, 105, .08);
|
|
55
58
|
--tw-face: var(--font-body, Manrope, "Segoe UI", system-ui, sans-serif);
|
|
@@ -228,17 +231,82 @@ html:not([data-fullscreen="on"]) .tw-bar { top: calc(var(--topbar-h, 56px) + 12p
|
|
|
228
231
|
box-shadow: 0 30px 80px var(--tw-drop);
|
|
229
232
|
padding: 64px 88px 72px 88px;
|
|
230
233
|
}
|
|
231
|
-
|
|
232
|
-
.
|
|
234
|
+
/* "open the page" (task 1004386): a menu on hover, focus or tap, and the split
|
|
235
|
+
view's frame pinned under the words. The menu's padding-top bridges the gap to
|
|
236
|
+
the button so the pointer never leaves the hover area on its way down. */
|
|
237
|
+
.tw-openwrap { position: relative; }
|
|
238
|
+
.tw-openwrap[hidden] { display: none; }
|
|
239
|
+
.tw-openmenu {
|
|
240
|
+
position: absolute;
|
|
241
|
+
top: 100%;
|
|
242
|
+
right: 0;
|
|
243
|
+
z-index: 5;
|
|
244
|
+
min-width: 190px;
|
|
245
|
+
padding-top: 8px;
|
|
246
|
+
display: none;
|
|
247
|
+
}
|
|
248
|
+
/* A split button: the main half repeats the last choice, the arrow opens the
|
|
249
|
+
menu. Click, keyboard and touch; hover alone opens nothing. */
|
|
250
|
+
.tw-openwrap { display: inline-flex; }
|
|
251
|
+
.tw-openwrap.is-open .tw-openmenu { display: block; }
|
|
252
|
+
.tw-stage .tw-btn--main { border-top-right-radius: 0; border-bottom-right-radius: 0; }
|
|
253
|
+
.tw-stage .tw-btn--more { min-width: 32px; padding-left: 8px; padding-right: 10px; margin-left: 1px; border-top-left-radius: 0; border-bottom-left-radius: 0; }
|
|
254
|
+
.tw-stage .tw-openmenu__item {
|
|
255
|
+
display: block;
|
|
256
|
+
box-sizing: border-box;
|
|
257
|
+
width: 100%;
|
|
233
258
|
margin: 0;
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
259
|
+
padding: 10px 16px;
|
|
260
|
+
border: 1px solid var(--tw-glass-edge);
|
|
261
|
+
border-radius: 12px;
|
|
262
|
+
background: var(--tw-bar-back);
|
|
263
|
+
color: var(--tw-ink);
|
|
264
|
+
font: inherit;
|
|
265
|
+
font-size: 14px;
|
|
266
|
+
text-align: left;
|
|
267
|
+
text-decoration: none;
|
|
268
|
+
cursor: pointer;
|
|
269
|
+
}
|
|
270
|
+
.tw-stage .tw-openmenu__item + .tw-openmenu__item { margin-top: 4px; }
|
|
271
|
+
.tw-stage .tw-openmenu__item:hover { background: var(--tw-glass-lift); color: var(--tw-ink); }
|
|
272
|
+
.tw-stage .tw-openmenu__item:focus-visible { outline: 2px solid var(--tw-accent); outline-offset: 2px; }
|
|
273
|
+
.tw-frame {
|
|
274
|
+
position: fixed;
|
|
275
|
+
left: 0;
|
|
276
|
+
right: 0;
|
|
277
|
+
bottom: 0;
|
|
278
|
+
z-index: 4;
|
|
279
|
+
display: flex;
|
|
280
|
+
flex-direction: column;
|
|
281
|
+
height: calc(var(--tw-split, .5) * 100vh);
|
|
282
|
+
background: var(--tw-room);
|
|
283
|
+
border-top: 1px solid var(--tw-glass-edge);
|
|
284
|
+
box-shadow: 0 -20px 60px var(--tw-drop);
|
|
240
285
|
}
|
|
241
|
-
.tw-
|
|
286
|
+
.tw-frame[hidden] { display: none; }
|
|
287
|
+
.tw-frame__bar { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 8px 16px; }
|
|
288
|
+
.tw-frame__name { font-size: 13px; color: var(--tw-ink-faint); }
|
|
289
|
+
.tw-frame__view { flex: 1 1 auto; width: 100%; min-height: 0; border: 0; background: #ffffff; }
|
|
290
|
+
/* While dragging, the frame must not swallow the pointer. */
|
|
291
|
+
.tw-frame.is-dragging .tw-frame__view { pointer-events: none; }
|
|
292
|
+
.tw-divider {
|
|
293
|
+
flex: none;
|
|
294
|
+
display: flex;
|
|
295
|
+
align-items: center;
|
|
296
|
+
justify-content: center;
|
|
297
|
+
height: 24px;
|
|
298
|
+
margin-top: -12px;
|
|
299
|
+
cursor: row-resize;
|
|
300
|
+
touch-action: none;
|
|
301
|
+
}
|
|
302
|
+
.tw-divider__grip { width: 56px; height: 5px; border-radius: 3px; background: var(--tw-glass-rim); }
|
|
303
|
+
.tw-divider:hover .tw-divider__grip,
|
|
304
|
+
.tw-frame.is-dragging .tw-divider__grip { background: var(--tw-accent); }
|
|
305
|
+
.tw-divider:focus-visible { outline: 2px solid var(--tw-accent); outline-offset: -2px; }
|
|
306
|
+
/* Split view: the words scroll above, the frame holds the bottom share. */
|
|
307
|
+
.tw-stage.tw-split { padding-bottom: calc(var(--tw-split, .5) * 100vh); }
|
|
308
|
+
.tw-stage.tw-split .tw-line { scroll-margin: 96px 0 calc(var(--tw-split, .5) * 100vh + 16px); }
|
|
309
|
+
html[data-fullscreen="on"] .tw-frame { left: 0; }
|
|
242
310
|
.tw-section { margin-bottom: 26px; }
|
|
243
311
|
.tw-paper h2 {
|
|
244
312
|
margin: 0 0 6px 0;
|
|
@@ -250,7 +318,46 @@ html:not([data-fullscreen="on"]) .tw-bar { top: calc(var(--topbar-h, 56px) + 12p
|
|
|
250
318
|
text-transform: uppercase;
|
|
251
319
|
color: var(--tw-paper-faint);
|
|
252
320
|
}
|
|
253
|
-
.tw-row { display: grid; grid-template-columns: minmax(0, 1fr) 150px; gap:
|
|
321
|
+
.tw-row { display: grid; grid-template-columns: minmax(0, 1fr) 68px 150px; gap: 10px 14px; align-items: start; }
|
|
322
|
+
/* A checked line (task 1004385): a light green wash on the row and a filled
|
|
323
|
+
green check. The check is the artist's own place-keeper; it is never sent. */
|
|
324
|
+
.tw-row--checked .tw-line { background: var(--tw-ok-wash); }
|
|
325
|
+
.tw-row--checked .tw-line:focus,
|
|
326
|
+
.tw-row--checked .tw-line:focus-visible { background: var(--tw-ok-wash); }
|
|
327
|
+
.tw-ctl { display: flex; align-items: center; justify-content: flex-end; gap: 6px; padding-top: 6px; }
|
|
328
|
+
.tw-ctl[hidden] { display: none; }
|
|
329
|
+
.tw-restore,
|
|
330
|
+
.tw-check {
|
|
331
|
+
box-sizing: border-box;
|
|
332
|
+
width: 28px;
|
|
333
|
+
height: 28px;
|
|
334
|
+
padding: 0;
|
|
335
|
+
border: 1px solid var(--tw-paper-faint);
|
|
336
|
+
border-radius: 50%;
|
|
337
|
+
background: transparent;
|
|
338
|
+
color: var(--tw-paper-soft);
|
|
339
|
+
font: inherit;
|
|
340
|
+
font-size: 15px;
|
|
341
|
+
line-height: 1;
|
|
342
|
+
cursor: pointer;
|
|
343
|
+
}
|
|
344
|
+
.tw-restore[hidden] { display: none; }
|
|
345
|
+
.tw-restore:hover { color: var(--tw-paper-ink); border-color: var(--tw-paper-ink); }
|
|
346
|
+
.tw-check:hover { color: var(--tw-ok); border-color: var(--tw-ok); }
|
|
347
|
+
.tw-check[aria-pressed="true"] { background: var(--tw-ok); border-color: var(--tw-ok); color: var(--tw-on-ok); }
|
|
348
|
+
.tw-restore:focus-visible,
|
|
349
|
+
.tw-check:focus-visible { outline: 2px solid var(--tw-accent); outline-offset: 2px; }
|
|
350
|
+
.tw-hint {
|
|
351
|
+
margin: 0 0 24px 0;
|
|
352
|
+
padding: 10px 14px;
|
|
353
|
+
border-radius: 8px;
|
|
354
|
+
background: var(--tw-ok-wash);
|
|
355
|
+
box-shadow: inset 3px 0 0 var(--tw-ok);
|
|
356
|
+
font-size: 13.5px;
|
|
357
|
+
line-height: 1.5;
|
|
358
|
+
color: var(--tw-paper-ink);
|
|
359
|
+
}
|
|
360
|
+
.tw-hint[hidden] { display: none; }
|
|
254
361
|
.tw-line {
|
|
255
362
|
display: block;
|
|
256
363
|
box-sizing: border-box;
|
|
@@ -275,6 +382,26 @@ html:not([data-fullscreen="on"]) .tw-bar { top: calc(var(--topbar-h, 56px) + 12p
|
|
|
275
382
|
.tw-line--changed { box-shadow: inset 3px 0 0 var(--tw-accent); }
|
|
276
383
|
.tw-line[readonly] { cursor: default; }
|
|
277
384
|
.tw-line[readonly]:hover { background: transparent; }
|
|
385
|
+
/* A line holding a brand hole ({{productName}}) is a typed-in box instead of a
|
|
386
|
+
textarea, so the hole can be a chip the artist types around and never into
|
|
387
|
+
(task 1004381): the words keep every space the artist types (pre-wrap). */
|
|
388
|
+
.tw-line--holes { min-height: 1.6em; white-space: pre-wrap; overflow-wrap: anywhere; cursor: text; }
|
|
389
|
+
.tw-line--holes[contenteditable="false"] { cursor: default; }
|
|
390
|
+
.tw-line--holes[contenteditable="false"]:hover { background: transparent; }
|
|
391
|
+
.tw-hole {
|
|
392
|
+
display: inline-block;
|
|
393
|
+
padding: 0 6px;
|
|
394
|
+
border-radius: 5px;
|
|
395
|
+
border: 1px solid var(--tw-rule-soft, rgba(23, 24, 26, .14));
|
|
396
|
+
background: var(--tw-line-hover);
|
|
397
|
+
color: var(--tw-paper-soft);
|
|
398
|
+
font-size: .92em;
|
|
399
|
+
line-height: 1.4;
|
|
400
|
+
white-space: nowrap;
|
|
401
|
+
cursor: default;
|
|
402
|
+
user-select: all;
|
|
403
|
+
}
|
|
404
|
+
.tw-note .tw-hole { font-size: 1em; padding: 0 4px; }
|
|
278
405
|
.tw-note { padding-top: 9px; font-size: 12px; line-height: 1.4; color: var(--tw-paper-faint); overflow-wrap: anywhere; }
|
|
279
406
|
.tw-note--shared { color: var(--tw-shared); }
|
|
280
407
|
.tw-note--engineer { color: var(--tw-engineer); }
|
|
@@ -442,8 +569,12 @@ html:not([data-fullscreen="on"]) .tw-bar { top: calc(var(--topbar-h, 56px) + 12p
|
|
|
442
569
|
.tw-actions { flex: 1 0 100%; flex-wrap: wrap; gap: 8px; padding-top: 6px; }
|
|
443
570
|
html[data-fullscreen="on"] .tw-actions { margin-right: -44px; }
|
|
444
571
|
.tw-paper { width: calc(100% - 24px); margin-top: 16px; padding: 36px 20px 44px; }
|
|
445
|
-
|
|
572
|
+
/* The two-row bar is tall here, so the live page never takes more than half. */
|
|
573
|
+
.tw-frame { height: min(calc(var(--tw-split, .5) * 100vh), 50vh); }
|
|
574
|
+
.tw-stage.tw-split { padding-bottom: min(calc(var(--tw-split, .5) * 100vh), 50vh); }
|
|
446
575
|
.tw-row { grid-template-columns: minmax(0, 1fr); gap: 0; }
|
|
576
|
+
.tw-ctl { justify-content: flex-start; padding: 4px 0 6px; }
|
|
577
|
+
.tw-restore, .tw-check { width: 36px; height: 36px; }
|
|
447
578
|
.tw-note { padding: 0 0 8px 0; }
|
|
448
579
|
.tw-note:empty { display: none; }
|
|
449
580
|
.tw-done__panel { padding: 28px 24px; border-radius: 24px; }
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
12
12
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
13
13
|
<link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
|
|
14
|
-
<link rel="stylesheet" href="/builders/tweak-editor.css?v=2026-09-
|
|
14
|
+
<link rel="stylesheet" href="/builders/tweak-editor.css?v=2026-09-29-tw26-aqfix">
|
|
15
15
|
</head>
|
|
16
16
|
<!-- THE TWEAK EDITOR (task 1004321 / BV2.TW10, goal 1000095; ADR 0341 D4 and
|
|
17
17
|
D11). docs/design/mocks/tweak-mode/Editor.dc.html is the master design:
|
|
@@ -53,7 +53,13 @@
|
|
|
53
53
|
<span class="tw-name"><span id="tw-name">Tweak a page</span> <span class="tw-surface" id="tw-surface"></span></span>
|
|
54
54
|
<span class="tw-counter" id="tw-counter" aria-live="polite"></span>
|
|
55
55
|
<div class="tw-actions">
|
|
56
|
-
<
|
|
56
|
+
<div class="tw-openwrap" id="tw-openwrap">
|
|
57
|
+
<button class="tw-btn tw-btn--main" id="tw-open" type="button">open the page ↗</button><button class="tw-btn tw-btn--more" id="tw-open-more" type="button" aria-haspopup="true" aria-expanded="false" aria-controls="tw-open-menu" aria-label="More ways to open the page">▾</button>
|
|
58
|
+
<div class="tw-openmenu" id="tw-open-menu" role="menu">
|
|
59
|
+
<a class="tw-openmenu__item" id="tw-open-tab" role="menuitem" href="/builders/studio" target="_blank" rel="noopener">open in new tab</a>
|
|
60
|
+
<button class="tw-openmenu__item" id="tw-open-split" type="button" role="menuitem">open in split view</button>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
57
63
|
<button class="tw-btn" id="tw-download" type="button" disabled>download .docx</button>
|
|
58
64
|
<button class="tw-btn" id="tw-upload" type="button" disabled>upload .docx</button>
|
|
59
65
|
<input id="tw-file" type="file" accept=".docx,application/vnd.openxmlformats-officedocument.wordprocessingml.document" hidden>
|
|
@@ -62,16 +68,25 @@
|
|
|
62
68
|
</div>
|
|
63
69
|
|
|
64
70
|
<article class="tw-paper" id="tw-paper" aria-labelledby="tw-title">
|
|
65
|
-
<
|
|
66
|
-
|
|
67
|
-
<p class="tw-intro">Every word on the page, in the order it shows them. Type over anything, or download it, work in Word, and upload it back.</p>
|
|
68
|
-
</div>
|
|
71
|
+
<h1 class="tw-sr" id="tw-title">Tweak a page</h1>
|
|
72
|
+
<p class="tw-hint" id="tw-hint" hidden></p>
|
|
69
73
|
<div class="tw-notice" id="tw-notice" role="status" hidden></div>
|
|
70
74
|
<div id="tw-doc">
|
|
71
75
|
<p class="tw-loading">Reading the page…</p>
|
|
72
76
|
</div>
|
|
73
77
|
</article>
|
|
74
78
|
|
|
79
|
+
<!-- Split view: the live page below the words, both on screen at once.
|
|
80
|
+
Opened from "open the page"; the frame loads only when it opens. -->
|
|
81
|
+
<section class="tw-frame" id="tw-frame" aria-label="The live page" hidden>
|
|
82
|
+
<div class="tw-divider" id="tw-divider" role="separator" aria-orientation="horizontal" aria-label="Resize the split: drag, or use the up and down arrows" aria-valuemin="20" aria-valuemax="80" aria-valuenow="50" tabindex="0"><span class="tw-divider__grip"></span></div>
|
|
83
|
+
<div class="tw-frame__bar">
|
|
84
|
+
<span class="tw-frame__name">the live page</span>
|
|
85
|
+
<button class="tw-btn" id="tw-split-close" type="button">close split view</button>
|
|
86
|
+
</div>
|
|
87
|
+
<iframe class="tw-frame__view" id="tw-frame-view" title="The live page" referrerpolicy="no-referrer"></iframe>
|
|
88
|
+
</section>
|
|
89
|
+
|
|
75
90
|
<!-- Submit: "<page> is in", then next up. Hidden until the submit lands. -->
|
|
76
91
|
<div class="tw-done" id="tw-done" hidden>
|
|
77
92
|
<div class="tw-done__panel" role="dialog" aria-modal="true" aria-labelledby="tw-done-title">
|
|
@@ -125,10 +140,11 @@
|
|
|
125
140
|
|
|
126
141
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
127
142
|
|
|
128
|
-
<script src="/builders/shell.js?v=2026-09-
|
|
143
|
+
<script src="/builders/shell.js?v=2026-09-29-renderdeploy"></script>
|
|
129
144
|
<script src="/builders/palette.js" defer></script>
|
|
130
145
|
<script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
|
|
131
|
-
<script src="/builders/
|
|
132
|
-
<script src="/builders/tweak-editor.js?v=2026-09-
|
|
146
|
+
<script src="/builders/brand-holes.js?v=2026-09-29-aqfix"></script>
|
|
147
|
+
<script src="/builders/tweak-editor-lib.js?v=2026-09-29-tw26"></script>
|
|
148
|
+
<script src="/builders/tweak-editor.js?v=2026-09-29-tw26-aqfix"></script>
|
|
133
149
|
</body>
|
|
134
150
|
</html>
|
|
@@ -41,6 +41,12 @@
|
|
|
41
41
|
pageId: null,
|
|
42
42
|
ed: null, // the editor read
|
|
43
43
|
values: {}, // key -> the words in the box
|
|
44
|
+
checked: new Set(), // keys the artist has checked (browser only)
|
|
45
|
+
live: null, // the page's live address
|
|
46
|
+
split: false, // the split view is open
|
|
47
|
+
openAs: 'tab', // what "open the page" does: the last choice
|
|
48
|
+
splitSize: 0.5, // the live page's share of the window
|
|
49
|
+
drag: false, // the divider is being dragged
|
|
44
50
|
writable: false, // the caller holds the round
|
|
45
51
|
save: 'saved', // saved | saving | failed
|
|
46
52
|
timer: null,
|
|
@@ -58,6 +64,105 @@
|
|
|
58
64
|
|
|
59
65
|
function lineById(key) { return st.ed.reading.lines.find((l) => l.key === key); }
|
|
60
66
|
|
|
67
|
+
function paintHint() {
|
|
68
|
+
const h = $('tw-hint');
|
|
69
|
+
if (!st.ed || !st.writable || !st.ed.reading.lines.length) { h.hidden = true; return; }
|
|
70
|
+
h.textContent = lib.hintText(lib.confirmedCount(st.ed.reading.lines, st.checked), st.ed.reading.lines.length);
|
|
71
|
+
h.hidden = false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function storeKey() { return lib.confirmStoreKey(st.pageId, st.ed.reading.reading_hash); }
|
|
75
|
+
function loadChecked() {
|
|
76
|
+
let raw = null;
|
|
77
|
+
try { raw = window.localStorage.getItem(storeKey()); } catch (_) { raw = null; }
|
|
78
|
+
return lib.parseConfirmed(raw, st.ed.reading.lines);
|
|
79
|
+
}
|
|
80
|
+
function saveChecked() {
|
|
81
|
+
try { window.localStorage.setItem(storeKey(), JSON.stringify([...st.checked])); } catch (_) { /* private mode: the checks last this visit */ }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function setChecked(key, on) {
|
|
85
|
+
if (on) st.checked.add(key); else st.checked.delete(key);
|
|
86
|
+
saveChecked();
|
|
87
|
+
paintRow(key);
|
|
88
|
+
paintHint();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Put the page's own words back in the box and take the check off.
|
|
92
|
+
function restore(key) {
|
|
93
|
+
const l = lineById(key);
|
|
94
|
+
const ta = $(`tw-${key}`);
|
|
95
|
+
if (!l || !ta || !st.writable) return;
|
|
96
|
+
st.values[key] = l.text;
|
|
97
|
+
if (ta.classList.contains('tw-line--holes')) ta.innerHTML = boxHtml(l.text);
|
|
98
|
+
else { ta.value = l.text; fit(ta); }
|
|
99
|
+
st.checked.delete(key);
|
|
100
|
+
saveChecked();
|
|
101
|
+
paintRow(key);
|
|
102
|
+
paintCounter();
|
|
103
|
+
paintHint();
|
|
104
|
+
schedule();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Shift+Enter: check this line and land in the next one.
|
|
108
|
+
function checkAndAdvance(key) {
|
|
109
|
+
setChecked(key, true);
|
|
110
|
+
const next = lib.nextLineKey(st.ed.reading.lines, key);
|
|
111
|
+
const ta = next && $(`tw-${next}`);
|
|
112
|
+
if (ta) {
|
|
113
|
+
ta.focus();
|
|
114
|
+
ta.scrollIntoView({ block: st.split ? 'nearest' : 'center', behavior: 'smooth' });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// The split view: the live page under the words. The frame loads when it opens.
|
|
119
|
+
function remember(key, value) {
|
|
120
|
+
try { window.localStorage.setItem(key, value); } catch (_) { /* private mode: this visit only */ }
|
|
121
|
+
}
|
|
122
|
+
function recall(key) {
|
|
123
|
+
try { return window.localStorage.getItem(key); } catch (_) { return null; }
|
|
124
|
+
}
|
|
125
|
+
const OPEN_KEY = 'hall-tweak-open-as';
|
|
126
|
+
const SPLIT_KEY = 'hall-tweak-split';
|
|
127
|
+
const SIZE_KEY = 'hall-tweak-split-size';
|
|
128
|
+
|
|
129
|
+
function setSplit(on) {
|
|
130
|
+
if (on && !st.live) return;
|
|
131
|
+
st.split = on;
|
|
132
|
+
remember(SPLIT_KEY, on ? 'on' : 'off');
|
|
133
|
+
$('tw-frame').hidden = !on;
|
|
134
|
+
$('tw-frame-view').src = on ? st.live : 'about:blank';
|
|
135
|
+
$('tw-open-split').textContent = on ? 'close split view' : 'open in split view';
|
|
136
|
+
document.querySelector('main.tw-stage').classList.toggle('tw-split', on);
|
|
137
|
+
setMenu(false);
|
|
138
|
+
}
|
|
139
|
+
function setMenu(open) {
|
|
140
|
+
$('tw-openwrap').classList.toggle('is-open', open);
|
|
141
|
+
$('tw-open-more').setAttribute('aria-expanded', String(open));
|
|
142
|
+
}
|
|
143
|
+
function setOpenAs(choice) {
|
|
144
|
+
st.openAs = lib.openChoice(choice);
|
|
145
|
+
remember(OPEN_KEY, st.openAs);
|
|
146
|
+
$('tw-open').textContent = lib.OPEN_WORDS[st.openAs];
|
|
147
|
+
}
|
|
148
|
+
function setSize(f) {
|
|
149
|
+
st.splitSize = lib.clampSplit(f);
|
|
150
|
+
const stage = document.querySelector('main.tw-stage');
|
|
151
|
+
if (stage && stage.style && stage.style.setProperty) stage.style.setProperty('--tw-split', String(st.splitSize));
|
|
152
|
+
$('tw-divider').setAttribute('aria-valuenow', String(Math.round(st.splitSize * 100)));
|
|
153
|
+
}
|
|
154
|
+
// Split view: in place when the page can be framed from here, otherwise its
|
|
155
|
+
// own window docked to the bottom half (the site refuses a frame from another host).
|
|
156
|
+
function openSplit() {
|
|
157
|
+
if (!st.live) return;
|
|
158
|
+
if (lib.canFrame(st.live, window.location)) setSplit(!st.split);
|
|
159
|
+
else { window.open(st.live, 'tw-live-page', lib.splitWindowFeatures(window.screen)); setMenu(false); }
|
|
160
|
+
}
|
|
161
|
+
function openTab() {
|
|
162
|
+
if (st.live) window.open(st.live, '_blank', 'noopener');
|
|
163
|
+
setMenu(false);
|
|
164
|
+
}
|
|
165
|
+
|
|
61
166
|
function paintCounter() {
|
|
62
167
|
if (!st.ed) { $('tw-counter').textContent = ''; return; }
|
|
63
168
|
const changed = lib.changedCount(st.ed.reading.lines, st.values);
|
|
@@ -69,14 +174,65 @@
|
|
|
69
174
|
return n ? { cls: `tw-note tw-note--${n.kind}`, text: n.text } : { cls: 'tw-note', text: '' };
|
|
70
175
|
}
|
|
71
176
|
|
|
177
|
+
// BRAND HOLES (task 1004381). A line whose words hold a {{token}} is a typed-in
|
|
178
|
+
// box, not a textarea: the token is a chip showing the name it stands for
|
|
179
|
+
// (window.__BRANDING__), which the artist types around and cannot type over,
|
|
180
|
+
// delete, or lose the spaces beside. The stored value keeps the {{token}}.
|
|
181
|
+
const H = window.OTBBrandHoles;
|
|
182
|
+
const holed = (l) => !!(H && H.hasHoles(l.text));
|
|
183
|
+
const wordsHtml = (text) => (H ? H.holesHtml(text, { cls: 'tw-hole' }) : escapeHtml(text));
|
|
184
|
+
const boxHtml = (v) => H.holesHtml(v, { cls: 'tw-hole', inBox: true });
|
|
185
|
+
|
|
186
|
+
function lineBoxHtml(id, l, v) {
|
|
187
|
+
const changed = lib.isChanged(l, v) ? ' tw-line--changed' : '';
|
|
188
|
+
if (holed(l)) {
|
|
189
|
+
return `<div id="${id}" class="tw-line tw-line--holes${changed}" role="textbox" aria-multiline="true" aria-labelledby="${id}-label" data-key="${escapeHtml(l.key)}" contenteditable="${st.writable ? 'true' : 'false'}" spellcheck="true">${boxHtml(v)}</div>`;
|
|
190
|
+
}
|
|
191
|
+
return `<textarea id="${id}" class="tw-line${changed}" rows="${lib.rowsFor(v)}" data-key="${escapeHtml(l.key)}"${st.writable ? '' : ' readonly'} spellcheck="true">${escapeHtml(v)}</textarea>`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Put the caret at the end of a chip box (after a refused edit is undone).
|
|
195
|
+
function caretToEnd(el) {
|
|
196
|
+
try {
|
|
197
|
+
const r = document.createRange();
|
|
198
|
+
r.selectNodeContents(el);
|
|
199
|
+
r.collapse(false);
|
|
200
|
+
const s = window.getSelection();
|
|
201
|
+
s.removeAllRanges();
|
|
202
|
+
s.addRange(r);
|
|
203
|
+
} catch (_) { /* no selection API: the words are right, the caret is not */ }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// What a chip box holds now, or null when the edit removed or added a hole
|
|
207
|
+
// (the box is then put back as it was: a hole is not the artist's to change).
|
|
208
|
+
function readHoleBox(el, line) {
|
|
209
|
+
const v = H.serializeHoles(el.childNodes);
|
|
210
|
+
if (H.sameHoles(line.text, v)) return v;
|
|
211
|
+
el.innerHTML = boxHtml(st.values[line.key]);
|
|
212
|
+
caretToEnd(el);
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// The check and the restore button beside a line. A read-only page has
|
|
217
|
+
// neither: there is nothing to check off or put back.
|
|
218
|
+
function controlsHtml(l, section) {
|
|
219
|
+
if (!st.writable) return '<span class="tw-ctl"></span>';
|
|
220
|
+
const on = st.checked.has(l.key);
|
|
221
|
+
return '<span class="tw-ctl">'
|
|
222
|
+
+ `<button class="tw-restore" type="button" data-restore="${escapeHtml(l.key)}" aria-label="Restore the page's words for this ${escapeHtml(section)} line" title="Restore the original words"${lib.isChanged(l, st.values[l.key]) ? '' : ' hidden'}>↺</button>`
|
|
223
|
+
+ `<button class="tw-check" type="button" data-check="${escapeHtml(l.key)}" aria-pressed="${on}" aria-label="Check this ${escapeHtml(section)} line" title="Check this line (Shift + Enter)">✓</button>`
|
|
224
|
+
+ '</span>';
|
|
225
|
+
}
|
|
226
|
+
|
|
72
227
|
function rowHtml(section, l) {
|
|
73
228
|
const v = st.values[l.key];
|
|
74
229
|
const n = noteHtml(l);
|
|
75
230
|
const id = `tw-${l.key}`;
|
|
76
|
-
return `<div class="tw-row" data-key="${escapeHtml(l.key)}">`
|
|
77
|
-
+ `<label class="tw-sr" for="${id}">${escapeHtml(section)} line</label>`
|
|
78
|
-
+
|
|
79
|
-
+
|
|
231
|
+
return `<div class="tw-row${st.checked.has(l.key) ? ' tw-row--checked' : ''}" data-key="${escapeHtml(l.key)}">`
|
|
232
|
+
+ `<label class="tw-sr" id="${id}-label" for="${id}">${escapeHtml(section)} line</label>`
|
|
233
|
+
+ lineBoxHtml(id, l, v)
|
|
234
|
+
+ controlsHtml(l, section)
|
|
235
|
+
+ `<span class="${n.cls}" id="${id}-note">${wordsHtml(n.text)}</span>`
|
|
80
236
|
+ '</div>';
|
|
81
237
|
}
|
|
82
238
|
|
|
@@ -105,10 +261,18 @@
|
|
|
105
261
|
const ta = $(`tw-${key}`);
|
|
106
262
|
if (!l || !ta) return;
|
|
107
263
|
ta.classList.toggle('tw-line--changed', lib.isChanged(l, st.values[key]));
|
|
264
|
+
const row = ta.closest('.tw-row');
|
|
265
|
+
if (row) {
|
|
266
|
+
row.classList.toggle('tw-row--checked', st.checked.has(key));
|
|
267
|
+
const chk = row.querySelector('[data-check]');
|
|
268
|
+
if (chk) chk.setAttribute('aria-pressed', String(st.checked.has(key)));
|
|
269
|
+
const res = row.querySelector('[data-restore]');
|
|
270
|
+
if (res) res.hidden = !lib.isChanged(l, st.values[key]);
|
|
271
|
+
}
|
|
108
272
|
const n = noteHtml(l);
|
|
109
273
|
const note = $(`tw-${key}-note`);
|
|
110
274
|
note.className = n.cls;
|
|
111
|
-
note.
|
|
275
|
+
note.innerHTML = wordsHtml(n.text);
|
|
112
276
|
}
|
|
113
277
|
|
|
114
278
|
function paintHead() {
|
|
@@ -118,8 +282,12 @@
|
|
|
118
282
|
$('tw-title').textContent = lib.displayTitle(p);
|
|
119
283
|
document.title = `Tweak ${lib.displayTitle(p)} — ${document.title.split(' — ').pop()}`;
|
|
120
284
|
const live = lib.liveUrl(p.url, window.location);
|
|
121
|
-
|
|
122
|
-
if (live) open.href = live; else
|
|
285
|
+
st.live = live;
|
|
286
|
+
if (live) $('tw-open-tab').href = live; else $('tw-openwrap').hidden = true;
|
|
287
|
+
setOpenAs(recall(OPEN_KEY));
|
|
288
|
+
setSize(recall(SIZE_KEY));
|
|
289
|
+
// Split view comes back open only where it opens in place: a popup on load would be blocked.
|
|
290
|
+
if (live && recall(SPLIT_KEY) === 'on' && lib.canFrame(live, window.location) && !st.split) setSplit(true);
|
|
123
291
|
$('tw-download').disabled = false;
|
|
124
292
|
$('tw-upload').disabled = !st.writable;
|
|
125
293
|
$('tw-submit').disabled = !st.writable;
|
|
@@ -182,10 +350,12 @@
|
|
|
182
350
|
st.values = lib.initialValues(st.ed.reading.lines, st.ed.draft);
|
|
183
351
|
st.writable = !!(st.ed.round && st.ed.round.held_by_you && st.ed.round.state === 'writing');
|
|
184
352
|
st.lastSent = JSON.stringify(lib.draftBody(st.ed.reading.reading_hash, st.ed.reading.lines, st.values));
|
|
353
|
+
st.checked = loadChecked();
|
|
185
354
|
paintHead();
|
|
186
355
|
roundNotice();
|
|
187
356
|
paintDoc();
|
|
188
357
|
paintCounter();
|
|
358
|
+
paintHint();
|
|
189
359
|
}
|
|
190
360
|
|
|
191
361
|
// ---------------------------------------------------------------------------
|
|
@@ -334,6 +504,7 @@
|
|
|
334
504
|
st.save = 'saved';
|
|
335
505
|
paintDoc();
|
|
336
506
|
paintCounter();
|
|
507
|
+
paintHint();
|
|
337
508
|
btn.textContent = lib.uploadedLabel(r.data.merge);
|
|
338
509
|
}
|
|
339
510
|
|
|
@@ -353,6 +524,9 @@
|
|
|
353
524
|
st.writable = false;
|
|
354
525
|
notice('');
|
|
355
526
|
document.querySelectorAll('textarea.tw-line').forEach((ta) => { ta.readOnly = true; });
|
|
527
|
+
document.querySelectorAll('.tw-line--holes').forEach((box) => { box.setAttribute('contenteditable', 'false'); });
|
|
528
|
+
document.querySelectorAll('.tw-ctl').forEach((c) => { c.hidden = true; });
|
|
529
|
+
paintHint();
|
|
356
530
|
$('tw-upload').disabled = true;
|
|
357
531
|
showDone({});
|
|
358
532
|
}
|
|
@@ -422,20 +596,79 @@
|
|
|
422
596
|
document.addEventListener('input', (ev) => {
|
|
423
597
|
const ta = ev.target;
|
|
424
598
|
if (!ta || !ta.classList || !ta.classList.contains('tw-line') || !st.writable) return;
|
|
425
|
-
|
|
426
|
-
|
|
599
|
+
if (ta.classList.contains('tw-line--holes')) {
|
|
600
|
+
const v = readHoleBox(ta, lineById(ta.dataset.key));
|
|
601
|
+
if (v === null) return;
|
|
602
|
+
st.values[ta.dataset.key] = v;
|
|
603
|
+
} else {
|
|
604
|
+
st.values[ta.dataset.key] = ta.value;
|
|
605
|
+
fit(ta);
|
|
606
|
+
}
|
|
427
607
|
paintRow(ta.dataset.key);
|
|
428
608
|
paintCounter();
|
|
429
609
|
schedule();
|
|
430
610
|
});
|
|
611
|
+
document.addEventListener('pointerdown', (ev) => {
|
|
612
|
+
const d = ev.target && ev.target.closest && ev.target.closest('#tw-divider');
|
|
613
|
+
if (!d || !st.split) return;
|
|
614
|
+
ev.preventDefault();
|
|
615
|
+
st.drag = true;
|
|
616
|
+
$('tw-frame').classList.add('is-dragging');
|
|
617
|
+
if (d.setPointerCapture && ev.pointerId !== undefined) { try { d.setPointerCapture(ev.pointerId); } catch (_) { /* fine */ } }
|
|
618
|
+
});
|
|
619
|
+
document.addEventListener('pointermove', (ev) => {
|
|
620
|
+
if (st.drag) setSize(lib.splitFromPointer(ev.clientY, window.innerHeight));
|
|
621
|
+
});
|
|
622
|
+
const endDrag = () => {
|
|
623
|
+
if (!st.drag) return;
|
|
624
|
+
st.drag = false;
|
|
625
|
+
$('tw-frame').classList.remove('is-dragging');
|
|
626
|
+
remember(SIZE_KEY, String(st.splitSize));
|
|
627
|
+
};
|
|
628
|
+
document.addEventListener('pointerup', endDrag);
|
|
629
|
+
document.addEventListener('pointercancel', endDrag);
|
|
630
|
+
|
|
431
631
|
document.addEventListener('focusout', (ev) => {
|
|
432
632
|
if (ev.target && ev.target.classList && ev.target.classList.contains('tw-line')) flush();
|
|
433
633
|
});
|
|
434
|
-
document.addEventListener('keydown', (ev) => {
|
|
634
|
+
document.addEventListener('keydown', (ev) => {
|
|
635
|
+
if (ev.key === 'Escape') { closeRefused(); setMenu(false); }
|
|
636
|
+
if (ev.target && ev.target.id === 'tw-divider') {
|
|
637
|
+
const next = lib.splitFromKey(st.splitSize, ev.key);
|
|
638
|
+
if (next !== null) { ev.preventDefault(); setSize(next); remember(SIZE_KEY, String(st.splitSize)); }
|
|
639
|
+
}
|
|
640
|
+
const ta = ev.target;
|
|
641
|
+
if (ev.key === 'Enter' && ev.shiftKey && ta && ta.classList && ta.classList.contains('tw-line') && st.writable) {
|
|
642
|
+
ev.preventDefault();
|
|
643
|
+
checkAndAdvance(ta.dataset.key);
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
// A chip box is one line of words: a plain Enter adds nothing (a textarea's
|
|
647
|
+
// newline is tidied to a space by the server anyway).
|
|
648
|
+
if (ev.key === 'Enter' && ta && ta.classList && ta.classList.contains('tw-line--holes')) ev.preventDefault();
|
|
649
|
+
});
|
|
650
|
+
// Paste into a chip box as plain words, so no markup (or a pasted chip) rides in.
|
|
651
|
+
document.addEventListener('paste', (ev) => {
|
|
652
|
+
const box = ev.target && ev.target.closest && ev.target.closest('.tw-line--holes');
|
|
653
|
+
if (!box || !ev.clipboardData) return;
|
|
654
|
+
ev.preventDefault();
|
|
655
|
+
const text = String(ev.clipboardData.getData('text/plain') || '').replace(/\s+/g, ' ');
|
|
656
|
+
document.execCommand('insertText', false, text);
|
|
657
|
+
});
|
|
435
658
|
window.addEventListener('resize', () => { document.querySelectorAll('textarea.tw-line').forEach(fit); });
|
|
436
659
|
|
|
437
660
|
document.addEventListener('click', async (ev) => {
|
|
438
661
|
const t = ev.target;
|
|
662
|
+
const chk = t.closest('[data-check]');
|
|
663
|
+
if (chk && st.writable) { setChecked(chk.dataset.check, !st.checked.has(chk.dataset.check)); return; }
|
|
664
|
+
const res = t.closest('[data-restore]');
|
|
665
|
+
if (res) { restore(res.dataset.restore); return; }
|
|
666
|
+
if (t.closest('#tw-open-more')) { setMenu(!$('tw-openwrap').classList.contains('is-open')); return; }
|
|
667
|
+
if (t.closest('#tw-open')) { if (st.openAs === 'split') openSplit(); else openTab(); return; }
|
|
668
|
+
if (t.closest('#tw-open-tab')) { setOpenAs('tab'); setMenu(false); return; }
|
|
669
|
+
if (t.closest('#tw-open-split')) { if (!st.split) setOpenAs('split'); openSplit(); return; }
|
|
670
|
+
if (t.closest('#tw-split-close')) { setSplit(false); return; }
|
|
671
|
+
if (!t.closest('#tw-openwrap')) setMenu(false);
|
|
439
672
|
if (t.closest('#tw-download')) { download(); return; }
|
|
440
673
|
if (t.closest('#tw-upload')) { $('tw-file').click(); return; }
|
|
441
674
|
if (t.closest('#tw-submit')) { submit(); return; }
|
|
@@ -162,7 +162,7 @@
|
|
|
162
162
|
|
|
163
163
|
<!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
|
|
164
164
|
chrome exists before the page script runs. -->
|
|
165
|
-
<script src="/builders/shell.js?v=2026-09-
|
|
165
|
+
<script src="/builders/shell.js?v=2026-09-29-renderdeploy"></script>
|
|
166
166
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
167
167
|
<script src="/builders/palette.js" defer></script>
|
|
168
168
|
<!-- shared frontend toolkit (window.OTB) — loaded before watch.js, which pulls from it. -->
|
|
@@ -171,7 +171,7 @@
|
|
|
171
171
|
|
|
172
172
|
<!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
|
|
173
173
|
time so the chrome exists before the deferred page scripts execute. -->
|
|
174
|
-
<script src="/builders/shell.js?v=2026-09-
|
|
174
|
+
<script src="/builders/shell.js?v=2026-09-29-renderdeploy"></script>
|
|
175
175
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
176
176
|
<script src="/builders/palette.js" defer></script>
|
|
177
177
|
<!-- shared frontend toolkit (window.OTB) — loaded FIRST so work.js can pull from it. -->
|
|
@@ -30,3 +30,9 @@ The overlay root is `.aq-overlay` and its palette is its own token tier (`tests/
|
|
|
30
30
|
## The proof
|
|
31
31
|
|
|
32
32
|
`studio.states.json` renders six panel states (approvals, approvals-before, approvals-phone, approvals-note, approved, sent-back) beside the studio's own. Populated, against the hall-preview harness (`node scripts/hall-preview/server.js --port 4634 --fixture-me`), which answers the queue from `copy-desk__approvals.json` (built by the real `composeApprovals`), the renders from `tasks__visuals.json`, and the two decisions from its CANNED WRITES. `HALL_PREVIEW_VISUALS=<dir>` makes the harness serve real images by the fixture's names (the review sheet used TW11's real renders of landing:index). All clean at 1440, 390 and 320, dark and light. Behaviour: `tests/hall_approval_queue.mjs` (the real panel in a vm over the real composer's answer) and `tests/copy_desk_page_approve.mjs` (the routes and the lifecycle port, whole path).
|
|
33
|
+
|
|
34
|
+
## It fits the screen, and a brand hole reads as the name (task 1004381)
|
|
35
|
+
|
|
36
|
+
Found on the live site with the first real rounds. **The dialog is capped by the viewport too** (`min(1180px, 100%, 100vw - 48px)` by `min(820px, 100%, 100dvh - 48px)`), not only by its overlay, so no ancestor and no hall zoom (`shell.js` sets CSS `zoom` on `<html>` at 125 and 150 percent) can push it past the edge. The header and the decision row wrap, the rendered page shrinks first, and the words column keeps the artboard's 300px until the page has shrunk, then narrows to 240 (`clamp(240px, 32%, 300px)`). Long words break. Checked with the kit at 1440, 1280, 1024 (a size the kit gained for this, opt-in) and 390, light and dark, and measured at 1.5x hall zoom, where the old sheet ran the hint past the dialog at 900 and 800 wide.
|
|
37
|
+
|
|
38
|
+
**A `{{productName}}` in a line or its "was:" reads as the product name**, in a quiet chip (`.aq-hole`), from `window.__BRANDING__` through `brand-holes.js` (shared with the tweak editor). The stored text keeps the token; only the display resolves it. `tests/hall_approval_queue.mjs` pins both.
|
|
@@ -26,8 +26,14 @@ The paper is paper and the room is the room whatever the hall's theme: the stage
|
|
|
26
26
|
- **"pick another" opens the other pages in place** as chips under the actions (the studio artboard's own "pick another"), where the editor artboard linked to the studio, whose flow panel is TW16's.
|
|
27
27
|
- **Boxes grow to their words** rather than the artboard's rows 1 or 2.
|
|
28
28
|
- **A refused upload opens a panel** (task 1004354, `#tw-refused`): the "is in" panel's glass and scrim over the whole window, a headline per refusal (`lib.uploadRefusal`), the server's words, the lines it names, "what to do", and "download this page again" or close (Escape too). An artist uploads from the bar, often far down the page, where the paper's notice was off screen. The notice stays on the paper after the panel closes. The kit has no file-upload action, so the panel is proven in `tests/hall_tweak_editor.mjs` and by shots of a real browser at 1440 and 390, not by a states.json state.
|
|
29
|
+
- **The check, the restore and Shift + Enter (task 1004385, owner-asked).** Beside each line a round restore button (only on a changed line: it puts the page's words back and takes the check off) and a round green check. A checked line takes a light green wash; the owner picked the check over a tint alone. **Shift + Enter** in a line checks it and lands in the next, and a green hint above the words says so (with "N of M checked"), because it is the quickest way through a page. Enter alone is unchanged. The checks are the artist's place-keeper and stay in the browser (`localStorage`, keyed by page and reading hash, so a page that moved never ticks the wrong words); the server has no such field and `WRITES` is unchanged. A page the caller cannot write has none of it. Rules in `tweak-editor-lib.js` (`nextLineKey`, `confirmedCount`, `parseConfirmed`, `confirmStoreKey`, `hintText`).
|
|
30
|
+
- **No page title or sub text at the top of the paper, and "open the page" is a menu (task 1004386, owner-asked).** The bar already names the page, so the paper opens on its first section; the h1 stays, visually hidden, for the one-h1 floor. "open the page" is a **split button** (owner-amended mid-build): the main half repeats the artist's last choice ("open the page ↗" for a new tab, "open in split view"), and the ▾ beside it opens **open in new tab** and **open in split view**. Click, keyboard and touch; hover alone opens nothing, because a hover menu closes when the pointer wanders and does not exist on touch. Split view has a **draggable divider** (mouse, touch, or the arrow keys and Home/End on the focused divider; the live page takes 20% to 80% of the window, `lib.clampSplit`), and the frame stops taking the pointer while it is dragged. The choice, the size and whether split view was open are remembered (`hall-tweak-open-as`, `hall-tweak-split-size`, `hall-tweak-split`), so the next page opens the same way; a split that would be a second window never reopens on load, because a browser blocks a popup nobody clicked. At phone width the live page takes at most half the screen, whatever the saved size, because the two-row bar is tall there. Split view pins the live page under the words (words above, page below, both scroll; "close split view" unloads it; the frame loads only when opened). **The live site answers `X-Frame-Options: SAMEORIGIN`** (set at the proxy in front of the app, not in this repo), so only a page on the editor's own host can be framed. A landing or status page is on another host, so its split view opens the page in a second window docked to the bottom half of the screen (`lib.canFrame`, `lib.splitWindowFeatures`). Allowing framing from the builders host (a `frame-ancestors` rule at the proxy) would let those pages split in place too.
|
|
29
31
|
- **Not on the artboard at all:** the phone layout (the bar becomes a two-row glass card, notes drop under their lines), the shell's full-screen button top right, the read-only and refusal notices on the paper, the chrome view (the switch off), and light mode.
|
|
30
32
|
|
|
31
33
|
## The proof
|
|
32
34
|
|
|
33
35
|
`tweak-editor.states.json` renders six states (doc, typed, chrome, submitted, picking, out). Against the kit's own stub the page renders its could-not-read state; populated, it renders against the hall-preview harness (`node scripts/hall-preview/server.js --port 4631 --fixture-me`), which answers the read from `copy-desk__pages__draft.json` (the real `landing:index` reading, through the real composer, with the artboard's two rewrites as the draft), next up from `copy-desk__next.json`, and the autosave, the submit and the claim from its CANNED WRITES (fixed answers that change nothing). All clean at 1440, 390 and 320, dark and light. The artboard needs the Design runtime, so for the review sheet it was rendered by expanding its own template over its own `renderVals()`; the ship notes of task 1004321 carry the sheet's path and every difference, which is the list above.
|
|
36
|
+
|
|
37
|
+
## A brand hole is a chip the artist types around (task 1004381)
|
|
38
|
+
|
|
39
|
+
A line whose words hold a `{{token}}` (the page's own brand placeholder, kept by name in the reading) is a typed-in box (`div.tw-line.tw-line--holes`, `role="textbox"`, `contenteditable`) instead of a textarea, because a textarea cannot hold a chip. The token is a `contenteditable="false"` chip showing the name it stands for (`window.__BRANDING__`, `brand-holes.js`), so the artist types around it and never over it; the box is `white-space: pre-wrap` so every space they type beside it is kept. The value the autosave PUTs is the box read back with each chip as its `{{token}}` again. An edit that deletes or adds a hole is undone on the spot and saves nothing (a hole is not the artist's to change, and `copy-apply.js` now refuses such a line as `placeholder_mismatch` too). Enter adds nothing and a paste lands as plain words. Lines with no hole keep the textarea exactly as before. The first real round (Contact) came back as "This project,{{productName}}has two environments", which is the bug this closes. `tests/hall_tweak_editor.mjs` pins the box, the save, and the undo.
|