hecks 3.10.0 → 3.12.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 +158 -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 +70 -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/newcomer.rb +50 -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/read_models.rb +1 -1
- 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 +4 -15
- data/lib/hecks/bluebook/assembly/scalars.rb +39 -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 +24 -1
- data/lib/hecks/bluebook/meta_validator/reconstruction/heads.rb +2 -1
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +6 -3
- 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 +2 -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 +34 -4
- 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 +33 -7
- 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 +133 -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 +13 -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.behaviors +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.behaviors +19 -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 +63 -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/projection_files/expression_tables.rb +7 -2
- data/lib/hecks/projections/deploy/box/compose.rb +6 -4
- data/lib/hecks/projections/deploy/box/edge.rb +62 -0
- data/lib/hecks/projections/deploy/box/logging.rb +99 -0
- data/lib/hecks/projections/deploy/box/roll_scripts.rb +16 -8
- data/lib/hecks/projections/deploy/box/settings/cdn.rb +40 -0
- data/lib/hecks/projections/deploy/box/settings/logs.rb +40 -0
- data/lib/hecks/projections/deploy/box/settings/patterns.rb +11 -0
- data/lib/hecks/projections/deploy/box/settings/types.rb +16 -1
- data/lib/hecks/projections/deploy/box/settings.rb +6 -2
- data/lib/hecks/projections/deploy/box/stack_yaml.rb +5 -2
- data/lib/hecks/projections/deploy/box/templates/Makefile.tmpl +16 -0
- data/lib/hecks/projections/deploy/box/templates/box.yaml.tmpl +8 -4
- data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +32 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-service.sh.tmpl +1 -1
- data/lib/hecks/projections/deploy/box/templates/edge.mk.tmpl +17 -0
- data/lib/hecks/projections/deploy/box/templates/edge.yaml.tmpl +65 -0
- data/lib/hecks/projections/deploy/box/templates/log-group.yaml.tmpl +12 -0
- data/lib/hecks/projections/deploy/box/templates/log-policy.yaml.tmpl +7 -0
- data/lib/hecks/projections/deploy/box/templates/render-compose-taskdef.sh.tmpl +5 -4
- data/lib/hecks/projections/deploy/box/templates/render-compose.sh.tmpl +5 -4
- data/lib/hecks/projections/deploy/box.rb +15 -3
- 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/projections/site/site_root/workflow.rb +5 -0
- data/lib/hecks/projector/cli_audience.rb +22 -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/cli_projector/usage.rb +15 -4
- 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/host.rb +11 -4
- data/lib/hecks/rust_build/prebuilt_binary.rb +101 -0
- 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/ci_gate_decision.rb +35 -0
- data/lib/hecks/tools/ci_gates.rb +20 -1
- 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/files.rb +8 -0
- data/lib/hecks/tools/lanes/prebuilt_workflow.rb +81 -0
- data/lib/hecks/tools/lanes/rows.rb +6 -0
- data/lib/hecks/tools/lanes.rb +1 -0
- data/lib/hecks/tools/oidc_manifests.rb +4 -5
- data/lib/hecks/tools/regeneration_run.rb +11 -4
- data/lib/hecks/tools/release_only.rb +111 -0
- 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 +19 -5
- 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/9ee54e2f0d4d6d3fe046827779b221e02de665fba911a048ae5dd8b8fbfb7d90.marshal +0 -0
- data/prebuilt/d74a00e996222c1d8b676d5959092effc46b4dc59e0555f3bf9554ac58e3094a.marshal +0 -0
- data/prebuilt/verdicts-df19f35868d3284a70f55e42dcef87ef4e65e6a678e5135f72e1f6cff4c1357b.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/extension.rs +13 -1
- data/rust/host/src/lib.rs +1 -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/platform_signin/attack_tests.rs +1100 -0
- data/rust/host/src/platform_signin/config.rs +512 -0
- data/rust/host/src/platform_signin/cookie.rs +237 -0
- data/rust/host/src/platform_signin/extension.rs +190 -0
- data/rust/host/src/platform_signin/fetcher.rs +322 -0
- data/rust/host/src/platform_signin/handler.rs +251 -0
- data/rust/host/src/platform_signin/handler_tests.rs +554 -0
- data/rust/host/src/platform_signin/jti.rs +196 -0
- data/rust/host/src/platform_signin/jwks.rs +542 -0
- data/rust/host/src/platform_signin/jws.rs +249 -0
- data/rust/host/src/platform_signin/known_answer_tests.rs +237 -0
- data/rust/host/src/platform_signin/mapping_tests.rs +254 -0
- data/rust/host/src/platform_signin/mod.rs +75 -0
- data/rust/host/src/platform_signin/mode_tests.rs +382 -0
- data/rust/host/src/platform_signin/pages.rs +204 -0
- data/rust/host/src/platform_signin/query.rs +233 -0
- data/rust/host/src/platform_signin/session.rs +151 -0
- data/rust/host/src/platform_signin/settings.rs +385 -0
- data/rust/host/src/platform_signin/strict_json.rs +351 -0
- data/rust/host/src/platform_signin/test_support.rs +235 -0
- data/rust/host/src/platform_signin/ttl_tests.rs +94 -0
- data/rust/host/src/platform_signin/verify.rs +604 -0
- data/rust/host/src/platform_signin/wiring_support.rs +319 -0
- 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 +53 -9
- 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 +34 -17
- 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 +178 -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,43 @@
|
|
|
1
|
+
__BANNER__
|
|
2
|
+
|
|
3
|
+
// The question a command asks before it runs, as a native `<dialog>`: a destructive command (cancel,
|
|
4
|
+
// discard, withdraw) and a command that publishes. The button that asks carries `data-confirm` with
|
|
5
|
+
// the dialog's id; the page's script opens it, and a browser with no script follows the button's link
|
|
6
|
+
// to a page that asks the same question.
|
|
7
|
+
import type { Aggregate, Command } from "../schema.ts";
|
|
8
|
+
import { esc } from "./html.ts";
|
|
9
|
+
import { backInput } from "./links.ts";
|
|
10
|
+
import type { Back } from "./links.ts";
|
|
11
|
+
import { commandLabel } from "./words.ts";
|
|
12
|
+
|
|
13
|
+
/** What makes one dialog differ from another on a page that holds several. */
|
|
14
|
+
export interface Asking {
|
|
15
|
+
/** Told apart when one command is asked about on several rows. */
|
|
16
|
+
suffix?: string;
|
|
17
|
+
/** Where a person is sent once the command has run. */
|
|
18
|
+
back?: Back | null;
|
|
19
|
+
/** The button's colour: `error` for something that cannot be undone, `neutral` for the rest. */
|
|
20
|
+
tone?: "error" | "neutral";
|
|
21
|
+
/** The words for the button, when they are not the command's own. */
|
|
22
|
+
button?: string;
|
|
23
|
+
/** Whether a draft still being written is saved first (see browser/autosave.js). */
|
|
24
|
+
flush?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The id of a command's dialog, with the suffix that tells rows apart. */
|
|
28
|
+
export const dialogId = (command: Command, suffix = ""): string => `confirm-${command.name}${suffix ? `-${suffix}` : ""}`;
|
|
29
|
+
|
|
30
|
+
/** The question a command asks, as a native dialog that posts to `action` when confirmed. */
|
|
31
|
+
export function confirmDialog(agg: Aggregate, command: Command, action: string, id: string, form?: string, asking: Asking = {}): string {
|
|
32
|
+
const dialog = dialogId(command, asking.suffix);
|
|
33
|
+
const what = esc(asking.button ?? commandLabel(agg, command));
|
|
34
|
+
const detail = command.goal ? `${esc(command.goal)}.` : `This applies to ${esc(id)}.`;
|
|
35
|
+
const owner = form ? ` form="${esc(form)}"` : "";
|
|
36
|
+
const tone = asking.tone === "neutral" ? "btn btn-neutral" : "btn btn-error";
|
|
37
|
+
const head = `<h2 id="${dialog}-title" class="text-xl">${what}?</h2>\n<p class="mt-2 text-muted">${detail}</p>`;
|
|
38
|
+
const dismiss = what === "Cancel" ? "Go back" : "Cancel";
|
|
39
|
+
const buttons = `<div class="modal-action"><button type="submit" class="btn btn-ghost" formmethod="dialog" formnovalidate>${dismiss}</button><button type="submit" class="${tone}"${owner}>${what}</button></div>`;
|
|
40
|
+
const box = form ? `${head}\n${buttons}` : `<form method="post" action="${esc(action)}"${asking.flush ? " data-flush-draft" : ""}>\n${backInput(asking.back ?? null)}${head}\n${buttons}\n</form>`;
|
|
41
|
+
const close = `<form method="dialog" class="modal-backdrop"><button aria-label="Close">close</button></form>`;
|
|
42
|
+
return `<dialog class="modal" id="${dialog}" aria-labelledby="${dialog}-title">\n<div class="modal-box">\n${box}\n</div>\n${close}\n</dialog>`;
|
|
43
|
+
}
|
|
@@ -1,80 +1,275 @@
|
|
|
1
1
|
__BANNER__
|
|
2
2
|
|
|
3
|
-
// The inputs of a form, derived from attributes. A value
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// The inputs of a form, derived from attributes. A value object is a group of its parts (one with a
|
|
4
|
+
// single part is just that input, named for the attribute); a `list_of` attribute is a run of
|
|
5
|
+
// repeatable rows (the rows it holds, then one blank row to fill, which the page's script lets the
|
|
6
|
+
// person add to, remove and reorder); a lifecycle state is not an input at all. Each input is named
|
|
7
|
+
// by its dotted path (`byline.name`, `body.blocks.0.text`), which input.ts reads back into the shape
|
|
8
|
+
// the command takes. Inputs are plain `input` elements, so a form posts with no script at all. A
|
|
9
|
+
// text a rule bounds above 200 characters is a writing box, a value object with a media key and a
|
|
10
|
+
// description is a picture (its picker opens from the group), and a list whose members hold a body,
|
|
11
|
+
// a picture or a list of their own is drawn as cards (see blocks.ts) so a new member is made from a
|
|
12
|
+
// template, not copied from the page.
|
|
7
13
|
import type { Aggregate, Attr } from "../schema.ts";
|
|
8
14
|
import { SCHEMA } from "../schema.ts";
|
|
9
15
|
import { esc, href } from "./html.ts";
|
|
16
|
+
import { icon } from "./icons.ts";
|
|
10
17
|
import { bodyToFields, bodyToHtml, emptyBody, fieldsToInputs } from "./body_model.js";
|
|
18
|
+
import { heavy } from "./block_list.js";
|
|
19
|
+
import { blockList } from "./blocks.ts";
|
|
20
|
+
import type { Problem } from "./blocks.ts";
|
|
21
|
+
import { isMoment, secondsOf } from "./moments.ts";
|
|
22
|
+
import { datalists, pickerControl } from "./pickers.ts";
|
|
23
|
+
import type { Choices } from "./pickers.ts";
|
|
24
|
+
import { label, limitsNote, singular } from "./words.ts";
|
|
11
25
|
|
|
12
26
|
type Value = unknown;
|
|
13
27
|
|
|
14
|
-
|
|
28
|
+
/** The field a refusal named and what it said, so the form can mark it and the script can focus it. */
|
|
29
|
+
export interface Invalid {
|
|
30
|
+
field: string;
|
|
31
|
+
message: string;
|
|
32
|
+
}
|
|
15
33
|
|
|
16
|
-
|
|
34
|
+
/** What a run of inputs needs to know beyond the attribute: whether to mark required, which field is invalid. */
|
|
35
|
+
interface Context {
|
|
36
|
+
mark: boolean;
|
|
37
|
+
invalid?: Invalid | null;
|
|
38
|
+
top: boolean;
|
|
39
|
+
/** The row's own heading already names the input, so its label is for assistive technology only. */
|
|
40
|
+
quiet?: boolean;
|
|
41
|
+
/** What is on offer for each picker (see pickers.ts); a picker with nothing on offer is a text box. */
|
|
42
|
+
choices?: Choices;
|
|
43
|
+
/** The body attribute that is written as a draft: its editor saves by itself (see browser/autosave.js). */
|
|
44
|
+
draft?: string;
|
|
45
|
+
/** The blocks of a list of mixed blocks that a refusal named (see blocks.ts). */
|
|
46
|
+
problems?: Problem[];
|
|
47
|
+
/** A list is drawn as plain rows, whatever its members hold: it is inside a card's group, one level in. */
|
|
48
|
+
rows?: boolean;
|
|
49
|
+
}
|
|
17
50
|
|
|
18
51
|
const inputType = (attr: Attr): string => (attr.kind === "integer" || attr.kind === "number" ? "number" : "text");
|
|
19
52
|
|
|
20
53
|
const member = (value: Value, key: string): Value => (value && typeof value === "object" ? (value as Record<string, Value>)[key] : undefined);
|
|
21
54
|
|
|
22
|
-
|
|
55
|
+
const idOf = (name: string): string => `f-${name.replace(/[^A-Za-z0-9]+/g, "-")}`;
|
|
56
|
+
|
|
57
|
+
function caption(attr: Attr, context: Context): string {
|
|
58
|
+
const text = esc(label(attr.name));
|
|
59
|
+
if (attr.optional) return `${text} <span class="ml-1 text-xs font-normal text-muted">optional</span>`;
|
|
60
|
+
return context.mark ? `${text} <span class="text-primary" title="Required" aria-hidden="true">*</span>` : text;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The words of the refusal that is about the field at `name`, or null: the one the command named for this
|
|
65
|
+
* attribute at the top of the form, or one that names this part of the value object it was about.
|
|
66
|
+
*/
|
|
67
|
+
function refusalAt(attr: Attr, name: string, context: Context): string | null {
|
|
68
|
+
if (context.top && context.invalid?.field === attr.name) return context.invalid.message;
|
|
69
|
+
const named = context.problems?.find((one) => one.slots.some((part) => name === `${one.name}.${part}` || name.startsWith(`${one.name}.${part}.`)));
|
|
70
|
+
return named ? named.message : null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const flagged = (attr: Attr, name: string, context: Context): boolean => refusalAt(attr, name, context) !== null;
|
|
74
|
+
|
|
75
|
+
/** The attributes of the control that is invalid: the state and the link to its message (and to its hint, when it has one). */
|
|
76
|
+
function invalidAttrs(name: string, attr: Attr, context: Context, hint = false): string {
|
|
77
|
+
const ids = [...(hint ? [`${idOf(name)}-hint`] : []), ...(flagged(attr, name, context) ? [`${idOf(name)}-error`] : [])];
|
|
78
|
+
const described = ids.length > 0 ? ` aria-describedby="${esc(ids.join(" "))}"` : "";
|
|
79
|
+
return `${flagged(attr, name, context) ? ` aria-invalid="true"` : ""}${described}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function problem(name: string, attr: Attr, context: Context): string {
|
|
83
|
+
const text = refusalAt(attr, name, context);
|
|
84
|
+
return text !== null ? `<p class="text-xs font-semibold text-error" id="${esc(idOf(name))}-error">${esc(text)}</p>` : "";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** A moment's input: the seconds in a number input; the script swaps it for a native date input in the viewer's zone. */
|
|
88
|
+
function moment(attr: Attr, name: string, value: Value, context: Context): string {
|
|
89
|
+
const id = esc(idOf(name));
|
|
90
|
+
const seconds = secondsOf(value);
|
|
91
|
+
const classes = flagged(attr, name, context) ? "input input-error w-full max-w-xl" : "input w-full max-w-xl";
|
|
92
|
+
const required = context.mark && !attr.optional ? ` aria-required="true"` : "";
|
|
93
|
+
const input = `<input type="number" step="1" class="${classes}" id="${id}" name="${esc(name)}" value="${seconds ?? ""}" data-epoch-raw${required}${invalidAttrs(name, attr, context)}>`;
|
|
94
|
+
const note = `<p class="text-xs text-muted" data-epoch-note>Whole seconds since 1970, in UTC.</p>`;
|
|
95
|
+
return `<div class="my-4 grid gap-1" data-moment-field="${esc(attr.widget ?? "datetime")}"><label for="${id}" class="${context.quiet ? "sr-only" : "font-semibold"}">${caption(attr, context)}</label>${input}${note}${problem(name, attr, context)}</div>`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The control a picker stands in for the text box, or null when the attribute has none. */
|
|
99
|
+
function picked(attr: Attr, name: string, value: Value, context: Context): string | null {
|
|
100
|
+
if (!attr.picker) return null;
|
|
101
|
+
const held = member(value, "value") ?? value ?? "";
|
|
102
|
+
const required = context.mark && !attr.optional ? ` aria-required="true"` : "";
|
|
103
|
+
const control = { id: idOf(name), name, value: String(held), optional: attr.optional, invalid: flagged(attr, name, context), extra: `${required}${invalidAttrs(name, attr, context)}` };
|
|
104
|
+
return pickerControl(attr.picker, context.choices ?? {}, control);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** A closed set's members as a native select, with the held value chosen; an optional one may be left unchosen. */
|
|
108
|
+
function select(attr: Attr, name: string, value: Value, context: Context): string {
|
|
109
|
+
const held = String(member(value, "value") ?? value ?? "");
|
|
110
|
+
const id = esc(idOf(name));
|
|
111
|
+
const required = context.mark && !attr.optional ? ` aria-required="true"` : "";
|
|
112
|
+
const classes = flagged(attr, name, context) ? "select select-error w-full max-w-xl" : "select w-full max-w-xl";
|
|
113
|
+
const blank = attr.optional || held === "" ? `<option value=""${held === "" ? " selected" : ""}>${attr.optional ? "None" : "Choose"}</option>` : "";
|
|
114
|
+
const options = (attr.options ?? []).map((option) => `<option value="${esc(option)}"${option === held ? " selected" : ""}>${esc(label(option))}</option>`);
|
|
115
|
+
const input = `<select class="${classes}" id="${id}" name="${esc(name)}"${required}${invalidAttrs(name, attr, context)}>${blank}${options.join("")}</select>`;
|
|
116
|
+
return `<div class="my-4 grid gap-1"><label for="${id}" class="${context.quiet ? "sr-only" : "font-semibold"}">${caption(attr, context)}</label>${input}${problem(name, attr, context)}</div>`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function scalar(attr: Attr, name: string, value: Value, context: Context): string {
|
|
120
|
+
if (isMoment(attr)) return moment(attr, name, value, context);
|
|
121
|
+
if (attr.options && !attr.list) return select(attr, name, value, context);
|
|
23
122
|
const text = esc(member(value, "value") ?? value ?? "");
|
|
123
|
+
const id = esc(idOf(name));
|
|
24
124
|
if (attr.kind === "boolean") {
|
|
25
|
-
return `<
|
|
125
|
+
return `<div class="my-4 flex items-center gap-2"><input type="checkbox" class="checkbox checkbox-sm" id="${id}" name="${esc(name)}"${value === true ? " checked" : ""}><label for="${id}" class="font-semibold">${caption(attr, context)}</label></div>`;
|
|
26
126
|
}
|
|
27
127
|
const step = attr.kind === "number" ? ` step="any"` : "";
|
|
28
|
-
|
|
128
|
+
const required = context.mark && !attr.optional ? ` aria-required="true"` : "";
|
|
129
|
+
const bad = flagged(attr, name, context);
|
|
130
|
+
const limit = attr.maxLength ? ` maxlength="${attr.maxLength}"` : "";
|
|
131
|
+
if (attr.multiline && !attr.list && attr.kind === "text") {
|
|
132
|
+
const classes = bad ? "textarea textarea-error w-full max-w-3xl" : "textarea w-full max-w-3xl";
|
|
133
|
+
const hint = `<p class="text-xs text-muted" id="${id}-hint">Up to ${attr.maxLength} characters.</p>`;
|
|
134
|
+
const box = `<textarea class="${classes}" rows="5" id="${id}" name="${esc(name)}"${limit}${required}${invalidAttrs(name, attr, context, true)}>\n${text}</textarea>`;
|
|
135
|
+
return `<div class="my-4 grid gap-1"><label for="${id}" class="${context.quiet ? "sr-only" : "font-semibold"}">${caption(attr, context)}</label>${box}${hint}${problem(name, attr, context)}</div>`;
|
|
136
|
+
}
|
|
137
|
+
const classes = bad ? "input input-error w-full max-w-xl" : "input w-full max-w-xl";
|
|
138
|
+
const chosen = picked(attr, name, value, context);
|
|
139
|
+
const input = chosen ?? `<input type="${inputType(attr)}"${step} class="${classes}" id="${id}" name="${esc(name)}" value="${text}"${limit}${required}${invalidAttrs(name, attr, context)}>`;
|
|
140
|
+
return `<div class="my-4 grid gap-1"><label for="${id}" class="${context.quiet ? "sr-only" : "font-semibold"}">${caption(attr, context)}</label>${input}${problem(name, attr, context)}</div>`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** A value object's one plain part as the attribute that holds it: it takes the attribute's name, picker and moment. */
|
|
144
|
+
function plainOf(attr: Attr, part: Attr, name: string): Attr {
|
|
145
|
+
return { ...part, name, optional: attr.optional, widget: attr.widget ?? part.widget, picker: attr.picker ?? part.picker, options: attr.options ?? part.options };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function parts(agg: Aggregate, attr: Attr, name: string, value: Value, context: Context): string {
|
|
149
|
+
const members = agg.valueObjects[attr.type] ?? [];
|
|
150
|
+
const inside = { ...context, top: false, mark: context.mark && !attr.optional };
|
|
151
|
+
return members.map((part) => field(agg, part, member(value, part.name), `${name}.`, inside)).join("\n");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** The words of the refusal about the value object at `name` as a whole, or about one of its parts by name. */
|
|
155
|
+
function groupRefusal(attr: Attr, name: string, context: Context): string | null {
|
|
156
|
+
return refusalAt(attr, name, context) ?? context.problems?.find((one) => one.name === name)?.message ?? null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** The control a picture's media picker is opened from, or nothing when the editor has no pictures. */
|
|
160
|
+
function pickButton(): string {
|
|
161
|
+
return SCHEMA.media ? `<button type="button" class="btn btn-sm w-fit" data-action="pick-picture" data-media="${esc(href("media"))}">${icon("image")}<span>Choose a picture</span></button>` : "";
|
|
29
162
|
}
|
|
30
163
|
|
|
31
|
-
|
|
164
|
+
/**
|
|
165
|
+
* A value object that is a picture: its file, its description (required wherever a picture is) and its
|
|
166
|
+
* caption as one group with a button that opens the picker, in whatever depth it sits; any other part
|
|
167
|
+
* it has is drawn after it as it is anywhere else.
|
|
168
|
+
*/
|
|
169
|
+
function pictureField(agg: Aggregate, attr: Attr, name: string, value: Value, context: Context): string {
|
|
170
|
+
const picture = attr.picture as NonNullable<Attr["picture"]>;
|
|
32
171
|
const members = agg.valueObjects[attr.type] ?? [];
|
|
33
|
-
|
|
172
|
+
const inside: Context = { ...context, top: false, mark: context.mark && !attr.optional };
|
|
173
|
+
const named = [picture.key, picture.alt, picture.caption].filter((part): part is string => part !== undefined);
|
|
174
|
+
const control = (part?: string): string => {
|
|
175
|
+
const found = members.find((one) => one.name === part);
|
|
176
|
+
return found ? field(agg, found, member(value, found.name), `${name}.`, inside) : "";
|
|
177
|
+
};
|
|
178
|
+
const others = members.filter((one) => !named.includes(one.name)).map((one) => field(agg, one, member(value, one.name), `${name}.`, inside));
|
|
179
|
+
const thumb = `<img class="size-20 shrink-0 border border-base-300 object-cover" alt="" width="80" height="80" data-picture-thumb hidden>`;
|
|
180
|
+
const said = groupRefusal(attr, name, context);
|
|
181
|
+
const bad = said !== null ? ` aria-invalid="true" aria-describedby="${esc(idOf(name))}-error"` : "";
|
|
182
|
+
const why = said !== null ? `<p class="text-xs font-semibold text-error" id="${esc(idOf(name))}-error">${esc(said)}</p>` : "";
|
|
183
|
+
return `<fieldset class="mt-6 [&>legend+*]:clear-both"${bad}><legend class="float-left mb-2 w-full border-t border-base-300 pt-4 text-lg font-semibold">${caption(attr, context)}</legend>
|
|
184
|
+
<div class="my-4 grid gap-1 border border-base-300 p-3" data-picture data-key="${esc(`${name}.${picture.key}`)}" data-alt="${esc(`${name}.${picture.alt}`)}">
|
|
185
|
+
<div class="flex flex-wrap items-start gap-3">${thumb}<div class="grid min-w-0 flex-1 gap-1 max-sm:basis-full">${control(picture.key)}${control(picture.alt)}${control(picture.caption)}${pickButton()}</div></div>
|
|
186
|
+
<p class="text-xs text-muted">A picture needs a description for people who cannot see it.</p>
|
|
187
|
+
</div>
|
|
188
|
+
${others.join("\n")}${why}
|
|
189
|
+
</fieldset>`;
|
|
34
190
|
}
|
|
35
191
|
|
|
36
|
-
function one(agg: Aggregate, attr: Attr, name: string, value: Value): string {
|
|
37
|
-
if (attr.kind !== "object") return scalar(attr, name, value);
|
|
38
|
-
|
|
192
|
+
function one(agg: Aggregate, attr: Attr, name: string, value: Value, context: Context): string {
|
|
193
|
+
if (attr.kind !== "object") return scalar(attr, name, value, context);
|
|
194
|
+
const members = agg.valueObjects[attr.type] ?? [];
|
|
195
|
+
const hit = groupRefusal(attr, name, context);
|
|
196
|
+
if (members.length === 1 && members[0].kind !== "object" && !members[0].list) {
|
|
197
|
+
const own: Context = hit !== null ? { ...context, top: true, invalid: { field: attr.name, message: hit } } : context;
|
|
198
|
+
return scalar(plainOf(attr, members[0], attr.name), `${name}.${members[0].name}`, member(value, members[0].name), own);
|
|
199
|
+
}
|
|
200
|
+
if (attr.picture) return pictureField(agg, attr, name, value, context);
|
|
201
|
+
const invalid = hit !== null ? ` aria-invalid="true" aria-describedby="${esc(idOf(name))}-error"` : "";
|
|
202
|
+
const why = hit !== null ? `<p class="text-xs font-semibold text-error" id="${esc(idOf(name))}-error">${esc(hit)}</p>` : "";
|
|
203
|
+
return `<fieldset class="mt-6 [&>legend+*]:clear-both"${invalid}><legend class="float-left mb-2 w-full border-t border-base-300 pt-4 text-lg font-semibold">${caption(attr, context)}</legend>\n${parts(agg, attr, name, value, context)}\n${why}</fieldset>`;
|
|
39
204
|
}
|
|
40
205
|
|
|
41
|
-
function listed(agg: Aggregate, attr: Attr, name: string, value: Value): string {
|
|
206
|
+
function listed(agg: Aggregate, attr: Attr, name: string, value: Value, context: Context): string {
|
|
42
207
|
const held = Array.isArray(value) ? value : [];
|
|
208
|
+
const inside = { ...context, mark: false, top: false };
|
|
209
|
+
const members = agg.valueObjects[attr.type] ?? [];
|
|
210
|
+
const only = attr.kind === "object" && members.length === 1 && members[0].kind !== "object" && !members[0].list ? members[0] : null;
|
|
211
|
+
const word = attr.name.replace(/([^aeiou])ies$/, "$1y").replace(/s$/, "");
|
|
43
212
|
const rows = [...held, undefined].map((item, index) => {
|
|
44
|
-
const body =
|
|
45
|
-
|
|
213
|
+
const body = only
|
|
214
|
+
? scalar({ ...plainOf(attr, only, word), optional: only.optional }, `${name}.${index}.${only.name}`, member(item, only.name), { ...inside, quiet: true })
|
|
215
|
+
: attr.kind === "object"
|
|
216
|
+
? parts(agg, attr, `${name}.${index}`, item, inside)
|
|
217
|
+
: scalar({ ...attr, name: word, list: false, optional: false }, `${name}.${index}`, item, { ...inside, quiet: true });
|
|
218
|
+
return `<fieldset class="relative my-3 border-l-2 border-base-300 pl-4 [&>legend+*]:clear-both" data-row data-index="${index}"><legend class="float-left w-full text-xs font-semibold text-muted">${esc(label(word))} ${index + 1}</legend>\n${body}\n</fieldset>`;
|
|
46
219
|
});
|
|
47
|
-
|
|
220
|
+
const noun = esc(singular(word));
|
|
221
|
+
const invalid = flagged(attr, name, context) ? ` aria-invalid="true" aria-describedby="${esc(idOf(name))}-error"` : "";
|
|
222
|
+
const max = attr.max ? ` data-max="${attr.max}"` : "";
|
|
223
|
+
const limits = limitsNote(attr.min, attr.max);
|
|
224
|
+
const note = limits ? `\n<p class="clear-both mb-2 text-xs text-muted">${esc(limits)}</p>` : "";
|
|
225
|
+
return `<fieldset class="mt-6 [&>legend+*]:clear-both" data-repeat data-path="${esc(name)}" data-noun="${noun}"${max}${invalid}><legend class="float-left mb-2 w-full border-t border-base-300 pt-4 text-lg font-semibold">${caption(attr, context)}</legend>${note}\n<div data-rows>\n${rows.join("\n")}\n</div>${problem(name, attr, context)}\n</fieldset>`;
|
|
48
226
|
}
|
|
49
227
|
|
|
50
|
-
// The rich-text
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
// and rewrites those inputs from the
|
|
54
|
-
function body(attr: Attr, name: string, value: Value): string {
|
|
228
|
+
// The rich-text editor. The server renders the body as its read-only text and as the hidden inputs
|
|
229
|
+
// the form posts (the same dotted-path fields every value object and list uses), so a browser
|
|
230
|
+
// without the script still posts the body unchanged; the editor replaces the text with a writing
|
|
231
|
+
// surface and rewrites those inputs from the document as it is edited.
|
|
232
|
+
function body(attr: Attr, name: string, value: Value, context: Context): string {
|
|
55
233
|
const tree = value && typeof value === "object" ? value : emptyBody();
|
|
56
234
|
const inputs = fieldsToInputs(bodyToFields(tree, name));
|
|
57
235
|
const media = SCHEMA.media ? ` data-media="${esc(href("media"))}"` : "";
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
<
|
|
62
|
-
|
|
236
|
+
const draft = context.draft === attr.name ? " data-draft" : "";
|
|
237
|
+
const said = groupRefusal(attr, name, context);
|
|
238
|
+
const bad = said !== null ? ` aria-invalid="true" aria-describedby="${esc(idOf(name))}-error"` : "";
|
|
239
|
+
const why = said !== null ? `\n<p class="mt-2 text-sm font-semibold text-error" id="${esc(idOf(name))}-error">${esc(said)}</p>` : "";
|
|
240
|
+
return `<fieldset class="mt-6 [&>legend+*]:clear-both"${bad}><legend class="float-left mb-2 w-full border-t border-base-300 pt-4 text-lg font-semibold">${caption(attr, context)}</legend>
|
|
241
|
+
<div class="max-w-3xl" data-body-editor${draft} data-name="${esc(name)}" data-label="${esc(label(attr.name))}" data-body="${esc(JSON.stringify(tree))}"${media}>
|
|
242
|
+
<div class="prose border border-base-300 bg-base-100 p-6" data-body-fallback>${bodyToHtml(tree, 2)}</div>
|
|
243
|
+
<p class="mt-2 text-xs text-muted" data-needs-script>Editing this text needs scripts turned on in the browser.</p>
|
|
63
244
|
<div data-fields>
|
|
64
245
|
${inputs}
|
|
65
246
|
</div>
|
|
66
|
-
</div
|
|
247
|
+
</div>${why}
|
|
67
248
|
</fieldset>`;
|
|
68
249
|
}
|
|
69
250
|
|
|
251
|
+
/** A list of mixed blocks as cards; each slot is drawn by `field`, so a part is entered as it is anywhere else. */
|
|
252
|
+
function blocks(agg: Aggregate, attr: Attr, name: string, value: Value, context: Context): string {
|
|
253
|
+
const slot = (part: Attr, prefix: string, held: Value, message?: string, required?: boolean, rows?: boolean): string => {
|
|
254
|
+
const own: Context = { ...context, top: message !== undefined, invalid: message ? { field: part.name, message } : null, mark: required === true, quiet: false, draft: undefined, rows };
|
|
255
|
+
return field(agg, required ? { ...part, optional: false } : part, held, prefix, own);
|
|
256
|
+
};
|
|
257
|
+
const message = groupRefusal(attr, name, context) ?? undefined;
|
|
258
|
+
const rendering = { slot, draft: context.draft === attr.name, problems: context.problems ?? [], caption: caption(attr, context), media: SCHEMA.media ? href("media") : undefined, message };
|
|
259
|
+
return blockList(agg, attr, name, value, rendering);
|
|
260
|
+
}
|
|
261
|
+
|
|
70
262
|
/** One attribute's inputs, `prefix` being the dotted path of the object that holds it. */
|
|
71
|
-
export function field(agg: Aggregate, attr: Attr, value: Value, prefix = ""): string {
|
|
263
|
+
export function field(agg: Aggregate, attr: Attr, value: Value, prefix = "", context: Context = { mark: true, top: true }): string {
|
|
72
264
|
const name = `${prefix}${attr.name}`;
|
|
73
|
-
if (attr.widget === "body") return body(attr, name, value);
|
|
74
|
-
|
|
265
|
+
if (attr.widget === "body") return body(attr, name, value, context);
|
|
266
|
+
if (attr.blockList && attr.list) return blocks(agg, attr, name, value, context);
|
|
267
|
+
if (attr.list && attr.kind === "object" && !context.rows && (attr.picture !== undefined || heavy(agg.valueObjects, attr.type))) return blocks(agg, attr, name, value, context);
|
|
268
|
+
return attr.list ? listed(agg, attr, name, value, context) : one(agg, attr, name, value, context);
|
|
75
269
|
}
|
|
76
270
|
|
|
77
271
|
/** The inputs for `attrs`, filled from `values` (an instance's state or a refused submission). */
|
|
78
|
-
export function fields(agg: Aggregate, attrs: Attr[], values: Record<string, Value> = {}): string {
|
|
79
|
-
|
|
272
|
+
export function fields(agg: Aggregate, attrs: Attr[], values: Record<string, Value> = {}, invalid: Invalid | null = null, mark = true, choices: Choices = {}, draft?: string, problems: Problem[] = []): string {
|
|
273
|
+
const inputs = attrs.map((attr) => field(agg, attr, values[attr.name], "", { mark, invalid, top: true, choices, draft, problems }));
|
|
274
|
+
return [...inputs, datalists(agg, attrs, choices)].filter((text) => text !== "").join("\n");
|
|
80
275
|
}
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
__BANNER__
|
|
2
2
|
|
|
3
|
-
// The
|
|
4
|
-
//
|
|
3
|
+
// The small helpers every page shares. Pages are plain strings built on the server; every value that
|
|
4
|
+
// came from the domain or the person is escaped on its way in, so no text becomes markup.
|
|
5
|
+
//
|
|
6
|
+
// Every class name below is written out whole. The stylesheet is built by Tailwind, which finds
|
|
7
|
+
// classes by reading these files as text, so a class made by joining strings would not be found and
|
|
8
|
+
// would have no style.
|
|
5
9
|
import { EDITOR } from "../config.ts";
|
|
6
|
-
import {
|
|
10
|
+
import type { Aggregate } from "../schema.ts";
|
|
11
|
+
import { icon } from "./icons.ts";
|
|
12
|
+
import { label } from "./words.ts";
|
|
7
13
|
|
|
8
14
|
/** Escapes text for an HTML body or a double-quoted attribute. */
|
|
9
15
|
export function esc(value: unknown): string {
|
|
@@ -15,88 +21,41 @@ export function href(...segments: string[]): string {
|
|
|
15
21
|
return [EDITOR.basePath, ...segments.map(encodeURIComponent)].join("/");
|
|
16
22
|
}
|
|
17
23
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
table { border-collapse: collapse; width: 100%; }
|
|
26
|
-
th, td { text-align: left; padding: 0.35rem 0.5rem; border-bottom: 1px solid var(--edge); vertical-align: top; }
|
|
27
|
-
fieldset { border: 1px solid var(--edge); border-radius: 6px; margin: 0.5rem 0; padding: 0.5rem 0.75rem; }
|
|
28
|
-
label { display: block; margin: 0.4rem 0; }
|
|
29
|
-
label small { opacity: 0.7; }
|
|
30
|
-
input:not([type=checkbox]), select, textarea { display: block; width: 100%; max-width: 32rem; font: inherit; padding: 0.3rem; box-sizing: border-box; }
|
|
31
|
-
button { font: inherit; padding: 0.35rem 0.9rem; cursor: pointer; }
|
|
32
|
-
.badge { display: inline-block; padding: 0 0.6rem; border-radius: 1rem; border: 1px solid var(--edge); font-size: 0.85rem; }
|
|
33
|
-
.refusal { border: 1px solid #c33; border-radius: 6px; padding: 0.5rem 0.75rem; margin: 0.75rem 0; }
|
|
34
|
-
.outline { border-left: 3px solid var(--edge); margin: 0.25rem 0 0.25rem 0.25rem; padding-left: 0.75rem; }
|
|
35
|
-
.outline ul { margin: 0.2rem 0; padding-left: 1.25rem; }
|
|
36
|
-
.note { opacity: 0.7; font-size: 0.85rem; }
|
|
37
|
-
.body { overflow-wrap: anywhere; }
|
|
38
|
-
.body p, .body h1, .body h2, .body h3, .body h4, .body blockquote, .body li { white-space: pre-wrap; }
|
|
39
|
-
.body p, .body h1, .body h2, .body h3, .body h4, .body blockquote, .body ul, .body ol, .body figure { margin: 0.4rem 0; }
|
|
40
|
-
.body blockquote { border-left: 3px solid var(--edge); padding-left: 0.75rem; }
|
|
41
|
-
.body [data-align=left] { text-align: left; }
|
|
42
|
-
.body [data-align=center] { text-align: center; }
|
|
43
|
-
.body [data-align=right] { text-align: right; }
|
|
44
|
-
.body [data-align=justify] { text-align: justify; }
|
|
45
|
-
.body [data-indent="1"] { margin-left: 2rem; } .body [data-indent="2"] { margin-left: 4rem; } .body [data-indent="3"] { margin-left: 6rem; }
|
|
46
|
-
.body [data-indent="4"] { margin-left: 8rem; } .body [data-indent="5"] { margin-left: 10rem; } .body [data-indent="6"] { margin-left: 12rem; }
|
|
47
|
-
.body [data-indent="7"] { margin-left: 14rem; } .body [data-indent="8"] { margin-left: 16rem; }
|
|
48
|
-
.body li[data-depth="1"] { margin-left: 1.5rem; } .body li[data-depth="2"] { margin-left: 3rem; } .body li[data-depth="3"] { margin-left: 4.5rem; }
|
|
49
|
-
.body li[data-depth="4"] { margin-left: 6rem; } .body li[data-depth="5"] { margin-left: 7.5rem; }
|
|
50
|
-
.body .body-media { display: inline-block; border: 1px dashed var(--edge); padding: 0.2rem 0.5rem; }
|
|
51
|
-
.body-editor { max-width: 48rem; }
|
|
52
|
-
.body-toolbar { display: flex; flex-wrap: wrap; gap: 0.25rem; margin-bottom: 0.4rem; }
|
|
53
|
-
.body-toolbar button, .body-toolbar select { font-size: 0.85rem; padding: 0.15rem 0.5rem; width: auto; display: inline-block; }
|
|
54
|
-
.body-surface { min-height: 8rem; border: 1px solid var(--edge); border-radius: 6px; padding: 0.4rem 0.75rem; }
|
|
55
|
-
.body-surface:focus { outline: 2px solid var(--accent); }
|
|
56
|
-
.body-surface figure { cursor: pointer; }
|
|
57
|
-
.body-picker { border: 1px solid var(--edge); border-radius: 6px; padding: 0.5rem 0.75rem; margin-bottom: 0.5rem; }
|
|
58
|
-
.picker-list { display: flex; flex-wrap: wrap; gap: 0.5rem; list-style: none; margin: 0.25rem 0; padding: 0; max-height: 12rem; overflow: auto; }
|
|
59
|
-
.picker-list button { display: flex; flex-direction: column; align-items: center; max-width: 6rem; font-size: 0.8rem; }
|
|
60
|
-
.picker-list button[aria-pressed=true] { outline: 2px solid var(--accent); }
|
|
61
|
-
.picker-list img { object-fit: cover; }
|
|
62
|
-
`;
|
|
24
|
+
/**
|
|
25
|
+
* The inline message a refused command shows above its form. The page's script moves focus to the
|
|
26
|
+
* first field marked invalid, or to this block when no field is named.
|
|
27
|
+
*/
|
|
28
|
+
export function refusal(message: string | null): string {
|
|
29
|
+
return message ? `<div class="alert alert-error alert-soft mb-4 items-start" tabindex="-1" data-refusal>${icon("alert")}<p class="font-medium">${esc(message)}</p></div>` : "";
|
|
30
|
+
}
|
|
63
31
|
|
|
64
|
-
/**
|
|
65
|
-
const
|
|
32
|
+
/** A badge's classes and mark by tone. Colour is never the only cue: each tone has its own shape and the state's words. */
|
|
33
|
+
const TONES: Record<string, [classes: string, mark: string]> = {
|
|
34
|
+
neutral: ["badge badge-soft gap-1 font-semibold", "dot"],
|
|
35
|
+
ok: ["badge badge-soft badge-success gap-1 font-semibold", "success"],
|
|
36
|
+
info: ["badge badge-soft badge-info gap-1 font-semibold", "info"],
|
|
37
|
+
warn: ["badge badge-soft badge-warning gap-1 font-semibold", "alert"],
|
|
38
|
+
danger: ["badge badge-soft badge-error gap-1 font-semibold", "alert"],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** A status as a badge: its words, a shape and a colour. */
|
|
42
|
+
export function badge(status: unknown, tone = "neutral"): string {
|
|
43
|
+
if (!status) return "";
|
|
44
|
+
const [classes, mark] = TONES[tone] ?? TONES.neutral;
|
|
45
|
+
return `<span class="${classes}">${icon(mark)}${esc(label(String(status)))}</span>`;
|
|
46
|
+
}
|
|
66
47
|
|
|
67
|
-
/** The
|
|
68
|
-
export function
|
|
69
|
-
|
|
70
|
-
return `<!doctype html>
|
|
71
|
-
<html lang="en">
|
|
72
|
-
<head>
|
|
73
|
-
<meta charset="utf-8">
|
|
74
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
75
|
-
<title>${esc(title)} - ${esc(EDITOR.title)}</title>
|
|
76
|
-
<style>${STYLE}</style>
|
|
77
|
-
</head>
|
|
78
|
-
<body>
|
|
79
|
-
<header>
|
|
80
|
-
<a class="brand" href="${esc(EDITOR.basePath)}">${esc(EDITOR.title)}</a>
|
|
81
|
-
<nav>
|
|
82
|
-
${nav}
|
|
83
|
-
</nav>
|
|
84
|
-
</header>
|
|
85
|
-
<main>
|
|
86
|
-
${body}
|
|
87
|
-
</main>
|
|
88
|
-
${hasWidget(body) ? `<script type="module" src="${esc(href("assets", "body_widget.js"))}"></script>` : ""}
|
|
89
|
-
</body>
|
|
90
|
-
</html>
|
|
91
|
-
`;
|
|
48
|
+
/** The badge of an aggregate's lifecycle state, in the tone the schema gives that state. */
|
|
49
|
+
export function statusBadge(agg: Aggregate, status: unknown): string {
|
|
50
|
+
return badge(status, agg.lifecycle?.tones[String(status)] ?? "neutral");
|
|
92
51
|
}
|
|
93
52
|
|
|
94
|
-
/**
|
|
95
|
-
export function
|
|
96
|
-
return
|
|
53
|
+
/** A button that copies `value` to the clipboard (the page's script shows it and wires it; without a script it stays hidden). */
|
|
54
|
+
export function copyButton(value: string, what: string): string {
|
|
55
|
+
return `<button type="button" class="btn btn-ghost btn-square btn-xs text-muted" data-copy="${esc(value)}" title="Copy ${esc(what)}" aria-label="Copy ${esc(what)}" hidden>${icon("copy")}</button>`;
|
|
97
56
|
}
|
|
98
57
|
|
|
99
|
-
/** A
|
|
100
|
-
export function
|
|
101
|
-
return
|
|
58
|
+
/** A value with a button that copies it. */
|
|
59
|
+
export function copyable(value: string, what: string): string {
|
|
60
|
+
return `<span class="inline-flex items-center gap-1"><span>${esc(value)}</span>${copyButton(value, what)}</span>`;
|
|
102
61
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
__BANNER__
|
|
2
|
+
|
|
3
|
+
// The editor's icons: one SVG sprite written into every page, and `icon(name)` to draw one. The
|
|
4
|
+
// symbols are the project's single source of icons (the generator's own list), so the browser code
|
|
5
|
+
// draws the same ones with `<use href="#i-name">` and nothing is fetched.
|
|
6
|
+
|
|
7
|
+
/** The sprite, placed once at the top of every page. It takes no room and is hidden from readers. */
|
|
8
|
+
export const SPRITE = `<svg class="absolute size-0 overflow-hidden" xmlns="http://www.w3.org/2000/svg" width="0" height="0" focusable="false" aria-hidden="true">
|
|
9
|
+
__ICONS__
|
|
10
|
+
</svg>`;
|
|
11
|
+
|
|
12
|
+
/** An icon that decorates a label next to it. */
|
|
13
|
+
export function icon(name: string): string {
|
|
14
|
+
return `<svg class="icon" aria-hidden="true" focusable="false"><use href="#i-${name}"/></svg>`;
|
|
15
|
+
}
|
|
@@ -3,18 +3,24 @@ __BANNER__
|
|
|
3
3
|
// A submitted form turned back into what a command takes. The form's inputs are named by dotted
|
|
4
4
|
// path (see fields.ts); this reads them against the same attributes, so a nested value object comes
|
|
5
5
|
// back nested and a run of repeatable rows comes back as a list. A row left entirely blank is not a
|
|
6
|
-
// row, and an optional part left blank is left out
|
|
7
|
-
//
|
|
6
|
+
// row, and an optional part left blank is left out, as is an optional group with nothing in it; a
|
|
7
|
+
// required text part left blank in a group that has something in it is sent as empty text so that
|
|
8
|
+
// the domain's own rule refuses it in its own words. A rich-text body is read the
|
|
8
9
|
// same way: the widget posts its tree as the same dotted-path fields (`body.blocks.0.spans.1.text`),
|
|
9
10
|
// so there is no second encoding.
|
|
10
11
|
import type { Aggregate, Attr } from "../schema.ts";
|
|
11
12
|
|
|
12
13
|
export type Form = Map<string, string>;
|
|
13
14
|
|
|
14
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* The fields of an `application/x-www-form-urlencoded` body, the last of a repeated name winning.
|
|
17
|
+
* A browser sends a line break in a field as CR LF; it is read back as the single LF the domain
|
|
18
|
+
* holds, or a line break in a body would grow by a character each time the body is saved and
|
|
19
|
+
* opened again.
|
|
20
|
+
*/
|
|
15
21
|
export function parseForm(body: string): Form {
|
|
16
22
|
const form: Form = new Map();
|
|
17
|
-
for (const [key, value] of new URLSearchParams(body)) form.set(key, value);
|
|
23
|
+
for (const [key, value] of new URLSearchParams(body)) form.set(key, value.replace(/\r\n?/g, "\n"));
|
|
18
24
|
return form;
|
|
19
25
|
}
|
|
20
26
|
|
|
@@ -42,12 +48,23 @@ function text(attr: Attr, form: Form, path: string, optional: boolean): unknown
|
|
|
42
48
|
return raw;
|
|
43
49
|
}
|
|
44
50
|
|
|
51
|
+
/**
|
|
52
|
+
* A value object from its fields. One that is optional, or sits in an optional one, and was left
|
|
53
|
+
* entirely blank is left out; one with anything in it is read again with its required parts counted
|
|
54
|
+
* as required, so a part left blank is sent empty and the domain's own rule refuses it in its own words.
|
|
55
|
+
*/
|
|
45
56
|
function object(agg: Aggregate, attr: Attr, form: Form, path: string, optional: boolean): unknown {
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
57
|
+
const read = (lenient: boolean): Record<string, unknown> => {
|
|
58
|
+
const built: Record<string, unknown> = {};
|
|
59
|
+
for (const part of agg.valueObjects[attr.type] ?? []) {
|
|
60
|
+
const value = build(agg, part, form, `${path}.`, lenient);
|
|
61
|
+
if (value !== undefined) built[part.name] = value;
|
|
62
|
+
}
|
|
63
|
+
return built;
|
|
64
|
+
};
|
|
65
|
+
const lenient = read(optional);
|
|
66
|
+
if (optional && blank(lenient)) return undefined;
|
|
67
|
+
const built = optional ? read(false) : lenient;
|
|
51
68
|
return Object.keys(built).length > 0 ? built : undefined;
|
|
52
69
|
}
|
|
53
70
|
|
|
@@ -64,9 +81,9 @@ export function build(agg: Aggregate, attr: Attr, form: Form, prefix: string, op
|
|
|
64
81
|
return rows.filter((row) => !blank(row));
|
|
65
82
|
}
|
|
66
83
|
|
|
67
|
-
/** The arguments a command takes, from a submitted form. */
|
|
68
|
-
export function commandArguments(agg: Aggregate, attrs: Attr[], form: Form): Record<string, unknown> {
|
|
69
|
-
const args: Record<string, unknown> =
|
|
84
|
+
/** The arguments a command takes, from a submitted form; `empty` names the list arguments sent empty. */
|
|
85
|
+
export function commandArguments(agg: Aggregate, attrs: Attr[], form: Form, empty: string[] = []): Record<string, unknown> {
|
|
86
|
+
const args: Record<string, unknown> = Object.fromEntries(empty.map((name) => [name, []]));
|
|
70
87
|
for (const attr of attrs) {
|
|
71
88
|
const value = build(agg, attr, form, "");
|
|
72
89
|
if (value !== undefined) args[attr.name] = value;
|