@santi020k/lumen-mcp 1.4.0 → 1.5.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.
@@ -159,55 +159,68 @@
159
159
  "VisuallyHidden": "17808de2f795be62dbeb1f8c649fedd7dda5acbd8af3bbc1d1bbec20e788e3b3"
160
160
  },
161
161
  "nativeComponents": {
162
- "Theme": "527f0afa78e08580631be6b45fb5980ef2b827472dce29190510eea7f7b73e35",
163
- "Text": "da836769d7734d25de5c5819b3d8eb7cdb0a7347baa4a757ecfcbde2388391ba",
164
- "Surface": "b0605587e07f7e0d15b6f5adaf3c2f067b388306e279ca32029e8a44201e1135",
165
- "Icon": "79ed08ed3eb03bdbe69f895932ef1fdf842320015e74428cae2cf7f70c9e97bf",
166
- "Icon button": "6088ce23834d92200ea6779fc8a435b2beace575f68247d474069c4df539e44d",
167
- "Button": "57380934403cb9b81a4642949ca603126a93d283a88dc8830b0a5a4722e397e0",
168
- "Text field": "5609ef9607cec03a5bf48ab2f706f19cbb0750db2697626390e2ecb7cc665afc",
169
- "Badge": "96c9707c9ea84f47fde14739fbd9a3789d99aa501d7271ca55f0d5fdb330379e",
170
- "Divider": "6848eae113b3ccaf01aa5d05b3463ab6c208374165423cc94334eb3448cbc597",
171
- "Spinner": "cb3f28a5162edb83906a181fa64b0557397cf4b921bd12f41a48165c075a8155",
172
- "Card": "5481383b9baa0e758b4ecb7cedaeff485998c2cddf6a99835ef5f48be25d4648",
173
- "Alert": "71c9658e4b5426c0da101adc44c60ffc56a82567a59be1f2776da6ab603c86c3",
174
- "Progress": "b38f105092118eb2a67feeadc6642cb884245321b3c03e6aa8f96e41c3252bfc",
175
- "Skeleton": "328820880ed1b1fedd86701053fca980e75ef6b0159628a77de60aa3fb9cfafa",
176
- "Disclosure": "6e940a230839fc34905dd3e629c961179d40bed0c8ed834333e9e71c20c2c56f",
177
- "Avatar": "35544a2d015338f7b240234619b2eebe5ca97710d3b39af32931c0cb5f544813",
178
- "Textarea": "221a265a3ed0eca986f1b6e377f92217ab343359a64081a021d0cf079eaa2c6e",
179
- "Field group": "1d736820d5eabd202090e35555be772a06c26a1c47745417d5a23faa07c1e3fa",
180
- "Chip": "fcbce7af9bb38a1bce7f5c337a0cdb8eb8721aa0a0f36c510ff764acdef986f5",
181
- "Button group": "82c35614035d04e4dc0815ee22be89b89856516ed7fe557ac430c9b2f37aa18f",
182
- "Toast": "654dcf711be6af245dcb4ea8ba8d100d4c5bcea0e281ab2e3009f0dd765f8caf",
162
+ "Theme": "d842af0deb21b8cda2e0174a7ddb430f4a4221b3367831eafe5577033e3ad7e7",
163
+ "Text": "a0ec8310022ffbce337e6438cd1b578970ba079dd0f869c3b19a271685240cbd",
164
+ "Surface": "d9e04e1d8c8b587bbb8ca4a34778dc1ed0bd28cb711b1a1d80cc28490fde80dd",
165
+ "Icon": "cc7097cde28d269c99574b10cae8ff0e505df242ecc58ba5b00b9412172658c3",
166
+ "Icon button": "fa306c12c2f8dda4f4c6d333dc8043574d85a0f2409c8c68126eb2152700f631",
167
+ "Button": "bb139a12f9f3cef1cadd347d33aa6a7e0192ebe5c90c785a1767cd831aa7565b",
168
+ "Text field": "8492a9b49db28d731ad29faf9cfbe1d6f2aa3b650302f9270698f60741760a6d",
169
+ "Badge": "93cadf1f9d49fcb46b24149301ff34e6180f30079c5209fa27ba44eb810f1fa8",
170
+ "Divider": "96f26b9bcc7118748598f15b256f551aa9645520e517111bd127bfbc6513a56d",
171
+ "Spinner": "66df46d36c01aa11214d764830e6490232ff70311e9f16987be60739b112f799",
172
+ "Card": "ca307cd01d6f1e2f199335e8a620a923869df4ec2c22329821ca7767705495af",
173
+ "Alert": "135cd171d5f98b4f3291f0da242b22e032fd06fe7199cfc9ef0e2c26cb7b0b4c",
174
+ "Progress": "9b2201033ebfcc87ca6548e89501aa965b46281854830bd42ce9f96a6c195fc0",
175
+ "Skeleton": "1a434c444602b1ceff149e995056b9e48bec516daa567feb77b69d677aaf6e83",
176
+ "Disclosure": "ef9284c05a877ec8726f6e4669d825668be871bc39a9c7d1ddb872aabe39140b",
177
+ "Avatar": "f9204d30580573f5e4dfd24c9606b558b57ed5858edb84687b9769a445b74b82",
178
+ "Textarea": "fdcfce4f52590b51f44865ce30d58a5a1a797b6307608fe8758327a5b4d33daa",
179
+ "Field group": "ee06223db232ff00a35e9162379cb4026b80ba49e4e5630783303e3fcf6d466f",
180
+ "Chip": "58efbce779f98e5e782f958acebabd54ff3a3cc53f1824491d828f36b6b7ad53",
181
+ "Button group": "e104fb26def3551a226dd2a8a17063feecf41ce7363f40ef0e69385d632af374",
182
+ "Toast": "cab5d2c2751c897dc678ce2bd9c71af6ddbdac409ad615864f88853c1e0cee57",
183
183
  "Toggle": "b2e7e0723a55a6118cf20f978a4f880d814440107827e509b11e8c3c92b00f4b",
184
184
  "Settings row": "8bc392c37d446ba1ed276556ac57910335d8dab7bc013e56fc9383819a48baa5",
185
185
  "Picker": "ed96336bb1fec3088a1aafebfcb7f0091352de1d47c666c0c54b67ebb3a472c0",
186
186
  "Slider": "95d3f6367e952633325dc1214a240424b0299fa4ce1457eadcbc995489ed0828",
187
187
  "Search field": "d65de25840be13dc68e30eb29fc7aaa4ebdc0f0d249a2746e980135ac189fd95",
188
- "Checkbox": "badc66c91eddd344b30058ec62f49e29ca8e3cb4c2fbe7a329f9d87b1dcd4516",
189
- "Radio group": "16af92535d9ce1d7b899820fcac65cb068e4e14e4f70e72b0eaedc83f3e0b66d",
190
- "Segmented control": "001c132f08dbaaebd00aee9bc0c68cdb5d198506314b07a6a1e473740abfc868",
191
- "Navigation bar": "6a196a6f9924057040e7cbf99fec903f864d177aca462199cdfe8d076a782539",
192
- "Empty state": "40301270a9d34234d0a3500e7d8d29f70f5886d4cb07647c3439c695619ec97a",
193
- "List row": "ea220c485ae90e6d0670e8f526f7925be568377c467bffb87c68d9ec79392441",
194
- "Banner": "95f00d14a494253cb4aed2694a883c3b4bffbf2316e7536594a02117b8991808",
195
- "Stat": "3b357dde8e5c31b1a91df44ebc12c01fdbf2d0e74dfb721a8b47a88722542145",
188
+ "Checkbox": "22028a1e1a7517fb2bdd6962eae040d067827c3be53af242cc551fbdb4996c79",
189
+ "Radio group": "b2098339feb47b9fe572bd888b91b256fa9f4ee0a7a50302a604e36108c2104b",
190
+ "Segmented control": "92f975ee8211ebf1a86d7e372e0d1f1cfd39d45f33f9bb8a43167c82becb5f9d",
191
+ "Navigation bar": "0ea9eaa8d6e90c55c65623fc7ab72591858416487691bb87c3330242a9fe0f4b",
192
+ "Empty state": "134a0cd1c653e5f3b07378ceb11db7987c97259221d283e589bc4c12deb7f1a0",
193
+ "List row": "fe64c26bd290a8664330fc7c7ec1aa6804d039b2a127678a7b1e43c83e922f6b",
194
+ "Banner": "1d7e36125b6700cd1dd6fe643f3a84f41cc4bd87e24e9e5a8739491beef332d1",
195
+ "Stat": "d41b2004c65666060e279694d9fa3be1cf9aa35d46cb4d2d80ceffdbf78278f6",
196
196
  "Gauge": "d8bb93ae076c2fe70e2dbc149cf6d99bdb2199c176dff0f6ccd0016eb1b62b49",
197
- "Section header": "4e1dd5fae63e25270c62b74fc347a18cff71050bb8498b003725cdb532edb734",
198
- "Status bar": "d67f1a895b5ae0541b4a4e5320858e27a3ec1dda50ef655e122da1373ff1bd90",
199
- "Refresh control": "2626b04e4dab27f7fecd9210ad6d3a150548c5e8053f94fbb7f3ce05342bceb4",
200
- "Date field": "2397d6907ac230497077d3b388b330fc43680f6e7cadd2faef3480a71d719ec3",
201
- "Floating action button": "ba8237274309f2fbd7b78d42ea6862dc018397209111d88d4abd9ce9b877da84",
197
+ "Section header": "4cbcc77c23f5626d80a675ac5cd0d6fd46328ecdd7fc579735dcf88fbeb313ad",
198
+ "Status bar": "781566e2fc6239bddf46dc6b5d5d23fc28af008f1b8b903c3bc03dba60c4e798",
199
+ "Collapsible navigation bar": "dca8d0fb545832bb6972198e1faf4db540d92c5b550bb77c94953849fd1d72cb",
200
+ "Navigation accessory": "56b7905d0265e5d89f509294090f9a7a15b8fceccca9b4607b407b3e2229e4c6",
201
+ "Navigation bar scroll behavior": "32617167b0c8411442223810609b17929558ca623a03ff4f19c3bf846e55abda",
202
+ "Navigation bar accessory": "c67dac67188494302cbd99700f57074ca6a022498121b2c366a39c06b7a89bbc",
203
+ "Adaptive navigation scaffold": "4360aa0c46eb761bd93c0d03e614321e30ce791d12982b064c2e7dc55aa37f89",
204
+ "Refresh control": "cc40b1e03afb06de40e0c4fdb01c4d40113f92823fdb91c2135fed706878a2ee",
205
+ "Date field": "64fcae6b6339db3f57c072d0e183d5c93e1b95759a600a3a345ee76d9a0dfdcc",
206
+ "Floating action button": "52eadc73e99b66fed25009e03812f0b06168de8539ced006c5b5eea8b6f7a98c",
207
+ "Link": "80389c18ef528e7b0f32c34b2ccd7464bbec84444192d96bfd3b10410ffd8790",
208
+ "Tab bar minimization": "5306ad157bc439c9813701e63a8ddea7ac53f9fa5ad5d1c593f5a5a71d4fc59e",
209
+ "Tab accessory": "d9fe38854a63220e6d8f2ba8c28ecd847069ea3469f146060fa2f920b431c329",
202
210
  "Shortcut recorder": "a5a2fa237d2efe604431bc35d46f063328fdc0fd8c0485d53d1692b1f0a76926",
203
211
  "Symbol picker": "57b3383a74549aa62dd97dfa627464818e4b65d5b8c130ecb3697059668fbb27",
204
- "Graphic": "54a853609b5abdc397d2e209685d816338db10b14b2d112369ecb6e532c2df3a",
205
- "Backdrop": "89d9f325e636ad8e30f0afc333a1a3c31b761075ca6b83c454abf3f95c427dcc",
206
- "Illustration": "65c87de0c17a8161645dfa7787c43b8b144f433fbb4a72fe1691afe319750eda",
207
- "Alert dialog": "8ee22136b2c66aec77df2bd6234f470cd0a8e97f52abeedd10c12ae9d9ec7bd7",
208
- "Sheet": "1873aeda95cd73a25ae89cb846c0bea94f287fa2559bc6bbe33809a25585f9ca",
209
- "Menu": "0542e0c7006431c82248b3d2456418504236c23e6cd2092ae65bdc4be94eaab7",
210
- "Share button": "7dcd8523d024d98679e3a78d05b237e80d26ce0222669fe1a6df588a6006effb"
212
+ "Graphic": "284672046f3a3d5e83834aca7f2847d52dff2a47c81da2ee2e927a3477eb24be",
213
+ "Backdrop": "65d9bf433bbaee2402bbfe1518d593df36fc6d0197f29c69a3ec68380a5598b4",
214
+ "Illustration": "ac5ade1e6774c32628e4c67029e47e234cffa2499a29535b619efd611751ec4b",
215
+ "Alert dialog": "bf28384464575d8c01120ffc1f97e8d1af6bfc3e0fa75fcc0a729633fa78f33b",
216
+ "Sheet": "64571d907efb9e125411ae5557a94bc2cd51ad10af1baaaa0411fb06223e6748",
217
+ "Menu": "19db0b34a54f2a298dcb7a74e09dd0e803639a82d53548ce216c8647bcc3a6ba",
218
+ "Share button": "b756d1f682b85e6a2a430565343774a66cf95047e2d702d837f0f5e9fc0c6c19",
219
+ "Wearable action": "d033694a60052eeaa5a6ad6fddf438dc67647b281ccf5ad11e0b58fbcf3bea52",
220
+ "Wearable progress": "2b0d4fc506d6ac626b77086212f2358ad2f90ab9c15b84125338a7da07995193",
221
+ "Wearable status": "a81185ab80524b0ab2d0e648d52c5f85606598c5e767693122f242f868a5924f",
222
+ "Wearable metric": "5e1f0850333a1997a0322ae5937ee0e43e24ef0acc598227d067d2639aff5f7d",
223
+ "Wearable list row": "56d6e690bb2ccb84037c6580e96ab7dcca44f2b8e3812d6105a91f85acd2ab67"
211
224
  },
212
225
  "recipes": {
213
226
  "all-components": "517dfb03b8ca5483f5412a00ca2a151763d8d4429a44e398d4d74733793f0fd7",
@@ -32935,13 +32948,13 @@
32935
32948
  }
32936
32949
  ],
32937
32950
  "docs": {
32938
- "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\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## 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 `1.2.0` | `LumenUI` | Not applicable |\n| Android / Compose | Maven Central `com.santi020k:lumen-compose:0.3.0` | `com.santi020k.lumen` | Not applicable |\n| Package metadata | `@santi020k/lumen-core` | `@santi020k/lumen-core` | Not applicable |\n| Optional 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:0.3.0` from Maven Central and wrap content in `LumenTheme`. Native\nadapters do not load CSS or the Astro runtime.\n\nBrand marks are intentionally excluded from the default Lucide catalog. When a product needs them,\ninstall 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.\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'\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. `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.\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. The pre-1.0 Astro and Elements `size=\"sm|lg\"`\nvisual alias is deprecated but remains available for incremental migration.\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\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`, `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- 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, and `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 `Chart` as the custom SVG/canvas escape hatch.\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. Install and register\n `@santi020k/lumen-icons-brand` for supported brand marks, then use names such as\n `brand:github`. Render project-specific or unsupported brand SVGs through the default `Icon`\n slot. Astro reports unknown named icons during 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- 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 emits the same data attributes for app-level adapters. `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` dispatch browser editing commands and emit\n `ui:editor-command`; use `data-ui-editor-value` for headings, links, and other value-bearing\n commands. Editable surfaces support common formatting shortcuts, synchronize toggle\n `aria-pressed` state, and emit `ui:editor-change` with HTML and plain text. Astro `UIPrimitives`\n and registered Web Components bind those controls directly; React apps can use\n `useRichTextEditor` for `rootProps`, `getCommandProps`, `getEditableProps`, and `executeCommand`.\n For Tiptap, ProseMirror, Lexical, or Markdown engines, keep the external editor\n package in the app and bridge toolbar buttons through the emitted command event instead of adding\n the editor engine to Lumen.\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` | `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 package also ships a small `lumen` binary. Individual components\nare addressable by name, including kebab-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 init --framework astro --tailwind\n```\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.\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- Do not require consumers to configure Tailwind for Lumen components.\n- Prefer component props and composition before adding custom CSS.\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",
32939
- "readme": "<p align=\"center\">\n <a href=\"https://lumen.santi020k.com\">\n <img src=\"./apps/docs/public/logo.svg\" alt=\"Lumen UI\" width=\"239\" height=\"60\">\n </a>\n</p>\n\n<h1 align=\"center\">Lumen UI</h1>\n\n<p align=\"center\">\n A cross-platform UI system with 150+ web primitives and shared native foundations.\n</p>\n\n<p align=\"center\">\n <a href=\"https://lumen.santi020k.com\">Documentation</a>\n ·\n <a href=\"https://lumen.santi020k.com/docs/components\">Components</a>\n ·\n <a href=\"https://www.figma.com/community/file/1662337342676541513\">Figma library</a>\n ·\n <a href=\"https://github.com/santi020k/lumen/issues\">Issues</a>\n</p>\n\n<p align=\"center\">\n <a href=\"https://github.com/santi020k/lumen/actions\"><img alt=\"GitHub Actions\" src=\"https://img.shields.io/github/actions/workflow/status/santi020k/lumen/ci.yml?branch=main&style=flat-square\"></a>\n <a href=\"https://github.com/santi020k/lumen/actions/workflows/codeql.yml\"><img alt=\"CodeQL\" 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=\"npm 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-111827?style=flat-square\"></a>\n</p>\n\nLumen gives applications a consistent visual language without tying them to one rendering model.\nIts packages share tokens, component contracts, styles, and interaction patterns while keeping each\nframework and platform's native authoring experience.\n\n- **Framework native:** Astro components, React primitives, and standards-based custom elements.\n- **Native foundations:** generated React Native, SwiftUI, and Jetpack Compose tokens preserve the\n same semantic roles without introducing DOM or CSS assumptions.\n- **Accessible by default:** semantic markup, keyboard paths, focus management, and reduced-motion\n support are built into the primitives.\n- **Standalone CSS:** no Tailwind configuration is required. Tailwind users get an explicit layer\n integration.\n- **Progressively enhanced:** Astro interactions use a small client runtime instead of requiring an\n application framework.\n- **Ready for design and AI workflows:** published Figma resources, a portable agent skill, an MCP\n server, `llms.txt`, and a machine-readable registry ship alongside the component system.\n- **Product-ready templates:** five responsive dashboard and application families are available as\n live previews and installable Astro, React, and Elements recipes.\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## Web quick start\n\nInstall the package for your framework:\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'\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 } 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 } 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 <label for=\"email\">Email</label>\n <lumen-input 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\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 platform status: Beta.** The shared token foundation is stable, while the React Native,\n> SwiftUI, and Compose component APIs are being validated through real applications and may evolve.\n> Review release notes when upgrading and share platform-specific feedback.\n\n## Native playgrounds\n\nThe repository includes three searchable, interactive galleries built from the real native\npackages:\n\n- [`apps/playground-react-native`](./apps/playground-react-native) runs through Expo on the web,\n iOS, and Android and includes EAS profiles for TestFlight, Android App Bundles, and APKs.\n- [`apps/playground-apple`](./apps/playground-apple) builds as an iOS Xcode app for devices and\n TestFlight, or as a 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.\n\nSee the [playground workflow](./docs/playgrounds.md) for run, capture, and distribution commands.\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## 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 SwiftUI foundations and native primitives |\n| [`lumen-compose`](./packages/compose) | Android library for Jetpack Compose foundations and native primitives |\n| [`@santi020k/lumen-icons-brand`](./packages/icons-brand) | Optional namespaced brand icons for the shared `Icon` API |\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\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\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- [`docs/cross-platform.md`](./docs/cross-platform.md) for the native architecture, support tiers,\n and component-parity policy.\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\n```bash\npnpm install\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\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.\n\n## License\n\nLumen is available under the [MIT License](./LICENSE).\n"
32951
+ "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\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## 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 `1.2.0` | `LumenUI` | Not applicable |\n| Android / Compose | Maven Central `com.santi020k:lumen-compose:0.4.0` | `com.santi020k.lumen` | Not applicable |\n| Wear OS / Compose | Maven Central `com.santi020k:lumen-compose-wear:0.4.0` | `com.santi020k.lumen` | Not applicable |\n| Package metadata | `@santi020k/lumen-core` | `@santi020k/lumen-core` | Not applicable |\n| Optional 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:0.4.0` from Maven Central and wrap content in `LumenTheme`. Native\nadapters do not load CSS or the Astro runtime.\n\nFor watchOS, use the focused `LumenWatch*` contracts from the same Swift package. For Wear OS,\ninstall `com.santi020k:lumen-compose-wear:0.4.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 Lucide catalog. When a product needs them,\ninstall 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.\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'\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. `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.\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. The pre-1.0 Astro and Elements `size=\"sm|lg\"`\nvisual alias is deprecated but remains available for incremental migration.\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\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`, `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- 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, and `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 `Chart` as the custom SVG/canvas escape hatch.\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. Install and register\n `@santi020k/lumen-icons-brand` for supported brand marks, then use names such as\n `brand:github`. Render project-specific or unsupported brand SVGs through the default `Icon`\n slot. Astro reports unknown named icons during 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- 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 emits the same data attributes for app-level adapters. `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` dispatch browser editing commands and emit\n `ui:editor-command`; use `data-ui-editor-value` for headings, links, and other value-bearing\n commands. Editable surfaces support common formatting shortcuts, synchronize toggle\n `aria-pressed` state, and emit `ui:editor-change` with HTML and plain text. Astro `UIPrimitives`\n and registered Web Components bind those controls directly; React apps can use\n `useRichTextEditor` for `rootProps`, `getCommandProps`, `getEditableProps`, and `executeCommand`.\n For Tiptap, ProseMirror, Lexical, or Markdown engines, keep the external editor\n package in the app and bridge toolbar buttons through the emitted command event instead of adding\n the editor engine to Lumen.\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` | `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 package also ships a small `lumen` binary. Individual components\nare addressable by name, including kebab-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 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.\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- Do not require consumers to configure Tailwind for Lumen components.\n- Prefer component props and composition before adding custom CSS.\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",
32952
+ "readme": "<p align=\"center\">\n <a href=\"https://lumen.santi020k.com\">\n <img src=\"./apps/docs/public/logo.svg\" alt=\"Lumen UI\" width=\"239\" height=\"60\">\n </a>\n</p>\n\n<h1 align=\"center\">Lumen UI</h1>\n\n<p align=\"center\">\n A cross-platform UI system with 150+ web primitives and shared native foundations.\n</p>\n\n<p align=\"center\">\n <a href=\"https://lumen.santi020k.com\">Documentation</a>\n ·\n <a href=\"https://lumen.santi020k.com/docs/components\">Components</a>\n ·\n <a href=\"https://www.figma.com/community/file/1662337342676541513\">Figma library</a>\n ·\n <a href=\"https://github.com/santi020k/lumen/issues\">Issues</a>\n</p>\n\n<p align=\"center\">\n <a href=\"https://github.com/santi020k/lumen/actions\"><img alt=\"GitHub Actions\" src=\"https://img.shields.io/github/actions/workflow/status/santi020k/lumen/ci.yml?branch=main&style=flat-square\"></a>\n <a href=\"https://github.com/santi020k/lumen/actions/workflows/codeql.yml\"><img alt=\"CodeQL\" 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=\"npm 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-111827?style=flat-square\"></a>\n</p>\n\nLumen gives applications a consistent visual language without tying them to one rendering model.\nIts packages share tokens, component contracts, styles, and interaction patterns while keeping each\nframework and platform's native authoring experience.\n\n- **Framework native:** Astro components, React primitives, and standards-based custom elements.\n- **Native foundations:** generated React Native, SwiftUI, and Jetpack Compose tokens preserve the\n same semantic roles without introducing DOM or CSS assumptions.\n- **Accessible by default:** semantic markup, keyboard paths, focus management, and reduced-motion\n support are built into the primitives.\n- **Standalone CSS:** no Tailwind configuration is required. Tailwind users get an explicit layer\n integration.\n- **Progressively enhanced:** Astro interactions use a small client runtime instead of requiring an\n application framework.\n- **Ready for design and AI workflows:** published Figma resources, a portable agent skill, an MCP\n server, `llms.txt`, a machine-readable registry, and a cross-platform release manifest ship\n alongside the component system.\n- **Product-ready templates:** five responsive dashboard and application families are available as\n live previews and installable Astro, React, and Elements recipes.\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## Web quick start\n\nInstall the package for your framework:\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'\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 } 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 } 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 <label for=\"email\">Email</label>\n <lumen-input 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\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 platform status: Beta.** The shared token foundation is stable, while the React Native,\n> SwiftUI, and Compose component APIs are being validated through real applications and may evolve.\n> Review release notes when upgrading and share platform-specific feedback.\n\n## Native playgrounds\n\nThe repository includes three searchable, interactive galleries built from the real native\npackages:\n\n- [`apps/playground-react-native`](./apps/playground-react-native) runs through Expo on the web,\n iOS, and Android and includes EAS profiles for TestFlight, Android App Bundles, and APKs.\n- [`apps/playground-apple`](./apps/playground-apple) builds as an iOS Xcode app for devices and\n TestFlight, or as a 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.\n\nSee the [playground workflow](./docs/playgrounds.md) for run, capture, and distribution commands.\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## 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 SwiftUI foundations and native primitives |\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 the shared `Icon` API |\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\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\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- [`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## 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\n```bash\npnpm install\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\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.\n\n## License\n\nLumen is available under the [MIT License](./LICENSE).\n"
32940
32953
  },
32941
32954
  "meta": {
32942
- "catalogHash": "da62e47060d9710896cd6ca5bfd676b97ae72cea966071535e76f895480cf745",
32955
+ "catalogHash": "b218bf14d64e7d7b5fea64afcf8894cd7a90dd030ca38c97d6831328f8f93b01",
32943
32956
  "componentCount": 156,
32944
- "nativeComponentCount": 49,
32957
+ "nativeComponentCount": 62,
32945
32958
  "packages": [
32946
32959
  "@santi020k/lumen-astro",
32947
32960
  "@santi020k/lumen-react",
@@ -32956,17 +32969,18 @@
32956
32969
  "@santi020k/lumen-astro": "1.4.0",
32957
32970
  "@santi020k/lumen-core": "1.4.0",
32958
32971
  "@santi020k/lumen-elements": "1.4.0",
32959
- "@santi020k/lumen-mcp": "1.4.0",
32972
+ "@santi020k/lumen-mcp": "1.5.0",
32960
32973
  "@santi020k/lumen-react": "1.4.0",
32961
- "@santi020k/lumen-react-native": "0.3.0",
32974
+ "@santi020k/lumen-react-native": "0.4.0",
32962
32975
  "@santi020k/lumen-tokens": "0.1.0",
32963
32976
  "LumenUI": "workspace",
32964
- "com.santi020k:lumen-compose": "workspace"
32977
+ "com.santi020k:lumen-compose": "workspace",
32978
+ "com.santi020k:lumen-compose-wear": "workspace"
32965
32979
  },
32966
32980
  "registryName": "lumen",
32967
32981
  "registryVersion": 1,
32968
- "schemaVersion": 4,
32969
- "serverVersion": "1.4.0"
32982
+ "schemaVersion": 5,
32983
+ "serverVersion": "1.5.0"
32970
32984
  },
32971
32985
  "nativeComponents": [
32972
32986
  {
@@ -36609,7 +36623,7 @@
36609
36623
  {
36610
36624
  "accessibility": "The group has a readable navigation label and every destination exposes selected and disabled state through native tab semantics.",
36611
36625
  "category": "Navigation",
36612
- "contract": "Controlled selection from a small set of peer app destinations",
36626
+ "contract": "Controlled selection from peer app destinations with re-selection callbacks and optional dot, text, or count badges",
36613
36627
  "guidance": "Use for a small set of peer app destinations. Keep hierarchical navigation, history, deep links, restoration, and screen rendering in the application router or native navigation stack.",
36614
36628
  "id": "navigation-bar",
36615
36629
  "implementations": {
@@ -36617,7 +36631,7 @@
36617
36631
  "api": [
36618
36632
  {
36619
36633
  "defaultValue": "Required",
36620
- "description": "Provides destination values, short labels, native icons, and disabled state.",
36634
+ "description": "Provides destination values, short labels, native icons, disabled state, and optional dot, text, or capped count badges.",
36621
36635
  "name": "items",
36622
36636
  "values": "List<LumenNavigationItem<Value>>"
36623
36637
  },
@@ -36633,6 +36647,12 @@
36633
36647
  "name": "onValueChange",
36634
36648
  "values": "(Value) -> Unit"
36635
36649
  },
36650
+ {
36651
+ "defaultValue": "null",
36652
+ "description": "Handles a second activation of the selected destination, such as scrolling to top or popping its stack.",
36653
+ "name": "onReselect",
36654
+ "values": "((Value) -> Unit)?"
36655
+ },
36636
36656
  {
36637
36657
  "defaultValue": "Primary navigation",
36638
36658
  "description": "Names the destination group for assistive technology.",
@@ -36640,7 +36660,7 @@
36640
36660
  "values": "String"
36641
36661
  }
36642
36662
  ],
36643
- "example": "LumenNavigationBar(\n items = destinations,\n selectedValue = destination,\n onValueChange = ::setDestination\n)",
36663
+ "example": "LumenNavigationBar(\n items = destinations,\n selectedValue = destination,\n onValueChange = ::setDestination,\n onReselect = ::scrollDestinationToTop\n)",
36644
36664
  "exportName": "LumenNavigationBar",
36645
36665
  "importStatement": "import com.santi020k.lumen.LumenNavigationBar",
36646
36666
  "install": "Include packages/compose from a shallow Git checkout pinned to the Lumen release tag, then add implementation(project(\":lumen-compose\")).",
@@ -36654,7 +36674,7 @@
36654
36674
  "api": [
36655
36675
  {
36656
36676
  "defaultValue": "Required",
36657
- "description": "Provides destination values, short labels, native icons, and disabled state.",
36677
+ "description": "Provides destination values, short labels, native icons, disabled state, and optional dot, text, or capped count badges.",
36658
36678
  "name": "items",
36659
36679
  "values": "readonly LumenNavigationItem[]"
36660
36680
  },
@@ -36670,6 +36690,12 @@
36670
36690
  "name": "onValueChange",
36671
36691
  "values": "(string) => void"
36672
36692
  },
36693
+ {
36694
+ "defaultValue": "undefined",
36695
+ "description": "Handles a second activation of the selected destination, such as scrolling to top or popping its stack.",
36696
+ "name": "onReselect",
36697
+ "values": "((value: string) => void)?"
36698
+ },
36673
36699
  {
36674
36700
  "defaultValue": "Primary navigation",
36675
36701
  "description": "Names the destination group for assistive technology.",
@@ -36677,7 +36703,7 @@
36677
36703
  "values": "String"
36678
36704
  }
36679
36705
  ],
36680
- "example": "<LumenNavigationBar\n items={destinations}\n value={destination}\n onValueChange={setDestination}\n/>",
36706
+ "example": "<LumenNavigationBar\n items={destinations}\n value={destination}\n onValueChange={setDestination}\n onReselect={scrollDestinationToTop}\n/>",
36681
36707
  "exportName": "LumenNavigationBar",
36682
36708
  "importStatement": "import { LumenNavigationBar } from '@santi020k/lumen-react-native'",
36683
36709
  "install": "pnpm add @santi020k/lumen-react-native",
@@ -36691,7 +36717,7 @@
36691
36717
  "api": [
36692
36718
  {
36693
36719
  "defaultValue": "Required",
36694
- "description": "Provides destination values, short labels, native icons, and disabled state.",
36720
+ "description": "Provides destination values, short labels, native icons, disabled state, and optional dot, text, or capped count badges.",
36695
36721
  "name": "items",
36696
36722
  "values": "[LumenNavigationItem<Selection>]"
36697
36723
  },
@@ -36707,6 +36733,12 @@
36707
36733
  "name": "onValueChange",
36708
36734
  "values": "Binding setter"
36709
36735
  },
36736
+ {
36737
+ "defaultValue": "nil",
36738
+ "description": "Handles a second activation of the selected destination, such as scrolling to top or popping its stack.",
36739
+ "name": "onReselect",
36740
+ "values": "((Selection) -> Void)?"
36741
+ },
36710
36742
  {
36711
36743
  "defaultValue": "Primary navigation",
36712
36744
  "description": "Names the destination group for assistive technology.",
@@ -36714,7 +36746,7 @@
36714
36746
  "values": "String"
36715
36747
  }
36716
36748
  ],
36717
- "example": "LumenNavigationBar(\n selection: $destination,\n items: destinations\n)",
36749
+ "example": "LumenNavigationBar(\n selection: $destination,\n items: destinations,\n onReselect: scrollDestinationToTop\n)",
36718
36750
  "exportName": "LumenNavigationBar",
36719
36751
  "importStatement": "import LumenUI",
36720
36752
  "install": "Add https://github.com/santi020k/lumen with Swift Package Manager, pin an exact or compatible release version, and link the LumenUI product to the application target.",
@@ -37565,6 +37597,256 @@
37565
37597
  "summary": "Present compact textual application status with optional trailing content.",
37566
37598
  "tier": "shared"
37567
37599
  },
37600
+ {
37601
+ "accessibility": "Hidden destinations stop receiving pointer input and are removed from the accessibility tree. The application keeps native list and router ownership.",
37602
+ "category": "Navigation",
37603
+ "contract": "Animated bottom navigation that hides after forward scroll travel and reveals on reverse travel or at the top",
37604
+ "guidance": "Use with the visibility hook when more content space is valuable during deliberate vertical scrolling. Keep safe areas, virtualization, and routing in application code.",
37605
+ "id": "collapsible-navigation-bar",
37606
+ "implementations": {
37607
+ "react-native": {
37608
+ "api": [
37609
+ {
37610
+ "defaultValue": "Required",
37611
+ "description": "Controls the animated expanded or collapsed state.",
37612
+ "name": "visible",
37613
+ "values": "boolean"
37614
+ },
37615
+ {
37616
+ "defaultValue": "Required",
37617
+ "description": "Defines peer destinations.",
37618
+ "name": "items",
37619
+ "values": "readonly LumenNavigationItem[]"
37620
+ },
37621
+ {
37622
+ "defaultValue": "Required",
37623
+ "description": "Identifies the selected destination.",
37624
+ "name": "value",
37625
+ "values": "string"
37626
+ },
37627
+ {
37628
+ "defaultValue": "Required",
37629
+ "description": "Requests destination selection.",
37630
+ "name": "onValueChange",
37631
+ "values": "(value: string) => void"
37632
+ },
37633
+ {
37634
+ "defaultValue": "undefined",
37635
+ "description": "Styles the animated outer container without replacing its collapse dimensions.",
37636
+ "name": "containerStyle",
37637
+ "values": "StyleProp<ViewStyle>"
37638
+ },
37639
+ {
37640
+ "defaultValue": "undefined",
37641
+ "description": "Adds compact application-owned content that collapses with the navigation bar.",
37642
+ "name": "accessory",
37643
+ "values": "ReactNode"
37644
+ }
37645
+ ],
37646
+ "example": "const navigation = useLumenNavigationBarVisibility()\n\n<FlatList\n data={items}\n onScroll={navigation.onScroll}\n scrollEventThrottle={16}\n renderItem={renderItem}\n/>\n<LumenCollapsibleNavigationBar\n items={destinations}\n value={destination}\n visible={navigation.visible}\n onValueChange={setDestination}\n/>",
37647
+ "exportName": "LumenCollapsibleNavigationBar",
37648
+ "importStatement": "import { LumenCollapsibleNavigationBar } from '@santi020k/lumen-react-native'",
37649
+ "install": "pnpm add @santi020k/lumen-react-native",
37650
+ "language": "tsx",
37651
+ "packageName": "@santi020k/lumen-react-native",
37652
+ "setup": "Mount one LumenProvider near the application root. Native adapters do not load CSS or the Astro runtime.",
37653
+ "sourceFile": "packages/react-native/src/index.ts",
37654
+ "symbol": "LumenCollapsibleNavigationBar"
37655
+ }
37656
+ },
37657
+ "name": "Collapsible navigation bar",
37658
+ "summary": "Animate a React Native destination bar in response to native vertical scroll travel.",
37659
+ "supportedPlatforms": [
37660
+ "iOS",
37661
+ "Android"
37662
+ ],
37663
+ "tier": "platform-specific"
37664
+ },
37665
+ {
37666
+ "accessibility": "The container preserves the semantics of its application-owned status and actions. When supplied to the collapsible bar, it leaves the accessibility tree with navigation.",
37667
+ "category": "Navigation",
37668
+ "contract": "Compact status or action content above a React Native bottom navigation bar",
37669
+ "guidance": "Use for one compact persistent activity such as playback, upload, recording, or an active call. Keep safe-area padding and domain state in the application.",
37670
+ "id": "navigation-accessory",
37671
+ "implementations": {
37672
+ "react-native": {
37673
+ "api": [
37674
+ {
37675
+ "defaultValue": "Required",
37676
+ "description": "Provides compact status or independently accessible actions.",
37677
+ "name": "children",
37678
+ "values": "ReactNode"
37679
+ },
37680
+ {
37681
+ "defaultValue": "undefined",
37682
+ "description": "Extends the fixed compact native container.",
37683
+ "name": "style",
37684
+ "values": "StyleProp<ViewStyle>"
37685
+ }
37686
+ ],
37687
+ "example": "<LumenCollapsibleNavigationBar\n accessory={(\n <LumenText variant=\"label\">Uploading 3 files</LumenText>\n )}\n items={destinations}\n value={destination}\n visible={navigation.visible}\n onValueChange={setDestination}\n/>",
37688
+ "exportName": "LumenNavigationAccessory",
37689
+ "importStatement": "import { LumenNavigationAccessory } from '@santi020k/lumen-react-native'",
37690
+ "install": "pnpm add @santi020k/lumen-react-native",
37691
+ "language": "tsx",
37692
+ "packageName": "@santi020k/lumen-react-native",
37693
+ "setup": "Mount one LumenProvider near the application root. Native adapters do not load CSS or the Astro runtime.",
37694
+ "sourceFile": "packages/react-native/src/index.ts",
37695
+ "symbol": "LumenNavigationAccessory"
37696
+ }
37697
+ },
37698
+ "name": "Navigation accessory",
37699
+ "summary": "Place compact application status or actions above React Native bottom navigation.",
37700
+ "supportedPlatforms": [
37701
+ "iOS",
37702
+ "Android"
37703
+ ],
37704
+ "tier": "platform-specific"
37705
+ },
37706
+ {
37707
+ "accessibility": "The behavior observes but does not consume nested-scroll input. After exit, the Material navigation bar is absent from layout and semantics.",
37708
+ "category": "Navigation",
37709
+ "contract": "Threshold-based nested-scroll visibility state for a Material navigation bar",
37710
+ "guidance": "Attach the modifier above a vertical lazy or scrollable child and pass the same state to the bottom bar. Keep Scaffold, navigation, and collection state application-owned.",
37711
+ "id": "navigation-bar-scroll-behavior",
37712
+ "implementations": {
37713
+ "compose": {
37714
+ "api": [
37715
+ {
37716
+ "defaultValue": "true",
37717
+ "description": "Sets the remembered initial visibility.",
37718
+ "name": "initiallyVisible",
37719
+ "values": "Boolean"
37720
+ },
37721
+ {
37722
+ "defaultValue": "16.dp",
37723
+ "description": "Sets deliberate travel required before visibility changes.",
37724
+ "name": "threshold",
37725
+ "values": "Dp"
37726
+ },
37727
+ {
37728
+ "defaultValue": "Available",
37729
+ "description": "Reveals navigation for application-owned events.",
37730
+ "name": "show()",
37731
+ "values": "function"
37732
+ },
37733
+ {
37734
+ "defaultValue": "Available",
37735
+ "description": "Hides navigation for application-owned events.",
37736
+ "name": "hide()",
37737
+ "values": "function"
37738
+ }
37739
+ ],
37740
+ "example": "val navigationScrollState = rememberLumenNavigationBarScrollState()\n\nScaffold(\n modifier = Modifier.lumenNavigationBarScrollBehavior(navigationScrollState),\n bottomBar = {\n LumenNavigationBar(\n items = destinations,\n selectedValue = destination,\n onValueChange = ::setDestination,\n scrollState = navigationScrollState\n )\n }\n) { padding ->\n LazyColumn(contentPadding = padding) { /* application content */ }\n}",
37741
+ "exportName": "LumenNavigationBarScrollState",
37742
+ "importStatement": "import com.santi020k.lumen.LumenNavigationBarScrollState",
37743
+ "install": "Include packages/compose from a shallow Git checkout pinned to the Lumen release tag, then add implementation(project(\":lumen-compose\")).",
37744
+ "language": "kotlin",
37745
+ "packageName": "com.santi020k:lumen-compose",
37746
+ "setup": "Wrap application content in LumenTheme and preserve Compose state and navigation.",
37747
+ "sourceFile": "packages/compose/src/main/kotlin/com/santi020k/lumen/PlatformComponents.kt",
37748
+ "symbol": "LumenNavigationBarScrollState"
37749
+ }
37750
+ },
37751
+ "name": "Navigation bar scroll behavior",
37752
+ "summary": "Coordinate Material bottom navigation visibility through the Compose nested-scroll chain.",
37753
+ "supportedPlatforms": [
37754
+ "Android"
37755
+ ],
37756
+ "tier": "platform-specific"
37757
+ },
37758
+ {
37759
+ "accessibility": "Application-owned content retains its native semantics and exits the layout together with hidden bottom navigation.",
37760
+ "category": "Navigation",
37761
+ "contract": "Compact token-aware content above Material bottom navigation coordinated with its scroll visibility",
37762
+ "guidance": "Place directly above LumenNavigationBar for one compact activity such as playback, upload, recording, or an active call. Keep domain state and actions application-owned.",
37763
+ "id": "navigation-bar-accessory",
37764
+ "implementations": {
37765
+ "compose": {
37766
+ "api": [
37767
+ {
37768
+ "defaultValue": "null",
37769
+ "description": "Coordinates entry and exit with scroll-responsive navigation.",
37770
+ "name": "scrollState",
37771
+ "values": "LumenNavigationBarScrollState?"
37772
+ },
37773
+ {
37774
+ "defaultValue": "Required",
37775
+ "description": "Provides compact status or independently accessible actions.",
37776
+ "name": "content",
37777
+ "values": "@Composable () -> Unit"
37778
+ }
37779
+ ],
37780
+ "example": "LumenNavigationBarAccessory(scrollState = navigationScrollState) {\n LumenText(\"Uploading 3 files\", variant = LumenTextVariant.Label)\n}",
37781
+ "exportName": "LumenNavigationBarAccessory",
37782
+ "importStatement": "import com.santi020k.lumen.LumenNavigationBarAccessory",
37783
+ "install": "Include packages/compose from a shallow Git checkout pinned to the Lumen release tag, then add implementation(project(\":lumen-compose\")).",
37784
+ "language": "kotlin",
37785
+ "packageName": "com.santi020k:lumen-compose",
37786
+ "setup": "Wrap application content in LumenTheme and preserve Compose state and navigation.",
37787
+ "sourceFile": "packages/compose/src/main/kotlin/com/santi020k/lumen/PlatformComponents.kt",
37788
+ "symbol": "LumenNavigationBarAccessory"
37789
+ }
37790
+ },
37791
+ "name": "Navigation bar accessory",
37792
+ "summary": "Place compact application status or actions above Material bottom navigation.",
37793
+ "supportedPlatforms": [
37794
+ "Android"
37795
+ ],
37796
+ "tier": "platform-specific"
37797
+ },
37798
+ {
37799
+ "accessibility": "Material supplies navigation bar or rail semantics, selected and disabled state, keyboard traversal, and badge placement while destination content remains application-owned.",
37800
+ "category": "Navigation",
37801
+ "contract": "Material adaptive navigation that switches between bottom bar and rail from window size and device posture",
37802
+ "guidance": "Use as the primary app surface when navigation should change between a bottom bar and rail across phones, tablets, folding devices, split screen, and desktop windows. Keep routing and destination state controlled.",
37803
+ "id": "adaptive-navigation-scaffold",
37804
+ "implementations": {
37805
+ "compose": {
37806
+ "api": [
37807
+ {
37808
+ "defaultValue": "Required",
37809
+ "description": "Provides controlled destinations, badges, icons, and disabled state.",
37810
+ "name": "items",
37811
+ "values": "List<LumenNavigationItem<Value>>"
37812
+ },
37813
+ {
37814
+ "defaultValue": "Required",
37815
+ "description": "Identifies the active application-owned destination.",
37816
+ "name": "selectedValue",
37817
+ "values": "Value"
37818
+ },
37819
+ {
37820
+ "defaultValue": "Application-provided",
37821
+ "description": "Separates destination changes from repeated activation.",
37822
+ "name": "onValueChange / onReselect",
37823
+ "values": "callbacks"
37824
+ },
37825
+ {
37826
+ "defaultValue": "Required",
37827
+ "description": "Renders the selected destination without transferring router ownership.",
37828
+ "name": "content",
37829
+ "values": "@Composable () -> Unit"
37830
+ }
37831
+ ],
37832
+ "example": "LumenAdaptiveNavigationScaffold(\n items = destinations,\n selectedValue = destination,\n onValueChange = ::setDestination,\n onReselect = ::scrollDestinationToTop\n) {\n DestinationContent(destination)\n}",
37833
+ "exportName": "LumenAdaptiveNavigationScaffold",
37834
+ "importStatement": "import com.santi020k.lumen.LumenAdaptiveNavigationScaffold",
37835
+ "install": "Include packages/compose from a shallow Git checkout pinned to the Lumen release tag, then add implementation(project(\":lumen-compose\")).",
37836
+ "language": "kotlin",
37837
+ "packageName": "com.santi020k:lumen-compose",
37838
+ "setup": "Wrap application content in LumenTheme and preserve Compose state and navigation.",
37839
+ "sourceFile": "packages/compose/src/main/kotlin/com/santi020k/lumen/PlatformComponents.kt",
37840
+ "symbol": "LumenAdaptiveNavigationScaffold"
37841
+ }
37842
+ },
37843
+ "name": "Adaptive navigation scaffold",
37844
+ "summary": "Adapt Material primary navigation between bottom bar and rail as the window changes.",
37845
+ "supportedPlatforms": [
37846
+ "Android"
37847
+ ],
37848
+ "tier": "platform-specific"
37849
+ },
37568
37850
  {
37569
37851
  "accessibility": "The native refresh gesture remains owned by the scroll container. Supply an accessibility label that describes the refreshed content.",
37570
37852
  "category": "Feedback",
@@ -37680,7 +37962,7 @@
37680
37962
  {
37681
37963
  "accessibility": "The required content description names the icon-only action while the control preserves native Material button semantics.",
37682
37964
  "category": "Actions",
37683
- "contract": "Material-native floating action with semantic intent and small or regular sizing",
37965
+ "contract": "Material-native floating action with semantic intent, sizing, and optional hide or follow-navigation behavior",
37684
37966
  "guidance": "Use for one prominent Material screen action. Keep navigation bars and scaffold placement in application code.",
37685
37967
  "id": "floating-action-button",
37686
37968
  "implementations": {
@@ -37715,9 +37997,21 @@
37715
37997
  "description": "Selects Material-native FAB geometry.",
37716
37998
  "name": "size",
37717
37999
  "values": "regular · small"
38000
+ },
38001
+ {
38002
+ "defaultValue": "alwaysVisible",
38003
+ "description": "Keeps, hides, or repositions the action as scroll-responsive navigation changes.",
38004
+ "name": "navigationBehavior",
38005
+ "values": "alwaysVisible · hideWithNavigation · followNavigation"
38006
+ },
38007
+ {
38008
+ "defaultValue": "null",
38009
+ "description": "Supplies the navigation visibility used by coordinated behavior.",
38010
+ "name": "scrollState",
38011
+ "values": "LumenNavigationBarScrollState?"
37718
38012
  }
37719
38013
  ],
37720
- "example": "LumenFloatingActionButton(\n imageVector = Icons.Default.Add,\n contentDescription = \"Create project\",\n onClick = ::createProject\n)",
38014
+ "example": "LumenFloatingActionButton(\n imageVector = Icons.Default.Add,\n contentDescription = \"Create project\",\n onClick = ::createProject,\n scrollState = navigationScrollState,\n navigationBehavior = LumenFloatingActionButtonNavigationBehavior.HideWithNavigation\n)",
37721
38015
  "exportName": "LumenFloatingActionButton",
37722
38016
  "importStatement": "import com.santi020k.lumen.LumenFloatingActionButton",
37723
38017
  "install": "Include packages/compose from a shallow Git checkout pinned to the Lumen release tag, then add implementation(project(\":lumen-compose\")).",
@@ -37735,6 +38029,139 @@
37735
38029
  ],
37736
38030
  "tier": "platform-specific"
37737
38031
  },
38032
+ {
38033
+ "accessibility": "SwiftUI preserves the native link role, visible label, keyboard focus, disabled state, and URL-opening behavior.",
38034
+ "category": "Actions",
38035
+ "contract": "Token-aware URL action backed by SwiftUI native link opening",
38036
+ "guidance": "Use for external or system URL actions that need Lumen semantic treatment. Keep in-app routing and URL policy in application code.",
38037
+ "id": "link",
38038
+ "implementations": {
38039
+ "swiftui": {
38040
+ "api": [
38041
+ {
38042
+ "defaultValue": "Required",
38043
+ "description": "Provides visible link content.",
38044
+ "name": "label",
38045
+ "values": "LocalizedStringKey or custom View"
38046
+ },
38047
+ {
38048
+ "defaultValue": "Required",
38049
+ "description": "Provides the native URL destination.",
38050
+ "name": "destination",
38051
+ "values": "URL"
38052
+ },
38053
+ {
38054
+ "defaultValue": "false",
38055
+ "description": "Adds a decorative external-destination symbol without changing the accessible label.",
38056
+ "name": "showsExternalIndicator",
38057
+ "values": "Bool"
38058
+ }
38059
+ ],
38060
+ "example": "LumenLink(\n \"Read privacy policy\",\n destination: privacyPolicyURL,\n showsExternalIndicator: true\n)",
38061
+ "exportName": "LumenLink",
38062
+ "importStatement": "import LumenUI",
38063
+ "install": "Add https://github.com/santi020k/lumen with Swift Package Manager, pin an exact or compatible release version, and link the LumenUI product to the application target.",
38064
+ "language": "swift",
38065
+ "packageName": "LumenUI",
38066
+ "setup": "Apply .lumenTheme(...) near the SwiftUI application root and preserve native environment behavior.",
38067
+ "sourceFile": "packages/swift/Sources/LumenUI/AppleComponents.swift",
38068
+ "symbol": "LumenLink"
38069
+ }
38070
+ },
38071
+ "name": "Link",
38072
+ "summary": "Open an Apple URL using native SwiftUI behavior and Lumen semantic styling.",
38073
+ "supportedPlatforms": [
38074
+ "iOS",
38075
+ "macOS"
38076
+ ],
38077
+ "tier": "platform-specific"
38078
+ },
38079
+ {
38080
+ "accessibility": "SwiftUI retains native tab focus, safe-area adjustment, animation, and reduced-motion behavior while the application owns selection and navigation state.",
38081
+ "category": "Navigation",
38082
+ "contract": "Availability-safe native tab-bar minimization with automatic, never, and scroll-direction policies",
38083
+ "guidance": "Apply to a native TabView. iOS 26 uses system minimization on iPhone; earlier supported releases retain the normal tab bar without a custom imitation.",
38084
+ "id": "tab-bar-minimization",
38085
+ "implementations": {
38086
+ "swiftui": {
38087
+ "api": [
38088
+ {
38089
+ "defaultValue": "Required",
38090
+ "description": "Selects the native minimization policy.",
38091
+ "name": "behavior",
38092
+ "values": "automatic · never · onScrollDown · onScrollUp"
38093
+ },
38094
+ {
38095
+ "defaultValue": "Required",
38096
+ "description": "Uses native minimization on iOS 26 and a no-op compatibility fallback before iOS 26.",
38097
+ "name": "availability",
38098
+ "values": "iOS 16 or newer"
38099
+ }
38100
+ ],
38101
+ "example": "TabView(selection: $selection) {\n FeedView().tabItem { Label(\"Feed\", systemImage: \"rectangle.stack\") }\n ProfileView().tabItem { Label(\"Profile\", systemImage: \"person\") }\n}\n.lumenTabBarMinimizeBehavior(.onScrollDown)",
38102
+ "exportName": "lumenTabBarMinimizeBehavior",
38103
+ "importStatement": "import LumenUI",
38104
+ "install": "Add https://github.com/santi020k/lumen with Swift Package Manager, pin an exact or compatible release version, and link the LumenUI product to the application target.",
38105
+ "language": "swift",
38106
+ "packageName": "LumenUI",
38107
+ "setup": "Apply .lumenTheme(...) near the SwiftUI application root and preserve native environment behavior.",
38108
+ "sourceFile": "packages/swift/Sources/LumenUI/NavigationComponents.swift",
38109
+ "symbol": "lumenTabBarMinimizeBehavior"
38110
+ }
38111
+ },
38112
+ "name": "Tab bar minimization",
38113
+ "summary": "Let an iPhone native tab bar minimize and expand in response to scrolling.",
38114
+ "supportedPlatforms": [
38115
+ "iOS"
38116
+ ],
38117
+ "tier": "platform-specific"
38118
+ },
38119
+ {
38120
+ "accessibility": "Expanded and compact content preserve their own readable labels, controls, and traversal order while SwiftUI owns placement changes.",
38121
+ "category": "Navigation",
38122
+ "contract": "Adaptive expanded and compact content above or inline with a native tab bar",
38123
+ "guidance": "Use for a mini player, call, upload, recording, or persistent status. Keep accessory domain state and actions application-owned.",
38124
+ "id": "tab-accessory",
38125
+ "implementations": {
38126
+ "swiftui": {
38127
+ "api": [
38128
+ {
38129
+ "defaultValue": "Required",
38130
+ "description": "Provides content displayed above a regular-size tab bar.",
38131
+ "name": "expanded",
38132
+ "values": "@ViewBuilder () -> View"
38133
+ },
38134
+ {
38135
+ "defaultValue": "Required",
38136
+ "description": "Provides reduced content displayed inline with a minimized tab bar.",
38137
+ "name": "compact",
38138
+ "values": "@ViewBuilder () -> View"
38139
+ },
38140
+ {
38141
+ "defaultValue": "true",
38142
+ "description": "Controls whether the surrounding tab accessory modifier presents content.",
38143
+ "name": "isEnabled",
38144
+ "values": "Bool"
38145
+ }
38146
+ ],
38147
+ "example": "TabView {\n FeedView().tabItem { Label(\"Feed\", systemImage: \"rectangle.stack\") }\n}\n.lumenTabViewBottomAccessory {\n LumenTabAccessory {\n ExpandedUploadStatus()\n } compact: {\n CompactUploadStatus()\n }\n}",
38148
+ "exportName": "LumenTabAccessory",
38149
+ "importStatement": "import LumenUI",
38150
+ "install": "Add https://github.com/santi020k/lumen with Swift Package Manager, pin an exact or compatible release version, and link the LumenUI product to the application target.",
38151
+ "language": "swift",
38152
+ "packageName": "LumenUI",
38153
+ "setup": "Apply .lumenTheme(...) near the SwiftUI application root and preserve native environment behavior.",
38154
+ "sourceFile": "packages/swift/Sources/LumenUI/NavigationComponents.swift",
38155
+ "symbol": "LumenTabAccessory"
38156
+ }
38157
+ },
38158
+ "name": "Tab accessory",
38159
+ "summary": "Adapt application-owned accessory content to expanded and inline tab-bar placement.",
38160
+ "supportedPlatforms": [
38161
+ "iOS"
38162
+ ],
38163
+ "tier": "platform-specific"
38164
+ },
37738
38165
  {
37739
38166
  "accessibility": "Recording, current shortcut, validation error, cancel, change, and clear states all have visible native labels.",
37740
38167
  "category": "macOS utilities",
@@ -38735,6 +39162,436 @@
38735
39162
  "name": "Share button",
38736
39163
  "summary": "Open the operating system share surface from a token-aware native action.",
38737
39164
  "tier": "shared"
39165
+ },
39166
+ {
39167
+ "accessibility": "Requires one concise action label, preserves native button semantics, and exposes disabled state without duplicating visible content.",
39168
+ "category": "Actions",
39169
+ "contract": "At-a-glance round primary action with semantic intent, disabled state, and wearable sizing",
39170
+ "guidance": "Use for one essential wrist action. Keep haptics, health or safety policy, synchronization, and command handling in the application.",
39171
+ "id": "wearable-action",
39172
+ "implementations": {
39173
+ "compose": {
39174
+ "api": [
39175
+ {
39176
+ "defaultValue": "Required",
39177
+ "description": "Names the essential action.",
39178
+ "name": "accessibilityLabel",
39179
+ "values": "Native localized text"
39180
+ },
39181
+ {
39182
+ "defaultValue": "Required",
39183
+ "description": "Runs application-owned behavior.",
39184
+ "name": "onClick",
39185
+ "values": "() -> Unit"
39186
+ },
39187
+ {
39188
+ "defaultValue": "brand",
39189
+ "description": "Applies semantic intent.",
39190
+ "name": "tone",
39191
+ "values": "brand · accent · success · warning · danger · neutral"
39192
+ },
39193
+ {
39194
+ "defaultValue": "120",
39195
+ "description": "Clamps the round target to wearable-safe bounds.",
39196
+ "name": "dimension",
39197
+ "values": "Native display units"
39198
+ },
39199
+ {
39200
+ "defaultValue": "true",
39201
+ "description": "Controls action and disabled presentation.",
39202
+ "name": "enabled",
39203
+ "values": "Boolean"
39204
+ }
39205
+ ],
39206
+ "example": "LumenWearActionButton(\n accessibilityLabel = \"Start contraction\",\n onClick = ::startContraction\n) { TimerLabel() }",
39207
+ "exportName": "LumenWearActionButton",
39208
+ "importStatement": "import com.santi020k.lumen.LumenWearActionButton",
39209
+ "install": "Include packages/compose from a shallow Git checkout pinned to the Lumen release tag, then add implementation(project(\":wear\")).",
39210
+ "language": "kotlin",
39211
+ "packageName": "com.santi020k:lumen-compose-wear",
39212
+ "setup": "Wrap wearable content in LumenWearTheme inside the application-owned Wear Material theme.",
39213
+ "sourceFile": "packages/compose/wear/src/main/kotlin/com/santi020k/lumen/wear/WearComponents.kt",
39214
+ "symbol": "LumenWearActionButton"
39215
+ },
39216
+ "swiftui": {
39217
+ "api": [
39218
+ {
39219
+ "defaultValue": "Required",
39220
+ "description": "Names the essential action.",
39221
+ "name": "accessibilityLabel",
39222
+ "values": "Native localized text"
39223
+ },
39224
+ {
39225
+ "defaultValue": "Required",
39226
+ "description": "Runs application-owned behavior.",
39227
+ "name": "action",
39228
+ "values": "() -> Unit"
39229
+ },
39230
+ {
39231
+ "defaultValue": "brand",
39232
+ "description": "Applies semantic intent.",
39233
+ "name": "tone",
39234
+ "values": "brand · accent · success · warning · danger · neutral"
39235
+ },
39236
+ {
39237
+ "defaultValue": "120",
39238
+ "description": "Clamps the round target to wearable-safe bounds.",
39239
+ "name": "dimension",
39240
+ "values": "Native display units"
39241
+ },
39242
+ {
39243
+ "defaultValue": "true",
39244
+ "description": "Controls action and disabled presentation.",
39245
+ "name": "enabled",
39246
+ "values": "Boolean"
39247
+ }
39248
+ ],
39249
+ "example": "LumenWatchActionButton(\"Start contraction\", action: startContraction) {\n TimerLabel()\n}",
39250
+ "exportName": "LumenWatchActionButton",
39251
+ "importStatement": "import LumenUI",
39252
+ "install": "Add https://github.com/santi020k/lumen with Swift Package Manager, pin an exact or compatible release version, and link the LumenUI product to the application target.",
39253
+ "language": "swift",
39254
+ "packageName": "LumenUI",
39255
+ "setup": "Apply .lumenTheme(...) near the SwiftUI application root and preserve native environment behavior.",
39256
+ "sourceFile": "packages/swift/Sources/LumenUI/WatchComponents.swift",
39257
+ "symbol": "LumenWatchActionButton"
39258
+ }
39259
+ },
39260
+ "name": "Wearable action",
39261
+ "summary": "Present one at-a-glance, round primary action on watchOS or Wear OS.",
39262
+ "tier": "shared"
39263
+ },
39264
+ {
39265
+ "accessibility": "Exposes a normalized progress range while application-owned inner content retains its own readable semantics.",
39266
+ "category": "Data display",
39267
+ "contract": "Clamped circular progress surrounding app-owned wearable content",
39268
+ "guidance": "Use for short at-a-glance progress. Keep long-running background work, Always On policy, and timeline updates application-owned.",
39269
+ "id": "wearable-progress",
39270
+ "implementations": {
39271
+ "compose": {
39272
+ "api": [
39273
+ {
39274
+ "defaultValue": "Required",
39275
+ "description": "Provides current progress.",
39276
+ "name": "value",
39277
+ "values": "Finite numeric value"
39278
+ },
39279
+ {
39280
+ "defaultValue": "1",
39281
+ "description": "Defines the upper bound.",
39282
+ "name": "maximum",
39283
+ "values": "Positive numeric value"
39284
+ },
39285
+ {
39286
+ "defaultValue": "brand",
39287
+ "description": "Applies semantic progress color.",
39288
+ "name": "tone",
39289
+ "values": "brand · accent · success · warning · danger · neutral"
39290
+ },
39291
+ {
39292
+ "defaultValue": "4",
39293
+ "description": "Controls the clamped ring stroke.",
39294
+ "name": "lineWidth",
39295
+ "values": "Native display units"
39296
+ },
39297
+ {
39298
+ "defaultValue": "Required",
39299
+ "description": "Provides application-owned center content.",
39300
+ "name": "content",
39301
+ "values": "Native view slot"
39302
+ }
39303
+ ],
39304
+ "example": "LumenWearProgressRing(value = elapsed, maximum = threshold) {\n TimerLabel()\n}",
39305
+ "exportName": "LumenWearProgressRing",
39306
+ "importStatement": "import com.santi020k.lumen.LumenWearProgressRing",
39307
+ "install": "Include packages/compose from a shallow Git checkout pinned to the Lumen release tag, then add implementation(project(\":wear\")).",
39308
+ "language": "kotlin",
39309
+ "packageName": "com.santi020k:lumen-compose-wear",
39310
+ "setup": "Wrap wearable content in LumenWearTheme inside the application-owned Wear Material theme.",
39311
+ "sourceFile": "packages/compose/wear/src/main/kotlin/com/santi020k/lumen/wear/WearComponents.kt",
39312
+ "symbol": "LumenWearProgressRing"
39313
+ },
39314
+ "swiftui": {
39315
+ "api": [
39316
+ {
39317
+ "defaultValue": "Required",
39318
+ "description": "Provides current progress.",
39319
+ "name": "value",
39320
+ "values": "Finite numeric value"
39321
+ },
39322
+ {
39323
+ "defaultValue": "1",
39324
+ "description": "Defines the upper bound.",
39325
+ "name": "maximum",
39326
+ "values": "Positive numeric value"
39327
+ },
39328
+ {
39329
+ "defaultValue": "brand",
39330
+ "description": "Applies semantic progress color.",
39331
+ "name": "tone",
39332
+ "values": "brand · accent · success · warning · danger · neutral"
39333
+ },
39334
+ {
39335
+ "defaultValue": "4",
39336
+ "description": "Controls the clamped ring stroke.",
39337
+ "name": "lineWidth",
39338
+ "values": "Native display units"
39339
+ },
39340
+ {
39341
+ "defaultValue": "Required",
39342
+ "description": "Provides application-owned center content.",
39343
+ "name": "content",
39344
+ "values": "Native view slot"
39345
+ }
39346
+ ],
39347
+ "example": "LumenWatchProgressRing(value: elapsed, maximum: threshold) {\n TimerLabel()\n}",
39348
+ "exportName": "LumenWatchProgressRing",
39349
+ "importStatement": "import LumenUI",
39350
+ "install": "Add https://github.com/santi020k/lumen with Swift Package Manager, pin an exact or compatible release version, and link the LumenUI product to the application target.",
39351
+ "language": "swift",
39352
+ "packageName": "LumenUI",
39353
+ "setup": "Apply .lumenTheme(...) near the SwiftUI application root and preserve native environment behavior.",
39354
+ "sourceFile": "packages/swift/Sources/LumenUI/WatchComponents.swift",
39355
+ "symbol": "LumenWatchProgressRing"
39356
+ }
39357
+ },
39358
+ "name": "Wearable progress",
39359
+ "summary": "Surround wearable content with clamped semantic circular progress.",
39360
+ "tier": "shared"
39361
+ },
39362
+ {
39363
+ "accessibility": "Keeps status meaning in concise text instead of relying on semantic color alone.",
39364
+ "category": "Feedback",
39365
+ "contract": "Compact text-first wearable status using semantic tone",
39366
+ "guidance": "Use for a short, high-value wrist status. Do not use a badge as the only representation of an urgent notification.",
39367
+ "id": "wearable-status",
39368
+ "implementations": {
39369
+ "compose": {
39370
+ "api": [
39371
+ {
39372
+ "defaultValue": "Required",
39373
+ "description": "Provides concise status meaning.",
39374
+ "name": "text",
39375
+ "values": "Native localized text"
39376
+ },
39377
+ {
39378
+ "defaultValue": "neutral",
39379
+ "description": "Applies semantic emphasis.",
39380
+ "name": "tone",
39381
+ "values": "brand · accent · success · warning · danger · neutral"
39382
+ },
39383
+ {
39384
+ "defaultValue": "nil",
39385
+ "description": "Adds a supporting wearable glyph.",
39386
+ "name": "leading",
39387
+ "values": "Optional native visual"
39388
+ }
39389
+ ],
39390
+ "example": "LumenWearStatus(text = \"Phone unavailable\", tone = LumenWearTone.Warning)",
39391
+ "exportName": "LumenWearStatus",
39392
+ "importStatement": "import com.santi020k.lumen.LumenWearStatus",
39393
+ "install": "Include packages/compose from a shallow Git checkout pinned to the Lumen release tag, then add implementation(project(\":wear\")).",
39394
+ "language": "kotlin",
39395
+ "packageName": "com.santi020k:lumen-compose-wear",
39396
+ "setup": "Wrap wearable content in LumenWearTheme inside the application-owned Wear Material theme.",
39397
+ "sourceFile": "packages/compose/wear/src/main/kotlin/com/santi020k/lumen/wear/WearComponents.kt",
39398
+ "symbol": "LumenWearStatus"
39399
+ },
39400
+ "swiftui": {
39401
+ "api": [
39402
+ {
39403
+ "defaultValue": "Required",
39404
+ "description": "Provides concise status meaning.",
39405
+ "name": "title",
39406
+ "values": "Native localized text"
39407
+ },
39408
+ {
39409
+ "defaultValue": "neutral",
39410
+ "description": "Applies semantic emphasis.",
39411
+ "name": "tone",
39412
+ "values": "brand · accent · success · warning · danger · neutral"
39413
+ },
39414
+ {
39415
+ "defaultValue": "nil",
39416
+ "description": "Adds a supporting wearable glyph.",
39417
+ "name": "systemName",
39418
+ "values": "Optional native visual"
39419
+ }
39420
+ ],
39421
+ "example": "LumenWatchStatus(\"5-1-1\", systemName: \"cross.case.fill\", tone: .danger)",
39422
+ "exportName": "LumenWatchStatus",
39423
+ "importStatement": "import LumenUI",
39424
+ "install": "Add https://github.com/santi020k/lumen with Swift Package Manager, pin an exact or compatible release version, and link the LumenUI product to the application target.",
39425
+ "language": "swift",
39426
+ "packageName": "LumenUI",
39427
+ "setup": "Apply .lumenTheme(...) near the SwiftUI application root and preserve native environment behavior.",
39428
+ "sourceFile": "packages/swift/Sources/LumenUI/WatchComponents.swift",
39429
+ "symbol": "LumenWatchStatus"
39430
+ }
39431
+ },
39432
+ "name": "Wearable status",
39433
+ "summary": "Show a compact, text-first semantic status on the wrist.",
39434
+ "tier": "shared"
39435
+ },
39436
+ {
39437
+ "accessibility": "Combines label, value, and optional detail into one concise readable metric.",
39438
+ "category": "Data display",
39439
+ "contract": "At-a-glance wearable label, value, and optional detail",
39440
+ "guidance": "Use for one high-priority value with a short label. Avoid dashboard grids that overload small round screens.",
39441
+ "id": "wearable-metric",
39442
+ "implementations": {
39443
+ "compose": {
39444
+ "api": [
39445
+ {
39446
+ "defaultValue": "Required",
39447
+ "description": "Names the metric.",
39448
+ "name": "label",
39449
+ "values": "Native localized text"
39450
+ },
39451
+ {
39452
+ "defaultValue": "Required",
39453
+ "description": "Provides the formatted value.",
39454
+ "name": "value",
39455
+ "values": "String"
39456
+ },
39457
+ {
39458
+ "defaultValue": "nil",
39459
+ "description": "Adds brief supporting context.",
39460
+ "name": "detail",
39461
+ "values": "Optional native localized text"
39462
+ },
39463
+ {
39464
+ "defaultValue": "neutral",
39465
+ "description": "Applies semantic value color.",
39466
+ "name": "tone",
39467
+ "values": "brand · accent · success · warning · danger · neutral"
39468
+ }
39469
+ ],
39470
+ "example": "LumenWearMetric(label = \"Duration\", value = elapsed, tone = LumenWearTone.Brand)",
39471
+ "exportName": "LumenWearMetric",
39472
+ "importStatement": "import com.santi020k.lumen.LumenWearMetric",
39473
+ "install": "Include packages/compose from a shallow Git checkout pinned to the Lumen release tag, then add implementation(project(\":wear\")).",
39474
+ "language": "kotlin",
39475
+ "packageName": "com.santi020k:lumen-compose-wear",
39476
+ "setup": "Wrap wearable content in LumenWearTheme inside the application-owned Wear Material theme.",
39477
+ "sourceFile": "packages/compose/wear/src/main/kotlin/com/santi020k/lumen/wear/WearComponents.kt",
39478
+ "symbol": "LumenWearMetric"
39479
+ },
39480
+ "swiftui": {
39481
+ "api": [
39482
+ {
39483
+ "defaultValue": "Required",
39484
+ "description": "Names the metric.",
39485
+ "name": "label",
39486
+ "values": "Native localized text"
39487
+ },
39488
+ {
39489
+ "defaultValue": "Required",
39490
+ "description": "Provides the formatted value.",
39491
+ "name": "value",
39492
+ "values": "String"
39493
+ },
39494
+ {
39495
+ "defaultValue": "nil",
39496
+ "description": "Adds brief supporting context.",
39497
+ "name": "detail",
39498
+ "values": "Optional native localized text"
39499
+ },
39500
+ {
39501
+ "defaultValue": "neutral",
39502
+ "description": "Applies semantic value color.",
39503
+ "name": "tone",
39504
+ "values": "brand · accent · success · warning · danger · neutral"
39505
+ }
39506
+ ],
39507
+ "example": "LumenWatchMetric(\"Duration\", value: elapsed, tone: .brand)",
39508
+ "exportName": "LumenWatchMetric",
39509
+ "importStatement": "import LumenUI",
39510
+ "install": "Add https://github.com/santi020k/lumen with Swift Package Manager, pin an exact or compatible release version, and link the LumenUI product to the application target.",
39511
+ "language": "swift",
39512
+ "packageName": "LumenUI",
39513
+ "setup": "Apply .lumenTheme(...) near the SwiftUI application root and preserve native environment behavior.",
39514
+ "sourceFile": "packages/swift/Sources/LumenUI/WatchComponents.swift",
39515
+ "symbol": "LumenWatchMetric"
39516
+ }
39517
+ },
39518
+ "name": "Wearable metric",
39519
+ "summary": "Present one at-a-glance wearable label, value, and optional detail.",
39520
+ "tier": "shared"
39521
+ },
39522
+ {
39523
+ "accessibility": "Preserves child actions and content in native traversal order without collapsing distinct controls.",
39524
+ "category": "Layout",
39525
+ "contract": "Compact wearable row with flexible leading, content, and trailing slots",
39526
+ "guidance": "Use inside native wearable scrolling containers. Keep list state, selection, navigation, and rotary input in the application.",
39527
+ "id": "wearable-list-row",
39528
+ "implementations": {
39529
+ "compose": {
39530
+ "api": [
39531
+ {
39532
+ "defaultValue": "nil / EmptyView",
39533
+ "description": "Provides compact identity.",
39534
+ "name": "leading",
39535
+ "values": "Optional native view slot"
39536
+ },
39537
+ {
39538
+ "defaultValue": "Required",
39539
+ "description": "Provides primary row content.",
39540
+ "name": "content",
39541
+ "values": "Native view slot"
39542
+ },
39543
+ {
39544
+ "defaultValue": "nil / EmptyView",
39545
+ "description": "Provides short status or actions.",
39546
+ "name": "trailing",
39547
+ "values": "Optional native view slot"
39548
+ }
39549
+ ],
39550
+ "example": "LumenWearListRow { TimerHistoryLabel() }",
39551
+ "exportName": "LumenWearListRow",
39552
+ "importStatement": "import com.santi020k.lumen.LumenWearListRow",
39553
+ "install": "Include packages/compose from a shallow Git checkout pinned to the Lumen release tag, then add implementation(project(\":wear\")).",
39554
+ "language": "kotlin",
39555
+ "packageName": "com.santi020k:lumen-compose-wear",
39556
+ "setup": "Wrap wearable content in LumenWearTheme inside the application-owned Wear Material theme.",
39557
+ "sourceFile": "packages/compose/wear/src/main/kotlin/com/santi020k/lumen/wear/WearComponents.kt",
39558
+ "symbol": "LumenWearListRow"
39559
+ },
39560
+ "swiftui": {
39561
+ "api": [
39562
+ {
39563
+ "defaultValue": "nil / EmptyView",
39564
+ "description": "Provides compact identity.",
39565
+ "name": "leading",
39566
+ "values": "Optional native view slot"
39567
+ },
39568
+ {
39569
+ "defaultValue": "Required",
39570
+ "description": "Provides primary row content.",
39571
+ "name": "content",
39572
+ "values": "Native view slot"
39573
+ },
39574
+ {
39575
+ "defaultValue": "nil / EmptyView",
39576
+ "description": "Provides short status or actions.",
39577
+ "name": "trailing",
39578
+ "values": "Optional native view slot"
39579
+ }
39580
+ ],
39581
+ "example": "LumenWatchListRow { TimerHistoryLabel() }",
39582
+ "exportName": "LumenWatchListRow",
39583
+ "importStatement": "import LumenUI",
39584
+ "install": "Add https://github.com/santi020k/lumen with Swift Package Manager, pin an exact or compatible release version, and link the LumenUI product to the application target.",
39585
+ "language": "swift",
39586
+ "packageName": "LumenUI",
39587
+ "setup": "Apply .lumenTheme(...) near the SwiftUI application root and preserve native environment behavior.",
39588
+ "sourceFile": "packages/swift/Sources/LumenUI/WatchComponents.swift",
39589
+ "symbol": "LumenWatchListRow"
39590
+ }
39591
+ },
39592
+ "name": "Wearable list row",
39593
+ "summary": "Compose a compact wearable row with flexible content slots.",
39594
+ "tier": "shared"
38738
39595
  }
38739
39596
  ],
38740
39597
  "nativeSources": {
@@ -38744,12 +39601,13 @@
38744
39601
  "packages/compose/src/main/kotlin/com/santi020k/lumen/FormComponents.kt": "package com.santi020k.lumen\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.heightIn\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.selection.toggleable\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.OutlinedTextField\nimport androidx.compose.material3.OutlinedTextFieldDefaults\nimport androidx.compose.material3.Switch\nimport androidx.compose.material3.SwitchDefaults\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.semantics.Role\nimport androidx.compose.ui.semantics.clearAndSetSemantics\nimport androidx.compose.ui.semantics.contentDescription\nimport androidx.compose.ui.semantics.semantics\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\n\ndata class LumenSearchFieldState(val showClearAction: Boolean) {\n companion object {\n fun resolve(value: String, enabled: Boolean): LumenSearchFieldState = LumenSearchFieldState(\n showClearAction = enabled && value.isNotEmpty()\n )\n }\n}\n\n@Composable\nfun LumenToggle(\n label: String,\n checked: Boolean,\n onCheckedChange: (Boolean) -> Unit,\n modifier: Modifier = Modifier,\n description: String? = null,\n showLabel: Boolean = true,\n enabled: Boolean = true\n) {\n val colors = LocalLumenTheme.current.colors\n val widthModifier = if (showLabel) Modifier.fillMaxWidth() else Modifier\n\n Row(\n modifier = modifier\n .then(widthModifier)\n .heightIn(min = LumenButtonMetrics.resolve(LumenControlSize.Md).minHeight)\n .toggleable(\n value = checked,\n enabled = enabled,\n role = Role.Switch,\n onValueChange = onCheckedChange\n )\n .semantics(mergeDescendants = true) { contentDescription = label },\n horizontalArrangement = Arrangement.spacedBy(LumenSpacing.Lg),\n verticalAlignment = Alignment.CenterVertically\n ) {\n if (showLabel) {\n Column(\n modifier = Modifier.weight(1f),\n verticalArrangement = Arrangement.spacedBy(LumenSpacing.Xs)\n ) {\n Text(\n text = label,\n color = colors.ink,\n style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.SemiBold)\n )\n if (description != null) {\n Text(text = description, color = colors.inkMuted, style = MaterialTheme.typography.bodySmall)\n }\n }\n }\n Switch(\n checked = checked,\n onCheckedChange = null,\n modifier = Modifier.clearAndSetSemantics {},\n enabled = enabled,\n colors = SwitchDefaults.colors(\n checkedThumbColor = colors.onBrand,\n checkedTrackColor = colors.brandSolid,\n uncheckedThumbColor = colors.surface,\n uncheckedTrackColor = colors.surfaceStrong,\n uncheckedBorderColor = colors.line\n )\n )\n }\n}\n\n@Composable\nfun LumenSettingsRow(\n title: String,\n modifier: Modifier = Modifier,\n description: String? = null,\n graphic: (@Composable () -> Unit)? = null,\n control: @Composable () -> Unit\n) {\n val colors = LocalLumenTheme.current.colors\n\n Row(\n modifier = modifier\n .fillMaxWidth()\n .heightIn(min = 56.dp)\n .padding(vertical = LumenSpacing.Sm),\n horizontalArrangement = Arrangement.spacedBy(LumenSpacing.Lg),\n verticalAlignment = Alignment.CenterVertically\n ) {\n graphic?.invoke()\n Column(\n modifier = Modifier.weight(1f),\n verticalArrangement = Arrangement.spacedBy(LumenSpacing.Xs)\n ) {\n Text(\n text = title,\n color = colors.ink,\n style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.SemiBold)\n )\n if (description != null) {\n Text(text = description, color = colors.inkMuted, style = MaterialTheme.typography.bodySmall)\n }\n }\n control()\n }\n}\n\n@Composable\nfun LumenSearchField(\n value: String,\n onValueChange: (String) -> Unit,\n modifier: Modifier = Modifier,\n prompt: String = \"Search\",\n clearLabel: String = \"Clear search\",\n enabled: Boolean = true,\n graphic: (@Composable () -> Unit)? = null\n) {\n val colors = LocalLumenTheme.current.colors\n val searchState = LumenSearchFieldState.resolve(value, enabled)\n val trailingIcon: (@Composable () -> Unit)? = if (searchState.showClearAction) {\n {\n TextButton(onClick = { onValueChange(\"\") }) {\n Text(clearLabel, color = colors.inkSoft, style = MaterialTheme.typography.labelSmall)\n }\n }\n } else {\n null\n }\n\n OutlinedTextField(\n value = value,\n onValueChange = onValueChange,\n modifier = modifier\n .fillMaxWidth()\n .heightIn(min = LumenButtonMetrics.resolve(LumenControlSize.Md).minHeight),\n enabled = enabled,\n singleLine = true,\n label = { Text(prompt) },\n leadingIcon = graphic,\n trailingIcon = trailingIcon,\n shape = RoundedCornerShape(LumenRadius.Sm),\n colors = OutlinedTextFieldDefaults.colors(\n focusedContainerColor = colors.surface,\n unfocusedContainerColor = colors.surface,\n disabledContainerColor = colors.surface,\n focusedTextColor = colors.ink,\n unfocusedTextColor = colors.ink,\n focusedBorderColor = colors.brand,\n unfocusedBorderColor = colors.line,\n cursorColor = colors.brand\n )\n )\n}\n",
38745
39602
  "packages/compose/src/main/kotlin/com/santi020k/lumen/LumenTheme.kt": "package com.santi020k.lumen\n\nimport androidx.compose.foundation.isSystemInDarkTheme\nimport androidx.compose.material3.ColorScheme\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Shapes\nimport androidx.compose.material3.Typography\nimport androidx.compose.material3.darkColorScheme\nimport androidx.compose.material3.lightColorScheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.runtime.staticCompositionLocalOf\nimport androidx.compose.ui.graphics.Color\n\n@Immutable\ndata class LumenThemeValues(\n val colors: LumenColorPalette,\n val isDark: Boolean\n)\n\n@Immutable\ndata class LumenMaterialColorOverrides(\n val brand: Color? = null,\n val accent: Color? = null,\n val success: Color? = null,\n val warning: Color? = null\n)\n\nval LocalLumenTheme = staticCompositionLocalOf {\n LumenThemeValues(colors = LumenColors.Light, isDark = false)\n}\n\nfun ColorScheme.toLumenColorPalette(\n fallback: LumenColorPalette,\n overrides: LumenMaterialColorOverrides = LumenMaterialColorOverrides()\n): LumenColorPalette = LumenColorPalette(\n canvas = background,\n surface = surface,\n surfaceMuted = surfaceVariant,\n surfaceStrong = surfaceContainerHigh,\n line = outline,\n ink = onBackground,\n inkSoft = onSurface,\n inkMuted = onSurfaceVariant,\n brand = overrides.brand ?: primary,\n brandSolid = overrides.brand ?: primary,\n brandSoft = primaryContainer,\n onBrand = onPrimary,\n accent = overrides.accent ?: secondary,\n success = overrides.success ?: fallback.success,\n warning = overrides.warning ?: fallback.warning,\n danger = error,\n onDanger = onError\n)\n\nfun LumenColorPalette.toMaterialColorScheme(isDark: Boolean): ColorScheme {\n return if (isDark) {\n darkColorScheme(\n primary = brand,\n onPrimary = onBrand,\n primaryContainer = brandSoft,\n onPrimaryContainer = ink,\n secondary = accent,\n background = canvas,\n onBackground = ink,\n surface = surface,\n onSurface = ink,\n surfaceVariant = surfaceMuted,\n onSurfaceVariant = inkMuted,\n surfaceContainerHigh = surfaceStrong,\n error = danger,\n onError = onDanger,\n outline = line\n )\n } else {\n lightColorScheme(\n primary = brand,\n onPrimary = onBrand,\n primaryContainer = brandSoft,\n onPrimaryContainer = ink,\n secondary = accent,\n background = canvas,\n onBackground = ink,\n surface = surface,\n onSurface = ink,\n surfaceVariant = surfaceMuted,\n onSurfaceVariant = inkMuted,\n surfaceContainerHigh = surfaceStrong,\n error = danger,\n onError = onDanger,\n outline = line\n )\n }\n}\n\n@Composable\nfun LumenTheme(\n darkTheme: Boolean = isSystemInDarkTheme(),\n values: LumenThemeValues? = null,\n materialColorScheme: ColorScheme? = null,\n materialColorOverrides: LumenMaterialColorOverrides = LumenMaterialColorOverrides(),\n typography: Typography = Typography(),\n shapes: Shapes = Shapes(),\n content: @Composable () -> Unit\n) {\n val defaultValues = if (darkTheme) {\n LumenThemeValues(colors = LumenColors.Dark, isDark = true)\n } else {\n LumenThemeValues(colors = LumenColors.Light, isDark = false)\n }\n val resolvedValues = values ?: materialColorScheme?.let { colorScheme ->\n LumenThemeValues(\n colors = colorScheme.toLumenColorPalette(defaultValues.colors, materialColorOverrides),\n isDark = darkTheme\n )\n } ?: defaultValues\n val resolvedColorScheme = materialColorScheme\n ?: resolvedValues.colors.toMaterialColorScheme(resolvedValues.isDark)\n\n CompositionLocalProvider(LocalLumenTheme provides resolvedValues) {\n MaterialTheme(\n colorScheme = resolvedColorScheme,\n typography = typography,\n shapes = shapes,\n content = content\n )\n }\n}\n",
38746
39603
  "packages/compose/src/main/kotlin/com/santi020k/lumen/OverlayComponents.kt": "package com.santi020k.lumen\n\nimport android.app.Activity\nimport android.content.ActivityNotFoundException\nimport android.content.Intent\nimport android.net.Uri\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.AlertDialog\nimport androidx.compose.material3.DropdownMenu\nimport androidx.compose.material3.DropdownMenuItem\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.ModalBottomSheet\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.text.font.FontWeight\n\n/** A controlled native confirmation dialog with explicit cancel and confirm actions. */\n@Composable\nfun LumenAlertDialog(\n visible: Boolean,\n title: String,\n confirmLabel: String,\n onConfirm: () -> Unit,\n onDismiss: () -> Unit,\n modifier: Modifier = Modifier,\n description: String? = null,\n cancelLabel: String = \"Cancel\",\n destructive: Boolean = false,\n confirmEnabled: Boolean = true,\n confirmLoading: Boolean = false\n) {\n if (!visible) return\n\n AlertDialog(\n onDismissRequest = onDismiss,\n modifier = modifier,\n title = { Text(title, fontWeight = FontWeight.SemiBold) },\n text = description?.let { value -> { Text(value) } },\n confirmButton = {\n LumenButton(\n onClick = onConfirm,\n intent = if (destructive) LumenButtonIntent.Danger else LumenButtonIntent.Primary,\n loading = confirmLoading,\n enabled = confirmEnabled\n ) {\n Text(confirmLabel)\n }\n },\n dismissButton = {\n LumenButton(onClick = onDismiss, intent = LumenButtonIntent.Quiet) {\n Text(cancelLabel)\n }\n }\n )\n}\n\n/** A controlled Material bottom sheet with a shared heading, description, and action layout. */\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun LumenSheet(\n visible: Boolean,\n onDismiss: () -> Unit,\n modifier: Modifier = Modifier,\n title: String? = null,\n description: String? = null,\n actions: @Composable () -> Unit = {},\n content: @Composable () -> Unit\n) {\n if (!visible) return\n\n ModalBottomSheet(onDismissRequest = onDismiss, modifier = modifier) {\n Column(\n modifier = Modifier\n .fillMaxWidth()\n .padding(horizontal = LumenSpacing.Xl)\n .padding(bottom = LumenSpacing.Xl),\n verticalArrangement = Arrangement.spacedBy(LumenSpacing.Lg)\n ) {\n if (title != null || description != null) {\n Column(verticalArrangement = Arrangement.spacedBy(LumenSpacing.Sm)) {\n title?.let { Text(it, fontWeight = FontWeight.SemiBold) }\n description?.let { Text(it, color = LocalLumenTheme.current.colors.inkSoft) }\n }\n }\n\n content()\n\n Row(\n modifier = Modifier.fillMaxWidth(),\n horizontalArrangement = Arrangement.End\n ) {\n actions()\n }\n }\n }\n}\n\nclass LumenMenuItem(\n val label: String,\n val enabled: Boolean = true,\n val destructive: Boolean = false,\n val leadingIcon: ImageVector? = null,\n val onClick: () -> Unit\n)\n\n/** A controlled native anchored menu with labeled disabled and destructive action states. */\n@Composable\nfun LumenMenu(\n expanded: Boolean,\n onDismissRequest: () -> Unit,\n items: List<LumenMenuItem>,\n modifier: Modifier = Modifier\n) {\n val colors = LocalLumenTheme.current.colors\n\n DropdownMenu(\n expanded = expanded,\n onDismissRequest = onDismissRequest,\n modifier = modifier\n ) {\n items.forEachIndexed { index, item ->\n DropdownMenuItem(\n text = {\n Text(\n item.label,\n color = if (item.destructive) colors.danger else colors.ink\n )\n },\n onClick = {\n onDismissRequest()\n item.onClick()\n },\n enabled = item.enabled,\n leadingIcon = item.leadingIcon?.let { icon ->\n {\n LumenIcon(\n imageVector = icon,\n tint = if (item.destructive) colors.danger else colors.inkSoft\n )\n }\n }\n )\n\n if (index < items.lastIndex) {\n HorizontalDivider(color = colors.line)\n }\n }\n }\n}\n\n@Immutable\ndata class LumenSharePayload(\n val text: String? = null,\n val uri: Uri? = null,\n val subject: String? = null,\n val mimeType: String = \"text/plain\"\n) {\n val hasContent: Boolean\n get() = !text.isNullOrBlank() || uri != null\n}\n\n/** A Lumen button that opens Android's native share sheet for text or a content URI. */\n@Composable\nfun LumenShareButton(\n payload: LumenSharePayload,\n chooserTitle: String,\n modifier: Modifier = Modifier,\n label: String = \"Share\",\n enabled: Boolean = true,\n onFailure: (ActivityNotFoundException) -> Unit = {}\n) {\n val context = LocalContext.current\n\n LumenButton(\n onClick = {\n val sendIntent = Intent(Intent.ACTION_SEND).apply {\n type = payload.mimeType\n payload.subject?.let { putExtra(Intent.EXTRA_SUBJECT, it) }\n payload.text?.let { putExtra(Intent.EXTRA_TEXT, it) }\n payload.uri?.let {\n putExtra(Intent.EXTRA_STREAM, it)\n addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)\n }\n }\n val chooser = Intent.createChooser(sendIntent, chooserTitle)\n\n if (context !is Activity) chooser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n\n try {\n context.startActivity(chooser)\n } catch (error: ActivityNotFoundException) {\n onFailure(error)\n }\n },\n modifier = modifier,\n intent = LumenButtonIntent.Secondary,\n enabled = enabled && payload.hasContent\n ) {\n Text(label)\n }\n}\n",
38747
- "packages/compose/src/main/kotlin/com/santi020k/lumen/PlatformComponents.kt": "package com.santi020k.lumen\n\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.FloatingActionButton\nimport androidx.compose.material3.NavigationBar as MaterialNavigationBar\nimport androidx.compose.material3.NavigationBarItem\nimport androidx.compose.material3.NavigationBarItemDefaults\nimport androidx.compose.material3.SmallFloatingActionButton\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.semantics.contentDescription\nimport androidx.compose.ui.semantics.semantics\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\n\n@Immutable\ndata class LumenNavigationItem<Value>(\n val value: Value,\n val label: String,\n val icon: ImageVector,\n val selectedIcon: ImageVector = icon,\n val enabled: Boolean = true\n)\n\ninternal fun <Value> isLumenNavigationItemSelected(itemValue: Value, selectedValue: Value): Boolean =\n itemValue == selectedValue\n\n/**\n * A Material-native controlled destination bar. Navigation stacks and screen content remain app-owned.\n */\n@Composable\nfun <Value> LumenNavigationBar(\n items: List<LumenNavigationItem<Value>>,\n selectedValue: Value,\n onValueChange: (Value) -> Unit,\n modifier: Modifier = Modifier,\n accessibilityLabel: String = \"Primary navigation\"\n) {\n val colors = LocalLumenTheme.current.colors\n\n MaterialNavigationBar(\n modifier = modifier.semantics { contentDescription = accessibilityLabel },\n containerColor = colors.surface,\n contentColor = colors.inkMuted\n ) {\n items.forEach { item ->\n val selected = isLumenNavigationItemSelected(item.value, selectedValue)\n\n NavigationBarItem(\n selected = selected,\n onClick = { onValueChange(item.value) },\n icon = {\n LumenIcon(\n imageVector = if (selected) item.selectedIcon else item.icon,\n contentDescription = null,\n size = LumenIconSize.Md,\n tint = (if (selected) colors.brand else colors.inkMuted).copy(\n alpha = if (item.enabled) 1f else 0.52f\n )\n )\n },\n enabled = item.enabled,\n label = { Text(item.label, maxLines = 1) },\n colors = NavigationBarItemDefaults.colors(\n selectedIconColor = colors.brand,\n selectedTextColor = colors.brand,\n indicatorColor = colors.brandSoft,\n unselectedIconColor = colors.inkMuted,\n unselectedTextColor = colors.inkMuted,\n disabledIconColor = colors.inkMuted.copy(alpha = 0.52f),\n disabledTextColor = colors.inkMuted.copy(alpha = 0.52f)\n )\n )\n }\n }\n}\n\nenum class LumenFloatingActionButtonIntent {\n Accent,\n Brand,\n Danger\n}\n\nenum class LumenFloatingActionButtonSize {\n Regular,\n Small\n}\n\n@Immutable\ndata class LumenFloatingActionButtonMetrics(\n val dimension: Dp,\n val iconSize: LumenIconSize\n) {\n companion object {\n fun resolve(size: LumenFloatingActionButtonSize): LumenFloatingActionButtonMetrics = when (size) {\n LumenFloatingActionButtonSize.Regular -> LumenFloatingActionButtonMetrics(56.dp, LumenIconSize.Lg)\n LumenFloatingActionButtonSize.Small -> LumenFloatingActionButtonMetrics(40.dp, LumenIconSize.Md)\n }\n }\n}\n\n@Immutable\ninternal data class LumenFloatingActionButtonPalette(\n val background: Color,\n val foreground: Color\n)\n\ninternal fun lumenFloatingActionButtonPalette(\n colors: LumenColorPalette,\n intent: LumenFloatingActionButtonIntent\n): LumenFloatingActionButtonPalette = when (intent) {\n LumenFloatingActionButtonIntent.Accent -> LumenFloatingActionButtonPalette(\n background = colors.accent,\n foreground = colors.canvas\n )\n LumenFloatingActionButtonIntent.Brand -> LumenFloatingActionButtonPalette(\n background = colors.brandSolid,\n foreground = colors.onBrand\n )\n LumenFloatingActionButtonIntent.Danger -> LumenFloatingActionButtonPalette(\n background = colors.danger,\n foreground = colors.onDanger\n )\n}\n\n/**\n * A Material-native floating action button using Lumen semantic colors and icon sizing.\n * The content description is required because floating actions are icon-only.\n */\n@Composable\nfun LumenFloatingActionButton(\n imageVector: ImageVector,\n contentDescription: String,\n onClick: () -> Unit,\n modifier: Modifier = Modifier,\n intent: LumenFloatingActionButtonIntent = LumenFloatingActionButtonIntent.Brand,\n size: LumenFloatingActionButtonSize = LumenFloatingActionButtonSize.Regular\n) {\n val palette = lumenFloatingActionButtonPalette(LocalLumenTheme.current.colors, intent)\n val metrics = LumenFloatingActionButtonMetrics.resolve(size)\n val content: @Composable () -> Unit = {\n LumenIcon(\n imageVector = imageVector,\n contentDescription = contentDescription,\n size = metrics.iconSize,\n tint = palette.foreground\n )\n }\n\n when (size) {\n LumenFloatingActionButtonSize.Regular -> FloatingActionButton(\n onClick = onClick,\n modifier = modifier,\n shape = RoundedCornerShape(LumenRadius.Lg),\n containerColor = palette.background,\n contentColor = palette.foreground,\n content = content\n )\n LumenFloatingActionButtonSize.Small -> SmallFloatingActionButton(\n onClick = onClick,\n modifier = modifier,\n shape = RoundedCornerShape(LumenRadius.Md),\n containerColor = palette.background,\n contentColor = palette.foreground,\n content = content\n )\n }\n}\n",
39604
+ "packages/compose/src/main/kotlin/com/santi020k/lumen/PlatformComponents.kt": "package com.santi020k.lumen\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.scaleIn\nimport androidx.compose.animation.scaleOut\nimport androidx.compose.animation.slideInVertically\nimport androidx.compose.animation.slideOutVertically\nimport androidx.compose.animation.core.animateDpAsState\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.defaultMinSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.offset\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.Badge\nimport androidx.compose.material3.BadgedBox\nimport androidx.compose.material3.FloatingActionButton\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.NavigationBar as MaterialNavigationBar\nimport androidx.compose.material3.NavigationBarItem\nimport androidx.compose.material3.NavigationBarItemDefaults\nimport androidx.compose.material3.SmallFloatingActionButton\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.runtime.Stable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.input.nestedscroll.NestedScrollConnection\nimport androidx.compose.ui.input.nestedscroll.NestedScrollSource\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.unit.IntOffset\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.semantics.contentDescription\nimport androidx.compose.ui.semantics.semantics\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\n\n@Immutable\ndata class LumenNavigationItem<Value>(\n val value: Value,\n val label: String,\n val icon: ImageVector,\n val selectedIcon: ImageVector = icon,\n val badge: LumenNavigationBadge? = null,\n val enabled: Boolean = true\n)\n\n@Immutable\ndata class LumenNavigationBadge(\n val text: String? = null,\n val accessibilityLabel: String = text ?: \"New activity\"\n) {\n companion object {\n fun dot(accessibilityLabel: String = \"New activity\"): LumenNavigationBadge =\n LumenNavigationBadge(accessibilityLabel = accessibilityLabel)\n\n fun count(value: Int): LumenNavigationBadge {\n val safeValue = value.coerceAtLeast(0)\n return LumenNavigationBadge(\n text = if (safeValue > 99) \"99+\" else safeValue.toString(),\n accessibilityLabel = \"$safeValue new items\"\n )\n }\n }\n}\n\ninternal fun <Value> isLumenNavigationItemSelected(itemValue: Value, selectedValue: Value): Boolean =\n itemValue == selectedValue\n\ninternal fun <Value> dispatchLumenNavigationSelection(\n itemValue: Value,\n selectedValue: Value,\n onValueChange: (Value) -> Unit,\n onReselect: ((Value) -> Unit)?\n) {\n if (itemValue == selectedValue && onReselect != null) onReselect(itemValue)\n else onValueChange(itemValue)\n}\n\n/**\n * Direction-aware visibility state for a bottom navigation bar.\n *\n * Attach [lumenNavigationBarScrollBehavior] to the parent of a vertically scrolling child, then\n * pass this state to [LumenNavigationBar]. Small direction changes are accumulated until the\n * threshold is reached so incidental finger movement does not make navigation flicker.\n */\n@Stable\nclass LumenNavigationBarScrollState internal constructor(\n initiallyVisible: Boolean,\n private val thresholdPx: Float\n) {\n var isVisible: Boolean by mutableStateOf(initiallyVisible)\n private set\n\n private var accumulatedDeltaY = 0f\n\n internal fun recordScrollDelta(deltaY: Float) {\n if (!deltaY.isFinite() || deltaY == 0f) return\n\n val changedDirection =\n (accumulatedDeltaY < 0f && deltaY > 0f) || (accumulatedDeltaY > 0f && deltaY < 0f)\n if (changedDirection) accumulatedDeltaY = 0f\n\n accumulatedDeltaY += deltaY\n when {\n accumulatedDeltaY <= -thresholdPx -> hide()\n accumulatedDeltaY >= thresholdPx -> show()\n }\n }\n\n internal val nestedScrollConnection = object : NestedScrollConnection {\n override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {\n recordScrollDelta(available.y)\n return Offset.Zero\n }\n }\n\n fun show() {\n isVisible = true\n accumulatedDeltaY = 0f\n }\n\n fun hide() {\n isVisible = false\n accumulatedDeltaY = 0f\n }\n}\n\n/** Remembers scroll-responsive navigation state using a density-aware movement threshold. */\n@Composable\nfun rememberLumenNavigationBarScrollState(\n initiallyVisible: Boolean = true,\n threshold: Dp = 16.dp\n): LumenNavigationBarScrollState {\n val thresholdPx = with(LocalDensity.current) {\n if (threshold.value.isFinite() && threshold > 0.dp) threshold.toPx() else 16.dp.toPx()\n }\n\n return remember(initiallyVisible, thresholdPx) {\n LumenNavigationBarScrollState(initiallyVisible, thresholdPx)\n }\n}\n\n/** Connects scroll deltas from a descendant scroll container to a Lumen navigation bar state. */\nfun Modifier.lumenNavigationBarScrollBehavior(\n state: LumenNavigationBarScrollState\n): Modifier = nestedScroll(state.nestedScrollConnection)\n\n/**\n * A Material-native controlled destination bar. Navigation stacks and screen content remain app-owned.\n */\n@Composable\nfun <Value> LumenNavigationBar(\n items: List<LumenNavigationItem<Value>>,\n selectedValue: Value,\n onValueChange: (Value) -> Unit,\n modifier: Modifier = Modifier,\n onReselect: ((Value) -> Unit)? = null,\n accessibilityLabel: String = \"Primary navigation\",\n scrollState: LumenNavigationBarScrollState? = null\n) {\n val colors = LocalLumenTheme.current.colors\n\n AnimatedVisibility(\n visible = scrollState?.isVisible ?: true,\n enter = slideInVertically(initialOffsetY = { it }) + fadeIn(),\n exit = slideOutVertically(targetOffsetY = { it }) + fadeOut()\n ) {\n MaterialNavigationBar(\n modifier = modifier.semantics { contentDescription = accessibilityLabel },\n containerColor = colors.surface,\n contentColor = colors.inkMuted\n ) {\n items.forEach { item ->\n val selected = isLumenNavigationItemSelected(item.value, selectedValue)\n\n NavigationBarItem(\n selected = selected,\n onClick = {\n dispatchLumenNavigationSelection(\n item.value,\n selectedValue,\n onValueChange,\n onReselect\n )\n },\n icon = {\n BadgedBox(\n badge = {\n item.badge?.let { badge ->\n LumenNavigationBadgeView(badge)\n }\n }\n ) {\n LumenIcon(\n imageVector = if (selected) item.selectedIcon else item.icon,\n contentDescription = null,\n size = LumenIconSize.Md,\n tint = (if (selected) colors.brand else colors.inkMuted).copy(\n alpha = if (item.enabled) 1f else 0.52f\n )\n )\n }\n },\n enabled = item.enabled,\n label = { Text(item.label, maxLines = 1) },\n colors = NavigationBarItemDefaults.colors(\n selectedIconColor = colors.brand,\n selectedTextColor = colors.brand,\n indicatorColor = colors.brandSoft,\n unselectedIconColor = colors.inkMuted,\n unselectedTextColor = colors.inkMuted,\n disabledIconColor = colors.inkMuted.copy(alpha = 0.52f),\n disabledTextColor = colors.inkMuted.copy(alpha = 0.52f)\n )\n )\n }\n }\n }\n}\n\n@Composable\nprivate fun LumenNavigationBadgeView(badge: LumenNavigationBadge) {\n val colors = LocalLumenTheme.current.colors\n\n Badge(\n modifier = Modifier.semantics { contentDescription = badge.accessibilityLabel },\n containerColor = colors.danger,\n contentColor = colors.onDanger\n ) {\n badge.text?.let { Text(it, maxLines = 1) }\n }\n}\n\n/**\n * A compact token-aware status or action surface intended directly above bottom navigation.\n */\n@Composable\nfun LumenNavigationBarAccessory(\n modifier: Modifier = Modifier,\n scrollState: LumenNavigationBarScrollState? = null,\n content: @Composable () -> Unit\n) {\n val colors = LocalLumenTheme.current.colors\n\n AnimatedVisibility(\n visible = scrollState?.isVisible ?: true,\n enter = slideInVertically(initialOffsetY = { it }) + fadeIn(),\n exit = slideOutVertically(targetOffsetY = { it }) + fadeOut()\n ) {\n Surface(\n modifier = modifier.fillMaxWidth(),\n color = colors.surface,\n contentColor = colors.ink\n ) {\n Box(\n modifier = Modifier\n .defaultMinSize(minHeight = 48.dp)\n .padding(horizontal = LumenSpacing.Lg, vertical = LumenSpacing.Sm)\n ) {\n content()\n }\n HorizontalDivider(color = colors.line)\n }\n }\n}\n\n/**\n * Material adaptive navigation that switches between a bottom bar and navigation rail as the\n * application window and device posture change. Destination content and routing remain app-owned.\n */\n@Composable\nfun <Value> LumenAdaptiveNavigationScaffold(\n items: List<LumenNavigationItem<Value>>,\n selectedValue: Value,\n onValueChange: (Value) -> Unit,\n modifier: Modifier = Modifier,\n onReselect: ((Value) -> Unit)? = null,\n content: @Composable () -> Unit\n) {\n val colors = LocalLumenTheme.current.colors\n\n NavigationSuiteScaffold(\n navigationSuiteItems = {\n items.forEach { navigationItem ->\n val selected = navigationItem.value == selectedValue\n\n item(\n selected = selected,\n onClick = {\n dispatchLumenNavigationSelection(\n navigationItem.value,\n selectedValue,\n onValueChange,\n onReselect\n )\n },\n icon = {\n LumenIcon(\n imageVector = if (selected) navigationItem.selectedIcon else navigationItem.icon,\n contentDescription = null,\n size = LumenIconSize.Md,\n tint = if (selected) colors.brand else colors.inkMuted\n )\n },\n enabled = navigationItem.enabled,\n label = { Text(navigationItem.label, maxLines = 1) },\n badge = navigationItem.badge?.let { badge ->\n { LumenNavigationBadgeView(badge) }\n }\n )\n }\n },\n modifier = modifier,\n containerColor = colors.canvas,\n contentColor = colors.ink,\n content = content\n )\n}\n\nenum class LumenFloatingActionButtonIntent {\n Accent,\n Brand,\n Danger\n}\n\nenum class LumenFloatingActionButtonSize {\n Regular,\n Small\n}\n\nenum class LumenFloatingActionButtonNavigationBehavior {\n AlwaysVisible,\n HideWithNavigation,\n FollowNavigation\n}\n\ninternal fun lumenFloatingActionButtonVisible(\n behavior: LumenFloatingActionButtonNavigationBehavior,\n navigationVisible: Boolean\n): Boolean = behavior != LumenFloatingActionButtonNavigationBehavior.HideWithNavigation || navigationVisible\n\ninternal fun lumenFloatingActionButtonOffset(\n behavior: LumenFloatingActionButtonNavigationBehavior,\n navigationVisible: Boolean\n): Dp = if (\n behavior == LumenFloatingActionButtonNavigationBehavior.FollowNavigation && !navigationVisible\n) {\n 16.dp\n} else {\n 0.dp\n}\n\n@Immutable\ndata class LumenFloatingActionButtonMetrics(\n val dimension: Dp,\n val iconSize: LumenIconSize\n) {\n companion object {\n fun resolve(size: LumenFloatingActionButtonSize): LumenFloatingActionButtonMetrics = when (size) {\n LumenFloatingActionButtonSize.Regular -> LumenFloatingActionButtonMetrics(56.dp, LumenIconSize.Lg)\n LumenFloatingActionButtonSize.Small -> LumenFloatingActionButtonMetrics(40.dp, LumenIconSize.Md)\n }\n }\n}\n\n@Immutable\ninternal data class LumenFloatingActionButtonPalette(\n val background: Color,\n val foreground: Color\n)\n\ninternal fun lumenFloatingActionButtonPalette(\n colors: LumenColorPalette,\n intent: LumenFloatingActionButtonIntent\n): LumenFloatingActionButtonPalette = when (intent) {\n LumenFloatingActionButtonIntent.Accent -> LumenFloatingActionButtonPalette(\n background = colors.accent,\n foreground = colors.canvas\n )\n LumenFloatingActionButtonIntent.Brand -> LumenFloatingActionButtonPalette(\n background = colors.brandSolid,\n foreground = colors.onBrand\n )\n LumenFloatingActionButtonIntent.Danger -> LumenFloatingActionButtonPalette(\n background = colors.danger,\n foreground = colors.onDanger\n )\n}\n\n/**\n * A Material-native floating action button using Lumen semantic colors and icon sizing.\n * The content description is required because floating actions are icon-only.\n */\n@Composable\nfun LumenFloatingActionButton(\n imageVector: ImageVector,\n contentDescription: String,\n onClick: () -> Unit,\n modifier: Modifier = Modifier,\n intent: LumenFloatingActionButtonIntent = LumenFloatingActionButtonIntent.Brand,\n size: LumenFloatingActionButtonSize = LumenFloatingActionButtonSize.Regular,\n scrollState: LumenNavigationBarScrollState? = null,\n navigationBehavior: LumenFloatingActionButtonNavigationBehavior =\n LumenFloatingActionButtonNavigationBehavior.AlwaysVisible\n) {\n val palette = lumenFloatingActionButtonPalette(LocalLumenTheme.current.colors, intent)\n val metrics = LumenFloatingActionButtonMetrics.resolve(size)\n val content: @Composable () -> Unit = {\n LumenIcon(\n imageVector = imageVector,\n contentDescription = contentDescription,\n size = metrics.iconSize,\n tint = palette.foreground\n )\n }\n\n val navigationVisible = scrollState?.isVisible ?: true\n val visible = lumenFloatingActionButtonVisible(navigationBehavior, navigationVisible)\n val offset by animateDpAsState(\n targetValue = lumenFloatingActionButtonOffset(navigationBehavior, navigationVisible),\n label = \"Lumen floating action navigation offset\"\n )\n\n AnimatedVisibility(\n visible = visible,\n enter = scaleIn() + fadeIn(),\n exit = scaleOut() + fadeOut()\n ) {\n when (size) {\n LumenFloatingActionButtonSize.Regular -> FloatingActionButton(\n onClick = onClick,\n modifier = modifier.offset { IntOffset(0, offset.roundToPx()) },\n shape = RoundedCornerShape(LumenRadius.Lg),\n containerColor = palette.background,\n contentColor = palette.foreground,\n content = content\n )\n LumenFloatingActionButtonSize.Small -> SmallFloatingActionButton(\n onClick = onClick,\n modifier = modifier.offset { IntOffset(0, offset.roundToPx()) },\n shape = RoundedCornerShape(LumenRadius.Md),\n containerColor = palette.background,\n contentColor = palette.foreground,\n content = content\n )\n }\n }\n}\n",
38748
39605
  "packages/compose/src/main/kotlin/com/santi020k/lumen/Primitives.kt": "package com.santi020k.lumen\n\nimport androidx.compose.foundation.BorderStroke\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.RowScope\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.defaultMinSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.ButtonDefaults\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.IconButtonDefaults\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.OutlinedTextField\nimport androidx.compose.material3.OutlinedTextFieldDefaults\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.semantics.contentDescription\nimport androidx.compose.ui.semantics.semantics\nimport androidx.compose.ui.semantics.stateDescription\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.input.VisualTransformation\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\n\nenum class LumenTextTone {\n Danger,\n Default,\n Muted,\n Soft,\n Success,\n Warning\n}\n\nenum class LumenTextVariant {\n Body,\n Caption,\n Label,\n Title\n}\n\n@Composable\nfun LumenText(\n text: String,\n modifier: Modifier = Modifier,\n variant: LumenTextVariant = LumenTextVariant.Body,\n tone: LumenTextTone = LumenTextTone.Default\n) {\n val theme = LocalLumenTheme.current\n val color = when (tone) {\n LumenTextTone.Danger -> theme.colors.danger\n LumenTextTone.Default -> theme.colors.ink\n LumenTextTone.Muted -> theme.colors.inkMuted\n LumenTextTone.Soft -> theme.colors.inkSoft\n LumenTextTone.Success -> theme.colors.success\n LumenTextTone.Warning -> theme.colors.warning\n }\n val style = when (variant) {\n LumenTextVariant.Body -> MaterialTheme.typography.bodyLarge\n LumenTextVariant.Caption -> MaterialTheme.typography.bodySmall\n LumenTextVariant.Label -> MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.SemiBold)\n LumenTextVariant.Title -> MaterialTheme.typography.headlineSmall.copy(fontWeight = FontWeight.Bold)\n }\n\n Text(text = text, modifier = modifier, color = color, style = style)\n}\n\nenum class LumenSurfacePadding(val value: Dp) {\n None(0.dp),\n Sm(LumenSpacing.Sm),\n Md(LumenSpacing.Md),\n Lg(LumenSpacing.Lg)\n}\n\nenum class LumenSurfaceRadius(val value: Dp) {\n None(0.dp),\n Sm(LumenRadius.Sm),\n Md(LumenRadius.Md),\n Lg(LumenRadius.Lg)\n}\n\nenum class LumenSurfaceTone {\n Canvas,\n Muted,\n Strong,\n Surface\n}\n\n@Composable\nfun LumenSurface(\n modifier: Modifier = Modifier,\n tone: LumenSurfaceTone = LumenSurfaceTone.Surface,\n padding: LumenSurfacePadding = LumenSurfacePadding.Md,\n radius: LumenSurfaceRadius = LumenSurfaceRadius.Md,\n content: @Composable () -> Unit\n) {\n val colors = LocalLumenTheme.current.colors\n val color = when (tone) {\n LumenSurfaceTone.Canvas -> colors.canvas\n LumenSurfaceTone.Muted -> colors.surfaceMuted\n LumenSurfaceTone.Strong -> colors.surfaceStrong\n LumenSurfaceTone.Surface -> colors.surface\n }\n\n Surface(\n modifier = modifier,\n color = color,\n shape = RoundedCornerShape(radius.value)\n ) {\n androidx.compose.foundation.layout.Box(\n modifier = Modifier.padding(padding.value),\n contentAlignment = Alignment.TopStart\n ) {\n content()\n }\n }\n}\n\nenum class LumenButtonIntent {\n Danger,\n Primary,\n Quiet,\n Secondary\n}\n\nenum class LumenControlSize {\n Lg,\n Md,\n Sm\n}\n\nenum class LumenIconSize(val dimension: Dp) {\n Sm(16.dp),\n Md(20.dp),\n Lg(24.dp)\n}\n\n@Composable\nfun LumenIcon(\n imageVector: ImageVector,\n modifier: Modifier = Modifier,\n contentDescription: String? = null,\n size: LumenIconSize = LumenIconSize.Md,\n tint: Color = LocalLumenTheme.current.colors.ink\n) {\n Icon(\n imageVector = imageVector,\n contentDescription = contentDescription,\n modifier = modifier.size(size.dimension),\n tint = tint\n )\n}\n\n@Immutable\ndata class LumenIconButtonMetrics(\n val iconSize: LumenIconSize,\n val touchTarget: Dp\n) {\n companion object {\n fun resolve(size: LumenControlSize): LumenIconButtonMetrics = when (size) {\n LumenControlSize.Lg -> LumenIconButtonMetrics(LumenIconSize.Lg, 52.dp)\n LumenControlSize.Md -> LumenIconButtonMetrics(LumenIconSize.Md, 44.dp)\n LumenControlSize.Sm -> LumenIconButtonMetrics(LumenIconSize.Sm, 44.dp)\n }\n }\n}\n\n@Composable\nfun LumenIconButton(\n imageVector: ImageVector,\n contentDescription: String,\n onClick: () -> Unit,\n modifier: Modifier = Modifier,\n intent: LumenButtonIntent = LumenButtonIntent.Quiet,\n size: LumenControlSize = LumenControlSize.Md,\n enabled: Boolean = true\n) {\n val palette = buttonPalette(LocalLumenTheme.current.colors, intent)\n val metrics = LumenIconButtonMetrics.resolve(size)\n val shape = RoundedCornerShape(LumenRadius.Sm)\n\n IconButton(\n onClick = onClick,\n modifier = modifier\n .size(metrics.touchTarget)\n .clip(shape)\n .border(1.dp, palette.border, shape),\n enabled = enabled,\n colors = IconButtonDefaults.iconButtonColors(\n containerColor = palette.background,\n contentColor = palette.foreground,\n disabledContainerColor = palette.background.copy(alpha = 0.52f),\n disabledContentColor = palette.foreground.copy(alpha = 0.52f)\n )\n ) {\n LumenIcon(\n imageVector = imageVector,\n contentDescription = contentDescription,\n size = metrics.iconSize,\n tint = palette.foreground.copy(alpha = if (enabled) 1f else 0.52f)\n )\n }\n}\n\n@Immutable\ndata class LumenButtonMetrics(\n val horizontalPadding: Dp,\n val minHeight: Dp\n) {\n companion object {\n fun resolve(size: LumenControlSize): LumenButtonMetrics = when (size) {\n LumenControlSize.Lg -> LumenButtonMetrics(LumenSpacing.Xl, 52.dp)\n LumenControlSize.Md -> LumenButtonMetrics(LumenSpacing.Lg, 44.dp)\n LumenControlSize.Sm -> LumenButtonMetrics(LumenSpacing.Md, 36.dp)\n }\n }\n}\n\nprivate data class LumenButtonPalette(\n val background: Color,\n val border: Color,\n val foreground: Color\n)\n\nprivate fun buttonPalette(\n colors: LumenColorPalette,\n intent: LumenButtonIntent\n): LumenButtonPalette = when (intent) {\n LumenButtonIntent.Danger -> LumenButtonPalette(colors.danger, colors.danger, colors.onDanger)\n LumenButtonIntent.Primary -> LumenButtonPalette(colors.brandSolid, colors.brandSolid, colors.onBrand)\n LumenButtonIntent.Quiet -> LumenButtonPalette(Color.Transparent, Color.Transparent, colors.inkSoft)\n LumenButtonIntent.Secondary -> LumenButtonPalette(colors.surfaceMuted, colors.line, colors.ink)\n}\n\n@Composable\nfun LumenButton(\n onClick: () -> Unit,\n modifier: Modifier = Modifier,\n intent: LumenButtonIntent = LumenButtonIntent.Primary,\n size: LumenControlSize = LumenControlSize.Md,\n loading: Boolean = false,\n enabled: Boolean = true,\n content: @Composable RowScope.() -> Unit\n) {\n val palette = buttonPalette(LocalLumenTheme.current.colors, intent)\n val metrics = LumenButtonMetrics.resolve(size)\n val semanticsModifier = if (loading) {\n Modifier.semantics { stateDescription = \"Loading\" }\n } else {\n Modifier\n }\n\n Button(\n onClick = onClick,\n modifier = modifier\n .then(semanticsModifier)\n .defaultMinSize(minHeight = metrics.minHeight),\n enabled = enabled && !loading,\n shape = RoundedCornerShape(LumenRadius.Sm),\n border = BorderStroke(1.dp, palette.border),\n colors = ButtonDefaults.buttonColors(\n containerColor = palette.background,\n contentColor = palette.foreground,\n disabledContainerColor = palette.background.copy(alpha = 0.52f),\n disabledContentColor = palette.foreground.copy(alpha = 0.52f)\n ),\n contentPadding = PaddingValues(\n horizontal = metrics.horizontalPadding,\n vertical = LumenSpacing.Sm\n )\n ) {\n Row(\n horizontalArrangement = Arrangement.Center,\n verticalAlignment = Alignment.CenterVertically\n ) {\n if (loading) {\n CircularProgressIndicator(\n modifier = Modifier.size(16.dp),\n color = palette.foreground,\n strokeWidth = 2.dp\n )\n Spacer(modifier = Modifier.width(LumenSpacing.Sm))\n }\n content()\n }\n }\n}\n\n@Composable\nfun LumenTextField(\n value: String,\n onValueChange: (String) -> Unit,\n label: String,\n modifier: Modifier = Modifier,\n size: LumenControlSize = LumenControlSize.Md,\n error: Boolean = false,\n errorMessage: String? = null,\n enabled: Boolean = true,\n visualTransformation: VisualTransformation = VisualTransformation.None\n) {\n val colors = LocalLumenTheme.current.colors\n val metrics = LumenButtonMetrics.resolve(size)\n\n OutlinedTextField(\n value = value,\n onValueChange = onValueChange,\n modifier = modifier.defaultMinSize(minHeight = metrics.minHeight),\n enabled = enabled,\n isError = error,\n singleLine = true,\n label = { Text(label) },\n supportingText = errorMessage?.let { message -> ({ Text(message) }) },\n visualTransformation = visualTransformation,\n shape = RoundedCornerShape(LumenRadius.Sm),\n colors = OutlinedTextFieldDefaults.colors(\n focusedContainerColor = colors.surface,\n unfocusedContainerColor = colors.surface,\n disabledContainerColor = colors.surface,\n focusedTextColor = colors.ink,\n unfocusedTextColor = colors.ink,\n focusedBorderColor = colors.brand,\n unfocusedBorderColor = colors.line,\n errorBorderColor = colors.danger,\n cursorColor = colors.brand\n )\n )\n}\n\nenum class LumenBadgeTone {\n Accent,\n Danger,\n Neutral,\n Success,\n Warning\n}\n\n@Composable\nfun LumenBadge(\n text: String,\n modifier: Modifier = Modifier,\n tone: LumenBadgeTone = LumenBadgeTone.Neutral\n) {\n val colors = LocalLumenTheme.current.colors\n val foreground = when (tone) {\n LumenBadgeTone.Accent -> colors.accent\n LumenBadgeTone.Danger -> colors.danger\n LumenBadgeTone.Neutral -> colors.inkSoft\n LumenBadgeTone.Success -> colors.success\n LumenBadgeTone.Warning -> colors.warning\n }\n val background = if (tone == LumenBadgeTone.Neutral) {\n colors.surfaceMuted\n } else {\n foreground.copy(alpha = 0.12f)\n }\n\n Surface(\n modifier = modifier,\n color = background,\n contentColor = foreground,\n shape = RoundedCornerShape(LumenRadius.Full)\n ) {\n Text(\n text = text,\n modifier = Modifier.padding(horizontal = LumenSpacing.Sm, vertical = LumenSpacing.Xs),\n style = MaterialTheme.typography.labelSmall.copy(fontWeight = FontWeight.SemiBold)\n )\n }\n}\n\n@Composable\nfun LumenDivider(modifier: Modifier = Modifier) {\n val colors = LocalLumenTheme.current.colors\n\n HorizontalDivider(modifier = modifier, color = colors.line)\n}\n\n@Composable\nfun LumenSpinner(\n modifier: Modifier = Modifier,\n label: String = \"Loading\"\n) {\n val colors = LocalLumenTheme.current.colors\n\n CircularProgressIndicator(\n modifier = modifier.semantics { contentDescription = label },\n color = colors.brand\n )\n}\n",
38749
39606
  "packages/compose/src/main/kotlin/com/santi020k/lumen/SelectionComponents.kt": "package com.santi020k.lumen\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.heightIn\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.selection.selectable\nimport androidx.compose.foundation.selection.selectableGroup\nimport androidx.compose.foundation.selection.toggleable\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.Checkbox\nimport androidx.compose.material3.CheckboxDefaults\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.RadioButton\nimport androidx.compose.material3.RadioButtonDefaults\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.semantics.Role\nimport androidx.compose.ui.semantics.clearAndSetSemantics\nimport androidx.compose.ui.semantics.contentDescription\nimport androidx.compose.ui.semantics.semantics\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\n\n@Immutable\ndata class LumenSelectionOption(\n val value: String,\n val label: String,\n val description: String? = null,\n val enabled: Boolean = true\n)\n\n@Composable\nfun LumenCheckbox(\n label: String,\n checked: Boolean,\n onCheckedChange: (Boolean) -> Unit,\n modifier: Modifier = Modifier,\n description: String? = null,\n enabled: Boolean = true\n) {\n val colors = LocalLumenTheme.current.colors\n\n Row(\n modifier = modifier\n .fillMaxWidth()\n .heightIn(min = LumenButtonMetrics.resolve(LumenControlSize.Md).minHeight)\n .toggleable(\n value = checked,\n enabled = enabled,\n role = Role.Checkbox,\n onValueChange = onCheckedChange\n )\n .semantics(mergeDescendants = true) { contentDescription = label }\n .padding(vertical = LumenSpacing.Sm),\n horizontalArrangement = Arrangement.spacedBy(LumenSpacing.Md),\n verticalAlignment = Alignment.Top\n ) {\n Checkbox(\n checked = checked,\n onCheckedChange = null,\n modifier = Modifier.clearAndSetSemantics {},\n enabled = enabled,\n colors = CheckboxDefaults.colors(\n checkedColor = colors.brandSolid,\n uncheckedColor = colors.line,\n checkmarkColor = colors.onBrand,\n disabledCheckedColor = colors.brandSolid.copy(alpha = 0.52f),\n disabledUncheckedColor = colors.line.copy(alpha = 0.52f)\n )\n )\n Column(\n modifier = Modifier.weight(1f).padding(top = 2.dp),\n verticalArrangement = Arrangement.spacedBy(LumenSpacing.Xs)\n ) {\n Text(\n text = label,\n color = colors.ink,\n style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.SemiBold)\n )\n if (description != null) {\n Text(text = description, color = colors.inkMuted, style = MaterialTheme.typography.bodySmall)\n }\n }\n }\n}\n\n@Composable\nfun LumenRadioGroup(\n label: String,\n options: List<LumenSelectionOption>,\n value: String,\n onValueChange: (String) -> Unit,\n modifier: Modifier = Modifier\n) {\n val colors = LocalLumenTheme.current.colors\n\n Column(\n modifier = modifier\n .fillMaxWidth()\n .selectableGroup()\n .semantics { contentDescription = label },\n verticalArrangement = Arrangement.spacedBy(LumenSpacing.Sm)\n ) {\n Text(\n text = label,\n color = colors.ink,\n style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.SemiBold)\n )\n options.forEach { option ->\n val selected = value == option.value\n\n Row(\n modifier = Modifier\n .fillMaxWidth()\n .heightIn(min = LumenButtonMetrics.resolve(LumenControlSize.Md).minHeight)\n .selectable(\n selected = selected,\n enabled = option.enabled,\n role = Role.RadioButton,\n onClick = { onValueChange(option.value) }\n )\n .padding(vertical = LumenSpacing.Sm),\n horizontalArrangement = Arrangement.spacedBy(LumenSpacing.Md),\n verticalAlignment = Alignment.Top\n ) {\n RadioButton(\n selected = selected,\n onClick = null,\n modifier = Modifier.clearAndSetSemantics {},\n enabled = option.enabled,\n colors = RadioButtonDefaults.colors(\n selectedColor = colors.brandSolid,\n unselectedColor = colors.line,\n disabledSelectedColor = colors.brandSolid.copy(alpha = 0.52f),\n disabledUnselectedColor = colors.line.copy(alpha = 0.52f)\n )\n )\n Column(\n modifier = Modifier.weight(1f).padding(top = 2.dp),\n verticalArrangement = Arrangement.spacedBy(LumenSpacing.Xs)\n ) {\n Text(text = option.label, color = colors.ink, style = MaterialTheme.typography.bodyMedium)\n if (option.description != null) {\n Text(\n text = option.description,\n color = colors.inkMuted,\n style = MaterialTheme.typography.bodySmall\n )\n }\n }\n }\n }\n }\n}\n\n@Composable\nfun LumenSegmentedControl(\n label: String,\n options: List<LumenSelectionOption>,\n value: String,\n onValueChange: (String) -> Unit,\n modifier: Modifier = Modifier,\n showLabel: Boolean = true\n) {\n val colors = LocalLumenTheme.current.colors\n\n Column(\n modifier = modifier.fillMaxWidth(),\n verticalArrangement = Arrangement.spacedBy(LumenSpacing.Sm)\n ) {\n if (showLabel) {\n Text(\n text = label,\n color = colors.ink,\n style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.SemiBold)\n )\n }\n Row(\n modifier = Modifier\n .fillMaxWidth()\n .clip(RoundedCornerShape(LumenRadius.Sm))\n .background(colors.surfaceMuted)\n .border(1.dp, colors.line, RoundedCornerShape(LumenRadius.Sm))\n .padding(2.dp)\n .selectableGroup()\n .semantics { contentDescription = label },\n verticalAlignment = Alignment.CenterVertically\n ) {\n options.forEach { option ->\n val selected = value == option.value\n\n Box(\n modifier = Modifier\n .weight(1f)\n .heightIn(min = 40.dp)\n .clip(RoundedCornerShape(LumenRadius.Sm))\n .background(if (selected) colors.surface else androidx.compose.ui.graphics.Color.Transparent)\n .selectable(\n selected = selected,\n enabled = option.enabled,\n role = Role.RadioButton,\n onClick = { onValueChange(option.value) }\n )\n .padding(horizontal = LumenSpacing.Sm, vertical = LumenSpacing.Sm),\n contentAlignment = Alignment.Center\n ) {\n Text(\n text = option.label,\n color = if (selected) colors.brand else colors.inkSoft,\n fontWeight = if (selected) FontWeight.Bold else FontWeight.SemiBold,\n maxLines = 1,\n overflow = TextOverflow.Ellipsis,\n style = MaterialTheme.typography.labelSmall\n )\n }\n }\n }\n }\n}\n",
38750
39607
  "packages/compose/src/main/kotlin/com/santi020k/lumen/SharedComponents.kt": "package com.santi020k.lumen\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.painter.Painter\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.semantics.ProgressBarRangeInfo\nimport androidx.compose.ui.semantics.Role\nimport androidx.compose.ui.semantics.clearAndSetSemantics\nimport androidx.compose.ui.semantics.contentDescription\nimport androidx.compose.ui.semantics.progressBarRangeInfo\nimport androidx.compose.ui.semantics.semantics\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\n\nenum class LumenCardVariant {\n Accent,\n Default,\n Destructive,\n Muted,\n Success,\n Warning\n}\n\n@Immutable\ndata class LumenCardPalette(\n val background: Color,\n val border: Color\n)\n\nfun lumenCardPalette(colors: LumenColorPalette, variant: LumenCardVariant): LumenCardPalette {\n if (variant == LumenCardVariant.Default) return LumenCardPalette(colors.surface, colors.line)\n\n if (variant == LumenCardVariant.Muted) return LumenCardPalette(colors.surfaceMuted, colors.line)\n\n val accent = when (variant) {\n LumenCardVariant.Accent -> colors.accent\n LumenCardVariant.Destructive -> colors.danger\n LumenCardVariant.Success -> colors.success\n LumenCardVariant.Warning -> colors.warning\n LumenCardVariant.Default, LumenCardVariant.Muted -> colors.line\n }\n\n return LumenCardPalette(\n background = accent.copy(alpha = 0.06f),\n border = accent.copy(alpha = 0.24f)\n )\n}\n\n@Composable\nfun LumenCard(\n modifier: Modifier = Modifier,\n variant: LumenCardVariant = LumenCardVariant.Default,\n onClick: (() -> Unit)? = null,\n enabled: Boolean = true,\n content: @Composable () -> Unit\n) {\n val colors = LocalLumenTheme.current.colors\n val palette = lumenCardPalette(colors, variant)\n val interactionModifier = if (onClick == null) {\n Modifier\n } else {\n Modifier.clickable(enabled = enabled, role = Role.Button, onClick = onClick)\n }\n\n Surface(\n modifier = modifier.then(interactionModifier),\n color = palette.background,\n contentColor = colors.ink,\n shape = RoundedCornerShape(LumenRadius.Lg),\n border = androidx.compose.foundation.BorderStroke(1.dp, palette.border)\n ) {\n Box(modifier = Modifier.padding(LumenSpacing.Xl)) {\n content()\n }\n }\n}\n\nenum class LumenAlertVariant {\n Default,\n Destructive,\n Success,\n Warning\n}\n\nprivate data class LumenAlertPalette(\n val background: Color,\n val border: Color,\n val foreground: Color\n)\n\nprivate fun alertPalette(\n colors: LumenColorPalette,\n variant: LumenAlertVariant\n): LumenAlertPalette = when (variant) {\n LumenAlertVariant.Default -> LumenAlertPalette(colors.surface, colors.line, colors.ink)\n LumenAlertVariant.Destructive -> LumenAlertPalette(\n colors.danger.copy(alpha = 0.08f),\n colors.danger.copy(alpha = 0.36f),\n colors.danger\n )\n LumenAlertVariant.Success -> LumenAlertPalette(\n colors.success.copy(alpha = 0.08f),\n colors.success.copy(alpha = 0.36f),\n colors.success\n )\n LumenAlertVariant.Warning -> LumenAlertPalette(\n colors.warning.copy(alpha = 0.10f),\n colors.warning.copy(alpha = 0.42f),\n colors.warning\n )\n}\n\n@Composable\nfun LumenAlert(\n modifier: Modifier = Modifier,\n variant: LumenAlertVariant = LumenAlertVariant.Default,\n content: @Composable () -> Unit\n) {\n val palette = alertPalette(LocalLumenTheme.current.colors, variant)\n\n Surface(\n modifier = modifier,\n color = palette.background,\n contentColor = palette.foreground,\n shape = RoundedCornerShape(LumenRadius.Md),\n border = androidx.compose.foundation.BorderStroke(1.dp, palette.border)\n ) {\n Box(\n modifier = Modifier.padding(horizontal = LumenSpacing.Lg, vertical = LumenSpacing.Md)\n ) {\n content()\n }\n }\n}\n\n@Immutable\ndata class LumenProgressValue(\n val max: Float,\n val value: Float\n) {\n val fraction: Float get() = value / max\n\n companion object {\n fun resolve(value: Float, max: Float): LumenProgressValue {\n val safeMax = if (max.isFinite() && max > 0f) max else 100f\n val finiteValue = if (value.isFinite()) value else 0f\n\n return LumenProgressValue(\n max = safeMax,\n value = finiteValue.coerceIn(0f, safeMax)\n )\n }\n }\n}\n\n@Composable\nfun LumenProgress(\n value: Float,\n modifier: Modifier = Modifier,\n max: Float = 100f,\n label: String? = null\n) {\n val colors = LocalLumenTheme.current.colors\n val progress = LumenProgressValue.resolve(value, max)\n val semanticsModifier = Modifier.semantics {\n progressBarRangeInfo = ProgressBarRangeInfo(progress.value, 0f..progress.max)\n if (label != null) contentDescription = label\n }\n\n Box(\n modifier = modifier\n .then(semanticsModifier)\n .fillMaxWidth()\n .height(LumenSpacing.Sm)\n .clip(RoundedCornerShape(LumenRadius.Full))\n .background(colors.surfaceStrong)\n ) {\n Box(\n modifier = Modifier\n .fillMaxWidth(progress.fraction)\n .fillMaxHeight()\n .background(colors.brandSolid)\n )\n }\n}\n\nenum class LumenAvatarSize(val dimension: Dp) {\n Sm(32.dp),\n Md(40.dp),\n Lg(56.dp)\n}\n\n@Composable\nfun LumenAvatar(\n modifier: Modifier = Modifier,\n painter: Painter? = null,\n fallback: String = \"?\",\n size: LumenAvatarSize = LumenAvatarSize.Md,\n label: String? = null\n) {\n val colors = LocalLumenTheme.current.colors\n\n Box(\n modifier = modifier\n .size(size.dimension)\n .clip(CircleShape)\n .background(colors.surfaceMuted)\n .border(1.dp, colors.line, CircleShape)\n .clearAndSetSemantics {\n if (label != null) contentDescription = label\n },\n contentAlignment = Alignment.Center\n ) {\n if (painter == null) {\n Text(\n text = fallback,\n color = colors.ink,\n fontWeight = FontWeight.Bold\n )\n } else {\n Image(\n painter = painter,\n contentDescription = null,\n modifier = Modifier.fillMaxWidth().fillMaxHeight(),\n contentScale = ContentScale.Crop\n )\n }\n }\n}\n",
38751
39608
  "packages/compose/src/main/kotlin/com/santi020k/lumen/StructuredComponents.kt": "package com.santi020k.lumen\n\nimport androidx.compose.foundation.BorderStroke\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.IntrinsicSize\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.heightIn\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.layout.widthIn\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.semantics.contentDescription\nimport androidx.compose.ui.semantics.heading\nimport androidx.compose.ui.semantics.semantics\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\n\nenum class LumenBannerVariant {\n Accent,\n Default,\n Destructive,\n Success,\n Warning\n}\n\nenum class LumenMetricTone {\n Accent,\n Brand,\n Danger,\n Neutral,\n Success,\n Warning\n}\n\n@Immutable\ndata class LumenBannerPalette(\n val accent: Color,\n val background: Color,\n val border: Color\n)\n\nfun lumenMetricColor(colors: LumenColorPalette, tone: LumenMetricTone): Color = when (tone) {\n LumenMetricTone.Accent -> colors.accent\n LumenMetricTone.Brand -> colors.brand\n LumenMetricTone.Danger -> colors.danger\n LumenMetricTone.Neutral -> colors.inkMuted\n LumenMetricTone.Success -> colors.success\n LumenMetricTone.Warning -> colors.warning\n}\n\nfun lumenBannerPalette(\n colors: LumenColorPalette,\n variant: LumenBannerVariant\n): LumenBannerPalette {\n val tone = when (variant) {\n LumenBannerVariant.Accent -> LumenMetricTone.Accent\n LumenBannerVariant.Default -> LumenMetricTone.Brand\n LumenBannerVariant.Destructive -> LumenMetricTone.Danger\n LumenBannerVariant.Success -> LumenMetricTone.Success\n LumenBannerVariant.Warning -> LumenMetricTone.Warning\n }\n val accent = lumenMetricColor(colors, tone)\n\n return LumenBannerPalette(\n accent = accent,\n background = if (variant == LumenBannerVariant.Default) colors.surface else accent.copy(alpha = 0.08f),\n border = if (variant == LumenBannerVariant.Default) colors.line else accent.copy(alpha = 0.32f)\n )\n}\n\n@Composable\nfun LumenEmptyState(\n title: String,\n modifier: Modifier = Modifier,\n description: String? = null,\n graphic: (@Composable () -> Unit)? = null,\n actions: (@Composable () -> Unit)? = null\n) {\n val colors = LocalLumenTheme.current.colors\n\n Column(\n modifier = modifier\n .fillMaxWidth()\n .widthIn(max = 440.dp)\n .padding(LumenSpacing.Xl),\n horizontalAlignment = Alignment.CenterHorizontally,\n verticalArrangement = Arrangement.spacedBy(LumenSpacing.Lg)\n ) {\n if (graphic != null) {\n Box(\n modifier = Modifier\n .size(56.dp)\n .clip(CircleShape)\n .background(colors.surfaceMuted),\n contentAlignment = Alignment.Center\n ) {\n graphic()\n }\n }\n\n Column(\n horizontalAlignment = Alignment.CenterHorizontally,\n verticalArrangement = Arrangement.spacedBy(LumenSpacing.Sm)\n ) {\n Text(\n text = title,\n color = colors.ink,\n style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),\n textAlign = TextAlign.Center\n )\n\n if (description != null) {\n Text(\n text = description,\n color = colors.inkMuted,\n style = MaterialTheme.typography.bodyMedium,\n textAlign = TextAlign.Center\n )\n }\n }\n\n actions?.invoke()\n }\n}\n\n@Composable\nfun LumenListRow(\n modifier: Modifier = Modifier,\n leading: (@Composable () -> Unit)? = null,\n trailing: (@Composable () -> Unit)? = null,\n content: @Composable () -> Unit\n) {\n Row(\n modifier = modifier\n .fillMaxWidth()\n .heightIn(min = 56.dp)\n .padding(horizontal = LumenSpacing.Lg, vertical = LumenSpacing.Md),\n horizontalArrangement = Arrangement.spacedBy(LumenSpacing.Md),\n verticalAlignment = Alignment.CenterVertically\n ) {\n leading?.invoke()\n\n Box(modifier = Modifier.weight(1f)) {\n content()\n }\n\n trailing?.invoke()\n }\n}\n\n@Composable\nfun LumenBanner(\n title: String,\n modifier: Modifier = Modifier,\n description: String? = null,\n variant: LumenBannerVariant = LumenBannerVariant.Default,\n dismissLabel: String = \"Dismiss\",\n onDismiss: (() -> Unit)? = null,\n graphic: (@Composable () -> Unit)? = null,\n actions: (@Composable () -> Unit)? = null\n) {\n val colors = LocalLumenTheme.current.colors\n val palette = lumenBannerPalette(colors, variant)\n val shape = RoundedCornerShape(LumenRadius.Md)\n\n Surface(\n modifier = modifier,\n color = palette.background,\n contentColor = colors.ink,\n shape = shape,\n border = BorderStroke(1.dp, palette.border)\n ) {\n Row(\n modifier = Modifier.height(IntrinsicSize.Min)\n ) {\n Box(\n modifier = Modifier\n .width(3.dp)\n .fillMaxHeight()\n .background(palette.accent)\n )\n Row(\n modifier = Modifier\n .weight(1f)\n .padding(horizontal = LumenSpacing.Lg, vertical = LumenSpacing.Md),\n horizontalArrangement = Arrangement.spacedBy(LumenSpacing.Md),\n verticalAlignment = Alignment.CenterVertically\n ) {\n graphic?.invoke()\n\n Column(\n modifier = Modifier.weight(1f),\n verticalArrangement = Arrangement.spacedBy(LumenSpacing.Xs)\n ) {\n Text(\n text = title,\n color = colors.ink,\n style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.SemiBold)\n )\n\n if (description != null) {\n Text(\n text = description,\n color = colors.inkSoft,\n style = MaterialTheme.typography.bodySmall\n )\n }\n }\n\n actions?.invoke()\n\n if (onDismiss != null) {\n TextButton(onClick = onDismiss) {\n Text(dismissLabel, color = palette.accent)\n }\n }\n }\n }\n }\n}\n\n@Composable\nfun LumenStat(\n label: String,\n value: String,\n modifier: Modifier = Modifier,\n detail: String? = null,\n tone: LumenMetricTone = LumenMetricTone.Brand,\n graphic: (@Composable () -> Unit)? = null\n) {\n val colors = LocalLumenTheme.current.colors\n val accent = lumenMetricColor(colors, tone)\n val accessibleText = listOfNotNull(label, value, detail).joinToString(\", \")\n\n Column(\n modifier = modifier\n .semantics(mergeDescendants = true) { contentDescription = accessibleText }\n .background(accent.copy(alpha = 0.06f), RoundedCornerShape(LumenRadius.Md))\n .border(1.dp, accent.copy(alpha = 0.18f), RoundedCornerShape(LumenRadius.Md))\n .padding(LumenSpacing.Lg),\n verticalArrangement = Arrangement.spacedBy(LumenSpacing.Sm)\n ) {\n graphic?.invoke()\n\n Text(\n text = value,\n color = colors.ink,\n style = MaterialTheme.typography.headlineSmall.copy(fontWeight = FontWeight.Bold)\n )\n Text(\n text = label,\n color = colors.inkSoft,\n style = MaterialTheme.typography.labelSmall.copy(fontWeight = FontWeight.SemiBold)\n )\n\n if (detail != null) {\n Text(text = detail, color = colors.inkMuted, style = MaterialTheme.typography.bodySmall)\n }\n }\n}\n\n@Composable\nfun LumenSectionHeader(\n title: String,\n modifier: Modifier = Modifier,\n subtitle: String? = null,\n count: String? = null,\n actions: (@Composable () -> Unit)? = null\n) {\n val colors = LocalLumenTheme.current.colors\n\n Row(\n modifier = modifier.fillMaxWidth(),\n horizontalArrangement = Arrangement.spacedBy(LumenSpacing.Md),\n verticalAlignment = Alignment.Top\n ) {\n Column(\n modifier = Modifier.weight(1f),\n verticalArrangement = Arrangement.spacedBy(LumenSpacing.Xs)\n ) {\n Row(\n horizontalArrangement = Arrangement.spacedBy(LumenSpacing.Sm),\n verticalAlignment = Alignment.CenterVertically\n ) {\n Text(\n text = title,\n modifier = Modifier.semantics { heading() },\n color = colors.ink,\n style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold)\n )\n if (count != null) LumenBadge(count)\n }\n\n if (subtitle != null) {\n Text(text = subtitle, color = colors.inkMuted, style = MaterialTheme.typography.bodySmall)\n }\n }\n\n actions?.invoke()\n }\n}\n\n@Composable\nfun LumenStatusBar(\n message: String,\n modifier: Modifier = Modifier,\n tone: LumenMetricTone = LumenMetricTone.Neutral,\n trailing: (@Composable () -> Unit)? = null\n) {\n val colors = LocalLumenTheme.current.colors\n\n Row(\n modifier = modifier\n .fillMaxWidth()\n .heightIn(min = 36.dp)\n .background(colors.surfaceMuted)\n .padding(horizontal = LumenSpacing.Lg),\n horizontalArrangement = Arrangement.spacedBy(LumenSpacing.Sm),\n verticalAlignment = Alignment.CenterVertically\n ) {\n Spacer(\n modifier = Modifier\n .size(7.dp)\n .clip(CircleShape)\n .background(lumenMetricColor(colors, tone))\n )\n Text(\n text = message,\n modifier = Modifier.weight(1f),\n color = colors.inkSoft,\n maxLines = 1,\n overflow = TextOverflow.Ellipsis,\n style = MaterialTheme.typography.bodySmall\n )\n trailing?.invoke()\n }\n}\n",
38752
- "packages/compose/src/main/kotlin/com/santi020k/lumen/Tokens.generated.kt": "// This file is generated by scripts/generate-platform-tokens.mjs.\npackage com.santi020k.lumen\n\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\n\n@Immutable\ndata class LumenColorPalette(\n val canvas: Color,\n val surface: Color,\n val surfaceMuted: Color,\n val surfaceStrong: Color,\n val line: Color,\n val ink: Color,\n val inkSoft: Color,\n val inkMuted: Color,\n val brand: Color,\n val brandSolid: Color,\n val brandSoft: Color,\n val onBrand: Color,\n val accent: Color,\n val success: Color,\n val warning: Color,\n val danger: Color,\n val onDanger: Color\n)\n\nobject LumenColors {\n val Light = LumenColorPalette(\n canvas = Color(0xFFFFFFFF),\n surface = Color(0xFFFFFFFF),\n surfaceMuted = Color(0xFFF3F4F6),\n surfaceStrong = Color(0xFFE5E7EB),\n line = Color(0xFFD7DAE0),\n ink = Color(0xFF0F1729),\n inkSoft = Color(0xFF454F5F),\n inkMuted = Color(0xFF6C7889),\n brand = Color(0xFF2463EB),\n brandSolid = Color(0xFF1452D7),\n brandSoft = Color(0xFFF0F5FF),\n onBrand = Color(0xFFFFFFFF),\n accent = Color(0xFF16A286),\n success = Color(0xFF1B9D4A),\n warning = Color(0xFFF59F0A),\n danger = Color(0xFFEF4343),\n onDanger = Color(0xFF000000)\n )\n\n val Dark = LumenColorPalette(\n canvas = Color(0xFF101219),\n surface = Color(0xFF171A21),\n surfaceMuted = Color(0xFF20242C),\n surfaceStrong = Color(0xFF303540),\n line = Color(0xFF393F4C),\n ink = Color(0xFFF3F5F7),\n inkSoft = Color(0xFFBCC3CD),\n inkMuted = Color(0xFF949CA8),\n brand = Color(0xFF29B4F5),\n brandSolid = Color(0xFF087AAF),\n brandSoft = Color(0xFF07354B),\n onBrand = Color(0xFFFFFFFF),\n accent = Color(0xFF1AC19A),\n success = Color(0xFF24C25E),\n warning = Color(0xFFF6A823),\n danger = Color(0xFFF15B5B),\n onDanger = Color(0xFF101219)\n )\n}\n\nobject LumenSpacing {\n val Zero = 0f.dp\n val Xs = 4f.dp\n val Sm = 8f.dp\n val Md = 12f.dp\n val Lg = 16f.dp\n val Xl = 24f.dp\n val Size2xl = 32f.dp\n val Size3xl = 48f.dp\n}\n\nobject LumenRadius {\n val Sm = 6f.dp\n val Md = 10f.dp\n val Lg = 12f.dp\n val Full = 9999f.dp\n}\n\nobject LumenTypography {\n val XsSize = 12f.sp\n val SmSize = 14f.sp\n val MdSize = 16f.sp\n val LgSize = 18f.sp\n val XlSize = 20f.sp\n val Size2xlSize = 24f.sp\n const val RegularWeight = 400\n const val MediumWeight = 500\n const val SemiboldWeight = 600\n const val BoldWeight = 700\n}\n\n@Immutable\ndata class LumenCubicBezier(\n val x1: Float,\n val y1: Float,\n val x2: Float,\n val y2: Float\n)\n\nobject LumenMotion {\n const val FastDurationMillis = 120\n const val StandardDurationMillis = 160\n const val SlowDurationMillis = 300\n val StandardEasing = LumenCubicBezier(0.32f, 0.72f, 0f, 1f)\n val EmphasizedEasing = LumenCubicBezier(0.22f, 1f, 0.36f, 1f)\n}\n\nobject LumenElevation {\n val Resting = 1f.dp\n val Raised = 3f.dp\n val Overlay = 6f.dp\n}\n"
39609
+ "packages/compose/src/main/kotlin/com/santi020k/lumen/Tokens.generated.kt": "// This file is generated by scripts/generate-platform-tokens.mjs.\npackage com.santi020k.lumen\n\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\n\n@Immutable\ndata class LumenColorPalette(\n val canvas: Color,\n val surface: Color,\n val surfaceMuted: Color,\n val surfaceStrong: Color,\n val line: Color,\n val ink: Color,\n val inkSoft: Color,\n val inkMuted: Color,\n val brand: Color,\n val brandSolid: Color,\n val brandSoft: Color,\n val onBrand: Color,\n val accent: Color,\n val success: Color,\n val warning: Color,\n val danger: Color,\n val onDanger: Color\n)\n\nobject LumenColors {\n val Light = LumenColorPalette(\n canvas = Color(0xFFFFFFFF),\n surface = Color(0xFFFFFFFF),\n surfaceMuted = Color(0xFFF3F4F6),\n surfaceStrong = Color(0xFFE5E7EB),\n line = Color(0xFFD7DAE0),\n ink = Color(0xFF0F1729),\n inkSoft = Color(0xFF454F5F),\n inkMuted = Color(0xFF6C7889),\n brand = Color(0xFF2463EB),\n brandSolid = Color(0xFF1452D7),\n brandSoft = Color(0xFFF0F5FF),\n onBrand = Color(0xFFFFFFFF),\n accent = Color(0xFF16A286),\n success = Color(0xFF1B9D4A),\n warning = Color(0xFFF59F0A),\n danger = Color(0xFFEF4343),\n onDanger = Color(0xFF000000)\n )\n\n val Dark = LumenColorPalette(\n canvas = Color(0xFF101219),\n surface = Color(0xFF171A21),\n surfaceMuted = Color(0xFF20242C),\n surfaceStrong = Color(0xFF303540),\n line = Color(0xFF393F4C),\n ink = Color(0xFFF3F5F7),\n inkSoft = Color(0xFFBCC3CD),\n inkMuted = Color(0xFF949CA8),\n brand = Color(0xFF29B4F5),\n brandSolid = Color(0xFF087AAF),\n brandSoft = Color(0xFF07354B),\n onBrand = Color(0xFFFFFFFF),\n accent = Color(0xFF1AC19A),\n success = Color(0xFF24C25E),\n warning = Color(0xFFF6A823),\n danger = Color(0xFFF15B5B),\n onDanger = Color(0xFF101219)\n )\n}\n\nobject LumenSpacing {\n val Zero = 0f.dp\n val Xs = 4f.dp\n val Sm = 8f.dp\n val Md = 12f.dp\n val Lg = 16f.dp\n val Xl = 24f.dp\n val Size2xl = 32f.dp\n val Size3xl = 48f.dp\n}\n\nobject LumenRadius {\n val Sm = 6f.dp\n val Md = 10f.dp\n val Lg = 12f.dp\n val Full = 9999f.dp\n}\n\nobject LumenTypography {\n val XsSize = 12f.sp\n val SmSize = 14f.sp\n val MdSize = 16f.sp\n val LgSize = 18f.sp\n val XlSize = 20f.sp\n val Size2xlSize = 24f.sp\n const val RegularWeight = 400\n const val MediumWeight = 500\n const val SemiboldWeight = 600\n const val BoldWeight = 700\n}\n\n@Immutable\ndata class LumenCubicBezier(\n val x1: Float,\n val y1: Float,\n val x2: Float,\n val y2: Float\n)\n\nobject LumenMotion {\n const val FastDurationMillis = 120\n const val StandardDurationMillis = 160\n const val SlowDurationMillis = 300\n val StandardEasing = LumenCubicBezier(0.32f, 0.72f, 0f, 1f)\n val EmphasizedEasing = LumenCubicBezier(0.22f, 1f, 0.36f, 1f)\n}\n\nobject LumenElevation {\n val Resting = 1f.dp\n val Raised = 3f.dp\n val Overlay = 6f.dp\n}\n",
39610
+ "packages/compose/wear/src/main/kotlin/com/santi020k/lumen/wear/WearComponents.kt": "package com.santi020k.lumen\n\nimport androidx.compose.foundation.Canvas\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.BoxScope\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.RowScope\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.text.BasicText\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.drawscope.Stroke\nimport androidx.compose.ui.semantics.ProgressBarRangeInfo\nimport androidx.compose.ui.semantics.Role\nimport androidx.compose.ui.semantics.contentDescription\nimport androidx.compose.ui.semantics.disabled\nimport androidx.compose.ui.semantics.progressBarRangeInfo\nimport androidx.compose.ui.semantics.role\nimport androidx.compose.ui.semantics.semantics\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\n\nenum class LumenWearTone {\n Accent,\n Brand,\n Danger,\n Neutral,\n Success,\n Warning\n}\n\ndata class LumenWearProgressValue(\n val maximum: Float,\n val value: Float\n) {\n val fraction: Float\n get() = value / maximum\n\n companion object {\n fun resolve(value: Float, maximum: Float): LumenWearProgressValue {\n val safeMaximum = if (maximum.isFinite() && maximum > 0f) maximum else 1f\n val safeValue = if (value.isFinite()) value.coerceIn(0f, safeMaximum) else 0f\n return LumenWearProgressValue(maximum = safeMaximum, value = safeValue)\n }\n }\n}\n\ndata class LumenWearActionMetrics(\n val dimension: Dp,\n val ringWidth: Dp\n) {\n companion object {\n fun resolve(dimension: Dp, ringWidth: Dp): LumenWearActionMetrics {\n val safeDimension = if (dimension.value.isFinite()) {\n dimension.value.coerceIn(48f, 180f).dp\n } else {\n 120.dp\n }\n val safeRingWidth = if (ringWidth.value.isFinite()) {\n ringWidth.value.coerceIn(2f, 12f).dp\n } else {\n 4.dp\n }\n return LumenWearActionMetrics(safeDimension, safeRingWidth)\n }\n }\n}\n\nfun lumenWearColor(palette: LumenColorPalette, tone: LumenWearTone): Color = when (tone) {\n LumenWearTone.Accent -> palette.accent\n LumenWearTone.Brand -> palette.brandSolid\n LumenWearTone.Danger -> palette.danger\n LumenWearTone.Neutral -> palette.inkMuted\n LumenWearTone.Success -> palette.success\n LumenWearTone.Warning -> palette.warning\n}\n\n@Composable\nfun LumenWearTheme(\n darkTheme: Boolean = true,\n values: LumenThemeValues? = null,\n content: @Composable () -> Unit\n) {\n val resolvedValues = values ?: LumenThemeValues(\n colors = if (darkTheme) LumenColors.Dark else LumenColors.Light,\n isDark = darkTheme\n )\n CompositionLocalProvider(LocalLumenTheme provides resolvedValues, content = content)\n}\n\n@Composable\nfun LumenWearActionButton(\n accessibilityLabel: String,\n onClick: () -> Unit,\n modifier: Modifier = Modifier,\n tone: LumenWearTone = LumenWearTone.Brand,\n dimension: Dp = 120.dp,\n enabled: Boolean = true,\n content: @Composable BoxScope.() -> Unit\n) {\n val colors = LocalLumenTheme.current.colors\n val metrics = LumenWearActionMetrics.resolve(dimension, 4.dp)\n val color = lumenWearColor(colors, tone)\n\n Box(\n modifier = modifier\n .size(metrics.dimension)\n .clip(CircleShape)\n .background(color)\n .alpha(if (enabled) 1f else 0.48f)\n .clickable(enabled = enabled, role = Role.Button, onClick = onClick)\n .semantics {\n role = Role.Button\n contentDescription = accessibilityLabel\n if (!enabled) disabled()\n },\n contentAlignment = Alignment.Center,\n content = content\n )\n}\n\n@Composable\nfun LumenWearProgressRing(\n value: Float,\n modifier: Modifier = Modifier,\n maximum: Float = 1f,\n tone: LumenWearTone = LumenWearTone.Brand,\n lineWidth: Dp = 4.dp,\n content: @Composable BoxScope.() -> Unit\n) {\n val colors = LocalLumenTheme.current.colors\n val progress = LumenWearProgressValue.resolve(value, maximum)\n val metrics = LumenWearActionMetrics.resolve(120.dp, lineWidth)\n\n Box(\n modifier = modifier.semantics {\n progressBarRangeInfo = ProgressBarRangeInfo(progress.value, 0f..progress.maximum)\n },\n contentAlignment = Alignment.Center\n ) {\n Canvas(modifier = Modifier.matchParentSize()) {\n val strokeWidth = metrics.ringWidth.toPx()\n val radius = (size.minDimension - strokeWidth) / 2\n drawCircle(\n color = colors.line.copy(alpha = 0.5f),\n radius = radius,\n style = Stroke(width = strokeWidth)\n )\n drawArc(\n color = lumenWearColor(colors, tone),\n startAngle = -90f,\n sweepAngle = 360f * progress.fraction,\n useCenter = false,\n topLeft = Offset(center.x - radius, center.y - radius),\n size = androidx.compose.ui.geometry.Size(radius * 2, radius * 2),\n style = Stroke(width = strokeWidth, cap = StrokeCap.Round)\n )\n }\n content()\n }\n}\n\n@Composable\nfun LumenWearStatus(\n text: String,\n modifier: Modifier = Modifier,\n tone: LumenWearTone = LumenWearTone.Neutral,\n leading: (@Composable RowScope.() -> Unit)? = null\n) {\n val colors = LocalLumenTheme.current.colors\n val color = lumenWearColor(colors, tone)\n Row(\n modifier = modifier\n .clip(CircleShape)\n .background(color.copy(alpha = 0.24f))\n .padding(horizontal = LumenSpacing.Sm, vertical = LumenSpacing.Xs),\n horizontalArrangement = Arrangement.spacedBy(LumenSpacing.Xs),\n verticalAlignment = Alignment.CenterVertically\n ) {\n leading?.invoke(this)\n BasicText(\n text = text,\n style = TextStyle(\n color = if (tone == LumenWearTone.Neutral) colors.inkSoft else color,\n fontSize = 12.sp,\n fontWeight = FontWeight.SemiBold\n )\n )\n }\n}\n\n@Composable\nfun LumenWearMetric(\n label: String,\n value: String,\n modifier: Modifier = Modifier,\n detail: String? = null,\n tone: LumenWearTone = LumenWearTone.Neutral\n) {\n val colors = LocalLumenTheme.current.colors\n Column(\n modifier = modifier\n .clip(RoundedCornerShape(LumenRadius.Md))\n .background(colors.surfaceMuted.copy(alpha = 0.84f))\n .padding(LumenSpacing.Sm),\n verticalArrangement = Arrangement.spacedBy(2.dp)\n ) {\n BasicText(label, style = TextStyle(color = colors.inkMuted, fontSize = 12.sp))\n BasicText(\n value,\n style = TextStyle(\n color = lumenWearColor(colors, tone),\n fontSize = 18.sp,\n fontWeight = FontWeight.Bold\n )\n )\n if (detail != null) {\n BasicText(detail, style = TextStyle(color = colors.inkSoft, fontSize = 12.sp))\n }\n }\n}\n\n@Composable\nfun LumenWearListRow(\n modifier: Modifier = Modifier,\n leading: (@Composable RowScope.() -> Unit)? = null,\n trailing: (@Composable RowScope.() -> Unit)? = null,\n content: @Composable RowScope.() -> Unit\n) {\n val colors = LocalLumenTheme.current.colors\n Row(\n modifier = modifier\n .fillMaxWidth()\n .clip(RoundedCornerShape(LumenRadius.Md))\n .background(colors.surfaceMuted.copy(alpha = 0.84f))\n .padding(LumenSpacing.Sm),\n horizontalArrangement = Arrangement.spacedBy(LumenSpacing.Sm),\n verticalAlignment = Alignment.CenterVertically\n ) {\n leading?.invoke(this)\n content()\n trailing?.invoke(this)\n }\n}\n"
38753
39611
  },
38754
39612
  "react-native": {
38755
39613
  "packages/react-native/src/additional-components.tsx": "import {\n type ReactElement,\n type ReactNode,\n type Ref\n} from 'react'\nimport {\n type HostInstance,\n Pressable,\n type PressableProps,\n Text,\n TextInput,\n type TextInputInstance,\n type TextInputProps,\n type TextStyle,\n View,\n type ViewProps\n} from 'react-native'\n\nimport { resolveLumenButtonOpacity } from './recipes.js'\nimport { type LumenAlertVariant, resolveLumenAlertColors } from './shared-recipes.js'\nimport { useLumenTheme } from './theme-context.js'\n\nconst resolveOptional = <Value,>(value: Value | undefined, fallback: Value): Value => value ?? fallback\n\nconst resolveTextareaBorderColor = (\n errorMessage: string | undefined,\n dangerColor: string,\n lineColor: string\n): string => errorMessage ? dangerColor : lineColor\n\nconst resolveTextareaOpacity = (editable: boolean): number => editable ? 1 : 0.52\n\nconst LumenTextareaMessage = ({\n color,\n fontSize,\n message\n}: {\n color: string\n fontSize: number\n message: string | undefined\n}): ReactElement | null => message ? <Text style={{ color, fontSize }}>{message}</Text> : null\n\nexport interface LumenToastProps extends ViewProps {\n action?: ReactNode\n description?: string\n dismissLabel?: string\n onDismiss?: () => void\n ref?: Ref<HostInstance>\n title: string\n variant?: LumenAlertVariant\n}\n\nexport const LumenToast = ({\n action,\n description,\n dismissLabel = 'Dismiss',\n onDismiss,\n ref,\n style,\n title,\n variant = 'default',\n ...props\n}: LumenToastProps): ReactElement => {\n const theme = useLumenTheme()\n const colors = resolveLumenAlertColors(theme.colors, variant)\n\n return (\n <View\n ref={ref}\n {...props}\n accessibilityRole=\"alert\"\n style={[\n {\n alignItems: 'center',\n backgroundColor: colors.backgroundColor,\n borderColor: colors.borderColor,\n borderRadius: theme.radii.md,\n borderWidth: 1,\n flexDirection: 'row',\n gap: theme.spacing.md,\n paddingHorizontal: theme.spacing.lg,\n paddingVertical: theme.spacing.md\n },\n style\n ]}\n >\n <View style={{ flex: 1, gap: theme.spacing.xs }}>\n <Text\n style={{\n color: colors.color,\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.inkSoft, fontSize: theme.fontSizes.xs }}>{description}</Text> :\n null}\n </View>\n {action}\n {onDismiss ?\n (\n <Pressable\n accessibilityLabel={dismissLabel}\n accessibilityRole=\"button\"\n onPress={onDismiss}\n style={({ pressed }) => ({\n opacity: resolveLumenButtonOpacity(false, pressed),\n padding: theme.spacing.sm\n })}\n >\n <Text style={{ color: colors.color, fontSize: theme.fontSizes.xs }}>×</Text>\n </Pressable>\n ) :\n null}\n </View>\n )\n}\n\nexport interface LumenTextareaProps extends Omit<\n TextInputProps,\n 'multiline' | 'onChangeText' | 'value'\n> {\n description?: string\n errorMessage?: string\n label: string\n onChangeText: (value: string) => void\n ref?: Ref<TextInputInstance>\n value: string\n}\n\nexport const LumenTextarea = ({\n description,\n editable = true,\n errorMessage,\n label,\n numberOfLines = 4,\n onChangeText,\n placeholder,\n placeholderTextColor,\n ref,\n style,\n value,\n ...props\n}: LumenTextareaProps): ReactElement => {\n const theme = useLumenTheme()\n\n const supportingColor = resolveTextareaBorderColor(\n errorMessage,\n theme.colors.danger,\n theme.colors.inkMuted\n )\n\n const supportingMessage = resolveOptional(errorMessage, description)\n\n return (\n <View style={{ 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 <TextInput\n ref={ref}\n {...props}\n accessibilityHint={resolveOptional(errorMessage, description)}\n accessibilityLabel={resolveOptional(props.accessibilityLabel, label)}\n editable={editable}\n multiline\n numberOfLines={numberOfLines}\n onChangeText={onChangeText}\n placeholder={placeholder}\n placeholderTextColor={resolveOptional(placeholderTextColor, theme.colors.inkMuted)}\n style={[\n {\n backgroundColor: theme.colors.surface,\n borderColor: resolveTextareaBorderColor(\n errorMessage,\n theme.colors.danger,\n theme.colors.line\n ),\n borderRadius: theme.radii.sm,\n borderWidth: 1,\n color: theme.colors.ink,\n fontSize: theme.fontSizes.sm,\n minHeight: 112,\n opacity: resolveTextareaOpacity(editable),\n padding: theme.spacing.md,\n textAlignVertical: 'top'\n },\n style\n ]}\n value={value}\n />\n <LumenTextareaMessage\n color={supportingColor}\n fontSize={theme.fontSizes.xs}\n message={supportingMessage}\n />\n </View>\n )\n}\n\nexport interface LumenChipProps extends Omit<PressableProps, 'children'> {\n label: string\n onRemove?: () => void\n ref?: Ref<HostInstance>\n removeLabel?: string\n selected?: boolean\n}\n\nexport const LumenChip = ({\n disabled = false,\n label,\n onPress,\n onRemove,\n ref,\n removeLabel = `Remove ${label}`,\n selected = false,\n style,\n ...props\n}: LumenChipProps): ReactElement => {\n const theme = useLumenTheme()\n\n const content = (\n <>\n <Text\n style={{\n color: selected ? theme.colors.brand : theme.colors.inkSoft,\n fontSize: theme.fontSizes.xs,\n fontWeight: String(theme.fontWeights.semibold) as TextStyle['fontWeight']\n }}\n >\n {label}\n </Text>\n {onRemove ?\n (\n <Pressable\n accessibilityLabel={removeLabel}\n accessibilityRole=\"button\"\n disabled={disabled}\n hitSlop={6}\n onPress={event => {\n event.stopPropagation()\n\n onRemove()\n }}\n >\n <Text style={{ color: theme.colors.inkMuted, fontSize: theme.fontSizes.xs }}>×</Text>\n </Pressable>\n ) :\n null}\n </>\n )\n\n const baseStyle = {\n alignItems: 'center' as const,\n alignSelf: 'flex-start' as const,\n backgroundColor: selected ? theme.colors.brandSoft : theme.colors.surfaceMuted,\n borderColor: selected ? theme.colors.brand : theme.colors.line,\n borderRadius: theme.radii.full,\n borderWidth: 1,\n flexDirection: 'row' as const,\n gap: theme.spacing.xs,\n minHeight: 32,\n opacity: disabled ? 0.52 : 1,\n paddingHorizontal: theme.spacing.md\n }\n\n if (!onPress) {\n return <View ref={ref} {...props} style={[baseStyle, style as ViewProps['style']]}>{content}</View>\n }\n\n return (\n <Pressable\n ref={ref}\n {...props}\n accessibilityRole=\"button\"\n accessibilityState={{ ...props.accessibilityState, disabled, selected }}\n disabled={disabled}\n onPress={onPress}\n style={state => [\n baseStyle,\n { opacity: resolveLumenButtonOpacity(disabled, state.pressed) },\n typeof style === 'function' ? style(state) : style\n ]}\n >\n {content}\n </Pressable>\n )\n}\n\nexport interface LumenFieldGroupProps extends ViewProps {\n children: ReactNode\n description?: string\n errorMessage?: string\n label: string\n ref?: Ref<HostInstance>\n required?: boolean\n}\n\nexport const LumenFieldGroup = ({\n children,\n description,\n errorMessage,\n label,\n ref,\n required = false,\n style,\n ...props\n}: LumenFieldGroupProps): ReactElement => {\n const theme = useLumenTheme()\n\n return (\n <View ref={ref} {...props} style={[{ gap: theme.spacing.sm }, style]}>\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 {required ? ' *' : ''}\n </Text>\n {description ?\n <Text style={{ color: theme.colors.inkMuted, fontSize: theme.fontSizes.xs }}>{description}</Text> :\n null}\n {children}\n {errorMessage ?\n (\n <Text accessibilityRole=\"alert\" style={{ color: theme.colors.danger, fontSize: theme.fontSizes.xs }}>\n {errorMessage}\n </Text>\n ) :\n null}\n </View>\n )\n}\n\nexport type LumenButtonGroupOrientation = 'horizontal' | 'vertical'\n\nexport interface LumenButtonGroupProps extends ViewProps {\n children: ReactNode\n orientation?: LumenButtonGroupOrientation\n ref?: Ref<HostInstance>\n}\n\nexport const LumenButtonGroup = ({\n children,\n orientation = 'horizontal',\n ref,\n style,\n ...props\n}: LumenButtonGroupProps): ReactElement => {\n const theme = useLumenTheme()\n\n return (\n <View\n ref={ref}\n {...props}\n style={[\n {\n alignItems: orientation === 'horizontal' ? 'center' : 'stretch',\n flexDirection: orientation === 'horizontal' ? 'row' : 'column',\n gap: theme.spacing.sm\n },\n style\n ]}\n >\n {children}\n </View>\n )\n}\n",
@@ -38760,13 +39618,14 @@
38760
39618
  "packages/react-native/src/form-recipes.test.ts": "import { describe, expect, test } from 'vitest'\n\nimport { resolveLumenSearchFieldState } from './form-recipes.js'\n\ndescribe('Lumen React Native form component recipes', () => {\n test('shows the clear action only for editable non-empty queries', () => {\n expect(resolveLumenSearchFieldState('lumen', true)).toEqual({\n opacity: 1,\n showClearAction: true\n })\n expect(resolveLumenSearchFieldState('', true).showClearAction).toBe(false)\n expect(resolveLumenSearchFieldState('lumen', false)).toEqual({\n opacity: 0.52,\n showClearAction: false\n })\n })\n})\n",
38761
39619
  "packages/react-native/src/form-recipes.ts": "export interface LumenSearchFieldState {\n opacity: number\n showClearAction: boolean\n}\n\nexport const resolveLumenSearchFieldState = (\n value: string,\n editable: boolean\n): LumenSearchFieldState => ({\n opacity: editable ? 1 : 0.52,\n showClearAction: editable && value.length > 0\n})\n",
38762
39620
  "packages/react-native/src/index.test.ts": "import { describe, expect, test } from 'vitest'\n\nimport {\n createLumenTheme,\n lumenDarkTheme,\n lumenLightTheme\n} from './theme.js'\nimport { lumenColorTokens } from './tokens.generated.js'\n\ndescribe('Lumen React Native foundations', () => {\n test('provides matching semantic roles for light and dark themes', () => {\n expect(Object.keys(lumenColorTokens.light)).toEqual(\n Object.keys(lumenColorTokens.dark)\n )\n expect(lumenLightTheme.colors.brand).toBe('#2463EB')\n expect(lumenDarkTheme.colors.brand).toBe('#29B4F5')\n })\n\n test('creates a complete theme for the requested scheme', () => {\n const theme = createLumenTheme('dark')\n\n expect(theme.scheme).toBe('dark')\n expect(theme.spacing.lg).toBe(16)\n expect(theme.radii.md).toBe(10)\n expect(theme.durations.standard).toBe(160)\n })\n})\n",
38763
- "packages/react-native/src/index.ts": "export {\n LumenButtonGroup,\n type LumenButtonGroupOrientation,\n type LumenButtonGroupProps,\n LumenChip,\n type LumenChipProps,\n LumenFieldGroup,\n type LumenFieldGroupProps,\n LumenTextarea,\n type LumenTextareaProps,\n LumenToast,\n type LumenToastProps\n} from './additional-components.js'\nexport {\n LumenBackdrop,\n type LumenBackdropProps,\n type LumenBackdropTone,\n type LumenBackdropVariant,\n LumenDisclosure,\n type LumenDisclosureProps,\n LumenGraphic,\n type LumenGraphicProps,\n type LumenGraphicTone,\n type LumenGraphicVariant,\n LumenIllustration,\n type LumenIllustrationProps,\n type LumenIllustrationTone,\n type LumenIllustrationVariant,\n LumenSkeleton,\n type LumenSkeletonProps,\n type LumenSkeletonShape\n} from './content-components.js'\nexport {\n type LumenBackdropIntensity,\n type LumenGraphicSize,\n type LumenIllustrationSize\n} from './content-recipes.js'\nexport {\n LumenSearchField,\n type LumenSearchFieldProps,\n LumenSettingsRow,\n type LumenSettingsRowProps,\n LumenToggle,\n type LumenToggleProps\n} from './form-components.js'\nexport {\n LumenAlertDialog,\n type LumenAlertDialogProps,\n LumenMenu,\n type LumenMenuItem,\n type LumenMenuProps,\n LumenShareButton,\n type LumenShareButtonProps,\n LumenSheet,\n type LumenSheetProps\n} from './overlay-components.js'\nexport {\n type LumenMenuPosition,\n type LumenMenuPositionInput,\n resolveLumenMenuPosition\n} from './overlay-recipes.js'\nexport {\n LumenNavigationBar,\n type LumenNavigationBarProps,\n type LumenNavigationItem,\n LumenRefreshControl,\n type LumenRefreshControlProps\n} from './platform-components.js'\nexport { type LumenRefreshIndicatorTone } from './platform-recipes.js'\nexport {\n LumenBadge,\n type LumenBadgeProps,\n LumenButton,\n type LumenButtonIntent,\n type LumenButtonProps,\n type LumenControlSize,\n LumenDivider,\n type LumenDividerProps,\n LumenIcon,\n LumenIconButton,\n type LumenIconButtonProps,\n type LumenIconGraphic,\n type LumenIconGraphicProps,\n type LumenIconProps,\n type LumenIconSize,\n LumenSpinner,\n type LumenSpinnerProps,\n LumenSurface,\n type LumenSurfacePadding,\n type LumenSurfaceProps,\n type LumenSurfaceRadius,\n type LumenSurfaceTone,\n LumenText,\n LumenTextField,\n type LumenTextFieldProps,\n type LumenTextProps,\n type LumenTextTone,\n type LumenTextVariant\n} from './primitives.js'\nexport {\n LumenProvider,\n type LumenProviderProps\n} from './provider.js'\nexport {\n LumenCheckbox,\n type LumenCheckboxProps,\n LumenRadioGroup,\n type LumenRadioGroupProps,\n LumenSegmentedControl,\n type LumenSegmentedControlProps,\n type LumenSelectionOption } from './selection-components.js'\nexport {\n LumenAlert,\n LumenAlertDescription,\n type LumenAlertDescriptionProps,\n type LumenAlertProps,\n LumenAlertTitle,\n type LumenAlertTitleProps,\n LumenAvatar,\n type LumenAvatarProps,\n LumenCard,\n type LumenCardProps,\n LumenProgress,\n type LumenProgressProps\n} from './shared-components.js'\nexport {\n type LumenAlertVariant,\n type LumenAvatarSize,\n type LumenCardVariant\n} from './shared-recipes.js'\nexport {\n LumenBanner,\n type LumenBannerProps,\n LumenEmptyState,\n type LumenEmptyStateProps,\n LumenListRow,\n type LumenListRowProps,\n LumenSectionHeader,\n type LumenSectionHeaderProps,\n LumenStat,\n type LumenStatProps,\n LumenStatusBar,\n type LumenStatusBarProps\n} from './structured-components.js'\nexport {\n type LumenBannerVariant,\n type LumenMetricTone\n} from './structured-recipes.js'\nexport {\n createLumenTheme,\n lumenDarkTheme,\n lumenLightTheme,\n type LumenTheme\n} from './theme.js'\nexport { useLumenTheme } from './theme-context.js'\nexport {\n type LumenColorScheme,\n lumenColorTokens,\n lumenDurations,\n lumenEasings,\n lumenElevation,\n lumenFontFamilies,\n lumenFontSizes,\n lumenFontWeights,\n lumenRadii,\n type LumenSemanticColor,\n lumenSpacing } from './tokens.generated.js'\n",
39621
+ "packages/react-native/src/index.ts": "export {\n LumenButtonGroup,\n type LumenButtonGroupOrientation,\n type LumenButtonGroupProps,\n LumenChip,\n type LumenChipProps,\n LumenFieldGroup,\n type LumenFieldGroupProps,\n LumenTextarea,\n type LumenTextareaProps,\n LumenToast,\n type LumenToastProps\n} from './additional-components.js'\nexport {\n LumenBackdrop,\n type LumenBackdropProps,\n type LumenBackdropTone,\n type LumenBackdropVariant,\n LumenDisclosure,\n type LumenDisclosureProps,\n LumenGraphic,\n type LumenGraphicProps,\n type LumenGraphicTone,\n type LumenGraphicVariant,\n LumenIllustration,\n type LumenIllustrationProps,\n type LumenIllustrationTone,\n type LumenIllustrationVariant,\n LumenSkeleton,\n type LumenSkeletonProps,\n type LumenSkeletonShape\n} from './content-components.js'\nexport {\n type LumenBackdropIntensity,\n type LumenGraphicSize,\n type LumenIllustrationSize\n} from './content-recipes.js'\nexport {\n LumenSearchField,\n type LumenSearchFieldProps,\n LumenSettingsRow,\n type LumenSettingsRowProps,\n LumenToggle,\n type LumenToggleProps\n} from './form-components.js'\nexport {\n LumenAlertDialog,\n type LumenAlertDialogProps,\n LumenMenu,\n type LumenMenuItem,\n type LumenMenuProps,\n LumenShareButton,\n type LumenShareButtonProps,\n LumenSheet,\n type LumenSheetProps\n} from './overlay-components.js'\nexport {\n type LumenMenuPosition,\n type LumenMenuPositionInput,\n resolveLumenMenuPosition\n} from './overlay-recipes.js'\nexport {\n LumenCollapsibleNavigationBar,\n type LumenCollapsibleNavigationBarProps,\n LumenNavigationAccessory,\n type LumenNavigationAccessoryProps,\n LumenNavigationBar,\n type LumenNavigationBarProps,\n type LumenNavigationItem,\n LumenRefreshControl,\n type LumenRefreshControlProps\n} from './platform-components.js'\nexport {\n type LumenNavigationBarVisibilityController,\n type LumenNavigationBarVisibilityOptions,\n useLumenNavigationBarVisibility\n} from './platform-hooks.js'\nexport {\n createLumenNavigationVisibilityState,\n type LumenNavigationBadge,\n type LumenNavigationVisibilityState,\n type LumenRefreshIndicatorTone,\n resolveLumenNavigationBadge,\n resolveLumenNavigationVisibility\n} from './platform-recipes.js'\nexport {\n LumenBadge,\n type LumenBadgeProps,\n LumenButton,\n type LumenButtonIntent,\n type LumenButtonProps,\n type LumenControlSize,\n LumenDivider,\n type LumenDividerProps,\n LumenIcon,\n LumenIconButton,\n type LumenIconButtonProps,\n type LumenIconGraphic,\n type LumenIconGraphicProps,\n type LumenIconProps,\n type LumenIconSize,\n LumenSpinner,\n type LumenSpinnerProps,\n LumenSurface,\n type LumenSurfacePadding,\n type LumenSurfaceProps,\n type LumenSurfaceRadius,\n type LumenSurfaceTone,\n LumenText,\n LumenTextField,\n type LumenTextFieldProps,\n type LumenTextProps,\n type LumenTextTone,\n type LumenTextVariant\n} from './primitives.js'\nexport { LumenProvider, type LumenProviderProps } from './provider.js'\nexport {\n LumenCheckbox,\n type LumenCheckboxProps,\n LumenRadioGroup,\n type LumenRadioGroupProps,\n LumenSegmentedControl,\n type LumenSegmentedControlProps,\n type LumenSelectionOption\n} from './selection-components.js'\nexport {\n LumenAlert,\n LumenAlertDescription,\n type LumenAlertDescriptionProps,\n type LumenAlertProps,\n LumenAlertTitle,\n type LumenAlertTitleProps,\n LumenAvatar,\n type LumenAvatarProps,\n LumenCard,\n type LumenCardProps,\n LumenProgress,\n type LumenProgressProps\n} from './shared-components.js'\nexport {\n type LumenAlertVariant,\n type LumenAvatarSize,\n type LumenCardVariant\n} from './shared-recipes.js'\nexport {\n LumenBanner,\n type LumenBannerProps,\n LumenEmptyState,\n type LumenEmptyStateProps,\n LumenListRow,\n type LumenListRowProps,\n LumenSectionHeader,\n type LumenSectionHeaderProps,\n LumenStat,\n type LumenStatProps,\n LumenStatusBar,\n type LumenStatusBarProps\n} from './structured-components.js'\nexport {\n type LumenBannerVariant,\n type LumenMetricTone\n} from './structured-recipes.js'\nexport {\n createLumenTheme,\n lumenDarkTheme,\n lumenLightTheme,\n type LumenTheme\n} from './theme.js'\nexport { useLumenTheme } from './theme-context.js'\nexport {\n type LumenColorScheme,\n lumenColorTokens,\n lumenDurations,\n lumenEasings,\n lumenElevation,\n lumenFontFamilies,\n lumenFontSizes,\n lumenFontWeights,\n lumenRadii,\n type LumenSemanticColor,\n lumenSpacing\n} from './tokens.generated.js'\n",
38764
39622
  "packages/react-native/src/overlay-components.tsx": "import {\n type ComponentRef,\n type ReactElement,\n type ReactNode,\n useRef,\n useState\n} from 'react'\nimport {\n Modal,\n Pressable,\n Share,\n type ShareContent,\n type ShareOptions,\n Text,\n type TextStyle,\n useWindowDimensions,\n View,\n type ViewStyle\n} from 'react-native'\n\nimport {\n type LumenMenuPosition,\n resolveLumenMenuPosition\n} from './overlay-recipes.js'\nimport { LumenButton } from './primitives.js'\nimport { useLumenTheme } from './theme-context.js'\n\nexport interface LumenAlertDialogProps {\n cancelLabel?: string\n confirmDisabled?: boolean\n confirmLabel: string\n confirmLoading?: boolean\n destructive?: boolean\n description?: string\n onConfirm: () => void\n onDismiss: () => void\n title: string\n visible: boolean\n}\n\n/** A controlled native confirmation dialog with explicit cancel and confirm actions. */\nexport const LumenAlertDialog = ({\n cancelLabel = 'Cancel',\n confirmDisabled = false,\n confirmLabel,\n confirmLoading = false,\n destructive = false,\n description,\n onConfirm,\n onDismiss,\n title,\n visible\n}: LumenAlertDialogProps): ReactElement => {\n const theme = useLumenTheme()\n\n return (\n <Modal\n animationType=\"fade\"\n onRequestClose={onDismiss}\n transparent\n visible={visible}\n >\n <View\n accessibilityViewIsModal\n style={{\n alignItems: 'center',\n backgroundColor: '#00000080',\n flex: 1,\n justifyContent: 'center',\n padding: theme.spacing.xl\n }}\n >\n <View\n accessibilityRole=\"alert\"\n style={{\n backgroundColor: theme.colors.surface,\n borderColor: theme.colors.line,\n borderRadius: theme.radii.lg,\n borderWidth: 1,\n gap: theme.spacing.lg,\n maxWidth: 440,\n padding: theme.spacing.xl,\n width: '100%'\n }}\n >\n <View style={{ gap: theme.spacing.sm }}>\n <Text\n style={{\n color: theme.colors.ink,\n fontSize: theme.fontSizes.lg,\n fontWeight: String(theme.fontWeights.semibold) as TextStyle['fontWeight']\n }}\n >\n {title}\n </Text>\n {description ?\n (\n <Text style={{ color: theme.colors.inkSoft, fontSize: theme.fontSizes.sm }}>\n {description}\n </Text>\n ) :\n null}\n </View>\n <View\n style={{\n flexDirection: 'row',\n flexWrap: 'wrap',\n gap: theme.spacing.sm,\n justifyContent: 'flex-end'\n }}\n >\n <LumenButton intent=\"quiet\" onPress={onDismiss}>\n {cancelLabel}\n </LumenButton>\n <LumenButton\n disabled={confirmDisabled}\n intent={destructive ? 'danger' : 'primary'}\n loading={confirmLoading}\n onPress={onConfirm}\n >\n {confirmLabel}\n </LumenButton>\n </View>\n </View>\n </View>\n </Modal>\n )\n}\n\nexport interface LumenSheetProps {\n actions?: ReactNode\n children: ReactNode\n description?: string\n onDismiss: () => void\n title?: string\n visible: boolean\n}\n\n/** A controlled bottom sheet that preserves native modal focus and dismissal behavior. */\nexport const LumenSheet = ({\n actions,\n children,\n description,\n onDismiss,\n title,\n visible\n}: LumenSheetProps): ReactElement => {\n const theme = useLumenTheme()\n\n return (\n <Modal\n animationType=\"slide\"\n onRequestClose={onDismiss}\n transparent\n visible={visible}\n >\n <View accessibilityViewIsModal style={{ flex: 1, justifyContent: 'flex-end' }}>\n <Pressable\n accessibilityElementsHidden\n importantForAccessibility=\"no-hide-descendants\"\n onPress={onDismiss}\n style={{ backgroundColor: '#00000066', flex: 1 }}\n />\n <View\n style={{\n backgroundColor: theme.colors.surface,\n borderColor: theme.colors.line,\n borderTopLeftRadius: theme.radii.lg,\n borderTopRightRadius: theme.radii.lg,\n borderWidth: 1,\n gap: theme.spacing.lg,\n maxHeight: '90%',\n padding: theme.spacing.xl\n }}\n >\n {title || description ?\n (\n <View style={{ gap: theme.spacing.sm }}>\n {title ?\n (\n <Text\n accessibilityRole=\"header\"\n style={{\n color: theme.colors.ink,\n fontSize: theme.fontSizes.lg,\n fontWeight: String(theme.fontWeights.semibold) as TextStyle['fontWeight']\n }}\n >\n {title}\n </Text>\n ) :\n null}\n {description ?\n <Text style={{ color: theme.colors.inkSoft }}>{description}</Text> :\n null}\n </View>\n ) :\n null}\n {children}\n {actions ? <View style={{ alignItems: 'flex-end' }}>{actions}</View> : null}\n </View>\n </View>\n </Modal>\n )\n}\n\nexport interface LumenMenuItem {\n destructive?: boolean\n disabled?: boolean\n label: string\n onPress: () => void\n}\n\nexport interface LumenMenuProps {\n accessibilityLabel: string\n items: readonly LumenMenuItem[]\n trigger: ReactNode\n}\n\nconst menuWidth = 240\n\n/** An anchored action menu with native modal focus containment and labeled item states. */\nexport const LumenMenu = ({\n accessibilityLabel,\n items,\n trigger\n}: LumenMenuProps): ReactElement => {\n const theme = useLumenTheme()\n const triggerRef = useRef<ComponentRef<typeof Pressable>>(null)\n const { height: windowHeight, width: windowWidth } = useWindowDimensions()\n\n const [position, setPosition] = useState<LumenMenuPosition>({\n left: theme.spacing.lg,\n top: theme.spacing.lg\n })\n\n const [visible, setVisible] = useState(false)\n\n const open = (): void => {\n triggerRef.current?.measureInWindow((x, y, width, height) => {\n setPosition(resolveLumenMenuPosition({\n anchorHeight: height,\n anchorWidth: width,\n anchorX: x,\n anchorY: y,\n itemCount: items.length,\n margin: theme.spacing.lg,\n menuWidth,\n windowHeight,\n windowWidth\n }))\n\n setVisible(true)\n })\n }\n\n return (\n <>\n <Pressable\n ref={triggerRef}\n accessibilityLabel={accessibilityLabel}\n accessibilityRole=\"button\"\n accessibilityState={{ expanded: visible }}\n onPress={open}\n >\n {trigger}\n </Pressable>\n <Modal\n animationType=\"fade\"\n onRequestClose={() => {\n setVisible(false)\n }}\n transparent\n visible={visible}\n >\n <Pressable\n accessibilityViewIsModal\n onPress={() => {\n setVisible(false)\n }}\n style={{ flex: 1 }}\n >\n <View\n accessibilityLabel={accessibilityLabel}\n accessibilityRole=\"menu\"\n style={{\n backgroundColor: theme.colors.surface,\n borderColor: theme.colors.line,\n borderRadius: theme.radii.md,\n borderWidth: 1,\n left: position.left,\n padding: theme.spacing.sm,\n position: 'absolute',\n top: position.top,\n width: menuWidth\n }}\n >\n {items.map(item => (\n <Pressable\n key={item.label}\n accessibilityRole=\"menuitem\"\n accessibilityState={{ disabled: item.disabled }}\n disabled={item.disabled}\n onPress={() => {\n setVisible(false)\n\n item.onPress()\n }}\n style={({ pressed }): ViewStyle => ({\n backgroundColor: pressed ? theme.colors.surfaceMuted : 'transparent',\n borderRadius: theme.radii.sm,\n minHeight: 44,\n opacity: item.disabled ? 0.52 : 1,\n paddingHorizontal: theme.spacing.md,\n paddingVertical: theme.spacing.md\n })}\n >\n <Text\n style={{\n color: item.destructive ? theme.colors.danger : theme.colors.ink,\n fontSize: theme.fontSizes.sm\n }}\n >\n {item.label}\n </Text>\n </Pressable>\n ))}\n </View>\n </Pressable>\n </Modal>\n </>\n )\n}\n\nexport interface LumenShareButtonProps {\n content: ShareContent\n disabled?: boolean\n label?: string\n onError?: (error: unknown) => void\n onShared?: () => void\n options?: ShareOptions\n}\n\n/** A Lumen button that opens the operating system's share sheet. */\nexport const LumenShareButton = ({\n content,\n disabled = false,\n label = 'Share',\n onError,\n onShared,\n options\n}: LumenShareButtonProps): ReactElement => {\n const share = async (): Promise<void> => {\n try {\n const result = await Share.share(content, options)\n\n if (result.action === Share.sharedAction) onShared?.()\n } catch (error) {\n onError?.(error)\n }\n }\n\n return (\n <LumenButton\n disabled={disabled}\n intent=\"secondary\"\n onPress={() => {\n void share()\n }}\n >\n {label}\n </LumenButton>\n )\n}\n",
38765
39623
  "packages/react-native/src/overlay-recipes.test.ts": "import { describe, expect, test } from 'vitest'\n\nimport { resolveLumenMenuPosition } from './overlay-recipes.js'\n\ndescribe('React Native overlay recipes', () => {\n test('keeps anchored menus within the horizontal viewport', () => {\n expect(resolveLumenMenuPosition({\n anchorHeight: 44,\n anchorWidth: 44,\n anchorX: 350,\n anchorY: 40,\n itemCount: 2,\n margin: 16,\n menuWidth: 240,\n windowHeight: 800,\n windowWidth: 390\n })).toEqual({ left: 134, top: 88 })\n })\n\n test('places a menu above its trigger when the space below is insufficient', () => {\n expect(resolveLumenMenuPosition({\n anchorHeight: 44,\n anchorWidth: 44,\n anchorX: 24,\n anchorY: 700,\n itemCount: 3,\n margin: 16,\n menuWidth: 240,\n windowHeight: 800,\n windowWidth: 390\n })).toEqual({ left: 16, top: 524 })\n })\n})\n",
38766
39624
  "packages/react-native/src/overlay-recipes.ts": "export interface LumenMenuPositionInput {\n anchorHeight: number\n anchorWidth: number\n anchorX: number\n anchorY: number\n itemCount: number\n margin: number\n menuWidth: number\n windowHeight: number\n windowWidth: number\n}\n\nexport interface LumenMenuPosition {\n left: number\n top: number\n}\n\nexport const resolveLumenMenuPosition = ({\n anchorHeight,\n anchorWidth,\n anchorX,\n anchorY,\n itemCount,\n margin,\n menuWidth,\n windowHeight,\n windowWidth\n}: LumenMenuPositionInput): LumenMenuPosition => {\n const left = Math.max(\n margin,\n Math.min(anchorX + anchorWidth - menuWidth, windowWidth - menuWidth - margin)\n )\n\n const estimatedHeight = Math.max(1, itemCount) * 48 + margin * 2\n const below = anchorY + anchorHeight + 4\n const top = below + estimatedHeight <= windowHeight ? below : Math.max(margin, anchorY - estimatedHeight)\n\n return { left, top }\n}\n",
38767
- "packages/react-native/src/platform-components.tsx": "import { type ComponentRef, type ReactElement, type Ref } from 'react'\nimport {\n type HostInstance,\n Pressable,\n RefreshControl,\n type RefreshControlProps,\n Text,\n type TextStyle,\n View,\n type ViewProps\n} from 'react-native'\n\nimport {\n type LumenRefreshIndicatorTone,\n resolveLumenNavigationItemState,\n resolveLumenRefreshColors\n} from './platform-recipes.js'\nimport {\n LumenIcon,\n type LumenIconGraphic\n} from './primitives.js'\nimport { useLumenTheme } from './theme-context.js'\n\nexport interface LumenNavigationItem {\n disabled?: boolean\n icon?: LumenIconGraphic\n label: string\n selectedIcon?: LumenIconGraphic\n value: string\n}\n\nexport interface LumenNavigationBarProps extends Omit<ViewProps, 'children'> {\n accessibilityLabel?: string\n items: readonly LumenNavigationItem[]\n onValueChange: (value: string) => void\n ref?: Ref<HostInstance>\n value: string\n}\n\n/**\n * A controlled, token-aware destination bar for app-level navigation.\n * Screen routing and navigation history remain application-owned.\n */\nexport const LumenNavigationBar = ({\n accessibilityLabel = 'Primary navigation',\n items,\n onValueChange,\n ref,\n style,\n value,\n ...props\n}: LumenNavigationBarProps): ReactElement => {\n const theme = useLumenTheme()\n\n return (\n <View\n ref={ref}\n {...props}\n accessibilityLabel={accessibilityLabel}\n accessibilityRole=\"tablist\"\n style={[\n {\n backgroundColor: theme.colors.surface,\n borderColor: theme.colors.line,\n borderTopWidth: 1,\n flexDirection: 'row',\n minHeight: 64,\n paddingHorizontal: theme.spacing.sm\n },\n style\n ]}\n >\n {items.map(item => {\n const itemState = resolveLumenNavigationItemState(\n theme.colors,\n item.value,\n value,\n Boolean(item.disabled),\n false\n )\n\n const selected = itemState.selected\n const Graphic = selected && item.selectedIcon ? item.selectedIcon : item.icon\n\n return (\n <Pressable\n accessibilityLabel={item.label}\n accessibilityRole=\"tab\"\n accessibilityState={{ disabled: item.disabled, selected }}\n disabled={item.disabled}\n key={item.value}\n onPress={() => {\n onValueChange(item.value)\n }}\n style={({ pressed }) => ({\n alignItems: 'center',\n flex: 1,\n gap: theme.spacing.xs,\n justifyContent: 'center',\n minHeight: 64,\n opacity: resolveLumenNavigationItemState(\n theme.colors,\n item.value,\n value,\n Boolean(item.disabled),\n pressed\n ).opacity,\n paddingHorizontal: theme.spacing.xs,\n paddingVertical: theme.spacing.sm\n })}\n >\n {Graphic ?\n (\n <LumenIcon color={itemState.color} decorative icon={Graphic} size=\"md\" />\n ) :\n null}\n <Text\n numberOfLines={1}\n style={{\n color: itemState.color,\n fontSize: theme.fontSizes.xs,\n fontWeight: String(\n selected ? theme.fontWeights.semibold : theme.fontWeights.medium\n ) as TextStyle['fontWeight']\n }}\n >\n {item.label}\n </Text>\n </Pressable>\n )\n })}\n </View>\n )\n}\n\nexport interface LumenRefreshControlProps extends Omit<\n RefreshControlProps,\n 'accessibilityLabel' | 'colors' | 'onRefresh' | 'progressBackgroundColor' | 'tintColor' | 'titleColor'\n> {\n accessibilityLabel: string\n indicatorTone?: LumenRefreshIndicatorTone\n onRefresh: NonNullable<RefreshControlProps['onRefresh']>\n ref?: Ref<ComponentRef<typeof RefreshControl>>\n}\n\n/**\n * A token-aware native pull-to-refresh indicator for React Native scroll containers.\n */\nexport const LumenRefreshControl = ({\n indicatorTone = 'brand',\n ref,\n ...props\n}: LumenRefreshControlProps): ReactElement => {\n const theme = useLumenTheme()\n const colors = resolveLumenRefreshColors(theme.colors, indicatorTone)\n\n return (\n <RefreshControl\n ref={ref}\n {...props}\n colors={[colors.indicator]}\n progressBackgroundColor={colors.surface}\n tintColor={colors.indicator}\n titleColor={colors.title}\n />\n )\n}\n",
38768
- "packages/react-native/src/platform-recipes.test.ts": "import { describe, expect, test } from 'vitest'\n\nimport {\n resolveLumenNavigationItemState,\n resolveLumenRefreshColors\n} from './platform-recipes.js'\nimport { lumenDarkTheme, lumenLightTheme } from './theme.js'\n\ndescribe('React Native platform recipes', () => {\n test('maps refresh indicators to semantic theme colors', () => {\n expect(resolveLumenRefreshColors(lumenLightTheme.colors, 'brand')).toEqual({\n indicator: lumenLightTheme.colors.brand,\n surface: lumenLightTheme.colors.surface,\n title: lumenLightTheme.colors.inkMuted\n })\n expect(resolveLumenRefreshColors(lumenDarkTheme.colors, 'accent').indicator).toBe(\n lumenDarkTheme.colors.accent\n )\n expect(resolveLumenRefreshColors(lumenDarkTheme.colors, 'neutral').indicator).toBe(\n lumenDarkTheme.colors.inkSoft\n )\n })\n\n test('resolves selected and disabled destination states', () => {\n expect(resolveLumenNavigationItemState(\n lumenLightTheme.colors,\n 'home',\n 'home',\n false,\n false\n )).toEqual({\n color: lumenLightTheme.colors.brand,\n opacity: 1,\n selected: true\n })\n expect(resolveLumenNavigationItemState(\n lumenDarkTheme.colors,\n 'search',\n 'home',\n true,\n true\n )).toEqual({\n color: lumenDarkTheme.colors.inkMuted,\n opacity: 0.52,\n selected: false\n })\n })\n})\n",
38769
- "packages/react-native/src/platform-recipes.ts": "import type { LumenTheme } from './theme.js'\n\nexport type LumenRefreshIndicatorTone = 'accent' | 'brand' | 'neutral'\n\nexport interface LumenNavigationItemState {\n color: string\n opacity: number\n selected: boolean\n}\n\nexport const resolveLumenNavigationItemState = (\n colors: LumenTheme['colors'],\n itemValue: string,\n selectedValue: string,\n disabled: boolean,\n pressed: boolean\n): LumenNavigationItemState => {\n const selected = itemValue === selectedValue\n let opacity = 1\n\n if (disabled) opacity = 0.52\n\n else if (pressed) opacity = 0.72\n\n return {\n color: selected ? colors.brand : colors.inkMuted,\n opacity,\n selected\n }\n}\n\nexport interface LumenRefreshColors {\n indicator: string\n surface: string\n title: string\n}\n\nconst resolveIndicatorColor = (\n colors: LumenTheme['colors'],\n tone: LumenRefreshIndicatorTone\n): string => {\n if (tone === 'accent') return colors.accent\n\n if (tone === 'neutral') return colors.inkSoft\n\n return colors.brand\n}\n\nexport const resolveLumenRefreshColors = (\n colors: LumenTheme['colors'],\n tone: LumenRefreshIndicatorTone\n): LumenRefreshColors => ({\n indicator: resolveIndicatorColor(colors, tone),\n surface: colors.surface,\n title: colors.inkMuted\n})\n",
39625
+ "packages/react-native/src/platform-components.tsx": "import {\n type ComponentRef,\n type ReactElement,\n type ReactNode,\n type Ref,\n useEffect,\n useState\n} from 'react'\nimport {\n Animated,\n Easing,\n type HostInstance,\n Pressable,\n RefreshControl,\n type RefreshControlProps,\n type StyleProp,\n Text,\n type TextStyle,\n View,\n type ViewProps,\n type ViewStyle\n} from 'react-native'\n\nimport {\n type LumenNavigationBadge,\n type LumenRefreshIndicatorTone,\n type LumenResolvedNavigationBadge,\n resolveLumenNavigationAction,\n resolveLumenNavigationBadge,\n resolveLumenNavigationItemState,\n resolveLumenRefreshColors\n} from './platform-recipes.js'\nimport { LumenIcon, type LumenIconGraphic } from './primitives.js'\nimport { useLumenTheme } from './theme-context.js'\n\nexport interface LumenNavigationItem {\n badge?: LumenNavigationBadge\n disabled?: boolean\n icon?: LumenIconGraphic\n label: string\n selectedIcon?: LumenIconGraphic\n value: string\n}\n\nexport interface LumenNavigationBarProps extends Omit<ViewProps, 'children'> {\n accessibilityLabel?: string\n items: readonly LumenNavigationItem[]\n onReselect?: (value: string) => void\n onValueChange: (value: string) => void\n ref?: Ref<HostInstance>\n value: string\n}\n\ninterface LumenNavigationItemIconProps {\n badge: LumenResolvedNavigationBadge | undefined\n color: string\n graphic: LumenIconGraphic | undefined\n}\n\nconst LumenNavigationItemIcon = ({\n badge,\n color,\n graphic: Graphic\n}: LumenNavigationItemIconProps): ReactElement | null => {\n const theme = useLumenTheme()\n\n if (!Graphic && !badge) return null\n\n return (\n <View style={{ minHeight: 24, minWidth: 24 }}>\n {Graphic ?\n (\n <LumenIcon color={color} decorative icon={Graphic} size=\"md\" />\n ) :\n null}\n {badge ?\n (\n <View\n accessibilityElementsHidden\n importantForAccessibility=\"no-hide-descendants\"\n style={{\n alignItems: 'center',\n backgroundColor: theme.colors.danger,\n borderRadius: 999,\n justifyContent: 'center',\n minHeight: badge.displayValue ? 16 : 8,\n minWidth: badge.displayValue ? 16 : 8,\n maxWidth: badge.displayValue ? 40 : 8,\n paddingHorizontal: badge.displayValue ? 4 : 0,\n position: 'absolute',\n right: -8,\n top: -5\n }}\n >\n {badge.displayValue ?\n (\n <Text\n numberOfLines={1}\n style={{\n color: theme.colors.onDanger,\n fontSize: 10,\n fontWeight: '700'\n }}\n >\n {badge.displayValue}\n </Text>\n ) :\n null}\n </View>\n ) :\n null}\n </View>\n )\n}\n\ninterface LumenNavigationDestinationProps {\n item: LumenNavigationItem\n onReselect: ((value: string) => void) | undefined\n onValueChange: (value: string) => void\n value: string\n}\n\nconst LumenNavigationDestination = ({\n item,\n onReselect,\n onValueChange,\n value\n}: LumenNavigationDestinationProps): ReactElement => {\n const theme = useLumenTheme()\n\n const itemState = resolveLumenNavigationItemState(\n theme.colors,\n item.value,\n value,\n Boolean(item.disabled),\n false\n )\n\n const selected = itemState.selected\n const badge = resolveLumenNavigationBadge(item.badge)\n const graphic = selected && item.selectedIcon ? item.selectedIcon : item.icon\n\n return (\n <Pressable\n accessibilityLabel={item.label}\n accessibilityRole=\"tab\"\n accessibilityState={{ disabled: item.disabled, selected }}\n accessibilityValue={badge ? { text: badge.accessibilityLabel } : undefined}\n disabled={item.disabled}\n onPress={() => {\n if (\n resolveLumenNavigationAction(\n item.value,\n value,\n Boolean(onReselect)\n ) === 'reselect'\n )\n onReselect?.(item.value)\n else onValueChange(item.value)\n }}\n style={({ pressed }) => ({\n alignItems: 'center',\n flex: 1,\n gap: theme.spacing.xs,\n justifyContent: 'center',\n minHeight: 64,\n opacity: resolveLumenNavigationItemState(\n theme.colors,\n item.value,\n value,\n Boolean(item.disabled),\n pressed\n ).opacity,\n paddingHorizontal: theme.spacing.xs,\n paddingVertical: theme.spacing.sm\n })}\n >\n <LumenNavigationItemIcon\n badge={badge}\n color={itemState.color}\n graphic={graphic}\n />\n <Text\n numberOfLines={1}\n style={{\n color: itemState.color,\n fontSize: theme.fontSizes.xs,\n fontWeight: String(\n selected ? theme.fontWeights.semibold : theme.fontWeights.medium\n ) as TextStyle['fontWeight']\n }}\n >\n {item.label}\n </Text>\n </Pressable>\n )\n}\n\n/**\n * A controlled, token-aware destination bar for app-level navigation.\n * Screen routing and navigation history remain application-owned.\n */\nexport const LumenNavigationBar = ({\n accessibilityLabel = 'Primary navigation',\n items,\n onReselect,\n onValueChange,\n ref,\n style,\n value,\n ...props\n}: LumenNavigationBarProps): ReactElement => {\n const theme = useLumenTheme()\n\n return (\n <View\n ref={ref}\n {...props}\n accessibilityLabel={accessibilityLabel}\n accessibilityRole=\"tablist\"\n style={[\n {\n backgroundColor: theme.colors.surface,\n borderColor: theme.colors.line,\n borderTopWidth: 1,\n flexDirection: 'row',\n minHeight: 64,\n paddingHorizontal: theme.spacing.sm\n },\n style\n ]}\n >\n {items.map(item => (\n <LumenNavigationDestination\n item={item}\n key={item.value}\n onReselect={onReselect}\n onValueChange={onValueChange}\n value={value}\n />\n ))}\n </View>\n )\n}\n\nexport interface LumenNavigationAccessoryProps extends Omit<ViewProps, 'children'> {\n children: ReactNode\n}\n\n/** A compact token-aware status or action surface placed above bottom navigation. */\nexport const LumenNavigationAccessory = ({\n children,\n style,\n ...props\n}: LumenNavigationAccessoryProps): ReactElement => {\n const theme = useLumenTheme()\n\n return (\n <View\n {...props}\n style={[\n {\n alignItems: 'center',\n backgroundColor: theme.colors.surface,\n borderColor: theme.colors.line,\n borderTopWidth: 1,\n flexDirection: 'row',\n height: 48,\n paddingHorizontal: theme.spacing.lg,\n paddingVertical: theme.spacing.sm\n },\n style\n ]}\n >\n {children}\n </View>\n )\n}\n\nexport interface LumenCollapsibleNavigationBarProps extends LumenNavigationBarProps {\n accessory?: ReactNode\n containerStyle?: StyleProp<ViewStyle>\n visible: boolean\n}\n\n/** An animated, layout-collapsing wrapper around LumenNavigationBar. */\nexport const LumenCollapsibleNavigationBar = ({\n accessory,\n containerStyle,\n visible,\n ...props\n}: LumenCollapsibleNavigationBarProps): ReactElement => {\n const theme = useLumenTheme()\n const [progress] = useState(() => new Animated.Value(visible ? 1 : 0))\n\n useEffect(() => {\n const animation = Animated.timing(progress, {\n duration: theme.durations.standard,\n easing: Easing.bezier(0.32, 0.72, 0, 1),\n toValue: visible ? 1 : 0,\n useNativeDriver: false\n })\n\n animation.start()\n\n return () => {\n animation.stop()\n }\n }, [progress, theme.durations.standard, visible])\n\n return (\n <Animated.View\n accessibilityElementsHidden={!visible}\n importantForAccessibility={visible ? 'auto' : 'no-hide-descendants'}\n pointerEvents={visible ? 'auto' : 'none'}\n style={[\n containerStyle,\n {\n height: progress.interpolate({\n inputRange: [0, 1],\n outputRange: [0, accessory ? 112 : 64]\n }),\n opacity: progress,\n overflow: 'hidden'\n }\n ]}\n >\n {accessory ?\n <LumenNavigationAccessory>{accessory}</LumenNavigationAccessory> :\n null}\n <LumenNavigationBar {...props} />\n </Animated.View>\n )\n}\n\nexport interface LumenRefreshControlProps extends Omit<\n RefreshControlProps,\n | 'accessibilityLabel' |\n 'colors' |\n 'onRefresh' |\n 'progressBackgroundColor' |\n 'tintColor' |\n 'titleColor'\n> {\n accessibilityLabel: string\n indicatorTone?: LumenRefreshIndicatorTone\n onRefresh: NonNullable<RefreshControlProps['onRefresh']>\n ref?: Ref<ComponentRef<typeof RefreshControl>>\n}\n\n/**\n * A token-aware native pull-to-refresh indicator for React Native scroll containers.\n */\nexport const LumenRefreshControl = ({\n indicatorTone = 'brand',\n ref,\n ...props\n}: LumenRefreshControlProps): ReactElement => {\n const theme = useLumenTheme()\n const colors = resolveLumenRefreshColors(theme.colors, indicatorTone)\n\n return (\n <RefreshControl\n ref={ref}\n {...props}\n colors={[colors.indicator]}\n progressBackgroundColor={colors.surface}\n tintColor={colors.indicator}\n titleColor={colors.title}\n />\n )\n}\n",
39626
+ "packages/react-native/src/platform-hooks.ts": "import { useCallback, useRef, useState } from 'react'\nimport type { NativeScrollEvent, NativeSyntheticEvent } from 'react-native'\n\nimport {\n createLumenNavigationVisibilityState,\n resolveLumenNavigationVisibility\n} from './platform-recipes.js'\n\nexport interface LumenNavigationBarVisibilityOptions {\n initiallyVisible?: boolean\n threshold?: number\n}\n\nexport interface LumenNavigationBarVisibilityController {\n hide: () => void\n onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void\n show: () => void\n visible: boolean\n}\n\n/**\n * Produces scroll-driven visibility for bottom navigation without owning the application's list.\n * Pass `onScroll` to a native scroll container and `visible` to LumenCollapsibleNavigationBar.\n */\nexport const useLumenNavigationBarVisibility = ({\n initiallyVisible = true,\n threshold = 16\n}: LumenNavigationBarVisibilityOptions = {}): LumenNavigationBarVisibilityController => {\n const navigationStateRef = useRef(\n createLumenNavigationVisibilityState(initiallyVisible)\n )\n\n const [visible, setVisible] = useState(initiallyVisible)\n\n const updateVisibility = useCallback((nextVisible: boolean) => {\n navigationStateRef.current = {\n ...navigationStateRef.current,\n accumulatedDelta: 0,\n visible: nextVisible\n }\n\n setVisible(nextVisible)\n }, [])\n\n const onScroll = useCallback(\n (event: NativeSyntheticEvent<NativeScrollEvent>) => {\n const nextState = resolveLumenNavigationVisibility(\n navigationStateRef.current,\n event.nativeEvent.contentOffset.y,\n threshold\n )\n\n navigationStateRef.current = nextState\n\n setVisible(current => current === nextState.visible ? current : nextState.visible)\n },\n [threshold]\n )\n\n const hide = useCallback(() => {\n updateVisibility(false)\n }, [updateVisibility])\n\n const show = useCallback(() => {\n updateVisibility(true)\n }, [updateVisibility])\n\n return { hide, onScroll, show, visible }\n}\n",
39627
+ "packages/react-native/src/platform-recipes.test.ts": "import { describe, expect, test } from 'vitest'\n\nimport {\n createLumenNavigationVisibilityState,\n resolveLumenNavigationAction,\n resolveLumenNavigationBadge,\n resolveLumenNavigationItemState,\n resolveLumenNavigationVisibility,\n resolveLumenRefreshColors\n} from './platform-recipes.js'\nimport { lumenDarkTheme, lumenLightTheme } from './theme.js'\n\ndescribe('React Native platform recipes', () => {\n test('maps refresh indicators to semantic theme colors', () => {\n expect(resolveLumenRefreshColors(lumenLightTheme.colors, 'brand')).toEqual({\n indicator: lumenLightTheme.colors.brand,\n surface: lumenLightTheme.colors.surface,\n title: lumenLightTheme.colors.inkMuted\n })\n expect(\n resolveLumenRefreshColors(lumenDarkTheme.colors, 'accent').indicator\n ).toBe(lumenDarkTheme.colors.accent)\n expect(\n resolveLumenRefreshColors(lumenDarkTheme.colors, 'neutral').indicator\n ).toBe(lumenDarkTheme.colors.inkSoft)\n })\n\n test('resolves selected and disabled destination states', () => {\n expect(\n resolveLumenNavigationItemState(\n lumenLightTheme.colors,\n 'home',\n 'home',\n false,\n false\n )\n ).toEqual({\n color: lumenLightTheme.colors.brand,\n opacity: 1,\n selected: true\n })\n expect(\n resolveLumenNavigationItemState(\n lumenDarkTheme.colors,\n 'search',\n 'home',\n true,\n true\n )\n ).toEqual({\n color: lumenDarkTheme.colors.inkMuted,\n opacity: 0.52,\n selected: false\n })\n })\n\n test('resolves dot, text, and capped count navigation badges', () => {\n expect(resolveLumenNavigationBadge(true)).toEqual({\n accessibilityLabel: 'New activity'\n })\n expect(resolveLumenNavigationBadge('Live')).toEqual({\n accessibilityLabel: 'Live',\n displayValue: 'Live'\n })\n expect(resolveLumenNavigationBadge(128)).toEqual({\n accessibilityLabel: '128 new items',\n displayValue: '99+'\n })\n expect(resolveLumenNavigationBadge(Number.NaN)).toBeUndefined()\n })\n\n test('distinguishes destination selection from re-selection', () => {\n expect(resolveLumenNavigationAction('home', 'search', true)).toBe('select')\n expect(resolveLumenNavigationAction('home', 'home', false)).toBe('select')\n expect(resolveLumenNavigationAction('home', 'home', true)).toBe('reselect')\n })\n\n test('hides and reveals navigation after deliberate directional travel', () => {\n let state = createLumenNavigationVisibilityState()\n\n state = resolveLumenNavigationVisibility(state, 8)\n expect(state.visible).toBe(true)\n state = resolveLumenNavigationVisibility(state, 16)\n expect(state.visible).toBe(false)\n\n state = resolveLumenNavigationVisibility(state, 12)\n state = resolveLumenNavigationVisibility(state, 17)\n state = resolveLumenNavigationVisibility(state, 1)\n expect(state.visible).toBe(true)\n })\n\n test('reveals navigation at the top and ignores invalid offsets', () => {\n const hidden = createLumenNavigationVisibilityState(false)\n const invalid = resolveLumenNavigationVisibility(hidden, Number.NaN)\n\n expect(invalid).toBe(hidden)\n expect(resolveLumenNavigationVisibility(hidden, -12)).toEqual({\n accumulatedDelta: 0,\n offset: 0,\n visible: true\n })\n })\n})\n",
39628
+ "packages/react-native/src/platform-recipes.ts": "import type { LumenTheme } from './theme.js'\n\nexport type LumenRefreshIndicatorTone = 'accent' | 'brand' | 'neutral'\nexport type LumenNavigationBadge = boolean | number | string\n\nexport interface LumenResolvedNavigationBadge {\n accessibilityLabel: string\n displayValue?: string\n}\n\nexport type LumenNavigationAction = 'reselect' | 'select'\n\nexport const resolveLumenNavigationAction = (\n itemValue: string,\n selectedValue: string,\n hasReselectHandler: boolean\n): LumenNavigationAction => itemValue === selectedValue && hasReselectHandler ? 'reselect' : 'select'\n\nexport const resolveLumenNavigationBadge = (\n badge: LumenNavigationBadge | undefined\n): LumenResolvedNavigationBadge | undefined => {\n if (badge === undefined || badge === false) return undefined\n\n if (badge === true) return { accessibilityLabel: 'New activity' }\n\n if (typeof badge === 'number') {\n if (!Number.isFinite(badge)) return undefined\n\n const count = Math.max(0, Math.trunc(badge))\n\n return {\n accessibilityLabel: `${count} new items`,\n displayValue: count > 99 ? '99+' : String(count)\n }\n }\n\n const displayValue = badge.trim()\n\n if (displayValue.length === 0) return undefined\n\n return { accessibilityLabel: displayValue, displayValue }\n}\n\nexport interface LumenNavigationItemState {\n color: string\n opacity: number\n selected: boolean\n}\n\nexport interface LumenNavigationVisibilityState {\n accumulatedDelta: number\n offset: number\n visible: boolean\n}\n\nexport const createLumenNavigationVisibilityState = (\n visible = true\n): LumenNavigationVisibilityState => ({\n accumulatedDelta: 0,\n offset: 0,\n visible\n})\n\nconst resolveScrollThreshold = (threshold: number): number => {\n if (!Number.isFinite(threshold) || threshold <= 0) return 16\n\n return threshold\n}\n\nconst accumulateScrollDelta = (\n accumulatedDelta: number,\n delta: number\n): number => {\n const continuingDirection = Math.sign(accumulatedDelta) === Math.sign(delta)\n\n return (continuingDirection ? accumulatedDelta : 0) + delta\n}\n\n/**\n * Resolves navigation visibility from a native vertical scroll offset.\n * Positive travel hides the bar, reverse travel reveals it, and reaching the top always reveals it.\n */\nexport const resolveLumenNavigationVisibility = (\n state: LumenNavigationVisibilityState,\n nextOffset: number,\n threshold = 16\n): LumenNavigationVisibilityState => {\n if (!Number.isFinite(nextOffset)) return state\n\n const safeOffset = Math.max(0, nextOffset)\n\n if (safeOffset === 0) return createLumenNavigationVisibilityState(true)\n\n const safeThreshold = resolveScrollThreshold(threshold)\n const delta = safeOffset - state.offset\n\n if (delta === 0) return { ...state, offset: safeOffset }\n\n const accumulatedDelta = accumulateScrollDelta(state.accumulatedDelta, delta)\n\n if (accumulatedDelta >= safeThreshold) {\n return { accumulatedDelta: 0, offset: safeOffset, visible: false }\n }\n\n if (accumulatedDelta <= -safeThreshold) {\n return { accumulatedDelta: 0, offset: safeOffset, visible: true }\n }\n\n return { accumulatedDelta, offset: safeOffset, visible: state.visible }\n}\n\nexport const resolveLumenNavigationItemState = (\n colors: LumenTheme['colors'],\n itemValue: string,\n selectedValue: string,\n disabled: boolean,\n pressed: boolean\n): LumenNavigationItemState => {\n const selected = itemValue === selectedValue\n let opacity = 1\n\n if (disabled) opacity = 0.52\n else if (pressed) opacity = 0.72\n\n return {\n color: selected ? colors.brand : colors.inkMuted,\n opacity,\n selected\n }\n}\n\nexport interface LumenRefreshColors {\n indicator: string\n surface: string\n title: string\n}\n\nconst resolveIndicatorColor = (\n colors: LumenTheme['colors'],\n tone: LumenRefreshIndicatorTone\n): string => {\n if (tone === 'accent') return colors.accent\n\n if (tone === 'neutral') return colors.inkSoft\n\n return colors.brand\n}\n\nexport const resolveLumenRefreshColors = (\n colors: LumenTheme['colors'],\n tone: LumenRefreshIndicatorTone\n): LumenRefreshColors => ({\n indicator: resolveIndicatorColor(colors, tone),\n surface: colors.surface,\n title: colors.inkMuted\n})\n",
38770
39629
  "packages/react-native/src/primitives.tsx": "import {\n type ComponentType,\n type ReactElement,\n type ReactNode,\n type Ref\n} from 'react'\nimport {\n ActivityIndicator,\n type ActivityIndicatorProps,\n type ColorValue,\n type HostInstance,\n Pressable,\n type PressableProps,\n Text,\n TextInput,\n type TextInputInstance,\n type TextInputProps,\n type TextProps,\n type TextStyle,\n View,\n type ViewProps\n} from 'react-native'\n\nimport {\n resolveLumenButtonColors,\n resolveLumenButtonOpacity,\n resolveLumenButtonSize,\n resolveLumenControlMinHeight,\n resolveLumenIconButtonSize,\n resolveLumenIconSize,\n resolveLumenPressableStyle,\n resolveLumenSurfaceColor,\n resolveLumenTextColor,\n resolveLumenTextStyle\n} from './recipes.js'\nimport { useLumenTheme } from './theme-context.js'\n\nexport type LumenTextTone =\n | 'danger' |\n 'default' |\n 'muted' |\n 'soft' |\n 'success' |\n 'warning'\nexport type LumenTextVariant = 'body' | 'caption' | 'label' | 'title'\n\nexport interface LumenTextProps extends TextProps {\n ref?: Ref<HostInstance>\n tone?: LumenTextTone\n variant?: LumenTextVariant\n}\n\nexport const LumenText = ({\n ref,\n style,\n tone = 'default',\n variant = 'body',\n ...props\n}: LumenTextProps): ReactElement => {\n const theme = useLumenTheme()\n\n return (\n <Text\n ref={ref}\n {...props}\n style={[\n { color: resolveLumenTextColor(theme.colors, tone) },\n resolveLumenTextStyle(theme, variant),\n style\n ]}\n />\n )\n}\n\nexport type LumenSurfacePadding = 'lg' | 'md' | 'none' | 'sm'\nexport type LumenSurfaceRadius = 'lg' | 'md' | 'none' | 'sm'\nexport type LumenSurfaceTone = 'canvas' | 'muted' | 'strong' | 'surface'\n\nexport interface LumenSurfaceProps extends ViewProps {\n padding?: LumenSurfacePadding\n radius?: LumenSurfaceRadius\n ref?: Ref<HostInstance>\n tone?: LumenSurfaceTone\n}\n\nexport const LumenSurface = ({\n padding = 'md',\n radius = 'md',\n ref,\n style,\n tone = 'surface',\n ...props\n}: LumenSurfaceProps): ReactElement => {\n const theme = useLumenTheme()\n const backgroundColor = resolveLumenSurfaceColor(theme, tone)\n const paddingValue = padding === 'none' ? 0 : theme.spacing[padding]\n const radiusValue = radius === 'none' ? 0 : theme.radii[radius]\n\n return (\n <View\n ref={ref}\n {...props}\n style={[{ backgroundColor, borderRadius: radiusValue, padding: paddingValue }, style]}\n />\n )\n}\n\nexport type LumenButtonIntent = 'danger' | 'primary' | 'quiet' | 'secondary'\nexport type LumenControlSize = 'lg' | 'md' | 'sm'\nexport type LumenIconSize = 'lg' | 'md' | 'sm'\n\nexport interface LumenIconGraphicProps {\n color?: ColorValue\n size?: number\n strokeWidth?: number\n}\n\nexport type LumenIconGraphic = ComponentType<LumenIconGraphicProps>\n\nexport interface LumenIconProps extends Omit<ViewProps, 'children'> {\n color?: ColorValue\n decorative?: boolean\n icon: LumenIconGraphic\n label?: string\n ref?: Ref<HostInstance>\n size?: LumenIconSize\n strokeWidth?: number\n}\n\nexport const LumenIcon = ({\n color,\n decorative,\n icon: Graphic,\n label,\n ref,\n size = 'md',\n strokeWidth = 2,\n style,\n ...props\n}: LumenIconProps): ReactElement => {\n const theme = useLumenTheme()\n const dimension = resolveLumenIconSize(size)\n const isDecorative = decorative ?? !label\n\n return (\n <View\n ref={ref}\n {...props}\n accessible={!isDecorative}\n accessibilityElementsHidden={isDecorative}\n accessibilityLabel={isDecorative ? undefined : label}\n accessibilityRole={isDecorative ? undefined : 'image'}\n importantForAccessibility={isDecorative ? 'no' : 'yes'}\n style={[\n {\n alignItems: 'center',\n height: dimension,\n justifyContent: 'center',\n width: dimension\n },\n style\n ]}\n >\n <Graphic\n color={color ?? theme.colors.ink}\n size={dimension}\n strokeWidth={strokeWidth}\n />\n </View>\n )\n}\n\nexport interface LumenIconButtonProps extends Omit<PressableProps, 'children'> {\n icon: LumenIconGraphic\n intent?: LumenButtonIntent\n label: string\n ref?: Ref<HostInstance>\n size?: LumenControlSize\n strokeWidth?: number\n}\n\nexport const LumenIconButton = ({\n accessibilityState,\n disabled = false,\n icon,\n intent = 'quiet',\n label,\n ref,\n size = 'md',\n strokeWidth = 2,\n style,\n ...props\n}: LumenIconButtonProps): ReactElement => {\n const theme = useLumenTheme()\n const colors = resolveLumenButtonColors(theme.colors, intent)\n const metrics = resolveLumenIconButtonSize(size)\n\n return (\n <Pressable\n ref={ref}\n {...props}\n accessibilityLabel={label}\n accessibilityRole=\"button\"\n accessibilityState={{ ...accessibilityState, disabled }}\n disabled={disabled}\n style={state => [\n {\n alignItems: 'center',\n backgroundColor: colors.backgroundColor,\n borderColor: colors.borderColor,\n borderRadius: theme.radii.sm,\n borderWidth: 1,\n height: metrics.touchTarget,\n justifyContent: 'center',\n opacity: resolveLumenButtonOpacity(disabled, state.pressed),\n width: metrics.touchTarget\n },\n resolveLumenPressableStyle(style, state)\n ]}\n >\n <LumenIcon\n color={colors.color}\n decorative\n icon={icon}\n size={metrics.iconSize}\n strokeWidth={strokeWidth}\n />\n </Pressable>\n )\n}\n\nexport interface LumenButtonProps extends Omit<PressableProps, 'children'> {\n children: ReactNode\n intent?: LumenButtonIntent\n loading?: boolean\n ref?: Ref<HostInstance>\n size?: LumenControlSize\n}\n\nexport const LumenButton = ({\n accessibilityState,\n children,\n disabled = false,\n intent = 'primary',\n loading = false,\n ref,\n size = 'md',\n style,\n ...props\n}: LumenButtonProps): ReactElement => {\n const theme = useLumenTheme()\n const colors = resolveLumenButtonColors(theme.colors, intent)\n const dimensions = resolveLumenButtonSize(theme, size)\n const isDisabled = disabled || loading\n\n return (\n <Pressable\n ref={ref}\n accessibilityRole=\"button\"\n {...props}\n accessibilityState={{\n ...accessibilityState,\n busy: loading,\n disabled: isDisabled\n }}\n disabled={isDisabled}\n style={state => [\n {\n alignItems: 'center',\n backgroundColor: colors.backgroundColor,\n borderColor: colors.borderColor,\n borderRadius: theme.radii.sm,\n borderWidth: 1,\n flexDirection: 'row',\n gap: theme.spacing.sm,\n justifyContent: 'center',\n opacity: resolveLumenButtonOpacity(isDisabled, state.pressed)\n },\n dimensions.container,\n resolveLumenPressableStyle(style, state)\n ]}\n >\n {loading ? <ActivityIndicator color={colors.color} size=\"small\" /> : null}\n <Text\n style={[\n {\n color: colors.color,\n fontWeight: String(theme.fontWeights.semibold) as TextStyle['fontWeight']\n },\n dimensions.label\n ]}\n >\n {children}\n </Text>\n </Pressable>\n )\n}\n\nexport interface LumenTextFieldProps extends TextInputProps {\n error?: boolean\n ref?: Ref<TextInputInstance>\n size?: LumenControlSize\n}\n\nexport const LumenTextField = ({\n editable = true,\n error = false,\n placeholderTextColor,\n ref,\n size = 'md',\n style,\n ...props\n}: LumenTextFieldProps): ReactElement => {\n const theme = useLumenTheme()\n const minHeight = resolveLumenControlMinHeight(size)\n const fontSize = size === 'lg' ? theme.fontSizes.md : theme.fontSizes.sm\n\n return (\n <TextInput\n ref={ref}\n {...props}\n aria-invalid={error || undefined}\n editable={editable}\n placeholderTextColor={placeholderTextColor ?? theme.colors.inkMuted}\n style={[\n {\n backgroundColor: theme.colors.surface,\n borderColor: error ? theme.colors.danger : theme.colors.line,\n borderRadius: theme.radii.sm,\n borderWidth: 1,\n color: theme.colors.ink,\n fontSize,\n minHeight,\n opacity: editable ? 1 : 0.52,\n paddingHorizontal: theme.spacing.md,\n paddingVertical: theme.spacing.sm\n },\n style\n ]}\n />\n )\n}\n\nexport interface LumenBadgeProps extends ViewProps {\n children: ReactNode\n ref?: Ref<HostInstance>\n tone?: 'accent' | 'danger' | 'neutral' | 'success' | 'warning'\n}\n\nexport const LumenBadge = ({\n children,\n ref,\n style,\n tone = 'neutral',\n ...props\n}: LumenBadgeProps): ReactElement => {\n const theme = useLumenTheme()\n const color = tone === 'neutral' ? theme.colors.inkSoft : theme.colors[tone]\n const backgroundColor = tone === 'neutral' ? theme.colors.surfaceMuted : `${color}1F`\n\n return (\n <View\n ref={ref}\n {...props}\n style={[\n {\n alignSelf: 'flex-start',\n backgroundColor,\n borderRadius: theme.radii.full,\n paddingHorizontal: theme.spacing.sm,\n paddingVertical: theme.spacing.xs\n },\n style\n ]}\n >\n <Text style={{ color, fontSize: theme.fontSizes.xs, fontWeight: '600' }}>\n {children}\n </Text>\n </View>\n )\n}\n\nexport interface LumenDividerProps extends ViewProps {\n ref?: Ref<HostInstance>\n}\n\nexport const LumenDivider = ({\n ref,\n style,\n ...props\n}: LumenDividerProps): ReactElement => {\n const theme = useLumenTheme()\n\n return (\n <View\n ref={ref}\n accessibilityElementsHidden\n importantForAccessibility=\"no\"\n {...props}\n style={[{ backgroundColor: theme.colors.line, height: 1 }, style]}\n />\n )\n}\n\nexport interface LumenSpinnerProps extends ActivityIndicatorProps {\n ref?: Ref<HostInstance>\n}\n\nexport const LumenSpinner = ({\n color,\n ref,\n ...props\n}: LumenSpinnerProps): ReactElement => {\n const theme = useLumenTheme()\n\n return (\n <ActivityIndicator\n ref={ref}\n accessibilityRole=\"progressbar\"\n color={color ?? theme.colors.brand}\n {...props}\n />\n )\n}\n",
38771
39630
  "packages/react-native/src/provider.tsx": "import {\n type ReactNode,\n useMemo } from 'react'\nimport { useColorScheme } from 'react-native'\n\nimport {\n createLumenTheme,\n type LumenTheme\n} from './theme.js'\nimport { LumenThemeContext } from './theme-context.js'\nimport type { LumenColorScheme } from './tokens.generated.js'\n\nexport interface LumenProviderProps {\n children: ReactNode\n scheme?: LumenColorScheme | 'system'\n theme?: LumenTheme\n}\n\nexport const LumenProvider = ({\n children,\n scheme = 'system',\n theme\n}: LumenProviderProps) => {\n const systemScheme = useColorScheme()\n const resolvedScheme = scheme === 'system' ? systemScheme ?? 'light' : scheme\n\n const value = useMemo(\n () => theme ?? createLumenTheme(resolvedScheme),\n [resolvedScheme, theme]\n )\n\n return (\n <LumenThemeContext value={value}>\n {children}\n </LumenThemeContext>\n )\n}\n",
38772
39631
  "packages/react-native/src/recipes.test.ts": "import { describe, expect, test } from 'vitest'\n\nimport {\n resolveLumenButtonColors,\n resolveLumenButtonOpacity,\n resolveLumenButtonSize,\n resolveLumenControlMinHeight,\n resolveLumenIconButtonSize,\n resolveLumenIconSize,\n resolveLumenSurfaceColor,\n resolveLumenTextColor\n} from './recipes.js'\nimport {\n lumenDarkTheme,\n lumenLightTheme\n} from './theme.js'\n\ndescribe('Lumen React Native primitive recipes', () => {\n test('maps shared button intents to semantic theme roles', () => {\n expect(resolveLumenButtonColors(lumenLightTheme.colors, 'primary')).toEqual({\n backgroundColor: lumenLightTheme.colors.brandSolid,\n borderColor: lumenLightTheme.colors.brandSolid,\n color: lumenLightTheme.colors.onBrand\n })\n expect(resolveLumenButtonColors(lumenDarkTheme.colors, 'danger').color).toBe(\n lumenDarkTheme.colors.onDanger\n )\n })\n\n test('keeps touch targets and disabled feedback predictable', () => {\n expect(resolveLumenButtonSize(lumenLightTheme, 'md').container.minHeight).toBe(44)\n expect(resolveLumenControlMinHeight('lg')).toBe(52)\n expect(resolveLumenButtonOpacity(true, false)).toBe(0.52)\n expect(resolveLumenButtonOpacity(false, true)).toBe(0.84)\n expect(resolveLumenIconSize('sm')).toBe(16)\n expect(resolveLumenIconSize('md')).toBe(20)\n expect(resolveLumenIconSize('lg')).toBe(24)\n expect(resolveLumenIconButtonSize('sm')).toEqual({ iconSize: 'sm', touchTarget: 44 })\n expect(resolveLumenIconButtonSize('lg')).toEqual({ iconSize: 'lg', touchTarget: 52 })\n })\n\n test('maps content and surfaces without platform-specific color literals', () => {\n expect(resolveLumenTextColor(lumenDarkTheme.colors, 'muted')).toBe(\n lumenDarkTheme.colors.inkMuted\n )\n expect(resolveLumenSurfaceColor(lumenDarkTheme, 'strong')).toBe(\n lumenDarkTheme.colors.surfaceStrong\n )\n })\n})\n",
@@ -38785,20 +39644,21 @@
38785
39644
  "packages/react-native/src/tokens.generated.ts": "/* This file is generated by scripts/generate-platform-tokens.mjs. */\n\nexport const lumenColorTokens = {\n light: {\n canvas: \"#FFFFFF\",\n surface: \"#FFFFFF\",\n surfaceMuted: \"#F3F4F6\",\n surfaceStrong: \"#E5E7EB\",\n line: \"#D7DAE0\",\n ink: \"#0F1729\",\n inkSoft: \"#454F5F\",\n inkMuted: \"#6C7889\",\n brand: \"#2463EB\",\n brandSolid: \"#1452D7\",\n brandSoft: \"#F0F5FF\",\n onBrand: \"#FFFFFF\",\n accent: \"#16A286\",\n success: \"#1B9D4A\",\n warning: \"#F59F0A\",\n danger: \"#EF4343\",\n onDanger: \"#000000\"\n },\n dark: {\n canvas: \"#101219\",\n surface: \"#171A21\",\n surfaceMuted: \"#20242C\",\n surfaceStrong: \"#303540\",\n line: \"#393F4C\",\n ink: \"#F3F5F7\",\n inkSoft: \"#BCC3CD\",\n inkMuted: \"#949CA8\",\n brand: \"#29B4F5\",\n brandSolid: \"#087AAF\",\n brandSoft: \"#07354B\",\n onBrand: \"#FFFFFF\",\n accent: \"#1AC19A\",\n success: \"#24C25E\",\n warning: \"#F6A823\",\n danger: \"#F15B5B\",\n onDanger: \"#101219\"\n }\n} as const\n\nexport const lumenCssColorTokens = {\n light: {\n canvas: \"0 0% 100%\",\n surface: \"0 0% 100%\",\n surfaceMuted: \"220 14% 96%\",\n surfaceStrong: \"220 13% 91%\",\n line: \"220 13% 86%\",\n ink: \"222 47% 11%\",\n inkSoft: \"215 16% 32%\",\n inkMuted: \"215 12% 48%\",\n brand: \"221 83% 53%\",\n brandSolid: \"221 83% 46%\",\n brandSoft: \"218 100% 97%\",\n onBrand: \"0 0% 100%\",\n accent: \"168 76% 36%\",\n success: \"142 71% 36%\",\n warning: \"38 92% 50%\",\n danger: \"0 84% 60%\",\n onDanger: \"0 0% 0%\"\n },\n dark: {\n canvas: \"224 24% 8%\",\n surface: \"224 18% 11%\",\n surfaceMuted: \"223 16% 15%\",\n surfaceStrong: \"222 14% 22%\",\n line: \"222 14% 26%\",\n ink: \"210 20% 96%\",\n inkSoft: \"214 14% 77%\",\n inkMuted: \"216 10% 62%\",\n brand: \"199 91% 56%\",\n brandSolid: \"199 91% 36%\",\n brandSoft: \"199 84% 16%\",\n onBrand: \"0 0% 100%\",\n accent: \"166 76% 43%\",\n success: \"142 69% 45%\",\n warning: \"38 92% 55%\",\n danger: \"0 84% 65%\",\n onDanger: \"224 24% 8%\"\n }\n} as const\n\nexport const lumenSpacing = {\n zero: 0,\n xs: 4,\n sm: 8,\n md: 12,\n lg: 16,\n xl: 24,\n \"2xl\": 32,\n \"3xl\": 48\n} as const\n\nexport const lumenRadii = {\n sm: 6,\n md: 10,\n lg: 12,\n full: 9999\n} as const\n\nexport const lumenFontFamilies = {\n sans: [\"Montserrat\",\"Avenir Next\",\"Segoe UI\",\"sans-serif\"]\n} as const\n\nexport const lumenFontSizes = {\n xs: 12,\n sm: 14,\n md: 16,\n lg: 18,\n xl: 20,\n \"2xl\": 24\n} as const\n\nexport const lumenFontWeights = {\n regular: 400,\n medium: 500,\n semibold: 600,\n bold: 700\n} as const\n\nexport const lumenDurations = {\n fast: 120,\n standard: 160,\n slow: 300\n} as const\n\nexport const lumenEasings = {\n standard: [0.32,0.72,0,1],\n emphasized: [0.22,1,0.36,1]\n} as const\n\nexport const lumenElevation = {\n resting: 1,\n raised: 3,\n overlay: 6\n} as const\n\nexport type LumenColorScheme = keyof typeof lumenColorTokens\nexport type LumenSemanticColor = keyof typeof lumenColorTokens.light\n"
38786
39645
  },
38787
39646
  "swiftui": {
38788
- "packages/swift/Sources/LumenUI/AdditionalComponents.swift": "import SwiftUI\n\npublic enum LumenButtonGroupOrientation: Sendable {\n case horizontal\n case vertical\n}\n\npublic struct LumenButtonGroup<Content: View>: View {\n private let content: Content\n private let orientation: LumenButtonGroupOrientation\n\n public init(\n orientation: LumenButtonGroupOrientation = .horizontal,\n @ViewBuilder content: () -> Content\n ) {\n self.orientation = orientation\n self.content = content()\n }\n\n public var body: some View {\n Group {\n if orientation == .horizontal {\n HStack(spacing: LumenSpacing.sm) { content }\n } else {\n VStack(alignment: .leading, spacing: LumenSpacing.sm) { content }\n }\n }\n .accessibilityElement(children: .contain)\n }\n}\n\npublic struct LumenFieldGroup<Content: View>: View {\n @Environment(\\.lumenTheme) private var theme\n\n private let content: Content\n private let description: LocalizedStringKey?\n private let errorMessage: LocalizedStringKey?\n private let required: Bool\n private let title: LocalizedStringKey\n\n public init(\n _ title: LocalizedStringKey,\n description: LocalizedStringKey? = nil,\n errorMessage: LocalizedStringKey? = nil,\n required: Bool = false,\n @ViewBuilder content: () -> Content\n ) {\n self.title = title\n self.description = description\n self.errorMessage = errorMessage\n self.required = required\n self.content = content()\n }\n\n public var body: some View {\n VStack(alignment: .leading, spacing: LumenSpacing.sm) {\n HStack(spacing: LumenSpacing.xs) {\n Text(title)\n .font(.callout.weight(.semibold))\n .foregroundStyle(theme.colors.ink)\n if required {\n Text(\"*\").foregroundStyle(theme.colors.danger).accessibilityLabel(\"Required\")\n }\n }\n if let description {\n Text(description)\n .font(.caption)\n .foregroundStyle(theme.colors.inkMuted)\n }\n content\n if let errorMessage {\n Text(errorMessage)\n .font(.caption)\n .foregroundStyle(theme.colors.danger)\n }\n }\n .accessibilityElement(children: .contain)\n }\n}\n\npublic struct LumenTextarea: View {\n @Binding private var text: String\n @Environment(\\.isEnabled) private var isEnabled\n @Environment(\\.lumenTheme) private var theme\n\n private let description: LocalizedStringKey?\n private let errorMessage: LocalizedStringKey?\n private let minHeight: CGFloat\n private let title: LocalizedStringKey\n\n public init(\n _ title: LocalizedStringKey,\n text: Binding<String>,\n description: LocalizedStringKey? = nil,\n errorMessage: LocalizedStringKey? = nil,\n minHeight: CGFloat = 112\n ) {\n self.title = title\n _text = text\n self.description = description\n self.errorMessage = errorMessage\n self.minHeight = minHeight\n }\n\n public var body: some View {\n VStack(alignment: .leading, spacing: LumenSpacing.xs) {\n Text(title)\n .font(.callout.weight(.semibold))\n .foregroundStyle(theme.colors.ink)\n TextEditor(text: $text)\n .scrollContentBackground(.hidden)\n .font(.callout)\n .foregroundStyle(theme.colors.ink)\n .frame(minHeight: minHeight)\n .padding(LumenSpacing.sm)\n .background(theme.colors.surface)\n .overlay {\n RoundedRectangle(cornerRadius: LumenRadius.sm, style: .continuous)\n .stroke(errorMessage == nil ? theme.colors.line : theme.colors.danger, lineWidth: 1)\n }\n .clipShape(RoundedRectangle(cornerRadius: LumenRadius.sm, style: .continuous))\n .opacity(isEnabled ? 1 : 0.52)\n .accessibilityLabel(title)\n .accessibilityValue(errorMessage ?? LocalizedStringKey(text))\n if let errorMessage {\n Text(errorMessage).font(.caption).foregroundStyle(theme.colors.danger)\n } else if let description {\n Text(description).font(.caption).foregroundStyle(theme.colors.inkMuted)\n }\n }\n }\n}\n\npublic struct LumenChip: View {\n @Environment(\\.isEnabled) private var isEnabled\n @Environment(\\.lumenTheme) private var theme\n\n private let label: LocalizedStringKey\n private let onPress: (() -> Void)?\n private let onRemove: (() -> Void)?\n private let removeLabel: LocalizedStringKey\n private let selected: Bool\n\n public init(\n _ label: LocalizedStringKey,\n selected: Bool = false,\n removeLabel: LocalizedStringKey = \"Remove\",\n onPress: (() -> Void)? = nil,\n onRemove: (() -> Void)? = nil\n ) {\n self.label = label\n self.selected = selected\n self.removeLabel = removeLabel\n self.onPress = onPress\n self.onRemove = onRemove\n }\n\n public var body: some View {\n HStack(spacing: LumenSpacing.xs) {\n if let onPress {\n Button(action: onPress) {\n Text(label).font(.caption.weight(.semibold))\n }\n .buttonStyle(.plain)\n .accessibilityAddTraits(selected ? .isSelected : [])\n } else {\n Text(label).font(.caption.weight(.semibold))\n }\n if let onRemove {\n Button(action: onRemove) {\n Image(systemName: \"xmark\").font(.caption2.weight(.bold))\n }\n .buttonStyle(.plain)\n .accessibilityLabel(removeLabel)\n }\n }\n .foregroundStyle(selected ? theme.colors.brand : theme.colors.inkSoft)\n .padding(.horizontal, LumenSpacing.md)\n .frame(minHeight: 32)\n .background(selected ? theme.colors.brandSoft : theme.colors.surfaceMuted)\n .overlay {\n Capsule().stroke(selected ? theme.colors.brand : theme.colors.line, lineWidth: 1)\n }\n .clipShape(Capsule())\n .opacity(isEnabled ? 1 : 0.52)\n .accessibilityElement(children: .contain)\n }\n}\n\npublic struct LumenToast<Actions: View>: View {\n @Environment(\\.lumenTheme) private var theme\n\n private let actions: Actions\n private let description: LocalizedStringKey?\n private let onDismiss: (() -> Void)?\n private let title: LocalizedStringKey\n private let variant: LumenBannerVariant\n\n public init(\n _ title: LocalizedStringKey,\n description: LocalizedStringKey? = nil,\n variant: LumenBannerVariant = .default,\n onDismiss: (() -> Void)? = nil,\n @ViewBuilder actions: () -> Actions\n ) {\n self.title = title\n self.description = description\n self.variant = variant\n self.onDismiss = onDismiss\n self.actions = actions()\n }\n\n public var body: some View {\n HStack(spacing: LumenSpacing.md) {\n VStack(alignment: .leading, spacing: LumenSpacing.xs) {\n Text(title).font(.callout.weight(.semibold)).foregroundStyle(accentColor)\n if let description {\n Text(description).font(.caption).foregroundStyle(theme.colors.inkSoft)\n }\n }\n .frame(maxWidth: .infinity, alignment: .leading)\n actions\n if let onDismiss {\n LumenIconButton(\n systemName: \"xmark\",\n label: \"Dismiss\",\n intent: .quiet,\n size: .sm,\n action: onDismiss\n )\n }\n }\n .padding(.horizontal, LumenSpacing.lg)\n .padding(.vertical, LumenSpacing.md)\n .background(backgroundColor)\n .overlay {\n RoundedRectangle(cornerRadius: LumenRadius.md, style: .continuous)\n .stroke(borderColor, lineWidth: 1)\n }\n .clipShape(RoundedRectangle(cornerRadius: LumenRadius.md, style: .continuous))\n .accessibilityElement(children: .contain)\n }\n\n private var accentColor: Color {\n switch variant {\n case .accent: theme.colors.accent\n case .default: theme.colors.brand\n case .destructive: theme.colors.danger\n case .success: theme.colors.success\n case .warning: theme.colors.warning\n }\n }\n\n private var backgroundColor: Color {\n variant == .default ? theme.colors.surface : accentColor.opacity(0.08)\n }\n\n private var borderColor: Color {\n variant == .default ? theme.colors.line : accentColor.opacity(0.32)\n }\n}\n\npublic extension LumenToast where Actions == EmptyView {\n init(\n _ title: LocalizedStringKey,\n description: LocalizedStringKey? = nil,\n variant: LumenBannerVariant = .default,\n onDismiss: (() -> Void)? = nil\n ) {\n self.init(title, description: description, variant: variant, onDismiss: onDismiss) {\n EmptyView()\n }\n }\n}\n",
38789
- "packages/swift/Sources/LumenUI/AppleComponents.swift": "#if os(iOS) || os(macOS)\nimport SwiftUI\n\npublic enum LumenDateFieldComponents: Sendable {\n case date\n case dateAndTime\n case time\n\n fileprivate var displayedComponents: DatePickerComponents {\n switch self {\n case .date:\n [.date]\n case .dateAndTime:\n [.date, .hourAndMinute]\n case .time:\n [.hourAndMinute]\n }\n }\n}\n\npublic enum LumenDateFieldBounds: Equatable, Sendable {\n case closed(ClosedRange<Date>)\n case from(Date)\n case through(Date)\n case unbounded\n\n public func clamped(_ value: Date) -> Date {\n switch self {\n case let .closed(range):\n min(max(value, range.lowerBound), range.upperBound)\n case let .from(lowerBound):\n max(value, lowerBound)\n case let .through(upperBound):\n min(value, upperBound)\n case .unbounded:\n value\n }\n }\n}\n\n/// A token-aware Apple date input with native picker behavior and Lumen validation messaging.\npublic struct LumenDateField: View {\n @Binding private var selection: Date\n @Environment(\\.lumenTheme) private var theme\n\n private let bounds: LumenDateFieldBounds\n private let components: LumenDateFieldComponents\n private let description: LocalizedStringKey?\n private let errorMessage: LocalizedStringKey?\n private let title: LocalizedStringKey\n\n public init(\n _ title: LocalizedStringKey,\n selection: Binding<Date>,\n components: LumenDateFieldComponents = .date,\n bounds: LumenDateFieldBounds = .unbounded,\n description: LocalizedStringKey? = nil,\n errorMessage: LocalizedStringKey? = nil\n ) {\n self.title = title\n _selection = selection\n self.components = components\n self.bounds = bounds\n self.description = description\n self.errorMessage = errorMessage\n }\n\n public var body: some View {\n VStack(alignment: .leading, spacing: LumenSpacing.sm) {\n datePicker\n .tint(theme.colors.brandSolid)\n\n if let errorMessage {\n Text(errorMessage)\n .font(.caption)\n .foregroundStyle(theme.colors.danger)\n .fixedSize(horizontal: false, vertical: true)\n } else if let description {\n Text(description)\n .font(.caption)\n .foregroundStyle(theme.colors.inkMuted)\n .fixedSize(horizontal: false, vertical: true)\n }\n }\n .accessibilityElement(children: .contain)\n }\n\n @ViewBuilder\n private var datePicker: some View {\n switch bounds {\n case let .closed(range):\n DatePicker(\n title,\n selection: boundedSelection,\n in: range,\n displayedComponents: components.displayedComponents\n )\n case let .from(lowerBound):\n DatePicker(\n title,\n selection: boundedSelection,\n in: lowerBound...,\n displayedComponents: components.displayedComponents\n )\n case let .through(upperBound):\n DatePicker(\n title,\n selection: boundedSelection,\n in: ...upperBound,\n displayedComponents: components.displayedComponents\n )\n case .unbounded:\n DatePicker(\n title,\n selection: boundedSelection,\n displayedComponents: components.displayedComponents\n )\n }\n }\n\n private var boundedSelection: Binding<Date> {\n Binding(\n get: { bounds.clamped(selection) },\n set: { selection = bounds.clamped($0) }\n )\n }\n}\n#endif\n",
38790
- "packages/swift/Sources/LumenUI/ContentComponents.swift": "#if os(iOS) || os(macOS)\nimport SwiftUI\n\npublic enum LumenSkeletonShape: Sendable {\n case circle\n case rectangle\n case text\n}\n\npublic struct LumenSkeleton: View {\n @Environment(\\.lumenTheme) private var theme\n\n private let height: CGFloat\n private let label: String?\n private let shape: LumenSkeletonShape\n private let width: CGFloat?\n\n public init(\n width: CGFloat? = nil,\n height: CGFloat = 16,\n shape: LumenSkeletonShape = .text,\n label: String? = nil\n ) {\n self.width = width\n self.height = height.isFinite && height > 0 ? height : 16\n self.shape = shape\n self.label = label\n }\n\n public var body: some View {\n skeletonShape\n .foregroundStyle(theme.colors.surfaceStrong.opacity(0.72))\n .frame(maxWidth: fillsAvailableWidth ? .infinity : nil, alignment: .leading)\n .frame(width: resolvedWidth, height: height)\n .accessibilityElement(children: .ignore)\n .accessibilityHidden(label == nil)\n .accessibilityLabel(Text(label ?? \"\"))\n .accessibilityValue(Text(label == nil ? \"\" : \"Loading\"))\n }\n\n private var fillsAvailableWidth: Bool {\n width == nil && shape != .circle\n }\n\n private var resolvedWidth: CGFloat? {\n shape == .circle ? height : width\n }\n\n @ViewBuilder\n private var skeletonShape: some View {\n switch shape {\n case .circle:\n Circle()\n case .rectangle:\n RoundedRectangle(cornerRadius: LumenRadius.md, style: .continuous)\n case .text:\n RoundedRectangle(cornerRadius: LumenRadius.sm, style: .continuous)\n }\n }\n}\n\npublic enum LumenGraphicSize: Sendable {\n case lg\n case md\n case sm\n\n var dimension: CGFloat {\n switch self {\n case .lg: 320\n case .md: 240\n case .sm: 160\n }\n }\n}\n\npublic enum LumenGraphicTone: Sendable {\n case accent\n case brand\n case neutral\n}\n\npublic enum LumenGraphicVariant: Sendable {\n case glow\n case grid\n case orbit\n}\n\npublic struct LumenGraphic<Content: View>: View {\n @Environment(\\.lumenTheme) private var theme\n\n private let content: Content\n private let label: String?\n private let size: LumenGraphicSize\n private let tone: LumenGraphicTone\n private let variant: LumenGraphicVariant\n\n public init(\n label: String? = nil,\n size: LumenGraphicSize = .md,\n tone: LumenGraphicTone = .brand,\n variant: LumenGraphicVariant = .orbit,\n @ViewBuilder content: () -> Content\n ) {\n self.label = label\n self.size = size\n self.tone = tone\n self.variant = variant\n self.content = content()\n }\n\n public var body: some View {\n ZStack {\n graphicLayer\n content\n }\n .frame(width: size.dimension, height: size.dimension)\n .clipped()\n .accessibilityElement(children: label == nil ? .ignore : .combine)\n .accessibilityHidden(label == nil)\n .accessibilityLabel(Text(label ?? \"\"))\n }\n\n private var color: Color {\n switch tone {\n case .accent: theme.colors.accent\n case .brand: theme.colors.brand\n case .neutral: theme.colors.inkMuted\n }\n }\n\n @ViewBuilder\n private var graphicLayer: some View {\n switch variant {\n case .glow:\n Circle()\n .fill(\n RadialGradient(\n colors: [color.opacity(0.3), color.opacity(0)],\n center: .center,\n startRadius: 0,\n endRadius: size.dimension * 0.42\n )\n )\n .padding(size.dimension * 0.08)\n case .grid:\n Canvas { context, canvasSize in\n var path = Path()\n for index in 0...6 {\n let position = CGFloat(index) * canvasSize.width / 6\n path.move(to: CGPoint(x: position, y: 0))\n path.addLine(to: CGPoint(x: position, y: canvasSize.height))\n path.move(to: CGPoint(x: 0, y: position))\n path.addLine(to: CGPoint(x: canvasSize.width, y: position))\n }\n context.stroke(path, with: .color(color.opacity(0.18)), lineWidth: 1)\n }\n .padding(size.dimension * 0.12)\n case .orbit:\n Canvas { context, canvasSize in\n let bounds = CGRect(origin: .zero, size: canvasSize)\n for ratio in [0.9, 0.62, 0.34] {\n let inset = canvasSize.width * (1 - ratio) / 2\n context.stroke(\n Path(ellipseIn: bounds.insetBy(dx: inset, dy: inset)),\n with: .color(color.opacity(0.24)),\n lineWidth: 1\n )\n }\n var axes = Path()\n axes.move(to: CGPoint(x: canvasSize.width * 0.14, y: canvasSize.height / 2))\n axes.addLine(to: CGPoint(x: canvasSize.width * 0.86, y: canvasSize.height / 2))\n axes.move(to: CGPoint(x: canvasSize.width / 2, y: canvasSize.height * 0.14))\n axes.addLine(to: CGPoint(x: canvasSize.width / 2, y: canvasSize.height * 0.86))\n context.stroke(axes, with: .color(color.opacity(0.18)), lineWidth: 1)\n }\n }\n }\n}\n\npublic enum LumenBackdropIntensity: Sendable {\n case medium\n case strong\n case subtle\n\n var opacity: Double {\n switch self {\n case .medium: 0.68\n case .strong: 1\n case .subtle: 0.4\n }\n }\n}\n\npublic enum LumenBackdropTone: Sendable {\n case accent\n case brand\n case neutral\n}\n\npublic enum LumenBackdropVariant: Sendable {\n case aurora\n case dots\n case grid\n case rays\n}\n\npublic struct LumenBackdrop<Content: View>: View {\n @Environment(\\.lumenTheme) private var theme\n\n private let content: Content\n private let intensity: LumenBackdropIntensity\n private let tone: LumenBackdropTone\n private let variant: LumenBackdropVariant\n\n public init(\n intensity: LumenBackdropIntensity = .medium,\n tone: LumenBackdropTone = .brand,\n variant: LumenBackdropVariant = .aurora,\n @ViewBuilder content: () -> Content\n ) {\n self.intensity = intensity\n self.tone = tone\n self.variant = variant\n self.content = content()\n }\n\n public var body: some View {\n ZStack {\n backdropLayer\n .opacity(intensity.opacity)\n .accessibilityHidden(true)\n content\n }\n .clipped()\n }\n\n private var color: Color {\n switch tone {\n case .accent: theme.colors.accent\n case .brand: theme.colors.brand\n case .neutral: theme.colors.inkMuted\n }\n }\n\n private var secondaryColor: Color {\n tone == .neutral ? theme.colors.surfaceStrong : theme.colors.accent\n }\n\n @ViewBuilder\n private var backdropLayer: some View {\n switch variant {\n case .aurora:\n GeometryReader { proxy in\n Circle()\n .fill(color.opacity(0.22))\n .frame(width: proxy.size.width * 0.72)\n .offset(x: -proxy.size.width * 0.2, y: -proxy.size.height * 0.46)\n Circle()\n .fill(secondaryColor.opacity(0.18))\n .frame(width: proxy.size.width * 0.62)\n .offset(x: proxy.size.width * 0.58, y: proxy.size.height * 0.44)\n }\n case .dots:\n Canvas { context, canvasSize in\n for row in 0..<6 {\n for column in 0..<8 {\n let point = CGPoint(\n x: (CGFloat(column) + 0.5) * canvasSize.width / 8,\n y: (CGFloat(row) + 0.5) * canvasSize.height / 6\n )\n context.fill(\n Path(ellipseIn: CGRect(x: point.x - 1.5, y: point.y - 1.5, width: 3, height: 3)),\n with: .color(color.opacity(0.28))\n )\n }\n }\n }\n case .grid:\n Canvas { context, canvasSize in\n var path = Path()\n for index in 0...6 {\n let x = CGFloat(index) * canvasSize.width / 6\n let y = CGFloat(index) * canvasSize.height / 6\n path.move(to: CGPoint(x: x, y: 0))\n path.addLine(to: CGPoint(x: x, y: canvasSize.height))\n path.move(to: CGPoint(x: 0, y: y))\n path.addLine(to: CGPoint(x: canvasSize.width, y: y))\n }\n context.stroke(path, with: .color(color.opacity(0.18)), lineWidth: 1)\n }\n case .rays:\n Canvas { context, canvasSize in\n var path = Path()\n let center = CGPoint(x: canvasSize.width / 2, y: canvasSize.height / 2)\n let radius = sqrt(\n canvasSize.width * canvasSize.width + canvasSize.height * canvasSize.height\n )\n for index in 0..<12 {\n let angle = CGFloat(index) * .pi / 6\n path.move(to: center)\n path.addLine(to: CGPoint(\n x: center.x + cos(angle) * radius,\n y: center.y + sin(angle) * radius\n ))\n }\n context.stroke(path, with: .color(color.opacity(0.2)), lineWidth: 1)\n }\n }\n }\n}\n\npublic enum LumenIllustrationSize: Sendable {\n case lg\n case md\n case sm\n\n var dimension: CGFloat {\n switch self {\n case .lg: 176\n case .md: 128\n case .sm: 96\n }\n }\n}\n\npublic enum LumenIllustrationTone: Sendable {\n case accent\n case auto\n case brand\n case neutral\n}\n\npublic enum LumenIllustrationVariant: Sendable {\n case empty\n case error\n case offline\n case success\n}\n\npublic struct LumenIllustration: View {\n @Environment(\\.lumenTheme) private var theme\n\n private let label: String?\n private let size: LumenIllustrationSize\n private let tone: LumenIllustrationTone\n private let variant: LumenIllustrationVariant\n\n public init(\n variant: LumenIllustrationVariant = .empty,\n tone: LumenIllustrationTone = .auto,\n size: LumenIllustrationSize = .md,\n label: String? = nil\n ) {\n self.variant = variant\n self.tone = tone\n self.size = size\n self.label = label\n }\n\n public var body: some View {\n Canvas { context, canvasSize in\n let scale = min(canvasSize.width, canvasSize.height) / 120\n let lineWidth = max(2, 2.5 * scale)\n let bounds = CGRect(origin: .zero, size: canvasSize)\n context.fill(\n Path(ellipseIn: bounds.insetBy(dx: 11 * scale, dy: 11 * scale)),\n with: .color(color.opacity(0.1))\n )\n drawArtwork(context: &context, scale: scale, lineWidth: lineWidth)\n }\n .frame(width: size.dimension, height: size.dimension)\n .accessibilityElement(children: .ignore)\n .accessibilityHidden(label == nil)\n .accessibilityLabel(Text(label ?? \"\"))\n }\n\n private var color: Color {\n switch tone {\n case .accent: theme.colors.accent\n case .brand: theme.colors.brand\n case .neutral: theme.colors.inkMuted\n case .auto:\n switch variant {\n case .empty: theme.colors.brand\n case .error: theme.colors.danger\n case .offline: theme.colors.inkMuted\n case .success: theme.colors.success\n }\n }\n }\n\n private func drawArtwork(context: inout GraphicsContext, scale: CGFloat, lineWidth: CGFloat) {\n switch variant {\n case .empty:\n var tray = Path(roundedRect: CGRect(x: 29, y: 43, width: 62, height: 40).scaled(by: scale), cornerRadius: 8 * scale)\n tray.move(to: CGPoint(x: 40 * scale, y: 60 * scale))\n tray.addLine(to: CGPoint(x: 80 * scale, y: 60 * scale))\n context.stroke(tray, with: .color(color), lineWidth: lineWidth)\n case .error, .success:\n context.stroke(\n Path(ellipseIn: CGRect(x: 30, y: 30, width: 60, height: 60).scaled(by: scale)),\n with: .color(color),\n lineWidth: lineWidth\n )\n var symbol = Path()\n if variant == .success {\n symbol.move(to: CGPoint(x: 44 * scale, y: 61 * scale))\n symbol.addLine(to: CGPoint(x: 55 * scale, y: 72 * scale))\n symbol.addLine(to: CGPoint(x: 77 * scale, y: 48 * scale))\n } else {\n symbol.move(to: CGPoint(x: 47 * scale, y: 47 * scale))\n symbol.addLine(to: CGPoint(x: 73 * scale, y: 73 * scale))\n symbol.move(to: CGPoint(x: 73 * scale, y: 47 * scale))\n symbol.addLine(to: CGPoint(x: 47 * scale, y: 73 * scale))\n }\n context.stroke(symbol, with: .color(color), style: StrokeStyle(lineWidth: lineWidth, lineCap: .round, lineJoin: .round))\n case .offline:\n var cloud = Path(roundedRect: CGRect(x: 24, y: 45, width: 72, height: 35).scaled(by: scale), cornerRadius: 18 * scale)\n cloud.move(to: CGPoint(x: 30 * scale, y: 30 * scale))\n cloud.addLine(to: CGPoint(x: 90 * scale, y: 90 * scale))\n context.stroke(cloud, with: .color(color), style: StrokeStyle(lineWidth: lineWidth, lineCap: .round))\n }\n }\n}\n\nprivate extension CGRect {\n func scaled(by scale: CGFloat) -> CGRect {\n CGRect(x: origin.x * scale, y: origin.y * scale, width: width * scale, height: height * scale)\n }\n}\n\npublic struct LumenDisclosure<Content: View>: View {\n @Binding private var isExpanded: Bool\n @Environment(\\.lumenTheme) private var theme\n\n private let content: Content\n private let description: String?\n private let title: String\n\n public init(\n _ title: String,\n isExpanded: Binding<Bool>,\n description: String? = nil,\n @ViewBuilder content: () -> Content\n ) {\n self.title = title\n _isExpanded = isExpanded\n self.description = description\n self.content = content()\n }\n\n public var body: some View {\n DisclosureGroup(isExpanded: $isExpanded) {\n content\n .frame(maxWidth: .infinity, alignment: .leading)\n .padding(.top, LumenSpacing.md)\n } label: {\n VStack(alignment: .leading, spacing: LumenSpacing.xs) {\n Text(title)\n .font(.callout.weight(.semibold))\n .foregroundStyle(theme.colors.ink)\n\n if let description {\n Text(description)\n .font(.caption)\n .foregroundStyle(theme.colors.inkMuted)\n .fixedSize(horizontal: false, vertical: true)\n }\n }\n }\n .tint(theme.colors.brandSolid)\n .padding(.horizontal, LumenSpacing.lg)\n .padding(.vertical, LumenSpacing.md)\n .background(theme.colors.surface)\n .overlay {\n RoundedRectangle(cornerRadius: LumenRadius.md, style: .continuous)\n .stroke(theme.colors.line, lineWidth: 1)\n }\n .clipShape(RoundedRectangle(cornerRadius: LumenRadius.md, style: .continuous))\n }\n}\n#endif\n",
39647
+ "packages/swift/Sources/LumenUI/AdditionalComponents.swift": "import SwiftUI\n\npublic enum LumenButtonGroupOrientation: Sendable {\n case horizontal\n case vertical\n}\n\npublic struct LumenButtonGroup<Content: View>: View {\n private let content: Content\n private let orientation: LumenButtonGroupOrientation\n\n public init(\n orientation: LumenButtonGroupOrientation = .horizontal,\n @ViewBuilder content: () -> Content\n ) {\n self.orientation = orientation\n self.content = content()\n }\n\n public var body: some View {\n Group {\n if orientation == .horizontal {\n HStack(spacing: LumenSpacing.sm) { content }\n } else {\n VStack(alignment: .leading, spacing: LumenSpacing.sm) { content }\n }\n }\n .accessibilityElement(children: .contain)\n }\n}\n\npublic struct LumenFieldGroup<Content: View>: View {\n @Environment(\\.lumenTheme) private var theme\n\n private let content: Content\n private let description: LocalizedStringKey?\n private let errorMessage: LocalizedStringKey?\n private let required: Bool\n private let title: LocalizedStringKey\n\n public init(\n _ title: LocalizedStringKey,\n description: LocalizedStringKey? = nil,\n errorMessage: LocalizedStringKey? = nil,\n required: Bool = false,\n @ViewBuilder content: () -> Content\n ) {\n self.title = title\n self.description = description\n self.errorMessage = errorMessage\n self.required = required\n self.content = content()\n }\n\n public var body: some View {\n VStack(alignment: .leading, spacing: LumenSpacing.sm) {\n HStack(spacing: LumenSpacing.xs) {\n Text(title)\n .font(.callout.weight(.semibold))\n .foregroundStyle(theme.colors.ink)\n if required {\n Text(\"*\").foregroundStyle(theme.colors.danger).accessibilityLabel(\"Required\")\n }\n }\n if let description {\n Text(description)\n .font(.caption)\n .foregroundStyle(theme.colors.inkMuted)\n }\n content\n if let errorMessage {\n Text(errorMessage)\n .font(.caption)\n .foregroundStyle(theme.colors.danger)\n }\n }\n .accessibilityElement(children: .contain)\n }\n}\n\n#if !os(watchOS)\npublic struct LumenTextarea: View {\n @Binding private var text: String\n @Environment(\\.isEnabled) private var isEnabled\n @Environment(\\.lumenTheme) private var theme\n\n private let description: LocalizedStringKey?\n private let errorMessage: LocalizedStringKey?\n private let minHeight: CGFloat\n private let title: LocalizedStringKey\n\n public init(\n _ title: LocalizedStringKey,\n text: Binding<String>,\n description: LocalizedStringKey? = nil,\n errorMessage: LocalizedStringKey? = nil,\n minHeight: CGFloat = 112\n ) {\n self.title = title\n _text = text\n self.description = description\n self.errorMessage = errorMessage\n self.minHeight = minHeight\n }\n\n public var body: some View {\n VStack(alignment: .leading, spacing: LumenSpacing.xs) {\n Text(title)\n .font(.callout.weight(.semibold))\n .foregroundStyle(theme.colors.ink)\n TextEditor(text: $text)\n .scrollContentBackground(.hidden)\n .font(.callout)\n .foregroundStyle(theme.colors.ink)\n .frame(minHeight: minHeight)\n .padding(LumenSpacing.sm)\n .background(theme.colors.surface)\n .overlay {\n RoundedRectangle(cornerRadius: LumenRadius.sm, style: .continuous)\n .stroke(errorMessage == nil ? theme.colors.line : theme.colors.danger, lineWidth: 1)\n }\n .clipShape(RoundedRectangle(cornerRadius: LumenRadius.sm, style: .continuous))\n .opacity(isEnabled ? 1 : 0.52)\n .accessibilityLabel(title)\n .accessibilityValue(errorMessage ?? LocalizedStringKey(text))\n if let errorMessage {\n Text(errorMessage).font(.caption).foregroundStyle(theme.colors.danger)\n } else if let description {\n Text(description).font(.caption).foregroundStyle(theme.colors.inkMuted)\n }\n }\n }\n}\n#endif\n\npublic struct LumenChip: View {\n @Environment(\\.isEnabled) private var isEnabled\n @Environment(\\.lumenTheme) private var theme\n\n private let label: LocalizedStringKey\n private let onPress: (() -> Void)?\n private let onRemove: (() -> Void)?\n private let removeLabel: LocalizedStringKey\n private let selected: Bool\n\n public init(\n _ label: LocalizedStringKey,\n selected: Bool = false,\n removeLabel: LocalizedStringKey = \"Remove\",\n onPress: (() -> Void)? = nil,\n onRemove: (() -> Void)? = nil\n ) {\n self.label = label\n self.selected = selected\n self.removeLabel = removeLabel\n self.onPress = onPress\n self.onRemove = onRemove\n }\n\n public var body: some View {\n HStack(spacing: LumenSpacing.xs) {\n if let onPress {\n Button(action: onPress) {\n Text(label).font(.caption.weight(.semibold))\n }\n .buttonStyle(.plain)\n .accessibilityAddTraits(selected ? .isSelected : [])\n } else {\n Text(label).font(.caption.weight(.semibold))\n }\n if let onRemove {\n Button(action: onRemove) {\n Image(systemName: \"xmark\").font(.caption2.weight(.bold))\n }\n .buttonStyle(.plain)\n .accessibilityLabel(removeLabel)\n }\n }\n .foregroundStyle(selected ? theme.colors.brand : theme.colors.inkSoft)\n .padding(.horizontal, LumenSpacing.md)\n .frame(minHeight: 32)\n .background(selected ? theme.colors.brandSoft : theme.colors.surfaceMuted)\n .overlay {\n Capsule().stroke(selected ? theme.colors.brand : theme.colors.line, lineWidth: 1)\n }\n .clipShape(Capsule())\n .opacity(isEnabled ? 1 : 0.52)\n .accessibilityElement(children: .contain)\n }\n}\n\npublic struct LumenToast<Actions: View>: View {\n @Environment(\\.lumenTheme) private var theme\n\n private let actions: Actions\n private let description: LocalizedStringKey?\n private let onDismiss: (() -> Void)?\n private let title: LocalizedStringKey\n private let variant: LumenBannerVariant\n\n public init(\n _ title: LocalizedStringKey,\n description: LocalizedStringKey? = nil,\n variant: LumenBannerVariant = .default,\n onDismiss: (() -> Void)? = nil,\n @ViewBuilder actions: () -> Actions\n ) {\n self.title = title\n self.description = description\n self.variant = variant\n self.onDismiss = onDismiss\n self.actions = actions()\n }\n\n public var body: some View {\n HStack(spacing: LumenSpacing.md) {\n VStack(alignment: .leading, spacing: LumenSpacing.xs) {\n Text(title).font(.callout.weight(.semibold)).foregroundStyle(accentColor)\n if let description {\n Text(description).font(.caption).foregroundStyle(theme.colors.inkSoft)\n }\n }\n .frame(maxWidth: .infinity, alignment: .leading)\n actions\n if let onDismiss {\n LumenIconButton(\n systemName: \"xmark\",\n label: \"Dismiss\",\n intent: .quiet,\n size: .sm,\n action: onDismiss\n )\n }\n }\n .padding(.horizontal, LumenSpacing.lg)\n .padding(.vertical, LumenSpacing.md)\n .background(backgroundColor)\n .overlay {\n RoundedRectangle(cornerRadius: LumenRadius.md, style: .continuous)\n .stroke(borderColor, lineWidth: 1)\n }\n .clipShape(RoundedRectangle(cornerRadius: LumenRadius.md, style: .continuous))\n .accessibilityElement(children: .contain)\n }\n\n private var accentColor: Color {\n switch variant {\n case .accent: theme.colors.accent\n case .default: theme.colors.brand\n case .destructive: theme.colors.danger\n case .success: theme.colors.success\n case .warning: theme.colors.warning\n }\n }\n\n private var backgroundColor: Color {\n variant == .default ? theme.colors.surface : accentColor.opacity(0.08)\n }\n\n private var borderColor: Color {\n variant == .default ? theme.colors.line : accentColor.opacity(0.32)\n }\n}\n\npublic extension LumenToast where Actions == EmptyView {\n init(\n _ title: LocalizedStringKey,\n description: LocalizedStringKey? = nil,\n variant: LumenBannerVariant = .default,\n onDismiss: (() -> Void)? = nil\n ) {\n self.init(title, description: description, variant: variant, onDismiss: onDismiss) {\n EmptyView()\n }\n }\n}\n",
39648
+ "packages/swift/Sources/LumenUI/AppleComponents.swift": "#if os(iOS) || os(macOS)\nimport SwiftUI\n\npublic enum LumenDateFieldComponents: Sendable {\n case date\n case dateAndTime\n case time\n\n fileprivate var displayedComponents: DatePickerComponents {\n switch self {\n case .date:\n [.date]\n case .dateAndTime:\n [.date, .hourAndMinute]\n case .time:\n [.hourAndMinute]\n }\n }\n}\n\npublic enum LumenDateFieldBounds: Equatable, Sendable {\n case closed(ClosedRange<Date>)\n case from(Date)\n case through(Date)\n case unbounded\n\n public func clamped(_ value: Date) -> Date {\n switch self {\n case let .closed(range):\n min(max(value, range.lowerBound), range.upperBound)\n case let .from(lowerBound):\n max(value, lowerBound)\n case let .through(upperBound):\n min(value, upperBound)\n case .unbounded:\n value\n }\n }\n}\n\n/// A token-aware Apple date input with native picker behavior and Lumen validation messaging.\npublic struct LumenDateField: View {\n @Binding private var selection: Date\n @Environment(\\.lumenTheme) private var theme\n\n private let bounds: LumenDateFieldBounds\n private let components: LumenDateFieldComponents\n private let description: LocalizedStringKey?\n private let errorMessage: LocalizedStringKey?\n private let title: LocalizedStringKey\n\n public init(\n _ title: LocalizedStringKey,\n selection: Binding<Date>,\n components: LumenDateFieldComponents = .date,\n bounds: LumenDateFieldBounds = .unbounded,\n description: LocalizedStringKey? = nil,\n errorMessage: LocalizedStringKey? = nil\n ) {\n self.title = title\n _selection = selection\n self.components = components\n self.bounds = bounds\n self.description = description\n self.errorMessage = errorMessage\n }\n\n public var body: some View {\n VStack(alignment: .leading, spacing: LumenSpacing.sm) {\n datePicker\n .tint(theme.colors.brandSolid)\n\n if let errorMessage {\n Text(errorMessage)\n .font(.caption)\n .foregroundStyle(theme.colors.danger)\n .fixedSize(horizontal: false, vertical: true)\n } else if let description {\n Text(description)\n .font(.caption)\n .foregroundStyle(theme.colors.inkMuted)\n .fixedSize(horizontal: false, vertical: true)\n }\n }\n .accessibilityElement(children: .contain)\n }\n\n @ViewBuilder\n private var datePicker: some View {\n switch bounds {\n case let .closed(range):\n DatePicker(\n title,\n selection: boundedSelection,\n in: range,\n displayedComponents: components.displayedComponents\n )\n case let .from(lowerBound):\n DatePicker(\n title,\n selection: boundedSelection,\n in: lowerBound...,\n displayedComponents: components.displayedComponents\n )\n case let .through(upperBound):\n DatePicker(\n title,\n selection: boundedSelection,\n in: ...upperBound,\n displayedComponents: components.displayedComponents\n )\n case .unbounded:\n DatePicker(\n title,\n selection: boundedSelection,\n displayedComponents: components.displayedComponents\n )\n }\n }\n\n private var boundedSelection: Binding<Date> {\n Binding(\n get: { bounds.clamped(selection) },\n set: { selection = bounds.clamped($0) }\n )\n }\n}\n\n/// A token-aware URL action that preserves SwiftUI's native link-opening behavior.\npublic struct LumenLink<Label: View>: View {\n @Environment(\\.lumenTheme) private var theme\n\n private let destination: URL\n private let label: Label\n private let showsExternalIndicator: Bool\n\n public init(\n destination: URL,\n showsExternalIndicator: Bool = false,\n @ViewBuilder label: () -> Label\n ) {\n self.destination = destination\n self.label = label()\n self.showsExternalIndicator = showsExternalIndicator\n }\n\n public var body: some View {\n Link(destination: destination) {\n HStack(spacing: LumenSpacing.xs) {\n label\n\n if showsExternalIndicator {\n Image(systemName: \"arrow.up.right\")\n .font(.caption2.weight(.semibold))\n .accessibilityHidden(true)\n }\n }\n }\n .foregroundStyle(theme.colors.brandSolid)\n .underline()\n }\n}\n\npublic extension LumenLink where Label == Text {\n init(\n _ title: LocalizedStringKey,\n destination: URL,\n showsExternalIndicator: Bool = false\n ) {\n self.init(destination: destination, showsExternalIndicator: showsExternalIndicator) {\n Text(title)\n }\n }\n}\n#endif\n",
39649
+ "packages/swift/Sources/LumenUI/ContentComponents.swift": "#if os(iOS) || os(macOS)\nimport SwiftUI\n\npublic enum LumenSkeletonShape: Sendable {\n case circle\n case rectangle\n case text\n}\n\npublic struct LumenSkeleton: View {\n @Environment(\\.lumenTheme) private var theme\n\n private let height: CGFloat\n private let label: String?\n private let shape: LumenSkeletonShape\n private let width: CGFloat?\n\n public init(\n width: CGFloat? = nil,\n height: CGFloat = 16,\n shape: LumenSkeletonShape = .text,\n label: String? = nil\n ) {\n self.width = width\n self.height = height.isFinite && height > 0 ? height : 16\n self.shape = shape\n self.label = label\n }\n\n public var body: some View {\n skeletonShape\n .foregroundStyle(theme.colors.surfaceStrong.opacity(0.72))\n .frame(maxWidth: fillsAvailableWidth ? .infinity : nil, alignment: .leading)\n .frame(width: resolvedWidth, height: height)\n .accessibilityElement(children: .ignore)\n .accessibilityHidden(label == nil)\n .accessibilityLabel(Text(label ?? \"\"))\n .accessibilityValue(Text(label == nil ? \"\" : \"Loading\"))\n }\n\n private var fillsAvailableWidth: Bool {\n width == nil && shape != .circle\n }\n\n private var resolvedWidth: CGFloat? {\n shape == .circle ? height : width\n }\n\n @ViewBuilder\n private var skeletonShape: some View {\n switch shape {\n case .circle:\n Circle()\n case .rectangle:\n RoundedRectangle(cornerRadius: LumenRadius.md, style: .continuous)\n case .text:\n RoundedRectangle(cornerRadius: LumenRadius.sm, style: .continuous)\n }\n }\n}\n\npublic enum LumenGraphicSize: Sendable {\n case lg\n case md\n case sm\n\n var dimension: CGFloat {\n switch self {\n case .lg: 320\n case .md: 240\n case .sm: 160\n }\n }\n}\n\npublic enum LumenGraphicTone: Sendable {\n case accent\n case brand\n case neutral\n}\n\npublic enum LumenGraphicVariant: Sendable {\n case glow\n case grid\n case orbit\n}\n\npublic struct LumenGraphic<Content: View>: View {\n @Environment(\\.lumenTheme) private var theme\n\n private let content: Content\n private let label: String?\n private let size: LumenGraphicSize\n private let tone: LumenGraphicTone\n private let variant: LumenGraphicVariant\n\n public init(\n label: String? = nil,\n size: LumenGraphicSize = .md,\n tone: LumenGraphicTone = .brand,\n variant: LumenGraphicVariant = .orbit,\n @ViewBuilder content: () -> Content\n ) {\n self.label = label\n self.size = size\n self.tone = tone\n self.variant = variant\n self.content = content()\n }\n\n public var body: some View {\n ZStack {\n graphicLayer\n content\n }\n .frame(width: size.dimension, height: size.dimension)\n .clipped()\n .accessibilityElement(children: label == nil ? .ignore : .combine)\n .accessibilityHidden(label == nil)\n .accessibilityLabel(Text(label ?? \"\"))\n }\n\n private var color: Color {\n switch tone {\n case .accent: theme.colors.accent\n case .brand: theme.colors.brand\n case .neutral: theme.colors.inkMuted\n }\n }\n\n @ViewBuilder\n private var graphicLayer: some View {\n switch variant {\n case .glow:\n Circle()\n .fill(\n RadialGradient(\n colors: [color.opacity(0.3), color.opacity(0)],\n center: .center,\n startRadius: 0,\n endRadius: size.dimension * 0.42\n )\n )\n .padding(size.dimension * 0.08)\n case .grid:\n Canvas { context, canvasSize in\n var path = Path()\n for index in 0...6 {\n let position = CGFloat(index) * canvasSize.width / 6\n path.move(to: CGPoint(x: position, y: 0))\n path.addLine(to: CGPoint(x: position, y: canvasSize.height))\n path.move(to: CGPoint(x: 0, y: position))\n path.addLine(to: CGPoint(x: canvasSize.width, y: position))\n }\n context.stroke(path, with: .color(color.opacity(0.18)), lineWidth: 1)\n }\n .padding(size.dimension * 0.12)\n case .orbit:\n Canvas { context, canvasSize in\n let bounds = CGRect(origin: .zero, size: canvasSize)\n for ratio in [0.9, 0.62, 0.34] {\n let inset = canvasSize.width * (1 - ratio) / 2\n context.stroke(\n Path(ellipseIn: bounds.insetBy(dx: inset, dy: inset)),\n with: .color(color.opacity(0.24)),\n lineWidth: 1\n )\n }\n var axes = Path()\n axes.move(to: CGPoint(x: canvasSize.width * 0.14, y: canvasSize.height / 2))\n axes.addLine(to: CGPoint(x: canvasSize.width * 0.86, y: canvasSize.height / 2))\n axes.move(to: CGPoint(x: canvasSize.width / 2, y: canvasSize.height * 0.14))\n axes.addLine(to: CGPoint(x: canvasSize.width / 2, y: canvasSize.height * 0.86))\n context.stroke(axes, with: .color(color.opacity(0.18)), lineWidth: 1)\n }\n }\n }\n}\n\npublic enum LumenBackdropIntensity: Sendable {\n case medium\n case strong\n case subtle\n\n var opacity: Double {\n switch self {\n case .medium: 0.68\n case .strong: 1\n case .subtle: 0.4\n }\n }\n}\n\npublic enum LumenBackdropTone: Sendable {\n case accent\n case brand\n case neutral\n}\n\npublic enum LumenBackdropVariant: Sendable {\n case aurora\n case dots\n case grid\n case rays\n}\n\npublic struct LumenBackdrop<Content: View>: View {\n @Environment(\\.lumenTheme) private var theme\n\n private let content: Content\n private let intensity: LumenBackdropIntensity\n private let tone: LumenBackdropTone\n private let variant: LumenBackdropVariant\n\n public init(\n intensity: LumenBackdropIntensity = .medium,\n tone: LumenBackdropTone = .brand,\n variant: LumenBackdropVariant = .aurora,\n @ViewBuilder content: () -> Content\n ) {\n self.intensity = intensity\n self.tone = tone\n self.variant = variant\n self.content = content()\n }\n\n public var body: some View {\n ZStack {\n backdropLayer\n .opacity(intensity.opacity)\n .accessibilityHidden(true)\n content\n }\n .clipped()\n }\n\n private var color: Color {\n switch tone {\n case .accent: theme.colors.accent\n case .brand: theme.colors.brand\n case .neutral: theme.colors.inkMuted\n }\n }\n\n private var secondaryColor: Color {\n tone == .neutral ? theme.colors.surfaceStrong : theme.colors.accent\n }\n\n @ViewBuilder\n private var backdropLayer: some View {\n switch variant {\n case .aurora:\n GeometryReader { proxy in\n Circle()\n .fill(color.opacity(0.22))\n .frame(width: proxy.size.width * 0.72)\n .offset(x: -proxy.size.width * 0.2, y: -proxy.size.height * 0.46)\n Circle()\n .fill(secondaryColor.opacity(0.18))\n .frame(width: proxy.size.width * 0.62)\n .offset(x: proxy.size.width * 0.58, y: proxy.size.height * 0.44)\n }\n case .dots:\n Canvas { context, canvasSize in\n for row in 0..<6 {\n for column in 0..<8 {\n let point = CGPoint(\n x: (CGFloat(column) + 0.5) * canvasSize.width / 8,\n y: (CGFloat(row) + 0.5) * canvasSize.height / 6\n )\n context.fill(\n Path(ellipseIn: CGRect(x: point.x - 1.5, y: point.y - 1.5, width: 3, height: 3)),\n with: .color(color.opacity(0.28))\n )\n }\n }\n }\n case .grid:\n Canvas { context, canvasSize in\n var path = Path()\n for index in 0...6 {\n let x = CGFloat(index) * canvasSize.width / 6\n let y = CGFloat(index) * canvasSize.height / 6\n path.move(to: CGPoint(x: x, y: 0))\n path.addLine(to: CGPoint(x: x, y: canvasSize.height))\n path.move(to: CGPoint(x: 0, y: y))\n path.addLine(to: CGPoint(x: canvasSize.width, y: y))\n }\n context.stroke(path, with: .color(color.opacity(0.18)), lineWidth: 1)\n }\n case .rays:\n Canvas { context, canvasSize in\n var path = Path()\n let center = CGPoint(x: canvasSize.width / 2, y: canvasSize.height / 2)\n let radius = sqrt(\n canvasSize.width * canvasSize.width + canvasSize.height * canvasSize.height\n )\n for index in 0..<12 {\n let angle = CGFloat(index) * .pi / 6\n path.move(to: center)\n path.addLine(to: CGPoint(\n x: center.x + cos(angle) * radius,\n y: center.y + sin(angle) * radius\n ))\n }\n context.stroke(path, with: .color(color.opacity(0.2)), lineWidth: 1)\n }\n }\n }\n}\n\npublic enum LumenIllustrationSize: Sendable {\n case lg\n case md\n case sm\n\n var dimension: CGFloat {\n switch self {\n case .lg: 176\n case .md: 128\n case .sm: 96\n }\n }\n}\n\npublic enum LumenIllustrationTone: Sendable {\n case accent\n case auto\n case brand\n case neutral\n}\n\npublic enum LumenIllustrationVariant: Sendable {\n case empty\n case error\n case offline\n case success\n}\n\npublic struct LumenIllustration: View {\n @Environment(\\.lumenTheme) private var theme\n\n private let label: String?\n private let size: LumenIllustrationSize\n private let tone: LumenIllustrationTone\n private let variant: LumenIllustrationVariant\n\n public init(\n variant: LumenIllustrationVariant = .empty,\n tone: LumenIllustrationTone = .auto,\n size: LumenIllustrationSize = .md,\n label: String? = nil\n ) {\n self.variant = variant\n self.tone = tone\n self.size = size\n self.label = label\n }\n\n public var body: some View {\n Canvas { context, canvasSize in\n let scale = min(canvasSize.width, canvasSize.height) / 120\n let lineWidth = max(2, 2.5 * scale)\n let bounds = CGRect(origin: .zero, size: canvasSize)\n context.fill(\n Path(ellipseIn: bounds.insetBy(dx: 11 * scale, dy: 11 * scale)),\n with: .color(color.opacity(0.1))\n )\n drawArtwork(context: &context, scale: scale, lineWidth: lineWidth)\n }\n .frame(width: size.dimension, height: size.dimension)\n .accessibilityElement(children: .ignore)\n .accessibilityHidden(label == nil)\n .accessibilityLabel(Text(label ?? \"\"))\n }\n\n private var color: Color {\n switch tone {\n case .accent: theme.colors.accent\n case .brand: theme.colors.brand\n case .neutral: theme.colors.inkMuted\n case .auto:\n switch variant {\n case .empty: theme.colors.brand\n case .error: theme.colors.danger\n case .offline: theme.colors.inkMuted\n case .success: theme.colors.success\n }\n }\n }\n\n private func drawArtwork(context: inout GraphicsContext, scale: CGFloat, lineWidth: CGFloat) {\n switch variant {\n case .empty:\n var tray = Path(roundedRect: CGRect(x: 29, y: 43, width: 62, height: 40).scaled(by: scale), cornerRadius: 8 * scale)\n tray.move(to: CGPoint(x: 40 * scale, y: 60 * scale))\n tray.addLine(to: CGPoint(x: 80 * scale, y: 60 * scale))\n context.stroke(tray, with: .color(color), lineWidth: lineWidth)\n case .error, .success:\n context.stroke(\n Path(ellipseIn: CGRect(x: 30, y: 30, width: 60, height: 60).scaled(by: scale)),\n with: .color(color),\n lineWidth: lineWidth\n )\n var symbol = Path()\n if variant == .success {\n symbol.move(to: CGPoint(x: 44 * scale, y: 61 * scale))\n symbol.addLine(to: CGPoint(x: 55 * scale, y: 72 * scale))\n symbol.addLine(to: CGPoint(x: 77 * scale, y: 48 * scale))\n } else {\n symbol.move(to: CGPoint(x: 47 * scale, y: 47 * scale))\n symbol.addLine(to: CGPoint(x: 73 * scale, y: 73 * scale))\n symbol.move(to: CGPoint(x: 73 * scale, y: 47 * scale))\n symbol.addLine(to: CGPoint(x: 47 * scale, y: 73 * scale))\n }\n context.stroke(symbol, with: .color(color), style: StrokeStyle(lineWidth: lineWidth, lineCap: .round, lineJoin: .round))\n case .offline:\n var cloud = Path(roundedRect: CGRect(x: 24, y: 45, width: 72, height: 35).scaled(by: scale), cornerRadius: 18 * scale)\n cloud.move(to: CGPoint(x: 30 * scale, y: 30 * scale))\n cloud.addLine(to: CGPoint(x: 90 * scale, y: 90 * scale))\n context.stroke(cloud, with: .color(color), style: StrokeStyle(lineWidth: lineWidth, lineCap: .round))\n }\n }\n}\n\nprivate extension CGRect {\n func scaled(by scale: CGFloat) -> CGRect {\n CGRect(x: origin.x * scale, y: origin.y * scale, width: width * scale, height: height * scale)\n }\n}\n\npublic struct LumenDisclosure<Content: View>: View {\n @Environment(\\.lumenTheme) private var theme\n @State private var internalIsExpanded: Bool\n\n private let content: Content\n private let customLabel: AnyView?\n private let description: String?\n private let externalIsExpanded: Binding<Bool>?\n private let title: String?\n\n public init(\n _ title: String,\n isExpanded: Binding<Bool>,\n description: String? = nil,\n @ViewBuilder content: () -> Content\n ) {\n self.content = content()\n self.customLabel = nil\n self.description = description\n self.externalIsExpanded = isExpanded\n _internalIsExpanded = State(initialValue: isExpanded.wrappedValue)\n self.title = title\n }\n\n public init(\n _ title: String,\n initiallyExpanded: Bool = false,\n description: String? = nil,\n @ViewBuilder content: () -> Content\n ) {\n self.content = content()\n self.customLabel = nil\n self.description = description\n self.externalIsExpanded = nil\n _internalIsExpanded = State(initialValue: initiallyExpanded)\n self.title = title\n }\n\n public init<Label: View>(\n isExpanded: Binding<Bool>,\n @ViewBuilder label: () -> Label,\n @ViewBuilder content: () -> Content\n ) {\n self.content = content()\n self.customLabel = AnyView(label())\n self.description = nil\n self.externalIsExpanded = isExpanded\n _internalIsExpanded = State(initialValue: isExpanded.wrappedValue)\n self.title = nil\n }\n\n public init<Label: View>(\n initiallyExpanded: Bool = false,\n @ViewBuilder label: () -> Label,\n @ViewBuilder content: () -> Content\n ) {\n self.content = content()\n self.customLabel = AnyView(label())\n self.description = nil\n self.externalIsExpanded = nil\n _internalIsExpanded = State(initialValue: initiallyExpanded)\n self.title = nil\n }\n\n public var body: some View {\n DisclosureGroup(isExpanded: expansionBinding) {\n content\n .frame(maxWidth: .infinity, alignment: .leading)\n .padding(.top, LumenSpacing.md)\n } label: {\n disclosureLabel\n }\n .tint(theme.colors.brandSolid)\n .padding(.horizontal, LumenSpacing.lg)\n .padding(.vertical, LumenSpacing.md)\n .background(theme.colors.surface)\n .overlay {\n RoundedRectangle(cornerRadius: LumenRadius.md, style: .continuous)\n .stroke(theme.colors.line, lineWidth: 1)\n }\n .clipShape(RoundedRectangle(cornerRadius: LumenRadius.md, style: .continuous))\n }\n\n private var expansionBinding: Binding<Bool> {\n externalIsExpanded ?? Binding(\n get: { internalIsExpanded },\n set: { internalIsExpanded = $0 }\n )\n }\n\n @ViewBuilder\n private var disclosureLabel: some View {\n if let customLabel {\n customLabel\n } else if let title {\n VStack(alignment: .leading, spacing: LumenSpacing.xs) {\n Text(title)\n .font(.callout.weight(.semibold))\n .foregroundStyle(theme.colors.ink)\n\n if let description {\n Text(description)\n .font(.caption)\n .foregroundStyle(theme.colors.inkMuted)\n .fixedSize(horizontal: false, vertical: true)\n }\n }\n }\n }\n}\n#endif\n",
38791
39650
  "packages/swift/Sources/LumenUI/FormControls.swift": "#if os(iOS) || os(macOS)\nimport SwiftUI\n\npublic struct LumenToggle<Label: View>: View {\n @Binding private var isOn: Bool\n @Environment(\\.lumenTheme) private var theme\n\n private let label: Label\n\n public init(\n isOn: Binding<Bool>,\n @ViewBuilder label: () -> Label\n ) {\n _isOn = isOn\n self.label = label()\n }\n\n public var body: some View {\n Toggle(isOn: $isOn) {\n label\n }\n .toggleStyle(.switch)\n .tint(theme.colors.brandSolid)\n }\n}\n\npublic extension LumenToggle where Label == Text {\n init(_ title: LocalizedStringKey, isOn: Binding<Bool>) {\n self.init(isOn: isOn) {\n Text(title)\n }\n }\n}\n\npublic struct LumenSettingsRow<Control: View>: View {\n @Environment(\\.lumenTheme) private var theme\n\n private let control: Control\n private let description: LocalizedStringKey?\n private let systemName: String?\n private let title: LocalizedStringKey\n\n public init(\n _ title: LocalizedStringKey,\n description: LocalizedStringKey? = nil,\n systemName: String? = nil,\n @ViewBuilder control: () -> Control\n ) {\n self.title = title\n self.description = description\n self.systemName = systemName\n self.control = control()\n }\n\n public var body: some View {\n HStack(alignment: .center, spacing: LumenSpacing.lg) {\n if let systemName {\n LumenIcon(systemName: systemName, size: .sm, color: theme.colors.inkSoft)\n }\n\n VStack(alignment: .leading, spacing: LumenSpacing.xs) {\n Text(title)\n .font(.callout.weight(.semibold))\n .foregroundStyle(theme.colors.ink)\n\n if let description {\n Text(description)\n .font(.caption)\n .foregroundStyle(theme.colors.inkMuted)\n .fixedSize(horizontal: false, vertical: true)\n }\n }\n .frame(maxWidth: .infinity, alignment: .leading)\n\n control\n .fixedSize(horizontal: true, vertical: false)\n }\n .accessibilityElement(children: .contain)\n }\n}\n\npublic enum LumenPickerStyle: Sendable {\n case automatic\n case menu\n case segmented\n}\n\npublic struct LumenPicker<SelectionValue: Hashable, Content: View>: View {\n @Binding private var selection: SelectionValue\n @Environment(\\.lumenTheme) private var theme\n\n private let content: Content\n private let showsLabel: Bool\n private let style: LumenPickerStyle\n private let title: LocalizedStringKey\n\n public init(\n _ title: LocalizedStringKey,\n selection: Binding<SelectionValue>,\n style: LumenPickerStyle = .automatic,\n showsLabel: Bool = true,\n @ViewBuilder content: () -> Content\n ) {\n self.title = title\n _selection = selection\n self.style = style\n self.showsLabel = showsLabel\n self.content = content()\n }\n\n public var body: some View {\n styledPicker\n .tint(theme.colors.brandSolid)\n }\n\n @ViewBuilder\n private var styledPicker: some View {\n switch style {\n case .automatic:\n picker\n case .menu:\n #if os(watchOS)\n picker\n #else\n picker.pickerStyle(.menu)\n #endif\n case .segmented:\n picker.pickerStyle(.segmented)\n }\n }\n\n @ViewBuilder\n private var picker: some View {\n let picker = Picker(title, selection: $selection) {\n content\n }\n\n if showsLabel {\n picker\n } else {\n picker.labelsHidden()\n }\n }\n}\n\npublic struct LumenSliderConfiguration: Equatable, Sendable {\n public let bounds: ClosedRange<Double>\n public let step: Double?\n\n public static func resolve(\n bounds: ClosedRange<Double>,\n step: Double?\n ) -> LumenSliderConfiguration {\n let safeStep = step.flatMap { value in\n value.isFinite && value > 0 ? value : nil\n }\n\n return LumenSliderConfiguration(bounds: bounds, step: safeStep)\n }\n}\n\npublic struct LumenSlider: View {\n @Binding private var value: Double\n @Environment(\\.lumenTheme) private var theme\n\n private let configuration: LumenSliderConfiguration\n private let label: LocalizedStringKey\n private let valueLabel: String?\n\n public init(\n _ label: LocalizedStringKey,\n value: Binding<Double>,\n in bounds: ClosedRange<Double>,\n step: Double? = nil,\n valueLabel: String? = nil\n ) {\n self.label = label\n _value = value\n configuration = LumenSliderConfiguration.resolve(bounds: bounds, step: step)\n self.valueLabel = valueLabel\n }\n\n public var body: some View {\n VStack(alignment: .leading, spacing: LumenSpacing.sm) {\n HStack(alignment: .firstTextBaseline, spacing: LumenSpacing.md) {\n Text(label)\n .font(.callout.weight(.semibold))\n .foregroundStyle(theme.colors.ink)\n\n Spacer(minLength: LumenSpacing.md)\n\n if let valueLabel {\n Text(valueLabel)\n .font(.caption.monospacedDigit())\n .foregroundStyle(theme.colors.inkMuted)\n }\n }\n\n slider\n .tint(theme.colors.brandSolid)\n }\n }\n\n @ViewBuilder\n private var slider: some View {\n if let step = configuration.step {\n Slider(\n value: $value,\n in: configuration.bounds,\n step: step,\n label: { Text(label) }\n )\n } else {\n Slider(\n value: $value,\n in: configuration.bounds,\n label: { Text(label) }\n )\n }\n }\n}\n\npublic struct LumenSearchField: View {\n @Binding private var text: String\n @Environment(\\.isEnabled) private var isEnabled\n @Environment(\\.lumenControlDensity) private var density\n @Environment(\\.lumenTheme) private var theme\n\n private let prompt: String\n\n public init(_ prompt: String = \"Search\", text: Binding<String>) {\n self.prompt = prompt\n _text = text\n }\n\n public var body: some View {\n HStack(spacing: LumenSpacing.sm) {\n LumenIcon(systemName: \"magnifyingglass\", size: .sm, color: theme.colors.inkMuted)\n\n TextField(prompt, text: $text)\n .textFieldStyle(.plain)\n .foregroundStyle(theme.colors.ink)\n\n if !text.isEmpty {\n Button {\n text = \"\"\n } label: {\n Image(systemName: \"xmark.circle.fill\")\n .foregroundStyle(theme.colors.inkMuted)\n }\n .buttonStyle(.plain)\n .accessibilityLabel(\"Clear search\")\n }\n }\n .frame(minHeight: LumenButtonMetrics.resolve(.md, density: density).minHeight)\n .padding(.horizontal, LumenSpacing.md)\n .background(theme.colors.surface)\n .overlay {\n RoundedRectangle(cornerRadius: LumenRadius.sm, style: .continuous)\n .stroke(theme.colors.line, lineWidth: 1)\n }\n .clipShape(RoundedRectangle(cornerRadius: LumenRadius.sm, style: .continuous))\n .opacity(isEnabled ? 1 : 0.52)\n }\n}\n#endif\n",
38792
39651
  "packages/swift/Sources/LumenUI/LumenTheme.swift": "import SwiftUI\n\npublic enum LumenColorScheme: Sendable {\n case dark\n case light\n}\n\npublic struct LumenTheme: Sendable {\n public let colors: LumenColorPalette\n public let scheme: LumenColorScheme\n\n public init(colors: LumenColorPalette, scheme: LumenColorScheme) {\n self.colors = colors\n self.scheme = scheme\n }\n\n public static let light = LumenTheme(colors: LumenColors.light, scheme: .light)\n public static let dark = LumenTheme(colors: LumenColors.dark, scheme: .dark)\n}\n\nextension LumenTheme {\n func resolvedPreferredColorScheme(enforceColorScheme: Bool) -> ColorScheme? {\n guard enforceColorScheme else { return nil }\n\n return scheme == .dark ? .dark : .light\n }\n}\n\nprivate struct LumenThemeKey: EnvironmentKey {\n static let defaultValue = LumenTheme.light\n}\n\npublic extension EnvironmentValues {\n var lumenTheme: LumenTheme {\n get { self[LumenThemeKey.self] }\n set { self[LumenThemeKey.self] = newValue }\n }\n}\n\npublic extension View {\n func lumenTheme(_ theme: LumenTheme, enforceColorScheme: Bool = true) -> some View {\n environment(\\.lumenTheme, theme)\n .preferredColorScheme(theme.resolvedPreferredColorScheme(enforceColorScheme: enforceColorScheme))\n }\n}\n",
38793
39652
  "packages/swift/Sources/LumenUI/MacComponents.swift": "#if os(macOS)\nimport AppKit\nimport SwiftUI\n\npublic struct LumenShortcut: Hashable {\n public let display: String\n public let keyCode: UInt16\n public let modifiers: NSEvent.ModifierFlags\n\n public init(\n keyCode: UInt16,\n modifiers: NSEvent.ModifierFlags,\n display: String? = nil\n ) {\n self.keyCode = keyCode\n self.modifiers = modifiers.intersection(.deviceIndependentFlagsMask)\n self.display = display ?? Self.format(\n keyCode: keyCode,\n modifiers: modifiers,\n characters: nil\n )\n }\n\n public func hash(into hasher: inout Hasher) {\n hasher.combine(keyCode)\n hasher.combine(modifiers.rawValue)\n }\n\n public static func == (lhs: LumenShortcut, rhs: LumenShortcut) -> Bool {\n lhs.keyCode == rhs.keyCode && lhs.modifiers.rawValue == rhs.modifiers.rawValue\n }\n\n static func from(_ event: NSEvent) -> LumenShortcut {\n LumenShortcut(\n keyCode: event.keyCode,\n modifiers: event.modifierFlags,\n display: format(\n keyCode: event.keyCode,\n modifiers: event.modifierFlags,\n characters: event.charactersIgnoringModifiers\n )\n )\n }\n\n static func format(\n keyCode: UInt16,\n modifiers: NSEvent.ModifierFlags,\n characters: String?\n ) -> String {\n let flags = modifiers.intersection(.deviceIndependentFlagsMask)\n var parts: [String] = []\n\n if flags.contains(.control) { parts.append(\"⌃\") }\n if flags.contains(.option) { parts.append(\"⌥\") }\n if flags.contains(.shift) { parts.append(\"⇧\") }\n if flags.contains(.command) { parts.append(\"⌘\") }\n\n let specialKeys: [UInt16: String] = [\n 36: \"↩\",\n 48: \"⇥\",\n 49: \"Space\",\n 51: \"⌫\",\n 115: \"↖\",\n 116: \"⇞\",\n 119: \"↘\",\n 121: \"⇟\",\n 123: \"←\",\n 124: \"→\",\n 125: \"↓\",\n 126: \"↑\",\n ]\n\n if let specialKey = specialKeys[keyCode] {\n parts.append(specialKey)\n } else if let characters, let character = characters.first {\n parts.append(String(character).uppercased())\n } else {\n parts.append(\"Key \\(keyCode)\")\n }\n\n return parts.joined()\n }\n}\n\n@MainActor\nprivate final class LumenShortcutRecorderState: ObservableObject {\n @Published var errorMessage: String?\n @Published var isRecording = false\n var monitor: Any?\n}\n\npublic struct LumenShortcutRecorder: View {\n @Binding private var shortcut: LumenShortcut?\n @Environment(\\.lumenTheme) private var theme\n @StateObject private var state = LumenShortcutRecorderState()\n\n private let label: LocalizedStringKey\n private let validation: ((LumenShortcut) -> String?)?\n\n public init(\n _ label: LocalizedStringKey,\n shortcut: Binding<LumenShortcut?>,\n validation: ((LumenShortcut) -> String?)? = nil\n ) {\n self.label = label\n _shortcut = shortcut\n self.validation = validation\n }\n\n public var body: some View {\n VStack(alignment: .leading, spacing: LumenSpacing.sm) {\n HStack(spacing: LumenSpacing.md) {\n Text(label)\n .font(.callout.weight(.semibold))\n .foregroundStyle(theme.colors.ink)\n\n Spacer(minLength: LumenSpacing.md)\n\n if state.isRecording {\n Text(\"Press keys…\")\n .font(.caption.monospaced())\n .foregroundStyle(theme.colors.inkMuted)\n\n LumenButton(\"Cancel\", intent: .quiet, size: .sm) {\n stopRecording()\n }\n } else {\n if let shortcut {\n Text(shortcut.display)\n .font(.caption.weight(.semibold).monospaced())\n .foregroundStyle(theme.colors.ink)\n .padding(.horizontal, LumenSpacing.sm)\n .padding(.vertical, LumenSpacing.xs)\n .background(theme.colors.surfaceStrong)\n .clipShape(RoundedRectangle(cornerRadius: LumenRadius.sm))\n .accessibilityLabel(\"Current shortcut \\(shortcut.display)\")\n }\n\n LumenButton(shortcut == nil ? \"Record…\" : \"Change…\", intent: .secondary, size: .sm) {\n startRecording()\n }\n\n if shortcut != nil {\n LumenButton(\"Clear\", intent: .quiet, size: .sm) {\n shortcut = nil\n state.errorMessage = nil\n }\n }\n }\n }\n\n if let errorMessage = state.errorMessage {\n HStack(spacing: LumenSpacing.sm) {\n LumenIcon(\n systemName: \"exclamationmark.triangle\",\n size: .sm,\n color: theme.colors.warning\n )\n Text(errorMessage)\n .font(.caption)\n .foregroundStyle(theme.colors.warning)\n }\n .accessibilityElement(children: .combine)\n }\n }\n .onDisappear {\n stopRecording()\n }\n }\n\n private func startRecording() {\n guard state.monitor == nil else { return }\n\n state.errorMessage = nil\n state.isRecording = true\n state.monitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in\n handle(event)\n return nil\n }\n }\n\n private func stopRecording() {\n if let monitor = state.monitor {\n NSEvent.removeMonitor(monitor)\n }\n state.monitor = nil\n state.isRecording = false\n }\n\n private func handle(_ event: NSEvent) {\n defer { stopRecording() }\n\n if event.keyCode == 53 {\n return\n }\n\n let shortcut = LumenShortcut.from(event)\n let commandModifiers: NSEvent.ModifierFlags = [.command, .control, .option, .shift]\n\n guard !shortcut.modifiers.intersection(commandModifiers).isEmpty else {\n state.errorMessage = \"Include at least one modifier key.\"\n return\n }\n\n if let validationMessage = validation?(shortcut) {\n state.errorMessage = validationMessage\n return\n }\n\n self.shortcut = shortcut\n state.errorMessage = nil\n }\n}\n\npublic struct LumenSymbolOption: Hashable, Identifiable, Sendable {\n public let category: String\n public let label: String\n public let name: String\n\n public var id: String { name }\n\n public init(name: String, label: String, category: String) {\n self.name = name\n self.label = label\n self.category = category\n }\n\n public static let common: [LumenSymbolOption] = [\n LumenSymbolOption(name: \"folder\", label: \"Folder\", category: \"Files\"),\n LumenSymbolOption(name: \"doc\", label: \"Document\", category: \"Files\"),\n LumenSymbolOption(name: \"tray\", label: \"Tray\", category: \"Files\"),\n LumenSymbolOption(name: \"archivebox\", label: \"Archive\", category: \"Files\"),\n LumenSymbolOption(name: \"desktopcomputer\", label: \"Desktop\", category: \"Devices\"),\n LumenSymbolOption(name: \"laptopcomputer\", label: \"Laptop\", category: \"Devices\"),\n LumenSymbolOption(name: \"display\", label: \"Display\", category: \"Devices\"),\n LumenSymbolOption(name: \"iphone\", label: \"Phone\", category: \"Devices\"),\n LumenSymbolOption(name: \"hammer\", label: \"Build\", category: \"Work\"),\n LumenSymbolOption(name: \"wrench.and.screwdriver\", label: \"Tools\", category: \"Work\"),\n LumenSymbolOption(name: \"briefcase\", label: \"Business\", category: \"Work\"),\n LumenSymbolOption(name: \"chart.bar\", label: \"Analytics\", category: \"Work\"),\n LumenSymbolOption(name: \"person.2\", label: \"People\", category: \"People\"),\n LumenSymbolOption(name: \"bubble.left.and.bubble.right\", label: \"Messages\", category: \"People\"),\n LumenSymbolOption(name: \"video\", label: \"Video\", category: \"People\"),\n LumenSymbolOption(name: \"calendar\", label: \"Calendar\", category: \"Planning\"),\n LumenSymbolOption(name: \"checklist\", label: \"Tasks\", category: \"Planning\"),\n LumenSymbolOption(name: \"clock\", label: \"Time\", category: \"Planning\"),\n LumenSymbolOption(name: \"flag\", label: \"Milestone\", category: \"Planning\"),\n LumenSymbolOption(name: \"star\", label: \"Favorite\", category: \"General\"),\n LumenSymbolOption(name: \"heart\", label: \"Health\", category: \"General\"),\n LumenSymbolOption(name: \"house\", label: \"Home\", category: \"General\"),\n LumenSymbolOption(name: \"gearshape\", label: \"Settings\", category: \"General\"),\n ]\n}\n\npublic struct LumenSymbolPicker: View {\n @Binding private var selectedName: String\n @Environment(\\.lumenTheme) private var theme\n @State private var query = \"\"\n\n private let options: [LumenSymbolOption]\n private let title: LocalizedStringKey\n\n public init(\n _ title: LocalizedStringKey = \"Choose a symbol\",\n selectedName: Binding<String>,\n options: [LumenSymbolOption] = LumenSymbolOption.common\n ) {\n self.title = title\n _selectedName = selectedName\n self.options = options\n }\n\n public var body: some View {\n VStack(alignment: .leading, spacing: LumenSpacing.md) {\n Text(title)\n .font(.headline)\n .foregroundStyle(theme.colors.ink)\n\n LumenSearchField(\"Search symbols\", text: $query)\n\n LumenDivider()\n\n if matchingOptions.isEmpty {\n LumenEmptyState(\n \"No symbols found\",\n systemName: \"magnifyingglass\",\n description: \"Try a different search term.\"\n )\n } else {\n ScrollView {\n LazyVStack(alignment: .leading, spacing: LumenSpacing.lg) {\n ForEach(categories, id: \\.self) { category in\n VStack(alignment: .leading, spacing: LumenSpacing.sm) {\n Text(category)\n .font(.caption.weight(.semibold))\n .foregroundStyle(theme.colors.inkMuted)\n\n LazyVGrid(columns: columns, alignment: .leading, spacing: LumenSpacing.sm) {\n ForEach(matchingOptions.filter { $0.category == category }) { option in\n symbolButton(option)\n }\n }\n }\n }\n }\n }\n }\n }\n }\n\n private var categories: [String] {\n options.map(\\.category).reduce(into: []) { result, category in\n if !result.contains(category) && matchingOptions.contains(where: { $0.category == category }) {\n result.append(category)\n }\n }\n }\n\n private var columns: [GridItem] {\n [GridItem(.adaptive(minimum: 64, maximum: 88), spacing: LumenSpacing.sm)]\n }\n\n private var matchingOptions: [LumenSymbolOption] {\n guard !query.isEmpty else { return options }\n\n return options.filter {\n $0.label.localizedCaseInsensitiveContains(query)\n || $0.name.localizedCaseInsensitiveContains(query)\n || $0.category.localizedCaseInsensitiveContains(query)\n }\n }\n\n private func symbolButton(_ option: LumenSymbolOption) -> some View {\n let isSelected = selectedName == option.name\n\n return Button {\n selectedName = option.name\n } label: {\n VStack(spacing: LumenSpacing.xs) {\n LumenIcon(\n systemName: option.name,\n size: .md,\n color: isSelected ? theme.colors.brand : theme.colors.inkSoft\n )\n Text(option.label)\n .font(.caption2)\n .lineLimit(1)\n }\n .frame(maxWidth: .infinity, minHeight: 56)\n .padding(.horizontal, LumenSpacing.xs)\n .background(isSelected ? theme.colors.brandSoft : theme.colors.surfaceMuted)\n .overlay {\n RoundedRectangle(cornerRadius: LumenRadius.sm, style: .continuous)\n .stroke(isSelected ? theme.colors.brand : theme.colors.line, lineWidth: 1)\n }\n .clipShape(RoundedRectangle(cornerRadius: LumenRadius.sm, style: .continuous))\n }\n .buttonStyle(.plain)\n .help(option.label)\n .accessibilityLabel(option.label)\n .accessibilityAddTraits(isSelected ? .isSelected : [])\n }\n}\n\npublic struct LumenSymbolPickerButton: View {\n @Binding private var selectedName: String\n @Environment(\\.lumenTheme) private var theme\n @State private var isPresented = false\n\n private let label: LocalizedStringKey\n private let options: [LumenSymbolOption]\n\n public init(\n _ label: LocalizedStringKey = \"Choose a symbol\",\n selectedName: Binding<String>,\n options: [LumenSymbolOption] = LumenSymbolOption.common\n ) {\n self.label = label\n _selectedName = selectedName\n self.options = options\n }\n\n public var body: some View {\n Button {\n isPresented = true\n } label: {\n ZStack(alignment: .bottomTrailing) {\n LumenIcon(systemName: selectedName, size: .lg, color: theme.colors.brand)\n .frame(width: 42, height: 42)\n .background(theme.colors.brandSoft)\n .clipShape(RoundedRectangle(cornerRadius: LumenRadius.md, style: .continuous))\n\n Image(systemName: \"pencil.circle.fill\")\n .font(.system(size: 14, weight: .semibold))\n .foregroundStyle(theme.colors.onBrand, theme.colors.brandSolid)\n .offset(x: 4, y: 4)\n }\n }\n .buttonStyle(.plain)\n .accessibilityLabel(label)\n .help(Text(label))\n .popover(isPresented: $isPresented, arrowEdge: .bottom) {\n LumenSymbolPicker(label, selectedName: $selectedName, options: options)\n .padding(LumenSpacing.lg)\n .frame(width: 400, height: 480)\n .lumenTheme(theme)\n }\n }\n}\n#endif\n",
38794
- "packages/swift/Sources/LumenUI/NavigationComponents.swift": "import SwiftUI\n\npublic struct LumenNavigationItem<Value: Hashable>: Identifiable {\n public let disabled: Bool\n public let label: LocalizedStringKey\n public let selectedSystemName: String?\n public let systemName: String?\n public let value: Value\n\n public var id: Value { value }\n\n public init(\n _ label: LocalizedStringKey,\n value: Value,\n systemName: String? = nil,\n selectedSystemName: String? = nil,\n disabled: Bool = false\n ) {\n self.label = label\n self.value = value\n self.systemName = systemName\n self.selectedSystemName = selectedSystemName\n self.disabled = disabled\n }\n}\n\n/// A controlled destination bar that leaves navigation paths and presented content app-owned.\npublic struct LumenNavigationBar<Selection: Hashable>: View {\n @Binding private var selection: Selection\n @Environment(\\.lumenTheme) private var theme\n\n private let items: [LumenNavigationItem<Selection>]\n private let label: LocalizedStringKey\n\n public init(\n _ label: LocalizedStringKey = \"Primary navigation\",\n selection: Binding<Selection>,\n items: [LumenNavigationItem<Selection>]\n ) {\n self.label = label\n _selection = selection\n self.items = items\n }\n\n public var body: some View {\n HStack(spacing: LumenSpacing.xs) {\n ForEach(items) { item in\n let selected = selection == item.value\n\n Button {\n selection = item.value\n } label: {\n VStack(spacing: LumenSpacing.xs) {\n if let systemName = selected ? item.selectedSystemName ?? item.systemName : item.systemName {\n LumenIcon(\n systemName: systemName,\n size: .md,\n color: selected ? theme.colors.brand : theme.colors.inkMuted\n )\n }\n\n Text(item.label)\n .font(.caption.weight(selected ? .semibold : .medium))\n .foregroundStyle(selected ? theme.colors.brand : theme.colors.inkMuted)\n .lineLimit(1)\n }\n .frame(maxWidth: .infinity, minHeight: 48)\n .contentShape(Rectangle())\n }\n .buttonStyle(.plain)\n .disabled(item.disabled)\n .opacity(item.disabled ? 0.52 : 1)\n .accessibilityAddTraits(selected ? .isSelected : [])\n }\n }\n .padding(.horizontal, LumenSpacing.sm)\n .padding(.vertical, LumenSpacing.sm)\n .background(theme.colors.surface)\n .overlay(alignment: .top) {\n Rectangle()\n .fill(theme.colors.line)\n .frame(height: 1)\n .accessibilityHidden(true)\n }\n .accessibilityElement(children: .contain)\n .accessibilityLabel(label)\n }\n}\n",
38795
- "packages/swift/Sources/LumenUI/OverlayComponents.swift": "import CoreTransferable\nimport SwiftUI\n\nprivate struct LumenAlertDialogModifier: ViewModifier {\n @Binding var isPresented: Bool\n\n let cancelLabel: LocalizedStringKey\n let confirmDisabled: Bool\n let confirmLabel: LocalizedStringKey\n let confirmLoading: Bool\n let confirmRole: ButtonRole?\n let description: LocalizedStringKey?\n let onConfirm: () -> Void\n let title: LocalizedStringKey\n\n func body(content: Content) -> some View {\n content.alert(title, isPresented: $isPresented) {\n Button(cancelLabel, role: .cancel) {}\n Button(role: confirmRole, action: onConfirm) {\n HStack(spacing: LumenSpacing.sm) {\n if confirmLoading {\n ProgressView()\n .controlSize(.small)\n }\n Text(confirmLabel)\n }\n }\n .disabled(confirmDisabled || confirmLoading)\n .accessibilityValue(confirmLoading ? Text(\"Loading\") : Text(\"\"))\n } message: {\n if let description {\n Text(description)\n }\n }\n }\n}\n\npublic extension View {\n /// Presents a controlled native confirmation alert with explicit cancel and confirm actions.\n func lumenAlertDialog(\n isPresented: Binding<Bool>,\n title: LocalizedStringKey,\n description: LocalizedStringKey? = nil,\n confirmLabel: LocalizedStringKey,\n cancelLabel: LocalizedStringKey = \"Cancel\",\n confirmRole: ButtonRole? = nil,\n confirmDisabled: Bool = false,\n confirmLoading: Bool = false,\n onConfirm: @escaping () -> Void\n ) -> some View {\n modifier(\n LumenAlertDialogModifier(\n isPresented: isPresented,\n cancelLabel: cancelLabel,\n confirmDisabled: confirmDisabled,\n confirmLabel: confirmLabel,\n confirmLoading: confirmLoading,\n confirmRole: confirmRole,\n description: description,\n onConfirm: onConfirm,\n title: title\n )\n )\n }\n}\n\nprivate struct LumenSheetModifier<SheetContent: View, Actions: View>: ViewModifier {\n @Binding var isPresented: Bool\n\n let actions: Actions\n let description: LocalizedStringKey?\n let onDismiss: () -> Void\n let sheetContent: SheetContent\n let title: LocalizedStringKey?\n\n func body(content: Content) -> some View {\n content.sheet(isPresented: $isPresented, onDismiss: onDismiss) {\n VStack(alignment: .leading, spacing: LumenSpacing.lg) {\n if title != nil || description != nil {\n VStack(alignment: .leading, spacing: LumenSpacing.sm) {\n if let title {\n Text(title)\n .font(.title2.weight(.semibold))\n .accessibilityAddTraits(.isHeader)\n }\n if let description {\n Text(description)\n .font(.callout)\n .foregroundStyle(.secondary)\n }\n }\n }\n\n sheetContent\n\n actions\n .frame(maxWidth: .infinity, alignment: .trailing)\n }\n .padding(LumenSpacing.xl)\n }\n }\n}\n\npublic extension View {\n /// Presents application-owned content in a native sheet with an optional heading and actions.\n func lumenSheet<SheetContent: View, Actions: View>(\n isPresented: Binding<Bool>,\n title: LocalizedStringKey? = nil,\n description: LocalizedStringKey? = nil,\n onDismiss: @escaping () -> Void = {},\n @ViewBuilder actions: () -> Actions,\n @ViewBuilder content: () -> SheetContent\n ) -> some View {\n modifier(\n LumenSheetModifier(\n isPresented: isPresented,\n actions: actions(),\n description: description,\n onDismiss: onDismiss,\n sheetContent: content(),\n title: title\n )\n )\n }\n\n func lumenSheet<SheetContent: View>(\n isPresented: Binding<Bool>,\n title: LocalizedStringKey? = nil,\n description: LocalizedStringKey? = nil,\n onDismiss: @escaping () -> Void = {},\n @ViewBuilder content: () -> SheetContent\n ) -> some View {\n lumenSheet(\n isPresented: isPresented,\n title: title,\n description: description,\n onDismiss: onDismiss,\n actions: { EmptyView() },\n content: content\n )\n }\n}\n\npublic struct LumenMenuItem {\n fileprivate let action: () -> Void\n fileprivate let disabled: Bool\n fileprivate let role: ButtonRole?\n fileprivate let systemName: String?\n fileprivate let title: LocalizedStringKey\n\n public init(\n _ title: LocalizedStringKey,\n systemName: String? = nil,\n role: ButtonRole? = nil,\n disabled: Bool = false,\n action: @escaping () -> Void\n ) {\n self.title = title\n self.systemName = systemName\n self.role = role\n self.disabled = disabled\n self.action = action\n }\n}\n\n/// A native anchored menu that preserves disabled and destructive action semantics.\npublic struct LumenMenu<MenuLabel: View>: View {\n private let items: [LumenMenuItem]\n private let label: MenuLabel\n\n public init(\n items: [LumenMenuItem],\n @ViewBuilder label: () -> MenuLabel\n ) {\n self.items = items\n self.label = label()\n }\n\n public var body: some View {\n Menu {\n ForEach(Array(items.enumerated()), id: \\.offset) { _, item in\n Button(role: item.role, action: item.action) {\n if let systemName = item.systemName {\n SwiftUI.Label(item.title, systemImage: systemName)\n } else {\n Text(item.title)\n }\n }\n .disabled(item.disabled)\n }\n } label: {\n label\n }\n }\n}\n\n/// A Lumen-styled button backed by SwiftUI's native ShareLink presentation.\npublic struct LumenShareButton: View {\n private let shareLink: AnyView\n\n public init<Label: View>(\n item: String,\n @ViewBuilder label: () -> Label\n ) {\n shareLink = AnyView(\n ShareLink(item: item, label: label)\n .buttonStyle(LumenButtonStyle(intent: .secondary))\n )\n }\n\n public init<Label: View>(\n item: URL,\n @ViewBuilder label: () -> Label\n ) {\n shareLink = AnyView(\n ShareLink(item: item, label: label)\n .buttonStyle(LumenButtonStyle(intent: .secondary))\n )\n }\n\n @available(tvOS, unavailable)\n public init<\n Item: Transferable,\n PreviewImage: Transferable,\n PreviewIcon: Transferable,\n Label: View\n >(\n item: Item,\n preview: SharePreview<PreviewImage, PreviewIcon>,\n @ViewBuilder label: () -> Label\n ) {\n shareLink = AnyView(\n ShareLink(item: item, preview: preview, label: label)\n .buttonStyle(LumenButtonStyle(intent: .secondary))\n )\n }\n\n public init(_ label: LocalizedStringKey = \"Share\", item: String) {\n self.init(item: item) {\n Text(label)\n }\n }\n\n public init(_ label: LocalizedStringKey = \"Share\", item: URL) {\n self.init(item: item) {\n Text(label)\n }\n }\n\n public var body: some View {\n shareLink\n }\n}\n",
39653
+ "packages/swift/Sources/LumenUI/NavigationComponents.swift": "import SwiftUI\n\n/// Cross-version policy for the native SwiftUI tab bar on iPhone.\npublic enum LumenTabBarMinimizeBehavior: Sendable {\n /// Let SwiftUI choose the platform-appropriate behavior.\n case automatic\n /// Keep the tab bar at its regular size.\n case never\n /// Minimize while scrolling toward later content and expand in the opposite direction.\n case onScrollDown\n /// Minimize while scrolling toward earlier content and expand in the opposite direction.\n case onScrollUp\n}\n\n/// Content that adapts to the expanded or inline placement of an iOS tab-view bottom accessory.\npublic struct LumenTabAccessory<Expanded: View, Compact: View>: View {\n private let compact: Compact\n private let expanded: Expanded\n\n public init(\n @ViewBuilder expanded: () -> Expanded,\n @ViewBuilder compact: () -> Compact\n ) {\n self.expanded = expanded()\n self.compact = compact()\n }\n\n public var body: some View {\n #if os(iOS) && compiler(>=6.2)\n if #available(iOS 26.0, *) {\n LumenAdaptiveTabAccessory(expanded: expanded, compact: compact)\n } else {\n expanded\n }\n #else\n expanded\n #endif\n }\n}\n\npublic extension View {\n /// Applies SwiftUI's native tab-bar minimization on iOS 26 and later.\n ///\n /// Earlier iOS versions and other Apple platforms retain their system-default tab behavior.\n @ViewBuilder\n func lumenTabBarMinimizeBehavior(\n _ behavior: LumenTabBarMinimizeBehavior\n ) -> some View {\n #if os(iOS) && compiler(>=6.2)\n if #available(iOS 26.0, *) {\n tabBarMinimizeBehavior(behavior.swiftUIValue)\n } else {\n self\n }\n #else\n self\n #endif\n }\n\n /// Adds an adaptive bottom accessory to a native `TabView`.\n ///\n /// iOS 26 uses the native tab accessory placement. Earlier versions present the same content\n /// in a token-aware safe-area inset above the system tab bar.\n @ViewBuilder\n func lumenTabViewBottomAccessory<Accessory: View>(\n isEnabled: Bool = true,\n @ViewBuilder content: () -> Accessory\n ) -> some View {\n #if os(iOS)\n #if compiler(>=6.2)\n if #available(iOS 26.0, *) {\n if isEnabled {\n tabViewBottomAccessory {\n content()\n }\n } else {\n self\n }\n } else {\n safeAreaInset(edge: .bottom, spacing: 0) {\n if isEnabled {\n LumenLegacyTabAccessorySurface {\n content()\n }\n }\n }\n }\n #else\n safeAreaInset(edge: .bottom, spacing: 0) {\n if isEnabled {\n LumenLegacyTabAccessorySurface {\n content()\n }\n }\n }\n #endif\n #else\n self\n #endif\n }\n}\n\n#if os(iOS) && compiler(>=6.2)\n@available(iOS 26.0, *)\nprivate extension LumenTabBarMinimizeBehavior {\n var swiftUIValue: TabBarMinimizeBehavior {\n switch self {\n case .automatic: .automatic\n case .never: .never\n case .onScrollDown: .onScrollDown\n case .onScrollUp: .onScrollUp\n }\n }\n}\n\n@available(iOS 26.0, *)\nprivate struct LumenAdaptiveTabAccessory<Expanded: View, Compact: View>: View {\n @Environment(\\.tabViewBottomAccessoryPlacement) private var placement\n\n let expanded: Expanded\n let compact: Compact\n\n var body: some View {\n if placement == .inline {\n compact\n } else {\n expanded\n }\n }\n}\n#endif\n\n#if os(iOS)\nprivate struct LumenLegacyTabAccessorySurface<Content: View>: View {\n @Environment(\\.lumenTheme) private var theme\n\n let content: Content\n\n init(@ViewBuilder content: () -> Content) {\n self.content = content()\n }\n\n var body: some View {\n content\n .frame(maxWidth: .infinity)\n .padding(.horizontal, LumenSpacing.lg)\n .padding(.vertical, LumenSpacing.sm)\n .background(theme.colors.surface)\n .overlay(alignment: .top) {\n Rectangle()\n .fill(theme.colors.line)\n .frame(height: 1)\n .accessibilityHidden(true)\n }\n }\n}\n#endif\n\npublic struct LumenNavigationItem<Value: Hashable>: Identifiable {\n public let badge: LumenNavigationBadge?\n public let disabled: Bool\n public let label: LocalizedStringKey\n public let selectedSystemName: String?\n public let systemName: String?\n public let value: Value\n\n public var id: Value { value }\n\n public init(\n _ label: LocalizedStringKey,\n value: Value,\n systemName: String? = nil,\n selectedSystemName: String? = nil,\n badge: LumenNavigationBadge? = nil,\n disabled: Bool = false\n ) {\n self.label = label\n self.value = value\n self.systemName = systemName\n self.selectedSystemName = selectedSystemName\n self.badge = badge\n self.disabled = disabled\n }\n}\n\n/// Compact destination status presented as a dot or short text value.\npublic enum LumenNavigationBadge: Equatable, Sendable {\n case dot(accessibilityLabel: String = \"New activity\")\n case text(String, accessibilityLabel: String? = nil)\n\n /// Produces a compact count capped at `99+` while retaining the full count for accessibility.\n public static func count(_ value: Int) -> LumenNavigationBadge {\n let safeValue = max(0, value)\n return .text(\n safeValue > 99 ? \"99+\" : String(safeValue),\n accessibilityLabel: \"\\(safeValue) new items\"\n )\n }\n\n internal var displayText: String? {\n switch self {\n case .dot: nil\n case let .text(value, _): value\n }\n }\n\n internal var spokenLabel: String {\n switch self {\n case let .dot(accessibilityLabel): accessibilityLabel\n case let .text(value, accessibilityLabel): accessibilityLabel ?? value\n }\n }\n}\n\ninternal func dispatchLumenNavigationSelection<Value: Equatable>(\n itemValue: Value,\n selectedValue: Value,\n onValueChange: (Value) -> Void,\n onReselect: ((Value) -> Void)?\n) {\n if itemValue == selectedValue, let onReselect {\n onReselect(itemValue)\n } else {\n onValueChange(itemValue)\n }\n}\n\n/// A controlled destination bar that leaves navigation paths and presented content app-owned.\npublic struct LumenNavigationBar<Selection: Hashable>: View {\n @Binding private var selection: Selection\n @Environment(\\.lumenTheme) private var theme\n\n private let items: [LumenNavigationItem<Selection>]\n private let label: LocalizedStringKey\n private let onReselect: ((Selection) -> Void)?\n\n public init(\n _ label: LocalizedStringKey = \"Primary navigation\",\n selection: Binding<Selection>,\n items: [LumenNavigationItem<Selection>],\n onReselect: ((Selection) -> Void)? = nil\n ) {\n self.label = label\n _selection = selection\n self.items = items\n self.onReselect = onReselect\n }\n\n public var body: some View {\n HStack(spacing: LumenSpacing.xs) {\n ForEach(items) { item in\n let selected = selection == item.value\n\n Button {\n dispatchLumenNavigationSelection(\n itemValue: item.value,\n selectedValue: selection,\n onValueChange: { selection = $0 },\n onReselect: onReselect\n )\n } label: {\n VStack(spacing: LumenSpacing.xs) {\n if let systemName = selected ? item.selectedSystemName ?? item.systemName : item.systemName {\n ZStack(alignment: .topTrailing) {\n LumenIcon(\n systemName: systemName,\n size: .md,\n color: selected ? theme.colors.brand : theme.colors.inkMuted\n )\n\n if let badge = item.badge {\n LumenNavigationBadgeView(badge: badge)\n .offset(x: 8, y: -6)\n }\n }\n }\n\n Text(item.label)\n .font(.caption.weight(selected ? .semibold : .medium))\n .foregroundStyle(selected ? theme.colors.brand : theme.colors.inkMuted)\n .lineLimit(1)\n }\n .frame(maxWidth: .infinity, minHeight: 48)\n .contentShape(Rectangle())\n }\n .buttonStyle(.plain)\n .disabled(item.disabled)\n .opacity(item.disabled ? 0.52 : 1)\n .accessibilityAddTraits(selected ? .isSelected : [])\n .accessibilityValue(item.badge.map { Text($0.spokenLabel) } ?? Text(\"\"))\n }\n }\n .padding(.horizontal, LumenSpacing.sm)\n .padding(.vertical, LumenSpacing.sm)\n .background(theme.colors.surface)\n .overlay(alignment: .top) {\n Rectangle()\n .fill(theme.colors.line)\n .frame(height: 1)\n .accessibilityHidden(true)\n }\n .accessibilityElement(children: .contain)\n .accessibilityLabel(label)\n }\n}\n\nprivate struct LumenNavigationBadgeView: View {\n @Environment(\\.lumenTheme) private var theme\n\n let badge: LumenNavigationBadge\n\n var body: some View {\n Group {\n if let displayText = badge.displayText {\n Text(displayText)\n .font(.caption2.weight(.bold))\n .foregroundStyle(theme.colors.onDanger)\n .padding(.horizontal, 5)\n .frame(minWidth: 16, minHeight: 16)\n .lineLimit(1)\n } else {\n Circle()\n .fill(theme.colors.danger)\n .frame(width: 8, height: 8)\n }\n }\n .background {\n if badge.displayText != nil {\n Capsule().fill(theme.colors.danger)\n }\n }\n .accessibilityHidden(true)\n }\n}\n",
39654
+ "packages/swift/Sources/LumenUI/OverlayComponents.swift": "import CoreTransferable\nimport SwiftUI\n\nprivate struct LumenAlertDialogModifier: ViewModifier {\n @Binding var isPresented: Bool\n\n let cancelLabel: LocalizedStringKey\n let confirmDisabled: Bool\n let confirmLabel: LocalizedStringKey\n let confirmLoading: Bool\n let confirmRole: ButtonRole?\n let description: LocalizedStringKey?\n let onConfirm: () -> Void\n let title: LocalizedStringKey\n\n func body(content: Content) -> some View {\n content.alert(title, isPresented: $isPresented) {\n Button(cancelLabel, role: .cancel) {}\n Button(role: confirmRole, action: onConfirm) {\n HStack(spacing: LumenSpacing.sm) {\n if confirmLoading {\n ProgressView()\n .controlSize(.small)\n }\n Text(confirmLabel)\n }\n }\n .disabled(confirmDisabled || confirmLoading)\n .accessibilityValue(confirmLoading ? Text(\"Loading\") : Text(\"\"))\n } message: {\n if let description {\n Text(description)\n }\n }\n }\n}\n\npublic extension View {\n /// Presents a controlled native confirmation alert with explicit cancel and confirm actions.\n func lumenAlertDialog(\n isPresented: Binding<Bool>,\n title: LocalizedStringKey,\n description: LocalizedStringKey? = nil,\n confirmLabel: LocalizedStringKey,\n cancelLabel: LocalizedStringKey = \"Cancel\",\n confirmRole: ButtonRole? = nil,\n confirmDisabled: Bool = false,\n confirmLoading: Bool = false,\n onConfirm: @escaping () -> Void\n ) -> some View {\n modifier(\n LumenAlertDialogModifier(\n isPresented: isPresented,\n cancelLabel: cancelLabel,\n confirmDisabled: confirmDisabled,\n confirmLabel: confirmLabel,\n confirmLoading: confirmLoading,\n confirmRole: confirmRole,\n description: description,\n onConfirm: onConfirm,\n title: title\n )\n )\n }\n}\n\nprivate struct LumenSheetModifier<SheetContent: View, Actions: View>: ViewModifier {\n @Binding var isPresented: Bool\n\n let actions: Actions\n let description: LocalizedStringKey?\n let onDismiss: () -> Void\n let sheetContent: SheetContent\n let title: LocalizedStringKey?\n\n func body(content: Content) -> some View {\n content.sheet(isPresented: $isPresented, onDismiss: onDismiss) {\n VStack(alignment: .leading, spacing: LumenSpacing.lg) {\n if title != nil || description != nil {\n VStack(alignment: .leading, spacing: LumenSpacing.sm) {\n if let title {\n Text(title)\n .font(.title2.weight(.semibold))\n .accessibilityAddTraits(.isHeader)\n }\n if let description {\n Text(description)\n .font(.callout)\n .foregroundStyle(.secondary)\n }\n }\n }\n\n sheetContent\n\n actions\n .frame(maxWidth: .infinity, alignment: .trailing)\n }\n .padding(LumenSpacing.xl)\n }\n }\n}\n\npublic extension View {\n /// Presents application-owned content in a native sheet with an optional heading and actions.\n func lumenSheet<SheetContent: View, Actions: View>(\n isPresented: Binding<Bool>,\n title: LocalizedStringKey? = nil,\n description: LocalizedStringKey? = nil,\n onDismiss: @escaping () -> Void = {},\n @ViewBuilder actions: () -> Actions,\n @ViewBuilder content: () -> SheetContent\n ) -> some View {\n modifier(\n LumenSheetModifier(\n isPresented: isPresented,\n actions: actions(),\n description: description,\n onDismiss: onDismiss,\n sheetContent: content(),\n title: title\n )\n )\n }\n\n func lumenSheet<SheetContent: View>(\n isPresented: Binding<Bool>,\n title: LocalizedStringKey? = nil,\n description: LocalizedStringKey? = nil,\n onDismiss: @escaping () -> Void = {},\n @ViewBuilder content: () -> SheetContent\n ) -> some View {\n lumenSheet(\n isPresented: isPresented,\n title: title,\n description: description,\n onDismiss: onDismiss,\n actions: { EmptyView() },\n content: content\n )\n }\n}\n\npublic struct LumenMenuItem {\n fileprivate let action: () -> Void\n fileprivate let disabled: Bool\n fileprivate let role: ButtonRole?\n fileprivate let systemName: String?\n fileprivate let title: LocalizedStringKey\n\n public init(\n _ title: LocalizedStringKey,\n systemName: String? = nil,\n role: ButtonRole? = nil,\n disabled: Bool = false,\n action: @escaping () -> Void\n ) {\n self.title = title\n self.systemName = systemName\n self.role = role\n self.disabled = disabled\n self.action = action\n }\n}\n\n/// A native anchored menu that preserves disabled and destructive action semantics.\n#if !os(watchOS)\npublic struct LumenMenu<MenuLabel: View>: View {\n private let items: [LumenMenuItem]\n private let label: MenuLabel\n\n public init(\n items: [LumenMenuItem],\n @ViewBuilder label: () -> MenuLabel\n ) {\n self.items = items\n self.label = label()\n }\n\n public var body: some View {\n Menu {\n ForEach(Array(items.enumerated()), id: \\.offset) { _, item in\n Button(role: item.role, action: item.action) {\n if let systemName = item.systemName {\n SwiftUI.Label(item.title, systemImage: systemName)\n } else {\n Text(item.title)\n }\n }\n .disabled(item.disabled)\n }\n } label: {\n label\n }\n }\n}\n#endif\n\n/// A Lumen-styled button backed by SwiftUI's native ShareLink presentation.\npublic struct LumenShareButton: View {\n private let shareLink: AnyView\n\n public init<Label: View>(\n item: String,\n @ViewBuilder label: () -> Label\n ) {\n shareLink = AnyView(\n ShareLink(item: item, label: label)\n .buttonStyle(LumenButtonStyle(intent: .secondary))\n )\n }\n\n public init<Label: View>(\n item: URL,\n @ViewBuilder label: () -> Label\n ) {\n shareLink = AnyView(\n ShareLink(item: item, label: label)\n .buttonStyle(LumenButtonStyle(intent: .secondary))\n )\n }\n\n @available(tvOS, unavailable)\n public init<\n Item: Transferable,\n PreviewImage: Transferable,\n PreviewIcon: Transferable,\n Label: View\n >(\n item: Item,\n preview: SharePreview<PreviewImage, PreviewIcon>,\n @ViewBuilder label: () -> Label\n ) {\n shareLink = AnyView(\n ShareLink(item: item, preview: preview, label: label)\n .buttonStyle(LumenButtonStyle(intent: .secondary))\n )\n }\n\n public init(_ label: LocalizedStringKey = \"Share\", item: String) {\n self.init(item: item) {\n Text(label)\n }\n }\n\n public init(_ label: LocalizedStringKey = \"Share\", item: URL) {\n self.init(item: item) {\n Text(label)\n }\n }\n\n public var body: some View {\n shareLink\n }\n}\n",
38796
39655
  "packages/swift/Sources/LumenUI/Platform.swift": "import SwiftUI\n\n/// The amount of space Lumen controls reserve for pointer or touch interaction.\npublic enum LumenControlDensity: Equatable, Sendable {\n /// Compact controls sized for pointer-first macOS interfaces.\n case compact\n /// Larger controls sized for touch-first iOS interfaces.\n case regular\n\n public static var platformDefault: LumenControlDensity {\n #if os(macOS) || targetEnvironment(macCatalyst)\n .compact\n #else\n .regular\n #endif\n }\n}\n\nprivate struct LumenControlDensityKey: EnvironmentKey {\n static let defaultValue = LumenControlDensity.platformDefault\n}\n\npublic extension EnvironmentValues {\n var lumenControlDensity: LumenControlDensity {\n get { self[LumenControlDensityKey.self] }\n set { self[LumenControlDensityKey.self] = newValue }\n }\n}\n\npublic extension View {\n /// Overrides Lumen's automatic macOS or iOS control density for this view hierarchy.\n func lumenControlDensity(_ density: LumenControlDensity) -> some View {\n environment(\\.lumenControlDensity, density)\n }\n}\n",
38797
39656
  "packages/swift/Sources/LumenUI/Primitives.swift": "import SwiftUI\n\npublic enum LumenTextTone: Sendable {\n case danger\n case `default`\n case muted\n case soft\n case success\n case warning\n}\n\npublic enum LumenTextVariant: Sendable {\n case body\n case caption\n case label\n case title\n}\n\npublic struct LumenText: View {\n @Environment(\\.lumenTheme) private var theme\n\n private let content: LocalizedStringKey\n private let tone: LumenTextTone\n private let variant: LumenTextVariant\n\n public init(\n _ content: LocalizedStringKey,\n variant: LumenTextVariant = .body,\n tone: LumenTextTone = .default\n ) {\n self.content = content\n self.variant = variant\n self.tone = tone\n }\n\n public var body: some View {\n Text(content)\n .font(font)\n .foregroundStyle(foregroundColor)\n }\n\n private var font: Font {\n switch variant {\n case .body: .body\n case .caption: .caption\n case .label: .callout.weight(.semibold)\n case .title: .title2.weight(.bold)\n }\n }\n\n private var foregroundColor: Color {\n switch tone {\n case .danger: theme.colors.danger\n case .default: theme.colors.ink\n case .muted: theme.colors.inkMuted\n case .soft: theme.colors.inkSoft\n case .success: theme.colors.success\n case .warning: theme.colors.warning\n }\n }\n}\n\npublic enum LumenSurfacePadding: Sendable {\n case lg\n case md\n case none\n case sm\n\n var value: CGFloat {\n switch self {\n case .lg: LumenSpacing.lg\n case .md: LumenSpacing.md\n case .none: 0\n case .sm: LumenSpacing.sm\n }\n }\n}\n\npublic enum LumenSurfaceRadius: Sendable {\n case lg\n case md\n case none\n case sm\n\n var value: CGFloat {\n switch self {\n case .lg: LumenRadius.lg\n case .md: LumenRadius.md\n case .none: 0\n case .sm: LumenRadius.sm\n }\n }\n}\n\npublic enum LumenSurfaceTone: Sendable {\n case canvas\n case muted\n case strong\n case surface\n}\n\npublic struct LumenSurface<Content: View>: View {\n @Environment(\\.lumenTheme) private var theme\n\n private let content: Content\n private let padding: LumenSurfacePadding\n private let radius: LumenSurfaceRadius\n private let tone: LumenSurfaceTone\n\n public init(\n tone: LumenSurfaceTone = .surface,\n padding: LumenSurfacePadding = .md,\n radius: LumenSurfaceRadius = .md,\n @ViewBuilder content: () -> Content\n ) {\n self.content = content()\n self.padding = padding\n self.radius = radius\n self.tone = tone\n }\n\n public var body: some View {\n content\n .padding(padding.value)\n .background(backgroundColor)\n .clipShape(RoundedRectangle(cornerRadius: radius.value, style: .continuous))\n }\n\n private var backgroundColor: Color {\n switch tone {\n case .canvas: theme.colors.canvas\n case .muted: theme.colors.surfaceMuted\n case .strong: theme.colors.surfaceStrong\n case .surface: theme.colors.surface\n }\n }\n}\n\npublic enum LumenButtonIntent: Sendable {\n case danger\n case primary\n case quiet\n case secondary\n}\n\npublic enum LumenControlSize: Sendable {\n case lg\n case md\n case sm\n}\n\npublic enum LumenIconSize: Sendable {\n case lg\n case md\n case sm\n\n var dimension: CGFloat {\n switch self {\n case .lg: 24\n case .md: 20\n case .sm: 16\n }\n }\n}\n\npublic struct LumenIcon: View {\n @Environment(\\.lumenTheme) private var theme\n\n private let color: Color?\n private let label: LocalizedStringKey?\n private let size: LumenIconSize\n private let systemName: String\n\n public init(\n systemName: String,\n size: LumenIconSize = .md,\n color: Color? = nil,\n label: LocalizedStringKey? = nil\n ) {\n self.systemName = systemName\n self.size = size\n self.color = color\n self.label = label\n }\n\n public var body: some View {\n Image(systemName: systemName)\n .font(.system(size: size.dimension, weight: .medium))\n .foregroundStyle(color ?? theme.colors.ink)\n .frame(width: size.dimension, height: size.dimension)\n .accessibilityHidden(label == nil)\n .accessibilityLabel(label ?? \"\")\n }\n}\n\npublic struct LumenIconButtonMetrics: Equatable, Sendable {\n public let iconSize: LumenIconSize\n public let touchTarget: CGFloat\n\n public static func resolve(\n _ size: LumenControlSize,\n density: LumenControlDensity = .platformDefault\n ) -> LumenIconButtonMetrics {\n switch (density, size) {\n case (.compact, .lg): LumenIconButtonMetrics(iconSize: .lg, touchTarget: 40)\n case (.compact, .md): LumenIconButtonMetrics(iconSize: .md, touchTarget: 32)\n case (.compact, .sm): LumenIconButtonMetrics(iconSize: .sm, touchTarget: 28)\n case (.regular, .lg): LumenIconButtonMetrics(iconSize: .lg, touchTarget: 52)\n case (.regular, .md): LumenIconButtonMetrics(iconSize: .md, touchTarget: 44)\n case (.regular, .sm): LumenIconButtonMetrics(iconSize: .sm, touchTarget: 44)\n }\n }\n}\n\npublic struct LumenIconButtonStyle: ButtonStyle {\n @Environment(\\.lumenControlDensity) private var density\n @Environment(\\.isEnabled) private var isEnabled\n @Environment(\\.lumenTheme) private var theme\n\n private let intent: LumenButtonIntent\n private let size: LumenControlSize\n\n public init(intent: LumenButtonIntent = .quiet, size: LumenControlSize = .md) {\n self.intent = intent\n self.size = size\n }\n\n public func makeBody(configuration: Configuration) -> some View {\n let metrics = LumenIconButtonMetrics.resolve(size, density: density)\n\n configuration.label\n .frame(width: metrics.touchTarget, height: metrics.touchTarget)\n .background(backgroundColor)\n .overlay {\n RoundedRectangle(cornerRadius: LumenRadius.sm, style: .continuous)\n .stroke(borderColor, lineWidth: 1)\n }\n .clipShape(RoundedRectangle(cornerRadius: LumenRadius.sm, style: .continuous))\n .opacity(isEnabled ? (configuration.isPressed ? 0.84 : 1) : 0.52)\n }\n\n private var backgroundColor: Color {\n switch intent {\n case .danger: theme.colors.danger\n case .primary: theme.colors.brandSolid\n case .quiet: .clear\n case .secondary: theme.colors.surfaceMuted\n }\n }\n\n private var borderColor: Color {\n switch intent {\n case .danger: theme.colors.danger\n case .primary: theme.colors.brandSolid\n case .quiet: .clear\n case .secondary: theme.colors.line\n }\n }\n}\n\npublic struct LumenIconButton: View {\n @Environment(\\.lumenControlDensity) private var density\n @Environment(\\.lumenTheme) private var theme\n\n private let action: () -> Void\n private let disabled: Bool\n private let intent: LumenButtonIntent\n private let label: LocalizedStringKey\n private let size: LumenControlSize\n private let systemName: String\n\n public init(\n systemName: String,\n label: LocalizedStringKey,\n intent: LumenButtonIntent = .quiet,\n size: LumenControlSize = .md,\n disabled: Bool = false,\n action: @escaping () -> Void\n ) {\n self.systemName = systemName\n self.label = label\n self.intent = intent\n self.size = size\n self.disabled = disabled\n self.action = action\n }\n\n public var body: some View {\n let metrics = LumenIconButtonMetrics.resolve(size, density: density)\n\n Button(action: action) {\n LumenIcon(\n systemName: systemName,\n size: metrics.iconSize,\n color: foregroundColor\n )\n }\n .buttonStyle(LumenIconButtonStyle(intent: intent, size: size))\n .disabled(disabled)\n .accessibilityLabel(label)\n }\n\n private var foregroundColor: Color {\n switch intent {\n case .danger: theme.colors.onDanger\n case .primary: theme.colors.onBrand\n case .quiet: theme.colors.inkSoft\n case .secondary: theme.colors.ink\n }\n }\n}\n\npublic struct LumenButtonMetrics: Equatable, Sendable {\n public let horizontalPadding: CGFloat\n public let minHeight: CGFloat\n\n public static func resolve(\n _ size: LumenControlSize,\n density: LumenControlDensity = .platformDefault\n ) -> LumenButtonMetrics {\n switch (density, size) {\n case (.compact, .lg):\n LumenButtonMetrics(horizontalPadding: LumenSpacing.lg, minHeight: 40)\n case (.compact, .md):\n LumenButtonMetrics(horizontalPadding: LumenSpacing.md, minHeight: 32)\n case (.compact, .sm):\n LumenButtonMetrics(horizontalPadding: LumenSpacing.md, minHeight: 28)\n case (.regular, .lg):\n LumenButtonMetrics(horizontalPadding: LumenSpacing.xl, minHeight: 52)\n case (.regular, .md):\n LumenButtonMetrics(horizontalPadding: LumenSpacing.lg, minHeight: 44)\n case (.regular, .sm):\n LumenButtonMetrics(horizontalPadding: LumenSpacing.md, minHeight: 36)\n }\n }\n}\n\npublic struct LumenButtonStyle: ButtonStyle {\n @Environment(\\.lumenControlDensity) private var density\n @Environment(\\.isEnabled) private var isEnabled\n @Environment(\\.lumenTheme) private var theme\n\n private let intent: LumenButtonIntent\n private let size: LumenControlSize\n\n public init(\n intent: LumenButtonIntent = .primary,\n size: LumenControlSize = .md\n ) {\n self.intent = intent\n self.size = size\n }\n\n public func makeBody(configuration: Configuration) -> some View {\n let metrics = LumenButtonMetrics.resolve(size, density: density)\n\n configuration.label\n .font(size == .lg ? .body.weight(.semibold) : .callout.weight(.semibold))\n .foregroundStyle(foregroundColor)\n .frame(minHeight: metrics.minHeight)\n .padding(.horizontal, metrics.horizontalPadding)\n .background(backgroundColor)\n .overlay {\n RoundedRectangle(cornerRadius: LumenRadius.sm, style: .continuous)\n .stroke(borderColor, lineWidth: 1)\n }\n .clipShape(RoundedRectangle(cornerRadius: LumenRadius.sm, style: .continuous))\n .opacity(isEnabled ? (configuration.isPressed ? 0.84 : 1) : 0.52)\n }\n\n private var backgroundColor: Color {\n switch intent {\n case .danger: theme.colors.danger\n case .primary: theme.colors.brandSolid\n case .quiet: .clear\n case .secondary: theme.colors.surfaceMuted\n }\n }\n\n private var borderColor: Color {\n switch intent {\n case .danger: theme.colors.danger\n case .primary: theme.colors.brandSolid\n case .quiet: .clear\n case .secondary: theme.colors.line\n }\n }\n\n private var foregroundColor: Color {\n switch intent {\n case .danger: theme.colors.onDanger\n case .primary: theme.colors.onBrand\n case .quiet: theme.colors.inkSoft\n case .secondary: theme.colors.ink\n }\n }\n}\n\npublic struct LumenButton<Label: View>: View {\n private let action: () -> Void\n private let disabled: Bool\n private let intent: LumenButtonIntent\n private let label: Label\n private let loading: Bool\n private let size: LumenControlSize\n\n public init(\n intent: LumenButtonIntent = .primary,\n size: LumenControlSize = .md,\n loading: Bool = false,\n disabled: Bool = false,\n action: @escaping () -> Void,\n @ViewBuilder label: () -> Label\n ) {\n self.action = action\n self.disabled = disabled\n self.intent = intent\n self.label = label()\n self.loading = loading\n self.size = size\n }\n\n public var body: some View {\n Button(action: action) {\n HStack(spacing: LumenSpacing.sm) {\n if loading {\n ProgressView()\n .controlSize(.small)\n }\n label\n }\n }\n .buttonStyle(LumenButtonStyle(intent: intent, size: size))\n .disabled(disabled || loading)\n .accessibilityValue(loading ? \"Loading\" : \"\")\n }\n}\n\npublic extension LumenButton where Label == Text {\n init(\n _ title: LocalizedStringKey,\n intent: LumenButtonIntent = .primary,\n size: LumenControlSize = .md,\n loading: Bool = false,\n disabled: Bool = false,\n action: @escaping () -> Void\n ) {\n self.init(\n intent: intent,\n size: size,\n loading: loading,\n disabled: disabled,\n action: action\n ) {\n Text(title)\n }\n }\n}\n\npublic struct LumenTextField: View {\n @Binding private var text: String\n @Environment(\\.isEnabled) private var isEnabled\n @Environment(\\.lumenControlDensity) private var density\n @Environment(\\.lumenTheme) private var theme\n\n private let error: Bool\n private let errorMessage: String?\n private let size: LumenControlSize\n private let title: String\n\n public init(\n _ title: String,\n text: Binding<String>,\n size: LumenControlSize = .md,\n error: Bool = false,\n errorMessage: String? = nil\n ) {\n self.title = title\n _text = text\n self.size = size\n self.error = error\n self.errorMessage = errorMessage\n }\n\n public var body: some View {\n TextField(title, text: $text)\n .textFieldStyle(.plain)\n .font(size == .lg ? .body : .callout)\n .foregroundStyle(theme.colors.ink)\n .frame(minHeight: LumenButtonMetrics.resolve(size, density: density).minHeight)\n .padding(.horizontal, LumenSpacing.md)\n .background(theme.colors.surface)\n .overlay {\n RoundedRectangle(cornerRadius: LumenRadius.sm, style: .continuous)\n .stroke(error ? theme.colors.danger : theme.colors.line, lineWidth: 1)\n }\n .clipShape(RoundedRectangle(cornerRadius: LumenRadius.sm, style: .continuous))\n .opacity(isEnabled ? 1 : 0.52)\n .accessibilityValue(error ? errorMessage ?? \"Invalid value\" : text)\n }\n}\n\npublic enum LumenBadgeTone: Sendable {\n case accent\n case danger\n case neutral\n case success\n case warning\n}\n\npublic struct LumenBadge: View {\n @Environment(\\.lumenTheme) private var theme\n\n private let content: LocalizedStringKey\n private let tone: LumenBadgeTone\n\n public init(\n _ content: LocalizedStringKey,\n tone: LumenBadgeTone = .neutral\n ) {\n self.content = content\n self.tone = tone\n }\n\n public var body: some View {\n Text(content)\n .font(.caption.weight(.semibold))\n .foregroundStyle(foregroundColor)\n .padding(.horizontal, LumenSpacing.sm)\n .padding(.vertical, LumenSpacing.xs)\n .background(backgroundColor)\n .clipShape(Capsule())\n }\n\n private var foregroundColor: Color {\n switch tone {\n case .accent: theme.colors.accent\n case .danger: theme.colors.danger\n case .neutral: theme.colors.inkSoft\n case .success: theme.colors.success\n case .warning: theme.colors.warning\n }\n }\n\n private var backgroundColor: Color {\n tone == .neutral ? theme.colors.surfaceMuted : foregroundColor.opacity(0.12)\n }\n}\n\npublic struct LumenDivider: View {\n @Environment(\\.lumenTheme) private var theme\n\n public init() {}\n\n public var body: some View {\n Rectangle()\n .fill(theme.colors.line)\n .frame(height: 1)\n .accessibilityHidden(true)\n }\n}\n\npublic struct LumenSpinner: View {\n @Environment(\\.lumenTheme) private var theme\n\n private let label: LocalizedStringKey\n\n public init(_ label: LocalizedStringKey = \"Loading\") {\n self.label = label\n }\n\n public var body: some View {\n ProgressView()\n .tint(theme.colors.brand)\n .accessibilityLabel(label)\n }\n}\n",
38798
39657
  "packages/swift/Sources/LumenUI/SelectionControls.swift": "#if os(iOS) || os(macOS)\nimport SwiftUI\n\npublic struct LumenSelectionOption<Value: Hashable>: Identifiable {\n public let description: String?\n public let id: Value\n public let isDisabled: Bool\n public let title: String\n\n public init(\n _ title: String,\n value: Value,\n description: String? = nil,\n isDisabled: Bool = false\n ) {\n self.title = title\n id = value\n self.description = description\n self.isDisabled = isDisabled\n }\n}\n\npublic struct LumenCheckbox: View {\n @Binding private var isChecked: Bool\n @Environment(\\.isEnabled) private var isEnabled\n @Environment(\\.lumenTheme) private var theme\n\n private let description: String?\n private let label: String\n\n public init(_ label: String, isChecked: Binding<Bool>, description: String? = nil) {\n self.label = label\n _isChecked = isChecked\n self.description = description\n }\n\n public var body: some View {\n Button { isChecked.toggle() } label: {\n HStack(alignment: .top, spacing: LumenSpacing.md) {\n Image(systemName: isChecked ? \"checkmark.square.fill\" : \"square\")\n .font(.system(size: 20, weight: .semibold))\n .foregroundStyle(isChecked ? theme.colors.brandSolid : theme.colors.inkMuted)\n .frame(width: 24, height: 24)\n .accessibilityHidden(true)\n\n VStack(alignment: .leading, spacing: LumenSpacing.xs) {\n Text(label).font(.callout.weight(.semibold)).foregroundStyle(theme.colors.ink)\n if let description {\n Text(description)\n .font(.caption)\n .foregroundStyle(theme.colors.inkMuted)\n .fixedSize(horizontal: false, vertical: true)\n }\n }\n .frame(maxWidth: .infinity, alignment: .leading)\n }\n .contentShape(Rectangle())\n }\n .buttonStyle(.plain)\n .opacity(isEnabled ? 1 : 0.52)\n .accessibilityLabel(Text(label))\n .accessibilityValue(Text(isChecked ? \"Checked\" : \"Not checked\"))\n }\n}\n\npublic struct LumenRadioGroup<Value: Hashable>: View {\n @Binding private var selection: Value\n @Environment(\\.lumenTheme) private var theme\n\n private let label: String\n private let options: [LumenSelectionOption<Value>]\n\n public init(_ label: String, selection: Binding<Value>, options: [LumenSelectionOption<Value>]) {\n self.label = label\n _selection = selection\n self.options = options\n }\n\n public var body: some View {\n VStack(alignment: .leading, spacing: LumenSpacing.sm) {\n Text(label).font(.callout.weight(.semibold)).foregroundStyle(theme.colors.ink)\n ForEach(options) { option in\n let isSelected = selection == option.id\n\n Button { selection = option.id } label: {\n HStack(alignment: .top, spacing: LumenSpacing.md) {\n Image(systemName: isSelected ? \"largecircle.fill.circle\" : \"circle\")\n .font(.system(size: 20, weight: .semibold))\n .foregroundStyle(isSelected ? theme.colors.brandSolid : theme.colors.inkMuted)\n .frame(width: 24, height: 24)\n .accessibilityHidden(true)\n VStack(alignment: .leading, spacing: LumenSpacing.xs) {\n Text(option.title).font(.callout).foregroundStyle(theme.colors.ink)\n if let description = option.description {\n Text(description)\n .font(.caption)\n .foregroundStyle(theme.colors.inkMuted)\n .fixedSize(horizontal: false, vertical: true)\n }\n }\n .frame(maxWidth: .infinity, alignment: .leading)\n }\n .contentShape(Rectangle())\n }\n .buttonStyle(.plain)\n .disabled(option.isDisabled)\n .opacity(option.isDisabled ? 0.52 : 1)\n .accessibilityLabel(Text(option.title))\n .accessibilityValue(Text(isSelected ? \"Selected\" : \"Not selected\"))\n .accessibilityAddTraits(isSelected ? .isSelected : [])\n }\n }\n .accessibilityElement(children: .contain)\n .accessibilityLabel(Text(label))\n }\n}\n\npublic struct LumenSegmentedControl<Value: Hashable>: View {\n @Binding private var selection: Value\n @Environment(\\.lumenTheme) private var theme\n\n private let label: String\n private let options: [LumenSelectionOption<Value>]\n private let showsLabel: Bool\n\n public init(\n _ label: String,\n selection: Binding<Value>,\n options: [LumenSelectionOption<Value>],\n showsLabel: Bool = true\n ) {\n self.label = label\n _selection = selection\n self.options = options\n self.showsLabel = showsLabel\n }\n\n public var body: some View {\n VStack(alignment: .leading, spacing: LumenSpacing.sm) {\n if showsLabel {\n Text(label).font(.callout.weight(.semibold)).foregroundStyle(theme.colors.ink)\n }\n HStack(spacing: 2) {\n ForEach(options) { option in\n let isSelected = selection == option.id\n\n Button { selection = option.id } label: {\n Text(option.title)\n .font(.caption.weight(isSelected ? .bold : .semibold))\n .lineLimit(1)\n .frame(maxWidth: .infinity, minHeight: 36)\n .padding(.horizontal, LumenSpacing.sm)\n .foregroundStyle(isSelected ? theme.colors.brand : theme.colors.inkSoft)\n .background(isSelected ? theme.colors.surface : .clear)\n .clipShape(RoundedRectangle(cornerRadius: LumenRadius.sm, style: .continuous))\n }\n .buttonStyle(.plain)\n .disabled(option.isDisabled)\n .opacity(option.isDisabled ? 0.52 : 1)\n .accessibilityLabel(Text(option.title))\n .accessibilityValue(Text(isSelected ? \"Selected\" : \"Not selected\"))\n .accessibilityAddTraits(isSelected ? .isSelected : [])\n }\n }\n .padding(2)\n .background(theme.colors.surfaceMuted)\n .overlay {\n RoundedRectangle(cornerRadius: LumenRadius.sm, style: .continuous)\n .stroke(theme.colors.line, lineWidth: 1)\n }\n .clipShape(RoundedRectangle(cornerRadius: LumenRadius.sm, style: .continuous))\n .accessibilityElement(children: .contain)\n .accessibilityLabel(Text(label))\n }\n }\n}\n#endif\n",
38799
39658
  "packages/swift/Sources/LumenUI/SharedComponents.swift": "import SwiftUI\n\npublic enum LumenCardVariant: Sendable {\n case accent\n case `default`\n case destructive\n case muted\n case success\n case warning\n}\n\npublic struct LumenCard<Content: View>: View {\n @Environment(\\.lumenTheme) private var theme\n\n private let action: (() -> Void)?\n private let content: Content\n private let variant: LumenCardVariant\n\n public init(\n variant: LumenCardVariant = .default,\n action: (() -> Void)? = nil,\n @ViewBuilder content: () -> Content\n ) {\n self.variant = variant\n self.action = action\n self.content = content()\n }\n\n public var body: some View {\n Group {\n if let action {\n Button(action: action) {\n cardSurface\n }\n .buttonStyle(LumenCardButtonStyle())\n } else {\n cardSurface\n }\n }\n }\n\n private var cardSurface: some View {\n content\n .frame(maxWidth: .infinity, alignment: .leading)\n .padding(LumenSpacing.xl)\n .background(backgroundColor)\n .overlay {\n RoundedRectangle(cornerRadius: LumenRadius.lg, style: .continuous)\n .stroke(borderColor, lineWidth: 1)\n }\n .clipShape(RoundedRectangle(cornerRadius: LumenRadius.lg, style: .continuous))\n }\n\n private var accentColor: Color? {\n switch variant {\n case .accent: theme.colors.accent\n case .default, .muted: nil\n case .destructive: theme.colors.danger\n case .success: theme.colors.success\n case .warning: theme.colors.warning\n }\n }\n\n private var backgroundColor: Color {\n switch variant {\n case .default: theme.colors.surface\n case .muted: theme.colors.surfaceMuted\n case .accent, .destructive, .success, .warning:\n accentColor?.opacity(0.06) ?? theme.colors.surface\n }\n }\n\n private var borderColor: Color {\n accentColor?.opacity(0.24) ?? theme.colors.line\n }\n}\n\nprivate struct LumenCardButtonStyle: ButtonStyle {\n func makeBody(configuration: Configuration) -> some View {\n configuration.label\n .opacity(configuration.isPressed ? 0.84 : 1)\n .scaleEffect(configuration.isPressed ? 0.995 : 1)\n }\n}\n\npublic enum LumenAlertVariant: Sendable {\n case `default`\n case destructive\n case success\n case warning\n}\n\npublic struct LumenAlert<Content: View>: View {\n @Environment(\\.lumenTheme) private var theme\n\n private let content: Content\n private let variant: LumenAlertVariant\n\n public init(\n variant: LumenAlertVariant = .default,\n @ViewBuilder content: () -> Content\n ) {\n self.variant = variant\n self.content = content()\n }\n\n public var body: some View {\n content\n .frame(maxWidth: .infinity, alignment: .leading)\n .padding(.horizontal, LumenSpacing.lg)\n .padding(.vertical, LumenSpacing.md)\n .foregroundStyle(foregroundColor)\n .background(backgroundColor)\n .overlay {\n RoundedRectangle(cornerRadius: LumenRadius.md, style: .continuous)\n .stroke(borderColor, lineWidth: 1)\n }\n .clipShape(RoundedRectangle(cornerRadius: LumenRadius.md, style: .continuous))\n }\n\n private var accentColor: Color {\n switch variant {\n case .default: theme.colors.ink\n case .destructive: theme.colors.danger\n case .success: theme.colors.success\n case .warning: theme.colors.warning\n }\n }\n\n private var backgroundColor: Color {\n variant == .default ? theme.colors.surface : accentColor.opacity(0.08)\n }\n\n private var borderColor: Color {\n variant == .default ? theme.colors.line : accentColor.opacity(0.36)\n }\n\n private var foregroundColor: Color {\n variant == .default ? theme.colors.ink : accentColor\n }\n}\n\npublic struct LumenProgressValue: Equatable, Sendable {\n public let max: Double\n public let value: Double\n\n public var fraction: Double { value / max }\n\n public static func resolve(value: Double, max: Double) -> LumenProgressValue {\n let safeMax = max.isFinite && max > 0 ? max : 100\n let finiteValue = value.isFinite ? value : 0\n let safeValue = min(safeMax, Swift.max(0, finiteValue))\n\n return LumenProgressValue(max: safeMax, value: safeValue)\n }\n}\n\npublic struct LumenProgress: View {\n @Environment(\\.lumenTheme) private var theme\n\n private let label: String?\n private let progress: LumenProgressValue\n\n public init(\n value: Double = 0,\n max: Double = 100,\n label: String? = nil\n ) {\n progress = LumenProgressValue.resolve(value: value, max: max)\n self.label = label\n }\n\n public var body: some View {\n GeometryReader { geometry in\n ZStack(alignment: .leading) {\n Capsule().fill(theme.colors.surfaceStrong)\n Capsule()\n .fill(theme.colors.brandSolid)\n .frame(width: geometry.size.width * progress.fraction)\n }\n }\n .frame(height: LumenSpacing.sm)\n .accessibilityElement(children: .ignore)\n .accessibilityLabel(Text(label ?? \"Progress\"))\n .accessibilityValue(Text(\"\\(Int(progress.fraction * 100)) percent\"))\n }\n}\n\npublic enum LumenAvatarSize: Sendable {\n case lg\n case md\n case sm\n\n var dimension: CGFloat {\n switch self {\n case .lg: 56\n case .md: 40\n case .sm: 32\n }\n }\n}\n\npublic struct LumenAvatar: View {\n @Environment(\\.lumenTheme) private var theme\n\n private let fallback: String\n private let image: Image?\n private let label: String?\n private let size: LumenAvatarSize\n\n public init(\n image: Image? = nil,\n fallback: String = \"?\",\n size: LumenAvatarSize = .md,\n label: String? = nil\n ) {\n self.image = image\n self.fallback = fallback\n self.size = size\n self.label = label\n }\n\n public var body: some View {\n ZStack {\n Circle().fill(theme.colors.surfaceMuted)\n\n if let image {\n image\n .resizable()\n .scaledToFill()\n } else {\n Text(fallback)\n .font(fallbackFont)\n .foregroundStyle(theme.colors.ink)\n }\n }\n .frame(width: size.dimension, height: size.dimension)\n .clipShape(Circle())\n .overlay {\n Circle().stroke(theme.colors.line, lineWidth: 1)\n }\n .accessibilityElement(children: .ignore)\n .accessibilityHidden(label == nil)\n .accessibilityLabel(Text(label ?? \"\"))\n }\n\n private var fallbackFont: Font {\n size == .lg ? .headline.weight(.bold) : .callout.weight(.bold)\n }\n}\n",
38800
39659
  "packages/swift/Sources/LumenUI/StructuredComponents.swift": "import SwiftUI\n\npublic struct LumenEmptyState<Actions: View>: View {\n @Environment(\\.lumenTheme) private var theme\n\n private let actions: Actions\n private let description: LocalizedStringKey?\n private let systemName: String\n private let title: LocalizedStringKey\n\n public init(\n _ title: LocalizedStringKey,\n systemName: String,\n description: LocalizedStringKey? = nil,\n @ViewBuilder actions: () -> Actions\n ) {\n self.title = title\n self.systemName = systemName\n self.description = description\n self.actions = actions()\n }\n\n public var body: some View {\n VStack(spacing: LumenSpacing.lg) {\n LumenIcon(systemName: systemName, size: .lg, color: theme.colors.inkMuted)\n .padding(LumenSpacing.md)\n .background(theme.colors.surfaceMuted, in: Circle())\n\n VStack(spacing: LumenSpacing.sm) {\n Text(title)\n .font(.headline)\n .foregroundStyle(theme.colors.ink)\n\n if let description {\n Text(description)\n .font(.callout)\n .foregroundStyle(theme.colors.inkMuted)\n .multilineTextAlignment(.center)\n .fixedSize(horizontal: false, vertical: true)\n }\n }\n\n actions\n }\n .frame(maxWidth: 440)\n .padding(LumenSpacing.xl)\n .frame(maxWidth: .infinity, maxHeight: .infinity)\n .accessibilityElement(children: .contain)\n }\n}\n\npublic extension LumenEmptyState where Actions == EmptyView {\n init(\n _ title: LocalizedStringKey,\n systemName: String,\n description: LocalizedStringKey? = nil\n ) {\n self.init(title, systemName: systemName, description: description) {\n EmptyView()\n }\n }\n}\n\npublic struct LumenListRow<Leading: View, Content: View, Trailing: View>: View {\n private let content: Content\n private let leading: Leading\n private let trailing: Trailing\n\n public init(\n @ViewBuilder leading: () -> Leading,\n @ViewBuilder content: () -> Content,\n @ViewBuilder trailing: () -> Trailing\n ) {\n self.leading = leading()\n self.content = content()\n self.trailing = trailing()\n }\n\n public var body: some View {\n HStack(alignment: .center, spacing: LumenSpacing.md) {\n leading\n .fixedSize()\n\n content\n .frame(maxWidth: .infinity, alignment: .leading)\n\n trailing\n .fixedSize(horizontal: true, vertical: false)\n }\n .padding(.horizontal, LumenSpacing.lg)\n .padding(.vertical, LumenSpacing.md)\n .contentShape(Rectangle())\n .accessibilityElement(children: .contain)\n }\n}\n\npublic extension LumenListRow where Trailing == EmptyView {\n init(\n @ViewBuilder leading: () -> Leading,\n @ViewBuilder content: () -> Content\n ) {\n self.init(leading: leading, content: content) {\n EmptyView()\n }\n }\n}\n\npublic enum LumenBannerVariant: Sendable {\n case accent\n case `default`\n case destructive\n case success\n case warning\n}\n\npublic struct LumenBanner<Actions: View>: View {\n @Environment(\\.lumenTheme) private var theme\n\n private let actions: Actions\n private let description: LocalizedStringKey?\n private let onDismiss: (() -> Void)?\n private let systemName: String?\n private let title: LocalizedStringKey\n private let variant: LumenBannerVariant\n\n public init(\n _ title: LocalizedStringKey,\n description: LocalizedStringKey? = nil,\n systemName: String? = nil,\n variant: LumenBannerVariant = .default,\n onDismiss: (() -> Void)? = nil,\n @ViewBuilder actions: () -> Actions\n ) {\n self.title = title\n self.description = description\n self.systemName = systemName\n self.variant = variant\n self.onDismiss = onDismiss\n self.actions = actions()\n }\n\n public var body: some View {\n HStack(alignment: .center, spacing: LumenSpacing.md) {\n LumenIcon(\n systemName: systemName ?? defaultSystemName,\n size: .md,\n color: accentColor\n )\n\n VStack(alignment: .leading, spacing: LumenSpacing.xs) {\n Text(title)\n .font(.callout.weight(.semibold))\n .foregroundStyle(theme.colors.ink)\n\n if let description {\n Text(description)\n .font(.caption)\n .foregroundStyle(theme.colors.inkSoft)\n .fixedSize(horizontal: false, vertical: true)\n }\n }\n .frame(maxWidth: .infinity, alignment: .leading)\n\n actions\n\n if let onDismiss {\n LumenIconButton(\n systemName: \"xmark\",\n label: \"Dismiss\",\n intent: .quiet,\n size: .sm,\n action: onDismiss\n )\n }\n }\n .padding(.horizontal, LumenSpacing.lg)\n .padding(.vertical, LumenSpacing.md)\n .background(backgroundColor)\n .overlay(alignment: .leading) {\n Rectangle()\n .fill(accentColor)\n .frame(width: 3)\n }\n .overlay {\n RoundedRectangle(cornerRadius: LumenRadius.md, style: .continuous)\n .stroke(borderColor, lineWidth: 1)\n }\n .clipShape(RoundedRectangle(cornerRadius: LumenRadius.md, style: .continuous))\n .accessibilityElement(children: .contain)\n }\n\n private var accentColor: Color {\n switch variant {\n case .accent: theme.colors.accent\n case .default: theme.colors.brand\n case .destructive: theme.colors.danger\n case .success: theme.colors.success\n case .warning: theme.colors.warning\n }\n }\n\n private var backgroundColor: Color {\n variant == .default ? theme.colors.surface : accentColor.opacity(0.08)\n }\n\n private var borderColor: Color {\n variant == .default ? theme.colors.line : accentColor.opacity(0.32)\n }\n\n private var defaultSystemName: String {\n switch variant {\n case .accent: \"sparkles\"\n case .default: \"info.circle\"\n case .destructive: \"xmark.octagon\"\n case .success: \"checkmark.circle\"\n case .warning: \"exclamationmark.triangle\"\n }\n }\n}\n\npublic extension LumenBanner where Actions == EmptyView {\n init(\n _ title: LocalizedStringKey,\n description: LocalizedStringKey? = nil,\n systemName: String? = nil,\n variant: LumenBannerVariant = .default,\n onDismiss: (() -> Void)? = nil\n ) {\n self.init(\n title,\n description: description,\n systemName: systemName,\n variant: variant,\n onDismiss: onDismiss\n ) {\n EmptyView()\n }\n }\n}\n\npublic enum LumenMetricTone: Sendable {\n case accent\n case brand\n case danger\n case neutral\n case success\n case warning\n}\n\npublic struct LumenStat: View {\n @Environment(\\.lumenTheme) private var theme\n\n private let detail: LocalizedStringKey?\n private let label: LocalizedStringKey\n private let systemName: String?\n private let tone: LumenMetricTone\n private let value: String\n\n public init(\n _ label: LocalizedStringKey,\n value: String,\n detail: LocalizedStringKey? = nil,\n systemName: String? = nil,\n tone: LumenMetricTone = .brand\n ) {\n self.label = label\n self.value = value\n self.detail = detail\n self.systemName = systemName\n self.tone = tone\n }\n\n public var body: some View {\n VStack(alignment: .leading, spacing: LumenSpacing.sm) {\n if let systemName {\n LumenIcon(systemName: systemName, size: .sm, color: accentColor)\n }\n\n Text(value)\n .font(.title2.weight(.bold))\n .monospacedDigit()\n .foregroundStyle(theme.colors.ink)\n\n Text(label)\n .font(.caption.weight(.semibold))\n .foregroundStyle(theme.colors.inkSoft)\n\n if let detail {\n Text(detail)\n .font(.caption)\n .foregroundStyle(theme.colors.inkMuted)\n }\n }\n .frame(maxWidth: .infinity, alignment: .leading)\n .padding(LumenSpacing.lg)\n .background(accentColor.opacity(0.06))\n .overlay {\n RoundedRectangle(cornerRadius: LumenRadius.md, style: .continuous)\n .stroke(accentColor.opacity(0.18), lineWidth: 1)\n }\n .clipShape(RoundedRectangle(cornerRadius: LumenRadius.md, style: .continuous))\n .accessibilityElement(children: .combine)\n }\n\n private var accentColor: Color {\n metricColor(tone, theme: theme)\n }\n}\n\npublic struct LumenGauge: View {\n @Environment(\\.lumenTheme) private var theme\n\n private let label: LocalizedStringKey\n private let progress: LumenProgressValue\n private let systemName: String?\n private let tone: LumenMetricTone\n private let valueLabel: String\n\n public init(\n _ label: LocalizedStringKey,\n value: Double,\n max: Double = 100,\n valueLabel: String,\n systemName: String? = nil,\n tone: LumenMetricTone = .brand\n ) {\n self.label = label\n progress = LumenProgressValue.resolve(value: value, max: max)\n self.valueLabel = valueLabel\n self.systemName = systemName\n self.tone = tone\n }\n\n public var body: some View {\n VStack(spacing: LumenSpacing.md) {\n ZStack {\n Circle()\n .stroke(theme.colors.surfaceStrong, lineWidth: 10)\n\n Circle()\n .trim(from: 0, to: progress.fraction)\n .stroke(accentColor, style: StrokeStyle(lineWidth: 10, lineCap: .round))\n .rotationEffect(.degrees(-90))\n\n VStack(spacing: LumenSpacing.xs) {\n if let systemName {\n LumenIcon(systemName: systemName, size: .md, color: accentColor)\n }\n Text(valueLabel)\n .font(.headline.monospacedDigit())\n .foregroundStyle(theme.colors.ink)\n }\n }\n .frame(width: 108, height: 108)\n\n Text(label)\n .font(.caption.weight(.semibold))\n .foregroundStyle(theme.colors.inkSoft)\n }\n .accessibilityElement(children: .ignore)\n .accessibilityLabel(Text(label))\n .accessibilityValue(Text(valueLabel))\n }\n\n private var accentColor: Color {\n metricColor(tone, theme: theme)\n }\n}\n\npublic struct LumenSectionHeader<Actions: View>: View {\n @Environment(\\.lumenTheme) private var theme\n\n private let actions: Actions\n private let count: String?\n private let subtitle: LocalizedStringKey?\n private let title: LocalizedStringKey\n\n public init(\n _ title: LocalizedStringKey,\n subtitle: LocalizedStringKey? = nil,\n count: String? = nil,\n @ViewBuilder actions: () -> Actions\n ) {\n self.title = title\n self.subtitle = subtitle\n self.count = count\n self.actions = actions()\n }\n\n public var body: some View {\n HStack(alignment: .top, spacing: LumenSpacing.md) {\n VStack(alignment: .leading, spacing: LumenSpacing.xs) {\n HStack(alignment: .firstTextBaseline, spacing: LumenSpacing.sm) {\n Text(title)\n .font(.headline)\n .foregroundStyle(theme.colors.ink)\n\n if let count {\n LumenBadge(LocalizedStringKey(count), tone: .neutral)\n .monospacedDigit()\n }\n }\n\n if let subtitle {\n Text(subtitle)\n .font(.caption)\n .foregroundStyle(theme.colors.inkMuted)\n }\n }\n .frame(maxWidth: .infinity, alignment: .leading)\n\n actions\n }\n .accessibilityElement(children: .contain)\n }\n}\n\npublic extension LumenSectionHeader where Actions == EmptyView {\n init(\n _ title: LocalizedStringKey,\n subtitle: LocalizedStringKey? = nil,\n count: String? = nil\n ) {\n self.init(title, subtitle: subtitle, count: count) {\n EmptyView()\n }\n }\n}\n\npublic struct LumenStatusBar<Trailing: View>: View {\n @Environment(\\.lumenTheme) private var theme\n\n private let message: LocalizedStringKey\n private let tone: LumenMetricTone\n private let trailing: Trailing\n\n public init(\n _ message: LocalizedStringKey,\n tone: LumenMetricTone = .neutral,\n @ViewBuilder trailing: () -> Trailing\n ) {\n self.message = message\n self.tone = tone\n self.trailing = trailing()\n }\n\n public var body: some View {\n HStack(spacing: LumenSpacing.sm) {\n Circle()\n .fill(metricColor(tone, theme: theme))\n .frame(width: 7, height: 7)\n .accessibilityHidden(true)\n\n Text(message)\n .lineLimit(1)\n\n Spacer(minLength: LumenSpacing.md)\n\n trailing\n }\n .font(.caption)\n .foregroundStyle(theme.colors.inkSoft)\n .padding(.horizontal, LumenSpacing.lg)\n .frame(minHeight: 36)\n .background(theme.colors.surfaceMuted)\n .accessibilityElement(children: .contain)\n }\n}\n\npublic extension LumenStatusBar where Trailing == EmptyView {\n init(_ message: LocalizedStringKey, tone: LumenMetricTone = .neutral) {\n self.init(message, tone: tone) {\n EmptyView()\n }\n }\n}\n\nprivate func metricColor(_ tone: LumenMetricTone, theme: LumenTheme) -> Color {\n switch tone {\n case .accent: theme.colors.accent\n case .brand: theme.colors.brand\n case .danger: theme.colors.danger\n case .neutral: theme.colors.inkMuted\n case .success: theme.colors.success\n case .warning: theme.colors.warning\n }\n}\n",
38801
- "packages/swift/Sources/LumenUI/Tokens.generated.swift": "// This file is generated by scripts/generate-platform-tokens.mjs.\nimport SwiftUI\n\npublic struct LumenColorPalette: Sendable {\n public let canvas: Color\n public let surface: Color\n public let surfaceMuted: Color\n public let surfaceStrong: Color\n public let line: Color\n public let ink: Color\n public let inkSoft: Color\n public let inkMuted: Color\n public let brand: Color\n public let brandSolid: Color\n public let brandSoft: Color\n public let onBrand: Color\n public let accent: Color\n public let success: Color\n public let warning: Color\n public let danger: Color\n public let onDanger: Color\n\n public init(\n canvas: Color,\n surface: Color,\n surfaceMuted: Color,\n surfaceStrong: Color,\n line: Color,\n ink: Color,\n inkSoft: Color,\n inkMuted: Color,\n brand: Color,\n brandSolid: Color,\n brandSoft: Color,\n onBrand: Color,\n accent: Color,\n success: Color,\n warning: Color,\n danger: Color,\n onDanger: Color\n ) {\n self.canvas = canvas\n self.surface = surface\n self.surfaceMuted = surfaceMuted\n self.surfaceStrong = surfaceStrong\n self.line = line\n self.ink = ink\n self.inkSoft = inkSoft\n self.inkMuted = inkMuted\n self.brand = brand\n self.brandSolid = brandSolid\n self.brandSoft = brandSoft\n self.onBrand = onBrand\n self.accent = accent\n self.success = success\n self.warning = warning\n self.danger = danger\n self.onDanger = onDanger\n }\n}\n\npublic enum LumenColors {\n public static let light = LumenColorPalette(\n canvas: Color(red: 1.000000, green: 1.000000, blue: 1.000000),\n surface: Color(red: 1.000000, green: 1.000000, blue: 1.000000),\n surfaceMuted: Color(red: 0.952941, green: 0.956863, blue: 0.964706),\n surfaceStrong: Color(red: 0.898039, green: 0.905882, blue: 0.921569),\n line: Color(red: 0.843137, green: 0.854902, blue: 0.878431),\n ink: Color(red: 0.058824, green: 0.090196, blue: 0.160784),\n inkSoft: Color(red: 0.270588, green: 0.309804, blue: 0.372549),\n inkMuted: Color(red: 0.423529, green: 0.470588, blue: 0.537255),\n brand: Color(red: 0.141176, green: 0.388235, blue: 0.921569),\n brandSolid: Color(red: 0.078431, green: 0.321569, blue: 0.843137),\n brandSoft: Color(red: 0.941176, green: 0.960784, blue: 1.000000),\n onBrand: Color(red: 1.000000, green: 1.000000, blue: 1.000000),\n accent: Color(red: 0.086275, green: 0.635294, blue: 0.525490),\n success: Color(red: 0.105882, green: 0.615686, blue: 0.290196),\n warning: Color(red: 0.960784, green: 0.623529, blue: 0.039216),\n danger: Color(red: 0.937255, green: 0.262745, blue: 0.262745),\n onDanger: Color(red: 0.000000, green: 0.000000, blue: 0.000000)\n )\n\n public static let dark = LumenColorPalette(\n canvas: Color(red: 0.062745, green: 0.070588, blue: 0.098039),\n surface: Color(red: 0.090196, green: 0.101961, blue: 0.129412),\n surfaceMuted: Color(red: 0.125490, green: 0.141176, blue: 0.172549),\n surfaceStrong: Color(red: 0.188235, green: 0.207843, blue: 0.250980),\n line: Color(red: 0.223529, green: 0.247059, blue: 0.298039),\n ink: Color(red: 0.952941, green: 0.960784, blue: 0.968627),\n inkSoft: Color(red: 0.737255, green: 0.764706, blue: 0.803922),\n inkMuted: Color(red: 0.580392, green: 0.611765, blue: 0.658824),\n brand: Color(red: 0.160784, green: 0.705882, blue: 0.960784),\n brandSolid: Color(red: 0.031373, green: 0.478431, blue: 0.686275),\n brandSoft: Color(red: 0.027451, green: 0.207843, blue: 0.294118),\n onBrand: Color(red: 1.000000, green: 1.000000, blue: 1.000000),\n accent: Color(red: 0.101961, green: 0.756863, blue: 0.603922),\n success: Color(red: 0.141176, green: 0.760784, blue: 0.368627),\n warning: Color(red: 0.964706, green: 0.658824, blue: 0.137255),\n danger: Color(red: 0.945098, green: 0.356863, blue: 0.356863),\n onDanger: Color(red: 0.062745, green: 0.070588, blue: 0.098039)\n )\n}\n\npublic enum LumenSpacing {\n public static let zero: CGFloat = 0\n public static let xs: CGFloat = 4\n public static let sm: CGFloat = 8\n public static let md: CGFloat = 12\n public static let lg: CGFloat = 16\n public static let xl: CGFloat = 24\n public static let size2xl: CGFloat = 32\n public static let size3xl: CGFloat = 48\n}\n\npublic enum LumenRadius {\n public static let sm: CGFloat = 6\n public static let md: CGFloat = 10\n public static let lg: CGFloat = 12\n public static let full: CGFloat = 9999\n}\n\npublic enum LumenTypography {\n public static let xsSize: CGFloat = 12\n public static let smSize: CGFloat = 14\n public static let mdSize: CGFloat = 16\n public static let lgSize: CGFloat = 18\n public static let xlSize: CGFloat = 20\n public static let size2xlSize: CGFloat = 24\n public static let regularWeight: Font.Weight = .regular\n public static let mediumWeight: Font.Weight = .medium\n public static let semiboldWeight: Font.Weight = .semibold\n public static let boldWeight: Font.Weight = .bold\n}\n\npublic struct LumenCubicBezier: Equatable, Sendable {\n public let x1: Double\n public let y1: Double\n public let x2: Double\n public let y2: Double\n\n public init(x1: Double, y1: Double, x2: Double, y2: Double) {\n self.x1 = x1\n self.y1 = y1\n self.x2 = x2\n self.y2 = y2\n }\n}\n\npublic enum LumenMotion {\n public static let fastDuration: Double = 0.12\n public static let standardDuration: Double = 0.16\n public static let slowDuration: Double = 0.30\n public static let standardEasing = LumenCubicBezier(x1: 0.32, y1: 0.72, x2: 0, y2: 1)\n public static let emphasizedEasing = LumenCubicBezier(x1: 0.22, y1: 1, x2: 0.36, y2: 1)\n}\n\npublic enum LumenElevation {\n public static let resting: CGFloat = 1\n public static let raised: CGFloat = 3\n public static let overlay: CGFloat = 6\n}\n"
39660
+ "packages/swift/Sources/LumenUI/Tokens.generated.swift": "// This file is generated by scripts/generate-platform-tokens.mjs.\nimport SwiftUI\n\npublic struct LumenColorPalette: Sendable {\n public let canvas: Color\n public let surface: Color\n public let surfaceMuted: Color\n public let surfaceStrong: Color\n public let line: Color\n public let ink: Color\n public let inkSoft: Color\n public let inkMuted: Color\n public let brand: Color\n public let brandSolid: Color\n public let brandSoft: Color\n public let onBrand: Color\n public let accent: Color\n public let success: Color\n public let warning: Color\n public let danger: Color\n public let onDanger: Color\n\n public init(\n canvas: Color,\n surface: Color,\n surfaceMuted: Color,\n surfaceStrong: Color,\n line: Color,\n ink: Color,\n inkSoft: Color,\n inkMuted: Color,\n brand: Color,\n brandSolid: Color,\n brandSoft: Color,\n onBrand: Color,\n accent: Color,\n success: Color,\n warning: Color,\n danger: Color,\n onDanger: Color\n ) {\n self.canvas = canvas\n self.surface = surface\n self.surfaceMuted = surfaceMuted\n self.surfaceStrong = surfaceStrong\n self.line = line\n self.ink = ink\n self.inkSoft = inkSoft\n self.inkMuted = inkMuted\n self.brand = brand\n self.brandSolid = brandSolid\n self.brandSoft = brandSoft\n self.onBrand = onBrand\n self.accent = accent\n self.success = success\n self.warning = warning\n self.danger = danger\n self.onDanger = onDanger\n }\n}\n\npublic enum LumenColors {\n public static let light = LumenColorPalette(\n canvas: Color(red: 1.000000, green: 1.000000, blue: 1.000000),\n surface: Color(red: 1.000000, green: 1.000000, blue: 1.000000),\n surfaceMuted: Color(red: 0.952941, green: 0.956863, blue: 0.964706),\n surfaceStrong: Color(red: 0.898039, green: 0.905882, blue: 0.921569),\n line: Color(red: 0.843137, green: 0.854902, blue: 0.878431),\n ink: Color(red: 0.058824, green: 0.090196, blue: 0.160784),\n inkSoft: Color(red: 0.270588, green: 0.309804, blue: 0.372549),\n inkMuted: Color(red: 0.423529, green: 0.470588, blue: 0.537255),\n brand: Color(red: 0.141176, green: 0.388235, blue: 0.921569),\n brandSolid: Color(red: 0.078431, green: 0.321569, blue: 0.843137),\n brandSoft: Color(red: 0.941176, green: 0.960784, blue: 1.000000),\n onBrand: Color(red: 1.000000, green: 1.000000, blue: 1.000000),\n accent: Color(red: 0.086275, green: 0.635294, blue: 0.525490),\n success: Color(red: 0.105882, green: 0.615686, blue: 0.290196),\n warning: Color(red: 0.960784, green: 0.623529, blue: 0.039216),\n danger: Color(red: 0.937255, green: 0.262745, blue: 0.262745),\n onDanger: Color(red: 0.000000, green: 0.000000, blue: 0.000000)\n )\n\n public static let dark = LumenColorPalette(\n canvas: Color(red: 0.062745, green: 0.070588, blue: 0.098039),\n surface: Color(red: 0.090196, green: 0.101961, blue: 0.129412),\n surfaceMuted: Color(red: 0.125490, green: 0.141176, blue: 0.172549),\n surfaceStrong: Color(red: 0.188235, green: 0.207843, blue: 0.250980),\n line: Color(red: 0.223529, green: 0.247059, blue: 0.298039),\n ink: Color(red: 0.952941, green: 0.960784, blue: 0.968627),\n inkSoft: Color(red: 0.737255, green: 0.764706, blue: 0.803922),\n inkMuted: Color(red: 0.580392, green: 0.611765, blue: 0.658824),\n brand: Color(red: 0.160784, green: 0.705882, blue: 0.960784),\n brandSolid: Color(red: 0.031373, green: 0.478431, blue: 0.686275),\n brandSoft: Color(red: 0.027451, green: 0.207843, blue: 0.294118),\n onBrand: Color(red: 1.000000, green: 1.000000, blue: 1.000000),\n accent: Color(red: 0.101961, green: 0.756863, blue: 0.603922),\n success: Color(red: 0.141176, green: 0.760784, blue: 0.368627),\n warning: Color(red: 0.964706, green: 0.658824, blue: 0.137255),\n danger: Color(red: 0.945098, green: 0.356863, blue: 0.356863),\n onDanger: Color(red: 0.062745, green: 0.070588, blue: 0.098039)\n )\n}\n\npublic enum LumenSpacing {\n public static let zero: CGFloat = 0\n public static let xs: CGFloat = 4\n public static let sm: CGFloat = 8\n public static let md: CGFloat = 12\n public static let lg: CGFloat = 16\n public static let xl: CGFloat = 24\n public static let size2xl: CGFloat = 32\n public static let size3xl: CGFloat = 48\n}\n\npublic enum LumenRadius {\n public static let sm: CGFloat = 6\n public static let md: CGFloat = 10\n public static let lg: CGFloat = 12\n public static let full: CGFloat = 9999\n}\n\npublic enum LumenTypography {\n public static let xsSize: CGFloat = 12\n public static let smSize: CGFloat = 14\n public static let mdSize: CGFloat = 16\n public static let lgSize: CGFloat = 18\n public static let xlSize: CGFloat = 20\n public static let size2xlSize: CGFloat = 24\n public static let regularWeight: Font.Weight = .regular\n public static let mediumWeight: Font.Weight = .medium\n public static let semiboldWeight: Font.Weight = .semibold\n public static let boldWeight: Font.Weight = .bold\n}\n\npublic struct LumenCubicBezier: Equatable, Sendable {\n public let x1: Double\n public let y1: Double\n public let x2: Double\n public let y2: Double\n\n public init(x1: Double, y1: Double, x2: Double, y2: Double) {\n self.x1 = x1\n self.y1 = y1\n self.x2 = x2\n self.y2 = y2\n }\n}\n\npublic enum LumenMotion {\n public static let fastDuration: Double = 0.12\n public static let standardDuration: Double = 0.16\n public static let slowDuration: Double = 0.30\n public static let standardEasing = LumenCubicBezier(x1: 0.32, y1: 0.72, x2: 0, y2: 1)\n public static let emphasizedEasing = LumenCubicBezier(x1: 0.22, y1: 1, x2: 0.36, y2: 1)\n}\n\npublic enum LumenElevation {\n public static let resting: CGFloat = 1\n public static let raised: CGFloat = 3\n public static let overlay: CGFloat = 6\n}\n",
39661
+ "packages/swift/Sources/LumenUI/WatchComponents.swift": "import SwiftUI\n\npublic enum LumenWatchTone: Sendable {\n case accent\n case brand\n case danger\n case neutral\n case success\n case warning\n}\n\npublic struct LumenWatchProgressValue: Equatable, Sendable {\n public let maximum: Double\n public let value: Double\n\n public static func resolve(_ value: Double, maximum: Double) -> LumenWatchProgressValue {\n let safeMaximum = maximum.isFinite && maximum > 0 ? maximum : 1\n let safeValue = value.isFinite ? min(max(value, 0), safeMaximum) : 0\n return LumenWatchProgressValue(maximum: safeMaximum, value: safeValue)\n }\n\n public var fraction: Double {\n value / maximum\n }\n\n var accessibilityPercentage: Int {\n Int(fraction * 100)\n }\n}\n\npublic struct LumenWatchActionMetrics: Equatable, Sendable {\n public let dimension: CGFloat\n public let ringWidth: CGFloat\n\n public static func resolve(dimension: CGFloat, ringWidth: CGFloat) -> LumenWatchActionMetrics {\n let safeDimension = dimension.isFinite ? min(max(dimension, 44), 180) : 120\n let safeRingWidth = ringWidth.isFinite ? min(max(ringWidth, 2), 12) : 4\n return LumenWatchActionMetrics(dimension: safeDimension, ringWidth: safeRingWidth)\n }\n}\n\nfunc lumenWatchColor(_ palette: LumenColorPalette, tone: LumenWatchTone) -> Color {\n switch tone {\n case .accent:\n palette.accent\n case .brand:\n palette.brandSolid\n case .danger:\n palette.danger\n case .neutral:\n palette.inkMuted\n case .success:\n palette.success\n case .warning:\n palette.warning\n }\n}\n\n#if os(watchOS)\npublic struct LumenWatchActionButton<Label: View>: View {\n @Environment(\\.isLuminanceReduced) private var isLuminanceReduced\n @Environment(\\.lumenTheme) private var theme\n\n private let accessibilityLabel: LocalizedStringKey\n private let action: () -> Void\n private let dimension: CGFloat\n private let enabled: Bool\n private let label: Label\n private let tone: LumenWatchTone\n\n public init(\n _ accessibilityLabel: LocalizedStringKey,\n tone: LumenWatchTone = .brand,\n dimension: CGFloat = 120,\n enabled: Bool = true,\n action: @escaping () -> Void,\n @ViewBuilder label: () -> Label\n ) {\n self.accessibilityLabel = accessibilityLabel\n self.tone = tone\n self.dimension = dimension\n self.enabled = enabled\n self.action = action\n self.label = label()\n }\n\n public var body: some View {\n let metrics = LumenWatchActionMetrics.resolve(dimension: dimension, ringWidth: 4)\n let color = lumenWatchColor(theme.colors, tone: tone)\n\n Button(action: action) {\n label\n .foregroundStyle(tone == .danger ? theme.colors.onDanger : theme.colors.onBrand)\n .frame(width: metrics.dimension, height: metrics.dimension)\n .background(color.opacity(enabled ? 1 : 0.48), in: Circle())\n .shadow(color: isLuminanceReduced ? .clear : color.opacity(0.38), radius: 7)\n }\n .buttonStyle(.plain)\n .disabled(!enabled)\n .accessibilityLabel(Text(accessibilityLabel))\n }\n}\n\npublic struct LumenWatchProgressRing<Content: View>: View {\n @Environment(\\.isLuminanceReduced) private var isLuminanceReduced\n @Environment(\\.lumenTheme) private var theme\n\n private let content: Content\n private let lineWidth: CGFloat\n private let maximum: Double\n private let tone: LumenWatchTone\n private let value: Double\n\n public init(\n value: Double,\n maximum: Double = 1,\n tone: LumenWatchTone = .brand,\n lineWidth: CGFloat = 4,\n @ViewBuilder content: () -> Content\n ) {\n self.value = value\n self.maximum = maximum\n self.tone = tone\n self.lineWidth = lineWidth\n self.content = content()\n }\n\n public var body: some View {\n let progress = LumenWatchProgressValue.resolve(value, maximum: maximum)\n let metrics = LumenWatchActionMetrics.resolve(dimension: 120, ringWidth: lineWidth)\n\n ZStack {\n if !isLuminanceReduced {\n Circle()\n .stroke(theme.colors.line.opacity(0.5), lineWidth: metrics.ringWidth)\n Circle()\n .trim(from: 0, to: progress.fraction)\n .stroke(\n lumenWatchColor(theme.colors, tone: tone),\n style: StrokeStyle(lineWidth: metrics.ringWidth, lineCap: .round)\n )\n .rotationEffect(.degrees(-90))\n .accessibilityHidden(true)\n }\n content\n }\n .accessibilityElement(children: .contain)\n .accessibilityValue(Text(\"\\(progress.accessibilityPercentage) percent\"))\n }\n}\n\npublic struct LumenWatchStatus: View {\n @Environment(\\.lumenTheme) private var theme\n\n private let systemName: String?\n private let title: LocalizedStringKey\n private let tone: LumenWatchTone\n\n public init(\n _ title: LocalizedStringKey,\n systemName: String? = nil,\n tone: LumenWatchTone = .neutral\n ) {\n self.title = title\n self.systemName = systemName\n self.tone = tone\n }\n\n public var body: some View {\n Group {\n if let systemName {\n Label(title, systemImage: systemName)\n } else {\n Text(title)\n }\n }\n .font(.caption2.weight(.semibold))\n .foregroundStyle(foregroundColor)\n .padding(.horizontal, LumenSpacing.sm)\n .padding(.vertical, LumenSpacing.xs)\n .background(color.opacity(0.24), in: Capsule())\n }\n\n private var color: Color {\n lumenWatchColor(theme.colors, tone: tone)\n }\n\n private var foregroundColor: Color {\n tone == .neutral ? theme.colors.inkSoft : color\n }\n}\n\npublic struct LumenWatchMetric: View {\n @Environment(\\.lumenTheme) private var theme\n\n private let detail: LocalizedStringKey?\n private let label: LocalizedStringKey\n private let tone: LumenWatchTone\n private let value: String\n\n public init(\n _ label: LocalizedStringKey,\n value: String,\n detail: LocalizedStringKey? = nil,\n tone: LumenWatchTone = .neutral\n ) {\n self.label = label\n self.value = value\n self.detail = detail\n self.tone = tone\n }\n\n public var body: some View {\n VStack(alignment: .leading, spacing: 2) {\n Text(label)\n .font(.caption2)\n .foregroundStyle(theme.colors.inkMuted)\n Text(value)\n .font(.system(.body, design: .rounded, weight: .bold))\n .foregroundStyle(lumenWatchColor(theme.colors, tone: tone))\n if let detail {\n Text(detail)\n .font(.caption2)\n .foregroundStyle(theme.colors.inkSoft)\n }\n }\n .padding(LumenSpacing.sm)\n .background(theme.colors.surfaceMuted.opacity(0.84), in: RoundedRectangle(cornerRadius: LumenRadius.md))\n .accessibilityElement(children: .combine)\n }\n}\n\npublic struct LumenWatchListRow<Content: View, Leading: View, Trailing: View>: View {\n @Environment(\\.lumenTheme) private var theme\n\n private let content: Content\n private let leading: Leading\n private let trailing: Trailing\n\n public init(\n @ViewBuilder leading: () -> Leading,\n @ViewBuilder content: () -> Content,\n @ViewBuilder trailing: () -> Trailing\n ) {\n self.leading = leading()\n self.content = content()\n self.trailing = trailing()\n }\n\n public var body: some View {\n HStack(spacing: LumenSpacing.sm) {\n leading\n content\n .frame(maxWidth: .infinity, alignment: .leading)\n trailing\n }\n .padding(LumenSpacing.sm)\n .background(theme.colors.surfaceMuted.opacity(0.84), in: RoundedRectangle(cornerRadius: LumenRadius.md))\n }\n}\n\npublic extension LumenWatchListRow where Leading == EmptyView, Trailing == EmptyView {\n init(@ViewBuilder content: () -> Content) {\n self.init(leading: { EmptyView() }, content: content, trailing: { EmptyView() })\n }\n}\n#endif\n"
38802
39662
  }
38803
39663
  },
38804
39664
  "recipes": [
@@ -39202,6 +40062,94 @@
39202
40062
  }
39203
40063
  }
39204
40064
  ],
40065
+ "releaseManifest": {
40066
+ "schemaVersion": 1,
40067
+ "release": {
40068
+ "compose": {
40069
+ "artifacts": [
40070
+ "com.santi020k:lumen-compose",
40071
+ "com.santi020k:lumen-compose-wear"
40072
+ ],
40073
+ "version": "0.4.0"
40074
+ },
40075
+ "npm": {
40076
+ "packages": {
40077
+ "@santi020k/lumen-astro": {
40078
+ "peerDependencies": {
40079
+ "astro": ">=5"
40080
+ },
40081
+ "version": "1.4.0"
40082
+ },
40083
+ "@santi020k/lumen-core": {
40084
+ "peerDependencies": {},
40085
+ "version": "1.4.0"
40086
+ },
40087
+ "@santi020k/lumen-elements": {
40088
+ "peerDependencies": {},
40089
+ "version": "1.4.0"
40090
+ },
40091
+ "@santi020k/lumen-icons-brand": {
40092
+ "peerDependencies": {},
40093
+ "version": "1.0.1"
40094
+ },
40095
+ "@santi020k/lumen": {
40096
+ "peerDependencies": {},
40097
+ "version": "1.5.0"
40098
+ },
40099
+ "@santi020k/lumen-mcp": {
40100
+ "peerDependencies": {},
40101
+ "version": "1.5.0"
40102
+ },
40103
+ "@santi020k/lumen-react": {
40104
+ "peerDependencies": {
40105
+ "react": ">=19"
40106
+ },
40107
+ "version": "1.4.0"
40108
+ },
40109
+ "@santi020k/lumen-react-hook-form": {
40110
+ "peerDependencies": {
40111
+ "@santi020k/lumen-react": "^1.0.0",
40112
+ "react": ">=19",
40113
+ "react-hook-form": ">=7.76.0 <8"
40114
+ },
40115
+ "version": "1.0.1"
40116
+ },
40117
+ "@santi020k/lumen-react-native": {
40118
+ "peerDependencies": {
40119
+ "react": ">=19.2",
40120
+ "react-native": ">=0.86.2"
40121
+ },
40122
+ "version": "0.4.0"
40123
+ },
40124
+ "@santi020k/lumen-tokens": {
40125
+ "peerDependencies": {},
40126
+ "version": "0.1.0"
40127
+ }
40128
+ }
40129
+ },
40130
+ "swift": {
40131
+ "package": "https://github.com/santi020k/lumen.git",
40132
+ "revision": null,
40133
+ "tag": "v1.5.0"
40134
+ },
40135
+ "version": "1.5.0"
40136
+ },
40137
+ "migration": {
40138
+ "codemod": null,
40139
+ "css": {
40140
+ "astro": "@santi020k/lumen-astro/styles.css",
40141
+ "elements": "@santi020k/lumen-elements/styles.css",
40142
+ "react": "@santi020k/lumen-react/styles.css"
40143
+ },
40144
+ "deprecatedExports": [],
40145
+ "removedExports": [],
40146
+ "runtime": {
40147
+ "astro": "Mount UIPrimitives once when an imported component declares the ui-primitives runtime.",
40148
+ "elements": "Call defineLumenElements once before rendering lumen-* elements.",
40149
+ "react": "Use the public React behavior hooks for behavior-heavy primitives."
40150
+ }
40151
+ }
40152
+ },
39205
40153
  "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- 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 } from '@santi020k/lumen-astro'\nimport { UIPrimitives } from '@santi020k/lumen-astro'\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- Do not require Tailwind configuration for Lumen components.\n- Use public tokens: canvas, surface, surface-muted, surface-strong, line, ink, ink-soft, ink-muted, brand, brand-solid, brand-soft, accent, success, warning, danger.\n- Mount `UIPrimitives` once for interactive Astro primitives.\n- Use `glass` for glass surfaces. Overlay `surface=\"glass\"` is a deprecated compatibility alias that maps to `glass`.\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:datatable-selection-change` remains a deprecated alias until 1.0); `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 `Sonner` viewports with `placement` and `maxCount`. 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",
39206
40154
  "tokens": {
39207
40155
  "chart": {