@santi020k/lumen-mcp 4.2.0 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/data/lumen-data.json +36 -36
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +11 -10
- package/package.json +5 -5
package/data/lumen-data.json
CHANGED
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
"PasswordField": "784fb8457944c5d6879a3ed1db154de8e14cae7eec3ec3ef38c3b174dd1222b2",
|
|
96
96
|
"PhoneInput": "2b10c9b1f6efc47e9348f6996397e3b5250f4d49166ec55987c89a51396c65b4",
|
|
97
97
|
"PieChart": "8ec3d6d45f391f05c7946a755865c8a68125e4882ab68e132bd87196d4dc59c9",
|
|
98
|
-
"Pill": "
|
|
98
|
+
"Pill": "a6d339985f1fc685c806610c163e3a86e32a34e381f90c0bce29ef3298b17d03",
|
|
99
99
|
"Popover": "91a1fc3fdc5b14b7c01fa5c9393d2b2257ed6da9b97977089f01e8643c0c1080",
|
|
100
100
|
"Progress": "42a0a7e700806ff8c31dc793cf4e79fa62b59b6dd310571bcd775d034680081c",
|
|
101
101
|
"Prose": "36e61f7f8b4a6c74a2a716a73dbf22cb4f86fae092b1518aa0a0e68765cea68d",
|
|
@@ -135,8 +135,8 @@
|
|
|
135
135
|
"LanguageToggle": "ff65170382ae5b1241ce142b670a32bb708e92fbfbeeac6a285bd39b13c612b6",
|
|
136
136
|
"Particles": "74334a8aa6578b86f01da2ab63a96ce6c8d888e99f3bcc3ab9845667eb97d27e",
|
|
137
137
|
"AnimatedNumber": "78cebe9ac425f1f2ab81c336ccaa166ff2007d1814398f451ca67702742aaa5a",
|
|
138
|
-
"RevealGroup": "
|
|
139
|
-
"ScrollReveal": "
|
|
138
|
+
"RevealGroup": "15479160d7c04dcaed2b29e4711ce75c05d5a8e3256cea89a0d4b33f714aa71c",
|
|
139
|
+
"ScrollReveal": "05b35a3412cb49d5d39be1ca6d04b0e75665bb218ef703d88bc6a9f5b227a140",
|
|
140
140
|
"Stat": "24d2d45bd11ec7dc2dcc1b00475f095c943d38b240acecfa32bb399c9c3398bb",
|
|
141
141
|
"StatDescription": "bcaea522ed8065b705e55ca8e8deef23a98ba3b28156a37015628c88de5e3ced",
|
|
142
142
|
"StatIcon": "30ac2c9950be183bd45d9d65d3954c1796e34af3d74642d8a629954d809906c0",
|
|
@@ -156,7 +156,7 @@
|
|
|
156
156
|
"Stepper": "681398765cf6b7c4a331a28d9acfc64a2a7b7c1839a4fb75b2e15c6f24ff7fca",
|
|
157
157
|
"FileUpload": "692a269733e740c7041b91e78ba696e23df8dec2e1cf1b5bff495d4c5c4d7368",
|
|
158
158
|
"Tour": "80e8e76b43dd906ea5d6eb1e2fd39f17b65f53c7c280a6916fa1b22adccf9137",
|
|
159
|
-
"Anchor": "
|
|
159
|
+
"Anchor": "26fa3998d660e1f66f0d41cdd499e2ebf1368bbe82cc506df7903060275003f5",
|
|
160
160
|
"Segmented": "59f98029399c5e3381d6f7d919237a140257e01cc1157259d8bef7ca58072aca",
|
|
161
161
|
"Toolbar": "4eceec970e9a4642d886a8f0d6ffb0849fa0d26892145a69bbc43d40243479b3",
|
|
162
162
|
"Descriptions": "54be8cc7f8269a8a078f7a266abe9ad8e3e52a466c9396ad60e06c36e54728af",
|
|
@@ -261,8 +261,8 @@
|
|
|
261
261
|
"Navigation bar accessory": "e17581eb9fdcf5fafb9ed675f8ba6fca90f028a2ee849b7dd3c19003bb147502",
|
|
262
262
|
"Adaptive navigation scaffold": "0a5bbcf6d0d5e1bd3e09c3d1eccafcfee3512cf0a2741c5226f796ffdeb28321",
|
|
263
263
|
"Refresh control": "c43c9cb9ed98493db3811356b54188c9893dfc325097a609f4c9041f57ce215d",
|
|
264
|
-
"Date field": "
|
|
265
|
-
"Date range field": "
|
|
264
|
+
"Date field": "5a64cf155b5f34b145823ffe37b076336afea27d26f0fc010398e125249dcb9a",
|
|
265
|
+
"Date range field": "92110b19e7832b853a2ea149587557c3265684c3c8a3a4df22009a66bf642134",
|
|
266
266
|
"Floating action button": "e01ae3b6ce915553a0948d418894723576454e7fd231464d936b98e0bfd0dc04",
|
|
267
267
|
"Widget text": "932a18da567e7588ffde1c5f4c6ab6ae459ada0428b2a10a366a7e7167f9b263",
|
|
268
268
|
"Widget icon": "d12fb76b8df12e61bf969259235ec382e5b1cad74ad57a900f52081e8156aab6",
|
|
@@ -22234,7 +22234,7 @@
|
|
|
22234
22234
|
{
|
|
22235
22235
|
"attribute": "count",
|
|
22236
22236
|
"defaultValue": "-",
|
|
22237
|
-
"description": "Appends a
|
|
22237
|
+
"description": "Appends a smaller count affix that stays together when the label wraps in a narrow container.",
|
|
22238
22238
|
"values": "number | string"
|
|
22239
22239
|
},
|
|
22240
22240
|
{
|
|
@@ -31444,7 +31444,7 @@
|
|
|
31444
31444
|
}
|
|
31445
31445
|
],
|
|
31446
31446
|
"propsExtends": "ComponentPropsWithoutRef<'div'>",
|
|
31447
|
-
"source": "export interface RevealGroupProps extends ComponentPropsWithoutRef<'div'> {\n animation?: MotionAnimation\n delay?: number\n duration?: MotionDuration\n once?: boolean\n stagger?: number\n threshold?: number\n}\n\nexport const RevealGroup = ({\n animation = 'slide-up',\n className,\n delay = 0,\n duration = 'slow',\n once = true,\n stagger = 80,\n style,\n threshold = 0.15,\n ...props\n}: RevealGroupProps) => {\n const rootRef = useRef<HTMLDivElement>(null)\n\n useEffect(() => {\n const root = rootRef.current\n\n if (!root) return\n\n root.dataset.uiScrollRevealBound = 'true'\n\n for (const [index, child] of [...root.children].entries()) {\n if (child instanceof HTMLElement) {\n child.style.setProperty('--ui-reveal-index', String(index))\n }\n }\n\n if (\n window.matchMedia('(prefers-reduced-motion: reduce)').matches ||\n typeof IntersectionObserver === 'undefined'\n ) {\n root.classList.add('is-revealed')\n\n return\n }\n\n const observer = new IntersectionObserver(\n
|
|
31447
|
+
"source": "export interface RevealGroupProps extends ComponentPropsWithoutRef<'div'> {\n animation?: MotionAnimation\n delay?: number\n duration?: MotionDuration\n once?: boolean\n stagger?: number\n threshold?: number\n}\n\nexport const RevealGroup = ({\n animation = 'slide-up',\n className,\n delay = 0,\n duration = 'slow',\n once = true,\n stagger = 80,\n style,\n threshold = 0.15,\n ...props\n}: RevealGroupProps) => {\n const rootRef = useRef<HTMLDivElement>(null)\n\n useEffect(() => {\n const root = rootRef.current\n\n if (!root) return\n\n root.dataset.uiScrollRevealBound = 'true'\n\n for (const [index, child] of [...root.children].entries()) {\n if (child instanceof HTMLElement) {\n child.style.setProperty('--ui-reveal-index', String(index))\n }\n }\n\n if (\n window.matchMedia('(prefers-reduced-motion: reduce)').matches ||\n typeof IntersectionObserver === 'undefined'\n ) {\n root.classList.add('is-revealed')\n\n return\n }\n\n const observer = new IntersectionObserver(\n entries => {\n const entry = entries.at(-1)\n\n if (!entry) return\n\n root.classList.toggle('is-revealed', entry.isIntersecting)\n\n if (entry.isIntersecting && once) observer.disconnect()\n }, { threshold: Math.min(1, Math.max(0, threshold)) }\n )\n\n observer.observe(root)\n\n return () => {\n observer.disconnect()\n }\n }, [once, threshold])\n\n return (\n <div\n className={composeClassName(\n 'ui-reveal-group', `ui-reveal-group-${animation}`, `ui-motion-duration-${duration}`, className\n )}\n data-ui-reveal-group\n data-ui-reveal-once={String(once)}\n data-ui-reveal-threshold={Math.min(1, Math.max(0, threshold))}\n ref={rootRef}\n style={\n {\n ...style,\n '--ui-reveal-delay': `${Math.max(0, delay)}ms`,\n '--ui-reveal-stagger': `${Math.max(0, stagger)}ms`\n } as CSSProperties\n }\n {...props}\n />\n )\n}",
|
|
31448
31448
|
"styleImport": "import '@santi020k/lumen-react/styles.css'"
|
|
31449
31449
|
}
|
|
31450
31450
|
},
|
|
@@ -31715,7 +31715,7 @@
|
|
|
31715
31715
|
}
|
|
31716
31716
|
],
|
|
31717
31717
|
"propsExtends": "ComponentPropsWithoutRef<'div'>",
|
|
31718
|
-
"source": "export interface ScrollRevealProps extends ComponentPropsWithoutRef<'div'> {\n animation?: MotionAnimation\n delay?: number\n duration?: MotionDuration\n once?: boolean\n threshold?: number\n}\n\nexport const ScrollReveal = ({\n animation = 'fade',\n className,\n delay = 0,\n duration = 'slow',\n once = true,\n style,\n threshold = 0.15,\n ...props\n}: ScrollRevealProps) => {\n const rootRef = useRef<HTMLDivElement>(null)\n\n useEffect(() => {\n const root = rootRef.current\n\n if (!root) return\n\n root.dataset.uiScrollRevealBound = 'true'\n\n if (\n window.matchMedia('(prefers-reduced-motion: reduce)').matches ||\n typeof IntersectionObserver === 'undefined'\n ) {\n root.classList.add('is-revealed')\n\n return\n }\n\n const observer = new IntersectionObserver(\n
|
|
31718
|
+
"source": "export interface ScrollRevealProps extends ComponentPropsWithoutRef<'div'> {\n animation?: MotionAnimation\n delay?: number\n duration?: MotionDuration\n once?: boolean\n threshold?: number\n}\n\nexport const ScrollReveal = ({\n animation = 'fade',\n className,\n delay = 0,\n duration = 'slow',\n once = true,\n style,\n threshold = 0.15,\n ...props\n}: ScrollRevealProps) => {\n const rootRef = useRef<HTMLDivElement>(null)\n\n useEffect(() => {\n const root = rootRef.current\n\n if (!root) return\n\n root.dataset.uiScrollRevealBound = 'true'\n\n if (\n window.matchMedia('(prefers-reduced-motion: reduce)').matches ||\n typeof IntersectionObserver === 'undefined'\n ) {\n root.classList.add('is-revealed')\n\n return\n }\n\n const observer = new IntersectionObserver(\n entries => {\n const entry = entries.at(-1)\n\n if (!entry) return\n\n root.classList.toggle('is-revealed', entry.isIntersecting)\n\n if (entry.isIntersecting && once) observer.disconnect()\n }, { threshold: Math.min(1, Math.max(0, threshold)) }\n )\n\n observer.observe(root)\n\n return () => {\n observer.disconnect()\n }\n }, [once, threshold])\n\n return (\n <div\n className={composeClassName(\n 'ui-scroll-reveal', `ui-scroll-reveal-${animation}`, `ui-motion-duration-${duration}`, className\n )}\n data-ui-reveal-once={String(once)}\n data-ui-reveal-threshold={Math.min(1, Math.max(0, threshold))}\n data-ui-scroll-reveal\n ref={rootRef}\n style={\n {\n ...style,\n '--ui-reveal-delay': `${Math.max(0, delay)}ms`\n } as CSSProperties\n }\n {...props}\n />\n )\n}",
|
|
31719
31719
|
"styleImport": "import '@santi020k/lumen-react/styles.css'"
|
|
31720
31720
|
}
|
|
31721
31721
|
},
|
|
@@ -35687,7 +35687,7 @@
|
|
|
35687
35687
|
{
|
|
35688
35688
|
"attribute": "activationOffset",
|
|
35689
35689
|
"defaultValue": "96",
|
|
35690
|
-
"description": "
|
|
35690
|
+
"description": "Minimum viewport-top activation offset in pixels. Native root scroll padding plus heading scroll margin can move the line lower; one CSS pixel of rounding is tolerated.",
|
|
35691
35691
|
"values": "number"
|
|
35692
35692
|
},
|
|
35693
35693
|
{
|
|
@@ -46091,11 +46091,11 @@
|
|
|
46091
46091
|
}
|
|
46092
46092
|
],
|
|
46093
46093
|
"docs": {
|
|
46094
|
-
"aiUsage": "# AI Usage Guide\n\nUse this guide when an AI agent is building an app with Lumen or updating examples that teach\nanother agent how to use the library.\n\nLumen 4 is released. See the [v4.2 highlights](lumen-4.2.md) and use the\n[release manifest](../registry/release-manifest.json) for each package's version; companion packages\ndo not all share the umbrella version. The public plugin and hosted MCP describe their deployed\nsnapshot, which can differ from this checkout. Inspect snapshot metadata before using its contracts.\n\n## Install in ChatGPT or Codex\n\nThe [Lumen UI plugin](https://chatgpt.com/plugins/plugin_asdk_app_6a8f6c526c5481918eb8a48806fa112b)\nis published in the shared Plugins Directory for ChatGPT and Codex.\n\n1. Open the listing and select **Install plugin**. Sign in to ChatGPT if prompted.\n2. Start a request with **@Lumen UI**, or choose one of the listing's starter prompts.\n3. Describe the interface and target framework, for example: “Find the right Lumen components for\n an accessible React settings screen.”\n\nThe plugin combines the Lumen workflow skill with the hosted public MCP catalog. It requires no\nseparate Lumen account, API key, or local server. Its tools only read public Lumen documentation;\nthey do not access private repositories or modify files and services. Your coding agent performs\nimplementation work using the retrieved contracts and its own permissions.\n\nSee the [publication record](./openai-plugin-submission.md) for version and verification details.\n\n## Install the Lumen Agent Skill\n\nLumen ships a portable Agent Skill for AI coding tools that support the open `SKILL.md` format:\n\n```bash\nnpx skills add santi020k/lumen --skill lumen-ui\n```\n\nThe source lives in [`skills/lumen-ui`](../skills/lumen-ui). The skill provides the durable\nframework-selection, component-composition, theming, accessibility, and verification workflow.\nUse it by naming `$lumen-ui` in a prompt, or let a compatible agent trigger it from Lumen-specific\nrequests.\n\nThe skill and MCP server complement each other: the skill explains how to work, while\n`@santi020k/lumen-mcp` supplies structured catalog search, framework-specific contracts, recipes,\nsource, tokens, resources, and rules.\n\n## Web selection controls\n\nUse `Select` as the default for web product option lists, and `Combobox` when the user needs to\nsearch the choices. `NativeSelect` remains supported for a product requirement that explicitly\ncalls for the browser or platform picker. Do not remove native fallbacks from custom controls.\n\nMount the matching framework behavior before verifying the result: Astro uses `UIPrimitives`,\nReact uses the public Select behavior hook, and Elements uses registered custom elements. Check\nthat opening the control shows the enhanced list, that keyboard selection and Escape work, and\nthat dependent choices, forms, mobile layouts, and both appearances remain correct.\n\nFor personal projects that require custom lists, enforce the choice in ESLint as well as agent\nguidance. The prepared `lumenCustomSelects()` helper in `@santi020k/eslint-config-libraries` rejects\n`NativeSelect` imports (including aliases), namespace JSX usage, raw JSX selects, and JSX native\nselect custom elements without replacing the project's existing syntax restrictions. Enable it\nonly after that package version is available. Keep intentional native-control exceptions scoped\nto specific files with a documented product reason. Library source and fallback implementations\nshould not opt into a consumer-only policy.\n\n## Pick the Right Package\n\nUse the framework requested by the user. Every adapter shares the same Lumen foundation.\n\n| App Target | Install | Import Components From | Load Styles From |\n| -------------------------- | ---------------------------------------------------------------------- | -------------------------------------------- | -------------------------------------- |\n| Astro | `@santi020k/lumen-astro` | `@santi020k/lumen-astro` | `@santi020k/lumen-astro/styles.css` |\n| React | `@santi020k/lumen-react` | `@santi020k/lumen-react` | `@santi020k/lumen-react/styles.css` |\n| React Hook Form composites | `@santi020k/lumen-react-hook-form` | `@santi020k/lumen-react-hook-form` | Uses React styles |\n| Web Components | `@santi020k/lumen-elements` | `@santi020k/lumen-elements/define` | `@santi020k/lumen-elements/styles.css` |\n| React Native / Expo | `@santi020k/lumen-react-native` | `@santi020k/lumen-react-native` | Not applicable |\n| Apple / SwiftUI | Swift Package `https://github.com/santi020k/lumen`, pinned to `4.2.0` | `LumenUI` | Not applicable |\n| Android / Compose | Maven Central `com.santi020k:lumen-compose:4.0.0` | `com.santi020k.lumen` | Not applicable |\n| Wear OS / Compose | Maven Central `com.santi020k:lumen-compose-wear:4.0.0` | `com.santi020k.lumen` | Not applicable |\n| Package metadata | `@santi020k/lumen-core` | `@santi020k/lumen-core` | Not applicable |\n| Optional web brand icons | `@santi020k/lumen-icons-brand` | Register once, then use the framework `Icon` | Uses framework styles |\n\nEach framework package includes the shared foundation. Install `@santi020k/lumen` separately only\nwhen you need its framework-neutral CLI or registry metadata.\n\nFor React Native, mount one `LumenProvider` near the app root. For SwiftUI, attach the `LumenUI`\nSwift Package product to the application target, use an exact or compatible release-version rule,\nand apply `.lumenTheme(...)` near the root. For Compose, install\n`com.santi020k:lumen-compose:4.0.0` from Maven Central and wrap content in `LumenTheme`. Native\nadapters do not load CSS or the Astro runtime.\n\nReact Native can use ordinary React hooks and platform-neutral application hooks. Import native\nLumen controllers such as `useDialog`, `useTabs`, `useSelect`, `useLanguageToggle`,\n`useThemeToggle`, and `useToast` from `@santi020k/lumen-react-native`; their state semantics track\nthe reusable React contracts while their returned props target native Lumen components. Never\nimport DOM-dependent hooks from `@santi020k/lumen-react` into a native bundle. Share business state\nin an application workspace package and keep the Lumen rendering adapter at each app boundary.\n\nFor watchOS, use the focused `LumenWatch*` contracts from the same Swift package. For Wear OS,\ninstall `com.santi020k:lumen-compose-wear:4.0.0` and wrap wearable content in `LumenWearTheme`\ninside the application-owned Wear Material theme. Keep complications, tiles, rotary input,\nAlways On behavior, haptics, synchronization, navigation, and health or safety logic app-owned.\n\nBrand marks are intentionally excluded from the default web Lucide bundle. In Astro, React, or Web\nComponents, install and register the optional pack once before rendering a namespaced icon:\n\n```ts\nimport { registerLumenBrandIcons } from \"@santi020k/lumen-icons-brand\";\n\nregisterLumenBrandIcons();\n```\n\nThen use any canonical Font Awesome Free brand name, such as `brand:github`, `brand:discord`,\n`brand:figma`, `brand:linkedin`, `brand:whatsapp`, or `brand:youtube`, through the existing Astro,\nReact, or Web Component `Icon` API. The package also provides `brand:x` as an alias for\n`brand:x-twitter`.\n\nBrand marks keep their recognizable filled geometry instead of being redrawn as Lucide outlines.\nThey still inherit the same Lumen size, alignment, `currentColor`, and accessible-label behavior as\nthe default icon catalog. React Native, SwiftUI, and Compose already include the corresponding\ncomplete brand catalog and accept `brand:github`, `.brandGithub`, and\n`LumenIconName.BrandGithub`, respectively, without a separate package.\n\n## Consumer composition in v4\n\n- For reporting filters, use React `DateRangeInput` with controlled `value`/`onValueChange`, localized\n `labels`, bounds and explicit presets. Keep draft changes inside the picker until Apply. See the\n [reporting example](https://lumen.santi020k.com/docs/web/reporting) and [migration guide](migrating-to-lumen.md).\n- Keep date-only values as validated `YYYY-MM-DD` strings. Do not round-trip a local date through\n UTC timestamps. Use the native adapter's existing date field and application-owned calendar/time-zone rules.\n- Compose a metric card from `Card`, `Stat` and a chart with `presentation=\"bare\"`; do not invent\n MetricCard or Status components. Products own status labels, aggregation, currency units and rounding.\n- Use unique chart X identities, abbreviated `xLabel` text and full `formatCategory` labels.\n Preserve absent observations explicitly; never turn missing data into a fabricated zero.\n- Compare two media treatments with `ImageComparison` and matching source dimensions. Supply a\n visible localized control label and useful image alternatives. See [image comparison](image-comparison.md).\n- Use `Prose` for ordinary Markdown, `Code`/`CodeTabs` for examples, and `CopyButton` for dynamic\n command output. Localize clipboard success/error feedback; do not swallow rejected writes.\n- Use public `asChild` composition when a semantic link needs button styling. Disable nested form\n controls separately. On Input, native numeric `size` and Lumen `visualSize` have different meanings.\n- Map the complete semantic theme in light and dark modes, including `brand-solid`/`on-brand` and\n `danger`/`on-danger`. Text-safe colors matter more than matching a decorative swatch.\n- NavigationMenu, Sidebar and Sheet compose a documentation shell. Keep ordinary links in normal\n Tab order and application routing in the app. Do not rebuild primitives with `ui-*` classes.\n\n### Theme bootstrap and generated command output\n\nWhen the application owns theme state, use `ThemeToggle controlled` and its own click handler.\nInitialize the document in the head before paint; unavailable storage must not prevent rendering:\n\n```html\n<script>\n let theme = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'\n try {\n const saved = localStorage.getItem('application-theme')\n if (saved === 'light' || saved === 'dark') theme = saved\n } catch {\n // Private browsing or storage policy can deny access.\n }\n document.documentElement.dataset.theme = theme\n document.documentElement.classList.toggle('dark', theme === 'dark')\n</script>\n```\n\nThe owner's click handler updates the document and the toggle's `aria-pressed` state, and persists\ninside a guarded storage write. Use a server-rendered theme when the application already has one.\nA controlled Lumen toggle does not read storage or change that state itself.\n\nFor commands generated from a form, point `CopyButton` at the current output element. It reads the\nlatest text when clicked, so the application only updates the output rather than managing clipboard\npromises or feedback timers:\n\n```astro\n---\nimport { CopyButton } from '@santi020k/lumen-astro'\n---\n\n<output id=\"generated-command\">pnpm run check</output>\n<CopyButton target=\"#generated-command\" label=\"Copy command\" copiedLabel=\"Command copied\" errorLabel=\"Copy failed; select the command and copy it manually\">\n Copy command\n</CopyButton>\n```\n\n## MCP-Assisted Workflow\n\nConnect `@santi020k/lumen-mcp` when the agent supports Model Context Protocol. The server returns\nvalidated structured content as well as readable text, so agents can select components without\nscraping documentation.\n\nThe MCP per-component catalog covers Astro, React, Web Components, React Native, SwiftUI, and\nCompose. Use `lumen_list_native_components` to discover platform availability and\n`lumen_get_native_component` for installation, setup, API, examples, accessibility guidance, and\noptional reference source. The skill still verifies installed packages when local versions differ.\n\n```bash\ncodex mcp add lumen -- npx -y @santi020k/lumen-mcp\n```\n\nFor Codex project configuration:\n\n```toml\n[mcp_servers.lumen]\nenabled = true\ncommand = \"npx\"\nargs = [\"-y\", \"@santi020k/lumen-mcp\"]\n```\n\nUse this sequence:\n\n1. Read `lumen://meta` and `lumen://diagnostics` to identify and verify the snapshot.\n2. Read `lumen://rules`.\n3. Search for the use case with `lumen_search`, passing the target `framework` when known.\n4. Inspect the selected framework with `lumen_get_component` and `detail: \"usage\"`.\n5. Follow its framework behavior setup and hook/controller contract.\n6. Read related multi-component guidance with `lumen_get_recipe`.\n7. Request `detail: \"source\"` only when the usage contract is insufficient.\n8. Read `lumen://tokens` before introducing custom styling.\n9. Retain `lumen://catalog-manifest` and use `lumen_diff_catalog` after upgrades to identify contracts that need refreshing.\n\nExample framework-aware component request:\n\n```json\n{\n \"name\": \"lumen_get_component\",\n \"arguments\": {\n \"name\": \"DateRangePicker\",\n \"framework\": \"react\",\n \"detail\": \"usage\"\n }\n}\n```\n\nStable resources are available at `lumen://rules`, `lumen://tokens`, `lumen://components`,\n`lumen://meta`, `lumen://catalog-manifest`, `lumen://diagnostics`,\n`lumen://components/{name}`, and `lumen://recipes/{name}`. See\n[`packages/mcp/README.md`](../packages/mcp/README.md) for Claude Desktop, Cursor, generic stdio,\nlocal repository, and programmatic examples.\n\n## Minimal Setup\n\nImport the stylesheet once in the app's global CSS, root layout, or app entry.\n\nThis section applies to web targets. Native adapters use their provider or theme APIs instead of a\nstylesheet.\n\n```css\n@import \"@santi020k/lumen-react/styles.css\";\n```\n\nIf Tailwind is present, keep both imports in the same shared CSS entry.\n\n```css\n@import \"@santi020k/lumen-react/layers.css\";\n@import \"tailwindcss\";\n@import \"@santi020k/lumen-react/styles.css\";\n```\n\nKeep the layer prelude first. It places Tailwind base styles below Lumen components and Tailwind\nutilities above them, so consumer spacing, display, radius, width, and responsive visibility\nclasses override Lumen defaults predictably.\n\nAstro apps should mount `UIPrimitives` once in the root layout when using interactive primitives\nsuch as dialogs, menus, popovers, tabs, carousels, command lists, toggles, validated forms, or\ntoasts. Do not place `UIPrimitives` in each component example or next to each primitive instance;\none root include enhances all matching `data-ui-*` markup on the page.\n\n```astro\n---\nimport UIPrimitives from '@santi020k/lumen-astro/runtime'\n---\n\n<UIPrimitives />\n```\n\n## Astro\n\nUse direct named imports from `@santi020k/lumen-astro`.\n\n```astro\n---\nimport { Button, Card, Input, Label } from '@santi020k/lumen-astro'\n---\n\n<Card>\n <Label for=\"email\">Email</Label>\n <Input id=\"email\" type=\"email\" placeholder=\"you@example.com\" />\n <Button type=\"submit\">Subscribe</Button>\n</Card>\n```\n\n`Stat` defaults to a `div`. Choose `as=\"article\"` for a self-contained metric with supporting\ncontent, or `as=\"section\"` for a labeled region. Do not use a semantic root solely for styling.\nIts visual variants are `default`, `accent`, and `glass`; reserve `accent` for featured metrics and\nkeep glass selective.\n\nFor long reading surfaces, use `Anchor` for local section navigation and pass each item’s optional\nheading `depth`, `index`, and `description` when hierarchy matters. Use `activationOffset` for fixed\napplication headers. Use `ScrollProgress` for document position; keep `Progress` for\napplication-controlled task completion. In Astro, dispatch `ui:progress-change` with\n`{ value, max? }` from the `Progress` root when browser state changes after hydration. Non-finite or\nmissing fields keep the current state, and every adapter clamps to finite ARIA values. Use `Spinner`\nfor unknown-duration waits.\n\nSkeleton and Spinner are decorative (`aria-hidden`) unless named. For a group of placeholders,\nannounce loading once: wrap them in one `role=\"status\"` region with `aria-busy=\"true\"` and a\n`VisuallyHidden` label, and keep the Skeletons unnamed. Size placeholders with `shape`\n(`rectangle`, `text`, `circle`), `width`, and `height`, and use deterministic widths rather than\nrandom values so server and client markup match. Data fetching and when to show placeholders stay\nin the application. `Pill`\naccepts `neutral`, `brand`, and `outline`\nvariants, plus native link and count composition.\n\n## React\n\nReact components share the same visual classes and data attributes as Astro components. For\nbehavior-heavy primitives, use the React behavior hooks from `@santi020k/lumen-react` (`useDialog`,\n`usePopover`, `useDropdownMenu`, `useTabs`, `useSelect`, `useToast`, and `useTooltip`), which track\nthe Astro runtime's ARIA, keyboard, Escape, dismissal, and toast controller semantics.\n\nThe React package entry is a client boundary. In Next.js, it can be imported by a Server Component\nwithout evaluating Lumen's contexts and hooks in the React server runtime; the rendered Lumen\nsubtree becomes a Client Component boundary. Vite and other client-rendered React applications use\nthe same entry without additional setup.\nFor stateless Badge, Card composition, layout, form-label, progress, loading, typography, and\nvisually-hidden usage in a React Server Component, import from\n`@santi020k/lumen-react/server`. That entrypoint contains the implementations directly and avoids\ncreating a client boundary. Keep event handlers, hooks, and interactive primitives on the package\nroot inside a Client Component.\n`Button`, `Link`, `ButtonLink`, and `Input` accept React 19 DOM refs directly as props.\n`Button asChild` composes the button contract onto one child element. Across adapters, native\nnumeric `size` remains available on `Input` and `NativeSelect`; use `visualSize` in Astro and React,\nor `visual-size` in Elements, for presentation.\n`Stat as` and `variant` follow the same semantic-root and visual guidance as the Astro component.\n\n```tsx\nimport \"@santi020k/lumen-react/styles.css\";\nimport { Button, Card, Input, Label } from \"@santi020k/lumen-react\";\n\nexport function SubscribeForm() {\n return (\n <Card>\n <Label htmlFor=\"email\">Email</Label>\n <Input id=\"email\" type=\"email\" placeholder=\"you@example.com\" />\n <Button type=\"submit\">Subscribe</Button>\n </Card>\n );\n}\n```\n\nFor React Hook Form, spread `register()` directly onto native-backed controls. Install\n`@santi020k/lumen-react-hook-form` only for controlled composites such as `Select`, `DatePicker`,\n`InputOTP`, and `ListBox`. Do not combine React Hook Form and `useFormValidation` unless the\napplication intentionally coordinates two validation sources. For schema validation, use\n`zodResolver` or `yupResolver` from `@hookform/resolvers`; keep Zod or Yup in the application and\nrender the resulting React Hook Form errors through Lumen's `FieldError` contract.\n\n## Web Components\n\nRegister custom elements once, then use `lumen-*` tags in HTML. The elements adapter includes\ninteractive behavior parity for DataTable, Dialog, Popover, DropdownMenu, ContextMenu, Tabs,\nSelect, ThemeBuilder, Toast, Tooltip, forms, RichTextEditor, Schedule, and VirtualList: selection,\nvalidation, rich text command, context menu, schedule drag/drop, theme, and range events, ARIA\nstate, roving/listbox keyboard paths, Escape/outside dismissal, focus return/trapping, native\nselect form participation, and the same toast controller events as the Astro runtime.\n\n```html\n<script type=\"module\">\n import { defineLumenElements } from \"@santi020k/lumen-elements/define\";\n\n defineLumenElements();\n</script>\n\n<lumen-card>\n <label for=\"email\">Email</label>\n <lumen-input\n id=\"email\"\n type=\"email\"\n placeholder=\"you@example.com\"\n ></lumen-input>\n <lumen-button type=\"submit\">Subscribe</lumen-button>\n</lumen-card>\n```\n\nFor a surface that uses only Badge, Button, Card, and Combobox, import\n`@santi020k/lumen-elements/components/badge`, `/button`, `/card`, and `/combobox` and call their\ncorresponding `defineLumenBadge`, `defineLumenButton`, `defineLumenCard`, and `defineLumenCombobox`\nfunctions. Those implementation-level entrypoints avoid the complete catalog bundle; the Combobox\nentrypoint includes its filtering and keyboard behavior. Continue using `defineLumenElements` for\nany component without a granular entrypoint.\n\nFor a foundation-only custom-element surface, import `defineLumenFoundations` from\n`@santi020k/lumen-elements/components/foundations`. It registers Card compound parts, Container,\nDirection, Grid, Label, Separator, Skeleton, Spinner, Stack, Typography, and VisuallyHidden without\npulling the complete catalog implementation.\n\nPut input elements inside a native `<form data-ui-form>`. Scalar Lumen controls participate in\n`FormData`, validity, disabled state, focus, and reset through form-associated custom-element\nbehavior with a native-control fallback. Do not generate `<lumen-form>` as the submission\ncontainer.\n\n## Component Selection\n\nThe shared catalog includes:\n\n`Accordion`, `Alert`, `AlertDialog`, `Agenda`, `AspectRatio`, `Attachment`, `Autocomplete`,\n`Avatar`, `Badge`, `BarChart`, `Breadcrumb`, `Bubble`, `Button`, `ButtonGroup`, `Calendar`, `Card`,\n`Carousel`, `Chart`, `Checkbox`, `CheckboxGroup`, `Collapsible`, `Code`, `Combobox`, `Command`,\n`Container`, `ColorPicker`,\n`ContextMenu`, `DataTable`, `DatePicker`, `DateRangePicker`, `Dialog`, `Direction`, `Drawer`,\n`DropdownMenu`, `Empty`, `ErrorState`, `ErrorSummary`, `Field`, `FieldError`, `Form`, `Grid`, `HoverCard`, `Icon`,\n`Graphic`, `Backdrop`, `Illustration`, `Input`, `InputGroup`, `InputOTP`, `Item`, `KanbanBoard`, `KanbanColumn`, `Kbd`, `Label`, `LineChart`, `ListBox`, `Marker`, `Menubar`,\n`Message`, `MessageScroller`, `NativeSelect`, `ContextNavigation`, `NavigationMenu`, `NumberField`, `Pagination`,\n`PasswordField`, `PieChart`, `Popover`, `Progress`, `CopyButton`, `RadioGroup`, `Resizable`, `RichTextEditor`,\n`ScrollArea`, `Schedule`, `SearchField`, `Select`, `Separator`, `Sheet`, `Sidebar`, `Skeleton`,\n`Slider`, `Sparkline`, `Spinner`, `Stack`, `Switch`, `Table`, `Tabs`, `TagGroup`, `Textarea`, `ThemeBuilder`,\n`TimeField`, `Toast`, `Toggle`, `ToggleGroup`, `Tooltip`, `Tree`, `TreeGrid`, `Typography`, and\n`VirtualList`, and `VisuallyHidden`.\n\n## Product Recipes\n\n- Follow [the error-handling guide](./error-handling.md) when a generated interface needs failure\n feedback. Use `FieldError` and `ErrorSummary` for validation, `Alert` for persistent inline\n problems, `Toast` for brief non-blocking failures, and `ErrorState` only when a page or region\n cannot show its primary content. Keep exception capture, logging, retry policy, and safe message\n mapping in application code. For React Native, SwiftUI, and Compose, follow the guide's native API\n mapping and announcement rules; do not invent a cross-platform error manager inside the component\n layer.\n\n- Start full product surfaces from the bundled `analytics-dashboard`, `saas-admin`,\n `commerce-dashboard`, `project-workspace`, or `auth-onboarding` recipe. Install with\n `lumen add <recipe>` for Astro, or add `--target react` / `--target elements`. Keep the generated\n shared template stylesheet, then replace sample data and routes while preserving public Lumen\n components, semantic tokens, accessible names, focus paths, and responsive navigation.\n- Use `Sparkline` for a compact trend beside a labelled metric, `BarChart` for categorical\n comparison, `LineChart` for ordered or time-series data, `PieChart` for a small\n part-to-whole breakdown. Pass already-aggregated `LumenChartSeries` data; keep statistics,\n resampling, model evaluation, and domain calculations in application code. Keep `showTable`\n enabled unless the same values are already available in a nearby semantic table. Prefer a bar\n chart when precise slice comparison matters or the pie would exceed roughly seven categories.\n Use `ScatterChart` for numeric relationships, `Heatmap` for a labeled matrix, `RangeChart` for\n low-to-high intervals, and `ComboChart` only when mixed marks share a meaningful domain.\n Use `WaterfallChart` for signed balance changes, `LollipopChart` for ranking, `DumbbellChart`\n for reference/current comparison, and `BulletChart` for actual/target comparison. Use `Histogram`\n for supplied bins, `BoxPlot` for supplied distribution summaries, `CalendarHeatmap` for daily\n activity, and `FunnelChart` for ordered cohort stages. Retrieve each specialized data contract;\n these components do not all accept `LumenChartSeries`. Keep statistical and conversion policy\n in the application, preserve missing days, and use density for unequal histogram bin widths. Use\n `Chart` as the custom SVG/canvas escape hatch. Preserve `null` gaps and the default accessible\n summary and fallback data. See `docs/data-visualization.md` for the full contract.\n Pass `labels` to replace every library-owned visible and assistive phrase. Finite zero is valid\n data, while an empty chart has no finite values; do not silently reinterpret an all-zero series\n as missing. Set `showTable={false}` (`showData={false}` on native adapters) only when an\n equivalent accessible table or scoreboard is adjacent.\n Keep stable identity in `datum.x` and put display text in `datum.xLabel`. Use `formatCategory`\n and `formatValue` for localized axes, SVG titles, and data tables. Choose\n `presentation=\"bare\"` inside an existing Card, `categoryWidth` for long horizontal labels,\n `markers=\"auto\"` or a numeric marker step for dense lines, and `emptyLabel` for actionable\n source-specific guidance.\n- Use `Icon name=\"search\"` or any Lucide icon name for standard interface pictograms. Kebab-case\n names and Lucide aliases work. Pair decorative icons with `decorative`, and pass `label` only\n when the icon carries meaning on its own. On the web, install and register\n `@santi020k/lumen-icons-brand` for supported brand marks; native adapters include those names.\n Then use `brand:github` or the adapter's generated equivalent. Render project-specific or\n unsupported brand SVGs through the default `Icon` slot. Astro reports unknown named icons during\n development instead of failing silently.\n- Use `Schedule` with `Agenda`, `Calendar`, and `DatePicker` for booking, CRM, content calendar, and\n event planning screens. Mark slots with `data-ui-schedule-slot` and events with\n `data-ui-schedule-event`; the runtime emits `ui:schedule-change` when a draggable event is moved.\n `Calendar` renders a hidden input plus selectable date grid; Astro `UIPrimitives` and registered\n Web Components wire month navigation and roving keyboard focus directly, while React apps can use\n the `Calendar` component or `useCalendar` for the same date selection contract.\n Astro `UIPrimitives` and registered Web Components bind schedule drag/drop directly; React apps\n can use `useSchedule` for `rootProps`, `getEventProps`, `getSlotProps`, and `ui:schedule-change`\n parity. Use `createScheduleSlots`, `moveScheduleEvent`, and `expandRecurringScheduleEvent` from\n `@santi020k/lumen-core` for reusable schedule state; use `getScheduleConflicts` and\n `canPlaceScheduleEvent` before committing drag/drop changes. Persist local schedule edits with\n `createScheduleStorageKey`, `saveScheduleEvents`, and `loadScheduleEvents` against any\n `localStorage`-compatible adapter. Use `resizeScheduleEvent` and `resizeScheduleEvents` for\n snap-to-grid resize handles.\n- Use `KanbanBoard` and `KanbanColumn` for status-based workspaces while continuing to use `Card` for\n items. Mark items with `data-ui-kanban-item` and put `data-ui-kanban-handle` on a dedicated button;\n do not make the whole card draggable. Astro and Elements emit the cancellable, controlled\n `ui:kanban-move-request` event, and React provides `useKanban`. The event reports `itemId`,\n `fromColumn`, `toColumn`, optional `beforeId`, and keyboard or pointer input, but never moves DOM or\n persists application data. The host owns validation, `aria-busy`, optimistic state, rollback,\n status names, and APIs. Use compact `Empty` states inside columns, preserve every column with\n `Skeleton` blocks while loading, and paginate large columns. See [Kanban composition and movement](kanban.md).\n- React applications can compose [TanStack-powered table views](data-table-views.md) through\n `DataTableView`, preserving application-owned cells and server queries. Numeric/date ranges and\n page-scoped selection are opt-in. Use `DataTableSavedViews` for explicit host-owned preference\n persistence, and `useLumenReviewWorkflow` for reviewed commands with stale-revision and unknown-outcome\n guards. Install `review-workflow`, `import-review` or `record-workspace` for executable React compositions.\n- Use `DataTable`, `Tree`, `TreeGrid`, `VirtualList`, `Pagination`, and `Command` for dense data\n collection workflows. Astro and Elements wire selectable/sortable `DataTable` behavior and\n `VirtualList` range events; React also provides built-in fixed-height list windowing. `Tree`\n and `TreeGrid` use roving keyboard focus, and `VirtualList` emits `ui:virtual-list-range` as the\n rendered range changes. Use `createDataViewState`,\n `serializeDataViewState`, `parseDataViewState`, `applyDataViewState`, and `getVirtualRange` for\n saved views, filtering, sorting, pagination, and virtualization math. Use\n `createDataViewStorageKey`, `saveDataViewState`, and `loadDataViewState` when saved views should\n survive navigation or reloads. Use `pinDataViewColumn`, `unpinDataViewColumn`,\n `toggleDataViewSelection`, `createDataViewSearchParams`, and `createDataViewServerRequest` when\n connecting dense views to router state or server-backed data.\n- Use `Resizable` for split-pane editors, dashboards, inspectors, and side-by-side review layouts.\n Astro `UIPrimitives` and registered Web Components generate separator handles and pane sizing\n directly; React apps can use the `Resizable` component or `useResizable` for pointer, keyboard,\n min/max, and double-click reset parity.\n- Use `ThemeBuilder`, `ColorPicker`, `Card`, `Button`, and `Input` to build token preview and export\n tools. Add `data-ui-theme-brand-hue`, `data-ui-theme-accent-hue`, `data-ui-theme-scheme`,\n `data-ui-theme-mode`, manual color inputs such as `data-ui-theme-primary-hex`, and\n `data-ui-theme-target` for scoped live previews. Add `data-ui-theme-export-format`,\n `data-ui-theme-export`, and `data-ui-theme-output` to copy CSS, Figma variable JSON, or design\n token JSON. Astro `UIPrimitives` and registered Web Components wire those controls directly;\n React apps can use `useThemeBuilder` for the same tokens, exports, and control props. Use\n `createThemePalette`, `createThemeBuilderTokens`, `exportThemeBuilderValue`, `exportThemeCss`,\n `parseThemeCss`, and `scoreThemeContrast`\n for theme import/export and accessibility checks. Use `suggestReadableInk`, `mergeThemeTokens`,\n and `tuneThemeContrast` when generated palettes need readable foreground repair.\n- Use `RichTextEditor` with `ButtonGroup`, `ToggleGroup`, and `Textarea` for editor compositions.\n Controls with `data-ui-editor-command` request formatting, block, history, link or list commands;\n use `data-ui-editor-value` for value-bearing commands. Before the browser executes a command,\n Lumen emits the bubbling, cancelable `ui:editor-command-request` event. External engines must\n call `preventDefault()` and set `event.detail.executed` to their synchronous result. A failed\n external command never falls back to the browser. The existing `ui:editor-command` event reports\n completion; use it for notifications, not executing the command again.\n Editable surfaces support formatting shortcuts and emit `ui:editor-change` with HTML and text.\n Set `data-ui-editor-native-state=\"false\"` when an external engine owns toolbar state.\n React apps can use `useRichTextEditor({ commandHandler: ({ command, value }) => boolean })`;\n providing a handler disables native toolbar state syncing by default. Use `nativeState` to\n explicitly control that policy. Engine commands are synchronous; engines with asynchronous\n work should manage completion themselves. Keep Tiptap, ProseMirror, Lexical and other engines\n in the application.\n- Use `Autocomplete`, `SearchField`, `NumberField`, `TimeField`, `DateRangePicker`, `ColorPicker`,\n and `TagGroup` when forms need more than plain text inputs. Add `data-ui-form` to forms that\n should reflect native Constraint Validation API state into `Field` error slots on blur and submit.\n For `InputOTP`, Astro `UIPrimitives` and registered Web Components render/sync a real native input\n with visual OTP segments; React apps can use the `InputOTP` component or `useInputOTP` for the\n same sanitization, paste handling, and caret navigation.\n For `DateRangePicker`, Astro `UIPrimitives` and registered Web Components sync the paired native\n date input `min`/`max` constraints directly; React apps can use `useDateRangePicker` for the same\n start/end syncing props.\n Use native attributes such as `required`, `pattern`, `min`, `max`, and `type`; add\n `data-error-required`, `data-error-pattern`, `data-error-min`, `data-error-max`, or\n `data-error-custom` when a control needs custom copy. Listen for `ui:validate` to call\n `control.setCustomValidity(message)` before the runtime reads validity, and listen for\n `ui:invalid` or `ui:valid` on the form for submission state. Astro `UIPrimitives` and registered\n Web Components bind `data-ui-form` directly; React apps can use `useFormValidation` with `Field`\n to get the same control props, CustomEvents, and error slot updates.\n- Use `Toast` for transient feedback. Static `<Toast>` markup works without\n JavaScript. With Astro `UIPrimitives` or registered Web Components, create toasts with\n `window.LumenToast.create(detail)`, `LumenToast.create(detail)` from `@santi020k/lumen-elements`,\n or `document.dispatchEvent(new CustomEvent('ui:toast', { detail }))`; update them with\n `.update(id, detail)` or `ui:toast-update`; and dismiss them with `.dismiss(id?)` or\n `ui:toast-dismiss`. `detail` supports `title`, `description`, `variant`, `duration`, `placement`,\n `max`, and `action: { label, value }`. Destructive toasts use `role=\"alert\"`; other variants use\n `role=\"status\"`. Runtime toasts pause their timeout on hover or focus, and Escape dismisses the\n focused toast.\n\n## Runtime CustomEvents\n\nWhen `UIPrimitives` is mounted, or when Web Components are registered for matching behavior, Lumen\nuses these CustomEvents:\n\n| Event | Target | Detail | Fires When |\n| -------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------- |\n| `ui:data-table-selection-change` | `DataTable` root `[data-ui-datatable]` | `{ values: string[] }` | Selectable row checkboxes, the select-all checkbox, or form reset changes selected row values. |\n| `ui:schedule-change` | `Schedule` root `[data-ui-schedule]` | `{ eventId?: string, slot?: string }` | A draggable schedule event is dropped on a `[data-ui-schedule-slot]`. |\n| `ui:virtual-list-range` | `VirtualList` root `[data-ui-virtual-list]` | `{ startIndex: number, endIndex: number }` | The virtual list calculates its visible range on init or scroll. |\n| `ui:kanban-move-request` | `KanbanBoard` root `[data-ui-kanban]` | `{ itemId: string, fromColumn: string, toColumn: string, beforeId?: string, input: 'keyboard' \\| 'pointer' }` | A handle requests a controlled move; the event is cancellable and never moves data or DOM. |\n| `ui:tag-remove` | `TagGroup` root `.ui-tag-group` or `[data-ui-tag-group]` | `{ value?: string }` | A `[data-ui-tag-remove]` control removes its closest tag or list item. |\n| `ui:editor-command-request` | `RichTextEditor` root `[data-ui-rich-text-editor]` | `{ command: string, executed: boolean, value?: string }` | Cancel before execution and set executed for external engine ownership. |\n| `ui:editor-command` | `RichTextEditor` root `[data-ui-rich-text-editor]` | `{ command: string, executed: boolean, value?: string }` | A toolbar control or keyboard shortcut runs an editor command. |\n| `ui:editor-change` | `RichTextEditor` root `[data-ui-rich-text-editor]` | `{ html: string, text: string }` | Editable content changes or an editor command runs. |\n| `ui:theme-change` | `ThemeBuilder` root `[data-ui-theme-builder]` | `{ hue: number, accentHue: number, mode: 'generated' \\| 'manual', scheme: 'dark' \\| 'light', tokens: Record<string, string> }` | Hue, manual color, mode, or scheme controls update generated tokens. |\n| `ui:theme-export` | `ThemeBuilder` root `[data-ui-theme-builder]` | `{ css?: string, format: 'css' \\| 'figma' \\| 'tokens', tokens: Record<string, string> \\| null, value: string }` | A `[data-ui-theme-export]` control writes the selected export to the output and clipboard when available. |\n| `ui:language-change` | `LanguageToggle` control | `{ previousValue: string, value: string }` | Requests or applies the next configured locale. |\n| `ui:validate` | Form `[data-ui-form]` | `{ control, form, value }` | Before validation state is read, so custom code can call `control.setCustomValidity(message)`. |\n| `ui:invalid` | Form `[data-ui-form]` | `{ control?, controls?, form }` | Blur or submit validation finds invalid controls. Failed submits focus the first invalid control. |\n| `ui:valid` | Form `[data-ui-form]` | `{ control?, controls?, form }` | Blur or submit validation finds the checked control or form valid. |\n| `ui:toast` | `document` | `{ id?, title?, description?, variant?, duration?, placement?, action? }` | Creates a runtime toast in the matching notification viewport. |\n| `ui:toast-update` | `document` | `{ id, title?, description?, variant?, duration? }` | Updates a runtime toast. |\n| `ui:toast-dismiss` | `document` | `{ id? }` | Dismisses one runtime toast, or all runtime toasts when `id` is omitted. |\n| `ui:toast-action` | Toast `[data-ui-toast]` | `{ id, value? }` | A runtime toast action button is pressed, unless the action specifies a custom event name. |\n\n## Registry Helper\n\n`@santi020k/lumen` exports typed component, recipe, and documentation metadata from\n`@santi020k/lumen/registry`. The umbrella package and every web adapter expose the same small\n`lumen` binary, so projects that install Astro, React, or Elements can use discovery and diagnostics\nwithout another direct dependency. Individual components are addressable by name, including\nkebab-case aliases such as `data-table`:\n\n```bash\nlumen list\nlumen show Button\nlumen show data-table\nlumen add Button\nlumen add Button --target react\nlumen add Button --target elements\nlumen add data-table\nlumen show scheduler\nlumen add scheduler\nlumen add scheduler --target react\nlumen add scheduler --target elements\nlumen add scheduler --dry-run\nlumen add scheduler --fail-on-conflict\nlumen add scheduler --force\nlumen add scheduler --merge\nlumen list --registry ./registry/lumen.registry.json\nlumen add custom-recipe --registry ./registry/custom.registry.json\nlumen add private-recipe --registry https://example.com/lumen.registry.json --registry-token \"$TOKEN\"\nlumen install\nlumen doctor --json\nlumen doctor-native --json\nlumen init --framework astro --tailwind\n```\n\n`lumen doctor-native` emits the same adoption inventory and Android toolchain preflight in readable\ntext or JSON. Treat direct-primitive matches as review suggestions, not rewrite instructions: first\nconfirm whether a surface is product-owned and whether the public Lumen contract preserves its\nbehavior. A failed JDK, SDK, platform-package, or required-NDK preflight should be fixed before\nstarting Gradle so native compilation does not obscure the setup problem.\n\n`lumen add <component>` defaults to an Astro wrapper. Use `--target react` for a local React wrapper\nor `--target elements` for a custom-elements starter. Bundled recipes also accept `--target astro`,\n`--target react`, and `--target elements`; external registry inline files keep their authored paths\nand sources. Those paths must be unique, relative forward-slash paths: the installer rejects\nabsolute paths, parent traversal, and symbolic-link path segments before writing any file. Registry\nmanifests are limited to 5 MiB by default; programmatic `loadLumenRegistry` callers can lower the\nlimit with `maxBytes` and control remote request time with `timeoutMs`.\n\nBefore inventing a wrapper or custom class pattern, check the component source in\n`packages/astro/components`. Astro is the reference implementation for props, class names, data\nattributes, and accessible markup.\n\n## Styling Rules for Generated Code\n\n- Read the [content flow contract](content-flow.md). Stack and Grid own sibling gaps; surfaces own\n padding; Field owns label/control/feedback spacing. Do not add another child margin for the same\n relationship. Use `gap=\"related\"` for closely related controls, `gap=\"group\"` for separate groups\n and `gap=\"section\"` for major sections. Retrieve spacing tokens through MCP.\n- Use Card parts for header/body/actions and a nested Stack inside complex CardContent. Card\n handles missing or hidden parts and wraps footer actions. Choose `density=\"compact\"`,\n `\"comfortable\"` (default) or `\"spacious\"` instead of scattering padding overrides.\n- Keep text rhythm inside Prose or Typography. Verify long and translated text, enlarged text,\n validation states, optional sections and mobile wrapping before calling a composition finished.\n- Use the `page-header` and `section-header` recipes for dashboard identity, optional breadcrumbs,\n status/count badges and independent actions. Keep headings in document order, pass translated\n navigation/action labels and unique heading ids, and connect buttons to application-owned handlers.\n- Retrieve complete `content-flow-header`, `content-flow-settings`, `content-flow-list` and\n `content-flow-actions` examples with `lumen_get_recipe`; the CLI installs the same compositions.\n Keep one outer Container for fluid gutters. Card permits interactive overflow; use AspectRatio\n to clip media. Reading blocks trim their outer margins, so avoid compensating offsets.\n\n- Do not require consumers to configure Tailwind for Lumen components.\n- Prefer component props and composition before adding custom CSS.\n- Assign each visible outer edge to one container. For flush content inside a rounded frame,\n let the frame own the border and corner clipping; remove child rounding with supported props.\n For example, use `<Image radius=\"none\" ... />` below a framed header: `Image` defaults to\n `radius=\"lg\"`, which otherwise leaves rounded gaps at the shared edge. Keep standalone image\n rounding when appropriate.\n- Use a single divider between header and body rather than overlapping full borders. Inset\n surfaces with padding may have their own radius; account for the inset so their curves align.\n Clip artwork at its frame, not an entire interactive surface where focus rings or overlays\n could be cut off. Verify shared corners, borders, and keyboard focus at phone and desktop\n widths in both light and dark themes.\n- Use the shared token names for custom surfaces: `canvas`, `surface`, `surface-muted`,\n `surface-strong`, `line`, `ink`, `ink-soft`, `ink-muted`, `brand`, `brand-solid`, `brand-soft`,\n `accent`, `success`, `warning`, and `danger`.\n- Use `glass` as a boolean prop or attribute for glass surfaces when supported.\n- Preserve semantic labels, keyboard paths, focus states, and native form attributes.\n\n## Figma Handoff\n\nUse `exportThemeFigmaVariables` from `@santi020k/lumen-core/figma` when a design workflow needs\nFigma variable-friendly color values, and `exportThemeDesignTokens` when exporting standard design\ntoken JSON for token importers. See [docs/figma.md](figma.md) for the recommended Figma workflow.\n\n## When Editing This Repo\n\n- Shared contracts belong in `packages/core`.\n- Astro implementation and standalone CSS belong in `packages/astro`.\n- React-specific primitives belong in `packages/react`.\n- Web Component-specific primitives belong in `packages/elements`.\n- Update the relevant package README when public usage changes.\n- Add tests beside package code as `*.test.ts` when behavior, exported metadata, or component\n contracts change.\n\n### React inline range calendar\n\nFor a range editor with both endpoint calendars visible, use the React-only\n`DateRangeCalendar` export. Pass `value={{ start, end }}`, `onValueChange`, required\n`labels={{ start, end, presets }}`, `locale`, optional `min`/`max`, and optional\n`presets={[{ label, value: { start, end } }]}`. `formatDate` customizes the displayed\nendpoint summaries. Keep a draft in the consuming application and apply it only\nwhen confirmed. ISO endpoints must be valid and ascending. Crossing a boundary\nmoves the other endpoint; presets outside the bounds are disabled. The component\nshares `useCalendar` keyboard behavior and stacks its calendars on narrow screens.\n\nFor React input-attached range selection, import `DateRangeInput` from\n`@santi020k/lumen-react`. Pass controlled ISO `value`/`onValueChange`, an accessible\n`label`, and localized `labels` containing `start`, `end`, `presets`, `apply`, and\n`cancel`. It owns draft state and confirmation, anchors a non-modal popover to its\ntrigger, and supports keyboard dismissal and responsive scrolling. Use `validate`\nfor domain restrictions and `renderSummary` for draft details. Optional\n`name={{ start: 'from', end: 'to' }}` submits applied ISO values through hidden inputs.\nSee the React README for the complete usage and browser fallback contract.\n\n## Fixed-height virtual lists\n\n`VirtualList` uses the same DOM windowing controller in Astro, React and Elements. All rows remain\nmounted; offscreen rows are hidden, and inert spacers preserve the complete scroll height. This\nreduces displayed rows, not the cost of creating the initial DOM. Use pagination or an application\nrenderer when retaining the entire dataset in the DOM is too expensive.\n\nUse `itemSize` (Astro/React) or `item-size` (Elements) for a fixed row height in pixels, defaulting\nto 44. `overscan` defaults to four rows on each side. Every direct element child is a row; put\ncontrols inside it, and keep headers or empty-state actions outside the list. Rows must fit the\nconfigured height; variable-height measurement is not supported. Do not add external row margins\nor gaps. Initially hidden rows remain hidden and are excluded from the window.\n\nThe controller refreshes on scrolling, resizing, direct child changes and sizing attribute changes.\nA focused row and its neighboring rows remain available for native Tab navigation, which can extend\nthe displayed window until focus moves. Name the scroll container using `aria-label` or\n`aria-labelledby`; it is keyboard-focusable by default. `ui:virtual-list-range` reports inclusive\n`startIndex` and `endIndex` when the displayed range or item count changes. Empty lists use\n`{ startIndex: 0, endIndex: -1 }`. `data-ui-range-start` and `data-ui-range-end` mirror the current\nwindow. Elements disconnect and React unmount restore original row styles and hidden state.\n\n## NumberField boundary\n\n`NumberField` remains a native number-input wrapper. The v4 consumer audits do not establish a\nshared requirement for a locale-aware draft parser. Applications needing decimal-comma entry,\nlossless monetary drafts or currency rules should retain their explicit parsing and validation\npolicy; Lumen must not infer currency units or precision from locale.\n\n\n## Combobox keyboard behavior in v4\n\nCombobox keeps DOM focus in its input while Arrow Up/Down changes the active option through\n`aria-activedescendant`. Enter commits the active option only while the list is open; without an\nactive option, normal form submission remains available. Home, End, Left and Right retain native\ntext editing. Composition input and canceled keyboard events do not trigger selection or dismissal.\nEscape closes the innermost open control first, preserving an enclosing popup or dialog until the\nnext Escape. Tab dismisses the list when focus leaves the component.\n\nAstro and Elements observe added, removed, relabeled and disabled options, and delegate pointer\nselection to the current list. React derives options from its current props and reports committed\nselections through `onChange`, including controlled inputs. Disabled and read-only inputs stay closed.\nApplications should keep focus on the input and observe `aria-activedescendant` instead of calling\nfocus on option buttons. The DOM adapters match both option labels and values.\n\n## Appearance presets\n\nChoose `default`, `studio` or `glass` at the theme boundary. See [appearance presets](appearance-presets.md) for exact framework APIs. Studio follows the neutral PostLens look. Glass remains explicit per supporting surface; React Native and Compose retain solid fallbacks without adding blur dependencies. Customize semantic colors, typography and surface dimensions through public theme APIs.\n\n## Version-aware build, review, and migration\n\nThe portable skills are `lumen-ui` for implementation, `lumen-review` for a read-only audit, and\n`lumen-migrate` for an explicitly requested upgrade. Their canonical sources live under `skills/`;\n`pnpm run generate:plugin-package` produces the client snapshots. Both client manifests use plugin\n1.1.0 and pin the v4 catalog. This candidate is not proof of publication or directory approval.\n\nBefore applying MCP contracts, call `lumen_check_compatibility` with resolved versions from installed\nmetadata or native lock files. A manifest range is not an installed version. When versions differ,\ninspect installed public types and README or use a matching MCP package. Do not silently upgrade.\nSearch supports English and Spanish queries with framework and platform filters.\n\nFor a v4 upgrade, read `lumen_get_migration` and preview `lumen migrate v4 --dry-run --json` locally.\n`--apply` rewrites only known static SDK import paths in JavaScript and TypeScript. It preserves\ncomments and examples and reports UI/native review triggers. Dependency updates and application-owned\nworkarounds require review; the CLI does not infer that a workaround can be removed. JSX, Astro,\nnative source, dynamic imports, and dependency manifests are not automatically rewritten by the\nSDK import transform.\n\nSee [v4 AI verification](lumen-4-ai.md) for actual agent benchmark coverage and release evidence.\n\nThe integrated `lumen migrate v4` command also retains the release's web spacing migrations and\noptional coordinated dependency workflow. SDK edits compose into that command's source transform,\nso its apply ledger fingerprints the final source and repeat runs do not rewrite spacing twice.\nSDK import edits remain limited to `.ts`, `.js`, and `.mjs`; this restriction does not disable the\nseparate documented JSX/Astro spacing migration. The JSON report includes installed package\nversions and explicit SDK dependency-review findings.\n\n\n## Login examples and Auth\n\nUse the `auth-onboarding` recipe for visual email-code sign-in, code verification, and passkey\nrecovery states in Astro, React, or Elements. The preview controls are disabled and do not authenticate.\nLumen supplies the UI; the optional `@santi020k/auth-client` supplies protocol operations.\nSee the [login integration reference](../packages/templates/README.md#login-examples-and-auth)\nfor helper calls, safe result handling, and the application-owned server and session requirements.\n## Complex React forms\n\nUse the optional `@santi020k/lumen-react/forms` hooks with public form primitives for dependent\nvalidation, unsaved edits, stable repeatable rows, step navigation, and cancellation-safe checks.\nRead [composable form workflows](powerful-forms.md) before implementation. Choose one validation\nowner; keep schemas, financial policy, requests, authorization, and draft storage in the consumer.\n\nFor photo and media editors, install the `media-workspace` recipe and follow\n[Studio media workspaces](studio-media-workspaces.md). Reuse public media controls; keep\nimage processing, storage and export in the consumer application.\n",
|
|
46095
|
-
"readme": "<p align=\"center\">\n <a href=\"https://lumen.santi020k.com\">\n <picture>\n <source media=\"(prefers-color-scheme: dark)\" srcset=\"./docs/assets/readme/hero-dark.svg\">\n <img src=\"./docs/assets/readme/hero-light.svg\" alt=\"Lumen UI — A shared language. Every interface.\" width=\"1200\" height=\"340\">\n </picture>\n </a>\n</p>\n\n<h1 align=\"center\">Lumen UI</h1>\n\n<p align=\"center\">\n <strong>Accessible primitives. Native authoring. Shared foundations.</strong><br>\n An open-source UI system for Astro, React, Web Components, React Native, SwiftUI, and Jetpack Compose.\n</p>\n\n<p align=\"center\">\n <a href=\"https://lumen.santi020k.com\">Documentation</a> ·\n <a href=\"https://lumen.santi020k.com/docs/components\">Components</a> ·\n <a href=\"https://lumen.santi020k.com/#playgrounds\">Playgrounds</a> ·\n <a href=\"https://www.figma.com/community/file/1662337342676541513\">Figma library</a> ·\n <a href=\"https://lumen.santi020k.com/support\">Support</a>\n</p>\n\n<p align=\"center\">\n <a href=\"https://github.com/santi020k/lumen/actions/workflows/ci.yml\"><img alt=\"CI status on main\" src=\"https://img.shields.io/github/actions/workflow/status/santi020k/lumen/ci.yml?branch=main&style=flat-square&label=CI\"></a>\n <a href=\"https://github.com/santi020k/lumen/actions/workflows/codeql.yml\"><img alt=\"CodeQL status\" src=\"https://github.com/santi020k/lumen/actions/workflows/codeql.yml/badge.svg\"></a>\n <a href=\"https://www.npmjs.com/package/@santi020k/lumen-astro\"><img alt=\"Published Astro package version\" src=\"https://img.shields.io/npm/v/@santi020k/lumen-astro?style=flat-square&label=npm\"></a>\n <a href=\"./LICENSE\"><img alt=\"MIT license\" src=\"https://img.shields.io/badge/license-MIT-036b8c?style=flat-square\"></a>\n</p>\n\nLumen gives your product a consistent visual language while preserving each framework's native\nway of building. Compose accessible controls, forms, charts, and complete product layouts from\nshared tokens and explicit component contracts.\n\n**Explore:** [Quick start](#web-quick-start) · [Native apps](#native-playgrounds) · [Templates](#dashboards-and-templates) · [AI and Figma](#ai-and-design-workflows) · [Packages](#packages) · [Repository apps](#repository-apps) · [Contributing](#contributing)\n\n> **Lumen 4:** See the [v4.2 highlights](docs/lumen-4.2.md) for SwiftUI appearance and agent guidance,\n> and [v4.1 highlights](docs/lumen-4.1.md) for loading and forms. Follow the [migration guide](docs/migrating-v3-to-v4.md) when\n> upgrading from v3. Package versions and store builds follow their own release schedules.\n\n## See Lumen in action\n\n<p>\n <a href=\"https://lumen.santi020k.com/docs/web/playground\">\n <img src=\"./apps/docs/src/assets/platforms/web.webp\" alt=\"A Lumen web workspace with navigation, metric cards, an interactive chart, and a task form\" width=\"1184\" height=\"802\">\n </a>\n</p>\n\n*Real Lumen components with illustrative data. [Open the web playground →](https://lumen.santi020k.com/docs/web/playground)*\n\n| Built for | What you get |\n| --- | --- |\n| **Web products** | Astro, React, and custom elements with standalone CSS and optional Tailwind integration. |\n| **Native interfaces** | React Native, SwiftUI, Compose, WidgetKit, and Wear OS foundations with platform conventions. |\n| **Accessible interaction** | Semantic structure, keyboard paths, focus management, and reduced-motion support. |\n| **Design and AI workflows** | Figma resources, a portable agent skill, MCP discovery, and machine-readable contracts. |\n\n## Web quick start\n\nChoose one adapter. Astro requires Astro 5+, React requires React 19+, and the web packages\nrequire Node.js 22.12+ for their tooling. Install in your existing application:\n\n```bash\n# Astro\npnpm add @santi020k/lumen-astro\n\n# React\npnpm add @santi020k/lumen-react\n\n# Web Components\npnpm add @santi020k/lumen-elements\n```\n\n### Astro\n\nImport the stylesheet and mount `UIPrimitives` once in your root layout. The runtime enhances all\ninteractive Lumen markup on the page.\n\n```astro\n---\nimport '@santi020k/lumen-astro/styles.css'\nimport UIPrimitives from '@santi020k/lumen-astro/runtime'\n---\n\n<html lang=\"en\">\n <body>\n <slot />\n <UIPrimitives />\n </body>\n</html>\n```\n\n```astro\n---\nimport { Button, Card, Input, Label } from '@santi020k/lumen-astro'\n---\n\n<Card>\n <Label for=\"email\">Email</Label>\n <Input id=\"email\" name=\"email\" type=\"email\" placeholder=\"you@example.com\" />\n <Button>Subscribe</Button>\n</Card>\n```\n\n### React\n\nLoad the stylesheet once from your app entry or global CSS:\n\n```tsx\nimport '@santi020k/lumen-react/styles.css'\nimport { Button, Card, Input, Label } from '@santi020k/lumen-react'\n\nexport function SubscribeForm() {\n return (\n <Card>\n <Label htmlFor=\"email\">Email</Label>\n <Input id=\"email\" name=\"email\" type=\"email\" placeholder=\"you@example.com\" />\n <Button>Subscribe</Button>\n </Card>\n )\n}\n```\n\n### Web Components\n\nImport the styles and register the elements once:\n\n```html\n<style>\n @import \"@santi020k/lumen-elements/styles.css\";\n</style>\n\n<script type=\"module\">\n import { defineLumenElements } from '@santi020k/lumen-elements/define'\n\n defineLumenElements()\n</script>\n\n<lumen-card>\n <lumen-label id=\"email-label\">Email</lumen-label>\n <lumen-input aria-labelledby=\"email-label\" id=\"email\" name=\"email\" type=\"email\" placeholder=\"you@example.com\"></lumen-input>\n <lumen-button>Subscribe</lumen-button>\n</lumen-card>\n```\n\nSee the [Web documentation](https://lumen.santi020k.com/docs/web) for installation, theming,\ncomponent examples, and API details.\n\nBrowse the [package directory](https://lumen.santi020k.com/docs/packages) for framework adapters,\nshared foundations, and optional integrations, with links to focused examples and usage guides.\n\nFor failure states, use the [error-handling guide](./docs/error-handling.md) to choose between field\nfeedback, summaries, persistent alerts, transient toasts, and the `ErrorState` recovery surface.\nLumen owns presentation and accessibility; applications retain logging, retry, and exception policy.\n\nFor native applications, choose the [React Native](https://lumen.santi020k.com/docs/react-native),\n[Apple / SwiftUI](https://lumen.santi020k.com/docs/apple), or\n[Android / Compose](https://lumen.santi020k.com/docs/android) guide. The\n[shared foundations](https://lumen.santi020k.com/docs/foundations) explain the cross-platform token\nand component contract; repository contributors can also use the\n[cross-platform architecture](./docs/cross-platform.md) and\n[native component reference](./docs/native-components.md).\n\n> **Native qualification:** Check the [compatibility matrix](docs/native-compatibility.md) and\n> [device evidence](docs/native-device-validation.md) for your target. Local builds, published\n> artifacts, and physical-device accessibility are distinct verification steps.\n\n## Tailwind CSS\n\nKeep the layer prelude, Tailwind import, and Lumen stylesheet in the same shared CSS entry. Replace\n`lumen-astro` with the package for your framework.\n\n```css\n@import \"@santi020k/lumen-astro/layers.css\";\n@import \"tailwindcss\";\n@import \"@santi020k/lumen-astro/styles.css\";\n```\n\nThis order places Tailwind base styles before Lumen components and Tailwind utilities above Lumen\ncomponent defaults.\n\n## Native playgrounds\n\n<table>\n <tr>\n <th>SwiftUI</th>\n <th>Jetpack Compose</th>\n <th>React Native</th>\n </tr>\n <tr>\n <td><a href=\"https://lumen.santi020k.com/docs/apple/playground\"><img src=\"./apps/docs/src/assets/platforms/apple.webp\" alt=\"Native SwiftUI release checklist example\" width=\"220\"></a></td>\n <td><a href=\"https://lumen.santi020k.com/docs/android/playground\"><img src=\"./apps/docs/src/assets/platforms/android.webp\" alt=\"Native Compose release checklist example\" width=\"220\"></a></td>\n <td><a href=\"https://lumen.santi020k.com/docs/react-native/playground\"><img src=\"./apps/docs/src/assets/platforms/react-native.webp\" alt=\"Expo browser preview of the contributor profile example\" width=\"220\"></a></td>\n </tr>\n <tr>\n <td>Native app capture</td>\n <td>Native app capture</td>\n <td>Expo browser preview</td>\n </tr>\n</table>\n\nSee [capture provenance](apps/docs/src/assets/platforms/README.md) for sources and verification scope.\n\nTry Lumen before adding it to your project. The [playground section](https://lumen.santi020k.com/#playgrounds)\nbrings together the browser demos and native apps. Explore component states and themes, then use\nthe open-source apps as implementation references. Start with the\n[web playground](https://lumen.santi020k.com/docs/web/playground) for Astro, React, and Web Components,\nor choose a native gallery:\n\n- [`apps/playground-react-native`](./apps/playground-react-native) runs through Expo on the web,\n iOS, and Android. [Try the browser preview](https://lumen.santi020k.com/docs/react-native/playground#preview)\n or use the local setup guide for native evaluation. EAS profiles cover TestFlight, Android App\n Bundles, and APKs; this gallery has no separate public store listing.\n- [`apps/playground-apple`](./apps/playground-apple) is available on the\n [App Store](https://apps.apple.com/app/id6805250815) for iPhone, iPad, and Mac. It also builds as\n an iOS Xcode app or macOS Swift Package executable for local exploration.\n- [`apps/playground-android`](./apps/playground-android) builds a native Compose application and a\n directly installable debug APK. It is also available on\n [Google Play](https://play.google.com/store/apps/details?id=com.santi020k.lumen.playground.compose)\n for Android phones and tablets.\n\nSee the [playground workflow](./docs/playgrounds.md) for a platform chooser, reference-app source,\nand run, capture, and distribution commands. Store builds follow their own release schedule and\nmay differ from the current repository version.\n\n## Appearance presets\n\nChoose an appearance preset or customize the existing themes with the [appearance guide](docs/appearance-presets.md). The Studio preset uses PostLens as its visual reference.\n\n## Dashboards and templates\n\nExplore the [template gallery](https://lumen.santi020k.com/templates) for complete analytics,\nSaaS admin, commerce, project workspace, and authentication/onboarding experiences. Each family\nuses public Lumen primitives and semantic tokens, includes responsive and accessibility coverage,\nand ships through the CLI for all three framework targets:\n\n```bash\nlumen add analytics-dashboard\nlumen add commerce-dashboard --target react\nlumen add auth-onboarding --target elements\n```\n\n## AI and design workflows\n\nInstall the portable Lumen skill in Codex, Claude Code, Cursor, Windsurf, and other compatible\ncoding agents:\n\n```bash\nnpx skills add santi020k/lumen --skill lumen-ui\n```\n\nThe skill teaches agents how to select, compose, theme, and verify Lumen primitives. Pair it with\n[`@santi020k/lumen-mcp`](./packages/mcp) when an agent needs to search the live catalog or retrieve\ncurrent source, props, tokens, and usage rules.\n\nFor ChatGPT and Codex, install the published\n[Lumen UI plugin](https://chatgpt.com/plugins/plugin_asdk_app_6a8f6c526c5481918eb8a48806fa112b)\nfrom the Plugins Directory. Select **Install plugin**, then mention **@Lumen UI** in a request such\nas “Find the right Lumen components for an accessible React settings screen.” The plugin bundles\nthe skill with the hosted, read-only catalog; no separate Lumen account or local MCP setup is needed.\n\nLumen is designed to reduce repetitive UI code and unnecessary agent context. Actual token usage\ndepends on the model, task, and corrections; see the [measurement method](docs/ai-efficiency.md).\n\n<details>\n<summary>Guides, registries, and integration references</summary>\n\nAdditional machine-readable surfaces include:\n\n- [`llms.txt`](./llms.txt) for a concise project map.\n- [`docs/ai-usage.md`](./docs/ai-usage.md) for downstream generation examples.\n- [`docs/styling-contract.md`](./docs/styling-contract.md) for stable parts and component variables.\n- [`docs/import-and-icon-performance.md`](./docs/import-and-icon-performance.md) for repeatable import and icon evidence.\n- [`docs/graphics.md`](./docs/graphics.md) for portable decorative artwork across web and native platforms.\n- [`docs/consumer-regression-fixtures.md`](./docs/consumer-regression-fixtures.md) for production-shaped local coverage.\n- [`docs/project-adoption.md`](./docs/project-adoption.md) for the sibling-project audit and staged\n web and native migration strategy.\n- [`registry/lumen.registry.json`](./registry/lumen.registry.json) for recipes and installable file\n groups.\n- [`docs/figma.md`](./docs/figma.md) for Figma variables and Code Connect workflows.\n- [`apps/figma-plugin/README.md`](./apps/figma-plugin/README.md) for the **beta** Figma plugin,\n Astro starters, and AI handoff.\n- [`plugins/lumen-ui`](./plugins/lumen-ui) for the packaged Codex and ChatGPT plugin, with its\n [submission and review record](./docs/openai-plugin-submission.md).\n- [`.claude-plugin/marketplace.json`](./.claude-plugin/marketplace.json) for the Claude Code\n marketplace, with its [installation and submission guide](./docs/claude-code-plugin.md).\n- [`docs/cross-platform.md`](./docs/cross-platform.md) for the native architecture, support tiers,\n and component-parity policy.\n- [`docs/native-patterns.md`](./docs/native-patterns.md) for developed native compositions and the\n boundary between Lumen presentation and platform-owned application structure.\n\n</details>\n\n## Packages\n\n| Package | Purpose |\n| --- | --- |\n| [`@santi020k/lumen-astro`](./packages/astro) | Reference implementation, Astro components, and progressive-enhancement runtime |\n| [`@santi020k/lumen-react`](./packages/react) | React components and behavior hooks |\n| [`@santi020k/lumen-react-hook-form`](./packages/react-hook-form) | Optional React Hook Form adapters for composite controls |\n| [`@santi020k/lumen-elements`](./packages/elements) | Standards-based Web Components |\n| [`@santi020k/lumen-core`](./packages/core) | Shared tokens, metadata, class helpers, and behavior contracts |\n| [`@santi020k/lumen-tokens`](./packages/tokens) | Canonical platform-neutral design token source |\n| [`@santi020k/lumen-react-native`](./packages/react-native) | React Native foundations and native primitives |\n| [`LumenUI`](./packages/swift) | Swift Package for iOS, iPadOS, macOS, tvOS, visionOS, and watchOS foundations and native primitives |\n| [`LumenWidgetUI`](./packages/swift-widget) | Focused WidgetKit-safe semantic text, icon, badge, and compact-stat foundations |\n| [`lumen-compose`](./packages/compose) | Android library for Jetpack Compose foundations and native primitives |\n| [`lumen-compose-wear`](./packages/compose/wear) | Focused Wear OS primitives for at-a-glance round-screen actions, progress, status, metrics, and rows |\n| [`@santi020k/lumen-icons-brand`](./packages/icons-brand) | Optional namespaced brand icons for web `Icon` adapters |\n| [`@santi020k/lumen`](./packages/lumen) | Umbrella package, CLI, registry metadata, and public package map |\n| [`@santi020k/lumen-mcp`](./packages/mcp) | MCP server for component discovery and source retrieval |\n\nThe private [`@santi020k/lumen-templates`](packages/templates/README.md) workspace supplies the\ngallery and preview renderer. Consumer recipes are installed through the public Lumen CLI.\n\n## Repository apps\n\nEach app has its own setup, source map, and verification guidance:\n\n| App | Purpose |\n| --- | --- |\n| [Documentation](apps/docs/README.md) | Public documentation, live examples, search, and native previews |\n| [Template showcase](apps/templates/README.md) | Standalone gallery of five product-template families |\n| [Next.js smoke app](apps/next-smoke/README.md) | Server/client boundaries and rendered consumer regression fixtures |\n| [Apple playground](apps/playground-apple/README.md) | iPhone, iPad, Mac, watchOS, and WidgetKit reference hosts |\n| [Android playground](apps/playground-android/README.md) | Compose phone/tablet app and separate Wear OS host |\n| [React Native playground](apps/playground-react-native/README.md) | Expo reference app and embedded browser gallery |\n| [Figma plugin](apps/figma-plugin/README.md) | Development beta for Astro starters and structured agent handoff |\n| [Motion studio](apps/motion/README.md) | Reusable product films and local video exports |\n| [Store artwork](apps/store-assets/README.md) | Canonical SVG sources for the playground app icons |\n\n## Lumen 4\n\nLumen 4 brings consumer-driven date selection, chart readability, clipboard feedback,\nloading states, native layout improvements and a new ImageComparison primitive. Start with the\n[v3 → v4 migration guide](docs/migrating-v3-to-v4.md), [consumer audit](docs/lumen-4-consumer-audit.md),\nand [v4.2 release highlights](docs/lumen-4.2.md). The community gallery includes published consumer\nscreenshots; it does not imply those applications already run the current release.\n\nFor existing applications, start with [v1 → v2](docs/migrating-v1-to-v2.md) when needed, then\n[v2 → v3](docs/migrating-v2-to-v3.md) before\n[v3 → v4](docs/migrating-v3-to-v4.md). For initial adoption, use\n[Migrating to Lumen](docs/migrating-to-lumen.md).\n\n## Contributing\n\nLumen is a pnpm workspace. Astro is the reference implementation; shared contracts belong in\n`packages/core`, while framework-specific behavior stays in its adapter package.\n\nUse Node.js 22.22.2+, 24.15.0+, or 26+ and pnpm 12.8.1, as declared in the root manifest.\n\n```bash\npnpm install --frozen-lockfile\npnpm run dev\npnpm run validate\n```\n\nRead [`CONTRIBUTING.md`](./CONTRIBUTING.md) before opening a pull request. User-visible package\nchanges require a changeset.\n\n## Community\n\nCreate reusable product animations and social videos locally with the\n[Lumen motion studio](./apps/motion/README.md). Its first pilot previews one interface in four\nappearances and exports portrait, square, and landscape MP4s through HyperFrames.\n\n[Feedback & support](https://lumen.santi020k.com/support) connects ideas, questions, bug reports,\nand the [public roadmap](https://lumen.santi020k.com/support#roadmap). Discuss improvements on GitHub\nand follow accepted work through implementation and release.\n\nLumen is free and MIT licensed while the project focuses on adoption, public examples, and\nreal-world feedback.\n\n- Explore task-oriented [guides](https://lumen.santi020k.com/guides).\n- Share a public project in the [community showcase](https://lumen.santi020k.com/community).\n- Evaluate the system with the open [team adoption path](https://lumen.santi020k.com/teams).\n- Tell us what would improve your next build through the\n [adoption survey](https://github.com/santi020k/lumen/issues/new?template=adoption-feedback.yml).\n\nMaintainers can use the [exposure playbook](./docs/exposure-playbook.md) for publishing cadence,\ncampaign measurement, video scripts, and launch copy, and the\n[marketing package](./docs/marketing/README.md) for execution-ready copy, channel decisions, a\nfour-week calendar, asset briefs, an approval queue, and a community-response library.\n\n## Find your next step\n\n| Resource | Use it for |\n| --- | --- |\n| [Cross-platform architecture](docs/cross-platform.md) | Shared foundations and adapter responsibilities. |\n| [Native compatibility](docs/native-compatibility.md) | Supported toolchains and platform baselines. |\n| [Native release runbook](docs/native-release-runbook.md) | Artifact qualification and distribution gates. |\n\n## License\n\nLumen's original code and artwork are available under the [MIT License](./LICENSE). Generated\ninterface icons retain the Lucide ISC or Feather MIT terms, and generated brand icons retain Font\nAwesome Free's CC BY 4.0 terms. The native packages include the required attribution and license\nnotices; see [`icons/THIRD_PARTY_NOTICES.md`](./icons/THIRD_PARTY_NOTICES.md).\n\nCreated and maintained by [Santiago Molina](https://santi020k.com).\n"
|
|
46094
|
+
"aiUsage": "# AI Usage Guide\n\nUse this guide when an AI agent is building an app with Lumen or updating examples that teach\nanother agent how to use the library.\n\nLumen 4 is released. See the [v4.2 highlights](lumen-4.2.md) and use the\n[release manifest](../registry/release-manifest.json) for each package's version; companion packages\ndo not all share the umbrella version. The public plugin and hosted MCP describe their deployed\nsnapshot, which can differ from this checkout. Inspect snapshot metadata before using its contracts.\n\n## Install in ChatGPT or Codex\n\nThe [Lumen UI plugin](https://chatgpt.com/plugins/plugin_asdk_app_6a8f6c526c5481918eb8a48806fa112b)\nis published in the shared Plugins Directory for ChatGPT and Codex.\n\n1. Open the listing and select **Install plugin**. Sign in to ChatGPT if prompted.\n2. Start a request with **@Lumen UI**, or choose one of the listing's starter prompts.\n3. Describe the interface and target framework, for example: “Find the right Lumen components for\n an accessible React settings screen.”\n\nThe plugin combines the Lumen workflow skill with the hosted public MCP catalog. It requires no\nseparate Lumen account, API key, or local server. Its tools only read public Lumen documentation;\nthey do not access private repositories or modify files and services. Your coding agent performs\nimplementation work using the retrieved contracts and its own permissions.\n\nSee the [publication record](./openai-plugin-submission.md) for version and verification details.\n\n## Install the Lumen Agent Skill\n\nLumen ships a portable Agent Skill for AI coding tools that support the open `SKILL.md` format:\n\n```bash\nnpx skills add santi020k/lumen --skill lumen-ui\n```\n\nThe source lives in [`skills/lumen-ui`](../skills/lumen-ui). The skill provides the durable\nframework-selection, component-composition, theming, accessibility, and verification workflow.\nUse it by naming `$lumen-ui` in a prompt, or let a compatible agent trigger it from Lumen-specific\nrequests.\n\nThe skill and MCP server complement each other: the skill explains how to work, while\n`@santi020k/lumen-mcp` supplies structured catalog search, framework-specific contracts, recipes,\nsource, tokens, resources, and rules.\n\n## Web selection controls\n\nUse `Select` as the default for web product option lists, and `Combobox` when the user needs to\nsearch the choices. `NativeSelect` remains supported for a product requirement that explicitly\ncalls for the browser or platform picker. Do not remove native fallbacks from custom controls.\n\nMount the matching framework behavior before verifying the result: Astro uses `UIPrimitives`,\nReact uses the public Select behavior hook, and Elements uses registered custom elements. Check\nthat opening the control shows the enhanced list, that keyboard selection and Escape work, and\nthat dependent choices, forms, mobile layouts, and both appearances remain correct.\n\nFor personal projects that require custom lists, enforce the choice in ESLint as well as agent\nguidance. The prepared `lumenCustomSelects()` helper in `@santi020k/eslint-config-libraries` rejects\n`NativeSelect` imports (including aliases), namespace JSX usage, raw JSX selects, and JSX native\nselect custom elements without replacing the project's existing syntax restrictions. Enable it\nonly after that package version is available. Keep intentional native-control exceptions scoped\nto specific files with a documented product reason. Library source and fallback implementations\nshould not opt into a consumer-only policy.\n\n## Pick the Right Package\n\nUse the framework requested by the user. Every adapter shares the same Lumen foundation.\n\n| App Target | Install | Import Components From | Load Styles From |\n| -------------------------- | ---------------------------------------------------------------------- | -------------------------------------------- | -------------------------------------- |\n| Astro | `@santi020k/lumen-astro` | `@santi020k/lumen-astro` | `@santi020k/lumen-astro/styles.css` |\n| React | `@santi020k/lumen-react` | `@santi020k/lumen-react` | `@santi020k/lumen-react/styles.css` |\n| React Hook Form composites | `@santi020k/lumen-react-hook-form` | `@santi020k/lumen-react-hook-form` | Uses React styles |\n| Web Components | `@santi020k/lumen-elements` | `@santi020k/lumen-elements/define` | `@santi020k/lumen-elements/styles.css` |\n| React Native / Expo | `@santi020k/lumen-react-native` | `@santi020k/lumen-react-native` | Not applicable |\n| Apple / SwiftUI | Swift Package `https://github.com/santi020k/lumen`, pinned to `4.3.0` | `LumenUI` | Not applicable |\n| Android / Compose | Maven Central `com.santi020k:lumen-compose:4.0.1` | `com.santi020k.lumen` | Not applicable |\n| Wear OS / Compose | Maven Central `com.santi020k:lumen-compose-wear:4.0.1` | `com.santi020k.lumen` | Not applicable |\n| Package metadata | `@santi020k/lumen-core` | `@santi020k/lumen-core` | Not applicable |\n| Optional web brand icons | `@santi020k/lumen-icons-brand` | Register once, then use the framework `Icon` | Uses framework styles |\n\nEach framework package includes the shared foundation. Install `@santi020k/lumen` separately only\nwhen you need its framework-neutral CLI or registry metadata.\n\nFor React Native, mount one `LumenProvider` near the app root. For SwiftUI, attach the `LumenUI`\nSwift Package product to the application target, use an exact or compatible release-version rule,\nand apply `.lumenTheme(...)` near the root. For Compose, install\n`com.santi020k:lumen-compose:4.0.1` from Maven Central and wrap content in `LumenTheme`. Native\nadapters do not load CSS or the Astro runtime.\n\nReact Native can use ordinary React hooks and platform-neutral application hooks. Import native\nLumen controllers such as `useDialog`, `useTabs`, `useSelect`, `useLanguageToggle`,\n`useThemeToggle`, and `useToast` from `@santi020k/lumen-react-native`; their state semantics track\nthe reusable React contracts while their returned props target native Lumen components. Never\nimport DOM-dependent hooks from `@santi020k/lumen-react` into a native bundle. Share business state\nin an application workspace package and keep the Lumen rendering adapter at each app boundary.\n\nFor watchOS, use the focused `LumenWatch*` contracts from the same Swift package. For Wear OS,\ninstall `com.santi020k:lumen-compose-wear:4.0.1` and wrap wearable content in `LumenWearTheme`\ninside the application-owned Wear Material theme. Keep complications, tiles, rotary input,\nAlways On behavior, haptics, synchronization, navigation, and health or safety logic app-owned.\n\nBrand marks are intentionally excluded from the default web Lucide bundle. In Astro, React, or Web\nComponents, install and register the optional pack once before rendering a namespaced icon:\n\n```ts\nimport { registerLumenBrandIcons } from \"@santi020k/lumen-icons-brand\";\n\nregisterLumenBrandIcons();\n```\n\nThen use any canonical Font Awesome Free brand name, such as `brand:github`, `brand:discord`,\n`brand:figma`, `brand:linkedin`, `brand:whatsapp`, or `brand:youtube`, through the existing Astro,\nReact, or Web Component `Icon` API. The package also provides `brand:x` as an alias for\n`brand:x-twitter`.\n\nBrand marks keep their recognizable filled geometry instead of being redrawn as Lucide outlines.\nThey still inherit the same Lumen size, alignment, `currentColor`, and accessible-label behavior as\nthe default icon catalog. React Native, SwiftUI, and Compose already include the corresponding\ncomplete brand catalog and accept `brand:github`, `.brandGithub`, and\n`LumenIconName.BrandGithub`, respectively, without a separate package.\n\n## Consumer composition in v4\n\n- For reporting filters, use React `DateRangeInput` with controlled `value`/`onValueChange`, localized\n `labels`, bounds and explicit presets. Keep draft changes inside the picker until Apply. See the\n [reporting example](https://lumen.santi020k.com/docs/web/reporting) and [migration guide](migrating-to-lumen.md).\n- Keep date-only values as validated `YYYY-MM-DD` strings. Do not round-trip a local date through\n UTC timestamps. Use the native adapter's existing date field and application-owned calendar/time-zone rules.\n- Compose a metric card from `Card`, `Stat` and a chart with `presentation=\"bare\"`; do not invent\n MetricCard or Status components. Products own status labels, aggregation, currency units and rounding.\n- Use unique chart X identities, abbreviated `xLabel` text and full `formatCategory` labels.\n Preserve absent observations explicitly; never turn missing data into a fabricated zero.\n- Compare two media treatments with `ImageComparison` and matching source dimensions. Supply a\n visible localized control label and useful image alternatives. See [image comparison](image-comparison.md).\n- Use `Prose` for ordinary Markdown, `Code`/`CodeTabs` for examples, and `CopyButton` for dynamic\n command output. Localize clipboard success/error feedback; do not swallow rejected writes.\n- Use public `asChild` composition when a semantic link needs button styling. Disable nested form\n controls separately. On Input, native numeric `size` and Lumen `visualSize` have different meanings.\n- Map the complete semantic theme in light and dark modes, including `brand-solid`/`on-brand` and\n `danger`/`on-danger`. Text-safe colors matter more than matching a decorative swatch.\n- NavigationMenu, Sidebar and Sheet compose a documentation shell. Keep ordinary links in normal\n Tab order and application routing in the app. Do not rebuild primitives with `ui-*` classes.\n\n### Theme bootstrap and generated command output\n\nWhen the application owns theme state, use `ThemeToggle controlled` and its own click handler.\nInitialize the document in the head before paint; unavailable storage must not prevent rendering:\n\n```html\n<script>\n let theme = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'\n try {\n const saved = localStorage.getItem('application-theme')\n if (saved === 'light' || saved === 'dark') theme = saved\n } catch {\n // Private browsing or storage policy can deny access.\n }\n document.documentElement.dataset.theme = theme\n document.documentElement.classList.toggle('dark', theme === 'dark')\n</script>\n```\n\nThe owner's click handler updates the document and the toggle's `aria-pressed` state, and persists\ninside a guarded storage write. Use a server-rendered theme when the application already has one.\nA controlled Lumen toggle does not read storage or change that state itself.\n\nFor commands generated from a form, point `CopyButton` at the current output element. It reads the\nlatest text when clicked, so the application only updates the output rather than managing clipboard\npromises or feedback timers:\n\n```astro\n---\nimport { CopyButton } from '@santi020k/lumen-astro'\n---\n\n<output id=\"generated-command\">pnpm run check</output>\n<CopyButton target=\"#generated-command\" label=\"Copy command\" copiedLabel=\"Command copied\" errorLabel=\"Copy failed; select the command and copy it manually\">\n Copy command\n</CopyButton>\n```\n\n## MCP-Assisted Workflow\n\nConnect `@santi020k/lumen-mcp` when the agent supports Model Context Protocol. The server returns\nvalidated structured content as well as readable text, so agents can select components without\nscraping documentation.\n\nThe MCP per-component catalog covers Astro, React, Web Components, React Native, SwiftUI, and\nCompose. Use `lumen_list_native_components` to discover platform availability and\n`lumen_get_native_component` for installation, setup, API, examples, accessibility guidance, and\noptional reference source. The skill still verifies installed packages when local versions differ.\n\n```bash\ncodex mcp add lumen -- npx -y @santi020k/lumen-mcp\n```\n\nFor Codex project configuration:\n\n```toml\n[mcp_servers.lumen]\nenabled = true\ncommand = \"npx\"\nargs = [\"-y\", \"@santi020k/lumen-mcp\"]\n```\n\nUse this sequence:\n\n1. Read `lumen://meta` and `lumen://diagnostics` to identify and verify the snapshot.\n2. Read `lumen://rules`.\n3. Search for the use case with `lumen_search`, passing the target `framework` when known.\n4. Inspect the selected framework with `lumen_get_component` and `detail: \"usage\"`.\n5. Follow its framework behavior setup and hook/controller contract.\n6. Read related multi-component guidance with `lumen_get_recipe`.\n7. Request `detail: \"source\"` only when the usage contract is insufficient.\n8. Read `lumen://tokens` before introducing custom styling.\n9. Retain `lumen://catalog-manifest` and use `lumen_diff_catalog` after upgrades to identify contracts that need refreshing.\n\nExample framework-aware component request:\n\n```json\n{\n \"name\": \"lumen_get_component\",\n \"arguments\": {\n \"name\": \"DateRangePicker\",\n \"framework\": \"react\",\n \"detail\": \"usage\"\n }\n}\n```\n\nStable resources are available at `lumen://rules`, `lumen://tokens`, `lumen://components`,\n`lumen://meta`, `lumen://catalog-manifest`, `lumen://diagnostics`,\n`lumen://components/{name}`, and `lumen://recipes/{name}`. See\n[`packages/mcp/README.md`](../packages/mcp/README.md) for Claude Desktop, Cursor, generic stdio,\nlocal repository, and programmatic examples.\n\n## Minimal Setup\n\nImport the stylesheet once in the app's global CSS, root layout, or app entry.\n\nThis section applies to web targets. Native adapters use their provider or theme APIs instead of a\nstylesheet.\n\n```css\n@import \"@santi020k/lumen-react/styles.css\";\n```\n\nIf Tailwind is present, keep these imports in the same shared CSS entry.\n\n```css\n@import \"@santi020k/lumen-react/layers.css\";\n@import \"tailwindcss\";\n@import \"@santi020k/lumen-react/styles.css\";\n@import \"@santi020k/lumen-react/tailwind.css\";\n```\n\nKeep the layer prelude first. It places Tailwind base styles below Lumen components and Tailwind\nutilities above them, so consumer spacing, display, radius, width, and responsive visibility\nclasses override Lumen defaults predictably. The optional `tailwind.css` bridge (Lumen 4.3+)\nprovides semantic color utilities such as `bg-surface`, `text-ink`, and `border-line` through\ninline aliases that follow nested themes. Keep class names literal and scan consumer code only;\nLumen ships its component CSS. See the [Tailwind guide](https://lumen.santi020k.com/docs/web/tailwind).\n\nAstro apps should mount `UIPrimitives` once in the root layout when using interactive primitives\nsuch as dialogs, menus, popovers, tabs, carousels, command lists, toggles, validated forms, or\ntoasts. Do not place `UIPrimitives` in each component example or next to each primitive instance;\none root include enhances all matching `data-ui-*` markup on the page.\n\n```astro\n---\nimport UIPrimitives from '@santi020k/lumen-astro/runtime'\n---\n\n<UIPrimitives />\n```\n\n## Astro\n\nUse direct named imports from `@santi020k/lumen-astro`.\n\n```astro\n---\nimport { Button, Card, Input, Label } from '@santi020k/lumen-astro'\n---\n\n<Card>\n <Label for=\"email\">Email</Label>\n <Input id=\"email\" type=\"email\" placeholder=\"you@example.com\" />\n <Button type=\"submit\">Subscribe</Button>\n</Card>\n```\n\n`Stat` defaults to a `div`. Choose `as=\"article\"` for a self-contained metric with supporting\ncontent, or `as=\"section\"` for a labeled region. Do not use a semantic root solely for styling.\nIts visual variants are `default`, `accent`, and `glass`; reserve `accent` for featured metrics and\nkeep glass selective.\n\nFor long reading surfaces, use `Anchor` for local section navigation and pass each item’s optional\nheading `depth`, `index`, and `description` when hierarchy matters. Anchor also respects root `scroll-padding-top` and heading `scroll-margin-top` after text resizing. Use `activationOffset` for fixed\napplication headers. Use `ScrollProgress` for document position; keep `Progress` for\napplication-controlled task completion. In Astro, dispatch `ui:progress-change` with\n`{ value, max? }` from the `Progress` root when browser state changes after hydration. Non-finite or\nmissing fields keep the current state, and every adapter clamps to finite ARIA values. Use `Spinner`\nfor unknown-duration waits.\n\nSkeleton and Spinner are decorative (`aria-hidden`) unless named. For a group of placeholders,\nannounce loading once: wrap them in one `role=\"status\"` region with `aria-busy=\"true\"` and a\n`VisuallyHidden` label, and keep the Skeletons unnamed. Size placeholders with `shape`\n(`rectangle`, `text`, `circle`), `width`, and `height`, and use deterministic widths rather than\nrandom values so server and client markup match. Data fetching and when to show placeholders stay\nin the application. `Pill`\naccepts `neutral`, `brand`, and `outline`\nvariants, plus native link and count composition.\n\n## React\n\nReact components share the same visual classes and data attributes as Astro components. For\nbehavior-heavy primitives, use the React behavior hooks from `@santi020k/lumen-react` (`useDialog`,\n`usePopover`, `useDropdownMenu`, `useTabs`, `useSelect`, `useToast`, and `useTooltip`), which track\nthe Astro runtime's ARIA, keyboard, Escape, dismissal, and toast controller semantics.\n\nThe React package entry is a client boundary. In Next.js, it can be imported by a Server Component\nwithout evaluating Lumen's contexts and hooks in the React server runtime; the rendered Lumen\nsubtree becomes a Client Component boundary. Vite and other client-rendered React applications use\nthe same entry without additional setup.\nFor stateless Badge, Card composition, layout, form-label, progress, loading, typography, and\nvisually-hidden usage in a React Server Component, import from\n`@santi020k/lumen-react/server`. That entrypoint contains the implementations directly and avoids\ncreating a client boundary. Keep event handlers, hooks, and interactive primitives on the package\nroot inside a Client Component.\n`Button`, `Link`, `ButtonLink`, and `Input` accept React 19 DOM refs directly as props.\n`Button asChild` composes the button contract onto one child element. Across adapters, native\nnumeric `size` remains available on `Input` and `NativeSelect`; use `visualSize` in Astro and React,\nor `visual-size` in Elements, for presentation.\n`Stat as` and `variant` follow the same semantic-root and visual guidance as the Astro component.\n\n```tsx\nimport \"@santi020k/lumen-react/styles.css\";\nimport { Button, Card, Input, Label } from \"@santi020k/lumen-react\";\n\nexport function SubscribeForm() {\n return (\n <Card>\n <Label htmlFor=\"email\">Email</Label>\n <Input id=\"email\" type=\"email\" placeholder=\"you@example.com\" />\n <Button type=\"submit\">Subscribe</Button>\n </Card>\n );\n}\n```\n\nFor React Hook Form, spread `register()` directly onto native-backed controls. Install\n`@santi020k/lumen-react-hook-form` only for controlled composites such as `Select`, `DatePicker`,\n`InputOTP`, and `ListBox`. Do not combine React Hook Form and `useFormValidation` unless the\napplication intentionally coordinates two validation sources. For schema validation, use\n`zodResolver` or `yupResolver` from `@hookform/resolvers`; keep Zod or Yup in the application and\nrender the resulting React Hook Form errors through Lumen's `FieldError` contract.\n\n## Web Components\n\nRegister custom elements once, then use `lumen-*` tags in HTML. The elements adapter includes\ninteractive behavior parity for DataTable, Dialog, Popover, DropdownMenu, ContextMenu, Tabs,\nSelect, ThemeBuilder, Toast, Tooltip, forms, RichTextEditor, Schedule, and VirtualList: selection,\nvalidation, rich text command, context menu, schedule drag/drop, theme, and range events, ARIA\nstate, roving/listbox keyboard paths, Escape/outside dismissal, focus return/trapping, native\nselect form participation, and the same toast controller events as the Astro runtime.\n\n```html\n<script type=\"module\">\n import { defineLumenElements } from \"@santi020k/lumen-elements/define\";\n\n defineLumenElements();\n</script>\n\n<lumen-card>\n <label for=\"email\">Email</label>\n <lumen-input\n id=\"email\"\n type=\"email\"\n placeholder=\"you@example.com\"\n ></lumen-input>\n <lumen-button type=\"submit\">Subscribe</lumen-button>\n</lumen-card>\n```\n\nFor a surface that uses only Badge, Button, Card, and Combobox, import\n`@santi020k/lumen-elements/components/badge`, `/button`, `/card`, and `/combobox` and call their\ncorresponding `defineLumenBadge`, `defineLumenButton`, `defineLumenCard`, and `defineLumenCombobox`\nfunctions. Those implementation-level entrypoints avoid the complete catalog bundle; the Combobox\nentrypoint includes its filtering and keyboard behavior. Continue using `defineLumenElements` for\nany component without a granular entrypoint.\n\nFor a foundation-only custom-element surface, import `defineLumenFoundations` from\n`@santi020k/lumen-elements/components/foundations`. It registers Card compound parts, Container,\nDirection, Grid, Label, Separator, Skeleton, Spinner, Stack, Typography, and VisuallyHidden without\npulling the complete catalog implementation.\n\nPut input elements inside a native `<form data-ui-form>`. Scalar Lumen controls participate in\n`FormData`, validity, disabled state, focus, and reset through form-associated custom-element\nbehavior with a native-control fallback. Do not generate `<lumen-form>` as the submission\ncontainer.\n\n## Component Selection\n\nThe shared catalog includes:\n\n`Accordion`, `Alert`, `AlertDialog`, `Agenda`, `AspectRatio`, `Attachment`, `Autocomplete`,\n`Avatar`, `Badge`, `BarChart`, `Breadcrumb`, `Bubble`, `Button`, `ButtonGroup`, `Calendar`, `Card`,\n`Carousel`, `Chart`, `Checkbox`, `CheckboxGroup`, `Collapsible`, `Code`, `Combobox`, `Command`,\n`Container`, `ColorPicker`,\n`ContextMenu`, `DataTable`, `DatePicker`, `DateRangePicker`, `Dialog`, `Direction`, `Drawer`,\n`DropdownMenu`, `Empty`, `ErrorState`, `ErrorSummary`, `Field`, `FieldError`, `Form`, `Grid`, `HoverCard`, `Icon`,\n`Graphic`, `Backdrop`, `Illustration`, `Input`, `InputGroup`, `InputOTP`, `Item`, `KanbanBoard`, `KanbanColumn`, `Kbd`, `Label`, `LineChart`, `ListBox`, `Marker`, `Menubar`,\n`Message`, `MessageScroller`, `NativeSelect`, `ContextNavigation`, `NavigationMenu`, `NumberField`, `Pagination`,\n`PasswordField`, `PieChart`, `Popover`, `Progress`, `CopyButton`, `RadioGroup`, `Resizable`, `RichTextEditor`,\n`ScrollArea`, `Schedule`, `SearchField`, `Select`, `Separator`, `Sheet`, `Sidebar`, `Skeleton`,\n`Slider`, `Sparkline`, `Spinner`, `Stack`, `Switch`, `Table`, `Tabs`, `TagGroup`, `Textarea`, `ThemeBuilder`,\n`TimeField`, `Toast`, `Toggle`, `ToggleGroup`, `Tooltip`, `Tree`, `TreeGrid`, `Typography`, and\n`VirtualList`, and `VisuallyHidden`.\n\n## Product Recipes\n\n- Follow [the error-handling guide](./error-handling.md) when a generated interface needs failure\n feedback. Use `FieldError` and `ErrorSummary` for validation, `Alert` for persistent inline\n problems, `Toast` for brief non-blocking failures, and `ErrorState` only when a page or region\n cannot show its primary content. Keep exception capture, logging, retry policy, and safe message\n mapping in application code. For React Native, SwiftUI, and Compose, follow the guide's native API\n mapping and announcement rules; do not invent a cross-platform error manager inside the component\n layer.\n\n- Start full product surfaces from the bundled `analytics-dashboard`, `saas-admin`,\n `commerce-dashboard`, `project-workspace`, or `auth-onboarding` recipe. Install with\n `lumen add <recipe>` for Astro, or add `--target react` / `--target elements`. Keep the generated\n shared template stylesheet, then replace sample data and routes while preserving public Lumen\n components, semantic tokens, accessible names, focus paths, and responsive navigation.\n- Use `Sparkline` for a compact trend beside a labelled metric, `BarChart` for categorical\n comparison, `LineChart` for ordered or time-series data, `PieChart` for a small\n part-to-whole breakdown. Pass already-aggregated `LumenChartSeries` data; keep statistics,\n resampling, model evaluation, and domain calculations in application code. Keep `showTable`\n enabled unless the same values are already available in a nearby semantic table. Prefer a bar\n chart when precise slice comparison matters or the pie would exceed roughly seven categories.\n Use `ScatterChart` for numeric relationships, `Heatmap` for a labeled matrix, `RangeChart` for\n low-to-high intervals, and `ComboChart` only when mixed marks share a meaningful domain.\n Use `WaterfallChart` for signed balance changes, `LollipopChart` for ranking, `DumbbellChart`\n for reference/current comparison, and `BulletChart` for actual/target comparison. Use `Histogram`\n for supplied bins, `BoxPlot` for supplied distribution summaries, `CalendarHeatmap` for daily\n activity, and `FunnelChart` for ordered cohort stages. Retrieve each specialized data contract;\n these components do not all accept `LumenChartSeries`. Keep statistical and conversion policy\n in the application, preserve missing days, and use density for unequal histogram bin widths. Use\n `Chart` as the custom SVG/canvas escape hatch. Preserve `null` gaps and the default accessible\n summary and fallback data. See `docs/data-visualization.md` for the full contract.\n Pass `labels` to replace every library-owned visible and assistive phrase. Finite zero is valid\n data, while an empty chart has no finite values; do not silently reinterpret an all-zero series\n as missing. Set `showTable={false}` (`showData={false}` on native adapters) only when an\n equivalent accessible table or scoreboard is adjacent.\n Keep stable identity in `datum.x` and put display text in `datum.xLabel`. Use `formatCategory`\n and `formatValue` for localized axes, SVG titles, and data tables. Choose\n `presentation=\"bare\"` inside an existing Card, `categoryWidth` for long horizontal labels,\n `markers=\"auto\"` or a numeric marker step for dense lines, and `emptyLabel` for actionable\n source-specific guidance.\n- Use `Icon name=\"search\"` or any Lucide icon name for standard interface pictograms. Kebab-case\n names and Lucide aliases work. Pair decorative icons with `decorative`, and pass `label` only\n when the icon carries meaning on its own. On the web, install and register\n `@santi020k/lumen-icons-brand` for supported brand marks; native adapters include those names.\n Then use `brand:github` or the adapter's generated equivalent. Render project-specific or\n unsupported brand SVGs through the default `Icon` slot. Astro reports unknown named icons during\n development instead of failing silently.\n- Use `Schedule` with `Agenda`, `Calendar`, and `DatePicker` for booking, CRM, content calendar, and\n event planning screens. Mark slots with `data-ui-schedule-slot` and events with\n `data-ui-schedule-event`; the runtime emits `ui:schedule-change` when a draggable event is moved.\n `Calendar` renders a hidden input plus selectable date grid; Astro `UIPrimitives` and registered\n Web Components wire month navigation and roving keyboard focus directly, while React apps can use\n the `Calendar` component or `useCalendar` for the same date selection contract.\n Astro `UIPrimitives` and registered Web Components bind schedule drag/drop directly; React apps\n can use `useSchedule` for `rootProps`, `getEventProps`, `getSlotProps`, and `ui:schedule-change`\n parity. Use `createScheduleSlots`, `moveScheduleEvent`, and `expandRecurringScheduleEvent` from\n `@santi020k/lumen-core` for reusable schedule state; use `getScheduleConflicts` and\n `canPlaceScheduleEvent` before committing drag/drop changes. Persist local schedule edits with\n `createScheduleStorageKey`, `saveScheduleEvents`, and `loadScheduleEvents` against any\n `localStorage`-compatible adapter. Use `resizeScheduleEvent` and `resizeScheduleEvents` for\n snap-to-grid resize handles.\n- Use `KanbanBoard` and `KanbanColumn` for status-based workspaces while continuing to use `Card` for\n items. Mark items with `data-ui-kanban-item` and put `data-ui-kanban-handle` on a dedicated button;\n do not make the whole card draggable. Astro and Elements emit the cancellable, controlled\n `ui:kanban-move-request` event, and React provides `useKanban`. The event reports `itemId`,\n `fromColumn`, `toColumn`, optional `beforeId`, and keyboard or pointer input, but never moves DOM or\n persists application data. The host owns validation, `aria-busy`, optimistic state, rollback,\n status names, and APIs. Use compact `Empty` states inside columns, preserve every column with\n `Skeleton` blocks while loading, and paginate large columns. See [Kanban composition and movement](kanban.md).\n- React applications can compose [TanStack-powered table views](data-table-views.md) through\n `DataTableView`, preserving application-owned cells and server queries. Numeric/date ranges and\n page-scoped selection are opt-in. Use `DataTableSavedViews` for explicit host-owned preference\n persistence, and `useLumenReviewWorkflow` for reviewed commands with stale-revision and unknown-outcome\n guards. Install `review-workflow`, `import-review` or `record-workspace` for executable React compositions.\n- Use `DataTable`, `Tree`, `TreeGrid`, `VirtualList`, `Pagination`, and `Command` for dense data\n collection workflows. Astro and Elements wire selectable/sortable `DataTable` behavior and\n `VirtualList` range events; React also provides built-in fixed-height list windowing. `Tree`\n and `TreeGrid` use roving keyboard focus, and `VirtualList` emits `ui:virtual-list-range` as the\n rendered range changes. Use `createDataViewState`,\n `serializeDataViewState`, `parseDataViewState`, `applyDataViewState`, and `getVirtualRange` for\n saved views, filtering, sorting, pagination, and virtualization math. Use\n `createDataViewStorageKey`, `saveDataViewState`, and `loadDataViewState` when saved views should\n survive navigation or reloads. Use `pinDataViewColumn`, `unpinDataViewColumn`,\n `toggleDataViewSelection`, `createDataViewSearchParams`, and `createDataViewServerRequest` when\n connecting dense views to router state or server-backed data.\n- Use `Resizable` for split-pane editors, dashboards, inspectors, and side-by-side review layouts.\n Astro `UIPrimitives` and registered Web Components generate separator handles and pane sizing\n directly; React apps can use the `Resizable` component or `useResizable` for pointer, keyboard,\n min/max, and double-click reset parity.\n- Use `ThemeBuilder`, `ColorPicker`, `Card`, `Button`, and `Input` to build token preview and export\n tools. Add `data-ui-theme-brand-hue`, `data-ui-theme-accent-hue`, `data-ui-theme-scheme`,\n `data-ui-theme-mode`, manual color inputs such as `data-ui-theme-primary-hex`, and\n `data-ui-theme-target` for scoped live previews. Add `data-ui-theme-export-format`,\n `data-ui-theme-export`, and `data-ui-theme-output` to copy CSS, Figma variable JSON, or design\n token JSON. Astro `UIPrimitives` and registered Web Components wire those controls directly;\n React apps can use `useThemeBuilder` for the same tokens, exports, and control props. Use\n `createThemePalette`, `createThemeBuilderTokens`, `exportThemeBuilderValue`, `exportThemeCss`,\n `parseThemeCss`, and `scoreThemeContrast`\n for theme import/export and accessibility checks. Use `suggestReadableInk`, `mergeThemeTokens`,\n and `tuneThemeContrast` when generated palettes need readable foreground repair.\n- Use `RichTextEditor` with `ButtonGroup`, `ToggleGroup`, and `Textarea` for editor compositions.\n Controls with `data-ui-editor-command` request formatting, block, history, link or list commands;\n use `data-ui-editor-value` for value-bearing commands. Before the browser executes a command,\n Lumen emits the bubbling, cancelable `ui:editor-command-request` event. External engines must\n call `preventDefault()` and set `event.detail.executed` to their synchronous result. A failed\n external command never falls back to the browser. The existing `ui:editor-command` event reports\n completion; use it for notifications, not executing the command again.\n Editable surfaces support formatting shortcuts and emit `ui:editor-change` with HTML and text.\n Set `data-ui-editor-native-state=\"false\"` when an external engine owns toolbar state.\n React apps can use `useRichTextEditor({ commandHandler: ({ command, value }) => boolean })`;\n providing a handler disables native toolbar state syncing by default. Use `nativeState` to\n explicitly control that policy. Engine commands are synchronous; engines with asynchronous\n work should manage completion themselves. Keep Tiptap, ProseMirror, Lexical and other engines\n in the application.\n- Use `Autocomplete`, `SearchField`, `NumberField`, `TimeField`, `DateRangePicker`, `ColorPicker`,\n and `TagGroup` when forms need more than plain text inputs. Add `data-ui-form` to forms that\n should reflect native Constraint Validation API state into `Field` error slots on blur and submit.\n For `InputOTP`, Astro `UIPrimitives` and registered Web Components render/sync a real native input\n with visual OTP segments; React apps can use the `InputOTP` component or `useInputOTP` for the\n same sanitization, paste handling, and caret navigation.\n For `DateRangePicker`, Astro `UIPrimitives` and registered Web Components sync the paired native\n date input `min`/`max` constraints directly; React apps can use `useDateRangePicker` for the same\n start/end syncing props.\n Use native attributes such as `required`, `pattern`, `min`, `max`, and `type`; add\n `data-error-required`, `data-error-pattern`, `data-error-min`, `data-error-max`, or\n `data-error-custom` when a control needs custom copy. Listen for `ui:validate` to call\n `control.setCustomValidity(message)` before the runtime reads validity, and listen for\n `ui:invalid` or `ui:valid` on the form for submission state. Astro `UIPrimitives` and registered\n Web Components bind `data-ui-form` directly; React apps can use `useFormValidation` with `Field`\n to get the same control props, CustomEvents, and error slot updates.\n- Use `Toast` for transient feedback. Static `<Toast>` markup works without\n JavaScript. With Astro `UIPrimitives` or registered Web Components, create toasts with\n `window.LumenToast.create(detail)`, `LumenToast.create(detail)` from `@santi020k/lumen-elements`,\n or `document.dispatchEvent(new CustomEvent('ui:toast', { detail }))`; update them with\n `.update(id, detail)` or `ui:toast-update`; and dismiss them with `.dismiss(id?)` or\n `ui:toast-dismiss`. `detail` supports `title`, `description`, `variant`, `duration`, `placement`,\n `max`, and `action: { label, value }`. Destructive toasts use `role=\"alert\"`; other variants use\n `role=\"status\"`. Runtime toasts pause their timeout on hover or focus, and Escape dismisses the\n focused toast.\n\n## Runtime CustomEvents\n\nWhen `UIPrimitives` is mounted, or when Web Components are registered for matching behavior, Lumen\nuses these CustomEvents:\n\n| Event | Target | Detail | Fires When |\n| -------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------- |\n| `ui:data-table-selection-change` | `DataTable` root `[data-ui-datatable]` | `{ values: string[] }` | Selectable row checkboxes, the select-all checkbox, or form reset changes selected row values. |\n| `ui:schedule-change` | `Schedule` root `[data-ui-schedule]` | `{ eventId?: string, slot?: string }` | A draggable schedule event is dropped on a `[data-ui-schedule-slot]`. |\n| `ui:virtual-list-range` | `VirtualList` root `[data-ui-virtual-list]` | `{ startIndex: number, endIndex: number }` | The virtual list calculates its visible range on init or scroll. |\n| `ui:kanban-move-request` | `KanbanBoard` root `[data-ui-kanban]` | `{ itemId: string, fromColumn: string, toColumn: string, beforeId?: string, input: 'keyboard' \\| 'pointer' }` | A handle requests a controlled move; the event is cancellable and never moves data or DOM. |\n| `ui:tag-remove` | `TagGroup` root `.ui-tag-group` or `[data-ui-tag-group]` | `{ value?: string }` | A `[data-ui-tag-remove]` control removes its closest tag or list item. |\n| `ui:editor-command-request` | `RichTextEditor` root `[data-ui-rich-text-editor]` | `{ command: string, executed: boolean, value?: string }` | Cancel before execution and set executed for external engine ownership. |\n| `ui:editor-command` | `RichTextEditor` root `[data-ui-rich-text-editor]` | `{ command: string, executed: boolean, value?: string }` | A toolbar control or keyboard shortcut runs an editor command. |\n| `ui:editor-change` | `RichTextEditor` root `[data-ui-rich-text-editor]` | `{ html: string, text: string }` | Editable content changes or an editor command runs. |\n| `ui:theme-change` | `ThemeBuilder` root `[data-ui-theme-builder]` | `{ hue: number, accentHue: number, mode: 'generated' \\| 'manual', scheme: 'dark' \\| 'light', tokens: Record<string, string> }` | Hue, manual color, mode, or scheme controls update generated tokens. |\n| `ui:theme-export` | `ThemeBuilder` root `[data-ui-theme-builder]` | `{ css?: string, format: 'css' \\| 'figma' \\| 'tokens', tokens: Record<string, string> \\| null, value: string }` | A `[data-ui-theme-export]` control writes the selected export to the output and clipboard when available. |\n| `ui:language-change` | `LanguageToggle` control | `{ previousValue: string, value: string }` | Requests or applies the next configured locale. |\n| `ui:validate` | Form `[data-ui-form]` | `{ control, form, value }` | Before validation state is read, so custom code can call `control.setCustomValidity(message)`. |\n| `ui:invalid` | Form `[data-ui-form]` | `{ control?, controls?, form }` | Blur or submit validation finds invalid controls. Failed submits focus the first invalid control. |\n| `ui:valid` | Form `[data-ui-form]` | `{ control?, controls?, form }` | Blur or submit validation finds the checked control or form valid. |\n| `ui:toast` | `document` | `{ id?, title?, description?, variant?, duration?, placement?, action? }` | Creates a runtime toast in the matching notification viewport. |\n| `ui:toast-update` | `document` | `{ id, title?, description?, variant?, duration? }` | Updates a runtime toast. |\n| `ui:toast-dismiss` | `document` | `{ id? }` | Dismisses one runtime toast, or all runtime toasts when `id` is omitted. |\n| `ui:toast-action` | Toast `[data-ui-toast]` | `{ id, value? }` | A runtime toast action button is pressed, unless the action specifies a custom event name. |\n\n## Registry Helper\n\n`@santi020k/lumen` exports typed component, recipe, and documentation metadata from\n`@santi020k/lumen/registry`. The umbrella package and every web adapter expose the same small\n`lumen` binary, so projects that install Astro, React, or Elements can use discovery and diagnostics\nwithout another direct dependency. Individual components are addressable by name, including\nkebab-case aliases such as `data-table`:\n\n```bash\nlumen list\nlumen show Button\nlumen show data-table\nlumen add Button\nlumen add Button --target react\nlumen add Button --target elements\nlumen add data-table\nlumen show scheduler\nlumen add scheduler\nlumen add scheduler --target react\nlumen add scheduler --target elements\nlumen add scheduler --dry-run\nlumen add scheduler --fail-on-conflict\nlumen add scheduler --force\nlumen add scheduler --merge\nlumen list --registry ./registry/lumen.registry.json\nlumen add custom-recipe --registry ./registry/custom.registry.json\nlumen add private-recipe --registry https://example.com/lumen.registry.json --registry-token \"$TOKEN\"\nlumen install\nlumen doctor --json\nlumen doctor-native --json\nlumen init --framework astro --tailwind\n```\n\n`lumen doctor-native` emits the same adoption inventory and Android toolchain preflight in readable\ntext or JSON. Treat direct-primitive matches as review suggestions, not rewrite instructions: first\nconfirm whether a surface is product-owned and whether the public Lumen contract preserves its\nbehavior. A failed JDK, SDK, platform-package, or required-NDK preflight should be fixed before\nstarting Gradle so native compilation does not obscure the setup problem.\n\n`lumen add <component>` defaults to an Astro wrapper. Use `--target react` for a local React wrapper\nor `--target elements` for a custom-elements starter. Bundled recipes also accept `--target astro`,\n`--target react`, and `--target elements`; external registry inline files keep their authored paths\nand sources. Those paths must be unique, relative forward-slash paths: the installer rejects\nabsolute paths, parent traversal, and symbolic-link path segments before writing any file. Registry\nmanifests are limited to 5 MiB by default; programmatic `loadLumenRegistry` callers can lower the\nlimit with `maxBytes` and control remote request time with `timeoutMs`.\n\nBefore inventing a wrapper or custom class pattern, check the component source in\n`packages/astro/components`. Astro is the reference implementation for props, class names, data\nattributes, and accessible markup.\n\n## Styling Rules for Generated Code\n\n- Read the [content flow contract](content-flow.md). Stack and Grid own sibling gaps; surfaces own\n padding; Field owns label/control/feedback spacing. Do not add another child margin for the same\n relationship. Use `gap=\"related\"` for closely related controls, `gap=\"group\"` for separate groups\n and `gap=\"section\"` for major sections. Retrieve spacing tokens through MCP.\n- Use Card parts for header/body/actions and a nested Stack inside complex CardContent. Card\n handles missing or hidden parts and wraps footer actions. Choose `density=\"compact\"`,\n `\"comfortable\"` (default) or `\"spacious\"` instead of scattering padding overrides.\n- Keep text rhythm inside Prose or Typography. Verify long and translated text, enlarged text,\n validation states, optional sections and mobile wrapping before calling a composition finished.\n- Use the `page-header` and `section-header` recipes for dashboard identity, optional breadcrumbs,\n status/count badges and independent actions. Keep headings in document order, pass translated\n navigation/action labels and unique heading ids, and connect buttons to application-owned handlers.\n- Retrieve complete `content-flow-header`, `content-flow-settings`, `content-flow-list` and\n `content-flow-actions` examples with `lumen_get_recipe`; the CLI installs the same compositions.\n Keep one outer Container for fluid gutters. Card permits interactive overflow; use AspectRatio\n to clip media. Reading blocks trim their outer margins, so avoid compensating offsets.\n\n- Do not require consumers to configure Tailwind for Lumen components.\n- Prefer component props and composition before adding custom CSS.\n- Assign each visible outer edge to one container. For flush content inside a rounded frame,\n let the frame own the border and corner clipping; remove child rounding with supported props.\n For example, use `<Image radius=\"none\" ... />` below a framed header: `Image` defaults to\n `radius=\"lg\"`, which otherwise leaves rounded gaps at the shared edge. Keep standalone image\n rounding when appropriate.\n- Use a single divider between header and body rather than overlapping full borders. Inset\n surfaces with padding may have their own radius; account for the inset so their curves align.\n Clip artwork at its frame, not an entire interactive surface where focus rings or overlays\n could be cut off. Verify shared corners, borders, and keyboard focus at phone and desktop\n widths in both light and dark themes.\n- Use the shared token names for custom surfaces: `canvas`, `surface`, `surface-muted`,\n `surface-strong`, `line`, `ink`, `ink-soft`, `ink-muted`, `brand`, `brand-solid`, `brand-soft`,\n `accent`, `success`, `warning`, and `danger`.\n- Use `glass` as a boolean prop or attribute for glass surfaces when supported.\n- Preserve semantic labels, keyboard paths, focus states, and native form attributes.\n\n## Figma Handoff\n\nUse `exportThemeFigmaVariables` from `@santi020k/lumen-core/figma` when a design workflow needs\nFigma variable-friendly color values, and `exportThemeDesignTokens` when exporting standard design\ntoken JSON for token importers. See [docs/figma.md](figma.md) for the recommended Figma workflow.\n\n## When Editing This Repo\n\n- Shared contracts belong in `packages/core`.\n- Astro implementation and standalone CSS belong in `packages/astro`.\n- React-specific primitives belong in `packages/react`.\n- Web Component-specific primitives belong in `packages/elements`.\n- Update the relevant package README when public usage changes.\n- Add tests beside package code as `*.test.ts` when behavior, exported metadata, or component\n contracts change.\n\n### React inline range calendar\n\nFor a range editor with both endpoint calendars visible, use the React-only\n`DateRangeCalendar` export. Pass `value={{ start, end }}`, `onValueChange`, required\n`labels={{ start, end, presets }}`, `locale`, optional `min`/`max`, and optional\n`presets={[{ label, value: { start, end } }]}`. `formatDate` customizes the displayed\nendpoint summaries. Keep a draft in the consuming application and apply it only\nwhen confirmed. ISO endpoints must be valid and ascending. Crossing a boundary\nmoves the other endpoint; presets outside the bounds are disabled. The component\nshares `useCalendar` keyboard behavior and stacks its calendars on narrow screens.\n\nFor React input-attached range selection, import `DateRangeInput` from\n`@santi020k/lumen-react`. Pass controlled ISO `value`/`onValueChange`, an accessible\n`label`, and localized `labels` containing `start`, `end`, `presets`, `apply`, and\n`cancel`. It owns draft state and confirmation, anchors a non-modal popover to its\ntrigger, and supports keyboard dismissal and responsive scrolling. Use `validate`\nfor domain restrictions and `renderSummary` for draft details. Optional\n`name={{ start: 'from', end: 'to' }}` submits applied ISO values through hidden inputs.\nSee the React README for the complete usage and browser fallback contract.\n\n## Fixed-height virtual lists\n\n`VirtualList` uses the same DOM windowing controller in Astro, React and Elements. All rows remain\nmounted; offscreen rows are hidden, and inert spacers preserve the complete scroll height. This\nreduces displayed rows, not the cost of creating the initial DOM. Use pagination or an application\nrenderer when retaining the entire dataset in the DOM is too expensive.\n\nUse `itemSize` (Astro/React) or `item-size` (Elements) for a fixed row height in pixels, defaulting\nto 44. `overscan` defaults to four rows on each side. Every direct element child is a row; put\ncontrols inside it, and keep headers or empty-state actions outside the list. Rows must fit the\nconfigured height; variable-height measurement is not supported. Do not add external row margins\nor gaps. Initially hidden rows remain hidden and are excluded from the window.\n\nThe controller refreshes on scrolling, resizing, direct child changes and sizing attribute changes.\nA focused row and its neighboring rows remain available for native Tab navigation, which can extend\nthe displayed window until focus moves. Name the scroll container using `aria-label` or\n`aria-labelledby`; it is keyboard-focusable by default. `ui:virtual-list-range` reports inclusive\n`startIndex` and `endIndex` when the displayed range or item count changes. Empty lists use\n`{ startIndex: 0, endIndex: -1 }`. `data-ui-range-start` and `data-ui-range-end` mirror the current\nwindow. Elements disconnect and React unmount restore original row styles and hidden state.\n\n## NumberField boundary\n\n`NumberField` remains a native number-input wrapper. The v4 consumer audits do not establish a\nshared requirement for a locale-aware draft parser. Applications needing decimal-comma entry,\nlossless monetary drafts or currency rules should retain their explicit parsing and validation\npolicy; Lumen must not infer currency units or precision from locale.\n\n\n## Combobox keyboard behavior in v4\n\nCombobox keeps DOM focus in its input while Arrow Up/Down changes the active option through\n`aria-activedescendant`. Enter commits the active option only while the list is open; without an\nactive option, normal form submission remains available. Home, End, Left and Right retain native\ntext editing. Composition input and canceled keyboard events do not trigger selection or dismissal.\nEscape closes the innermost open control first, preserving an enclosing popup or dialog until the\nnext Escape. Tab dismisses the list when focus leaves the component.\n\nAstro and Elements observe added, removed, relabeled and disabled options, and delegate pointer\nselection to the current list. React derives options from its current props and reports committed\nselections through `onChange`, including controlled inputs. Disabled and read-only inputs stay closed.\nApplications should keep focus on the input and observe `aria-activedescendant` instead of calling\nfocus on option buttons. The DOM adapters match both option labels and values.\n\n## Appearance presets\n\nChoose `default`, `studio` or `glass` at the theme boundary. See [appearance presets](appearance-presets.md) for exact framework APIs. Studio follows the neutral PostLens look. Glass remains explicit per supporting surface; React Native and Compose retain solid fallbacks without adding blur dependencies. Customize semantic colors, typography and surface dimensions through public theme APIs.\n\n## Version-aware build, review, and migration\n\nThe portable skills are `lumen-ui` for implementation, `lumen-review` for a read-only audit, and\n`lumen-migrate` for an explicitly requested upgrade. Their canonical sources live under `skills/`;\n`pnpm run generate:plugin-package` produces the client snapshots. Both client manifests use plugin\n1.1.0 and pin the v4 catalog. This candidate is not proof of publication or directory approval.\n\nBefore applying MCP contracts, call `lumen_check_compatibility` with resolved versions from installed\nmetadata or native lock files. A manifest range is not an installed version. When versions differ,\ninspect installed public types and README or use a matching MCP package. Do not silently upgrade.\nSearch supports English and Spanish queries with framework and platform filters.\n\nFor a v4 upgrade, read `lumen_get_migration` and preview `lumen migrate v4 --dry-run --json` locally.\n`--apply` rewrites only known static SDK import paths in JavaScript and TypeScript. It preserves\ncomments and examples and reports UI/native review triggers. Dependency updates and application-owned\nworkarounds require review; the CLI does not infer that a workaround can be removed. JSX, Astro,\nnative source, dynamic imports, and dependency manifests are not automatically rewritten by the\nSDK import transform.\n\nSee [v4 AI verification](lumen-4-ai.md) for actual agent benchmark coverage and release evidence.\n\nThe integrated `lumen migrate v4` command also retains the release's web spacing migrations and\noptional coordinated dependency workflow. SDK edits compose into that command's source transform,\nso its apply ledger fingerprints the final source and repeat runs do not rewrite spacing twice.\nSDK import edits remain limited to `.ts`, `.js`, and `.mjs`; this restriction does not disable the\nseparate documented JSX/Astro spacing migration. The JSON report includes installed package\nversions and explicit SDK dependency-review findings.\n\n\n## Login examples and Auth\n\nUse the `auth-onboarding` recipe for visual email-code sign-in, code verification, and passkey\nrecovery states in Astro, React, or Elements. The preview controls are disabled and do not authenticate.\nLumen supplies the UI; the optional `@santi020k/auth-client` supplies protocol operations.\nSee the [login integration reference](../packages/templates/README.md#login-examples-and-auth)\nfor helper calls, safe result handling, and the application-owned server and session requirements.\n## Complex React forms\n\nUse the optional `@santi020k/lumen-react/forms` hooks with public form primitives for dependent\nvalidation, unsaved edits, stable repeatable rows, step navigation, and cancellation-safe checks.\nRead [composable form workflows](powerful-forms.md) before implementation. Choose one validation\nowner; keep schemas, financial policy, requests, authorization, and draft storage in the consumer.\n\nFor photo and media editors, install the `media-workspace` recipe and follow\n[Studio media workspaces](studio-media-workspaces.md). Reuse public media controls; keep\nimage processing, storage and export in the consumer application.\n",
|
|
46095
|
+
"readme": "<p align=\"center\">\n <a href=\"https://lumen.santi020k.com\">\n <picture>\n <source media=\"(prefers-color-scheme: dark)\" srcset=\"./docs/assets/readme/hero-dark.svg\">\n <img src=\"./docs/assets/readme/hero-light.svg\" alt=\"Lumen UI — A shared language. Every interface.\" width=\"1200\" height=\"340\">\n </picture>\n </a>\n</p>\n\n<h1 align=\"center\">Lumen UI</h1>\n\n<p align=\"center\">\n <strong>Accessible primitives. Native authoring. Shared foundations.</strong><br>\n An open-source UI system for Astro, React, Web Components, React Native, SwiftUI, and Jetpack Compose.\n</p>\n\n<p align=\"center\">\n <a href=\"https://lumen.santi020k.com\">Documentation</a> ·\n <a href=\"https://lumen.santi020k.com/docs/components\">Components</a> ·\n <a href=\"https://lumen.santi020k.com/#playgrounds\">Playgrounds</a> ·\n <a href=\"https://www.figma.com/community/file/1662337342676541513\">Figma library</a> ·\n <a href=\"https://lumen.santi020k.com/support\">Support</a>\n</p>\n\n<p align=\"center\">\n <a href=\"https://github.com/santi020k/lumen/actions/workflows/ci.yml\"><img alt=\"CI status on main\" src=\"https://img.shields.io/github/actions/workflow/status/santi020k/lumen/ci.yml?branch=main&style=flat-square&label=CI\"></a>\n <a href=\"https://github.com/santi020k/lumen/actions/workflows/codeql.yml\"><img alt=\"CodeQL status\" src=\"https://github.com/santi020k/lumen/actions/workflows/codeql.yml/badge.svg\"></a>\n <a href=\"https://www.npmjs.com/package/@santi020k/lumen-astro\"><img alt=\"Published Astro package version\" src=\"https://img.shields.io/npm/v/@santi020k/lumen-astro?style=flat-square&label=npm\"></a>\n <a href=\"./LICENSE\"><img alt=\"MIT license\" src=\"https://img.shields.io/badge/license-MIT-036b8c?style=flat-square\"></a>\n</p>\n\nLumen gives your product a consistent visual language while preserving each framework's native\nway of building. Compose accessible controls, forms, charts, and complete product layouts from\nshared tokens and explicit component contracts.\n\n**Explore:** [Quick start](#web-quick-start) · [Native apps](#native-playgrounds) · [Templates](#dashboards-and-templates) · [AI and Figma](#ai-and-design-workflows) · [Packages](#packages) · [Repository apps](#repository-apps) · [Contributing](#contributing)\n\n> **Lumen 4:** See the [upcoming v4.3 Tailwind complement](docs/lumen-4.3.md), the [v4.2 highlights](docs/lumen-4.2.md) for SwiftUI appearance and agent guidance,\n> and [v4.1 highlights](docs/lumen-4.1.md) for loading and forms. Follow the [migration guide](docs/migrating-v3-to-v4.md) when\n> upgrading from v3. Package versions and store builds follow their own release schedules.\n\n## See Lumen in action\n\n<p>\n <a href=\"https://lumen.santi020k.com/docs/web/playground\">\n <img src=\"./apps/docs/src/assets/platforms/web.webp\" alt=\"A Lumen web workspace with navigation, metric cards, an interactive chart, and a task form\" width=\"1184\" height=\"802\">\n </a>\n</p>\n\n*Real Lumen components with illustrative data. [Open the web playground →](https://lumen.santi020k.com/docs/web/playground)*\n\n| Built for | What you get |\n| --- | --- |\n| **Web products** | Astro, React, and custom elements with standalone CSS and optional Tailwind integration. |\n| **Native interfaces** | React Native, SwiftUI, Compose, WidgetKit, and Wear OS foundations with platform conventions. |\n| **Accessible interaction** | Semantic structure, keyboard paths, focus management, and reduced-motion support. |\n| **Design and AI workflows** | Figma resources, a portable agent skill, MCP discovery, and machine-readable contracts. |\n\n## Web quick start\n\nChoose one adapter. Astro requires Astro 5+, React requires React 19+, and the web packages\nrequire Node.js 22.12+ for their tooling. Install in your existing application:\n\n```bash\n# Astro\npnpm add @santi020k/lumen-astro\n\n# React\npnpm add @santi020k/lumen-react\n\n# Web Components\npnpm add @santi020k/lumen-elements\n```\n\n### Astro\n\nImport the stylesheet and mount `UIPrimitives` once in your root layout. The runtime enhances all\ninteractive Lumen markup on the page.\n\n```astro\n---\nimport '@santi020k/lumen-astro/styles.css'\nimport UIPrimitives from '@santi020k/lumen-astro/runtime'\n---\n\n<html lang=\"en\">\n <body>\n <slot />\n <UIPrimitives />\n </body>\n</html>\n```\n\n```astro\n---\nimport { Button, Card, Input, Label } from '@santi020k/lumen-astro'\n---\n\n<Card>\n <Label for=\"email\">Email</Label>\n <Input id=\"email\" name=\"email\" type=\"email\" placeholder=\"you@example.com\" />\n <Button>Subscribe</Button>\n</Card>\n```\n\n### React\n\nLoad the stylesheet once from your app entry or global CSS:\n\n```tsx\nimport '@santi020k/lumen-react/styles.css'\nimport { Button, Card, Input, Label } from '@santi020k/lumen-react'\n\nexport function SubscribeForm() {\n return (\n <Card>\n <Label htmlFor=\"email\">Email</Label>\n <Input id=\"email\" name=\"email\" type=\"email\" placeholder=\"you@example.com\" />\n <Button>Subscribe</Button>\n </Card>\n )\n}\n```\n\n### Web Components\n\nImport the styles and register the elements once:\n\n```html\n<style>\n @import \"@santi020k/lumen-elements/styles.css\";\n</style>\n\n<script type=\"module\">\n import { defineLumenElements } from '@santi020k/lumen-elements/define'\n\n defineLumenElements()\n</script>\n\n<lumen-card>\n <lumen-label id=\"email-label\">Email</lumen-label>\n <lumen-input aria-labelledby=\"email-label\" id=\"email\" name=\"email\" type=\"email\" placeholder=\"you@example.com\"></lumen-input>\n <lumen-button>Subscribe</lumen-button>\n</lumen-card>\n```\n\nSee the [Web documentation](https://lumen.santi020k.com/docs/web) for installation, theming,\ncomponent examples, and API details.\n\nBrowse the [package directory](https://lumen.santi020k.com/docs/packages) for framework adapters,\nshared foundations, and optional integrations, with links to focused examples and usage guides.\n\nFor failure states, use the [error-handling guide](./docs/error-handling.md) to choose between field\nfeedback, summaries, persistent alerts, transient toasts, and the `ErrorState` recovery surface.\nLumen owns presentation and accessibility; applications retain logging, retry, and exception policy.\n\nFor native applications, choose the [React Native](https://lumen.santi020k.com/docs/react-native),\n[Apple / SwiftUI](https://lumen.santi020k.com/docs/apple), or\n[Android / Compose](https://lumen.santi020k.com/docs/android) guide. The\n[shared foundations](https://lumen.santi020k.com/docs/foundations) explain the cross-platform token\nand component contract; repository contributors can also use the\n[cross-platform architecture](./docs/cross-platform.md) and\n[native component reference](./docs/native-components.md).\n\n> **Native qualification:** Check the [compatibility matrix](docs/native-compatibility.md) and\n> [device evidence](docs/native-device-validation.md) for your target. Local builds, published\n> artifacts, and physical-device accessibility are distinct verification steps.\n\n## Tailwind CSS\n\nUse Tailwind CSS v4 as an optional complement for responsive layouts and component customization.\nThe `tailwind.css` bridge (Lumen 4.3+) exposes Lumen semantic colors such as `bg-surface`,\n`text-ink`, and `border-line`, including nested themes. See the\n[Tailwind guide](https://lumen.santi020k.com/docs/web/tailwind) for framework examples and troubleshooting.\n\nKeep the layer prelude, Tailwind import, and Lumen stylesheet in the same shared CSS entry. Replace\n`lumen-astro` with the package for your framework.\n\n```css\n@import \"@santi020k/lumen-astro/layers.css\";\n@import \"tailwindcss\";\n@import \"@santi020k/lumen-astro/styles.css\";\n@import \"@santi020k/lumen-astro/tailwind.css\";\n```\n\nThis order places Tailwind base styles before Lumen components and Tailwind utilities above Lumen\ncomponent defaults.\n\n## Native playgrounds\n\n<table>\n <tr>\n <th>SwiftUI</th>\n <th>Jetpack Compose</th>\n <th>React Native</th>\n </tr>\n <tr>\n <td><a href=\"https://lumen.santi020k.com/docs/apple/playground\"><img src=\"./apps/docs/src/assets/platforms/apple.webp\" alt=\"Native SwiftUI release checklist example\" width=\"220\"></a></td>\n <td><a href=\"https://lumen.santi020k.com/docs/android/playground\"><img src=\"./apps/docs/src/assets/platforms/android.webp\" alt=\"Native Compose release checklist example\" width=\"220\"></a></td>\n <td><a href=\"https://lumen.santi020k.com/docs/react-native/playground\"><img src=\"./apps/docs/src/assets/platforms/react-native.webp\" alt=\"Expo browser preview of the contributor profile example\" width=\"220\"></a></td>\n </tr>\n <tr>\n <td>Native app capture</td>\n <td>Native app capture</td>\n <td>Expo browser preview</td>\n </tr>\n</table>\n\nSee [capture provenance](apps/docs/src/assets/platforms/README.md) for sources and verification scope.\n\nTry Lumen before adding it to your project. The [playground section](https://lumen.santi020k.com/#playgrounds)\nbrings together the browser demos and native apps. Explore component states and themes, then use\nthe open-source apps as implementation references. Start with the\n[web playground](https://lumen.santi020k.com/docs/web/playground) for Astro, React, and Web Components,\nor choose a native gallery:\n\n- [`apps/playground-react-native`](./apps/playground-react-native) runs through Expo on the web,\n iOS, and Android. [Try the browser preview](https://lumen.santi020k.com/docs/react-native/playground#preview)\n or use the local setup guide for native evaluation. EAS profiles cover TestFlight, Android App\n Bundles, and APKs; this gallery has no separate public store listing.\n- [`apps/playground-apple`](./apps/playground-apple) is available on the\n [App Store](https://apps.apple.com/app/id6805250815) for iPhone, iPad, and Mac. It also builds as\n an iOS Xcode app or macOS Swift Package executable for local exploration.\n- [`apps/playground-android`](./apps/playground-android) builds a native Compose application and a\n directly installable debug APK. It is also available on\n [Google Play](https://play.google.com/store/apps/details?id=com.santi020k.lumen.playground.compose)\n for Android phones and tablets.\n\nSee the [playground workflow](./docs/playgrounds.md) for a platform chooser, reference-app source,\nand run, capture, and distribution commands. Store builds follow their own release schedule and\nmay differ from the current repository version.\n\n## Appearance presets\n\nChoose an appearance preset or customize the existing themes with the [appearance guide](docs/appearance-presets.md). The Studio preset uses PostLens as its visual reference.\n\n## Dashboards and templates\n\nExplore the [template gallery](https://lumen.santi020k.com/templates) for complete analytics,\nSaaS admin, commerce, project workspace, and authentication/onboarding experiences. Each family\nuses public Lumen primitives and semantic tokens, includes responsive and accessibility coverage,\nand ships through the CLI for all three framework targets:\n\n```bash\nlumen add analytics-dashboard\nlumen add commerce-dashboard --target react\nlumen add auth-onboarding --target elements\n```\n\n## AI and design workflows\n\nInstall the portable Lumen skill in Codex, Claude Code, Cursor, Windsurf, and other compatible\ncoding agents:\n\n```bash\nnpx skills add santi020k/lumen --skill lumen-ui\n```\n\nThe skill teaches agents how to select, compose, theme, and verify Lumen primitives. Pair it with\n[`@santi020k/lumen-mcp`](./packages/mcp) when an agent needs to search the live catalog or retrieve\ncurrent source, props, tokens, and usage rules.\n\nFor ChatGPT and Codex, install the published\n[Lumen UI plugin](https://chatgpt.com/plugins/plugin_asdk_app_6a8f6c526c5481918eb8a48806fa112b)\nfrom the Plugins Directory. Select **Install plugin**, then mention **@Lumen UI** in a request such\nas “Find the right Lumen components for an accessible React settings screen.” The plugin bundles\nthe skill with the hosted, read-only catalog; no separate Lumen account or local MCP setup is needed.\n\nLumen is designed to reduce repetitive UI code and unnecessary agent context. Actual token usage\ndepends on the model, task, and corrections; see the [measurement method](docs/ai-efficiency.md).\n\n<details>\n<summary>Guides, registries, and integration references</summary>\n\nAdditional machine-readable surfaces include:\n\n- [`llms.txt`](./llms.txt) for a concise project map.\n- [`docs/ai-usage.md`](./docs/ai-usage.md) for downstream generation examples.\n- [`docs/styling-contract.md`](./docs/styling-contract.md) for stable parts and component variables.\n- [`docs/import-and-icon-performance.md`](./docs/import-and-icon-performance.md) for repeatable import and icon evidence.\n- [`docs/graphics.md`](./docs/graphics.md) for portable decorative artwork across web and native platforms.\n- [`docs/consumer-regression-fixtures.md`](./docs/consumer-regression-fixtures.md) for production-shaped local coverage.\n- [`docs/project-adoption.md`](./docs/project-adoption.md) for the sibling-project audit and staged\n web and native migration strategy.\n- [`registry/lumen.registry.json`](./registry/lumen.registry.json) for recipes and installable file\n groups.\n- [`docs/figma.md`](./docs/figma.md) for Figma variables and Code Connect workflows.\n- [`apps/figma-plugin/README.md`](./apps/figma-plugin/README.md) for the **beta** Figma plugin,\n Astro starters, and AI handoff.\n- [`plugins/lumen-ui`](./plugins/lumen-ui) for the packaged Codex and ChatGPT plugin, with its\n [submission and review record](./docs/openai-plugin-submission.md).\n- [`.claude-plugin/marketplace.json`](./.claude-plugin/marketplace.json) for the Claude Code\n marketplace, with its [installation and submission guide](./docs/claude-code-plugin.md).\n- [`docs/cross-platform.md`](./docs/cross-platform.md) for the native architecture, support tiers,\n and component-parity policy.\n- [`docs/native-patterns.md`](./docs/native-patterns.md) for developed native compositions and the\n boundary between Lumen presentation and platform-owned application structure.\n\n</details>\n\n## Packages\n\n| Package | Purpose |\n| --- | --- |\n| [`@santi020k/lumen-astro`](./packages/astro) | Reference implementation, Astro components, and progressive-enhancement runtime |\n| [`@santi020k/lumen-react`](./packages/react) | React components and behavior hooks |\n| [`@santi020k/lumen-react-hook-form`](./packages/react-hook-form) | Optional React Hook Form adapters for composite controls |\n| [`@santi020k/lumen-elements`](./packages/elements) | Standards-based Web Components |\n| [`@santi020k/lumen-core`](./packages/core) | Shared tokens, metadata, class helpers, and behavior contracts |\n| [`@santi020k/lumen-tokens`](./packages/tokens) | Canonical platform-neutral design token source |\n| [`@santi020k/lumen-react-native`](./packages/react-native) | React Native foundations and native primitives |\n| [`LumenUI`](./packages/swift) | Swift Package for iOS, iPadOS, macOS, tvOS, visionOS, and watchOS foundations and native primitives |\n| [`LumenWidgetUI`](./packages/swift-widget) | Focused WidgetKit-safe semantic text, icon, badge, and compact-stat foundations |\n| [`lumen-compose`](./packages/compose) | Android library for Jetpack Compose foundations and native primitives |\n| [`lumen-compose-wear`](./packages/compose/wear) | Focused Wear OS primitives for at-a-glance round-screen actions, progress, status, metrics, and rows |\n| [`@santi020k/lumen-icons-brand`](./packages/icons-brand) | Optional namespaced brand icons for web `Icon` adapters |\n| [`@santi020k/lumen`](./packages/lumen) | Umbrella package, CLI, registry metadata, and public package map |\n| [`@santi020k/lumen-mcp`](./packages/mcp) | MCP server for component discovery and source retrieval |\n\nThe private [`@santi020k/lumen-templates`](packages/templates/README.md) workspace supplies the\ngallery and preview renderer. Consumer recipes are installed through the public Lumen CLI.\n\n## Repository apps\n\nEach app has its own setup, source map, and verification guidance:\n\n| App | Purpose |\n| --- | --- |\n| [Documentation](apps/docs/README.md) | Public documentation, live examples, search, and native previews |\n| [Template showcase](apps/templates/README.md) | Standalone gallery of five product-template families |\n| [Next.js smoke app](apps/next-smoke/README.md) | Server/client boundaries and rendered consumer regression fixtures |\n| [Apple playground](apps/playground-apple/README.md) | iPhone, iPad, Mac, watchOS, and WidgetKit reference hosts |\n| [Android playground](apps/playground-android/README.md) | Compose phone/tablet app and separate Wear OS host |\n| [React Native playground](apps/playground-react-native/README.md) | Expo reference app and embedded browser gallery |\n| [Figma plugin](apps/figma-plugin/README.md) | Development beta for Astro starters and structured agent handoff |\n| [Motion studio](apps/motion/README.md) | Reusable product films and local video exports |\n| [Store artwork](apps/store-assets/README.md) | Canonical SVG sources for the playground app icons |\n\n## Lumen 4\n\nLumen 4 brings consumer-driven date selection, chart readability, clipboard feedback,\nloading states, native layout improvements and a new ImageComparison primitive. Start with the\n[v3 → v4 migration guide](docs/migrating-v3-to-v4.md), [consumer audit](docs/lumen-4-consumer-audit.md),\nand [v4.2 release highlights](docs/lumen-4.2.md). The community gallery includes published consumer\nscreenshots; it does not imply those applications already run the current release.\n\nFor existing applications, start with [v1 → v2](docs/migrating-v1-to-v2.md) when needed, then\n[v2 → v3](docs/migrating-v2-to-v3.md) before\n[v3 → v4](docs/migrating-v3-to-v4.md). For initial adoption, use\n[Migrating to Lumen](docs/migrating-to-lumen.md).\n\n## Contributing\n\nLumen is a pnpm workspace. Astro is the reference implementation; shared contracts belong in\n`packages/core`, while framework-specific behavior stays in its adapter package.\n\nUse Node.js 22.22.2+, 24.15.0+, or 26+ and pnpm 12.8.1, as declared in the root manifest.\n\n```bash\npnpm install --frozen-lockfile\npnpm run dev\npnpm run validate\n```\n\nRead [`CONTRIBUTING.md`](./CONTRIBUTING.md) before opening a pull request. User-visible package\nchanges require a changeset.\n\n## Community\n\nCreate reusable product animations and social videos locally with the\n[Lumen motion studio](./apps/motion/README.md). Its first pilot previews one interface in four\nappearances and exports portrait, square, and landscape MP4s through HyperFrames.\n\n[Feedback & support](https://lumen.santi020k.com/support) connects ideas, questions, bug reports,\nand the [public roadmap](https://lumen.santi020k.com/support#roadmap). Discuss improvements on GitHub\nand follow accepted work through implementation and release.\n\nLumen is free and MIT licensed while the project focuses on adoption, public examples, and\nreal-world feedback.\n\n- Explore task-oriented [guides](https://lumen.santi020k.com/guides).\n- Share a public project in the [community showcase](https://lumen.santi020k.com/community).\n- Evaluate the system with the open [team adoption path](https://lumen.santi020k.com/teams).\n- Tell us what would improve your next build through the\n [adoption survey](https://github.com/santi020k/lumen/issues/new?template=adoption-feedback.yml).\n\nMaintainers can use the [exposure playbook](./docs/exposure-playbook.md) for publishing cadence,\ncampaign measurement, video scripts, and launch copy, and the\n[marketing package](./docs/marketing/README.md) for execution-ready copy, channel decisions, a\nfour-week calendar, asset briefs, an approval queue, and a community-response library.\n\n## Find your next step\n\n| Resource | Use it for |\n| --- | --- |\n| [Cross-platform architecture](docs/cross-platform.md) | Shared foundations and adapter responsibilities. |\n| [Native compatibility](docs/native-compatibility.md) | Supported toolchains and platform baselines. |\n| [Native release runbook](docs/native-release-runbook.md) | Artifact qualification and distribution gates. |\n\n## License\n\nLumen's original code and artwork are available under the [MIT License](./LICENSE). Generated\ninterface icons retain the Lucide ISC or Feather MIT terms, and generated brand icons retain Font\nAwesome Free's CC BY 4.0 terms. The native packages include the required attribution and license\nnotices; see [`icons/THIRD_PARTY_NOTICES.md`](./icons/THIRD_PARTY_NOTICES.md).\n\nCreated and maintained by [Santiago Molina](https://santi020k.com).\n"
|
|
46096
46096
|
},
|
|
46097
46097
|
"meta": {
|
|
46098
|
-
"catalogHash": "
|
|
46098
|
+
"catalogHash": "b77700bb359d9cb8104c5a315ba84cdcd8f84a4127cb1f646851e69db314de2f",
|
|
46099
46099
|
"componentCount": 196,
|
|
46100
46100
|
"nativeComponentCount": 125,
|
|
46101
46101
|
"packages": [
|
|
@@ -46109,21 +46109,21 @@
|
|
|
46109
46109
|
"com.santi020k:lumen-compose"
|
|
46110
46110
|
],
|
|
46111
46111
|
"packageVersions": {
|
|
46112
|
-
"@santi020k/lumen-astro": "4.
|
|
46113
|
-
"@santi020k/lumen-core": "4.
|
|
46114
|
-
"@santi020k/lumen-elements": "4.
|
|
46115
|
-
"@santi020k/lumen-mcp": "4.
|
|
46116
|
-
"@santi020k/lumen-react": "4.
|
|
46117
|
-
"@santi020k/lumen-react-native": "4.0.
|
|
46112
|
+
"@santi020k/lumen-astro": "4.3.0",
|
|
46113
|
+
"@santi020k/lumen-core": "4.3.0",
|
|
46114
|
+
"@santi020k/lumen-elements": "4.3.0",
|
|
46115
|
+
"@santi020k/lumen-mcp": "4.3.0",
|
|
46116
|
+
"@santi020k/lumen-react": "4.3.0",
|
|
46117
|
+
"@santi020k/lumen-react-native": "4.0.2",
|
|
46118
46118
|
"@santi020k/lumen-tokens": "4.0.0",
|
|
46119
|
-
"LumenUI": "4.
|
|
46120
|
-
"com.santi020k:lumen-compose": "4.0.
|
|
46121
|
-
"com.santi020k:lumen-compose-wear": "4.0.
|
|
46119
|
+
"LumenUI": "4.3.0",
|
|
46120
|
+
"com.santi020k:lumen-compose": "4.0.1",
|
|
46121
|
+
"com.santi020k:lumen-compose-wear": "4.0.1"
|
|
46122
46122
|
},
|
|
46123
46123
|
"registryName": "lumen",
|
|
46124
46124
|
"registryVersion": 1,
|
|
46125
46125
|
"schemaVersion": 8,
|
|
46126
|
-
"serverVersion": "4.
|
|
46126
|
+
"serverVersion": "4.3.0"
|
|
46127
46127
|
},
|
|
46128
46128
|
"migration": {
|
|
46129
46129
|
"schemaVersion": 1,
|
|
@@ -60413,13 +60413,13 @@
|
|
|
60413
60413
|
"packages/react-native/src/command-recipes.test.ts": "import { expect, test } from 'vitest'\n\nimport { isLumenCommandGroupsValid, type LumenCommandGroup, lumenCommandGroups, moveLumenCommandActive, resolveLumenCommandActive } from './command-recipes.js'\nconst groups: readonly LumenCommandGroup[] = [\n { id: 'navigation',\n label: 'Navigation',\n items: [\n { id: 'docs', label: 'Documentation', detail: 'Read guides', keywords: ['manual'], shortcut: 'Ctrl+D' },\n { id: 'blocked', label: 'Disabled', disabled: true }\n ] },\n { id: 'actions', label: 'Actions', items: [{ id: 'theme', label: 'Toggle theme' }] }\n]\ntest('case-insensitive substring filtering retains groups/order and matches host synonyms', () => {\n for (const query of [' DOCUMENT ', 'guides', 'manual', 'ctrl+d']) {\n expect(lumenCommandGroups(groups, query)?.map(group => group.items.map(item => item.id))).toEqual([['docs']])\n }\n expect(lumenCommandGroups(groups, 'missing')).toEqual([])\n expect(groups[0]?.items).toHaveLength(2)\n})\ntest('navigation wraps enabled commands, boundaries work, stale/disabled IDs cannot activate', () => {\n expect(moveLumenCommandActive(groups, '', null, 'next')).toBe('docs')\n expect(moveLumenCommandActive(groups, '', null, 'previous')).toBe('theme')\n expect(moveLumenCommandActive(groups, '', 'docs', 'next')).toBe('theme')\n expect(moveLumenCommandActive(groups, '', 'theme', 'next')).toBe('docs')\n expect(moveLumenCommandActive(groups, '', 'docs', 'previous')).toBe('theme')\n expect(moveLumenCommandActive(groups, '', 'docs', 'last')).toBe('theme')\n expect(moveLumenCommandActive(groups, '', 'theme', 'first')).toBe('docs')\n expect(resolveLumenCommandActive(groups, 'theme', 'docs')).toBeNull()\n expect(resolveLumenCommandActive(groups, '', 'blocked')).toBeNull()\n expect(resolveLumenCommandActive(groups, '', 'unknown')).toBeNull()\n expect(moveLumenCommandActive(groups, 'Disabled', null, 'next')).toBeNull()\n})\ntest('identity is validated before filtering and handles large literal queries without regex', () => {\n for (const input of [[...groups, groups[0] ?? { id: 'navigation', label: '', items: [] }], [{ id: ' ', label: 'Blank', items: [] }], [{ id: 'a', label: 'A', items: [{ id: 'x', label: 'X' }] }, { id: 'b', label: 'B', items: [{ id: 'x', label: 'Duplicate' }] }]]) {\n expect(isLumenCommandGroupsValid(input)).toBe(false)\n expect(lumenCommandGroups(input, 'missing')).toBeNull()\n }\n expect(lumenCommandGroups(groups, '('.repeat(100000))).toEqual([])\n expect(lumenCommandGroups([], '')).toEqual([])\n})\n\ntest('rejects decoded groups, nested items and searchable fields without throwing', () => {\n const malformed: unknown[] = [null,\n 42,\n [],\n {},\n { id: null, label: '', items: [] },\n { id: 'g', label: null, items: [] },\n { id: 'g', label: '', items: null },\n ...[null,\n 42,\n {},\n { id: null, label: '' },\n { id: 'a', label: 42 },\n { id: 'a', label: '', detail: 42 },\n { id: 'a', label: '', shortcut: {} },\n { id: 'a', label: '', keywords: 'word' },\n { id: 'a', label: '', keywords: [null] },\n { id: 'a', label: '', keywords: new Array<unknown>(1) }]\n .map(item => ({ id: 'g', label: '', items: [item] }))]\n\n for (const input of [...malformed.map(group => [group]), null, {}, 'groups', new Array<unknown>(1)]) {\n expect(Reflect.apply(isLumenCommandGroupsValid, undefined, [input])).toBe(false)\n expect(Reflect.apply(lumenCommandGroups, undefined, [input, 'a'])).toBeNull()\n }\n})\n\ntest('rejects decoded non-string queries throughout filtering and keyboard helpers', () => {\n for (const query of [null, {}, 7, ['docs']]) {\n expect(Reflect.apply(lumenCommandGroups, undefined, [groups, query])).toBeNull()\n expect(Reflect.apply(resolveLumenCommandActive, undefined, [groups, query, 'docs'])).toBeNull()\n expect(Reflect.apply(moveLumenCommandActive, undefined, [groups, query, 'docs', 'next'])).toBeNull()\n }\n})\n",
|
|
60414
60414
|
"packages/react-native/src/command-recipes.ts": "export interface LumenCommandItem {\n id: string\n label: string\n detail?: string\n keywords?: readonly string[]\n shortcut?: string\n disabled?: boolean\n}\nexport interface LumenCommandGroup { id: string, label: string, items: readonly LumenCommandItem[] }\nexport type LumenCommandNavigation = 'next' | 'previous' | 'first' | 'last'\n\nconst isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null && !Array.isArray(value)\nconst optionalBoolean = (value: unknown): boolean => value === undefined || typeof value === 'boolean'\nconst optionalString = (value: unknown): boolean => value === undefined || typeof value === 'string'\n\nconst validKeywords = (value: unknown): boolean => {\n if (value === undefined) return true\n\n if (!Array.isArray(value)) return false\n\n for (const keyword of value as readonly unknown[]) if (typeof keyword !== 'string') return false\n\n return true\n}\n\nconst validCommand = (value: unknown): value is LumenCommandItem => {\n if (!isRecord(value) || typeof value.id !== 'string' || typeof value.label !== 'string') return false\n\n return optionalString(value.detail) && optionalString(value.shortcut) &&\n optionalBoolean(value.disabled) && validKeywords(value.keywords)\n}\n\nconst validGroup = (value: unknown): value is LumenCommandGroup => {\n if (!isRecord(value) || typeof value.id !== 'string' || typeof value.label !== 'string' || !Array.isArray(value.items)) return false\n\n for (const item of value.items as readonly unknown[]) if (!validCommand(item)) return false\n\n return true\n}\n\nexport const isLumenCommandGroupsValid = (groups: readonly LumenCommandGroup[]): boolean => {\n if (!Array.isArray(groups)) return false\n\n const groupIds = new Set<string>()\n const itemIds = new Set<string>()\n\n for (const group of groups as readonly unknown[]) {\n if (!validGroup(group) || group.id.trim().length === 0 || groupIds.has(group.id)) return false\n\n groupIds.add(group.id)\n\n for (const item of group.items) {\n if (item.id.trim().length === 0 || itemIds.has(item.id)) return false\n\n itemIds.add(item.id)\n }\n }\n\n return true\n}\n\nconst commandMatches = (item: LumenCommandItem, query: string): boolean => [item.label || item.id, item.detail ?? '', item.shortcut ?? '', ...(item.keywords ?? [])]\n .some(text => text.toLowerCase().includes(query))\n\nexport const lumenCommandGroups = (\n groups: readonly LumenCommandGroup[], query: string\n): readonly LumenCommandGroup[] | null => {\n if (typeof query !== 'string' || !isLumenCommandGroupsValid(groups)) return null\n\n const search = query.trim().toLowerCase()\n\n return groups.map(group => ({ ...group, items: group.items.filter(item => commandMatches(item, search)) }))\n .filter(group => group.items.length > 0)\n}\nexport const resolveLumenCommandActive = (\n groups: readonly LumenCommandGroup[], query: string, activeId: string | null\n): LumenCommandItem | null => {\n const filtered = lumenCommandGroups(groups, query)\n\n if (!filtered || activeId === null) return null\n\n return filtered.flatMap(group => group.items).find(item => item.id === activeId && !item.disabled) ?? null\n}\n\nconst enabledCommands = (groups: readonly LumenCommandGroup[], query: string): readonly LumenCommandItem[] => {\n const filtered = lumenCommandGroups(groups, query)\n\n if (!filtered) return []\n\n return filtered.flatMap(group => group.items).filter(item => !item.disabled)\n}\n\nconst boundaryCommand = (items: readonly LumenCommandItem[], direction: LumenCommandNavigation): string | null => {\n const item = direction === 'next' || direction === 'first' ? items[0] : items.at(-1)\n\n return item?.id ?? null\n}\n\nexport const moveLumenCommandActive = (\n groups: readonly LumenCommandGroup[], query: string, activeId: string | null, direction: LumenCommandNavigation\n): string | null => {\n const items = enabledCommands(groups, query)\n\n if (items.length === 0) return null\n\n if (direction === 'first') return boundaryCommand(items, direction)\n\n if (direction === 'last') return boundaryCommand(items, direction)\n\n const index = items.findIndex(item => item.id === activeId)\n\n if (index < 0) return boundaryCommand(items, direction)\n\n const delta = direction === 'next' ? 1 : -1\n\n return items[(index + delta + items.length) % items.length]?.id ?? null\n}\n",
|
|
60415
60415
|
"packages/react-native/src/comparison-components.tsx": "import { type ReactElement, useState } from 'react'\nimport { I18nManager, type ImageSourcePropType, View, type ViewProps, type ViewStyle } from 'react-native'\n\nimport { formatLumenImageComparisonValue, type LumenImageComparisonMode, normalizeLumenImageComparisonMode } from '@santi020k/lumen-core'\n\nimport { LumenText } from './foundation-primitives.js'\nimport { LumenImage, type LumenImageFit } from './media-components.js'\nimport { useLumenTheme } from './theme-context.js'\nimport { LumenSlider } from './value-components.js'\n\nexport interface LumenImageComparisonProps extends Omit<ViewProps, 'children'> {\n after: ImageSourcePropType\n afterLabel?: string\n aspectRatio?: number\n before: ImageSourcePropType\n beforeLabel?: string\n enabled?: boolean\n fit?: LumenImageFit\n label: string\n locale?: string\n mode?: LumenImageComparisonMode\n onValueChange: (value: number) => void\n value: number\n}\n\nconst comparisonPosition = (value: number): number => Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : 0.5\n\nconst comparisonRatio = (ratio: number): number => {\n if (!Number.isFinite(ratio) || ratio < 0.1 || ratio > 10) return 16 / 9\n\n return ratio\n}\n\nconst comparisonEdge = (rtl: boolean): Pick<ViewStyle, 'left' | 'right'> => rtl ? { right: 0 } : { left: 0 }\n\nconst ComparisonAlternatePreview = ({ mode, ratio, before, after, beforeLabel, afterLabel, label, fit }: {\n mode: LumenImageComparisonMode\n ratio: number\n before: ImageSourcePropType\n after: ImageSourcePropType\n beforeLabel: string\n afterLabel: string\n label: string\n fit: LumenImageFit\n}) => {\n const theme = useLumenTheme()\n\n return (\n <View style={{ gap: theme.spacing.sm }}>\n <LumenText>{label}</LumenText>\n <View style={{ flexDirection: mode === 'side-by-side' ? 'row' : 'column', gap: theme.spacing.sm }}>\n {mode !== 'after' && (\n <View style={{ flex: 1, minWidth: 0, gap: theme.spacing.xs }}>\n <LumenImage decorative fit={fit} radius=\"lg\" source={before} aspectRatio={ratio} />\n <LumenText>{beforeLabel}</LumenText>\n </View>\n )}\n {mode !== 'before' && (\n <View style={{ flex: 1, minWidth: 0, gap: theme.spacing.xs }}>\n <LumenImage decorative fit={fit} radius=\"lg\" source={after} aspectRatio={ratio} />\n <LumenText>{afterLabel}</LumenText>\n </View>\n )}\n </View>\n </View>\n )\n}\n\nexport const LumenImageComparison = ({\n after, afterLabel = 'After', aspectRatio = 16 / 9, before, beforeLabel = 'Before',\n enabled = true, fit = 'cover', label, locale, mode: requestedMode, onValueChange, style, value, ...props\n}: LumenImageComparisonProps): ReactElement => {\n const mode = normalizeLumenImageComparisonMode(requestedMode)\n const theme = useLumenTheme()\n const [width, setWidth] = useState(0)\n const position = comparisonPosition(value)\n const ratio = comparisonRatio(aspectRatio)\n const percentage = formatLumenImageComparisonValue(position * 100, '', locale).trim()\n const rtl = I18nManager.isRTL\n\n return (\n <View {...props} style={[{ gap: theme.spacing.sm }, style]}>\n {mode !== 'reveal' ?\n (\n <ComparisonAlternatePreview\n mode={mode}\n ratio={ratio}\n before={before}\n after={after}\n beforeLabel={beforeLabel}\n afterLabel={afterLabel}\n label={label}\n fit={fit}\n />\n ) :\n (\n <>\n <View\n onLayout={event => {\n setWidth(event.nativeEvent.layout.width)\n }}\n style={{ aspectRatio: ratio, borderRadius: theme.radii.lg, overflow: 'hidden', width: '100%' }}\n >\n <LumenImage decorative fit={fit} radius=\"none\" source={before} style={{ height: '100%', position: 'absolute' }} />\n <View style={{ bottom: 0, ...comparisonEdge(rtl), overflow: 'hidden', position: 'absolute', top: 0, width: `${position * 100}%` }}>\n <LumenImage decorative fit={fit} radius=\"none\" source={after} style={{ height: '100%', ...comparisonEdge(rtl), position: 'absolute', width }} />\n </View>\n <View pointerEvents=\"none\" style={{ backgroundColor: theme.colors.ink, bottom: 0, left: `${(rtl ? 1 - position : position) * 100}%`, position: 'absolute', top: 0, width: theme.spacing.xs }} />\n </View>\n <View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>\n <LumenText>{afterLabel}</LumenText>\n <LumenText>{beforeLabel}</LumenText>\n </View>\n <LumenSlider enabled={enabled} label={label} max={1} min={0} onValueChange={onValueChange} step={0.01} value={position} valueLabel={`${afterLabel} ${percentage}`} />\n </>\n )}\n </View>\n )\n}\n",
|
|
60416
|
-
"packages/react-native/src/component-behavior.test.tsx": "// cspell:words Espacios Configuración Cambios guardados Registro actualizado Otro\nimport { act, type ComponentRef, createRef, type ReactElement, type Ref, StrictMode, useImperativeHandle, useState } from 'react'\nimport type { View } from 'react-native'\n\nimport { DateTimePickerAndroid } from '@react-native-community/datetimepicker'\nimport { getLumenPhoneCountry } from '@santi020k/lumen-core'\nimport type { LumenMediaViewportValue } from '@santi020k/lumen-core/media-workspace'\nimport { createRoot, type Root, type TestInstance } from 'test-renderer'\nimport { afterEach, describe, expect, test, vi } from 'vitest'\n\nimport { LumenBrandGithubIconGraphic } from './static-icons/brand-github.generated.js'\nimport { LumenSearchIconGraphic } from './static-icons/search.generated.js'\nimport { LumenButtonGroup, LumenChip, LumenFieldGroup, LumenTextarea, LumenToast } from './additional-components.js'\nimport { LumenAutocomplete, LumenInputOTP, LumenNumberField, LumenPasswordField } from './advanced-form-components.js'\nimport { LumenBreadcrumb } from './breadcrumb-components.js'\nimport { LumenBarChart, LumenComboChart, LumenHeatmap, LumenHistogram, LumenLineChart, LumenRangeChart, LumenScatterChart, LumenWaterfallChart } from './chart-components.js'\nimport { LumenImageComparison } from './comparison-components.js'\nimport { LumenDateField, LumenDateRangeField } from './datetime-components.js'\nimport { LumenSearchField, LumenToggle } from './form-components.js'\nimport { LumenIcon as GraphicIcon, LumenIconButton as GraphicIconButton, type LumenIconGraphicProps } from './graphics.js'\nimport { type LumenToastHookController, type LumenToastHookOptions, useToast } from './hooks.js'\nimport { LumenMediaThumbnail, LumenMediaViewport } from './media-workspace-components.js'\nimport { LumenMultiSelect } from './multi-select-components.js'\nimport { LumenAlertDialog, LumenMenu, LumenSheet } from './overlay-components.js'\nimport { LumenPhoneInput } from './phone-components.js'\nimport { resolveLumenPhoneInputValue } from './phone-recipes.js'\nimport { LumenNavigationBar } from './platform-components.js'\nimport { LumenButton, LumenIcon, LumenIconButton, LumenText, LumenTextField } from './primitives.js'\nimport { LumenProvider } from './provider.js'\nimport { LumenRating } from './rating-components.js'\nimport { resolveLumenRating } from './rating-recipes.js'\nimport { LumenCheckbox, LumenTabs } from './selection-components.js'\nimport { LumenStepper } from './stepper-components.js'\nimport { resolveLumenStepState } from './stepper-recipes.js'\nimport { LumenBanner, LumenErrorState, LumenStatusBar } from './structured-components.js'\nimport { LumenDataTable } from './table-components.js'\nimport type { LumenTableRow } from './table-recipes.js'\nimport { createLumenTheme } from './theme.js'\nimport { LumenTimeField, type LumenTimeFieldProps } from './time-components.js'\nimport { LumenTimeline, LumenTimelineItem } from './timeline-components.js'\nimport { LumenPicker, LumenRangeSlider, LumenSlider } from './value-components.js'\n\nObject.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })\n\ntest.each([\n { labelledBy: 'consumer-label' },\n { labelledBy: ['consumer-label', 'consumer-context'] },\n { labelledBy: '' },\n { labelledBy: [] }\n])('grouped text fields retain explicit native labelled-by ownership: $labelledBy', async ({ labelledBy }) => {\n const root = await renderNative(\n <LumenFieldGroup label=\"Inherited label\">\n <LumenTextField accessibilityLabelledBy={labelledBy} />\n </LumenFieldGroup>\n )\n const input = root.container.queryAll(instance => instance.type === 'TextInput')[0]\n if (!input) throw new Error('Expected grouped text field')\n expect(readProp(input, 'accessibilityLabelledBy')).toEqual(labelledBy)\n expect(readProp(input, 'aria-labelledby')).toBeUndefined()\n expect(readProp(input, 'accessibilityLabel')).toBeUndefined()\n})\n\ntest('grouped text fields preserve explicit ARIA and native label references together', async () => {\n const root = await renderNative(\n <LumenFieldGroup label=\"Inherited label\">\n <LumenTextField accessibilityLabelledBy=\"native-label\" aria-labelledby=\"aria-label\" />\n </LumenFieldGroup>\n )\n const input = root.container.queryAll(instance => instance.type === 'TextInput')[0]\n if (!input) throw new Error('Expected grouped text field')\n expect(readProp(input, 'accessibilityLabelledBy')).toBe('native-label')\n expect(readProp(input, 'aria-labelledby')).toBe('aria-label')\n expect(readProp(input, 'accessibilityLabel')).toBeUndefined()\n})\n\nconst nativeDirection = vi.hoisted(() => ({ isRTL: false }))\nconst nativePlatform = vi.hoisted(() => ({ OS: 'ios' }))\nconst nativeMotion = vi.hoisted(() => ({ enabled: false }))\nconst accessibilityFocus = vi.hoisted(() => vi.fn())\nconst accessibilityAnnouncement = vi.hoisted(() => vi.fn<(message: string, options: { queue: boolean }) => void>())\ntype RecommendedTimeout = (duration: number) => Promise<number>\nconst accessibilityTimeout = vi.hoisted(() => vi.fn<RecommendedTimeout>(duration => Promise.resolve(duration)))\nconst nativeWindow = vi.hoisted(() => ({ fontScale: 1, height: 800, scale: 2, width: 400 }))\nconst safeAreaInsets = vi.hoisted(() => ({\n bottom: 34,\n left: 8,\n right: 12,\n top: 20\n}))\n\nvi.mock('react-native', async () => {\n const { createElement, isValidElement, useImperativeHandle } = await import('react')\n const hostComponent = (name: string) => (\n props: Record<string, unknown>\n ): ReactElement => createElement(name, props)\n type FocusablePressableProps = Record<string, unknown> & {\n ref?: Ref<{ focus: () => void }>\n }\n const FocusablePressable = ({ ref, ...props }: FocusablePressableProps): ReactElement => {\n useImperativeHandle(ref, () => ({ focus: vi.fn() }))\n\n return createElement('Pressable', props)\n }\n\n return {\n AccessibilityInfo: {\n addEventListener: () => ({ remove: vi.fn() }),\n announceForAccessibilityWithOptions: accessibilityAnnouncement,\n getRecommendedTimeoutMillis: accessibilityTimeout,\n isReduceMotionEnabled: () => Promise.resolve(nativeMotion.enabled),\n sendAccessibilityEvent: accessibilityFocus\n },\n ActivityIndicator: hostComponent('ActivityIndicator'),\n FlatList: (props: Record<string, unknown>): ReactElement => {\n const renderItem = props.renderItem\n const data: readonly unknown[] = Array.isArray(props.data) ? props.data : []\n const children = data.map((item, index) => {\n if (typeof renderItem !== 'function') return null\n\n const rendered: unknown = Reflect.apply(renderItem, undefined, [{ item, index }])\n\n return isValidElement(rendered) ? rendered : null\n })\n\n return createElement('FlatList', props, children)\n },\n I18nManager: nativeDirection,\n Image: hostComponent('Image'),\n KeyboardAvoidingView: hostComponent('KeyboardAvoidingView'),\n Modal: hostComponent('Modal'),\n Platform: nativePlatform,\n Pressable: FocusablePressable,\n ScrollView: hostComponent('ScrollView'),\n Share: { share: () => Promise.resolve({ action: 'sharedAction' }) },\n Switch: hostComponent('Switch'),\n Text: hostComponent('Text'),\n TextInput: hostComponent('TextInput'),\n useColorScheme: () => 'light',\n useWindowDimensions: () => nativeWindow,\n View: hostComponent('View')\n }\n})\n\nvi.mock('@react-native-community/datetimepicker', async () => {\n const { createElement } = await import('react')\n const NativeDatePicker = (props: Record<string, unknown>): ReactElement => (\n createElement('NativeDatePicker', props)\n )\n\n return {\n DateTimePickerAndroid: { dismiss: vi.fn(), open: vi.fn() },\n default: NativeDatePicker\n }\n})\n\nvi.mock('react-native-svg', async () => {\n const { createElement } = await import('react')\n const hostComponent = (name: string) => (\n props: Record<string, unknown>\n ): ReactElement => createElement(name, props)\n\n return Object.fromEntries(\n ['Circle', 'Defs', 'Ellipse', 'G', 'Line', 'LinearGradient', 'Path', 'Polygon', 'Polyline', 'Rect', 'Stop', 'Svg', 'Text']\n .map(name => [name, hostComponent(name)])\n )\n})\n\nconst mountedRoots: Root[] = []\n\ninterface ToastProbeProps {\n ref: Ref<LumenToastHookController>\n options?: LumenToastHookOptions\n}\n\nconst ToastProbe = ({ ref, options }: ToastProbeProps): ReactElement | null => {\n const controller = useToast(options)\n\n useImperativeHandle(ref, () => controller, [controller])\n\n return null\n}\n\nconst readToastController = (ref: { current: LumenToastHookController | null }): LumenToastHookController => {\n if (!ref.current) throw new Error('Toast controller is not mounted')\n\n return ref.current\n}\n\nconst deferredTimeout = () => {\n let complete: (duration: number) => void = () => {\n throw new Error('Missing timeout resolver')\n }\n const promise = new Promise<number>(resolve => {\n complete = resolve\n })\n\n return { promise, resolve: complete }\n}\n\nconst renderNative = async (element: ReactElement): Promise<Root> => {\n const root = createRoot({\n publicTextComponentTypes: ['Text'],\n textComponentTypes: ['RCTText', 'Text']\n })\n\n mountedRoots.push(root)\n\n await act(async () => {\n root.render(<LumenProvider scheme=\"light\">{element}</LumenProvider>)\n await Promise.resolve()\n })\n\n return root\n}\n\nconst readProp = (instance: TestInstance, property: string): unknown => {\n const props = instance.props as Record<string, unknown>\n\n return props[property]\n}\n\nconst callAction = (value: unknown, missingActionMessage: string): void => {\n if (typeof value !== 'function') throw new Error(missingActionMessage)\n\n Reflect.apply(value, undefined, [])\n}\n\nconst callKeyboardAction = (value: unknown, key: string): ReturnType<typeof vi.fn> => {\n if (typeof value !== 'function') throw new Error('Tab is missing its keyboard action.')\n\n const preventDefault = vi.fn()\n\n Reflect.apply(value, undefined, [{\n nativeEvent: { code: key, key },\n preventDefault\n }])\n\n return preventDefault\n}\n\nconst findByAccessibilityRole = (root: Root, role: string): TestInstance => {\n const matches = root.container.queryAll(\n instance => readProp(instance, 'accessibilityRole') === role\n )\n\n expect(matches).toHaveLength(1)\n\n const match = matches[0]\n\n if (!match) throw new Error(`Expected one ${role} accessibility node.`)\n\n return match\n}\n\nconst findByAccessibilityLabel = (root: Root, label: string): TestInstance => {\n const matches = root.container.queryAll(\n instance => readProp(instance, 'accessibilityLabel') === label\n )\n\n expect(matches).toHaveLength(1)\n\n const match = matches[0]\n\n if (!match) throw new Error(`Expected one accessibility node labeled ${label}.`)\n\n return match\n}\n\nconst findHostComponent = (root: Root, type: string): TestInstance => {\n const matches = root.container.queryAll(instance => instance.type === type)\n\n expect(matches).toHaveLength(1)\n\n const match = matches[0]\n\n if (!match) throw new Error(`Expected one ${type} host component.`)\n\n return match\n}\n\nafterEach(async () => {\n nativeDirection.isRTL = false\n const roots = mountedRoots.splice(0)\n\n await act(async () => {\n for (const root of roots) root.unmount()\n await Promise.resolve()\n })\n nativeWindow.fontScale = 1\n nativePlatform.OS = 'ios'\n accessibilityAnnouncement.mockClear()\n accessibilityTimeout.mockReset().mockImplementation(duration => Promise.resolve(duration))\n vi.useRealTimers()\n vi.unstubAllGlobals()\n})\n\ndescribe('Lumen React Native component behavior', () => {\n test.each(['bar', 'line', 'scatter', 'range', 'combo'] as const)(\n '%s chart fits its container and resizes without removing data', async kind => {\n const data = [3, 7, 4, 8, 5].map((y, x) => ({ x, y }))\n const series = [{ id: 'activity', label: 'Changes', data }]\n const props = { label: 'Weekly activity', showData: false, series }\n const charts = {\n bar: <LumenBarChart {...props} />,\n line: <LumenLineChart {...props} />,\n scatter: <LumenScatterChart {...props} />,\n range: <LumenRangeChart label=\"Weekly range\" showData={false} data={data.map(({ x, y }) => ({ x, low: y, high: y + 2 }))} />,\n combo: <LumenComboChart {...props} series={[{ id: 'activity', label: 'Changes', data, mark: 'bar' }]} />\n }\n const root = await renderNative(charts[kind])\n const layouts = root.container.queryAll(instance => instance.type === 'View' && typeof readProp(instance, 'onLayout') === 'function')\n expect(layouts).toHaveLength(1)\n const layout = layouts[0]\n if (!layout) throw new Error('Missing chart container')\n const handler = readProp(layout, 'onLayout')\n if (typeof handler !== 'function') throw new Error('Missing chart layout handler')\n\n for (const width of [268, 720, 300, 0, Number.NaN]) {\n await act(async () => {\n Reflect.apply(handler, undefined, [{ nativeEvent: { layout: { width } } }])\n await Promise.resolve()\n })\n const expectedWidth = width > 0 && Number.isFinite(width) ? width : 300\n const svg = findHostComponent(root, 'Svg')\n expect(readProp(svg, 'width')).toBe(expectedWidth)\n expect(readProp(svg, 'height')).toBe(Math.min(320, Math.max(220, expectedWidth * 0.75)))\n expect(root.container.queryAll(instance => instance.type === 'ScrollView')).toHaveLength(0)\n const bars = root.container.queryAll(instance => instance.type === 'Rect')\n expect(bars).toHaveLength(['bar', 'combo'].includes(kind) ? 5 : 0)\n for (const bar of bars) {\n const coordinates = /translate\\(([-\\d.]+) ([-\\d.]+)\\)/u.exec(String(readProp(bar, 'transform')))\n if (!coordinates?.[1]) throw new Error('Missing bar coordinates')\n expect(Number(coordinates[1])).toBeGreaterThanOrEqual(0)\n expect(Number(coordinates[1]) + Number(readProp(bar, 'width'))).toBeLessThanOrEqual(expectedWidth)\n }\n }\n }\n )\n\n test.each([\n { rtl: false, values: [20, 50, 120, 20, 120] },\n { rtl: true, values: [120, 100, 20, 120, 20] }\n ])('slider touch follows RTL=$rtl while accessibility values remain numeric', async ({ rtl, values }) => {\n nativeDirection.isRTL = rtl\n const onValueChange = vi.fn<(value: number) => void>()\n const root = await renderNative(\n <LumenSlider label=\"Volume\" min={20} max={120} step={10} value={70} onValueChange={onValueChange} />\n )\n const slider = findByAccessibilityRole(root, 'adjustable')\n expect(readProp(slider, 'accessible')).toBe(true)\n const dispatch = async (property: string, nativeEvent: unknown): Promise<void> => {\n const handler = readProp(slider, property)\n if (typeof handler !== 'function') throw new Error(`Missing slider ${property} callback`)\n await act(async () => {\n Reflect.apply(handler, undefined, [{ nativeEvent }])\n await Promise.resolve()\n })\n }\n await dispatch('onLayout', { layout: { width: 200 } })\n for (const [index, position] of [0, 50, 200, -50, 250].entries()) {\n for (const event of ['onResponderGrant', 'onResponderMove']) {\n await dispatch(event, { locationX: position })\n expect(onValueChange).toHaveBeenLastCalledWith(values[index])\n }\n }\n await dispatch('onAccessibilityAction', { actionName: 'increment' })\n expect(onValueChange).toHaveBeenLastCalledWith(80)\n await dispatch('onAccessibilityAction', { actionName: 'decrement' })\n expect(onValueChange).toHaveBeenLastCalledWith(60)\n expect(readProp(slider, 'accessibilityValue')).toMatchObject({ min: 20, max: 120, now: 70 })\n })\n\n test('range endpoints stay controlled, independently named and cannot cross', async () => {\n const onValueChange = vi.fn<(value: readonly [number, number]) => void>()\n const root = await renderNative(\n <LumenRangeSlider\n label=\"Capacity\"\n startLabel=\"Lower\"\n endLabel=\"Upper\"\n value={[20, 60]}\n min={0}\n max={100}\n step={10}\n onValueChange={onValueChange}\n formatValue={value => `${value}%`}\n />\n )\n const sliders = root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'adjustable')\n expect(sliders).toHaveLength(2)\n const [start, end] = sliders\n if (!start || !end) throw new Error('Missing interval endpoints')\n expect(readProp(start, 'accessibilityLabel')).toBe('Capacity · Lower')\n expect(readProp(end, 'accessibilityLabel')).toBe('Capacity · Upper')\n expect(readProp(start, 'accessibilityValue')).toMatchObject({ now: 20, text: '20%' })\n expect(readProp(start, 'aria-valuenow')).toBe(20)\n expect(readProp(start, 'aria-valuetext')).toBe('20%')\n const move = async (slider: TestInstance, direction: string): Promise<void> => {\n const handler = readProp(slider, 'onAccessibilityAction')\n if (typeof handler !== 'function') throw new Error('Missing range adjustment')\n await act(async () => {\n Reflect.apply(handler, undefined, [{ nativeEvent: { actionName: direction } }])\n await Promise.resolve()\n })\n }\n await move(start, 'increment')\n expect(onValueChange).toHaveBeenLastCalledWith([30, 60])\n await move(end, 'decrement')\n expect(onValueChange).toHaveBeenLastCalledWith([20, 50])\n // The application has not accepted either update; subsequent intent uses its controlled value.\n await move(start, 'increment')\n expect(onValueChange).toHaveBeenLastCalledWith([30, 60])\n await act(async () => {\n root.render(\n <LumenProvider>\n <LumenRangeSlider\n label=\"Capacity\"\n value={[60, 60]}\n min={0}\n max={100}\n step={10}\n onValueChange={onValueChange}\n />\n </LumenProvider>\n )\n await Promise.resolve()\n })\n const next = root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'adjustable')\n if (!next[0] || !next[1]) throw new Error('Missing updated interval endpoints')\n await move(next[0], 'increment')\n expect(onValueChange).toHaveBeenLastCalledWith([60, 60])\n await move(next[1], 'decrement')\n expect(onValueChange).toHaveBeenLastCalledWith([60, 60])\n })\n\n test.each([{ enabled: false, readOnly: false }, { enabled: true, readOnly: true }])(\n 'range endpoints reject touch and accessibility when enabled=$enabled readOnly=$readOnly',\n async ({ enabled, readOnly }) => {\n const onValueChange = vi.fn<(value: readonly [number, number]) => void>()\n const root = await renderNative(\n <LumenRangeSlider\n label=\"Capacity\"\n value={[20, 80]}\n enabled={enabled}\n readOnly={readOnly}\n onValueChange={onValueChange}\n />\n )\n const sliders = root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'adjustable')\n for (const slider of sliders) {\n expect(readProp(slider, 'accessibilityState')).toEqual({ disabled: true })\n for (const [event, nativeEvent] of [\n ['onAccessibilityAction', { actionName: 'increment' }],\n ['onResponderGrant', { locationX: 100 }],\n ['onResponderMove', { locationX: 100 }]\n ] as const) {\n const handler = readProp(slider, event)\n if (typeof handler !== 'function') throw new Error(`Missing ${event}`)\n await act(async () => {\n Reflect.apply(handler, undefined, [{ nativeEvent }])\n await Promise.resolve()\n })\n }\n }\n expect(onValueChange).not.toHaveBeenCalled()\n }\n )\n\n test('uses the Android accessibility timeout before dismissing a toast', async () => {\n nativePlatform.OS = 'android'\n vi.useFakeTimers()\n accessibilityTimeout.mockResolvedValue(20_000)\n const ref = createRef<LumenToastHookController>()\n\n await renderNative(<ToastProbe ref={ref} />)\n await act(async () => {\n readToastController(ref).create({ title: 'Saved' })\n await Promise.resolve()\n })\n\n expect(accessibilityTimeout).toHaveBeenCalledExactlyOnceWith(5000)\n\n await act(async () => {\n await vi.advanceTimersByTimeAsync(19_999)\n })\n expect(readToastController(ref).toasts).toHaveLength(1)\n\n await act(async () => {\n await vi.advanceTimersByTimeAsync(1)\n })\n expect(readToastController(ref).toasts).toHaveLength(0)\n })\n\n test.each(['shorter', 'invalid', 'oversized', 'rejected'])('retains the requested duration when Android returns a %s recommendation', async result => {\n nativePlatform.OS = 'android'\n vi.useFakeTimers()\n\n if (result === 'rejected') accessibilityTimeout.mockRejectedValue(new Error('Native timeout unavailable'))\n else if (result === 'shorter') accessibilityTimeout.mockResolvedValue(1)\n else if (result === 'oversized') accessibilityTimeout.mockResolvedValue(Number.MAX_SAFE_INTEGER)\n else accessibilityTimeout.mockResolvedValue(Number.NaN)\n\n const ref = createRef<LumenToastHookController>()\n\n await renderNative(<ToastProbe ref={ref} />)\n await act(async () => {\n readToastController(ref).create({ duration: 1000, title: 'Saved' })\n await Promise.resolve()\n })\n expect(accessibilityTimeout).toHaveBeenCalledExactlyOnceWith(1000)\n\n await act(async () => {\n await vi.advanceTimersByTimeAsync(999)\n })\n expect(readToastController(ref).toasts).toHaveLength(1)\n\n await act(async () => {\n await vi.advanceTimersByTimeAsync(1)\n })\n expect(readToastController(ref).toasts).toHaveLength(0)\n })\n\n test.each(['dismiss', 'clear', 'unmount', 'evict'])('ignores late Android timeout results after %s', async operation => {\n nativePlatform.OS = 'android'\n vi.useFakeTimers()\n const pending = deferredTimeout()\n\n accessibilityTimeout.mockReturnValueOnce(pending.promise)\n\n const ref = createRef<LumenToastHookController>()\n const root = await renderNative(<ToastProbe options={{ maxCount: 1 }} ref={ref} />)\n let id = ''\n\n await act(async () => {\n id = readToastController(ref).create({ title: 'Saved' })\n await Promise.resolve()\n })\n expect(accessibilityTimeout).toHaveBeenCalledExactlyOnceWith(5000)\n\n await act(async () => {\n if (operation === 'dismiss') readToastController(ref).dismiss(id)\n if (operation === 'clear') readToastController(ref).clear()\n if (operation === 'unmount') root.unmount()\n if (operation === 'evict') readToastController(ref).create({ duration: 0, title: 'Persistent replacement' })\n await Promise.resolve()\n })\n await act(async () => {\n pending.resolve(20_000)\n await Promise.resolve()\n })\n\n expect(vi.getTimerCount()).toBe(0)\n })\n\n test('ignores stale Android recommendations when a toast duration is updated', async () => {\n nativePlatform.OS = 'android'\n vi.useFakeTimers()\n const pending = deferredTimeout()\n\n accessibilityTimeout.mockReturnValueOnce(pending.promise).mockResolvedValueOnce(10_000)\n\n const ref = createRef<LumenToastHookController>()\n\n await renderNative(<ToastProbe ref={ref} />)\n await act(async () => {\n const id = readToastController(ref).create({ title: 'Saved' })\n\n readToastController(ref).update(id, { duration: 1000 })\n await Promise.resolve()\n })\n expect(accessibilityTimeout).toHaveBeenNthCalledWith(1, 5000)\n expect(accessibilityTimeout).toHaveBeenNthCalledWith(2, 1000)\n expect(accessibilityTimeout).toHaveBeenCalledTimes(2)\n\n await act(async () => {\n pending.resolve(100_000)\n await Promise.resolve()\n await vi.advanceTimersByTimeAsync(10_000)\n })\n\n expect(readToastController(ref).toasts).toHaveLength(0)\n expect(vi.getTimerCount()).toBe(0)\n })\n\n test('keeps explicitly persistent Android toasts without requesting a timeout', async () => {\n nativePlatform.OS = 'android'\n vi.useFakeTimers()\n const ref = createRef<LumenToastHookController>()\n\n await renderNative(<ToastProbe ref={ref} />)\n await act(async () => {\n for (const duration of [0, -1, Number.POSITIVE_INFINITY]) readToastController(ref).create({ duration, title: 'Saved' })\n await vi.advanceTimersByTimeAsync(100_000)\n })\n\n expect(readToastController(ref).toasts).toHaveLength(3)\n expect(accessibilityTimeout).not.toHaveBeenCalled()\n expect(vi.getTimerCount()).toBe(0)\n })\n\n test.each(['ios', 'web'])('preserves the requested %s toast duration without Android timeout calls', async platform => {\n nativePlatform.OS = platform\n vi.useFakeTimers()\n const ref = createRef<LumenToastHookController>()\n\n await renderNative(<ToastProbe ref={ref} />)\n await act(async () => {\n readToastController(ref).create({ duration: 1000, title: 'Saved' })\n await vi.advanceTimersByTimeAsync(1000)\n })\n\n expect(readToastController(ref).toasts).toHaveLength(0)\n expect(accessibilityTimeout).not.toHaveBeenCalled()\n })\n\n test('keeps enlarged navigation labels readable and preserves destination semantics', async () => {\n const select = vi.fn()\n const reselect = vi.fn()\n const items = [\n { label: 'Espacios de trabajo', value: 'workspaces' },\n { label: 'Configuración', value: 'settings' },\n { disabled: true, label: 'Archivo', value: 'archive' }\n ]\n const navigation = <LumenNavigationBar items={items} onReselect={reselect} onValueChange={select} value=\"workspaces\" />\n const root = await renderNative(navigation)\n const labels = () => root.container.queryAll(instance => instance.type === 'Text')\n expect(labels().map(label => readProp(label, 'numberOfLines'))).toEqual([1, 1, 1])\n\n nativeWindow.fontScale = 3\n await act(async () => {\n root.render(<LumenProvider scheme=\"light\"><LumenNavigationBar items={items} onReselect={reselect} onValueChange={select} value=\"workspaces\" /></LumenProvider>)\n await Promise.resolve()\n })\n expect(labels().every(label => readProp(label, 'numberOfLines') === undefined)).toBe(true)\n expect(labels().every(label => {\n const style = readProp(label, 'style')\n return typeof style === 'object' && style !== null && 'maxWidth' in style && style.maxWidth === '100%'\n })).toBe(true)\n const selected = findByAccessibilityLabel(root, 'Espacios de trabajo')\n expect(readProp(selected, 'accessibilityState')).toMatchObject({ selected: true })\n callAction(readProp(selected, 'onPress'), 'Missing reselect action')\n expect(reselect).toHaveBeenCalledWith('workspaces')\n const settings = findByAccessibilityLabel(root, 'Configuración')\n callAction(readProp(settings, 'onPress'), 'Missing destination action')\n expect(select).toHaveBeenCalledWith('settings')\n expect(readProp(findByAccessibilityLabel(root, 'Archivo'), 'disabled')).toBe(true)\n })\n\n test('keeps alert actions inside safe areas and makes oversized content scrollable', async () => {\n const root = await renderNative(\n <LumenAlertDialog\n confirmLabel=\"Delete\"\n onConfirm={() => {}}\n onDismiss={() => {}}\n safeAreaInsets={safeAreaInsets}\n title=\"Delete project?\"\n visible\n />\n )\n const modalContainer = root.container.queryAll(\n instance => readProp(instance, 'accessibilityViewIsModal') === true\n )[0]\n\n if (!modalContainer) throw new Error('Expected the alert modal container.')\n\n expect(readProp(modalContainer, 'style')).toMatchObject({\n paddingBottom: 58,\n paddingLeft: 32,\n paddingRight: 36,\n paddingTop: 44\n })\n expect(readProp(findByAccessibilityRole(root, 'alert'), 'style')).toMatchObject({\n maxHeight: '100%'\n })\n })\n\n test('keeps sheet actions above the bottom inset and scrolls application content', async () => {\n const root = await renderNative(\n <LumenSheet\n actions={<LumenButton>Save</LumenButton>}\n onDismiss={() => {}}\n safeAreaInsets={safeAreaInsets}\n scrollable\n title=\"Settings\"\n visible\n >\n <LumenText>Sheet content</LumenText>\n </LumenSheet>\n )\n const keyboardSurface = root.container.queryAll(\n instance => instance.type === 'KeyboardAvoidingView'\n )[0]\n const sheetPanel = root.container.queryAll(instance => {\n const style = readProp(instance, 'style')\n\n return typeof style === 'object' &&\n style !== null &&\n 'paddingBottom' in style &&\n style.paddingBottom === 34\n })[0]\n\n if (!keyboardSurface || !sheetPanel) throw new Error('Expected the sheet surfaces.')\n\n expect(readProp(keyboardSurface, 'style')).toMatchObject({\n paddingBottom: 0,\n paddingLeft: 8,\n paddingRight: 12,\n paddingTop: 20\n })\n expect(readProp(sheetPanel, 'style')).toMatchObject({ paddingBottom: 34 })\n expect(root.container.queryAll(instance => instance.type === 'ScrollView')).toHaveLength(1)\n })\n\n test('lets virtualized sheet content own scrolling', async () => {\n const root = await renderNative(\n <LumenSheet onDismiss={() => {}} scrollable={false} visible>\n <LumenText>Virtualized content</LumenText>\n </LumenSheet>\n )\n\n expect(root.container.queryAll(instance => instance.type === 'ScrollView')).toHaveLength(0)\n })\n\n test('button exposes loading as busy and disabled native state', async () => {\n const root = await renderNative(<LumenButton loading>Save changes</LumenButton>)\n const button = findByAccessibilityRole(root, 'button')\n\n expect(readProp(button, 'disabled')).toBe(true)\n expect(readProp(button, 'accessibilityState')).toEqual({\n busy: true,\n disabled: true\n })\n })\n\n test('button preserves its native role when a consumer supplies another role', async () => {\n const root = await renderNative(\n <LumenButton accessibilityRole=\"link\">Open workspace</LumenButton>\n )\n\n expect(findByAccessibilityRole(root, 'button')).toBeDefined()\n expect(root.container.queryAll(\n instance => readProp(instance, 'accessibilityRole') === 'link'\n )).toHaveLength(0)\n })\n\n test('text inputs expose disabled native state without dropping caller state', async () => {\n const textFieldRoot = await renderNative(\n <LumenTextField\n accessibilityLabel=\"Project name\"\n accessibilityState={{ busy: true }}\n editable={false}\n />\n )\n const textareaRoot = await renderNative(\n <LumenTextarea\n accessibilityState={{ busy: true }}\n editable={false}\n label=\"Project notes\"\n onChangeText={() => {}}\n value=\"\"\n />\n )\n const searchRoot = await renderNative(\n <LumenSearchField\n accessibilityState={{ busy: true }}\n editable={false}\n onChangeText={() => {}}\n prompt=\"Search projects\"\n value=\"\"\n />\n )\n\n for (const [root, label] of [\n [textFieldRoot, 'Project name'],\n [textareaRoot, 'Project notes'],\n [searchRoot, 'Search projects']\n ] as const) {\n expect(readProp(findByAccessibilityLabel(root, label), 'accessibilityState')).toEqual({\n busy: true,\n disabled: true\n })\n }\n })\n\n test('validation is exposed on the native controls without dropping caller context', async () => {\n const textFieldRoot = await renderNative(\n <LumenTextField accessibilityLabel=\"Project name\" error />\n )\n const textareaRoot = await renderNative(\n <LumenTextarea\n accessibilityHint=\"Consumer guidance\"\n errorMessage=\"Project notes are required\"\n label=\"Project notes\"\n onChangeText={() => {}}\n value=\"\"\n />\n )\n const dateRoot = await renderNative(\n <LumenDateField\n errorMessage=\"Choose a valid birthday\"\n label=\"Birthday\"\n onValueChange={() => {}}\n value={null}\n />\n )\n\n const textField = findByAccessibilityLabel(textFieldRoot, 'Project name')\n const textarea = findByAccessibilityLabel(textareaRoot, 'Project notes')\n const dateButton = findByAccessibilityLabel(dateRoot, 'Birthday, Select a date')\n\n expect(readProp(textField, 'aria-invalid')).toBe(true)\n expect(readProp(textarea, 'aria-invalid')).toBe(true)\n expect(readProp(textarea, 'accessibilityHint')).toBe('Project notes are required')\n expect(readProp(dateButton, 'aria-invalid')).toBe(true)\n expect(readProp(dateButton, 'accessibilityHint')).toBe('Choose a valid birthday')\n })\n\n test('grouped fields expose required and range validation context', async () => {\n const fieldGroupRoot = await renderNative(\n <LumenFieldGroup label=\"Project details\" required>\n <LumenText>Details</LumenText>\n </LumenFieldGroup>\n )\n const dateRangeRoot = await renderNative(\n <LumenDateRangeField\n accessibilityHint=\"Consumer guidance\"\n errorMessage=\"Choose a valid schedule\"\n label=\"Schedule\"\n onValueChange={() => {}}\n value={{ end: null, start: null }}\n />\n )\n\n expect(findByAccessibilityLabel(fieldGroupRoot, 'Project details, required')).toBeDefined()\n\n const dateRange = findByAccessibilityLabel(dateRangeRoot, 'Schedule')\n\n expect(readProp(dateRange, 'aria-invalid')).toBe(true)\n expect(readProp(dateRange, 'accessibilityHint')).toBe('Choose a valid schedule')\n })\n\n test('pickers inside field groups announce the group error and description', async () => {\n const root = await renderNative(\n <>\n <LumenFieldGroup description=\"Used for billing\" errorMessage=\"Choose a plan\" label=\"Plan\" required>\n <LumenPicker label=\"Plan\" onValueChange={() => {}} options={[{ label: 'Free', value: 'free' }]} value=\"free\" />\n </LumenFieldGroup>\n <LumenFieldGroup description=\"Shown on invoices\" label=\"Currency\">\n <LumenPicker label=\"Currency\" onValueChange={() => {}} options={[{ label: 'USD', value: 'usd' }]} value=\"usd\" />\n </LumenFieldGroup>\n <LumenPicker label=\"Region\" onValueChange={() => {}} options={[{ label: 'EU', value: 'eu' }]} value=\"eu\" />\n </>\n )\n const [invalidPicker, describedPicker, standalonePicker] = root.container.queryAll(\n instance => readProp(instance, 'accessibilityRole') === 'combobox'\n )\n\n expect(invalidPicker && readProp(invalidPicker, 'accessibilityHint')).toBe('Choose a plan')\n expect(invalidPicker && readProp(invalidPicker, 'aria-invalid')).toBe(true)\n expect(describedPicker && readProp(describedPicker, 'accessibilityHint')).toBe('Shown on invoices')\n expect(describedPicker && readProp(describedPicker, 'aria-invalid')).toBeUndefined()\n expect(standalonePicker && readProp(standalonePicker, 'accessibilityHint')).toBeUndefined()\n expect(standalonePicker && readProp(standalonePicker, 'aria-invalid')).toBeUndefined()\n })\n\n test('localizes required-field descriptions without exposing them on optional fields', async () => {\n const root = await renderNative(\n <>\n <LumenFieldGroup label=\"Nombre\" required requiredLabel=\"obligatorio\"><LumenTextField /></LumenFieldGroup>\n <LumenFieldGroup label=\"Notas\" requiredLabel=\"obligatorio\"><LumenTextField /></LumenFieldGroup>\n </>\n )\n\n expect(findByAccessibilityLabel(root, 'Nombre, obligatorio')).toBeDefined()\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityLabel') === 'Notas, obligatorio')).toHaveLength(0)\n expect(findByAccessibilityLabel(root, 'Notas')).toBeDefined()\n })\n\n test('field groups provide inherited native and web relationships without overriding inputs', async () => {\n const root = await renderNative(\n <LumenFieldGroup\n description=\"Shown on the public profile\"\n errorMessage=\"Choose a unique name\"\n label=\"Project name\"\n required\n >\n <LumenText>Nested row content</LumenText>\n <LumenTextField />\n <LumenTextField accessibilityLabel=\"Short project name\" />\n </LumenFieldGroup>\n )\n const inputs = root.container.queryAll(instance => instance.type === 'TextInput')\n const inherited = inputs[0]\n const explicit = inputs[1]\n\n if (!inherited || !explicit) throw new Error('Expected two grouped text fields.')\n\n expect(readProp(inherited, 'accessibilityLabel')).toBe('Project name')\n expect(readProp(inherited, 'aria-labelledby')).toMatch(/-label$/u)\n expect(readProp(inherited, 'aria-describedby')).toMatch(/-description .*?-error$/u)\n expect(readProp(inherited, 'aria-required')).toBe(true)\n expect(readProp(inherited, 'aria-invalid')).toBe(true)\n expect(readProp(explicit, 'accessibilityLabel')).toBe('Short project name')\n expect(readProp(explicit, 'aria-labelledby')).toBeUndefined()\n\n const validationMessages = root.container.queryAll(\n instance => readProp(instance, 'children') === 'Choose a unique name'\n )\n\n expect(validationMessages).toHaveLength(1)\n\n const validationMessage = validationMessages[0]\n\n if (!validationMessage) throw new Error('Expected the grouped validation message.')\n\n expect(readProp(validationMessage, 'accessibilityLiveRegion')).toBe('polite')\n expect(readProp(validationMessage, 'accessibilityRole')).toBe('alert')\n })\n\n test('read-only phone input locks country selection and number editing', async () => {\n const country = getLumenPhoneCountry('CO')\n if (!country) throw new Error('Missing phone fixture')\n const root = await renderNative(<LumenPhoneInput label=\"Phone\" onValueChange={() => {}} readOnly value={resolveLumenPhoneInputValue([country], country, '', {})} />)\n expect(readProp(findByAccessibilityLabel(root, 'Country code, Colombia, +57'), 'disabled')).toBe(true)\n expect(readProp(findByAccessibilityLabel(root, 'Phone number'), 'editable')).toBe(false)\n })\n\n test('phone input exposes disabled state on both native controls', async () => {\n const country = getLumenPhoneCountry('US')\n\n expect(country).toBeDefined()\n\n if (!country) return\n\n const value = resolveLumenPhoneInputValue([country], country, '', {})\n const root = await renderNative(\n <LumenPhoneInput\n countries={[country]}\n enabled={false}\n label=\"Phone\"\n onValueChange={() => {}}\n value={value}\n />\n )\n const countrySelector = findByAccessibilityLabel(root, 'Country code, United States, +1')\n const numberInput = findByAccessibilityLabel(root, 'Phone number')\n\n expect(readProp(countrySelector, 'accessibilityState')).toEqual({\n disabled: true,\n expanded: false\n })\n expect(readProp(numberInput, 'accessibilityState')).toEqual({ disabled: true })\n })\n\n test('phone input exposes validation on the number editor', async () => {\n const country = getLumenPhoneCountry('US')\n\n expect(country).toBeDefined()\n\n if (!country) return\n\n const value = resolveLumenPhoneInputValue([country], country, '415', {})\n const root = await renderNative(\n <LumenPhoneInput\n countries={[country]}\n errorMessage=\"Enter a complete phone number\"\n label=\"Phone\"\n onValueChange={() => {}}\n value={value}\n />\n )\n const numberInput = findByAccessibilityLabel(root, 'Phone number')\n\n expect(readProp(numberInput, 'aria-invalid')).toBe(true)\n expect(readProp(numberInput, 'accessibilityHint')).toBe('Enter a complete phone number')\n })\n\n test('phone input exposes an empty country allow-list as a disabled selector', async () => {\n const country = getLumenPhoneCountry('US')\n\n expect(country).toBeDefined()\n\n if (!country) return\n\n const value = resolveLumenPhoneInputValue([country], country, '', {})\n const root = await renderNative(\n <LumenPhoneInput\n countries={[]}\n label=\"Phone\"\n onValueChange={() => {}}\n value={value}\n />\n )\n const countrySelector = findByAccessibilityLabel(root, 'Country code, United States, +1')\n\n expect(readProp(countrySelector, 'disabled')).toBe(true)\n expect(readProp(countrySelector, 'accessibilityState')).toEqual({\n disabled: true,\n expanded: false\n })\n })\n\n test('generic picker disables empty options and does not reopen after disabling', async () => {\n const onValueChange = vi.fn<(value: string) => void>()\n const root = await renderNative(\n <LumenPicker\n label=\"Workspace\"\n onValueChange={onValueChange}\n options={[]}\n value=\"none\"\n />\n )\n let trigger = findByAccessibilityLabel(root, 'Workspace')\n\n expect(readProp(trigger, 'disabled')).toBe(true)\n expect(readProp(trigger, 'accessibilityState')).toEqual({\n disabled: true,\n expanded: false\n })\n\n await act(async () => {\n root.render(\n <LumenProvider scheme=\"light\">\n <LumenPicker\n label=\"Workspace\"\n onValueChange={onValueChange}\n options={[{ label: 'Design', value: 'design' }]}\n value=\"design\"\n />\n </LumenProvider>\n )\n await Promise.resolve()\n })\n\n trigger = findByAccessibilityLabel(root, 'Workspace')\n\n await act(async () => {\n callAction(readProp(trigger, 'onPress'), 'Picker is missing its native press action.')\n await Promise.resolve()\n })\n\n expect(findByAccessibilityRole(root, 'menu')).toBeDefined()\n\n await act(async () => {\n root.render(\n <LumenProvider scheme=\"light\">\n <LumenPicker\n enabled={false}\n label=\"Workspace\"\n onValueChange={onValueChange}\n options={[{ label: 'Design', value: 'design' }]}\n value=\"design\"\n />\n </LumenProvider>\n )\n await Promise.resolve()\n })\n\n expect(root.container.queryAll(\n instance => readProp(instance, 'accessibilityRole') === 'menu'\n )).toHaveLength(0)\n\n await act(async () => {\n root.render(\n <LumenProvider scheme=\"light\">\n <LumenPicker\n label=\"Workspace\"\n onValueChange={onValueChange}\n options={[{ label: 'Design', value: 'design' }]}\n value=\"design\"\n />\n </LumenProvider>\n )\n await Promise.resolve()\n })\n\n expect(root.container.queryAll(\n instance => readProp(instance, 'accessibilityRole') === 'menu'\n )).toHaveLength(0)\n })\n\n test('date picker does not reopen after the field is disabled and re-enabled', async () => {\n const root = await renderNative(\n <LumenDateField label=\"Birthday\" onValueChange={() => {}} value={null} />\n )\n const trigger = findByAccessibilityLabel(root, 'Birthday, Select a date')\n\n await act(async () => {\n callAction(readProp(trigger, 'onPress'), 'Date field is missing its native press action.')\n await Promise.resolve()\n })\n\n expect(findHostComponent(root, 'NativeDatePicker')).toBeDefined()\n\n await act(async () => {\n root.render(\n <LumenProvider scheme=\"light\">\n <LumenDateField enabled={false} label=\"Birthday\" onValueChange={() => {}} value={null} />\n </LumenProvider>\n )\n await Promise.resolve()\n })\n\n expect(root.container.queryAll(instance => instance.type === 'NativeDatePicker')).toHaveLength(0)\n\n await act(async () => {\n root.render(\n <LumenProvider scheme=\"light\">\n <LumenDateField label=\"Birthday\" onValueChange={() => {}} value={null} />\n </LumenProvider>\n )\n await Promise.resolve()\n })\n\n expect(root.container.queryAll(instance => instance.type === 'NativeDatePicker')).toHaveLength(0)\n })\n\n test('disabling an Android date field dismisses its imperative native dialog', async () => {\n nativePlatform.OS = 'android'\n\n try {\n const root = await renderNative(\n <LumenDateField label=\"Birthday\" onValueChange={() => {}} value={null} />\n )\n const trigger = findByAccessibilityLabel(root, 'Birthday, Select a date')\n\n await act(async () => {\n callAction(readProp(trigger, 'onPress'), 'Date field is missing its native press action.')\n await Promise.resolve()\n })\n\n expect(DateTimePickerAndroid.open).toHaveBeenCalledOnce()\n\n await act(async () => {\n root.render(\n <LumenProvider scheme=\"light\">\n <LumenDateField enabled={false} label=\"Birthday\" onValueChange={() => {}} value={null} />\n </LumenProvider>\n )\n await Promise.resolve()\n })\n\n expect(DateTimePickerAndroid.dismiss).toHaveBeenCalledWith('date')\n } finally {\n nativePlatform.OS = 'ios'\n }\n })\n\n test('phone picker closes and clears its query when the input becomes disabled', async () => {\n const country = getLumenPhoneCountry('US')\n\n expect(country).toBeDefined()\n\n if (!country) return\n\n const value = resolveLumenPhoneInputValue([country], country, '', {})\n const renderPhone = (enabled: boolean): ReactElement => (\n <LumenProvider scheme=\"light\">\n <LumenPhoneInput\n countries={[country]}\n enabled={enabled}\n label=\"Phone\"\n onValueChange={() => {}}\n value={value}\n />\n </LumenProvider>\n )\n const root = await renderNative(\n <LumenPhoneInput\n countries={[country]}\n label=\"Phone\"\n onValueChange={() => {}}\n value={value}\n />\n )\n const selector = findByAccessibilityLabel(root, 'Country code, United States, +1')\n\n await act(async () => {\n callAction(readProp(selector, 'onPress'), 'Country selector is missing its native press action.')\n await Promise.resolve()\n })\n\n expect(readProp(findHostComponent(root, 'Modal'), 'visible')).toBe(true)\n expect(readProp(findByAccessibilityLabel(root, 'Search countries'), 'editable')).toBe(true)\n\n await act(async () => {\n const onChangeText = readProp(findByAccessibilityLabel(root, 'Search countries'), 'onChangeText')\n\n if (typeof onChangeText !== 'function') throw new Error('Country search is missing its change action.')\n\n Reflect.apply(onChangeText, undefined, ['united'])\n await Promise.resolve()\n })\n\n await act(async () => {\n root.render(renderPhone(false))\n await Promise.resolve()\n })\n\n expect(readProp(findHostComponent(root, 'Modal'), 'visible')).toBe(false)\n expect(readProp(findByAccessibilityLabel(root, 'Country code, United States, +1'), 'accessibilityState'))\n .toEqual({ disabled: true, expanded: false })\n\n await act(async () => {\n root.render(renderPhone(true))\n await Promise.resolve()\n })\n\n expect(readProp(findByAccessibilityLabel(root, 'Country code, United States, +1'), 'accessibilityState'))\n .toEqual({ disabled: false, expanded: false })\n\n const reopenedSelector = findByAccessibilityLabel(root, 'Country code, United States, +1')\n\n await act(async () => {\n callAction(\n readProp(reopenedSelector, 'onPress'),\n 'Re-enabled country selector is missing its native press action.'\n )\n await Promise.resolve()\n })\n\n expect(readProp(findByAccessibilityLabel(root, 'Search countries'), 'value')).toBe('')\n })\n\n test('chip removal exposes the independent action disabled state', async () => {\n const root = await renderNative(\n <LumenChip disabled label=\"Design\" onRemove={() => {}} />\n )\n const removeButton = findByAccessibilityLabel(root, 'Remove Design')\n\n expect(readProp(removeButton, 'accessibilityRole')).toBe('button')\n expect(readProp(removeButton, 'disabled')).toBe(true)\n expect(readProp(removeButton, 'accessibilityState')).toEqual({ disabled: true })\n expect(readProp(removeButton, 'style')).toMatchObject({\n alignItems: 'center',\n justifyContent: 'center',\n minHeight: 24,\n minWidth: 24\n })\n })\n\n test('toast announces politely and keeps dismissal independently operable', async () => {\n const onDismiss = vi.fn<() => void>()\n const root = await renderNative(\n <LumenToast dismissLabel=\"Close update\" onDismiss={onDismiss} title=\"Changes saved\" />\n )\n const alert = findByAccessibilityRole(root, 'alert')\n const dismissButton = findByAccessibilityLabel(root, 'Close update')\n const style = readProp(dismissButton, 'style')\n\n expect(readProp(alert, 'accessibilityLiveRegion')).toBe('polite')\n expect(readProp(dismissButton, 'accessibilityRole')).toBe('button')\n expect(style).toBeTypeOf('function')\n\n if (typeof style !== 'function') throw new Error('Toast dismissal is missing its pressable style.')\n\n const restingStyle: unknown = Reflect.apply(style, undefined, [{ pressed: false }])\n\n expect(restingStyle).toMatchObject({\n alignItems: 'center',\n justifyContent: 'center',\n minHeight: 44,\n minWidth: 44\n })\n\n await act(async () => {\n callAction(readProp(dismissButton, 'onPress'), 'Toast dismissal is missing its press action.')\n await Promise.resolve()\n })\n\n expect(onDismiss).toHaveBeenCalledOnce()\n })\n\n test('queues localized iOS toast copy once and announces changed content', async () => {\n const toast = (description: string) => (\n <StrictMode><LumenToast description={description} title=\"Cambios guardados\" /></StrictMode>\n )\n const root = await renderNative(toast('Registro actualizado'))\n\n expect(accessibilityAnnouncement).toHaveBeenCalledExactlyOnceWith('Cambios guardados. Registro actualizado', { queue: true })\n\n await act(async () => {\n root.render(<LumenProvider scheme=\"light\">{toast('Registro actualizado')}</LumenProvider>)\n await Promise.resolve()\n })\n\n expect(accessibilityAnnouncement).toHaveBeenCalledOnce()\n\n await act(async () => {\n root.render(<LumenProvider scheme=\"light\">{toast('Otro registro actualizado')}</LumenProvider>)\n await Promise.resolve()\n })\n\n expect(accessibilityAnnouncement).toHaveBeenLastCalledWith('Cambios guardados. Otro registro actualizado', { queue: true })\n expect(accessibilityAnnouncement).toHaveBeenCalledTimes(2)\n })\n\n test('honors iOS error announcement urgency and off without speaking diagnostic references', async () => {\n const root = await renderNative(<LumenErrorState announcement=\"assertive\" description=\"Try again\" reference=\"request-123\" title=\"Unable to save\" />)\n\n expect(accessibilityAnnouncement).toHaveBeenCalledExactlyOnceWith('Unable to save. Try again', { queue: false })\n\n await act(async () => {\n root.render(<LumenProvider scheme=\"light\"><LumenErrorState announcement=\"off\" title=\"Unable to save\" /></LumenProvider>)\n await Promise.resolve()\n })\n\n expect(accessibilityAnnouncement).toHaveBeenCalledOnce()\n\n await act(async () => {\n root.render(<LumenProvider scheme=\"light\"><LumenErrorState announcement=\"polite\" title=\"Unable to save\" /></LumenProvider>)\n await Promise.resolve()\n })\n\n expect(accessibilityAnnouncement).toHaveBeenLastCalledWith('Unable to save', { queue: true })\n expect(accessibilityAnnouncement).toHaveBeenCalledTimes(2)\n })\n\n test('does not announce initially disabled errors or empty toast copy on iOS', async () => {\n await renderNative(\n <>\n <LumenErrorState announcement=\"off\" title=\"Unable to save\" />\n <LumenToast title=\" \" />\n </>\n )\n\n expect(accessibilityAnnouncement).not.toHaveBeenCalled()\n })\n\n test.each(['android', 'web'])('keeps %s live regions without duplicate imperative announcements', async platform => {\n nativePlatform.OS = platform\n const root = await renderNative(\n <>\n <LumenToast title=\"Saved\" />\n <LumenErrorState announcement=\"assertive\" title=\"Unable to save\" />\n </>\n )\n\n expect(readProp(findByAccessibilityRole(root, 'alert'), 'accessibilityLiveRegion')).toBe('polite')\n expect(readProp(findByAccessibilityRole(root, 'summary'), 'accessibilityLiveRegion')).toBe('assertive')\n expect(accessibilityAnnouncement).not.toHaveBeenCalled()\n })\n\n test('banner dismissal has a full independent touch target', async () => {\n const root = await renderNative(\n <LumenBanner dismissLabel=\"Close notice\" onDismiss={() => {}} title=\"Maintenance soon\" />\n )\n const dismissButton = findByAccessibilityLabel(root, 'Close notice')\n const style = readProp(dismissButton, 'style')\n\n expect(readProp(dismissButton, 'accessibilityRole')).toBe('button')\n expect(style).toBeTypeOf('function')\n\n if (typeof style !== 'function') throw new Error('Banner dismissal is missing its pressable style.')\n\n const restingStyle: unknown = Reflect.apply(style, undefined, [{ pressed: false }])\n\n expect(restingStyle).toMatchObject({\n alignItems: 'center',\n justifyContent: 'center',\n minHeight: 44,\n minWidth: 44\n })\n })\n\n test('status bars allow long messages to wrap without shrinking trailing content', async () => {\n const message = 'El análisis permanece en el dispositivo para proteger tu privacidad.'\n const root = await renderNative(\n <LumenProvider>\n <LumenStatusBar\n message={message}\n trailing={<LumenText>Analyze</LumenText>}\n />\n </LumenProvider>\n )\n const messageNodes = root.container.queryAll(\n instance => instance.type === 'Text' && readProp(instance, 'children') === message\n )\n\n expect(messageNodes).toHaveLength(1)\n\n const messageNode = messageNodes[0]\n\n if (!messageNode) throw new Error('Expected the status message fixture.')\n\n expect(readProp(messageNode, 'numberOfLines')).toBeUndefined()\n expect(readProp(messageNode, 'style')).toMatchObject({ flex: 1, flexShrink: 1 })\n\n const fixedTrailingContainers = root.container.queryAll(instance => {\n const style = readProp(instance, 'style')\n\n return instance.type === 'View' && typeof style === 'object' && style !== null &&\n Reflect.get(style, 'flexShrink') === 0\n })\n\n expect(fixedTrailingContainers).toHaveLength(1)\n })\n\n test('search clear action preserves its label and full touch target', async () => {\n const onChangeText = vi.fn<(value: string) => void>()\n const root = await renderNative(\n <LumenSearchField\n clearLabel=\"Clear query\"\n onChangeText={onChangeText}\n value=\"lumen\"\n />\n )\n const clearButton = findByAccessibilityLabel(root, 'Clear query')\n const style = readProp(clearButton, 'style')\n\n expect(readProp(clearButton, 'accessibilityRole')).toBe('button')\n expect(style).toBeTypeOf('function')\n\n if (typeof style !== 'function') throw new Error('Clear action is missing its pressable style.')\n\n const restingStyle: unknown = Reflect.apply(style, undefined, [{ pressed: false }])\n\n expect(restingStyle).toMatchObject({\n alignItems: 'center',\n justifyContent: 'center',\n minHeight: 44,\n minWidth: 44\n })\n\n await act(async () => {\n callAction(readProp(clearButton, 'onPress'), 'Clear action is missing its native press action.')\n await Promise.resolve()\n })\n\n expect(onChangeText).toHaveBeenCalledExactlyOnceWith('')\n })\n\n test('checkbox announces state and emits the next controlled value', async () => {\n const onCheckedChange = vi.fn<(checked: boolean) => void>()\n const root = await renderNative(\n <LumenCheckbox\n checked={false}\n label=\"Include diagnostics\"\n onCheckedChange={onCheckedChange}\n />\n )\n const checkbox = findByAccessibilityRole(root, 'checkbox')\n const onPress = readProp(checkbox, 'onPress')\n\n expect(readProp(checkbox, 'accessibilityLabel')).toBe('Include diagnostics')\n expect(readProp(checkbox, 'aria-checked')).toBe(false)\n expect(readProp(checkbox, 'aria-disabled')).toBe(false)\n\n expect(readProp(checkbox, 'accessibilityState')).toEqual({\n checked: false,\n disabled: false\n })\n expect(onPress).toBeTypeOf('function')\n\n await act(async () => {\n callAction(onPress, 'Checkbox is missing its native press action.')\n await Promise.resolve()\n })\n\n expect(onCheckedChange).toHaveBeenCalledExactlyOnceWith(true)\n })\n\n test('checkbox preserves a host-provided accessible name', async () => {\n const root = await renderNative(\n <LumenCheckbox checked={false} label=\"Diagnostics\" accessibilityLabel=\"Include diagnostic report\" onCheckedChange={() => undefined} />\n )\n\n expect(readProp(findByAccessibilityRole(root, 'checkbox'), 'accessibilityLabel')).toBe('Include diagnostic report')\n })\n\n test('checkbox announces its visible description as a hint like toggle', async () => {\n const root = await renderNative(\n <>\n <LumenCheckbox\n checked={false}\n description=\"Attach logs to the support ticket\"\n label=\"Include diagnostics\"\n onCheckedChange={() => undefined}\n />\n <LumenCheckbox\n accessibilityHint=\"Consumer guidance\"\n checked={false}\n description=\"Visible guidance\"\n label=\"Share usage\"\n onCheckedChange={() => undefined}\n />\n <LumenCheckbox\n accessibilityHint=\"\"\n checked={false}\n description=\"Visible guidance\"\n label=\"Quiet hints\"\n onCheckedChange={() => undefined}\n />\n </>\n )\n\n expect(readProp(findByAccessibilityLabel(root, 'Include diagnostics'), 'accessibilityHint')).toBe('Attach logs to the support ticket')\n expect(readProp(findByAccessibilityLabel(root, 'Share usage'), 'accessibilityHint')).toBe('Consumer guidance')\n expect(readProp(findByAccessibilityLabel(root, 'Quiet hints'), 'accessibilityHint')).toBe('')\n })\n\n test('toggle makes the full labeled row the single interactive switch', async () => {\n const onValueChange = vi.fn<(value: boolean) => void>()\n const root = await renderNative(\n <LumenToggle\n description=\"Receive product and security updates\"\n label=\"Automatic updates\"\n onValueChange={onValueChange}\n value={false}\n />\n )\n const toggle = findByAccessibilityRole(root, 'switch')\n const onPress = readProp(toggle, 'onPress')\n const nativeSwitches = root.container.queryAll(instance => instance.type === 'Switch')\n\n expect(readProp(toggle, 'accessibilityLabel')).toBe('Automatic updates')\n expect(readProp(toggle, 'accessibilityHint')).toBe('Receive product and security updates')\n expect(readProp(toggle, 'aria-checked')).toBe(false)\n expect(readProp(toggle, 'accessibilityState')).toEqual({\n checked: false,\n disabled: false\n })\n expect(nativeSwitches).toHaveLength(1)\n\n const nativeSwitch = nativeSwitches[0]\n\n if (!nativeSwitch) throw new Error('Expected the decorative native switch.')\n\n expect(readProp(nativeSwitch, 'accessibilityElementsHidden')).toBe(true)\n expect(readProp(nativeSwitch, 'importantForAccessibility')).toBe('no-hide-descendants')\n expect(readProp(nativeSwitch, 'pointerEvents')).toBe('none')\n expect(readProp(nativeSwitch, 'onValueChange')).toBeUndefined()\n\n await act(async () => {\n callAction(onPress, 'Toggle row is missing its press action.')\n await Promise.resolve()\n })\n\n expect(onValueChange).toHaveBeenCalledExactlyOnceWith(true)\n })\n\n test('web toggle keeps the decorative native input out of the tab order', async () => {\n nativePlatform.OS = 'web'\n const onValueChange = vi.fn()\n const root = await renderNative(<LumenToggle label=\"Updates\" onValueChange={onValueChange} value={false} />)\n const control = root.container.queryAll(instance => instance.type === 'Switch')[0]\n if (!control) throw new Error('Missing decorative switch')\n expect(readProp(control, 'disabled')).toBe(true)\n expect(readProp(control, 'aria-hidden')).toBe(true)\n const row = findByAccessibilityRole(root, 'switch')\n expect(readProp(row, 'disabled')).toBe(false)\n await act(async () => {\n callAction(readProp(row, 'onPress'), 'Missing row action')\n await Promise.resolve()\n })\n expect(onValueChange).toHaveBeenCalledExactlyOnceWith(true)\n nativePlatform.OS = 'ios'\n })\n\n test('toggle preserves a consumer accessibility hint over supporting copy', async () => {\n const root = await renderNative(\n <LumenToggle\n accessibilityHint=\"Changes how updates are installed\"\n description=\"Receive product and security updates\"\n label=\"Automatic updates\"\n onValueChange={() => {}}\n value={false}\n />\n )\n\n expect(readProp(findByAccessibilityRole(root, 'switch'), 'accessibilityHint'))\n .toBe('Changes how updates are installed')\n })\n\n test('disabled toggle row cannot emit a value change', async () => {\n const onValueChange = vi.fn<(value: boolean) => void>()\n const root = await renderNative(\n <LumenToggle\n disabled\n label=\"Automatic updates\"\n onValueChange={onValueChange}\n value\n />\n )\n const toggle = findByAccessibilityRole(root, 'switch')\n\n expect(readProp(toggle, 'accessibilityState')).toEqual({\n checked: true,\n disabled: true\n })\n expect(readProp(toggle, 'disabled')).toBe(true)\n\n await act(async () => {\n callAction(readProp(toggle, 'onPress'), 'Disabled toggle row is missing its guarded press action.')\n await Promise.resolve()\n })\n\n expect(onValueChange).not.toHaveBeenCalled()\n })\n\n test('tabs preserve selected and disabled semantics while emitting enabled selection', async () => {\n const onValueChange = vi.fn<(value: string) => void>()\n const root = await renderNative(\n <LumenTabs\n label=\"Workspace views\"\n onValueChange={onValueChange}\n options={[\n { label: 'Overview', value: 'overview' },\n { disabled: true, label: 'Activity', value: 'activity' }\n ]}\n value=\"overview\"\n >\n <LumenText>Current workspace health</LumenText>\n </LumenTabs>\n )\n const tabList = findByAccessibilityRole(root, 'tablist')\n const tabs = root.container.queryAll(\n instance => readProp(instance, 'accessibilityRole') === 'tab'\n )\n\n expect(readProp(tabList, 'accessibilityLabel')).toBe('Workspace views')\n expect(tabs).toHaveLength(2)\n const [overviewTab, activityTab] = tabs\n\n if (!overviewTab || !activityTab) throw new Error('Expected the two configured tabs.')\n\n expect(readProp(overviewTab, 'accessibilityState')).toEqual({\n disabled: false,\n selected: true\n })\n expect(readProp(activityTab, 'accessibilityState')).toEqual({\n disabled: true,\n selected: false\n })\n\n const enabledTabPress = readProp(overviewTab, 'onPress')\n\n await act(async () => {\n callAction(enabledTabPress, 'Enabled tab is missing its press action.')\n await Promise.resolve()\n })\n\n expect(onValueChange).toHaveBeenCalledExactlyOnceWith('overview')\n })\n\n test('uses the translated tab label by default and accepts an explicit translated panel name', async () => {\n const fixture = (panelAccessibilityLabel?: string): ReactElement => (\n <LumenTabs label=\"Vistas\" onValueChange={vi.fn()} options={[{ label: 'Resumen', value: 'overview' }]} {...(panelAccessibilityLabel ? { panelAccessibilityLabel } : {})} value=\"overview\">\n <LumenText>Contenido</LumenText>\n </LumenTabs>\n )\n const root = await renderNative(fixture())\n const panel = root.container.queryAll(instance => readProp(instance, 'role') === 'tabpanel')[0]\n if (!panel) throw new Error('Missing tab panel')\n expect(readProp(panel, 'accessibilityLabel')).toBe('Resumen')\n await act(async () => {\n root.render(<LumenProvider>{fixture('Contenido del resumen')}</LumenProvider>)\n await Promise.resolve()\n })\n const translatedPanel = root.container.queryAll(instance => readProp(instance, 'role') === 'tabpanel')[0]\n if (!translatedPanel) throw new Error('Missing translated tab panel')\n expect(readProp(translatedPanel, 'accessibilityLabel')).toBe('Contenido del resumen')\n })\n\n test('tabs support directional keyboard navigation without selecting disabled tabs', async () => {\n const onValueChange = vi.fn<(value: string) => void>()\n const KeyboardTabsFixture = (): ReactElement => {\n const [value, setValue] = useState('overview')\n\n return (\n <LumenTabs\n label=\"Workspace views\"\n onValueChange={nextValue => {\n onValueChange(nextValue)\n setValue(nextValue)\n }}\n options={[\n { label: 'Overview', value: 'overview' },\n { label: 'Activity', value: 'activity' },\n { disabled: true, label: 'Billing', value: 'billing' }\n ]}\n value={value}\n >\n <LumenText>Current workspace health</LumenText>\n </LumenTabs>\n )\n }\n const root = await renderNative(<KeyboardTabsFixture />)\n const pressTabKey = async (optionIndex: number, key: string): Promise<void> => {\n const tab = root.container.queryAll(\n instance => readProp(instance, 'accessibilityRole') === 'tab'\n )[optionIndex]\n\n if (!tab) throw new Error(`Expected tab fixture at index ${optionIndex}.`)\n\n let prevented = false\n\n await act(async () => {\n const preventDefault = callKeyboardAction(readProp(tab, 'onKeyDown'), key)\n\n prevented = preventDefault.mock.calls.length > 0\n\n await Promise.resolve()\n })\n\n expect(prevented).toBe(true)\n }\n\n await pressTabKey(0, 'ArrowRight')\n expect(onValueChange).toHaveBeenLastCalledWith('activity')\n\n await pressTabKey(1, 'ArrowRight')\n expect(onValueChange).toHaveBeenLastCalledWith('overview')\n expect(onValueChange).not.toHaveBeenCalledWith('billing')\n })\n\n test('tabs do not reselect the current tab when every alternative is disabled', async () => {\n const onValueChange = vi.fn<(value: string) => void>()\n const root = await renderNative(\n <LumenTabs\n label=\"Workspace views\"\n onValueChange={onValueChange}\n options={[\n { label: 'Overview', value: 'overview' },\n { disabled: true, label: 'Activity', value: 'activity' }\n ]}\n value=\"overview\"\n >\n <LumenText>Current workspace health</LumenText>\n </LumenTabs>\n )\n const overviewTab = root.container.queryAll(\n instance => readProp(instance, 'accessibilityRole') === 'tab'\n )[0]\n\n if (!overviewTab) throw new Error('Expected the selected tab fixture.')\n\n expect(callKeyboardAction(readProp(overviewTab, 'onKeyDown'), 'ArrowRight'))\n .toHaveBeenCalledOnce()\n expect(onValueChange).not.toHaveBeenCalled()\n })\n})\n\ndescribe('LumenSheet consumer layouts', () => {\n afterEach(() => {\n nativeMotion.enabled = false\n nativeWindow.width = 400\n nativeWindow.height = 800\n nativeWindow.fontScale = 1\n nativePlatform.OS = 'ios'\n accessibilityFocus.mockClear()\n })\n\n test.each([\n { fontScale: 2, height: 800, alignment: 'center' },\n { fontScale: 1, height: 320, alignment: 'flex-end' }\n ])('keeps headings and actions scrollable when space is limited: %j', async ({ alignment, ...window }) => {\n Object.assign(nativeWindow, window, { width: 1024 })\n const root = await renderNative(\n <LumenSheet actions={<LumenButton>Save changes</LumenButton>} onDismiss={vi.fn()} presentation=\"adaptive\" title=\"A long translated heading\" visible>\n <LumenTextField accessibilityLabel=\"Details\" />\n </LumenSheet>\n )\n const scroll = findHostComponent(root, 'ScrollView')\n\n expect(scroll.queryAll(instance => readProp(instance, 'accessibilityRole') === 'header')).toHaveLength(1)\n expect(scroll.queryAll(instance => readProp(instance, 'accessibilityRole') === 'button')).toHaveLength(1)\n expect(readProp(scroll, 'keyboardShouldPersistTaps')).toBe('handled')\n expect(readProp(findHostComponent(root, 'KeyboardAvoidingView'), 'style')).toMatchObject({ justifyContent: alignment })\n })\n\n test('preserves application-owned virtualization at accessibility text sizes', async () => {\n nativeWindow.fontScale = 3\n const root = await renderNative(\n <LumenSheet onDismiss={vi.fn()} scrollable={false} visible>\n <LumenText>Virtualized collection</LumenText>\n </LumenSheet>\n )\n\n expect(root.container.queryAll(instance => instance.type === 'ScrollView')).toHaveLength(0)\n })\n\n test('focuses an explicit initial control after presentation and restores the trigger after closing', async () => {\n const initialFocusRef = createRef<ComponentRef<typeof View>>()\n const returnFocusRef = createRef<ComponentRef<typeof View>>()\n let restoreFrame: (() => void) | undefined\n vi.stubGlobal('requestAnimationFrame', (callback: () => void) => {\n restoreFrame = callback\n\n return 1\n })\n vi.stubGlobal('cancelAnimationFrame', vi.fn())\n const fixture = (visible: boolean): ReactElement => (\n <>\n <LumenButton ref={returnFocusRef}>Edit</LumenButton>\n <LumenSheet\n initialFocusRef={initialFocusRef}\n onDismiss={vi.fn()}\n returnFocusRef={returnFocusRef}\n visible={visible}\n >\n <LumenButton ref={initialFocusRef}>Save</LumenButton>\n </LumenSheet>\n </>\n )\n const root = await renderNative(fixture(true))\n callAction(readProp(findHostComponent(root, 'Modal'), 'onShow'), 'Missing modal presentation event')\n expect(accessibilityFocus).toHaveBeenCalledExactlyOnceWith(initialFocusRef.current, 'focus')\n await act(async () => {\n root.render(<LumenProvider>{fixture(false)}</LumenProvider>)\n await Promise.resolve()\n })\n expect(accessibilityFocus).toHaveBeenCalledTimes(1)\n if (!restoreFrame) throw new Error('Expected a deferred focus restoration')\n restoreFrame()\n expect(accessibilityFocus).toHaveBeenLastCalledWith(returnFocusRef.current, 'focus')\n })\n\n test('wraps related actions at ordinary text sizes and stacks them at accessibility sizes', async () => {\n const fixture = (): ReactElement => (\n <LumenButtonGroup accessibilityLabel=\"Form actions\">\n <LumenButton>Cancel</LumenButton>\n <LumenButton>Save</LumenButton>\n </LumenButtonGroup>\n )\n const root = await renderNative(fixture())\n const group = root.container.queryAll(instance => readProp(instance, 'accessibilityLabel') === 'Form actions')[0]\n if (!group) throw new Error('Missing action group')\n expect(readProp(group, 'style')).toEqual(expect.arrayContaining([expect.objectContaining({ flexDirection: 'row', flexWrap: 'wrap' })]))\n nativeWindow.fontScale = 2\n await act(async () => {\n root.render(<LumenProvider>{fixture()}</LumenProvider>)\n await Promise.resolve()\n })\n const updated = root.container.queryAll(instance => readProp(instance, 'accessibilityLabel') === 'Form actions')[0]\n if (!updated) throw new Error('Missing updated action group')\n expect(readProp(updated, 'style')).toEqual(expect.arrayContaining([expect.objectContaining({ flexDirection: 'column', flexWrap: 'nowrap' })]))\n })\n\n test('keeps actions outside the scrolling body and opts into keyboard avoidance', async () => {\n const root = await renderNative(\n <LumenSheet\n actions={<LumenButton>Save</LumenButton>}\n avoidKeyboard\n keyboardVerticalOffset={24}\n onDismiss={vi.fn()}\n safeAreaInsets={{ bottom: 34 }}\n scrollable\n title=\"Edit record\"\n visible\n >\n <LumenTextField accessibilityLabel=\"Name\" />\n </LumenSheet>\n )\n const scroll = root.container.queryAll(instance => instance.type === 'ScrollView')[0]\n const keyboard = root.container.queryAll(instance => instance.type === 'KeyboardAvoidingView')[0]\n if (!scroll || !keyboard) throw new Error('Missing keyboard or scrolling surface')\n expect(readProp(keyboard, 'enabled')).toBe(true)\n expect(readProp(keyboard, 'behavior')).toBe('padding')\n expect(readProp(keyboard, 'keyboardVerticalOffset')).toBe(24)\n expect(readProp(scroll, 'keyboardShouldPersistTaps')).toBe('handled')\n expect(scroll.queryAll(instance => readProp(instance, 'accessibilityRole') === 'button')).toHaveLength(0)\n expect(findByAccessibilityRole(root, 'header')).toBeDefined()\n expect(root.container.queryAll(instance => {\n const style = readProp(instance, 'style')\n return typeof style === 'object' && style !== null && 'paddingBottom' in style && style.paddingBottom === 34\n })).toHaveLength(1)\n })\n\n test('blocks platform and backdrop dismissal until the application unlocks it', async () => {\n const dismiss = vi.fn()\n const root = await renderNative(\n <LumenSheet dismissible={false} onDismiss={dismiss} visible><LumenText>Saving</LumenText></LumenSheet>\n )\n const modal = root.container.queryAll(instance => instance.type === 'Modal')[0]\n const backdrop = root.container.queryAll(instance => instance.type === 'Pressable')[0]\n if (!modal || !backdrop) throw new Error('Missing modal or backdrop')\n callAction(readProp(modal, 'onRequestClose'), 'Missing platform dismiss')\n callAction(readProp(backdrop, 'onPress'), 'Missing backdrop dismiss')\n expect(dismiss).not.toHaveBeenCalled()\n expect(readProp(backdrop, 'disabled')).toBe(true)\n await act(async () => {\n root.render(<LumenProvider scheme=\"light\"><LumenSheet onDismiss={dismiss} visible><LumenText>Saved</LumenText></LumenSheet></LumenProvider>)\n await Promise.resolve()\n })\n const updatedModal = root.container.queryAll(instance => instance.type === 'Modal')[0]\n if (!updatedModal) throw new Error('Missing updated modal')\n callAction(readProp(updatedModal, 'onRequestClose'), 'Missing platform dismiss')\n expect(dismiss).toHaveBeenCalledOnce()\n })\n\n test('names sheets and hides backdrop targets from keyboard and accessibility traversal', async () => {\n const root = await renderNative(<LumenSheet onDismiss={vi.fn()} title=\"Edit record\" visible><LumenText>Details</LumenText></LumenSheet>)\n const modal = root.container.queryAll(instance => instance.type === 'Modal')[0]\n const backdrop = root.container.queryAll(instance => instance.type === 'Pressable')[0]\n if (!modal || !backdrop) throw new Error('Missing modal or backdrop')\n expect(readProp(modal, 'accessibilityLabel')).toBe('Edit record')\n expect(readProp(backdrop, 'tabIndex')).toBe(-1)\n expect(readProp(backdrop, 'aria-hidden')).toBe(true)\n })\n\n test('honors the native reduced-motion setting', async () => {\n nativeMotion.enabled = true\n const root = await renderNative(<LumenSheet onDismiss={vi.fn()} visible><LumenText>Details</LumenText></LumenSheet>)\n const modal = root.container.queryAll(instance => instance.type === 'Modal')[0]\n if (!modal) throw new Error('Missing modal')\n expect(readProp(modal, 'animationType')).toBe('none')\n })\n\n test('honors reduced motion in confirmation dialogs and menus', async () => {\n nativeMotion.enabled = true\n const root = await renderNative(\n <>\n <LumenAlertDialog confirmLabel=\"Save\" onConfirm={vi.fn()} onDismiss={vi.fn()} title=\"Save changes\" visible />\n <LumenMenu accessibilityLabel=\"Actions\" items={[]} trigger={<LumenText>Open menu</LumenText>} />\n </>\n )\n const modals = root.container.queryAll(instance => instance.type === 'Modal')\n\n expect(modals).toHaveLength(2)\n expect(modals.map(modal => readProp(modal, 'animationType'))).toEqual(['none', 'none'])\n })\n\n test('uses a centered adaptive dialog on wide windows and Android keyboard behavior', async () => {\n nativeWindow.width = 1024\n nativePlatform.OS = 'android'\n const root = await renderNative(\n <LumenSheet avoidKeyboard onDismiss={vi.fn()} presentation=\"adaptive\" visible><LumenText>Details</LumenText></LumenSheet>\n )\n const modal = root.container.queryAll(instance => instance.type === 'Modal')[0]\n const keyboard = root.container.queryAll(instance => instance.type === 'KeyboardAvoidingView')[0]\n if (!modal || !keyboard) throw new Error('Missing modal or keyboard surface')\n expect(readProp(modal, 'animationType')).toBe('fade')\n expect(readProp(keyboard, 'behavior')).toBe('height')\n expect(readProp(keyboard, 'style')).toMatchObject({ alignItems: 'center', justifyContent: 'center' })\n })\n})\n\ndescribe('Static graphic icon accessibility', () => {\n const Graphic = ({ size, strokeWidth }: LumenIconGraphicProps): ReactElement => (\n <LumenText>{`Graphic ${String(size)} ${String(strokeWidth)}`}</LumenText>\n )\n\n test('names informative graphics and hides decorative graphics', async () => {\n const root = await renderNative(\n <>\n <GraphicIcon icon={Graphic} label=\"Search records\" size=\"lg\" strokeWidth={3} />\n <GraphicIcon icon={Graphic} decorative label=\"Decorative search\" />\n </>\n )\n const informative = root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'image' && readProp(instance, 'accessible') === true)\n expect(informative).toHaveLength(1)\n const named = informative[0]\n if (!named) throw new Error('Missing informative graphic')\n expect(readProp(named, 'accessibilityLabel')).toBe('Search records')\n expect(readProp(named, 'importantForAccessibility')).toBe('yes')\n expect(root.container.queryAll(instance => readProp(instance, 'children') === 'Graphic 24 3').length).toBeGreaterThan(0)\n const hidden = root.container.queryAll(instance => readProp(instance, 'importantForAccessibility') === 'no' && readProp(instance, 'accessible') === false)\n expect(hidden).toHaveLength(1)\n const decorative = hidden[0]\n if (!decorative) throw new Error('Missing decorative graphic')\n expect(readProp(decorative, 'accessibilityLabel')).toBeUndefined()\n expect(readProp(decorative, 'accessibilityElementsHidden')).toBe(true)\n })\n\n test('preserves named and custom icons through the root entrypoint', async () => {\n const root = await renderNative(\n <>\n <LumenIcon name=\"search\" label=\"Named search\" />\n <LumenIcon icon={Graphic} label=\"Custom search\" />\n <LumenIconButton name=\"search\" label=\"Named search action\" disabled />\n <LumenIconButton icon={Graphic} label=\"Custom search action\" disabled />\n </>\n )\n const icons = root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'image')\n expect(icons.map(icon => readProp(icon, 'accessibilityLabel'))).toEqual(['Named search', 'Custom search'])\n const buttons = root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'button')\n expect(buttons.map(button => readProp(button, 'accessibilityLabel'))).toEqual(['Named search action', 'Custom search action'])\n expect(buttons.map(button => readProp(button, 'accessibilityState'))).toEqual([{ disabled: true }, { disabled: true }])\n })\n\n test('canonical per-icon imports match named artwork and preserve accessible labels', async () => {\n const root = await renderNative(\n <>\n <LumenIcon name=\"search\" label=\"Named search\" size=\"lg\" strokeWidth={3} />\n <GraphicIcon icon={LumenSearchIconGraphic} label=\"Static search\" size=\"lg\" strokeWidth={3} />\n <LumenIcon name=\"brand:github\" decorative />\n <GraphicIcon icon={LumenBrandGithubIconGraphic} decorative />\n </>\n )\n const graphics = root.container.queryAll(instance => instance.type === 'Svg')\n expect(graphics).toHaveLength(4)\n const geometry = (graphic: TestInstance | undefined): unknown => {\n if (!graphic) throw new Error('Missing canonical SVG')\n return {\n props: Object.fromEntries(Object.entries(graphic.props).filter(([name]) => name !== 'children')),\n children: graphic.children.filter(child => typeof child !== 'string').map(child => ({ type: child.type, props: child.props }))\n }\n }\n expect(geometry(graphics[0])).toEqual(geometry(graphics[1]))\n expect(geometry(graphics[2])).toEqual(geometry(graphics[3]))\n const icons = root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'image')\n expect(icons.map(icon => readProp(icon, 'accessibilityLabel'))).toEqual(['Named search', 'Static search'])\n })\n\n test('keeps graphic buttons named and announces disabled state', async () => {\n const root = await renderNative(<GraphicIconButton icon={Graphic} label=\"Search records\" disabled />)\n const button = root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'button')[0]\n if (!button) throw new Error('Missing graphic button')\n expect(readProp(button, 'accessibilityLabel')).toBe('Search records')\n expect(readProp(button, 'accessibilityState')).toEqual({ disabled: true })\n expect(readProp(button, 'disabled')).toBe(true)\n })\n})\n\nconst callInputAction = (value: unknown, proposal: string): void => {\n if (typeof value !== 'function') throw new Error('Missing native input callback')\n Reflect.apply(value, undefined, [proposal])\n}\n\nconst runNativeAction = async (action: () => void): Promise<void> => {\n await act(async () => {\n action()\n\n await Promise.resolve()\n })\n}\n\ndescribe('advanced native inputs', () => {\n test('secure entry hides on blur and after disabling and re-enabling', async () => {\n const props = { label: 'Password', onValueChange: vi.fn(), value: 'synthetic-fixture' }\n const root = await renderNative(<LumenPasswordField {...props} />)\n expect(readProp(findByAccessibilityLabel(root, 'Password'), 'secureTextEntry')).toBe(true)\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Show password'), 'onPress'), 'Missing reveal')\n })\n expect(readProp(findByAccessibilityLabel(root, 'Password'), 'secureTextEntry')).toBe(false)\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Password'), 'onBlur'), 'Missing blur')\n })\n expect(readProp(findByAccessibilityLabel(root, 'Password'), 'secureTextEntry')).toBe(true)\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Show password'), 'onPress'), 'Missing reveal')\n })\n await runNativeAction(() => {\n root.render(<LumenProvider><LumenPasswordField {...props} enabled={false} /></LumenProvider>)\n })\n await runNativeAction(() => {\n root.render(<LumenProvider><LumenPasswordField {...props} /></LumenProvider>)\n })\n expect(readProp(findByAccessibilityLabel(root, 'Password'), 'secureTextEntry')).toBe(true)\n })\n\n test('OTP preserves native autofill, normalizes paste and emits completion only for changed full codes', async () => {\n const onValueChange = vi.fn<(value: string) => void>()\n const onComplete = vi.fn<(value: string) => void>()\n const root = await renderNative(<LumenInputOTP label=\"Code\" value=\"123\" onValueChange={onValueChange} onComplete={onComplete} />)\n const input = findByAccessibilityLabel(root, 'Code')\n expect(readProp(input, 'textContentType')).toBe('oneTimeCode')\n await runNativeAction(() => {\n callInputAction(readProp(input, 'onChangeText'), '١٢٣-456')\n })\n expect(onValueChange).toHaveBeenCalledExactlyOnceWith('123456')\n expect(onComplete).toHaveBeenCalledExactlyOnceWith('123456')\n await runNativeAction(() => {\n callInputAction(readProp(input, 'onChangeText'), 'not a code')\n })\n expect(onValueChange).toHaveBeenCalledTimes(1)\n await runNativeAction(() => {\n root.render(<LumenProvider><LumenInputOTP label=\"Code\" value=\"123456\" onValueChange={onValueChange} onComplete={onComplete} readOnly /></LumenProvider>)\n })\n await runNativeAction(() => {\n callInputAction(readProp(findByAccessibilityLabel(root, 'Code'), 'onChangeText'), '654321')\n })\n expect(onValueChange).toHaveBeenCalledTimes(1)\n })\n\n test('number entry retains raw drafts and steps exact localized values', async () => {\n const onValueChange = vi.fn<(value: string) => void>()\n const root = await renderNative(<LumenNumberField label=\"Amount\" value=\"0,1\" locale=\"es-CO\" step=\"0.2\" onValueChange={onValueChange} />)\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Increase value'), 'onPress'), 'Missing increase')\n })\n expect(onValueChange).toHaveBeenLastCalledWith('0,3')\n await runNativeAction(() => {\n callInputAction(readProp(findByAccessibilityLabel(root, 'Amount'), 'onChangeText'), '12,')\n })\n expect(onValueChange).toHaveBeenLastCalledWith('12,')\n await runNativeAction(() => {\n root.render(<LumenProvider><LumenNumberField label=\"Amount\" value=\"12,\" locale=\"es-CO\" onValueChange={onValueChange} /></LumenProvider>)\n })\n expect(readProp(findByAccessibilityLabel(root, 'Increase value'), 'disabled')).toBe(true)\n expect(readProp(findByAccessibilityLabel(root, 'Amount'), 'aria-invalid')).toBe(true)\n })\n\n test('autocomplete publishes query before selection and closes its results', async () => {\n const calls: string[] = []\n const root = await renderNative(\n <LumenAutocomplete\n label=\"City\"\n query=\"Bo\"\n options={[{ label: 'Bogotá', value: 'bogota' }]}\n onQueryChange={query => {\n calls.push(query)\n }}\n onValueChange={value => {\n calls.push(value)\n }}\n />\n )\n await runNativeAction(() => {\n callInputAction(readProp(findByAccessibilityLabel(root, 'City'), 'onChangeText'), 'Bog')\n })\n calls.length = 0\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Bogotá'), 'onPress'), 'Missing result')\n })\n expect(calls).toEqual(['Bogotá', 'bogota'])\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityLabel') === 'Close results')).toHaveLength(0)\n })\n\n test('autocomplete loading hides results and disabled state dismisses selection', async () => {\n const props = { label: 'City', onQueryChange: vi.fn(), onValueChange: vi.fn(), options: [{ label: 'Bogotá', value: 'bogota' }], query: '' }\n const root = await renderNative(<LumenAutocomplete {...props} loading />)\n await runNativeAction(() => {\n callInputAction(readProp(findByAccessibilityLabel(root, 'City'), 'onChangeText'), 'Bo')\n })\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityLabel') === 'Bogotá')).toHaveLength(0)\n await runNativeAction(() => {\n root.render(<LumenProvider><LumenAutocomplete {...props} enabled={false} /></LumenProvider>)\n })\n await runNativeAction(() => {\n root.render(<LumenProvider><LumenAutocomplete {...props} /></LumenProvider>)\n })\n expect(readProp(findByAccessibilityLabel(root, 'City'), 'accessibilityState')).toMatchObject({ expanded: false })\n })\n\n test.each(['ios', 'android', 'web'])('time formatting tolerates malformed locales on %s', async platform => {\n nativePlatform.OS = platform\n for (const locale of ['not_a_locale', 'en--US', '💥']) {\n const props = { label: 'Time', value: { hour: 9, minute: 30 }, locale, onValueChange: vi.fn() }\n const root = await renderNative(<LumenTimeField {...props} is24Hour />)\n\n expect(findByAccessibilityLabel(root, 'Time, 09:30')).toBeDefined()\n await runNativeAction(() => {\n root.render(<LumenProvider><LumenTimeField {...props} is24Hour={false} /></LumenProvider>)\n })\n expect(findByAccessibilityLabel(root, 'Time, 9:30 AM')).toBeDefined()\n }\n })\n\n test('time selection requires confirmation and cancellation preserves the value', async () => {\n nativePlatform.OS = 'ios'\n const onValueChange = vi.fn()\n const root = await renderNative(\n <LumenTimeField\n label=\"Time\"\n value={{ hour: 9, minute: 30 }}\n locale=\"en-US\"\n is24Hour\n minTime={{ hour: 9, minute: 0 }}\n maxTime={{ hour: 17, minute: 0 }}\n onValueChange={onValueChange}\n />\n )\n const open = async (): Promise<void> => {\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Time, 09:30'), 'onPress'), 'Missing time trigger')\n })\n }\n await open()\n await runNativeAction(() => {\n const picker = findHostComponent(root, 'NativeDatePicker')\n const change = readProp(picker, 'onChange')\n if (typeof change !== 'function') throw new Error('Missing time change')\n Reflect.apply(change, undefined, [{ type: 'set' }, new Date(2000, 0, 1, 10, 45)])\n })\n expect(onValueChange).not.toHaveBeenCalled()\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Cancel'), 'onPress'), 'Missing cancel')\n })\n expect(onValueChange).not.toHaveBeenCalled()\n await open()\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Confirm'), 'onPress'), 'Missing confirm')\n })\n expect(onValueChange).toHaveBeenCalledExactlyOnceWith({ hour: 9, minute: 30 })\n })\n\n test.each(['ios', 'web'])('time sheet resets controlled changes without discarding equivalent inputs on %s', async platform => {\n nativePlatform.OS = platform\n const updates = [\n { value: { hour: 11, minute: 15 }, expected: { hour: 11, minute: 15 } },\n { minTime: { hour: 10, minute: 0 }, expected: { hour: 10, minute: 0 } },\n { maxTime: { hour: 9, minute: 15 }, expected: { hour: 9, minute: 0 } }\n ]\n for (const { expected, ...update } of updates) {\n const onValueChange = vi.fn()\n const props: LumenTimeFieldProps = { label: 'Time',\n value: { hour: 9, minute: 30 },\n locale: 'en-US',\n is24Hour: true,\n minTime: { hour: 9, minute: 0 },\n maxTime: { hour: 17, minute: 0 },\n onValueChange }\n const root = await renderNative(<LumenTimeField {...props} />)\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Time, 09:30'), 'onPress'), 'Missing time trigger')\n })\n await runNativeAction(() => {\n const picker = findHostComponent(root, platform === 'web' ? 'input' : 'NativeDatePicker')\n const change = readProp(picker, 'onChange')\n if (typeof change !== 'function') throw new Error('Missing time change')\n Reflect.apply(change, undefined, platform === 'web' ? [{ currentTarget: { value: '10:45' } }] : [{ type: 'set' }, new Date(2000, 0, 1, 10, 45)])\n })\n await runNativeAction(() => {\n root.render(\n <LumenProvider>\n <LumenTimeField\n {...props}\n value={{ hour: 9, minute: 30 }}\n minTime={{ hour: 9, minute: 0 }}\n maxTime={{ hour: 17, minute: 0 }}\n />\n </LumenProvider>\n )\n })\n expect(readProp(findHostComponent(root, 'Modal'), 'visible')).toBe(true)\n const draft = readProp(findHostComponent(root, platform === 'web' ? 'input' : 'NativeDatePicker'), 'value')\n expect(draft).toEqual(platform === 'web' ? '10:45' : new Date(2000, 0, 1, 10, 45))\n await runNativeAction(() => {\n root.render(<LumenProvider><LumenTimeField {...props} {...update} /></LumenProvider>)\n })\n expect(readProp(findHostComponent(root, 'Modal'), 'visible')).toBe(false)\n expect(onValueChange).not.toHaveBeenCalled()\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, `Time, ${update.value ? '11:15' : '09:30'}`), 'onPress'), 'Missing updated time trigger')\n })\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Confirm'), 'onPress'), 'Missing time confirm')\n })\n expect(onValueChange).toHaveBeenCalledExactlyOnceWith(expected)\n }\n })\n\n test('Android time rejects out-of-range selection and stale callbacks after disabling', async () => {\n nativePlatform.OS = 'android'\n const onValueChange = vi.fn()\n const props = { label: 'Time', locale: 'en-US', onValueChange, value: null }\n const root = await renderNative(<LumenTimeField {...props} minTime={{ hour: 9, minute: 0 }} />)\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Time, Choose a time'), 'onPress'), 'Missing time trigger')\n })\n const options = vi.mocked(DateTimePickerAndroid.open).mock.calls.at(-1)?.[0]\n if (!options?.onValueChange) throw new Error('Missing Android time handler')\n const change = options.onValueChange\n await runNativeAction(() => {\n change({ nativeEvent: { timestamp: 0, utcOffset: 0 } }, new Date(2000, 0, 1, 8, 0))\n })\n expect(onValueChange).not.toHaveBeenCalled()\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Time, Choose a time'), 'onPress'), 'Missing time trigger')\n })\n const current = vi.mocked(DateTimePickerAndroid.open).mock.calls.at(-1)?.[0]\n if (!current?.onValueChange) throw new Error('Missing Android time handler')\n const stale = current.onValueChange\n await runNativeAction(() => {\n root.render(<LumenProvider><LumenTimeField {...props} enabled={false} /></LumenProvider>)\n })\n expect(DateTimePickerAndroid.dismiss).toHaveBeenCalledWith('time')\n await runNativeAction(() => {\n stale({ nativeEvent: { timestamp: 0, utcOffset: 0 } }, new Date(2000, 0, 1, 10, 0))\n })\n expect(onValueChange).not.toHaveBeenCalled()\n })\n\n test('Android time validates current bounds and uses the current callback while the dialog is open', async () => {\n nativePlatform.OS = 'android'\n const openingCallback = vi.fn()\n const currentCallback = vi.fn()\n const props = { label: 'Time', locale: 'en-US', value: null }\n const root = await renderNative(\n <LumenTimeField {...props} minTime={{ hour: 9, minute: 0 }} onValueChange={openingCallback} />\n )\n const chooseAfterUpdate = async (hour: number): Promise<void> => {\n await runNativeAction(() => {\n root.render(\n <LumenProvider>\n <LumenTimeField {...props} minTime={{ hour: 9, minute: 0 }} onValueChange={openingCallback} />\n </LumenProvider>\n )\n })\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Time, Choose a time'), 'onPress'), 'Missing time trigger')\n })\n const options = vi.mocked(DateTimePickerAndroid.open).mock.calls.at(-1)?.[0]\n if (!options?.onValueChange) throw new Error('Missing Android time handler')\n const change = options.onValueChange\n await runNativeAction(() => {\n root.render(<LumenProvider><LumenTimeField {...props} minTime={{ hour: 12, minute: 0 }} maxTime={{ hour: 17, minute: 0 }} onValueChange={currentCallback} rangeErrorLabel=\"Choose an afternoon time\" /></LumenProvider>)\n })\n await runNativeAction(() => {\n change({ nativeEvent: { timestamp: 0, utcOffset: 0 } }, new Date(2000, 0, 1, hour, 30))\n })\n }\n await chooseAfterUpdate(10)\n expect(openingCallback).not.toHaveBeenCalled()\n expect(currentCallback).not.toHaveBeenCalled()\n expect(root.container.queryAll(instance => readProp(instance, 'children') === 'Choose an afternoon time').length).toBeGreaterThan(0)\n await chooseAfterUpdate(18)\n expect(currentCallback).not.toHaveBeenCalled()\n await chooseAfterUpdate(13)\n expect(openingCallback).not.toHaveBeenCalled()\n expect(currentCallback).toHaveBeenCalledExactlyOnceWith({ hour: 13, minute: 30 })\n })\n\n test.each(['en--US', 'invalid_tag', '💥'])('image comparison falls back for malformed locale %s', async locale => {\n const root = await renderNative(<LumenImageComparison label=\"Comparison\" before={{ uri: 'fixture:before' }} after={{ uri: 'fixture:after' }} value={0.25} locale={locale} onValueChange={() => {}} />)\n const control = findByAccessibilityRole(root, 'adjustable')\n expect(readProp(control, 'accessibilityValue')).toMatchObject({ text: 'After 25%' })\n })\n\n test.each(['side-by-side', 'before', 'after'] as const)('image comparison %s mode presents labels without an adjustable control', async mode => {\n const root = await renderNative(<LumenImageComparison label=\"Comparison\" before={{ uri: 'fixture:before' }} after={{ uri: 'fixture:after' }} value={0.25} mode={mode} onValueChange={() => {}} />)\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'adjustable')).toHaveLength(0)\n expect(root.container.queryAll(instance => readProp(instance, 'children') === 'Before').length > 0).toBe(mode !== 'after')\n expect(root.container.queryAll(instance => readProp(instance, 'children') === 'After').length > 0).toBe(mode !== 'before')\n })\n\n test('image comparison exposes one adjustable control and localized after percentage', async () => {\n const root = await renderNative(<LumenImageComparison label=\"Comparison\" before={{ uri: 'fixture:before' }} after={{ uri: 'fixture:after' }} value={0.25} locale=\"es-CO\" onValueChange={() => {}} />)\n const control = findByAccessibilityRole(root, 'adjustable')\n expect(readProp(control, 'accessibilityLabel')).toBe('Comparison')\n expect(readProp(control, 'accessibilityValue')).toMatchObject({ text: 'After 25%' })\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'image')).toHaveLength(0)\n })\n})\n\ndescribe('native chart data inspection', () => {\n test('renders zero as neutral and missing measurements with a separate marker and exact fallback', async () => {\n const root = await renderNative(\n <LumenHeatmap\n label=\"Activity\"\n colorScale=\"diverging\"\n data={[\n { x: 'Mon', y: 'AM', value: -2 },\n { x: 'Tue', y: 'AM', value: 0 },\n { x: 'Wed', y: 'AM', value: null },\n { x: 'Mon', y: 'AM', value: 40 }\n ]}\n />\n )\n const neutral = root.container.queryAll(instance => instance.type === 'Rect' && readProp(instance, 'fillOpacity') === 0)\n expect(neutral).toHaveLength(1)\n const missing = root.container.queryAll(instance => instance.type === 'Text' && readProp(instance, 'children') === '× Not available')\n expect(missing).toHaveLength(1)\n await act(async () => {\n callAction(readProp(findByAccessibilityRole(root, 'button'), 'onPress'), 'Missing chart disclosure')\n await Promise.resolve()\n })\n const rows = root.container.queryAll(instance => instance.type === 'Text').map(instance => readProp(instance, 'children'))\n expect(rows).toContain('Wed, AM: Not available')\n expect(rows).toContain('Tue, AM: 0')\n expect(rows).toContain('Mon, AM: -2')\n expect(rows).not.toContain('Mon, AM: 40')\n })\n\n test('opens and closes exact source values using a labeled disclosure', async () => {\n const root = await renderNative(\n <LumenWaterfallChart\n label=\"Revenue\"\n data={[\n { id: 'opening', label: 'Opening', kind: 'total', value: 100 },\n { id: 'cost', label: 'Costs', value: -25 }\n ]}\n />\n )\n const button = findByAccessibilityRole(root, 'button')\n expect(readProp(button, 'accessibilityState')).toMatchObject({ expanded: false })\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'list')).toHaveLength(0)\n await act(async () => {\n callAction(readProp(button, 'onPress'), 'Missing chart disclosure')\n await Promise.resolve()\n })\n expect(readProp(findByAccessibilityRole(root, 'button'), 'accessibilityState')).toMatchObject({ expanded: true })\n const rows = root.container.queryAll(instance => instance.type === 'Text').map(instance => readProp(instance, 'children'))\n expect(rows).toContain('Costs, Start: 100, End: 75, Value: -25')\n await act(async () => {\n callAction(readProp(findByAccessibilityRole(root, 'button'), 'onPress'), 'Missing chart disclosure')\n await Promise.resolve()\n })\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'list')).toHaveLength(0)\n })\n\n test('shows a localized invalid-data state rather than a partial histogram', async () => {\n const root = await renderNative(\n <LumenHistogram\n label=\"Distribution\"\n labels={{ invalidData: 'Check the bins' }}\n data={[\n { start: 0, end: 10, count: 5 }, { start: 5, end: 15, count: 3 }\n ]}\n />\n )\n expect(readProp(findByAccessibilityRole(root, 'alert'), 'children')).toBe('Check the bins')\n expect(root.container.queryAll(instance => instance.type === 'Svg')).toHaveLength(0)\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'button')).toHaveLength(0)\n })\n})\n\ndescribe('controlled multiple selection', () => {\n test('removal retains unavailable values and never mutates the controlled set', async () => {\n const values = new Set(['one', 'missing'])\n const onValuesChange = vi.fn<(values: ReadonlySet<string>) => void>()\n const root = await renderNative(<LumenMultiSelect label=\"Teams\" values={values} onValuesChange={onValuesChange} query=\"\" onQueryChange={() => undefined} options={[{ label: 'One', value: 'one' }]} />)\n const remove = root.container.queryAll(instance => readProp(instance, 'accessibilityLabel') === 'Remove One').find(instance => typeof readProp(instance, 'onPress') === 'function')\n if (!remove) throw new Error('Missing selected removal')\n const handler = readProp(remove, 'onPress')\n if (typeof handler !== 'function') throw new Error('Missing removal handler')\n await act(async () => {\n Reflect.apply(handler, undefined, [])\n await Promise.resolve()\n })\n expect(onValuesChange).toHaveBeenLastCalledWith(new Set(['missing']))\n expect(values).toEqual(new Set(['one', 'missing']))\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityLabel') === 'Remove missing').length).toBeGreaterThan(0)\n })\n\n test.each([{ readOnly: true }, { enabled: false }])('blocked controls reject removal: %o', async blocked => {\n const onValuesChange = vi.fn<(values: ReadonlySet<string>) => void>()\n const root = await renderNative(<LumenMultiSelect {...blocked} label=\"Teams\" values={new Set(['one'])} onValuesChange={onValuesChange} query=\"\" onQueryChange={() => undefined} options={[{ label: 'One', value: 'one' }]} />)\n const remove = root.container.queryAll(instance => readProp(instance, 'accessibilityLabel') === 'Remove One').find(instance => typeof readProp(instance, 'onPress') === 'function')\n if (!remove) throw new Error('Missing removal')\n const handler = readProp(remove, 'onPress')\n if (typeof handler !== 'function') throw new Error('Missing removal handler')\n await act(async () => {\n Reflect.apply(handler, undefined, [])\n await Promise.resolve()\n })\n expect(onValuesChange).not.toHaveBeenCalled()\n expect(readProp(remove, 'disabled')).toBe(true)\n })\n\n test('disabled selected results cannot be removed', async () => {\n const root = await renderNative(\n <LumenMultiSelect\n label=\"Teams\"\n values={new Set(['one'])}\n onValuesChange={() => {\n throw new Error('Disabled callback')\n }}\n query=\"\"\n onQueryChange={() => undefined}\n options={[{ label: 'One', value: 'one', disabled: true }]}\n />\n )\n const remove = root.container.queryAll(instance => readProp(instance, 'accessibilityLabel') === 'Remove One').find(instance => typeof readProp(instance, 'onPress') === 'function')\n if (!remove) throw new Error('Missing removal')\n expect(readProp(remove, 'disabled')).toBe(true)\n })\n})\n\ndescribe('multiple selection results', () => {\n test('checkbox intent retains values outside the result list', async () => {\n const onValuesChange = vi.fn<(values: ReadonlySet<string>) => void>()\n const values = new Set(['missing'])\n const root = await renderNative(<LumenMultiSelect label=\"Teams\" values={values} onValuesChange={onValuesChange} query=\"\" onQueryChange={() => undefined} options={[{ label: 'One', value: 'one' }]} />)\n const option = root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'checkbox').find(instance => typeof readProp(instance, 'onPress') === 'function')\n if (!option) throw new Error('Missing option')\n const handler = readProp(option, 'onPress')\n if (typeof handler !== 'function') throw new Error('Missing checkbox handler')\n await act(async () => {\n Reflect.apply(handler, undefined, [])\n await Promise.resolve()\n })\n expect(onValuesChange).toHaveBeenLastCalledWith(new Set(['missing', 'one']))\n expect(values).toEqual(new Set(['missing']))\n })\n\n test.each([{ loading: true }, { resultsErrorMessage: 'Search failed' }])('stale results are unavailable: %o', async state => {\n const root = await renderNative(<LumenMultiSelect {...state} label=\"Teams\" values={new Set<string>()} onValuesChange={() => undefined} query=\"\" onQueryChange={() => undefined} options={[{ label: 'One', value: 'one' }]} />)\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'checkbox')).toHaveLength(0)\n })\n})\n\ndescribe('multiple selection option identity', () => {\n test.each([\n { options: [{ label: 'One', value: '' }] },\n { options: [{ label: ' ', value: 'one' }] },\n { options: [{ label: 'One', value: 'one' }, { label: 'Duplicate', value: 'one' }] }\n ])('rejects invalid option identities: %o', async ({ options }) => {\n await expect(renderNative(<LumenMultiSelect label=\"Teams\" values={new Set<string>()} onValuesChange={() => undefined} query=\"\" onQueryChange={() => undefined} options={options} />)).rejects.toThrow('MultiSelect options require')\n })\n})\n\ntest('multiple selection forwards application safe-area insets to its modal', async () => {\n const root = await renderNative(<LumenMultiSelect label=\"Teams\" values={new Set<string>()} onValuesChange={() => undefined} query=\"\" onQueryChange={() => undefined} options={[]} safeAreaInsets={{ bottom: 96 }} />)\n const panels = root.container.queryAll(instance => {\n const style = readProp(instance, 'style')\n\n return typeof style === 'object' && style !== null && 'paddingBottom' in style && style.paddingBottom === 96\n })\n expect(panels.length).toBeGreaterThan(0)\n})\n\ndescribe('rating controlled selection', () => {\n test('clamps nonfinite values and limits the rendered option count', () => {\n expect(resolveLumenRating(Number.NaN, Number.POSITIVE_INFINITY)).toEqual({ max: 5, value: 0 })\n expect(resolveLumenRating(-5, 0)).toEqual({ max: 1, value: 0 })\n expect(resolveLumenRating(999, 999)).toEqual({ max: 100, value: 100 })\n expect(resolveLumenRating(3.9)).toEqual({ max: 5, value: 3 })\n })\n\n test('reports selection without mutating controlled value and blocks read-only edits', async () => {\n const onValueChange = vi.fn<(value: number) => void>()\n const root = await renderNative(\n <LumenRating\n label=\"Score\"\n value={2}\n onValueChange={onValueChange}\n formatOption={(value, max) => `${value} of ${max}`}\n />\n )\n const option = findByAccessibilityLabel(root, '4 of 5')\n act(() => {\n callAction(readProp(option, 'onPress'), 'Missing rating action')\n })\n expect(onValueChange).toHaveBeenCalledWith(4)\n expect(readProp(option, 'accessibilityState')).toEqual({ checked: false, disabled: false, selected: false })\n act(() => {\n root.render(\n <LumenProvider>\n <LumenRating\n label=\"Score\"\n value={2}\n readOnly\n onValueChange={onValueChange}\n />\n </LumenProvider>\n )\n })\n const locked = findByAccessibilityLabel(root, '4 / 5')\n act(() => {\n callAction(readProp(locked, 'onPress'), 'Missing rating action')\n })\n expect(onValueChange).toHaveBeenCalledTimes(1)\n expect(readProp(locked, 'disabled')).toBe(true)\n })\n})\n\ndescribe('progress and history contracts', () => {\n test('normalizes step progress without changing host state', () => {\n expect(resolveLumenStepState(0, -1, 3)).toBe('current')\n expect(resolveLumenStepState(0, Number.NaN, 3)).toBe('current')\n expect(resolveLumenStepState(1, 1.9, 3)).toBe('current')\n expect(resolveLumenStepState(2, 99, 3)).toBe('complete')\n expect(resolveLumenStepState(2, 1, 3)).toBe('upcoming')\n })\n\n test('presents localized controlled progression and accepts an empty workflow', async () => {\n const steps = [{ id: 'prepare', title: 'Prepare' }, { id: 'review', title: 'Review', description: 'Confirm details' }]\n const root = await renderNative(\n <LumenStepper\n label=\"Workflow\"\n steps={steps}\n currentStep={1}\n formatState={state => `Localized ${state}`}\n />\n )\n const review = findByAccessibilityLabel(root, '2 / 2, Review, Confirm details')\n expect(readProp(review, 'accessibilityValue')).toEqual({ text: 'Localized current' })\n expect(readProp(review, 'accessibilityState')).toEqual({ selected: true })\n act(() => {\n root.render(<LumenProvider><LumenStepper label=\"Workflow\" steps={steps} currentStep={2} /></LumenProvider>)\n })\n expect(readProp(findByAccessibilityLabel(root, '2 / 2, Review, Confirm details'), 'accessibilityValue'))\n .toEqual({ text: 'Complete' })\n act(() => {\n root.render(<LumenProvider><LumenStepper label=\"Workflow\" steps={[]} currentStep={0} /></LumenProvider>)\n })\n expect(root.container.queryAll(instance => readProp(instance, 'accessible') === true)).toHaveLength(0)\n })\n\n test('keeps timeline content actions accessible and host owned', async () => {\n const onPress = vi.fn()\n const root = await renderNative(\n <LumenTimeline label=\"History\">\n <LumenTimelineItem>\n <LumenText>Approved</LumenText>\n <LumenButton onPress={onPress}>View approval</LumenButton>\n </LumenTimelineItem>\n </LumenTimeline>\n )\n const button = findByAccessibilityRole(root, 'button')\n act(() => {\n callAction(readProp(button, 'onPress'), 'Missing timeline action')\n })\n expect(onPress).toHaveBeenCalledOnce()\n })\n\n test('breadcrumb reports navigation IDs and cannot navigate current or disabled locations', async () => {\n const onNavigate = vi.fn<(id: string) => void>()\n const items = [{ id: 'home', label: 'Home' },\n { id: 'private', label: 'Private', disabled: true },\n { id: 'current', label: 'Details' }]\n const root = await renderNative(\n <LumenBreadcrumb\n label=\"Location\"\n items={items}\n onNavigate={onNavigate}\n currentLabel=\"Here\"\n />\n )\n const home = findByAccessibilityLabel(root, 'Home')\n act(() => {\n callAction(readProp(home, 'onPress'), 'Missing breadcrumb action')\n })\n expect(onNavigate).toHaveBeenCalledExactlyOnceWith('home')\n const locked = findByAccessibilityLabel(root, 'Private')\n act(() => {\n callAction(readProp(locked, 'onPress'), 'Missing disabled breadcrumb action')\n })\n expect(readProp(findByAccessibilityLabel(root, 'Details, Here'), 'onPress')).toBeUndefined()\n expect(readProp(findByAccessibilityLabel(root, 'Details, Here'), 'aria-current')).toBe('page')\n })\n})\n\ndescribe('data table controlled interactions', () => {\n const columns = [{ key: 'amount', label: 'Amount', sortable: true }]\n const rows: readonly LumenTableRow[] = [\n { id: 'one', label: 'One', cells: { amount: { text: '20', sortValue: 20 } } },\n { id: 'two', label: 'Two', cells: { amount: { text: '2', sortValue: 2 } }, disabled: true }\n ]\n test('emits manual sort requests without reordering and retains hidden selections', async () => {\n const onSortChange = vi.fn()\n const onSelectionChange = vi.fn()\n const root = await renderNative(\n <LumenDataTable\n label=\"Records\"\n columns={columns}\n rows={rows}\n onSortChange={onSortChange}\n onSelectionChange={onSelectionChange}\n selectedIds={new Set(['hidden'])}\n />\n )\n act(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Amount'), 'onPress'), 'Missing sort action')\n })\n expect(onSortChange).toHaveBeenCalledWith({ key: 'amount', direction: 'ascending' })\n act(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Select visible'), 'onPress'), 'Missing bulk selection')\n })\n expect(onSelectionChange).toHaveBeenCalledWith(new Set(['hidden', 'one']))\n expect(root.container.queryAll(instance => readProp(instance, 'accessible') === true)\n .map(instance => readProp(instance, 'accessibilityLabel'))).toEqual(['Amount, 20', 'Amount, 2'])\n })\n test('loading and error states hide stale controls; retry remains host owned', async () => {\n const onRetry = vi.fn()\n const root = await renderNative(\n <LumenDataTable\n label=\"Records\"\n columns={columns}\n rows={rows}\n loading\n loadingLabel=\"Fetching\"\n onSortChange={vi.fn()}\n />\n )\n expect(readProp(findByAccessibilityRole(root, 'progressbar'), 'children')).toBe('Fetching')\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'button')).toHaveLength(0)\n act(() => {\n root.render(\n <LumenProvider>\n <LumenDataTable\n label=\"Records\"\n columns={columns}\n rows={rows}\n error=\"Try again\"\n onRetry={onRetry}\n retryLabel=\"Reload\"\n />\n </LumenProvider>\n )\n })\n act(() => {\n callAction(readProp(findByAccessibilityRole(root, 'button'), 'onPress'), 'Missing retry')\n })\n expect(onRetry).toHaveBeenCalledOnce()\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'checkbox')).toHaveLength(0)\n })\n test('read-only tables block both sort and selection callbacks', async () => {\n const onSortChange = vi.fn()\n const onSelectionChange = vi.fn()\n const root = await renderNative(\n <LumenDataTable\n label=\"Records\"\n columns={columns}\n rows={rows}\n readOnly\n onSortChange={onSortChange}\n onSelectionChange={onSelectionChange}\n />\n )\n act(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Amount'), 'onPress'), 'Missing guarded sort')\n })\n act(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Select visible'), 'onPress'), 'Missing guarded selection')\n })\n expect(onSortChange).not.toHaveBeenCalled()\n expect(onSelectionChange).not.toHaveBeenCalled()\n })\n})\n\ntest.each(['light', 'dark'] as const)('nested button content uses its %s foreground without overriding explicit tones', async scheme => {\n const theme = createLumenTheme(scheme)\n const root = await renderNative(\n <LumenProvider theme={theme}>\n <LumenButton>\n <LumenText accessibilityLabel=\"Inherited label\">Continue</LumenText>\n <LumenText accessibilityLabel=\"Explicit tone\" tone=\"danger\">Warning</LumenText>\n <LumenIcon name=\"search\" label=\"Inherited icon\" />\n </LumenButton>\n <LumenText accessibilityLabel=\"Outside control\">Outside</LumenText>\n </LumenProvider>\n )\n expect(readProp(findByAccessibilityLabel(root, 'Inherited label'), 'style')).toEqual(expect.arrayContaining([{ color: theme.colors.onBrand }]))\n expect(readProp(findByAccessibilityLabel(root, 'Explicit tone'), 'style')).toEqual(expect.arrayContaining([{ color: theme.colors.danger }]))\n expect(readProp(findByAccessibilityLabel(root, 'Outside control'), 'style')).toEqual(expect.arrayContaining([{ color: theme.colors.ink }]))\n const svg = findByAccessibilityLabel(root, 'Inherited icon').children[0]\n if (typeof svg !== 'object') throw new Error('Missing inherited icon content')\n expect(readProp(svg, 'stroke')).toBe(theme.colors.onBrand)\n})\n\ntest.each([false, true])('web checkbox Space toggles once and respects disabled=%s', async disabled => {\n nativePlatform.OS = 'web'\n const change = vi.fn<(checked: boolean) => void>()\n const hostKey = vi.fn()\n const root = await renderNative(<LumenCheckbox label=\"Choice\" checked disabled={disabled} onCheckedChange={change} onKeyDown={hostKey} />)\n const handler = readProp(findByAccessibilityRole(root, 'checkbox'), 'onKeyDown')\n if (typeof handler !== 'function') throw new Error('Missing checkbox keyboard handler')\n const preventDefault = vi.fn()\n await act(async () => {\n Reflect.apply(handler, undefined, [{ nativeEvent: { key: ' ', repeat: false }, defaultPrevented: false, preventDefault }])\n await Promise.resolve()\n })\n expect(hostKey).toHaveBeenCalledOnce()\n expect(preventDefault).toHaveBeenCalledOnce()\n expect(change).toHaveBeenCalledTimes(disabled ? 0 : 1)\n expect(change.mock.calls).toEqual(disabled ? [] : [[false]])\n await act(async () => {\n Reflect.apply(handler, undefined, [{ nativeEvent: { key: ' ', repeat: true }, defaultPrevented: false, preventDefault }])\n Reflect.apply(handler, undefined, [{ nativeEvent: { key: ' ', repeat: false }, defaultPrevented: true, preventDefault }])\n await Promise.resolve()\n })\n expect(change).toHaveBeenCalledTimes(disabled ? 0 : 1)\n})\n\ntest('media thumbnail keeps controlled selection and blocks loading requests', async () => {\n const onSelectionChange = vi.fn()\n const props = { label: 'Landscape', selected: true, onSelectionChange }\n const root = await renderNative(<LumenMediaThumbnail {...props}><LumenText>Photo</LumenText></LumenMediaThumbnail>)\n const button = findByAccessibilityLabel(root, 'Landscape')\n expect(readProp(button, 'accessibilityState')).toMatchObject({ selected: true })\n await runNativeAction(() => {\n callAction(readProp(button, 'onPress'), 'Missing thumbnail action')\n })\n expect(onSelectionChange).toHaveBeenCalledWith(false)\n await runNativeAction(() => {\n root.render(<LumenProvider><LumenMediaThumbnail {...props} state=\"loading\"><LumenText>Photo</LumenText></LumenMediaThumbnail></LumenProvider>)\n })\n const loading = findByAccessibilityLabel(root, 'Landscape')\n expect(readProp(loading, 'accessibilityState')).toMatchObject({ busy: true, disabled: true })\n await runNativeAction(() => {\n callAction(readProp(loading, 'onPress'), 'Missing loading action')\n })\n expect(onSelectionChange).toHaveBeenCalledTimes(1)\n})\n\ntest('native media inspection actions request bounded values and respect disabled state', async () => {\n const onValueChange = vi.fn()\n const props = { label: 'Inspect', value: { zoom: 2, x: 1, y: 0 }, onValueChange }\n const root = await renderNative(<LumenMediaViewport {...props}><LumenText>Photo</LumenText></LumenMediaViewport>)\n const right = root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'button')\n expect(right).toHaveLength(7)\n const fit = right[2]\n if (!fit) throw new Error('Missing fit action')\n await runNativeAction(() => {\n callAction(readProp(fit, 'onPress'), 'Missing fit handler')\n })\n expect(onValueChange).toHaveBeenCalledWith({ zoom: 1, x: 0, y: 0 })\n await runNativeAction(() => {\n root.render(\n <LumenProvider>\n <LumenMediaViewport {...props} disabled><LumenText>Photo</LumenText></LumenMediaViewport>\n </LumenProvider>\n )\n })\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'button').every(instance => {\n const state = readProp(instance, 'accessibilityState')\n return typeof state === 'object' && state !== null && 'disabled' in state && state.disabled === true\n })).toBe(true)\n})\n\ntest.each([true, false])('media pinch-to-pan preserves the host-accepted zoom (accept=%s)', async accept => {\n const change = vi.fn<(value: LumenMediaViewportValue) => void>()\n const Harness = () => {\n const [value, setValue] = useState<LumenMediaViewportValue>({ zoom: 2, x: 0, y: 0 })\n\n return (\n <LumenMediaViewport\n label=\"Inspect gesture\"\n value={value}\n onValueChange={next => {\n change(next)\n if (accept) setValue(next)\n }}\n >\n <LumenText>Photo</LumenText>\n </LumenMediaViewport>\n )\n }\n const root = await renderNative(<Harness />)\n const dispatch = async (property: string, nativeEvent: unknown): Promise<void> => {\n const handler = readProp(findByAccessibilityLabel(root, 'Inspect gesture'), property)\n if (typeof handler !== 'function') throw new Error(`Missing media ${property} callback`)\n await runNativeAction(() => {\n Reflect.apply(handler, undefined, [{ nativeEvent }])\n })\n }\n const touch = (pageX: number, pageY: number) => ({ pageX, pageY })\n\n await dispatch('onLayout', { layout: { width: 200, height: 100 } })\n await dispatch('onResponderGrant', { pageX: 50, pageY: 50, touches: [touch(50, 50), touch(100, 50)] })\n await dispatch('onResponderMove', { pageX: 50, pageY: 50, touches: [touch(50, 50), touch(150, 50)] })\n expect(change).toHaveBeenLastCalledWith({ zoom: 4, x: 0, y: 0 })\n change.mockClear()\n await dispatch('onResponderMove', { pageX: 150, pageY: 50, touches: [touch(150, 50)] })\n expect(change).not.toHaveBeenCalled()\n await dispatch('onResponderMove', { pageX: 175, pageY: 50, touches: [touch(175, 50)] })\n expect(change).toHaveBeenLastCalledWith({ zoom: accept ? 4 : 2, x: accept ? 1 / 12 : 0.25, y: 0 })\n})\n",
|
|
60416
|
+
"packages/react-native/src/component-behavior.test.tsx": "// cspell:words Espacios Configuración Cambios guardados Registro actualizado Otro\nimport { act, type ComponentRef, createRef, type ReactElement, type Ref, StrictMode, useImperativeHandle, useState } from 'react'\nimport type { View } from 'react-native'\n\nimport { DateTimePickerAndroid } from '@react-native-community/datetimepicker'\nimport { getLumenPhoneCountry } from '@santi020k/lumen-core'\nimport type { LumenMediaViewportValue } from '@santi020k/lumen-core/media-workspace'\nimport { createRoot, type Root, type TestInstance } from 'test-renderer'\nimport { afterEach, describe, expect, test, vi } from 'vitest'\n\nimport { LumenBrandGithubIconGraphic } from './static-icons/brand-github.generated.js'\nimport { LumenSearchIconGraphic } from './static-icons/search.generated.js'\nimport { LumenButtonGroup, LumenChip, LumenFieldGroup, LumenTextarea, LumenToast } from './additional-components.js'\nimport { LumenAutocomplete, LumenInputOTP, LumenNumberField, LumenPasswordField } from './advanced-form-components.js'\nimport { LumenBreadcrumb } from './breadcrumb-components.js'\nimport { LumenBarChart, LumenComboChart, LumenHeatmap, LumenHistogram, LumenLineChart, LumenRangeChart, LumenScatterChart, LumenWaterfallChart } from './chart-components.js'\nimport { LumenImageComparison } from './comparison-components.js'\nimport { LumenDateField, LumenDateRangeField } from './datetime-components.js'\nimport { LumenSearchField, LumenToggle } from './form-components.js'\nimport { LumenIcon as GraphicIcon, LumenIconButton as GraphicIconButton, type LumenIconGraphicProps } from './graphics.js'\nimport { type LumenToastHookController, type LumenToastHookOptions, useToast } from './hooks.js'\nimport { LumenMediaThumbnail, LumenMediaViewport } from './media-workspace-components.js'\nimport { LumenMultiSelect } from './multi-select-components.js'\nimport { LumenAlertDialog, LumenMenu, LumenSheet } from './overlay-components.js'\nimport { LumenPhoneInput } from './phone-components.js'\nimport { resolveLumenPhoneInputValue } from './phone-recipes.js'\nimport { LumenNavigationBar } from './platform-components.js'\nimport { LumenButton, LumenIcon, LumenIconButton, LumenText, LumenTextField } from './primitives.js'\nimport { LumenProvider } from './provider.js'\nimport { LumenRating } from './rating-components.js'\nimport { resolveLumenRating } from './rating-recipes.js'\nimport { LumenCheckbox, LumenTabs } from './selection-components.js'\nimport { LumenStepper } from './stepper-components.js'\nimport { resolveLumenStepState } from './stepper-recipes.js'\nimport { LumenBanner, LumenErrorState, LumenStatusBar } from './structured-components.js'\nimport { LumenDataTable } from './table-components.js'\nimport type { LumenTableRow } from './table-recipes.js'\nimport { createLumenTheme } from './theme.js'\nimport { LumenTimeField, type LumenTimeFieldProps } from './time-components.js'\nimport { LumenTimeline, LumenTimelineItem } from './timeline-components.js'\nimport { LumenPicker, LumenRangeSlider, LumenSlider } from './value-components.js'\n\nObject.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })\n\ntest.each([\n { labelledBy: 'consumer-label' },\n { labelledBy: ['consumer-label', 'consumer-context'] },\n { labelledBy: '' },\n { labelledBy: [] }\n])('grouped text fields retain explicit native labelled-by ownership: $labelledBy', async ({ labelledBy }) => {\n const root = await renderNative(\n <LumenFieldGroup label=\"Inherited label\">\n <LumenTextField accessibilityLabelledBy={labelledBy} />\n </LumenFieldGroup>\n )\n const input = root.container.queryAll(instance => instance.type === 'TextInput')[0]\n if (!input) throw new Error('Expected grouped text field')\n expect(readProp(input, 'accessibilityLabelledBy')).toEqual(labelledBy)\n expect(readProp(input, 'aria-labelledby')).toBeUndefined()\n expect(readProp(input, 'accessibilityLabel')).toBeUndefined()\n})\n\ntest('grouped text fields preserve explicit ARIA and native label references together', async () => {\n const root = await renderNative(\n <LumenFieldGroup label=\"Inherited label\">\n <LumenTextField accessibilityLabelledBy=\"native-label\" aria-labelledby=\"aria-label\" />\n </LumenFieldGroup>\n )\n const input = root.container.queryAll(instance => instance.type === 'TextInput')[0]\n if (!input) throw new Error('Expected grouped text field')\n expect(readProp(input, 'accessibilityLabelledBy')).toBe('native-label')\n expect(readProp(input, 'aria-labelledby')).toBe('aria-label')\n expect(readProp(input, 'accessibilityLabel')).toBeUndefined()\n})\n\nconst nativeDirection = vi.hoisted(() => ({ isRTL: false }))\nconst nativePlatform = vi.hoisted(() => ({ OS: 'ios' }))\nconst nativeMotion = vi.hoisted(() => ({ enabled: false }))\nconst accessibilityFocus = vi.hoisted(() => vi.fn())\nconst accessibilityAnnouncement = vi.hoisted(() => vi.fn<(message: string, options: { queue: boolean }) => void>())\ntype RecommendedTimeout = (duration: number) => Promise<number>\nconst accessibilityTimeout = vi.hoisted(() => vi.fn<RecommendedTimeout>(duration => Promise.resolve(duration)))\nconst nativeWindow = vi.hoisted(() => ({ fontScale: 1, height: 800, scale: 2, width: 400 }))\nconst safeAreaInsets = vi.hoisted(() => ({\n bottom: 34,\n left: 8,\n right: 12,\n top: 20\n}))\n\nvi.mock('react-native', async () => {\n const { createElement, isValidElement, useImperativeHandle } = await import('react')\n const hostComponent = (name: string) => (\n props: Record<string, unknown>\n ): ReactElement => createElement(name, props)\n type FocusablePressableProps = Record<string, unknown> & {\n ref?: Ref<{ focus: () => void }>\n }\n const FocusablePressable = ({ ref, ...props }: FocusablePressableProps): ReactElement => {\n useImperativeHandle(ref, () => ({ focus: vi.fn() }))\n\n return createElement('Pressable', props)\n }\n\n return {\n AccessibilityInfo: {\n addEventListener: () => ({ remove: vi.fn() }),\n announceForAccessibilityWithOptions: accessibilityAnnouncement,\n getRecommendedTimeoutMillis: accessibilityTimeout,\n isReduceMotionEnabled: () => Promise.resolve(nativeMotion.enabled),\n sendAccessibilityEvent: accessibilityFocus\n },\n ActivityIndicator: hostComponent('ActivityIndicator'),\n FlatList: (props: Record<string, unknown>): ReactElement => {\n const renderItem = props.renderItem\n const data: readonly unknown[] = Array.isArray(props.data) ? props.data : []\n const children = data.map((item, index) => {\n if (typeof renderItem !== 'function') return null\n\n const rendered: unknown = Reflect.apply(renderItem, undefined, [{ item, index }])\n\n return isValidElement(rendered) ? rendered : null\n })\n\n return createElement('FlatList', props, children)\n },\n I18nManager: nativeDirection,\n Image: hostComponent('Image'),\n KeyboardAvoidingView: hostComponent('KeyboardAvoidingView'),\n Modal: hostComponent('Modal'),\n Platform: nativePlatform,\n Pressable: FocusablePressable,\n ScrollView: hostComponent('ScrollView'),\n Share: { share: () => Promise.resolve({ action: 'sharedAction' }) },\n Switch: hostComponent('Switch'),\n Text: hostComponent('Text'),\n TextInput: hostComponent('TextInput'),\n useColorScheme: () => 'light',\n useWindowDimensions: () => nativeWindow,\n View: hostComponent('View')\n }\n})\n\nvi.mock('@react-native-community/datetimepicker', async () => {\n const { createElement } = await import('react')\n const NativeDatePicker = (props: Record<string, unknown>): ReactElement => (\n createElement('NativeDatePicker', props)\n )\n\n return {\n DateTimePickerAndroid: { dismiss: vi.fn(), open: vi.fn() },\n default: NativeDatePicker\n }\n})\n\nvi.mock('react-native-svg', async () => {\n const { createElement } = await import('react')\n const hostComponent = (name: string) => (\n props: Record<string, unknown>\n ): ReactElement => createElement(name, props)\n\n return Object.fromEntries(\n ['Circle', 'Defs', 'Ellipse', 'G', 'Line', 'LinearGradient', 'Path', 'Polygon', 'Polyline', 'Rect', 'Stop', 'Svg', 'Text']\n .map(name => [name, hostComponent(name)])\n )\n})\n\nconst mountedRoots: Root[] = []\n\ninterface ToastProbeProps {\n ref: Ref<LumenToastHookController>\n options?: LumenToastHookOptions\n}\n\nconst ToastProbe = ({ ref, options }: ToastProbeProps): ReactElement | null => {\n const controller = useToast(options)\n\n useImperativeHandle(ref, () => controller, [controller])\n\n return null\n}\n\nconst readToastController = (ref: { current: LumenToastHookController | null }): LumenToastHookController => {\n if (!ref.current) throw new Error('Toast controller is not mounted')\n\n return ref.current\n}\n\nconst deferredTimeout = () => {\n let complete: (duration: number) => void = () => {\n throw new Error('Missing timeout resolver')\n }\n const promise = new Promise<number>(resolve => {\n complete = resolve\n })\n\n return { promise, resolve: complete }\n}\n\nconst renderNative = async (element: ReactElement): Promise<Root> => {\n const root = createRoot({\n publicTextComponentTypes: ['Text'],\n textComponentTypes: ['RCTText', 'Text']\n })\n\n mountedRoots.push(root)\n\n await act(async () => {\n root.render(<LumenProvider scheme=\"light\">{element}</LumenProvider>)\n await Promise.resolve()\n })\n\n return root\n}\n\nconst readProp = (instance: TestInstance, property: string): unknown => {\n const props = instance.props as Record<string, unknown>\n\n return props[property]\n}\n\nconst callAction = (value: unknown, missingActionMessage: string): void => {\n if (typeof value !== 'function') throw new Error(missingActionMessage)\n\n Reflect.apply(value, undefined, [])\n}\n\nconst callKeyboardAction = (value: unknown, key: string): ReturnType<typeof vi.fn> => {\n if (typeof value !== 'function') throw new Error('Tab is missing its keyboard action.')\n\n const preventDefault = vi.fn()\n\n Reflect.apply(value, undefined, [{\n nativeEvent: { code: key, key },\n preventDefault\n }])\n\n return preventDefault\n}\n\nconst findByAccessibilityRole = (root: Root, role: string): TestInstance => {\n const matches = root.container.queryAll(\n instance => readProp(instance, 'accessibilityRole') === role\n )\n\n expect(matches).toHaveLength(1)\n\n const match = matches[0]\n\n if (!match) throw new Error(`Expected one ${role} accessibility node.`)\n\n return match\n}\n\nconst findByAccessibilityLabel = (root: Root, label: string): TestInstance => {\n const matches = root.container.queryAll(\n instance => readProp(instance, 'accessibilityLabel') === label\n )\n\n expect(matches).toHaveLength(1)\n\n const match = matches[0]\n\n if (!match) throw new Error(`Expected one accessibility node labeled ${label}.`)\n\n return match\n}\n\nconst findHostComponent = (root: Root, type: string): TestInstance => {\n const matches = root.container.queryAll(instance => instance.type === type)\n\n expect(matches).toHaveLength(1)\n\n const match = matches[0]\n\n if (!match) throw new Error(`Expected one ${type} host component.`)\n\n return match\n}\n\nafterEach(async () => {\n nativeDirection.isRTL = false\n const roots = mountedRoots.splice(0)\n\n await act(async () => {\n for (const root of roots) root.unmount()\n await Promise.resolve()\n })\n nativeWindow.fontScale = 1\n nativePlatform.OS = 'ios'\n accessibilityAnnouncement.mockClear()\n accessibilityTimeout.mockReset().mockImplementation(duration => Promise.resolve(duration))\n vi.useRealTimers()\n vi.unstubAllGlobals()\n})\n\ndescribe('Lumen React Native component behavior', () => {\n test.each(['bar', 'line', 'scatter', 'range', 'combo'] as const)(\n '%s chart fits its container and resizes without removing data', async kind => {\n const data = [3, 7, 4, 8, 5].map((y, x) => ({ x, y }))\n const series = [{ id: 'activity', label: 'Changes', data }]\n const props = { label: 'Weekly activity', showData: false, series }\n const charts = {\n bar: <LumenBarChart {...props} />,\n line: <LumenLineChart {...props} />,\n scatter: <LumenScatterChart {...props} />,\n range: <LumenRangeChart label=\"Weekly range\" showData={false} data={data.map(({ x, y }) => ({ x, low: y, high: y + 2 }))} />,\n combo: <LumenComboChart {...props} series={[{ id: 'activity', label: 'Changes', data, mark: 'bar' }]} />\n }\n const root = await renderNative(charts[kind])\n const layouts = root.container.queryAll(instance => instance.type === 'View' && typeof readProp(instance, 'onLayout') === 'function')\n expect(layouts).toHaveLength(1)\n const layout = layouts[0]\n if (!layout) throw new Error('Missing chart container')\n const handler = readProp(layout, 'onLayout')\n if (typeof handler !== 'function') throw new Error('Missing chart layout handler')\n\n for (const width of [268, 720, 300, 0, Number.NaN]) {\n await act(async () => {\n Reflect.apply(handler, undefined, [{ nativeEvent: { layout: { width } } }])\n await Promise.resolve()\n })\n const expectedWidth = width > 0 && Number.isFinite(width) ? width : 300\n const svg = findHostComponent(root, 'Svg')\n expect(readProp(svg, 'width')).toBe(expectedWidth)\n expect(readProp(svg, 'height')).toBe(Math.min(320, Math.max(220, expectedWidth * 0.75)))\n expect(root.container.queryAll(instance => instance.type === 'ScrollView')).toHaveLength(0)\n const bars = root.container.queryAll(instance => instance.type === 'Rect')\n expect(bars).toHaveLength(['bar', 'combo'].includes(kind) ? 5 : 0)\n for (const bar of bars) {\n const coordinates = /translate\\(([-\\d.]+) ([-\\d.]+)\\)/u.exec(String(readProp(bar, 'transform')))\n if (!coordinates?.[1]) throw new Error('Missing bar coordinates')\n expect(Number(coordinates[1])).toBeGreaterThanOrEqual(0)\n expect(Number(coordinates[1]) + Number(readProp(bar, 'width'))).toBeLessThanOrEqual(expectedWidth)\n }\n }\n }\n )\n\n test.each([\n { rtl: false, values: [20, 50, 120, 20, 120] },\n { rtl: true, values: [120, 100, 20, 120, 20] }\n ])('slider touch follows RTL=$rtl while accessibility values remain numeric', async ({ rtl, values }) => {\n nativeDirection.isRTL = rtl\n const onValueChange = vi.fn<(value: number) => void>()\n const root = await renderNative(\n <LumenSlider label=\"Volume\" min={20} max={120} step={10} value={70} onValueChange={onValueChange} />\n )\n const slider = findByAccessibilityRole(root, 'adjustable')\n expect(readProp(slider, 'accessible')).toBe(true)\n const dispatch = async (property: string, nativeEvent: unknown): Promise<void> => {\n const handler = readProp(slider, property)\n if (typeof handler !== 'function') throw new Error(`Missing slider ${property} callback`)\n await act(async () => {\n Reflect.apply(handler, undefined, [{ nativeEvent }])\n await Promise.resolve()\n })\n }\n await dispatch('onLayout', { layout: { width: 200 } })\n for (const [index, position] of [0, 50, 200, -50, 250].entries()) {\n for (const event of ['onResponderGrant', 'onResponderMove']) {\n await dispatch(event, { locationX: position })\n expect(onValueChange).toHaveBeenLastCalledWith(values[index])\n }\n }\n await dispatch('onAccessibilityAction', { actionName: 'increment' })\n expect(onValueChange).toHaveBeenLastCalledWith(80)\n await dispatch('onAccessibilityAction', { actionName: 'decrement' })\n expect(onValueChange).toHaveBeenLastCalledWith(60)\n expect(readProp(slider, 'accessibilityValue')).toMatchObject({ min: 20, max: 120, now: 70 })\n })\n\n test('range endpoints stay controlled, independently named and cannot cross', async () => {\n const onValueChange = vi.fn<(value: readonly [number, number]) => void>()\n const root = await renderNative(\n <LumenRangeSlider\n label=\"Capacity\"\n startLabel=\"Lower\"\n endLabel=\"Upper\"\n value={[20, 60]}\n min={0}\n max={100}\n step={10}\n onValueChange={onValueChange}\n formatValue={value => `${value}%`}\n />\n )\n const sliders = root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'adjustable')\n expect(sliders).toHaveLength(2)\n const [start, end] = sliders\n if (!start || !end) throw new Error('Missing interval endpoints')\n expect(readProp(start, 'accessibilityLabel')).toBe('Capacity · Lower')\n expect(readProp(end, 'accessibilityLabel')).toBe('Capacity · Upper')\n expect(readProp(start, 'accessibilityValue')).toMatchObject({ now: 20, text: '20%' })\n expect(readProp(start, 'aria-valuenow')).toBe(20)\n expect(readProp(start, 'aria-valuetext')).toBe('20%')\n const move = async (slider: TestInstance, direction: string): Promise<void> => {\n const handler = readProp(slider, 'onAccessibilityAction')\n if (typeof handler !== 'function') throw new Error('Missing range adjustment')\n await act(async () => {\n Reflect.apply(handler, undefined, [{ nativeEvent: { actionName: direction } }])\n await Promise.resolve()\n })\n }\n await move(start, 'increment')\n expect(onValueChange).toHaveBeenLastCalledWith([30, 60])\n await move(end, 'decrement')\n expect(onValueChange).toHaveBeenLastCalledWith([20, 50])\n // The application has not accepted either update; subsequent intent uses its controlled value.\n await move(start, 'increment')\n expect(onValueChange).toHaveBeenLastCalledWith([30, 60])\n await act(async () => {\n root.render(\n <LumenProvider>\n <LumenRangeSlider\n label=\"Capacity\"\n value={[60, 60]}\n min={0}\n max={100}\n step={10}\n onValueChange={onValueChange}\n />\n </LumenProvider>\n )\n await Promise.resolve()\n })\n const next = root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'adjustable')\n if (!next[0] || !next[1]) throw new Error('Missing updated interval endpoints')\n await move(next[0], 'increment')\n expect(onValueChange).toHaveBeenLastCalledWith([60, 60])\n await move(next[1], 'decrement')\n expect(onValueChange).toHaveBeenLastCalledWith([60, 60])\n })\n\n test.each([{ enabled: false, readOnly: false }, { enabled: true, readOnly: true }])(\n 'range endpoints reject touch and accessibility when enabled=$enabled readOnly=$readOnly',\n async ({ enabled, readOnly }) => {\n const onValueChange = vi.fn<(value: readonly [number, number]) => void>()\n const root = await renderNative(\n <LumenRangeSlider\n label=\"Capacity\"\n value={[20, 80]}\n enabled={enabled}\n readOnly={readOnly}\n onValueChange={onValueChange}\n />\n )\n const sliders = root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'adjustable')\n for (const slider of sliders) {\n expect(readProp(slider, 'accessibilityState')).toEqual({ disabled: true })\n for (const [event, nativeEvent] of [\n ['onAccessibilityAction', { actionName: 'increment' }],\n ['onResponderGrant', { locationX: 100 }],\n ['onResponderMove', { locationX: 100 }]\n ] as const) {\n const handler = readProp(slider, event)\n if (typeof handler !== 'function') throw new Error(`Missing ${event}`)\n await act(async () => {\n Reflect.apply(handler, undefined, [{ nativeEvent }])\n await Promise.resolve()\n })\n }\n }\n expect(onValueChange).not.toHaveBeenCalled()\n }\n )\n\n test('uses the Android accessibility timeout before dismissing a toast', async () => {\n nativePlatform.OS = 'android'\n vi.useFakeTimers()\n accessibilityTimeout.mockResolvedValue(20_000)\n const ref = createRef<LumenToastHookController>()\n\n await renderNative(<ToastProbe ref={ref} />)\n await act(async () => {\n readToastController(ref).create({ title: 'Saved' })\n await Promise.resolve()\n })\n\n expect(accessibilityTimeout).toHaveBeenCalledExactlyOnceWith(5000)\n\n await act(async () => {\n await vi.advanceTimersByTimeAsync(19_999)\n })\n expect(readToastController(ref).toasts).toHaveLength(1)\n\n await act(async () => {\n await vi.advanceTimersByTimeAsync(1)\n })\n expect(readToastController(ref).toasts).toHaveLength(0)\n })\n\n test.each(['shorter', 'invalid', 'oversized', 'rejected'])('retains the requested duration when Android returns a %s recommendation', async result => {\n nativePlatform.OS = 'android'\n vi.useFakeTimers()\n\n if (result === 'rejected') accessibilityTimeout.mockRejectedValue(new Error('Native timeout unavailable'))\n else if (result === 'shorter') accessibilityTimeout.mockResolvedValue(1)\n else if (result === 'oversized') accessibilityTimeout.mockResolvedValue(Number.MAX_SAFE_INTEGER)\n else accessibilityTimeout.mockResolvedValue(Number.NaN)\n\n const ref = createRef<LumenToastHookController>()\n\n await renderNative(<ToastProbe ref={ref} />)\n await act(async () => {\n readToastController(ref).create({ duration: 1000, title: 'Saved' })\n await Promise.resolve()\n })\n expect(accessibilityTimeout).toHaveBeenCalledExactlyOnceWith(1000)\n\n await act(async () => {\n await vi.advanceTimersByTimeAsync(999)\n })\n expect(readToastController(ref).toasts).toHaveLength(1)\n\n await act(async () => {\n await vi.advanceTimersByTimeAsync(1)\n })\n expect(readToastController(ref).toasts).toHaveLength(0)\n })\n\n test.each(['dismiss', 'clear', 'unmount', 'evict'])('ignores late Android timeout results after %s', async operation => {\n nativePlatform.OS = 'android'\n vi.useFakeTimers()\n const pending = deferredTimeout()\n\n accessibilityTimeout.mockReturnValueOnce(pending.promise)\n\n const ref = createRef<LumenToastHookController>()\n const root = await renderNative(<ToastProbe options={{ maxCount: 1 }} ref={ref} />)\n let id = ''\n\n await act(async () => {\n id = readToastController(ref).create({ title: 'Saved' })\n await Promise.resolve()\n })\n expect(accessibilityTimeout).toHaveBeenCalledExactlyOnceWith(5000)\n\n await act(async () => {\n if (operation === 'dismiss') readToastController(ref).dismiss(id)\n if (operation === 'clear') readToastController(ref).clear()\n if (operation === 'unmount') root.unmount()\n if (operation === 'evict') readToastController(ref).create({ duration: 0, title: 'Persistent replacement' })\n await Promise.resolve()\n })\n await act(async () => {\n pending.resolve(20_000)\n await Promise.resolve()\n })\n\n expect(vi.getTimerCount()).toBe(0)\n })\n\n test('ignores stale Android recommendations when a toast duration is updated', async () => {\n nativePlatform.OS = 'android'\n vi.useFakeTimers()\n const pending = deferredTimeout()\n\n accessibilityTimeout.mockReturnValueOnce(pending.promise).mockResolvedValueOnce(10_000)\n\n const ref = createRef<LumenToastHookController>()\n\n await renderNative(<ToastProbe ref={ref} />)\n await act(async () => {\n const id = readToastController(ref).create({ title: 'Saved' })\n\n readToastController(ref).update(id, { duration: 1000 })\n await Promise.resolve()\n })\n expect(accessibilityTimeout).toHaveBeenNthCalledWith(1, 5000)\n expect(accessibilityTimeout).toHaveBeenNthCalledWith(2, 1000)\n expect(accessibilityTimeout).toHaveBeenCalledTimes(2)\n\n await act(async () => {\n pending.resolve(100_000)\n await Promise.resolve()\n await vi.advanceTimersByTimeAsync(10_000)\n })\n\n expect(readToastController(ref).toasts).toHaveLength(0)\n expect(vi.getTimerCount()).toBe(0)\n })\n\n test('keeps explicitly persistent Android toasts without requesting a timeout', async () => {\n nativePlatform.OS = 'android'\n vi.useFakeTimers()\n const ref = createRef<LumenToastHookController>()\n\n await renderNative(<ToastProbe ref={ref} />)\n await act(async () => {\n for (const duration of [0, -1, Number.POSITIVE_INFINITY]) readToastController(ref).create({ duration, title: 'Saved' })\n await vi.advanceTimersByTimeAsync(100_000)\n })\n\n expect(readToastController(ref).toasts).toHaveLength(3)\n expect(accessibilityTimeout).not.toHaveBeenCalled()\n expect(vi.getTimerCount()).toBe(0)\n })\n\n test.each(['ios', 'web'])('preserves the requested %s toast duration without Android timeout calls', async platform => {\n nativePlatform.OS = platform\n vi.useFakeTimers()\n const ref = createRef<LumenToastHookController>()\n\n await renderNative(<ToastProbe ref={ref} />)\n await act(async () => {\n readToastController(ref).create({ duration: 1000, title: 'Saved' })\n await vi.advanceTimersByTimeAsync(1000)\n })\n\n expect(readToastController(ref).toasts).toHaveLength(0)\n expect(accessibilityTimeout).not.toHaveBeenCalled()\n })\n\n test('keeps enlarged navigation labels readable and preserves destination semantics', async () => {\n const select = vi.fn()\n const reselect = vi.fn()\n const items = [\n { label: 'Espacios de trabajo', value: 'workspaces' },\n { label: 'Configuración', value: 'settings' },\n { disabled: true, label: 'Archivo', value: 'archive' }\n ]\n const navigation = <LumenNavigationBar items={items} onReselect={reselect} onValueChange={select} value=\"workspaces\" />\n const root = await renderNative(navigation)\n const labels = () => root.container.queryAll(instance => instance.type === 'Text')\n expect(labels().map(label => readProp(label, 'numberOfLines'))).toEqual([1, 1, 1])\n\n nativeWindow.fontScale = 3\n await act(async () => {\n root.render(<LumenProvider scheme=\"light\"><LumenNavigationBar items={items} onReselect={reselect} onValueChange={select} value=\"workspaces\" /></LumenProvider>)\n await Promise.resolve()\n })\n expect(labels().every(label => readProp(label, 'numberOfLines') === undefined)).toBe(true)\n expect(labels().every(label => {\n const style = readProp(label, 'style')\n return typeof style === 'object' && style !== null && 'maxWidth' in style && style.maxWidth === '100%'\n })).toBe(true)\n const selected = findByAccessibilityLabel(root, 'Espacios de trabajo')\n expect(readProp(selected, 'accessibilityState')).toMatchObject({ selected: true })\n callAction(readProp(selected, 'onPress'), 'Missing reselect action')\n expect(reselect).toHaveBeenCalledWith('workspaces')\n const settings = findByAccessibilityLabel(root, 'Configuración')\n callAction(readProp(settings, 'onPress'), 'Missing destination action')\n expect(select).toHaveBeenCalledWith('settings')\n expect(readProp(findByAccessibilityLabel(root, 'Archivo'), 'disabled')).toBe(true)\n })\n\n test('keeps alert actions inside safe areas and makes oversized content scrollable', async () => {\n const root = await renderNative(\n <LumenAlertDialog\n confirmLabel=\"Delete\"\n onConfirm={() => {}}\n onDismiss={() => {}}\n safeAreaInsets={safeAreaInsets}\n title=\"Delete project?\"\n visible\n />\n )\n const modalContainer = root.container.queryAll(\n instance => readProp(instance, 'accessibilityViewIsModal') === true\n )[0]\n\n if (!modalContainer) throw new Error('Expected the alert modal container.')\n\n expect(readProp(modalContainer, 'style')).toMatchObject({\n paddingBottom: 58,\n paddingLeft: 32,\n paddingRight: 36,\n paddingTop: 44\n })\n expect(readProp(findByAccessibilityRole(root, 'alert'), 'style')).toMatchObject({\n maxHeight: '100%'\n })\n })\n\n test('keeps sheet actions above the bottom inset and scrolls application content', async () => {\n const root = await renderNative(\n <LumenSheet\n actions={<LumenButton>Save</LumenButton>}\n onDismiss={() => {}}\n safeAreaInsets={safeAreaInsets}\n scrollable\n title=\"Settings\"\n visible\n >\n <LumenText>Sheet content</LumenText>\n </LumenSheet>\n )\n const keyboardSurface = root.container.queryAll(\n instance => instance.type === 'KeyboardAvoidingView'\n )[0]\n const sheetPanel = root.container.queryAll(instance => {\n const style = readProp(instance, 'style')\n\n return typeof style === 'object' &&\n style !== null &&\n 'paddingBottom' in style &&\n style.paddingBottom === 34\n })[0]\n\n if (!keyboardSurface || !sheetPanel) throw new Error('Expected the sheet surfaces.')\n\n expect(readProp(keyboardSurface, 'style')).toMatchObject({\n paddingBottom: 0,\n paddingLeft: 8,\n paddingRight: 12,\n paddingTop: 20\n })\n expect(readProp(sheetPanel, 'style')).toMatchObject({ paddingBottom: 34 })\n expect(root.container.queryAll(instance => instance.type === 'ScrollView')).toHaveLength(1)\n })\n\n test('lets virtualized sheet content own scrolling', async () => {\n const root = await renderNative(\n <LumenSheet onDismiss={() => {}} scrollable={false} visible>\n <LumenText>Virtualized content</LumenText>\n </LumenSheet>\n )\n\n expect(root.container.queryAll(instance => instance.type === 'ScrollView')).toHaveLength(0)\n })\n\n test('button exposes loading as busy and disabled native state', async () => {\n const root = await renderNative(<LumenButton loading>Save changes</LumenButton>)\n const button = findByAccessibilityRole(root, 'button')\n\n expect(readProp(button, 'disabled')).toBe(true)\n expect(readProp(button, 'accessibilityState')).toEqual({\n busy: true,\n disabled: true\n })\n })\n\n test('button preserves its native role when a consumer supplies another role', async () => {\n const root = await renderNative(\n <LumenButton accessibilityRole=\"link\">Open workspace</LumenButton>\n )\n\n expect(findByAccessibilityRole(root, 'button')).toBeDefined()\n expect(root.container.queryAll(\n instance => readProp(instance, 'accessibilityRole') === 'link'\n )).toHaveLength(0)\n })\n\n test('text inputs expose disabled native state without dropping caller state', async () => {\n const textFieldRoot = await renderNative(\n <LumenTextField\n accessibilityLabel=\"Project name\"\n accessibilityState={{ busy: true }}\n editable={false}\n />\n )\n const textareaRoot = await renderNative(\n <LumenTextarea\n accessibilityState={{ busy: true }}\n editable={false}\n label=\"Project notes\"\n onChangeText={() => {}}\n value=\"\"\n />\n )\n const searchRoot = await renderNative(\n <LumenSearchField\n accessibilityState={{ busy: true }}\n editable={false}\n onChangeText={() => {}}\n prompt=\"Search projects\"\n value=\"\"\n />\n )\n\n for (const [root, label] of [\n [textFieldRoot, 'Project name'],\n [textareaRoot, 'Project notes'],\n [searchRoot, 'Search projects']\n ] as const) {\n expect(readProp(findByAccessibilityLabel(root, label), 'accessibilityState')).toEqual({\n busy: true,\n disabled: true\n })\n }\n })\n\n test('validation is exposed on the native controls without dropping caller context', async () => {\n const textFieldRoot = await renderNative(\n <LumenTextField accessibilityLabel=\"Project name\" error />\n )\n const textareaRoot = await renderNative(\n <LumenTextarea\n accessibilityHint=\"Consumer guidance\"\n errorMessage=\"Project notes are required\"\n label=\"Project notes\"\n onChangeText={() => {}}\n value=\"\"\n />\n )\n const dateRoot = await renderNative(\n <LumenDateField\n errorMessage=\"Choose a valid birthday\"\n label=\"Birthday\"\n onValueChange={() => {}}\n value={null}\n />\n )\n\n const textField = findByAccessibilityLabel(textFieldRoot, 'Project name')\n const textarea = findByAccessibilityLabel(textareaRoot, 'Project notes')\n const dateButton = findByAccessibilityLabel(dateRoot, 'Birthday, Select a date')\n\n expect(readProp(textField, 'aria-invalid')).toBe(true)\n expect(readProp(textarea, 'aria-invalid')).toBe(true)\n expect(readProp(textarea, 'accessibilityHint')).toBe('Project notes are required')\n expect(readProp(dateButton, 'aria-invalid')).toBe(true)\n expect(readProp(dateButton, 'accessibilityHint')).toBe('Choose a valid birthday')\n })\n\n test('grouped fields expose required and range validation context', async () => {\n const fieldGroupRoot = await renderNative(\n <LumenFieldGroup label=\"Project details\" required>\n <LumenText>Details</LumenText>\n </LumenFieldGroup>\n )\n const dateRangeRoot = await renderNative(\n <LumenDateRangeField\n accessibilityHint=\"Consumer guidance\"\n errorMessage=\"Choose a valid schedule\"\n label=\"Schedule\"\n onValueChange={() => {}}\n value={{ end: null, start: null }}\n />\n )\n\n expect(findByAccessibilityLabel(fieldGroupRoot, 'Project details, required')).toBeDefined()\n\n const dateRange = findByAccessibilityLabel(dateRangeRoot, 'Schedule')\n\n expect(readProp(dateRange, 'aria-invalid')).toBe(true)\n expect(readProp(dateRange, 'accessibilityHint')).toBe('Choose a valid schedule')\n })\n\n test('pickers inside field groups announce the group error and description', async () => {\n const root = await renderNative(\n <>\n <LumenFieldGroup description=\"Used for billing\" errorMessage=\"Choose a plan\" label=\"Plan\" required>\n <LumenPicker label=\"Plan\" onValueChange={() => {}} options={[{ label: 'Free', value: 'free' }]} value=\"free\" />\n </LumenFieldGroup>\n <LumenFieldGroup description=\"Shown on invoices\" label=\"Currency\">\n <LumenPicker label=\"Currency\" onValueChange={() => {}} options={[{ label: 'USD', value: 'usd' }]} value=\"usd\" />\n </LumenFieldGroup>\n <LumenPicker label=\"Region\" onValueChange={() => {}} options={[{ label: 'EU', value: 'eu' }]} value=\"eu\" />\n </>\n )\n const [invalidPicker, describedPicker, standalonePicker] = root.container.queryAll(\n instance => readProp(instance, 'accessibilityRole') === 'combobox'\n )\n\n expect(invalidPicker && readProp(invalidPicker, 'accessibilityHint')).toBe('Choose a plan')\n expect(invalidPicker && readProp(invalidPicker, 'aria-invalid')).toBe(true)\n expect(describedPicker && readProp(describedPicker, 'accessibilityHint')).toBe('Shown on invoices')\n expect(describedPicker && readProp(describedPicker, 'aria-invalid')).toBeUndefined()\n expect(standalonePicker && readProp(standalonePicker, 'accessibilityHint')).toBeUndefined()\n expect(standalonePicker && readProp(standalonePicker, 'aria-invalid')).toBeUndefined()\n })\n\n test('localizes required-field descriptions without exposing them on optional fields', async () => {\n const root = await renderNative(\n <>\n <LumenFieldGroup label=\"Nombre\" required requiredLabel=\"obligatorio\"><LumenTextField /></LumenFieldGroup>\n <LumenFieldGroup label=\"Notas\" requiredLabel=\"obligatorio\"><LumenTextField /></LumenFieldGroup>\n </>\n )\n\n expect(findByAccessibilityLabel(root, 'Nombre, obligatorio')).toBeDefined()\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityLabel') === 'Notas, obligatorio')).toHaveLength(0)\n expect(findByAccessibilityLabel(root, 'Notas')).toBeDefined()\n })\n\n test('field groups provide inherited native and web relationships without overriding inputs', async () => {\n const root = await renderNative(\n <LumenFieldGroup\n description=\"Shown on the public profile\"\n errorMessage=\"Choose a unique name\"\n label=\"Project name\"\n required\n >\n <LumenText>Nested row content</LumenText>\n <LumenTextField />\n <LumenTextField accessibilityLabel=\"Short project name\" />\n </LumenFieldGroup>\n )\n const inputs = root.container.queryAll(instance => instance.type === 'TextInput')\n const inherited = inputs[0]\n const explicit = inputs[1]\n\n if (!inherited || !explicit) throw new Error('Expected two grouped text fields.')\n\n expect(readProp(inherited, 'accessibilityLabel')).toBe('Project name')\n expect(readProp(inherited, 'aria-labelledby')).toMatch(/-label$/u)\n expect(readProp(inherited, 'aria-describedby')).toMatch(/-description .*?-error$/u)\n expect(readProp(inherited, 'aria-required')).toBe(true)\n expect(readProp(inherited, 'aria-invalid')).toBe(true)\n expect(readProp(explicit, 'accessibilityLabel')).toBe('Short project name')\n expect(readProp(explicit, 'aria-labelledby')).toBeUndefined()\n\n const validationMessages = root.container.queryAll(\n instance => readProp(instance, 'children') === 'Choose a unique name'\n )\n\n expect(validationMessages).toHaveLength(1)\n\n const validationMessage = validationMessages[0]\n\n if (!validationMessage) throw new Error('Expected the grouped validation message.')\n\n expect(readProp(validationMessage, 'accessibilityLiveRegion')).toBe('polite')\n expect(readProp(validationMessage, 'accessibilityRole')).toBe('alert')\n })\n\n test('read-only phone input locks country selection and number editing', async () => {\n const country = getLumenPhoneCountry('CO')\n if (!country) throw new Error('Missing phone fixture')\n const root = await renderNative(<LumenPhoneInput label=\"Phone\" onValueChange={() => {}} readOnly value={resolveLumenPhoneInputValue([country], country, '', {})} />)\n expect(readProp(findByAccessibilityLabel(root, 'Country code, Colombia, +57'), 'disabled')).toBe(true)\n expect(readProp(findByAccessibilityLabel(root, 'Phone number'), 'editable')).toBe(false)\n })\n\n test('phone input exposes disabled state on both native controls', async () => {\n const country = getLumenPhoneCountry('US')\n\n expect(country).toBeDefined()\n\n if (!country) return\n\n const value = resolveLumenPhoneInputValue([country], country, '', {})\n const root = await renderNative(\n <LumenPhoneInput\n countries={[country]}\n enabled={false}\n label=\"Phone\"\n onValueChange={() => {}}\n value={value}\n />\n )\n const countrySelector = findByAccessibilityLabel(root, 'Country code, United States, +1')\n const numberInput = findByAccessibilityLabel(root, 'Phone number')\n\n expect(readProp(countrySelector, 'accessibilityState')).toEqual({\n disabled: true,\n expanded: false\n })\n expect(readProp(numberInput, 'accessibilityState')).toEqual({ disabled: true })\n })\n\n test('phone input exposes validation on the number editor', async () => {\n const country = getLumenPhoneCountry('US')\n\n expect(country).toBeDefined()\n\n if (!country) return\n\n const value = resolveLumenPhoneInputValue([country], country, '415', {})\n const root = await renderNative(\n <LumenPhoneInput\n countries={[country]}\n errorMessage=\"Enter a complete phone number\"\n label=\"Phone\"\n onValueChange={() => {}}\n value={value}\n />\n )\n const numberInput = findByAccessibilityLabel(root, 'Phone number')\n\n expect(readProp(numberInput, 'aria-invalid')).toBe(true)\n expect(readProp(numberInput, 'accessibilityHint')).toBe('Enter a complete phone number')\n })\n\n test('phone input exposes an empty country allow-list as a disabled selector', async () => {\n const country = getLumenPhoneCountry('US')\n\n expect(country).toBeDefined()\n\n if (!country) return\n\n const value = resolveLumenPhoneInputValue([country], country, '', {})\n const root = await renderNative(\n <LumenPhoneInput\n countries={[]}\n label=\"Phone\"\n onValueChange={() => {}}\n value={value}\n />\n )\n const countrySelector = findByAccessibilityLabel(root, 'Country code, United States, +1')\n\n expect(readProp(countrySelector, 'disabled')).toBe(true)\n expect(readProp(countrySelector, 'accessibilityState')).toEqual({\n disabled: true,\n expanded: false\n })\n })\n\n test('generic picker disables empty options and does not reopen after disabling', async () => {\n const onValueChange = vi.fn<(value: string) => void>()\n const root = await renderNative(\n <LumenPicker\n label=\"Workspace\"\n onValueChange={onValueChange}\n options={[]}\n value=\"none\"\n />\n )\n let trigger = findByAccessibilityLabel(root, 'Workspace')\n\n expect(readProp(trigger, 'disabled')).toBe(true)\n expect(readProp(trigger, 'accessibilityState')).toEqual({\n disabled: true,\n expanded: false\n })\n\n await act(async () => {\n root.render(\n <LumenProvider scheme=\"light\">\n <LumenPicker\n label=\"Workspace\"\n onValueChange={onValueChange}\n options={[{ label: 'Design', value: 'design' }]}\n value=\"design\"\n />\n </LumenProvider>\n )\n await Promise.resolve()\n })\n\n trigger = findByAccessibilityLabel(root, 'Workspace')\n\n await act(async () => {\n callAction(readProp(trigger, 'onPress'), 'Picker is missing its native press action.')\n await Promise.resolve()\n })\n\n expect(findByAccessibilityRole(root, 'menu')).toBeDefined()\n\n await act(async () => {\n root.render(\n <LumenProvider scheme=\"light\">\n <LumenPicker\n enabled={false}\n label=\"Workspace\"\n onValueChange={onValueChange}\n options={[{ label: 'Design', value: 'design' }]}\n value=\"design\"\n />\n </LumenProvider>\n )\n await Promise.resolve()\n })\n\n expect(root.container.queryAll(\n instance => readProp(instance, 'accessibilityRole') === 'menu'\n )).toHaveLength(0)\n\n await act(async () => {\n root.render(\n <LumenProvider scheme=\"light\">\n <LumenPicker\n label=\"Workspace\"\n onValueChange={onValueChange}\n options={[{ label: 'Design', value: 'design' }]}\n value=\"design\"\n />\n </LumenProvider>\n )\n await Promise.resolve()\n })\n\n expect(root.container.queryAll(\n instance => readProp(instance, 'accessibilityRole') === 'menu'\n )).toHaveLength(0)\n })\n\n test('date picker does not reopen after the field is disabled and re-enabled', async () => {\n const root = await renderNative(\n <LumenDateField label=\"Birthday\" onValueChange={() => {}} value={null} />\n )\n const trigger = findByAccessibilityLabel(root, 'Birthday, Select a date')\n\n await act(async () => {\n callAction(readProp(trigger, 'onPress'), 'Date field is missing its native press action.')\n await Promise.resolve()\n })\n\n expect(findHostComponent(root, 'NativeDatePicker')).toBeDefined()\n\n await act(async () => {\n root.render(\n <LumenProvider scheme=\"light\">\n <LumenDateField enabled={false} label=\"Birthday\" onValueChange={() => {}} value={null} />\n </LumenProvider>\n )\n await Promise.resolve()\n })\n\n expect(root.container.queryAll(instance => instance.type === 'NativeDatePicker')).toHaveLength(0)\n\n await act(async () => {\n root.render(\n <LumenProvider scheme=\"light\">\n <LumenDateField label=\"Birthday\" onValueChange={() => {}} value={null} />\n </LumenProvider>\n )\n await Promise.resolve()\n })\n\n expect(root.container.queryAll(instance => instance.type === 'NativeDatePicker')).toHaveLength(0)\n })\n\n test('Android date fields commit selections within bounds and ignore dismissal', async () => {\n nativePlatform.OS = 'android'\n const onValueChange = vi.fn()\n const minimumDate = new Date(2026, 0, 10)\n const maximumDate = new Date(2026, 0, 20)\n\n try {\n const root = await renderNative(\n <LumenDateField label=\"Birthday\" minimumDate={minimumDate} maximumDate={maximumDate} onValueChange={onValueChange} value={null} />\n )\n\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Birthday, Select a date'), 'onPress'), 'Missing date trigger')\n })\n\n const options = vi.mocked(DateTimePickerAndroid.open).mock.calls.at(-1)?.[0]\n\n if (!options?.onValueChange) throw new Error('Missing Android date selection handler')\n\n expect(options).not.toHaveProperty('onChange')\n\n await runNativeAction(() => {\n options.onDismiss?.()\n })\n expect(onValueChange).not.toHaveBeenCalled()\n\n const change = options.onValueChange\n const event = { nativeEvent: { timestamp: 0, utcOffset: 0 } }\n\n await runNativeAction(() => {\n change(event, new Date(2026, 0, 1))\n })\n expect(onValueChange).toHaveBeenLastCalledWith(minimumDate)\n await runNativeAction(() => {\n change(event, new Date(2026, 0, 30))\n })\n expect(onValueChange).toHaveBeenLastCalledWith(maximumDate)\n await runNativeAction(() => {\n change(event, new Date(2026, 0, 15))\n })\n expect(onValueChange).toHaveBeenLastCalledWith(new Date(2026, 0, 15))\n expect(onValueChange).toHaveBeenCalledTimes(3)\n } finally {\n nativePlatform.OS = 'ios'\n }\n })\n\n test('disabling an Android date field dismisses its imperative native dialog', async () => {\n nativePlatform.OS = 'android'\n\n try {\n const root = await renderNative(\n <LumenDateField label=\"Birthday\" onValueChange={() => {}} value={null} />\n )\n const trigger = findByAccessibilityLabel(root, 'Birthday, Select a date')\n\n await act(async () => {\n callAction(readProp(trigger, 'onPress'), 'Date field is missing its native press action.')\n await Promise.resolve()\n })\n\n expect(DateTimePickerAndroid.open).toHaveBeenCalledOnce()\n\n await act(async () => {\n root.render(\n <LumenProvider scheme=\"light\">\n <LumenDateField enabled={false} label=\"Birthday\" onValueChange={() => {}} value={null} />\n </LumenProvider>\n )\n await Promise.resolve()\n })\n\n expect(DateTimePickerAndroid.dismiss).toHaveBeenCalledWith('date')\n } finally {\n nativePlatform.OS = 'ios'\n }\n })\n\n test('phone picker closes and clears its query when the input becomes disabled', async () => {\n const country = getLumenPhoneCountry('US')\n\n expect(country).toBeDefined()\n\n if (!country) return\n\n const value = resolveLumenPhoneInputValue([country], country, '', {})\n const renderPhone = (enabled: boolean): ReactElement => (\n <LumenProvider scheme=\"light\">\n <LumenPhoneInput\n countries={[country]}\n enabled={enabled}\n label=\"Phone\"\n onValueChange={() => {}}\n value={value}\n />\n </LumenProvider>\n )\n const root = await renderNative(\n <LumenPhoneInput\n countries={[country]}\n label=\"Phone\"\n onValueChange={() => {}}\n value={value}\n />\n )\n const selector = findByAccessibilityLabel(root, 'Country code, United States, +1')\n\n await act(async () => {\n callAction(readProp(selector, 'onPress'), 'Country selector is missing its native press action.')\n await Promise.resolve()\n })\n\n expect(readProp(findHostComponent(root, 'Modal'), 'visible')).toBe(true)\n expect(readProp(findByAccessibilityLabel(root, 'Search countries'), 'editable')).toBe(true)\n\n await act(async () => {\n const onChangeText = readProp(findByAccessibilityLabel(root, 'Search countries'), 'onChangeText')\n\n if (typeof onChangeText !== 'function') throw new Error('Country search is missing its change action.')\n\n Reflect.apply(onChangeText, undefined, ['united'])\n await Promise.resolve()\n })\n\n await act(async () => {\n root.render(renderPhone(false))\n await Promise.resolve()\n })\n\n expect(readProp(findHostComponent(root, 'Modal'), 'visible')).toBe(false)\n expect(readProp(findByAccessibilityLabel(root, 'Country code, United States, +1'), 'accessibilityState'))\n .toEqual({ disabled: true, expanded: false })\n\n await act(async () => {\n root.render(renderPhone(true))\n await Promise.resolve()\n })\n\n expect(readProp(findByAccessibilityLabel(root, 'Country code, United States, +1'), 'accessibilityState'))\n .toEqual({ disabled: false, expanded: false })\n\n const reopenedSelector = findByAccessibilityLabel(root, 'Country code, United States, +1')\n\n await act(async () => {\n callAction(\n readProp(reopenedSelector, 'onPress'),\n 'Re-enabled country selector is missing its native press action.'\n )\n await Promise.resolve()\n })\n\n expect(readProp(findByAccessibilityLabel(root, 'Search countries'), 'value')).toBe('')\n })\n\n test('chip removal exposes the independent action disabled state', async () => {\n const root = await renderNative(\n <LumenChip disabled label=\"Design\" onRemove={() => {}} />\n )\n const removeButton = findByAccessibilityLabel(root, 'Remove Design')\n\n expect(readProp(removeButton, 'accessibilityRole')).toBe('button')\n expect(readProp(removeButton, 'disabled')).toBe(true)\n expect(readProp(removeButton, 'accessibilityState')).toEqual({ disabled: true })\n expect(readProp(removeButton, 'style')).toMatchObject({\n alignItems: 'center',\n justifyContent: 'center',\n minHeight: 24,\n minWidth: 24\n })\n })\n\n test('toast announces politely and keeps dismissal independently operable', async () => {\n const onDismiss = vi.fn<() => void>()\n const root = await renderNative(\n <LumenToast dismissLabel=\"Close update\" onDismiss={onDismiss} title=\"Changes saved\" />\n )\n const alert = findByAccessibilityRole(root, 'alert')\n const dismissButton = findByAccessibilityLabel(root, 'Close update')\n const style = readProp(dismissButton, 'style')\n\n expect(readProp(alert, 'accessibilityLiveRegion')).toBe('polite')\n expect(readProp(dismissButton, 'accessibilityRole')).toBe('button')\n expect(style).toBeTypeOf('function')\n\n if (typeof style !== 'function') throw new Error('Toast dismissal is missing its pressable style.')\n\n const restingStyle: unknown = Reflect.apply(style, undefined, [{ pressed: false }])\n\n expect(restingStyle).toMatchObject({\n alignItems: 'center',\n justifyContent: 'center',\n minHeight: 44,\n minWidth: 44\n })\n\n await act(async () => {\n callAction(readProp(dismissButton, 'onPress'), 'Toast dismissal is missing its press action.')\n await Promise.resolve()\n })\n\n expect(onDismiss).toHaveBeenCalledOnce()\n })\n\n test('queues localized iOS toast copy once and announces changed content', async () => {\n const toast = (description: string) => (\n <StrictMode><LumenToast description={description} title=\"Cambios guardados\" /></StrictMode>\n )\n const root = await renderNative(toast('Registro actualizado'))\n\n expect(accessibilityAnnouncement).toHaveBeenCalledExactlyOnceWith('Cambios guardados. Registro actualizado', { queue: true })\n\n await act(async () => {\n root.render(<LumenProvider scheme=\"light\">{toast('Registro actualizado')}</LumenProvider>)\n await Promise.resolve()\n })\n\n expect(accessibilityAnnouncement).toHaveBeenCalledOnce()\n\n await act(async () => {\n root.render(<LumenProvider scheme=\"light\">{toast('Otro registro actualizado')}</LumenProvider>)\n await Promise.resolve()\n })\n\n expect(accessibilityAnnouncement).toHaveBeenLastCalledWith('Cambios guardados. Otro registro actualizado', { queue: true })\n expect(accessibilityAnnouncement).toHaveBeenCalledTimes(2)\n })\n\n test('honors iOS error announcement urgency and off without speaking diagnostic references', async () => {\n const root = await renderNative(<LumenErrorState announcement=\"assertive\" description=\"Try again\" reference=\"request-123\" title=\"Unable to save\" />)\n\n expect(accessibilityAnnouncement).toHaveBeenCalledExactlyOnceWith('Unable to save. Try again', { queue: false })\n\n await act(async () => {\n root.render(<LumenProvider scheme=\"light\"><LumenErrorState announcement=\"off\" title=\"Unable to save\" /></LumenProvider>)\n await Promise.resolve()\n })\n\n expect(accessibilityAnnouncement).toHaveBeenCalledOnce()\n\n await act(async () => {\n root.render(<LumenProvider scheme=\"light\"><LumenErrorState announcement=\"polite\" title=\"Unable to save\" /></LumenProvider>)\n await Promise.resolve()\n })\n\n expect(accessibilityAnnouncement).toHaveBeenLastCalledWith('Unable to save', { queue: true })\n expect(accessibilityAnnouncement).toHaveBeenCalledTimes(2)\n })\n\n test('does not announce initially disabled errors or empty toast copy on iOS', async () => {\n await renderNative(\n <>\n <LumenErrorState announcement=\"off\" title=\"Unable to save\" />\n <LumenToast title=\" \" />\n </>\n )\n\n expect(accessibilityAnnouncement).not.toHaveBeenCalled()\n })\n\n test.each(['android', 'web'])('keeps %s live regions without duplicate imperative announcements', async platform => {\n nativePlatform.OS = platform\n const root = await renderNative(\n <>\n <LumenToast title=\"Saved\" />\n <LumenErrorState announcement=\"assertive\" title=\"Unable to save\" />\n </>\n )\n\n expect(readProp(findByAccessibilityRole(root, 'alert'), 'accessibilityLiveRegion')).toBe('polite')\n expect(readProp(findByAccessibilityRole(root, 'summary'), 'accessibilityLiveRegion')).toBe('assertive')\n expect(accessibilityAnnouncement).not.toHaveBeenCalled()\n })\n\n test('banner dismissal has a full independent touch target', async () => {\n const root = await renderNative(\n <LumenBanner dismissLabel=\"Close notice\" onDismiss={() => {}} title=\"Maintenance soon\" />\n )\n const dismissButton = findByAccessibilityLabel(root, 'Close notice')\n const style = readProp(dismissButton, 'style')\n\n expect(readProp(dismissButton, 'accessibilityRole')).toBe('button')\n expect(style).toBeTypeOf('function')\n\n if (typeof style !== 'function') throw new Error('Banner dismissal is missing its pressable style.')\n\n const restingStyle: unknown = Reflect.apply(style, undefined, [{ pressed: false }])\n\n expect(restingStyle).toMatchObject({\n alignItems: 'center',\n justifyContent: 'center',\n minHeight: 44,\n minWidth: 44\n })\n })\n\n test('status bars allow long messages to wrap without shrinking trailing content', async () => {\n const message = 'El análisis permanece en el dispositivo para proteger tu privacidad.'\n const root = await renderNative(\n <LumenProvider>\n <LumenStatusBar\n message={message}\n trailing={<LumenText>Analyze</LumenText>}\n />\n </LumenProvider>\n )\n const messageNodes = root.container.queryAll(\n instance => instance.type === 'Text' && readProp(instance, 'children') === message\n )\n\n expect(messageNodes).toHaveLength(1)\n\n const messageNode = messageNodes[0]\n\n if (!messageNode) throw new Error('Expected the status message fixture.')\n\n expect(readProp(messageNode, 'numberOfLines')).toBeUndefined()\n expect(readProp(messageNode, 'style')).toMatchObject({ flex: 1, flexShrink: 1 })\n\n const fixedTrailingContainers = root.container.queryAll(instance => {\n const style = readProp(instance, 'style')\n\n return instance.type === 'View' && typeof style === 'object' && style !== null &&\n Reflect.get(style, 'flexShrink') === 0\n })\n\n expect(fixedTrailingContainers).toHaveLength(1)\n })\n\n test('search clear action preserves its label and full touch target', async () => {\n const onChangeText = vi.fn<(value: string) => void>()\n const root = await renderNative(\n <LumenSearchField\n clearLabel=\"Clear query\"\n onChangeText={onChangeText}\n value=\"lumen\"\n />\n )\n const clearButton = findByAccessibilityLabel(root, 'Clear query')\n const style = readProp(clearButton, 'style')\n\n expect(readProp(clearButton, 'accessibilityRole')).toBe('button')\n expect(style).toBeTypeOf('function')\n\n if (typeof style !== 'function') throw new Error('Clear action is missing its pressable style.')\n\n const restingStyle: unknown = Reflect.apply(style, undefined, [{ pressed: false }])\n\n expect(restingStyle).toMatchObject({\n alignItems: 'center',\n justifyContent: 'center',\n minHeight: 44,\n minWidth: 44\n })\n\n await act(async () => {\n callAction(readProp(clearButton, 'onPress'), 'Clear action is missing its native press action.')\n await Promise.resolve()\n })\n\n expect(onChangeText).toHaveBeenCalledExactlyOnceWith('')\n })\n\n test('checkbox announces state and emits the next controlled value', async () => {\n const onCheckedChange = vi.fn<(checked: boolean) => void>()\n const root = await renderNative(\n <LumenCheckbox\n checked={false}\n label=\"Include diagnostics\"\n onCheckedChange={onCheckedChange}\n />\n )\n const checkbox = findByAccessibilityRole(root, 'checkbox')\n const onPress = readProp(checkbox, 'onPress')\n\n expect(readProp(checkbox, 'accessibilityLabel')).toBe('Include diagnostics')\n expect(readProp(checkbox, 'aria-checked')).toBe(false)\n expect(readProp(checkbox, 'aria-disabled')).toBe(false)\n\n expect(readProp(checkbox, 'accessibilityState')).toEqual({\n checked: false,\n disabled: false\n })\n expect(onPress).toBeTypeOf('function')\n\n await act(async () => {\n callAction(onPress, 'Checkbox is missing its native press action.')\n await Promise.resolve()\n })\n\n expect(onCheckedChange).toHaveBeenCalledExactlyOnceWith(true)\n })\n\n test('checkbox preserves a host-provided accessible name', async () => {\n const root = await renderNative(\n <LumenCheckbox checked={false} label=\"Diagnostics\" accessibilityLabel=\"Include diagnostic report\" onCheckedChange={() => undefined} />\n )\n\n expect(readProp(findByAccessibilityRole(root, 'checkbox'), 'accessibilityLabel')).toBe('Include diagnostic report')\n })\n\n test('checkbox announces its visible description as a hint like toggle', async () => {\n const root = await renderNative(\n <>\n <LumenCheckbox\n checked={false}\n description=\"Attach logs to the support ticket\"\n label=\"Include diagnostics\"\n onCheckedChange={() => undefined}\n />\n <LumenCheckbox\n accessibilityHint=\"Consumer guidance\"\n checked={false}\n description=\"Visible guidance\"\n label=\"Share usage\"\n onCheckedChange={() => undefined}\n />\n <LumenCheckbox\n accessibilityHint=\"\"\n checked={false}\n description=\"Visible guidance\"\n label=\"Quiet hints\"\n onCheckedChange={() => undefined}\n />\n </>\n )\n\n expect(readProp(findByAccessibilityLabel(root, 'Include diagnostics'), 'accessibilityHint')).toBe('Attach logs to the support ticket')\n expect(readProp(findByAccessibilityLabel(root, 'Share usage'), 'accessibilityHint')).toBe('Consumer guidance')\n expect(readProp(findByAccessibilityLabel(root, 'Quiet hints'), 'accessibilityHint')).toBe('')\n })\n\n test('toggle makes the full labeled row the single interactive switch', async () => {\n const onValueChange = vi.fn<(value: boolean) => void>()\n const root = await renderNative(\n <LumenToggle\n description=\"Receive product and security updates\"\n label=\"Automatic updates\"\n onValueChange={onValueChange}\n value={false}\n />\n )\n const toggle = findByAccessibilityRole(root, 'switch')\n const onPress = readProp(toggle, 'onPress')\n const nativeSwitches = root.container.queryAll(instance => instance.type === 'Switch')\n\n expect(readProp(toggle, 'accessibilityLabel')).toBe('Automatic updates')\n expect(readProp(toggle, 'accessibilityHint')).toBe('Receive product and security updates')\n expect(readProp(toggle, 'aria-checked')).toBe(false)\n expect(readProp(toggle, 'accessibilityState')).toEqual({\n checked: false,\n disabled: false\n })\n expect(nativeSwitches).toHaveLength(1)\n\n const nativeSwitch = nativeSwitches[0]\n\n if (!nativeSwitch) throw new Error('Expected the decorative native switch.')\n\n expect(readProp(nativeSwitch, 'accessibilityElementsHidden')).toBe(true)\n expect(readProp(nativeSwitch, 'importantForAccessibility')).toBe('no-hide-descendants')\n expect(readProp(nativeSwitch, 'pointerEvents')).toBe('none')\n expect(readProp(nativeSwitch, 'onValueChange')).toBeUndefined()\n\n await act(async () => {\n callAction(onPress, 'Toggle row is missing its press action.')\n await Promise.resolve()\n })\n\n expect(onValueChange).toHaveBeenCalledExactlyOnceWith(true)\n })\n\n test('web toggle keeps the decorative native input out of the tab order', async () => {\n nativePlatform.OS = 'web'\n const onValueChange = vi.fn()\n const root = await renderNative(<LumenToggle label=\"Updates\" onValueChange={onValueChange} value={false} />)\n const control = root.container.queryAll(instance => instance.type === 'Switch')[0]\n if (!control) throw new Error('Missing decorative switch')\n expect(readProp(control, 'disabled')).toBe(true)\n expect(readProp(control, 'aria-hidden')).toBe(true)\n const row = findByAccessibilityRole(root, 'switch')\n expect(readProp(row, 'disabled')).toBe(false)\n await act(async () => {\n callAction(readProp(row, 'onPress'), 'Missing row action')\n await Promise.resolve()\n })\n expect(onValueChange).toHaveBeenCalledExactlyOnceWith(true)\n nativePlatform.OS = 'ios'\n })\n\n test('toggle preserves a consumer accessibility hint over supporting copy', async () => {\n const root = await renderNative(\n <LumenToggle\n accessibilityHint=\"Changes how updates are installed\"\n description=\"Receive product and security updates\"\n label=\"Automatic updates\"\n onValueChange={() => {}}\n value={false}\n />\n )\n\n expect(readProp(findByAccessibilityRole(root, 'switch'), 'accessibilityHint'))\n .toBe('Changes how updates are installed')\n })\n\n test('disabled toggle row cannot emit a value change', async () => {\n const onValueChange = vi.fn<(value: boolean) => void>()\n const root = await renderNative(\n <LumenToggle\n disabled\n label=\"Automatic updates\"\n onValueChange={onValueChange}\n value\n />\n )\n const toggle = findByAccessibilityRole(root, 'switch')\n\n expect(readProp(toggle, 'accessibilityState')).toEqual({\n checked: true,\n disabled: true\n })\n expect(readProp(toggle, 'disabled')).toBe(true)\n\n await act(async () => {\n callAction(readProp(toggle, 'onPress'), 'Disabled toggle row is missing its guarded press action.')\n await Promise.resolve()\n })\n\n expect(onValueChange).not.toHaveBeenCalled()\n })\n\n test('tabs preserve selected and disabled semantics while emitting enabled selection', async () => {\n const onValueChange = vi.fn<(value: string) => void>()\n const root = await renderNative(\n <LumenTabs\n label=\"Workspace views\"\n onValueChange={onValueChange}\n options={[\n { label: 'Overview', value: 'overview' },\n { disabled: true, label: 'Activity', value: 'activity' }\n ]}\n value=\"overview\"\n >\n <LumenText>Current workspace health</LumenText>\n </LumenTabs>\n )\n const tabList = findByAccessibilityRole(root, 'tablist')\n const tabs = root.container.queryAll(\n instance => readProp(instance, 'accessibilityRole') === 'tab'\n )\n\n expect(readProp(tabList, 'accessibilityLabel')).toBe('Workspace views')\n expect(tabs).toHaveLength(2)\n const [overviewTab, activityTab] = tabs\n\n if (!overviewTab || !activityTab) throw new Error('Expected the two configured tabs.')\n\n expect(readProp(overviewTab, 'accessibilityState')).toEqual({\n disabled: false,\n selected: true\n })\n expect(readProp(activityTab, 'accessibilityState')).toEqual({\n disabled: true,\n selected: false\n })\n\n const enabledTabPress = readProp(overviewTab, 'onPress')\n\n await act(async () => {\n callAction(enabledTabPress, 'Enabled tab is missing its press action.')\n await Promise.resolve()\n })\n\n expect(onValueChange).toHaveBeenCalledExactlyOnceWith('overview')\n })\n\n test('uses the translated tab label by default and accepts an explicit translated panel name', async () => {\n const fixture = (panelAccessibilityLabel?: string): ReactElement => (\n <LumenTabs label=\"Vistas\" onValueChange={vi.fn()} options={[{ label: 'Resumen', value: 'overview' }]} {...(panelAccessibilityLabel ? { panelAccessibilityLabel } : {})} value=\"overview\">\n <LumenText>Contenido</LumenText>\n </LumenTabs>\n )\n const root = await renderNative(fixture())\n const panel = root.container.queryAll(instance => readProp(instance, 'role') === 'tabpanel')[0]\n if (!panel) throw new Error('Missing tab panel')\n expect(readProp(panel, 'accessibilityLabel')).toBe('Resumen')\n await act(async () => {\n root.render(<LumenProvider>{fixture('Contenido del resumen')}</LumenProvider>)\n await Promise.resolve()\n })\n const translatedPanel = root.container.queryAll(instance => readProp(instance, 'role') === 'tabpanel')[0]\n if (!translatedPanel) throw new Error('Missing translated tab panel')\n expect(readProp(translatedPanel, 'accessibilityLabel')).toBe('Contenido del resumen')\n })\n\n test('tabs support directional keyboard navigation without selecting disabled tabs', async () => {\n const onValueChange = vi.fn<(value: string) => void>()\n const KeyboardTabsFixture = (): ReactElement => {\n const [value, setValue] = useState('overview')\n\n return (\n <LumenTabs\n label=\"Workspace views\"\n onValueChange={nextValue => {\n onValueChange(nextValue)\n setValue(nextValue)\n }}\n options={[\n { label: 'Overview', value: 'overview' },\n { label: 'Activity', value: 'activity' },\n { disabled: true, label: 'Billing', value: 'billing' }\n ]}\n value={value}\n >\n <LumenText>Current workspace health</LumenText>\n </LumenTabs>\n )\n }\n const root = await renderNative(<KeyboardTabsFixture />)\n const pressTabKey = async (optionIndex: number, key: string): Promise<void> => {\n const tab = root.container.queryAll(\n instance => readProp(instance, 'accessibilityRole') === 'tab'\n )[optionIndex]\n\n if (!tab) throw new Error(`Expected tab fixture at index ${optionIndex}.`)\n\n let prevented = false\n\n await act(async () => {\n const preventDefault = callKeyboardAction(readProp(tab, 'onKeyDown'), key)\n\n prevented = preventDefault.mock.calls.length > 0\n\n await Promise.resolve()\n })\n\n expect(prevented).toBe(true)\n }\n\n await pressTabKey(0, 'ArrowRight')\n expect(onValueChange).toHaveBeenLastCalledWith('activity')\n\n await pressTabKey(1, 'ArrowRight')\n expect(onValueChange).toHaveBeenLastCalledWith('overview')\n expect(onValueChange).not.toHaveBeenCalledWith('billing')\n })\n\n test('tabs do not reselect the current tab when every alternative is disabled', async () => {\n const onValueChange = vi.fn<(value: string) => void>()\n const root = await renderNative(\n <LumenTabs\n label=\"Workspace views\"\n onValueChange={onValueChange}\n options={[\n { label: 'Overview', value: 'overview' },\n { disabled: true, label: 'Activity', value: 'activity' }\n ]}\n value=\"overview\"\n >\n <LumenText>Current workspace health</LumenText>\n </LumenTabs>\n )\n const overviewTab = root.container.queryAll(\n instance => readProp(instance, 'accessibilityRole') === 'tab'\n )[0]\n\n if (!overviewTab) throw new Error('Expected the selected tab fixture.')\n\n expect(callKeyboardAction(readProp(overviewTab, 'onKeyDown'), 'ArrowRight'))\n .toHaveBeenCalledOnce()\n expect(onValueChange).not.toHaveBeenCalled()\n })\n})\n\ndescribe('LumenSheet consumer layouts', () => {\n afterEach(() => {\n nativeMotion.enabled = false\n nativeWindow.width = 400\n nativeWindow.height = 800\n nativeWindow.fontScale = 1\n nativePlatform.OS = 'ios'\n accessibilityFocus.mockClear()\n })\n\n test.each([\n { fontScale: 2, height: 800, alignment: 'center' },\n { fontScale: 1, height: 320, alignment: 'flex-end' }\n ])('keeps headings and actions scrollable when space is limited: %j', async ({ alignment, ...window }) => {\n Object.assign(nativeWindow, window, { width: 1024 })\n const root = await renderNative(\n <LumenSheet actions={<LumenButton>Save changes</LumenButton>} onDismiss={vi.fn()} presentation=\"adaptive\" title=\"A long translated heading\" visible>\n <LumenTextField accessibilityLabel=\"Details\" />\n </LumenSheet>\n )\n const scroll = findHostComponent(root, 'ScrollView')\n\n expect(scroll.queryAll(instance => readProp(instance, 'accessibilityRole') === 'header')).toHaveLength(1)\n expect(scroll.queryAll(instance => readProp(instance, 'accessibilityRole') === 'button')).toHaveLength(1)\n expect(readProp(scroll, 'keyboardShouldPersistTaps')).toBe('handled')\n expect(readProp(findHostComponent(root, 'KeyboardAvoidingView'), 'style')).toMatchObject({ justifyContent: alignment })\n })\n\n test('preserves application-owned virtualization at accessibility text sizes', async () => {\n nativeWindow.fontScale = 3\n const root = await renderNative(\n <LumenSheet onDismiss={vi.fn()} scrollable={false} visible>\n <LumenText>Virtualized collection</LumenText>\n </LumenSheet>\n )\n\n expect(root.container.queryAll(instance => instance.type === 'ScrollView')).toHaveLength(0)\n })\n\n test('focuses an explicit initial control after presentation and restores the trigger after closing', async () => {\n const initialFocusRef = createRef<ComponentRef<typeof View>>()\n const returnFocusRef = createRef<ComponentRef<typeof View>>()\n let restoreFrame: (() => void) | undefined\n vi.stubGlobal('requestAnimationFrame', (callback: () => void) => {\n restoreFrame = callback\n\n return 1\n })\n vi.stubGlobal('cancelAnimationFrame', vi.fn())\n const fixture = (visible: boolean): ReactElement => (\n <>\n <LumenButton ref={returnFocusRef}>Edit</LumenButton>\n <LumenSheet\n initialFocusRef={initialFocusRef}\n onDismiss={vi.fn()}\n returnFocusRef={returnFocusRef}\n visible={visible}\n >\n <LumenButton ref={initialFocusRef}>Save</LumenButton>\n </LumenSheet>\n </>\n )\n const root = await renderNative(fixture(true))\n callAction(readProp(findHostComponent(root, 'Modal'), 'onShow'), 'Missing modal presentation event')\n expect(accessibilityFocus).toHaveBeenCalledExactlyOnceWith(initialFocusRef.current, 'focus')\n await act(async () => {\n root.render(<LumenProvider>{fixture(false)}</LumenProvider>)\n await Promise.resolve()\n })\n expect(accessibilityFocus).toHaveBeenCalledTimes(1)\n if (!restoreFrame) throw new Error('Expected a deferred focus restoration')\n restoreFrame()\n expect(accessibilityFocus).toHaveBeenLastCalledWith(returnFocusRef.current, 'focus')\n })\n\n test('wraps related actions at ordinary text sizes and stacks them at accessibility sizes', async () => {\n const fixture = (): ReactElement => (\n <LumenButtonGroup accessibilityLabel=\"Form actions\">\n <LumenButton>Cancel</LumenButton>\n <LumenButton>Save</LumenButton>\n </LumenButtonGroup>\n )\n const root = await renderNative(fixture())\n const group = root.container.queryAll(instance => readProp(instance, 'accessibilityLabel') === 'Form actions')[0]\n if (!group) throw new Error('Missing action group')\n expect(readProp(group, 'style')).toEqual(expect.arrayContaining([expect.objectContaining({ flexDirection: 'row', flexWrap: 'wrap' })]))\n nativeWindow.fontScale = 2\n await act(async () => {\n root.render(<LumenProvider>{fixture()}</LumenProvider>)\n await Promise.resolve()\n })\n const updated = root.container.queryAll(instance => readProp(instance, 'accessibilityLabel') === 'Form actions')[0]\n if (!updated) throw new Error('Missing updated action group')\n expect(readProp(updated, 'style')).toEqual(expect.arrayContaining([expect.objectContaining({ flexDirection: 'column', flexWrap: 'nowrap' })]))\n })\n\n test('keeps actions outside the scrolling body and opts into keyboard avoidance', async () => {\n const root = await renderNative(\n <LumenSheet\n actions={<LumenButton>Save</LumenButton>}\n avoidKeyboard\n keyboardVerticalOffset={24}\n onDismiss={vi.fn()}\n safeAreaInsets={{ bottom: 34 }}\n scrollable\n title=\"Edit record\"\n visible\n >\n <LumenTextField accessibilityLabel=\"Name\" />\n </LumenSheet>\n )\n const scroll = root.container.queryAll(instance => instance.type === 'ScrollView')[0]\n const keyboard = root.container.queryAll(instance => instance.type === 'KeyboardAvoidingView')[0]\n if (!scroll || !keyboard) throw new Error('Missing keyboard or scrolling surface')\n expect(readProp(keyboard, 'enabled')).toBe(true)\n expect(readProp(keyboard, 'behavior')).toBe('padding')\n expect(readProp(keyboard, 'keyboardVerticalOffset')).toBe(24)\n expect(readProp(scroll, 'keyboardShouldPersistTaps')).toBe('handled')\n expect(scroll.queryAll(instance => readProp(instance, 'accessibilityRole') === 'button')).toHaveLength(0)\n expect(findByAccessibilityRole(root, 'header')).toBeDefined()\n expect(root.container.queryAll(instance => {\n const style = readProp(instance, 'style')\n return typeof style === 'object' && style !== null && 'paddingBottom' in style && style.paddingBottom === 34\n })).toHaveLength(1)\n })\n\n test('blocks platform and backdrop dismissal until the application unlocks it', async () => {\n const dismiss = vi.fn()\n const root = await renderNative(\n <LumenSheet dismissible={false} onDismiss={dismiss} visible><LumenText>Saving</LumenText></LumenSheet>\n )\n const modal = root.container.queryAll(instance => instance.type === 'Modal')[0]\n const backdrop = root.container.queryAll(instance => instance.type === 'Pressable')[0]\n if (!modal || !backdrop) throw new Error('Missing modal or backdrop')\n callAction(readProp(modal, 'onRequestClose'), 'Missing platform dismiss')\n callAction(readProp(backdrop, 'onPress'), 'Missing backdrop dismiss')\n expect(dismiss).not.toHaveBeenCalled()\n expect(readProp(backdrop, 'disabled')).toBe(true)\n await act(async () => {\n root.render(<LumenProvider scheme=\"light\"><LumenSheet onDismiss={dismiss} visible><LumenText>Saved</LumenText></LumenSheet></LumenProvider>)\n await Promise.resolve()\n })\n const updatedModal = root.container.queryAll(instance => instance.type === 'Modal')[0]\n if (!updatedModal) throw new Error('Missing updated modal')\n callAction(readProp(updatedModal, 'onRequestClose'), 'Missing platform dismiss')\n expect(dismiss).toHaveBeenCalledOnce()\n })\n\n test('names sheets and hides backdrop targets from keyboard and accessibility traversal', async () => {\n const root = await renderNative(<LumenSheet onDismiss={vi.fn()} title=\"Edit record\" visible><LumenText>Details</LumenText></LumenSheet>)\n const modal = root.container.queryAll(instance => instance.type === 'Modal')[0]\n const backdrop = root.container.queryAll(instance => instance.type === 'Pressable')[0]\n if (!modal || !backdrop) throw new Error('Missing modal or backdrop')\n expect(readProp(modal, 'accessibilityLabel')).toBe('Edit record')\n expect(readProp(backdrop, 'tabIndex')).toBe(-1)\n expect(readProp(backdrop, 'aria-hidden')).toBe(true)\n })\n\n test('honors the native reduced-motion setting', async () => {\n nativeMotion.enabled = true\n const root = await renderNative(<LumenSheet onDismiss={vi.fn()} visible><LumenText>Details</LumenText></LumenSheet>)\n const modal = root.container.queryAll(instance => instance.type === 'Modal')[0]\n if (!modal) throw new Error('Missing modal')\n expect(readProp(modal, 'animationType')).toBe('none')\n })\n\n test('honors reduced motion in confirmation dialogs and menus', async () => {\n nativeMotion.enabled = true\n const root = await renderNative(\n <>\n <LumenAlertDialog confirmLabel=\"Save\" onConfirm={vi.fn()} onDismiss={vi.fn()} title=\"Save changes\" visible />\n <LumenMenu accessibilityLabel=\"Actions\" items={[]} trigger={<LumenText>Open menu</LumenText>} />\n </>\n )\n const modals = root.container.queryAll(instance => instance.type === 'Modal')\n\n expect(modals).toHaveLength(2)\n expect(modals.map(modal => readProp(modal, 'animationType'))).toEqual(['none', 'none'])\n })\n\n test('uses a centered adaptive dialog on wide windows and Android keyboard behavior', async () => {\n nativeWindow.width = 1024\n nativePlatform.OS = 'android'\n const root = await renderNative(\n <LumenSheet avoidKeyboard onDismiss={vi.fn()} presentation=\"adaptive\" visible><LumenText>Details</LumenText></LumenSheet>\n )\n const modal = root.container.queryAll(instance => instance.type === 'Modal')[0]\n const keyboard = root.container.queryAll(instance => instance.type === 'KeyboardAvoidingView')[0]\n if (!modal || !keyboard) throw new Error('Missing modal or keyboard surface')\n expect(readProp(modal, 'animationType')).toBe('fade')\n expect(readProp(keyboard, 'behavior')).toBe('height')\n expect(readProp(keyboard, 'style')).toMatchObject({ alignItems: 'center', justifyContent: 'center' })\n })\n})\n\ndescribe('Static graphic icon accessibility', () => {\n const Graphic = ({ size, strokeWidth }: LumenIconGraphicProps): ReactElement => (\n <LumenText>{`Graphic ${String(size)} ${String(strokeWidth)}`}</LumenText>\n )\n\n test('names informative graphics and hides decorative graphics', async () => {\n const root = await renderNative(\n <>\n <GraphicIcon icon={Graphic} label=\"Search records\" size=\"lg\" strokeWidth={3} />\n <GraphicIcon icon={Graphic} decorative label=\"Decorative search\" />\n </>\n )\n const informative = root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'image' && readProp(instance, 'accessible') === true)\n expect(informative).toHaveLength(1)\n const named = informative[0]\n if (!named) throw new Error('Missing informative graphic')\n expect(readProp(named, 'accessibilityLabel')).toBe('Search records')\n expect(readProp(named, 'importantForAccessibility')).toBe('yes')\n expect(root.container.queryAll(instance => readProp(instance, 'children') === 'Graphic 24 3').length).toBeGreaterThan(0)\n const hidden = root.container.queryAll(instance => readProp(instance, 'importantForAccessibility') === 'no' && readProp(instance, 'accessible') === false)\n expect(hidden).toHaveLength(1)\n const decorative = hidden[0]\n if (!decorative) throw new Error('Missing decorative graphic')\n expect(readProp(decorative, 'accessibilityLabel')).toBeUndefined()\n expect(readProp(decorative, 'accessibilityElementsHidden')).toBe(true)\n })\n\n test('preserves named and custom icons through the root entrypoint', async () => {\n const root = await renderNative(\n <>\n <LumenIcon name=\"search\" label=\"Named search\" />\n <LumenIcon icon={Graphic} label=\"Custom search\" />\n <LumenIconButton name=\"search\" label=\"Named search action\" disabled />\n <LumenIconButton icon={Graphic} label=\"Custom search action\" disabled />\n </>\n )\n const icons = root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'image')\n expect(icons.map(icon => readProp(icon, 'accessibilityLabel'))).toEqual(['Named search', 'Custom search'])\n const buttons = root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'button')\n expect(buttons.map(button => readProp(button, 'accessibilityLabel'))).toEqual(['Named search action', 'Custom search action'])\n expect(buttons.map(button => readProp(button, 'accessibilityState'))).toEqual([{ disabled: true }, { disabled: true }])\n })\n\n test('canonical per-icon imports match named artwork and preserve accessible labels', async () => {\n const root = await renderNative(\n <>\n <LumenIcon name=\"search\" label=\"Named search\" size=\"lg\" strokeWidth={3} />\n <GraphicIcon icon={LumenSearchIconGraphic} label=\"Static search\" size=\"lg\" strokeWidth={3} />\n <LumenIcon name=\"brand:github\" decorative />\n <GraphicIcon icon={LumenBrandGithubIconGraphic} decorative />\n </>\n )\n const graphics = root.container.queryAll(instance => instance.type === 'Svg')\n expect(graphics).toHaveLength(4)\n const geometry = (graphic: TestInstance | undefined): unknown => {\n if (!graphic) throw new Error('Missing canonical SVG')\n return {\n props: Object.fromEntries(Object.entries(graphic.props).filter(([name]) => name !== 'children')),\n children: graphic.children.filter(child => typeof child !== 'string').map(child => ({ type: child.type, props: child.props }))\n }\n }\n expect(geometry(graphics[0])).toEqual(geometry(graphics[1]))\n expect(geometry(graphics[2])).toEqual(geometry(graphics[3]))\n const icons = root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'image')\n expect(icons.map(icon => readProp(icon, 'accessibilityLabel'))).toEqual(['Named search', 'Static search'])\n })\n\n test('keeps graphic buttons named and announces disabled state', async () => {\n const root = await renderNative(<GraphicIconButton icon={Graphic} label=\"Search records\" disabled />)\n const button = root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'button')[0]\n if (!button) throw new Error('Missing graphic button')\n expect(readProp(button, 'accessibilityLabel')).toBe('Search records')\n expect(readProp(button, 'accessibilityState')).toEqual({ disabled: true })\n expect(readProp(button, 'disabled')).toBe(true)\n })\n})\n\nconst callInputAction = (value: unknown, proposal: string): void => {\n if (typeof value !== 'function') throw new Error('Missing native input callback')\n Reflect.apply(value, undefined, [proposal])\n}\n\nconst runNativeAction = async (action: () => void): Promise<void> => {\n await act(async () => {\n action()\n\n await Promise.resolve()\n })\n}\n\ndescribe('advanced native inputs', () => {\n test('secure entry hides on blur and after disabling and re-enabling', async () => {\n const props = { label: 'Password', onValueChange: vi.fn(), value: 'synthetic-fixture' }\n const root = await renderNative(<LumenPasswordField {...props} />)\n expect(readProp(findByAccessibilityLabel(root, 'Password'), 'secureTextEntry')).toBe(true)\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Show password'), 'onPress'), 'Missing reveal')\n })\n expect(readProp(findByAccessibilityLabel(root, 'Password'), 'secureTextEntry')).toBe(false)\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Password'), 'onBlur'), 'Missing blur')\n })\n expect(readProp(findByAccessibilityLabel(root, 'Password'), 'secureTextEntry')).toBe(true)\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Show password'), 'onPress'), 'Missing reveal')\n })\n await runNativeAction(() => {\n root.render(<LumenProvider><LumenPasswordField {...props} enabled={false} /></LumenProvider>)\n })\n await runNativeAction(() => {\n root.render(<LumenProvider><LumenPasswordField {...props} /></LumenProvider>)\n })\n expect(readProp(findByAccessibilityLabel(root, 'Password'), 'secureTextEntry')).toBe(true)\n })\n\n test('OTP preserves native autofill, normalizes paste and emits completion only for changed full codes', async () => {\n const onValueChange = vi.fn<(value: string) => void>()\n const onComplete = vi.fn<(value: string) => void>()\n const root = await renderNative(<LumenInputOTP label=\"Code\" value=\"123\" onValueChange={onValueChange} onComplete={onComplete} />)\n const input = findByAccessibilityLabel(root, 'Code')\n expect(readProp(input, 'textContentType')).toBe('oneTimeCode')\n await runNativeAction(() => {\n callInputAction(readProp(input, 'onChangeText'), '١٢٣-456')\n })\n expect(onValueChange).toHaveBeenCalledExactlyOnceWith('123456')\n expect(onComplete).toHaveBeenCalledExactlyOnceWith('123456')\n await runNativeAction(() => {\n callInputAction(readProp(input, 'onChangeText'), 'not a code')\n })\n expect(onValueChange).toHaveBeenCalledTimes(1)\n await runNativeAction(() => {\n root.render(<LumenProvider><LumenInputOTP label=\"Code\" value=\"123456\" onValueChange={onValueChange} onComplete={onComplete} readOnly /></LumenProvider>)\n })\n await runNativeAction(() => {\n callInputAction(readProp(findByAccessibilityLabel(root, 'Code'), 'onChangeText'), '654321')\n })\n expect(onValueChange).toHaveBeenCalledTimes(1)\n })\n\n test('number entry retains raw drafts and steps exact localized values', async () => {\n const onValueChange = vi.fn<(value: string) => void>()\n const root = await renderNative(<LumenNumberField label=\"Amount\" value=\"0,1\" locale=\"es-CO\" step=\"0.2\" onValueChange={onValueChange} />)\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Increase value'), 'onPress'), 'Missing increase')\n })\n expect(onValueChange).toHaveBeenLastCalledWith('0,3')\n await runNativeAction(() => {\n callInputAction(readProp(findByAccessibilityLabel(root, 'Amount'), 'onChangeText'), '12,')\n })\n expect(onValueChange).toHaveBeenLastCalledWith('12,')\n await runNativeAction(() => {\n root.render(<LumenProvider><LumenNumberField label=\"Amount\" value=\"12,\" locale=\"es-CO\" onValueChange={onValueChange} /></LumenProvider>)\n })\n expect(readProp(findByAccessibilityLabel(root, 'Increase value'), 'disabled')).toBe(true)\n expect(readProp(findByAccessibilityLabel(root, 'Amount'), 'aria-invalid')).toBe(true)\n })\n\n test('autocomplete publishes query before selection and closes its results', async () => {\n const calls: string[] = []\n const root = await renderNative(\n <LumenAutocomplete\n label=\"City\"\n query=\"Bo\"\n options={[{ label: 'Bogotá', value: 'bogota' }]}\n onQueryChange={query => {\n calls.push(query)\n }}\n onValueChange={value => {\n calls.push(value)\n }}\n />\n )\n await runNativeAction(() => {\n callInputAction(readProp(findByAccessibilityLabel(root, 'City'), 'onChangeText'), 'Bog')\n })\n calls.length = 0\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Bogotá'), 'onPress'), 'Missing result')\n })\n expect(calls).toEqual(['Bogotá', 'bogota'])\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityLabel') === 'Close results')).toHaveLength(0)\n })\n\n test('autocomplete loading hides results and disabled state dismisses selection', async () => {\n const props = { label: 'City', onQueryChange: vi.fn(), onValueChange: vi.fn(), options: [{ label: 'Bogotá', value: 'bogota' }], query: '' }\n const root = await renderNative(<LumenAutocomplete {...props} loading />)\n await runNativeAction(() => {\n callInputAction(readProp(findByAccessibilityLabel(root, 'City'), 'onChangeText'), 'Bo')\n })\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityLabel') === 'Bogotá')).toHaveLength(0)\n await runNativeAction(() => {\n root.render(<LumenProvider><LumenAutocomplete {...props} enabled={false} /></LumenProvider>)\n })\n await runNativeAction(() => {\n root.render(<LumenProvider><LumenAutocomplete {...props} /></LumenProvider>)\n })\n expect(readProp(findByAccessibilityLabel(root, 'City'), 'accessibilityState')).toMatchObject({ expanded: false })\n })\n\n test.each(['ios', 'android', 'web'])('time formatting tolerates malformed locales on %s', async platform => {\n nativePlatform.OS = platform\n for (const locale of ['not_a_locale', 'en--US', '💥']) {\n const props = { label: 'Time', value: { hour: 9, minute: 30 }, locale, onValueChange: vi.fn() }\n const root = await renderNative(<LumenTimeField {...props} is24Hour />)\n\n expect(findByAccessibilityLabel(root, 'Time, 09:30')).toBeDefined()\n await runNativeAction(() => {\n root.render(<LumenProvider><LumenTimeField {...props} is24Hour={false} /></LumenProvider>)\n })\n expect(findByAccessibilityLabel(root, 'Time, 9:30 AM')).toBeDefined()\n }\n })\n\n test('time selection requires confirmation and cancellation preserves the value', async () => {\n nativePlatform.OS = 'ios'\n const onValueChange = vi.fn()\n const root = await renderNative(\n <LumenTimeField\n label=\"Time\"\n value={{ hour: 9, minute: 30 }}\n locale=\"en-US\"\n is24Hour\n minTime={{ hour: 9, minute: 0 }}\n maxTime={{ hour: 17, minute: 0 }}\n onValueChange={onValueChange}\n />\n )\n const open = async (): Promise<void> => {\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Time, 09:30'), 'onPress'), 'Missing time trigger')\n })\n }\n await open()\n await runNativeAction(() => {\n const picker = findHostComponent(root, 'NativeDatePicker')\n const change = readProp(picker, 'onChange')\n if (typeof change !== 'function') throw new Error('Missing time change')\n Reflect.apply(change, undefined, [{ type: 'set' }, new Date(2000, 0, 1, 10, 45)])\n })\n expect(onValueChange).not.toHaveBeenCalled()\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Cancel'), 'onPress'), 'Missing cancel')\n })\n expect(onValueChange).not.toHaveBeenCalled()\n await open()\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Confirm'), 'onPress'), 'Missing confirm')\n })\n expect(onValueChange).toHaveBeenCalledExactlyOnceWith({ hour: 9, minute: 30 })\n })\n\n test.each(['ios', 'web'])('time sheet resets controlled changes without discarding equivalent inputs on %s', async platform => {\n nativePlatform.OS = platform\n const updates = [\n { value: { hour: 11, minute: 15 }, expected: { hour: 11, minute: 15 } },\n { minTime: { hour: 10, minute: 0 }, expected: { hour: 10, minute: 0 } },\n { maxTime: { hour: 9, minute: 15 }, expected: { hour: 9, minute: 0 } }\n ]\n for (const { expected, ...update } of updates) {\n const onValueChange = vi.fn()\n const props: LumenTimeFieldProps = { label: 'Time',\n value: { hour: 9, minute: 30 },\n locale: 'en-US',\n is24Hour: true,\n minTime: { hour: 9, minute: 0 },\n maxTime: { hour: 17, minute: 0 },\n onValueChange }\n const root = await renderNative(<LumenTimeField {...props} />)\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Time, 09:30'), 'onPress'), 'Missing time trigger')\n })\n await runNativeAction(() => {\n const picker = findHostComponent(root, platform === 'web' ? 'input' : 'NativeDatePicker')\n const change = readProp(picker, 'onChange')\n if (typeof change !== 'function') throw new Error('Missing time change')\n Reflect.apply(change, undefined, platform === 'web' ? [{ currentTarget: { value: '10:45' } }] : [{ type: 'set' }, new Date(2000, 0, 1, 10, 45)])\n })\n await runNativeAction(() => {\n root.render(\n <LumenProvider>\n <LumenTimeField\n {...props}\n value={{ hour: 9, minute: 30 }}\n minTime={{ hour: 9, minute: 0 }}\n maxTime={{ hour: 17, minute: 0 }}\n />\n </LumenProvider>\n )\n })\n expect(readProp(findHostComponent(root, 'Modal'), 'visible')).toBe(true)\n const draft = readProp(findHostComponent(root, platform === 'web' ? 'input' : 'NativeDatePicker'), 'value')\n expect(draft).toEqual(platform === 'web' ? '10:45' : new Date(2000, 0, 1, 10, 45))\n await runNativeAction(() => {\n root.render(<LumenProvider><LumenTimeField {...props} {...update} /></LumenProvider>)\n })\n expect(readProp(findHostComponent(root, 'Modal'), 'visible')).toBe(false)\n expect(onValueChange).not.toHaveBeenCalled()\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, `Time, ${update.value ? '11:15' : '09:30'}`), 'onPress'), 'Missing updated time trigger')\n })\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Confirm'), 'onPress'), 'Missing time confirm')\n })\n expect(onValueChange).toHaveBeenCalledExactlyOnceWith(expected)\n }\n })\n\n test('Android time rejects out-of-range selection and stale callbacks after disabling', async () => {\n nativePlatform.OS = 'android'\n const onValueChange = vi.fn()\n const props = { label: 'Time', locale: 'en-US', onValueChange, value: null }\n const root = await renderNative(<LumenTimeField {...props} minTime={{ hour: 9, minute: 0 }} />)\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Time, Choose a time'), 'onPress'), 'Missing time trigger')\n })\n const options = vi.mocked(DateTimePickerAndroid.open).mock.calls.at(-1)?.[0]\n if (!options?.onValueChange) throw new Error('Missing Android time handler')\n const change = options.onValueChange\n await runNativeAction(() => {\n change({ nativeEvent: { timestamp: 0, utcOffset: 0 } }, new Date(2000, 0, 1, 8, 0))\n })\n expect(onValueChange).not.toHaveBeenCalled()\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Time, Choose a time'), 'onPress'), 'Missing time trigger')\n })\n const current = vi.mocked(DateTimePickerAndroid.open).mock.calls.at(-1)?.[0]\n if (!current?.onValueChange) throw new Error('Missing Android time handler')\n const stale = current.onValueChange\n await runNativeAction(() => {\n root.render(<LumenProvider><LumenTimeField {...props} enabled={false} /></LumenProvider>)\n })\n expect(DateTimePickerAndroid.dismiss).toHaveBeenCalledWith('time')\n await runNativeAction(() => {\n stale({ nativeEvent: { timestamp: 0, utcOffset: 0 } }, new Date(2000, 0, 1, 10, 0))\n })\n expect(onValueChange).not.toHaveBeenCalled()\n })\n\n test('Android time validates current bounds and uses the current callback while the dialog is open', async () => {\n nativePlatform.OS = 'android'\n const openingCallback = vi.fn()\n const currentCallback = vi.fn()\n const props = { label: 'Time', locale: 'en-US', value: null }\n const root = await renderNative(\n <LumenTimeField {...props} minTime={{ hour: 9, minute: 0 }} onValueChange={openingCallback} />\n )\n const chooseAfterUpdate = async (hour: number): Promise<void> => {\n await runNativeAction(() => {\n root.render(\n <LumenProvider>\n <LumenTimeField {...props} minTime={{ hour: 9, minute: 0 }} onValueChange={openingCallback} />\n </LumenProvider>\n )\n })\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Time, Choose a time'), 'onPress'), 'Missing time trigger')\n })\n const options = vi.mocked(DateTimePickerAndroid.open).mock.calls.at(-1)?.[0]\n if (!options?.onValueChange) throw new Error('Missing Android time handler')\n const change = options.onValueChange\n await runNativeAction(() => {\n root.render(<LumenProvider><LumenTimeField {...props} minTime={{ hour: 12, minute: 0 }} maxTime={{ hour: 17, minute: 0 }} onValueChange={currentCallback} rangeErrorLabel=\"Choose an afternoon time\" /></LumenProvider>)\n })\n await runNativeAction(() => {\n change({ nativeEvent: { timestamp: 0, utcOffset: 0 } }, new Date(2000, 0, 1, hour, 30))\n })\n }\n await chooseAfterUpdate(10)\n expect(openingCallback).not.toHaveBeenCalled()\n expect(currentCallback).not.toHaveBeenCalled()\n expect(root.container.queryAll(instance => readProp(instance, 'children') === 'Choose an afternoon time').length).toBeGreaterThan(0)\n await chooseAfterUpdate(18)\n expect(currentCallback).not.toHaveBeenCalled()\n await chooseAfterUpdate(13)\n expect(openingCallback).not.toHaveBeenCalled()\n expect(currentCallback).toHaveBeenCalledExactlyOnceWith({ hour: 13, minute: 30 })\n })\n\n test.each(['en--US', 'invalid_tag', '💥'])('image comparison falls back for malformed locale %s', async locale => {\n const root = await renderNative(<LumenImageComparison label=\"Comparison\" before={{ uri: 'fixture:before' }} after={{ uri: 'fixture:after' }} value={0.25} locale={locale} onValueChange={() => {}} />)\n const control = findByAccessibilityRole(root, 'adjustable')\n expect(readProp(control, 'accessibilityValue')).toMatchObject({ text: 'After 25%' })\n })\n\n test.each(['side-by-side', 'before', 'after'] as const)('image comparison %s mode presents labels without an adjustable control', async mode => {\n const root = await renderNative(<LumenImageComparison label=\"Comparison\" before={{ uri: 'fixture:before' }} after={{ uri: 'fixture:after' }} value={0.25} mode={mode} onValueChange={() => {}} />)\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'adjustable')).toHaveLength(0)\n expect(root.container.queryAll(instance => readProp(instance, 'children') === 'Before').length > 0).toBe(mode !== 'after')\n expect(root.container.queryAll(instance => readProp(instance, 'children') === 'After').length > 0).toBe(mode !== 'before')\n })\n\n test('image comparison exposes one adjustable control and localized after percentage', async () => {\n const root = await renderNative(<LumenImageComparison label=\"Comparison\" before={{ uri: 'fixture:before' }} after={{ uri: 'fixture:after' }} value={0.25} locale=\"es-CO\" onValueChange={() => {}} />)\n const control = findByAccessibilityRole(root, 'adjustable')\n expect(readProp(control, 'accessibilityLabel')).toBe('Comparison')\n expect(readProp(control, 'accessibilityValue')).toMatchObject({ text: 'After 25%' })\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'image')).toHaveLength(0)\n })\n})\n\ndescribe('native chart data inspection', () => {\n test('renders zero as neutral and missing measurements with a separate marker and exact fallback', async () => {\n const root = await renderNative(\n <LumenHeatmap\n label=\"Activity\"\n colorScale=\"diverging\"\n data={[\n { x: 'Mon', y: 'AM', value: -2 },\n { x: 'Tue', y: 'AM', value: 0 },\n { x: 'Wed', y: 'AM', value: null },\n { x: 'Mon', y: 'AM', value: 40 }\n ]}\n />\n )\n const neutral = root.container.queryAll(instance => instance.type === 'Rect' && readProp(instance, 'fillOpacity') === 0)\n expect(neutral).toHaveLength(1)\n const missing = root.container.queryAll(instance => instance.type === 'Text' && readProp(instance, 'children') === '× Not available')\n expect(missing).toHaveLength(1)\n await act(async () => {\n callAction(readProp(findByAccessibilityRole(root, 'button'), 'onPress'), 'Missing chart disclosure')\n await Promise.resolve()\n })\n const rows = root.container.queryAll(instance => instance.type === 'Text').map(instance => readProp(instance, 'children'))\n expect(rows).toContain('Wed, AM: Not available')\n expect(rows).toContain('Tue, AM: 0')\n expect(rows).toContain('Mon, AM: -2')\n expect(rows).not.toContain('Mon, AM: 40')\n })\n\n test('opens and closes exact source values using a labeled disclosure', async () => {\n const root = await renderNative(\n <LumenWaterfallChart\n label=\"Revenue\"\n data={[\n { id: 'opening', label: 'Opening', kind: 'total', value: 100 },\n { id: 'cost', label: 'Costs', value: -25 }\n ]}\n />\n )\n const button = findByAccessibilityRole(root, 'button')\n expect(readProp(button, 'accessibilityState')).toMatchObject({ expanded: false })\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'list')).toHaveLength(0)\n await act(async () => {\n callAction(readProp(button, 'onPress'), 'Missing chart disclosure')\n await Promise.resolve()\n })\n expect(readProp(findByAccessibilityRole(root, 'button'), 'accessibilityState')).toMatchObject({ expanded: true })\n const rows = root.container.queryAll(instance => instance.type === 'Text').map(instance => readProp(instance, 'children'))\n expect(rows).toContain('Costs, Start: 100, End: 75, Value: -25')\n await act(async () => {\n callAction(readProp(findByAccessibilityRole(root, 'button'), 'onPress'), 'Missing chart disclosure')\n await Promise.resolve()\n })\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'list')).toHaveLength(0)\n })\n\n test('shows a localized invalid-data state rather than a partial histogram', async () => {\n const root = await renderNative(\n <LumenHistogram\n label=\"Distribution\"\n labels={{ invalidData: 'Check the bins' }}\n data={[\n { start: 0, end: 10, count: 5 }, { start: 5, end: 15, count: 3 }\n ]}\n />\n )\n expect(readProp(findByAccessibilityRole(root, 'alert'), 'children')).toBe('Check the bins')\n expect(root.container.queryAll(instance => instance.type === 'Svg')).toHaveLength(0)\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'button')).toHaveLength(0)\n })\n})\n\ndescribe('controlled multiple selection', () => {\n test('removal retains unavailable values and never mutates the controlled set', async () => {\n const values = new Set(['one', 'missing'])\n const onValuesChange = vi.fn<(values: ReadonlySet<string>) => void>()\n const root = await renderNative(<LumenMultiSelect label=\"Teams\" values={values} onValuesChange={onValuesChange} query=\"\" onQueryChange={() => undefined} options={[{ label: 'One', value: 'one' }]} />)\n const remove = root.container.queryAll(instance => readProp(instance, 'accessibilityLabel') === 'Remove One').find(instance => typeof readProp(instance, 'onPress') === 'function')\n if (!remove) throw new Error('Missing selected removal')\n const handler = readProp(remove, 'onPress')\n if (typeof handler !== 'function') throw new Error('Missing removal handler')\n await act(async () => {\n Reflect.apply(handler, undefined, [])\n await Promise.resolve()\n })\n expect(onValuesChange).toHaveBeenLastCalledWith(new Set(['missing']))\n expect(values).toEqual(new Set(['one', 'missing']))\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityLabel') === 'Remove missing').length).toBeGreaterThan(0)\n })\n\n test.each([{ readOnly: true }, { enabled: false }])('blocked controls reject removal: %o', async blocked => {\n const onValuesChange = vi.fn<(values: ReadonlySet<string>) => void>()\n const root = await renderNative(<LumenMultiSelect {...blocked} label=\"Teams\" values={new Set(['one'])} onValuesChange={onValuesChange} query=\"\" onQueryChange={() => undefined} options={[{ label: 'One', value: 'one' }]} />)\n const remove = root.container.queryAll(instance => readProp(instance, 'accessibilityLabel') === 'Remove One').find(instance => typeof readProp(instance, 'onPress') === 'function')\n if (!remove) throw new Error('Missing removal')\n const handler = readProp(remove, 'onPress')\n if (typeof handler !== 'function') throw new Error('Missing removal handler')\n await act(async () => {\n Reflect.apply(handler, undefined, [])\n await Promise.resolve()\n })\n expect(onValuesChange).not.toHaveBeenCalled()\n expect(readProp(remove, 'disabled')).toBe(true)\n })\n\n test('disabled selected results cannot be removed', async () => {\n const root = await renderNative(\n <LumenMultiSelect\n label=\"Teams\"\n values={new Set(['one'])}\n onValuesChange={() => {\n throw new Error('Disabled callback')\n }}\n query=\"\"\n onQueryChange={() => undefined}\n options={[{ label: 'One', value: 'one', disabled: true }]}\n />\n )\n const remove = root.container.queryAll(instance => readProp(instance, 'accessibilityLabel') === 'Remove One').find(instance => typeof readProp(instance, 'onPress') === 'function')\n if (!remove) throw new Error('Missing removal')\n expect(readProp(remove, 'disabled')).toBe(true)\n })\n})\n\ndescribe('multiple selection results', () => {\n test('checkbox intent retains values outside the result list', async () => {\n const onValuesChange = vi.fn<(values: ReadonlySet<string>) => void>()\n const values = new Set(['missing'])\n const root = await renderNative(<LumenMultiSelect label=\"Teams\" values={values} onValuesChange={onValuesChange} query=\"\" onQueryChange={() => undefined} options={[{ label: 'One', value: 'one' }]} />)\n const option = root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'checkbox').find(instance => typeof readProp(instance, 'onPress') === 'function')\n if (!option) throw new Error('Missing option')\n const handler = readProp(option, 'onPress')\n if (typeof handler !== 'function') throw new Error('Missing checkbox handler')\n await act(async () => {\n Reflect.apply(handler, undefined, [])\n await Promise.resolve()\n })\n expect(onValuesChange).toHaveBeenLastCalledWith(new Set(['missing', 'one']))\n expect(values).toEqual(new Set(['missing']))\n })\n\n test.each([{ loading: true }, { resultsErrorMessage: 'Search failed' }])('stale results are unavailable: %o', async state => {\n const root = await renderNative(<LumenMultiSelect {...state} label=\"Teams\" values={new Set<string>()} onValuesChange={() => undefined} query=\"\" onQueryChange={() => undefined} options={[{ label: 'One', value: 'one' }]} />)\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'checkbox')).toHaveLength(0)\n })\n})\n\ndescribe('multiple selection option identity', () => {\n test.each([\n { options: [{ label: 'One', value: '' }] },\n { options: [{ label: ' ', value: 'one' }] },\n { options: [{ label: 'One', value: 'one' }, { label: 'Duplicate', value: 'one' }] }\n ])('rejects invalid option identities: %o', async ({ options }) => {\n await expect(renderNative(<LumenMultiSelect label=\"Teams\" values={new Set<string>()} onValuesChange={() => undefined} query=\"\" onQueryChange={() => undefined} options={options} />)).rejects.toThrow('MultiSelect options require')\n })\n})\n\ntest('multiple selection forwards application safe-area insets to its modal', async () => {\n const root = await renderNative(<LumenMultiSelect label=\"Teams\" values={new Set<string>()} onValuesChange={() => undefined} query=\"\" onQueryChange={() => undefined} options={[]} safeAreaInsets={{ bottom: 96 }} />)\n const panels = root.container.queryAll(instance => {\n const style = readProp(instance, 'style')\n\n return typeof style === 'object' && style !== null && 'paddingBottom' in style && style.paddingBottom === 96\n })\n expect(panels.length).toBeGreaterThan(0)\n})\n\ndescribe('rating controlled selection', () => {\n test('clamps nonfinite values and limits the rendered option count', () => {\n expect(resolveLumenRating(Number.NaN, Number.POSITIVE_INFINITY)).toEqual({ max: 5, value: 0 })\n expect(resolveLumenRating(-5, 0)).toEqual({ max: 1, value: 0 })\n expect(resolveLumenRating(999, 999)).toEqual({ max: 100, value: 100 })\n expect(resolveLumenRating(3.9)).toEqual({ max: 5, value: 3 })\n })\n\n test('reports selection without mutating controlled value and blocks read-only edits', async () => {\n const onValueChange = vi.fn<(value: number) => void>()\n const root = await renderNative(\n <LumenRating\n label=\"Score\"\n value={2}\n onValueChange={onValueChange}\n formatOption={(value, max) => `${value} of ${max}`}\n />\n )\n const option = findByAccessibilityLabel(root, '4 of 5')\n act(() => {\n callAction(readProp(option, 'onPress'), 'Missing rating action')\n })\n expect(onValueChange).toHaveBeenCalledWith(4)\n expect(readProp(option, 'accessibilityState')).toEqual({ checked: false, disabled: false, selected: false })\n act(() => {\n root.render(\n <LumenProvider>\n <LumenRating\n label=\"Score\"\n value={2}\n readOnly\n onValueChange={onValueChange}\n />\n </LumenProvider>\n )\n })\n const locked = findByAccessibilityLabel(root, '4 / 5')\n act(() => {\n callAction(readProp(locked, 'onPress'), 'Missing rating action')\n })\n expect(onValueChange).toHaveBeenCalledTimes(1)\n expect(readProp(locked, 'disabled')).toBe(true)\n })\n})\n\ndescribe('progress and history contracts', () => {\n test('normalizes step progress without changing host state', () => {\n expect(resolveLumenStepState(0, -1, 3)).toBe('current')\n expect(resolveLumenStepState(0, Number.NaN, 3)).toBe('current')\n expect(resolveLumenStepState(1, 1.9, 3)).toBe('current')\n expect(resolveLumenStepState(2, 99, 3)).toBe('complete')\n expect(resolveLumenStepState(2, 1, 3)).toBe('upcoming')\n })\n\n test('presents localized controlled progression and accepts an empty workflow', async () => {\n const steps = [{ id: 'prepare', title: 'Prepare' }, { id: 'review', title: 'Review', description: 'Confirm details' }]\n const root = await renderNative(\n <LumenStepper\n label=\"Workflow\"\n steps={steps}\n currentStep={1}\n formatState={state => `Localized ${state}`}\n />\n )\n const review = findByAccessibilityLabel(root, '2 / 2, Review, Confirm details')\n expect(readProp(review, 'accessibilityValue')).toEqual({ text: 'Localized current' })\n expect(readProp(review, 'accessibilityState')).toEqual({ selected: true })\n act(() => {\n root.render(<LumenProvider><LumenStepper label=\"Workflow\" steps={steps} currentStep={2} /></LumenProvider>)\n })\n expect(readProp(findByAccessibilityLabel(root, '2 / 2, Review, Confirm details'), 'accessibilityValue'))\n .toEqual({ text: 'Complete' })\n act(() => {\n root.render(<LumenProvider><LumenStepper label=\"Workflow\" steps={[]} currentStep={0} /></LumenProvider>)\n })\n expect(root.container.queryAll(instance => readProp(instance, 'accessible') === true)).toHaveLength(0)\n })\n\n test('keeps timeline content actions accessible and host owned', async () => {\n const onPress = vi.fn()\n const root = await renderNative(\n <LumenTimeline label=\"History\">\n <LumenTimelineItem>\n <LumenText>Approved</LumenText>\n <LumenButton onPress={onPress}>View approval</LumenButton>\n </LumenTimelineItem>\n </LumenTimeline>\n )\n const button = findByAccessibilityRole(root, 'button')\n act(() => {\n callAction(readProp(button, 'onPress'), 'Missing timeline action')\n })\n expect(onPress).toHaveBeenCalledOnce()\n })\n\n test('breadcrumb reports navigation IDs and cannot navigate current or disabled locations', async () => {\n const onNavigate = vi.fn<(id: string) => void>()\n const items = [{ id: 'home', label: 'Home' },\n { id: 'private', label: 'Private', disabled: true },\n { id: 'current', label: 'Details' }]\n const root = await renderNative(\n <LumenBreadcrumb\n label=\"Location\"\n items={items}\n onNavigate={onNavigate}\n currentLabel=\"Here\"\n />\n )\n const home = findByAccessibilityLabel(root, 'Home')\n act(() => {\n callAction(readProp(home, 'onPress'), 'Missing breadcrumb action')\n })\n expect(onNavigate).toHaveBeenCalledExactlyOnceWith('home')\n const locked = findByAccessibilityLabel(root, 'Private')\n act(() => {\n callAction(readProp(locked, 'onPress'), 'Missing disabled breadcrumb action')\n })\n expect(readProp(findByAccessibilityLabel(root, 'Details, Here'), 'onPress')).toBeUndefined()\n expect(readProp(findByAccessibilityLabel(root, 'Details, Here'), 'aria-current')).toBe('page')\n })\n})\n\ndescribe('data table controlled interactions', () => {\n const columns = [{ key: 'amount', label: 'Amount', sortable: true }]\n const rows: readonly LumenTableRow[] = [\n { id: 'one', label: 'One', cells: { amount: { text: '20', sortValue: 20 } } },\n { id: 'two', label: 'Two', cells: { amount: { text: '2', sortValue: 2 } }, disabled: true }\n ]\n test('emits manual sort requests without reordering and retains hidden selections', async () => {\n const onSortChange = vi.fn()\n const onSelectionChange = vi.fn()\n const root = await renderNative(\n <LumenDataTable\n label=\"Records\"\n columns={columns}\n rows={rows}\n onSortChange={onSortChange}\n onSelectionChange={onSelectionChange}\n selectedIds={new Set(['hidden'])}\n />\n )\n act(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Amount'), 'onPress'), 'Missing sort action')\n })\n expect(onSortChange).toHaveBeenCalledWith({ key: 'amount', direction: 'ascending' })\n act(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Select visible'), 'onPress'), 'Missing bulk selection')\n })\n expect(onSelectionChange).toHaveBeenCalledWith(new Set(['hidden', 'one']))\n expect(root.container.queryAll(instance => readProp(instance, 'accessible') === true)\n .map(instance => readProp(instance, 'accessibilityLabel'))).toEqual(['Amount, 20', 'Amount, 2'])\n })\n test('loading and error states hide stale controls; retry remains host owned', async () => {\n const onRetry = vi.fn()\n const root = await renderNative(\n <LumenDataTable\n label=\"Records\"\n columns={columns}\n rows={rows}\n loading\n loadingLabel=\"Fetching\"\n onSortChange={vi.fn()}\n />\n )\n expect(readProp(findByAccessibilityRole(root, 'progressbar'), 'children')).toBe('Fetching')\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'button')).toHaveLength(0)\n act(() => {\n root.render(\n <LumenProvider>\n <LumenDataTable\n label=\"Records\"\n columns={columns}\n rows={rows}\n error=\"Try again\"\n onRetry={onRetry}\n retryLabel=\"Reload\"\n />\n </LumenProvider>\n )\n })\n act(() => {\n callAction(readProp(findByAccessibilityRole(root, 'button'), 'onPress'), 'Missing retry')\n })\n expect(onRetry).toHaveBeenCalledOnce()\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'checkbox')).toHaveLength(0)\n })\n test('read-only tables block both sort and selection callbacks', async () => {\n const onSortChange = vi.fn()\n const onSelectionChange = vi.fn()\n const root = await renderNative(\n <LumenDataTable\n label=\"Records\"\n columns={columns}\n rows={rows}\n readOnly\n onSortChange={onSortChange}\n onSelectionChange={onSelectionChange}\n />\n )\n act(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Amount'), 'onPress'), 'Missing guarded sort')\n })\n act(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Select visible'), 'onPress'), 'Missing guarded selection')\n })\n expect(onSortChange).not.toHaveBeenCalled()\n expect(onSelectionChange).not.toHaveBeenCalled()\n })\n})\n\ntest.each(['light', 'dark'] as const)('nested button content uses its %s foreground without overriding explicit tones', async scheme => {\n const theme = createLumenTheme(scheme)\n const root = await renderNative(\n <LumenProvider theme={theme}>\n <LumenButton>\n <LumenText accessibilityLabel=\"Inherited label\">Continue</LumenText>\n <LumenText accessibilityLabel=\"Explicit tone\" tone=\"danger\">Warning</LumenText>\n <LumenIcon name=\"search\" label=\"Inherited icon\" />\n </LumenButton>\n <LumenText accessibilityLabel=\"Outside control\">Outside</LumenText>\n </LumenProvider>\n )\n expect(readProp(findByAccessibilityLabel(root, 'Inherited label'), 'style')).toEqual(expect.arrayContaining([{ color: theme.colors.onBrand }]))\n expect(readProp(findByAccessibilityLabel(root, 'Explicit tone'), 'style')).toEqual(expect.arrayContaining([{ color: theme.colors.danger }]))\n expect(readProp(findByAccessibilityLabel(root, 'Outside control'), 'style')).toEqual(expect.arrayContaining([{ color: theme.colors.ink }]))\n const svg = findByAccessibilityLabel(root, 'Inherited icon').children[0]\n if (typeof svg !== 'object') throw new Error('Missing inherited icon content')\n expect(readProp(svg, 'stroke')).toBe(theme.colors.onBrand)\n})\n\ntest.each([false, true])('web checkbox Space toggles once and respects disabled=%s', async disabled => {\n nativePlatform.OS = 'web'\n const change = vi.fn<(checked: boolean) => void>()\n const hostKey = vi.fn()\n const root = await renderNative(<LumenCheckbox label=\"Choice\" checked disabled={disabled} onCheckedChange={change} onKeyDown={hostKey} />)\n const handler = readProp(findByAccessibilityRole(root, 'checkbox'), 'onKeyDown')\n if (typeof handler !== 'function') throw new Error('Missing checkbox keyboard handler')\n const preventDefault = vi.fn()\n await act(async () => {\n Reflect.apply(handler, undefined, [{ nativeEvent: { key: ' ', repeat: false }, defaultPrevented: false, preventDefault }])\n await Promise.resolve()\n })\n expect(hostKey).toHaveBeenCalledOnce()\n expect(preventDefault).toHaveBeenCalledOnce()\n expect(change).toHaveBeenCalledTimes(disabled ? 0 : 1)\n expect(change.mock.calls).toEqual(disabled ? [] : [[false]])\n await act(async () => {\n Reflect.apply(handler, undefined, [{ nativeEvent: { key: ' ', repeat: true }, defaultPrevented: false, preventDefault }])\n Reflect.apply(handler, undefined, [{ nativeEvent: { key: ' ', repeat: false }, defaultPrevented: true, preventDefault }])\n await Promise.resolve()\n })\n expect(change).toHaveBeenCalledTimes(disabled ? 0 : 1)\n})\n\ntest('media thumbnail keeps controlled selection and blocks loading requests', async () => {\n const onSelectionChange = vi.fn()\n const props = { label: 'Landscape', selected: true, onSelectionChange }\n const root = await renderNative(<LumenMediaThumbnail {...props}><LumenText>Photo</LumenText></LumenMediaThumbnail>)\n const button = findByAccessibilityLabel(root, 'Landscape')\n expect(readProp(button, 'accessibilityState')).toMatchObject({ selected: true })\n await runNativeAction(() => {\n callAction(readProp(button, 'onPress'), 'Missing thumbnail action')\n })\n expect(onSelectionChange).toHaveBeenCalledWith(false)\n await runNativeAction(() => {\n root.render(<LumenProvider><LumenMediaThumbnail {...props} state=\"loading\"><LumenText>Photo</LumenText></LumenMediaThumbnail></LumenProvider>)\n })\n const loading = findByAccessibilityLabel(root, 'Landscape')\n expect(readProp(loading, 'accessibilityState')).toMatchObject({ busy: true, disabled: true })\n await runNativeAction(() => {\n callAction(readProp(loading, 'onPress'), 'Missing loading action')\n })\n expect(onSelectionChange).toHaveBeenCalledTimes(1)\n})\n\ntest('native media inspection actions request bounded values and respect disabled state', async () => {\n const onValueChange = vi.fn()\n const props = { label: 'Inspect', value: { zoom: 2, x: 1, y: 0 }, onValueChange }\n const root = await renderNative(<LumenMediaViewport {...props}><LumenText>Photo</LumenText></LumenMediaViewport>)\n const right = root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'button')\n expect(right).toHaveLength(7)\n const fit = right[2]\n if (!fit) throw new Error('Missing fit action')\n await runNativeAction(() => {\n callAction(readProp(fit, 'onPress'), 'Missing fit handler')\n })\n expect(onValueChange).toHaveBeenCalledWith({ zoom: 1, x: 0, y: 0 })\n await runNativeAction(() => {\n root.render(\n <LumenProvider>\n <LumenMediaViewport {...props} disabled><LumenText>Photo</LumenText></LumenMediaViewport>\n </LumenProvider>\n )\n })\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'button').every(instance => {\n const state = readProp(instance, 'accessibilityState')\n return typeof state === 'object' && state !== null && 'disabled' in state && state.disabled === true\n })).toBe(true)\n})\n\ntest.each([true, false])('media pinch-to-pan preserves the host-accepted zoom (accept=%s)', async accept => {\n const change = vi.fn<(value: LumenMediaViewportValue) => void>()\n const Harness = () => {\n const [value, setValue] = useState<LumenMediaViewportValue>({ zoom: 2, x: 0, y: 0 })\n\n return (\n <LumenMediaViewport\n label=\"Inspect gesture\"\n value={value}\n onValueChange={next => {\n change(next)\n if (accept) setValue(next)\n }}\n >\n <LumenText>Photo</LumenText>\n </LumenMediaViewport>\n )\n }\n const root = await renderNative(<Harness />)\n const dispatch = async (property: string, nativeEvent: unknown): Promise<void> => {\n const handler = readProp(findByAccessibilityLabel(root, 'Inspect gesture'), property)\n if (typeof handler !== 'function') throw new Error(`Missing media ${property} callback`)\n await runNativeAction(() => {\n Reflect.apply(handler, undefined, [{ nativeEvent }])\n })\n }\n const touch = (pageX: number, pageY: number) => ({ pageX, pageY })\n\n await dispatch('onLayout', { layout: { width: 200, height: 100 } })\n await dispatch('onResponderGrant', { pageX: 50, pageY: 50, touches: [touch(50, 50), touch(100, 50)] })\n await dispatch('onResponderMove', { pageX: 50, pageY: 50, touches: [touch(50, 50), touch(150, 50)] })\n expect(change).toHaveBeenLastCalledWith({ zoom: 4, x: 0, y: 0 })\n change.mockClear()\n await dispatch('onResponderMove', { pageX: 150, pageY: 50, touches: [touch(150, 50)] })\n expect(change).not.toHaveBeenCalled()\n await dispatch('onResponderMove', { pageX: 175, pageY: 50, touches: [touch(175, 50)] })\n expect(change).toHaveBeenLastCalledWith({ zoom: accept ? 4 : 2, x: accept ? 1 / 12 : 0.25, y: 0 })\n})\n",
|
|
60417
60417
|
"packages/react-native/src/content-components.tsx": "import { type ReactElement, type ReactNode } from 'react'\nimport {\n type DimensionValue,\n Pressable,\n Text,\n type TextStyle,\n View,\n type ViewProps\n} from 'react-native'\nimport { Circle, Line, Polygon, Polyline, Rect, Svg } from 'react-native-svg'\n\nimport {\n type LumenBackdropIntensity,\n type LumenGraphicSize,\n type LumenIllustrationSize,\n resolveLumenBackdropOpacity,\n resolveLumenDisclosureState,\n resolveLumenGraphicSize,\n resolveLumenIllustrationSize,\n resolveLumenSkeletonHeight } from './content-recipes.js'\nimport {\n type LumenIllustrationElement,\n lumenIllustrations\n} from './illustrations.generated.js'\nimport type { LumenViewRef } from './native-ref-types.js'\nimport { resolveLumenButtonOpacity } from './recipes.js'\nimport { useLumenTheme } from './theme-context.js'\nimport { lumenGraphicOpacities, lumenGraphicStrokeWidths } from './tokens.generated.js'\n\nexport type LumenSkeletonShape = 'circle' | 'rectangle' | 'text'\n\nexport interface LumenSkeletonProps extends Omit<ViewProps, 'children'> {\n height?: number\n label?: string\n ref?: LumenViewRef\n shape?: LumenSkeletonShape\n width?: DimensionValue\n}\n\nexport const LumenSkeleton = ({\n height = 16,\n label,\n ref,\n shape = 'text',\n style,\n width,\n ...props\n}: LumenSkeletonProps): ReactElement => {\n const theme = useLumenTheme()\n const safeHeight = resolveLumenSkeletonHeight(height)\n const resolvedWidth = width ?? (shape === 'circle' ? safeHeight : '100%')\n let borderRadius: number = theme.radii.md\n\n if (shape === 'circle') borderRadius = safeHeight / 2\n\n if (shape === 'text') borderRadius = theme.radii.sm\n\n return (\n <View\n ref={ref}\n {...props}\n accessibilityElementsHidden={!label}\n accessibilityLabel={label}\n accessibilityRole={label ? 'progressbar' : undefined}\n aria-busy={label ? true : undefined}\n importantForAccessibility={label ? 'yes' : 'no-hide-descendants'}\n style={[\n {\n backgroundColor: theme.colors.surfaceStrong,\n borderRadius,\n height: safeHeight,\n opacity: 0.72,\n width: resolvedWidth\n },\n style\n ]}\n />\n )\n}\n\nexport type LumenGraphicTone = 'accent' | 'brand' | 'neutral'\nexport type LumenGraphicVariant = 'glow' | 'grid' | 'orbit'\n\nexport interface LumenGraphicProps extends ViewProps {\n children?: ReactNode\n label?: string\n ref?: LumenViewRef\n size?: LumenGraphicSize\n tone?: LumenGraphicTone\n variant?: LumenGraphicVariant\n}\n\nconst graphicLines = Array.from({ length: 7 }, (_, index) => index)\nconst backdropDots = Array.from({ length: 48 }, (_, index) => index)\n\nexport const LumenGraphic = ({\n children,\n label,\n ref,\n size = 'md',\n style,\n tone = 'brand',\n variant = 'orbit',\n ...props\n}: LumenGraphicProps): ReactElement => {\n const theme = useLumenTheme()\n const dimension = resolveLumenGraphicSize(size)\n\n const color = {\n accent: theme.colors.accent,\n brand: theme.colors.brand,\n neutral: theme.colors.inkMuted\n }[tone]\n\n return (\n <View\n ref={ref}\n {...props}\n accessible={Boolean(label)}\n accessibilityElementsHidden={!label}\n accessibilityLabel={label}\n accessibilityRole={label ? 'image' : undefined}\n importantForAccessibility={label ? 'yes' : 'no-hide-descendants'}\n style={[\n {\n alignItems: 'center',\n height: dimension,\n justifyContent: 'center',\n overflow: 'hidden',\n position: 'relative',\n width: dimension\n },\n style\n ]}\n >\n {variant === 'glow' ?\n <View style={{ backgroundColor: color, borderRadius: dimension / 2, height: '72%', opacity: 0.18, width: '72%' }} /> :\n null}\n {variant === 'grid' ?\n (\n <View style={{ height: '76%', opacity: 0.18, position: 'absolute', width: '76%' }}>\n {graphicLines.map(index => (\n <View\n key={`horizontal-${index}`}\n style={{ backgroundColor: color, height: 1, left: 0, position: 'absolute', right: 0, top: `${index * 16.66}%` }}\n />\n ))}\n {graphicLines.map(index => (\n <View\n key={`vertical-${index}`}\n style={{ backgroundColor: color, bottom: 0, left: `${index * 16.66}%`, position: 'absolute', top: 0, width: 1 }}\n />\n ))}\n </View>\n ) :\n null}\n {variant === 'orbit' ?\n (\n <>\n {[0.9, 0.62, 0.34].map(ratio => (\n <View\n key={ratio}\n style={{\n borderColor: color,\n borderRadius: dimension,\n borderWidth: 1,\n height: dimension * ratio,\n opacity: 0.24,\n position: 'absolute',\n width: dimension * ratio\n }}\n />\n ))}\n <View style={{ backgroundColor: color, height: 1, opacity: 0.18, position: 'absolute', width: '72%' }} />\n <View style={{ backgroundColor: color, height: '72%', opacity: 0.18, position: 'absolute', width: 1 }} />\n </>\n ) :\n null}\n <View style={{ alignItems: 'center', justifyContent: 'center', position: 'absolute' }}>\n {children}\n </View>\n </View>\n )\n}\n\nexport type LumenBackdropTone = 'accent' | 'brand' | 'neutral'\nexport type LumenBackdropVariant = 'aurora' | 'dots' | 'grid' | 'rays'\n\nexport interface LumenBackdropProps extends ViewProps {\n children?: ReactNode\n intensity?: LumenBackdropIntensity\n ref?: LumenViewRef\n tone?: LumenBackdropTone\n variant?: LumenBackdropVariant\n}\n\nexport const LumenBackdrop = ({\n children,\n intensity = 'medium',\n ref,\n style,\n tone = 'brand',\n variant = 'aurora',\n ...props\n}: LumenBackdropProps): ReactElement => {\n const theme = useLumenTheme()\n const color = { accent: theme.colors.accent, brand: theme.colors.brand, neutral: theme.colors.inkMuted }[tone]\n const secondaryColor = tone === 'neutral' ? theme.colors.surfaceStrong : theme.colors.accent\n const opacity = resolveLumenBackdropOpacity(intensity)\n\n return (\n <View ref={ref} {...props} style={[{ minHeight: 160, overflow: 'hidden', position: 'relative' }, style]}>\n <View\n accessibilityElementsHidden\n importantForAccessibility=\"no-hide-descendants\"\n style={{ bottom: 0, left: 0, opacity, pointerEvents: 'none', position: 'absolute', right: 0, top: 0 }}\n >\n {variant === 'aurora' ?\n (\n <>\n <View style={{ backgroundColor: color, borderRadius: 140, height: 280, left: -70, opacity: 0.22, position: 'absolute', top: -100, width: 280 }} />\n <View style={{ backgroundColor: secondaryColor, borderRadius: 120, bottom: -110, height: 240, opacity: 0.18, position: 'absolute', right: -50, width: 240 }} />\n </>\n ) :\n null}\n {variant === 'grid' ?\n (\n <>\n {graphicLines.map(index => (\n <View key={`backdrop-horizontal-${index}`} style={{ backgroundColor: color, height: 1, left: 0, opacity: 0.18, position: 'absolute', right: 0, top: `${index * 16.66}%` }} />\n ))}\n {graphicLines.map(index => (\n <View key={`backdrop-vertical-${index}`} style={{ backgroundColor: color, bottom: 0, left: `${index * 16.66}%`, opacity: 0.18, position: 'absolute', top: 0, width: 1 }} />\n ))}\n </>\n ) :\n null}\n {variant === 'dots' ?\n backdropDots.map(index => (\n <View\n key={`backdrop-dot-${index}`}\n style={{\n backgroundColor: color,\n borderRadius: 2,\n height: 3,\n left: `${(index % 8) * 14 + 1}%`,\n opacity: 0.28,\n position: 'absolute',\n top: `${Math.floor(index / 8) * 19 + 2}%`,\n width: 3\n }}\n />\n )) :\n null}\n {variant === 'rays' ?\n graphicLines.map(index => (\n <View\n key={`backdrop-ray-${index}`}\n style={{\n backgroundColor: color,\n height: 1,\n left: '50%',\n opacity: 0.2,\n position: 'absolute',\n top: '50%',\n transform: [{ rotate: `${index * 24 - 72}deg` }, { translateX: -240 }],\n width: 480\n }}\n />\n )) :\n null}\n </View>\n <View style={{ zIndex: 1 }}>{children}</View>\n </View>\n )\n}\n\nexport type LumenIllustrationTone = 'accent' | 'auto' | 'brand' | 'neutral'\nexport type LumenIllustrationVariant = 'empty' | 'error' | 'offline' | 'success'\n\nexport interface LumenIllustrationProps extends ViewProps {\n label?: string\n ref?: LumenViewRef\n size?: LumenIllustrationSize\n tone?: LumenIllustrationTone\n variant?: LumenIllustrationVariant\n}\n\nconst illustrationPoints = (points: readonly number[]): string => {\n const pairs: string[] = []\n\n for (let index = 0; index < points.length; index += 2) {\n pairs.push(`${String(points[index])},${String(points[index + 1])}`)\n }\n\n return pairs.join(' ')\n}\n\nconst renderIllustrationElement = (\n element: LumenIllustrationElement,\n color: string,\n index: number,\n strokeWidth: number\n): ReactElement => {\n const shared = {\n fill: 'none',\n stroke: color,\n strokeLinecap: 'round' as const,\n strokeLinejoin: 'round' as const,\n strokeWidth\n }\n\n if (element.kind === 'circle') {\n return <Circle key={index} {...shared} cx={element.cx} cy={element.cy} r={element.r} />\n }\n\n if (element.kind === 'rounded-rect') {\n return (\n <Rect\n key={index}\n {...shared}\n height={element.height}\n rx={element.radius}\n transform={`translate(${element.x} ${element.y})`}\n width={element.width}\n />\n )\n }\n\n const points = illustrationPoints(element.points)\n\n if (element.kind === 'line') {\n return (\n <Line\n key={index}\n {...shared}\n x1={element.points[0]}\n x2={element.points[2]}\n y1={element.points[1]}\n y2={element.points[3]}\n />\n )\n }\n\n return element.kind === 'polygon' ?\n <Polygon key={index} {...shared} points={points} /> :\n <Polyline key={index} {...shared} points={points} />\n}\n\nexport const LumenIllustration = ({\n label,\n ref,\n size = 'md',\n style,\n tone = 'auto',\n variant = 'empty',\n ...props\n}: LumenIllustrationProps): ReactElement => {\n const theme = useLumenTheme()\n const dimension = resolveLumenIllustrationSize(size)\n\n const semanticColor = {\n empty: theme.colors.brand,\n error: theme.colors.danger,\n offline: theme.colors.inkMuted,\n success: theme.colors.success\n }[variant]\n\n const color = tone === 'auto' ?\n semanticColor :\n {\n accent: theme.colors.accent,\n brand: theme.colors.brand,\n neutral: theme.colors.inkMuted\n }[tone]\n\n const strokeWidth = lumenGraphicStrokeWidths.standard\n const artwork = lumenIllustrations[variant]\n\n return (\n <View\n ref={ref}\n {...props}\n accessible={Boolean(label)}\n accessibilityElementsHidden={!label}\n accessibilityLabel={label}\n accessibilityRole={label ? 'image' : undefined}\n importantForAccessibility={label ? 'yes' : 'no-hide-descendants'}\n style={[{ alignItems: 'center', height: dimension, justifyContent: 'center', position: 'relative', width: dimension }, style]}\n >\n <Svg height={dimension} viewBox=\"0 0 120 120\" width={dimension}>\n <Circle\n cx=\"60\"\n cy=\"60\"\n fill={color}\n fillOpacity={lumenGraphicOpacities.wash}\n r=\"48\"\n />\n {artwork.elements.map((element, index) => renderIllustrationElement(element, color, index, strokeWidth))}\n </Svg>\n </View>\n )\n}\n\nexport interface LumenDisclosureProps extends Omit<ViewProps, 'children'> {\n children: ReactNode\n description?: string\n disabled?: boolean\n expanded: boolean\n graphic?: ReactNode\n onExpandedChange: (expanded: boolean) => void\n ref?: LumenViewRef\n title: string\n}\n\nexport const LumenDisclosure = ({\n children,\n description,\n disabled = false,\n expanded,\n graphic,\n onExpandedChange,\n ref,\n style,\n title,\n ...props\n}: LumenDisclosureProps): ReactElement => {\n const theme = useLumenTheme()\n const state = resolveLumenDisclosureState(expanded, disabled)\n\n return (\n <View\n ref={ref}\n {...props}\n style={[\n {\n backgroundColor: theme.colors.surface,\n borderColor: theme.colors.line,\n borderRadius: theme.radii.md,\n borderWidth: 1,\n overflow: 'hidden'\n },\n style\n ]}\n >\n <Pressable\n accessibilityRole=\"button\"\n accessibilityState={{ disabled, expanded }}\n aria-disabled={disabled}\n aria-expanded={expanded}\n disabled={disabled}\n onPress={() => {\n onExpandedChange(!expanded)\n }}\n style={({ pressed }) => ({\n alignItems: 'center',\n flexDirection: 'row',\n gap: theme.spacing.md,\n minHeight: 48,\n opacity: state.opacity * resolveLumenButtonOpacity(disabled, pressed),\n paddingHorizontal: theme.spacing.lg,\n paddingVertical: theme.spacing.md\n })}\n >\n {graphic ? <View>{graphic}</View> : null}\n <View style={{ flex: 1, gap: theme.spacing.xs }}>\n <Text\n style={{\n color: theme.colors.ink,\n fontSize: theme.fontSizes.sm,\n fontWeight: String(theme.fontWeights.semibold) as TextStyle['fontWeight']\n }}\n >\n {title}\n </Text>\n {description ?\n <Text style={{ color: theme.colors.inkMuted, fontSize: theme.fontSizes.xs }}>{description}</Text> :\n null}\n </View>\n <Text\n accessibilityElementsHidden\n importantForAccessibility=\"no-hide-descendants\"\n style={{ color: theme.colors.inkSoft, fontSize: theme.fontSizes.lg }}\n >\n {expanded ? '−' : '+'}\n </Text>\n </Pressable>\n {state.contentVisible ?\n (\n <View\n style={{\n borderTopColor: theme.colors.line,\n borderTopWidth: 1,\n padding: theme.spacing.lg\n }}\n >\n {children}\n </View>\n ) :\n null}\n </View>\n )\n}\n",
|
|
60418
60418
|
"packages/react-native/src/content-recipes.test.ts": "import { describe, expect, test } from 'vitest'\n\nimport {\n resolveLumenBackdropOpacity,\n resolveLumenDisclosureState,\n resolveLumenGraphicSize,\n resolveLumenIllustrationSize,\n resolveLumenSkeletonHeight\n} from './content-recipes.js'\n\ndescribe('content state recipes', () => {\n test('preserves controlled disclosure state', () => {\n expect(resolveLumenDisclosureState(true, false)).toEqual({\n contentVisible: true,\n disabled: false,\n opacity: 1\n })\n expect(resolveLumenDisclosureState(false, true)).toEqual({\n contentVisible: false,\n disabled: true,\n opacity: 0.52\n })\n })\n\n test('normalizes invalid skeleton heights', () => {\n expect(resolveLumenSkeletonHeight(24)).toBe(24)\n expect(resolveLumenSkeletonHeight(0)).toBe(16)\n expect(resolveLumenSkeletonHeight(Number.NaN)).toBe(16)\n })\n\n test('keeps graphic sizes aligned with the shared contract', () => {\n expect(resolveLumenGraphicSize('sm')).toBe(160)\n expect(resolveLumenGraphicSize('md')).toBe(240)\n expect(resolveLumenGraphicSize('lg')).toBe(320)\n })\n\n test('normalizes backdrop intensity and illustration sizes', () => {\n expect(resolveLumenBackdropOpacity('subtle')).toBe(0.4)\n expect(resolveLumenBackdropOpacity('medium')).toBe(0.68)\n expect(resolveLumenBackdropOpacity('strong')).toBe(1)\n expect(resolveLumenIllustrationSize('sm')).toBe(96)\n expect(resolveLumenIllustrationSize('md')).toBe(128)\n expect(resolveLumenIllustrationSize('lg')).toBe(176)\n })\n})\n",
|
|
60419
60419
|
"packages/react-native/src/content-recipes.ts": "import { lumenGraphicFrameSizes, lumenIllustrationSizes } from './tokens.generated.js'\n\nexport interface LumenDisclosureState {\n contentVisible: boolean\n disabled: boolean\n opacity: number\n}\n\nexport const resolveLumenDisclosureState = (\n expanded: boolean,\n disabled: boolean\n): LumenDisclosureState => ({\n contentVisible: expanded,\n disabled,\n opacity: disabled ? 0.52 : 1\n})\n\nexport const resolveLumenSkeletonHeight = (height: number): number => (\n Number.isFinite(height) && height > 0 ? height : 16\n)\n\nexport type LumenGraphicSize = 'lg' | 'md' | 'sm'\n\nexport const resolveLumenGraphicSize = (size: LumenGraphicSize): number => ({\n lg: lumenGraphicFrameSizes.lg,\n md: lumenGraphicFrameSizes.md,\n sm: lumenGraphicFrameSizes.sm\n})[size]\n\nexport type LumenBackdropIntensity = 'medium' | 'strong' | 'subtle'\n\nexport const resolveLumenBackdropOpacity = (\n intensity: LumenBackdropIntensity\n): number => ({ medium: 0.68, strong: 1, subtle: 0.4 })[intensity]\n\nexport type LumenIllustrationSize = 'lg' | 'md' | 'sm'\n\nexport const resolveLumenIllustrationSize = (\n size: LumenIllustrationSize\n): number => ({\n lg: lumenIllustrationSizes.lg,\n md: lumenIllustrationSizes.md,\n sm: lumenIllustrationSizes.sm\n})[size]\n",
|
|
60420
60420
|
"packages/react-native/src/control-content-context.ts": "import { createContext, use } from 'react'\nimport type { ColorValue } from 'react-native'\n\n/** Default foreground for content nested inside a themed control. */\nexport const LumenControlContentContext = createContext<ColorValue | null>(null)\n\nexport const useLumenControlContentColor = (): ColorValue | null => use(LumenControlContentContext)\n",
|
|
60421
60421
|
"packages/react-native/src/data-table-components.test.tsx": "import { act, createElement, type ReactElement } from 'react'\n\nimport { createRoot, type Root, type TestInstance } from 'test-renderer'\nimport { afterEach, expect, test, vi } from 'vitest'\n\nimport { LumenDataTable, type LumenDataTableProps } from './table-components.js'\n\nObject.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })\nvi.mock('react-native', () => ({\n View: (props: Record<string, unknown>): ReactElement => createElement('View', props),\n ScrollView: (props: Record<string, unknown>): ReactElement => createElement('ScrollView', props)\n}))\nvi.mock('./primitives.js', () => ({\n LumenButton: (props: Record<string, unknown>): ReactElement => createElement('Button', props),\n LumenText: (props: Record<string, unknown>): ReactElement => createElement('Text', props)\n}))\nvi.mock('./selection-components.js', () => ({\n LumenCheckbox: (props: Record<string, unknown>): ReactElement => createElement('Checkbox', props)\n}))\nvi.mock('./theme-context.js', () => ({ useLumenTheme: () => ({ spacing: { xs: 4, sm: 8, md: 16 },\n radii: { md: 8 },\n colors: { line: '#888' } }) }))\nconst roots: Root[] = []\nconst columns = [{ key: 'amount', label: 'Amount', sortable: true }]\nconst rows = [\n { id: 'large', label: 'Large', cells: { amount: { text: '20 items', sortValue: 20 } } },\n { id: 'small', label: 'Small', cells: { amount: { text: '2 items', sortValue: 2 } } },\n { id: 'locked', label: 'Locked', disabled: true, cells: { amount: { text: 'Missing', sortValue: null } } }\n]\nafterEach(() => {\n act(() => {\n for (const root of roots) root.unmount()\n })\n roots.length = 0\n})\nconst read = (instance: TestInstance, key: string): unknown => (instance.props as Record<string, unknown>)[key]\nconst render = (props: Partial<LumenDataTableProps> = {}): {\n root: Root\n selection: ReturnType<typeof vi.fn>\n sort: ReturnType<typeof vi.fn>\n} => {\n const root = createRoot()\n const selection = vi.fn()\n const sort = vi.fn()\n\n roots.push(root)\n act(() => {\n root.render(\n <LumenDataTable\n label=\"Synthetic records\"\n rows={rows}\n columns={columns}\n selectedIds={new Set(['hidden', 'locked'])}\n onSelectionChange={selection}\n onSortChange={sort}\n {...props}\n />\n )\n })\n\n return { root, selection, sort }\n}\nconst find = (root: Root, type: string, label: string): TestInstance => {\n const item = root.container.queryAll(instance => instance.type === type && read(instance, 'accessibilityLabel') === label)[0]\n\n if (!item) throw new Error(`Missing ${label}`)\n\n return item\n}\nconst invoke = (instance: TestInstance, key: string, args: readonly unknown[] = []): void => {\n const handler = read(instance, key)\n\n if (typeof handler !== 'function') throw new Error(`Missing ${key}`)\n act(() => {\n Reflect.apply(handler, undefined, args)\n })\n}\nconst order = (root: Root): readonly unknown[] => root.container.queryAll(item => item.type === 'Checkbox').map(item => read(item, 'label'))\n\ntest.each([null, [], ['small'], {}, new Set([42])])('malformed controlled selection renders invalid feedback (%#)', selectedIds => {\n const props: Partial<LumenDataTableProps> = { onSelectionChange: undefined, invalidLabel: 'Invalid selection' }\n Reflect.set(props, 'selectedIds', selectedIds)\n const { root } = render(props)\n expect(root.container.queryAll(item => item.type === 'Text').some(item => item.children.includes('Invalid selection'))).toBe(true)\n expect(root.container.queryAll(item => item.type === 'Checkbox')).toHaveLength(0)\n})\n\ntest('client sorting changes displayed stable rows, localizes status and proposes controlled next sort', () => {\n const { root, sort } = render({ sortMode: 'client',\n sort: { key: 'amount', direction: 'ascending' },\n formatSort: value => value?.direction === 'ascending' ? 'Increasing' : 'Decreasing' })\n\n expect(order(root)).toEqual(['Small', 'Large', 'Locked'])\n expect(find(root, 'Button', 'Amount, Increasing')).toBeDefined()\n invoke(find(root, 'Button', 'Amount, Increasing'), 'onPress')\n expect(sort).toHaveBeenCalledWith({ key: 'amount', direction: 'descending' })\n expect(order(root)).toEqual(['Small', 'Large', 'Locked'])\n expect(rows.map(row => row.id)).toEqual(['large', 'small', 'locked'])\n})\n\ntest('manual sorting retains host order while emitting a sort request', () => {\n const { root, sort } = render({ sortMode: 'manual', sort: { key: 'amount', direction: 'ascending' } })\n\n expect(order(root)).toEqual(['Large', 'Small', 'Locked'])\n invoke(find(root, 'Button', 'Amount, ascending'), 'onPress')\n expect(sort).toHaveBeenCalledWith({ key: 'amount', direction: 'descending' })\n expect(order(root)).toEqual(['Large', 'Small', 'Locked'])\n})\n\ntest('bulk and individual selection preserve hidden and disabled IDs without mutating controlled input', () => {\n const ids = new Set(['hidden', 'locked'])\n const { root, selection } = render({ selectedIds: ids })\n\n invoke(find(root, 'Button', 'Select visible'), 'onPress')\n expect(selection).toHaveBeenLastCalledWith(new Set(['hidden', 'locked', 'large', 'small']))\n invoke(find(root, 'Checkbox', 'Small'), 'onCheckedChange', [true])\n expect(selection).toHaveBeenLastCalledWith(new Set(['hidden', 'locked', 'small']))\n invoke(find(root, 'Checkbox', 'Locked'), 'onCheckedChange', [false])\n expect(selection).toHaveBeenCalledTimes(2)\n expect(ids).toEqual(new Set(['hidden', 'locked']))\n const selected = render({ selectedIds: new Set(['hidden', 'locked', 'large', 'small']), deselectAllLabel: 'Clear visible' })\n\n invoke(find(selected.root, 'Button', 'Clear visible'), 'onPress')\n expect(selected.selection).toHaveBeenCalledWith(ids)\n})\n\ntest('disabled and readOnly block sort and selection proposals even for retained selected rows', () => {\n for (const props of [{ disabled: true }, { readOnly: true }]) {\n const { root, selection, sort } = render(props)\n\n invoke(find(root, 'Button', 'Amount'), 'onPress')\n invoke(find(root, 'Button', 'Select visible'), 'onPress')\n invoke(find(root, 'Checkbox', 'Small'), 'onCheckedChange', [true])\n expect(selection).not.toHaveBeenCalled()\n expect(sort).not.toHaveBeenCalled()\n expect(read(find(root, 'Checkbox', 'Small'), 'disabled')).toBe(true)\n }\n})\n\ntest('loading, even an empty error, invalid IDs and empty data replace stale controls', () => {\n for (const props of [{ loading: true }, { error: '' }, { rows: [rows[0], rows[0]].filter(row => row !== undefined) }, { rows: [] }]) {\n const { root, selection, sort } = render(props)\n\n expect(root.container.queryAll(item => item.type === 'Button' || item.type === 'Checkbox')).toHaveLength(0)\n expect(selection).not.toHaveBeenCalled()\n expect(sort).not.toHaveBeenCalled()\n }\n})\n\ntest('localized error recovery remains host-owned and disabled retry stays disabled', () => {\n const retry = vi.fn()\n const { root } = render({ error: 'Unavailable data', retryLabel: 'Try records again', onRetry: retry, disabled: true })\n\n expect(read(find(root, 'Button', 'Try records again'), 'disabled')).toBe(true)\n expect(retry).not.toHaveBeenCalled()\n const enabled = render({ error: 'Unavailable data', retryLabel: 'Try records again', onRetry: retry })\n\n invoke(find(enabled.root, 'Button', 'Try records again'), 'onPress')\n expect(retry).toHaveBeenCalledOnce()\n})\n\ntest('horizontal layout retains sort controls and inherited prototype cells use the missing label', () => {\n const { root } = render({ layout: 'scroll',\n columns: [{ key: 'toString', label: 'Prototype name', sortable: true }],\n rows: [{ id: 'one', label: 'One', cells: {} }],\n missingLabel: 'No cell value' })\n\n expect(root.container.queryAll(item => item.type === 'ScrollView' && read(item, 'horizontal') === true)).toHaveLength(1)\n expect(find(root, 'Button', 'Prototype name')).toBeDefined()\n expect(root.container.queryAll(item => item.type === 'Text' && read(item, 'children') === 'No cell value')).toHaveLength(1)\n})\n",
|
|
60422
|
-
"packages/react-native/src/datetime-components.tsx": "import {\n type ChangeEvent,\n createElement,\n type ReactElement,\n useEffect,\n useState\n} from 'react'\nimport {\n Platform,\n Pressable,\n Text,\n type TextStyle,\n View,\n type ViewProps\n} from 'react-native'\n\nimport {\n DateTimePickerAndroid,\n type DateTimePickerChangeEvent\n} from '@react-native-community/datetimepicker'\n\nimport {\n clampLumenDate,\n formatLumenDateDisplayValue,\n formatLumenDateInputValue,\n isValidLumenDate,\n type LumenDateRangeValue,\n parseLumenDateInputValue,\n resolveLumenDateBounds,\n resolveLumenDatePickerValue,\n resolveLumenDateRangeEndChange,\n resolveLumenDateRangeEndMinimum,\n resolveLumenDateRangeStartChange\n} from './form-recipes.js'\nimport { NativeDatePicker, type NativeDatePickerProps } from './native-date-picker.js'\nimport {\n resolveLumenAriaInvalid,\n resolveLumenButtonOpacity,\n resolveLumenValidationHint\n} from './recipes.js'\nimport { useLumenTheme } from './theme-context.js'\n\nconst defaultDateFormatter = new Intl.DateTimeFormat(undefined, { dateStyle: 'medium' })\n\ninterface DateSupportingTextProps {\n description?: string | undefined\n errorMessage?: string | undefined\n}\n\nconst resolveDateLabelColor = (\n danger: string,\n ink: string,\n errorMessage?: string\n): string => errorMessage ? danger : ink\n\nconst resolveDatePickerExpanded = (enabled: boolean, expanded: boolean): boolean => (\n enabled && expanded\n)\n\nconst dismissAndroidDatePicker = (): void => {\n if (Platform.OS === 'android') void DateTimePickerAndroid.dismiss('date')\n}\n\nconst DateSupportingText = ({ description, errorMessage }: DateSupportingTextProps): ReactElement | null => {\n const theme = useLumenTheme()\n const supportingText = errorMessage ?? description\n\n if (!supportingText) return null\n\n return (\n <Text\n accessibilityLiveRegion={errorMessage ? 'polite' : 'none'}\n style={{\n color: errorMessage ? theme.colors.danger : theme.colors.inkMuted,\n fontSize: theme.fontSizes.xs\n }}\n >\n {supportingText}\n </Text>\n )\n}\n\ninterface NativeDatePickerDisclosureProps extends NativeDatePickerProps {\n expanded: boolean\n label: string\n onDateChange: (date: Date) => void\n webValue: Date | null\n}\n\nconst NativeDatePickerDisclosure = ({\n expanded,\n label,\n onDateChange,\n webValue,\n ...props\n}: NativeDatePickerDisclosureProps): ReactElement | null => {\n if (!expanded || Platform.OS === 'android') return null\n\n if (Platform.OS === 'web') {\n return createElement('input', {\n 'aria-label': label,\n disabled: props.disabled,\n max: props.maximumDate ? formatLumenDateInputValue(props.maximumDate) : undefined,\n min: props.minimumDate ? formatLumenDateInputValue(props.minimumDate) : undefined,\n onChange: (event: ChangeEvent<HTMLInputElement>) => {\n const date = parseLumenDateInputValue(event.currentTarget.value)\n\n if (date) onDateChange(date)\n },\n style: {\n accentColor: props.accentColor,\n border: '1px solid currentColor',\n borderRadius: 6,\n colorScheme: props.themeVariant,\n font: 'inherit',\n minHeight: 44,\n padding: '0 12px'\n },\n type: 'date',\n value: webValue ? formatLumenDateInputValue(webValue) : ''\n })\n }\n\n return <NativeDatePicker {...props} />\n}\n\nexport interface LumenDateFieldProps extends Omit<ViewProps, 'children'> {\n description?: string\n enabled?: boolean\n errorMessage?: string\n formatValue?: (date: Date) => string\n label: string\n maximumDate?: Date\n minimumDate?: Date\n onValueChange: (value: Date) => void\n placeholder?: string\n value: Date | null\n}\n\nconst LumenDateFieldControl = ({\n description,\n enabled = true,\n errorMessage,\n formatValue,\n label,\n maximumDate,\n minimumDate,\n onValueChange,\n placeholder = 'Select a date',\n style,\n value,\n ...props\n}: LumenDateFieldProps): ReactElement => {\n const theme = useLumenTheme()\n const [expanded, setExpanded] = useState(false)\n const pickerExpanded = resolveDatePickerExpanded(enabled, expanded)\n const validValue = isValidLumenDate(value) ? value : null\n const pickerValue = resolveLumenDatePickerValue(value, minimumDate, maximumDate)\n\n const displayValue = formatLumenDateDisplayValue(\n value,\n placeholder,\n formatValue ?? defaultDateFormatter.format.bind(defaultDateFormatter)\n )\n\n const pickerBounds = resolveLumenDateBounds(minimumDate, maximumDate)\n\n useEffect(() => dismissAndroidDatePicker, [])\n\n const commitValue = (_event: DateTimePickerChangeEvent, selectedDate?: Date): void => {\n if (!selectedDate) return\n\n onValueChange(clampLumenDate(selectedDate, minimumDate, maximumDate))\n }\n\n const openPicker = (): void => {\n if (!enabled) return\n\n if (Platform.OS === 'android') {\n DateTimePickerAndroid.open({\n ...pickerBounds,\n mode: 'date',\n onChange: commitValue,\n value: pickerValue\n })\n\n return\n }\n\n setExpanded(previous => !previous)\n }\n\n return (\n <View {...props} style={[{ gap: theme.spacing.xs }, style]}>\n <Text\n style={{\n color: errorMessage ? theme.colors.danger : theme.colors.ink,\n fontSize: theme.fontSizes.sm,\n fontWeight: String(theme.fontWeights.semibold) as TextStyle['fontWeight']\n }}\n >\n {label}\n </Text>\n <Pressable\n accessibilityHint={resolveLumenValidationHint(errorMessage, description)}\n accessibilityLabel={`${label}, ${displayValue}`}\n accessibilityRole=\"button\"\n accessibilityState={{ disabled: !enabled, expanded: pickerExpanded }}\n aria-invalid={resolveLumenAriaInvalid(errorMessage)}\n disabled={!enabled}\n onPress={openPicker}\n style={({ pressed }) => ({\n alignItems: 'center',\n backgroundColor: theme.colors.surface,\n borderColor: errorMessage ? theme.colors.danger : theme.colors.line,\n borderRadius: theme.radii.sm,\n borderWidth: 1,\n flexDirection: 'row',\n minHeight: 44,\n opacity: enabled ? resolveLumenButtonOpacity(false, pressed) : 0.52,\n paddingHorizontal: theme.spacing.md\n })}\n >\n <Text\n style={{\n color: validValue ? theme.colors.ink : theme.colors.inkMuted,\n flex: 1,\n fontSize: theme.fontSizes.sm\n }}\n >\n {displayValue}\n </Text>\n <Text\n accessibilityElementsHidden\n importantForAccessibility=\"no-hide-descendants\"\n style={{ color: theme.colors.inkMuted }}\n >\n ▾\n </Text>\n </Pressable>\n <NativeDatePickerDisclosure\n {...pickerBounds}\n accentColor={theme.colors.brandSolid}\n disabled={!enabled}\n display={Platform.OS === 'ios' ? 'inline' : 'default'}\n expanded={pickerExpanded}\n label={label}\n mode=\"date\"\n onDateChange={selectedDate => {\n onValueChange(clampLumenDate(selectedDate, minimumDate, maximumDate))\n }}\n onChange={commitValue}\n themeVariant={theme.scheme}\n value={pickerValue}\n webValue={validValue}\n />\n <DateSupportingText\n {...(description ? { description } : {})}\n {...(errorMessage ? { errorMessage } : {})}\n />\n </View>\n )\n}\n\nexport const LumenDateField = (props: LumenDateFieldProps): ReactElement => (\n <LumenDateFieldControl key={String(props.enabled ?? true)} {...props} />\n)\n\nexport interface LumenDateRangeFieldProps extends Omit<ViewProps, 'children'> {\n description?: string\n enabled?: boolean\n endLabel?: string\n errorMessage?: string\n formatValue?: (date: Date) => string\n label: string\n maximumDate?: Date\n minimumDate?: Date\n onValueChange: (value: LumenDateRangeValue) => void\n placeholder?: string\n startLabel?: string\n value: LumenDateRangeValue\n}\n\nconst resolveSharedDateFieldProps = (\n enabled: boolean,\n formatValue?: (date: Date) => string,\n maximumDate?: Date,\n placeholder?: string\n): Pick<LumenDateFieldProps, 'enabled' | 'formatValue' | 'maximumDate' | 'placeholder'> => ({\n enabled,\n ...(formatValue ? { formatValue } : {}),\n ...(maximumDate ? { maximumDate } : {}),\n ...(placeholder ? { placeholder } : {})\n})\n\nexport const LumenDateRangeField = ({\n description,\n enabled = true,\n endLabel = 'End date',\n errorMessage,\n formatValue,\n label,\n maximumDate,\n minimumDate,\n onValueChange,\n placeholder,\n startLabel = 'Start date',\n style,\n value,\n ...props\n}: LumenDateRangeFieldProps): ReactElement => {\n const theme = useLumenTheme()\n const endMinimum = resolveLumenDateRangeEndMinimum(value.start, minimumDate)\n const labelColor = resolveDateLabelColor(theme.colors.danger, theme.colors.ink, errorMessage)\n const sharedFieldProps = resolveSharedDateFieldProps(enabled, formatValue, maximumDate, placeholder)\n\n return (\n <View\n {...props}\n accessibilityHint={resolveLumenValidationHint(errorMessage, description, props.accessibilityHint)}\n accessibilityLabel={props.accessibilityLabel ?? label}\n aria-invalid={resolveLumenAriaInvalid(errorMessage)}\n style={[{ gap: theme.spacing.sm }, style]}\n >\n <Text\n style={{\n color: labelColor,\n fontSize: theme.fontSizes.sm,\n fontWeight: String(theme.fontWeights.semibold) as TextStyle['fontWeight']\n }}\n >\n {label}\n </Text>\n <LumenDateField\n {...sharedFieldProps}\n label={startLabel}\n {...(minimumDate ? { minimumDate } : {})}\n onValueChange={start => {\n onValueChange(resolveLumenDateRangeStartChange(value, start, minimumDate, maximumDate))\n }}\n value={value.start}\n />\n <LumenDateField\n {...sharedFieldProps}\n label={endLabel}\n {...(endMinimum ? { minimumDate: endMinimum } : {})}\n onValueChange={end => {\n onValueChange(resolveLumenDateRangeEndChange(value, end, minimumDate, maximumDate))\n }}\n value={value.end}\n />\n <DateSupportingText\n {...(description ? { description } : {})}\n {...(errorMessage ? { errorMessage } : {})}\n />\n </View>\n )\n}\n",
|
|
60422
|
+
"packages/react-native/src/datetime-components.tsx": "import {\n type ChangeEvent,\n createElement,\n type ReactElement,\n useEffect,\n useState\n} from 'react'\nimport {\n Platform,\n Pressable,\n Text,\n type TextStyle,\n View,\n type ViewProps\n} from 'react-native'\n\nimport {\n DateTimePickerAndroid,\n type DateTimePickerChangeEvent\n} from '@react-native-community/datetimepicker'\n\nimport {\n clampLumenDate,\n formatLumenDateDisplayValue,\n formatLumenDateInputValue,\n isValidLumenDate,\n type LumenDateRangeValue,\n parseLumenDateInputValue,\n resolveLumenDateBounds,\n resolveLumenDatePickerValue,\n resolveLumenDateRangeEndChange,\n resolveLumenDateRangeEndMinimum,\n resolveLumenDateRangeStartChange\n} from './form-recipes.js'\nimport { NativeDatePicker, type NativeDatePickerProps } from './native-date-picker.js'\nimport {\n resolveLumenAriaInvalid,\n resolveLumenButtonOpacity,\n resolveLumenValidationHint\n} from './recipes.js'\nimport { useLumenTheme } from './theme-context.js'\n\nconst defaultDateFormatter = new Intl.DateTimeFormat(undefined, { dateStyle: 'medium' })\n\ninterface DateSupportingTextProps {\n description?: string | undefined\n errorMessage?: string | undefined\n}\n\nconst resolveDateLabelColor = (\n danger: string,\n ink: string,\n errorMessage?: string\n): string => errorMessage ? danger : ink\n\nconst resolveDatePickerExpanded = (enabled: boolean, expanded: boolean): boolean => (\n enabled && expanded\n)\n\nconst dismissAndroidDatePicker = (): void => {\n if (Platform.OS === 'android') void DateTimePickerAndroid.dismiss('date')\n}\n\nconst DateSupportingText = ({ description, errorMessage }: DateSupportingTextProps): ReactElement | null => {\n const theme = useLumenTheme()\n const supportingText = errorMessage ?? description\n\n if (!supportingText) return null\n\n return (\n <Text\n accessibilityLiveRegion={errorMessage ? 'polite' : 'none'}\n style={{\n color: errorMessage ? theme.colors.danger : theme.colors.inkMuted,\n fontSize: theme.fontSizes.xs\n }}\n >\n {supportingText}\n </Text>\n )\n}\n\ninterface NativeDatePickerDisclosureProps extends NativeDatePickerProps {\n expanded: boolean\n label: string\n onDateChange: (date: Date) => void\n webValue: Date | null\n}\n\nconst NativeDatePickerDisclosure = ({\n expanded,\n label,\n onDateChange,\n webValue,\n ...props\n}: NativeDatePickerDisclosureProps): ReactElement | null => {\n if (!expanded || Platform.OS === 'android') return null\n\n if (Platform.OS === 'web') {\n return createElement('input', {\n 'aria-label': label,\n disabled: props.disabled,\n max: props.maximumDate ? formatLumenDateInputValue(props.maximumDate) : undefined,\n min: props.minimumDate ? formatLumenDateInputValue(props.minimumDate) : undefined,\n onChange: (event: ChangeEvent<HTMLInputElement>) => {\n const date = parseLumenDateInputValue(event.currentTarget.value)\n\n if (date) onDateChange(date)\n },\n style: {\n accentColor: props.accentColor,\n border: '1px solid currentColor',\n borderRadius: 6,\n colorScheme: props.themeVariant,\n font: 'inherit',\n minHeight: 44,\n padding: '0 12px'\n },\n type: 'date',\n value: webValue ? formatLumenDateInputValue(webValue) : ''\n })\n }\n\n return <NativeDatePicker {...props} />\n}\n\nexport interface LumenDateFieldProps extends Omit<ViewProps, 'children'> {\n description?: string\n enabled?: boolean\n errorMessage?: string\n formatValue?: (date: Date) => string\n label: string\n maximumDate?: Date\n minimumDate?: Date\n onValueChange: (value: Date) => void\n placeholder?: string\n value: Date | null\n}\n\nconst LumenDateFieldControl = ({\n description,\n enabled = true,\n errorMessage,\n formatValue,\n label,\n maximumDate,\n minimumDate,\n onValueChange,\n placeholder = 'Select a date',\n style,\n value,\n ...props\n}: LumenDateFieldProps): ReactElement => {\n const theme = useLumenTheme()\n const [expanded, setExpanded] = useState(false)\n const pickerExpanded = resolveDatePickerExpanded(enabled, expanded)\n const validValue = isValidLumenDate(value) ? value : null\n const pickerValue = resolveLumenDatePickerValue(value, minimumDate, maximumDate)\n\n const displayValue = formatLumenDateDisplayValue(\n value,\n placeholder,\n formatValue ?? defaultDateFormatter.format.bind(defaultDateFormatter)\n )\n\n const pickerBounds = resolveLumenDateBounds(minimumDate, maximumDate)\n\n useEffect(() => dismissAndroidDatePicker, [])\n\n const commitValue = (_event: DateTimePickerChangeEvent, selectedDate?: Date): void => {\n if (!selectedDate) return\n\n onValueChange(clampLumenDate(selectedDate, minimumDate, maximumDate))\n }\n\n const openPicker = (): void => {\n if (!enabled) return\n\n if (Platform.OS === 'android') {\n DateTimePickerAndroid.open({\n ...pickerBounds,\n mode: 'date',\n onValueChange: commitValue,\n value: pickerValue\n })\n\n return\n }\n\n setExpanded(previous => !previous)\n }\n\n return (\n <View {...props} style={[{ gap: theme.spacing.xs }, style]}>\n <Text\n style={{\n color: errorMessage ? theme.colors.danger : theme.colors.ink,\n fontSize: theme.fontSizes.sm,\n fontWeight: String(theme.fontWeights.semibold) as TextStyle['fontWeight']\n }}\n >\n {label}\n </Text>\n <Pressable\n accessibilityHint={resolveLumenValidationHint(errorMessage, description)}\n accessibilityLabel={`${label}, ${displayValue}`}\n accessibilityRole=\"button\"\n accessibilityState={{ disabled: !enabled, expanded: pickerExpanded }}\n aria-invalid={resolveLumenAriaInvalid(errorMessage)}\n disabled={!enabled}\n onPress={openPicker}\n style={({ pressed }) => ({\n alignItems: 'center',\n backgroundColor: theme.colors.surface,\n borderColor: errorMessage ? theme.colors.danger : theme.colors.line,\n borderRadius: theme.radii.sm,\n borderWidth: 1,\n flexDirection: 'row',\n minHeight: 44,\n opacity: enabled ? resolveLumenButtonOpacity(false, pressed) : 0.52,\n paddingHorizontal: theme.spacing.md\n })}\n >\n <Text\n style={{\n color: validValue ? theme.colors.ink : theme.colors.inkMuted,\n flex: 1,\n fontSize: theme.fontSizes.sm\n }}\n >\n {displayValue}\n </Text>\n <Text\n accessibilityElementsHidden\n importantForAccessibility=\"no-hide-descendants\"\n style={{ color: theme.colors.inkMuted }}\n >\n ▾\n </Text>\n </Pressable>\n <NativeDatePickerDisclosure\n {...pickerBounds}\n accentColor={theme.colors.brandSolid}\n disabled={!enabled}\n display={Platform.OS === 'ios' ? 'inline' : 'default'}\n expanded={pickerExpanded}\n label={label}\n mode=\"date\"\n onDateChange={selectedDate => {\n onValueChange(clampLumenDate(selectedDate, minimumDate, maximumDate))\n }}\n onChange={commitValue}\n themeVariant={theme.scheme}\n value={pickerValue}\n webValue={validValue}\n />\n <DateSupportingText\n {...(description ? { description } : {})}\n {...(errorMessage ? { errorMessage } : {})}\n />\n </View>\n )\n}\n\nexport const LumenDateField = (props: LumenDateFieldProps): ReactElement => (\n <LumenDateFieldControl key={String(props.enabled ?? true)} {...props} />\n)\n\nexport interface LumenDateRangeFieldProps extends Omit<ViewProps, 'children'> {\n description?: string\n enabled?: boolean\n endLabel?: string\n errorMessage?: string\n formatValue?: (date: Date) => string\n label: string\n maximumDate?: Date\n minimumDate?: Date\n onValueChange: (value: LumenDateRangeValue) => void\n placeholder?: string\n startLabel?: string\n value: LumenDateRangeValue\n}\n\nconst resolveSharedDateFieldProps = (\n enabled: boolean,\n formatValue?: (date: Date) => string,\n maximumDate?: Date,\n placeholder?: string\n): Pick<LumenDateFieldProps, 'enabled' | 'formatValue' | 'maximumDate' | 'placeholder'> => ({\n enabled,\n ...(formatValue ? { formatValue } : {}),\n ...(maximumDate ? { maximumDate } : {}),\n ...(placeholder ? { placeholder } : {})\n})\n\nexport const LumenDateRangeField = ({\n description,\n enabled = true,\n endLabel = 'End date',\n errorMessage,\n formatValue,\n label,\n maximumDate,\n minimumDate,\n onValueChange,\n placeholder,\n startLabel = 'Start date',\n style,\n value,\n ...props\n}: LumenDateRangeFieldProps): ReactElement => {\n const theme = useLumenTheme()\n const endMinimum = resolveLumenDateRangeEndMinimum(value.start, minimumDate)\n const labelColor = resolveDateLabelColor(theme.colors.danger, theme.colors.ink, errorMessage)\n const sharedFieldProps = resolveSharedDateFieldProps(enabled, formatValue, maximumDate, placeholder)\n\n return (\n <View\n {...props}\n accessibilityHint={resolveLumenValidationHint(errorMessage, description, props.accessibilityHint)}\n accessibilityLabel={props.accessibilityLabel ?? label}\n aria-invalid={resolveLumenAriaInvalid(errorMessage)}\n style={[{ gap: theme.spacing.sm }, style]}\n >\n <Text\n style={{\n color: labelColor,\n fontSize: theme.fontSizes.sm,\n fontWeight: String(theme.fontWeights.semibold) as TextStyle['fontWeight']\n }}\n >\n {label}\n </Text>\n <LumenDateField\n {...sharedFieldProps}\n label={startLabel}\n {...(minimumDate ? { minimumDate } : {})}\n onValueChange={start => {\n onValueChange(resolveLumenDateRangeStartChange(value, start, minimumDate, maximumDate))\n }}\n value={value.start}\n />\n <LumenDateField\n {...sharedFieldProps}\n label={endLabel}\n {...(endMinimum ? { minimumDate: endMinimum } : {})}\n onValueChange={end => {\n onValueChange(resolveLumenDateRangeEndChange(value, end, minimumDate, maximumDate))\n }}\n value={value.end}\n />\n <DateSupportingText\n {...(description ? { description } : {})}\n {...(errorMessage ? { errorMessage } : {})}\n />\n </View>\n )\n}\n",
|
|
60423
60423
|
"packages/react-native/src/datetime.ts": "export {\n LumenDateField,\n type LumenDateFieldProps,\n LumenDateRangeField,\n type LumenDateRangeFieldProps\n} from './datetime-components.js'\nexport {\n type LumenDateRangeValue\n} from './form-recipes.js'\nexport { LumenTimeField, type LumenTimeFieldProps } from './time-components.js'\nexport { type LumenTimeSelection } from '@santi020k/lumen-core'\n",
|
|
60424
60424
|
"packages/react-native/src/field-context.ts": "import { createContext, use } from 'react'\n\nexport interface LumenFieldContextValue {\n /** Spoken guidance for controls that announce field context through a hint instead of relationships. */\n description?: string\n descriptionId?: string\n error: boolean\n errorId?: string\n errorMessage?: string\n label: string\n labelId: string\n required: boolean\n}\n\nexport const LumenFieldContext = createContext<LumenFieldContextValue | null>(null)\n\nexport const useLumenFieldContext = (): LumenFieldContextValue | null => use(LumenFieldContext)\n",
|
|
60425
60425
|
"packages/react-native/src/form-components.tsx": "import {\n type ReactElement,\n type ReactNode\n} from 'react'\nimport {\n Platform,\n Pressable,\n Switch,\n type SwitchProps,\n Text,\n TextInput,\n type TextInputProps,\n type TextStyle,\n View,\n type ViewProps\n} from 'react-native'\n\nimport { resolveLumenSearchFieldState } from './form-recipes.js'\nimport type { LumenSwitchRef, LumenTextInputRef, LumenViewRef } from './native-ref-types.js'\nimport { resolveLumenButtonOpacity } from './recipes.js'\nimport { useLumenTheme } from './theme-context.js'\n\nconst resolveSearchValue = <Value,>(value: Value | undefined, fallback: Value): Value => value ?? fallback\n\nexport interface LumenToggleProps extends Omit<SwitchProps, 'onValueChange' | 'value'> {\n description?: string\n label: string\n onValueChange: (value: boolean) => void\n ref?: LumenSwitchRef\n showLabel?: boolean\n value: boolean\n}\n\nexport const LumenToggle = ({\n accessibilityHint,\n description,\n disabled = false,\n label,\n onValueChange,\n ref,\n showLabel = true,\n style,\n value,\n ...props\n}: LumenToggleProps): ReactElement => {\n const theme = useLumenTheme()\n\n return (\n <Pressable\n accessibilityHint={accessibilityHint ?? description}\n accessibilityLabel={label}\n accessibilityRole=\"switch\"\n accessibilityState={{ checked: value, disabled }}\n aria-checked={value}\n disabled={disabled}\n onPress={() => {\n if (!disabled) onValueChange(!value)\n }}\n style={{\n alignItems: 'center',\n flexDirection: 'row',\n gap: theme.spacing.lg,\n minHeight: 44\n }}\n >\n {showLabel ?\n (\n <View style={{ flex: 1, gap: theme.spacing.xs }}>\n <Text\n style={{\n color: theme.colors.ink,\n fontSize: theme.fontSizes.sm,\n fontWeight: String(theme.fontWeights.semibold) as TextStyle['fontWeight']\n }}\n >\n {label}\n </Text>\n {description ?\n <Text style={{ color: theme.colors.inkSoft, fontSize: theme.fontSizes.xs }}>{description}</Text> :\n null}\n </View>\n ) :\n null}\n <Switch\n ref={ref}\n {...props}\n accessibilityElementsHidden\n aria-hidden\n disabled={disabled || Platform.OS === 'web'}\n importantForAccessibility=\"no-hide-descendants\"\n ios_backgroundColor={theme.colors.surfaceStrong}\n pointerEvents=\"none\"\n style={[{ opacity: disabled ? 0.52 : 1 }, style]}\n thumbColor={value ? theme.colors.onBrand : theme.colors.surface}\n trackColor={{ false: theme.colors.surfaceStrong, true: theme.colors.brandSolid }}\n value={value}\n />\n </Pressable>\n )\n}\n\nexport interface LumenSettingsRowProps extends ViewProps {\n control: ReactNode\n description?: string\n graphic?: ReactNode\n ref?: LumenViewRef\n title: string\n}\n\nexport const LumenSettingsRow = ({\n control,\n description,\n graphic,\n ref,\n style,\n title,\n ...props\n}: LumenSettingsRowProps): ReactElement => {\n const theme = useLumenTheme()\n\n return (\n <View\n ref={ref}\n {...props}\n style={[\n {\n alignItems: 'center',\n flexDirection: 'row',\n gap: theme.spacing.lg,\n minHeight: 56,\n paddingVertical: theme.spacing.sm\n },\n style\n ]}\n >\n {graphic ? <View>{graphic}</View> : null}\n <View style={{ flex: 1, gap: theme.spacing.xs }}>\n <Text\n style={{\n color: theme.colors.ink,\n fontSize: theme.fontSizes.sm,\n fontWeight: String(theme.fontWeights.semibold) as TextStyle['fontWeight']\n }}\n >\n {title}\n </Text>\n {description ?\n <Text style={{ color: theme.colors.inkMuted, fontSize: theme.fontSizes.xs }}>{description}</Text> :\n null}\n </View>\n <View>{control}</View>\n </View>\n )\n}\n\nexport interface LumenSearchFieldProps extends Omit<TextInputProps, 'onChangeText' | 'value'> {\n clearLabel?: string\n graphic?: ReactNode\n onChangeText: (value: string) => void\n prompt?: string\n ref?: LumenTextInputRef\n value: string\n}\n\nexport const LumenSearchField = ({\n accessibilityState,\n clearLabel = 'Clear search',\n editable = true,\n graphic,\n onChangeText,\n placeholder,\n placeholderTextColor,\n prompt = 'Search',\n ref,\n returnKeyType = 'search',\n style,\n value,\n ...props\n}: LumenSearchFieldProps): ReactElement => {\n const theme = useLumenTheme()\n const searchState = resolveLumenSearchFieldState(value, editable)\n\n return (\n <View\n style={{\n alignItems: 'center',\n backgroundColor: theme.colors.surface,\n borderColor: theme.colors.line,\n borderRadius: theme.radii.sm,\n borderWidth: 1,\n flexDirection: 'row',\n gap: theme.spacing.sm,\n minHeight: 44,\n opacity: searchState.opacity,\n paddingLeft: theme.spacing.md,\n paddingRight: theme.spacing.xs\n }}\n >\n {graphic}\n <TextInput\n ref={ref}\n {...props}\n accessibilityLabel={resolveSearchValue(props.accessibilityLabel, prompt)}\n accessibilityState={{ ...accessibilityState, disabled: !editable }}\n editable={editable}\n onChangeText={onChangeText}\n placeholder={resolveSearchValue(placeholder, prompt)}\n placeholderTextColor={resolveSearchValue(placeholderTextColor, theme.colors.inkMuted)}\n returnKeyType={returnKeyType}\n style={[\n {\n color: theme.colors.ink,\n flex: 1,\n fontSize: theme.fontSizes.sm,\n minHeight: 42,\n paddingVertical: theme.spacing.sm\n },\n style\n ]}\n value={value}\n />\n {searchState.showClearAction ?\n (\n <Pressable\n accessibilityLabel={clearLabel}\n accessibilityRole=\"button\"\n hitSlop={4}\n onPress={() => {\n onChangeText('')\n }}\n style={({ pressed }) => ({\n alignItems: 'center',\n justifyContent: 'center',\n minHeight: 44,\n minWidth: 44,\n opacity: resolveLumenButtonOpacity(false, pressed),\n paddingHorizontal: theme.spacing.sm\n })}\n >\n <Text\n style={{\n color: theme.colors.inkSoft,\n fontSize: theme.fontSizes.xs,\n fontWeight: String(theme.fontWeights.semibold) as TextStyle['fontWeight']\n }}\n >\n {clearLabel}\n </Text>\n </Pressable>\n ) :\n null}\n </View>\n )\n}\n",
|
|
@@ -61580,7 +61580,7 @@
|
|
|
61580
61580
|
"com.santi020k:lumen-compose",
|
|
61581
61581
|
"com.santi020k:lumen-compose-wear"
|
|
61582
61582
|
],
|
|
61583
|
-
"version": "4.0.
|
|
61583
|
+
"version": "4.0.1"
|
|
61584
61584
|
},
|
|
61585
61585
|
"npm": {
|
|
61586
61586
|
"packages": {
|
|
@@ -61588,17 +61588,17 @@
|
|
|
61588
61588
|
"peerDependencies": {
|
|
61589
61589
|
"astro": ">=5"
|
|
61590
61590
|
},
|
|
61591
|
-
"version": "4.
|
|
61591
|
+
"version": "4.3.0"
|
|
61592
61592
|
},
|
|
61593
61593
|
"@santi020k/lumen-core": {
|
|
61594
61594
|
"peerDependencies": {
|
|
61595
61595
|
"@rive-app/canvas": "^2.44.0"
|
|
61596
61596
|
},
|
|
61597
|
-
"version": "4.
|
|
61597
|
+
"version": "4.3.0"
|
|
61598
61598
|
},
|
|
61599
61599
|
"@santi020k/lumen-elements": {
|
|
61600
61600
|
"peerDependencies": {},
|
|
61601
|
-
"version": "4.
|
|
61601
|
+
"version": "4.3.0"
|
|
61602
61602
|
},
|
|
61603
61603
|
"@santi020k/lumen-icons-brand": {
|
|
61604
61604
|
"peerDependencies": {},
|
|
@@ -61608,18 +61608,18 @@
|
|
|
61608
61608
|
"peerDependencies": {
|
|
61609
61609
|
"react": ">=19"
|
|
61610
61610
|
},
|
|
61611
|
-
"version": "4.
|
|
61611
|
+
"version": "4.3.0"
|
|
61612
61612
|
},
|
|
61613
61613
|
"@santi020k/lumen-mcp": {
|
|
61614
61614
|
"peerDependencies": {},
|
|
61615
|
-
"version": "4.
|
|
61615
|
+
"version": "4.3.0"
|
|
61616
61616
|
},
|
|
61617
61617
|
"@santi020k/lumen-react": {
|
|
61618
61618
|
"peerDependencies": {
|
|
61619
61619
|
"react": ">=19",
|
|
61620
61620
|
"motion": "^14.0.0"
|
|
61621
61621
|
},
|
|
61622
|
-
"version": "4.
|
|
61622
|
+
"version": "4.3.0"
|
|
61623
61623
|
},
|
|
61624
61624
|
"@santi020k/lumen-react-hook-form": {
|
|
61625
61625
|
"peerDependencies": {
|
|
@@ -61636,7 +61636,7 @@
|
|
|
61636
61636
|
"react-native": ">=0.86.2",
|
|
61637
61637
|
"react-native-svg": ">=12 <16"
|
|
61638
61638
|
},
|
|
61639
|
-
"version": "4.0.
|
|
61639
|
+
"version": "4.0.2"
|
|
61640
61640
|
},
|
|
61641
61641
|
"@santi020k/lumen-tokens": {
|
|
61642
61642
|
"peerDependencies": {},
|
|
@@ -61647,9 +61647,9 @@
|
|
|
61647
61647
|
"swift": {
|
|
61648
61648
|
"package": "https://github.com/santi020k/lumen.git",
|
|
61649
61649
|
"revision": null,
|
|
61650
|
-
"tag": "v4.
|
|
61650
|
+
"tag": "v4.3.0"
|
|
61651
61651
|
},
|
|
61652
|
-
"version": "4.
|
|
61652
|
+
"version": "4.3.0"
|
|
61653
61653
|
},
|
|
61654
61654
|
"migration": {
|
|
61655
61655
|
"codemod": null,
|
|
@@ -61667,7 +61667,7 @@
|
|
|
61667
61667
|
}
|
|
61668
61668
|
}
|
|
61669
61669
|
},
|
|
61670
|
-
"rules": "# Lumen UI\n\nLumen is a multi-framework primitive UI system with Astro as the reference implementation.\n\nUse these files first when generating, reviewing, or migrating apps that use Lumen:\n\n- README.md: public overview and package strategy.\n- docs/ai-usage.md: AI-focused setup, package selection, and component selection.\n- https://lumen.santi020k.com/guides/build-ui-with-ai: tested Astro and React adoption example with a reusable prompt.\n- docs/ai-efficiency.md: reproducible AI token measurement, quality checks, and limits on efficiency claims.\n- skills/lumen-ui/SKILL.md: portable Agent Skill for building, reviewing, and theming Lumen interfaces.\n- docs/feature-roadmap.md: competitor-informed gaps and implementation priorities.\n- registry/lumen.registry.json: installable component, recipe, and documentation manifest.\n- packages/core/src/components.ts: authoritative shared component catalog.\n- packages/astro/components: reference component markup and prop shapes.\n- packages/astro/styles/lumen.css: standalone CSS contract for all frameworks.\n- @santi020k/lumen-mcp: Model Context Protocol server exposing snapshot provenance, structured component discovery, framework-aware usage/source, recipes, tolerant search, tokens, rules, and lumen:// resources for AI agents.\n\nInstall the portable Lumen Agent Skill:\n\n```bash\nnpx skills add santi020k/lumen --skill lumen-ui\n```\n\nDefault usage:\n\n```astro\n---\nimport { Button, Card, Input, Label } from '@santi020k/lumen-astro'\nimport UIPrimitives from '@santi020k/lumen-astro/runtime'\nimport '@santi020k/lumen-astro/styles.css'\n---\n\n<UIPrimitives />\n<Card>\n <Label for=\"email\">Email</Label>\n <Input id=\"email\" type=\"email\" />\n <Button>Subscribe</Button>\n</Card>\n```\n\nRules for agents:\n\n- When Lumen MCP is connected, read `lumen://meta`, `lumen://diagnostics`, and `lumen://rules`, pass the target framework to `lumen_search`, and follow the framework behavior returned by `lumen_get_component`. Retain `lumen://catalog-manifest` and use `lumen_diff_catalog` after upgrades when the client supports cached context.\n- Prefer Astro unless the user explicitly targets React or Web Components.\n- In React, use the visual components with behavior hooks for Dialog, Popover, DropdownMenu, Tabs, Select, Toast, and Tooltip instead of mounting the Astro runtime.\n- In Web Components, registered `lumen-*` elements include behavior parity for Dialog, Popover, DropdownMenu, Tabs, Select, Toast, and Tooltip, including ARIA state, keyboard paths, Escape/outside dismissal, focus handling, native select form participation, and the shared toast controller.\n- For React report filters, use `DateRangeInput` (`value`, `onValueChange`, localized `labels`, bounds and presets) or inline `DateRangeCalendar`. These React-specific compositions are documented in packages/react/README.md and the reporting example at /docs/web/reporting.\n- React record tables can opt into DataTableView from @santi020k/lumen-react/components/data-table-view for search, filters, visibility, density, pagination and controlled sorting. Read docs/data-table-views.md; applications retain cells, data fetching and domain actions.\n- Keep server-paginated DataTable rows in server order with `sortMode=\"manual\"`, controlled `sort` and `onSortChange`. Apply sorting before pagination in the application backend. Use Dialog dismissal policies for pending workflows.\n- Chart X identities must be unique. Use `xLabel` for abbreviated axes, `formatCategory` for full details, and explicit null gaps for missing observations. Read docs/data-visualization.md.\n- Use ImageComparison for before/after media with matching dimensions, alternative text and a visible localized control label. See docs/image-comparison.md for adapter contracts.\n- Read docs/migrating-to-lumen.md before upgrading to v4, especially date-trigger IDs, loading buttons, hidden semantics, copy feedback and native initializer changes.\n- Do not require Tailwind configuration for Lumen components.\n- Give each outer edge one border and radius owner. Flush images inside rounded frames use Image radius=\"none\"; Image defaults to radius=\"lg\". Use one divider at shared edges, account for padding on inset curves, and check phone/desktop corners and keyboard focus in both themes. Keep artwork clipping scoped so it does not cut off focus rings or overlays. See docs/ai-usage.md for styling rules.\n- Use public tokens: canvas, surface, surface-muted, surface-strong, line, ink, ink-soft, ink-muted, brand, brand-solid, brand-soft, on-brand, accent, success, warning, danger, on-danger.\n- Default to `Select` for web product option lists and `Combobox` for searchable choices. Use `NativeSelect` only for an explicit browser or platform picker requirement. Keep native fallbacks and verify the enhanced list with the matching framework behavior enabled.\n- Mount `UIPrimitives` once for interactive Astro primitives.\n- Use `glass` for glass surfaces. The obsolete overlay `surface=\"glass\"` alias is removed.\n- Treat schedule, data collection, rich text, theme builder, and advanced fields as supported surfaces with progressively enhanced behavior.\n- Use `lumen list`, `lumen show <component>`, and `lumen add <component>` for individually addressable registry components; component names also resolve from kebab-case aliases such as `data-table`.\n- Runtime CustomEvents used by `UIPrimitives`: `ui:data-table-selection-change` from `[data-ui-datatable]` with `{ values }`; `ui:schedule-change` from `[data-ui-schedule]` with `{ eventId, slot }`; `ui:virtual-list-range` from `[data-ui-virtual-list]` with `{ startIndex, endIndex }`; `ui:tag-remove` from tag groups with `{ value }`; `ui:editor-command` from rich text editors with `{ command }`; `ui:theme-change` from theme builders with `{ hue, tokens }`; `ui:theme-export` from theme builders with `{ css }`; `ui:validate`, `ui:invalid`, and `ui:valid` from `[data-ui-form]` for native Constraint Validation API enhancement; document-level `ui:toast`, `ui:toast-update`, and `ui:toast-dismiss` for runtime toasts; `ui:toast-action` from `[data-ui-toast]` when an action button is pressed.\n- Add `data-ui-form` to forms that should reflect `required`, `pattern`, `min`, `max`, `type`, and custom `setCustomValidity()` state into Field error slots on blur and submit. Use `data-error-required`, `data-error-pattern`, `data-error-min`, `data-error-max`, or `data-error-custom` to override browser validation messages.\n- Toasts use `ToastViewport` for explicit `placement` and `maxCount` configuration. With `UIPrimitives` or registered Web Components, use `window.LumenToast.create(detail)`, `.update(id, detail)`, and `.dismiss(id?)`, import `LumenToast` from `@santi020k/lumen-elements`, or dispatch the matching document events. Runtime toast `detail` supports `title`, `description`, `variant`, `duration`, `placement`, `max`, and `action`.\n\n## Content flow\n\n- Reuse content-flow-header, content-flow-settings, content-flow-list and content-flow-actions recipes; lumen_get_recipe returns complete framework source.\n- Container gutters grow from group to section spacing. Override --ui-container-gutter at the product boundary instead of adding nested page padding.\n- Prose and Typography trim their outer child margins and separate headings from preceding text. Card allows interactive overflow; AspectRatio owns media clipping.\n\n- Read docs/content-flow.md. Stack/Grid own sibling gaps; surfaces own padding; Field owns control feedback spacing. Do not add child margins for a relationship already controlled by a parent gap.\n- Use Stack/Grid gap=\"related\" (8px), \"group\" (16px, default), or \"section\" (32px). Size gaps match the canonical scale: xs=4, sm=8, md=12, lg=16, xl=24, 2xl=32, 3xl=48. CSS variables use rem. Retrieve spacing and spacingRoles through lumen_get_tokens.\n- Use CardHeader, CardContent and CardFooter. Card owns their gap; missing, hidden and empty parts leave no spacing. Use a nested Stack inside the body and allow footer actions to wrap. Card density is compact, comfortable (default), or spacious; it does not reduce control touch targets.\n- Keep Prose/Typography text rhythm separate from sibling gaps. Check long translated text, enlarged text, validation, optional parts and phone/desktop wrapping in both themes. Use one outer Container for page gutters and one border owner per visible edge.\n\nBefore using MCP contracts, call lumen_check_compatibility with resolved installed Lumen versions.\nIf versions differ, use installed public types and package README or a matching MCP package.\nNever silently upgrade an application. For an explicit v4 upgrade, read lumen_get_migration and\npreview lumen migrate v4 --dry-run --json; review dependency updates and application workarounds.\n\n- [React form workflows](docs/powerful-forms.md): optional dependent validation, edit tracking, steps, repeatable rows, and async checks.\n\n- WorldMap: reusable dotted/solid SVG map in Astro, React and Elements. Import country geometry explicitly from @santi020k/lumen-core/world-map-data; see docs/world-map.md.\n",
|
|
61670
|
+
"rules": "# Lumen UI\n\nLumen is a multi-framework primitive UI system with Astro as the reference implementation.\n\nUse these files first when generating, reviewing, or migrating apps that use Lumen:\n\n- README.md: public overview and package strategy.\n- docs/ai-usage.md: AI-focused setup, package selection, and component selection.\n- https://lumen.santi020k.com/guides/build-ui-with-ai: tested Astro and React adoption example with a reusable prompt.\n- docs/ai-efficiency.md: reproducible AI token measurement, quality checks, and limits on efficiency claims.\n- skills/lumen-ui/SKILL.md: portable Agent Skill for building, reviewing, and theming Lumen interfaces.\n- docs/feature-roadmap.md: competitor-informed gaps and implementation priorities.\n- registry/lumen.registry.json: installable component, recipe, and documentation manifest.\n- packages/core/src/components.ts: authoritative shared component catalog.\n- packages/astro/components: reference component markup and prop shapes.\n- packages/astro/styles/lumen.css: standalone CSS contract for all frameworks.\n- @santi020k/lumen-mcp: Model Context Protocol server exposing snapshot provenance, structured component discovery, framework-aware usage/source, recipes, tolerant search, tokens, rules, and lumen:// resources for AI agents.\n\nInstall the portable Lumen Agent Skill:\n\n```bash\nnpx skills add santi020k/lumen --skill lumen-ui\n```\n\nDefault usage:\n\n```astro\n---\nimport { Button, Card, Input, Label } from '@santi020k/lumen-astro'\nimport UIPrimitives from '@santi020k/lumen-astro/runtime'\nimport '@santi020k/lumen-astro/styles.css'\n---\n\n<UIPrimitives />\n<Card>\n <Label for=\"email\">Email</Label>\n <Input id=\"email\" type=\"email\" />\n <Button>Subscribe</Button>\n</Card>\n```\n\nRules for agents:\n\n- When Lumen MCP is connected, read `lumen://meta`, `lumen://diagnostics`, and `lumen://rules`, pass the target framework to `lumen_search`, and follow the framework behavior returned by `lumen_get_component`. Retain `lumen://catalog-manifest` and use `lumen_diff_catalog` after upgrades when the client supports cached context.\n- Prefer Astro unless the user explicitly targets React or Web Components.\n- In React, use the visual components with behavior hooks for Dialog, Popover, DropdownMenu, Tabs, Select, Toast, and Tooltip instead of mounting the Astro runtime.\n- In Web Components, registered `lumen-*` elements include behavior parity for Dialog, Popover, DropdownMenu, Tabs, Select, Toast, and Tooltip, including ARIA state, keyboard paths, Escape/outside dismissal, focus handling, native select form participation, and the shared toast controller.\n- For React report filters, use `DateRangeInput` (`value`, `onValueChange`, localized `labels`, bounds and presets) or inline `DateRangeCalendar`. These React-specific compositions are documented in packages/react/README.md and the reporting example at /docs/web/reporting.\n- React record tables can opt into DataTableView from @santi020k/lumen-react/components/data-table-view for search, filters, visibility, density, pagination and controlled sorting. Read docs/data-table-views.md; applications retain cells, data fetching and domain actions.\n- Keep server-paginated DataTable rows in server order with `sortMode=\"manual\"`, controlled `sort` and `onSortChange`. Apply sorting before pagination in the application backend. Use Dialog dismissal policies for pending workflows.\n- Chart X identities must be unique. Use `xLabel` for abbreviated axes, `formatCategory` for full details, and explicit null gaps for missing observations. Read docs/data-visualization.md.\n- Use ImageComparison for before/after media with matching dimensions, alternative text and a visible localized control label. See docs/image-comparison.md for adapter contracts.\n- Read docs/migrating-to-lumen.md before upgrading to v4, especially date-trigger IDs, loading buttons, hidden semantics, copy feedback and native initializer changes.\n- Do not require Tailwind configuration for Lumen components.\n- Give each outer edge one border and radius owner. Flush images inside rounded frames use Image radius=\"none\"; Image defaults to radius=\"lg\". Use one divider at shared edges, account for padding on inset curves, and check phone/desktop corners and keyboard focus in both themes. Keep artwork clipping scoped so it does not cut off focus rings or overlays. See docs/ai-usage.md for styling rules.\n- Use public tokens: canvas, surface, surface-muted, surface-strong, line, ink, ink-soft, ink-muted, brand, brand-solid, brand-soft, on-brand, accent, success, warning, danger, on-danger.\n- Default to `Select` for web product option lists and `Combobox` for searchable choices. Use `NativeSelect` only for an explicit browser or platform picker requirement. Keep native fallbacks and verify the enhanced list with the matching framework behavior enabled.\n- Mount `UIPrimitives` once for interactive Astro primitives.\n- Use `glass` for glass surfaces. The obsolete overlay `surface=\"glass\"` alias is removed.\n- Treat schedule, data collection, rich text, theme builder, and advanced fields as supported surfaces with progressively enhanced behavior.\n- Use `lumen list`, `lumen show <component>`, and `lumen add <component>` for individually addressable registry components; component names also resolve from kebab-case aliases such as `data-table`.\n- Runtime CustomEvents used by `UIPrimitives`: `ui:data-table-selection-change` from `[data-ui-datatable]` with `{ values }`; `ui:schedule-change` from `[data-ui-schedule]` with `{ eventId, slot }`; `ui:virtual-list-range` from `[data-ui-virtual-list]` with `{ startIndex, endIndex }`; `ui:tag-remove` from tag groups with `{ value }`; `ui:editor-command` from rich text editors with `{ command }`; `ui:theme-change` from theme builders with `{ hue, tokens }`; `ui:theme-export` from theme builders with `{ css }`; `ui:validate`, `ui:invalid`, and `ui:valid` from `[data-ui-form]` for native Constraint Validation API enhancement; document-level `ui:toast`, `ui:toast-update`, and `ui:toast-dismiss` for runtime toasts; `ui:toast-action` from `[data-ui-toast]` when an action button is pressed.\n- Add `data-ui-form` to forms that should reflect `required`, `pattern`, `min`, `max`, `type`, and custom `setCustomValidity()` state into Field error slots on blur and submit. Use `data-error-required`, `data-error-pattern`, `data-error-min`, `data-error-max`, or `data-error-custom` to override browser validation messages.\n- Toasts use `ToastViewport` for explicit `placement` and `maxCount` configuration. With `UIPrimitives` or registered Web Components, use `window.LumenToast.create(detail)`, `.update(id, detail)`, and `.dismiss(id?)`, import `LumenToast` from `@santi020k/lumen-elements`, or dispatch the matching document events. Runtime toast `detail` supports `title`, `description`, `variant`, `duration`, `placement`, `max`, and `action`.\n\n## Content flow\n\n- Reuse content-flow-header, content-flow-settings, content-flow-list and content-flow-actions recipes; lumen_get_recipe returns complete framework source.\n- Container gutters grow from group to section spacing. Override --ui-container-gutter at the product boundary instead of adding nested page padding.\n- Prose and Typography trim their outer child margins and separate headings from preceding text. Card allows interactive overflow; AspectRatio owns media clipping.\n\n- Read docs/content-flow.md. Stack/Grid own sibling gaps; surfaces own padding; Field owns control feedback spacing. Do not add child margins for a relationship already controlled by a parent gap.\n- Use Stack/Grid gap=\"related\" (8px), \"group\" (16px, default), or \"section\" (32px). Size gaps match the canonical scale: xs=4, sm=8, md=12, lg=16, xl=24, 2xl=32, 3xl=48. CSS variables use rem. Retrieve spacing and spacingRoles through lumen_get_tokens.\n- Use CardHeader, CardContent and CardFooter. Card owns their gap; missing, hidden and empty parts leave no spacing. Use a nested Stack inside the body and allow footer actions to wrap. Card density is compact, comfortable (default), or spacious; it does not reduce control touch targets.\n- Keep Prose/Typography text rhythm separate from sibling gaps. Check long translated text, enlarged text, validation, optional parts and phone/desktop wrapping in both themes. Use one outer Container for page gutters and one border owner per visible edge.\n\nBefore using MCP contracts, call lumen_check_compatibility with resolved installed Lumen versions.\nIf versions differ, use installed public types and package README or a matching MCP package.\nNever silently upgrade an application. For an explicit v4 upgrade, read lumen_get_migration and\npreview lumen migrate v4 --dry-run --json; review dependency updates and application workarounds.\n\n- [React form workflows](docs/powerful-forms.md): optional dependent validation, edit tracking, steps, repeatable rows, and async checks.\n\n- WorldMap: reusable dotted/solid SVG map in Astro, React and Elements. Import country geometry explicitly from @santi020k/lumen-core/world-map-data; see docs/world-map.md.\n\n- Tailwind CSS v4 complement: https://lumen.santi020k.com/docs/web/tailwind\n",
|
|
61671
61671
|
"tokens": {
|
|
61672
61672
|
"presets": {
|
|
61673
61673
|
"attribute": "data-lumen-preset",
|