hecks 3.9.1 → 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 +124 -2
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +8 -0
- data/lib/hecks/deploy/oidc.json +16 -3
- 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/clearing.rb +51 -0
- data/lib/hecks/hecks/adapters/deploy_toolchain/statuses.rb +2 -1
- data/lib/hecks/hecks/adapters/deploy_toolchain.rb +2 -0
- 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/site_toolchain/role_prober.rb +78 -0
- data/lib/hecks/hecks/adapters/site_toolchain.rb +29 -4
- data/lib/hecks/hecks/adapters/terminal.rb +13 -11
- data/lib/hecks/hecks/adapters/world_clearing.rb +65 -0
- 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 +61 -18
- data/lib/hecks/hecks/hecks.world +11 -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/fargate/settings/tuning.rb +15 -0
- data/lib/hecks/projections/deploy/fargate/settings.rb +3 -1
- data/lib/hecks/projections/deploy/fargate/stack/readers.rb +1 -0
- data/lib/hecks/projections/deploy/preview/preview.sh.erb +1 -1
- data/lib/hecks/projections/deploy/shared.rb +5 -2
- data/lib/hecks/projections/deploy/templates/fargate/Makefile.tmpl +7 -4
- 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 +95 -0
- data/lib/hecks/projections/site/cms_editor/blocks.rb +157 -0
- data/lib/hecks/projections/site/cms_editor/checks.rb +77 -0
- 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 +85 -0
- 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/pretty.rb +40 -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 +161 -0
- data/lib/hecks/projections/site/cms_editor/setting.rb +130 -0
- data/lib/hecks/projections/site/cms_editor/targets.rb +86 -0
- data/lib/hecks/projections/site/cms_editor/templates/package.json.tmpl +36 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/app.ts.tmpl +370 -0
- 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 +87 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/auth/session.ts.tmpl +36 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/auth/sso.ts.tmpl +20 -0
- 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 +133 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/config.ts.tmpl +29 -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 +43 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/media/handler.ts.tmpl +177 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/media/multipart.ts.tmpl +53 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/media/sniff.ts.tmpl +94 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/media/storage.ts.tmpl +50 -0
- 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 +214 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/server.ts.tmpl +65 -0
- 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 +123 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/body_parse.js.tmpl +320 -0
- 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 +275 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/html.ts.tmpl +61 -0
- 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 +92 -0
- 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 +63 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/pages.ts.tmpl +213 -0
- 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 +17 -0
- 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 +133 -0
- data/lib/hecks/projections/site/role_probe.rb +74 -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 +29 -1
- data/lib/hecks/site/bluebook/site.ports.hecksagon +6 -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/cli.rb +1 -1
- data/lib/hecks/tools/site_routes/editor_files.rb +45 -0
- data/lib/hecks/tools/site_routes.rb +14 -7
- 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 +9 -1
- 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/bridging.rs +1 -1
- data/rust/codegen/src/commands.rs +13 -8
- data/rust/codegen/src/constraints.rs +25 -0
- data/rust/codegen/src/domain_generator.rs +11 -9
- data/rust/codegen/src/expr_emitter.rs +13 -0
- data/rust/codegen/src/json_codec.rs +54 -15
- data/rust/codegen/src/main.rs +2 -1
- data/rust/codegen/src/mutations.rs +174 -4
- data/rust/codegen/src/naming.rs +30 -0
- 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 +73 -4
- data/rust/src/kernel/expression_operators/arithmetic.rs +130 -5
- data/rust/src/kernel/expression_operators/presence.rs +41 -3
- 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 +174 -36
- 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/prebuilt/0ee7f83cba59ce7d1c45c65ba166b071779f70143449f584f33ff7989f64eccb.marshal +0 -0
- data/prebuilt/6e4a6bcf0aa880e2e6b056e1846b70e5a768090b9dc998bbadeaf5d6bcfd3db8.marshal +0 -0
- data/prebuilt/verdicts-91aaf93ce8981bd9a754948f075d95650dc2ec6435015376f5c60992b36a24b1.json +0 -1
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
__BANNER__
|
|
2
|
+
|
|
3
|
+
// A value shown read-only, by its shape: a scalar as text, a value object as a list of its named
|
|
4
|
+
// parts, a list of scalars as chips and a list of value objects as a small table. A rich-text body
|
|
5
|
+
// is not shown this way; detail.ts renders it with bodyToHtml.
|
|
6
|
+
import { esc } from "./html.ts";
|
|
7
|
+
import { label } from "./words.ts";
|
|
8
|
+
|
|
9
|
+
const blank = (value: unknown): boolean => value === null || value === undefined || value === "";
|
|
10
|
+
|
|
11
|
+
/** The value inside a value object that has only a `value` part, so "Ann" is not `{value: "Ann"}`. */
|
|
12
|
+
function plain(value: unknown): unknown {
|
|
13
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
14
|
+
const entries = Object.entries(value as Record<string, unknown>);
|
|
15
|
+
if (entries.length === 1 && entries[0][0] === "value") return plain(entries[0][1]);
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const isObject = (value: unknown): value is Record<string, unknown> => !!value && typeof value === "object" && !Array.isArray(value);
|
|
21
|
+
|
|
22
|
+
const none = (text: string): string => `<span class="text-muted">${text}</span>`;
|
|
23
|
+
|
|
24
|
+
function chips(items: unknown[]): string {
|
|
25
|
+
return `<ul class="flex flex-wrap gap-2">${items.map((item) => `<li class="rounded-full border border-base-300 px-3 py-0.5">${outline(item)}</li>`).join("")}</ul>`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function subtable(rows: Record<string, unknown>[]): string {
|
|
29
|
+
const keys = [...new Set(rows.flatMap((row) => Object.keys(row)))];
|
|
30
|
+
const head = keys.map((key) => `<th scope="col" class="text-xs text-muted">${esc(label(key))}</th>`).join("");
|
|
31
|
+
const lines = rows.map((row) => `<tr>${keys.map((key) => `<td class="align-top">${outline(row[key])}</td>`).join("")}</tr>`);
|
|
32
|
+
return `<div class="overflow-x-auto"><table class="table table-sm"><thead><tr>${head}</tr></thead><tbody>${lines.join("")}</tbody></table></div>`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parts(value: Record<string, unknown>): string {
|
|
36
|
+
const rows = Object.entries(value).map(([key, part]) => `<div class="grid gap-0 sm:grid-cols-[8rem_minmax(0,1fr)] sm:gap-3"><dt class="text-muted">${esc(label(key))}</dt><dd>${outline(part)}</dd></div>`);
|
|
37
|
+
return `<dl class="grid gap-2">${rows.join("")}</dl>`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** `value` as read-only markup. */
|
|
41
|
+
export function outline(value: unknown): string {
|
|
42
|
+
const shown = plain(value);
|
|
43
|
+
if (blank(shown)) return none("Not set");
|
|
44
|
+
if (Array.isArray(shown)) {
|
|
45
|
+
if (shown.length === 0) return none("None");
|
|
46
|
+
return shown.every((item) => isObject(plain(item))) ? subtable(shown.map((item) => plain(item) as Record<string, unknown>)) : chips(shown);
|
|
47
|
+
}
|
|
48
|
+
if (isObject(shown)) return parts(shown);
|
|
49
|
+
if (typeof shown === "boolean") return shown ? "Yes" : "No";
|
|
50
|
+
return esc(shown);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A value as one line of plain text: single-part objects show the part, lists their size. */
|
|
54
|
+
export function summaryText(value: unknown): string {
|
|
55
|
+
const shown = plain(value);
|
|
56
|
+
if (blank(shown)) return "";
|
|
57
|
+
if (Array.isArray(shown)) return `${shown.length} ${shown.length === 1 ? "item" : "items"}`;
|
|
58
|
+
if (isObject(shown)) return Object.values(shown).map(plain).filter((part) => !blank(part) && !Array.isArray(part)).join(", ");
|
|
59
|
+
return typeof shown === "boolean" ? (shown ? "Yes" : "No") : String(shown);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** A one-line summary of a value, for a table cell, escaped. */
|
|
63
|
+
export const summary = (value: unknown): string => esc(summaryText(value));
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
__BANNER__
|
|
2
|
+
|
|
3
|
+
// The pages: an overview of what the domain holds, a list per aggregate (table.ts), one instance
|
|
4
|
+
// (detail.ts), and a form per command generated from the command's attributes. Every page is built
|
|
5
|
+
// into the frame in shell.ts. Lifecycle state is a badge, and a lifecycle move is offered only from
|
|
6
|
+
// the states it applies in. An editor of several chapters groups the overview and the navigation by
|
|
7
|
+
// chapter and puts the chapter in each address.
|
|
8
|
+
import { EDITOR } from "../config.ts";
|
|
9
|
+
import { at, keyOf } from "../names.ts";
|
|
10
|
+
import type { Aggregate, Attr, Command, Query } from "../schema.ts";
|
|
11
|
+
import { SCHEMA } from "../schema.ts";
|
|
12
|
+
import { detailPage as detail } from "./detail.ts";
|
|
13
|
+
import type { Submitted } from "./detail.ts";
|
|
14
|
+
import { confirmDialog } from "./dialogs.ts";
|
|
15
|
+
import { fields } from "./fields.ts";
|
|
16
|
+
import type { Invalid } from "./fields.ts";
|
|
17
|
+
import { esc, href, refusal } from "./html.ts";
|
|
18
|
+
import { backInput, backLink, withBack } from "./links.ts";
|
|
19
|
+
import type { Back } from "./links.ts";
|
|
20
|
+
import type { View } from "./paging.ts";
|
|
21
|
+
import type { Choices } from "./pickers.ts";
|
|
22
|
+
import { previewTools } from "./preview.ts";
|
|
23
|
+
import { scheduledTable } from "./schedule.ts";
|
|
24
|
+
import { SCHEDULED, countMark, groupOf, groups, onlyFor, page } from "./shell.ts";
|
|
25
|
+
import type { Crumb } from "./shell.ts";
|
|
26
|
+
import { listPage as list } from "./table.ts";
|
|
27
|
+
import type { World } from "./world.ts";
|
|
28
|
+
import { commandLabel, label, plural } from "./words.ts";
|
|
29
|
+
|
|
30
|
+
type State = Record<string, unknown>;
|
|
31
|
+
type Row = [string, State];
|
|
32
|
+
|
|
33
|
+
export type { Submitted };
|
|
34
|
+
|
|
35
|
+
const OVERVIEW: Crumb = ["Overview", href()];
|
|
36
|
+
|
|
37
|
+
const head = (title: string, note: string, extra = ""): string =>
|
|
38
|
+
`<div class="mb-6 flex flex-wrap items-start justify-between gap-3"><div><h1 class="text-3xl">${esc(title)}</h1><p class="mt-1 max-w-prose text-muted">${esc(note)}</p></div>${extra}</div>`;
|
|
39
|
+
|
|
40
|
+
/** The trail to an aggregate's list: the overview, then the chapter when there are several. */
|
|
41
|
+
const trail = (agg: Aggregate): Crumb[] => [OVERVIEW, ...(agg.chapter ? [[label(agg.chapter)] as Crumb] : []), [plural(agg.name), href(...at(agg))]];
|
|
42
|
+
|
|
43
|
+
/** The words a page of an aggregate carries in its title: the plural, and the chapter when it could be mistaken for another. */
|
|
44
|
+
const titleOf = (agg: Aggregate): string => (agg.chapter ? `${plural(agg.name)} (${label(agg.chapter)})` : plural(agg.name));
|
|
45
|
+
|
|
46
|
+
function overviewItems(aggregates: Aggregate[]): string {
|
|
47
|
+
const items = aggregates.map((agg) => {
|
|
48
|
+
const note = agg.description ? `<span class="text-muted">${esc(agg.description)}</span>` : "";
|
|
49
|
+
return onlyFor(agg, `<li class="flex items-center justify-between gap-4"><a class="grid flex-1 gap-0.5 p-4 hover:bg-base-200/60" href="${esc(href(...at(agg)))}"><span class="text-base font-semibold">${esc(plural(agg.name))}</span>${note}</a><span class="pr-4 tabular-nums text-muted">${countMark(keyOf(agg))}</span></li>`);
|
|
50
|
+
});
|
|
51
|
+
return `<ul class="divide-y divide-base-300 border-y border-base-300 bg-base-100">\n${items.join("\n")}\n</ul>`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The overview: every aggregate the domain declares, with what it is and how much of it there is. */
|
|
55
|
+
export function home(): string {
|
|
56
|
+
const sections = groups().map(([chapter, aggregates]) => {
|
|
57
|
+
if (chapter === null) return overviewItems(aggregates);
|
|
58
|
+
return groupOf(chapter, `<h2 class="mb-2 mt-8 text-lg">${esc(label(chapter))}</h2>\n${overviewItems(aggregates)}`);
|
|
59
|
+
});
|
|
60
|
+
const body = `${head(SCHEMA.chapters ? EDITOR.brand : SCHEMA.domain, "Choose what to edit.")}
|
|
61
|
+
${sections.join("\n")}`;
|
|
62
|
+
return page("Overview", body, { crumbs: [["Overview"]] });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The page for a path that names nothing, or, with a `title`, for another reason the person is
|
|
67
|
+
* sent back (the domain not answering, a chapter closed to their role): the heading and the tab
|
|
68
|
+
* say that reason, not "Not found".
|
|
69
|
+
*/
|
|
70
|
+
export function notFound(what: string, title = "Not found"): string {
|
|
71
|
+
const body = `${head(title, what)}
|
|
72
|
+
<p><a class="btn" href="${esc(href())}">Back to the overview</a></p>`;
|
|
73
|
+
return page(title, body, { crumbs: [OVERVIEW, [title]] });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** An aggregate's list: one page of its rows, with the filters in `view`. */
|
|
77
|
+
export function listPage(agg: Aggregate, rows: Row[], query: Query | null, error: string | null, asked: boolean, view: View, choices: Choices = {}): string {
|
|
78
|
+
return page(titleOf(agg), list(agg, rows, query, error, asked, view, choices), { active: keyOf(agg), crumbs: [OVERVIEW, ...(agg.chapter ? [[label(agg.chapter)] as Crumb] : []), [plural(agg.name)]] });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** One instance, with the records and actions that go with it and its preview. */
|
|
82
|
+
export function detailPage(agg: Aggregate, id: string, values: State, shown: Submitted | null, world: World): string {
|
|
83
|
+
const crumbs: Crumb[] = [...trail(agg), [id]];
|
|
84
|
+
const tools = previewTools(agg, id);
|
|
85
|
+
return page(id, detail(agg, id, values, shown, world, tools), { active: keyOf(agg), crumbs, drawer: tools.drawer });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Every action waiting to run, soonest first. */
|
|
89
|
+
export function scheduledPage(world: World): string {
|
|
90
|
+
const body = `${head("Scheduled", "What is waiting to run, soonest first. These are saved here and run by something outside this editor.")}
|
|
91
|
+
${scheduledTable(world, href("scheduled"))}`;
|
|
92
|
+
return page("Scheduled", body, { active: SCHEDULED, crumbs: [OVERVIEW, ["Scheduled"]] });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* What a command's form starts from: the instance's values, and for a working copy the instance has
|
|
97
|
+
* not saved yet (a draft not yet saved) the live content it stands for, and for a rich-text body or a
|
|
98
|
+
* block list the one it holds of the same shape, so an edit begins from the current content and not
|
|
99
|
+
* an empty page.
|
|
100
|
+
*/
|
|
101
|
+
function startingValues(agg: Aggregate, command: Command, state: State): State {
|
|
102
|
+
const values = { ...state };
|
|
103
|
+
const unset = (value: unknown): boolean => value == null || (Array.isArray(value) && value.length === 0);
|
|
104
|
+
const drafts = agg.drafts;
|
|
105
|
+
if (drafts && drafts.save === command.name && unset(values[drafts.attribute]) && !unset(state[drafts.live])) values[drafts.attribute] = state[drafts.live];
|
|
106
|
+
for (const attr of command.attributes.filter((candidate: Attr) => (candidate.widget === "body" || candidate.blockList) && unset(values[candidate.name]))) {
|
|
107
|
+
const held = agg.attributes.find((other) => other.type === attr.type && other.name !== attr.name && !unset(state[other.name]));
|
|
108
|
+
if (held) values[attr.name] = state[held.name];
|
|
109
|
+
}
|
|
110
|
+
return values;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const FORM = "command-form";
|
|
114
|
+
|
|
115
|
+
function invalidOf(own: Submitted | null): Invalid | null {
|
|
116
|
+
return own?.field && own.message ? { field: own.field, message: own.message.replace(/^.*?: /s, "") } : null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The buttons of a command's form: the command itself, and for one that makes something, the same and then another. */
|
|
120
|
+
function submits(agg: Aggregate, command: Command): string {
|
|
121
|
+
const text = esc(commandLabel(agg, command));
|
|
122
|
+
if (command.destructive) return `<button type="submit" class="btn btn-error" data-busy data-confirm="confirm-${esc(command.name)}">${text}</button>`;
|
|
123
|
+
const again = command.creates ? `<button type="submit" class="btn" name="__again" value="1" data-busy>Save and add another</button>` : "";
|
|
124
|
+
return `${again}<button type="submit" class="btn btn-neutral" data-busy>${text}</button>`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** What a command's form may be given beyond the command: the instance's values, the choices on offer and the way back. */
|
|
128
|
+
interface Extras {
|
|
129
|
+
state?: State;
|
|
130
|
+
/** Whether the form names the instance to act on, for a command that acts on another aggregate's. */
|
|
131
|
+
to?: boolean;
|
|
132
|
+
cancel?: string;
|
|
133
|
+
choices?: Choices;
|
|
134
|
+
back?: Back | null;
|
|
135
|
+
/** The instance the form acts on, which a draft's buttons need. */
|
|
136
|
+
id?: string;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The status line, and the buttons that publish or throw away the draft, on the page of the command
|
|
141
|
+
* that saves one. The question each asks is a dialog after the form; the publish dialog saves what is
|
|
142
|
+
* still unsaved first (`data-flush-draft`, see browser/autosave.js).
|
|
143
|
+
*/
|
|
144
|
+
function draftTools(agg: Aggregate, command: Command, extras: Extras): { bar: string; dialogs: string } {
|
|
145
|
+
const drafts = agg.drafts;
|
|
146
|
+
if (!drafts || drafts.save !== command.name || !extras.id) return { bar: "", dialogs: "" };
|
|
147
|
+
const id = extras.id;
|
|
148
|
+
const back = extras.back ?? null;
|
|
149
|
+
const saved = (extras.state ?? {})[drafts.attribute];
|
|
150
|
+
const held = saved != null && !(Array.isArray(saved) && saved.length === 0);
|
|
151
|
+
const make = (name?: string): Command | undefined => agg.commands.find((candidate) => candidate.name === name && candidate.on === agg.name && candidate.attributes.length === 0);
|
|
152
|
+
const ask = (command: Command | undefined, words: string, classes: string, tone: "neutral" | "error", flush: boolean, hidden: boolean): [string, string] => {
|
|
153
|
+
if (!command) return ["", ""];
|
|
154
|
+
const to = href(...at(agg), "id", id, command.name);
|
|
155
|
+
const link = back ? withBack(to, back.path) : to;
|
|
156
|
+
const button = `<a class="${classes}" href="${esc(link)}" data-confirm="confirm-${esc(command.name)}"${hidden ? " hidden data-draft-discard" : ""}>${esc(words)}</a>`;
|
|
157
|
+
return [button, confirmDialog(agg, command, to, id, undefined, { back, tone, button: words, flush })];
|
|
158
|
+
};
|
|
159
|
+
const publish = ask(make(drafts.publish), "Publish draft", "btn btn-neutral", "neutral", true, false);
|
|
160
|
+
const discard = ask(make(drafts.discard), "Discard draft", "btn btn-error btn-outline", "error", false, !held);
|
|
161
|
+
const live = esc(label(drafts.live).toLowerCase());
|
|
162
|
+
const note = `${held ? "A draft is saved." : "No draft is saved yet."} The live ${live} changes only when the draft is published.`;
|
|
163
|
+
const bar = `<div class="mb-4 flex flex-wrap items-center gap-2" data-draft-bar><p class="mr-auto text-sm text-muted" role="status" aria-live="polite" data-draft-status>${note}</p>${discard[0]}${publish[0]}</div>`;
|
|
164
|
+
return { bar, dialogs: `${publish[1]}\n${discard[1]}` };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** A form for one command; `to` adds the id input a command acting on another aggregate needs. */
|
|
168
|
+
export function commandForm(agg: Aggregate, command: Command, action: string, shown: Submitted | null, extras: Extras = {}): string {
|
|
169
|
+
const { state = {}, to = false, choices = {}, back = null } = extras;
|
|
170
|
+
const own = shown && shown.command === command.name ? shown : null;
|
|
171
|
+
const note = command.goal ? `<p class="mb-4 max-w-prose text-muted">${esc(command.goal)}.</p>` : "";
|
|
172
|
+
const target = to
|
|
173
|
+
? `<div class="my-4 grid gap-1"><label for="f-to" class="font-semibold">${esc(command.on ?? "")} id <span class="text-primary" aria-hidden="true">*</span></label><input type="text" class="input w-full max-w-xl" id="f-to" name="__to" value="${esc(own?.values.__to ?? "")}"></div>`
|
|
174
|
+
: "";
|
|
175
|
+
const dialog = command.destructive ? confirmDialog(agg, command, action, "", FORM, { back }) : "";
|
|
176
|
+
const draft = draftTools(agg, command, extras);
|
|
177
|
+
const saved = command.attributes.find((attr) => attr.name === agg.drafts?.attribute);
|
|
178
|
+
const whole = saved !== undefined && saved.widget !== "body" && saved.blockList === undefined;
|
|
179
|
+
const mode = draft.bar ? ` data-draft="${esc(agg.drafts?.attribute)}"${whole ? " data-draft-form" : ""}` : "";
|
|
180
|
+
const leave = back?.path ?? extras.cancel ?? href(...at(agg));
|
|
181
|
+
return `<form class="max-w-3xl" id="${FORM}" method="post" action="${esc(action)}" data-form${mode}>
|
|
182
|
+
${backInput(back)}${refusal(own?.message ?? null)}${note}
|
|
183
|
+
${draft.bar}
|
|
184
|
+
${target}
|
|
185
|
+
${fields(agg, command.attributes, own ? own.values : startingValues(agg, command, state), invalidOf(own), true, choices, draft.bar ? agg.drafts?.attribute : undefined, own?.problems ?? [])}
|
|
186
|
+
<div data-actionbar class="sticky bottom-0 z-10 -mx-4 mt-8 flex items-center justify-end gap-2 border-t border-base-300 bg-base-100 px-4 pt-3 pb-[max(0.75rem,env(safe-area-inset-bottom))] shadow-[0_-8px_16px_-12px_rgb(0_0_0/0.25)]"><p class="mr-auto text-xs text-muted"><span class="text-primary" aria-hidden="true">*</span> Required</p><a class="btn btn-ghost" href="${esc(leave)}">Cancel</a>${submits(agg, command)}</div>
|
|
187
|
+
</form>
|
|
188
|
+
${dialog}${draft.dialogs}`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** The page of one creating command (or a command that acts on another aggregate's instance); `state` fills it, `back` is where saving returns to. */
|
|
192
|
+
export function commandPage(agg: Aggregate, command: Command, action: string, shown: Submitted | null, choices: Choices = {}, extras: Extras = {}): string {
|
|
193
|
+
const crumbs: Crumb[] = [...trail(agg), [commandLabel(agg, command)]];
|
|
194
|
+
const form = commandForm(agg, command, action, shown, { ...extras, to: command.on !== null && command.on !== agg.name, choices });
|
|
195
|
+
return page(commandLabel(agg, command), `${backLink(extras.back ?? null)}${head(commandLabel(agg, command), label(agg.name))}\n${form}`, { active: keyOf(agg), crumbs });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** The page of a command that acts on one instance: its form, or the question a destructive one asks. */
|
|
199
|
+
export function instanceCommandPage(agg: Aggregate, id: string, command: Command, values: State, shown: Submitted | null, choices: Choices = {}, back: Back | null = null): string {
|
|
200
|
+
const action = href(...at(agg), "id", id, command.name);
|
|
201
|
+
const crumbs: Crumb[] = [...trail(agg), [id, href(...at(agg), "id", id)], [commandLabel(agg, command)]];
|
|
202
|
+
const tools = previewTools(agg, id);
|
|
203
|
+
const title = `${backLink(back)}${head(commandLabel(agg, command), `${label(agg.name)} ${id}`, tools.button)}`;
|
|
204
|
+
const cancel = back?.path ?? href(...at(agg), "id", id);
|
|
205
|
+
const frame = { active: keyOf(agg), crumbs, drawer: tools.drawer };
|
|
206
|
+
if (command.destructive && command.attributes.length === 0) {
|
|
207
|
+
const own = shown && shown.command === command.name ? shown.message : null;
|
|
208
|
+
const ask = `<form class="max-w-3xl" method="post" action="${esc(action)}" data-form>${backInput(back)}${refusal(own)}<p>${esc(command.goal ?? `This applies to ${id}`)}. Choose ${esc(commandLabel(agg, command))} to go ahead.</p>
|
|
209
|
+
<div class="mt-6 flex justify-end gap-2"><a class="btn btn-ghost" href="${esc(cancel)}">Cancel</a><button type="submit" class="btn btn-error" data-busy>${esc(commandLabel(agg, command))}</button></div></form>`;
|
|
210
|
+
return page(commandLabel(agg, command), `${title}\n${ask}`, frame);
|
|
211
|
+
}
|
|
212
|
+
return page(commandLabel(agg, command), `${title}\n${commandForm(agg, command, action, shown, { state: values, cancel, choices, back, id })}`, frame);
|
|
213
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
__BANNER__
|
|
2
|
+
|
|
3
|
+
// A list at scale: which instances a page of a list shows. The address holds all of it, so a page of
|
|
4
|
+
// a list is a link anyone can follow: `_page` (the page, from 1), `_status` (a lifecycle state) and
|
|
5
|
+
// `_q` (text), beside the `query` (and its arguments) the view of the domain's own queries uses. The
|
|
6
|
+
// names start with an underscore so they never collide with a query's own argument. The server
|
|
7
|
+
// narrows and cuts the rows; the browser only orders the page it was given.
|
|
8
|
+
import { at } from "../names.ts";
|
|
9
|
+
import type { Aggregate } from "../schema.ts";
|
|
10
|
+
import { summaryText } from "./outline.ts";
|
|
11
|
+
import { href } from "./html.ts";
|
|
12
|
+
|
|
13
|
+
type State = Record<string, unknown>;
|
|
14
|
+
type Row = [string, State];
|
|
15
|
+
|
|
16
|
+
/** The attribute names that name an instance to a person, in the order they are tried. */
|
|
17
|
+
const TITLES = ["title", "name", "label"];
|
|
18
|
+
|
|
19
|
+
/** What a list is showing: the filters in force, which page, and how many there are. */
|
|
20
|
+
export interface View {
|
|
21
|
+
q: string;
|
|
22
|
+
status: string;
|
|
23
|
+
page: number;
|
|
24
|
+
pages: number;
|
|
25
|
+
size: number;
|
|
26
|
+
/** How many rows match the filters. */
|
|
27
|
+
total: number;
|
|
28
|
+
/** How many rows there are before any filter. */
|
|
29
|
+
unfiltered: number;
|
|
30
|
+
/** The first and last row shown, counted from 1; both 0 when none. */
|
|
31
|
+
from: number;
|
|
32
|
+
to: number;
|
|
33
|
+
/** The other pairs of the address (the view's query and its arguments), kept on every link. */
|
|
34
|
+
params: [string, string][];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The states a lifecycle can be in, the one it starts in first. */
|
|
38
|
+
export function statesOf(agg: Aggregate): string[] {
|
|
39
|
+
const lifecycle = agg.lifecycle;
|
|
40
|
+
if (!lifecycle) return [];
|
|
41
|
+
return [...new Set([lifecycle.default, ...lifecycle.transitions.flatMap((move) => [...move.from, move.to])])];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The attribute that names an instance to a person: its title, name or label, when it has a plain one. */
|
|
45
|
+
export function titleOf(agg: Aggregate): string | undefined {
|
|
46
|
+
const plain = agg.attributes.filter((attr) => !attr.list && attr.widget !== "body").map((attr) => attr.name);
|
|
47
|
+
return TITLES.find((name) => plain.includes(name));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The text a search looks in: the identity, and the title. */
|
|
51
|
+
function searchable(agg: Aggregate, [id, state]: Row): string {
|
|
52
|
+
const title = titleOf(agg);
|
|
53
|
+
return [id, title ? summaryText(state[title]) : ""].join(" ").toLowerCase();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** `status` when it is a state the lifecycle has, else none. */
|
|
57
|
+
export const knownStatus = (agg: Aggregate, status: string): string => (statesOf(agg).includes(status) ? status : "");
|
|
58
|
+
|
|
59
|
+
/** The rows that are in `status` (when it is a state the lifecycle has) and hold every word of `q`. */
|
|
60
|
+
export function narrow(agg: Aggregate, rows: Row[], q: string, status: string): Row[] {
|
|
61
|
+
const words = q.toLowerCase().split(/\s+/).filter(Boolean);
|
|
62
|
+
const inState = status !== "" && agg.lifecycle ? (row: Row) => String(row[1][agg.lifecycle?.field ?? ""]) === status : () => true;
|
|
63
|
+
return rows.filter((row) => inState(row) && words.every((word) => searchable(agg, row).includes(word)));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The address of a list with its filters; `extra` overrides pairs, and an empty value leaves one out. */
|
|
67
|
+
export function listHref(agg: Aggregate, params: [string, string][], extra: Record<string, string> = {}): string {
|
|
68
|
+
const pairs = [...params.filter(([key]) => !(key in extra)), ...Object.entries(extra)].filter(([, value]) => value !== "");
|
|
69
|
+
const query = new URLSearchParams(pairs).toString();
|
|
70
|
+
return `${href(...at(agg))}${query ? `?${query}` : ""}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** What a page of `matching` rows shows: the filters, the page asked for (clamped to the pages there are) and the counts. */
|
|
74
|
+
export function viewOf(matching: number, unfiltered: number, asked: { q: string; status: string; page: number; size: number; params: [string, string][] }): View {
|
|
75
|
+
const pages = Math.max(1, Math.ceil(matching / asked.size));
|
|
76
|
+
const page = Math.min(Math.max(1, asked.page), pages);
|
|
77
|
+
const from = matching === 0 ? 0 : (page - 1) * asked.size + 1;
|
|
78
|
+
return { q: asked.q, status: asked.status, page, pages, size: asked.size, total: matching, unfiltered, from, to: Math.min(matching, page * asked.size), params: asked.params };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The rows of the page `view` names. */
|
|
82
|
+
export const slice = (rows: Row[], view: View): Row[] => rows.slice(Math.max(0, view.from - 1), view.to);
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
__BANNER__
|
|
2
|
+
|
|
3
|
+
// Pickers: an attribute that names an instance of another aggregate is entered by choosing it. What
|
|
4
|
+
// is on offer comes from the target's own listing query (see choices.ts), and a choice shows the
|
|
5
|
+
// target's title, name or label with its key. A target with few instances is a native `select`; with
|
|
6
|
+
// more, or when any text is allowed, a text box with a `datalist`; a `<kind>:<key>` value is a kind
|
|
7
|
+
// and then a record, which the page's script assembles and a browser with no script leaves as the
|
|
8
|
+
// one text box it is. The server checks every strict key exists before it sends the command.
|
|
9
|
+
import { hostName } from "../names.ts";
|
|
10
|
+
import type { Aggregate, Attr, Picker, PickerKind } from "../schema.ts";
|
|
11
|
+
import { esc } from "./html.ts";
|
|
12
|
+
|
|
13
|
+
/** One instance on offer: its key, and the words that name it to a person. */
|
|
14
|
+
export interface Choice {
|
|
15
|
+
key: string;
|
|
16
|
+
label: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** What is on offer for each target, by `targetKey`. */
|
|
20
|
+
export type Choices = Record<string, Choice[]>;
|
|
21
|
+
|
|
22
|
+
/** The part of a picker that says which aggregate it offers. */
|
|
23
|
+
export type Target = Omit<Picker, "kinds">;
|
|
24
|
+
|
|
25
|
+
/** How many choices a native select holds before a text box with suggestions takes over. */
|
|
26
|
+
export const SELECT_MAX = 30;
|
|
27
|
+
|
|
28
|
+
/** The most instances offered for one target; a key past them can still be typed and is still checked. */
|
|
29
|
+
export const OFFER_MAX = 500;
|
|
30
|
+
|
|
31
|
+
/** The host's name for the aggregate a target offers: the key its choices are held under. */
|
|
32
|
+
export const targetKey = (target: Target): string => hostName(target.target ?? "", target.chapter);
|
|
33
|
+
|
|
34
|
+
const idOf = (key: string): string => `choices-${key.replace(/[^A-Za-z0-9]+/g, "-")}`;
|
|
35
|
+
|
|
36
|
+
/** A choice as one line: its words, and its key when that is not the same thing. */
|
|
37
|
+
export const choiceText = (choice: Choice): string => (choice.label && choice.label !== choice.key ? `${choice.label} (${choice.key})` : choice.key);
|
|
38
|
+
|
|
39
|
+
/** The targets a picker names: itself, or each of its kinds that names an aggregate. */
|
|
40
|
+
export function targetsOfPicker(picker: Picker): Target[] {
|
|
41
|
+
return (picker.kinds ?? [picker]).filter((target) => target.target !== undefined);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Every target `attrs` name, nested value objects included, each once; a target any picker leaves open is not strict. */
|
|
45
|
+
export function targetsOf(agg: Aggregate, attrs: Attr[], found = new Map<string, Target>(), seen = new Set<string>()): Target[] {
|
|
46
|
+
for (const attr of attrs) {
|
|
47
|
+
for (const target of attr.picker ? targetsOfPicker(attr.picker) : []) {
|
|
48
|
+
const was = found.get(targetKey(target));
|
|
49
|
+
found.set(targetKey(target), { ...target, strict: Boolean(target.strict) && (was?.strict ?? true) });
|
|
50
|
+
}
|
|
51
|
+
if (attr.kind === "object" && !seen.has(attr.type)) targetsOf(agg, agg.valueObjects[attr.type] ?? [], found, new Set([...seen, attr.type]));
|
|
52
|
+
}
|
|
53
|
+
return [...found.values()];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The `datalist`s a form's pickers refer to: one per target that needs one. */
|
|
57
|
+
export function datalists(agg: Aggregate, attrs: Attr[], choices: Choices): string {
|
|
58
|
+
return targetsOf(agg, attrs)
|
|
59
|
+
.filter((target) => (choices[targetKey(target)] ?? []).length > 0 && (!target.strict || choices[targetKey(target)].length > SELECT_MAX))
|
|
60
|
+
.map((target) => {
|
|
61
|
+
const options = choices[targetKey(target)].map((choice) => `<option value="${esc(choice.key)}" label="${esc(choiceText(choice))}"></option>`);
|
|
62
|
+
return `<datalist id="${esc(idOf(targetKey(target)))}">${options.join("")}</datalist>`;
|
|
63
|
+
})
|
|
64
|
+
.join("\n");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** What a picker's control needs from the input it stands in for. */
|
|
68
|
+
export interface Control {
|
|
69
|
+
id: string;
|
|
70
|
+
name: string;
|
|
71
|
+
value: string;
|
|
72
|
+
optional: boolean;
|
|
73
|
+
/** Whether the field is the one a refusal named. */
|
|
74
|
+
invalid: boolean;
|
|
75
|
+
/** Further attributes of the control, already escaped, each with its leading space. */
|
|
76
|
+
extra: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const inputClasses = (control: Control): string => (control.invalid ? "input input-error w-full max-w-xl" : "input w-full max-w-xl");
|
|
80
|
+
|
|
81
|
+
/** A native select of the choices, with the held value kept when it is not on offer. */
|
|
82
|
+
function select(options: Choice[], control: Control): string {
|
|
83
|
+
const held = control.value !== "" && !options.some((choice) => choice.key === control.value);
|
|
84
|
+
const blank = `<option value="">${control.optional ? "None" : "Choose..."}</option>`;
|
|
85
|
+
const own = held ? [`<option value="${esc(control.value)}" selected>${esc(control.value)} (not on offer)</option>`] : [];
|
|
86
|
+
const each = options.map((choice) => `<option value="${esc(choice.key)}"${choice.key === control.value ? " selected" : ""}>${esc(choiceText(choice))}</option>`);
|
|
87
|
+
const classes = control.invalid ? "select select-error w-full max-w-xl" : "select w-full max-w-xl";
|
|
88
|
+
return `<select class="${classes}" id="${esc(control.id)}" name="${esc(control.name)}"${control.extra}>${[blank, ...own, ...each].join("")}</select>`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** A text box with suggestions; a strict one is checked against them before the form is sent, unless they were cut short. */
|
|
92
|
+
function suggesting(target: Target, control: Control, cut: boolean): string {
|
|
93
|
+
const strict = target.strict ? (cut ? " data-strict data-open" : " data-strict") : "";
|
|
94
|
+
return `<input type="text" class="${inputClasses(control)}" id="${esc(control.id)}" name="${esc(control.name)}" value="${esc(control.value)}" list="${esc(idOf(targetKey(target)))}" autocomplete="off"${strict}${control.extra}>`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** A `<kind>:<key>` value: its one text box, and the kinds and records the script builds a pair of selects from. */
|
|
98
|
+
function kindsControl(kinds: PickerKind[], choices: Choices, control: Control): string {
|
|
99
|
+
const shown = kinds.map((kind) => ({ kind: kind.kind, strict: Boolean(kind.strict && kind.target), options: kind.target ? (choices[targetKey(kind)] ?? []) : [] }));
|
|
100
|
+
const input = `<input type="text" class="${inputClasses(control)}" id="${esc(control.id)}" name="${esc(control.name)}" value="${esc(control.value)}" autocomplete="off"${control.extra}>`;
|
|
101
|
+
return `<div class="grid max-w-xl gap-2" data-key-picker data-kinds="${esc(JSON.stringify(shown))}">${input}</div>`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The control for a picker, or null when what is on offer could not be read (the input stays a text box). */
|
|
105
|
+
export function pickerControl(picker: Picker, choices: Choices, control: Control): string | null {
|
|
106
|
+
if (picker.kinds) return kindsControl(picker.kinds, choices, control);
|
|
107
|
+
const options = choices[targetKey(picker)];
|
|
108
|
+
if (!options) return null;
|
|
109
|
+
return picker.strict && options.length <= SELECT_MAX ? select(options, control) : suggesting(picker, control, options.length >= OFFER_MAX);
|
|
110
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
__BANNER__
|
|
2
|
+
|
|
3
|
+
// A record as the site shows it, in a side drawer beside the editor. The row's `preview` is an address
|
|
4
|
+
// template; the record's key and its parts fill it, and the page frames the result. This is only a
|
|
5
|
+
// framed address: nothing of the editor is sent to it and nothing is injected into it, and the
|
|
6
|
+
// editor's content security policy names its origin (and no other) as a place a frame may load from.
|
|
7
|
+
//
|
|
8
|
+
// The frame is sandboxed with `allow-scripts allow-same-origin` and nothing else. The site's own
|
|
9
|
+
// scripts must run for the page to look as it does, and without `allow-same-origin` the page would
|
|
10
|
+
// have an opaque origin, so its storage, its cookies and its requests to itself would all fail. On a
|
|
11
|
+
// site served from another origin the flag lets the page keep its own origin and gives it no way to
|
|
12
|
+
// reach the editor. A path template is on the editor's own origin, where the pair removes the sandbox
|
|
13
|
+
// altogether: use an absolute address for a site served elsewhere.
|
|
14
|
+
import { EDITOR } from "../config.ts";
|
|
15
|
+
import type { Aggregate } from "../schema.ts";
|
|
16
|
+
import { esc } from "./html.ts";
|
|
17
|
+
import { icon } from "./icons.ts";
|
|
18
|
+
import { partsOf } from "./links.ts";
|
|
19
|
+
import type { Parts } from "./links.ts";
|
|
20
|
+
import { pictureAggregate, schedulingAggregate } from "./world.ts";
|
|
21
|
+
import { label } from "./words.ts";
|
|
22
|
+
|
|
23
|
+
/** A value placed in an address: encoded, with the colon of a key left as it is. */
|
|
24
|
+
const placed = (value: string): string => encodeURIComponent(value).replace(/%3A/gi, ":");
|
|
25
|
+
|
|
26
|
+
/** The address a record is previewed at, or null when the row names no preview. */
|
|
27
|
+
export function previewUrl(agg: Aggregate, id: string): string | null {
|
|
28
|
+
if (!EDITOR.preview) return null;
|
|
29
|
+
const parts = partsOf(agg, id);
|
|
30
|
+
return EDITOR.preview.replace(/\{(key|kind|slug|id)\}/g, (_, name: keyof Parts) => placed(parts[name]));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** What a page needs to offer a preview: the button for the heading, and the drawer for the end of the page. */
|
|
34
|
+
export interface Tools {
|
|
35
|
+
button: string;
|
|
36
|
+
drawer: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const NONE: Tools = { button: "", drawer: "" };
|
|
40
|
+
|
|
41
|
+
const WIDTHS = [
|
|
42
|
+
["phone", "Phone", "375 pixels wide"],
|
|
43
|
+
["tablet", "Tablet", "768 pixels wide"],
|
|
44
|
+
["desktop", "Desktop", "the width of the window"],
|
|
45
|
+
] as const;
|
|
46
|
+
|
|
47
|
+
function widths(): string {
|
|
48
|
+
const buttons = WIDTHS.map(
|
|
49
|
+
([name, text, note]) =>
|
|
50
|
+
`<button type="button" class="btn btn-sm join-item" data-preview-width="${name}" aria-pressed="${name === "desktop" ? "true" : "false"}" title="${text}, ${note}">${icon(name)}<span class="hidden sm:inline">${text}</span></button>`,
|
|
51
|
+
);
|
|
52
|
+
return `<div class="join" role="group" aria-label="Preview width">${buttons.join("")}</div>`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function drawer(agg: Aggregate, id: string, url: string): string {
|
|
56
|
+
const title = `Preview of ${label(agg.name).toLowerCase()} ${id}`;
|
|
57
|
+
return `<div class="drawer drawer-end" data-preview>
|
|
58
|
+
<input id="preview-toggle" type="checkbox" class="drawer-toggle" aria-hidden="true" tabindex="-1">
|
|
59
|
+
<div class="drawer-side z-40">
|
|
60
|
+
<label for="preview-toggle" aria-label="Close the preview" class="drawer-overlay"></label>
|
|
61
|
+
<section class="flex h-full w-full max-w-6xl flex-col border-l border-base-300 bg-base-200" role="dialog" aria-modal="true" aria-label="${esc(title)}">
|
|
62
|
+
<div class="flex flex-wrap items-center gap-2 border-b border-base-300 bg-base-100 p-2">
|
|
63
|
+
<h2 class="mr-auto px-2 text-base">Preview</h2>
|
|
64
|
+
${widths()}
|
|
65
|
+
<button type="button" class="btn btn-sm" data-preview-refresh>${icon("refresh")}<span class="hidden sm:inline">Refresh</span></button>
|
|
66
|
+
<a class="btn btn-sm" href="${esc(url)}" target="_blank" rel="noopener" data-preview-external>${icon("external")}<span class="hidden sm:inline">Open in new tab</span></a>
|
|
67
|
+
<button type="button" class="btn btn-ghost btn-sm btn-square" aria-label="Close the preview" data-preview-close>${icon("close")}</button>
|
|
68
|
+
</div>
|
|
69
|
+
<div class="grid min-h-0 flex-1 justify-items-center overflow-auto p-2" data-preview-stage>
|
|
70
|
+
<iframe class="h-full w-full max-w-full border border-base-300 bg-white" title="${esc(title)}" sandbox="allow-scripts allow-same-origin" referrerpolicy="no-referrer" data-preview-frame data-src="${esc(url)}"></iframe>
|
|
71
|
+
</div>
|
|
72
|
+
<p class="px-3 pb-2 text-xs text-muted">This is the site's own page at <span class="break-all">${esc(url)}</span>. It reads only: nothing here publishes. A page that stays blank is one the site does not let this editor show in a frame.</p>
|
|
73
|
+
</section>
|
|
74
|
+
</div>
|
|
75
|
+
</div>`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Whether a record of `agg` is something the site shows a page for: the pictures' records and the scheduled actions are not. */
|
|
79
|
+
const shown = (agg: Aggregate): boolean => agg !== pictureAggregate() && agg !== schedulingAggregate();
|
|
80
|
+
|
|
81
|
+
/** The preview button and drawer for one record, or nothing when the row names no preview or the record has no page. */
|
|
82
|
+
export function previewTools(agg: Aggregate, id: string): Tools {
|
|
83
|
+
const url = shown(agg) ? previewUrl(agg, id) : null;
|
|
84
|
+
if (!url) return NONE;
|
|
85
|
+
const button = `<a class="btn" href="${esc(url)}" target="_blank" rel="noopener" data-preview-open="preview-toggle">${icon("eye")}Preview</a>`;
|
|
86
|
+
return { button, drawer: drawer(agg, id, url) };
|
|
87
|
+
}
|