From 36d4b1c77a9d5ae1ba3dcc1f3dc8d964324745eb Mon Sep 17 00:00:00 2001 From: meh Date: Sun, 13 Sep 2026 01:41:59 +0700 Subject: [PATCH 1/2] fix(components): complete stable control contracts --- docs/api-contract.md | 4 +++ docs/ui-usage.md | 32 +++++++++++++++++-- .../ConnectionSettings.layout.tsx | 16 ++++++++++ .../ImmersiveLanding.layout.tsx | 17 ++++++++-- .../ImmersiveLandingArrows.layout.tsx | 5 ++- .../ImmersiveLandingNavigation.layout.tsx | 6 ++++ .../components/CookieConsent.tsx | 23 ++++++++++--- .../components/FirefoxPWABanner.tsx | 19 +++++++++-- .../components/PWAInstallPrompt.tsx | 12 +++++-- src/components/immersive-landing/types.ts | 6 ++++ .../LanguageSwitcher.layout.tsx | 2 ++ .../live-chat/LiveChatPanel.layout.tsx | 4 +++ src/components/slider/Slider.css | 2 +- tests/ps-qa-headless/connection-settings.ron | 28 ++++++++++++++++ tests/ps-qa-headless/immersive-landing.ron | 10 ++++++ tests/ps-qa/connection-settings.ron | 28 ++++++++++++++++ tests/ps-qa/immersive-landing.ron | 10 ++++++ tests/ps-qa/slider.ron | 10 ++++++ tests/qa-harness/components.ts | 28 ++++++++++++++++ tests/qa-harness/generate-checks.ts | 2 +- tests/qa-harness/mount.tsx | 21 ++++++++++++ 21 files changed, 269 insertions(+), 16 deletions(-) diff --git a/docs/api-contract.md b/docs/api-contract.md index aa04e891..b6b25f5e 100644 --- a/docs/api-contract.md +++ b/docs/api-contract.md @@ -574,12 +574,14 @@ onSaveFailed?: (error: unknown) => void onSaved?: () => void showAppPublicId?: boolean store: ConnectionSettingsStore +validateAppPublicId?: (id: string) => string | undefined ``` ### CookieConsent ```ts analytics: boolean +id?: string marketing: boolean onConsentChange?: (payload: { type: ConsentType; analytics: boolean; marketing: boolean; }) => void storageKeys?: CookieConsentStorageKeys @@ -957,6 +959,7 @@ _No props beyond HTML attributes and `UIBaseProps`._ ```ts extensionUrl?: string icon?: string | JSX.Element +id?: string onDismiss?: () => void onInstall?: () => void showDelayMs?: number @@ -1507,6 +1510,7 @@ value?: number ```ts appIcon?: string appName?: string +id?: string onDismiss?: () => void onInstall?: () => void storageKey?: string diff --git a/docs/ui-usage.md b/docs/ui-usage.md index 03baeff3..e32f08c9 100644 --- a/docs/ui-usage.md +++ b/docs/ui-usage.md @@ -164,7 +164,19 @@ Components require `solid-layouts >=0.2.4` so caller styles reach their root ele `showDelayMs` when the surrounding onboarding flow needs a different delay; the same option is available as `ImmersiveLanding.firefoxPWAConfig.showDelayMs`. The banner still applies its Firefox, standalone-mode, and dismissal checks - before starting that delay. + before starting that delay. Give it an `id` to derive stable IDs for its + banner and actions; `ImmersiveLanding` derives this base from its own `id` + unless `firefoxPWAConfig.id` overrides it. +- Give `PWAInstallPrompt` an `id` to derive stable IDs for its dialog and + actions. `ImmersiveLanding` derives this base from its own `id` unless + `pwaConfig.id` overrides it. +- `ImmersiveLanding.id` is placed on the component root. Its page viewport, + arrows, navigation, optional PWA prompts, and cookie controls remain inside + that ownership subtree and derive their control IDs from the same base. +- Give `CookieConsent` an `id` when its controls need stable authored IDs. Its + banner, dialog, preference inputs, and actions derive unique IDs from that + base. `ImmersiveLanding` forwards `cookieConfig.id` and otherwise derives the + cookie family from the landing's own `id`. - `Collapsible.Content` retains closed content by default. Set `keepMounted={false}` to mount it only while expanded; the check is reactive, so it mounts and unmounts as the state changes. - `Popover` accepts `anchorRect` as a rectangle or rectangle accessor when content must be positioned without a trigger element. - Compound components: `Dialog.Trigger`, `Tabs.List`, `Select.Option`, etc. (`Object.assign` statics; also exported flat: `AccordionRoot`, `AlertTitle`, …). Parts are styleable/testable via `data-slot="..."` and state attrs (`data-open`, `data-selected`, `data-invalid`). @@ -730,13 +742,29 @@ export const connection = createConnectionSettings({ }); + /^[0-9A-Za-z]{16}$/.test(id) + ? undefined + : "Application ID must be 16 letters or numbers" + } /> ``` +When `showAppPublicId`, `labels.appPublicId`, and `validateAppPublicId` are all +present, Save validates the visible application ID before changing persisted +settings or running `onApply`. Return `undefined` to accept it or a message to +show the failure and call `onSaveFailed`. + Read `connection.urls.api` from the transport. It resolves overrides and never returns an empty string. diff --git a/src/components/connection-settings/ConnectionSettings.layout.tsx b/src/components/connection-settings/ConnectionSettings.layout.tsx index 3ff36408..0b6e7caa 100644 --- a/src/components/connection-settings/ConnectionSettings.layout.tsx +++ b/src/components/connection-settings/ConnectionSettings.layout.tsx @@ -52,6 +52,8 @@ export type ConnectionSettingsProps = Omit< labels: ConnectionSettingsLabels; /** Omit to hide the app id field entirely. */ showAppPublicId?: boolean; + /** Refuse an application id before it is persisted or applied. */ + validateAppPublicId?: (id: string) => string | undefined; /** * Rendered inside the revealed region, after the endpoint fields. * @@ -86,6 +88,7 @@ export const ConnectionSettingsLayout: Layout< "endpoints", "labels", "showAppPublicId", + "validateAppPublicId", "onSaved", "onSaveFailed", "onResetDone", @@ -196,6 +199,19 @@ export const ConnectionSettingsLayout: Layout< } } + if ( + props.showAppPublicId && + props.labels.appPublicId && + props.validateAppPublicId + ) { + const problem = props.validateAppPublicId(appIdValue()); + if (problem) { + setFailure(problem); + props.onSaveFailed?.(new Error(problem)); + return; + } + } + /* * Per endpoint, and only for the endpoints this panel shows. * diff --git a/src/components/immersive-landing/ImmersiveLanding.layout.tsx b/src/components/immersive-landing/ImmersiveLanding.layout.tsx index 090c6b03..96b9830e 100644 --- a/src/components/immersive-landing/ImmersiveLanding.layout.tsx +++ b/src/components/immersive-landing/ImmersiveLanding.layout.tsx @@ -99,8 +99,6 @@ const ImmersiveLanding: Layout =
{renderChildren()}
- -
{renderOverlay()} @@ -117,6 +115,7 @@ const ImmersiveLanding: Layout = {/* Desktop side arrows */} {showArrowNav() && ( = {/* Bottom navigation (dots, counter, mobile arrows) */} {showNav() && ( = = = +
); }; diff --git a/src/components/immersive-landing/ImmersiveLandingArrows.layout.tsx b/src/components/immersive-landing/ImmersiveLandingArrows.layout.tsx index 6fcf13b8..be726426 100644 --- a/src/components/immersive-landing/ImmersiveLandingArrows.layout.tsx +++ b/src/components/immersive-landing/ImmersiveLandingArrows.layout.tsx @@ -7,7 +7,8 @@ import type { Layout } from "../../lib/layouts"; import { componentRecipe } from "./ImmersiveLanding.recipe"; const ImmersiveLandingArrows: Layout = () => { - const others = omit(props, "onPrev", "onNext", "isFirstPage", "isLastPage", "class"); + const others = omit(props, "id", "onPrev", "onNext", "isFirstPage", "isLastPage", "class"); + const baseId = () => props.id; const handleNext = () => { if (props.onNext) { @@ -26,6 +27,7 @@ const ImmersiveLandingArrows: Layout