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,210 @@
|
|
|
1
|
+
__BANNER__
|
|
2
|
+
|
|
3
|
+
// What forms do with a script: repeatable rows that can be added, removed and moved, a question
|
|
4
|
+
// before a destructive command, a button that says it is busy while a form is sent, and focus on the
|
|
5
|
+
// first thing that needs fixing. A form posts the same without any of it.
|
|
6
|
+
|
|
7
|
+
import { mountPickers, unmountPickers } from "./pickers.js";
|
|
8
|
+
|
|
9
|
+
const icon = (name) => `<svg class="icon" aria-hidden="true" focusable="false"><use href="#i-${name}"/></svg>`;
|
|
10
|
+
|
|
11
|
+
const sanitize = (text) => text.replace(/[^A-Za-z0-9]+/g, "-");
|
|
12
|
+
|
|
13
|
+
/** A polite line for assistive technology, for changes that have no other way to be heard. */
|
|
14
|
+
function say(text) {
|
|
15
|
+
let line = document.querySelector("[data-say]");
|
|
16
|
+
if (!line) {
|
|
17
|
+
line = document.createElement("p");
|
|
18
|
+
line.className = "sr-only";
|
|
19
|
+
line.setAttribute("role", "status");
|
|
20
|
+
line.dataset.say = "";
|
|
21
|
+
document.body.append(line);
|
|
22
|
+
}
|
|
23
|
+
line.textContent = text;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Renames every name, id and path inside `root` that starts with `from` to start with `to` (ids and `for` use the sanitised spelling). */
|
|
27
|
+
function rename(root, from, to) {
|
|
28
|
+
const plain = (value) => (value.startsWith(from) ? to + value.slice(from.length) : value);
|
|
29
|
+
const safe = (value) => value.replace(sanitize(from), sanitize(to));
|
|
30
|
+
for (const element of root.querySelectorAll("[name], [data-path], [id], [for], [aria-describedby]")) {
|
|
31
|
+
if (element.hasAttribute("name")) element.setAttribute("name", plain(element.getAttribute("name")));
|
|
32
|
+
if (element.hasAttribute("data-path")) element.setAttribute("data-path", plain(element.getAttribute("data-path")));
|
|
33
|
+
for (const attribute of ["id", "for", "aria-describedby"]) if (element.hasAttribute(attribute)) element.setAttribute(attribute, safe(element.getAttribute(attribute)));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** One row's tools: move up, move down, remove. */
|
|
38
|
+
function tools(noun) {
|
|
39
|
+
const holder = document.createElement("div");
|
|
40
|
+
holder.className = "absolute -top-1 right-0 flex";
|
|
41
|
+
holder.dataset.tools = "";
|
|
42
|
+
const make = (action, glyph, text) => {
|
|
43
|
+
const button = document.createElement("button");
|
|
44
|
+
button.type = "button";
|
|
45
|
+
button.className = "btn btn-ghost btn-square btn-xs";
|
|
46
|
+
button.dataset.action = action;
|
|
47
|
+
button.setAttribute("aria-label", `${text} ${noun}`);
|
|
48
|
+
button.title = `${text} ${noun}`;
|
|
49
|
+
button.innerHTML = icon(glyph);
|
|
50
|
+
return button;
|
|
51
|
+
};
|
|
52
|
+
holder.append(make("up", "arrow-up", "Move up"), make("down", "arrow-down", "Move down"), make("remove", "trash", "Remove"));
|
|
53
|
+
return holder;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Numbers the rows again after one was added, removed or moved: names, ids, legends, and which tools apply. */
|
|
57
|
+
function renumber(repeat) {
|
|
58
|
+
const path = repeat.dataset.path;
|
|
59
|
+
const rows = [...repeat.querySelector("[data-rows]").children];
|
|
60
|
+
rows.forEach((row, at) => {
|
|
61
|
+
const was = row.dataset.index;
|
|
62
|
+
if (was !== String(at)) {
|
|
63
|
+
rename(row, `${path}.${was}.`, `${path}.${at}.`);
|
|
64
|
+
row.dataset.index = String(at);
|
|
65
|
+
}
|
|
66
|
+
const legend = row.querySelector(":scope > legend");
|
|
67
|
+
legend.textContent = legend.textContent.replace(/\d+$/, String(at + 1));
|
|
68
|
+
row.querySelector(':scope > [data-tools] [data-action="up"]').disabled = at === 0;
|
|
69
|
+
row.querySelector(':scope > [data-tools] [data-action="down"]').disabled = at === rows.length - 1;
|
|
70
|
+
});
|
|
71
|
+
const most = Number(repeat.dataset.max ?? 0);
|
|
72
|
+
repeat.querySelector(":scope > button.btn")?.setAttribute("aria-disabled", String(most > 0 && rows.length >= most));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** A copy of `row` with nothing filled in, to be the next row. */
|
|
76
|
+
function blank(row) {
|
|
77
|
+
const copy = row.cloneNode(true);
|
|
78
|
+
copy.querySelector(":scope > [data-tools]")?.remove();
|
|
79
|
+
unmountPickers(copy);
|
|
80
|
+
for (const input of copy.querySelectorAll("input, textarea")) {
|
|
81
|
+
if (input.type === "checkbox") input.checked = false;
|
|
82
|
+
else input.value = "";
|
|
83
|
+
if (input.dataset.initial !== undefined) input.dataset.initial = "";
|
|
84
|
+
input.removeAttribute("aria-invalid");
|
|
85
|
+
}
|
|
86
|
+
for (const select of copy.querySelectorAll("select")) {
|
|
87
|
+
select.selectedIndex = 0;
|
|
88
|
+
select.removeAttribute("aria-invalid");
|
|
89
|
+
}
|
|
90
|
+
for (const nested of copy.querySelectorAll("[data-rows]")) [...nested.children].slice(1).forEach((extra) => extra.remove());
|
|
91
|
+
// A run of rows inside the row is made live again for the copy: its buttons are new, not copied.
|
|
92
|
+
for (const nested of copy.querySelectorAll("[data-repeat]")) {
|
|
93
|
+
delete nested.dataset.dressed;
|
|
94
|
+
nested.querySelectorAll(":scope > button, [data-tools]").forEach((made) => made.remove());
|
|
95
|
+
}
|
|
96
|
+
return copy;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function repeating(repeat) {
|
|
100
|
+
const rows = repeat.querySelector("[data-rows]");
|
|
101
|
+
const noun = repeat.dataset.noun;
|
|
102
|
+
const dress = (row) => {
|
|
103
|
+
if (!row.querySelector(":scope > [data-tools]")) row.append(tools(noun));
|
|
104
|
+
for (const nested of row.querySelectorAll(":scope [data-repeat]")) if (!nested.dataset.dressed) repeating(nested);
|
|
105
|
+
};
|
|
106
|
+
repeat.dataset.dressed = "true";
|
|
107
|
+
[...rows.children].forEach(dress);
|
|
108
|
+
const add = document.createElement("button");
|
|
109
|
+
add.type = "button";
|
|
110
|
+
add.className = "btn btn-ghost btn-sm mt-1";
|
|
111
|
+
add.innerHTML = `${icon("plus")}<span>Add another ${noun}</span>`;
|
|
112
|
+
repeat.append(add);
|
|
113
|
+
add.addEventListener("click", () => {
|
|
114
|
+
const most = Number(repeat.dataset.max ?? 0);
|
|
115
|
+
if (most > 0 && rows.children.length >= most) return say(`At most ${most} ${noun}s fit here.`);
|
|
116
|
+
const last = rows.lastElementChild;
|
|
117
|
+
const copy = blank(last);
|
|
118
|
+
const at = rows.children.length;
|
|
119
|
+
rename(copy, `${repeat.dataset.path}.${last.dataset.index}.`, `${repeat.dataset.path}.${at}.`);
|
|
120
|
+
copy.dataset.index = String(at);
|
|
121
|
+
rows.append(copy);
|
|
122
|
+
dress(copy);
|
|
123
|
+
renumber(repeat);
|
|
124
|
+
mountPickers(copy);
|
|
125
|
+
copy.querySelector("input")?.focus();
|
|
126
|
+
say(`Added ${noun} ${at + 1}.`);
|
|
127
|
+
});
|
|
128
|
+
repeat.addEventListener("click", (event) => {
|
|
129
|
+
const button = event.target.closest("[data-tools] button");
|
|
130
|
+
if (!button || button.closest("[data-repeat]") !== repeat) return;
|
|
131
|
+
const row = button.closest("[data-row]");
|
|
132
|
+
const action = button.dataset.action;
|
|
133
|
+
if (action === "remove") return remove(repeat, rows, row, noun);
|
|
134
|
+
const target = action === "up" ? row.previousElementSibling : row.nextElementSibling;
|
|
135
|
+
if (!target) return;
|
|
136
|
+
if (action === "up") target.before(row);
|
|
137
|
+
else target.after(row);
|
|
138
|
+
renumber(repeat);
|
|
139
|
+
row.querySelector(`:scope > [data-tools] [data-action="${action}"]`).focus();
|
|
140
|
+
say(`Moved ${noun} ${[...rows.children].indexOf(row) + 1} of ${rows.children.length}.`);
|
|
141
|
+
});
|
|
142
|
+
renumber(repeat);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function remove(repeat, rows, row, noun) {
|
|
146
|
+
if (rows.children.length === 1) {
|
|
147
|
+
for (const input of row.querySelectorAll("input, textarea")) input.type === "checkbox" ? (input.checked = false) : (input.value = "");
|
|
148
|
+
for (const select of row.querySelectorAll("select")) select.selectedIndex = 0;
|
|
149
|
+
say(`Cleared ${noun}.`);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const next = row.nextElementSibling ?? row.previousElementSibling;
|
|
153
|
+
row.remove();
|
|
154
|
+
renumber(repeat);
|
|
155
|
+
next.querySelector("input, button")?.focus();
|
|
156
|
+
say(`Removed ${noun}.`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** A destructive command asks first: its button opens the dialog the page carries. */
|
|
160
|
+
function confirming() {
|
|
161
|
+
for (const trigger of document.querySelectorAll("[data-confirm]")) {
|
|
162
|
+
const dialog = document.getElementById(trigger.dataset.confirm);
|
|
163
|
+
if (!dialog || typeof dialog.showModal !== "function") continue;
|
|
164
|
+
trigger.addEventListener("click", (event) => {
|
|
165
|
+
event.preventDefault();
|
|
166
|
+
dialog.showModal();
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** A form that is on its way says so: the button is disabled and marked busy until the page changes. */
|
|
172
|
+
function busy() {
|
|
173
|
+
for (const form of document.querySelectorAll("form[data-form]")) {
|
|
174
|
+
form.addEventListener("submit", () => {
|
|
175
|
+
for (const button of form.querySelectorAll("button[data-busy]")) {
|
|
176
|
+
setTimeout(() => {
|
|
177
|
+
button.disabled = true;
|
|
178
|
+
button.setAttribute("aria-busy", "true");
|
|
179
|
+
}, 0);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
window.addEventListener("pageshow", () => {
|
|
184
|
+
for (const button of document.querySelectorAll("button[data-busy]")) {
|
|
185
|
+
button.disabled = false;
|
|
186
|
+
button.removeAttribute("aria-busy");
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** After a refusal, focus goes to the first field it names, or to the message. */
|
|
192
|
+
export function firstProblem() {
|
|
193
|
+
const target = document.querySelector('[aria-invalid="true"]') ?? document.querySelector("[data-refusal]");
|
|
194
|
+
if (!target) return;
|
|
195
|
+
target.scrollIntoView({ block: "center" });
|
|
196
|
+
const control = target.matches("input, select, textarea") ? target : target.querySelector("input, select, textarea") ?? target;
|
|
197
|
+
control.focus({ preventScroll: true });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Makes the runs of repeatable rows under `scope` live (a card made after the page loaded holds some). */
|
|
201
|
+
export function enhanceRows(scope) {
|
|
202
|
+
for (const repeat of scope.querySelectorAll("[data-repeat]")) if (!repeat.dataset.dressed) repeating(repeat);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function enhanceForms() {
|
|
206
|
+
enhanceRows(document);
|
|
207
|
+
confirming();
|
|
208
|
+
busy();
|
|
209
|
+
firstProblem();
|
|
210
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
__BANNER__
|
|
2
|
+
|
|
3
|
+
// What a list remembers. The filters a person last used on a list (its search words and its status)
|
|
4
|
+
// are kept for the browser tab in `sessionStorage`, per aggregate, and applied when the list is opened
|
|
5
|
+
// again with none named. Naming any of them, even to empty them, replaces what was kept; the Clear
|
|
6
|
+
// link forgets it. Storage can be blocked or full, so every use of it is allowed to fail and the list
|
|
7
|
+
// is then simply unfiltered.
|
|
8
|
+
|
|
9
|
+
const KEPT = ["_q", "_status"];
|
|
10
|
+
|
|
11
|
+
/** Whether the address names any filter or page of its own. */
|
|
12
|
+
const named = (params) => ["_q", "_status", "_page"].some((key) => params.has(key));
|
|
13
|
+
|
|
14
|
+
export function rememberFilters() {
|
|
15
|
+
const form = document.querySelector("form[data-list-filters]");
|
|
16
|
+
if (!form) return;
|
|
17
|
+
const key = `editor:list:${form.dataset.list}`;
|
|
18
|
+
const params = new URLSearchParams(window.location.search);
|
|
19
|
+
document.querySelector("[data-clear-filters]")?.addEventListener("click", () => {
|
|
20
|
+
try {
|
|
21
|
+
window.sessionStorage.removeItem(key);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
/* storage is blocked: nothing was kept to forget */
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
try {
|
|
27
|
+
if (params.has("_q") || params.has("_status")) {
|
|
28
|
+
const kept = Object.fromEntries(KEPT.map((name) => [name, params.get(name) ?? ""]));
|
|
29
|
+
if (KEPT.every((name) => kept[name] === "")) window.sessionStorage.removeItem(key);
|
|
30
|
+
else window.sessionStorage.setItem(key, JSON.stringify(kept));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const saved = named(params) ? null : JSON.parse(window.sessionStorage.getItem(key) ?? "null");
|
|
34
|
+
if (!saved) return;
|
|
35
|
+
for (const name of KEPT) if (typeof saved[name] === "string" && saved[name] !== "") params.set(name, saved[name]);
|
|
36
|
+
window.location.replace(`${window.location.pathname}?${params.toString()}`);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
/* storage is blocked or holds something that is not ours: the list stays as the address names it */
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
__BANNER__
|
|
2
|
+
|
|
3
|
+
// The editor's browser script, bundled by `npm run build` into dist/editor.js. Every page is
|
|
4
|
+
// complete HTML that works without it; this only adds behaviour. The rich-text editor is a larger
|
|
5
|
+
// piece of code, loaded only on a page that has a body to write.
|
|
6
|
+
import { enhanceBlocks } from "./blocks.js";
|
|
7
|
+
import { enhanceForms } from "./forms.js";
|
|
8
|
+
import { rememberFilters } from "./lists.js";
|
|
9
|
+
import { enhanceMoments } from "./moments.js";
|
|
10
|
+
import { enhancePickers } from "./pickers.js";
|
|
11
|
+
import { enhancePreview } from "./preview.js";
|
|
12
|
+
import { enhanceShell } from "./shell.js";
|
|
13
|
+
import { adoptToasts } from "./toast.js";
|
|
14
|
+
|
|
15
|
+
rememberFilters();
|
|
16
|
+
enhanceShell();
|
|
17
|
+
enhanceMoments();
|
|
18
|
+
enhancePickers();
|
|
19
|
+
enhanceForms();
|
|
20
|
+
enhanceBlocks();
|
|
21
|
+
enhancePreview();
|
|
22
|
+
adoptToasts();
|
|
23
|
+
|
|
24
|
+
if (document.querySelector("[data-body-editor]")) {
|
|
25
|
+
import("./body_editor.js").then((editor) => editor.mountAll());
|
|
26
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
__BANNER__
|
|
2
|
+
|
|
3
|
+
// The picture picker the writing surface opens for an image block. Pictures are dropped or chosen,
|
|
4
|
+
// each with the alt text the domain requires, uploaded with a progress bar and a message of its
|
|
5
|
+
// own when it is refused, and then shown beside the pictures the domain already holds, as a grid of
|
|
6
|
+
// thumbnails (they come from the storage adapter's own addresses). A picture is chosen, given alt
|
|
7
|
+
// text for this place in the text and an optional caption, and handed to the editor as a media key.
|
|
8
|
+
// Everything the server sends is put into the page as text, never as markup.
|
|
9
|
+
const icon = (name) => `<svg class="icon" aria-hidden="true" focusable="false"><use href="#i-${name}"/></svg>`;
|
|
10
|
+
|
|
11
|
+
const make = (tag, props = {}, ...children) => {
|
|
12
|
+
const element = Object.assign(document.createElement(tag), props);
|
|
13
|
+
element.append(...children);
|
|
14
|
+
return element;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const ALLOWED = "image/jpeg,image/png,image/webp,image/gif,image/avif";
|
|
18
|
+
|
|
19
|
+
async function getPictures(url) {
|
|
20
|
+
const response = await fetch(url, { headers: { Accept: "application/json" } });
|
|
21
|
+
if (!response.ok) throw new Error((await response.json().catch(() => ({}))).error ?? "The pictures could not be listed.");
|
|
22
|
+
return (await response.json()).pictures;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Uploads one picture, reporting progress as a fraction; resolves with the picture or rejects with the words to show. */
|
|
26
|
+
function upload(url, file, alt, progress) {
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
const request = new XMLHttpRequest();
|
|
29
|
+
request.open("POST", url);
|
|
30
|
+
request.setRequestHeader("Accept", "application/json");
|
|
31
|
+
request.upload.addEventListener("progress", (event) => event.lengthComputable && progress(event.loaded / event.total));
|
|
32
|
+
request.addEventListener("load", () => {
|
|
33
|
+
let answer = {};
|
|
34
|
+
try { answer = JSON.parse(request.responseText); } catch (error) { /* not JSON */ }
|
|
35
|
+
if (request.status >= 200 && request.status < 300) resolve(answer);
|
|
36
|
+
else reject(new Error(answer.error ?? "The upload failed."));
|
|
37
|
+
});
|
|
38
|
+
request.addEventListener("error", () => reject(new Error("The upload could not be sent. Check the connection and try again.")));
|
|
39
|
+
const form = new FormData();
|
|
40
|
+
form.append("alt", alt);
|
|
41
|
+
form.append("file", file);
|
|
42
|
+
request.send(form);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** One registered picture as a button: its thumbnail and its alt text. */
|
|
47
|
+
function entry(picture, choose) {
|
|
48
|
+
const image = make("img", { src: picture.url, alt: "", loading: "lazy", width: 88, height: 88, className: "aspect-square w-full rounded object-cover" });
|
|
49
|
+
const button = make("button", { type: "button", title: picture.alt, className: "grid w-full gap-1 rounded-field border border-transparent p-1 text-left text-xs hover:bg-base-200" }, image, make("span", { className: "truncate", textContent: picture.alt || picture.key.slice(0, 12) }));
|
|
50
|
+
button.dataset.key = picture.key;
|
|
51
|
+
button.addEventListener("click", () => choose(picture, button));
|
|
52
|
+
return make("li", {}, button);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Opens the picker inside `frame`, below `bar`. `options.url` is the media endpoint,
|
|
57
|
+
* `options.current` the picture being replaced (or null), `options.onPick(key, alt, caption)` is called
|
|
58
|
+
* with the choice and `options.onClose()` when the picker goes away.
|
|
59
|
+
*/
|
|
60
|
+
export function openPicker(frame, bar, options) {
|
|
61
|
+
frame.querySelector("[data-picker]")?.remove();
|
|
62
|
+
const { url, current, onPick, onClose } = options;
|
|
63
|
+
let chosen = current?.mediaRef || null;
|
|
64
|
+
const staged = [];
|
|
65
|
+
|
|
66
|
+
const grid = make("ul", { className: "grid max-h-56 grid-cols-[repeat(auto-fill,minmax(5.5rem,1fr))] gap-2 overflow-auto" });
|
|
67
|
+
grid.setAttribute("aria-label", "Pictures already registered");
|
|
68
|
+
const list = make("ul", { className: "grid gap-1" });
|
|
69
|
+
const problem = make("div", { className: "alert alert-error alert-soft", hidden: true });
|
|
70
|
+
problem.setAttribute("role", "alert");
|
|
71
|
+
const files = make("input", { type: "file", accept: ALLOWED, multiple: true, className: "file-input file-input-sm w-full max-w-xs" });
|
|
72
|
+
files.setAttribute("aria-label", "Choose pictures to upload");
|
|
73
|
+
const zone = make("div", { className: "grid justify-items-center gap-2 rounded-field border-2 border-dashed border-edge p-5 text-center text-muted" },
|
|
74
|
+
make("span", { innerHTML: icon("upload") }), make("p", { textContent: "Drop pictures here, or choose them. JPEG, PNG, WebP, GIF or AVIF." }), files);
|
|
75
|
+
const upButton = make("button", { type: "button", className: "btn btn-sm", hidden: true });
|
|
76
|
+
const alt = make("input", { type: "text", className: "input w-full", required: true, value: current?.alt ?? "" });
|
|
77
|
+
alt.setAttribute("aria-required", "true");
|
|
78
|
+
const caption = make("input", { type: "text", className: "input w-full", value: current?.caption ?? "" });
|
|
79
|
+
const insert = make("button", { type: "button", className: "btn btn-neutral" }, "Insert");
|
|
80
|
+
const cancel = make("button", { type: "button", className: "btn btn-ghost" }, "Cancel");
|
|
81
|
+
const close = make("button", { type: "button", className: "btn btn-ghost btn-square btn-sm", innerHTML: icon("close") });
|
|
82
|
+
close.setAttribute("aria-label", "Close the picker");
|
|
83
|
+
const field = (text, input) => make("label", { className: "grid gap-1 font-semibold" }, text, input);
|
|
84
|
+
const panel = make("div", { className: "picker-panel fixed inset-x-2 top-16 z-20 mx-auto grid max-w-2xl gap-4 overflow-y-auto overscroll-contain rounded-box border border-base-300 bg-base-100 p-4 shadow-xl" },
|
|
85
|
+
make("div", { className: "flex items-center justify-between" }, make("h3", { className: "text-lg", textContent: "Insert a picture" }), close),
|
|
86
|
+
zone, list, upButton, make("p", { className: "text-sm text-muted", textContent: "Or choose a picture already registered:" }), grid,
|
|
87
|
+
field("Alt text for this picture (required)", alt), field("Caption (optional)", caption), problem, make("div", { className: "flex gap-2" }, insert, cancel));
|
|
88
|
+
panel.dataset.picker = "";
|
|
89
|
+
panel.setAttribute("role", "dialog");
|
|
90
|
+
panel.setAttribute("aria-label", "Insert a picture");
|
|
91
|
+
// The panel stands in the window, between the app bar and the page's sticky action bar (when it has
|
|
92
|
+
// one), and scrolls inside itself; it never grows past what the window shows.
|
|
93
|
+
const fit = () => {
|
|
94
|
+
const actions = document.querySelector("[data-actionbar]");
|
|
95
|
+
const floor = actions ? Math.max(0, window.innerHeight - actions.getBoundingClientRect().top) : 0;
|
|
96
|
+
const top = Math.max(panel.getBoundingClientRect().top, 0);
|
|
97
|
+
panel.style.bottom = "auto";
|
|
98
|
+
panel.style.maxHeight = `calc(${window.visualViewport?.height ?? window.innerHeight}px - ${top}px - ${floor}px - 0.5rem - env(safe-area-inset-bottom, 0px))`;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const say = (message) => {
|
|
102
|
+
problem.textContent = message ?? "";
|
|
103
|
+
problem.hidden = !message;
|
|
104
|
+
};
|
|
105
|
+
const shut = () => { window.removeEventListener("resize", fit); window.visualViewport?.removeEventListener("resize", fit); panel.remove(); onClose?.(); };
|
|
106
|
+
|
|
107
|
+
const choose = (picture, button) => {
|
|
108
|
+
chosen = picture.key;
|
|
109
|
+
if (!alt.value.trim()) alt.value = picture.alt;
|
|
110
|
+
for (const other of grid.querySelectorAll("button")) {
|
|
111
|
+
other.removeAttribute("aria-pressed");
|
|
112
|
+
other.classList.remove("ring-2", "ring-primary");
|
|
113
|
+
}
|
|
114
|
+
button.setAttribute("aria-pressed", "true");
|
|
115
|
+
button.classList.add("ring-2", "ring-primary");
|
|
116
|
+
say(null);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const stage = (file) => {
|
|
120
|
+
const preview = make("img", { src: URL.createObjectURL(file), alt: "", className: "size-14 rounded object-cover" });
|
|
121
|
+
const text = make("input", { type: "text", className: "input input-sm w-full", placeholder: "Alt text (required)" });
|
|
122
|
+
text.setAttribute("aria-label", `Alt text for ${file.name}`);
|
|
123
|
+
text.setAttribute("aria-required", "true");
|
|
124
|
+
const bar = make("progress", { className: "progress progress-primary w-full", max: 1, value: 0, hidden: true });
|
|
125
|
+
const result = make("p", { className: "text-xs" });
|
|
126
|
+
const drop = make("button", { type: "button", className: "btn btn-ghost btn-square btn-xs", innerHTML: icon("close") });
|
|
127
|
+
drop.setAttribute("aria-label", `Remove ${file.name}`);
|
|
128
|
+
const row = make("li", { className: "grid grid-cols-[3.5rem_minmax(0,1fr)_auto] items-start gap-3 border-t border-base-300 py-2" },
|
|
129
|
+
preview, make("div", { className: "grid gap-1" }, make("p", { className: "truncate text-xs text-muted", textContent: file.name }), text, bar, result), drop);
|
|
130
|
+
const item = { file, row, text, bar, result, done: false };
|
|
131
|
+
drop.addEventListener("click", () => { staged.splice(staged.indexOf(item), 1); URL.revokeObjectURL(preview.src); row.remove(); refresh(); });
|
|
132
|
+
staged.push(item);
|
|
133
|
+
list.append(row);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const refresh = () => {
|
|
137
|
+
const pending = staged.filter((item) => !item.done);
|
|
138
|
+
upButton.hidden = pending.length === 0;
|
|
139
|
+
upButton.textContent = pending.length === 1 ? "Upload this picture" : `Upload ${pending.length} pictures`;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const add = (picked) => {
|
|
143
|
+
for (const file of picked) {
|
|
144
|
+
if (!ALLOWED.split(",").includes(file.type)) {
|
|
145
|
+
say(`${file.name} is not a JPEG, PNG, WebP, GIF or AVIF picture.`);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
stage(file);
|
|
149
|
+
}
|
|
150
|
+
refresh();
|
|
151
|
+
staged.at(-1)?.text.focus();
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const send = async () => {
|
|
155
|
+
say(null);
|
|
156
|
+
for (const item of staged.filter((entryItem) => !entryItem.done)) {
|
|
157
|
+
const words = item.text.value.trim();
|
|
158
|
+
if (!words) {
|
|
159
|
+
item.result.textContent = "Add alt text to upload this picture.";
|
|
160
|
+
item.result.className = "text-xs font-semibold text-error";
|
|
161
|
+
item.text.setAttribute("aria-invalid", "true");
|
|
162
|
+
item.text.focus();
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
item.text.removeAttribute("aria-invalid");
|
|
166
|
+
item.bar.hidden = false;
|
|
167
|
+
item.result.textContent = "";
|
|
168
|
+
try {
|
|
169
|
+
const made = await upload(url, item.file, words, (fraction) => { item.bar.value = fraction; });
|
|
170
|
+
item.done = true;
|
|
171
|
+
item.bar.value = 1;
|
|
172
|
+
item.result.textContent = "Uploaded.";
|
|
173
|
+
item.result.className = "text-xs font-semibold text-success";
|
|
174
|
+
item.text.disabled = true;
|
|
175
|
+
const picture = { key: made.key, alt: made.alt ?? words, url: made.url ?? `${url}/${encodeURIComponent(made.key)}` };
|
|
176
|
+
const node = entry(picture, choose);
|
|
177
|
+
grid.prepend(node);
|
|
178
|
+
choose(picture, node.firstChild);
|
|
179
|
+
} catch (err) {
|
|
180
|
+
item.bar.hidden = true;
|
|
181
|
+
item.result.textContent = err.message;
|
|
182
|
+
item.result.className = "text-xs font-semibold text-error";
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
refresh();
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const submit = () => {
|
|
189
|
+
say(null);
|
|
190
|
+
if (!chosen) return say("Choose a picture, or upload one first.");
|
|
191
|
+
if (!alt.value.trim()) return say("Add alt text: describe the picture for someone who cannot see it.");
|
|
192
|
+
onPick(chosen, alt.value.trim(), caption.value.trim());
|
|
193
|
+
shut();
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
files.addEventListener("change", () => { add([...files.files]); files.value = ""; });
|
|
197
|
+
zone.addEventListener("dragover", (event) => { event.preventDefault(); zone.classList.add("border-primary", "bg-soft", "text-primary"); });
|
|
198
|
+
zone.addEventListener("dragleave", () => zone.classList.remove("border-primary", "bg-soft", "text-primary"));
|
|
199
|
+
zone.addEventListener("drop", (event) => {
|
|
200
|
+
event.preventDefault();
|
|
201
|
+
zone.classList.remove("border-primary", "bg-soft", "text-primary");
|
|
202
|
+
add([...event.dataTransfer.files]);
|
|
203
|
+
});
|
|
204
|
+
upButton.addEventListener("click", send);
|
|
205
|
+
insert.addEventListener("click", submit);
|
|
206
|
+
cancel.addEventListener("click", shut);
|
|
207
|
+
close.addEventListener("click", shut);
|
|
208
|
+
panel.addEventListener("keydown", (event) => {
|
|
209
|
+
if (event.key === "Escape") { event.preventDefault(); shut(); }
|
|
210
|
+
if (event.key === "Enter" && event.target.tagName === "INPUT" && event.target.type !== "file") {
|
|
211
|
+
event.preventDefault();
|
|
212
|
+
if (staged.includes(staged.find((item) => item.text === event.target))) send();
|
|
213
|
+
else submit();
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
frame.append(panel);
|
|
218
|
+
fit();
|
|
219
|
+
window.addEventListener("resize", fit);
|
|
220
|
+
window.visualViewport?.addEventListener("resize", fit);
|
|
221
|
+
getPictures(url)
|
|
222
|
+
.then((pictures) => {
|
|
223
|
+
for (const picture of pictures) {
|
|
224
|
+
const node = entry(picture, choose);
|
|
225
|
+
grid.append(node);
|
|
226
|
+
if (picture.key === chosen) choose(picture, node.firstChild);
|
|
227
|
+
}
|
|
228
|
+
if (pictures.length === 0) grid.replaceWith(make("p", { className: "text-sm text-muted", textContent: "No pictures yet." }));
|
|
229
|
+
})
|
|
230
|
+
.catch((err) => say(err.message));
|
|
231
|
+
files.focus();
|
|
232
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
__BANNER__
|
|
2
|
+
|
|
3
|
+
// What a script does with moments (see ui/moments.ts and browser/epoch.js). A moment shown read-only
|
|
4
|
+
// is restated in the viewer's own zone, with how far away it is. A moment in a form is a native date
|
|
5
|
+
// or date-and-time input in that zone; the seconds the domain holds stay in the plain input the form
|
|
6
|
+
// posts, which is hidden, and are rewritten only when the person changes the date, so a moment that
|
|
7
|
+
// is not edited is sent back exactly as it came.
|
|
8
|
+
import { formatMoment, fromLocal, relative, toLocal, zoneLabel } from "./epoch.js";
|
|
9
|
+
|
|
10
|
+
/** Restates every `time` the page carries in the viewer's zone, followed by how far from now it is. */
|
|
11
|
+
function restate() {
|
|
12
|
+
const now = Math.floor(Date.now() / 1000);
|
|
13
|
+
for (const element of document.querySelectorAll("time[data-moment]")) {
|
|
14
|
+
const seconds = Number(element.dataset.epoch);
|
|
15
|
+
const mode = element.dataset.moment;
|
|
16
|
+
element.textContent = formatMoment(seconds, mode);
|
|
17
|
+
element.title = zoneLabel(seconds);
|
|
18
|
+
const ago = document.createElement("span");
|
|
19
|
+
ago.className = "ml-1 text-muted";
|
|
20
|
+
ago.textContent = `(${relative(seconds, now, mode)})`;
|
|
21
|
+
element.after(ago);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The note under a field: which zone the date is in. */
|
|
26
|
+
function note(field, seconds) {
|
|
27
|
+
const text = field.querySelector("[data-epoch-note]");
|
|
28
|
+
if (text) text.textContent = `Shown in your time zone, ${zoneLabel(seconds ?? Math.floor(Date.now() / 1000))}.`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Swaps a number input of seconds for a native date input in the viewer's zone. */
|
|
32
|
+
function mount(field) {
|
|
33
|
+
const raw = field.querySelector("[data-epoch-raw]");
|
|
34
|
+
if (!raw || field.dataset.mounted) return;
|
|
35
|
+
const mode = field.dataset.momentField;
|
|
36
|
+
const local = document.createElement("input");
|
|
37
|
+
local.type = mode === "date" ? "date" : "datetime-local";
|
|
38
|
+
local.className = raw.classList.contains("input-error") ? "input input-error w-full max-w-xl" : "input w-full max-w-xl";
|
|
39
|
+
local.id = raw.id;
|
|
40
|
+
for (const name of ["aria-required", "aria-invalid", "aria-describedby"]) {
|
|
41
|
+
if (raw.hasAttribute(name)) local.setAttribute(name, raw.getAttribute(name));
|
|
42
|
+
raw.removeAttribute(name);
|
|
43
|
+
}
|
|
44
|
+
raw.removeAttribute("id");
|
|
45
|
+
const held = raw.value === "" ? null : Number(raw.value);
|
|
46
|
+
if (held !== null && Number.isFinite(held)) local.value = toLocal(held, mode);
|
|
47
|
+
raw.type = "hidden";
|
|
48
|
+
raw.before(local);
|
|
49
|
+
field.dataset.mounted = "true";
|
|
50
|
+
note(field, held);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The seconds follow the input as it is edited; a cleared or incomplete input leaves the field empty. */
|
|
54
|
+
function follow(event) {
|
|
55
|
+
const local = event.target;
|
|
56
|
+
const field = local.closest?.("[data-moment-field]");
|
|
57
|
+
if (!field || local.matches("[data-epoch-raw]")) return;
|
|
58
|
+
const raw = field.querySelector("[data-epoch-raw]");
|
|
59
|
+
const seconds = fromLocal(local.value, field.dataset.momentField);
|
|
60
|
+
raw.value = seconds === null ? "" : String(seconds);
|
|
61
|
+
note(field, seconds);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Mounts the date inputs under `scope` that are not yet (a card made after the page loaded holds some). */
|
|
65
|
+
export function mountMoments(scope) {
|
|
66
|
+
for (const field of scope.querySelectorAll("[data-moment-field]")) mount(field);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function enhanceMoments() {
|
|
70
|
+
restate();
|
|
71
|
+
for (const field of document.querySelectorAll("[data-moment-field]")) mount(field);
|
|
72
|
+
document.addEventListener("input", follow);
|
|
73
|
+
}
|