chore: fix prettier formatting, regenerate phase9 manifest, add missing config files (.env.example, .node-version, .nvmrc, CI workflow)
CI / test (push) Failing after 28s
CI / test (push) Failing after 28s
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
# Production-safe example environment (Phase 14 integration defaults).
|
||||
# All demo and analytics integrations are blocked by default.
|
||||
NEXT_PUBLIC_DEMO_ENABLED=false
|
||||
DEMO_SUBMISSION_MODE=blocked
|
||||
DEMO_REQUEST_TIMEOUT_MS=4000
|
||||
DEMO_IDEMPOTENCY_TTL_SECONDS=900
|
||||
DEMO_TEST_SCENARIOS_ENABLED=false
|
||||
DEMO_LOCAL_TEST_MODE=false
|
||||
NEXT_PUBLIC_ANALYTICS_MODE=disabled
|
||||
SITE_ORIGIN=http://127.0.0.1:3000
|
||||
@@ -0,0 +1,27 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 11.9.0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24.17.0
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm test:integration
|
||||
- run: pnpm test:security
|
||||
- run: pnpm test:privacy
|
||||
- run: pnpm test:browser
|
||||
- run: pnpm test:a11y
|
||||
- run: pnpm test:visual
|
||||
@@ -0,0 +1 @@
|
||||
24.17.0
|
||||
@@ -38,7 +38,7 @@ The implementation must:
|
||||
Breakpoints are layout decisions, not device labels.
|
||||
|
||||
| Mode | CSS range | Reference frames | Required behavior |
|
||||
|---|---:|---|---|
|
||||
| ------- | ----------------: | ---------------- | ------------------------------------------------------------------------------------------------------- |
|
||||
| Compact | 0–767 px | 320, 390 | One-column sections; mobile drawer; stacked CTAs; product preview simplified; no horizontal page scroll |
|
||||
| Medium | 768–1119 px | 768, 1024 | Two-column where specified; mobile drawer remains; 4-column workflow allowed; balanced content widths |
|
||||
| Wide | 1120 px and above | 1280, 1440 | Desktop navigation and controls; 12-column composition; hero 7/5 split; max container 1280 px |
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
**Phases:** 1 — Design Foundation through 9 — Developer Handoff
|
||||
|
||||
| ID | Decision | Status | Rationale | Consequence |
|
||||
|---|---|---|---|---|
|
||||
| ----- | ---------------------------------------------------------------------------------------------------------- | -------------------- | -------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
|
||||
| D-001 | Use restrained operational blue as the main brand family | Approved | Supports trust, clarity, and product-led presentation | Blue carries navigation, links, selection, and routine primary actions |
|
||||
| D-002 | Reserve orange for demo conversion and tightly related emphasis | Approved | Preserves conversion salience and prevents visual noise | Orange is not a routine navigation or generic action color |
|
||||
| D-003 | Use Inter Variable for English and French | Approved | Screen-oriented, broad Latin support, strong UI and marketing utility | One Latin family across website and product-facing UI |
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
"files": [
|
||||
{
|
||||
"path": "09_DEVELOPER_HANDOFF_v1.0.md",
|
||||
"bytes": 8091,
|
||||
"sha256": "e82d486f1e566d1bf0263f1baa7dd8035ae62da5c36bb6bfa417e61331ec8d51"
|
||||
"bytes": 8378,
|
||||
"sha256": "6cb5a76ee42e48caadd672e852520aeed0a974a9e4642be924c6b0b3176e8b1f"
|
||||
},
|
||||
{
|
||||
"path": "DECISION_LOG_v9.0.md",
|
||||
"bytes": 38775,
|
||||
"sha256": "85569a7418f8cad57d094ee9ec1d83b7727b5ab91a405cbda26de0271a8d76a1"
|
||||
"bytes": 63034,
|
||||
"sha256": "8e00d2cfa72b2bad65d855827a0fdfd09cce3e30e9885e46d8f8c6c7feb5f09f"
|
||||
},
|
||||
{
|
||||
"path": "DEFINITION_OF_DONE_v1.0.md",
|
||||
@@ -108,8 +108,8 @@
|
||||
},
|
||||
{
|
||||
"path": "components/COMPONENT_CONTRACTS_v1.0.md",
|
||||
"bytes": 9800,
|
||||
"sha256": "84b455fe725bff11b6ab54bb13373b1aa52991b062a36fad07db47a940187a53"
|
||||
"bytes": 9698,
|
||||
"sha256": "c36fe8a5221d0f76f0b74894d7227135adb7bae45eca0a7554a31b9433218052"
|
||||
},
|
||||
{
|
||||
"path": "components/COMPONENT_STATE_MATRIX_v1.0.csv",
|
||||
@@ -138,23 +138,23 @@
|
||||
},
|
||||
{
|
||||
"path": "implementation/component-contracts_v1.0.ts",
|
||||
"bytes": 2844,
|
||||
"sha256": "177dd2420b804290cf1296e43f41a7e759c8859b3253073fbdc523a81f6f94c0"
|
||||
"bytes": 3041,
|
||||
"sha256": "45fb0222d99308b2f28014572316b13c2d7308bc16b1bd02f63b6e2257148399"
|
||||
},
|
||||
{
|
||||
"path": "implementation/hreflang-generator_v1.0.ts",
|
||||
"bytes": 751,
|
||||
"sha256": "ab919a36a5d08a28c38ae51b4ddd0d84cef5d3ec3bed13649ebd26c3fb80ac8a"
|
||||
"bytes": 799,
|
||||
"sha256": "aa9b5e2eb602b2f651b1195a2d0ff7c2a0e6157bf702d3b27ab25b5b86249ba8"
|
||||
},
|
||||
{
|
||||
"path": "implementation/localization-config_v1.0.ts",
|
||||
"bytes": 772,
|
||||
"sha256": "bd8ca09f121bfe516ece817948a81d58136e9205fbd2c05dad2ce87f2d7a1782"
|
||||
"bytes": 806,
|
||||
"sha256": "fd884ce4cd807ccdd196bd4c09e34128b3a26df73e0f267c120f270179c852d0"
|
||||
},
|
||||
{
|
||||
"path": "implementation/theme-config_v1.0.ts",
|
||||
"bytes": 725,
|
||||
"sha256": "f56d37da4129d5830ba9ebd77b18cd6084a144e8514bfd8a8152387ecf76607c"
|
||||
"bytes": 752,
|
||||
"sha256": "5ae9687c6ffa832ae12b70fa8272a61853960eea8a3cc18e3a68ef1f120046f8"
|
||||
},
|
||||
{
|
||||
"path": "interaction/ANALYTICS_EVENT_SPEC_v1.0.csv",
|
||||
@@ -168,28 +168,28 @@
|
||||
},
|
||||
{
|
||||
"path": "interaction/analytics-contract_v1.0.ts",
|
||||
"bytes": 832,
|
||||
"sha256": "2716eb69cc98032005e068ab7bdc265a4c896a8e1a1bed682c7fdd4f45368123"
|
||||
"bytes": 918,
|
||||
"sha256": "d79b32503a5f8346816c647b8a7f45a62468b96f4fc9525d6d5147b5d2f2ec45"
|
||||
},
|
||||
{
|
||||
"path": "interaction/demo-form-contract_v1.0.ts",
|
||||
"bytes": 632,
|
||||
"sha256": "194db77f7cfca417dbe35e6f2c2ea94cdd02806b83b1d0f1993fe509d8c10154"
|
||||
"bytes": 699,
|
||||
"sha256": "073cf70224f93312d177f77abd7987704205628209e30c44824ae45f929ee59e"
|
||||
},
|
||||
{
|
||||
"path": "interaction/theme-bootstrap_v1.0.js",
|
||||
"bytes": 753,
|
||||
"sha256": "7b89a849bdcd72e1049714ebf76f0c0decb39e44261480f0cb08bb7dfe734e4a"
|
||||
"bytes": 757,
|
||||
"sha256": "a90b31cc2b91fd085b8cd4442b85fce56127501602b9b7f90af75fd3c8f4c3b0"
|
||||
},
|
||||
{
|
||||
"path": "localization/LOCALIZATION_IMPLEMENTATION_v1.0.md",
|
||||
"bytes": 2062,
|
||||
"sha256": "87a494d7ef7ed9f5012ed1699edf90e38995aa89a0efc3800db854e1d57e1024"
|
||||
"bytes": 2069,
|
||||
"sha256": "63773e2f4777a6a322570d07d8badac8867dc4d6eef2d71bf05cbf81dfd2c85b"
|
||||
},
|
||||
{
|
||||
"path": "localization/ROUTE_MANIFEST_v1.0.json",
|
||||
"bytes": 1573,
|
||||
"sha256": "fa29b8d31da1f36f87d216a1228fd1bbe1cde337636950b40b0024a10efaf49a"
|
||||
"bytes": 1433,
|
||||
"sha256": "e08ac94ba1f3899bb02dfa934695f530d1089f9f03b0fadaa0feabd466ad8ec4"
|
||||
},
|
||||
{
|
||||
"path": "localization/TRANSLATION_KEY_INVENTORY_v1.0.csv",
|
||||
@@ -198,18 +198,18 @@
|
||||
},
|
||||
{
|
||||
"path": "localization/locales/ar/homepage.json",
|
||||
"bytes": 18615,
|
||||
"sha256": "fe08aabd60203dbdebc718117f5e497b52a453a8a6fb671fe0bca28651bf0d47"
|
||||
"bytes": 17419,
|
||||
"sha256": "19c1b9d0c90c017da1679b2cde4f123a98625ab6dae8002f58354839309f0474"
|
||||
},
|
||||
{
|
||||
"path": "localization/locales/en/homepage.json",
|
||||
"bytes": 13869,
|
||||
"sha256": "a5ed956c7f07fad016ca39b40b770c25e8beeae2799ff0fece82dbf087f18e51"
|
||||
"bytes": 12769,
|
||||
"sha256": "ba88daa108e3df63285327d0f7d7be6c0df3d971d800d48617e1e62c3414acfb"
|
||||
},
|
||||
{
|
||||
"path": "localization/locales/fr/homepage.json",
|
||||
"bytes": 15512,
|
||||
"sha256": "24079356bd47431ffeb66b7eb2981e108c03080205861b1bb42514b03b945a0a"
|
||||
"bytes": 14564,
|
||||
"sha256": "5a3dfe1dc43c99a30cfc275309b5084b5d9f583a33ea136d388babb0714de719"
|
||||
},
|
||||
{
|
||||
"path": "qa/ACCESSIBILITY_CHECKLIST_v1.0.csv",
|
||||
@@ -243,18 +243,18 @@
|
||||
},
|
||||
{
|
||||
"path": "qa/validate-phase9-package_v1.0.mjs",
|
||||
"bytes": 2534,
|
||||
"sha256": "631892318cf95c0530a605e5bf08def9b06c93af79b555205f3aeb8a2bcd87ed"
|
||||
"bytes": 2766,
|
||||
"sha256": "8bd42770f659b51e63d73b282d7d101d6d817650f8c9b8875cad1b0eb3eece4e"
|
||||
},
|
||||
{
|
||||
"path": "qa/visual-regression-scenarios_v1.0.json",
|
||||
"bytes": 1401,
|
||||
"sha256": "08a04f676cbb93e73b4c6bc112a55ebf287cb97be3dd3176ab80161e6de743c2"
|
||||
"bytes": 1209,
|
||||
"sha256": "27d961c33c32775485475f7037b8d3bbd13f634b65fe2f59d6eea898860edf5f"
|
||||
},
|
||||
{
|
||||
"path": "reference/FINDINGS_REPORT_v1.0.md",
|
||||
"bytes": 3062,
|
||||
"sha256": "ba34afd535856d0eaa8ed7e174c23a7b5f0eae87344c8fcee5daafe00c068ce4"
|
||||
"bytes": 4135,
|
||||
"sha256": "20eaba0c9fafd166220a64b2f0ede3c36b3bd9fc6f32982c1f6c7745f9311965"
|
||||
},
|
||||
{
|
||||
"path": "reference/PHASE_8_HANDOFF_v1.0.md",
|
||||
@@ -288,13 +288,13 @@
|
||||
},
|
||||
{
|
||||
"path": "tokens/semantic-tokens_v1.0.css",
|
||||
"bytes": 3553,
|
||||
"sha256": "99912762e078bfc78b4878fc46b8c7d03d90c073e9c236942a58d83f6585a1b9"
|
||||
"bytes": 3589,
|
||||
"sha256": "f28b2f03139a6aa89b85d00574f4fe56aa90d4ca1e03e80a1fb6eb8e5d9fa621"
|
||||
},
|
||||
{
|
||||
"path": "tokens/tokens_v1.0.ts",
|
||||
"bytes": 442,
|
||||
"sha256": "ec265270251f8dd01d9166aeca3fcec9be2b5e534d825c9058fb6ed3f51e3224"
|
||||
"sha256": "3a1a2bb1bd29e55bf024a468a7c95bf2ec29d3cdc479f580050e8503c19c40a7"
|
||||
},
|
||||
{
|
||||
"path": "validation/CONTENT_AND_STRUCTURE_VALIDATION_v1.0.json",
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
# Component Contracts
|
||||
|
||||
|
||||
|
||||
Every component below is implemented once for all locales, directions, and themes. Props are semantic; components do not fetch translations internally unless the architecture establishes a single typed content boundary.
|
||||
|
||||
|
||||
## SiteHeader
|
||||
|
||||
**Purpose:** Primary navigation, brand, locale/theme controls, login, demo CTA
|
||||
@@ -23,7 +20,6 @@ Every component below is implemented once for all locales, directions, and theme
|
||||
|
||||
**Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant.
|
||||
|
||||
|
||||
## MobileDrawer
|
||||
|
||||
**Purpose:** Compact/medium navigation overlay
|
||||
@@ -42,7 +38,6 @@ Every component below is implemented once for all locales, directions, and theme
|
||||
|
||||
**Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant.
|
||||
|
||||
|
||||
## LocaleSelector
|
||||
|
||||
**Purpose:** Switch locale by stable route ID
|
||||
@@ -61,7 +56,6 @@ Every component below is implemented once for all locales, directions, and theme
|
||||
|
||||
**Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant.
|
||||
|
||||
|
||||
## ThemeSelector
|
||||
|
||||
**Purpose:** Set Light/Dark/System preference
|
||||
@@ -80,7 +74,6 @@ Every component below is implemented once for all locales, directions, and theme
|
||||
|
||||
**Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant.
|
||||
|
||||
|
||||
## HeroSection
|
||||
|
||||
**Purpose:** Communicate category, promise, primary and secondary actions
|
||||
@@ -99,7 +92,6 @@ Every component below is implemented once for all locales, directions, and theme
|
||||
|
||||
**Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant.
|
||||
|
||||
|
||||
## ProductPreview
|
||||
|
||||
**Purpose:** Illustrative/evidence-governed product visual
|
||||
@@ -118,7 +110,6 @@ Every component below is implemented once for all locales, directions, and theme
|
||||
|
||||
**Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant.
|
||||
|
||||
|
||||
## LifecycleProof
|
||||
|
||||
**Purpose:** Show operational coverage without customer logos
|
||||
@@ -137,7 +128,6 @@ Every component below is implemented once for all locales, directions, and theme
|
||||
|
||||
**Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant.
|
||||
|
||||
|
||||
## ComparisonSection
|
||||
|
||||
**Purpose:** Fragmented versus connected workflow
|
||||
@@ -156,7 +146,6 @@ Every component below is implemented once for all locales, directions, and theme
|
||||
|
||||
**Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant.
|
||||
|
||||
|
||||
## WorkflowSteps
|
||||
|
||||
**Purpose:** Four-step rental flow and tour source
|
||||
@@ -175,7 +164,6 @@ Every component below is implemented once for all locales, directions, and theme
|
||||
|
||||
**Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant.
|
||||
|
||||
|
||||
## RoleCards
|
||||
|
||||
**Purpose:** Explain value by operating role
|
||||
@@ -194,7 +182,6 @@ Every component below is implemented once for all locales, directions, and theme
|
||||
|
||||
**Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant.
|
||||
|
||||
|
||||
## ModuleGrid
|
||||
|
||||
**Purpose:** Describe approved product modules
|
||||
@@ -213,7 +200,6 @@ Every component below is implemented once for all locales, directions, and theme
|
||||
|
||||
**Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant.
|
||||
|
||||
|
||||
## MeasurementPlan
|
||||
|
||||
**Purpose:** Show KPI definitions without invented outcomes
|
||||
@@ -232,7 +218,6 @@ Every component below is implemented once for all locales, directions, and theme
|
||||
|
||||
**Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant.
|
||||
|
||||
|
||||
## IntegrationCategories
|
||||
|
||||
**Purpose:** Neutral category list
|
||||
@@ -251,7 +236,6 @@ Every component below is implemented once for all locales, directions, and theme
|
||||
|
||||
**Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant.
|
||||
|
||||
|
||||
## SecurityChecklist
|
||||
|
||||
**Purpose:** Evidence-request checklist
|
||||
@@ -270,7 +254,6 @@ Every component below is implemented once for all locales, directions, and theme
|
||||
|
||||
**Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant.
|
||||
|
||||
|
||||
## PricingPreview
|
||||
|
||||
**Purpose:** Explain pricing factors and demo route
|
||||
@@ -289,7 +272,6 @@ Every component below is implemented once for all locales, directions, and theme
|
||||
|
||||
**Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant.
|
||||
|
||||
|
||||
## FAQAccordion
|
||||
|
||||
**Purpose:** Localized questions and answers
|
||||
@@ -308,7 +290,6 @@ Every component below is implemented once for all locales, directions, and theme
|
||||
|
||||
**Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant.
|
||||
|
||||
|
||||
## FinalCTA
|
||||
|
||||
**Purpose:** Final conversion block and approved secondary contact
|
||||
@@ -327,7 +308,6 @@ Every component below is implemented once for all locales, directions, and theme
|
||||
|
||||
**Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant.
|
||||
|
||||
|
||||
## SiteFooter
|
||||
|
||||
**Purpose:** Localized navigation, legal, evidence status internal only
|
||||
@@ -346,7 +326,6 @@ Every component below is implemented once for all locales, directions, and theme
|
||||
|
||||
**Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant.
|
||||
|
||||
|
||||
## DemoDialog
|
||||
|
||||
**Purpose:** Qualification form and submission states
|
||||
@@ -365,7 +344,6 @@ Every component below is implemented once for all locales, directions, and theme
|
||||
|
||||
**Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant.
|
||||
|
||||
|
||||
## ProductTourDialog
|
||||
|
||||
**Purpose:** Four-step guided overview
|
||||
|
||||
@@ -1,26 +1,136 @@
|
||||
export type Locale = "en" | "fr" | "ar";
|
||||
export type Direction = "ltr" | "rtl";
|
||||
export type EvidenceStatus = "approved" | "illustrative" | "pending" | "expired" | "rejected";
|
||||
export type ThemePreference = "light" | "dark" | "system";
|
||||
export type Locale = 'en' | 'fr' | 'ar';
|
||||
export type Direction = 'ltr' | 'rtl';
|
||||
export type EvidenceStatus = 'approved' | 'illustrative' | 'pending' | 'expired' | 'rejected';
|
||||
export type ThemePreference = 'light' | 'dark' | 'system';
|
||||
|
||||
export interface LinkModel { id: string; label: string; href: string; external?: boolean; }
|
||||
export interface SiteHeaderProps { locale: Locale; direction: Direction; links: LinkModel[]; login?: LinkModel; onDemo(): void; }
|
||||
export interface DrawerProps extends SiteHeaderProps { open: boolean; onOpenChange(open: boolean): void; returnFocusRef?: HTMLElement | null; }
|
||||
export interface LocaleSelectorProps { locale: Locale; routeId: string; hash?: string; onLocaleChange(locale: Locale): void; }
|
||||
export interface ThemeSelectorProps { preference: ThemePreference; onPreferenceChange(value: ThemePreference): void; }
|
||||
export interface HeroModel { eyebrow: string; title: string; body: string; primaryLabel: string; secondaryLabel: string; }
|
||||
export interface ProductPreviewModel { status: EvidenceStatus; caption: string; locale: Locale; direction: Direction; theme: "light"|"dark"; assetId?: string; }
|
||||
export interface LifecycleItem { id: string; title: string; description: string; icon: string; }
|
||||
export interface ComparisonModel { title: string; body: string; before: { title: string; items: string[] }; after: { title: string; items: string[] }; }
|
||||
export interface WorkflowStep { id: string; ordinal: string; title: string; description: string; }
|
||||
export interface RoleCard { id: string; title: string; description: string; }
|
||||
export interface ModuleCard { id: string; title: string; description: string; evidenceStatus: EvidenceStatus; }
|
||||
export interface MetricDefinition { id: string; title: string; definition: string; publishedValue?: never; }
|
||||
export interface IntegrationCategory { id: string; label: string; verifiedPartners?: never; }
|
||||
export interface SecurityCheck { id: string; title: string; description: string; certificationBadge?: never; }
|
||||
export interface PricingModel { mode: "quote-only" | "approved-pricing"; title: string; body: string; factors: string[]; note: string; }
|
||||
export interface FaqItem { id: string; question: string; answer: string; }
|
||||
export interface DemoFormValues { fullName: string; workEmail: string; company: string; fleetSize: string; market?: string; preferredLanguage?: Locale; message?: string; }
|
||||
export type DemoFormState = "idle"|"validating"|"submitting"|"success"|"recoverable_error"|"terminal_error";
|
||||
export interface DemoFormProps { state: DemoFormState; values: DemoFormValues; fieldErrors: Partial<Record<keyof DemoFormValues,string>>; formError?: string; onSubmit(values: DemoFormValues): Promise<void>; }
|
||||
export interface TourProps { steps: WorkflowStep[]; activeIndex: number; open: boolean; onOpenChange(open:boolean): void; onStepChange(index:number): void; }
|
||||
export interface LinkModel {
|
||||
id: string;
|
||||
label: string;
|
||||
href: string;
|
||||
external?: boolean;
|
||||
}
|
||||
export interface SiteHeaderProps {
|
||||
locale: Locale;
|
||||
direction: Direction;
|
||||
links: LinkModel[];
|
||||
login?: LinkModel;
|
||||
onDemo(): void;
|
||||
}
|
||||
export interface DrawerProps extends SiteHeaderProps {
|
||||
open: boolean;
|
||||
onOpenChange(open: boolean): void;
|
||||
returnFocusRef?: HTMLElement | null;
|
||||
}
|
||||
export interface LocaleSelectorProps {
|
||||
locale: Locale;
|
||||
routeId: string;
|
||||
hash?: string;
|
||||
onLocaleChange(locale: Locale): void;
|
||||
}
|
||||
export interface ThemeSelectorProps {
|
||||
preference: ThemePreference;
|
||||
onPreferenceChange(value: ThemePreference): void;
|
||||
}
|
||||
export interface HeroModel {
|
||||
eyebrow: string;
|
||||
title: string;
|
||||
body: string;
|
||||
primaryLabel: string;
|
||||
secondaryLabel: string;
|
||||
}
|
||||
export interface ProductPreviewModel {
|
||||
status: EvidenceStatus;
|
||||
caption: string;
|
||||
locale: Locale;
|
||||
direction: Direction;
|
||||
theme: 'light' | 'dark';
|
||||
assetId?: string;
|
||||
}
|
||||
export interface LifecycleItem {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
}
|
||||
export interface ComparisonModel {
|
||||
title: string;
|
||||
body: string;
|
||||
before: { title: string; items: string[] };
|
||||
after: { title: string; items: string[] };
|
||||
}
|
||||
export interface WorkflowStep {
|
||||
id: string;
|
||||
ordinal: string;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
export interface RoleCard {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
export interface ModuleCard {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
evidenceStatus: EvidenceStatus;
|
||||
}
|
||||
export interface MetricDefinition {
|
||||
id: string;
|
||||
title: string;
|
||||
definition: string;
|
||||
publishedValue?: never;
|
||||
}
|
||||
export interface IntegrationCategory {
|
||||
id: string;
|
||||
label: string;
|
||||
verifiedPartners?: never;
|
||||
}
|
||||
export interface SecurityCheck {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
certificationBadge?: never;
|
||||
}
|
||||
export interface PricingModel {
|
||||
mode: 'quote-only' | 'approved-pricing';
|
||||
title: string;
|
||||
body: string;
|
||||
factors: string[];
|
||||
note: string;
|
||||
}
|
||||
export interface FaqItem {
|
||||
id: string;
|
||||
question: string;
|
||||
answer: string;
|
||||
}
|
||||
export interface DemoFormValues {
|
||||
fullName: string;
|
||||
workEmail: string;
|
||||
company: string;
|
||||
fleetSize: string;
|
||||
market?: string;
|
||||
preferredLanguage?: Locale;
|
||||
message?: string;
|
||||
}
|
||||
export type DemoFormState =
|
||||
| 'idle'
|
||||
| 'validating'
|
||||
| 'submitting'
|
||||
| 'success'
|
||||
| 'recoverable_error'
|
||||
| 'terminal_error';
|
||||
export interface DemoFormProps {
|
||||
state: DemoFormState;
|
||||
values: DemoFormValues;
|
||||
fieldErrors: Partial<Record<keyof DemoFormValues, string>>;
|
||||
formError?: string;
|
||||
onSubmit(values: DemoFormValues): Promise<void>;
|
||||
}
|
||||
export interface TourProps {
|
||||
steps: WorkflowStep[];
|
||||
activeIndex: number;
|
||||
open: boolean;
|
||||
onOpenChange(open: boolean): void;
|
||||
onStepChange(index: number): void;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import manifest from "../localization/ROUTE_MANIFEST_v1.0.json";
|
||||
type Locale = "en"|"fr"|"ar";
|
||||
import manifest from '../localization/ROUTE_MANIFEST_v1.0.json';
|
||||
type Locale = 'en' | 'fr' | 'ar';
|
||||
export function localizedPath(routeId: string, locale: Locale): string {
|
||||
const route = manifest.routes.find((item) => item.id === routeId);
|
||||
if (!route) throw new Error(`Unknown route: ${routeId}`);
|
||||
@@ -7,7 +7,15 @@ export function localizedPath(routeId:string, locale:Locale):string {
|
||||
return slug ? `/${locale}/${slug}` : `/${locale}`;
|
||||
}
|
||||
export function hreflangLinks(origin: string, routeId: string) {
|
||||
const alternates = manifest.locales.map((locale) => ({ rel:"alternate", hreflang:locale, href:new URL(localizedPath(routeId, locale as Locale),origin).href }));
|
||||
alternates.push({ rel:"alternate", hreflang:"x-default", href:new URL(localizedPath(routeId,"en"),origin).href });
|
||||
const alternates = manifest.locales.map((locale) => ({
|
||||
rel: 'alternate',
|
||||
hreflang: locale,
|
||||
href: new URL(localizedPath(routeId, locale as Locale), origin).href,
|
||||
}));
|
||||
alternates.push({
|
||||
rel: 'alternate',
|
||||
hreflang: 'x-default',
|
||||
href: new URL(localizedPath(routeId, 'en'), origin).href,
|
||||
});
|
||||
return alternates;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
export const locales = ["en", "fr", "ar"] as const;
|
||||
export type Locale = typeof locales[number];
|
||||
export const defaultLocale: Locale = "en";
|
||||
export const directionByLocale: Record<Locale,"ltr"|"rtl"> = { en:"ltr", fr:"ltr", ar:"rtl" };
|
||||
export const localeCookie = "hpc-locale";
|
||||
export const localeStorageKey = "hpc.locale";
|
||||
export const campaignQueryAllowlist = ["utm_source","utm_medium","utm_campaign","utm_content","gclid"] as const;
|
||||
export const rootDetectionOrder = ["cookie","accept-language","default-en"] as const;
|
||||
export function isLocale(value: unknown): value is Locale { return typeof value === "string" && (locales as readonly string[]).includes(value); }
|
||||
export function isolateLtr(value: string): { dir:"ltr"; value:string } { return { dir:"ltr", value }; }
|
||||
export const locales = ['en', 'fr', 'ar'] as const;
|
||||
export type Locale = (typeof locales)[number];
|
||||
export const defaultLocale: Locale = 'en';
|
||||
export const directionByLocale: Record<Locale, 'ltr' | 'rtl'> = { en: 'ltr', fr: 'ltr', ar: 'rtl' };
|
||||
export const localeCookie = 'hpc-locale';
|
||||
export const localeStorageKey = 'hpc.locale';
|
||||
export const campaignQueryAllowlist = [
|
||||
'utm_source',
|
||||
'utm_medium',
|
||||
'utm_campaign',
|
||||
'utm_content',
|
||||
'gclid',
|
||||
] as const;
|
||||
export const rootDetectionOrder = ['cookie', 'accept-language', 'default-en'] as const;
|
||||
export function isLocale(value: unknown): value is Locale {
|
||||
return typeof value === 'string' && (locales as readonly string[]).includes(value);
|
||||
}
|
||||
export function isolateLtr(value: string): { dir: 'ltr'; value: string } {
|
||||
return { dir: 'ltr', value };
|
||||
}
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
export const themeCookie = "hpc-theme";
|
||||
export const themeStorageKey = "hpc.theme.preference";
|
||||
export const themePreferences = ["light","dark","system"] as const;
|
||||
export type ThemePreference = typeof themePreferences[number];
|
||||
export type ResolvedTheme = "light"|"dark";
|
||||
export function isThemePreference(v:unknown): v is ThemePreference { return typeof v === "string" && (themePreferences as readonly string[]).includes(v); }
|
||||
export function resolveTheme(preference:ThemePreference, systemDark:boolean):ResolvedTheme { return preference === "system" ? (systemDark ? "dark" : "light") : preference; }
|
||||
export const themeCookieAttributes = { path:"/", sameSite:"lax" as const, maxAgeSeconds:31536000, secureInProduction:true };
|
||||
export const themeCookie = 'hpc-theme';
|
||||
export const themeStorageKey = 'hpc.theme.preference';
|
||||
export const themePreferences = ['light', 'dark', 'system'] as const;
|
||||
export type ThemePreference = (typeof themePreferences)[number];
|
||||
export type ResolvedTheme = 'light' | 'dark';
|
||||
export function isThemePreference(v: unknown): v is ThemePreference {
|
||||
return typeof v === 'string' && (themePreferences as readonly string[]).includes(v);
|
||||
}
|
||||
export function resolveTheme(preference: ThemePreference, systemDark: boolean): ResolvedTheme {
|
||||
return preference === 'system' ? (systemDark ? 'dark' : 'light') : preference;
|
||||
}
|
||||
export const themeCookieAttributes = {
|
||||
path: '/',
|
||||
sameSite: 'lax' as const,
|
||||
maxAgeSeconds: 31536000,
|
||||
secureInProduction: true,
|
||||
};
|
||||
|
||||
@@ -1,9 +1,41 @@
|
||||
export type AnalyticsEventName =
|
||||
| "page_view" | "section_view" | "navigation_click"
|
||||
| "locale_change" | "theme_change" | "mobile_nav_open" | "mobile_nav_close"
|
||||
| "product_tour_open" | "product_tour_step" | "product_tour_complete" | "product_tour_close"
|
||||
| "demo_open" | "demo_submit_attempt" | "demo_validation_error" | "demo_submit_success" | "demo_submit_failure" | "demo_close"
|
||||
| "faq_open";
|
||||
export interface AnalyticsContext { routeId:string; locale:"en"|"fr"|"ar"; direction:"ltr"|"rtl"; resolvedTheme:"light"|"dark"; source?:string; step?:number; errorCode?:string; }
|
||||
export interface AnalyticsAdapter { consentGranted():boolean; track(name:AnalyticsEventName, context:AnalyticsContext):void; }
|
||||
export const forbiddenPayloadKeys = ["name","email","company","message","phone","fieldValue","serverResponse"] as const;
|
||||
| 'page_view'
|
||||
| 'section_view'
|
||||
| 'navigation_click'
|
||||
| 'locale_change'
|
||||
| 'theme_change'
|
||||
| 'mobile_nav_open'
|
||||
| 'mobile_nav_close'
|
||||
| 'product_tour_open'
|
||||
| 'product_tour_step'
|
||||
| 'product_tour_complete'
|
||||
| 'product_tour_close'
|
||||
| 'demo_open'
|
||||
| 'demo_submit_attempt'
|
||||
| 'demo_validation_error'
|
||||
| 'demo_submit_success'
|
||||
| 'demo_submit_failure'
|
||||
| 'demo_close'
|
||||
| 'faq_open';
|
||||
export interface AnalyticsContext {
|
||||
routeId: string;
|
||||
locale: 'en' | 'fr' | 'ar';
|
||||
direction: 'ltr' | 'rtl';
|
||||
resolvedTheme: 'light' | 'dark';
|
||||
source?: string;
|
||||
step?: number;
|
||||
errorCode?: string;
|
||||
}
|
||||
export interface AnalyticsAdapter {
|
||||
consentGranted(): boolean;
|
||||
track(name: AnalyticsEventName, context: AnalyticsContext): void;
|
||||
}
|
||||
export const forbiddenPayloadKeys = [
|
||||
'name',
|
||||
'email',
|
||||
'company',
|
||||
'message',
|
||||
'phone',
|
||||
'fieldValue',
|
||||
'serverResponse',
|
||||
] as const;
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
export const fleetSizeValues = ["1-9","10-24","25-49","50-99","100-249","250+"] as const;
|
||||
export type FleetSize = typeof fleetSizeValues[number];
|
||||
export interface DemoRequest { fullName:string; workEmail:string; company:string; fleetSize:FleetSize; market?:string; preferredLanguage?:"en"|"fr"|"ar"; message?:string; idempotencyKey:string; source:string; }
|
||||
export interface DemoSuccess { requestId:string; acceptedAt:string; nextStepCode:"sales_followup"|"manual_review"; }
|
||||
export interface DemoFailure { code:"validation"|"rate_limited"|"duplicate"|"unavailable"|"unknown"; retryable:boolean; fieldErrors?:Record<string,string>; }
|
||||
export const fleetSizeValues = ['1-9', '10-24', '25-49', '50-99', '100-249', '250+'] as const;
|
||||
export type FleetSize = (typeof fleetSizeValues)[number];
|
||||
export interface DemoRequest {
|
||||
fullName: string;
|
||||
workEmail: string;
|
||||
company: string;
|
||||
fleetSize: FleetSize;
|
||||
market?: string;
|
||||
preferredLanguage?: 'en' | 'fr' | 'ar';
|
||||
message?: string;
|
||||
idempotencyKey: string;
|
||||
source: string;
|
||||
}
|
||||
export interface DemoSuccess {
|
||||
requestId: string;
|
||||
acceptedAt: string;
|
||||
nextStepCode: 'sales_followup' | 'manual_review';
|
||||
}
|
||||
export interface DemoFailure {
|
||||
code: 'validation' | 'rate_limited' | 'duplicate' | 'unavailable' | 'unknown';
|
||||
retryable: boolean;
|
||||
fieldErrors?: Record<string, string>;
|
||||
}
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
var cookieMatch = document.cookie.match(/(?:^|; )hpc-theme=([^;]+)/);
|
||||
var cookieValue = cookieMatch ? decodeURIComponent(cookieMatch[1]) : null;
|
||||
var stored = null;
|
||||
try { stored = localStorage.getItem("hpc.theme.preference"); } catch (_) {}
|
||||
var preference = allowed[cookieValue] ? cookieValue : (allowed[stored] ? stored : "system");
|
||||
var dark = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
var resolved = preference === "system" ? (dark ? "dark" : "light") : preference;
|
||||
try {
|
||||
stored = localStorage.getItem('hpc.theme.preference');
|
||||
} catch (_) {}
|
||||
var preference = allowed[cookieValue] ? cookieValue : allowed[stored] ? stored : 'system';
|
||||
var dark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
var resolved = preference === 'system' ? (dark ? 'dark' : 'light') : preference;
|
||||
var root = document.documentElement;
|
||||
root.dataset.themePreference = preference;
|
||||
root.dataset.theme = resolved;
|
||||
|
||||
@@ -9,7 +9,7 @@ English is the authoring source. French and Arabic are independent production re
|
||||
Set document attributes on the server:
|
||||
|
||||
```html
|
||||
<html lang="ar" dir="rtl" data-theme-preference="system" data-theme="dark">
|
||||
<html lang="ar" dir="rtl" data-theme-preference="system" data-theme="dark"></html>
|
||||
```
|
||||
|
||||
Do not wait for hydration to set language, direction, or resolved theme.
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
{
|
||||
"version": "1.0",
|
||||
"defaultLocale": "en",
|
||||
"locales": [
|
||||
"en",
|
||||
"fr",
|
||||
"ar"
|
||||
],
|
||||
"locales": ["en", "fr", "ar"],
|
||||
"rootDetection": true,
|
||||
"routes": [
|
||||
{
|
||||
@@ -16,15 +12,7 @@
|
||||
"ar": ""
|
||||
},
|
||||
"indexable": true,
|
||||
"hashes": [
|
||||
"top",
|
||||
"product",
|
||||
"workflow",
|
||||
"modules",
|
||||
"pricing",
|
||||
"faq",
|
||||
"contact"
|
||||
]
|
||||
"hashes": ["top", "product", "workflow", "modules", "pricing", "faq", "contact"]
|
||||
},
|
||||
{
|
||||
"id": "login",
|
||||
@@ -68,22 +56,12 @@
|
||||
}
|
||||
],
|
||||
"switching": {
|
||||
"preserveQueryAllowlist": [
|
||||
"utm_source",
|
||||
"utm_medium",
|
||||
"utm_campaign",
|
||||
"utm_content",
|
||||
"gclid"
|
||||
],
|
||||
"preserveQueryAllowlist": ["utm_source", "utm_medium", "utm_campaign", "utm_content", "gclid"],
|
||||
"preserveValidHash": true,
|
||||
"dropUnknownQuery": true
|
||||
},
|
||||
"hreflang": {
|
||||
"values": [
|
||||
"en",
|
||||
"fr",
|
||||
"ar"
|
||||
],
|
||||
"values": ["en", "fr", "ar"],
|
||||
"xDefaultLocale": "en",
|
||||
"selfCanonical": true
|
||||
}
|
||||
|
||||
@@ -7,26 +7,11 @@
|
||||
},
|
||||
"header": {
|
||||
"nav": [
|
||||
[
|
||||
"product",
|
||||
"المنتج"
|
||||
],
|
||||
[
|
||||
"workflow",
|
||||
"سير العمل"
|
||||
],
|
||||
[
|
||||
"modules",
|
||||
"الوحدات"
|
||||
],
|
||||
[
|
||||
"pricing",
|
||||
"التسعير"
|
||||
],
|
||||
[
|
||||
"faq",
|
||||
"الأسئلة الشائعة"
|
||||
]
|
||||
["product", "المنتج"],
|
||||
["workflow", "سير العمل"],
|
||||
["modules", "الوحدات"],
|
||||
["pricing", "التسعير"],
|
||||
["faq", "الأسئلة الشائعة"]
|
||||
],
|
||||
"login": "تسجيل الدخول",
|
||||
"demo": "احجز عرضًا توضيحيًا",
|
||||
@@ -50,22 +35,10 @@
|
||||
"title": "سير عمل واحد يغطي دورة التأجير كاملة.",
|
||||
"body": "حافظ على ترابط سياق العميل والمركبة والعقد والمهام مع تقدم عملية التأجير.",
|
||||
"items": [
|
||||
[
|
||||
"الحجز",
|
||||
"سجّل الطلب والبيانات المطلوبة."
|
||||
],
|
||||
[
|
||||
"المركبة",
|
||||
"تابع التوافر والجاهزية."
|
||||
],
|
||||
[
|
||||
"العقد",
|
||||
"اجمع مهام التسليم والسجلات في موضع واحد."
|
||||
],
|
||||
[
|
||||
"الإرجاع",
|
||||
"سجّل الحالة وأغلق أعمال المتابعة."
|
||||
]
|
||||
["الحجز", "سجّل الطلب والبيانات المطلوبة."],
|
||||
["المركبة", "تابع التوافر والجاهزية."],
|
||||
["العقد", "اجمع مهام التسليم والسجلات في موضع واحد."],
|
||||
["الإرجاع", "سجّل الحالة وأغلق أعمال المتابعة."]
|
||||
]
|
||||
},
|
||||
"comparison": {
|
||||
@@ -92,48 +65,20 @@
|
||||
"title": "حرّك عملية التأجير إلى الأمام من دون فقدان السياق.",
|
||||
"body": "توضح كل خطوة القرار والمسؤول والإجراء التالي بدل دفن التشغيل داخل قائمة عامة من الميزات.",
|
||||
"steps": [
|
||||
[
|
||||
"01",
|
||||
"تسجيل الحجز",
|
||||
"سجّل التواريخ وبيانات السائق والمتطلبات والمصدر."
|
||||
],
|
||||
[
|
||||
"02",
|
||||
"تخصيص مركبة جاهزة",
|
||||
"طابق التوافر مع الفئة والموقع وحالة الجاهزية."
|
||||
],
|
||||
[
|
||||
"03",
|
||||
"إكمال التسليم",
|
||||
"تحقق من المستندات والفحص والعقد ومهام تسليم المركبة."
|
||||
],
|
||||
[
|
||||
"04",
|
||||
"معالجة الإرجاع",
|
||||
"سجّل الحالة وأغلق الأعمال المفتوحة وأعد المركبة إلى التوافر."
|
||||
]
|
||||
["01", "تسجيل الحجز", "سجّل التواريخ وبيانات السائق والمتطلبات والمصدر."],
|
||||
["02", "تخصيص مركبة جاهزة", "طابق التوافر مع الفئة والموقع وحالة الجاهزية."],
|
||||
["03", "إكمال التسليم", "تحقق من المستندات والفحص والعقد ومهام تسليم المركبة."],
|
||||
["04", "معالجة الإرجاع", "سجّل الحالة وأغلق الأعمال المفتوحة وأعد المركبة إلى التوافر."]
|
||||
]
|
||||
},
|
||||
"roles": {
|
||||
"eyebrow": "مصمم لفريق التشغيل",
|
||||
"title": "امنح كل دور السياق الذي يحتاجه من دون إنشاء نسخ متعددة من الحقيقة.",
|
||||
"roles": [
|
||||
[
|
||||
"المالك أو المدير",
|
||||
"اطّلع على عبء العمل والاستثناءات والمؤشرات التشغيلية عبر النشاط."
|
||||
],
|
||||
[
|
||||
"فريق الحجوزات",
|
||||
"حوّل الطلبات إلى حجوزات مؤكدة مع عدد أقل من عمليات التسليم."
|
||||
],
|
||||
[
|
||||
"الأسطول والتشغيل",
|
||||
"اعرف المركبات المتاحة أو المحجوبة أو المستحقة أو التي تنتظر إجراءً."
|
||||
],
|
||||
[
|
||||
"مكتب الاستقبال",
|
||||
"حضّر مهام التسليم والإرجاع قبل انتظار العميل."
|
||||
]
|
||||
["المالك أو المدير", "اطّلع على عبء العمل والاستثناءات والمؤشرات التشغيلية عبر النشاط."],
|
||||
["فريق الحجوزات", "حوّل الطلبات إلى حجوزات مؤكدة مع عدد أقل من عمليات التسليم."],
|
||||
["الأسطول والتشغيل", "اعرف المركبات المتاحة أو المحجوبة أو المستحقة أو التي تنتظر إجراءً."],
|
||||
["مكتب الاستقبال", "حضّر مهام التسليم والإرجاع قبل انتظار العميل."]
|
||||
]
|
||||
},
|
||||
"modules": {
|
||||
@@ -141,30 +86,12 @@
|
||||
"title": "نواة مترابطة منظّمة حول أعمال التأجير الفعلية.",
|
||||
"body": "توضح حدود الوحدات المسؤوليات، بينما تمنع السجلات المشتركة فوضى النسخ واللصق المعتادة.",
|
||||
"items": [
|
||||
[
|
||||
"الحجوزات",
|
||||
"التوافر والتواريخ ومتطلبات العميل والحالة ضمن سير واحد."
|
||||
],
|
||||
[
|
||||
"جاهزية الأسطول",
|
||||
"توافر المركبات والحجوزات التشغيلية وحالة الخدمة وسياق الموقع."
|
||||
],
|
||||
[
|
||||
"عقود الإيجار",
|
||||
"متطلبات التسليم وحالة العقد والسجلات التشغيلية."
|
||||
],
|
||||
[
|
||||
"فحص المركبة",
|
||||
"فحوص منظمة عند التسليم والإرجاع مع مهام متابعة."
|
||||
],
|
||||
[
|
||||
"سجلات العملاء",
|
||||
"سجل التأجير وبيانات الاتصال المرتبطة بالعمل الجاري."
|
||||
],
|
||||
[
|
||||
"التقارير التشغيلية",
|
||||
"تعريفات وعروض تشغيلية يمكن التحقق منها مقابل النشاط المصدر."
|
||||
]
|
||||
["الحجوزات", "التوافر والتواريخ ومتطلبات العميل والحالة ضمن سير واحد."],
|
||||
["جاهزية الأسطول", "توافر المركبات والحجوزات التشغيلية وحالة الخدمة وسياق الموقع."],
|
||||
["عقود الإيجار", "متطلبات التسليم وحالة العقد والسجلات التشغيلية."],
|
||||
["فحص المركبة", "فحوص منظمة عند التسليم والإرجاع مع مهام متابعة."],
|
||||
["سجلات العملاء", "سجل التأجير وبيانات الاتصال المرتبطة بالعمل الجاري."],
|
||||
["التقارير التشغيلية", "تعريفات وعروض تشغيلية يمكن التحقق منها مقابل النشاط المصدر."]
|
||||
]
|
||||
},
|
||||
"results": {
|
||||
@@ -172,22 +99,10 @@
|
||||
"title": "قِس التغيير، لا الخطاب التسويقي.",
|
||||
"body": "لا ينبغي نشر أي نتيجة قبل اعتماد خط أساس حقيقي لعميل وفترة مقارنة وتعريف واضح للبيانات. لذلك تعرض هذه النسخة منهجية القياس بدل نسب مئوية مختلقة.",
|
||||
"metrics": [
|
||||
[
|
||||
"زمن معالجة الحجز",
|
||||
"من استلام طلب صالح للعمل حتى تأكيد الحجز."
|
||||
],
|
||||
[
|
||||
"زمن إعادة المركبة إلى الجاهزية",
|
||||
"من تسجيل الإرجاع حتى حالة جاهز للتأجير."
|
||||
],
|
||||
[
|
||||
"معدل استخدام الأسطول",
|
||||
"مقارنة أيام التوافر بأيام التأجير وفق تعريف واحد معتمد."
|
||||
],
|
||||
[
|
||||
"متابعة حالات التأخير",
|
||||
"عمليات التأجير المتأخرة المفتوحة مع مسؤول وإجراء تالٍ."
|
||||
]
|
||||
["زمن معالجة الحجز", "من استلام طلب صالح للعمل حتى تأكيد الحجز."],
|
||||
["زمن إعادة المركبة إلى الجاهزية", "من تسجيل الإرجاع حتى حالة جاهز للتأجير."],
|
||||
["معدل استخدام الأسطول", "مقارنة أيام التوافر بأيام التأجير وفق تعريف واحد معتمد."],
|
||||
["متابعة حالات التأخير", "عمليات التأجير المتأخرة المفتوحة مع مسؤول وإجراء تالٍ."]
|
||||
],
|
||||
"methodTitle": "المنهجية المطلوبة قبل نشر أي نتيجة",
|
||||
"method": [
|
||||
@@ -215,22 +130,10 @@
|
||||
"title": "قيّم المنصة بالأدلة، لا بالشعارات.",
|
||||
"body": "لا تُنشر شهادات أو وعود بمستوى الخدمة قبل وجود وثائق معتمدة. أثناء التقييم، اطلب إجابات محددة حول الضوابط المهمة لنشاطك.",
|
||||
"checks": [
|
||||
[
|
||||
"التحكم في الوصول",
|
||||
"الأدوار والصلاحيات وإجراءات المسؤول وإنهاء وصول المستخدم."
|
||||
],
|
||||
[
|
||||
"قابلية التدقيق",
|
||||
"التغييرات المسجلة والقابلة للبحث والتصدير."
|
||||
],
|
||||
[
|
||||
"معالجة البيانات",
|
||||
"الملكية والاحتفاظ والتصدير والحذف والجهات الفرعية."
|
||||
],
|
||||
[
|
||||
"استمرارية العمل",
|
||||
"النسخ الاحتياطي والاستعادة والتواصل عند الحوادث والدعم."
|
||||
]
|
||||
["التحكم في الوصول", "الأدوار والصلاحيات وإجراءات المسؤول وإنهاء وصول المستخدم."],
|
||||
["قابلية التدقيق", "التغييرات المسجلة والقابلة للبحث والتصدير."],
|
||||
["معالجة البيانات", "الملكية والاحتفاظ والتصدير والحذف والجهات الفرعية."],
|
||||
["استمرارية العمل", "النسخ الاحتياطي والاستعادة والتواصل عند الحوادث والدعم."]
|
||||
]
|
||||
},
|
||||
"pricing": {
|
||||
@@ -289,65 +192,32 @@
|
||||
[
|
||||
"المنتج",
|
||||
[
|
||||
[
|
||||
"سير العمل",
|
||||
"#workflow"
|
||||
],
|
||||
[
|
||||
"الوحدات",
|
||||
"#modules"
|
||||
],
|
||||
[
|
||||
"التسعير",
|
||||
"#pricing"
|
||||
]
|
||||
["سير العمل", "#workflow"],
|
||||
["الوحدات", "#modules"],
|
||||
["التسعير", "#pricing"]
|
||||
]
|
||||
],
|
||||
[
|
||||
"الشركة",
|
||||
[
|
||||
[
|
||||
"تواصل مع المبيعات",
|
||||
"#contact"
|
||||
],
|
||||
[
|
||||
"احجز عرضًا توضيحيًا",
|
||||
"#demo"
|
||||
]
|
||||
["تواصل مع المبيعات", "#contact"],
|
||||
["احجز عرضًا توضيحيًا", "#demo"]
|
||||
]
|
||||
],
|
||||
[
|
||||
"الحساب",
|
||||
[
|
||||
[
|
||||
"تسجيل الدخول",
|
||||
"#login"
|
||||
],
|
||||
[
|
||||
"اللغة",
|
||||
"#language"
|
||||
],
|
||||
[
|
||||
"المظهر",
|
||||
"#theme"
|
||||
]
|
||||
["تسجيل الدخول", "#login"],
|
||||
["اللغة", "#language"],
|
||||
["المظهر", "#theme"]
|
||||
]
|
||||
],
|
||||
[
|
||||
"قانوني",
|
||||
[
|
||||
[
|
||||
"الخصوصية",
|
||||
"#!"
|
||||
],
|
||||
[
|
||||
"الشروط",
|
||||
"#!"
|
||||
],
|
||||
[
|
||||
"إمكانية الوصول",
|
||||
"#!"
|
||||
]
|
||||
["الخصوصية", "#!"],
|
||||
["الشروط", "#!"],
|
||||
["إمكانية الوصول", "#!"]
|
||||
]
|
||||
]
|
||||
],
|
||||
@@ -377,11 +247,7 @@
|
||||
"أكثر من 500 مركبة",
|
||||
"غير متأكد بعد"
|
||||
],
|
||||
"langOptions": [
|
||||
"English",
|
||||
"Français",
|
||||
"العربية"
|
||||
],
|
||||
"langOptions": ["English", "Français", "العربية"],
|
||||
"successTitle": "تم تسجيل طلب العرض",
|
||||
"successBody": "اكتمل مسار الحجز في هذا النموذج الأولي. لم يُرسل أي طلب إلى نظام مبيعات فعلي.",
|
||||
"submitAnother": "إرسال طلب آخر",
|
||||
|
||||
@@ -7,26 +7,11 @@
|
||||
},
|
||||
"header": {
|
||||
"nav": [
|
||||
[
|
||||
"product",
|
||||
"Product"
|
||||
],
|
||||
[
|
||||
"workflow",
|
||||
"Workflow"
|
||||
],
|
||||
[
|
||||
"modules",
|
||||
"Modules"
|
||||
],
|
||||
[
|
||||
"pricing",
|
||||
"Pricing"
|
||||
],
|
||||
[
|
||||
"faq",
|
||||
"FAQ"
|
||||
]
|
||||
["product", "Product"],
|
||||
["workflow", "Workflow"],
|
||||
["modules", "Modules"],
|
||||
["pricing", "Pricing"],
|
||||
["faq", "FAQ"]
|
||||
],
|
||||
"login": "Log in",
|
||||
"demo": "Book a demo",
|
||||
@@ -50,22 +35,10 @@
|
||||
"title": "One workflow across the rental lifecycle.",
|
||||
"body": "Keep the customer, vehicle, agreement, and task context connected as the rental moves forward.",
|
||||
"items": [
|
||||
[
|
||||
"Reservation",
|
||||
"Capture the request and required details."
|
||||
],
|
||||
[
|
||||
"Vehicle",
|
||||
"Track availability and readiness."
|
||||
],
|
||||
[
|
||||
"Agreement",
|
||||
"Keep pickup tasks and records together."
|
||||
],
|
||||
[
|
||||
"Return",
|
||||
"Record condition and close follow-up work."
|
||||
]
|
||||
["Reservation", "Capture the request and required details."],
|
||||
["Vehicle", "Track availability and readiness."],
|
||||
["Agreement", "Keep pickup tasks and records together."],
|
||||
["Return", "Record condition and close follow-up work."]
|
||||
]
|
||||
},
|
||||
"comparison": {
|
||||
@@ -92,48 +65,23 @@
|
||||
"title": "Move the rental forward without losing the thread.",
|
||||
"body": "Each step exposes the decision, owner, and next action instead of burying operations inside a generic feature list.",
|
||||
"steps": [
|
||||
[
|
||||
"01",
|
||||
"Capture the reservation",
|
||||
"Record dates, driver details, requirements, and source."
|
||||
],
|
||||
[
|
||||
"02",
|
||||
"Assign a ready vehicle",
|
||||
"Match availability with class, location, and readiness."
|
||||
],
|
||||
[
|
||||
"03",
|
||||
"Complete pickup",
|
||||
"Confirm documents, inspection, agreement, and handover tasks."
|
||||
],
|
||||
[
|
||||
"04",
|
||||
"Process the return",
|
||||
"Record condition, close open work, and release the vehicle."
|
||||
]
|
||||
["01", "Capture the reservation", "Record dates, driver details, requirements, and source."],
|
||||
["02", "Assign a ready vehicle", "Match availability with class, location, and readiness."],
|
||||
["03", "Complete pickup", "Confirm documents, inspection, agreement, and handover tasks."],
|
||||
["04", "Process the return", "Record condition, close open work, and release the vehicle."]
|
||||
]
|
||||
},
|
||||
"roles": {
|
||||
"eyebrow": "Built for the operating team",
|
||||
"title": "Give each role the context it needs, without creating separate versions of the truth.",
|
||||
"roles": [
|
||||
[
|
||||
"Owner or manager",
|
||||
"See workload, exceptions, and operating signals across the business."
|
||||
],
|
||||
[
|
||||
"Reservations team",
|
||||
"Move requests from inquiry to confirmed rental with fewer handoffs."
|
||||
],
|
||||
["Owner or manager", "See workload, exceptions, and operating signals across the business."],
|
||||
["Reservations team", "Move requests from inquiry to confirmed rental with fewer handoffs."],
|
||||
[
|
||||
"Fleet and operations",
|
||||
"Know which vehicles are available, blocked, due, or awaiting action."
|
||||
],
|
||||
[
|
||||
"Front desk",
|
||||
"Prepare pickup and return tasks before the customer is waiting."
|
||||
]
|
||||
["Front desk", "Prepare pickup and return tasks before the customer is waiting."]
|
||||
]
|
||||
},
|
||||
"modules": {
|
||||
@@ -141,26 +89,14 @@
|
||||
"title": "A connected core, organized around real rental work.",
|
||||
"body": "Module boundaries clarify responsibility while shared records prevent the usual copy-and-paste archaeology.",
|
||||
"items": [
|
||||
[
|
||||
"Reservations",
|
||||
"Availability, dates, customer requirements, and status in one flow."
|
||||
],
|
||||
[
|
||||
"Fleet readiness",
|
||||
"Vehicle availability, blocks, service state, and location context."
|
||||
],
|
||||
[
|
||||
"Rental agreements",
|
||||
"Pickup requirements, agreement status, and operational records."
|
||||
],
|
||||
["Reservations", "Availability, dates, customer requirements, and status in one flow."],
|
||||
["Fleet readiness", "Vehicle availability, blocks, service state, and location context."],
|
||||
["Rental agreements", "Pickup requirements, agreement status, and operational records."],
|
||||
[
|
||||
"Vehicle inspections",
|
||||
"Structured pickup and return condition checks with follow-up tasks."
|
||||
],
|
||||
[
|
||||
"Customer records",
|
||||
"Rental history and contact context attached to active work."
|
||||
],
|
||||
["Customer records", "Rental history and contact context attached to active work."],
|
||||
[
|
||||
"Operations reporting",
|
||||
"Definitions and operational views that can be validated against source activity."
|
||||
@@ -172,22 +108,13 @@
|
||||
"title": "Measure the change, not the marketing.",
|
||||
"body": "No outcome claim should appear until a real customer baseline, comparison period, and data definition are approved. The production page therefore shows the measurement plan instead of fictional percentages.",
|
||||
"metrics": [
|
||||
[
|
||||
"Reservation handling time",
|
||||
"From usable request to confirmed reservation."
|
||||
],
|
||||
[
|
||||
"Vehicle turnaround time",
|
||||
"From return recorded to ready-for-rental status."
|
||||
],
|
||||
["Reservation handling time", "From usable request to confirmed reservation."],
|
||||
["Vehicle turnaround time", "From return recorded to ready-for-rental status."],
|
||||
[
|
||||
"Fleet utilization",
|
||||
"Available rental days compared with rented days, using one approved definition."
|
||||
],
|
||||
[
|
||||
"Overdue follow-up",
|
||||
"Open overdue rentals with an assigned owner and next action."
|
||||
]
|
||||
["Overdue follow-up", "Open overdue rentals with an assigned owner and next action."]
|
||||
],
|
||||
"methodTitle": "Methodology required before publishing results",
|
||||
"method": [
|
||||
@@ -215,22 +142,10 @@
|
||||
"title": "Evaluate the platform with evidence, not badges.",
|
||||
"body": "Certifications and service-level claims remain unpublished until approved documentation exists. During evaluation, request concrete answers for the controls that matter to your operation.",
|
||||
"checks": [
|
||||
[
|
||||
"Access control",
|
||||
"Roles, permissions, administrator actions, and offboarding."
|
||||
],
|
||||
[
|
||||
"Auditability",
|
||||
"Which changes are recorded, searchable, and exportable."
|
||||
],
|
||||
[
|
||||
"Data handling",
|
||||
"Ownership, retention, export, deletion, and subprocessors."
|
||||
],
|
||||
[
|
||||
"Continuity",
|
||||
"Backup, recovery, incident communication, and support process."
|
||||
]
|
||||
["Access control", "Roles, permissions, administrator actions, and offboarding."],
|
||||
["Auditability", "Which changes are recorded, searchable, and exportable."],
|
||||
["Data handling", "Ownership, retention, export, deletion, and subprocessors."],
|
||||
["Continuity", "Backup, recovery, incident communication, and support process."]
|
||||
]
|
||||
},
|
||||
"pricing": {
|
||||
@@ -289,65 +204,32 @@
|
||||
[
|
||||
"Product",
|
||||
[
|
||||
[
|
||||
"Workflow",
|
||||
"#workflow"
|
||||
],
|
||||
[
|
||||
"Modules",
|
||||
"#modules"
|
||||
],
|
||||
[
|
||||
"Pricing",
|
||||
"#pricing"
|
||||
]
|
||||
["Workflow", "#workflow"],
|
||||
["Modules", "#modules"],
|
||||
["Pricing", "#pricing"]
|
||||
]
|
||||
],
|
||||
[
|
||||
"Company",
|
||||
[
|
||||
[
|
||||
"Contact sales",
|
||||
"#contact"
|
||||
],
|
||||
[
|
||||
"Book a demo",
|
||||
"#demo"
|
||||
]
|
||||
["Contact sales", "#contact"],
|
||||
["Book a demo", "#demo"]
|
||||
]
|
||||
],
|
||||
[
|
||||
"Account",
|
||||
[
|
||||
[
|
||||
"Log in",
|
||||
"#login"
|
||||
],
|
||||
[
|
||||
"Language",
|
||||
"#language"
|
||||
],
|
||||
[
|
||||
"Theme",
|
||||
"#theme"
|
||||
]
|
||||
["Log in", "#login"],
|
||||
["Language", "#language"],
|
||||
["Theme", "#theme"]
|
||||
]
|
||||
],
|
||||
[
|
||||
"Legal",
|
||||
[
|
||||
[
|
||||
"Privacy",
|
||||
"#!"
|
||||
],
|
||||
[
|
||||
"Terms",
|
||||
"#!"
|
||||
],
|
||||
[
|
||||
"Accessibility",
|
||||
"#!"
|
||||
]
|
||||
["Privacy", "#!"],
|
||||
["Terms", "#!"],
|
||||
["Accessibility", "#!"]
|
||||
]
|
||||
]
|
||||
],
|
||||
@@ -377,11 +259,7 @@
|
||||
"More than 500 vehicles",
|
||||
"Not sure yet"
|
||||
],
|
||||
"langOptions": [
|
||||
"English",
|
||||
"Français",
|
||||
"العربية"
|
||||
],
|
||||
"langOptions": ["English", "Français", "العربية"],
|
||||
"successTitle": "Demo request recorded",
|
||||
"successBody": "This prototype has completed the booking flow. No request was sent to a real sales system.",
|
||||
"submitAnother": "Submit another request",
|
||||
|
||||
@@ -7,26 +7,11 @@
|
||||
},
|
||||
"header": {
|
||||
"nav": [
|
||||
[
|
||||
"product",
|
||||
"Produit"
|
||||
],
|
||||
[
|
||||
"workflow",
|
||||
"Parcours"
|
||||
],
|
||||
[
|
||||
"modules",
|
||||
"Modules"
|
||||
],
|
||||
[
|
||||
"pricing",
|
||||
"Tarifs"
|
||||
],
|
||||
[
|
||||
"faq",
|
||||
"FAQ"
|
||||
]
|
||||
["product", "Produit"],
|
||||
["workflow", "Parcours"],
|
||||
["modules", "Modules"],
|
||||
["pricing", "Tarifs"],
|
||||
["faq", "FAQ"]
|
||||
],
|
||||
"login": "Se connecter",
|
||||
"demo": "Réserver une démo",
|
||||
@@ -50,22 +35,10 @@
|
||||
"title": "Un même parcours pour tout le cycle de location.",
|
||||
"body": "Conservez le contexte du client, du véhicule, du contrat et des tâches au fil de la location.",
|
||||
"items": [
|
||||
[
|
||||
"Réservation",
|
||||
"Saisir la demande et les informations nécessaires."
|
||||
],
|
||||
[
|
||||
"Véhicule",
|
||||
"Suivre la disponibilité et l’état de préparation."
|
||||
],
|
||||
[
|
||||
"Contrat",
|
||||
"Regrouper les tâches de départ et les documents."
|
||||
],
|
||||
[
|
||||
"Retour",
|
||||
"Enregistrer l’état et clôturer les actions de suivi."
|
||||
]
|
||||
["Réservation", "Saisir la demande et les informations nécessaires."],
|
||||
["Véhicule", "Suivre la disponibilité et l’état de préparation."],
|
||||
["Contrat", "Regrouper les tâches de départ et les documents."],
|
||||
["Retour", "Enregistrer l’état et clôturer les actions de suivi."]
|
||||
]
|
||||
},
|
||||
"comparison": {
|
||||
@@ -118,10 +91,7 @@
|
||||
"eyebrow": "Conçu pour l’équipe opérationnelle",
|
||||
"title": "Donnez à chaque rôle le contexte utile, sans multiplier les versions de la vérité.",
|
||||
"roles": [
|
||||
[
|
||||
"Direction ou responsable",
|
||||
"Voir la charge, les exceptions et les signaux d’exploitation."
|
||||
],
|
||||
["Direction ou responsable", "Voir la charge, les exceptions et les signaux d’exploitation."],
|
||||
[
|
||||
"Équipe réservations",
|
||||
"Faire passer les demandes vers une location confirmée avec moins de transmissions."
|
||||
@@ -130,10 +100,7 @@
|
||||
"Parc et opérations",
|
||||
"Savoir quels véhicules sont disponibles, bloqués, attendus ou en attente d’action."
|
||||
],
|
||||
[
|
||||
"Comptoir",
|
||||
"Préparer les tâches de départ et de retour avant que le client n’attende."
|
||||
]
|
||||
["Comptoir", "Préparer les tâches de départ et de retour avant que le client n’attende."]
|
||||
]
|
||||
},
|
||||
"modules": {
|
||||
@@ -141,30 +108,15 @@
|
||||
"title": "Un socle connecté, organisé autour du travail réel de location.",
|
||||
"body": "Les modules clarifient les responsabilités, tandis que les dossiers partagés évitent l’archéologie du copier-coller.",
|
||||
"items": [
|
||||
[
|
||||
"Réservations",
|
||||
"Disponibilités, dates, besoins du client et statut dans un même flux."
|
||||
],
|
||||
[
|
||||
"Préparation du parc",
|
||||
"Disponibilité, blocages, état de service et contexte d’agence."
|
||||
],
|
||||
[
|
||||
"Contrats de location",
|
||||
"Exigences de départ, état du contrat et dossiers opérationnels."
|
||||
],
|
||||
["Réservations", "Disponibilités, dates, besoins du client et statut dans un même flux."],
|
||||
["Préparation du parc", "Disponibilité, blocages, état de service et contexte d’agence."],
|
||||
["Contrats de location", "Exigences de départ, état du contrat et dossiers opérationnels."],
|
||||
[
|
||||
"Inspections des véhicules",
|
||||
"Contrôles structurés au départ et au retour avec actions de suivi."
|
||||
],
|
||||
[
|
||||
"Dossiers clients",
|
||||
"Historique de location et coordonnées rattachés au travail en cours."
|
||||
],
|
||||
[
|
||||
"Rapports opérationnels",
|
||||
"Définitions et vues vérifiables par rapport à l’activité source."
|
||||
]
|
||||
["Dossiers clients", "Historique de location et coordonnées rattachés au travail en cours."],
|
||||
["Rapports opérationnels", "Définitions et vues vérifiables par rapport à l’activité source."]
|
||||
]
|
||||
},
|
||||
"results": {
|
||||
@@ -176,18 +128,12 @@
|
||||
"Temps de traitement des réservations",
|
||||
"De la demande exploitable à la réservation confirmée."
|
||||
],
|
||||
[
|
||||
"Temps de remise en service",
|
||||
"Du retour enregistré au statut prêt à louer."
|
||||
],
|
||||
["Temps de remise en service", "Du retour enregistré au statut prêt à louer."],
|
||||
[
|
||||
"Taux d’utilisation du parc",
|
||||
"Jours louables comparés aux jours loués, selon une définition approuvée."
|
||||
],
|
||||
[
|
||||
"Suivi des retards",
|
||||
"Locations en retard avec responsable et prochaine action."
|
||||
]
|
||||
["Suivi des retards", "Locations en retard avec responsable et prochaine action."]
|
||||
],
|
||||
"methodTitle": "Méthode requise avant toute publication",
|
||||
"method": [
|
||||
@@ -215,22 +161,10 @@
|
||||
"title": "Évaluez la plateforme avec des preuves, pas avec des badges.",
|
||||
"body": "Aucune certification ni promesse de niveau de service n’est publiée sans documentation approuvée. Pendant l’évaluation, demandez des réponses concrètes sur les contrôles utiles à votre activité.",
|
||||
"checks": [
|
||||
[
|
||||
"Contrôle des accès",
|
||||
"Rôles, autorisations, actions administrateur et départs."
|
||||
],
|
||||
[
|
||||
"Traçabilité",
|
||||
"Modifications enregistrées, recherchables et exportables."
|
||||
],
|
||||
[
|
||||
"Gestion des données",
|
||||
"Propriété, conservation, export, suppression et sous-traitants."
|
||||
],
|
||||
[
|
||||
"Continuité",
|
||||
"Sauvegarde, reprise, communication d’incident et support."
|
||||
]
|
||||
["Contrôle des accès", "Rôles, autorisations, actions administrateur et départs."],
|
||||
["Traçabilité", "Modifications enregistrées, recherchables et exportables."],
|
||||
["Gestion des données", "Propriété, conservation, export, suppression et sous-traitants."],
|
||||
["Continuité", "Sauvegarde, reprise, communication d’incident et support."]
|
||||
]
|
||||
},
|
||||
"pricing": {
|
||||
@@ -289,65 +223,32 @@
|
||||
[
|
||||
"Produit",
|
||||
[
|
||||
[
|
||||
"Parcours",
|
||||
"#workflow"
|
||||
],
|
||||
[
|
||||
"Modules",
|
||||
"#modules"
|
||||
],
|
||||
[
|
||||
"Tarifs",
|
||||
"#pricing"
|
||||
]
|
||||
["Parcours", "#workflow"],
|
||||
["Modules", "#modules"],
|
||||
["Tarifs", "#pricing"]
|
||||
]
|
||||
],
|
||||
[
|
||||
"Entreprise",
|
||||
[
|
||||
[
|
||||
"Contacter le service commercial",
|
||||
"#contact"
|
||||
],
|
||||
[
|
||||
"Réserver une démo",
|
||||
"#demo"
|
||||
]
|
||||
["Contacter le service commercial", "#contact"],
|
||||
["Réserver une démo", "#demo"]
|
||||
]
|
||||
],
|
||||
[
|
||||
"Compte",
|
||||
[
|
||||
[
|
||||
"Se connecter",
|
||||
"#login"
|
||||
],
|
||||
[
|
||||
"Langue",
|
||||
"#language"
|
||||
],
|
||||
[
|
||||
"Thème",
|
||||
"#theme"
|
||||
]
|
||||
["Se connecter", "#login"],
|
||||
["Langue", "#language"],
|
||||
["Thème", "#theme"]
|
||||
]
|
||||
],
|
||||
[
|
||||
"Mentions légales",
|
||||
[
|
||||
[
|
||||
"Confidentialité",
|
||||
"#!"
|
||||
],
|
||||
[
|
||||
"Conditions",
|
||||
"#!"
|
||||
],
|
||||
[
|
||||
"Accessibilité",
|
||||
"#!"
|
||||
]
|
||||
["Confidentialité", "#!"],
|
||||
["Conditions", "#!"],
|
||||
["Accessibilité", "#!"]
|
||||
]
|
||||
]
|
||||
],
|
||||
@@ -377,11 +278,7 @@
|
||||
"Plus de 500 véhicules",
|
||||
"Je ne sais pas encore"
|
||||
],
|
||||
"langOptions": [
|
||||
"English",
|
||||
"Français",
|
||||
"العربية"
|
||||
],
|
||||
"langOptions": ["English", "Français", "العربية"],
|
||||
"successTitle": "Demande de démonstration enregistrée",
|
||||
"successBody": "Ce prototype a terminé le parcours de réservation. Aucune demande n’a été envoyée à un véritable système commercial.",
|
||||
"submitAnother": "Envoyer une autre demande",
|
||||
|
||||
@@ -3,22 +3,77 @@ import path from 'node:path';
|
||||
import crypto from 'node:crypto';
|
||||
const root = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..');
|
||||
const required = [
|
||||
'09_DEVELOPER_HANDOFF_v1.0.md','DECISION_LOG_v9.0.md','DEFINITION_OF_DONE_v1.0.md','OPEN_ISSUE_REGISTER_v1.0.csv',
|
||||
'tokens/design-tokens_v1.0.json','tokens/semantic-tokens_v1.0.css','components/COMPONENT_CONTRACTS_v1.0.md',
|
||||
'localization/ROUTE_MANIFEST_v1.0.json','localization/locales/en/homepage.json','localization/locales/fr/homepage.json','localization/locales/ar/homepage.json',
|
||||
'interaction/theme-bootstrap_v1.0.js','qa/QA_MATRIX_v1.0.csv','qa/ACCESSIBILITY_CHECKLIST_v1.0.csv','qa/PERFORMANCE_BUDGETS_v1.0.json'
|
||||
'09_DEVELOPER_HANDOFF_v1.0.md',
|
||||
'DECISION_LOG_v9.0.md',
|
||||
'DEFINITION_OF_DONE_v1.0.md',
|
||||
'OPEN_ISSUE_REGISTER_v1.0.csv',
|
||||
'tokens/design-tokens_v1.0.json',
|
||||
'tokens/semantic-tokens_v1.0.css',
|
||||
'components/COMPONENT_CONTRACTS_v1.0.md',
|
||||
'localization/ROUTE_MANIFEST_v1.0.json',
|
||||
'localization/locales/en/homepage.json',
|
||||
'localization/locales/fr/homepage.json',
|
||||
'localization/locales/ar/homepage.json',
|
||||
'interaction/theme-bootstrap_v1.0.js',
|
||||
'qa/QA_MATRIX_v1.0.csv',
|
||||
'qa/ACCESSIBILITY_CHECKLIST_v1.0.csv',
|
||||
'qa/PERFORMANCE_BUDGETS_v1.0.json',
|
||||
];
|
||||
const errors=[]; let checks=0;
|
||||
for (const file of required){ checks++; if(!fs.existsSync(path.join(root,file))) errors.push(`Missing ${file}`); }
|
||||
const errors = [];
|
||||
let checks = 0;
|
||||
for (const file of required) {
|
||||
checks++;
|
||||
if (!fs.existsSync(path.join(root, file))) errors.push(`Missing ${file}`);
|
||||
}
|
||||
const readJson = (f) => JSON.parse(fs.readFileSync(path.join(root, f), 'utf8'));
|
||||
for (const file of ['tokens/design-tokens_v1.0.json','localization/ROUTE_MANIFEST_v1.0.json','qa/PERFORMANCE_BUDGETS_v1.0.json','qa/visual-regression-scenarios_v1.0.json']) { try{readJson(file);checks++;}catch(e){errors.push(`${file}: ${e.message}`);} }
|
||||
function shape(value){ if(Array.isArray(value)) return value.map(shape); if(value && typeof value==='object') return Object.fromEntries(Object.keys(value).sort().map(k=>[k,shape(value[k])])); return typeof value; }
|
||||
const locales=['en','fr','ar'].map(l=>readJson(`localization/locales/${l}/homepage.json`));
|
||||
for (const file of [
|
||||
'tokens/design-tokens_v1.0.json',
|
||||
'localization/ROUTE_MANIFEST_v1.0.json',
|
||||
'qa/PERFORMANCE_BUDGETS_v1.0.json',
|
||||
'qa/visual-regression-scenarios_v1.0.json',
|
||||
]) {
|
||||
try {
|
||||
readJson(file);
|
||||
checks++;
|
||||
} catch (e) {
|
||||
errors.push(`${file}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
function shape(value) {
|
||||
if (Array.isArray(value)) return value.map(shape);
|
||||
if (value && typeof value === 'object')
|
||||
return Object.fromEntries(
|
||||
Object.keys(value)
|
||||
.sort()
|
||||
.map((k) => [k, shape(value[k])]),
|
||||
);
|
||||
return typeof value;
|
||||
}
|
||||
const locales = ['en', 'fr', 'ar'].map((l) => readJson(`localization/locales/${l}/homepage.json`));
|
||||
const base = JSON.stringify(shape(locales[0]));
|
||||
for (let i=1;i<locales.length;i++){ checks++; if(JSON.stringify(shape(locales[i]))!==base) errors.push(`Locale shape mismatch: ${['en','fr','ar'][i]}`); }
|
||||
for (let i = 1; i < locales.length; i++) {
|
||||
checks++;
|
||||
if (JSON.stringify(shape(locales[i])) !== base)
|
||||
errors.push(`Locale shape mismatch: ${['en', 'fr', 'ar'][i]}`);
|
||||
}
|
||||
const routes = readJson('localization/ROUTE_MANIFEST_v1.0.json');
|
||||
for (const route of routes.routes){ for (const locale of routes.locales){ checks++; if(!(locale in route.slugs)) errors.push(`Route ${route.id} missing ${locale}`); } }
|
||||
const issueCsv=fs.readFileSync(path.join(root,'OPEN_ISSUE_REGISTER_v1.0.csv'),'utf8'); checks++; if(/,(Critical|High),[^\n]*,,/.test(issueCsv)) errors.push('Critical/High issue missing ownership data');
|
||||
const css=fs.readFileSync(path.join(root,'tokens/semantic-tokens_v1.0.css'),'utf8'); checks++; if(!css.includes('--color-action-conversion')) errors.push('Conversion semantic token missing');
|
||||
if(errors.length){ console.error(JSON.stringify({status:'FAILED',checks,errors},null,2)); process.exit(1); }
|
||||
console.log(JSON.stringify({status:'PASSED',checks,files:fs.readdirSync(root).length},null,2));
|
||||
for (const route of routes.routes) {
|
||||
for (const locale of routes.locales) {
|
||||
checks++;
|
||||
if (!(locale in route.slugs)) errors.push(`Route ${route.id} missing ${locale}`);
|
||||
}
|
||||
}
|
||||
const issueCsv = fs.readFileSync(path.join(root, 'OPEN_ISSUE_REGISTER_v1.0.csv'), 'utf8');
|
||||
checks++;
|
||||
if (/,(Critical|High),[^\n]*,,/.test(issueCsv))
|
||||
errors.push('Critical/High issue missing ownership data');
|
||||
const css = fs.readFileSync(path.join(root, 'tokens/semantic-tokens_v1.0.css'), 'utf8');
|
||||
checks++;
|
||||
if (!css.includes('--color-action-conversion')) errors.push('Conversion semantic token missing');
|
||||
if (errors.length) {
|
||||
console.error(JSON.stringify({ status: 'FAILED', checks, errors }, null, 2));
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(
|
||||
JSON.stringify({ status: 'PASSED', checks, files: fs.readdirSync(root).length }, null, 2),
|
||||
);
|
||||
|
||||
@@ -5,80 +5,56 @@
|
||||
{
|
||||
"id": "VR-001",
|
||||
"route": "/en",
|
||||
"viewport": [
|
||||
1440,
|
||||
1000
|
||||
],
|
||||
"viewport": [1440, 1000],
|
||||
"theme": "light",
|
||||
"state": "homepage-top"
|
||||
},
|
||||
{
|
||||
"id": "VR-002",
|
||||
"route": "/fr",
|
||||
"viewport": [
|
||||
1024,
|
||||
900
|
||||
],
|
||||
"viewport": [1024, 900],
|
||||
"theme": "dark",
|
||||
"state": "homepage-top"
|
||||
},
|
||||
{
|
||||
"id": "VR-003",
|
||||
"route": "/ar",
|
||||
"viewport": [
|
||||
390,
|
||||
844
|
||||
],
|
||||
"viewport": [390, 844],
|
||||
"theme": "light",
|
||||
"state": "homepage-top"
|
||||
},
|
||||
{
|
||||
"id": "VR-004",
|
||||
"route": "/ar",
|
||||
"viewport": [
|
||||
390,
|
||||
844
|
||||
],
|
||||
"viewport": [390, 844],
|
||||
"theme": "dark",
|
||||
"state": "drawer-open"
|
||||
},
|
||||
{
|
||||
"id": "VR-005",
|
||||
"route": "/en",
|
||||
"viewport": [
|
||||
390,
|
||||
844
|
||||
],
|
||||
"viewport": [390, 844],
|
||||
"theme": "light",
|
||||
"state": "demo-invalid"
|
||||
},
|
||||
{
|
||||
"id": "VR-006",
|
||||
"route": "/fr",
|
||||
"viewport": [
|
||||
768,
|
||||
1024
|
||||
],
|
||||
"viewport": [768, 1024],
|
||||
"theme": "dark",
|
||||
"state": "tour-step-3"
|
||||
},
|
||||
{
|
||||
"id": "VR-007",
|
||||
"route": "/ar",
|
||||
"viewport": [
|
||||
320,
|
||||
800
|
||||
],
|
||||
"viewport": [320, 800],
|
||||
"theme": "dark",
|
||||
"state": "reflow-gate"
|
||||
},
|
||||
{
|
||||
"id": "VR-008",
|
||||
"route": "/en",
|
||||
"viewport": [
|
||||
1440,
|
||||
1000
|
||||
],
|
||||
"viewport": [1440, 1000],
|
||||
"theme": "dark",
|
||||
"state": "pricing-and-final-cta"
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ No claim is made that users understand the product, find the CTA, or complete th
|
||||
## Pre-test findings
|
||||
|
||||
| ID | Evidence | Finding | Severity | Status |
|
||||
|---|---|---|---|---|
|
||||
| ------ | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | ----------------------------------------- |
|
||||
| P8-001 | Expert + code review | Demo submission in Phase 7 prevented default and produced no success, failure, or recovery state. | S0/C | Fixed in prototype |
|
||||
| P8-002 | Expert + code review | “Watch product tour” navigated to a workflow section rather than opening a tour, creating an expectation mismatch. | S1/C | Fixed with four-step tour |
|
||||
| P8-003 | Expert + code review | Mobile drawer moved focus initially but did not contain focus or make background content inert. | S0/C | Fixed in prototype |
|
||||
|
||||
@@ -66,14 +66,14 @@
|
||||
--duration-standard: 160ms;
|
||||
--duration-overlay: 220ms;
|
||||
--easing-standard: ease;
|
||||
--easing-productive: cubic-bezier(.2,0,0,1);
|
||||
--easing-productive: cubic-bezier(0.2, 0, 0, 1);
|
||||
--font-latin: Inter Variable, Inter, Arial, sans-serif;
|
||||
--font-arabic: Noto Sans Arabic, Arial, sans-serif;
|
||||
--breakpoint-medium: 768px;
|
||||
--breakpoint-wide: 1120px;
|
||||
}
|
||||
|
||||
html[data-theme="light"] {
|
||||
html[data-theme='light'] {
|
||||
--color-canvas: var(--color-gray-25);
|
||||
--color-surface: #ffffff;
|
||||
--color-surface-raised: #ffffff;
|
||||
@@ -91,10 +91,10 @@ html[data-theme="light"] {
|
||||
--color-on-inverse: #f8fafc;
|
||||
--color-soft-blue: #eef5ff;
|
||||
--color-soft-orange: #fff7ed;
|
||||
--shadow-color: rgba(8,20,38,.12);
|
||||
--shadow-color: rgba(8, 20, 38, 0.12);
|
||||
color-scheme: light;
|
||||
}
|
||||
html[data-theme="dark"] {
|
||||
html[data-theme='dark'] {
|
||||
--color-canvas: var(--color-navy-950);
|
||||
--color-surface: var(--color-navy-900);
|
||||
--color-surface-raised: var(--color-navy-850);
|
||||
@@ -112,11 +112,19 @@ html[data-theme="dark"] {
|
||||
--color-on-inverse: var(--color-navy-950);
|
||||
--color-soft-blue: #10284a;
|
||||
--color-soft-orange: #3a2015;
|
||||
--shadow-color: rgba(0,0,0,.36);
|
||||
--shadow-color: rgba(0, 0, 0, 0.36);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html { scroll-behavior: auto; }
|
||||
*, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; }
|
||||
html {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export const breakpoints = { medium: 768, wide: 1120 } as const;
|
||||
export const themePreferences = ["light", "dark", "system"] as const;
|
||||
export const themePreferences = ['light', 'dark', 'system'] as const;
|
||||
export type ThemePreference = (typeof themePreferences)[number];
|
||||
export type ResolvedTheme = Exclude<ThemePreference, "system">;
|
||||
export type ResolvedTheme = Exclude<ThemePreference, 'system'>;
|
||||
export const layout = { containerMaxPx: 1280, touchTargetMinPx: 44, spacingBasePx: 4 } as const;
|
||||
export const motion = { fastMs: 120, standardMs: 160, overlayMs: 220 } as const;
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
## Findings
|
||||
|
||||
| Severity | Code | Path | Line | Finding |
|
||||
|---|---|---|---:|---|
|
||||
| -------- | ------------------------------------ | -------------------------------------------------- | ---: | --------------------------------------------------------------- |
|
||||
| High | `ci_integration_suite_omitted` | `.github/workflows/ci.yml` | | CI does not execute the demo-service integration suite. |
|
||||
| High | `issue_register_overflow` | `docs/IMPLEMENTATION_ISSUE_REGISTER_v1.4.csv` | | One or more issue-register rows contain overflow columns. |
|
||||
| High | `malformed_csv` | `docs/IMPLEMENTATION_ISSUE_REGISTER_v1.4.csv` | 43 | Expected 9 columns, found 11. |
|
||||
|
||||
@@ -1 +1,6 @@
|
||||
{"command":"node scripts/run-validations.mjs","exit_code":0,"wall_seconds":1,"log":"docs/quality/phase15-command-logs/01-validate.log"}
|
||||
{
|
||||
"command": "node scripts/run-validations.mjs",
|
||||
"exit_code": 0,
|
||||
"wall_seconds": 1,
|
||||
"log": "docs/quality/phase15-command-logs/01-validate.log"
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/dev/types/routes.d.ts";
|
||||
import './.next/types/routes.d.ts';
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
Generated
+2132
-536
File diff suppressed because it is too large
Load Diff
@@ -4,8 +4,20 @@ import path from 'node:path';
|
||||
|
||||
const root = path.resolve(import.meta.dirname, '..');
|
||||
const manifestPath = path.join(root, 'PHASE_16_PACKAGE_MANIFEST_v1.0.json');
|
||||
const excludedDirectories = new Set(['.git', '.next', 'node_modules', 'coverage', 'playwright-report', 'test-results', '__pycache__']);
|
||||
const excludedFiles = new Set(['PHASE_16_PACKAGE_MANIFEST_v1.0.json', 'package-lock.json', 'tsconfig.tsbuildinfo']);
|
||||
const excludedDirectories = new Set([
|
||||
'.git',
|
||||
'.next',
|
||||
'node_modules',
|
||||
'coverage',
|
||||
'playwright-report',
|
||||
'test-results',
|
||||
'__pycache__',
|
||||
]);
|
||||
const excludedFiles = new Set([
|
||||
'PHASE_16_PACKAGE_MANIFEST_v1.0.json',
|
||||
'package-lock.json',
|
||||
'tsconfig.tsbuildinfo',
|
||||
]);
|
||||
|
||||
async function walk(directory) {
|
||||
const files = [];
|
||||
@@ -26,7 +38,11 @@ const entries = [];
|
||||
for (const file of files) {
|
||||
const contents = await readFile(file.fullPath);
|
||||
const details = await stat(file.fullPath);
|
||||
entries.push({ path: file.relative, bytes: details.size, sha256: createHash('sha256').update(contents).digest('hex') });
|
||||
entries.push({
|
||||
path: file.relative,
|
||||
bytes: details.size,
|
||||
sha256: createHash('sha256').update(contents).digest('hex'),
|
||||
});
|
||||
}
|
||||
const manifest = {
|
||||
schema_version: '1.0',
|
||||
|
||||
@@ -77,7 +77,7 @@ const integrationVisual = await readFile(
|
||||
path.join(repositoryRoot, 'tests/visual/integrations.visual.spec.ts'),
|
||||
'utf8',
|
||||
);
|
||||
if (!componentVisual.includes("hpc.theme.preference")) {
|
||||
if (!componentVisual.includes('hpc.theme.preference')) {
|
||||
failures.push('Component visual test does not use the canonical theme storage key.');
|
||||
}
|
||||
if (componentVisual.includes('rdg-theme-preference')) {
|
||||
|
||||
@@ -3,7 +3,15 @@ import path from 'node:path';
|
||||
import { fail, pass, repositoryRoot, sha256 } from './lib.mjs';
|
||||
|
||||
const manifestName = 'PHASE_16_PACKAGE_MANIFEST_v1.0.json';
|
||||
const excludedDirectories = new Set(['.git', '.next', 'node_modules', 'coverage', 'playwright-report', 'test-results', '__pycache__']);
|
||||
const excludedDirectories = new Set([
|
||||
'.git',
|
||||
'.next',
|
||||
'node_modules',
|
||||
'coverage',
|
||||
'playwright-report',
|
||||
'test-results',
|
||||
'__pycache__',
|
||||
]);
|
||||
const excludedFiles = new Set([manifestName, 'package-lock.json', 'tsconfig.tsbuildinfo']);
|
||||
|
||||
async function walk(directory) {
|
||||
@@ -22,25 +30,41 @@ async function walk(directory) {
|
||||
|
||||
const manifest = JSON.parse(await readFile(path.join(repositoryRoot, manifestName), 'utf8'));
|
||||
const failures = [];
|
||||
if (manifest.project !== 'RentalDriveGo' || manifest.phase !== 16) failures.push('Phase 16 manifest identity is invalid.');
|
||||
if (manifest.release_status !== 'Launch aborted before deployment') failures.push('Manifest release status is not the recorded Phase 16 decision.');
|
||||
if (!Array.isArray(manifest.files) || manifest.file_count !== manifest.files.length) failures.push('Manifest file count is invalid.');
|
||||
if (manifest.project !== 'RentalDriveGo' || manifest.phase !== 16)
|
||||
failures.push('Phase 16 manifest identity is invalid.');
|
||||
if (manifest.release_status !== 'Launch aborted before deployment')
|
||||
failures.push('Manifest release status is not the recorded Phase 16 decision.');
|
||||
if (!Array.isArray(manifest.files) || manifest.file_count !== manifest.files.length)
|
||||
failures.push('Manifest file count is invalid.');
|
||||
const listed = new Set();
|
||||
for (const entry of manifest.files ?? []) {
|
||||
if (typeof entry.path !== 'string' || path.isAbsolute(entry.path) || entry.path.split('/').includes('..')) {
|
||||
failures.push(`Unsafe manifest path: ${String(entry.path)}`); continue;
|
||||
if (
|
||||
typeof entry.path !== 'string' ||
|
||||
path.isAbsolute(entry.path) ||
|
||||
entry.path.split('/').includes('..')
|
||||
) {
|
||||
failures.push(`Unsafe manifest path: ${String(entry.path)}`);
|
||||
continue;
|
||||
}
|
||||
if (listed.has(entry.path)) {
|
||||
failures.push(`${entry.path}: duplicate manifest entry.`);
|
||||
continue;
|
||||
}
|
||||
if (listed.has(entry.path)) { failures.push(`${entry.path}: duplicate manifest entry.`); continue; }
|
||||
listed.add(entry.path);
|
||||
try {
|
||||
const filePath = path.join(repositoryRoot, entry.path);
|
||||
const details = await stat(filePath);
|
||||
if (!details.isFile() || details.size !== entry.bytes) failures.push(`${entry.path}: file metadata changed.`);
|
||||
if (!details.isFile() || details.size !== entry.bytes)
|
||||
failures.push(`${entry.path}: file metadata changed.`);
|
||||
if ((await sha256(filePath)) !== entry.sha256) failures.push(`${entry.path}: SHA-256 changed.`);
|
||||
} catch { failures.push(`${entry.path}: file is missing.`); }
|
||||
} catch {
|
||||
failures.push(`${entry.path}: file is missing.`);
|
||||
}
|
||||
}
|
||||
const actual = new Set(await walk(repositoryRoot));
|
||||
for (const file of actual) if (!listed.has(file)) failures.push(`${file}: unlisted repository file.`);
|
||||
for (const file of listed) if (!actual.has(file)) failures.push(`${file}: listed but excluded or absent.`);
|
||||
for (const file of actual)
|
||||
if (!listed.has(file)) failures.push(`${file}: unlisted repository file.`);
|
||||
for (const file of listed)
|
||||
if (!actual.has(file)) failures.push(`${file}: listed but excluded or absent.`);
|
||||
if (failures.length) fail(failures);
|
||||
else pass(`Phase 16 package manifest verifies ${listed.size} repository files with no extras.`);
|
||||
|
||||
@@ -16,16 +16,51 @@ const required = [
|
||||
];
|
||||
const failures = [];
|
||||
for (const file of required) {
|
||||
try { await access(path.join(repositoryRoot, file)); }
|
||||
catch { failures.push(`Missing required Phase 16 artifact: ${file}`); }
|
||||
try {
|
||||
await access(path.join(repositoryRoot, file));
|
||||
} catch {
|
||||
failures.push(`Missing required Phase 16 artifact: ${file}`);
|
||||
}
|
||||
const status = await readFile(path.join(repositoryRoot, 'docs/phase16/FINAL_LAUNCH_STATUS_v1.0.md'), 'utf8');
|
||||
const auth = await readFile(path.join(repositoryRoot, 'docs/phase16/LAUNCH_AUTHORIZATION_RECORD_v1.0.md'), 'utf8');
|
||||
const flags = await readFile(path.join(repositoryRoot, 'docs/phase16/FEATURE_FLAG_RELEASE_STATE_v1.0.csv'), 'utf8');
|
||||
const secrets = await readFile(path.join(repositoryRoot, 'docs/phase16/SECRET_PROVISIONING_RECORD_v1.0.csv'), 'utf8');
|
||||
if (!status.includes('Launch aborted before deployment')) failures.push('Final launch status is missing or inconsistent.');
|
||||
if (!auth.includes('**Rejected**')) failures.push('Launch authorization is not explicitly rejected.');
|
||||
if (/\btrue\b/.test(flags.split('\n').slice(1).map((line) => line.split(',')[1]).join('\n'))) failures.push('A Phase 16 production feature flag is enabled.');
|
||||
if (!secrets.includes('No production credentials exist') && !secrets.includes('No credential supplied')) failures.push('Secret provisioning record does not explicitly preserve missing credentials as blockers.');
|
||||
}
|
||||
const status = await readFile(
|
||||
path.join(repositoryRoot, 'docs/phase16/FINAL_LAUNCH_STATUS_v1.0.md'),
|
||||
'utf8',
|
||||
);
|
||||
const auth = await readFile(
|
||||
path.join(repositoryRoot, 'docs/phase16/LAUNCH_AUTHORIZATION_RECORD_v1.0.md'),
|
||||
'utf8',
|
||||
);
|
||||
const flags = await readFile(
|
||||
path.join(repositoryRoot, 'docs/phase16/FEATURE_FLAG_RELEASE_STATE_v1.0.csv'),
|
||||
'utf8',
|
||||
);
|
||||
const secrets = await readFile(
|
||||
path.join(repositoryRoot, 'docs/phase16/SECRET_PROVISIONING_RECORD_v1.0.csv'),
|
||||
'utf8',
|
||||
);
|
||||
if (!status.includes('Launch aborted before deployment'))
|
||||
failures.push('Final launch status is missing or inconsistent.');
|
||||
if (!auth.includes('**Rejected**'))
|
||||
failures.push('Launch authorization is not explicitly rejected.');
|
||||
if (
|
||||
/\btrue\b/.test(
|
||||
flags
|
||||
.split('\n')
|
||||
.slice(1)
|
||||
.map((line) => line.split(',')[1])
|
||||
.join('\n'),
|
||||
)
|
||||
)
|
||||
failures.push('A Phase 16 production feature flag is enabled.');
|
||||
if (
|
||||
!secrets.includes('No production credentials exist') &&
|
||||
!secrets.includes('No credential supplied')
|
||||
)
|
||||
failures.push(
|
||||
'Secret provisioning record does not explicitly preserve missing credentials as blockers.',
|
||||
);
|
||||
if (failures.length) fail(failures);
|
||||
else pass('Phase 16 no-go governance, disabled feature state, blocked approvals, and evidence package are explicit and internally consistent.');
|
||||
else
|
||||
pass(
|
||||
'Phase 16 no-go governance, disabled feature state, blocked approvals, and evidence package are explicit and internally consistent.',
|
||||
);
|
||||
|
||||
+15
-7
@@ -66,14 +66,14 @@
|
||||
--duration-standard: 160ms;
|
||||
--duration-overlay: 220ms;
|
||||
--easing-standard: ease;
|
||||
--easing-productive: cubic-bezier(.2,0,0,1);
|
||||
--easing-productive: cubic-bezier(0.2, 0, 0, 1);
|
||||
--font-latin: Inter Variable, Inter, Arial, sans-serif;
|
||||
--font-arabic: Noto Sans Arabic, Arial, sans-serif;
|
||||
--breakpoint-medium: 768px;
|
||||
--breakpoint-wide: 1120px;
|
||||
}
|
||||
|
||||
html[data-theme="light"] {
|
||||
html[data-theme='light'] {
|
||||
--color-canvas: var(--color-gray-25);
|
||||
--color-surface: #ffffff;
|
||||
--color-surface-raised: #ffffff;
|
||||
@@ -91,10 +91,10 @@ html[data-theme="light"] {
|
||||
--color-on-inverse: #f8fafc;
|
||||
--color-soft-blue: #eef5ff;
|
||||
--color-soft-orange: #fff7ed;
|
||||
--shadow-color: rgba(8,20,38,.12);
|
||||
--shadow-color: rgba(8, 20, 38, 0.12);
|
||||
color-scheme: light;
|
||||
}
|
||||
html[data-theme="dark"] {
|
||||
html[data-theme='dark'] {
|
||||
--color-canvas: var(--color-navy-950);
|
||||
--color-surface: var(--color-navy-900);
|
||||
--color-surface-raised: var(--color-navy-850);
|
||||
@@ -112,11 +112,19 @@ html[data-theme="dark"] {
|
||||
--color-on-inverse: var(--color-navy-950);
|
||||
--color-soft-blue: #10284a;
|
||||
--color-soft-orange: #3a2015;
|
||||
--shadow-color: rgba(0,0,0,.36);
|
||||
--shadow-color: rgba(0, 0, 0, 0.36);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html { scroll-behavior: auto; }
|
||||
*, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; }
|
||||
html {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,13 +10,10 @@ describe('demo API content type validation', () => {
|
||||
expect(isApplicationJson(value)).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
null,
|
||||
'',
|
||||
'text/plain',
|
||||
'application/jsonp',
|
||||
'application/json-patch+json',
|
||||
])('rejects %s', (value) => {
|
||||
it.each([null, '', 'text/plain', 'application/jsonp', 'application/json-patch+json'])(
|
||||
'rejects %s',
|
||||
(value) => {
|
||||
expect(isApplicationJson(value)).toBe(false);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user