hecks 3.10.0 → 3.11.0
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.
- checksums.yaml +4 -4
- data/examples/pizzas/bluebook/pizzas.bluebook +29 -0
- data/examples/pizzas/bluebook/pizzas.hecksagon +7 -0
- data/exe/hecks +4 -4
- data/lib/hecks/adapters/driven/d1.rb +2 -10
- data/lib/hecks/adapters/driven/heki.adapter +1 -1
- data/lib/hecks/adapters/driven/heki.rb +2 -12
- data/lib/hecks/adapters/driven/lambda.adapter +1 -1
- data/lib/hecks/adapters/driven/lambda.rb +3 -18
- data/lib/hecks/adapters/driven/postgres.rb +4 -11
- data/lib/hecks/adapters/driven/sqlite.adapter +1 -1
- data/lib/hecks/adapters/driven/sqlite.rb +2 -10
- data/lib/hecks/adapters/driving/admission.rb +46 -0
- data/lib/hecks/adapters/driving/cli/nesting.rb +36 -0
- data/lib/hecks/adapters/driving/cli/shorthand.rb +88 -0
- data/lib/hecks/adapters/driving/cli.rb +127 -0
- data/lib/hecks/adapters/driving/cli_runner/answers.rb +160 -0
- data/lib/hecks/adapters/driving/cli_runner/resolution.rb +155 -0
- data/lib/hecks/adapters/driving/cli_runner/settling.rb +120 -0
- data/lib/hecks/adapters/driving/cli_runner/streaming.rb +93 -0
- data/lib/hecks/adapters/driving/cli_runner/suggestions.rb +47 -0
- data/lib/hecks/adapters/driving/cli_runner.rb +69 -0
- data/lib/hecks/adapters/driving/command_request.rb +121 -0
- data/lib/hecks/adapters/driving/handle.rb +214 -0
- data/lib/hecks/adapters/driving/json.rb +136 -0
- data/lib/hecks/adapters/driving/launcher_options.rb +267 -0
- data/lib/hecks/adapters/driving/mcp/properties.rb +49 -0
- data/lib/hecks/adapters/driving/mcp/tool_calls.rb +116 -0
- data/lib/hecks/adapters/driving/mcp/tool_definitions/auditing.rb +53 -0
- data/lib/hecks/adapters/driving/mcp/tool_definitions/calling.rb +71 -0
- data/lib/hecks/adapters/driving/mcp/tool_definitions/reading.rb +99 -0
- data/lib/hecks/adapters/driving/mcp/tool_definitions.rb +18 -0
- data/lib/hecks/adapters/driving/mcp.rb +159 -0
- data/lib/hecks/adapters/driving/mcp_guide_cache.rb +78 -0
- data/lib/hecks/adapters/driving/mcp_scope/argument_policy.rb +97 -0
- data/lib/hecks/adapters/driving/mcp_scope/env_problems.rb +69 -0
- data/lib/hecks/adapters/driving/mcp_scope/presenter.rb +83 -0
- data/lib/hecks/adapters/driving/mcp_scope.rb +213 -0
- data/lib/hecks/adapters/driving/ruby/aggregate_module.rb +171 -0
- data/lib/hecks/adapters/driving/ruby/chapter.rb +91 -0
- data/lib/hecks/adapters/driving/ruby.rb +83 -0
- data/lib/hecks/adapters/driving/usage_cache.rb +144 -0
- data/lib/hecks/{doors.rb → adapters/driving.rb} +10 -6
- data/lib/hecks/behaviors/expectations/boot.rb +1 -1
- data/lib/hecks/bench/ruby_runner.rb +1 -1
- data/lib/hecks/bluebook/ask_resolution.rb +130 -0
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +8 -2
- data/lib/hecks/bluebook/assembly/contracts/rules.rb +4 -2
- data/lib/hecks/bluebook/assembly/contracts/structure.rb +3 -1
- data/lib/hecks/bluebook/assembly/marks.rb +1 -0
- data/lib/hecks/bluebook/attribute.rb +1 -1
- data/lib/hecks/bluebook/behaviour/command.rb +13 -8
- data/lib/hecks/bluebook/behaviour/policy.rb +30 -0
- data/lib/hecks/bluebook/capabilities/attribute_values.rb +72 -0
- data/lib/hecks/bluebook/capabilities.rb +91 -9
- data/lib/hecks/bluebook/domain_port.rb +16 -4
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +18 -6
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +13 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation/computed_sources.rb +98 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation/fan_out_rows.rb +42 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation/projected_fields.rb +14 -2
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation/provisions.rb +69 -3
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation/with_projections.rb +17 -10
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +5 -0
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -0
- data/lib/hecks/bluebook/dsl/command_builder/mutations.rb +8 -1
- data/lib/hecks/bluebook/dsl/command_builder/operands.rb +121 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +2 -0
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +10 -1
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +45 -2
- data/lib/hecks/bluebook/dsl/policy_builder.rb +47 -10
- data/lib/hecks/bluebook/dsl/rule_reference.rb +7 -4
- data/lib/hecks/bluebook/dsl/world_builder.rb +1 -1
- data/lib/hecks/bluebook/expression/ast_json/emitters.rb +6 -0
- data/lib/hecks/bluebook/expression/ast_json.rb +9 -6
- data/lib/hecks/bluebook/expression/ast_reader.rb +14 -9
- data/lib/hecks/bluebook/expression/projection.json +36 -0
- data/lib/hecks/bluebook/expression/resolver/operations.rb +31 -0
- data/lib/hecks/bluebook/expression/resolver/parsing.rb +38 -5
- data/lib/hecks/bluebook/expression/resolver/scanning.rb +22 -5
- data/lib/hecks/bluebook/expression/resolver/values.rb +10 -0
- data/lib/hecks/bluebook/expression/resolver.rb +20 -1
- data/lib/hecks/bluebook/hecksagon.rb +23 -3
- data/lib/hecks/bluebook/lifecycle.rb +22 -3
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +13 -1
- data/lib/hecks/bluebook/meta_validator/modes.rb +4 -1
- data/lib/hecks/bluebook/meta_validator/readings.rb +18 -1
- data/lib/hecks/bluebook/meta_validator/reconstruction/heads.rb +2 -1
- data/lib/hecks/bluebook/meta_validator/shapes.rb +5 -0
- data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +1 -1
- data/lib/hecks/bluebook/meta_validator.rb +12 -12
- data/lib/hecks/bluebook/model_check/ask_checks.rb +71 -0
- data/lib/hecks/bluebook/model_check/policy_checks.rb +2 -1
- data/lib/hecks/bluebook/model_check/reference_checks.rb +70 -0
- data/lib/hecks/bluebook/model_check.rb +8 -2
- data/lib/hecks/bluebook/policy.rb +4 -2
- data/lib/hecks/bluebook/smoke_test.rb +1 -1
- data/lib/hecks/bluebook/synthesizer.rb +2 -0
- data/lib/hecks/bluebook.rb +1 -0
- data/lib/hecks/cli/command_table.rb +1 -1
- data/lib/hecks/cli/console.rb +2 -1
- data/lib/hecks/cli/document.rb +1 -1
- data/lib/hecks/cli/launcher_words.rb +4 -4
- data/lib/hecks/cli/mcp.rb +4 -4
- data/lib/hecks/cli/project_cli/launcher_source.rb +2 -2
- data/lib/hecks/cli/project_cli/templates.rb +5 -5
- data/lib/hecks/cli/project_cli.rb +9 -6
- data/lib/hecks/cli/run.rb +2 -2
- data/lib/hecks/cli/smoke_http/checks.rb +4 -1
- data/lib/hecks/cli/smoke_http/transport.rb +3 -3
- data/lib/hecks/cli/smoke_http.rb +3 -0
- data/lib/hecks/codemod.rb +5 -2
- data/lib/hecks/corpus/routes.rb +7 -0
- data/lib/hecks/corpus/rust.rb +3 -2
- data/lib/hecks/deploy/bluebook/deploy.bluebook +18 -2
- data/lib/hecks/doc/reference/rendering.rb +6 -3
- data/lib/hecks/forms/app/commands.rb +8 -7
- data/lib/hecks/forms/app/queries.rb +2 -1
- data/lib/hecks/forms/app.rb +1 -1
- data/lib/hecks/forms/record_renderer.rb +1 -1
- data/lib/hecks/framework/bluebook/privacy.bluebook +11 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +4 -1
- data/lib/hecks/fuzzing/failure_triage.rb +2 -1
- data/lib/hecks/fuzzing/mutation/run.rb +11 -4
- data/lib/hecks/fuzzing/properties/catalog.rb +6 -4
- data/lib/hecks/fuzzing/properties/guaranteed_by_construction.rb +18 -11
- data/lib/hecks/fuzzing/properties/guards.rb +1 -1
- data/lib/hecks/fuzzing/self_consistency/saga_redelivery.rb +1 -1
- data/lib/hecks/fuzzing/self_consistency.rb +6 -2
- data/lib/hecks/fuzzing/shrinker.rb +3 -1
- data/lib/hecks/fuzzing/value_generator.rb +19 -5
- data/lib/hecks/gate/stages.yml +4 -10
- data/lib/hecks/grammar/evolve.rb +1 -1
- data/lib/hecks/grammar/expression_operators.json +372 -0
- data/lib/hecks/grammar/operators.rb +3 -1
- data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +10 -10
- data/lib/hecks/hecks/adapters/codebase/inspection/rubycritic.rb +35 -0
- data/lib/hecks/hecks/adapters/codebase/inspection.rb +104 -0
- data/lib/hecks/hecks/adapters/codebase/publishing.rb +2 -1
- data/lib/hecks/hecks/adapters/codebase/release_facts.rb +12 -2
- data/lib/hecks/hecks/adapters/codebase/source_tree.rb +2 -1
- data/lib/hecks/hecks/adapters/codebase/test_runner.rb +1 -1
- data/lib/hecks/hecks/adapters/cost_explorer.rb +8 -5
- data/lib/hecks/hecks/adapters/deploy_toolchain/statuses.rb +2 -1
- data/lib/hecks/hecks/adapters/finding_github.rb +13 -6
- data/lib/hecks/hecks/adapters/first_admin.rb +2 -2
- data/lib/hecks/hecks/adapters/github_rulesets.rb +6 -3
- data/lib/hecks/hecks/adapters/in_process_boot.rb +1 -1
- data/lib/hecks/hecks/adapters/journal_store/compaction.rb +1 -1
- data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +10 -20
- data/lib/hecks/hecks/adapters/journal_store/held_domain.rb +28 -5
- data/lib/hecks/hecks/adapters/local_files.rb +2 -2
- data/lib/hecks/hecks/adapters/rehearsal/audit_report.rb +107 -0
- data/lib/hecks/hecks/adapters/rehearsal/census.rb +71 -0
- data/lib/hecks/hecks/adapters/rehearsal/declaration.rb +133 -0
- data/lib/hecks/hecks/adapters/rehearsal/environment.rb +83 -0
- data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +142 -0
- data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +96 -0
- data/lib/hecks/hecks/adapters/rehearsal/postgres_census.rb +100 -0
- data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +69 -0
- data/lib/hecks/hecks/adapters/rehearsal/postgres_store.rb +129 -0
- data/lib/hecks/hecks/adapters/rehearsal/rows.rb +41 -0
- data/lib/hecks/hecks/adapters/rehearsal/rust_audit.rb +83 -0
- data/lib/hecks/hecks/adapters/rehearsal/subject.rb +104 -0
- data/lib/hecks/hecks/adapters/rehearsal.rb +54 -0
- data/lib/hecks/hecks/adapters/rehearsal_store.adapter +7 -0
- data/lib/hecks/hecks/adapters/rehearsal_store.rb +81 -0
- data/lib/hecks/hecks/adapters/terminal.rb +13 -11
- data/lib/hecks/hecks/codebase.behaviors +13 -1
- data/lib/hecks/hecks/codebase.bluebook +239 -5
- data/lib/hecks/hecks/custodian.behaviors +24 -6
- data/lib/hecks/hecks/custodian.bluebook +6 -1227
- data/lib/hecks/hecks/deploy.behaviors +1 -1
- data/lib/hecks/hecks/deploy.world +1 -3
- data/lib/hecks/hecks/hecks.behaviors +1 -1
- data/lib/hecks/hecks/hecks.hecksagon +53 -18
- data/lib/hecks/hecks/hecks.world +10 -10
- data/lib/hecks/hecks/rehearsal.bluebook +477 -0
- data/lib/hecks/hecks/site.behaviors +1 -1
- data/lib/hecks/hecks/site.world +0 -1
- data/lib/hecks/hecks/tooling.bluebook +1238 -0
- data/lib/hecks/indifferent_key.rb +20 -0
- data/lib/hecks/language/adapter.bluebook +13 -3
- data/lib/hecks/language/bluebook/aggregate.bluebook +23 -0
- data/lib/hecks/language/bluebook/bluebook.bluebook +15 -0
- data/lib/hecks/language/bluebook/entity.bluebook +18 -0
- data/lib/hecks/language/bluebook/policy.bluebook +9 -1
- data/lib/hecks/language/bluebook/query.bluebook +1 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +11 -4
- data/lib/hecks/language/hecksagon/domain_port.bluebook +1 -1
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +22 -0
- data/lib/hecks/language/oidc.json +10 -0
- data/lib/hecks/language/port.bluebook +4 -4
- data/lib/hecks/literal.rb +12 -3
- data/lib/hecks/ports/key_vault.rb +7 -2
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +21 -10
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/boot.rb +4 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +2 -1
- data/lib/hecks/ports/persistence/repository_factory.rb +6 -3
- data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +29 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-service.sh.tmpl +1 -1
- data/lib/hecks/projections/deploy/preview/preview.sh.erb +1 -1
- data/lib/hecks/projections/deploy/shared.rb +5 -2
- data/lib/hecks/projections/model/deviations.rb +2 -2
- data/lib/hecks/projections/model.rb +2 -2
- data/lib/hecks/projections/oidc.rb +13 -1
- data/lib/hecks/projections/shape.rb +5 -2
- data/lib/hecks/projections/site/cms_editor/attributes.rb +26 -6
- data/lib/hecks/projections/site/cms_editor/blocks.rb +157 -0
- data/lib/hecks/projections/site/cms_editor/checks.rb +14 -1
- data/lib/hecks/projections/site/cms_editor/clearing.rb +70 -0
- data/lib/hecks/projections/site/cms_editor/color.rb +124 -0
- data/lib/hecks/projections/site/cms_editor/composition.rb +42 -0
- data/lib/hecks/projections/site/cms_editor/destructive.rb +61 -0
- data/lib/hecks/projections/site/cms_editor/drafts.rb +102 -0
- data/lib/hecks/projections/site/cms_editor/hints.rb +74 -0
- data/lib/hecks/projections/site/cms_editor/icons.rb +86 -0
- data/lib/hecks/projections/site/cms_editor/key_kinds.rb +31 -0
- data/lib/hecks/projections/site/cms_editor/limits.rb +72 -0
- data/lib/hecks/projections/site/cms_editor/media.rb +1 -1
- data/lib/hecks/projections/site/cms_editor/moment.rb +34 -0
- data/lib/hecks/projections/site/cms_editor/palette.rb +115 -0
- data/lib/hecks/projections/site/cms_editor/pickers.rb +85 -0
- data/lib/hecks/projections/site/cms_editor/preview.rb +91 -0
- data/lib/hecks/projections/site/cms_editor/relations.rb +92 -0
- data/lib/hecks/projections/site/cms_editor/scale_checks.rb +66 -0
- data/lib/hecks/projections/site/cms_editor/scheduling.rb +150 -0
- data/lib/hecks/projections/site/cms_editor/schema.rb +96 -19
- data/lib/hecks/projections/site/cms_editor/setting.rb +52 -9
- data/lib/hecks/projections/site/cms_editor/targets.rb +86 -0
- data/lib/hecks/projections/site/cms_editor/templates/package.json.tmpl +18 -1
- data/lib/hecks/projections/site/cms_editor/templates/src/app.ts.tmpl +251 -57
- data/lib/hecks/projections/site/cms_editor/templates/src/assets.ts.tmpl +98 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/auth/membership.ts.tmpl +32 -24
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/app.css.tmpl +412 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/autosave.js.tmpl +144 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/blocks.js.tmpl +500 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/body_doc.js.tmpl +175 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/body_editor.js.tmpl +418 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/epoch.js.tmpl +61 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/forms.js.tmpl +210 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/lists.js.tmpl +40 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/main.js.tmpl +26 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/media_picker.js.tmpl +232 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/moments.js.tmpl +73 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/pickers.js.tmpl +152 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/preview.js.tmpl +98 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/shell.js.tmpl +140 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/toast.js.tmpl +66 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/choices.ts.tmpl +99 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/commands.ts.tmpl +78 -24
- data/lib/hecks/projections/site/cms_editor/templates/src/config.ts.tmpl +6 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/flash.ts.tmpl +52 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/host.ts.tmpl +19 -2
- data/lib/hecks/projections/site/cms_editor/templates/src/media/handler.ts.tmpl +31 -8
- data/lib/hecks/projections/site/cms_editor/templates/src/names.ts.tmpl +36 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/schema.ts.tmpl +131 -2
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/availability.ts.tmpl +15 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/block_list.js.tmpl +213 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/blocks.ts.tmpl +275 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/body_model.js.tmpl +8 -5
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/detail.ts.tmpl +88 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/dialogs.ts.tmpl +43 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/fields.ts.tmpl +229 -34
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/html.ts.tmpl +40 -81
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/icons.ts.tmpl +15 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/input.ts.tmpl +29 -12
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/links.ts.tmpl +100 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/moments.ts.tmpl +44 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/outline.ts.tmpl +53 -17
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/pages.ts.tmpl +178 -88
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/paging.ts.tmpl +82 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/pickers.ts.tmpl +110 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/preview.ts.tmpl +87 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/related.ts.tmpl +97 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/schedule.ts.tmpl +160 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/shell.ts.tmpl +211 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/table.ts.tmpl +151 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/theme.js.tmpl +16 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/values.ts.tmpl +69 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/words.ts.tmpl +97 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/world.ts.tmpl +35 -0
- data/lib/hecks/projections/site/cms_editor/templates/tsconfig.json.tmpl +2 -1
- data/lib/hecks/projections/site/cms_editor/theme.rb +29 -0
- data/lib/hecks/projections/site/cms_editor/theme_checks.rb +57 -0
- data/lib/hecks/projections/site/cms_editor.rb +49 -23
- data/lib/hecks/projections/site/role_probe.rb +2 -0
- data/lib/hecks/projector/cli_audience.rb +2 -1
- data/lib/hecks/projector/cli_projector/listing.rb +1 -1
- data/lib/hecks/projector/cli_projector/option_specs.rb +1 -1
- data/lib/hecks/projector/docs_projector/closing.rb +1 -1
- data/lib/hecks/projector/docs_projector.rb +1 -1
- data/lib/hecks/projector/exporter/bindings.rb +40 -10
- data/lib/hecks/projector/exporter/capability_values.rb +55 -0
- data/lib/hecks/projector/narrate_projector/reactions.rb +1 -1
- data/lib/hecks/projector/target.rb +11 -0
- data/lib/hecks/projector.rb +32 -5
- data/lib/hecks/quality_control/adapters/github_checks.rb +12 -8
- data/lib/hecks/quality_control/adapters/github_issues.rb +8 -4
- data/lib/hecks/quality_control/cli/qa_open_pr.rb +2 -2
- data/lib/hecks/quality_control/cli/qa_sweep/checks/shrinking.rb +1 -1
- data/lib/hecks/quality_control/cli/qa_sweep/finding_report.rb +1 -1
- data/lib/hecks/quality_control/cli/qa_sweep.rb +1 -1
- data/lib/hecks/quality_control/quality_control.bluebook +6 -2
- data/lib/hecks/release/approval.rb +64 -0
- data/lib/hecks/router/{door.rb → aggregate_reader.rb} +5 -4
- data/lib/hecks/router/namespace_installer.rb +16 -16
- data/lib/hecks/router.rb +1 -1
- data/lib/hecks/runtime/best_effort.rb +22 -0
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +12 -2
- data/lib/hecks/runtime/command_interpreter.rb +3 -2
- data/lib/hecks/runtime/command_rules/emission.rb +1 -1
- data/lib/hecks/runtime/dispatcher.rb +4 -4
- data/lib/hecks/runtime/entity_element/location.rb +1 -1
- data/lib/hecks/runtime/event.rb +5 -0
- data/lib/hecks/runtime/identity.rb +7 -6
- data/lib/hecks/runtime/interpreting.rb +2 -1
- data/lib/hecks/runtime/invocation_builder.rb +1 -1
- data/lib/hecks/runtime/loader/boot_steps.rb +8 -5
- data/lib/hecks/runtime/loader.rb +10 -10
- data/lib/hecks/runtime/policy_interpreter/arguments.rb +7 -4
- data/lib/hecks/runtime/policy_interpreter/fan_out.rb +3 -3
- data/lib/hecks/runtime/policy_interpreter.rb +6 -6
- data/lib/hecks/runtime/port_operation_interpreter.rb +1 -1
- data/lib/hecks/runtime/reaction_invocation.rb +1 -1
- data/lib/hecks/runtime/rebuild_sweep.rb +11 -1
- data/lib/hecks/runtime/registry/ask_verification.rb +35 -0
- data/lib/hecks/runtime/registry/hecksagon_rules.rb +10 -0
- data/lib/hecks/runtime/registry/merging.rb +1 -1
- data/lib/hecks/runtime/registry/providers.rb +16 -0
- data/lib/hecks/runtime/registry/read_repository.rb +6 -4
- data/lib/hecks/runtime/registry/verification.rb +4 -1
- data/lib/hecks/runtime/registry/world_defaults.rb +29 -0
- data/lib/hecks/runtime/registry.rb +1 -1
- data/lib/hecks/runtime/saga_interpreter/compensation.rb +1 -1
- data/lib/hecks/runtime/saga_interpreter/delivery.rb +4 -4
- data/lib/hecks/runtime/saga_interpreter/transitions.rb +1 -1
- data/lib/hecks/runtime/saga_interpreter.rb +3 -3
- data/lib/hecks/runtime/tenant_check.rb +5 -4
- data/lib/hecks/runtime/value/coercion.rb +2 -2
- data/lib/hecks/runtime/value/validation.rb +16 -4
- data/lib/hecks/runtime.rb +9 -9
- data/lib/hecks/rust_build/project_rust/target_ir.rb +1 -1
- data/lib/hecks/rust_build/wasm.rb +1 -1
- data/lib/hecks/rust_build/wasm_browser.rb +1 -1
- data/lib/hecks/rust_build.rb +11 -0
- data/lib/hecks/served_options.rb +53 -0
- data/lib/hecks/site/bluebook/site.bluebook +1 -0
- data/lib/hecks/storehouse/audit.rb +1 -1
- data/lib/hecks/storehouse/dispatching.rb +8 -6
- data/lib/hecks/storehouse/introspection.rb +2 -2
- data/lib/hecks/storehouse/querying.rb +6 -4
- data/lib/hecks/storehouse.rb +4 -4
- data/lib/hecks/tools/comment_style/source_file.rb +4 -1
- data/lib/hecks/tools/deploy_recipe_lint/fixtures.rb +4 -1
- data/lib/hecks/tools/lanes/rows.rb +6 -0
- data/lib/hecks/tools/oidc_manifests.rb +4 -5
- data/lib/hecks/tools/regeneration_run.rb +11 -4
- data/lib/hecks/tools/site_routes/editor_files.rb +12 -5
- data/lib/hecks/tools/tenant_provisioning.rb +2 -2
- data/lib/hecks/tools/tools_doc.rb +4 -4
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +4 -4
- data/lib/hecks.rb +15 -15
- data/lib/rubocop/cop/hecks/env_dump.rb +69 -0
- data/lib/rubocop/cop/hecks/monotonic_duration.rb +53 -0
- data/lib/rubocop/cop/hecks/no_wall_clock_in_runtime.rb +34 -0
- data/lib/rubocop/cop/hecks/raw_shell_interpolation.rb +79 -0
- data/lib/rubocop/cop/hecks/sleep_in_spec.rb +34 -0
- data/lib/rubocop/cop/hecks/swallowed_rescue.rb +37 -0
- data/lib/rubocop/cop/hecks/symbol_string_key_mix.rb +50 -0
- data/lib/rubocop/cop/hecks/untyped_raise.rb +37 -0
- data/prebuilt/05c358d38d661a3774f813d49c66f5b68939af94432d66257a56f49ee2598994.marshal +0 -0
- data/prebuilt/26c40c61f09b09b11651c64668eda3ceb180f83cf2a3802d3033d2fd99ac841b.marshal +0 -0
- data/prebuilt/verdicts-230ac18c25abfd3d92ede032bee2661a4c2cd2773ab0cf0e2ae52546ba2f7af9.json +1 -0
- data/rust/.gitignore +6 -0
- data/rust/Cargo.toml +6 -0
- data/rust/build/src/binding_pass.rs +103 -0
- data/rust/build/src/cargo_sync.rs +6 -0
- data/rust/build/src/lineage_pass.rs +3 -3
- data/rust/build/src/main.rs +17 -5
- data/rust/build/src/pipeline.rs +48 -5
- data/rust/build/src/seams_pass.rs +123 -0
- data/rust/build/src/translation_pass.rs +167 -0
- data/rust/codegen/src/asks.rs +103 -0
- data/rust/codegen/src/commands.rs +13 -8
- data/rust/codegen/src/constraints.rs +25 -0
- data/rust/codegen/src/domain_generator.rs +2 -0
- data/rust/codegen/src/expr_emitter.rs +13 -0
- data/rust/codegen/src/json_codec.rs +54 -15
- data/rust/codegen/src/main.rs +1 -0
- data/rust/codegen/src/mutations.rs +128 -3
- data/rust/codegen/src/queries.rs +11 -2
- data/rust/codegen/src/reactions.rs +1 -1
- data/rust/codegen/src/types.rs +166 -8
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/src/api.rs +4 -4
- data/rust/host/src/bin/validate_harness.rs +43 -0
- data/rust/host/src/boundary_fuzz/expr_json.rs +2 -2
- data/rust/host/src/boundary_fuzz/ir.rs +1 -0
- data/rust/host/src/boundary_fuzz/server.rs +1 -1
- data/rust/host/src/checkout.rs +35 -19
- data/rust/host/src/commerce_ir.rs +408 -2
- data/rust/host/src/expr_json.rs +118 -0
- data/rust/host/src/mint.rs +1 -1
- data/rust/host/src/payments/tests.rs +27 -1
- data/rust/host/src/payments.rs +12 -4
- data/rust/host/src/query_step.rs +22 -4
- data/rust/host/src/reference_validate.rs +653 -108
- data/rust/host/src/resend.rs +2 -0
- data/rust/host/src/server.rs +19 -6
- data/rust/host/src/ui_schema.rs +132 -5
- data/rust/host/src/web/newsletter.rs +7 -8
- data/rust/host/src/web/newsletter_send.rs +38 -13
- data/rust/host/src/web/registrations.rs +229 -26
- data/rust/host/src/web.rs +3 -3
- data/rust/parser/src/emit.rs +38 -6
- data/rust/parser/src/expr/ast_json.rs +7 -1
- data/rust/parser/src/expr/resolver.rs +171 -6
- data/rust/parser/src/ir.rs +7 -0
- data/rust/parser/src/keywords.rs +24 -0
- data/rust/parser/src/main.rs +3 -0
- data/rust/parser/src/parse/aggregate.rs +2 -1
- data/rust/parser/src/parse/chapter.rs +2 -2
- data/rust/parser/src/parse/command.rs +2 -0
- data/rust/parser/src/parse/hecksagon.rs +5 -2
- data/rust/parser/src/parse/lifecycle.rs +81 -0
- data/rust/parser/src/parse/mod.rs +15 -1
- data/rust/parser/src/parse/policy.rs +13 -2
- data/rust/parser/src/parse/value_object.rs +13 -8
- data/rust/parser/src/ruby_value.rs +1 -1
- data/rust/src/exemplar/commands.rs +4 -4
- data/rust/src/exemplar/constraints.rs +1 -1
- data/rust/src/exemplar/json.rs +43 -1
- data/rust/src/kernel/cli.rs +7 -5
- data/rust/src/kernel/dispatch.rs +2 -2
- data/rust/src/kernel/dispatch_guard_tests.rs +1 -1
- data/rust/src/kernel/expr.rs +66 -3
- data/rust/src/kernel/expression_operators/arithmetic.rs +130 -5
- data/rust/src/kernel/expression_operators/text.rs +26 -2
- data/rust/src/kernel/json.rs +15 -0
- data/rust/src/kernel/mod.rs +1 -1
- data/rust/src/kernel/orchestrate.rs +54 -5
- data/rust/src/kernel/orchestrate_guard_tests.rs +67 -0
- data/rust/src/kernel/pattern.rs +60 -0
- data/rust/src/kernel/reference_lookup.rs +90 -7
- metadata +140 -38
- data/lib/hecks/doors/cli_door/nesting.rb +0 -34
- data/lib/hecks/doors/cli_door/shorthand.rb +0 -85
- data/lib/hecks/doors/cli_door.rb +0 -124
- data/lib/hecks/doors/cli_runner/answers.rb +0 -138
- data/lib/hecks/doors/cli_runner/resolution.rb +0 -146
- data/lib/hecks/doors/cli_runner/settling.rb +0 -115
- data/lib/hecks/doors/cli_runner/streaming.rb +0 -90
- data/lib/hecks/doors/cli_runner/suggestions.rb +0 -43
- data/lib/hecks/doors/cli_runner.rb +0 -66
- data/lib/hecks/doors/command_request.rb +0 -119
- data/lib/hecks/doors/handle.rb +0 -208
- data/lib/hecks/doors/json_door.rb +0 -132
- data/lib/hecks/doors/launcher_options.rb +0 -247
- data/lib/hecks/doors/mcp_door/properties.rb +0 -47
- data/lib/hecks/doors/mcp_door/tool_calls.rb +0 -113
- data/lib/hecks/doors/mcp_door/tool_definitions/auditing.rb +0 -51
- data/lib/hecks/doors/mcp_door/tool_definitions/calling.rb +0 -69
- data/lib/hecks/doors/mcp_door/tool_definitions/reading.rb +0 -97
- data/lib/hecks/doors/mcp_door/tool_definitions.rb +0 -15
- data/lib/hecks/doors/mcp_door.rb +0 -156
- data/lib/hecks/doors/mcp_door_scope/argument_policy.rb +0 -92
- data/lib/hecks/doors/mcp_door_scope/env_problems.rb +0 -55
- data/lib/hecks/doors/mcp_door_scope/presenter.rb +0 -78
- data/lib/hecks/doors/mcp_door_scope.rb +0 -201
- data/lib/hecks/doors/mcp_guide_cache.rb +0 -75
- data/lib/hecks/doors/ruby_door/aggregate_door.rb +0 -162
- data/lib/hecks/doors/ruby_door/chapter.rb +0 -89
- data/lib/hecks/doors/ruby_door.rb +0 -80
- data/lib/hecks/doors/usage_cache.rb +0 -139
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/body_widget.js.tmpl +0 -282
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/media_picker.js.tmpl +0 -109
- data/prebuilt/c28d6d9db14eea3e17161e9d9c262efee30b256cf97693a7cfd552bce24b2f7f.marshal +0 -0
- data/prebuilt/cb375cba61ca0dbe8ddb9739c306510fe05e5f4b7a9b6955caff8224c63ea0e0.marshal +0 -0
- data/prebuilt/verdicts-d6d584ad43b5f3cebf88aafbbdbf024c38574acf18e06adffbc555f7786b1c2f.json +0 -1
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
__BANNER__
|
|
2
|
+
|
|
3
|
+
// A body written as a draft saves by itself. The form's command saves a draft beside the live body
|
|
4
|
+
// (the page says which: `data-draft`), so this posts that same form, form-encoded, to its own address
|
|
5
|
+
// with a header that makes the server answer in plain text instead of redirecting. It never posts to
|
|
6
|
+
// the live body: the server runs only the draft-saving command for such a post.
|
|
7
|
+
//
|
|
8
|
+
// A save happens after five seconds with no typing, and when the writing surface loses focus, and
|
|
9
|
+
// only when something changed and the form passes its own checks. The status line says what is
|
|
10
|
+
// happening: "Saving...", "Draft saved 14:03", or "Could not save: <reason>" with a way to try again.
|
|
11
|
+
// The browser asks before the page is left only while there are changes that are not saved.
|
|
12
|
+
|
|
13
|
+
const QUIET_MS = 5000;
|
|
14
|
+
|
|
15
|
+
/** The time of day, as the person's browser writes it, without seconds. */
|
|
16
|
+
const clock = () => new Date().toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Starts saving the draft of the body `root` holds.
|
|
20
|
+
*
|
|
21
|
+
* @param {HTMLElement} root the element that holds the writing surface (`data-body-editor`), or a
|
|
22
|
+
* list of mixed blocks (`data-block-list`)
|
|
23
|
+
* @param {{ form?: HTMLFormElement, blocked?: () => string | null, refused?: (problems: object[]) => void }} [options]
|
|
24
|
+
* `form` when it is not the root's own; `blocked` says why the form is not ready to save (null when
|
|
25
|
+
* it is); `refused` is given the blocks a refused save named
|
|
26
|
+
* @returns {{ touch(): void, blur(): void }} what the writing surface calls when the text changes
|
|
27
|
+
* and when it loses focus
|
|
28
|
+
*/
|
|
29
|
+
export function startDraft(root, options = {}) {
|
|
30
|
+
const form = options.form ?? root.closest("form");
|
|
31
|
+
const status = document.querySelector("[data-draft-status]");
|
|
32
|
+
const discard = document.querySelector("[data-draft-discard]");
|
|
33
|
+
let dirty = false;
|
|
34
|
+
let version = 0;
|
|
35
|
+
let timer = null;
|
|
36
|
+
let current = null;
|
|
37
|
+
let leaving = false;
|
|
38
|
+
|
|
39
|
+
const say = (text, retry = false) => {
|
|
40
|
+
if (!status) return;
|
|
41
|
+
status.replaceChildren(text);
|
|
42
|
+
if (!retry) return;
|
|
43
|
+
const button = Object.assign(document.createElement("button"), { type: "button", className: "link ml-2", textContent: "Try again" });
|
|
44
|
+
button.addEventListener("click", () => save());
|
|
45
|
+
status.append(button);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** Whether the form may be sent: its own checks pass and the body has something in it. */
|
|
49
|
+
const passes = () => form.checkValidity() && !root.editor?.isEmpty;
|
|
50
|
+
|
|
51
|
+
const send = async () => {
|
|
52
|
+
const sent = version;
|
|
53
|
+
say("Saving...");
|
|
54
|
+
try {
|
|
55
|
+
const response = await fetch(form.action, {
|
|
56
|
+
method: "POST",
|
|
57
|
+
body: new URLSearchParams(new FormData(form)),
|
|
58
|
+
headers: { "X-Editor-Autosave": "1" },
|
|
59
|
+
credentials: "same-origin",
|
|
60
|
+
redirect: "manual",
|
|
61
|
+
});
|
|
62
|
+
if (response.type === "opaqueredirect") return fail("Sign in again to keep saving.");
|
|
63
|
+
const text = (await response.text()).trim();
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
named(response.headers.get("x-editor-problems"));
|
|
66
|
+
return fail(text || `The editor answered ${response.status}.`);
|
|
67
|
+
}
|
|
68
|
+
if (version === sent) dirty = false;
|
|
69
|
+
say(`Draft saved ${clock()}`);
|
|
70
|
+
discard?.removeAttribute("hidden");
|
|
71
|
+
document.dispatchEvent(new CustomEvent("editor:saved"));
|
|
72
|
+
return version === sent;
|
|
73
|
+
} catch {
|
|
74
|
+
return fail("the editor could not be reached.");
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** Hands the blocks a refusal names to the list, which opens them. */
|
|
79
|
+
const named = (header) => {
|
|
80
|
+
try {
|
|
81
|
+
if (header) options.refused?.(JSON.parse(decodeURIComponent(header)));
|
|
82
|
+
} catch {
|
|
83
|
+
/* a header that is not ours is ignored */
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const fail = (reason) => {
|
|
88
|
+
say(`Could not save: ${reason}`, true);
|
|
89
|
+
return false;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/** Saves what is unsaved, once at a time; resolves to whether everything is saved. */
|
|
93
|
+
const save = () => {
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
if (current) return current.then(() => save());
|
|
96
|
+
if (!dirty) return Promise.resolve(true);
|
|
97
|
+
const why = options.blocked?.();
|
|
98
|
+
if (why) {
|
|
99
|
+
say(`Not saved: ${why}`);
|
|
100
|
+
return Promise.resolve(false);
|
|
101
|
+
}
|
|
102
|
+
if (!passes()) {
|
|
103
|
+
say("Not saved: write something first, or fix what the form marks.");
|
|
104
|
+
return Promise.resolve(false);
|
|
105
|
+
}
|
|
106
|
+
current = send().finally(() => {
|
|
107
|
+
current = null;
|
|
108
|
+
});
|
|
109
|
+
return current;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
window.addEventListener("beforeunload", (event) => {
|
|
113
|
+
if (leaving || (!dirty && !current)) return;
|
|
114
|
+
event.preventDefault();
|
|
115
|
+
event.returnValue = "";
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
document.addEventListener("submit", async (event) => {
|
|
119
|
+
const target = event.target;
|
|
120
|
+
if (target === form) {
|
|
121
|
+
leaving = true;
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (!target.matches?.("[data-flush-draft]") || (!dirty && !current)) return;
|
|
125
|
+
event.preventDefault();
|
|
126
|
+
if (await save()) {
|
|
127
|
+
leaving = true;
|
|
128
|
+
target.submit();
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
touch() {
|
|
134
|
+
dirty = true;
|
|
135
|
+
version += 1;
|
|
136
|
+
say("Changes not saved yet.");
|
|
137
|
+
clearTimeout(timer);
|
|
138
|
+
timer = setTimeout(save, QUIET_MS);
|
|
139
|
+
},
|
|
140
|
+
blur() {
|
|
141
|
+
if (dirty) save();
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
__BANNER__
|
|
2
|
+
|
|
3
|
+
// What a list of mixed blocks does with a script (the markup is ui/blocks.ts). Cards open and close;
|
|
4
|
+
// a new card is made from the template the page carries for its kind; cards move up and down by
|
|
5
|
+
// buttons, with the result announced in a polite live region and focus kept on the button that was
|
|
6
|
+
// pressed; a card is removed after a question in a native dialog; and the cards are numbered again
|
|
7
|
+
// after each change, renaming their fields so the form posts the list in the order it is shown.
|
|
8
|
+
// A body inside a card gets its writing surface only when the card is first opened, so a page of
|
|
9
|
+
// forty cards holds a handful of editors. A list written as a draft saves by itself (autosave.js),
|
|
10
|
+
// and holds back while a card still lacks what its kind needs.
|
|
11
|
+
//
|
|
12
|
+
// The same script serves a page whose working copy is a value object (`data-draft-form` on the form):
|
|
13
|
+
// the form as a whole saves by itself, whichever of its lists, pictures, writing surfaces and plain
|
|
14
|
+
// inputs the person touches, and a picture outside any list opens the picker from its own group.
|
|
15
|
+
import { retarget, safe } from "../ui/block_list.js";
|
|
16
|
+
import { startDraft } from "./autosave.js";
|
|
17
|
+
import { enhanceRows, firstProblem } from "./forms.js";
|
|
18
|
+
import { mountMoments } from "./moments.js";
|
|
19
|
+
import { mountPickers } from "./pickers.js";
|
|
20
|
+
|
|
21
|
+
/** Loads the picture picker, or is null when the editor has no pictures to choose from. */
|
|
22
|
+
const loadPicker = __PICKER_LOADER__;
|
|
23
|
+
|
|
24
|
+
const pluralOf = (noun) => (/[^aeiou]y$/.test(noun) ? `${noun.slice(0, -1)}ies` : /(s|x|z|ch|sh)$/.test(noun) ? `${noun}es` : `${noun}s`);
|
|
25
|
+
|
|
26
|
+
/** The attributes whose text names a field of a card, and so changes when the card's index does. */
|
|
27
|
+
const NAMED = ["name", "id", "for", "aria-controls", "aria-describedby", "aria-labelledby", "href", "data-path", "data-name", "data-key", "data-alt"];
|
|
28
|
+
|
|
29
|
+
/** Renames the fields of `card` (and the cards templates it carries) from index `was` to `at`. */
|
|
30
|
+
function renumberFields(card, path, was, at) {
|
|
31
|
+
for (const element of [card, ...card.querySelectorAll("*")]) {
|
|
32
|
+
if (element.localName === "template") element.innerHTML = retarget(element.innerHTML, path, was, at);
|
|
33
|
+
for (const name of NAMED) {
|
|
34
|
+
if (element.hasAttribute(name)) element.setAttribute(name, retarget(element.getAttribute(name), path, was, at));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The cards of a group, in the order shown. */
|
|
40
|
+
const cardsOf = (group) => [...group.querySelector(":scope > [data-cards]").children];
|
|
41
|
+
|
|
42
|
+
/** The group a card is in. */
|
|
43
|
+
const groupOf = (card) => card.closest("[data-group]");
|
|
44
|
+
|
|
45
|
+
/** The list a group belongs to (the outermost group). */
|
|
46
|
+
const listOf = (node) => node.closest("[data-block-list]");
|
|
47
|
+
|
|
48
|
+
/** The working copies saved by the form as a whole, by form. */
|
|
49
|
+
const formDrafts = new WeakMap();
|
|
50
|
+
|
|
51
|
+
/** The polite line that speaks for `root`: a list's own, or a form's (made when it has none yet). */
|
|
52
|
+
function lineOf(root) {
|
|
53
|
+
if (!root.matches("form")) return root.querySelector("[data-block-say]");
|
|
54
|
+
let line = root.querySelector(":scope > [data-form-say]");
|
|
55
|
+
if (!line) {
|
|
56
|
+
line = Object.assign(document.createElement("p"), { className: "sr-only" });
|
|
57
|
+
line.setAttribute("role", "status");
|
|
58
|
+
line.setAttribute("aria-live", "polite");
|
|
59
|
+
line.dataset.formSay = "";
|
|
60
|
+
root.append(line);
|
|
61
|
+
}
|
|
62
|
+
return line;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const say = (root, text) => {
|
|
66
|
+
const line = lineOf(root);
|
|
67
|
+
line.textContent = "";
|
|
68
|
+
setTimeout(() => { line.textContent = text; }, 30);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** The controls of a card that are its own: not those of a card nested in it, nor a writing surface's fields. */
|
|
72
|
+
const ownControls = (card) =>
|
|
73
|
+
[...card.querySelectorAll("input, select, textarea")].filter((control) => control.closest("[data-card]") === card && !control.closest("[data-body-editor]"));
|
|
74
|
+
|
|
75
|
+
/** The one line that stands for a card: its first required text, else its longest text. */
|
|
76
|
+
function summaryOf(card) {
|
|
77
|
+
const prefix = `${groupOf(card).dataset.path}.${card.dataset.index}.`;
|
|
78
|
+
const requires = (card.dataset.requires ?? "").split(" ").filter(Boolean).map((slot) => `${prefix}${slot}`);
|
|
79
|
+
const alt = card.dataset.summaryAlt ? `${prefix}${card.dataset.summaryAlt}` : null;
|
|
80
|
+
const named = (control) => requires.includes(control.name) || requires.some((slot) => control.name === `${slot}.value`);
|
|
81
|
+
const texts = ownControls(card)
|
|
82
|
+
.filter((control) => (control.type === "text" || control.type === "textarea") && control.value.trim() !== "" && (!control.closest("[data-picture]") || named(control) || control.name === alt))
|
|
83
|
+
.map((control) => ({ text: control.value.trim(), required: named(control), alt: control.name === alt }));
|
|
84
|
+
const chosen = texts.find((one) => one.alt) ?? texts.find((one) => one.required) ?? texts.reduce((best, one) => (!best || one.text.length > best.text.length ? one : best), null);
|
|
85
|
+
if (chosen) return chosen.text.length > 80 ? `${chosen.text.slice(0, 79)}…` : chosen.text;
|
|
86
|
+
const counts = [...card.querySelectorAll(":scope > [data-card-panel] > [data-group]")]
|
|
87
|
+
.map((group) => [group.dataset.noun, cardsOf(group).length])
|
|
88
|
+
.filter(([, count]) => count > 0)
|
|
89
|
+
.map(([noun, count]) => `${count} ${count === 1 ? noun : pluralOf(noun)}`);
|
|
90
|
+
return counts.join(", ");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const titleOf = (card) => {
|
|
94
|
+
const kind = card.querySelector(":scope > [data-card-head] [data-kind-badge]")?.textContent;
|
|
95
|
+
const number = card.querySelector(":scope > [data-card-head] [data-card-number]")?.textContent;
|
|
96
|
+
const noun = groupOf(card).dataset.noun;
|
|
97
|
+
const summary = card.querySelector(":scope > [data-card-head] [data-card-summary]")?.textContent;
|
|
98
|
+
return `${noun} ${number}${kind ? `, ${kind}` : ""}${summary ? `: ${summary}` : ""}`;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/** Sets the words that name a card to the tools and the toggle (they say which card they move, open or remove). */
|
|
102
|
+
function nameTools(card) {
|
|
103
|
+
const noun = groupOf(card).dataset.noun;
|
|
104
|
+
const number = card.querySelector(":scope > [data-card-head] [data-card-number]").textContent;
|
|
105
|
+
const summary = card.querySelector(":scope > [data-card-head] [data-card-summary]").textContent;
|
|
106
|
+
const kind = card.querySelector(":scope > [data-card-head] [data-kind-badge]")?.textContent;
|
|
107
|
+
const what = `${noun} ${number}${kind ? `, ${kind}` : ""}${summary ? `, ${summary}` : ""}`;
|
|
108
|
+
const words = { up: "Move up", down: "Move down", remove: "Remove" };
|
|
109
|
+
for (const button of card.querySelectorAll(":scope > [data-card-head] [data-tools] button")) {
|
|
110
|
+
const label = `${words[button.dataset.action]} ${what}`;
|
|
111
|
+
button.setAttribute("aria-label", label);
|
|
112
|
+
button.title = label;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Numbers the group's cards again, and says which tools no longer apply. */
|
|
117
|
+
function renumber(group) {
|
|
118
|
+
const path = group.dataset.path;
|
|
119
|
+
const cards = cardsOf(group);
|
|
120
|
+
cards.forEach((card, at) => {
|
|
121
|
+
if (card.dataset.index !== String(at)) {
|
|
122
|
+
renumberFields(card, path, card.dataset.index, String(at));
|
|
123
|
+
card.dataset.index = String(at);
|
|
124
|
+
}
|
|
125
|
+
card.querySelector(":scope > [data-card-head] [data-card-number]").textContent = String(at + 1);
|
|
126
|
+
const static_ = card.querySelector(":scope > [data-card-head] [data-card-static] span");
|
|
127
|
+
if (static_) static_.textContent = `${group.dataset.noun.charAt(0).toUpperCase()}${group.dataset.noun.slice(1)} ${at + 1}`;
|
|
128
|
+
card.querySelector('[data-action="up"]')?.setAttribute("aria-disabled", String(at === 0));
|
|
129
|
+
card.querySelector('[data-action="down"]')?.setAttribute("aria-disabled", String(at === cards.length - 1));
|
|
130
|
+
nameTools(card);
|
|
131
|
+
});
|
|
132
|
+
const count = group.querySelector(":scope > legend [data-count]");
|
|
133
|
+
if (count) count.textContent = `(${cards.length})`;
|
|
134
|
+
const empty = group.querySelector(":scope > [data-empty]");
|
|
135
|
+
if (empty) empty.hidden = cards.length > 0;
|
|
136
|
+
const max = Number(group.dataset.max ?? 0);
|
|
137
|
+
for (const add of group.querySelectorAll(":scope > [data-adder] [data-action=add], :scope > [data-action=add]")) {
|
|
138
|
+
add.setAttribute("aria-disabled", String(max > 0 && cards.length >= max));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Opens or closes a card. Opening mounts the writing surfaces inside it that are not yet mounted. */
|
|
143
|
+
function toggle(card, open) {
|
|
144
|
+
const button = card.querySelector(':scope > [data-card-head] [data-action="toggle"]');
|
|
145
|
+
const panel = card.querySelector(":scope > [data-card-panel]");
|
|
146
|
+
const was = !panel.hidden;
|
|
147
|
+
const now = open ?? !was;
|
|
148
|
+
panel.hidden = !now;
|
|
149
|
+
button.setAttribute("aria-expanded", String(now));
|
|
150
|
+
button.querySelector("use")?.setAttribute("href", `#i-${now ? "chevron-down" : "chevron-right"}`);
|
|
151
|
+
if (now) mountEditors(panel);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Gives each visible rich-text body inside `scope` its writing surface, once. */
|
|
155
|
+
function mountEditors(scope) {
|
|
156
|
+
const pending = [...scope.querySelectorAll("[data-body-editor]:not([data-mounted])")];
|
|
157
|
+
if (pending.length === 0) return;
|
|
158
|
+
for (const root of pending) {
|
|
159
|
+
const card = root.closest("[data-card]");
|
|
160
|
+
const noun = groupOf(card).dataset.noun;
|
|
161
|
+
root.dataset.base ??= root.dataset.label;
|
|
162
|
+
root.dataset.label = `${root.dataset.base} of ${noun} ${Number(card.dataset.index) + 1}`;
|
|
163
|
+
}
|
|
164
|
+
import("./body_editor.js").then((editor) => editor.mountWithin(scope));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Makes a card's script-only parts live: its toggle, its tools, and the cards nested in it. */
|
|
168
|
+
function dress(card) {
|
|
169
|
+
card.querySelector(":scope > [data-card-head] [data-card-static]")?.setAttribute("hidden", "");
|
|
170
|
+
for (const node of card.querySelectorAll(":scope > [data-card-head] [data-tools], :scope > [data-card-head] [data-action=toggle]")) node.hidden = false;
|
|
171
|
+
const button = card.querySelector(':scope > [data-card-head] [data-action="toggle"]');
|
|
172
|
+
const open = card.hasAttribute("data-invalid") || card.hasAttribute("data-open");
|
|
173
|
+
const panel = card.querySelector(":scope > [data-card-panel]");
|
|
174
|
+
panel.hidden = !open;
|
|
175
|
+
button.setAttribute("aria-expanded", String(open));
|
|
176
|
+
button.querySelector("use")?.setAttribute("href", `#i-${open ? "chevron-down" : "chevron-right"}`);
|
|
177
|
+
for (const group of card.querySelectorAll(":scope > [data-card-panel] > [data-group]")) {
|
|
178
|
+
for (const add of group.querySelectorAll(":scope > [data-action=add]")) add.hidden = false;
|
|
179
|
+
for (const nested of cardsOf(group)) dress(nested);
|
|
180
|
+
renumber(group);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** A new card for `group`, from the template of `kind` ("" for a nested group). */
|
|
185
|
+
function create(group, kind) {
|
|
186
|
+
const template = [...group.querySelectorAll(":scope > template[data-card-template]")].find((one) => one.dataset.kind === kind);
|
|
187
|
+
if (!template) return null;
|
|
188
|
+
const cards = group.querySelector(":scope > [data-cards]");
|
|
189
|
+
const token = group.dataset.depth === "0" ? "ZZI0ZZ" : "ZZI1ZZ";
|
|
190
|
+
cards.insertAdjacentHTML("beforeend", template.innerHTML.split(token).join(String(cardsOf(group).length)));
|
|
191
|
+
const card = cards.lastElementChild;
|
|
192
|
+
dress(card);
|
|
193
|
+
enhanceRows(card);
|
|
194
|
+
mountPickers(card);
|
|
195
|
+
mountMoments(card);
|
|
196
|
+
return card;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Whether a card still lacks something its kind requires or a picture's description, with what. */
|
|
200
|
+
function lacks(card) {
|
|
201
|
+
const prefix = `${groupOf(card).dataset.path}.${card.dataset.index}.`;
|
|
202
|
+
const names = (card.dataset.requires ?? "").split(" ").filter(Boolean);
|
|
203
|
+
const held = (slot) => [...card.querySelectorAll("input, select, textarea")].some((control) => {
|
|
204
|
+
const own = control.name === `${prefix}${slot}` || control.name.startsWith(`${prefix}${slot}.`);
|
|
205
|
+
return own && control.type !== "checkbox" && control.value.trim() !== "";
|
|
206
|
+
});
|
|
207
|
+
const missing = names.filter((slot) => !held(slot));
|
|
208
|
+
const own = [...card.querySelectorAll("[data-picture]")].filter((picture) => picture.closest("[data-card]") === card);
|
|
209
|
+
if (own.some(undescribed) && !missing.includes("alt")) missing.push("alt");
|
|
210
|
+
return missing;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Whether a picture names its file and has no description. */
|
|
214
|
+
function undescribed(picture) {
|
|
215
|
+
const form = picture.closest("form") ?? document;
|
|
216
|
+
const key = form.querySelector(`[name="${CSS.escape(picture.dataset.key)}"]`);
|
|
217
|
+
const alt = form.querySelector(`[name="${CSS.escape(picture.dataset.alt)}"]`);
|
|
218
|
+
return Boolean(key?.value.trim()) && alt !== null && alt.value.trim() === "";
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** The card, and the nested cards under it, that lack something. */
|
|
222
|
+
function incomplete(list) {
|
|
223
|
+
return [...list.querySelectorAll("[data-card]")].filter((card) => lacks(card).length > 0);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** The pictures under `scope` that are in no list's card: a picture slot of a value object. */
|
|
227
|
+
const loose = (scope) => [...scope.querySelectorAll("[data-picture]")].filter((picture) => !picture.closest("[data-card]"));
|
|
228
|
+
|
|
229
|
+
/** Marks a picture's alt control invalid, with the line that says what to do, or clears the mark. */
|
|
230
|
+
function markAlt(picture, show) {
|
|
231
|
+
const alt = picture.querySelector(`[name="${picture.dataset.alt}"]`);
|
|
232
|
+
if (!alt) return;
|
|
233
|
+
const id = `${alt.id}-error`;
|
|
234
|
+
picture.querySelector(`#${CSS.escape(id)}`)?.remove();
|
|
235
|
+
alt.removeAttribute("aria-invalid");
|
|
236
|
+
if (!show) return;
|
|
237
|
+
alt.setAttribute("aria-invalid", "true");
|
|
238
|
+
alt.setAttribute("aria-describedby", id);
|
|
239
|
+
alt.after(Object.assign(document.createElement("p"), { id, className: "text-xs font-semibold text-error", textContent: "Describe the picture for people who cannot see it." }));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** The registered pictures' addresses by key, fetched once for the thumbnails. */
|
|
243
|
+
let known = null;
|
|
244
|
+
function pictures(url) {
|
|
245
|
+
known ??= fetch(url, { headers: { Accept: "application/json" }, credentials: "same-origin" })
|
|
246
|
+
.then((response) => (response.ok ? response.json() : { pictures: [] }))
|
|
247
|
+
.then((answer) => new Map(answer.pictures.map((picture) => [picture.key, picture.url])))
|
|
248
|
+
.catch(() => new Map());
|
|
249
|
+
return known;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Shows the thumbnail of the picture a card names, when it is a registered one. */
|
|
253
|
+
async function thumb(picture) {
|
|
254
|
+
const url = picture.querySelector("[data-action=pick-picture]")?.dataset.media;
|
|
255
|
+
const image = picture.querySelector("[data-picture-thumb]");
|
|
256
|
+
const key = picture.querySelector(`[name="${picture.dataset.key}"]`)?.value.trim();
|
|
257
|
+
if (!url || !image) return;
|
|
258
|
+
const found = key ? (await pictures(url)).get(key) : null;
|
|
259
|
+
image.hidden = !found;
|
|
260
|
+
if (found) {
|
|
261
|
+
image.addEventListener("error", () => { image.hidden = true; }, { once: true });
|
|
262
|
+
image.src = found;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Opens the picture picker for a card's picture and puts the choice in its key and alt text. */
|
|
267
|
+
async function pick(picture, list, afterwards) {
|
|
268
|
+
const button = picture.querySelector("[data-action=pick-picture]");
|
|
269
|
+
const key = picture.querySelector(`[name="${CSS.escape(picture.dataset.key)}"]`);
|
|
270
|
+
const alt = picture.querySelector(`[name="${CSS.escape(picture.dataset.alt)}"]`);
|
|
271
|
+
if (!loadPicker) return;
|
|
272
|
+
const { openPicker } = await loadPicker();
|
|
273
|
+
openPicker(picture, picture, {
|
|
274
|
+
url: button.dataset.media,
|
|
275
|
+
current: { mediaRef: key.value, alt: alt?.value ?? "" },
|
|
276
|
+
onPick: (chosen, text) => {
|
|
277
|
+
key.value = chosen;
|
|
278
|
+
if (alt) alt.value = text;
|
|
279
|
+
key.dispatchEvent(new Event("input", { bubbles: true }));
|
|
280
|
+
markAlt(picture, false);
|
|
281
|
+
known = null;
|
|
282
|
+
thumb(picture);
|
|
283
|
+
say(list, "Picture chosen.");
|
|
284
|
+
key.dispatchEvent(new Event("change", { bubbles: true }));
|
|
285
|
+
},
|
|
286
|
+
onClose: () => { button.focus(); afterwards(); },
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function enhance(list) {
|
|
291
|
+
const form = list.closest("form");
|
|
292
|
+
let draft = null;
|
|
293
|
+
let asking = null;
|
|
294
|
+
const dialog = list.querySelector(":scope > [data-remove-dialog]");
|
|
295
|
+
const touch = () => (draft ?? formDrafts.get(form))?.touch();
|
|
296
|
+
|
|
297
|
+
const refresh = (card) => {
|
|
298
|
+
const line = card.querySelector(":scope > [data-card-head] [data-card-summary]");
|
|
299
|
+
if (line) line.textContent = summaryOf(card);
|
|
300
|
+
nameTools(card);
|
|
301
|
+
for (const parent of [card.parentElement?.closest("[data-card]")]) if (parent) refresh(parent);
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const finish = (group, card, words) => {
|
|
305
|
+
renumber(group);
|
|
306
|
+
say(listOf(group), words);
|
|
307
|
+
touch();
|
|
308
|
+
return card;
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const add = (group, kind) => {
|
|
312
|
+
const max = Number(group.dataset.max ?? 0);
|
|
313
|
+
const noun = group.dataset.noun;
|
|
314
|
+
if (max > 0 && cardsOf(group).length >= max) return say(list, `At most ${max} ${noun}s fit here.`);
|
|
315
|
+
const card = create(group, kind);
|
|
316
|
+
if (!card) return null;
|
|
317
|
+
renumber(group);
|
|
318
|
+
toggle(card, true);
|
|
319
|
+
refresh(card);
|
|
320
|
+
card.querySelector("input:not([type=hidden]), select, textarea")?.focus();
|
|
321
|
+
card.scrollIntoView({ block: "nearest" });
|
|
322
|
+
return finish(group, card, `Added ${titleOf(card)}. ${cardsOf(group).length} in the list.`);
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const move = (card, step) => {
|
|
326
|
+
const group = groupOf(card);
|
|
327
|
+
const cards = cardsOf(group);
|
|
328
|
+
const at = cards.indexOf(card);
|
|
329
|
+
const to = at + step;
|
|
330
|
+
if (to < 0 || to >= cards.length) return say(list, `${titleOf(card)} is already ${step < 0 ? "first" : "last"}.`);
|
|
331
|
+
if (step < 0) cards[to].before(card);
|
|
332
|
+
else cards[to].after(card);
|
|
333
|
+
card.querySelector(`[data-action="${step < 0 ? "up" : "down"}"]`).focus();
|
|
334
|
+
finish(group, card, `Moved ${titleOf(card)} to position ${to + 1} of ${cards.length}.`);
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const remove = (card) => {
|
|
338
|
+
const group = groupOf(card);
|
|
339
|
+
const cards = cardsOf(group);
|
|
340
|
+
const at = cards.indexOf(card);
|
|
341
|
+
const words = titleOf(card);
|
|
342
|
+
const next = cards[at + 1] ?? cards[at - 1];
|
|
343
|
+
card.remove();
|
|
344
|
+
renumber(group);
|
|
345
|
+
const target = next?.querySelector(':scope > [data-card-head] [data-action="toggle"]') ?? group.querySelector(":scope > [data-adder] select, :scope > [data-action=add]");
|
|
346
|
+
target?.focus();
|
|
347
|
+
finish(group, card, `Removed ${words}. ${cardsOf(group).length} left in the list.`);
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
list.addEventListener("click", (event) => {
|
|
351
|
+
const button = event.target.closest("[data-action]");
|
|
352
|
+
if (!button || listOf(button) !== list) return;
|
|
353
|
+
const card = button.closest("[data-card]");
|
|
354
|
+
const action = button.dataset.action;
|
|
355
|
+
if (action === "toggle") return toggle(card);
|
|
356
|
+
if (action === "up") return move(card, -1);
|
|
357
|
+
if (action === "down") return move(card, 1);
|
|
358
|
+
if (action === "pick-picture") return pick(button.closest("[data-picture]"), list, () => {});
|
|
359
|
+
if (action === "remove") {
|
|
360
|
+
asking = { card, opener: button };
|
|
361
|
+
dialog.querySelector("[data-remove-title]").textContent = `Remove ${titleOf(card)}?`;
|
|
362
|
+
return dialog.showModal();
|
|
363
|
+
}
|
|
364
|
+
if (action === "remove-cancel") return dialog.close();
|
|
365
|
+
if (action === "remove-confirm") {
|
|
366
|
+
const chosen = asking;
|
|
367
|
+
asking = null;
|
|
368
|
+
dialog.close();
|
|
369
|
+
return remove(chosen.card);
|
|
370
|
+
}
|
|
371
|
+
if (action === "add") {
|
|
372
|
+
const group = button.closest("[data-group]");
|
|
373
|
+
const kind = group.querySelector(":scope > [data-adder] [data-add-kind]")?.value ?? "";
|
|
374
|
+
add(group, kind);
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
form?.addEventListener("input", (event) => {
|
|
379
|
+
const card = event.target.closest?.("[data-card]");
|
|
380
|
+
if (card) refresh(card);
|
|
381
|
+
const picture = event.target.closest?.("[data-picture]");
|
|
382
|
+
if (picture && event.target.name === picture.dataset.key) thumb(picture);
|
|
383
|
+
touch();
|
|
384
|
+
});
|
|
385
|
+
form?.addEventListener("change", touch);
|
|
386
|
+
form?.addEventListener("editor:changed", (event) => {
|
|
387
|
+
const card = event.target.closest?.("[data-card]");
|
|
388
|
+
if (card) refresh(card);
|
|
389
|
+
touch();
|
|
390
|
+
});
|
|
391
|
+
form?.addEventListener("focusout", (event) => {
|
|
392
|
+
const picture = event.target.closest?.("[data-picture]");
|
|
393
|
+
if (picture && event.target.name === picture.dataset.alt) markAlt(picture, undescribed(picture));
|
|
394
|
+
(draft ?? formDrafts.get(form))?.blur();
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
dialog.addEventListener("close", () => {
|
|
398
|
+
if (asking) asking.opener.focus();
|
|
399
|
+
asking = null;
|
|
400
|
+
});
|
|
401
|
+
for (const card of cardsOf(list)) dress(card);
|
|
402
|
+
for (const node of list.querySelectorAll(":scope > [data-adder]")) node.hidden = false;
|
|
403
|
+
for (const group of [list, ...list.querySelectorAll("[data-group]")]) renumber(group);
|
|
404
|
+
for (const card of list.querySelectorAll("[data-card]")) refresh(card);
|
|
405
|
+
for (const picture of list.querySelectorAll("[data-picture]")) thumb(picture);
|
|
406
|
+
for (const open of list.querySelectorAll("[data-card][data-invalid] > [data-card-panel], [data-card][data-open] > [data-card-panel]")) mountEditors(open);
|
|
407
|
+
|
|
408
|
+
if (list.hasAttribute("data-draft")) {
|
|
409
|
+
draft = startDraft(list, {
|
|
410
|
+
form,
|
|
411
|
+
blocked: () => {
|
|
412
|
+
const first = incomplete(list)[0];
|
|
413
|
+
return first ? `${titleOf(first)} needs more before it can be saved. Complete it, or use Save draft to see what the domain says.` : null;
|
|
414
|
+
},
|
|
415
|
+
refused: (problems) => reveal(list, problems),
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** Opens the cards a refusal names, and the cards that hold them, so what is wrong can be seen. */
|
|
421
|
+
function reveal(scope, problems) {
|
|
422
|
+
for (const problem of problems) {
|
|
423
|
+
let card = problem.name ? scope.querySelector(`#blk-${safe(problem.name)}`) : null;
|
|
424
|
+
while (card) {
|
|
425
|
+
toggle(card, true);
|
|
426
|
+
card = card.parentElement?.closest("[data-card]");
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** A form whose working copy is a value object saves as a whole, whichever part of it is touched. */
|
|
432
|
+
function enhanceForm(form) {
|
|
433
|
+
const draft = startDraft(form, {
|
|
434
|
+
form,
|
|
435
|
+
blocked: () => {
|
|
436
|
+
const first = incomplete(form)[0];
|
|
437
|
+
if (first) return `${titleOf(first)} needs more before it can be saved. Complete it, remove it, or use Save draft to see what the domain says.`;
|
|
438
|
+
return loose(form).some(undescribed) ? "A picture needs its description before it can be saved." : null;
|
|
439
|
+
},
|
|
440
|
+
refused: (problems) => reveal(form, problems),
|
|
441
|
+
});
|
|
442
|
+
formDrafts.set(form, draft);
|
|
443
|
+
form.addEventListener("input", () => draft.touch());
|
|
444
|
+
form.addEventListener("change", () => draft.touch());
|
|
445
|
+
form.addEventListener("editor:changed", () => draft.touch());
|
|
446
|
+
form.addEventListener("focusout", () => draft.blur());
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* A form is not sent while a picture names its file and has no description: the group says so beside
|
|
451
|
+
* the field, the card that holds it opens, and focus goes there. (The domain refuses the same, in its
|
|
452
|
+
* own words, when this is not in the way, as for a request that does not come from this page.)
|
|
453
|
+
*/
|
|
454
|
+
function guardPictures(form) {
|
|
455
|
+
form.addEventListener("submit", (event) => {
|
|
456
|
+
const missing = [...form.querySelectorAll("[data-picture]")].filter(undescribed);
|
|
457
|
+
if (missing.length === 0) return;
|
|
458
|
+
event.preventDefault();
|
|
459
|
+
event.stopImmediatePropagation();
|
|
460
|
+
for (const picture of missing) {
|
|
461
|
+
let card = picture.closest("[data-card]");
|
|
462
|
+
while (card) {
|
|
463
|
+
toggle(card, true);
|
|
464
|
+
card = card.parentElement?.closest("[data-card]");
|
|
465
|
+
}
|
|
466
|
+
markAlt(picture, true);
|
|
467
|
+
}
|
|
468
|
+
const first = missing[0].querySelector(`[name="${missing[0].dataset.alt}"]`);
|
|
469
|
+
first?.scrollIntoView({ block: "center" });
|
|
470
|
+
first?.focus({ preventScroll: true });
|
|
471
|
+
say(form, "A picture needs its description before it can be saved.");
|
|
472
|
+
}, true);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** A picture that is no card's (a slot of a value object) opens the picker from its own group. */
|
|
476
|
+
function enhanceLoose(form) {
|
|
477
|
+
const own = (picture) => picture && !picture.closest("[data-card]");
|
|
478
|
+
if (loose(form).length === 0) return;
|
|
479
|
+
for (const picture of loose(form)) thumb(picture);
|
|
480
|
+
form.addEventListener("click", (event) => {
|
|
481
|
+
const picture = event.target.closest?.("[data-action=pick-picture]")?.closest("[data-picture]");
|
|
482
|
+
if (own(picture)) pick(picture, form, () => {});
|
|
483
|
+
});
|
|
484
|
+
form.addEventListener("input", (event) => {
|
|
485
|
+
const picture = event.target.closest?.("[data-picture]");
|
|
486
|
+
if (own(picture) && event.target.name === picture.dataset.key) thumb(picture);
|
|
487
|
+
});
|
|
488
|
+
form.addEventListener("focusout", (event) => {
|
|
489
|
+
const picture = event.target.closest?.("[data-picture]");
|
|
490
|
+
if (own(picture) && event.target.name === picture.dataset.alt) markAlt(picture, undescribed(picture));
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export function enhanceBlocks() {
|
|
495
|
+
for (const form of document.querySelectorAll("form[data-draft-form]")) enhanceForm(form);
|
|
496
|
+
for (const list of document.querySelectorAll("[data-block-list]")) enhance(list);
|
|
497
|
+
for (const form of document.querySelectorAll("form")) enhanceLoose(form);
|
|
498
|
+
for (const form of document.querySelectorAll("form[data-form]")) guardPictures(form);
|
|
499
|
+
if (document.querySelector("[data-card][data-open], [data-card][data-invalid]")) firstProblem();
|
|
500
|
+
}
|