From 7427d808d7f8fdb7866bef917fc27ef3cffdce48 Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Wed, 4 Nov 2026 03:41:16 +0100 Subject: [PATCH 01/55] docs: bootstrap the proposed shared UI specification catalog --- README.md | 12 +- docs/component-library.md | 94 +++++++ specs/CATALOG.md | 35 +++ specs/README.md | 257 +++++++++++++++++ specs/_template.md | 372 +++++++++++++++++++++++++ specs/catalog.json | 37 +++ specs/shared-ui-foundation.md | 506 ++++++++++++++++++++++++++++++++++ 7 files changed, 1309 insertions(+), 4 deletions(-) create mode 100644 docs/component-library.md create mode 100644 specs/CATALOG.md create mode 100644 specs/README.md create mode 100644 specs/_template.md create mode 100644 specs/catalog.json create mode 100644 specs/shared-ui-foundation.md diff --git a/README.md b/README.md index 2099fd0..6e50062 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,12 @@ Shared presentation components for Home Assistant applications, including Gateway and Domus. -The package will own reusable Lit components, design tokens, tests and a Storybook catalog. -Each consuming application owns its domain logic, navigation content and workflows. +The proposed library has one Lit component implementation, direct Lit/HTML consumption and real +React, Vue, Angular and Svelte adapters. Each app owns its data, permissions, routes and workflows. +One Storybook will document the same components and their accessibility/theme/localization states. -Repository setup and the initial component extraction are in progress. No package or catalog -has been published yet. +The repository is configured; product implementation and publication have not started. +Start with the [component-library guide](docs/component-library.md), +[primary Draft](specs/shared-ui-foundation.md), [specification standard](specs/README.md) +and [capability catalog](specs/CATALOG.md). The [implementation issue](https://github.com/vypdev/ha-ui/issues/7) +retains the full scope and pending native specification gate. diff --git a/docs/component-library.md b/docs/component-library.md new file mode 100644 index 0000000..85c5c94 --- /dev/null +++ b/docs/component-library.md @@ -0,0 +1,94 @@ +# Shared component library + +The library is being specified. No core package, adapter, Storybook site or consumer migration +is implemented or published yet. The [primary Draft](../specs/shared-ui-foundation.md), +[capability catalog](../specs/CATALOG.md) and [feature issue #7](https://github.com/vypdev/ha-ui/issues/7) +hold the proposed contract and pending acceptance gates. + +## Intended use + +One Lit implementation exposes DOM components. Apps choose direct Lit/HTML use or a real React, +Vue, Angular or Svelte adapter. Proposed package names are @vypdev/ha-ui and the corresponding +-react, -vue, -angular and -svelte packages; registry rights and actual exports remain unverified. +A consumer installs only its own framework binding. Framework-native forms, events, complex values, +content projection and teardown require compiled tests, not just custom-element declarations. + +```text +One core -> selected framework binding -> app-owned pages and workflows + -> shared production-backed Storybook +``` + +Text equivalent: One core supplies the same visual behavior to a chosen framework and the catalog. +Gateway and Domus retain separate page composition, domain rules, data, permissions and routes. +Their shared navigation pattern is the upper HA-style dashboard bar. Domus still chooses household +sections such as Overview, Tasks, Calendar, Shopping, Inventory, Family and Settings as they become real. + +## Components and catalog + +The proposed families are tokens, typography, spacing, upper navigation, buttons, fields, selects, +checkboxes, cards, rows/grids, dialogs and feedback states. The one Storybook will use production +components with EN/ES, light/dark, narrow/wide, keyboard and reduced-motion examples. It will provide +reviewable PR artifacts and its own static GitHub Pages site; no site URL is claimed before delivery. +No private HA bundles, parent-frame DOM, storage, credentials or API access belong in this package. + +## Compatibility and adoption + +Exact supported versions, peer ranges, signatures and export paths are pending engineering verification. +Next.js, Nuxt and SvelteKit need explicit tested client/import boundaries; a generic Web Component +claim does not prove SSR or hydration support. The initial goal is a server shell with client-mounted +components, not universal server rendering of Shadow DOM. + +Before extraction, verify packed consumers for Lit/DOM and all four adapters. Then pin the Gateway +source/license baseline, retain MIT and asset notices, and extract behavior once. Actual Gateway and +Domus migrations happen under their own admitted repository work, using the same reviewed cohort. +Retire local duplicates only after equivalent app, recovery, locale and accessibility journeys pass. +Core/adapter publication, Storybook delivery and each app's acceptance are distinct gates. + +## Quality and release + +All authored behavior starts with tests and requires 100% supported statements, branches, functions +and lines globally and per module, with independent source inventory and executable architecture +constraints. The primary Draft allocates 64 distinct minimum cases. Future CI Check must aggregate +all required checks on every PR head; this documentation bootstrap does not create that workflow. +Record exact toolchain, source, lock, package digests and framework/browser results. Verify npm +publication rights before any publish. Preserve one tested previous app/cohort rollback and all user +data and original-operation recovery. Never overwrite an existing package version after partial delivery. + +## Catalog validation + +Bootstrap uses the unchanged original Copilot catalog SDK from the official source revision +`3e7ceea08e6131360e026937d1c0cc1b120472e0` used by the installed Action at preparation time. +The following three source files were byte-verified against that revision in an existing checkout: + +| Original SDK file | SHA-256 | +|---|---| +| src/infrastructure/specification_catalog_validator.cjs | 7de4c036e5dee6477581502984d02037e6fd3bd3fa2f2434ef4dc862666365b1 | +| src/infrastructure/specification_catalog_publication.cjs | 92d31ff9a9ff7a9f8491dc492ccf7e1b039ba5d6ff0da43e7f252111b7e76d5b | +| src/domain/specification_catalog_path_policy.cjs | bb97e2432d97c61b5cc123adcdb43acc49f01d3bb52693d372c37829133ab139 | + +These are external tool identities, not copied files or ha-ui implementation paths. No Copilot +source, generated workflow or installed resource is modified. For this bootstrap, use a reviewed +read-only Copilot checkout and an absolute target root. `COPILOT_SOURCE_ROOT` and `HA_UI_ROOT` below +are operator-selected absolute paths; the three files must match the verified source before use. +From the ha-ui repository, run with Node 24 or newer: + +```bash +export HA_UI_ROOT="$PWD" +export COPILOT_SOURCE_ROOT="/absolute/path/to/reviewed/copilot" +node -e 'const path = require("node:path"); const sdk = require(path.join(process.env.COPILOT_SOURCE_ROOT, "src/infrastructure/specification_catalog_validator.cjs")); sdk.main(["--write"], process.env.HA_UI_ROOT);' +node -e 'const path = require("node:path"); const sdk = require(path.join(process.env.COPILOT_SOURCE_ROOT, "src/infrastructure/specification_catalog_validator.cjs")); sdk.main([], process.env.HA_UI_ROOT);' +``` + +Generation writes only the target generated index. Validation checks actual path/cardinality and +index agreement; it does not establish semantic completeness, native feature admission, implemented +behavior or release readiness. Do not run an absolute SDK CLI without the explicit target root: +its default root belongs to the external tool checkout. Repository-local reproducible validation +scripts and CI are feature implementation deliverables. Do not invent or weaken a replacement +validator to turn missing evidence into acceptance. + +## Next transition + +Merge the reviewed documentation prerequisite, then retry existing issue #7 against the changed +trusted source. The native Action must clarify and publish the feature SDD and expose its exact +ready branch before implementation. The standard/template describe the expected format; this +bootstrap does not set a native readiness marker or bypass that lifecycle. diff --git a/specs/CATALOG.md b/specs/CATALOG.md new file mode 100644 index 0000000..cae5998 --- /dev/null +++ b/specs/CATALOG.md @@ -0,0 +1,35 @@ +# Product capability specification catalog + +> Generated from [`catalog.json`](./catalog.json). Do not edit this table by hand. +> Run `pnpm run generate:specifications` after changing catalog metadata. + +This catalog answers which product contract owns a capability and where its +implementation, verification, workflow, and user-documentation evidence lives. +An **As-built baseline** records verified current behavior; it does not hide known +debt or convert unknown historic intent into a design decision. + +| Capability ID | Status | Scope | Primary SDD | Evidence | +|---|---|---|---|---| +| `shared-ui-foundation` | Proposed | One DOM component implementation with React, Vue, Angular and Svelte adapters, direct Lit/DOM use, shared Storybook and verified Gateway/Domus adoption. Draft; no runtime implementation or delivery accepted. | [Shared Home Assistant presentation components and framework adapters](./shared-ui-foundation.md) | 3 paths · 2026-11-04 | + +## Evidence map + +### `shared-ui-foundation` — Shared Home Assistant presentation components and framework adapters + +- Owner: vypdev maintainers; Efrain Espada (accountable product/publication owner) +- Last verified: 2026-11-04 +- Specifications: [`specs/shared-ui-foundation.md`](./shared-ui-foundation.md) +- Workflows: [`.github/workflows/copilot_issue.yml`](../.github/workflows/copilot_issue.yml) +- Entrypoints: Not yet implemented (proposed capability). +- Core code: Not yet implemented (proposed capability). +- Tests: Not yet implemented (proposed capability). +- User documentation: [`README.md`](../README.md) · [`docs/component-library.md`](../docs/component-library.md) + +## Maintenance contract + +1. Read the relevant SDD before changing a catalogued capability. +2. Change the SDD, catalog evidence, tests, and user documentation together when + behavior or an architecture boundary changes. +3. Use repository-relative paths in `catalog.json`; each path is validated and every + top-level product SDD must have exactly one capability owner. +4. Run `pnpm run validate:specifications` in local and CI validation. diff --git a/specs/README.md b/specs/README.md new file mode 100644 index 0000000..79d6b54 --- /dev/null +++ b/specs/README.md @@ -0,0 +1,257 @@ +# Product Specification Standard + +This directory contains implementation-driving product and engineering +specifications. A spec is a shared product contract, not only a technical design. +It must make the intended behavior understandable to someone who did not take +part in the original discussion and testable by someone who did not implement it. + +New specifications start from [`_template.md`](./_template.md). Existing specs +should converge on this standard when they are materially revised. + +The capability inventory and its implementation evidence live in +[`catalog.json`](./catalog.json); [`CATALOG.md`](./CATALOG.md) is the generated +human-readable view. Use the original Copilot SDK catalog generator and validator after changing a +specification or catalogued path. The current bootstrap procedure and exact SDK +identity are documented in [the component-library guide](../docs/component-library.md#catalog-validation). +This repository does not yet have package scripts or an application CI workflow. +Adding reproducible repository-local validation is an implementation deliverable, +not a claim established by this documentation bootstrap. + +### Retrospective (as-built) specifications + +An already implemented capability MAY be documented with status +`As-built baseline`. This is not a claim that the feature is perfect or that all +historic design choices were intentional. Its purpose is to establish a +reviewable contract before subsequent changes. + +An as-built baseline MUST: + +- label verified repository behavior as **observed behavior**; +- label the behavior maintainers choose to preserve as the **intentional + contract**; +- record defects, missing evidence, and architectural weaknesses under **known + debt and limitations**; +- mark historic motivation as **unknown** when evidence does not establish it; +- keep desired improvements separate from the current contract and link them to + follow-up work rather than presenting them as implemented; +- cite current code, workflows, tests, and user documentation; and +- use the same UX, architecture, configuration, security, testing, + documentation, acceptance, and Definition of Done standard as a prospective + specification. + +The baseline date and `Last verified` catalog date identify the repository +snapshot that was audited. A behavior-changing implementation MUST update the +relevant SDD, catalog evidence, tests, and user documentation in the same change. + +## 1. Core criteria + +Every spec must be: + +- **Product-near:** use the names, states, actions, and surfaces that users and + operators will actually see. +- **Visual where it helps:** show multi-step flows, state changes, ownership, or + several relationships with a compact diagram, table, timeline, or Markdown + wireframe. +- **Textually complete:** every visual has an adjacent textual equivalent; color, + emoji, screenshots, and Mermaid are never the only source of meaning. +- **Explicit:** distinguish current facts, proposed behavior, recommended + defaults, configurable choices, fixed safety limits, non-goals, and open + questions. +- **End-to-end:** include happy paths, alternatives, partial success, failure, + retry, cancellation, migration, and cleanup where relevant. +- **Verifiable:** important statements use normative language and map to an + automated test, contract check, or explicit human acceptance artifact. +- **Proportional:** a concern may be marked not applicable with a reason; it must + not disappear silently. + +Use `MUST`, `SHOULD`, and `MAY` deliberately. A `MUST` requires an acceptance +scenario. Avoid prescribing implementation details unless they protect product +behavior, architecture, interoperability, or safety. + +## 2. Required specification contract + +A specification must cover the following concerns or mark them not applicable: + +| Concern | Required outcome | +|---|---| +| Decision metadata | Status, date, owners, scope, review gates, and unresolved decisions | +| Problem and evidence | Current behavior, concrete pain, affected actors, and evidence/source | +| Goals and limits | Measurable outcomes, non-goals, fixed safety/product invariants | +| Product journey | Current versus proposed flow, actors, entrypoints, visible surfaces, next actions | +| Domain model | Shared terminology, invariants, states, transitions, and ownership of facts | +| Configuration | Recommended defaults, bounded alternatives, validation, precedence, persistence, migration | +| Architecture | Dependency direction, pure policy, use cases, ports, adapters, presentation, state and trust boundaries | +| UI/UX and content | Information hierarchy, examples for every primary state, navigation, accessibility, localization | +| Failure and recovery | Impact, partial-success facts, retryability, idempotency, cleanup, operator action | +| Security and privacy | Permissions, untrusted input, secrets, execution boundaries, auditability | +| Observability | User status, operator evidence, logs/metrics, correlation, noise budget | +| Compatibility and rollout | Existing in-flight state, migration, deprecation, rollout and rollback | +| Testing | Numeric risk-derived budget, distribution, coverage, fixtures, integration and human evidence | +| Documentation | User, setup, configuration, recovery, migration, architecture and discoverability | +| Acceptance and DoD | Observable scenarios, traceability, implementation order, all quality gates | + +## 3. Product and GitHub UI/UX standard + +GitHub issues, pull requests, reviews, comments, checks, labels, releases, and Job +Summaries are product surfaces when a person uses them to understand or operate a +feature. CLI output and generated documentation are product surfaces for the same +reason. + +For each primary state, the first visible content must answer in this order: + +1. What is happening now? +2. What completed already? +3. What happens next? +4. Is human action required? +5. What is the impact, especially for partial success or failure? +6. Where can the result or transition be inspected? +7. Where are technical details available? + +A spec affecting these surfaces must include representative rendered Markdown +or a wireframe for at least pending, action-required, blocked/failed, partially +complete, and complete states. Use actual product terminology and plausible data. +Do not specify only abstract fields such as `status`, `message`, and `url`. + +The UX contract must define, when applicable: + +- one primary status and one primary action; +- progress/state representation and the facts behind it; +- deterministic titles, headings, labels, and durable markers; +- descriptive links to the issue, PR, comparison, commit, run, artifact, release, + package, documentation, and recovery action; +- progressive disclosure: user meaning first, technical diagnostics later; +- error content in `impact -> cause -> action -> retained state` order; +- idempotent update behavior and a bounded comment/notification budget; +- locale source, supported locales, and fallback behavior; +- narrow/mobile readability, light/dark themes, and logical heading/table use; +- sanitization of mentions, commands, Markdown, HTML markers, URLs, and diagrams; + and +- a textual fallback for Mermaid, images, icons, and other visual elements. + +For a flow with three or more dependent transitions, include one overview visual. +For a user-facing issue or PR change, include at least one concrete content +mockup. Screenshots can supplement these artifacts but cannot replace the +semantic written contract. + +## 4. Clean Architecture standard + +Apply Clean Architecture to the repository's real boundaries rather than +inventing a parallel folder taxonomy. A design must identify: + +- domain entities, value objects, invariants, and pure policies; +- application use cases and narrow immutable input/output contracts; +- semantic ports that express capabilities without provider DTOs; +- data/provider adapters and their error mappings; +- infrastructure and composition ownership; +- entrypoint responsibility and trusted/untrusted event boundaries; +- presentation view models and renderers, separate from mutation policy; +- durable state, concurrency, idempotency, and migration ownership; and +- forbidden dependency directions that can be enforced automatically. + +Use a component/dependency diagram when three or more layers or external systems +interact. Use a sequence or state diagram when ordering, retries, asynchronous +events, or partial completion affect correctness. + +Architecture requirements are incomplete without executable constraints. The +spec must state which dependency, contract, cycle, schema, or workflow checks +will prevent boundary erosion. + +## 5. Configuration standard + +Configuration exists to support legitimate product variation, not to make every +implementation detail optional. Every input must define: + +| Field | Required information | +|---|---| +| Name and type | Stable public name, type, and owning surface | +| Recommended default | Safe default and why it is recommended | +| Allowed values/range | Bounded choices with observable semantics | +| Validation | Invalid values and invalid cross-field combinations | +| Scope and precedence | Project/repository/operation scope and override order | +| Persistence | Whether an in-flight operation snapshots or rereads the value | +| Migration | Behavior for absent, retired, renamed, or future values | +| Security | Values that must never accept arbitrary code, refs, URLs, or secrets | + +Show at least one recommended configuration and one meaningful alternative. +State which correctness and safety invariants are intentionally not configurable. + +## 6. Test budget standard + +Every implementation-driving spec must contain a numeric test budget. Derive it +from the behavior and risk inventory; do not reuse one universal count for small +and large changes. + +The budget table must distribute a minimum number of distinct cases across the +applicable areas: + +- domain/configuration/pure planning; +- state transitions, idempotency, replay, cancellation, and races; +- application use cases; +- adapters and provider error mapping; +- workflows, permissions, schemas, and setup contracts; +- presentation states, links, localization, accessibility, and sanitization; +- integration/end-to-end and compatibility/migration; and +- security/abuse cases. + +Each `it`/`test` case counts once. Parameterized rows count separately only when +they represent distinct behavior. The number is a floor, not a substitute for +covering every requirement and failure mode. + +The spec must also define: + +- repository-wide thresholds that remain in force; +- higher branch coverage for new pure policy where justified; +- changed-module expectations; +- deterministic fakes instead of real waits or live services; +- fixtures/golden output without relying only on snapshots; +- workflow/contract checks that parse structure rather than grep prose; +- required manual UX evidence that automation cannot establish; and +- a traceability mapping from each normative requirement to tests or evidence. + +## 7. Documentation standard + +Documentation is an implementation deliverable. Identify the applicable +audiences and artifacts: + +- user journey and recommended default; +- setup, permissions, environments, credentials, and external prerequisites; +- complete configuration reference and examples; +- operator state model, troubleshooting, recovery, replay, and cleanup; +- upgrade, migration, compatibility, deprecation, and rollback; +- architecture, contracts, trust boundaries, and contributor guidance; and +- route/navigation registration, links, assets, examples, and validation jobs. + +Lead documentation with the normal path and one helpful visual, then disclose +configuration, failures, and internals progressively. Examples must be checked +against implementation fixtures or contract tests whenever practical. + +## 8. Acceptance, traceability, and review + +Acceptance scenarios describe observable behavior, not implementation activity. +Cover normal, alternative, boundary, invalid, duplicate, out-of-order, partial, +failure, recovery, migration, security, UX, and accessibility behavior as +applicable. + +Maintain a traceability table for substantial specs: + +```text +Requirement -> domain/policy/use case/adapter/presentation -> test or evidence -> documentation +``` + +Before changing a spec to `Ready for implementation`, reviewers must be able to +answer yes to the following: + +- Is the origin/current behavior supported by evidence? +- Can a user understand the proposed product without reading implementation code? +- Are product defaults and non-configurable safety limits explicit? +- Are architecture boundaries and executable constraints clear? +- Is the numeric test budget proportional and complete? +- Are documentation and migration deliverables named? +- Do UI examples cover pending, action, error/partial, and completed states? +- Can status and required action be understood quickly without logs? +- Are security, idempotency, retries, concurrency, and cleanup addressed? +- Does every `MUST` map to acceptance and verification? +- Are material decisions resolved or explicitly blocking readiness? + +The Definition of Done must repeat these gates. A concern mentioned only in an +introductory paragraph is not an enforceable delivery requirement. diff --git a/specs/_template.md b/specs/_template.md new file mode 100644 index 0000000..6c29045 --- /dev/null +++ b/specs/_template.md @@ -0,0 +1,372 @@ +# + +- Status: Draft +- Date: YYYY-MM-DD +- Catalog capability ID: +- Last verified: +- Owners: +- Scope: +- Related issues/PRs: +- Required review gates: product UX, architecture, testing, documentation, + security/operations +- Open decisions blocking readiness: + +> Start with the repository standard in [`README.md`](./README.md). Remove this +> note when the spec is ready. Mark a non-applicable section explicitly and give +> a reason instead of silently deleting the concern. + +## 1. Executive summary + +Describe the user-visible outcome, the recommended default, and the principal +safety/correctness rule in plain language. + +```text + +``` + +## 2. Problem, current behavior, and evidence + +### 2.1 Problem + +Who is affected, what they cannot understand/do safely, and why it matters. + +### 2.2 Current behavior + +Number the actual current sequence. Separate verified facts from assumptions. + +### 2.3 Evidence + +- Code/workflow/documentation references: +- Incidents or concrete examples: +- External primary sources: +- Unknowns: + +### 2.4 Retrospective classification (as-built baselines only) + +- Observed behavior: +- Intentional contract: +- Known debt and limitations: +- Unknown rationale: +- Proposed improvements: + +For a prospective specification, mark this subsection not applicable because no +implemented baseline is being reconstructed. + +## 3. Actors, surfaces, and terminology + +| Actor | Goal | Entry point | Visible surfaces | +|---|---|---|---| +| | | | | + +Define product terms, branch/data roles, ownership of facts, and language that +must remain consistent across UI, code, tests, and documentation. + +## 4. Goals, non-goals, and fixed invariants + +Declare each goal with a stable ID in an unfenced table with the exact columns +`Requirement` and `Behavior`. Include the selected SDD in the section 15 path table +as `create` or `update`. Native publication uses the selected immutable Git base. + +### 4.1 Goals + +1. + +### 4.2 Non-goals + +1. + +### 4.3 Fixed product/safety invariants + +1. + +## 5. Current versus proposed product journey + +| Stage | Current | Proposed | User/operator effect | +|---|---|---|---| +| | | | | + +For three or more dependent transitions, add a visual and a textual equivalent: + +```mermaid +flowchart LR + A[User entry point] --> B[System transition] + B --> C[Visible result] +``` + +Text equivalent: ` -> -> `. + +## 6. Functional behavior and state model + +### 6.1 Happy path + +1. + +### 6.2 Alternative paths + +- + +### 6.3 State machine + +| State | Entered when | User-visible meaning | Allowed next states | Recovery/owner | +|---|---|---|---|---| +| | | | | | + +Define duplicate, stale, out-of-order, canceled, and partially completed event +behavior where applicable. + +## 7. User-facing configuration + +| Input | Type | Recommended default | Allowed values/range | Scope/persistence | +|---|---|---|---|---| +| `` | | `` — | | | + +Document: + +- validation and invalid cross-field combinations; +- precedence and operation-time snapshot/reread behavior; +- migration, retired, unknown, and deprecated values; +- one recommended example and one meaningful alternative; and +- behavior intentionally not configurable. + +## 8. Clean Architecture design + +### 8.1 Responsibilities and dependency direction + +| Layer/boundary | Owns | Must not own/import | +|---|---|---| +| Domain/pure policy | | | +| Application | | | +| Adapters/data | | | +| Infrastructure/composition | | | +| Entrypoints | | | +| Presentation | | | + +```mermaid +flowchart LR + E[Entrypoint] --> A[Application use case] + A --> D[Domain policy] + A --> P[Semantic port] + I[Provider adapter] --> P + A --> V[Presentation model] +``` + +### 8.2 Contracts, state, and trust boundaries + +- Pure decisions: +- Application contracts: +- Semantic ports: +- Durable state/schema ownership: +- Concurrency/idempotency strategy: +- Trusted and untrusted inputs: +- Provider error mapping: + +### 8.3 Executable architecture constraints + +- + +## 9. UI/UX and content contract + +Treat issues, PRs, comments, labels, checks, summaries, CLI output, and docs as UI +when applicable. + +### 9.1 Information hierarchy + +1. Current status +2. Completed facts +3. Next transition +4. Human action, or explicit statement that none is needed +5. Impact and inspectable links +6. Collapsed technical evidence + +### 9.2 Representative primary view + +```markdown + + +# + +> **Current status:** +> +> **Action required:** + +## Progress + +- [x] +- [ ] + +## What happens next + + + +## Links + +[Descriptive destination](...) · [Descriptive destination](...) + +
+Technical details + +
+``` + +Add concrete variants for: + +- pending/no action; +- action required; +- blocked or failed before irreversible effects; +- partially successful after an irreversible effect; and +- completed. + +### 9.3 Issue, PR, and comment behavior + +- Deterministic titles and purpose: +- What merge/close/retry will do: +- Durable marker and idempotent update policy: +- Comment/notification budget: +- Labels and checks as supplemental state: +- Direct links and navigation: + +### 9.4 Accessibility, localization, and responsive behavior + +- Supported locales and fallback: +- Textual equivalent for visuals: +- Narrow/mobile and light/dark expectations: +- Heading/table/link rules: +- Sanitization of untrusted Markdown, mentions, commands, markers, and URLs: + +## 10. Failure, recovery, and cleanup + +| Failure/partial state | User impact | Retained facts | Automatic retry | Required action | Cleanup | +|---|---|---|---|---|---| +| | | | | | | + +User-facing errors follow `impact -> cause -> action -> retained state` and must +not imply that an already successful irreversible action failed. + +## 11. Security, permissions, and privacy + +1. +2. +3. +4. + +## 12. Observability and operational UX + +- User-facing state: +- Job Summary/operator evidence: +- Logs, metrics, and correlation: +- Pending external dependency versus workflow failure: +- Rate-limit/retry behavior: +- Noise/comment budget: + +## 13. Compatibility, migration, rollout, and rollback + +- Existing in-flight operations/data: +- Schema/configuration migration: +- Deprecated behavior and compatibility window: +- Rollout stages/feature flag if needed: +- Rollback and irreversible effects: + +## 14. Testing strategy and numeric budget + +The count is derived from the behavior/risk inventory and is a minimum, not a +substitute for requirement coverage. + +| Area | Minimum distinct cases | Behaviors/risks covered | +|---|---:|---| +| Domain/configuration/pure planning | | | +| State/application/idempotency/races | | | +| Adapters/provider contracts | | | +| Workflows/setup/schema | | | +| UI/UX/localization/sanitization | | | +| Integration/security/migration | | | +| **Total** | **** | No double counting | + +Add an unfenced inventory with exact columns `Case ID`, `Area`, `Requirements`, +and `Scenario`. Each case has a fully qualified unique ID, one exact budget area, +and comma-separated requirement IDs. Counts per area and the total must match. +Section 17 also needs `Requirement` and `Cases` columns with reciprocal full IDs; +ranges, shorthand, fenced examples and aliases do not establish additional cases. + +Define: + +- repository and changed-module coverage thresholds; +- pure-policy branch coverage; +- table-driven, replay, race, adapter, workflow-contract, and security tests; +- deterministic clocks/IDs/fakes with no real waits or live services; +- golden UI fixtures plus semantic assertions; and +- manual UX evidence for readability that automation cannot establish. + +## 15. Documentation and discoverability + +| Audience | Artifact/page | Required content | Validation/navigation | +|---|---|---|---| +| User | | | | +| Setup owner | | | | +| Operator | | | | +| Contributor | | | | + +Declare prospective repository files in a separate unfenced table with exact +columns `Path` and `Change`: `create` for absent files, `update` for existing files +that will change, and `reference` for existing files used as evidence. Include +this SDD itself with `create` or `update`. Never register absent implementation +files as current catalog evidence. + +Include migration and troubleshooting documentation where applicable. Keep +examples synchronized with implementation fixtures or contract tests. + +## 16. Acceptance scenarios + +1. Given ``, when ``, then ``. +2. +3. +4. +5. +6. +7. +8. + +## 17. Requirements traceability + +| Requirement | Policy/use case/adapter/presentation | Test or evidence | Documentation | +|---|---|---|---| +| `
` | | | | + +## 18. Implementation sequence + +1. Contracts, decisions, configuration, and architecture checks. +2. Pure policies and tests. +3. State/application orchestration and tests. +4. Adapters/workflows and contract tests. +5. Presentation, fixtures, accessibility, and localization. +6. Migration, recovery, security, documentation, and generated artifacts. +7. Integration, coverage, human UX evidence, and final validation. + +Adjust ordering to risk and dependencies; do not postpone documentation or UX +until after behavior is considered complete. + +For an as-built baseline, interpret this as the required change sequence for +future maintenance and state explicitly that the document itself proposes no +behavior change. + +## 19. Definition of Done + +- [ ] Every normative requirement has acceptance and traceability. +- [ ] Architecture boundaries are implemented and automatically enforced. +- [ ] Numeric test budget, distribution, and coverage requirements pass. +- [ ] Configuration defaults, limits, validation, persistence, and migration agree. +- [ ] Primary UI states and content examples are implemented and reviewed. +- [ ] Accessibility, localization, responsive behavior, sanitization, and noise + budgets pass. +- [ ] User/setup/operator/contributor documentation is complete and discoverable. +- [ ] Failure, partial success, retry, idempotency, security, and cleanup pass. +- [ ] Required generated artifacts and repository validation commands pass. +- [ ] `specs/catalog.json` evidence and `specs/CATALOG.md` are current and + the documented original SDK catalog validation passes. +- [ ] No readiness-blocking decision remains unresolved. + +## 20. References and decisions + +- Primary sources: +- Related specs/ADRs/issues/PRs: +- Decisions and rejected alternatives: +- Follow-up work explicitly outside this spec: diff --git a/specs/catalog.json b/specs/catalog.json new file mode 100644 index 0000000..77cbc13 --- /dev/null +++ b/specs/catalog.json @@ -0,0 +1,37 @@ +{ + "version": 1, + "capabilities": [ + { + "id": "shared-ui-foundation", + "title": "Shared Home Assistant presentation components and framework adapters", + "status": "proposed", + "scope": "One DOM component implementation with React, Vue, Angular and Svelte adapters, direct Lit/DOM use, shared Storybook and verified Gateway/Domus adoption. Draft; no runtime implementation or delivery accepted.", + "owner": "vypdev maintainers; Efrain Espada (accountable product/publication owner)", + "lastVerified": "2026-11-04", + "specs": [ + "specs/shared-ui-foundation.md" + ], + "workflows": [ + ".github/workflows/copilot_issue.yml" + ], + "entrypoints": [], + "code": [], + "tests": [], + "documentation": [ + "README.md", + "docs/component-library.md" + ], + "readiness": { + "status": "draft", + "blockingGates": [ + "Native feature publication/readiness", + "Exact support pins and public API verification", + "Implementation, coverage and consumer acceptance", + "npm publication rights and source-to-artifact integrity", + "Storybook Pages delivery and human UX review", + "Actual Gateway and Domus adoption" + ] + } + } + ] +} diff --git a/specs/shared-ui-foundation.md b/specs/shared-ui-foundation.md new file mode 100644 index 0000000..1ba76af --- /dev/null +++ b/specs/shared-ui-foundation.md @@ -0,0 +1,506 @@ +# Shared Home Assistant presentation foundation + +- Status: Draft +- Date: 2026-11-04 +- Catalog capability ID: shared-ui-foundation +- Last verified: Not applicable to implementation; documentation inventory checked on 2026-11-04. +- Owners: vypdev maintainers; Efrain Espada is the accountable product and publication owner. +- Scope: Shared presentation components, themes, catalog, framework adapters and actual consumer adoption. +- Related issues/PRs: [implementation issue #7](https://github.com/vypdev/ha-ui/issues/7), [documentation bootstrap #8](https://github.com/vypdev/ha-ui/issues/8), [Domus adoption #30](https://github.com/vypdev/domus/issues/30). +- Required review gates: Product UX, architecture, security, test inventory/coverage, packaging, original native SDD publication, consumer acceptance and publication rights. +- Open decisions blocking readiness: Exact dependency/support pins and public component signatures need engineering verification on issue #7. npm publication rights, Pages delivery and actual application compatibility remain unverified. These are pending gates, not delivered behavior. + +## 1. Executive summary + +Build one reusable presentation library for Gateway, Domus and future HA applications. +Lit implements the visual components; the public API uses DOM properties, attributes, slots, +typed events and CSS tokens. Deliver React, Vue, Angular and Svelte adapters in the same repository, +plus direct Lit and HTML/JavaScript consumption. A consumer installs only its chosen adapter. +The applications keep their own data, permissions, routes and page composition. + +```text +Reviewed source -> packed core and adapters -> real compiled consumers -> shared Storybook + -> Gateway adoption and Domus adoption -> verified versioned publication +``` + +Text equivalent: Review one component source, test its packed framework consumers, inspect the +same components in Storybook, then validate actual app adoption before declaring reuse complete. +This Draft is a catalog prerequisite. Merging its documentation does not publish a package, +establish native feature readiness or demonstrate any working adapter. + +## 2. Problem, current behavior, and evidence + +### 2.1 Problem + +Gateway has reusable local presentation helpers. Copying those into each new app would duplicate +visual behavior and fixes. Domus needs household-specific sections and workflows with the same +maintained components and the common upper dashboard navigation, while future apps may use other frameworks. + +### 2.2 Current behavior + +1. At the admitted bootstrap base `8a0657d79a3c8d85414dbc1636ec94531e79a2bf`, ha-ui contains its README and installed Copilot resources, with no product source, package manifest, tests or Storybook. +2. The reviewed Gateway source at commit e47dde303e475a6d03e1d6697ec87a934e93263b has [Lit helper exports](https://github.com/vypdev/homeassistant-gateway/blob/e47dde303e475a6d03e1d6697ec87a934e93263b/frontend/src/ui/index.ts) and [primitives](https://github.com/vypdev/homeassistant-gateway/blob/e47dde303e475a6d03e1d6697ec87a934e93263b/frontend/src/ui/ui-primitives.ts). These paths are external research, not implementation evidence in ha-ui. +3. Its template callbacks and TemplateResult values are not a framework-neutral DOM contract. Extraction needs a deliberate public boundary and an adapter for existing Lit call sites. +4. Domus has separate task/household UI and unfinished recurrence/calendar work. Library documentation does not complete those workflows. + +### 2.3 Evidence and unknowns + +The owner authorized the separate repository, Storybook, required four framework adapters and +Domus-owned UX with the shared upper tab pattern. Issue #7 retains that scope. Gateway source is +MIT-licensed, copyright 2026 vypdev; implementation must pin the exact extraction commit and inspect +every copied code/asset notice. Upstream HA visual compatibility does not grant permission to copy +unreviewed assets. No npm owner permissions, package artifact, Pages site or adapter execution is yet proven. + +### 2.4 Retrospective classification + +Not applicable: this prospective Draft proposes a new library. Installed workflow files establish +repository setup, not an as-built component baseline. + +## 3. Actors, surfaces, and terminology + +| Actor | Goal | Entry point | Visible surface | +|---|---|---|---| +| Household member | Use familiar accessible controls | Domus or Gateway | App-owned pages using shared components | +| App developer | Compose a framework-native experience | Core or selected adapter | Typed imports, examples and migration guide | +| Component maintainer | Fix visual behavior once | ha-ui source and PR | Storybook, contracts and consumer fixtures | +| Publication owner | Release a reviewed compatible cohort | Native repository lifecycle | CI evidence, packed artifacts, package and Pages results | + +Core means the one DOM component implementation. Adapter means a thin framework binding, never +another visual implementation. Cohort means compatible core/adapter releases tested together. +Navigation bar means links to application destinations; panel tabs mean local tab/tabpanel state. +They may look alike but must use the correct distinct accessibility semantics. + +## 4. Goals, non-goals, and fixed invariants + +### 4.1 Goals + +| Requirement | Behavior | +|---|---| +| UIF-01 | Deliver one framework-neutral DOM component API with a Lit implementation and explicit registration; keep application data, authorization and routing in consumers. | +| UIF-02 | Deliver real React, Vue, Angular and Svelte adapter packages plus direct Lit/DOM consumption; verify idiomatic bindings, cleanup, forms and client/import boundaries. | +| UIF-03 | Extract reviewed Gateway presentation families once with theme tokens and preserved source/asset provenance; bundle all visual runtime assets locally. | +| UIF-04 | Provide the common HA-style upper navigation with explicit navigation-versus-panel semantics; let Domus choose its own sections and household experience. | +| UIF-05 | Publish one Storybook built from the same components, with EN/ES, light/dark, keyboard, narrow-screen and reduced-motion acceptance and reviewable PR artifacts. | +| UIF-06 | Start authored behavior with tests, enforce Clean Architecture and 100% statements, branches, functions and lines globally and per authored module; run CI Check on every PR commit. | +| UIF-07 | Ship versioned typed ESM packages with verified packed-artifact installation, framework compatibility, source integrity, publication rights and rollback guidance. | +| UIF-08 | Demonstrate actual Gateway and Domus adoption of the same reviewed package without duplicate visuals or weakened existing application/recovery behavior. | + +### 4.2 Non-goals + +No household task, calendar, inventory, points, rewards or automation policy belongs here. +No HA authentication, token handling, services, entities, MCP logic or app router belongs here. +This change does not publish a clone of HA private frontend bundles, a new framework, or a +second component implementation per adapter. Generic custom-element compatibility alone does +not establish a supported framework or SSR integration. + +### 4.3 Fixed invariants + +All required adapters and direct consumption remain first-cohort deliverables. Core behavior is +shared. Tests precede behavior and coverage may not be weakened by excluding authored modules. +Consumer permission/origin rules and original-operation recovery remain intact. Missing evidence +stays pending. No fake component screen, success state or package version represents delivery. + +## 5. Current versus proposed product journey + +| Stage | Current | Proposed | Observable effect | +|---|---|---|---| +| Choose framework | Gateway-local Lit helpers | Core or one real adapter | Framework choice does not fork visuals | +| Discover components | App-local catalog | One production-backed Storybook | Maintainer can inspect states before adoption | +| Build an app page | App components and styles | Shared controls, app-owned composition | Domus retains household-specific experience | +| Fix/upgrade | Independent duplicated changes | Reviewed cohort and consumer verification | One fix propagates through explicit app upgrades | +| Recover | App-specific rollback | Retained app artifact and compatible package cohort | A failed upgrade preserves user data and work | + +## 6. Functional behavior and state model + +### 6.1 Component contract + +Use the proposed collision-resistant tag prefix `vyp-ha-`; do not register private HA `ha-*` names. +Define explicit client-side registration and per-component imports. Repeated registration of the +same compatible constructor is harmless; a conflicting installed cohort produces an actionable +error. Contract/type imports must be safe without browser globals. Components upgrade in the client; +server-rendered Shadow DOM and universal hydration are not promised. + +Public inputs include typed properties for objects, bounded string/boolean attributes where +appropriate, named slots and documented CSS tokens. Text labels render as inert text. Do not accept +raw HTML strings or framework rendering callbacks in the common API. A Lit-only compatibility +entrypoint may translate existing helper calls, without leaking into core or other adapters. + +Controlled state is owned by the app. Setting a property is not a user action and must not emit a +change event. User intent emits a documented typed event once, with reviewed bubbling/composed and +cancellation semantics. Inputs, options, event detail and link schemes are validated. Inputs assigned +before upgrade survive registration. Form controls support name/value, disabled, validation, reset, +submission and accessible labels through native semantics or one verified equivalent contract. + +Shared families include tokens/type/spacing, upper navigation, buttons, text fields, selects, +checkboxes, cards, rows/grids, dialogs, status/empty/loading/error feedback and responsive layout. +Define each actual signature, event detail, slot and theme token before implementing its family. +No family is considered complete solely because a single example renders. + +### 6.2 Framework adapters + +Proposed packages are `@vypdev/ha-ui`, `@vypdev/ha-ui-react`, `@vypdev/ha-ui-vue`, +`@vypdev/ha-ui-angular` and `@vypdev/ha-ui-svelte`. Names remain subject to registry-rights verification. +Each adapter exposes typed idiomatic bindings/events/content projection and a tested form bridge; +Angular includes its appropriate value-accessor integration. React must survive its verified +rerender/remount lifecycle; Vue must preserve property/value updates; Svelte must preserve its +supported compiler binding contract. All delegate visuals, keyboard and accessibility to core. + +Publish an exact tested compiler/runtime/browser matrix and peer ranges derived from it before +claiming support. Do not infer support for untested versions. Next.js, Nuxt and SvelteKit fixtures +must demonstrate a server shell with an explicit client registration/mount boundary, without +hydration warnings or server DOM access. Keep unsupported server component rendering explicit. + +### 6.3 Lifecycle + +| State | Meaning | Transition | Failure/recovery | +|---|---|---|---| +| Imported contracts | No browser effects | App reaches client boundary | Server import remains inert | +| Registered | Compatible tag defined | Mount component | Conflicts fail before silent reuse | +| Connected | Current inputs/listeners active | User action or input update | App owns accepted mutation/result | +| Disconnected | Owned subscriptions/overlays released | Reconnect | One current subscription set only | +| Upgrade rejected | Incompatible package/API | Restore reviewed cohort | Preserve app data, drafts and requests | + +## 7. User-facing configuration + +| Input | Type | Recommended default | Allowed values | Scope/persistence | +|---|---|---|---|---| +| Theme | Token set | Host-supplied reviewed HA-compatible tokens | Reviewed light/dark tokens and documented overrides | App root; no library storage | +| Locale | Language tag and messages | App locale, English fallback | EN/ES in initial acceptance; app labels remain data | App owns persistence | +| Navigation kind | Explicit enum | Navigation for app sections | Navigation links or local panel tabs | Component instance | +| Selected destination | Stable item ID | App-selected current location | One existing enabled item | App router or local controller | +| Motion | Host/media preference | Honor reduced motion | System preference or stronger reduced motion | Client session, no hidden persistence | +| Component state | Typed properties | Enabled and idle | Documented disabled/loading/error states | App-owned current inputs | + +Invalid item identities, contradictory selected/disabled states, unsafe links and unknown enum values +must fail with a documented bounded diagnostic rather than selecting an arbitrary destination. +There is no global mutable singleton for locale, routing or household data. Recommended: app-level +theme/locale plus navigation links. Alternative: a standalone settings dialog with panel tabs and +its own explicit locale. A visual theme override cannot weaken focus, semantics or disabled behavior. + +## 8. Clean Architecture design + +| Boundary | Owns | Forbidden dependencies | +|---|---|---| +| Pure contracts/policy | Values, events, navigation/selection validation | DOM I/O, frameworks, app models | +| Component controllers | Presentation transitions and semantic ports | HA API, auth, database or application orchestration | +| Lit presentation | One DOM/visual implementation | React/Vue/Angular/Svelte packages, app business policy | +| Framework adapters | Framework mapping, cleanup and form bridge | Other frameworks or duplicate CSS/render behavior | +| Composition/examples | Explicit registration and fixture composition | Private HA modules and production credentials | +| Consuming apps | Router, permissions, commands, state and page composition | Dependence on another app running | + +```mermaid +flowchart LR + A[App-owned page] --> F[Selected adapter] + A --> C[Core DOM contract] + F --> C + L[Lit visual implementation] --> C + L --> P[Pure presentation policy] + S[Storybook and consumer fixtures] --> F + S --> L +``` + +Text equivalent: Applications choose an adapter or core; adapters bind the common contract, one Lit +implementation renders it, and pure policy contains framework-independent decisions. Fixtures exercise +those public boundaries. No shared durable database, command ledger or business transaction is applicable. +Architecture checks enforce inward imports, acyclicity, framework isolation and absence of app/private-HA +imports. Test-only consumers may depend on their framework; they are not production core dependencies. + +## 9. UI/UX and content contract + +Domus chooses its own Overview, Tasks, Calendar, Shopping, Inventory, Family and Settings composition +as those workflows become real. Gateway retains its technical dashboard. Common upper navigation +matches the observed flat dashboard bar, active indicator, spacing, focus and narrow overflow behavior. +App destinations use links/current-location semantics; panel tabs use the tab/tabpanel pattern. +The shared component never invents routes, changes permissions or hides content as authorization. + +Representative Storybook examples show meaning before details: + +| State | Primary visible example | Action and retained state | +|---|---|---| +| Pending | Loading household tasks | No action; current app data remains | +| Action required | Name is required | Focus the named field; draft retained | +| Blocked | Tasks could not be loaded | Try again through the app's handler; existing tasks retained | +| Partial | Three changes saved; one needs review | Review the remaining change; saved facts remain visible | +| Complete | Changes saved | Continue to the selected section; no repeated mutation | + +These are presentation fixtures, not business services inside ha-ui. Use concise accessible text, +headings and non-color status. Dynamic labels remain inert. The app supplies messages and authorization. +Storybook documents every family and significant state in both themes and EN/ES, narrow/wide layouts, +keyboard use and reduced motion. Test accessible names/roles, contrast, focus return and touch targets; +visual references supplement semantic assertions. A human review records whether the bar and controls +fit the host experience. No automated screenshot score substitutes for that judgment. +No unsolicited notifications, issue comments or remote telemetry are generated by components. + +## 10. Failure, recovery, and cleanup + +| Failure | Impact | Retained facts | Retry/action | Cleanup | +|---|---|---|---|---| +| Invalid input | A control cannot represent its requested state | Current valid app state | Correct the named input | No partial app mutation | +| Adapter teardown | A page closes | App data and commands | Remount with current inputs | Remove listeners/observers/overlays | +| Registration conflict | New cohort cannot mount safely | Existing definitions and app data | Load one compatible cohort | Never overwrite registered constructors | +| Consumer build fails | Upgrade is not accepted | Reviewed lock/artifact | Correct compatibility or roll back | Remove only owned generated output | +| Publication partial | Some remote artifacts may exist | Exact package digests and registry observations | Inspect each result before resuming | Never overwrite an existing version | +| App action fails | App decides business effect/result | App-owned drafts/receipt | App supplies recovery action | Library never retries a mutation | + +Error order is impact, known cause, useful action and retained state. No hidden POST retry is applicable. +Retain review evidence, source/lock identity and one current rollback; remove disposable build/test output +after use without deleting authored inputs, another session's resources or unresolved diagnostics. + +## 11. Security, permissions, and privacy + +Core has no HA API access, tokens, storage, analytics, parent-frame DOM inspection or network fetches. +All runtime scripts/fonts/icons/styles are reviewed and bundled locally for both deployment modes. +Unknown assets and unsafe link schemes fail review. Text/event detail are data, never executable markup. +Each app retains permission and origin enforcement. Test malicious labels and injection-shaped inputs. +Storybook/fixtures use fictional household data and no private owner screenshots or credentials. +Publication uses the approved repository lifecycle with minimal package/Pages permissions; package +rights must be verified before publication. This Draft grants no new token or repository permissions. + +## 12. Observability and operational UX + +Consumers receive documented typed errors, not raw household payload dumps. CI and release evidence +identify source, lock, toolchain, package digest, tested framework/browser versions and results. +A missing package right or external delivery outage remains pending, distinct from a failed component +contract. No library background retries or remote log transport exist. Build/Pages diagnostics provide +one primary status/action and links; internal traces remain secondary and exclude secrets/private data. + +## 13. Compatibility, migration, rollout, and rollback + +1. Pin the reviewed Gateway source/license baseline; inventory every helper/style/asset to be extracted. +2. Prove packed fixtures for core and all adapters before broad extraction. Lock exact toolchains and + derive public peer ranges from executed evidence; browser support includes current stable Chromium, + Firefox and WebKit as pinned by the chosen browser harness, with actual results recorded. +3. Extract each family once and retain a tested Lit compatibility boundary while moving Gateway callers. +4. Adopt the same exact reviewed cohort in Domus through its admitted application work. Preserve + Ingress/standalone, EN/ES, private drafts, identity/origin checks and original-operation recovery. +5. Retire local duplicate visuals only when the equivalent app journey passes. Record separate package, + Storybook, Gateway and Domus readiness. A preview is not a Stable release. + +Public exports/types/events/tokens follow semantic versioning. Start with an explicit compatible +cohort, coordinate breaking changes and publish migration notes. Do not silently auto-upgrade consumers. +Retain the prior tested app artifact/lock for rollback; this library has no household data migration. +A component update may not clear data or drafts to make adoption tests pass. + +## 14. Testing strategy and numeric budget + +The following 64 distinct cases are a minimum derived from contract, binding, packaging and adoption +risks. No test is yet implemented. Parameterization counts only independently asserted behavior. +A shared event exercised through several frameworks is separate only where actual framework behavior +is compiled and asserted. Later native appendices retain these cases without renaming or double counting. + +| Area | Minimum distinct cases | Behaviors/risks covered | +|---|---:|---| +| Core DOM contracts | 16 | UIF-01, UIF-02, UIF-03, UIF-04, UIF-05, UIF-07 | +| Framework adapters | 16 | UIF-01, UIF-02, UIF-06 | +| Compiled consumer journeys | 10 | UIF-02, UIF-05, UIF-07 | +| Storybook and accessibility | 8 | UIF-03, UIF-04, UIF-05, UIF-06, UIF-07 | +| Packaging, CI and security | 8 | UIF-01, UIF-02, UIF-03, UIF-06, UIF-07 | +| Adoption and recovery | 6 | UIF-03, UIF-04, UIF-05, UIF-07, UIF-08 | +| **Total** | **64** | Each defined case counted once | + +| Case ID | Area | Requirements | Scenario | +|---|---|---|---| +| shared-ui-foundation-C01 | Core DOM contracts | UIF-01,UIF-03 | Explicit repeated registration reuses compatible constructors and creates no duplicate styles or listeners. | +| shared-ui-foundation-C02 | Core DOM contracts | UIF-01,UIF-07 | A conflicting registered component cohort fails explicitly before an incompatible definition is silently reused. | +| shared-ui-foundation-C03 | Core DOM contracts | UIF-01,UIF-02 | Complex properties assigned before upgrade survive first connection and later replacement without string coercion. | +| shared-ui-foundation-C04 | Core DOM contracts | UIF-01,UIF-02 | Controlled value updates do not emit user-change events or overwrite the current value with stale internal state. | +| shared-ui-foundation-C05 | Core DOM contracts | UIF-01 | Invalid public configuration reports a bounded diagnostic without partial registration or invented application state. | +| shared-ui-foundation-C06 | Core DOM contracts | UIF-01,UIF-02 | Typed composed/bubbling events expose the documented inert detail once; cancellable requests honor cancellation. | +| shared-ui-foundation-C07 | Core DOM contracts | UIF-01,UIF-05 | Named/default slots and accessible names remain correct when localized content changes dynamically. | +| shared-ui-foundation-C08 | Core DOM contracts | UIF-03,UIF-05 | Buttons preserve native activation, disabled/loading prevention and visible keyboard focus without duplicate activation. | +| shared-ui-foundation-C09 | Core DOM contracts | UIF-03,UIF-05 | Text fields preserve labels, descriptions, validation, value, focus and form submission/reset semantics. | +| shared-ui-foundation-C10 | Core DOM contracts | UIF-03,UIF-05 | Select controls preserve typed values, keyboard selection, disabled options and form semantics. | +| shared-ui-foundation-C11 | Core DOM contracts | UIF-03,UIF-05 | Checkbox controls preserve checked/disabled state, accessible name and form reset semantics. | +| shared-ui-foundation-C12 | Core DOM contracts | UIF-04,UIF-05 | Upper navigation exposes links and current location, including modified-click behavior, without claiming panel tab semantics. | +| shared-ui-foundation-C13 | Core DOM contracts | UIF-04,UIF-05 | Panel tabs expose selected tab/panel relationships and keyboard movement with no duplicate or hidden focus targets. | +| shared-ui-foundation-C14 | Core DOM contracts | UIF-03,UIF-05 | Dialog open/cancel/confirm and disconnect return focus appropriately and clean overlays without trapping focus. | +| shared-ui-foundation-C15 | Core DOM contracts | UIF-01,UIF-03 | Untrusted labels and event detail remain inert text; unsafe link targets and raw HTML payloads are rejected. | +| shared-ui-foundation-C16 | Core DOM contracts | UIF-03,UIF-05 | Runtime light/dark and spacing tokens update connected components without changing global document styles. | +| shared-ui-foundation-C17 | Framework adapters | UIF-01,UIF-02,UIF-06 | React: compiled typed inputs and events preserve complex values and exactly-once event delivery. | +| shared-ui-foundation-C18 | Framework adapters | UIF-01,UIF-02,UIF-06 | React: idiomatic controlled forms preserve value, validation, disabled state and reset. | +| shared-ui-foundation-C19 | Framework adapters | UIF-01,UIF-02,UIF-06 | React: slots or framework content projection preserve accessible names and imperative focus. | +| shared-ui-foundation-C20 | Framework adapters | UIF-01,UIF-02,UIF-06 | React: mount, rerender, unmount and remount remove old listeners and preserve only current subscriptions. | +| shared-ui-foundation-C21 | Framework adapters | UIF-01,UIF-02,UIF-06 | Vue: compiled typed inputs and events preserve complex values and exactly-once event delivery. | +| shared-ui-foundation-C22 | Framework adapters | UIF-01,UIF-02,UIF-06 | Vue: idiomatic controlled forms preserve value, validation, disabled state and reset. | +| shared-ui-foundation-C23 | Framework adapters | UIF-01,UIF-02,UIF-06 | Vue: slots or framework content projection preserve accessible names and imperative focus. | +| shared-ui-foundation-C24 | Framework adapters | UIF-01,UIF-02,UIF-06 | Vue: mount, rerender, unmount and remount remove old listeners and preserve only current subscriptions. | +| shared-ui-foundation-C25 | Framework adapters | UIF-01,UIF-02,UIF-06 | Angular: compiled typed inputs and events preserve complex values and exactly-once event delivery. | +| shared-ui-foundation-C26 | Framework adapters | UIF-01,UIF-02,UIF-06 | Angular: idiomatic controlled forms preserve value, validation, disabled state and reset. | +| shared-ui-foundation-C27 | Framework adapters | UIF-01,UIF-02,UIF-06 | Angular: slots or framework content projection preserve accessible names and imperative focus. | +| shared-ui-foundation-C28 | Framework adapters | UIF-01,UIF-02,UIF-06 | Angular: mount, rerender, unmount and remount remove old listeners and preserve only current subscriptions. | +| shared-ui-foundation-C29 | Framework adapters | UIF-01,UIF-02,UIF-06 | Svelte: compiled typed inputs and events preserve complex values and exactly-once event delivery. | +| shared-ui-foundation-C30 | Framework adapters | UIF-01,UIF-02,UIF-06 | Svelte: idiomatic controlled forms preserve value, validation, disabled state and reset. | +| shared-ui-foundation-C31 | Framework adapters | UIF-01,UIF-02,UIF-06 | Svelte: slots or framework content projection preserve accessible names and imperative focus. | +| shared-ui-foundation-C32 | Framework adapters | UIF-01,UIF-02,UIF-06 | Svelte: mount, rerender, unmount and remount remove old listeners and preserve only current subscriptions. | +| shared-ui-foundation-C33 | Compiled consumer journeys | UIF-02,UIF-05,UIF-07 | Lit/DOM: a clean consumer installs packed artifacts, type-checks and runs tabs, fields, button and dialog interactions in a real browser. | +| shared-ui-foundation-C34 | Compiled consumer journeys | UIF-02,UIF-05,UIF-07 | Lit/DOM: the same browser consumer updates EN/ES content and theme at runtime, exercises narrow-screen keyboard navigation, then tears down without retained effects. | +| shared-ui-foundation-C35 | Compiled consumer journeys | UIF-02,UIF-05,UIF-07 | React: a clean consumer installs packed artifacts, type-checks and runs tabs, fields, button and dialog interactions in a real browser. | +| shared-ui-foundation-C36 | Compiled consumer journeys | UIF-02,UIF-05,UIF-07 | React: the same browser consumer updates EN/ES content and theme at runtime, exercises narrow-screen keyboard navigation, then tears down without retained effects. | +| shared-ui-foundation-C37 | Compiled consumer journeys | UIF-02,UIF-05,UIF-07 | Vue: a clean consumer installs packed artifacts, type-checks and runs tabs, fields, button and dialog interactions in a real browser. | +| shared-ui-foundation-C38 | Compiled consumer journeys | UIF-02,UIF-05,UIF-07 | Vue: the same browser consumer updates EN/ES content and theme at runtime, exercises narrow-screen keyboard navigation, then tears down without retained effects. | +| shared-ui-foundation-C39 | Compiled consumer journeys | UIF-02,UIF-05,UIF-07 | Angular: a clean consumer installs packed artifacts, type-checks and runs tabs, fields, button and dialog interactions in a real browser. | +| shared-ui-foundation-C40 | Compiled consumer journeys | UIF-02,UIF-05,UIF-07 | Angular: the same browser consumer updates EN/ES content and theme at runtime, exercises narrow-screen keyboard navigation, then tears down without retained effects. | +| shared-ui-foundation-C41 | Compiled consumer journeys | UIF-02,UIF-05,UIF-07 | Svelte: a clean consumer installs packed artifacts, type-checks and runs tabs, fields, button and dialog interactions in a real browser. | +| shared-ui-foundation-C42 | Compiled consumer journeys | UIF-02,UIF-05,UIF-07 | Svelte: the same browser consumer updates EN/ES content and theme at runtime, exercises narrow-screen keyboard navigation, then tears down without retained effects. | +| shared-ui-foundation-C43 | Storybook and accessibility | UIF-03,UIF-05 | Every component family is discoverable in the static Storybook and imports the production implementation. | +| shared-ui-foundation-C44 | Storybook and accessibility | UIF-05 | Normal, loading, disabled, empty and error examples communicate status and one useful action without color-only meaning. | +| shared-ui-foundation-C45 | Storybook and accessibility | UIF-05 | English and Spanish dynamic labels, descriptions and fallback are understandable at narrow and wide widths. | +| shared-ui-foundation-C46 | Storybook and accessibility | UIF-04,UIF-05 | Upper bar horizontal overflow preserves reachable destinations and visible active/focus state without clipped names. | +| shared-ui-foundation-C47 | Storybook and accessibility | UIF-03,UIF-05 | Light/dark reference states meet contrast and visible-focus checks against the recorded Gateway/HA visual references. | +| shared-ui-foundation-C48 | Storybook and accessibility | UIF-05 | Reduced-motion settings remove nonessential transitions without hiding state or breaking interaction. | +| shared-ui-foundation-C49 | Storybook and accessibility | UIF-05,UIF-06 | Keyboard and accessibility-tree assertions cover dialogs, forms and both navigation semantics beyond screenshot snapshots. | +| shared-ui-foundation-C50 | Storybook and accessibility | UIF-05,UIF-07 | Static Storybook works at the repository Pages subpath with local assets and retains a reviewable per-PR build. | +| shared-ui-foundation-C51 | Packaging, CI and security | UIF-03,UIF-07 | Packed core and adapter exports, declarations, source maps, licenses and asset notices match the reviewed source cohort. | +| shared-ui-foundation-C52 | Packaging, CI and security | UIF-02,UIF-07 | Each adapter declares only its framework peers plus core; a core-only consumer pulls no other framework or unnecessary component entrypoint. | +| shared-ui-foundation-C53 | Packaging, CI and security | UIF-01,UIF-02,UIF-07 | Documented server-safe contract imports work without window/document/customElements; browser registration stays behind the client boundary. | +| shared-ui-foundation-C54 | Packaging, CI and security | UIF-02,UIF-07 | Next.js fixture renders its server shell and mounts the client adapter without duplicate listeners, unsafe server DOM access or hydration warnings. | +| shared-ui-foundation-C55 | Packaging, CI and security | UIF-02,UIF-07 | Nuxt fixture renders its server shell and mounts the client adapter with the documented boundary and no hydration warnings. | +| shared-ui-foundation-C56 | Packaging, CI and security | UIF-02,UIF-07 | SvelteKit fixture renders its server shell and mounts the client adapter with the documented boundary and no hydration warnings. | +| shared-ui-foundation-C57 | Packaging, CI and security | UIF-06,UIF-07 | CI Check fails on any required test, per-module coverage, architecture, packed consumer or catalog failure and runs for each PR head. | +| shared-ui-foundation-C58 | Packaging, CI and security | UIF-01,UIF-03,UIF-07 | Browser assertions and static dependency checks reject hidden network loads, private HA modules, parent DOM/storage access and bundled credentials. | +| shared-ui-foundation-C59 | Adoption and recovery | UIF-03,UIF-04,UIF-08 | Gateway consumes the exact reviewed package for its common bar and presentation in both themes without altering MCP/business behavior. | +| shared-ui-foundation-C60 | Adoption and recovery | UIF-03,UIF-05,UIF-08 | Gateway package adoption preserves existing keyboard/narrow-screen workflows and recorded recovery behavior. | +| shared-ui-foundation-C61 | Adoption and recovery | UIF-04,UIF-08 | Domus Ingress uses the shared upper bar and existing household/task actions with verified identity and permission boundaries. | +| shared-ui-foundation-C62 | Adoption and recovery | UIF-04,UIF-08 | Domus standalone uses the same packed component cohort without HA parent-frame dependencies or changed origin policy. | +| shared-ui-foundation-C63 | Adoption and recovery | UIF-05,UIF-08 | Domus EN/ES private drafts, original-operation recovery and task/preference persistence survive shell adoption and reload. | +| shared-ui-foundation-C64 | Adoption and recovery | UIF-07,UIF-08 | A rejected package update can roll back to the retained reviewed cohort and previous app artifact without deleting household data or retained requests. | + +Enforce 100% supported statements, branches, functions and lines globally and per authored module, +with an independent source inventory. Include production core/adapters and authored build/validation +logic; framework/generated/third-party code is identified separately rather than excluded to hide gaps. +Run meaningful negative architecture/coverage fixtures. Use deterministic clocks/IDs/fakes where +applicable, semantic assertions beyond golden images, and actual packed-package consumers. +Run the browser matrix in CI, including server-shell/client-boundary fixtures and both app deployments. +Local owner HA dogfooding is separate recorded acceptance using current authorization; it is not a unit +suite dependency. Required human visual/UX review remains open until recorded. + +## 15. Documentation and discoverability + +The documentation guide is the initial entrypoint. It will link usage for each framework, component +states, properties/events/slots/tokens, supported matrix, forms, SSR boundaries, migration, publishing, +source/license inventory and cleanup. Storybook and examples must use the actual shipped API. +The table is prospective at the bootstrap commit: implementation files do not exist or enter catalog +evidence until they are real. An implementation must declare additional actual paths before using them. + +| Path | Change | +|---|---| +| specs/shared-ui-foundation.md | update | +| specs/catalog.json | update | +| specs/CATALOG.md | update | +| specs/README.md | reference | +| specs/_template.md | reference | +| README.md | update | +| docs/component-library.md | update | +| package.json | create | +| pnpm-workspace.yaml | create | +| pnpm-lock.yaml | create | +| tsconfig.json | create | +| LICENSE | create | +| .github/workflows/ci_check.yml | create | +| .github/workflows/storybook_pages.yml | create | +| packages/core/package.json | create | +| packages/core/src/index.ts | create | +| packages/core/src/contracts.ts | create | +| packages/core/src/register.ts | create | +| packages/react/package.json | create | +| packages/react/src/index.ts | create | +| packages/vue/package.json | create | +| packages/vue/src/index.ts | create | +| packages/angular/package.json | create | +| packages/angular/src/index.ts | create | +| packages/svelte/package.json | create | +| packages/svelte/src/index.ts | create | +| tests/contracts/core.test.ts | create | +| tests/contracts/adapters.test.ts | create | +| tests/architecture.test.mjs | create | +| tests/coverage-evidence.test.mjs | create | +| tests/consumers/packed.test.ts | create | +| tests/browser/components.spec.ts | create | +| examples/consumers/README.md | create | +| .storybook/main.ts | create | +| .storybook/preview.ts | create | +| stories/foundation.stories.ts | create | +| docs/compatibility.md | create | +| docs/migration.md | create | +| docs/provenance.md | create | +| docs/evidence/shared-ui-foundation.json | create | + +The table describes the feature after this documentation bootstrap is merged; its existing rows are +updates/references at that future feature base. This documentation PR creates the standard, template, +primary draft, catalog/index and guide. It changes no Copilot-owned resource. Consumer repository +paths are owned and admitted separately; they are not falsely declared as ha-ui implementation files. + +## 16. Acceptance scenarios + +1. Given a clean supported framework project, installing only its packed adapter and core renders and + operates the same tabs, fields, buttons and dialogs with correct typed values and no extra frameworks. +2. Given a server-rendered app shell, importing safe contracts has no browser effects; the documented + client boundary mounts once with no hydration warning. Unsupported server rendering stays explicit. +3. Given an EN/ES narrow-screen keyboard user, destinations, labels, disabled/error states and focus + remain understandable in both themes and reduced motion. +4. Given app rerender/disconnect/remount or repeated registration, no listener, observer, overlay or + user-action delivery is duplicated; a conflicting cohort fails explicitly. +5. Given an unsafe label/link or malformed event payload, no code runs, private data is read or app + mutation is retried by the library. +6. Given a common component fix, Storybook and all framework consumers use that same implementation; + Gateway and Domus separately prove their preserved journeys with the reviewed package cohort. +7. Given a failing CI requirement or incomplete adapter/adoption result, CI Check and documentation + report incomplete acceptance instead of claiming release readiness. +8. Given a rejected app upgrade or partial publication, retain exact prior artifacts/data and inspect + observed remote state before retry; no existing package version is overwritten. + +## 17. Requirements traceability + +Each requirement links reciprocally to the distinct inventory above. Sections 6–13 own the contracts; +section 15 names prospective code, examples and documentation. Actual case-to-test/evidence mapping +is an implementation deliverable and currently absent. + +| Requirement | Cases | +|---|---| +| UIF-01 | shared-ui-foundation-C01, shared-ui-foundation-C02, shared-ui-foundation-C03, shared-ui-foundation-C04, shared-ui-foundation-C05, shared-ui-foundation-C06, shared-ui-foundation-C07, shared-ui-foundation-C15, shared-ui-foundation-C17, shared-ui-foundation-C18, shared-ui-foundation-C19, shared-ui-foundation-C20, shared-ui-foundation-C21, shared-ui-foundation-C22, shared-ui-foundation-C23, shared-ui-foundation-C24, shared-ui-foundation-C25, shared-ui-foundation-C26, shared-ui-foundation-C27, shared-ui-foundation-C28, shared-ui-foundation-C29, shared-ui-foundation-C30, shared-ui-foundation-C31, shared-ui-foundation-C32, shared-ui-foundation-C53, shared-ui-foundation-C58 | +| UIF-02 | shared-ui-foundation-C03, shared-ui-foundation-C04, shared-ui-foundation-C06, shared-ui-foundation-C17, shared-ui-foundation-C18, shared-ui-foundation-C19, shared-ui-foundation-C20, shared-ui-foundation-C21, shared-ui-foundation-C22, shared-ui-foundation-C23, shared-ui-foundation-C24, shared-ui-foundation-C25, shared-ui-foundation-C26, shared-ui-foundation-C27, shared-ui-foundation-C28, shared-ui-foundation-C29, shared-ui-foundation-C30, shared-ui-foundation-C31, shared-ui-foundation-C32, shared-ui-foundation-C33, shared-ui-foundation-C34, shared-ui-foundation-C35, shared-ui-foundation-C36, shared-ui-foundation-C37, shared-ui-foundation-C38, shared-ui-foundation-C39, shared-ui-foundation-C40, shared-ui-foundation-C41, shared-ui-foundation-C42, shared-ui-foundation-C52, shared-ui-foundation-C53, shared-ui-foundation-C54, shared-ui-foundation-C55, shared-ui-foundation-C56 | +| UIF-03 | shared-ui-foundation-C01, shared-ui-foundation-C08, shared-ui-foundation-C09, shared-ui-foundation-C10, shared-ui-foundation-C11, shared-ui-foundation-C14, shared-ui-foundation-C15, shared-ui-foundation-C16, shared-ui-foundation-C43, shared-ui-foundation-C47, shared-ui-foundation-C51, shared-ui-foundation-C58, shared-ui-foundation-C59, shared-ui-foundation-C60 | +| UIF-04 | shared-ui-foundation-C12, shared-ui-foundation-C13, shared-ui-foundation-C46, shared-ui-foundation-C59, shared-ui-foundation-C61, shared-ui-foundation-C62 | +| UIF-05 | shared-ui-foundation-C07, shared-ui-foundation-C08, shared-ui-foundation-C09, shared-ui-foundation-C10, shared-ui-foundation-C11, shared-ui-foundation-C12, shared-ui-foundation-C13, shared-ui-foundation-C14, shared-ui-foundation-C16, shared-ui-foundation-C33, shared-ui-foundation-C34, shared-ui-foundation-C35, shared-ui-foundation-C36, shared-ui-foundation-C37, shared-ui-foundation-C38, shared-ui-foundation-C39, shared-ui-foundation-C40, shared-ui-foundation-C41, shared-ui-foundation-C42, shared-ui-foundation-C43, shared-ui-foundation-C44, shared-ui-foundation-C45, shared-ui-foundation-C46, shared-ui-foundation-C47, shared-ui-foundation-C48, shared-ui-foundation-C49, shared-ui-foundation-C50, shared-ui-foundation-C60, shared-ui-foundation-C63 | +| UIF-06 | shared-ui-foundation-C17, shared-ui-foundation-C18, shared-ui-foundation-C19, shared-ui-foundation-C20, shared-ui-foundation-C21, shared-ui-foundation-C22, shared-ui-foundation-C23, shared-ui-foundation-C24, shared-ui-foundation-C25, shared-ui-foundation-C26, shared-ui-foundation-C27, shared-ui-foundation-C28, shared-ui-foundation-C29, shared-ui-foundation-C30, shared-ui-foundation-C31, shared-ui-foundation-C32, shared-ui-foundation-C49, shared-ui-foundation-C57 | +| UIF-07 | shared-ui-foundation-C02, shared-ui-foundation-C33, shared-ui-foundation-C34, shared-ui-foundation-C35, shared-ui-foundation-C36, shared-ui-foundation-C37, shared-ui-foundation-C38, shared-ui-foundation-C39, shared-ui-foundation-C40, shared-ui-foundation-C41, shared-ui-foundation-C42, shared-ui-foundation-C50, shared-ui-foundation-C51, shared-ui-foundation-C52, shared-ui-foundation-C53, shared-ui-foundation-C54, shared-ui-foundation-C55, shared-ui-foundation-C56, shared-ui-foundation-C57, shared-ui-foundation-C58, shared-ui-foundation-C64 | +| UIF-08 | shared-ui-foundation-C59, shared-ui-foundation-C60, shared-ui-foundation-C61, shared-ui-foundation-C62, shared-ui-foundation-C63, shared-ui-foundation-C64 | + +## 18. Implementation sequence + +1. Merge this truthful documentation prerequisite; retry the existing issue #7 against the new trusted + source. Native clarification, SDD publication and exact branch admission still precede feature code. +2. Resolve toolchain/support pins, signatures, registration and package rights; establish CI Check, + independent coverage/architecture checks and test-first core/adapter scaffolding. +3. Prove real packed consumption in all required frameworks and server-shell/client boundaries. +4. Inventory provenance, then extract visual families once with contracts, accessibility and stories. +5. Complete the Storybook delivery and consumer documentation from the same shipped implementation. +6. Adopt actual reviewed artifacts through Gateway and Domus work, preserve recovery and verify both + deployment modes; publish only when each required gate has supporting evidence. + +## 19. Definition of Done + +- [ ] Native feature SDD publication and exact Action-managed branch readiness verified. +- [ ] All eight requirements and 64 cases map to real tests/evidence without duplicates. +- [ ] One component implementation; four working adapters plus direct Lit/DOM consumption. +- [ ] Clean Architecture, complete per-module/global coverage and every-commit CI Check pass. +- [ ] Typed properties/events, forms, slots, lifecycle, client/SSR boundaries and compatibility matrix pass. +- [ ] Production-backed Storybook, EN/ES, themes, narrow/wide keyboard and reduced motion reviewed. +- [ ] MIT/source/asset provenance and package rights verified; packed/source integrity and Pages delivery accepted. +- [ ] Real Gateway and Domus adoption preserves business behavior, permissions, drafts and recovery. +- [ ] User/contributor/operator documentation, migrations and retained rollback are complete. +- [ ] Catalog evidence and original SDK-generated index match real implemented files and current status. +- [ ] No required adapter, app acceptance, human review or publication gate remains unverified. + +## 20. References and decisions + +- [ha-ui feature #7](https://github.com/vypdev/ha-ui/issues/7): full first-cohort scope. +- [Domus adoption #30](https://github.com/vypdev/domus/issues/30): app-owned shell and workflows. +- [Gateway source](https://github.com/vypdev/homeassistant-gateway): read-only extraction reference; exact code/license pin must accompany copied material. +- [Lit React integration](https://lit.dev/docs/frameworks/react/), [Vue Web Components](https://vuejs.org/guide/extras/web-components.html), [Angular custom elements schema](https://angular.dev/api/core/CUSTOM_ELEMENTS_SCHEMA) and [Svelte custom elements](https://svelte.dev/docs/svelte/custom-elements): primary integration references, not execution evidence for this library. +- [Copilot pre-branch SDDs](https://github.com/vypdev/copilot/blob/3e7ceea08e6131360e026937d1c0cc1b120472e0/docs/issues/pre-branch-sdds.mdx): proposed bootstrap and native gate. + +Decisions: one repo/visual implementation; required React/Vue/Angular/Svelte adapters; Lit/DOM direct +use; own Storybook; Domus owns UX except the common upper bar pattern. Rejected: duplicated visual +libraries per framework, copying app components into Domus, or claiming Web Components alone prove +framework support. Exact signatures and toolchain pins remain engineering gates for issue #7. From 2c14ddcd38cdebfc17922614e286c34c2038c392 Mon Sep 17 00:00:00 2001 From: "copilot-action[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 4 Nov 2026 04:39:22 +0100 Subject: [PATCH 02/55] docs(sdd): specify issue contract in specs/shared-ui-framework-delivery.md --- specs/CATALOG.md | 4 +- specs/catalog.json | 3 +- specs/shared-ui-framework-delivery.md | 413 ++++++++++++++++++++++++++ 3 files changed, 417 insertions(+), 3 deletions(-) create mode 100644 specs/shared-ui-framework-delivery.md diff --git a/specs/CATALOG.md b/specs/CATALOG.md index cae5998..2ddf040 100644 --- a/specs/CATALOG.md +++ b/specs/CATALOG.md @@ -10,7 +10,7 @@ debt or convert unknown historic intent into a design decision. | Capability ID | Status | Scope | Primary SDD | Evidence | |---|---|---|---|---| -| `shared-ui-foundation` | Proposed | One DOM component implementation with React, Vue, Angular and Svelte adapters, direct Lit/DOM use, shared Storybook and verified Gateway/Domus adoption. Draft; no runtime implementation or delivery accepted. | [Shared Home Assistant presentation components and framework adapters](./shared-ui-foundation.md) | 3 paths · 2026-11-04 | +| `shared-ui-foundation` | Proposed | One DOM component implementation with React, Vue, Angular and Svelte adapters, direct Lit/DOM use, shared Storybook and verified Gateway/Domus adoption. Draft; no runtime implementation or delivery accepted. | [Shared Home Assistant presentation components and framework adapters](./shared-ui-foundation.md) + 1 companion | 3 paths · 2026-11-04 | ## Evidence map @@ -18,7 +18,7 @@ debt or convert unknown historic intent into a design decision. - Owner: vypdev maintainers; Efrain Espada (accountable product/publication owner) - Last verified: 2026-11-04 -- Specifications: [`specs/shared-ui-foundation.md`](./shared-ui-foundation.md) +- Specifications: [`specs/shared-ui-foundation.md`](./shared-ui-foundation.md) · [`specs/shared-ui-framework-delivery.md`](./shared-ui-framework-delivery.md) - Workflows: [`.github/workflows/copilot_issue.yml`](../.github/workflows/copilot_issue.yml) - Entrypoints: Not yet implemented (proposed capability). - Core code: Not yet implemented (proposed capability). diff --git a/specs/catalog.json b/specs/catalog.json index 77cbc13..be27dd6 100644 --- a/specs/catalog.json +++ b/specs/catalog.json @@ -9,7 +9,8 @@ "owner": "vypdev maintainers; Efrain Espada (accountable product/publication owner)", "lastVerified": "2026-11-04", "specs": [ - "specs/shared-ui-foundation.md" + "specs/shared-ui-foundation.md", + "specs/shared-ui-framework-delivery.md" ], "workflows": [ ".github/workflows/copilot_issue.yml" diff --git a/specs/shared-ui-framework-delivery.md b/specs/shared-ui-framework-delivery.md new file mode 100644 index 0000000..2021580 --- /dev/null +++ b/specs/shared-ui-framework-delivery.md @@ -0,0 +1,413 @@ +# Shared UI Framework Delivery + +- Status: Draft +- Date: 2026-11-04 +- Catalog capability ID: `shared-ui-foundation` +- Last verified: not applicable; this is a prospective companion +- Owners: vypdev maintainers; Efra Espada (accountable product and publication owner) +- Scope: Define executable delivery gates for the shared UI capability while retaining its existing owning contract. +- Related issues/PRs: [Issue #7](https://github.com/vypdev/ha-ui/issues/7); [Domus adoption issue #30](https://github.com/vypdev/domus/issues/30) +- Required review gates: product UX, architecture, testing, documentation, security/operations +- Open decisions blocking readiness: Exact locks, compiler/browser execution, justified public peer ranges, and verified npm publisher rights remain implementation gates. No unresolved product-scope decision is known. + +This companion supplements [the retained foundation specification](shared-ui-foundation.md), which remains authoritative for product goals, UX, architecture, the complete baseline case inventory, and Gateway and Domus adoption gates. It does not replace, redefine, or recount that baseline. It is a proposed addition under the existing catalog owner; it is not implementation evidence or branch readiness. + +## 1. Executive summary + +Deliver the retained shared component capability through a reproducible, tested package cohort. First establish a pinned toolchain and real compiled consumer fixtures; then accept one immutable packed candidate, exercise it in fixtures and both applications, verify publication authority and artifact integrity, and publish and adopt that same cohort. The implementation stays within the existing owner's contract. The three requirements below refine delivery controls only. + +```text +Pinned toolchain -> compiled fixtures and reviewed packed candidate -> Gateway and Domus checks -> verified publication -> installed consumer acceptance +``` + +No framework support, publication authority, or completed adoption is inferred from candidate versions or package names. Planned acceptance cases in this draft have not been executed. + +## 2. Problem, current behavior, and evidence + +### 2.1 Problem + +The owner needs delivery evidence that makes the same shared implementation reproducible across framework adapters and real consumers. Unlocked tools, changed candidate bytes, temporary fixture paths, or partial publication could otherwise produce misleading support or adoption claims. + +### 2.2 Current behavior + +1. The validated catalog at base `d885bccf2c3152c3151471d3efaabd901429d6bd` records `shared-ui-foundation` as proposed, with no runtime implementation, entrypoints, code, or tests. +2. The repository currently contains documentation and the reviewed Copilot installation; no shared package, adapters, or Storybook are implemented or published. +3. Candidate registry versions are read-only research, not executed support claims. No consumer acceptance or publication-rights evidence is supplied. + +### 2.3 Evidence + +- Code/workflow/documentation references: `specs/shared-ui-foundation.md`; `specs/catalog.json`; `specs/CATALOG.md`; `specs/README.md`; `specs/_template.md`; `README.md`; `docs/component-library.md`. +- Related source: [Gateway presentation sources at the cited revision](https://github.com/vypdev/homeassistant-gateway/tree/e47dde303e475a6d03e1d6697ec87a934e93263b/frontend/src/ui). +- Owner scope decision: [Domus issue comment](https://github.com/vypdev/domus/issues/30#issuecomment-6620523378). +- External primary sources: candidate versions from read-only official registry metadata: Lit 3.3.3, React 19.3.1, Vue 3.5.44, Angular 22.2.4, Svelte 5.57.3, Vite 8.3.5, Storybook 10.6.5, Vitest 5.1.0, Playwright 1.64.1, TypeScript 6.0.3, and Node 24.20.0. These are candidates only. Angular compiler-cli 22.2.4 requires TypeScript >=6.0 <6.1; registry-latest TypeScript 7.0.2 is incompatible. +- Unknowns: Actual passing locks and browser matrix, public peer ranges, npm publisher rights, and implementation results. + +### 2.4 Retrospective classification (as-built baselines only) + +Not applicable: this is prospective delivery work and reconstructs no implemented baseline. + +## 3. Actors, surfaces, and terminology + +| Actor | Goal | Entry point | Visible surfaces | +|---|---|---|---| +| Component maintainer | Build and verify the same implementation and adapters | Repository changes and CI | Checks, packed artifacts, Storybook, docs | +| Gateway and Domus maintainers | Verify and adopt the accepted cohort | Their admitted owning work | Consumer builds, browser behavior, deployment evidence | +| Publication owner | Verify provenance, rights, and release cohort | Release review | Integrity records and registry results | +| Application user | Use the retained application experience | Gateway or Domus | Each application's own UI | + +“Candidate” means a locally built packed artifact not yet accepted. “Accepted cohort” means the exact mutually reviewed package bytes and source revision. “Published cohort” means those exact bytes were observed at their authorized package coordinates. Neither term means application adoption. The primary product and technical language is English. + +## 4. Goals, non-goals, and fixed invariants + +### 4.1 Goals + +| Requirement | Behavior | +|---|---| +| UIFD-01 | The compatibility gate MUST reject incompatible or unlocked candidate toolchains before accepting package artifacts, while retaining the previous reviewed lock and support evidence. | +| UIFD-02 | Promotion MUST verify the exact accepted candidate archive bytes and prevent temporary candidate dependency paths from entering shipped consumer manifests. | +| UIFD-03 | Partial cohort publication MUST preserve individually observed version and digest results and refuse overwrites or a false complete-cohort outcome during recovery. | + +### 4.2 Non-goals + +1. Redefine the retained foundation's product goals, component API, UX, architecture, or 64-case baseline. +2. Treat candidate package versions as proven compatibility, publisher rights, or implementation evidence. +3. Add another catalog owner, visual implementation, or private temporary dependency to a shipped application. + +### 4.3 Fixed product/safety invariants + +1. `specs/shared-ui-foundation.md` remains verbatim and authoritative; its UIF-01..UIF-08, 64 distinct baseline cases, required adapters, and both app-adoption gates remain required. This companion neither redeclares nor recounts those cases. +2. One common implementation owns visual behavior and accessibility. Gateway and Domus retain their own experiences; only the shared upper dashboard-style tab bar is the common navigation pattern. +3. Never claim support, publication, or adoption without the corresponding executed evidence. Preserve Copilot source and installed resources unchanged. + +## 5. Current versus proposed product journey + +| Stage | Current | Proposed | User/operator effect | +|---|---|---|---| +| Toolchain | No implementation evidence | Frozen, compatible lock and support evidence gate artifacts | Reproducible candidate and explicit failure reason | +| Candidate | No accepted archive | Build and record source revision, archive digest, dependencies, licenses, and compatibility | Reviewers can identify exact bytes | +| Consumer check | No real fixtures or app acceptance | Test packed bytes in framework fixtures and both admitted application workstreams | Adoption refers to one candidate | +| Publication | No publication evidence | Verify rights; publish exact accepted cohort; record per-coordinate result | Partial release is visible and recoverable | + +```mermaid +flowchart LR + A[Freeze compatible toolchain] --> B[Build and digest candidate] + B --> C[Fixtures and both app checks] + C --> D[Verify rights and publish exact cohort] + D --> E[Installed consumer and deployment acceptance] +``` + +Text equivalent: freeze toolchain -> build candidate -> check fixtures and applications -> verify rights and publish -> accept installed consumer behavior. + +## 6. Functional behavior and state model + +### 6.1 Happy path + +1. CI validates exact direct and transitive locks, runtime/compiler compatibility, and the declared supported matrix. +2. The build produces reproducible packed artifacts and records source revision, package versions, integrity digests, dependencies, licenses, and compatibility. +3. Packed artifacts are installed in actual compiled direct-DOM and framework fixtures and exercised in both applications through their admitted work. +4. Review accepts the candidate only after the retained foundation gates and companion refinements pass. Publication rights and artifact digests are verified before publishing those exact bytes. +5. Consumer pins and deployed acceptance refer to the published version/digest cohort. Publication alone does not mark the capability implemented. + +### 6.2 Alternative paths + +- Incompatible or unlocked tools reject the candidate before acceptance; retain prior lock and evidence. +- Changed candidate bytes fail promotion; retain the accepted archive for inspection. +- A partial release records observed versions/digests by package and remains incomplete. Resume only exact verified results; never overwrite or claim completion. +- A rights failure blocks publication while allowing local candidate checks to be reported separately. + +### 6.3 State machine + +| State | Entered when | User-visible meaning | Allowed next states | Recovery/owner | +|---|---|---|---|---| +| Draft | Companion proposed | Delivery contract is not ready | Candidate | Maintainers resolve gates | +| Candidate | Frozen build produced | Unaccepted bytes available for checks | Accepted, rejected | CI and maintainers | +| Accepted | All applicable evidence and digests reviewed | Exact candidate may proceed to authorized release | Publishing, rejected on discrepancy | Review owner | +| Publishing | Authorized cohort release begins | Some package results may be irreversible | Published cohort, partial | Publication owner resumes by digest | +| Partial | One or more results observed, cohort incomplete | Release incomplete; consumers must not promote | Publishing, blocked | Publication owner inspects exact results | +| Published cohort | All exact package bytes observed | Packages available; app acceptance remains separate | Consumer acceptance | Application owners | +| Consumer accepted | Required installed/deployment evidence passes | Required adoption evidence is complete | Complete | Capability owner | +| Rejected/blocked | A gate fails | No new acceptance claim | Candidate after correction, or blocked | Responsible gate owner | + +Duplicate release attempts MUST treat an exact version and matching digest as already observed; a conflicting digest MUST fail closed. Out-of-order or stale evidence cannot advance state. No automatic live-registry retry is specified; the owner resumes after inspecting recorded results. + +## 7. User-facing configuration + +This companion adds no end-user configuration. Tool versions and package peer ranges are release engineering inputs, recorded in exact locks and the compatibility record; they are not user-selectable settings. Proposed candidate pins are the versions listed in 2.3, with Node 24.20.0 and TypeScript 6.0.3 candidates. The reviewed lock MUST satisfy engine and compiler constraints; no absent or inconsistent lock may resolve implicit latest versions. Only versions actually passing compiled fixtures and browser checks may become supported pins or justified public peer ranges. The lock is immutable for an accepted candidate. No runtime feature or safety invariant is configurable. + +## 8. Clean Architecture design + +### 8.1 Responsibilities and dependency direction + +| Layer/boundary | Owns | Must not own/import | +|---|---|---| +| Domain/pure policy | Compatibility, cohort completeness, digest and promotion invariants | Framework, registry SDK, application business rules | +| Application | Candidate acceptance and staged release decisions | Concrete registry clients or consumer-specific UX | +| Semantic ports | Read lock/build evidence, inspect packed bytes, record publish observations | Provider DTOs or policy decisions | +| Adapters/data | Package-manager, test-runner, browser, and authorized registry integration | Product policy | +| Infrastructure/composition | Frozen tools, deterministic fixtures, CI wiring | Duplicated compatibility decisions | +| Entrypoints | CI and explicit release invocation | Hidden retries or duplicated orchestration | +| Presentation | CI summaries, compatibility and release evidence | Mutations or acceptance policy | + +```mermaid +flowchart LR + E[CI or release entrypoint] --> A[Acceptance and promotion use cases] + A --> D[Compatibility and cohort policies] + A --> P[Evidence and registry ports] + I[Tool and registry adapters] --> P + A --> V[Review and operator evidence] +``` + +### 8.2 Contracts, state, and trust boundaries + +- Pure decisions: supported lock compatibility, candidate equality, promotion eligibility, and cohort completion. +- Application contracts: immutable source revision, lock digest, artifact identity/digest, and per-coordinate publication observation. +- Semantic ports: compatibility evidence, packed archive inspection, and publication observation. +- Durable state/schema ownership: repository lock and review evidence before release; append-only observed release record for publication results. +- Concurrency/idempotency: serialize publication per cohort; matching observed version/digest is idempotent; mismatch fails without overwrite. +- Trusted and untrusted inputs: package metadata, archives, consumer manifests, registry responses, and generated diagnostics are untrusted until structurally validated. +- Provider error mapping: distinguish incompatibility, integrity mismatch, authorization failure, transient provider error, and partial publication; never erase successful observations. + +### 8.3 Executable architecture constraints + +`tests/architecture.test.mjs` MUST enforce package dependency direction, reject application/service/business-rule imports from the component packages, and reject adapter dependencies on another framework adapter. `tests/delivery.test.mjs` MUST parse candidate and cohort records structurally and enforce digest, lock, and no-private-path rules. The common package alone owns visual behavior; adapters may bind framework lifecycle and forms behavior only. + +## 9. UI/UX and content contract + +### 9.1 Information hierarchy + +CI, release review, and docs show current state, verified completed facts, next transition, required human action, impact and inspectable links, then technical diagnostics. A package publication result is never presented as application adoption. + +### 9.2 Representative primary view + +```markdown +# Shared UI delivery: partial publication + +> **Current status:** 3 of 5 packages have matching published digests. +> +> **Action required:** Inspect the two remaining package results before resuming. + +## Progress + +- [x] Accepted candidate source revision and archive digests recorded +- [x] Three package results match the accepted cohort +- [ ] Two package results remain unverified +- [ ] Gateway and Domus installed acceptance remains pending + +## What happens next + +The publication owner checks the recorded versions and resumes only matching +accepted bytes. Consumer pins do not change while the cohort is partial. + +## Links + +[Issue #7](https://github.com/vypdev/ha-ui/issues/7) · [Release run]() · [Compatibility record](docs/compatibility.md) + +
Technical detailsSource revision, package coordinates, versions, digests, and sanitized provider diagnostics.
+``` + +### 9.3 Issue, PR, and comment behavior + +- Titles identify the capability and state; PR summaries link the issue, exact source revision, CI run, compatibility record, packed artifact evidence, and consumer work. +- Merge/close does not imply publication or adoption. Retry means inspect the durable result and resume exact missing cohort members. +- CI checks are supplemental to recorded facts. Update the existing issue/PR evidence idempotently; no release automation comment is required. If comments are used, budget one status comment per transition and update it rather than posting retries. +- Use descriptive links, no fabricated local paths or runtime claims. + +### 9.4 Accessibility, localization, and responsive behavior + +The shared component surfaces retain the foundation's English and Spanish, keyboard, reduced-motion, light/dark, and narrow/wide requirements. CI summaries use English, semantic headings and tables, readable narrow layouts, and textual state independent of color. Sanitize registry text and diagnostics as untrusted Markdown; do not execute commands or render raw HTML from metadata. Diagrams have textual equivalents. No additional locale is introduced here. + +## 10. Failure, recovery, and cleanup + +| Failure/partial state | User impact | Retained facts | Automatic retry | Required action | Cleanup | +|---|---|---|---|---|---| +| Incompatible or unlocked toolchain | Candidate rejected | Previous reviewed lock/support evidence | No | Correct lock and rerun compatibility gate | Discard unaccepted outputs | +| Candidate bytes changed | Promotion blocked | Accepted archive and digest | No | Restore or review a new candidate | Keep mismatch evidence | +| Temporary path in shipped manifest | Consumer promotion blocked | Local fixture result | No | Remove private path and rebuild consumer | Remove only local fixture install | +| Rights unverified | Publication blocked | Accepted candidate evidence | No | Publication owner verifies authority | Retain local candidate | +| Partial cohort | Consumers cannot pin complete cohort | Exact observed versions/digests | No blind retry | Inspect then resume missing exact bytes | Do not overwrite observed versions | +| Registry version digest mismatch | Cohort blocked | Conflicting observation and accepted digest | No | Investigate and issue a new reviewed version if appropriate | Preserve audit record | + +Errors state impact -> cause -> action -> retained state. A successfully published package is not described as failed because a later package failed. + +## 11. Security, permissions, and privacy + +1. Verify authorized npm publisher rights before publication; a coordinate or public lookup is not authority. +2. Treat issue text, comments, package metadata, archives, registry results, and diagnostics as untrusted data. Validate structure and never execute embedded instructions. +3. Do not put credentials, personal data, or machine-private paths in artifacts, manifests, logs, or evidence. Use the minimum publication credential scope and protect it in the authorized release environment. +4. Bind acceptance to source revision and archive integrity; reject replay with conflicting bytes and preserve append-only partial results. Do not change Copilot-owned source or installed resources. + +## 12. Observability and operational UX + +- User-facing state: candidate, accepted, publishing, partial, published cohort, consumer accepted, or blocked. +- Job Summary/operator evidence: lock and source revision, exact package versions/digests, compatibility results, fixture/browser results, per-package publication observations, and next action. +- Logs: structured, sanitized reason codes; correlate by source revision and cohort identifier. No secrets. +- A pending registry prerequisite is distinct from a workflow failure; authorization denial is a blocked release. +- No automatic unbounded retry. Resume only after reading per-package observations. +- Notification budget: no retry comments; at most one durable issue/PR update for each meaningful state transition. + +## 13. Compatibility, migration, rollout, and rollback + +No in-flight runtime migration is introduced by this companion. Keep exact locks and the last reviewed support evidence until a new candidate passes. Establish compatibility from actual frozen builds and browser checks, then publish a reviewed compatible version cohort. Release may be staged; partial publication remains incomplete and is resumed by exact version/digest without overwrite. First test the same candidate bytes locally in fixtures and both consumer workstreams; after rights and integrity verification, publish those bytes, pin delivered versions in the actual repositories, then complete installed/deployment acceptance. Do not make an application depend on a private temporary path. Rollback consumer pins only through each application's owning admitted work and recovery contract; package publication itself is not treated as reversible. + +## 14. Testing strategy and numeric budget + +The six new refinement cases below are additional to, and do not recount, all 64 retained baseline cases `shared-ui-foundation-C01` through `shared-ui-foundation-C64`. They are deterministic acceptance tests using isolated toolchain, archive, manifest, and registry-provider fixtures; they do not claim real registry publication. The six-case minimum is a floor for this companion only. All authored implementation still requires 100% supported executable statement, branch, function, and line coverage, with an independent source inventory and the retained foundation's full coverage and CI gates. CI runs applicable checks for every PR update, including type, architecture, build, browser, packaging, and coverage evidence. Use no live services or real waits; test exact failure and recovery facts. Human review must separately inspect application UX and deployment evidence. + +| Area | Minimum distinct cases | Behaviors/risks covered | +|---|---:|---| +| Toolchain compatibility rejection | 2 | Observable scenarios in the case inventory. | +| Candidate promotion integrity | 2 | Observable scenarios in the case inventory. | +| Partial cohort recovery | 2 | Observable scenarios in the case inventory. | +| Total | 6 | Distinct inventory cases; repeated executions do not add cases. | + +| Case ID | Area | Requirements | Scenario | +|---|---|---|---| +| shared-ui-framework-delivery-C01 | Toolchain compatibility rejection | UIFD-01 | With the supported Angular compiler candidate, substituting incompatible TypeScript 7 fails the compatibility/build gate before artifact acceptance; the reviewed TypeScript 6 lock remains unchanged. | +| shared-ui-framework-delivery-C02 | Toolchain compatibility rejection | UIFD-01 | An absent or inconsistent frozen transitive lock fails candidate creation without resolving an implicit latest dependency or changing retained lock/support evidence. | +| shared-ui-framework-delivery-C03 | Candidate promotion integrity | UIFD-02 | A candidate archive whose bytes change after acceptance fails digest/source-cohort verification before publication or consumer promotion, retaining the original accepted archive. | +| shared-ui-framework-delivery-C04 | Candidate promotion integrity | UIFD-02 | A staged app manifest with a machine-private temporary package dependency fails the shipped-consumer gate, while an isolated fixture can still exercise the accepted packed candidate locally. | +| shared-ui-framework-delivery-C05 | Partial cohort recovery | UIFD-03 | A simulated failure while publishing one adapter retains each already-observed exact version/digest and reports an incomplete cohort; it neither updates shipped consumer pins nor repeats verified successful publications. | +| shared-ui-framework-delivery-C06 | Partial cohort recovery | UIFD-03 | Recovery observes an existing version with different bytes than its accepted cohort record and fails without overwriting, silently substituting another version, or claiming that all adapters are delivered. | + +## 15. Documentation and discoverability + +| Audience | Artifact/page | Required content | Validation/navigation | +|---|---|---|---| +| User and application maintainer | `docs/component-library.md` | Normal adoption route, package use, and separation of publication from adoption | Links and packed consumer fixtures | +| Setup and release owner | `docs/compatibility.md`; `docs/provenance.md` | Exact matrix, rights, dependencies, licenses, revision, integrity, and staged release recovery | Compatibility and delivery checks | +| Operator | `docs/component-library.md` | State meanings and partial release recovery | Release evidence links | +| Contributor | `docs/migration.md`; `examples/consumers/README.md` | Migration, architecture, and compiled-consumer boundaries | Examples synchronized with fixtures | +| Storybook reviewer | `stories/foundation.stories.ts` | Shipped implementation examples and state/theme controls | Storybook build and review | + +Prospective repository files (current documents are classified against the immutable base `d885bccf2c3152c3151471d3efaabd901429d6bd`): + +| Path | Change | +|---|---| +| `specs/shared-ui-framework-delivery.md` | create | +| `specs/shared-ui-foundation.md` | reference | +| `specs/catalog.json` | update | +| `specs/CATALOG.md` | update | +| `specs/README.md` | reference | +| `specs/_template.md` | reference | +| `README.md` | update | +| `docs/component-library.md` | update | +| `package.json` | create | +| `pnpm-workspace.yaml` | create | +| `pnpm-lock.yaml` | create | +| `tsconfig.json` | create | +| `LICENSE` | create | +| `.github/workflows/ci_check.yml` | create | +| `.github/workflows/storybook_pages.yml` | create | +| `packages/core/package.json` | create | +| `packages/core/src/index.ts` | create | +| `packages/core/src/contracts.ts` | create | +| `packages/core/src/register.ts` | create | +| `packages/react/package.json` | create | +| `packages/react/src/index.ts` | create | +| `packages/vue/package.json` | create | +| `packages/vue/src/index.ts` | create | +| `packages/angular/package.json` | create | +| `packages/angular/src/index.ts` | create | +| `packages/svelte/package.json` | create | +| `packages/svelte/src/index.ts` | create | +| `tests/contracts/core.test.ts` | create | +| `tests/contracts/adapters.test.ts` | create | +| `tests/architecture.test.mjs` | create | +| `tests/coverage-evidence.test.mjs` | create | +| `tests/consumers/packed.test.ts` | create | +| `tests/browser/components.spec.ts` | create | +| `examples/consumers/README.md` | create | +| `.storybook/main.ts` | create | +| `.storybook/preview.ts` | create | +| `stories/foundation.stories.ts` | create | +| `docs/compatibility.md` | create | +| `docs/migration.md` | create | +| `docs/provenance.md` | create | +| `docs/evidence/shared-ui-foundation.json` | create | +| `packages/core/src/components/button.ts` | create | +| `packages/core/src/components/field.ts` | create | +| `packages/core/src/components/tabs.ts` | create | +| `packages/core/src/components/dialog.ts` | create | +| `packages/core/src/tokens.ts` | create | +| `packages/svelte/src/Button.svelte` | create | +| `packages/svelte/src/Field.svelte` | create | +| `packages/svelte/src/Tabs.svelte` | create | +| `packages/svelte/src/Dialog.svelte` | create | +| `playwright.config.ts` | create | +| `vitest.config.ts` | create | +| `examples/dom/package.json` | create | +| `examples/dom/README.md` | create | +| `examples/react/package.json` | create | +| `examples/react/README.md` | create | +| `examples/vue/package.json` | create | +| `examples/vue/README.md` | create | +| `examples/angular/package.json` | create | +| `examples/angular/README.md` | create | +| `examples/svelte/package.json` | create | +| `examples/svelte/README.md` | create | +| `examples/next/package.json` | create | +| `examples/next/README.md` | create | +| `examples/nuxt/package.json` | create | +| `examples/nuxt/README.md` | create | +| `examples/sveltekit/package.json` | create | +| `examples/sveltekit/README.md` | create | +| `tooling/packaging.mjs` | create | +| `tooling/compatibility.mjs` | create | +| `tests/delivery.test.mjs` | create | + +The catalog/index update registers the companion under the existing owner; it does not claim implementation. `LICENSE` preserves the upstream MIT source notice and every reused asset requires provenance verification. The Copilot installation and its source remain unchanged. Package coordinates are identifiers, not repository paths. + +## 16. Acceptance scenarios + +1. Given a frozen supported candidate lock, when compatibility and compiled consumer checks run, then only passing toolchain evidence can advance a candidate toward review; planned tests are not treated as completed results. +2. Given the Angular compiler candidate, when TypeScript 7 is substituted, then the candidate is rejected and the reviewed TypeScript 6 lock/support evidence remains intact. +3. Given an accepted archive, when its bytes differ during promotion, then publication and consumer promotion fail and the original accepted bytes remain inspectable. +4. Given an application manifest with a private temporary candidate dependency, when the shipped-consumer gate runs, then shipping is blocked while isolated fixture testing remains possible. +5. Given publication succeeds for only part of a cohort, when recovery begins, then exact per-package results remain recorded and no consumer pins or complete-cohort claim are advanced. +6. Given an existing package version has a conflicting digest, when recovery inspects it, then it neither overwrites nor substitutes the artifact and remains incomplete. +7. Given candidate evidence passes, when publication rights are not verified, then release is blocked with the accepted candidate retained and a clear owner action. +8. Given a package cohort is published, when application acceptance is reviewed, then publication alone does not satisfy Gateway or Domus adoption; each retained owning gate must pass. + +## 17. Requirements traceability + +| Requirement | Cases | +|---|---| +| UIFD-01 | shared-ui-framework-delivery-C01, shared-ui-framework-delivery-C02 | +| UIFD-02 | shared-ui-framework-delivery-C03, shared-ui-framework-delivery-C04 | +| UIFD-03 | shared-ui-framework-delivery-C05, shared-ui-framework-delivery-C06 | + +The retained foundation's requirements and cases remain traced in `specs/shared-ui-foundation.md`; this table covers only the three companion refinements. Documentation and architecture deliverables are additionally verified by their named structural checks and the retained baseline's gates, not by inventing generic cases. + +## 18. Implementation sequence + +1. Register this companion under the existing catalog owner, retaining the primary specification verbatim. +2. Freeze compatible direct and transitive toolchain locks; implement and run the compatibility rejection gate. +3. Establish package contracts and dependency constraints, then compiled direct-DOM and four framework fixtures using actual packed artifacts. +4. Implement accepted-candidate digest and no-private-path promotion checks; retain the full foundation's tests-first, coverage, component, Storybook, and browser scope. +5. Exercise the immutable candidate in both admitted application workstreams and complete their separate UX, deployment, and Bugbot gates. +6. Verify publication rights and provenance, then publish the exact accepted compatible cohort. Record and recover partial outcomes by exact version/digest. +7. Pin the delivered cohort in actual consumers and complete installed/deployment acceptance; update catalog evidence only for verified implementation. +8. Complete human UX review, documentation, original SDK catalog validation, and all Definition of Done gates. + +## 19. Definition of Done + +- [ ] The retained `specs/shared-ui-foundation.md` remains verbatim and its UIF-01..UIF-08, all 64 cases, adapters, Storybook, and both app-adoption gates pass. +- [ ] UIFD-01 through UIFD-03 have their six distinct executable refinement cases and reciprocal traceability. +- [ ] Exact supported locks and compatibility matrix come from passing builds and browser evidence; no broad or legacy support is inferred. +- [ ] Candidate acceptance and consumer promotion verify exact archive bytes and reject private temporary dependencies. +- [ ] All authored implementation has 100% supported statement, branch, function, and line coverage; source inventory, type, architecture, build, browser, and packaging checks pass for each PR update. +- [ ] Provenance, licenses, source revision, dependency list, integrity, and publisher rights are verified before publication. +- [ ] Partial publication recovery preserves exact observations and never overwrites or falsely completes a cohort. +- [ ] Actual packed artifacts pass fixtures; Gateway and Domus separately pass their owning installed/deployment and human UX gates with no open, reopened, or unknown Bugbot findings. +- [ ] English documentation, migration, troubleshooting, compatibility, provenance, and examples are discoverable and validated. +- [ ] Catalog and generated view are current using the documented original SDK validation; catalog evidence does not claim absent implementation. +- [ ] Copilot source and installed resources remain unchanged; all required review gates and original publication checks pass. + +## 20. References and decisions + +- Primary owner contract: `specs/shared-ui-foundation.md`. +- Canonical issue: [vypdev/ha-ui issue #7](https://github.com/vypdev/ha-ui/issues/7). +- Consumer adoption: [vypdev/domus issue #30](https://github.com/vypdev/domus/issues/30); Gateway work remains with its owning admitted work. +- Gateway source evidence: [presentation directory at cited revision](https://github.com/vypdev/homeassistant-gateway/tree/e47dde303e475a6d03e1d6697ec87a934e93263b/frontend/src/ui). +- Decisions: one Lit implementation behind DOM contracts and framework adapters; no HA private runtime dependency; candidate testing precedes publication; both applications exercise the same immutable candidate before rights-verified release; publication and application adoption remain separate evidence. +- Follow-up outside this companion: none. All product and component acceptance remains governed by the retained foundation specification. \ No newline at end of file From 2fd60c66826483a07b8162e1ae839cb4ba4ea6b3 Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Wed, 4 Nov 2026 05:05:09 +0100 Subject: [PATCH 03/55] feat: establish tested shared navigation contracts and package quality gates --- .github/workflows/ci_check.yml | 59 ++ .gitignore | 7 + .node-version | 1 + .npmrc | 3 + LICENSE | 21 + README.md | 31 +- docs/compatibility.md | 32 + docs/component-library.md | 21 +- docs/evidence/shared-ui-foundation.json | 72 +++ docs/provenance.md | 19 + package.json | 23 + packages/core/LICENSE | 21 + packages/core/package.json | 31 + packages/core/src/contracts.ts | 68 +++ packages/core/src/index.ts | 2 + packages/core/tsconfig.json | 16 + pnpm-lock.yaml | 777 ++++++++++++++++++++++++ pnpm-workspace.yaml | 3 + specs/CATALOG.md | 12 +- specs/README.md | 7 +- specs/catalog.json | 29 +- specs/shared-ui-framework-delivery.md | 9 +- tests/architecture.test.mjs | 60 ++ tests/consumers/packed.test.ts | 54 ++ tests/contracts/core.test.ts | 68 +++ tests/coverage-evidence.test.mjs | 12 + tests/coverage.test.mjs | 57 ++ tooling/coverage.mjs | 56 ++ tsconfig.json | 27 + vitest.config.ts | 6 + 30 files changed, 1568 insertions(+), 36 deletions(-) create mode 100644 .github/workflows/ci_check.yml create mode 100644 .gitignore create mode 100644 .node-version create mode 100644 .npmrc create mode 100644 LICENSE create mode 100644 docs/compatibility.md create mode 100644 docs/evidence/shared-ui-foundation.json create mode 100644 docs/provenance.md create mode 100644 package.json create mode 100644 packages/core/LICENSE create mode 100644 packages/core/package.json create mode 100644 packages/core/src/contracts.ts create mode 100644 packages/core/src/index.ts create mode 100644 packages/core/tsconfig.json create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 tests/architecture.test.mjs create mode 100644 tests/consumers/packed.test.ts create mode 100644 tests/contracts/core.test.ts create mode 100644 tests/coverage-evidence.test.mjs create mode 100644 tests/coverage.test.mjs create mode 100644 tooling/coverage.mjs create mode 100644 tsconfig.json create mode 100644 vitest.config.ts diff --git a/.github/workflows/ci_check.yml b/.github/workflows/ci_check.yml new file mode 100644 index 0000000..6aebdd7 --- /dev/null +++ b/.github/workflows/ci_check.yml @@ -0,0 +1,59 @@ +name: CI Check +on: + pull_request: + types: [opened, reopened, synchronize, ready_for_review, converted_to_draft] + push: + branches: [develop, master] + merge_group: + types: [checks_requested] +permissions: + contents: read +jobs: + quality: + name: CI Check + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version-file: .node-version + package-manager-cache: false + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + with: + run_install: false + cache: false + - run: pnpm install --frozen-lockfile --ignore-scripts + - run: pnpm check + - run: pnpm test + - run: pnpm build + - run: pnpm test:package + # Read the original catalog SDK at the reviewed revision; never rewrite it. + - name: Read the original specification SDK + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: vypdev/copilot + ref: 3e7ceea08e6131360e026937d1c0cc1b120472e0 + path: .validation/copilot-sdk + persist-credentials: false + sparse-checkout-cone-mode: false + sparse-checkout: | + src/infrastructure/specification_catalog_validator.cjs + src/infrastructure/specification_catalog_publication.cjs + src/domain/specification_catalog_path_policy.cjs + - name: Validate the actual specification catalog + run: >- + node -e "require('./.validation/copilot-sdk/src/infrastructure/specification_catalog_validator.cjs').main([], process.cwd())" + - name: Retain source coverage and compiled package evidence + if: ${{ always() }} + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: shared-ui-validation-${{ github.sha }} + path: | + coverage/ + packages/core/dist/ + test-results/ + if-no-files-found: error + retention-days: 7 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fb75e53 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +coverage/ +.validation/ +work/ +**/dist/ +**/test-results/ +**/.vite/ diff --git a/.node-version b/.node-version new file mode 100644 index 0000000..b7179ae --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +24.20.0 diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..68ef455 --- /dev/null +++ b/.npmrc @@ -0,0 +1,3 @@ +engine-strict=true +strict-peer-dependencies=true +auto-install-peers=false diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..31822ab --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 vypdev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 6e50062..6b79800 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,25 @@ Shared presentation components for Home Assistant applications, including Gateway and Domus. -The proposed library has one Lit component implementation, direct Lit/HTML consumption and real -React, Vue, Angular and Svelte adapters. Each app owns its data, permissions, routes and workflows. -One Storybook will document the same components and their accessibility/theme/localization states. - -The repository is configured; product implementation and publication have not started. -Start with the [component-library guide](docs/component-library.md), -[primary Draft](specs/shared-ui-foundation.md), [specification standard](specs/README.md) -and [capability catalog](specs/CATALOG.md). The [implementation issue](https://github.com/vypdev/ha-ui/issues/7) -retains the full scope and pending native specification gate. +The first source increment implements the DOM-independent navigation data contract and a pinned, +tested build/coverage foundation. Rendered components, the React/Vue/Angular/Svelte adapters, +compiled framework consumers, Storybook and publication remain required and unimplemented. +No package is published or ready for application adoption. + +Use Node 24.20.0 and the package manager pinned in package.json: + +```bash +pnpm install --frozen-lockfile --ignore-scripts +pnpm check +pnpm test +pnpm build +pnpm test:package +``` + +The contract validates controlled navigation or panel inputs without reading browser globals, +choosing application routes or accepting rendering callbacks. See the +[component guide](docs/component-library.md), [compatibility status](docs/compatibility.md), +[source provenance](docs/provenance.md), [foundation](specs/shared-ui-foundation.md), +[delivery companion](specs/shared-ui-framework-delivery.md) and [capability catalog](specs/CATALOG.md). +[Issue #7](https://github.com/vypdev/ha-ui/issues/7) retains all 64 baseline cases and six delivery +refinements. Clean checks on this increment do not complete that scope. diff --git a/docs/compatibility.md b/docs/compatibility.md new file mode 100644 index 0000000..6a0d70e --- /dev/null +++ b/docs/compatibility.md @@ -0,0 +1,32 @@ +# Compatibility and validation status + +Only the DOM-independent navigation input contract and its build/coverage foundation are implemented. +No rendered component, framework adapter or framework SSR support is currently accepted. + +| Tool | Exact pin | Current use | +|---|---|---| +| Node | 24.20.0 | Local validation and CI | +| pnpm | 10.34.0 | Frozen workspace install | +| TypeScript | 6.0.3 | Strict contract/type declarations; compatible candidate for Angular compiler-cli 22.2.4 | +| Vite | 8.3.5 | Test transform toolchain | +| Vitest and V8 coverage | 5.1.0 | All four metrics globally and per authored runtime file | + +The core preview coordinate/version is package: @vypdev/ha-ui at 0.1.0-next.1. It is unpublished; +its source currently contains presentation data contracts only. It is not a supported component release. +React 19.3.1, Vue 3.5.44, Angular 22.2.4 and Svelte 5.57.3 remain candidate pins until actual packed, +compiled/browser fixtures pass. Next.js, Nuxt and SvelteKit client/SSR boundaries remain unverified. +Do not infer support from custom-element interoperability or compiler metadata alone. + +The controlled navigation model requires kind, label, selected and items. Kind is navigation or panels; +selected is an existing enabled item ID or null. IDs use 1..128 ASCII letters, digits, underscore or +hyphen; accessible labels contain 1..256 Unicode code points without control/unpaired surrogate values. +At most 64 items are accepted, without truncation. Navigation items require an explicit HTTP(S) or +relative application link; panel items omit links and use item IDs for future named panel slots. +Disabled defaults to false. Labels and links retain their original values; snapshots are deeply frozen. +Invalid/extra fields, conflicting selection and unsafe or credential-bearing links raise the bounded +NavigationInputError with code invalid_navigation. This validates data, not authorization or rendering. + +CI Check runs for each PR head with no path filter. Current gates are strict types, all implemented +tests, independent source-inventory coverage, architecture/CI contracts, compiled declarations, actual reproducible packed-contract consumption without DOM types, +and the original pinned Copilot catalog validator. Browser, packed framework fixtures, Storybook, publication +and actual application acceptance must be added with their implementations before release readiness. diff --git a/docs/component-library.md b/docs/component-library.md index 85c5c94..e92abee 100644 --- a/docs/component-library.md +++ b/docs/component-library.md @@ -1,7 +1,8 @@ # Shared component library -The library is being specified. No core package, adapter, Storybook site or consumer migration -is implemented or published yet. The [primary Draft](../specs/shared-ui-foundation.md), +The first source increment implements the DOM-independent navigation data contract and its +tested quality/build foundation. Rendered components, adapters, Storybook and consumer migrations +are not implemented or published yet. The [primary Draft](../specs/shared-ui-foundation.md), [capability catalog](../specs/CATALOG.md) and [feature issue #7](https://github.com/vypdev/ha-ui/issues/7) hold the proposed contract and pending acceptance gates. @@ -48,8 +49,8 @@ Core/adapter publication, Storybook delivery and each app's acceptance are disti All authored behavior starts with tests and requires 100% supported statements, branches, functions and lines globally and per module, with independent source inventory and executable architecture -constraints. The primary Draft allocates 64 distinct minimum cases. Future CI Check must aggregate -all required checks on every PR head; this documentation bootstrap does not create that workflow. +constraints. The primary Draft allocates 64 distinct minimum cases. CI Check must aggregate +all required implemented checks on every PR head; the initial CI now validates all implemented source; unimplemented browser/adapter/adoption gates remain pending. Record exact toolchain, source, lock, package digests and framework/browser results. Verify npm publication rights before any publish. Preserve one tested previous app/cohort rollback and all user data and original-operation recovery. Never overwrite an existing package version after partial delivery. @@ -82,13 +83,13 @@ node -e 'const path = require("node:path"); const sdk = require(path.join(proces Generation writes only the target generated index. Validation checks actual path/cardinality and index agreement; it does not establish semantic completeness, native feature admission, implemented behavior or release readiness. Do not run an absolute SDK CLI without the explicit target root: -its default root belongs to the external tool checkout. Repository-local reproducible validation -scripts and CI are feature implementation deliverables. Do not invent or weaken a replacement +its default root belongs to the external tool checkout. CI reads the same original SDK at its exact pinned revision; it validates rather than regenerates the index. +The local procedure above remains available for catalog maintenance. Do not invent or weaken a replacement validator to turn missing evidence into acceptance. ## Next transition -Merge the reviewed documentation prerequisite, then retry existing issue #7 against the changed -trusted source. The native Action must clarify and publish the feature SDD and expose its exact -ready branch before implementation. The standard/template describe the expected format; this -bootstrap does not set a native readiness marker or bypass that lifecycle. +Native issue #7 published the delivery companion on its exact managed branch before implementation. +Complete the component renderers, actual framework adapters/consumers, Storybook and staged delivery +acceptance on that branch, retaining all 64 baseline and six companion cases. The first source +contract and green incremental tests do not establish a complete component library or app adoption. diff --git a/docs/evidence/shared-ui-foundation.json b/docs/evidence/shared-ui-foundation.json new file mode 100644 index 0000000..5b18151 --- /dev/null +++ b/docs/evidence/shared-ui-foundation.json @@ -0,0 +1,72 @@ +{ + "recordedAtUTC": "2026-11-04T04:03:52.911705+00:00", + "status": "Partial source increment; no rendered component, adapter, Storybook, framework support, published release or app adoption accepted.", + "issue": 7, + "nativePublication": { + "run": 41767973796, + "commit": "2c14ddcd38cdebfc17922614e286c34c2038c392", + "base": "d885bccf2c3152c3151471d3efaabd901429d6bd", + "branch": "feature/7-build-shared-home-assistant-components-with-react-vue-angular-and-svelte-adapters" + }, + "retainedFoundation": { + "sha256": "2c1de5b2fdd330461aa198a42d8099b9af2d00ab4ecbecfe4fdd755290b2f922", + "baselineCases": 64, + "additionalDeliveryCases": 6, + "allStillRequired": true + }, + "implemented": [ + "DOM-independent closed navigation/panel data contract; immutable snapshots, bounded text/items, explicit selection and safe links.", + "Pinned Node24.20.0/pnpm10.34.0/TypeScript6.0.3/Vitest5.1.0 build and test foundation.", + "105 passing unit/architecture/CI-contract tests; separate filesystem-inventory coverage audit and actual packed-consumer integration both pass.", + "Strict four-metric per-module/global coverage; every authored package/tool inventoried including the zero-executable re-export module.", + "Every-PR-commit CI Check uses frozen dependencies, all implemented gates, actual package compilation/consumption and the unchanged pinned original catalog SDK." + ], + "coverage": { + "statements": { + "total": 81, + "covered": 81, + "skipped": 0, + "pct": 100 + }, + "branches": { + "total": 77, + "covered": 77, + "skipped": 0, + "pct": 100 + }, + "functions": { + "total": 16, + "covered": 16, + "skipped": 0, + "pct": 100 + }, + "lines": { + "total": 58, + "covered": 58, + "skipped": 0, + "pct": 100 + } + }, + "sourceIdentities": { + "packages/core/src/contracts.ts": "6c37990359192c056daf54764870288aa20d4422fa88cfaf9186ccfae8fb101f", + "packages/core/src/index.ts": "10bd6958315d1ae23d216c0b752126e3fc7153f442f8c71e206cfa7e9757f86d", + "tooling/coverage.mjs": "2272a671a4d039fb40866f3535f6cd371b891623b64bf1b9a23a63677ad2387b" + }, + "packedContract": { + "scope": "Core data contract only; no rendered component, adapter or application adoption", + "sha256": "6d7a0da478ee9d1f5654323ca6cc9232d9f2bc07c94962313f17b7d7ecb0862c", + "bytes": 4399, + "reproducible": true, + "cleanOfflineInstall": true, + "compiledWithoutDomTypes": true, + "serverImport": true + }, + "limitations": [ + "Current navigation tests validate data without rendering DOM; no actual tab accessibility, focus, events or keyboard behavior is claimed.", + "The packed fixture covers the core data contract only; all actual Lit/DOM, React, Vue, Angular and Svelte UI consumers and SSR/client boundaries remain required.", + "No live npm publication/authority, static Storybook delivery, Gateway extraction/adoption or Domus adoption is claimed.", + "Fresh hosted CI and complete native review are required for the final source head." + ], + "copilotModified": false, + "capabilityComplete": false +} diff --git a/docs/provenance.md b/docs/provenance.md new file mode 100644 index 0000000..6c3f63b --- /dev/null +++ b/docs/provenance.md @@ -0,0 +1,19 @@ +# Source and asset provenance + +The repository and core package carry the MIT notice, copyright 2026 vypdev. The notice is retained +from [Gateway at e47dde303e475a6d03e1d6697ec87a934e93263b](https://github.com/vypdev/homeassistant-gateway/blob/e47dde303e475a6d03e1d6697ec87a934e93263b/LICENSE). +No Gateway visual component, font, icon or other asset is extracted in the first contract increment. +A reviewed per-family/source/asset inventory remains required before extraction and publication. + +The coverage policy and its negative unit fixtures are adapted from the same organization's MIT +[Domus coverage policy](https://github.com/vypdev/domus/blob/8c0a36c0b9804bdaf33a301a4dc85ae3dc12ee21/tooling/frontend-coverage.mjs) +and [policy tests](https://github.com/vypdev/domus/blob/8c0a36c0b9804bdaf33a301a4dc85ae3dc12ee21/tests/node/frontend-coverage.test.mjs). +The adaptation accepts multiple source directories so every package and authored tool is inventoried +independently of test imports. It is project-owned validation code, not a copied UI implementation. + +Catalog validation reads the unchanged original three-file Copilot SDK at revision +3e7ceea08e6131360e026937d1c0cc1b120472e0. CI checks out only those original files into an ignored +validation directory and calls the public main function with this repository's explicit root. +It does not change Copilot source or installed resources. Source hashes and the local procedure are +in the [component guide](component-library.md#catalog-validation). Catalog consistency, implemented +coverage, actual framework execution, publication rights and application adoption are separate facts. diff --git a/package.json b/package.json new file mode 100644 index 0000000..2c200c2 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "ha-ui-workspace", + "private": true, + "type": "module", + "packageManager": "pnpm@10.34.0", + "engines": { + "node": ">=24.20.0 <25" + }, + "scripts": { + "check": "tsc --noEmit -p tsconfig.json", + "test": "vitest run --coverage && node --test tests/coverage-evidence.test.mjs", + "build": "tsc -p packages/core/tsconfig.json", + "test:package": "node --test tests/consumers/packed.test.ts" + }, + "devDependencies": { + "@types/node": "24.19.2", + "@vitest/coverage-v8": "5.1.0", + "typescript": "6.0.3", + "vite": "8.3.5", + "vitest": "5.1.0", + "yaml": "2.9.0" + } +} diff --git a/packages/core/LICENSE b/packages/core/LICENSE new file mode 100644 index 0000000..31822ab --- /dev/null +++ b/packages/core/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 vypdev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 0000000..48b1c33 --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,31 @@ +{ + "name": "@vypdev/ha-ui", + "version": "0.1.0-next.1", + "description": "Shared presentation contracts and components for Home Assistant applications", + "type": "module", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/vypdev/ha-ui.git", + "directory": "packages/core" + }, + "files": [ + "dist", + "src", + "LICENSE" + ], + "sideEffects": false, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./contracts": { + "types": "./dist/contracts.d.ts", + "import": "./dist/contracts.js" + } + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/core/src/contracts.ts b/packages/core/src/contracts.ts new file mode 100644 index 0000000..f8e2db6 --- /dev/null +++ b/packages/core/src/contracts.ts @@ -0,0 +1,68 @@ +/** DOM-independent presentation data. Construction grants no application authority. */ +export type NavigationKind = 'navigation' | 'panels'; +export interface NavigationItem { + readonly id: string; + readonly label: string; + readonly disabled: boolean; + readonly href: string | null; +} +export interface NavigationState { + readonly kind: NavigationKind; + readonly label: string; + readonly selected: string | null; + readonly items: readonly NavigationItem[]; +} +export class NavigationInputError extends Error { + readonly code = 'invalid_navigation'; + constructor() { super('invalid_navigation'); this.name = 'NavigationInputError'; } +} +function invalid(): never { throw new NavigationInputError(); } +function object(value: unknown, allowed: readonly string[], required: readonly string[]): Record { + if (typeof value !== 'object' || value === null || Array.isArray(value)) invalid(); + if (Object.keys(value).some(key => !allowed.includes(key)) || required.some(key => !Object.hasOwn(value, key))) invalid(); + return value as Record; +} +function text(value: unknown, maximum: number): string { + if (typeof value !== 'string' || !value.trim() || Array.from(value).length > maximum) invalid(); + for (const character of value) { + const point = character.codePointAt(0)!; + if (point < 32 || point === 127 || point >= 0xd800 && point <= 0xdfff) invalid(); + } + return value; +} +function identifier(value: unknown): string { + const id = text(value, 128); + if (!/^[A-Za-z0-9_-]+$/u.test(id)) invalid(); + return id; +} +function link(value: unknown): string { + const href = text(value, 2048); + if (href.trim() !== href || href.startsWith('//') || href.includes('\\')) invalid(); + let parsed: URL; + try { parsed = new URL(href, 'https://ha-ui.invalid/'); } catch { return invalid(); } + if (!['https:', 'http:'].includes(parsed.protocol) || parsed.username || parsed.password) invalid(); + return href; +} +/** Validate a whole controlled state; never choose a route or invoke a renderer callback. */ +export function navigationState(raw: unknown): NavigationState { + const source = object(raw, ['kind', 'label', 'selected', 'items'], ['kind', 'label', 'selected', 'items']); + const kind = source.kind; + if (kind !== 'navigation' && kind !== 'panels') invalid(); + const label = text(source.label, 256); + if (!Array.isArray(source.items) || source.items.length > 64) invalid(); + const items = source.items.map((value: unknown): NavigationItem => { + const row = object(value, ['id', 'label', 'disabled', 'href'], ['id', 'label']); + const id = identifier(row.id), itemLabel = text(row.label, 256); + const disabled = row.disabled === undefined ? false : row.disabled; + if (typeof disabled !== 'boolean') invalid(); + const href = kind === 'navigation' ? link(row.href) : row.href == null ? null : invalid(); + return Object.freeze({id, label: itemLabel, disabled, href}); + }); + if (new Set(items.map(item => item.id)).size !== items.length) invalid(); + const selected = source.selected === null ? null : identifier(source.selected); + if (selected !== null) { + const current = items.find(item => item.id === selected); + if (current === undefined || current.disabled) invalid(); + } + return Object.freeze({kind, label, selected, items: Object.freeze(items)}); +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 0000000..abdb4ed --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,2 @@ +export {NavigationInputError, navigationState} from './contracts.js'; +export type {NavigationKind, NavigationItem, NavigationState} from './contracts.js'; diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 0000000..288b799 --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "noEmit": false, + "rootDir": "src", + "outDir": "dist", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "composite": false, + "inlineSources": true + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..92eb494 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,777 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: false + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@types/node': + specifier: 24.19.2 + version: 24.19.2 + '@vitest/coverage-v8': + specifier: 5.1.0 + version: 5.1.0(vitest@5.1.0) + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: 8.3.5 + version: 8.3.5(@types/node@24.19.2)(yaml@2.9.0) + vitest: + specifier: 5.1.0 + version: 5.1.0(@types/node@24.19.2)(@vitest/coverage-v8@5.1.0)(vite@8.3.5(@types/node@24.19.2)(yaml@2.9.0)) + yaml: + specifier: 2.9.0 + version: 2.9.0 + + packages/core: {} + +packages: + + '@babel/helper-string-parser@7.29.11': + resolution: {integrity: sha512-snZbO7MEDOof+hvNa97EjEY7qD7h2kwvQr5AJC3ZhQudKW5dnej6k3kP9NQV88hJwaS8vDqRLFo9rivLEhle2g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.12': + resolution: {integrity: sha512-WJ7JBp/Q4+z/ImydtI7jS6NVco5++632ExBemSX+Ysb+G1lCwQ5BVo3SY+jGGOcZXI4ZccbZSUSJLNBW6eEpJg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.29.11': + resolution: {integrity: sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@oxc-project/types@0.156.0': + resolution: {integrity: sha512-1Duyg48TdJNdyhEB8Xn1L36bT9Pv6GUtlKjHxOys8xkaF/rQq3XJE+bAurSK/AgIz5mGFsb77cn9Pb20+WCScw==} + + '@rolldown/binding-android-arm-eabi@1.2.16': + resolution: {integrity: sha512-ia3DwAs0OsMGS7+IConptSiyk9pEXkOx/AbJ2n/lAGxH3QkWQus1t8d7gnAT38FCU2sQu8lUbeR6AiZFv4JxaA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@rolldown/binding-android-arm64@1.2.16': + resolution: {integrity: sha512-kx5wBiMB6E7TEvpl2B9gjLU004BkvEm2Dr941BFjx/SontPwSzNuQUIoVKxHHoSjpFIfHMmcguyLOtyn0hObRg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.2.16': + resolution: {integrity: sha512-qslZrXnKJU4cUpjoAmtAo/KQM9e2IRMlmpig6Zpwj+8KWxW8v+nkAf9216Bsek9lOC1jvg2yCjygCFo2f83msQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.2.16': + resolution: {integrity: sha512-FdF+y48ND13eAyM2mQn+85kvAqNlvRWROy2rhgA463X/0Gv4vGtazZtfaF1mRBLscYvtOlh4QnLjOWiFzX69OQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.2.16': + resolution: {integrity: sha512-ddjqFM5e95AgLbUPtKIS7V1KTVlA0RUgCVr5mn+/IEhsdy3pr+Vuj/HZ7nlMN2FyxIwUktOH1LEPIQXO1v9y1Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.2.16': + resolution: {integrity: sha512-tHdKWbFkFBI687iim3LIyIvyL7j66VvQYkDPss5Stn/qyE2tWkR1YD0dXmXPFKKqT/o5ZEJVsaRsWS2meQwGOA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.2.16': + resolution: {integrity: sha512-FJiLGAblPBMlBEdbOqw6kW6QcXDWAf/oJu7gYs9ZJZwmg+K+/zjFLxPTIY9izJUzxzc24FHvmseyA0IvJSl17w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.2.16': + resolution: {integrity: sha512-hrJpblMsrQ58U9veWpbXzkhV5zStunds6HWLprqq1qauvgBr1E6P+24kLj6lSLI9vjRn/RYpjprhkHQPKGWpNg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.2.16': + resolution: {integrity: sha512-qJd/dC6j6CwLG9/s/RP2nb1RZ0GIgj7Ioj7x/SYa3RzjVrlzrmH0fhIOpfvqQDeIsdP0loi+kHaMhJzY1PBIaA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.2.16': + resolution: {integrity: sha512-Vx7Xg5uUJuNcujJjI70OICaH0EYj80BtOLZTSN7uOYg8QOLlyKnaG0IW7Fn/J0IpJsaKzmDKMhOQv1JBM9m9vg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.2.16': + resolution: {integrity: sha512-TNKhgE3uJTsTzatUSSYt0Kgo+Oa2uSlSjz4r1aEIea7HX6ULBym4fAlYUhG1i6pjNf2njfCUAfKwr6C3E9Xg1w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.2.16': + resolution: {integrity: sha512-7chXdpjXZ8qG0+mIxECN3cT9kvhZ3/oZhMX6Pp876P7CDrIy8nJswva6Mq357JR7eYXfuskwZxMeV1SzoZdHrA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.2.16': + resolution: {integrity: sha512-Kwjt+7uNTJK5/MDkGWGVO2KQPnj8yAV75dMpfwyHZrJSXWHrniOB0aUEWlVYf3H1hOqFLIStLdM5QXq6bnKmOQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-win32-arm64-msvc@1.2.16': + resolution: {integrity: sha512-/LH6WfjDRXXJz/p5xl4TmU+jpcnwcxbKEQBW/GZE0JHfN0lGIyTtsUEGaQ/q/GikkGJ2yLKe6pwm7AnEWO8pdw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.2.16': + resolution: {integrity: sha512-1lv1KvX0Z871zfk7vw+dnXMMnHNyDwMzWG8Kv3HxDXMOhzCX4SHc7HK319fInP0i+0p9D8brkfzL7pJOaGl+tw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.10': + resolution: {integrity: sha512-lTyOMnOcmRBzdCv6/4zUZCrEhV0xrqQiUZBZtVcbmLHBXKLnN8q89v/wKRF/Q0HbwVsqbIG3o5HDoFl9sduM+g==} + + '@types/node@24.19.2': + resolution: {integrity: sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==} + + '@vitest/coverage-v8@5.1.0': + resolution: {integrity: sha512-OY9aShBQG5KOIw2zCo9fDoCHI9RHliNHJ//nM7m6jGg41Hyke9JOGWmeMlK74xYErsxMoK8mIB3gAeEyFVA7LA==} + peerDependencies: + '@vitest/browser': 5.1.0 + vitest: 5.1.0 + peerDependenciesMeta: + '@vitest/browser': + optional: true + + '@vitest/istanbul-lib-coverage@2.1.0': + resolution: {integrity: sha512-WOjuS+xcjSIy7ruYBQ+9BUcV7XZQ0FyVcbWbWxW9TRx/kLGx5SQaeFRIFDJxjNplb6YsT7SpKo2zuXXUo91OIQ==} + engines: {node: '>=22'} + + '@vitest/istanbul-lib-report@2.1.0': + resolution: {integrity: sha512-+ZFta25nWtGCOBG9/IET8w1RO5Oawsd6H8Uf6AJ9EA/StFuKVtKAEJEJ6uRt84OH+T4YkI0ASmIHAhRA+0KVxw==} + engines: {node: '>=22'} + + '@vitest/mocker@5.1.0': + resolution: {integrity: sha512-5umB9/jlWcqVS6fsrFmv5Gdnpu6wAUio/lEOwAmvB7v+lFo/IW2B9FhAD0p19GCULTgMzLCdUmuCDCnAzWgppQ==} + peerDependencies: + msw: ^2.4.9 || ^3.0.0 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/spy@5.1.0': + resolution: {integrity: sha512-2DPO5OMHWKrBngC+fCmTQqemNF0gidRhp3lxbBmRhW+M1XJrfgPpqNqWVevXtn2f2ufD5ZaR1B6B641aAlcX5A==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-v8-to-istanbul@1.0.7: + resolution: {integrity: sha512-kFL68AG6ajd8fg248zwM9GQrUWEp79gsmjum34OEXjs4yHuUMZfYKwOLW9GMmB4oNvVrj+EAGxsP7ye2UR9UlA==} + + chai@6.3.0: + resolution: {integrity: sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==} + engines: {node: '>=18'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + magic-string@1.4.5: + resolution: {integrity: sha512-oLLC2+YvQUfAa+vxoufE/yc1icb87M9iXIRqAoR/2EhE/8cb3SF/+Gi0c2By1B+5Vz2e2vQtUvHiu1+63Znh+w==} + + magicast@0.5.5: + resolution: {integrity: sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==} + + nanoid@3.3.20: + resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + obug@3.0.0: + resolution: {integrity: sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==} + engines: {node: '>=12.20.0'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + postcss@8.5.32: + resolution: {integrity: sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==} + engines: {node: ^10 || ^12 || >=14} + + rolldown@1.2.16: + resolution: {integrity: sha512-bWvlMpFKiTfDKfnK7FC2MPgPeWCf/Kpwsq49ApdW+FQaBZb6O6dZ4MAPbPUjp7hYQrw/RobqCzn64BRrQecGHw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + source-map-js@1.2.2: + resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} + engines: {node: '>=0.10.0'} + + std-env@4.3.0: + resolution: {integrity: sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==} + + tinybench@6.2.3: + resolution: {integrity: sha512-Al01C2JIBnO2CXCAPbpAl6sUiOZHeThGtHYNTa51JRw9PyWOCdr+9Ong05R4qg/Sk3+H645E8ov1h2c58ityNw==} + engines: {node: '>=20.0.0'} + + tinyexec@1.3.1: + resolution: {integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.2.0: + resolution: {integrity: sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A==} + engines: {node: '>=14.0.0'} + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + + vite@8.3.5: + resolution: {integrity: sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.7.1 || ^0.8.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@5.1.0: + resolution: {integrity: sha512-MzmNFnrwa8v8lF1bLxw3UJ4EREwVOVb75VqJZmUDRjJ6EluK/x9th7qTbaeDk41WxE1GxWqmTXcdsfobfH5sOA==} + engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 5.1.0 + '@vitest/browser-preview': 5.1.0 + '@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0 + '@vitest/coverage-istanbul': 5.1.0 + '@vitest/coverage-v8': 5.1.0 + '@vitest/ui': 5.1.0 + happy-dom: '*' + jsdom: '*' + vite: ^6.4.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + why-is-node-running@3.2.1: + resolution: {integrity: sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==} + engines: {node: '>=20.11'} + hasBin: true + + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + +snapshots: + + '@babel/helper-string-parser@7.29.11': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/parser@7.29.12': + dependencies: + '@babel/types': 7.29.11 + + '@babel/types@7.29.11': + dependencies: + '@babel/helper-string-parser': 7.29.11 + '@babel/helper-validator-identifier': 7.29.7 + + '@bcoe/v8-coverage@1.0.2': {} + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@oxc-project/types@0.156.0': {} + + '@rolldown/binding-android-arm-eabi@1.2.16': + optional: true + + '@rolldown/binding-android-arm64@1.2.16': + optional: true + + '@rolldown/binding-darwin-arm64@1.2.16': + optional: true + + '@rolldown/binding-darwin-x64@1.2.16': + optional: true + + '@rolldown/binding-freebsd-x64@1.2.16': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.2.16': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.2.16': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.2.16': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.2.16': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.2.16': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.2.16': + optional: true + + '@rolldown/binding-linux-x64-musl@1.2.16': + optional: true + + '@rolldown/binding-openharmony-arm64@1.2.16': + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.2.16': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.2.16': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.10': {} + + '@types/node@24.19.2': + dependencies: + undici-types: 7.24.6 + + '@vitest/coverage-v8@5.1.0(vitest@5.1.0)': + dependencies: + '@bcoe/v8-coverage': 1.0.2 + '@vitest/istanbul-lib-coverage': 2.1.0 + '@vitest/istanbul-lib-report': 2.1.0 + ast-v8-to-istanbul: 1.0.7 + magicast: 0.5.5 + obug: 3.0.0 + std-env: 4.3.0 + tinyrainbow: 3.2.0 + vitest: 5.1.0(@types/node@24.19.2)(@vitest/coverage-v8@5.1.0)(vite@8.3.5(@types/node@24.19.2)(yaml@2.9.0)) + + '@vitest/istanbul-lib-coverage@2.1.0': {} + + '@vitest/istanbul-lib-report@2.1.0': + dependencies: + '@vitest/istanbul-lib-coverage': 2.1.0 + + '@vitest/mocker@5.1.0(vite@8.3.5(@types/node@24.19.2)(yaml@2.9.0))': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@vitest/spy': 5.1.0 + estree-walker: 3.0.3 + magic-string: 1.4.5 + optionalDependencies: + vite: 8.3.5(@types/node@24.19.2)(yaml@2.9.0) + + '@vitest/spy@5.1.0': {} + + assertion-error@2.0.1: {} + + ast-v8-to-istanbul@1.0.7: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 + + chai@6.3.0: {} + + detect-libc@2.1.2: {} + + es-module-lexer@2.3.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.10 + + expect-type@1.4.0: {} + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + fsevents@2.3.3: + optional: true + + js-tokens@10.0.0: {} + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + + magic-string@1.4.5: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + magicast@0.5.5: + dependencies: + '@babel/parser': 7.29.12 + '@babel/types': 7.29.11 + source-map-js: 1.2.2 + + nanoid@3.3.20: {} + + obug@3.0.0: {} + + picocolors@1.1.1: {} + + picomatch@4.0.7: {} + + postcss@8.5.32: + dependencies: + nanoid: 3.3.20 + picocolors: 1.1.1 + source-map-js: 1.2.2 + + rolldown@1.2.16: + dependencies: + '@oxc-project/types': 0.156.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm-eabi': 1.2.16 + '@rolldown/binding-android-arm64': 1.2.16 + '@rolldown/binding-darwin-arm64': 1.2.16 + '@rolldown/binding-darwin-x64': 1.2.16 + '@rolldown/binding-freebsd-x64': 1.2.16 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.16 + '@rolldown/binding-linux-arm64-gnu': 1.2.16 + '@rolldown/binding-linux-arm64-musl': 1.2.16 + '@rolldown/binding-linux-ppc64-gnu': 1.2.16 + '@rolldown/binding-linux-s390x-gnu': 1.2.16 + '@rolldown/binding-linux-x64-gnu': 1.2.16 + '@rolldown/binding-linux-x64-musl': 1.2.16 + '@rolldown/binding-openharmony-arm64': 1.2.16 + '@rolldown/binding-win32-arm64-msvc': 1.2.16 + '@rolldown/binding-win32-x64-msvc': 1.2.16 + + source-map-js@1.2.2: {} + + std-env@4.3.0: {} + + tinybench@6.2.3: {} + + tinyexec@1.3.1: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tinyrainbow@3.2.0: {} + + typescript@6.0.3: {} + + undici-types@7.24.6: {} + + vite@8.3.5(@types/node@24.19.2)(yaml@2.9.0): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.7 + postcss: 8.5.32 + rolldown: 1.2.16 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 24.19.2 + fsevents: 2.3.3 + yaml: 2.9.0 + + vitest@5.1.0(@types/node@24.19.2)(@vitest/coverage-v8@5.1.0)(vite@8.3.5(@types/node@24.19.2)(yaml@2.9.0)): + dependencies: + '@types/chai': 5.2.3 + '@vitest/mocker': 5.1.0(vite@8.3.5(@types/node@24.19.2)(yaml@2.9.0)) + chai: 6.3.0 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 1.4.5 + obug: 3.0.0 + picomatch: 4.0.7 + std-env: 4.3.0 + tinybench: 6.2.3 + tinyexec: 1.3.1 + tinyglobby: 0.2.17 + vite: 8.3.5(@types/node@24.19.2)(yaml@2.9.0) + why-is-node-running: 3.2.1 + optionalDependencies: + '@types/node': 24.19.2 + '@vitest/coverage-v8': 5.1.0(vitest@5.1.0) + transitivePeerDependencies: + - msw + + why-is-node-running@3.2.1: {} + + yaml@2.9.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..e535fd3 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - packages/* + - examples/* diff --git a/specs/CATALOG.md b/specs/CATALOG.md index 2ddf040..6557890 100644 --- a/specs/CATALOG.md +++ b/specs/CATALOG.md @@ -10,7 +10,7 @@ debt or convert unknown historic intent into a design decision. | Capability ID | Status | Scope | Primary SDD | Evidence | |---|---|---|---|---| -| `shared-ui-foundation` | Proposed | One DOM component implementation with React, Vue, Angular and Svelte adapters, direct Lit/DOM use, shared Storybook and verified Gateway/Domus adoption. Draft; no runtime implementation or delivery accepted. | [Shared Home Assistant presentation components and framework adapters](./shared-ui-foundation.md) + 1 companion | 3 paths · 2026-11-04 | +| `shared-ui-foundation` | Proposed | One DOM component implementation with React, Vue, Angular and Svelte adapters, direct Lit/DOM use, shared Storybook and verified Gateway/Domus adoption. Draft; no runtime implementation or delivery accepted. | [Shared Home Assistant presentation components and framework adapters](./shared-ui-foundation.md) + 1 companion | 15 paths · 2026-11-04 | ## Evidence map @@ -19,11 +19,11 @@ debt or convert unknown historic intent into a design decision. - Owner: vypdev maintainers; Efrain Espada (accountable product/publication owner) - Last verified: 2026-11-04 - Specifications: [`specs/shared-ui-foundation.md`](./shared-ui-foundation.md) · [`specs/shared-ui-framework-delivery.md`](./shared-ui-framework-delivery.md) -- Workflows: [`.github/workflows/copilot_issue.yml`](../.github/workflows/copilot_issue.yml) -- Entrypoints: Not yet implemented (proposed capability). -- Core code: Not yet implemented (proposed capability). -- Tests: Not yet implemented (proposed capability). -- User documentation: [`README.md`](../README.md) · [`docs/component-library.md`](../docs/component-library.md) +- Workflows: [`.github/workflows/copilot_issue.yml`](../.github/workflows/copilot_issue.yml) · [`.github/workflows/ci_check.yml`](../.github/workflows/ci_check.yml) +- Entrypoints: [`packages/core/src/index.ts`](../packages/core/src/index.ts) +- Core code: [`packages/core/src/contracts.ts`](../packages/core/src/contracts.ts) · [`tooling/coverage.mjs`](../tooling/coverage.mjs) +- Tests: [`tests/contracts/core.test.ts`](../tests/contracts/core.test.ts) · [`tests/coverage.test.mjs`](../tests/coverage.test.mjs) · [`tests/architecture.test.mjs`](../tests/architecture.test.mjs) · [`tests/coverage-evidence.test.mjs`](../tests/coverage-evidence.test.mjs) · [`tests/consumers/packed.test.ts`](../tests/consumers/packed.test.ts) +- User documentation: [`README.md`](../README.md) · [`docs/component-library.md`](../docs/component-library.md) · [`docs/compatibility.md`](../docs/compatibility.md) · [`docs/provenance.md`](../docs/provenance.md) · [`docs/evidence/shared-ui-foundation.json`](../docs/evidence/shared-ui-foundation.json) ## Maintenance contract diff --git a/specs/README.md b/specs/README.md index 79d6b54..79c5b5c 100644 --- a/specs/README.md +++ b/specs/README.md @@ -13,9 +13,10 @@ The capability inventory and its implementation evidence live in human-readable view. Use the original Copilot SDK catalog generator and validator after changing a specification or catalogued path. The current bootstrap procedure and exact SDK identity are documented in [the component-library guide](../docs/component-library.md#catalog-validation). -This repository does not yet have package scripts or an application CI workflow. -Adding reproducible repository-local validation is an implementation deliverable, -not a claim established by this documentation bootstrap. +The repository now has pinned package scripts and CI for its first source increment. +Run the documented type, test/coverage, build and packed-consumer checks; CI also reads +the original SDK at its reviewed source revision. These checks establish current-source +evidence, not completion of the component/adapter/adoption acceptance inventory. ### Retrospective (as-built) specifications diff --git a/specs/catalog.json b/specs/catalog.json index be27dd6..4f6af19 100644 --- a/specs/catalog.json +++ b/specs/catalog.json @@ -13,25 +13,40 @@ "specs/shared-ui-framework-delivery.md" ], "workflows": [ - ".github/workflows/copilot_issue.yml" + ".github/workflows/copilot_issue.yml", + ".github/workflows/ci_check.yml" + ], + "entrypoints": [ + "packages/core/src/index.ts" + ], + "code": [ + "packages/core/src/contracts.ts", + "tooling/coverage.mjs" + ], + "tests": [ + "tests/contracts/core.test.ts", + "tests/coverage.test.mjs", + "tests/architecture.test.mjs", + "tests/coverage-evidence.test.mjs", + "tests/consumers/packed.test.ts" ], - "entrypoints": [], - "code": [], - "tests": [], "documentation": [ "README.md", - "docs/component-library.md" + "docs/component-library.md", + "docs/compatibility.md", + "docs/provenance.md", + "docs/evidence/shared-ui-foundation.json" ], "readiness": { "status": "draft", "blockingGates": [ - "Native feature publication/readiness", "Exact support pins and public API verification", "Implementation, coverage and consumer acceptance", "npm publication rights and source-to-artifact integrity", "Storybook Pages delivery and human UX review", "Actual Gateway and Domus adoption" - ] + ], + "implementationEvidence": "partial: DOM-independent navigation data contract and quality/toolchain setup only; no rendered components, adapters, framework fixtures, Storybook or publication accepted." } } ] diff --git a/specs/shared-ui-framework-delivery.md b/specs/shared-ui-framework-delivery.md index 2021580..df5eb06 100644 --- a/specs/shared-ui-framework-delivery.md +++ b/specs/shared-ui-framework-delivery.md @@ -287,7 +287,7 @@ Prospective repository files (current documents are classified against the immut | `specs/shared-ui-foundation.md` | reference | | `specs/catalog.json` | update | | `specs/CATALOG.md` | update | -| `specs/README.md` | reference | +| `specs/README.md` | update | | `specs/_template.md` | reference | | `README.md` | update | | `docs/component-library.md` | update | @@ -295,6 +295,13 @@ Prospective repository files (current documents are classified against the immut | `pnpm-workspace.yaml` | create | | `pnpm-lock.yaml` | create | | `tsconfig.json` | create | +| `packages/core/LICENSE` | create | +| `.gitignore` | create | +| `.node-version` | create | +| `.npmrc` | create | +| `packages/core/tsconfig.json` | create | +| `tooling/coverage.mjs` | create | +| `tests/coverage.test.mjs` | create | | `LICENSE` | create | | `.github/workflows/ci_check.yml` | create | | `.github/workflows/storybook_pages.yml` | create | diff --git a/tests/architecture.test.mjs b/tests/architecture.test.mjs new file mode 100644 index 0000000..1cba97b --- /dev/null +++ b/tests/architecture.test.mjs @@ -0,0 +1,60 @@ +import {test, expect} from 'vitest'; +import {readFileSync, globSync} from 'node:fs'; +import {resolve, relative, dirname} from 'node:path'; +import ts from 'typescript'; +import {parse} from 'yaml'; + +function imports(code) { + const source = ts.createSourceFile('source.ts', code, ts.ScriptTarget.Latest, true); + const results = []; + function visit(node) { + if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier) + results.push(node.moduleSpecifier.text); + if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) + results.push(ts.isStringLiteral(node.arguments[0]) ? node.arguments[0].text : ''); + ts.forEachChild(node, visit); + } + visit(source); return results; +} +function allowed(specifier) { + return !specifier.startsWith('node:') && !/^(react|react-dom|vue|svelte|@angular)(\/|$)/u.test(specifier) + && !specifier.includes('domus') && !specifier.includes('gateway') && specifier !== ''; +} +test('the pure contract does not acquire framework or application imports', () => { + const code = readFileSync('packages/core/src/contracts.ts', 'utf8'); + expect(imports(code)).toEqual([]); +}); +test('architecture policy rejects direct, re-exported and dynamic framework dependencies', () => { + for (const code of ['import x from "react";', 'export * from "@angular/core";', 'await import("vue");', 'await import(target);']) + expect(imports(code).some(specifier => !allowed(specifier))).toBe(true); +}); +test('every actual core module respects the dependency boundary', () => { + for (const file of globSync('packages/core/src/**/*.ts')) for (const dependency of imports(readFileSync(file, 'utf8'))) { + expect(allowed(dependency), `${file}: ${dependency}`).toBe(true); + if (dependency.startsWith('.')) expect(relative(resolve('packages/core/src'), resolve(dirname(file), dependency)).startsWith('..')).toBe(false); + } +}); +test('CI Check runs for every PR update with frozen tools and all implemented quality gates', () => { + const workflow = parse(readFileSync('.github/workflows/ci_check.yml', 'utf8')); + expect(workflow.name).toBe('CI Check'); + expect(workflow.on.pull_request.types).toEqual(expect.arrayContaining(['opened','reopened','synchronize','ready_for_review','converted_to_draft'])); + expect(workflow.on.pull_request.paths).toBeUndefined(); + const job = workflow.jobs.quality; + expect(job.name).toBe('CI Check'); expect(job.if).toBeUndefined(); + expect(job['continue-on-error']).toBeUndefined(); + expect(job.steps.every(step => step['continue-on-error'] === undefined)).toBe(true); + const commands = job.steps.flatMap(step => step.run ? [step.run] : []); + for (const command of ['pnpm install --frozen-lockfile --ignore-scripts', 'pnpm check', 'pnpm test', 'pnpm build', 'pnpm test:package']) expect(commands).toContain(command); + const sdk = job.steps.find(step => step.with?.repository === 'vypdev/copilot'); + expect(sdk.with.ref).toBe('3e7ceea08e6131360e026937d1c0cc1b120472e0'); + expect(sdk.with['persist-credentials']).toBe(false); + expect(commands.some(command => command.includes('specification_catalog_validator.cjs') && command.includes('main([], process.cwd())'))).toBe(true); + const artifact = job.steps.find(step => step.uses?.startsWith('actions/upload-artifact@')); + expect(artifact.if).toBe('${{ always() }}'); +}); +test('test configuration covers every package and authored tool per file without hiding runtime modules', () => { + const source = readFileSync('vitest.config.ts', 'utf8'); + expect(source).toContain("completeCoverage(['packages/*/src', 'tooling'], 'coverage')"); + const manifest = JSON.parse(readFileSync('package.json', 'utf8')); + expect(manifest.scripts.test).toBe('vitest run --coverage && node --test tests/coverage-evidence.test.mjs'); +}); diff --git a/tests/consumers/packed.test.ts b/tests/consumers/packed.test.ts new file mode 100644 index 0000000..23c6b8c --- /dev/null +++ b/tests/consumers/packed.test.ts @@ -0,0 +1,54 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {promisify} from 'node:util'; +import {execFile} from 'node:child_process'; +import {mkdtemp, mkdir, readFile, writeFile, readdir, rm} from 'node:fs/promises'; +import {tmpdir} from 'node:os'; +import {resolve, join} from 'node:path'; +import {fileURLToPath} from 'node:url'; +import {createHash} from 'node:crypto'; + +const execute = promisify(execFile); +const root = fileURLToPath(new URL('../../', import.meta.url)); +const digest = (bytes: Uint8Array) => createHash('sha256').update(bytes).digest('hex'); + +test('the actual packed contract is reproducible and compiles/imports in a clean DOM-free consumer', {timeout: 60_000}, async () => { + const scratch = await mkdtemp(join(tmpdir(), 'ha-ui-packed-contract-')); + let passed = false; + try { + const archives = []; + for (const index of [1, 2]) { + const destination = join(scratch, 'pack-' + index); await mkdir(destination); + await execute('pnpm', ['build'], {cwd: root}); + await execute('pnpm', ['--dir', 'packages/core', 'pack', '--pack-destination', destination], {cwd: root}); + const names = (await readdir(destination)).filter(name => name.endsWith('.tgz')); + assert.equal(names.length, 1); archives.push(join(destination, names[0])); + } + const first = await readFile(archives[0]), second = await readFile(archives[1]); + assert.deepEqual(first, second, 'Repeated independent builds must produce the same packed bytes'); + const consumer = join(scratch, 'consumer'); await mkdir(consumer); + await writeFile(join(consumer, 'package.json'), JSON.stringify({name:'packed-contract-fixture', private:true, type:'module', packageManager:'pnpm@10.34.0', dependencies:{'@vypdev/ha-ui':'file:'+archives[0]}})); + await execute('pnpm', ['install', '--offline', '--ignore-scripts'], {cwd: consumer}); + await writeFile(join(consumer, 'contract.ts'), `import {navigationState, type NavigationKind} from '@vypdev/ha-ui/contracts'; +const kind: NavigationKind = 'panels'; +const selected: string | null = navigationState({kind, label:'Sections', selected:null, items:[]}).selected; +void selected;`); + await writeFile(join(consumer, 'tsconfig.json'), JSON.stringify({compilerOptions:{target:'ES2024',module:'NodeNext',moduleResolution:'NodeNext',strict:true,noEmit:true,lib:['ES2024'],types:[]},include:['contract.ts']})); + await execute(process.execPath, [resolve(root, 'node_modules/typescript/bin/tsc'), '-p', 'tsconfig.json'], {cwd: consumer}); + const probe = await execute(process.execPath, ['--input-type=module', '-e', `import {navigationState} from '@vypdev/ha-ui/contracts'; +if(typeof document!=='undefined'||typeof customElements!=='undefined') throw Error('Expected a server-only consumer'); +const value=navigationState({kind:'navigation',label:'Sections',selected:'tasks',items:[{id:'tasks',label:'Tareas 🧹',href:'#tasks'}]}); +if(value.selected!=='tasks'||!Object.isFrozen(value.items))throw Error('Packed contract did not preserve controlled state'); +console.log('Packed contract imported without DOM globals');`], {cwd: consumer}); + assert.match(probe.stdout, /Packed contract imported without DOM globals/u); + const installed = join(consumer, 'node_modules/@vypdev/ha-ui'); + for (const file of ['dist/contracts.js','dist/contracts.d.ts','src/contracts.ts','LICENSE']) + assert.deepEqual(await readFile(join(installed,file)),await readFile(resolve(root,'packages/core',file))); + await mkdir(resolve(root, 'test-results'), {recursive:true}); + await writeFile(resolve(root, 'test-results/packed-contract.json'), JSON.stringify({scope:'Core data contract only; no rendered component, adapter or application adoption',sha256:digest(first),bytes:first.byteLength,reproducible:true,cleanOfflineInstall:true,compiledWithoutDomTypes:true,serverImport:true},null,2)+'\n'); + passed = true; + } finally { + if (passed) await rm(scratch, {recursive:true}); + else console.error('Failed packed-consumer fixture retained at '+scratch); + } +}); diff --git a/tests/contracts/core.test.ts b/tests/contracts/core.test.ts new file mode 100644 index 0000000..54b005b --- /dev/null +++ b/tests/contracts/core.test.ts @@ -0,0 +1,68 @@ +import {describe, expect, test} from 'vitest'; +import {NavigationInputError, navigationState} from '../../packages/core/src/index.js'; + +const section = (patch: Record = {}) => ({id: 'tasks', label: 'Tasks', href: '#tasks', ...patch}); +const state = (patch: Record = {}) => ({kind: 'navigation', label: 'Household sections', selected: 'tasks', items: [section()], ...patch}); + +function rejected(value: unknown) { + expect(() => navigationState(value)).toThrow(NavigationInputError); + try { navigationState(value); } catch (error) { + expect(error).toBeInstanceOf(NavigationInputError); + expect((error as NavigationInputError).code).toBe('invalid_navigation'); + expect((error as Error).message).toBe('invalid_navigation'); + } +} + +describe('framework-neutral navigation contracts', () => { + test('imports and validates without browser globals or application identity', () => { + expect(typeof document).toBe('undefined'); + expect(navigationState(state())).toEqual({...state(), items: [{...section(), disabled: false}]}); + }); + test('takes immutable independent snapshots without altering caller objects', () => { + const source = state(), value = navigationState(source); + expect(value).not.toBe(source); expect(value.items).not.toBe(source.items); + expect(value.items[0]).not.toBe(source.items[0]); + source.items[0].label = 'Changed'; source.items.push(section({id: 'other'})); + expect(value.items).toHaveLength(1); expect(value.items[0].label).toBe('Tasks'); + expect(Object.isFrozen(value)).toBe(true); expect(Object.isFrozen(value.items)).toBe(true); + expect(Object.isFrozen(value.items[0])).toBe(true); + }); + test('retains literal multilingual labels and valid supplementary code points', () => { + const label = 'Tareas 🧹 '; + expect(navigationState(state({label, items: [section({label})]})).label).toBe(label); + expect(navigationState(state({label: '🧹'.repeat(256)})).label).toHaveLength(512); + expect(navigationState(state({items: [section({id: 'a'.repeat(128)})], selected: 'a'.repeat(128)})).selected).toHaveLength(128); + }); + test('supports an explicit empty state and no automatic destination selection', () => { + expect(navigationState(state({items: [], selected: null})).items).toEqual([]); + expect(navigationState(state({selected: null})).selected).toBeNull(); + }); + test('panel mode uses IDs for slots and accepts no link target', () => { + const value = navigationState(state({kind: 'panels', items: [{id: 'tasks', label: 'Tasks'}]})); + expect(value.items[0]).toEqual({id: 'tasks', label: 'Tasks', disabled: false, href: null}); + expect(navigationState(state({kind: 'panels', items: [section({href: null})]}))).toEqual(value); + }); + test('disabled items remain identifiable but cannot become selected', () => { + const value = navigationState(state({selected: null, items: [section({disabled: true})]})); + expect(value.items[0].disabled).toBe(true); + rejected(state({items: [section({disabled: true})]})); + }); + test.each(['#tasks', '/tasks?status=open', './tasks', '../tasks', '?view=agenda', 'https://example.invalid/tasks', 'http://example.invalid:8123/tasks'])('preserves the application-owned safe link %s', href => { + expect(navigationState(state({items: [section({href})]})).items[0].href).toBe(href); + }); + test('accepts exactly sixty-four distinct items without truncating the model', () => { + const items = Array.from({length: 64}, (_, n) => section({id: 'section-' + n})); + expect(navigationState(state({items, selected: 'section-63'})).items).toHaveLength(64); + }); + test.each([null, undefined, true, 'navigation', [], {}, {...state(), userId: 'private'}, {...state(), kind: 'unknown'}, {...state(), selected: undefined}])('rejects invalid or extended state %j', rejected); + test.each([null, [], true, {}, {...section(), html: ''}])('rejects invalid or extended items %j', item => rejected(state({items: [item]}))); + test.each([null, {}, 'items', Array.from({length: 65}, (_, n) => section({id: 'i-' + n}))])('rejects malformed or excessive item lists', items => rejected(state({items}))); + test('rejects duplicate identifiers without choosing one arbitrarily', () => rejected(state({items: [section(), section()]}))); + test.each(['', ' ', 'a b', 'a/b', 'a'.repeat(129), 'á', 'a:b'])('rejects unsupported identifier %j', id => rejected(state({items: [section({id})], selected: id}))); + test.each(['missing', 3, false, {}])('rejects a missing or malformed current destination %j', selected => rejected(state({selected}))); + test.each(['', ' ', 3, null, '\u0000bad', '\u007Fbad', '\ud800', '🧹'.repeat(257)])('rejects invalid accessible label %j', label => rejected(state({label}))); + test.each(['', null, 3, '\u0001', '\udfff', 'x'.repeat(257)])('rejects invalid item label %j', label => rejected(state({items: [section({label})]}))); + test.each([null, 1, 'false'])('does not coerce disabled %j to a boolean', disabled => rejected(state({items: [section({disabled})]}))); + test.each(['javascript:alert(1)', 'data:text/html,unsafe', 'file:///private/data', '//example.invalid', '\\example.invalid', 'https://user:password@example.invalid', 'https://user@example.invalid', 'https://:password@example.invalid', 'http://[invalid', ' #tasks', '#tasks ', '', null, 3, 'x'.repeat(2049)])('rejects unsafe or malformed links %j', href => rejected(state({items: [section({href})]}))); + test('rejects a link in panel mode instead of silently changing interaction semantics', () => rejected(state({kind: 'panels'}))); +}); diff --git a/tests/coverage-evidence.test.mjs b/tests/coverage-evidence.test.mjs new file mode 100644 index 0000000..01d6325 --- /dev/null +++ b/tests/coverage-evidence.test.mjs @@ -0,0 +1,12 @@ +import assert from 'node:assert/strict'; +import {globSync, readFileSync} from 'node:fs'; +import {resolve} from 'node:path'; +import {test} from 'node:test'; +import {coverageProblems, runtimeExtensions} from '../tooling/coverage.mjs'; + +test('coverage includes every authored package/tool file and all four metrics', () => { + const files = ['packages/*/src', 'tooling'].flatMap(directory => globSync(`${directory}/**/*.${runtimeExtensions}`)) + .filter(path => !/\.d\.(ts|mts|cts)$/u.test(path)).map(path => resolve(path)); + const report = JSON.parse(readFileSync('coverage/coverage-summary.json', 'utf8')); + assert.deepEqual(coverageProblems(report, files), []); +}); diff --git a/tests/coverage.test.mjs b/tests/coverage.test.mjs new file mode 100644 index 0000000..190520b --- /dev/null +++ b/tests/coverage.test.mjs @@ -0,0 +1,57 @@ +import {describe, expect, it} from "vitest"; +import {completeCoverage, coverageProblems, metrics} from "../tooling/coverage.mjs"; + +function fullGroup() { + return Object.fromEntries(metrics.map(metric => [metric, {total: 2, covered: 2, skipped: 0, pct: 100}])); +} +function fullReport() { return {total: fullGroup(), "source.ts": fullGroup()}; } + +describe("complete first-party frontend coverage", () => { + it("requires all runtime extensions, four metrics and per-file checks without exclusions", () => { + const config = completeCoverage(["owned"], "reports"); + expect(config.include).toEqual(["owned/**/*.{js,mjs,cjs,jsx,ts,mts,cts,tsx}"]); + expect(config.exclude).toBeUndefined(); + expect(config.thresholds).toEqual({lines: 100, statements: 100, functions: 100, branches: 100, perFile: true}); + expect(config.reporter).toContain("json-summary"); + expect(config.reportsDirectory).toBe("reports"); + }); + it("accepts all supported producer metrics for the exact nonempty source inventory", () => { + expect(coverageProblems(fullReport(), ["source.ts"])).toEqual([]); + const emptyMetric = {total: 0, covered: 0, skipped: 0, pct: 100}; + const report = fullReport(); + report.total.branches = emptyMetric; + report["source.ts"].branches = emptyMetric; + expect(coverageProblems(report, ["source.ts"])).toEqual([]); + }); + it.each([null, [], "100", false])("rejects a malformed summary: %j", report => { + expect(coverageProblems(report, ["source.ts"])).toEqual(["Coverage summary must be an object"]); + }); + it("rejects an empty runtime inventory", () => { + expect(coverageProblems({}, [])).toEqual(["Coverage source inventory must not be empty"]); + }); + it("rejects omitted files and unexpected evidence", () => { + expect(coverageProblems(fullReport(), ["unimported.ts"])).toEqual([ + "Unexpected coverage file: source.ts", "Missing coverage object: unimported.ts", + ]); + }); + it.each([undefined, null, []])("rejects missing or malformed aggregate evidence: %j", total => { + expect(coverageProblems({...fullReport(), total}, ["source.ts"])) + .toEqual(["Missing coverage object: total"]); + }); + it.each(metrics)("rejects a missing %s metric even if the other metrics are perfect", metric => { + const report = fullReport(); + delete report["source.ts"][metric]; + expect(coverageProblems(report, ["source.ts"])) + .toEqual([`source.ts: incomplete ${metric} coverage`]); + }); + it.each([ + null, [], {total: 1.5}, {total: -1}, {total: Number.MAX_SAFE_INTEGER + 1}, + {total: 2, covered: 1, skipped: 0, pct: 100}, + {total: 2, covered: 2, skipped: 1, pct: 100}, + {total: 2, covered: 2, skipped: 0, pct: 99}, + ])("rejects invalid counts or hidden gaps: %j", invalid => { + const report = {...fullReport(), total: {...fullGroup(), lines: invalid}}; + expect(coverageProblems(report, ["source.ts"])) + .toEqual(["total: incomplete lines coverage"]); + }); +}); diff --git a/tooling/coverage.mjs b/tooling/coverage.mjs new file mode 100644 index 0000000..b9c5f7f --- /dev/null +++ b/tooling/coverage.mjs @@ -0,0 +1,56 @@ +/** Reusable first-party frontend coverage policy, independent of product behavior. */ +export const runtimeExtensions = "{js,mjs,cjs,jsx,ts,mts,cts,tsx}"; +export const metrics = /** @type {const} */ (["lines", "statements", "functions", "branches"]); + +/** + * Include unimported runtime modules and enforce every supported metric per file. + * @param {readonly string[]} sourceDirectories + * @param {string} reportsDirectory + * @returns {import("vitest/node").CoverageV8Options} + */ +export function completeCoverage(sourceDirectories, reportsDirectory) { + return { + provider: "v8", + include: sourceDirectories.map(directory => `${directory}/**/*.${runtimeExtensions}`), + reporter: ["text", "json-summary", "lcov"], + reportsDirectory, + thresholds: {lines: 100, statements: 100, functions: 100, branches: 100, perFile: true}, + }; +} + +/** @param {unknown} value @returns {value is Record} */ +function record(value) { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +/** + * Fail closed for absent source files, missing metrics and incomplete evidence. + * The source inventory comes from the filesystem, independently of test imports. + * @param {unknown} report + * @param {readonly string[]} sourceFiles + * @returns {string[]} + */ +export function coverageProblems(report, sourceFiles) { + if (!record(report)) return ["Coverage summary must be an object"]; + if (sourceFiles.length === 0) return ["Coverage source inventory must not be empty"]; + const expected = new Set(["total", ...sourceFiles]); + const problems = []; + for (const path of Object.keys(report)) { + if (!expected.has(path)) problems.push(`Unexpected coverage file: ${path}`); + } + for (const path of expected) { + const group = report[path]; + if (!record(group)) { + problems.push(`Missing coverage object: ${path}`); + continue; + } + for (const metric of metrics) { + const value = group[metric]; + if (!record(value) || !Number.isSafeInteger(value.total) || Number(value.total) < 0 + || value.covered !== value.total || value.skipped !== 0 || value.pct !== 100) { + problems.push(`${path}: incomplete ${metric} coverage`); + } + } + } + return problems; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..80f94cd --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2024", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noEmit": true, + "skipLibCheck": false, + "allowJs": true, + "checkJs": true, + "types": [ + "node" + ], + "lib": [ + "ES2024", + "DOM" + ], + "verbatimModuleSyntax": true + }, + "include": [ + "packages/*/src/**/*.ts", + "tooling/**/*.mjs", + "vitest.config.ts", + "tests/contracts/**/*.test.ts", + "tests/consumers/**/*.test.ts" + ] +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..3279d12 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,6 @@ +import {defineConfig} from 'vitest/config'; +import {completeCoverage} from './tooling/coverage.mjs'; +export default defineConfig({test: { + include: ['tests/**/*.test.{ts,mjs}'], exclude: ['tests/coverage-evidence.test.mjs', 'tests/consumers/packed.test.ts'], + coverage: completeCoverage(['packages/*/src', 'tooling'], 'coverage'), +}}); From f981e2c7fe26d8344dc199951c31c0a1e5daa15a Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Wed, 4 Nov 2026 05:15:31 +0100 Subject: [PATCH 04/55] fix: reject C1 control characters throughout navigation input --- docs/compatibility.md | 2 +- docs/evidence/shared-ui-foundation.json | 22 ++++++++++++++-------- packages/core/src/contracts.ts | 2 +- tests/contracts/core.test.ts | 14 ++++++++++++++ 4 files changed, 30 insertions(+), 10 deletions(-) diff --git a/docs/compatibility.md b/docs/compatibility.md index 6a0d70e..11d3836 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -19,7 +19,7 @@ Do not infer support from custom-element interoperability or compiler metadata a The controlled navigation model requires kind, label, selected and items. Kind is navigation or panels; selected is an existing enabled item ID or null. IDs use 1..128 ASCII letters, digits, underscore or -hyphen; accessible labels contain 1..256 Unicode code points without control/unpaired surrogate values. +hyphen; accessible labels contain 1..256 Unicode code points without C0, DEL, C1 or unpaired surrogate values. At most 64 items are accepted, without truncation. Navigation items require an explicit HTTP(S) or relative application link; panel items omit links and use item IDs for future named panel slots. Disabled defaults to false. Labels and links retain their original values; snapshots are deeply frozen. diff --git a/docs/evidence/shared-ui-foundation.json b/docs/evidence/shared-ui-foundation.json index 5b18151..4ba2b7f 100644 --- a/docs/evidence/shared-ui-foundation.json +++ b/docs/evidence/shared-ui-foundation.json @@ -1,5 +1,5 @@ { - "recordedAtUTC": "2026-11-04T04:03:52.911705+00:00", + "recordedAtUTC": "2026-11-04T04:15:29.176261+00:00", "status": "Partial source increment; no rendered component, adapter, Storybook, framework support, published release or app adoption accepted.", "issue": 7, "nativePublication": { @@ -17,7 +17,7 @@ "implemented": [ "DOM-independent closed navigation/panel data contract; immutable snapshots, bounded text/items, explicit selection and safe links.", "Pinned Node24.20.0/pnpm10.34.0/TypeScript6.0.3/Vitest5.1.0 build and test foundation.", - "105 passing unit/architecture/CI-contract tests; separate filesystem-inventory coverage audit and actual packed-consumer integration both pass.", + "138 passing unit/architecture/CI-contract tests; separate filesystem-inventory coverage audit and actual packed-consumer integration both pass.", "Strict four-metric per-module/global coverage; every authored package/tool inventoried including the zero-executable re-export module.", "Every-PR-commit CI Check uses frozen dependencies, all implemented gates, actual package compilation/consumption and the unchanged pinned original catalog SDK." ], @@ -29,8 +29,8 @@ "pct": 100 }, "branches": { - "total": 77, - "covered": 77, + "total": 78, + "covered": 78, "skipped": 0, "pct": 100 }, @@ -48,14 +48,14 @@ } }, "sourceIdentities": { - "packages/core/src/contracts.ts": "6c37990359192c056daf54764870288aa20d4422fa88cfaf9186ccfae8fb101f", + "packages/core/src/contracts.ts": "a786f32769cd69d0653d6292ae8b231b19986a07ad46c998a02d3948c2d5006e", "packages/core/src/index.ts": "10bd6958315d1ae23d216c0b752126e3fc7153f442f8c71e206cfa7e9757f86d", "tooling/coverage.mjs": "2272a671a4d039fb40866f3535f6cd371b891623b64bf1b9a23a63677ad2387b" }, "packedContract": { "scope": "Core data contract only; no rendered component, adapter or application adoption", - "sha256": "6d7a0da478ee9d1f5654323ca6cc9232d9f2bc07c94962313f17b7d7ecb0862c", - "bytes": 4399, + "sha256": "fa68300cb8494e7ff9a52435c03f99b3fc8d896c269f184378044e8821b35e65", + "bytes": 4396, "reproducible": true, "cleanOfflineInstall": true, "compiledWithoutDomTypes": true, @@ -68,5 +68,11 @@ "Fresh hosted CI and complete native review are required for the final source head." ], "copilotModified": false, - "capabilityComplete": false + "capabilityComplete": false, + "reviewRegression": { + "finding": 4463468597, + "before": "32 C1 cases failed; 78 contract cases passed.", + "after": "All 138 unit/architecture/CI-contract cases pass; every C1 code point rejected in group labels, item labels and links; visible U+00A0/U+00A1 preserved.", + "manualResolution": false + } } diff --git a/packages/core/src/contracts.ts b/packages/core/src/contracts.ts index f8e2db6..3d7a863 100644 --- a/packages/core/src/contracts.ts +++ b/packages/core/src/contracts.ts @@ -26,7 +26,7 @@ function text(value: unknown, maximum: number): string { if (typeof value !== 'string' || !value.trim() || Array.from(value).length > maximum) invalid(); for (const character of value) { const point = character.codePointAt(0)!; - if (point < 32 || point === 127 || point >= 0xd800 && point <= 0xdfff) invalid(); + if (point < 32 || point >= 0x7f && point <= 0x9f || point >= 0xd800 && point <= 0xdfff) invalid(); } return value; } diff --git a/tests/contracts/core.test.ts b/tests/contracts/core.test.ts index 54b005b..87c8232 100644 --- a/tests/contracts/core.test.ts +++ b/tests/contracts/core.test.ts @@ -66,3 +66,17 @@ describe('framework-neutral navigation contracts', () => { test.each(['javascript:alert(1)', 'data:text/html,unsafe', 'file:///private/data', '//example.invalid', '\\example.invalid', 'https://user:password@example.invalid', 'https://user@example.invalid', 'https://:password@example.invalid', 'http://[invalid', ' #tasks', '#tasks ', '', null, 3, 'x'.repeat(2049)])('rejects unsafe or malformed links %j', href => rejected(state({items: [section({href})]}))); test('rejects a link in panel mode instead of silently changing interaction semantics', () => rejected(state({kind: 'panels'}))); }); + +// Regression for the reviewed Unicode control boundary. +test.each(Array.from({length: 32}, (_, index) => (0x80 + index).toString(16)))( + 'rejects Unicode C1 control U+%s in labels and links', code => { + const control = String.fromCodePoint(Number.parseInt(code, 16)); + rejected(state({label: 'A' + control + 'B'})); + rejected(state({items: [section({label: 'A' + control + 'B'})]})); + rejected(state({items: [section({href: '#A' + control + 'B'})]})); + }, +); +test('preserves visible Unicode just beyond the C1 boundary', () => { + const label = 'A\u00a0¡B'; + expect(navigationState(state({label, items: [section({label})]})).items[0].label).toBe(label); +}); From 3045e4240c22053a5b6804d676b21d8a48769c19 Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Wed, 4 Nov 2026 05:27:42 +0100 Subject: [PATCH 05/55] feat: add a controlled shared dashboard navigation component --- .github/workflows/ci_check.yml | 2 + .gitignore | 2 + README.md | 6 +- docs/compatibility.md | 19 +- docs/component-library.md | 39 +++- docs/evidence/shared-ui-foundation.json | 72 +++++--- docs/provenance.md | 9 +- package.json | 2 + packages/core/package.json | 7 + packages/core/src/components/tabs.ts | 96 ++++++++++ pnpm-lock.yaml | 231 +++++++++++++++++++++++- specs/CATALOG.md | 8 +- specs/catalog.json | 14 +- specs/shared-ui-framework-delivery.md | 2 + tests/architecture.test.mjs | 2 +- tests/browser/tabs.test.ts | 197 ++++++++++++++++++++ tests/consumers/packed.test.ts | 55 +++++- tests/contracts/registration.test.ts | 7 + tsconfig.json | 3 +- vitest.config.ts | 6 +- 20 files changed, 722 insertions(+), 57 deletions(-) create mode 100644 packages/core/src/components/tabs.ts create mode 100644 tests/browser/tabs.test.ts create mode 100644 tests/contracts/registration.test.ts diff --git a/.github/workflows/ci_check.yml b/.github/workflows/ci_check.yml index 6aebdd7..f9f9fb0 100644 --- a/.github/workflows/ci_check.yml +++ b/.github/workflows/ci_check.yml @@ -27,6 +27,7 @@ jobs: cache: false - run: pnpm install --frozen-lockfile --ignore-scripts - run: pnpm check + - run: pnpm exec playwright install --with-deps chromium --only-shell - run: pnpm test - run: pnpm build - run: pnpm test:package @@ -55,5 +56,6 @@ jobs: coverage/ packages/core/dist/ test-results/ + .vitest/attachments/ if-no-files-found: error retention-days: 7 diff --git a/.gitignore b/.gitignore index fb75e53..f4b0911 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ work/ **/dist/ **/test-results/ **/.vite/ + +.vitest/ diff --git a/README.md b/README.md index 6b79800..6fe30c8 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,9 @@ Shared presentation components for Home Assistant applications, including Gateway and Domus. -The first source increment implements the DOM-independent navigation data contract and a pinned, -tested build/coverage foundation. Rendered components, the React/Vue/Angular/Svelte adapters, +The current source implements a DOM-independent navigation data contract, one controlled HA-style +upper navigation/panel component and a pinned, tested build/coverage foundation. Other component +families, the React/Vue/Angular/Svelte adapters, compiled framework consumers, Storybook and publication remain required and unimplemented. No package is published or ready for application adoption. @@ -12,6 +13,7 @@ Use Node 24.20.0 and the package manager pinned in package.json: ```bash pnpm install --frozen-lockfile --ignore-scripts pnpm check +pnpm exec playwright install --with-deps chromium --only-shell pnpm test pnpm build pnpm test:package diff --git a/docs/compatibility.md b/docs/compatibility.md index 11d3836..5df647d 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -1,7 +1,8 @@ # Compatibility and validation status -Only the DOM-independent navigation input contract and its build/coverage foundation are implemented. -No rendered component, framework adapter or framework SSR support is currently accepted. +The DOM-independent navigation contract, controlled upper navigation/panel component and their +build/coverage foundation are implemented and tested in Chromium. Framework adapters, additional +component families, browser support breadth and framework SSR integration remain unaccepted. | Tool | Exact pin | Current use | |---|---|---| @@ -9,10 +10,12 @@ No rendered component, framework adapter or framework SSR support is currently a | pnpm | 10.34.0 | Frozen workspace install | | TypeScript | 6.0.3 | Strict contract/type declarations; compatible candidate for Angular compiler-cli 22.2.4 | | Vite | 8.3.5 | Test transform toolchain | -| Vitest and V8 coverage | 5.1.0 | All four metrics globally and per authored runtime file | +| Lit | 3.3.3 | Internal shared navigation renderer | +| Vitest, browser provider and V8 coverage | 5.1.0 | Node and real Chromium; all four metrics globally and per authored runtime file | +| Playwright | 1.64.1 | Chromium 156.0.8078.4 component and clean packed-consumer tests | The core preview coordinate/version is package: @vypdev/ha-ui at 0.1.0-next.1. It is unpublished; -its source currently contains presentation data contracts only. It is not a supported component release. +its source currently contains presentation contracts and one preview navigation/panel component. It is not a supported component release. React 19.3.1, Vue 3.5.44, Angular 22.2.4 and Svelte 5.57.3 remain candidate pins until actual packed, compiled/browser fixtures pass. Next.js, Nuxt and SvelteKit client/SSR boundaries remain unverified. Do not infer support from custom-element interoperability or compiler metadata alone. @@ -27,6 +30,8 @@ Invalid/extra fields, conflicting selection and unsafe or credential-bearing lin NavigationInputError with code invalid_navigation. This validates data, not authorization or rendering. CI Check runs for each PR head with no path filter. Current gates are strict types, all implemented -tests, independent source-inventory coverage, architecture/CI contracts, compiled declarations, actual reproducible packed-contract consumption without DOM types, -and the original pinned Copilot catalog validator. Browser, packed framework fixtures, Storybook, publication -and actual application acceptance must be added with their implementations before release readiness. +tests, independent source-inventory coverage, architecture/CI contracts, compiled declarations, real Chromium component behavior, actual reproducible packed consumption with +a DOM-free contract and a compiled browser client, and the original pinned Copilot catalog validator. +The packed browser fixture exercises real links, panel slots, keyboard, live Spanish labels/theme, +and reduced motion. It does not establish other browser engines, framework adapters or framework SSR. +Packed framework fixtures, Storybook, publication and actual application acceptance remain required. diff --git a/docs/component-library.md b/docs/component-library.md index e92abee..d46986c 100644 --- a/docs/component-library.md +++ b/docs/component-library.md @@ -50,7 +50,7 @@ Core/adapter publication, Storybook delivery and each app's acceptance are disti All authored behavior starts with tests and requires 100% supported statements, branches, functions and lines globally and per module, with independent source inventory and executable architecture constraints. The primary Draft allocates 64 distinct minimum cases. CI Check must aggregate -all required implemented checks on every PR head; the initial CI now validates all implemented source; unimplemented browser/adapter/adoption gates remain pending. +all required implemented checks on every PR head; the initial CI now validates all implemented source; broader browser support, adapter and adoption gates remain pending. Record exact toolchain, source, lock, package digests and framework/browser results. Verify npm publication rights before any publish. Preserve one tested previous app/cohort rollback and all user data and original-operation recovery. Never overwrite an existing package version after partial delivery. @@ -93,3 +93,40 @@ Native issue #7 published the delivery companion on its exact managed branch bef Complete the component renderers, actual framework adapters/consumers, Storybook and staged delivery acceptance on that branch, retaining all 64 baseline and six companion cases. The first source contract and green incremental tests do not establish a complete component library or app adoption. + +## First component contract: controlled upper navigation + +Import `HaTabs` and `registerTabs` from `@vypdev/ha-ui/tabs`; call registration only at the +client boundary. It defines `vyp-ha-tabs` explicitly, preserves pre-upgrade property values, +allows repeated calls with the same constructor and rejects another installed constructor. +The root and contracts entrypoints stay DOM-independent. The component entrypoint is intended for clients. Lit may install its own DOM shim when imported +on a server; ha-ui does not define a tag there and rejects registration even with that shim. No server-rendered +Shadow DOM or framework hydration support is claimed. + +Set the `state` property to the validated `NavigationState` contract. It is property-only; +serialized object attributes and HTML-string rendering are unsupported. Assignment validates +and snapshots before replacing the previous state. No programmatic assignment emits intent. +Each user action emits one cancelable, bubbling, composed `ha-selection-request` CustomEvent, +with frozen `{kind, id}` detail. Apps own selected state and routes. Canceling intent prevents +the original link navigation; unmodified accepted navigation retains the native anchor default. +Modified link clicks retain browser behavior and do not emit a selection request. Disabled +links have no target or tab stop. Stale DOM events during a pending state replacement are rejected. + +Navigation renders a labeled `nav` with links and `aria-current=page`. Panels render a labeled +horizontal `tablist`, native button tabs and actual same-shadow-root labeled `tabpanel` sections. +Panel item IDs name light-DOM slots. Inactive panel wrappers are hidden, including slotted focus +targets. Arrow keys skip disabled tabs and wrap; Home/End move focus. RTL reverses horizontal +arrows. Activation is manual through native Enter/Space/click, while Tab follows normal focus +order. Null selection keeps a keyboard entry without silently selecting a panel. + +The common 56px dashboard bar, 16px horizontal item padding, 2px current indicator and inset +focus outline adapt Gateway's MIT dashboard geometry. Domus still owns section names and layout. +Public CSS tokens are `--vyp-ha-font-family`, `--vyp-ha-border`, `--vyp-ha-surface`, +`--vyp-ha-tab-padding`, `--vyp-ha-text-secondary`, `--vyp-ha-text` and `--vyp-ha-primary`. +Defaults inherit the corresponding HA theme variables where available. Overrides update live; +no global styles, network, storage, timers or animation are introduced. Exposed shadow parts are +`bar`, `item` and `panel`. Framework adapters and packed browser consumers remain separate gates. + +The interaction model follows [WAI-ARIA APG tabs](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/) +and uses Lit's documented [reactive properties](https://lit.dev/docs/components/properties/) +and [events](https://lit.dev/docs/components/events/). diff --git a/docs/evidence/shared-ui-foundation.json b/docs/evidence/shared-ui-foundation.json index 4ba2b7f..dc93959 100644 --- a/docs/evidence/shared-ui-foundation.json +++ b/docs/evidence/shared-ui-foundation.json @@ -1,6 +1,6 @@ { - "recordedAtUTC": "2026-11-04T04:15:29.176261+00:00", - "status": "Partial source increment; no rendered component, adapter, Storybook, framework support, published release or app adoption accepted.", + "recordedAtUTC": "2026-11-04T04:26:46.483795+00:00", + "status": "Partial source increment: data contract and one real navigation/panel component. No framework support, Storybook, published release or app adoption accepted.", "issue": 7, "nativePublication": { "run": 41767973796, @@ -15,57 +15,71 @@ "allStillRequired": true }, "implemented": [ - "DOM-independent closed navigation/panel data contract; immutable snapshots, bounded text/items, explicit selection and safe links.", - "Pinned Node24.20.0/pnpm10.34.0/TypeScript6.0.3/Vitest5.1.0 build and test foundation.", - "138 passing unit/architecture/CI-contract tests; separate filesystem-inventory coverage audit and actual packed-consumer integration both pass.", - "Strict four-metric per-module/global coverage; every authored package/tool inventoried including the zero-executable re-export module.", - "Every-PR-commit CI Check uses frozen dependencies, all implemented gates, actual package compilation/consumption and the unchanged pinned original catalog SDK." + "Closed DOM-independent navigation/panel data contract; bounded and immutable inputs, safe links and Unicode C0/DEL/C1 control rejection.", + "Explicit client registration, validated property-only controlled state and typed cancelable composed user intent; no app router or business logic.", + "Real navigation links/current-location semantics and same-root local tab/panel slots, manual keyboard activation, RTL, disabled and stale-event prevention, active-tab focus return.", + "167 passing unit, architecture, CI-contract and real Chromium tests; separate independent coverage audit and clean actual packed-consumer integration pass.", + "Strict four-metric per-module/global coverage including every authored package/tool and the zero-executable barrel.", + "Pinned Node24.20.0/pnpm10.34.0/TypeScript6.0.3/Lit3.3.3/Vitest5.1.0/Playwright1.64.1; every-PR CI installs Chromium and runs all implemented gates plus the unchanged pinned original catalog SDK." ], "coverage": { "statements": { - "total": 81, - "covered": 81, + "total": 148, + "covered": 148, "skipped": 0, "pct": 100 }, "branches": { - "total": 78, - "covered": 78, + "total": 139, + "covered": 139, "skipped": 0, "pct": 100 }, "functions": { - "total": 16, - "covered": 16, + "total": 34, + "covered": 34, "skipped": 0, "pct": 100 }, "lines": { - "total": 58, - "covered": 58, + "total": 100, + "covered": 100, "skipped": 0, "pct": 100 } }, "sourceIdentities": { + "packages/core/src/components/tabs.ts": "5e84a250c9758388d10f4b2e9de55be6d18e57754aaf38d1a0deb71262c58e36", "packages/core/src/contracts.ts": "a786f32769cd69d0653d6292ae8b231b19986a07ad46c998a02d3948c2d5006e", "packages/core/src/index.ts": "10bd6958315d1ae23d216c0b752126e3fc7153f442f8c71e206cfa7e9757f86d", "tooling/coverage.mjs": "2272a671a4d039fb40866f3535f6cd371b891623b64bf1b9a23a63677ad2387b" }, "packedContract": { - "scope": "Core data contract only; no rendered component, adapter or application adoption", - "sha256": "fa68300cb8494e7ff9a52435c03f99b3fc8d896c269f184378044e8821b35e65", - "bytes": 4396, + "scope": "Core data and navigation component only; no framework adapter or application adoption", + "sha256": "450e1dc97f1056041174f169287893ebcd616cc98bf8bbb907435ebb13179e96", + "bytes": 9585, "reproducible": true, "cleanOfflineInstall": true, "compiledWithoutDomTypes": true, - "serverImport": true + "serverImport": true, + "packedBrowser": { + "engine": "chromium", + "version": "156.0.8078.4", + "compiledClient": true, + "navigation": true, + "panels": true, + "runtimeSpanishAndTheme": true, + "reducedMotion": true, + "consoleErrors": [] + } }, "limitations": [ - "Current navigation tests validate data without rendering DOM; no actual tab accessibility, focus, events or keyboard behavior is claimed.", - "The packed fixture covers the core data contract only; all actual Lit/DOM, React, Vue, Angular and Svelte UI consumers and SSR/client boundaries remain required.", - "No live npm publication/authority, static Storybook delivery, Gateway extraction/adoption or Domus adoption is claimed.", - "Fresh hosted CI and complete native review are required for the final source head." + "Only the navigation/panel family is implemented; forms, dialogs and other presentation families remain required.", + "Clean packed Chromium consumer covers the shared component only; actual React, Vue, Angular, Svelte and SSR/client fixtures remain required.", + "Only the exact recorded Chromium runtime was exercised; Firefox, WebKit and public browser support claims remain pending.", + "Storybook, npm authority/publication, Gateway adoption, Domus adoption and installed human UX acceptance remain unverified.", + "All 64 retained baseline and six delivery-refinement cases remain required; green partial-source tests do not complete issue #7.", + "Fresh hosted CI and complete native review are required for the published source head." ], "copilotModified": false, "capabilityComplete": false, @@ -74,5 +88,17 @@ "before": "32 C1 cases failed; 78 contract cases passed.", "after": "All 138 unit/architecture/CI-contract cases pass; every C1 code point rejected in group labels, item labels and links; visible U+00A0/U+00A1 preserved.", "manualResolution": false + }, + "lockSha256": "3011110ba520f33cca391abf1f7ad5262485159ced72b579b180f9923b0611dc", + "browserEvidence": { + "componentCases": 28, + "engine": "Chromium 156.0.8078.4", + "actualPackageImages": [ + "test-results/packed-navigation-light.png", + "test-results/packed-panels-dark-es.png" + ], + "imagesInspected": true, + "focusRegression": "focus-return-red.log reproduces the stale tab-stop; full green run has no skipped tests.", + "initialTestAttempt": "Browser cases were authored before the renderer. The first attempted execution could not start because the pinned Chromium binary was absent; no behavioral red result is claimed for that attempt. After runtime provisioning, the first actual run found two fixture/server-boundary issues, recorded in browser-first-execution.log." } } diff --git a/docs/provenance.md b/docs/provenance.md index 6c3f63b..44dd786 100644 --- a/docs/provenance.md +++ b/docs/provenance.md @@ -2,8 +2,13 @@ The repository and core package carry the MIT notice, copyright 2026 vypdev. The notice is retained from [Gateway at e47dde303e475a6d03e1d6697ec87a934e93263b](https://github.com/vypdev/homeassistant-gateway/blob/e47dde303e475a6d03e1d6697ec87a934e93263b/LICENSE). -No Gateway visual component, font, icon or other asset is extracted in the first contract increment. -A reviewed per-family/source/asset inventory remains required before extraction and publication. +The first contract increment extracted no visual components or assets. The upper navigation increment +adapts the reviewed dashboard geometry from +[ui-catalog.css lines 105–121 at that revision](https://github.com/vypdev/homeassistant-gateway/blob/e47dde303e475a6d03e1d6697ec87a934e93263b/frontend/src/ui/ui-catalog.css#L105-L121): +56px bar, 16px horizontal padding, 2px current indicator, horizontal overflow and inset visible focus. +The standalone DOM semantics, validation, events and controller are authored in ha-ui. No private HA +component, font, icon or external asset is imported. The MIT notices are retained. Other families +need their own reviewed source/asset inventory before extraction and publication. The coverage policy and its negative unit fixtures are adapted from the same organization's MIT [Domus coverage policy](https://github.com/vypdev/domus/blob/8c0a36c0b9804bdaf33a301a4dc85ae3dc12ee21/tooling/frontend-coverage.mjs) diff --git a/package.json b/package.json index 2c200c2..1374475 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,9 @@ }, "devDependencies": { "@types/node": "24.19.2", + "@vitest/browser-playwright": "5.1.0", "@vitest/coverage-v8": "5.1.0", + "playwright": "1.64.1", "typescript": "6.0.3", "vite": "8.3.5", "vitest": "5.1.0", diff --git a/packages/core/package.json b/packages/core/package.json index 48b1c33..cb417ef 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -23,9 +23,16 @@ "./contracts": { "types": "./dist/contracts.d.ts", "import": "./dist/contracts.js" + }, + "./tabs": { + "types": "./dist/components/tabs.d.ts", + "import": "./dist/components/tabs.js" } }, "publishConfig": { "access": "public" + }, + "dependencies": { + "lit": "3.3.3" } } diff --git a/packages/core/src/components/tabs.ts b/packages/core/src/components/tabs.ts new file mode 100644 index 0000000..a858d7d --- /dev/null +++ b/packages/core/src/components/tabs.ts @@ -0,0 +1,96 @@ +import {LitElement, css, html, nothing} from 'lit'; +import {navigationState, type NavigationItem, type NavigationKind, type NavigationState} from '../contracts.js'; + +export interface NavigationSelectionDetail { readonly kind: NavigationKind; readonly id: string; } +export type NavigationSelectionEvent = CustomEvent; + +/** A controlled HA-style navigation bar or local, manually activated panel tabs. */ +export class HaTabs extends LitElement { + static properties = {state: {attribute: false, noAccessor: true}}; + // Dashboard geometry adapted from Gateway's MIT ui-catalog.css; no HA private component dependency. + static styles = css` + :host { display:block; min-width:0; font:500 14px/1 var(--vyp-ha-font-family,var(--primary-font-family,Roboto,Noto,sans-serif)); } + .bar { display:flex; align-items:stretch; gap:0; overflow-x:auto; min-height:56px; + border-bottom:1px solid var(--vyp-ha-border,var(--divider-color,#e0e0e0)); + background:var(--vyp-ha-surface,var(--card-background-color,#fff)); } + a,button { flex:0 0 auto; box-sizing:border-box; min-width:0; min-height:56px; display:inline-flex; + align-items:center; justify-content:center; padding:0 var(--vyp-ha-tab-padding,16px); margin:0; + border:0; border-bottom:2px solid transparent; border-radius:0; font:inherit; white-space:nowrap; + color:var(--vyp-ha-text-secondary,var(--secondary-text-color,#595959)); background:transparent; + cursor:pointer; text-decoration:none; } + [aria-current=page],[aria-selected=true] { color:var(--vyp-ha-text,var(--primary-text-color,#1c1c1c)); border-bottom-color:currentColor; } + a:hover,button:hover { color:var(--vyp-ha-text,var(--primary-text-color,#1c1c1c)); } + a:focus-visible,button:focus-visible { outline:2px solid var(--vyp-ha-primary,var(--primary-color,#0075b5)); outline-offset:-4px; } + [aria-disabled=true],button:disabled { cursor:not-allowed; opacity:.5; } + [hidden] { display:none !important; } + `; + #model = navigationState({kind:'navigation',label:'Sections',selected:null,items:[]}); + #focused: string | null = null; + get state(): NavigationState { return this.#model; } + set state(value: NavigationState) { + const next = navigationState(value), previous = this.#model; + this.#model = next; this.#focused = null; this.requestUpdate('state', previous); + } + #activate(event: MouseEvent, item: NavigationItem, snapshot: NavigationState): void { + if (snapshot !== this.state || item.disabled) { event.preventDefault(); return; } + if (event.defaultPrevented) return; + if (snapshot.kind === 'navigation' && (event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey)) return; + const intent: NavigationSelectionEvent = new CustomEvent('ha-selection-request', { + detail: Object.freeze({kind:snapshot.kind,id:item.id}), bubbles:true, composed:true, cancelable:true, + }); + if (!this.dispatchEvent(intent)) event.preventDefault(); + } + #move(event: KeyboardEvent, item: NavigationItem, snapshot: NavigationState): void { + if (snapshot !== this.state || item.disabled || event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey) return; + const enabled = snapshot.items.filter(row => !row.disabled); + const index = enabled.findIndex(row => row.id === item.id); + const direction = getComputedStyle(this).direction === 'rtl' ? -1 : 1; + let destination: number; + switch (event.key) { + case 'Home': destination=0; break; + case 'End': destination=enabled.length-1; break; + case 'ArrowRight': destination=(index+direction+enabled.length)%enabled.length; break; + case 'ArrowLeft': destination=(index-direction+enabled.length)%enabled.length; break; + default: return; + } + event.preventDefault(); this.#focused=enabled[destination].id; this.requestUpdate(); + this.renderRoot.querySelector('#tab-'+this.#focused)!.focus(); + } + #leave(event: FocusEvent): void { + const bar=event.currentTarget as HTMLElement; + if (!(event.relatedTarget instanceof Node) || !bar.contains(event.relatedTarget)) { + this.#focused=null; this.requestUpdate(); + } + } + protected render() { + const snapshot=this.state; + if (snapshot.kind === 'navigation') return html``; + const enabled=snapshot.items.filter(item=>!item.disabled); + const tabStop=enabled.find(item=>item.id === this.#focused)?.id ?? snapshot.selected ?? enabled[0]?.id; + return html`
+ ${snapshot.items.map(item => html``)} +
${snapshot.items.map(item => html`
`)}`; + } +} + +/** Call at the explicit client boundary. Imports never define tags; component rendering and registration require a live browser. */ +export function registerTabs(registry: CustomElementRegistry | undefined = globalThis.customElements): void { + if (registry === undefined || typeof document === 'undefined') throw new Error('registerTabs requires a browser CustomElementRegistry'); + const current=registry.get('vyp-ha-tabs'); + if (current !== undefined && current !== HaTabs) throw new Error('vyp-ha-tabs registration conflict; use one reviewed package cohort'); + if (current === undefined) registry.define('vyp-ha-tabs',HaTabs); +} + +declare global { + interface HTMLElementTagNameMap { 'vyp-ha-tabs': HaTabs; } + interface HTMLElementEventMap { 'ha-selection-request': NavigationSelectionEvent; } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 92eb494..e227b12 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,9 +11,15 @@ importers: '@types/node': specifier: 24.19.2 version: 24.19.2 + '@vitest/browser-playwright': + specifier: 5.1.0 + version: 5.1.0(playwright@1.64.1)(vite@8.3.5(@types/node@24.19.2)(yaml@2.9.0))(vitest@5.1.0) '@vitest/coverage-v8': specifier: 5.1.0 - version: 5.1.0(vitest@5.1.0) + version: 5.1.0(@vitest/browser@5.1.0)(vitest@5.1.0) + playwright: + specifier: 1.64.1 + version: 1.64.1 typescript: specifier: 6.0.3 version: 6.0.3 @@ -22,12 +28,16 @@ importers: version: 8.3.5(@types/node@24.19.2)(yaml@2.9.0) vitest: specifier: 5.1.0 - version: 5.1.0(@types/node@24.19.2)(@vitest/coverage-v8@5.1.0)(vite@8.3.5(@types/node@24.19.2)(yaml@2.9.0)) + version: 5.1.0(@types/node@24.19.2)(@vitest/browser-playwright@5.1.0)(@vitest/coverage-v8@5.1.0)(@vitest/ui@5.1.0)(vite@8.3.5(@types/node@24.19.2)(yaml@2.9.0)) yaml: specifier: 2.9.0 version: 2.9.0 - packages/core: {} + packages/core: + dependencies: + lit: + specifier: 3.3.3 + version: 3.3.3 packages: @@ -52,6 +62,9 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} + '@blazediff/core@1.10.0': + resolution: {integrity: sha512-AOQff0zgR7cGsZL+4E7hVkmujoPUpm0J9xzWGWZj5wCjd3gmxESXAPfKyuzs93VdpQNFhHlBhfOjrcZ+XTERtQ==} + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -62,9 +75,18 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@lit-labs/ssr-dom-shim@1.6.0': + resolution: {integrity: sha512-VHb0ALPMTlgKjM6yIxxoQNnpKyUKLD04VzeQdsiXkMqkvYlAHxq9glGLmgbb889/1GsohSOAjvQYoiBppXFqrQ==} + + '@lit/reactive-element@2.1.2': + resolution: {integrity: sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==} + '@oxc-project/types@0.156.0': resolution: {integrity: sha512-1Duyg48TdJNdyhEB8Xn1L36bT9Pv6GUtlKjHxOys8xkaF/rQq3XJE+bAurSK/AgIz5mGFsb77cn9Pb20+WCScw==} + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + '@rolldown/binding-android-arm-eabi@1.2.16': resolution: {integrity: sha512-ia3DwAs0OsMGS7+IConptSiyk9pEXkOx/AbJ2n/lAGxH3QkWQus1t8d7gnAT38FCU2sQu8lUbeR6AiZFv4JxaA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -176,6 +198,20 @@ packages: '@types/node@24.19.2': resolution: {integrity: sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@vitest/browser-playwright@5.1.0': + resolution: {integrity: sha512-3PbVvX/KNeUEzUeTTXw5bQDyfJS+WqU/UJ8RNWIojHb0mE6d/IBjuLmt+i7OpJ00tjNWv0O6XBm5ler/tEf3eg==} + peerDependencies: + playwright: '*' + vitest: 5.1.0 + + '@vitest/browser@5.1.0': + resolution: {integrity: sha512-2sjGVAC/Rs8Jwv+xxBgklNG2g1LGyNgcFMBw71AcfBstLz25UGv1+fVvvZ+z8IfSuNg3nipERijnFtoEIdRDEA==} + peerDependencies: + vitest: 5.1.0 + '@vitest/coverage-v8@5.1.0': resolution: {integrity: sha512-OY9aShBQG5KOIw2zCo9fDoCHI9RHliNHJ//nM7m6jGg41Hyke9JOGWmeMlK74xYErsxMoK8mIB3gAeEyFVA7LA==} peerDependencies: @@ -204,9 +240,20 @@ packages: vite: optional: true + '@vitest/pretty-format@5.1.0': + resolution: {integrity: sha512-IRx5mc30CrFzyMNni6SZKQ1r53kv049Mcvvk0WbWcD6PUz/bFjG4zYv0KosQAeonPpGp5WxqYBMqdSOfJfvVWw==} + '@vitest/spy@5.1.0': resolution: {integrity: sha512-2DPO5OMHWKrBngC+fCmTQqemNF0gidRhp3lxbBmRhW+M1XJrfgPpqNqWVevXtn2f2ufD5ZaR1B6B641aAlcX5A==} + '@vitest/ui@5.1.0': + resolution: {integrity: sha512-00S3x4NRmqIBUiTVWF9/xLaA9EP9KeKv0+1sBO7e2Y4luBISlW+2ttnhlk/zjbTqEvVhJWCCo7MXzu6zNbefqQ==} + peerDependencies: + vitest: 5.1.0 + + '@vitest/utils@5.1.0': + resolution: {integrity: sha512-V+qgCwkvVw+s1t1S5TaoBHsLi5Sz/YmohCMoOu0mZ8ZQst2bKhRYARAQucWpVg8D0mv0x5MHcWXY5QdOsoiGBQ==} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -218,6 +265,9 @@ packages: resolution: {integrity: sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==} engines: {node: '>=18'} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -241,6 +291,12 @@ packages: picomatch: optional: true + fflate@0.8.3: + resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} + + flatted@3.4.4: + resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -323,12 +379,25 @@ packages: resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} engines: {node: '>= 12.0.0'} + lit-element@4.2.2: + resolution: {integrity: sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==} + + lit-html@3.3.3: + resolution: {integrity: sha512-el8M6jK2o3RXBnrSHX3ZKrsN8zEV63pSExTO1wYJz7QndGYZ8353e2a5PPX+qHe2aGayfnchQmkAojaWAREOIA==} + + lit@3.3.3: + resolution: {integrity: sha512-fycuvZg/hkpozL00lm1pEJH5nN/lr9ZXd6mJI2HSN4+Bzc+LDNdEApJ6HFbPkdFNHLvOplIIuJvxkS4XUxqirw==} + magic-string@1.4.5: resolution: {integrity: sha512-oLLC2+YvQUfAa+vxoufE/yc1icb87M9iXIRqAoR/2EhE/8cb3SF/+Gi0c2By1B+5Vz2e2vQtUvHiu1+63Znh+w==} magicast@0.5.5: resolution: {integrity: sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==} + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + nanoid@3.3.20: resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -338,6 +407,9 @@ packages: resolution: {integrity: sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==} engines: {node: '>=12.20.0'} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -345,6 +417,20 @@ packages: resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} engines: {node: '>=12'} + playwright-core@1.64.1: + resolution: {integrity: sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ==} + engines: {node: '>=20'} + hasBin: true + + playwright@1.64.1: + resolution: {integrity: sha512-HCrVhHr5QQuwYoswEUGTb7gGp522yflbv/ST8nF4sFWGuLjQCt17Q/EubpGGSNKYn1tsTCPZxqHhHCfVURu7UA==} + engines: {node: '>=20'} + hasBin: true + + pngjs@7.0.0: + resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} + engines: {node: '>=14.19.0'} + postcss@8.5.32: resolution: {integrity: sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==} engines: {node: ^10 || ^12 || >=14} @@ -354,6 +440,10 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + source-map-js@1.2.2: resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} engines: {node: '>=0.10.0'} @@ -377,6 +467,10 @@ packages: resolution: {integrity: sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A==} engines: {node: '>=14.0.0'} + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + typescript@6.0.3: resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} engines: {node: '>=14.17'} @@ -474,6 +568,18 @@ packages: engines: {node: '>=20.11'} hasBin: true + ws@8.22.1: + resolution: {integrity: sha512-oyL6OPYme/CmiFvNuGzeoxCJ4nOrZE2O1FwM9MWM1AsGri6nLXB2pg+lNA8aLeXMSav0J6eIVS1j6vUPzUtK9g==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + yaml@2.9.0: resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} engines: {node: '>= 14.6'} @@ -496,6 +602,8 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} + '@blazediff/core@1.10.0': {} + '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/sourcemap-codec@1.6.0': {} @@ -505,8 +613,16 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.6.0 + '@lit-labs/ssr-dom-shim@1.6.0': {} + + '@lit/reactive-element@2.1.2': + dependencies: + '@lit-labs/ssr-dom-shim': 1.6.0 + '@oxc-project/types@0.156.0': {} + '@polka/url@1.0.0-next.29': {} + '@rolldown/binding-android-arm-eabi@1.2.16': optional: true @@ -567,7 +683,40 @@ snapshots: dependencies: undici-types: 7.24.6 - '@vitest/coverage-v8@5.1.0(vitest@5.1.0)': + '@types/trusted-types@2.0.7': {} + + '@vitest/browser-playwright@5.1.0(playwright@1.64.1)(vite@8.3.5(@types/node@24.19.2)(yaml@2.9.0))(vitest@5.1.0)': + dependencies: + '@vitest/browser': 5.1.0(vite@8.3.5(@types/node@24.19.2)(yaml@2.9.0))(vitest@5.1.0) + '@vitest/mocker': 5.1.0(vite@8.3.5(@types/node@24.19.2)(yaml@2.9.0)) + playwright: 1.64.1 + tinyrainbow: 3.2.0 + vitest: 5.1.0(@types/node@24.19.2)(@vitest/browser-playwright@5.1.0)(@vitest/coverage-v8@5.1.0)(@vitest/ui@5.1.0)(vite@8.3.5(@types/node@24.19.2)(yaml@2.9.0)) + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + + '@vitest/browser@5.1.0(vite@8.3.5(@types/node@24.19.2)(yaml@2.9.0))(vitest@5.1.0)': + dependencies: + '@blazediff/core': 1.10.0 + '@vitest/mocker': 5.1.0(vite@8.3.5(@types/node@24.19.2)(yaml@2.9.0)) + '@vitest/ui': 5.1.0(vitest@5.1.0) + '@vitest/utils': 5.1.0 + magic-string: 1.4.5 + pngjs: 7.0.0 + sirv: 3.0.2 + tinyrainbow: 3.2.0 + vitest: 5.1.0(@types/node@24.19.2)(@vitest/browser-playwright@5.1.0)(@vitest/coverage-v8@5.1.0)(@vitest/ui@5.1.0)(vite@8.3.5(@types/node@24.19.2)(yaml@2.9.0)) + ws: 8.22.1 + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + + '@vitest/coverage-v8@5.1.0(@vitest/browser@5.1.0)(vitest@5.1.0)': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/istanbul-lib-coverage': 2.1.0 @@ -577,7 +726,9 @@ snapshots: obug: 3.0.0 std-env: 4.3.0 tinyrainbow: 3.2.0 - vitest: 5.1.0(@types/node@24.19.2)(@vitest/coverage-v8@5.1.0)(vite@8.3.5(@types/node@24.19.2)(yaml@2.9.0)) + vitest: 5.1.0(@types/node@24.19.2)(@vitest/browser-playwright@5.1.0)(@vitest/coverage-v8@5.1.0)(@vitest/ui@5.1.0)(vite@8.3.5(@types/node@24.19.2)(yaml@2.9.0)) + optionalDependencies: + '@vitest/browser': 5.1.0(vite@8.3.5(@types/node@24.19.2)(yaml@2.9.0))(vitest@5.1.0) '@vitest/istanbul-lib-coverage@2.1.0': {} @@ -594,8 +745,28 @@ snapshots: optionalDependencies: vite: 8.3.5(@types/node@24.19.2)(yaml@2.9.0) + '@vitest/pretty-format@5.1.0': + dependencies: + tinyrainbow: 3.2.0 + '@vitest/spy@5.1.0': {} + '@vitest/ui@5.1.0(vitest@5.1.0)': + dependencies: + '@vitest/utils': 5.1.0 + fflate: 0.8.3 + flatted: 3.4.4 + pathe: 2.0.3 + sirv: 3.0.2 + tinyrainbow: 3.2.0 + vitest: 5.1.0(@types/node@24.19.2)(@vitest/browser-playwright@5.1.0)(@vitest/coverage-v8@5.1.0)(@vitest/ui@5.1.0)(vite@8.3.5(@types/node@24.19.2)(yaml@2.9.0)) + + '@vitest/utils@5.1.0': + dependencies: + '@vitest/pretty-format': 5.1.0 + convert-source-map: 2.0.0 + tinyrainbow: 3.2.0 + assertion-error@2.0.1: {} ast-v8-to-istanbul@1.0.7: @@ -606,6 +777,8 @@ snapshots: chai@6.3.0: {} + convert-source-map@2.0.0: {} + detect-libc@2.1.2: {} es-module-lexer@2.3.2: {} @@ -620,6 +793,10 @@ snapshots: optionalDependencies: picomatch: 4.0.7 + fflate@0.8.3: {} + + flatted@3.4.4: {} + fsevents@2.3.3: optional: true @@ -674,6 +851,22 @@ snapshots: lightningcss-win32-arm64-msvc: 1.33.0 lightningcss-win32-x64-msvc: 1.33.0 + lit-element@4.2.2: + dependencies: + '@lit-labs/ssr-dom-shim': 1.6.0 + '@lit/reactive-element': 2.1.2 + lit-html: 3.3.3 + + lit-html@3.3.3: + dependencies: + '@types/trusted-types': 2.0.7 + + lit@3.3.3: + dependencies: + '@lit/reactive-element': 2.1.2 + lit-element: 4.2.2 + lit-html: 3.3.3 + magic-string@1.4.5: dependencies: '@jridgewell/sourcemap-codec': 1.6.0 @@ -684,14 +877,26 @@ snapshots: '@babel/types': 7.29.11 source-map-js: 1.2.2 + mrmime@2.0.1: {} + nanoid@3.3.20: {} obug@3.0.0: {} + pathe@2.0.3: {} + picocolors@1.1.1: {} picomatch@4.0.7: {} + playwright-core@1.64.1: {} + + playwright@1.64.1: + dependencies: + playwright-core: 1.64.1 + + pngjs@7.0.0: {} + postcss@8.5.32: dependencies: nanoid: 3.3.20 @@ -719,6 +924,12 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.2.16 '@rolldown/binding-win32-x64-msvc': 1.2.16 + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + source-map-js@1.2.2: {} std-env@4.3.0: {} @@ -734,6 +945,8 @@ snapshots: tinyrainbow@3.2.0: {} + totalist@3.0.1: {} + typescript@6.0.3: {} undici-types@7.24.6: {} @@ -750,7 +963,7 @@ snapshots: fsevents: 2.3.3 yaml: 2.9.0 - vitest@5.1.0(@types/node@24.19.2)(@vitest/coverage-v8@5.1.0)(vite@8.3.5(@types/node@24.19.2)(yaml@2.9.0)): + vitest@5.1.0(@types/node@24.19.2)(@vitest/browser-playwright@5.1.0)(@vitest/coverage-v8@5.1.0)(@vitest/ui@5.1.0)(vite@8.3.5(@types/node@24.19.2)(yaml@2.9.0)): dependencies: '@types/chai': 5.2.3 '@vitest/mocker': 5.1.0(vite@8.3.5(@types/node@24.19.2)(yaml@2.9.0)) @@ -768,10 +981,14 @@ snapshots: why-is-node-running: 3.2.1 optionalDependencies: '@types/node': 24.19.2 - '@vitest/coverage-v8': 5.1.0(vitest@5.1.0) + '@vitest/browser-playwright': 5.1.0(playwright@1.64.1)(vite@8.3.5(@types/node@24.19.2)(yaml@2.9.0))(vitest@5.1.0) + '@vitest/coverage-v8': 5.1.0(@vitest/browser@5.1.0)(vitest@5.1.0) + '@vitest/ui': 5.1.0(vitest@5.1.0) transitivePeerDependencies: - msw why-is-node-running@3.2.1: {} + ws@8.22.1: {} + yaml@2.9.0: {} diff --git a/specs/CATALOG.md b/specs/CATALOG.md index 6557890..2f87a8e 100644 --- a/specs/CATALOG.md +++ b/specs/CATALOG.md @@ -10,7 +10,7 @@ debt or convert unknown historic intent into a design decision. | Capability ID | Status | Scope | Primary SDD | Evidence | |---|---|---|---|---| -| `shared-ui-foundation` | Proposed | One DOM component implementation with React, Vue, Angular and Svelte adapters, direct Lit/DOM use, shared Storybook and verified Gateway/Domus adoption. Draft; no runtime implementation or delivery accepted. | [Shared Home Assistant presentation components and framework adapters](./shared-ui-foundation.md) + 1 companion | 15 paths · 2026-11-04 | +| `shared-ui-foundation` | Proposed | One DOM component implementation with React, Vue, Angular and Svelte adapters, direct Lit/DOM use, shared Storybook and verified Gateway/Domus adoption. Partial source; no complete capability or release accepted. | [Shared Home Assistant presentation components and framework adapters](./shared-ui-foundation.md) + 1 companion | 19 paths · 2026-11-04 | ## Evidence map @@ -20,9 +20,9 @@ debt or convert unknown historic intent into a design decision. - Last verified: 2026-11-04 - Specifications: [`specs/shared-ui-foundation.md`](./shared-ui-foundation.md) · [`specs/shared-ui-framework-delivery.md`](./shared-ui-framework-delivery.md) - Workflows: [`.github/workflows/copilot_issue.yml`](../.github/workflows/copilot_issue.yml) · [`.github/workflows/ci_check.yml`](../.github/workflows/ci_check.yml) -- Entrypoints: [`packages/core/src/index.ts`](../packages/core/src/index.ts) -- Core code: [`packages/core/src/contracts.ts`](../packages/core/src/contracts.ts) · [`tooling/coverage.mjs`](../tooling/coverage.mjs) -- Tests: [`tests/contracts/core.test.ts`](../tests/contracts/core.test.ts) · [`tests/coverage.test.mjs`](../tests/coverage.test.mjs) · [`tests/architecture.test.mjs`](../tests/architecture.test.mjs) · [`tests/coverage-evidence.test.mjs`](../tests/coverage-evidence.test.mjs) · [`tests/consumers/packed.test.ts`](../tests/consumers/packed.test.ts) +- Entrypoints: [`packages/core/src/index.ts`](../packages/core/src/index.ts) · [`packages/core/src/components/tabs.ts`](../packages/core/src/components/tabs.ts) +- Core code: [`packages/core/src/contracts.ts`](../packages/core/src/contracts.ts) · [`tooling/coverage.mjs`](../tooling/coverage.mjs) · [`packages/core/src/components/tabs.ts`](../packages/core/src/components/tabs.ts) +- Tests: [`tests/contracts/core.test.ts`](../tests/contracts/core.test.ts) · [`tests/coverage.test.mjs`](../tests/coverage.test.mjs) · [`tests/architecture.test.mjs`](../tests/architecture.test.mjs) · [`tests/coverage-evidence.test.mjs`](../tests/coverage-evidence.test.mjs) · [`tests/consumers/packed.test.ts`](../tests/consumers/packed.test.ts) · [`tests/contracts/registration.test.ts`](../tests/contracts/registration.test.ts) · [`tests/browser/tabs.test.ts`](../tests/browser/tabs.test.ts) - User documentation: [`README.md`](../README.md) · [`docs/component-library.md`](../docs/component-library.md) · [`docs/compatibility.md`](../docs/compatibility.md) · [`docs/provenance.md`](../docs/provenance.md) · [`docs/evidence/shared-ui-foundation.json`](../docs/evidence/shared-ui-foundation.json) ## Maintenance contract diff --git a/specs/catalog.json b/specs/catalog.json index 4f6af19..cc0fda3 100644 --- a/specs/catalog.json +++ b/specs/catalog.json @@ -5,7 +5,7 @@ "id": "shared-ui-foundation", "title": "Shared Home Assistant presentation components and framework adapters", "status": "proposed", - "scope": "One DOM component implementation with React, Vue, Angular and Svelte adapters, direct Lit/DOM use, shared Storybook and verified Gateway/Domus adoption. Draft; no runtime implementation or delivery accepted.", + "scope": "One DOM component implementation with React, Vue, Angular and Svelte adapters, direct Lit/DOM use, shared Storybook and verified Gateway/Domus adoption. Partial source; no complete capability or release accepted.", "owner": "vypdev maintainers; Efrain Espada (accountable product/publication owner)", "lastVerified": "2026-11-04", "specs": [ @@ -17,18 +17,22 @@ ".github/workflows/ci_check.yml" ], "entrypoints": [ - "packages/core/src/index.ts" + "packages/core/src/index.ts", + "packages/core/src/components/tabs.ts" ], "code": [ "packages/core/src/contracts.ts", - "tooling/coverage.mjs" + "tooling/coverage.mjs", + "packages/core/src/components/tabs.ts" ], "tests": [ "tests/contracts/core.test.ts", "tests/coverage.test.mjs", "tests/architecture.test.mjs", "tests/coverage-evidence.test.mjs", - "tests/consumers/packed.test.ts" + "tests/consumers/packed.test.ts", + "tests/contracts/registration.test.ts", + "tests/browser/tabs.test.ts" ], "documentation": [ "README.md", @@ -46,7 +50,7 @@ "Storybook Pages delivery and human UX review", "Actual Gateway and Domus adoption" ], - "implementationEvidence": "partial: DOM-independent navigation data contract and quality/toolchain setup only; no rendered components, adapters, framework fixtures, Storybook or publication accepted." + "implementationEvidence": "partial: closed data contract and controlled HA-style navigation/panel renderer; strict Node/Chromium coverage and clean actual packed DOM-free/browser consumers pass. Other families, adapters, complete framework/browser matrix, Storybook, publication and app adoption remain required." } } ] diff --git a/specs/shared-ui-framework-delivery.md b/specs/shared-ui-framework-delivery.md index df5eb06..d0c965f 100644 --- a/specs/shared-ui-framework-delivery.md +++ b/specs/shared-ui-framework-delivery.md @@ -318,6 +318,8 @@ Prospective repository files (current documents are classified against the immut | `packages/svelte/package.json` | create | | `packages/svelte/src/index.ts` | create | | `tests/contracts/core.test.ts` | create | +| `tests/contracts/registration.test.ts` | create | +| `tests/browser/tabs.test.ts` | create | | `tests/contracts/adapters.test.ts` | create | | `tests/architecture.test.mjs` | create | | `tests/coverage-evidence.test.mjs` | create | diff --git a/tests/architecture.test.mjs b/tests/architecture.test.mjs index 1cba97b..3e7d5f4 100644 --- a/tests/architecture.test.mjs +++ b/tests/architecture.test.mjs @@ -44,7 +44,7 @@ test('CI Check runs for every PR update with frozen tools and all implemented qu expect(job['continue-on-error']).toBeUndefined(); expect(job.steps.every(step => step['continue-on-error'] === undefined)).toBe(true); const commands = job.steps.flatMap(step => step.run ? [step.run] : []); - for (const command of ['pnpm install --frozen-lockfile --ignore-scripts', 'pnpm check', 'pnpm test', 'pnpm build', 'pnpm test:package']) expect(commands).toContain(command); + for (const command of ['pnpm install --frozen-lockfile --ignore-scripts', 'pnpm check', 'pnpm exec playwright install --with-deps chromium --only-shell', 'pnpm test', 'pnpm build', 'pnpm test:package']) expect(commands).toContain(command); const sdk = job.steps.find(step => step.with?.repository === 'vypdev/copilot'); expect(sdk.with.ref).toBe('3e7ceea08e6131360e026937d1c0cc1b120472e0'); expect(sdk.with['persist-credentials']).toBe(false); diff --git a/tests/browser/tabs.test.ts b/tests/browser/tabs.test.ts new file mode 100644 index 0000000..38149c4 --- /dev/null +++ b/tests/browser/tabs.test.ts @@ -0,0 +1,197 @@ +import {afterEach, expect, test, vi} from 'vitest'; +import {page, userEvent} from 'vitest/browser'; +import {navigationState, NavigationInputError} from '../../packages/core/src/contracts.js'; +import {HaTabs, registerTabs, type NavigationSelectionEvent} from '../../packages/core/src/components/tabs.js'; + +const rows = [{id:'overview',label:'Overview',href:'#overview'}, {id:'locked',label:'Locked',href:'#locked',disabled:true}, {id:'tasks',label:'Tasks',href:'#tasks'}]; +const model = (patch: Record = {}) => navigationState({kind:'navigation',label:'Household sections',selected:'overview',items:rows,...patch}); +const panels = (patch: Record = {}) => model({kind:'panels',items:rows.map(({href, ...row}) => row),...patch}); +async function mount(state = model()) { + registerTabs(); const element = document.createElement('vyp-ha-tabs'); + element.state = state; document.body.append(element); await element.updateComplete; return element; +} +const root = (element: HaTabs) => element.shadowRoot!; +const controls = (element: HaTabs) => [...root(element).querySelectorAll('a,button')]; +const intents = (element: HaTabs) => { + const received: NavigationSelectionEvent[] = []; + element.addEventListener('ha-selection-request', event => received.push(event)); return received; +}; +afterEach(() => { document.body.replaceChildren(); }); + +test('upgrades an already connected property without replacing the caller state', async () => { + const element = document.createElement('vyp-ha-tabs'); + element.state = model({label:'Secciones del hogar'}); document.body.append(element); + registerTabs(); await element.updateComplete; + expect(element).toBeInstanceOf(HaTabs); + expect(root(element).querySelector('nav')!.getAttribute('aria-label')).toBe('Secciones del hogar'); + expect(element.state.label).toBe('Secciones del hogar'); +}); +test('registration is explicit, idempotent and rejects an incompatible constructor', () => { + registerTabs(); registerTabs(customElements); + expect(customElements.get('vyp-ha-tabs')).toBe(HaTabs); + expect(() => registerTabs({get: () => HTMLElement} as unknown as CustomElementRegistry)).toThrow(/conflict/u); +}); +test('has an empty labeled navigation before the app supplies state', async () => { + registerTabs(); const element = document.createElement('vyp-ha-tabs'); document.body.append(element); await element.updateComplete; + expect(root(element).querySelector('nav')!.getAttribute('aria-label')).toBe('Sections'); + expect(controls(element)).toHaveLength(0); +}); +test('navigation exposes real links and current location without tab semantics or executable labels', async () => { + const element = await mount(model({items:[{id:'overview',label:'',href:'#overview'}, ...rows.slice(1)]})); + const links = controls(element); + expect(links[0].getAttribute('href')).toBe('#overview'); expect(links[0].getAttribute('aria-current')).toBe('page'); + expect(links[0].textContent).toBe(''); expect(root(element).querySelector('script')).toBeNull(); + expect(links[1].getAttribute('href')).toBeNull(); expect(links[1].getAttribute('aria-disabled')).toBe('true'); expect(links[1].tabIndex).toBe(-1); + expect(links[2].hasAttribute('aria-current')).toBe(false); + expect(root(element).querySelector('[role=tablist],[role=tab],[role=tabpanel]')).toBeNull(); +}); +test('one user intent bubbles through a shadow host, is cancelable and does not mutate controlled selection', async () => { + const container = document.createElement('div'); container.attachShadow({mode:'open'}); + document.body.append(container); const element = await mount(); container.shadowRoot!.append(element); + const received: NavigationSelectionEvent[] = []; + document.body.addEventListener('ha-selection-request', event => { received.push(event); event.preventDefault(); }, {once:true}); + const click = new MouseEvent('click',{bubbles:true,composed:true,cancelable:true,button:0}); + controls(element)[2].dispatchEvent(click); + expect(received).toHaveLength(1); expect(received[0].detail).toEqual({kind:'navigation',id:'tasks'}); + expect(received[0].cancelable).toBe(true); expect(received[0].composed).toBe(true); expect(Object.isFrozen(received[0].detail)).toBe(true); + expect(click.defaultPrevented).toBe(true); expect(element.state.selected).toBe('overview'); + element.state = model({selected:'tasks'}); await element.updateComplete; + expect(controls(element)[2].getAttribute('aria-current')).toBe('page'); expect(received).toHaveLength(1); +}); +test.each([{ctrlKey:true},{metaKey:true},{shiftKey:true},{altKey:true},{button:1}])('retains native modified navigation without emitting intent: %j', async options => { + const element = await mount(); const received = intents(element); + const click = new MouseEvent('click',{bubbles:true,cancelable:true,...options}); + // Stop the fixture navigating only after the component has observed the event. + element.addEventListener('click', event => event.preventDefault(), {once:true}); controls(element)[2].dispatchEvent(click); + expect(received).toHaveLength(0); expect(controls(element)[2].getAttribute('href')).toBe('#tasks'); +}); +test('respects an already canceled click and prevents even synthetic activation of disabled links', async () => { + const element = await mount(); const received = intents(element); + const canceled = new MouseEvent('click',{bubbles:true,cancelable:true}); canceled.preventDefault(); controls(element)[2].dispatchEvent(canceled); + const disabled = new MouseEvent('click',{bubbles:true,cancelable:true}); controls(element)[1].dispatchEvent(disabled); + expect(disabled.defaultPrevented).toBe(true); expect(received).toHaveLength(0); +}); +test('uncanceled navigation keeps the real link default and current controlled location', async () => { + const element = await mount(); const received = intents(element); + const click = new MouseEvent('click',{bubbles:true,composed:true,cancelable:true}); + let canceledByComponent = true; + element.addEventListener('click', event => { canceledByComponent = event.defaultPrevented; event.preventDefault(); }, {once:true}); + controls(element)[2].dispatchEvent(click); + expect(canceledByComponent).toBe(false); expect(received).toHaveLength(1); expect(element.state.selected).toBe('overview'); +}); +test('panel tabs own real same-root labeled panels and keep hidden slot content out of focus', async () => { + const element = await mount(panels()); + const content = document.createElement('button'); content.slot='tasks'; content.textContent='Task content'; element.append(content); + const tabs = controls(element), sections = [...root(element).querySelectorAll('[role=tabpanel]')]; + expect(root(element).querySelector('[role=tablist]')!.getAttribute('aria-label')).toBe('Household sections'); + for (const [index, tab] of tabs.entries()) { + expect(tab.getAttribute('role')).toBe('tab'); expect(tab.getAttribute('type')).toBe('button'); + expect(tab.getAttribute('aria-controls')).toBe(sections[index].id); + expect(sections[index].getAttribute('aria-labelledby')).toBe(tab.id); + } + expect(tabs.map(tab=>tab.tabIndex)).toEqual([0,-1,-1]); expect(tabs[0].getAttribute('aria-selected')).toBe('true'); + expect(sections.map(panel=>panel.hidden)).toEqual([false,true,true]); expect(content.checkVisibility()).toBe(false); + element.state=panels({selected:'tasks'}); await element.updateComplete; + expect(content.checkVisibility()).toBe(true); expect((root(element).querySelector('slot[name=tasks]') as HTMLSlotElement).assignedElements()).toEqual([content]); +}); +test('manual keyboard movement skips disabled tabs, wraps, uses Home/End and activates once with Enter/Space', async () => { + const element = await mount(panels()); const received = intents(element); controls(element)[0].focus(); + await userEvent.keyboard('{ArrowRight}'); await element.updateComplete; + expect(root(element).activeElement).toBe(controls(element)[2]); expect(received).toHaveLength(0); expect(element.state.selected).toBe('overview'); + await userEvent.keyboard('{ArrowRight}'); expect(root(element).activeElement).toBe(controls(element)[0]); + await userEvent.keyboard('{ArrowLeft}'); expect(root(element).activeElement).toBe(controls(element)[2]); + await userEvent.keyboard('{Home}'); expect(root(element).activeElement).toBe(controls(element)[0]); + await userEvent.keyboard('{End}'); expect(root(element).activeElement).toBe(controls(element)[2]); + await userEvent.keyboard('{Enter}'); expect(received).toHaveLength(1); + await userEvent.keyboard(' '); expect(received).toHaveLength(2); expect(received.map(event=>event.detail.id)).toEqual(['tasks','tasks']); + expect(element.state.selected).toBe('overview'); +}); +test('RTL reverses horizontal arrow movement and ignores unrelated keys', async () => { + const element = await mount(panels()); element.dir='rtl'; controls(element)[0].focus(); + await userEvent.keyboard('{ArrowRight}'); expect(root(element).activeElement).toBe(controls(element)[2]); + await userEvent.keyboard('{ArrowLeft}'); expect(root(element).activeElement).toBe(controls(element)[0]); + const ignored = new KeyboardEvent('keydown',{key:'ArrowDown',bubbles:true,cancelable:true}); controls(element)[0].dispatchEvent(ignored); + expect(ignored.defaultPrevented).toBe(false); expect(root(element).activeElement).toBe(controls(element)[0]); +}); +test('null selection keeps one enabled keyboard entry without selecting a panel; no enabled items stay inert', async () => { + const element=await mount(panels({selected:null})); expect(controls(element).map(tab=>tab.tabIndex)).toEqual([0,-1,-1]); + expect([...root(element).querySelectorAll('[role=tabpanel]')].every(panel=>panel.hidden)).toBe(true); + element.state=panels({selected:null,items:[{id:'locked',label:'Locked',disabled:true}]}); await element.updateComplete; + const key=new KeyboardEvent('keydown',{key:'End',bubbles:true,cancelable:true}); controls(element)[0].dispatchEvent(key); + expect(key.defaultPrevented).toBe(false); expect(controls(element)[0].tabIndex).toBe(-1); + element.state=panels({selected:null,items:[]}); await element.updateComplete; expect(controls(element)).toHaveLength(0); +}); +test('invalid assignments leave previous validated input and DOM intact', async () => { + const element=await mount(); const previous=element.state; + expect(()=>{element.state={...previous,selected:'absent'};}).toThrow(NavigationInputError); + expect(element.state).toBe(previous); expect(controls(element)[0].getAttribute('aria-current')).toBe('page'); +}); +test('disconnect/reconnect does not retain duplicate subscriptions or emit property-change events', async () => { + const element=await mount(panels()); const received=intents(element); element.remove(); document.body.append(element); + element.state=panels({label:'Secciones',selected:'tasks'}); await element.updateComplete; + expect(received).toHaveLength(0); await page.getByRole('tab',{name:'Overview',exact:true}).click(); + expect(received).toHaveLength(1); expect(received[0].detail).toEqual({kind:'panels',id:'overview'}); +}); +test('HA theme tokens update live, the 56px bar scrolls narrowly, and keyboard focus remains visible without motion', async () => { + const element=await mount(panels({items:Array.from({length:10},(_,n)=>({id:'tab-'+n,label:'Household section '+n})),selected:'tab-0'})); + element.style.cssText='display:block;width:240px;--primary-text-color:rgb(28,28,28);--card-background-color:rgb(255,255,255);--primary-color:rgb(0,110,180)'; + expect(getComputedStyle(controls(element)[0]).minHeight).toBe('56px'); + const bar=root(element).querySelector('.bar')!; expect(bar.scrollWidth).toBeGreaterThan(bar.clientWidth); + expect(getComputedStyle(controls(element)[0]).color).toBe('rgb(28, 28, 28)'); + element.style.setProperty('--primary-text-color','rgb(235,235,235)'); element.style.setProperty('--card-background-color','rgb(20,20,20)'); + expect(getComputedStyle(controls(element)[0]).color).toBe('rgb(235, 235, 235)'); + expect(getComputedStyle(bar).backgroundColor).toBe('rgb(20, 20, 20)'); + controls(element)[0].focus(); await userEvent.keyboard('{End}'); + const current=root(element).activeElement!; expect(current).toBe(controls(element)[9]); + expect(current.matches(':focus-visible')).toBe(true); expect(getComputedStyle(current).outlineStyle).toBe('solid'); + expect(getComputedStyle(current).animationName).toBe('none'); expect(getComputedStyle(current).transitionDuration).toBe('0s'); + expect(bar.scrollLeft).toBeGreaterThan(0); +}); + +test('reports a missing browser registry without silently falling back to another implementation', () => { + vi.stubGlobal('customElements', undefined); + try { expect(()=>registerTabs()).toThrow('registerTabs requires a browser CustomElementRegistry'); } + finally { vi.unstubAllGlobals(); } +}); +test('rejects stale click/key handlers before a replacement input has rendered', async () => { + const element=await mount(panels()); const old=controls(element)[0], received=intents(element); + element.state=panels({selected:'tasks'}); + const click=new MouseEvent('click',{bubbles:true,cancelable:true}); old.dispatchEvent(click); + const key=new KeyboardEvent('keydown',{key:'End',bubbles:true,cancelable:true}); old.dispatchEvent(key); + expect(click.defaultPrevented).toBe(true); expect(key.defaultPrevented).toBe(false); expect(received).toHaveLength(0); + await element.updateComplete; +}); +test.each([{ctrlKey:true},{metaKey:true},{altKey:true}])('does not trap modified keyboard navigation: %j', async options => { + const element=await mount(panels()); controls(element)[0].focus(); + const key=new KeyboardEvent('keydown',{key:'End',bubbles:true,cancelable:true,...options}); controls(element)[0].dispatchEvent(key); + expect(key.defaultPrevented).toBe(false); expect(root(element).activeElement).toBe(controls(element)[0]); +}); +test('retains a previously canceled keyboard event', async () => { + const element=await mount(panels()); controls(element)[0].focus(); + const key=new KeyboardEvent('keydown',{key:'End',bubbles:true,cancelable:true}); key.preventDefault(); controls(element)[0].dispatchEvent(key); + expect(root(element).activeElement).toBe(controls(element)[0]); +}); + +test('leaving a manually focused tab restores the active tab as the next keyboard entry', async () => { + const element=await mount(panels()); const outside=document.createElement('button'); outside.textContent='Outside'; document.body.append(outside); + controls(element)[0].focus(); await userEvent.keyboard('{End}'); await element.updateComplete; + expect(controls(element).map(tab=>tab.tabIndex)).toEqual([-1,-1,0]); + outside.focus(); await element.updateComplete; + expect(controls(element).map(tab=>tab.tabIndex)).toEqual([0,-1,-1]); expect(element.state.selected).toBe('overview'); +}); + +test('blur without a related focus target also resets the roving keyboard entry', async () => { + const element=await mount(panels()); controls(element)[0].focus(); await userEvent.keyboard('{End}'); await element.updateComplete; + controls(element)[2].blur(); await element.updateComplete; + expect(controls(element).map(tab=>tab.tabIndex)).toEqual([0,-1,-1]); +}); +test('runtime Spanish labels keep the same identities, relationships and single user event', async () => { + const element=await mount(panels()); const received=intents(element); + element.state=panels({label:'Secciones del hogar',items:[{id:'overview',label:'Resumen'},{id:'locked',label:'Bloqueado',disabled:true},{id:'tasks',label:'Tareas'}]}); + await element.updateComplete; + expect(root(element).querySelector('[role=tablist]')!.getAttribute('aria-label')).toBe('Secciones del hogar'); + expect(received).toHaveLength(0); await page.getByRole('tab',{name:'Tareas',exact:true}).click(); + expect(received).toHaveLength(1); expect(received[0].detail.id).toBe('tasks'); + const disabled=new MouseEvent('click',{bubbles:true,cancelable:true}); controls(element)[1].dispatchEvent(disabled); + expect(disabled.defaultPrevented).toBe(true); expect(received).toHaveLength(1); +}); diff --git a/tests/consumers/packed.test.ts b/tests/consumers/packed.test.ts index 23c6b8c..fbe41fc 100644 --- a/tests/consumers/packed.test.ts +++ b/tests/consumers/packed.test.ts @@ -7,14 +7,18 @@ import {tmpdir} from 'node:os'; import {resolve, join} from 'node:path'; import {fileURLToPath} from 'node:url'; import {createHash} from 'node:crypto'; +import {createServer} from 'vite'; +import {chromium} from 'playwright'; const execute = promisify(execFile); const root = fileURLToPath(new URL('../../', import.meta.url)); const digest = (bytes: Uint8Array) => createHash('sha256').update(bytes).digest('hex'); -test('the actual packed contract is reproducible and compiles/imports in a clean DOM-free consumer', {timeout: 60_000}, async () => { +test('the actual packed package is reproducible and passes clean DOM-free and real-browser consumers', {timeout: 60_000}, async () => { const scratch = await mkdtemp(join(tmpdir(), 'ha-ui-packed-contract-')); let passed = false; + let server: Awaited> | undefined; + let browser: Awaited> | undefined; try { const archives = []; for (const index of [1, 2]) { @@ -42,12 +46,57 @@ if(value.selected!=='tasks'||!Object.isFrozen(value.items))throw Error('Packed c console.log('Packed contract imported without DOM globals');`], {cwd: consumer}); assert.match(probe.stdout, /Packed contract imported without DOM globals/u); const installed = join(consumer, 'node_modules/@vypdev/ha-ui'); - for (const file of ['dist/contracts.js','dist/contracts.d.ts','src/contracts.ts','LICENSE']) + const packageFiles=['LICENSE']; + for (const module of ['contracts','index','components/tabs']) { + packageFiles.push('src/'+module+'.ts'); + for (const suffix of ['.js','.js.map','.d.ts','.d.ts.map']) packageFiles.push('dist/'+module+suffix); + } + for (const file of packageFiles) assert.deepEqual(await readFile(join(installed,file)),await readFile(resolve(root,'packages/core',file))); + await writeFile(join(consumer,'index.html'), 'Packed navigation consumer
'); + await writeFile(join(consumer,'client.ts'), `import {registerTabs} from '@vypdev/ha-ui/tabs'; +import {navigationState} from '@vypdev/ha-ui/contracts'; +registerTabs(); +const element=document.createElement('vyp-ha-tabs'); +element.state=navigationState({kind:'navigation',label:'Household sections',selected:'overview',items:[{id:'overview',label:'Overview',href:'#overview'},{id:'tasks',label:'Tasks',href:'#tasks'}]}); +element.addEventListener('ha-selection-request',event=>{event.preventDefault();element.state=navigationState({...element.state,selected:event.detail.id});}); +document.querySelector('main')!.append(element);`); + await writeFile(join(consumer,'client.tsconfig.json'),JSON.stringify({compilerOptions:{target:'ES2024',module:'NodeNext',moduleResolution:'NodeNext',strict:true,noEmit:true,lib:['ES2024','DOM'],types:[]},include:['client.ts']})); + await execute(process.execPath,[resolve(root,'node_modules/typescript/bin/tsc'),'-p','client.tsconfig.json'],{cwd:consumer}); + server=await createServer({configFile:false,root:consumer,logLevel:'error',server:{host:'127.0.0.1',port:0}}); + await server.listen(); const address=server.httpServer!.address(); assert.ok(address && typeof address==='object'); + browser=await chromium.launch(); const browserPage=await browser.newPage({viewport:{width:360,height:640},reducedMotion:'reduce'}); + const errors: string[]=[]; browserPage.on('pageerror',error=>errors.push(error.message)); + await browserPage.goto('http://127.0.0.1:'+address.port); + assert.equal(await browserPage.getByRole('navigation',{name:'Household sections'}).isVisible(),true); + await browserPage.getByRole('link',{name:'Tasks',exact:true}).click(); + await browserPage.waitForFunction(()=>document.querySelector('vyp-ha-tabs')!.state.selected==='tasks'); + assert.equal(await browserPage.getByRole('link',{name:'Tasks',exact:true}).getAttribute('aria-current'),'page'); + assert.equal(new URL(browserPage.url()).hash,'','Canceled native link must not change location'); + await mkdir(resolve(root,'test-results'),{recursive:true}); + await browserPage.locator('vyp-ha-tabs').screenshot({path:resolve(root,'test-results/packed-navigation-light.png')}); + await browserPage.evaluate(async()=>{ + const element=document.querySelector('vyp-ha-tabs')!; + element.state={kind:'panels',label:'Secciones del hogar',selected:'overview',items:[{id:'overview',label:'Resumen',disabled:false,href:null},{id:'tasks',label:'Tareas',disabled:false,href:null}]}; + const content=document.createElement('button'); content.slot='tasks'; content.textContent='Nueva tarea'; element.append(content); + document.documentElement.lang='es'; + element.style.cssText='--primary-text-color:rgb(235,235,235);--secondary-text-color:rgb(189,189,189);--card-background-color:rgb(20,20,20);--divider-color:rgb(66,66,66);--primary-color:rgb(3,169,244)'; + await element.updateComplete; + }); + await browserPage.getByRole('tab',{name:'Resumen',exact:true}).focus(); + await browserPage.keyboard.press('End'); + assert.equal(await browserPage.getByRole('tab',{name:'Tareas',exact:true}).evaluate(element=>element.matches(':focus-visible')),true); + await browserPage.keyboard.press('Enter'); + assert.equal(await browserPage.getByRole('tabpanel',{name:'Tareas',exact:true}).isVisible(),true); + assert.equal(await browserPage.getByRole('button',{name:'Nueva tarea',exact:true}).isVisible(),true); + await browserPage.locator('vyp-ha-tabs').screenshot({path:resolve(root,'test-results/packed-panels-dark-es.png')}); + assert.deepEqual(errors,[]); + const packedBrowser={engine:'chromium',version:browser.version(),compiledClient:true,navigation:true,panels:true,runtimeSpanishAndTheme:true,reducedMotion:true,consoleErrors:errors}; await mkdir(resolve(root, 'test-results'), {recursive:true}); - await writeFile(resolve(root, 'test-results/packed-contract.json'), JSON.stringify({scope:'Core data contract only; no rendered component, adapter or application adoption',sha256:digest(first),bytes:first.byteLength,reproducible:true,cleanOfflineInstall:true,compiledWithoutDomTypes:true,serverImport:true},null,2)+'\n'); + await writeFile(resolve(root, 'test-results/packed-contract.json'), JSON.stringify({scope:'Core data and navigation component only; no framework adapter or application adoption',sha256:digest(first),bytes:first.byteLength,reproducible:true,cleanOfflineInstall:true,compiledWithoutDomTypes:true,serverImport:true,packedBrowser},null,2)+'\n'); passed = true; } finally { + await browser?.close(); await server?.close(); if (passed) await rm(scratch, {recursive:true}); else console.error('Failed packed-consumer fixture retained at '+scratch); } diff --git a/tests/contracts/registration.test.ts b/tests/contracts/registration.test.ts new file mode 100644 index 0000000..9e81333 --- /dev/null +++ b/tests/contracts/registration.test.ts @@ -0,0 +1,7 @@ +import {expect, test} from 'vitest'; +import {registerTabs} from '../../packages/core/src/components/tabs.js'; +test('does not define a tag or render on the server; registration rejects even a dependency DOM shim', () => { + expect(typeof document).toBe('undefined'); + expect(globalThis.customElements.get('vyp-ha-tabs')).toBeUndefined(); + expect(()=>registerTabs()).toThrow('registerTabs requires a browser CustomElementRegistry'); +}); diff --git a/tsconfig.json b/tsconfig.json index 80f94cd..3eb62f6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,6 +22,7 @@ "tooling/**/*.mjs", "vitest.config.ts", "tests/contracts/**/*.test.ts", - "tests/consumers/**/*.test.ts" + "tests/consumers/**/*.test.ts", + "tests/browser/**/*.test.ts" ] } diff --git a/vitest.config.ts b/vitest.config.ts index 3279d12..892868f 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,6 +1,10 @@ import {defineConfig} from 'vitest/config'; +import {playwright} from '@vitest/browser-playwright'; import {completeCoverage} from './tooling/coverage.mjs'; export default defineConfig({test: { - include: ['tests/**/*.test.{ts,mjs}'], exclude: ['tests/coverage-evidence.test.mjs', 'tests/consumers/packed.test.ts'], + projects: [ + {test: {name:'node', include:['tests/**/*.test.{ts,mjs}'], exclude:['tests/browser/**','tests/coverage-evidence.test.mjs','tests/consumers/packed.test.ts']}}, + {test: {name:'browser', include:['tests/browser/**/*.test.ts'], browser:{enabled:true,provider:playwright(),headless:true,instances:[{browser:'chromium'}]}}}, + ], coverage: completeCoverage(['packages/*/src', 'tooling'], 'coverage'), }}); From 2c8e7ba8a7e39150a4319ef8cb866d01ddc299b4 Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Wed, 4 Nov 2026 05:34:29 +0100 Subject: [PATCH 06/55] fix: freeze packed consumer dependencies without ambient metadata --- docs/compatibility.md | 6 ++++ docs/evidence/shared-ui-foundation.json | 14 ++++++++- tests/consumers/packed.test.ts | 42 +++++++++++++++++++++++-- 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/docs/compatibility.md b/docs/compatibility.md index 5df647d..ec438f6 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -35,3 +35,9 @@ a DOM-free contract and a compiled browser client, and the original pinned Copil The packed browser fixture exercises real links, panel slots, keyboard, live Spanish labels/theme, and reduced motion. It does not establish other browser engines, framework adapters or framework SSR. Packed framework fixtures, Storybook, publication and actual application acceptance remain required. + +The clean package fixture derives its runtime-only graph and integrity records from the reviewed +workspace lock, binds the actual tarball's integrity, and installs offline with a frozen lock. A fresh +metadata cache is selected and verified; the lock must remain byte-identical. A separate unlocked +negative fixture must reproduce the missing-metadata failure. This prevents developer cache contents +from making package acceptance pass when a fresh hosted runner cannot resolve the same dependency. diff --git a/docs/evidence/shared-ui-foundation.json b/docs/evidence/shared-ui-foundation.json index dc93959..eefa0ff 100644 --- a/docs/evidence/shared-ui-foundation.json +++ b/docs/evidence/shared-ui-foundation.json @@ -1,5 +1,5 @@ { - "recordedAtUTC": "2026-11-04T04:26:46.483795+00:00", + "recordedAtUTC": "2026-11-04T04:34:29.482308+00:00", "status": "Partial source increment: data contract and one real navigation/panel component. No framework support, Storybook, published release or app adoption accepted.", "issue": 7, "nativePublication": { @@ -60,6 +60,9 @@ "bytes": 9585, "reproducible": true, "cleanOfflineInstall": true, + "frozenRuntimeLock": true, + "isolatedMetadataCache": true, + "negativeWithoutLockRejected": true, "compiledWithoutDomTypes": true, "serverImport": true, "packedBrowser": { @@ -100,5 +103,14 @@ "imagesInspected": true, "focusRegression": "focus-return-red.log reproduces the stale tab-stop; full green run has no skipped tests.", "initialTestAttempt": "Browser cases were authored before the renderer. The first attempted execution could not start because the pinned Chromium binary was absent; no behavioral red result is claimed for that attempt. After runtime provisioning, the first actual run found two fixture/server-boundary issues, recorded in browser-first-execution.log." + }, + "consumerIsolationRegression": { + "failedHostedRun": "https://github.com/vypdev/ha-ui/actions/runs/41773354203/job/126387929863", + "reason": "Unlocked offline fixture attempted to resolve Lit through absent ambient registry metadata. Product unit/browser coverage and build passed.", + "localRed": "isolated-metadata-red.log reproduces ERR_PNPM_NO_OFFLINE_META with a fresh fixture cache.", + "correction": "Derive the exact runtime dependency graph and integrity records from the reviewed workspace lock; bind the actual tarball integrity, use a frozen offline install and assert the fixture lock remains byte-identical.", + "permanentNegativeControl": "An independent unlocked fixture must reject with ERR_PNPM_NO_OFFLINE_META against the same empty metadata cache.", + "localGreen": "frozen-packed-green.log; strict types, frozen isolated install, DOM-free compilation/import and real packed Chromium journeys pass.", + "freshHostedRequired": true } } diff --git a/tests/consumers/packed.test.ts b/tests/consumers/packed.test.ts index fbe41fc..e448940 100644 --- a/tests/consumers/packed.test.ts +++ b/tests/consumers/packed.test.ts @@ -4,11 +4,12 @@ import {promisify} from 'node:util'; import {execFile} from 'node:child_process'; import {mkdtemp, mkdir, readFile, writeFile, readdir, rm} from 'node:fs/promises'; import {tmpdir} from 'node:os'; -import {resolve, join} from 'node:path'; +import {resolve, join, relative} from 'node:path'; import {fileURLToPath} from 'node:url'; import {createHash} from 'node:crypto'; import {createServer} from 'vite'; import {chromium} from 'playwright'; +import {parse, stringify} from 'yaml'; const execute = promisify(execFile); const root = fileURLToPath(new URL('../../', import.meta.url)); @@ -32,7 +33,42 @@ test('the actual packed package is reproducible and passes clean DOM-free and re assert.deepEqual(first, second, 'Repeated independent builds must produce the same packed bytes'); const consumer = join(scratch, 'consumer'); await mkdir(consumer); await writeFile(join(consumer, 'package.json'), JSON.stringify({name:'packed-contract-fixture', private:true, type:'module', packageManager:'pnpm@10.34.0', dependencies:{'@vypdev/ha-ui':'file:'+archives[0]}})); - await execute('pnpm', ['install', '--offline', '--ignore-scripts'], {cwd: consumer}); + const cache=join(scratch,'empty-metadata-cache'); + const configuration=stringify({packages:[],autoInstallPeers:false,cacheDir:cache}); + await writeFile(join(consumer,'pnpm-workspace.yaml'),configuration); + // A separate negative fixture reproduces the exact hosted failure with no ambient metadata. + const negative=join(scratch,'without-lock'); await mkdir(negative); + await writeFile(join(negative,'package.json'),await readFile(join(consumer,'package.json'))); + await writeFile(join(negative,'pnpm-workspace.yaml'),configuration); + await assert.rejects(execute('pnpm',['install','--offline','--ignore-scripts'],{cwd:negative}),error=>{ + assert.ok(error instanceof Error && 'stdout' in error); + assert.match(String(error.stdout),/ERR_PNPM_NO_OFFLINE_META/u); return true; + }); + const sourceLock=parse(await readFile(resolve(root,'pnpm-lock.yaml'),'utf8')); + const manifest=JSON.parse(await readFile(resolve(root,'packages/core/package.json'),'utf8')); + const direct=sourceLock.importers['packages/core'].dependencies as Record; + const runtime: Record={}; + for (const [name,pin] of Object.entries(direct)) { + assert.equal(pin.specifier,manifest.dependencies[name]); runtime[name]=pin.version; + } + const packages: Record={}, snapshots: Record={}; + const pending=Object.entries(runtime).map(([name,version])=>name+'@'+version); + for (const key of pending) { + if (Object.hasOwn(snapshots,key)) continue; + assert.ok(Object.hasOwn(sourceLock.packages,key) && Object.hasOwn(sourceLock.snapshots,key),'Every runtime node must have a reviewed exact source lock entry: '+key); + packages[key]=sourceLock.packages[key]; snapshots[key]=sourceLock.snapshots[key]; + for (const [name,version] of Object.entries(sourceLock.snapshots[key].dependencies ?? {})) pending.push(name+'@'+String(version)); + } + const fileVersion='file:'+relative(consumer,archives[0]).replaceAll('\\','/'); + const archiveKey=manifest.name+'@'+fileVersion; + packages[archiveKey]={resolution:{integrity:'sha512-'+createHash('sha512').update(first).digest('base64'),tarball:fileVersion},version:manifest.version}; + snapshots[archiveKey]={dependencies:runtime}; + const frozenLock=stringify({lockfileVersion:sourceLock.lockfileVersion,settings:sourceLock.settings,importers:{'.':{dependencies:{[manifest.name]:{specifier:'file:'+archives[0],version:fileVersion}}}},packages,snapshots}); + await writeFile(join(consumer,'pnpm-lock.yaml'),frozenLock); + const cacheConfiguration=await execute('pnpm',['config','get','cache-dir'],{cwd:consumer}); assert.equal(cacheConfiguration.stdout.trim(),cache); + await execute('pnpm', ['install', '--frozen-lockfile', '--offline', '--ignore-scripts'], {cwd: consumer}); + assert.equal(await readFile(join(consumer,'pnpm-lock.yaml'),'utf8'),frozenLock); + await assert.rejects(readFile(join(cache,'metadata-v1.3/registry.npmjs.org/lit.json')),{code:'ENOENT'}); await writeFile(join(consumer, 'contract.ts'), `import {navigationState, type NavigationKind} from '@vypdev/ha-ui/contracts'; const kind: NavigationKind = 'panels'; const selected: string | null = navigationState({kind, label:'Sections', selected:null, items:[]}).selected; @@ -93,7 +129,7 @@ document.querySelector('main')!.append(element);`); assert.deepEqual(errors,[]); const packedBrowser={engine:'chromium',version:browser.version(),compiledClient:true,navigation:true,panels:true,runtimeSpanishAndTheme:true,reducedMotion:true,consoleErrors:errors}; await mkdir(resolve(root, 'test-results'), {recursive:true}); - await writeFile(resolve(root, 'test-results/packed-contract.json'), JSON.stringify({scope:'Core data and navigation component only; no framework adapter or application adoption',sha256:digest(first),bytes:first.byteLength,reproducible:true,cleanOfflineInstall:true,compiledWithoutDomTypes:true,serverImport:true,packedBrowser},null,2)+'\n'); + await writeFile(resolve(root, 'test-results/packed-contract.json'), JSON.stringify({scope:'Core data and navigation component only; no framework adapter or application adoption',sha256:digest(first),bytes:first.byteLength,reproducible:true,cleanOfflineInstall:true,frozenRuntimeLock:true,isolatedMetadataCache:true,negativeWithoutLockRejected:true,compiledWithoutDomTypes:true,serverImport:true,packedBrowser},null,2)+'\n'); passed = true; } finally { await browser?.close(); await server?.close(); From a875c8d378f8a17214cf80bdbb03c2500094ae7f Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Wed, 4 Nov 2026 05:37:00 +0100 Subject: [PATCH 07/55] docs: reflect the implemented navigation preview in current guides --- docs/compatibility.md | 2 +- docs/component-library.md | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/docs/compatibility.md b/docs/compatibility.md index ec438f6..5986a65 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -24,7 +24,7 @@ The controlled navigation model requires kind, label, selected and items. Kind i selected is an existing enabled item ID or null. IDs use 1..128 ASCII letters, digits, underscore or hyphen; accessible labels contain 1..256 Unicode code points without C0, DEL, C1 or unpaired surrogate values. At most 64 items are accepted, without truncation. Navigation items require an explicit HTTP(S) or -relative application link; panel items omit links and use item IDs for future named panel slots. +relative application link; panel items omit links and use item IDs for named panel slots. Disabled defaults to false. Labels and links retain their original values; snapshots are deeply frozen. Invalid/extra fields, conflicting selection and unsafe or credential-bearing links raise the bounded NavigationInputError with code invalid_navigation. This validates data, not authorization or rendering. diff --git a/docs/component-library.md b/docs/component-library.md index d46986c..b350dcf 100644 --- a/docs/component-library.md +++ b/docs/component-library.md @@ -1,8 +1,9 @@ # Shared component library -The first source increment implements the DOM-independent navigation data contract and its -tested quality/build foundation. Rendered components, adapters, Storybook and consumer migrations -are not implemented or published yet. The [primary Draft](../specs/shared-ui-foundation.md), +The current source implements the DOM-independent navigation contract, one controlled upper +navigation/panel renderer and the tested quality/build foundation. The renderer passes direct +Chromium and clean actual-package browser consumers. Other component families, framework adapters, +Storybook, publication and consumer migrations remain required. The [primary Draft](../specs/shared-ui-foundation.md), [capability catalog](../specs/CATALOG.md) and [feature issue #7](https://github.com/vypdev/ha-ui/issues/7) hold the proposed contract and pending acceptance gates. @@ -39,8 +40,10 @@ Next.js, Nuxt and SvelteKit need explicit tested client/import boundaries; a gen claim does not prove SSR or hydration support. The initial goal is a server shell with client-mounted components, not universal server rendering of Shadow DOM. -Before extraction, verify packed consumers for Lit/DOM and all four adapters. Then pin the Gateway -source/license baseline, retain MIT and asset notices, and extract behavior once. Actual Gateway and +The first navigation preview exercises the proposed DOM contract and reviewed Gateway dashboard +geometry. It does not establish broad extraction or a supported framework cohort. Before extending +extraction to additional families, verify the packed consumers for Lit/DOM and all four adapters, +retain the Gateway source/license baseline and MIT/asset notices, and extract behavior once. Actual Gateway and Domus migrations happen under their own admitted repository work, using the same reviewed cohort. Retire local duplicates only after equivalent app, recovery, locale and accessibility journeys pass. Core/adapter publication, Storybook delivery and each app's acceptance are distinct gates. From be482f18ba1d448ed9bc45d5959676c001272286 Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Wed, 4 Nov 2026 06:28:54 +0100 Subject: [PATCH 08/55] feat: deliver navigation bindings for React Vue Angular and Svelte --- .github/workflows/ci_check.yml | 3 +- .gitignore | 2 + README.md | 9 +- docs/compatibility.md | 15 +- docs/component-library.md | 10 +- docs/evidence/shared-ui-foundation.json | 245 +- docs/framework-adapters.md | 85 + docs/provenance.md | 7 + package.json | 25 +- packages/angular/LICENSE | 21 + packages/angular/ng-package.json | 17 + packages/angular/package.json | 32 + packages/angular/src/index.ts | 15 + packages/angular/tsconfig.json | 20 + packages/core/package.json | 3 + packages/react/LICENSE | 21 + packages/react/package.json | 36 + packages/react/src/index.ts | 22 + packages/react/tsconfig.json | 17 + packages/svelte/LICENSE | 21 + packages/svelte/package.json | 38 + packages/svelte/src/Tabs.svelte | 18 + packages/svelte/src/index.ts | 2 + packages/svelte/src/types.ts | 9 + packages/svelte/svelte.config.js | 2 + packages/svelte/tsconfig.json | 13 + packages/vue/LICENSE | 21 + packages/vue/package.json | 36 + packages/vue/src/index.ts | 21 + packages/vue/tsconfig.json | 17 + pnpm-lock.yaml | 2495 ++++++++++++++++- pnpm-workspace.yaml | 1 + specs/CATALOG.md | 10 +- specs/catalog.json | 57 +- specs/shared-ui-framework-delivery.md | 53 +- tests/architecture.test.mjs | 53 + tests/browser/angular.test.ts | 35 + tests/browser/fixtures/SvelteTabs.svelte | 15 + tests/browser/framework-fixture.ts | 5 + tests/browser/react.test.ts | 40 + tests/browser/svelte.test.mjs | 30 + tests/browser/vue.test.ts | 38 + tests/consumers/fixtures/angular/client.ts | 20 + tests/consumers/fixtures/angular/index.html | 1 + tests/consumers/fixtures/angular/model.ts | 5 + tests/consumers/fixtures/angular/package.json | 19 + .../consumers/fixtures/angular/pnpm-lock.yaml | 184 ++ .../fixtures/angular/pnpm-workspace.yaml | 4 + .../consumers/fixtures/angular/tsconfig.json | 26 + tests/consumers/fixtures/react/client.ts | 19 + tests/consumers/fixtures/react/index.html | 1 + tests/consumers/fixtures/react/model.ts | 5 + tests/consumers/fixtures/react/package.json | 17 + tests/consumers/fixtures/react/pnpm-lock.yaml | 149 + .../fixtures/react/pnpm-workspace.yaml | 4 + tests/consumers/fixtures/react/tsconfig.json | 18 + .../consumers/fixtures/svelte/Consumer.svelte | 16 + tests/consumers/fixtures/svelte/client.ts | 4 + tests/consumers/fixtures/svelte/index.html | 1 + tests/consumers/fixtures/svelte/model.ts | 5 + tests/consumers/fixtures/svelte/package.json | 14 + .../consumers/fixtures/svelte/pnpm-lock.yaml | 242 ++ .../fixtures/svelte/pnpm-workspace.yaml | 4 + tests/consumers/fixtures/svelte/tsconfig.json | 19 + tests/consumers/fixtures/vue/client.ts | 17 + tests/consumers/fixtures/vue/index.html | 1 + tests/consumers/fixtures/vue/model.ts | 5 + tests/consumers/fixtures/vue/package.json | 14 + tests/consumers/fixtures/vue/pnpm-lock.yaml | 283 ++ .../fixtures/vue/pnpm-workspace.yaml | 4 + tests/consumers/fixtures/vue/tsconfig.json | 18 + tests/consumers/frameworks.ts | 104 + tests/consumers/packed.test.ts | 23 +- tests/coverage.test.mjs | 2 +- tooling/coverage.mjs | 2 +- tsconfig.consumers.json | 14 + tsconfig.json | 19 +- tsconfig.svelte-source.json | 12 + tsconfig.svelte-tests.json | 11 + vitest.config.ts | 3 +- 80 files changed, 4786 insertions(+), 158 deletions(-) create mode 100644 docs/framework-adapters.md create mode 100644 packages/angular/LICENSE create mode 100644 packages/angular/ng-package.json create mode 100644 packages/angular/package.json create mode 100644 packages/angular/src/index.ts create mode 100644 packages/angular/tsconfig.json create mode 100644 packages/react/LICENSE create mode 100644 packages/react/package.json create mode 100644 packages/react/src/index.ts create mode 100644 packages/react/tsconfig.json create mode 100644 packages/svelte/LICENSE create mode 100644 packages/svelte/package.json create mode 100644 packages/svelte/src/Tabs.svelte create mode 100644 packages/svelte/src/index.ts create mode 100644 packages/svelte/src/types.ts create mode 100644 packages/svelte/svelte.config.js create mode 100644 packages/svelte/tsconfig.json create mode 100644 packages/vue/LICENSE create mode 100644 packages/vue/package.json create mode 100644 packages/vue/src/index.ts create mode 100644 packages/vue/tsconfig.json create mode 100644 tests/browser/angular.test.ts create mode 100644 tests/browser/fixtures/SvelteTabs.svelte create mode 100644 tests/browser/framework-fixture.ts create mode 100644 tests/browser/react.test.ts create mode 100644 tests/browser/svelte.test.mjs create mode 100644 tests/browser/vue.test.ts create mode 100644 tests/consumers/fixtures/angular/client.ts create mode 100644 tests/consumers/fixtures/angular/index.html create mode 100644 tests/consumers/fixtures/angular/model.ts create mode 100644 tests/consumers/fixtures/angular/package.json create mode 100644 tests/consumers/fixtures/angular/pnpm-lock.yaml create mode 100644 tests/consumers/fixtures/angular/pnpm-workspace.yaml create mode 100644 tests/consumers/fixtures/angular/tsconfig.json create mode 100644 tests/consumers/fixtures/react/client.ts create mode 100644 tests/consumers/fixtures/react/index.html create mode 100644 tests/consumers/fixtures/react/model.ts create mode 100644 tests/consumers/fixtures/react/package.json create mode 100644 tests/consumers/fixtures/react/pnpm-lock.yaml create mode 100644 tests/consumers/fixtures/react/pnpm-workspace.yaml create mode 100644 tests/consumers/fixtures/react/tsconfig.json create mode 100644 tests/consumers/fixtures/svelte/Consumer.svelte create mode 100644 tests/consumers/fixtures/svelte/client.ts create mode 100644 tests/consumers/fixtures/svelte/index.html create mode 100644 tests/consumers/fixtures/svelte/model.ts create mode 100644 tests/consumers/fixtures/svelte/package.json create mode 100644 tests/consumers/fixtures/svelte/pnpm-lock.yaml create mode 100644 tests/consumers/fixtures/svelte/pnpm-workspace.yaml create mode 100644 tests/consumers/fixtures/svelte/tsconfig.json create mode 100644 tests/consumers/fixtures/vue/client.ts create mode 100644 tests/consumers/fixtures/vue/index.html create mode 100644 tests/consumers/fixtures/vue/model.ts create mode 100644 tests/consumers/fixtures/vue/package.json create mode 100644 tests/consumers/fixtures/vue/pnpm-lock.yaml create mode 100644 tests/consumers/fixtures/vue/pnpm-workspace.yaml create mode 100644 tests/consumers/fixtures/vue/tsconfig.json create mode 100644 tests/consumers/frameworks.ts create mode 100644 tsconfig.consumers.json create mode 100644 tsconfig.svelte-source.json create mode 100644 tsconfig.svelte-tests.json diff --git a/.github/workflows/ci_check.yml b/.github/workflows/ci_check.yml index f9f9fb0..4720e65 100644 --- a/.github/workflows/ci_check.yml +++ b/.github/workflows/ci_check.yml @@ -54,8 +54,9 @@ jobs: name: shared-ui-validation-${{ github.sha }} path: | coverage/ - packages/core/dist/ + packages/*/dist/ test-results/ .vitest/attachments/ if-no-files-found: error + include-hidden-files: true retention-days: 7 diff --git a/.gitignore b/.gitignore index f4b0911..4097934 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ work/ **/.vite/ .vitest/ + +**/.svelte-kit/ diff --git a/README.md b/README.md index 6fe30c8..43ec5d2 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,10 @@ Shared presentation components for Home Assistant applications, including Gateway and Domus. The current source implements a DOM-independent navigation data contract, one controlled HA-style -upper navigation/panel component and a pinned, tested build/coverage foundation. Other component -families, the React/Vue/Angular/Svelte adapters, -compiled framework consumers, Storybook and publication remain required and unimplemented. +upper navigation/panel component and a pinned, tested build/coverage foundation. React, Vue, Angular +and Svelte navigation bindings now pass real compiled, packed consumers. +Other component families, form bridges, framework SSR boundaries, Storybook, publication and actual +application adoption remain required. No package is published or ready for application adoption. Use Node 24.20.0 and the package manager pinned in package.json: @@ -22,7 +23,7 @@ pnpm test:package The contract validates controlled navigation or panel inputs without reading browser globals, choosing application routes or accepting rendering callbacks. See the [component guide](docs/component-library.md), [compatibility status](docs/compatibility.md), -[source provenance](docs/provenance.md), [foundation](specs/shared-ui-foundation.md), +[framework bindings](docs/framework-adapters.md), [source provenance](docs/provenance.md), [foundation](specs/shared-ui-foundation.md), [delivery companion](specs/shared-ui-framework-delivery.md) and [capability catalog](specs/CATALOG.md). [Issue #7](https://github.com/vypdev/ha-ui/issues/7) retains all 64 baseline cases and six delivery refinements. Clean checks on this increment do not complete that scope. diff --git a/docs/compatibility.md b/docs/compatibility.md index 5986a65..cb8c9f6 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -1,8 +1,8 @@ # Compatibility and validation status The DOM-independent navigation contract, controlled upper navigation/panel component and their -build/coverage foundation are implemented and tested in Chromium. Framework adapters, additional -component families, browser support breadth and framework SSR integration remain unaccepted. +build/coverage foundation are implemented and tested in Chromium. Navigation adapters for four frameworks now pass real compiled package consumers. Other component +families, form bridges, browser support breadth and framework SSR integration remain unaccepted. | Tool | Exact pin | Current use | |---|---|---| @@ -16,8 +16,8 @@ component families, browser support breadth and framework SSR integration remain The core preview coordinate/version is package: @vypdev/ha-ui at 0.1.0-next.1. It is unpublished; its source currently contains presentation contracts and one preview navigation/panel component. It is not a supported component release. -React 19.3.1, Vue 3.5.44, Angular 22.2.4 and Svelte 5.57.3 remain candidate pins until actual packed, -compiled/browser fixtures pass. Next.js, Nuxt and SvelteKit client/SSR boundaries remain unverified. +React 19.3.1, Vue 3.5.44, Angular 22.2.4 and Svelte 5.57.3 pass navigation-only compiled/browser +fixtures using actual packages. They remain preview pins until the complete component and delivery gates pass. Next.js, Nuxt and SvelteKit client/SSR boundaries remain unverified. Do not infer support from custom-element interoperability or compiler metadata alone. The controlled navigation model requires kind, label, selected and items. Kind is navigation or panels; @@ -33,11 +33,14 @@ CI Check runs for each PR head with no path filter. Current gates are strict typ tests, independent source-inventory coverage, architecture/CI contracts, compiled declarations, real Chromium component behavior, actual reproducible packed consumption with a DOM-free contract and a compiled browser client, and the original pinned Copilot catalog validator. The packed browser fixture exercises real links, panel slots, keyboard, live Spanish labels/theme, -and reduced motion. It does not establish other browser engines, framework adapters or framework SSR. -Packed framework fixtures, Storybook, publication and actual application acceptance remain required. +and reduced motion. The additional four framework fixtures establish navigation binding behavior only; other browser +engines, form bridges and framework SSR remain unverified. +Complete component-family/framework fixtures, Storybook, publication and actual app adoption remain required. The clean package fixture derives its runtime-only graph and integrity records from the reviewed workspace lock, binds the actual tarball's integrity, and installs offline with a frozen lock. A fresh metadata cache is selected and verified; the lock must remain byte-identical. A separate unlocked negative fixture must reproduce the missing-metadata failure. This prevents developer cache contents from making package acceptance pass when a fresh hosted runner cannot resolve the same dependency. + +See [framework bindings](framework-adapters.md) for the exact preview API, compilation and teardown evidence. diff --git a/docs/component-library.md b/docs/component-library.md index b350dcf..42adfa0 100644 --- a/docs/component-library.md +++ b/docs/component-library.md @@ -2,8 +2,8 @@ The current source implements the DOM-independent navigation contract, one controlled upper navigation/panel renderer and the tested quality/build foundation. The renderer passes direct -Chromium and clean actual-package browser consumers. Other component families, framework adapters, -Storybook, publication and consumer migrations remain required. The [primary Draft](../specs/shared-ui-foundation.md), +Chromium and clean actual-package browser consumers. Four navigation adapters now also pass real compiled package consumers. Other component families, +form bridges, framework SSR, Storybook, publication and consumer migrations remain required. The [primary Draft](../specs/shared-ui-foundation.md), [capability catalog](../specs/CATALOG.md) and [feature issue #7](https://github.com/vypdev/ha-ui/issues/7) hold the proposed contract and pending acceptance gates. @@ -11,7 +11,8 @@ hold the proposed contract and pending acceptance gates. One Lit implementation exposes DOM components. Apps choose direct Lit/HTML use or a real React, Vue, Angular or Svelte adapter. Proposed package names are @vypdev/ha-ui and the corresponding --react, -vue, -angular and -svelte packages; registry rights and actual exports remain unverified. +-react, -vue, -angular and -svelte packages. Navigation exports and exact preview evidence are in +the [framework guide](framework-adapters.md); registry publication rights remain unverified. A consumer installs only its own framework binding. Framework-native forms, events, complex values, content projection and teardown require compiled tests, not just custom-element declarations. @@ -128,7 +129,8 @@ Public CSS tokens are `--vyp-ha-font-family`, `--vyp-ha-border`, `--vyp-ha-surfa `--vyp-ha-tab-padding`, `--vyp-ha-text-secondary`, `--vyp-ha-text` and `--vyp-ha-primary`. Defaults inherit the corresponding HA theme variables where available. Overrides update live; no global styles, network, storage, timers or animation are introduced. Exposed shadow parts are -`bar`, `item` and `panel`. Framework adapters and packed browser consumers remain separate gates. +`bar`, `item` and `panel`. Navigation binding and packed browser evidence are in the [framework guide](framework-adapters.md); +forms, other families and the complete framework/client boundary matrix remain separate gates. The interaction model follows [WAI-ARIA APG tabs](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/) and uses Lit's documented [reactive properties](https://lit.dev/docs/components/properties/) diff --git a/docs/evidence/shared-ui-foundation.json b/docs/evidence/shared-ui-foundation.json index eefa0ff..e2bd399 100644 --- a/docs/evidence/shared-ui-foundation.json +++ b/docs/evidence/shared-ui-foundation.json @@ -1,6 +1,6 @@ { - "recordedAtUTC": "2026-11-04T04:34:29.482308+00:00", - "status": "Partial source increment: data contract and one real navigation/panel component. No framework support, Storybook, published release or app adoption accepted.", + "recordedAtUTC": "2026-11-04T05:24:06.553322+00:00", + "status": "Partial source increment: one navigation/panel family with four real navigation adapters. No complete component/form/framework SSR/Storybook/publication/application adoption acceptance.", "issue": 7, "nativePublication": { "run": 41767973796, @@ -15,49 +15,56 @@ "allStillRequired": true }, "implemented": [ - "Closed DOM-independent navigation/panel data contract; bounded and immutable inputs, safe links and Unicode C0/DEL/C1 control rejection.", - "Explicit client registration, validated property-only controlled state and typed cancelable composed user intent; no app router or business logic.", - "Real navigation links/current-location semantics and same-root local tab/panel slots, manual keyboard activation, RTL, disabled and stale-event prevention, active-tab focus return.", - "167 passing unit, architecture, CI-contract and real Chromium tests; separate independent coverage audit and clean actual packed-consumer integration pass.", - "Strict four-metric per-module/global coverage including every authored package/tool and the zero-executable barrel.", - "Pinned Node24.20.0/pnpm10.34.0/TypeScript6.0.3/Lit3.3.3/Vitest5.1.0/Playwright1.64.1; every-PR CI installs Chromium and runs all implemented gates plus the unchanged pinned original catalog SDK." + "One controlled framework-neutral navigation/panel implementation with explicit registration, immutable validated data and native typed/cancelable intent.", + "Real React, Vue, Angular and Svelte navigation bindings; runtime imports are their own framework only, with type-only core imports and no alternate visuals.", + "187 passing Node/Chromium contract, architecture, CI and component/binding tests; independent filesystem inventory covers every authored runtime file, including Svelte.", + "Strict source and consumer-tool types plus Svelte package/test-fixture checks; no library checks skipped. Angular APF and Svelte declarations built by official package tools.", + "All five unpublished coordinates independently built/packed twice with identical bytes. Actual installed sources and publication metadata verified.", + "Four real clean frozen offline consumers compile against installed package declarations and pass props/events/content/disabled/EN-ES/themes/keyboard/unmount/remount in Chromium; no foreign framework is installed.", + "Angular actual consumer is AOT compiled and uses the official linker for all partial framework libraries, without a JIT fallback." ], "coverage": { "statements": { - "total": 148, - "covered": 148, + "total": 203, + "covered": 203, "skipped": 0, "pct": 100 }, "branches": { - "total": 139, - "covered": 139, + "total": 141, + "covered": 141, "skipped": 0, "pct": 100 }, "functions": { - "total": 34, - "covered": 34, + "total": 62, + "covered": 62, "skipped": 0, "pct": 100 }, "lines": { - "total": 100, - "covered": 100, + "total": 135, + "covered": 135, "skipped": 0, "pct": 100 } }, "sourceIdentities": { + "packages/angular/src/index.ts": "e17c1826cff3840e8e68d65757a9f628b73fbeff5242e51406307e9f8bc9ab75", "packages/core/src/components/tabs.ts": "5e84a250c9758388d10f4b2e9de55be6d18e57754aaf38d1a0deb71262c58e36", "packages/core/src/contracts.ts": "a786f32769cd69d0653d6292ae8b231b19986a07ad46c998a02d3948c2d5006e", "packages/core/src/index.ts": "10bd6958315d1ae23d216c0b752126e3fc7153f442f8c71e206cfa7e9757f86d", - "tooling/coverage.mjs": "2272a671a4d039fb40866f3535f6cd371b891623b64bf1b9a23a63677ad2387b" + "packages/react/src/index.ts": "8ac4696196e591818a2041d9747fd92c3b60a2b2b3e84f8d7b086938bf81d230", + "packages/svelte/src/Tabs.svelte": "974845c12ec9d760b6f76a43ae89fd4c1618d7b4d56381d98d16f65abc5b27c4", + "packages/svelte/src/index.ts": "9fbc2e899ae344ee2f046b688dd3bbf14a74fdc7c755e3ea0a6cb37d57acc2d6", + "packages/svelte/src/types.ts": "742b73b64fcfd139f293ebab02972b3fbe94bf15b0860d5122af4e141a2aa58a", + "packages/vue/src/index.ts": "90c5273ef2cf67fb6d8bb87f1514afe7322d675ecef55a91c9e94790c79679ae", + "tooling/coverage.mjs": "ad527a5d04517adb1992ac65a9abfffcabb9a2001f7f652c732dbe35899b56c8" }, "packedContract": { "scope": "Core data and navigation component only; no framework adapter or application adoption", - "sha256": "450e1dc97f1056041174f169287893ebcd616cc98bf8bbb907435ebb13179e96", - "bytes": 9585, + "sha256": "bb09f0f83797c895fdd8d425edfcbdac3307e36d7d64b30c96856f2f5f9d01ad", + "bytes": 9609, "reproducible": true, "cleanOfflineInstall": true, "frozenRuntimeLock": true, @@ -77,12 +84,13 @@ } }, "limitations": [ - "Only the navigation/panel family is implemented; forms, dialogs and other presentation families remain required.", - "Clean packed Chromium consumer covers the shared component only; actual React, Vue, Angular, Svelte and SSR/client fixtures remain required.", - "Only the exact recorded Chromium runtime was exercised; Firefox, WebKit and public browser support claims remain pending.", - "Storybook, npm authority/publication, Gateway adoption, Domus adoption and installed human UX acceptance remain unverified.", - "All 64 retained baseline and six delivery-refinement cases remain required; green partial-source tests do not complete issue #7.", - "Fresh hosted CI and complete native review are required for the published source head." + "Only navigation/panels are implemented. Buttons, fields, selects, dialogs, other families and framework-native form bridges remain required.", + "Exact recorded framework pins pass navigation-only compiled consumers. Full cohort support and public peer ranges cannot be inferred from that scope.", + "Next.js, Nuxt and SvelteKit explicit server-shell/client fixtures, hydration behavior and other browser engines remain unverified.", + "All six delivery refinements, accepted-candidate promotion/publication recovery, Storybook, publisher authority and live publication remain required.", + "Gateway and Domus adoption, installed deployment/recovery and human UX acceptance remain unverified.", + "All 64 original baseline cases and six additional delivery cases remain required; no partial increment completes issue #7.", + "Fresh hosted CI and complete native Bugbot review are required for the source head." ], "copilotModified": false, "capabilityComplete": false, @@ -92,13 +100,17 @@ "after": "All 138 unit/architecture/CI-contract cases pass; every C1 code point rejected in group labels, item labels and links; visible U+00A0/U+00A1 preserved.", "manualResolution": false }, - "lockSha256": "3011110ba520f33cca391abf1f7ad5262485159ced72b579b180f9923b0611dc", + "lockSha256": "78ddfdb8c4822b70cbbf48ba31e1c5968b558d99d1c6c6ea1b98da23e8bdbb44", "browserEvidence": { "componentCases": 28, "engine": "Chromium 156.0.8078.4", "actualPackageImages": [ "test-results/packed-navigation-light.png", - "test-results/packed-panels-dark-es.png" + "test-results/packed-panels-dark-es.png", + "test-results/packed-react-dark-es.png", + "test-results/packed-vue-dark-es.png", + "test-results/packed-angular-dark-es.png", + "test-results/packed-svelte-dark-es.png" ], "imagesInspected": true, "focusRegression": "focus-return-red.log reproduces the stale tab-stop; full green run has no skipped tests.", @@ -112,5 +124,184 @@ "permanentNegativeControl": "An independent unlocked fixture must reject with ERR_PNPM_NO_OFFLINE_META against the same empty metadata cache.", "localGreen": "frozen-packed-green.log; strict types, frozen isolated install, DOM-free compilation/import and real packed Chromium journeys pass.", "freshHostedRequired": true + }, + "packedFrameworks": { + "scope": "Navigation adapters only; forms, framework SSR and application adoption remain required", + "frameworks": [ + { + "framework": "react", + "compiled": true, + "frozenOfflineInstall": true, + "noForeignFramework": true, + "candidateArchives": [ + { + "file": "file:../pack-1/vypdev-ha-ui-react-0.1.0-next.1.tgz", + "sha256": "5c64d1378091e38eee17ccf3aa74f246bd6489f587c612a28f073149d3406c89" + }, + { + "file": "file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz", + "sha256": "bb09f0f83797c895fdd8d425edfcbdac3307e36d7d64b30c96856f2f5f9d01ad" + } + ], + "props": true, + "events": true, + "slots": true, + "disabled": true, + "runtimeSpanishAndTheme": true, + "keyboard": true, + "teardownAndRemount": true, + "browser": "156.0.8078.4", + "errors": [] + }, + { + "framework": "vue", + "compiled": true, + "frozenOfflineInstall": true, + "noForeignFramework": true, + "candidateArchives": [ + { + "file": "file:../pack-1/vypdev-ha-ui-vue-0.1.0-next.1.tgz", + "sha256": "e60799e995b06101f12e5c659b85f8f6359cbe1c7c1c6ac3d7834b036d1baa8b" + }, + { + "file": "file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz", + "sha256": "bb09f0f83797c895fdd8d425edfcbdac3307e36d7d64b30c96856f2f5f9d01ad" + } + ], + "props": true, + "events": true, + "slots": true, + "disabled": true, + "runtimeSpanishAndTheme": true, + "keyboard": true, + "teardownAndRemount": true, + "browser": "156.0.8078.4", + "errors": [] + }, + { + "framework": "angular", + "compiled": true, + "frozenOfflineInstall": true, + "noForeignFramework": true, + "candidateArchives": [ + { + "file": "file:../pack-1/vypdev-ha-ui-angular-0.1.0-next.1.tgz", + "sha256": "736fa5858d1c54bb34109f0a2d3b67392c2436bfccbc17ce985c14955ba6ec7a" + }, + { + "file": "file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz", + "sha256": "bb09f0f83797c895fdd8d425edfcbdac3307e36d7d64b30c96856f2f5f9d01ad" + } + ], + "props": true, + "events": true, + "slots": true, + "disabled": true, + "runtimeSpanishAndTheme": true, + "keyboard": true, + "teardownAndRemount": true, + "browser": "156.0.8078.4", + "errors": [] + }, + { + "framework": "svelte", + "compiled": true, + "frozenOfflineInstall": true, + "noForeignFramework": true, + "candidateArchives": [ + { + "file": "file:../pack-1/vypdev-ha-ui-svelte-0.1.0-next.1.tgz", + "sha256": "afdd612a39a017c55a41be38e2d79e2335a749af646d0e62b63b1049feb9ca52" + }, + { + "file": "file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz", + "sha256": "bb09f0f83797c895fdd8d425edfcbdac3307e36d7d64b30c96856f2f5f9d01ad" + } + ], + "props": true, + "events": true, + "slots": true, + "disabled": true, + "runtimeSpanishAndTheme": true, + "keyboard": true, + "teardownAndRemount": true, + "browser": "156.0.8078.4", + "errors": [] + } + ] + }, + "packedCohort": { + "scope": "Unpublished navigation preview only; no registry publication or full capability acceptance", + "cohort": [ + { + "coordinate": "@vypdev/ha-ui", + "version": "0.1.0-next.1", + "sha256": "bb09f0f83797c895fdd8d425edfcbdac3307e36d7d64b30c96856f2f5f9d01ad", + "bytes": 9609 + }, + { + "coordinate": "@vypdev/ha-ui-angular", + "version": "0.1.0-next.1", + "sha256": "736fa5858d1c54bb34109f0a2d3b67392c2436bfccbc17ce985c14955ba6ec7a", + "bytes": 2838 + }, + { + "coordinate": "@vypdev/ha-ui-react", + "version": "0.1.0-next.1", + "sha256": "5c64d1378091e38eee17ccf3aa74f246bd6489f587c612a28f073149d3406c89", + "bytes": 2706 + }, + { + "coordinate": "@vypdev/ha-ui-svelte", + "version": "0.1.0-next.1", + "sha256": "afdd612a39a017c55a41be38e2d79e2335a749af646d0e62b63b1049feb9ca52", + "bytes": 2226 + }, + { + "coordinate": "@vypdev/ha-ui-vue", + "version": "0.1.0-next.1", + "sha256": "e60799e995b06101f12e5c659b85f8f6359cbe1c7c1c6ac3d7834b036d1baa8b", + "bytes": 2931 + } + ], + "reproducible": true, + "sourceLockSha256": "78ddfdb8c4822b70cbbf48ba31e1c5968b558d99d1c6c6ea1b98da23e8bdbb44" + }, + "frameworkPreviewPins": { + "react": "19.3.1", + "reactTypes": "19.3.0", + "vue": "3.5.44", + "angular": "22.2.4", + "svelte": "5.57.3", + "typescript": "6.0.3", + "ngPackagr": "22.2.5", + "sveltePackage": "3.0.2", + "sveltePlugin": "7.3.1", + "svelteCheck": "4.7.8", + "babel": "8.0.1" + }, + "frameworkBoundaries": { + "validated": "Navigation bindings and local controlled panels only", + "forms": false, + "frameworkSSRAndHydration": false, + "otherBrowserEngines": false, + "applicationAdoption": false, + "publication": false, + "storybook": false + }, + "coldSourceValidation": { + "compiledDistributionsPresent": false, + "packages": [ + "core", + "react", + "vue", + "angular", + "svelte" + ], + "types": "All strict source, Node-tool and both Svelte checks passed", + "unitAndBrowserTests": 187, + "coverage": "100% statements/branches/functions/lines per authored file and globally", + "restoration": "Every previous compiled distribution restored byte-for-byte", + "reason": "Source checking must not depend on old local declaration output; Svelte source checks use the source project while package emission retains its separate build project." } } diff --git a/docs/framework-adapters.md b/docs/framework-adapters.md new file mode 100644 index 0000000..f6d35ae --- /dev/null +++ b/docs/framework-adapters.md @@ -0,0 +1,85 @@ +# Framework navigation bindings + +This unpublished preview has one visual implementation and four real navigation adapters. It is +not the completed component library: fields, buttons, dialogs, form bridges, framework SSR/client +fixtures, broader browser support, Storybook, publication and app adoption remain required. + +## Client registration and ownership + +Call `registerTabs` from `@vypdev/ha-ui/tabs` at the application client boundary before mounting the +chosen framework. Importing a binding does not register tags or load the core DOM renderer. The +framework adapters use type-only core imports; their runtime imports are their own framework only. +All use the same `vyp-ha-tabs` element, validated `NavigationState`, CSS tokens and typed native +`ha-selection-request` event. No routing, household state or authorization belongs in a binding. + +The app accepts selection by supplying a new state object. A native event callback may call +`preventDefault()` synchronously; the original event object and its cancellation are preserved. +Programmatic state updates emit no selection request. Node-safe contract imports are supported; +framework server rendering, hydration and raw component-module imports are separate, unaccepted gates. + +| Binding | Validated preview | State/event/content API | +|---|---|---| +| Direct Lit/DOM | Lit 3.3.3 | `state` property; `ha-selection-request`; native named slots | +| React | 19.3.1; types 19.3.0 | `Tabs`, `state`, `onSelectionRequest`; React children with native slot attributes | +| Vue | 3.5.44 | `Tabs`, `state`, typed `selectionRequest` emit; default slot containing native named content | +| Angular | 22.2.4 | Standalone `Tabs` component on the core tag; `[state]`, `(selectionRequest)`, projected content | +| Svelte | 5.57.3 | `Tabs`, `state`, `onSelectionRequest`, `panels` map of Svelte snippets keyed by core item ID | + +Peer versions are exact tested candidates, not inferred broader ranges. Every app installs core +plus its own binding and framework. None of these bindings installs another framework. + +## Projection and lifecycle + +React uses layout effects to apply complex properties and attach/remove a native event subscription. +Strict Mode, rerenders, callback replacement/removal and a retained former host are tested. Supported +host attributes are id, className, style, dir and lang; raw HTML strings are not a binding API. + +Vue uses mounted/unmount hooks, a shallow host ref and a post-render state watch. Its typed emit +preserves the native event. It forwards id, class, style, dir and lang without accepting arbitrary +DOM content setters. Both supplied and absent content/callbacks, prop replacement and teardown pass. + +Angular uses its standard Package Format with partial compilation, published FESM and declarations, +and retained authored source. It is a component on `vyp-ha-tabs` itself, with a projection-only Angular +template. Renderer2 applies properties and owns the native listener cleanup. There is no nested visual +wrapper or second tab renderer. The real consumer compiles its app with ngc and links every partial +Angular dependency using the official linker; it does not enable JIT as an acceptance fallback. +The source dependency on core is an exact cohort version so the standalone APF manifest is publishable; +pnpm links that version to the local workspace during development. + +Svelte uses runes, onMount cleanup and explicit snippet-to-slot projection. For example: + +```svelte +{#snippet tasks()}{/snippet} + +``` + +Svelte's legacy `slot` syntax on a child of a Svelte component is not this API. The snippet map +provides native named slots without letting the compiler silently treat them as legacy slots. +Absent panels and callbacks, prop replacement, snippet removal, unmount and remount all pass. +The published Svelte component is preprocessed and carries generated component declarations. + +## Verification boundaries + +`pnpm check` strictly checks source, Node consumer tooling, the Svelte package and Svelte test fixtures. +The consumer tooling uses bundler module resolution for the Angular linker's published declaration +contract; production TypeScript package declarations retain their own resolution settings. No library +declaration checks are skipped. `pnpm test` executes real Chromium bindings and enforces all four +coverage metrics per authored module, including Svelte source and zero-executable type/barrel files. + +`pnpm test:package` independently builds and packs all five coordinates twice and compares bytes. +Each actual package installs in a clean, frozen offline fixture with an isolated metadata cache. +Fixture lock registry records must equal the reviewed source lock; only the new unpublished candidate's +local archive integrity is bound for that test campaign. Fixture-only overrides point core dependencies +to the same candidate bytes. They are not publication metadata or installed application dependencies. + +Every framework consumer compiles against installed declarations and runs real props, single native +events, content projection, disabled booleans, English/Spanish, live theme, keyboard, removal and remount +journeys. Installed authored source must match the repository. Published manifests contain neither +workspace nor file references. The resulting artifacts include the exact cohort digests and original +browser screenshots. These checks cover navigation only and do not complete the retained 70-case +capability acceptance inventory or prove a real npm publication. + +The complete source/type/browser check sequence also passes with all five dist trees absent. +Svelte source validation uses `tsconfig.svelte-source.json`; Svelte package emission keeps its own +compiler project and follows the core build in pnpm's dependency graph. This prevents a local +declaration cache from hiding a fresh-checkout CI failure. diff --git a/docs/provenance.md b/docs/provenance.md index 44dd786..143682c 100644 --- a/docs/provenance.md +++ b/docs/provenance.md @@ -22,3 +22,10 @@ validation directory and calls the public main function with this repository's e It does not change Copilot source or installed resources. Source hashes and the local procedure are in the [component guide](component-library.md#catalog-validation). Catalog consistency, implemented coverage, actual framework execution, publication rights and application adoption are separate facts. + +The four navigation adapters are authored in this repository and carry the same MIT notice. +They add framework lifecycle/property/event/content bindings; none contains visual styles or a +second navigation controller. Angular packaging retains its authored source and uses standard FESM, +partial declarations and an exact tslib dependency. React/Vue include their source/declaration maps; +Svelte includes source, preprocessed components and generated declarations. Third-party compilers, +frameworks and helpers retain their own package licenses; no third-party runtime source is copied here. diff --git a/package.json b/package.json index 1374475..930503a 100644 --- a/package.json +++ b/package.json @@ -7,19 +7,38 @@ "node": ">=24.20.0 <25" }, "scripts": { - "check": "tsc --noEmit -p tsconfig.json", + "check": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.consumers.json && svelte-check --tsconfig tsconfig.svelte-source.json && svelte-check --tsconfig tsconfig.svelte-tests.json", "test": "vitest run --coverage && node --test tests/coverage-evidence.test.mjs", - "build": "tsc -p packages/core/tsconfig.json", + "build": "pnpm -r --sort run build", "test:package": "node --test tests/consumers/packed.test.ts" }, "devDependencies": { + "@angular/common": "22.2.4", + "@angular/compiler": "22.2.4", + "@angular/compiler-cli": "22.2.4", + "@angular/core": "22.2.4", + "@angular/platform-browser": "22.2.4", + "@babel/core": "8.0.1", + "@sveltejs/package": "3.0.2", + "@sveltejs/vite-plugin-svelte": "7.3.1", "@types/node": "24.19.2", + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", "@vitest/browser-playwright": "5.1.0", "@vitest/coverage-v8": "5.1.0", + "ng-packagr": "22.2.5", "playwright": "1.64.1", + "react": "19.3.1", + "react-dom": "19.3.1", + "rxjs": "7.8.2", + "svelte": "5.57.3", + "svelte-check": "4.7.8", + "tslib": "2.8.1", "typescript": "6.0.3", "vite": "8.3.5", "vitest": "5.1.0", - "yaml": "2.9.0" + "vue": "3.5.44", + "yaml": "2.9.0", + "@types/convert-source-map": "2.0.3" } } diff --git a/packages/angular/LICENSE b/packages/angular/LICENSE new file mode 100644 index 0000000..31822ab --- /dev/null +++ b/packages/angular/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 vypdev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/angular/ng-package.json b/packages/angular/ng-package.json new file mode 100644 index 0000000..1395efd --- /dev/null +++ b/packages/angular/ng-package.json @@ -0,0 +1,17 @@ +{ + "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "dist", + "lib": { + "entryFile": "src/index.ts" + }, + "allowedNonPeerDependencies": [ + "@vypdev/ha-ui" + ], + "assets": [ + { + "glob": "**/*.ts", + "input": "src", + "output": "src" + } + ] +} diff --git a/packages/angular/package.json b/packages/angular/package.json new file mode 100644 index 0000000..0579c29 --- /dev/null +++ b/packages/angular/package.json @@ -0,0 +1,32 @@ +{ + "name": "@vypdev/ha-ui-angular", + "version": "0.1.0-next.1", + "description": "Angular bindings for the shared Home Assistant UI components", + "type": "module", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/vypdev/ha-ui.git", + "directory": "packages/angular" + }, + "files": [ + "fesm2022", + "types", + "src", + "LICENSE" + ], + "sideEffects": false, + "dependencies": { + "@vypdev/ha-ui": "0.1.0-next.1", + "tslib": "2.8.1" + }, + "peerDependencies": { + "@angular/core": "22.2.4" + }, + "publishConfig": { + "access": "public" + }, + "scripts": { + "build": "ng-packagr -p ng-package.json -c tsconfig.json" + } +} diff --git a/packages/angular/src/index.ts b/packages/angular/src/index.ts new file mode 100644 index 0000000..8f90a5a --- /dev/null +++ b/packages/angular/src/index.ts @@ -0,0 +1,15 @@ +import {Component,ElementRef,EventEmitter,Input,Output,Renderer2,inject,type OnChanges,type OnDestroy} from '@angular/core'; +import type {NavigationState} from '@vypdev/ha-ui/contracts'; +import type {HaTabs,NavigationSelectionEvent} from '@vypdev/ha-ui/tabs'; + +/** Angular owns the light DOM; the registered core host alone owns its visual Shadow DOM. */ +@Component({selector:'vyp-ha-tabs',standalone:true,template:''}) +export class Tabs implements OnChanges,OnDestroy { + @Input({required:true}) state!:NavigationState; + @Output() readonly selectionRequest=new EventEmitter(false); + readonly #host=inject>(ElementRef).nativeElement; + readonly #renderer=inject(Renderer2); + readonly #stop=this.#renderer.listen(this.#host,'ha-selection-request',(event:NavigationSelectionEvent)=>this.selectionRequest.emit(event)); + ngOnChanges():void {this.#renderer.setProperty(this.#host,'state',this.state);} + ngOnDestroy():void {this.#stop();} +} diff --git a/packages/angular/tsconfig.json b/packages/angular/tsconfig.json new file mode 100644 index 0000000..d2bfbb5 --- /dev/null +++ b/packages/angular/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "noEmit": false, + "paths": {}, + "useDefineForClassFields": false, + "declaration": true, + "declarationMap": false, + "sourceMap": true, + "inlineSources": true + }, + "angularCompilerOptions": { + "compilationMode": "partial", + "strictTemplates": true + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [] +} diff --git a/packages/core/package.json b/packages/core/package.json index cb417ef..adfe977 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -34,5 +34,8 @@ }, "dependencies": { "lit": "3.3.3" + }, + "scripts": { + "build": "tsc -p tsconfig.json" } } diff --git a/packages/react/LICENSE b/packages/react/LICENSE new file mode 100644 index 0000000..31822ab --- /dev/null +++ b/packages/react/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 vypdev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/react/package.json b/packages/react/package.json new file mode 100644 index 0000000..fed43db --- /dev/null +++ b/packages/react/package.json @@ -0,0 +1,36 @@ +{ + "name": "@vypdev/ha-ui-react", + "version": "0.1.0-next.1", + "description": "React bindings for the shared Home Assistant UI components", + "type": "module", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/vypdev/ha-ui.git", + "directory": "packages/react" + }, + "files": [ + "dist", + "src", + "LICENSE" + ], + "sideEffects": false, + "dependencies": { + "@vypdev/ha-ui": "workspace:*" + }, + "peerDependencies": { + "react": "19.3.1" + }, + "publishConfig": { + "access": "public" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "tsc -p tsconfig.json" + } +} diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts new file mode 100644 index 0000000..53e2d11 --- /dev/null +++ b/packages/react/src/index.ts @@ -0,0 +1,22 @@ +'use client'; +import {createElement, useLayoutEffect, useRef, type HTMLAttributes, type ReactNode} from 'react'; +import type {NavigationState} from '@vypdev/ha-ui/contracts'; +import type {HaTabs, NavigationSelectionEvent} from '@vypdev/ha-ui/tabs'; + +export interface TabsProps extends Pick,'id'|'className'|'style'|'dir'|'lang'> { + state: NavigationState; + onSelectionRequest?: (event:NavigationSelectionEvent)=>void; + children?: ReactNode; +} +/** Only React bindings; the app explicitly registers the shared element at its client boundary. */ +export function Tabs({state,onSelectionRequest,children,id,className,style,dir,lang}:TabsProps) { + const element=useRef(null); + useLayoutEffect(()=>{element.current!.state=state;},[state]); + useLayoutEffect(()=>{ + const host=element.current!; + const handle=(event:NavigationSelectionEvent)=>onSelectionRequest?.(event); + host.addEventListener('ha-selection-request',handle); + return ()=>host.removeEventListener('ha-selection-request',handle); + },[onSelectionRequest]); + return createElement('vyp-ha-tabs',{ref:element,id,className,style,dir,lang},children); +} diff --git a/packages/react/tsconfig.json b/packages/react/tsconfig.json new file mode 100644 index 0000000..30f4a7f --- /dev/null +++ b/packages/react/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "noEmit": false, + "paths": {}, + "rootDir": "src", + "outDir": "dist", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "inlineSources": true + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [] +} diff --git a/packages/svelte/LICENSE b/packages/svelte/LICENSE new file mode 100644 index 0000000..31822ab --- /dev/null +++ b/packages/svelte/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 vypdev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/svelte/package.json b/packages/svelte/package.json new file mode 100644 index 0000000..d646ad8 --- /dev/null +++ b/packages/svelte/package.json @@ -0,0 +1,38 @@ +{ + "name": "@vypdev/ha-ui-svelte", + "version": "0.1.0-next.1", + "description": "Svelte bindings for the shared Home Assistant UI components", + "type": "module", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/vypdev/ha-ui.git", + "directory": "packages/svelte" + }, + "files": [ + "dist", + "src", + "LICENSE" + ], + "sideEffects": false, + "dependencies": { + "@vypdev/ha-ui": "workspace:*" + }, + "peerDependencies": { + "svelte": "5.57.3" + }, + "publishConfig": { + "access": "public" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "svelte": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "svelte": "./dist/index.js", + "scripts": { + "build": "svelte-package -i src -o dist --tsconfig tsconfig.json" + } +} diff --git a/packages/svelte/src/Tabs.svelte b/packages/svelte/src/Tabs.svelte new file mode 100644 index 0000000..a4c2e38 --- /dev/null +++ b/packages/svelte/src/Tabs.svelte @@ -0,0 +1,18 @@ + + + {#each Object.entries(panels ?? {}) as [id,content] (id)} +
{@render content()}
+ {/each} +
diff --git a/packages/svelte/src/index.ts b/packages/svelte/src/index.ts new file mode 100644 index 0000000..0fb0353 --- /dev/null +++ b/packages/svelte/src/index.ts @@ -0,0 +1,2 @@ +export {default as Tabs} from './Tabs.svelte'; +export type {TabsProps} from './types.js'; diff --git a/packages/svelte/src/types.ts b/packages/svelte/src/types.ts new file mode 100644 index 0000000..bfc631c --- /dev/null +++ b/packages/svelte/src/types.ts @@ -0,0 +1,9 @@ +import type {Snippet} from 'svelte'; +import type {HTMLAttributes} from 'svelte/elements'; +import type {NavigationState} from '@vypdev/ha-ui/contracts'; +import type {NavigationSelectionEvent} from '@vypdev/ha-ui/tabs'; +export interface TabsProps extends Pick,'id'|'class'|'style'|'lang'|'dir'> { + state:NavigationState; + onSelectionRequest?:(event:NavigationSelectionEvent)=>void; + panels?:Readonly>; +} diff --git a/packages/svelte/svelte.config.js b/packages/svelte/svelte.config.js new file mode 100644 index 0000000..5a5c9d1 --- /dev/null +++ b/packages/svelte/svelte.config.js @@ -0,0 +1,2 @@ +import {vitePreprocess} from '@sveltejs/vite-plugin-svelte'; +export default {preprocess: vitePreprocess({script:true})}; diff --git a/packages/svelte/tsconfig.json b/packages/svelte/tsconfig.json new file mode 100644 index 0000000..1e60107 --- /dev/null +++ b/packages/svelte/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "paths": {}, + "module": "ESNext", + "moduleResolution": "Bundler" + }, + "include": [ + "src/**/*.ts", + "src/**/*.svelte" + ], + "exclude": [] +} diff --git a/packages/vue/LICENSE b/packages/vue/LICENSE new file mode 100644 index 0000000..31822ab --- /dev/null +++ b/packages/vue/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 vypdev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/vue/package.json b/packages/vue/package.json new file mode 100644 index 0000000..078330e --- /dev/null +++ b/packages/vue/package.json @@ -0,0 +1,36 @@ +{ + "name": "@vypdev/ha-ui-vue", + "version": "0.1.0-next.1", + "description": "Vue bindings for the shared Home Assistant UI components", + "type": "module", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/vypdev/ha-ui.git", + "directory": "packages/vue" + }, + "files": [ + "dist", + "src", + "LICENSE" + ], + "sideEffects": false, + "dependencies": { + "@vypdev/ha-ui": "workspace:*" + }, + "peerDependencies": { + "vue": "3.5.44" + }, + "publishConfig": { + "access": "public" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "tsc -p tsconfig.json" + } +} diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts new file mode 100644 index 0000000..5f02160 --- /dev/null +++ b/packages/vue/src/index.ts @@ -0,0 +1,21 @@ +import {defineComponent,h,onMounted,onBeforeUnmount,shallowRef,watch,type PropType} from 'vue'; +import type {NavigationState} from '@vypdev/ha-ui/contracts'; +import type {HaTabs,NavigationSelectionEvent} from '@vypdev/ha-ui/tabs'; + +/** Vue owns props, slots and subscriptions; all visuals remain in the explicitly registered core. */ +export const Tabs=defineComponent({ + name:'HaUiTabs', inheritAttrs:false, + props:{state:{type:Object as PropType,required:true}}, + emits:{selectionRequest:(_event:NavigationSelectionEvent)=>true}, + setup(props,{attrs,slots,emit}) { + const element=shallowRef(null); + const handle=(event:NavigationSelectionEvent)=>emit('selectionRequest',event); + onMounted(()=>{ + element.value!.state=props.state; + element.value!.addEventListener('ha-selection-request',handle); + }); + watch(()=>props.state,state=>{element.value!.state=state;},{flush:'post'}); + onBeforeUnmount(()=>{element.value!.removeEventListener('ha-selection-request',handle);}); + return ()=>h('vyp-ha-tabs',{ref:element,id:attrs.id,class:attrs.class,style:attrs.style,dir:attrs.dir,lang:attrs.lang},slots.default?.()); + }, +}); diff --git a/packages/vue/tsconfig.json b/packages/vue/tsconfig.json new file mode 100644 index 0000000..30f4a7f --- /dev/null +++ b/packages/vue/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "noEmit": false, + "paths": {}, + "rootDir": "src", + "outDir": "dist", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "inlineSources": true + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e227b12..42a64dd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,56 +8,247 @@ importers: .: devDependencies: + '@angular/common': + specifier: 22.2.4 + version: 22.2.4(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))(rxjs@7.8.2) + '@angular/compiler': + specifier: 22.2.4 + version: 22.2.4 + '@angular/compiler-cli': + specifier: 22.2.4 + version: 22.2.4(@angular/compiler@22.2.4)(typescript@6.0.3) + '@angular/core': + specifier: 22.2.4 + version: 22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2) + '@angular/platform-browser': + specifier: 22.2.4 + version: 22.2.4(@angular/common@22.2.4(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2)) + '@babel/core': + specifier: 8.0.1 + version: 8.0.1 + '@sveltejs/package': + specifier: 3.0.2 + version: 3.0.2(svelte@5.57.3)(typescript@6.0.3) + '@sveltejs/vite-plugin-svelte': + specifier: 7.3.1 + version: 7.3.1(svelte@5.57.3)(vite@8.3.5(@types/node@24.19.2)(esbuild@0.28.2)(less@4.9.1)(sass-embedded@1.107.1)(sass@1.107.1)(yaml@2.9.0)) + '@types/convert-source-map': + specifier: 2.0.3 + version: 2.0.3 '@types/node': specifier: 24.19.2 version: 24.19.2 + '@types/react': + specifier: 19.3.0 + version: 19.3.0 + '@types/react-dom': + specifier: 19.3.0 + version: 19.3.0(@types/react@19.3.0) '@vitest/browser-playwright': specifier: 5.1.0 - version: 5.1.0(playwright@1.64.1)(vite@8.3.5(@types/node@24.19.2)(yaml@2.9.0))(vitest@5.1.0) + version: 5.1.0(playwright@1.64.1)(vite@8.3.5(@types/node@24.19.2)(esbuild@0.28.2)(less@4.9.1)(sass-embedded@1.107.1)(sass@1.107.1)(yaml@2.9.0))(vitest@5.1.0) '@vitest/coverage-v8': specifier: 5.1.0 version: 5.1.0(@vitest/browser@5.1.0)(vitest@5.1.0) + ng-packagr: + specifier: 22.2.5 + version: 22.2.5(@angular/compiler-cli@22.2.4(@angular/compiler@22.2.4)(typescript@6.0.3))(tslib@2.8.1)(typescript@6.0.3) playwright: specifier: 1.64.1 version: 1.64.1 + react: + specifier: 19.3.1 + version: 19.3.1 + react-dom: + specifier: 19.3.1 + version: 19.3.1(react@19.3.1) + rxjs: + specifier: 7.8.2 + version: 7.8.2 + svelte: + specifier: 5.57.3 + version: 5.57.3 + svelte-check: + specifier: 4.7.8 + version: 4.7.8(picomatch@4.0.7)(svelte@5.57.3)(typescript@6.0.3) + tslib: + specifier: 2.8.1 + version: 2.8.1 typescript: specifier: 6.0.3 version: 6.0.3 vite: specifier: 8.3.5 - version: 8.3.5(@types/node@24.19.2)(yaml@2.9.0) + version: 8.3.5(@types/node@24.19.2)(esbuild@0.28.2)(less@4.9.1)(sass-embedded@1.107.1)(sass@1.107.1)(yaml@2.9.0) vitest: specifier: 5.1.0 - version: 5.1.0(@types/node@24.19.2)(@vitest/browser-playwright@5.1.0)(@vitest/coverage-v8@5.1.0)(@vitest/ui@5.1.0)(vite@8.3.5(@types/node@24.19.2)(yaml@2.9.0)) + version: 5.1.0(@types/node@24.19.2)(@vitest/browser-playwright@5.1.0)(@vitest/coverage-v8@5.1.0)(@vitest/ui@5.1.0)(vite@8.3.5(@types/node@24.19.2)(esbuild@0.28.2)(less@4.9.1)(sass-embedded@1.107.1)(sass@1.107.1)(yaml@2.9.0)) + vue: + specifier: 3.5.44 + version: 3.5.44(typescript@6.0.3) yaml: specifier: 2.9.0 version: 2.9.0 + packages/angular: + dependencies: + '@vypdev/ha-ui': + specifier: 0.1.0-next.1 + version: link:../core + tslib: + specifier: 2.8.1 + version: 2.8.1 + packages/core: dependencies: lit: specifier: 3.3.3 version: 3.3.3 + packages/react: + dependencies: + '@vypdev/ha-ui': + specifier: workspace:* + version: link:../core + + packages/svelte: + dependencies: + '@vypdev/ha-ui': + specifier: workspace:* + version: link:../core + + packages/vue: + dependencies: + '@vypdev/ha-ui': + specifier: workspace:* + version: link:../core + packages: + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@angular/common@22.2.4': + resolution: {integrity: sha512-i5oosToLSAXumbkOF5pZyti0W0jrfRppj4y6a913f+AY4Em+iHT87JIOKrfybR7vkHs0eceZnpS0CpzylVAvgA==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} + peerDependencies: + '@angular/core': 22.2.4 + rxjs: ^6.5.3 || ^7.4.0 + + '@angular/compiler-cli@22.2.4': + resolution: {integrity: sha512-Ow6AlIdmidjFxa3v4+uZrbPVSc45jLMUO9ERdFp69z6fv5CP9jd4DhK+wKVCvIlCp5ygelia8admEypdY3p5Hw==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} + hasBin: true + peerDependencies: + '@angular/compiler': 22.2.4 + typescript: '>=6.0 <6.1' + peerDependenciesMeta: + typescript: + optional: true + + '@angular/compiler@22.2.4': + resolution: {integrity: sha512-Az14b7hE1iCMqNhskOjA28+8w3VQDhSkrVUU+2gyucDHey1qw9rX5x45/z3qNanz4VtTBShvqEDsjctkobKJSA==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} + + '@angular/core@22.2.4': + resolution: {integrity: sha512-FME4hPQ6+RbOkaRtUHWytwyAFXZYjIQ81TDJ3Dj7tx+vzKs0StpH9g/0AoZwQUDmhPlN84IazBimQ7II7lmwVw==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} + peerDependencies: + '@angular/compiler': 22.2.4 + rxjs: ^6.5.3 || ^7.4.0 + zone.js: ~0.15.0 || ~0.16.0 + peerDependenciesMeta: + '@angular/compiler': + optional: true + zone.js: + optional: true + + '@angular/platform-browser@22.2.4': + resolution: {integrity: sha512-NAQ8/Zcl8nRlNqxlXQGrQJ/c5V8lN5vJSlc8qkD3OUN+ClyNuS6UCXffYtKvvbovpyb/q1uYSQjqiEctz+5niw==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} + peerDependencies: + '@angular/animations': 22.2.4 + '@angular/common': 22.2.4 + '@angular/core': 22.2.4 + peerDependenciesMeta: + '@angular/animations': + optional: true + + '@babel/code-frame@8.0.6': + resolution: {integrity: sha512-uMHhvmDmwFiLuiozaoRYTZHqMFuu3fem/X9JY78lohAuLoh8LSGGC8JqUzo2cVuBsBxkjMZNk56CE7ock8Xxkw==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/compat-data@8.0.5': + resolution: {integrity: sha512-YLsYoQMvL8l8WrGpN3Zj7O1wK5LEBN+cQtux7BcuHyxIXve724XG+zuJ1n3U1cUweRtTzQOA4IHbuQw3N34SZw==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/core@8.0.1': + resolution: {integrity: sha512-5FgxM4dLQpMJHSiVATk8foW263dVHQHBVpXYiimNECVWG01f4nFyEbQixeT6Mwvg7TayREJ2gpKl3o2RoMdnqw==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/generator@8.1.0': + resolution: {integrity: sha512-7fiBJ61wDV3AHbg7n/EfLBv3RU+lsdRP8QNJnydkKWA7e48FnnvKQWbVHsJQCBE7JHTj4xlzZL9fvQYsBUip2A==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/helper-compilation-targets@8.0.7': + resolution: {integrity: sha512-ilAZCdOOlOVA7dQbaFNw2f/59p6GW33NxTopc3DvEE3efp3ZXTrGX/yZScaKw8/vd9bq31Ag0EcXiLw32bAgJg==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/helper-globals@8.0.6': + resolution: {integrity: sha512-OdnA/N52LhzFpeUuVz5YGuSWjc7TPHiFmVYZbPlSgExocO2GwbxVpSni9MVs5wwW+wZ4kPuwBEBN9pIPDYz1mA==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/helper-string-parser@7.29.11': resolution: {integrity: sha512-snZbO7MEDOof+hvNa97EjEY7qD7h2kwvQr5AJC3ZhQudKW5dnej6k3kP9NQV88hJwaS8vDqRLFo9rivLEhle2g==} engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@8.0.8': + resolution: {integrity: sha512-fVM+gSuKbdv9cpmyRNh5EvsDHUZCdxQH3FdnFKSkNYHpbsn95bbrxm+ES3MVeQjcqwi2O5ur/DjLfmTQaO60ng==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/helper-validator-identifier@7.29.7': resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@8.0.6': + resolution: {integrity: sha512-HYx0YmuchRxRSpnjvcNffnPS8DCauKNmnumIuHG3jnQAVewRA7kfJGwF13ayEpSZs7s4R/oBH4eppuXcAQd5JQ==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/helper-validator-option@8.0.0': + resolution: {integrity: sha512-U4Dybxh4WESWHt5XhBeExi4DrY0/DNK1aHpQbsrQXCUbFHuMweT0TpLEWKvaraV2Y6fS+ZXunsZ8zIuZIgvF2Q==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/helpers@8.1.0': + resolution: {integrity: sha512-949bjw10dhaRdtQeCpfVShFA4jIcto97BkqmWnRjpzyQuHVZ7EDriaoCn/4D8h1NqSKlmI20xPU0vHf3jQbQRg==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/parser@7.29.12': resolution: {integrity: sha512-WJ7JBp/Q4+z/ImydtI7jS6NVco5++632ExBemSX+Ysb+G1lCwQ5BVo3SY+jGGOcZXI4ZccbZSUSJLNBW6eEpJg==} engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@8.1.0': + resolution: {integrity: sha512-fpqZYAC+Tthr19JbEKxXhCGutvfm/+roMe+ULrgUKI+GVyzkQiTMTSheORcWIAzW8T3Oo3W5UavWSvAWmsp7fA==} + engines: {node: ^22.18.0 || >=24.11.0} + hasBin: true + + '@babel/template@8.0.0': + resolution: {integrity: sha512-eAD0QW/AlbamBbw0FeGiwasbCVPq5ncW0HNVyLP3B9czqLyh4gvw+5JTSNt6le9+ziAU7mqDZsKTHf3jTb4chQ==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/traverse@8.1.0': + resolution: {integrity: sha512-oQ0QI6MR4cm6bOR/Ua5HLPxvO7vborT7Le5HNt9GmZUKpNsaPl2yj4gsQ440lXaZjc4vmUbSeVgEcK4cHuCL2A==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/types@7.29.11': resolution: {integrity: sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==} engines: {node: '>=6.9.0'} + '@babel/types@8.1.0': + resolution: {integrity: sha512-8V8aSTDx012YPmDQF30vrokyxVFdRAU2YBS2VzrnfLAGkP8floVnjckwgFcD0Jrvf6GWMSRdCrYVsk3tyMwZRg==} + engines: {node: ^22.18.0 || >=24.11.0} + '@bcoe/v8-coverage@1.0.2': resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} @@ -65,6 +256,174 @@ packages: '@blazediff/core@1.10.0': resolution: {integrity: sha512-AOQff0zgR7cGsZL+4E7hVkmujoPUpm0J9xzWGWZj5wCjd3gmxESXAPfKyuzs93VdpQNFhHlBhfOjrcZ+XTERtQ==} + '@bufbuild/protobuf@2.16.0': + resolution: {integrity: sha512-FWa0sPlqYGJgpTs6OxBRcL/AW4JT0OIO+W1ajerluoTVh8CV0B7XM1qsNCRgnkSCkRCgOICKHRQlY17jrsm0+Q==} + + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/gen-mapping@0.4.0-beta.0': + resolution: {integrity: sha512-JdGNkbE4GlNPYQhM0L95fBQr7ctLZJ276QXQLTad4t1oSdnnCI3fDq9DW3BqYAWv8Wc3+HS+4Gsii1oPMCfz1w==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -84,6 +443,88 @@ packages: '@oxc-project/types@0.156.0': resolution: {integrity: sha512-1Duyg48TdJNdyhEB8Xn1L36bT9Pv6GUtlKjHxOys8xkaF/rQq3XJE+bAurSK/AgIz5mGFsb77cn9Pb20+WCScw==} + '@parcel/watcher-android-arm64@2.6.0': + resolution: {integrity: sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.6.0': + resolution: {integrity: sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.6.0': + resolution: {integrity: sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.6.0': + resolution: {integrity: sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.6.0': + resolution: {integrity: sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm-musl@2.6.0': + resolution: {integrity: sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-arm64-glibc@2.6.0': + resolution: {integrity: sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm64-musl@2.6.0': + resolution: {integrity: sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-x64-glibc@2.6.0': + resolution: {integrity: sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-x64-musl@2.6.0': + resolution: {integrity: sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@parcel/watcher-win32-arm64@2.6.0': + resolution: {integrity: sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-x64@2.6.0': + resolution: {integrity: sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.6.0': + resolution: {integrity: sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==} + engines: {node: '>= 10.0.0'} + '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} @@ -186,18 +627,62 @@ packages: '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@sveltejs/acorn-typescript@1.0.13': + resolution: {integrity: sha512-wgKggnhZVL9Bfx1OaKKTrYY9BFRk6C8UAkQNUcIv1+llzYrIqy+RZm5HPKzn0NpEBvTVhTqB4kQyllZywsRBRQ==} + peerDependencies: + acorn: ^8.9.0 + + '@sveltejs/load-config@0.2.3': + resolution: {integrity: sha512-VT3qmUb8pRV2QrZjd8iAmtg8lf4W0TIjZbvXtz5MKei/q96teWZgGJyyidJzOjzZzvdq616eSRVeMYIQChUTAQ==} + engines: {node: '>= 18.0.0'} + + '@sveltejs/package@3.0.2': + resolution: {integrity: sha512-1HhN/Lgw1Bqyu7T6fWILt6euTyt+h+yBH2yUVYxTkGVUdIV1Xsp/gGaHnfBkD584i1fXte1cR4+zYtUsQC/F6A==} + engines: {node: '>=22'} + hasBin: true + peerDependencies: + svelte: ^3.44.0 || ^4.0.0 || ^5.0.0-next.1 + typescript: ^6.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@sveltejs/vite-plugin-svelte@7.3.1': + resolution: {integrity: sha512-ZPsLN8B1e/En+Ak5s4V7srFDT532oS0qieLsQwu63NGKsS+iAjoO2Js1BochlHlglcU+Pt7WAO3C5Ee+4f6gVA==} + engines: {node: ^20.19 || ^22.12 || >=24} + peerDependencies: + svelte: ^5.46.4 + vite: ^8.0.0-beta.7 || ^8.0.0 + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/convert-source-map@2.0.3': + resolution: {integrity: sha512-ag0BfJLZf6CQz8VIuRIEYQ5Ggwk/82uvTQf27RcpyDNbY0Vw49LIPqAxk5tqYfrCs9xDaIMvl4aj7ZopnYL8bA==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} '@types/estree@1.0.10': resolution: {integrity: sha512-lTyOMnOcmRBzdCv6/4zUZCrEhV0xrqQiUZBZtVcbmLHBXKLnN8q89v/wKRF/Q0HbwVsqbIG3o5HDoFl9sduM+g==} + '@types/gensync@1.0.5': + resolution: {integrity: sha512-MbsRCT7mTikHwKZ0X+LVUTLRrZZRLipTuXEO9qOYO+zmjMVk81axyClMROf6uoPD9MRVu46bx8zoR0Ad9q3NAg==} + + '@types/jsesc@2.5.1': + resolution: {integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==} + '@types/node@24.19.2': resolution: {integrity: sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==} + '@types/react-dom@19.3.0': + resolution: {integrity: sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==} + peerDependencies: + '@types/react': ^19.3.0 + + '@types/react@19.3.0': + resolution: {integrity: sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==} + '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -254,82 +739,441 @@ packages: '@vitest/utils@5.1.0': resolution: {integrity: sha512-V+qgCwkvVw+s1t1S5TaoBHsLi5Sz/YmohCMoOu0mZ8ZQst2bKhRYARAQucWpVg8D0mv0x5MHcWXY5QdOsoiGBQ==} - assertion-error@2.0.1: - resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} - engines: {node: '>=12'} - - ast-v8-to-istanbul@1.0.7: - resolution: {integrity: sha512-kFL68AG6ajd8fg248zwM9GQrUWEp79gsmjum34OEXjs4yHuUMZfYKwOLW9GMmB4oNvVrj+EAGxsP7ye2UR9UlA==} - - chai@6.3.0: - resolution: {integrity: sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==} - engines: {node: '>=18'} - - convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - - detect-libc@2.1.2: - resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} - engines: {node: '>=8'} + '@vue/compiler-core@3.5.44': + resolution: {integrity: sha512-k34nCytbK8PQ1gR2mGROYOw6Pnv/mumqIdrZ3x2tbyXZDWm7ViKGLTfdB+DRrWqEQsLgGJvn0WN9jBif5zCTLQ==} - es-module-lexer@2.3.2: - resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + '@vue/compiler-dom@3.5.44': + resolution: {integrity: sha512-W5AoBFWdw1lQAKiHowkfgFKFab82oSRT6sGsPfw/q1GRjsy/ZJFLHGhJyBRIFN9APLTUduDH9U2JAiPOIyKuYg==} - estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + '@vue/compiler-sfc@3.5.44': + resolution: {integrity: sha512-dzHmQ4jXTpdr5FjIQUeykVakN/uzqFbUo9/C5ikBFg3a3PeM6uRxeczx1ZbXBML0/zJstEh3ygYipzR5Z7b2Pw==} - expect-type@1.4.0: - resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} - engines: {node: '>=12.0.0'} + '@vue/compiler-ssr@3.5.44': + resolution: {integrity: sha512-xr5gb1JVHlfXJwSlRz1FeP6uxoazdzHuMTVLE7up+4Vrqxk219CvxLPxD9NXBt47yvZW7+p4DXfyp4O1fkByIA==} - fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true + '@vue/reactivity@3.5.44': + resolution: {integrity: sha512-u+QJdKeBBEADkPdevsL+6E12QlYVfBD8Zcw5qjDQKtamI6l14KyqazJMobhCKelhWN5zpyKUiEDoTyHE93cqWw==} - fflate@0.8.3: - resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} + '@vue/runtime-core@3.5.44': + resolution: {integrity: sha512-uEa/ndwjhrrhnOfb2p+sZy6zeoamJOISqaLSJGk/8R6nABT1ZldpzcJoPzKj+zHVomVBPcmQYyC2r4LtvDV6CQ==} - flatted@3.4.4: - resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} + '@vue/runtime-dom@3.5.44': + resolution: {integrity: sha512-3fc4ARcXVhVBaJ3ujWiNZJARD6pkyRcwG+ZYhMzjKtKUp7MAY0HLwr7r3Smep3Li6ZaWT6ewdmx8XqdaD5oNZg==} - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] + '@vue/server-renderer@3.5.44': + resolution: {integrity: sha512-R4S1UoH4h5Frrauv8Z5XE5TxV9oBiUSgWIC4TyVK1FK+RocRnOLwFA5Xcmo2011RlgJp+AbZCnw6afrhlgRuUQ==} - js-tokens@10.0.0: - resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + '@vue/shared@3.5.44': + resolution: {integrity: sha512-H82XLH0DjmSOdbEwO15x3QTo1f5PWEtv237eLpYKc0mqLu+B8hnq16lFWmuBmNurkeiG3wr+oM1FhOll/YVFRg==} - lightningcss-android-arm64@1.33.0: - resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} - engines: {node: '>= 12.0.0'} + '@yuku-core/binding-android-arm64@0.19.0': + resolution: {integrity: sha512-wdL2XmTiZixAOaHk9Z0JJZ8u9Qm8d333Zjpc6gaxXFX+UR5BlsfxZ7a2+2Rm71pamppsPYGUGFq2oVLbh6DISw==} cpu: [arm64] os: [android] - lightningcss-darwin-arm64@1.33.0: - resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} - engines: {node: '>= 12.0.0'} + '@yuku-core/binding-darwin-arm64@0.19.0': + resolution: {integrity: sha512-7sNJa6XJhyetnTy64jw3NEi4YgUcezHk1G5GH9FSvXAuC8wIlbaOwT9lgq5qo5sspjrZJQlBR1TWflP49uRpMw==} cpu: [arm64] os: [darwin] - lightningcss-darwin-x64@1.33.0: - resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} - engines: {node: '>= 12.0.0'} + '@yuku-core/binding-darwin-x64@0.19.0': + resolution: {integrity: sha512-pTC7dYEc59isdyizc3nsuyFNBu2LU5wMYZ1be6UMr59PY/AJNtO1aBLWV0MgL/z7C1nv/2sX4TNjGgKLoeevJw==} cpu: [x64] os: [darwin] - lightningcss-freebsd-x64@1.33.0: - resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} - engines: {node: '>= 12.0.0'} + '@yuku-core/binding-freebsd-x64@0.19.0': + resolution: {integrity: sha512-xrZjF6XOEwyLOiDIfrwDdRCS6ghxcJeirNC1KivQhVcUwlRonaU79S3/Uiyx3lBylPnT8qKJ3/aMT+H52nGTJQ==} cpu: [x64] os: [freebsd] - lightningcss-linux-arm-gnueabihf@1.33.0: + '@yuku-core/binding-linux-arm-gnu@0.19.0': + resolution: {integrity: sha512-0OyIH53C0I1CbADHn9FSdCfX5qbpeLwJhmOVhonFJNYheybOnkSJ50qGMfTJIb8y5zrwqvLs7FVBvz6Fk8maZg==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@yuku-core/binding-linux-arm-musl@0.19.0': + resolution: {integrity: sha512-z/iPfkCOCSXE34VXNt0uP1ORSW+4mEa9b5CiJcWZeqZH08g49bFU/jRM2T04f+Wpw5mJ6HIzNBiojxeAbrHjeg==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@yuku-core/binding-linux-arm64-gnu@0.19.0': + resolution: {integrity: sha512-jLNTi+gzaRwBJ+gwhCJ+rDb5dmx/8hEBx93UWd7hjMSqPTL1D1wvVSvXVAldV5kALvju7b0/m51EHEhs4+I1fA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@yuku-core/binding-linux-arm64-musl@0.19.0': + resolution: {integrity: sha512-YwyPIdW/rN2Fmv0MTFWi47FaB3F9g+2b+sJISyAw83Yd60RWH4T3Nx6XkAHHVN+uKJNBAd6So7/BVihSiFYvuQ==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@yuku-core/binding-linux-x64-gnu@0.19.0': + resolution: {integrity: sha512-/vskaAXVX3ESR43FiTdfiYS7z1b9uoQJwVQ/0RgUHgN2sWoAyCZs+aX4CgU7eKGIHTVfp9VFwv+c+2XchYiEtQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@yuku-core/binding-linux-x64-musl@0.19.0': + resolution: {integrity: sha512-FuXL5igeSKib9Rye02TdH+D6SQezTyGxIATMu1Qsn/QywrzwpGl35OLpS2C1x5vpGLeVE2XgiLd5mVPFHmIglQ==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@yuku-core/binding-win32-arm64@0.19.0': + resolution: {integrity: sha512-YuJAe7zEKT1Xa7LuHlhGJpPnYQeVtvo0rZvNjJRqkH0hQGOw4C8QeP30Tg5VKGzZfo2x8CjhjEhKt4TmLwfmdg==} + cpu: [arm64] + os: [win32] + + '@yuku-core/binding-win32-x64@0.19.0': + resolution: {integrity: sha512-o8iY/mXwlRkedp0LNTr4+TEfXTPQwoNZmv6k+1WIAd1ZXEn8xJJTBCtB0osaVvdZo/x/cNmV16UrWrrGpM5Mvw==} + cpu: [x64] + os: [win32] + + '@yuku-toolchain/types@0.19.0': + resolution: {integrity: sha512-tFkNbsP4WXX9UMUC5syJGdK9dPMLK0PPaIuXCcCYH75sZ8MrebfBZ2XvCILRTw8AgLivSSCfDa4R67iYBDa4Kg==} + + acorn@8.19.0: + resolution: {integrity: sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + ansi-regex@6.5.0: + resolution: {integrity: sha512-zhvbbbdt1kbw7EJ9OQoxmQcuaNR9P5kTj7e/YdmOSU10aHWbycofJxA6jKfoEy3vlLdctOF9ZHaxLtmSm998wQ==} + engines: {node: '>=12'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + aria-query@5.3.1: + resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} + engines: {node: '>= 0.4'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-v8-to-istanbul@1.0.7: + resolution: {integrity: sha512-kFL68AG6ajd8fg248zwM9GQrUWEp79gsmjum34OEXjs4yHuUMZfYKwOLW9GMmB4oNvVrj+EAGxsP7ye2UR9UlA==} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + baseline-browser-mapping@2.11.35: + resolution: {integrity: sha512-GNk8KX52CHqeNKGk8+7tZnQab/2k1t/BgrUHPKNdijzpEhF1saPQZHVKb6m5loSAxB39gyrTYw34Qf9CQ3u1qQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + browserslist@4.29.5: + resolution: {integrity: sha512-khTmKu5NGYTotqcYo8Bew2k4RAE9PuXRSFRXG2lQBrCEQHyeQ/vX6gKMxfokwywBsLiXZMnHsmWMG1pnO8J7uQ==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + caniuse-lite@1.0.30001820: + resolution: {integrity: sha512-gfWD6MxqVCqAOwmODfZf7DchWvlrA+SRdNX7ceLfg3xRqe5ssjjpF7EgslC/0tiLiNTajdaNN/q8c/5u/7Gz8w==} + + chai@6.3.0: + resolution: {integrity: sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==} + engines: {node: '>=18'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + + chokidar@6.0.0: + resolution: {integrity: sha512-Ec2Z7wpcKBiMpR7DxteoqS/sq5G5d2A4fpxzPv6+fD7xNNdrTghn7L11YwV5EN1B3i72Erc3XVT5FkWIDx90+A==} + engines: {node: '>= 22.22'} + + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-spinners@3.4.0: + resolution: {integrity: sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==} + engines: {node: '>=18.20'} + + cliui@9.0.1: + resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} + engines: {node: '>=20'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + colorjs.io@0.7.1: + resolution: {integrity: sha512-LY7OHnJZxHwT5UlzNa9bbhHHDbzB6yE5+3MIPwJEQKRvSCt/T4G7epsj+9j2BExUIIfXFIJGsIXUKdfrK9Q5tA==} + + commander@15.0.0: + resolution: {integrity: sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==} + engines: {node: '>=22.12.0'} + + common-path-prefix@3.0.0: + resolution: {integrity: sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==} + + convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + copy-anything@3.0.5: + resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} + engines: {node: '>=12.13'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + dedent-js@1.0.1: + resolution: {integrity: sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ==} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + dependency-graph@1.0.0: + resolution: {integrity: sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg==} + engines: {node: '>=4'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + devalue@5.9.4: + resolution: {integrity: sha512-sPAT4pztbu6586/hrhOnMKS17IJrvg12mXiSPSS3W5qDeN2RGgvZ0diZCm31dBbnevfVmujNO3IM2wrS4Y2Rhg==} + + dts-resolver@3.0.0: + resolution: {integrity: sha512-1T1f+z+4tl9XD+m+0HBgWoL/nm0bOIffyWaUuUSBlFg/86IWvfx+wjNaO/ybU0AJzG9/Mi5hBUgGV6zCmWEN7Q==} + engines: {node: ^22.18.0 || >=24.0.0} + peerDependencies: + oxc-resolver: '>=11.0.0' + peerDependenciesMeta: + oxc-resolver: + optional: true + + electron-to-chromium@1.5.475: + resolution: {integrity: sha512-S9tceHJbIsHH8VRwh9x+8lJO50SYwTdwVGdEjdz2FERyP59j1N6rdXOSklIusrhxt9GG/WN4V/kRgmDHl9MM5w==} + + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + + empathic@2.1.0: + resolution: {integrity: sha512-AnfC1ATldl49/cvZdLPDjBfrRNwbDO05aibiOtzQu3qtlbJtomNLhF30HEtn/7iBz50dlMECqATo3fG0LrdEgw==} + engines: {node: '>=14'} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + errno@0.1.8: + resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==} + hasBin: true + + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + esm-env@1.2.2: + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} + + esrap@2.4.0: + resolution: {integrity: sha512-CQPcneEHw/czj32uwQC52nYnax8H4SXt25FCM+LIwGZ49cMIxpOj3bP8wmzcI+Z0HIy5KkClaPhRLfCySzwHPA==} + peerDependencies: + '@typescript-eslint/types': ^8.2.0 + peerDependenciesMeta: + '@typescript-eslint/types': + optional: true + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-uri@3.1.9: + resolution: {integrity: sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fflate@0.8.3: + resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} + + find-cache-directory@6.0.0: + resolution: {integrity: sha512-CvFd5ivA6HcSHbD+59P7CyzINHXzwhuQK8RY7CxJZtgDSAtRlHiCaQpZQ2lMR/WRyUIEmzUvL6G2AGurMfegZA==} + engines: {node: '>=20'} + + find-up-simple@1.0.1: + resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==} + engines: {node: '>=18'} + + flatted@3.4.4: + resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} + + flru@1.0.2: + resolution: {integrity: sha512-kWyh8ADvHBFz6ua5xYOPnUroZTT/bwWfrCeL0Wj1dzG4/YOmOcfJ99W8dOVyyynJN35rZ9aCOtHChqQovV7yog==} + engines: {node: '>=6'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.7.0: + resolution: {integrity: sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==} + engines: {node: '>=18'} + + get-tsconfig@5.0.0-beta.6: + resolution: {integrity: sha512-X6fBC0pmImC70gvX2zm56go9hx0MyoGVdG0tUCkg/D+Xnh5TJsOZ7iDbOdI3PvmtrDxnu1YdDufpK2QJX1Meqw==} + engines: {node: '>=20.20.0'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + immutable@5.1.9: + resolution: {integrity: sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==} + + import-meta-resolve@4.2.0: + resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} + + injection-js@2.6.1: + resolution: {integrity: sha512-dbR5bdhi7TWDoCye9cByZqeg/gAfamm8Vu3G1KZOTYkOif8WkuM8CD0oeDPtZYMzT5YH76JAFB7bkmyY9OJi2A==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-interactive@2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} + + is-reference@3.0.3: + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + + is-what@4.1.16: + resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} + engines: {node: '>=12.13'} + + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + + less@4.9.1: + resolution: {integrity: sha512-orp15PfJvvNDIqJdVWzMI9Sjpjp3VTiw3sfvbB+67LlISTEn8uVT2EdYSuyl02BLvaftv6sdk9Umnxmm5rckmg==} + engines: {node: '>=18'} + hasBin: true + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.33.0: resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} engines: {node: '>= 12.0.0'} cpu: [arm] @@ -388,25 +1232,104 @@ packages: lit@3.3.3: resolution: {integrity: sha512-fycuvZg/hkpozL00lm1pEJH5nN/lr9ZXd6mJI2HSN4+Bzc+LDNdEApJ6HFbPkdFNHLvOplIIuJvxkS4XUxqirw==} + locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + log-symbols@7.0.1: + resolution: {integrity: sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==} + engines: {node: '>=18'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + magic-string@1.4.5: resolution: {integrity: sha512-oLLC2+YvQUfAa+vxoufE/yc1icb87M9iXIRqAoR/2EhE/8cb3SF/+Gi0c2By1B+5Vz2e2vQtUvHiu1+63Znh+w==} magicast@0.5.5: resolution: {integrity: sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==} + make-dir@5.1.0: + resolution: {integrity: sha512-IfpFq6UM39dUNiphpA6uDezNx/AvWyhwfICWPR3t1VspkgkMZrL+Rk1RbN1bx+aeNYwOrqGJgEgV3yotk+ZUVw==} + engines: {node: '>=18'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + nanoid@3.3.20: resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + needle@2.9.1: + resolution: {integrity: sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==} + engines: {node: '>= 4.4.x'} + hasBin: true + + needle@3.5.0: + resolution: {integrity: sha512-jaQyPKKk2YokHrEg+vFDYxXIHTCBgiZwSHOoVx/8V3GIBS8/VN6NdVRmg8q1ERtPkMvmOvebsgga4sAj5hls/w==} + engines: {node: '>= 4.4.x'} + hasBin: true + + ng-packagr@22.2.5: + resolution: {integrity: sha512-AlAruvF6HQa9TIMg17TyLRQ1gxIyTeJgF/KMABWdH0vCkzXE6PatQeTII0ukrkElSgahPRlZZwW/1bm3m7nJQA==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} + hasBin: true + peerDependencies: + '@angular/compiler-cli': ^22.0.0 || ^22.2.0-next || ^22.3.0-next + tailwindcss: ^2.0.0 || ^3.0.0 || ^4.0.0 + tslib: ^2.3.0 + typescript: '>=6.0 <6.1' + peerDependenciesMeta: + tailwindcss: + optional: true + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + node-releases@2.0.63: + resolution: {integrity: sha512-6li2QanLsAXpIH2R6j77MzRyvOyrvdTqlKtO4CgBdHGNvN99ZZfyxszHc5o5+IZBWD5u6Hzwg83JoGXmqZNG9Q==} + engines: {node: '>=18'} + + obug@2.2.1: + resolution: {integrity: sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==} + engines: {node: '>=12.20.0'} + obug@3.0.0: resolution: {integrity: sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==} engines: {node: '>=12.20.0'} + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + + ora@9.4.1: + resolution: {integrity: sha512-6VlU9MLXbjVQD04AZCMX28hVtA5bUoadvUqO76MUCVA0ilwJbMiHsITRPfyVm6p/BC0Av/BXMujx39WCe1LEqw==} + engines: {node: '>=20'} + + parse-node-version@1.0.1: + resolution: {integrity: sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==} + engines: {node: '>= 0.10'} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -417,6 +1340,10 @@ packages: resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} engines: {node: '>=12'} + pkg-dir@8.0.0: + resolution: {integrity: sha512-4peoBq4Wks0riS0z8741NVv+/8IiTvqnZAr8QGgtdifrtpdXbNw/FxRS1l6NFqm4EMzuS0EDqNNx4XGaz8cuyQ==} + engines: {node: '>=18'} + playwright-core@1.64.1: resolution: {integrity: sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ==} engines: {node: '>=20'} @@ -435,11 +1362,221 @@ packages: resolution: {integrity: sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==} engines: {node: ^10 || ^12 || >=14} + probe-image-size@7.5.0: + resolution: {integrity: sha512-ietdx4XPgmnzHRZOq1/KtcnsBkL4bRsBe+Szj5Tuq1/KPqDG42OspG1IhJb7vz6Z9pSzhp0J6UPnw6hujJTWoQ==} + + prr@1.0.1: + resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==} + + react-dom@19.3.1: + resolution: {integrity: sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==} + peerDependencies: + react: ^19.3.1 + + react@19.3.1: + resolution: {integrity: sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==} + engines: {node: '>=0.10.0'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + readdirp@5.1.1: + resolution: {integrity: sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==} + engines: {node: '>= 20.19.0'} + + reflect-metadata@0.2.2: + resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + + rolldown-plugin-dts@0.28.8: + resolution: {integrity: sha512-FVhfqxorCWcc20BVT5eVRfRjzLLl6mgzjcMR4gSBf0K4oQMXT0qoHhhzfXy0L7y83fQy7Ug5ZDNk90xM3/LiCQ==} + engines: {node: ^22.18.0 || ^24.11.0 || >=26.0.0} + peerDependencies: + '@typescript/native-preview': '*' + '@volar/typescript': ~2.4.0 + '@vue/language-core': ~3.2.0 || ~3.3.0 + rolldown: ^1.2.0 + typescript: ^5.0.0 || ^6.0.0 || ~7.0.0 + vue-tsc: ~3.2.0 || ~3.3.0 + peerDependenciesMeta: + '@typescript/native-preview': + optional: true + '@volar/typescript': + optional: true + '@vue/language-core': + optional: true + typescript: + optional: true + vue-tsc: + optional: true + rolldown@1.2.16: resolution: {integrity: sha512-bWvlMpFKiTfDKfnK7FC2MPgPeWCf/Kpwsq49ApdW+FQaBZb6O6dZ4MAPbPUjp7hYQrw/RobqCzn64BRrQecGHw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sass-embedded-all-unknown@1.107.1: + resolution: {integrity: sha512-8JVe87KQHSu/uMmcbcRa33AVDdwf0FSCR1tqA9fTaLrsqpgApD2YgqFr7KKCYTNRVSwzqAKdDaD2YJgx/siuVQ==} + cpu: ['!arm', '!arm64', '!riscv64', '!x64'] + + sass-embedded-android-arm64@1.107.1: + resolution: {integrity: sha512-tk1uq03Jqba2OuktUuIVQ0eSERB4oCcYiZ88wdzX1edWT/Yc7c4x6ECl2+7zqXec6SGIUDZlI86nurN5fTJLHA==} + engines: {node: '>=14.0.0'} + cpu: [arm64] + os: [android] + + sass-embedded-android-arm@1.107.1: + resolution: {integrity: sha512-qst7MmgqFwAHjbDN9Z+zjwMqmPM6RGu5ynXJFZmEmkkjCoQQX8DHUZhNCc2bASeLk/wyz1/9RCziUcJ6kn+E5w==} + engines: {node: '>=14.0.0'} + cpu: [arm] + os: [android] + + sass-embedded-android-riscv64@1.107.1: + resolution: {integrity: sha512-WzBSIg44+XP15kc+G2qtpfYBm0BtxjuC/X11eEg2L5Ihw45DoBtSLkpkSpLd9tvxDJOiTK3AHd1/iv5oGNvibg==} + engines: {node: '>=14.0.0'} + cpu: [riscv64] + os: [android] + + sass-embedded-android-x64@1.107.1: + resolution: {integrity: sha512-K6YN7kqj9aBLmraVDiPIHF7U8qoD9E/6AJIVwj1Y5H0EgPjGOsv1sYu9Uy6SBLvWGg4dsydhbPU7VC+hTGWk9A==} + engines: {node: '>=14.0.0'} + cpu: [x64] + os: [android] + + sass-embedded-darwin-arm64@1.107.1: + resolution: {integrity: sha512-okhyzPI4cLNwa/xRIWbpkRGKmebaNp1+15V5PxVsQbzd8c+dn7eZje2Vf5CWxv9jRZ0nJgM1K/F61Wwb0w8Tyg==} + engines: {node: '>=14.0.0'} + cpu: [arm64] + os: [darwin] + + sass-embedded-darwin-x64@1.107.1: + resolution: {integrity: sha512-hMMcxMxTiWoMaCfEvZqofN2l/yW++S0ANdVA3uHbVYO1LeRf9MrOWlOkJCjsbJO5FvfZdKGUxq/hc/5FKX9PXQ==} + engines: {node: '>=14.0.0'} + cpu: [x64] + os: [darwin] + + sass-embedded-linux-arm64@1.107.1: + resolution: {integrity: sha512-cDeuJi63cL2cfr9OZoB2z1jd7Omhdgbj7zBxawZ62NFLe/gYVgeUsHbkB0rh1xbsuMJ9nOu9Mu6bkw8N0XnigQ==} + engines: {node: '>=14.0.0'} + cpu: [arm64] + os: [linux] + libc: glibc + + sass-embedded-linux-arm@1.107.1: + resolution: {integrity: sha512-k/UIsseDf63xo5qwV1kZgTmJtFlBAKLQf3xb++NmQPv7zvdOZxJ/MVUd1cuDx7vuanVKhM+U6R1mW2GuQxEGHA==} + engines: {node: '>=14.0.0'} + cpu: [arm] + os: [linux] + libc: glibc + + sass-embedded-linux-musl-arm64@1.107.1: + resolution: {integrity: sha512-Pr7kApBThIJjKG7FkGIsFvYrhj8aCu7kEuov7WiK3ExUiv5m+Vz8eYzjqIEGGCAyVIGMuuvHCvPmuMUynPZ1CA==} + engines: {node: '>=14.0.0'} + cpu: [arm64] + os: [linux] + libc: musl + + sass-embedded-linux-musl-arm@1.107.1: + resolution: {integrity: sha512-jshFSmPp7/tq3zV5l7KEDWuZKN6aewTnKMkWzgXN4oiBvRKFWAeQqG2Acn+gjMwtrvY0aRGZxrgreJGwRmFZlg==} + engines: {node: '>=14.0.0'} + cpu: [arm] + os: [linux] + libc: musl + + sass-embedded-linux-musl-riscv64@1.107.1: + resolution: {integrity: sha512-5oJVR7xt0yQnIlW/1Xz+H5Bl2Tix5Gl4qrlrVvUCzfdFUWBwLSZi6KlabjC41UjRisandO3nxHXPZeI1T99krQ==} + engines: {node: '>=14.0.0'} + cpu: [riscv64] + os: [linux] + libc: musl + + sass-embedded-linux-musl-x64@1.107.1: + resolution: {integrity: sha512-ntHvwls0ImF6p9qgZBuIF+BKIZ+6FqrRKZn0JhiFzb48y5o/V0mre6YEmdq8YcYVxPrGaU+Ajk96lpTE7gUC8w==} + engines: {node: '>=14.0.0'} + cpu: [x64] + os: [linux] + libc: musl + + sass-embedded-linux-riscv64@1.107.1: + resolution: {integrity: sha512-m8zLEdUPafxCAXtqx5FgBylIlPaUcb4eBx1lE8ENp/kaqqTMmaapofl+YWFI8xII3id+4rePZBipR142vHnNpQ==} + engines: {node: '>=14.0.0'} + cpu: [riscv64] + os: [linux] + libc: glibc + + sass-embedded-linux-x64@1.107.1: + resolution: {integrity: sha512-ix29w9TrUrvpzjOir+Ndx3RH5VLJncjq6CMystVUVbx5rpYyyaSzciOm5NKF227ifivhuhWYOFPCkq1NedmXfQ==} + engines: {node: '>=14.0.0'} + cpu: [x64] + os: [linux] + libc: glibc + + sass-embedded-unknown-all@1.107.1: + resolution: {integrity: sha512-7JKs9q+V/kyVBesAjMa8hXx/qk6/cGKq/7cPSZoAWGu6oHx41wiJVfkZVAYt+fOFRHFXQunIKxIR9ZV7nA63PA==} + os: ['!android', '!darwin', '!linux', '!win32'] + + sass-embedded-win32-arm64@1.107.1: + resolution: {integrity: sha512-/DmYPuToDvhsT4Oz1RQuOvQuYFzhcjzAfxjwhRPdWXxGO1OR8+BhbxyXqOUMpMLwgFT9Du3GPjD1yvy8BXnTZQ==} + engines: {node: '>=14.0.0'} + cpu: [arm64] + os: [win32] + + sass-embedded-win32-x64@1.107.1: + resolution: {integrity: sha512-B25UYFtuhnYiqvfLNlJZyDp2xV7T0aT9nueE2w+QhDPM/wCZyeQfj1dgMgq0HYBwEybgx0oLuxomgTqplmQ+eg==} + engines: {node: '>=14.0.0'} + cpu: [x64] + os: [win32] + + sass-embedded@1.107.1: + resolution: {integrity: sha512-DpU0lgDxNE2H7iycTfvvmig1NZ92SqO63D1PPRI8sBniHdpEOzxQno71YRXvEDQgPkaJ7mnljwX16s80AqydaQ==} + engines: {node: '>=20.19.0'} + hasBin: true + + sass@1.107.1: + resolution: {integrity: sha512-aJ1/LvxOOb04YhmiBjzZn3jVwdHKol+OVhoMV6fFl02fF9T3i+5Mg9l/CnzqtM+NX5Jn5o3lAbs/ZJivTCLHfQ==} + engines: {node: '>=20.19.0'} + hasBin: true + + sax@1.6.1: + resolution: {integrity: sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==} + engines: {node: '>=11.0.0'} + + scheduler@0.28.0: + resolution: {integrity: sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==} + + scule@1.3.0: + resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} + + semver@7.8.6: + resolution: {integrity: sha512-9lpSSU4Mly3mrtruwLs1eLK6+gzbzqeBFwRNjIjE+9LuDpWdYctrQlQ3ej1Ilgi7gknpsTRnPoLP8DpCHqHIJw==} + engines: {node: '>=10'} + hasBin: true + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + sirv@3.0.2: resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} engines: {node: '>=18'} @@ -448,9 +1585,59 @@ packages: resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} engines: {node: '>=0.10.0'} + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + std-env@4.3.0: resolution: {integrity: sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==} + stdin-discarder@0.3.2: + resolution: {integrity: sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==} + engines: {node: '>=18'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string-width@8.3.0: + resolution: {integrity: sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==} + engines: {node: '>=20'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + svelte-check@4.7.8: + resolution: {integrity: sha512-laZ49GFHZ3b0z2PWinJWL0syehc/PTR8aHaUaTiBkWJ+4Y38nhxjmXsSz7CbGa3VdKopViVbrBH3bGNqIAI0bQ==} + engines: {node: '>= 18.0.0'} + hasBin: true + peerDependencies: + svelte: ^4.0.0 || ^5.0.0-next.0 + typescript: ^5.0.0 || ^6.0.0 + + svelte2tsx@0.7.63: + resolution: {integrity: sha512-gRgz/V6wOx0NDZhm6OMD2VIh+/HrgZc3k6ibX93rkNPEkWNn/P3Uhb1fYCqOrimEUpQd8r65dhiHZIJXSQBeow==} + peerDependencies: + svelte: ^3.55 || ^4.0.0-next.0 || ^4.0 || ^5.0.0-next.0 + typescript: ^4.9.4 || ^5.0.0 || ^6.0.0 + + svelte@5.57.3: + resolution: {integrity: sha512-5mAftM3SoDV9X0zdF386F9p2pv8iKOwi/KfBS1XnBAkVCKFzVOLHUoa8iGWAt4buoicLmyWVRpuuD5wplXOQsg==} + engines: {node: '>=18'} + + sync-child-process@1.1.1: + resolution: {integrity: sha512-54zMOnH5qgjdKW8SGpCx3daSP+v+TtTfOxgPdhUE3P0hytMMpknZfP67S4JeRcx5IQRWJERz6890yO4dkTywww==} + engines: {node: '>=16.0.0'} + + sync-message-port@1.2.0: + resolution: {integrity: sha512-gAQ9qrUN/UCypHtGFbbe7Rc/f9bzO88IwrG8TDo/aMKAApKyD6E3W4Cm0EfhfBb6Z6SKt59tTCTfD+n1xmAvMg==} + engines: {node: '>=16.0.0'} + tinybench@6.2.3: resolution: {integrity: sha512-Al01C2JIBnO2CXCAPbpAl6sUiOZHeThGtHYNTa51JRw9PyWOCdr+9Ong05R4qg/Sk3+H645E8ov1h2c58ityNw==} engines: {node: '>=20.0.0'} @@ -471,6 +1658,9 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + typescript@6.0.3: resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} engines: {node: '>=14.17'} @@ -479,6 +1669,19 @@ packages: undici-types@7.24.6: resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + update-browserslist-db@1.3.4: + resolution: {integrity: sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + varint@6.0.0: + resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==} + + verkit@0.5.0: + resolution: {integrity: sha512-u4J1hr6Y+C/hit34GwL3eIcUf3wngT8TlYUjK7tk0jxydKLAq3sYvifjdTDA6U3KCKRcQCQDGf/srjxfZF3l7g==} + engines: {node: '>=18.12.0'} + vite@8.3.5: resolution: {integrity: sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -522,6 +1725,14 @@ packages: yaml: optional: true + vitefu@1.1.3: + resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + vite: + optional: true + vitest@5.1.0: resolution: {integrity: sha512-MzmNFnrwa8v8lF1bLxw3UJ4EREwVOVb75VqJZmUDRjJ6EluK/x9th7qTbaeDk41WxE1GxWqmTXcdsfobfH5sOA==} engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0} @@ -563,11 +1774,23 @@ packages: jsdom: optional: true + vue@3.5.44: + resolution: {integrity: sha512-dvvmaOCG7lAMkJTPqPwWeGMsPS6DIsFsfZDUgBp9V6mHf9b3+/wyVwYR4Es+WV6uJC0FKRxp81Obcq0ZwUcGFQ==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + why-is-node-running@3.2.1: resolution: {integrity: sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==} engines: {node: '>=20.11'} hasBin: true + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + ws@8.22.1: resolution: {integrity: sha512-oyL6OPYme/CmiFvNuGzeoxCJ4nOrZE2O1FwM9MWM1AsGri6nLXB2pg+lNA8aLeXMSav0J6eIVS1j6vUPzUtK9g==} engines: {node: '>=10.0.0'} @@ -580,30 +1803,283 @@ packages: utf-8-validate: optional: true + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + yaml@2.9.0: resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} engines: {node: '>= 14.6'} hasBin: true + yargs-parser@22.0.0: + resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yargs@18.2.0: + resolution: {integrity: sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yoctocolors@2.2.0: + resolution: {integrity: sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==} + engines: {node: '>=18'} + + yuku-ast@0.19.0: + resolution: {integrity: sha512-PyCsiScaelhxBGzL8dyeJkLqvRZycoh78R31Vrt/WqYrm0hMFoKwIw/X7Dx5mBKLWAMlV9I9HEyBOHnj2gZVyw==} + engines: {node: '>=14.18.0'} + + yuku-codegen@0.19.0: + resolution: {integrity: sha512-uymAKHjDuaP4BEpkXC/BSLz4DUEF5oArZ5AuD3888nb2miChjWSgpPdojEJxm3pjpXkgUaCRJQyNAbnMN33wHg==} + engines: {node: '>=14.18.0'} + + yuku-core@0.19.0: + resolution: {integrity: sha512-kYK9e1XdfirNhkipzW0wmMcSWcVF/CWnuugodfVYRCUwvoBefweL9HzB96OiKEhsx78V5WbUxbJqlizMX869Qg==} + engines: {node: '>=14.18.0'} + + yuku-parser@0.19.0: + resolution: {integrity: sha512-XrBh9ZiQK5mJVRxZBAvD6LwRyAYrK3MFtH3+onyuxFL5hFD44EC3QOmIC1FjTj6H2MMwq46/NAI+NZxLZPZgxg==} + engines: {node: '>=14.18.0'} + + zimmerframe@1.1.5: + resolution: {integrity: sha512-msJxIvYDYcoNL+PJsu+7qmpDWsYmAxTY+2TNYXXF0hzBzBk0BMecOqDOG/EckUoKCuKwObfbugIl8QpqHDXeFA==} + snapshots: + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@angular/common@22.2.4(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))(rxjs@7.8.2)': + dependencies: + '@angular/core': 22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2) + rxjs: 7.8.2 + tslib: 2.8.1 + + '@angular/compiler-cli@22.2.4(@angular/compiler@22.2.4)(typescript@6.0.3)': + dependencies: + '@angular/compiler': 22.2.4 + '@babel/core': 8.0.1 + '@jridgewell/sourcemap-codec': 1.6.0 + chokidar: 5.0.0 + convert-source-map: 1.9.0 + reflect-metadata: 0.2.2 + semver: 7.8.6 + tslib: 2.8.1 + yargs: 18.2.0 + optionalDependencies: + typescript: 6.0.3 + + '@angular/compiler@22.2.4': + dependencies: + tslib: 2.8.1 + + '@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2)': + dependencies: + rxjs: 7.8.2 + tslib: 2.8.1 + optionalDependencies: + '@angular/compiler': 22.2.4 + + '@angular/platform-browser@22.2.4(@angular/common@22.2.4(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))': + dependencies: + '@angular/common': 22.2.4(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))(rxjs@7.8.2) + '@angular/core': 22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2) + tslib: 2.8.1 + + '@babel/code-frame@8.0.6': + dependencies: + '@babel/helper-validator-identifier': 8.0.6 + js-tokens: 10.0.0 + + '@babel/compat-data@8.0.5': {} + + '@babel/core@8.0.1': + dependencies: + '@babel/code-frame': 8.0.6 + '@babel/generator': 8.1.0 + '@babel/helper-compilation-targets': 8.0.7 + '@babel/helpers': 8.1.0 + '@babel/parser': 8.1.0 + '@babel/template': 8.0.0 + '@babel/traverse': 8.1.0 + '@babel/types': 8.1.0 + '@types/gensync': 1.0.5 + convert-source-map: 2.0.0 + empathic: 2.1.0 + gensync: 1.0.0-beta.2 + import-meta-resolve: 4.2.0 + json5: 2.2.3 + obug: 2.2.1 + semver: 7.8.6 + + '@babel/generator@8.1.0': + dependencies: + '@babel/parser': 8.1.0 + '@babel/types': 8.1.0 + '@jridgewell/gen-mapping': 0.4.0-beta.0 + '@jridgewell/trace-mapping': 0.3.31 + '@types/jsesc': 2.5.1 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@8.0.7': + dependencies: + '@babel/compat-data': 8.0.5 + '@babel/helper-validator-option': 8.0.0 + browserslist: 4.29.5 + flru: 1.0.2 + verkit: 0.5.0 + + '@babel/helper-globals@8.0.6': {} + '@babel/helper-string-parser@7.29.11': {} + '@babel/helper-string-parser@8.0.8': {} + '@babel/helper-validator-identifier@7.29.7': {} + '@babel/helper-validator-identifier@8.0.6': {} + + '@babel/helper-validator-option@8.0.0': {} + + '@babel/helpers@8.1.0': + dependencies: + '@babel/template': 8.0.0 + '@babel/types': 8.1.0 + '@babel/parser@7.29.12': dependencies: '@babel/types': 7.29.11 + '@babel/parser@8.1.0': + dependencies: + '@babel/types': 8.1.0 + + '@babel/template@8.0.0': + dependencies: + '@babel/code-frame': 8.0.6 + '@babel/parser': 8.1.0 + '@babel/types': 8.1.0 + + '@babel/traverse@8.1.0': + dependencies: + '@babel/code-frame': 8.0.6 + '@babel/generator': 8.1.0 + '@babel/helper-globals': 8.0.6 + '@babel/parser': 8.1.0 + '@babel/template': 8.0.0 + '@babel/types': 8.1.0 + obug: 3.0.0 + '@babel/types@7.29.11': dependencies: '@babel/helper-string-parser': 7.29.11 '@babel/helper-validator-identifier': 7.29.7 + '@babel/types@8.1.0': + dependencies: + '@babel/helper-string-parser': 8.0.8 + '@babel/helper-validator-identifier': 8.0.6 + '@bcoe/v8-coverage@1.0.2': {} '@blazediff/core@1.10.0': {} + '@bufbuild/protobuf@2.16.0': {} + + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/gen-mapping@0.4.0-beta.0': + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/sourcemap-codec@1.6.0': {} @@ -621,6 +2097,63 @@ snapshots: '@oxc-project/types@0.156.0': {} + '@parcel/watcher-android-arm64@2.6.0': + optional: true + + '@parcel/watcher-darwin-arm64@2.6.0': + optional: true + + '@parcel/watcher-darwin-x64@2.6.0': + optional: true + + '@parcel/watcher-freebsd-x64@2.6.0': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.6.0': + optional: true + + '@parcel/watcher-linux-arm-musl@2.6.0': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.6.0': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.6.0': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.6.0': + optional: true + + '@parcel/watcher-linux-x64-musl@2.6.0': + optional: true + + '@parcel/watcher-win32-arm64@2.6.0': + optional: true + + '@parcel/watcher-win32-x64@2.6.0': + optional: true + + '@parcel/watcher@2.6.0': + dependencies: + detect-libc: 2.1.2 + is-glob: 4.0.3 + node-addon-api: 7.1.1 + picomatch: 4.0.7 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.6.0 + '@parcel/watcher-darwin-arm64': 2.6.0 + '@parcel/watcher-darwin-x64': 2.6.0 + '@parcel/watcher-freebsd-x64': 2.6.0 + '@parcel/watcher-linux-arm-glibc': 2.6.0 + '@parcel/watcher-linux-arm-musl': 2.6.0 + '@parcel/watcher-linux-arm64-glibc': 2.6.0 + '@parcel/watcher-linux-arm64-musl': 2.6.0 + '@parcel/watcher-linux-x64-glibc': 2.6.0 + '@parcel/watcher-linux-x64-musl': 2.6.0 + '@parcel/watcher-win32-arm64': 2.6.0 + '@parcel/watcher-win32-x64': 2.6.0 + optional: true + '@polka/url@1.0.0-next.29': {} '@rolldown/binding-android-arm-eabi@1.2.16': @@ -670,45 +2203,83 @@ snapshots: '@rolldown/pluginutils@1.0.1': {} + '@sveltejs/acorn-typescript@1.0.13(acorn@8.19.0)': + dependencies: + acorn: 8.19.0 + + '@sveltejs/load-config@0.2.3': {} + + '@sveltejs/package@3.0.2(svelte@5.57.3)(typescript@6.0.3)': + dependencies: + '@sveltejs/load-config': 0.2.3 + chokidar: 6.0.0 + svelte: 5.57.3 + svelte2tsx: 0.7.63(svelte@5.57.3)(typescript@6.0.3) + optionalDependencies: + typescript: 6.0.3 + + '@sveltejs/vite-plugin-svelte@7.3.1(svelte@5.57.3)(vite@8.3.5(@types/node@24.19.2)(esbuild@0.28.2)(less@4.9.1)(sass-embedded@1.107.1)(sass@1.107.1)(yaml@2.9.0))': + dependencies: + deepmerge: 4.3.1 + magic-string: 1.4.5 + obug: 2.2.1 + svelte: 5.57.3 + vite: 8.3.5(@types/node@24.19.2)(esbuild@0.28.2)(less@4.9.1)(sass-embedded@1.107.1)(sass@1.107.1)(yaml@2.9.0) + vitefu: 1.1.3(vite@8.3.5(@types/node@24.19.2)(esbuild@0.28.2)(less@4.9.1)(sass-embedded@1.107.1)(sass@1.107.1)(yaml@2.9.0)) + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 + '@types/convert-source-map@2.0.3': {} + '@types/deep-eql@4.0.2': {} '@types/estree@1.0.10': {} + '@types/gensync@1.0.5': {} + + '@types/jsesc@2.5.1': {} + '@types/node@24.19.2': dependencies: undici-types: 7.24.6 + '@types/react-dom@19.3.0(@types/react@19.3.0)': + dependencies: + '@types/react': 19.3.0 + + '@types/react@19.3.0': + dependencies: + csstype: 3.2.3 + '@types/trusted-types@2.0.7': {} - '@vitest/browser-playwright@5.1.0(playwright@1.64.1)(vite@8.3.5(@types/node@24.19.2)(yaml@2.9.0))(vitest@5.1.0)': + '@vitest/browser-playwright@5.1.0(playwright@1.64.1)(vite@8.3.5(@types/node@24.19.2)(esbuild@0.28.2)(less@4.9.1)(sass-embedded@1.107.1)(sass@1.107.1)(yaml@2.9.0))(vitest@5.1.0)': dependencies: - '@vitest/browser': 5.1.0(vite@8.3.5(@types/node@24.19.2)(yaml@2.9.0))(vitest@5.1.0) - '@vitest/mocker': 5.1.0(vite@8.3.5(@types/node@24.19.2)(yaml@2.9.0)) + '@vitest/browser': 5.1.0(vite@8.3.5(@types/node@24.19.2)(esbuild@0.28.2)(less@4.9.1)(sass-embedded@1.107.1)(sass@1.107.1)(yaml@2.9.0))(vitest@5.1.0) + '@vitest/mocker': 5.1.0(vite@8.3.5(@types/node@24.19.2)(esbuild@0.28.2)(less@4.9.1)(sass-embedded@1.107.1)(sass@1.107.1)(yaml@2.9.0)) playwright: 1.64.1 tinyrainbow: 3.2.0 - vitest: 5.1.0(@types/node@24.19.2)(@vitest/browser-playwright@5.1.0)(@vitest/coverage-v8@5.1.0)(@vitest/ui@5.1.0)(vite@8.3.5(@types/node@24.19.2)(yaml@2.9.0)) + vitest: 5.1.0(@types/node@24.19.2)(@vitest/browser-playwright@5.1.0)(@vitest/coverage-v8@5.1.0)(@vitest/ui@5.1.0)(vite@8.3.5(@types/node@24.19.2)(esbuild@0.28.2)(less@4.9.1)(sass-embedded@1.107.1)(sass@1.107.1)(yaml@2.9.0)) transitivePeerDependencies: - bufferutil - msw - utf-8-validate - vite - '@vitest/browser@5.1.0(vite@8.3.5(@types/node@24.19.2)(yaml@2.9.0))(vitest@5.1.0)': + '@vitest/browser@5.1.0(vite@8.3.5(@types/node@24.19.2)(esbuild@0.28.2)(less@4.9.1)(sass-embedded@1.107.1)(sass@1.107.1)(yaml@2.9.0))(vitest@5.1.0)': dependencies: '@blazediff/core': 1.10.0 - '@vitest/mocker': 5.1.0(vite@8.3.5(@types/node@24.19.2)(yaml@2.9.0)) + '@vitest/mocker': 5.1.0(vite@8.3.5(@types/node@24.19.2)(esbuild@0.28.2)(less@4.9.1)(sass-embedded@1.107.1)(sass@1.107.1)(yaml@2.9.0)) '@vitest/ui': 5.1.0(vitest@5.1.0) '@vitest/utils': 5.1.0 magic-string: 1.4.5 pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.2.0 - vitest: 5.1.0(@types/node@24.19.2)(@vitest/browser-playwright@5.1.0)(@vitest/coverage-v8@5.1.0)(@vitest/ui@5.1.0)(vite@8.3.5(@types/node@24.19.2)(yaml@2.9.0)) + vitest: 5.1.0(@types/node@24.19.2)(@vitest/browser-playwright@5.1.0)(@vitest/coverage-v8@5.1.0)(@vitest/ui@5.1.0)(vite@8.3.5(@types/node@24.19.2)(esbuild@0.28.2)(less@4.9.1)(sass-embedded@1.107.1)(sass@1.107.1)(yaml@2.9.0)) ws: 8.22.1 transitivePeerDependencies: - bufferutil @@ -726,9 +2297,9 @@ snapshots: obug: 3.0.0 std-env: 4.3.0 tinyrainbow: 3.2.0 - vitest: 5.1.0(@types/node@24.19.2)(@vitest/browser-playwright@5.1.0)(@vitest/coverage-v8@5.1.0)(@vitest/ui@5.1.0)(vite@8.3.5(@types/node@24.19.2)(yaml@2.9.0)) + vitest: 5.1.0(@types/node@24.19.2)(@vitest/browser-playwright@5.1.0)(@vitest/coverage-v8@5.1.0)(@vitest/ui@5.1.0)(vite@8.3.5(@types/node@24.19.2)(esbuild@0.28.2)(less@4.9.1)(sass-embedded@1.107.1)(sass@1.107.1)(yaml@2.9.0)) optionalDependencies: - '@vitest/browser': 5.1.0(vite@8.3.5(@types/node@24.19.2)(yaml@2.9.0))(vitest@5.1.0) + '@vitest/browser': 5.1.0(vite@8.3.5(@types/node@24.19.2)(esbuild@0.28.2)(less@4.9.1)(sass-embedded@1.107.1)(sass@1.107.1)(yaml@2.9.0))(vitest@5.1.0) '@vitest/istanbul-lib-coverage@2.1.0': {} @@ -736,72 +2307,389 @@ snapshots: dependencies: '@vitest/istanbul-lib-coverage': 2.1.0 - '@vitest/mocker@5.1.0(vite@8.3.5(@types/node@24.19.2)(yaml@2.9.0))': + '@vitest/mocker@5.1.0(vite@8.3.5(@types/node@24.19.2)(esbuild@0.28.2)(less@4.9.1)(sass-embedded@1.107.1)(sass@1.107.1)(yaml@2.9.0))': dependencies: '@jridgewell/trace-mapping': 0.3.31 '@vitest/spy': 5.1.0 estree-walker: 3.0.3 magic-string: 1.4.5 optionalDependencies: - vite: 8.3.5(@types/node@24.19.2)(yaml@2.9.0) + vite: 8.3.5(@types/node@24.19.2)(esbuild@0.28.2)(less@4.9.1)(sass-embedded@1.107.1)(sass@1.107.1)(yaml@2.9.0) '@vitest/pretty-format@5.1.0': dependencies: tinyrainbow: 3.2.0 - '@vitest/spy@5.1.0': {} + '@vitest/spy@5.1.0': {} + + '@vitest/ui@5.1.0(vitest@5.1.0)': + dependencies: + '@vitest/utils': 5.1.0 + fflate: 0.8.3 + flatted: 3.4.4 + pathe: 2.0.3 + sirv: 3.0.2 + tinyrainbow: 3.2.0 + vitest: 5.1.0(@types/node@24.19.2)(@vitest/browser-playwright@5.1.0)(@vitest/coverage-v8@5.1.0)(@vitest/ui@5.1.0)(vite@8.3.5(@types/node@24.19.2)(esbuild@0.28.2)(less@4.9.1)(sass-embedded@1.107.1)(sass@1.107.1)(yaml@2.9.0)) + + '@vitest/utils@5.1.0': + dependencies: + '@vitest/pretty-format': 5.1.0 + convert-source-map: 2.0.0 + tinyrainbow: 3.2.0 + + '@vue/compiler-core@3.5.44': + dependencies: + '@babel/parser': 7.29.12 + '@vue/shared': 3.5.44 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.2 + + '@vue/compiler-dom@3.5.44': + dependencies: + '@vue/compiler-core': 3.5.44 + '@vue/shared': 3.5.44 + + '@vue/compiler-sfc@3.5.44': + dependencies: + '@babel/parser': 7.29.12 + '@vue/compiler-core': 3.5.44 + '@vue/compiler-dom': 3.5.44 + '@vue/compiler-ssr': 3.5.44 + '@vue/shared': 3.5.44 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.32 + source-map-js: 1.2.2 + + '@vue/compiler-ssr@3.5.44': + dependencies: + '@vue/compiler-dom': 3.5.44 + '@vue/shared': 3.5.44 + + '@vue/reactivity@3.5.44': + dependencies: + '@vue/shared': 3.5.44 + + '@vue/runtime-core@3.5.44': + dependencies: + '@vue/reactivity': 3.5.44 + '@vue/shared': 3.5.44 + + '@vue/runtime-dom@3.5.44': + dependencies: + '@vue/reactivity': 3.5.44 + '@vue/runtime-core': 3.5.44 + '@vue/shared': 3.5.44 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.44': + dependencies: + '@vue/compiler-ssr': 3.5.44 + '@vue/runtime-dom': 3.5.44 + '@vue/shared': 3.5.44 + + '@vue/shared@3.5.44': {} + + '@yuku-core/binding-android-arm64@0.19.0': + optional: true + + '@yuku-core/binding-darwin-arm64@0.19.0': + optional: true + + '@yuku-core/binding-darwin-x64@0.19.0': + optional: true + + '@yuku-core/binding-freebsd-x64@0.19.0': + optional: true + + '@yuku-core/binding-linux-arm-gnu@0.19.0': + optional: true + + '@yuku-core/binding-linux-arm-musl@0.19.0': + optional: true + + '@yuku-core/binding-linux-arm64-gnu@0.19.0': + optional: true + + '@yuku-core/binding-linux-arm64-musl@0.19.0': + optional: true + + '@yuku-core/binding-linux-x64-gnu@0.19.0': + optional: true + + '@yuku-core/binding-linux-x64-musl@0.19.0': + optional: true + + '@yuku-core/binding-win32-arm64@0.19.0': + optional: true + + '@yuku-core/binding-win32-x64@0.19.0': + optional: true + + '@yuku-toolchain/types@0.19.0': {} + + acorn@8.19.0: {} + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.9 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@6.5.0: {} + + ansi-styles@6.2.3: {} + + aria-query@5.3.1: {} + + assertion-error@2.0.1: {} + + ast-v8-to-istanbul@1.0.7: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 + + axobject-query@4.1.0: {} + + baseline-browser-mapping@2.11.35: {} + + browserslist@4.29.5: + dependencies: + baseline-browser-mapping: 2.11.35 + caniuse-lite: 1.0.30001820 + electron-to-chromium: 1.5.475 + node-releases: 2.0.63 + update-browserslist-db: 1.3.4(browserslist@4.29.5) + + caniuse-lite@1.0.30001820: {} + + chai@6.3.0: {} + + chalk@5.6.2: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + chokidar@5.0.0: + dependencies: + readdirp: 5.1.1 + + chokidar@6.0.0: + dependencies: + readdirp: 5.1.1 + + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-spinners@3.4.0: {} - '@vitest/ui@5.1.0(vitest@5.1.0)': + cliui@9.0.1: dependencies: - '@vitest/utils': 5.1.0 - fflate: 0.8.3 - flatted: 3.4.4 - pathe: 2.0.3 - sirv: 3.0.2 - tinyrainbow: 3.2.0 - vitest: 5.1.0(@types/node@24.19.2)(@vitest/browser-playwright@5.1.0)(@vitest/coverage-v8@5.1.0)(@vitest/ui@5.1.0)(vite@8.3.5(@types/node@24.19.2)(yaml@2.9.0)) + string-width: 7.2.0 + strip-ansi: 7.2.0 + wrap-ansi: 9.0.2 - '@vitest/utils@5.1.0': + clsx@2.1.1: {} + + colorjs.io@0.7.1: {} + + commander@15.0.0: {} + + common-path-prefix@3.0.0: {} + + convert-source-map@1.9.0: {} + + convert-source-map@2.0.0: {} + + copy-anything@3.0.5: dependencies: - '@vitest/pretty-format': 5.1.0 - convert-source-map: 2.0.0 - tinyrainbow: 3.2.0 + is-what: 4.1.16 - assertion-error@2.0.1: {} + csstype@3.2.3: {} - ast-v8-to-istanbul@1.0.7: + debug@3.2.7: dependencies: - '@jridgewell/trace-mapping': 0.3.31 - estree-walker: 3.0.3 - js-tokens: 10.0.0 + ms: 2.1.3 + optional: true - chai@6.3.0: {} + dedent-js@1.0.1: {} - convert-source-map@2.0.0: {} + deepmerge@4.3.1: {} + + dependency-graph@1.0.0: {} detect-libc@2.1.2: {} + devalue@5.9.4: {} + + dts-resolver@3.0.0: {} + + electron-to-chromium@1.5.475: {} + + emoji-regex@10.6.0: {} + + empathic@2.1.0: {} + + entities@7.0.1: {} + + errno@0.1.8: + dependencies: + prr: 1.0.1 + optional: true + es-module-lexer@2.3.2: {} + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + + escalade@3.2.0: {} + + esm-env@1.2.2: {} + + esrap@2.4.0: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + estree-walker@2.0.2: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.10 expect-type@1.4.0: {} + fast-deep-equal@3.1.3: {} + + fast-uri@3.1.9: {} + fdir@6.5.0(picomatch@4.0.7): optionalDependencies: picomatch: 4.0.7 fflate@0.8.3: {} + find-cache-directory@6.0.0: + dependencies: + common-path-prefix: 3.0.0 + pkg-dir: 8.0.0 + + find-up-simple@1.0.1: {} + flatted@3.4.4: {} + flru@1.0.2: {} + fsevents@2.3.3: optional: true + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.7.0: {} + + get-tsconfig@5.0.0-beta.6: + dependencies: + resolve-pkg-maps: 1.0.0 + + graceful-fs@4.2.11: + optional: true + + has-flag@4.0.0: {} + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + optional: true + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + optional: true + + immutable@5.1.9: {} + + import-meta-resolve@4.2.0: {} + + injection-js@2.6.1: + dependencies: + tslib: 2.8.1 + + is-extglob@2.1.1: + optional: true + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + optional: true + + is-interactive@2.0.0: {} + + is-reference@3.0.3: + dependencies: + '@types/estree': 1.0.10 + + is-unicode-supported@2.1.0: {} + + is-what@4.1.16: {} + js-tokens@10.0.0: {} + jsesc@3.1.0: {} + + json-schema-traverse@1.0.0: {} + + json5@2.2.3: {} + + jsonc-parser@3.3.1: {} + + less@4.9.1: + dependencies: + copy-anything: 3.0.5 + parse-node-version: 1.0.1 + optionalDependencies: + errno: 0.1.8 + graceful-fs: 4.2.11 + make-dir: 5.1.0 + mime: 1.6.0 + needle: 3.5.0 + probe-image-size: 7.5.0 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + lightningcss-android-arm64@1.33.0: optional: true @@ -867,6 +2755,20 @@ snapshots: lit-element: 4.2.2 lit-html: 3.3.3 + locate-character@3.0.0: {} + + lodash.merge@4.6.2: + optional: true + + log-symbols@7.0.1: + dependencies: + is-unicode-supported: 2.1.0 + yoctocolors: 2.2.0 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + magic-string@1.4.5: dependencies: '@jridgewell/sourcemap-codec': 1.6.0 @@ -877,18 +2779,106 @@ snapshots: '@babel/types': 7.29.11 source-map-js: 1.2.2 + make-dir@5.1.0: + optional: true + + mime@1.6.0: + optional: true + + mimic-function@5.0.1: {} + + mri@1.2.0: {} + mrmime@2.0.1: {} + ms@2.1.3: + optional: true + nanoid@3.3.20: {} + needle@2.9.1: + dependencies: + debug: 3.2.7 + iconv-lite: 0.4.24 + sax: 1.6.1 + transitivePeerDependencies: + - supports-color + optional: true + + needle@3.5.0: + dependencies: + iconv-lite: 0.6.3 + sax: 1.6.1 + optional: true + + ng-packagr@22.2.5(@angular/compiler-cli@22.2.4(@angular/compiler@22.2.4)(typescript@6.0.3))(tslib@2.8.1)(typescript@6.0.3): + dependencies: + '@ampproject/remapping': 2.3.0 + '@angular/compiler-cli': 22.2.4(@angular/compiler@22.2.4)(typescript@6.0.3) + ajv: 8.20.0 + browserslist: 4.29.5 + chokidar: 5.0.0 + commander: 15.0.0 + dependency-graph: 1.0.0 + esbuild: 0.28.2 + find-cache-directory: 6.0.0 + injection-js: 2.6.1 + jsonc-parser: 3.3.1 + less: 4.9.1 + ora: 9.4.1 + postcss: 8.5.32 + rolldown: 1.2.16 + rolldown-plugin-dts: 0.28.8(rolldown@1.2.16)(typescript@6.0.3) + rxjs: 7.8.2 + sass: 1.107.1 + sass-embedded: 1.107.1 + tinyglobby: 0.2.17 + tslib: 2.8.1 + typescript: 6.0.3 + transitivePeerDependencies: + - '@typescript/native-preview' + - '@volar/typescript' + - '@vue/language-core' + - oxc-resolver + - supports-color + - vue-tsc + + node-addon-api@7.1.1: + optional: true + + node-releases@2.0.63: {} + + obug@2.2.1: {} + obug@3.0.0: {} + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + + ora@9.4.1: + dependencies: + chalk: 5.6.2 + cli-cursor: 5.0.0 + cli-spinners: 3.4.0 + is-interactive: 2.0.0 + is-unicode-supported: 2.1.0 + log-symbols: 7.0.1 + stdin-discarder: 0.3.2 + string-width: 8.3.0 + + parse-node-version@1.0.1: {} + pathe@2.0.3: {} picocolors@1.1.1: {} picomatch@4.0.7: {} + pkg-dir@8.0.0: + dependencies: + find-up-simple: 1.0.1 + playwright-core@1.64.1: {} playwright@1.64.1: @@ -903,6 +2893,53 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.2 + probe-image-size@7.5.0: + dependencies: + lodash.merge: 4.6.2 + needle: 2.9.1 + transitivePeerDependencies: + - supports-color + optional: true + + prr@1.0.1: + optional: true + + react-dom@19.3.1(react@19.3.1): + dependencies: + react: 19.3.1 + scheduler: 0.28.0 + + react@19.3.1: {} + + readdirp@4.1.2: {} + + readdirp@5.1.1: {} + + reflect-metadata@0.2.2: {} + + require-from-string@2.0.2: {} + + resolve-pkg-maps@1.0.0: {} + + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + + rolldown-plugin-dts@0.28.8(rolldown@1.2.16)(typescript@6.0.3): + dependencies: + dts-resolver: 3.0.0 + get-tsconfig: 5.0.0-beta.6 + obug: 3.0.0 + rolldown: 1.2.16 + yuku-ast: 0.19.0 + yuku-codegen: 0.19.0 + yuku-parser: 0.19.0 + optionalDependencies: + typescript: 6.0.3 + transitivePeerDependencies: + - oxc-resolver + rolldown@1.2.16: dependencies: '@oxc-project/types': 0.156.0 @@ -924,6 +2961,123 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.2.16 '@rolldown/binding-win32-x64-msvc': 1.2.16 + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + sade@1.8.1: + dependencies: + mri: 1.2.0 + + safer-buffer@2.1.2: + optional: true + + sass-embedded-all-unknown@1.107.1: + dependencies: + sass: 1.107.1 + optional: true + + sass-embedded-android-arm64@1.107.1: + optional: true + + sass-embedded-android-arm@1.107.1: + optional: true + + sass-embedded-android-riscv64@1.107.1: + optional: true + + sass-embedded-android-x64@1.107.1: + optional: true + + sass-embedded-darwin-arm64@1.107.1: + optional: true + + sass-embedded-darwin-x64@1.107.1: + optional: true + + sass-embedded-linux-arm64@1.107.1: + optional: true + + sass-embedded-linux-arm@1.107.1: + optional: true + + sass-embedded-linux-musl-arm64@1.107.1: + optional: true + + sass-embedded-linux-musl-arm@1.107.1: + optional: true + + sass-embedded-linux-musl-riscv64@1.107.1: + optional: true + + sass-embedded-linux-musl-x64@1.107.1: + optional: true + + sass-embedded-linux-riscv64@1.107.1: + optional: true + + sass-embedded-linux-x64@1.107.1: + optional: true + + sass-embedded-unknown-all@1.107.1: + dependencies: + sass: 1.107.1 + optional: true + + sass-embedded-win32-arm64@1.107.1: + optional: true + + sass-embedded-win32-x64@1.107.1: + optional: true + + sass-embedded@1.107.1: + dependencies: + '@bufbuild/protobuf': 2.16.0 + colorjs.io: 0.7.1 + immutable: 5.1.9 + rxjs: 7.8.2 + supports-color: 8.1.1 + sync-child-process: 1.1.1 + varint: 6.0.0 + optionalDependencies: + sass-embedded-all-unknown: 1.107.1 + sass-embedded-android-arm: 1.107.1 + sass-embedded-android-arm64: 1.107.1 + sass-embedded-android-riscv64: 1.107.1 + sass-embedded-android-x64: 1.107.1 + sass-embedded-darwin-arm64: 1.107.1 + sass-embedded-darwin-x64: 1.107.1 + sass-embedded-linux-arm: 1.107.1 + sass-embedded-linux-arm64: 1.107.1 + sass-embedded-linux-musl-arm: 1.107.1 + sass-embedded-linux-musl-arm64: 1.107.1 + sass-embedded-linux-musl-riscv64: 1.107.1 + sass-embedded-linux-musl-x64: 1.107.1 + sass-embedded-linux-riscv64: 1.107.1 + sass-embedded-linux-x64: 1.107.1 + sass-embedded-unknown-all: 1.107.1 + sass-embedded-win32-arm64: 1.107.1 + sass-embedded-win32-x64: 1.107.1 + + sass@1.107.1: + dependencies: + chokidar: 5.0.0 + immutable: 5.1.9 + source-map-js: 1.2.2 + optionalDependencies: + '@parcel/watcher': 2.6.0 + + sax@1.6.1: + optional: true + + scheduler@0.28.0: {} + + scule@1.3.0: {} + + semver@7.8.6: {} + + signal-exit@4.1.0: {} + sirv@3.0.2: dependencies: '@polka/url': 1.0.0-next.29 @@ -932,8 +3086,78 @@ snapshots: source-map-js@1.2.2: {} + source-map@0.6.1: + optional: true + std-env@4.3.0: {} + stdin-discarder@0.3.2: {} + + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.7.0 + strip-ansi: 7.2.0 + + string-width@8.3.0: + dependencies: + get-east-asian-width: 1.7.0 + strip-ansi: 7.2.0 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.5.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + svelte-check@4.7.8(picomatch@4.0.7)(svelte@5.57.3)(typescript@6.0.3): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@sveltejs/load-config': 0.2.3 + chokidar: 4.0.3 + fdir: 6.5.0(picomatch@4.0.7) + picocolors: 1.1.1 + sade: 1.8.1 + svelte: 5.57.3 + typescript: 6.0.3 + transitivePeerDependencies: + - picomatch + + svelte2tsx@0.7.63(svelte@5.57.3)(typescript@6.0.3): + dependencies: + dedent-js: 1.0.1 + scule: 1.3.0 + svelte: 5.57.3 + typescript: 6.0.3 + + svelte@5.57.3: + dependencies: + '@jridgewell/remapping': 2.3.5 + '@jridgewell/sourcemap-codec': 1.6.0 + '@sveltejs/acorn-typescript': 1.0.13(acorn@8.19.0) + '@types/estree': 1.0.10 + acorn: 8.19.0 + aria-query: 5.3.1 + axobject-query: 4.1.0 + clsx: 2.1.1 + devalue: 5.9.4 + esm-env: 1.2.2 + esrap: 2.4.0 + is-reference: 3.0.3 + locate-character: 3.0.0 + magic-string: 0.30.21 + zimmerframe: 1.1.5 + transitivePeerDependencies: + - '@typescript-eslint/types' + + sync-child-process@1.1.1: + dependencies: + sync-message-port: 1.2.0 + + sync-message-port@1.2.0: {} + tinybench@6.2.3: {} tinyexec@1.3.1: {} @@ -947,11 +3171,23 @@ snapshots: totalist@3.0.1: {} + tslib@2.8.1: {} + typescript@6.0.3: {} undici-types@7.24.6: {} - vite@8.3.5(@types/node@24.19.2)(yaml@2.9.0): + update-browserslist-db@1.3.4(browserslist@4.29.5): + dependencies: + browserslist: 4.29.5 + escalade: 3.2.0 + picocolors: 1.1.1 + + varint@6.0.0: {} + + verkit@0.5.0: {} + + vite@8.3.5(@types/node@24.19.2)(esbuild@0.28.2)(less@4.9.1)(sass-embedded@1.107.1)(sass@1.107.1)(yaml@2.9.0): dependencies: lightningcss: 1.33.0 picomatch: 4.0.7 @@ -960,13 +3196,21 @@ snapshots: tinyglobby: 0.2.17 optionalDependencies: '@types/node': 24.19.2 + esbuild: 0.28.2 fsevents: 2.3.3 + less: 4.9.1 + sass: 1.107.1 + sass-embedded: 1.107.1 yaml: 2.9.0 - vitest@5.1.0(@types/node@24.19.2)(@vitest/browser-playwright@5.1.0)(@vitest/coverage-v8@5.1.0)(@vitest/ui@5.1.0)(vite@8.3.5(@types/node@24.19.2)(yaml@2.9.0)): + vitefu@1.1.3(vite@8.3.5(@types/node@24.19.2)(esbuild@0.28.2)(less@4.9.1)(sass-embedded@1.107.1)(sass@1.107.1)(yaml@2.9.0)): + optionalDependencies: + vite: 8.3.5(@types/node@24.19.2)(esbuild@0.28.2)(less@4.9.1)(sass-embedded@1.107.1)(sass@1.107.1)(yaml@2.9.0) + + vitest@5.1.0(@types/node@24.19.2)(@vitest/browser-playwright@5.1.0)(@vitest/coverage-v8@5.1.0)(@vitest/ui@5.1.0)(vite@8.3.5(@types/node@24.19.2)(esbuild@0.28.2)(less@4.9.1)(sass-embedded@1.107.1)(sass@1.107.1)(yaml@2.9.0)): dependencies: '@types/chai': 5.2.3 - '@vitest/mocker': 5.1.0(vite@8.3.5(@types/node@24.19.2)(yaml@2.9.0)) + '@vitest/mocker': 5.1.0(vite@8.3.5(@types/node@24.19.2)(esbuild@0.28.2)(less@4.9.1)(sass-embedded@1.107.1)(sass@1.107.1)(yaml@2.9.0)) chai: 6.3.0 es-module-lexer: 2.3.2 expect-type: 1.4.0 @@ -977,18 +3221,81 @@ snapshots: tinybench: 6.2.3 tinyexec: 1.3.1 tinyglobby: 0.2.17 - vite: 8.3.5(@types/node@24.19.2)(yaml@2.9.0) + vite: 8.3.5(@types/node@24.19.2)(esbuild@0.28.2)(less@4.9.1)(sass-embedded@1.107.1)(sass@1.107.1)(yaml@2.9.0) why-is-node-running: 3.2.1 optionalDependencies: '@types/node': 24.19.2 - '@vitest/browser-playwright': 5.1.0(playwright@1.64.1)(vite@8.3.5(@types/node@24.19.2)(yaml@2.9.0))(vitest@5.1.0) + '@vitest/browser-playwright': 5.1.0(playwright@1.64.1)(vite@8.3.5(@types/node@24.19.2)(esbuild@0.28.2)(less@4.9.1)(sass-embedded@1.107.1)(sass@1.107.1)(yaml@2.9.0))(vitest@5.1.0) '@vitest/coverage-v8': 5.1.0(@vitest/browser@5.1.0)(vitest@5.1.0) '@vitest/ui': 5.1.0(vitest@5.1.0) transitivePeerDependencies: - msw + vue@3.5.44(typescript@6.0.3): + dependencies: + '@vue/compiler-dom': 3.5.44 + '@vue/compiler-sfc': 3.5.44 + '@vue/runtime-dom': 3.5.44 + '@vue/server-renderer': 3.5.44 + '@vue/shared': 3.5.44 + optionalDependencies: + typescript: 6.0.3 + why-is-node-running@3.2.1: {} + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.2.0 + ws@8.22.1: {} + y18n@5.0.8: {} + yaml@2.9.0: {} + + yargs-parser@22.0.0: {} + + yargs@18.2.0: + dependencies: + cliui: 9.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + string-width: 8.3.0 + y18n: 5.0.8 + yargs-parser: 22.0.0 + + yoctocolors@2.2.0: {} + + yuku-ast@0.19.0: + dependencies: + '@yuku-toolchain/types': 0.19.0 + + yuku-codegen@0.19.0: + dependencies: + '@yuku-toolchain/types': 0.19.0 + + yuku-core@0.19.0: + dependencies: + '@yuku-toolchain/types': 0.19.0 + optionalDependencies: + '@yuku-core/binding-android-arm64': 0.19.0 + '@yuku-core/binding-darwin-arm64': 0.19.0 + '@yuku-core/binding-darwin-x64': 0.19.0 + '@yuku-core/binding-freebsd-x64': 0.19.0 + '@yuku-core/binding-linux-arm-gnu': 0.19.0 + '@yuku-core/binding-linux-arm-musl': 0.19.0 + '@yuku-core/binding-linux-arm64-gnu': 0.19.0 + '@yuku-core/binding-linux-arm64-musl': 0.19.0 + '@yuku-core/binding-linux-x64-gnu': 0.19.0 + '@yuku-core/binding-linux-x64-musl': 0.19.0 + '@yuku-core/binding-win32-arm64': 0.19.0 + '@yuku-core/binding-win32-x64': 0.19.0 + + yuku-parser@0.19.0: + dependencies: + '@yuku-toolchain/types': 0.19.0 + yuku-core: 0.19.0 + + zimmerframe@1.1.5: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index e535fd3..3332ecc 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,4 @@ packages: - packages/* - examples/* +linkWorkspacePackages: true diff --git a/specs/CATALOG.md b/specs/CATALOG.md index 2f87a8e..9280fa2 100644 --- a/specs/CATALOG.md +++ b/specs/CATALOG.md @@ -10,7 +10,7 @@ debt or convert unknown historic intent into a design decision. | Capability ID | Status | Scope | Primary SDD | Evidence | |---|---|---|---|---| -| `shared-ui-foundation` | Proposed | One DOM component implementation with React, Vue, Angular and Svelte adapters, direct Lit/DOM use, shared Storybook and verified Gateway/Domus adoption. Partial source; no complete capability or release accepted. | [Shared Home Assistant presentation components and framework adapters](./shared-ui-foundation.md) + 1 companion | 19 paths · 2026-11-04 | +| `shared-ui-foundation` | Proposed | One DOM component implementation with React, Vue, Angular and Svelte adapters, direct Lit/DOM use, shared Storybook and verified Gateway/Domus adoption. Partial source; no complete capability or release accepted. | [Shared Home Assistant presentation components and framework adapters](./shared-ui-foundation.md) + 1 companion | 66 paths · 2026-11-04 | ## Evidence map @@ -20,10 +20,10 @@ debt or convert unknown historic intent into a design decision. - Last verified: 2026-11-04 - Specifications: [`specs/shared-ui-foundation.md`](./shared-ui-foundation.md) · [`specs/shared-ui-framework-delivery.md`](./shared-ui-framework-delivery.md) - Workflows: [`.github/workflows/copilot_issue.yml`](../.github/workflows/copilot_issue.yml) · [`.github/workflows/ci_check.yml`](../.github/workflows/ci_check.yml) -- Entrypoints: [`packages/core/src/index.ts`](../packages/core/src/index.ts) · [`packages/core/src/components/tabs.ts`](../packages/core/src/components/tabs.ts) -- Core code: [`packages/core/src/contracts.ts`](../packages/core/src/contracts.ts) · [`tooling/coverage.mjs`](../tooling/coverage.mjs) · [`packages/core/src/components/tabs.ts`](../packages/core/src/components/tabs.ts) -- Tests: [`tests/contracts/core.test.ts`](../tests/contracts/core.test.ts) · [`tests/coverage.test.mjs`](../tests/coverage.test.mjs) · [`tests/architecture.test.mjs`](../tests/architecture.test.mjs) · [`tests/coverage-evidence.test.mjs`](../tests/coverage-evidence.test.mjs) · [`tests/consumers/packed.test.ts`](../tests/consumers/packed.test.ts) · [`tests/contracts/registration.test.ts`](../tests/contracts/registration.test.ts) · [`tests/browser/tabs.test.ts`](../tests/browser/tabs.test.ts) -- User documentation: [`README.md`](../README.md) · [`docs/component-library.md`](../docs/component-library.md) · [`docs/compatibility.md`](../docs/compatibility.md) · [`docs/provenance.md`](../docs/provenance.md) · [`docs/evidence/shared-ui-foundation.json`](../docs/evidence/shared-ui-foundation.json) +- Entrypoints: [`packages/core/src/index.ts`](../packages/core/src/index.ts) · [`packages/core/src/components/tabs.ts`](../packages/core/src/components/tabs.ts) · [`packages/react/src/index.ts`](../packages/react/src/index.ts) · [`packages/vue/src/index.ts`](../packages/vue/src/index.ts) · [`packages/angular/src/index.ts`](../packages/angular/src/index.ts) · [`packages/svelte/src/index.ts`](../packages/svelte/src/index.ts) +- Core code: [`packages/core/src/contracts.ts`](../packages/core/src/contracts.ts) · [`tooling/coverage.mjs`](../tooling/coverage.mjs) · [`packages/core/src/components/tabs.ts`](../packages/core/src/components/tabs.ts) · [`packages/react/src/index.ts`](../packages/react/src/index.ts) · [`packages/vue/src/index.ts`](../packages/vue/src/index.ts) · [`packages/angular/src/index.ts`](../packages/angular/src/index.ts) · [`packages/svelte/src/index.ts`](../packages/svelte/src/index.ts) · [`packages/svelte/src/Tabs.svelte`](../packages/svelte/src/Tabs.svelte) · [`packages/svelte/src/types.ts`](../packages/svelte/src/types.ts) +- Tests: [`tests/contracts/core.test.ts`](../tests/contracts/core.test.ts) · [`tests/coverage.test.mjs`](../tests/coverage.test.mjs) · [`tests/architecture.test.mjs`](../tests/architecture.test.mjs) · [`tests/coverage-evidence.test.mjs`](../tests/coverage-evidence.test.mjs) · [`tests/consumers/packed.test.ts`](../tests/consumers/packed.test.ts) · [`tests/contracts/registration.test.ts`](../tests/contracts/registration.test.ts) · [`tests/browser/tabs.test.ts`](../tests/browser/tabs.test.ts) · [`tests/browser/react.test.ts`](../tests/browser/react.test.ts) · [`tests/browser/vue.test.ts`](../tests/browser/vue.test.ts) · [`tests/browser/angular.test.ts`](../tests/browser/angular.test.ts) · [`tests/browser/svelte.test.mjs`](../tests/browser/svelte.test.mjs) · [`tests/browser/framework-fixture.ts`](../tests/browser/framework-fixture.ts) · [`tests/browser/fixtures/SvelteTabs.svelte`](../tests/browser/fixtures/SvelteTabs.svelte) · [`tests/consumers/frameworks.ts`](../tests/consumers/frameworks.ts) · [`tests/consumers/fixtures/svelte/pnpm-lock.yaml`](../tests/consumers/fixtures/svelte/pnpm-lock.yaml) · [`tests/consumers/fixtures/svelte/index.html`](../tests/consumers/fixtures/svelte/index.html) · [`tests/consumers/fixtures/svelte/Consumer.svelte`](../tests/consumers/fixtures/svelte/Consumer.svelte) · [`tests/consumers/fixtures/svelte/model.ts`](../tests/consumers/fixtures/svelte/model.ts) · [`tests/consumers/fixtures/svelte/package.json`](../tests/consumers/fixtures/svelte/package.json) · [`tests/consumers/fixtures/svelte/client.ts`](../tests/consumers/fixtures/svelte/client.ts) · [`tests/consumers/fixtures/svelte/tsconfig.json`](../tests/consumers/fixtures/svelte/tsconfig.json) · [`tests/consumers/fixtures/svelte/pnpm-workspace.yaml`](../tests/consumers/fixtures/svelte/pnpm-workspace.yaml) · [`tests/consumers/fixtures/vue/pnpm-lock.yaml`](../tests/consumers/fixtures/vue/pnpm-lock.yaml) · [`tests/consumers/fixtures/vue/index.html`](../tests/consumers/fixtures/vue/index.html) · [`tests/consumers/fixtures/vue/model.ts`](../tests/consumers/fixtures/vue/model.ts) · [`tests/consumers/fixtures/vue/package.json`](../tests/consumers/fixtures/vue/package.json) · [`tests/consumers/fixtures/vue/client.ts`](../tests/consumers/fixtures/vue/client.ts) · [`tests/consumers/fixtures/vue/tsconfig.json`](../tests/consumers/fixtures/vue/tsconfig.json) · [`tests/consumers/fixtures/vue/pnpm-workspace.yaml`](../tests/consumers/fixtures/vue/pnpm-workspace.yaml) · [`tests/consumers/fixtures/angular/pnpm-lock.yaml`](../tests/consumers/fixtures/angular/pnpm-lock.yaml) · [`tests/consumers/fixtures/angular/index.html`](../tests/consumers/fixtures/angular/index.html) · [`tests/consumers/fixtures/angular/model.ts`](../tests/consumers/fixtures/angular/model.ts) · [`tests/consumers/fixtures/angular/package.json`](../tests/consumers/fixtures/angular/package.json) · [`tests/consumers/fixtures/angular/client.ts`](../tests/consumers/fixtures/angular/client.ts) · [`tests/consumers/fixtures/angular/tsconfig.json`](../tests/consumers/fixtures/angular/tsconfig.json) · [`tests/consumers/fixtures/angular/pnpm-workspace.yaml`](../tests/consumers/fixtures/angular/pnpm-workspace.yaml) · [`tests/consumers/fixtures/react/pnpm-lock.yaml`](../tests/consumers/fixtures/react/pnpm-lock.yaml) · [`tests/consumers/fixtures/react/index.html`](../tests/consumers/fixtures/react/index.html) · [`tests/consumers/fixtures/react/model.ts`](../tests/consumers/fixtures/react/model.ts) · [`tests/consumers/fixtures/react/package.json`](../tests/consumers/fixtures/react/package.json) · [`tests/consumers/fixtures/react/client.ts`](../tests/consumers/fixtures/react/client.ts) · [`tests/consumers/fixtures/react/tsconfig.json`](../tests/consumers/fixtures/react/tsconfig.json) · [`tests/consumers/fixtures/react/pnpm-workspace.yaml`](../tests/consumers/fixtures/react/pnpm-workspace.yaml) +- User documentation: [`README.md`](../README.md) · [`docs/component-library.md`](../docs/component-library.md) · [`docs/compatibility.md`](../docs/compatibility.md) · [`docs/provenance.md`](../docs/provenance.md) · [`docs/evidence/shared-ui-foundation.json`](../docs/evidence/shared-ui-foundation.json) · [`docs/framework-adapters.md`](../docs/framework-adapters.md) ## Maintenance contract diff --git a/specs/catalog.json b/specs/catalog.json index cc0fda3..31c4f1c 100644 --- a/specs/catalog.json +++ b/specs/catalog.json @@ -18,12 +18,22 @@ ], "entrypoints": [ "packages/core/src/index.ts", - "packages/core/src/components/tabs.ts" + "packages/core/src/components/tabs.ts", + "packages/react/src/index.ts", + "packages/vue/src/index.ts", + "packages/angular/src/index.ts", + "packages/svelte/src/index.ts" ], "code": [ "packages/core/src/contracts.ts", "tooling/coverage.mjs", - "packages/core/src/components/tabs.ts" + "packages/core/src/components/tabs.ts", + "packages/react/src/index.ts", + "packages/vue/src/index.ts", + "packages/angular/src/index.ts", + "packages/svelte/src/index.ts", + "packages/svelte/src/Tabs.svelte", + "packages/svelte/src/types.ts" ], "tests": [ "tests/contracts/core.test.ts", @@ -32,14 +42,51 @@ "tests/coverage-evidence.test.mjs", "tests/consumers/packed.test.ts", "tests/contracts/registration.test.ts", - "tests/browser/tabs.test.ts" + "tests/browser/tabs.test.ts", + "tests/browser/react.test.ts", + "tests/browser/vue.test.ts", + "tests/browser/angular.test.ts", + "tests/browser/svelte.test.mjs", + "tests/browser/framework-fixture.ts", + "tests/browser/fixtures/SvelteTabs.svelte", + "tests/consumers/frameworks.ts", + "tests/consumers/fixtures/svelte/pnpm-lock.yaml", + "tests/consumers/fixtures/svelte/index.html", + "tests/consumers/fixtures/svelte/Consumer.svelte", + "tests/consumers/fixtures/svelte/model.ts", + "tests/consumers/fixtures/svelte/package.json", + "tests/consumers/fixtures/svelte/client.ts", + "tests/consumers/fixtures/svelte/tsconfig.json", + "tests/consumers/fixtures/svelte/pnpm-workspace.yaml", + "tests/consumers/fixtures/vue/pnpm-lock.yaml", + "tests/consumers/fixtures/vue/index.html", + "tests/consumers/fixtures/vue/model.ts", + "tests/consumers/fixtures/vue/package.json", + "tests/consumers/fixtures/vue/client.ts", + "tests/consumers/fixtures/vue/tsconfig.json", + "tests/consumers/fixtures/vue/pnpm-workspace.yaml", + "tests/consumers/fixtures/angular/pnpm-lock.yaml", + "tests/consumers/fixtures/angular/index.html", + "tests/consumers/fixtures/angular/model.ts", + "tests/consumers/fixtures/angular/package.json", + "tests/consumers/fixtures/angular/client.ts", + "tests/consumers/fixtures/angular/tsconfig.json", + "tests/consumers/fixtures/angular/pnpm-workspace.yaml", + "tests/consumers/fixtures/react/pnpm-lock.yaml", + "tests/consumers/fixtures/react/index.html", + "tests/consumers/fixtures/react/model.ts", + "tests/consumers/fixtures/react/package.json", + "tests/consumers/fixtures/react/client.ts", + "tests/consumers/fixtures/react/tsconfig.json", + "tests/consumers/fixtures/react/pnpm-workspace.yaml" ], "documentation": [ "README.md", "docs/component-library.md", "docs/compatibility.md", "docs/provenance.md", - "docs/evidence/shared-ui-foundation.json" + "docs/evidence/shared-ui-foundation.json", + "docs/framework-adapters.md" ], "readiness": { "status": "draft", @@ -50,7 +97,7 @@ "Storybook Pages delivery and human UX review", "Actual Gateway and Domus adoption" ], - "implementationEvidence": "partial: closed data contract and controlled HA-style navigation/panel renderer; strict Node/Chromium coverage and clean actual packed DOM-free/browser consumers pass. Other families, adapters, complete framework/browser matrix, Storybook, publication and app adoption remain required." + "implementationEvidence": "partial: data contract and one shared navigation/panel family, four real navigation bindings and reproducible five-package cohort. Clean frozen offline consumers compile and run props/events/content/disabled/theme/locale/keyboard/remount in Chromium. Other families, form bridges, framework SSR, full browser matrix, Storybook, publication and app adoption remain required." } } ] diff --git a/specs/shared-ui-framework-delivery.md b/specs/shared-ui-framework-delivery.md index d0c965f..7186345 100644 --- a/specs/shared-ui-framework-delivery.md +++ b/specs/shared-ui-framework-delivery.md @@ -363,6 +363,57 @@ Prospective repository files (current documents are classified against the immut | `tooling/packaging.mjs` | create | | `tooling/compatibility.mjs` | create | | `tests/delivery.test.mjs` | create | +| `docs/framework-adapters.md` | create | +| `packages/angular/LICENSE` | create | +| `packages/angular/ng-package.json` | create | +| `packages/angular/tsconfig.json` | create | +| `packages/react/LICENSE` | create | +| `packages/react/tsconfig.json` | create | +| `packages/svelte/LICENSE` | create | +| `packages/svelte/src/types.ts` | create | +| `packages/svelte/svelte.config.js` | create | +| `packages/svelte/tsconfig.json` | create | +| `packages/vue/LICENSE` | create | +| `packages/vue/tsconfig.json` | create | +| `tests/browser/angular.test.ts` | create | +| `tests/browser/fixtures/SvelteTabs.svelte` | create | +| `tests/browser/framework-fixture.ts` | create | +| `tests/browser/react.test.ts` | create | +| `tests/browser/svelte.test.mjs` | create | +| `tests/browser/vue.test.ts` | create | +| `tests/consumers/fixtures/angular/client.ts` | create | +| `tests/consumers/fixtures/angular/index.html` | create | +| `tests/consumers/fixtures/angular/model.ts` | create | +| `tests/consumers/fixtures/angular/package.json` | create | +| `tests/consumers/fixtures/angular/pnpm-lock.yaml` | create | +| `tests/consumers/fixtures/angular/pnpm-workspace.yaml` | create | +| `tests/consumers/fixtures/angular/tsconfig.json` | create | +| `tests/consumers/fixtures/react/client.ts` | create | +| `tests/consumers/fixtures/react/index.html` | create | +| `tests/consumers/fixtures/react/model.ts` | create | +| `tests/consumers/fixtures/react/package.json` | create | +| `tests/consumers/fixtures/react/pnpm-lock.yaml` | create | +| `tests/consumers/fixtures/react/pnpm-workspace.yaml` | create | +| `tests/consumers/fixtures/react/tsconfig.json` | create | +| `tests/consumers/fixtures/svelte/Consumer.svelte` | create | +| `tests/consumers/fixtures/svelte/client.ts` | create | +| `tests/consumers/fixtures/svelte/index.html` | create | +| `tests/consumers/fixtures/svelte/model.ts` | create | +| `tests/consumers/fixtures/svelte/package.json` | create | +| `tests/consumers/fixtures/svelte/pnpm-lock.yaml` | create | +| `tests/consumers/fixtures/svelte/pnpm-workspace.yaml` | create | +| `tests/consumers/fixtures/svelte/tsconfig.json` | create | +| `tests/consumers/fixtures/vue/client.ts` | create | +| `tests/consumers/fixtures/vue/index.html` | create | +| `tests/consumers/fixtures/vue/model.ts` | create | +| `tests/consumers/fixtures/vue/package.json` | create | +| `tests/consumers/fixtures/vue/pnpm-lock.yaml` | create | +| `tests/consumers/fixtures/vue/pnpm-workspace.yaml` | create | +| `tests/consumers/fixtures/vue/tsconfig.json` | create | +| `tests/consumers/frameworks.ts` | create | +| `tsconfig.consumers.json` | create | +| `tsconfig.svelte-tests.json` | create | +| `tsconfig.svelte-source.json` | create | The catalog/index update registers the companion under the existing owner; it does not claim implementation. `LICENSE` preserves the upstream MIT source notice and every reused asset requires provenance verification. The Copilot installation and its source remain unchanged. Package coordinates are identifiers, not repository paths. @@ -419,4 +470,4 @@ The retained foundation's requirements and cases remain traced in `specs/shared- - Consumer adoption: [vypdev/domus issue #30](https://github.com/vypdev/domus/issues/30); Gateway work remains with its owning admitted work. - Gateway source evidence: [presentation directory at cited revision](https://github.com/vypdev/homeassistant-gateway/tree/e47dde303e475a6d03e1d6697ec87a934e93263b/frontend/src/ui). - Decisions: one Lit implementation behind DOM contracts and framework adapters; no HA private runtime dependency; candidate testing precedes publication; both applications exercise the same immutable candidate before rights-verified release; publication and application adoption remain separate evidence. -- Follow-up outside this companion: none. All product and component acceptance remains governed by the retained foundation specification. \ No newline at end of file +- Follow-up outside this companion: none. All product and component acceptance remains governed by the retained foundation specification. diff --git a/tests/architecture.test.mjs b/tests/architecture.test.mjs index 3e7d5f4..a6ee303 100644 --- a/tests/architecture.test.mjs +++ b/tests/architecture.test.mjs @@ -58,3 +58,56 @@ test('test configuration covers every package and authored tool per file without const manifest = JSON.parse(readFileSync('package.json', 'utf8')); expect(manifest.scripts.test).toBe('vitest run --coverage && node --test tests/coverage-evidence.test.mjs'); }); + +const adapterFrameworks={react:'react',vue:'vue',angular:'@angular/core',svelte:'svelte'}; +function adapterImport(framework,specifier) { + if (specifier==='@vypdev/ha-ui' || specifier.startsWith('@vypdev/ha-ui/')) return true; + if (specifier.startsWith('.')) return true; + const own=adapterFrameworks[framework]; + return specifier===own || specifier.startsWith(own+'/'); +} +test('each adapter imports only its own framework, local bindings and the shared core',()=>{ + for (const framework of Object.keys(adapterFrameworks)) for (const file of globSync('packages/'+framework+'/src/**/*.{ts,svelte}')) { + const original=readFileSync(file,'utf8'); + const code=file.endsWith('.svelte')?original.match(/]*>([\s\S]*?)<\/script>/u)[1]:original; + for (const dependency of imports(code)) { + expect(adapterImport(framework,dependency),file+': '+dependency).toBe(true); + if (dependency.startsWith('.')) expect(relative(resolve('packages',framework,'src'),resolve(dirname(file),dependency)).startsWith('..')).toBe(false); + } + expect(original).not.toMatch(/]|]/u); + } +}); +test('adapter direction rejects other frameworks, adapters, application code and server I/O',()=>{ + for (const [framework,dependency] of [['react','vue'],['vue','@vypdev/ha-ui-react'],['angular','lit'],['svelte','@angular/core'],['react','domus'],['vue','node:fs'],['angular','homeassistant-gateway']]) + expect(adapterImport(framework,dependency)).toBe(false); +}); +test('framework entrypoints never import the core DOM renderer at runtime or register tags',()=>{ + for (const framework of Object.keys(adapterFrameworks)) for (const file of globSync('packages/'+framework+'/src/**/*.{ts,svelte}')) { + const original=readFileSync(file,'utf8'); + const code=file.endsWith('.svelte')?original.match(/]*>([\s\S]*?)<\/script>/u)[1]:original; + const parsed=ts.createSourceFile('source.ts',code,ts.ScriptTarget.Latest,true); + for (const node of parsed.statements) if (ts.isImportDeclaration(node) && node.moduleSpecifier.text.startsWith('@vypdev/ha-ui')) + expect(node.importClause.isTypeOnly,file).toBe(true); + expect(code).not.toMatch(/customElements|registerTabs/u); + } +}); +test('adapter manifests keep other frameworks out and retain the exact shared cohort',()=>{ + for (const [framework,peer] of Object.entries(adapterFrameworks)) { + const manifest=JSON.parse(readFileSync('packages/'+framework+'/package.json','utf8')); + expect(Object.keys(manifest.peerDependencies)).toEqual([peer]); + expect(manifest.dependencies['@vypdev/ha-ui']).toBe(framework==='angular'?'0.1.0-next.1':'workspace:*'); + expect(Object.keys(manifest.dependencies).sort()).toEqual(framework==='angular'?['@vypdev/ha-ui','tslib']:['@vypdev/ha-ui']); + expect(manifest.version).toBe(JSON.parse(readFileSync('packages/core/package.json','utf8')).version); + } +}); + +test('strict checks cover the source, Node consumer tooling and every Svelte fixture',()=>{ + const manifest=JSON.parse(readFileSync('package.json','utf8')); + for (const command of ['tsc --noEmit -p tsconfig.json','tsc --noEmit -p tsconfig.consumers.json','svelte-check --tsconfig tsconfig.svelte-source.json','svelte-check --tsconfig tsconfig.svelte-tests.json'])expect(manifest.scripts.check).toContain(command); + const source=JSON.parse(readFileSync('tsconfig.json','utf8')); + expect(source.compilerOptions.strict).toBe(true);expect(source.compilerOptions.skipLibCheck).toBe(false); + for (const framework of Object.keys(adapterFrameworks)) { + const fixture=JSON.parse(readFileSync('tests/consumers/fixtures/'+framework+'/tsconfig.json','utf8')); + expect(fixture.compilerOptions.strict).toBe(true);expect(fixture.compilerOptions.skipLibCheck).toBe(false); + } +}); diff --git a/tests/browser/angular.test.ts b/tests/browser/angular.test.ts new file mode 100644 index 0000000..6b560d7 --- /dev/null +++ b/tests/browser/angular.test.ts @@ -0,0 +1,35 @@ +import '@angular/compiler'; +import {afterEach, expect, test, vi} from 'vitest'; +import {page} from 'vitest/browser'; +import {Component,signal} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; +import {BrowserTestingModule,platformBrowserTesting} from '@angular/platform-browser/testing'; +import {Tabs} from '../../packages/angular/src/index.js'; +import {registerTabs, type NavigationSelectionEvent} from '../../packages/core/src/components/tabs.js'; +import {panelState,view,tab} from './framework-fixture.js'; +TestBed.initTestEnvironment(BrowserTestingModule,platformBrowserTesting()); +@Component({standalone:true,imports:[Tabs],template:''}) +class Consumer { + state=signal(panelState()); selected=vi.fn((event:NavigationSelectionEvent)=>event.preventDefault()); +} +afterEach(()=>{TestBed.resetTestingModule();document.body.replaceChildren();}); +test('mounts the actual core tag as its host and preserves pre-upgrade properties',async()=>{ + const fixture=TestBed.createComponent(Consumer);fixture.detectChanges();const element=view(); + expect(customElements.get('vyp-ha-tabs')).toBeUndefined();expect(element.state.kind).toBe('panels'); + registerTabs();await element.updateComplete;expect(tab(element,'overview').textContent).toBe('Overview'); + expect(document.querySelectorAll('vyp-ha-tabs')).toHaveLength(1);expect(element.hasAttribute('state')).toBe(false); +}); +test('Angular input/output and projected content delegate to the same controlled core',async()=>{ + registerTabs();const fixture=TestBed.createComponent(Consumer);fixture.detectChanges();const element=view();await element.updateComplete; + fixture.componentInstance.state.set(panelState(true));fixture.detectChanges();await element.updateComplete; + await page.getByRole('tab',{name:'Tareas',exact:true}).click(); + expect(fixture.componentInstance.selected).toHaveBeenCalledTimes(1); + const event=fixture.componentInstance.selected.mock.calls[0][0];expect(event.detail).toEqual({kind:'panels',id:'tasks'});expect(event.defaultPrevented).toBe(true); + expect(element.state.selected).toBe('overview'); + fixture.componentInstance.state.set({...element.state,selected:'tasks'});fixture.detectChanges();await element.updateComplete; + await expect.element(page.getByRole('button',{name:'Add task',exact:true})).toBeVisible(); +}); +test('destroy releases its renderer-owned listener before the old host can emit again',async()=>{ + registerTabs();const fixture=TestBed.createComponent(Consumer);fixture.detectChanges();const element=view();await element.updateComplete; + const handler=fixture.componentInstance.selected;fixture.destroy();tab(element,'tasks').click();expect(handler).not.toHaveBeenCalled(); +}); diff --git a/tests/browser/fixtures/SvelteTabs.svelte b/tests/browser/fixtures/SvelteTabs.svelte new file mode 100644 index 0000000..febb081 --- /dev/null +++ b/tests/browser/fixtures/SvelteTabs.svelte @@ -0,0 +1,15 @@ + +{#snippet tasks()}{/snippet} + diff --git a/tests/browser/framework-fixture.ts b/tests/browser/framework-fixture.ts new file mode 100644 index 0000000..d58be00 --- /dev/null +++ b/tests/browser/framework-fixture.ts @@ -0,0 +1,5 @@ +import {navigationState} from '../../packages/core/src/contracts.js'; +import type {HaTabs} from '../../packages/core/src/components/tabs.js'; +export const panelState = (spanish=false) => navigationState({kind:'panels',label:spanish?'Secciones del hogar':'Household sections',selected:'overview',items:[{id:'overview',label:spanish?'Resumen':'Overview'},{id:'locked',label:spanish?'Bloqueado':'Locked',disabled:true},{id:'tasks',label:spanish?'Tareas':'Tasks'}]}); +export const view = () => document.querySelector('vyp-ha-tabs')!; +export const tab = (element: HaTabs, id: string) => element.shadowRoot!.querySelector('#tab-'+id)!; diff --git a/tests/browser/react.test.ts b/tests/browser/react.test.ts new file mode 100644 index 0000000..54ea673 --- /dev/null +++ b/tests/browser/react.test.ts @@ -0,0 +1,40 @@ +import {afterEach, expect, test, vi} from 'vitest'; +import {page} from 'vitest/browser'; +import {act, createElement, StrictMode} from 'react'; +import {createRoot, type Root} from 'react-dom/client'; +import {Tabs} from '../../packages/react/src/index.js'; +import {registerTabs, type NavigationSelectionEvent} from '../../packages/core/src/components/tabs.js'; +import {panelState, view, tab} from './framework-fixture.js'; +let root: Root | undefined; +vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT',true); +afterEach(async()=>{await act(()=>root?.unmount());root=undefined;document.body.replaceChildren();}); +function render(state=panelState(), handler?: (event:NavigationSelectionEvent)=>void, content=true) { + if (!root) { const target=document.createElement('main');document.body.append(target);root=createRoot(target); } + return act(()=>root!.render(createElement(StrictMode,null,createElement(Tabs,{state,onSelectionRequest:handler,id:'react-tabs',className:'household',lang:state.label==='Household sections'?'en':'es',dir:'ltr',style:{'--primary-color':'rgb(0,110,180)'} as React.CSSProperties},content?createElement('button',{slot:'tasks'},'Add task'):undefined)))); +} +test('mounts before explicit registration, preserving complex properties through upgrade',async()=>{ + const state=panelState(); await render(state);const element=view(); + expect(customElements.get('vyp-ha-tabs')).toBeUndefined();expect(element.state).toBe(state); + registerTabs();await element.updateComplete;expect(tab(element,'overview').textContent).toBe('Overview'); + expect(element.id).toBe('react-tabs');expect(element.className).toBe('household');expect(element.hasAttribute('state')).toBe(false); +}); +test('Strict Mode, rerenders and native cancellation yield one current callback',async()=>{ + registerTabs();const old=vi.fn(),current=vi.fn((event:NavigationSelectionEvent)=>event.preventDefault()); + await render(panelState(),old);const element=view();await element.updateComplete; + await render(panelState(true),current);await element.updateComplete;expect(view()).toBe(element); + await page.getByRole('tab',{name:'Tareas',exact:true}).click(); + expect(old).not.toHaveBeenCalled();expect(current).toHaveBeenCalledTimes(1); + const event=current.mock.calls[0][0];expect(event.detail).toEqual({kind:'panels',id:'tasks'});expect(event.defaultPrevented).toBe(true); + expect(element.state.selected).toBe('overview');expect(element.lang).toBe('es'); + await render({...panelState(true),selected:'tasks'},current);await element.updateComplete;await expect.element(page.getByRole('button',{name:'Add task',exact:true})).toBeVisible(); +}); +test('callback removal and slot removal do not retain old handlers or content',async()=>{ + registerTabs();const handler=vi.fn();await render(panelState(),handler);const element=view();await element.updateComplete; + await render(panelState(),undefined,false);await element.updateComplete; + await page.getByRole('tab',{name:'Tasks',exact:true}).click();expect(handler).not.toHaveBeenCalled();expect(element.children).toHaveLength(0); +}); +test('unmount removes its native subscription from a retained former host',async()=>{ + registerTabs();const handler=vi.fn();await render(panelState(),handler);const element=view();await element.updateComplete; + await act(()=>root!.unmount());root=undefined; + tab(element,'tasks').click();expect(handler).not.toHaveBeenCalled(); +}); diff --git a/tests/browser/svelte.test.mjs b/tests/browser/svelte.test.mjs new file mode 100644 index 0000000..59e45c3 --- /dev/null +++ b/tests/browser/svelte.test.mjs @@ -0,0 +1,30 @@ +import {afterEach,expect,test,vi} from 'vitest'; +import {page} from 'vitest/browser'; +import {mount,unmount,flushSync} from 'svelte'; +import Consumer from './fixtures/SvelteTabs.svelte'; +import Tabs from '../../packages/svelte/src/Tabs.svelte'; +import {registerTabs} from '../../packages/core/src/components/tabs.js'; +import {panelState,view,tab} from './framework-fixture.js'; +let instance; +afterEach(async()=>{if(instance)await unmount(instance);instance=undefined;document.body.replaceChildren();}); +function attach(component,props){const target=document.createElement('main');document.body.append(target);instance=mount(component,{target,props});flushSync();return instance;} +test('compiled Svelte preserves pre-upgrade properties without serialized objects',async()=>{ + attach(Consumer,{initial:panelState()});const element=view();expect(customElements.get('vyp-ha-tabs')).toBeUndefined();expect(element.state.kind).toBe('panels'); + registerTabs();await element.updateComplete;expect(tab(element,'overview').textContent).toBe('Overview');expect(element.id).toBe('svelte-tabs');expect(element.className).toBe('household');expect(element.hasAttribute('state')).toBe(false); +}); +test('rune props, callbacks and snippet content remain controlled in the real component',async()=>{ + registerTabs();const observe=vi.fn(event=>event.preventDefault());const consumer=attach(Consumer,{initial:panelState(),observe});const element=view();await element.updateComplete; + flushSync(()=>consumer.update(panelState(true)));await element.updateComplete;expect(view()).toBe(element); + await page.getByRole('tab',{name:'Tareas',exact:true}).click();expect(observe).toHaveBeenCalledTimes(1);expect(observe.mock.calls[0][0].defaultPrevented).toBe(true);expect(observe.mock.calls[0][0].detail).toEqual({kind:'panels',id:'tasks'});expect(element.state.selected).toBe('overview'); + flushSync(()=>consumer.update({...element.state,selected:'tasks'}));await element.updateComplete;await expect.element(page.getByRole('button',{name:'Add task',exact:true})).toBeVisible(); + flushSync(()=>consumer.clearPanels());expect(element.children).toHaveLength(0); + flushSync(()=>consumer.clearHandler());await page.getByRole('tab',{name:'Resumen',exact:true}).click();expect(observe).toHaveBeenCalledTimes(1); +}); +test('no callback or content is required and no alternate visuals are rendered',async()=>{ + registerTabs();attach(Tabs,{state:panelState()});const element=view();await element.updateComplete; + await page.getByRole('tab',{name:'Tasks',exact:true}).click();expect(element.children).toHaveLength(0);expect(element.state.selected).toBe('overview'); +}); +test('unmount removes the native callback on a retained old host',async()=>{ + registerTabs();const observe=vi.fn();attach(Consumer,{initial:panelState(),observe});const element=view();await element.updateComplete; + await unmount(instance);instance=undefined;tab(element,'tasks').click();expect(observe).not.toHaveBeenCalled(); +}); diff --git a/tests/browser/vue.test.ts b/tests/browser/vue.test.ts new file mode 100644 index 0000000..53de5fe --- /dev/null +++ b/tests/browser/vue.test.ts @@ -0,0 +1,38 @@ +import {afterEach, expect, test, vi} from 'vitest'; +import {page} from 'vitest/browser'; +import {createApp, h, nextTick, shallowRef, type App} from 'vue'; +import {Tabs} from '../../packages/vue/src/index.js'; +import {registerTabs, type NavigationSelectionEvent} from '../../packages/core/src/components/tabs.js'; +import {panelState, view, tab} from './framework-fixture.js'; +let app: App | undefined; +afterEach(()=>{app?.unmount();app=undefined;document.body.replaceChildren();}); +function mount(handler?: (event:NavigationSelectionEvent)=>void, content=true) { + const state=shallowRef(panelState()),listener=shallowRef(handler),children=shallowRef(content); + const target=document.createElement('main');document.body.append(target); + app=createApp({render:()=>h(Tabs,{state:state.value,onSelectionRequest:listener.value,id:'vue-tabs',class:'household',dir:'ltr',lang:state.value.label==='Household sections'?'en':'es'},children.value?{default:()=>h('button',{slot:'tasks'},'Add task')}:undefined)}); + app.mount(target);return {state,listener,children}; +} +test('preserves pre-upgrade property state with no serialized object attribute',async()=>{ + mount();const element=view();expect(customElements.get('vyp-ha-tabs')).toBeUndefined(); + expect(element.state.kind).toBe('panels');registerTabs();await element.updateComplete; + expect(element.id).toBe('vue-tabs');expect(element.className).toBe('household');expect(element.hasAttribute('state')).toBe(false);expect(tab(element,'overview').textContent).toBe('Overview'); +}); +test('prop and callback replacements keep one current event and native cancellation',async()=>{ + registerTabs();const old=vi.fn(),current=vi.fn((event:NavigationSelectionEvent)=>event.preventDefault()); + const input=mount(old);const element=view();await element.updateComplete; + input.state.value=panelState(true);input.listener.value=current;await nextTick();await element.updateComplete; + expect(view()).toBe(element);await page.getByRole('tab',{name:'Tareas',exact:true}).click(); + expect(old).not.toHaveBeenCalled();expect(current).toHaveBeenCalledTimes(1);expect(current.mock.calls[0][0].defaultPrevented).toBe(true); + expect(current.mock.calls[0][0].detail).toEqual({kind:'panels',id:'tasks'});expect(element.state.selected).toBe('overview'); + input.state.value={...input.state.value,selected:'tasks'};await nextTick();await element.updateComplete; + await expect.element(page.getByRole('button',{name:'Add task',exact:true})).toBeVisible(); +}); +test('optional slots and callback removal stay controlled',async()=>{ + registerTabs();const handler=vi.fn();const input=mount(handler);const element=view();await element.updateComplete; + input.listener.value=undefined;input.children.value=false;await nextTick();await element.updateComplete; + await page.getByRole('tab',{name:'Tasks',exact:true}).click();expect(handler).not.toHaveBeenCalled();expect(element.children).toHaveLength(0); +}); +test('unmount releases the native listener even if the host is retained',async()=>{ + registerTabs();const handler=vi.fn();mount(handler);const element=view();await element.updateComplete; + app!.unmount();app=undefined;tab(element,'tasks').click();expect(handler).not.toHaveBeenCalled(); +}); diff --git a/tests/consumers/fixtures/angular/client.ts b/tests/consumers/fixtures/angular/client.ts new file mode 100644 index 0000000..9a2078d --- /dev/null +++ b/tests/consumers/fixtures/angular/client.ts @@ -0,0 +1,20 @@ +import {Component,signal} from '@angular/core'; +import {bootstrapApplication} from '@angular/platform-browser'; +import {Tabs} from '@vypdev/ha-ui-angular'; +import {registerTabs,type NavigationSelectionEvent} from '@vypdev/ha-ui/tabs'; +import {model} from './model.js'; +registerTabs(); +@Component({selector:'fixture-app',standalone:true,imports:[Tabs],template:` +
+ + + {{count()}} + @if(visible()){} +
`}) +class Consumer { + state=signal(model());spanish=signal(false);dark=signal(false);disabled=signal(false);visible=signal(true);count=signal(0); + select(event:NavigationSelectionEvent){event.preventDefault();this.state.set(model(this.spanish(),event.detail.id,this.disabled()));this.count.update(value=>value+1);} + language(){this.spanish.set(!this.spanish());this.state.set(model(this.spanish(),this.state().selected!,this.disabled()));} + toggleDisabled(){this.disabled.set(!this.disabled());this.state.set(model(this.spanish(),'overview',this.disabled()));} +} +void bootstrapApplication(Consumer); diff --git a/tests/consumers/fixtures/angular/index.html b/tests/consumers/fixtures/angular/index.html new file mode 100644 index 0000000..520921e --- /dev/null +++ b/tests/consumers/fixtures/angular/index.html @@ -0,0 +1 @@ +Compiled framework consumer diff --git a/tests/consumers/fixtures/angular/model.ts b/tests/consumers/fixtures/angular/model.ts new file mode 100644 index 0000000..91b2d4a --- /dev/null +++ b/tests/consumers/fixtures/angular/model.ts @@ -0,0 +1,5 @@ +import {navigationState} from '@vypdev/ha-ui/contracts'; +import type {NavigationState} from '@vypdev/ha-ui/contracts'; +export function model(spanish=false,selected='overview',disabled=false):NavigationState { + return navigationState({kind:'panels',label:spanish?'Secciones del hogar':'Household sections',selected,items:[{id:'overview',label:spanish?'Resumen':'Overview'},{id:'tasks',label:spanish?'Tareas':'Tasks',disabled}]}); +} diff --git a/tests/consumers/fixtures/angular/package.json b/tests/consumers/fixtures/angular/package.json new file mode 100644 index 0000000..f732641 --- /dev/null +++ b/tests/consumers/fixtures/angular/package.json @@ -0,0 +1,19 @@ +{ + "name": "ha-ui-angular-consumer", + "private": true, + "type": "module", + "packageManager": "pnpm@10.34.0", + "dependencies": { + "@vypdev/ha-ui": "file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz", + "@vypdev/ha-ui-angular": "file:../pack-1/vypdev-ha-ui-angular-0.1.0-next.1.tgz", + "@angular/core": "22.2.4", + "@angular/common": "22.2.4", + "@angular/compiler": "22.2.4", + "@angular/platform-browser": "22.2.4", + "rxjs": "7.8.2", + "tslib": "2.8.1" + }, + "devDependencies": { + "typescript": "6.0.3" + } +} diff --git a/tests/consumers/fixtures/angular/pnpm-lock.yaml b/tests/consumers/fixtures/angular/pnpm-lock.yaml new file mode 100644 index 0000000..0a4c334 --- /dev/null +++ b/tests/consumers/fixtures/angular/pnpm-lock.yaml @@ -0,0 +1,184 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: false + excludeLinksFromLockfile: false + +overrides: + '@vypdev/ha-ui': file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz + +importers: + + .: + dependencies: + '@angular/common': + specifier: 22.2.4 + version: 22.2.4(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))(rxjs@7.8.2) + '@angular/compiler': + specifier: 22.2.4 + version: 22.2.4 + '@angular/core': + specifier: 22.2.4 + version: 22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2) + '@angular/platform-browser': + specifier: 22.2.4 + version: 22.2.4(@angular/common@22.2.4(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2)) + '@vypdev/ha-ui': + specifier: file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz + version: file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz + '@vypdev/ha-ui-angular': + specifier: file:../pack-1/vypdev-ha-ui-angular-0.1.0-next.1.tgz + version: file:../pack-1/vypdev-ha-ui-angular-0.1.0-next.1.tgz(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2)) + rxjs: + specifier: 7.8.2 + version: 7.8.2 + tslib: + specifier: 2.8.1 + version: 2.8.1 + devDependencies: + typescript: + specifier: 6.0.3 + version: 6.0.3 + +packages: + + '@angular/common@22.2.4': + resolution: {integrity: sha512-i5oosToLSAXumbkOF5pZyti0W0jrfRppj4y6a913f+AY4Em+iHT87JIOKrfybR7vkHs0eceZnpS0CpzylVAvgA==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} + peerDependencies: + '@angular/core': 22.2.4 + rxjs: ^6.5.3 || ^7.4.0 + + '@angular/compiler@22.2.4': + resolution: {integrity: sha512-Az14b7hE1iCMqNhskOjA28+8w3VQDhSkrVUU+2gyucDHey1qw9rX5x45/z3qNanz4VtTBShvqEDsjctkobKJSA==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} + + '@angular/core@22.2.4': + resolution: {integrity: sha512-FME4hPQ6+RbOkaRtUHWytwyAFXZYjIQ81TDJ3Dj7tx+vzKs0StpH9g/0AoZwQUDmhPlN84IazBimQ7II7lmwVw==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} + peerDependencies: + '@angular/compiler': 22.2.4 + rxjs: ^6.5.3 || ^7.4.0 + zone.js: ~0.15.0 || ~0.16.0 + peerDependenciesMeta: + '@angular/compiler': + optional: true + zone.js: + optional: true + + '@angular/platform-browser@22.2.4': + resolution: {integrity: sha512-NAQ8/Zcl8nRlNqxlXQGrQJ/c5V8lN5vJSlc8qkD3OUN+ClyNuS6UCXffYtKvvbovpyb/q1uYSQjqiEctz+5niw==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} + peerDependencies: + '@angular/animations': 22.2.4 + '@angular/common': 22.2.4 + '@angular/core': 22.2.4 + peerDependenciesMeta: + '@angular/animations': + optional: true + + '@lit-labs/ssr-dom-shim@1.6.0': + resolution: {integrity: sha512-VHb0ALPMTlgKjM6yIxxoQNnpKyUKLD04VzeQdsiXkMqkvYlAHxq9glGLmgbb889/1GsohSOAjvQYoiBppXFqrQ==} + + '@lit/reactive-element@2.1.2': + resolution: {integrity: sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@vypdev/ha-ui-angular@file:../pack-1/vypdev-ha-ui-angular-0.1.0-next.1.tgz': + resolution: {integrity: sha512-s7DoeAOD/Nm4EPpQzIT2C0rSnsRt6LdHm1/4qZADOBacJSeXUQDZfG8hLMiTn+L4w2u3nqZ6luN/hywOGyzgqg==, tarball: file:../pack-1/vypdev-ha-ui-angular-0.1.0-next.1.tgz} + version: 0.1.0-next.1 + peerDependencies: + '@angular/core': 22.2.4 + + '@vypdev/ha-ui@file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz': + resolution: {integrity: sha512-dGtzvFkv/M6fOZGQVI47SpL+jW3yzHRvZ9JFVqKSJ0Tp/JAl2anc9/kqe0PX55Yu+FUVHsjH4o/BoMtIG6kFOw==, tarball: file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz} + version: 0.1.0-next.1 + + lit-element@4.2.2: + resolution: {integrity: sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==} + + lit-html@3.3.3: + resolution: {integrity: sha512-el8M6jK2o3RXBnrSHX3ZKrsN8zEV63pSExTO1wYJz7QndGYZ8353e2a5PPX+qHe2aGayfnchQmkAojaWAREOIA==} + + lit@3.3.3: + resolution: {integrity: sha512-fycuvZg/hkpozL00lm1pEJH5nN/lr9ZXd6mJI2HSN4+Bzc+LDNdEApJ6HFbPkdFNHLvOplIIuJvxkS4XUxqirw==} + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + +snapshots: + + '@angular/common@22.2.4(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))(rxjs@7.8.2)': + dependencies: + '@angular/core': 22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2) + rxjs: 7.8.2 + tslib: 2.8.1 + + '@angular/compiler@22.2.4': + dependencies: + tslib: 2.8.1 + + '@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2)': + dependencies: + rxjs: 7.8.2 + tslib: 2.8.1 + optionalDependencies: + '@angular/compiler': 22.2.4 + + '@angular/platform-browser@22.2.4(@angular/common@22.2.4(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))': + dependencies: + '@angular/common': 22.2.4(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))(rxjs@7.8.2) + '@angular/core': 22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2) + tslib: 2.8.1 + + '@lit-labs/ssr-dom-shim@1.6.0': {} + + '@lit/reactive-element@2.1.2': + dependencies: + '@lit-labs/ssr-dom-shim': 1.6.0 + + '@types/trusted-types@2.0.7': {} + + '@vypdev/ha-ui-angular@file:../pack-1/vypdev-ha-ui-angular-0.1.0-next.1.tgz(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))': + dependencies: + '@angular/core': 22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2) + '@vypdev/ha-ui': file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz + tslib: 2.8.1 + + '@vypdev/ha-ui@file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz': + dependencies: + lit: 3.3.3 + + lit-element@4.2.2: + dependencies: + '@lit-labs/ssr-dom-shim': 1.6.0 + '@lit/reactive-element': 2.1.2 + lit-html: 3.3.3 + + lit-html@3.3.3: + dependencies: + '@types/trusted-types': 2.0.7 + + lit@3.3.3: + dependencies: + '@lit/reactive-element': 2.1.2 + lit-element: 4.2.2 + lit-html: 3.3.3 + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + tslib@2.8.1: {} + + typescript@6.0.3: {} diff --git a/tests/consumers/fixtures/angular/pnpm-workspace.yaml b/tests/consumers/fixtures/angular/pnpm-workspace.yaml new file mode 100644 index 0000000..f2eddb4 --- /dev/null +++ b/tests/consumers/fixtures/angular/pnpm-workspace.yaml @@ -0,0 +1,4 @@ +packages: [] +autoInstallPeers: false +overrides: + "@vypdev/ha-ui": file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz diff --git a/tests/consumers/fixtures/angular/tsconfig.json b/tests/consumers/fixtures/angular/tsconfig.json new file mode 100644 index 0000000..5f72be8 --- /dev/null +++ b/tests/consumers/fixtures/angular/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2024", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noEmit": false, + "skipLibCheck": false, + "lib": [ + "ES2024", + "DOM" + ], + "types": [], + "outDir": "compiled", + "rootDir": ".", + "experimentalDecorators": true, + "useDefineForClassFields": false + }, + "include": [ + "*.ts" + ], + "angularCompilerOptions": { + "compilationMode": "full", + "strictTemplates": true + } +} diff --git a/tests/consumers/fixtures/react/client.ts b/tests/consumers/fixtures/react/client.ts new file mode 100644 index 0000000..e7d5db5 --- /dev/null +++ b/tests/consumers/fixtures/react/client.ts @@ -0,0 +1,19 @@ +import {createElement,useState,type CSSProperties} from 'react'; +import {createRoot} from 'react-dom/client'; +import {Tabs} from '@vypdev/ha-ui-react'; +import {registerTabs,type NavigationSelectionEvent} from '@vypdev/ha-ui/tabs'; +import {model} from './model.js'; +registerTabs(); +function Consumer() { + const [state,setState]=useState(model()),[spanish,setSpanish]=useState(false),[dark,setDark]=useState(false),[disabled,setDisabled]=useState(false),[visible,setVisible]=useState(true),[count,setCount]=useState(0); + const select=(event:NavigationSelectionEvent)=>{event.preventDefault();setState(model(spanish,event.detail.id,disabled));setCount(value=>value+1);}; + const language=()=>{setSpanish(!spanish);setState(model(!spanish,state.selected!,disabled));}; + const toggleDisabled=()=>{setDisabled(!disabled);setState(model(spanish,'overview',!disabled));}; + const style={'--primary-text-color':dark?'rgb(235,235,235)':'rgb(28,28,28)','--secondary-text-color':dark?'rgb(189,189,189)':'rgb(89,89,89)','--card-background-color':dark?'rgb(20,20,20)':'rgb(255,255,255)'} as CSSProperties; + return createElement('section',{style}, + createElement('button',{onClick:language},'Switch language'),createElement('button',{onClick:()=>setDark(!dark)},'Toggle theme'), + createElement('button',{onClick:toggleDisabled},'Toggle disabled'),createElement('button',{onClick:()=>setVisible(!visible)},'Toggle component'), + createElement('output',{'aria-label':'Selection events'},String(count)), + visible?createElement(Tabs,{state,onSelectionRequest:select},createElement('button',{slot:'tasks'},'Add task')):null); +} +createRoot(document.querySelector('main')!).render(createElement(Consumer)); diff --git a/tests/consumers/fixtures/react/index.html b/tests/consumers/fixtures/react/index.html new file mode 100644 index 0000000..4599a8d --- /dev/null +++ b/tests/consumers/fixtures/react/index.html @@ -0,0 +1 @@ +Compiled framework consumer
diff --git a/tests/consumers/fixtures/react/model.ts b/tests/consumers/fixtures/react/model.ts new file mode 100644 index 0000000..91b2d4a --- /dev/null +++ b/tests/consumers/fixtures/react/model.ts @@ -0,0 +1,5 @@ +import {navigationState} from '@vypdev/ha-ui/contracts'; +import type {NavigationState} from '@vypdev/ha-ui/contracts'; +export function model(spanish=false,selected='overview',disabled=false):NavigationState { + return navigationState({kind:'panels',label:spanish?'Secciones del hogar':'Household sections',selected,items:[{id:'overview',label:spanish?'Resumen':'Overview'},{id:'tasks',label:spanish?'Tareas':'Tasks',disabled}]}); +} diff --git a/tests/consumers/fixtures/react/package.json b/tests/consumers/fixtures/react/package.json new file mode 100644 index 0000000..b8bddf2 --- /dev/null +++ b/tests/consumers/fixtures/react/package.json @@ -0,0 +1,17 @@ +{ + "name": "ha-ui-react-consumer", + "private": true, + "type": "module", + "packageManager": "pnpm@10.34.0", + "dependencies": { + "@vypdev/ha-ui": "file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz", + "@vypdev/ha-ui-react": "file:../pack-1/vypdev-ha-ui-react-0.1.0-next.1.tgz", + "react": "19.3.1", + "react-dom": "19.3.1" + }, + "devDependencies": { + "typescript": "6.0.3", + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0" + } +} diff --git a/tests/consumers/fixtures/react/pnpm-lock.yaml b/tests/consumers/fixtures/react/pnpm-lock.yaml new file mode 100644 index 0000000..e0627d9 --- /dev/null +++ b/tests/consumers/fixtures/react/pnpm-lock.yaml @@ -0,0 +1,149 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: false + excludeLinksFromLockfile: false + +overrides: + '@vypdev/ha-ui': file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz + +importers: + + .: + dependencies: + '@vypdev/ha-ui': + specifier: file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz + version: file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz + '@vypdev/ha-ui-react': + specifier: file:../pack-1/vypdev-ha-ui-react-0.1.0-next.1.tgz + version: file:../pack-1/vypdev-ha-ui-react-0.1.0-next.1.tgz(react@19.3.1) + react: + specifier: 19.3.1 + version: 19.3.1 + react-dom: + specifier: 19.3.1 + version: 19.3.1(react@19.3.1) + devDependencies: + '@types/react': + specifier: 19.3.0 + version: 19.3.0 + '@types/react-dom': + specifier: 19.3.0 + version: 19.3.0(@types/react@19.3.0) + typescript: + specifier: 6.0.3 + version: 6.0.3 + +packages: + + '@lit-labs/ssr-dom-shim@1.6.0': + resolution: {integrity: sha512-VHb0ALPMTlgKjM6yIxxoQNnpKyUKLD04VzeQdsiXkMqkvYlAHxq9glGLmgbb889/1GsohSOAjvQYoiBppXFqrQ==} + + '@lit/reactive-element@2.1.2': + resolution: {integrity: sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==} + + '@types/react-dom@19.3.0': + resolution: {integrity: sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==} + peerDependencies: + '@types/react': ^19.3.0 + + '@types/react@19.3.0': + resolution: {integrity: sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@vypdev/ha-ui-react@file:../pack-1/vypdev-ha-ui-react-0.1.0-next.1.tgz': + resolution: {integrity: sha512-YW8N3lQcayr/pPzmVSiTpthWH3ikCVfEzs6JiHBHB1cce70JWjf2ytr4RJjuBofN+rg6SukUtaqcaUeeRqzFhQ==, tarball: file:../pack-1/vypdev-ha-ui-react-0.1.0-next.1.tgz} + version: 0.1.0-next.1 + peerDependencies: + react: 19.3.1 + + '@vypdev/ha-ui@file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz': + resolution: {integrity: sha512-dGtzvFkv/M6fOZGQVI47SpL+jW3yzHRvZ9JFVqKSJ0Tp/JAl2anc9/kqe0PX55Yu+FUVHsjH4o/BoMtIG6kFOw==, tarball: file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz} + version: 0.1.0-next.1 + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + lit-element@4.2.2: + resolution: {integrity: sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==} + + lit-html@3.3.3: + resolution: {integrity: sha512-el8M6jK2o3RXBnrSHX3ZKrsN8zEV63pSExTO1wYJz7QndGYZ8353e2a5PPX+qHe2aGayfnchQmkAojaWAREOIA==} + + lit@3.3.3: + resolution: {integrity: sha512-fycuvZg/hkpozL00lm1pEJH5nN/lr9ZXd6mJI2HSN4+Bzc+LDNdEApJ6HFbPkdFNHLvOplIIuJvxkS4XUxqirw==} + + react-dom@19.3.1: + resolution: {integrity: sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==} + peerDependencies: + react: ^19.3.1 + + react@19.3.1: + resolution: {integrity: sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==} + engines: {node: '>=0.10.0'} + + scheduler@0.28.0: + resolution: {integrity: sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==} + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + +snapshots: + + '@lit-labs/ssr-dom-shim@1.6.0': {} + + '@lit/reactive-element@2.1.2': + dependencies: + '@lit-labs/ssr-dom-shim': 1.6.0 + + '@types/react-dom@19.3.0(@types/react@19.3.0)': + dependencies: + '@types/react': 19.3.0 + + '@types/react@19.3.0': + dependencies: + csstype: 3.2.3 + + '@types/trusted-types@2.0.7': {} + + '@vypdev/ha-ui-react@file:../pack-1/vypdev-ha-ui-react-0.1.0-next.1.tgz(react@19.3.1)': + dependencies: + '@vypdev/ha-ui': file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz + react: 19.3.1 + + '@vypdev/ha-ui@file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz': + dependencies: + lit: 3.3.3 + + csstype@3.2.3: {} + + lit-element@4.2.2: + dependencies: + '@lit-labs/ssr-dom-shim': 1.6.0 + '@lit/reactive-element': 2.1.2 + lit-html: 3.3.3 + + lit-html@3.3.3: + dependencies: + '@types/trusted-types': 2.0.7 + + lit@3.3.3: + dependencies: + '@lit/reactive-element': 2.1.2 + lit-element: 4.2.2 + lit-html: 3.3.3 + + react-dom@19.3.1(react@19.3.1): + dependencies: + react: 19.3.1 + scheduler: 0.28.0 + + react@19.3.1: {} + + scheduler@0.28.0: {} + + typescript@6.0.3: {} diff --git a/tests/consumers/fixtures/react/pnpm-workspace.yaml b/tests/consumers/fixtures/react/pnpm-workspace.yaml new file mode 100644 index 0000000..f2eddb4 --- /dev/null +++ b/tests/consumers/fixtures/react/pnpm-workspace.yaml @@ -0,0 +1,4 @@ +packages: [] +autoInstallPeers: false +overrides: + "@vypdev/ha-ui": file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz diff --git a/tests/consumers/fixtures/react/tsconfig.json b/tests/consumers/fixtures/react/tsconfig.json new file mode 100644 index 0000000..1fbe771 --- /dev/null +++ b/tests/consumers/fixtures/react/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2024", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noEmit": true, + "skipLibCheck": false, + "lib": [ + "ES2024", + "DOM" + ], + "types": [] + }, + "include": [ + "*.ts" + ] +} diff --git a/tests/consumers/fixtures/svelte/Consumer.svelte b/tests/consumers/fixtures/svelte/Consumer.svelte new file mode 100644 index 0000000..6baadd7 --- /dev/null +++ b/tests/consumers/fixtures/svelte/Consumer.svelte @@ -0,0 +1,16 @@ + +{#snippet tasks()}{/snippet} +
+ + + {count} + {#if visible}{/if} +
diff --git a/tests/consumers/fixtures/svelte/client.ts b/tests/consumers/fixtures/svelte/client.ts new file mode 100644 index 0000000..b4ff92b --- /dev/null +++ b/tests/consumers/fixtures/svelte/client.ts @@ -0,0 +1,4 @@ +import {mount} from 'svelte'; +import {registerTabs} from '@vypdev/ha-ui/tabs'; +import Consumer from './Consumer.svelte'; +registerTabs();mount(Consumer,{target:document.querySelector('main')!}); diff --git a/tests/consumers/fixtures/svelte/index.html b/tests/consumers/fixtures/svelte/index.html new file mode 100644 index 0000000..4599a8d --- /dev/null +++ b/tests/consumers/fixtures/svelte/index.html @@ -0,0 +1 @@ +Compiled framework consumer
diff --git a/tests/consumers/fixtures/svelte/model.ts b/tests/consumers/fixtures/svelte/model.ts new file mode 100644 index 0000000..91b2d4a --- /dev/null +++ b/tests/consumers/fixtures/svelte/model.ts @@ -0,0 +1,5 @@ +import {navigationState} from '@vypdev/ha-ui/contracts'; +import type {NavigationState} from '@vypdev/ha-ui/contracts'; +export function model(spanish=false,selected='overview',disabled=false):NavigationState { + return navigationState({kind:'panels',label:spanish?'Secciones del hogar':'Household sections',selected,items:[{id:'overview',label:spanish?'Resumen':'Overview'},{id:'tasks',label:spanish?'Tareas':'Tasks',disabled}]}); +} diff --git a/tests/consumers/fixtures/svelte/package.json b/tests/consumers/fixtures/svelte/package.json new file mode 100644 index 0000000..6ce8114 --- /dev/null +++ b/tests/consumers/fixtures/svelte/package.json @@ -0,0 +1,14 @@ +{ + "name": "ha-ui-svelte-consumer", + "private": true, + "type": "module", + "packageManager": "pnpm@10.34.0", + "dependencies": { + "@vypdev/ha-ui": "file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz", + "@vypdev/ha-ui-svelte": "file:../pack-1/vypdev-ha-ui-svelte-0.1.0-next.1.tgz", + "svelte": "5.57.3" + }, + "devDependencies": { + "typescript": "6.0.3" + } +} diff --git a/tests/consumers/fixtures/svelte/pnpm-lock.yaml b/tests/consumers/fixtures/svelte/pnpm-lock.yaml new file mode 100644 index 0000000..80c405c --- /dev/null +++ b/tests/consumers/fixtures/svelte/pnpm-lock.yaml @@ -0,0 +1,242 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: false + excludeLinksFromLockfile: false + +overrides: + '@vypdev/ha-ui': file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz + +importers: + + .: + dependencies: + '@vypdev/ha-ui': + specifier: file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz + version: file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz + '@vypdev/ha-ui-svelte': + specifier: file:../pack-1/vypdev-ha-ui-svelte-0.1.0-next.1.tgz + version: file:../pack-1/vypdev-ha-ui-svelte-0.1.0-next.1.tgz(svelte@5.57.3) + svelte: + specifier: 5.57.3 + version: 5.57.3 + devDependencies: + typescript: + specifier: 6.0.3 + version: 6.0.3 + +packages: + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@lit-labs/ssr-dom-shim@1.6.0': + resolution: {integrity: sha512-VHb0ALPMTlgKjM6yIxxoQNnpKyUKLD04VzeQdsiXkMqkvYlAHxq9glGLmgbb889/1GsohSOAjvQYoiBppXFqrQ==} + + '@lit/reactive-element@2.1.2': + resolution: {integrity: sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==} + + '@sveltejs/acorn-typescript@1.0.13': + resolution: {integrity: sha512-wgKggnhZVL9Bfx1OaKKTrYY9BFRk6C8UAkQNUcIv1+llzYrIqy+RZm5HPKzn0NpEBvTVhTqB4kQyllZywsRBRQ==} + peerDependencies: + acorn: ^8.9.0 + + '@types/estree@1.0.10': + resolution: {integrity: sha512-lTyOMnOcmRBzdCv6/4zUZCrEhV0xrqQiUZBZtVcbmLHBXKLnN8q89v/wKRF/Q0HbwVsqbIG3o5HDoFl9sduM+g==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@vypdev/ha-ui-svelte@file:../pack-1/vypdev-ha-ui-svelte-0.1.0-next.1.tgz': + resolution: {integrity: sha512-N1MkQQXMEqYlnd8qvjOHfDulrlriBe8Zrt8V+DKJAqFQK/bmJZYiFtUk12Q05pFj62ho8+86dH0ztNFljIzBlg==, tarball: file:../pack-1/vypdev-ha-ui-svelte-0.1.0-next.1.tgz} + version: 0.1.0-next.1 + peerDependencies: + svelte: 5.57.3 + + '@vypdev/ha-ui@file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz': + resolution: {integrity: sha512-dGtzvFkv/M6fOZGQVI47SpL+jW3yzHRvZ9JFVqKSJ0Tp/JAl2anc9/kqe0PX55Yu+FUVHsjH4o/BoMtIG6kFOw==, tarball: file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz} + version: 0.1.0-next.1 + + acorn@8.19.0: + resolution: {integrity: sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==} + engines: {node: '>=0.4.0'} + hasBin: true + + aria-query@5.3.1: + resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} + engines: {node: '>= 0.4'} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + devalue@5.9.4: + resolution: {integrity: sha512-sPAT4pztbu6586/hrhOnMKS17IJrvg12mXiSPSS3W5qDeN2RGgvZ0diZCm31dBbnevfVmujNO3IM2wrS4Y2Rhg==} + + esm-env@1.2.2: + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} + + esrap@2.4.0: + resolution: {integrity: sha512-CQPcneEHw/czj32uwQC52nYnax8H4SXt25FCM+LIwGZ49cMIxpOj3bP8wmzcI+Z0HIy5KkClaPhRLfCySzwHPA==} + peerDependencies: + '@typescript-eslint/types': ^8.2.0 + peerDependenciesMeta: + '@typescript-eslint/types': + optional: true + + is-reference@3.0.3: + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + + lit-element@4.2.2: + resolution: {integrity: sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==} + + lit-html@3.3.3: + resolution: {integrity: sha512-el8M6jK2o3RXBnrSHX3ZKrsN8zEV63pSExTO1wYJz7QndGYZ8353e2a5PPX+qHe2aGayfnchQmkAojaWAREOIA==} + + lit@3.3.3: + resolution: {integrity: sha512-fycuvZg/hkpozL00lm1pEJH5nN/lr9ZXd6mJI2HSN4+Bzc+LDNdEApJ6HFbPkdFNHLvOplIIuJvxkS4XUxqirw==} + + locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + svelte@5.57.3: + resolution: {integrity: sha512-5mAftM3SoDV9X0zdF386F9p2pv8iKOwi/KfBS1XnBAkVCKFzVOLHUoa8iGWAt4buoicLmyWVRpuuD5wplXOQsg==} + engines: {node: '>=18'} + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + zimmerframe@1.1.5: + resolution: {integrity: sha512-msJxIvYDYcoNL+PJsu+7qmpDWsYmAxTY+2TNYXXF0hzBzBk0BMecOqDOG/EckUoKCuKwObfbugIl8QpqHDXeFA==} + +snapshots: + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@lit-labs/ssr-dom-shim@1.6.0': {} + + '@lit/reactive-element@2.1.2': + dependencies: + '@lit-labs/ssr-dom-shim': 1.6.0 + + '@sveltejs/acorn-typescript@1.0.13(acorn@8.19.0)': + dependencies: + acorn: 8.19.0 + + '@types/estree@1.0.10': {} + + '@types/trusted-types@2.0.7': {} + + '@vypdev/ha-ui-svelte@file:../pack-1/vypdev-ha-ui-svelte-0.1.0-next.1.tgz(svelte@5.57.3)': + dependencies: + '@vypdev/ha-ui': file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz + svelte: 5.57.3 + + '@vypdev/ha-ui@file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz': + dependencies: + lit: 3.3.3 + + acorn@8.19.0: {} + + aria-query@5.3.1: {} + + axobject-query@4.1.0: {} + + clsx@2.1.1: {} + + devalue@5.9.4: {} + + esm-env@1.2.2: {} + + esrap@2.4.0: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + is-reference@3.0.3: + dependencies: + '@types/estree': 1.0.10 + + lit-element@4.2.2: + dependencies: + '@lit-labs/ssr-dom-shim': 1.6.0 + '@lit/reactive-element': 2.1.2 + lit-html: 3.3.3 + + lit-html@3.3.3: + dependencies: + '@types/trusted-types': 2.0.7 + + lit@3.3.3: + dependencies: + '@lit/reactive-element': 2.1.2 + lit-element: 4.2.2 + lit-html: 3.3.3 + + locate-character@3.0.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + svelte@5.57.3: + dependencies: + '@jridgewell/remapping': 2.3.5 + '@jridgewell/sourcemap-codec': 1.6.0 + '@sveltejs/acorn-typescript': 1.0.13(acorn@8.19.0) + '@types/estree': 1.0.10 + acorn: 8.19.0 + aria-query: 5.3.1 + axobject-query: 4.1.0 + clsx: 2.1.1 + devalue: 5.9.4 + esm-env: 1.2.2 + esrap: 2.4.0 + is-reference: 3.0.3 + locate-character: 3.0.0 + magic-string: 0.30.21 + zimmerframe: 1.1.5 + transitivePeerDependencies: + - '@typescript-eslint/types' + + typescript@6.0.3: {} + + zimmerframe@1.1.5: {} diff --git a/tests/consumers/fixtures/svelte/pnpm-workspace.yaml b/tests/consumers/fixtures/svelte/pnpm-workspace.yaml new file mode 100644 index 0000000..f2eddb4 --- /dev/null +++ b/tests/consumers/fixtures/svelte/pnpm-workspace.yaml @@ -0,0 +1,4 @@ +packages: [] +autoInstallPeers: false +overrides: + "@vypdev/ha-ui": file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz diff --git a/tests/consumers/fixtures/svelte/tsconfig.json b/tests/consumers/fixtures/svelte/tsconfig.json new file mode 100644 index 0000000..587e0ad --- /dev/null +++ b/tests/consumers/fixtures/svelte/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2024", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "noEmit": true, + "skipLibCheck": false, + "lib": [ + "ES2024", + "DOM" + ], + "types": [] + }, + "include": [ + "*.ts", + "*.svelte" + ] +} diff --git a/tests/consumers/fixtures/vue/client.ts b/tests/consumers/fixtures/vue/client.ts new file mode 100644 index 0000000..e21426f --- /dev/null +++ b/tests/consumers/fixtures/vue/client.ts @@ -0,0 +1,17 @@ +import {createApp,h,shallowRef} from 'vue'; +import {Tabs} from '@vypdev/ha-ui-vue'; +import {registerTabs,type NavigationSelectionEvent} from '@vypdev/ha-ui/tabs'; +import {model} from './model.js'; +registerTabs(); +createApp({setup(){ + const state=shallowRef(model()),spanish=shallowRef(false),dark=shallowRef(false),disabled=shallowRef(false),visible=shallowRef(true),count=shallowRef(0); + const select=(event:NavigationSelectionEvent)=>{event.preventDefault();state.value=model(spanish.value,event.detail.id,disabled.value);count.value++;}; + const language=()=>{spanish.value=!spanish.value;state.value=model(spanish.value,state.value.selected!,disabled.value);}; + const toggleDisabled=()=>{disabled.value=!disabled.value;state.value=model(spanish.value,'overview',disabled.value);}; + return ()=>h('section',{style:{'--primary-text-color':dark.value?'rgb(235,235,235)':'rgb(28,28,28)','--secondary-text-color':dark.value?'rgb(189,189,189)':'rgb(89,89,89)','--card-background-color':dark.value?'rgb(20,20,20)':'rgb(255,255,255)'}},[ + h('button',{onClick:language},'Switch language'),h('button',{onClick:()=>dark.value=!dark.value},'Toggle theme'), + h('button',{onClick:toggleDisabled},'Toggle disabled'),h('button',{onClick:()=>visible.value=!visible.value},'Toggle component'), + h('output',{'aria-label':'Selection events'},String(count.value)), + visible.value?h(Tabs,{state:state.value,onSelectionRequest:select},{default:()=>h('button',{slot:'tasks'},'Add task')}):null, + ]); +}}).mount(document.querySelector('main')!); diff --git a/tests/consumers/fixtures/vue/index.html b/tests/consumers/fixtures/vue/index.html new file mode 100644 index 0000000..4599a8d --- /dev/null +++ b/tests/consumers/fixtures/vue/index.html @@ -0,0 +1 @@ +Compiled framework consumer
diff --git a/tests/consumers/fixtures/vue/model.ts b/tests/consumers/fixtures/vue/model.ts new file mode 100644 index 0000000..91b2d4a --- /dev/null +++ b/tests/consumers/fixtures/vue/model.ts @@ -0,0 +1,5 @@ +import {navigationState} from '@vypdev/ha-ui/contracts'; +import type {NavigationState} from '@vypdev/ha-ui/contracts'; +export function model(spanish=false,selected='overview',disabled=false):NavigationState { + return navigationState({kind:'panels',label:spanish?'Secciones del hogar':'Household sections',selected,items:[{id:'overview',label:spanish?'Resumen':'Overview'},{id:'tasks',label:spanish?'Tareas':'Tasks',disabled}]}); +} diff --git a/tests/consumers/fixtures/vue/package.json b/tests/consumers/fixtures/vue/package.json new file mode 100644 index 0000000..7a296e8 --- /dev/null +++ b/tests/consumers/fixtures/vue/package.json @@ -0,0 +1,14 @@ +{ + "name": "ha-ui-vue-consumer", + "private": true, + "type": "module", + "packageManager": "pnpm@10.34.0", + "dependencies": { + "@vypdev/ha-ui": "file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz", + "@vypdev/ha-ui-vue": "file:../pack-1/vypdev-ha-ui-vue-0.1.0-next.1.tgz", + "vue": "3.5.44" + }, + "devDependencies": { + "typescript": "6.0.3" + } +} diff --git a/tests/consumers/fixtures/vue/pnpm-lock.yaml b/tests/consumers/fixtures/vue/pnpm-lock.yaml new file mode 100644 index 0000000..5b8f17b --- /dev/null +++ b/tests/consumers/fixtures/vue/pnpm-lock.yaml @@ -0,0 +1,283 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: false + excludeLinksFromLockfile: false + +overrides: + '@vypdev/ha-ui': file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz + +importers: + + .: + dependencies: + '@vypdev/ha-ui': + specifier: file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz + version: file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz + '@vypdev/ha-ui-vue': + specifier: file:../pack-1/vypdev-ha-ui-vue-0.1.0-next.1.tgz + version: file:../pack-1/vypdev-ha-ui-vue-0.1.0-next.1.tgz(vue@3.5.44(typescript@6.0.3)) + vue: + specifier: 3.5.44 + version: 3.5.44(typescript@6.0.3) + devDependencies: + typescript: + specifier: 6.0.3 + version: 6.0.3 + +packages: + + '@babel/helper-string-parser@7.29.11': + resolution: {integrity: sha512-snZbO7MEDOof+hvNa97EjEY7qD7h2kwvQr5AJC3ZhQudKW5dnej6k3kP9NQV88hJwaS8vDqRLFo9rivLEhle2g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.12': + resolution: {integrity: sha512-WJ7JBp/Q4+z/ImydtI7jS6NVco5++632ExBemSX+Ysb+G1lCwQ5BVo3SY+jGGOcZXI4ZccbZSUSJLNBW6eEpJg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.29.11': + resolution: {integrity: sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==} + engines: {node: '>=6.9.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@lit-labs/ssr-dom-shim@1.6.0': + resolution: {integrity: sha512-VHb0ALPMTlgKjM6yIxxoQNnpKyUKLD04VzeQdsiXkMqkvYlAHxq9glGLmgbb889/1GsohSOAjvQYoiBppXFqrQ==} + + '@lit/reactive-element@2.1.2': + resolution: {integrity: sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@vue/compiler-core@3.5.44': + resolution: {integrity: sha512-k34nCytbK8PQ1gR2mGROYOw6Pnv/mumqIdrZ3x2tbyXZDWm7ViKGLTfdB+DRrWqEQsLgGJvn0WN9jBif5zCTLQ==} + + '@vue/compiler-dom@3.5.44': + resolution: {integrity: sha512-W5AoBFWdw1lQAKiHowkfgFKFab82oSRT6sGsPfw/q1GRjsy/ZJFLHGhJyBRIFN9APLTUduDH9U2JAiPOIyKuYg==} + + '@vue/compiler-sfc@3.5.44': + resolution: {integrity: sha512-dzHmQ4jXTpdr5FjIQUeykVakN/uzqFbUo9/C5ikBFg3a3PeM6uRxeczx1ZbXBML0/zJstEh3ygYipzR5Z7b2Pw==} + + '@vue/compiler-ssr@3.5.44': + resolution: {integrity: sha512-xr5gb1JVHlfXJwSlRz1FeP6uxoazdzHuMTVLE7up+4Vrqxk219CvxLPxD9NXBt47yvZW7+p4DXfyp4O1fkByIA==} + + '@vue/reactivity@3.5.44': + resolution: {integrity: sha512-u+QJdKeBBEADkPdevsL+6E12QlYVfBD8Zcw5qjDQKtamI6l14KyqazJMobhCKelhWN5zpyKUiEDoTyHE93cqWw==} + + '@vue/runtime-core@3.5.44': + resolution: {integrity: sha512-uEa/ndwjhrrhnOfb2p+sZy6zeoamJOISqaLSJGk/8R6nABT1ZldpzcJoPzKj+zHVomVBPcmQYyC2r4LtvDV6CQ==} + + '@vue/runtime-dom@3.5.44': + resolution: {integrity: sha512-3fc4ARcXVhVBaJ3ujWiNZJARD6pkyRcwG+ZYhMzjKtKUp7MAY0HLwr7r3Smep3Li6ZaWT6ewdmx8XqdaD5oNZg==} + + '@vue/server-renderer@3.5.44': + resolution: {integrity: sha512-R4S1UoH4h5Frrauv8Z5XE5TxV9oBiUSgWIC4TyVK1FK+RocRnOLwFA5Xcmo2011RlgJp+AbZCnw6afrhlgRuUQ==} + + '@vue/shared@3.5.44': + resolution: {integrity: sha512-H82XLH0DjmSOdbEwO15x3QTo1f5PWEtv237eLpYKc0mqLu+B8hnq16lFWmuBmNurkeiG3wr+oM1FhOll/YVFRg==} + + '@vypdev/ha-ui-vue@file:../pack-1/vypdev-ha-ui-vue-0.1.0-next.1.tgz': + resolution: {integrity: sha512-aN0lS7oJEntPqjdiUC9W4isVPpZt5w6PrnZ9yVBnrsj7hkkZ5x3gq8HPT1yaIM7gMu/PZuLyvvtWZgVR3H18Rg==, tarball: file:../pack-1/vypdev-ha-ui-vue-0.1.0-next.1.tgz} + version: 0.1.0-next.1 + peerDependencies: + vue: 3.5.44 + + '@vypdev/ha-ui@file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz': + resolution: {integrity: sha512-dGtzvFkv/M6fOZGQVI47SpL+jW3yzHRvZ9JFVqKSJ0Tp/JAl2anc9/kqe0PX55Yu+FUVHsjH4o/BoMtIG6kFOw==, tarball: file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz} + version: 0.1.0-next.1 + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + lit-element@4.2.2: + resolution: {integrity: sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==} + + lit-html@3.3.3: + resolution: {integrity: sha512-el8M6jK2o3RXBnrSHX3ZKrsN8zEV63pSExTO1wYJz7QndGYZ8353e2a5PPX+qHe2aGayfnchQmkAojaWAREOIA==} + + lit@3.3.3: + resolution: {integrity: sha512-fycuvZg/hkpozL00lm1pEJH5nN/lr9ZXd6mJI2HSN4+Bzc+LDNdEApJ6HFbPkdFNHLvOplIIuJvxkS4XUxqirw==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + nanoid@3.3.20: + resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + postcss@8.5.32: + resolution: {integrity: sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==} + engines: {node: ^10 || ^12 || >=14} + + source-map-js@1.2.2: + resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} + engines: {node: '>=0.10.0'} + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + vue@3.5.44: + resolution: {integrity: sha512-dvvmaOCG7lAMkJTPqPwWeGMsPS6DIsFsfZDUgBp9V6mHf9b3+/wyVwYR4Es+WV6uJC0FKRxp81Obcq0ZwUcGFQ==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + +snapshots: + + '@babel/helper-string-parser@7.29.11': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/parser@7.29.12': + dependencies: + '@babel/types': 7.29.11 + + '@babel/types@7.29.11': + dependencies: + '@babel/helper-string-parser': 7.29.11 + '@babel/helper-validator-identifier': 7.29.7 + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@lit-labs/ssr-dom-shim@1.6.0': {} + + '@lit/reactive-element@2.1.2': + dependencies: + '@lit-labs/ssr-dom-shim': 1.6.0 + + '@types/trusted-types@2.0.7': {} + + '@vue/compiler-core@3.5.44': + dependencies: + '@babel/parser': 7.29.12 + '@vue/shared': 3.5.44 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.2 + + '@vue/compiler-dom@3.5.44': + dependencies: + '@vue/compiler-core': 3.5.44 + '@vue/shared': 3.5.44 + + '@vue/compiler-sfc@3.5.44': + dependencies: + '@babel/parser': 7.29.12 + '@vue/compiler-core': 3.5.44 + '@vue/compiler-dom': 3.5.44 + '@vue/compiler-ssr': 3.5.44 + '@vue/shared': 3.5.44 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.32 + source-map-js: 1.2.2 + + '@vue/compiler-ssr@3.5.44': + dependencies: + '@vue/compiler-dom': 3.5.44 + '@vue/shared': 3.5.44 + + '@vue/reactivity@3.5.44': + dependencies: + '@vue/shared': 3.5.44 + + '@vue/runtime-core@3.5.44': + dependencies: + '@vue/reactivity': 3.5.44 + '@vue/shared': 3.5.44 + + '@vue/runtime-dom@3.5.44': + dependencies: + '@vue/reactivity': 3.5.44 + '@vue/runtime-core': 3.5.44 + '@vue/shared': 3.5.44 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.44': + dependencies: + '@vue/compiler-ssr': 3.5.44 + '@vue/runtime-dom': 3.5.44 + '@vue/shared': 3.5.44 + + '@vue/shared@3.5.44': {} + + '@vypdev/ha-ui-vue@file:../pack-1/vypdev-ha-ui-vue-0.1.0-next.1.tgz(vue@3.5.44(typescript@6.0.3))': + dependencies: + '@vypdev/ha-ui': file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz + vue: 3.5.44(typescript@6.0.3) + + '@vypdev/ha-ui@file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz': + dependencies: + lit: 3.3.3 + + csstype@3.2.3: {} + + entities@7.0.1: {} + + estree-walker@2.0.2: {} + + lit-element@4.2.2: + dependencies: + '@lit-labs/ssr-dom-shim': 1.6.0 + '@lit/reactive-element': 2.1.2 + lit-html: 3.3.3 + + lit-html@3.3.3: + dependencies: + '@types/trusted-types': 2.0.7 + + lit@3.3.3: + dependencies: + '@lit/reactive-element': 2.1.2 + lit-element: 4.2.2 + lit-html: 3.3.3 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + nanoid@3.3.20: {} + + picocolors@1.1.1: {} + + postcss@8.5.32: + dependencies: + nanoid: 3.3.20 + picocolors: 1.1.1 + source-map-js: 1.2.2 + + source-map-js@1.2.2: {} + + typescript@6.0.3: {} + + vue@3.5.44(typescript@6.0.3): + dependencies: + '@vue/compiler-dom': 3.5.44 + '@vue/compiler-sfc': 3.5.44 + '@vue/runtime-dom': 3.5.44 + '@vue/server-renderer': 3.5.44 + '@vue/shared': 3.5.44 + optionalDependencies: + typescript: 6.0.3 diff --git a/tests/consumers/fixtures/vue/pnpm-workspace.yaml b/tests/consumers/fixtures/vue/pnpm-workspace.yaml new file mode 100644 index 0000000..f2eddb4 --- /dev/null +++ b/tests/consumers/fixtures/vue/pnpm-workspace.yaml @@ -0,0 +1,4 @@ +packages: [] +autoInstallPeers: false +overrides: + "@vypdev/ha-ui": file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz diff --git a/tests/consumers/fixtures/vue/tsconfig.json b/tests/consumers/fixtures/vue/tsconfig.json new file mode 100644 index 0000000..1fbe771 --- /dev/null +++ b/tests/consumers/fixtures/vue/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2024", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noEmit": true, + "skipLibCheck": false, + "lib": [ + "ES2024", + "DOM" + ], + "types": [] + }, + "include": [ + "*.ts" + ] +} diff --git a/tests/consumers/frameworks.ts b/tests/consumers/frameworks.ts new file mode 100644 index 0000000..53dd0ff --- /dev/null +++ b/tests/consumers/frameworks.ts @@ -0,0 +1,104 @@ +import assert from 'node:assert/strict'; +import {readFile,writeFile,readdir,cp,mkdir} from 'node:fs/promises'; +import {resolve,join,dirname,relative} from 'node:path'; +import {promisify} from 'node:util'; +import {globSync} from 'node:fs'; +import {execFile} from 'node:child_process'; +import {createHash} from 'node:crypto'; +import {parse,stringify} from 'yaml'; +import {createServer,type Plugin} from 'vite'; +import {svelte} from '@sveltejs/vite-plugin-svelte'; +import {transformAsync} from '@babel/core'; +import linker from '@angular/compiler-cli/linker/babel'; +import {needsLinking} from '@angular/compiler-cli/linker'; +import type {Browser} from 'playwright'; +import type {HaTabs} from '@vypdev/ha-ui/tabs'; + +const execute=promisify(execFile); +export async function exerciseFrameworks(root:string,scratch:string,browser:Browser) { + const sourceLock=parse(await readFile(join(root,'pnpm-lock.yaml'),'utf8')); + const results=[]; + for (const framework of ['react','vue','angular','svelte']) { + const folder=join(scratch,'consumer-'+framework); + await cp(join(root,'tests/consumers/fixtures',framework),folder,{recursive:true}); + const lock=parse(await readFile(join(folder,'pnpm-lock.yaml'),'utf8')); + const candidateArchives=[]; + for (const [key,description] of Object.entries(lock.packages) as [string,{resolution:{tarball?:string;integrity:string}}][]) { + if (description.resolution.tarball) { + assert.ok(key.startsWith('@vypdev/ha-ui')); + const archive=resolve(folder,description.resolution.tarball.slice('file:'.length)); + assert.equal(dirname(archive),join(scratch,'pack-1')); + const bytes=await readFile(archive); + description.resolution.integrity='sha512-'+createHash('sha512').update(bytes).digest('base64'); + candidateArchives.push({file:description.resolution.tarball,sha256:createHash('sha256').update(bytes).digest('hex')}); + } else assert.deepEqual(description,sourceLock.packages[key],'Registry resolution must match the reviewed source lock: '+key); + } + const frozen=stringify(lock);await writeFile(join(folder,'pnpm-lock.yaml'),frozen); + const config=parse(await readFile(join(folder,'pnpm-workspace.yaml'),'utf8')); + config.cacheDir=join(folder,'empty-metadata-cache');await writeFile(join(folder,'pnpm-workspace.yaml'),stringify(config)); + await execute('pnpm',['install','--frozen-lockfile','--offline','--ignore-scripts'],{cwd:folder}); + assert.equal(await readFile(join(folder,'pnpm-lock.yaml'),'utf8'),frozen); + const installedRoot=join(folder,'node_modules/@vypdev/ha-ui-'+framework); + const published=JSON.parse(await readFile(join(installedRoot,'package.json'),'utf8')); + assert.equal(published.version,'0.1.0-next.1'); + assert.equal(published.dependencies['@vypdev/ha-ui'],'0.1.0-next.1'); + assert.doesNotMatch(JSON.stringify(published),/workspace:|file:/u,'Published metadata cannot retain fixture or workspace references'); + for (const source of globSync('packages/'+framework+'/src/**/*.{ts,svelte}',{cwd:root})) + assert.deepEqual(await readFile(join(installedRoot,relative(join(root,'packages',framework),join(root,source)))),await readFile(join(root,source)),'Installed authored source must match '+source); + const installed=await readdir(join(folder,'node_modules/.pnpm')); + const identities={react:/^(react@|react-dom@)/u,vue:/^vue@/u,angular:/^@angular\+/u,svelte:/^svelte@/u}; + for (const [other,pattern] of Object.entries(identities)) if (other!==framework) + assert.equal(installed.some(name=>pattern.test(name)),false,'A selected adapter must not install '+other); + const compiler=framework==='svelte'?join(root,'node_modules/svelte-check/bin/svelte-check'):framework==='angular'?join(root,'node_modules/@angular/compiler-cli/bundles/src/bin/ngc.js'):join(folder,'node_modules/typescript/bin/tsc'); + const args=framework==='svelte'?['--tsconfig','tsconfig.json']:['-p','tsconfig.json']; + await execute(process.execPath,[compiler,...args],{cwd:folder}); + const plugins:Plugin[]=framework==='angular'?[{ + name:'official-angular-linker',enforce:'pre', + async transform(code,id) { + if (!needsLinking(id.split('?')[0],code)) return; + const output=await transformAsync(code,{filename:id,configFile:false,babelrc:false,plugins:[linker],sourceMaps:true}); + assert.ok(output?.code);return {code:output.code,map:output.map?JSON.stringify(output.map):null}; + }, + }]:framework==='svelte'?svelte({configFile:false,compilerOptions:{dev:false}}):[]; + const server=await createServer({root:folder,configFile:false,plugins,logLevel:'error',optimizeDeps:framework==='angular'?{noDiscovery:true}:{},server:{host:'127.0.0.1',port:0}}); + const page=await browser.newPage({viewport:{width:360,height:640},reducedMotion:'reduce'}); + const errors:string[]=[];page.on('pageerror',error=>errors.push(error.message)); + try { + await server.listen();const address=server.httpServer!.address();assert.ok(address&&typeof address==='object'); + await page.goto('http://127.0.0.1:'+address.port); + await page.getByRole('tab',{name:'Tasks',exact:true}).click(); + await page.getByRole('button',{name:'Add task',exact:true}).waitFor({state:'visible'}); + assert.equal(await page.getByLabel('Selection events').textContent(),'1'); + assert.equal(await page.locator('vyp-ha-tabs').getAttribute('state'),null); + await page.getByRole('button',{name:'Switch language',exact:true}).click(); + await page.getByRole('tab',{name:'Tareas',exact:true}).waitFor({state:'visible'}); + assert.equal(await page.getByRole('tab',{name:'Tareas',exact:true}).getAttribute('aria-selected'),'true'); + await page.getByRole('button',{name:'Toggle theme',exact:true}).click(); + await page.waitForFunction(()=>getComputedStyle(document.querySelector('vyp-ha-tabs')!.shadowRoot!.querySelector('.bar')!).backgroundColor==='rgb(20, 20, 20)'); + await page.getByRole('button',{name:'Toggle disabled',exact:true}).click(); + await page.waitForFunction(()=>document.querySelector('vyp-ha-tabs')!.shadowRoot!.querySelector('#tab-tasks')!.disabled); + assert.equal(await page.getByRole('tab',{name:'Tareas',exact:true}).isDisabled(),true); + assert.equal(await page.getByLabel('Selection events').textContent(),'1'); + await page.getByRole('button',{name:'Toggle disabled',exact:true}).click(); + await page.getByRole('tab',{name:'Resumen',exact:true}).focus();await page.keyboard.press('End');await page.keyboard.press('Enter'); + await page.waitForFunction(()=>document.querySelector('output')!.textContent==='2'); + const retained=await page.locator('vyp-ha-tabs').elementHandle();assert.ok(retained); + await page.getByRole('button',{name:'Toggle component',exact:true}).click(); + await page.locator('vyp-ha-tabs').waitFor({state:'detached'}); + await retained.evaluate(element=>element.shadowRoot!.querySelector('#tab-tasks')!.click()); + assert.equal(await page.getByLabel('Selection events').textContent(),'2','Unmounted adapter must not invoke a retained callback'); + await page.getByRole('button',{name:'Toggle component',exact:true}).click(); + await page.getByRole('tab',{name:'Resumen',exact:true}).click(); + await page.waitForFunction(()=>document.querySelector('output')!.textContent==='3'); + await page.getByRole('tab',{name:'Tareas',exact:true}).click(); + await page.waitForFunction(()=>document.querySelector('output')!.textContent==='4'); + await page.getByRole('button',{name:'Add task',exact:true}).waitFor({state:'visible'}); + await mkdir(join(root,'test-results'),{recursive:true}); + await page.locator('vyp-ha-tabs').screenshot({path:join(root,'test-results','packed-'+framework+'-dark-es.png')}); + assert.deepEqual(errors,[]); + results.push({framework,compiled:true,frozenOfflineInstall:true,noForeignFramework:true,candidateArchives,props:true,events:true,slots:true,disabled:true,runtimeSpanishAndTheme:true,keyboard:true,teardownAndRemount:true,browser:browser.version(),errors}); + console.log('Packed '+framework+' compiler, bindings and real Chromium lifecycle passed'); + } finally {await page.close();await server.close();} + } + return results; +} diff --git a/tests/consumers/packed.test.ts b/tests/consumers/packed.test.ts index e448940..05c4234 100644 --- a/tests/consumers/packed.test.ts +++ b/tests/consumers/packed.test.ts @@ -10,12 +10,13 @@ import {createHash} from 'node:crypto'; import {createServer} from 'vite'; import {chromium} from 'playwright'; import {parse, stringify} from 'yaml'; +import {exerciseFrameworks} from './frameworks.ts'; const execute = promisify(execFile); const root = fileURLToPath(new URL('../../', import.meta.url)); const digest = (bytes: Uint8Array) => createHash('sha256').update(bytes).digest('hex'); -test('the actual packed package is reproducible and passes clean DOM-free and real-browser consumers', {timeout: 60_000}, async () => { +test('the actual packed package is reproducible and passes clean DOM-free and real-browser consumers', {timeout: 180_000}, async () => { const scratch = await mkdtemp(join(tmpdir(), 'ha-ui-packed-contract-')); let passed = false; let server: Awaited> | undefined; @@ -25,9 +26,21 @@ test('the actual packed package is reproducible and passes clean DOM-free and re for (const index of [1, 2]) { const destination = join(scratch, 'pack-' + index); await mkdir(destination); await execute('pnpm', ['build'], {cwd: root}); - await execute('pnpm', ['--dir', 'packages/core', 'pack', '--pack-destination', destination], {cwd: root}); + for (const framework of ['core','react','vue','angular','svelte']) { + const directory=framework==='angular'?'packages/angular/dist':'packages/'+framework; + await execute('pnpm',['--dir',directory,'pack','--pack-destination',destination],{cwd:root}); + } const names = (await readdir(destination)).filter(name => name.endsWith('.tgz')); - assert.equal(names.length, 1); archives.push(join(destination, names[0])); + assert.equal(names.length, 5); archives.push(join(destination, 'vypdev-ha-ui-0.1.0-next.1.tgz')); + if(index===2)for(const file of names)assert.deepEqual(await readFile(join(scratch,'pack-1',file)),await readFile(join(destination,file)),'Every cohort coordinate must reproduce: '+file); + } + const cohort=[]; + for (const file of (await readdir(join(scratch,'pack-1'))).filter(name=>name.endsWith('.tgz')).sort()) { + const bytes=await readFile(join(scratch,'pack-1',file)); + const packedMetadata=await execute('tar',['-xOf',join(scratch,'pack-1',file),'package/package.json']); + const manifest=JSON.parse(packedMetadata.stdout); + assert.equal(manifest.version,'0.1.0-next.1');assert.doesNotMatch(packedMetadata.stdout,/workspace:|file:/u); + cohort.push({coordinate:manifest.name,version:manifest.version,sha256:digest(bytes),bytes:bytes.byteLength}); } const first = await readFile(archives[0]), second = await readFile(archives[1]); assert.deepEqual(first, second, 'Repeated independent builds must produce the same packed bytes'); @@ -130,6 +143,10 @@ document.querySelector('main')!.append(element);`); const packedBrowser={engine:'chromium',version:browser.version(),compiledClient:true,navigation:true,panels:true,runtimeSpanishAndTheme:true,reducedMotion:true,consoleErrors:errors}; await mkdir(resolve(root, 'test-results'), {recursive:true}); await writeFile(resolve(root, 'test-results/packed-contract.json'), JSON.stringify({scope:'Core data and navigation component only; no framework adapter or application adoption',sha256:digest(first),bytes:first.byteLength,reproducible:true,cleanOfflineInstall:true,frozenRuntimeLock:true,isolatedMetadataCache:true,negativeWithoutLockRejected:true,compiledWithoutDomTypes:true,serverImport:true,packedBrowser},null,2)+'\n'); + await browserPage.close();await server.close();server=undefined; + const frameworks=await exerciseFrameworks(root,scratch,browser); + await writeFile(resolve(root,'test-results/packed-frameworks.json'),JSON.stringify({scope:'Navigation adapters only; forms, framework SSR and application adoption remain required',frameworks},null,2)+'\n'); + await writeFile(resolve(root,'test-results/packed-cohort.json'),JSON.stringify({scope:'Unpublished navigation preview only; no registry publication or full capability acceptance',cohort,reproducible:true,sourceLockSha256:digest(await readFile(resolve(root,'pnpm-lock.yaml')))},null,2)+'\n'); passed = true; } finally { await browser?.close(); await server?.close(); diff --git a/tests/coverage.test.mjs b/tests/coverage.test.mjs index 190520b..521df80 100644 --- a/tests/coverage.test.mjs +++ b/tests/coverage.test.mjs @@ -9,7 +9,7 @@ function fullReport() { return {total: fullGroup(), "source.ts": fullGroup()}; } describe("complete first-party frontend coverage", () => { it("requires all runtime extensions, four metrics and per-file checks without exclusions", () => { const config = completeCoverage(["owned"], "reports"); - expect(config.include).toEqual(["owned/**/*.{js,mjs,cjs,jsx,ts,mts,cts,tsx}"]); + expect(config.include).toEqual(["owned/**/*.{js,mjs,cjs,jsx,ts,mts,cts,tsx,svelte}"]); expect(config.exclude).toBeUndefined(); expect(config.thresholds).toEqual({lines: 100, statements: 100, functions: 100, branches: 100, perFile: true}); expect(config.reporter).toContain("json-summary"); diff --git a/tooling/coverage.mjs b/tooling/coverage.mjs index b9c5f7f..87b4547 100644 --- a/tooling/coverage.mjs +++ b/tooling/coverage.mjs @@ -1,5 +1,5 @@ /** Reusable first-party frontend coverage policy, independent of product behavior. */ -export const runtimeExtensions = "{js,mjs,cjs,jsx,ts,mts,cts,tsx}"; +export const runtimeExtensions = "{js,mjs,cjs,jsx,ts,mts,cts,tsx,svelte}"; export const metrics = /** @type {const} */ (["lines", "statements", "functions", "branches"]); /** diff --git a/tsconfig.consumers.json b/tsconfig.consumers.json new file mode 100644 index 0000000..39be685 --- /dev/null +++ b/tsconfig.consumers.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true + }, + "include": [ + "tests/consumers/**/*.ts" + ], + "exclude": [ + "tests/consumers/fixtures" + ] +} diff --git a/tsconfig.json b/tsconfig.json index 3eb62f6..9021979 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,14 +15,29 @@ "ES2024", "DOM" ], - "verbatimModuleSyntax": true + "verbatimModuleSyntax": true, + "experimentalDecorators": true, + "paths": { + "@vypdev/ha-ui": [ + "./packages/core/src/index.ts" + ], + "@vypdev/ha-ui/contracts": [ + "./packages/core/src/contracts.ts" + ], + "@vypdev/ha-ui/tabs": [ + "./packages/core/src/components/tabs.ts" + ] + } }, "include": [ "packages/*/src/**/*.ts", "tooling/**/*.mjs", "vitest.config.ts", "tests/contracts/**/*.test.ts", - "tests/consumers/**/*.test.ts", "tests/browser/**/*.test.ts" + ], + "exclude": [ + "packages/svelte/src", + "tests/consumers" ] } diff --git a/tsconfig.svelte-source.json b/tsconfig.svelte-source.json new file mode 100644 index 0000000..984a76b --- /dev/null +++ b/tsconfig.svelte-source.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Bundler" + }, + "include": [ + "packages/svelte/src/**/*.ts", + "packages/svelte/src/**/*.svelte" + ], + "exclude": [] +} diff --git a/tsconfig.svelte-tests.json b/tsconfig.svelte-tests.json new file mode 100644 index 0000000..b885571 --- /dev/null +++ b/tsconfig.svelte-tests.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Bundler" + }, + "include": [ + "tests/browser/fixtures/**/*.svelte" + ], + "exclude": [] +} diff --git a/vitest.config.ts b/vitest.config.ts index 892868f..3c0cf0b 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,10 +1,11 @@ import {defineConfig} from 'vitest/config'; import {playwright} from '@vitest/browser-playwright'; +import {svelte} from '@sveltejs/vite-plugin-svelte'; import {completeCoverage} from './tooling/coverage.mjs'; export default defineConfig({test: { projects: [ {test: {name:'node', include:['tests/**/*.test.{ts,mjs}'], exclude:['tests/browser/**','tests/coverage-evidence.test.mjs','tests/consumers/packed.test.ts']}}, - {test: {name:'browser', include:['tests/browser/**/*.test.ts'], browser:{enabled:true,provider:playwright(),headless:true,instances:[{browser:'chromium'}]}}}, + {plugins:[svelte({configFile:'packages/svelte/svelte.config.js'})],test: {name:'browser', include:['tests/browser/**/*.test.{ts,mjs}'], browser:{enabled:true,provider:playwright(),headless:true,instances:[{browser:'chromium'}]}}}, ], coverage: completeCoverage(['packages/*/src', 'tooling'], 'coverage'), }}); From 213f5273348651c88e78e506c7c6fd4a823f7846 Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Wed, 4 Nov 2026 07:19:17 +0100 Subject: [PATCH 09/55] feat: add controlled form-associated text fields --- README.md | 3 +- docs/compatibility.md | 10 +- docs/component-library.md | 57 +++++++ docs/evidence/shared-ui-foundation.json | 110 ++++++++----- docs/provenance.md | 6 + packages/core/package.json | 8 + packages/core/src/components/field.ts | 148 ++++++++++++++++++ packages/core/src/contracts.ts | 7 +- packages/core/src/field-contracts.ts | 50 ++++++ packages/core/src/index.ts | 2 + packages/core/src/presentation-text.ts | 5 + specs/CATALOG.md | 8 +- specs/catalog.json | 15 +- specs/shared-ui-framework-delivery.md | 4 + tests/architecture.test.mjs | 4 +- tests/browser/field.test.ts | 198 ++++++++++++++++++++++++ tests/consumers/packed.test.ts | 42 ++++- tests/contracts/field.test.ts | 49 ++++++ tests/contracts/registration.test.ts | 6 + 19 files changed, 672 insertions(+), 60 deletions(-) create mode 100644 packages/core/src/components/field.ts create mode 100644 packages/core/src/field-contracts.ts create mode 100644 packages/core/src/presentation-text.ts create mode 100644 tests/browser/field.test.ts create mode 100644 tests/contracts/field.test.ts diff --git a/README.md b/README.md index 43ec5d2..f19610a 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,8 @@ Shared presentation components for Home Assistant applications, including Gateway and Domus. The current source implements a DOM-independent navigation data contract, one controlled HA-style -upper navigation/panel component and a pinned, tested build/coverage foundation. React, Vue, Angular +upper navigation/panel component, a controlled form-associated text field and a pinned, tested +build/coverage foundation. React, Vue, Angular and Svelte navigation bindings now pass real compiled, packed consumers. Other component families, form bridges, framework SSR boundaries, Storybook, publication and actual application adoption remain required. diff --git a/docs/compatibility.md b/docs/compatibility.md index cb8c9f6..e1b4cf1 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -10,12 +10,13 @@ families, form bridges, browser support breadth and framework SSR integration re | pnpm | 10.34.0 | Frozen workspace install | | TypeScript | 6.0.3 | Strict contract/type declarations; compatible candidate for Angular compiler-cli 22.2.4 | | Vite | 8.3.5 | Test transform toolchain | -| Lit | 3.3.3 | Internal shared navigation renderer | +| Lit | 3.3.3 | Internal shared navigation and text-field renderer | | Vitest, browser provider and V8 coverage | 5.1.0 | Node and real Chromium; all four metrics globally and per authored runtime file | | Playwright | 1.64.1 | Chromium 156.0.8078.4 component and clean packed-consumer tests | The core preview coordinate/version is package: @vypdev/ha-ui at 0.1.0-next.1. It is unpublished; -its source currently contains presentation contracts and one preview navigation/panel component. It is not a supported component release. +its source currently contains presentation contracts, one preview navigation/panel component and +one controlled text field. It is not a supported component release. React 19.3.1, Vue 3.5.44, Angular 22.2.4 and Svelte 5.57.3 pass navigation-only compiled/browser fixtures using actual packages. They remain preview pins until the complete component and delivery gates pass. Next.js, Nuxt and SvelteKit client/SSR boundaries remain unverified. Do not infer support from custom-element interoperability or compiler metadata alone. @@ -44,3 +45,8 @@ negative fixture must reproduce the missing-metadata failure. This prevents deve from making package acceptance pass when a fresh hosted runner cannot resolve the same dependency. See [framework bindings](framework-adapters.md) for the exact preview API, compilation and teardown evidence. + +The core text field uses native ElementInternals in Chromium. Native form submission, +required/length/custom validation, disabled fieldsets, readonly, reset and controlled edits are +covered. Field framework bridges and other engines remain unverified; navigation-only framework +results do not imply general form support. diff --git a/docs/component-library.md b/docs/component-library.md index 42adfa0..bbaa7b5 100644 --- a/docs/component-library.md +++ b/docs/component-library.md @@ -135,3 +135,60 @@ forms, other families and the complete framework/client boundary matrix remain s The interaction model follows [WAI-ARIA APG tabs](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/) and uses Lit's documented [reactive properties](https://lit.dev/docs/components/properties/) and [events](https://lit.dev/docs/components/events/). + +## Controlled text field + +The second shared family is a single-line text field. `textFieldState` validates immutable presentation +configuration: label, optional description/placeholder/error, required/readOnly, UTF-16 min/max +lengths and localized required/short/long messages. `textFieldValue` accepts well-formed single-line +strings up to 16,384 UTF-16 units; it preserves literal text and does not log values. Form names are +bounded inert text. Invalid input has the bounded `invalid_field` diagnostic. + +`vyp-ha-field` is a form-associated element with one native input, an actual label and +linked help/error text. Its state object and value are properties. Name, disabled and readonly have standard +host attribute semantics; the value attribute supplies defaultValue for initial/reset behavior. +Current value is never reflected into HTML. Explicit client registration and properties assigned +before upgrade follow the same cohort rules as navigation. + +Values remain controlled by the application. A typed, cancelable, composed `ha-value-request` +requests input/reset/restore/autocomplete changes; assigning a value does not emit one. A controller +accepts by assigning value, or rejects by leaving it unchanged/canceling. The field retains accepted +form state and restores the caret when an asynchronously accepted request matches the proposal. +Composition must not emit partial text or overwrite a newer application value. Native form reset +and state-restoration callbacks use that same controlled protocol; framework form bridges must +acknowledge it using their own model APIs, including Angular's value accessor. + +ElementInternals supplies form ownership, name/value submission, disabled fieldsets and constraint +validation. The readonly host attribute bars validation and makes the native input non-editable. +The public form/labels/validity APIs, focus, blur, custom validity, reset, reparenting and keyboard +submission are exercised in actual Chromium forms. Import `HaTextField` and `registerField` from +`@vypdev/ha-ui/field`, and call registration at the explicit client boundary. Import the pure +contracts from `@vypdev/ha-ui/field-contracts`; these require no DOM globals or DOM types. The initial field is text-only; other input types, +selects, checkboxes, buttons and dialogs remain required separate work. React, Vue, Angular and +Svelte field bindings and native form-model bridges are the next increment; existing navigation +bindings do not establish form support. Browser state-restoration callbacks are exercised +explicitly; automatic history restoration, real device IME behavior and other engines remain +unverified. No complete family matrix or product acceptance is claimed. + +The implementation follows the [HTML custom-element form contract](https://html.spec.whatwg.org/dev/custom-elements.html) +and [form control infrastructure](https://html.spec.whatwg.org/dev/form-control-infrastructure.html). + + +The standard `form` attribute supports an external form owner. `disabled` is a boolean attribute: +`disabled="false"` still disables the field. `readOnly` projects the immutable state's readOnly +flag and reflects `readonly`. Invalid name/default attributes are restored before their bounded +error is reported; current values are never included in diagnostics. Removing a text attribute +retains its absence. Programmatic values participate in required/min/max/custom validation. +Native form validation reveals linked errors as well as field-level checkValidity/reportValidity. +An application can cancel Enter in its key handler; a deferred submit checks that the owner is +unchanged and that the field is still enabled. It preserves the first native default submitter, +including its name/value and disabled state. With no submitter, multiple blocking fields prevent +implicit submission. Synthetic key events do not trigger submission. + +The native input has a real associated label and delegates focus through an external label too. +The core owns all field markup, 56px minimum height, 16px padding, focus/error treatment and live +HA theme inheritance. Field tokens additionally include `--vyp-ha-field-radius` and +`--vyp-ha-danger`; shadow parts are `field`, `label`, `input`, `help` and `error`. Controlled +reset requires the application to acknowledge the request, just like a normal edit. Cancellation +does not undo a value explicitly assigned by application code. This preview stores no values, +performs no network calls and grants no application permission. diff --git a/docs/evidence/shared-ui-foundation.json b/docs/evidence/shared-ui-foundation.json index e2bd399..2f8815d 100644 --- a/docs/evidence/shared-ui-foundation.json +++ b/docs/evidence/shared-ui-foundation.json @@ -1,6 +1,6 @@ { - "recordedAtUTC": "2026-11-04T05:24:06.553322+00:00", - "status": "Partial source increment: one navigation/panel family with four real navigation adapters. No complete component/form/framework SSR/Storybook/publication/application adoption acceptance.", + "recordedAtUTC": "2026-11-04T06:18:51.424816+00:00", + "status": "Partial source increment: shared navigation/panels with four navigation adapters and one controlled native form-associated text field. Field framework bridges and full capability acceptance remain required.", "issue": 7, "nativePublication": { "run": 41767973796, @@ -15,56 +15,66 @@ "allStillRequired": true }, "implemented": [ - "One controlled framework-neutral navigation/panel implementation with explicit registration, immutable validated data and native typed/cancelable intent.", + "Shared controlled navigation/panels and a single-line text field; immutable pure contracts, explicit client registration, original typed cancelable events and one visual implementation per family.", "Real React, Vue, Angular and Svelte navigation bindings; runtime imports are their own framework only, with type-only core imports and no alternate visuals.", - "187 passing Node/Chromium contract, architecture, CI and component/binding tests; independent filesystem inventory covers every authored runtime file, including Svelte.", + "300 passing Node/Chromium contract, architecture, CI and component/binding tests; independent filesystem inventory covers every authored runtime file including Svelte.", "Strict source and consumer-tool types plus Svelte package/test-fixture checks; no library checks skipped. Angular APF and Svelte declarations built by official package tools.", "All five unpublished coordinates independently built/packed twice with identical bytes. Actual installed sources and publication metadata verified.", "Four real clean frozen offline consumers compile against installed package declarations and pass props/events/content/disabled/EN-ES/themes/keyboard/unmount/remount in Chromium; no foreign framework is installed.", - "Angular actual consumer is AOT compiled and uses the official linker for all partial framework libraries, without a JIT fallback." + "Angular actual consumer is AOT compiled and uses the official linker for all partial framework libraries, without a JIT fallback.", + "Core text field passes actual form ownership/submission, default submitter, required/length/custom validation, disabled fieldsets, readonly, reset, explicit restoration callbacks, focus/labels, controlled caret, composition-boundary and invalid-input regression tests. Its actual installed package passes compiled Chromium form journeys." ], "coverage": { - "statements": { - "total": 203, - "covered": 203, + "lines": { + "total": 246, + "covered": 246, "skipped": 0, "pct": 100 }, - "branches": { - "total": 141, - "covered": 141, + "statements": { + "total": 433, + "covered": 433, "skipped": 0, "pct": 100 }, "functions": { - "total": 62, - "covered": 62, + "total": 119, + "covered": 119, "skipped": 0, "pct": 100 }, - "lines": { - "total": 135, - "covered": 135, + "branches": { + "total": 326, + "covered": 326, + "skipped": 0, + "pct": 100 + }, + "branchesTrue": { + "total": 0, + "covered": 0, "skipped": 0, "pct": 100 } }, "sourceIdentities": { "packages/angular/src/index.ts": "e17c1826cff3840e8e68d65757a9f628b73fbeff5242e51406307e9f8bc9ab75", + "packages/core/src/components/field.ts": "dcdb80f521a15e4f41000d3e62937e94925c9fe72cdebb548c365e3eb9b68199", "packages/core/src/components/tabs.ts": "5e84a250c9758388d10f4b2e9de55be6d18e57754aaf38d1a0deb71262c58e36", - "packages/core/src/contracts.ts": "a786f32769cd69d0653d6292ae8b231b19986a07ad46c998a02d3948c2d5006e", - "packages/core/src/index.ts": "10bd6958315d1ae23d216c0b752126e3fc7153f442f8c71e206cfa7e9757f86d", + "packages/core/src/contracts.ts": "457c418cbfcafe5ebb1f834a1f8c2c19f484bad15ea8df5bc65502eec6e19c91", + "packages/core/src/field-contracts.ts": "f96143c5828a04761eb3da28347b8560709c98eb222d4410ced952b3891a78dd", + "packages/core/src/index.ts": "11c097d6824555a190c240ba7dd21cb985363a9415adb8a132747f545b3ba9df", + "packages/core/src/presentation-text.ts": "ad2eb8396a26638ba8d2f2f7cee3656195646aaea077cdc0d3ecd06fc95b3df2", "packages/react/src/index.ts": "8ac4696196e591818a2041d9747fd92c3b60a2b2b3e84f8d7b086938bf81d230", - "packages/svelte/src/Tabs.svelte": "974845c12ec9d760b6f76a43ae89fd4c1618d7b4d56381d98d16f65abc5b27c4", "packages/svelte/src/index.ts": "9fbc2e899ae344ee2f046b688dd3bbf14a74fdc7c755e3ea0a6cb37d57acc2d6", "packages/svelte/src/types.ts": "742b73b64fcfd139f293ebab02972b3fbe94bf15b0860d5122af4e141a2aa58a", "packages/vue/src/index.ts": "90c5273ef2cf67fb6d8bb87f1514afe7322d675ecef55a91c9e94790c79679ae", + "packages/svelte/src/Tabs.svelte": "974845c12ec9d760b6f76a43ae89fd4c1618d7b4d56381d98d16f65abc5b27c4", "tooling/coverage.mjs": "ad527a5d04517adb1992ac65a9abfffcabb9a2001f7f652c732dbe35899b56c8" }, "packedContract": { - "scope": "Core data and navigation component only; no framework adapter or application adoption", - "sha256": "bb09f0f83797c895fdd8d425edfcbdac3307e36d7d64b30c96856f2f5f9d01ad", - "bytes": 9609, + "scope": "Core data, navigation and one text field; field framework adapters, broader controls and application adoption remain required", + "sha256": "51d41982c7e20d667894ae4172523d909cc84103cb1ee02aa95cf3f25f371596", + "bytes": 29752, "reproducible": true, "cleanOfflineInstall": true, "frozenRuntimeLock": true, @@ -78,13 +88,21 @@ "compiledClient": true, "navigation": true, "panels": true, + "textField": { + "controlledInput": true, + "nativeSubmitter": true, + "requiredValidation": true, + "reset": true, + "disabledSubmission": true + }, "runtimeSpanishAndTheme": true, "reducedMotion": true, "consoleErrors": [] } }, "limitations": [ - "Only navigation/panels are implemented. Buttons, fields, selects, dialogs, other families and framework-native form bridges remain required.", + "Only navigation/panels and one text field are implemented. Field adapters/form bridges, buttons, selects, checkboxes, dialogs and other families remain required.", + "Text-field composition events and restoration callbacks are explicitly exercised; real device IME and automatic browser history restoration remain unverified.", "Exact recorded framework pins pass navigation-only compiled consumers. Full cohort support and public peer ranges cannot be inferred from that scope.", "Next.js, Nuxt and SvelteKit explicit server-shell/client fixtures, hydration behavior and other browser engines remain unverified.", "All six delivery refinements, accepted-candidate promotion/publication recovery, Storybook, publisher authority and live publication remain required.", @@ -102,19 +120,33 @@ }, "lockSha256": "78ddfdb8c4822b70cbbf48ba31e1c5968b558d99d1c6c6ea1b98da23e8bdbb44", "browserEvidence": { - "componentCases": 28, + "componentCases": 53, "engine": "Chromium 156.0.8078.4", "actualPackageImages": [ + "test-results/packed-angular-dark-es.png", + "test-results/packed-field-dark-es.png", "test-results/packed-navigation-light.png", "test-results/packed-panels-dark-es.png", "test-results/packed-react-dark-es.png", - "test-results/packed-vue-dark-es.png", - "test-results/packed-angular-dark-es.png", - "test-results/packed-svelte-dark-es.png" + "test-results/packed-svelte-dark-es.png", + "test-results/packed-vue-dark-es.png" ], "imagesInspected": true, "focusRegression": "focus-return-red.log reproduces the stale tab-stop; full green run has no skipped tests.", - "initialTestAttempt": "Browser cases were authored before the renderer. The first attempted execution could not start because the pinned Chromium binary was absent; no behavioral red result is claimed for that attempt. After runtime provisioning, the first actual run found two fixture/server-boundary issues, recorded in browser-first-execution.log." + "initialTestAttempt": "Browser cases were authored before the renderer. The first attempted execution could not start because the pinned Chromium binary was absent; no behavioral red result is claimed for that attempt. After runtime provisioning, the first actual run found two fixture/server-boundary issues, recorded in browser-first-execution.log.", + "textFieldCases": 25, + "localRetainedImages": [ + "work/current-preview/packed-angular-dark-es.png", + "work/current-preview/packed-field-dark-es.png", + "work/current-preview/packed-navigation-light.png", + "work/current-preview/packed-panels-dark-es.png", + "work/current-preview/packed-react-dark-es.png", + "work/current-preview/packed-svelte-dark-es.png", + "work/current-preview/packed-vue-dark-es.png" + ], + "fieldVisualReview": "The actual packed dark Spanish field image was inspected; unchanged navigation/framework images match the previously inspected bytes.", + "fieldRegression": "invalid-edit-red.log reproduced one failure: a malformed synthetic edit left a rejected draft visible. current-coverage.log passes the permanent regression and restores accepted DOM/form state without emitting intent.", + "fieldInitialRed": "renderer-red.log rejected the missing renderer module before implementation; this is an API-absence red, not a behavioral assertion result." }, "consumerIsolationRegression": { "failedHostedRun": "https://github.com/vypdev/ha-ui/actions/runs/41773354203/job/126387929863", @@ -140,7 +172,7 @@ }, { "file": "file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz", - "sha256": "bb09f0f83797c895fdd8d425edfcbdac3307e36d7d64b30c96856f2f5f9d01ad" + "sha256": "51d41982c7e20d667894ae4172523d909cc84103cb1ee02aa95cf3f25f371596" } ], "props": true, @@ -165,7 +197,7 @@ }, { "file": "file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz", - "sha256": "bb09f0f83797c895fdd8d425edfcbdac3307e36d7d64b30c96856f2f5f9d01ad" + "sha256": "51d41982c7e20d667894ae4172523d909cc84103cb1ee02aa95cf3f25f371596" } ], "props": true, @@ -190,7 +222,7 @@ }, { "file": "file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz", - "sha256": "bb09f0f83797c895fdd8d425edfcbdac3307e36d7d64b30c96856f2f5f9d01ad" + "sha256": "51d41982c7e20d667894ae4172523d909cc84103cb1ee02aa95cf3f25f371596" } ], "props": true, @@ -215,7 +247,7 @@ }, { "file": "file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz", - "sha256": "bb09f0f83797c895fdd8d425edfcbdac3307e36d7d64b30c96856f2f5f9d01ad" + "sha256": "51d41982c7e20d667894ae4172523d909cc84103cb1ee02aa95cf3f25f371596" } ], "props": true, @@ -231,13 +263,13 @@ ] }, "packedCohort": { - "scope": "Unpublished navigation preview only; no registry publication or full capability acceptance", + "scope": "Unpublished navigation and core text-field preview; no registry publication or full capability acceptance", "cohort": [ { "coordinate": "@vypdev/ha-ui", "version": "0.1.0-next.1", - "sha256": "bb09f0f83797c895fdd8d425edfcbdac3307e36d7d64b30c96856f2f5f9d01ad", - "bytes": 9609 + "sha256": "51d41982c7e20d667894ae4172523d909cc84103cb1ee02aa95cf3f25f371596", + "bytes": 29752 }, { "coordinate": "@vypdev/ha-ui-angular", @@ -287,7 +319,8 @@ "otherBrowserEngines": false, "applicationAdoption": false, "publication": false, - "storybook": false + "storybook": false, + "coreNativeTextField": true }, "coldSourceValidation": { "compiledDistributionsPresent": false, @@ -299,9 +332,10 @@ "svelte" ], "types": "All strict source, Node-tool and both Svelte checks passed", - "unitAndBrowserTests": 187, + "unitAndBrowserTests": 300, "coverage": "100% statements/branches/functions/lines per authored file and globally", "restoration": "Every previous compiled distribution restored byte-for-byte", - "reason": "Source checking must not depend on old local declaration output; Svelte source checks use the source project while package emission retains its separate build project." + "reason": "Source checking must not depend on old local declaration output; Svelte source checks use the source project while package emission retains its separate build project.", + "restoredFiles": 44 } } diff --git a/docs/provenance.md b/docs/provenance.md index 143682c..0d8d928 100644 --- a/docs/provenance.md +++ b/docs/provenance.md @@ -29,3 +29,9 @@ second navigation controller. Angular packaging retains its authored source and partial declarations and an exact tslib dependency. React/Vue include their source/declaration maps; Svelte includes source, preprocessed components and generated declarations. Third-party compilers, frameworks and helpers retain their own package licenses; no third-party runtime source is copied here. + +The text-field increment adapts the 56px field minimum height, 16px padding, 6px label/help gaps +and 14px/12px text geometry from +[Gateway ui-catalog.css lines 143–150 at the same revision](https://github.com/vypdev/homeassistant-gateway/blob/e47dde303e475a6d03e1d6697ec87a934e93263b/frontend/src/ui/ui-catalog.css#L143-L150). +Form ownership, controlled requests, validation, composition handling and accessible native input +semantics are authored here. No private Home Assistant component or external asset is imported. diff --git a/packages/core/package.json b/packages/core/package.json index adfe977..3e8510b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -27,6 +27,14 @@ "./tabs": { "types": "./dist/components/tabs.d.ts", "import": "./dist/components/tabs.js" + }, + "./field-contracts": { + "types": "./dist/field-contracts.d.ts", + "import": "./dist/field-contracts.js" + }, + "./field": { + "types": "./dist/components/field.d.ts", + "import": "./dist/components/field.js" } }, "publishConfig": { diff --git a/packages/core/src/components/field.ts b/packages/core/src/components/field.ts new file mode 100644 index 0000000..a54b0cd --- /dev/null +++ b/packages/core/src/components/field.ts @@ -0,0 +1,148 @@ +import {LitElement,css,html,nothing} from 'lit'; +import {FieldInputError,textFieldName,textFieldState,textFieldValue,textFieldValidity,type TextFieldState} from '../field-contracts.js'; + +export type FieldValueReason='input'|'reset'|'restore'|'autocomplete'; +export interface FieldValueDetail {readonly value:string;readonly reason:FieldValueReason;} +export type FieldValueEvent=CustomEvent; +type Proposal={value:string;reason:FieldValueReason;start:number|null;end:number|null;direction:'forward'|'backward'|'none'|null}; +const attributeText={fromAttribute:(value:string|null)=>value??''}; +function boolean(value:unknown):boolean {if(typeof value!=='boolean')throw new FieldInputError();return value;} + +/** A controlled single-line field with native form ownership and constraint validation. */ +export class HaTextField extends LitElement { + static formAssociated=true; + static shadowRootOptions={...LitElement.shadowRootOptions,delegatesFocus:true}; + static properties={state:{attribute:false,noAccessor:true},value:{attribute:false,noAccessor:true}, + defaultValue:{attribute:'value',converter:attributeText,noAccessor:true},name:{converter:attributeText,noAccessor:true}, + disabled:{type:Boolean,noAccessor:true},readOnly:{attribute:'readonly',type:Boolean,noAccessor:true}}; + // Field geometry adapted from Gateway's MIT ui-catalog.css. All rendering stays in the shared core. + static styles=css` + :host {display:block;min-width:0;font:400 14px/1.35 var(--vyp-ha-font-family,var(--primary-font-family,Roboto,Noto,sans-serif));color:var(--vyp-ha-text,var(--primary-text-color,#1c1c1c));} + .field {display:grid;gap:6px;} label {font-size:14px;line-height:1.35;} + input {box-sizing:border-box;width:100%;min-width:0;min-height:56px;border:1px solid var(--vyp-ha-border,var(--divider-color,#e0e0e0)); + border-radius:var(--vyp-ha-field-radius,4px);padding:16px;color:inherit;background:var(--vyp-ha-surface,var(--card-background-color,#fff));font:inherit;} + input:focus-visible {outline:2px solid var(--vyp-ha-primary,var(--primary-color,#0075b5));outline-offset:1px;} + input:disabled {opacity:.5;cursor:not-allowed;} input[aria-invalid=true] {border-color:var(--vyp-ha-danger,var(--error-color,#db4437));} + .help,.error {font-size:12px;line-height:1.35;} .help {color:var(--vyp-ha-text-secondary,var(--secondary-text-color,#595959));} + .error {color:var(--vyp-ha-danger,var(--error-color,#db4437));} + `; + #internals=this.attachInternals(); + #state=textFieldState({label:'Value'}); + #value='';#defaultValue='';#name='';#disabled=false;#formDisabled=false; + #dirty=false;#showValidation=false;#customError='';#composing=false;#revision=0;#compositionRevision=0; + #pending:Proposal|null=null;#finalComposition:string|null=null;#fromAttribute=false;#restoringAttribute=false; + constructor(){super();this.addEventListener('invalid',this.#reveal);} + attributeChangedCallback(name:string,previous:string|null,next:string|null):void{ + if(this.#restoringAttribute)return; + try{if(name==='value')textFieldValue(next??'');else if(name==='name')textFieldName(next??'');} + catch(error){ + this.#restoringAttribute=true; + if(previous===null)this.removeAttribute(name);else this.setAttribute(name,previous); + this.#restoringAttribute=false;throw error; + } + this.#fromAttribute=true;super.attributeChangedCallback(name,previous,next);this.#fromAttribute=false; + } + get #input():HTMLInputElement|null{return this.shadowRoot?.querySelector('input')??null;} + get state():TextFieldState{return this.#state;} + set state(raw:TextFieldState){const next=textFieldState(raw),previous=this.#state;this.#state=next;this.#sync();this.requestUpdate('state',previous);} + get value():string{return this.#value;} + set value(raw:string){ + const next=textFieldValue(raw),previous=this.#value,pending=this.#pending; + this.#dirty=pending?.reason==='reset'&&pending.value===next?false:true; + if(next!==previous)this.#revision++; + this.#value=next;this.#sync(); + if(pending!==null&&pending.value===next){this.#input?.setSelectionRange(pending.start,pending.end,pending.direction??undefined);} + this.#pending=null;this.requestUpdate('value',previous); + } + get defaultValue():string{return this.#defaultValue;} + set defaultValue(raw:string){ + const next=textFieldValue(raw),previous=this.#defaultValue;if(next===previous)return; + this.#defaultValue=next;if(!this.#dirty){this.#revision++;this.#value=next;this.#sync();} + this.#reflect('value',next);this.requestUpdate('defaultValue',previous); + } + get name():string{return this.#name;} + set name(raw:string){const next=textFieldName(raw);if(next===this.#name)return;this.#name=next;this.#reflect('name',next);} + get disabled():boolean{return this.#disabled;} + set disabled(raw:boolean){const next=boolean(raw);if(next===this.#disabled)return;this.#disabled=next;this.toggleAttribute('disabled',next);this.#sync();this.requestUpdate();} + get readOnly():boolean{return this.#state.readOnly;} + set readOnly(raw:boolean){const next=boolean(raw);if(next===this.readOnly)return;this.state=textFieldState({...this.state,readOnly:next});} + #reflect(name:string,value:string):void{if(!this.#fromAttribute&&this.getAttribute(name)!==value)this.setAttribute(name,value);} + get form():HTMLFormElement|null{return this.#internals.form;} + get labels():NodeListOf{return this.#internals.labels as NodeListOf;} + get validity():ValidityState{return this.#internals.validity;} + get validationMessage():string{return this.#internals.validationMessage;} + get willValidate():boolean{return this.#internals.willValidate;} + checkValidity():boolean{this.#reveal();return this.#internals.checkValidity();} + reportValidity():boolean{this.#reveal();return this.#internals.reportValidity();} + setCustomValidity(message:string):void{textFieldValidity(this.value,this.state,message);this.#customError=message;this.#sync();this.requestUpdate();} + focus(options?:FocusOptions):void{this.#input?.focus(options);} + blur():void{this.#input?.blur();} + formDisabledCallback(disabled:boolean):void{this.#formDisabled=disabled;this.#sync();this.requestUpdate();} + formResetCallback():void{this.#showValidation=false;this.#request(this.defaultValue,'reset');this.requestUpdate();} + formStateRestoreCallback(value:string|File|FormData,mode:'restore'|'autocomplete'):void{ + if(mode!=='restore'&&mode!=='autocomplete')throw new FieldInputError();this.#request(value,mode); + } + #sync():void{ + const disabled=this.disabled||this.#formDisabled,control=this.#input,validity=textFieldValidity(this.value,this.state,this.#customError); + this.toggleAttribute('readonly',this.readOnly); + this.#internals.setFormValue(disabled?null:this.value,this.value); + this.#internals.setValidity(validity.flags,validity.message,control??undefined); + if(control!==null){control.disabled=disabled;control.readOnly=this.readOnly;if(!this.#composing&&control.value!==this.value)control.value=this.value;} + } + protected updated():void{this.#sync();} + #reveal():void{this.#showValidation=true;this.#sync();this.requestUpdate();} + #request(raw:unknown,reason:FieldValueReason):void{ + try{ + const value=textFieldValue(raw),control=this.#input; + this.#pending={value,reason,start:control?.selectionStart??null,end:control?.selectionEnd??null,direction:control?.selectionDirection??null}; + const event:FieldValueEvent=new CustomEvent('ha-value-request',{detail:Object.freeze({value,reason}),bubbles:true,composed:true,cancelable:true}); + this.dispatchEvent(event); + }finally{this.#sync();} + } + #edit(event:InputEvent):void{ + if(this.#composing||event.isComposing)return; + const value=this.#input!.value; + if(this.disabled||this.#formDisabled||this.readOnly||value===this.#finalComposition||value===this.value){this.#sync();return;} + this.#dirty=true;this.#request(value,'input'); + } + #startComposition():void{this.#composing=true;this.#compositionRevision=this.#revision;} + #endComposition():void{ + this.#composing=false;const value=this.#input!.value; + if(this.#revision!==this.#compositionRevision){this.#sync();return;} + this.#edit(new InputEvent('input'));this.#finalComposition=value; + queueMicrotask(()=>{this.#finalComposition=null;}); + } + #enter(event:KeyboardEvent):void{ + if(!event.isTrusted||event.key!=='Enter'||event.isComposing||this.#composing||event.ctrlKey||event.metaKey||event.altKey||event.shiftKey)return; + // Shadow inputs have no outer form owner. Defer until application key handlers can cancel. + const form=this.form; + setTimeout(()=>{ + if(event.defaultPrevented||this.disabled||this.#formDisabled||form===null||this.form!==form)return; + const controls=[...(form.getRootNode() as Document|ShadowRoot).querySelectorAll('input,button,vyp-ha-field')].filter(control=>control.form===form); + const submitter=controls.find(control=>(control instanceof HTMLButtonElement||control instanceof HTMLInputElement)&&(control.type==='submit'||control.type==='image')) as HTMLButtonElement|HTMLInputElement|undefined; + if(submitter!==undefined){if(!submitter.disabled)form.requestSubmit(submitter);return;} + const blockers=controls.filter(control=>control instanceof HaTextField||(control instanceof HTMLInputElement&&['text','search','url','tel','email','password','date','month','week','time','datetime-local','number'].includes(control.type))); + if(blockers.length<=1)form.requestSubmit(); + }); + } + protected render(){ + const state=this.state,validity=textFieldValidity(this.value,state,this.#customError),message=(this.#showValidation||state.error||this.#customError)?validity.message:''; + const described=[state.description?'help':'',message?'error':''].filter(Boolean).join(' '); + return html`
+ + ${state.description?html`
${state.description}
`:nothing} + ${message?html`
${message}
`:nothing}
`; + } +} +/** Register once at an explicit browser boundary; never import this entrypoint during SSR rendering. */ +export function registerField(registry:CustomElementRegistry|undefined=globalThis.customElements):void{ + if(registry===undefined||typeof document==='undefined')throw new Error('registerField requires a browser CustomElementRegistry'); + const current=registry.get('vyp-ha-field');if(current!==undefined&¤t!==HaTextField)throw new Error('vyp-ha-field registration conflict; use one reviewed package cohort'); + if(current===undefined)registry.define('vyp-ha-field',HaTextField); +} +declare global { + interface HTMLElementTagNameMap {'vyp-ha-field':HaTextField;} + interface HTMLElementEventMap {'ha-value-request':FieldValueEvent;} +} diff --git a/packages/core/src/contracts.ts b/packages/core/src/contracts.ts index 3d7a863..bd70a5a 100644 --- a/packages/core/src/contracts.ts +++ b/packages/core/src/contracts.ts @@ -1,3 +1,4 @@ +import {presentationText} from './presentation-text.js'; /** DOM-independent presentation data. Construction grants no application authority. */ export type NavigationKind = 'navigation' | 'panels'; export interface NavigationItem { @@ -23,11 +24,7 @@ function object(value: unknown, allowed: readonly string[], required: readonly s return value as Record; } function text(value: unknown, maximum: number): string { - if (typeof value !== 'string' || !value.trim() || Array.from(value).length > maximum) invalid(); - for (const character of value) { - const point = character.codePointAt(0)!; - if (point < 32 || point >= 0x7f && point <= 0x9f || point >= 0xd800 && point <= 0xdfff) invalid(); - } + if (!presentationText(value, maximum)) invalid(); return value; } function identifier(value: unknown): string { diff --git a/packages/core/src/field-contracts.ts b/packages/core/src/field-contracts.ts new file mode 100644 index 0000000..9ba9d6f --- /dev/null +++ b/packages/core/src/field-contracts.ts @@ -0,0 +1,50 @@ +import {presentationText} from './presentation-text.js'; +export interface TextFieldState { + readonly label:string; readonly description:string; readonly placeholder:string; + readonly required:boolean; readonly readOnly:boolean; readonly minLength:number; readonly maxLength:number; + readonly error:string; readonly requiredMessage:string; readonly tooShortMessage:string; readonly tooLongMessage:string; +} +export class FieldInputError extends Error { + readonly code='invalid_field'; + constructor(){super('invalid_field');this.name='FieldInputError';} +} +function invalid():never{throw new FieldInputError();} +function length(value:unknown,fallback:number):number { + const current=value===undefined?fallback:value; + if(typeof current!=='number'||!Number.isSafeInteger(current)||current<0||current>16384)invalid(); + return current; +} +export function textFieldState(raw:unknown):TextFieldState { + const keys=['label','description','placeholder','required','readOnly','minLength','maxLength','error','requiredMessage','tooShortMessage','tooLongMessage']; + if(typeof raw!=='object'||raw===null||Array.isArray(raw)||!Object.hasOwn(raw,'label')||Object.keys(raw).some(key=>!keys.includes(key)))invalid(); + const source=raw as Record; + function text(key:string,maximum:number,empty:boolean,fallback:string):string { + const current=source[key]===undefined?fallback:source[key]; + if(!presentationText(current,maximum,empty))invalid();return current; + } + function boolean(key:string):boolean { + const current=source[key]===undefined?false:source[key]; + if(typeof current!=='boolean')invalid();return current; + } + const minLength=length(source.minLength,0),maxLength=length(source.maxLength,16384); + if(minLength>maxLength)invalid(); + return Object.freeze({label:text('label',256,false,''),description:text('description',512,true,''),placeholder:text('placeholder',512,true,''),required:boolean('required'),readOnly:boolean('readOnly'),minLength,maxLength,error:text('error',512,true,''),requiredMessage:text('requiredMessage',512,false,'Enter a value.'),tooShortMessage:text('tooShortMessage',512,false,'Enter more characters.'),tooLongMessage:text('tooLongMessage',512,false,'Enter fewer characters.')}); +} +/** A literal single-line API value; no HTML parsing, normalization or diagnostic echo. */ +export function textFieldValue(value:unknown):string { + if(typeof value!=='string'||value.length>16384||/[\r\n]/u.test(value)||!value.isWellFormed())invalid();return value; +} +export function textFieldName(value:unknown):string { + if(!presentationText(value,128,true))invalid();return value; +} +export interface TextFieldValidity { + readonly flags:Readonly<{valueMissing:boolean;tooShort:boolean;tooLong:boolean;customError:boolean}>; + readonly message:string; +} +export function textFieldValidity(rawValue:unknown,rawState:unknown,customError:unknown=''):TextFieldValidity { + const value=textFieldValue(rawValue),state=textFieldState(rawState); + if(!presentationText(customError,512,true))invalid(); + const flags=Object.freeze({valueMissing:state.required&&value.length===0,tooShort:value.length>0&&value.lengthstate.maxLength,customError:!!(customError||state.error)}); + const message=customError||state.error||(flags.valueMissing?state.requiredMessage:flags.tooShort?state.tooShortMessage:flags.tooLong?state.tooLongMessage:''); + return Object.freeze({flags,message}); +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index abdb4ed..8b2ec83 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,2 +1,4 @@ export {NavigationInputError, navigationState} from './contracts.js'; export type {NavigationKind, NavigationItem, NavigationState} from './contracts.js'; +export {FieldInputError,textFieldState,textFieldValue,textFieldName,textFieldValidity} from './field-contracts.js'; +export type {TextFieldState,TextFieldValidity} from './field-contracts.js'; diff --git a/packages/core/src/presentation-text.ts b/packages/core/src/presentation-text.ts new file mode 100644 index 0000000..05780aa --- /dev/null +++ b/packages/core/src/presentation-text.ts @@ -0,0 +1,5 @@ +/** Pure inert presentation-text policy shared by component contracts. */ +export function presentationText(value:unknown,maximum:number,empty=false):value is string { + if (typeof value!=='string' || (!value.trim() && !(empty && value==='')) || Array.from(value).length>maximum) return false; + return !/[\u0000-\u001f\u007f-\u009f\ud800-\udfff]/u.test(value); +} diff --git a/specs/CATALOG.md b/specs/CATALOG.md index 9280fa2..8004255 100644 --- a/specs/CATALOG.md +++ b/specs/CATALOG.md @@ -10,7 +10,7 @@ debt or convert unknown historic intent into a design decision. | Capability ID | Status | Scope | Primary SDD | Evidence | |---|---|---|---|---| -| `shared-ui-foundation` | Proposed | One DOM component implementation with React, Vue, Angular and Svelte adapters, direct Lit/DOM use, shared Storybook and verified Gateway/Domus adoption. Partial source; no complete capability or release accepted. | [Shared Home Assistant presentation components and framework adapters](./shared-ui-foundation.md) + 1 companion | 66 paths · 2026-11-04 | +| `shared-ui-foundation` | Proposed | One DOM component implementation with React, Vue, Angular and Svelte adapters, direct Lit/DOM use, shared Storybook and verified Gateway/Domus adoption. Partial source; no complete capability or release accepted. | [Shared Home Assistant presentation components and framework adapters](./shared-ui-foundation.md) + 1 companion | 73 paths · 2026-11-04 | ## Evidence map @@ -20,9 +20,9 @@ debt or convert unknown historic intent into a design decision. - Last verified: 2026-11-04 - Specifications: [`specs/shared-ui-foundation.md`](./shared-ui-foundation.md) · [`specs/shared-ui-framework-delivery.md`](./shared-ui-framework-delivery.md) - Workflows: [`.github/workflows/copilot_issue.yml`](../.github/workflows/copilot_issue.yml) · [`.github/workflows/ci_check.yml`](../.github/workflows/ci_check.yml) -- Entrypoints: [`packages/core/src/index.ts`](../packages/core/src/index.ts) · [`packages/core/src/components/tabs.ts`](../packages/core/src/components/tabs.ts) · [`packages/react/src/index.ts`](../packages/react/src/index.ts) · [`packages/vue/src/index.ts`](../packages/vue/src/index.ts) · [`packages/angular/src/index.ts`](../packages/angular/src/index.ts) · [`packages/svelte/src/index.ts`](../packages/svelte/src/index.ts) -- Core code: [`packages/core/src/contracts.ts`](../packages/core/src/contracts.ts) · [`tooling/coverage.mjs`](../tooling/coverage.mjs) · [`packages/core/src/components/tabs.ts`](../packages/core/src/components/tabs.ts) · [`packages/react/src/index.ts`](../packages/react/src/index.ts) · [`packages/vue/src/index.ts`](../packages/vue/src/index.ts) · [`packages/angular/src/index.ts`](../packages/angular/src/index.ts) · [`packages/svelte/src/index.ts`](../packages/svelte/src/index.ts) · [`packages/svelte/src/Tabs.svelte`](../packages/svelte/src/Tabs.svelte) · [`packages/svelte/src/types.ts`](../packages/svelte/src/types.ts) -- Tests: [`tests/contracts/core.test.ts`](../tests/contracts/core.test.ts) · [`tests/coverage.test.mjs`](../tests/coverage.test.mjs) · [`tests/architecture.test.mjs`](../tests/architecture.test.mjs) · [`tests/coverage-evidence.test.mjs`](../tests/coverage-evidence.test.mjs) · [`tests/consumers/packed.test.ts`](../tests/consumers/packed.test.ts) · [`tests/contracts/registration.test.ts`](../tests/contracts/registration.test.ts) · [`tests/browser/tabs.test.ts`](../tests/browser/tabs.test.ts) · [`tests/browser/react.test.ts`](../tests/browser/react.test.ts) · [`tests/browser/vue.test.ts`](../tests/browser/vue.test.ts) · [`tests/browser/angular.test.ts`](../tests/browser/angular.test.ts) · [`tests/browser/svelte.test.mjs`](../tests/browser/svelte.test.mjs) · [`tests/browser/framework-fixture.ts`](../tests/browser/framework-fixture.ts) · [`tests/browser/fixtures/SvelteTabs.svelte`](../tests/browser/fixtures/SvelteTabs.svelte) · [`tests/consumers/frameworks.ts`](../tests/consumers/frameworks.ts) · [`tests/consumers/fixtures/svelte/pnpm-lock.yaml`](../tests/consumers/fixtures/svelte/pnpm-lock.yaml) · [`tests/consumers/fixtures/svelte/index.html`](../tests/consumers/fixtures/svelte/index.html) · [`tests/consumers/fixtures/svelte/Consumer.svelte`](../tests/consumers/fixtures/svelte/Consumer.svelte) · [`tests/consumers/fixtures/svelte/model.ts`](../tests/consumers/fixtures/svelte/model.ts) · [`tests/consumers/fixtures/svelte/package.json`](../tests/consumers/fixtures/svelte/package.json) · [`tests/consumers/fixtures/svelte/client.ts`](../tests/consumers/fixtures/svelte/client.ts) · [`tests/consumers/fixtures/svelte/tsconfig.json`](../tests/consumers/fixtures/svelte/tsconfig.json) · [`tests/consumers/fixtures/svelte/pnpm-workspace.yaml`](../tests/consumers/fixtures/svelte/pnpm-workspace.yaml) · [`tests/consumers/fixtures/vue/pnpm-lock.yaml`](../tests/consumers/fixtures/vue/pnpm-lock.yaml) · [`tests/consumers/fixtures/vue/index.html`](../tests/consumers/fixtures/vue/index.html) · [`tests/consumers/fixtures/vue/model.ts`](../tests/consumers/fixtures/vue/model.ts) · [`tests/consumers/fixtures/vue/package.json`](../tests/consumers/fixtures/vue/package.json) · [`tests/consumers/fixtures/vue/client.ts`](../tests/consumers/fixtures/vue/client.ts) · [`tests/consumers/fixtures/vue/tsconfig.json`](../tests/consumers/fixtures/vue/tsconfig.json) · [`tests/consumers/fixtures/vue/pnpm-workspace.yaml`](../tests/consumers/fixtures/vue/pnpm-workspace.yaml) · [`tests/consumers/fixtures/angular/pnpm-lock.yaml`](../tests/consumers/fixtures/angular/pnpm-lock.yaml) · [`tests/consumers/fixtures/angular/index.html`](../tests/consumers/fixtures/angular/index.html) · [`tests/consumers/fixtures/angular/model.ts`](../tests/consumers/fixtures/angular/model.ts) · [`tests/consumers/fixtures/angular/package.json`](../tests/consumers/fixtures/angular/package.json) · [`tests/consumers/fixtures/angular/client.ts`](../tests/consumers/fixtures/angular/client.ts) · [`tests/consumers/fixtures/angular/tsconfig.json`](../tests/consumers/fixtures/angular/tsconfig.json) · [`tests/consumers/fixtures/angular/pnpm-workspace.yaml`](../tests/consumers/fixtures/angular/pnpm-workspace.yaml) · [`tests/consumers/fixtures/react/pnpm-lock.yaml`](../tests/consumers/fixtures/react/pnpm-lock.yaml) · [`tests/consumers/fixtures/react/index.html`](../tests/consumers/fixtures/react/index.html) · [`tests/consumers/fixtures/react/model.ts`](../tests/consumers/fixtures/react/model.ts) · [`tests/consumers/fixtures/react/package.json`](../tests/consumers/fixtures/react/package.json) · [`tests/consumers/fixtures/react/client.ts`](../tests/consumers/fixtures/react/client.ts) · [`tests/consumers/fixtures/react/tsconfig.json`](../tests/consumers/fixtures/react/tsconfig.json) · [`tests/consumers/fixtures/react/pnpm-workspace.yaml`](../tests/consumers/fixtures/react/pnpm-workspace.yaml) +- Entrypoints: [`packages/core/src/index.ts`](../packages/core/src/index.ts) · [`packages/core/src/components/tabs.ts`](../packages/core/src/components/tabs.ts) · [`packages/react/src/index.ts`](../packages/react/src/index.ts) · [`packages/vue/src/index.ts`](../packages/vue/src/index.ts) · [`packages/angular/src/index.ts`](../packages/angular/src/index.ts) · [`packages/svelte/src/index.ts`](../packages/svelte/src/index.ts) · [`packages/core/src/components/field.ts`](../packages/core/src/components/field.ts) · [`packages/core/src/field-contracts.ts`](../packages/core/src/field-contracts.ts) +- Core code: [`packages/core/src/contracts.ts`](../packages/core/src/contracts.ts) · [`tooling/coverage.mjs`](../tooling/coverage.mjs) · [`packages/core/src/components/tabs.ts`](../packages/core/src/components/tabs.ts) · [`packages/react/src/index.ts`](../packages/react/src/index.ts) · [`packages/vue/src/index.ts`](../packages/vue/src/index.ts) · [`packages/angular/src/index.ts`](../packages/angular/src/index.ts) · [`packages/svelte/src/index.ts`](../packages/svelte/src/index.ts) · [`packages/svelte/src/Tabs.svelte`](../packages/svelte/src/Tabs.svelte) · [`packages/svelte/src/types.ts`](../packages/svelte/src/types.ts) · [`packages/core/src/components/field.ts`](../packages/core/src/components/field.ts) · [`packages/core/src/field-contracts.ts`](../packages/core/src/field-contracts.ts) · [`packages/core/src/presentation-text.ts`](../packages/core/src/presentation-text.ts) +- Tests: [`tests/contracts/core.test.ts`](../tests/contracts/core.test.ts) · [`tests/coverage.test.mjs`](../tests/coverage.test.mjs) · [`tests/architecture.test.mjs`](../tests/architecture.test.mjs) · [`tests/coverage-evidence.test.mjs`](../tests/coverage-evidence.test.mjs) · [`tests/consumers/packed.test.ts`](../tests/consumers/packed.test.ts) · [`tests/contracts/registration.test.ts`](../tests/contracts/registration.test.ts) · [`tests/browser/tabs.test.ts`](../tests/browser/tabs.test.ts) · [`tests/browser/react.test.ts`](../tests/browser/react.test.ts) · [`tests/browser/vue.test.ts`](../tests/browser/vue.test.ts) · [`tests/browser/angular.test.ts`](../tests/browser/angular.test.ts) · [`tests/browser/svelte.test.mjs`](../tests/browser/svelte.test.mjs) · [`tests/browser/framework-fixture.ts`](../tests/browser/framework-fixture.ts) · [`tests/browser/fixtures/SvelteTabs.svelte`](../tests/browser/fixtures/SvelteTabs.svelte) · [`tests/consumers/frameworks.ts`](../tests/consumers/frameworks.ts) · [`tests/consumers/fixtures/svelte/pnpm-lock.yaml`](../tests/consumers/fixtures/svelte/pnpm-lock.yaml) · [`tests/consumers/fixtures/svelte/index.html`](../tests/consumers/fixtures/svelte/index.html) · [`tests/consumers/fixtures/svelte/Consumer.svelte`](../tests/consumers/fixtures/svelte/Consumer.svelte) · [`tests/consumers/fixtures/svelte/model.ts`](../tests/consumers/fixtures/svelte/model.ts) · [`tests/consumers/fixtures/svelte/package.json`](../tests/consumers/fixtures/svelte/package.json) · [`tests/consumers/fixtures/svelte/client.ts`](../tests/consumers/fixtures/svelte/client.ts) · [`tests/consumers/fixtures/svelte/tsconfig.json`](../tests/consumers/fixtures/svelte/tsconfig.json) · [`tests/consumers/fixtures/svelte/pnpm-workspace.yaml`](../tests/consumers/fixtures/svelte/pnpm-workspace.yaml) · [`tests/consumers/fixtures/vue/pnpm-lock.yaml`](../tests/consumers/fixtures/vue/pnpm-lock.yaml) · [`tests/consumers/fixtures/vue/index.html`](../tests/consumers/fixtures/vue/index.html) · [`tests/consumers/fixtures/vue/model.ts`](../tests/consumers/fixtures/vue/model.ts) · [`tests/consumers/fixtures/vue/package.json`](../tests/consumers/fixtures/vue/package.json) · [`tests/consumers/fixtures/vue/client.ts`](../tests/consumers/fixtures/vue/client.ts) · [`tests/consumers/fixtures/vue/tsconfig.json`](../tests/consumers/fixtures/vue/tsconfig.json) · [`tests/consumers/fixtures/vue/pnpm-workspace.yaml`](../tests/consumers/fixtures/vue/pnpm-workspace.yaml) · [`tests/consumers/fixtures/angular/pnpm-lock.yaml`](../tests/consumers/fixtures/angular/pnpm-lock.yaml) · [`tests/consumers/fixtures/angular/index.html`](../tests/consumers/fixtures/angular/index.html) · [`tests/consumers/fixtures/angular/model.ts`](../tests/consumers/fixtures/angular/model.ts) · [`tests/consumers/fixtures/angular/package.json`](../tests/consumers/fixtures/angular/package.json) · [`tests/consumers/fixtures/angular/client.ts`](../tests/consumers/fixtures/angular/client.ts) · [`tests/consumers/fixtures/angular/tsconfig.json`](../tests/consumers/fixtures/angular/tsconfig.json) · [`tests/consumers/fixtures/angular/pnpm-workspace.yaml`](../tests/consumers/fixtures/angular/pnpm-workspace.yaml) · [`tests/consumers/fixtures/react/pnpm-lock.yaml`](../tests/consumers/fixtures/react/pnpm-lock.yaml) · [`tests/consumers/fixtures/react/index.html`](../tests/consumers/fixtures/react/index.html) · [`tests/consumers/fixtures/react/model.ts`](../tests/consumers/fixtures/react/model.ts) · [`tests/consumers/fixtures/react/package.json`](../tests/consumers/fixtures/react/package.json) · [`tests/consumers/fixtures/react/client.ts`](../tests/consumers/fixtures/react/client.ts) · [`tests/consumers/fixtures/react/tsconfig.json`](../tests/consumers/fixtures/react/tsconfig.json) · [`tests/consumers/fixtures/react/pnpm-workspace.yaml`](../tests/consumers/fixtures/react/pnpm-workspace.yaml) · [`tests/contracts/field.test.ts`](../tests/contracts/field.test.ts) · [`tests/browser/field.test.ts`](../tests/browser/field.test.ts) - User documentation: [`README.md`](../README.md) · [`docs/component-library.md`](../docs/component-library.md) · [`docs/compatibility.md`](../docs/compatibility.md) · [`docs/provenance.md`](../docs/provenance.md) · [`docs/evidence/shared-ui-foundation.json`](../docs/evidence/shared-ui-foundation.json) · [`docs/framework-adapters.md`](../docs/framework-adapters.md) ## Maintenance contract diff --git a/specs/catalog.json b/specs/catalog.json index 31c4f1c..7f6a1e5 100644 --- a/specs/catalog.json +++ b/specs/catalog.json @@ -22,7 +22,9 @@ "packages/react/src/index.ts", "packages/vue/src/index.ts", "packages/angular/src/index.ts", - "packages/svelte/src/index.ts" + "packages/svelte/src/index.ts", + "packages/core/src/components/field.ts", + "packages/core/src/field-contracts.ts" ], "code": [ "packages/core/src/contracts.ts", @@ -33,7 +35,10 @@ "packages/angular/src/index.ts", "packages/svelte/src/index.ts", "packages/svelte/src/Tabs.svelte", - "packages/svelte/src/types.ts" + "packages/svelte/src/types.ts", + "packages/core/src/components/field.ts", + "packages/core/src/field-contracts.ts", + "packages/core/src/presentation-text.ts" ], "tests": [ "tests/contracts/core.test.ts", @@ -78,7 +83,9 @@ "tests/consumers/fixtures/react/package.json", "tests/consumers/fixtures/react/client.ts", "tests/consumers/fixtures/react/tsconfig.json", - "tests/consumers/fixtures/react/pnpm-workspace.yaml" + "tests/consumers/fixtures/react/pnpm-workspace.yaml", + "tests/contracts/field.test.ts", + "tests/browser/field.test.ts" ], "documentation": [ "README.md", @@ -97,7 +104,7 @@ "Storybook Pages delivery and human UX review", "Actual Gateway and Domus adoption" ], - "implementationEvidence": "partial: data contract and one shared navigation/panel family, four real navigation bindings and reproducible five-package cohort. Clean frozen offline consumers compile and run props/events/content/disabled/theme/locale/keyboard/remount in Chromium. Other families, form bridges, framework SSR, full browser matrix, Storybook, publication and app adoption remain required." + "implementationEvidence": "partial: pure data contracts, shared navigation/panels and a controlled native form-associated text field. Four real navigation bindings and reproducible clean compiled consumers pass Chromium. Field framework bridges, other families, framework SSR, full browser matrix, Storybook, publication and application adoption remain required." } } ] diff --git a/specs/shared-ui-framework-delivery.md b/specs/shared-ui-framework-delivery.md index 7186345..0eb92ad 100644 --- a/specs/shared-ui-framework-delivery.md +++ b/specs/shared-ui-framework-delivery.md @@ -414,6 +414,10 @@ Prospective repository files (current documents are classified against the immut | `tsconfig.consumers.json` | create | | `tsconfig.svelte-tests.json` | create | | `tsconfig.svelte-source.json` | create | +| `packages/core/src/field-contracts.ts` | create | +| `packages/core/src/presentation-text.ts` | create | +| `tests/contracts/field.test.ts` | create | +| `tests/browser/field.test.ts` | create | The catalog/index update registers the companion under the existing owner; it does not claim implementation. `LICENSE` preserves the upstream MIT source notice and every reused asset requires provenance verification. The Copilot installation and its source remain unchanged. Package coordinates are identifiers, not repository paths. diff --git a/tests/architecture.test.mjs b/tests/architecture.test.mjs index a6ee303..6f2d295 100644 --- a/tests/architecture.test.mjs +++ b/tests/architecture.test.mjs @@ -22,7 +22,9 @@ function allowed(specifier) { } test('the pure contract does not acquire framework or application imports', () => { const code = readFileSync('packages/core/src/contracts.ts', 'utf8'); - expect(imports(code)).toEqual([]); + expect(imports(code)).toEqual(['./presentation-text.js']); + expect(imports(readFileSync('packages/core/src/field-contracts.ts','utf8'))).toEqual(['./presentation-text.js']); + expect(imports(readFileSync('packages/core/src/presentation-text.ts','utf8'))).toEqual([]); }); test('architecture policy rejects direct, re-exported and dynamic framework dependencies', () => { for (const code of ['import x from "react";', 'export * from "@angular/core";', 'await import("vue");', 'await import(target);']) diff --git a/tests/browser/field.test.ts b/tests/browser/field.test.ts new file mode 100644 index 0000000..feb601c --- /dev/null +++ b/tests/browser/field.test.ts @@ -0,0 +1,198 @@ +import {afterEach,expect,test} from 'vitest'; +import {userEvent} from 'vitest/browser'; +import {textFieldState,FieldInputError} from '../../packages/core/src/field-contracts.js'; +import {HaTextField,registerField,type FieldValueEvent} from '../../packages/core/src/components/field.js'; + +const model=(patch:Record={})=>textFieldState({label:'Task name',...patch}); +const input=(field:HaTextField)=>field.shadowRoot!.querySelector('input')!; +async function mount(patch:Record={}) { + registerField(); const form=document.createElement('form'); + const field=document.createElement('vyp-ha-field');field.state=model(patch);field.name='task';form.append(field);document.body.append(form); + await field.updateComplete;return {field,form}; +} +const entries=(form:HTMLFormElement)=>[...new FormData(form).entries()]; +function edit(field:HaTextField,value:string,options:InputEventInit={}) { + input(field).value=value;input(field).dispatchEvent(new InputEvent('input',{bubbles:true,composed:true,...options})); +} +function accept(field:HaTextField) {field.addEventListener('ha-value-request',event=>{if(!event.defaultPrevented)field.value=event.detail.value;});} +afterEach(()=>document.body.replaceChildren()); + +test('preserves values assigned before explicit upgrade and never serializes controlled objects',async()=>{ + const field=document.createElement('vyp-ha-field');field.state=model({label:'Nombre'});field.value='Before upgrade';field.name='task';document.body.append(field); + registerField();await field.updateComplete; + expect(field).toBeInstanceOf(HaTextField);expect(input(field).value).toBe('Before upgrade');expect(input(field).labels![0].textContent).toContain('Nombre'); + expect(field.getAttribute('value')).toBeNull();expect(field.hasAttribute('state')).toBe(false); +}); +test('registers explicitly and idempotently, rejecting conflicting definitions',()=>{ + registerField();registerField(customElements);expect(customElements.get('vyp-ha-field')).toBe(HaTextField); + expect(()=>registerField({get:()=>HTMLElement} as unknown as CustomElementRegistry)).toThrow(/conflict/u); +}); +test('provides an empty usable default and native form association',async()=>{ + const field=document.createElement('vyp-ha-field');document.body.append(field);await field.updateComplete; + expect(field.state.label).toBe('Value');expect(field.value).toBe('');expect(field.defaultValue).toBe('');expect(field.form).toBeNull();expect(field.labels.length).toBe(0); + const form=document.createElement('form');form.id='external';document.body.append(form);field.setAttribute('form','external');field.name='externalName'; + expect(field.form).toBe(form);expect(entries(form)).toEqual([['externalName','']]); + const label=document.createElement('label');field.id='externalField';label.htmlFor=field.id;label.textContent='External';document.body.append(label); + expect([...field.labels]).toEqual([label]);field.removeAttribute('form');expect(field.form).toBeNull(); +}); +test('renders inert label/help/placeholder/error text and connects accessible descriptions',async()=>{ + const {field}=await mount({label:'Name',description:'Helpful ',placeholder:'Example',error:'Review this'}); + expect(field.shadowRoot!.querySelector('script,b')).toBeNull();expect(input(field).labels![0].textContent).toBe('Name');expect(input(field).placeholder).toBe('Example'); + expect(input(field).getAttribute('aria-describedby')).toBe('help error');expect(input(field).getAttribute('aria-invalid')).toBe('true'); + expect(field.shadowRoot!.getElementById('error')!.textContent).toBe('Review this'); + field.state=model();await field.updateComplete;expect(input(field).hasAttribute('aria-describedby')).toBe(false);expect(input(field).getAttribute('aria-invalid')).toBe('false'); +}); +test('keeps accepted values controlled, requests once through shadow boundaries and supports cancellation',async()=>{ + const {field,form}=await mount();field.value='Accepted';await field.updateComplete; + const received:FieldValueEvent[]=[];form.addEventListener('ha-value-request',event=>{received.push(event);event.preventDefault();}); + edit(field,'Proposed');expect(received).toHaveLength(1);expect(received[0].detail).toEqual({value:'Proposed',reason:'input'}); + expect(Object.isFrozen(received[0].detail)).toBe(true);expect(received[0].bubbles&&received[0].composed&&received[0].cancelable).toBe(true); + expect(field.value).toBe('Accepted');expect(input(field).value).toBe('Accepted');expect(entries(form)).toEqual([['task','Accepted']]); + field.value='Server update';await field.updateComplete;expect(received).toHaveLength(1);expect(input(field).value).toBe('Server update'); +}); +test('accepts synchronous edits, transformed results and deferred acknowledgments without losing the caret',async()=>{ + const {field}=await mount();const listener=(event:FieldValueEvent)=>{field.value=event.detail.value;};field.addEventListener('ha-value-request',listener); + edit(field,'Wash dishes');expect(field.value).toBe('Wash dishes'); + input(field).setSelectionRange(2,4,'backward');field.value=field.value;expect(input(field).selectionStart).toBe(2);expect(input(field).selectionDirection).toBe('backward'); + field.removeEventListener('ha-value-request',listener);input(field).value='Wash kitchen';input(field).setSelectionRange(5,7,'backward'); + input(field).dispatchEvent(new InputEvent('input',{bubbles:true}));expect(input(field).value).toBe('Wash dishes'); + field.value='Wash kitchen';expect(input(field).selectionStart).toBe(5);expect(input(field).selectionEnd).toBe(7);expect(input(field).selectionDirection).toBe('backward'); + field.addEventListener('ha-value-request',event=>{field.value=event.detail.value.toUpperCase();},{once:true});edit(field,'new');expect(field.value).toBe('NEW'); + edit(field,'NEW');expect(field.value).toBe('NEW'); +}); +test('preserves literal values and rejects malformed assignments atomically without diagnostic echo',async()=>{ + const {field,form}=await mount();field.value='';await field.updateComplete; + for(const [key,value] of [['state',{label:'secret\ninvalid'}],['value','secret\ninvalid'],['defaultValue','secret\ninvalid'],['name','secret\ninvalid'],['disabled','false'],['readOnly','false']] as const) { + expect(()=>Reflect.set(field,key,value)).toThrow(FieldInputError); + try{Reflect.set(field,key,value);}catch(error){expect((error as Error).message).not.toContain('secret');} + } + expect(field.value).toBe('');expect(entries(form)).toEqual([['task','']]);expect(field.shadowRoot!.querySelector('script')).toBeNull(); +}); +test('honors pristine defaults, dirty values, standard attributes and controlled form reset',async()=>{ + const {field,form}=await mount();field.setAttribute('value','Initial');await field.updateComplete; + expect(field.value).toBe('Initial');expect(field.defaultValue).toBe('Initial');field.defaultValue='New baseline';expect(field.value).toBe('New baseline'); + field.value='Edited';field.defaultValue='Reset value';expect(field.value).toBe('Edited');expect(field.getAttribute('value')).toBe('Reset value'); + form.reset();expect(field.value).toBe('Edited');accept(field);form.reset();expect(field.value).toBe('Reset value'); + field.defaultValue='Pristine again';expect(field.value).toBe('Pristine again');field.removeAttribute('value');expect(field.value).toBe('');expect(field.defaultValue).toBe(''); + field.removeAttribute('name');expect(field.name).toBe('');expect(field.hasAttribute('name')).toBe(false);expect(field.hasAttribute('value')).toBe(false);expect(entries(form)).toEqual([]);field.name='new';field.name='new';expect(field.getAttribute('name')).toBe('new'); +}); +test('respects canceled reset and supports explicit state restore requests without assuming acceptance',async()=>{ + const {field,form}=await mount();field.defaultValue='Default';field.value='Current';const reasons:string[]=[]; + field.addEventListener('ha-value-request',event=>{reasons.push(event.detail.reason);if(event.detail.reason==='reset')event.preventDefault();else field.value=event.detail.value;}); + form.reset();expect(field.value).toBe('Current');field.formStateRestoreCallback('Restored','restore');expect(field.value).toBe('Restored'); + field.formStateRestoreCallback('Autofill','autocomplete');expect(field.value).toBe('Autofill');expect(reasons).toEqual(['reset','restore','autocomplete']); + expect(()=>field.formStateRestoreCallback(new FormData(),'restore')).toThrow(FieldInputError); + expect(()=>field.formStateRestoreCallback('x','other' as 'restore')).toThrow(FieldInputError); + form.addEventListener('reset',event=>event.preventDefault(),{once:true});form.reset();expect(reasons).toHaveLength(3); +}); +test('implements native required, length and custom validity, with localized error messages',async()=>{ + const {field,form}=await mount({required:true,minLength:3,maxLength:5,requiredMessage:'Introduce un valor.',tooShortMessage:'Demasiado corto.',tooLongMessage:'Demasiado largo.'}); + const invalid:Event[]=[];field.addEventListener('invalid',event=>{invalid.push(event);event.preventDefault();}); + expect(field.validity.valueMissing).toBe(true);expect(field.willValidate).toBe(true);expect(field.validationMessage).toBe('Introduce un valor.');expect(field.shadowRoot!.getElementById('error')).toBeNull(); + expect(field.checkValidity()).toBe(false);await field.updateComplete;expect(invalid).toHaveLength(1);expect(input(field).getAttribute('aria-invalid')).toBe('true');expect(field.shadowRoot!.getElementById('error')!.textContent).toBe('Introduce un valor.'); + field.value='ab';expect(field.validity.tooShort).toBe(true);expect(field.validationMessage).toBe('Demasiado corto.'); + field.value='123456';expect(field.validity.tooLong).toBe(true);expect(field.validationMessage).toBe('Demasiado largo.'); + field.value='Valid';expect(field.checkValidity()).toBe(true);expect(field.reportValidity()).toBe(true);expect(form.checkValidity()).toBe(true); + field.setCustomValidity('Review this value.');expect(field.validity.customError).toBe(true);expect(field.reportValidity()).toBe(false);expect(field.validationMessage).toBe('Review this value.'); + expect(()=>field.setCustomValidity('secret\ninvalid')).toThrow(FieldInputError);expect(field.validationMessage).toBe('Review this value.'); + field.setCustomValidity('');expect(field.validity.valid).toBe(true);await field.updateComplete;expect(field.shadowRoot!.getElementById('error')).toBeNull(); +}); +test('shows constraint errors on blur, keeps help connected and delegates focus/blur',async()=>{ + const {field}=await mount({required:true,description:'Choose a name.'});field.focus({preventScroll:true});expect(field.shadowRoot!.activeElement).toBe(input(field)); + field.blur();await field.updateComplete;expect(field.shadowRoot!.activeElement).toBeNull();expect(input(field).getAttribute('aria-describedby')).toBe('help error'); +}); +test('excludes disabled and disabled-fieldset values while preserving the first legend exception',async()=>{ + const {field,form}=await mount({required:true});field.value='Saved';field.disabled=true;field.disabled=true;await field.updateComplete; + expect(field.hasAttribute('disabled')).toBe(true);expect(input(field).disabled).toBe(true);expect(field.willValidate).toBe(false);expect(entries(form)).toEqual([]); + field.disabled=false;field.setAttribute('disabled','false');expect(field.disabled).toBe(true);field.removeAttribute('disabled');await field.updateComplete; + const fieldset=document.createElement('fieldset');const legend=document.createElement('legend');fieldset.append(legend);form.append(fieldset);fieldset.append(field);fieldset.disabled=true;await field.updateComplete; + expect(field.disabled).toBe(false);expect(input(field).disabled).toBe(true);expect(entries(form)).toEqual([]);const received:FieldValueEvent[]=[];field.addEventListener('ha-value-request',event=>received.push(event));edit(field,'Synthetic');expect(received).toHaveLength(0);expect(input(field).value).toBe('Saved'); + legend.append(field);await field.updateComplete;expect(input(field).disabled).toBe(false);expect(entries(form)).toEqual([['task','Saved']]); +}); +test('reflects readonly state and native readonly attributes without discarding form data',async()=>{ + const {field,form}=await mount({required:true,readOnly:true});expect(field.readOnly).toBe(true);expect(input(field).readOnly).toBe(true);expect(field.hasAttribute('readonly')).toBe(true);expect(field.willValidate).toBe(false);expect(entries(form)).toEqual([['task','']]); + const received:FieldValueEvent[]=[];field.addEventListener('ha-value-request',event=>received.push(event));edit(field,'Synthetic');expect(received).toHaveLength(0); + field.readOnly=false;field.readOnly=false;await field.updateComplete;expect(field.state.readOnly).toBe(false);expect(field.willValidate).toBe(true); + field.setAttribute('readonly','false');await field.updateComplete;expect(field.state.readOnly).toBe(true);expect(input(field).readOnly).toBe(true);field.removeAttribute('readonly');expect(field.readOnly).toBe(false); +}); +test('commits one composition proposal and lets a newer external value win over the composing draft',async()=>{ + const {field}=await mount();const received:FieldValueEvent[]=[];field.addEventListener('ha-value-request',event=>received.push(event));accept(field); + input(field).dispatchEvent(new CompositionEvent('compositionstart'));edit(field,'に',{isComposing:true});expect(received).toHaveLength(0);expect(input(field).value).toBe('に'); + input(field).value='日本';input(field).dispatchEvent(new CompositionEvent('compositionend'));edit(field,'日本');expect(received).toHaveLength(1);expect(field.value).toBe('日本');await Promise.resolve(); + input(field).dispatchEvent(new CompositionEvent('compositionstart'));edit(field,'草稿',{isComposing:true});field.value='External';input(field).dispatchEvent(new CompositionEvent('compositionend'));expect(field.value).toBe('External');expect(input(field).value).toBe('External');expect(received).toHaveLength(1); + edit(field,'After');expect(field.value).toBe('After');expect(received).toHaveLength(2); +}); +test('does not corrupt a rejected composition or process composing input without a start event',async()=>{ + const {field}=await mount();field.value='Accepted';const received:FieldValueEvent[]=[];field.addEventListener('ha-value-request',event=>received.push(event)); + edit(field,'Draft',{isComposing:true});expect(received).toHaveLength(0);input(field).dispatchEvent(new CompositionEvent('compositionstart'));edit(field,'Candidate',{isComposing:true});input(field).dispatchEvent(new CompositionEvent('compositionend'));edit(field,'Candidate'); + expect(received).toHaveLength(1);expect(input(field).value).toBe('Accepted');await Promise.resolve();edit(field,'Later');expect(received).toHaveLength(2); +}); +test('submits on trusted Enter with the native default submitter and never bypasses form validation',async()=>{ + const {field,form}=await mount({required:true});const button=document.createElement('button');button.type='submit';button.name='action';button.value='save';button.textContent='Save';form.append(button); + const submissions:SubmitEvent[]=[];form.addEventListener('submit',event=>{event.preventDefault();submissions.push(event as SubmitEvent);});field.addEventListener('invalid',event=>event.preventDefault()); + field.focus();await userEvent.keyboard('{Enter}');expect(submissions).toHaveLength(0);accept(field);await userEvent.keyboard('Kitchen{Enter}');expect(submissions).toHaveLength(1);expect(submissions[0].submitter).toBe(button);expect([...new FormData(form,button)]).toEqual([['task','Kitchen'],['action','save']]); + button.disabled=true;await userEvent.keyboard('{Enter}');expect(submissions).toHaveLength(1); +}); +test('handles implicit submission without a button, multiple blockers, canceled keys and external submitters',async()=>{ + const {field,form}=await mount();const submissions:SubmitEvent[]=[];form.id='owner';form.addEventListener('submit',event=>{event.preventDefault();submissions.push(event as SubmitEvent);}); + field.focus();await userEvent.keyboard('{Enter}');expect(submissions).toHaveLength(1);expect(submissions[0].submitter).toBeNull(); + const other=document.createElement('input');form.append(other);await userEvent.keyboard('{Enter}');expect(submissions).toHaveLength(1);other.remove(); + const second=document.createElement('vyp-ha-field');form.append(second);await second.updateComplete;await userEvent.keyboard('{Enter}');expect(submissions).toHaveLength(1);second.remove(); + const button=document.createElement('input');button.type='submit';button.setAttribute('form','owner');document.body.prepend(button);await userEvent.keyboard('{Enter}');expect(submissions.at(-1)!.submitter).toBe(button); + field.addEventListener('keydown',event=>event.preventDefault(),{once:true});await userEvent.keyboard('{Enter}');expect(submissions).toHaveLength(2); + for(const options of [{key:'a'},{key:'Enter',isComposing:true},{key:'Enter',ctrlKey:true}]){input(field).dispatchEvent(new KeyboardEvent('keydown',{bubbles:true,cancelable:true,...options}));await Promise.resolve();} + expect(submissions).toHaveLength(2);button.remove();field.remove();document.body.append(field);field.focus();await userEvent.keyboard('{Enter}');expect(submissions).toHaveLength(2); +}); +test('retains values across disconnect/reparenting and uses live HA theme tokens and locale',async()=>{ + const {field,form}=await mount();field.value='Saved';field.remove();document.body.append(field);expect(field.value).toBe('Saved');form.append(field);expect(entries(form)).toEqual([['task','Saved']]); + field.style.setProperty('--primary-text-color','rgb(222, 223, 224)');field.style.setProperty('--card-background-color','rgb(30, 31, 32)');field.style.setProperty('--divider-color','rgb(70, 71, 72)'); + field.state=model({label:'Nombre de la tarea',description:'Elige un nombre.',placeholder:'Cocina'});await field.updateComplete; + expect(getComputedStyle(input(field)).color).toBe('rgb(222, 223, 224)');expect(getComputedStyle(input(field)).backgroundColor).toBe('rgb(30, 31, 32)');expect(getComputedStyle(input(field)).minHeight).toBe('56px');expect(input(field).labels![0].textContent).toBe('Nombre de la tarea');expect(input(field).value).toBe('Saved'); +}); + +test('supports restoration and default changes before the first render without requiring a selection range',async()=>{ + registerField();const field=document.createElement('vyp-ha-field');field.focus();field.blur();field.defaultValue='Baseline';field.defaultValue='Baseline'; + field.formStateRestoreCallback('Deferred','restore');document.body.append(field);await field.updateComplete;field.value='Deferred';expect(input(field).value).toBe('Deferred'); + const early=document.createElement('vyp-ha-field');accept(early);early.formStateRestoreCallback('Before render','restore');document.body.append(early);await early.updateComplete;expect(input(early).value).toBe('Before render'); +}); + +test('reveals field errors when the native form validates and preserves their accessible association',async()=>{ + const {field,form}=await mount({required:true});field.addEventListener('invalid',event=>event.preventDefault()); + expect(form.checkValidity()).toBe(false);await field.updateComplete;expect(input(field).getAttribute('aria-invalid')).toBe('true');expect(field.shadowRoot!.getElementById('error')!.textContent).toBe('Enter a value.'); +}); +test('ignores synthetic Enter and composing or modified keyboard input',async()=>{ + const {field,form}=await mount();const submissions:Event[]=[];form.addEventListener('submit',event=>{event.preventDefault();submissions.push(event);}); + input(field).dispatchEvent(new KeyboardEvent('keydown',{key:'Enter',bubbles:true}));await new Promise(resolve=>setTimeout(resolve,0));expect(submissions).toHaveLength(0); + field.focus();for(const key of ['Control','Meta','Alt','Shift'])await userEvent.keyboard('{'+key+'>}{Enter}{/'+key+'}'); + input(field).dispatchEvent(new CompositionEvent('compositionstart'));await userEvent.keyboard('{Enter}');input(field).dispatchEvent(new CompositionEvent('compositionend'));expect(submissions).toHaveLength(0); +}); + +test('focuses the native input through an associated external label',async()=>{ + const {field}=await mount();field.id='namedField';const label=document.createElement('label');label.htmlFor=field.id;label.textContent='External field label';document.body.append(label); + await userEvent.click(label);expect(field.shadowRoot!.activeElement).toBe(input(field)); +}); +test('does not submit a new owner or a control disabled during a trusted key event',async()=>{ + for(const transition of ['own','fieldset','owner']) { + const {field,form}=await mount();const fieldset=document.createElement('fieldset');form.append(fieldset);fieldset.append(field);const other=document.createElement('form');document.body.append(other); + let submissions=0;for(const owner of [form,other])owner.addEventListener('submit',event=>{event.preventDefault();submissions++;}); + field.addEventListener('keydown',()=>{if(transition==='own')field.disabled=true;else if(transition==='fieldset')fieldset.disabled=true;else other.append(field);},{once:true}); + field.focus();await userEvent.keyboard('{Enter}');expect(submissions).toBe(0); + } +}); + +test('restores invalid text attributes atomically and continues accepting later valid attributes',async()=>{ + const {field,form}=await mount();const errors:Error[]=[];const capture=(event:ErrorEvent)=>{event.preventDefault();errors.push(event.error as Error);};window.addEventListener('error',capture); + try{ + field.setAttribute('name','secret\ninvalid');expect(field.name).toBe('task');expect(field.getAttribute('name')).toBe('task'); + field.setAttribute('value','secret\ninvalid');expect(field.defaultValue).toBe('');expect(field.hasAttribute('value')).toBe(false);expect(entries(form)).toEqual([['task','']]); + field.setAttribute('name','valid');field.setAttribute('value','Valid');expect(field.name).toBe('valid');expect(field.value).toBe('Valid');expect(errors).toHaveLength(2); + for(const error of errors){expect(error).toBeInstanceOf(FieldInputError);expect(error.message).toBe('invalid_field');} + }finally{window.removeEventListener('error',capture);} +}); + +test('restores the accepted DOM value after a malformed synthetic edit without emitting a request',async()=>{ + const {field,form}=await mount();field.value='Accepted';const received:FieldValueEvent[]=[],errors:Error[]=[]; + field.addEventListener('ha-value-request',event=>received.push(event));const capture=(event:ErrorEvent)=>{event.preventDefault();errors.push(event.error as Error);};window.addEventListener('error',capture); + try{edit(field,'\ud800');expect(input(field).value).toBe('Accepted');expect(field.value).toBe('Accepted');expect(entries(form)).toEqual([['task','Accepted']]);expect(received).toHaveLength(0);expect(errors).toHaveLength(1);expect(errors[0]).toBeInstanceOf(FieldInputError);} + finally{window.removeEventListener('error',capture);} +}); diff --git a/tests/consumers/packed.test.ts b/tests/consumers/packed.test.ts index 05c4234..85db9fd 100644 --- a/tests/consumers/packed.test.ts +++ b/tests/consumers/packed.test.ts @@ -10,6 +10,7 @@ import {createHash} from 'node:crypto'; import {createServer} from 'vite'; import {chromium} from 'playwright'; import {parse, stringify} from 'yaml'; +import type {HaTextField} from '../../packages/core/src/components/field.js'; import {exerciseFrameworks} from './frameworks.ts'; const execute = promisify(execFile); @@ -83,6 +84,9 @@ test('the actual packed package is reproducible and passes clean DOM-free and re assert.equal(await readFile(join(consumer,'pnpm-lock.yaml'),'utf8'),frozenLock); await assert.rejects(readFile(join(cache,'metadata-v1.3/registry.npmjs.org/lit.json')),{code:'ENOENT'}); await writeFile(join(consumer, 'contract.ts'), `import {navigationState, type NavigationKind} from '@vypdev/ha-ui/contracts'; +import {textFieldState,textFieldValidity} from '@vypdev/ha-ui/field-contracts'; +const field=textFieldState({label:'Name',required:true}); +if(!textFieldValidity('',field).flags.valueMissing)throw Error('Expected pure field validation'); const kind: NavigationKind = 'panels'; const selected: string | null = navigationState({kind, label:'Sections', selected:null, items:[]}).selected; void selected;`); @@ -96,7 +100,7 @@ console.log('Packed contract imported without DOM globals');`], {cwd: consumer}) assert.match(probe.stdout, /Packed contract imported without DOM globals/u); const installed = join(consumer, 'node_modules/@vypdev/ha-ui'); const packageFiles=['LICENSE']; - for (const module of ['contracts','index','components/tabs']) { + for (const module of ['contracts','index','presentation-text','field-contracts','components/tabs','components/field']) { packageFiles.push('src/'+module+'.ts'); for (const suffix of ['.js','.js.map','.d.ts','.d.ts.map']) packageFiles.push('dist/'+module+suffix); } @@ -104,12 +108,22 @@ console.log('Packed contract imported without DOM globals');`], {cwd: consumer}) assert.deepEqual(await readFile(join(installed,file)),await readFile(resolve(root,'packages/core',file))); await writeFile(join(consumer,'index.html'), 'Packed navigation consumer
'); await writeFile(join(consumer,'client.ts'), `import {registerTabs} from '@vypdev/ha-ui/tabs'; +import {registerField} from '@vypdev/ha-ui/field'; +import {textFieldState} from '@vypdev/ha-ui/field-contracts'; import {navigationState} from '@vypdev/ha-ui/contracts'; registerTabs(); const element=document.createElement('vyp-ha-tabs'); element.state=navigationState({kind:'navigation',label:'Household sections',selected:'overview',items:[{id:'overview',label:'Overview',href:'#overview'},{id:'tasks',label:'Tasks',href:'#tasks'}]}); element.addEventListener('ha-selection-request',event=>{event.preventDefault();element.state=navigationState({...element.state,selected:event.detail.id});}); -document.querySelector('main')!.append(element);`); +document.querySelector('main')!.append(element); +registerField(); +const form=document.createElement('form'),field=document.createElement('vyp-ha-field'); +field.state=textFieldState({label:'Task name',description:'A name for this task.',required:true});field.name='task';field.defaultValue='Kitchen'; +field.addEventListener('ha-value-request',event=>{if(!event.defaultPrevented)field.value=event.detail.value;}); +const save=document.createElement('button');save.type='submit';save.name='action';save.value='save';save.textContent='Save task'; +const reset=document.createElement('button');reset.type='reset';reset.textContent='Reset task';form.append(field,save,reset); +form.addEventListener('submit',event=>{event.preventDefault();form.dataset.submitted=JSON.stringify([...new FormData(form,(event as SubmitEvent).submitter as HTMLButtonElement)]);}); +document.querySelector('main')!.append(form);`); await writeFile(join(consumer,'client.tsconfig.json'),JSON.stringify({compilerOptions:{target:'ES2024',module:'NodeNext',moduleResolution:'NodeNext',strict:true,noEmit:true,lib:['ES2024','DOM'],types:[]},include:['client.ts']})); await execute(process.execPath,[resolve(root,'node_modules/typescript/bin/tsc'),'-p','client.tsconfig.json'],{cwd:consumer}); server=await createServer({configFile:false,root:consumer,logLevel:'error',server:{host:'127.0.0.1',port:0}}); @@ -139,14 +153,32 @@ document.querySelector('main')!.append(element);`); assert.equal(await browserPage.getByRole('tabpanel',{name:'Tareas',exact:true}).isVisible(),true); assert.equal(await browserPage.getByRole('button',{name:'Nueva tarea',exact:true}).isVisible(),true); await browserPage.locator('vyp-ha-tabs').screenshot({path:resolve(root,'test-results/packed-panels-dark-es.png')}); + const textbox=browserPage.getByRole('textbox',{name:'Task name',exact:true}); + assert.equal(await textbox.inputValue(),'Kitchen');await textbox.fill('');await browserPage.getByRole('button',{name:'Save task',exact:true}).click(); + assert.equal(await textbox.getAttribute('aria-invalid'),'true');assert.equal(await browserPage.getByText('Enter a value.',{exact:true}).isVisible(),true); + assert.equal(await browserPage.locator('form').getAttribute('data-submitted'),null); + await textbox.fill('Wash dishes');await textbox.press('Enter'); + await browserPage.waitForFunction(()=>document.querySelector('form')!.dataset.submitted!==undefined); + assert.deepEqual(JSON.parse((await browserPage.locator('form').getAttribute('data-submitted'))!),[['task','Wash dishes'],['action','save']]); + await browserPage.getByRole('button',{name:'Reset task',exact:true}).click();assert.equal(await textbox.inputValue(),'Kitchen'); + await browserPage.evaluate(async()=>{ + const field=document.querySelector('vyp-ha-field')!; + document.body.style.backgroundColor='rgb(20,20,20)'; + field.state={...field.state,label:'Nombre de la tarea',description:'Un nombre para esta tarea.',requiredMessage:'Introduce un valor.'}; + field.style.cssText='--primary-text-color:rgb(235,235,235);--secondary-text-color:rgb(189,189,189);--card-background-color:rgb(20,20,20);--divider-color:rgb(66,66,66);--primary-color:rgb(3,169,244)'; + await field.updateComplete;field.disabled=true; + if([...new FormData(field.form!)].length!==0)throw Error('Disabled field must be absent from submission');field.disabled=false; + }); + assert.equal(await browserPage.getByRole('textbox',{name:'Nombre de la tarea',exact:true}).inputValue(),'Kitchen'); + await browserPage.locator('vyp-ha-field').screenshot({path:resolve(root,'test-results/packed-field-dark-es.png')}); assert.deepEqual(errors,[]); - const packedBrowser={engine:'chromium',version:browser.version(),compiledClient:true,navigation:true,panels:true,runtimeSpanishAndTheme:true,reducedMotion:true,consoleErrors:errors}; + const packedBrowser={engine:'chromium',version:browser.version(),compiledClient:true,navigation:true,panels:true,textField:{controlledInput:true,nativeSubmitter:true,requiredValidation:true,reset:true,disabledSubmission:true},runtimeSpanishAndTheme:true,reducedMotion:true,consoleErrors:errors}; await mkdir(resolve(root, 'test-results'), {recursive:true}); - await writeFile(resolve(root, 'test-results/packed-contract.json'), JSON.stringify({scope:'Core data and navigation component only; no framework adapter or application adoption',sha256:digest(first),bytes:first.byteLength,reproducible:true,cleanOfflineInstall:true,frozenRuntimeLock:true,isolatedMetadataCache:true,negativeWithoutLockRejected:true,compiledWithoutDomTypes:true,serverImport:true,packedBrowser},null,2)+'\n'); + await writeFile(resolve(root, 'test-results/packed-contract.json'), JSON.stringify({scope:'Core data, navigation and one text field; field framework adapters, broader controls and application adoption remain required',sha256:digest(first),bytes:first.byteLength,reproducible:true,cleanOfflineInstall:true,frozenRuntimeLock:true,isolatedMetadataCache:true,negativeWithoutLockRejected:true,compiledWithoutDomTypes:true,serverImport:true,packedBrowser},null,2)+'\n'); await browserPage.close();await server.close();server=undefined; const frameworks=await exerciseFrameworks(root,scratch,browser); await writeFile(resolve(root,'test-results/packed-frameworks.json'),JSON.stringify({scope:'Navigation adapters only; forms, framework SSR and application adoption remain required',frameworks},null,2)+'\n'); - await writeFile(resolve(root,'test-results/packed-cohort.json'),JSON.stringify({scope:'Unpublished navigation preview only; no registry publication or full capability acceptance',cohort,reproducible:true,sourceLockSha256:digest(await readFile(resolve(root,'pnpm-lock.yaml')))},null,2)+'\n'); + await writeFile(resolve(root,'test-results/packed-cohort.json'),JSON.stringify({scope:'Unpublished navigation and core text-field preview; no registry publication or full capability acceptance',cohort,reproducible:true,sourceLockSha256:digest(await readFile(resolve(root,'pnpm-lock.yaml')))},null,2)+'\n'); passed = true; } finally { await browser?.close(); await server?.close(); diff --git a/tests/contracts/field.test.ts b/tests/contracts/field.test.ts new file mode 100644 index 0000000..bdc782e --- /dev/null +++ b/tests/contracts/field.test.ts @@ -0,0 +1,49 @@ +import {expect,test} from 'vitest'; +import {FieldInputError,textFieldState,textFieldValue,textFieldName,textFieldValidity} from '../../packages/core/src/field-contracts.js'; +const state=(patch:Record={})=>({label:'Task title',...patch}); +function reject(action:()=>unknown){expect(action).toThrow(FieldInputError);try{action();}catch(error){expect((error as FieldInputError).code).toBe('invalid_field');expect((error as Error).message).toBe('invalid_field');}} +test('field contracts import without DOM and return immutable independent configuration',()=>{ + expect(typeof document).toBe('undefined');const input=state(),value=textFieldState(input);input.label='Changed'; + expect(value.label).toBe('Task title');expect(Object.isFrozen(value)).toBe(true); + expect(value).toEqual({label:'Task title',description:'',placeholder:'',required:false,readOnly:false,minLength:0,maxLength:16384,error:'',requiredMessage:'Enter a value.',tooShortMessage:'Enter more characters.',tooLongMessage:'Enter fewer characters.'}); +}); +test('explicit valid values and literal multilingual text remain intact',()=>{ + const input=state({label:'Título 🧹 ',description:'Description',placeholder:'A title',required:true,readOnly:true,minLength:2,maxLength:32,error:'Review this value',requiredMessage:'Introduce un valor.',tooShortMessage:'Introduce más caracteres.',tooLongMessage:'Introduce menos caracteres.'}); + expect(textFieldState(input)).toEqual(input); +}); +test.each([null,undefined,[],true,'field',{},Object.create({label:'Inherited'}),state({html:'unsafe'})])('rejects malformed or extended configuration',value=>reject(()=>textFieldState(value))); +test.each(['', ' ', 3, null, '\u0000', '\u007f', '\ud800', 'a'.repeat(257)])('rejects an invalid accessible label',label=>reject(()=>textFieldState(state({label})))); +test.each(Array.from({length:32},(_,n)=>String.fromCodePoint(0x80+n)))('rejects every C1 label control',control=>reject(()=>textFieldState(state({label:'A'+control+'B'})))); +test.each(['description','placeholder','error'])('optional %s preserves an explicit empty string and has bounded inert text',field=>{ + expect(textFieldState(state({[field]:''}))[field as 'description']).toBe(''); + for(const value of [null,3,' ','\udfff','x'.repeat(513)])reject(()=>textFieldState(state({[field]:value}))); +}); +test.each(['requiredMessage','tooShortMessage','tooLongMessage'])('requires a nonempty bounded %s',field=>{ + for(const value of ['',null,' ','x'.repeat(513),'\u0085'])reject(()=>textFieldState(state({[field]:value}))); +}); +test.each(['required','readOnly'])('does not coerce %s to a boolean',field=>{ + for(const value of [null,1,'false'])reject(()=>textFieldState(state({[field]:value}))); +}); +test.each(['minLength','maxLength'])('validates %s as a bounded integer',field=>{ + for(const value of [null,'3',-1,1.5,Infinity,NaN,16385])reject(()=>textFieldState(state({[field]:value}))); +}); +test('rejects inverted length limits but preserves zero and the exact upper bound',()=>{ + reject(()=>textFieldState(state({minLength:3,maxLength:2}))); + expect(textFieldState(state({maxLength:0})).maxLength).toBe(0); + expect(textFieldState(state({minLength:16384})).minLength).toBe(16384); +}); +test.each(['','Title',' Título 🧹 ','','x'.repeat(16384),'\t'])('preserves a valid single-line field value',value=>expect(textFieldValue(value)).toBe(value)); +test.each([null,undefined,3,{},'a\nb','a\rb','\ud800','x'.repeat(16385)])('rejects an invalid value without diagnostics containing it',value=>reject(()=>textFieldValue(value))); +test.each(['','task_title','Título de tarea','items[]'])('preserves a valid form name',name=>expect(textFieldName(name)).toBe(name)); +test.each([null,undefined,{},' ','x'.repeat(129),'a\u0085b'])('rejects an invalid form name',name=>reject(()=>textFieldName(name))); +test('validity is immutable and distinguishes optional empty, required, short, long and explicit errors',()=>{ + const config=state({required:true,minLength:3,maxLength:5}); + const missing=textFieldValidity('',config);expect(missing.flags).toEqual({valueMissing:true,tooShort:false,tooLong:false,customError:false});expect(missing.message).toBe('Enter a value.'); + expect(textFieldValidity('a',config).message).toBe('Enter more characters.');expect(textFieldValidity('longer',config).message).toBe('Enter fewer characters.'); + expect(textFieldValidity('good',config).message).toBe('');expect(textFieldValidity('',state({minLength:3})).message).toBe(''); + expect(textFieldValidity('',{...config,error:'Server error'}).message).toBe('Server error');expect(textFieldValidity('',config,'Custom error').message).toBe('Custom error'); + expect(Object.isFrozen(missing)).toBe(true);expect(Object.isFrozen(missing.flags)).toBe(true); +}); +test('public validity inputs keep the same bounded validation contract',()=>{ + reject(()=>textFieldValidity(null,state()));reject(()=>textFieldValidity('ok',null));reject(()=>textFieldValidity('ok',state(),'\u0000')); +}); diff --git a/tests/contracts/registration.test.ts b/tests/contracts/registration.test.ts index 9e81333..82ce487 100644 --- a/tests/contracts/registration.test.ts +++ b/tests/contracts/registration.test.ts @@ -5,3 +5,9 @@ test('does not define a tag or render on the server; registration rejects even a expect(globalThis.customElements.get('vyp-ha-tabs')).toBeUndefined(); expect(()=>registerTabs()).toThrow('registerTabs requires a browser CustomElementRegistry'); }); + +import {registerField} from '../../packages/core/src/components/field.js'; +test('field registration rejects server rendering without defining its tag',()=>{ + expect(globalThis.customElements.get('vyp-ha-field')).toBeUndefined(); + expect(()=>registerField()).toThrow('registerField requires a browser CustomElementRegistry'); +}); From 8aefb26503a8882a5093c431b43afb6d5864348c Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Wed, 4 Nov 2026 07:23:30 +0100 Subject: [PATCH 10/55] docs: fix companion compatibility record link --- specs/shared-ui-framework-delivery.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/shared-ui-framework-delivery.md b/specs/shared-ui-framework-delivery.md index 0eb92ad..6bbc11e 100644 --- a/specs/shared-ui-framework-delivery.md +++ b/specs/shared-ui-framework-delivery.md @@ -200,7 +200,7 @@ accepted bytes. Consumer pins do not change while the cohort is partial. ## Links -[Issue #7](https://github.com/vypdev/ha-ui/issues/7) · [Release run]() · [Compatibility record](docs/compatibility.md) +[Issue #7](https://github.com/vypdev/ha-ui/issues/7) · [Release run]() · [Compatibility record](../docs/compatibility.md)
Technical detailsSource revision, package coordinates, versions, digests, and sanitized provider diagnostics.
``` From ecb57a83ee9600574982035baa77993c20db4335 Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Wed, 4 Nov 2026 08:05:19 +0100 Subject: [PATCH 11/55] feat: bridge text fields to four framework form models --- README.md | 4 +- docs/compatibility.md | 14 +- docs/component-library.md | 9 +- docs/evidence/shared-ui-foundation.json | 185 ++++++++++++------ docs/framework-adapters.md | 85 +++++++- docs/provenance.md | 6 + package.json | 5 +- packages/angular/package.json | 3 +- packages/angular/src/index.ts | 1 + packages/angular/src/text-field.ts | 41 ++++ packages/core/src/components/field.ts | 5 +- packages/core/src/field-contracts.ts | 10 + packages/core/src/index.ts | 4 +- packages/react/src/index.ts | 2 + packages/react/src/text-field.ts | 24 +++ packages/svelte/src/Field.svelte | 18 ++ packages/svelte/src/index.ts | 2 + packages/svelte/src/types.ts | 7 + packages/vue/src/index.ts | 1 + packages/vue/src/text-field.ts | 24 +++ pnpm-lock.yaml | 32 +++ specs/CATALOG.md | 6 +- specs/catalog.json | 17 +- specs/shared-ui-framework-delivery.md | 10 + tests/architecture.test.mjs | 5 +- tests/browser/angular-field.test.ts | 74 +++++++ tests/browser/field-framework-fixture.ts | 13 ++ tests/browser/fixtures/SvelteField.svelte | 19 ++ tests/browser/react-field.test.ts | 52 +++++ tests/browser/svelte-field.test.mjs | 37 ++++ tests/browser/vue-field.test.ts | 42 ++++ tests/consumers/fields.ts | 37 ++++ tests/consumers/fixtures/angular/client.ts | 25 ++- tests/consumers/fixtures/angular/model.ts | 3 + tests/consumers/fixtures/angular/package.json | 3 +- .../consumers/fixtures/angular/pnpm-lock.yaml | 42 +++- tests/consumers/fixtures/react/client.ts | 23 ++- tests/consumers/fixtures/react/model.ts | 3 + tests/consumers/fixtures/react/pnpm-lock.yaml | 4 +- .../consumers/fixtures/svelte/Consumer.svelte | 17 +- tests/consumers/fixtures/svelte/client.ts | 3 +- tests/consumers/fixtures/svelte/model.ts | 3 + .../consumers/fixtures/svelte/pnpm-lock.yaml | 4 +- tests/consumers/fixtures/vue/client.ts | 17 +- tests/consumers/fixtures/vue/model.ts | 3 + tests/consumers/fixtures/vue/pnpm-lock.yaml | 4 +- tests/consumers/frameworks.ts | 11 +- tests/consumers/packed.test.ts | 7 +- tests/contracts/field.test.ts | 7 +- tsconfig.json | 6 + vitest.config.ts | 6 +- 51 files changed, 858 insertions(+), 127 deletions(-) create mode 100644 packages/angular/src/text-field.ts create mode 100644 packages/react/src/text-field.ts create mode 100644 packages/svelte/src/Field.svelte create mode 100644 packages/vue/src/text-field.ts create mode 100644 tests/browser/angular-field.test.ts create mode 100644 tests/browser/field-framework-fixture.ts create mode 100644 tests/browser/fixtures/SvelteField.svelte create mode 100644 tests/browser/react-field.test.ts create mode 100644 tests/browser/svelte-field.test.mjs create mode 100644 tests/browser/vue-field.test.ts create mode 100644 tests/consumers/fields.ts diff --git a/README.md b/README.md index f19610a..7a708c8 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ Shared presentation components for Home Assistant applications, including Gatewa The current source implements a DOM-independent navigation data contract, one controlled HA-style upper navigation/panel component, a controlled form-associated text field and a pinned, tested build/coverage foundation. React, Vue, Angular -and Svelte navigation bindings now pass real compiled, packed consumers. -Other component families, form bridges, framework SSR boundaries, Storybook, publication and actual +and Svelte navigation and text-field form bindings pass real compiled, packed consumers. +Other component families and their form bridges, framework SSR boundaries, Storybook, publication and actual application adoption remain required. No package is published or ready for application adoption. diff --git a/docs/compatibility.md b/docs/compatibility.md index e1b4cf1..0b98608 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -1,8 +1,8 @@ # Compatibility and validation status The DOM-independent navigation contract, controlled upper navigation/panel component and their -build/coverage foundation are implemented and tested in Chromium. Navigation adapters for four frameworks now pass real compiled package consumers. Other component -families, form bridges, browser support breadth and framework SSR integration remain unaccepted. +build/coverage foundation are implemented and tested in Chromium. Navigation and text-field form adapters for four frameworks pass real compiled package consumers. Other component +families and their form bridges, browser support breadth and framework SSR integration remain unaccepted. | Tool | Exact pin | Current use | |---|---|---| @@ -17,7 +17,7 @@ families, form bridges, browser support breadth and framework SSR integration re The core preview coordinate/version is package: @vypdev/ha-ui at 0.1.0-next.1. It is unpublished; its source currently contains presentation contracts, one preview navigation/panel component and one controlled text field. It is not a supported component release. -React 19.3.1, Vue 3.5.44, Angular 22.2.4 and Svelte 5.57.3 pass navigation-only compiled/browser +React 19.3.1, Vue 3.5.44, Angular 22.2.4 and Svelte 5.57.3 pass navigation and single-line text-field compiled/browser fixtures using actual packages. They remain preview pins until the complete component and delivery gates pass. Next.js, Nuxt and SvelteKit client/SSR boundaries remain unverified. Do not infer support from custom-element interoperability or compiler metadata alone. @@ -34,8 +34,8 @@ CI Check runs for each PR head with no path filter. Current gates are strict typ tests, independent source-inventory coverage, architecture/CI contracts, compiled declarations, real Chromium component behavior, actual reproducible packed consumption with a DOM-free contract and a compiled browser client, and the original pinned Copilot catalog validator. The packed browser fixture exercises real links, panel slots, keyboard, live Spanish labels/theme, -and reduced motion. The additional four framework fixtures establish navigation binding behavior only; other browser -engines, form bridges and framework SSR remain unverified. +and reduced motion. The additional four framework fixtures establish navigation and text-field model/submission/validation/disabled/reset/lifecycle behavior; +other control types, browser engines and framework SSR remain unverified. Complete component-family/framework fixtures, Storybook, publication and actual app adoption remain required. The clean package fixture derives its runtime-only graph and integrity records from the reviewed @@ -48,5 +48,5 @@ See [framework bindings](framework-adapters.md) for the exact preview API, compi The core text field uses native ElementInternals in Chromium. Native form submission, required/length/custom validation, disabled fieldsets, readonly, reset and controlled edits are -covered. Field framework bridges and other engines remain unverified; navigation-only framework -results do not imply general form support. +covered. Four field framework bridges pass; other controls, other engines and the full library +acceptance remain unverified. Angular Forms is pinned to 22.2.4 alongside Angular Core. diff --git a/docs/component-library.md b/docs/component-library.md index bbaa7b5..67ab021 100644 --- a/docs/component-library.md +++ b/docs/component-library.md @@ -165,8 +165,8 @@ submission are exercised in actual Chromium forms. Import `HaTextField` and `reg `@vypdev/ha-ui/field`, and call registration at the explicit client boundary. Import the pure contracts from `@vypdev/ha-ui/field-contracts`; these require no DOM globals or DOM types. The initial field is text-only; other input types, selects, checkboxes, buttons and dialogs remain required separate work. React, Vue, Angular and -Svelte field bindings and native form-model bridges are the next increment; existing navigation -bindings do not establish form support. Browser state-restoration callbacks are exercised +Svelte text-field bindings and native form-model bridges pass the pinned compiled consumers; other +control families and their bridges remain required. Browser state-restoration callbacks are exercised explicitly; automatic history restoration, real device IME behavior and other engines remain unverified. No complete family matrix or product acceptance is claimed. @@ -192,3 +192,8 @@ HA theme inheritance. Field tokens additionally include `--vyp-ha-field-radius` reset requires the application to acknowledge the request, just like a normal edit. Cancellation does not undo a value explicitly assigned by application code. This preview stores no values, performs no network calls and grants no application permission. + +`fieldValueDetail` is available from the DOM-independent field-contracts entrypoint. It validates +exactly value and reason, accepting input/reset/restore/autocomplete only, snapshots the bounded +text value and freezes the result. Framework model bridges validate before invoking app observers +and use that snapshot after them. FieldValueReason and FieldValueDetail types are also DOM-free. diff --git a/docs/evidence/shared-ui-foundation.json b/docs/evidence/shared-ui-foundation.json index 2f8815d..7700374 100644 --- a/docs/evidence/shared-ui-foundation.json +++ b/docs/evidence/shared-ui-foundation.json @@ -1,6 +1,6 @@ { - "recordedAtUTC": "2026-11-04T06:18:51.424816+00:00", - "status": "Partial source increment: shared navigation/panels with four navigation adapters and one controlled native form-associated text field. Field framework bridges and full capability acceptance remain required.", + "recordedAtUTC": "2026-11-04T07:02:08.742289+00:00", + "status": "Partial source increment: shared navigation/panels and one controlled text field, with real React/Vue/Angular/Svelte navigation bindings and text-field form bridges. Other families and full delivery/application acceptance remain required.", "issue": 7, "nativePublication": { "run": 41767973796, @@ -16,36 +16,37 @@ }, "implemented": [ "Shared controlled navigation/panels and a single-line text field; immutable pure contracts, explicit client registration, original typed cancelable events and one visual implementation per family.", - "Real React, Vue, Angular and Svelte navigation bindings; runtime imports are their own framework only, with type-only core imports and no alternate visuals.", - "300 passing Node/Chromium contract, architecture, CI and component/binding tests; independent filesystem inventory covers every authored runtime file including Svelte.", + "Real React/Vue/Angular/Svelte navigation and single-line text-field bindings. Renderer imports stay type-only; runtime shared imports are DOM-independent validation policies plus each adapter own framework. No alternate visuals.", + "352 passing Node/Chromium contract, architecture, CI and component/binding tests; all 17 authored runtime files are inventoried independently, including Svelte and three zero-executable modules.", "Strict source and consumer-tool types plus Svelte package/test-fixture checks; no library checks skipped. Angular APF and Svelte declarations built by official package tools.", "All five unpublished coordinates independently built/packed twice with identical bytes. Actual installed sources and publication metadata verified.", - "Four real clean frozen offline consumers compile against installed package declarations and pass props/events/content/disabled/EN-ES/themes/keyboard/unmount/remount in Chromium; no foreign framework is installed.", + "Four real clean frozen offline consumers compile installed package declarations and exercise navigation plus controlled text-field editing/cancellation/model writes/native default submitter/validation/disable/reset/EN-ES/themes/teardown/remount in Chromium; no foreign framework is installed.", "Angular actual consumer is AOT compiled and uses the official linker for all partial framework libraries, without a JIT fallback.", - "Core text field passes actual form ownership/submission, default submitter, required/length/custom validation, disabled fieldsets, readonly, reset, explicit restoration callbacks, focus/labels, controlled caret, composition-boundary and invalid-input regression tests. Its actual installed package passes compiled Chromium form journeys." + "Core text field passes actual form ownership/submission, default submitter, required/length/custom validation, disabled fieldsets, readonly, reset, explicit restoration callbacks, focus/labels, controlled caret, composition-boundary and invalid-input regression tests. Its actual installed package passes compiled Chromium form journeys.", + "Angular text field implements ControlValueAccessor and the shared pure Validator: reactive controls, ngModel, nullable/nonnullable reset, dirty/touched and updateOn change/blur/submit pass. Foreign malformed event payloads are rejected before callbacks; validated snapshots survive observer payload mutation." ], "coverage": { "lines": { - "total": 246, - "covered": 246, + "total": 305, + "covered": 305, "skipped": 0, "pct": 100 }, "statements": { - "total": 433, - "covered": 433, + "total": 559, + "covered": 559, "skipped": 0, "pct": 100 }, "functions": { - "total": 119, - "covered": 119, + "total": 154, + "covered": 154, "skipped": 0, "pct": 100 }, "branches": { - "total": 326, - "covered": 326, + "total": 369, + "covered": 369, "skipped": 0, "pct": 100 }, @@ -57,24 +58,28 @@ } }, "sourceIdentities": { - "packages/angular/src/index.ts": "e17c1826cff3840e8e68d65757a9f628b73fbeff5242e51406307e9f8bc9ab75", - "packages/core/src/components/field.ts": "dcdb80f521a15e4f41000d3e62937e94925c9fe72cdebb548c365e3eb9b68199", + "packages/angular/src/index.ts": "b7f1dbce4ca58dbe6be01b53e9dc079593dc38c7ff4c71d0ffcca10de06a4f48", + "packages/angular/src/text-field.ts": "fe0e40f32969c5e3d43bc721f1f0474ecc2fca45e33e12415c30af64a23f4ea4", + "packages/core/src/components/field.ts": "11a2d35a5b4e12b6d034363aef720250e0b157e2187ff3f7bd0f7e5bb63d878a", "packages/core/src/components/tabs.ts": "5e84a250c9758388d10f4b2e9de55be6d18e57754aaf38d1a0deb71262c58e36", "packages/core/src/contracts.ts": "457c418cbfcafe5ebb1f834a1f8c2c19f484bad15ea8df5bc65502eec6e19c91", - "packages/core/src/field-contracts.ts": "f96143c5828a04761eb3da28347b8560709c98eb222d4410ced952b3891a78dd", - "packages/core/src/index.ts": "11c097d6824555a190c240ba7dd21cb985363a9415adb8a132747f545b3ba9df", + "packages/core/src/field-contracts.ts": "130e143f3f69dfaa6e0397cf68cd2ec824823b44ff3f4488d92b19180d50b738", + "packages/core/src/index.ts": "b4b038b1a44cb1bc09c738153a052ff4e599d6bac8ebd951204cb4c75a304ec1", "packages/core/src/presentation-text.ts": "ad2eb8396a26638ba8d2f2f7cee3656195646aaea077cdc0d3ecd06fc95b3df2", - "packages/react/src/index.ts": "8ac4696196e591818a2041d9747fd92c3b60a2b2b3e84f8d7b086938bf81d230", - "packages/svelte/src/index.ts": "9fbc2e899ae344ee2f046b688dd3bbf14a74fdc7c755e3ea0a6cb37d57acc2d6", - "packages/svelte/src/types.ts": "742b73b64fcfd139f293ebab02972b3fbe94bf15b0860d5122af4e141a2aa58a", - "packages/vue/src/index.ts": "90c5273ef2cf67fb6d8bb87f1514afe7322d675ecef55a91c9e94790c79679ae", + "packages/react/src/index.ts": "de04557dd8d23b9c331776fdc2574476853456160650f371a0bc427d22464bca", + "packages/react/src/text-field.ts": "1d9249cb835aa1205bcb634fee2f75af01bc5c7833f9f71391e72de560c67656", + "packages/svelte/src/index.ts": "6cdd70a671fd4d05232b7646838f43ddce086acc8d4af83409d76f366a0ae4db", + "packages/svelte/src/types.ts": "e673d97f2db9564c518bd6d08715093c3db26e9201becb5129cae7a4a99113ab", + "packages/vue/src/index.ts": "638fc7f426cb4c02527938d184e16afb4fec8845e86197159316c1968e2d2df8", + "packages/vue/src/text-field.ts": "e160d93f62669ffb989e3d8d78fb28b0da287226d8157676d5842b6a7c83f612", + "packages/svelte/src/Field.svelte": "174e62b2fd01af8279ab42ce58ab530a0ee14d370bcdb646139f5a143052b8a0", "packages/svelte/src/Tabs.svelte": "974845c12ec9d760b6f76a43ae89fd4c1618d7b4d56381d98d16f65abc5b27c4", "tooling/coverage.mjs": "ad527a5d04517adb1992ac65a9abfffcabb9a2001f7f652c732dbe35899b56c8" }, "packedContract": { "scope": "Core data, navigation and one text field; field framework adapters, broader controls and application adoption remain required", - "sha256": "51d41982c7e20d667894ae4172523d909cc84103cb1ee02aa95cf3f25f371596", - "bytes": 29752, + "sha256": "9ea5d40493a494332fd2688f8b9c57540d96c5538eb3d1b55023f0333213d4fd", + "bytes": 30287, "reproducible": true, "cleanOfflineInstall": true, "frozenRuntimeLock": true, @@ -101,9 +106,9 @@ } }, "limitations": [ - "Only navigation/panels and one text field are implemented. Field adapters/form bridges, buttons, selects, checkboxes, dialogs and other families remain required.", + "Only navigation/panels and one text field with four form bridges are implemented. Buttons, selects, checkboxes, dialogs, other families and their framework bridges remain required.", "Text-field composition events and restoration callbacks are explicitly exercised; real device IME and automatic browser history restoration remain unverified.", - "Exact recorded framework pins pass navigation-only compiled consumers. Full cohort support and public peer ranges cannot be inferred from that scope.", + "Exact recorded framework pins pass navigation and one text-field compiled consumer. Full cohort support and broader public peer ranges cannot be inferred from those two families.", "Next.js, Nuxt and SvelteKit explicit server-shell/client fixtures, hydration behavior and other browser engines remain unverified.", "All six delivery refinements, accepted-candidate promotion/publication recovery, Storybook, publisher authority and live publication remain required.", "Gateway and Domus adoption, installed deployment/recovery and human UX acceptance remain unverified.", @@ -118,18 +123,26 @@ "after": "All 138 unit/architecture/CI-contract cases pass; every C1 code point rejected in group labels, item labels and links; visible U+00A0/U+00A1 preserved.", "manualResolution": false }, - "lockSha256": "78ddfdb8c4822b70cbbf48ba31e1c5968b558d99d1c6c6ea1b98da23e8bdbb44", + "lockSha256": "06fb89a58baf3beb3bc713a88055f184379d218aa187f53792cb4130f38f42d6", "browserEvidence": { "componentCases": 53, "engine": "Chromium 156.0.8078.4", "actualPackageImages": [ "test-results/packed-angular-dark-es.png", + "test-results/packed-angular-field-dark-es.png", + "test-results/packed-angular-field-error-dark-es.png", "test-results/packed-field-dark-es.png", "test-results/packed-navigation-light.png", "test-results/packed-panels-dark-es.png", "test-results/packed-react-dark-es.png", + "test-results/packed-react-field-dark-es.png", + "test-results/packed-react-field-error-dark-es.png", "test-results/packed-svelte-dark-es.png", - "test-results/packed-vue-dark-es.png" + "test-results/packed-svelte-field-dark-es.png", + "test-results/packed-svelte-field-error-dark-es.png", + "test-results/packed-vue-dark-es.png", + "test-results/packed-vue-field-dark-es.png", + "test-results/packed-vue-field-error-dark-es.png" ], "imagesInspected": true, "focusRegression": "focus-return-red.log reproduces the stale tab-stop; full green run has no skipped tests.", @@ -137,16 +150,28 @@ "textFieldCases": 25, "localRetainedImages": [ "work/current-preview/packed-angular-dark-es.png", + "work/current-preview/packed-angular-field-dark-es.png", + "work/current-preview/packed-angular-field-error-dark-es.png", "work/current-preview/packed-field-dark-es.png", "work/current-preview/packed-navigation-light.png", "work/current-preview/packed-panels-dark-es.png", "work/current-preview/packed-react-dark-es.png", + "work/current-preview/packed-react-field-dark-es.png", + "work/current-preview/packed-react-field-error-dark-es.png", "work/current-preview/packed-svelte-dark-es.png", - "work/current-preview/packed-vue-dark-es.png" + "work/current-preview/packed-svelte-field-dark-es.png", + "work/current-preview/packed-svelte-field-error-dark-es.png", + "work/current-preview/packed-vue-dark-es.png", + "work/current-preview/packed-vue-field-dark-es.png", + "work/current-preview/packed-vue-field-error-dark-es.png" ], "fieldVisualReview": "The actual packed dark Spanish field image was inspected; unchanged navigation/framework images match the previously inspected bytes.", "fieldRegression": "invalid-edit-red.log reproduced one failure: a malformed synthetic edit left a rejected draft visible. current-coverage.log passes the permanent regression and restores accepted DOM/form state without emitting intent.", - "fieldInitialRed": "renderer-red.log rejected the missing renderer module before implementation; this is an API-absence red, not a behavioral assertion result." + "fieldInitialRed": "renderer-red.log rejected the missing renderer module before implementation; this is an API-absence red, not a behavioral assertion result.", + "frameworkTextFieldCases": 35, + "frameworkFieldVisualReview": "All eight framework field images are represented by three unique byte groups; each unique valid/error dark Spanish image was inspected. Core geometry and semantics remain shared.", + "frameworkFieldRed": "invalid-event-red.log reproduced four failing adapter cases before the pure payload guard. The final source campaign includes those regressions and observer-mutation cases.", + "packedFieldInitialAttempts": "packed.log stopped before any browser launch because the local browser path variable was absent. packed-with-runtime.log then exposed a premature React UI-counter assertion; the driver now waits for the observed render state. current-packed.log passes all actual compiled forms; no unsupported behavioral red claim is made for the launch failure." }, "consumerIsolationRegression": { "failedHostedRun": "https://github.com/vypdev/ha-ui/actions/runs/41773354203/job/126387929863", @@ -158,7 +183,7 @@ "freshHostedRequired": true }, "packedFrameworks": { - "scope": "Navigation adapters only; forms, framework SSR and application adoption remain required", + "scope": "Navigation and one controlled text field with four framework form bridges; other controls, framework SSR and application adoption remain required", "frameworks": [ { "framework": "react", @@ -168,11 +193,11 @@ "candidateArchives": [ { "file": "file:../pack-1/vypdev-ha-ui-react-0.1.0-next.1.tgz", - "sha256": "5c64d1378091e38eee17ccf3aa74f246bd6489f587c612a28f073149d3406c89" + "sha256": "16fd7155151a13936bcfd7b738142ebd0055cfcb3b3366b7219eb3ae0e66ed6b" }, { "file": "file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz", - "sha256": "51d41982c7e20d667894ae4172523d909cc84103cb1ee02aa95cf3f25f371596" + "sha256": "9ea5d40493a494332fd2688f8b9c57540d96c5538eb3d1b55023f0333213d4fd" } ], "props": true, @@ -182,6 +207,17 @@ "runtimeSpanishAndTheme": true, "keyboard": true, "teardownAndRemount": true, + "textField": { + "controlledModel": true, + "exactlyOnce": true, + "cancellation": true, + "programmaticWrite": true, + "nativeSubmitter": true, + "requiredValidation": true, + "disabledSubmission": true, + "frameworkReset": true, + "teardownAndRemount": true + }, "browser": "156.0.8078.4", "errors": [] }, @@ -193,11 +229,11 @@ "candidateArchives": [ { "file": "file:../pack-1/vypdev-ha-ui-vue-0.1.0-next.1.tgz", - "sha256": "e60799e995b06101f12e5c659b85f8f6359cbe1c7c1c6ac3d7834b036d1baa8b" + "sha256": "21d87f361b247c73385f5d87d6ef4e3cbaca586a1e4ea2f1f02bd4fb857fa0a8" }, { "file": "file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz", - "sha256": "51d41982c7e20d667894ae4172523d909cc84103cb1ee02aa95cf3f25f371596" + "sha256": "9ea5d40493a494332fd2688f8b9c57540d96c5538eb3d1b55023f0333213d4fd" } ], "props": true, @@ -207,6 +243,17 @@ "runtimeSpanishAndTheme": true, "keyboard": true, "teardownAndRemount": true, + "textField": { + "controlledModel": true, + "exactlyOnce": true, + "cancellation": true, + "programmaticWrite": true, + "nativeSubmitter": true, + "requiredValidation": true, + "disabledSubmission": true, + "frameworkReset": true, + "teardownAndRemount": true + }, "browser": "156.0.8078.4", "errors": [] }, @@ -218,11 +265,11 @@ "candidateArchives": [ { "file": "file:../pack-1/vypdev-ha-ui-angular-0.1.0-next.1.tgz", - "sha256": "736fa5858d1c54bb34109f0a2d3b67392c2436bfccbc17ce985c14955ba6ec7a" + "sha256": "46d4ce0ccaad8d180b9892dc9859c28dbcc644fb68600223bdf6d2191939c963" }, { "file": "file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz", - "sha256": "51d41982c7e20d667894ae4172523d909cc84103cb1ee02aa95cf3f25f371596" + "sha256": "9ea5d40493a494332fd2688f8b9c57540d96c5538eb3d1b55023f0333213d4fd" } ], "props": true, @@ -232,6 +279,17 @@ "runtimeSpanishAndTheme": true, "keyboard": true, "teardownAndRemount": true, + "textField": { + "controlledModel": true, + "exactlyOnce": true, + "cancellation": true, + "programmaticWrite": true, + "nativeSubmitter": true, + "requiredValidation": true, + "disabledSubmission": true, + "frameworkReset": true, + "teardownAndRemount": true + }, "browser": "156.0.8078.4", "errors": [] }, @@ -243,11 +301,11 @@ "candidateArchives": [ { "file": "file:../pack-1/vypdev-ha-ui-svelte-0.1.0-next.1.tgz", - "sha256": "afdd612a39a017c55a41be38e2d79e2335a749af646d0e62b63b1049feb9ca52" + "sha256": "732558a22ee756c2ef73d20e64faf70355c41d618fec82dab64ba7e7694e359f" }, { "file": "file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz", - "sha256": "51d41982c7e20d667894ae4172523d909cc84103cb1ee02aa95cf3f25f371596" + "sha256": "9ea5d40493a494332fd2688f8b9c57540d96c5538eb3d1b55023f0333213d4fd" } ], "props": true, @@ -257,6 +315,17 @@ "runtimeSpanishAndTheme": true, "keyboard": true, "teardownAndRemount": true, + "textField": { + "controlledModel": true, + "exactlyOnce": true, + "cancellation": true, + "programmaticWrite": true, + "nativeSubmitter": true, + "requiredValidation": true, + "disabledSubmission": true, + "frameworkReset": true, + "teardownAndRemount": true + }, "browser": "156.0.8078.4", "errors": [] } @@ -268,36 +337,36 @@ { "coordinate": "@vypdev/ha-ui", "version": "0.1.0-next.1", - "sha256": "51d41982c7e20d667894ae4172523d909cc84103cb1ee02aa95cf3f25f371596", - "bytes": 29752 + "sha256": "9ea5d40493a494332fd2688f8b9c57540d96c5538eb3d1b55023f0333213d4fd", + "bytes": 30287 }, { "coordinate": "@vypdev/ha-ui-angular", "version": "0.1.0-next.1", - "sha256": "736fa5858d1c54bb34109f0a2d3b67392c2436bfccbc17ce985c14955ba6ec7a", - "bytes": 2838 + "sha256": "46d4ce0ccaad8d180b9892dc9859c28dbcc644fb68600223bdf6d2191939c963", + "bytes": 5528 }, { "coordinate": "@vypdev/ha-ui-react", "version": "0.1.0-next.1", - "sha256": "5c64d1378091e38eee17ccf3aa74f246bd6489f587c612a28f073149d3406c89", - "bytes": 2706 + "sha256": "16fd7155151a13936bcfd7b738142ebd0055cfcb3b3366b7219eb3ae0e66ed6b", + "bytes": 4175 }, { "coordinate": "@vypdev/ha-ui-svelte", "version": "0.1.0-next.1", - "sha256": "afdd612a39a017c55a41be38e2d79e2335a749af646d0e62b63b1049feb9ca52", - "bytes": 2226 + "sha256": "732558a22ee756c2ef73d20e64faf70355c41d618fec82dab64ba7e7694e359f", + "bytes": 2917 }, { "coordinate": "@vypdev/ha-ui-vue", "version": "0.1.0-next.1", - "sha256": "e60799e995b06101f12e5c659b85f8f6359cbe1c7c1c6ac3d7834b036d1baa8b", - "bytes": 2931 + "sha256": "21d87f361b247c73385f5d87d6ef4e3cbaca586a1e4ea2f1f02bd4fb857fa0a8", + "bytes": 4571 } ], "reproducible": true, - "sourceLockSha256": "78ddfdb8c4822b70cbbf48ba31e1c5968b558d99d1c6c6ea1b98da23e8bdbb44" + "sourceLockSha256": "06fb89a58baf3beb3bc713a88055f184379d218aa187f53792cb4130f38f42d6" }, "frameworkPreviewPins": { "react": "19.3.1", @@ -310,11 +379,15 @@ "sveltePackage": "3.0.2", "sveltePlugin": "7.3.1", "svelteCheck": "4.7.8", - "babel": "8.0.1" + "babel": "8.0.1", + "angularForms": "22.2.4" }, "frameworkBoundaries": { - "validated": "Navigation bindings and local controlled panels only", - "forms": false, + "validated": "Navigation and one controlled single-line text field; other controls remain required", + "forms": { + "singleLineTextField": true, + "otherControls": false + }, "frameworkSSRAndHydration": false, "otherBrowserEngines": false, "applicationAdoption": false, @@ -332,10 +405,10 @@ "svelte" ], "types": "All strict source, Node-tool and both Svelte checks passed", - "unitAndBrowserTests": 300, + "unitAndBrowserTests": 352, "coverage": "100% statements/branches/functions/lines per authored file and globally", "restoration": "Every previous compiled distribution restored byte-for-byte", - "reason": "Source checking must not depend on old local declaration output; Svelte source checks use the source project while package emission retains its separate build project.", - "restoredFiles": 44 + "reason": "All five distributions absent: strict source/consumer/Svelte checks and the full source browser campaign pass. Vitest resolves the current DOM-independent field policy source explicitly; the installed-package fixtures have no source alias.", + "restoredFiles": 55 } } diff --git a/docs/framework-adapters.md b/docs/framework-adapters.md index f6d35ae..dc2f675 100644 --- a/docs/framework-adapters.md +++ b/docs/framework-adapters.md @@ -1,14 +1,15 @@ -# Framework navigation bindings +# Framework bindings -This unpublished preview has one visual implementation and four real navigation adapters. It is -not the completed component library: fields, buttons, dialogs, form bridges, framework SSR/client -fixtures, broader browser support, Storybook, publication and app adoption remain required. +This unpublished preview has shared navigation and one controlled text field, with four real +framework bindings and text-field form bridges. Other controls and their bridges, framework +SSR/client fixtures, broader browser support, Storybook, publication and app adoption remain required. ## Client registration and ownership Call `registerTabs` from `@vypdev/ha-ui/tabs` at the application client boundary before mounting the chosen framework. Importing a binding does not register tags or load the core DOM renderer. The -framework adapters use type-only core imports; their runtime imports are their own framework only. +framework adapters import the renderer only as types; runtime imports are their own framework +and DOM-independent core validation policies. No binding imports Lit or registers a tag. All use the same `vyp-ha-tabs` element, validated `NavigationState`, CSS tokens and typed native `ha-selection-request` event. No routing, household state or authorization belongs in a binding. @@ -76,10 +77,82 @@ Every framework consumer compiles against installed declarations and runs real p events, content projection, disabled booleans, English/Spanish, live theme, keyboard, removal and remount journeys. Installed authored source must match the repository. Published manifests contain neither workspace nor file references. The resulting artifacts include the exact cohort digests and original -browser screenshots. These checks cover navigation only and do not complete the retained 70-case +browser screenshots. These checks cover navigation and one text-field family, and do not complete the retained 70-case capability acceptance inventory or prove a real npm publication. The complete source/type/browser check sequence also passes with all five dist trees absent. Svelte source validation uses `tsconfig.svelte-source.json`; Svelte package emission keeps its own compiler project and follows the core build in pnpm's dependency graph. This prevents a local declaration cache from hiding a fresh-checkout CI failure. + +## Text-field binding contract + +`TextField` is exported by each adapter. All use the explicitly registered +`vyp-ha-field`; applications call `registerField` at the client boundary. State, value, +name, defaultValue and disabled are passed as properties, with a separate optional external +form ID attribute. The core still owns validation, rendering, keyboard and accessibility. +Current values never become HTML attributes. The original cancelable `valueRequest` event +is validated by the pure fieldValueDetail policy before application observers or a framework model callback; a canceled request updates no model. + +React accepts controlled `value` and optional `onValueChange(value)`/`onValueRequest(event)`. +Vue accepts `modelValue`, emits `update:modelValue` and `valueRequest`, and supports `v-model`. +Svelte 5 uses controlled `value` and typed `onValueChange`/`onValueRequest` callback props; +application `$state` remains the model owner. Native form reset requests the supplied +`defaultValue`; the framework model acknowledges it just like a normal edit. + +Angular exposes the actual core tag through a standalone component, synchronous +`valueRequest` output and `ControlValueAccessor`. It supports reactive controls and ngModel, +including model writes, dirty/touched, disable/enable and update-on-blur/submit. The shared +pure validation policy feeds Angular validity. Reset preserves Angular's current reset value +and pristine state rather than applying a conflicting native default a second time. Without +an Angular forms directive, its value input/output remain explicitly application-controlled. +The exact pinned source and actual compiled consumers pass these text-field contracts. Other +control types and their form bridges remain unverified; no complete library acceptance is implied. + + +```tsx + +``` + +```vue + +``` + +```svelte + +title=next} + name="task" defaultValue="Kitchen" /> +``` + +```html +
+ + +``` + +For Angular, import the standalone `TextField` and the applicable forms module. A forms directive +owns value and disabled state through its control; value/disabled inputs apply to standalone +output-driven use. `haField` validation errors contain shared presentation flags only. App-specific +validators and the mapping of their messages to state.error remain application responsibilities. +Angular Forms 22.2.4 and its usual Common/Core/Platform Browser/RxJS peers are required, as exercised +by the frozen fixture. Reactive controls preserve updateOn change/blur/submit, dirty/touched and +nullable/nonnullable resets. ngModel and explicit application-controlled use are also tested. + +Malformed event detail is rejected with invalid_field before observers or model callbacks. The +validated value snapshot is used after the observer, preventing payload mutation from changing a +model callback's input. Request cancellation does not undo an independent model assignment already +made by application code. No adapter performs a household operation, network request or storage write. + +The native Angular form directive owns its reset event; without one, a standalone reactive control +retains its model until the app resets the control. The core native reset request remains controlled. +Source tests use the current pure contract source. Packed fixtures use installed exports with no +source alias; both paths are required. The implementation follows Angular's +[ControlValueAccessor](https://angular.dev/api/forms/ControlValueAccessor) and +[Validator](https://angular.dev/api/forms/Validator) contracts; Svelte uses typed callback props and +application-owned rune state. diff --git a/docs/provenance.md b/docs/provenance.md index 0d8d928..d15396b 100644 --- a/docs/provenance.md +++ b/docs/provenance.md @@ -35,3 +35,9 @@ and 14px/12px text geometry from [Gateway ui-catalog.css lines 143–150 at the same revision](https://github.com/vypdev/homeassistant-gateway/blob/e47dde303e475a6d03e1d6697ec87a934e93263b/frontend/src/ui/ui-catalog.css#L143-L150). Form ownership, controlled requests, validation, composition handling and accessible native input semantics are authored here. No private Home Assistant component or external asset is imported. + +The four text-field bindings and Angular value-accessor/validator bridge are authored here. They +reuse the pure core event/value/validation policies and the existing core renderer, adding no CSS +or alternative controller. Angular Forms 22.2.4 is an exact framework peer; its runtime source is +not copied. Source-browser policy aliases are test-only; actual frozen package consumers resolve +installed exports and verify authored source bytes against the repository. diff --git a/package.json b/package.json index 930503a..4605d1a 100644 --- a/package.json +++ b/package.json @@ -17,10 +17,12 @@ "@angular/compiler": "22.2.4", "@angular/compiler-cli": "22.2.4", "@angular/core": "22.2.4", + "@angular/forms": "22.2.4", "@angular/platform-browser": "22.2.4", "@babel/core": "8.0.1", "@sveltejs/package": "3.0.2", "@sveltejs/vite-plugin-svelte": "7.3.1", + "@types/convert-source-map": "2.0.3", "@types/node": "24.19.2", "@types/react": "19.3.0", "@types/react-dom": "19.3.0", @@ -38,7 +40,6 @@ "vite": "8.3.5", "vitest": "5.1.0", "vue": "3.5.44", - "yaml": "2.9.0", - "@types/convert-source-map": "2.0.3" + "yaml": "2.9.0" } } diff --git a/packages/angular/package.json b/packages/angular/package.json index 0579c29..d6c7b79 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -21,7 +21,8 @@ "tslib": "2.8.1" }, "peerDependencies": { - "@angular/core": "22.2.4" + "@angular/core": "22.2.4", + "@angular/forms": "22.2.4" }, "publishConfig": { "access": "public" diff --git a/packages/angular/src/index.ts b/packages/angular/src/index.ts index 8f90a5a..c67e2cc 100644 --- a/packages/angular/src/index.ts +++ b/packages/angular/src/index.ts @@ -13,3 +13,4 @@ export class Tabs implements OnChanges,OnDestroy { ngOnChanges():void {this.#renderer.setProperty(this.#host,'state',this.state);} ngOnDestroy():void {this.#stop();} } +export {TextField} from './text-field.js'; diff --git a/packages/angular/src/text-field.ts b/packages/angular/src/text-field.ts new file mode 100644 index 0000000..489f31d --- /dev/null +++ b/packages/angular/src/text-field.ts @@ -0,0 +1,41 @@ +import {Component,ElementRef,EventEmitter,Input,Output,Renderer2,forwardRef,inject,type OnChanges,type OnDestroy} from '@angular/core'; +import {NG_VALUE_ACCESSOR,NG_VALIDATORS,type ControlValueAccessor,type Validator,type AbstractControl,type ValidationErrors} from '@angular/forms'; +import {textFieldValue,textFieldValidity,fieldValueDetail,type TextFieldState} from '@vypdev/ha-ui/field-contracts'; +import type {HaTextField,FieldValueEvent} from '@vypdev/ha-ui/field'; + +/** Angular maps model and validation ports; the core host owns the only visual implementation. */ +@Component({selector:'vyp-ha-field',standalone:true,template:'',providers:[ + {provide:NG_VALUE_ACCESSOR,useExisting:forwardRef(()=>TextField),multi:true}, + {provide:NG_VALIDATORS,useExisting:forwardRef(()=>TextField),multi:true}, +]}) +export class TextField implements ControlValueAccessor,Validator,OnChanges,OnDestroy { + @Input({required:true}) state!:TextFieldState; + @Input() value='';@Input() name='';@Input() defaultValue='';@Input() disabled=false;@Input() form?:string; + @Output() readonly valueRequest=new EventEmitter(false); + readonly #host=inject>(ElementRef).nativeElement; + readonly #renderer=inject(Renderer2); + #modelValue='';#change:((value:string)=>void)|undefined;#touched:(()=>void)|undefined;#validatorChanged:(()=>void)|undefined; + readonly #stopValue=this.#renderer.listen(this.#host,'ha-value-request',(event:FieldValueEvent)=>{ + const detail=fieldValueDetail(event.detail);this.valueRequest.emit(event);if(event.defaultPrevented||this.#change===undefined)return; + // Angular's form reset already wrote the correct nullable/nonnullable default before the native callback. + if(detail.reason==='reset'){this.writeValue(this.#modelValue);return;} + this.writeValue(detail.value);this.#change(detail.value); + }); + readonly #stopTouch=this.#renderer.listen(this.#host,'focusout',()=>this.#touched?.()); + ngOnChanges():void{ + for(const [key,value] of Object.entries({state:this.state,name:this.name,defaultValue:this.defaultValue}))this.#renderer.setProperty(this.#host,key,value); + if(this.form===undefined)this.#renderer.removeAttribute(this.#host,'form');else this.#renderer.setAttribute(this.#host,'form',this.form); + if(this.#change===undefined){this.#renderer.setProperty(this.#host,'disabled',this.disabled);this.writeValue(this.value);} + this.#validatorChanged?.(); + } + writeValue(value:unknown):void{const next=textFieldValue(value??'');this.#modelValue=next;this.#renderer.setProperty(this.#host,'value',next);} + registerOnChange(fn:(value:string)=>void):void{this.#change=fn;} + registerOnTouched(fn:()=>void):void{this.#touched=fn;} + setDisabledState(disabled:boolean):void{this.#renderer.setProperty(this.#host,'disabled',disabled);} + validate(control:AbstractControl):ValidationErrors|null{ + const result=textFieldValidity(control.value??'',this.state); + return !this.state.readOnly&&Object.values(result.flags).some(Boolean)?{haField:result.flags}:null; + } + registerOnValidatorChange(fn:()=>void):void{this.#validatorChanged=fn;} + ngOnDestroy():void{this.#stopValue();this.#stopTouch();} +} diff --git a/packages/core/src/components/field.ts b/packages/core/src/components/field.ts index a54b0cd..a0f21ec 100644 --- a/packages/core/src/components/field.ts +++ b/packages/core/src/components/field.ts @@ -1,8 +1,7 @@ import {LitElement,css,html,nothing} from 'lit'; -import {FieldInputError,textFieldName,textFieldState,textFieldValue,textFieldValidity,type TextFieldState} from '../field-contracts.js'; +import {FieldInputError,textFieldName,textFieldState,textFieldValue,textFieldValidity,type TextFieldState,type FieldValueReason,type FieldValueDetail} from '../field-contracts.js'; -export type FieldValueReason='input'|'reset'|'restore'|'autocomplete'; -export interface FieldValueDetail {readonly value:string;readonly reason:FieldValueReason;} +export type {FieldValueReason,FieldValueDetail} from '../field-contracts.js'; export type FieldValueEvent=CustomEvent; type Proposal={value:string;reason:FieldValueReason;start:number|null;end:number|null;direction:'forward'|'backward'|'none'|null}; const attributeText={fromAttribute:(value:string|null)=>value??''}; diff --git a/packages/core/src/field-contracts.ts b/packages/core/src/field-contracts.ts index 9ba9d6f..d134e53 100644 --- a/packages/core/src/field-contracts.ts +++ b/packages/core/src/field-contracts.ts @@ -48,3 +48,13 @@ export function textFieldValidity(rawValue:unknown,rawState:unknown,customError: const message=customError||state.error||(flags.valueMissing?state.requiredMessage:flags.tooShort?state.tooShortMessage:flags.tooLong?state.tooLongMessage:''); return Object.freeze({flags,message}); } + +export type FieldValueReason='input'|'reset'|'restore'|'autocomplete'; +export interface FieldValueDetail {readonly value:string;readonly reason:FieldValueReason;} +/** Validate event payloads at each model bridge before any application callback. */ +export function fieldValueDetail(raw:unknown):FieldValueDetail { + if(typeof raw!=='object'||raw===null||Array.isArray(raw)||Object.keys(raw).length!==2||!Object.hasOwn(raw,'value')||!Object.hasOwn(raw,'reason'))invalid(); + const source=raw as Record,value=textFieldValue(source.value),reason=source.reason as FieldValueReason; + if(!['input','reset','restore','autocomplete'].includes(reason))invalid(); + return Object.freeze({value,reason}); +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 8b2ec83..00f3d54 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,4 +1,4 @@ export {NavigationInputError, navigationState} from './contracts.js'; export type {NavigationKind, NavigationItem, NavigationState} from './contracts.js'; -export {FieldInputError,textFieldState,textFieldValue,textFieldName,textFieldValidity} from './field-contracts.js'; -export type {TextFieldState,TextFieldValidity} from './field-contracts.js'; +export {FieldInputError,textFieldState,textFieldValue,textFieldName,textFieldValidity,fieldValueDetail} from './field-contracts.js'; +export type {TextFieldState,TextFieldValidity,FieldValueReason,FieldValueDetail} from './field-contracts.js'; diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 53e2d11..90f8f1d 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -20,3 +20,5 @@ export function Tabs({state,onSelectionRequest,children,id,className,style,dir,l },[onSelectionRequest]); return createElement('vyp-ha-tabs',{ref:element,id,className,style,dir,lang},children); } +export {TextField} from './text-field.js'; +export type {TextFieldProps} from './text-field.js'; diff --git a/packages/react/src/text-field.ts b/packages/react/src/text-field.ts new file mode 100644 index 0000000..4a93612 --- /dev/null +++ b/packages/react/src/text-field.ts @@ -0,0 +1,24 @@ +'use client'; +import {createElement,useLayoutEffect,useRef,type HTMLAttributes} from 'react'; +import {fieldValueDetail} from '@vypdev/ha-ui/field-contracts'; +import type {TextFieldState} from '@vypdev/ha-ui/field-contracts'; +import type {HaTextField,FieldValueEvent} from '@vypdev/ha-ui/field'; +export interface TextFieldProps extends Pick,'id'|'className'|'style'|'dir'|'lang'> { + state:TextFieldState;value:string;name?:string;defaultValue?:string;disabled?:boolean;form?:string; + onValueRequest?:(event:FieldValueEvent)=>void;onValueChange?:(value:string)=>void; +} +/** React owns the model; the explicitly registered core owns native form behavior and visuals. */ +export function TextField({state,value,name='',defaultValue='',disabled=false,form,onValueRequest,onValueChange,id,className,style,dir,lang}:TextFieldProps){ + const element=useRef(null); + useLayoutEffect(()=>{ + const host=element.current!;host.state=state;host.name=name;host.defaultValue=defaultValue;host.disabled=disabled; + if(form===undefined)host.removeAttribute('form');else host.setAttribute('form',form); + host.value=value; + },[state,value,name,defaultValue,disabled,form]); + useLayoutEffect(()=>{ + const host=element.current!; + const handle=(event:FieldValueEvent)=>{const detail=fieldValueDetail(event.detail);onValueRequest?.(event);if(!event.defaultPrevented)onValueChange?.(detail.value);}; + host.addEventListener('ha-value-request',handle);return ()=>host.removeEventListener('ha-value-request',handle); + },[onValueRequest,onValueChange]); + return createElement('vyp-ha-field',{ref:element,id,className,style,dir,lang}); +} diff --git a/packages/svelte/src/Field.svelte b/packages/svelte/src/Field.svelte new file mode 100644 index 0000000..d2464f6 --- /dev/null +++ b/packages/svelte/src/Field.svelte @@ -0,0 +1,18 @@ + + diff --git a/packages/svelte/src/index.ts b/packages/svelte/src/index.ts index 0fb0353..df00694 100644 --- a/packages/svelte/src/index.ts +++ b/packages/svelte/src/index.ts @@ -1,2 +1,4 @@ export {default as Tabs} from './Tabs.svelte'; export type {TabsProps} from './types.js'; +export {default as TextField} from './Field.svelte'; +export type {TextFieldProps} from './types.js'; diff --git a/packages/svelte/src/types.ts b/packages/svelte/src/types.ts index bfc631c..917217e 100644 --- a/packages/svelte/src/types.ts +++ b/packages/svelte/src/types.ts @@ -7,3 +7,10 @@ export interface TabsProps extends Pick,'id'|'class' onSelectionRequest?:(event:NavigationSelectionEvent)=>void; panels?:Readonly>; } + +import type {TextFieldState} from '@vypdev/ha-ui/field-contracts'; +import type {FieldValueEvent} from '@vypdev/ha-ui/field'; +export interface TextFieldProps extends Pick,'id'|'class'|'style'|'lang'|'dir'> { + state:TextFieldState;value:string;name?:string;defaultValue?:string;disabled?:boolean;form?:string; + onValueRequest?:(event:FieldValueEvent)=>void;onValueChange?:(value:string)=>void; +} diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts index 5f02160..8a6295d 100644 --- a/packages/vue/src/index.ts +++ b/packages/vue/src/index.ts @@ -19,3 +19,4 @@ export const Tabs=defineComponent({ return ()=>h('vyp-ha-tabs',{ref:element,id:attrs.id,class:attrs.class,style:attrs.style,dir:attrs.dir,lang:attrs.lang},slots.default?.()); }, }); +export {TextField} from './text-field.js'; diff --git a/packages/vue/src/text-field.ts b/packages/vue/src/text-field.ts new file mode 100644 index 0000000..19ba48e --- /dev/null +++ b/packages/vue/src/text-field.ts @@ -0,0 +1,24 @@ +import {defineComponent,h,onMounted,onBeforeUnmount,shallowRef,watch,type PropType} from 'vue'; +import {fieldValueDetail} from '@vypdev/ha-ui/field-contracts'; +import type {TextFieldState} from '@vypdev/ha-ui/field-contracts'; +import type {HaTextField,FieldValueEvent} from '@vypdev/ha-ui/field'; +/** Vue's v-model acknowledges the shared core request; cancellation stays synchronous. */ +export const TextField=defineComponent({ + name:'HaUiTextField',inheritAttrs:false, + props:{state:{type:Object as PropType,required:true},modelValue:{type:String,required:true}, + name:{type:String,default:''},defaultValue:{type:String,default:''},disabled:{type:Boolean,default:false},form:{type:String}}, + emits:{valueRequest:(_event:FieldValueEvent)=>true,'update:modelValue':(_value:string)=>true}, + setup(props,{attrs,emit}){ + const element=shallowRef(null); + const apply=()=>{ + const host=element.value!;host.state=props.state;host.name=props.name;host.defaultValue=props.defaultValue;host.disabled=props.disabled; + if(props.form===undefined)host.removeAttribute('form');else host.setAttribute('form',props.form); + host.value=props.modelValue; + }; + const handle=(event:FieldValueEvent)=>{const detail=fieldValueDetail(event.detail);emit('valueRequest',event);if(!event.defaultPrevented)emit('update:modelValue',detail.value);}; + onMounted(()=>{apply();element.value!.addEventListener('ha-value-request',handle);}); + watch(()=>[props.state,props.modelValue,props.name,props.defaultValue,props.disabled,props.form],apply,{flush:'post'}); + onBeforeUnmount(()=>element.value!.removeEventListener('ha-value-request',handle)); + return ()=>h('vyp-ha-field',{ref:element,id:attrs.id,class:attrs.class,style:attrs.style,dir:attrs.dir,lang:attrs.lang}); + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 42a64dd..1d3edcc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@angular/core': specifier: 22.2.4 version: 22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2) + '@angular/forms': + specifier: 22.2.4 + version: 22.2.4(@angular/common@22.2.4(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))(@angular/platform-browser@22.2.4(@angular/common@22.2.4(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2)))(rxjs@7.8.2) '@angular/platform-browser': specifier: 22.2.4 version: 22.2.4(@angular/common@22.2.4(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2)) @@ -164,6 +167,15 @@ packages: zone.js: optional: true + '@angular/forms@22.2.4': + resolution: {integrity: sha512-siFy9s0p4PjPUeDX7zI7bo5x3YstBnFfBGqO/obL0TdR2rBxrmW8iR11TxbGh41NKYc5GvbZezVKsN8WEW2ooA==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} + peerDependencies: + '@angular/common': 22.2.4 + '@angular/core': 22.2.4 + '@angular/platform-browser': 22.2.4 + rxjs: ^6.5.3 || ^7.4.0 + '@angular/platform-browser@22.2.4': resolution: {integrity: sha512-NAQ8/Zcl8nRlNqxlXQGrQJ/c5V8lN5vJSlc8qkD3OUN+ClyNuS6UCXffYtKvvbovpyb/q1uYSQjqiEctz+5niw==} engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} @@ -627,6 +639,9 @@ packages: '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@sveltejs/acorn-typescript@1.0.13': resolution: {integrity: sha512-wgKggnhZVL9Bfx1OaKKTrYY9BFRk6C8UAkQNUcIv1+llzYrIqy+RZm5HPKzn0NpEBvTVhTqB4kQyllZywsRBRQ==} peerDependencies: @@ -1843,6 +1858,9 @@ packages: zimmerframe@1.1.5: resolution: {integrity: sha512-msJxIvYDYcoNL+PJsu+7qmpDWsYmAxTY+2TNYXXF0hzBzBk0BMecOqDOG/EckUoKCuKwObfbugIl8QpqHDXeFA==} + zod@4.6.7: + resolution: {integrity: sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==} + snapshots: '@ampproject/remapping@2.3.0': @@ -1881,6 +1899,16 @@ snapshots: optionalDependencies: '@angular/compiler': 22.2.4 + '@angular/forms@22.2.4(@angular/common@22.2.4(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))(@angular/platform-browser@22.2.4(@angular/common@22.2.4(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2)))(rxjs@7.8.2)': + dependencies: + '@angular/common': 22.2.4(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))(rxjs@7.8.2) + '@angular/core': 22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2) + '@angular/platform-browser': 22.2.4(@angular/common@22.2.4(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2)) + '@standard-schema/spec': 1.1.0 + rxjs: 7.8.2 + tslib: 2.8.1 + zod: 4.6.7 + '@angular/platform-browser@22.2.4(@angular/common@22.2.4(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))': dependencies: '@angular/common': 22.2.4(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))(rxjs@7.8.2) @@ -2203,6 +2231,8 @@ snapshots: '@rolldown/pluginutils@1.0.1': {} + '@standard-schema/spec@1.1.0': {} + '@sveltejs/acorn-typescript@1.0.13(acorn@8.19.0)': dependencies: acorn: 8.19.0 @@ -3299,3 +3329,5 @@ snapshots: yuku-core: 0.19.0 zimmerframe@1.1.5: {} + + zod@4.6.7: {} diff --git a/specs/CATALOG.md b/specs/CATALOG.md index 8004255..696caa3 100644 --- a/specs/CATALOG.md +++ b/specs/CATALOG.md @@ -10,7 +10,7 @@ debt or convert unknown historic intent into a design decision. | Capability ID | Status | Scope | Primary SDD | Evidence | |---|---|---|---|---| -| `shared-ui-foundation` | Proposed | One DOM component implementation with React, Vue, Angular and Svelte adapters, direct Lit/DOM use, shared Storybook and verified Gateway/Domus adoption. Partial source; no complete capability or release accepted. | [Shared Home Assistant presentation components and framework adapters](./shared-ui-foundation.md) + 1 companion | 73 paths · 2026-11-04 | +| `shared-ui-foundation` | Proposed | One DOM component implementation with React, Vue, Angular and Svelte adapters, direct Lit/DOM use, shared Storybook and verified Gateway/Domus adoption. Partial source; no complete capability or release accepted. | [Shared Home Assistant presentation components and framework adapters](./shared-ui-foundation.md) + 1 companion | 84 paths · 2026-11-04 | ## Evidence map @@ -21,8 +21,8 @@ debt or convert unknown historic intent into a design decision. - Specifications: [`specs/shared-ui-foundation.md`](./shared-ui-foundation.md) · [`specs/shared-ui-framework-delivery.md`](./shared-ui-framework-delivery.md) - Workflows: [`.github/workflows/copilot_issue.yml`](../.github/workflows/copilot_issue.yml) · [`.github/workflows/ci_check.yml`](../.github/workflows/ci_check.yml) - Entrypoints: [`packages/core/src/index.ts`](../packages/core/src/index.ts) · [`packages/core/src/components/tabs.ts`](../packages/core/src/components/tabs.ts) · [`packages/react/src/index.ts`](../packages/react/src/index.ts) · [`packages/vue/src/index.ts`](../packages/vue/src/index.ts) · [`packages/angular/src/index.ts`](../packages/angular/src/index.ts) · [`packages/svelte/src/index.ts`](../packages/svelte/src/index.ts) · [`packages/core/src/components/field.ts`](../packages/core/src/components/field.ts) · [`packages/core/src/field-contracts.ts`](../packages/core/src/field-contracts.ts) -- Core code: [`packages/core/src/contracts.ts`](../packages/core/src/contracts.ts) · [`tooling/coverage.mjs`](../tooling/coverage.mjs) · [`packages/core/src/components/tabs.ts`](../packages/core/src/components/tabs.ts) · [`packages/react/src/index.ts`](../packages/react/src/index.ts) · [`packages/vue/src/index.ts`](../packages/vue/src/index.ts) · [`packages/angular/src/index.ts`](../packages/angular/src/index.ts) · [`packages/svelte/src/index.ts`](../packages/svelte/src/index.ts) · [`packages/svelte/src/Tabs.svelte`](../packages/svelte/src/Tabs.svelte) · [`packages/svelte/src/types.ts`](../packages/svelte/src/types.ts) · [`packages/core/src/components/field.ts`](../packages/core/src/components/field.ts) · [`packages/core/src/field-contracts.ts`](../packages/core/src/field-contracts.ts) · [`packages/core/src/presentation-text.ts`](../packages/core/src/presentation-text.ts) -- Tests: [`tests/contracts/core.test.ts`](../tests/contracts/core.test.ts) · [`tests/coverage.test.mjs`](../tests/coverage.test.mjs) · [`tests/architecture.test.mjs`](../tests/architecture.test.mjs) · [`tests/coverage-evidence.test.mjs`](../tests/coverage-evidence.test.mjs) · [`tests/consumers/packed.test.ts`](../tests/consumers/packed.test.ts) · [`tests/contracts/registration.test.ts`](../tests/contracts/registration.test.ts) · [`tests/browser/tabs.test.ts`](../tests/browser/tabs.test.ts) · [`tests/browser/react.test.ts`](../tests/browser/react.test.ts) · [`tests/browser/vue.test.ts`](../tests/browser/vue.test.ts) · [`tests/browser/angular.test.ts`](../tests/browser/angular.test.ts) · [`tests/browser/svelte.test.mjs`](../tests/browser/svelte.test.mjs) · [`tests/browser/framework-fixture.ts`](../tests/browser/framework-fixture.ts) · [`tests/browser/fixtures/SvelteTabs.svelte`](../tests/browser/fixtures/SvelteTabs.svelte) · [`tests/consumers/frameworks.ts`](../tests/consumers/frameworks.ts) · [`tests/consumers/fixtures/svelte/pnpm-lock.yaml`](../tests/consumers/fixtures/svelte/pnpm-lock.yaml) · [`tests/consumers/fixtures/svelte/index.html`](../tests/consumers/fixtures/svelte/index.html) · [`tests/consumers/fixtures/svelte/Consumer.svelte`](../tests/consumers/fixtures/svelte/Consumer.svelte) · [`tests/consumers/fixtures/svelte/model.ts`](../tests/consumers/fixtures/svelte/model.ts) · [`tests/consumers/fixtures/svelte/package.json`](../tests/consumers/fixtures/svelte/package.json) · [`tests/consumers/fixtures/svelte/client.ts`](../tests/consumers/fixtures/svelte/client.ts) · [`tests/consumers/fixtures/svelte/tsconfig.json`](../tests/consumers/fixtures/svelte/tsconfig.json) · [`tests/consumers/fixtures/svelte/pnpm-workspace.yaml`](../tests/consumers/fixtures/svelte/pnpm-workspace.yaml) · [`tests/consumers/fixtures/vue/pnpm-lock.yaml`](../tests/consumers/fixtures/vue/pnpm-lock.yaml) · [`tests/consumers/fixtures/vue/index.html`](../tests/consumers/fixtures/vue/index.html) · [`tests/consumers/fixtures/vue/model.ts`](../tests/consumers/fixtures/vue/model.ts) · [`tests/consumers/fixtures/vue/package.json`](../tests/consumers/fixtures/vue/package.json) · [`tests/consumers/fixtures/vue/client.ts`](../tests/consumers/fixtures/vue/client.ts) · [`tests/consumers/fixtures/vue/tsconfig.json`](../tests/consumers/fixtures/vue/tsconfig.json) · [`tests/consumers/fixtures/vue/pnpm-workspace.yaml`](../tests/consumers/fixtures/vue/pnpm-workspace.yaml) · [`tests/consumers/fixtures/angular/pnpm-lock.yaml`](../tests/consumers/fixtures/angular/pnpm-lock.yaml) · [`tests/consumers/fixtures/angular/index.html`](../tests/consumers/fixtures/angular/index.html) · [`tests/consumers/fixtures/angular/model.ts`](../tests/consumers/fixtures/angular/model.ts) · [`tests/consumers/fixtures/angular/package.json`](../tests/consumers/fixtures/angular/package.json) · [`tests/consumers/fixtures/angular/client.ts`](../tests/consumers/fixtures/angular/client.ts) · [`tests/consumers/fixtures/angular/tsconfig.json`](../tests/consumers/fixtures/angular/tsconfig.json) · [`tests/consumers/fixtures/angular/pnpm-workspace.yaml`](../tests/consumers/fixtures/angular/pnpm-workspace.yaml) · [`tests/consumers/fixtures/react/pnpm-lock.yaml`](../tests/consumers/fixtures/react/pnpm-lock.yaml) · [`tests/consumers/fixtures/react/index.html`](../tests/consumers/fixtures/react/index.html) · [`tests/consumers/fixtures/react/model.ts`](../tests/consumers/fixtures/react/model.ts) · [`tests/consumers/fixtures/react/package.json`](../tests/consumers/fixtures/react/package.json) · [`tests/consumers/fixtures/react/client.ts`](../tests/consumers/fixtures/react/client.ts) · [`tests/consumers/fixtures/react/tsconfig.json`](../tests/consumers/fixtures/react/tsconfig.json) · [`tests/consumers/fixtures/react/pnpm-workspace.yaml`](../tests/consumers/fixtures/react/pnpm-workspace.yaml) · [`tests/contracts/field.test.ts`](../tests/contracts/field.test.ts) · [`tests/browser/field.test.ts`](../tests/browser/field.test.ts) +- Core code: [`packages/core/src/contracts.ts`](../packages/core/src/contracts.ts) · [`tooling/coverage.mjs`](../tooling/coverage.mjs) · [`packages/core/src/components/tabs.ts`](../packages/core/src/components/tabs.ts) · [`packages/react/src/index.ts`](../packages/react/src/index.ts) · [`packages/vue/src/index.ts`](../packages/vue/src/index.ts) · [`packages/angular/src/index.ts`](../packages/angular/src/index.ts) · [`packages/svelte/src/index.ts`](../packages/svelte/src/index.ts) · [`packages/svelte/src/Tabs.svelte`](../packages/svelte/src/Tabs.svelte) · [`packages/svelte/src/types.ts`](../packages/svelte/src/types.ts) · [`packages/core/src/components/field.ts`](../packages/core/src/components/field.ts) · [`packages/core/src/field-contracts.ts`](../packages/core/src/field-contracts.ts) · [`packages/core/src/presentation-text.ts`](../packages/core/src/presentation-text.ts) · [`packages/react/src/text-field.ts`](../packages/react/src/text-field.ts) · [`packages/vue/src/text-field.ts`](../packages/vue/src/text-field.ts) · [`packages/angular/src/text-field.ts`](../packages/angular/src/text-field.ts) · [`packages/svelte/src/Field.svelte`](../packages/svelte/src/Field.svelte) +- Tests: [`tests/contracts/core.test.ts`](../tests/contracts/core.test.ts) · [`tests/coverage.test.mjs`](../tests/coverage.test.mjs) · [`tests/architecture.test.mjs`](../tests/architecture.test.mjs) · [`tests/coverage-evidence.test.mjs`](../tests/coverage-evidence.test.mjs) · [`tests/consumers/packed.test.ts`](../tests/consumers/packed.test.ts) · [`tests/contracts/registration.test.ts`](../tests/contracts/registration.test.ts) · [`tests/browser/tabs.test.ts`](../tests/browser/tabs.test.ts) · [`tests/browser/react.test.ts`](../tests/browser/react.test.ts) · [`tests/browser/vue.test.ts`](../tests/browser/vue.test.ts) · [`tests/browser/angular.test.ts`](../tests/browser/angular.test.ts) · [`tests/browser/svelte.test.mjs`](../tests/browser/svelte.test.mjs) · [`tests/browser/framework-fixture.ts`](../tests/browser/framework-fixture.ts) · [`tests/browser/fixtures/SvelteTabs.svelte`](../tests/browser/fixtures/SvelteTabs.svelte) · [`tests/consumers/frameworks.ts`](../tests/consumers/frameworks.ts) · [`tests/consumers/fixtures/svelte/pnpm-lock.yaml`](../tests/consumers/fixtures/svelte/pnpm-lock.yaml) · [`tests/consumers/fixtures/svelte/index.html`](../tests/consumers/fixtures/svelte/index.html) · [`tests/consumers/fixtures/svelte/Consumer.svelte`](../tests/consumers/fixtures/svelte/Consumer.svelte) · [`tests/consumers/fixtures/svelte/model.ts`](../tests/consumers/fixtures/svelte/model.ts) · [`tests/consumers/fixtures/svelte/package.json`](../tests/consumers/fixtures/svelte/package.json) · [`tests/consumers/fixtures/svelte/client.ts`](../tests/consumers/fixtures/svelte/client.ts) · [`tests/consumers/fixtures/svelte/tsconfig.json`](../tests/consumers/fixtures/svelte/tsconfig.json) · [`tests/consumers/fixtures/svelte/pnpm-workspace.yaml`](../tests/consumers/fixtures/svelte/pnpm-workspace.yaml) · [`tests/consumers/fixtures/vue/pnpm-lock.yaml`](../tests/consumers/fixtures/vue/pnpm-lock.yaml) · [`tests/consumers/fixtures/vue/index.html`](../tests/consumers/fixtures/vue/index.html) · [`tests/consumers/fixtures/vue/model.ts`](../tests/consumers/fixtures/vue/model.ts) · [`tests/consumers/fixtures/vue/package.json`](../tests/consumers/fixtures/vue/package.json) · [`tests/consumers/fixtures/vue/client.ts`](../tests/consumers/fixtures/vue/client.ts) · [`tests/consumers/fixtures/vue/tsconfig.json`](../tests/consumers/fixtures/vue/tsconfig.json) · [`tests/consumers/fixtures/vue/pnpm-workspace.yaml`](../tests/consumers/fixtures/vue/pnpm-workspace.yaml) · [`tests/consumers/fixtures/angular/pnpm-lock.yaml`](../tests/consumers/fixtures/angular/pnpm-lock.yaml) · [`tests/consumers/fixtures/angular/index.html`](../tests/consumers/fixtures/angular/index.html) · [`tests/consumers/fixtures/angular/model.ts`](../tests/consumers/fixtures/angular/model.ts) · [`tests/consumers/fixtures/angular/package.json`](../tests/consumers/fixtures/angular/package.json) · [`tests/consumers/fixtures/angular/client.ts`](../tests/consumers/fixtures/angular/client.ts) · [`tests/consumers/fixtures/angular/tsconfig.json`](../tests/consumers/fixtures/angular/tsconfig.json) · [`tests/consumers/fixtures/angular/pnpm-workspace.yaml`](../tests/consumers/fixtures/angular/pnpm-workspace.yaml) · [`tests/consumers/fixtures/react/pnpm-lock.yaml`](../tests/consumers/fixtures/react/pnpm-lock.yaml) · [`tests/consumers/fixtures/react/index.html`](../tests/consumers/fixtures/react/index.html) · [`tests/consumers/fixtures/react/model.ts`](../tests/consumers/fixtures/react/model.ts) · [`tests/consumers/fixtures/react/package.json`](../tests/consumers/fixtures/react/package.json) · [`tests/consumers/fixtures/react/client.ts`](../tests/consumers/fixtures/react/client.ts) · [`tests/consumers/fixtures/react/tsconfig.json`](../tests/consumers/fixtures/react/tsconfig.json) · [`tests/consumers/fixtures/react/pnpm-workspace.yaml`](../tests/consumers/fixtures/react/pnpm-workspace.yaml) · [`tests/contracts/field.test.ts`](../tests/contracts/field.test.ts) · [`tests/browser/field.test.ts`](../tests/browser/field.test.ts) · [`tests/browser/react-field.test.ts`](../tests/browser/react-field.test.ts) · [`tests/browser/vue-field.test.ts`](../tests/browser/vue-field.test.ts) · [`tests/browser/angular-field.test.ts`](../tests/browser/angular-field.test.ts) · [`tests/browser/svelte-field.test.mjs`](../tests/browser/svelte-field.test.mjs) · [`tests/browser/field-framework-fixture.ts`](../tests/browser/field-framework-fixture.ts) · [`tests/browser/fixtures/SvelteField.svelte`](../tests/browser/fixtures/SvelteField.svelte) · [`tests/consumers/fields.ts`](../tests/consumers/fields.ts) - User documentation: [`README.md`](../README.md) · [`docs/component-library.md`](../docs/component-library.md) · [`docs/compatibility.md`](../docs/compatibility.md) · [`docs/provenance.md`](../docs/provenance.md) · [`docs/evidence/shared-ui-foundation.json`](../docs/evidence/shared-ui-foundation.json) · [`docs/framework-adapters.md`](../docs/framework-adapters.md) ## Maintenance contract diff --git a/specs/catalog.json b/specs/catalog.json index 7f6a1e5..b19ea02 100644 --- a/specs/catalog.json +++ b/specs/catalog.json @@ -38,7 +38,11 @@ "packages/svelte/src/types.ts", "packages/core/src/components/field.ts", "packages/core/src/field-contracts.ts", - "packages/core/src/presentation-text.ts" + "packages/core/src/presentation-text.ts", + "packages/react/src/text-field.ts", + "packages/vue/src/text-field.ts", + "packages/angular/src/text-field.ts", + "packages/svelte/src/Field.svelte" ], "tests": [ "tests/contracts/core.test.ts", @@ -85,7 +89,14 @@ "tests/consumers/fixtures/react/tsconfig.json", "tests/consumers/fixtures/react/pnpm-workspace.yaml", "tests/contracts/field.test.ts", - "tests/browser/field.test.ts" + "tests/browser/field.test.ts", + "tests/browser/react-field.test.ts", + "tests/browser/vue-field.test.ts", + "tests/browser/angular-field.test.ts", + "tests/browser/svelte-field.test.mjs", + "tests/browser/field-framework-fixture.ts", + "tests/browser/fixtures/SvelteField.svelte", + "tests/consumers/fields.ts" ], "documentation": [ "README.md", @@ -104,7 +115,7 @@ "Storybook Pages delivery and human UX review", "Actual Gateway and Domus adoption" ], - "implementationEvidence": "partial: pure data contracts, shared navigation/panels and a controlled native form-associated text field. Four real navigation bindings and reproducible clean compiled consumers pass Chromium. Field framework bridges, other families, framework SSR, full browser matrix, Storybook, publication and application adoption remain required." + "implementationEvidence": "partial: shared navigation/panels and a controlled text field, with React/Vue/Angular/Svelte bindings and text-field form bridges. Frozen compiled consumers pass native form submission, cancellation, validation, disable/reset and lifecycle; Angular includes AOT and CVA. Other controls, full SSR/browser matrix, Storybook, delivery/publication and application adoption remain required." } } ] diff --git a/specs/shared-ui-framework-delivery.md b/specs/shared-ui-framework-delivery.md index 6bbc11e..c95920f 100644 --- a/specs/shared-ui-framework-delivery.md +++ b/specs/shared-ui-framework-delivery.md @@ -418,6 +418,16 @@ Prospective repository files (current documents are classified against the immut | `packages/core/src/presentation-text.ts` | create | | `tests/contracts/field.test.ts` | create | | `tests/browser/field.test.ts` | create | +| `packages/react/src/text-field.ts` | create | +| `packages/vue/src/text-field.ts` | create | +| `packages/angular/src/text-field.ts` | create | +| `tests/browser/react-field.test.ts` | create | +| `tests/browser/vue-field.test.ts` | create | +| `tests/browser/angular-field.test.ts` | create | +| `tests/browser/svelte-field.test.mjs` | create | +| `tests/browser/field-framework-fixture.ts` | create | +| `tests/browser/fixtures/SvelteField.svelte` | create | +| `tests/consumers/fields.ts` | create | The catalog/index update registers the companion under the existing owner; it does not claim implementation. `LICENSE` preserves the upstream MIT source notice and every reused asset requires provenance verification. The Copilot installation and its source remain unchanged. Package coordinates are identifiers, not repository paths. diff --git a/tests/architecture.test.mjs b/tests/architecture.test.mjs index 6f2d295..51e82ee 100644 --- a/tests/architecture.test.mjs +++ b/tests/architecture.test.mjs @@ -65,6 +65,7 @@ const adapterFrameworks={react:'react',vue:'vue',angular:'@angular/core',svelte: function adapterImport(framework,specifier) { if (specifier==='@vypdev/ha-ui' || specifier.startsWith('@vypdev/ha-ui/')) return true; if (specifier.startsWith('.')) return true; + if (framework==='angular' && specifier==='@angular/forms') return true; const own=adapterFrameworks[framework]; return specifier===own || specifier.startsWith(own+'/'); } @@ -89,14 +90,14 @@ test('framework entrypoints never import the core DOM renderer at runtime or reg const code=file.endsWith('.svelte')?original.match(/]*>([\s\S]*?)<\/script>/u)[1]:original; const parsed=ts.createSourceFile('source.ts',code,ts.ScriptTarget.Latest,true); for (const node of parsed.statements) if (ts.isImportDeclaration(node) && node.moduleSpecifier.text.startsWith('@vypdev/ha-ui')) - expect(node.importClause.isTypeOnly,file).toBe(true); + expect(node.importClause.isTypeOnly||node.moduleSpecifier.text==='@vypdev/ha-ui/field-contracts',file).toBe(true); expect(code).not.toMatch(/customElements|registerTabs/u); } }); test('adapter manifests keep other frameworks out and retain the exact shared cohort',()=>{ for (const [framework,peer] of Object.entries(adapterFrameworks)) { const manifest=JSON.parse(readFileSync('packages/'+framework+'/package.json','utf8')); - expect(Object.keys(manifest.peerDependencies)).toEqual([peer]); + expect(Object.keys(manifest.peerDependencies)).toEqual(framework==='angular'?[peer,'@angular/forms']:[peer]); expect(manifest.dependencies['@vypdev/ha-ui']).toBe(framework==='angular'?'0.1.0-next.1':'workspace:*'); expect(Object.keys(manifest.dependencies).sort()).toEqual(framework==='angular'?['@vypdev/ha-ui','tslib']:['@vypdev/ha-ui']); expect(manifest.version).toBe(JSON.parse(readFileSync('packages/core/package.json','utf8')).version); diff --git a/tests/browser/angular-field.test.ts b/tests/browser/angular-field.test.ts new file mode 100644 index 0000000..73b5a25 --- /dev/null +++ b/tests/browser/angular-field.test.ts @@ -0,0 +1,74 @@ +import '@angular/compiler'; +import {afterEach,expect,test,vi} from 'vitest'; +import {Component,signal} from '@angular/core'; +import {TestBed,type ComponentFixture} from '@angular/core/testing'; +import {BrowserTestingModule,platformBrowserTesting} from '@angular/platform-browser/testing'; +import {FormControl,FormGroup,ReactiveFormsModule,FormsModule,NgModel} from '@angular/forms'; +import {By} from '@angular/platform-browser'; +import {TextField} from '../../packages/angular/src/index.js'; +import {registerField,type FieldValueEvent} from '../../packages/core/src/components/field.js'; +import {fieldModel,fieldHost,fieldInput,propose,formEntries,malformedRequest,mutableRequest} from './field-framework-fixture.js'; +TestBed.initTestEnvironment(BrowserTestingModule,platformBrowserTesting()); +@Component({standalone:true,imports:[TextField,ReactiveFormsModule],template:`
`}) +class ReactiveConsumer { + group=new FormGroup({task:new FormControl('Kitchen',{nonNullable:true})});state=signal(fieldModel());observe=vi.fn((_event:FieldValueEvent)=>{}); +} +@Component({standalone:true,imports:[TextField,FormsModule],template:'
'}) +class ModelConsumer {state=fieldModel();value:string|null='Kitchen';} +@Component({standalone:true,imports:[TextField],template:`
`}) +class PlainConsumer {state=signal(fieldModel());value=signal('Kitchen');disabled=signal(false);external=signal(undefined);observe=vi.fn((_event:FieldValueEvent)=>{});} +function reactive(strategy:'change'|'blur'|'submit'='change'){ + const fixture=TestBed.createComponent(ReactiveConsumer);fixture.componentInstance.group=new FormGroup({task:new FormControl('Kitchen',{nonNullable:true,updateOn:strategy})});fixture.detectChanges();return fixture; +} +async function settle(fixture:ComponentFixture){fixture.detectChanges();await fixture.whenStable();await fieldHost().updateComplete;} +afterEach(()=>{TestBed.resetTestingModule();document.body.replaceChildren();}); +test('CVA validates and preserves a model written before explicit element upgrade',async()=>{ + const fixture=reactive();const field=fieldHost();expect(customElements.get('vyp-ha-field')).toBeUndefined();expect(field.value).toBe('Kitchen');expect(fixture.componentInstance.group.valid).toBe(true); + registerField();await settle(fixture);expect(fieldInput(field).value).toBe('Kitchen');expect(field.getAttribute('value')).toBe('Native baseline');expect(field.hasAttribute('state')).toBe(false);expect(document.querySelectorAll('vyp-ha-field')).toHaveLength(1); +}); +test('reactive controls receive exactly one accepted change, dirty/touched and programmatic writes',async()=>{ + registerField();const fixture=reactive(),field=fieldHost(),control=fixture.componentInstance.group.controls.task;await settle(fixture);const values=vi.fn();control.valueChanges.subscribe(values); + propose(field,'Laundry');await settle(fixture);expect(control.value).toBe('Laundry');expect(control.dirty).toBe(true);expect(values).toHaveBeenCalledExactlyOnceWith('Laundry');expect(formEntries()).toEqual([['task','Laundry']]);expect(fixture.componentInstance.observe).toHaveBeenCalledTimes(1); + field.focus();field.blur();expect(control.touched).toBe(true);control.setValue('Programmatic');await settle(fixture);expect(fieldInput(field).value).toBe('Programmatic');expect(fixture.componentInstance.observe).toHaveBeenCalledTimes(1); +}); +test('canceled requests never change Angular model/view state',async()=>{ + registerField();const fixture=reactive(),field=fieldHost();await settle(fixture);fixture.componentInstance.observe.mockImplementation(event=>event.preventDefault());propose(field,'Rejected'); + expect(fixture.componentInstance.group.controls.task.value).toBe('Kitchen');expect(fieldInput(field).value).toBe('Kitchen');expect(fixture.componentInstance.group.pristine).toBe(true); +}); +test('shared constraints update Angular validation without copying presentation policy',async()=>{ + registerField();const fixture=reactive(),field=fieldHost(),control=fixture.componentInstance.group.controls.task;await settle(fixture); + control.setValue('');expect(control.errors?.['haField'].valueMissing).toBe(true);control.setValue('a');expect(control.errors?.['haField'].tooShort).toBe(true);control.setValue('Long'.repeat(6));expect(control.errors?.['haField'].tooLong).toBe(true); + control.setValue('Kitchen');fixture.componentInstance.state.set(fieldModel({error:'Review this value.'}));await settle(fixture);expect(control.errors?.['haField'].customError).toBe(true); + fixture.componentInstance.state.set(fieldModel({readOnly:true,error:'Review this value.'}));await settle(fixture);expect(control.valid).toBe(true);expect(field.willValidate).toBe(false); + fixture.componentInstance.state.set(fieldModel());await settle(fixture);expect(control.valid).toBe(true); +}); +test('enable/disable and native/model resets preserve Angular defaults and pristine state',async()=>{ + registerField();const fixture=reactive(),field=fieldHost(),control=fixture.componentInstance.group.controls.task;await settle(fixture);control.disable();await settle(fixture);expect(fieldInput(field).disabled).toBe(true);expect(formEntries()).toEqual([]); + control.enable();await settle(fixture);propose(field,'Laundry');field.focus();field.blur();document.querySelector('form')!.reset();await settle(fixture); + expect(control.value).toBe('Kitchen');expect(field.value).toBe('Kitchen');expect(control.pristine).toBe(true);expect(control.untouched).toBe(true); + control.setValue('Changed');control.reset();await settle(fixture);expect(fieldInput(field).value).toBe('Kitchen'); +}); +test('updateOn blur preserves the draft until touch commits the model',async()=>{ + registerField();const fixture=reactive('blur'),field=fieldHost(),control=fixture.componentInstance.group.controls.task;await settle(fixture);field.focus();propose(field,'Laundry'); + expect(control.value).toBe('Kitchen');expect(fieldInput(field).value).toBe('Laundry');field.blur();await settle(fixture);expect(control.value).toBe('Laundry');expect(control.touched).toBe(true); +}); +test('updateOn submit commits through the actual Angular form directive',async()=>{ + registerField();const fixture=reactive('submit'),field=fieldHost(),control=fixture.componentInstance.group.controls.task;await settle(fixture);propose(field,'Laundry');expect(control.value).toBe('Kitchen'); + const form=document.querySelector('form')!;form.addEventListener('submit',event=>event.preventDefault());form.requestSubmit();await settle(fixture);expect(control.value).toBe('Laundry');expect(field.value).toBe('Laundry'); +}); +test('ngModel supports nullable programmatic reset and user changes',async()=>{ + registerField();const fixture=TestBed.createComponent(ModelConsumer);await settle(fixture);const field=fieldHost();expect(field.value).toBe('Kitchen');propose(field,'Laundry');await settle(fixture);expect(fixture.componentInstance.value).toBe('Laundry'); + const directive=fixture.debugElement.query(By.directive(NgModel)).injector.get(NgModel);directive.control.reset();await settle(fixture);expect(field.value).toBe('');expect(directive.control.pristine).toBe(true); +}); +test('standalone value/output, disabled/external owner and no registered callbacks remain safe',async()=>{ + registerField();const fixture=TestBed.createComponent(PlainConsumer);await settle(fixture);const field=fieldHost();propose(field,'Unhandled');expect(field.value).toBe('Kitchen');field.focus();field.blur(); + fixture.componentInstance.external.set('plain-form');fixture.componentInstance.disabled.set(true);await settle(fixture);expect(field.form?.id).toBe('plain-form');expect(fieldInput(field).disabled).toBe(true);fixture.componentInstance.disabled.set(false);fixture.componentInstance.external.set(undefined);fixture.componentInstance.value.set('Acknowledged');await settle(fixture);expect(field.hasAttribute('form')).toBe(false);expect(field.value).toBe('Acknowledged'); +}); +test('destroy removes both field and touch subscriptions from a retained host',async()=>{ + registerField();const fixture=reactive(),field=fieldHost(),control=fixture.componentInstance.group.controls.task;await settle(fixture);const observer=fixture.componentInstance.observe;fixture.destroy();propose(field,'Retained');field.dispatchEvent(new FocusEvent('focusout')); + expect(observer).not.toHaveBeenCalled();expect(control.value).toBe('Kitchen');expect(control.untouched).toBe(true); +}); + +test('rejects malformed native request detail before Angular output or model changes',async()=>{registerField();const fixture=reactive(),field=fieldHost();await settle(fixture);fixture.componentInstance.observe.mockImplementation(event=>event.preventDefault());expect(malformedRequest(field)).toHaveLength(1);expect(fixture.componentInstance.observe).not.toHaveBeenCalled();expect(fixture.componentInstance.group.controls.task.value).toBe('Kitchen');}); + +test('CVA uses its validated snapshot after a foreign payload is mutated',async()=>{registerField();const fixture=reactive(),field=fieldHost();await settle(fixture);fixture.componentInstance.observe.mockImplementation(event=>{Reflect.set(event.detail,'value',42);});mutableRequest(field);await settle(fixture);expect(fixture.componentInstance.group.controls.task.value).toBe('Snapshot');expect(field.value).toBe('Snapshot');}); diff --git a/tests/browser/field-framework-fixture.ts b/tests/browser/field-framework-fixture.ts new file mode 100644 index 0000000..d6694dd --- /dev/null +++ b/tests/browser/field-framework-fixture.ts @@ -0,0 +1,13 @@ +import {textFieldState} from '../../packages/core/src/field-contracts.js'; +import type {HaTextField} from '../../packages/core/src/components/field.js'; +export const fieldModel=(patch:Record={})=>textFieldState({label:'Task name',description:'A household task.',required:true,minLength:2,maxLength:20,...patch}); +export const fieldHost=()=>document.querySelector('vyp-ha-field')!; +export const fieldInput=(host:HaTextField)=>host.shadowRoot!.querySelector('input')!; +export function propose(host:HaTextField,value:string){fieldInput(host).value=value;fieldInput(host).dispatchEvent(new InputEvent('input',{bubbles:true,composed:true}));} +export const formEntries=()=>[...new FormData(document.querySelector('form')!).entries()]; +export function malformedRequest(host:HaTextField){ + const errors:Error[]=[];const capture=(event:ErrorEvent)=>{event.preventDefault();errors.push(event.error as Error);};window.addEventListener('error',capture); + try{host.dispatchEvent(new CustomEvent('ha-value-request',{detail:{value:42,reason:'input'},bubbles:true,composed:true,cancelable:true}));} + finally{window.removeEventListener('error',capture);}return errors; +} +export function mutableRequest(host:HaTextField){host.dispatchEvent(new CustomEvent('ha-value-request',{detail:{value:'Snapshot',reason:'input'},bubbles:true,composed:true,cancelable:true}));} diff --git a/tests/browser/fixtures/SvelteField.svelte b/tests/browser/fixtures/SvelteField.svelte new file mode 100644 index 0000000..b95aae7 --- /dev/null +++ b/tests/browser/fixtures/SvelteField.svelte @@ -0,0 +1,19 @@ + +
{value=next;}:undefined} id="svelte-field" class="field" lang="en" dir="ltr" /> diff --git a/tests/browser/react-field.test.ts b/tests/browser/react-field.test.ts new file mode 100644 index 0000000..745684c --- /dev/null +++ b/tests/browser/react-field.test.ts @@ -0,0 +1,52 @@ +import {afterEach,expect,test,vi} from 'vitest'; +import {act,createElement,StrictMode,useState} from 'react'; +import {createRoot,type Root} from 'react-dom/client'; +import {TextField} from '../../packages/react/src/index.js'; +import {registerField,type FieldValueEvent} from '../../packages/core/src/components/field.js'; +import {fieldModel,fieldHost,fieldInput,propose,formEntries,malformedRequest,mutableRequest} from './field-framework-fixture.js'; +import type {TextFieldState} from '../../packages/core/src/field-contracts.js'; +vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT',true); +let root:Root|undefined;let setValue:(value:string)=>void;let current=''; +type Options={state?:TextFieldState;observe?:(event:FieldValueEvent)=>void;change?:(value:string)=>void;ack?:boolean;disabled?:boolean;external?:boolean}; +function Consumer({state=fieldModel(),observe,change,ack=true,disabled=false,external=false}:Options){ + const [value,set]=useState('Kitchen');setValue=set;current=value; + return createElement('form',{id:'react-form'},createElement(TextField,{state,value,onValueRequest:observe,onValueChange:ack?(next:string)=>{change?.(next);set(next);}:undefined,name:'task',defaultValue:'Initial',disabled,form:external?'react-form':undefined,id:'react-field',className:'field',lang:'en',dir:'ltr'})); +} +async function render(options:Options={}){if(!root){const target=document.createElement('main');document.body.append(target);root=createRoot(target);}await act(()=>root!.render(createElement(StrictMode,null,createElement(Consumer,options))));return fieldHost();} +afterEach(async()=>{await act(()=>root?.unmount());root=undefined;document.body.replaceChildren();}); +test('pre-upgrade field props survive explicit registration without object/current-value attributes',async()=>{ + const field=await render();expect(customElements.get('vyp-ha-field')).toBeUndefined();expect(field.value).toBe('Kitchen');registerField();await field.updateComplete; + expect(fieldInput(field).value).toBe('Kitchen');expect(field.state.label).toBe('Task name');expect(field.getAttribute('value')).toBe('Initial');expect(field.hasAttribute('state')).toBe(false);expect(field.id).toBe('react-field');expect(field.className).toBe('field'); +}); +test('controlled React state acknowledges one request and programmatic changes emit none',async()=>{ + registerField();const observe=vi.fn(),change=vi.fn();const field=await render({observe,change});await field.updateComplete; + await act(()=>propose(field,'Laundry'));await field.updateComplete;expect(current).toBe('Laundry');expect(field.value).toBe('Laundry');expect(formEntries()).toEqual([['task','Laundry']]);expect(observe).toHaveBeenCalledTimes(1);expect(change).toHaveBeenCalledExactlyOnceWith('Laundry'); + await act(()=>setValue('Programmatic'));await field.updateComplete;expect(fieldInput(field).value).toBe('Programmatic');expect(observe).toHaveBeenCalledTimes(1); + field.focus();expect(field.shadowRoot!.activeElement).toBe(fieldInput(field)); +}); +test('cancellation, callback replacement/removal and unhandled requests retain the accepted model',async()=>{ + registerField();const old=vi.fn(),currentObserver=vi.fn((event:FieldValueEvent)=>event.preventDefault()),change=vi.fn();const field=await render({observe:old});await field.updateComplete; + await render({observe:currentObserver,change});await act(()=>propose(field,'Rejected'));expect(current).toBe('Kitchen');expect(fieldInput(field).value).toBe('Kitchen');expect(change).not.toHaveBeenCalled();expect(old).not.toHaveBeenCalled();expect(currentObserver).toHaveBeenCalledTimes(1); + await render({ack:false});await act(()=>propose(field,'Unhandled'));expect(field.value).toBe('Kitchen');expect(currentObserver).toHaveBeenCalledTimes(1); +}); +test('preserves required validation, disabled/external form properties and model reset',async()=>{ + registerField();const field=await render({external:true});await field.updateComplete;expect(field.form?.id).toBe('react-form'); + await act(()=>setValue(''));await field.updateComplete;field.addEventListener('invalid',event=>event.preventDefault());expect(document.querySelector('form')!.checkValidity()).toBe(false); + await render({disabled:true});await field.updateComplete;expect(fieldInput(field).disabled).toBe(true);expect(formEntries()).toEqual([]); + await render();await field.updateComplete;expect(field.hasAttribute('form')).toBe(false);expect(fieldInput(field).disabled).toBe(false); + await act(()=>document.querySelector('form')!.reset());await field.updateComplete;expect(current).toBe('Initial');expect(field.value).toBe('Initial'); +}); +test('unmount releases both callbacks even if the old host emits later',async()=>{ + registerField();const observe=vi.fn(),change=vi.fn();const field=await render({observe,change});await field.updateComplete;await act(()=>root!.unmount());root=undefined; + propose(field,'Retained');expect(observe).not.toHaveBeenCalled();expect(change).not.toHaveBeenCalled(); +}); + +test('optional React field props and callbacks need no wrapper model or attributes',async()=>{ + registerField();const target=document.createElement('main');document.body.append(target);root=createRoot(target); + await act(()=>root!.render(createElement(TextField,{state:fieldModel(),value:'Kitchen'})));const field=fieldHost();await field.updateComplete; + await act(()=>propose(field,'Unhandled'));expect(field.value).toBe('Kitchen');expect(field.name).toBe('');expect(field.defaultValue).toBe('');expect(field.disabled).toBe(false); +}); + +test('rejects malformed native request detail before either application callback',async()=>{registerField();const observe=vi.fn((event:FieldValueEvent)=>event.preventDefault());const field=await render({observe});await field.updateComplete;expect(malformedRequest(field)).toHaveLength(1);expect(observe).not.toHaveBeenCalled();expect(current).toBe('Kitchen');}); + +test('uses the validated snapshot if an observer mutates a foreign event payload',async()=>{registerField();const field=await render({observe:event=>{Reflect.set(event.detail,'value',42);}});await field.updateComplete;await act(()=>mutableRequest(field));await field.updateComplete;expect(current).toBe('Snapshot');expect(field.value).toBe('Snapshot');}); diff --git a/tests/browser/svelte-field.test.mjs b/tests/browser/svelte-field.test.mjs new file mode 100644 index 0000000..fedc03f --- /dev/null +++ b/tests/browser/svelte-field.test.mjs @@ -0,0 +1,37 @@ +import {afterEach,expect,test,vi} from 'vitest'; +import {mount,unmount,flushSync} from 'svelte'; +import Consumer from './fixtures/SvelteField.svelte'; +import TextField from '../../packages/svelte/src/Field.svelte'; +import {registerField} from '../../packages/core/src/components/field.js'; +import {fieldModel,fieldHost,fieldInput,propose,formEntries,malformedRequest,mutableRequest} from './field-framework-fixture.js'; +let instance; +function attach(component=Consumer,props={}){const target=document.createElement('main');document.body.append(target);instance=mount(component,{target,props});flushSync();return instance;} +afterEach(async()=>{if(instance)await unmount(instance);instance=undefined;document.body.replaceChildren();}); +test('compiled Svelte preserves pre-upgrade field props',async()=>{ + attach();const field=fieldHost();expect(customElements.get('vyp-ha-field')).toBeUndefined();registerField();await field.updateComplete; + expect(fieldInput(field).value).toBe('Kitchen');expect(field.state.label).toBe('Task name');expect(field.getAttribute('value')).toBe('Initial');expect(field.hasAttribute('state')).toBe(false);expect(field.id).toBe('svelte-field');expect(field.className).toBe('field'); +}); +test('typed callback props acknowledge one request through application runes',async()=>{ + registerField();const observe=vi.fn(),consumer=attach(Consumer,{observe}),field=fieldHost();await field.updateComplete; + flushSync(()=>propose(field,'Laundry'));await field.updateComplete;expect(consumer.current()).toBe('Laundry');expect(formEntries()).toEqual([['task','Laundry']]);expect(observe).toHaveBeenCalledTimes(1); + flushSync(()=>consumer.update('Programmatic'));await field.updateComplete;expect(fieldInput(field).value).toBe('Programmatic');expect(observe).toHaveBeenCalledTimes(1); +}); +test('replacement, cancellation and callback removal preserve the controlled model',async()=>{ + registerField();const old=vi.fn(),current=vi.fn(event=>event.preventDefault()),consumer=attach(Consumer,{observe:old}),field=fieldHost();await field.updateComplete; + flushSync(()=>consumer.setHandler(current));flushSync(()=>propose(field,'Rejected'));expect(consumer.current()).toBe('Kitchen');expect(fieldInput(field).value).toBe('Kitchen');expect(old).not.toHaveBeenCalled();expect(current).toHaveBeenCalledTimes(1); + flushSync(()=>{consumer.setHandler(undefined);consumer.stopAcknowledging();});flushSync(()=>propose(field,'Unhandled'));expect(field.value).toBe('Kitchen');expect(current).toHaveBeenCalledTimes(1); +}); +test('updates constraints, disabled/external owner and reset through the application model',async()=>{ + registerField();const consumer=attach(),field=fieldHost();await field.updateComplete;flushSync(()=>{consumer.setExternal(true);consumer.setDisabled(true);});await field.updateComplete; + expect(field.form?.id).toBe('svelte-form');expect(fieldInput(field).disabled).toBe(true);expect(formEntries()).toEqual([]); + flushSync(()=>{consumer.setExternal(false);consumer.setDisabled(false);consumer.configure(fieldModel({label:'Nombre',maxLength:3}));});await field.updateComplete;expect(field.hasAttribute('form')).toBe(false);expect(field.validity.tooLong).toBe(true);expect(fieldInput(field).labels[0].textContent).toBe('Nombre'); + flushSync(()=>document.querySelector('form').reset());await field.updateComplete;expect(consumer.current()).toBe('Initial');expect(field.value).toBe('Initial'); +}); +test('standalone defaults and absent callbacks stay controlled and unmount releases subscriptions',async()=>{ + registerField();attach(TextField,{state:fieldModel(),value:'Kitchen'});const field=fieldHost();await field.updateComplete;propose(field,'Unhandled');expect(fieldInput(field).value).toBe('Kitchen');expect(field.name).toBe('');expect(field.defaultValue).toBe(''); + await unmount(instance);instance=undefined;const observe=vi.fn();attach(Consumer,{observe});const retained=fieldHost();await retained.updateComplete;await unmount(instance);instance=undefined;propose(retained,'Retained');expect(observe).not.toHaveBeenCalled(); +}); + +test('rejects malformed native request detail before Svelte callbacks',async()=>{registerField();const observe=vi.fn(event=>event.preventDefault()),consumer=attach(Consumer,{observe}),field=fieldHost();await field.updateComplete;expect(malformedRequest(field)).toHaveLength(1);expect(observe).not.toHaveBeenCalled();expect(consumer.current()).toBe('Kitchen');}); + +test('application rune updates use the validated snapshot after a foreign payload is mutated',async()=>{registerField();const consumer=attach(Consumer,{observe:event=>{Reflect.set(event.detail,'value',42);}}),field=fieldHost();await field.updateComplete;flushSync(()=>mutableRequest(field));await field.updateComplete;expect(consumer.current()).toBe('Snapshot');expect(field.value).toBe('Snapshot');}); diff --git a/tests/browser/vue-field.test.ts b/tests/browser/vue-field.test.ts new file mode 100644 index 0000000..6dc80f9 --- /dev/null +++ b/tests/browser/vue-field.test.ts @@ -0,0 +1,42 @@ +import {afterEach,expect,test,vi} from 'vitest'; +import {createApp,h,nextTick,shallowRef,type App} from 'vue'; +import {TextField} from '../../packages/vue/src/index.js'; +import {registerField,type FieldValueEvent} from '../../packages/core/src/components/field.js'; +import {fieldModel,fieldHost,fieldInput,propose,formEntries,malformedRequest,mutableRequest} from './field-framework-fixture.js'; +let app:App|undefined; +function mount(observer?:(event:FieldValueEvent)=>void){ + const value=shallowRef('Kitchen'),state=shallowRef(fieldModel()),disabled=shallowRef(false),external=shallowRef(false),listener=shallowRef(observer),ack=shallowRef(true); + const target=document.createElement('main');document.body.append(target);app=createApp({render:()=>h('form',{id:'vue-form'},[h(TextField,{state:state.value,modelValue:value.value,'onUpdate:modelValue':ack.value?(next:string)=>{value.value=next;}:undefined,onValueRequest:listener.value,name:'task',defaultValue:'Initial',disabled:disabled.value,form:external.value?'vue-form':undefined,id:'vue-field',class:'field',lang:'en',dir:'ltr'})])});app.mount(target);return {value,state,disabled,external,listener,ack}; +} +afterEach(()=>{app?.unmount();app=undefined;document.body.replaceChildren();}); +test('Vue preserves field properties before explicit upgrade',async()=>{ + mount();const field=fieldHost();expect(customElements.get('vyp-ha-field')).toBeUndefined();registerField();await field.updateComplete; + expect(fieldInput(field).value).toBe('Kitchen');expect(field.state.label).toBe('Task name');expect(field.getAttribute('value')).toBe('Initial');expect(field.hasAttribute('state')).toBe(false);expect(field.id).toBe('vue-field');expect(field.className).toBe('field'); +}); +test('v-model acknowledges one original request and prop updates do not emit changes',async()=>{ + registerField();const observe=vi.fn();const model=mount(observe),field=fieldHost();await field.updateComplete; + propose(field,'Laundry');await nextTick();await field.updateComplete;expect(model.value.value).toBe('Laundry');expect(formEntries()).toEqual([['task','Laundry']]);expect(observe).toHaveBeenCalledTimes(1);expect(observe.mock.calls[0][0].detail).toEqual({value:'Laundry',reason:'input'}); + model.value.value='Programmatic';await nextTick();await field.updateComplete;expect(fieldInput(field).value).toBe('Programmatic');expect(observe).toHaveBeenCalledTimes(1); +}); +test('synchronous cancellation and listener/model-handler removal retain the accepted value',async()=>{ + registerField();const old=vi.fn(),current=vi.fn((event:FieldValueEvent)=>event.preventDefault());const model=mount(old),field=fieldHost();await field.updateComplete; + model.listener.value=current;await nextTick();propose(field,'Rejected');await nextTick();expect(model.value.value).toBe('Kitchen');expect(field.value).toBe('Kitchen');expect(old).not.toHaveBeenCalled();expect(current).toHaveBeenCalledTimes(1); + model.listener.value=undefined;model.ack.value=false;await nextTick();propose(field,'Unhandled');await nextTick();expect(fieldInput(field).value).toBe('Kitchen');expect(current).toHaveBeenCalledTimes(1); +}); +test('updates shared constraints, disabled and external owner properties and acknowledges reset',async()=>{ + registerField();const model=mount(),field=fieldHost();await field.updateComplete;model.external.value=true;model.disabled.value=true;await nextTick();await field.updateComplete; + expect(field.form?.id).toBe('vue-form');expect(fieldInput(field).disabled).toBe(true);expect(formEntries()).toEqual([]);model.disabled.value=false;model.external.value=false;model.state.value=fieldModel({label:'Nombre',maxLength:3});await nextTick();await field.updateComplete; + expect(field.hasAttribute('form')).toBe(false);expect(field.validity.tooLong).toBe(true);expect(fieldInput(field).labels![0].textContent).toBe('Nombre');model.state.value=fieldModel();await nextTick();document.querySelector('form')!.reset();await nextTick();await field.updateComplete;expect(model.value.value).toBe('Initial');expect(field.value).toBe('Initial'); +}); +test('Vue unmount removes native field subscriptions',async()=>{ + registerField();const observe=vi.fn();mount(observe);const field=fieldHost();await field.updateComplete;app!.unmount();app=undefined;propose(field,'Retained');expect(observe).not.toHaveBeenCalled(); +}); + +test('optional Vue field props and model subscribers need no extra attributes',async()=>{ + registerField();const target=document.createElement('main');document.body.append(target);app=createApp(TextField,{state:fieldModel(),modelValue:'Kitchen'});app.mount(target);const field=fieldHost();await field.updateComplete; + propose(field,'Unhandled');await nextTick();expect(field.value).toBe('Kitchen');expect(field.name).toBe('');expect(field.defaultValue).toBe('');expect(field.disabled).toBe(false); +}); + +test('rejects malformed native request detail before the app or model subscriber',async()=>{registerField();const observe=vi.fn((event:FieldValueEvent)=>event.preventDefault()),model=mount(observe),field=fieldHost();await field.updateComplete;expect(malformedRequest(field)).toHaveLength(1);expect(observe).not.toHaveBeenCalled();expect(model.value.value).toBe('Kitchen');}); + +test('model updates use the validated snapshot after a foreign payload is mutated',async()=>{registerField();const model=mount(event=>{Reflect.set(event.detail,'value',42);}),field=fieldHost();await field.updateComplete;mutableRequest(field);await nextTick();await field.updateComplete;expect(model.value.value).toBe('Snapshot');expect(field.value).toBe('Snapshot');}); diff --git a/tests/consumers/fields.ts b/tests/consumers/fields.ts new file mode 100644 index 0000000..ce61636 --- /dev/null +++ b/tests/consumers/fields.ts @@ -0,0 +1,37 @@ +import assert from 'node:assert/strict'; +import type {Page} from 'playwright'; +import type {HaTextField} from '@vypdev/ha-ui/field'; + +/** Actual installed framework models, not source aliases or simulated form handlers. */ +export async function exerciseField(page:Page){ + const textbox=page.getByRole('textbox',{name:'Task name',exact:true}),model=page.getByLabel('Field model'),events=page.getByLabel('Field events'); + const form=page.getByRole('form',{name:'Task form'}); + assert.equal(await textbox.inputValue(),'Kitchen');assert.equal(await model.textContent(),'Kitchen'); + assert.equal(await page.locator('vyp-ha-field').getAttribute('state'),null); + await textbox.fill('Laundry');await page.waitForFunction(()=>document.querySelector('output[aria-label="Field model"]')!.textContent==='Laundry'); + assert.equal(await events.textContent(),'1');await textbox.press('Enter'); + await page.waitForFunction(()=>document.querySelector('output[aria-label="Saved task"]')!.textContent!==''); + assert.deepEqual(JSON.parse((await page.getByLabel('Saved task').textContent())!),[['task','Laundry'],['action','save']]); + await page.getByRole('button',{name:'Reject field changes',exact:true}).click();await textbox.fill('Rejected'); + await page.waitForFunction(()=>document.querySelector('output[aria-label="Field events"]')!.textContent==='2'); + assert.equal(await textbox.inputValue(),'Laundry');assert.equal(await model.textContent(),'Laundry');assert.equal(await events.textContent(),'2'); + await page.getByRole('button',{name:'Reject field changes',exact:true}).click();await page.getByRole('button',{name:'Set field value',exact:true}).click(); + await textbox.waitFor({state:'visible'});await page.waitForFunction(()=>document.querySelector('vyp-ha-field')!.value==='Programmatic'); + assert.equal(await events.textContent(),'2');assert.notEqual(await page.locator('vyp-ha-field').getAttribute('value'),'Programmatic'); + await page.getByRole('button',{name:'Toggle field disabled',exact:true}).click();assert.equal(await textbox.isDisabled(),true); + assert.deepEqual(await form.evaluate(element=>[...new FormData(element as HTMLFormElement)]),[]); + await page.getByRole('button',{name:'Toggle field disabled',exact:true}).click();assert.equal(await textbox.isDisabled(),false); + await textbox.fill('');await page.getByRole('button',{name:'Save task',exact:true}).click();assert.equal(await textbox.getAttribute('aria-invalid'),'true'); + assert.equal(await page.getByText('Enter a value.',{exact:true}).isVisible(),true); + await page.getByRole('button',{name:'Reset task',exact:true}).click();await page.waitForFunction(()=>document.querySelector('output[aria-label="Field model"]')!.textContent==='Kitchen'); + assert.equal(await textbox.inputValue(),'Kitchen'); + const retained=await page.locator('vyp-ha-field').elementHandle();assert.ok(retained);const before=await events.textContent(); + await page.getByRole('button',{name:'Toggle field',exact:true}).click();await page.locator('vyp-ha-field').waitFor({state:'detached'}); + await retained.evaluate(host=>{const control=host.shadowRoot!.querySelector('input')!;control.value='Retained';control.dispatchEvent(new InputEvent('input',{bubbles:true,composed:true}));}); + assert.equal(await events.textContent(),before);assert.equal(await model.textContent(),'Kitchen'); + await page.getByRole('button',{name:'Toggle field',exact:true}).click();await textbox.fill('Remounted'); + await page.waitForFunction(()=>document.querySelector('output[aria-label="Field model"]')!.textContent==='Remounted'); + assert.equal(await events.textContent(),String(Number(before)+1)); + await page.getByRole('button',{name:'Reset task',exact:true}).click();await page.waitForFunction(()=>document.querySelector('output[aria-label="Field model"]')!.textContent==='Kitchen'); + return {controlledModel:true,exactlyOnce:true,cancellation:true,programmaticWrite:true,nativeSubmitter:true,requiredValidation:true,disabledSubmission:true,frameworkReset:true,teardownAndRemount:true}; +} diff --git a/tests/consumers/fixtures/angular/client.ts b/tests/consumers/fixtures/angular/client.ts index 9a2078d..3b3e81e 100644 --- a/tests/consumers/fixtures/angular/client.ts +++ b/tests/consumers/fixtures/angular/client.ts @@ -1,18 +1,31 @@ -import {Component,signal} from '@angular/core'; +import {Component,signal,computed} from '@angular/core'; import {bootstrapApplication} from '@angular/platform-browser'; -import {Tabs} from '@vypdev/ha-ui-angular'; +import {Tabs,TextField} from '@vypdev/ha-ui-angular'; import {registerTabs,type NavigationSelectionEvent} from '@vypdev/ha-ui/tabs'; -import {model} from './model.js'; -registerTabs(); -@Component({selector:'fixture-app',standalone:true,imports:[Tabs],template:` -
+import {FormControl,FormGroup,ReactiveFormsModule} from '@angular/forms'; +import {registerField,type FieldValueEvent} from '@vypdev/ha-ui/field'; +import {model,fieldModel} from './model.js'; +registerTabs();registerField(); +@Component({selector:'fixture-app',standalone:true,imports:[Tabs,TextField,ReactiveFormsModule],template:` +
{{count()}} @if(visible()){} + + + {{group.controls.task.value}}{{fieldEvents()}}{{saved()}} +
+ @if(fieldVisible()){} + +
`}) class Consumer { state=signal(model());spanish=signal(false);dark=signal(false);disabled=signal(false);visible=signal(true);count=signal(0); + group=new FormGroup({task:new FormControl('Kitchen',{nonNullable:true})});fieldVisible=signal(true);reject=signal(false);fieldEvents=signal(0);saved=signal('');fieldState=computed(()=>fieldModel(this.spanish())); + observe(event:FieldValueEvent){this.fieldEvents.update(count=>count+1);if(this.reject())event.preventDefault();} + toggleFieldDisabled(){const control=this.group.controls.task;if(control.disabled)control.enable();else control.disable();} + save(event:Event){event.preventDefault();const form=event.currentTarget as HTMLFormElement;if(form.reportValidity())this.saved.set(JSON.stringify([...new FormData(form,(event as SubmitEvent).submitter)]));} select(event:NavigationSelectionEvent){event.preventDefault();this.state.set(model(this.spanish(),event.detail.id,this.disabled()));this.count.update(value=>value+1);} language(){this.spanish.set(!this.spanish());this.state.set(model(this.spanish(),this.state().selected!,this.disabled()));} toggleDisabled(){this.disabled.set(!this.disabled());this.state.set(model(this.spanish(),'overview',this.disabled()));} diff --git a/tests/consumers/fixtures/angular/model.ts b/tests/consumers/fixtures/angular/model.ts index 91b2d4a..33e122a 100644 --- a/tests/consumers/fixtures/angular/model.ts +++ b/tests/consumers/fixtures/angular/model.ts @@ -3,3 +3,6 @@ import type {NavigationState} from '@vypdev/ha-ui/contracts'; export function model(spanish=false,selected='overview',disabled=false):NavigationState { return navigationState({kind:'panels',label:spanish?'Secciones del hogar':'Household sections',selected,items:[{id:'overview',label:spanish?'Resumen':'Overview'},{id:'tasks',label:spanish?'Tareas':'Tasks',disabled}]}); } + +import {textFieldState} from '@vypdev/ha-ui/field-contracts'; +export function fieldModel(spanish=false){return textFieldState({label:spanish?'Nombre de la tarea':'Task name',description:spanish?'Una tarea del hogar.':'A household task.',required:true,minLength:3,maxLength:24,requiredMessage:spanish?'Introduce un valor.':'Enter a value.'});} diff --git a/tests/consumers/fixtures/angular/package.json b/tests/consumers/fixtures/angular/package.json index f732641..791632a 100644 --- a/tests/consumers/fixtures/angular/package.json +++ b/tests/consumers/fixtures/angular/package.json @@ -11,7 +11,8 @@ "@angular/compiler": "22.2.4", "@angular/platform-browser": "22.2.4", "rxjs": "7.8.2", - "tslib": "2.8.1" + "tslib": "2.8.1", + "@angular/forms": "22.2.4" }, "devDependencies": { "typescript": "6.0.3" diff --git a/tests/consumers/fixtures/angular/pnpm-lock.yaml b/tests/consumers/fixtures/angular/pnpm-lock.yaml index 0a4c334..0a8a03b 100644 --- a/tests/consumers/fixtures/angular/pnpm-lock.yaml +++ b/tests/consumers/fixtures/angular/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@angular/core': specifier: 22.2.4 version: 22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2) + '@angular/forms': + specifier: 22.2.4 + version: 22.2.4(@angular/common@22.2.4(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))(@angular/platform-browser@22.2.4(@angular/common@22.2.4(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2)))(rxjs@7.8.2) '@angular/platform-browser': specifier: 22.2.4 version: 22.2.4(@angular/common@22.2.4(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2)) @@ -28,7 +31,7 @@ importers: version: file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz '@vypdev/ha-ui-angular': specifier: file:../pack-1/vypdev-ha-ui-angular-0.1.0-next.1.tgz - version: file:../pack-1/vypdev-ha-ui-angular-0.1.0-next.1.tgz(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2)) + version: file:../pack-1/vypdev-ha-ui-angular-0.1.0-next.1.tgz(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))(@angular/forms@22.2.4(@angular/common@22.2.4(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))(@angular/platform-browser@22.2.4(@angular/common@22.2.4(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2)))(rxjs@7.8.2)) rxjs: specifier: 7.8.2 version: 7.8.2 @@ -66,6 +69,15 @@ packages: zone.js: optional: true + '@angular/forms@22.2.4': + resolution: {integrity: sha512-siFy9s0p4PjPUeDX7zI7bo5x3YstBnFfBGqO/obL0TdR2rBxrmW8iR11TxbGh41NKYc5GvbZezVKsN8WEW2ooA==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} + peerDependencies: + '@angular/common': 22.2.4 + '@angular/core': 22.2.4 + '@angular/platform-browser': 22.2.4 + rxjs: ^6.5.3 || ^7.4.0 + '@angular/platform-browser@22.2.4': resolution: {integrity: sha512-NAQ8/Zcl8nRlNqxlXQGrQJ/c5V8lN5vJSlc8qkD3OUN+ClyNuS6UCXffYtKvvbovpyb/q1uYSQjqiEctz+5niw==} engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} @@ -83,17 +95,21 @@ packages: '@lit/reactive-element@2.1.2': resolution: {integrity: sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} '@vypdev/ha-ui-angular@file:../pack-1/vypdev-ha-ui-angular-0.1.0-next.1.tgz': - resolution: {integrity: sha512-s7DoeAOD/Nm4EPpQzIT2C0rSnsRt6LdHm1/4qZADOBacJSeXUQDZfG8hLMiTn+L4w2u3nqZ6luN/hywOGyzgqg==, tarball: file:../pack-1/vypdev-ha-ui-angular-0.1.0-next.1.tgz} + resolution: {integrity: sha512-yGs4ylEBWbktOQy/PMbdSVX/iJ6yD/YmPcWiJpRkgk1xNFdaELx42EWMfJu3bsbHn1DHaZ8bbuIfyMUOunJakA==, tarball: file:../pack-1/vypdev-ha-ui-angular-0.1.0-next.1.tgz} version: 0.1.0-next.1 peerDependencies: '@angular/core': 22.2.4 + '@angular/forms': 22.2.4 '@vypdev/ha-ui@file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz': - resolution: {integrity: sha512-dGtzvFkv/M6fOZGQVI47SpL+jW3yzHRvZ9JFVqKSJ0Tp/JAl2anc9/kqe0PX55Yu+FUVHsjH4o/BoMtIG6kFOw==, tarball: file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz} + resolution: {integrity: sha512-eQ0pPhKQBB9PEJkyeOMiRnWl6o3X6QdxH/mop65f0RSFtuQQX7OQLGMtsupDUziFzcqhigZUKDDV85berl8Cig==, tarball: file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz} version: 0.1.0-next.1 lit-element@4.2.2: @@ -116,6 +132,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + zod@4.6.7: + resolution: {integrity: sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==} + snapshots: '@angular/common@22.2.4(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))(rxjs@7.8.2)': @@ -135,6 +154,16 @@ snapshots: optionalDependencies: '@angular/compiler': 22.2.4 + '@angular/forms@22.2.4(@angular/common@22.2.4(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))(@angular/platform-browser@22.2.4(@angular/common@22.2.4(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2)))(rxjs@7.8.2)': + dependencies: + '@angular/common': 22.2.4(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))(rxjs@7.8.2) + '@angular/core': 22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2) + '@angular/platform-browser': 22.2.4(@angular/common@22.2.4(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2)) + '@standard-schema/spec': 1.1.0 + rxjs: 7.8.2 + tslib: 2.8.1 + zod: 4.6.7 + '@angular/platform-browser@22.2.4(@angular/common@22.2.4(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))': dependencies: '@angular/common': 22.2.4(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))(rxjs@7.8.2) @@ -147,11 +176,14 @@ snapshots: dependencies: '@lit-labs/ssr-dom-shim': 1.6.0 + '@standard-schema/spec@1.1.0': {} + '@types/trusted-types@2.0.7': {} - '@vypdev/ha-ui-angular@file:../pack-1/vypdev-ha-ui-angular-0.1.0-next.1.tgz(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))': + '@vypdev/ha-ui-angular@file:../pack-1/vypdev-ha-ui-angular-0.1.0-next.1.tgz(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))(@angular/forms@22.2.4(@angular/common@22.2.4(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))(@angular/platform-browser@22.2.4(@angular/common@22.2.4(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2)))(rxjs@7.8.2))': dependencies: '@angular/core': 22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2) + '@angular/forms': 22.2.4(@angular/common@22.2.4(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))(@angular/platform-browser@22.2.4(@angular/common@22.2.4(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.2.4(@angular/compiler@22.2.4)(rxjs@7.8.2)))(rxjs@7.8.2) '@vypdev/ha-ui': file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz tslib: 2.8.1 @@ -182,3 +214,5 @@ snapshots: tslib@2.8.1: {} typescript@6.0.3: {} + + zod@4.6.7: {} diff --git a/tests/consumers/fixtures/react/client.ts b/tests/consumers/fixtures/react/client.ts index e7d5db5..6d6be31 100644 --- a/tests/consumers/fixtures/react/client.ts +++ b/tests/consumers/fixtures/react/client.ts @@ -1,19 +1,28 @@ -import {createElement,useState,type CSSProperties} from 'react'; +import {createElement,useState,StrictMode,type CSSProperties,type SyntheticEvent} from 'react'; import {createRoot} from 'react-dom/client'; -import {Tabs} from '@vypdev/ha-ui-react'; +import {Tabs,TextField} from '@vypdev/ha-ui-react'; import {registerTabs,type NavigationSelectionEvent} from '@vypdev/ha-ui/tabs'; -import {model} from './model.js'; -registerTabs(); +import {registerField,type FieldValueEvent} from '@vypdev/ha-ui/field'; +import {model,fieldModel} from './model.js'; +registerTabs();registerField(); function Consumer() { const [state,setState]=useState(model()),[spanish,setSpanish]=useState(false),[dark,setDark]=useState(false),[disabled,setDisabled]=useState(false),[visible,setVisible]=useState(true),[count,setCount]=useState(0); + const [value,setValue]=useState('Kitchen'),[fieldDisabled,setFieldDisabled]=useState(false),[fieldVisible,setFieldVisible]=useState(true),[reject,setReject]=useState(false),[fieldEvents,setFieldEvents]=useState(0),[saved,setSaved]=useState(''); + const observe=(event:FieldValueEvent)=>{setFieldEvents(count=>count+1);if(reject)event.preventDefault();}; + const save=(event:SyntheticEvent)=>{event.preventDefault();const form=event.currentTarget;if(form.reportValidity())setSaved(JSON.stringify([...new FormData(form,(event.nativeEvent as SubmitEvent).submitter)]));}; const select=(event:NavigationSelectionEvent)=>{event.preventDefault();setState(model(spanish,event.detail.id,disabled));setCount(value=>value+1);}; const language=()=>{setSpanish(!spanish);setState(model(!spanish,state.selected!,disabled));}; const toggleDisabled=()=>{setDisabled(!disabled);setState(model(spanish,'overview',!disabled));}; - const style={'--primary-text-color':dark?'rgb(235,235,235)':'rgb(28,28,28)','--secondary-text-color':dark?'rgb(189,189,189)':'rgb(89,89,89)','--card-background-color':dark?'rgb(20,20,20)':'rgb(255,255,255)'} as CSSProperties; + const style={backgroundColor:dark?'rgb(20,20,20)':'rgb(255,255,255)','--divider-color':dark?'rgb(66,66,66)':'rgb(224,224,224)','--primary-text-color':dark?'rgb(235,235,235)':'rgb(28,28,28)','--secondary-text-color':dark?'rgb(189,189,189)':'rgb(89,89,89)','--card-background-color':dark?'rgb(20,20,20)':'rgb(255,255,255)'} as CSSProperties; return createElement('section',{style}, createElement('button',{onClick:language},'Switch language'),createElement('button',{onClick:()=>setDark(!dark)},'Toggle theme'), createElement('button',{onClick:toggleDisabled},'Toggle disabled'),createElement('button',{onClick:()=>setVisible(!visible)},'Toggle component'), createElement('output',{'aria-label':'Selection events'},String(count)), - visible?createElement(Tabs,{state,onSelectionRequest:select},createElement('button',{slot:'tasks'},'Add task')):null); + visible?createElement(Tabs,{state,onSelectionRequest:select},createElement('button',{slot:'tasks'},'Add task')):null, + createElement('button',{onClick:()=>setValue('Programmatic')},'Set field value'),createElement('button',{onClick:()=>setReject(!reject)},'Reject field changes'), + createElement('button',{onClick:()=>setFieldDisabled(!fieldDisabled)},'Toggle field disabled'),createElement('button',{onClick:()=>setFieldVisible(!fieldVisible)},'Toggle field'), + createElement('output',{'aria-label':'Field model'},value),createElement('output',{'aria-label':'Field events'},String(fieldEvents)),createElement('output',{'aria-label':'Saved task'},saved), + createElement('form',{'aria-label':'Task form',onSubmit:save},fieldVisible?createElement(TextField,{state:fieldModel(spanish),value,name:'task',defaultValue:'Kitchen',disabled:fieldDisabled,onValueRequest:observe,onValueChange:setValue}):null, + createElement('button',{type:'submit',name:'action',value:'save'},'Save task'),createElement('button',{type:'reset'},'Reset task'))); } -createRoot(document.querySelector('main')!).render(createElement(Consumer)); +createRoot(document.querySelector('main')!).render(createElement(StrictMode,null,createElement(Consumer))); diff --git a/tests/consumers/fixtures/react/model.ts b/tests/consumers/fixtures/react/model.ts index 91b2d4a..33e122a 100644 --- a/tests/consumers/fixtures/react/model.ts +++ b/tests/consumers/fixtures/react/model.ts @@ -3,3 +3,6 @@ import type {NavigationState} from '@vypdev/ha-ui/contracts'; export function model(spanish=false,selected='overview',disabled=false):NavigationState { return navigationState({kind:'panels',label:spanish?'Secciones del hogar':'Household sections',selected,items:[{id:'overview',label:spanish?'Resumen':'Overview'},{id:'tasks',label:spanish?'Tareas':'Tasks',disabled}]}); } + +import {textFieldState} from '@vypdev/ha-ui/field-contracts'; +export function fieldModel(spanish=false){return textFieldState({label:spanish?'Nombre de la tarea':'Task name',description:spanish?'Una tarea del hogar.':'A household task.',required:true,minLength:3,maxLength:24,requiredMessage:spanish?'Introduce un valor.':'Enter a value.'});} diff --git a/tests/consumers/fixtures/react/pnpm-lock.yaml b/tests/consumers/fixtures/react/pnpm-lock.yaml index e0627d9..d66d053 100644 --- a/tests/consumers/fixtures/react/pnpm-lock.yaml +++ b/tests/consumers/fixtures/react/pnpm-lock.yaml @@ -54,13 +54,13 @@ packages: resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} '@vypdev/ha-ui-react@file:../pack-1/vypdev-ha-ui-react-0.1.0-next.1.tgz': - resolution: {integrity: sha512-YW8N3lQcayr/pPzmVSiTpthWH3ikCVfEzs6JiHBHB1cce70JWjf2ytr4RJjuBofN+rg6SukUtaqcaUeeRqzFhQ==, tarball: file:../pack-1/vypdev-ha-ui-react-0.1.0-next.1.tgz} + resolution: {integrity: sha512-9L1HO9Lr5jlc7dowSlmU78DdLL90lIoi5pm/UNiadLeg+O5ci8WED/hTWClEiKL3wxfTbHm5mqGVpFxv6xIi5g==, tarball: file:../pack-1/vypdev-ha-ui-react-0.1.0-next.1.tgz} version: 0.1.0-next.1 peerDependencies: react: 19.3.1 '@vypdev/ha-ui@file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz': - resolution: {integrity: sha512-dGtzvFkv/M6fOZGQVI47SpL+jW3yzHRvZ9JFVqKSJ0Tp/JAl2anc9/kqe0PX55Yu+FUVHsjH4o/BoMtIG6kFOw==, tarball: file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz} + resolution: {integrity: sha512-eQ0pPhKQBB9PEJkyeOMiRnWl6o3X6QdxH/mop65f0RSFtuQQX7OQLGMtsupDUziFzcqhigZUKDDV85berl8Cig==, tarball: file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz} version: 0.1.0-next.1 csstype@3.2.3: diff --git a/tests/consumers/fixtures/svelte/Consumer.svelte b/tests/consumers/fixtures/svelte/Consumer.svelte index 6baadd7..97384eb 100644 --- a/tests/consumers/fixtures/svelte/Consumer.svelte +++ b/tests/consumers/fixtures/svelte/Consumer.svelte @@ -1,16 +1,27 @@ {#snippet tasks()}{/snippet} -
+
{count} {#if visible}{/if} + + + {value}{fieldEvents}{saved} +
+ {#if fieldVisible}value=next} />{/if} + +
diff --git a/tests/consumers/fixtures/svelte/client.ts b/tests/consumers/fixtures/svelte/client.ts index b4ff92b..f3f88b8 100644 --- a/tests/consumers/fixtures/svelte/client.ts +++ b/tests/consumers/fixtures/svelte/client.ts @@ -1,4 +1,5 @@ import {mount} from 'svelte'; import {registerTabs} from '@vypdev/ha-ui/tabs'; +import {registerField} from '@vypdev/ha-ui/field'; import Consumer from './Consumer.svelte'; -registerTabs();mount(Consumer,{target:document.querySelector('main')!}); +registerTabs();registerField();mount(Consumer,{target:document.querySelector('main')!}); diff --git a/tests/consumers/fixtures/svelte/model.ts b/tests/consumers/fixtures/svelte/model.ts index 91b2d4a..33e122a 100644 --- a/tests/consumers/fixtures/svelte/model.ts +++ b/tests/consumers/fixtures/svelte/model.ts @@ -3,3 +3,6 @@ import type {NavigationState} from '@vypdev/ha-ui/contracts'; export function model(spanish=false,selected='overview',disabled=false):NavigationState { return navigationState({kind:'panels',label:spanish?'Secciones del hogar':'Household sections',selected,items:[{id:'overview',label:spanish?'Resumen':'Overview'},{id:'tasks',label:spanish?'Tareas':'Tasks',disabled}]}); } + +import {textFieldState} from '@vypdev/ha-ui/field-contracts'; +export function fieldModel(spanish=false){return textFieldState({label:spanish?'Nombre de la tarea':'Task name',description:spanish?'Una tarea del hogar.':'A household task.',required:true,minLength:3,maxLength:24,requiredMessage:spanish?'Introduce un valor.':'Enter a value.'});} diff --git a/tests/consumers/fixtures/svelte/pnpm-lock.yaml b/tests/consumers/fixtures/svelte/pnpm-lock.yaml index 80c405c..41a142b 100644 --- a/tests/consumers/fixtures/svelte/pnpm-lock.yaml +++ b/tests/consumers/fixtures/svelte/pnpm-lock.yaml @@ -61,13 +61,13 @@ packages: resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} '@vypdev/ha-ui-svelte@file:../pack-1/vypdev-ha-ui-svelte-0.1.0-next.1.tgz': - resolution: {integrity: sha512-N1MkQQXMEqYlnd8qvjOHfDulrlriBe8Zrt8V+DKJAqFQK/bmJZYiFtUk12Q05pFj62ho8+86dH0ztNFljIzBlg==, tarball: file:../pack-1/vypdev-ha-ui-svelte-0.1.0-next.1.tgz} + resolution: {integrity: sha512-EKozyab8BCoCl9QsXiaXim5XlCJD9qkiIobcLCyFLKAi/Qr+Tr8Els1FhkYPTtGTHBYp9KPIdyKIWUH7SbWjlg==, tarball: file:../pack-1/vypdev-ha-ui-svelte-0.1.0-next.1.tgz} version: 0.1.0-next.1 peerDependencies: svelte: 5.57.3 '@vypdev/ha-ui@file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz': - resolution: {integrity: sha512-dGtzvFkv/M6fOZGQVI47SpL+jW3yzHRvZ9JFVqKSJ0Tp/JAl2anc9/kqe0PX55Yu+FUVHsjH4o/BoMtIG6kFOw==, tarball: file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz} + resolution: {integrity: sha512-eQ0pPhKQBB9PEJkyeOMiRnWl6o3X6QdxH/mop65f0RSFtuQQX7OQLGMtsupDUziFzcqhigZUKDDV85berl8Cig==, tarball: file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz} version: 0.1.0-next.1 acorn@8.19.0: diff --git a/tests/consumers/fixtures/vue/client.ts b/tests/consumers/fixtures/vue/client.ts index e21426f..3bd8a2f 100644 --- a/tests/consumers/fixtures/vue/client.ts +++ b/tests/consumers/fixtures/vue/client.ts @@ -1,17 +1,26 @@ import {createApp,h,shallowRef} from 'vue'; -import {Tabs} from '@vypdev/ha-ui-vue'; +import {Tabs,TextField} from '@vypdev/ha-ui-vue'; import {registerTabs,type NavigationSelectionEvent} from '@vypdev/ha-ui/tabs'; -import {model} from './model.js'; -registerTabs(); +import {registerField,type FieldValueEvent} from '@vypdev/ha-ui/field'; +import {model,fieldModel} from './model.js'; +registerTabs();registerField(); createApp({setup(){ const state=shallowRef(model()),spanish=shallowRef(false),dark=shallowRef(false),disabled=shallowRef(false),visible=shallowRef(true),count=shallowRef(0); + const value=shallowRef('Kitchen'),fieldDisabled=shallowRef(false),fieldVisible=shallowRef(true),reject=shallowRef(false),fieldEvents=shallowRef(0),saved=shallowRef(''); + const observe=(event:FieldValueEvent)=>{fieldEvents.value++;if(reject.value)event.preventDefault();}; + const save=(event:Event)=>{event.preventDefault();const form=event.currentTarget as HTMLFormElement;if(form.reportValidity())saved.value=JSON.stringify([...new FormData(form,(event as SubmitEvent).submitter)]);}; const select=(event:NavigationSelectionEvent)=>{event.preventDefault();state.value=model(spanish.value,event.detail.id,disabled.value);count.value++;}; const language=()=>{spanish.value=!spanish.value;state.value=model(spanish.value,state.value.selected!,disabled.value);}; const toggleDisabled=()=>{disabled.value=!disabled.value;state.value=model(spanish.value,'overview',disabled.value);}; - return ()=>h('section',{style:{'--primary-text-color':dark.value?'rgb(235,235,235)':'rgb(28,28,28)','--secondary-text-color':dark.value?'rgb(189,189,189)':'rgb(89,89,89)','--card-background-color':dark.value?'rgb(20,20,20)':'rgb(255,255,255)'}},[ + return ()=>h('section',{style:{backgroundColor:dark.value?'rgb(20,20,20)':'rgb(255,255,255)','--divider-color':dark.value?'rgb(66,66,66)':'rgb(224,224,224)','--primary-text-color':dark.value?'rgb(235,235,235)':'rgb(28,28,28)','--secondary-text-color':dark.value?'rgb(189,189,189)':'rgb(89,89,89)','--card-background-color':dark.value?'rgb(20,20,20)':'rgb(255,255,255)'}},[ h('button',{onClick:language},'Switch language'),h('button',{onClick:()=>dark.value=!dark.value},'Toggle theme'), h('button',{onClick:toggleDisabled},'Toggle disabled'),h('button',{onClick:()=>visible.value=!visible.value},'Toggle component'), h('output',{'aria-label':'Selection events'},String(count.value)), visible.value?h(Tabs,{state:state.value,onSelectionRequest:select},{default:()=>h('button',{slot:'tasks'},'Add task')}):null, + h('button',{onClick:()=>value.value='Programmatic'},'Set field value'),h('button',{onClick:()=>reject.value=!reject.value},'Reject field changes'), + h('button',{onClick:()=>fieldDisabled.value=!fieldDisabled.value},'Toggle field disabled'),h('button',{onClick:()=>fieldVisible.value=!fieldVisible.value},'Toggle field'), + h('output',{'aria-label':'Field model'},value.value),h('output',{'aria-label':'Field events'},String(fieldEvents.value)),h('output',{'aria-label':'Saved task'},saved.value), + h('form',{'aria-label':'Task form',onSubmit:save},[fieldVisible.value?h(TextField,{state:fieldModel(spanish.value),modelValue:value.value,'onUpdate:modelValue':(next:string)=>value.value=next,name:'task',defaultValue:'Kitchen',disabled:fieldDisabled.value,onValueRequest:observe}):null, + h('button',{type:'submit',name:'action',value:'save'},'Save task'),h('button',{type:'reset'},'Reset task')]), ]); }}).mount(document.querySelector('main')!); diff --git a/tests/consumers/fixtures/vue/model.ts b/tests/consumers/fixtures/vue/model.ts index 91b2d4a..33e122a 100644 --- a/tests/consumers/fixtures/vue/model.ts +++ b/tests/consumers/fixtures/vue/model.ts @@ -3,3 +3,6 @@ import type {NavigationState} from '@vypdev/ha-ui/contracts'; export function model(spanish=false,selected='overview',disabled=false):NavigationState { return navigationState({kind:'panels',label:spanish?'Secciones del hogar':'Household sections',selected,items:[{id:'overview',label:spanish?'Resumen':'Overview'},{id:'tasks',label:spanish?'Tareas':'Tasks',disabled}]}); } + +import {textFieldState} from '@vypdev/ha-ui/field-contracts'; +export function fieldModel(spanish=false){return textFieldState({label:spanish?'Nombre de la tarea':'Task name',description:spanish?'Una tarea del hogar.':'A household task.',required:true,minLength:3,maxLength:24,requiredMessage:spanish?'Introduce un valor.':'Enter a value.'});} diff --git a/tests/consumers/fixtures/vue/pnpm-lock.yaml b/tests/consumers/fixtures/vue/pnpm-lock.yaml index 5b8f17b..1868b27 100644 --- a/tests/consumers/fixtures/vue/pnpm-lock.yaml +++ b/tests/consumers/fixtures/vue/pnpm-lock.yaml @@ -84,13 +84,13 @@ packages: resolution: {integrity: sha512-H82XLH0DjmSOdbEwO15x3QTo1f5PWEtv237eLpYKc0mqLu+B8hnq16lFWmuBmNurkeiG3wr+oM1FhOll/YVFRg==} '@vypdev/ha-ui-vue@file:../pack-1/vypdev-ha-ui-vue-0.1.0-next.1.tgz': - resolution: {integrity: sha512-aN0lS7oJEntPqjdiUC9W4isVPpZt5w6PrnZ9yVBnrsj7hkkZ5x3gq8HPT1yaIM7gMu/PZuLyvvtWZgVR3H18Rg==, tarball: file:../pack-1/vypdev-ha-ui-vue-0.1.0-next.1.tgz} + resolution: {integrity: sha512-C1ccI0yUN24wsuANpVcSpVTpE+m+puWY0093paTYQUf9Fm+z+D489BL1xQRFBhNaAS6sdzBZF27HL5VLWKo10Q==, tarball: file:../pack-1/vypdev-ha-ui-vue-0.1.0-next.1.tgz} version: 0.1.0-next.1 peerDependencies: vue: 3.5.44 '@vypdev/ha-ui@file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz': - resolution: {integrity: sha512-dGtzvFkv/M6fOZGQVI47SpL+jW3yzHRvZ9JFVqKSJ0Tp/JAl2anc9/kqe0PX55Yu+FUVHsjH4o/BoMtIG6kFOw==, tarball: file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz} + resolution: {integrity: sha512-eQ0pPhKQBB9PEJkyeOMiRnWl6o3X6QdxH/mop65f0RSFtuQQX7OQLGMtsupDUziFzcqhigZUKDDV85berl8Cig==, tarball: file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz} version: 0.1.0-next.1 csstype@3.2.3: diff --git a/tests/consumers/frameworks.ts b/tests/consumers/frameworks.ts index 53dd0ff..515b86f 100644 --- a/tests/consumers/frameworks.ts +++ b/tests/consumers/frameworks.ts @@ -1,3 +1,4 @@ +import {exerciseField} from './fields.ts'; import assert from 'node:assert/strict'; import {readFile,writeFile,readdir,cp,mkdir} from 'node:fs/promises'; import {resolve,join,dirname,relative} from 'node:path'; @@ -66,6 +67,7 @@ export async function exerciseFrameworks(root:string,scratch:string,browser:Brow try { await server.listen();const address=server.httpServer!.address();assert.ok(address&&typeof address==='object'); await page.goto('http://127.0.0.1:'+address.port); + const textField=await exerciseField(page); await page.getByRole('tab',{name:'Tasks',exact:true}).click(); await page.getByRole('button',{name:'Add task',exact:true}).waitFor({state:'visible'}); assert.equal(await page.getByLabel('Selection events').textContent(),'1'); @@ -95,8 +97,15 @@ export async function exerciseFrameworks(root:string,scratch:string,browser:Brow await page.getByRole('button',{name:'Add task',exact:true}).waitFor({state:'visible'}); await mkdir(join(root,'test-results'),{recursive:true}); await page.locator('vyp-ha-tabs').screenshot({path:join(root,'test-results','packed-'+framework+'-dark-es.png')}); + const textbox=page.getByRole('textbox',{name:'Nombre de la tarea',exact:true}); + assert.equal(await textbox.inputValue(),'Kitchen'); + assert.equal(await textbox.evaluate(control=>getComputedStyle(control).color),'rgb(235, 235, 235)'); + await page.locator('vyp-ha-field').screenshot({path:join(root,'test-results','packed-'+framework+'-field-dark-es.png')}); + await textbox.fill('');await page.getByRole('button',{name:'Save task',exact:true}).click(); + assert.equal(await page.getByText('Introduce un valor.',{exact:true}).isVisible(),true); + await page.locator('vyp-ha-field').screenshot({path:join(root,'test-results','packed-'+framework+'-field-error-dark-es.png')}); assert.deepEqual(errors,[]); - results.push({framework,compiled:true,frozenOfflineInstall:true,noForeignFramework:true,candidateArchives,props:true,events:true,slots:true,disabled:true,runtimeSpanishAndTheme:true,keyboard:true,teardownAndRemount:true,browser:browser.version(),errors}); + results.push({framework,compiled:true,frozenOfflineInstall:true,noForeignFramework:true,candidateArchives,props:true,events:true,slots:true,disabled:true,runtimeSpanishAndTheme:true,keyboard:true,teardownAndRemount:true,textField,browser:browser.version(),errors}); console.log('Packed '+framework+' compiler, bindings and real Chromium lifecycle passed'); } finally {await page.close();await server.close();} } diff --git a/tests/consumers/packed.test.ts b/tests/consumers/packed.test.ts index 85db9fd..cb81dd6 100644 --- a/tests/consumers/packed.test.ts +++ b/tests/consumers/packed.test.ts @@ -84,15 +84,18 @@ test('the actual packed package is reproducible and passes clean DOM-free and re assert.equal(await readFile(join(consumer,'pnpm-lock.yaml'),'utf8'),frozenLock); await assert.rejects(readFile(join(cache,'metadata-v1.3/registry.npmjs.org/lit.json')),{code:'ENOENT'}); await writeFile(join(consumer, 'contract.ts'), `import {navigationState, type NavigationKind} from '@vypdev/ha-ui/contracts'; -import {textFieldState,textFieldValidity} from '@vypdev/ha-ui/field-contracts'; +import {textFieldState,textFieldValidity,fieldValueDetail} from '@vypdev/ha-ui/field-contracts'; const field=textFieldState({label:'Name',required:true}); if(!textFieldValidity('',field).flags.valueMissing)throw Error('Expected pure field validation'); +const request=fieldValueDetail({value:'Kitchen',reason:'input'});if(request.reason!=='input')throw Error('Expected typed request'); const kind: NavigationKind = 'panels'; const selected: string | null = navigationState({kind, label:'Sections', selected:null, items:[]}).selected; void selected;`); await writeFile(join(consumer, 'tsconfig.json'), JSON.stringify({compilerOptions:{target:'ES2024',module:'NodeNext',moduleResolution:'NodeNext',strict:true,noEmit:true,lib:['ES2024'],types:[]},include:['contract.ts']})); await execute(process.execPath, [resolve(root, 'node_modules/typescript/bin/tsc'), '-p', 'tsconfig.json'], {cwd: consumer}); const probe = await execute(process.execPath, ['--input-type=module', '-e', `import {navigationState} from '@vypdev/ha-ui/contracts'; +import {fieldValueDetail} from '@vypdev/ha-ui/field-contracts'; +if(!Object.isFrozen(fieldValueDetail({value:'Kitchen',reason:'input'})))throw Error('Expected immutable DOM-free request contract'); if(typeof document!=='undefined'||typeof customElements!=='undefined') throw Error('Expected a server-only consumer'); const value=navigationState({kind:'navigation',label:'Sections',selected:'tasks',items:[{id:'tasks',label:'Tareas 🧹',href:'#tasks'}]}); if(value.selected!=='tasks'||!Object.isFrozen(value.items))throw Error('Packed contract did not preserve controlled state'); @@ -177,7 +180,7 @@ document.querySelector('main')!.append(form);`); await writeFile(resolve(root, 'test-results/packed-contract.json'), JSON.stringify({scope:'Core data, navigation and one text field; field framework adapters, broader controls and application adoption remain required',sha256:digest(first),bytes:first.byteLength,reproducible:true,cleanOfflineInstall:true,frozenRuntimeLock:true,isolatedMetadataCache:true,negativeWithoutLockRejected:true,compiledWithoutDomTypes:true,serverImport:true,packedBrowser},null,2)+'\n'); await browserPage.close();await server.close();server=undefined; const frameworks=await exerciseFrameworks(root,scratch,browser); - await writeFile(resolve(root,'test-results/packed-frameworks.json'),JSON.stringify({scope:'Navigation adapters only; forms, framework SSR and application adoption remain required',frameworks},null,2)+'\n'); + await writeFile(resolve(root,'test-results/packed-frameworks.json'),JSON.stringify({scope:'Navigation and one controlled text field with four framework form bridges; other controls, framework SSR and application adoption remain required',frameworks},null,2)+'\n'); await writeFile(resolve(root,'test-results/packed-cohort.json'),JSON.stringify({scope:'Unpublished navigation and core text-field preview; no registry publication or full capability acceptance',cohort,reproducible:true,sourceLockSha256:digest(await readFile(resolve(root,'pnpm-lock.yaml')))},null,2)+'\n'); passed = true; } finally { diff --git a/tests/contracts/field.test.ts b/tests/contracts/field.test.ts index bdc782e..30a5380 100644 --- a/tests/contracts/field.test.ts +++ b/tests/contracts/field.test.ts @@ -1,5 +1,5 @@ import {expect,test} from 'vitest'; -import {FieldInputError,textFieldState,textFieldValue,textFieldName,textFieldValidity} from '../../packages/core/src/field-contracts.js'; +import {FieldInputError,textFieldState,textFieldValue,textFieldName,textFieldValidity,fieldValueDetail} from '../../packages/core/src/field-contracts.js'; const state=(patch:Record={})=>({label:'Task title',...patch}); function reject(action:()=>unknown){expect(action).toThrow(FieldInputError);try{action();}catch(error){expect((error as FieldInputError).code).toBe('invalid_field');expect((error as Error).message).toBe('invalid_field');}} test('field contracts import without DOM and return immutable independent configuration',()=>{ @@ -47,3 +47,8 @@ test('validity is immutable and distinguishes optional empty, required, short, l test('public validity inputs keep the same bounded validation contract',()=>{ reject(()=>textFieldValidity(null,state()));reject(()=>textFieldValidity('ok',null));reject(()=>textFieldValidity('ok',state(),'\u0000')); }); + +test.each(['input','reset','restore','autocomplete'])('field request %s is validated and independently frozen',reason=>{ + const input={value:'Kitchen',reason},detail=fieldValueDetail(input);input.value='Changed';expect(detail).toEqual({value:'Kitchen',reason});expect(Object.isFrozen(detail)).toBe(true); +}); +test.each([null,undefined,[],false,'event',{}, {value:'Kitchen'}, {reason:'input'}, {value:'Kitchen',reason:'other'}, {value:42,reason:'input'}, {value:'x',reason:'input',extra:true},Object.assign(Object.create({value:'Inherited'}),{reason:'input',other:true}),Object.assign(Object.create({reason:'input'}),{value:'Kitchen',other:true})])('rejects malformed field intent detail before model use',detail=>reject(()=>fieldValueDetail(detail))); diff --git a/tsconfig.json b/tsconfig.json index 9021979..1347c44 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,6 +26,12 @@ ], "@vypdev/ha-ui/tabs": [ "./packages/core/src/components/tabs.ts" + ], + "@vypdev/ha-ui/field-contracts": [ + "./packages/core/src/field-contracts.ts" + ], + "@vypdev/ha-ui/field": [ + "./packages/core/src/components/field.ts" ] } }, diff --git a/vitest.config.ts b/vitest.config.ts index 3c0cf0b..483c877 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,11 +1,13 @@ +import {fileURLToPath} from 'node:url'; import {defineConfig} from 'vitest/config'; import {playwright} from '@vitest/browser-playwright'; import {svelte} from '@sveltejs/vite-plugin-svelte'; import {completeCoverage} from './tooling/coverage.mjs'; +const contractSources={'@vypdev/ha-ui/field-contracts':fileURLToPath(new URL('./packages/core/src/field-contracts.ts',import.meta.url))}; export default defineConfig({test: { projects: [ - {test: {name:'node', include:['tests/**/*.test.{ts,mjs}'], exclude:['tests/browser/**','tests/coverage-evidence.test.mjs','tests/consumers/packed.test.ts']}}, - {plugins:[svelte({configFile:'packages/svelte/svelte.config.js'})],test: {name:'browser', include:['tests/browser/**/*.test.{ts,mjs}'], browser:{enabled:true,provider:playwright(),headless:true,instances:[{browser:'chromium'}]}}}, + {resolve:{alias:contractSources},test: {name:'node', include:['tests/**/*.test.{ts,mjs}'], exclude:['tests/browser/**','tests/coverage-evidence.test.mjs','tests/consumers/packed.test.ts']}}, + {resolve:{alias:contractSources},plugins:[svelte({configFile:'packages/svelte/svelte.config.js'})],test: {name:'browser', include:['tests/browser/**/*.test.{ts,mjs}'], browser:{enabled:true,provider:playwright(),headless:true,instances:[{browser:'chromium'}]}}}, ], coverage: completeCoverage(['packages/*/src', 'tooling'], 'coverage'), }}); From abd53f67fc2a8e5df5bea2464eff46e39b2a2ed6 Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Wed, 4 Nov 2026 08:12:58 +0100 Subject: [PATCH 12/55] test: acknowledge captured pre-render restoration requests --- docs/evidence/shared-ui-foundation.json | 7 +++++++ tests/browser/field.test.ts | 7 +++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/evidence/shared-ui-foundation.json b/docs/evidence/shared-ui-foundation.json index 7700374..a25efe6 100644 --- a/docs/evidence/shared-ui-foundation.json +++ b/docs/evidence/shared-ui-foundation.json @@ -410,5 +410,12 @@ "restoration": "Every previous compiled distribution restored byte-for-byte", "reason": "All five distributions absent: strict source/consumer/Svelte checks and the full source browser campaign pass. Vitest resolves the current DOM-independent field policy source explicitly; the installed-package fixtures have no source alias.", "restoredFiles": 55 + }, + "restoreReviewRegression": { + "finding": "https://github.com/vypdev/ha-ui/pull/10#discussion_r4464672339", + "problem": "The deferred pre-render test assigned a literal expected value without proving restoration intent was observed.", + "correction": "Capture the restore request before connection, assert its immutable detail and unchanged baseline, then acknowledge its captured value after render; assert no second request. The synchronous acknowledgment case remains.", + "validation": "352 source tests pass; coverage remains 100% per authored runtime file and globally. Runtime source/cohort bytes are unchanged.", + "manualResolution": false } } diff --git a/tests/browser/field.test.ts b/tests/browser/field.test.ts index feb601c..e5ca95a 100644 --- a/tests/browser/field.test.ts +++ b/tests/browser/field.test.ts @@ -150,9 +150,12 @@ test('retains values across disconnect/reparenting and uses live HA theme tokens expect(getComputedStyle(input(field)).color).toBe('rgb(222, 223, 224)');expect(getComputedStyle(input(field)).backgroundColor).toBe('rgb(30, 31, 32)');expect(getComputedStyle(input(field)).minHeight).toBe('56px');expect(input(field).labels![0].textContent).toBe('Nombre de la tarea');expect(input(field).value).toBe('Saved'); }); -test('supports restoration and default changes before the first render without requiring a selection range',async()=>{ +test('delivers pre-render restoration for deferred or synchronous consumer acknowledgment',async()=>{ registerField();const field=document.createElement('vyp-ha-field');field.focus();field.blur();field.defaultValue='Baseline';field.defaultValue='Baseline'; - field.formStateRestoreCallback('Deferred','restore');document.body.append(field);await field.updateComplete;field.value='Deferred';expect(input(field).value).toBe('Deferred'); + const requests:FieldValueEvent[]=[];field.addEventListener('ha-value-request',event=>requests.push(event)); + field.formStateRestoreCallback('Deferred','restore');expect(requests).toHaveLength(1);expect(requests[0].detail).toEqual({value:'Deferred',reason:'restore'});expect(field.value).toBe('Baseline'); + document.body.append(field);await field.updateComplete;expect(input(field).value).toBe('Baseline'); + field.value=requests[0].detail.value;expect(input(field).value).toBe('Deferred');expect(requests).toHaveLength(1); const early=document.createElement('vyp-ha-field');accept(early);early.formStateRestoreCallback('Before render','restore');document.body.append(early);await early.updateComplete;expect(input(early).value).toBe('Before render'); }); From b753030595c64bd8223b63625c651882eaf8c657 Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Wed, 4 Nov 2026 09:41:04 +0100 Subject: [PATCH 13/55] feat: add a production-backed accessible Storybook catalog --- .github/workflows/ci_check.yml | 5 + .github/workflows/storybook_pages.yml | 52 + .gitignore | 3 + .storybook/main.ts | 8 + .storybook/preview.ts | 16 + README.md | 5 +- docs/compatibility.md | 5 +- docs/component-library.md | 10 +- docs/evidence/shared-ui-foundation.json | 334 ++++- docs/framework-adapters.md | 2 +- docs/provenance.md | 14 + docs/storybook.md | 84 ++ package.json | 18 +- packages/core/src/components/field.ts | 4 +- packages/core/src/components/tabs.ts | 7 +- .../@storybook__web-components@10.6.5.patch | 12 + pnpm-lock.yaml | 1135 ++++++++++++++++- pnpm-workspace.yaml | 2 + specs/CATALOG.md | 12 +- specs/catalog.json | 29 +- specs/shared-ui-framework-delivery.md | 15 + stories/catalog-example.ts | 8 + stories/catalog-frame.ts | 17 + stories/catalog-state.ts | 15 + stories/field.example.ts | 19 + stories/field.stories.ts | 28 + stories/foundation.example.ts | 17 + stories/foundation.stories.ts | 22 + stories/panels.example.ts | 17 + stories/panels.stories.ts | 19 + tests/architecture.test.mjs | 27 +- tests/browser/field.test.ts | 9 + tests/browser/tabs.test.ts | 10 + tests/catalog-config.test.mjs | 8 + tests/consumers/fixtures/angular/client.ts | 2 +- tests/consumers/fixtures/react/client.ts | 2 +- .../consumers/fixtures/svelte/Consumer.svelte | 2 +- tests/consumers/fixtures/vue/client.ts | 2 +- tests/consumers/frameworks.ts | 1 + tests/consumers/packed.test.ts | 6 +- tests/contracts/catalog.test.ts | 17 + tests/coverage-evidence.test.mjs | 4 +- tests/storybook/static.test.ts | 70 + tests/typing/storybook.ts | 12 + tsconfig.consumers.json | 3 +- tsconfig.storybook.json | 18 + vitest.config.ts | 8 +- 47 files changed, 2051 insertions(+), 84 deletions(-) create mode 100644 .github/workflows/storybook_pages.yml create mode 100644 .storybook/main.ts create mode 100644 .storybook/preview.ts create mode 100644 docs/storybook.md create mode 100644 patches/@storybook__web-components@10.6.5.patch create mode 100644 stories/catalog-example.ts create mode 100644 stories/catalog-frame.ts create mode 100644 stories/catalog-state.ts create mode 100644 stories/field.example.ts create mode 100644 stories/field.stories.ts create mode 100644 stories/foundation.example.ts create mode 100644 stories/foundation.stories.ts create mode 100644 stories/panels.example.ts create mode 100644 stories/panels.stories.ts create mode 100644 tests/catalog-config.test.mjs create mode 100644 tests/contracts/catalog.test.ts create mode 100644 tests/storybook/static.test.ts create mode 100644 tests/typing/storybook.ts create mode 100644 tsconfig.storybook.json diff --git a/.github/workflows/ci_check.yml b/.github/workflows/ci_check.yml index 4720e65..f65ddc0 100644 --- a/.github/workflows/ci_check.yml +++ b/.github/workflows/ci_check.yml @@ -6,6 +6,8 @@ on: branches: [develop, master] merge_group: types: [checks_requested] +env: + STORYBOOK_DISABLE_TELEMETRY: "1" permissions: contents: read jobs: @@ -31,6 +33,8 @@ jobs: - run: pnpm test - run: pnpm build - run: pnpm test:package + - run: pnpm storybook:build + - run: pnpm test:storybook # Read the original catalog SDK at the reviewed revision; never rewrite it. - name: Read the original specification SDK uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -53,6 +57,7 @@ jobs: with: name: shared-ui-validation-${{ github.sha }} path: | + build/storybook/ coverage/ packages/*/dist/ test-results/ diff --git a/.github/workflows/storybook_pages.yml b/.github/workflows/storybook_pages.yml new file mode 100644 index 0000000..20ea34f --- /dev/null +++ b/.github/workflows/storybook_pages.yml @@ -0,0 +1,52 @@ +name: Storybook Pages +on: + push: + branches: [master] + workflow_dispatch: +permissions: + contents: read +env: + STORYBOOK_DISABLE_TELEMETRY: "1" +concurrency: + group: storybook-pages + cancel-in-progress: false +jobs: + build: + if: github.ref == 'refs/heads/master' + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version-file: .node-version + package-manager-cache: false + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + with: + run_install: false + cache: false + - run: pnpm install --frozen-lockfile --ignore-scripts + - run: pnpm check + - run: pnpm exec playwright install --with-deps chromium --only-shell + - run: pnpm test + - run: pnpm test:package + - run: pnpm storybook:build + - run: pnpm test:storybook + - uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4 + with: + path: build/storybook + deploy: + needs: build + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Publish the verified static catalog + id: deployment + uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4 diff --git a/.gitignore b/.gitignore index 4097934..f47fa4b 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,6 @@ work/ .vitest/ **/.svelte-kit/ + +build/ +.storybook-cache/ diff --git a/.storybook/main.ts b/.storybook/main.ts new file mode 100644 index 0000000..8d87992 --- /dev/null +++ b/.storybook/main.ts @@ -0,0 +1,8 @@ +import type {StorybookConfig} from '@storybook/web-components-vite'; +const config:StorybookConfig={ + framework:'@storybook/web-components-vite',stories:['../stories/**/*.stories.ts'], + addons:['@storybook/addon-docs','@storybook/addon-a11y','@storybook/addon-vitest'], + core:{disableTelemetry:true,enableCrashReports:false}, + viteFinal:async config=>({...config,base:'./'}), +}; +export default config; diff --git a/.storybook/preview.ts b/.storybook/preview.ts new file mode 100644 index 0000000..16a7dd3 --- /dev/null +++ b/.storybook/preview.ts @@ -0,0 +1,16 @@ +import type {Preview} from '@storybook/web-components-vite'; +import {registerTabs} from '@vypdev/ha-ui/tabs'; +import {registerField} from '@vypdev/ha-ui/field'; +import {catalogFrame} from '../stories/catalog-frame.js'; +const preview:Preview={ + beforeAll:()=>{registerTabs();registerField();},decorators:[catalogFrame], + initialGlobals:{theme:'light',locale:'en',width:'wide',motion:'system'}, + globalTypes:{ + theme:{description:'HA-compatible theme',toolbar:{title:'Theme',icon:'circlehollow',items:[{value:'light',title:'Light'},{value:'dark',title:'Dark'}],dynamicTitle:true}}, + locale:{description:'Example language; English fallback',toolbar:{title:'Language',icon:'globe',items:[{value:'en',title:'English'},{value:'es',title:'Español'}],dynamicTitle:true}}, + width:{description:'Example width',toolbar:{title:'Width',icon:'mobile',items:[{value:'wide',title:'Wide'},{value:'narrow',title:'Narrow'}],dynamicTitle:true}}, + motion:{description:'System preference or stronger reduced motion',toolbar:{title:'Motion',icon:'lightning',items:[{value:'system',title:'System'},{value:'reduced',title:'Reduced'}],dynamicTitle:true}}, + }, + parameters:{layout:'fullscreen',a11y:{test:'error'},docs:{codePanel:true},controls:{expanded:true}}, +}; +export default preview; diff --git a/README.md b/README.md index 7a708c8..f098204 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,9 @@ The current source implements a DOM-independent navigation data contract, one co upper navigation/panel component, a controlled form-associated text field and a pinned, tested build/coverage foundation. React, Vue, Angular and Svelte navigation and text-field form bindings pass real compiled, packed consumers. -Other component families and their form bridges, framework SSR boundaries, Storybook, publication and actual +Other component families and their form bridges, framework SSR boundaries, public Storybook hosting, publication and actual application adoption remain required. +The [shared Storybook](docs/storybook.md) now covers the implemented families. No package is published or ready for application adoption. Use Node 24.20.0 and the package manager pinned in package.json: @@ -19,6 +20,8 @@ pnpm exec playwright install --with-deps chromium --only-shell pnpm test pnpm build pnpm test:package +pnpm storybook:build +pnpm test:storybook ``` The contract validates controlled navigation or panel inputs without reading browser globals, diff --git a/docs/compatibility.md b/docs/compatibility.md index 0b98608..faa301a 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -12,6 +12,8 @@ families and their form bridges, browser support breadth and framework SSR integ | Vite | 8.3.5 | Test transform toolchain | | Lit | 3.3.3 | Internal shared navigation and text-field renderer | | Vitest, browser provider and V8 coverage | 5.1.0 | Node and real Chromium; all four metrics globally and per authored runtime file | +| Storybook / Web Components / docs / a11y / Vitest addon | 10.6.5 | Real catalog with strict declaration-only compatibility patch | +| axe-core | 4.14.0 | All static locale/theme/width combinations | | Playwright | 1.64.1 | Chromium 156.0.8078.4 component and clean packed-consumer tests | The core preview coordinate/version is package: @vypdev/ha-ui at 0.1.0-next.1. It is unpublished; @@ -36,7 +38,8 @@ a DOM-free contract and a compiled browser client, and the original pinned Copil The packed browser fixture exercises real links, panel slots, keyboard, live Spanish labels/theme, and reduced motion. The additional four framework fixtures establish navigation and text-field model/submission/validation/disabled/reset/lifecycle behavior; other control types, browser engines and framework SSR remain unverified. -Complete component-family/framework fixtures, Storybook, publication and actual app adoption remain required. +Complete component-family/framework fixtures, public Storybook hosting, publication and actual app adoption remain required. +The [implemented catalog and static browser gates](storybook.md) cover only the current families. The clean package fixture derives its runtime-only graph and integrity records from the reviewed workspace lock, binds the actual tarball's integrity, and installs offline with a frozen lock. A fresh diff --git a/docs/component-library.md b/docs/component-library.md index 67ab021..9e1da1f 100644 --- a/docs/component-library.md +++ b/docs/component-library.md @@ -2,8 +2,8 @@ The current source implements the DOM-independent navigation contract, one controlled upper navigation/panel renderer and the tested quality/build foundation. The renderer passes direct -Chromium and clean actual-package browser consumers. Four navigation adapters now also pass real compiled package consumers. Other component families, -form bridges, framework SSR, Storybook, publication and consumer migrations remain required. The [primary Draft](../specs/shared-ui-foundation.md), +Chromium and clean actual-package browser consumers. Four navigation and text-field form adapters now also pass real compiled package consumers. Other component families, +other form bridges, framework SSR, public Storybook hosting, publication and consumer migrations remain required. The [primary Draft](../specs/shared-ui-foundation.md), [capability catalog](../specs/CATALOG.md) and [feature issue #7](https://github.com/vypdev/ha-ui/issues/7) hold the proposed contract and pending acceptance gates. @@ -29,9 +29,9 @@ sections such as Overview, Tasks, Calendar, Shopping, Inventory, Family and Sett ## Components and catalog The proposed families are tokens, typography, spacing, upper navigation, buttons, fields, selects, -checkboxes, cards, rows/grids, dialogs and feedback states. The one Storybook will use production -components with EN/ES, light/dark, narrow/wide, keyboard and reduced-motion examples. It will provide -reviewable PR artifacts and its own static GitHub Pages site; no site URL is claimed before delivery. +checkboxes, cards, rows/grids, dialogs and feedback states. The [shared Storybook](storybook.md) uses production +components with EN/ES, light/dark, narrow/wide, keyboard and reduced-motion examples. CI retains +reviewable PR artifacts; public GitHub Pages hosting remains pending. No private HA bundles, parent-frame DOM, storage, credentials or API access belong in this package. ## Compatibility and adoption diff --git a/docs/evidence/shared-ui-foundation.json b/docs/evidence/shared-ui-foundation.json index a25efe6..d5be3da 100644 --- a/docs/evidence/shared-ui-foundation.json +++ b/docs/evidence/shared-ui-foundation.json @@ -1,6 +1,6 @@ { - "recordedAtUTC": "2026-11-04T07:02:08.742289+00:00", - "status": "Partial source increment: shared navigation/panels and one controlled text field, with real React/Vue/Angular/Svelte navigation bindings and text-field form bridges. Other families and full delivery/application acceptance remain required.", + "recordedAtUTC": "2026-11-04T08:40:19.745238+00:00", + "status": "Partial source increment: shared navigation/panels and one controlled text field, four framework form bridges and a real Storybook catalog. Other families and complete delivery/application acceptance remain required.", "issue": 7, "nativePublication": { "run": 41767973796, @@ -17,36 +17,38 @@ "implemented": [ "Shared controlled navigation/panels and a single-line text field; immutable pure contracts, explicit client registration, original typed cancelable events and one visual implementation per family.", "Real React/Vue/Angular/Svelte navigation and single-line text-field bindings. Renderer imports stay type-only; runtime shared imports are DOM-independent validation policies plus each adapter own framework. No alternate visuals.", - "352 passing Node/Chromium contract, architecture, CI and component/binding tests; all 17 authored runtime files are inventoried independently, including Svelte and three zero-executable modules.", + "389 passing Node/Chromium contract, architecture, CI, component/binding and Storybook interaction/a11y tests; all 25 authored runtime modules are inventoried independently, including Svelte and three zero-executable modules. CSF test definitions delegate runtime composition to covered example modules.", "Strict source and consumer-tool types plus Svelte package/test-fixture checks; no library checks skipped. Angular APF and Svelte declarations built by official package tools.", "All five unpublished coordinates independently built/packed twice with identical bytes. Actual installed sources and publication metadata verified.", "Four real clean frozen offline consumers compile installed package declarations and exercise navigation plus controlled text-field editing/cancellation/model writes/native default submitter/validation/disable/reset/EN-ES/themes/teardown/remount in Chromium; no foreign framework is installed.", "Angular actual consumer is AOT compiled and uses the official linker for all partial framework libraries, without a JIT fallback.", "Core text field passes actual form ownership/submission, default submitter, required/length/custom validation, disabled fieldsets, readonly, reset, explicit restoration callbacks, focus/labels, controlled caret, composition-boundary and invalid-input regression tests. Its actual installed package passes compiled Chromium form journeys.", - "Angular text field implements ControlValueAccessor and the shared pure Validator: reactive controls, ngModel, nullable/nonnullable reset, dirty/touched and updateOn change/blur/submit pass. Foreign malformed event payloads are rejected before callbacks; validated snapshots survive observer payload mutation." + "Angular text field implements ControlValueAccessor and the shared pure Validator: reactive controls, ngModel, nullable/nonnullable reset, dirty/touched and updateOn change/blur/submit pass. Foreign malformed event payloads are rejected before callbacks; validated snapshots survive observer payload mutation.", + "Sixteen real production-backed CSF stories and three autodocs pages; static /ha-ui/ matrix covers 32 locale/theme/width combinations, reduced motion, local-only assets, native forms, manual keyboard panels, actual manager controls and toolbar updates.", + "Storybook found and regressed two shared renderer issues: insufficient light error contrast and partially clipped focused navigation destinations. Both reproduce before their common fixes." ], "coverage": { "lines": { - "total": 305, - "covered": 305, + "total": 340, + "covered": 340, "skipped": 0, "pct": 100 }, "statements": { - "total": 559, - "covered": 559, + "total": 607, + "covered": 607, "skipped": 0, "pct": 100 }, "functions": { - "total": 154, - "covered": 154, + "total": 173, + "covered": 173, "skipped": 0, "pct": 100 }, "branches": { - "total": 369, - "covered": 369, + "total": 404, + "covered": 404, "skipped": 0, "pct": 100 }, @@ -58,28 +60,36 @@ } }, "sourceIdentities": { + ".storybook/main.ts": "922a2ae0b23bb08fab4dc00303af285b1f9eb47e78566cc60d4376149b70a11c", + ".storybook/preview.ts": "5a56e8301ab10922cfb8cf9ddc90641c2cd42237c317435420261f5fc2c8f981", "packages/angular/src/index.ts": "b7f1dbce4ca58dbe6be01b53e9dc079593dc38c7ff4c71d0ffcca10de06a4f48", "packages/angular/src/text-field.ts": "fe0e40f32969c5e3d43bc721f1f0474ecc2fca45e33e12415c30af64a23f4ea4", - "packages/core/src/components/field.ts": "11a2d35a5b4e12b6d034363aef720250e0b157e2187ff3f7bd0f7e5bb63d878a", - "packages/core/src/components/tabs.ts": "5e84a250c9758388d10f4b2e9de55be6d18e57754aaf38d1a0deb71262c58e36", "packages/core/src/contracts.ts": "457c418cbfcafe5ebb1f834a1f8c2c19f484bad15ea8df5bc65502eec6e19c91", "packages/core/src/field-contracts.ts": "130e143f3f69dfaa6e0397cf68cd2ec824823b44ff3f4488d92b19180d50b738", "packages/core/src/index.ts": "b4b038b1a44cb1bc09c738153a052ff4e599d6bac8ebd951204cb4c75a304ec1", "packages/core/src/presentation-text.ts": "ad2eb8396a26638ba8d2f2f7cee3656195646aaea077cdc0d3ecd06fc95b3df2", + "packages/core/src/components/field.ts": "24d6c0705759054ff9e0da2f3bd653ee495d82705afb54084553cf6f01387ac3", + "packages/core/src/components/tabs.ts": "53e476dde73ab0307a658985f4f2ec91b2086e69df42155d6fdb67925c6a5bb0", "packages/react/src/index.ts": "de04557dd8d23b9c331776fdc2574476853456160650f371a0bc427d22464bca", "packages/react/src/text-field.ts": "1d9249cb835aa1205bcb634fee2f75af01bc5c7833f9f71391e72de560c67656", + "packages/svelte/src/Field.svelte": "174e62b2fd01af8279ab42ce58ab530a0ee14d370bcdb646139f5a143052b8a0", + "packages/svelte/src/Tabs.svelte": "974845c12ec9d760b6f76a43ae89fd4c1618d7b4d56381d98d16f65abc5b27c4", "packages/svelte/src/index.ts": "6cdd70a671fd4d05232b7646838f43ddce086acc8d4af83409d76f366a0ae4db", "packages/svelte/src/types.ts": "e673d97f2db9564c518bd6d08715093c3db26e9201becb5129cae7a4a99113ab", "packages/vue/src/index.ts": "638fc7f426cb4c02527938d184e16afb4fec8845e86197159316c1968e2d2df8", "packages/vue/src/text-field.ts": "e160d93f62669ffb989e3d8d78fb28b0da287226d8157676d5842b6a7c83f612", - "packages/svelte/src/Field.svelte": "174e62b2fd01af8279ab42ce58ab530a0ee14d370bcdb646139f5a143052b8a0", - "packages/svelte/src/Tabs.svelte": "974845c12ec9d760b6f76a43ae89fd4c1618d7b4d56381d98d16f65abc5b27c4", + "stories/catalog-example.ts": "c5fa904779d81d2f081573051234b2d57f98c74c5c4ca99cbc8717584c2494e8", + "stories/catalog-frame.ts": "49424d90f1d165d29fcf9c404050e01165847066ea07628bf50cb8b4ba9564aa", + "stories/catalog-state.ts": "dcec5fea0ffa6da921c222fdda5e1b0c0a6796f10ae43ce69b7fe8b757c3909e", + "stories/field.example.ts": "2fded8b87675334f8ae2d026a63dc2c233b6d9cf28f5646b2eb13b9cf44af8f1", + "stories/foundation.example.ts": "6fc175e9a44ff0504c46271bd45837f6a4799d9815614a2389b9a238c85ab42b", + "stories/panels.example.ts": "dec84608d463dc340acf874d44f2cae3c7cd188390e23221cd728a4bdbf0a04f", "tooling/coverage.mjs": "ad527a5d04517adb1992ac65a9abfffcabb9a2001f7f652c732dbe35899b56c8" }, "packedContract": { - "scope": "Core data, navigation and one text field; field framework adapters, broader controls and application adoption remain required", - "sha256": "9ea5d40493a494332fd2688f8b9c57540d96c5538eb3d1b55023f0333213d4fd", - "bytes": 30287, + "scope": "Core data, navigation and one text field; four framework form bridges are verified separately; broader controls and application adoption remain required", + "sha256": "107cc28faea98986080b3c422eb888c3402ab86c7a43121c2031fa2c606a1a15", + "bytes": 30511, "reproducible": true, "cleanOfflineInstall": true, "frozenRuntimeLock": true, @@ -110,7 +120,7 @@ "Text-field composition events and restoration callbacks are explicitly exercised; real device IME and automatic browser history restoration remain unverified.", "Exact recorded framework pins pass navigation and one text-field compiled consumer. Full cohort support and broader public peer ranges cannot be inferred from those two families.", "Next.js, Nuxt and SvelteKit explicit server-shell/client fixtures, hydration behavior and other browser engines remain unverified.", - "All six delivery refinements, accepted-candidate promotion/publication recovery, Storybook, publisher authority and live publication remain required.", + "All six delivery refinements, accepted-candidate promotion/publication recovery, public Storybook hosting, publisher authority and live publication remain required.", "Gateway and Domus adoption, installed deployment/recovery and human UX acceptance remain unverified.", "All 64 original baseline cases and six additional delivery cases remain required; no partial increment completes issue #7.", "Fresh hosted CI and complete native Bugbot review are required for the source head." @@ -123,9 +133,9 @@ "after": "All 138 unit/architecture/CI-contract cases pass; every C1 code point rejected in group labels, item labels and links; visible U+00A0/U+00A1 preserved.", "manualResolution": false }, - "lockSha256": "06fb89a58baf3beb3bc713a88055f184379d218aa187f53792cb4130f38f42d6", + "lockSha256": "3daf9264c171fd42c9ac73cecdb1b159a926a931a5da2a2806bb7e6a99f87044", "browserEvidence": { - "componentCases": 53, + "componentCases": 55, "engine": "Chromium 156.0.8078.4", "actualPackageImages": [ "test-results/packed-angular-dark-es.png", @@ -147,7 +157,7 @@ "imagesInspected": true, "focusRegression": "focus-return-red.log reproduces the stale tab-stop; full green run has no skipped tests.", "initialTestAttempt": "Browser cases were authored before the renderer. The first attempted execution could not start because the pinned Chromium binary was absent; no behavioral red result is claimed for that attempt. After runtime provisioning, the first actual run found two fixture/server-boundary issues, recorded in browser-first-execution.log.", - "textFieldCases": 25, + "textFieldCases": 26, "localRetainedImages": [ "work/current-preview/packed-angular-dark-es.png", "work/current-preview/packed-angular-field-dark-es.png", @@ -169,7 +179,7 @@ "fieldRegression": "invalid-edit-red.log reproduced one failure: a malformed synthetic edit left a rejected draft visible. current-coverage.log passes the permanent regression and restores accepted DOM/form state without emitting intent.", "fieldInitialRed": "renderer-red.log rejected the missing renderer module before implementation; this is an API-absence red, not a behavioral assertion result.", "frameworkTextFieldCases": 35, - "frameworkFieldVisualReview": "All eight framework field images are represented by three unique byte groups; each unique valid/error dark Spanish image was inspected. Core geometry and semantics remain shared.", + "frameworkFieldVisualReview": "All eight framework field images are represented by three unique byte groups; each unique valid/error dark Spanish image was inspected. Core geometry and semantics remain shared. The current dark error fixture supplies its public semantic error token; a representative new original image is inspected and all four consumers assert the computed color.", "frameworkFieldRed": "invalid-event-red.log reproduced four failing adapter cases before the pure payload guard. The final source campaign includes those regressions and observer-mutation cases.", "packedFieldInitialAttempts": "packed.log stopped before any browser launch because the local browser path variable was absent. packed-with-runtime.log then exposed a premature React UI-counter assertion; the driver now waits for the observed render state. current-packed.log passes all actual compiled forms; no unsupported behavioral red claim is made for the launch failure." }, @@ -197,7 +207,7 @@ }, { "file": "file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz", - "sha256": "9ea5d40493a494332fd2688f8b9c57540d96c5538eb3d1b55023f0333213d4fd" + "sha256": "107cc28faea98986080b3c422eb888c3402ab86c7a43121c2031fa2c606a1a15" } ], "props": true, @@ -233,7 +243,7 @@ }, { "file": "file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz", - "sha256": "9ea5d40493a494332fd2688f8b9c57540d96c5538eb3d1b55023f0333213d4fd" + "sha256": "107cc28faea98986080b3c422eb888c3402ab86c7a43121c2031fa2c606a1a15" } ], "props": true, @@ -269,7 +279,7 @@ }, { "file": "file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz", - "sha256": "9ea5d40493a494332fd2688f8b9c57540d96c5538eb3d1b55023f0333213d4fd" + "sha256": "107cc28faea98986080b3c422eb888c3402ab86c7a43121c2031fa2c606a1a15" } ], "props": true, @@ -305,7 +315,7 @@ }, { "file": "file:../pack-1/vypdev-ha-ui-0.1.0-next.1.tgz", - "sha256": "9ea5d40493a494332fd2688f8b9c57540d96c5538eb3d1b55023f0333213d4fd" + "sha256": "107cc28faea98986080b3c422eb888c3402ab86c7a43121c2031fa2c606a1a15" } ], "props": true, @@ -337,8 +347,8 @@ { "coordinate": "@vypdev/ha-ui", "version": "0.1.0-next.1", - "sha256": "9ea5d40493a494332fd2688f8b9c57540d96c5538eb3d1b55023f0333213d4fd", - "bytes": 30287 + "sha256": "107cc28faea98986080b3c422eb888c3402ab86c7a43121c2031fa2c606a1a15", + "bytes": 30511 }, { "coordinate": "@vypdev/ha-ui-angular", @@ -366,7 +376,7 @@ } ], "reproducible": true, - "sourceLockSha256": "06fb89a58baf3beb3bc713a88055f184379d218aa187f53792cb4130f38f42d6" + "sourceLockSha256": "3daf9264c171fd42c9ac73cecdb1b159a926a931a5da2a2806bb7e6a99f87044" }, "frameworkPreviewPins": { "react": "19.3.1", @@ -392,7 +402,7 @@ "otherBrowserEngines": false, "applicationAdoption": false, "publication": false, - "storybook": false, + "storybook": "Implemented catalog/static subpath verified for current families; public Pages deployment and complete-family/human acceptance pending", "coreNativeTextField": true }, "coldSourceValidation": { @@ -404,11 +414,11 @@ "angular", "svelte" ], - "types": "All strict source, Node-tool and both Svelte checks passed", - "unitAndBrowserTests": 352, + "types": "All strict source, Node-tool, Storybook and both Svelte checks passed", + "unitAndBrowserTests": 389, "coverage": "100% statements/branches/functions/lines per authored file and globally", "restoration": "Every previous compiled distribution restored byte-for-byte", - "reason": "All five distributions absent: strict source/consumer/Svelte checks and the full source browser campaign pass. Vitest resolves the current DOM-independent field policy source explicitly; the installed-package fixtures have no source alias.", + "reason": "All five distributions absent: strict source/consumer/Storybook/Svelte checks and the full source browser campaign pass. Installed-package fixtures have no source alias.", "restoredFiles": 55 }, "restoreReviewRegression": { @@ -417,5 +427,259 @@ "correction": "Capture the restore request before connection, assert its immutable detail and unchanged baseline, then acknowledge its captured value after render; assert no second request. The synchronous acknowledgment case remains.", "validation": "352 source tests pass; coverage remains 100% per authored runtime file and globally. Runtime source/cohort bytes are unchanged.", "manualResolution": false + }, + "storybook": { + "version": "10.6.5", + "a11y": "4.14.0", + "runtimeCoverageInventory": 25, + "testDefinitionsExcludedByVitest": "Three CSF test files; all runtime render composition lives in inventoried example modules", + "staticBrowser": { + "subpath": "/ha-ui/", + "storyCount": 16, + "docsCount": 3, + "matrix": [ + { + "story": "navigation-household-sections--long-labels", + "locale": "en", + "theme": "light", + "width": 360, + "a11yViolations": 0 + }, + { + "story": "tabs-local-panels--unselected", + "locale": "en", + "theme": "light", + "width": 360, + "a11yViolations": 0 + }, + { + "story": "forms-text-field--read-only", + "locale": "en", + "theme": "light", + "width": 360, + "a11yViolations": 0 + }, + { + "story": "forms-text-field--error", + "locale": "en", + "theme": "light", + "width": 360, + "a11yViolations": 0 + }, + { + "story": "navigation-household-sections--long-labels", + "locale": "en", + "theme": "light", + "width": 960, + "a11yViolations": 0 + }, + { + "story": "tabs-local-panels--unselected", + "locale": "en", + "theme": "light", + "width": 960, + "a11yViolations": 0 + }, + { + "story": "forms-text-field--read-only", + "locale": "en", + "theme": "light", + "width": 960, + "a11yViolations": 0 + }, + { + "story": "forms-text-field--error", + "locale": "en", + "theme": "light", + "width": 960, + "a11yViolations": 0 + }, + { + "story": "navigation-household-sections--long-labels", + "locale": "en", + "theme": "dark", + "width": 360, + "a11yViolations": 0 + }, + { + "story": "tabs-local-panels--unselected", + "locale": "en", + "theme": "dark", + "width": 360, + "a11yViolations": 0 + }, + { + "story": "forms-text-field--read-only", + "locale": "en", + "theme": "dark", + "width": 360, + "a11yViolations": 0 + }, + { + "story": "forms-text-field--error", + "locale": "en", + "theme": "dark", + "width": 360, + "a11yViolations": 0 + }, + { + "story": "navigation-household-sections--long-labels", + "locale": "en", + "theme": "dark", + "width": 960, + "a11yViolations": 0 + }, + { + "story": "tabs-local-panels--unselected", + "locale": "en", + "theme": "dark", + "width": 960, + "a11yViolations": 0 + }, + { + "story": "forms-text-field--read-only", + "locale": "en", + "theme": "dark", + "width": 960, + "a11yViolations": 0 + }, + { + "story": "forms-text-field--error", + "locale": "en", + "theme": "dark", + "width": 960, + "a11yViolations": 0 + }, + { + "story": "navigation-household-sections--long-labels", + "locale": "es", + "theme": "light", + "width": 360, + "a11yViolations": 0 + }, + { + "story": "tabs-local-panels--unselected", + "locale": "es", + "theme": "light", + "width": 360, + "a11yViolations": 0 + }, + { + "story": "forms-text-field--read-only", + "locale": "es", + "theme": "light", + "width": 360, + "a11yViolations": 0 + }, + { + "story": "forms-text-field--error", + "locale": "es", + "theme": "light", + "width": 360, + "a11yViolations": 0 + }, + { + "story": "navigation-household-sections--long-labels", + "locale": "es", + "theme": "light", + "width": 960, + "a11yViolations": 0 + }, + { + "story": "tabs-local-panels--unselected", + "locale": "es", + "theme": "light", + "width": 960, + "a11yViolations": 0 + }, + { + "story": "forms-text-field--read-only", + "locale": "es", + "theme": "light", + "width": 960, + "a11yViolations": 0 + }, + { + "story": "forms-text-field--error", + "locale": "es", + "theme": "light", + "width": 960, + "a11yViolations": 0 + }, + { + "story": "navigation-household-sections--long-labels", + "locale": "es", + "theme": "dark", + "width": 360, + "a11yViolations": 0 + }, + { + "story": "tabs-local-panels--unselected", + "locale": "es", + "theme": "dark", + "width": 360, + "a11yViolations": 0 + }, + { + "story": "forms-text-field--read-only", + "locale": "es", + "theme": "dark", + "width": 360, + "a11yViolations": 0 + }, + { + "story": "forms-text-field--error", + "locale": "es", + "theme": "dark", + "width": 360, + "a11yViolations": 0 + }, + { + "story": "navigation-household-sections--long-labels", + "locale": "es", + "theme": "dark", + "width": 960, + "a11yViolations": 0 + }, + { + "story": "tabs-local-panels--unselected", + "locale": "es", + "theme": "dark", + "width": 960, + "a11yViolations": 0 + }, + { + "story": "forms-text-field--read-only", + "locale": "es", + "theme": "dark", + "width": 960, + "a11yViolations": 0 + }, + { + "story": "forms-text-field--error", + "locale": "es", + "theme": "dark", + "width": 960, + "a11yViolations": 0 + } + ], + "keyboard": true, + "nativeForm": true, + "managerDocs": true, + "managerControls": true, + "external": [], + "badAssets": [], + "errors": [], + "visualReview": "Original manager/autodocs, changed controls, narrow EN/ES/light/dark field representatives and invalid/error states inspected. Screenshots are fixtures, not owner HA acceptance." + }, + "declarationPatch": { + "changedFile": "dist/index.d.ts", + "otherOriginalFilesByteIdentical": 36, + "javascriptChanged": false, + "skipLibCheck": false + }, + "publicPagesDeployed": false, + "ownerHAInstallation": false, + "humanAcceptance": false } } diff --git a/docs/framework-adapters.md b/docs/framework-adapters.md index dc2f675..cf0d5af 100644 --- a/docs/framework-adapters.md +++ b/docs/framework-adapters.md @@ -2,7 +2,7 @@ This unpublished preview has shared navigation and one controlled text field, with four real framework bindings and text-field form bridges. Other controls and their bridges, framework -SSR/client fixtures, broader browser support, Storybook, publication and app adoption remain required. +SSR/client fixtures, broader browser support, public Storybook hosting, publication and app adoption remain required. ## Client registration and ownership diff --git a/docs/provenance.md b/docs/provenance.md index d15396b..ebd7ff5 100644 --- a/docs/provenance.md +++ b/docs/provenance.md @@ -41,3 +41,17 @@ reuse the pure core event/value/validation policies and the existing core render or alternative controller. Angular Forms 22.2.4 is an exact framework peer; its runtime source is not copied. Source-browser policy aliases are test-only; actual frozen package consumers resolve installed exports and verify authored source bytes against the repository. + +The Storybook catalog and fixtures are authored here and import production package entrypoints. +Storybook 10.6.5 and its official addons declare MIT licenses; their distributed manager assets +remain dependency assets rather than copied package code. The pinned pnpm patch changes only one +line of `@storybook/web-components/dist/index.d.ts` to repair its inherited constraint. The upstream +package is [Storybook Web Components 10.6.5](https://www.npmjs.com/package/@storybook/web-components/v/10.6.5). +The original tarball and all regular members were compared with the installed package: 36 files, +including every JavaScript file, stayed byte-identical. Strict positive/negative declaration tests +remain enabled. See the [catalog guide](storybook.md#pinned-declaration-compatibility-patch). + +The catalog contrast check found the prior light error fallback below AA for 12px text. The authored +renderer and fictional palette now use `#b3261e`; public HA/custom theme overrides still apply. +The authored focus handler fully reveals a partially visible focused navigation/tab destination +with nearest, instant scrolling. Both changes are tested once in the common implementation. diff --git a/docs/storybook.md b/docs/storybook.md new file mode 100644 index 0000000..9c548a6 --- /dev/null +++ b/docs/storybook.md @@ -0,0 +1,84 @@ +# Shared component catalog + +The current catalog displays the implemented upper navigation, local panels and single-line text +field using the real package entrypoints. Sixteen CSF stories have automatic documentation, +controls, interaction tests and accessibility checks. This is an incremental preview; other +families, the full framework/SSR/browser matrix and application adoption are still required. + +```bash +pnpm install --frozen-lockfile --ignore-scripts +pnpm exec playwright install --with-deps chromium --only-shell +pnpm storybook:build +pnpm test:storybook +pnpm storybook:preview +``` + +The last command serves the complete static catalog at `http://localhost:6006/ha-ui/`. +`pnpm storybook` runs the editable development catalog. Neither mode connects to Home Assistant, +uses household records or requires a token. All household content is fictional. Fonts and assets +are local to the static build; the browser gate rejects external requests and failed asset loads. + +## What to inspect + +| Catalog section | Production API | Significant states | +|---|---|---| +| Navigation / Household sections | `@vypdev/ha-ui/tabs`, `kind: navigation` | Current link, disabled, empty, long labels, narrow overflow | +| Tabs / Local panels | The same renderer, `kind: panels` | Named slots, manual keyboard activation, disabled and unselected | +| Forms / Text field | `@vypdev/ha-ui/field` | Controlled draft, native submit/reset, required, readonly, disabled and error | + +The toolbar chooses English/Spanish, light/dark, narrow/wide and reduced/system motion. Unsupported +locale values fall back to English. System reduced-motion preferences are always respected. The +controls table supplies example inputs; each example owns its temporary application state and +acknowledges typed component intent. Controls and toolbar updates recreate the example with the +chosen inputs. No preference, form value or selected route is persisted. + +`ha-selection-request` is cancelable and carries immutable `{kind, id}` detail. The example +intercepts section links and supplies the next validated state. Local panels project real named +slots matching item IDs. `ha-value-request` carries immutable `{value, reason}` detail; the field +example validates it, updates its own draft and submits actual `FormData`. The fixture's native +submit/reset buttons are example scaffolding, not the pending shared button family. + +The public state properties, validation rules, events, slots and theme tokens are described in the +[component API](component-library.md#first-component-contract-controlled-upper-navigation). +[Framework usage](framework-adapters.md) covers React, Vue, Angular Forms and Svelte. Those bindings +use these same renderers; the catalog does not duplicate their markup or claim SSR support. + +## Verification and coverage + +`pnpm test` executes every CSF story in Chromium through the official Vitest addon. Accessibility +violations fail the test. Runtime example composition, locale policy, palettes, decorator and +preview setup join all package/tool source in the independent inventory and require 100% of all +four supported coverage metrics per file and globally. CSF files are test definitions, like browser +test files: Vitest excludes collected test modules from coverage. Their render functions are +imported from independently measured `*.example.ts` modules; architecture checks prohibit inline +renderers or duplicate control markup in those test definitions. No runtime example is excluded. + +The static gate builds the manager, autodocs and previews, then serves only the repository base +`/ha-ui/`. It checks 32 locale/theme/width combinations with axe, real focus/overflow and manual +panel activation, native form editing/validation/submit/reset, controls-driven value changes and +all four manager toolbar settings. It records original screenshots and `test-results/storybook/evidence.json`. +The contrast and partial-focus regressions fail against the earlier renderer and pass after the +shared fixes. No additional browser engine, owner HA installation or human acceptance is inferred. + +## Delivery + +Every PR update runs `CI Check`, including strict types, full coverage, compiled package consumers, +static catalog build and the static browser gate. Its retained artifact includes `build/storybook/`, +coverage and browser evidence. This makes a candidate reviewable before merge. The separate Pages +workflow builds and rechecks trusted `master` before upload; only its deployment job receives Pages +write and OIDC permission. It never executes PR code with deployment permissions. + +The intended public destination is the repository Pages subpath `/ha-ui/`. Public hosting is pending +until the complete feature can merge and Pages is enabled for Actions. The existence of a workflow +or a passing local build does not establish a deployed site. Do not merge an incomplete feature to +publish a preview, and do not treat Storybook delivery as package publication or app adoption. + +## Pinned declaration compatibility patch + +Storybook 10.6.5 is exact in the frozen lock. Its Web Components declaration has an inconsistent +inherited generic constraint and an unused inline `@ts-expect-error`, which fail TypeScript 6 strict +checking. The one-line pnpm patch intersects the inherited meta input with the base constraint and +removes that unused directive. It changes only `dist/index.d.ts`: all original JavaScript files stay +byte-identical. Library checking and strictness remain enabled; positive and negative CSF type +fixtures compile. No Copilot resource is modified. Review and remove the patch when a future pinned +Storybook release passes those same checks without it. See [provenance](provenance.md). diff --git a/package.json b/package.json index 4605d1a..d7d56e6 100644 --- a/package.json +++ b/package.json @@ -7,10 +7,14 @@ "node": ">=24.20.0 <25" }, "scripts": { - "check": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.consumers.json && svelte-check --tsconfig tsconfig.svelte-source.json && svelte-check --tsconfig tsconfig.svelte-tests.json", + "check": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.consumers.json && svelte-check --tsconfig tsconfig.svelte-source.json && svelte-check --tsconfig tsconfig.svelte-tests.json && tsc --noEmit -p tsconfig.storybook.json", "test": "vitest run --coverage && node --test tests/coverage-evidence.test.mjs", "build": "pnpm -r --sort run build", - "test:package": "node --test tests/consumers/packed.test.ts" + "test:package": "node --test tests/consumers/packed.test.ts", + "storybook": "storybook dev --port 6006 --no-open", + "storybook:build": "pnpm build && storybook build --output-dir build/storybook", + "test:storybook": "node --test tests/storybook/static.test.ts", + "storybook:preview": "vite preview --outDir build/storybook --base /ha-ui/ --port 6006 --strictPort" }, "devDependencies": { "@angular/common": "22.2.4", @@ -20,19 +24,29 @@ "@angular/forms": "22.2.4", "@angular/platform-browser": "22.2.4", "@babel/core": "8.0.1", + "@storybook/addon-a11y": "10.6.5", + "@storybook/addon-docs": "10.6.5", + "@storybook/addon-vitest": "10.6.5", + "@storybook/web-components-vite": "10.6.5", "@sveltejs/package": "3.0.2", "@sveltejs/vite-plugin-svelte": "7.3.1", + "@types/chai": "5.2.3", "@types/convert-source-map": "2.0.3", "@types/node": "24.19.2", "@types/react": "19.3.0", "@types/react-dom": "19.3.0", + "@vitest/browser": "5.1.0", "@vitest/browser-playwright": "5.1.0", "@vitest/coverage-v8": "5.1.0", + "@vypdev/ha-ui": "workspace:*", + "axe-core": "4.14.0", + "lit": "3.3.3", "ng-packagr": "22.2.5", "playwright": "1.64.1", "react": "19.3.1", "react-dom": "19.3.1", "rxjs": "7.8.2", + "storybook": "10.6.5", "svelte": "5.57.3", "svelte-check": "4.7.8", "tslib": "2.8.1", diff --git a/packages/core/src/components/field.ts b/packages/core/src/components/field.ts index a0f21ec..f1db2ef 100644 --- a/packages/core/src/components/field.ts +++ b/packages/core/src/components/field.ts @@ -21,9 +21,9 @@ export class HaTextField extends LitElement { input {box-sizing:border-box;width:100%;min-width:0;min-height:56px;border:1px solid var(--vyp-ha-border,var(--divider-color,#e0e0e0)); border-radius:var(--vyp-ha-field-radius,4px);padding:16px;color:inherit;background:var(--vyp-ha-surface,var(--card-background-color,#fff));font:inherit;} input:focus-visible {outline:2px solid var(--vyp-ha-primary,var(--primary-color,#0075b5));outline-offset:1px;} - input:disabled {opacity:.5;cursor:not-allowed;} input[aria-invalid=true] {border-color:var(--vyp-ha-danger,var(--error-color,#db4437));} + input:disabled {opacity:.5;cursor:not-allowed;} input[aria-invalid=true] {border-color:var(--vyp-ha-danger,var(--error-color,#b3261e));} .help,.error {font-size:12px;line-height:1.35;} .help {color:var(--vyp-ha-text-secondary,var(--secondary-text-color,#595959));} - .error {color:var(--vyp-ha-danger,var(--error-color,#db4437));} + .error {color:var(--vyp-ha-danger,var(--error-color,#b3261e));} `; #internals=this.attachInternals(); #state=textFieldState({label:'Value'}); diff --git a/packages/core/src/components/tabs.ts b/packages/core/src/components/tabs.ts index a858d7d..d6a5d40 100644 --- a/packages/core/src/components/tabs.ts +++ b/packages/core/src/components/tabs.ts @@ -56,6 +56,9 @@ export class HaTabs extends LitElement { event.preventDefault(); this.#focused=enabled[destination].id; this.requestUpdate(); this.renderRoot.querySelector('#tab-'+this.#focused)!.focus(); } + #reveal(): void { + this.renderRoot.querySelector(':focus')?.scrollIntoView({block:'nearest',inline:'nearest',behavior:'instant'}); + } #leave(event: FocusEvent): void { const bar=event.currentTarget as HTMLElement; if (!(event.relatedTarget instanceof Node) || !bar.contains(event.relatedTarget)) { @@ -64,14 +67,14 @@ export class HaTabs extends LitElement { } protected render() { const snapshot=this.state; - if (snapshot.kind === 'navigation') return html`