@bongos/core 1.24.79 → 1.24.81
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 +554 -394
- package/.claude/skills/ask-for-help/SKILL.md +1 -1
- package/.claude/skills/builder-sequence/SKILL.md +1 -1
- package/.claude/skills/design/SKILL.md +1 -1
- package/.claude/skills/goal-create/SKILL.md +1 -1
- package/.claude/skills/planning-session/SKILL.md +1 -1
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +8 -0
- package/clients/bongos-client/index.cjs +8 -0
- package/clients/bongos-client/index.d.ts +11 -0
- package/clients/bongos-client/index.mjs +8 -0
- package/docs/adr/0274-one-kernel-three-role-packs.md +2 -2
- package/docs/adr/0399-the-visor-is-glass-over-the-scene-and-owns-no-camera.md +91 -0
- package/docs/adr/0400-a-chain-reserves-work-and-each-sub-quest-is-still-claimed-alone.md +109 -0
- package/docs/adr/0401-one-options-menu-opens-every-settings-screen-in-a-frame.md +53 -0
- package/docs/adr/0402-helm-is-the-halls-look-and-the-hall-is-dark-only.md +142 -0
- package/docs/adr/README.md +4 -0
- package/docs/api/openapi.json +195 -3
- package/docs/api-reference.md +8 -4
- package/docs/architecture.md +7 -3
- package/docs/copy-inventory.md +393 -378
- package/docs/copy-registry.json +722 -578
- package/docs/design/characters/character-sprites-source.webp +0 -0
- package/docs/file-map.md +7 -8
- package/docs/module-api-changelog.md +4 -0
- package/docs/modules-contract.md +5 -0
- package/docs/onboarding/slash-commands.md +1 -1
- package/docs/packs/{engineer.md → artisan.md} +5 -5
- package/docs/packs/artist.md +50 -4
- package/docs/page-inventory.json +16 -7
- package/docs/page-readings.json +3184 -3338
- package/docs/{packs/ideator.md → recipes/ideation-playbook.md} +5 -48
- package/migrations/core_296_crafts_artisan_artist.sql +72 -0
- package/migrations/core_297_builder_character.sql +30 -0
- package/modules/copy-desk/proposals.js +1 -1
- package/modules/economy/credits-by-craft.js +29 -15
- package/modules/hall-ui/public/ai-studio.html +6 -4
- package/modules/hall-ui/public/ai-studio.states.json +1 -0
- package/modules/hall-ui/public/atlas.html +5 -5
- package/modules/hall-ui/public/atlas.js +3 -1
- package/modules/hall-ui/public/atlas.states.json +1 -0
- package/modules/hall-ui/public/blockers.html +5 -5
- package/modules/hall-ui/public/board-room.html +4 -4
- package/modules/hall-ui/public/board-room.states.json +1 -0
- package/modules/hall-ui/public/characters/artisan.png +0 -0
- package/modules/hall-ui/public/characters/artist.png +0 -0
- package/modules/hall-ui/public/characters/patron.png +0 -0
- package/modules/hall-ui/public/city.css +3 -4
- package/modules/hall-ui/public/city.html +9 -9
- package/modules/hall-ui/public/city.js +17 -56
- package/modules/hall-ui/public/city.states.json +7 -32
- package/modules/hall-ui/public/collab.html +4 -4
- package/modules/hall-ui/public/collab.states.json +1 -0
- package/modules/hall-ui/public/commands.html +4 -4
- package/modules/hall-ui/public/commands.states.json +1 -0
- package/modules/hall-ui/public/controls.css +63 -11
- package/modules/hall-ui/public/copy-desk.html +4 -4
- package/modules/hall-ui/public/copy-desk.states.json +1 -0
- package/modules/hall-ui/public/craft-mode.js +258 -64
- package/modules/hall-ui/public/deploy.html +4 -4
- package/modules/hall-ui/public/deploy.states.json +1 -0
- package/modules/hall-ui/public/diagram-viewer.js +2 -1
- package/modules/hall-ui/public/dom-utils.js +1 -1
- package/modules/hall-ui/public/goal-map.html +9 -6
- package/modules/hall-ui/public/goals.html +5 -5
- package/modules/hall-ui/public/goals.states.json +1 -0
- package/modules/hall-ui/public/government.html +4 -4
- package/modules/hall-ui/public/government.states.json +1 -0
- package/modules/hall-ui/public/hall-render.js +3 -1
- package/modules/hall-ui/public/home-city-draw.js +3 -3
- package/modules/hall-ui/public/home-city.css +4 -0
- package/modules/hall-ui/public/home-city.js +17 -9
- package/modules/hall-ui/public/home-gallery.js +8 -1
- package/modules/hall-ui/public/home-idea-box.css +2 -2
- package/modules/hall-ui/public/home-idea-box.js +3 -3
- package/modules/hall-ui/public/idea.html +5 -5
- package/modules/hall-ui/public/idea.states.json +1 -0
- package/modules/hall-ui/public/ideas.html +5 -5
- package/modules/hall-ui/public/ideas.states.json +1 -0
- package/modules/hall-ui/public/index-founding.states.json +1 -0
- package/modules/hall-ui/public/index-genesis-decide.states.json +1 -0
- package/modules/hall-ui/public/index-genesis-demo.states.json +1 -0
- package/modules/hall-ui/public/index-genesis.states.json +1 -0
- package/modules/hall-ui/public/index.html +11 -7
- package/modules/hall-ui/public/index.states.json +1 -0
- package/modules/hall-ui/public/market-watch-item.html +4 -4
- package/modules/hall-ui/public/market-watch-item.states.json +1 -0
- package/modules/hall-ui/public/market-watch-rules.html +4 -4
- package/modules/hall-ui/public/market-watch-rules.states.json +1 -0
- package/modules/hall-ui/public/market-watch.html +6 -9
- package/modules/hall-ui/public/market-watch.js +22 -45
- package/modules/hall-ui/public/market-watch.states.json +1 -0
- package/modules/hall-ui/public/modules.html +4 -4
- package/modules/hall-ui/public/modules.states.json +1 -0
- package/modules/hall-ui/public/money.html +4 -4
- package/modules/hall-ui/public/money.states.json +1 -0
- package/modules/hall-ui/public/my-agents.html +4 -4
- package/modules/hall-ui/public/my-agents.states.json +1 -0
- package/modules/hall-ui/public/options-menu.js +409 -0
- package/modules/hall-ui/public/paintbrush.js +3 -3
- package/modules/hall-ui/public/palette.js +11 -13
- package/modules/hall-ui/public/primer.html +5 -5
- package/modules/hall-ui/public/primer.states.json +1 -0
- package/modules/hall-ui/public/profile-crafts.js +1 -1
- package/modules/hall-ui/public/profile-roles.js +23 -19
- package/modules/hall-ui/public/profile.css +1 -1
- package/modules/hall-ui/public/profile.html +19 -18
- package/modules/hall-ui/public/profile.js +19 -18
- package/modules/hall-ui/public/profile.states.json +5 -4
- package/modules/hall-ui/public/project-settings.html +4 -4
- package/modules/hall-ui/public/roster.html +4 -4
- package/modules/hall-ui/public/roster.js +3 -3
- package/modules/hall-ui/public/roster.states.json +1 -0
- package/modules/hall-ui/public/settings.css +24 -4
- package/modules/hall-ui/public/settings.html +21 -17
- package/modules/hall-ui/public/settings.js +44 -44
- package/modules/hall-ui/public/settings.states.json +1 -0
- package/modules/hall-ui/public/shell-game.css +110 -0
- package/modules/hall-ui/public/shell-game.js +316 -3
- package/modules/hall-ui/public/shell.js +15 -66
- package/modules/hall-ui/public/sky-interaction.js +7 -0
- package/modules/hall-ui/public/speciality-tools.js +1 -2
- package/modules/hall-ui/public/street-card.states.json +1 -0
- package/modules/hall-ui/public/street-inside.js +3 -4
- package/modules/hall-ui/public/street.css +1 -5
- package/modules/hall-ui/public/street.html +6 -10
- package/modules/hall-ui/public/street.js +8 -59
- package/modules/hall-ui/public/street.states.json +7 -32
- package/modules/hall-ui/public/studio.html +5 -5
- package/modules/hall-ui/public/studio.states.json +1 -0
- package/modules/hall-ui/public/style.css +197 -112
- package/modules/hall-ui/public/task-detail.js +5 -4
- package/modules/hall-ui/public/task.html +5 -5
- package/modules/hall-ui/public/task.states.json +1 -0
- package/modules/hall-ui/public/thinking.css +10 -0
- package/modules/hall-ui/public/thinking.html +9 -6
- package/modules/hall-ui/public/thinking.states.json +1 -0
- package/modules/hall-ui/public/tweak-editor.html +5 -5
- package/modules/hall-ui/public/tweak-editor.js +5 -1
- package/modules/hall-ui/public/tweak-editor.states.json +1 -0
- package/modules/hall-ui/public/update-pill.js +91 -2
- package/modules/hall-ui/public/visor.css +80 -0
- package/modules/hall-ui/public/visor.js +316 -0
- package/modules/hall-ui/public/watch-repo-invite.js +159 -0
- package/modules/hall-ui/public/watch.html +6 -5
- package/modules/hall-ui/public/watch.js +17 -2
- package/modules/hall-ui/public/watch.states.json +1 -0
- package/modules/hall-ui/public/welcome-boot.js +15 -6
- package/modules/hall-ui/public/work-founding.states.json +1 -0
- package/modules/hall-ui/public/work.html +5 -5
- package/modules/hall-ui/public/work.states.json +1 -0
- package/modules/hall-ui/records/builders-roster.md +2 -2
- package/modules/hall-ui/records/craft-mode.md +13 -11
- package/modules/hall-ui/records/game-mode.md +2 -2
- package/modules/hall-ui/records/governor-street.md +1 -1
- package/modules/hall-ui/records/home-city.md +3 -1
- package/modules/hall-ui/records/market-watch-settings.md +2 -0
- package/modules/hall-ui/records/options-menu.md +15 -0
- package/modules/hall-ui/records/profile-roles.md +11 -11
- package/modules/hall-ui/records/sky.md +3 -3
- package/modules/hall-ui/records/start-screen.md +25 -0
- package/modules/hall-ui/records/tweak-editor.md +1 -1
- package/modules/hall-ui/records/update-pill.md +12 -0
- package/modules/hall-ui/records/visor.md +23 -0
- package/modules/hall-ui/records/welcome-tower.md +5 -5
- package/modules/hall-ui/specimens/components.states.json +2 -1
- package/modules/ideas/ideator-stats.js +3 -1
- package/modules/ideas/inbox.js +2 -2
- package/modules/lifecycle/activity.js +1 -1
- package/modules/lifecycle/cascade.js +4 -4
- package/modules/lifecycle/goal-spec-task.js +1 -1
- package/modules/lifecycle/help-requests.js +19 -15
- package/modules/lifecycle/kickoff-checklist.js +3 -3
- package/modules/lifecycle/kickoff-seed.js +1 -1
- package/modules/lifecycle/migrations/lifecycle_021_crafts_artisan_artist.sql +29 -0
- package/modules/lifecycle/mingle.js +4 -4
- package/modules/lifecycle/page-tweak-write.js +1 -1
- package/modules/lifecycle/routes/task-write-routes.js +3 -1
- package/modules/lifecycle/routes/tasks.js +3 -3
- package/modules/lifecycle/ship-card.js +2 -3
- package/modules/lifecycle/task-classifier.js +39 -29
- package/modules/lifecycle/tests/cascade.mjs +7 -7
- package/modules/onboarding/founding-mode.js +3 -0
- package/modules/onboarding/genesis-home.js +21 -1
- package/modules/onboarding/routes/onboarding.js +2 -0
- package/modules/page-notes/notes.js +3 -2
- package/modules/page-notes/split.js +1 -1
- package/modules/platform-identity/craft-rollup.js +35 -7
- package/modules/platform-identity/migrations/platform_identity_036_crafts_artisan_artist.sql +45 -0
- package/modules/platform-identity/platform-identity.js +2 -2
- package/modules/platform-identity/routes/community-leaderboard.js +2 -1
- package/modules/platform-identity/routes/community-search.js +2 -1
- package/modules/platform-identity/routes/sso.js +2 -1
- package/modules/platform-identity/tests/platform-identity.mjs +6 -6
- package/modules/public-landing/public/community.html +3 -5
- package/modules/public-landing/public/index.html +1 -1
- package/modules/public-landing/public/projects.html +1 -1
- package/modules/specialities/migrations/specialities_004_crafts_artisan_artist.sql +30 -0
- package/modules/specialities/routes/specialities.js +2 -2
- package/modules/status-ui/public/status.js +1 -1
- package/modules/ui-design/kit/fixtures/community-leaderboard.json +10 -10
- package/modules/ui-design/kit/fixtures/community-search.json +2 -2
- package/modules/ui-design/kit/fixtures/scouting-recruiter.json +1 -1
- package/modules/ui-design/public/brand.html +2 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +76 -0
- package/scripts/bongos/autobongos-loop.js +1 -1
- package/scripts/bongos/discipline-modes.json +7 -12
- package/scripts/bongos/fitness.js +1 -1
- package/scripts/bongos/gen-hall-sitemap.js +7 -7
- package/scripts/bongos/grade-smoke.js +2 -2
- package/scripts/bongos/init.js +2 -2
- package/scripts/bongos/newcomer-floor.config.json +1 -2
- package/scripts/bongos/newcomer-floor.js +4 -3
- package/scripts/bongos/newcomer-floor.templates.json +10 -12
- package/scripts/bongos/newcomer-floor.test.js +47 -38
- package/scripts/bongos/role-pack-guard.js +5 -4
- package/scripts/bongos/seed-tasks.example.json +3 -3
- package/scripts/bongos/setup.js +20 -107
- package/scripts/bongos/start.js +2 -2
- package/scripts/bongos/task.js +5 -4
- package/scripts/bongos/triage.js +1 -1
- package/scripts/bongos/worktree.js +16 -4
- package/scripts/hall-preview/README.md +2 -3
- package/scripts/setup-builder.js +6 -46
- package/src/bongos/auth-admission.js +8 -8
- package/src/bongos/craft-mode.js +2 -0
- package/src/bongos/db.js +39 -1
- package/src/bongos/github-repos.js +78 -0
- package/src/bongos/module-scope-map.js +22 -0
- package/src/bongos/role-stats.js +27 -11
- package/src/bongos/routes/github.js +100 -2
- package/src/bongos/routes/me.js +33 -0
- package/src/bongos/serve-internal.js +33 -11
- package/src/crafts.js +66 -0
- package/src/hall-helm.js +79 -0
- package/src/module-api.js +6 -1
- package/src/module-loader/manifest-schema.js +16 -0
- package/src/modules.js +2 -2
- package/src/role-responsibilities.js +6 -5
- package/tests/acclaim_feed_evidence_db.mjs +1 -1
- package/tests/activity_rollup_as_of.mjs +1 -1
- package/tests/api_alias_caller_ratchet.mjs +3 -3
- package/tests/application_lifecycle.mjs +1 -1
- package/tests/artist_review.mjs +1 -1
- package/tests/artist_review_goal.mjs +1 -1
- package/tests/blocker_hall_live_proof.mjs +1 -1
- package/tests/blocker_tasks_named_db.mjs +2 -2
- package/tests/canonical_profile_url.mjs +1 -1
- package/tests/cap_newcomer_tasks.mjs +13 -13
- package/tests/character_creation.mjs +294 -0
- package/tests/claim_gate_open_blocker.mjs +2 -2
- package/tests/claim_gate_rebase.mjs +6 -6
- package/tests/claude_materialize.mjs +1 -1
- package/tests/clawback_confirmed_backlog.mjs +3 -3
- package/tests/collab_page.mjs +8 -9
- package/tests/collab_self_settling.mjs +2 -2
- package/tests/community_leaderboard_route.mjs +13 -5
- package/tests/community_proof_db.mjs +3 -0
- package/tests/community_search_route.mjs +7 -6
- package/tests/conductor.mjs +1 -1
- package/tests/conductor_main_e2e.mjs +8 -8
- package/tests/copy_desk_flags_db.mjs +1 -1
- package/tests/copy_desk_page_approve.mjs +2 -2
- package/tests/copy_desk_page_claim.mjs +2 -2
- package/tests/copy_desk_page_next.mjs +1 -1
- package/tests/copy_desk_page_release.mjs +1 -1
- package/tests/copy_desk_page_submit.mjs +2 -2
- package/tests/core_258_box_session_claims_db.mjs +1 -1
- package/tests/craft_mode.mjs +48 -34
- package/tests/craft_mode_home.mjs +20 -20
- package/tests/craft_mode_looks.mjs +16 -15
- package/tests/craft_mode_nav.mjs +31 -29
- package/tests/craft_review_mode.mjs +46 -41
- package/tests/crafts_vocabulary.mjs +70 -0
- package/tests/cross_discipline_priority_direction.mjs +5 -5
- package/tests/design_reference.mjs +1 -1
- package/tests/fitness.mjs +7 -11
- package/tests/funded_project_pays_proof_db.mjs +1 -1
- package/tests/genesis_home.mjs +21 -1
- package/tests/github_chip.mjs +3 -3
- package/tests/github_repo_invite.mjs +379 -0
- package/tests/goal_criterion_confirm_ui.mjs +1 -1
- package/tests/goal_invite_consent.mjs +1 -1
- package/tests/goal_map_page.mjs +2 -2
- package/tests/goal_sort_staleness.mjs +1 -1
- package/tests/goal_spec_task.mjs +1 -1
- package/tests/goal_spec_task_db.mjs +3 -3
- package/tests/grade_engine.mjs +2 -2
- package/tests/grader_no_artifact.mjs +1 -1
- package/tests/grader_prompt_fencing.mjs +1 -1
- package/tests/grader_prompt_window_bound.mjs +1 -1
- package/tests/grader_reader_lens.mjs +2 -2
- package/tests/grader_severity_gate.mjs +1 -1
- package/tests/grader_unbacked_score.mjs +1 -1
- package/tests/hall_approval_queue.mjs +1 -1
- package/tests/hall_board_world.mjs +2 -2
- package/tests/hall_builders_page_pass.mjs +3 -2
- package/tests/hall_city.mjs +16 -30
- package/tests/hall_controls.mjs +52 -4
- package/tests/hall_fullscreen.mjs +27 -3
- package/tests/hall_helm.mjs +187 -0
- package/tests/hall_kit_charts.mjs +1 -1
- package/tests/hall_landing_boot.mjs +1 -1
- package/tests/hall_my_agents.mjs +16 -16
- package/tests/hall_nav.mjs +22 -15
- package/tests/hall_options_menu.mjs +247 -0
- package/tests/hall_oversight_world.mjs +1 -1
- package/tests/hall_palette.mjs +8 -9
- package/tests/hall_profile_world.mjs +3 -3
- package/tests/hall_record_world.mjs +6 -4
- package/tests/hall_settings_world.mjs +2 -1
- package/tests/hall_sitemap.mjs +6 -5
- package/tests/hall_start_screen.mjs +307 -0
- package/tests/hall_street.mjs +15 -28
- package/tests/hall_street_inside.mjs +17 -9
- package/tests/hall_studio_world.mjs +3 -3
- package/tests/hall_topbar_320.mjs +4 -2
- package/tests/hall_visor.mjs +224 -0
- package/tests/help_requests.mjs +47 -15
- package/tests/helpers.mjs +4 -4
- package/tests/home_city.mjs +6 -4
- package/tests/home_city_widget.mjs +33 -14
- package/tests/home_gallery_widget.mjs +4 -4
- package/tests/home_idea_box.mjs +6 -6
- package/tests/hub_community_page.mjs +3 -3
- package/tests/hub_craft_rollup.mjs +75 -36
- package/tests/hub_recent_ships.mjs +3 -2
- package/tests/idea_cascade.mjs +2 -2
- package/tests/idea_credit_streams_db.mjs +1 -1
- package/tests/idea_detail_page.mjs +1 -1
- package/tests/idea_ideate_full.mjs +5 -2
- package/tests/idea_spark_hall.mjs +1 -1
- package/tests/init.mjs +3 -3
- package/tests/instance_manifest.mjs +2 -2
- package/tests/kickoff_checklist.mjs +1 -1
- package/tests/leaderboard_roles.mjs +34 -25
- package/tests/lifecycle_claim_ladder_refs.mjs +55 -0
- package/tests/lightbulb.mjs +14 -10
- package/tests/main_discipline.mjs +8 -8
- package/tests/markdown_line_endings.mjs +1 -1
- package/tests/market_watch_pages.mjs +52 -74
- package/tests/me_streak_leaderboard_queries.mjs +3 -3
- package/tests/migration_allocation.mjs +1 -1
- package/tests/mingle.mjs +36 -21
- package/tests/module-scope-map.mjs +22 -2
- package/tests/module_api.mjs +1 -0
- package/tests/module_contributions.mjs +13 -13
- package/tests/module_manifest.mjs +29 -0
- package/tests/module_skills_hook.mjs +2 -2
- package/tests/modules.mjs +14 -7
- package/tests/nav_permission_atoms.mjs +2 -1
- package/tests/needs_rebase.mjs +1 -1
- package/tests/newcomer_restock.mjs +13 -13
- package/tests/page_notes_panel.mjs +22 -22
- package/tests/page_tweak_no_block.mjs +1 -1
- package/tests/paintbrush.mjs +7 -7
- package/tests/privacy_matrix.mjs +1 -0
- package/tests/profile_activity.mjs +20 -23
- package/tests/profile_earnings.mjs +1 -1
- package/tests/profile_nudge_prefs.mjs +4 -4
- package/tests/profile_role_headline.mjs +73 -46
- package/tests/profile_role_stats.mjs +55 -45
- package/tests/profile_rollup_consent.mjs +19 -6
- package/tests/profile_route.mjs +12 -12
- package/tests/profile_ui_cross_project.mjs +4 -4
- package/tests/project_invite_suggestions.mjs +2 -2
- package/tests/promote_goal_id.mjs +1 -1
- package/tests/publish_manifest.mjs +1 -1
- package/tests/rank_floor_wiring.mjs +1 -1
- package/tests/regrade_actual_minutes.mjs +1 -1
- package/tests/removal_proposal.mjs +2 -2
- package/tests/reward_gate.mjs +4 -4
- package/tests/reward_mode_cost_plus_only.mjs +1 -1
- package/tests/role_responsibilities.mjs +18 -12
- package/tests/role_stats_db.mjs +17 -16
- package/tests/scouting_rollup_fanout.mjs +5 -5
- package/tests/seed_tasks.mjs +5 -2
- package/tests/session_earnings_mirror_db.mjs +1 -1
- package/tests/session_reward_on_land_db.mjs +1 -1
- package/tests/session_reward_reconcile_db.mjs +2 -2
- package/tests/sessions_kit_pager.mjs +1 -1
- package/tests/settings_art_key_absent.mjs +2 -2
- package/tests/settings_main_role.mjs +21 -20
- package/tests/setup_flow.mjs +3 -3
- package/tests/setup_smoke.mjs +20 -0
- package/tests/ship_card.mjs +6 -6
- package/tests/specialities.mjs +16 -4
- package/tests/speciality_db_shape.mjs +6 -6
- package/tests/speciality_injection.mjs +17 -17
- package/tests/speciality_offer_ui.mjs +5 -5
- package/tests/speciality_offered.mjs +6 -5
- package/tests/speciality_session_skills.mjs +7 -7
- package/tests/speciality_skills.mjs +11 -11
- package/tests/speciality_walkthrough_ui.mjs +2 -2
- package/tests/sso_checkin_identity_spoof.mjs +2 -1
- package/tests/task_classifier_vocab.mjs +10 -0
- package/tests/task_cli.mjs +11 -3
- package/tests/task_criteria_unlink_db.mjs +2 -2
- package/tests/task_discipline_required.mjs +38 -16
- package/tests/task_goal_required.mjs +4 -4
- package/tests/task_needs_db.mjs +1 -1
- package/tests/tweak_replaces_artist_review.mjs +1 -1
- package/tests/update_pill.mjs +153 -2
- package/tests/upkeep_home.mjs +1 -1
- package/tests/v3_c1_options_proof.mjs +145 -0
- package/tests/visual_review.mjs +3 -3
- package/tests/watch_applications_queue.mjs +8 -5
- package/tests/watch_sealed_floor.mjs +3 -1
- package/tests/welcome_boot.mjs +12 -9
- package/tests/welcome_boot_browser.mjs +13 -13
- package/tests/worktree_helper.mjs +16 -1
package/tests/hall_city.mjs
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
// the ring road, or a task with no building;
|
|
13
13
|
// * the line under the button saying anything but the docket's own line;
|
|
14
14
|
// * the start governing button going anywhere but the street;
|
|
15
|
-
// *
|
|
16
|
-
//
|
|
15
|
+
// * a light/dark switch coming back: the hall is dark only (task 1005192,
|
|
16
|
+
// ADR 0402), so the city draws its night faces and reads no stored choice;
|
|
17
17
|
// * motion that escapes the Kill Switch, or docket text reaching innerHTML.
|
|
18
18
|
//
|
|
19
19
|
// Run: node tests/hall_city.mjs
|
|
@@ -263,7 +263,7 @@ test('parcels ride only the roads of plots that hold a district, and leave throu
|
|
|
263
263
|
|
|
264
264
|
// ── the page ────────────────────────────────────────────────────────────────
|
|
265
265
|
|
|
266
|
-
test('the page holds only the wordmark, the
|
|
266
|
+
test('the page holds only the wordmark, the city, one door and one line, and no light/dark switch', () => {
|
|
267
267
|
const html = read('city.html').replace(/<!--[\s\S]*?-->/g, ''); // the markup, not the prose about it
|
|
268
268
|
assert.equal((html.match(/<header\b/g) || []).length, 1);
|
|
269
269
|
assert.equal((html.match(/<main\b/g) || []).length, 1);
|
|
@@ -271,7 +271,8 @@ test('the page holds only the wordmark, the switch, the city, one button and one
|
|
|
271
271
|
assert.match(html, /<a class="city-start" id="cityStart" href="#decide"/);
|
|
272
272
|
assert.equal((html.match(/<a\b/g) || []).length, 3, 'one door, and the two faint archived links to the street');
|
|
273
273
|
assert.ok(html.includes('<p class="city-archived">Archived: <a href="/builders/street">Street</a> & <a href="/builders/street#building=charter">Building Level Views</a>.</p>'), 'the street is archived behind two faint links');
|
|
274
|
-
assert.equal((html.match(/<button\b/g) || []).length,
|
|
274
|
+
assert.equal((html.match(/<button\b/g) || []).length, 0, 'no switch: the hall is dark only (task 1005192, ADR 0402)');
|
|
275
|
+
assert.doesNotMatch(html, /cityMode/);
|
|
275
276
|
assert.match(html, /class="city-mark">\{\{productName\}\}</);
|
|
276
277
|
assert.equal(City.STREET_HREF, '/builders/street');
|
|
277
278
|
});
|
|
@@ -289,7 +290,7 @@ test('the city sits in the hall chrome, behind the shell\'s full-screen switch',
|
|
|
289
290
|
// task 1004685 (the living city): gov-living.js, the model under the drawing, and
|
|
290
291
|
// gov-feeds.js, its one reader per page (shared with the shell's tally), load before it
|
|
291
292
|
// task 1004702: the card answers its first ask in place, so the deck's card and sender (and the desk rows under them) load before the page
|
|
292
|
-
assert.deepEqual(scripts, ['dom-utils', 'shell-game', 'shell-trail', 'craft-mode', 'shell', 'hall-dialog', 'palette', 'gov-living', 'gov-feeds', 'city-draw', 'hall-kit', 'street-draw', 'street-card', 'street-inside', 'street-desk', 'street-deck', 'city-decide', 'city-switchboard', 'city'], 'the shell first, then the shared dialog (task 1005085)
|
|
293
|
+
assert.deepEqual(scripts, ['dom-utils', 'shell-game', 'shell-trail', 'craft-mode', 'shell', 'hall-dialog', 'visor', 'palette', 'gov-living', 'gov-feeds', 'city-draw', 'hall-kit', 'street-draw', 'street-card', 'street-inside', 'street-desk', 'street-deck', 'city-decide', 'city-switchboard', 'city'], 'the shell first, then the shared dialog (task 1005085) and the visor (task 1005148)');
|
|
293
294
|
// task 1004925: every script but dom-utils.js is deferred, so none holds up the
|
|
294
295
|
// parser; defer runs them in document order, which is why the order above still
|
|
295
296
|
// holds. dom-utils.js stays synchronous: the one-escaper guard
|
|
@@ -346,7 +347,7 @@ function fakeDom() {
|
|
|
346
347
|
return el;
|
|
347
348
|
};
|
|
348
349
|
const html = mk('html');
|
|
349
|
-
for (const id of ['
|
|
350
|
+
for (const id of ['cityStage', 'cityFlat', 'cityBlds', 'cityParcels', 'cityPins', 'cityLine', 'cityStart']) mk('x').setAttribute('id', id);
|
|
350
351
|
const document = {
|
|
351
352
|
documentElement: html,
|
|
352
353
|
getElementById: (id) => byId.get(id) || null,
|
|
@@ -421,33 +422,18 @@ test('a failed docket read draws no pins and says the docket could not be read',
|
|
|
421
422
|
assert.equal(p.byId.get('cityLine').textContent, City.FALLBACK_LINE);
|
|
422
423
|
});
|
|
423
424
|
|
|
424
|
-
test('the
|
|
425
|
-
const p = await bootPage({ feeds: FEEDS([]), stored: 'light', osDark:
|
|
426
|
-
assert.equal(p.html.attrs['data-theme'], '
|
|
427
|
-
assert.equal(p.
|
|
428
|
-
|
|
429
|
-
p.byId.get('cityMode').fire('click');
|
|
430
|
-
assert.equal(p.store.get('hall-theme'), 'dark');
|
|
431
|
-
assert.equal(p.html.attrs['data-theme'], 'dark');
|
|
432
|
-
assert.equal(p.byId.get('cityMode').attrs['aria-label'], 'Switch to light mode');
|
|
433
|
-
assert.notEqual(JSON.stringify(p.byId.get('cityFlat').children[0].attrs), before, 'the city redraws in the new mode');
|
|
425
|
+
test('the city is dark only: no stored choice and no OS setting reaches it (task 1005192, ADR 0402)', async () => {
|
|
426
|
+
const p = await bootPage({ feeds: FEEDS([]), stored: 'light', osDark: false, shell: { applyTheme() { throw new Error('the city no longer drives the shell'); } } });
|
|
427
|
+
assert.equal(p.html.attrs['data-theme'], 'dark', 'a stored light choice and a light OS are both ignored');
|
|
428
|
+
assert.equal(p.store.get('hall-theme'), 'light', 'and the old preference is never written');
|
|
429
|
+
assert.equal(p.observers.length, 0, 'nothing flips the theme, so nothing watches it');
|
|
434
430
|
const q = await bootPage({ feeds: FEEDS([]), stored: null, osDark: true });
|
|
435
|
-
assert.equal(q.
|
|
431
|
+
assert.equal(JSON.stringify(q.byId.get('cityFlat').children[0].attrs), JSON.stringify(p.byId.get('cityFlat').children[0].attrs), 'one drawing whatever was stored');
|
|
432
|
+
const src = read('city.js');
|
|
433
|
+
assert.doesNotMatch(src, /hall-theme|localStorage|prefers-color-scheme/, 'city.js reads no theme preference');
|
|
436
434
|
});
|
|
437
435
|
|
|
438
|
-
|
|
439
|
-
let shellPaints = 0;
|
|
440
|
-
const p = await bootPage({ feeds: FEEDS([]), stored: 'light', osDark: true, shell: { applyTheme() { shellPaints++; } } });
|
|
441
|
-
assert.equal(p.observers.length, 1, 'the page watches html[data-theme]');
|
|
442
|
-
p.byId.get('cityMode').fire('click');
|
|
443
|
-
assert.equal(shellPaints, 1, 'this switch repaints the shell\'s');
|
|
444
|
-
const before = JSON.stringify(p.byId.get('cityFlat').children[0].attrs);
|
|
445
|
-
p.store.set('hall-theme', 'light'); p.html.setAttribute('data-theme', 'light'); // the shell's switch
|
|
446
|
-
p.observers[0].fn();
|
|
447
|
-
assert.equal(p.byId.get('cityMode').attrs['aria-label'], 'Switch to dark mode', 'this switch follows the shell\'s');
|
|
448
|
-
assert.notEqual(JSON.stringify(p.byId.get('cityFlat').children[0].attrs), before, 'and the city redraws in that mode');
|
|
449
|
-
assert.equal(shellPaints, 1, 'following the shell does not echo back to it');
|
|
450
|
-
});
|
|
436
|
+
|
|
451
437
|
|
|
452
438
|
test('the artboard fits the stage the hall chrome leaves, not the window (task 1004544)', async () => {
|
|
453
439
|
const p = await bootPage({ feeds: FEEDS([]), stage: { clientWidth: 688, clientHeight: 900 } });
|
package/tests/hall_controls.mjs
CHANGED
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
// button (it loads later) without re-drawing it;
|
|
10
10
|
// * THE SIZES — primary is DESIGN.md's 46–48px door, the row size and the
|
|
11
11
|
// text button keep the 24px floor, the field clears it;
|
|
12
|
+
// * THE HELM SHAPE AND VOICE (task 1005192, ADR 0402) — in the hall a button
|
|
13
|
+
// is the cut corner in the name voice, every piece of it a hall-tier token
|
|
14
|
+
// spent with the old pill/lowercase as its fallback (the status page links
|
|
15
|
+
// this sheet without the hall's tier), and a cut button's focus ring is
|
|
16
|
+
// drawn inside the shape, because a clip-path clips an outline;
|
|
12
17
|
// * CONTRAST, MEASURED — the button's ink on its fill (rest AND hover), the
|
|
13
18
|
// ghost's and the text button's ink on both grounds, the field's text and
|
|
14
19
|
// its edge (3:1, WCAG 1.4.11), in both modes, resolved from the real
|
|
@@ -144,11 +149,11 @@ test('style.css imports controls.css right after the shared token sheet', () =>
|
|
|
144
149
|
|
|
145
150
|
// ── 2. the sizes ─────────────────────────────────────────────────────────────
|
|
146
151
|
|
|
147
|
-
test('primary is the 46–48px
|
|
152
|
+
test('primary is the 46–48px door; the row size and the text button keep the 24px floor', () => {
|
|
148
153
|
const base = sharedRule('.btn');
|
|
149
154
|
const h = px(prop(base, 'min-height'));
|
|
150
155
|
assert.ok(h >= 46 && h <= 48, `.btn is ${h}px — DESIGN.md's door is 46–48px`);
|
|
151
|
-
assert.equal(prop(base, 'border-radius'), 'var(--radius-pill)', 'pill
|
|
156
|
+
assert.equal(prop(base, 'border-radius'), 'var(--press-radius, var(--radius-pill))', 'the hall\'s corner, and the pill where the hall\'s tier is not loaded');
|
|
152
157
|
assert.match(prop(base, 'border'), /transparent$/, 'the edge is reserved, so a modifier that sets only its colour draws');
|
|
153
158
|
for (const sel of ['.btn--sm', '.btn--text']) {
|
|
154
159
|
assert.ok(px(prop(sharedWith(sel, 'min-height'), 'min-height')) >= 24, `${sel} keeps 24px of height`);
|
|
@@ -203,6 +208,49 @@ test('no hall sheet puts white ink on an accent fill', () => {
|
|
|
203
208
|
const PRIMARY = ['.btn', '.btn-accent', '.landing__btn', '.task__claim', '.setup-gate__save', '.td-btn--primary', '.pbtn--save', '.craft-save'];
|
|
204
209
|
const GHOST = ['.btn--ghost', '.btn-ghost', '.landing__btn--quiet', '.td-btn', '.wip__release', '.pbtn', '.settings-btn'];
|
|
205
210
|
const SMALL = ['.btn--sm', '.btn-ghost', '.task__claim', '.td-btn', '.wip__release'];
|
|
211
|
+
// the ghost's fill: the hall's press fill, and nothing at all without the hall's tier
|
|
212
|
+
const GHOST_FILL = 'var(--press-fill, transparent)';
|
|
213
|
+
|
|
214
|
+
// ── 3b. the Helm shape and voice (task 1005192, ADR 0402) ───────────────────
|
|
215
|
+
|
|
216
|
+
test('in the hall a button is the cut corner in the name voice, and without the hall\'s tier it is the pill it was', () => {
|
|
217
|
+
const base = sharedRule('.btn');
|
|
218
|
+
// every Helm piece is a token with the old look as its fallback
|
|
219
|
+
assert.equal(prop(base, 'clip-path'), 'var(--cut-path, none)');
|
|
220
|
+
assert.equal(prop(base, 'font-family'), 'var(--name-face, var(--font-body))');
|
|
221
|
+
assert.equal(prop(base, 'font-weight'), 'var(--name-weight, 500)');
|
|
222
|
+
assert.equal(prop(base, 'letter-spacing'), 'var(--name-space-tight, normal)');
|
|
223
|
+
assert.equal(prop(base, 'text-transform'), 'var(--name-case, lowercase)');
|
|
224
|
+
assert.equal(prop(sharedWith('.btn--sm', 'clip-path'), 'clip-path'), 'var(--cut-path-sm, none)', 'the row size takes the smaller cut');
|
|
225
|
+
// the tokens are the hall tier's, and the status page (which links this sheet) defines none of them
|
|
226
|
+
const status = fs.readFileSync(path.join(ROOT, 'modules', 'status-ui', 'public', 'style.css'), 'utf8');
|
|
227
|
+
for (const name of ['--cut-path', '--cut-path-sm', '--press-radius', '--press-fill', '--press-focus', '--name-face', '--name-weight', '--name-space-tight', '--name-case']) {
|
|
228
|
+
assert.ok(root[name], `${name} is in the hall's token block`);
|
|
229
|
+
assert.ok(!new RegExp(`${name}\\s*:`).test(status), `the status page does not define ${name}, so the fallback stands there`);
|
|
230
|
+
}
|
|
231
|
+
assert.equal(root['--cut-path'], 'polygon(var(--cut) 0, 100% 0, 100% calc(100% - var(--cut)), calc(100% - var(--cut)) 100%, 0 100%, 0 var(--cut))', 'top-left and bottom-right are cut');
|
|
232
|
+
assert.equal(root['--name-case'], 'uppercase');
|
|
233
|
+
// a link in a sentence is not a cut control and does not shout
|
|
234
|
+
const text = sharedWith('.btn--text', 'clip-path');
|
|
235
|
+
assert.equal(prop(text, 'clip-path'), 'none');
|
|
236
|
+
assert.equal(prop(text, 'text-transform'), 'none');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test('a cut button keeps a visible focus ring: inset, two-tone, 3:1 on its own fill', () => {
|
|
240
|
+
// clip-path clips the outline and the outer halo, so the base rule hands the shell's
|
|
241
|
+
// :focus-visible rule an inset ring instead; the text button (not cut) hands it back
|
|
242
|
+
assert.equal(prop(sharedRule('.btn'), '--focus-ring'), 'var(--press-focus)');
|
|
243
|
+
assert.equal(prop(sharedWith('.btn--text', '--focus-ring'), '--focus-ring'), 'initial');
|
|
244
|
+
const shell = rulesOf(styleSrc).find((x) => x.sels.length === 1 && x.sels[0] === ':focus-visible');
|
|
245
|
+
assert.ok(shell, 'the shell\'s ring rule');
|
|
246
|
+
assert.match(prop(shell.body, 'box-shadow'), /^var\(--focus-ring, /, 'the shell\'s ring spends what a cut control hands in');
|
|
247
|
+
assert.equal(root['--press-focus'], 'inset 0 0 0 2px var(--focus), inset 0 0 0 4px var(--bg)', 'the focus colour on the edge, then a 2px band of the ground');
|
|
248
|
+
// one of the two tones reads against each fill a cut button paints (WCAG 1.4.11, 3:1), in the mode the hall renders
|
|
249
|
+
for (const [what, fill] of [['primary', 'var(--accent)'], ['primary hover', 'var(--accent-lift)'], ['ghost on the page', onGround(GHOST_FILL, 'var(--bg)', 'dark')], ['ghost on a panel', onGround(GHOST_FILL, 'var(--bg-card)', 'dark')], ['danger', 'var(--danger)']]) {
|
|
250
|
+
const best = Math.max(contrast('var(--focus)', fill, 'dark'), contrast('var(--bg)', fill, 'dark'));
|
|
251
|
+
assert.ok(best >= 3, `dark ${what}: the ring's better tone is ${best.toFixed(2)}:1 on the fill`);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
206
254
|
|
|
207
255
|
test('every older button name sits in the shared rule it means', () => {
|
|
208
256
|
const fill = (sel) => {
|
|
@@ -210,11 +258,11 @@ test('every older button name sits in the shared rule it means', () => {
|
|
|
210
258
|
return r.length ? prop(r[0].body, 'background') : null;
|
|
211
259
|
};
|
|
212
260
|
for (const sel of PRIMARY) assert.equal(fill(sel), 'var(--accent)', `${sel} is a primary door`);
|
|
213
|
-
for (const sel of GHOST) assert.equal(fill(sel),
|
|
261
|
+
for (const sel of GHOST) assert.equal(fill(sel), GHOST_FILL, `${sel} is a ghost`);
|
|
214
262
|
for (const sel of SMALL) assert.equal(prop(sharedWith(sel, 'min-height'), 'min-height'), '30px', `${sel} is the row size`);
|
|
215
263
|
// and the primary-on-a-ghost-base names come AFTER the ghost rule, or the ghost wins on order
|
|
216
264
|
const list = rulesOf(controls);
|
|
217
|
-
const ghostAt = list.findIndex((x) => x.sels.includes('.td-btn') && prop(x.body, 'background') ===
|
|
265
|
+
const ghostAt = list.findIndex((x) => x.sels.includes('.td-btn') && prop(x.body, 'background') === GHOST_FILL);
|
|
218
266
|
const primaryAt = list.findIndex((x) => x.sels.includes('.td-btn--primary') && prop(x.body, 'background'));
|
|
219
267
|
assert.ok(primaryAt > ghostAt, '.td-btn--primary follows .td-btn, so the shared order paints it');
|
|
220
268
|
});
|
|
@@ -93,7 +93,8 @@ function boot({ page = 'thinking', optIn = true, store = {}, storageThrows = fal
|
|
|
93
93
|
addEventListener() {}, dispatchEvent() {},
|
|
94
94
|
get cookie() { return ''; }, set cookie(_v) {},
|
|
95
95
|
};
|
|
96
|
-
const
|
|
96
|
+
const assigned = []; // task 1005144: where the page sent the browser (game mode's Escape opens the menu)
|
|
97
|
+
const location = { hostname: 'h', origin: 'https://h', href: 'https://h/' + page + search, pathname: path || '/' + page, hash, search, assign(u) { assigned.push(u); }, replace() {} };
|
|
97
98
|
// `session` is the TAB's sessionStorage: pass one object to several boots and
|
|
98
99
|
// they are page loads in one tab (the trail, task 1004476).
|
|
99
100
|
const sessionStorage = {
|
|
@@ -133,6 +134,7 @@ function boot({ page = 'thinking', optIn = true, store = {}, storageThrows = fal
|
|
|
133
134
|
const root = documentObj.documentElement;
|
|
134
135
|
return {
|
|
135
136
|
store,
|
|
137
|
+
assigned,
|
|
136
138
|
game: () => windowObj.OTBShellGame,
|
|
137
139
|
gameStamp: () => root.dataset.game === 'on',
|
|
138
140
|
// The "how to leave" line: showing = the shell created it and it is not hidden.
|
|
@@ -518,8 +520,8 @@ test('game mode: every hall page carries the switch — none opts out, and each
|
|
|
518
520
|
test('game mode: leaving brings the bars back for THIS page only, and never rewrites a page\'s own memory', () => {
|
|
519
521
|
const store = { 'hall-game': 'on', 'hall-fullscreen:thinking': 'on' };
|
|
520
522
|
const goals = boot({ page: 'goals', optIn: false, store });
|
|
521
|
-
goals.
|
|
522
|
-
assert.equal(goals.on(), false, '
|
|
523
|
+
goals.button().click();
|
|
524
|
+
assert.equal(goals.on(), false, 'the corner button still leaves (Escape opens the options menu, below)');
|
|
523
525
|
assert.equal(store['hall-fullscreen:goals'], undefined, 'nothing remembered for the page');
|
|
524
526
|
assert.equal(boot({ page: 'goals', optIn: false, store }).on(), true, 'the next load opens without bars again');
|
|
525
527
|
const sky = boot({ page: 'thinking', store });
|
|
@@ -528,6 +530,28 @@ test('game mode: leaving brings the bars back for THIS page only, and never rewr
|
|
|
528
530
|
assert.equal(store['hall-fullscreen:thinking'], 'on', 'the sky\'s own choice survives game mode');
|
|
529
531
|
});
|
|
530
532
|
|
|
533
|
+
// task 1005144 (BONGOS-V3 R04, ADR 0399 §2.4 row 6): in game mode the last Escape is the
|
|
534
|
+
// options menu's. It opens the menu, remembering where it came from, and the bars stay away.
|
|
535
|
+
test('game mode: the last Escape opens the options menu, and the bars stay hidden', () => {
|
|
536
|
+
const store = { 'hall-game': 'on' };
|
|
537
|
+
const goals = boot({ page: 'goals', optIn: false, store, hash: '#/goal/7' });
|
|
538
|
+
goals.press('Escape');
|
|
539
|
+
assert.equal(goals.on(), true, 'the shell did not take the press');
|
|
540
|
+
assert.deepEqual(goals.assigned, ['/settings?from=' + encodeURIComponent('/goals#/goal/7')], 'the menu, and the way back');
|
|
541
|
+
const apex = boot({ page: 'goals', optIn: false, store, path: '/builders/goals' });
|
|
542
|
+
apex.press('Escape');
|
|
543
|
+
assert.deepEqual(apex.assigned, ['/builders/settings?from=' + encodeURIComponent('/builders/goals')], 'on the apex too');
|
|
544
|
+
const used = boot({ page: 'goals', optIn: false, store });
|
|
545
|
+
used.press('Escape', { pageUses: true });
|
|
546
|
+
used.press('Escape', { target: { tagName: 'INPUT' } });
|
|
547
|
+
assert.deepEqual(used.assigned, [], 'a press a page used, or one in a field, never opens it');
|
|
548
|
+
const plain = boot({ page: 'goals', optIn: false, store: {} });
|
|
549
|
+
plain.button().click();
|
|
550
|
+
plain.press('Escape');
|
|
551
|
+
assert.deepEqual(plain.assigned, [], 'outside game mode Escape is the shell\'s, as before');
|
|
552
|
+
assert.equal(plain.on(), false, 'and it leaves full screen');
|
|
553
|
+
});
|
|
554
|
+
|
|
531
555
|
test('game mode: the Escape hand-off is unchanged — a press the page used, or one in a field, does not leave', () => {
|
|
532
556
|
const p = boot({ page: 'goals', optIn: false, store: { 'hall-game': 'on' } });
|
|
533
557
|
p.press('Escape', { pageUses: true });
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// tests/hall_helm.mjs — Helm, the look the builders' hall wears (task 1005192, ADR 0402).
|
|
2
|
+
//
|
|
3
|
+
// WHY. Two owner decisions are easy to break without anyone seeing it on their own
|
|
4
|
+
// machine: Helm is the hall's alone (the landing, the hub and the status page keep
|
|
5
|
+
// their look), and the hall is dark only (no page offers light, and a light OS never
|
|
6
|
+
// flashes it). Both are decided in one serve-time seam, src/hall-helm.js, which
|
|
7
|
+
// recognises a hall page by the sheet it links. This file runs that seam over the
|
|
8
|
+
// real pages, and holds the pieces of the kit a browser would hide:
|
|
9
|
+
// * every hall page is served with html[data-theme="dark"], and nothing else is;
|
|
10
|
+
// * the Helm faces reach a hall whose pack names the default faces, and never a
|
|
11
|
+
// hall that chose its own, and never the landing or the status page;
|
|
12
|
+
// * the font stylesheet loads the way tests/google_fonts_nonblocking.mjs asks of
|
|
13
|
+
// a typed-in link (both preconnects on the page, display=swap), which that sweep
|
|
14
|
+
// cannot see because the link is built at serve time;
|
|
15
|
+
// * no hall script writes a theme choice, and the shell offers no switch;
|
|
16
|
+
// * the Helm neutrals: the two dark twins carry the night, the ice and no black;
|
|
17
|
+
// * the kit's recipes exist once, as hall-tier tokens and shared rules.
|
|
18
|
+
//
|
|
19
|
+
// Run: node --test tests/hall_helm.mjs
|
|
20
|
+
import { test } from 'node:test';
|
|
21
|
+
import assert from 'node:assert/strict';
|
|
22
|
+
import fs from 'node:fs';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import { fileURLToPath } from 'node:url';
|
|
25
|
+
import { createRequire } from 'node:module';
|
|
26
|
+
|
|
27
|
+
const require = createRequire(import.meta.url);
|
|
28
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
29
|
+
const HALL = path.join(ROOT, 'modules', 'hall-ui', 'public');
|
|
30
|
+
const read = (...p) => fs.readFileSync(path.join(ROOT, ...p), 'utf8');
|
|
31
|
+
const decomment = (css) => css.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
32
|
+
|
|
33
|
+
const { applyHelm, isHallPage, wearsHelmFaces, stampHtml, HELM_FONTS_HREF } = require('../src/hall-helm.js');
|
|
34
|
+
const { injectBrandingGlobal } = require('../src/bongos/serve-internal.js');
|
|
35
|
+
const { clientBranding, resolveBranding } = require('../src/branding.js');
|
|
36
|
+
|
|
37
|
+
const NEUTRAL = JSON.parse(read('config', 'branding.neutral.json'));
|
|
38
|
+
const DEFAULTS = NEUTRAL.theme.ui.fonts;
|
|
39
|
+
const hallPages = fs.readdirSync(HALL).filter((f) => f.endsWith('.html'));
|
|
40
|
+
const htmlTag = (html) => (html.match(/<html\b[^>]*>/i) || [''])[0];
|
|
41
|
+
|
|
42
|
+
// ── the seam ────────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
test('a hall page is the one that links the hall sheet; the landing, the hub and the status page are not', () => {
|
|
45
|
+
for (const f of hallPages) assert.ok(isHallPage(read('modules', 'hall-ui', 'public', f)), `${f} links the hall's sheet`);
|
|
46
|
+
for (const p of [['modules', 'public-landing', 'public', 'index.html'], ['modules', 'public-landing', 'public', 'projects.html'], ['modules', 'status-ui', 'public', 'index.html'], ['modules', 'ui-design', 'public', 'brand.html']]) {
|
|
47
|
+
assert.equal(isHallPage(read(...p)), false, `${p.join('/')} is not a hall page`);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('every hall page is served pinned dark, in the Helm faces, and no other surface is touched', () => {
|
|
52
|
+
const brand = clientBranding(resolveBranding({ neutral: NEUTRAL }));
|
|
53
|
+
for (const f of hallPages) {
|
|
54
|
+
const out = injectBrandingGlobal(read('modules', 'hall-ui', 'public', f), brand, {});
|
|
55
|
+
const tag = htmlTag(out);
|
|
56
|
+
assert.match(tag, / data-theme="dark"/, `${f}: dark before first paint`);
|
|
57
|
+
assert.match(tag, / data-faces="helm"/, `${f}: the Helm faces`);
|
|
58
|
+
assert.equal((tag.match(/data-theme=/g) || []).length, 1, `${f}: stamped once`);
|
|
59
|
+
assert.ok(out.includes(`href="${HELM_FONTS_HREF.replace(/&/g, '&')}"`), `${f}: the two families are linked`);
|
|
60
|
+
// the link follows the pack's own splice, so it is the last word in <head>
|
|
61
|
+
assert.ok(out.indexOf('Barlow') > out.indexOf('--font-display:'), `${f}: after the pack's :root`);
|
|
62
|
+
}
|
|
63
|
+
for (const p of [['modules', 'public-landing', 'public', 'index.html'], ['modules', 'status-ui', 'public', 'index.html']]) {
|
|
64
|
+
const src = read(...p);
|
|
65
|
+
const out = injectBrandingGlobal(src, brand, {});
|
|
66
|
+
assert.equal(htmlTag(out), htmlTag(src), `${p.join('/')}: its <html> tag is as authored`);
|
|
67
|
+
assert.doesNotMatch(out, /Barlow|data-faces/, `${p.join('/')}: no Helm face`);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('a hall that wears faces of its own keeps them; it is still dark only', () => {
|
|
72
|
+
const page = '<!doctype html><html lang="en"><head><link rel="stylesheet" href="/builders/style.css?v=1"></head><body></body></html>';
|
|
73
|
+
const own = { fonts: { display: '"Archivo", sans-serif', body: DEFAULTS.body, links: [] } };
|
|
74
|
+
assert.equal(wearsHelmFaces(own, DEFAULTS), false, 'one face of its own is a pack that chose');
|
|
75
|
+
const kept = applyHelm(page, own, DEFAULTS);
|
|
76
|
+
assert.match(htmlTag(kept.html), /data-theme="dark"/);
|
|
77
|
+
assert.doesNotMatch(kept.html, /data-faces/);
|
|
78
|
+
assert.equal(kept.tail, '', 'and no Helm stylesheet is linked');
|
|
79
|
+
// the default faces, and a pack that names none, take Helm
|
|
80
|
+
assert.equal(wearsHelmFaces({ fonts: { display: DEFAULTS.display, body: DEFAULTS.body } }, DEFAULTS), true);
|
|
81
|
+
assert.equal(wearsHelmFaces({}, DEFAULTS), true);
|
|
82
|
+
// a look from the style library with its own faces (ADR 0358) keeps them through the real resolver
|
|
83
|
+
const looked = clientBranding(resolveBranding({ neutral: NEUTRAL, env: { BONGOS_LOOK: 'expedition' } }));
|
|
84
|
+
if (looked.theme.look === 'expedition') {
|
|
85
|
+
const out = injectBrandingGlobal(page, looked, {});
|
|
86
|
+
assert.match(htmlTag(out), /data-theme="dark"/);
|
|
87
|
+
assert.doesNotMatch(out, /data-faces|Barlow/, 'the expedition look keeps Archivo');
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('the stamp replaces an attribute already there and leaves the others alone', () => {
|
|
92
|
+
assert.equal(stampHtml('<html lang="en" data-artist="on">', 'data-theme', 'dark'), '<html lang="en" data-artist="on" data-theme="dark">');
|
|
93
|
+
assert.equal(stampHtml('<html data-theme="light" lang="en">', 'data-theme', 'dark'), '<html lang="en" data-theme="dark">');
|
|
94
|
+
assert.equal(applyHelm('<html><head></head></html>', {}, DEFAULTS).html, '<html><head></head></html>', 'a page with no hall sheet is untouched');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('the Helm stylesheet loads without blocking: swap, and both preconnects on every hall page', () => {
|
|
98
|
+
assert.match(HELM_FONTS_HREF, /^https:\/\/fonts\.googleapis\.com\/css2\?/);
|
|
99
|
+
assert.match(HELM_FONTS_HREF, /[?&]display=swap$/);
|
|
100
|
+
assert.match(HELM_FONTS_HREF, /family=Barlow\+Condensed:wght@400;500;600/);
|
|
101
|
+
assert.match(HELM_FONTS_HREF, /family=Barlow:wght@400;500/);
|
|
102
|
+
const missing = [];
|
|
103
|
+
for (const f of hallPages) {
|
|
104
|
+
const html = read('modules', 'hall-ui', 'public', f);
|
|
105
|
+
if (!/<link rel="preconnect" href="https:\/\/fonts\.googleapis\.com">/.test(html)) missing.push(`${f}: fonts.googleapis.com`);
|
|
106
|
+
if (!/<link rel="preconnect" href="https:\/\/fonts\.gstatic\.com" crossorigin>/.test(html)) missing.push(`${f}: fonts.gstatic.com (crossorigin)`);
|
|
107
|
+
}
|
|
108
|
+
assert.deepEqual(missing, [], 'a hall page that links a web font warms both font origins first');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// ── dark only ───────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
test('no hall script stores or offers a theme, and the shell pins dark', () => {
|
|
114
|
+
const shell = read('modules', 'hall-ui', 'public', 'shell.js');
|
|
115
|
+
assert.match(shell, /function applyTheme\(\) \{\s*document\.documentElement\.dataset\.theme = 'dark';\s*\}/, 'the shell sets dark, whatever was stored');
|
|
116
|
+
assert.doesNotMatch(shell, /theme-toggle|toggleTheme/, 'no switch on the top bar');
|
|
117
|
+
const writers = [];
|
|
118
|
+
for (const f of fs.readdirSync(HALL).filter((x) => x.endsWith('.js'))) {
|
|
119
|
+
const src = read('modules', 'hall-ui', 'public', f).replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:'"])\/\/.*$/gm, '$1');
|
|
120
|
+
if (/['"]hall-theme['"]/.test(src)) writers.push(`${f}: names the retired 'hall-theme' preference`);
|
|
121
|
+
if (/dataset\.theme\s*=\s*(?![\s=]|'dark')|setAttribute\(\s*['"]data-theme['"]\s*,\s*(?!\s|'dark')/.test(src)) writers.push(`${f}: writes a theme other than dark`);
|
|
122
|
+
}
|
|
123
|
+
assert.deepEqual(writers, []);
|
|
124
|
+
for (const f of hallPages) assert.doesNotMatch(read('modules', 'hall-ui', 'public', f), /id="(theme-toggle|cityMode|streetMode)"/, `${f}: no light/dark switch`);
|
|
125
|
+
assert.doesNotMatch(decomment(read('modules', 'hall-ui', 'public', 'style.css')), /\.theme-toggle/, 'and no styles for one');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// ── the Helm tier ───────────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
const STYLE = read('modules', 'hall-ui', 'public', 'style.css');
|
|
131
|
+
const blockAt = (marker) => {
|
|
132
|
+
const at = STYLE.indexOf(marker);
|
|
133
|
+
assert.notEqual(at, -1, `style.css holds ${marker}`);
|
|
134
|
+
const open = STYLE.indexOf('{', at);
|
|
135
|
+
let depth = 0;
|
|
136
|
+
for (let i = open; i < STYLE.length; i++) {
|
|
137
|
+
if (STYLE[i] === '{') depth++;
|
|
138
|
+
else if (STYLE[i] === '}' && --depth === 0) return STYLE.slice(open + 1, i);
|
|
139
|
+
}
|
|
140
|
+
throw new Error('unbalanced');
|
|
141
|
+
};
|
|
142
|
+
const decls = (body) => Object.fromEntries([...decomment(body).matchAll(/(--[\w-]+)\s*:\s*([^;]+);/g)].map((m) => [m[1], m[2].replace(/\s+/g, ' ').trim()]));
|
|
143
|
+
const root = decls(blockAt('\n:root {'));
|
|
144
|
+
const dark = decls(blockAt('html[data-theme="dark"] {'));
|
|
145
|
+
|
|
146
|
+
test('the dark tier is the Helm neutrals: night grounds, white and ice inks, ice hairlines', () => {
|
|
147
|
+
assert.deepEqual(
|
|
148
|
+
Object.fromEntries(['--bg', '--bg-card', '--bg-deep', '--bg-frame', '--ink', '--ink-soft', '--rule', '--rule-soft', '--void'].map((k) => [k, dark[k]])),
|
|
149
|
+
{ '--bg': '#050d16', '--bg-card': '#0a1622', '--bg-deep': '#050d16', '--bg-frame': '#07111b', '--ink': '#ffffff', '--ink-soft': '#c8f1f7', '--rule': 'rgba(200, 241, 247, .4)', '--rule-soft': 'rgba(200, 241, 247, .2)', '--void': '#050d16' },
|
|
150
|
+
);
|
|
151
|
+
// the accent is still the pack's: the moon is never pinned (the Unpinned Accent Rule)
|
|
152
|
+
for (const name of ['--accent', '--accent-deep', '--sea', '--terracotta', '--font-display', '--font-body']) assert.equal(dark[name], undefined, `${name} is not set in the dark tier`);
|
|
153
|
+
// in dark a label is the name voice
|
|
154
|
+
assert.equal(dark['--label-face'], 'var(--font-display)');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('the Helm faces are hall-tier tokens, handed to the two font tokens only under html[data-faces="helm"]', () => {
|
|
158
|
+
assert.match(root['--helm-display'], /^"Barlow Condensed", /);
|
|
159
|
+
assert.match(root['--helm-body'], /^"Barlow", /);
|
|
160
|
+
// the contract tier still equals the pack (tests/hall_tokens.mjs holds that); Helm does not overwrite it there
|
|
161
|
+
assert.equal(root['--font-display'], DEFAULTS.display);
|
|
162
|
+
const faces = decls(blockAt('\nhtml[data-faces="helm"] {'));
|
|
163
|
+
assert.deepEqual(faces, { '--font-display': 'var(--helm-display)', '--font-body': 'var(--helm-body)' });
|
|
164
|
+
assert.equal((decomment(STYLE).match(/Barlow/g) || []).length, 2, 'the family names live once each, in the token block');
|
|
165
|
+
for (const f of fs.readdirSync(HALL).filter((x) => x.endsWith('.css') && x !== 'style.css')) {
|
|
166
|
+
assert.doesNotMatch(read('modules', 'hall-ui', 'public', f), /Barlow/, `${f} takes the face from the tokens`);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('the kit\'s recipes are named once: the name voice, the cut, the segment, the panel over art', () => {
|
|
171
|
+
assert.equal(root['--name-face'], 'var(--font-display)');
|
|
172
|
+
assert.equal(root['--name-case'], 'uppercase');
|
|
173
|
+
for (const k of ['--name-space', '--name-space-wide', '--name-space-tight']) {
|
|
174
|
+
const em = Number((root[k].match(/^(\.\d+)em$/) || [])[1]);
|
|
175
|
+
assert.ok(em >= 0.16 && em <= 0.3, `${k} is wide tracking (${root[k]})`);
|
|
176
|
+
}
|
|
177
|
+
assert.equal(root['--cut'], '12px');
|
|
178
|
+
assert.equal(root['--seg-skew'], '-28deg');
|
|
179
|
+
assert.equal(root['--night-glass'], 'color-mix(in srgb, var(--bg) 72%, transparent)');
|
|
180
|
+
const controls = decomment(read('modules', 'hall-ui', 'public', 'controls.css'));
|
|
181
|
+
const seg = controls.match(/\n\.seg \{([^}]*)\}/);
|
|
182
|
+
assert.ok(seg, 'the segment is a shared rule');
|
|
183
|
+
assert.match(seg[1], /transform: skewX\(var\(--seg-skew\)\);/);
|
|
184
|
+
assert.match(seg[1], /height: var\(--seg-h\);/);
|
|
185
|
+
assert.match(controls, /\n\.seg--done \{ background: var\(--ink-soft\);/, 'a finished step is information, so it is ice, not the accent');
|
|
186
|
+
assert.match(controls, /\n\.seg--now \{ background: var\(--accent\);/, 'the accent is the step you can act on');
|
|
187
|
+
});
|
|
@@ -379,7 +379,7 @@ test('the migrated pages run for real against the kit and emit its classes', ()
|
|
|
379
379
|
assert.equal(typeof s.window.OTBHallRender, 'function', 'the renderer factory survived the migration');
|
|
380
380
|
assert.equal(typeof s.window.OTBKit.barSeriesHtml, 'function');
|
|
381
381
|
assert.match(
|
|
382
|
-
s.window.OTBKit.barSeriesHtml([{ label: '
|
|
382
|
+
s.window.OTBKit.barSeriesHtml([{ label: 'artisan', value: 3 }]),
|
|
383
383
|
/class="kit-bars"/,
|
|
384
384
|
);
|
|
385
385
|
});
|
|
@@ -72,7 +72,7 @@ async function boot({ meStatus, settle = false }) {
|
|
|
72
72
|
// the same reason categoryChip is: an undefined helper throws mid-render.
|
|
73
73
|
roleAxisLabel: () => 'Role',
|
|
74
74
|
roleLabel: (d) => (!d ? '' : d === 'unclassified' ? 'No role yet' : d === 'ui' ? 'UI' : d.charAt(0).toUpperCase() + d.slice(1)),
|
|
75
|
-
roleOptions: () => [{ value: '', label: '— any —' }, { value: '
|
|
75
|
+
roleOptions: () => [{ value: '', label: '— any —' }, { value: 'artisan', label: 'Artisan' }, { value: 'artist', label: 'Artist' }],
|
|
76
76
|
roleIsUnset: (d) => !d || d === 'unclassified',
|
|
77
77
|
ROLE_UNSET: 'unclassified',
|
|
78
78
|
moduleOn: () => true,
|
package/tests/hall_my_agents.mjs
CHANGED
|
@@ -388,48 +388,48 @@ function bootCraft({ session = {} } = {}) {
|
|
|
388
388
|
}
|
|
389
389
|
const seatOf = (has, title) => ({ known: true, has_position: has, title: has ? title : null });
|
|
390
390
|
|
|
391
|
-
test('CRAFT: with no seat, Governing is not offered anywhere', () => {
|
|
391
|
+
test('CRAFT: with no seat, Patron (Governing until task 1005141) is not offered anywhere', () => {
|
|
392
392
|
const b = bootCraft();
|
|
393
393
|
b.craft.setPositions(seatOf(false));
|
|
394
|
-
assert.match(b.craft.switchHtml(), /data-craft-opt="
|
|
395
|
-
b.craft.choose('
|
|
394
|
+
assert.match(b.craft.switchHtml(), /data-craft-opt="patron"[^>]* hidden>/, 'the switch hides it');
|
|
395
|
+
b.craft.choose('patron');
|
|
396
396
|
assert.equal(b.root.dataset.craft, undefined, 'and choosing it does nothing');
|
|
397
397
|
b.craft.open();
|
|
398
398
|
const picker = b.body.children[0];
|
|
399
399
|
assert.ok(picker, 'the picker opened');
|
|
400
|
-
assert.doesNotMatch(picker.innerHTML, /data-craft-pick="
|
|
401
|
-
assert.match(picker.innerHTML, /data-doors="
|
|
400
|
+
assert.doesNotMatch(picker.innerHTML, /data-craft-pick="patron"/, 'no Patron door');
|
|
401
|
+
assert.match(picker.innerHTML, /data-doors="2"/, 'two doors, laid out as two');
|
|
402
402
|
});
|
|
403
403
|
|
|
404
|
-
test('CRAFT: a stored Governing reads as no mode for a builder known to hold no seat', () => {
|
|
404
|
+
test('CRAFT: a stored Patron — even under its pre-rename id, Governing — reads as no mode for a builder known to hold no seat', () => {
|
|
405
405
|
const b = bootCraft({ session: { 'hall-craft': 'governing' } });
|
|
406
|
-
assert.equal(b.craft.current(), '
|
|
406
|
+
assert.equal(b.craft.current(), 'patron', 'unknown seats: today\'s behaviour, the old id read as Patron (task 1005141)');
|
|
407
407
|
b.craft.setPositions(seatOf(false));
|
|
408
408
|
assert.equal(b.craft.current(), null);
|
|
409
409
|
assert.equal(b.session['hall-craft'], undefined, 'and the stored choice is dropped');
|
|
410
410
|
});
|
|
411
411
|
|
|
412
|
-
test('CRAFT: a seat names the
|
|
412
|
+
test('CRAFT: a seat names the Patron mode', () => {
|
|
413
413
|
const b = bootCraft();
|
|
414
414
|
b.craft.setPositions(seatOf(true, 'Treasury Lead'));
|
|
415
415
|
assert.equal(b.craft.seatName(), 'Treasury Lead');
|
|
416
|
-
assert.match(b.craft.switchHtml(), /data-craft-opt="
|
|
417
|
-
b.craft.choose('
|
|
418
|
-
assert.equal(b.root.dataset.craft, '
|
|
416
|
+
assert.match(b.craft.switchHtml(), /data-craft-opt="patron"[^>]*>[\s\S]*?Treasury Lead<\/span>/);
|
|
417
|
+
b.craft.choose('patron');
|
|
418
|
+
assert.equal(b.root.dataset.craft, 'patron');
|
|
419
419
|
b.craft.open();
|
|
420
420
|
assert.match(b.body.children[0].innerHTML, /craft-door__name">Treasury Lead</);
|
|
421
|
-
assert.match(b.body.children[0].innerHTML, /data-doors="
|
|
421
|
+
assert.match(b.body.children[0].innerHTML, /data-doors="3"/);
|
|
422
422
|
});
|
|
423
423
|
|
|
424
|
-
test('CRAFT: unknown seats are today\'s picker —
|
|
424
|
+
test('CRAFT: unknown seats are today\'s picker — three doors and a free Patron', () => {
|
|
425
425
|
const b = bootCraft();
|
|
426
426
|
b.craft.setPositions(null);
|
|
427
427
|
assert.equal(b.craft.seatName(), null);
|
|
428
428
|
assert.doesNotMatch(b.craft.switchHtml(), / hidden>/);
|
|
429
|
-
assert.match(b.craft.switchHtml(), />
|
|
429
|
+
assert.match(b.craft.switchHtml(), />Patron<\/span>/);
|
|
430
430
|
b.craft.setPositions({ known: false, has_position: false });
|
|
431
|
-
b.craft.choose('
|
|
432
|
-
assert.equal(b.root.dataset.craft, '
|
|
431
|
+
b.craft.choose('patron');
|
|
432
|
+
assert.equal(b.root.dataset.craft, 'patron', 'an unknown answer never takes the mode away');
|
|
433
433
|
});
|
|
434
434
|
|
|
435
435
|
// ---------------------------------------------------------------------------
|