@warpgogol/werkstatt-shared 0.2.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.
- package/package.json +352 -0
- package/src/checks/index.ts +24 -0
- package/src/checks/lib/astro-site-url.ts +30 -0
- package/src/checks/lib/i18n.ts +30 -0
- package/src/checks/result-helpers.ts +102 -0
- package/src/checks/suppressions-config.ts +195 -0
- package/src/index.ts +14 -0
- package/src/integration/README.md +38 -0
- package/src/integration/adapters.ts +143 -0
- package/src/integration/crm-buffer.ts +491 -0
- package/src/integration/dispatch.ts +106 -0
- package/src/integration/funnel.ts +379 -0
- package/src/integration/index.ts +56 -0
- package/src/integration/lifecycle.ts +155 -0
- package/src/integration/orchestration.ts +439 -0
- package/src/integration/port-barrel.ts +124 -0
- package/src/integration/port.ts +143 -0
- package/src/integration/qstash.ts +134 -0
- package/src/integration/sharding.ts +133 -0
- package/src/integration/tests/deliver-event.test.ts +52 -0
- package/src/integration/tests/dispatch.test.ts +88 -0
- package/src/integration/tests/funnel-bridge.test.ts +46 -0
- package/src/integration/tests/hub-runtime.test.ts +128 -0
- package/src/integration/tests/lifecycle.test.ts +69 -0
- package/src/integration/tests/qstash.test.ts +83 -0
- package/src/integration/tests/sharding.test.ts +64 -0
- package/src/integration/vitest.config.ts +8 -0
- package/src/integration-adapter-supabase-crm/adapter.ts +437 -0
- package/src/integration-adapter-supabase-crm/client.ts +605 -0
- package/src/integration-adapter-supabase-crm/index.ts +28 -0
- package/src/integration-adapter-supabase-crm/pipedrive-sync-target.ts +482 -0
- package/src/integration-adapter-supabase-crm/supabase-rest.ts +89 -0
- package/src/integration-adapter-supabase-crm/tenant-registry.ts +334 -0
- package/src/integration-adapter-supabase-crm/tests/change-balance.pbt.test.ts +83 -0
- package/src/integration-adapter-supabase-crm/tests/funnel-persistence.test.ts +409 -0
- package/src/integration-adapter-supabase-crm/tests/lifecycle-sync.test.ts +288 -0
- package/src/integration-adapter-supabase-crm/tests/stage-map.test.ts +80 -0
- package/src/integration-adapter-supabase-crm/vitest.config.ts +8 -0
- package/src/integration-adapter-supabase-crm/worker.ts +223 -0
- package/src/observability/conventions.ts +93 -0
- package/src/observability/env-resolver.ts +57 -0
- package/src/observability/index.ts +57 -0
- package/src/observability/metric-registry.ts +184 -0
- package/src/observability/otlp-converter.ts +125 -0
- package/src/observability/otlp-json.ts +135 -0
- package/src/observability/otlp-transport.ts +68 -0
- package/src/observability/pusher.ts +142 -0
- package/src/observability/redact.ts +27 -0
- package/src/observability/tests/conventions.test.ts +100 -0
- package/src/observability/tests/metric-registry.test.ts +117 -0
- package/src/observability/tests/otlp-converter.pbt.test.ts +77 -0
- package/src/observability/tests/otlp-converter.test.ts +157 -0
- package/src/observability/tests/otlp-json.test.ts +120 -0
- package/src/observability/tests/pusher.test.ts +75 -0
- package/src/observability/tests/redact.pbt.test.ts +71 -0
- package/src/observability/tests/redact.test.ts +37 -0
- package/src/observability/typed-refs.ts +137 -0
- package/src/observability/vitest.config.ts +8 -0
- package/src/ontology/analytics/matomo-binding.yaml +172 -0
- package/src/ontology/analytics/matomo-fleet.registry.yaml +26 -0
- package/src/ontology/analytics/messkanon.yaml +151 -0
- package/src/ontology/archetype-registry.ts +61 -0
- package/src/ontology/archetypes/components/brand-label.yaml +19 -0
- package/src/ontology/archetypes/components/copyright.yaml +19 -0
- package/src/ontology/archetypes/components/currency-aware-price-display.yaml +19 -0
- package/src/ontology/archetypes/components/currency-selector.yaml +19 -0
- package/src/ontology/archetypes/components/footer-promo.yaml +19 -0
- package/src/ontology/archetypes/components/lang-switcher.yaml +19 -0
- package/src/ontology/archetypes/components/layout.yaml +19 -0
- package/src/ontology/archetypes/components/live-photo.yaml +20 -0
- package/src/ontology/archetypes/components/material-credit.yaml +20 -0
- package/src/ontology/archetypes/components/media.yaml +21 -0
- package/src/ontology/archetypes/components/nachweis-card.yaml +34 -0
- package/src/ontology/archetypes/components/nachweis-detail.yaml +43 -0
- package/src/ontology/archetypes/components/nachweis-list.yaml +37 -0
- package/src/ontology/archetypes/components/nachweis-verify.yaml +31 -0
- package/src/ontology/archetypes/components/not-found.yaml +22 -0
- package/src/ontology/archetypes/components/offer-capacity.yaml +21 -0
- package/src/ontology/archetypes/components/passport-header.yaml +19 -0
- package/src/ontology/archetypes/components/passport-provenance.yaml +20 -0
- package/src/ontology/archetypes/components/passport-score-grid.yaml +20 -0
- package/src/ontology/archetypes/components/passport-star-map.yaml +20 -0
- package/src/ontology/archetypes/components/person-profile.yaml +20 -0
- package/src/ontology/archetypes/components/pulsar.yaml +20 -0
- package/src/ontology/archetypes/components/responsive-image.yaml +19 -0
- package/src/ontology/archetypes/components/scroll-to-top.yaml +22 -0
- package/src/ontology/archetypes/components/social-meta.yaml +20 -0
- package/src/ontology/archetypes/components/structured-data.yaml +19 -0
- package/src/ontology/archetypes/index.json +1003 -0
- package/src/ontology/archetypes/index.yaml +821 -0
- package/src/ontology/archetypes/sections/approach.yaml +23 -0
- package/src/ontology/archetypes/sections/article-list.yaml +30 -0
- package/src/ontology/archetypes/sections/audience-cards.yaml +30 -0
- package/src/ontology/archetypes/sections/breadcrumbs.yaml +21 -0
- package/src/ontology/archetypes/sections/chat-widget.yaml +37 -0
- package/src/ontology/archetypes/sections/comparison-cards.yaml +31 -0
- package/src/ontology/archetypes/sections/controlled-responsibility-block.yaml +34 -0
- package/src/ontology/archetypes/sections/credits.yaml +29 -0
- package/src/ontology/archetypes/sections/donation-card.yaml +53 -0
- package/src/ontology/archetypes/sections/donation-use.yaml +23 -0
- package/src/ontology/archetypes/sections/dynamic-status-block.yaml +35 -0
- package/src/ontology/archetypes/sections/faq-list.yaml +26 -0
- package/src/ontology/archetypes/sections/final-cta.yaml +53 -0
- package/src/ontology/archetypes/sections/hero-decision-card.yaml +65 -0
- package/src/ontology/archetypes/sections/hero.yaml +50 -0
- package/src/ontology/archetypes/sections/impact.yaml +23 -0
- package/src/ontology/archetypes/sections/markdown.yaml +43 -0
- package/src/ontology/archetypes/sections/mountain-journey.yaml +43 -0
- package/src/ontology/archetypes/sections/navigation.yaml +20 -0
- package/src/ontology/archetypes/sections/notausgang-block.yaml +41 -0
- package/src/ontology/archetypes/sections/open-source-registry.yaml +26 -0
- package/src/ontology/archetypes/sections/ownership-block.yaml +57 -0
- package/src/ontology/archetypes/sections/people.yaml +48 -0
- package/src/ontology/archetypes/sections/price-card.yaml +50 -0
- package/src/ontology/archetypes/sections/problem.yaml +23 -0
- package/src/ontology/archetypes/sections/send-message.yaml +63 -0
- package/src/ontology/archetypes/sections/service-metadata-block.yaml +40 -0
- package/src/ontology/archetypes/sections/social-proof.yaml +27 -0
- package/src/ontology/archetypes/sections/transparency.yaml +32 -0
- package/src/ontology/archetypes/sections/trust-strip.yaml +42 -0
- package/src/ontology/archetypes/sections/video-section.yaml +27 -0
- package/src/ontology/archetypes/sections/women.yaml +29 -0
- package/src/ontology/biome-token-projection.ts +229 -0
- package/src/ontology/biomes/check-concrete-blueprint.yaml +118 -0
- package/src/ontology/biomes/handwerk-material-warm.yaml +135 -0
- package/src/ontology/biomes/nonprofit-trust.yaml +123 -0
- package/src/ontology/blueprints/offer.yaml +39 -0
- package/src/ontology/blueprints/prompts/local-market-signal.md +13 -0
- package/src/ontology/blueprints/prompts/website-local-narrative.md +24 -0
- package/src/ontology/blueprints/ratgeber.yaml +62 -0
- package/src/ontology/blueprints/website-local.yaml +309 -0
- package/src/ontology/blueprints/website-service.yaml +84 -0
- package/src/ontology/capabilities/lead.submit.yaml +44 -0
- package/src/ontology/cosmic/index.ts +48 -0
- package/src/ontology/cosmic/moon-catalog.ts +173 -0
- package/src/ontology/cosmic/planet-catalog.ts +82 -0
- package/src/ontology/cosmic/star-catalog.ts +337 -0
- package/src/ontology/enums.ts +159 -0
- package/src/ontology/external-surfaces/index.ts +158 -0
- package/src/ontology/external-surfaces/jsonld-types.yaml +135 -0
- package/src/ontology/external-surfaces/sitemap-shape.yaml +6 -0
- package/src/ontology/external-surfaces/url-schema.yaml +95 -0
- package/src/ontology/growth/events/contact-email-click.yaml +21 -0
- package/src/ontology/growth/events/contact-form-submit.yaml +25 -0
- package/src/ontology/growth/events/contact-phone-click.yaml +21 -0
- package/src/ontology/growth/events/contact-route-click.yaml +21 -0
- package/src/ontology/growth/events/contact-submit.yaml +28 -0
- package/src/ontology/growth/events/contact-whatsapp-click.yaml +21 -0
- package/src/ontology/growth/events/cta-click.yaml +30 -0
- package/src/ontology/growth/events/donation-confirmed.yaml +32 -0
- package/src/ontology/growth/events/donation-form-start.yaml +21 -0
- package/src/ontology/growth/events/donation-form-submit.yaml +29 -0
- package/src/ontology/growth/events/donation-intent.yaml +29 -0
- package/src/ontology/growth/events/form-error.yaml +27 -0
- package/src/ontology/growth/events/form-start.yaml +21 -0
- package/src/ontology/growth/events/form-submit.yaml +21 -0
- package/src/ontology/growth/events/outbound-click.yaml +25 -0
- package/src/ontology/growth/events/page-view.yaml +24 -0
- package/src/ontology/growth/events/passport-view.yaml +28 -0
- package/src/ontology/growth/events/star-map-navigate.yaml +30 -0
- package/src/ontology/index.ts +165 -0
- package/src/ontology/manifest.ts +344 -0
- package/src/ontology/operations/index.ts +1 -0
- package/src/ontology/schemas/biome.ts +303 -0
- package/src/ontology/schemas/capability.ts +104 -0
- package/src/ontology/schemas/constellation.ts +114 -0
- package/src/ontology/schemas/dns-records.ts +53 -0
- package/src/ontology/schemas/index.ts +95 -0
- package/src/ontology/schemas/manifest-resolver.ts +123 -0
- package/src/ontology/schemas/page-entry.ts +213 -0
- package/src/ontology/schemas/section-archetype.ts +109 -0
- package/src/ontology/schemas/site-family.ts +98 -0
- package/src/ontology/schemas/system/growth.ts +80 -0
- package/src/ontology/schemas/system/integrations.ts +143 -0
- package/src/ontology/schemas/system/manifest.ts +492 -0
- package/src/ontology/schemas/system/page-output.ts +99 -0
- package/src/ontology/schemas/system/release.ts +58 -0
- package/src/ontology/schemas/system/text.ts +58 -0
- package/src/ontology/schemas/system.ts +53 -0
- package/src/ontology/shared-section-props/body-fragments.ts +182 -0
- package/src/ontology/shared-section-props/catalog.ts +195 -0
- package/src/ontology/shared-section-props/common.ts +178 -0
- package/src/ontology/shared-section-props/index.ts +34 -0
- package/src/ontology/shared-section-props/visual-header.ts +163 -0
- package/src/ontology/site-families/charity-donation-trust/cultural-rules.yaml +6 -0
- package/src/ontology/site-families/charity-donation-trust/family.yaml +44 -0
- package/src/ontology/site-families/charity-donation-trust/linguistic-rules.yaml +6 -0
- package/src/ontology/site-families/check-quality-operator/cultural-rules.yaml +6 -0
- package/src/ontology/site-families/check-quality-operator/family.yaml +40 -0
- package/src/ontology/site-families/check-quality-operator/linguistic-rules.yaml +6 -0
- package/src/ontology/site-families/handwerk-trust-engineering/cultural-rules.yaml +6 -0
- package/src/ontology/site-families/handwerk-trust-engineering/family.yaml +53 -0
- package/src/ontology/site-families/handwerk-trust-engineering/linguistic-rules.yaml +6 -0
- package/src/ontology/tests/catalogs.pbt.test.ts +89 -0
- package/src/ontology/tests/enums.test.ts +203 -0
- package/src/ontology/tests/sternsystem-owner.test.ts +63 -0
- package/src/ontology/vitest.config.ts +8 -0
- package/src/passport/dht-sign.test.ts +99 -0
- package/src/passport/dht-sign.ts +73 -0
- package/src/passport/identity-sign.test.ts +134 -0
- package/src/passport/identity-sign.ts +85 -0
- package/src/passport/index.ts +53 -0
- package/src/passport/key-rotate.ts +133 -0
- package/src/passport/schema.ts +198 -0
- package/src/passport/sign.ts +225 -0
- package/src/passport/tests/schema.pbt.test.ts +93 -0
- package/src/passport/tests/schema.test.ts +222 -0
- package/src/passport/vitest.config.ts +8 -0
- package/src/share/agent/api-catalog.ts +77 -0
- package/src/share/agent/capability.ts +68 -0
- package/src/share/agent/dns-aid.test.ts +74 -0
- package/src/share/agent/dns-aid.ts +53 -0
- package/src/share/agent/fleet-catalog.ts +149 -0
- package/src/share/agent/index.ts +29 -0
- package/src/share/agent/knowledge.ts +96 -0
- package/src/share/agent/manifest.ts +126 -0
- package/src/share/agent/mcp-card.ts +63 -0
- package/src/share/agent/omit-empty.ts +77 -0
- package/src/share/agent/openapi.ts +157 -0
- package/src/share/agent/proof.ts +61 -0
- package/src/share/attribution-display.ts +86 -0
- package/src/share/check-hints.ts +38 -0
- package/src/share/content/entity-id.ts +65 -0
- package/src/share/content/index.ts +16 -0
- package/src/share/content/merge.ts +86 -0
- package/src/share/content/resolve-field-path.ts +39 -0
- package/src/share/content/substitute-deep.ts +47 -0
- package/src/share/content-discipline/index.ts +14 -0
- package/src/share/content-discipline/parsers.ts +66 -0
- package/src/share/content-discipline/types.ts +118 -0
- package/src/share/content-reference.ts +333 -0
- package/src/share/counter-utils.ts +75 -0
- package/src/share/css-value-normalize.ts +48 -0
- package/src/share/dev-props-validator.ts +131 -0
- package/src/share/entitlement.ts +172 -0
- package/src/share/env.d.ts +73 -0
- package/src/share/feature-policy.ts +367 -0
- package/src/share/formula-eval.ts +364 -0
- package/src/share/fs/index.ts +96 -0
- package/src/share/i18n/localization.ts +57 -0
- package/src/share/image-provider.ts +314 -0
- package/src/share/index.ts +86 -0
- package/src/share/knowledge/claim.ts +129 -0
- package/src/share/knowledge/derivation.ts +55 -0
- package/src/share/knowledge/freshness.ts +127 -0
- package/src/share/knowledge/index.ts +23 -0
- package/src/share/knowledge/ledger.ts +222 -0
- package/src/share/knowledge/plan.ts +122 -0
- package/src/share/knowledge/source.ts +170 -0
- package/src/share/legal/translation-policy.ts +133 -0
- package/src/share/material-credits.ts +181 -0
- package/src/share/middleware/language-redirect.ts +49 -0
- package/src/share/offer-capacity.ts +149 -0
- package/src/share/onboarding-yaml/index.ts +162 -0
- package/src/share/page.ts +351 -0
- package/src/share/redirects.test.ts +62 -0
- package/src/share/redirects.ts +54 -0
- package/src/share/rfc0042-utils.ts +95 -0
- package/src/share/runtime-context.ts +70 -0
- package/src/share/schemas/claim-records.ts +69 -0
- package/src/share/schemas/claims.ts +150 -0
- package/src/share/schemas/effects.ts +212 -0
- package/src/share/schemas/features.ts +147 -0
- package/src/share/schemas/horizontal-align.ts +22 -0
- package/src/share/schemas/icon-color.ts +27 -0
- package/src/share/schemas/index.ts +43 -0
- package/src/share/schemas/live-photo.ts +53 -0
- package/src/share/schemas/material-credit.ts +292 -0
- package/src/share/schemas/media.ts +220 -0
- package/src/share/schemas/navigation.ts +74 -0
- package/src/share/schemas/page-base.ts +33 -0
- package/src/share/schemas/participant.ts +247 -0
- package/src/share/schemas/person.ts +24 -0
- package/src/share/schemas/print.ts +93 -0
- package/src/share/schemas/section-background.ts +106 -0
- package/src/share/schemas/section-body.ts +138 -0
- package/src/share/schemas/section-cards.ts +37 -0
- package/src/share/schemas/section-cta.ts +74 -0
- package/src/share/schemas/section-header.ts +77 -0
- package/src/share/schemas/section-image.ts +77 -0
- package/src/share/schemas/section-motion.ts +52 -0
- package/src/share/schemas/section-shell.ts +63 -0
- package/src/share/schemas/section-stats.ts +37 -0
- package/src/share/schemas/site-background.ts +78 -0
- package/src/share/schemas/standard-list-item.ts +38 -0
- package/src/share/scripts/external-links.ts +45 -0
- package/src/share/scripts/gsap-counter.ts +168 -0
- package/src/share/scripts/gsap-mountain-journey.ts +236 -0
- package/src/share/scripts/gsap-parallax.ts +50 -0
- package/src/share/scripts/gsap-reveal.ts +65 -0
- package/src/share/scripts/gsap-stagger.ts +54 -0
- package/src/share/scripts/index.ts +25 -0
- package/src/share/scripts/inline-number-animation.ts +120 -0
- package/src/share/scripts/lenis.ts +109 -0
- package/src/share/scripts/live-photos.ts +250 -0
- package/src/share/scripts/lordicon.ts +94 -0
- package/src/share/scripts/orchestrator.ts +138 -0
- package/src/share/scripts/scheduler.ts +53 -0
- package/src/share/scripts/video-player.ts +116 -0
- package/src/share/semantic/ai.ts +140 -0
- package/src/share/semantic/block-extraction.ts +52 -0
- package/src/share/semantic/block-extractors/index.ts +502 -0
- package/src/share/semantic/breadcrumbs.ts +120 -0
- package/src/share/semantic/build-page.ts +309 -0
- package/src/share/semantic/business-projection.ts +278 -0
- package/src/share/semantic/derived-prices-loader.ts +36 -0
- package/src/share/semantic/extract.ts +118 -0
- package/src/share/semantic/feed.ts +124 -0
- package/src/share/semantic/ids.ts +76 -0
- package/src/share/semantic/image-sitemap.ts +153 -0
- package/src/share/semantic/index.ts +36 -0
- package/src/share/semantic/jsonld/article.ts +47 -0
- package/src/share/semantic/jsonld/breadcrumb.ts +47 -0
- package/src/share/semantic/jsonld/collection-list.ts +30 -0
- package/src/share/semantic/jsonld/context.ts +46 -0
- package/src/share/semantic/jsonld/faq.ts +49 -0
- package/src/share/semantic/jsonld/initiative.ts +53 -0
- package/src/share/semantic/jsonld/organization.ts +101 -0
- package/src/share/semantic/jsonld/person.ts +71 -0
- package/src/share/semantic/jsonld/service.ts +80 -0
- package/src/share/semantic/jsonld/shared.ts +31 -0
- package/src/share/semantic/jsonld/types.ts +18 -0
- package/src/share/semantic/jsonld/webpage.ts +98 -0
- package/src/share/semantic/jsonld/website.ts +27 -0
- package/src/share/semantic/jsonld.ts +75 -0
- package/src/share/semantic/llms-policy.ts +74 -0
- package/src/share/semantic/llms.ts +394 -0
- package/src/share/semantic/markdown-hygiene.ts +68 -0
- package/src/share/semantic/markdown-twin-provenance.ts +242 -0
- package/src/share/semantic/models.ts +402 -0
- package/src/share/semantic/organization-profile.ts +160 -0
- package/src/share/semantic/output-projection.ts +131 -0
- package/src/share/semantic/page-builders/markdown-page.ts +84 -0
- package/src/share/semantic/page-markdown.ts +234 -0
- package/src/share/semantic/page-utils.ts +125 -0
- package/src/share/semantic/price-marker-resolver.ts +92 -0
- package/src/share/semantic/robots.ts +127 -0
- package/src/share/semantic/update-stamp.ts +136 -0
- package/src/share/shared-context.ts +177 -0
- package/src/share/string-utils.ts +19 -0
- package/src/share/tests/api-catalog.test.ts +97 -0
- package/src/share/tests/breadcrumbs.test.ts +126 -0
- package/src/share/tests/build-page-price-markers.test.ts +160 -0
- package/src/share/tests/capability.test.ts +107 -0
- package/src/share/tests/claim.test.ts +90 -0
- package/src/share/tests/content-ref-index.test.ts +328 -0
- package/src/share/tests/create-dev-props-validator.test.ts +38 -0
- package/src/share/tests/derivation.test.ts +39 -0
- package/src/share/tests/dev-props-validator.test.ts +88 -0
- package/src/share/tests/fleet-catalog.test.ts +134 -0
- package/src/share/tests/formula-eval.test.ts +619 -0
- package/src/share/tests/freshness.test.ts +60 -0
- package/src/share/tests/hero-section-numbering-parity.test.ts +51 -0
- package/src/share/tests/image-sitemap.test.ts +101 -0
- package/src/share/tests/jsonld-service.test.ts +282 -0
- package/src/share/tests/knowledge.test.ts +86 -0
- package/src/share/tests/ledger.test.ts +125 -0
- package/src/share/tests/llms-0789.test.ts +63 -0
- package/src/share/tests/manifest.test.ts +100 -0
- package/src/share/tests/markdown-twin-provenance.test.ts +108 -0
- package/src/share/tests/mcp-card.test.ts +57 -0
- package/src/share/tests/onboarding-yaml.test.ts +111 -0
- package/src/share/tests/ontology-role-by-cosmic-name.test.ts +28 -0
- package/src/share/tests/openapi.test.ts +116 -0
- package/src/share/tests/organization-jsonld.test.ts +74 -0
- package/src/share/tests/output-projection.test.ts +61 -0
- package/src/share/tests/plan.test.ts +97 -0
- package/src/share/tests/price-marker-resolver.test.ts +141 -0
- package/src/share/tests/proof.test.ts +72 -0
- package/src/share/tests/resolve-field-path.test.ts +52 -0
- package/src/share/tests/source.test.ts +81 -0
- package/src/share/tests/substitute-deep.test.ts +65 -0
- package/src/share/tests/text-normalize.pbt.test.ts +143 -0
- package/src/share/tests/text-normalize.test.ts +285 -0
- package/src/share/tests/twin-path.test.ts +43 -0
- package/src/share/text-normalize.ts +548 -0
- package/src/share/text-position.ts +19 -0
- package/src/share/types/hls-js-light.d.ts +16 -0
- package/src/share/visibility.ts +167 -0
- package/src/share/vitest.config.ts +8 -0
- package/src/share/wrap-inline-numbers.ts +139 -0
- package/src/surface/blueprint-schema.ts +314 -0
- package/src/surface/blueprint-types.ts +256 -0
- package/src/surface/blueprint.ts +300 -0
- package/src/surface/breaker.ts +57 -0
- package/src/surface/decision-composer.ts +156 -0
- package/src/surface/eligibility.ts +372 -0
- package/src/surface/evidence-records.ts +97 -0
- package/src/surface/fleet.ts +60 -0
- package/src/surface/geo.ts +170 -0
- package/src/surface/governance.ts +146 -0
- package/src/surface/index.ts +216 -0
- package/src/surface/io/index.ts +18 -0
- package/src/surface/io/surface-module-context-io.ts +60 -0
- package/src/surface/io/visibility-outcomes-io.ts +46 -0
- package/src/surface/module-context.ts +120 -0
- package/src/surface/substance.ts +161 -0
- package/src/surface/tests/eligibility.pbt.test.ts +137 -0
- package/src/surface/tests/eligibility.test.ts +302 -0
- package/src/surface/types.ts +289 -0
- package/src/surface/visibility.ts +57 -0
- package/src/surface/vitest.config.ts +8 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Animates .js-inline-number spans using GSAP count-up + ScrollTrigger. Deferred-loaded; never imported unconditionally.</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not apply opacity, y, scale, or any spatial tween to inline number spans — causes layout reflow.</item>
|
|
6
|
+
<item>Do not load gsap at module level — dynamic import is mandatory.</item>
|
|
7
|
+
<item>Do not animate numbers inside headings — those are excluded at SSR pre-wrap time.</item>
|
|
8
|
+
</non-goals>
|
|
9
|
+
</MODULE_CONTRACT>
|
|
10
|
+
<CHANGE_SUMMARY>
|
|
11
|
+
<item>RFC-0041: Created as shared GSAP inline number animation module in @warpgogol/werkstatt-shared/share/scripts.</item>
|
|
12
|
+
</CHANGE_SUMMARY>
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// @ai-invariant: RFC-0041. No spatial tweens on inline elements. Dynamic GSAP import only.
|
|
16
|
+
|
|
17
|
+
export interface InlineNumberAnimationOptions {
|
|
18
|
+
prefersReducedMotion?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Animates all `.js-inline-number` spans found in the document using GSAP count-up.
|
|
23
|
+
* Duration is read from each span's `data-duration` attribute (default 3.0 s).
|
|
24
|
+
* No spatial tweens — only the numeric value is animated to prevent CLS.
|
|
25
|
+
* Safe to call multiple times — spans are guarded with `data-gsap-ready`.
|
|
26
|
+
* No-ops when `prefersReducedMotion` is true (sets final values synchronously).
|
|
27
|
+
*/
|
|
28
|
+
export async function initInlineNumberAnimation(
|
|
29
|
+
options: InlineNumberAnimationOptions = {},
|
|
30
|
+
): Promise<void> {
|
|
31
|
+
const { prefersReducedMotion = false } = options;
|
|
32
|
+
|
|
33
|
+
const spans = document.querySelectorAll<HTMLElement>(".js-inline-number");
|
|
34
|
+
if (spans.length === 0) return;
|
|
35
|
+
|
|
36
|
+
// Locale from document lang attribute, fallback to de-DE
|
|
37
|
+
const locale = document.documentElement.lang || "de-DE";
|
|
38
|
+
|
|
39
|
+
const formatNumber = (value: number) =>
|
|
40
|
+
new Intl.NumberFormat(locale, {
|
|
41
|
+
minimumFractionDigits: 0,
|
|
42
|
+
maximumFractionDigits: 0,
|
|
43
|
+
}).format(Math.round(value));
|
|
44
|
+
|
|
45
|
+
// Reduced-motion: set final values immediately, no GSAP
|
|
46
|
+
if (prefersReducedMotion) {
|
|
47
|
+
spans.forEach((el) => {
|
|
48
|
+
if (el.dataset.gsapReady === "true") return;
|
|
49
|
+
el.dataset.gsapReady = "true";
|
|
50
|
+
const numeric = Number(el.dataset.numeric ?? 0);
|
|
51
|
+
el.textContent = formatNumber(numeric);
|
|
52
|
+
});
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Dynamically import GSAP — excluded from orchestrator's initial bundle
|
|
57
|
+
const { gsap } = await import("gsap");
|
|
58
|
+
const { ScrollTrigger } = await import("gsap/ScrollTrigger");
|
|
59
|
+
gsap.registerPlugin(ScrollTrigger);
|
|
60
|
+
|
|
61
|
+
// Wire ScrollTrigger to Lenis: on every Lenis scroll tick, feed the current
|
|
62
|
+
// scroll position into ScrollTrigger so it doesn't rely on native scroll events.
|
|
63
|
+
type LenisScrollEvent = { scroll: number };
|
|
64
|
+
type LenisInstance = { on: (event: string, cb: (e: LenisScrollEvent) => void) => void };
|
|
65
|
+
|
|
66
|
+
const wireLenis = (lenis: LenisInstance) => {
|
|
67
|
+
lenis.on("scroll", (e: LenisScrollEvent) => {
|
|
68
|
+
void e; // position already reflected in window.scrollY by Lenis native scroll
|
|
69
|
+
ScrollTrigger.update();
|
|
70
|
+
});
|
|
71
|
+
// Defer refresh to next rAF so layout is fully settled before positions are measured
|
|
72
|
+
requestAnimationFrame(() => ScrollTrigger.refresh());
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const existingLenis = (window as unknown as Record<string, unknown>)["wgLenis"] as
|
|
76
|
+
LenisInstance | undefined;
|
|
77
|
+
|
|
78
|
+
if (existingLenis) {
|
|
79
|
+
wireLenis(existingLenis);
|
|
80
|
+
} else {
|
|
81
|
+
window.addEventListener(
|
|
82
|
+
"lenis:ready",
|
|
83
|
+
(e) => {
|
|
84
|
+
const { lenis } = (e as CustomEvent<{ lenis: LenisInstance }>).detail;
|
|
85
|
+
if (lenis) wireLenis(lenis);
|
|
86
|
+
},
|
|
87
|
+
{ once: true },
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
spans.forEach((el) => {
|
|
92
|
+
if (el.dataset.gsapReady === "true") return;
|
|
93
|
+
el.dataset.gsapReady = "true";
|
|
94
|
+
|
|
95
|
+
const endValue = Number(el.dataset.numeric ?? 0);
|
|
96
|
+
const duration = Number(el.dataset.duration ?? 3.0);
|
|
97
|
+
|
|
98
|
+
const counter = { value: 0 };
|
|
99
|
+
|
|
100
|
+
// Only count-up tween — NO spatial tweens (y/opacity) on inline elements
|
|
101
|
+
gsap.to(counter, {
|
|
102
|
+
value: endValue,
|
|
103
|
+
duration,
|
|
104
|
+
ease: "power2.out",
|
|
105
|
+
scrollTrigger: {
|
|
106
|
+
trigger: el,
|
|
107
|
+
start: "top 85%",
|
|
108
|
+
once: true,
|
|
109
|
+
fastScrollEnd: true,
|
|
110
|
+
},
|
|
111
|
+
onUpdate: () => {
|
|
112
|
+
el.textContent = formatNumber(counter.value);
|
|
113
|
+
},
|
|
114
|
+
onComplete: () => {
|
|
115
|
+
// Ensure exact final value (avoids floating-point display artifacts)
|
|
116
|
+
el.textContent = formatNumber(endValue);
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Maintains packages/share/src/scripts/lenis.ts as an authored share script module so agents can evolve it without rediscovering local boundaries.</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not manage app-specific UI states (e.g. scroll-to-top button visibility).</item>
|
|
6
|
+
</non-goals>
|
|
7
|
+
</MODULE_CONTRACT>
|
|
8
|
+
<CHANGE_SUMMARY>
|
|
9
|
+
<item>Migrated Lenis initialization to @warpgogol/werkstatt-shared/share/scripts for platform-wide reuse.</item>
|
|
10
|
+
</CHANGE_SUMMARY>
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// @ai-invariant: This is a high-risk module. Preserve its core logic and minimize external side effects during modification.
|
|
14
|
+
|
|
15
|
+
export type LenisInstance = import("lenis").default;
|
|
16
|
+
|
|
17
|
+
export type InitLenisOptions = {
|
|
18
|
+
prefersReducedMotion?: boolean;
|
|
19
|
+
headerOffset?: number;
|
|
20
|
+
duration?: number;
|
|
21
|
+
onScroll?: (lenis: LenisInstance) => void;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
let lenisInstance: LenisInstance | null = null;
|
|
25
|
+
|
|
26
|
+
export async function initLenis(
|
|
27
|
+
options: InitLenisOptions = {},
|
|
28
|
+
): Promise<LenisInstance | undefined> {
|
|
29
|
+
const { prefersReducedMotion = false, headerOffset = 0, duration = 1.2, onScroll } = options;
|
|
30
|
+
|
|
31
|
+
if (prefersReducedMotion) {
|
|
32
|
+
(window as Window & { wgLenis?: LenisInstance }).wgLenis = undefined;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const Lenis = await import("lenis");
|
|
37
|
+
|
|
38
|
+
lenisInstance = new Lenis.default({
|
|
39
|
+
duration,
|
|
40
|
+
easing: (t: number) => Math.min(1, 1.001 - 2 ** (-10 * t)),
|
|
41
|
+
orientation: "vertical",
|
|
42
|
+
gestureOrientation: "vertical",
|
|
43
|
+
smoothWheel: true,
|
|
44
|
+
wheelMultiplier: 1,
|
|
45
|
+
touchMultiplier: 2,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
(window as Window & { wgLenis?: LenisInstance }).wgLenis = lenisInstance;
|
|
49
|
+
|
|
50
|
+
if (onScroll) {
|
|
51
|
+
lenisInstance.on("scroll", () => onScroll(lenisInstance!));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
window.dispatchEvent(
|
|
55
|
+
new CustomEvent("lenis:ready", {
|
|
56
|
+
detail: { lenis: lenisInstance },
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const activeLenis = lenisInstance;
|
|
61
|
+
|
|
62
|
+
// Handle anchor links for smooth scrolling
|
|
63
|
+
document.addEventListener("click", (e) => {
|
|
64
|
+
const target = e.target as HTMLElement;
|
|
65
|
+
const link = target.closest('a[href^="#"]');
|
|
66
|
+
|
|
67
|
+
if (link) {
|
|
68
|
+
const href = link.getAttribute("href");
|
|
69
|
+
if (href && href !== "#") {
|
|
70
|
+
const targetElement = document.querySelector(href);
|
|
71
|
+
if (targetElement) {
|
|
72
|
+
// Skip if it's a modal or something that should handle itself
|
|
73
|
+
if (targetElement.classList.contains("wl-modal")) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
e.preventDefault();
|
|
78
|
+
activeLenis.scrollTo(targetElement as HTMLElement, {
|
|
79
|
+
offset: -headerOffset,
|
|
80
|
+
immediate: prefersReducedMotion,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Scroll to hash target on page load (cross-page anchor navigation)
|
|
88
|
+
if (window.location.hash) {
|
|
89
|
+
const hashTarget = document.querySelector(window.location.hash);
|
|
90
|
+
if (hashTarget && !hashTarget.classList.contains("wl-modal")) {
|
|
91
|
+
// Defer until after the browser's initial scroll attempt
|
|
92
|
+
requestAnimationFrame(() => {
|
|
93
|
+
activeLenis.scrollTo(hashTarget as HTMLElement, {
|
|
94
|
+
offset: -headerOffset,
|
|
95
|
+
immediate: prefersReducedMotion,
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function raf(time: number) {
|
|
102
|
+
activeLenis.raf(time);
|
|
103
|
+
requestAnimationFrame(raf);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
requestAnimationFrame(raf);
|
|
107
|
+
|
|
108
|
+
return activeLenis;
|
|
109
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>
|
|
4
|
+
[RFC-0202] Living-photos runtime. Wires the `in-viewport` (IntersectionObserver) and `tap`
|
|
5
|
+
(button) playback triggers for [data-live-photo] elements emitted by <LivePhoto>. No GSAP, no
|
|
6
|
+
framework — a few KB of vanilla DOM. The `autoplay` trigger is native and is never touched
|
|
7
|
+
here (those elements carry no data-trigger). Respects prefers-reduced-motion: non-interactive
|
|
8
|
+
playback is suppressed (static poster wins); a user tap always wins.
|
|
9
|
+
</purpose>
|
|
10
|
+
<non-goals>
|
|
11
|
+
<item>Do not handle the autoplay (native) path — those elements have no data-trigger.</item>
|
|
12
|
+
<item>Do not resolve clips or own presentation — that is @warpgogol/werkstatt-site/ui.</item>
|
|
13
|
+
</non-goals>
|
|
14
|
+
</MODULE_CONTRACT>
|
|
15
|
+
<CHANGE_SUMMARY>
|
|
16
|
+
<item>RFC-0202: introduced the living-photos runtime.</item>
|
|
17
|
+
</CHANGE_SUMMARY>
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export interface LivePhotosOptions {
|
|
21
|
+
prefersReducedMotion?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const PLAYING = "live-photo--playing";
|
|
25
|
+
const USER_PLAYING = "live-photo--user-playing";
|
|
26
|
+
const READY = "live-photo--ready";
|
|
27
|
+
const UNSUPPORTED = "live-photo--unsupported";
|
|
28
|
+
|
|
29
|
+
/** Cached WebM support check — true if browser claims VP8/VP9 support. */
|
|
30
|
+
let webmSupportCache: boolean | undefined;
|
|
31
|
+
|
|
32
|
+
function isIOSLike(): boolean {
|
|
33
|
+
return (
|
|
34
|
+
/iPad|iPhone|iPod/.test(navigator.userAgent) ||
|
|
35
|
+
(navigator.platform === "MacIntel" && "ontouchend" in document)
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Detect WebM support via canPlayType. Now more optimistic since we have MP4 fallbacks. */
|
|
40
|
+
function detectWebMSupport(): boolean {
|
|
41
|
+
if (webmSupportCache !== undefined) return webmSupportCache;
|
|
42
|
+
|
|
43
|
+
const video = document.createElement("video");
|
|
44
|
+
const canPlay =
|
|
45
|
+
video.canPlayType('video/webm; codecs="vp8, vorbis"') !== "" ||
|
|
46
|
+
video.canPlayType('video/webm; codecs="vp9"') !== "" ||
|
|
47
|
+
video.canPlayType("video/webm") !== "";
|
|
48
|
+
|
|
49
|
+
// Check if MP4 is supported as fallback (almost universal)
|
|
50
|
+
const canPlayMP4 =
|
|
51
|
+
video.canPlayType('video/mp4; codecs="avc1.42E01E"') !== "" ||
|
|
52
|
+
video.canPlayType("video/mp4") !== "";
|
|
53
|
+
|
|
54
|
+
// If we have MP4 support, we can be less strict about WebM
|
|
55
|
+
// iOS will get MP4 fallback, so we don't need to mark as unsupported
|
|
56
|
+
webmSupportCache = canPlay || canPlayMP4;
|
|
57
|
+
return webmSupportCache;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Mark a living photo as unsupported — hide video, keep poster visible. */
|
|
61
|
+
function markUnsupported(root: HTMLElement, video: HTMLVideoElement): void {
|
|
62
|
+
root.classList.add(UNSUPPORTED);
|
|
63
|
+
root.classList.remove(PLAYING, USER_PLAYING, READY);
|
|
64
|
+
video.style.display = "none";
|
|
65
|
+
// Hide the toggle button on unsupported devices
|
|
66
|
+
const toggle = root.querySelector<HTMLButtonElement>(".live-photo__toggle");
|
|
67
|
+
if (toggle) toggle.style.display = "none";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function setReady(root: HTMLElement, ready: boolean): void {
|
|
71
|
+
if (ready) {
|
|
72
|
+
root.classList.add(READY);
|
|
73
|
+
} else {
|
|
74
|
+
root.classList.remove(READY);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function ensureVideoSources(video: HTMLVideoElement): void {
|
|
79
|
+
let changed = false;
|
|
80
|
+
for (const source of Array.from(video.querySelectorAll<HTMLSourceElement>("source[data-src]"))) {
|
|
81
|
+
const src = source.dataset.src;
|
|
82
|
+
if (!src || source.getAttribute("src") === src) continue;
|
|
83
|
+
source.setAttribute("src", src);
|
|
84
|
+
changed = true;
|
|
85
|
+
}
|
|
86
|
+
if (changed) video.load();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function attemptPlay(root: HTMLElement, video: HTMLVideoElement, _byUser: boolean): void {
|
|
90
|
+
ensureVideoSources(video);
|
|
91
|
+
const canPlay = video.readyState >= 3; // HAVE_FUTURE_DATA
|
|
92
|
+
|
|
93
|
+
if (!canPlay) {
|
|
94
|
+
// Wait for video to be ready before playing (prevents iOS black flash)
|
|
95
|
+
video.addEventListener(
|
|
96
|
+
"canplaythrough",
|
|
97
|
+
() => {
|
|
98
|
+
setReady(root, true);
|
|
99
|
+
void video.play().catch(() => handlePlayFailure(root));
|
|
100
|
+
},
|
|
101
|
+
{ once: true },
|
|
102
|
+
);
|
|
103
|
+
// Also listen for error — if video can't load, revert to poster
|
|
104
|
+
video.addEventListener(
|
|
105
|
+
"error",
|
|
106
|
+
() => {
|
|
107
|
+
handlePlayFailure(root);
|
|
108
|
+
},
|
|
109
|
+
{ once: true },
|
|
110
|
+
);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
setReady(root, true);
|
|
115
|
+
void video.play().catch(() => handlePlayFailure(root));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function handlePlayFailure(root: HTMLElement): void {
|
|
119
|
+
// Play policy rejected (e.g., iOS without user gesture) or video error — restore poster
|
|
120
|
+
root.classList.remove(PLAYING, USER_PLAYING, READY);
|
|
121
|
+
const toggle = root.querySelector<HTMLButtonElement>(".live-photo__toggle");
|
|
122
|
+
if (toggle) toggle.setAttribute("aria-pressed", "false");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function setPlaying(
|
|
126
|
+
root: HTMLElement,
|
|
127
|
+
video: HTMLVideoElement,
|
|
128
|
+
on: boolean,
|
|
129
|
+
byUser: boolean,
|
|
130
|
+
): void {
|
|
131
|
+
if (on) {
|
|
132
|
+
root.classList.add(PLAYING);
|
|
133
|
+
if (byUser) root.classList.add(USER_PLAYING);
|
|
134
|
+
attemptPlay(root, video, byUser);
|
|
135
|
+
} else {
|
|
136
|
+
root.classList.remove(PLAYING, USER_PLAYING, READY);
|
|
137
|
+
video.pause();
|
|
138
|
+
}
|
|
139
|
+
const toggle = root.querySelector<HTMLButtonElement>(".live-photo__toggle");
|
|
140
|
+
if (toggle) toggle.setAttribute("aria-pressed", String(root.classList.contains(PLAYING)));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function initLivePhotos(options: LivePhotosOptions = {}): Promise<void> {
|
|
144
|
+
const prefersReducedMotion =
|
|
145
|
+
options.prefersReducedMotion ?? window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
146
|
+
|
|
147
|
+
const roots = document.querySelectorAll<HTMLElement>("[data-live-photo][data-trigger]");
|
|
148
|
+
if (roots.length === 0) return;
|
|
149
|
+
|
|
150
|
+
// One shared observer for all in-viewport clips.
|
|
151
|
+
const observed = new Map<Element, HTMLVideoElement>();
|
|
152
|
+
const observer =
|
|
153
|
+
"IntersectionObserver" in window
|
|
154
|
+
? new IntersectionObserver(
|
|
155
|
+
(entries) => {
|
|
156
|
+
for (const entry of entries) {
|
|
157
|
+
const video = observed.get(entry.target);
|
|
158
|
+
if (!video) continue;
|
|
159
|
+
const isIntersecting = entry.isIntersecting;
|
|
160
|
+
// iOS Safari respects autoplay attribute more than programmatic play()
|
|
161
|
+
if (isIntersecting) {
|
|
162
|
+
video.setAttribute("autoplay", "");
|
|
163
|
+
} else {
|
|
164
|
+
video.removeAttribute("autoplay");
|
|
165
|
+
}
|
|
166
|
+
setPlaying(entry.target as HTMLElement, video, isIntersecting, false);
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
{ threshold: 0.25 },
|
|
170
|
+
)
|
|
171
|
+
: null;
|
|
172
|
+
|
|
173
|
+
for (const root of Array.from(roots)) {
|
|
174
|
+
if (root.dataset.livePhotoReady === "true") continue;
|
|
175
|
+
root.dataset.livePhotoReady = "true";
|
|
176
|
+
|
|
177
|
+
const video = root.querySelector<HTMLVideoElement>(".live-photo__video");
|
|
178
|
+
if (!video) continue;
|
|
179
|
+
|
|
180
|
+
// Check WebM support early — if unsupported, hide video and keep poster
|
|
181
|
+
if (!detectWebMSupport()) {
|
|
182
|
+
markUnsupported(root, video);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const mp4Src = root.dataset.mp4Src;
|
|
187
|
+
|
|
188
|
+
// iOS Safari decodes VP8/VP9 WebM but ignores its alpha channel: a transparent
|
|
189
|
+
// living photo plays with a dirty opaque background instead of the cut-out subject.
|
|
190
|
+
// (Other browsers honour VP9 alpha and render it correctly.) These decorative
|
|
191
|
+
// portraits ship WebM-only — there is no iOS-playable alpha source — so the only
|
|
192
|
+
// correct iOS behaviour is to keep the transparent static poster. When an explicit
|
|
193
|
+
// MP4 fallback is authored (opaque clips), we still let iOS play it below.
|
|
194
|
+
if (isIOSLike() && !mp4Src) {
|
|
195
|
+
markUnsupported(root, video);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (mp4Src && isIOSLike()) {
|
|
200
|
+
const firstSource = video.querySelector<HTMLSourceElement>("source");
|
|
201
|
+
if (firstSource) {
|
|
202
|
+
firstSource.dataset.src = mp4Src;
|
|
203
|
+
firstSource.setAttribute("type", "video/mp4");
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const trigger = root.dataset.trigger;
|
|
208
|
+
const tapBehavior = root.dataset.tapBehavior ?? "toggle";
|
|
209
|
+
const toggle = root.querySelector<HTMLButtonElement>(".live-photo__toggle");
|
|
210
|
+
|
|
211
|
+
// Control: user-initiated, always permitted (even under reduced motion).
|
|
212
|
+
if (toggle) {
|
|
213
|
+
toggle.addEventListener("click", () => {
|
|
214
|
+
const playing = root.classList.contains(PLAYING);
|
|
215
|
+
if (playing && tapBehavior === "toggle") {
|
|
216
|
+
setPlaying(root, video, false, false);
|
|
217
|
+
} else {
|
|
218
|
+
// For iOS: ensure autoplay attribute is set before user-triggered play
|
|
219
|
+
video.setAttribute("autoplay", "");
|
|
220
|
+
setPlaying(root, video, true, true);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Video load error handling — iOS may fail to decode WebM
|
|
226
|
+
video.addEventListener("error", () => {
|
|
227
|
+
// Video failed to load/decode — ensure poster remains visible
|
|
228
|
+
root.classList.remove(PLAYING, USER_PLAYING, READY);
|
|
229
|
+
if (!mp4Src) video.style.display = "none";
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Metadata loaded — we know dimensions but may not be ready to play yet
|
|
233
|
+
video.addEventListener("loadedmetadata", () => {
|
|
234
|
+
// Video metadata available, but we wait for canplaythrough before marking ready
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// in-viewport auto playback — suppressed under reduced motion (poster stays).
|
|
238
|
+
if (trigger === "in-viewport" && !prefersReducedMotion && observer) {
|
|
239
|
+
observed.set(root, video);
|
|
240
|
+
observer.observe(root);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Pause when tab is hidden (mobile UX + power saving)
|
|
244
|
+
document.addEventListener("visibilitychange", () => {
|
|
245
|
+
if (document.hidden && root.classList.contains(PLAYING)) {
|
|
246
|
+
setPlaying(root, video, false, false);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// @ai-invariant: This is a high-risk module. Preserve its core logic and minimize external side effects during modification.
|
|
2
|
+
/*************** <MODULE_CONTRACT>
|
|
3
|
+
<purpose>Facilitates the dynamic loading and hydration of Lordicon elements based on viewport visibility.</purpose>
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
<non-goals>
|
|
7
|
+
<item>Do not handle raw content parsing for icons.</item>
|
|
8
|
+
<item>Do not manage global application state or configuration.</item>
|
|
9
|
+
<item>Do not perform network requests outside of JSON prefetching.</item>
|
|
10
|
+
</non-goals>
|
|
11
|
+
</MODULE_CONTRACT>
|
|
12
|
+
|
|
13
|
+
<CHANGE_SUMMARY>
|
|
14
|
+
<item>Tidied by compass.changesummary.tidy; see git history for prior entries.</item>
|
|
15
|
+
</CHANGE_SUMMARY> ***************/
|
|
16
|
+
|
|
17
|
+
async function initLordIcon(): Promise<void> {
|
|
18
|
+
const { defineElement } = await import("@lordicon/element");
|
|
19
|
+
defineElement();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let lordIconLoaderPromise: Promise<void> | null = null;
|
|
23
|
+
|
|
24
|
+
const jsonPrefetchCache = new Map<string, Promise<void>>();
|
|
25
|
+
|
|
26
|
+
const ensureLordIconLoaded = () => {
|
|
27
|
+
if (!lordIconLoaderPromise) {
|
|
28
|
+
lordIconLoaderPromise = new Promise((resolve) => {
|
|
29
|
+
void (async () => {
|
|
30
|
+
const { scheduleTask } = await import("./scheduler.ts");
|
|
31
|
+
scheduleTask(async () => {
|
|
32
|
+
await initLordIcon();
|
|
33
|
+
resolve();
|
|
34
|
+
});
|
|
35
|
+
})();
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return lordIconLoaderPromise;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const prefetchJsonOnce = (url: string) => {
|
|
42
|
+
const existing = jsonPrefetchCache.get(url);
|
|
43
|
+
if (existing) return existing;
|
|
44
|
+
|
|
45
|
+
const p = fetch(url, { cache: "force-cache" })
|
|
46
|
+
.then(() => {})
|
|
47
|
+
.catch(() => {});
|
|
48
|
+
jsonPrefetchCache.set(url, p);
|
|
49
|
+
return p;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export function initLordIconOnDemand(): void {
|
|
53
|
+
const icons = Array.from(document.querySelectorAll("lord-icon"));
|
|
54
|
+
if (!icons.length) return;
|
|
55
|
+
|
|
56
|
+
const hydrateIcon = async (icon: HTMLElement) => {
|
|
57
|
+
// 1. Ensure library is loaded/loading
|
|
58
|
+
ensureLordIconLoaded();
|
|
59
|
+
|
|
60
|
+
// 2. Swap src if needed
|
|
61
|
+
const src = icon.getAttribute("data-src");
|
|
62
|
+
if (src && !icon.getAttribute("src")) {
|
|
63
|
+
void prefetchJsonOnce(src);
|
|
64
|
+
icon.setAttribute("src", src);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
if (typeof IntersectionObserver !== "function") {
|
|
69
|
+
// Fallback: load everything immediately
|
|
70
|
+
ensureLordIconLoaded();
|
|
71
|
+
icons.forEach((icon) => {
|
|
72
|
+
const src = icon.getAttribute("data-src");
|
|
73
|
+
if (src) icon.setAttribute("src", src);
|
|
74
|
+
});
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const observer = new IntersectionObserver(
|
|
79
|
+
(entries) => {
|
|
80
|
+
for (const entry of entries) {
|
|
81
|
+
if (entry.isIntersecting) {
|
|
82
|
+
const icon = entry.target as HTMLElement;
|
|
83
|
+
hydrateIcon(icon);
|
|
84
|
+
observer.unobserve(icon);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
{ rootMargin: "100px" }, // Smaller margin to defer loading of far-off icons
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
for (const icon of icons) {
|
|
92
|
+
observer.observe(icon);
|
|
93
|
+
}
|
|
94
|
+
}
|