@santi020k/lumen-mcp 4.0.0 → 4.2.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.
@@ -8,16 +8,16 @@
8
8
  "AspectRatio": "87bf3256c5da7d2f0d00560564111c51bdd188801c28fe0e3f1520f300559911",
9
9
  "Attachment": "9e477b26552bd9489f35e340ba53608c8e53a0cf7847745bce48f41a7ec062c4",
10
10
  "AttachmentList": "faeb2df7207af925fe72b046dbe8735a27c6cb269ade04f1f8536ba0e00963ca",
11
- "AttachmentPreview": "2a80d65f4c7e5914fd866c53ac3b97673bae44fb7933ab9c5ea90e6bd8df088d",
11
+ "AttachmentPreview": "32a9fe485c4defae12e10d5faf1be9b7781c15a63bcecbfa592abe20e23cd1e2",
12
12
  "Autocomplete": "7f17759d2ba590420283d16216c4fa12d7ab8296f4c4af259377646f61dc097f",
13
13
  "Avatar": "60a22d4181f2a04178e43eba02c96be0a4f08d3f3e2c6693d030326749a63220",
14
14
  "Backdrop": "69e54d576e43ebaecf5beec41c9594a8a1db1cbe8a3cb70f87a9b3da8b70eeed",
15
15
  "BackToTop": "90d46ec1afc97a2e229d671f2da139de68c2cda2610b72231f2946f1674a73ce",
16
16
  "Badge": "198c9577446e73721d27974d1950e98990034f63dc25fa2c01abd813945200c5",
17
- "BarChart": "8ebdc0421409ac0da721c5c618c4b59a2998bb895370ec4de64c914612d935ce",
17
+ "BarChart": "8194186ee59a53e92b719352d45ef21ac7e7ef12ab9f9f35ee9662a124b57365",
18
18
  "Breadcrumb": "700d5860a964387089f8fc8918145bb8e82fca04f0e6c6d8fb2e20bd90cdb152",
19
19
  "Bubble": "61d1f46dd7b7695e8c1906faf487b5b8dc12c769b170af87269db24f0b91d916",
20
- "Button": "1732443d1980ec157a96b03c88d53c2caeac5b1ddf71d1edb4ac7fbadb5ec062",
20
+ "Button": "acfa846c121335fc3f7a67c8657eb5035aeef1b2fddfb4a1e1d65d7dde84ee56",
21
21
  "ButtonGroup": "fc3d52bc7eae343546394308b910f8f2d708f618c08eb0c1fc75c0ca93a58e48",
22
22
  "Calendar": "e886a642f9bd9cb8d8fca14ccb17adbdb63441e09d850474907c99319e8ba24a",
23
23
  "Callout": "86c2031542acbfd5cefb6447b32c4385ceb1936b6430d82bd8dcb158f5f90741",
@@ -36,20 +36,20 @@
36
36
  "CodeTabs": "61dcbf3a401afee952c7935a639452c40eae2bbac7974c748dd4de58f9e4603d",
37
37
  "Combobox": "07e3fc8b0d0407064d8bc5d7110787bfbd94c8a122acee6917d524601f881fb8",
38
38
  "Container": "537320bf21c9d1e522523d143c12cf1578806c9e7f68b562926adb682ee12e38",
39
- "CopyButton": "67a898558420299a6fc44c875bec428d63b118b384068f70cf744df0b816707e",
39
+ "CopyButton": "bfe6a15d112703cfc28d606549a9282b54afb26e57b129198ffbab0c52f14bb7",
40
40
  "Command": "3ed71d3282ba1188018417b14b6c62e90d111eed37693d57e83ae47df2142567",
41
41
  "ContextNavigation": "cb3b2b1ed4fa6e52f30d57b3217da7929ee218f8d00d1c31192d0604fed77e3f",
42
42
  "ContextMenu": "8b9b5b009deb7c99fb367bf68216d9fe144163b8cb759c46154e3d0bbaed5048",
43
43
  "ColorPicker": "c5c777fd5aa2c9d91d8eff7ef6386af457807ab01a1400cebd7204c40fdedaec",
44
- "DataTable": "a220a4eabab7bcd2c4a35987419db761e19ce11f120adfb523b44243daab5149",
45
- "DatePicker": "b9f3f4ad42a85266730d95c19be3d9cea03615d3d0d75a310ffbc1b766963f33",
46
- "DateRangePicker": "b9ca3d0d56e7a26710bfdedea2ab34d5e334b1bb31a9abbee7478252ba9b8123",
44
+ "DataTable": "c062c4405dbb154d41c4f6c7fae477263c366e44bff5f499383c4e7a11bb05d2",
45
+ "DatePicker": "2654f1b189d001f04cd717a631d4a108324a7332a548d617d5bdaabc9c34a922",
46
+ "DateRangePicker": "b29018be38fab4dc0cf84956d030a39fd2034b1fa9a922ab9f68783e8b044151",
47
47
  "Dialog": "2173bc40116da149c25d01f68b1d9cef7df3d1f0784dfae6775979bd420ea3a2",
48
48
  "DialogHeader": "80d453ce7eed0a7953386108ca9f93ff2392af6c7d0df51d0ad84c07f5cef0f9",
49
49
  "DialogTitle": "d7dd34901252475ae18b3e9be7def4e7ae4aee541aad4956298c39af19d33fbd",
50
50
  "DialogBody": "54b5c40a530eab6136467d7cafb16223ca188b9e1bb40300d24e473619adaab1",
51
51
  "DialogFooter": "9e9f1445f1f91dc73d5d3e2a73ca3678cb44303414dceda5e1974541fcb1ed10",
52
- "DialogClose": "30b9511514929a02ff7d554b2e74d563fc4b579009c3122c10e7d7e2877acd07",
52
+ "DialogClose": "343ed8c2a80feb4d661fbbd66cf9c866d3071ed5189c7badbd5806a08ef322f2",
53
53
  "Direction": "ac13fa49498f18ea7734a565324402953549202080eaa3054c5068f347ec29bf",
54
54
  "Drawer": "2f046cdf9f3b860c484b1a2e08000d04338428dd0c92344045b315bf2dadb2ed",
55
55
  "DropdownMenu": "edca5e544781fcce8e5fe3898ebc20c34a29953b528a62a930792aa4aba920da",
@@ -65,10 +65,10 @@
65
65
  "Graphic": "01b4a421cbc88a830cfa7002924639a1718b356a3bedfce0299e91bee43f2e1a",
66
66
  "HoverCard": "14bc75cc0138d0e0569bec1c36a0ef13eaba5ed128595f937806ea9b7638f4ee",
67
67
  "Icon": "5339c26722d3c3c0d9c70035ea043b4d8eb9667e38a401f124579f733994aeb6",
68
- "Image": "c1ee057e6ea96bddd70f95794649b30b5f4b1ff426d8051983d0b299fe4a74a0",
68
+ "Image": "edd4f88f3829a70f6c8cb793e2cedf73c13e930ce1a3ccf6e9a6cab3d7d1768a",
69
69
  "DeviceFrame": "fa1fa5248cb797c09eb70da56823692c919ed6b160737cd2a0ce5d03ff3f9ce3",
70
- "ImageComparison": "3ff7e159823bca985cc92c542c3ffb49c171a6d33caf13170d0adeb503a09fe7",
71
- "MediaViewport": "949ca661b6c3ecfe6418ca938cb33c8dcfb083f54d868385440a618e1114fe49",
70
+ "ImageComparison": "e547f0aad30ec9aa90660b1308a7715b33760a5f716de035aaa51fff02eb6676",
71
+ "MediaViewport": "648368475d1149f8c6e39f556a5ecca48675d2d4b0b89cb00a8687da21b4cf81",
72
72
  "MediaThumbnail": "ee4fe6bf672d32a4b2bcd0e6eca1111036f2fdb5b1b8dc5d7670c291c88a7f32",
73
73
  "MediaFilmstrip": "985898797261d0a711be9e419f034a468c628f0c39cffa8409ce3f03537224da",
74
74
  "Illustration": "1db8d922e5a2490948bb7298d25ca50e10cf44eb297856548a5d2842be1bcfca",
@@ -81,23 +81,23 @@
81
81
  "Kbd": "14825287aa0d6ffcec6b51b61b61d8a7c44d7cba1c7c40a77a7d46d0632c25de",
82
82
  "Label": "10cb7069a9a24205bda0fd87e74d79bb9066164870dd5959b52fbdea6cd7b242",
83
83
  "Link": "d939a1c5f221f60c5dac42dc148434326728305fa0eb0e6b10094a538f9a7b04",
84
- "LineChart": "74bce4665da745771ae12d37c427ea7815763b119d38c26a387519712750a1d5",
85
- "ListBox": "65724ac8eeeb0a9e43c05017ef152cfff5388627103e19bc14dcad8d5ee4d02c",
84
+ "LineChart": "cb77c2591bbfdc8bef127af77ba4e59c45df43ad7ea9ca7a9852bb407c16d874",
85
+ "ListBox": "10cccfa9535d0e045dacc68fe475e306fd39f9a7bc577f57480d9798595c4a45",
86
86
  "Marker": "b2c3679b841cee6cc4ce6322d6dd7758f85a9371bc4817a750f6de4e7c0cc3d2",
87
87
  "Menubar": "ceaa113420c1a99cc1d7e856c04b5a75d8569473ebd8a24a83c947ec3b6608e5",
88
88
  "Message": "26c4a46c2090e9834c52dc56bfd2d84f7d70d9fcff6885270df9839eb895ab8e",
89
89
  "MessageScroller": "a0907d18407e38f303312d2e6540132131b07ce5001a9c7d54310a0583596a8f",
90
90
  "NativeSelect": "419d7e692d59d9dcce5f87b2f151861bbd9ef38554a055eb2059c0831c046e31",
91
91
  "NavigationMenu": "fc5494bfec5341fb5bcb0cca5fb451b12feade75b2a90b49088221021c4faaf0",
92
- "AmountField": "bac03ca035740d499f9cf3e16041119edaec3b325c5b24f9dab06c54d11cab32",
92
+ "AmountField": "a8c260536be29ab44ed85aecc2a659651b755f8b6acc1d81e61d9d3cd84aa51b",
93
93
  "NumberField": "26d8eff3b5d7eeab400b77e787585865af9f5c395f6f83d30d52cffc2c9c2a50",
94
94
  "Pagination": "1e85cbdac6aa9a6417ce9cc7e015f2dfceecba126803529c88d8fe228e2d50a9",
95
95
  "PasswordField": "784fb8457944c5d6879a3ed1db154de8e14cae7eec3ec3ef38c3b174dd1222b2",
96
- "PhoneInput": "c4d999a3cf2bf46ea1ae09da7b9b1a753ccfe77a426f06c6c403d2ae13f53060",
97
- "PieChart": "27ae45992174becb95be0c7bf9a6b5d9b445f0dd82f916ee8beb1f453538ce05",
96
+ "PhoneInput": "2b10c9b1f6efc47e9348f6996397e3b5250f4d49166ec55987c89a51396c65b4",
97
+ "PieChart": "8ec3d6d45f391f05c7946a755865c8a68125e4882ab68e132bd87196d4dc59c9",
98
98
  "Pill": "a0dffa43c0e089ae68d8dd11df5ccca3b83eb0acb5d777752f29475b0bb064a1",
99
99
  "Popover": "91a1fc3fdc5b14b7c01fa5c9393d2b2257ed6da9b97977089f01e8643c0c1080",
100
- "Progress": "baa1150cbcd082436338ecfabb93142c16e05e399eb3eace532b610d05e57a1d",
100
+ "Progress": "42a0a7e700806ff8c31dc793cf4e79fa62b59b6dd310571bcd775d034680081c",
101
101
  "Prose": "36e61f7f8b4a6c74a2a716a73dbf22cb4f86fae092b1518aa0a0e68765cea68d",
102
102
  "RadioGroup": "10423caa00dba5ef159036ba11d3845371e859656dd7c8cb86490f0e63da821f",
103
103
  "Resizable": "a6246108a484cf509fb27385e60b7bddd07ce77af02518e2fa89a4c2f2ebaed7",
@@ -106,15 +106,15 @@
106
106
  "ScrollProgress": "f2b2fcb0234a7eaf1488381037ecc69e93ec55989b1ae6f55d3b8749f5ba93a4",
107
107
  "Schedule": "20cad55139cfc812c3bb221cd17d22fd9ce36af7dfde1c6af444bead018b0d63",
108
108
  "SearchField": "1addd653896a728c84bfa9d39a7a05a01de8a9e7da56ef550c7594de72587a8f",
109
- "Select": "75fdef4ff1f2fd25143128af42ddfcb4a80060ddf5513ab67667659941f74a5a",
109
+ "Select": "861b3fa84249f5ffd9131b3daad461cd53a70d069efe682fccab00a8fa2b823e",
110
110
  "Separator": "552aaaa93c8b96569f471c6f5de52e9951cfa3660f36c8b19202ffd36c009c0b",
111
111
  "Sheet": "1745ea3173d28c270ff9f882e019b162457a31d732e452055d082a5442bc84ea",
112
112
  "Sidebar": "48e299d7419f0172470bf4f12bbe83a191bb6af736478486882850d3bccf0c13",
113
- "Skeleton": "1c6a90f9edbea44310172b01ab10d264e903f1be77f21b2ca12c5722d9b60f26",
113
+ "Skeleton": "f01d4d32ba03d9cad69792a7ce12f1e6989defbe936cb10a11847e307890ac52",
114
114
  "SkipLink": "7dda000ca2f4a06bc12862315543272aaff1dad6065a6283cb81593ee7b00e66",
115
115
  "Slider": "a262873c940a39d0c6fec8815a97562f0b90a24e039e1f80ca896af06bccea16",
116
116
  "Sparkline": "962ac9166de4fae7c6715603151b8db39b008888a2ff9ec52364f8bc328bb233",
117
- "Spinner": "b5d4ad3f91831c71e68cee4c3aea4c43df6cfc3b1cfd51becbffc896b68f9f84",
117
+ "Spinner": "e81ebf2f4bf864e042834b8dba7cf8891d3dfe6c635e19402f811857f9cd9367",
118
118
  "Switch": "78e684f4ba8e616c4a7f2518e9011af16a842b76fd9ff4218cffe60e040b4ee0",
119
119
  "Table": "953879cf84ec1f7e05e6da4f0b690be5bb71a5249e93032895a52ab409dc94c7",
120
120
  "Tabs": "dd4dc58ac56a0ee0435bace62c97e6502b8380e9287bcf1b7651d4aa499259e1",
@@ -131,7 +131,7 @@
131
131
  "Tree": "82fec780732d44258a1870898c9df666c5982433ce625ca3b8bf8d62d0dacd3c",
132
132
  "TreeGrid": "50577908772c53c08d336747253ca7c4527145d94caac2e268e6148cb21255a5",
133
133
  "Typography": "50c18e8bd632c5dff527a239c7e45ee010a619dc5fe1029c9e1b38d1882eecc5",
134
- "VirtualList": "1b3cd9ccef388a165049bd24c3a22b06db6ed3fd2d61fc81c7bc0956843cb9b4",
134
+ "VirtualList": "54f833d758a881b9b71fe68495fe7eeca89c3f53e519ee5ba55cb05d12a48601",
135
135
  "LanguageToggle": "ff65170382ae5b1241ce142b670a32bb708e92fbfbeeac6a285bd39b13c612b6",
136
136
  "Particles": "74334a8aa6578b86f01da2ab63a96ce6c8d888e99f3bcc3ab9845667eb97d27e",
137
137
  "AnimatedNumber": "78cebe9ac425f1f2ab81c336ccaa166ff2007d1814398f451ca67702742aaa5a",
@@ -173,10 +173,10 @@
173
173
  "Affix": "554b64a7393d73e9b53ac7bb32793d16e82b609873c4c53a6ad39f844c1dcb23",
174
174
  "SpeedDial": "7e8c5ceb0cfd8e941343809ccec8109c5686872c29438686a494c330955804a1",
175
175
  "Grid": "d6c8c478ab1a19de7e523eec43a79f694a6ee47cff3907c509ca297b66f21234",
176
- "ScatterChart": "79fcb8ccefd8a3dacbc5343e91473adc68d1179ca78e6d2a8f866d7eb6f51fdf",
177
- "Heatmap": "c9d78df5b911fb09a02204b0bb461deb1bdb871db1004e0cece83362d4b26996",
178
- "RangeChart": "b24bfc330886b2d6d940d7bcb84598e9ad022d6b0f4378e1b78134f12fb1ca08",
179
- "ComboChart": "65ad90a4df445ce47ec36f86930f5103b9835ce7aa95dfaed5415fe06a566936",
176
+ "ScatterChart": "ff64746a2b1a73ad394676e4092859992930391899f991a268a29c9e1fd6c679",
177
+ "Heatmap": "c514471eda6905601d744a6a41526b99eb0b911e8473d5cae6637274a538807b",
178
+ "RangeChart": "0455ea2a195e94ff1f6b7560c66ec5e1cd8edb64fd447ac923d8c1a2451af3be",
179
+ "ComboChart": "2d20981bc182cb0a7876570145acdd66ae1ce9b37943685068ad1a422167ac5c",
180
180
  "VisuallyHidden": "10562829ec1f598aacc2ad819e93c0cd3b257c7eec60818854a08723d9dcc888",
181
181
  "CalendarHeatmap": "45f91bc45a6a50274962146933338b852f57488d52108129f92abe279602054b",
182
182
  "FunnelChart": "a46ee7ad3fde691fee8140469a2a19bf9a2935e85b4ebbb8f57f372eb8b6dbfb",
@@ -191,12 +191,12 @@
191
191
  "ChartMotion": "ae7fdc0ef0673641f0a9d72ccfb4dc5970dde8c05a1329f3fce9f25decf25512",
192
192
  "MotionGroup": "5c18e73d3b2541339d7c8b292c5c44de996bfe265967ee40e74bfe5ec2482d14",
193
193
  "VisualEffect": "fe652fd01c219994f4d23994e0eecbca86e1e5bb96b77f7eedcd9ecb9cc84adc",
194
- "ApprovalCard": "a35a7862a95599afddd3dfb86a6c7167f0e0c2e6cd289b92aacfc2b4ff8b0a83",
195
- "PromptComposer": "7447c7ed545a134793335e20d3c914c6a76ad2f4f2a951172154d2178c984fd7",
194
+ "ApprovalCard": "287b3ae743e5bf452c7b7084fdca0775d1910607729678435e4b19e7586035e0",
195
+ "PromptComposer": "111eab65bf6d59700ec4b3a19b17a45840335906b84363d2d661917b323eb496",
196
196
  "SourceCitation": "07799bb00bf251ca0fa3d6663a7212bb9f07dacdc027e1bd01309a86b2dddb76",
197
197
  "StreamMessage": "c3753a27af188ef2aa3cb4e0e475a788c805a9326f4e010a365d7996d534f550",
198
198
  "ToolActivity": "4501917aa7b7ffe929bde82114d13ab5caf4a9f5ad17fa3a1f806bbe461a52d9",
199
- "WorldMap": "987451974c139ac30b387dc9838a4c5fc97d569d054af33172bf013a9dc47a32"
199
+ "WorldMap": "1b92c9c6bdebcd0e29afe5536ac6ceed3d6c5e79ee4a118097ab89134b60d839"
200
200
  },
201
201
  "nativeComponents": {
202
202
  "Sparkline": "bf7707572d45de0978788f7825831fcb3d89e390b5a474ff8aaf518d412738c6",
@@ -215,44 +215,44 @@
215
215
  "Heatmap": "b6bf78925c07e11be982bd072bb9ae618158968b74473f817a217091962b1f5f",
216
216
  "Range chart": "8ea75e50ab85881f9ba5d8919e2ab173aaf37c1202e8865b95dc4af289a67327",
217
217
  "Combo chart": "65043d6894c8abcd3be2296fde4c3c2bc1d6b2cf72e2116e37193f924a3f4260",
218
- "Theme": "679fe9e0d809e2cc52ca1d15274aee3537faf690666c9eb06a94e56e0c1386ef",
219
- "Text": "a3dcb736b5d6b650cafffdfa71dfeaaf1f14ad19b14b34a5c5cc381d855ac44d",
220
- "Surface": "ce0831778b01062dfc6b7efb19aed8f3b089f61d871e202baab7961f2162e9ea",
218
+ "Theme": "fcc8da2907218646f83c494f9607d2bfe2be05b183f8d843ce92f10714f75fa9",
219
+ "Text": "9c38c1261814e037a58ac93d1b5c2d6008a4409bc404383a14ba96aa46ff8602",
220
+ "Surface": "15e5e51f77b26a3880b4c8bd04df1a596c09f1e5b8f0163803458c473027c243",
221
221
  "Icon": "279318eeae9955028e5fe7ea20080d671bf51d3c090fac80b66f77a540cfd114",
222
222
  "Icon button": "de9debec2cbca588d9bdc05d81a6987db1f56d14684b4c05f147e3950ede9472",
223
- "Button": "ad9ae28ca4b12f472f36bc65fc1d199730bf8f5da732fb1610b6e70940918447",
224
- "Text field": "c14309da104c73d03dd6f351e9e71d24bc1e09d805376cd4bdeba3aa61db722c",
225
- "Badge": "e10c8798c099d2d24b3f264cd954c66b1f6a1f1ec1749acc8af935ea6a3723ac",
226
- "Divider": "6f71136abcf9695cbe83ee2f86dd40de9967a12fd5523fb66a745cb3ab8602e6",
227
- "Spinner": "bebca1b597a0052885e798567ce6e6d0a703cbd25e866d85256e1ffef5938cbe",
223
+ "Button": "3329aab89e51b786bb854636b8b9fdac531559ef072ed2475cd82c92f2496f49",
224
+ "Text field": "138f62d36ec29181ef7fcfc6f91c89da1f97a6f067ed42e16e26a826a0399555",
225
+ "Badge": "a741860205e8aa6bbaae1e35df61b5ed388447e79f68b8d67fe590753f2a772e",
226
+ "Divider": "9a6b2b8994c6793f732cdc9ae4bb967f422a07479c942c5ce2cfe6b00cc39bc0",
227
+ "Spinner": "eb6a4bf6e4737dfd170a575f1e99b5c9acd2337fb73b604a861db0f2b0879c50",
228
228
  "Card": "251bcef892b82f9b9f55497966ebc80136b9b52785ab67eeb3e764e75aa86f72",
229
229
  "Alert": "9a822c4778f64ae81d0dd10c3bd20bbba27858b07026c487c41cbb0eb8df6c18",
230
230
  "Progress": "bd9809fc782b6ec9d2e75d3fdc56231df69127d8c41cd357ecbc608fc4f40d7a",
231
231
  "Skeleton": "67bd48d90b5ee6456da8bdee9a6d6fbb2d010cbe137712f13fed0d26585607db",
232
232
  "Disclosure": "419d032e9d995f9a0a5f4e8156a5c7f16b7c71642a7af4cd72e3f72b49c70628",
233
233
  "Avatar": "eee8db476fd7078ff76126a186d545b930898c11159c89ebfce7f15b3f7b1243",
234
- "Textarea": "256ce88fcd1af9b7ba40a552763fe07825b832e91ed939a4395e6930849ad06d",
235
- "Field group": "e03faa56f4952f2ba3b82e0f43eb4017192b76d9a0d6ee910110c591da511e6f",
236
- "Chip": "fb73b224c223aa6e61b9179529172afe4ee985be99e45262fed67c2f5b3d5827",
237
- "Button group": "559a621914829752ec84042c28c39fe87864592a4d3f7920d7fe8eb053fd7223",
238
- "Toast": "9a30d46c4b0ad1f3cf938bcafb9c5e696d1035833821543110fd5119eb5a41e8",
234
+ "Textarea": "27503a8403070323e49d88ddff0d86251a8804e30053119c41a7be7039b86c23",
235
+ "Field group": "b63191d08ffa40bc86c291a220a0309d315bf78368c15978659944f5a3a151f8",
236
+ "Chip": "3d533ab7de764317e5ebd204b7e2552648642c98891aa1a4ec2b59fa19be2385",
237
+ "Button group": "64493e805809ba9a20441f8a90a382e4bee1fb36d4499003bf0cab48db6bc6cb",
238
+ "Toast": "8c78b8b7a690b86ba74f9ba57a478b64992a48810ee1f9fe2cf0cda7868c37de",
239
239
  "Toggle": "b09bb3c67fb3875e6b5f414d6286b10d45351d5235f0333bec36393431a3a5c6",
240
240
  "Settings row": "371dcb32ec68b57373313f53c57e147fc3bea1298bb6b9c4466fe2b24860a129",
241
- "Picker": "e4f034e3a80e3d67272e9b80475d4f050e710aad9df60c92309b471cc624e914",
242
- "Slider": "ffb206b742ca19045fcc61937ffcd3a9b58992d42de8b654051ec5a3b4f6cdbb",
241
+ "Picker": "594233e68b35afc33eb251dccf2023e752359584b840109895dc56c8885a76bb",
242
+ "Slider": "cd56539eab6423965415a0d6cbd189de72e0f7904249b516f4a94aeff654c02a",
243
243
  "Search field": "95d38f9bc17a33ed877ae4efc96d7ea75268d6a9cf2b20ceca5e6d94155eb60d",
244
244
  "Phone input": "68e405abc8a6868b057d92d30445801b057ab7b831c92e76deb60c4415df6d95",
245
- "Checkbox": "00104539763166630046eb0d5b78afd6e9ff8a3a53797ad651697eac334e290f",
246
- "Radio group": "aa976fd169873524b4c3fbd9a428472f3d2e5bf28258eb62f82a8ac037813d8e",
247
- "Segmented control": "c4c1e7e941f1ff1447504b26eeada5f6df975a51b4a1b5603ff3c7a1e601886a",
248
- "Tabs": "dbb59a0120fc82728953f04a89b6973230a169d0260ecf43bb65fae3586248f3",
245
+ "Checkbox": "ba62a1796d67f5f4851b070816e04dbda76c0912d5bb4beec554271677f8ef47",
246
+ "Radio group": "eeef71b881b5a84031a4b2f8a3b202fe30cafcefdd9d5936787825b1016f93bb",
247
+ "Segmented control": "c8a6c7453ded870a3b587e82d48a5907cd7a8337e300026d2bbb09308988429f",
248
+ "Tabs": "7c2fe9fbb8e48d2e74c38efb7b83658dfa04d15f4fddb06c0a619e78c2755879",
249
249
  "Navigation bar": "4ebb7b649cb490d2db19cd4ddee0e2ebef96e2b8513c7ebabe1750f5bdf630f6",
250
250
  "Empty state": "3aa9d6d38bf30e1b0542eccb743be8ca8305628a70ac548f4d7ba06f506fdc16",
251
251
  "Error state": "4fb2059980bf0aa245be77b6d748056e621643fd1aa3c6013a282818dd0af357",
252
252
  "List row": "858e3c35181b8f8c29f12e2aac8a4c0df44e0f7e10b6ea20b96f476de395fa83",
253
253
  "Banner": "e689f8db9772326e03bf8a69a47751d4d1892033b8c8d37b833206d867ffb145",
254
254
  "Stat": "990c8aad85721625c94e924a1cb0d1611b727dbd33e6738c334f5f60e4b3ecc6",
255
- "Gauge": "f995aa6d2816d2f8b0b7dd21e27737d5df11bf963aaa72cb5de4a02c0c830764",
255
+ "Gauge": "8fe85dcf9666d1107877e101499421b2b80b0de19a7983eb52b2fc991b632f22",
256
256
  "Section header": "9fa81aac8b69c95dc974a4bf03d766dbadef032df92e535b556117e1e88ae257",
257
257
  "Status bar": "e1666b25f04cfe88cfabe664f88184fd83442e588f953da9765ebbc1ba588579",
258
258
  "Collapsible navigation bar": "7348ace3e82a85e039e3ef0e955b456bbbaf776031547a7c6b7082f9fb455084",
@@ -300,7 +300,7 @@
300
300
  "Top app bar": "873805490b3ef07516a879843d785593d6b80120da88ca9514a1856a519e9d0a",
301
301
  "Swipe actions": "8cff687b8a4a9ed528f4bff2f84f88d7f34ae03e51f60854117a40301c40fa6a",
302
302
  "Multi select": "822c0d9b6abdf9638a254aa0c1f28a424fa40250f4edd9ecd62ad6526297a056",
303
- "Range slider": "fe8e95ca5c331b7c68a8938e89fb04ec69d000f9896d9d0d2e31d12db20d34f2",
303
+ "Range slider": "3f0b38a51f7f047a39e3def2fbd8d03fc8b2474978e4200f495dd2aa57db81b1",
304
304
  "Adaptive list detail scaffold": "dad38baad36e690df3c353beb6c91162c120f2342d3f1d4b0f77f58ac22aff06",
305
305
  "Mentions": "15cb940ee9d41b3077072b9b80ff8cd0959bc463f56d6b827c0f25243fb68c2a",
306
306
  "Tour": "1a381c56d08e8a556e9bfbe928d92561a8491ee47bd1fabe04d426b86a634509",
@@ -1724,6 +1724,9 @@
1724
1724
  "values": "React nodes / Astro named slots"
1725
1725
  },
1726
1726
  {
1727
+ "frameworks": [
1728
+ "react"
1729
+ ],
1727
1730
  "attribute": "onStateChange",
1728
1731
  "defaultValue": "-",
1729
1732
  "description": "React callback for transitions after the initial render; DOM adapters emit ui:attachment-preview-change with { state }.",
@@ -1827,7 +1830,6 @@
1827
1830
  "labels",
1828
1831
  "retryKey / retry-key",
1829
1832
  "caption / actions",
1830
- "onStateChange",
1831
1833
  "class, className",
1832
1834
  "...native attributes"
1833
1835
  ],
@@ -2953,6 +2955,9 @@
2953
2955
  "values": "boolean (Astro / Elements attribute)"
2954
2956
  },
2955
2957
  {
2958
+ "frameworks": [
2959
+ "react"
2960
+ ],
2956
2961
  "attribute": "onDatumActivate",
2957
2962
  "defaultValue": "-",
2958
2963
  "description": "Enables React datum actions and receives validated values with raw identities. Navigation and filtering remain application owned.",
@@ -3182,7 +3187,6 @@
3182
3187
  "presentation",
3183
3188
  "glass",
3184
3189
  "drilldown",
3185
- "onDatumActivate",
3186
3190
  "explore-data-label, datum-action-prefix, datumActionFormatter",
3187
3191
  "labels.exploreData, labels.formatDatumAction",
3188
3192
  "series",
@@ -3905,6 +3909,8 @@
3905
3909
  },
3906
3910
  "elements": {
3907
3911
  "attributes": [
3912
+ "aria-busy",
3913
+ "aria-disabled",
3908
3914
  "data-slot",
3909
3915
  "disabled",
3910
3916
  "loading",
@@ -3928,7 +3934,7 @@
3928
3934
  "props": [],
3929
3935
  "propsExtends": "HTMLElement attributes",
3930
3936
  "registration": "defineLumenElements()",
3931
- "source": "lumenButtonElementConfig = {\n attributeClasses: {\n variant: {\n default: 'ui-button--default',\n destructive: 'ui-button--destructive',\n ghost: 'ui-button--ghost',\n link: 'ui-button--link',\n outline: 'ui-button--outline',\n secondary: 'ui-button--secondary'\n },\n size: {\n default: 'ui-button--default-size',\n icon: 'ui-button--icon',\n lg: 'ui-button--lg',\n sm: 'ui-button--sm'\n },\n disabled: { true: 'ui-button--disabled' },\n loading: { true: 'ui-button--loading' }\n },\n baseClassName: 'ui-button',\n defaults: {\n 'data-slot': 'button',\n role: 'button',\n size: 'default',\n tabindex: '0',\n variant: 'default'\n },\n role: 'button',\n tagName: 'lumen-button'\n} as const satisfies LumenElementConfig",
3937
+ "source": "lumenButtonElementConfig = {\n attributeClasses: {\n variant: {\n default: 'ui-button--default',\n destructive: 'ui-button--destructive',\n ghost: 'ui-button--ghost',\n link: 'ui-button--link',\n outline: 'ui-button--outline',\n secondary: 'ui-button--secondary'\n },\n size: {\n default: 'ui-button--default-size',\n icon: 'ui-button--icon',\n lg: 'ui-button--lg',\n sm: 'ui-button--sm'\n },\n disabled: { true: 'ui-button--disabled' },\n loading: { true: 'ui-button--loading' }\n },\n baseClassName: 'ui-button',\n defaults: {\n 'data-slot': 'button',\n role: 'button',\n size: 'default',\n tabindex: '0',\n variant: 'default'\n },\n observedAttributes: ['variant', 'size', 'disabled', 'loading', 'aria-busy', 'aria-disabled'],\n role: 'button',\n tagName: 'lumen-button'\n} as const satisfies LumenElementConfig",
3932
3938
  "styleImport": "import '@santi020k/lumen-elements/styles.css'",
3933
3939
  "tagName": "lumen-button"
3934
3940
  },
@@ -8147,6 +8153,9 @@
8147
8153
  "values": "\"default\" | \"sm\" | \"lg\" | \"icon\""
8148
8154
  },
8149
8155
  {
8156
+ "frameworks": [
8157
+ "react"
8158
+ ],
8150
8159
  "attribute": "copiedContent, errorContent",
8151
8160
  "defaultValue": "localized labels",
8152
8161
  "description": "Customizes visible React feedback while label props remain the accessible announcement.",
@@ -8330,7 +8339,6 @@
8330
8339
  "label, copiedLabel, errorLabel",
8331
8340
  "resetAfter",
8332
8341
  "toast",
8333
- "copiedContent, errorContent",
8334
8342
  "class, className",
8335
8343
  "...native attributes"
8336
8344
  ],
@@ -9329,18 +9337,27 @@
9329
9337
  "values": "\"client\" | \"manual\""
9330
9338
  },
9331
9339
  {
9340
+ "frameworks": [
9341
+ "react"
9342
+ ],
9332
9343
  "attribute": "column.render, renderDetails",
9333
9344
  "defaultValue": "-",
9334
9345
  "description": "Renders rich cells and stable record details without changing sort values.",
9335
9346
  "values": "React render callbacks"
9336
9347
  },
9337
9348
  {
9349
+ "frameworks": [
9350
+ "react"
9351
+ ],
9338
9352
  "attribute": "expandedRowIds, onExpandedRowIdsChange",
9339
9353
  "defaultValue": "-",
9340
9354
  "description": "Preserves expansion by stable record ID across pages.",
9341
9355
  "values": "controlled React expansion"
9342
9356
  },
9343
9357
  {
9358
+ "frameworks": [
9359
+ "react"
9360
+ ],
9344
9361
  "attribute": "DataTableSortControls",
9345
9362
  "defaultValue": "-",
9346
9363
  "description": "Shares controlled sort state with headers for a visible mobile sort path.",
@@ -9472,9 +9489,6 @@
9472
9489
  "glass",
9473
9490
  "layout",
9474
9491
  "sortMode",
9475
- "column.render, renderDetails",
9476
- "expandedRowIds, onExpandedRowIdsChange",
9477
- "DataTableSortControls",
9478
9492
  "columns",
9479
9493
  "rows",
9480
9494
  "name",
@@ -9724,10 +9738,20 @@
9724
9738
  "description": "Applies the tokenized liquid-glass surface treatment with backdrop-filter fallbacks; \"subtle\" and \"strong\" adjust the fill intensity.",
9725
9739
  "values": "boolean | \"subtle\" | \"strong\""
9726
9740
  },
9741
+ {
9742
+ "frameworks": [
9743
+ "astro",
9744
+ "react"
9745
+ ],
9746
+ "attribute": "aria-invalid, aria-describedby, aria-errormessage",
9747
+ "defaultValue": "-",
9748
+ "description": "Mirrored to the visible trigger. Application errors retain precedence. Elements consumers put these attributes on the authored data-ui-date-picker-native input, not the custom-element host.",
9749
+ "values": "native input ARIA attributes"
9750
+ },
9727
9751
  {
9728
9752
  "attribute": "value, defaultValue",
9729
9753
  "defaultValue": "-",
9730
- "description": "Sets the selected date while the trigger shows a localized, human-readable value.",
9754
+ "description": "Sets the selected date while the trigger shows a localized, human-readable value. React reopens the calendar on the selected month.",
9731
9755
  "values": "ISO date string (YYYY-MM-DD)"
9732
9756
  },
9733
9757
  {
@@ -9745,7 +9769,7 @@
9745
9769
  {
9746
9770
  "attribute": "UIPrimitives",
9747
9771
  "defaultValue": "required",
9748
- "description": "Enhances the form-backed input into the custom Calendar popover. React and registered Elements provide equivalent behavior.",
9772
+ "description": "Enhances the form-backed input into a floating Calendar popover that follows scroll and resize and uses the top layer when supported. React and registered Elements provide equivalent behavior.",
9749
9773
  "values": "Astro runtime"
9750
9774
  },
9751
9775
  {
@@ -9841,16 +9865,16 @@
9841
9865
  "available": true,
9842
9866
  "behavior": {
9843
9867
  "mode": "registration",
9844
- "setup": "Call defineLumenElements() once before rendering Lumen custom elements. Registration includes this component’s interactive behavior."
9868
+ "setup": "Call defineLumenElements() once before rendering Lumen custom elements. Registration includes this component’s interactive behavior. Author the native input, trigger, control and popover children. Place aria-invalid, aria-describedby and aria-errormessage on the input marked data-ui-date-picker-native; the enhancer mirrors them to the visible trigger and observes later input attribute changes. Host ARIA attributes are not forwarded."
9845
9869
  },
9846
- "example": "<lumen-date-picker name=\"launchDate\" aria-label=\"Launch date\"></lumen-date-picker>",
9870
+ "example": "<p id=\"launch-date-hint\">Choose a launch date.</p>\n<lumen-date-picker>\n <input class=\"ui-date-picker ui-date-picker__native\" data-ui-date-picker-native type=\"date\" name=\"launchDate\" aria-label=\"Launch date\" aria-describedby=\"launch-date-hint\">\n <div class=\"ui-date-picker__control\" data-ui-date-picker-control hidden>\n <button class=\"ui-input ui-date-picker ui-date-picker__trigger\" data-ui-date-picker-trigger type=\"button\" aria-controls=\"launch-date-popover\" aria-haspopup=\"dialog\" aria-expanded=\"false\"><span data-ui-date-picker-value>Choose a date</span></button>\n <div class=\"ui-date-picker__popover\" data-ui-date-picker-popover id=\"launch-date-popover\" role=\"dialog\" aria-label=\"Choose a launch date\" hidden><lumen-calendar></lumen-calendar></div>\n </div>\n</lumen-date-picker>",
9847
9871
  "importStatement": "import { defineLumenElements } from '@santi020k/lumen-elements/define'",
9848
9872
  "language": "html",
9849
9873
  "packageName": "@santi020k/lumen-elements",
9850
9874
  "props": [],
9851
9875
  "propsExtends": "HTMLElement attributes",
9852
9876
  "registration": "defineLumenElements()",
9853
- "source": "DatePicker: {\n attributeClasses: glassAttributeClasses('ui-date-picker-field--glass'),\n baseClassName: 'ui-input ui-date-picker',\n defaults: { type: 'date' },\n tagName: 'lumen-date-picker'\n }",
9877
+ "source": "DatePicker: {\n attributeClasses: glassAttributeClasses('ui-date-picker-field--glass'),\n baseClassName: 'ui-date-picker-field',\n defaults: { type: 'date' },\n tagName: 'lumen-date-picker'\n }",
9854
9878
  "styleImport": "import '@santi020k/lumen-elements/styles.css'",
9855
9879
  "tagName": "lumen-date-picker"
9856
9880
  },
@@ -9907,7 +9931,7 @@
9907
9931
  }
9908
9932
  ],
9909
9933
  "propsExtends": "Omit<\n ComponentPropsWithoutRef<'input'>,\n 'defaultValue' | 'type' | 'value'\n> & SurfaceProps",
9910
- "source": "export type DatePickerProps = Omit<\n ComponentPropsWithoutRef<'input'>,\n 'defaultValue' | 'type' | 'value'\n> &\nSurfaceProps & {\n defaultValue?: string\n formatDate?: (value: string) => string\n inputRef?: Ref<HTMLInputElement>\n labels?: { chooseDate?: string, previousMonth?: string, nextMonth?: string }\n locale?: string\n onValueChange?: (value: string) => void\n placeholder?: string\n value?: string\n}\n\nexport const DatePicker = ({\n className,\n defaultValue,\n disabled,\n formatDate,\n glass = false,\n id,\n inputRef,\n labels,\n locale,\n max,\n min,\n name,\n onChange,\n onInvalid,\n onValueChange,\n placeholder,\n readOnly,\n required,\n value,\n ...props\n}: DatePickerProps) => {\n const generatedId = useId()\n const datePickerId = id ?? generatedId\n const nativeInputId = `${datePickerId}-native`\n const popoverId = `${datePickerId}-popover`\n const rootRef = useRef<HTMLDivElement | null>(null)\n const nativeInputRef = useRef<HTMLInputElement | null>(null)\n const triggerRef = useRef<HTMLButtonElement | null>(null)\n const [internalValue, setInternalValue] = useState(defaultValue ?? '')\n const { open: isOpen, setOpen } = useDatePickerDisclosure(disabled === true || readOnly === true)\n const selectedValue = value ?? internalValue\n const hasSelectedValue = selectedValue !== ''\n const dateLabels = { ...resolveDateControlLabels(locale || 'en'), ...labels }\n const placeholderText = placeholder ?? dateLabels.chooseDate\n const maxStr = stringifyDatePickerConstraint(max)\n const minStr = stringifyDatePickerConstraint(min)\n const accessibleLabel = props['aria-label']\n\n useEffect(() => {\n const owner = nativeInputRef.current?.form\n let active = true\n let resetTimer: ReturnType<typeof globalThis.setTimeout> | undefined\n\n const reset = (event: Event) => {\n globalThis.clearTimeout(resetTimer)\n\n resetTimer = globalThis.setTimeout(() => {\n if (!active || event.defaultPrevented || !nativeInputRef.current?.isConnected) return\n\n if (value === undefined) setInternalValue(defaultValue ?? '')\n\n setOpen(false)\n })\n }\n\n owner?.addEventListener('reset', reset)\n\n return () => {\n active = false\n\n globalThis.clearTimeout(resetTimer)\n\n owner?.removeEventListener('reset', reset)\n }\n }, [defaultValue, props.form, value, setOpen])\n\n useEffect(() => {\n if (!isOpen) return\n\n const root = rootRef.current\n\n if (!root) return\n\n const owner = root.ownerDocument\n\n const handlePointerDown = (event: MouseEvent) => {\n if (!event.composedPath().includes(root)) setOpen(false)\n }\n\n owner.addEventListener('mousedown', handlePointerDown)\n\n rootRef.current?.querySelector<HTMLElement>('[role=\"gridcell\"][tabindex=\"0\"]')?.focus({ preventScroll: true })\n\n return () => {\n owner.removeEventListener('mousedown', handlePointerDown)\n }\n }, [isOpen, setOpen])\n\n const selectDate = (nextValue: string) => {\n if (disabled || readOnly) return\n\n if (value === undefined) setInternalValue(nextValue)\n\n onValueChange?.(nextValue)\n\n if (nativeInputRef.current) {\n // Use the native setter so React's change event observes the selected value.\n const descriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')\n\n descriptor?.set?.call(nativeInputRef.current, nextValue)\n\n nativeInputRef.current.dispatchEvent(\n new Event('input', { bubbles: true })\n )\n\n nativeInputRef.current.dispatchEvent(\n new Event('change', { bubbles: true })\n )\n }\n\n setOpen(false)\n\n globalThis.setTimeout(() => triggerRef.current?.focus())\n }\n\n return (\n <div\n className={composeClassName(\n 'ui-date-picker-field', glass && 'ui-date-picker-field--glass', glass === 'subtle' && 'ui-glass-subtle', glass === 'strong' && 'ui-glass-strong', className\n )}\n data-placeholder={hasSelectedValue ? undefined : 'true'}\n data-ui-date-picker\n data-ui-glass-track={glass ? true : undefined}\n ref={rootRef}\n onKeyDown={event => {\n if (!isOpen || event.key !== 'Escape') return\n\n event.preventDefault()\n\n event.stopPropagation()\n\n setOpen(false)\n\n triggerRef.current?.focus({ preventScroll: true })\n }}\n >\n <input\n aria-hidden=\"true\"\n className=\"ui-date-picker ui-date-picker__native\"\n data-ui-date-picker-native\n data-ui-enhanced=\"true\"\n disabled={disabled}\n id={nativeInputId}\n max={max}\n min={min}\n name={name}\n onChange={event => {\n if (value === undefined) setInternalValue(event.currentTarget.value)\n\n onChange?.(event)\n }}\n onInvalid={event => {\n event.preventDefault()\n\n triggerRef.current?.setAttribute('aria-invalid', 'true')\n\n triggerRef.current?.focus()\n\n onInvalid?.(event)\n }}\n ref={node => {\n nativeInputRef.current = node\n\n setRefValue(inputRef, node)\n }}\n required={required}\n readOnly={readOnly}\n tabIndex={-1}\n type=\"date\"\n value={selectedValue}\n {...props}\n />\n <div className=\"ui-date-picker__control\" data-ui-date-picker-control>\n <button\n aria-controls={popoverId}\n aria-expanded={isOpen}\n aria-haspopup=\"dialog\"\n aria-label={accessibleLabel}\n aria-labelledby={props['aria-labelledby']}\n aria-describedby={props['aria-describedby']}\n aria-disabled={readOnly ? true : undefined}\n aria-required={required ?? undefined}\n className=\"ui-input ui-date-picker ui-date-picker__trigger\"\n data-ui-date-picker-trigger\n disabled={disabled}\n id={datePickerId}\n onClick={() => {\n if (disabled || readOnly) return\n\n setOpen(!isOpen)\n }}\n onKeyDown={event => {\n if (event.key !== 'ArrowDown' || disabled || readOnly) return\n\n event.preventDefault()\n\n setOpen(true)\n }}\n ref={triggerRef}\n type=\"button\"\n >\n <span data-ui-date-picker-value>\n {formatDatePickerDisplayValue(selectedValue, placeholderText, locale, formatDate)}\n </span>\n <svg\n aria-hidden=\"true\"\n className=\"ui-date-picker__icon\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n >\n <path d=\"M7 2v3M17 2v3M3.5 9h17M5 4h14a2 2 0 0 1 2 2v13a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2Z\" />\n </svg>\n </button>\n <div\n aria-label={dateLabels.chooseDate}\n className=\"ui-date-picker__popover\"\n data-state={isOpen ? 'open' : 'closed'}\n data-ui-date-picker-popover\n hidden={!isOpen}\n id={popoverId}\n role=\"dialog\"\n >\n <Calendar\n disabled={disabled}\n readOnly={readOnly}\n labels={dateLabels}\n locale={locale}\n max={maxStr}\n min={minStr}\n onValueChange={selectDate}\n value={selectedValue}\n />\n </div>\n </div>\n </div>\n )\n}",
9934
+ "source": "export type DatePickerProps = Omit<\n ComponentPropsWithoutRef<'input'>,\n 'defaultValue' | 'type' | 'value'\n> &\nSurfaceProps & {\n defaultValue?: string\n formatDate?: (value: string) => string\n inputRef?: Ref<HTMLInputElement>\n labels?: { chooseDate?: string, previousMonth?: string, nextMonth?: string }\n locale?: string\n onValueChange?: (value: string) => void\n placeholder?: string\n value?: string\n}\n\nexport const DatePicker = ({\n className,\n defaultValue,\n disabled,\n formatDate,\n glass = false,\n id,\n inputRef,\n labels,\n locale,\n max,\n min,\n name,\n onChange,\n onInvalid,\n onValueChange,\n placeholder,\n readOnly,\n required,\n value,\n ...props\n}: DatePickerProps) => {\n const generatedId = useId()\n const datePickerId = id ?? generatedId\n const nativeInputId = `${datePickerId}-native`\n const popoverId = `${datePickerId}-popover`\n const rootRef = useRef<HTMLDivElement | null>(null)\n const nativeInputRef = useRef<HTMLInputElement | null>(null)\n const triggerRef = useRef<HTMLButtonElement | null>(null)\n const popoverRef = useRef<HTMLDivElement | null>(null)\n const [internalValue, setInternalValue] = useState(defaultValue ?? '')\n const { open: isOpen, setOpen } = useDatePickerDisclosure(disabled === true || readOnly === true)\n const selectedValue = value ?? internalValue\n const hasSelectedValue = selectedValue !== ''\n const dateLabels = { ...resolveDateControlLabels(locale || 'en'), ...labels }\n const placeholderText = placeholder ?? dateLabels.chooseDate\n const maxStr = stringifyDatePickerConstraint(max)\n const minStr = stringifyDatePickerConstraint(min)\n const accessibleLabel = props['aria-label']\n const consumerInvalid = props['aria-invalid']\n const invalidAttemptedRef = useRef(false)\n\n const closePicker = useCallback(() => {\n setOpen(false)\n }, [setOpen])\n\n // Lift the calendar into the top layer so scrolling dialogs cannot clip it.\n useFloatingPanel(isOpen, triggerRef, popoverRef, closePicker, {})\n\n const syncValidity = useCallback(() => {\n const native = nativeInputRef.current\n const trigger = triggerRef.current\n\n if (!native || !trigger) return\n\n if (native.validity.valid) invalidAttemptedRef.current = false\n\n const invalid = consumerInvalid ?? (invalidAttemptedRef.current ? true : undefined)\n\n if (invalid === undefined) trigger.removeAttribute('aria-invalid')\n else trigger.setAttribute('aria-invalid', String(invalid))\n }, [consumerInvalid])\n\n useLayoutEffect(syncValidity, [disabled, max, min, readOnly, required, selectedValue, syncValidity])\n\n useEffect(() => {\n const owner = nativeInputRef.current?.form\n let active = true\n let resetTimer: ReturnType<typeof globalThis.setTimeout> | undefined\n\n const reset = (event: Event) => {\n globalThis.clearTimeout(resetTimer)\n\n resetTimer = globalThis.setTimeout(() => {\n if (!active || event.defaultPrevented || !nativeInputRef.current?.isConnected) return\n\n invalidAttemptedRef.current = false\n\n syncValidity()\n\n if (value === undefined) setInternalValue(defaultValue ?? '')\n\n setOpen(false)\n })\n }\n\n owner?.addEventListener('reset', reset)\n\n return () => {\n active = false\n\n globalThis.clearTimeout(resetTimer)\n\n owner?.removeEventListener('reset', reset)\n }\n }, [defaultValue, props.form, value, setOpen, syncValidity])\n\n useEffect(() => {\n if (!isOpen) return\n\n const root = rootRef.current\n\n if (!root) return\n\n const owner = root.ownerDocument\n\n const handlePointerDown = (event: MouseEvent) => {\n if (!event.composedPath().includes(root)) setOpen(false)\n }\n\n const handleKeyDown = (event: globalThis.KeyboardEvent) => {\n if (event.key !== 'Escape' || event.defaultPrevented || event.isComposing) return\n\n event.preventDefault()\n\n setOpen(false)\n\n triggerRef.current?.focus({ preventScroll: true })\n }\n\n owner.addEventListener('keydown', handleKeyDown)\n\n owner.addEventListener('mousedown', handlePointerDown)\n\n rootRef.current?.querySelector<HTMLElement>('[role=\"gridcell\"][tabindex=\"0\"]')?.focus({ preventScroll: true })\n\n return () => {\n owner.removeEventListener('mousedown', handlePointerDown)\n\n owner.removeEventListener('keydown', handleKeyDown)\n }\n }, [isOpen, setOpen])\n\n const selectDate = (nextValue: string) => {\n if (disabled || readOnly) return\n\n if (value === undefined) setInternalValue(nextValue)\n\n onValueChange?.(nextValue)\n\n if (nativeInputRef.current) {\n // Use the native setter so React's change event observes the selected value.\n const descriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')\n\n descriptor?.set?.call(nativeInputRef.current, nextValue)\n\n nativeInputRef.current.dispatchEvent(\n new Event('input', { bubbles: true })\n )\n\n nativeInputRef.current.dispatchEvent(\n new Event('change', { bubbles: true })\n )\n }\n\n setOpen(false)\n\n globalThis.setTimeout(() => triggerRef.current?.focus())\n }\n\n return (\n <div\n className={composeClassName(\n 'ui-date-picker-field', glass && 'ui-date-picker-field--glass', glass === 'subtle' && 'ui-glass-subtle', glass === 'strong' && 'ui-glass-strong', className\n )}\n data-placeholder={hasSelectedValue ? undefined : 'true'}\n data-ui-date-picker\n data-ui-owner=\"react\"\n data-ui-glass-track={glass ? true : undefined}\n ref={rootRef}\n onKeyDown={event => {\n if (!isOpen || event.key !== 'Escape' || event.defaultPrevented || event.nativeEvent.isComposing) return\n\n event.preventDefault()\n\n event.stopPropagation()\n\n setOpen(false)\n\n triggerRef.current?.focus({ preventScroll: true })\n }}\n >\n <input\n aria-hidden=\"true\"\n className=\"ui-date-picker ui-date-picker__native\"\n data-ui-date-picker-native\n data-ui-enhanced=\"true\"\n disabled={disabled}\n id={nativeInputId}\n max={max}\n min={min}\n name={name}\n onChange={event => {\n if (value === undefined) setInternalValue(event.currentTarget.value)\n\n syncValidity()\n\n onChange?.(event)\n }}\n onInvalid={event => {\n event.preventDefault()\n\n invalidAttemptedRef.current = true\n\n syncValidity()\n\n triggerRef.current?.focus()\n\n onInvalid?.(event)\n }}\n ref={node => {\n nativeInputRef.current = node\n\n setRefValue(inputRef, node)\n }}\n required={required}\n readOnly={readOnly}\n tabIndex={-1}\n type=\"date\"\n value={selectedValue}\n {...props}\n />\n <div className=\"ui-date-picker__control\" data-ui-date-picker-control>\n <button\n aria-controls={popoverId}\n aria-expanded={isOpen}\n aria-haspopup=\"dialog\"\n aria-label={accessibleLabel}\n aria-labelledby={props['aria-labelledby']}\n aria-describedby={props['aria-describedby']}\n aria-errormessage={props['aria-errormessage']}\n aria-invalid={consumerInvalid}\n aria-disabled={readOnly ? true : undefined}\n aria-required={required ?? undefined}\n className=\"ui-input ui-date-picker ui-date-picker__trigger\"\n data-ui-date-picker-trigger\n disabled={disabled}\n id={datePickerId}\n onClick={() => {\n if (disabled || readOnly) return\n\n setOpen(!isOpen)\n }}\n onKeyDown={event => {\n if (event.key !== 'ArrowDown' || disabled || readOnly) return\n\n event.preventDefault()\n\n setOpen(true)\n }}\n ref={triggerRef}\n type=\"button\"\n >\n <span data-ui-date-picker-value>\n {formatDatePickerDisplayValue(selectedValue, placeholderText, locale, formatDate)}\n </span>\n <svg\n aria-hidden=\"true\"\n className=\"ui-date-picker__icon\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n >\n <path d=\"M7 2v3M17 2v3M3.5 9h17M5 4h14a2 2 0 0 1 2 2v13a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2Z\" />\n </svg>\n </button>\n <div\n aria-label={dateLabels.chooseDate}\n className=\"ui-date-picker__popover\"\n data-state={isOpen ? 'open' : 'closed'}\n data-ui-date-picker-popover\n hidden={!isOpen}\n id={popoverId}\n ref={popoverRef}\n role=\"dialog\"\n >\n <Calendar\n key={isOpen ? 'open' : 'closed'}\n disabled={disabled}\n readOnly={readOnly}\n labels={dateLabels}\n locale={locale}\n max={maxStr}\n min={minStr}\n onValueChange={selectDate}\n value={selectedValue}\n />\n </div>\n </div>\n </div>\n )\n}",
9911
9935
  "styleImport": "import '@santi020k/lumen-react/styles.css'"
9912
9936
  }
9913
9937
  },
@@ -10016,6 +10040,9 @@
10016
10040
  "values": "two DatePicker controls"
10017
10041
  },
10018
10042
  {
10043
+ "frameworks": [
10044
+ "react"
10045
+ ],
10019
10046
  "attribute": "onRangeChange",
10020
10047
  "defaultValue": "-",
10021
10048
  "description": "React only: receives the synchronized start and end ISO date values.",
@@ -10098,7 +10125,6 @@
10098
10125
  "attributes": [
10099
10126
  "data-ui-date-range-picker",
10100
10127
  "children",
10101
- "onRangeChange",
10102
10128
  "data-range-state",
10103
10129
  "class, className",
10104
10130
  "...native attributes"
@@ -11186,6 +11212,9 @@
11186
11212
  "values": "boolean"
11187
11213
  },
11188
11214
  {
11215
+ "frameworks": [
11216
+ "react"
11217
+ ],
11189
11218
  "attribute": "onClick",
11190
11219
  "defaultValue": "-",
11191
11220
  "description": "Calling preventDefault cancels dismissal.",
@@ -11275,7 +11304,6 @@
11275
11304
  "data-ui-dialog-close",
11276
11305
  "children",
11277
11306
  "disabled / loading",
11278
- "onClick",
11279
11307
  "class, className",
11280
11308
  "...native attributes"
11281
11309
  ],
@@ -14674,6 +14702,9 @@
14674
14702
  "values": "boolean"
14675
14703
  },
14676
14704
  {
14705
+ "frameworks": [
14706
+ "react"
14707
+ ],
14677
14708
  "attribute": "as",
14678
14709
  "defaultValue": "\"img\"",
14679
14710
  "description": "React only: renders a compatible optimizer such as next/image while retaining Lumen classes and forwarding optimizer props.",
@@ -14762,7 +14793,6 @@
14762
14793
  "priority",
14763
14794
  "preload, fill, placeholder",
14764
14795
  "invertOnDark",
14765
- "as",
14766
14796
  "class, className",
14767
14797
  "...native attributes"
14768
14798
  ],
@@ -15187,6 +15217,9 @@
15187
15217
  "values": "number"
15188
15218
  },
15189
15219
  {
15220
+ "frameworks": [
15221
+ "react"
15222
+ ],
15190
15223
  "attribute": "onValueChange",
15191
15224
  "defaultValue": "-",
15192
15225
  "description": "React callback for user changes. Astro and Elements emit ui:image-comparison-change with { value }.",
@@ -15345,7 +15378,6 @@
15345
15378
  "before / after",
15346
15379
  "beforeLabel / afterLabel",
15347
15380
  "value / defaultValue",
15348
- "onValueChange",
15349
15381
  "class, className",
15350
15382
  "...native attributes"
15351
15383
  ],
@@ -15584,6 +15616,9 @@
15584
15616
  "values": "number"
15585
15617
  },
15586
15618
  {
15619
+ "frameworks": [
15620
+ "react"
15621
+ ],
15587
15622
  "attribute": "onValueChange",
15588
15623
  "defaultValue": "-",
15589
15624
  "description": "React callback; Astro and Elements emit ui:media-viewport-change with { zoom, x, y }.",
@@ -15706,7 +15741,6 @@
15706
15741
  "label / labels",
15707
15742
  "value / defaultValue",
15708
15743
  "maxZoom / ratio",
15709
- "onValueChange",
15710
15744
  "disabled / locale",
15711
15745
  "children",
15712
15746
  "class, className",
@@ -18249,6 +18283,9 @@
18249
18283
  "values": "LumenChartAnnotation[]"
18250
18284
  },
18251
18285
  {
18286
+ "frameworks": [
18287
+ "react"
18288
+ ],
18252
18289
  "attribute": "cursor, onCursorChange",
18253
18290
  "defaultValue": "uncontrolled",
18254
18291
  "description": "React can supply the active X identity and receive cursor changes. Astro and Elements emit ui:chart-cursor-change.",
@@ -18267,6 +18304,9 @@
18267
18304
  "values": "boolean (Astro / Elements attribute)"
18268
18305
  },
18269
18306
  {
18307
+ "frameworks": [
18308
+ "react"
18309
+ ],
18270
18310
  "attribute": "onDatumActivate",
18271
18311
  "defaultValue": "-",
18272
18312
  "description": "Enables React datum actions and receives validated values with raw identities. Navigation and filtering remain application owned.",
@@ -18505,10 +18545,8 @@
18505
18545
  "domain, xDomain",
18506
18546
  "interactive, syncGroup",
18507
18547
  "annotations",
18508
- "cursor, onCursorChange",
18509
18548
  "width, height",
18510
18549
  "drilldown",
18511
- "onDatumActivate",
18512
18550
  "explore-data-label, datum-action-prefix, datumActionFormatter",
18513
18551
  "labels.exploreData, labels.formatDatumAction",
18514
18552
  "series",
@@ -18820,12 +18858,67 @@
18820
18858
  {
18821
18859
  "apiReference": [
18822
18860
  {
18861
+ "frameworks": [
18862
+ "elements"
18863
+ ],
18864
+ "attribute": "empty-message, data-selection-follows-focus",
18865
+ "defaultValue": "\"No options available\", \"false\"",
18866
+ "description": "Sets the Elements empty-state text and optionally commits selection as keyboard focus moves.",
18867
+ "values": "string, boolean string"
18868
+ },
18869
+ {
18870
+ "frameworks": [
18871
+ "elements"
18872
+ ],
18873
+ "attribute": "aria-label, required",
18874
+ "defaultValue": "-",
18875
+ "description": "Names the Elements visible list and marks its generated select required. For other label or feedback associations, author a native select child and put required and ARIA attributes on that select.",
18876
+ "values": "string, boolean"
18877
+ },
18878
+ {
18879
+ "frameworks": [
18880
+ "astro",
18881
+ "react"
18882
+ ],
18883
+ "attribute": "label, aria-label, aria-labelledby",
18884
+ "defaultValue": "-",
18885
+ "description": "Names the single accessible visible list. External labels targeting or wrapping the native select also name it; explicit ARIA names take precedence.",
18886
+ "values": "string"
18887
+ },
18888
+ {
18889
+ "frameworks": [
18890
+ "astro",
18891
+ "react"
18892
+ ],
18893
+ "attribute": "required, aria-invalid, aria-describedby, aria-errormessage",
18894
+ "defaultValue": "-",
18895
+ "description": "Forwarded to the visible list. Required single selection stays invalid until a choice is made; native validation focuses the list. Consumer aria-invalid wins.",
18896
+ "values": "native select attributes"
18897
+ },
18898
+ {
18899
+ "frameworks": [
18900
+ "react"
18901
+ ],
18902
+ "attribute": "onFocus, onBlur",
18903
+ "defaultValue": "-",
18904
+ "description": "Fire once when focus enters or leaves the visible list, with the native HTMLSelectElement event target and form name.",
18905
+ "values": "React select focus event handlers"
18906
+ },
18907
+ {
18908
+ "frameworks": [
18909
+ "astro",
18910
+ "react"
18911
+ ],
18823
18912
  "attribute": "options",
18824
18913
  "defaultValue": "[]",
18825
18914
  "description": "Defines options and optional accessible sections.",
18826
18915
  "values": "Array<string | { label, value, disabled?, section? }>"
18827
18916
  },
18828
18917
  {
18918
+ "frameworks": [
18919
+ "astro",
18920
+ "react"
18921
+ ],
18829
18922
  "attribute": "value",
18830
18923
  "defaultValue": "-",
18831
18924
  "description": "Controls the selected value or values.",
@@ -18844,12 +18937,20 @@
18844
18937
  "values": "boolean"
18845
18938
  },
18846
18939
  {
18940
+ "frameworks": [
18941
+ "astro",
18942
+ "react"
18943
+ ],
18847
18944
  "attribute": "emptyMessage",
18848
18945
  "defaultValue": "\"No options available\"",
18849
18946
  "description": "Labels the empty collection state.",
18850
18947
  "values": "string"
18851
18948
  },
18852
18949
  {
18950
+ "frameworks": [
18951
+ "astro",
18952
+ "react"
18953
+ ],
18853
18954
  "attribute": "selectionFollowsFocus",
18854
18955
  "defaultValue": "false",
18855
18956
  "description": "Selects each enabled option as keyboard focus moves instead of waiting for Space or Enter.",
@@ -18868,7 +18969,7 @@
18868
18969
  "values": "HTML attributes"
18869
18970
  }
18870
18971
  ],
18871
- "astroSource": "---\nimport type { HTMLAttributes } from 'astro/types'\n\nimport { resolveLumenAstroProps } from '@santi020k/lumen-core'\n\ninterface ListBoxOption {\n disabled?: boolean\n label: string\n section?: string\n value: string\n}\n\ninterface Props extends Omit<HTMLAttributes<'select'>, 'size' | 'value'> {\n class?: string | null\n className?: string\n emptyMessage?: string\n label?: string\n loading?: boolean\n loadingMessage?: string\n multiple?: boolean\n options?: (ListBoxOption | string)[]\n selectionFollowsFocus?: boolean\n value?: string | string[]\n}\n\nconst {\n class: classProp = '',\n className = '',\n disabled = false,\n emptyMessage = 'No options available',\n id,\n label,\n loading = false,\n loadingMessage = 'Loading options',\n multiple = false,\n name,\n options = [],\n required = false,\n selectionFollowsFocus = false,\n value,\n ...rest\n} = Astro.props\n\nconst normalizedOptions = options.map(option => typeof option === 'string' ? { label: option, value: option } : option)\nconst optionGroups: { label?: string, options: ListBoxOption[] }[] = []\n\nfor (const option of normalizedOptions) {\n const currentGroup = optionGroups.at(-1)\n\n if (!currentGroup || currentGroup.label !== option.section) {\n optionGroups.push({\n ...(option.section ? { label: option.section } : {}),\n options: [option]\n })\n } else {\n currentGroup.options.push(option)\n }\n}\n\nlet initialValues: string[] = []\n\nif (Array.isArray(value)) initialValues = value\nelse if (value !== undefined) initialValues = [value]\n\nconst selectedValues = new Set(initialValues)\nconst controlId = id ?? `ui-list-box-${name ?? 'field'}`\nconst listId = `${controlId}-list`\nconst { classList, passthrough } = resolveLumenAstroProps(rest, ['ui-list-box'], classProp, className)\n---\n\n<div\n class:list={classList}\n data-loading={loading ? 'true' : undefined}\n data-multiple={multiple ? 'true' : undefined}\n data-selection-follows-focus={selectionFollowsFocus ? 'true' : undefined}\n data-ui-list-box\n>\n {label && <label class=\"ui-label\" for={controlId}>{label}</label>}\n <select\n class=\"ui-list-box__native\"\n data-ui-list-box-native\n disabled={disabled || loading}\n id={controlId}\n multiple={multiple}\n name={name}\n required={required}\n size={Math.min(Math.max(normalizedOptions.length, 2), 8)}\n {...passthrough}\n >\n {optionGroups.map(group => group.label ?\n (\n <optgroup label={group.label}>\n {group.options.map(option => (\n <option\n disabled={option.disabled}\n selected={selectedValues.has(option.value)}\n value={option.value}\n >\n {option.label}\n </option>\n ))}\n </optgroup>\n ) :\n group.options.map(option => (\n <option\n disabled={option.disabled}\n selected={selectedValues.has(option.value)}\n value={option.value}\n >\n {option.label}\n </option>\n )))}\n </select>\n <div\n aria-busy={loading ? 'true' : undefined}\n aria-disabled={disabled || loading ? 'true' : undefined}\n aria-multiselectable={multiple ? 'true' : undefined}\n class=\"ui-list-box__list\"\n data-ui-list-box-list\n hidden\n id={listId}\n role=\"listbox\"\n tabindex={disabled || loading ? undefined : '0'}\n >\n {!loading && !normalizedOptions.length && (\n <div aria-disabled=\"true\" class=\"ui-list-box__empty\" role=\"option\">{emptyMessage}</div>\n )}\n {!loading && optionGroups.map((group, groupIndex) => {\n const groupLabelId = group.label ? `${controlId}-group-${groupIndex}` : undefined\n\n const renderedOptions = group.options.map(option => (\n <div\n aria-disabled={option.disabled ? 'true' : undefined}\n aria-selected={selectedValues.has(option.value) ? 'true' : 'false'}\n data-ui-list-box-option\n data-value={option.value}\n role=\"option\"\n >\n {option.label}\n </div>\n ))\n\n return group.label ?\n (\n <div aria-labelledby={groupLabelId} role=\"group\">\n <div class=\"ui-list-box__section\" id={groupLabelId}>{group.label}</div>\n {renderedOptions}\n </div>\n ) :\n renderedOptions\n })}\n </div>\n {loading && <p class=\"ui-list-box__status\" role=\"status\">{loadingMessage}</p>}\n</div>\n",
18972
+ "astroSource": "---\nimport type { HTMLAttributes } from 'astro/types'\n\nimport { resolveLumenAstroProps } from '@santi020k/lumen-core'\n\ninterface ListBoxOption {\n disabled?: boolean\n label: string\n section?: string\n value: string\n}\n\ninterface Props extends Omit<HTMLAttributes<'select'>, 'size' | 'value'> {\n class?: string | null\n className?: string\n emptyMessage?: string\n label?: string\n loading?: boolean\n loadingMessage?: string\n multiple?: boolean\n options?: (ListBoxOption | string)[]\n selectionFollowsFocus?: boolean\n value?: string | string[]\n}\n\nconst {\n class: classProp = '',\n className = '',\n disabled = false,\n emptyMessage = 'No options available',\n id,\n label,\n loading = false,\n loadingMessage = 'Loading options',\n multiple = false,\n name,\n options = [],\n required = false,\n selectionFollowsFocus = false,\n value,\n ...rest\n} = Astro.props\n\nconst normalizedOptions = options.map(option => typeof option === 'string' ? { label: option, value: option } : option)\nconst optionGroups: { label?: string, options: ListBoxOption[] }[] = []\n\nfor (const option of normalizedOptions) {\n const currentGroup = optionGroups.at(-1)\n\n if (!currentGroup || currentGroup.label !== option.section) {\n optionGroups.push({\n ...(option.section ? { label: option.section } : {}),\n options: [option]\n })\n } else {\n currentGroup.options.push(option)\n }\n}\n\nlet initialValues: string[] = []\n\nif (Array.isArray(value)) initialValues = value\nelse if (value !== undefined) initialValues = [value]\n\nconst selectedValues = new Set(initialValues)\n// Named lists keep their stable id; unnamed lists need a unique one so labels and options never collide.\nconst controlId = id ?? (name ? `ui-list-box-${name}` : `ui-list-box-${Math.random().toString(36).slice(2)}`)\nconst listId = `${controlId}-list`\nconst { classList, passthrough } = resolveLumenAstroProps(rest, ['ui-list-box'], classProp, className)\n---\n\n<div\n class:list={classList}\n data-loading={loading ? 'true' : undefined}\n data-multiple={multiple ? 'true' : undefined}\n data-selection-follows-focus={selectionFollowsFocus ? 'true' : undefined}\n data-ui-list-box\n>\n {label && <label class=\"ui-label\" for={controlId}>{label}</label>}\n <select\n class=\"ui-list-box__native\"\n data-ui-list-box-native\n disabled={disabled || loading}\n id={controlId}\n multiple={multiple}\n name={name}\n required={required}\n size={Math.min(Math.max(normalizedOptions.length, 2), 8)}\n {...passthrough}\n >\n {optionGroups.map(group => group.label ?\n (\n <optgroup label={group.label}>\n {group.options.map(option => (\n <option\n disabled={option.disabled}\n selected={selectedValues.has(option.value)}\n value={option.value}\n >\n {option.label}\n </option>\n ))}\n </optgroup>\n ) :\n group.options.map(option => (\n <option\n disabled={option.disabled}\n selected={selectedValues.has(option.value)}\n value={option.value}\n >\n {option.label}\n </option>\n )))}\n </select>\n <div\n aria-busy={loading ? 'true' : undefined}\n aria-disabled={disabled || loading ? 'true' : undefined}\n aria-multiselectable={multiple ? 'true' : undefined}\n class=\"ui-list-box__list\"\n data-ui-list-box-list\n hidden\n id={listId}\n role=\"listbox\"\n tabindex={disabled || loading ? undefined : '0'}\n >\n {!loading && !normalizedOptions.length && (\n <div aria-disabled=\"true\" class=\"ui-list-box__empty\" role=\"option\">{emptyMessage}</div>\n )}\n {!loading && optionGroups.map((group, groupIndex) => {\n const groupLabelId = group.label ? `${controlId}-group-${groupIndex}` : undefined\n\n const renderedOptions = group.options.map(option => (\n <div\n aria-disabled={option.disabled ? 'true' : undefined}\n aria-selected={selectedValues.has(option.value) ? 'true' : 'false'}\n data-ui-list-box-option\n data-value={option.value}\n role=\"option\"\n >\n {option.label}\n </div>\n ))\n\n return group.label ?\n (\n <div aria-labelledby={groupLabelId} role=\"group\">\n <div class=\"ui-list-box__section\" id={groupLabelId}>{group.label}</div>\n {renderedOptions}\n </div>\n ) :\n renderedOptions\n })}\n </div>\n {loading && <p class=\"ui-list-box__status\" role=\"status\">{loadingMessage}</p>}\n</div>\n",
18872
18973
  "category": "Forms",
18873
18974
  "collections": [],
18874
18975
  "contract": null,
@@ -18951,18 +19052,16 @@
18951
19052
  }
18952
19053
  ],
18953
19054
  "propsExtends": "Omit<HTMLAttributes<'select'>, 'size' | 'value'>",
18954
- "source": "---\nimport type { HTMLAttributes } from 'astro/types'\n\nimport { resolveLumenAstroProps } from '@santi020k/lumen-core'\n\ninterface ListBoxOption {\n disabled?: boolean\n label: string\n section?: string\n value: string\n}\n\ninterface Props extends Omit<HTMLAttributes<'select'>, 'size' | 'value'> {\n class?: string | null\n className?: string\n emptyMessage?: string\n label?: string\n loading?: boolean\n loadingMessage?: string\n multiple?: boolean\n options?: (ListBoxOption | string)[]\n selectionFollowsFocus?: boolean\n value?: string | string[]\n}\n\nconst {\n class: classProp = '',\n className = '',\n disabled = false,\n emptyMessage = 'No options available',\n id,\n label,\n loading = false,\n loadingMessage = 'Loading options',\n multiple = false,\n name,\n options = [],\n required = false,\n selectionFollowsFocus = false,\n value,\n ...rest\n} = Astro.props\n\nconst normalizedOptions = options.map(option => typeof option === 'string' ? { label: option, value: option } : option)\nconst optionGroups: { label?: string, options: ListBoxOption[] }[] = []\n\nfor (const option of normalizedOptions) {\n const currentGroup = optionGroups.at(-1)\n\n if (!currentGroup || currentGroup.label !== option.section) {\n optionGroups.push({\n ...(option.section ? { label: option.section } : {}),\n options: [option]\n })\n } else {\n currentGroup.options.push(option)\n }\n}\n\nlet initialValues: string[] = []\n\nif (Array.isArray(value)) initialValues = value\nelse if (value !== undefined) initialValues = [value]\n\nconst selectedValues = new Set(initialValues)\nconst controlId = id ?? `ui-list-box-${name ?? 'field'}`\nconst listId = `${controlId}-list`\nconst { classList, passthrough } = resolveLumenAstroProps(rest, ['ui-list-box'], classProp, className)\n---\n\n<div\n class:list={classList}\n data-loading={loading ? 'true' : undefined}\n data-multiple={multiple ? 'true' : undefined}\n data-selection-follows-focus={selectionFollowsFocus ? 'true' : undefined}\n data-ui-list-box\n>\n {label && <label class=\"ui-label\" for={controlId}>{label}</label>}\n <select\n class=\"ui-list-box__native\"\n data-ui-list-box-native\n disabled={disabled || loading}\n id={controlId}\n multiple={multiple}\n name={name}\n required={required}\n size={Math.min(Math.max(normalizedOptions.length, 2), 8)}\n {...passthrough}\n >\n {optionGroups.map(group => group.label ?\n (\n <optgroup label={group.label}>\n {group.options.map(option => (\n <option\n disabled={option.disabled}\n selected={selectedValues.has(option.value)}\n value={option.value}\n >\n {option.label}\n </option>\n ))}\n </optgroup>\n ) :\n group.options.map(option => (\n <option\n disabled={option.disabled}\n selected={selectedValues.has(option.value)}\n value={option.value}\n >\n {option.label}\n </option>\n )))}\n </select>\n <div\n aria-busy={loading ? 'true' : undefined}\n aria-disabled={disabled || loading ? 'true' : undefined}\n aria-multiselectable={multiple ? 'true' : undefined}\n class=\"ui-list-box__list\"\n data-ui-list-box-list\n hidden\n id={listId}\n role=\"listbox\"\n tabindex={disabled || loading ? undefined : '0'}\n >\n {!loading && !normalizedOptions.length && (\n <div aria-disabled=\"true\" class=\"ui-list-box__empty\" role=\"option\">{emptyMessage}</div>\n )}\n {!loading && optionGroups.map((group, groupIndex) => {\n const groupLabelId = group.label ? `${controlId}-group-${groupIndex}` : undefined\n\n const renderedOptions = group.options.map(option => (\n <div\n aria-disabled={option.disabled ? 'true' : undefined}\n aria-selected={selectedValues.has(option.value) ? 'true' : 'false'}\n data-ui-list-box-option\n data-value={option.value}\n role=\"option\"\n >\n {option.label}\n </div>\n ))\n\n return group.label ?\n (\n <div aria-labelledby={groupLabelId} role=\"group\">\n <div class=\"ui-list-box__section\" id={groupLabelId}>{group.label}</div>\n {renderedOptions}\n </div>\n ) :\n renderedOptions\n })}\n </div>\n {loading && <p class=\"ui-list-box__status\" role=\"status\">{loadingMessage}</p>}\n</div>\n",
19055
+ "source": "---\nimport type { HTMLAttributes } from 'astro/types'\n\nimport { resolveLumenAstroProps } from '@santi020k/lumen-core'\n\ninterface ListBoxOption {\n disabled?: boolean\n label: string\n section?: string\n value: string\n}\n\ninterface Props extends Omit<HTMLAttributes<'select'>, 'size' | 'value'> {\n class?: string | null\n className?: string\n emptyMessage?: string\n label?: string\n loading?: boolean\n loadingMessage?: string\n multiple?: boolean\n options?: (ListBoxOption | string)[]\n selectionFollowsFocus?: boolean\n value?: string | string[]\n}\n\nconst {\n class: classProp = '',\n className = '',\n disabled = false,\n emptyMessage = 'No options available',\n id,\n label,\n loading = false,\n loadingMessage = 'Loading options',\n multiple = false,\n name,\n options = [],\n required = false,\n selectionFollowsFocus = false,\n value,\n ...rest\n} = Astro.props\n\nconst normalizedOptions = options.map(option => typeof option === 'string' ? { label: option, value: option } : option)\nconst optionGroups: { label?: string, options: ListBoxOption[] }[] = []\n\nfor (const option of normalizedOptions) {\n const currentGroup = optionGroups.at(-1)\n\n if (!currentGroup || currentGroup.label !== option.section) {\n optionGroups.push({\n ...(option.section ? { label: option.section } : {}),\n options: [option]\n })\n } else {\n currentGroup.options.push(option)\n }\n}\n\nlet initialValues: string[] = []\n\nif (Array.isArray(value)) initialValues = value\nelse if (value !== undefined) initialValues = [value]\n\nconst selectedValues = new Set(initialValues)\n// Named lists keep their stable id; unnamed lists need a unique one so labels and options never collide.\nconst controlId = id ?? (name ? `ui-list-box-${name}` : `ui-list-box-${Math.random().toString(36).slice(2)}`)\nconst listId = `${controlId}-list`\nconst { classList, passthrough } = resolveLumenAstroProps(rest, ['ui-list-box'], classProp, className)\n---\n\n<div\n class:list={classList}\n data-loading={loading ? 'true' : undefined}\n data-multiple={multiple ? 'true' : undefined}\n data-selection-follows-focus={selectionFollowsFocus ? 'true' : undefined}\n data-ui-list-box\n>\n {label && <label class=\"ui-label\" for={controlId}>{label}</label>}\n <select\n class=\"ui-list-box__native\"\n data-ui-list-box-native\n disabled={disabled || loading}\n id={controlId}\n multiple={multiple}\n name={name}\n required={required}\n size={Math.min(Math.max(normalizedOptions.length, 2), 8)}\n {...passthrough}\n >\n {optionGroups.map(group => group.label ?\n (\n <optgroup label={group.label}>\n {group.options.map(option => (\n <option\n disabled={option.disabled}\n selected={selectedValues.has(option.value)}\n value={option.value}\n >\n {option.label}\n </option>\n ))}\n </optgroup>\n ) :\n group.options.map(option => (\n <option\n disabled={option.disabled}\n selected={selectedValues.has(option.value)}\n value={option.value}\n >\n {option.label}\n </option>\n )))}\n </select>\n <div\n aria-busy={loading ? 'true' : undefined}\n aria-disabled={disabled || loading ? 'true' : undefined}\n aria-multiselectable={multiple ? 'true' : undefined}\n class=\"ui-list-box__list\"\n data-ui-list-box-list\n hidden\n id={listId}\n role=\"listbox\"\n tabindex={disabled || loading ? undefined : '0'}\n >\n {!loading && !normalizedOptions.length && (\n <div aria-disabled=\"true\" class=\"ui-list-box__empty\" role=\"option\">{emptyMessage}</div>\n )}\n {!loading && optionGroups.map((group, groupIndex) => {\n const groupLabelId = group.label ? `${controlId}-group-${groupIndex}` : undefined\n\n const renderedOptions = group.options.map(option => (\n <div\n aria-disabled={option.disabled ? 'true' : undefined}\n aria-selected={selectedValues.has(option.value) ? 'true' : 'false'}\n data-ui-list-box-option\n data-value={option.value}\n role=\"option\"\n >\n {option.label}\n </div>\n ))\n\n return group.label ?\n (\n <div aria-labelledby={groupLabelId} role=\"group\">\n <div class=\"ui-list-box__section\" id={groupLabelId}>{group.label}</div>\n {renderedOptions}\n </div>\n ) :\n renderedOptions\n })}\n </div>\n {loading && <p class=\"ui-list-box__status\" role=\"status\">{loadingMessage}</p>}\n</div>\n",
18955
19056
  "styleImport": "import '@santi020k/lumen-astro/styles.css'"
18956
19057
  },
18957
19058
  "elements": {
18958
19059
  "attributes": [
18959
19060
  "data-ui-list-box",
18960
- "options",
18961
- "value",
19061
+ "empty-message, data-selection-follows-focus",
19062
+ "aria-label, required",
18962
19063
  "multiple",
18963
19064
  "loading",
18964
- "emptyMessage",
18965
- "selectionFollowsFocus",
18966
19065
  "class, className",
18967
19066
  "...native attributes"
18968
19067
  ],
@@ -19040,7 +19139,7 @@
19040
19139
  }
19041
19140
  ],
19042
19141
  "propsExtends": "Omit<\n ComponentPropsWithRef<'select'>,\n 'children' | 'defaultValue' | 'onChange' | 'size' | 'value'\n>",
19043
- "source": "export interface ListBoxProps extends Omit<\n ComponentPropsWithRef<'select'>,\n 'children' | 'defaultValue' | 'onChange' | 'size' | 'value'\n> {\n defaultValue?: string | string[]\n emptyMessage?: ReactNode\n label?: ReactNode\n loading?: boolean\n loadingMessage?: ReactNode\n onValueChange?: (values: string[]) => void\n options?: SelectOption[]\n selectionFollowsFocus?: boolean\n value?: string | string[]\n}\n\nexport const ListBox = (inputProps: ListBoxProps) => {\n const {\n className,\n defaultValue,\n disabled,\n emptyMessage,\n id,\n label,\n loading,\n loadingMessage,\n multiple,\n name,\n onValueChange,\n options,\n ref,\n required,\n selectionFollowsFocus,\n value,\n ...props\n } = normalizeListBoxProps(inputProps)\n\n const generatedId = useId()\n const controlId = id ?? `ui-list-box-${generatedId}`\n\n const normalizedOptions = options.map(\n option => typeof option === 'string' ? { label: option, value: option } : option\n )\n\n const optionGroups = groupListBoxOptions(normalizedOptions)\n const [internalValues, setInternalValues] = useState(() => normalizeListBoxValues(defaultValue))\n const selectedValues = getListBoxSelectedValues(value, internalValues)\n const selectedSet = new Set(selectedValues)\n\n const firstEnabledIndex = Math.max(\n 0, normalizedOptions.findIndex(option => !option.disabled)\n )\n\n const [activeIndex, setActiveIndex] = useState(firstEnabledIndex)\n const selectRef = useRef<HTMLSelectElement | null>(null)\n const typeaheadRef = useRef('')\n\n const typeaheadTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(\n undefined\n )\n\n const commitValues = (nextValues: string[]) => {\n if (value === undefined) {\n setInternalValues(nextValues)\n }\n\n onValueChange?.(nextValues)\n\n const select = selectRef.current\n\n if (!select) return\n\n for (const option of select.options) {\n option.selected = nextValues.includes(option.value)\n }\n\n select.dispatchEvent(new Event('input', { bubbles: true }))\n\n select.dispatchEvent(new Event('change', { bubbles: true }))\n }\n\n const selectOption = (index: number) => {\n const option = normalizedOptions[index]\n\n if (!option || option.disabled || disabled || loading) return\n\n const optionValue = option.value\n let nextValues: string[]\n\n if (!multiple) {\n nextValues = [optionValue]\n } else if (selectedSet.has(optionValue)) {\n nextValues = selectedValues.filter(\n selectedValue => selectedValue !== optionValue\n )\n } else {\n nextValues = [...selectedValues, optionValue]\n }\n\n commitValues(nextValues)\n }\n\n const moveActive = (direction: 1 | -1) => {\n if (!normalizedOptions.length || loading) return\n\n let nextIndex = activeIndex\n\n for (const _option of normalizedOptions) {\n nextIndex =\n (nextIndex + direction + normalizedOptions.length) %\n normalizedOptions.length\n\n if (!normalizedOptions[nextIndex]?.disabled) {\n setActiveIndex(nextIndex)\n\n if (selectionFollowsFocus) selectOption(nextIndex)\n\n return\n }\n }\n }\n\n useEffect(\n () => () => {\n if (typeaheadTimerRef.current) clearTimeout(typeaheadTimerRef.current)\n }, []\n )\n\n useEffect(() => {\n const select = selectRef.current\n const form = select?.form\n\n if (!form || value !== undefined) return\n\n const handleReset = () => {\n setInternalValues(normalizeListBoxValues(defaultValue))\n\n setActiveIndex(firstEnabledIndex)\n }\n\n form.addEventListener('reset', handleReset)\n\n return () => {\n form.removeEventListener('reset', handleReset)\n }\n }, [defaultValue, firstEnabledIndex, value])\n\n const handleNavigationKey = (key: string): boolean => {\n if (key === 'ArrowDown') moveActive(1)\n else if (key === 'ArrowUp') moveActive(-1)\n else if (key === 'Home') {\n setActiveIndex(firstEnabledIndex)\n\n if (selectionFollowsFocus) selectOption(firstEnabledIndex)\n } else if (key === 'End') {\n const lastEnabledIndex = findLastEnabledOptionIndex(normalizedOptions)\n\n if (lastEnabledIndex >= 0) {\n setActiveIndex(lastEnabledIndex)\n\n if (selectionFollowsFocus) selectOption(lastEnabledIndex)\n }\n } else if (key === 'Enter' || key === ' ') selectOption(activeIndex)\n else return false\n\n return true\n }\n\n const handleTypeaheadKey = (event: KeyboardEvent<HTMLDivElement>): boolean => {\n if (\n event.key.length !== 1 ||\n event.ctrlKey ||\n event.metaKey ||\n event.altKey\n ) return false\n\n typeaheadRef.current += event.key.toLowerCase()\n\n if (typeaheadTimerRef.current) clearTimeout(typeaheadTimerRef.current)\n\n typeaheadTimerRef.current = setTimeout(() => {\n typeaheadRef.current = ''\n }, 700)\n\n const nextIndex = normalizedOptions.findIndex(\n option => !option.disabled &&\n option.label.toLowerCase().startsWith(typeaheadRef.current)\n )\n\n if (nextIndex >= 0) {\n setActiveIndex(nextIndex)\n\n if (selectionFollowsFocus) selectOption(nextIndex)\n }\n\n return true\n }\n\n const handleListKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n if (handleNavigationKey(event.key) || handleTypeaheadKey(event)) event.preventDefault()\n }\n\n return (\n <div\n className={composeClassName('ui-list-box', className)}\n data-multiple={multiple ? 'true' : undefined}\n data-ui-list-box\n >\n <ListBoxLabel controlId={controlId} label={label} />\n <select\n {...props}\n className=\"ui-list-box__native\"\n data-ui-enhanced\n data-ui-list-box-native\n disabled={disabled || loading}\n id={controlId}\n multiple={multiple}\n name={name}\n ref={node => {\n selectRef.current = node\n\n setRefValue(ref, node)\n }}\n required={required}\n value={getListBoxSelectValue(multiple, selectedValues)}\n >\n <ListBoxNativeOptions groups={optionGroups} />\n </select>\n <div\n aria-busy={optionalTrue(loading)}\n aria-disabled={optionalTrue(disabled || loading)}\n aria-multiselectable={optionalTrue(multiple)}\n className=\"ui-list-box__list\"\n data-ui-list-box-list\n onKeyDown={handleListKeyDown}\n role=\"listbox\"\n tabIndex={disabled || loading ? undefined : 0}\n >\n <ListBoxVisibleOptions\n activeIndex={activeIndex}\n controlId={controlId}\n emptyMessage={emptyMessage}\n groups={optionGroups}\n loading={loading}\n onSelect={index => {\n setActiveIndex(index)\n\n selectOption(index)\n }}\n selectedValues={selectedSet}\n />\n </div>\n {loading ?\n (\n <p className=\"ui-list-box__status\" role=\"status\">\n {loadingMessage}\n </p>\n ) :\n null}\n </div>\n )\n}",
19142
+ "source": "export interface ListBoxProps extends Omit<\n ComponentPropsWithRef<'select'>,\n 'children' | 'defaultValue' | 'onChange' | 'size' | 'value'\n> {\n defaultValue?: string | string[]\n emptyMessage?: ReactNode\n label?: ReactNode\n loading?: boolean\n loadingMessage?: ReactNode\n onValueChange?: (values: string[]) => void\n options?: SelectOption[]\n selectionFollowsFocus?: boolean\n value?: string | string[]\n}\n\nexport const ListBox = (inputProps: ListBoxProps) => {\n const {\n className,\n defaultValue,\n disabled,\n emptyMessage,\n id,\n label,\n loading,\n loadingMessage,\n multiple,\n name,\n onValueChange,\n options,\n ref,\n required,\n selectionFollowsFocus,\n value,\n ...props\n } = normalizeListBoxProps(inputProps)\n\n const generatedId = useId()\n const controlId = id ?? `ui-list-box-${generatedId}`\n\n const normalizedOptions = options.map(\n option => typeof option === 'string' ? { label: option, value: option } : option\n )\n\n const optionGroups = groupListBoxOptions(normalizedOptions)\n const [internalValues, setInternalValues] = useState(() => normalizeListBoxValues(defaultValue))\n const selectedValues = getListBoxSelectedValues(value, internalValues)\n const selectedSet = new Set(selectedValues)\n const firstEnabledIndex = normalizedOptions.findIndex(option => !option.disabled)\n const isEnabledOption = (index: number): boolean => isEnabledListBoxOption(normalizedOptions, index)\n // -1 means \"no explicit active option\"; see resolveListBoxActiveIndex.\n const [activeIndex, setActiveIndex] = useState(-1)\n const resolvedActiveIndex = resolveListBoxActiveIndex(normalizedOptions, activeIndex, selectedSet)\n const selectRef = useRef<HTMLSelectElement | null>(null)\n const listRef = useRef<HTMLDivElement | null>(null)\n const rootRef = useRef<HTMLDivElement | null>(null)\n // Labels that point at the select by id name the visible list, since the select is hidden.\n const [externalLabelledBy, setExternalLabelledBy] = useState<string | undefined>(undefined)\n\n useLayoutEffect(() => {\n const root = rootRef.current\n\n if (!root) return\n\n return observeExternalListBoxLabels(root, controlId, setExternalLabelledBy)\n }, [controlId])\n\n // Consumer focus callbacks describe the whole control, once per entry and exit.\n const focusWithinRef = useRef(false)\n // Only invalid state Lumen generated from native validation; consumer aria-invalid always wins.\n const [generatedInvalid, setGeneratedInvalid] = useState(false)\n const selectedKey = JSON.stringify(selectedValues)\n const typeaheadRef = useRef('')\n\n const typeaheadTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(\n undefined\n )\n\n const commitValues = (nextValues: string[]) => {\n if (value === undefined) {\n setInternalValues(nextValues)\n }\n\n onValueChange?.(nextValues)\n\n const select = selectRef.current\n\n if (!select) return\n\n for (const option of select.options) {\n option.selected = nextValues.includes(option.value)\n }\n\n select.dispatchEvent(new Event('input', { bubbles: true }))\n\n select.dispatchEvent(new Event('change', { bubbles: true }))\n }\n\n const selectOption = (index: number) => {\n const option = normalizedOptions[index]\n\n if (!option || option.disabled || disabled || loading) return\n\n const optionValue = option.value\n let nextValues: string[]\n\n if (!multiple) {\n nextValues = [optionValue]\n } else if (selectedSet.has(optionValue)) {\n nextValues = selectedValues.filter(\n selectedValue => selectedValue !== optionValue\n )\n } else {\n nextValues = [...selectedValues, optionValue]\n }\n\n commitValues(nextValues)\n }\n\n const moveActive = (direction: 1 | -1) => {\n if (!normalizedOptions.length || loading) return\n\n let nextIndex = Math.max(0, resolvedActiveIndex)\n\n for (const _option of normalizedOptions) {\n nextIndex =\n (nextIndex + direction + normalizedOptions.length) %\n normalizedOptions.length\n\n if (!normalizedOptions[nextIndex]?.disabled) {\n setActiveIndex(nextIndex)\n\n if (selectionFollowsFocus) selectOption(nextIndex)\n\n return\n }\n }\n }\n\n useEffect(\n () => () => {\n if (typeaheadTimerRef.current) clearTimeout(typeaheadTimerRef.current)\n }, []\n )\n\n useEffect(() => {\n const select = selectRef.current\n\n if (generatedInvalid && select && (!select.willValidate || select.validity.valid)) setGeneratedInvalid(false)\n }, [disabled, generatedInvalid, loading, required, selectedKey])\n\n const resetSourceRef = useRef({ defaultValue, value })\n\n useLayoutEffect(() => {\n resetSourceRef.current = { defaultValue, value }\n })\n\n useEffect(() => {\n const select = selectRef.current\n const form = select?.form\n\n if (!select || !form) return\n\n let resetTimer: ReturnType<typeof setTimeout> | undefined\n\n // Wait for reset listeners to run so a canceled reset keeps the current selection.\n const handleReset = (event: Event) => {\n clearTimeout(resetTimer)\n\n resetTimer = setTimeout(() => {\n if (event.defaultPrevented) return\n\n const source = resetSourceRef.current\n const defaults = normalizeListBoxValues(source.value ?? source.defaultValue)\n\n if (source.value === undefined) setInternalValues(defaults)\n\n for (const option of select.options) option.selected = defaults.includes(option.value)\n\n setGeneratedInvalid(false)\n\n setActiveIndex(-1)\n })\n }\n\n form.addEventListener('reset', handleReset)\n\n return () => {\n clearTimeout(resetTimer)\n\n form.removeEventListener('reset', handleReset)\n }\n }, [props.form])\n\n const activateIndex = (index: number) => {\n if (index < 0) return\n\n setActiveIndex(index)\n\n if (selectionFollowsFocus) selectOption(index)\n }\n\n const handleNavigationKey = (key: string): boolean => {\n if (key === 'ArrowDown') moveActive(1)\n else if (key === 'ArrowUp') moveActive(-1)\n else if (key === 'Home') activateIndex(firstEnabledIndex)\n else if (key === 'End') activateIndex(findLastEnabledOptionIndex(normalizedOptions))\n else if (key === 'Enter' || key === ' ') selectOption(resolvedActiveIndex)\n else return false\n\n return true\n }\n\n const handleTypeaheadKey = (event: KeyboardEvent<HTMLDivElement>): boolean => {\n if (\n event.key.length !== 1 ||\n event.ctrlKey ||\n event.metaKey ||\n event.altKey\n ) return false\n\n typeaheadRef.current += event.key.toLowerCase()\n\n if (typeaheadTimerRef.current) clearTimeout(typeaheadTimerRef.current)\n\n typeaheadTimerRef.current = setTimeout(() => {\n typeaheadRef.current = ''\n }, 700)\n\n const nextIndex = normalizedOptions.findIndex(\n option => !option.disabled &&\n option.label.toLowerCase().startsWith(typeaheadRef.current)\n )\n\n if (nextIndex >= 0) {\n setActiveIndex(nextIndex)\n\n if (selectionFollowsFocus) selectOption(nextIndex)\n }\n\n return true\n }\n\n const handleListKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n if (handleNavigationKey(event.key) || handleTypeaheadKey(event)) event.preventDefault()\n }\n\n return (\n <div\n className={composeClassName('ui-list-box', className)}\n data-multiple={multiple ? 'true' : undefined}\n data-ui-list-box\n data-ui-owner=\"react\"\n ref={rootRef}\n >\n <ListBoxLabel controlId={controlId} label={label} />\n <select\n {...props}\n // The visible list is the only focus stop and accessible control; the select keeps form semantics.\n aria-hidden=\"true\"\n className=\"ui-list-box__native\"\n data-ui-enhanced\n data-ui-list-box-native\n disabled={disabled || loading}\n id={controlId}\n multiple={multiple}\n name={name}\n onChange={() => {\n // The visible list owns selection; this controlled proxy only carries native form events.\n }}\n onBlur={event => {\n if (!focusWithinRef.current || isInsideListBox(rootRef.current, event.relatedTarget)) return\n\n focusWithinRef.current = false\n\n props.onBlur?.(event)\n }}\n onFocus={event => {\n if (!focusWithinRef.current) {\n focusWithinRef.current = true\n\n props.onFocus?.(event)\n }\n\n if (event.currentTarget.ownerDocument.activeElement === event.currentTarget) {\n listRef.current?.focus({ preventScroll: true })\n }\n }}\n onInvalid={event => {\n props.onInvalid?.(event)\n\n event.preventDefault()\n\n setGeneratedInvalid(true)\n\n if (isFirstInvalidControl(event.currentTarget)) listRef.current?.focus()\n }}\n ref={node => {\n selectRef.current = node\n\n setRefValue(ref, node)\n }}\n required={required}\n tabIndex={-1}\n value={getListBoxSelectValue(multiple, selectedValues)}\n >\n {/* An empty choice keeps single selection unselected until the user picks an option. */}\n {multiple ? null : <option value=\"\" />}\n <ListBoxNativeOptions groups={optionGroups} />\n </select>\n <div\n {...getListBoxListAria(props, {\n activeOptionId: resolvedActiveIndex >= 0 ? `${controlId}-option-${resolvedActiveIndex}` : undefined,\n controlId,\n externalLabelledBy,\n disabled,\n generatedInvalid,\n label,\n loading,\n multiple,\n required\n })}\n className=\"ui-list-box__list\"\n data-ui-list-box-list\n onBlur={event => {\n if (isInsideListBox(rootRef.current, event.relatedTarget)) return\n\n selectRef.current?.dispatchEvent(new FocusEvent('focusout', { bubbles: true, relatedTarget: event.relatedTarget }))\n }}\n onFocus={event => {\n if (focusWithinRef.current) return\n\n selectRef.current?.dispatchEvent(new FocusEvent('focusin', { bubbles: true, relatedTarget: event.relatedTarget }))\n }}\n onKeyDown={handleListKeyDown}\n ref={listRef}\n role=\"listbox\"\n tabIndex={disabled || loading ? undefined : 0}\n >\n <ListBoxVisibleOptions\n activeIndex={resolvedActiveIndex}\n controlId={controlId}\n emptyMessage={emptyMessage}\n groups={optionGroups}\n loading={loading}\n onSelect={index => {\n if (!isEnabledOption(index)) return\n\n setActiveIndex(index)\n\n selectOption(index)\n }}\n selectedValues={selectedSet}\n />\n </div>\n {loading ?\n (\n <p className=\"ui-list-box__status\" role=\"status\">\n {loadingMessage}\n </p>\n ) :\n null}\n </div>\n )\n}",
19044
19143
  "styleImport": "import '@santi020k/lumen-react/styles.css'"
19045
19144
  }
19046
19145
  },
@@ -20429,7 +20528,6 @@
20429
20528
  "fractionDigits / fraction-digits",
20430
20529
  "allowNegative / allow-negative",
20431
20530
  "name",
20432
- "onValueChange / ui:amount-change",
20433
20531
  "invalidMessage / invalid-message",
20434
20532
  "class, className",
20435
20533
  "...native attributes"
@@ -21108,10 +21206,23 @@
21108
21206
  "values": "string"
21109
21207
  },
21110
21208
  {
21111
- "attribute": "inputProps / inputRef",
21209
+ "frameworks": [
21210
+ "astro",
21211
+ "react"
21212
+ ],
21213
+ "attribute": "inputProps",
21214
+ "defaultValue": "-",
21215
+ "description": "Passes native number-input attributes without DOM patches.",
21216
+ "values": "input attributes"
21217
+ },
21218
+ {
21219
+ "frameworks": [
21220
+ "react"
21221
+ ],
21222
+ "attribute": "inputRef",
21112
21223
  "defaultValue": "-",
21113
- "description": "Passes native number-input attributes and exposes its ref without DOM patches.",
21114
- "values": "input attributes / React ref"
21224
+ "description": "Exposes the native number-input ref.",
21225
+ "values": "React ref"
21115
21226
  },
21116
21227
  {
21117
21228
  "attribute": "disabled / readOnly / required",
@@ -21174,10 +21285,19 @@
21174
21285
  "values": "string"
21175
21286
  },
21176
21287
  {
21177
- "attribute": "value / onValueChange",
21288
+ "attribute": "value",
21178
21289
  "defaultValue": "Uncontrolled",
21179
- "description": "Controls formatted national input and exposes validity plus E.164 in React.",
21180
- "values": "LumenPhoneNumber / callback"
21290
+ "description": "Sets the national input value; React accepts the structured phone-number contract.",
21291
+ "values": "string (Astro / Elements) | LumenPhoneNumber (React)"
21292
+ },
21293
+ {
21294
+ "frameworks": [
21295
+ "react"
21296
+ ],
21297
+ "attribute": "onValueChange",
21298
+ "defaultValue": "-",
21299
+ "description": "Receives formatting, validity, and E.164 changes in React.",
21300
+ "values": "(value: LumenPhoneNumber) => void"
21181
21301
  },
21182
21302
  {
21183
21303
  "attribute": "size",
@@ -21337,7 +21457,6 @@
21337
21457
  "elements": {
21338
21458
  "attributes": [
21339
21459
  "id / input-id",
21340
- "inputProps / inputRef",
21341
21460
  "disabled / readOnly / required",
21342
21461
  "errorMessage / showValidationError",
21343
21462
  "countries",
@@ -21348,7 +21467,7 @@
21348
21467
  "locale",
21349
21468
  "name",
21350
21469
  "placeholder",
21351
- "value / onValueChange",
21470
+ "value",
21352
21471
  "size",
21353
21472
  "class, className",
21354
21473
  "...native attributes"
@@ -21641,6 +21760,9 @@
21641
21760
  "values": "boolean (Astro / Elements attribute)"
21642
21761
  },
21643
21762
  {
21763
+ "frameworks": [
21764
+ "react"
21765
+ ],
21644
21766
  "attribute": "onDatumActivate",
21645
21767
  "defaultValue": "-",
21646
21768
  "description": "Enables React datum actions and receives validated values with raw identities. Navigation and filtering remain application owned.",
@@ -21835,7 +21957,6 @@
21835
21957
  "variant",
21836
21958
  "glass",
21837
21959
  "drilldown",
21838
- "onDatumActivate",
21839
21960
  "explore-data-label, datum-action-prefix, datumActionFormatter",
21840
21961
  "labels.exploreData, labels.formatDatumAction",
21841
21962
  "series",
@@ -22636,7 +22757,7 @@
22636
22757
  "values": "HTML attributes"
22637
22758
  }
22638
22759
  ],
22639
- "astroSource": "---\nimport type { HTMLAttributes } from 'astro/types'\n\nimport { resolveLumenAstroProps } from '@santi020k/lumen-core'\n\ninterface Props extends HTMLAttributes<'div'> {\n class?: string | null\n className?: string\n max?: number\n value?: number\n}\n\nconst { class: classProp = '', className = '', max = 100, value = 0, ...rest } = Astro.props\nconst safeMax = max > 0 ? max : 100\nconst safeValue = Math.min(safeMax, Math.max(0, value))\nconst percentage = (safeValue / safeMax) * 100\n\nconst { classList, passthrough } = resolveLumenAstroProps(rest, [\n 'ui-progress'\n], classProp, className)\n---\n\n<div\n aria-valuemax={safeMax}\n aria-valuemin=\"0\"\n aria-valuenow={safeValue}\n class:list={classList}\n data-ui-progress\n role=\"progressbar\"\n {...passthrough}\n>\n <span\n class=\"ui-progress__bar\"\n data-slot=\"progress-indicator\"\n style={`width: ${percentage}%`}\n >\n </span>\n</div>\n",
22760
+ "astroSource": "---\nimport type { HTMLAttributes } from 'astro/types'\n\nimport { normalizeLumenProgress, resolveLumenAstroProps } from '@santi020k/lumen-core'\n\ninterface Props extends HTMLAttributes<'div'> {\n class?: string | null\n className?: string\n max?: number\n value?: number\n}\n\nconst { class: classProp = '', className = '', max = 100, value = 0, ...rest } = Astro.props\nconst { max: safeMax, percentage, value: safeValue } = normalizeLumenProgress(max, value)\n\nconst { classList, passthrough } = resolveLumenAstroProps(rest, [\n 'ui-progress'\n], classProp, className)\n---\n\n<div\n aria-valuemax={safeMax}\n aria-valuemin=\"0\"\n aria-valuenow={safeValue}\n class:list={classList}\n data-ui-progress\n role=\"progressbar\"\n {...passthrough}\n>\n <span\n class=\"ui-progress__bar\"\n data-slot=\"progress-indicator\"\n style={`width: ${percentage}%`}\n >\n </span>\n</div>\n",
22640
22761
  "category": "Feedback",
22641
22762
  "collections": [],
22642
22763
  "contract": null,
@@ -22689,7 +22810,7 @@
22689
22810
  }
22690
22811
  ],
22691
22812
  "propsExtends": "HTMLAttributes<'div'>",
22692
- "source": "---\nimport type { HTMLAttributes } from 'astro/types'\n\nimport { resolveLumenAstroProps } from '@santi020k/lumen-core'\n\ninterface Props extends HTMLAttributes<'div'> {\n class?: string | null\n className?: string\n max?: number\n value?: number\n}\n\nconst { class: classProp = '', className = '', max = 100, value = 0, ...rest } = Astro.props\nconst safeMax = max > 0 ? max : 100\nconst safeValue = Math.min(safeMax, Math.max(0, value))\nconst percentage = (safeValue / safeMax) * 100\n\nconst { classList, passthrough } = resolveLumenAstroProps(rest, [\n 'ui-progress'\n], classProp, className)\n---\n\n<div\n aria-valuemax={safeMax}\n aria-valuemin=\"0\"\n aria-valuenow={safeValue}\n class:list={classList}\n data-ui-progress\n role=\"progressbar\"\n {...passthrough}\n>\n <span\n class=\"ui-progress__bar\"\n data-slot=\"progress-indicator\"\n style={`width: ${percentage}%`}\n >\n </span>\n</div>\n",
22813
+ "source": "---\nimport type { HTMLAttributes } from 'astro/types'\n\nimport { normalizeLumenProgress, resolveLumenAstroProps } from '@santi020k/lumen-core'\n\ninterface Props extends HTMLAttributes<'div'> {\n class?: string | null\n className?: string\n max?: number\n value?: number\n}\n\nconst { class: classProp = '', className = '', max = 100, value = 0, ...rest } = Astro.props\nconst { max: safeMax, percentage, value: safeValue } = normalizeLumenProgress(max, value)\n\nconst { classList, passthrough } = resolveLumenAstroProps(rest, [\n 'ui-progress'\n], classProp, className)\n---\n\n<div\n aria-valuemax={safeMax}\n aria-valuemin=\"0\"\n aria-valuenow={safeValue}\n class:list={classList}\n data-ui-progress\n role=\"progressbar\"\n {...passthrough}\n>\n <span\n class=\"ui-progress__bar\"\n data-slot=\"progress-indicator\"\n style={`width: ${percentage}%`}\n >\n </span>\n</div>\n",
22693
22814
  "styleImport": "import '@santi020k/lumen-astro/styles.css'"
22694
22815
  },
22695
22816
  "elements": {
@@ -22744,7 +22865,7 @@
22744
22865
  }
22745
22866
  ],
22746
22867
  "propsExtends": "ComponentPropsWithoutRef<'div'>",
22747
- "source": "export interface ProgressProps extends ComponentPropsWithoutRef<'div'> {\n max?: number\n value?: number\n}\n\nexport const Progress = ({\n className,\n max = 100,\n value = 0,\n ...props\n}: ProgressProps) => {\n const safeMax = max > 0 ? max : 100\n const safeValue = Math.min(safeMax, Math.max(0, value))\n const percentage = (safeValue / safeMax) * 100\n\n return (\n <div\n aria-valuemax={safeMax}\n aria-valuemin={0}\n aria-valuenow={safeValue}\n className={composeClassName('ui-progress', className)}\n role=\"progressbar\"\n {...props}\n >\n <span\n className=\"ui-progress__bar\"\n data-slot=\"progress-indicator\"\n style={{ width: `${percentage}%` }}\n />\n </div>\n )\n}",
22868
+ "source": "export interface ProgressProps extends ComponentPropsWithoutRef<'div'> {\n max?: number\n value?: number\n}\n\nexport const Progress = ({\n className,\n max = 100,\n value = 0,\n ...props\n}: ProgressProps) => {\n const { max: safeMax, percentage, value: safeValue } = normalizeLumenProgress(max, value)\n\n return (\n <div\n aria-valuemax={safeMax}\n aria-valuemin={0}\n aria-valuenow={safeValue}\n className={composeClassName('ui-progress', className)}\n role=\"progressbar\"\n {...props}\n >\n <span\n className=\"ui-progress__bar\"\n data-slot=\"progress-indicator\"\n style={{ width: `${percentage}%` }}\n />\n </div>\n )\n}",
22748
22869
  "styleImport": "import '@santi020k/lumen-react/styles.css'"
22749
22870
  }
22750
22871
  },
@@ -24881,7 +25002,7 @@
24881
25002
  }
24882
25003
  ],
24883
25004
  "propsExtends": "Omit<\n NativeSelectProps,\n | 'defaultValue' |\n 'disabled' |\n 'id' |\n 'name' |\n 'onChange' |\n 'options' |\n 'placeholder' |\n 'required' |\n 'value'\n >, SelectOptions",
24884
- "source": "export interface SelectProps\n extends\n Omit<\n NativeSelectProps,\n | 'defaultValue' |\n 'disabled' |\n 'id' |\n 'name' |\n 'onChange' |\n 'options' |\n 'placeholder' |\n 'required' |\n 'value'\n >,\n SelectOptions {\n glass?: LumenGlassProp\n inputRef?: Ref<HTMLSelectElement>\n onChange?: ComponentPropsWithoutRef<'select'>['onChange']\n visualSize?: LumenControlVisualSize\n}\n\nexport const Select = ({\n children,\n className,\n defaultValue,\n disabled = false,\n glass = false,\n id,\n inputRef,\n name,\n onChange,\n onValueChange,\n options = emptyOptions,\n placeholder,\n required = false,\n visualSize = 'default',\n value,\n ...props\n}: SelectProps) => {\n const select = useSelect({\n defaultValue,\n disabled,\n id,\n name,\n onValueChange,\n options,\n placeholder,\n required,\n value\n })\n\n const sizeClass = getSelectSizeClass(visualSize)\n\n return (\n <div\n {...select.rootProps}\n className={composeClassName(\n 'ui-select-field', glassClass('ui-select-field', glass), className\n )}\n data-ui-glass-track={glass ? true : undefined}\n >\n <select\n {...select.nativeSelectProps}\n {...props}\n className={composeClassName('ui-select ui-select__native', sizeClass)}\n onChange={composeHandlers(onChange, select.nativeSelectProps.onChange)}\n ref={inputRef}\n >\n {placeholder && (\n <option data-ui-select-placeholder disabled value=\"\">\n {placeholder}\n </option>\n )}\n {children}\n {select.options.map(option => (\n <option\n disabled={option.disabled}\n key={option.value}\n value={option.value}\n >\n {option.label}\n </option>\n ))}\n </select>\n\n <div {...select.controlProps} className=\"ui-select__control\">\n <button\n {...select.triggerProps}\n aria-label={props['aria-label']}\n aria-labelledby={props['aria-labelledby']}\n aria-describedby={props['aria-describedby']}\n aria-invalid={props['aria-invalid']}\n aria-errormessage={props['aria-errormessage']}\n className={composeClassName(\n 'ui-select ui-select__trigger', sizeClass\n )}\n type={select.triggerProps.type ?? 'button'}\n >\n <span data-ui-select-value>{select.triggerText}</span>\n </button>\n\n <div {...select.listProps} className=\"ui-select__list\">\n {select.options.map(option => {\n const optionProps = select.getOptionProps(option)\n\n return (\n <button\n key={option.value}\n {...optionProps}\n type={optionProps.type ?? 'button'}\n >\n {option.label}\n </button>\n )\n })}\n </div>\n </div>\n </div>\n )\n}",
25005
+ "source": "export interface SelectProps\n extends\n Omit<\n NativeSelectProps,\n | 'defaultValue' |\n 'disabled' |\n 'id' |\n 'name' |\n 'onChange' |\n 'options' |\n 'placeholder' |\n 'required' |\n 'value'\n >,\n SelectOptions {\n glass?: LumenGlassProp\n inputRef?: Ref<HTMLSelectElement>\n onChange?: ComponentPropsWithoutRef<'select'>['onChange']\n visualSize?: LumenControlVisualSize\n}\n\nexport const Select = ({\n children,\n className,\n defaultValue,\n disabled = false,\n glass = false,\n id,\n inputRef,\n name,\n onChange,\n onValueChange,\n options = emptyOptions,\n placeholder,\n required = false,\n visualSize = 'default',\n value,\n ...props\n}: SelectProps) => {\n const select = useSelect({\n defaultValue,\n disabled,\n id,\n name,\n onValueChange,\n options,\n placeholder,\n required,\n value\n })\n\n const sizeClass = getSelectSizeClass(visualSize)\n\n return (\n <div\n {...select.rootProps}\n className={composeClassName(\n 'ui-select-field', glassClass('ui-select-field', glass), className\n )}\n data-ui-glass-track={glass ? true : undefined}\n >\n <select\n {...select.nativeSelectProps}\n {...props}\n className={composeClassName('ui-select ui-select__native', sizeClass)}\n onChange={composeHandlers(onChange, select.nativeSelectProps.onChange)}\n ref={inputRef}\n >\n {placeholder && (\n <option data-ui-select-placeholder disabled value=\"\">\n {placeholder}\n </option>\n )}\n {children}\n {select.options.map(option => (\n <option\n disabled={option.disabled}\n key={option.value}\n value={option.value}\n >\n {option.label}\n </option>\n ))}\n </select>\n\n <div {...select.controlProps} className=\"ui-select__control\">\n <button\n {...select.triggerProps}\n aria-label={props['aria-label']}\n aria-labelledby={props['aria-labelledby']}\n aria-describedby={props['aria-describedby']}\n aria-invalid={props['aria-invalid']}\n aria-errormessage={props['aria-errormessage']}\n className={composeClassName(\n 'ui-select ui-select__trigger', sizeClass\n )}\n type={select.triggerProps.type ?? 'button'}\n >\n <span data-ui-select-value>{select.triggerText}</span>\n </button>\n\n <div\n {...select.listProps}\n aria-label={props['aria-label']}\n aria-labelledby={props['aria-labelledby']}\n className=\"ui-select__list\"\n >\n {select.options.map(option => {\n const optionProps = select.getOptionProps(option)\n\n return (\n <button\n key={option.value}\n {...optionProps}\n type={optionProps.type ?? 'button'}\n >\n {option.label}\n </button>\n )\n })}\n </div>\n </div>\n </div>\n )\n}",
24885
25006
  "styleImport": "import '@santi020k/lumen-react/styles.css'"
24886
25007
  }
24887
25008
  },
@@ -25640,6 +25761,24 @@
25640
25761
  },
25641
25762
  {
25642
25763
  "apiReference": [
25764
+ {
25765
+ "attribute": "shape",
25766
+ "defaultValue": "\"rectangle\"",
25767
+ "description": "Text lines default to 0.75rem high; circles stay square from one width or height.",
25768
+ "values": "\"rectangle\" | \"text\" | \"circle\""
25769
+ },
25770
+ {
25771
+ "attribute": "width, height",
25772
+ "defaultValue": "-",
25773
+ "description": "Numbers are pixels. Strings accept lengths, percentages, and calc() or var() expressions; heights below the 1rem default are allowed.",
25774
+ "values": "number | CSS length"
25775
+ },
25776
+ {
25777
+ "attribute": "animation",
25778
+ "defaultValue": "\"shimmer\"",
25779
+ "description": "Pulse fades instead of sweeping; none is static. Reduced motion stops either animation.",
25780
+ "values": "\"shimmer\" | \"pulse\" | \"none\""
25781
+ },
25643
25782
  {
25644
25783
  "attribute": "aria-label, aria-labelledby",
25645
25784
  "defaultValue": "-",
@@ -25671,7 +25810,7 @@
25671
25810
  "values": "HTML attributes"
25672
25811
  }
25673
25812
  ],
25674
- "astroSource": "---\nimport type { HTMLAttributes } from 'astro/types'\n\nimport { resolveLumenAstroProps } from '@santi020k/lumen-core'\n\ninterface Props extends HTMLAttributes<'div'> {\n class?: string | null\n className?: string\n}\n\nconst {\n 'aria-hidden': ariaHidden,\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledby,\n class: classProp = '',\n className = '',\n role,\n ...rest\n} = Astro.props\n\nconst isAccessible = Boolean(ariaLabel || ariaLabelledby)\n\nconst { classList, passthrough } = resolveLumenAstroProps(rest, [\n 'ui-skeleton'\n], classProp, className)\n---\n\n<div\n aria-hidden={ariaHidden ?? (isAccessible ? undefined : 'true')}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledby}\n class:list={classList}\n role={role ?? (isAccessible ? 'status' : undefined)}\n {...passthrough}\n>\n <slot />\n</div>\n",
25813
+ "astroSource": "---\nimport type { HTMLAttributes } from 'astro/types'\n\nimport {\n type LumenSkeletonAnimation,\n type LumenSkeletonShape,\n type LumenSkeletonSize,\n resolveLumenAstroProps,\n resolveLumenLoadingSemantics,\n resolveLumenSkeletonAnimation,\n resolveLumenSkeletonShape,\n resolveLumenSkeletonSize\n} from '@santi020k/lumen-core'\n\ninterface Props extends HTMLAttributes<'div'> {\n /** `shimmer` (default) keeps the existing sweep; `pulse` fades; `none` is static. */\n animation?: LumenSkeletonAnimation\n class?: string | null\n className?: string\n /** Pixels for numbers, or a CSS length such as `0.75rem` or `60%`. */\n height?: LumenSkeletonSize\n shape?: LumenSkeletonShape\n width?: LumenSkeletonSize\n}\n\nconst {\n animation,\n 'aria-hidden': ariaHidden,\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledby,\n class: classProp = '',\n className = '',\n height,\n role,\n shape,\n style,\n width,\n ...rest\n} = Astro.props\n\nconst semantics = resolveLumenLoadingSemantics({ ariaHidden, ariaLabel, ariaLabelledby, role })\nconst resolvedShape = resolveLumenSkeletonShape(shape)\nconst resolvedAnimation = resolveLumenSkeletonAnimation(animation)\nconst sizeVariables: Record<string, string> = {}\nconst resolvedHeight = resolveLumenSkeletonSize(height)\nconst resolvedWidth = resolveLumenSkeletonSize(width)\n\nif (resolvedHeight) sizeVariables['--ui-skeleton-height'] = resolvedHeight\n\nif (resolvedWidth) sizeVariables['--ui-skeleton-width'] = resolvedWidth\n\n// Consumer styles follow the size variables so explicit declarations still win.\nconst skeletonStyle = typeof style === 'object' && style !== null ?\n { ...sizeVariables, ...style } :\n [\n ...Object.entries(sizeVariables).map(([name, value]) => `${name}:${value}`),\n typeof style === 'string' ? style : ''\n ].filter(Boolean).join(';')\n\nconst { classList, passthrough } = resolveLumenAstroProps(rest, [\n 'ui-skeleton',\n resolvedShape === 'rectangle' ? '' : `ui-skeleton--${resolvedShape}`,\n resolvedAnimation === 'pulse' ? 'ui-skeleton--pulse' : '',\n resolvedAnimation === 'none' ? 'ui-skeleton--static' : ''\n], classProp, className)\n---\n\n<div\n aria-hidden={ariaHidden ?? semantics.ariaHidden}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledby}\n class:list={classList}\n role={role ?? semantics.role}\n style={skeletonStyle || undefined}\n {...passthrough}\n>\n <slot />\n</div>\n",
25675
25814
  "category": "Feedback",
25676
25815
  "collections": [],
25677
25816
  "contract": null,
@@ -25696,6 +25835,11 @@
25696
25835
  "language": "astro",
25697
25836
  "packageName": "@santi020k/lumen-astro",
25698
25837
  "props": [
25838
+ {
25839
+ "name": "animation",
25840
+ "optional": true,
25841
+ "type": "LumenSkeletonAnimation"
25842
+ },
25699
25843
  {
25700
25844
  "name": "class",
25701
25845
  "optional": true,
@@ -25705,14 +25849,34 @@
25705
25849
  "name": "className",
25706
25850
  "optional": true,
25707
25851
  "type": "string"
25852
+ },
25853
+ {
25854
+ "name": "height",
25855
+ "optional": true,
25856
+ "type": "LumenSkeletonSize"
25857
+ },
25858
+ {
25859
+ "name": "shape",
25860
+ "optional": true,
25861
+ "type": "LumenSkeletonShape"
25862
+ },
25863
+ {
25864
+ "name": "width",
25865
+ "optional": true,
25866
+ "type": "LumenSkeletonSize"
25708
25867
  }
25709
25868
  ],
25710
25869
  "propsExtends": "HTMLAttributes<'div'>",
25711
- "source": "---\nimport type { HTMLAttributes } from 'astro/types'\n\nimport { resolveLumenAstroProps } from '@santi020k/lumen-core'\n\ninterface Props extends HTMLAttributes<'div'> {\n class?: string | null\n className?: string\n}\n\nconst {\n 'aria-hidden': ariaHidden,\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledby,\n class: classProp = '',\n className = '',\n role,\n ...rest\n} = Astro.props\n\nconst isAccessible = Boolean(ariaLabel || ariaLabelledby)\n\nconst { classList, passthrough } = resolveLumenAstroProps(rest, [\n 'ui-skeleton'\n], classProp, className)\n---\n\n<div\n aria-hidden={ariaHidden ?? (isAccessible ? undefined : 'true')}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledby}\n class:list={classList}\n role={role ?? (isAccessible ? 'status' : undefined)}\n {...passthrough}\n>\n <slot />\n</div>\n",
25870
+ "source": "---\nimport type { HTMLAttributes } from 'astro/types'\n\nimport {\n type LumenSkeletonAnimation,\n type LumenSkeletonShape,\n type LumenSkeletonSize,\n resolveLumenAstroProps,\n resolveLumenLoadingSemantics,\n resolveLumenSkeletonAnimation,\n resolveLumenSkeletonShape,\n resolveLumenSkeletonSize\n} from '@santi020k/lumen-core'\n\ninterface Props extends HTMLAttributes<'div'> {\n /** `shimmer` (default) keeps the existing sweep; `pulse` fades; `none` is static. */\n animation?: LumenSkeletonAnimation\n class?: string | null\n className?: string\n /** Pixels for numbers, or a CSS length such as `0.75rem` or `60%`. */\n height?: LumenSkeletonSize\n shape?: LumenSkeletonShape\n width?: LumenSkeletonSize\n}\n\nconst {\n animation,\n 'aria-hidden': ariaHidden,\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledby,\n class: classProp = '',\n className = '',\n height,\n role,\n shape,\n style,\n width,\n ...rest\n} = Astro.props\n\nconst semantics = resolveLumenLoadingSemantics({ ariaHidden, ariaLabel, ariaLabelledby, role })\nconst resolvedShape = resolveLumenSkeletonShape(shape)\nconst resolvedAnimation = resolveLumenSkeletonAnimation(animation)\nconst sizeVariables: Record<string, string> = {}\nconst resolvedHeight = resolveLumenSkeletonSize(height)\nconst resolvedWidth = resolveLumenSkeletonSize(width)\n\nif (resolvedHeight) sizeVariables['--ui-skeleton-height'] = resolvedHeight\n\nif (resolvedWidth) sizeVariables['--ui-skeleton-width'] = resolvedWidth\n\n// Consumer styles follow the size variables so explicit declarations still win.\nconst skeletonStyle = typeof style === 'object' && style !== null ?\n { ...sizeVariables, ...style } :\n [\n ...Object.entries(sizeVariables).map(([name, value]) => `${name}:${value}`),\n typeof style === 'string' ? style : ''\n ].filter(Boolean).join(';')\n\nconst { classList, passthrough } = resolveLumenAstroProps(rest, [\n 'ui-skeleton',\n resolvedShape === 'rectangle' ? '' : `ui-skeleton--${resolvedShape}`,\n resolvedAnimation === 'pulse' ? 'ui-skeleton--pulse' : '',\n resolvedAnimation === 'none' ? 'ui-skeleton--static' : ''\n], classProp, className)\n---\n\n<div\n aria-hidden={ariaHidden ?? semantics.ariaHidden}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledby}\n class:list={classList}\n role={role ?? semantics.role}\n style={skeletonStyle || undefined}\n {...passthrough}\n>\n <slot />\n</div>\n",
25712
25871
  "styleImport": "import '@santi020k/lumen-astro/styles.css'"
25713
25872
  },
25714
25873
  "elements": {
25715
25874
  "attributes": [
25875
+ "animation",
25876
+ "height",
25877
+ "shape",
25878
+ "width",
25879
+ "width, height",
25716
25880
  "aria-label, aria-labelledby",
25717
25881
  "aria-hidden",
25718
25882
  "role",
@@ -25731,7 +25895,7 @@
25731
25895
  "props": [],
25732
25896
  "propsExtends": "HTMLElement attributes",
25733
25897
  "registration": "defineLumenElements()",
25734
- "source": "lumenSkeletonElementConfig = {\n baseClassName: 'ui-skeleton',\n tagName: 'lumen-skeleton'\n} as const satisfies LumenElementConfig",
25898
+ "source": "lumenSkeletonElementConfig = {\n attributeClasses: {\n animation: { none: 'ui-skeleton--static', pulse: 'ui-skeleton--pulse' },\n shape: { circle: 'ui-skeleton--circle', text: 'ui-skeleton--text' }\n },\n baseClassName: 'ui-skeleton',\n observedAttributes: ['animation', 'height', 'shape', 'width', ...loadingSemanticAttributes],\n tagName: 'lumen-skeleton'\n} as const satisfies LumenElementConfig",
25735
25899
  "styleImport": "import '@santi020k/lumen-elements/styles.css'",
25736
25900
  "tagName": "lumen-skeleton"
25737
25901
  },
@@ -25741,9 +25905,30 @@
25741
25905
  "importStatement": "import { Skeleton } from '@santi020k/lumen-react'",
25742
25906
  "language": "tsx",
25743
25907
  "packageName": "@santi020k/lumen-react",
25744
- "props": [],
25908
+ "props": [
25909
+ {
25910
+ "name": "animation",
25911
+ "optional": true,
25912
+ "type": "LumenSkeletonAnimation"
25913
+ },
25914
+ {
25915
+ "name": "height",
25916
+ "optional": true,
25917
+ "type": "LumenSkeletonSize"
25918
+ },
25919
+ {
25920
+ "name": "shape",
25921
+ "optional": true,
25922
+ "type": "LumenSkeletonShape"
25923
+ },
25924
+ {
25925
+ "name": "width",
25926
+ "optional": true,
25927
+ "type": "LumenSkeletonSize"
25928
+ }
25929
+ ],
25745
25930
  "propsExtends": "ComponentPropsWithRef<'div'>",
25746
- "source": "export type SkeletonProps = ComponentPropsWithRef<'div'>\n\nexport const Skeleton = ({ className, ...props }: SkeletonProps) => (\n <div className={composeClassName('ui-skeleton', className)} {...props} />\n)",
25931
+ "source": "export interface SkeletonProps extends ComponentPropsWithRef<'div'> {\n /** `shimmer` (default) keeps the existing sweep; `pulse` fades; `none` is static. */\n animation?: LumenSkeletonAnimation\n /** Pixels for numbers, or a CSS length such as `0.75rem` or `60%`. */\n height?: LumenSkeletonSize\n shape?: LumenSkeletonShape\n width?: LumenSkeletonSize\n}\n\nexport const Skeleton = ({\n animation,\n 'aria-hidden': ariaHidden,\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledby,\n className,\n height,\n role,\n shape,\n style,\n width,\n ...props\n}: SkeletonProps) => {\n const semantics = resolveLumenLoadingSemantics({ ariaHidden, ariaLabel, ariaLabelledby, role })\n const resolvedShape = resolveLumenSkeletonShape(shape)\n const resolvedAnimation = resolveLumenSkeletonAnimation(animation)\n\n const skeletonStyle: LumenCSSProperties = {\n '--ui-skeleton-height': resolveLumenSkeletonSize(height),\n '--ui-skeleton-width': resolveLumenSkeletonSize(width),\n ...style\n }\n\n return (\n <div\n aria-hidden={ariaHidden ?? (semantics.ariaHidden ? true : undefined)}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledby}\n className={composeClassName(\n 'ui-skeleton',\n resolvedShape !== 'rectangle' && `ui-skeleton--${resolvedShape}`,\n resolvedAnimation === 'pulse' && 'ui-skeleton--pulse',\n resolvedAnimation === 'none' && 'ui-skeleton--static',\n className\n )}\n role={role ?? semantics.role}\n style={skeletonStyle}\n {...props}\n />\n )\n}",
25747
25932
  "styleImport": "import '@santi020k/lumen-react/styles.css'"
25748
25933
  }
25749
25934
  },
@@ -25766,6 +25951,11 @@
25766
25951
  ],
25767
25952
  "name": "Skeleton",
25768
25953
  "props": [
25954
+ {
25955
+ "name": "animation",
25956
+ "optional": true,
25957
+ "type": "LumenSkeletonAnimation"
25958
+ },
25769
25959
  {
25770
25960
  "name": "class",
25771
25961
  "optional": true,
@@ -25775,6 +25965,21 @@
25775
25965
  "name": "className",
25776
25966
  "optional": true,
25777
25967
  "type": "string"
25968
+ },
25969
+ {
25970
+ "name": "height",
25971
+ "optional": true,
25972
+ "type": "LumenSkeletonSize"
25973
+ },
25974
+ {
25975
+ "name": "shape",
25976
+ "optional": true,
25977
+ "type": "LumenSkeletonShape"
25978
+ },
25979
+ {
25980
+ "name": "width",
25981
+ "optional": true,
25982
+ "type": "LumenSkeletonSize"
25778
25983
  }
25779
25984
  ],
25780
25985
  "propsExtends": "HTMLAttributes<'div'>",
@@ -26431,7 +26636,7 @@
26431
26636
  "values": "HTML attributes"
26432
26637
  }
26433
26638
  ],
26434
- "astroSource": "---\nimport type { HTMLAttributes } from 'astro/types'\n\nimport { resolveLumenAstroProps } from '@santi020k/lumen-core'\n\ninterface Props extends HTMLAttributes<'span'> {\n class?: string | null\n className?: string\n}\n\nconst {\n 'aria-hidden': ariaHidden,\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledby,\n class: classProp = '',\n className = '',\n role,\n ...rest\n} = Astro.props\n\nconst isAccessible = Boolean(ariaLabel || ariaLabelledby)\n\nconst { classList, passthrough } = resolveLumenAstroProps(rest, [\n 'ui-spinner'\n], classProp, className)\n---\n\n<span\n aria-hidden={ariaHidden ?? (isAccessible ? undefined : 'true')}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledby}\n class:list={classList}\n role={role ?? (isAccessible ? 'status' : undefined)}\n {...passthrough}\n>\n <slot />\n</span>\n",
26639
+ "astroSource": "---\nimport type { HTMLAttributes } from 'astro/types'\n\nimport { resolveLumenAstroProps, resolveLumenLoadingSemantics } from '@santi020k/lumen-core'\n\ninterface Props extends HTMLAttributes<'span'> {\n class?: string | null\n className?: string\n}\n\nconst {\n 'aria-hidden': ariaHidden,\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledby,\n class: classProp = '',\n className = '',\n role,\n ...rest\n} = Astro.props\n\nconst semantics = resolveLumenLoadingSemantics({ ariaHidden, ariaLabel, ariaLabelledby, role })\n\nconst { classList, passthrough } = resolveLumenAstroProps(rest, [\n 'ui-spinner'\n], classProp, className)\n---\n\n<span\n aria-hidden={ariaHidden ?? semantics.ariaHidden}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledby}\n class:list={classList}\n role={role ?? semantics.role}\n {...passthrough}\n>\n <slot />\n</span>\n",
26435
26640
  "category": "Feedback",
26436
26641
  "collections": [],
26437
26642
  "contract": null,
@@ -26468,7 +26673,7 @@
26468
26673
  }
26469
26674
  ],
26470
26675
  "propsExtends": "HTMLAttributes<'span'>",
26471
- "source": "---\nimport type { HTMLAttributes } from 'astro/types'\n\nimport { resolveLumenAstroProps } from '@santi020k/lumen-core'\n\ninterface Props extends HTMLAttributes<'span'> {\n class?: string | null\n className?: string\n}\n\nconst {\n 'aria-hidden': ariaHidden,\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledby,\n class: classProp = '',\n className = '',\n role,\n ...rest\n} = Astro.props\n\nconst isAccessible = Boolean(ariaLabel || ariaLabelledby)\n\nconst { classList, passthrough } = resolveLumenAstroProps(rest, [\n 'ui-spinner'\n], classProp, className)\n---\n\n<span\n aria-hidden={ariaHidden ?? (isAccessible ? undefined : 'true')}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledby}\n class:list={classList}\n role={role ?? (isAccessible ? 'status' : undefined)}\n {...passthrough}\n>\n <slot />\n</span>\n",
26676
+ "source": "---\nimport type { HTMLAttributes } from 'astro/types'\n\nimport { resolveLumenAstroProps, resolveLumenLoadingSemantics } from '@santi020k/lumen-core'\n\ninterface Props extends HTMLAttributes<'span'> {\n class?: string | null\n className?: string\n}\n\nconst {\n 'aria-hidden': ariaHidden,\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledby,\n class: classProp = '',\n className = '',\n role,\n ...rest\n} = Astro.props\n\nconst semantics = resolveLumenLoadingSemantics({ ariaHidden, ariaLabel, ariaLabelledby, role })\n\nconst { classList, passthrough } = resolveLumenAstroProps(rest, [\n 'ui-spinner'\n], classProp, className)\n---\n\n<span\n aria-hidden={ariaHidden ?? semantics.ariaHidden}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledby}\n class:list={classList}\n role={role ?? semantics.role}\n {...passthrough}\n>\n <slot />\n</span>\n",
26472
26677
  "styleImport": "import '@santi020k/lumen-astro/styles.css'"
26473
26678
  },
26474
26679
  "elements": {
@@ -26491,7 +26696,7 @@
26491
26696
  "props": [],
26492
26697
  "propsExtends": "HTMLElement attributes",
26493
26698
  "registration": "defineLumenElements()",
26494
- "source": "lumenSpinnerElementConfig = {\n baseClassName: 'ui-spinner',\n tagName: 'lumen-spinner'\n} as const satisfies LumenElementConfig",
26699
+ "source": "lumenSpinnerElementConfig = {\n baseClassName: 'ui-spinner',\n observedAttributes: loadingSemanticAttributes,\n tagName: 'lumen-spinner'\n} as const satisfies LumenElementConfig",
26495
26700
  "styleImport": "import '@santi020k/lumen-elements/styles.css'",
26496
26701
  "tagName": "lumen-spinner"
26497
26702
  },
@@ -26503,7 +26708,7 @@
26503
26708
  "packageName": "@santi020k/lumen-react",
26504
26709
  "props": [],
26505
26710
  "propsExtends": "ComponentPropsWithoutRef<'span'>",
26506
- "source": "export type SpinnerProps = ComponentPropsWithoutRef<'span'>\n\nexport const Spinner = ({\n 'aria-hidden': ariaHidden,\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledby,\n className,\n role,\n ...props\n}: SpinnerProps) => {\n const isAccessible = Boolean(ariaLabel || ariaLabelledby)\n\n return (\n <span\n aria-hidden={ariaHidden ?? (isAccessible ? undefined : true)}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledby}\n className={composeClassName('ui-spinner', className)}\n role={role ?? (isAccessible ? 'status' : undefined)}\n {...props}\n />\n )\n}",
26711
+ "source": "export type SpinnerProps = ComponentPropsWithoutRef<'span'>\n\nexport const Spinner = ({\n 'aria-hidden': ariaHidden,\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledby,\n className,\n role,\n ...props\n}: SpinnerProps) => {\n const semantics = resolveLumenLoadingSemantics({ ariaHidden, ariaLabel, ariaLabelledby, role })\n\n return (\n <span\n aria-hidden={ariaHidden ?? (semantics.ariaHidden ? true : undefined)}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledby}\n className={composeClassName('ui-spinner', className)}\n role={role ?? semantics.role}\n {...props}\n />\n )\n}",
26507
26712
  "styleImport": "import '@santi020k/lumen-react/styles.css'"
26508
26713
  }
26509
26714
  },
@@ -30040,18 +30245,27 @@
30040
30245
  "values": "\"mounted\" | \"data\""
30041
30246
  },
30042
30247
  {
30248
+ "frameworks": [
30249
+ "react"
30250
+ ],
30043
30251
  "attribute": "items",
30044
30252
  "defaultValue": "-",
30045
30253
  "description": "React: enable data rendering with immutable records; do not supply children.",
30046
30254
  "values": "readonly T[]"
30047
30255
  },
30048
30256
  {
30257
+ "frameworks": [
30258
+ "react"
30259
+ ],
30049
30260
  "attribute": "getKey",
30050
30261
  "defaultValue": "-",
30051
30262
  "description": "React data mode: stable unique item identity.",
30052
30263
  "values": "(item: T, index: number) => string | number"
30053
30264
  },
30054
30265
  {
30266
+ "frameworks": [
30267
+ "react"
30268
+ ],
30055
30269
  "attribute": "renderItem",
30056
30270
  "defaultValue": "-",
30057
30271
  "description": "React data mode: render only the visible window and focused neighbors.",
@@ -30156,9 +30370,6 @@
30156
30370
  "glass",
30157
30371
  "tabindex",
30158
30372
  "mode",
30159
- "items",
30160
- "getKey",
30161
- "renderItem",
30162
30373
  "itemSize",
30163
30374
  "overscan",
30164
30375
  "class, className",
@@ -38995,6 +39206,9 @@
38995
39206
  "values": "boolean (Astro / Elements attribute)"
38996
39207
  },
38997
39208
  {
39209
+ "frameworks": [
39210
+ "react"
39211
+ ],
38998
39212
  "attribute": "onDatumActivate",
38999
39213
  "defaultValue": "-",
39000
39214
  "description": "Enables React datum actions and receives validated values with raw identities. Navigation and filtering remain application owned.",
@@ -39215,7 +39429,6 @@
39215
39429
  "elements": {
39216
39430
  "attributes": [
39217
39431
  "drilldown",
39218
- "onDatumActivate",
39219
39432
  "explore-data-label, datum-action-prefix, datumActionFormatter",
39220
39433
  "labels.exploreData, labels.formatDatumAction",
39221
39434
  "formatX, formatY",
@@ -39508,6 +39721,9 @@
39508
39721
  "values": "boolean (Astro / Elements attribute)"
39509
39722
  },
39510
39723
  {
39724
+ "frameworks": [
39725
+ "react"
39726
+ ],
39511
39727
  "attribute": "onDatumActivate",
39512
39728
  "defaultValue": "-",
39513
39729
  "description": "Enables React datum actions and receives validated values with raw identities. Navigation and filtering remain application owned.",
@@ -39665,7 +39881,6 @@
39665
39881
  "colorScale, midpoint",
39666
39882
  "showLegend",
39667
39883
  "drilldown",
39668
- "onDatumActivate",
39669
39884
  "explore-data-label, datum-action-prefix, datumActionFormatter",
39670
39885
  "labels.exploreData, labels.formatDatumAction",
39671
39886
  "data",
@@ -39874,6 +40089,9 @@
39874
40089
  "values": "boolean (Astro / Elements attribute)"
39875
40090
  },
39876
40091
  {
40092
+ "frameworks": [
40093
+ "react"
40094
+ ],
39877
40095
  "attribute": "onDatumActivate",
39878
40096
  "defaultValue": "-",
39879
40097
  "description": "Enables React datum actions and receives validated values with raw identities. Navigation and filtering remain application owned.",
@@ -40035,7 +40253,6 @@
40035
40253
  "elements": {
40036
40254
  "attributes": [
40037
40255
  "drilldown",
40038
- "onDatumActivate",
40039
40256
  "explore-data-label, datum-action-prefix, datumActionFormatter",
40040
40257
  "labels.exploreData, labels.formatDatumAction",
40041
40258
  "data",
@@ -40246,6 +40463,9 @@
40246
40463
  "values": "boolean (Astro / Elements attribute)"
40247
40464
  },
40248
40465
  {
40466
+ "frameworks": [
40467
+ "react"
40468
+ ],
40249
40469
  "attribute": "onDatumActivate",
40250
40470
  "defaultValue": "-",
40251
40471
  "description": "Enables React datum actions and receives validated values with raw identities. Navigation and filtering remain application owned.",
@@ -40407,7 +40627,6 @@
40407
40627
  "elements": {
40408
40628
  "attributes": [
40409
40629
  "drilldown",
40410
- "onDatumActivate",
40411
40630
  "explore-data-label, datum-action-prefix, datumActionFormatter",
40412
40631
  "labels.exploreData, labels.formatDatumAction",
40413
40632
  "series",
@@ -44404,7 +44623,6 @@
44404
44623
  "requestId",
44405
44624
  "label, statusLabel",
44406
44625
  "approveLabel, rejectLabel",
44407
- "onResponse / ui:approval-response",
44408
44626
  "class, className",
44409
44627
  "...native attributes"
44410
44628
  ],
@@ -44699,8 +44917,6 @@
44699
44917
  "maxLength",
44700
44918
  "pending, disabled",
44701
44919
  "submitOnEnter",
44702
- "onPromptSubmit / ui:prompt-submit",
44703
- "onStop / ui:prompt-stop",
44704
44920
  "class, className",
44705
44921
  "...native attributes"
44706
44922
  ],
@@ -45421,6 +45637,9 @@
45421
45637
  "values": "boolean"
45422
45638
  },
45423
45639
  {
45640
+ "frameworks": [
45641
+ "react"
45642
+ ],
45424
45643
  "attribute": "onCountrySelect",
45425
45644
  "defaultValue": "-",
45426
45645
  "description": "React callback; Astro and Elements emit ui:world-map-select with countryId, highlighted and label.",
@@ -45600,7 +45819,6 @@
45600
45819
  "labels / listLabel",
45601
45820
  "selectedCountry",
45602
45821
  "interactive / animated",
45603
- "onCountrySelect",
45604
45822
  "heading / description / caption",
45605
45823
  "CSS variables",
45606
45824
  "class, className",
@@ -45873,11 +46091,11 @@
45873
46091
  }
45874
46092
  ],
45875
46093
  "docs": {
45876
- "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\nThis branch documents the locally prepared Lumen 4 candidate. Version 4 install examples become\nusable after publication; the public plugin and hosted MCP still describe their deployed version.\nUse the workspace packages and generated local MCP snapshot to evaluate this candidate.\n\n## Install in ChatGPT or Codex\n\nThe [Lumen UI plugin](https://chatgpt.com/plugins/plugin_asdk_app_6a8f6c526c5481918eb8a48806fa112b)\nis published in the shared Plugins Directory for ChatGPT and Codex.\n\n1. Open the listing and select **Install plugin**. Sign in to ChatGPT if prompted.\n2. Start a request with **@Lumen UI**, or choose one of the listing's starter prompts.\n3. Describe the interface and target framework, for example: “Find the right Lumen components for\n an accessible React settings screen.”\n\nThe plugin combines the Lumen workflow skill with the hosted public MCP catalog. It requires no\nseparate Lumen account, API key, or local server. Its tools only read public Lumen documentation;\nthey do not access private repositories or modify files and services. Your coding agent performs\nimplementation work using the retrieved contracts and its own permissions.\n\nSee the [publication record](./openai-plugin-submission.md) for version and verification details.\n\n## Install the Lumen Agent Skill\n\nLumen ships a portable Agent Skill for AI coding tools that support the open `SKILL.md` format:\n\n```bash\nnpx skills add santi020k/lumen --skill lumen-ui\n```\n\nThe source lives in [`skills/lumen-ui`](../skills/lumen-ui). The skill provides the durable\nframework-selection, component-composition, theming, accessibility, and verification workflow.\nUse it by naming `$lumen-ui` in a prompt, or let a compatible agent trigger it from Lumen-specific\nrequests.\n\nThe skill and MCP server complement each other: the skill explains how to work, while\n`@santi020k/lumen-mcp` supplies structured catalog search, framework-specific contracts, recipes,\nsource, tokens, resources, and rules.\n\n## Pick the Right Package\n\nUse the framework requested by the user. Every adapter shares the same Lumen foundation.\n\n| App Target | Install | Import Components From | Load Styles From |\n| -------------------------- | ---------------------------------------------------------------------- | -------------------------------------------- | -------------------------------------- |\n| Astro | `@santi020k/lumen-astro` | `@santi020k/lumen-astro` | `@santi020k/lumen-astro/styles.css` |\n| React | `@santi020k/lumen-react` | `@santi020k/lumen-react` | `@santi020k/lumen-react/styles.css` |\n| React Hook Form composites | `@santi020k/lumen-react-hook-form` | `@santi020k/lumen-react-hook-form` | Uses React styles |\n| Web Components | `@santi020k/lumen-elements` | `@santi020k/lumen-elements/define` | `@santi020k/lumen-elements/styles.css` |\n| React Native / Expo | `@santi020k/lumen-react-native` | `@santi020k/lumen-react-native` | Not applicable |\n| Apple / SwiftUI | Swift Package `https://github.com/santi020k/lumen`, pinned to `4.0.0` | `LumenUI` | Not applicable |\n| Android / Compose | Maven Central `com.santi020k:lumen-compose:4.0.0` | `com.santi020k.lumen` | Not applicable |\n| Wear OS / Compose | Maven Central `com.santi020k:lumen-compose-wear:4.0.0` | `com.santi020k.lumen` | Not applicable |\n| Package metadata | `@santi020k/lumen-core` | `@santi020k/lumen-core` | Not applicable |\n| Optional web brand icons | `@santi020k/lumen-icons-brand` | Register once, then use the framework `Icon` | Uses framework styles |\n\nEach framework package includes the shared foundation. Install `@santi020k/lumen` separately only\nwhen you need its framework-neutral CLI or registry metadata.\n\nFor React Native, mount one `LumenProvider` near the app root. For SwiftUI, attach the `LumenUI`\nSwift Package product to the application target, use an exact or compatible release-version rule,\nand apply `.lumenTheme(...)` near the root. For Compose, install\n`com.santi020k:lumen-compose:4.0.0` from Maven Central and wrap content in `LumenTheme`. Native\nadapters do not load CSS or the Astro runtime.\n\nReact Native can use ordinary React hooks and platform-neutral application hooks. Import native\nLumen controllers such as `useDialog`, `useTabs`, `useSelect`, `useLanguageToggle`,\n`useThemeToggle`, and `useToast` from `@santi020k/lumen-react-native`; their state semantics track\nthe reusable React contracts while their returned props target native Lumen components. Never\nimport DOM-dependent hooks from `@santi020k/lumen-react` into a native bundle. Share business state\nin an application workspace package and keep the Lumen rendering adapter at each app boundary.\n\nFor watchOS, use the focused `LumenWatch*` contracts from the same Swift package. For Wear OS,\ninstall `com.santi020k:lumen-compose-wear:4.0.0` and wrap wearable content in `LumenWearTheme`\ninside the application-owned Wear Material theme. Keep complications, tiles, rotary input,\nAlways On behavior, haptics, synchronization, navigation, and health or safety logic app-owned.\n\nBrand marks are intentionally excluded from the default web Lucide bundle. In Astro, React, or Web\nComponents, install and register the optional pack once before rendering a namespaced icon:\n\n```ts\nimport { registerLumenBrandIcons } from \"@santi020k/lumen-icons-brand\";\n\nregisterLumenBrandIcons();\n```\n\nThen use any canonical Font Awesome Free brand name, such as `brand:github`, `brand:discord`,\n`brand:figma`, `brand:linkedin`, `brand:whatsapp`, or `brand:youtube`, through the existing Astro,\nReact, or Web Component `Icon` API. The package also provides `brand:x` as an alias for\n`brand:x-twitter`.\n\nBrand marks keep their recognizable filled geometry instead of being redrawn as Lucide outlines.\nThey still inherit the same Lumen size, alignment, `currentColor`, and accessible-label behavior as\nthe default icon catalog. React Native, SwiftUI, and Compose already include the corresponding\ncomplete brand catalog and accept `brand:github`, `.brandGithub`, and\n`LumenIconName.BrandGithub`, respectively, without a separate package.\n\n## Consumer composition in v4\n\n- For reporting filters, use React `DateRangeInput` with controlled `value`/`onValueChange`, localized\n `labels`, bounds and explicit presets. Keep draft changes inside the picker until Apply. See the\n [reporting example](https://lumen.santi020k.com/docs/web/reporting) and [migration guide](migrating-to-lumen.md).\n- Keep date-only values as validated `YYYY-MM-DD` strings. Do not round-trip a local date through\n UTC timestamps. Use the native adapter's existing date field and application-owned calendar/time-zone rules.\n- Compose a metric card from `Card`, `Stat` and a chart with `presentation=\"bare\"`; do not invent\n MetricCard or Status components. Products own status labels, aggregation, currency units and rounding.\n- Use unique chart X identities, abbreviated `xLabel` text and full `formatCategory` labels.\n Preserve absent observations explicitly; never turn missing data into a fabricated zero.\n- Compare two media treatments with `ImageComparison` and matching source dimensions. Supply a\n visible localized control label and useful image alternatives. See [image comparison](image-comparison.md).\n- Use `Prose` for ordinary Markdown, `Code`/`CodeTabs` for examples, and `CopyButton` for dynamic\n command output. Localize clipboard success/error feedback; do not swallow rejected writes.\n- Use public `asChild` composition when a semantic link needs button styling. Disable nested form\n controls separately. On Input, native numeric `size` and Lumen `visualSize` have different meanings.\n- Map the complete semantic theme in light and dark modes, including `brand-solid`/`on-brand` and\n `danger`/`on-danger`. Text-safe colors matter more than matching a decorative swatch.\n- NavigationMenu, Sidebar and Sheet compose a documentation shell. Keep ordinary links in normal\n Tab order and application routing in the app. Do not rebuild primitives with `ui-*` classes.\n\n### Theme bootstrap and generated command output\n\nWhen the application owns theme state, use `ThemeToggle controlled` and its own click handler.\nInitialize the document in the head before paint; unavailable storage must not prevent rendering:\n\n```html\n<script>\n let theme = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'\n try {\n const saved = localStorage.getItem('application-theme')\n if (saved === 'light' || saved === 'dark') theme = saved\n } catch {\n // Private browsing or storage policy can deny access.\n }\n document.documentElement.dataset.theme = theme\n document.documentElement.classList.toggle('dark', theme === 'dark')\n</script>\n```\n\nThe owner's click handler updates the document and the toggle's `aria-pressed` state, and persists\ninside a guarded storage write. Use a server-rendered theme when the application already has one.\nA controlled Lumen toggle does not read storage or change that state itself.\n\nFor commands generated from a form, point `CopyButton` at the current output element. It reads the\nlatest text when clicked, so the application only updates the output rather than managing clipboard\npromises or feedback timers:\n\n```astro\n---\nimport { CopyButton } from '@santi020k/lumen-astro'\n---\n\n<output id=\"generated-command\">pnpm run check</output>\n<CopyButton target=\"#generated-command\" label=\"Copy command\" copiedLabel=\"Command copied\" errorLabel=\"Copy failed; select the command and copy it manually\">\n Copy command\n</CopyButton>\n```\n\n## MCP-Assisted Workflow\n\nConnect `@santi020k/lumen-mcp` when the agent supports Model Context Protocol. The server returns\nvalidated structured content as well as readable text, so agents can select components without\nscraping documentation.\n\nThe MCP per-component catalog covers Astro, React, Web Components, React Native, SwiftUI, and\nCompose. Use `lumen_list_native_components` to discover platform availability and\n`lumen_get_native_component` for installation, setup, API, examples, accessibility guidance, and\noptional reference source. The skill still verifies installed packages when local versions differ.\n\n```bash\ncodex mcp add lumen -- npx -y @santi020k/lumen-mcp\n```\n\nFor Codex project configuration:\n\n```toml\n[mcp_servers.lumen]\nenabled = true\ncommand = \"npx\"\nargs = [\"-y\", \"@santi020k/lumen-mcp\"]\n```\n\nUse this sequence:\n\n1. Read `lumen://meta` and `lumen://diagnostics` to identify and verify the snapshot.\n2. Read `lumen://rules`.\n3. Search for the use case with `lumen_search`, passing the target `framework` when known.\n4. Inspect the selected framework with `lumen_get_component` and `detail: \"usage\"`.\n5. Follow its framework behavior setup and hook/controller contract.\n6. Read related multi-component guidance with `lumen_get_recipe`.\n7. Request `detail: \"source\"` only when the usage contract is insufficient.\n8. Read `lumen://tokens` before introducing custom styling.\n9. Retain `lumen://catalog-manifest` and use `lumen_diff_catalog` after upgrades to identify contracts that need refreshing.\n\nExample framework-aware component request:\n\n```json\n{\n \"name\": \"lumen_get_component\",\n \"arguments\": {\n \"name\": \"DateRangePicker\",\n \"framework\": \"react\",\n \"detail\": \"usage\"\n }\n}\n```\n\nStable resources are available at `lumen://rules`, `lumen://tokens`, `lumen://components`,\n`lumen://meta`, `lumen://catalog-manifest`, `lumen://diagnostics`,\n`lumen://components/{name}`, and `lumen://recipes/{name}`. See\n[`packages/mcp/README.md`](../packages/mcp/README.md) for Claude Desktop, Cursor, generic stdio,\nlocal repository, and programmatic examples.\n\n## Minimal Setup\n\nImport the stylesheet once in the app's global CSS, root layout, or app entry.\n\nThis section applies to web targets. Native adapters use their provider or theme APIs instead of a\nstylesheet.\n\n```css\n@import \"@santi020k/lumen-react/styles.css\";\n```\n\nIf Tailwind is present, keep both imports in the same shared CSS entry.\n\n```css\n@import \"@santi020k/lumen-react/layers.css\";\n@import \"tailwindcss\";\n@import \"@santi020k/lumen-react/styles.css\";\n```\n\nKeep the layer prelude first. It places Tailwind base styles below Lumen components and Tailwind\nutilities above them, so consumer spacing, display, radius, width, and responsive visibility\nclasses override Lumen defaults predictably.\n\nAstro apps should mount `UIPrimitives` once in the root layout when using interactive primitives\nsuch as dialogs, menus, popovers, tabs, carousels, command lists, toggles, validated forms, or\ntoasts. Do not place `UIPrimitives` in each component example or next to each primitive instance;\none root include enhances all matching `data-ui-*` markup on the page.\n\n```astro\n---\nimport UIPrimitives from '@santi020k/lumen-astro/runtime'\n---\n\n<UIPrimitives />\n```\n\n## Astro\n\nUse direct named imports from `@santi020k/lumen-astro`.\n\n```astro\n---\nimport { Button, Card, Input, Label } from '@santi020k/lumen-astro'\n---\n\n<Card>\n <Label for=\"email\">Email</Label>\n <Input id=\"email\" type=\"email\" placeholder=\"you@example.com\" />\n <Button type=\"submit\">Subscribe</Button>\n</Card>\n```\n\n`Stat` defaults to a `div`. Choose `as=\"article\"` for a self-contained metric with supporting\ncontent, or `as=\"section\"` for a labeled region. Do not use a semantic root solely for styling.\nIts visual variants are `default`, `accent`, and `glass`; reserve `accent` for featured metrics and\nkeep glass selective.\n\nFor long reading surfaces, use `Anchor` for local section navigation and pass each item’s optional\nheading `depth`, `index`, and `description` when hierarchy matters. Use `activationOffset` for fixed\napplication headers. Use `ScrollProgress` for document position; keep `Progress` for\napplication-controlled task completion. In Astro, dispatch `ui:progress-change` with\n`{ value, max? }` from the `Progress` root when browser state changes after hydration. `Pill`\naccepts `neutral`, `brand`, and `outline`\nvariants, plus native link and count composition.\n\n## React\n\nReact components share the same visual classes and data attributes as Astro components. For\nbehavior-heavy primitives, use the React behavior hooks from `@santi020k/lumen-react` (`useDialog`,\n`usePopover`, `useDropdownMenu`, `useTabs`, `useSelect`, `useToast`, and `useTooltip`), which track\nthe Astro runtime's ARIA, keyboard, Escape, dismissal, and toast controller semantics.\n\nThe React package entry is a client boundary. In Next.js, it can be imported by a Server Component\nwithout evaluating Lumen's contexts and hooks in the React server runtime; the rendered Lumen\nsubtree becomes a Client Component boundary. Vite and other client-rendered React applications use\nthe same entry without additional setup.\nFor stateless Badge, Card composition, layout, form-label, progress, loading, typography, and\nvisually-hidden usage in a React Server Component, import from\n`@santi020k/lumen-react/server`. That entrypoint contains the implementations directly and avoids\ncreating a client boundary. Keep event handlers, hooks, and interactive primitives on the package\nroot inside a Client Component.\n`Button`, `Link`, `ButtonLink`, and `Input` accept React 19 DOM refs directly as props.\n`Button asChild` composes the button contract onto one child element. Across adapters, native\nnumeric `size` remains available on `Input` and `NativeSelect`; use `visualSize` in Astro and React,\nor `visual-size` in Elements, for presentation.\n`Stat as` and `variant` follow the same semantic-root and visual guidance as the Astro component.\n\n```tsx\nimport \"@santi020k/lumen-react/styles.css\";\nimport { Button, Card, Input, Label } from \"@santi020k/lumen-react\";\n\nexport function SubscribeForm() {\n return (\n <Card>\n <Label htmlFor=\"email\">Email</Label>\n <Input id=\"email\" type=\"email\" placeholder=\"you@example.com\" />\n <Button type=\"submit\">Subscribe</Button>\n </Card>\n );\n}\n```\n\nFor React Hook Form, spread `register()` directly onto native-backed controls. Install\n`@santi020k/lumen-react-hook-form` only for controlled composites such as `Select`, `DatePicker`,\n`InputOTP`, and `ListBox`. Do not combine React Hook Form and `useFormValidation` unless the\napplication intentionally coordinates two validation sources. For schema validation, use\n`zodResolver` or `yupResolver` from `@hookform/resolvers`; keep Zod or Yup in the application and\nrender the resulting React Hook Form errors through Lumen's `FieldError` contract.\n\n## Web Components\n\nRegister custom elements once, then use `lumen-*` tags in HTML. The elements adapter includes\ninteractive behavior parity for DataTable, Dialog, Popover, DropdownMenu, ContextMenu, Tabs,\nSelect, ThemeBuilder, Toast, Tooltip, forms, RichTextEditor, Schedule, and VirtualList: selection,\nvalidation, rich text command, context menu, schedule drag/drop, theme, and range events, ARIA\nstate, roving/listbox keyboard paths, Escape/outside dismissal, focus return/trapping, native\nselect form participation, and the same toast controller events as the Astro runtime.\n\n```html\n<script type=\"module\">\n import { defineLumenElements } from \"@santi020k/lumen-elements/define\";\n\n defineLumenElements();\n</script>\n\n<lumen-card>\n <label for=\"email\">Email</label>\n <lumen-input\n id=\"email\"\n type=\"email\"\n placeholder=\"you@example.com\"\n ></lumen-input>\n <lumen-button type=\"submit\">Subscribe</lumen-button>\n</lumen-card>\n```\n\nFor a surface that uses only Badge, Button, Card, and Combobox, import\n`@santi020k/lumen-elements/components/badge`, `/button`, `/card`, and `/combobox` and call their\ncorresponding `defineLumenBadge`, `defineLumenButton`, `defineLumenCard`, and `defineLumenCombobox`\nfunctions. Those implementation-level entrypoints avoid the complete catalog bundle; the Combobox\nentrypoint includes its filtering and keyboard behavior. Continue using `defineLumenElements` for\nany component without a granular entrypoint.\n\nFor a foundation-only custom-element surface, import `defineLumenFoundations` from\n`@santi020k/lumen-elements/components/foundations`. It registers Card compound parts, Container,\nDirection, Grid, Label, Separator, Skeleton, Spinner, Stack, Typography, and VisuallyHidden without\npulling the complete catalog implementation.\n\nPut input elements inside a native `<form data-ui-form>`. Scalar Lumen controls participate in\n`FormData`, validity, disabled state, focus, and reset through form-associated custom-element\nbehavior with a native-control fallback. Do not generate `<lumen-form>` as the submission\ncontainer.\n\n## Component Selection\n\nThe shared catalog includes:\n\n`Accordion`, `Alert`, `AlertDialog`, `Agenda`, `AspectRatio`, `Attachment`, `Autocomplete`,\n`Avatar`, `Badge`, `BarChart`, `Breadcrumb`, `Bubble`, `Button`, `ButtonGroup`, `Calendar`, `Card`,\n`Carousel`, `Chart`, `Checkbox`, `CheckboxGroup`, `Collapsible`, `Code`, `Combobox`, `Command`,\n`Container`, `ColorPicker`,\n`ContextMenu`, `DataTable`, `DatePicker`, `DateRangePicker`, `Dialog`, `Direction`, `Drawer`,\n`DropdownMenu`, `Empty`, `ErrorState`, `ErrorSummary`, `Field`, `FieldError`, `Form`, `Grid`, `HoverCard`, `Icon`,\n`Graphic`, `Backdrop`, `Illustration`, `Input`, `InputGroup`, `InputOTP`, `Item`, `KanbanBoard`, `KanbanColumn`, `Kbd`, `Label`, `LineChart`, `ListBox`, `Marker`, `Menubar`,\n`Message`, `MessageScroller`, `NativeSelect`, `ContextNavigation`, `NavigationMenu`, `NumberField`, `Pagination`,\n`PasswordField`, `PieChart`, `Popover`, `Progress`, `CopyButton`, `RadioGroup`, `Resizable`, `RichTextEditor`,\n`ScrollArea`, `Schedule`, `SearchField`, `Select`, `Separator`, `Sheet`, `Sidebar`, `Skeleton`,\n`Slider`, `Sparkline`, `Spinner`, `Stack`, `Switch`, `Table`, `Tabs`, `TagGroup`, `Textarea`, `ThemeBuilder`,\n`TimeField`, `Toast`, `Toggle`, `ToggleGroup`, `Tooltip`, `Tree`, `TreeGrid`, `Typography`, and\n`VirtualList`, and `VisuallyHidden`.\n\n## Product Recipes\n\n- Follow [the error-handling guide](./error-handling.md) when a generated interface needs failure\n feedback. Use `FieldError` and `ErrorSummary` for validation, `Alert` for persistent inline\n problems, `Toast` for brief non-blocking failures, and `ErrorState` only when a page or region\n cannot show its primary content. Keep exception capture, logging, retry policy, and safe message\n mapping in application code. For React Native, SwiftUI, and Compose, follow the guide's native API\n mapping and announcement rules; do not invent a cross-platform error manager inside the component\n layer.\n\n- Start full product surfaces from the bundled `analytics-dashboard`, `saas-admin`,\n `commerce-dashboard`, `project-workspace`, or `auth-onboarding` recipe. Install with\n `lumen add <recipe>` for Astro, or add `--target react` / `--target elements`. Keep the generated\n shared template stylesheet, then replace sample data and routes while preserving public Lumen\n components, semantic tokens, accessible names, focus paths, and responsive navigation.\n- Use `Sparkline` for a compact trend beside a labelled metric, `BarChart` for categorical\n comparison, `LineChart` for ordered or time-series data, `PieChart` for a small\n part-to-whole breakdown. Pass already-aggregated `LumenChartSeries` data; keep statistics,\n resampling, model evaluation, and domain calculations in application code. Keep `showTable`\n enabled unless the same values are already available in a nearby semantic table. Prefer a bar\n chart when precise slice comparison matters or the pie would exceed roughly seven categories.\n Use `ScatterChart` for numeric relationships, `Heatmap` for a labeled matrix, `RangeChart` for\n low-to-high intervals, and `ComboChart` only when mixed marks share a meaningful domain.\n Use `WaterfallChart` for signed balance changes, `LollipopChart` for ranking, `DumbbellChart`\n for reference/current comparison, and `BulletChart` for actual/target comparison. Use `Histogram`\n for supplied bins, `BoxPlot` for supplied distribution summaries, `CalendarHeatmap` for daily\n activity, and `FunnelChart` for ordered cohort stages. Retrieve each specialized data contract;\n these components do not all accept `LumenChartSeries`. Keep statistical and conversion policy\n in the application, preserve missing days, and use density for unequal histogram bin widths. Use\n `Chart` as the custom SVG/canvas escape hatch. Preserve `null` gaps and the default accessible\n summary and fallback data. See `docs/data-visualization.md` for the full contract.\n Pass `labels` to replace every library-owned visible and assistive phrase. Finite zero is valid\n data, while an empty chart has no finite values; do not silently reinterpret an all-zero series\n as missing. Set `showTable={false}` (`showData={false}` on native adapters) only when an\n equivalent accessible table or scoreboard is adjacent.\n Keep stable identity in `datum.x` and put display text in `datum.xLabel`. Use `formatCategory`\n and `formatValue` for localized axes, SVG titles, and data tables. Choose\n `presentation=\"bare\"` inside an existing Card, `categoryWidth` for long horizontal labels,\n `markers=\"auto\"` or a numeric marker step for dense lines, and `emptyLabel` for actionable\n source-specific guidance.\n- Use `Icon name=\"search\"` or any Lucide icon name for standard interface pictograms. Kebab-case\n names and Lucide aliases work. Pair decorative icons with `decorative`, and pass `label` only\n when the icon carries meaning on its own. On the web, install and register\n `@santi020k/lumen-icons-brand` for supported brand marks; native adapters include those names.\n Then use `brand:github` or the adapter's generated equivalent. Render project-specific or\n unsupported brand SVGs through the default `Icon` slot. Astro reports unknown named icons during\n development instead of failing silently.\n- Use `Schedule` with `Agenda`, `Calendar`, and `DatePicker` for booking, CRM, content calendar, and\n event planning screens. Mark slots with `data-ui-schedule-slot` and events with\n `data-ui-schedule-event`; the runtime emits `ui:schedule-change` when a draggable event is moved.\n `Calendar` renders a hidden input plus selectable date grid; Astro `UIPrimitives` and registered\n Web Components wire month navigation and roving keyboard focus directly, while React apps can use\n the `Calendar` component or `useCalendar` for the same date selection contract.\n Astro `UIPrimitives` and registered Web Components bind schedule drag/drop directly; React apps\n can use `useSchedule` for `rootProps`, `getEventProps`, `getSlotProps`, and `ui:schedule-change`\n parity. Use `createScheduleSlots`, `moveScheduleEvent`, and `expandRecurringScheduleEvent` from\n `@santi020k/lumen-core` for reusable schedule state; use `getScheduleConflicts` and\n `canPlaceScheduleEvent` before committing drag/drop changes. Persist local schedule edits with\n `createScheduleStorageKey`, `saveScheduleEvents`, and `loadScheduleEvents` against any\n `localStorage`-compatible adapter. Use `resizeScheduleEvent` and `resizeScheduleEvents` for\n snap-to-grid resize handles.\n- Use `KanbanBoard` and `KanbanColumn` for status-based workspaces while continuing to use `Card` for\n items. Mark items with `data-ui-kanban-item` and put `data-ui-kanban-handle` on a dedicated button;\n do not make the whole card draggable. Astro and Elements emit the cancellable, controlled\n `ui:kanban-move-request` event, and React provides `useKanban`. The event reports `itemId`,\n `fromColumn`, `toColumn`, optional `beforeId`, and keyboard or pointer input, but never moves DOM or\n persists application data. The host owns validation, `aria-busy`, optimistic state, rollback,\n status names, and APIs. Use compact `Empty` states inside columns, preserve every column with\n `Skeleton` blocks while loading, and paginate large columns. See [Kanban composition and movement](kanban.md).\n- React applications can compose [TanStack-powered table views](data-table-views.md) through\n `DataTableView`, preserving application-owned cells and server queries. Numeric/date ranges and\n page-scoped selection are opt-in. Use `DataTableSavedViews` for explicit host-owned preference\n persistence, and `useLumenReviewWorkflow` for reviewed commands with stale-revision and unknown-outcome\n guards. Install `review-workflow`, `import-review` or `record-workspace` for executable React compositions.\n- Use `DataTable`, `Tree`, `TreeGrid`, `VirtualList`, `Pagination`, and `Command` for dense data\n collection workflows. Astro and Elements wire selectable/sortable `DataTable` behavior and\n `VirtualList` range events; React also provides built-in fixed-height list windowing. `Tree`\n and `TreeGrid` use roving keyboard focus, and `VirtualList` emits `ui:virtual-list-range` as the\n rendered range changes. Use `createDataViewState`,\n `serializeDataViewState`, `parseDataViewState`, `applyDataViewState`, and `getVirtualRange` for\n saved views, filtering, sorting, pagination, and virtualization math. Use\n `createDataViewStorageKey`, `saveDataViewState`, and `loadDataViewState` when saved views should\n survive navigation or reloads. Use `pinDataViewColumn`, `unpinDataViewColumn`,\n `toggleDataViewSelection`, `createDataViewSearchParams`, and `createDataViewServerRequest` when\n connecting dense views to router state or server-backed data.\n- Use `Resizable` for split-pane editors, dashboards, inspectors, and side-by-side review layouts.\n Astro `UIPrimitives` and registered Web Components generate separator handles and pane sizing\n directly; React apps can use the `Resizable` component or `useResizable` for pointer, keyboard,\n min/max, and double-click reset parity.\n- Use `ThemeBuilder`, `ColorPicker`, `Card`, `Button`, and `Input` to build token preview and export\n tools. Add `data-ui-theme-brand-hue`, `data-ui-theme-accent-hue`, `data-ui-theme-scheme`,\n `data-ui-theme-mode`, manual color inputs such as `data-ui-theme-primary-hex`, and\n `data-ui-theme-target` for scoped live previews. Add `data-ui-theme-export-format`,\n `data-ui-theme-export`, and `data-ui-theme-output` to copy CSS, Figma variable JSON, or design\n token JSON. Astro `UIPrimitives` and registered Web Components wire those controls directly;\n React apps can use `useThemeBuilder` for the same tokens, exports, and control props. Use\n `createThemePalette`, `createThemeBuilderTokens`, `exportThemeBuilderValue`, `exportThemeCss`,\n `parseThemeCss`, and `scoreThemeContrast`\n for theme import/export and accessibility checks. Use `suggestReadableInk`, `mergeThemeTokens`,\n and `tuneThemeContrast` when generated palettes need readable foreground repair.\n- Use `RichTextEditor` with `ButtonGroup`, `ToggleGroup`, and `Textarea` for editor compositions.\n Controls with `data-ui-editor-command` request formatting, block, history, link or list commands;\n use `data-ui-editor-value` for value-bearing commands. Before the browser executes a command,\n Lumen emits the bubbling, cancelable `ui:editor-command-request` event. External engines must\n call `preventDefault()` and set `event.detail.executed` to their synchronous result. A failed\n external command never falls back to the browser. The existing `ui:editor-command` event reports\n completion; use it for notifications, not executing the command again.\n Editable surfaces support formatting shortcuts and emit `ui:editor-change` with HTML and text.\n Set `data-ui-editor-native-state=\"false\"` when an external engine owns toolbar state.\n React apps can use `useRichTextEditor({ commandHandler: ({ command, value }) => boolean })`;\n providing a handler disables native toolbar state syncing by default. Use `nativeState` to\n explicitly control that policy. Engine commands are synchronous; engines with asynchronous\n work should manage completion themselves. Keep Tiptap, ProseMirror, Lexical and other engines\n in the application.\n- Use `Autocomplete`, `SearchField`, `NumberField`, `TimeField`, `DateRangePicker`, `ColorPicker`,\n and `TagGroup` when forms need more than plain text inputs. Add `data-ui-form` to forms that\n should reflect native Constraint Validation API state into `Field` error slots on blur and submit.\n For `InputOTP`, Astro `UIPrimitives` and registered Web Components render/sync a real native input\n with visual OTP segments; React apps can use the `InputOTP` component or `useInputOTP` for the\n same sanitization, paste handling, and caret navigation.\n For `DateRangePicker`, Astro `UIPrimitives` and registered Web Components sync the paired native\n date input `min`/`max` constraints directly; React apps can use `useDateRangePicker` for the same\n start/end syncing props.\n Use native attributes such as `required`, `pattern`, `min`, `max`, and `type`; add\n `data-error-required`, `data-error-pattern`, `data-error-min`, `data-error-max`, or\n `data-error-custom` when a control needs custom copy. Listen for `ui:validate` to call\n `control.setCustomValidity(message)` before the runtime reads validity, and listen for\n `ui:invalid` or `ui:valid` on the form for submission state. Astro `UIPrimitives` and registered\n Web Components bind `data-ui-form` directly; React apps can use `useFormValidation` with `Field`\n to get the same control props, CustomEvents, and error slot updates.\n- Use `Toast` for transient feedback. Static `<Toast>` markup works without\n JavaScript. With Astro `UIPrimitives` or registered Web Components, create toasts with\n `window.LumenToast.create(detail)`, `LumenToast.create(detail)` from `@santi020k/lumen-elements`,\n or `document.dispatchEvent(new CustomEvent('ui:toast', { detail }))`; update them with\n `.update(id, detail)` or `ui:toast-update`; and dismiss them with `.dismiss(id?)` or\n `ui:toast-dismiss`. `detail` supports `title`, `description`, `variant`, `duration`, `placement`,\n `max`, and `action: { label, value }`. Destructive toasts use `role=\"alert\"`; other variants use\n `role=\"status\"`. Runtime toasts pause their timeout on hover or focus, and Escape dismisses the\n focused toast.\n\n## Runtime CustomEvents\n\nWhen `UIPrimitives` is mounted, or when Web Components are registered for matching behavior, Lumen\nuses these CustomEvents:\n\n| Event | Target | Detail | Fires When |\n| -------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------- |\n| `ui:data-table-selection-change` | `DataTable` root `[data-ui-datatable]` | `{ values: string[] }` | Selectable row checkboxes, the select-all checkbox, or form reset changes selected row values. |\n| `ui:schedule-change` | `Schedule` root `[data-ui-schedule]` | `{ eventId?: string, slot?: string }` | A draggable schedule event is dropped on a `[data-ui-schedule-slot]`. |\n| `ui:virtual-list-range` | `VirtualList` root `[data-ui-virtual-list]` | `{ startIndex: number, endIndex: number }` | The virtual list calculates its visible range on init or scroll. |\n| `ui:kanban-move-request` | `KanbanBoard` root `[data-ui-kanban]` | `{ itemId: string, fromColumn: string, toColumn: string, beforeId?: string, input: 'keyboard' \\| 'pointer' }` | A handle requests a controlled move; the event is cancellable and never moves data or DOM. |\n| `ui:tag-remove` | `TagGroup` root `.ui-tag-group` or `[data-ui-tag-group]` | `{ value?: string }` | A `[data-ui-tag-remove]` control removes its closest tag or list item. |\n| `ui:editor-command-request` | `RichTextEditor` root `[data-ui-rich-text-editor]` | `{ command: string, executed: boolean, value?: string }` | Cancel before execution and set executed for external engine ownership. |\n| `ui:editor-command` | `RichTextEditor` root `[data-ui-rich-text-editor]` | `{ command: string, executed: boolean, value?: string }` | A toolbar control or keyboard shortcut runs an editor command. |\n| `ui:editor-change` | `RichTextEditor` root `[data-ui-rich-text-editor]` | `{ html: string, text: string }` | Editable content changes or an editor command runs. |\n| `ui:theme-change` | `ThemeBuilder` root `[data-ui-theme-builder]` | `{ hue: number, accentHue: number, mode: 'generated' \\| 'manual', scheme: 'dark' \\| 'light', tokens: Record<string, string> }` | Hue, manual color, mode, or scheme controls update generated tokens. |\n| `ui:theme-export` | `ThemeBuilder` root `[data-ui-theme-builder]` | `{ css?: string, format: 'css' \\| 'figma' \\| 'tokens', tokens: Record<string, string> \\| null, value: string }` | A `[data-ui-theme-export]` control writes the selected export to the output and clipboard when available. |\n| `ui:language-change` | `LanguageToggle` control | `{ previousValue: string, value: string }` | Requests or applies the next configured locale. |\n| `ui:validate` | Form `[data-ui-form]` | `{ control, form, value }` | Before validation state is read, so custom code can call `control.setCustomValidity(message)`. |\n| `ui:invalid` | Form `[data-ui-form]` | `{ control?, controls?, form }` | Blur or submit validation finds invalid controls. Failed submits focus the first invalid control. |\n| `ui:valid` | Form `[data-ui-form]` | `{ control?, controls?, form }` | Blur or submit validation finds the checked control or form valid. |\n| `ui:toast` | `document` | `{ id?, title?, description?, variant?, duration?, placement?, action? }` | Creates a runtime toast in the matching notification viewport. |\n| `ui:toast-update` | `document` | `{ id, title?, description?, variant?, duration? }` | Updates a runtime toast. |\n| `ui:toast-dismiss` | `document` | `{ id? }` | Dismisses one runtime toast, or all runtime toasts when `id` is omitted. |\n| `ui:toast-action` | Toast `[data-ui-toast]` | `{ id, value? }` | A runtime toast action button is pressed, unless the action specifies a custom event name. |\n\n## Registry Helper\n\n`@santi020k/lumen` exports typed component, recipe, and documentation metadata from\n`@santi020k/lumen/registry`. The umbrella package and every web adapter expose the same small\n`lumen` binary, so projects that install Astro, React, or Elements can use discovery and diagnostics\nwithout another direct dependency. Individual components are addressable by name, including\nkebab-case aliases such as `data-table`:\n\n```bash\nlumen list\nlumen show Button\nlumen show data-table\nlumen add Button\nlumen add Button --target react\nlumen add Button --target elements\nlumen add data-table\nlumen show scheduler\nlumen add scheduler\nlumen add scheduler --target react\nlumen add scheduler --target elements\nlumen add scheduler --dry-run\nlumen add scheduler --fail-on-conflict\nlumen add scheduler --force\nlumen add scheduler --merge\nlumen list --registry ./registry/lumen.registry.json\nlumen add custom-recipe --registry ./registry/custom.registry.json\nlumen add private-recipe --registry https://example.com/lumen.registry.json --registry-token \"$TOKEN\"\nlumen install\nlumen doctor --json\nlumen doctor-native --json\nlumen init --framework astro --tailwind\n```\n\n`lumen doctor-native` emits the same adoption inventory and Android toolchain preflight in readable\ntext or JSON. Treat direct-primitive matches as review suggestions, not rewrite instructions: first\nconfirm whether a surface is product-owned and whether the public Lumen contract preserves its\nbehavior. A failed JDK, SDK, platform-package, or required-NDK preflight should be fixed before\nstarting Gradle so native compilation does not obscure the setup problem.\n\n`lumen add <component>` defaults to an Astro wrapper. Use `--target react` for a local React wrapper\nor `--target elements` for a custom-elements starter. Bundled recipes also accept `--target astro`,\n`--target react`, and `--target elements`; external registry inline files keep their authored paths\nand sources. Those paths must be unique, relative forward-slash paths: the installer rejects\nabsolute paths, parent traversal, and symbolic-link path segments before writing any file. Registry\nmanifests are limited to 5 MiB by default; programmatic `loadLumenRegistry` callers can lower the\nlimit with `maxBytes` and control remote request time with `timeoutMs`.\n\nBefore inventing a wrapper or custom class pattern, check the component source in\n`packages/astro/components`. Astro is the reference implementation for props, class names, data\nattributes, and accessible markup.\n\n## Styling Rules for Generated Code\n\n- Read the [content flow contract](content-flow.md). Stack and Grid own sibling gaps; surfaces own\n padding; Field owns label/control/feedback spacing. Do not add another child margin for the same\n relationship. Use `gap=\"related\"` for closely related controls, `gap=\"group\"` for separate groups\n and `gap=\"section\"` for major sections. Retrieve spacing tokens through MCP.\n- Use Card parts for header/body/actions and a nested Stack inside complex CardContent. Card\n handles missing or hidden parts and wraps footer actions. Choose `density=\"compact\"`,\n `\"comfortable\"` (default) or `\"spacious\"` instead of scattering padding overrides.\n- Keep text rhythm inside Prose or Typography. Verify long and translated text, enlarged text,\n validation states, optional sections and mobile wrapping before calling a composition finished.\n- Use the `page-header` and `section-header` recipes for dashboard identity, optional breadcrumbs,\n status/count badges and independent actions. Keep headings in document order, pass translated\n navigation/action labels and unique heading ids, and connect buttons to application-owned handlers.\n- Retrieve complete `content-flow-header`, `content-flow-settings`, `content-flow-list` and\n `content-flow-actions` examples with `lumen_get_recipe`; the CLI installs the same compositions.\n Keep one outer Container for fluid gutters. Card permits interactive overflow; use AspectRatio\n to clip media. Reading blocks trim their outer margins, so avoid compensating offsets.\n\n- Do not require consumers to configure Tailwind for Lumen components.\n- Prefer component props and composition before adding custom CSS.\n- Assign each visible outer edge to one container. For flush content inside a rounded frame,\n let the frame own the border and corner clipping; remove child rounding with supported props.\n For example, use `<Image radius=\"none\" ... />` below a framed header: `Image` defaults to\n `radius=\"lg\"`, which otherwise leaves rounded gaps at the shared edge. Keep standalone image\n rounding when appropriate.\n- Use a single divider between header and body rather than overlapping full borders. Inset\n surfaces with padding may have their own radius; account for the inset so their curves align.\n Clip artwork at its frame, not an entire interactive surface where focus rings or overlays\n could be cut off. Verify shared corners, borders, and keyboard focus at phone and desktop\n widths in both light and dark themes.\n- Use the shared token names for custom surfaces: `canvas`, `surface`, `surface-muted`,\n `surface-strong`, `line`, `ink`, `ink-soft`, `ink-muted`, `brand`, `brand-solid`, `brand-soft`,\n `accent`, `success`, `warning`, and `danger`.\n- Use `glass` as a boolean prop or attribute for glass surfaces when supported.\n- Preserve semantic labels, keyboard paths, focus states, and native form attributes.\n\n## Figma Handoff\n\nUse `exportThemeFigmaVariables` from `@santi020k/lumen-core/figma` when a design workflow needs\nFigma variable-friendly color values, and `exportThemeDesignTokens` when exporting standard design\ntoken JSON for token importers. See [docs/figma.md](figma.md) for the recommended Figma workflow.\n\n## When Editing This Repo\n\n- Shared contracts belong in `packages/core`.\n- Astro implementation and standalone CSS belong in `packages/astro`.\n- React-specific primitives belong in `packages/react`.\n- Web Component-specific primitives belong in `packages/elements`.\n- Update the relevant package README when public usage changes.\n- Add tests beside package code as `*.test.ts` when behavior, exported metadata, or component\n contracts change.\n\n### React inline range calendar\n\nFor a range editor with both endpoint calendars visible, use the React-only\n`DateRangeCalendar` export. Pass `value={{ start, end }}`, `onValueChange`, required\n`labels={{ start, end, presets }}`, `locale`, optional `min`/`max`, and optional\n`presets={[{ label, value: { start, end } }]}`. `formatDate` customizes the displayed\nendpoint summaries. Keep a draft in the consuming application and apply it only\nwhen confirmed. ISO endpoints must be valid and ascending. Crossing a boundary\nmoves the other endpoint; presets outside the bounds are disabled. The component\nshares `useCalendar` keyboard behavior and stacks its calendars on narrow screens.\n\nFor React input-attached range selection, import `DateRangeInput` from\n`@santi020k/lumen-react`. Pass controlled ISO `value`/`onValueChange`, an accessible\n`label`, and localized `labels` containing `start`, `end`, `presets`, `apply`, and\n`cancel`. It owns draft state and confirmation, anchors a non-modal popover to its\ntrigger, and supports keyboard dismissal and responsive scrolling. Use `validate`\nfor domain restrictions and `renderSummary` for draft details. Optional\n`name={{ start: 'from', end: 'to' }}` submits applied ISO values through hidden inputs.\nSee the React README for the complete usage and browser fallback contract.\n\n## Fixed-height virtual lists\n\n`VirtualList` uses the same DOM windowing controller in Astro, React and Elements. All rows remain\nmounted; offscreen rows are hidden, and inert spacers preserve the complete scroll height. This\nreduces displayed rows, not the cost of creating the initial DOM. Use pagination or an application\nrenderer when retaining the entire dataset in the DOM is too expensive.\n\nUse `itemSize` (Astro/React) or `item-size` (Elements) for a fixed row height in pixels, defaulting\nto 44. `overscan` defaults to four rows on each side. Every direct element child is a row; put\ncontrols inside it, and keep headers or empty-state actions outside the list. Rows must fit the\nconfigured height; variable-height measurement is not supported. Do not add external row margins\nor gaps. Initially hidden rows remain hidden and are excluded from the window.\n\nThe controller refreshes on scrolling, resizing, direct child changes and sizing attribute changes.\nA focused row and its neighboring rows remain available for native Tab navigation, which can extend\nthe displayed window until focus moves. Name the scroll container using `aria-label` or\n`aria-labelledby`; it is keyboard-focusable by default. `ui:virtual-list-range` reports inclusive\n`startIndex` and `endIndex` when the displayed range or item count changes. Empty lists use\n`{ startIndex: 0, endIndex: -1 }`. `data-ui-range-start` and `data-ui-range-end` mirror the current\nwindow. Elements disconnect and React unmount restore original row styles and hidden state.\n\n## NumberField boundary\n\n`NumberField` remains a native number-input wrapper. The v4 consumer audits do not establish a\nshared requirement for a locale-aware draft parser. Applications needing decimal-comma entry,\nlossless monetary drafts or currency rules should retain their explicit parsing and validation\npolicy; Lumen must not infer currency units or precision from locale.\n\n\n## Combobox keyboard behavior in v4\n\nCombobox keeps DOM focus in its input while Arrow Up/Down changes the active option through\n`aria-activedescendant`. Enter commits the active option only while the list is open; without an\nactive option, normal form submission remains available. Home, End, Left and Right retain native\ntext editing. Composition input and canceled keyboard events do not trigger selection or dismissal.\nEscape closes the innermost open control first, preserving an enclosing popup or dialog until the\nnext Escape. Tab dismisses the list when focus leaves the component.\n\nAstro and Elements observe added, removed, relabeled and disabled options, and delegate pointer\nselection to the current list. React derives options from its current props and reports committed\nselections through `onChange`, including controlled inputs. Disabled and read-only inputs stay closed.\nApplications should keep focus on the input and observe `aria-activedescendant` instead of calling\nfocus on option buttons. The DOM adapters match both option labels and values.\n\n## Appearance presets\n\nChoose `default`, `studio` or `glass` at the theme boundary. See [appearance presets](appearance-presets.md) for exact framework APIs. Studio follows the neutral PostLens look. Glass remains explicit per supporting surface; React Native and Compose retain solid fallbacks without adding blur dependencies. Customize semantic colors, typography and surface dimensions through public theme APIs.\n\n## Version-aware build, review, and migration\n\nThe portable skills are `lumen-ui` for implementation, `lumen-review` for a read-only audit, and\n`lumen-migrate` for an explicitly requested upgrade. Their canonical sources live under `skills/`;\n`pnpm run generate:plugin-package` produces the client snapshots. Both client manifests use plugin\n1.1.0 and pin the v4 catalog. This candidate is not proof of publication or directory approval.\n\nBefore applying MCP contracts, call `lumen_check_compatibility` with resolved versions from installed\nmetadata or native lock files. A manifest range is not an installed version. When versions differ,\ninspect installed public types and README or use a matching MCP package. Do not silently upgrade.\nSearch supports English and Spanish queries with framework and platform filters.\n\nFor a v4 upgrade, read `lumen_get_migration` and preview `lumen migrate v4 --dry-run --json` locally.\n`--apply` rewrites only known static SDK import paths in JavaScript and TypeScript. It preserves\ncomments and examples and reports UI/native review triggers. Dependency updates and application-owned\nworkarounds require review; the CLI does not infer that a workaround can be removed. JSX, Astro,\nnative source, dynamic imports, and dependency manifests are not automatically rewritten by the\nSDK import transform.\n\nSee [v4 AI verification](lumen-4-ai.md) for actual agent benchmark coverage and release evidence.\n\nThe integrated `lumen migrate v4` command also retains the release's web spacing migrations and\noptional coordinated dependency workflow. SDK edits compose into that command's source transform,\nso its apply ledger fingerprints the final source and repeat runs do not rewrite spacing twice.\nSDK import edits remain limited to `.ts`, `.js`, and `.mjs`; this restriction does not disable the\nseparate documented JSX/Astro spacing migration. The JSON report includes installed package\nversions and explicit SDK dependency-review findings.\n\n\n## Login examples and Auth\n\nUse the `auth-onboarding` recipe for visual email-code sign-in, code verification, and passkey\nrecovery states in Astro, React, or Elements. The preview controls are disabled and do not authenticate.\nLumen supplies the UI; the optional `@santi020k/auth-client` supplies protocol operations.\nSee the [login integration reference](../packages/templates/README.md#login-examples-and-auth)\nfor helper calls, safe result handling, and the application-owned server and session requirements.\n## Complex React forms\n\nUse the optional `@santi020k/lumen-react/forms` hooks with public form primitives for dependent\nvalidation, unsaved edits, stable repeatable rows, step navigation, and cancellation-safe checks.\nRead [composable form workflows](powerful-forms.md) before implementation. Choose one validation\nowner; keep schemas, financial policy, requests, authorization, and draft storage in the consumer.\n\nFor photo and media editors, install the `media-workspace` recipe and follow\n[Studio media workspaces](studio-media-workspaces.md). Reuse public media controls; keep\nimage processing, storage and export in the consumer application.\n",
45877
- "readme": "<p align=\"center\">\n <a href=\"https://lumen.santi020k.com\">\n <picture>\n <source media=\"(prefers-color-scheme: dark)\" srcset=\"./docs/assets/readme/hero-dark.svg\">\n <img src=\"./docs/assets/readme/hero-light.svg\" alt=\"Lumen UI — A shared language. Every interface.\" width=\"1200\" height=\"340\">\n </picture>\n </a>\n</p>\n\n<h1 align=\"center\">Lumen UI</h1>\n\n<p align=\"center\">\n <strong>Accessible primitives. Native authoring. Shared foundations.</strong><br>\n An open-source UI system for Astro, React, Web Components, React Native, SwiftUI, and Jetpack Compose.\n</p>\n\n<p align=\"center\">\n <a href=\"https://lumen.santi020k.com\">Documentation</a> ·\n <a href=\"https://lumen.santi020k.com/docs/components\">Components</a> ·\n <a href=\"https://lumen.santi020k.com/#playgrounds\">Playgrounds</a> ·\n <a href=\"https://www.figma.com/community/file/1662337342676541513\">Figma library</a> ·\n <a href=\"https://lumen.santi020k.com/support\">Support</a>\n</p>\n\n<p align=\"center\">\n <a href=\"https://github.com/santi020k/lumen/actions/workflows/ci.yml\"><img alt=\"CI status on main\" src=\"https://img.shields.io/github/actions/workflow/status/santi020k/lumen/ci.yml?branch=main&amp;style=flat-square&amp;label=CI\"></a>\n <a href=\"https://github.com/santi020k/lumen/actions/workflows/codeql.yml\"><img alt=\"CodeQL status\" src=\"https://github.com/santi020k/lumen/actions/workflows/codeql.yml/badge.svg\"></a>\n <a href=\"https://www.npmjs.com/package/@santi020k/lumen-astro\"><img alt=\"Published Astro package version\" src=\"https://img.shields.io/npm/v/@santi020k/lumen-astro?style=flat-square&amp;label=npm\"></a>\n <a href=\"./LICENSE\"><img alt=\"MIT license\" src=\"https://img.shields.io/badge/license-MIT-036b8c?style=flat-square\"></a>\n</p>\n\nLumen gives your product a consistent visual language while preserving each framework's native\nway of building. Compose accessible controls, forms, charts, and complete product layouts from\nshared tokens and explicit component contracts.\n\n**Explore:** [Quick start](#web-quick-start) · [Native apps](#native-playgrounds) · [Templates](#dashboards-and-templates) · [AI and Figma](#ai-and-design-workflows) · [Packages](#packages) · [Repository apps](#repository-apps) · [Contributing](#contributing)\n\n> **Lumen 4 candidate:** This branch documents the upcoming major version. The npm badge reports\n> the published version; local examples and store builds may differ. See the\n> [migration and readiness notes](#lumen-4-candidate) before upgrading.\n\n## See Lumen in action\n\n<p>\n <a href=\"https://lumen.santi020k.com/docs/web/playground\">\n <img src=\"./apps/docs/src/assets/platforms/web.webp\" alt=\"A Lumen web workspace with navigation, metric cards, an interactive chart, and a task form\" width=\"1184\" height=\"802\">\n </a>\n</p>\n\n*Real Lumen components with illustrative data. [Open the web playground →](https://lumen.santi020k.com/docs/web/playground)*\n\n| Built for | What you get |\n| --- | --- |\n| **Web products** | Astro, React, and custom elements with standalone CSS and optional Tailwind integration. |\n| **Native interfaces** | React Native, SwiftUI, Compose, WidgetKit, and Wear OS foundations with platform conventions. |\n| **Accessible interaction** | Semantic structure, keyboard paths, focus management, and reduced-motion support. |\n| **Design and AI workflows** | Figma resources, a portable agent skill, MCP discovery, and machine-readable contracts. |\n\n## Web quick start\n\nChoose one adapter. Astro requires Astro 5+, React requires React 19+, and the web packages\nrequire Node.js 22.12+ for their tooling. Install in your existing application:\n\n```bash\n# Astro\npnpm add @santi020k/lumen-astro\n\n# React\npnpm add @santi020k/lumen-react\n\n# Web Components\npnpm add @santi020k/lumen-elements\n```\n\n### Astro\n\nImport the stylesheet and mount `UIPrimitives` once in your root layout. The runtime enhances all\ninteractive Lumen markup on the page.\n\n```astro\n---\nimport '@santi020k/lumen-astro/styles.css'\nimport UIPrimitives from '@santi020k/lumen-astro/runtime'\n---\n\n<html lang=\"en\">\n <body>\n <slot />\n <UIPrimitives />\n </body>\n</html>\n```\n\n```astro\n---\nimport { Button, Card, Input, Label } from '@santi020k/lumen-astro'\n---\n\n<Card>\n <Label for=\"email\">Email</Label>\n <Input id=\"email\" name=\"email\" type=\"email\" placeholder=\"you@example.com\" />\n <Button>Subscribe</Button>\n</Card>\n```\n\n### React\n\nLoad the stylesheet once from your app entry or global CSS:\n\n```tsx\nimport '@santi020k/lumen-react/styles.css'\nimport { Button, Card, Input, Label } from '@santi020k/lumen-react'\n\nexport function SubscribeForm() {\n return (\n <Card>\n <Label htmlFor=\"email\">Email</Label>\n <Input id=\"email\" name=\"email\" type=\"email\" placeholder=\"you@example.com\" />\n <Button>Subscribe</Button>\n </Card>\n )\n}\n```\n\n### Web Components\n\nImport the styles and register the elements once:\n\n```html\n<style>\n @import \"@santi020k/lumen-elements/styles.css\";\n</style>\n\n<script type=\"module\">\n import { defineLumenElements } from '@santi020k/lumen-elements/define'\n\n defineLumenElements()\n</script>\n\n<lumen-card>\n <lumen-label id=\"email-label\">Email</lumen-label>\n <lumen-input aria-labelledby=\"email-label\" id=\"email\" name=\"email\" type=\"email\" placeholder=\"you@example.com\"></lumen-input>\n <lumen-button>Subscribe</lumen-button>\n</lumen-card>\n```\n\nSee the [Web documentation](https://lumen.santi020k.com/docs/web) for installation, theming,\ncomponent examples, and API details.\n\nBrowse the [package directory](https://lumen.santi020k.com/docs/packages) for framework adapters,\nshared foundations, and optional integrations, with links to focused examples and usage guides.\n\nFor failure states, use the [error-handling guide](./docs/error-handling.md) to choose between field\nfeedback, summaries, persistent alerts, transient toasts, and the `ErrorState` recovery surface.\nLumen owns presentation and accessibility; applications retain logging, retry, and exception policy.\n\nFor native applications, choose the [React Native](https://lumen.santi020k.com/docs/react-native),\n[Apple / SwiftUI](https://lumen.santi020k.com/docs/apple), or\n[Android / Compose](https://lumen.santi020k.com/docs/android) guide. The\n[shared foundations](https://lumen.santi020k.com/docs/foundations) explain the cross-platform token\nand component contract; repository contributors can also use the\n[cross-platform architecture](./docs/cross-platform.md) and\n[native component reference](./docs/native-components.md).\n\n> **Native qualification:** Check the [compatibility matrix](docs/native-compatibility.md) and\n> [device evidence](docs/native-device-validation.md) for your target. Local builds, published\n> artifacts, and physical-device accessibility are distinct verification steps.\n\n## Tailwind CSS\n\nKeep the layer prelude, Tailwind import, and Lumen stylesheet in the same shared CSS entry. Replace\n`lumen-astro` with the package for your framework.\n\n```css\n@import \"@santi020k/lumen-astro/layers.css\";\n@import \"tailwindcss\";\n@import \"@santi020k/lumen-astro/styles.css\";\n```\n\nThis order places Tailwind base styles before Lumen components and Tailwind utilities above Lumen\ncomponent defaults.\n\n## Native playgrounds\n\n<table>\n <tr>\n <th>SwiftUI</th>\n <th>Jetpack Compose</th>\n <th>React Native</th>\n </tr>\n <tr>\n <td><a href=\"https://lumen.santi020k.com/docs/apple/playground\"><img src=\"./apps/docs/src/assets/platforms/apple.webp\" alt=\"Native SwiftUI release checklist example\" width=\"220\"></a></td>\n <td><a href=\"https://lumen.santi020k.com/docs/android/playground\"><img src=\"./apps/docs/src/assets/platforms/android.webp\" alt=\"Native Compose release checklist example\" width=\"220\"></a></td>\n <td><a href=\"https://lumen.santi020k.com/docs/react-native/playground\"><img src=\"./apps/docs/src/assets/platforms/react-native.webp\" alt=\"Expo browser preview of the contributor profile example\" width=\"220\"></a></td>\n </tr>\n <tr>\n <td>Native app capture</td>\n <td>Native app capture</td>\n <td>Expo browser preview</td>\n </tr>\n</table>\n\nSee [capture provenance](apps/docs/src/assets/platforms/README.md) for sources and verification scope.\n\nTry Lumen before adding it to your project. The [playground section](https://lumen.santi020k.com/#playgrounds)\nbrings together the browser demos and native apps. Explore component states and themes, then use\nthe open-source apps as implementation references. Start with the\n[web playground](https://lumen.santi020k.com/docs/web/playground) for Astro, React, and Web Components,\nor choose a native gallery:\n\n- [`apps/playground-react-native`](./apps/playground-react-native) runs through Expo on the web,\n iOS, and Android. [Try the browser preview](https://lumen.santi020k.com/docs/react-native/playground#preview)\n or use the local setup guide for native evaluation. EAS profiles cover TestFlight, Android App\n Bundles, and APKs; this gallery has no separate public store listing.\n- [`apps/playground-apple`](./apps/playground-apple) is available on the\n [App Store](https://apps.apple.com/app/id6805250815) for iPhone, iPad, and Mac. It also builds as\n an iOS Xcode app or macOS Swift Package executable for local exploration.\n- [`apps/playground-android`](./apps/playground-android) builds a native Compose application and a\n directly installable debug APK. It is also available on\n [Google Play](https://play.google.com/store/apps/details?id=com.santi020k.lumen.playground.compose)\n for Android phones and tablets.\n\nSee the [playground workflow](./docs/playgrounds.md) for a platform chooser, reference-app source,\nand run, capture, and distribution commands. Store builds follow their own release schedule and\nmay differ from the current repository candidate.\n\n## Appearance presets\n\nChoose an appearance preset or customize the existing themes with the [appearance guide](docs/appearance-presets.md). The Studio preset uses PostLens as its visual reference.\n\n## Dashboards and templates\n\nExplore the [template gallery](https://lumen.santi020k.com/templates) for complete analytics,\nSaaS admin, commerce, project workspace, and authentication/onboarding experiences. Each family\nuses public Lumen primitives and semantic tokens, includes responsive and accessibility coverage,\nand ships through the CLI for all three framework targets:\n\n```bash\nlumen add analytics-dashboard\nlumen add commerce-dashboard --target react\nlumen add auth-onboarding --target elements\n```\n\n## AI and design workflows\n\nInstall the portable Lumen skill in Codex, Claude Code, Cursor, Windsurf, and other compatible\ncoding agents:\n\n```bash\nnpx skills add santi020k/lumen --skill lumen-ui\n```\n\nThe skill teaches agents how to select, compose, theme, and verify Lumen primitives. Pair it with\n[`@santi020k/lumen-mcp`](./packages/mcp) when an agent needs to search the live catalog or retrieve\ncurrent source, props, tokens, and usage rules.\n\nFor ChatGPT and Codex, install the published\n[Lumen UI plugin](https://chatgpt.com/plugins/plugin_asdk_app_6a8f6c526c5481918eb8a48806fa112b)\nfrom the Plugins Directory. Select **Install plugin**, then mention **@Lumen UI** in a request such\nas “Find the right Lumen components for an accessible React settings screen.” The plugin bundles\nthe skill with the hosted, read-only catalog; no separate Lumen account or local MCP setup is needed.\n\nLumen is designed to reduce repetitive UI code and unnecessary agent context. Actual token usage\ndepends on the model, task, and corrections; see the [measurement method](docs/ai-efficiency.md).\n\n<details>\n<summary>Guides, registries, and integration references</summary>\n\nAdditional machine-readable surfaces include:\n\n- [`llms.txt`](./llms.txt) for a concise project map.\n- [`docs/ai-usage.md`](./docs/ai-usage.md) for downstream generation examples.\n- [`docs/styling-contract.md`](./docs/styling-contract.md) for stable parts and component variables.\n- [`docs/import-and-icon-performance.md`](./docs/import-and-icon-performance.md) for repeatable import and icon evidence.\n- [`docs/graphics.md`](./docs/graphics.md) for portable decorative artwork across web and native platforms.\n- [`docs/consumer-regression-fixtures.md`](./docs/consumer-regression-fixtures.md) for production-shaped local coverage.\n- [`docs/project-adoption.md`](./docs/project-adoption.md) for the sibling-project audit and staged\n web and native migration strategy.\n- [`registry/lumen.registry.json`](./registry/lumen.registry.json) for recipes and installable file\n groups.\n- [`docs/figma.md`](./docs/figma.md) for Figma variables and Code Connect workflows.\n- [`apps/figma-plugin/README.md`](./apps/figma-plugin/README.md) for the **beta** Figma plugin,\n Astro starters, and AI handoff.\n- [`plugins/lumen-ui`](./plugins/lumen-ui) for the packaged Codex and ChatGPT plugin, with its\n [submission and review record](./docs/openai-plugin-submission.md).\n- [`.claude-plugin/marketplace.json`](./.claude-plugin/marketplace.json) for the Claude Code\n marketplace, with its [installation and submission guide](./docs/claude-code-plugin.md).\n- [`docs/cross-platform.md`](./docs/cross-platform.md) for the native architecture, support tiers,\n and component-parity policy.\n- [`docs/native-patterns.md`](./docs/native-patterns.md) for developed native compositions and the\n boundary between Lumen presentation and platform-owned application structure.\n\n</details>\n\n## Packages\n\n| Package | Purpose |\n| --- | --- |\n| [`@santi020k/lumen-astro`](./packages/astro) | Reference implementation, Astro components, and progressive-enhancement runtime |\n| [`@santi020k/lumen-react`](./packages/react) | React components and behavior hooks |\n| [`@santi020k/lumen-react-hook-form`](./packages/react-hook-form) | Optional React Hook Form adapters for composite controls |\n| [`@santi020k/lumen-elements`](./packages/elements) | Standards-based Web Components |\n| [`@santi020k/lumen-core`](./packages/core) | Shared tokens, metadata, class helpers, and behavior contracts |\n| [`@santi020k/lumen-tokens`](./packages/tokens) | Canonical platform-neutral design token source |\n| [`@santi020k/lumen-react-native`](./packages/react-native) | React Native foundations and native primitives |\n| [`LumenUI`](./packages/swift) | Swift Package for iOS, iPadOS, macOS, tvOS, visionOS, and watchOS foundations and native primitives |\n| [`LumenWidgetUI`](./packages/swift-widget) | Focused WidgetKit-safe semantic text, icon, badge, and compact-stat foundations |\n| [`lumen-compose`](./packages/compose) | Android library for Jetpack Compose foundations and native primitives |\n| [`lumen-compose-wear`](./packages/compose/wear) | Focused Wear OS primitives for at-a-glance round-screen actions, progress, status, metrics, and rows |\n| [`@santi020k/lumen-icons-brand`](./packages/icons-brand) | Optional namespaced brand icons for web `Icon` adapters |\n| [`@santi020k/lumen`](./packages/lumen) | Umbrella package, CLI, registry metadata, and public package map |\n| [`@santi020k/lumen-mcp`](./packages/mcp) | MCP server for component discovery and source retrieval |\n\nThe private [`@santi020k/lumen-templates`](packages/templates/README.md) workspace supplies the\ngallery and preview renderer. Consumer recipes are installed through the public Lumen CLI.\n\n## Repository apps\n\nEach app has its own setup, source map, and verification guidance:\n\n| App | Purpose |\n| --- | --- |\n| [Documentation](apps/docs/README.md) | Public documentation, live examples, search, and native previews |\n| [Template showcase](apps/templates/README.md) | Standalone gallery of five product-template families |\n| [Next.js smoke app](apps/next-smoke/README.md) | Server/client boundaries and rendered consumer regression fixtures |\n| [Apple playground](apps/playground-apple/README.md) | iPhone, iPad, Mac, watchOS, and WidgetKit reference hosts |\n| [Android playground](apps/playground-android/README.md) | Compose phone/tablet app and separate Wear OS host |\n| [React Native playground](apps/playground-react-native/README.md) | Expo reference app and embedded browser gallery |\n| [Figma plugin](apps/figma-plugin/README.md) | Development beta for Astro starters and structured agent handoff |\n| [Motion studio](apps/motion/README.md) | Reusable product films and local video exports |\n| [Store artwork](apps/store-assets/README.md) | Canonical SVG sources for the playground app icons |\n\n## Lumen 4 candidate\n\nThe v4 release branch brings consumer-driven date selection, chart readability, clipboard feedback,\nloading states, native layout improvements and a new ImageComparison primitive. Start with the\n[v3 → v4 migration guide](docs/migrating-v3-to-v4.md), [consumer audit](docs/lumen-4-consumer-audit.md),\nand [local readiness record](docs/lumen-4-readiness.md). The refreshed community gallery includes\npublished consumer screenshots; it does not imply those applications already run this candidate.\n\nFor existing applications, start with [v1 → v2](docs/migrating-v1-to-v2.md) when needed, then\n[v2 → v3](docs/migrating-v2-to-v3.md) before\n[v3 → v4](docs/migrating-v3-to-v4.md). For initial adoption, use\n[Migrating to Lumen](docs/migrating-to-lumen.md).\n\n## Contributing\n\nLumen is a pnpm workspace. Astro is the reference implementation; shared contracts belong in\n`packages/core`, while framework-specific behavior stays in its adapter package.\n\nUse Node.js 22.22.2+, 24.15.0+, or 26+ and pnpm 12.8.1, as declared in the root manifest.\n\n```bash\npnpm install --frozen-lockfile\npnpm run dev\npnpm run validate\n```\n\nRead [`CONTRIBUTING.md`](./CONTRIBUTING.md) before opening a pull request. User-visible package\nchanges require a changeset.\n\n## Community\n\nCreate reusable product animations and social videos locally with the\n[Lumen motion studio](./apps/motion/README.md). Its first pilot previews one interface in four\nappearances and exports portrait, square, and landscape MP4s through HyperFrames.\n\n[Feedback & support](https://lumen.santi020k.com/support) connects ideas, questions, bug reports,\nand the [public roadmap](https://lumen.santi020k.com/support#roadmap). Discuss improvements on GitHub\nand follow accepted work through implementation and release.\n\nLumen is free and MIT licensed while the project focuses on adoption, public examples, and\nreal-world feedback.\n\n- Explore task-oriented [guides](https://lumen.santi020k.com/guides).\n- Share a public project in the [community showcase](https://lumen.santi020k.com/community).\n- Evaluate the system with the open [team adoption path](https://lumen.santi020k.com/teams).\n- Tell us what would improve your next build through the\n [adoption survey](https://github.com/santi020k/lumen/issues/new?template=adoption-feedback.yml).\n\nMaintainers can use the [exposure playbook](./docs/exposure-playbook.md) for publishing cadence,\ncampaign measurement, video scripts, and launch copy, and the\n[marketing package](./docs/marketing/README.md) for execution-ready copy, channel decisions, a\nfour-week calendar, asset briefs, an approval queue, and a community-response library.\n\n## Find your next step\n\n| Resource | Use it for |\n| --- | --- |\n| [Cross-platform architecture](docs/cross-platform.md) | Shared foundations and adapter responsibilities. |\n| [Native compatibility](docs/native-compatibility.md) | Supported toolchains and platform baselines. |\n| [Native release runbook](docs/native-release-runbook.md) | Artifact qualification and distribution gates. |\n\n## License\n\nLumen's original code and artwork are available under the [MIT License](./LICENSE). Generated\ninterface icons retain the Lucide ISC or Feather MIT terms, and generated brand icons retain Font\nAwesome Free's CC BY 4.0 terms. The native packages include the required attribution and license\nnotices; see [`icons/THIRD_PARTY_NOTICES.md`](./icons/THIRD_PARTY_NOTICES.md).\n\nCreated and maintained by [Santiago Molina](https://santi020k.com).\n"
46094
+ "aiUsage": "# AI Usage Guide\n\nUse this guide when an AI agent is building an app with Lumen or updating examples that teach\nanother agent how to use the library.\n\nLumen 4 is released. See the [v4.2 highlights](lumen-4.2.md) and use the\n[release manifest](../registry/release-manifest.json) for each package's version; companion packages\ndo not all share the umbrella version. The public plugin and hosted MCP describe their deployed\nsnapshot, which can differ from this checkout. Inspect snapshot metadata before using its contracts.\n\n## Install in ChatGPT or Codex\n\nThe [Lumen UI plugin](https://chatgpt.com/plugins/plugin_asdk_app_6a8f6c526c5481918eb8a48806fa112b)\nis published in the shared Plugins Directory for ChatGPT and Codex.\n\n1. Open the listing and select **Install plugin**. Sign in to ChatGPT if prompted.\n2. Start a request with **@Lumen UI**, or choose one of the listing's starter prompts.\n3. Describe the interface and target framework, for example: “Find the right Lumen components for\n an accessible React settings screen.”\n\nThe plugin combines the Lumen workflow skill with the hosted public MCP catalog. It requires no\nseparate Lumen account, API key, or local server. Its tools only read public Lumen documentation;\nthey do not access private repositories or modify files and services. Your coding agent performs\nimplementation work using the retrieved contracts and its own permissions.\n\nSee the [publication record](./openai-plugin-submission.md) for version and verification details.\n\n## Install the Lumen Agent Skill\n\nLumen ships a portable Agent Skill for AI coding tools that support the open `SKILL.md` format:\n\n```bash\nnpx skills add santi020k/lumen --skill lumen-ui\n```\n\nThe source lives in [`skills/lumen-ui`](../skills/lumen-ui). The skill provides the durable\nframework-selection, component-composition, theming, accessibility, and verification workflow.\nUse it by naming `$lumen-ui` in a prompt, or let a compatible agent trigger it from Lumen-specific\nrequests.\n\nThe skill and MCP server complement each other: the skill explains how to work, while\n`@santi020k/lumen-mcp` supplies structured catalog search, framework-specific contracts, recipes,\nsource, tokens, resources, and rules.\n\n## Web selection controls\n\nUse `Select` as the default for web product option lists, and `Combobox` when the user needs to\nsearch the choices. `NativeSelect` remains supported for a product requirement that explicitly\ncalls for the browser or platform picker. Do not remove native fallbacks from custom controls.\n\nMount the matching framework behavior before verifying the result: Astro uses `UIPrimitives`,\nReact uses the public Select behavior hook, and Elements uses registered custom elements. Check\nthat opening the control shows the enhanced list, that keyboard selection and Escape work, and\nthat dependent choices, forms, mobile layouts, and both appearances remain correct.\n\nFor personal projects that require custom lists, enforce the choice in ESLint as well as agent\nguidance. The prepared `lumenCustomSelects()` helper in `@santi020k/eslint-config-libraries` rejects\n`NativeSelect` imports (including aliases), namespace JSX usage, raw JSX selects, and JSX native\nselect custom elements without replacing the project's existing syntax restrictions. Enable it\nonly after that package version is available. Keep intentional native-control exceptions scoped\nto specific files with a documented product reason. Library source and fallback implementations\nshould not opt into a consumer-only policy.\n\n## Pick the Right Package\n\nUse the framework requested by the user. Every adapter shares the same Lumen foundation.\n\n| App Target | Install | Import Components From | Load Styles From |\n| -------------------------- | ---------------------------------------------------------------------- | -------------------------------------------- | -------------------------------------- |\n| Astro | `@santi020k/lumen-astro` | `@santi020k/lumen-astro` | `@santi020k/lumen-astro/styles.css` |\n| React | `@santi020k/lumen-react` | `@santi020k/lumen-react` | `@santi020k/lumen-react/styles.css` |\n| React Hook Form composites | `@santi020k/lumen-react-hook-form` | `@santi020k/lumen-react-hook-form` | Uses React styles |\n| Web Components | `@santi020k/lumen-elements` | `@santi020k/lumen-elements/define` | `@santi020k/lumen-elements/styles.css` |\n| React Native / Expo | `@santi020k/lumen-react-native` | `@santi020k/lumen-react-native` | Not applicable |\n| Apple / SwiftUI | Swift Package `https://github.com/santi020k/lumen`, pinned to `4.2.0` | `LumenUI` | Not applicable |\n| Android / Compose | Maven Central `com.santi020k:lumen-compose:4.0.0` | `com.santi020k.lumen` | Not applicable |\n| Wear OS / Compose | Maven Central `com.santi020k:lumen-compose-wear:4.0.0` | `com.santi020k.lumen` | Not applicable |\n| Package metadata | `@santi020k/lumen-core` | `@santi020k/lumen-core` | Not applicable |\n| Optional web brand icons | `@santi020k/lumen-icons-brand` | Register once, then use the framework `Icon` | Uses framework styles |\n\nEach framework package includes the shared foundation. Install `@santi020k/lumen` separately only\nwhen you need its framework-neutral CLI or registry metadata.\n\nFor React Native, mount one `LumenProvider` near the app root. For SwiftUI, attach the `LumenUI`\nSwift Package product to the application target, use an exact or compatible release-version rule,\nand apply `.lumenTheme(...)` near the root. For Compose, install\n`com.santi020k:lumen-compose:4.0.0` from Maven Central and wrap content in `LumenTheme`. Native\nadapters do not load CSS or the Astro runtime.\n\nReact Native can use ordinary React hooks and platform-neutral application hooks. Import native\nLumen controllers such as `useDialog`, `useTabs`, `useSelect`, `useLanguageToggle`,\n`useThemeToggle`, and `useToast` from `@santi020k/lumen-react-native`; their state semantics track\nthe reusable React contracts while their returned props target native Lumen components. Never\nimport DOM-dependent hooks from `@santi020k/lumen-react` into a native bundle. Share business state\nin an application workspace package and keep the Lumen rendering adapter at each app boundary.\n\nFor watchOS, use the focused `LumenWatch*` contracts from the same Swift package. For Wear OS,\ninstall `com.santi020k:lumen-compose-wear:4.0.0` and wrap wearable content in `LumenWearTheme`\ninside the application-owned Wear Material theme. Keep complications, tiles, rotary input,\nAlways On behavior, haptics, synchronization, navigation, and health or safety logic app-owned.\n\nBrand marks are intentionally excluded from the default web Lucide bundle. In Astro, React, or Web\nComponents, install and register the optional pack once before rendering a namespaced icon:\n\n```ts\nimport { registerLumenBrandIcons } from \"@santi020k/lumen-icons-brand\";\n\nregisterLumenBrandIcons();\n```\n\nThen use any canonical Font Awesome Free brand name, such as `brand:github`, `brand:discord`,\n`brand:figma`, `brand:linkedin`, `brand:whatsapp`, or `brand:youtube`, through the existing Astro,\nReact, or Web Component `Icon` API. The package also provides `brand:x` as an alias for\n`brand:x-twitter`.\n\nBrand marks keep their recognizable filled geometry instead of being redrawn as Lucide outlines.\nThey still inherit the same Lumen size, alignment, `currentColor`, and accessible-label behavior as\nthe default icon catalog. React Native, SwiftUI, and Compose already include the corresponding\ncomplete brand catalog and accept `brand:github`, `.brandGithub`, and\n`LumenIconName.BrandGithub`, respectively, without a separate package.\n\n## Consumer composition in v4\n\n- For reporting filters, use React `DateRangeInput` with controlled `value`/`onValueChange`, localized\n `labels`, bounds and explicit presets. Keep draft changes inside the picker until Apply. See the\n [reporting example](https://lumen.santi020k.com/docs/web/reporting) and [migration guide](migrating-to-lumen.md).\n- Keep date-only values as validated `YYYY-MM-DD` strings. Do not round-trip a local date through\n UTC timestamps. Use the native adapter's existing date field and application-owned calendar/time-zone rules.\n- Compose a metric card from `Card`, `Stat` and a chart with `presentation=\"bare\"`; do not invent\n MetricCard or Status components. Products own status labels, aggregation, currency units and rounding.\n- Use unique chart X identities, abbreviated `xLabel` text and full `formatCategory` labels.\n Preserve absent observations explicitly; never turn missing data into a fabricated zero.\n- Compare two media treatments with `ImageComparison` and matching source dimensions. Supply a\n visible localized control label and useful image alternatives. See [image comparison](image-comparison.md).\n- Use `Prose` for ordinary Markdown, `Code`/`CodeTabs` for examples, and `CopyButton` for dynamic\n command output. Localize clipboard success/error feedback; do not swallow rejected writes.\n- Use public `asChild` composition when a semantic link needs button styling. Disable nested form\n controls separately. On Input, native numeric `size` and Lumen `visualSize` have different meanings.\n- Map the complete semantic theme in light and dark modes, including `brand-solid`/`on-brand` and\n `danger`/`on-danger`. Text-safe colors matter more than matching a decorative swatch.\n- NavigationMenu, Sidebar and Sheet compose a documentation shell. Keep ordinary links in normal\n Tab order and application routing in the app. Do not rebuild primitives with `ui-*` classes.\n\n### Theme bootstrap and generated command output\n\nWhen the application owns theme state, use `ThemeToggle controlled` and its own click handler.\nInitialize the document in the head before paint; unavailable storage must not prevent rendering:\n\n```html\n<script>\n let theme = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'\n try {\n const saved = localStorage.getItem('application-theme')\n if (saved === 'light' || saved === 'dark') theme = saved\n } catch {\n // Private browsing or storage policy can deny access.\n }\n document.documentElement.dataset.theme = theme\n document.documentElement.classList.toggle('dark', theme === 'dark')\n</script>\n```\n\nThe owner's click handler updates the document and the toggle's `aria-pressed` state, and persists\ninside a guarded storage write. Use a server-rendered theme when the application already has one.\nA controlled Lumen toggle does not read storage or change that state itself.\n\nFor commands generated from a form, point `CopyButton` at the current output element. It reads the\nlatest text when clicked, so the application only updates the output rather than managing clipboard\npromises or feedback timers:\n\n```astro\n---\nimport { CopyButton } from '@santi020k/lumen-astro'\n---\n\n<output id=\"generated-command\">pnpm run check</output>\n<CopyButton target=\"#generated-command\" label=\"Copy command\" copiedLabel=\"Command copied\" errorLabel=\"Copy failed; select the command and copy it manually\">\n Copy command\n</CopyButton>\n```\n\n## MCP-Assisted Workflow\n\nConnect `@santi020k/lumen-mcp` when the agent supports Model Context Protocol. The server returns\nvalidated structured content as well as readable text, so agents can select components without\nscraping documentation.\n\nThe MCP per-component catalog covers Astro, React, Web Components, React Native, SwiftUI, and\nCompose. Use `lumen_list_native_components` to discover platform availability and\n`lumen_get_native_component` for installation, setup, API, examples, accessibility guidance, and\noptional reference source. The skill still verifies installed packages when local versions differ.\n\n```bash\ncodex mcp add lumen -- npx -y @santi020k/lumen-mcp\n```\n\nFor Codex project configuration:\n\n```toml\n[mcp_servers.lumen]\nenabled = true\ncommand = \"npx\"\nargs = [\"-y\", \"@santi020k/lumen-mcp\"]\n```\n\nUse this sequence:\n\n1. Read `lumen://meta` and `lumen://diagnostics` to identify and verify the snapshot.\n2. Read `lumen://rules`.\n3. Search for the use case with `lumen_search`, passing the target `framework` when known.\n4. Inspect the selected framework with `lumen_get_component` and `detail: \"usage\"`.\n5. Follow its framework behavior setup and hook/controller contract.\n6. Read related multi-component guidance with `lumen_get_recipe`.\n7. Request `detail: \"source\"` only when the usage contract is insufficient.\n8. Read `lumen://tokens` before introducing custom styling.\n9. Retain `lumen://catalog-manifest` and use `lumen_diff_catalog` after upgrades to identify contracts that need refreshing.\n\nExample framework-aware component request:\n\n```json\n{\n \"name\": \"lumen_get_component\",\n \"arguments\": {\n \"name\": \"DateRangePicker\",\n \"framework\": \"react\",\n \"detail\": \"usage\"\n }\n}\n```\n\nStable resources are available at `lumen://rules`, `lumen://tokens`, `lumen://components`,\n`lumen://meta`, `lumen://catalog-manifest`, `lumen://diagnostics`,\n`lumen://components/{name}`, and `lumen://recipes/{name}`. See\n[`packages/mcp/README.md`](../packages/mcp/README.md) for Claude Desktop, Cursor, generic stdio,\nlocal repository, and programmatic examples.\n\n## Minimal Setup\n\nImport the stylesheet once in the app's global CSS, root layout, or app entry.\n\nThis section applies to web targets. Native adapters use their provider or theme APIs instead of a\nstylesheet.\n\n```css\n@import \"@santi020k/lumen-react/styles.css\";\n```\n\nIf Tailwind is present, keep both imports in the same shared CSS entry.\n\n```css\n@import \"@santi020k/lumen-react/layers.css\";\n@import \"tailwindcss\";\n@import \"@santi020k/lumen-react/styles.css\";\n```\n\nKeep the layer prelude first. It places Tailwind base styles below Lumen components and Tailwind\nutilities above them, so consumer spacing, display, radius, width, and responsive visibility\nclasses override Lumen defaults predictably.\n\nAstro apps should mount `UIPrimitives` once in the root layout when using interactive primitives\nsuch as dialogs, menus, popovers, tabs, carousels, command lists, toggles, validated forms, or\ntoasts. Do not place `UIPrimitives` in each component example or next to each primitive instance;\none root include enhances all matching `data-ui-*` markup on the page.\n\n```astro\n---\nimport UIPrimitives from '@santi020k/lumen-astro/runtime'\n---\n\n<UIPrimitives />\n```\n\n## Astro\n\nUse direct named imports from `@santi020k/lumen-astro`.\n\n```astro\n---\nimport { Button, Card, Input, Label } from '@santi020k/lumen-astro'\n---\n\n<Card>\n <Label for=\"email\">Email</Label>\n <Input id=\"email\" type=\"email\" placeholder=\"you@example.com\" />\n <Button type=\"submit\">Subscribe</Button>\n</Card>\n```\n\n`Stat` defaults to a `div`. Choose `as=\"article\"` for a self-contained metric with supporting\ncontent, or `as=\"section\"` for a labeled region. Do not use a semantic root solely for styling.\nIts visual variants are `default`, `accent`, and `glass`; reserve `accent` for featured metrics and\nkeep glass selective.\n\nFor long reading surfaces, use `Anchor` for local section navigation and pass each item’s optional\nheading `depth`, `index`, and `description` when hierarchy matters. Use `activationOffset` for fixed\napplication headers. Use `ScrollProgress` for document position; keep `Progress` for\napplication-controlled task completion. In Astro, dispatch `ui:progress-change` with\n`{ value, max? }` from the `Progress` root when browser state changes after hydration. Non-finite or\nmissing fields keep the current state, and every adapter clamps to finite ARIA values. Use `Spinner`\nfor unknown-duration waits.\n\nSkeleton and Spinner are decorative (`aria-hidden`) unless named. For a group of placeholders,\nannounce loading once: wrap them in one `role=\"status\"` region with `aria-busy=\"true\"` and a\n`VisuallyHidden` label, and keep the Skeletons unnamed. Size placeholders with `shape`\n(`rectangle`, `text`, `circle`), `width`, and `height`, and use deterministic widths rather than\nrandom values so server and client markup match. Data fetching and when to show placeholders stay\nin the application. `Pill`\naccepts `neutral`, `brand`, and `outline`\nvariants, plus native link and count composition.\n\n## React\n\nReact components share the same visual classes and data attributes as Astro components. For\nbehavior-heavy primitives, use the React behavior hooks from `@santi020k/lumen-react` (`useDialog`,\n`usePopover`, `useDropdownMenu`, `useTabs`, `useSelect`, `useToast`, and `useTooltip`), which track\nthe Astro runtime's ARIA, keyboard, Escape, dismissal, and toast controller semantics.\n\nThe React package entry is a client boundary. In Next.js, it can be imported by a Server Component\nwithout evaluating Lumen's contexts and hooks in the React server runtime; the rendered Lumen\nsubtree becomes a Client Component boundary. Vite and other client-rendered React applications use\nthe same entry without additional setup.\nFor stateless Badge, Card composition, layout, form-label, progress, loading, typography, and\nvisually-hidden usage in a React Server Component, import from\n`@santi020k/lumen-react/server`. That entrypoint contains the implementations directly and avoids\ncreating a client boundary. Keep event handlers, hooks, and interactive primitives on the package\nroot inside a Client Component.\n`Button`, `Link`, `ButtonLink`, and `Input` accept React 19 DOM refs directly as props.\n`Button asChild` composes the button contract onto one child element. Across adapters, native\nnumeric `size` remains available on `Input` and `NativeSelect`; use `visualSize` in Astro and React,\nor `visual-size` in Elements, for presentation.\n`Stat as` and `variant` follow the same semantic-root and visual guidance as the Astro component.\n\n```tsx\nimport \"@santi020k/lumen-react/styles.css\";\nimport { Button, Card, Input, Label } from \"@santi020k/lumen-react\";\n\nexport function SubscribeForm() {\n return (\n <Card>\n <Label htmlFor=\"email\">Email</Label>\n <Input id=\"email\" type=\"email\" placeholder=\"you@example.com\" />\n <Button type=\"submit\">Subscribe</Button>\n </Card>\n );\n}\n```\n\nFor React Hook Form, spread `register()` directly onto native-backed controls. Install\n`@santi020k/lumen-react-hook-form` only for controlled composites such as `Select`, `DatePicker`,\n`InputOTP`, and `ListBox`. Do not combine React Hook Form and `useFormValidation` unless the\napplication intentionally coordinates two validation sources. For schema validation, use\n`zodResolver` or `yupResolver` from `@hookform/resolvers`; keep Zod or Yup in the application and\nrender the resulting React Hook Form errors through Lumen's `FieldError` contract.\n\n## Web Components\n\nRegister custom elements once, then use `lumen-*` tags in HTML. The elements adapter includes\ninteractive behavior parity for DataTable, Dialog, Popover, DropdownMenu, ContextMenu, Tabs,\nSelect, ThemeBuilder, Toast, Tooltip, forms, RichTextEditor, Schedule, and VirtualList: selection,\nvalidation, rich text command, context menu, schedule drag/drop, theme, and range events, ARIA\nstate, roving/listbox keyboard paths, Escape/outside dismissal, focus return/trapping, native\nselect form participation, and the same toast controller events as the Astro runtime.\n\n```html\n<script type=\"module\">\n import { defineLumenElements } from \"@santi020k/lumen-elements/define\";\n\n defineLumenElements();\n</script>\n\n<lumen-card>\n <label for=\"email\">Email</label>\n <lumen-input\n id=\"email\"\n type=\"email\"\n placeholder=\"you@example.com\"\n ></lumen-input>\n <lumen-button type=\"submit\">Subscribe</lumen-button>\n</lumen-card>\n```\n\nFor a surface that uses only Badge, Button, Card, and Combobox, import\n`@santi020k/lumen-elements/components/badge`, `/button`, `/card`, and `/combobox` and call their\ncorresponding `defineLumenBadge`, `defineLumenButton`, `defineLumenCard`, and `defineLumenCombobox`\nfunctions. Those implementation-level entrypoints avoid the complete catalog bundle; the Combobox\nentrypoint includes its filtering and keyboard behavior. Continue using `defineLumenElements` for\nany component without a granular entrypoint.\n\nFor a foundation-only custom-element surface, import `defineLumenFoundations` from\n`@santi020k/lumen-elements/components/foundations`. It registers Card compound parts, Container,\nDirection, Grid, Label, Separator, Skeleton, Spinner, Stack, Typography, and VisuallyHidden without\npulling the complete catalog implementation.\n\nPut input elements inside a native `<form data-ui-form>`. Scalar Lumen controls participate in\n`FormData`, validity, disabled state, focus, and reset through form-associated custom-element\nbehavior with a native-control fallback. Do not generate `<lumen-form>` as the submission\ncontainer.\n\n## Component Selection\n\nThe shared catalog includes:\n\n`Accordion`, `Alert`, `AlertDialog`, `Agenda`, `AspectRatio`, `Attachment`, `Autocomplete`,\n`Avatar`, `Badge`, `BarChart`, `Breadcrumb`, `Bubble`, `Button`, `ButtonGroup`, `Calendar`, `Card`,\n`Carousel`, `Chart`, `Checkbox`, `CheckboxGroup`, `Collapsible`, `Code`, `Combobox`, `Command`,\n`Container`, `ColorPicker`,\n`ContextMenu`, `DataTable`, `DatePicker`, `DateRangePicker`, `Dialog`, `Direction`, `Drawer`,\n`DropdownMenu`, `Empty`, `ErrorState`, `ErrorSummary`, `Field`, `FieldError`, `Form`, `Grid`, `HoverCard`, `Icon`,\n`Graphic`, `Backdrop`, `Illustration`, `Input`, `InputGroup`, `InputOTP`, `Item`, `KanbanBoard`, `KanbanColumn`, `Kbd`, `Label`, `LineChart`, `ListBox`, `Marker`, `Menubar`,\n`Message`, `MessageScroller`, `NativeSelect`, `ContextNavigation`, `NavigationMenu`, `NumberField`, `Pagination`,\n`PasswordField`, `PieChart`, `Popover`, `Progress`, `CopyButton`, `RadioGroup`, `Resizable`, `RichTextEditor`,\n`ScrollArea`, `Schedule`, `SearchField`, `Select`, `Separator`, `Sheet`, `Sidebar`, `Skeleton`,\n`Slider`, `Sparkline`, `Spinner`, `Stack`, `Switch`, `Table`, `Tabs`, `TagGroup`, `Textarea`, `ThemeBuilder`,\n`TimeField`, `Toast`, `Toggle`, `ToggleGroup`, `Tooltip`, `Tree`, `TreeGrid`, `Typography`, and\n`VirtualList`, and `VisuallyHidden`.\n\n## Product Recipes\n\n- Follow [the error-handling guide](./error-handling.md) when a generated interface needs failure\n feedback. Use `FieldError` and `ErrorSummary` for validation, `Alert` for persistent inline\n problems, `Toast` for brief non-blocking failures, and `ErrorState` only when a page or region\n cannot show its primary content. Keep exception capture, logging, retry policy, and safe message\n mapping in application code. For React Native, SwiftUI, and Compose, follow the guide's native API\n mapping and announcement rules; do not invent a cross-platform error manager inside the component\n layer.\n\n- Start full product surfaces from the bundled `analytics-dashboard`, `saas-admin`,\n `commerce-dashboard`, `project-workspace`, or `auth-onboarding` recipe. Install with\n `lumen add <recipe>` for Astro, or add `--target react` / `--target elements`. Keep the generated\n shared template stylesheet, then replace sample data and routes while preserving public Lumen\n components, semantic tokens, accessible names, focus paths, and responsive navigation.\n- Use `Sparkline` for a compact trend beside a labelled metric, `BarChart` for categorical\n comparison, `LineChart` for ordered or time-series data, `PieChart` for a small\n part-to-whole breakdown. Pass already-aggregated `LumenChartSeries` data; keep statistics,\n resampling, model evaluation, and domain calculations in application code. Keep `showTable`\n enabled unless the same values are already available in a nearby semantic table. Prefer a bar\n chart when precise slice comparison matters or the pie would exceed roughly seven categories.\n Use `ScatterChart` for numeric relationships, `Heatmap` for a labeled matrix, `RangeChart` for\n low-to-high intervals, and `ComboChart` only when mixed marks share a meaningful domain.\n Use `WaterfallChart` for signed balance changes, `LollipopChart` for ranking, `DumbbellChart`\n for reference/current comparison, and `BulletChart` for actual/target comparison. Use `Histogram`\n for supplied bins, `BoxPlot` for supplied distribution summaries, `CalendarHeatmap` for daily\n activity, and `FunnelChart` for ordered cohort stages. Retrieve each specialized data contract;\n these components do not all accept `LumenChartSeries`. Keep statistical and conversion policy\n in the application, preserve missing days, and use density for unequal histogram bin widths. Use\n `Chart` as the custom SVG/canvas escape hatch. Preserve `null` gaps and the default accessible\n summary and fallback data. See `docs/data-visualization.md` for the full contract.\n Pass `labels` to replace every library-owned visible and assistive phrase. Finite zero is valid\n data, while an empty chart has no finite values; do not silently reinterpret an all-zero series\n as missing. Set `showTable={false}` (`showData={false}` on native adapters) only when an\n equivalent accessible table or scoreboard is adjacent.\n Keep stable identity in `datum.x` and put display text in `datum.xLabel`. Use `formatCategory`\n and `formatValue` for localized axes, SVG titles, and data tables. Choose\n `presentation=\"bare\"` inside an existing Card, `categoryWidth` for long horizontal labels,\n `markers=\"auto\"` or a numeric marker step for dense lines, and `emptyLabel` for actionable\n source-specific guidance.\n- Use `Icon name=\"search\"` or any Lucide icon name for standard interface pictograms. Kebab-case\n names and Lucide aliases work. Pair decorative icons with `decorative`, and pass `label` only\n when the icon carries meaning on its own. On the web, install and register\n `@santi020k/lumen-icons-brand` for supported brand marks; native adapters include those names.\n Then use `brand:github` or the adapter's generated equivalent. Render project-specific or\n unsupported brand SVGs through the default `Icon` slot. Astro reports unknown named icons during\n development instead of failing silently.\n- Use `Schedule` with `Agenda`, `Calendar`, and `DatePicker` for booking, CRM, content calendar, and\n event planning screens. Mark slots with `data-ui-schedule-slot` and events with\n `data-ui-schedule-event`; the runtime emits `ui:schedule-change` when a draggable event is moved.\n `Calendar` renders a hidden input plus selectable date grid; Astro `UIPrimitives` and registered\n Web Components wire month navigation and roving keyboard focus directly, while React apps can use\n the `Calendar` component or `useCalendar` for the same date selection contract.\n Astro `UIPrimitives` and registered Web Components bind schedule drag/drop directly; React apps\n can use `useSchedule` for `rootProps`, `getEventProps`, `getSlotProps`, and `ui:schedule-change`\n parity. Use `createScheduleSlots`, `moveScheduleEvent`, and `expandRecurringScheduleEvent` from\n `@santi020k/lumen-core` for reusable schedule state; use `getScheduleConflicts` and\n `canPlaceScheduleEvent` before committing drag/drop changes. Persist local schedule edits with\n `createScheduleStorageKey`, `saveScheduleEvents`, and `loadScheduleEvents` against any\n `localStorage`-compatible adapter. Use `resizeScheduleEvent` and `resizeScheduleEvents` for\n snap-to-grid resize handles.\n- Use `KanbanBoard` and `KanbanColumn` for status-based workspaces while continuing to use `Card` for\n items. Mark items with `data-ui-kanban-item` and put `data-ui-kanban-handle` on a dedicated button;\n do not make the whole card draggable. Astro and Elements emit the cancellable, controlled\n `ui:kanban-move-request` event, and React provides `useKanban`. The event reports `itemId`,\n `fromColumn`, `toColumn`, optional `beforeId`, and keyboard or pointer input, but never moves DOM or\n persists application data. The host owns validation, `aria-busy`, optimistic state, rollback,\n status names, and APIs. Use compact `Empty` states inside columns, preserve every column with\n `Skeleton` blocks while loading, and paginate large columns. See [Kanban composition and movement](kanban.md).\n- React applications can compose [TanStack-powered table views](data-table-views.md) through\n `DataTableView`, preserving application-owned cells and server queries. Numeric/date ranges and\n page-scoped selection are opt-in. Use `DataTableSavedViews` for explicit host-owned preference\n persistence, and `useLumenReviewWorkflow` for reviewed commands with stale-revision and unknown-outcome\n guards. Install `review-workflow`, `import-review` or `record-workspace` for executable React compositions.\n- Use `DataTable`, `Tree`, `TreeGrid`, `VirtualList`, `Pagination`, and `Command` for dense data\n collection workflows. Astro and Elements wire selectable/sortable `DataTable` behavior and\n `VirtualList` range events; React also provides built-in fixed-height list windowing. `Tree`\n and `TreeGrid` use roving keyboard focus, and `VirtualList` emits `ui:virtual-list-range` as the\n rendered range changes. Use `createDataViewState`,\n `serializeDataViewState`, `parseDataViewState`, `applyDataViewState`, and `getVirtualRange` for\n saved views, filtering, sorting, pagination, and virtualization math. Use\n `createDataViewStorageKey`, `saveDataViewState`, and `loadDataViewState` when saved views should\n survive navigation or reloads. Use `pinDataViewColumn`, `unpinDataViewColumn`,\n `toggleDataViewSelection`, `createDataViewSearchParams`, and `createDataViewServerRequest` when\n connecting dense views to router state or server-backed data.\n- Use `Resizable` for split-pane editors, dashboards, inspectors, and side-by-side review layouts.\n Astro `UIPrimitives` and registered Web Components generate separator handles and pane sizing\n directly; React apps can use the `Resizable` component or `useResizable` for pointer, keyboard,\n min/max, and double-click reset parity.\n- Use `ThemeBuilder`, `ColorPicker`, `Card`, `Button`, and `Input` to build token preview and export\n tools. Add `data-ui-theme-brand-hue`, `data-ui-theme-accent-hue`, `data-ui-theme-scheme`,\n `data-ui-theme-mode`, manual color inputs such as `data-ui-theme-primary-hex`, and\n `data-ui-theme-target` for scoped live previews. Add `data-ui-theme-export-format`,\n `data-ui-theme-export`, and `data-ui-theme-output` to copy CSS, Figma variable JSON, or design\n token JSON. Astro `UIPrimitives` and registered Web Components wire those controls directly;\n React apps can use `useThemeBuilder` for the same tokens, exports, and control props. Use\n `createThemePalette`, `createThemeBuilderTokens`, `exportThemeBuilderValue`, `exportThemeCss`,\n `parseThemeCss`, and `scoreThemeContrast`\n for theme import/export and accessibility checks. Use `suggestReadableInk`, `mergeThemeTokens`,\n and `tuneThemeContrast` when generated palettes need readable foreground repair.\n- Use `RichTextEditor` with `ButtonGroup`, `ToggleGroup`, and `Textarea` for editor compositions.\n Controls with `data-ui-editor-command` request formatting, block, history, link or list commands;\n use `data-ui-editor-value` for value-bearing commands. Before the browser executes a command,\n Lumen emits the bubbling, cancelable `ui:editor-command-request` event. External engines must\n call `preventDefault()` and set `event.detail.executed` to their synchronous result. A failed\n external command never falls back to the browser. The existing `ui:editor-command` event reports\n completion; use it for notifications, not executing the command again.\n Editable surfaces support formatting shortcuts and emit `ui:editor-change` with HTML and text.\n Set `data-ui-editor-native-state=\"false\"` when an external engine owns toolbar state.\n React apps can use `useRichTextEditor({ commandHandler: ({ command, value }) => boolean })`;\n providing a handler disables native toolbar state syncing by default. Use `nativeState` to\n explicitly control that policy. Engine commands are synchronous; engines with asynchronous\n work should manage completion themselves. Keep Tiptap, ProseMirror, Lexical and other engines\n in the application.\n- Use `Autocomplete`, `SearchField`, `NumberField`, `TimeField`, `DateRangePicker`, `ColorPicker`,\n and `TagGroup` when forms need more than plain text inputs. Add `data-ui-form` to forms that\n should reflect native Constraint Validation API state into `Field` error slots on blur and submit.\n For `InputOTP`, Astro `UIPrimitives` and registered Web Components render/sync a real native input\n with visual OTP segments; React apps can use the `InputOTP` component or `useInputOTP` for the\n same sanitization, paste handling, and caret navigation.\n For `DateRangePicker`, Astro `UIPrimitives` and registered Web Components sync the paired native\n date input `min`/`max` constraints directly; React apps can use `useDateRangePicker` for the same\n start/end syncing props.\n Use native attributes such as `required`, `pattern`, `min`, `max`, and `type`; add\n `data-error-required`, `data-error-pattern`, `data-error-min`, `data-error-max`, or\n `data-error-custom` when a control needs custom copy. Listen for `ui:validate` to call\n `control.setCustomValidity(message)` before the runtime reads validity, and listen for\n `ui:invalid` or `ui:valid` on the form for submission state. Astro `UIPrimitives` and registered\n Web Components bind `data-ui-form` directly; React apps can use `useFormValidation` with `Field`\n to get the same control props, CustomEvents, and error slot updates.\n- Use `Toast` for transient feedback. Static `<Toast>` markup works without\n JavaScript. With Astro `UIPrimitives` or registered Web Components, create toasts with\n `window.LumenToast.create(detail)`, `LumenToast.create(detail)` from `@santi020k/lumen-elements`,\n or `document.dispatchEvent(new CustomEvent('ui:toast', { detail }))`; update them with\n `.update(id, detail)` or `ui:toast-update`; and dismiss them with `.dismiss(id?)` or\n `ui:toast-dismiss`. `detail` supports `title`, `description`, `variant`, `duration`, `placement`,\n `max`, and `action: { label, value }`. Destructive toasts use `role=\"alert\"`; other variants use\n `role=\"status\"`. Runtime toasts pause their timeout on hover or focus, and Escape dismisses the\n focused toast.\n\n## Runtime CustomEvents\n\nWhen `UIPrimitives` is mounted, or when Web Components are registered for matching behavior, Lumen\nuses these CustomEvents:\n\n| Event | Target | Detail | Fires When |\n| -------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------- |\n| `ui:data-table-selection-change` | `DataTable` root `[data-ui-datatable]` | `{ values: string[] }` | Selectable row checkboxes, the select-all checkbox, or form reset changes selected row values. |\n| `ui:schedule-change` | `Schedule` root `[data-ui-schedule]` | `{ eventId?: string, slot?: string }` | A draggable schedule event is dropped on a `[data-ui-schedule-slot]`. |\n| `ui:virtual-list-range` | `VirtualList` root `[data-ui-virtual-list]` | `{ startIndex: number, endIndex: number }` | The virtual list calculates its visible range on init or scroll. |\n| `ui:kanban-move-request` | `KanbanBoard` root `[data-ui-kanban]` | `{ itemId: string, fromColumn: string, toColumn: string, beforeId?: string, input: 'keyboard' \\| 'pointer' }` | A handle requests a controlled move; the event is cancellable and never moves data or DOM. |\n| `ui:tag-remove` | `TagGroup` root `.ui-tag-group` or `[data-ui-tag-group]` | `{ value?: string }` | A `[data-ui-tag-remove]` control removes its closest tag or list item. |\n| `ui:editor-command-request` | `RichTextEditor` root `[data-ui-rich-text-editor]` | `{ command: string, executed: boolean, value?: string }` | Cancel before execution and set executed for external engine ownership. |\n| `ui:editor-command` | `RichTextEditor` root `[data-ui-rich-text-editor]` | `{ command: string, executed: boolean, value?: string }` | A toolbar control or keyboard shortcut runs an editor command. |\n| `ui:editor-change` | `RichTextEditor` root `[data-ui-rich-text-editor]` | `{ html: string, text: string }` | Editable content changes or an editor command runs. |\n| `ui:theme-change` | `ThemeBuilder` root `[data-ui-theme-builder]` | `{ hue: number, accentHue: number, mode: 'generated' \\| 'manual', scheme: 'dark' \\| 'light', tokens: Record<string, string> }` | Hue, manual color, mode, or scheme controls update generated tokens. |\n| `ui:theme-export` | `ThemeBuilder` root `[data-ui-theme-builder]` | `{ css?: string, format: 'css' \\| 'figma' \\| 'tokens', tokens: Record<string, string> \\| null, value: string }` | A `[data-ui-theme-export]` control writes the selected export to the output and clipboard when available. |\n| `ui:language-change` | `LanguageToggle` control | `{ previousValue: string, value: string }` | Requests or applies the next configured locale. |\n| `ui:validate` | Form `[data-ui-form]` | `{ control, form, value }` | Before validation state is read, so custom code can call `control.setCustomValidity(message)`. |\n| `ui:invalid` | Form `[data-ui-form]` | `{ control?, controls?, form }` | Blur or submit validation finds invalid controls. Failed submits focus the first invalid control. |\n| `ui:valid` | Form `[data-ui-form]` | `{ control?, controls?, form }` | Blur or submit validation finds the checked control or form valid. |\n| `ui:toast` | `document` | `{ id?, title?, description?, variant?, duration?, placement?, action? }` | Creates a runtime toast in the matching notification viewport. |\n| `ui:toast-update` | `document` | `{ id, title?, description?, variant?, duration? }` | Updates a runtime toast. |\n| `ui:toast-dismiss` | `document` | `{ id? }` | Dismisses one runtime toast, or all runtime toasts when `id` is omitted. |\n| `ui:toast-action` | Toast `[data-ui-toast]` | `{ id, value? }` | A runtime toast action button is pressed, unless the action specifies a custom event name. |\n\n## Registry Helper\n\n`@santi020k/lumen` exports typed component, recipe, and documentation metadata from\n`@santi020k/lumen/registry`. The umbrella package and every web adapter expose the same small\n`lumen` binary, so projects that install Astro, React, or Elements can use discovery and diagnostics\nwithout another direct dependency. Individual components are addressable by name, including\nkebab-case aliases such as `data-table`:\n\n```bash\nlumen list\nlumen show Button\nlumen show data-table\nlumen add Button\nlumen add Button --target react\nlumen add Button --target elements\nlumen add data-table\nlumen show scheduler\nlumen add scheduler\nlumen add scheduler --target react\nlumen add scheduler --target elements\nlumen add scheduler --dry-run\nlumen add scheduler --fail-on-conflict\nlumen add scheduler --force\nlumen add scheduler --merge\nlumen list --registry ./registry/lumen.registry.json\nlumen add custom-recipe --registry ./registry/custom.registry.json\nlumen add private-recipe --registry https://example.com/lumen.registry.json --registry-token \"$TOKEN\"\nlumen install\nlumen doctor --json\nlumen doctor-native --json\nlumen init --framework astro --tailwind\n```\n\n`lumen doctor-native` emits the same adoption inventory and Android toolchain preflight in readable\ntext or JSON. Treat direct-primitive matches as review suggestions, not rewrite instructions: first\nconfirm whether a surface is product-owned and whether the public Lumen contract preserves its\nbehavior. A failed JDK, SDK, platform-package, or required-NDK preflight should be fixed before\nstarting Gradle so native compilation does not obscure the setup problem.\n\n`lumen add <component>` defaults to an Astro wrapper. Use `--target react` for a local React wrapper\nor `--target elements` for a custom-elements starter. Bundled recipes also accept `--target astro`,\n`--target react`, and `--target elements`; external registry inline files keep their authored paths\nand sources. Those paths must be unique, relative forward-slash paths: the installer rejects\nabsolute paths, parent traversal, and symbolic-link path segments before writing any file. Registry\nmanifests are limited to 5 MiB by default; programmatic `loadLumenRegistry` callers can lower the\nlimit with `maxBytes` and control remote request time with `timeoutMs`.\n\nBefore inventing a wrapper or custom class pattern, check the component source in\n`packages/astro/components`. Astro is the reference implementation for props, class names, data\nattributes, and accessible markup.\n\n## Styling Rules for Generated Code\n\n- Read the [content flow contract](content-flow.md). Stack and Grid own sibling gaps; surfaces own\n padding; Field owns label/control/feedback spacing. Do not add another child margin for the same\n relationship. Use `gap=\"related\"` for closely related controls, `gap=\"group\"` for separate groups\n and `gap=\"section\"` for major sections. Retrieve spacing tokens through MCP.\n- Use Card parts for header/body/actions and a nested Stack inside complex CardContent. Card\n handles missing or hidden parts and wraps footer actions. Choose `density=\"compact\"`,\n `\"comfortable\"` (default) or `\"spacious\"` instead of scattering padding overrides.\n- Keep text rhythm inside Prose or Typography. Verify long and translated text, enlarged text,\n validation states, optional sections and mobile wrapping before calling a composition finished.\n- Use the `page-header` and `section-header` recipes for dashboard identity, optional breadcrumbs,\n status/count badges and independent actions. Keep headings in document order, pass translated\n navigation/action labels and unique heading ids, and connect buttons to application-owned handlers.\n- Retrieve complete `content-flow-header`, `content-flow-settings`, `content-flow-list` and\n `content-flow-actions` examples with `lumen_get_recipe`; the CLI installs the same compositions.\n Keep one outer Container for fluid gutters. Card permits interactive overflow; use AspectRatio\n to clip media. Reading blocks trim their outer margins, so avoid compensating offsets.\n\n- Do not require consumers to configure Tailwind for Lumen components.\n- Prefer component props and composition before adding custom CSS.\n- Assign each visible outer edge to one container. For flush content inside a rounded frame,\n let the frame own the border and corner clipping; remove child rounding with supported props.\n For example, use `<Image radius=\"none\" ... />` below a framed header: `Image` defaults to\n `radius=\"lg\"`, which otherwise leaves rounded gaps at the shared edge. Keep standalone image\n rounding when appropriate.\n- Use a single divider between header and body rather than overlapping full borders. Inset\n surfaces with padding may have their own radius; account for the inset so their curves align.\n Clip artwork at its frame, not an entire interactive surface where focus rings or overlays\n could be cut off. Verify shared corners, borders, and keyboard focus at phone and desktop\n widths in both light and dark themes.\n- Use the shared token names for custom surfaces: `canvas`, `surface`, `surface-muted`,\n `surface-strong`, `line`, `ink`, `ink-soft`, `ink-muted`, `brand`, `brand-solid`, `brand-soft`,\n `accent`, `success`, `warning`, and `danger`.\n- Use `glass` as a boolean prop or attribute for glass surfaces when supported.\n- Preserve semantic labels, keyboard paths, focus states, and native form attributes.\n\n## Figma Handoff\n\nUse `exportThemeFigmaVariables` from `@santi020k/lumen-core/figma` when a design workflow needs\nFigma variable-friendly color values, and `exportThemeDesignTokens` when exporting standard design\ntoken JSON for token importers. See [docs/figma.md](figma.md) for the recommended Figma workflow.\n\n## When Editing This Repo\n\n- Shared contracts belong in `packages/core`.\n- Astro implementation and standalone CSS belong in `packages/astro`.\n- React-specific primitives belong in `packages/react`.\n- Web Component-specific primitives belong in `packages/elements`.\n- Update the relevant package README when public usage changes.\n- Add tests beside package code as `*.test.ts` when behavior, exported metadata, or component\n contracts change.\n\n### React inline range calendar\n\nFor a range editor with both endpoint calendars visible, use the React-only\n`DateRangeCalendar` export. Pass `value={{ start, end }}`, `onValueChange`, required\n`labels={{ start, end, presets }}`, `locale`, optional `min`/`max`, and optional\n`presets={[{ label, value: { start, end } }]}`. `formatDate` customizes the displayed\nendpoint summaries. Keep a draft in the consuming application and apply it only\nwhen confirmed. ISO endpoints must be valid and ascending. Crossing a boundary\nmoves the other endpoint; presets outside the bounds are disabled. The component\nshares `useCalendar` keyboard behavior and stacks its calendars on narrow screens.\n\nFor React input-attached range selection, import `DateRangeInput` from\n`@santi020k/lumen-react`. Pass controlled ISO `value`/`onValueChange`, an accessible\n`label`, and localized `labels` containing `start`, `end`, `presets`, `apply`, and\n`cancel`. It owns draft state and confirmation, anchors a non-modal popover to its\ntrigger, and supports keyboard dismissal and responsive scrolling. Use `validate`\nfor domain restrictions and `renderSummary` for draft details. Optional\n`name={{ start: 'from', end: 'to' }}` submits applied ISO values through hidden inputs.\nSee the React README for the complete usage and browser fallback contract.\n\n## Fixed-height virtual lists\n\n`VirtualList` uses the same DOM windowing controller in Astro, React and Elements. All rows remain\nmounted; offscreen rows are hidden, and inert spacers preserve the complete scroll height. This\nreduces displayed rows, not the cost of creating the initial DOM. Use pagination or an application\nrenderer when retaining the entire dataset in the DOM is too expensive.\n\nUse `itemSize` (Astro/React) or `item-size` (Elements) for a fixed row height in pixels, defaulting\nto 44. `overscan` defaults to four rows on each side. Every direct element child is a row; put\ncontrols inside it, and keep headers or empty-state actions outside the list. Rows must fit the\nconfigured height; variable-height measurement is not supported. Do not add external row margins\nor gaps. Initially hidden rows remain hidden and are excluded from the window.\n\nThe controller refreshes on scrolling, resizing, direct child changes and sizing attribute changes.\nA focused row and its neighboring rows remain available for native Tab navigation, which can extend\nthe displayed window until focus moves. Name the scroll container using `aria-label` or\n`aria-labelledby`; it is keyboard-focusable by default. `ui:virtual-list-range` reports inclusive\n`startIndex` and `endIndex` when the displayed range or item count changes. Empty lists use\n`{ startIndex: 0, endIndex: -1 }`. `data-ui-range-start` and `data-ui-range-end` mirror the current\nwindow. Elements disconnect and React unmount restore original row styles and hidden state.\n\n## NumberField boundary\n\n`NumberField` remains a native number-input wrapper. The v4 consumer audits do not establish a\nshared requirement for a locale-aware draft parser. Applications needing decimal-comma entry,\nlossless monetary drafts or currency rules should retain their explicit parsing and validation\npolicy; Lumen must not infer currency units or precision from locale.\n\n\n## Combobox keyboard behavior in v4\n\nCombobox keeps DOM focus in its input while Arrow Up/Down changes the active option through\n`aria-activedescendant`. Enter commits the active option only while the list is open; without an\nactive option, normal form submission remains available. Home, End, Left and Right retain native\ntext editing. Composition input and canceled keyboard events do not trigger selection or dismissal.\nEscape closes the innermost open control first, preserving an enclosing popup or dialog until the\nnext Escape. Tab dismisses the list when focus leaves the component.\n\nAstro and Elements observe added, removed, relabeled and disabled options, and delegate pointer\nselection to the current list. React derives options from its current props and reports committed\nselections through `onChange`, including controlled inputs. Disabled and read-only inputs stay closed.\nApplications should keep focus on the input and observe `aria-activedescendant` instead of calling\nfocus on option buttons. The DOM adapters match both option labels and values.\n\n## Appearance presets\n\nChoose `default`, `studio` or `glass` at the theme boundary. See [appearance presets](appearance-presets.md) for exact framework APIs. Studio follows the neutral PostLens look. Glass remains explicit per supporting surface; React Native and Compose retain solid fallbacks without adding blur dependencies. Customize semantic colors, typography and surface dimensions through public theme APIs.\n\n## Version-aware build, review, and migration\n\nThe portable skills are `lumen-ui` for implementation, `lumen-review` for a read-only audit, and\n`lumen-migrate` for an explicitly requested upgrade. Their canonical sources live under `skills/`;\n`pnpm run generate:plugin-package` produces the client snapshots. Both client manifests use plugin\n1.1.0 and pin the v4 catalog. This candidate is not proof of publication or directory approval.\n\nBefore applying MCP contracts, call `lumen_check_compatibility` with resolved versions from installed\nmetadata or native lock files. A manifest range is not an installed version. When versions differ,\ninspect installed public types and README or use a matching MCP package. Do not silently upgrade.\nSearch supports English and Spanish queries with framework and platform filters.\n\nFor a v4 upgrade, read `lumen_get_migration` and preview `lumen migrate v4 --dry-run --json` locally.\n`--apply` rewrites only known static SDK import paths in JavaScript and TypeScript. It preserves\ncomments and examples and reports UI/native review triggers. Dependency updates and application-owned\nworkarounds require review; the CLI does not infer that a workaround can be removed. JSX, Astro,\nnative source, dynamic imports, and dependency manifests are not automatically rewritten by the\nSDK import transform.\n\nSee [v4 AI verification](lumen-4-ai.md) for actual agent benchmark coverage and release evidence.\n\nThe integrated `lumen migrate v4` command also retains the release's web spacing migrations and\noptional coordinated dependency workflow. SDK edits compose into that command's source transform,\nso its apply ledger fingerprints the final source and repeat runs do not rewrite spacing twice.\nSDK import edits remain limited to `.ts`, `.js`, and `.mjs`; this restriction does not disable the\nseparate documented JSX/Astro spacing migration. The JSON report includes installed package\nversions and explicit SDK dependency-review findings.\n\n\n## Login examples and Auth\n\nUse the `auth-onboarding` recipe for visual email-code sign-in, code verification, and passkey\nrecovery states in Astro, React, or Elements. The preview controls are disabled and do not authenticate.\nLumen supplies the UI; the optional `@santi020k/auth-client` supplies protocol operations.\nSee the [login integration reference](../packages/templates/README.md#login-examples-and-auth)\nfor helper calls, safe result handling, and the application-owned server and session requirements.\n## Complex React forms\n\nUse the optional `@santi020k/lumen-react/forms` hooks with public form primitives for dependent\nvalidation, unsaved edits, stable repeatable rows, step navigation, and cancellation-safe checks.\nRead [composable form workflows](powerful-forms.md) before implementation. Choose one validation\nowner; keep schemas, financial policy, requests, authorization, and draft storage in the consumer.\n\nFor photo and media editors, install the `media-workspace` recipe and follow\n[Studio media workspaces](studio-media-workspaces.md). Reuse public media controls; keep\nimage processing, storage and export in the consumer application.\n",
46095
+ "readme": "<p align=\"center\">\n <a href=\"https://lumen.santi020k.com\">\n <picture>\n <source media=\"(prefers-color-scheme: dark)\" srcset=\"./docs/assets/readme/hero-dark.svg\">\n <img src=\"./docs/assets/readme/hero-light.svg\" alt=\"Lumen UI — A shared language. Every interface.\" width=\"1200\" height=\"340\">\n </picture>\n </a>\n</p>\n\n<h1 align=\"center\">Lumen UI</h1>\n\n<p align=\"center\">\n <strong>Accessible primitives. Native authoring. Shared foundations.</strong><br>\n An open-source UI system for Astro, React, Web Components, React Native, SwiftUI, and Jetpack Compose.\n</p>\n\n<p align=\"center\">\n <a href=\"https://lumen.santi020k.com\">Documentation</a> ·\n <a href=\"https://lumen.santi020k.com/docs/components\">Components</a> ·\n <a href=\"https://lumen.santi020k.com/#playgrounds\">Playgrounds</a> ·\n <a href=\"https://www.figma.com/community/file/1662337342676541513\">Figma library</a> ·\n <a href=\"https://lumen.santi020k.com/support\">Support</a>\n</p>\n\n<p align=\"center\">\n <a href=\"https://github.com/santi020k/lumen/actions/workflows/ci.yml\"><img alt=\"CI status on main\" src=\"https://img.shields.io/github/actions/workflow/status/santi020k/lumen/ci.yml?branch=main&amp;style=flat-square&amp;label=CI\"></a>\n <a href=\"https://github.com/santi020k/lumen/actions/workflows/codeql.yml\"><img alt=\"CodeQL status\" src=\"https://github.com/santi020k/lumen/actions/workflows/codeql.yml/badge.svg\"></a>\n <a href=\"https://www.npmjs.com/package/@santi020k/lumen-astro\"><img alt=\"Published Astro package version\" src=\"https://img.shields.io/npm/v/@santi020k/lumen-astro?style=flat-square&amp;label=npm\"></a>\n <a href=\"./LICENSE\"><img alt=\"MIT license\" src=\"https://img.shields.io/badge/license-MIT-036b8c?style=flat-square\"></a>\n</p>\n\nLumen gives your product a consistent visual language while preserving each framework's native\nway of building. Compose accessible controls, forms, charts, and complete product layouts from\nshared tokens and explicit component contracts.\n\n**Explore:** [Quick start](#web-quick-start) · [Native apps](#native-playgrounds) · [Templates](#dashboards-and-templates) · [AI and Figma](#ai-and-design-workflows) · [Packages](#packages) · [Repository apps](#repository-apps) · [Contributing](#contributing)\n\n> **Lumen 4:** See the [v4.2 highlights](docs/lumen-4.2.md) for SwiftUI appearance and agent guidance,\n> and [v4.1 highlights](docs/lumen-4.1.md) for loading and forms. Follow the [migration guide](docs/migrating-v3-to-v4.md) when\n> upgrading from v3. Package versions and store builds follow their own release schedules.\n\n## See Lumen in action\n\n<p>\n <a href=\"https://lumen.santi020k.com/docs/web/playground\">\n <img src=\"./apps/docs/src/assets/platforms/web.webp\" alt=\"A Lumen web workspace with navigation, metric cards, an interactive chart, and a task form\" width=\"1184\" height=\"802\">\n </a>\n</p>\n\n*Real Lumen components with illustrative data. [Open the web playground →](https://lumen.santi020k.com/docs/web/playground)*\n\n| Built for | What you get |\n| --- | --- |\n| **Web products** | Astro, React, and custom elements with standalone CSS and optional Tailwind integration. |\n| **Native interfaces** | React Native, SwiftUI, Compose, WidgetKit, and Wear OS foundations with platform conventions. |\n| **Accessible interaction** | Semantic structure, keyboard paths, focus management, and reduced-motion support. |\n| **Design and AI workflows** | Figma resources, a portable agent skill, MCP discovery, and machine-readable contracts. |\n\n## Web quick start\n\nChoose one adapter. Astro requires Astro 5+, React requires React 19+, and the web packages\nrequire Node.js 22.12+ for their tooling. Install in your existing application:\n\n```bash\n# Astro\npnpm add @santi020k/lumen-astro\n\n# React\npnpm add @santi020k/lumen-react\n\n# Web Components\npnpm add @santi020k/lumen-elements\n```\n\n### Astro\n\nImport the stylesheet and mount `UIPrimitives` once in your root layout. The runtime enhances all\ninteractive Lumen markup on the page.\n\n```astro\n---\nimport '@santi020k/lumen-astro/styles.css'\nimport UIPrimitives from '@santi020k/lumen-astro/runtime'\n---\n\n<html lang=\"en\">\n <body>\n <slot />\n <UIPrimitives />\n </body>\n</html>\n```\n\n```astro\n---\nimport { Button, Card, Input, Label } from '@santi020k/lumen-astro'\n---\n\n<Card>\n <Label for=\"email\">Email</Label>\n <Input id=\"email\" name=\"email\" type=\"email\" placeholder=\"you@example.com\" />\n <Button>Subscribe</Button>\n</Card>\n```\n\n### React\n\nLoad the stylesheet once from your app entry or global CSS:\n\n```tsx\nimport '@santi020k/lumen-react/styles.css'\nimport { Button, Card, Input, Label } from '@santi020k/lumen-react'\n\nexport function SubscribeForm() {\n return (\n <Card>\n <Label htmlFor=\"email\">Email</Label>\n <Input id=\"email\" name=\"email\" type=\"email\" placeholder=\"you@example.com\" />\n <Button>Subscribe</Button>\n </Card>\n )\n}\n```\n\n### Web Components\n\nImport the styles and register the elements once:\n\n```html\n<style>\n @import \"@santi020k/lumen-elements/styles.css\";\n</style>\n\n<script type=\"module\">\n import { defineLumenElements } from '@santi020k/lumen-elements/define'\n\n defineLumenElements()\n</script>\n\n<lumen-card>\n <lumen-label id=\"email-label\">Email</lumen-label>\n <lumen-input aria-labelledby=\"email-label\" id=\"email\" name=\"email\" type=\"email\" placeholder=\"you@example.com\"></lumen-input>\n <lumen-button>Subscribe</lumen-button>\n</lumen-card>\n```\n\nSee the [Web documentation](https://lumen.santi020k.com/docs/web) for installation, theming,\ncomponent examples, and API details.\n\nBrowse the [package directory](https://lumen.santi020k.com/docs/packages) for framework adapters,\nshared foundations, and optional integrations, with links to focused examples and usage guides.\n\nFor failure states, use the [error-handling guide](./docs/error-handling.md) to choose between field\nfeedback, summaries, persistent alerts, transient toasts, and the `ErrorState` recovery surface.\nLumen owns presentation and accessibility; applications retain logging, retry, and exception policy.\n\nFor native applications, choose the [React Native](https://lumen.santi020k.com/docs/react-native),\n[Apple / SwiftUI](https://lumen.santi020k.com/docs/apple), or\n[Android / Compose](https://lumen.santi020k.com/docs/android) guide. The\n[shared foundations](https://lumen.santi020k.com/docs/foundations) explain the cross-platform token\nand component contract; repository contributors can also use the\n[cross-platform architecture](./docs/cross-platform.md) and\n[native component reference](./docs/native-components.md).\n\n> **Native qualification:** Check the [compatibility matrix](docs/native-compatibility.md) and\n> [device evidence](docs/native-device-validation.md) for your target. Local builds, published\n> artifacts, and physical-device accessibility are distinct verification steps.\n\n## Tailwind CSS\n\nKeep the layer prelude, Tailwind import, and Lumen stylesheet in the same shared CSS entry. Replace\n`lumen-astro` with the package for your framework.\n\n```css\n@import \"@santi020k/lumen-astro/layers.css\";\n@import \"tailwindcss\";\n@import \"@santi020k/lumen-astro/styles.css\";\n```\n\nThis order places Tailwind base styles before Lumen components and Tailwind utilities above Lumen\ncomponent defaults.\n\n## Native playgrounds\n\n<table>\n <tr>\n <th>SwiftUI</th>\n <th>Jetpack Compose</th>\n <th>React Native</th>\n </tr>\n <tr>\n <td><a href=\"https://lumen.santi020k.com/docs/apple/playground\"><img src=\"./apps/docs/src/assets/platforms/apple.webp\" alt=\"Native SwiftUI release checklist example\" width=\"220\"></a></td>\n <td><a href=\"https://lumen.santi020k.com/docs/android/playground\"><img src=\"./apps/docs/src/assets/platforms/android.webp\" alt=\"Native Compose release checklist example\" width=\"220\"></a></td>\n <td><a href=\"https://lumen.santi020k.com/docs/react-native/playground\"><img src=\"./apps/docs/src/assets/platforms/react-native.webp\" alt=\"Expo browser preview of the contributor profile example\" width=\"220\"></a></td>\n </tr>\n <tr>\n <td>Native app capture</td>\n <td>Native app capture</td>\n <td>Expo browser preview</td>\n </tr>\n</table>\n\nSee [capture provenance](apps/docs/src/assets/platforms/README.md) for sources and verification scope.\n\nTry Lumen before adding it to your project. The [playground section](https://lumen.santi020k.com/#playgrounds)\nbrings together the browser demos and native apps. Explore component states and themes, then use\nthe open-source apps as implementation references. Start with the\n[web playground](https://lumen.santi020k.com/docs/web/playground) for Astro, React, and Web Components,\nor choose a native gallery:\n\n- [`apps/playground-react-native`](./apps/playground-react-native) runs through Expo on the web,\n iOS, and Android. [Try the browser preview](https://lumen.santi020k.com/docs/react-native/playground#preview)\n or use the local setup guide for native evaluation. EAS profiles cover TestFlight, Android App\n Bundles, and APKs; this gallery has no separate public store listing.\n- [`apps/playground-apple`](./apps/playground-apple) is available on the\n [App Store](https://apps.apple.com/app/id6805250815) for iPhone, iPad, and Mac. It also builds as\n an iOS Xcode app or macOS Swift Package executable for local exploration.\n- [`apps/playground-android`](./apps/playground-android) builds a native Compose application and a\n directly installable debug APK. It is also available on\n [Google Play](https://play.google.com/store/apps/details?id=com.santi020k.lumen.playground.compose)\n for Android phones and tablets.\n\nSee the [playground workflow](./docs/playgrounds.md) for a platform chooser, reference-app source,\nand run, capture, and distribution commands. Store builds follow their own release schedule and\nmay differ from the current repository version.\n\n## Appearance presets\n\nChoose an appearance preset or customize the existing themes with the [appearance guide](docs/appearance-presets.md). The Studio preset uses PostLens as its visual reference.\n\n## Dashboards and templates\n\nExplore the [template gallery](https://lumen.santi020k.com/templates) for complete analytics,\nSaaS admin, commerce, project workspace, and authentication/onboarding experiences. Each family\nuses public Lumen primitives and semantic tokens, includes responsive and accessibility coverage,\nand ships through the CLI for all three framework targets:\n\n```bash\nlumen add analytics-dashboard\nlumen add commerce-dashboard --target react\nlumen add auth-onboarding --target elements\n```\n\n## AI and design workflows\n\nInstall the portable Lumen skill in Codex, Claude Code, Cursor, Windsurf, and other compatible\ncoding agents:\n\n```bash\nnpx skills add santi020k/lumen --skill lumen-ui\n```\n\nThe skill teaches agents how to select, compose, theme, and verify Lumen primitives. Pair it with\n[`@santi020k/lumen-mcp`](./packages/mcp) when an agent needs to search the live catalog or retrieve\ncurrent source, props, tokens, and usage rules.\n\nFor ChatGPT and Codex, install the published\n[Lumen UI plugin](https://chatgpt.com/plugins/plugin_asdk_app_6a8f6c526c5481918eb8a48806fa112b)\nfrom the Plugins Directory. Select **Install plugin**, then mention **@Lumen UI** in a request such\nas “Find the right Lumen components for an accessible React settings screen.” The plugin bundles\nthe skill with the hosted, read-only catalog; no separate Lumen account or local MCP setup is needed.\n\nLumen is designed to reduce repetitive UI code and unnecessary agent context. Actual token usage\ndepends on the model, task, and corrections; see the [measurement method](docs/ai-efficiency.md).\n\n<details>\n<summary>Guides, registries, and integration references</summary>\n\nAdditional machine-readable surfaces include:\n\n- [`llms.txt`](./llms.txt) for a concise project map.\n- [`docs/ai-usage.md`](./docs/ai-usage.md) for downstream generation examples.\n- [`docs/styling-contract.md`](./docs/styling-contract.md) for stable parts and component variables.\n- [`docs/import-and-icon-performance.md`](./docs/import-and-icon-performance.md) for repeatable import and icon evidence.\n- [`docs/graphics.md`](./docs/graphics.md) for portable decorative artwork across web and native platforms.\n- [`docs/consumer-regression-fixtures.md`](./docs/consumer-regression-fixtures.md) for production-shaped local coverage.\n- [`docs/project-adoption.md`](./docs/project-adoption.md) for the sibling-project audit and staged\n web and native migration strategy.\n- [`registry/lumen.registry.json`](./registry/lumen.registry.json) for recipes and installable file\n groups.\n- [`docs/figma.md`](./docs/figma.md) for Figma variables and Code Connect workflows.\n- [`apps/figma-plugin/README.md`](./apps/figma-plugin/README.md) for the **beta** Figma plugin,\n Astro starters, and AI handoff.\n- [`plugins/lumen-ui`](./plugins/lumen-ui) for the packaged Codex and ChatGPT plugin, with its\n [submission and review record](./docs/openai-plugin-submission.md).\n- [`.claude-plugin/marketplace.json`](./.claude-plugin/marketplace.json) for the Claude Code\n marketplace, with its [installation and submission guide](./docs/claude-code-plugin.md).\n- [`docs/cross-platform.md`](./docs/cross-platform.md) for the native architecture, support tiers,\n and component-parity policy.\n- [`docs/native-patterns.md`](./docs/native-patterns.md) for developed native compositions and the\n boundary between Lumen presentation and platform-owned application structure.\n\n</details>\n\n## Packages\n\n| Package | Purpose |\n| --- | --- |\n| [`@santi020k/lumen-astro`](./packages/astro) | Reference implementation, Astro components, and progressive-enhancement runtime |\n| [`@santi020k/lumen-react`](./packages/react) | React components and behavior hooks |\n| [`@santi020k/lumen-react-hook-form`](./packages/react-hook-form) | Optional React Hook Form adapters for composite controls |\n| [`@santi020k/lumen-elements`](./packages/elements) | Standards-based Web Components |\n| [`@santi020k/lumen-core`](./packages/core) | Shared tokens, metadata, class helpers, and behavior contracts |\n| [`@santi020k/lumen-tokens`](./packages/tokens) | Canonical platform-neutral design token source |\n| [`@santi020k/lumen-react-native`](./packages/react-native) | React Native foundations and native primitives |\n| [`LumenUI`](./packages/swift) | Swift Package for iOS, iPadOS, macOS, tvOS, visionOS, and watchOS foundations and native primitives |\n| [`LumenWidgetUI`](./packages/swift-widget) | Focused WidgetKit-safe semantic text, icon, badge, and compact-stat foundations |\n| [`lumen-compose`](./packages/compose) | Android library for Jetpack Compose foundations and native primitives |\n| [`lumen-compose-wear`](./packages/compose/wear) | Focused Wear OS primitives for at-a-glance round-screen actions, progress, status, metrics, and rows |\n| [`@santi020k/lumen-icons-brand`](./packages/icons-brand) | Optional namespaced brand icons for web `Icon` adapters |\n| [`@santi020k/lumen`](./packages/lumen) | Umbrella package, CLI, registry metadata, and public package map |\n| [`@santi020k/lumen-mcp`](./packages/mcp) | MCP server for component discovery and source retrieval |\n\nThe private [`@santi020k/lumen-templates`](packages/templates/README.md) workspace supplies the\ngallery and preview renderer. Consumer recipes are installed through the public Lumen CLI.\n\n## Repository apps\n\nEach app has its own setup, source map, and verification guidance:\n\n| App | Purpose |\n| --- | --- |\n| [Documentation](apps/docs/README.md) | Public documentation, live examples, search, and native previews |\n| [Template showcase](apps/templates/README.md) | Standalone gallery of five product-template families |\n| [Next.js smoke app](apps/next-smoke/README.md) | Server/client boundaries and rendered consumer regression fixtures |\n| [Apple playground](apps/playground-apple/README.md) | iPhone, iPad, Mac, watchOS, and WidgetKit reference hosts |\n| [Android playground](apps/playground-android/README.md) | Compose phone/tablet app and separate Wear OS host |\n| [React Native playground](apps/playground-react-native/README.md) | Expo reference app and embedded browser gallery |\n| [Figma plugin](apps/figma-plugin/README.md) | Development beta for Astro starters and structured agent handoff |\n| [Motion studio](apps/motion/README.md) | Reusable product films and local video exports |\n| [Store artwork](apps/store-assets/README.md) | Canonical SVG sources for the playground app icons |\n\n## Lumen 4\n\nLumen 4 brings consumer-driven date selection, chart readability, clipboard feedback,\nloading states, native layout improvements and a new ImageComparison primitive. Start with the\n[v3 → v4 migration guide](docs/migrating-v3-to-v4.md), [consumer audit](docs/lumen-4-consumer-audit.md),\nand [v4.2 release highlights](docs/lumen-4.2.md). The community gallery includes published consumer\nscreenshots; it does not imply those applications already run the current release.\n\nFor existing applications, start with [v1 → v2](docs/migrating-v1-to-v2.md) when needed, then\n[v2 → v3](docs/migrating-v2-to-v3.md) before\n[v3 → v4](docs/migrating-v3-to-v4.md). For initial adoption, use\n[Migrating to Lumen](docs/migrating-to-lumen.md).\n\n## Contributing\n\nLumen is a pnpm workspace. Astro is the reference implementation; shared contracts belong in\n`packages/core`, while framework-specific behavior stays in its adapter package.\n\nUse Node.js 22.22.2+, 24.15.0+, or 26+ and pnpm 12.8.1, as declared in the root manifest.\n\n```bash\npnpm install --frozen-lockfile\npnpm run dev\npnpm run validate\n```\n\nRead [`CONTRIBUTING.md`](./CONTRIBUTING.md) before opening a pull request. User-visible package\nchanges require a changeset.\n\n## Community\n\nCreate reusable product animations and social videos locally with the\n[Lumen motion studio](./apps/motion/README.md). Its first pilot previews one interface in four\nappearances and exports portrait, square, and landscape MP4s through HyperFrames.\n\n[Feedback & support](https://lumen.santi020k.com/support) connects ideas, questions, bug reports,\nand the [public roadmap](https://lumen.santi020k.com/support#roadmap). Discuss improvements on GitHub\nand follow accepted work through implementation and release.\n\nLumen is free and MIT licensed while the project focuses on adoption, public examples, and\nreal-world feedback.\n\n- Explore task-oriented [guides](https://lumen.santi020k.com/guides).\n- Share a public project in the [community showcase](https://lumen.santi020k.com/community).\n- Evaluate the system with the open [team adoption path](https://lumen.santi020k.com/teams).\n- Tell us what would improve your next build through the\n [adoption survey](https://github.com/santi020k/lumen/issues/new?template=adoption-feedback.yml).\n\nMaintainers can use the [exposure playbook](./docs/exposure-playbook.md) for publishing cadence,\ncampaign measurement, video scripts, and launch copy, and the\n[marketing package](./docs/marketing/README.md) for execution-ready copy, channel decisions, a\nfour-week calendar, asset briefs, an approval queue, and a community-response library.\n\n## Find your next step\n\n| Resource | Use it for |\n| --- | --- |\n| [Cross-platform architecture](docs/cross-platform.md) | Shared foundations and adapter responsibilities. |\n| [Native compatibility](docs/native-compatibility.md) | Supported toolchains and platform baselines. |\n| [Native release runbook](docs/native-release-runbook.md) | Artifact qualification and distribution gates. |\n\n## License\n\nLumen's original code and artwork are available under the [MIT License](./LICENSE). Generated\ninterface icons retain the Lucide ISC or Feather MIT terms, and generated brand icons retain Font\nAwesome Free's CC BY 4.0 terms. The native packages include the required attribution and license\nnotices; see [`icons/THIRD_PARTY_NOTICES.md`](./icons/THIRD_PARTY_NOTICES.md).\n\nCreated and maintained by [Santiago Molina](https://santi020k.com).\n"
45878
46096
  },
45879
46097
  "meta": {
45880
- "catalogHash": "ce84a5a24ab90e21eedafcb5f66be2167395a2311eac3054ecb515ebe3961823",
46098
+ "catalogHash": "7113faa8466fe24529069b1918abbbfdf18c101d00341b2824cba2149c416e77",
45881
46099
  "componentCount": 196,
45882
46100
  "nativeComponentCount": 125,
45883
46101
  "packages": [
@@ -45891,21 +46109,21 @@
45891
46109
  "com.santi020k:lumen-compose"
45892
46110
  ],
45893
46111
  "packageVersions": {
45894
- "@santi020k/lumen-astro": "4.0.0",
45895
- "@santi020k/lumen-core": "4.0.0",
45896
- "@santi020k/lumen-elements": "4.0.0",
45897
- "@santi020k/lumen-mcp": "4.0.0",
45898
- "@santi020k/lumen-react": "4.0.0",
45899
- "@santi020k/lumen-react-native": "4.0.0",
46112
+ "@santi020k/lumen-astro": "4.1.0",
46113
+ "@santi020k/lumen-core": "4.1.0",
46114
+ "@santi020k/lumen-elements": "4.2.0",
46115
+ "@santi020k/lumen-mcp": "4.2.0",
46116
+ "@santi020k/lumen-react": "4.1.0",
46117
+ "@santi020k/lumen-react-native": "4.0.1",
45900
46118
  "@santi020k/lumen-tokens": "4.0.0",
45901
- "LumenUI": "4.0.0",
46119
+ "LumenUI": "4.2.0",
45902
46120
  "com.santi020k:lumen-compose": "4.0.0",
45903
46121
  "com.santi020k:lumen-compose-wear": "4.0.0"
45904
46122
  },
45905
46123
  "registryName": "lumen",
45906
46124
  "registryVersion": 1,
45907
46125
  "schemaVersion": 8,
45908
- "serverVersion": "4.0.0"
46126
+ "serverVersion": "4.2.0"
45909
46127
  },
45910
46128
  "migration": {
45911
46129
  "schemaVersion": 1,
@@ -46346,11 +46564,12 @@
46346
46564
  "approval": {
46347
46565
  "approver": "Santiago Molina (santi020k)",
46348
46566
  "date": "2026-10-07",
46349
- "decision": "Publish Lumen 4.0.0 and deploy the coordinated release; deliver playground 1.0.3 for Apple and Android review with current screenshots and listing metadata under the standing authorization, after all checks and reviews pass. Standing authorization is recorded by santi020k in PR #95 comment 6027959606.",
46350
- "reviewedRevision": "cdb28a38aca0071af35021341bb7fbbefd7730a6",
46567
+ "decision": "Approve the independently reviewed Mac certificate extraction correction and real macOS tool regression under the standing authorization recorded by santi020k in PR #95 comment 6027959606. Delivery remains conditional on all required hosted checks and current Codex review passing before protected merge. Preserve immutable v4.0.0 artifacts and approved iOS and Android submissions and rollout holds; deliver the corrected Mac build from merged main.",
46568
+ "reviewedRevision": "eea78c5a5d49f9f6610fbe23b75a8e721db03f7e",
46351
46569
  "evidence": [
46352
- "https://github.com/santi020k/lumen/commit/cdb28a38aca0071af35021341bb7fbbefd7730a6",
46353
- "https://github.com/santi020k/lumen/pull/95"
46570
+ "https://github.com/santi020k/lumen/commit/eea78c5a5d49f9f6610fbe23b75a8e721db03f7e",
46571
+ "https://github.com/santi020k/lumen/pull/95",
46572
+ "https://github.com/santi020k/lumen/pull/101"
46354
46573
  ]
46355
46574
  }
46356
46575
  },
@@ -60162,7 +60381,7 @@
60162
60381
  },
60163
60382
  "react-native": {
60164
60383
  "packages/react-native/src/accessibility-announcement.ts": "import { useEffect, useRef } from 'react'\nimport { AccessibilityInfo, Platform } from 'react-native'\n\nimport type { LumenErrorStateAnnouncement } from './structured-recipes.js'\n\n/** Android and web use live regions; iOS requires an explicit announcement. */\nexport const useLumenAccessibilityAnnouncement = (\n message: string,\n announcement: LumenErrorStateAnnouncement\n): void => {\n const previousRef = useRef<{ message: string, announcement: LumenErrorStateAnnouncement } | null>(null)\n\n useEffect(() => {\n if (Platform.OS !== 'ios' || announcement === 'off' || !message.trim()) {\n previousRef.current = null\n\n return\n }\n\n if (previousRef.current?.message === message && previousRef.current.announcement === announcement) return\n\n previousRef.current = { announcement, message }\n\n AccessibilityInfo.announceForAccessibilityWithOptions(message, { queue: announcement === 'polite' })\n }, [announcement, message])\n}\n",
60165
- "packages/react-native/src/additional-components.tsx": "import {\n type ReactElement,\n type ReactNode,\n useId\n} from 'react'\nimport {\n Pressable,\n type PressableProps,\n Text,\n TextInput,\n type TextInputProps,\n type TextStyle,\n useWindowDimensions,\n View,\n type ViewProps } from 'react-native'\n\nimport { useLumenAccessibilityAnnouncement } from './accessibility-announcement.js'\nimport { LumenFieldContext } from './field-context.js'\nimport type { LumenTextInputRef, LumenViewRef } from './native-ref-types.js'\nimport {\n resolveLumenAriaInvalid,\n resolveLumenButtonOpacity,\n resolveLumenValidationHint\n} 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?: LumenViewRef\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 useLumenAccessibilityAnnouncement([title, description].filter(Boolean).join('. '), 'polite')\n\n return (\n <View\n ref={ref}\n {...props}\n accessibilityLiveRegion=\"polite\"\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 alignItems: 'center',\n justifyContent: 'center',\n minHeight: 44,\n minWidth: 44,\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?: LumenTextInputRef\n value: string\n}\n\nexport const LumenTextarea = ({\n accessibilityHint,\n accessibilityLabel,\n accessibilityState,\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={resolveLumenValidationHint(errorMessage, description, accessibilityHint)}\n accessibilityLabel={accessibilityLabel ?? label}\n accessibilityState={{ ...accessibilityState, disabled: !editable }}\n aria-invalid={resolveLumenAriaInvalid(errorMessage)}\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?: LumenViewRef\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 const isDisabled = disabled === true\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 accessibilityState={{ disabled: isDisabled }}\n disabled={isDisabled}\n hitSlop={6}\n onPress={event => {\n event.stopPropagation()\n\n onRemove()\n }}\n style={{\n alignItems: 'center',\n justifyContent: 'center',\n minHeight: 24,\n minWidth: 24\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: isDisabled ? 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: isDisabled, selected }}\n disabled={isDisabled}\n onPress={onPress}\n style={state => [\n baseStyle,\n { opacity: resolveLumenButtonOpacity(isDisabled, 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?: LumenViewRef\n required?: boolean\n /** Localized spoken description of a required field. */\n requiredLabel?: string\n}\n\nconst requiredFieldLabel = (label: string, required: boolean, requiredLabel: string): string | undefined => required ? `${label}, ${requiredLabel}` : undefined\n\nexport const LumenFieldGroup = ({\n children,\n description,\n errorMessage,\n label,\n ref,\n required = false,\n requiredLabel = 'required',\n style,\n ...props\n}: LumenFieldGroupProps): ReactElement => {\n const theme = useLumenTheme()\n const fieldId = useId().replaceAll(':', '')\n const labelId = `${fieldId}-label`\n const descriptionId = description ? `${fieldId}-description` : undefined\n const errorId = errorMessage ? `${fieldId}-error` : undefined\n\n return (\n <LumenFieldContext value={{\n ...(descriptionId ? { descriptionId } : {}),\n error: Boolean(errorMessage),\n ...(errorId ? { errorId } : {}),\n label,\n labelId,\n required\n }}\n >\n <View ref={ref} {...props} style={[{ gap: theme.spacing.sm }, style]}>\n <Text\n accessibilityLabel={requiredFieldLabel(label, required, requiredLabel)}\n nativeID={labelId}\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 (\n <Text nativeID={descriptionId} style={{ color: theme.colors.inkMuted, fontSize: theme.fontSizes.xs }}>\n {description}\n </Text>\n ) :\n null}\n {children}\n {errorMessage ?\n (\n <Text\n accessibilityLiveRegion=\"polite\"\n accessibilityRole=\"alert\"\n nativeID={errorId}\n style={{ color: theme.colors.danger, fontSize: theme.fontSizes.xs }}\n >\n {errorMessage}\n </Text>\n ) :\n null}\n </View>\n </LumenFieldContext>\n )\n}\n\nexport type LumenButtonGroupOrientation = 'horizontal' | 'vertical'\n\nexport interface LumenButtonGroupProps extends ViewProps {\n children: ReactNode\n orientation?: LumenButtonGroupOrientation\n ref?: LumenViewRef\n}\n\nexport const LumenButtonGroup = ({\n children,\n orientation = 'horizontal',\n ref,\n style,\n ...props\n}: LumenButtonGroupProps): ReactElement => {\n const theme = useLumenTheme()\n const { fontScale } = useWindowDimensions()\n const horizontal = orientation === 'horizontal' && fontScale < 2\n\n return (\n <View\n ref={ref}\n {...props}\n style={[\n {\n alignItems: horizontal ? 'center' : 'stretch',\n flexDirection: horizontal ? 'row' : 'column',\n flexWrap: horizontal ? 'wrap' : 'nowrap',\n gap: theme.spacing.sm\n },\n style\n ]}\n >\n {children}\n </View>\n )\n}\n",
60384
+ "packages/react-native/src/additional-components.tsx": "import {\n type ReactElement,\n type ReactNode,\n useId\n} from 'react'\nimport {\n Pressable,\n type PressableProps,\n Text,\n TextInput,\n type TextInputProps,\n type TextStyle,\n useWindowDimensions,\n View,\n type ViewProps } from 'react-native'\n\nimport { useLumenAccessibilityAnnouncement } from './accessibility-announcement.js'\nimport { LumenFieldContext, type LumenFieldContextValue } from './field-context.js'\nimport type { LumenTextInputRef, LumenViewRef } from './native-ref-types.js'\nimport {\n resolveLumenAriaInvalid,\n resolveLumenButtonOpacity,\n resolveLumenValidationHint\n} 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?: LumenViewRef\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 useLumenAccessibilityAnnouncement([title, description].filter(Boolean).join('. '), 'polite')\n\n return (\n <View\n ref={ref}\n {...props}\n accessibilityLiveRegion=\"polite\"\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 alignItems: 'center',\n justifyContent: 'center',\n minHeight: 44,\n minWidth: 44,\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?: LumenTextInputRef\n value: string\n}\n\nexport const LumenTextarea = ({\n accessibilityHint,\n accessibilityLabel,\n accessibilityState,\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={resolveLumenValidationHint(errorMessage, description, accessibilityHint)}\n accessibilityLabel={accessibilityLabel ?? label}\n accessibilityState={{ ...accessibilityState, disabled: !editable }}\n aria-invalid={resolveLumenAriaInvalid(errorMessage)}\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?: LumenViewRef\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 const isDisabled = disabled === true\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 accessibilityState={{ disabled: isDisabled }}\n disabled={isDisabled}\n hitSlop={6}\n onPress={event => {\n event.stopPropagation()\n\n onRemove()\n }}\n style={{\n alignItems: 'center',\n justifyContent: 'center',\n minHeight: 24,\n minWidth: 24\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: isDisabled ? 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: isDisabled, selected }}\n disabled={isDisabled}\n onPress={onPress}\n style={state => [\n baseStyle,\n { opacity: resolveLumenButtonOpacity(isDisabled, 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?: LumenViewRef\n required?: boolean\n /** Localized spoken description of a required field. */\n requiredLabel?: string\n}\n\ninterface LumenFieldContextSource {\n description: string | undefined\n errorMessage: string | undefined\n fieldId: string\n label: string\n required: boolean\n}\n\nconst createLumenFieldContextValue = ({\n description, errorMessage, fieldId, label, required\n}: LumenFieldContextSource): LumenFieldContextValue => ({\n ...(description ? { description, descriptionId: `${fieldId}-description` } : {}),\n error: Boolean(errorMessage),\n ...(errorMessage ? { errorId: `${fieldId}-error`, errorMessage } : {}),\n label,\n labelId: `${fieldId}-label`,\n required\n})\n\nconst requiredFieldLabel = (label: string, required: boolean, requiredLabel: string): string | undefined => required ? `${label}, ${requiredLabel}` : undefined\n\nexport const LumenFieldGroup = ({\n children,\n description,\n errorMessage,\n label,\n ref,\n required = false,\n requiredLabel = 'required',\n style,\n ...props\n}: LumenFieldGroupProps): ReactElement => {\n const theme = useLumenTheme()\n const fieldId = useId().replaceAll(':', '')\n const labelId = `${fieldId}-label`\n const descriptionId = description ? `${fieldId}-description` : undefined\n const errorId = errorMessage ? `${fieldId}-error` : undefined\n\n return (\n <LumenFieldContext value={createLumenFieldContextValue({ description, errorMessage, fieldId, label, required })}>\n <View ref={ref} {...props} style={[{ gap: theme.spacing.sm }, style]}>\n <Text\n accessibilityLabel={requiredFieldLabel(label, required, requiredLabel)}\n nativeID={labelId}\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 (\n <Text nativeID={descriptionId} style={{ color: theme.colors.inkMuted, fontSize: theme.fontSizes.xs }}>\n {description}\n </Text>\n ) :\n null}\n {children}\n {errorMessage ?\n (\n <Text\n accessibilityLiveRegion=\"polite\"\n accessibilityRole=\"alert\"\n nativeID={errorId}\n style={{ color: theme.colors.danger, fontSize: theme.fontSizes.xs }}\n >\n {errorMessage}\n </Text>\n ) :\n null}\n </View>\n </LumenFieldContext>\n )\n}\n\nexport type LumenButtonGroupOrientation = 'horizontal' | 'vertical'\n\nexport interface LumenButtonGroupProps extends ViewProps {\n children: ReactNode\n orientation?: LumenButtonGroupOrientation\n ref?: LumenViewRef\n}\n\nexport const LumenButtonGroup = ({\n children,\n orientation = 'horizontal',\n ref,\n style,\n ...props\n}: LumenButtonGroupProps): ReactElement => {\n const theme = useLumenTheme()\n const { fontScale } = useWindowDimensions()\n const horizontal = orientation === 'horizontal' && fontScale < 2\n\n return (\n <View\n ref={ref}\n {...props}\n style={[\n {\n alignItems: horizontal ? 'center' : 'stretch',\n flexDirection: horizontal ? 'row' : 'column',\n flexWrap: horizontal ? 'wrap' : 'nowrap',\n gap: theme.spacing.sm\n },\n style\n ]}\n >\n {children}\n </View>\n )\n}\n",
60166
60385
  "packages/react-native/src/advanced-form-components.tsx": "import { type ReactElement, useState } from 'react'\nimport { ScrollView, type TextInputProps, View } from 'react-native'\n\nimport { isLumenDecimalInBounds, type LumenDecimalDraft, type LumenDecimalOptions, normalizeLumenNumericOTP, parseLumenDecimalDraft, stepLumenDecimalDraft } from '@santi020k/lumen-core'\n\nimport { LumenFieldGroup } from './additional-components.js'\nimport { LumenButton, LumenText, LumenTextField } from './foundation-primitives.js'\nimport type { LumenTextInputRef } from './native-ref-types.js'\nimport { useLumenTheme } from './theme-context.js'\n\ninterface ControlledFieldProps extends Omit<TextInputProps, 'editable' | 'onChangeText' | 'value'> {\n description?: string\n enabled?: boolean\n errorMessage?: string\n label: string\n onValueChange: (value: string) => void\n readOnly?: boolean\n ref?: LumenTextInputRef\n value: string\n}\n\nconst isEditable = (enabled?: boolean, readOnly?: boolean): boolean => (enabled ?? true) && !(readOnly ?? false)\n\nconst fieldGroupProps = (label: string, description?: string, errorMessage?: string) => ({\n ...(description === undefined ? {} : { description }),\n ...(errorMessage === undefined ? {} : { errorMessage }),\n label\n})\n\nexport interface LumenPasswordFieldProps extends Omit<ControlledFieldProps, 'secureTextEntry' | 'textContentType' | 'autoComplete'> {\n hideLabel?: string\n newPassword?: boolean\n showLabel?: string\n}\n\nconst LumenPasswordFieldControl = ({\n description, enabled, errorMessage, hideLabel = 'Hide password', label,\n newPassword = false, onBlur, onValueChange, readOnly, ref,\n showLabel = 'Show password', value, ...props\n}: LumenPasswordFieldProps): ReactElement => {\n const [revealed, setRevealed] = useState(false)\n const editable = isEditable(enabled, readOnly)\n const visible = editable && revealed\n\n return (\n <LumenFieldGroup {...fieldGroupProps(label, description, errorMessage)}>\n <LumenTextField\n {...(ref ? { ref } : {})}\n {...props}\n autoCapitalize=\"none\"\n autoComplete={newPassword ? 'new-password' : 'current-password'}\n autoCorrect={false}\n editable={editable}\n onBlur={event => {\n setRevealed(false)\n\n onBlur?.(event)\n }}\n onChangeText={proposal => {\n if (editable) onValueChange(proposal)\n }}\n secureTextEntry={!visible}\n textContentType={newPassword ? 'newPassword' : 'password'}\n value={value}\n />\n <LumenButton\n disabled={!editable}\n intent=\"quiet\"\n accessibilityLabel={visible ? hideLabel : showLabel}\n onPress={() => {\n if (editable) setRevealed(current => !current)\n }}\n >\n {visible ? hideLabel : showLabel}\n </LumenButton>\n </LumenFieldGroup>\n )\n}\n\nexport const LumenPasswordField = (props: LumenPasswordFieldProps): ReactElement => (\n <LumenPasswordFieldControl key={`${props.enabled ?? true}-${props.readOnly ?? false}`} {...props} />\n)\n\nexport interface LumenInputOTPProps extends Omit<ControlledFieldProps, 'autoComplete' | 'keyboardType' | 'maxLength' | 'secureTextEntry' | 'textContentType'> {\n length?: number\n masked?: boolean\n onComplete?: (value: string) => void\n}\n\nexport const LumenInputOTP = ({\n description, enabled, errorMessage, label, length = 6, masked = false,\n onComplete, onValueChange, readOnly, ref, value, ...props\n}: LumenInputOTPProps): ReactElement => {\n if (normalizeLumenNumericOTP(value, length) !== value) throw new RangeError('OTP value must contain at most length ASCII digits.')\n\n const editable = isEditable(enabled, readOnly)\n\n return (\n <LumenFieldGroup {...fieldGroupProps(label, description, errorMessage)}>\n <LumenTextField\n {...(ref ? { ref } : {})}\n {...props}\n autoComplete=\"one-time-code\"\n autoCorrect={false}\n editable={editable}\n keyboardType=\"number-pad\"\n onChangeText={proposal => {\n if (!editable) return\n\n const normalized = normalizeLumenNumericOTP(proposal, length)\n\n if (normalized === null || normalized === value) return\n\n onValueChange(normalized)\n\n if (normalized.length === length) onComplete?.(normalized)\n }}\n secureTextEntry={masked}\n textContentType=\"oneTimeCode\"\n value={value}\n />\n </LumenFieldGroup>\n )\n}\n\nexport interface LumenNumberFieldProps extends ControlledFieldProps {\n decrementLabel?: string\n incrementLabel?: string\n invalidNumberLabel?: string\n locale?: string\n max?: string\n min?: string\n outOfRangeLabel?: string\n showStepper?: boolean\n step?: string\n}\n\nconst numberLabels = (\n decrementLabel?: string, incrementLabel?: string, invalidNumberLabel?: string, outOfRangeLabel?: string\n) => ({\n decrement: decrementLabel ?? 'Decrease value',\n increment: incrementLabel ?? 'Increase value',\n invalid: invalidNumberLabel ?? 'Enter a valid number',\n range: outOfRangeLabel ?? 'Enter a number within the allowed range'\n})\n\nconst numberOptions = (locale?: string, max?: string, min?: string, step?: string): LumenDecimalOptions => ({\n ...(locale === undefined ? {} : { locale }),\n ...(max === undefined ? {} : { max }),\n ...(min === undefined ? {} : { min }),\n ...(step === undefined ? {} : { step })\n})\n\nconst numberValidation = (\n draft: LumenDecimalDraft, inBounds: boolean, labels: ReturnType<typeof numberLabels>\n): string | undefined => {\n if (draft.kind === 'empty') return undefined\n\n if (draft.kind !== 'valid') return labels.invalid\n\n return inBounds ? undefined : labels.range\n}\n\nexport const LumenNumberField = ({\n decrementLabel, description, enabled, errorMessage,\n incrementLabel, invalidNumberLabel,\n label, locale, max, min, onValueChange, outOfRangeLabel,\n readOnly, ref, showStepper = true, step, value, ...props\n}: LumenNumberFieldProps): ReactElement => {\n const theme = useLumenTheme()\n const options = numberOptions(locale, max, min, step)\n const labels = numberLabels(decrementLabel, incrementLabel, invalidNumberLabel, outOfRangeLabel)\n const draft = parseLumenDecimalDraft(value, locale)\n const inBounds = isLumenDecimalInBounds(value, options)\n const validation = numberValidation(draft, inBounds, labels)\n const editable = isEditable(enabled, readOnly)\n const message = errorMessage ?? validation\n const decrease = stepLumenDecimalDraft(value, -1, options)\n const increase = stepLumenDecimalDraft(value, 1, options)\n\n const canStep = (proposal: string | null): boolean => {\n if (proposal === null) return false\n\n if (draft.kind !== 'valid') return true\n\n const next = parseLumenDecimalDraft(proposal, locale)\n\n return next.kind === 'valid' &&\n next.coefficient * 10n ** BigInt(draft.scale) !== draft.coefficient * 10n ** BigInt(next.scale)\n }\n\n return (\n <LumenFieldGroup {...fieldGroupProps(label, description, message)}>\n <LumenTextField\n {...(ref ? { ref } : {})}\n {...props}\n editable={editable}\n keyboardType=\"decimal-pad\"\n onChangeText={proposal => {\n if (editable) onValueChange(proposal)\n }}\n value={value}\n />\n {showStepper ?\n (\n <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: theme.spacing.sm }}>\n <LumenButton\n disabled={!editable || !canStep(decrease)}\n intent=\"quiet\"\n accessibilityLabel={labels.decrement}\n onPress={() => {\n if (editable && decrease !== null) onValueChange(decrease)\n }}\n >\n {labels.decrement}\n </LumenButton>\n <LumenButton\n disabled={!editable || !canStep(increase)}\n intent=\"quiet\"\n accessibilityLabel={labels.increment}\n onPress={() => {\n if (editable && increase !== null) onValueChange(increase)\n }}\n >\n {labels.increment}\n </LumenButton>\n </View>\n ) :\n null}\n </LumenFieldGroup>\n )\n}\n\nexport interface LumenAutocompleteOption {\n description?: string\n disabled?: boolean\n label: string\n value: string\n}\n\nexport interface LumenAutocompleteProps extends Omit<ControlledFieldProps, 'onValueChange' | 'value'> {\n dismissLabel?: string\n emptyLabel?: string\n loading?: boolean\n loadingLabel?: string\n onQueryChange: (query: string) => void\n onRetry?: () => void\n onValueChange: (value: string) => void\n options: readonly LumenAutocompleteOption[]\n query: string\n resultsErrorMessage?: string\n retryLabel?: string\n value?: string\n}\n\nconst defaultCopy = (value: string | undefined, fallback: string): string => value ?? fallback\n\ninterface AutocompleteLabels { dismiss: string, empty: string, loading: string, retry: string }\n\nconst autocompleteStatus = (loading: boolean | undefined, error: string | undefined, count: number,\n labels: AutocompleteLabels): string | undefined => {\n if (loading) return labels.loading\n\n if (error !== undefined) return error\n\n return count === 0 ? labels.empty : undefined\n}\n\nconst LumenAutocompleteControl = ({\n description, dismissLabel, emptyLabel, enabled,\n errorMessage, label, loading, loadingLabel, onFocus,\n onQueryChange, onRetry, onValueChange, options, query, readOnly, ref,\n resultsErrorMessage, retryLabel, value, ...props\n}: LumenAutocompleteProps): ReactElement => {\n const theme = useLumenTheme()\n const [expanded, setExpanded] = useState(false)\n const editable = isEditable(enabled, readOnly)\n\n if (new Set(options.map(option => option.value)).size !== options.length) throw new RangeError('Autocomplete option values must be unique.')\n\n const labels = {\n dismiss: defaultCopy(dismissLabel, 'Close results'),\n empty: defaultCopy(emptyLabel, 'No results'),\n loading: defaultCopy(loadingLabel, 'Loading results'),\n retry: defaultCopy(retryLabel, 'Retry')\n }\n\n const status = autocompleteStatus(loading, resultsErrorMessage, options.length, labels)\n\n return (\n <LumenFieldGroup {...fieldGroupProps(label, description, errorMessage)}>\n <LumenTextField\n {...(ref ? { ref } : {})}\n {...props}\n accessibilityRole=\"combobox\"\n accessibilityState={{ expanded }}\n editable={editable}\n onChangeText={proposal => {\n if (editable) {\n onQueryChange(proposal)\n\n setExpanded(true)\n }\n }}\n onFocus={event => {\n if (editable) setExpanded(true)\n\n onFocus?.(event)\n }}\n value={query}\n />\n {expanded && editable ?\n (\n <View style={{ gap: theme.spacing.sm }}>\n {status ? <LumenText accessibilityLiveRegion=\"polite\">{status}</LumenText> : null}\n {!loading && resultsErrorMessage && onRetry ?\n (\n <LumenButton\n intent=\"quiet\"\n accessibilityLabel={labels.retry}\n onPress={() => {\n onRetry()\n }}\n >\n {labels.retry}\n </LumenButton>\n ) :\n null}\n {!status ?\n (\n <ScrollView keyboardShouldPersistTaps=\"handled\" style={{ maxHeight: 240 }}>\n {options.map(option => (\n <LumenButton\n accessibilityHint={option.description}\n accessibilityState={{ selected: option.value === value }}\n disabled={option.disabled}\n intent=\"quiet\"\n key={option.value}\n accessibilityLabel={option.label}\n onPress={() => {\n if (option.disabled) return\n\n onQueryChange(option.label)\n\n onValueChange(option.value)\n\n setExpanded(false)\n }}\n >\n {option.label}\n </LumenButton>\n ))}\n </ScrollView>\n ) :\n null}\n <LumenButton\n intent=\"quiet\"\n accessibilityLabel={labels.dismiss}\n onPress={() => {\n setExpanded(false)\n }}\n >\n {labels.dismiss}\n </LumenButton>\n </View>\n ) :\n null}\n </LumenFieldGroup>\n )\n}\n\nexport const LumenAutocomplete = (props: LumenAutocompleteProps): ReactElement => (\n <LumenAutocompleteControl key={`${props.enabled ?? true}-${props.readOnly ?? false}`} {...props} />\n)\n",
60167
60386
  "packages/react-native/src/agenda-components.test.tsx": "import { act, createElement, type ReactElement } from 'react'\n\nimport { createRoot, type Root, type TestInstance } from 'test-renderer'\nimport { afterEach, expect, test, vi } from 'vitest'\n\nimport { LumenAgenda, type LumenAgendaProps } from './agenda-components.js'\n\nObject.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })\nvi.mock('react-native', () => ({\n View: (props: Record<string, unknown>): ReactElement => createElement('View', props),\n ScrollView: (props: Record<string, unknown>): ReactElement => createElement('ScrollView', props),\n Pressable: (props: Record<string, unknown>): ReactElement => createElement('Pressable', props)\n}))\nvi.mock('./primitives.js', () => ({ LumenText: (props: Record<string, unknown>): ReactElement => createElement('Text', props) }))\nvi.mock('./theme-context.js', () => ({ useLumenTheme: () => ({ colors: { line: '#eee' }, spacing: { sm: 12 } }) }))\nconst roots: Root[] = []\nafterEach(() => {\n act(() => {\n roots.forEach(root => {\n root.unmount()\n })\n })\n roots.length = 0\n})\nconst render = (props: LumenAgendaProps): Root => {\n const root = createRoot()\n roots.push(root)\n act(() => {\n root.render(<LumenAgenda {...props} />)\n })\n return root\n}\nconst read = (instance: TestInstance, key: string): unknown => (instance.props as Record<string, unknown>)[key]\nconst press = (instance: TestInstance): void => {\n const callback = read(instance, 'onPress')\n if (typeof callback !== 'function') throw new Error('Missing press')\n act(() => {\n Reflect.apply(callback, undefined, [])\n })\n}\nconst find = (root: Root, label: string): TestInstance => {\n const match = root.container.queryAll(instance => instance.type === 'Pressable' && read(instance, 'accessibilityLabel') === label)[0]\n if (!match) throw new Error(`Missing ${label}`)\n return match\n}\nconst props = (): LumenAgendaProps => ({ label: 'Agenda', selectedDay: { year: 2026, month: 3, day: 8 }, dayCount: 1, onSelectedDayChange: vi.fn(), onEventPress: vi.fn(), events: [{ event: { id: 'review', label: 'Review', startDay: { year: 2026, month: 3, day: 8 } }, startMinute: 600, endMinute: 660 }] })\ntest('controlled navigation and event activation emit host values without changing input', () => {\n const input = props()\n const root = render(input)\n press(find(root, 'Next days'))\n expect(input.onSelectedDayChange).toHaveBeenCalledWith({ year: 2026, month: 3, day: 9 })\n press(find(root, '2026-03-08, 10:00 – 11:00, Review'))\n expect(input.onEventPress).toHaveBeenCalledWith(input.events[0], input.selectedDay)\n expect(input.selectedDay.day).toBe(8)\n})\ntest('disabled and read-only block navigation and event actions including direct handlers', () => {\n for (const state of [{ disabled: true }, { readOnly: true }]) {\n const input = props()\n const root = render({ ...input, ...state })\n press(find(root, 'Next days'))\n press(find(root, '2026-03-08, 10:00 – 11:00, Review'))\n expect(input.onSelectedDayChange).not.toHaveBeenCalled()\n expect(input.onEventPress).not.toHaveBeenCalled()\n }\n})\ntest('loading error invalid data hide stale actions; empty ranges keep navigation', () => {\n for (const state of [{ loading: true }, { error: 'Unavailable' }, { dayCount: 32 }, { events: [...props().events, ...props().events] }]) {\n const input = props()\n const root = render({ ...input, ...state })\n expect(root.container.queryAll(instance => instance.type === 'Pressable')).toHaveLength(0)\n }\n const root = render({ ...props(), events: [] })\n expect(root.container.queryAll(instance => instance.type === 'Pressable')).toHaveLength(2)\n})\n",
60168
60387
  "packages/react-native/src/agenda-components.tsx": "import type { ReactElement } from 'react'\nimport { Pressable, View, type ViewProps } from 'react-native'\n\nimport { formatLumenAgendaMinute, type LumenAgendaEvent, lumenAgendaGroups } from './agenda-recipes.js'\nimport { addLumenCalendarDays, type LumenCalendarDay, lumenCalendarDayKey } from './calendar-recipes.js'\nimport { LumenText } from './primitives.js'\nimport { useLumenTheme } from './theme-context.js'\n\nexport interface LumenAgendaProps extends Pick<ViewProps, 'style' | 'testID' | 'onLayout'> {\n label: string\n selectedDay: LumenCalendarDay\n onSelectedDayChange: (day: LumenCalendarDay) => void\n events: readonly LumenAgendaEvent[]\n dayCount?: number\n onEventPress?: (event: LumenAgendaEvent, day: LumenCalendarDay) => void\n disabled?: boolean\n readOnly?: boolean\n loading?: boolean\n error?: string | null\n loadingLabel?: string\n emptyLabel?: string\n invalidLabel?: string\n previousLabel?: string\n nextLabel?: string\n allDayLabel?: string\n formatDay?: (day: LumenCalendarDay) => string\n formatTime?: (minute: number) => string\n}\n\nconst agendaStatus = (props: LumenAgendaProps, invalid: boolean): string | null => {\n if (props.error != null) return props.error\n\n if (props.loading) return props.loadingLabel ?? 'Loading'\n\n if (invalid) return props.invalidLabel ?? 'Invalid agenda'\n\n return null\n}\n\nconst navigationDestination = (day: LumenCalendarDay, amount: number, count: number): LumenCalendarDay | null => {\n const next = addLumenCalendarDays(day, amount)\n\n return next && addLumenCalendarDays(next, count - 1) ? next : null\n}\n\ninterface AgendaRowProps {\n input: LumenAgendaEvent\n day: LumenCalendarDay\n time: string\n dayLabel: string\n locked: boolean\n onEventPress: LumenAgendaProps['onEventPress']\n}\n\nconst AgendaRow = ({ input, day, time, dayLabel, locked, onEventPress }: AgendaRowProps): ReactElement => {\n const theme = useLumenTheme()\n const unavailable = locked || input.event.disabled === true\n const name = [dayLabel, time, input.event.label, input.event.detail].filter(Boolean).join(', ')\n\n const content = (\n <>\n <LumenText>{input.event.label}</LumenText>\n <LumenText>{time}</LumenText>\n {input.event.detail && <LumenText>{input.event.detail}</LumenText>}\n </>\n )\n\n const style = { minHeight: 44, paddingVertical: theme.spacing.sm }\n\n if (!onEventPress) return <View accessible accessibilityLabel={name} style={style}>{content}</View>\n\n return (\n <Pressable\n accessibilityRole=\"button\"\n accessibilityLabel={name}\n accessibilityState={{ disabled: unavailable }}\n disabled={unavailable}\n style={style}\n onPress={() => {\n if (!unavailable) onEventPress(input, day)\n }}\n >\n {content}\n </Pressable>\n )\n}\n\nexport const LumenAgenda = (input: LumenAgendaProps): ReactElement => {\n const props = {\n dayCount: 7,\n formatDay: lumenCalendarDayKey,\n formatTime: formatLumenAgendaMinute,\n allDayLabel: 'All day',\n emptyLabel: 'No events',\n previousLabel: 'Previous days',\n nextLabel: 'Next days',\n ...input\n }\n\n const { label, selectedDay, onSelectedDayChange, events, dayCount, formatDay, formatTime } = props\n const theme = useLumenTheme()\n const groups = lumenAgendaGroups(events, selectedDay, dayCount)\n const status = agendaStatus(props, groups.length === 0)\n const locked = props.disabled === true || props.readOnly === true || status !== null\n\n const actions = [\n { day: navigationDestination(selectedDay, -dayCount, dayCount), label: props.previousLabel, id: 'previous' },\n { day: navigationDestination(selectedDay, dayCount, dayCount), label: props.nextLabel, id: 'next' }\n ]\n\n return (\n <View style={props.style} testID={props.testID} onLayout={props.onLayout} accessibilityLabel={label}>\n <LumenText accessibilityRole=\"header\">{label}</LumenText>\n {status !== null ?\n <LumenText accessibilityLiveRegion=\"polite\">{status}</LumenText> :\n (\n <>\n <View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>\n {actions.map(action => (\n <Pressable\n key={action.id}\n accessibilityRole=\"button\"\n accessibilityLabel={action.label}\n accessibilityState={{ disabled: locked || !action.day }}\n disabled={locked || !action.day}\n onPress={() => {\n if (!locked && action.day) onSelectedDayChange(action.day)\n }}\n style={{ minWidth: 44, minHeight: 44, padding: theme.spacing.sm, justifyContent: 'center', flexShrink: 1 }}\n >\n <LumenText>{action.label}</LumenText>\n </Pressable>\n ))}\n </View>\n {groups.map(group => (\n <View\n key={lumenCalendarDayKey(group.day)}\n style={{ padding: theme.spacing.sm, borderBottomWidth: 1, borderColor: theme.colors.line }}\n >\n <LumenText accessibilityRole=\"header\">{formatDay(group.day)}</LumenText>\n {group.segments.length === 0 && <LumenText>{props.emptyLabel}</LumenText>}\n {group.segments.map(segment => (\n <AgendaRow\n key={segment.event.event.id}\n input={segment.event}\n day={group.day}\n dayLabel={formatDay(group.day)}\n locked={locked}\n onEventPress={props.onEventPress}\n time={segment.startMinute === null ?\n props.allDayLabel :\n `${formatTime(segment.startMinute)} – ${formatTime(segment.endMinute ?? 1440)}`}\n />\n ))}\n </View>\n ))}\n </>\n )}\n </View>\n )\n}\n",
@@ -60194,7 +60413,7 @@
60194
60413
  "packages/react-native/src/command-recipes.test.ts": "import { expect, test } from 'vitest'\n\nimport { isLumenCommandGroupsValid, type LumenCommandGroup, lumenCommandGroups, moveLumenCommandActive, resolveLumenCommandActive } from './command-recipes.js'\nconst groups: readonly LumenCommandGroup[] = [\n { id: 'navigation',\n label: 'Navigation',\n items: [\n { id: 'docs', label: 'Documentation', detail: 'Read guides', keywords: ['manual'], shortcut: 'Ctrl+D' },\n { id: 'blocked', label: 'Disabled', disabled: true }\n ] },\n { id: 'actions', label: 'Actions', items: [{ id: 'theme', label: 'Toggle theme' }] }\n]\ntest('case-insensitive substring filtering retains groups/order and matches host synonyms', () => {\n for (const query of [' DOCUMENT ', 'guides', 'manual', 'ctrl+d']) {\n expect(lumenCommandGroups(groups, query)?.map(group => group.items.map(item => item.id))).toEqual([['docs']])\n }\n expect(lumenCommandGroups(groups, 'missing')).toEqual([])\n expect(groups[0]?.items).toHaveLength(2)\n})\ntest('navigation wraps enabled commands, boundaries work, stale/disabled IDs cannot activate', () => {\n expect(moveLumenCommandActive(groups, '', null, 'next')).toBe('docs')\n expect(moveLumenCommandActive(groups, '', null, 'previous')).toBe('theme')\n expect(moveLumenCommandActive(groups, '', 'docs', 'next')).toBe('theme')\n expect(moveLumenCommandActive(groups, '', 'theme', 'next')).toBe('docs')\n expect(moveLumenCommandActive(groups, '', 'docs', 'previous')).toBe('theme')\n expect(moveLumenCommandActive(groups, '', 'docs', 'last')).toBe('theme')\n expect(moveLumenCommandActive(groups, '', 'theme', 'first')).toBe('docs')\n expect(resolveLumenCommandActive(groups, 'theme', 'docs')).toBeNull()\n expect(resolveLumenCommandActive(groups, '', 'blocked')).toBeNull()\n expect(resolveLumenCommandActive(groups, '', 'unknown')).toBeNull()\n expect(moveLumenCommandActive(groups, 'Disabled', null, 'next')).toBeNull()\n})\ntest('identity is validated before filtering and handles large literal queries without regex', () => {\n for (const input of [[...groups, groups[0] ?? { id: 'navigation', label: '', items: [] }], [{ id: ' ', label: 'Blank', items: [] }], [{ id: 'a', label: 'A', items: [{ id: 'x', label: 'X' }] }, { id: 'b', label: 'B', items: [{ id: 'x', label: 'Duplicate' }] }]]) {\n expect(isLumenCommandGroupsValid(input)).toBe(false)\n expect(lumenCommandGroups(input, 'missing')).toBeNull()\n }\n expect(lumenCommandGroups(groups, '('.repeat(100000))).toEqual([])\n expect(lumenCommandGroups([], '')).toEqual([])\n})\n\ntest('rejects decoded groups, nested items and searchable fields without throwing', () => {\n const malformed: unknown[] = [null,\n 42,\n [],\n {},\n { id: null, label: '', items: [] },\n { id: 'g', label: null, items: [] },\n { id: 'g', label: '', items: null },\n ...[null,\n 42,\n {},\n { id: null, label: '' },\n { id: 'a', label: 42 },\n { id: 'a', label: '', detail: 42 },\n { id: 'a', label: '', shortcut: {} },\n { id: 'a', label: '', keywords: 'word' },\n { id: 'a', label: '', keywords: [null] },\n { id: 'a', label: '', keywords: new Array<unknown>(1) }]\n .map(item => ({ id: 'g', label: '', items: [item] }))]\n\n for (const input of [...malformed.map(group => [group]), null, {}, 'groups', new Array<unknown>(1)]) {\n expect(Reflect.apply(isLumenCommandGroupsValid, undefined, [input])).toBe(false)\n expect(Reflect.apply(lumenCommandGroups, undefined, [input, 'a'])).toBeNull()\n }\n})\n\ntest('rejects decoded non-string queries throughout filtering and keyboard helpers', () => {\n for (const query of [null, {}, 7, ['docs']]) {\n expect(Reflect.apply(lumenCommandGroups, undefined, [groups, query])).toBeNull()\n expect(Reflect.apply(resolveLumenCommandActive, undefined, [groups, query, 'docs'])).toBeNull()\n expect(Reflect.apply(moveLumenCommandActive, undefined, [groups, query, 'docs', 'next'])).toBeNull()\n }\n})\n",
60195
60414
  "packages/react-native/src/command-recipes.ts": "export interface LumenCommandItem {\n id: string\n label: string\n detail?: string\n keywords?: readonly string[]\n shortcut?: string\n disabled?: boolean\n}\nexport interface LumenCommandGroup { id: string, label: string, items: readonly LumenCommandItem[] }\nexport type LumenCommandNavigation = 'next' | 'previous' | 'first' | 'last'\n\nconst isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null && !Array.isArray(value)\nconst optionalBoolean = (value: unknown): boolean => value === undefined || typeof value === 'boolean'\nconst optionalString = (value: unknown): boolean => value === undefined || typeof value === 'string'\n\nconst validKeywords = (value: unknown): boolean => {\n if (value === undefined) return true\n\n if (!Array.isArray(value)) return false\n\n for (const keyword of value as readonly unknown[]) if (typeof keyword !== 'string') return false\n\n return true\n}\n\nconst validCommand = (value: unknown): value is LumenCommandItem => {\n if (!isRecord(value) || typeof value.id !== 'string' || typeof value.label !== 'string') return false\n\n return optionalString(value.detail) && optionalString(value.shortcut) &&\n optionalBoolean(value.disabled) && validKeywords(value.keywords)\n}\n\nconst validGroup = (value: unknown): value is LumenCommandGroup => {\n if (!isRecord(value) || typeof value.id !== 'string' || typeof value.label !== 'string' || !Array.isArray(value.items)) return false\n\n for (const item of value.items as readonly unknown[]) if (!validCommand(item)) return false\n\n return true\n}\n\nexport const isLumenCommandGroupsValid = (groups: readonly LumenCommandGroup[]): boolean => {\n if (!Array.isArray(groups)) return false\n\n const groupIds = new Set<string>()\n const itemIds = new Set<string>()\n\n for (const group of groups as readonly unknown[]) {\n if (!validGroup(group) || group.id.trim().length === 0 || groupIds.has(group.id)) return false\n\n groupIds.add(group.id)\n\n for (const item of group.items) {\n if (item.id.trim().length === 0 || itemIds.has(item.id)) return false\n\n itemIds.add(item.id)\n }\n }\n\n return true\n}\n\nconst commandMatches = (item: LumenCommandItem, query: string): boolean => [item.label || item.id, item.detail ?? '', item.shortcut ?? '', ...(item.keywords ?? [])]\n .some(text => text.toLowerCase().includes(query))\n\nexport const lumenCommandGroups = (\n groups: readonly LumenCommandGroup[], query: string\n): readonly LumenCommandGroup[] | null => {\n if (typeof query !== 'string' || !isLumenCommandGroupsValid(groups)) return null\n\n const search = query.trim().toLowerCase()\n\n return groups.map(group => ({ ...group, items: group.items.filter(item => commandMatches(item, search)) }))\n .filter(group => group.items.length > 0)\n}\nexport const resolveLumenCommandActive = (\n groups: readonly LumenCommandGroup[], query: string, activeId: string | null\n): LumenCommandItem | null => {\n const filtered = lumenCommandGroups(groups, query)\n\n if (!filtered || activeId === null) return null\n\n return filtered.flatMap(group => group.items).find(item => item.id === activeId && !item.disabled) ?? null\n}\n\nconst enabledCommands = (groups: readonly LumenCommandGroup[], query: string): readonly LumenCommandItem[] => {\n const filtered = lumenCommandGroups(groups, query)\n\n if (!filtered) return []\n\n return filtered.flatMap(group => group.items).filter(item => !item.disabled)\n}\n\nconst boundaryCommand = (items: readonly LumenCommandItem[], direction: LumenCommandNavigation): string | null => {\n const item = direction === 'next' || direction === 'first' ? items[0] : items.at(-1)\n\n return item?.id ?? null\n}\n\nexport const moveLumenCommandActive = (\n groups: readonly LumenCommandGroup[], query: string, activeId: string | null, direction: LumenCommandNavigation\n): string | null => {\n const items = enabledCommands(groups, query)\n\n if (items.length === 0) return null\n\n if (direction === 'first') return boundaryCommand(items, direction)\n\n if (direction === 'last') return boundaryCommand(items, direction)\n\n const index = items.findIndex(item => item.id === activeId)\n\n if (index < 0) return boundaryCommand(items, direction)\n\n const delta = direction === 'next' ? 1 : -1\n\n return items[(index + delta + items.length) % items.length]?.id ?? null\n}\n",
60196
60415
  "packages/react-native/src/comparison-components.tsx": "import { type ReactElement, useState } from 'react'\nimport { I18nManager, type ImageSourcePropType, View, type ViewProps, type ViewStyle } from 'react-native'\n\nimport { formatLumenImageComparisonValue, type LumenImageComparisonMode, normalizeLumenImageComparisonMode } from '@santi020k/lumen-core'\n\nimport { LumenText } from './foundation-primitives.js'\nimport { LumenImage, type LumenImageFit } from './media-components.js'\nimport { useLumenTheme } from './theme-context.js'\nimport { LumenSlider } from './value-components.js'\n\nexport interface LumenImageComparisonProps extends Omit<ViewProps, 'children'> {\n after: ImageSourcePropType\n afterLabel?: string\n aspectRatio?: number\n before: ImageSourcePropType\n beforeLabel?: string\n enabled?: boolean\n fit?: LumenImageFit\n label: string\n locale?: string\n mode?: LumenImageComparisonMode\n onValueChange: (value: number) => void\n value: number\n}\n\nconst comparisonPosition = (value: number): number => Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : 0.5\n\nconst comparisonRatio = (ratio: number): number => {\n if (!Number.isFinite(ratio) || ratio < 0.1 || ratio > 10) return 16 / 9\n\n return ratio\n}\n\nconst comparisonEdge = (rtl: boolean): Pick<ViewStyle, 'left' | 'right'> => rtl ? { right: 0 } : { left: 0 }\n\nconst ComparisonAlternatePreview = ({ mode, ratio, before, after, beforeLabel, afterLabel, label, fit }: {\n mode: LumenImageComparisonMode\n ratio: number\n before: ImageSourcePropType\n after: ImageSourcePropType\n beforeLabel: string\n afterLabel: string\n label: string\n fit: LumenImageFit\n}) => {\n const theme = useLumenTheme()\n\n return (\n <View style={{ gap: theme.spacing.sm }}>\n <LumenText>{label}</LumenText>\n <View style={{ flexDirection: mode === 'side-by-side' ? 'row' : 'column', gap: theme.spacing.sm }}>\n {mode !== 'after' && (\n <View style={{ flex: 1, minWidth: 0, gap: theme.spacing.xs }}>\n <LumenImage decorative fit={fit} radius=\"lg\" source={before} aspectRatio={ratio} />\n <LumenText>{beforeLabel}</LumenText>\n </View>\n )}\n {mode !== 'before' && (\n <View style={{ flex: 1, minWidth: 0, gap: theme.spacing.xs }}>\n <LumenImage decorative fit={fit} radius=\"lg\" source={after} aspectRatio={ratio} />\n <LumenText>{afterLabel}</LumenText>\n </View>\n )}\n </View>\n </View>\n )\n}\n\nexport const LumenImageComparison = ({\n after, afterLabel = 'After', aspectRatio = 16 / 9, before, beforeLabel = 'Before',\n enabled = true, fit = 'cover', label, locale, mode: requestedMode, onValueChange, style, value, ...props\n}: LumenImageComparisonProps): ReactElement => {\n const mode = normalizeLumenImageComparisonMode(requestedMode)\n const theme = useLumenTheme()\n const [width, setWidth] = useState(0)\n const position = comparisonPosition(value)\n const ratio = comparisonRatio(aspectRatio)\n const percentage = formatLumenImageComparisonValue(position * 100, '', locale).trim()\n const rtl = I18nManager.isRTL\n\n return (\n <View {...props} style={[{ gap: theme.spacing.sm }, style]}>\n {mode !== 'reveal' ?\n (\n <ComparisonAlternatePreview\n mode={mode}\n ratio={ratio}\n before={before}\n after={after}\n beforeLabel={beforeLabel}\n afterLabel={afterLabel}\n label={label}\n fit={fit}\n />\n ) :\n (\n <>\n <View\n onLayout={event => {\n setWidth(event.nativeEvent.layout.width)\n }}\n style={{ aspectRatio: ratio, borderRadius: theme.radii.lg, overflow: 'hidden', width: '100%' }}\n >\n <LumenImage decorative fit={fit} radius=\"none\" source={before} style={{ height: '100%', position: 'absolute' }} />\n <View style={{ bottom: 0, ...comparisonEdge(rtl), overflow: 'hidden', position: 'absolute', top: 0, width: `${position * 100}%` }}>\n <LumenImage decorative fit={fit} radius=\"none\" source={after} style={{ height: '100%', ...comparisonEdge(rtl), position: 'absolute', width }} />\n </View>\n <View pointerEvents=\"none\" style={{ backgroundColor: theme.colors.ink, bottom: 0, left: `${(rtl ? 1 - position : position) * 100}%`, position: 'absolute', top: 0, width: theme.spacing.xs }} />\n </View>\n <View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>\n <LumenText>{afterLabel}</LumenText>\n <LumenText>{beforeLabel}</LumenText>\n </View>\n <LumenSlider enabled={enabled} label={label} max={1} min={0} onValueChange={onValueChange} step={0.01} value={position} valueLabel={`${afterLabel} ${percentage}`} />\n </>\n )}\n </View>\n )\n}\n",
60197
- "packages/react-native/src/component-behavior.test.tsx": "// cspell:words Espacios Configuración Cambios guardados Registro actualizado Otro\nimport { act, type ComponentRef, createRef, type ReactElement, type Ref, StrictMode, useImperativeHandle, useState } from 'react'\nimport type { View } from 'react-native'\n\nimport { DateTimePickerAndroid } from '@react-native-community/datetimepicker'\nimport { getLumenPhoneCountry } from '@santi020k/lumen-core'\nimport type { LumenMediaViewportValue } from '@santi020k/lumen-core/media-workspace'\nimport { createRoot, type Root, type TestInstance } from 'test-renderer'\nimport { afterEach, describe, expect, test, vi } from 'vitest'\n\nimport { LumenBrandGithubIconGraphic } from './static-icons/brand-github.generated.js'\nimport { LumenSearchIconGraphic } from './static-icons/search.generated.js'\nimport { LumenButtonGroup, LumenChip, LumenFieldGroup, LumenTextarea, LumenToast } from './additional-components.js'\nimport { LumenAutocomplete, LumenInputOTP, LumenNumberField, LumenPasswordField } from './advanced-form-components.js'\nimport { LumenBreadcrumb } from './breadcrumb-components.js'\nimport { LumenBarChart, LumenComboChart, LumenHeatmap, LumenHistogram, LumenLineChart, LumenRangeChart, LumenScatterChart, LumenWaterfallChart } from './chart-components.js'\nimport { LumenImageComparison } from './comparison-components.js'\nimport { LumenDateField, LumenDateRangeField } from './datetime-components.js'\nimport { LumenSearchField, LumenToggle } from './form-components.js'\nimport { LumenIcon as GraphicIcon, LumenIconButton as GraphicIconButton, type LumenIconGraphicProps } from './graphics.js'\nimport { type LumenToastHookController, type LumenToastHookOptions, useToast } from './hooks.js'\nimport { LumenMediaThumbnail, LumenMediaViewport } from './media-workspace-components.js'\nimport { LumenMultiSelect } from './multi-select-components.js'\nimport { LumenAlertDialog, LumenMenu, LumenSheet } from './overlay-components.js'\nimport { LumenPhoneInput } from './phone-components.js'\nimport { resolveLumenPhoneInputValue } from './phone-recipes.js'\nimport { LumenNavigationBar } from './platform-components.js'\nimport { LumenButton, LumenIcon, LumenIconButton, LumenText, LumenTextField } from './primitives.js'\nimport { LumenProvider } from './provider.js'\nimport { LumenRating } from './rating-components.js'\nimport { resolveLumenRating } from './rating-recipes.js'\nimport { LumenCheckbox, LumenTabs } from './selection-components.js'\nimport { LumenStepper } from './stepper-components.js'\nimport { resolveLumenStepState } from './stepper-recipes.js'\nimport { LumenBanner, LumenErrorState, LumenStatusBar } from './structured-components.js'\nimport { LumenDataTable } from './table-components.js'\nimport type { LumenTableRow } from './table-recipes.js'\nimport { createLumenTheme } from './theme.js'\nimport { LumenTimeField, type LumenTimeFieldProps } from './time-components.js'\nimport { LumenTimeline, LumenTimelineItem } from './timeline-components.js'\nimport { LumenPicker, LumenRangeSlider, LumenSlider } from './value-components.js'\n\nObject.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })\n\nconst nativeDirection = vi.hoisted(() => ({ isRTL: false }))\nconst nativePlatform = vi.hoisted(() => ({ OS: 'ios' }))\nconst nativeMotion = vi.hoisted(() => ({ enabled: false }))\nconst accessibilityFocus = vi.hoisted(() => vi.fn())\nconst accessibilityAnnouncement = vi.hoisted(() => vi.fn<(message: string, options: { queue: boolean }) => void>())\ntype RecommendedTimeout = (duration: number) => Promise<number>\nconst accessibilityTimeout = vi.hoisted(() => vi.fn<RecommendedTimeout>(duration => Promise.resolve(duration)))\nconst nativeWindow = vi.hoisted(() => ({ fontScale: 1, height: 800, scale: 2, width: 400 }))\nconst safeAreaInsets = vi.hoisted(() => ({\n bottom: 34,\n left: 8,\n right: 12,\n top: 20\n}))\n\nvi.mock('react-native', async () => {\n const { createElement, isValidElement, useImperativeHandle } = await import('react')\n const hostComponent = (name: string) => (\n props: Record<string, unknown>\n ): ReactElement => createElement(name, props)\n type FocusablePressableProps = Record<string, unknown> & {\n ref?: Ref<{ focus: () => void }>\n }\n const FocusablePressable = ({ ref, ...props }: FocusablePressableProps): ReactElement => {\n useImperativeHandle(ref, () => ({ focus: vi.fn() }))\n\n return createElement('Pressable', props)\n }\n\n return {\n AccessibilityInfo: {\n addEventListener: () => ({ remove: vi.fn() }),\n announceForAccessibilityWithOptions: accessibilityAnnouncement,\n getRecommendedTimeoutMillis: accessibilityTimeout,\n isReduceMotionEnabled: () => Promise.resolve(nativeMotion.enabled),\n sendAccessibilityEvent: accessibilityFocus\n },\n ActivityIndicator: hostComponent('ActivityIndicator'),\n FlatList: (props: Record<string, unknown>): ReactElement => {\n const renderItem = props.renderItem\n const data: readonly unknown[] = Array.isArray(props.data) ? props.data : []\n const children = data.map((item, index) => {\n if (typeof renderItem !== 'function') return null\n\n const rendered: unknown = Reflect.apply(renderItem, undefined, [{ item, index }])\n\n return isValidElement(rendered) ? rendered : null\n })\n\n return createElement('FlatList', props, children)\n },\n I18nManager: nativeDirection,\n Image: hostComponent('Image'),\n KeyboardAvoidingView: hostComponent('KeyboardAvoidingView'),\n Modal: hostComponent('Modal'),\n Platform: nativePlatform,\n Pressable: FocusablePressable,\n ScrollView: hostComponent('ScrollView'),\n Share: { share: () => Promise.resolve({ action: 'sharedAction' }) },\n Switch: hostComponent('Switch'),\n Text: hostComponent('Text'),\n TextInput: hostComponent('TextInput'),\n useColorScheme: () => 'light',\n useWindowDimensions: () => nativeWindow,\n View: hostComponent('View')\n }\n})\n\nvi.mock('@react-native-community/datetimepicker', async () => {\n const { createElement } = await import('react')\n const NativeDatePicker = (props: Record<string, unknown>): ReactElement => (\n createElement('NativeDatePicker', props)\n )\n\n return {\n DateTimePickerAndroid: { dismiss: vi.fn(), open: vi.fn() },\n default: NativeDatePicker\n }\n})\n\nvi.mock('react-native-svg', async () => {\n const { createElement } = await import('react')\n const hostComponent = (name: string) => (\n props: Record<string, unknown>\n ): ReactElement => createElement(name, props)\n\n return Object.fromEntries(\n ['Circle', 'Defs', 'Ellipse', 'G', 'Line', 'LinearGradient', 'Path', 'Polygon', 'Polyline', 'Rect', 'Stop', 'Svg', 'Text']\n .map(name => [name, hostComponent(name)])\n )\n})\n\nconst mountedRoots: Root[] = []\n\ninterface ToastProbeProps {\n ref: Ref<LumenToastHookController>\n options?: LumenToastHookOptions\n}\n\nconst ToastProbe = ({ ref, options }: ToastProbeProps): ReactElement | null => {\n const controller = useToast(options)\n\n useImperativeHandle(ref, () => controller, [controller])\n\n return null\n}\n\nconst readToastController = (ref: { current: LumenToastHookController | null }): LumenToastHookController => {\n if (!ref.current) throw new Error('Toast controller is not mounted')\n\n return ref.current\n}\n\nconst deferredTimeout = () => {\n let complete: (duration: number) => void = () => {\n throw new Error('Missing timeout resolver')\n }\n const promise = new Promise<number>(resolve => {\n complete = resolve\n })\n\n return { promise, resolve: complete }\n}\n\nconst renderNative = async (element: ReactElement): Promise<Root> => {\n const root = createRoot({\n publicTextComponentTypes: ['Text'],\n textComponentTypes: ['RCTText', 'Text']\n })\n\n mountedRoots.push(root)\n\n await act(async () => {\n root.render(<LumenProvider scheme=\"light\">{element}</LumenProvider>)\n await Promise.resolve()\n })\n\n return root\n}\n\nconst readProp = (instance: TestInstance, property: string): unknown => {\n const props = instance.props as Record<string, unknown>\n\n return props[property]\n}\n\nconst callAction = (value: unknown, missingActionMessage: string): void => {\n if (typeof value !== 'function') throw new Error(missingActionMessage)\n\n Reflect.apply(value, undefined, [])\n}\n\nconst callKeyboardAction = (value: unknown, key: string): ReturnType<typeof vi.fn> => {\n if (typeof value !== 'function') throw new Error('Tab is missing its keyboard action.')\n\n const preventDefault = vi.fn()\n\n Reflect.apply(value, undefined, [{\n nativeEvent: { code: key, key },\n preventDefault\n }])\n\n return preventDefault\n}\n\nconst findByAccessibilityRole = (root: Root, role: string): TestInstance => {\n const matches = root.container.queryAll(\n instance => readProp(instance, 'accessibilityRole') === role\n )\n\n expect(matches).toHaveLength(1)\n\n const match = matches[0]\n\n if (!match) throw new Error(`Expected one ${role} accessibility node.`)\n\n return match\n}\n\nconst findByAccessibilityLabel = (root: Root, label: string): TestInstance => {\n const matches = root.container.queryAll(\n instance => readProp(instance, 'accessibilityLabel') === label\n )\n\n expect(matches).toHaveLength(1)\n\n const match = matches[0]\n\n if (!match) throw new Error(`Expected one accessibility node labeled ${label}.`)\n\n return match\n}\n\nconst findHostComponent = (root: Root, type: string): TestInstance => {\n const matches = root.container.queryAll(instance => instance.type === type)\n\n expect(matches).toHaveLength(1)\n\n const match = matches[0]\n\n if (!match) throw new Error(`Expected one ${type} host component.`)\n\n return match\n}\n\nafterEach(async () => {\n nativeDirection.isRTL = false\n const roots = mountedRoots.splice(0)\n\n await act(async () => {\n for (const root of roots) root.unmount()\n await Promise.resolve()\n })\n nativeWindow.fontScale = 1\n nativePlatform.OS = 'ios'\n accessibilityAnnouncement.mockClear()\n accessibilityTimeout.mockReset().mockImplementation(duration => Promise.resolve(duration))\n vi.useRealTimers()\n vi.unstubAllGlobals()\n})\n\ndescribe('Lumen React Native component behavior', () => {\n test.each(['bar', 'line', 'scatter', 'range', 'combo'] as const)(\n '%s chart fits its container and resizes without removing data', async kind => {\n const data = [3, 7, 4, 8, 5].map((y, x) => ({ x, y }))\n const series = [{ id: 'activity', label: 'Changes', data }]\n const props = { label: 'Weekly activity', showData: false, series }\n const charts = {\n bar: <LumenBarChart {...props} />,\n line: <LumenLineChart {...props} />,\n scatter: <LumenScatterChart {...props} />,\n range: <LumenRangeChart label=\"Weekly range\" showData={false} data={data.map(({ x, y }) => ({ x, low: y, high: y + 2 }))} />,\n combo: <LumenComboChart {...props} series={[{ id: 'activity', label: 'Changes', data, mark: 'bar' }]} />\n }\n const root = await renderNative(charts[kind])\n const layouts = root.container.queryAll(instance => instance.type === 'View' && typeof readProp(instance, 'onLayout') === 'function')\n expect(layouts).toHaveLength(1)\n const layout = layouts[0]\n if (!layout) throw new Error('Missing chart container')\n const handler = readProp(layout, 'onLayout')\n if (typeof handler !== 'function') throw new Error('Missing chart layout handler')\n\n for (const width of [268, 720, 300, 0, Number.NaN]) {\n await act(async () => {\n Reflect.apply(handler, undefined, [{ nativeEvent: { layout: { width } } }])\n await Promise.resolve()\n })\n const expectedWidth = width > 0 && Number.isFinite(width) ? width : 300\n const svg = findHostComponent(root, 'Svg')\n expect(readProp(svg, 'width')).toBe(expectedWidth)\n expect(readProp(svg, 'height')).toBe(Math.min(320, Math.max(220, expectedWidth * 0.75)))\n expect(root.container.queryAll(instance => instance.type === 'ScrollView')).toHaveLength(0)\n const bars = root.container.queryAll(instance => instance.type === 'Rect')\n expect(bars).toHaveLength(['bar', 'combo'].includes(kind) ? 5 : 0)\n for (const bar of bars) {\n const coordinates = /translate\\(([-\\d.]+) ([-\\d.]+)\\)/u.exec(String(readProp(bar, 'transform')))\n if (!coordinates?.[1]) throw new Error('Missing bar coordinates')\n expect(Number(coordinates[1])).toBeGreaterThanOrEqual(0)\n expect(Number(coordinates[1]) + Number(readProp(bar, 'width'))).toBeLessThanOrEqual(expectedWidth)\n }\n }\n }\n )\n\n test.each([\n { rtl: false, values: [20, 50, 120, 20, 120] },\n { rtl: true, values: [120, 100, 20, 120, 20] }\n ])('slider touch follows RTL=$rtl while accessibility values remain numeric', async ({ rtl, values }) => {\n nativeDirection.isRTL = rtl\n const onValueChange = vi.fn<(value: number) => void>()\n const root = await renderNative(\n <LumenSlider label=\"Volume\" min={20} max={120} step={10} value={70} onValueChange={onValueChange} />\n )\n const slider = findByAccessibilityRole(root, 'adjustable')\n expect(readProp(slider, 'accessible')).toBe(true)\n const dispatch = async (property: string, nativeEvent: unknown): Promise<void> => {\n const handler = readProp(slider, property)\n if (typeof handler !== 'function') throw new Error(`Missing slider ${property} callback`)\n await act(async () => {\n Reflect.apply(handler, undefined, [{ nativeEvent }])\n await Promise.resolve()\n })\n }\n await dispatch('onLayout', { layout: { width: 200 } })\n for (const [index, position] of [0, 50, 200, -50, 250].entries()) {\n for (const event of ['onResponderGrant', 'onResponderMove']) {\n await dispatch(event, { locationX: position })\n expect(onValueChange).toHaveBeenLastCalledWith(values[index])\n }\n }\n await dispatch('onAccessibilityAction', { actionName: 'increment' })\n expect(onValueChange).toHaveBeenLastCalledWith(80)\n await dispatch('onAccessibilityAction', { actionName: 'decrement' })\n expect(onValueChange).toHaveBeenLastCalledWith(60)\n expect(readProp(slider, 'accessibilityValue')).toMatchObject({ min: 20, max: 120, now: 70 })\n })\n\n test('range endpoints stay controlled, independently named and cannot cross', async () => {\n const onValueChange = vi.fn<(value: readonly [number, number]) => void>()\n const root = await renderNative(\n <LumenRangeSlider\n label=\"Capacity\"\n startLabel=\"Lower\"\n endLabel=\"Upper\"\n value={[20, 60]}\n min={0}\n max={100}\n step={10}\n onValueChange={onValueChange}\n formatValue={value => `${value}%`}\n />\n )\n const sliders = root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'adjustable')\n expect(sliders).toHaveLength(2)\n const [start, end] = sliders\n if (!start || !end) throw new Error('Missing interval endpoints')\n expect(readProp(start, 'accessibilityLabel')).toBe('Capacity · Lower')\n expect(readProp(end, 'accessibilityLabel')).toBe('Capacity · Upper')\n expect(readProp(start, 'accessibilityValue')).toMatchObject({ now: 20, text: '20%' })\n expect(readProp(start, 'aria-valuenow')).toBe(20)\n expect(readProp(start, 'aria-valuetext')).toBe('20%')\n const move = async (slider: TestInstance, direction: string): Promise<void> => {\n const handler = readProp(slider, 'onAccessibilityAction')\n if (typeof handler !== 'function') throw new Error('Missing range adjustment')\n await act(async () => {\n Reflect.apply(handler, undefined, [{ nativeEvent: { actionName: direction } }])\n await Promise.resolve()\n })\n }\n await move(start, 'increment')\n expect(onValueChange).toHaveBeenLastCalledWith([30, 60])\n await move(end, 'decrement')\n expect(onValueChange).toHaveBeenLastCalledWith([20, 50])\n // The application has not accepted either update; subsequent intent uses its controlled value.\n await move(start, 'increment')\n expect(onValueChange).toHaveBeenLastCalledWith([30, 60])\n await act(async () => {\n root.render(\n <LumenProvider>\n <LumenRangeSlider\n label=\"Capacity\"\n value={[60, 60]}\n min={0}\n max={100}\n step={10}\n onValueChange={onValueChange}\n />\n </LumenProvider>\n )\n await Promise.resolve()\n })\n const next = root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'adjustable')\n if (!next[0] || !next[1]) throw new Error('Missing updated interval endpoints')\n await move(next[0], 'increment')\n expect(onValueChange).toHaveBeenLastCalledWith([60, 60])\n await move(next[1], 'decrement')\n expect(onValueChange).toHaveBeenLastCalledWith([60, 60])\n })\n\n test.each([{ enabled: false, readOnly: false }, { enabled: true, readOnly: true }])(\n 'range endpoints reject touch and accessibility when enabled=$enabled readOnly=$readOnly',\n async ({ enabled, readOnly }) => {\n const onValueChange = vi.fn<(value: readonly [number, number]) => void>()\n const root = await renderNative(\n <LumenRangeSlider\n label=\"Capacity\"\n value={[20, 80]}\n enabled={enabled}\n readOnly={readOnly}\n onValueChange={onValueChange}\n />\n )\n const sliders = root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'adjustable')\n for (const slider of sliders) {\n expect(readProp(slider, 'accessibilityState')).toEqual({ disabled: true })\n for (const [event, nativeEvent] of [\n ['onAccessibilityAction', { actionName: 'increment' }],\n ['onResponderGrant', { locationX: 100 }],\n ['onResponderMove', { locationX: 100 }]\n ] as const) {\n const handler = readProp(slider, event)\n if (typeof handler !== 'function') throw new Error(`Missing ${event}`)\n await act(async () => {\n Reflect.apply(handler, undefined, [{ nativeEvent }])\n await Promise.resolve()\n })\n }\n }\n expect(onValueChange).not.toHaveBeenCalled()\n }\n )\n\n test('uses the Android accessibility timeout before dismissing a toast', async () => {\n nativePlatform.OS = 'android'\n vi.useFakeTimers()\n accessibilityTimeout.mockResolvedValue(20_000)\n const ref = createRef<LumenToastHookController>()\n\n await renderNative(<ToastProbe ref={ref} />)\n await act(async () => {\n readToastController(ref).create({ title: 'Saved' })\n await Promise.resolve()\n })\n\n expect(accessibilityTimeout).toHaveBeenCalledExactlyOnceWith(5000)\n\n await act(async () => {\n await vi.advanceTimersByTimeAsync(19_999)\n })\n expect(readToastController(ref).toasts).toHaveLength(1)\n\n await act(async () => {\n await vi.advanceTimersByTimeAsync(1)\n })\n expect(readToastController(ref).toasts).toHaveLength(0)\n })\n\n test.each(['shorter', 'invalid', 'oversized', 'rejected'])('retains the requested duration when Android returns a %s recommendation', async result => {\n nativePlatform.OS = 'android'\n vi.useFakeTimers()\n\n if (result === 'rejected') accessibilityTimeout.mockRejectedValue(new Error('Native timeout unavailable'))\n else if (result === 'shorter') accessibilityTimeout.mockResolvedValue(1)\n else if (result === 'oversized') accessibilityTimeout.mockResolvedValue(Number.MAX_SAFE_INTEGER)\n else accessibilityTimeout.mockResolvedValue(Number.NaN)\n\n const ref = createRef<LumenToastHookController>()\n\n await renderNative(<ToastProbe ref={ref} />)\n await act(async () => {\n readToastController(ref).create({ duration: 1000, title: 'Saved' })\n await Promise.resolve()\n })\n expect(accessibilityTimeout).toHaveBeenCalledExactlyOnceWith(1000)\n\n await act(async () => {\n await vi.advanceTimersByTimeAsync(999)\n })\n expect(readToastController(ref).toasts).toHaveLength(1)\n\n await act(async () => {\n await vi.advanceTimersByTimeAsync(1)\n })\n expect(readToastController(ref).toasts).toHaveLength(0)\n })\n\n test.each(['dismiss', 'clear', 'unmount', 'evict'])('ignores late Android timeout results after %s', async operation => {\n nativePlatform.OS = 'android'\n vi.useFakeTimers()\n const pending = deferredTimeout()\n\n accessibilityTimeout.mockReturnValueOnce(pending.promise)\n\n const ref = createRef<LumenToastHookController>()\n const root = await renderNative(<ToastProbe options={{ maxCount: 1 }} ref={ref} />)\n let id = ''\n\n await act(async () => {\n id = readToastController(ref).create({ title: 'Saved' })\n await Promise.resolve()\n })\n expect(accessibilityTimeout).toHaveBeenCalledExactlyOnceWith(5000)\n\n await act(async () => {\n if (operation === 'dismiss') readToastController(ref).dismiss(id)\n if (operation === 'clear') readToastController(ref).clear()\n if (operation === 'unmount') root.unmount()\n if (operation === 'evict') readToastController(ref).create({ duration: 0, title: 'Persistent replacement' })\n await Promise.resolve()\n })\n await act(async () => {\n pending.resolve(20_000)\n await Promise.resolve()\n })\n\n expect(vi.getTimerCount()).toBe(0)\n })\n\n test('ignores stale Android recommendations when a toast duration is updated', async () => {\n nativePlatform.OS = 'android'\n vi.useFakeTimers()\n const pending = deferredTimeout()\n\n accessibilityTimeout.mockReturnValueOnce(pending.promise).mockResolvedValueOnce(10_000)\n\n const ref = createRef<LumenToastHookController>()\n\n await renderNative(<ToastProbe ref={ref} />)\n await act(async () => {\n const id = readToastController(ref).create({ title: 'Saved' })\n\n readToastController(ref).update(id, { duration: 1000 })\n await Promise.resolve()\n })\n expect(accessibilityTimeout).toHaveBeenNthCalledWith(1, 5000)\n expect(accessibilityTimeout).toHaveBeenNthCalledWith(2, 1000)\n expect(accessibilityTimeout).toHaveBeenCalledTimes(2)\n\n await act(async () => {\n pending.resolve(100_000)\n await Promise.resolve()\n await vi.advanceTimersByTimeAsync(10_000)\n })\n\n expect(readToastController(ref).toasts).toHaveLength(0)\n expect(vi.getTimerCount()).toBe(0)\n })\n\n test('keeps explicitly persistent Android toasts without requesting a timeout', async () => {\n nativePlatform.OS = 'android'\n vi.useFakeTimers()\n const ref = createRef<LumenToastHookController>()\n\n await renderNative(<ToastProbe ref={ref} />)\n await act(async () => {\n for (const duration of [0, -1, Number.POSITIVE_INFINITY]) readToastController(ref).create({ duration, title: 'Saved' })\n await vi.advanceTimersByTimeAsync(100_000)\n })\n\n expect(readToastController(ref).toasts).toHaveLength(3)\n expect(accessibilityTimeout).not.toHaveBeenCalled()\n expect(vi.getTimerCount()).toBe(0)\n })\n\n test.each(['ios', 'web'])('preserves the requested %s toast duration without Android timeout calls', async platform => {\n nativePlatform.OS = platform\n vi.useFakeTimers()\n const ref = createRef<LumenToastHookController>()\n\n await renderNative(<ToastProbe ref={ref} />)\n await act(async () => {\n readToastController(ref).create({ duration: 1000, title: 'Saved' })\n await vi.advanceTimersByTimeAsync(1000)\n })\n\n expect(readToastController(ref).toasts).toHaveLength(0)\n expect(accessibilityTimeout).not.toHaveBeenCalled()\n })\n\n test('keeps enlarged navigation labels readable and preserves destination semantics', async () => {\n const select = vi.fn()\n const reselect = vi.fn()\n const items = [\n { label: 'Espacios de trabajo', value: 'workspaces' },\n { label: 'Configuración', value: 'settings' },\n { disabled: true, label: 'Archivo', value: 'archive' }\n ]\n const navigation = <LumenNavigationBar items={items} onReselect={reselect} onValueChange={select} value=\"workspaces\" />\n const root = await renderNative(navigation)\n const labels = () => root.container.queryAll(instance => instance.type === 'Text')\n expect(labels().map(label => readProp(label, 'numberOfLines'))).toEqual([1, 1, 1])\n\n nativeWindow.fontScale = 3\n await act(async () => {\n root.render(<LumenProvider scheme=\"light\"><LumenNavigationBar items={items} onReselect={reselect} onValueChange={select} value=\"workspaces\" /></LumenProvider>)\n await Promise.resolve()\n })\n expect(labels().every(label => readProp(label, 'numberOfLines') === undefined)).toBe(true)\n expect(labels().every(label => {\n const style = readProp(label, 'style')\n return typeof style === 'object' && style !== null && 'maxWidth' in style && style.maxWidth === '100%'\n })).toBe(true)\n const selected = findByAccessibilityLabel(root, 'Espacios de trabajo')\n expect(readProp(selected, 'accessibilityState')).toMatchObject({ selected: true })\n callAction(readProp(selected, 'onPress'), 'Missing reselect action')\n expect(reselect).toHaveBeenCalledWith('workspaces')\n const settings = findByAccessibilityLabel(root, 'Configuración')\n callAction(readProp(settings, 'onPress'), 'Missing destination action')\n expect(select).toHaveBeenCalledWith('settings')\n expect(readProp(findByAccessibilityLabel(root, 'Archivo'), 'disabled')).toBe(true)\n })\n\n test('keeps alert actions inside safe areas and makes oversized content scrollable', async () => {\n const root = await renderNative(\n <LumenAlertDialog\n confirmLabel=\"Delete\"\n onConfirm={() => {}}\n onDismiss={() => {}}\n safeAreaInsets={safeAreaInsets}\n title=\"Delete project?\"\n visible\n />\n )\n const modalContainer = root.container.queryAll(\n instance => readProp(instance, 'accessibilityViewIsModal') === true\n )[0]\n\n if (!modalContainer) throw new Error('Expected the alert modal container.')\n\n expect(readProp(modalContainer, 'style')).toMatchObject({\n paddingBottom: 58,\n paddingLeft: 32,\n paddingRight: 36,\n paddingTop: 44\n })\n expect(readProp(findByAccessibilityRole(root, 'alert'), 'style')).toMatchObject({\n maxHeight: '100%'\n })\n })\n\n test('keeps sheet actions above the bottom inset and scrolls application content', async () => {\n const root = await renderNative(\n <LumenSheet\n actions={<LumenButton>Save</LumenButton>}\n onDismiss={() => {}}\n safeAreaInsets={safeAreaInsets}\n scrollable\n title=\"Settings\"\n visible\n >\n <LumenText>Sheet content</LumenText>\n </LumenSheet>\n )\n const keyboardSurface = root.container.queryAll(\n instance => instance.type === 'KeyboardAvoidingView'\n )[0]\n const sheetPanel = root.container.queryAll(instance => {\n const style = readProp(instance, 'style')\n\n return typeof style === 'object' &&\n style !== null &&\n 'paddingBottom' in style &&\n style.paddingBottom === 34\n })[0]\n\n if (!keyboardSurface || !sheetPanel) throw new Error('Expected the sheet surfaces.')\n\n expect(readProp(keyboardSurface, 'style')).toMatchObject({\n paddingBottom: 0,\n paddingLeft: 8,\n paddingRight: 12,\n paddingTop: 20\n })\n expect(readProp(sheetPanel, 'style')).toMatchObject({ paddingBottom: 34 })\n expect(root.container.queryAll(instance => instance.type === 'ScrollView')).toHaveLength(1)\n })\n\n test('lets virtualized sheet content own scrolling', async () => {\n const root = await renderNative(\n <LumenSheet onDismiss={() => {}} scrollable={false} visible>\n <LumenText>Virtualized content</LumenText>\n </LumenSheet>\n )\n\n expect(root.container.queryAll(instance => instance.type === 'ScrollView')).toHaveLength(0)\n })\n\n test('button exposes loading as busy and disabled native state', async () => {\n const root = await renderNative(<LumenButton loading>Save changes</LumenButton>)\n const button = findByAccessibilityRole(root, 'button')\n\n expect(readProp(button, 'disabled')).toBe(true)\n expect(readProp(button, 'accessibilityState')).toEqual({\n busy: true,\n disabled: true\n })\n })\n\n test('button preserves its native role when a consumer supplies another role', async () => {\n const root = await renderNative(\n <LumenButton accessibilityRole=\"link\">Open workspace</LumenButton>\n )\n\n expect(findByAccessibilityRole(root, 'button')).toBeDefined()\n expect(root.container.queryAll(\n instance => readProp(instance, 'accessibilityRole') === 'link'\n )).toHaveLength(0)\n })\n\n test('text inputs expose disabled native state without dropping caller state', async () => {\n const textFieldRoot = await renderNative(\n <LumenTextField\n accessibilityLabel=\"Project name\"\n accessibilityState={{ busy: true }}\n editable={false}\n />\n )\n const textareaRoot = await renderNative(\n <LumenTextarea\n accessibilityState={{ busy: true }}\n editable={false}\n label=\"Project notes\"\n onChangeText={() => {}}\n value=\"\"\n />\n )\n const searchRoot = await renderNative(\n <LumenSearchField\n accessibilityState={{ busy: true }}\n editable={false}\n onChangeText={() => {}}\n prompt=\"Search projects\"\n value=\"\"\n />\n )\n\n for (const [root, label] of [\n [textFieldRoot, 'Project name'],\n [textareaRoot, 'Project notes'],\n [searchRoot, 'Search projects']\n ] as const) {\n expect(readProp(findByAccessibilityLabel(root, label), 'accessibilityState')).toEqual({\n busy: true,\n disabled: true\n })\n }\n })\n\n test('validation is exposed on the native controls without dropping caller context', async () => {\n const textFieldRoot = await renderNative(\n <LumenTextField accessibilityLabel=\"Project name\" error />\n )\n const textareaRoot = await renderNative(\n <LumenTextarea\n accessibilityHint=\"Consumer guidance\"\n errorMessage=\"Project notes are required\"\n label=\"Project notes\"\n onChangeText={() => {}}\n value=\"\"\n />\n )\n const dateRoot = await renderNative(\n <LumenDateField\n errorMessage=\"Choose a valid birthday\"\n label=\"Birthday\"\n onValueChange={() => {}}\n value={null}\n />\n )\n\n const textField = findByAccessibilityLabel(textFieldRoot, 'Project name')\n const textarea = findByAccessibilityLabel(textareaRoot, 'Project notes')\n const dateButton = findByAccessibilityLabel(dateRoot, 'Birthday, Select a date')\n\n expect(readProp(textField, 'aria-invalid')).toBe(true)\n expect(readProp(textarea, 'aria-invalid')).toBe(true)\n expect(readProp(textarea, 'accessibilityHint')).toBe('Project notes are required')\n expect(readProp(dateButton, 'aria-invalid')).toBe(true)\n expect(readProp(dateButton, 'accessibilityHint')).toBe('Choose a valid birthday')\n })\n\n test('grouped fields expose required and range validation context', async () => {\n const fieldGroupRoot = await renderNative(\n <LumenFieldGroup label=\"Project details\" required>\n <LumenText>Details</LumenText>\n </LumenFieldGroup>\n )\n const dateRangeRoot = await renderNative(\n <LumenDateRangeField\n accessibilityHint=\"Consumer guidance\"\n errorMessage=\"Choose a valid schedule\"\n label=\"Schedule\"\n onValueChange={() => {}}\n value={{ end: null, start: null }}\n />\n )\n\n expect(findByAccessibilityLabel(fieldGroupRoot, 'Project details, required')).toBeDefined()\n\n const dateRange = findByAccessibilityLabel(dateRangeRoot, 'Schedule')\n\n expect(readProp(dateRange, 'aria-invalid')).toBe(true)\n expect(readProp(dateRange, 'accessibilityHint')).toBe('Choose a valid schedule')\n })\n\n test('localizes required-field descriptions without exposing them on optional fields', async () => {\n const root = await renderNative(\n <>\n <LumenFieldGroup label=\"Nombre\" required requiredLabel=\"obligatorio\"><LumenTextField /></LumenFieldGroup>\n <LumenFieldGroup label=\"Notas\" requiredLabel=\"obligatorio\"><LumenTextField /></LumenFieldGroup>\n </>\n )\n\n expect(findByAccessibilityLabel(root, 'Nombre, obligatorio')).toBeDefined()\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityLabel') === 'Notas, obligatorio')).toHaveLength(0)\n expect(findByAccessibilityLabel(root, 'Notas')).toBeDefined()\n })\n\n test('field groups provide inherited native and web relationships without overriding inputs', async () => {\n const root = await renderNative(\n <LumenFieldGroup\n description=\"Shown on the public profile\"\n errorMessage=\"Choose a unique name\"\n label=\"Project name\"\n required\n >\n <LumenText>Nested row content</LumenText>\n <LumenTextField />\n <LumenTextField accessibilityLabel=\"Short project name\" />\n </LumenFieldGroup>\n )\n const inputs = root.container.queryAll(instance => instance.type === 'TextInput')\n const inherited = inputs[0]\n const explicit = inputs[1]\n\n if (!inherited || !explicit) throw new Error('Expected two grouped text fields.')\n\n expect(readProp(inherited, 'accessibilityLabel')).toBe('Project name')\n expect(readProp(inherited, 'aria-labelledby')).toMatch(/-label$/u)\n expect(readProp(inherited, 'aria-describedby')).toMatch(/-description .*?-error$/u)\n expect(readProp(inherited, 'aria-required')).toBe(true)\n expect(readProp(inherited, 'aria-invalid')).toBe(true)\n expect(readProp(explicit, 'accessibilityLabel')).toBe('Short project name')\n expect(readProp(explicit, 'aria-labelledby')).toBeUndefined()\n\n const validationMessages = root.container.queryAll(\n instance => readProp(instance, 'children') === 'Choose a unique name'\n )\n\n expect(validationMessages).toHaveLength(1)\n\n const validationMessage = validationMessages[0]\n\n if (!validationMessage) throw new Error('Expected the grouped validation message.')\n\n expect(readProp(validationMessage, 'accessibilityLiveRegion')).toBe('polite')\n expect(readProp(validationMessage, 'accessibilityRole')).toBe('alert')\n })\n\n test('read-only phone input locks country selection and number editing', async () => {\n const country = getLumenPhoneCountry('CO')\n if (!country) throw new Error('Missing phone fixture')\n const root = await renderNative(<LumenPhoneInput label=\"Phone\" onValueChange={() => {}} readOnly value={resolveLumenPhoneInputValue([country], country, '', {})} />)\n expect(readProp(findByAccessibilityLabel(root, 'Country code, Colombia, +57'), 'disabled')).toBe(true)\n expect(readProp(findByAccessibilityLabel(root, 'Phone number'), 'editable')).toBe(false)\n })\n\n test('phone input exposes disabled state on both native controls', async () => {\n const country = getLumenPhoneCountry('US')\n\n expect(country).toBeDefined()\n\n if (!country) return\n\n const value = resolveLumenPhoneInputValue([country], country, '', {})\n const root = await renderNative(\n <LumenPhoneInput\n countries={[country]}\n enabled={false}\n label=\"Phone\"\n onValueChange={() => {}}\n value={value}\n />\n )\n const countrySelector = findByAccessibilityLabel(root, 'Country code, United States, +1')\n const numberInput = findByAccessibilityLabel(root, 'Phone number')\n\n expect(readProp(countrySelector, 'accessibilityState')).toEqual({\n disabled: true,\n expanded: false\n })\n expect(readProp(numberInput, 'accessibilityState')).toEqual({ disabled: true })\n })\n\n test('phone input exposes validation on the number editor', async () => {\n const country = getLumenPhoneCountry('US')\n\n expect(country).toBeDefined()\n\n if (!country) return\n\n const value = resolveLumenPhoneInputValue([country], country, '415', {})\n const root = await renderNative(\n <LumenPhoneInput\n countries={[country]}\n errorMessage=\"Enter a complete phone number\"\n label=\"Phone\"\n onValueChange={() => {}}\n value={value}\n />\n )\n const numberInput = findByAccessibilityLabel(root, 'Phone number')\n\n expect(readProp(numberInput, 'aria-invalid')).toBe(true)\n expect(readProp(numberInput, 'accessibilityHint')).toBe('Enter a complete phone number')\n })\n\n test('phone input exposes an empty country allow-list as a disabled selector', async () => {\n const country = getLumenPhoneCountry('US')\n\n expect(country).toBeDefined()\n\n if (!country) return\n\n const value = resolveLumenPhoneInputValue([country], country, '', {})\n const root = await renderNative(\n <LumenPhoneInput\n countries={[]}\n label=\"Phone\"\n onValueChange={() => {}}\n value={value}\n />\n )\n const countrySelector = findByAccessibilityLabel(root, 'Country code, United States, +1')\n\n expect(readProp(countrySelector, 'disabled')).toBe(true)\n expect(readProp(countrySelector, 'accessibilityState')).toEqual({\n disabled: true,\n expanded: false\n })\n })\n\n test('generic picker disables empty options and does not reopen after disabling', async () => {\n const onValueChange = vi.fn<(value: string) => void>()\n const root = await renderNative(\n <LumenPicker\n label=\"Workspace\"\n onValueChange={onValueChange}\n options={[]}\n value=\"none\"\n />\n )\n let trigger = findByAccessibilityLabel(root, 'Workspace')\n\n expect(readProp(trigger, 'disabled')).toBe(true)\n expect(readProp(trigger, 'accessibilityState')).toEqual({\n disabled: true,\n expanded: false\n })\n\n await act(async () => {\n root.render(\n <LumenProvider scheme=\"light\">\n <LumenPicker\n label=\"Workspace\"\n onValueChange={onValueChange}\n options={[{ label: 'Design', value: 'design' }]}\n value=\"design\"\n />\n </LumenProvider>\n )\n await Promise.resolve()\n })\n\n trigger = findByAccessibilityLabel(root, 'Workspace')\n\n await act(async () => {\n callAction(readProp(trigger, 'onPress'), 'Picker is missing its native press action.')\n await Promise.resolve()\n })\n\n expect(findByAccessibilityRole(root, 'menu')).toBeDefined()\n\n await act(async () => {\n root.render(\n <LumenProvider scheme=\"light\">\n <LumenPicker\n enabled={false}\n label=\"Workspace\"\n onValueChange={onValueChange}\n options={[{ label: 'Design', value: 'design' }]}\n value=\"design\"\n />\n </LumenProvider>\n )\n await Promise.resolve()\n })\n\n expect(root.container.queryAll(\n instance => readProp(instance, 'accessibilityRole') === 'menu'\n )).toHaveLength(0)\n\n await act(async () => {\n root.render(\n <LumenProvider scheme=\"light\">\n <LumenPicker\n label=\"Workspace\"\n onValueChange={onValueChange}\n options={[{ label: 'Design', value: 'design' }]}\n value=\"design\"\n />\n </LumenProvider>\n )\n await Promise.resolve()\n })\n\n expect(root.container.queryAll(\n instance => readProp(instance, 'accessibilityRole') === 'menu'\n )).toHaveLength(0)\n })\n\n test('date picker does not reopen after the field is disabled and re-enabled', async () => {\n const root = await renderNative(\n <LumenDateField label=\"Birthday\" onValueChange={() => {}} value={null} />\n )\n const trigger = findByAccessibilityLabel(root, 'Birthday, Select a date')\n\n await act(async () => {\n callAction(readProp(trigger, 'onPress'), 'Date field is missing its native press action.')\n await Promise.resolve()\n })\n\n expect(findHostComponent(root, 'NativeDatePicker')).toBeDefined()\n\n await act(async () => {\n root.render(\n <LumenProvider scheme=\"light\">\n <LumenDateField enabled={false} label=\"Birthday\" onValueChange={() => {}} value={null} />\n </LumenProvider>\n )\n await Promise.resolve()\n })\n\n expect(root.container.queryAll(instance => instance.type === 'NativeDatePicker')).toHaveLength(0)\n\n await act(async () => {\n root.render(\n <LumenProvider scheme=\"light\">\n <LumenDateField label=\"Birthday\" onValueChange={() => {}} value={null} />\n </LumenProvider>\n )\n await Promise.resolve()\n })\n\n expect(root.container.queryAll(instance => instance.type === 'NativeDatePicker')).toHaveLength(0)\n })\n\n test('disabling an Android date field dismisses its imperative native dialog', async () => {\n nativePlatform.OS = 'android'\n\n try {\n const root = await renderNative(\n <LumenDateField label=\"Birthday\" onValueChange={() => {}} value={null} />\n )\n const trigger = findByAccessibilityLabel(root, 'Birthday, Select a date')\n\n await act(async () => {\n callAction(readProp(trigger, 'onPress'), 'Date field is missing its native press action.')\n await Promise.resolve()\n })\n\n expect(DateTimePickerAndroid.open).toHaveBeenCalledOnce()\n\n await act(async () => {\n root.render(\n <LumenProvider scheme=\"light\">\n <LumenDateField enabled={false} label=\"Birthday\" onValueChange={() => {}} value={null} />\n </LumenProvider>\n )\n await Promise.resolve()\n })\n\n expect(DateTimePickerAndroid.dismiss).toHaveBeenCalledWith('date')\n } finally {\n nativePlatform.OS = 'ios'\n }\n })\n\n test('phone picker closes and clears its query when the input becomes disabled', async () => {\n const country = getLumenPhoneCountry('US')\n\n expect(country).toBeDefined()\n\n if (!country) return\n\n const value = resolveLumenPhoneInputValue([country], country, '', {})\n const renderPhone = (enabled: boolean): ReactElement => (\n <LumenProvider scheme=\"light\">\n <LumenPhoneInput\n countries={[country]}\n enabled={enabled}\n label=\"Phone\"\n onValueChange={() => {}}\n value={value}\n />\n </LumenProvider>\n )\n const root = await renderNative(\n <LumenPhoneInput\n countries={[country]}\n label=\"Phone\"\n onValueChange={() => {}}\n value={value}\n />\n )\n const selector = findByAccessibilityLabel(root, 'Country code, United States, +1')\n\n await act(async () => {\n callAction(readProp(selector, 'onPress'), 'Country selector is missing its native press action.')\n await Promise.resolve()\n })\n\n expect(readProp(findHostComponent(root, 'Modal'), 'visible')).toBe(true)\n expect(readProp(findByAccessibilityLabel(root, 'Search countries'), 'editable')).toBe(true)\n\n await act(async () => {\n const onChangeText = readProp(findByAccessibilityLabel(root, 'Search countries'), 'onChangeText')\n\n if (typeof onChangeText !== 'function') throw new Error('Country search is missing its change action.')\n\n Reflect.apply(onChangeText, undefined, ['united'])\n await Promise.resolve()\n })\n\n await act(async () => {\n root.render(renderPhone(false))\n await Promise.resolve()\n })\n\n expect(readProp(findHostComponent(root, 'Modal'), 'visible')).toBe(false)\n expect(readProp(findByAccessibilityLabel(root, 'Country code, United States, +1'), 'accessibilityState'))\n .toEqual({ disabled: true, expanded: false })\n\n await act(async () => {\n root.render(renderPhone(true))\n await Promise.resolve()\n })\n\n expect(readProp(findByAccessibilityLabel(root, 'Country code, United States, +1'), 'accessibilityState'))\n .toEqual({ disabled: false, expanded: false })\n\n const reopenedSelector = findByAccessibilityLabel(root, 'Country code, United States, +1')\n\n await act(async () => {\n callAction(\n readProp(reopenedSelector, 'onPress'),\n 'Re-enabled country selector is missing its native press action.'\n )\n await Promise.resolve()\n })\n\n expect(readProp(findByAccessibilityLabel(root, 'Search countries'), 'value')).toBe('')\n })\n\n test('chip removal exposes the independent action disabled state', async () => {\n const root = await renderNative(\n <LumenChip disabled label=\"Design\" onRemove={() => {}} />\n )\n const removeButton = findByAccessibilityLabel(root, 'Remove Design')\n\n expect(readProp(removeButton, 'accessibilityRole')).toBe('button')\n expect(readProp(removeButton, 'disabled')).toBe(true)\n expect(readProp(removeButton, 'accessibilityState')).toEqual({ disabled: true })\n expect(readProp(removeButton, 'style')).toMatchObject({\n alignItems: 'center',\n justifyContent: 'center',\n minHeight: 24,\n minWidth: 24\n })\n })\n\n test('toast announces politely and keeps dismissal independently operable', async () => {\n const onDismiss = vi.fn<() => void>()\n const root = await renderNative(\n <LumenToast dismissLabel=\"Close update\" onDismiss={onDismiss} title=\"Changes saved\" />\n )\n const alert = findByAccessibilityRole(root, 'alert')\n const dismissButton = findByAccessibilityLabel(root, 'Close update')\n const style = readProp(dismissButton, 'style')\n\n expect(readProp(alert, 'accessibilityLiveRegion')).toBe('polite')\n expect(readProp(dismissButton, 'accessibilityRole')).toBe('button')\n expect(style).toBeTypeOf('function')\n\n if (typeof style !== 'function') throw new Error('Toast dismissal is missing its pressable style.')\n\n const restingStyle: unknown = Reflect.apply(style, undefined, [{ pressed: false }])\n\n expect(restingStyle).toMatchObject({\n alignItems: 'center',\n justifyContent: 'center',\n minHeight: 44,\n minWidth: 44\n })\n\n await act(async () => {\n callAction(readProp(dismissButton, 'onPress'), 'Toast dismissal is missing its press action.')\n await Promise.resolve()\n })\n\n expect(onDismiss).toHaveBeenCalledOnce()\n })\n\n test('queues localized iOS toast copy once and announces changed content', async () => {\n const toast = (description: string) => (\n <StrictMode><LumenToast description={description} title=\"Cambios guardados\" /></StrictMode>\n )\n const root = await renderNative(toast('Registro actualizado'))\n\n expect(accessibilityAnnouncement).toHaveBeenCalledExactlyOnceWith('Cambios guardados. Registro actualizado', { queue: true })\n\n await act(async () => {\n root.render(<LumenProvider scheme=\"light\">{toast('Registro actualizado')}</LumenProvider>)\n await Promise.resolve()\n })\n\n expect(accessibilityAnnouncement).toHaveBeenCalledOnce()\n\n await act(async () => {\n root.render(<LumenProvider scheme=\"light\">{toast('Otro registro actualizado')}</LumenProvider>)\n await Promise.resolve()\n })\n\n expect(accessibilityAnnouncement).toHaveBeenLastCalledWith('Cambios guardados. Otro registro actualizado', { queue: true })\n expect(accessibilityAnnouncement).toHaveBeenCalledTimes(2)\n })\n\n test('honors iOS error announcement urgency and off without speaking diagnostic references', async () => {\n const root = await renderNative(<LumenErrorState announcement=\"assertive\" description=\"Try again\" reference=\"request-123\" title=\"Unable to save\" />)\n\n expect(accessibilityAnnouncement).toHaveBeenCalledExactlyOnceWith('Unable to save. Try again', { queue: false })\n\n await act(async () => {\n root.render(<LumenProvider scheme=\"light\"><LumenErrorState announcement=\"off\" title=\"Unable to save\" /></LumenProvider>)\n await Promise.resolve()\n })\n\n expect(accessibilityAnnouncement).toHaveBeenCalledOnce()\n\n await act(async () => {\n root.render(<LumenProvider scheme=\"light\"><LumenErrorState announcement=\"polite\" title=\"Unable to save\" /></LumenProvider>)\n await Promise.resolve()\n })\n\n expect(accessibilityAnnouncement).toHaveBeenLastCalledWith('Unable to save', { queue: true })\n expect(accessibilityAnnouncement).toHaveBeenCalledTimes(2)\n })\n\n test('does not announce initially disabled errors or empty toast copy on iOS', async () => {\n await renderNative(\n <>\n <LumenErrorState announcement=\"off\" title=\"Unable to save\" />\n <LumenToast title=\" \" />\n </>\n )\n\n expect(accessibilityAnnouncement).not.toHaveBeenCalled()\n })\n\n test.each(['android', 'web'])('keeps %s live regions without duplicate imperative announcements', async platform => {\n nativePlatform.OS = platform\n const root = await renderNative(\n <>\n <LumenToast title=\"Saved\" />\n <LumenErrorState announcement=\"assertive\" title=\"Unable to save\" />\n </>\n )\n\n expect(readProp(findByAccessibilityRole(root, 'alert'), 'accessibilityLiveRegion')).toBe('polite')\n expect(readProp(findByAccessibilityRole(root, 'summary'), 'accessibilityLiveRegion')).toBe('assertive')\n expect(accessibilityAnnouncement).not.toHaveBeenCalled()\n })\n\n test('banner dismissal has a full independent touch target', async () => {\n const root = await renderNative(\n <LumenBanner dismissLabel=\"Close notice\" onDismiss={() => {}} title=\"Maintenance soon\" />\n )\n const dismissButton = findByAccessibilityLabel(root, 'Close notice')\n const style = readProp(dismissButton, 'style')\n\n expect(readProp(dismissButton, 'accessibilityRole')).toBe('button')\n expect(style).toBeTypeOf('function')\n\n if (typeof style !== 'function') throw new Error('Banner dismissal is missing its pressable style.')\n\n const restingStyle: unknown = Reflect.apply(style, undefined, [{ pressed: false }])\n\n expect(restingStyle).toMatchObject({\n alignItems: 'center',\n justifyContent: 'center',\n minHeight: 44,\n minWidth: 44\n })\n })\n\n test('status bars allow long messages to wrap without shrinking trailing content', async () => {\n const message = 'El análisis permanece en el dispositivo para proteger tu privacidad.'\n const root = await renderNative(\n <LumenProvider>\n <LumenStatusBar\n message={message}\n trailing={<LumenText>Analyze</LumenText>}\n />\n </LumenProvider>\n )\n const messageNodes = root.container.queryAll(\n instance => instance.type === 'Text' && readProp(instance, 'children') === message\n )\n\n expect(messageNodes).toHaveLength(1)\n\n const messageNode = messageNodes[0]\n\n if (!messageNode) throw new Error('Expected the status message fixture.')\n\n expect(readProp(messageNode, 'numberOfLines')).toBeUndefined()\n expect(readProp(messageNode, 'style')).toMatchObject({ flex: 1, flexShrink: 1 })\n\n const fixedTrailingContainers = root.container.queryAll(instance => {\n const style = readProp(instance, 'style')\n\n return instance.type === 'View' && typeof style === 'object' && style !== null &&\n Reflect.get(style, 'flexShrink') === 0\n })\n\n expect(fixedTrailingContainers).toHaveLength(1)\n })\n\n test('search clear action preserves its label and full touch target', async () => {\n const onChangeText = vi.fn<(value: string) => void>()\n const root = await renderNative(\n <LumenSearchField\n clearLabel=\"Clear query\"\n onChangeText={onChangeText}\n value=\"lumen\"\n />\n )\n const clearButton = findByAccessibilityLabel(root, 'Clear query')\n const style = readProp(clearButton, 'style')\n\n expect(readProp(clearButton, 'accessibilityRole')).toBe('button')\n expect(style).toBeTypeOf('function')\n\n if (typeof style !== 'function') throw new Error('Clear action is missing its pressable style.')\n\n const restingStyle: unknown = Reflect.apply(style, undefined, [{ pressed: false }])\n\n expect(restingStyle).toMatchObject({\n alignItems: 'center',\n justifyContent: 'center',\n minHeight: 44,\n minWidth: 44\n })\n\n await act(async () => {\n callAction(readProp(clearButton, 'onPress'), 'Clear action is missing its native press action.')\n await Promise.resolve()\n })\n\n expect(onChangeText).toHaveBeenCalledExactlyOnceWith('')\n })\n\n test('checkbox announces state and emits the next controlled value', async () => {\n const onCheckedChange = vi.fn<(checked: boolean) => void>()\n const root = await renderNative(\n <LumenCheckbox\n checked={false}\n label=\"Include diagnostics\"\n onCheckedChange={onCheckedChange}\n />\n )\n const checkbox = findByAccessibilityRole(root, 'checkbox')\n const onPress = readProp(checkbox, 'onPress')\n\n expect(readProp(checkbox, 'accessibilityLabel')).toBe('Include diagnostics')\n expect(readProp(checkbox, 'aria-checked')).toBe(false)\n expect(readProp(checkbox, 'aria-disabled')).toBe(false)\n\n expect(readProp(checkbox, 'accessibilityState')).toEqual({\n checked: false,\n disabled: false\n })\n expect(onPress).toBeTypeOf('function')\n\n await act(async () => {\n callAction(onPress, 'Checkbox is missing its native press action.')\n await Promise.resolve()\n })\n\n expect(onCheckedChange).toHaveBeenCalledExactlyOnceWith(true)\n })\n\n test('checkbox preserves a host-provided accessible name', async () => {\n const root = await renderNative(\n <LumenCheckbox checked={false} label=\"Diagnostics\" accessibilityLabel=\"Include diagnostic report\" onCheckedChange={() => undefined} />\n )\n\n expect(readProp(findByAccessibilityRole(root, 'checkbox'), 'accessibilityLabel')).toBe('Include diagnostic report')\n })\n\n test('toggle makes the full labeled row the single interactive switch', async () => {\n const onValueChange = vi.fn<(value: boolean) => void>()\n const root = await renderNative(\n <LumenToggle\n description=\"Receive product and security updates\"\n label=\"Automatic updates\"\n onValueChange={onValueChange}\n value={false}\n />\n )\n const toggle = findByAccessibilityRole(root, 'switch')\n const onPress = readProp(toggle, 'onPress')\n const nativeSwitches = root.container.queryAll(instance => instance.type === 'Switch')\n\n expect(readProp(toggle, 'accessibilityLabel')).toBe('Automatic updates')\n expect(readProp(toggle, 'accessibilityHint')).toBe('Receive product and security updates')\n expect(readProp(toggle, 'aria-checked')).toBe(false)\n expect(readProp(toggle, 'accessibilityState')).toEqual({\n checked: false,\n disabled: false\n })\n expect(nativeSwitches).toHaveLength(1)\n\n const nativeSwitch = nativeSwitches[0]\n\n if (!nativeSwitch) throw new Error('Expected the decorative native switch.')\n\n expect(readProp(nativeSwitch, 'accessibilityElementsHidden')).toBe(true)\n expect(readProp(nativeSwitch, 'importantForAccessibility')).toBe('no-hide-descendants')\n expect(readProp(nativeSwitch, 'pointerEvents')).toBe('none')\n expect(readProp(nativeSwitch, 'onValueChange')).toBeUndefined()\n\n await act(async () => {\n callAction(onPress, 'Toggle row is missing its press action.')\n await Promise.resolve()\n })\n\n expect(onValueChange).toHaveBeenCalledExactlyOnceWith(true)\n })\n\n test('web toggle keeps the decorative native input out of the tab order', async () => {\n nativePlatform.OS = 'web'\n const onValueChange = vi.fn()\n const root = await renderNative(<LumenToggle label=\"Updates\" onValueChange={onValueChange} value={false} />)\n const control = root.container.queryAll(instance => instance.type === 'Switch')[0]\n if (!control) throw new Error('Missing decorative switch')\n expect(readProp(control, 'disabled')).toBe(true)\n expect(readProp(control, 'aria-hidden')).toBe(true)\n const row = findByAccessibilityRole(root, 'switch')\n expect(readProp(row, 'disabled')).toBe(false)\n await act(async () => {\n callAction(readProp(row, 'onPress'), 'Missing row action')\n await Promise.resolve()\n })\n expect(onValueChange).toHaveBeenCalledExactlyOnceWith(true)\n nativePlatform.OS = 'ios'\n })\n\n test('toggle preserves a consumer accessibility hint over supporting copy', async () => {\n const root = await renderNative(\n <LumenToggle\n accessibilityHint=\"Changes how updates are installed\"\n description=\"Receive product and security updates\"\n label=\"Automatic updates\"\n onValueChange={() => {}}\n value={false}\n />\n )\n\n expect(readProp(findByAccessibilityRole(root, 'switch'), 'accessibilityHint'))\n .toBe('Changes how updates are installed')\n })\n\n test('disabled toggle row cannot emit a value change', async () => {\n const onValueChange = vi.fn<(value: boolean) => void>()\n const root = await renderNative(\n <LumenToggle\n disabled\n label=\"Automatic updates\"\n onValueChange={onValueChange}\n value\n />\n )\n const toggle = findByAccessibilityRole(root, 'switch')\n\n expect(readProp(toggle, 'accessibilityState')).toEqual({\n checked: true,\n disabled: true\n })\n expect(readProp(toggle, 'disabled')).toBe(true)\n\n await act(async () => {\n callAction(readProp(toggle, 'onPress'), 'Disabled toggle row is missing its guarded press action.')\n await Promise.resolve()\n })\n\n expect(onValueChange).not.toHaveBeenCalled()\n })\n\n test('tabs preserve selected and disabled semantics while emitting enabled selection', async () => {\n const onValueChange = vi.fn<(value: string) => void>()\n const root = await renderNative(\n <LumenTabs\n label=\"Workspace views\"\n onValueChange={onValueChange}\n options={[\n { label: 'Overview', value: 'overview' },\n { disabled: true, label: 'Activity', value: 'activity' }\n ]}\n value=\"overview\"\n >\n <LumenText>Current workspace health</LumenText>\n </LumenTabs>\n )\n const tabList = findByAccessibilityRole(root, 'tablist')\n const tabs = root.container.queryAll(\n instance => readProp(instance, 'accessibilityRole') === 'tab'\n )\n\n expect(readProp(tabList, 'accessibilityLabel')).toBe('Workspace views')\n expect(tabs).toHaveLength(2)\n const [overviewTab, activityTab] = tabs\n\n if (!overviewTab || !activityTab) throw new Error('Expected the two configured tabs.')\n\n expect(readProp(overviewTab, 'accessibilityState')).toEqual({\n disabled: false,\n selected: true\n })\n expect(readProp(activityTab, 'accessibilityState')).toEqual({\n disabled: true,\n selected: false\n })\n\n const enabledTabPress = readProp(overviewTab, 'onPress')\n\n await act(async () => {\n callAction(enabledTabPress, 'Enabled tab is missing its press action.')\n await Promise.resolve()\n })\n\n expect(onValueChange).toHaveBeenCalledExactlyOnceWith('overview')\n })\n\n test('uses the translated tab label by default and accepts an explicit translated panel name', async () => {\n const fixture = (panelAccessibilityLabel?: string): ReactElement => (\n <LumenTabs label=\"Vistas\" onValueChange={vi.fn()} options={[{ label: 'Resumen', value: 'overview' }]} {...(panelAccessibilityLabel ? { panelAccessibilityLabel } : {})} value=\"overview\">\n <LumenText>Contenido</LumenText>\n </LumenTabs>\n )\n const root = await renderNative(fixture())\n const panel = root.container.queryAll(instance => readProp(instance, 'role') === 'tabpanel')[0]\n if (!panel) throw new Error('Missing tab panel')\n expect(readProp(panel, 'accessibilityLabel')).toBe('Resumen')\n await act(async () => {\n root.render(<LumenProvider>{fixture('Contenido del resumen')}</LumenProvider>)\n await Promise.resolve()\n })\n const translatedPanel = root.container.queryAll(instance => readProp(instance, 'role') === 'tabpanel')[0]\n if (!translatedPanel) throw new Error('Missing translated tab panel')\n expect(readProp(translatedPanel, 'accessibilityLabel')).toBe('Contenido del resumen')\n })\n\n test('tabs support directional keyboard navigation without selecting disabled tabs', async () => {\n const onValueChange = vi.fn<(value: string) => void>()\n const KeyboardTabsFixture = (): ReactElement => {\n const [value, setValue] = useState('overview')\n\n return (\n <LumenTabs\n label=\"Workspace views\"\n onValueChange={nextValue => {\n onValueChange(nextValue)\n setValue(nextValue)\n }}\n options={[\n { label: 'Overview', value: 'overview' },\n { label: 'Activity', value: 'activity' },\n { disabled: true, label: 'Billing', value: 'billing' }\n ]}\n value={value}\n >\n <LumenText>Current workspace health</LumenText>\n </LumenTabs>\n )\n }\n const root = await renderNative(<KeyboardTabsFixture />)\n const pressTabKey = async (optionIndex: number, key: string): Promise<void> => {\n const tab = root.container.queryAll(\n instance => readProp(instance, 'accessibilityRole') === 'tab'\n )[optionIndex]\n\n if (!tab) throw new Error(`Expected tab fixture at index ${optionIndex}.`)\n\n let prevented = false\n\n await act(async () => {\n const preventDefault = callKeyboardAction(readProp(tab, 'onKeyDown'), key)\n\n prevented = preventDefault.mock.calls.length > 0\n\n await Promise.resolve()\n })\n\n expect(prevented).toBe(true)\n }\n\n await pressTabKey(0, 'ArrowRight')\n expect(onValueChange).toHaveBeenLastCalledWith('activity')\n\n await pressTabKey(1, 'ArrowRight')\n expect(onValueChange).toHaveBeenLastCalledWith('overview')\n expect(onValueChange).not.toHaveBeenCalledWith('billing')\n })\n\n test('tabs do not reselect the current tab when every alternative is disabled', async () => {\n const onValueChange = vi.fn<(value: string) => void>()\n const root = await renderNative(\n <LumenTabs\n label=\"Workspace views\"\n onValueChange={onValueChange}\n options={[\n { label: 'Overview', value: 'overview' },\n { disabled: true, label: 'Activity', value: 'activity' }\n ]}\n value=\"overview\"\n >\n <LumenText>Current workspace health</LumenText>\n </LumenTabs>\n )\n const overviewTab = root.container.queryAll(\n instance => readProp(instance, 'accessibilityRole') === 'tab'\n )[0]\n\n if (!overviewTab) throw new Error('Expected the selected tab fixture.')\n\n expect(callKeyboardAction(readProp(overviewTab, 'onKeyDown'), 'ArrowRight'))\n .toHaveBeenCalledOnce()\n expect(onValueChange).not.toHaveBeenCalled()\n })\n})\n\ndescribe('LumenSheet consumer layouts', () => {\n afterEach(() => {\n nativeMotion.enabled = false\n nativeWindow.width = 400\n nativeWindow.height = 800\n nativeWindow.fontScale = 1\n nativePlatform.OS = 'ios'\n accessibilityFocus.mockClear()\n })\n\n test.each([\n { fontScale: 2, height: 800, alignment: 'center' },\n { fontScale: 1, height: 320, alignment: 'flex-end' }\n ])('keeps headings and actions scrollable when space is limited: %j', async ({ alignment, ...window }) => {\n Object.assign(nativeWindow, window, { width: 1024 })\n const root = await renderNative(\n <LumenSheet actions={<LumenButton>Save changes</LumenButton>} onDismiss={vi.fn()} presentation=\"adaptive\" title=\"A long translated heading\" visible>\n <LumenTextField accessibilityLabel=\"Details\" />\n </LumenSheet>\n )\n const scroll = findHostComponent(root, 'ScrollView')\n\n expect(scroll.queryAll(instance => readProp(instance, 'accessibilityRole') === 'header')).toHaveLength(1)\n expect(scroll.queryAll(instance => readProp(instance, 'accessibilityRole') === 'button')).toHaveLength(1)\n expect(readProp(scroll, 'keyboardShouldPersistTaps')).toBe('handled')\n expect(readProp(findHostComponent(root, 'KeyboardAvoidingView'), 'style')).toMatchObject({ justifyContent: alignment })\n })\n\n test('preserves application-owned virtualization at accessibility text sizes', async () => {\n nativeWindow.fontScale = 3\n const root = await renderNative(\n <LumenSheet onDismiss={vi.fn()} scrollable={false} visible>\n <LumenText>Virtualized collection</LumenText>\n </LumenSheet>\n )\n\n expect(root.container.queryAll(instance => instance.type === 'ScrollView')).toHaveLength(0)\n })\n\n test('focuses an explicit initial control after presentation and restores the trigger after closing', async () => {\n const initialFocusRef = createRef<ComponentRef<typeof View>>()\n const returnFocusRef = createRef<ComponentRef<typeof View>>()\n let restoreFrame: (() => void) | undefined\n vi.stubGlobal('requestAnimationFrame', (callback: () => void) => {\n restoreFrame = callback\n\n return 1\n })\n vi.stubGlobal('cancelAnimationFrame', vi.fn())\n const fixture = (visible: boolean): ReactElement => (\n <>\n <LumenButton ref={returnFocusRef}>Edit</LumenButton>\n <LumenSheet\n initialFocusRef={initialFocusRef}\n onDismiss={vi.fn()}\n returnFocusRef={returnFocusRef}\n visible={visible}\n >\n <LumenButton ref={initialFocusRef}>Save</LumenButton>\n </LumenSheet>\n </>\n )\n const root = await renderNative(fixture(true))\n callAction(readProp(findHostComponent(root, 'Modal'), 'onShow'), 'Missing modal presentation event')\n expect(accessibilityFocus).toHaveBeenCalledExactlyOnceWith(initialFocusRef.current, 'focus')\n await act(async () => {\n root.render(<LumenProvider>{fixture(false)}</LumenProvider>)\n await Promise.resolve()\n })\n expect(accessibilityFocus).toHaveBeenCalledTimes(1)\n if (!restoreFrame) throw new Error('Expected a deferred focus restoration')\n restoreFrame()\n expect(accessibilityFocus).toHaveBeenLastCalledWith(returnFocusRef.current, 'focus')\n })\n\n test('wraps related actions at ordinary text sizes and stacks them at accessibility sizes', async () => {\n const fixture = (): ReactElement => (\n <LumenButtonGroup accessibilityLabel=\"Form actions\">\n <LumenButton>Cancel</LumenButton>\n <LumenButton>Save</LumenButton>\n </LumenButtonGroup>\n )\n const root = await renderNative(fixture())\n const group = root.container.queryAll(instance => readProp(instance, 'accessibilityLabel') === 'Form actions')[0]\n if (!group) throw new Error('Missing action group')\n expect(readProp(group, 'style')).toEqual(expect.arrayContaining([expect.objectContaining({ flexDirection: 'row', flexWrap: 'wrap' })]))\n nativeWindow.fontScale = 2\n await act(async () => {\n root.render(<LumenProvider>{fixture()}</LumenProvider>)\n await Promise.resolve()\n })\n const updated = root.container.queryAll(instance => readProp(instance, 'accessibilityLabel') === 'Form actions')[0]\n if (!updated) throw new Error('Missing updated action group')\n expect(readProp(updated, 'style')).toEqual(expect.arrayContaining([expect.objectContaining({ flexDirection: 'column', flexWrap: 'nowrap' })]))\n })\n\n test('keeps actions outside the scrolling body and opts into keyboard avoidance', async () => {\n const root = await renderNative(\n <LumenSheet\n actions={<LumenButton>Save</LumenButton>}\n avoidKeyboard\n keyboardVerticalOffset={24}\n onDismiss={vi.fn()}\n safeAreaInsets={{ bottom: 34 }}\n scrollable\n title=\"Edit record\"\n visible\n >\n <LumenTextField accessibilityLabel=\"Name\" />\n </LumenSheet>\n )\n const scroll = root.container.queryAll(instance => instance.type === 'ScrollView')[0]\n const keyboard = root.container.queryAll(instance => instance.type === 'KeyboardAvoidingView')[0]\n if (!scroll || !keyboard) throw new Error('Missing keyboard or scrolling surface')\n expect(readProp(keyboard, 'enabled')).toBe(true)\n expect(readProp(keyboard, 'behavior')).toBe('padding')\n expect(readProp(keyboard, 'keyboardVerticalOffset')).toBe(24)\n expect(readProp(scroll, 'keyboardShouldPersistTaps')).toBe('handled')\n expect(scroll.queryAll(instance => readProp(instance, 'accessibilityRole') === 'button')).toHaveLength(0)\n expect(findByAccessibilityRole(root, 'header')).toBeDefined()\n expect(root.container.queryAll(instance => {\n const style = readProp(instance, 'style')\n return typeof style === 'object' && style !== null && 'paddingBottom' in style && style.paddingBottom === 34\n })).toHaveLength(1)\n })\n\n test('blocks platform and backdrop dismissal until the application unlocks it', async () => {\n const dismiss = vi.fn()\n const root = await renderNative(\n <LumenSheet dismissible={false} onDismiss={dismiss} visible><LumenText>Saving</LumenText></LumenSheet>\n )\n const modal = root.container.queryAll(instance => instance.type === 'Modal')[0]\n const backdrop = root.container.queryAll(instance => instance.type === 'Pressable')[0]\n if (!modal || !backdrop) throw new Error('Missing modal or backdrop')\n callAction(readProp(modal, 'onRequestClose'), 'Missing platform dismiss')\n callAction(readProp(backdrop, 'onPress'), 'Missing backdrop dismiss')\n expect(dismiss).not.toHaveBeenCalled()\n expect(readProp(backdrop, 'disabled')).toBe(true)\n await act(async () => {\n root.render(<LumenProvider scheme=\"light\"><LumenSheet onDismiss={dismiss} visible><LumenText>Saved</LumenText></LumenSheet></LumenProvider>)\n await Promise.resolve()\n })\n const updatedModal = root.container.queryAll(instance => instance.type === 'Modal')[0]\n if (!updatedModal) throw new Error('Missing updated modal')\n callAction(readProp(updatedModal, 'onRequestClose'), 'Missing platform dismiss')\n expect(dismiss).toHaveBeenCalledOnce()\n })\n\n test('names sheets and hides backdrop targets from keyboard and accessibility traversal', async () => {\n const root = await renderNative(<LumenSheet onDismiss={vi.fn()} title=\"Edit record\" visible><LumenText>Details</LumenText></LumenSheet>)\n const modal = root.container.queryAll(instance => instance.type === 'Modal')[0]\n const backdrop = root.container.queryAll(instance => instance.type === 'Pressable')[0]\n if (!modal || !backdrop) throw new Error('Missing modal or backdrop')\n expect(readProp(modal, 'accessibilityLabel')).toBe('Edit record')\n expect(readProp(backdrop, 'tabIndex')).toBe(-1)\n expect(readProp(backdrop, 'aria-hidden')).toBe(true)\n })\n\n test('honors the native reduced-motion setting', async () => {\n nativeMotion.enabled = true\n const root = await renderNative(<LumenSheet onDismiss={vi.fn()} visible><LumenText>Details</LumenText></LumenSheet>)\n const modal = root.container.queryAll(instance => instance.type === 'Modal')[0]\n if (!modal) throw new Error('Missing modal')\n expect(readProp(modal, 'animationType')).toBe('none')\n })\n\n test('honors reduced motion in confirmation dialogs and menus', async () => {\n nativeMotion.enabled = true\n const root = await renderNative(\n <>\n <LumenAlertDialog confirmLabel=\"Save\" onConfirm={vi.fn()} onDismiss={vi.fn()} title=\"Save changes\" visible />\n <LumenMenu accessibilityLabel=\"Actions\" items={[]} trigger={<LumenText>Open menu</LumenText>} />\n </>\n )\n const modals = root.container.queryAll(instance => instance.type === 'Modal')\n\n expect(modals).toHaveLength(2)\n expect(modals.map(modal => readProp(modal, 'animationType'))).toEqual(['none', 'none'])\n })\n\n test('uses a centered adaptive dialog on wide windows and Android keyboard behavior', async () => {\n nativeWindow.width = 1024\n nativePlatform.OS = 'android'\n const root = await renderNative(\n <LumenSheet avoidKeyboard onDismiss={vi.fn()} presentation=\"adaptive\" visible><LumenText>Details</LumenText></LumenSheet>\n )\n const modal = root.container.queryAll(instance => instance.type === 'Modal')[0]\n const keyboard = root.container.queryAll(instance => instance.type === 'KeyboardAvoidingView')[0]\n if (!modal || !keyboard) throw new Error('Missing modal or keyboard surface')\n expect(readProp(modal, 'animationType')).toBe('fade')\n expect(readProp(keyboard, 'behavior')).toBe('height')\n expect(readProp(keyboard, 'style')).toMatchObject({ alignItems: 'center', justifyContent: 'center' })\n })\n})\n\ndescribe('Static graphic icon accessibility', () => {\n const Graphic = ({ size, strokeWidth }: LumenIconGraphicProps): ReactElement => (\n <LumenText>{`Graphic ${String(size)} ${String(strokeWidth)}`}</LumenText>\n )\n\n test('names informative graphics and hides decorative graphics', async () => {\n const root = await renderNative(\n <>\n <GraphicIcon icon={Graphic} label=\"Search records\" size=\"lg\" strokeWidth={3} />\n <GraphicIcon icon={Graphic} decorative label=\"Decorative search\" />\n </>\n )\n const informative = root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'image' && readProp(instance, 'accessible') === true)\n expect(informative).toHaveLength(1)\n const named = informative[0]\n if (!named) throw new Error('Missing informative graphic')\n expect(readProp(named, 'accessibilityLabel')).toBe('Search records')\n expect(readProp(named, 'importantForAccessibility')).toBe('yes')\n expect(root.container.queryAll(instance => readProp(instance, 'children') === 'Graphic 24 3').length).toBeGreaterThan(0)\n const hidden = root.container.queryAll(instance => readProp(instance, 'importantForAccessibility') === 'no' && readProp(instance, 'accessible') === false)\n expect(hidden).toHaveLength(1)\n const decorative = hidden[0]\n if (!decorative) throw new Error('Missing decorative graphic')\n expect(readProp(decorative, 'accessibilityLabel')).toBeUndefined()\n expect(readProp(decorative, 'accessibilityElementsHidden')).toBe(true)\n })\n\n test('preserves named and custom icons through the root entrypoint', async () => {\n const root = await renderNative(\n <>\n <LumenIcon name=\"search\" label=\"Named search\" />\n <LumenIcon icon={Graphic} label=\"Custom search\" />\n <LumenIconButton name=\"search\" label=\"Named search action\" disabled />\n <LumenIconButton icon={Graphic} label=\"Custom search action\" disabled />\n </>\n )\n const icons = root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'image')\n expect(icons.map(icon => readProp(icon, 'accessibilityLabel'))).toEqual(['Named search', 'Custom search'])\n const buttons = root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'button')\n expect(buttons.map(button => readProp(button, 'accessibilityLabel'))).toEqual(['Named search action', 'Custom search action'])\n expect(buttons.map(button => readProp(button, 'accessibilityState'))).toEqual([{ disabled: true }, { disabled: true }])\n })\n\n test('canonical per-icon imports match named artwork and preserve accessible labels', async () => {\n const root = await renderNative(\n <>\n <LumenIcon name=\"search\" label=\"Named search\" size=\"lg\" strokeWidth={3} />\n <GraphicIcon icon={LumenSearchIconGraphic} label=\"Static search\" size=\"lg\" strokeWidth={3} />\n <LumenIcon name=\"brand:github\" decorative />\n <GraphicIcon icon={LumenBrandGithubIconGraphic} decorative />\n </>\n )\n const graphics = root.container.queryAll(instance => instance.type === 'Svg')\n expect(graphics).toHaveLength(4)\n const geometry = (graphic: TestInstance | undefined): unknown => {\n if (!graphic) throw new Error('Missing canonical SVG')\n return {\n props: Object.fromEntries(Object.entries(graphic.props).filter(([name]) => name !== 'children')),\n children: graphic.children.filter(child => typeof child !== 'string').map(child => ({ type: child.type, props: child.props }))\n }\n }\n expect(geometry(graphics[0])).toEqual(geometry(graphics[1]))\n expect(geometry(graphics[2])).toEqual(geometry(graphics[3]))\n const icons = root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'image')\n expect(icons.map(icon => readProp(icon, 'accessibilityLabel'))).toEqual(['Named search', 'Static search'])\n })\n\n test('keeps graphic buttons named and announces disabled state', async () => {\n const root = await renderNative(<GraphicIconButton icon={Graphic} label=\"Search records\" disabled />)\n const button = root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'button')[0]\n if (!button) throw new Error('Missing graphic button')\n expect(readProp(button, 'accessibilityLabel')).toBe('Search records')\n expect(readProp(button, 'accessibilityState')).toEqual({ disabled: true })\n expect(readProp(button, 'disabled')).toBe(true)\n })\n})\n\nconst callInputAction = (value: unknown, proposal: string): void => {\n if (typeof value !== 'function') throw new Error('Missing native input callback')\n Reflect.apply(value, undefined, [proposal])\n}\n\nconst runNativeAction = async (action: () => void): Promise<void> => {\n await act(async () => {\n action()\n\n await Promise.resolve()\n })\n}\n\ndescribe('advanced native inputs', () => {\n test('secure entry hides on blur and after disabling and re-enabling', async () => {\n const props = { label: 'Password', onValueChange: vi.fn(), value: 'synthetic-fixture' }\n const root = await renderNative(<LumenPasswordField {...props} />)\n expect(readProp(findByAccessibilityLabel(root, 'Password'), 'secureTextEntry')).toBe(true)\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Show password'), 'onPress'), 'Missing reveal')\n })\n expect(readProp(findByAccessibilityLabel(root, 'Password'), 'secureTextEntry')).toBe(false)\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Password'), 'onBlur'), 'Missing blur')\n })\n expect(readProp(findByAccessibilityLabel(root, 'Password'), 'secureTextEntry')).toBe(true)\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Show password'), 'onPress'), 'Missing reveal')\n })\n await runNativeAction(() => {\n root.render(<LumenProvider><LumenPasswordField {...props} enabled={false} /></LumenProvider>)\n })\n await runNativeAction(() => {\n root.render(<LumenProvider><LumenPasswordField {...props} /></LumenProvider>)\n })\n expect(readProp(findByAccessibilityLabel(root, 'Password'), 'secureTextEntry')).toBe(true)\n })\n\n test('OTP preserves native autofill, normalizes paste and emits completion only for changed full codes', async () => {\n const onValueChange = vi.fn<(value: string) => void>()\n const onComplete = vi.fn<(value: string) => void>()\n const root = await renderNative(<LumenInputOTP label=\"Code\" value=\"123\" onValueChange={onValueChange} onComplete={onComplete} />)\n const input = findByAccessibilityLabel(root, 'Code')\n expect(readProp(input, 'textContentType')).toBe('oneTimeCode')\n await runNativeAction(() => {\n callInputAction(readProp(input, 'onChangeText'), '١٢٣-456')\n })\n expect(onValueChange).toHaveBeenCalledExactlyOnceWith('123456')\n expect(onComplete).toHaveBeenCalledExactlyOnceWith('123456')\n await runNativeAction(() => {\n callInputAction(readProp(input, 'onChangeText'), 'not a code')\n })\n expect(onValueChange).toHaveBeenCalledTimes(1)\n await runNativeAction(() => {\n root.render(<LumenProvider><LumenInputOTP label=\"Code\" value=\"123456\" onValueChange={onValueChange} onComplete={onComplete} readOnly /></LumenProvider>)\n })\n await runNativeAction(() => {\n callInputAction(readProp(findByAccessibilityLabel(root, 'Code'), 'onChangeText'), '654321')\n })\n expect(onValueChange).toHaveBeenCalledTimes(1)\n })\n\n test('number entry retains raw drafts and steps exact localized values', async () => {\n const onValueChange = vi.fn<(value: string) => void>()\n const root = await renderNative(<LumenNumberField label=\"Amount\" value=\"0,1\" locale=\"es-CO\" step=\"0.2\" onValueChange={onValueChange} />)\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Increase value'), 'onPress'), 'Missing increase')\n })\n expect(onValueChange).toHaveBeenLastCalledWith('0,3')\n await runNativeAction(() => {\n callInputAction(readProp(findByAccessibilityLabel(root, 'Amount'), 'onChangeText'), '12,')\n })\n expect(onValueChange).toHaveBeenLastCalledWith('12,')\n await runNativeAction(() => {\n root.render(<LumenProvider><LumenNumberField label=\"Amount\" value=\"12,\" locale=\"es-CO\" onValueChange={onValueChange} /></LumenProvider>)\n })\n expect(readProp(findByAccessibilityLabel(root, 'Increase value'), 'disabled')).toBe(true)\n expect(readProp(findByAccessibilityLabel(root, 'Amount'), 'aria-invalid')).toBe(true)\n })\n\n test('autocomplete publishes query before selection and closes its results', async () => {\n const calls: string[] = []\n const root = await renderNative(\n <LumenAutocomplete\n label=\"City\"\n query=\"Bo\"\n options={[{ label: 'Bogotá', value: 'bogota' }]}\n onQueryChange={query => {\n calls.push(query)\n }}\n onValueChange={value => {\n calls.push(value)\n }}\n />\n )\n await runNativeAction(() => {\n callInputAction(readProp(findByAccessibilityLabel(root, 'City'), 'onChangeText'), 'Bog')\n })\n calls.length = 0\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Bogotá'), 'onPress'), 'Missing result')\n })\n expect(calls).toEqual(['Bogotá', 'bogota'])\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityLabel') === 'Close results')).toHaveLength(0)\n })\n\n test('autocomplete loading hides results and disabled state dismisses selection', async () => {\n const props = { label: 'City', onQueryChange: vi.fn(), onValueChange: vi.fn(), options: [{ label: 'Bogotá', value: 'bogota' }], query: '' }\n const root = await renderNative(<LumenAutocomplete {...props} loading />)\n await runNativeAction(() => {\n callInputAction(readProp(findByAccessibilityLabel(root, 'City'), 'onChangeText'), 'Bo')\n })\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityLabel') === 'Bogotá')).toHaveLength(0)\n await runNativeAction(() => {\n root.render(<LumenProvider><LumenAutocomplete {...props} enabled={false} /></LumenProvider>)\n })\n await runNativeAction(() => {\n root.render(<LumenProvider><LumenAutocomplete {...props} /></LumenProvider>)\n })\n expect(readProp(findByAccessibilityLabel(root, 'City'), 'accessibilityState')).toMatchObject({ expanded: false })\n })\n\n test.each(['ios', 'android', 'web'])('time formatting tolerates malformed locales on %s', async platform => {\n nativePlatform.OS = platform\n for (const locale of ['not_a_locale', 'en--US', '💥']) {\n const props = { label: 'Time', value: { hour: 9, minute: 30 }, locale, onValueChange: vi.fn() }\n const root = await renderNative(<LumenTimeField {...props} is24Hour />)\n\n expect(findByAccessibilityLabel(root, 'Time, 09:30')).toBeDefined()\n await runNativeAction(() => {\n root.render(<LumenProvider><LumenTimeField {...props} is24Hour={false} /></LumenProvider>)\n })\n expect(findByAccessibilityLabel(root, 'Time, 9:30 AM')).toBeDefined()\n }\n })\n\n test('time selection requires confirmation and cancellation preserves the value', async () => {\n nativePlatform.OS = 'ios'\n const onValueChange = vi.fn()\n const root = await renderNative(\n <LumenTimeField\n label=\"Time\"\n value={{ hour: 9, minute: 30 }}\n locale=\"en-US\"\n is24Hour\n minTime={{ hour: 9, minute: 0 }}\n maxTime={{ hour: 17, minute: 0 }}\n onValueChange={onValueChange}\n />\n )\n const open = async (): Promise<void> => {\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Time, 09:30'), 'onPress'), 'Missing time trigger')\n })\n }\n await open()\n await runNativeAction(() => {\n const picker = findHostComponent(root, 'NativeDatePicker')\n const change = readProp(picker, 'onChange')\n if (typeof change !== 'function') throw new Error('Missing time change')\n Reflect.apply(change, undefined, [{ type: 'set' }, new Date(2000, 0, 1, 10, 45)])\n })\n expect(onValueChange).not.toHaveBeenCalled()\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Cancel'), 'onPress'), 'Missing cancel')\n })\n expect(onValueChange).not.toHaveBeenCalled()\n await open()\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Confirm'), 'onPress'), 'Missing confirm')\n })\n expect(onValueChange).toHaveBeenCalledExactlyOnceWith({ hour: 9, minute: 30 })\n })\n\n test.each(['ios', 'web'])('time sheet resets controlled changes without discarding equivalent inputs on %s', async platform => {\n nativePlatform.OS = platform\n const updates = [\n { value: { hour: 11, minute: 15 }, expected: { hour: 11, minute: 15 } },\n { minTime: { hour: 10, minute: 0 }, expected: { hour: 10, minute: 0 } },\n { maxTime: { hour: 9, minute: 15 }, expected: { hour: 9, minute: 0 } }\n ]\n for (const { expected, ...update } of updates) {\n const onValueChange = vi.fn()\n const props: LumenTimeFieldProps = { label: 'Time',\n value: { hour: 9, minute: 30 },\n locale: 'en-US',\n is24Hour: true,\n minTime: { hour: 9, minute: 0 },\n maxTime: { hour: 17, minute: 0 },\n onValueChange }\n const root = await renderNative(<LumenTimeField {...props} />)\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Time, 09:30'), 'onPress'), 'Missing time trigger')\n })\n await runNativeAction(() => {\n const picker = findHostComponent(root, platform === 'web' ? 'input' : 'NativeDatePicker')\n const change = readProp(picker, 'onChange')\n if (typeof change !== 'function') throw new Error('Missing time change')\n Reflect.apply(change, undefined, platform === 'web' ? [{ currentTarget: { value: '10:45' } }] : [{ type: 'set' }, new Date(2000, 0, 1, 10, 45)])\n })\n await runNativeAction(() => {\n root.render(\n <LumenProvider>\n <LumenTimeField\n {...props}\n value={{ hour: 9, minute: 30 }}\n minTime={{ hour: 9, minute: 0 }}\n maxTime={{ hour: 17, minute: 0 }}\n />\n </LumenProvider>\n )\n })\n expect(readProp(findHostComponent(root, 'Modal'), 'visible')).toBe(true)\n const draft = readProp(findHostComponent(root, platform === 'web' ? 'input' : 'NativeDatePicker'), 'value')\n expect(draft).toEqual(platform === 'web' ? '10:45' : new Date(2000, 0, 1, 10, 45))\n await runNativeAction(() => {\n root.render(<LumenProvider><LumenTimeField {...props} {...update} /></LumenProvider>)\n })\n expect(readProp(findHostComponent(root, 'Modal'), 'visible')).toBe(false)\n expect(onValueChange).not.toHaveBeenCalled()\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, `Time, ${update.value ? '11:15' : '09:30'}`), 'onPress'), 'Missing updated time trigger')\n })\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Confirm'), 'onPress'), 'Missing time confirm')\n })\n expect(onValueChange).toHaveBeenCalledExactlyOnceWith(expected)\n }\n })\n\n test('Android time rejects out-of-range selection and stale callbacks after disabling', async () => {\n nativePlatform.OS = 'android'\n const onValueChange = vi.fn()\n const props = { label: 'Time', locale: 'en-US', onValueChange, value: null }\n const root = await renderNative(<LumenTimeField {...props} minTime={{ hour: 9, minute: 0 }} />)\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Time, Choose a time'), 'onPress'), 'Missing time trigger')\n })\n const options = vi.mocked(DateTimePickerAndroid.open).mock.calls.at(-1)?.[0]\n if (!options?.onValueChange) throw new Error('Missing Android time handler')\n const change = options.onValueChange\n await runNativeAction(() => {\n change({ nativeEvent: { timestamp: 0, utcOffset: 0 } }, new Date(2000, 0, 1, 8, 0))\n })\n expect(onValueChange).not.toHaveBeenCalled()\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Time, Choose a time'), 'onPress'), 'Missing time trigger')\n })\n const current = vi.mocked(DateTimePickerAndroid.open).mock.calls.at(-1)?.[0]\n if (!current?.onValueChange) throw new Error('Missing Android time handler')\n const stale = current.onValueChange\n await runNativeAction(() => {\n root.render(<LumenProvider><LumenTimeField {...props} enabled={false} /></LumenProvider>)\n })\n expect(DateTimePickerAndroid.dismiss).toHaveBeenCalledWith('time')\n await runNativeAction(() => {\n stale({ nativeEvent: { timestamp: 0, utcOffset: 0 } }, new Date(2000, 0, 1, 10, 0))\n })\n expect(onValueChange).not.toHaveBeenCalled()\n })\n\n test('Android time validates current bounds and uses the current callback while the dialog is open', async () => {\n nativePlatform.OS = 'android'\n const openingCallback = vi.fn()\n const currentCallback = vi.fn()\n const props = { label: 'Time', locale: 'en-US', value: null }\n const root = await renderNative(\n <LumenTimeField {...props} minTime={{ hour: 9, minute: 0 }} onValueChange={openingCallback} />\n )\n const chooseAfterUpdate = async (hour: number): Promise<void> => {\n await runNativeAction(() => {\n root.render(\n <LumenProvider>\n <LumenTimeField {...props} minTime={{ hour: 9, minute: 0 }} onValueChange={openingCallback} />\n </LumenProvider>\n )\n })\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Time, Choose a time'), 'onPress'), 'Missing time trigger')\n })\n const options = vi.mocked(DateTimePickerAndroid.open).mock.calls.at(-1)?.[0]\n if (!options?.onValueChange) throw new Error('Missing Android time handler')\n const change = options.onValueChange\n await runNativeAction(() => {\n root.render(<LumenProvider><LumenTimeField {...props} minTime={{ hour: 12, minute: 0 }} maxTime={{ hour: 17, minute: 0 }} onValueChange={currentCallback} rangeErrorLabel=\"Choose an afternoon time\" /></LumenProvider>)\n })\n await runNativeAction(() => {\n change({ nativeEvent: { timestamp: 0, utcOffset: 0 } }, new Date(2000, 0, 1, hour, 30))\n })\n }\n await chooseAfterUpdate(10)\n expect(openingCallback).not.toHaveBeenCalled()\n expect(currentCallback).not.toHaveBeenCalled()\n expect(root.container.queryAll(instance => readProp(instance, 'children') === 'Choose an afternoon time').length).toBeGreaterThan(0)\n await chooseAfterUpdate(18)\n expect(currentCallback).not.toHaveBeenCalled()\n await chooseAfterUpdate(13)\n expect(openingCallback).not.toHaveBeenCalled()\n expect(currentCallback).toHaveBeenCalledExactlyOnceWith({ hour: 13, minute: 30 })\n })\n\n test.each(['en--US', 'invalid_tag', '💥'])('image comparison falls back for malformed locale %s', async locale => {\n const root = await renderNative(<LumenImageComparison label=\"Comparison\" before={{ uri: 'fixture:before' }} after={{ uri: 'fixture:after' }} value={0.25} locale={locale} onValueChange={() => {}} />)\n const control = findByAccessibilityRole(root, 'adjustable')\n expect(readProp(control, 'accessibilityValue')).toMatchObject({ text: 'After 25%' })\n })\n\n test.each(['side-by-side', 'before', 'after'] as const)('image comparison %s mode presents labels without an adjustable control', async mode => {\n const root = await renderNative(<LumenImageComparison label=\"Comparison\" before={{ uri: 'fixture:before' }} after={{ uri: 'fixture:after' }} value={0.25} mode={mode} onValueChange={() => {}} />)\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'adjustable')).toHaveLength(0)\n expect(root.container.queryAll(instance => readProp(instance, 'children') === 'Before').length > 0).toBe(mode !== 'after')\n expect(root.container.queryAll(instance => readProp(instance, 'children') === 'After').length > 0).toBe(mode !== 'before')\n })\n\n test('image comparison exposes one adjustable control and localized after percentage', async () => {\n const root = await renderNative(<LumenImageComparison label=\"Comparison\" before={{ uri: 'fixture:before' }} after={{ uri: 'fixture:after' }} value={0.25} locale=\"es-CO\" onValueChange={() => {}} />)\n const control = findByAccessibilityRole(root, 'adjustable')\n expect(readProp(control, 'accessibilityLabel')).toBe('Comparison')\n expect(readProp(control, 'accessibilityValue')).toMatchObject({ text: 'After 25%' })\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'image')).toHaveLength(0)\n })\n})\n\ndescribe('native chart data inspection', () => {\n test('renders zero as neutral and missing measurements with a separate marker and exact fallback', async () => {\n const root = await renderNative(\n <LumenHeatmap\n label=\"Activity\"\n colorScale=\"diverging\"\n data={[\n { x: 'Mon', y: 'AM', value: -2 },\n { x: 'Tue', y: 'AM', value: 0 },\n { x: 'Wed', y: 'AM', value: null },\n { x: 'Mon', y: 'AM', value: 40 }\n ]}\n />\n )\n const neutral = root.container.queryAll(instance => instance.type === 'Rect' && readProp(instance, 'fillOpacity') === 0)\n expect(neutral).toHaveLength(1)\n const missing = root.container.queryAll(instance => instance.type === 'Text' && readProp(instance, 'children') === '× Not available')\n expect(missing).toHaveLength(1)\n await act(async () => {\n callAction(readProp(findByAccessibilityRole(root, 'button'), 'onPress'), 'Missing chart disclosure')\n await Promise.resolve()\n })\n const rows = root.container.queryAll(instance => instance.type === 'Text').map(instance => readProp(instance, 'children'))\n expect(rows).toContain('Wed, AM: Not available')\n expect(rows).toContain('Tue, AM: 0')\n expect(rows).toContain('Mon, AM: -2')\n expect(rows).not.toContain('Mon, AM: 40')\n })\n\n test('opens and closes exact source values using a labeled disclosure', async () => {\n const root = await renderNative(\n <LumenWaterfallChart\n label=\"Revenue\"\n data={[\n { id: 'opening', label: 'Opening', kind: 'total', value: 100 },\n { id: 'cost', label: 'Costs', value: -25 }\n ]}\n />\n )\n const button = findByAccessibilityRole(root, 'button')\n expect(readProp(button, 'accessibilityState')).toMatchObject({ expanded: false })\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'list')).toHaveLength(0)\n await act(async () => {\n callAction(readProp(button, 'onPress'), 'Missing chart disclosure')\n await Promise.resolve()\n })\n expect(readProp(findByAccessibilityRole(root, 'button'), 'accessibilityState')).toMatchObject({ expanded: true })\n const rows = root.container.queryAll(instance => instance.type === 'Text').map(instance => readProp(instance, 'children'))\n expect(rows).toContain('Costs, Start: 100, End: 75, Value: -25')\n await act(async () => {\n callAction(readProp(findByAccessibilityRole(root, 'button'), 'onPress'), 'Missing chart disclosure')\n await Promise.resolve()\n })\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'list')).toHaveLength(0)\n })\n\n test('shows a localized invalid-data state rather than a partial histogram', async () => {\n const root = await renderNative(\n <LumenHistogram\n label=\"Distribution\"\n labels={{ invalidData: 'Check the bins' }}\n data={[\n { start: 0, end: 10, count: 5 }, { start: 5, end: 15, count: 3 }\n ]}\n />\n )\n expect(readProp(findByAccessibilityRole(root, 'alert'), 'children')).toBe('Check the bins')\n expect(root.container.queryAll(instance => instance.type === 'Svg')).toHaveLength(0)\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'button')).toHaveLength(0)\n })\n})\n\ndescribe('controlled multiple selection', () => {\n test('removal retains unavailable values and never mutates the controlled set', async () => {\n const values = new Set(['one', 'missing'])\n const onValuesChange = vi.fn<(values: ReadonlySet<string>) => void>()\n const root = await renderNative(<LumenMultiSelect label=\"Teams\" values={values} onValuesChange={onValuesChange} query=\"\" onQueryChange={() => undefined} options={[{ label: 'One', value: 'one' }]} />)\n const remove = root.container.queryAll(instance => readProp(instance, 'accessibilityLabel') === 'Remove One').find(instance => typeof readProp(instance, 'onPress') === 'function')\n if (!remove) throw new Error('Missing selected removal')\n const handler = readProp(remove, 'onPress')\n if (typeof handler !== 'function') throw new Error('Missing removal handler')\n await act(async () => {\n Reflect.apply(handler, undefined, [])\n await Promise.resolve()\n })\n expect(onValuesChange).toHaveBeenLastCalledWith(new Set(['missing']))\n expect(values).toEqual(new Set(['one', 'missing']))\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityLabel') === 'Remove missing').length).toBeGreaterThan(0)\n })\n\n test.each([{ readOnly: true }, { enabled: false }])('blocked controls reject removal: %o', async blocked => {\n const onValuesChange = vi.fn<(values: ReadonlySet<string>) => void>()\n const root = await renderNative(<LumenMultiSelect {...blocked} label=\"Teams\" values={new Set(['one'])} onValuesChange={onValuesChange} query=\"\" onQueryChange={() => undefined} options={[{ label: 'One', value: 'one' }]} />)\n const remove = root.container.queryAll(instance => readProp(instance, 'accessibilityLabel') === 'Remove One').find(instance => typeof readProp(instance, 'onPress') === 'function')\n if (!remove) throw new Error('Missing removal')\n const handler = readProp(remove, 'onPress')\n if (typeof handler !== 'function') throw new Error('Missing removal handler')\n await act(async () => {\n Reflect.apply(handler, undefined, [])\n await Promise.resolve()\n })\n expect(onValuesChange).not.toHaveBeenCalled()\n expect(readProp(remove, 'disabled')).toBe(true)\n })\n\n test('disabled selected results cannot be removed', async () => {\n const root = await renderNative(\n <LumenMultiSelect\n label=\"Teams\"\n values={new Set(['one'])}\n onValuesChange={() => {\n throw new Error('Disabled callback')\n }}\n query=\"\"\n onQueryChange={() => undefined}\n options={[{ label: 'One', value: 'one', disabled: true }]}\n />\n )\n const remove = root.container.queryAll(instance => readProp(instance, 'accessibilityLabel') === 'Remove One').find(instance => typeof readProp(instance, 'onPress') === 'function')\n if (!remove) throw new Error('Missing removal')\n expect(readProp(remove, 'disabled')).toBe(true)\n })\n})\n\ndescribe('multiple selection results', () => {\n test('checkbox intent retains values outside the result list', async () => {\n const onValuesChange = vi.fn<(values: ReadonlySet<string>) => void>()\n const values = new Set(['missing'])\n const root = await renderNative(<LumenMultiSelect label=\"Teams\" values={values} onValuesChange={onValuesChange} query=\"\" onQueryChange={() => undefined} options={[{ label: 'One', value: 'one' }]} />)\n const option = root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'checkbox').find(instance => typeof readProp(instance, 'onPress') === 'function')\n if (!option) throw new Error('Missing option')\n const handler = readProp(option, 'onPress')\n if (typeof handler !== 'function') throw new Error('Missing checkbox handler')\n await act(async () => {\n Reflect.apply(handler, undefined, [])\n await Promise.resolve()\n })\n expect(onValuesChange).toHaveBeenLastCalledWith(new Set(['missing', 'one']))\n expect(values).toEqual(new Set(['missing']))\n })\n\n test.each([{ loading: true }, { resultsErrorMessage: 'Search failed' }])('stale results are unavailable: %o', async state => {\n const root = await renderNative(<LumenMultiSelect {...state} label=\"Teams\" values={new Set<string>()} onValuesChange={() => undefined} query=\"\" onQueryChange={() => undefined} options={[{ label: 'One', value: 'one' }]} />)\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'checkbox')).toHaveLength(0)\n })\n})\n\ndescribe('multiple selection option identity', () => {\n test.each([\n { options: [{ label: 'One', value: '' }] },\n { options: [{ label: ' ', value: 'one' }] },\n { options: [{ label: 'One', value: 'one' }, { label: 'Duplicate', value: 'one' }] }\n ])('rejects invalid option identities: %o', async ({ options }) => {\n await expect(renderNative(<LumenMultiSelect label=\"Teams\" values={new Set<string>()} onValuesChange={() => undefined} query=\"\" onQueryChange={() => undefined} options={options} />)).rejects.toThrow('MultiSelect options require')\n })\n})\n\ntest('multiple selection forwards application safe-area insets to its modal', async () => {\n const root = await renderNative(<LumenMultiSelect label=\"Teams\" values={new Set<string>()} onValuesChange={() => undefined} query=\"\" onQueryChange={() => undefined} options={[]} safeAreaInsets={{ bottom: 96 }} />)\n const panels = root.container.queryAll(instance => {\n const style = readProp(instance, 'style')\n\n return typeof style === 'object' && style !== null && 'paddingBottom' in style && style.paddingBottom === 96\n })\n expect(panels.length).toBeGreaterThan(0)\n})\n\ndescribe('rating controlled selection', () => {\n test('clamps nonfinite values and limits the rendered option count', () => {\n expect(resolveLumenRating(Number.NaN, Number.POSITIVE_INFINITY)).toEqual({ max: 5, value: 0 })\n expect(resolveLumenRating(-5, 0)).toEqual({ max: 1, value: 0 })\n expect(resolveLumenRating(999, 999)).toEqual({ max: 100, value: 100 })\n expect(resolveLumenRating(3.9)).toEqual({ max: 5, value: 3 })\n })\n\n test('reports selection without mutating controlled value and blocks read-only edits', async () => {\n const onValueChange = vi.fn<(value: number) => void>()\n const root = await renderNative(\n <LumenRating\n label=\"Score\"\n value={2}\n onValueChange={onValueChange}\n formatOption={(value, max) => `${value} of ${max}`}\n />\n )\n const option = findByAccessibilityLabel(root, '4 of 5')\n act(() => {\n callAction(readProp(option, 'onPress'), 'Missing rating action')\n })\n expect(onValueChange).toHaveBeenCalledWith(4)\n expect(readProp(option, 'accessibilityState')).toEqual({ checked: false, disabled: false, selected: false })\n act(() => {\n root.render(\n <LumenProvider>\n <LumenRating\n label=\"Score\"\n value={2}\n readOnly\n onValueChange={onValueChange}\n />\n </LumenProvider>\n )\n })\n const locked = findByAccessibilityLabel(root, '4 / 5')\n act(() => {\n callAction(readProp(locked, 'onPress'), 'Missing rating action')\n })\n expect(onValueChange).toHaveBeenCalledTimes(1)\n expect(readProp(locked, 'disabled')).toBe(true)\n })\n})\n\ndescribe('progress and history contracts', () => {\n test('normalizes step progress without changing host state', () => {\n expect(resolveLumenStepState(0, -1, 3)).toBe('current')\n expect(resolveLumenStepState(0, Number.NaN, 3)).toBe('current')\n expect(resolveLumenStepState(1, 1.9, 3)).toBe('current')\n expect(resolveLumenStepState(2, 99, 3)).toBe('complete')\n expect(resolveLumenStepState(2, 1, 3)).toBe('upcoming')\n })\n\n test('presents localized controlled progression and accepts an empty workflow', async () => {\n const steps = [{ id: 'prepare', title: 'Prepare' }, { id: 'review', title: 'Review', description: 'Confirm details' }]\n const root = await renderNative(\n <LumenStepper\n label=\"Workflow\"\n steps={steps}\n currentStep={1}\n formatState={state => `Localized ${state}`}\n />\n )\n const review = findByAccessibilityLabel(root, '2 / 2, Review, Confirm details')\n expect(readProp(review, 'accessibilityValue')).toEqual({ text: 'Localized current' })\n expect(readProp(review, 'accessibilityState')).toEqual({ selected: true })\n act(() => {\n root.render(<LumenProvider><LumenStepper label=\"Workflow\" steps={steps} currentStep={2} /></LumenProvider>)\n })\n expect(readProp(findByAccessibilityLabel(root, '2 / 2, Review, Confirm details'), 'accessibilityValue'))\n .toEqual({ text: 'Complete' })\n act(() => {\n root.render(<LumenProvider><LumenStepper label=\"Workflow\" steps={[]} currentStep={0} /></LumenProvider>)\n })\n expect(root.container.queryAll(instance => readProp(instance, 'accessible') === true)).toHaveLength(0)\n })\n\n test('keeps timeline content actions accessible and host owned', async () => {\n const onPress = vi.fn()\n const root = await renderNative(\n <LumenTimeline label=\"History\">\n <LumenTimelineItem>\n <LumenText>Approved</LumenText>\n <LumenButton onPress={onPress}>View approval</LumenButton>\n </LumenTimelineItem>\n </LumenTimeline>\n )\n const button = findByAccessibilityRole(root, 'button')\n act(() => {\n callAction(readProp(button, 'onPress'), 'Missing timeline action')\n })\n expect(onPress).toHaveBeenCalledOnce()\n })\n\n test('breadcrumb reports navigation IDs and cannot navigate current or disabled locations', async () => {\n const onNavigate = vi.fn<(id: string) => void>()\n const items = [{ id: 'home', label: 'Home' },\n { id: 'private', label: 'Private', disabled: true },\n { id: 'current', label: 'Details' }]\n const root = await renderNative(\n <LumenBreadcrumb\n label=\"Location\"\n items={items}\n onNavigate={onNavigate}\n currentLabel=\"Here\"\n />\n )\n const home = findByAccessibilityLabel(root, 'Home')\n act(() => {\n callAction(readProp(home, 'onPress'), 'Missing breadcrumb action')\n })\n expect(onNavigate).toHaveBeenCalledExactlyOnceWith('home')\n const locked = findByAccessibilityLabel(root, 'Private')\n act(() => {\n callAction(readProp(locked, 'onPress'), 'Missing disabled breadcrumb action')\n })\n expect(readProp(findByAccessibilityLabel(root, 'Details, Here'), 'onPress')).toBeUndefined()\n expect(readProp(findByAccessibilityLabel(root, 'Details, Here'), 'aria-current')).toBe('page')\n })\n})\n\ndescribe('data table controlled interactions', () => {\n const columns = [{ key: 'amount', label: 'Amount', sortable: true }]\n const rows: readonly LumenTableRow[] = [\n { id: 'one', label: 'One', cells: { amount: { text: '20', sortValue: 20 } } },\n { id: 'two', label: 'Two', cells: { amount: { text: '2', sortValue: 2 } }, disabled: true }\n ]\n test('emits manual sort requests without reordering and retains hidden selections', async () => {\n const onSortChange = vi.fn()\n const onSelectionChange = vi.fn()\n const root = await renderNative(\n <LumenDataTable\n label=\"Records\"\n columns={columns}\n rows={rows}\n onSortChange={onSortChange}\n onSelectionChange={onSelectionChange}\n selectedIds={new Set(['hidden'])}\n />\n )\n act(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Amount'), 'onPress'), 'Missing sort action')\n })\n expect(onSortChange).toHaveBeenCalledWith({ key: 'amount', direction: 'ascending' })\n act(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Select visible'), 'onPress'), 'Missing bulk selection')\n })\n expect(onSelectionChange).toHaveBeenCalledWith(new Set(['hidden', 'one']))\n expect(root.container.queryAll(instance => readProp(instance, 'accessible') === true)\n .map(instance => readProp(instance, 'accessibilityLabel'))).toEqual(['Amount, 20', 'Amount, 2'])\n })\n test('loading and error states hide stale controls; retry remains host owned', async () => {\n const onRetry = vi.fn()\n const root = await renderNative(\n <LumenDataTable\n label=\"Records\"\n columns={columns}\n rows={rows}\n loading\n loadingLabel=\"Fetching\"\n onSortChange={vi.fn()}\n />\n )\n expect(readProp(findByAccessibilityRole(root, 'progressbar'), 'children')).toBe('Fetching')\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'button')).toHaveLength(0)\n act(() => {\n root.render(\n <LumenProvider>\n <LumenDataTable\n label=\"Records\"\n columns={columns}\n rows={rows}\n error=\"Try again\"\n onRetry={onRetry}\n retryLabel=\"Reload\"\n />\n </LumenProvider>\n )\n })\n act(() => {\n callAction(readProp(findByAccessibilityRole(root, 'button'), 'onPress'), 'Missing retry')\n })\n expect(onRetry).toHaveBeenCalledOnce()\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'checkbox')).toHaveLength(0)\n })\n test('read-only tables block both sort and selection callbacks', async () => {\n const onSortChange = vi.fn()\n const onSelectionChange = vi.fn()\n const root = await renderNative(\n <LumenDataTable\n label=\"Records\"\n columns={columns}\n rows={rows}\n readOnly\n onSortChange={onSortChange}\n onSelectionChange={onSelectionChange}\n />\n )\n act(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Amount'), 'onPress'), 'Missing guarded sort')\n })\n act(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Select visible'), 'onPress'), 'Missing guarded selection')\n })\n expect(onSortChange).not.toHaveBeenCalled()\n expect(onSelectionChange).not.toHaveBeenCalled()\n })\n})\n\ntest.each(['light', 'dark'] as const)('nested button content uses its %s foreground without overriding explicit tones', async scheme => {\n const theme = createLumenTheme(scheme)\n const root = await renderNative(\n <LumenProvider theme={theme}>\n <LumenButton>\n <LumenText accessibilityLabel=\"Inherited label\">Continue</LumenText>\n <LumenText accessibilityLabel=\"Explicit tone\" tone=\"danger\">Warning</LumenText>\n <LumenIcon name=\"search\" label=\"Inherited icon\" />\n </LumenButton>\n <LumenText accessibilityLabel=\"Outside control\">Outside</LumenText>\n </LumenProvider>\n )\n expect(readProp(findByAccessibilityLabel(root, 'Inherited label'), 'style')).toEqual(expect.arrayContaining([{ color: theme.colors.onBrand }]))\n expect(readProp(findByAccessibilityLabel(root, 'Explicit tone'), 'style')).toEqual(expect.arrayContaining([{ color: theme.colors.danger }]))\n expect(readProp(findByAccessibilityLabel(root, 'Outside control'), 'style')).toEqual(expect.arrayContaining([{ color: theme.colors.ink }]))\n const svg = findByAccessibilityLabel(root, 'Inherited icon').children[0]\n if (typeof svg !== 'object') throw new Error('Missing inherited icon content')\n expect(readProp(svg, 'stroke')).toBe(theme.colors.onBrand)\n})\n\ntest.each([false, true])('web checkbox Space toggles once and respects disabled=%s', async disabled => {\n nativePlatform.OS = 'web'\n const change = vi.fn<(checked: boolean) => void>()\n const hostKey = vi.fn()\n const root = await renderNative(<LumenCheckbox label=\"Choice\" checked disabled={disabled} onCheckedChange={change} onKeyDown={hostKey} />)\n const handler = readProp(findByAccessibilityRole(root, 'checkbox'), 'onKeyDown')\n if (typeof handler !== 'function') throw new Error('Missing checkbox keyboard handler')\n const preventDefault = vi.fn()\n await act(async () => {\n Reflect.apply(handler, undefined, [{ nativeEvent: { key: ' ', repeat: false }, defaultPrevented: false, preventDefault }])\n await Promise.resolve()\n })\n expect(hostKey).toHaveBeenCalledOnce()\n expect(preventDefault).toHaveBeenCalledOnce()\n expect(change).toHaveBeenCalledTimes(disabled ? 0 : 1)\n expect(change.mock.calls).toEqual(disabled ? [] : [[false]])\n await act(async () => {\n Reflect.apply(handler, undefined, [{ nativeEvent: { key: ' ', repeat: true }, defaultPrevented: false, preventDefault }])\n Reflect.apply(handler, undefined, [{ nativeEvent: { key: ' ', repeat: false }, defaultPrevented: true, preventDefault }])\n await Promise.resolve()\n })\n expect(change).toHaveBeenCalledTimes(disabled ? 0 : 1)\n})\n\ntest('media thumbnail keeps controlled selection and blocks loading requests', async () => {\n const onSelectionChange = vi.fn()\n const props = { label: 'Landscape', selected: true, onSelectionChange }\n const root = await renderNative(<LumenMediaThumbnail {...props}><LumenText>Photo</LumenText></LumenMediaThumbnail>)\n const button = findByAccessibilityLabel(root, 'Landscape')\n expect(readProp(button, 'accessibilityState')).toMatchObject({ selected: true })\n await runNativeAction(() => {\n callAction(readProp(button, 'onPress'), 'Missing thumbnail action')\n })\n expect(onSelectionChange).toHaveBeenCalledWith(false)\n await runNativeAction(() => {\n root.render(<LumenProvider><LumenMediaThumbnail {...props} state=\"loading\"><LumenText>Photo</LumenText></LumenMediaThumbnail></LumenProvider>)\n })\n const loading = findByAccessibilityLabel(root, 'Landscape')\n expect(readProp(loading, 'accessibilityState')).toMatchObject({ busy: true, disabled: true })\n await runNativeAction(() => {\n callAction(readProp(loading, 'onPress'), 'Missing loading action')\n })\n expect(onSelectionChange).toHaveBeenCalledTimes(1)\n})\n\ntest('native media inspection actions request bounded values and respect disabled state', async () => {\n const onValueChange = vi.fn()\n const props = { label: 'Inspect', value: { zoom: 2, x: 1, y: 0 }, onValueChange }\n const root = await renderNative(<LumenMediaViewport {...props}><LumenText>Photo</LumenText></LumenMediaViewport>)\n const right = root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'button')\n expect(right).toHaveLength(7)\n const fit = right[2]\n if (!fit) throw new Error('Missing fit action')\n await runNativeAction(() => {\n callAction(readProp(fit, 'onPress'), 'Missing fit handler')\n })\n expect(onValueChange).toHaveBeenCalledWith({ zoom: 1, x: 0, y: 0 })\n await runNativeAction(() => {\n root.render(\n <LumenProvider>\n <LumenMediaViewport {...props} disabled><LumenText>Photo</LumenText></LumenMediaViewport>\n </LumenProvider>\n )\n })\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'button').every(instance => {\n const state = readProp(instance, 'accessibilityState')\n return typeof state === 'object' && state !== null && 'disabled' in state && state.disabled === true\n })).toBe(true)\n})\n\ntest.each([true, false])('media pinch-to-pan preserves the host-accepted zoom (accept=%s)', async accept => {\n const change = vi.fn<(value: LumenMediaViewportValue) => void>()\n const Harness = () => {\n const [value, setValue] = useState<LumenMediaViewportValue>({ zoom: 2, x: 0, y: 0 })\n\n return (\n <LumenMediaViewport\n label=\"Inspect gesture\"\n value={value}\n onValueChange={next => {\n change(next)\n if (accept) setValue(next)\n }}\n >\n <LumenText>Photo</LumenText>\n </LumenMediaViewport>\n )\n }\n const root = await renderNative(<Harness />)\n const dispatch = async (property: string, nativeEvent: unknown): Promise<void> => {\n const handler = readProp(findByAccessibilityLabel(root, 'Inspect gesture'), property)\n if (typeof handler !== 'function') throw new Error(`Missing media ${property} callback`)\n await runNativeAction(() => {\n Reflect.apply(handler, undefined, [{ nativeEvent }])\n })\n }\n const touch = (pageX: number, pageY: number) => ({ pageX, pageY })\n\n await dispatch('onLayout', { layout: { width: 200, height: 100 } })\n await dispatch('onResponderGrant', { pageX: 50, pageY: 50, touches: [touch(50, 50), touch(100, 50)] })\n await dispatch('onResponderMove', { pageX: 50, pageY: 50, touches: [touch(50, 50), touch(150, 50)] })\n expect(change).toHaveBeenLastCalledWith({ zoom: 4, x: 0, y: 0 })\n change.mockClear()\n await dispatch('onResponderMove', { pageX: 150, pageY: 50, touches: [touch(150, 50)] })\n expect(change).not.toHaveBeenCalled()\n await dispatch('onResponderMove', { pageX: 175, pageY: 50, touches: [touch(175, 50)] })\n expect(change).toHaveBeenLastCalledWith({ zoom: accept ? 4 : 2, x: accept ? 1 / 12 : 0.25, y: 0 })\n})\n",
60416
+ "packages/react-native/src/component-behavior.test.tsx": "// cspell:words Espacios Configuración Cambios guardados Registro actualizado Otro\nimport { act, type ComponentRef, createRef, type ReactElement, type Ref, StrictMode, useImperativeHandle, useState } from 'react'\nimport type { View } from 'react-native'\n\nimport { DateTimePickerAndroid } from '@react-native-community/datetimepicker'\nimport { getLumenPhoneCountry } from '@santi020k/lumen-core'\nimport type { LumenMediaViewportValue } from '@santi020k/lumen-core/media-workspace'\nimport { createRoot, type Root, type TestInstance } from 'test-renderer'\nimport { afterEach, describe, expect, test, vi } from 'vitest'\n\nimport { LumenBrandGithubIconGraphic } from './static-icons/brand-github.generated.js'\nimport { LumenSearchIconGraphic } from './static-icons/search.generated.js'\nimport { LumenButtonGroup, LumenChip, LumenFieldGroup, LumenTextarea, LumenToast } from './additional-components.js'\nimport { LumenAutocomplete, LumenInputOTP, LumenNumberField, LumenPasswordField } from './advanced-form-components.js'\nimport { LumenBreadcrumb } from './breadcrumb-components.js'\nimport { LumenBarChart, LumenComboChart, LumenHeatmap, LumenHistogram, LumenLineChart, LumenRangeChart, LumenScatterChart, LumenWaterfallChart } from './chart-components.js'\nimport { LumenImageComparison } from './comparison-components.js'\nimport { LumenDateField, LumenDateRangeField } from './datetime-components.js'\nimport { LumenSearchField, LumenToggle } from './form-components.js'\nimport { LumenIcon as GraphicIcon, LumenIconButton as GraphicIconButton, type LumenIconGraphicProps } from './graphics.js'\nimport { type LumenToastHookController, type LumenToastHookOptions, useToast } from './hooks.js'\nimport { LumenMediaThumbnail, LumenMediaViewport } from './media-workspace-components.js'\nimport { LumenMultiSelect } from './multi-select-components.js'\nimport { LumenAlertDialog, LumenMenu, LumenSheet } from './overlay-components.js'\nimport { LumenPhoneInput } from './phone-components.js'\nimport { resolveLumenPhoneInputValue } from './phone-recipes.js'\nimport { LumenNavigationBar } from './platform-components.js'\nimport { LumenButton, LumenIcon, LumenIconButton, LumenText, LumenTextField } from './primitives.js'\nimport { LumenProvider } from './provider.js'\nimport { LumenRating } from './rating-components.js'\nimport { resolveLumenRating } from './rating-recipes.js'\nimport { LumenCheckbox, LumenTabs } from './selection-components.js'\nimport { LumenStepper } from './stepper-components.js'\nimport { resolveLumenStepState } from './stepper-recipes.js'\nimport { LumenBanner, LumenErrorState, LumenStatusBar } from './structured-components.js'\nimport { LumenDataTable } from './table-components.js'\nimport type { LumenTableRow } from './table-recipes.js'\nimport { createLumenTheme } from './theme.js'\nimport { LumenTimeField, type LumenTimeFieldProps } from './time-components.js'\nimport { LumenTimeline, LumenTimelineItem } from './timeline-components.js'\nimport { LumenPicker, LumenRangeSlider, LumenSlider } from './value-components.js'\n\nObject.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })\n\ntest.each([\n { labelledBy: 'consumer-label' },\n { labelledBy: ['consumer-label', 'consumer-context'] },\n { labelledBy: '' },\n { labelledBy: [] }\n])('grouped text fields retain explicit native labelled-by ownership: $labelledBy', async ({ labelledBy }) => {\n const root = await renderNative(\n <LumenFieldGroup label=\"Inherited label\">\n <LumenTextField accessibilityLabelledBy={labelledBy} />\n </LumenFieldGroup>\n )\n const input = root.container.queryAll(instance => instance.type === 'TextInput')[0]\n if (!input) throw new Error('Expected grouped text field')\n expect(readProp(input, 'accessibilityLabelledBy')).toEqual(labelledBy)\n expect(readProp(input, 'aria-labelledby')).toBeUndefined()\n expect(readProp(input, 'accessibilityLabel')).toBeUndefined()\n})\n\ntest('grouped text fields preserve explicit ARIA and native label references together', async () => {\n const root = await renderNative(\n <LumenFieldGroup label=\"Inherited label\">\n <LumenTextField accessibilityLabelledBy=\"native-label\" aria-labelledby=\"aria-label\" />\n </LumenFieldGroup>\n )\n const input = root.container.queryAll(instance => instance.type === 'TextInput')[0]\n if (!input) throw new Error('Expected grouped text field')\n expect(readProp(input, 'accessibilityLabelledBy')).toBe('native-label')\n expect(readProp(input, 'aria-labelledby')).toBe('aria-label')\n expect(readProp(input, 'accessibilityLabel')).toBeUndefined()\n})\n\nconst nativeDirection = vi.hoisted(() => ({ isRTL: false }))\nconst nativePlatform = vi.hoisted(() => ({ OS: 'ios' }))\nconst nativeMotion = vi.hoisted(() => ({ enabled: false }))\nconst accessibilityFocus = vi.hoisted(() => vi.fn())\nconst accessibilityAnnouncement = vi.hoisted(() => vi.fn<(message: string, options: { queue: boolean }) => void>())\ntype RecommendedTimeout = (duration: number) => Promise<number>\nconst accessibilityTimeout = vi.hoisted(() => vi.fn<RecommendedTimeout>(duration => Promise.resolve(duration)))\nconst nativeWindow = vi.hoisted(() => ({ fontScale: 1, height: 800, scale: 2, width: 400 }))\nconst safeAreaInsets = vi.hoisted(() => ({\n bottom: 34,\n left: 8,\n right: 12,\n top: 20\n}))\n\nvi.mock('react-native', async () => {\n const { createElement, isValidElement, useImperativeHandle } = await import('react')\n const hostComponent = (name: string) => (\n props: Record<string, unknown>\n ): ReactElement => createElement(name, props)\n type FocusablePressableProps = Record<string, unknown> & {\n ref?: Ref<{ focus: () => void }>\n }\n const FocusablePressable = ({ ref, ...props }: FocusablePressableProps): ReactElement => {\n useImperativeHandle(ref, () => ({ focus: vi.fn() }))\n\n return createElement('Pressable', props)\n }\n\n return {\n AccessibilityInfo: {\n addEventListener: () => ({ remove: vi.fn() }),\n announceForAccessibilityWithOptions: accessibilityAnnouncement,\n getRecommendedTimeoutMillis: accessibilityTimeout,\n isReduceMotionEnabled: () => Promise.resolve(nativeMotion.enabled),\n sendAccessibilityEvent: accessibilityFocus\n },\n ActivityIndicator: hostComponent('ActivityIndicator'),\n FlatList: (props: Record<string, unknown>): ReactElement => {\n const renderItem = props.renderItem\n const data: readonly unknown[] = Array.isArray(props.data) ? props.data : []\n const children = data.map((item, index) => {\n if (typeof renderItem !== 'function') return null\n\n const rendered: unknown = Reflect.apply(renderItem, undefined, [{ item, index }])\n\n return isValidElement(rendered) ? rendered : null\n })\n\n return createElement('FlatList', props, children)\n },\n I18nManager: nativeDirection,\n Image: hostComponent('Image'),\n KeyboardAvoidingView: hostComponent('KeyboardAvoidingView'),\n Modal: hostComponent('Modal'),\n Platform: nativePlatform,\n Pressable: FocusablePressable,\n ScrollView: hostComponent('ScrollView'),\n Share: { share: () => Promise.resolve({ action: 'sharedAction' }) },\n Switch: hostComponent('Switch'),\n Text: hostComponent('Text'),\n TextInput: hostComponent('TextInput'),\n useColorScheme: () => 'light',\n useWindowDimensions: () => nativeWindow,\n View: hostComponent('View')\n }\n})\n\nvi.mock('@react-native-community/datetimepicker', async () => {\n const { createElement } = await import('react')\n const NativeDatePicker = (props: Record<string, unknown>): ReactElement => (\n createElement('NativeDatePicker', props)\n )\n\n return {\n DateTimePickerAndroid: { dismiss: vi.fn(), open: vi.fn() },\n default: NativeDatePicker\n }\n})\n\nvi.mock('react-native-svg', async () => {\n const { createElement } = await import('react')\n const hostComponent = (name: string) => (\n props: Record<string, unknown>\n ): ReactElement => createElement(name, props)\n\n return Object.fromEntries(\n ['Circle', 'Defs', 'Ellipse', 'G', 'Line', 'LinearGradient', 'Path', 'Polygon', 'Polyline', 'Rect', 'Stop', 'Svg', 'Text']\n .map(name => [name, hostComponent(name)])\n )\n})\n\nconst mountedRoots: Root[] = []\n\ninterface ToastProbeProps {\n ref: Ref<LumenToastHookController>\n options?: LumenToastHookOptions\n}\n\nconst ToastProbe = ({ ref, options }: ToastProbeProps): ReactElement | null => {\n const controller = useToast(options)\n\n useImperativeHandle(ref, () => controller, [controller])\n\n return null\n}\n\nconst readToastController = (ref: { current: LumenToastHookController | null }): LumenToastHookController => {\n if (!ref.current) throw new Error('Toast controller is not mounted')\n\n return ref.current\n}\n\nconst deferredTimeout = () => {\n let complete: (duration: number) => void = () => {\n throw new Error('Missing timeout resolver')\n }\n const promise = new Promise<number>(resolve => {\n complete = resolve\n })\n\n return { promise, resolve: complete }\n}\n\nconst renderNative = async (element: ReactElement): Promise<Root> => {\n const root = createRoot({\n publicTextComponentTypes: ['Text'],\n textComponentTypes: ['RCTText', 'Text']\n })\n\n mountedRoots.push(root)\n\n await act(async () => {\n root.render(<LumenProvider scheme=\"light\">{element}</LumenProvider>)\n await Promise.resolve()\n })\n\n return root\n}\n\nconst readProp = (instance: TestInstance, property: string): unknown => {\n const props = instance.props as Record<string, unknown>\n\n return props[property]\n}\n\nconst callAction = (value: unknown, missingActionMessage: string): void => {\n if (typeof value !== 'function') throw new Error(missingActionMessage)\n\n Reflect.apply(value, undefined, [])\n}\n\nconst callKeyboardAction = (value: unknown, key: string): ReturnType<typeof vi.fn> => {\n if (typeof value !== 'function') throw new Error('Tab is missing its keyboard action.')\n\n const preventDefault = vi.fn()\n\n Reflect.apply(value, undefined, [{\n nativeEvent: { code: key, key },\n preventDefault\n }])\n\n return preventDefault\n}\n\nconst findByAccessibilityRole = (root: Root, role: string): TestInstance => {\n const matches = root.container.queryAll(\n instance => readProp(instance, 'accessibilityRole') === role\n )\n\n expect(matches).toHaveLength(1)\n\n const match = matches[0]\n\n if (!match) throw new Error(`Expected one ${role} accessibility node.`)\n\n return match\n}\n\nconst findByAccessibilityLabel = (root: Root, label: string): TestInstance => {\n const matches = root.container.queryAll(\n instance => readProp(instance, 'accessibilityLabel') === label\n )\n\n expect(matches).toHaveLength(1)\n\n const match = matches[0]\n\n if (!match) throw new Error(`Expected one accessibility node labeled ${label}.`)\n\n return match\n}\n\nconst findHostComponent = (root: Root, type: string): TestInstance => {\n const matches = root.container.queryAll(instance => instance.type === type)\n\n expect(matches).toHaveLength(1)\n\n const match = matches[0]\n\n if (!match) throw new Error(`Expected one ${type} host component.`)\n\n return match\n}\n\nafterEach(async () => {\n nativeDirection.isRTL = false\n const roots = mountedRoots.splice(0)\n\n await act(async () => {\n for (const root of roots) root.unmount()\n await Promise.resolve()\n })\n nativeWindow.fontScale = 1\n nativePlatform.OS = 'ios'\n accessibilityAnnouncement.mockClear()\n accessibilityTimeout.mockReset().mockImplementation(duration => Promise.resolve(duration))\n vi.useRealTimers()\n vi.unstubAllGlobals()\n})\n\ndescribe('Lumen React Native component behavior', () => {\n test.each(['bar', 'line', 'scatter', 'range', 'combo'] as const)(\n '%s chart fits its container and resizes without removing data', async kind => {\n const data = [3, 7, 4, 8, 5].map((y, x) => ({ x, y }))\n const series = [{ id: 'activity', label: 'Changes', data }]\n const props = { label: 'Weekly activity', showData: false, series }\n const charts = {\n bar: <LumenBarChart {...props} />,\n line: <LumenLineChart {...props} />,\n scatter: <LumenScatterChart {...props} />,\n range: <LumenRangeChart label=\"Weekly range\" showData={false} data={data.map(({ x, y }) => ({ x, low: y, high: y + 2 }))} />,\n combo: <LumenComboChart {...props} series={[{ id: 'activity', label: 'Changes', data, mark: 'bar' }]} />\n }\n const root = await renderNative(charts[kind])\n const layouts = root.container.queryAll(instance => instance.type === 'View' && typeof readProp(instance, 'onLayout') === 'function')\n expect(layouts).toHaveLength(1)\n const layout = layouts[0]\n if (!layout) throw new Error('Missing chart container')\n const handler = readProp(layout, 'onLayout')\n if (typeof handler !== 'function') throw new Error('Missing chart layout handler')\n\n for (const width of [268, 720, 300, 0, Number.NaN]) {\n await act(async () => {\n Reflect.apply(handler, undefined, [{ nativeEvent: { layout: { width } } }])\n await Promise.resolve()\n })\n const expectedWidth = width > 0 && Number.isFinite(width) ? width : 300\n const svg = findHostComponent(root, 'Svg')\n expect(readProp(svg, 'width')).toBe(expectedWidth)\n expect(readProp(svg, 'height')).toBe(Math.min(320, Math.max(220, expectedWidth * 0.75)))\n expect(root.container.queryAll(instance => instance.type === 'ScrollView')).toHaveLength(0)\n const bars = root.container.queryAll(instance => instance.type === 'Rect')\n expect(bars).toHaveLength(['bar', 'combo'].includes(kind) ? 5 : 0)\n for (const bar of bars) {\n const coordinates = /translate\\(([-\\d.]+) ([-\\d.]+)\\)/u.exec(String(readProp(bar, 'transform')))\n if (!coordinates?.[1]) throw new Error('Missing bar coordinates')\n expect(Number(coordinates[1])).toBeGreaterThanOrEqual(0)\n expect(Number(coordinates[1]) + Number(readProp(bar, 'width'))).toBeLessThanOrEqual(expectedWidth)\n }\n }\n }\n )\n\n test.each([\n { rtl: false, values: [20, 50, 120, 20, 120] },\n { rtl: true, values: [120, 100, 20, 120, 20] }\n ])('slider touch follows RTL=$rtl while accessibility values remain numeric', async ({ rtl, values }) => {\n nativeDirection.isRTL = rtl\n const onValueChange = vi.fn<(value: number) => void>()\n const root = await renderNative(\n <LumenSlider label=\"Volume\" min={20} max={120} step={10} value={70} onValueChange={onValueChange} />\n )\n const slider = findByAccessibilityRole(root, 'adjustable')\n expect(readProp(slider, 'accessible')).toBe(true)\n const dispatch = async (property: string, nativeEvent: unknown): Promise<void> => {\n const handler = readProp(slider, property)\n if (typeof handler !== 'function') throw new Error(`Missing slider ${property} callback`)\n await act(async () => {\n Reflect.apply(handler, undefined, [{ nativeEvent }])\n await Promise.resolve()\n })\n }\n await dispatch('onLayout', { layout: { width: 200 } })\n for (const [index, position] of [0, 50, 200, -50, 250].entries()) {\n for (const event of ['onResponderGrant', 'onResponderMove']) {\n await dispatch(event, { locationX: position })\n expect(onValueChange).toHaveBeenLastCalledWith(values[index])\n }\n }\n await dispatch('onAccessibilityAction', { actionName: 'increment' })\n expect(onValueChange).toHaveBeenLastCalledWith(80)\n await dispatch('onAccessibilityAction', { actionName: 'decrement' })\n expect(onValueChange).toHaveBeenLastCalledWith(60)\n expect(readProp(slider, 'accessibilityValue')).toMatchObject({ min: 20, max: 120, now: 70 })\n })\n\n test('range endpoints stay controlled, independently named and cannot cross', async () => {\n const onValueChange = vi.fn<(value: readonly [number, number]) => void>()\n const root = await renderNative(\n <LumenRangeSlider\n label=\"Capacity\"\n startLabel=\"Lower\"\n endLabel=\"Upper\"\n value={[20, 60]}\n min={0}\n max={100}\n step={10}\n onValueChange={onValueChange}\n formatValue={value => `${value}%`}\n />\n )\n const sliders = root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'adjustable')\n expect(sliders).toHaveLength(2)\n const [start, end] = sliders\n if (!start || !end) throw new Error('Missing interval endpoints')\n expect(readProp(start, 'accessibilityLabel')).toBe('Capacity · Lower')\n expect(readProp(end, 'accessibilityLabel')).toBe('Capacity · Upper')\n expect(readProp(start, 'accessibilityValue')).toMatchObject({ now: 20, text: '20%' })\n expect(readProp(start, 'aria-valuenow')).toBe(20)\n expect(readProp(start, 'aria-valuetext')).toBe('20%')\n const move = async (slider: TestInstance, direction: string): Promise<void> => {\n const handler = readProp(slider, 'onAccessibilityAction')\n if (typeof handler !== 'function') throw new Error('Missing range adjustment')\n await act(async () => {\n Reflect.apply(handler, undefined, [{ nativeEvent: { actionName: direction } }])\n await Promise.resolve()\n })\n }\n await move(start, 'increment')\n expect(onValueChange).toHaveBeenLastCalledWith([30, 60])\n await move(end, 'decrement')\n expect(onValueChange).toHaveBeenLastCalledWith([20, 50])\n // The application has not accepted either update; subsequent intent uses its controlled value.\n await move(start, 'increment')\n expect(onValueChange).toHaveBeenLastCalledWith([30, 60])\n await act(async () => {\n root.render(\n <LumenProvider>\n <LumenRangeSlider\n label=\"Capacity\"\n value={[60, 60]}\n min={0}\n max={100}\n step={10}\n onValueChange={onValueChange}\n />\n </LumenProvider>\n )\n await Promise.resolve()\n })\n const next = root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'adjustable')\n if (!next[0] || !next[1]) throw new Error('Missing updated interval endpoints')\n await move(next[0], 'increment')\n expect(onValueChange).toHaveBeenLastCalledWith([60, 60])\n await move(next[1], 'decrement')\n expect(onValueChange).toHaveBeenLastCalledWith([60, 60])\n })\n\n test.each([{ enabled: false, readOnly: false }, { enabled: true, readOnly: true }])(\n 'range endpoints reject touch and accessibility when enabled=$enabled readOnly=$readOnly',\n async ({ enabled, readOnly }) => {\n const onValueChange = vi.fn<(value: readonly [number, number]) => void>()\n const root = await renderNative(\n <LumenRangeSlider\n label=\"Capacity\"\n value={[20, 80]}\n enabled={enabled}\n readOnly={readOnly}\n onValueChange={onValueChange}\n />\n )\n const sliders = root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'adjustable')\n for (const slider of sliders) {\n expect(readProp(slider, 'accessibilityState')).toEqual({ disabled: true })\n for (const [event, nativeEvent] of [\n ['onAccessibilityAction', { actionName: 'increment' }],\n ['onResponderGrant', { locationX: 100 }],\n ['onResponderMove', { locationX: 100 }]\n ] as const) {\n const handler = readProp(slider, event)\n if (typeof handler !== 'function') throw new Error(`Missing ${event}`)\n await act(async () => {\n Reflect.apply(handler, undefined, [{ nativeEvent }])\n await Promise.resolve()\n })\n }\n }\n expect(onValueChange).not.toHaveBeenCalled()\n }\n )\n\n test('uses the Android accessibility timeout before dismissing a toast', async () => {\n nativePlatform.OS = 'android'\n vi.useFakeTimers()\n accessibilityTimeout.mockResolvedValue(20_000)\n const ref = createRef<LumenToastHookController>()\n\n await renderNative(<ToastProbe ref={ref} />)\n await act(async () => {\n readToastController(ref).create({ title: 'Saved' })\n await Promise.resolve()\n })\n\n expect(accessibilityTimeout).toHaveBeenCalledExactlyOnceWith(5000)\n\n await act(async () => {\n await vi.advanceTimersByTimeAsync(19_999)\n })\n expect(readToastController(ref).toasts).toHaveLength(1)\n\n await act(async () => {\n await vi.advanceTimersByTimeAsync(1)\n })\n expect(readToastController(ref).toasts).toHaveLength(0)\n })\n\n test.each(['shorter', 'invalid', 'oversized', 'rejected'])('retains the requested duration when Android returns a %s recommendation', async result => {\n nativePlatform.OS = 'android'\n vi.useFakeTimers()\n\n if (result === 'rejected') accessibilityTimeout.mockRejectedValue(new Error('Native timeout unavailable'))\n else if (result === 'shorter') accessibilityTimeout.mockResolvedValue(1)\n else if (result === 'oversized') accessibilityTimeout.mockResolvedValue(Number.MAX_SAFE_INTEGER)\n else accessibilityTimeout.mockResolvedValue(Number.NaN)\n\n const ref = createRef<LumenToastHookController>()\n\n await renderNative(<ToastProbe ref={ref} />)\n await act(async () => {\n readToastController(ref).create({ duration: 1000, title: 'Saved' })\n await Promise.resolve()\n })\n expect(accessibilityTimeout).toHaveBeenCalledExactlyOnceWith(1000)\n\n await act(async () => {\n await vi.advanceTimersByTimeAsync(999)\n })\n expect(readToastController(ref).toasts).toHaveLength(1)\n\n await act(async () => {\n await vi.advanceTimersByTimeAsync(1)\n })\n expect(readToastController(ref).toasts).toHaveLength(0)\n })\n\n test.each(['dismiss', 'clear', 'unmount', 'evict'])('ignores late Android timeout results after %s', async operation => {\n nativePlatform.OS = 'android'\n vi.useFakeTimers()\n const pending = deferredTimeout()\n\n accessibilityTimeout.mockReturnValueOnce(pending.promise)\n\n const ref = createRef<LumenToastHookController>()\n const root = await renderNative(<ToastProbe options={{ maxCount: 1 }} ref={ref} />)\n let id = ''\n\n await act(async () => {\n id = readToastController(ref).create({ title: 'Saved' })\n await Promise.resolve()\n })\n expect(accessibilityTimeout).toHaveBeenCalledExactlyOnceWith(5000)\n\n await act(async () => {\n if (operation === 'dismiss') readToastController(ref).dismiss(id)\n if (operation === 'clear') readToastController(ref).clear()\n if (operation === 'unmount') root.unmount()\n if (operation === 'evict') readToastController(ref).create({ duration: 0, title: 'Persistent replacement' })\n await Promise.resolve()\n })\n await act(async () => {\n pending.resolve(20_000)\n await Promise.resolve()\n })\n\n expect(vi.getTimerCount()).toBe(0)\n })\n\n test('ignores stale Android recommendations when a toast duration is updated', async () => {\n nativePlatform.OS = 'android'\n vi.useFakeTimers()\n const pending = deferredTimeout()\n\n accessibilityTimeout.mockReturnValueOnce(pending.promise).mockResolvedValueOnce(10_000)\n\n const ref = createRef<LumenToastHookController>()\n\n await renderNative(<ToastProbe ref={ref} />)\n await act(async () => {\n const id = readToastController(ref).create({ title: 'Saved' })\n\n readToastController(ref).update(id, { duration: 1000 })\n await Promise.resolve()\n })\n expect(accessibilityTimeout).toHaveBeenNthCalledWith(1, 5000)\n expect(accessibilityTimeout).toHaveBeenNthCalledWith(2, 1000)\n expect(accessibilityTimeout).toHaveBeenCalledTimes(2)\n\n await act(async () => {\n pending.resolve(100_000)\n await Promise.resolve()\n await vi.advanceTimersByTimeAsync(10_000)\n })\n\n expect(readToastController(ref).toasts).toHaveLength(0)\n expect(vi.getTimerCount()).toBe(0)\n })\n\n test('keeps explicitly persistent Android toasts without requesting a timeout', async () => {\n nativePlatform.OS = 'android'\n vi.useFakeTimers()\n const ref = createRef<LumenToastHookController>()\n\n await renderNative(<ToastProbe ref={ref} />)\n await act(async () => {\n for (const duration of [0, -1, Number.POSITIVE_INFINITY]) readToastController(ref).create({ duration, title: 'Saved' })\n await vi.advanceTimersByTimeAsync(100_000)\n })\n\n expect(readToastController(ref).toasts).toHaveLength(3)\n expect(accessibilityTimeout).not.toHaveBeenCalled()\n expect(vi.getTimerCount()).toBe(0)\n })\n\n test.each(['ios', 'web'])('preserves the requested %s toast duration without Android timeout calls', async platform => {\n nativePlatform.OS = platform\n vi.useFakeTimers()\n const ref = createRef<LumenToastHookController>()\n\n await renderNative(<ToastProbe ref={ref} />)\n await act(async () => {\n readToastController(ref).create({ duration: 1000, title: 'Saved' })\n await vi.advanceTimersByTimeAsync(1000)\n })\n\n expect(readToastController(ref).toasts).toHaveLength(0)\n expect(accessibilityTimeout).not.toHaveBeenCalled()\n })\n\n test('keeps enlarged navigation labels readable and preserves destination semantics', async () => {\n const select = vi.fn()\n const reselect = vi.fn()\n const items = [\n { label: 'Espacios de trabajo', value: 'workspaces' },\n { label: 'Configuración', value: 'settings' },\n { disabled: true, label: 'Archivo', value: 'archive' }\n ]\n const navigation = <LumenNavigationBar items={items} onReselect={reselect} onValueChange={select} value=\"workspaces\" />\n const root = await renderNative(navigation)\n const labels = () => root.container.queryAll(instance => instance.type === 'Text')\n expect(labels().map(label => readProp(label, 'numberOfLines'))).toEqual([1, 1, 1])\n\n nativeWindow.fontScale = 3\n await act(async () => {\n root.render(<LumenProvider scheme=\"light\"><LumenNavigationBar items={items} onReselect={reselect} onValueChange={select} value=\"workspaces\" /></LumenProvider>)\n await Promise.resolve()\n })\n expect(labels().every(label => readProp(label, 'numberOfLines') === undefined)).toBe(true)\n expect(labels().every(label => {\n const style = readProp(label, 'style')\n return typeof style === 'object' && style !== null && 'maxWidth' in style && style.maxWidth === '100%'\n })).toBe(true)\n const selected = findByAccessibilityLabel(root, 'Espacios de trabajo')\n expect(readProp(selected, 'accessibilityState')).toMatchObject({ selected: true })\n callAction(readProp(selected, 'onPress'), 'Missing reselect action')\n expect(reselect).toHaveBeenCalledWith('workspaces')\n const settings = findByAccessibilityLabel(root, 'Configuración')\n callAction(readProp(settings, 'onPress'), 'Missing destination action')\n expect(select).toHaveBeenCalledWith('settings')\n expect(readProp(findByAccessibilityLabel(root, 'Archivo'), 'disabled')).toBe(true)\n })\n\n test('keeps alert actions inside safe areas and makes oversized content scrollable', async () => {\n const root = await renderNative(\n <LumenAlertDialog\n confirmLabel=\"Delete\"\n onConfirm={() => {}}\n onDismiss={() => {}}\n safeAreaInsets={safeAreaInsets}\n title=\"Delete project?\"\n visible\n />\n )\n const modalContainer = root.container.queryAll(\n instance => readProp(instance, 'accessibilityViewIsModal') === true\n )[0]\n\n if (!modalContainer) throw new Error('Expected the alert modal container.')\n\n expect(readProp(modalContainer, 'style')).toMatchObject({\n paddingBottom: 58,\n paddingLeft: 32,\n paddingRight: 36,\n paddingTop: 44\n })\n expect(readProp(findByAccessibilityRole(root, 'alert'), 'style')).toMatchObject({\n maxHeight: '100%'\n })\n })\n\n test('keeps sheet actions above the bottom inset and scrolls application content', async () => {\n const root = await renderNative(\n <LumenSheet\n actions={<LumenButton>Save</LumenButton>}\n onDismiss={() => {}}\n safeAreaInsets={safeAreaInsets}\n scrollable\n title=\"Settings\"\n visible\n >\n <LumenText>Sheet content</LumenText>\n </LumenSheet>\n )\n const keyboardSurface = root.container.queryAll(\n instance => instance.type === 'KeyboardAvoidingView'\n )[0]\n const sheetPanel = root.container.queryAll(instance => {\n const style = readProp(instance, 'style')\n\n return typeof style === 'object' &&\n style !== null &&\n 'paddingBottom' in style &&\n style.paddingBottom === 34\n })[0]\n\n if (!keyboardSurface || !sheetPanel) throw new Error('Expected the sheet surfaces.')\n\n expect(readProp(keyboardSurface, 'style')).toMatchObject({\n paddingBottom: 0,\n paddingLeft: 8,\n paddingRight: 12,\n paddingTop: 20\n })\n expect(readProp(sheetPanel, 'style')).toMatchObject({ paddingBottom: 34 })\n expect(root.container.queryAll(instance => instance.type === 'ScrollView')).toHaveLength(1)\n })\n\n test('lets virtualized sheet content own scrolling', async () => {\n const root = await renderNative(\n <LumenSheet onDismiss={() => {}} scrollable={false} visible>\n <LumenText>Virtualized content</LumenText>\n </LumenSheet>\n )\n\n expect(root.container.queryAll(instance => instance.type === 'ScrollView')).toHaveLength(0)\n })\n\n test('button exposes loading as busy and disabled native state', async () => {\n const root = await renderNative(<LumenButton loading>Save changes</LumenButton>)\n const button = findByAccessibilityRole(root, 'button')\n\n expect(readProp(button, 'disabled')).toBe(true)\n expect(readProp(button, 'accessibilityState')).toEqual({\n busy: true,\n disabled: true\n })\n })\n\n test('button preserves its native role when a consumer supplies another role', async () => {\n const root = await renderNative(\n <LumenButton accessibilityRole=\"link\">Open workspace</LumenButton>\n )\n\n expect(findByAccessibilityRole(root, 'button')).toBeDefined()\n expect(root.container.queryAll(\n instance => readProp(instance, 'accessibilityRole') === 'link'\n )).toHaveLength(0)\n })\n\n test('text inputs expose disabled native state without dropping caller state', async () => {\n const textFieldRoot = await renderNative(\n <LumenTextField\n accessibilityLabel=\"Project name\"\n accessibilityState={{ busy: true }}\n editable={false}\n />\n )\n const textareaRoot = await renderNative(\n <LumenTextarea\n accessibilityState={{ busy: true }}\n editable={false}\n label=\"Project notes\"\n onChangeText={() => {}}\n value=\"\"\n />\n )\n const searchRoot = await renderNative(\n <LumenSearchField\n accessibilityState={{ busy: true }}\n editable={false}\n onChangeText={() => {}}\n prompt=\"Search projects\"\n value=\"\"\n />\n )\n\n for (const [root, label] of [\n [textFieldRoot, 'Project name'],\n [textareaRoot, 'Project notes'],\n [searchRoot, 'Search projects']\n ] as const) {\n expect(readProp(findByAccessibilityLabel(root, label), 'accessibilityState')).toEqual({\n busy: true,\n disabled: true\n })\n }\n })\n\n test('validation is exposed on the native controls without dropping caller context', async () => {\n const textFieldRoot = await renderNative(\n <LumenTextField accessibilityLabel=\"Project name\" error />\n )\n const textareaRoot = await renderNative(\n <LumenTextarea\n accessibilityHint=\"Consumer guidance\"\n errorMessage=\"Project notes are required\"\n label=\"Project notes\"\n onChangeText={() => {}}\n value=\"\"\n />\n )\n const dateRoot = await renderNative(\n <LumenDateField\n errorMessage=\"Choose a valid birthday\"\n label=\"Birthday\"\n onValueChange={() => {}}\n value={null}\n />\n )\n\n const textField = findByAccessibilityLabel(textFieldRoot, 'Project name')\n const textarea = findByAccessibilityLabel(textareaRoot, 'Project notes')\n const dateButton = findByAccessibilityLabel(dateRoot, 'Birthday, Select a date')\n\n expect(readProp(textField, 'aria-invalid')).toBe(true)\n expect(readProp(textarea, 'aria-invalid')).toBe(true)\n expect(readProp(textarea, 'accessibilityHint')).toBe('Project notes are required')\n expect(readProp(dateButton, 'aria-invalid')).toBe(true)\n expect(readProp(dateButton, 'accessibilityHint')).toBe('Choose a valid birthday')\n })\n\n test('grouped fields expose required and range validation context', async () => {\n const fieldGroupRoot = await renderNative(\n <LumenFieldGroup label=\"Project details\" required>\n <LumenText>Details</LumenText>\n </LumenFieldGroup>\n )\n const dateRangeRoot = await renderNative(\n <LumenDateRangeField\n accessibilityHint=\"Consumer guidance\"\n errorMessage=\"Choose a valid schedule\"\n label=\"Schedule\"\n onValueChange={() => {}}\n value={{ end: null, start: null }}\n />\n )\n\n expect(findByAccessibilityLabel(fieldGroupRoot, 'Project details, required')).toBeDefined()\n\n const dateRange = findByAccessibilityLabel(dateRangeRoot, 'Schedule')\n\n expect(readProp(dateRange, 'aria-invalid')).toBe(true)\n expect(readProp(dateRange, 'accessibilityHint')).toBe('Choose a valid schedule')\n })\n\n test('pickers inside field groups announce the group error and description', async () => {\n const root = await renderNative(\n <>\n <LumenFieldGroup description=\"Used for billing\" errorMessage=\"Choose a plan\" label=\"Plan\" required>\n <LumenPicker label=\"Plan\" onValueChange={() => {}} options={[{ label: 'Free', value: 'free' }]} value=\"free\" />\n </LumenFieldGroup>\n <LumenFieldGroup description=\"Shown on invoices\" label=\"Currency\">\n <LumenPicker label=\"Currency\" onValueChange={() => {}} options={[{ label: 'USD', value: 'usd' }]} value=\"usd\" />\n </LumenFieldGroup>\n <LumenPicker label=\"Region\" onValueChange={() => {}} options={[{ label: 'EU', value: 'eu' }]} value=\"eu\" />\n </>\n )\n const [invalidPicker, describedPicker, standalonePicker] = root.container.queryAll(\n instance => readProp(instance, 'accessibilityRole') === 'combobox'\n )\n\n expect(invalidPicker && readProp(invalidPicker, 'accessibilityHint')).toBe('Choose a plan')\n expect(invalidPicker && readProp(invalidPicker, 'aria-invalid')).toBe(true)\n expect(describedPicker && readProp(describedPicker, 'accessibilityHint')).toBe('Shown on invoices')\n expect(describedPicker && readProp(describedPicker, 'aria-invalid')).toBeUndefined()\n expect(standalonePicker && readProp(standalonePicker, 'accessibilityHint')).toBeUndefined()\n expect(standalonePicker && readProp(standalonePicker, 'aria-invalid')).toBeUndefined()\n })\n\n test('localizes required-field descriptions without exposing them on optional fields', async () => {\n const root = await renderNative(\n <>\n <LumenFieldGroup label=\"Nombre\" required requiredLabel=\"obligatorio\"><LumenTextField /></LumenFieldGroup>\n <LumenFieldGroup label=\"Notas\" requiredLabel=\"obligatorio\"><LumenTextField /></LumenFieldGroup>\n </>\n )\n\n expect(findByAccessibilityLabel(root, 'Nombre, obligatorio')).toBeDefined()\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityLabel') === 'Notas, obligatorio')).toHaveLength(0)\n expect(findByAccessibilityLabel(root, 'Notas')).toBeDefined()\n })\n\n test('field groups provide inherited native and web relationships without overriding inputs', async () => {\n const root = await renderNative(\n <LumenFieldGroup\n description=\"Shown on the public profile\"\n errorMessage=\"Choose a unique name\"\n label=\"Project name\"\n required\n >\n <LumenText>Nested row content</LumenText>\n <LumenTextField />\n <LumenTextField accessibilityLabel=\"Short project name\" />\n </LumenFieldGroup>\n )\n const inputs = root.container.queryAll(instance => instance.type === 'TextInput')\n const inherited = inputs[0]\n const explicit = inputs[1]\n\n if (!inherited || !explicit) throw new Error('Expected two grouped text fields.')\n\n expect(readProp(inherited, 'accessibilityLabel')).toBe('Project name')\n expect(readProp(inherited, 'aria-labelledby')).toMatch(/-label$/u)\n expect(readProp(inherited, 'aria-describedby')).toMatch(/-description .*?-error$/u)\n expect(readProp(inherited, 'aria-required')).toBe(true)\n expect(readProp(inherited, 'aria-invalid')).toBe(true)\n expect(readProp(explicit, 'accessibilityLabel')).toBe('Short project name')\n expect(readProp(explicit, 'aria-labelledby')).toBeUndefined()\n\n const validationMessages = root.container.queryAll(\n instance => readProp(instance, 'children') === 'Choose a unique name'\n )\n\n expect(validationMessages).toHaveLength(1)\n\n const validationMessage = validationMessages[0]\n\n if (!validationMessage) throw new Error('Expected the grouped validation message.')\n\n expect(readProp(validationMessage, 'accessibilityLiveRegion')).toBe('polite')\n expect(readProp(validationMessage, 'accessibilityRole')).toBe('alert')\n })\n\n test('read-only phone input locks country selection and number editing', async () => {\n const country = getLumenPhoneCountry('CO')\n if (!country) throw new Error('Missing phone fixture')\n const root = await renderNative(<LumenPhoneInput label=\"Phone\" onValueChange={() => {}} readOnly value={resolveLumenPhoneInputValue([country], country, '', {})} />)\n expect(readProp(findByAccessibilityLabel(root, 'Country code, Colombia, +57'), 'disabled')).toBe(true)\n expect(readProp(findByAccessibilityLabel(root, 'Phone number'), 'editable')).toBe(false)\n })\n\n test('phone input exposes disabled state on both native controls', async () => {\n const country = getLumenPhoneCountry('US')\n\n expect(country).toBeDefined()\n\n if (!country) return\n\n const value = resolveLumenPhoneInputValue([country], country, '', {})\n const root = await renderNative(\n <LumenPhoneInput\n countries={[country]}\n enabled={false}\n label=\"Phone\"\n onValueChange={() => {}}\n value={value}\n />\n )\n const countrySelector = findByAccessibilityLabel(root, 'Country code, United States, +1')\n const numberInput = findByAccessibilityLabel(root, 'Phone number')\n\n expect(readProp(countrySelector, 'accessibilityState')).toEqual({\n disabled: true,\n expanded: false\n })\n expect(readProp(numberInput, 'accessibilityState')).toEqual({ disabled: true })\n })\n\n test('phone input exposes validation on the number editor', async () => {\n const country = getLumenPhoneCountry('US')\n\n expect(country).toBeDefined()\n\n if (!country) return\n\n const value = resolveLumenPhoneInputValue([country], country, '415', {})\n const root = await renderNative(\n <LumenPhoneInput\n countries={[country]}\n errorMessage=\"Enter a complete phone number\"\n label=\"Phone\"\n onValueChange={() => {}}\n value={value}\n />\n )\n const numberInput = findByAccessibilityLabel(root, 'Phone number')\n\n expect(readProp(numberInput, 'aria-invalid')).toBe(true)\n expect(readProp(numberInput, 'accessibilityHint')).toBe('Enter a complete phone number')\n })\n\n test('phone input exposes an empty country allow-list as a disabled selector', async () => {\n const country = getLumenPhoneCountry('US')\n\n expect(country).toBeDefined()\n\n if (!country) return\n\n const value = resolveLumenPhoneInputValue([country], country, '', {})\n const root = await renderNative(\n <LumenPhoneInput\n countries={[]}\n label=\"Phone\"\n onValueChange={() => {}}\n value={value}\n />\n )\n const countrySelector = findByAccessibilityLabel(root, 'Country code, United States, +1')\n\n expect(readProp(countrySelector, 'disabled')).toBe(true)\n expect(readProp(countrySelector, 'accessibilityState')).toEqual({\n disabled: true,\n expanded: false\n })\n })\n\n test('generic picker disables empty options and does not reopen after disabling', async () => {\n const onValueChange = vi.fn<(value: string) => void>()\n const root = await renderNative(\n <LumenPicker\n label=\"Workspace\"\n onValueChange={onValueChange}\n options={[]}\n value=\"none\"\n />\n )\n let trigger = findByAccessibilityLabel(root, 'Workspace')\n\n expect(readProp(trigger, 'disabled')).toBe(true)\n expect(readProp(trigger, 'accessibilityState')).toEqual({\n disabled: true,\n expanded: false\n })\n\n await act(async () => {\n root.render(\n <LumenProvider scheme=\"light\">\n <LumenPicker\n label=\"Workspace\"\n onValueChange={onValueChange}\n options={[{ label: 'Design', value: 'design' }]}\n value=\"design\"\n />\n </LumenProvider>\n )\n await Promise.resolve()\n })\n\n trigger = findByAccessibilityLabel(root, 'Workspace')\n\n await act(async () => {\n callAction(readProp(trigger, 'onPress'), 'Picker is missing its native press action.')\n await Promise.resolve()\n })\n\n expect(findByAccessibilityRole(root, 'menu')).toBeDefined()\n\n await act(async () => {\n root.render(\n <LumenProvider scheme=\"light\">\n <LumenPicker\n enabled={false}\n label=\"Workspace\"\n onValueChange={onValueChange}\n options={[{ label: 'Design', value: 'design' }]}\n value=\"design\"\n />\n </LumenProvider>\n )\n await Promise.resolve()\n })\n\n expect(root.container.queryAll(\n instance => readProp(instance, 'accessibilityRole') === 'menu'\n )).toHaveLength(0)\n\n await act(async () => {\n root.render(\n <LumenProvider scheme=\"light\">\n <LumenPicker\n label=\"Workspace\"\n onValueChange={onValueChange}\n options={[{ label: 'Design', value: 'design' }]}\n value=\"design\"\n />\n </LumenProvider>\n )\n await Promise.resolve()\n })\n\n expect(root.container.queryAll(\n instance => readProp(instance, 'accessibilityRole') === 'menu'\n )).toHaveLength(0)\n })\n\n test('date picker does not reopen after the field is disabled and re-enabled', async () => {\n const root = await renderNative(\n <LumenDateField label=\"Birthday\" onValueChange={() => {}} value={null} />\n )\n const trigger = findByAccessibilityLabel(root, 'Birthday, Select a date')\n\n await act(async () => {\n callAction(readProp(trigger, 'onPress'), 'Date field is missing its native press action.')\n await Promise.resolve()\n })\n\n expect(findHostComponent(root, 'NativeDatePicker')).toBeDefined()\n\n await act(async () => {\n root.render(\n <LumenProvider scheme=\"light\">\n <LumenDateField enabled={false} label=\"Birthday\" onValueChange={() => {}} value={null} />\n </LumenProvider>\n )\n await Promise.resolve()\n })\n\n expect(root.container.queryAll(instance => instance.type === 'NativeDatePicker')).toHaveLength(0)\n\n await act(async () => {\n root.render(\n <LumenProvider scheme=\"light\">\n <LumenDateField label=\"Birthday\" onValueChange={() => {}} value={null} />\n </LumenProvider>\n )\n await Promise.resolve()\n })\n\n expect(root.container.queryAll(instance => instance.type === 'NativeDatePicker')).toHaveLength(0)\n })\n\n test('disabling an Android date field dismisses its imperative native dialog', async () => {\n nativePlatform.OS = 'android'\n\n try {\n const root = await renderNative(\n <LumenDateField label=\"Birthday\" onValueChange={() => {}} value={null} />\n )\n const trigger = findByAccessibilityLabel(root, 'Birthday, Select a date')\n\n await act(async () => {\n callAction(readProp(trigger, 'onPress'), 'Date field is missing its native press action.')\n await Promise.resolve()\n })\n\n expect(DateTimePickerAndroid.open).toHaveBeenCalledOnce()\n\n await act(async () => {\n root.render(\n <LumenProvider scheme=\"light\">\n <LumenDateField enabled={false} label=\"Birthday\" onValueChange={() => {}} value={null} />\n </LumenProvider>\n )\n await Promise.resolve()\n })\n\n expect(DateTimePickerAndroid.dismiss).toHaveBeenCalledWith('date')\n } finally {\n nativePlatform.OS = 'ios'\n }\n })\n\n test('phone picker closes and clears its query when the input becomes disabled', async () => {\n const country = getLumenPhoneCountry('US')\n\n expect(country).toBeDefined()\n\n if (!country) return\n\n const value = resolveLumenPhoneInputValue([country], country, '', {})\n const renderPhone = (enabled: boolean): ReactElement => (\n <LumenProvider scheme=\"light\">\n <LumenPhoneInput\n countries={[country]}\n enabled={enabled}\n label=\"Phone\"\n onValueChange={() => {}}\n value={value}\n />\n </LumenProvider>\n )\n const root = await renderNative(\n <LumenPhoneInput\n countries={[country]}\n label=\"Phone\"\n onValueChange={() => {}}\n value={value}\n />\n )\n const selector = findByAccessibilityLabel(root, 'Country code, United States, +1')\n\n await act(async () => {\n callAction(readProp(selector, 'onPress'), 'Country selector is missing its native press action.')\n await Promise.resolve()\n })\n\n expect(readProp(findHostComponent(root, 'Modal'), 'visible')).toBe(true)\n expect(readProp(findByAccessibilityLabel(root, 'Search countries'), 'editable')).toBe(true)\n\n await act(async () => {\n const onChangeText = readProp(findByAccessibilityLabel(root, 'Search countries'), 'onChangeText')\n\n if (typeof onChangeText !== 'function') throw new Error('Country search is missing its change action.')\n\n Reflect.apply(onChangeText, undefined, ['united'])\n await Promise.resolve()\n })\n\n await act(async () => {\n root.render(renderPhone(false))\n await Promise.resolve()\n })\n\n expect(readProp(findHostComponent(root, 'Modal'), 'visible')).toBe(false)\n expect(readProp(findByAccessibilityLabel(root, 'Country code, United States, +1'), 'accessibilityState'))\n .toEqual({ disabled: true, expanded: false })\n\n await act(async () => {\n root.render(renderPhone(true))\n await Promise.resolve()\n })\n\n expect(readProp(findByAccessibilityLabel(root, 'Country code, United States, +1'), 'accessibilityState'))\n .toEqual({ disabled: false, expanded: false })\n\n const reopenedSelector = findByAccessibilityLabel(root, 'Country code, United States, +1')\n\n await act(async () => {\n callAction(\n readProp(reopenedSelector, 'onPress'),\n 'Re-enabled country selector is missing its native press action.'\n )\n await Promise.resolve()\n })\n\n expect(readProp(findByAccessibilityLabel(root, 'Search countries'), 'value')).toBe('')\n })\n\n test('chip removal exposes the independent action disabled state', async () => {\n const root = await renderNative(\n <LumenChip disabled label=\"Design\" onRemove={() => {}} />\n )\n const removeButton = findByAccessibilityLabel(root, 'Remove Design')\n\n expect(readProp(removeButton, 'accessibilityRole')).toBe('button')\n expect(readProp(removeButton, 'disabled')).toBe(true)\n expect(readProp(removeButton, 'accessibilityState')).toEqual({ disabled: true })\n expect(readProp(removeButton, 'style')).toMatchObject({\n alignItems: 'center',\n justifyContent: 'center',\n minHeight: 24,\n minWidth: 24\n })\n })\n\n test('toast announces politely and keeps dismissal independently operable', async () => {\n const onDismiss = vi.fn<() => void>()\n const root = await renderNative(\n <LumenToast dismissLabel=\"Close update\" onDismiss={onDismiss} title=\"Changes saved\" />\n )\n const alert = findByAccessibilityRole(root, 'alert')\n const dismissButton = findByAccessibilityLabel(root, 'Close update')\n const style = readProp(dismissButton, 'style')\n\n expect(readProp(alert, 'accessibilityLiveRegion')).toBe('polite')\n expect(readProp(dismissButton, 'accessibilityRole')).toBe('button')\n expect(style).toBeTypeOf('function')\n\n if (typeof style !== 'function') throw new Error('Toast dismissal is missing its pressable style.')\n\n const restingStyle: unknown = Reflect.apply(style, undefined, [{ pressed: false }])\n\n expect(restingStyle).toMatchObject({\n alignItems: 'center',\n justifyContent: 'center',\n minHeight: 44,\n minWidth: 44\n })\n\n await act(async () => {\n callAction(readProp(dismissButton, 'onPress'), 'Toast dismissal is missing its press action.')\n await Promise.resolve()\n })\n\n expect(onDismiss).toHaveBeenCalledOnce()\n })\n\n test('queues localized iOS toast copy once and announces changed content', async () => {\n const toast = (description: string) => (\n <StrictMode><LumenToast description={description} title=\"Cambios guardados\" /></StrictMode>\n )\n const root = await renderNative(toast('Registro actualizado'))\n\n expect(accessibilityAnnouncement).toHaveBeenCalledExactlyOnceWith('Cambios guardados. Registro actualizado', { queue: true })\n\n await act(async () => {\n root.render(<LumenProvider scheme=\"light\">{toast('Registro actualizado')}</LumenProvider>)\n await Promise.resolve()\n })\n\n expect(accessibilityAnnouncement).toHaveBeenCalledOnce()\n\n await act(async () => {\n root.render(<LumenProvider scheme=\"light\">{toast('Otro registro actualizado')}</LumenProvider>)\n await Promise.resolve()\n })\n\n expect(accessibilityAnnouncement).toHaveBeenLastCalledWith('Cambios guardados. Otro registro actualizado', { queue: true })\n expect(accessibilityAnnouncement).toHaveBeenCalledTimes(2)\n })\n\n test('honors iOS error announcement urgency and off without speaking diagnostic references', async () => {\n const root = await renderNative(<LumenErrorState announcement=\"assertive\" description=\"Try again\" reference=\"request-123\" title=\"Unable to save\" />)\n\n expect(accessibilityAnnouncement).toHaveBeenCalledExactlyOnceWith('Unable to save. Try again', { queue: false })\n\n await act(async () => {\n root.render(<LumenProvider scheme=\"light\"><LumenErrorState announcement=\"off\" title=\"Unable to save\" /></LumenProvider>)\n await Promise.resolve()\n })\n\n expect(accessibilityAnnouncement).toHaveBeenCalledOnce()\n\n await act(async () => {\n root.render(<LumenProvider scheme=\"light\"><LumenErrorState announcement=\"polite\" title=\"Unable to save\" /></LumenProvider>)\n await Promise.resolve()\n })\n\n expect(accessibilityAnnouncement).toHaveBeenLastCalledWith('Unable to save', { queue: true })\n expect(accessibilityAnnouncement).toHaveBeenCalledTimes(2)\n })\n\n test('does not announce initially disabled errors or empty toast copy on iOS', async () => {\n await renderNative(\n <>\n <LumenErrorState announcement=\"off\" title=\"Unable to save\" />\n <LumenToast title=\" \" />\n </>\n )\n\n expect(accessibilityAnnouncement).not.toHaveBeenCalled()\n })\n\n test.each(['android', 'web'])('keeps %s live regions without duplicate imperative announcements', async platform => {\n nativePlatform.OS = platform\n const root = await renderNative(\n <>\n <LumenToast title=\"Saved\" />\n <LumenErrorState announcement=\"assertive\" title=\"Unable to save\" />\n </>\n )\n\n expect(readProp(findByAccessibilityRole(root, 'alert'), 'accessibilityLiveRegion')).toBe('polite')\n expect(readProp(findByAccessibilityRole(root, 'summary'), 'accessibilityLiveRegion')).toBe('assertive')\n expect(accessibilityAnnouncement).not.toHaveBeenCalled()\n })\n\n test('banner dismissal has a full independent touch target', async () => {\n const root = await renderNative(\n <LumenBanner dismissLabel=\"Close notice\" onDismiss={() => {}} title=\"Maintenance soon\" />\n )\n const dismissButton = findByAccessibilityLabel(root, 'Close notice')\n const style = readProp(dismissButton, 'style')\n\n expect(readProp(dismissButton, 'accessibilityRole')).toBe('button')\n expect(style).toBeTypeOf('function')\n\n if (typeof style !== 'function') throw new Error('Banner dismissal is missing its pressable style.')\n\n const restingStyle: unknown = Reflect.apply(style, undefined, [{ pressed: false }])\n\n expect(restingStyle).toMatchObject({\n alignItems: 'center',\n justifyContent: 'center',\n minHeight: 44,\n minWidth: 44\n })\n })\n\n test('status bars allow long messages to wrap without shrinking trailing content', async () => {\n const message = 'El análisis permanece en el dispositivo para proteger tu privacidad.'\n const root = await renderNative(\n <LumenProvider>\n <LumenStatusBar\n message={message}\n trailing={<LumenText>Analyze</LumenText>}\n />\n </LumenProvider>\n )\n const messageNodes = root.container.queryAll(\n instance => instance.type === 'Text' && readProp(instance, 'children') === message\n )\n\n expect(messageNodes).toHaveLength(1)\n\n const messageNode = messageNodes[0]\n\n if (!messageNode) throw new Error('Expected the status message fixture.')\n\n expect(readProp(messageNode, 'numberOfLines')).toBeUndefined()\n expect(readProp(messageNode, 'style')).toMatchObject({ flex: 1, flexShrink: 1 })\n\n const fixedTrailingContainers = root.container.queryAll(instance => {\n const style = readProp(instance, 'style')\n\n return instance.type === 'View' && typeof style === 'object' && style !== null &&\n Reflect.get(style, 'flexShrink') === 0\n })\n\n expect(fixedTrailingContainers).toHaveLength(1)\n })\n\n test('search clear action preserves its label and full touch target', async () => {\n const onChangeText = vi.fn<(value: string) => void>()\n const root = await renderNative(\n <LumenSearchField\n clearLabel=\"Clear query\"\n onChangeText={onChangeText}\n value=\"lumen\"\n />\n )\n const clearButton = findByAccessibilityLabel(root, 'Clear query')\n const style = readProp(clearButton, 'style')\n\n expect(readProp(clearButton, 'accessibilityRole')).toBe('button')\n expect(style).toBeTypeOf('function')\n\n if (typeof style !== 'function') throw new Error('Clear action is missing its pressable style.')\n\n const restingStyle: unknown = Reflect.apply(style, undefined, [{ pressed: false }])\n\n expect(restingStyle).toMatchObject({\n alignItems: 'center',\n justifyContent: 'center',\n minHeight: 44,\n minWidth: 44\n })\n\n await act(async () => {\n callAction(readProp(clearButton, 'onPress'), 'Clear action is missing its native press action.')\n await Promise.resolve()\n })\n\n expect(onChangeText).toHaveBeenCalledExactlyOnceWith('')\n })\n\n test('checkbox announces state and emits the next controlled value', async () => {\n const onCheckedChange = vi.fn<(checked: boolean) => void>()\n const root = await renderNative(\n <LumenCheckbox\n checked={false}\n label=\"Include diagnostics\"\n onCheckedChange={onCheckedChange}\n />\n )\n const checkbox = findByAccessibilityRole(root, 'checkbox')\n const onPress = readProp(checkbox, 'onPress')\n\n expect(readProp(checkbox, 'accessibilityLabel')).toBe('Include diagnostics')\n expect(readProp(checkbox, 'aria-checked')).toBe(false)\n expect(readProp(checkbox, 'aria-disabled')).toBe(false)\n\n expect(readProp(checkbox, 'accessibilityState')).toEqual({\n checked: false,\n disabled: false\n })\n expect(onPress).toBeTypeOf('function')\n\n await act(async () => {\n callAction(onPress, 'Checkbox is missing its native press action.')\n await Promise.resolve()\n })\n\n expect(onCheckedChange).toHaveBeenCalledExactlyOnceWith(true)\n })\n\n test('checkbox preserves a host-provided accessible name', async () => {\n const root = await renderNative(\n <LumenCheckbox checked={false} label=\"Diagnostics\" accessibilityLabel=\"Include diagnostic report\" onCheckedChange={() => undefined} />\n )\n\n expect(readProp(findByAccessibilityRole(root, 'checkbox'), 'accessibilityLabel')).toBe('Include diagnostic report')\n })\n\n test('checkbox announces its visible description as a hint like toggle', async () => {\n const root = await renderNative(\n <>\n <LumenCheckbox\n checked={false}\n description=\"Attach logs to the support ticket\"\n label=\"Include diagnostics\"\n onCheckedChange={() => undefined}\n />\n <LumenCheckbox\n accessibilityHint=\"Consumer guidance\"\n checked={false}\n description=\"Visible guidance\"\n label=\"Share usage\"\n onCheckedChange={() => undefined}\n />\n <LumenCheckbox\n accessibilityHint=\"\"\n checked={false}\n description=\"Visible guidance\"\n label=\"Quiet hints\"\n onCheckedChange={() => undefined}\n />\n </>\n )\n\n expect(readProp(findByAccessibilityLabel(root, 'Include diagnostics'), 'accessibilityHint')).toBe('Attach logs to the support ticket')\n expect(readProp(findByAccessibilityLabel(root, 'Share usage'), 'accessibilityHint')).toBe('Consumer guidance')\n expect(readProp(findByAccessibilityLabel(root, 'Quiet hints'), 'accessibilityHint')).toBe('')\n })\n\n test('toggle makes the full labeled row the single interactive switch', async () => {\n const onValueChange = vi.fn<(value: boolean) => void>()\n const root = await renderNative(\n <LumenToggle\n description=\"Receive product and security updates\"\n label=\"Automatic updates\"\n onValueChange={onValueChange}\n value={false}\n />\n )\n const toggle = findByAccessibilityRole(root, 'switch')\n const onPress = readProp(toggle, 'onPress')\n const nativeSwitches = root.container.queryAll(instance => instance.type === 'Switch')\n\n expect(readProp(toggle, 'accessibilityLabel')).toBe('Automatic updates')\n expect(readProp(toggle, 'accessibilityHint')).toBe('Receive product and security updates')\n expect(readProp(toggle, 'aria-checked')).toBe(false)\n expect(readProp(toggle, 'accessibilityState')).toEqual({\n checked: false,\n disabled: false\n })\n expect(nativeSwitches).toHaveLength(1)\n\n const nativeSwitch = nativeSwitches[0]\n\n if (!nativeSwitch) throw new Error('Expected the decorative native switch.')\n\n expect(readProp(nativeSwitch, 'accessibilityElementsHidden')).toBe(true)\n expect(readProp(nativeSwitch, 'importantForAccessibility')).toBe('no-hide-descendants')\n expect(readProp(nativeSwitch, 'pointerEvents')).toBe('none')\n expect(readProp(nativeSwitch, 'onValueChange')).toBeUndefined()\n\n await act(async () => {\n callAction(onPress, 'Toggle row is missing its press action.')\n await Promise.resolve()\n })\n\n expect(onValueChange).toHaveBeenCalledExactlyOnceWith(true)\n })\n\n test('web toggle keeps the decorative native input out of the tab order', async () => {\n nativePlatform.OS = 'web'\n const onValueChange = vi.fn()\n const root = await renderNative(<LumenToggle label=\"Updates\" onValueChange={onValueChange} value={false} />)\n const control = root.container.queryAll(instance => instance.type === 'Switch')[0]\n if (!control) throw new Error('Missing decorative switch')\n expect(readProp(control, 'disabled')).toBe(true)\n expect(readProp(control, 'aria-hidden')).toBe(true)\n const row = findByAccessibilityRole(root, 'switch')\n expect(readProp(row, 'disabled')).toBe(false)\n await act(async () => {\n callAction(readProp(row, 'onPress'), 'Missing row action')\n await Promise.resolve()\n })\n expect(onValueChange).toHaveBeenCalledExactlyOnceWith(true)\n nativePlatform.OS = 'ios'\n })\n\n test('toggle preserves a consumer accessibility hint over supporting copy', async () => {\n const root = await renderNative(\n <LumenToggle\n accessibilityHint=\"Changes how updates are installed\"\n description=\"Receive product and security updates\"\n label=\"Automatic updates\"\n onValueChange={() => {}}\n value={false}\n />\n )\n\n expect(readProp(findByAccessibilityRole(root, 'switch'), 'accessibilityHint'))\n .toBe('Changes how updates are installed')\n })\n\n test('disabled toggle row cannot emit a value change', async () => {\n const onValueChange = vi.fn<(value: boolean) => void>()\n const root = await renderNative(\n <LumenToggle\n disabled\n label=\"Automatic updates\"\n onValueChange={onValueChange}\n value\n />\n )\n const toggle = findByAccessibilityRole(root, 'switch')\n\n expect(readProp(toggle, 'accessibilityState')).toEqual({\n checked: true,\n disabled: true\n })\n expect(readProp(toggle, 'disabled')).toBe(true)\n\n await act(async () => {\n callAction(readProp(toggle, 'onPress'), 'Disabled toggle row is missing its guarded press action.')\n await Promise.resolve()\n })\n\n expect(onValueChange).not.toHaveBeenCalled()\n })\n\n test('tabs preserve selected and disabled semantics while emitting enabled selection', async () => {\n const onValueChange = vi.fn<(value: string) => void>()\n const root = await renderNative(\n <LumenTabs\n label=\"Workspace views\"\n onValueChange={onValueChange}\n options={[\n { label: 'Overview', value: 'overview' },\n { disabled: true, label: 'Activity', value: 'activity' }\n ]}\n value=\"overview\"\n >\n <LumenText>Current workspace health</LumenText>\n </LumenTabs>\n )\n const tabList = findByAccessibilityRole(root, 'tablist')\n const tabs = root.container.queryAll(\n instance => readProp(instance, 'accessibilityRole') === 'tab'\n )\n\n expect(readProp(tabList, 'accessibilityLabel')).toBe('Workspace views')\n expect(tabs).toHaveLength(2)\n const [overviewTab, activityTab] = tabs\n\n if (!overviewTab || !activityTab) throw new Error('Expected the two configured tabs.')\n\n expect(readProp(overviewTab, 'accessibilityState')).toEqual({\n disabled: false,\n selected: true\n })\n expect(readProp(activityTab, 'accessibilityState')).toEqual({\n disabled: true,\n selected: false\n })\n\n const enabledTabPress = readProp(overviewTab, 'onPress')\n\n await act(async () => {\n callAction(enabledTabPress, 'Enabled tab is missing its press action.')\n await Promise.resolve()\n })\n\n expect(onValueChange).toHaveBeenCalledExactlyOnceWith('overview')\n })\n\n test('uses the translated tab label by default and accepts an explicit translated panel name', async () => {\n const fixture = (panelAccessibilityLabel?: string): ReactElement => (\n <LumenTabs label=\"Vistas\" onValueChange={vi.fn()} options={[{ label: 'Resumen', value: 'overview' }]} {...(panelAccessibilityLabel ? { panelAccessibilityLabel } : {})} value=\"overview\">\n <LumenText>Contenido</LumenText>\n </LumenTabs>\n )\n const root = await renderNative(fixture())\n const panel = root.container.queryAll(instance => readProp(instance, 'role') === 'tabpanel')[0]\n if (!panel) throw new Error('Missing tab panel')\n expect(readProp(panel, 'accessibilityLabel')).toBe('Resumen')\n await act(async () => {\n root.render(<LumenProvider>{fixture('Contenido del resumen')}</LumenProvider>)\n await Promise.resolve()\n })\n const translatedPanel = root.container.queryAll(instance => readProp(instance, 'role') === 'tabpanel')[0]\n if (!translatedPanel) throw new Error('Missing translated tab panel')\n expect(readProp(translatedPanel, 'accessibilityLabel')).toBe('Contenido del resumen')\n })\n\n test('tabs support directional keyboard navigation without selecting disabled tabs', async () => {\n const onValueChange = vi.fn<(value: string) => void>()\n const KeyboardTabsFixture = (): ReactElement => {\n const [value, setValue] = useState('overview')\n\n return (\n <LumenTabs\n label=\"Workspace views\"\n onValueChange={nextValue => {\n onValueChange(nextValue)\n setValue(nextValue)\n }}\n options={[\n { label: 'Overview', value: 'overview' },\n { label: 'Activity', value: 'activity' },\n { disabled: true, label: 'Billing', value: 'billing' }\n ]}\n value={value}\n >\n <LumenText>Current workspace health</LumenText>\n </LumenTabs>\n )\n }\n const root = await renderNative(<KeyboardTabsFixture />)\n const pressTabKey = async (optionIndex: number, key: string): Promise<void> => {\n const tab = root.container.queryAll(\n instance => readProp(instance, 'accessibilityRole') === 'tab'\n )[optionIndex]\n\n if (!tab) throw new Error(`Expected tab fixture at index ${optionIndex}.`)\n\n let prevented = false\n\n await act(async () => {\n const preventDefault = callKeyboardAction(readProp(tab, 'onKeyDown'), key)\n\n prevented = preventDefault.mock.calls.length > 0\n\n await Promise.resolve()\n })\n\n expect(prevented).toBe(true)\n }\n\n await pressTabKey(0, 'ArrowRight')\n expect(onValueChange).toHaveBeenLastCalledWith('activity')\n\n await pressTabKey(1, 'ArrowRight')\n expect(onValueChange).toHaveBeenLastCalledWith('overview')\n expect(onValueChange).not.toHaveBeenCalledWith('billing')\n })\n\n test('tabs do not reselect the current tab when every alternative is disabled', async () => {\n const onValueChange = vi.fn<(value: string) => void>()\n const root = await renderNative(\n <LumenTabs\n label=\"Workspace views\"\n onValueChange={onValueChange}\n options={[\n { label: 'Overview', value: 'overview' },\n { disabled: true, label: 'Activity', value: 'activity' }\n ]}\n value=\"overview\"\n >\n <LumenText>Current workspace health</LumenText>\n </LumenTabs>\n )\n const overviewTab = root.container.queryAll(\n instance => readProp(instance, 'accessibilityRole') === 'tab'\n )[0]\n\n if (!overviewTab) throw new Error('Expected the selected tab fixture.')\n\n expect(callKeyboardAction(readProp(overviewTab, 'onKeyDown'), 'ArrowRight'))\n .toHaveBeenCalledOnce()\n expect(onValueChange).not.toHaveBeenCalled()\n })\n})\n\ndescribe('LumenSheet consumer layouts', () => {\n afterEach(() => {\n nativeMotion.enabled = false\n nativeWindow.width = 400\n nativeWindow.height = 800\n nativeWindow.fontScale = 1\n nativePlatform.OS = 'ios'\n accessibilityFocus.mockClear()\n })\n\n test.each([\n { fontScale: 2, height: 800, alignment: 'center' },\n { fontScale: 1, height: 320, alignment: 'flex-end' }\n ])('keeps headings and actions scrollable when space is limited: %j', async ({ alignment, ...window }) => {\n Object.assign(nativeWindow, window, { width: 1024 })\n const root = await renderNative(\n <LumenSheet actions={<LumenButton>Save changes</LumenButton>} onDismiss={vi.fn()} presentation=\"adaptive\" title=\"A long translated heading\" visible>\n <LumenTextField accessibilityLabel=\"Details\" />\n </LumenSheet>\n )\n const scroll = findHostComponent(root, 'ScrollView')\n\n expect(scroll.queryAll(instance => readProp(instance, 'accessibilityRole') === 'header')).toHaveLength(1)\n expect(scroll.queryAll(instance => readProp(instance, 'accessibilityRole') === 'button')).toHaveLength(1)\n expect(readProp(scroll, 'keyboardShouldPersistTaps')).toBe('handled')\n expect(readProp(findHostComponent(root, 'KeyboardAvoidingView'), 'style')).toMatchObject({ justifyContent: alignment })\n })\n\n test('preserves application-owned virtualization at accessibility text sizes', async () => {\n nativeWindow.fontScale = 3\n const root = await renderNative(\n <LumenSheet onDismiss={vi.fn()} scrollable={false} visible>\n <LumenText>Virtualized collection</LumenText>\n </LumenSheet>\n )\n\n expect(root.container.queryAll(instance => instance.type === 'ScrollView')).toHaveLength(0)\n })\n\n test('focuses an explicit initial control after presentation and restores the trigger after closing', async () => {\n const initialFocusRef = createRef<ComponentRef<typeof View>>()\n const returnFocusRef = createRef<ComponentRef<typeof View>>()\n let restoreFrame: (() => void) | undefined\n vi.stubGlobal('requestAnimationFrame', (callback: () => void) => {\n restoreFrame = callback\n\n return 1\n })\n vi.stubGlobal('cancelAnimationFrame', vi.fn())\n const fixture = (visible: boolean): ReactElement => (\n <>\n <LumenButton ref={returnFocusRef}>Edit</LumenButton>\n <LumenSheet\n initialFocusRef={initialFocusRef}\n onDismiss={vi.fn()}\n returnFocusRef={returnFocusRef}\n visible={visible}\n >\n <LumenButton ref={initialFocusRef}>Save</LumenButton>\n </LumenSheet>\n </>\n )\n const root = await renderNative(fixture(true))\n callAction(readProp(findHostComponent(root, 'Modal'), 'onShow'), 'Missing modal presentation event')\n expect(accessibilityFocus).toHaveBeenCalledExactlyOnceWith(initialFocusRef.current, 'focus')\n await act(async () => {\n root.render(<LumenProvider>{fixture(false)}</LumenProvider>)\n await Promise.resolve()\n })\n expect(accessibilityFocus).toHaveBeenCalledTimes(1)\n if (!restoreFrame) throw new Error('Expected a deferred focus restoration')\n restoreFrame()\n expect(accessibilityFocus).toHaveBeenLastCalledWith(returnFocusRef.current, 'focus')\n })\n\n test('wraps related actions at ordinary text sizes and stacks them at accessibility sizes', async () => {\n const fixture = (): ReactElement => (\n <LumenButtonGroup accessibilityLabel=\"Form actions\">\n <LumenButton>Cancel</LumenButton>\n <LumenButton>Save</LumenButton>\n </LumenButtonGroup>\n )\n const root = await renderNative(fixture())\n const group = root.container.queryAll(instance => readProp(instance, 'accessibilityLabel') === 'Form actions')[0]\n if (!group) throw new Error('Missing action group')\n expect(readProp(group, 'style')).toEqual(expect.arrayContaining([expect.objectContaining({ flexDirection: 'row', flexWrap: 'wrap' })]))\n nativeWindow.fontScale = 2\n await act(async () => {\n root.render(<LumenProvider>{fixture()}</LumenProvider>)\n await Promise.resolve()\n })\n const updated = root.container.queryAll(instance => readProp(instance, 'accessibilityLabel') === 'Form actions')[0]\n if (!updated) throw new Error('Missing updated action group')\n expect(readProp(updated, 'style')).toEqual(expect.arrayContaining([expect.objectContaining({ flexDirection: 'column', flexWrap: 'nowrap' })]))\n })\n\n test('keeps actions outside the scrolling body and opts into keyboard avoidance', async () => {\n const root = await renderNative(\n <LumenSheet\n actions={<LumenButton>Save</LumenButton>}\n avoidKeyboard\n keyboardVerticalOffset={24}\n onDismiss={vi.fn()}\n safeAreaInsets={{ bottom: 34 }}\n scrollable\n title=\"Edit record\"\n visible\n >\n <LumenTextField accessibilityLabel=\"Name\" />\n </LumenSheet>\n )\n const scroll = root.container.queryAll(instance => instance.type === 'ScrollView')[0]\n const keyboard = root.container.queryAll(instance => instance.type === 'KeyboardAvoidingView')[0]\n if (!scroll || !keyboard) throw new Error('Missing keyboard or scrolling surface')\n expect(readProp(keyboard, 'enabled')).toBe(true)\n expect(readProp(keyboard, 'behavior')).toBe('padding')\n expect(readProp(keyboard, 'keyboardVerticalOffset')).toBe(24)\n expect(readProp(scroll, 'keyboardShouldPersistTaps')).toBe('handled')\n expect(scroll.queryAll(instance => readProp(instance, 'accessibilityRole') === 'button')).toHaveLength(0)\n expect(findByAccessibilityRole(root, 'header')).toBeDefined()\n expect(root.container.queryAll(instance => {\n const style = readProp(instance, 'style')\n return typeof style === 'object' && style !== null && 'paddingBottom' in style && style.paddingBottom === 34\n })).toHaveLength(1)\n })\n\n test('blocks platform and backdrop dismissal until the application unlocks it', async () => {\n const dismiss = vi.fn()\n const root = await renderNative(\n <LumenSheet dismissible={false} onDismiss={dismiss} visible><LumenText>Saving</LumenText></LumenSheet>\n )\n const modal = root.container.queryAll(instance => instance.type === 'Modal')[0]\n const backdrop = root.container.queryAll(instance => instance.type === 'Pressable')[0]\n if (!modal || !backdrop) throw new Error('Missing modal or backdrop')\n callAction(readProp(modal, 'onRequestClose'), 'Missing platform dismiss')\n callAction(readProp(backdrop, 'onPress'), 'Missing backdrop dismiss')\n expect(dismiss).not.toHaveBeenCalled()\n expect(readProp(backdrop, 'disabled')).toBe(true)\n await act(async () => {\n root.render(<LumenProvider scheme=\"light\"><LumenSheet onDismiss={dismiss} visible><LumenText>Saved</LumenText></LumenSheet></LumenProvider>)\n await Promise.resolve()\n })\n const updatedModal = root.container.queryAll(instance => instance.type === 'Modal')[0]\n if (!updatedModal) throw new Error('Missing updated modal')\n callAction(readProp(updatedModal, 'onRequestClose'), 'Missing platform dismiss')\n expect(dismiss).toHaveBeenCalledOnce()\n })\n\n test('names sheets and hides backdrop targets from keyboard and accessibility traversal', async () => {\n const root = await renderNative(<LumenSheet onDismiss={vi.fn()} title=\"Edit record\" visible><LumenText>Details</LumenText></LumenSheet>)\n const modal = root.container.queryAll(instance => instance.type === 'Modal')[0]\n const backdrop = root.container.queryAll(instance => instance.type === 'Pressable')[0]\n if (!modal || !backdrop) throw new Error('Missing modal or backdrop')\n expect(readProp(modal, 'accessibilityLabel')).toBe('Edit record')\n expect(readProp(backdrop, 'tabIndex')).toBe(-1)\n expect(readProp(backdrop, 'aria-hidden')).toBe(true)\n })\n\n test('honors the native reduced-motion setting', async () => {\n nativeMotion.enabled = true\n const root = await renderNative(<LumenSheet onDismiss={vi.fn()} visible><LumenText>Details</LumenText></LumenSheet>)\n const modal = root.container.queryAll(instance => instance.type === 'Modal')[0]\n if (!modal) throw new Error('Missing modal')\n expect(readProp(modal, 'animationType')).toBe('none')\n })\n\n test('honors reduced motion in confirmation dialogs and menus', async () => {\n nativeMotion.enabled = true\n const root = await renderNative(\n <>\n <LumenAlertDialog confirmLabel=\"Save\" onConfirm={vi.fn()} onDismiss={vi.fn()} title=\"Save changes\" visible />\n <LumenMenu accessibilityLabel=\"Actions\" items={[]} trigger={<LumenText>Open menu</LumenText>} />\n </>\n )\n const modals = root.container.queryAll(instance => instance.type === 'Modal')\n\n expect(modals).toHaveLength(2)\n expect(modals.map(modal => readProp(modal, 'animationType'))).toEqual(['none', 'none'])\n })\n\n test('uses a centered adaptive dialog on wide windows and Android keyboard behavior', async () => {\n nativeWindow.width = 1024\n nativePlatform.OS = 'android'\n const root = await renderNative(\n <LumenSheet avoidKeyboard onDismiss={vi.fn()} presentation=\"adaptive\" visible><LumenText>Details</LumenText></LumenSheet>\n )\n const modal = root.container.queryAll(instance => instance.type === 'Modal')[0]\n const keyboard = root.container.queryAll(instance => instance.type === 'KeyboardAvoidingView')[0]\n if (!modal || !keyboard) throw new Error('Missing modal or keyboard surface')\n expect(readProp(modal, 'animationType')).toBe('fade')\n expect(readProp(keyboard, 'behavior')).toBe('height')\n expect(readProp(keyboard, 'style')).toMatchObject({ alignItems: 'center', justifyContent: 'center' })\n })\n})\n\ndescribe('Static graphic icon accessibility', () => {\n const Graphic = ({ size, strokeWidth }: LumenIconGraphicProps): ReactElement => (\n <LumenText>{`Graphic ${String(size)} ${String(strokeWidth)}`}</LumenText>\n )\n\n test('names informative graphics and hides decorative graphics', async () => {\n const root = await renderNative(\n <>\n <GraphicIcon icon={Graphic} label=\"Search records\" size=\"lg\" strokeWidth={3} />\n <GraphicIcon icon={Graphic} decorative label=\"Decorative search\" />\n </>\n )\n const informative = root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'image' && readProp(instance, 'accessible') === true)\n expect(informative).toHaveLength(1)\n const named = informative[0]\n if (!named) throw new Error('Missing informative graphic')\n expect(readProp(named, 'accessibilityLabel')).toBe('Search records')\n expect(readProp(named, 'importantForAccessibility')).toBe('yes')\n expect(root.container.queryAll(instance => readProp(instance, 'children') === 'Graphic 24 3').length).toBeGreaterThan(0)\n const hidden = root.container.queryAll(instance => readProp(instance, 'importantForAccessibility') === 'no' && readProp(instance, 'accessible') === false)\n expect(hidden).toHaveLength(1)\n const decorative = hidden[0]\n if (!decorative) throw new Error('Missing decorative graphic')\n expect(readProp(decorative, 'accessibilityLabel')).toBeUndefined()\n expect(readProp(decorative, 'accessibilityElementsHidden')).toBe(true)\n })\n\n test('preserves named and custom icons through the root entrypoint', async () => {\n const root = await renderNative(\n <>\n <LumenIcon name=\"search\" label=\"Named search\" />\n <LumenIcon icon={Graphic} label=\"Custom search\" />\n <LumenIconButton name=\"search\" label=\"Named search action\" disabled />\n <LumenIconButton icon={Graphic} label=\"Custom search action\" disabled />\n </>\n )\n const icons = root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'image')\n expect(icons.map(icon => readProp(icon, 'accessibilityLabel'))).toEqual(['Named search', 'Custom search'])\n const buttons = root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'button')\n expect(buttons.map(button => readProp(button, 'accessibilityLabel'))).toEqual(['Named search action', 'Custom search action'])\n expect(buttons.map(button => readProp(button, 'accessibilityState'))).toEqual([{ disabled: true }, { disabled: true }])\n })\n\n test('canonical per-icon imports match named artwork and preserve accessible labels', async () => {\n const root = await renderNative(\n <>\n <LumenIcon name=\"search\" label=\"Named search\" size=\"lg\" strokeWidth={3} />\n <GraphicIcon icon={LumenSearchIconGraphic} label=\"Static search\" size=\"lg\" strokeWidth={3} />\n <LumenIcon name=\"brand:github\" decorative />\n <GraphicIcon icon={LumenBrandGithubIconGraphic} decorative />\n </>\n )\n const graphics = root.container.queryAll(instance => instance.type === 'Svg')\n expect(graphics).toHaveLength(4)\n const geometry = (graphic: TestInstance | undefined): unknown => {\n if (!graphic) throw new Error('Missing canonical SVG')\n return {\n props: Object.fromEntries(Object.entries(graphic.props).filter(([name]) => name !== 'children')),\n children: graphic.children.filter(child => typeof child !== 'string').map(child => ({ type: child.type, props: child.props }))\n }\n }\n expect(geometry(graphics[0])).toEqual(geometry(graphics[1]))\n expect(geometry(graphics[2])).toEqual(geometry(graphics[3]))\n const icons = root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'image')\n expect(icons.map(icon => readProp(icon, 'accessibilityLabel'))).toEqual(['Named search', 'Static search'])\n })\n\n test('keeps graphic buttons named and announces disabled state', async () => {\n const root = await renderNative(<GraphicIconButton icon={Graphic} label=\"Search records\" disabled />)\n const button = root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'button')[0]\n if (!button) throw new Error('Missing graphic button')\n expect(readProp(button, 'accessibilityLabel')).toBe('Search records')\n expect(readProp(button, 'accessibilityState')).toEqual({ disabled: true })\n expect(readProp(button, 'disabled')).toBe(true)\n })\n})\n\nconst callInputAction = (value: unknown, proposal: string): void => {\n if (typeof value !== 'function') throw new Error('Missing native input callback')\n Reflect.apply(value, undefined, [proposal])\n}\n\nconst runNativeAction = async (action: () => void): Promise<void> => {\n await act(async () => {\n action()\n\n await Promise.resolve()\n })\n}\n\ndescribe('advanced native inputs', () => {\n test('secure entry hides on blur and after disabling and re-enabling', async () => {\n const props = { label: 'Password', onValueChange: vi.fn(), value: 'synthetic-fixture' }\n const root = await renderNative(<LumenPasswordField {...props} />)\n expect(readProp(findByAccessibilityLabel(root, 'Password'), 'secureTextEntry')).toBe(true)\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Show password'), 'onPress'), 'Missing reveal')\n })\n expect(readProp(findByAccessibilityLabel(root, 'Password'), 'secureTextEntry')).toBe(false)\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Password'), 'onBlur'), 'Missing blur')\n })\n expect(readProp(findByAccessibilityLabel(root, 'Password'), 'secureTextEntry')).toBe(true)\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Show password'), 'onPress'), 'Missing reveal')\n })\n await runNativeAction(() => {\n root.render(<LumenProvider><LumenPasswordField {...props} enabled={false} /></LumenProvider>)\n })\n await runNativeAction(() => {\n root.render(<LumenProvider><LumenPasswordField {...props} /></LumenProvider>)\n })\n expect(readProp(findByAccessibilityLabel(root, 'Password'), 'secureTextEntry')).toBe(true)\n })\n\n test('OTP preserves native autofill, normalizes paste and emits completion only for changed full codes', async () => {\n const onValueChange = vi.fn<(value: string) => void>()\n const onComplete = vi.fn<(value: string) => void>()\n const root = await renderNative(<LumenInputOTP label=\"Code\" value=\"123\" onValueChange={onValueChange} onComplete={onComplete} />)\n const input = findByAccessibilityLabel(root, 'Code')\n expect(readProp(input, 'textContentType')).toBe('oneTimeCode')\n await runNativeAction(() => {\n callInputAction(readProp(input, 'onChangeText'), '١٢٣-456')\n })\n expect(onValueChange).toHaveBeenCalledExactlyOnceWith('123456')\n expect(onComplete).toHaveBeenCalledExactlyOnceWith('123456')\n await runNativeAction(() => {\n callInputAction(readProp(input, 'onChangeText'), 'not a code')\n })\n expect(onValueChange).toHaveBeenCalledTimes(1)\n await runNativeAction(() => {\n root.render(<LumenProvider><LumenInputOTP label=\"Code\" value=\"123456\" onValueChange={onValueChange} onComplete={onComplete} readOnly /></LumenProvider>)\n })\n await runNativeAction(() => {\n callInputAction(readProp(findByAccessibilityLabel(root, 'Code'), 'onChangeText'), '654321')\n })\n expect(onValueChange).toHaveBeenCalledTimes(1)\n })\n\n test('number entry retains raw drafts and steps exact localized values', async () => {\n const onValueChange = vi.fn<(value: string) => void>()\n const root = await renderNative(<LumenNumberField label=\"Amount\" value=\"0,1\" locale=\"es-CO\" step=\"0.2\" onValueChange={onValueChange} />)\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Increase value'), 'onPress'), 'Missing increase')\n })\n expect(onValueChange).toHaveBeenLastCalledWith('0,3')\n await runNativeAction(() => {\n callInputAction(readProp(findByAccessibilityLabel(root, 'Amount'), 'onChangeText'), '12,')\n })\n expect(onValueChange).toHaveBeenLastCalledWith('12,')\n await runNativeAction(() => {\n root.render(<LumenProvider><LumenNumberField label=\"Amount\" value=\"12,\" locale=\"es-CO\" onValueChange={onValueChange} /></LumenProvider>)\n })\n expect(readProp(findByAccessibilityLabel(root, 'Increase value'), 'disabled')).toBe(true)\n expect(readProp(findByAccessibilityLabel(root, 'Amount'), 'aria-invalid')).toBe(true)\n })\n\n test('autocomplete publishes query before selection and closes its results', async () => {\n const calls: string[] = []\n const root = await renderNative(\n <LumenAutocomplete\n label=\"City\"\n query=\"Bo\"\n options={[{ label: 'Bogotá', value: 'bogota' }]}\n onQueryChange={query => {\n calls.push(query)\n }}\n onValueChange={value => {\n calls.push(value)\n }}\n />\n )\n await runNativeAction(() => {\n callInputAction(readProp(findByAccessibilityLabel(root, 'City'), 'onChangeText'), 'Bog')\n })\n calls.length = 0\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Bogotá'), 'onPress'), 'Missing result')\n })\n expect(calls).toEqual(['Bogotá', 'bogota'])\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityLabel') === 'Close results')).toHaveLength(0)\n })\n\n test('autocomplete loading hides results and disabled state dismisses selection', async () => {\n const props = { label: 'City', onQueryChange: vi.fn(), onValueChange: vi.fn(), options: [{ label: 'Bogotá', value: 'bogota' }], query: '' }\n const root = await renderNative(<LumenAutocomplete {...props} loading />)\n await runNativeAction(() => {\n callInputAction(readProp(findByAccessibilityLabel(root, 'City'), 'onChangeText'), 'Bo')\n })\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityLabel') === 'Bogotá')).toHaveLength(0)\n await runNativeAction(() => {\n root.render(<LumenProvider><LumenAutocomplete {...props} enabled={false} /></LumenProvider>)\n })\n await runNativeAction(() => {\n root.render(<LumenProvider><LumenAutocomplete {...props} /></LumenProvider>)\n })\n expect(readProp(findByAccessibilityLabel(root, 'City'), 'accessibilityState')).toMatchObject({ expanded: false })\n })\n\n test.each(['ios', 'android', 'web'])('time formatting tolerates malformed locales on %s', async platform => {\n nativePlatform.OS = platform\n for (const locale of ['not_a_locale', 'en--US', '💥']) {\n const props = { label: 'Time', value: { hour: 9, minute: 30 }, locale, onValueChange: vi.fn() }\n const root = await renderNative(<LumenTimeField {...props} is24Hour />)\n\n expect(findByAccessibilityLabel(root, 'Time, 09:30')).toBeDefined()\n await runNativeAction(() => {\n root.render(<LumenProvider><LumenTimeField {...props} is24Hour={false} /></LumenProvider>)\n })\n expect(findByAccessibilityLabel(root, 'Time, 9:30 AM')).toBeDefined()\n }\n })\n\n test('time selection requires confirmation and cancellation preserves the value', async () => {\n nativePlatform.OS = 'ios'\n const onValueChange = vi.fn()\n const root = await renderNative(\n <LumenTimeField\n label=\"Time\"\n value={{ hour: 9, minute: 30 }}\n locale=\"en-US\"\n is24Hour\n minTime={{ hour: 9, minute: 0 }}\n maxTime={{ hour: 17, minute: 0 }}\n onValueChange={onValueChange}\n />\n )\n const open = async (): Promise<void> => {\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Time, 09:30'), 'onPress'), 'Missing time trigger')\n })\n }\n await open()\n await runNativeAction(() => {\n const picker = findHostComponent(root, 'NativeDatePicker')\n const change = readProp(picker, 'onChange')\n if (typeof change !== 'function') throw new Error('Missing time change')\n Reflect.apply(change, undefined, [{ type: 'set' }, new Date(2000, 0, 1, 10, 45)])\n })\n expect(onValueChange).not.toHaveBeenCalled()\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Cancel'), 'onPress'), 'Missing cancel')\n })\n expect(onValueChange).not.toHaveBeenCalled()\n await open()\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Confirm'), 'onPress'), 'Missing confirm')\n })\n expect(onValueChange).toHaveBeenCalledExactlyOnceWith({ hour: 9, minute: 30 })\n })\n\n test.each(['ios', 'web'])('time sheet resets controlled changes without discarding equivalent inputs on %s', async platform => {\n nativePlatform.OS = platform\n const updates = [\n { value: { hour: 11, minute: 15 }, expected: { hour: 11, minute: 15 } },\n { minTime: { hour: 10, minute: 0 }, expected: { hour: 10, minute: 0 } },\n { maxTime: { hour: 9, minute: 15 }, expected: { hour: 9, minute: 0 } }\n ]\n for (const { expected, ...update } of updates) {\n const onValueChange = vi.fn()\n const props: LumenTimeFieldProps = { label: 'Time',\n value: { hour: 9, minute: 30 },\n locale: 'en-US',\n is24Hour: true,\n minTime: { hour: 9, minute: 0 },\n maxTime: { hour: 17, minute: 0 },\n onValueChange }\n const root = await renderNative(<LumenTimeField {...props} />)\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Time, 09:30'), 'onPress'), 'Missing time trigger')\n })\n await runNativeAction(() => {\n const picker = findHostComponent(root, platform === 'web' ? 'input' : 'NativeDatePicker')\n const change = readProp(picker, 'onChange')\n if (typeof change !== 'function') throw new Error('Missing time change')\n Reflect.apply(change, undefined, platform === 'web' ? [{ currentTarget: { value: '10:45' } }] : [{ type: 'set' }, new Date(2000, 0, 1, 10, 45)])\n })\n await runNativeAction(() => {\n root.render(\n <LumenProvider>\n <LumenTimeField\n {...props}\n value={{ hour: 9, minute: 30 }}\n minTime={{ hour: 9, minute: 0 }}\n maxTime={{ hour: 17, minute: 0 }}\n />\n </LumenProvider>\n )\n })\n expect(readProp(findHostComponent(root, 'Modal'), 'visible')).toBe(true)\n const draft = readProp(findHostComponent(root, platform === 'web' ? 'input' : 'NativeDatePicker'), 'value')\n expect(draft).toEqual(platform === 'web' ? '10:45' : new Date(2000, 0, 1, 10, 45))\n await runNativeAction(() => {\n root.render(<LumenProvider><LumenTimeField {...props} {...update} /></LumenProvider>)\n })\n expect(readProp(findHostComponent(root, 'Modal'), 'visible')).toBe(false)\n expect(onValueChange).not.toHaveBeenCalled()\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, `Time, ${update.value ? '11:15' : '09:30'}`), 'onPress'), 'Missing updated time trigger')\n })\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Confirm'), 'onPress'), 'Missing time confirm')\n })\n expect(onValueChange).toHaveBeenCalledExactlyOnceWith(expected)\n }\n })\n\n test('Android time rejects out-of-range selection and stale callbacks after disabling', async () => {\n nativePlatform.OS = 'android'\n const onValueChange = vi.fn()\n const props = { label: 'Time', locale: 'en-US', onValueChange, value: null }\n const root = await renderNative(<LumenTimeField {...props} minTime={{ hour: 9, minute: 0 }} />)\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Time, Choose a time'), 'onPress'), 'Missing time trigger')\n })\n const options = vi.mocked(DateTimePickerAndroid.open).mock.calls.at(-1)?.[0]\n if (!options?.onValueChange) throw new Error('Missing Android time handler')\n const change = options.onValueChange\n await runNativeAction(() => {\n change({ nativeEvent: { timestamp: 0, utcOffset: 0 } }, new Date(2000, 0, 1, 8, 0))\n })\n expect(onValueChange).not.toHaveBeenCalled()\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Time, Choose a time'), 'onPress'), 'Missing time trigger')\n })\n const current = vi.mocked(DateTimePickerAndroid.open).mock.calls.at(-1)?.[0]\n if (!current?.onValueChange) throw new Error('Missing Android time handler')\n const stale = current.onValueChange\n await runNativeAction(() => {\n root.render(<LumenProvider><LumenTimeField {...props} enabled={false} /></LumenProvider>)\n })\n expect(DateTimePickerAndroid.dismiss).toHaveBeenCalledWith('time')\n await runNativeAction(() => {\n stale({ nativeEvent: { timestamp: 0, utcOffset: 0 } }, new Date(2000, 0, 1, 10, 0))\n })\n expect(onValueChange).not.toHaveBeenCalled()\n })\n\n test('Android time validates current bounds and uses the current callback while the dialog is open', async () => {\n nativePlatform.OS = 'android'\n const openingCallback = vi.fn()\n const currentCallback = vi.fn()\n const props = { label: 'Time', locale: 'en-US', value: null }\n const root = await renderNative(\n <LumenTimeField {...props} minTime={{ hour: 9, minute: 0 }} onValueChange={openingCallback} />\n )\n const chooseAfterUpdate = async (hour: number): Promise<void> => {\n await runNativeAction(() => {\n root.render(\n <LumenProvider>\n <LumenTimeField {...props} minTime={{ hour: 9, minute: 0 }} onValueChange={openingCallback} />\n </LumenProvider>\n )\n })\n await runNativeAction(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Time, Choose a time'), 'onPress'), 'Missing time trigger')\n })\n const options = vi.mocked(DateTimePickerAndroid.open).mock.calls.at(-1)?.[0]\n if (!options?.onValueChange) throw new Error('Missing Android time handler')\n const change = options.onValueChange\n await runNativeAction(() => {\n root.render(<LumenProvider><LumenTimeField {...props} minTime={{ hour: 12, minute: 0 }} maxTime={{ hour: 17, minute: 0 }} onValueChange={currentCallback} rangeErrorLabel=\"Choose an afternoon time\" /></LumenProvider>)\n })\n await runNativeAction(() => {\n change({ nativeEvent: { timestamp: 0, utcOffset: 0 } }, new Date(2000, 0, 1, hour, 30))\n })\n }\n await chooseAfterUpdate(10)\n expect(openingCallback).not.toHaveBeenCalled()\n expect(currentCallback).not.toHaveBeenCalled()\n expect(root.container.queryAll(instance => readProp(instance, 'children') === 'Choose an afternoon time').length).toBeGreaterThan(0)\n await chooseAfterUpdate(18)\n expect(currentCallback).not.toHaveBeenCalled()\n await chooseAfterUpdate(13)\n expect(openingCallback).not.toHaveBeenCalled()\n expect(currentCallback).toHaveBeenCalledExactlyOnceWith({ hour: 13, minute: 30 })\n })\n\n test.each(['en--US', 'invalid_tag', '💥'])('image comparison falls back for malformed locale %s', async locale => {\n const root = await renderNative(<LumenImageComparison label=\"Comparison\" before={{ uri: 'fixture:before' }} after={{ uri: 'fixture:after' }} value={0.25} locale={locale} onValueChange={() => {}} />)\n const control = findByAccessibilityRole(root, 'adjustable')\n expect(readProp(control, 'accessibilityValue')).toMatchObject({ text: 'After 25%' })\n })\n\n test.each(['side-by-side', 'before', 'after'] as const)('image comparison %s mode presents labels without an adjustable control', async mode => {\n const root = await renderNative(<LumenImageComparison label=\"Comparison\" before={{ uri: 'fixture:before' }} after={{ uri: 'fixture:after' }} value={0.25} mode={mode} onValueChange={() => {}} />)\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'adjustable')).toHaveLength(0)\n expect(root.container.queryAll(instance => readProp(instance, 'children') === 'Before').length > 0).toBe(mode !== 'after')\n expect(root.container.queryAll(instance => readProp(instance, 'children') === 'After').length > 0).toBe(mode !== 'before')\n })\n\n test('image comparison exposes one adjustable control and localized after percentage', async () => {\n const root = await renderNative(<LumenImageComparison label=\"Comparison\" before={{ uri: 'fixture:before' }} after={{ uri: 'fixture:after' }} value={0.25} locale=\"es-CO\" onValueChange={() => {}} />)\n const control = findByAccessibilityRole(root, 'adjustable')\n expect(readProp(control, 'accessibilityLabel')).toBe('Comparison')\n expect(readProp(control, 'accessibilityValue')).toMatchObject({ text: 'After 25%' })\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'image')).toHaveLength(0)\n })\n})\n\ndescribe('native chart data inspection', () => {\n test('renders zero as neutral and missing measurements with a separate marker and exact fallback', async () => {\n const root = await renderNative(\n <LumenHeatmap\n label=\"Activity\"\n colorScale=\"diverging\"\n data={[\n { x: 'Mon', y: 'AM', value: -2 },\n { x: 'Tue', y: 'AM', value: 0 },\n { x: 'Wed', y: 'AM', value: null },\n { x: 'Mon', y: 'AM', value: 40 }\n ]}\n />\n )\n const neutral = root.container.queryAll(instance => instance.type === 'Rect' && readProp(instance, 'fillOpacity') === 0)\n expect(neutral).toHaveLength(1)\n const missing = root.container.queryAll(instance => instance.type === 'Text' && readProp(instance, 'children') === '× Not available')\n expect(missing).toHaveLength(1)\n await act(async () => {\n callAction(readProp(findByAccessibilityRole(root, 'button'), 'onPress'), 'Missing chart disclosure')\n await Promise.resolve()\n })\n const rows = root.container.queryAll(instance => instance.type === 'Text').map(instance => readProp(instance, 'children'))\n expect(rows).toContain('Wed, AM: Not available')\n expect(rows).toContain('Tue, AM: 0')\n expect(rows).toContain('Mon, AM: -2')\n expect(rows).not.toContain('Mon, AM: 40')\n })\n\n test('opens and closes exact source values using a labeled disclosure', async () => {\n const root = await renderNative(\n <LumenWaterfallChart\n label=\"Revenue\"\n data={[\n { id: 'opening', label: 'Opening', kind: 'total', value: 100 },\n { id: 'cost', label: 'Costs', value: -25 }\n ]}\n />\n )\n const button = findByAccessibilityRole(root, 'button')\n expect(readProp(button, 'accessibilityState')).toMatchObject({ expanded: false })\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'list')).toHaveLength(0)\n await act(async () => {\n callAction(readProp(button, 'onPress'), 'Missing chart disclosure')\n await Promise.resolve()\n })\n expect(readProp(findByAccessibilityRole(root, 'button'), 'accessibilityState')).toMatchObject({ expanded: true })\n const rows = root.container.queryAll(instance => instance.type === 'Text').map(instance => readProp(instance, 'children'))\n expect(rows).toContain('Costs, Start: 100, End: 75, Value: -25')\n await act(async () => {\n callAction(readProp(findByAccessibilityRole(root, 'button'), 'onPress'), 'Missing chart disclosure')\n await Promise.resolve()\n })\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'list')).toHaveLength(0)\n })\n\n test('shows a localized invalid-data state rather than a partial histogram', async () => {\n const root = await renderNative(\n <LumenHistogram\n label=\"Distribution\"\n labels={{ invalidData: 'Check the bins' }}\n data={[\n { start: 0, end: 10, count: 5 }, { start: 5, end: 15, count: 3 }\n ]}\n />\n )\n expect(readProp(findByAccessibilityRole(root, 'alert'), 'children')).toBe('Check the bins')\n expect(root.container.queryAll(instance => instance.type === 'Svg')).toHaveLength(0)\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'button')).toHaveLength(0)\n })\n})\n\ndescribe('controlled multiple selection', () => {\n test('removal retains unavailable values and never mutates the controlled set', async () => {\n const values = new Set(['one', 'missing'])\n const onValuesChange = vi.fn<(values: ReadonlySet<string>) => void>()\n const root = await renderNative(<LumenMultiSelect label=\"Teams\" values={values} onValuesChange={onValuesChange} query=\"\" onQueryChange={() => undefined} options={[{ label: 'One', value: 'one' }]} />)\n const remove = root.container.queryAll(instance => readProp(instance, 'accessibilityLabel') === 'Remove One').find(instance => typeof readProp(instance, 'onPress') === 'function')\n if (!remove) throw new Error('Missing selected removal')\n const handler = readProp(remove, 'onPress')\n if (typeof handler !== 'function') throw new Error('Missing removal handler')\n await act(async () => {\n Reflect.apply(handler, undefined, [])\n await Promise.resolve()\n })\n expect(onValuesChange).toHaveBeenLastCalledWith(new Set(['missing']))\n expect(values).toEqual(new Set(['one', 'missing']))\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityLabel') === 'Remove missing').length).toBeGreaterThan(0)\n })\n\n test.each([{ readOnly: true }, { enabled: false }])('blocked controls reject removal: %o', async blocked => {\n const onValuesChange = vi.fn<(values: ReadonlySet<string>) => void>()\n const root = await renderNative(<LumenMultiSelect {...blocked} label=\"Teams\" values={new Set(['one'])} onValuesChange={onValuesChange} query=\"\" onQueryChange={() => undefined} options={[{ label: 'One', value: 'one' }]} />)\n const remove = root.container.queryAll(instance => readProp(instance, 'accessibilityLabel') === 'Remove One').find(instance => typeof readProp(instance, 'onPress') === 'function')\n if (!remove) throw new Error('Missing removal')\n const handler = readProp(remove, 'onPress')\n if (typeof handler !== 'function') throw new Error('Missing removal handler')\n await act(async () => {\n Reflect.apply(handler, undefined, [])\n await Promise.resolve()\n })\n expect(onValuesChange).not.toHaveBeenCalled()\n expect(readProp(remove, 'disabled')).toBe(true)\n })\n\n test('disabled selected results cannot be removed', async () => {\n const root = await renderNative(\n <LumenMultiSelect\n label=\"Teams\"\n values={new Set(['one'])}\n onValuesChange={() => {\n throw new Error('Disabled callback')\n }}\n query=\"\"\n onQueryChange={() => undefined}\n options={[{ label: 'One', value: 'one', disabled: true }]}\n />\n )\n const remove = root.container.queryAll(instance => readProp(instance, 'accessibilityLabel') === 'Remove One').find(instance => typeof readProp(instance, 'onPress') === 'function')\n if (!remove) throw new Error('Missing removal')\n expect(readProp(remove, 'disabled')).toBe(true)\n })\n})\n\ndescribe('multiple selection results', () => {\n test('checkbox intent retains values outside the result list', async () => {\n const onValuesChange = vi.fn<(values: ReadonlySet<string>) => void>()\n const values = new Set(['missing'])\n const root = await renderNative(<LumenMultiSelect label=\"Teams\" values={values} onValuesChange={onValuesChange} query=\"\" onQueryChange={() => undefined} options={[{ label: 'One', value: 'one' }]} />)\n const option = root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'checkbox').find(instance => typeof readProp(instance, 'onPress') === 'function')\n if (!option) throw new Error('Missing option')\n const handler = readProp(option, 'onPress')\n if (typeof handler !== 'function') throw new Error('Missing checkbox handler')\n await act(async () => {\n Reflect.apply(handler, undefined, [])\n await Promise.resolve()\n })\n expect(onValuesChange).toHaveBeenLastCalledWith(new Set(['missing', 'one']))\n expect(values).toEqual(new Set(['missing']))\n })\n\n test.each([{ loading: true }, { resultsErrorMessage: 'Search failed' }])('stale results are unavailable: %o', async state => {\n const root = await renderNative(<LumenMultiSelect {...state} label=\"Teams\" values={new Set<string>()} onValuesChange={() => undefined} query=\"\" onQueryChange={() => undefined} options={[{ label: 'One', value: 'one' }]} />)\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'checkbox')).toHaveLength(0)\n })\n})\n\ndescribe('multiple selection option identity', () => {\n test.each([\n { options: [{ label: 'One', value: '' }] },\n { options: [{ label: ' ', value: 'one' }] },\n { options: [{ label: 'One', value: 'one' }, { label: 'Duplicate', value: 'one' }] }\n ])('rejects invalid option identities: %o', async ({ options }) => {\n await expect(renderNative(<LumenMultiSelect label=\"Teams\" values={new Set<string>()} onValuesChange={() => undefined} query=\"\" onQueryChange={() => undefined} options={options} />)).rejects.toThrow('MultiSelect options require')\n })\n})\n\ntest('multiple selection forwards application safe-area insets to its modal', async () => {\n const root = await renderNative(<LumenMultiSelect label=\"Teams\" values={new Set<string>()} onValuesChange={() => undefined} query=\"\" onQueryChange={() => undefined} options={[]} safeAreaInsets={{ bottom: 96 }} />)\n const panels = root.container.queryAll(instance => {\n const style = readProp(instance, 'style')\n\n return typeof style === 'object' && style !== null && 'paddingBottom' in style && style.paddingBottom === 96\n })\n expect(panels.length).toBeGreaterThan(0)\n})\n\ndescribe('rating controlled selection', () => {\n test('clamps nonfinite values and limits the rendered option count', () => {\n expect(resolveLumenRating(Number.NaN, Number.POSITIVE_INFINITY)).toEqual({ max: 5, value: 0 })\n expect(resolveLumenRating(-5, 0)).toEqual({ max: 1, value: 0 })\n expect(resolveLumenRating(999, 999)).toEqual({ max: 100, value: 100 })\n expect(resolveLumenRating(3.9)).toEqual({ max: 5, value: 3 })\n })\n\n test('reports selection without mutating controlled value and blocks read-only edits', async () => {\n const onValueChange = vi.fn<(value: number) => void>()\n const root = await renderNative(\n <LumenRating\n label=\"Score\"\n value={2}\n onValueChange={onValueChange}\n formatOption={(value, max) => `${value} of ${max}`}\n />\n )\n const option = findByAccessibilityLabel(root, '4 of 5')\n act(() => {\n callAction(readProp(option, 'onPress'), 'Missing rating action')\n })\n expect(onValueChange).toHaveBeenCalledWith(4)\n expect(readProp(option, 'accessibilityState')).toEqual({ checked: false, disabled: false, selected: false })\n act(() => {\n root.render(\n <LumenProvider>\n <LumenRating\n label=\"Score\"\n value={2}\n readOnly\n onValueChange={onValueChange}\n />\n </LumenProvider>\n )\n })\n const locked = findByAccessibilityLabel(root, '4 / 5')\n act(() => {\n callAction(readProp(locked, 'onPress'), 'Missing rating action')\n })\n expect(onValueChange).toHaveBeenCalledTimes(1)\n expect(readProp(locked, 'disabled')).toBe(true)\n })\n})\n\ndescribe('progress and history contracts', () => {\n test('normalizes step progress without changing host state', () => {\n expect(resolveLumenStepState(0, -1, 3)).toBe('current')\n expect(resolveLumenStepState(0, Number.NaN, 3)).toBe('current')\n expect(resolveLumenStepState(1, 1.9, 3)).toBe('current')\n expect(resolveLumenStepState(2, 99, 3)).toBe('complete')\n expect(resolveLumenStepState(2, 1, 3)).toBe('upcoming')\n })\n\n test('presents localized controlled progression and accepts an empty workflow', async () => {\n const steps = [{ id: 'prepare', title: 'Prepare' }, { id: 'review', title: 'Review', description: 'Confirm details' }]\n const root = await renderNative(\n <LumenStepper\n label=\"Workflow\"\n steps={steps}\n currentStep={1}\n formatState={state => `Localized ${state}`}\n />\n )\n const review = findByAccessibilityLabel(root, '2 / 2, Review, Confirm details')\n expect(readProp(review, 'accessibilityValue')).toEqual({ text: 'Localized current' })\n expect(readProp(review, 'accessibilityState')).toEqual({ selected: true })\n act(() => {\n root.render(<LumenProvider><LumenStepper label=\"Workflow\" steps={steps} currentStep={2} /></LumenProvider>)\n })\n expect(readProp(findByAccessibilityLabel(root, '2 / 2, Review, Confirm details'), 'accessibilityValue'))\n .toEqual({ text: 'Complete' })\n act(() => {\n root.render(<LumenProvider><LumenStepper label=\"Workflow\" steps={[]} currentStep={0} /></LumenProvider>)\n })\n expect(root.container.queryAll(instance => readProp(instance, 'accessible') === true)).toHaveLength(0)\n })\n\n test('keeps timeline content actions accessible and host owned', async () => {\n const onPress = vi.fn()\n const root = await renderNative(\n <LumenTimeline label=\"History\">\n <LumenTimelineItem>\n <LumenText>Approved</LumenText>\n <LumenButton onPress={onPress}>View approval</LumenButton>\n </LumenTimelineItem>\n </LumenTimeline>\n )\n const button = findByAccessibilityRole(root, 'button')\n act(() => {\n callAction(readProp(button, 'onPress'), 'Missing timeline action')\n })\n expect(onPress).toHaveBeenCalledOnce()\n })\n\n test('breadcrumb reports navigation IDs and cannot navigate current or disabled locations', async () => {\n const onNavigate = vi.fn<(id: string) => void>()\n const items = [{ id: 'home', label: 'Home' },\n { id: 'private', label: 'Private', disabled: true },\n { id: 'current', label: 'Details' }]\n const root = await renderNative(\n <LumenBreadcrumb\n label=\"Location\"\n items={items}\n onNavigate={onNavigate}\n currentLabel=\"Here\"\n />\n )\n const home = findByAccessibilityLabel(root, 'Home')\n act(() => {\n callAction(readProp(home, 'onPress'), 'Missing breadcrumb action')\n })\n expect(onNavigate).toHaveBeenCalledExactlyOnceWith('home')\n const locked = findByAccessibilityLabel(root, 'Private')\n act(() => {\n callAction(readProp(locked, 'onPress'), 'Missing disabled breadcrumb action')\n })\n expect(readProp(findByAccessibilityLabel(root, 'Details, Here'), 'onPress')).toBeUndefined()\n expect(readProp(findByAccessibilityLabel(root, 'Details, Here'), 'aria-current')).toBe('page')\n })\n})\n\ndescribe('data table controlled interactions', () => {\n const columns = [{ key: 'amount', label: 'Amount', sortable: true }]\n const rows: readonly LumenTableRow[] = [\n { id: 'one', label: 'One', cells: { amount: { text: '20', sortValue: 20 } } },\n { id: 'two', label: 'Two', cells: { amount: { text: '2', sortValue: 2 } }, disabled: true }\n ]\n test('emits manual sort requests without reordering and retains hidden selections', async () => {\n const onSortChange = vi.fn()\n const onSelectionChange = vi.fn()\n const root = await renderNative(\n <LumenDataTable\n label=\"Records\"\n columns={columns}\n rows={rows}\n onSortChange={onSortChange}\n onSelectionChange={onSelectionChange}\n selectedIds={new Set(['hidden'])}\n />\n )\n act(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Amount'), 'onPress'), 'Missing sort action')\n })\n expect(onSortChange).toHaveBeenCalledWith({ key: 'amount', direction: 'ascending' })\n act(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Select visible'), 'onPress'), 'Missing bulk selection')\n })\n expect(onSelectionChange).toHaveBeenCalledWith(new Set(['hidden', 'one']))\n expect(root.container.queryAll(instance => readProp(instance, 'accessible') === true)\n .map(instance => readProp(instance, 'accessibilityLabel'))).toEqual(['Amount, 20', 'Amount, 2'])\n })\n test('loading and error states hide stale controls; retry remains host owned', async () => {\n const onRetry = vi.fn()\n const root = await renderNative(\n <LumenDataTable\n label=\"Records\"\n columns={columns}\n rows={rows}\n loading\n loadingLabel=\"Fetching\"\n onSortChange={vi.fn()}\n />\n )\n expect(readProp(findByAccessibilityRole(root, 'progressbar'), 'children')).toBe('Fetching')\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'button')).toHaveLength(0)\n act(() => {\n root.render(\n <LumenProvider>\n <LumenDataTable\n label=\"Records\"\n columns={columns}\n rows={rows}\n error=\"Try again\"\n onRetry={onRetry}\n retryLabel=\"Reload\"\n />\n </LumenProvider>\n )\n })\n act(() => {\n callAction(readProp(findByAccessibilityRole(root, 'button'), 'onPress'), 'Missing retry')\n })\n expect(onRetry).toHaveBeenCalledOnce()\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'checkbox')).toHaveLength(0)\n })\n test('read-only tables block both sort and selection callbacks', async () => {\n const onSortChange = vi.fn()\n const onSelectionChange = vi.fn()\n const root = await renderNative(\n <LumenDataTable\n label=\"Records\"\n columns={columns}\n rows={rows}\n readOnly\n onSortChange={onSortChange}\n onSelectionChange={onSelectionChange}\n />\n )\n act(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Amount'), 'onPress'), 'Missing guarded sort')\n })\n act(() => {\n callAction(readProp(findByAccessibilityLabel(root, 'Select visible'), 'onPress'), 'Missing guarded selection')\n })\n expect(onSortChange).not.toHaveBeenCalled()\n expect(onSelectionChange).not.toHaveBeenCalled()\n })\n})\n\ntest.each(['light', 'dark'] as const)('nested button content uses its %s foreground without overriding explicit tones', async scheme => {\n const theme = createLumenTheme(scheme)\n const root = await renderNative(\n <LumenProvider theme={theme}>\n <LumenButton>\n <LumenText accessibilityLabel=\"Inherited label\">Continue</LumenText>\n <LumenText accessibilityLabel=\"Explicit tone\" tone=\"danger\">Warning</LumenText>\n <LumenIcon name=\"search\" label=\"Inherited icon\" />\n </LumenButton>\n <LumenText accessibilityLabel=\"Outside control\">Outside</LumenText>\n </LumenProvider>\n )\n expect(readProp(findByAccessibilityLabel(root, 'Inherited label'), 'style')).toEqual(expect.arrayContaining([{ color: theme.colors.onBrand }]))\n expect(readProp(findByAccessibilityLabel(root, 'Explicit tone'), 'style')).toEqual(expect.arrayContaining([{ color: theme.colors.danger }]))\n expect(readProp(findByAccessibilityLabel(root, 'Outside control'), 'style')).toEqual(expect.arrayContaining([{ color: theme.colors.ink }]))\n const svg = findByAccessibilityLabel(root, 'Inherited icon').children[0]\n if (typeof svg !== 'object') throw new Error('Missing inherited icon content')\n expect(readProp(svg, 'stroke')).toBe(theme.colors.onBrand)\n})\n\ntest.each([false, true])('web checkbox Space toggles once and respects disabled=%s', async disabled => {\n nativePlatform.OS = 'web'\n const change = vi.fn<(checked: boolean) => void>()\n const hostKey = vi.fn()\n const root = await renderNative(<LumenCheckbox label=\"Choice\" checked disabled={disabled} onCheckedChange={change} onKeyDown={hostKey} />)\n const handler = readProp(findByAccessibilityRole(root, 'checkbox'), 'onKeyDown')\n if (typeof handler !== 'function') throw new Error('Missing checkbox keyboard handler')\n const preventDefault = vi.fn()\n await act(async () => {\n Reflect.apply(handler, undefined, [{ nativeEvent: { key: ' ', repeat: false }, defaultPrevented: false, preventDefault }])\n await Promise.resolve()\n })\n expect(hostKey).toHaveBeenCalledOnce()\n expect(preventDefault).toHaveBeenCalledOnce()\n expect(change).toHaveBeenCalledTimes(disabled ? 0 : 1)\n expect(change.mock.calls).toEqual(disabled ? [] : [[false]])\n await act(async () => {\n Reflect.apply(handler, undefined, [{ nativeEvent: { key: ' ', repeat: true }, defaultPrevented: false, preventDefault }])\n Reflect.apply(handler, undefined, [{ nativeEvent: { key: ' ', repeat: false }, defaultPrevented: true, preventDefault }])\n await Promise.resolve()\n })\n expect(change).toHaveBeenCalledTimes(disabled ? 0 : 1)\n})\n\ntest('media thumbnail keeps controlled selection and blocks loading requests', async () => {\n const onSelectionChange = vi.fn()\n const props = { label: 'Landscape', selected: true, onSelectionChange }\n const root = await renderNative(<LumenMediaThumbnail {...props}><LumenText>Photo</LumenText></LumenMediaThumbnail>)\n const button = findByAccessibilityLabel(root, 'Landscape')\n expect(readProp(button, 'accessibilityState')).toMatchObject({ selected: true })\n await runNativeAction(() => {\n callAction(readProp(button, 'onPress'), 'Missing thumbnail action')\n })\n expect(onSelectionChange).toHaveBeenCalledWith(false)\n await runNativeAction(() => {\n root.render(<LumenProvider><LumenMediaThumbnail {...props} state=\"loading\"><LumenText>Photo</LumenText></LumenMediaThumbnail></LumenProvider>)\n })\n const loading = findByAccessibilityLabel(root, 'Landscape')\n expect(readProp(loading, 'accessibilityState')).toMatchObject({ busy: true, disabled: true })\n await runNativeAction(() => {\n callAction(readProp(loading, 'onPress'), 'Missing loading action')\n })\n expect(onSelectionChange).toHaveBeenCalledTimes(1)\n})\n\ntest('native media inspection actions request bounded values and respect disabled state', async () => {\n const onValueChange = vi.fn()\n const props = { label: 'Inspect', value: { zoom: 2, x: 1, y: 0 }, onValueChange }\n const root = await renderNative(<LumenMediaViewport {...props}><LumenText>Photo</LumenText></LumenMediaViewport>)\n const right = root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'button')\n expect(right).toHaveLength(7)\n const fit = right[2]\n if (!fit) throw new Error('Missing fit action')\n await runNativeAction(() => {\n callAction(readProp(fit, 'onPress'), 'Missing fit handler')\n })\n expect(onValueChange).toHaveBeenCalledWith({ zoom: 1, x: 0, y: 0 })\n await runNativeAction(() => {\n root.render(\n <LumenProvider>\n <LumenMediaViewport {...props} disabled><LumenText>Photo</LumenText></LumenMediaViewport>\n </LumenProvider>\n )\n })\n expect(root.container.queryAll(instance => readProp(instance, 'accessibilityRole') === 'button').every(instance => {\n const state = readProp(instance, 'accessibilityState')\n return typeof state === 'object' && state !== null && 'disabled' in state && state.disabled === true\n })).toBe(true)\n})\n\ntest.each([true, false])('media pinch-to-pan preserves the host-accepted zoom (accept=%s)', async accept => {\n const change = vi.fn<(value: LumenMediaViewportValue) => void>()\n const Harness = () => {\n const [value, setValue] = useState<LumenMediaViewportValue>({ zoom: 2, x: 0, y: 0 })\n\n return (\n <LumenMediaViewport\n label=\"Inspect gesture\"\n value={value}\n onValueChange={next => {\n change(next)\n if (accept) setValue(next)\n }}\n >\n <LumenText>Photo</LumenText>\n </LumenMediaViewport>\n )\n }\n const root = await renderNative(<Harness />)\n const dispatch = async (property: string, nativeEvent: unknown): Promise<void> => {\n const handler = readProp(findByAccessibilityLabel(root, 'Inspect gesture'), property)\n if (typeof handler !== 'function') throw new Error(`Missing media ${property} callback`)\n await runNativeAction(() => {\n Reflect.apply(handler, undefined, [{ nativeEvent }])\n })\n }\n const touch = (pageX: number, pageY: number) => ({ pageX, pageY })\n\n await dispatch('onLayout', { layout: { width: 200, height: 100 } })\n await dispatch('onResponderGrant', { pageX: 50, pageY: 50, touches: [touch(50, 50), touch(100, 50)] })\n await dispatch('onResponderMove', { pageX: 50, pageY: 50, touches: [touch(50, 50), touch(150, 50)] })\n expect(change).toHaveBeenLastCalledWith({ zoom: 4, x: 0, y: 0 })\n change.mockClear()\n await dispatch('onResponderMove', { pageX: 150, pageY: 50, touches: [touch(150, 50)] })\n expect(change).not.toHaveBeenCalled()\n await dispatch('onResponderMove', { pageX: 175, pageY: 50, touches: [touch(175, 50)] })\n expect(change).toHaveBeenLastCalledWith({ zoom: accept ? 4 : 2, x: accept ? 1 / 12 : 0.25, y: 0 })\n})\n",
60198
60417
  "packages/react-native/src/content-components.tsx": "import { type ReactElement, type ReactNode } from 'react'\nimport {\n type DimensionValue,\n Pressable,\n Text,\n type TextStyle,\n View,\n type ViewProps\n} from 'react-native'\nimport { Circle, Line, Polygon, Polyline, Rect, Svg } from 'react-native-svg'\n\nimport {\n type LumenBackdropIntensity,\n type LumenGraphicSize,\n type LumenIllustrationSize,\n resolveLumenBackdropOpacity,\n resolveLumenDisclosureState,\n resolveLumenGraphicSize,\n resolveLumenIllustrationSize,\n resolveLumenSkeletonHeight } from './content-recipes.js'\nimport {\n type LumenIllustrationElement,\n lumenIllustrations\n} from './illustrations.generated.js'\nimport type { LumenViewRef } from './native-ref-types.js'\nimport { resolveLumenButtonOpacity } from './recipes.js'\nimport { useLumenTheme } from './theme-context.js'\nimport { lumenGraphicOpacities, lumenGraphicStrokeWidths } from './tokens.generated.js'\n\nexport type LumenSkeletonShape = 'circle' | 'rectangle' | 'text'\n\nexport interface LumenSkeletonProps extends Omit<ViewProps, 'children'> {\n height?: number\n label?: string\n ref?: LumenViewRef\n shape?: LumenSkeletonShape\n width?: DimensionValue\n}\n\nexport const LumenSkeleton = ({\n height = 16,\n label,\n ref,\n shape = 'text',\n style,\n width,\n ...props\n}: LumenSkeletonProps): ReactElement => {\n const theme = useLumenTheme()\n const safeHeight = resolveLumenSkeletonHeight(height)\n const resolvedWidth = width ?? (shape === 'circle' ? safeHeight : '100%')\n let borderRadius: number = theme.radii.md\n\n if (shape === 'circle') borderRadius = safeHeight / 2\n\n if (shape === 'text') borderRadius = theme.radii.sm\n\n return (\n <View\n ref={ref}\n {...props}\n accessibilityElementsHidden={!label}\n accessibilityLabel={label}\n accessibilityRole={label ? 'progressbar' : undefined}\n aria-busy={label ? true : undefined}\n importantForAccessibility={label ? 'yes' : 'no-hide-descendants'}\n style={[\n {\n backgroundColor: theme.colors.surfaceStrong,\n borderRadius,\n height: safeHeight,\n opacity: 0.72,\n width: resolvedWidth\n },\n style\n ]}\n />\n )\n}\n\nexport type LumenGraphicTone = 'accent' | 'brand' | 'neutral'\nexport type LumenGraphicVariant = 'glow' | 'grid' | 'orbit'\n\nexport interface LumenGraphicProps extends ViewProps {\n children?: ReactNode\n label?: string\n ref?: LumenViewRef\n size?: LumenGraphicSize\n tone?: LumenGraphicTone\n variant?: LumenGraphicVariant\n}\n\nconst graphicLines = Array.from({ length: 7 }, (_, index) => index)\nconst backdropDots = Array.from({ length: 48 }, (_, index) => index)\n\nexport const LumenGraphic = ({\n children,\n label,\n ref,\n size = 'md',\n style,\n tone = 'brand',\n variant = 'orbit',\n ...props\n}: LumenGraphicProps): ReactElement => {\n const theme = useLumenTheme()\n const dimension = resolveLumenGraphicSize(size)\n\n const color = {\n accent: theme.colors.accent,\n brand: theme.colors.brand,\n neutral: theme.colors.inkMuted\n }[tone]\n\n return (\n <View\n ref={ref}\n {...props}\n accessible={Boolean(label)}\n accessibilityElementsHidden={!label}\n accessibilityLabel={label}\n accessibilityRole={label ? 'image' : undefined}\n importantForAccessibility={label ? 'yes' : 'no-hide-descendants'}\n style={[\n {\n alignItems: 'center',\n height: dimension,\n justifyContent: 'center',\n overflow: 'hidden',\n position: 'relative',\n width: dimension\n },\n style\n ]}\n >\n {variant === 'glow' ?\n <View style={{ backgroundColor: color, borderRadius: dimension / 2, height: '72%', opacity: 0.18, width: '72%' }} /> :\n null}\n {variant === 'grid' ?\n (\n <View style={{ height: '76%', opacity: 0.18, position: 'absolute', width: '76%' }}>\n {graphicLines.map(index => (\n <View\n key={`horizontal-${index}`}\n style={{ backgroundColor: color, height: 1, left: 0, position: 'absolute', right: 0, top: `${index * 16.66}%` }}\n />\n ))}\n {graphicLines.map(index => (\n <View\n key={`vertical-${index}`}\n style={{ backgroundColor: color, bottom: 0, left: `${index * 16.66}%`, position: 'absolute', top: 0, width: 1 }}\n />\n ))}\n </View>\n ) :\n null}\n {variant === 'orbit' ?\n (\n <>\n {[0.9, 0.62, 0.34].map(ratio => (\n <View\n key={ratio}\n style={{\n borderColor: color,\n borderRadius: dimension,\n borderWidth: 1,\n height: dimension * ratio,\n opacity: 0.24,\n position: 'absolute',\n width: dimension * ratio\n }}\n />\n ))}\n <View style={{ backgroundColor: color, height: 1, opacity: 0.18, position: 'absolute', width: '72%' }} />\n <View style={{ backgroundColor: color, height: '72%', opacity: 0.18, position: 'absolute', width: 1 }} />\n </>\n ) :\n null}\n <View style={{ alignItems: 'center', justifyContent: 'center', position: 'absolute' }}>\n {children}\n </View>\n </View>\n )\n}\n\nexport type LumenBackdropTone = 'accent' | 'brand' | 'neutral'\nexport type LumenBackdropVariant = 'aurora' | 'dots' | 'grid' | 'rays'\n\nexport interface LumenBackdropProps extends ViewProps {\n children?: ReactNode\n intensity?: LumenBackdropIntensity\n ref?: LumenViewRef\n tone?: LumenBackdropTone\n variant?: LumenBackdropVariant\n}\n\nexport const LumenBackdrop = ({\n children,\n intensity = 'medium',\n ref,\n style,\n tone = 'brand',\n variant = 'aurora',\n ...props\n}: LumenBackdropProps): ReactElement => {\n const theme = useLumenTheme()\n const color = { accent: theme.colors.accent, brand: theme.colors.brand, neutral: theme.colors.inkMuted }[tone]\n const secondaryColor = tone === 'neutral' ? theme.colors.surfaceStrong : theme.colors.accent\n const opacity = resolveLumenBackdropOpacity(intensity)\n\n return (\n <View ref={ref} {...props} style={[{ minHeight: 160, overflow: 'hidden', position: 'relative' }, style]}>\n <View\n accessibilityElementsHidden\n importantForAccessibility=\"no-hide-descendants\"\n style={{ bottom: 0, left: 0, opacity, pointerEvents: 'none', position: 'absolute', right: 0, top: 0 }}\n >\n {variant === 'aurora' ?\n (\n <>\n <View style={{ backgroundColor: color, borderRadius: 140, height: 280, left: -70, opacity: 0.22, position: 'absolute', top: -100, width: 280 }} />\n <View style={{ backgroundColor: secondaryColor, borderRadius: 120, bottom: -110, height: 240, opacity: 0.18, position: 'absolute', right: -50, width: 240 }} />\n </>\n ) :\n null}\n {variant === 'grid' ?\n (\n <>\n {graphicLines.map(index => (\n <View key={`backdrop-horizontal-${index}`} style={{ backgroundColor: color, height: 1, left: 0, opacity: 0.18, position: 'absolute', right: 0, top: `${index * 16.66}%` }} />\n ))}\n {graphicLines.map(index => (\n <View key={`backdrop-vertical-${index}`} style={{ backgroundColor: color, bottom: 0, left: `${index * 16.66}%`, opacity: 0.18, position: 'absolute', top: 0, width: 1 }} />\n ))}\n </>\n ) :\n null}\n {variant === 'dots' ?\n backdropDots.map(index => (\n <View\n key={`backdrop-dot-${index}`}\n style={{\n backgroundColor: color,\n borderRadius: 2,\n height: 3,\n left: `${(index % 8) * 14 + 1}%`,\n opacity: 0.28,\n position: 'absolute',\n top: `${Math.floor(index / 8) * 19 + 2}%`,\n width: 3\n }}\n />\n )) :\n null}\n {variant === 'rays' ?\n graphicLines.map(index => (\n <View\n key={`backdrop-ray-${index}`}\n style={{\n backgroundColor: color,\n height: 1,\n left: '50%',\n opacity: 0.2,\n position: 'absolute',\n top: '50%',\n transform: [{ rotate: `${index * 24 - 72}deg` }, { translateX: -240 }],\n width: 480\n }}\n />\n )) :\n null}\n </View>\n <View style={{ zIndex: 1 }}>{children}</View>\n </View>\n )\n}\n\nexport type LumenIllustrationTone = 'accent' | 'auto' | 'brand' | 'neutral'\nexport type LumenIllustrationVariant = 'empty' | 'error' | 'offline' | 'success'\n\nexport interface LumenIllustrationProps extends ViewProps {\n label?: string\n ref?: LumenViewRef\n size?: LumenIllustrationSize\n tone?: LumenIllustrationTone\n variant?: LumenIllustrationVariant\n}\n\nconst illustrationPoints = (points: readonly number[]): string => {\n const pairs: string[] = []\n\n for (let index = 0; index < points.length; index += 2) {\n pairs.push(`${String(points[index])},${String(points[index + 1])}`)\n }\n\n return pairs.join(' ')\n}\n\nconst renderIllustrationElement = (\n element: LumenIllustrationElement,\n color: string,\n index: number,\n strokeWidth: number\n): ReactElement => {\n const shared = {\n fill: 'none',\n stroke: color,\n strokeLinecap: 'round' as const,\n strokeLinejoin: 'round' as const,\n strokeWidth\n }\n\n if (element.kind === 'circle') {\n return <Circle key={index} {...shared} cx={element.cx} cy={element.cy} r={element.r} />\n }\n\n if (element.kind === 'rounded-rect') {\n return (\n <Rect\n key={index}\n {...shared}\n height={element.height}\n rx={element.radius}\n transform={`translate(${element.x} ${element.y})`}\n width={element.width}\n />\n )\n }\n\n const points = illustrationPoints(element.points)\n\n if (element.kind === 'line') {\n return (\n <Line\n key={index}\n {...shared}\n x1={element.points[0]}\n x2={element.points[2]}\n y1={element.points[1]}\n y2={element.points[3]}\n />\n )\n }\n\n return element.kind === 'polygon' ?\n <Polygon key={index} {...shared} points={points} /> :\n <Polyline key={index} {...shared} points={points} />\n}\n\nexport const LumenIllustration = ({\n label,\n ref,\n size = 'md',\n style,\n tone = 'auto',\n variant = 'empty',\n ...props\n}: LumenIllustrationProps): ReactElement => {\n const theme = useLumenTheme()\n const dimension = resolveLumenIllustrationSize(size)\n\n const semanticColor = {\n empty: theme.colors.brand,\n error: theme.colors.danger,\n offline: theme.colors.inkMuted,\n success: theme.colors.success\n }[variant]\n\n const color = tone === 'auto' ?\n semanticColor :\n {\n accent: theme.colors.accent,\n brand: theme.colors.brand,\n neutral: theme.colors.inkMuted\n }[tone]\n\n const strokeWidth = lumenGraphicStrokeWidths.standard\n const artwork = lumenIllustrations[variant]\n\n return (\n <View\n ref={ref}\n {...props}\n accessible={Boolean(label)}\n accessibilityElementsHidden={!label}\n accessibilityLabel={label}\n accessibilityRole={label ? 'image' : undefined}\n importantForAccessibility={label ? 'yes' : 'no-hide-descendants'}\n style={[{ alignItems: 'center', height: dimension, justifyContent: 'center', position: 'relative', width: dimension }, style]}\n >\n <Svg height={dimension} viewBox=\"0 0 120 120\" width={dimension}>\n <Circle\n cx=\"60\"\n cy=\"60\"\n fill={color}\n fillOpacity={lumenGraphicOpacities.wash}\n r=\"48\"\n />\n {artwork.elements.map((element, index) => renderIllustrationElement(element, color, index, strokeWidth))}\n </Svg>\n </View>\n )\n}\n\nexport interface LumenDisclosureProps extends Omit<ViewProps, 'children'> {\n children: ReactNode\n description?: string\n disabled?: boolean\n expanded: boolean\n graphic?: ReactNode\n onExpandedChange: (expanded: boolean) => void\n ref?: LumenViewRef\n title: string\n}\n\nexport const LumenDisclosure = ({\n children,\n description,\n disabled = false,\n expanded,\n graphic,\n onExpandedChange,\n ref,\n style,\n title,\n ...props\n}: LumenDisclosureProps): ReactElement => {\n const theme = useLumenTheme()\n const state = resolveLumenDisclosureState(expanded, disabled)\n\n return (\n <View\n ref={ref}\n {...props}\n style={[\n {\n backgroundColor: theme.colors.surface,\n borderColor: theme.colors.line,\n borderRadius: theme.radii.md,\n borderWidth: 1,\n overflow: 'hidden'\n },\n style\n ]}\n >\n <Pressable\n accessibilityRole=\"button\"\n accessibilityState={{ disabled, expanded }}\n aria-disabled={disabled}\n aria-expanded={expanded}\n disabled={disabled}\n onPress={() => {\n onExpandedChange(!expanded)\n }}\n style={({ pressed }) => ({\n alignItems: 'center',\n flexDirection: 'row',\n gap: theme.spacing.md,\n minHeight: 48,\n opacity: state.opacity * resolveLumenButtonOpacity(disabled, pressed),\n paddingHorizontal: theme.spacing.lg,\n paddingVertical: theme.spacing.md\n })}\n >\n {graphic ? <View>{graphic}</View> : null}\n <View style={{ flex: 1, gap: theme.spacing.xs }}>\n <Text\n style={{\n color: theme.colors.ink,\n fontSize: theme.fontSizes.sm,\n fontWeight: String(theme.fontWeights.semibold) as TextStyle['fontWeight']\n }}\n >\n {title}\n </Text>\n {description ?\n <Text style={{ color: theme.colors.inkMuted, fontSize: theme.fontSizes.xs }}>{description}</Text> :\n null}\n </View>\n <Text\n accessibilityElementsHidden\n importantForAccessibility=\"no-hide-descendants\"\n style={{ color: theme.colors.inkSoft, fontSize: theme.fontSizes.lg }}\n >\n {expanded ? '−' : '+'}\n </Text>\n </Pressable>\n {state.contentVisible ?\n (\n <View\n style={{\n borderTopColor: theme.colors.line,\n borderTopWidth: 1,\n padding: theme.spacing.lg\n }}\n >\n {children}\n </View>\n ) :\n null}\n </View>\n )\n}\n",
60199
60418
  "packages/react-native/src/content-recipes.test.ts": "import { describe, expect, test } from 'vitest'\n\nimport {\n resolveLumenBackdropOpacity,\n resolveLumenDisclosureState,\n resolveLumenGraphicSize,\n resolveLumenIllustrationSize,\n resolveLumenSkeletonHeight\n} from './content-recipes.js'\n\ndescribe('content state recipes', () => {\n test('preserves controlled disclosure state', () => {\n expect(resolveLumenDisclosureState(true, false)).toEqual({\n contentVisible: true,\n disabled: false,\n opacity: 1\n })\n expect(resolveLumenDisclosureState(false, true)).toEqual({\n contentVisible: false,\n disabled: true,\n opacity: 0.52\n })\n })\n\n test('normalizes invalid skeleton heights', () => {\n expect(resolveLumenSkeletonHeight(24)).toBe(24)\n expect(resolveLumenSkeletonHeight(0)).toBe(16)\n expect(resolveLumenSkeletonHeight(Number.NaN)).toBe(16)\n })\n\n test('keeps graphic sizes aligned with the shared contract', () => {\n expect(resolveLumenGraphicSize('sm')).toBe(160)\n expect(resolveLumenGraphicSize('md')).toBe(240)\n expect(resolveLumenGraphicSize('lg')).toBe(320)\n })\n\n test('normalizes backdrop intensity and illustration sizes', () => {\n expect(resolveLumenBackdropOpacity('subtle')).toBe(0.4)\n expect(resolveLumenBackdropOpacity('medium')).toBe(0.68)\n expect(resolveLumenBackdropOpacity('strong')).toBe(1)\n expect(resolveLumenIllustrationSize('sm')).toBe(96)\n expect(resolveLumenIllustrationSize('md')).toBe(128)\n expect(resolveLumenIllustrationSize('lg')).toBe(176)\n })\n})\n",
60200
60419
  "packages/react-native/src/content-recipes.ts": "import { lumenGraphicFrameSizes, lumenIllustrationSizes } from './tokens.generated.js'\n\nexport interface LumenDisclosureState {\n contentVisible: boolean\n disabled: boolean\n opacity: number\n}\n\nexport const resolveLumenDisclosureState = (\n expanded: boolean,\n disabled: boolean\n): LumenDisclosureState => ({\n contentVisible: expanded,\n disabled,\n opacity: disabled ? 0.52 : 1\n})\n\nexport const resolveLumenSkeletonHeight = (height: number): number => (\n Number.isFinite(height) && height > 0 ? height : 16\n)\n\nexport type LumenGraphicSize = 'lg' | 'md' | 'sm'\n\nexport const resolveLumenGraphicSize = (size: LumenGraphicSize): number => ({\n lg: lumenGraphicFrameSizes.lg,\n md: lumenGraphicFrameSizes.md,\n sm: lumenGraphicFrameSizes.sm\n})[size]\n\nexport type LumenBackdropIntensity = 'medium' | 'strong' | 'subtle'\n\nexport const resolveLumenBackdropOpacity = (\n intensity: LumenBackdropIntensity\n): number => ({ medium: 0.68, strong: 1, subtle: 0.4 })[intensity]\n\nexport type LumenIllustrationSize = 'lg' | 'md' | 'sm'\n\nexport const resolveLumenIllustrationSize = (\n size: LumenIllustrationSize\n): number => ({\n lg: lumenIllustrationSizes.lg,\n md: lumenIllustrationSizes.md,\n sm: lumenIllustrationSizes.sm\n})[size]\n",
@@ -60202,11 +60421,11 @@
60202
60421
  "packages/react-native/src/data-table-components.test.tsx": "import { act, createElement, type ReactElement } from 'react'\n\nimport { createRoot, type Root, type TestInstance } from 'test-renderer'\nimport { afterEach, expect, test, vi } from 'vitest'\n\nimport { LumenDataTable, type LumenDataTableProps } from './table-components.js'\n\nObject.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })\nvi.mock('react-native', () => ({\n View: (props: Record<string, unknown>): ReactElement => createElement('View', props),\n ScrollView: (props: Record<string, unknown>): ReactElement => createElement('ScrollView', props)\n}))\nvi.mock('./primitives.js', () => ({\n LumenButton: (props: Record<string, unknown>): ReactElement => createElement('Button', props),\n LumenText: (props: Record<string, unknown>): ReactElement => createElement('Text', props)\n}))\nvi.mock('./selection-components.js', () => ({\n LumenCheckbox: (props: Record<string, unknown>): ReactElement => createElement('Checkbox', props)\n}))\nvi.mock('./theme-context.js', () => ({ useLumenTheme: () => ({ spacing: { xs: 4, sm: 8, md: 16 },\n radii: { md: 8 },\n colors: { line: '#888' } }) }))\nconst roots: Root[] = []\nconst columns = [{ key: 'amount', label: 'Amount', sortable: true }]\nconst rows = [\n { id: 'large', label: 'Large', cells: { amount: { text: '20 items', sortValue: 20 } } },\n { id: 'small', label: 'Small', cells: { amount: { text: '2 items', sortValue: 2 } } },\n { id: 'locked', label: 'Locked', disabled: true, cells: { amount: { text: 'Missing', sortValue: null } } }\n]\nafterEach(() => {\n act(() => {\n for (const root of roots) root.unmount()\n })\n roots.length = 0\n})\nconst read = (instance: TestInstance, key: string): unknown => (instance.props as Record<string, unknown>)[key]\nconst render = (props: Partial<LumenDataTableProps> = {}): {\n root: Root\n selection: ReturnType<typeof vi.fn>\n sort: ReturnType<typeof vi.fn>\n} => {\n const root = createRoot()\n const selection = vi.fn()\n const sort = vi.fn()\n\n roots.push(root)\n act(() => {\n root.render(\n <LumenDataTable\n label=\"Synthetic records\"\n rows={rows}\n columns={columns}\n selectedIds={new Set(['hidden', 'locked'])}\n onSelectionChange={selection}\n onSortChange={sort}\n {...props}\n />\n )\n })\n\n return { root, selection, sort }\n}\nconst find = (root: Root, type: string, label: string): TestInstance => {\n const item = root.container.queryAll(instance => instance.type === type && read(instance, 'accessibilityLabel') === label)[0]\n\n if (!item) throw new Error(`Missing ${label}`)\n\n return item\n}\nconst invoke = (instance: TestInstance, key: string, args: readonly unknown[] = []): void => {\n const handler = read(instance, key)\n\n if (typeof handler !== 'function') throw new Error(`Missing ${key}`)\n act(() => {\n Reflect.apply(handler, undefined, args)\n })\n}\nconst order = (root: Root): readonly unknown[] => root.container.queryAll(item => item.type === 'Checkbox').map(item => read(item, 'label'))\n\ntest.each([null, [], ['small'], {}, new Set([42])])('malformed controlled selection renders invalid feedback (%#)', selectedIds => {\n const props: Partial<LumenDataTableProps> = { onSelectionChange: undefined, invalidLabel: 'Invalid selection' }\n Reflect.set(props, 'selectedIds', selectedIds)\n const { root } = render(props)\n expect(root.container.queryAll(item => item.type === 'Text').some(item => item.children.includes('Invalid selection'))).toBe(true)\n expect(root.container.queryAll(item => item.type === 'Checkbox')).toHaveLength(0)\n})\n\ntest('client sorting changes displayed stable rows, localizes status and proposes controlled next sort', () => {\n const { root, sort } = render({ sortMode: 'client',\n sort: { key: 'amount', direction: 'ascending' },\n formatSort: value => value?.direction === 'ascending' ? 'Increasing' : 'Decreasing' })\n\n expect(order(root)).toEqual(['Small', 'Large', 'Locked'])\n expect(find(root, 'Button', 'Amount, Increasing')).toBeDefined()\n invoke(find(root, 'Button', 'Amount, Increasing'), 'onPress')\n expect(sort).toHaveBeenCalledWith({ key: 'amount', direction: 'descending' })\n expect(order(root)).toEqual(['Small', 'Large', 'Locked'])\n expect(rows.map(row => row.id)).toEqual(['large', 'small', 'locked'])\n})\n\ntest('manual sorting retains host order while emitting a sort request', () => {\n const { root, sort } = render({ sortMode: 'manual', sort: { key: 'amount', direction: 'ascending' } })\n\n expect(order(root)).toEqual(['Large', 'Small', 'Locked'])\n invoke(find(root, 'Button', 'Amount, ascending'), 'onPress')\n expect(sort).toHaveBeenCalledWith({ key: 'amount', direction: 'descending' })\n expect(order(root)).toEqual(['Large', 'Small', 'Locked'])\n})\n\ntest('bulk and individual selection preserve hidden and disabled IDs without mutating controlled input', () => {\n const ids = new Set(['hidden', 'locked'])\n const { root, selection } = render({ selectedIds: ids })\n\n invoke(find(root, 'Button', 'Select visible'), 'onPress')\n expect(selection).toHaveBeenLastCalledWith(new Set(['hidden', 'locked', 'large', 'small']))\n invoke(find(root, 'Checkbox', 'Small'), 'onCheckedChange', [true])\n expect(selection).toHaveBeenLastCalledWith(new Set(['hidden', 'locked', 'small']))\n invoke(find(root, 'Checkbox', 'Locked'), 'onCheckedChange', [false])\n expect(selection).toHaveBeenCalledTimes(2)\n expect(ids).toEqual(new Set(['hidden', 'locked']))\n const selected = render({ selectedIds: new Set(['hidden', 'locked', 'large', 'small']), deselectAllLabel: 'Clear visible' })\n\n invoke(find(selected.root, 'Button', 'Clear visible'), 'onPress')\n expect(selected.selection).toHaveBeenCalledWith(ids)\n})\n\ntest('disabled and readOnly block sort and selection proposals even for retained selected rows', () => {\n for (const props of [{ disabled: true }, { readOnly: true }]) {\n const { root, selection, sort } = render(props)\n\n invoke(find(root, 'Button', 'Amount'), 'onPress')\n invoke(find(root, 'Button', 'Select visible'), 'onPress')\n invoke(find(root, 'Checkbox', 'Small'), 'onCheckedChange', [true])\n expect(selection).not.toHaveBeenCalled()\n expect(sort).not.toHaveBeenCalled()\n expect(read(find(root, 'Checkbox', 'Small'), 'disabled')).toBe(true)\n }\n})\n\ntest('loading, even an empty error, invalid IDs and empty data replace stale controls', () => {\n for (const props of [{ loading: true }, { error: '' }, { rows: [rows[0], rows[0]].filter(row => row !== undefined) }, { rows: [] }]) {\n const { root, selection, sort } = render(props)\n\n expect(root.container.queryAll(item => item.type === 'Button' || item.type === 'Checkbox')).toHaveLength(0)\n expect(selection).not.toHaveBeenCalled()\n expect(sort).not.toHaveBeenCalled()\n }\n})\n\ntest('localized error recovery remains host-owned and disabled retry stays disabled', () => {\n const retry = vi.fn()\n const { root } = render({ error: 'Unavailable data', retryLabel: 'Try records again', onRetry: retry, disabled: true })\n\n expect(read(find(root, 'Button', 'Try records again'), 'disabled')).toBe(true)\n expect(retry).not.toHaveBeenCalled()\n const enabled = render({ error: 'Unavailable data', retryLabel: 'Try records again', onRetry: retry })\n\n invoke(find(enabled.root, 'Button', 'Try records again'), 'onPress')\n expect(retry).toHaveBeenCalledOnce()\n})\n\ntest('horizontal layout retains sort controls and inherited prototype cells use the missing label', () => {\n const { root } = render({ layout: 'scroll',\n columns: [{ key: 'toString', label: 'Prototype name', sortable: true }],\n rows: [{ id: 'one', label: 'One', cells: {} }],\n missingLabel: 'No cell value' })\n\n expect(root.container.queryAll(item => item.type === 'ScrollView' && read(item, 'horizontal') === true)).toHaveLength(1)\n expect(find(root, 'Button', 'Prototype name')).toBeDefined()\n expect(root.container.queryAll(item => item.type === 'Text' && read(item, 'children') === 'No cell value')).toHaveLength(1)\n})\n",
60203
60422
  "packages/react-native/src/datetime-components.tsx": "import {\n type ChangeEvent,\n createElement,\n type ReactElement,\n useEffect,\n useState\n} from 'react'\nimport {\n Platform,\n Pressable,\n Text,\n type TextStyle,\n View,\n type ViewProps\n} from 'react-native'\n\nimport {\n DateTimePickerAndroid,\n type DateTimePickerChangeEvent\n} from '@react-native-community/datetimepicker'\n\nimport {\n clampLumenDate,\n formatLumenDateDisplayValue,\n formatLumenDateInputValue,\n isValidLumenDate,\n type LumenDateRangeValue,\n parseLumenDateInputValue,\n resolveLumenDateBounds,\n resolveLumenDatePickerValue,\n resolveLumenDateRangeEndChange,\n resolveLumenDateRangeEndMinimum,\n resolveLumenDateRangeStartChange\n} from './form-recipes.js'\nimport { NativeDatePicker, type NativeDatePickerProps } from './native-date-picker.js'\nimport {\n resolveLumenAriaInvalid,\n resolveLumenButtonOpacity,\n resolveLumenValidationHint\n} from './recipes.js'\nimport { useLumenTheme } from './theme-context.js'\n\nconst defaultDateFormatter = new Intl.DateTimeFormat(undefined, { dateStyle: 'medium' })\n\ninterface DateSupportingTextProps {\n description?: string | undefined\n errorMessage?: string | undefined\n}\n\nconst resolveDateLabelColor = (\n danger: string,\n ink: string,\n errorMessage?: string\n): string => errorMessage ? danger : ink\n\nconst resolveDatePickerExpanded = (enabled: boolean, expanded: boolean): boolean => (\n enabled && expanded\n)\n\nconst dismissAndroidDatePicker = (): void => {\n if (Platform.OS === 'android') void DateTimePickerAndroid.dismiss('date')\n}\n\nconst DateSupportingText = ({ description, errorMessage }: DateSupportingTextProps): ReactElement | null => {\n const theme = useLumenTheme()\n const supportingText = errorMessage ?? description\n\n if (!supportingText) return null\n\n return (\n <Text\n accessibilityLiveRegion={errorMessage ? 'polite' : 'none'}\n style={{\n color: errorMessage ? theme.colors.danger : theme.colors.inkMuted,\n fontSize: theme.fontSizes.xs\n }}\n >\n {supportingText}\n </Text>\n )\n}\n\ninterface NativeDatePickerDisclosureProps extends NativeDatePickerProps {\n expanded: boolean\n label: string\n onDateChange: (date: Date) => void\n webValue: Date | null\n}\n\nconst NativeDatePickerDisclosure = ({\n expanded,\n label,\n onDateChange,\n webValue,\n ...props\n}: NativeDatePickerDisclosureProps): ReactElement | null => {\n if (!expanded || Platform.OS === 'android') return null\n\n if (Platform.OS === 'web') {\n return createElement('input', {\n 'aria-label': label,\n disabled: props.disabled,\n max: props.maximumDate ? formatLumenDateInputValue(props.maximumDate) : undefined,\n min: props.minimumDate ? formatLumenDateInputValue(props.minimumDate) : undefined,\n onChange: (event: ChangeEvent<HTMLInputElement>) => {\n const date = parseLumenDateInputValue(event.currentTarget.value)\n\n if (date) onDateChange(date)\n },\n style: {\n accentColor: props.accentColor,\n border: '1px solid currentColor',\n borderRadius: 6,\n colorScheme: props.themeVariant,\n font: 'inherit',\n minHeight: 44,\n padding: '0 12px'\n },\n type: 'date',\n value: webValue ? formatLumenDateInputValue(webValue) : ''\n })\n }\n\n return <NativeDatePicker {...props} />\n}\n\nexport interface LumenDateFieldProps extends Omit<ViewProps, 'children'> {\n description?: string\n enabled?: boolean\n errorMessage?: string\n formatValue?: (date: Date) => string\n label: string\n maximumDate?: Date\n minimumDate?: Date\n onValueChange: (value: Date) => void\n placeholder?: string\n value: Date | null\n}\n\nconst LumenDateFieldControl = ({\n description,\n enabled = true,\n errorMessage,\n formatValue,\n label,\n maximumDate,\n minimumDate,\n onValueChange,\n placeholder = 'Select a date',\n style,\n value,\n ...props\n}: LumenDateFieldProps): ReactElement => {\n const theme = useLumenTheme()\n const [expanded, setExpanded] = useState(false)\n const pickerExpanded = resolveDatePickerExpanded(enabled, expanded)\n const validValue = isValidLumenDate(value) ? value : null\n const pickerValue = resolveLumenDatePickerValue(value, minimumDate, maximumDate)\n\n const displayValue = formatLumenDateDisplayValue(\n value,\n placeholder,\n formatValue ?? defaultDateFormatter.format.bind(defaultDateFormatter)\n )\n\n const pickerBounds = resolveLumenDateBounds(minimumDate, maximumDate)\n\n useEffect(() => dismissAndroidDatePicker, [])\n\n const commitValue = (_event: DateTimePickerChangeEvent, selectedDate?: Date): void => {\n if (!selectedDate) return\n\n onValueChange(clampLumenDate(selectedDate, minimumDate, maximumDate))\n }\n\n const openPicker = (): void => {\n if (!enabled) return\n\n if (Platform.OS === 'android') {\n DateTimePickerAndroid.open({\n ...pickerBounds,\n mode: 'date',\n onChange: commitValue,\n value: pickerValue\n })\n\n return\n }\n\n setExpanded(previous => !previous)\n }\n\n return (\n <View {...props} style={[{ gap: theme.spacing.xs }, style]}>\n <Text\n style={{\n color: errorMessage ? theme.colors.danger : theme.colors.ink,\n fontSize: theme.fontSizes.sm,\n fontWeight: String(theme.fontWeights.semibold) as TextStyle['fontWeight']\n }}\n >\n {label}\n </Text>\n <Pressable\n accessibilityHint={resolveLumenValidationHint(errorMessage, description)}\n accessibilityLabel={`${label}, ${displayValue}`}\n accessibilityRole=\"button\"\n accessibilityState={{ disabled: !enabled, expanded: pickerExpanded }}\n aria-invalid={resolveLumenAriaInvalid(errorMessage)}\n disabled={!enabled}\n onPress={openPicker}\n style={({ pressed }) => ({\n alignItems: 'center',\n backgroundColor: theme.colors.surface,\n borderColor: errorMessage ? theme.colors.danger : theme.colors.line,\n borderRadius: theme.radii.sm,\n borderWidth: 1,\n flexDirection: 'row',\n minHeight: 44,\n opacity: enabled ? resolveLumenButtonOpacity(false, pressed) : 0.52,\n paddingHorizontal: theme.spacing.md\n })}\n >\n <Text\n style={{\n color: validValue ? theme.colors.ink : theme.colors.inkMuted,\n flex: 1,\n fontSize: theme.fontSizes.sm\n }}\n >\n {displayValue}\n </Text>\n <Text\n accessibilityElementsHidden\n importantForAccessibility=\"no-hide-descendants\"\n style={{ color: theme.colors.inkMuted }}\n >\n ▾\n </Text>\n </Pressable>\n <NativeDatePickerDisclosure\n {...pickerBounds}\n accentColor={theme.colors.brandSolid}\n disabled={!enabled}\n display={Platform.OS === 'ios' ? 'inline' : 'default'}\n expanded={pickerExpanded}\n label={label}\n mode=\"date\"\n onDateChange={selectedDate => {\n onValueChange(clampLumenDate(selectedDate, minimumDate, maximumDate))\n }}\n onChange={commitValue}\n themeVariant={theme.scheme}\n value={pickerValue}\n webValue={validValue}\n />\n <DateSupportingText\n {...(description ? { description } : {})}\n {...(errorMessage ? { errorMessage } : {})}\n />\n </View>\n )\n}\n\nexport const LumenDateField = (props: LumenDateFieldProps): ReactElement => (\n <LumenDateFieldControl key={String(props.enabled ?? true)} {...props} />\n)\n\nexport interface LumenDateRangeFieldProps extends Omit<ViewProps, 'children'> {\n description?: string\n enabled?: boolean\n endLabel?: string\n errorMessage?: string\n formatValue?: (date: Date) => string\n label: string\n maximumDate?: Date\n minimumDate?: Date\n onValueChange: (value: LumenDateRangeValue) => void\n placeholder?: string\n startLabel?: string\n value: LumenDateRangeValue\n}\n\nconst resolveSharedDateFieldProps = (\n enabled: boolean,\n formatValue?: (date: Date) => string,\n maximumDate?: Date,\n placeholder?: string\n): Pick<LumenDateFieldProps, 'enabled' | 'formatValue' | 'maximumDate' | 'placeholder'> => ({\n enabled,\n ...(formatValue ? { formatValue } : {}),\n ...(maximumDate ? { maximumDate } : {}),\n ...(placeholder ? { placeholder } : {})\n})\n\nexport const LumenDateRangeField = ({\n description,\n enabled = true,\n endLabel = 'End date',\n errorMessage,\n formatValue,\n label,\n maximumDate,\n minimumDate,\n onValueChange,\n placeholder,\n startLabel = 'Start date',\n style,\n value,\n ...props\n}: LumenDateRangeFieldProps): ReactElement => {\n const theme = useLumenTheme()\n const endMinimum = resolveLumenDateRangeEndMinimum(value.start, minimumDate)\n const labelColor = resolveDateLabelColor(theme.colors.danger, theme.colors.ink, errorMessage)\n const sharedFieldProps = resolveSharedDateFieldProps(enabled, formatValue, maximumDate, placeholder)\n\n return (\n <View\n {...props}\n accessibilityHint={resolveLumenValidationHint(errorMessage, description, props.accessibilityHint)}\n accessibilityLabel={props.accessibilityLabel ?? label}\n aria-invalid={resolveLumenAriaInvalid(errorMessage)}\n style={[{ gap: theme.spacing.sm }, style]}\n >\n <Text\n style={{\n color: labelColor,\n fontSize: theme.fontSizes.sm,\n fontWeight: String(theme.fontWeights.semibold) as TextStyle['fontWeight']\n }}\n >\n {label}\n </Text>\n <LumenDateField\n {...sharedFieldProps}\n label={startLabel}\n {...(minimumDate ? { minimumDate } : {})}\n onValueChange={start => {\n onValueChange(resolveLumenDateRangeStartChange(value, start, minimumDate, maximumDate))\n }}\n value={value.start}\n />\n <LumenDateField\n {...sharedFieldProps}\n label={endLabel}\n {...(endMinimum ? { minimumDate: endMinimum } : {})}\n onValueChange={end => {\n onValueChange(resolveLumenDateRangeEndChange(value, end, minimumDate, maximumDate))\n }}\n value={value.end}\n />\n <DateSupportingText\n {...(description ? { description } : {})}\n {...(errorMessage ? { errorMessage } : {})}\n />\n </View>\n )\n}\n",
60204
60423
  "packages/react-native/src/datetime.ts": "export {\n LumenDateField,\n type LumenDateFieldProps,\n LumenDateRangeField,\n type LumenDateRangeFieldProps\n} from './datetime-components.js'\nexport {\n type LumenDateRangeValue\n} from './form-recipes.js'\nexport { LumenTimeField, type LumenTimeFieldProps } from './time-components.js'\nexport { type LumenTimeSelection } from '@santi020k/lumen-core'\n",
60205
- "packages/react-native/src/field-context.ts": "import { createContext, use } from 'react'\n\nexport interface LumenFieldContextValue {\n descriptionId?: string\n error: boolean\n errorId?: string\n label: string\n labelId: string\n required: boolean\n}\n\nexport const LumenFieldContext = createContext<LumenFieldContextValue | null>(null)\n\nexport const useLumenFieldContext = (): LumenFieldContextValue | null => use(LumenFieldContext)\n",
60424
+ "packages/react-native/src/field-context.ts": "import { createContext, use } from 'react'\n\nexport interface LumenFieldContextValue {\n /** Spoken guidance for controls that announce field context through a hint instead of relationships. */\n description?: string\n descriptionId?: string\n error: boolean\n errorId?: string\n errorMessage?: string\n label: string\n labelId: string\n required: boolean\n}\n\nexport const LumenFieldContext = createContext<LumenFieldContextValue | null>(null)\n\nexport const useLumenFieldContext = (): LumenFieldContextValue | null => use(LumenFieldContext)\n",
60206
60425
  "packages/react-native/src/form-components.tsx": "import {\n type ReactElement,\n type ReactNode\n} from 'react'\nimport {\n Platform,\n Pressable,\n Switch,\n type SwitchProps,\n Text,\n TextInput,\n type TextInputProps,\n type TextStyle,\n View,\n type ViewProps\n} from 'react-native'\n\nimport { resolveLumenSearchFieldState } from './form-recipes.js'\nimport type { LumenSwitchRef, LumenTextInputRef, LumenViewRef } from './native-ref-types.js'\nimport { resolveLumenButtonOpacity } from './recipes.js'\nimport { useLumenTheme } from './theme-context.js'\n\nconst resolveSearchValue = <Value,>(value: Value | undefined, fallback: Value): Value => value ?? fallback\n\nexport interface LumenToggleProps extends Omit<SwitchProps, 'onValueChange' | 'value'> {\n description?: string\n label: string\n onValueChange: (value: boolean) => void\n ref?: LumenSwitchRef\n showLabel?: boolean\n value: boolean\n}\n\nexport const LumenToggle = ({\n accessibilityHint,\n description,\n disabled = false,\n label,\n onValueChange,\n ref,\n showLabel = true,\n style,\n value,\n ...props\n}: LumenToggleProps): ReactElement => {\n const theme = useLumenTheme()\n\n return (\n <Pressable\n accessibilityHint={accessibilityHint ?? description}\n accessibilityLabel={label}\n accessibilityRole=\"switch\"\n accessibilityState={{ checked: value, disabled }}\n aria-checked={value}\n disabled={disabled}\n onPress={() => {\n if (!disabled) onValueChange(!value)\n }}\n style={{\n alignItems: 'center',\n flexDirection: 'row',\n gap: theme.spacing.lg,\n minHeight: 44\n }}\n >\n {showLabel ?\n (\n <View style={{ flex: 1, gap: theme.spacing.xs }}>\n <Text\n style={{\n color: theme.colors.ink,\n fontSize: theme.fontSizes.sm,\n fontWeight: String(theme.fontWeights.semibold) as TextStyle['fontWeight']\n }}\n >\n {label}\n </Text>\n {description ?\n <Text style={{ color: theme.colors.inkSoft, fontSize: theme.fontSizes.xs }}>{description}</Text> :\n null}\n </View>\n ) :\n null}\n <Switch\n ref={ref}\n {...props}\n accessibilityElementsHidden\n aria-hidden\n disabled={disabled || Platform.OS === 'web'}\n importantForAccessibility=\"no-hide-descendants\"\n ios_backgroundColor={theme.colors.surfaceStrong}\n pointerEvents=\"none\"\n style={[{ opacity: disabled ? 0.52 : 1 }, style]}\n thumbColor={value ? theme.colors.onBrand : theme.colors.surface}\n trackColor={{ false: theme.colors.surfaceStrong, true: theme.colors.brandSolid }}\n value={value}\n />\n </Pressable>\n )\n}\n\nexport interface LumenSettingsRowProps extends ViewProps {\n control: ReactNode\n description?: string\n graphic?: ReactNode\n ref?: LumenViewRef\n title: string\n}\n\nexport const LumenSettingsRow = ({\n control,\n description,\n graphic,\n ref,\n style,\n title,\n ...props\n}: LumenSettingsRowProps): ReactElement => {\n const theme = useLumenTheme()\n\n return (\n <View\n ref={ref}\n {...props}\n style={[\n {\n alignItems: 'center',\n flexDirection: 'row',\n gap: theme.spacing.lg,\n minHeight: 56,\n paddingVertical: theme.spacing.sm\n },\n style\n ]}\n >\n {graphic ? <View>{graphic}</View> : null}\n <View style={{ flex: 1, gap: theme.spacing.xs }}>\n <Text\n style={{\n color: theme.colors.ink,\n fontSize: theme.fontSizes.sm,\n fontWeight: String(theme.fontWeights.semibold) as TextStyle['fontWeight']\n }}\n >\n {title}\n </Text>\n {description ?\n <Text style={{ color: theme.colors.inkMuted, fontSize: theme.fontSizes.xs }}>{description}</Text> :\n null}\n </View>\n <View>{control}</View>\n </View>\n )\n}\n\nexport interface LumenSearchFieldProps extends Omit<TextInputProps, 'onChangeText' | 'value'> {\n clearLabel?: string\n graphic?: ReactNode\n onChangeText: (value: string) => void\n prompt?: string\n ref?: LumenTextInputRef\n value: string\n}\n\nexport const LumenSearchField = ({\n accessibilityState,\n clearLabel = 'Clear search',\n editable = true,\n graphic,\n onChangeText,\n placeholder,\n placeholderTextColor,\n prompt = 'Search',\n ref,\n returnKeyType = 'search',\n style,\n value,\n ...props\n}: LumenSearchFieldProps): ReactElement => {\n const theme = useLumenTheme()\n const searchState = resolveLumenSearchFieldState(value, editable)\n\n return (\n <View\n style={{\n alignItems: 'center',\n backgroundColor: theme.colors.surface,\n borderColor: theme.colors.line,\n borderRadius: theme.radii.sm,\n borderWidth: 1,\n flexDirection: 'row',\n gap: theme.spacing.sm,\n minHeight: 44,\n opacity: searchState.opacity,\n paddingLeft: theme.spacing.md,\n paddingRight: theme.spacing.xs\n }}\n >\n {graphic}\n <TextInput\n ref={ref}\n {...props}\n accessibilityLabel={resolveSearchValue(props.accessibilityLabel, prompt)}\n accessibilityState={{ ...accessibilityState, disabled: !editable }}\n editable={editable}\n onChangeText={onChangeText}\n placeholder={resolveSearchValue(placeholder, prompt)}\n placeholderTextColor={resolveSearchValue(placeholderTextColor, theme.colors.inkMuted)}\n returnKeyType={returnKeyType}\n style={[\n {\n color: theme.colors.ink,\n flex: 1,\n fontSize: theme.fontSizes.sm,\n minHeight: 42,\n paddingVertical: theme.spacing.sm\n },\n style\n ]}\n value={value}\n />\n {searchState.showClearAction ?\n (\n <Pressable\n accessibilityLabel={clearLabel}\n accessibilityRole=\"button\"\n hitSlop={4}\n onPress={() => {\n onChangeText('')\n }}\n style={({ pressed }) => ({\n alignItems: 'center',\n justifyContent: 'center',\n minHeight: 44,\n minWidth: 44,\n opacity: resolveLumenButtonOpacity(false, pressed),\n paddingHorizontal: theme.spacing.sm\n })}\n >\n <Text\n style={{\n color: theme.colors.inkSoft,\n fontSize: theme.fontSizes.xs,\n fontWeight: String(theme.fontWeights.semibold) as TextStyle['fontWeight']\n }}\n >\n {clearLabel}\n </Text>\n </Pressable>\n ) :\n null}\n </View>\n )\n}\n",
60207
60426
  "packages/react-native/src/form-recipes.test.ts": "import { describe, expect, test } from 'vitest'\n\nimport {\n clampLumenDate,\n formatLumenDateDisplayValue,\n formatLumenDateInputValue,\n parseLumenDateInputValue,\n resolveLumenDateBounds,\n resolveLumenDatePickerValue,\n resolveLumenDateRangeEndChange,\n resolveLumenDateRangeStartChange,\n resolveLumenSearchFieldState\n} 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 test('normalizes and clamps picker dates to inclusive bounds', () => {\n const minimum = new Date(2026, 7, 10)\n const maximum = new Date(2026, 7, 20)\n\n expect(clampLumenDate(new Date(2026, 7, 5, 18), minimum, maximum)).toEqual(minimum)\n expect(clampLumenDate(new Date(2026, 7, 15, 18), minimum, maximum)).toEqual(new Date(2026, 7, 15))\n expect(resolveLumenDatePickerValue(null, minimum, maximum, new Date(2026, 7, 25))).toEqual(maximum)\n expect(resolveLumenDateBounds(maximum, minimum)).toEqual({\n maximumDate: maximum,\n minimumDate: maximum\n })\n expect(resolveLumenDateBounds(new Date(Number.NaN), maximum)).toEqual({ maximumDate: maximum })\n expect(resolveLumenDateBounds(minimum, new Date(Number.NaN))).toEqual({ minimumDate: minimum })\n expect(resolveLumenDatePickerValue(new Date(Number.NaN), minimum, maximum, minimum)).toEqual(minimum)\n })\n\n test('uses the placeholder without formatting an invalid controlled date', () => {\n const formatter = (date: Date): string => date.toISOString()\n\n expect(formatLumenDateDisplayValue(new Date(Number.NaN), 'Select a date', formatter))\n .toBe('Select a date')\n })\n\n test('keeps a controlled date range ordered as either boundary changes', () => {\n const initial = {\n end: new Date(2026, 7, 12),\n start: new Date(2026, 7, 10)\n }\n const movedStart = resolveLumenDateRangeStartChange(initial, new Date(2026, 7, 15))\n\n expect(movedStart).toEqual({\n end: new Date(2026, 7, 15),\n start: new Date(2026, 7, 15)\n })\n expect(resolveLumenDateRangeEndChange(movedStart, new Date(2026, 7, 8))).toEqual(movedStart)\n })\n\n test('round-trips valid browser date input without timezone conversion', () => {\n expect(formatLumenDateInputValue(new Date(2026, 7, 26, 23, 45))).toBe('2026-08-26')\n expect(parseLumenDateInputValue('2026-08-26')).toEqual(new Date(2026, 7, 26))\n expect(parseLumenDateInputValue('2026-02-30')).toBeNull()\n expect(parseLumenDateInputValue('08/26/2026')).toBeNull()\n })\n})\n",
60208
60427
  "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\nexport interface LumenDateRangeValue {\n end: Date | null\n start: Date | null\n}\n\nexport const isValidLumenDate = (value: Date | null | undefined): value is Date => (\n value !== null && value !== undefined && Number.isFinite(value.getTime())\n)\n\nconst normalizeLumenDate = (value: Date): Date => (\n isValidLumenDate(value) ? new Date(value.getFullYear(), value.getMonth(), value.getDate()) : new Date(0)\n)\n\nexport const formatLumenDateDisplayValue = (\n value: Date | null,\n placeholder: string,\n formatter: (date: Date) => string\n): string => isValidLumenDate(value) ? formatter(value) : placeholder\n\nconst padLumenDatePart = (value: number): string => String(value).padStart(2, '0')\n\nexport const formatLumenDateInputValue = (value: Date): string => {\n const normalized = normalizeLumenDate(value)\n\n return `${normalized.getFullYear()}-${padLumenDatePart(normalized.getMonth() + 1)}-${padLumenDatePart(normalized.getDate())}`\n}\n\nexport const parseLumenDateInputValue = (value: string): Date | null => {\n const match = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(value)\n\n if (!match) return null\n\n const year = Number(match[1])\n const month = Number(match[2]) - 1\n const day = Number(match[3])\n const date = new Date(year, month, day)\n\n if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) return null\n\n return date\n}\n\nexport interface LumenDateBounds {\n maximumDate?: Date\n minimumDate?: Date\n}\n\nexport const resolveLumenDateBounds = (\n minimumDate?: Date,\n maximumDate?: Date\n): LumenDateBounds => {\n const bounds: LumenDateBounds = {}\n\n if (isValidLumenDate(minimumDate)) bounds.minimumDate = normalizeLumenDate(minimumDate)\n\n if (isValidLumenDate(maximumDate)) bounds.maximumDate = normalizeLumenDate(maximumDate)\n\n if (bounds.minimumDate && bounds.maximumDate && bounds.maximumDate < bounds.minimumDate) {\n bounds.maximumDate = bounds.minimumDate\n }\n\n return bounds\n}\n\nexport const clampLumenDate = (\n value: Date,\n minimumDate?: Date,\n maximumDate?: Date\n): Date => {\n const normalizedValue = normalizeLumenDate(value)\n\n const { maximumDate: normalizedMaximum, minimumDate: normalizedMinimum } = resolveLumenDateBounds(\n minimumDate,\n maximumDate\n )\n\n if (normalizedMinimum && normalizedValue < normalizedMinimum) return normalizedMinimum\n\n if (normalizedMaximum && normalizedValue > normalizedMaximum) return normalizedMaximum\n\n return normalizedValue\n}\n\nexport const resolveLumenDatePickerValue = (\n value: Date | null,\n minimumDate?: Date,\n maximumDate?: Date,\n fallback = new Date()\n): Date => clampLumenDate(isValidLumenDate(value) ? value : fallback, minimumDate, maximumDate)\n\nexport const resolveLumenDateRangeEndMinimum = (\n start: Date | null,\n minimumDate?: Date\n): Date | undefined => {\n if (!start) return minimumDate\n\n if (!minimumDate || start > minimumDate) return start\n\n return minimumDate\n}\n\nexport const resolveLumenDateRangeStartChange = (\n value: LumenDateRangeValue,\n start: Date,\n minimumDate?: Date,\n maximumDate?: Date\n): LumenDateRangeValue => {\n const nextStart = clampLumenDate(start, minimumDate, maximumDate)\n const nextEnd = value.end && normalizeLumenDate(value.end) < nextStart ? nextStart : value.end\n\n return { end: nextEnd, start: nextStart }\n}\n\nexport const resolveLumenDateRangeEndChange = (\n value: LumenDateRangeValue,\n end: Date,\n minimumDate?: Date,\n maximumDate?: Date\n): LumenDateRangeValue => {\n const effectiveMinimum = resolveLumenDateRangeEndMinimum(value.start, minimumDate)\n\n return {\n end: clampLumenDate(end, effectiveMinimum, maximumDate),\n start: value.start\n }\n}\n",
60209
- "packages/react-native/src/foundation-primitives.tsx": "import {\n type ReactElement,\n type ReactNode\n} from 'react'\nimport {\n ActivityIndicator,\n type ActivityIndicatorProps,\n type ColorValue,\n Pressable,\n type PressableProps,\n Text,\n TextInput,\n type TextInputProps,\n type TextProps,\n type TextStyle,\n View,\n type ViewProps\n} from 'react-native'\n\nimport { LumenControlContentContext, useLumenControlContentColor } from './control-content-context.js'\nimport {\n type LumenFieldContextValue,\n useLumenFieldContext\n} from './field-context.js'\nimport type {\n LumenActivityIndicatorRef,\n LumenTextInputRef,\n LumenTextRef,\n LumenViewRef\n} from './native-ref-types.js'\nimport {\n resolveLumenButtonColors,\n resolveLumenButtonOpacity,\n resolveLumenButtonSize,\n resolveLumenControlMinHeight,\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?: LumenTextRef\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 const contentColor = useLumenControlContentColor()\n\n return (\n <Text\n ref={ref}\n {...props}\n style={[\n { color: tone === 'default' ? contentColor ?? theme.colors.ink : resolveLumenTextColor(theme.colors, tone) },\n resolveLumenTextStyle(theme, variant),\n style\n ]}\n />\n )\n}\n\nexport type LumenSurfacePadding = 'lg' | 'md' | 'none' | 'sm' | 'xl'\nexport type LumenSurfaceRadius = '2xl' | '3xl' | 'lg' | 'md' | 'none' | 'sm' | 'xl'\nexport type LumenSurfaceTone = 'canvas' | 'muted' | 'strong' | 'surface'\n\nexport interface LumenSurfaceProps extends ViewProps {\n padding?: LumenSurfacePadding\n radius?: LumenSurfaceRadius\n ref?: LumenViewRef\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' | 'success'\nexport type LumenControlSize = 'lg' | 'md' | 'sm'\nexport type LumenIconSize = 'lg' | 'md' | 'sm'\n\nexport interface LumenButtonProps extends Omit<PressableProps, 'children'> {\n children: ReactNode\n intent?: LumenButtonIntent\n loading?: boolean\n ref?: LumenViewRef\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 === true || loading\n\n return (\n <Pressable\n ref={ref}\n {...props}\n accessibilityRole=\"button\"\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 <LumenControlContentContext value={colors.color}>{children}</LumenControlContentContext>\n </Text>\n </Pressable>\n )\n}\n\nexport interface LumenTextFieldProps extends TextInputProps {\n 'aria-describedby'?: string\n 'aria-required'?: boolean\n error?: boolean\n ref?: LumenTextInputRef\n size?: LumenControlSize\n}\n\nconst hasExplicitLumenTextFieldLabel = (\n props: Pick<LumenTextFieldProps, 'accessibilityLabel' | 'aria-label' | 'aria-labelledby'>\n): boolean => [\n props.accessibilityLabel,\n props['aria-label'],\n props['aria-labelledby']\n].some(value => value !== undefined)\n\nconst resolveLumenTextFieldDescribedBy = (\n ariaDescribedBy: string | undefined,\n field: LumenFieldContextValue | null\n): string | undefined => {\n const inheritedDescription = [field?.descriptionId, field?.errorId]\n .filter((value): value is string => Boolean(value))\n .join(' ')\n\n return ariaDescribedBy ?? (inheritedDescription || undefined)\n}\n\nconst resolveLumenTextFieldFontSize = (\n size: LumenControlSize,\n mediumFontSize: number,\n smallFontSize: number\n): number => size === 'lg' ? mediumFontSize : smallFontSize\n\nconst resolveLumenTextFieldBorderColor = (\n isInvalid: boolean,\n dangerColor: ColorValue,\n lineColor: ColorValue\n): ColorValue => isInvalid ? dangerColor : lineColor\n\nconst resolveLumenTextFieldOpacity = (editable: boolean): number => editable ? 1 : 0.52\n\ninterface LumenTextFieldAccessibility {\n label: string | undefined\n labelledBy: string | undefined\n required: boolean | undefined\n}\n\nconst resolveLumenTextFieldAccessibility = (\n props: LumenTextFieldProps,\n field: LumenFieldContextValue | null,\n ariaRequired: boolean | undefined\n): LumenTextFieldAccessibility => {\n const hasExplicitLabel = hasExplicitLumenTextFieldLabel(props)\n\n return {\n label: hasExplicitLabel ? props.accessibilityLabel : field?.label,\n labelledBy: hasExplicitLabel ? props['aria-labelledby'] : field?.labelId,\n required: ariaRequired ?? field?.required\n }\n}\n\nexport const LumenTextField = ({\n 'aria-describedby': ariaDescribedBy,\n 'aria-required': ariaRequired,\n accessibilityState,\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 field = useLumenFieldContext()\n const minHeight = resolveLumenControlMinHeight(size)\n const fontSize = resolveLumenTextFieldFontSize(size, theme.fontSizes.md, theme.fontSizes.sm)\n const accessibility = resolveLumenTextFieldAccessibility(props, field, ariaRequired)\n const describedBy = resolveLumenTextFieldDescribedBy(ariaDescribedBy, field)\n const isInvalid = error || (field?.error ?? false)\n\n return (\n <TextInput\n ref={ref}\n {...props}\n accessibilityLabel={accessibility.label}\n accessibilityState={{\n ...accessibilityState,\n disabled: !editable\n }}\n aria-describedby={describedBy}\n aria-invalid={isInvalid || undefined}\n aria-labelledby={accessibility.labelledBy}\n aria-required={accessibility.required}\n editable={editable}\n placeholderTextColor={placeholderTextColor ?? theme.colors.inkMuted}\n style={[\n {\n backgroundColor: theme.colors.surface,\n borderColor: resolveLumenTextFieldBorderColor(\n isInvalid,\n theme.colors.danger,\n theme.colors.line\n ),\n borderRadius: theme.radii.sm,\n borderWidth: 1,\n color: theme.colors.ink,\n fontSize,\n minHeight,\n opacity: resolveLumenTextFieldOpacity(editable),\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?: LumenViewRef\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?: LumenViewRef\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?: LumenActivityIndicatorRef\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",
60428
+ "packages/react-native/src/foundation-primitives.tsx": "import {\n type ReactElement,\n type ReactNode\n} from 'react'\nimport {\n ActivityIndicator,\n type ActivityIndicatorProps,\n type ColorValue,\n Pressable,\n type PressableProps,\n Text,\n TextInput,\n type TextInputProps,\n type TextProps,\n type TextStyle,\n View,\n type ViewProps\n} from 'react-native'\n\nimport { LumenControlContentContext, useLumenControlContentColor } from './control-content-context.js'\nimport {\n type LumenFieldContextValue,\n useLumenFieldContext\n} from './field-context.js'\nimport type {\n LumenActivityIndicatorRef,\n LumenTextInputRef,\n LumenTextRef,\n LumenViewRef\n} from './native-ref-types.js'\nimport {\n resolveLumenButtonColors,\n resolveLumenButtonOpacity,\n resolveLumenButtonSize,\n resolveLumenControlMinHeight,\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?: LumenTextRef\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 const contentColor = useLumenControlContentColor()\n\n return (\n <Text\n ref={ref}\n {...props}\n style={[\n { color: tone === 'default' ? contentColor ?? theme.colors.ink : resolveLumenTextColor(theme.colors, tone) },\n resolveLumenTextStyle(theme, variant),\n style\n ]}\n />\n )\n}\n\nexport type LumenSurfacePadding = 'lg' | 'md' | 'none' | 'sm' | 'xl'\nexport type LumenSurfaceRadius = '2xl' | '3xl' | 'lg' | 'md' | 'none' | 'sm' | 'xl'\nexport type LumenSurfaceTone = 'canvas' | 'muted' | 'strong' | 'surface'\n\nexport interface LumenSurfaceProps extends ViewProps {\n padding?: LumenSurfacePadding\n radius?: LumenSurfaceRadius\n ref?: LumenViewRef\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' | 'success'\nexport type LumenControlSize = 'lg' | 'md' | 'sm'\nexport type LumenIconSize = 'lg' | 'md' | 'sm'\n\nexport interface LumenButtonProps extends Omit<PressableProps, 'children'> {\n children: ReactNode\n intent?: LumenButtonIntent\n loading?: boolean\n ref?: LumenViewRef\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 === true || loading\n\n return (\n <Pressable\n ref={ref}\n {...props}\n accessibilityRole=\"button\"\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 <LumenControlContentContext value={colors.color}>{children}</LumenControlContentContext>\n </Text>\n </Pressable>\n )\n}\n\nexport interface LumenTextFieldProps extends TextInputProps {\n 'aria-describedby'?: string\n 'aria-required'?: boolean\n error?: boolean\n ref?: LumenTextInputRef\n size?: LumenControlSize\n}\n\nconst hasExplicitLumenTextFieldLabel = (\n props: Pick<LumenTextFieldProps, 'accessibilityLabel' | 'accessibilityLabelledBy' | 'aria-label' | 'aria-labelledby'>\n): boolean => [\n props.accessibilityLabel,\n props.accessibilityLabelledBy,\n props['aria-label'],\n props['aria-labelledby']\n].some(value => value !== undefined)\n\nconst resolveLumenTextFieldDescribedBy = (\n ariaDescribedBy: string | undefined,\n field: LumenFieldContextValue | null\n): string | undefined => {\n const inheritedDescription = [field?.descriptionId, field?.errorId]\n .filter((value): value is string => Boolean(value))\n .join(' ')\n\n return ariaDescribedBy ?? (inheritedDescription || undefined)\n}\n\nconst resolveLumenTextFieldFontSize = (\n size: LumenControlSize,\n mediumFontSize: number,\n smallFontSize: number\n): number => size === 'lg' ? mediumFontSize : smallFontSize\n\nconst resolveLumenTextFieldBorderColor = (\n isInvalid: boolean,\n dangerColor: ColorValue,\n lineColor: ColorValue\n): ColorValue => isInvalid ? dangerColor : lineColor\n\nconst resolveLumenTextFieldOpacity = (editable: boolean): number => editable ? 1 : 0.52\n\ninterface LumenTextFieldAccessibility {\n label: string | undefined\n labelledBy: string | undefined\n required: boolean | undefined\n}\n\nconst resolveLumenTextFieldAccessibility = (\n props: LumenTextFieldProps,\n field: LumenFieldContextValue | null,\n ariaRequired: boolean | undefined\n): LumenTextFieldAccessibility => {\n const hasExplicitLabel = hasExplicitLumenTextFieldLabel(props)\n\n return {\n label: hasExplicitLabel ? props.accessibilityLabel : field?.label,\n labelledBy: hasExplicitLabel ? props['aria-labelledby'] : field?.labelId,\n required: ariaRequired ?? field?.required\n }\n}\n\nexport const LumenTextField = ({\n 'aria-describedby': ariaDescribedBy,\n 'aria-required': ariaRequired,\n accessibilityState,\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 field = useLumenFieldContext()\n const minHeight = resolveLumenControlMinHeight(size)\n const fontSize = resolveLumenTextFieldFontSize(size, theme.fontSizes.md, theme.fontSizes.sm)\n const accessibility = resolveLumenTextFieldAccessibility(props, field, ariaRequired)\n const describedBy = resolveLumenTextFieldDescribedBy(ariaDescribedBy, field)\n const isInvalid = error || (field?.error ?? false)\n\n return (\n <TextInput\n ref={ref}\n {...props}\n accessibilityLabel={accessibility.label}\n accessibilityState={{\n ...accessibilityState,\n disabled: !editable\n }}\n aria-describedby={describedBy}\n aria-invalid={isInvalid || undefined}\n aria-labelledby={accessibility.labelledBy}\n aria-required={accessibility.required}\n editable={editable}\n placeholderTextColor={placeholderTextColor ?? theme.colors.inkMuted}\n style={[\n {\n backgroundColor: theme.colors.surface,\n borderColor: resolveLumenTextFieldBorderColor(\n isInvalid,\n theme.colors.danger,\n theme.colors.line\n ),\n borderRadius: theme.radii.sm,\n borderWidth: 1,\n color: theme.colors.ink,\n fontSize,\n minHeight,\n opacity: resolveLumenTextFieldOpacity(editable),\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?: LumenViewRef\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?: LumenViewRef\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?: LumenActivityIndicatorRef\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",
60210
60429
  "packages/react-native/src/foundations.ts": "export {\n LumenBadge,\n type LumenBadgeProps,\n LumenButton,\n type LumenButtonIntent,\n type LumenButtonProps,\n type LumenControlSize,\n LumenDivider,\n type LumenDividerProps,\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 } from './foundation-primitives.js'\nexport { LumenProvider, type LumenProviderProps } from './provider.js'\nexport { createLumenTheme, lumenDarkTheme, lumenLightTheme, type LumenTheme } from './theme.js'\nexport { type LumenAppearance, type LumenSurfaceMaterial, type LumenThemeOptions, type LumenThemePreset } from './theme.js'\nexport { useLumenTheme } from './theme-context.js'\n",
60211
60430
  "packages/react-native/src/graphics.ts": "export {\n LumenStaticIcon as LumenIcon,\n LumenStaticIconButton as LumenIconButton,\n type LumenStaticIconButtonProps as LumenIconButtonProps,\n type LumenIconGraphic,\n type LumenIconGraphicProps,\n type LumenStaticIconProps as LumenIconProps\n} from './icon-primitives.js'\nexport { LumenProvider, type LumenProviderProps } from './provider.js'\nexport { useLumenTheme } from './theme-context.js'\n",
60212
60431
  "packages/react-native/src/hook-recipes.test.ts": "import { describe, expect, test } from 'vitest'\n\nimport {\n createLumenHookToast,\n getNextLumenThemeScheme,\n normalizeLumenHookSelectionOptions,\n updateLumenHookToast\n} from './hook-recipes.js'\n\ndescribe('React Native hook recipes', () => {\n test('normalizes primitive and structured native selection options', () => {\n expect(normalizeLumenHookSelectionOptions([\n 'overview',\n { disabled: true, label: 'Activity', value: 'activity' }\n ])).toEqual([\n { label: 'overview', value: 'overview' },\n { disabled: true, label: 'Activity', value: 'activity' }\n ])\n })\n\n test('toggles between the supported explicit native schemes', () => {\n expect(getNextLumenThemeScheme('light')).toBe('dark')\n expect(getNextLumenThemeScheme('dark')).toBe('light')\n })\n\n test('creates and updates stable toast records', () => {\n const toast = createLumenHookToast('toast-1', { title: 'Saved' })\n\n expect(toast).toEqual({ id: 'toast-1', title: 'Saved', variant: 'default' })\n expect(updateLumenHookToast(toast, {\n description: 'Available offline',\n variant: 'success'\n })).toEqual({\n description: 'Available offline',\n id: 'toast-1',\n title: 'Saved',\n variant: 'success'\n })\n })\n})\n",
@@ -60263,7 +60482,7 @@
60263
60482
  "packages/react-native/src/schedule-components.tsx": "import { type ReactElement, useState } from 'react'\nimport { Pressable, ScrollView, View, type ViewProps } from 'react-native'\n\nimport { formatLumenAgendaMinute, type LumenAgendaEvent, type LumenAgendaSegment } from './agenda-recipes.js'\nimport { addLumenCalendarDays, type LumenCalendarDay, lumenCalendarDayKey } from './calendar-recipes.js'\nimport { LumenText } from './primitives.js'\nimport { type LumenScheduleDay, lumenScheduleLayout } from './schedule-recipes.js'\nimport { useLumenTheme } from './theme-context.js'\n\nexport interface LumenScheduleProps extends Pick<ViewProps, 'style' | 'testID'> {\n label: string\n selectedDay: LumenCalendarDay\n onSelectedDayChange: (day: LumenCalendarDay) => void\n events: readonly LumenAgendaEvent[]\n dayCount?: number\n startHour?: number\n endHour?: number\n onEventPress?: (event: LumenAgendaEvent, day: LumenCalendarDay) => void\n onEventMove?: (event: LumenAgendaEvent, targetDay: LumenCalendarDay) => void\n disabled?: boolean\n readOnly?: boolean\n loading?: boolean\n error?: string | null\n loadingLabel?: string\n emptyLabel?: string\n invalidLabel?: string\n previousLabel?: string\n nextLabel?: string\n previousDayLabel?: string\n nextDayLabel?: string\n allDayLabel?: string\n formatDay?: (day: LumenCalendarDay) => string\n formatTime?: (minute: number) => string\n}\n\nconst scheduleStatus = (input: LumenScheduleProps, invalid: boolean): string | null => {\n if (input.error != null) return input.error\n\n if (input.loading) return input.loadingLabel ?? 'Loading'\n\n if (invalid) return input.invalidLabel ?? 'Invalid schedule'\n\n return null\n}\n\ninterface EventButtonProps {\n segment: LumenAgendaSegment\n day: LumenCalendarDay\n name: string\n disabled: boolean\n onPress: (event: LumenAgendaEvent, day: LumenCalendarDay) => void\n}\n\nconst ScheduleEvent = ({ segment, day, name, disabled, onPress }: EventButtonProps): ReactElement => {\n const theme = useLumenTheme()\n\n return (\n <Pressable\n accessibilityRole=\"button\"\n accessibilityLabel={name}\n disabled={disabled}\n accessibilityState={{ disabled }}\n onPress={() => {\n if (!disabled) onPress(segment.event, day)\n }}\n style={{\n minWidth: 44,\n minHeight: 44,\n flex: 1,\n padding: theme.spacing.xs,\n backgroundColor: theme.colors.brandSoft,\n overflow: 'hidden'\n }}\n >\n <LumenText numberOfLines={2}>{segment.event.event.label}</LumenText>\n </Pressable>\n )\n}\n\nconst columnWidth = (day: LumenScheduleDay): number => {\n const widths = day.placements.map(item => item.laneCount * 100)\n\n return widths.reduce((width, value) => Math.max(width, value), 200)\n}\n\nconst rangeDestination = (day: LumenCalendarDay, amount: number, count: number): LumenCalendarDay | null => {\n const next = addLumenCalendarDays(day, amount)\n\n return next && addLumenCalendarDays(next, count - 1) ? next : null\n}\n\ninterface FocusedEvent { id: string, day: LumenCalendarDay }\n\nconst focusedEvent = (\n days: readonly LumenScheduleDay[], focused: FocusedEvent | null\n): LumenAgendaEvent | undefined => {\n if (!focused) return undefined\n\n const day = days.find(group => lumenCalendarDayKey(group.day) === lumenCalendarDayKey(focused.day))\n\n if (!day) return undefined\n\n return [...day.allDay, ...day.placements.map(item => item.segment)]\n .find(segment => segment.event.event.id === focused.id)?.event\n}\n\nexport const LumenSchedule = (input: LumenScheduleProps): ReactElement => {\n const props = {\n dayCount: 7,\n startHour: 8,\n endHour: 18,\n formatDay: lumenCalendarDayKey,\n formatTime: formatLumenAgendaMinute,\n emptyLabel: 'No events in this time range',\n allDayLabel: 'All day',\n previousLabel: 'Previous range',\n nextLabel: 'Next range',\n previousDayLabel: 'Move to previous day',\n nextDayLabel: 'Move to next day',\n ...input\n }\n\n const theme = useLumenTheme()\n const [focused, setFocused] = useState<FocusedEvent | null>(null)\n const days = lumenScheduleLayout(props.events, props.selectedDay, props.dayCount, props.startHour, props.endHour)\n const status = scheduleStatus(props, days.length === 0)\n const locked = props.disabled === true || props.readOnly === true || status !== null\n const move = props.onEventMove\n const noActions = !props.onEventPress && !move\n const active = focusedEvent(days, focused)\n\n const focus = (event: LumenAgendaEvent, day: LumenCalendarDay): void => {\n if (locked || event.event.disabled) return\n\n setFocused({ id: event.event.id, day })\n\n props.onEventPress?.(event, day)\n }\n\n const name = (segment: LumenAgendaSegment, day: LumenCalendarDay): string => {\n let time = props.allDayLabel\n\n if (segment.startMinute !== null) {\n time = `${props.formatTime(segment.startMinute)} – ${props.formatTime(segment.endMinute ?? 1440)}`\n }\n\n return [props.formatDay(day), time, segment.event.event.label, segment.event.event.detail].filter(Boolean).join(', ')\n }\n\n const allDayHeight = days.reduce((count, day) => Math.max(count, day.allDay.length), 1) * 48\n\n const hours = status === null ?\n Array.from({ length: props.endHour - props.startHour + 1 }, (_, index) => props.startHour + index) :\n []\n\n const navigation = [\n { id: 'previous', label: props.previousLabel, day: rangeDestination(props.selectedDay, -props.dayCount, props.dayCount) },\n { id: 'next', label: props.nextLabel, day: rangeDestination(props.selectedDay, props.dayCount, props.dayCount) }\n ]\n\n return (\n <View style={props.style} testID={props.testID} accessibilityLabel={props.label}>\n <LumenText accessibilityRole=\"header\">{props.label}</LumenText>\n {status !== null ?\n <LumenText accessibilityLiveRegion=\"polite\">{status}</LumenText> :\n (\n <>\n <View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>\n {navigation.map(action => (\n <Pressable\n key={action.id}\n accessibilityRole=\"button\"\n accessibilityLabel={action.label}\n disabled={locked || !action.day}\n accessibilityState={{ disabled: locked || !action.day }}\n style={{ minWidth: 44, minHeight: 44, flexShrink: 1, justifyContent: 'center' }}\n onPress={() => {\n if (!locked && action.day) props.onSelectedDayChange(action.day)\n }}\n >\n <LumenText>{action.label}</LumenText>\n </Pressable>\n ))}\n </View>\n {days.every(day => day.placements.length === 0 && day.allDay.length === 0) && (\n <LumenText>{props.emptyLabel}</LumenText>\n )}\n <ScrollView horizontal>\n <ScrollView style={{ maxHeight: 480 }} nestedScrollEnabled>\n <View style={{ flexDirection: 'row' }}>\n <View style={{ width: 64 }}>\n <View style={{ height: 44 + allDayHeight }}><LumenText>{props.allDayLabel}</LumenText></View>\n <View style={{ height: (props.endHour - props.startHour) * 60 }}>\n {hours.map(hour => <View key={hour} style={{ position: 'absolute', top: Math.min((hour - props.startHour) * 60, (props.endHour - props.startHour) * 60 - 20) }}><LumenText>{props.formatTime(hour * 60)}</LumenText></View>)}\n </View>\n </View>\n {days.map(day => {\n const width = columnWidth(day)\n\n return (\n <View\n key={lumenCalendarDayKey(day.day)}\n style={{ width, borderLeftWidth: 1, borderColor: theme.colors.line }}\n >\n <View style={{ height: 44, justifyContent: 'center', padding: theme.spacing.xs }}><LumenText accessibilityRole=\"header\">{props.formatDay(day.day)}</LumenText></View>\n <View style={{ height: allDayHeight }}>\n {day.allDay.map(segment => (\n <View key={segment.event.event.id} style={{ height: 48 }}>\n <ScheduleEvent\n segment={segment}\n day={day.day}\n name={name(segment, day.day)}\n disabled={locked || segment.event.event.disabled === true || noActions}\n onPress={focus}\n />\n </View>\n ))}\n </View>\n <View style={{ height: (props.endHour - props.startHour) * 60 }}>\n {hours.slice(0, -1).map(hour => <View key={hour} style={{ position: 'absolute', top: (hour - props.startHour) * 60, left: 0, right: 0, borderTopWidth: 1, borderColor: theme.colors.line }} />)}\n {day.placements.map(item => <View key={item.segment.event.event.id} style={{ position: 'absolute', top: item.top, height: item.height, left: item.lane * width / item.laneCount, width: width / item.laneCount, paddingHorizontal: 2 }}><ScheduleEvent segment={item.segment} day={day.day} name={name(item.segment, day.day)} disabled={locked || item.segment.event.event.disabled === true || noActions} onPress={focus} /></View>)}\n </View>\n </View>\n )\n })}\n </View>\n </ScrollView>\n </ScrollView>\n {move && active && focused && (\n <View>\n <LumenText>{active.event.label}</LumenText>\n {[-1, 1].map(direction => {\n const target = addLumenCalendarDays(focused.day, direction)\n const unavailable = locked || active.event.disabled === true || !target\n\n return (\n <Pressable\n key={direction}\n accessibilityRole=\"button\"\n accessibilityLabel={direction < 0 ? props.previousDayLabel : props.nextDayLabel}\n disabled={unavailable}\n accessibilityState={{ disabled: unavailable }}\n style={{ minHeight: 44, minWidth: 44 }}\n onPress={() => {\n if (!unavailable) move(active, target)\n }}\n >\n <LumenText>{direction < 0 ? props.previousDayLabel : props.nextDayLabel}</LumenText>\n </Pressable>\n )\n })}\n </View>\n )}\n </>\n )}\n </View>\n )\n}\n",
60264
60483
  "packages/react-native/src/schedule-recipes.test.ts": "import { expect, test } from 'vitest'\n\nimport type { LumenAgendaEvent } from './agenda-recipes.js'\nimport { lumenScheduleLayout } from './schedule-recipes.js'\nconst day = { year: 2026, month: 3, day: 8 }\nconst timed = (id: string, startMinute: number, endMinute: number): LumenAgendaEvent => ({\n event: { id, label: id, startDay: day }, startMinute, endMinute\n})\ntest('overlapping chains share lane count; separated intervals reclaim full width', () => {\n const layout = lumenScheduleLayout([timed('a', 540, 660), timed('b', 570, 630), timed('c', 630, 690), timed('d', 720, 780)], day, 1)\n expect(layout[0]?.placements.map(item => [item.segment.event.event.id, item.lane, item.laneCount])).toEqual([\n ['a', 0, 2], ['b', 1, 2], ['c', 1, 2], ['d', 0, 1]\n ])\n})\ntest('short events keep accessible height without hit collisions, clipping to hour window', () => {\n const layout = lumenScheduleLayout([timed('a', 480, 485), timed('b', 486, 490), timed('last', 1079, 1080), timed('off', 1080, 1100)], day, 1)\n const items = layout[0]?.placements ?? []\n expect(items).toHaveLength(3)\n expect(items[0]?.height).toBe(48)\n expect(items[1]?.laneCount).toBe(2)\n expect(items[2]?.top).toBe(552)\n for (const item of items) expect(item.top + item.height).toBeLessThanOrEqual(600)\n})\ntest('all-day band and overnight clipping reuse Agenda; invalid IDs/window/range fail closed', () => {\n const overnight = { ...timed('night', 1380, 60), event: { id: 'night', label: 'Night', startDay: day, endDay: { ...day, day: 9 } } }\n const allDay = { event: { id: 'holiday', label: 'Holiday', startDay: day } }\n const layout = lumenScheduleLayout([overnight, allDay], day, 2, 0, 24)\n expect(layout[0]?.allDay).toHaveLength(1)\n expect(layout[1]?.placements[0]?.segment.startMinute).toBe(0)\n expect(lumenScheduleLayout([allDay, allDay], day)).toEqual([])\n for (const [count, start, end] of [[0, 8, 18], [8, 8, 18], [1, -1, 18], [1, 18, 8], [1, 8, 25], [1, 8.5, 18]]) {\n expect(lumenScheduleLayout([], day, count, start, end)).toEqual([])\n }\n expect(lumenScheduleLayout([], { year: 9999, month: 12, day: 31 }, 2)).toEqual([])\n})\n",
60265
60484
  "packages/react-native/src/schedule-recipes.ts": "import { type LumenAgendaEvent, lumenAgendaGroups, type LumenAgendaSegment } from './agenda-recipes.js'\nimport type { LumenCalendarDay } from './calendar-recipes.js'\n\nexport interface LumenSchedulePlacement {\n segment: LumenAgendaSegment\n top: number\n height: number\n lane: number\n laneCount: number\n}\nexport interface LumenScheduleDay {\n day: LumenCalendarDay\n allDay: readonly LumenAgendaSegment[]\n placements: readonly LumenSchedulePlacement[]\n}\n\nconst assignLanes = (placements: LumenSchedulePlacement[]): LumenSchedulePlacement[] => {\n let group: LumenSchedulePlacement[] = []\n let laneEnds: number[] = []\n let groupEnd = -1\n\n const finish = (): void => {\n for (const item of group) item.laneCount = laneEnds.length\n }\n\n for (const item of placements) {\n if (item.top >= groupEnd) {\n finish()\n\n group = []\n\n laneEnds = []\n }\n\n let lane = laneEnds.findIndex(value => value <= item.top)\n\n if (lane < 0) lane = laneEnds.length\n\n laneEnds[lane] = item.top + item.height\n\n item.lane = lane\n\n groupEnd = Math.max(groupEnd, item.top + item.height)\n\n group.push(item)\n }\n\n finish()\n\n return placements\n}\n\nconst placeSegments = (\n segments: readonly LumenAgendaSegment[], start: number, end: number\n): LumenSchedulePlacement[] => {\n const placements: LumenSchedulePlacement[] = []\n\n for (const segment of segments) {\n if (segment.startMinute === null || segment.endMinute === null) continue\n\n if (segment.startMinute >= end || segment.endMinute <= start) continue\n\n const top = Math.min(Math.max(segment.startMinute, start), end - 48) - start\n const height = Math.max(48, Math.min(segment.endMinute, end) - start - top)\n\n placements.push({ segment, top, height, lane: 0, laneCount: 1 })\n }\n\n placements.sort((a, b) => a.top - b.top)\n\n return assignLanes(placements)\n}\n\nconst validHours = (start: number, end: number): boolean => {\n const integers = Number.isInteger(start) && Number.isInteger(end)\n\n return integers && start >= 0 && end <= 24 && start < end\n}\n\nexport const lumenScheduleLayout = (\n events: readonly LumenAgendaEvent[], selectedDay: LumenCalendarDay, dayCount = 7, startHour = 8, endHour = 18\n): readonly LumenScheduleDay[] => {\n if (!Number.isInteger(dayCount) || dayCount < 1 || dayCount > 7) return []\n\n if (!validHours(startHour, endHour)) return []\n\n return lumenAgendaGroups(events, selectedDay, dayCount).map(group => ({\n day: group.day,\n allDay: group.segments.filter(segment => segment.startMinute === null),\n placements: placeSegments(group.segments, startHour * 60, endHour * 60)\n }))\n}\n",
60266
- "packages/react-native/src/selection-components.tsx": "import {\n type ComponentRef,\n type ReactElement,\n type ReactNode,\n useEffect,\n useId,\n useRef\n} from 'react'\nimport {\n type HostInstance,\n Platform,\n Pressable,\n type PressableProps,\n ScrollView,\n Text,\n type TextStyle,\n View,\n type ViewProps\n} from 'react-native'\n\nimport type { LumenViewRef } from './native-ref-types.js'\nimport { resolveLumenButtonOpacity } from './recipes.js'\nimport { resolveLumenSelectionState } from './selection-recipes.js'\nimport { useLumenTheme } from './theme-context.js'\n\nexport interface LumenSelectionOption {\n description?: string\n disabled?: boolean\n label: string\n value: string\n}\n\nexport interface LumenCheckboxProps extends Omit<PressableProps, 'children' | 'disabled' | 'onPress'> {\n checked: boolean\n description?: string\n disabled?: boolean\n label: string\n onCheckedChange: (checked: boolean) => void\n ref?: LumenViewRef\n}\n\nexport const LumenCheckbox = ({\n checked,\n description,\n disabled = false,\n label,\n onCheckedChange,\n onKeyDown,\n ref,\n style,\n ...props\n}: LumenCheckboxProps): ReactElement => {\n const theme = useLumenTheme()\n const state = resolveLumenSelectionState(checked, disabled)\n\n return (\n <Pressable\n ref={ref}\n {...props}\n accessibilityRole=\"checkbox\"\n accessibilityLabel={props.accessibilityLabel ?? label}\n aria-checked={checked}\n aria-disabled={disabled}\n accessibilityState={{ checked, disabled }}\n disabled={disabled}\n onKeyDown={event => {\n onKeyDown?.(event)\n\n const key = event.nativeEvent.key || event.nativeEvent.code\n\n if (Platform.OS !== 'web' || event.defaultPrevented || (key !== ' ' && key !== 'Spacebar')) return\n\n event.preventDefault()\n\n const repeated = 'repeat' in event.nativeEvent && event.nativeEvent.repeat === true\n\n if (!disabled && !repeated) onCheckedChange(!checked)\n }}\n onPress={() => {\n if (!disabled) onCheckedChange(!checked)\n }}\n style={pressState => [\n {\n alignItems: 'flex-start',\n flexDirection: 'row',\n gap: theme.spacing.md,\n minHeight: 44,\n opacity: state.opacity,\n paddingVertical: theme.spacing.sm\n },\n typeof style === 'function' ? style(pressState) : style\n ]}\n >\n {({ pressed }) => (\n <>\n <View\n accessibilityElementsHidden\n importantForAccessibility=\"no-hide-descendants\"\n style={{\n alignItems: 'center',\n backgroundColor: checked ? theme.colors.brandSolid : theme.colors.surface,\n borderColor: checked ? theme.colors.brandSolid : theme.colors.line,\n borderRadius: theme.radii.sm,\n borderWidth: 1,\n height: 22,\n justifyContent: 'center',\n marginTop: 1,\n opacity: resolveLumenButtonOpacity(disabled, pressed),\n width: 22\n }}\n >\n {checked ?\n <Text style={{ color: theme.colors.onBrand, fontSize: 14, fontWeight: '700' }}>✓</Text> :\n null}\n </View>\n <View style={{ flex: 1, gap: theme.spacing.xs }}>\n <Text\n style={{\n color: theme.colors.ink,\n fontSize: theme.fontSizes.sm,\n fontWeight: String(theme.fontWeights.semibold) as TextStyle['fontWeight']\n }}\n >\n {label}\n </Text>\n {description ?\n <Text style={{ color: theme.colors.inkMuted, fontSize: theme.fontSizes.xs }}>{description}</Text> :\n null}\n </View>\n </>\n )}\n </Pressable>\n )\n}\n\nexport interface LumenRadioGroupProps extends Omit<ViewProps, 'children'> {\n label: string\n onValueChange: (value: string) => void\n options: readonly LumenSelectionOption[]\n ref?: LumenViewRef\n value: string\n}\n\nexport const LumenRadioGroup = ({\n label,\n onValueChange,\n options,\n ref,\n style,\n value,\n ...props\n}: LumenRadioGroupProps): ReactElement => {\n const theme = useLumenTheme()\n\n return (\n <View\n ref={ref}\n {...props}\n accessibilityLabel={props.accessibilityLabel ?? label}\n accessibilityRole=\"radiogroup\"\n style={[{ gap: theme.spacing.sm }, style]}\n >\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 {options.map(option => {\n const selected = value === option.value\n const state = resolveLumenSelectionState(selected, option.disabled ?? false)\n\n return (\n <Pressable\n aria-checked={selected}\n aria-disabled={state.disabled}\n key={option.value}\n accessibilityRole=\"radio\"\n accessibilityState={{ checked: selected, disabled: state.disabled }}\n disabled={state.disabled}\n onPress={() => {\n onValueChange(option.value)\n }}\n style={({ pressed }) => ({\n alignItems: 'flex-start',\n flexDirection: 'row',\n gap: theme.spacing.md,\n minHeight: 44,\n opacity: state.opacity * resolveLumenButtonOpacity(state.disabled, pressed),\n paddingVertical: theme.spacing.sm\n })}\n >\n <View\n accessibilityElementsHidden\n importantForAccessibility=\"no-hide-descendants\"\n style={{\n alignItems: 'center',\n borderColor: selected ? theme.colors.brandSolid : theme.colors.line,\n borderRadius: 11,\n borderWidth: 2,\n height: 22,\n justifyContent: 'center',\n marginTop: 1,\n width: 22\n }}\n >\n {selected ?\n <View style={{ backgroundColor: theme.colors.brandSolid, borderRadius: 5, height: 10, width: 10 }} /> :\n null}\n </View>\n <View style={{ flex: 1, gap: theme.spacing.xs }}>\n <Text style={{ color: theme.colors.ink, fontSize: theme.fontSizes.sm }}>{option.label}</Text>\n {option.description ?\n (\n <Text style={{ color: theme.colors.inkMuted, fontSize: theme.fontSizes.xs }}>\n {option.description}\n </Text>\n ) :\n null}\n </View>\n </Pressable>\n )\n })}\n </View>\n )\n}\n\nexport interface LumenSegmentedControlProps extends Omit<ViewProps, 'children'> {\n label: string\n onValueChange: (value: string) => void\n options: readonly LumenSelectionOption[]\n ref?: LumenViewRef\n showLabel?: boolean\n value: string\n}\n\nexport const LumenSegmentedControl = ({\n label,\n onValueChange,\n options,\n ref,\n showLabel = true,\n style,\n value,\n ...props\n}: LumenSegmentedControlProps): ReactElement => {\n const theme = useLumenTheme()\n\n return (\n <View ref={ref} {...props} style={[{ gap: theme.spacing.sm }, style]}>\n {showLabel ?\n (\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 ) :\n null}\n <View\n accessibilityLabel={label}\n accessibilityRole=\"radiogroup\"\n style={{\n backgroundColor: theme.colors.surfaceMuted,\n borderColor: theme.colors.line,\n borderRadius: theme.radii.sm,\n borderWidth: 1,\n flexDirection: 'row',\n padding: 2\n }}\n >\n {options.map(option => {\n const selected = value === option.value\n const state = resolveLumenSelectionState(selected, option.disabled ?? false)\n\n return (\n <Pressable\n aria-checked={selected}\n aria-disabled={state.disabled}\n key={option.value}\n accessibilityLabel={option.label}\n accessibilityRole=\"radio\"\n accessibilityState={{ checked: selected, disabled: state.disabled }}\n disabled={state.disabled}\n onPress={() => {\n onValueChange(option.value)\n }}\n style={({ pressed }) => ({\n alignItems: 'center',\n backgroundColor: selected ? theme.colors.surface : 'transparent',\n borderRadius: theme.radii.sm,\n flex: 1,\n justifyContent: 'center',\n minHeight: 40,\n opacity: state.opacity * resolveLumenButtonOpacity(state.disabled, pressed),\n paddingHorizontal: theme.spacing.sm,\n paddingVertical: theme.spacing.sm\n })}\n >\n <Text\n numberOfLines={1}\n style={{\n color: selected ? theme.colors.brand : theme.colors.inkSoft,\n fontSize: theme.fontSizes.xs,\n fontWeight: String(\n selected ? theme.fontWeights.bold : theme.fontWeights.semibold\n ) as TextStyle['fontWeight']\n }}\n >\n {option.label}\n </Text>\n </Pressable>\n )\n })}\n </View>\n </View>\n )\n}\n\nexport interface LumenTabsProps extends Omit<ViewProps, 'children'> {\n children: ReactNode\n label: string\n onValueChange: (value: string) => void\n options: readonly LumenSelectionOption[]\n /** Localized panel name; defaults to the selected tab's visible label. */\n panelAccessibilityLabel?: string\n ref?: LumenViewRef\n value: string\n}\n\nexport const LumenTabs = ({\n children,\n label,\n onValueChange,\n options,\n panelAccessibilityLabel,\n ref,\n style,\n value,\n ...props\n}: LumenTabsProps): ReactElement => {\n const theme = useLumenTheme()\n const selectedOption = options.find(option => option.value === value)\n const selectedIndex = options.findIndex(option => option.value === value)\n const tabsId = useId().replaceAll(':', '')\n const pendingFocusValueRef = useRef<string | undefined>(undefined)\n const tabInstancesRef = useRef<(HostInstance | null)[]>([])\n const tabLayoutsRef = useRef(new Map<number, { width: number, x: number }>())\n const tabListRef = useRef<ComponentRef<typeof ScrollView>>(null)\n const viewportWidthRef = useRef(0)\n\n const scrollTabIntoView = (index: number, animated: boolean): void => {\n const layout = tabLayoutsRef.current.get(index)\n\n if (!layout) return\n\n const centeredOffset = layout.x - Math.max(0, (viewportWidthRef.current - layout.width) / 2)\n\n tabListRef.current?.scrollTo({ animated, x: Math.max(0, centeredOffset) })\n }\n\n useEffect(() => {\n if (selectedIndex < 0) return\n\n scrollTabIntoView(selectedIndex, true)\n\n if (pendingFocusValueRef.current === value) {\n tabInstancesRef.current[selectedIndex]?.focus()\n\n pendingFocusValueRef.current = undefined\n }\n }, [selectedIndex, value])\n\n const selectTab = (optionIndex: number): boolean => {\n const option = options[optionIndex]\n\n if (!option || option.disabled) return false\n\n if (option.value === value) {\n tabInstancesRef.current[optionIndex]?.focus()\n\n return true\n }\n\n pendingFocusValueRef.current = option.value\n\n onValueChange(option.value)\n\n return true\n }\n\n const moveSelection = (currentIndex: number, direction: -1 | 1): void => {\n for (let offset = 1; offset < options.length; offset += 1) {\n const nextIndex = (currentIndex + direction * offset + options.length) % options.length\n\n if (selectTab(nextIndex)) return\n }\n }\n\n return (\n <View ref={ref} {...props} style={[{ gap: theme.spacing.md }, style]}>\n <ScrollView\n accessibilityLabel={label}\n accessibilityRole=\"tablist\"\n contentContainerStyle={{ gap: theme.spacing.xs }}\n horizontal\n onLayout={event => {\n viewportWidthRef.current = event.nativeEvent.layout.width\n\n if (selectedIndex >= 0) scrollTabIntoView(selectedIndex, false)\n }}\n ref={tabListRef}\n showsHorizontalScrollIndicator={false}\n style={{ borderBottomColor: theme.colors.line, borderBottomWidth: 1 }}\n >\n {options.map((option, optionIndex) => {\n const selected = option.value === value\n const state = resolveLumenSelectionState(selected, option.disabled ?? false)\n const tabId = `${tabsId}-tab-${optionIndex}`\n\n return (\n <Pressable\n aria-disabled={state.disabled}\n aria-selected={selected}\n accessibilityRole=\"tab\"\n accessibilityState={{ disabled: state.disabled, selected }}\n disabled={state.disabled}\n key={option.value}\n nativeID={tabId}\n onKeyDown={event => {\n const key = event.nativeEvent.key || event.nativeEvent.code\n\n if (key === 'ArrowLeft') {\n event.preventDefault()\n\n moveSelection(optionIndex, -1)\n } else if (key === 'ArrowRight') {\n event.preventDefault()\n\n moveSelection(optionIndex, 1)\n }\n }}\n onLayout={event => {\n tabLayoutsRef.current.set(optionIndex, event.nativeEvent.layout)\n\n if (selected) scrollTabIntoView(optionIndex, false)\n }}\n onPress={() => {\n onValueChange(option.value)\n }}\n ref={instance => {\n tabInstancesRef.current[optionIndex] = instance\n }}\n style={({ pressed }) => ({\n borderBottomColor: selected ? theme.colors.brandSolid : 'transparent',\n borderBottomWidth: 2,\n minHeight: 44,\n opacity: state.opacity * resolveLumenButtonOpacity(state.disabled, pressed),\n paddingHorizontal: theme.spacing.md,\n paddingVertical: theme.spacing.sm\n })}\n >\n <Text\n style={{\n color: selected ? theme.colors.brand : theme.colors.inkSoft,\n fontSize: theme.fontSizes.sm,\n fontWeight: String(\n selected ? theme.fontWeights.bold : theme.fontWeights.semibold\n ) as TextStyle['fontWeight']\n }}\n >\n {option.label}\n </Text>\n </Pressable>\n )\n })}\n </ScrollView>\n <View\n aria-labelledby={selectedIndex >= 0 ? `${tabsId}-tab-${selectedIndex}` : undefined}\n accessibilityLabel={panelAccessibilityLabel ?? selectedOption?.label ?? value}\n accessibilityLiveRegion=\"polite\"\n role=\"tabpanel\"\n >\n {children}\n </View>\n </View>\n )\n}\n",
60485
+ "packages/react-native/src/selection-components.tsx": "import {\n type ComponentRef,\n type ReactElement,\n type ReactNode,\n useEffect,\n useId,\n useRef\n} from 'react'\nimport {\n type HostInstance,\n Platform,\n Pressable,\n type PressableProps,\n ScrollView,\n Text,\n type TextStyle,\n View,\n type ViewProps\n} from 'react-native'\n\nimport type { LumenViewRef } from './native-ref-types.js'\nimport { resolveLumenButtonOpacity } from './recipes.js'\nimport { resolveLumenSelectionState } from './selection-recipes.js'\nimport { useLumenTheme } from './theme-context.js'\n\nexport interface LumenSelectionOption {\n description?: string\n disabled?: boolean\n label: string\n value: string\n}\n\nexport interface LumenCheckboxProps extends Omit<PressableProps, 'children' | 'disabled' | 'onPress'> {\n checked: boolean\n description?: string\n disabled?: boolean\n label: string\n onCheckedChange: (checked: boolean) => void\n ref?: LumenViewRef\n}\n\nexport const LumenCheckbox = ({\n checked,\n description,\n disabled = false,\n label,\n onCheckedChange,\n onKeyDown,\n ref,\n style,\n ...props\n}: LumenCheckboxProps): ReactElement => {\n const theme = useLumenTheme()\n const state = resolveLumenSelectionState(checked, disabled)\n\n return (\n <Pressable\n ref={ref}\n {...props}\n accessibilityRole=\"checkbox\"\n // An explicit native label hides the visible description, so it is announced as the hint, like LumenToggle.\n accessibilityHint={props.accessibilityHint ?? description}\n accessibilityLabel={props.accessibilityLabel ?? label}\n aria-checked={checked}\n aria-disabled={disabled}\n accessibilityState={{ checked, disabled }}\n disabled={disabled}\n onKeyDown={event => {\n onKeyDown?.(event)\n\n const key = event.nativeEvent.key || event.nativeEvent.code\n\n if (Platform.OS !== 'web' || event.defaultPrevented || (key !== ' ' && key !== 'Spacebar')) return\n\n event.preventDefault()\n\n const repeated = 'repeat' in event.nativeEvent && event.nativeEvent.repeat === true\n\n if (!disabled && !repeated) onCheckedChange(!checked)\n }}\n onPress={() => {\n if (!disabled) onCheckedChange(!checked)\n }}\n style={pressState => [\n {\n alignItems: 'flex-start',\n flexDirection: 'row',\n gap: theme.spacing.md,\n minHeight: 44,\n opacity: state.opacity,\n paddingVertical: theme.spacing.sm\n },\n typeof style === 'function' ? style(pressState) : style\n ]}\n >\n {({ pressed }) => (\n <>\n <View\n accessibilityElementsHidden\n importantForAccessibility=\"no-hide-descendants\"\n style={{\n alignItems: 'center',\n backgroundColor: checked ? theme.colors.brandSolid : theme.colors.surface,\n borderColor: checked ? theme.colors.brandSolid : theme.colors.line,\n borderRadius: theme.radii.sm,\n borderWidth: 1,\n height: 22,\n justifyContent: 'center',\n marginTop: 1,\n opacity: resolveLumenButtonOpacity(disabled, pressed),\n width: 22\n }}\n >\n {checked ?\n <Text style={{ color: theme.colors.onBrand, fontSize: 14, fontWeight: '700' }}>✓</Text> :\n null}\n </View>\n <View style={{ flex: 1, gap: theme.spacing.xs }}>\n <Text\n style={{\n color: theme.colors.ink,\n fontSize: theme.fontSizes.sm,\n fontWeight: String(theme.fontWeights.semibold) as TextStyle['fontWeight']\n }}\n >\n {label}\n </Text>\n {description ?\n <Text style={{ color: theme.colors.inkMuted, fontSize: theme.fontSizes.xs }}>{description}</Text> :\n null}\n </View>\n </>\n )}\n </Pressable>\n )\n}\n\nexport interface LumenRadioGroupProps extends Omit<ViewProps, 'children'> {\n label: string\n onValueChange: (value: string) => void\n options: readonly LumenSelectionOption[]\n ref?: LumenViewRef\n value: string\n}\n\nexport const LumenRadioGroup = ({\n label,\n onValueChange,\n options,\n ref,\n style,\n value,\n ...props\n}: LumenRadioGroupProps): ReactElement => {\n const theme = useLumenTheme()\n\n return (\n <View\n ref={ref}\n {...props}\n accessibilityLabel={props.accessibilityLabel ?? label}\n accessibilityRole=\"radiogroup\"\n style={[{ gap: theme.spacing.sm }, style]}\n >\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 {options.map(option => {\n const selected = value === option.value\n const state = resolveLumenSelectionState(selected, option.disabled ?? false)\n\n return (\n <Pressable\n aria-checked={selected}\n aria-disabled={state.disabled}\n key={option.value}\n accessibilityRole=\"radio\"\n accessibilityState={{ checked: selected, disabled: state.disabled }}\n disabled={state.disabled}\n onPress={() => {\n onValueChange(option.value)\n }}\n style={({ pressed }) => ({\n alignItems: 'flex-start',\n flexDirection: 'row',\n gap: theme.spacing.md,\n minHeight: 44,\n opacity: state.opacity * resolveLumenButtonOpacity(state.disabled, pressed),\n paddingVertical: theme.spacing.sm\n })}\n >\n <View\n accessibilityElementsHidden\n importantForAccessibility=\"no-hide-descendants\"\n style={{\n alignItems: 'center',\n borderColor: selected ? theme.colors.brandSolid : theme.colors.line,\n borderRadius: 11,\n borderWidth: 2,\n height: 22,\n justifyContent: 'center',\n marginTop: 1,\n width: 22\n }}\n >\n {selected ?\n <View style={{ backgroundColor: theme.colors.brandSolid, borderRadius: 5, height: 10, width: 10 }} /> :\n null}\n </View>\n <View style={{ flex: 1, gap: theme.spacing.xs }}>\n <Text style={{ color: theme.colors.ink, fontSize: theme.fontSizes.sm }}>{option.label}</Text>\n {option.description ?\n (\n <Text style={{ color: theme.colors.inkMuted, fontSize: theme.fontSizes.xs }}>\n {option.description}\n </Text>\n ) :\n null}\n </View>\n </Pressable>\n )\n })}\n </View>\n )\n}\n\nexport interface LumenSegmentedControlProps extends Omit<ViewProps, 'children'> {\n label: string\n onValueChange: (value: string) => void\n options: readonly LumenSelectionOption[]\n ref?: LumenViewRef\n showLabel?: boolean\n value: string\n}\n\nexport const LumenSegmentedControl = ({\n label,\n onValueChange,\n options,\n ref,\n showLabel = true,\n style,\n value,\n ...props\n}: LumenSegmentedControlProps): ReactElement => {\n const theme = useLumenTheme()\n\n return (\n <View ref={ref} {...props} style={[{ gap: theme.spacing.sm }, style]}>\n {showLabel ?\n (\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 ) :\n null}\n <View\n accessibilityLabel={label}\n accessibilityRole=\"radiogroup\"\n style={{\n backgroundColor: theme.colors.surfaceMuted,\n borderColor: theme.colors.line,\n borderRadius: theme.radii.sm,\n borderWidth: 1,\n flexDirection: 'row',\n padding: 2\n }}\n >\n {options.map(option => {\n const selected = value === option.value\n const state = resolveLumenSelectionState(selected, option.disabled ?? false)\n\n return (\n <Pressable\n aria-checked={selected}\n aria-disabled={state.disabled}\n key={option.value}\n accessibilityLabel={option.label}\n accessibilityRole=\"radio\"\n accessibilityState={{ checked: selected, disabled: state.disabled }}\n disabled={state.disabled}\n onPress={() => {\n onValueChange(option.value)\n }}\n style={({ pressed }) => ({\n alignItems: 'center',\n backgroundColor: selected ? theme.colors.surface : 'transparent',\n borderRadius: theme.radii.sm,\n flex: 1,\n justifyContent: 'center',\n minHeight: 40,\n opacity: state.opacity * resolveLumenButtonOpacity(state.disabled, pressed),\n paddingHorizontal: theme.spacing.sm,\n paddingVertical: theme.spacing.sm\n })}\n >\n <Text\n numberOfLines={1}\n style={{\n color: selected ? theme.colors.brand : theme.colors.inkSoft,\n fontSize: theme.fontSizes.xs,\n fontWeight: String(\n selected ? theme.fontWeights.bold : theme.fontWeights.semibold\n ) as TextStyle['fontWeight']\n }}\n >\n {option.label}\n </Text>\n </Pressable>\n )\n })}\n </View>\n </View>\n )\n}\n\nexport interface LumenTabsProps extends Omit<ViewProps, 'children'> {\n children: ReactNode\n label: string\n onValueChange: (value: string) => void\n options: readonly LumenSelectionOption[]\n /** Localized panel name; defaults to the selected tab's visible label. */\n panelAccessibilityLabel?: string\n ref?: LumenViewRef\n value: string\n}\n\nexport const LumenTabs = ({\n children,\n label,\n onValueChange,\n options,\n panelAccessibilityLabel,\n ref,\n style,\n value,\n ...props\n}: LumenTabsProps): ReactElement => {\n const theme = useLumenTheme()\n const selectedOption = options.find(option => option.value === value)\n const selectedIndex = options.findIndex(option => option.value === value)\n const tabsId = useId().replaceAll(':', '')\n const pendingFocusValueRef = useRef<string | undefined>(undefined)\n const tabInstancesRef = useRef<(HostInstance | null)[]>([])\n const tabLayoutsRef = useRef(new Map<number, { width: number, x: number }>())\n const tabListRef = useRef<ComponentRef<typeof ScrollView>>(null)\n const viewportWidthRef = useRef(0)\n\n const scrollTabIntoView = (index: number, animated: boolean): void => {\n const layout = tabLayoutsRef.current.get(index)\n\n if (!layout) return\n\n const centeredOffset = layout.x - Math.max(0, (viewportWidthRef.current - layout.width) / 2)\n\n tabListRef.current?.scrollTo({ animated, x: Math.max(0, centeredOffset) })\n }\n\n useEffect(() => {\n if (selectedIndex < 0) return\n\n scrollTabIntoView(selectedIndex, true)\n\n if (pendingFocusValueRef.current === value) {\n tabInstancesRef.current[selectedIndex]?.focus()\n\n pendingFocusValueRef.current = undefined\n }\n }, [selectedIndex, value])\n\n const selectTab = (optionIndex: number): boolean => {\n const option = options[optionIndex]\n\n if (!option || option.disabled) return false\n\n if (option.value === value) {\n tabInstancesRef.current[optionIndex]?.focus()\n\n return true\n }\n\n pendingFocusValueRef.current = option.value\n\n onValueChange(option.value)\n\n return true\n }\n\n const moveSelection = (currentIndex: number, direction: -1 | 1): void => {\n for (let offset = 1; offset < options.length; offset += 1) {\n const nextIndex = (currentIndex + direction * offset + options.length) % options.length\n\n if (selectTab(nextIndex)) return\n }\n }\n\n return (\n <View ref={ref} {...props} style={[{ gap: theme.spacing.md }, style]}>\n <ScrollView\n accessibilityLabel={label}\n accessibilityRole=\"tablist\"\n contentContainerStyle={{ gap: theme.spacing.xs }}\n horizontal\n onLayout={event => {\n viewportWidthRef.current = event.nativeEvent.layout.width\n\n if (selectedIndex >= 0) scrollTabIntoView(selectedIndex, false)\n }}\n ref={tabListRef}\n showsHorizontalScrollIndicator={false}\n style={{ borderBottomColor: theme.colors.line, borderBottomWidth: 1 }}\n >\n {options.map((option, optionIndex) => {\n const selected = option.value === value\n const state = resolveLumenSelectionState(selected, option.disabled ?? false)\n const tabId = `${tabsId}-tab-${optionIndex}`\n\n return (\n <Pressable\n aria-disabled={state.disabled}\n aria-selected={selected}\n accessibilityRole=\"tab\"\n accessibilityState={{ disabled: state.disabled, selected }}\n disabled={state.disabled}\n key={option.value}\n nativeID={tabId}\n onKeyDown={event => {\n const key = event.nativeEvent.key || event.nativeEvent.code\n\n if (key === 'ArrowLeft') {\n event.preventDefault()\n\n moveSelection(optionIndex, -1)\n } else if (key === 'ArrowRight') {\n event.preventDefault()\n\n moveSelection(optionIndex, 1)\n }\n }}\n onLayout={event => {\n tabLayoutsRef.current.set(optionIndex, event.nativeEvent.layout)\n\n if (selected) scrollTabIntoView(optionIndex, false)\n }}\n onPress={() => {\n onValueChange(option.value)\n }}\n ref={instance => {\n tabInstancesRef.current[optionIndex] = instance\n }}\n style={({ pressed }) => ({\n borderBottomColor: selected ? theme.colors.brandSolid : 'transparent',\n borderBottomWidth: 2,\n minHeight: 44,\n opacity: state.opacity * resolveLumenButtonOpacity(state.disabled, pressed),\n paddingHorizontal: theme.spacing.md,\n paddingVertical: theme.spacing.sm\n })}\n >\n <Text\n style={{\n color: selected ? theme.colors.brand : theme.colors.inkSoft,\n fontSize: theme.fontSizes.sm,\n fontWeight: String(\n selected ? theme.fontWeights.bold : theme.fontWeights.semibold\n ) as TextStyle['fontWeight']\n }}\n >\n {option.label}\n </Text>\n </Pressable>\n )\n })}\n </ScrollView>\n <View\n aria-labelledby={selectedIndex >= 0 ? `${tabsId}-tab-${selectedIndex}` : undefined}\n accessibilityLabel={panelAccessibilityLabel ?? selectedOption?.label ?? value}\n accessibilityLiveRegion=\"polite\"\n role=\"tabpanel\"\n >\n {children}\n </View>\n </View>\n )\n}\n",
60267
60486
  "packages/react-native/src/selection-recipes.test.ts": "import { describe, expect, test } from 'vitest'\n\nimport { resolveLumenSelectionState } from './selection-recipes.js'\n\ndescribe('resolveLumenSelectionState', () => {\n test('preserves selection and enabled state', () => {\n expect(resolveLumenSelectionState(true, false)).toEqual({\n disabled: false,\n opacity: 1,\n selected: true\n })\n })\n\n test('dims disabled choices without changing selection', () => {\n expect(resolveLumenSelectionState(false, true)).toEqual({\n disabled: true,\n opacity: 0.52,\n selected: false\n })\n })\n})\n",
60268
60487
  "packages/react-native/src/selection-recipes.ts": "export interface LumenSelectionState {\n disabled: boolean\n opacity: number\n selected: boolean\n}\n\nexport const resolveLumenSelectionState = (\n selected: boolean,\n disabled: boolean\n): LumenSelectionState => ({\n disabled,\n opacity: disabled ? 0.52 : 1,\n selected\n})\n",
60269
60488
  "packages/react-native/src/shared-components.tsx": "import {\n createContext,\n type ReactElement,\n type ReactNode,\n use\n} from 'react'\nimport {\n type ColorValue,\n Image,\n type ImageSourcePropType,\n Pressable,\n type PressableProps,\n Text,\n type TextProps,\n type TextStyle,\n View,\n type ViewProps\n} from 'react-native'\n\nimport type { LumenTextRef, LumenViewRef } from './native-ref-types.js'\nimport {\n type LumenSurfacePadding,\n type LumenSurfaceRadius\n} from './primitives.js'\nimport {\n resolveLumenButtonOpacity,\n resolveLumenPressableStyle\n} from './recipes.js'\nimport {\n type LumenAlertVariant,\n type LumenAvatarSize,\n type LumenCardVariant,\n resolveLumenAlertColors,\n resolveLumenAvatarSize,\n resolveLumenCardColors,\n resolveLumenProgressValue,\n resolveLumenSurfacePadding,\n resolveLumenSurfaceRadius\n} from './shared-recipes.js'\nimport { useLumenTheme } from './theme-context.js'\n\nconst resolveLumenBorderWidth = (theme: ReturnType<typeof useLumenTheme>): number => theme.appearance?.borderWidth ?? 1\n\nexport interface LumenCardProps extends Omit<PressableProps, 'children'> {\n children: ReactNode\n padding?: LumenSurfacePadding\n radius?: LumenSurfaceRadius\n ref?: LumenViewRef\n variant?: LumenCardVariant\n}\n\nexport const LumenCard = ({\n accessibilityRole,\n accessibilityState,\n children,\n disabled = false,\n onLongPress,\n onPress,\n padding = 'xl',\n radius = 'lg',\n ref,\n style,\n variant = 'default',\n ...props\n}: LumenCardProps): ReactElement => {\n const theme = useLumenTheme()\n const isDisabled = disabled === true\n const interactive = Boolean(onPress ?? onLongPress)\n const colors = resolveLumenCardColors(theme.colors, variant)\n\n const cardStyle = {\n backgroundColor: colors.backgroundColor,\n borderColor: colors.borderColor,\n borderRadius: resolveLumenSurfaceRadius(theme, radius),\n borderWidth: resolveLumenBorderWidth(theme),\n gap: theme.spacing.lg,\n padding: resolveLumenSurfacePadding(theme, padding)\n }\n\n if (!interactive) {\n return (\n <View\n ref={ref}\n {...props}\n accessibilityRole={accessibilityRole}\n accessibilityState={accessibilityState}\n style={[\n cardStyle,\n resolveLumenPressableStyle(style, { pressed: false })\n ]}\n >\n {children}\n </View>\n )\n }\n\n return (\n <Pressable\n ref={ref}\n {...props}\n accessibilityRole={accessibilityRole ?? 'button'}\n accessibilityState={{\n ...accessibilityState,\n disabled: isDisabled || undefined\n }}\n disabled={isDisabled}\n onLongPress={onLongPress}\n onPress={onPress}\n style={state => [\n {\n ...cardStyle,\n borderColor: state.pressed ? theme.colors.brand : colors.borderColor,\n opacity: resolveLumenButtonOpacity(isDisabled, state.pressed)\n },\n resolveLumenPressableStyle(style, state)\n ]}\n >\n {children}\n </Pressable>\n )\n}\n\nexport interface LumenAlertProps extends ViewProps {\n children: ReactNode\n ref?: LumenViewRef\n variant?: LumenAlertVariant\n}\n\ninterface LumenAlertContentColors {\n description: ColorValue\n foreground: ColorValue\n}\n\nconst LumenAlertColorContext = createContext<LumenAlertContentColors | null>(\n null\n)\n\nexport const LumenAlert = ({\n children,\n ref,\n style,\n variant = 'default',\n ...props\n}: LumenAlertProps): ReactElement => {\n const theme = useLumenTheme()\n const colors = resolveLumenAlertColors(theme.colors, variant)\n\n return (\n <View\n ref={ref}\n {...props}\n style={[\n {\n backgroundColor: colors.backgroundColor,\n borderColor: colors.borderColor,\n borderRadius: theme.radii.md,\n borderWidth: 1,\n gap: theme.spacing.xs,\n paddingHorizontal: theme.spacing.lg,\n paddingVertical: theme.spacing.md\n },\n style\n ]}\n >\n <LumenAlertColorContext\n value={{\n description:\n variant === 'default' ? theme.colors.inkSoft : colors.color,\n foreground: colors.color\n }}\n >\n {children}\n </LumenAlertColorContext>\n </View>\n )\n}\n\nexport interface LumenAlertTitleProps extends TextProps {\n ref?: LumenTextRef\n}\n\nexport const LumenAlertTitle = ({\n ref,\n style,\n ...props\n}: LumenAlertTitleProps): ReactElement => {\n const theme = useLumenTheme()\n const colors = use(LumenAlertColorContext)\n\n return (\n <Text\n ref={ref}\n {...props}\n style={[\n {\n color: colors?.foreground ?? theme.colors.ink,\n fontSize: theme.fontSizes.sm,\n fontWeight: String(\n theme.fontWeights.semibold\n ) as TextStyle['fontWeight']\n },\n style\n ]}\n />\n )\n}\n\nexport interface LumenAlertDescriptionProps extends TextProps {\n ref?: LumenTextRef\n}\n\nexport const LumenAlertDescription = ({\n ref,\n style,\n ...props\n}: LumenAlertDescriptionProps): ReactElement => {\n const theme = useLumenTheme()\n const colors = use(LumenAlertColorContext)\n\n return (\n <Text\n ref={ref}\n {...props}\n style={[\n {\n color: colors?.description ?? theme.colors.inkSoft,\n fontSize: theme.fontSizes.sm,\n lineHeight: 20\n },\n style\n ]}\n />\n )\n}\n\nexport interface LumenProgressProps extends ViewProps {\n color?: ColorValue\n label?: string\n max?: number\n ref?: LumenViewRef\n value?: number\n}\n\nexport const LumenProgress = ({\n color,\n label,\n max = 100,\n ref,\n style,\n value = 0,\n ...props\n}: LumenProgressProps): ReactElement => {\n const theme = useLumenTheme()\n const progress = resolveLumenProgressValue(value, max)\n\n return (\n <View\n ref={ref}\n {...props}\n accessibilityLabel={label}\n accessibilityRole=\"progressbar\"\n accessibilityValue={{ max: progress.max, min: 0, now: progress.value }}\n style={[\n {\n backgroundColor: theme.colors.surfaceStrong,\n borderRadius: theme.radii.full,\n height: theme.spacing.sm,\n overflow: 'hidden',\n width: '100%'\n },\n style\n ]}\n >\n <View\n importantForAccessibility=\"no\"\n style={{\n backgroundColor: color ?? theme.colors.brandSolid,\n borderRadius: theme.radii.full,\n height: '100%',\n width: `${progress.percentage}%`\n }}\n />\n </View>\n )\n}\n\nexport interface LumenAvatarProps extends ViewProps {\n fallback?: string\n label?: string\n ref?: LumenViewRef\n size?: LumenAvatarSize\n source?: ImageSourcePropType\n}\n\nexport const LumenAvatar = ({\n fallback = '?',\n label,\n ref,\n size = 'md',\n source,\n style,\n ...props\n}: LumenAvatarProps): ReactElement => {\n const theme = useLumenTheme()\n const dimension = resolveLumenAvatarSize(size)\n const fontSize = size === 'lg' ? theme.fontSizes.lg : theme.fontSizes.sm\n\n return (\n <View\n ref={ref}\n {...props}\n accessible={Boolean(label)}\n accessibilityLabel={label}\n accessibilityRole={label ? 'image' : undefined}\n style={[\n {\n alignItems: 'center',\n backgroundColor: theme.colors.surfaceMuted,\n borderColor: theme.colors.line,\n borderRadius: theme.radii.full,\n borderWidth: 1,\n height: dimension,\n justifyContent: 'center',\n overflow: 'hidden',\n width: dimension\n },\n style\n ]}\n >\n {source ?\n (\n <Image\n accessibilityElementsHidden\n importantForAccessibility=\"no\"\n resizeMode=\"cover\"\n source={source}\n style={{ height: dimension, width: dimension }}\n />\n ) :\n (\n <Text\n accessibilityElementsHidden\n importantForAccessibility=\"no\"\n style={{\n color: theme.colors.ink,\n fontSize,\n fontWeight: String(\n theme.fontWeights.bold\n ) as TextStyle['fontWeight']\n }}\n >\n {fallback}\n </Text>\n )}\n </View>\n )\n}\n",
@@ -60310,7 +60529,7 @@
60310
60529
  "packages/react-native/src/tree-select-components.tsx": "import { type ReactElement, useState } from 'react'\nimport { FlatList, View, type ViewProps } from 'react-native'\n\nimport { LumenButton, LumenText } from './primitives.js'\nimport { useLumenTheme } from './theme-context.js'\nimport type { LumenTreeNode, LumenTreeRow } from './tree-recipes.js'\nimport { LumenTreeSelectModel } from './tree-select-recipes.js'\n\nexport interface LumenTreeSelectProps extends Pick<ViewProps, 'style' | 'testID' | 'nativeID' | 'accessibilityHint'> {\n label: string\n nodes: readonly LumenTreeNode[]\n value: string | null\n onValueChange: (value: string) => void\n disabled?: boolean | undefined\n readOnly?: boolean | undefined\n loading?: boolean | undefined\n error?: string | null | undefined\n placeholder?: string | undefined\n unknownSelectionLabel?: string | undefined\n loadingLabel?: string | undefined\n emptyLabel?: string | undefined\n invalidLabel?: string | undefined\n expandedLabel?: string | undefined\n collapsedLabel?: string | undefined\n formatOption?: ((label: string, path: readonly string[], level: number) => string) | undefined\n}\n\nconst defaultOption = (_label: string, path: readonly string[], level: number): string => `${path.join(' / ')}, level ${level}`\n\nconst statusLabel = (props: LumenTreeSelectProps, model: LumenTreeSelectModel): string | null => {\n if (props.loading) return props.loadingLabel ?? 'Loading'\n\n if (props.error !== undefined && props.error !== null) return props.error\n\n if (!model.valid) return props.invalidLabel ?? 'Invalid options'\n\n return null\n}\n\nconst TreeSelectOption = ({ props, model, row, close }: {\n props: LumenTreeSelectProps\n model: LumenTreeSelectModel\n row: LumenTreeRow\n close: () => void\n}): ReactElement => {\n const theme = useLumenTheme()\n const blocked = (props.disabled ?? false) || (props.readOnly ?? false) || !model.canSelect(row.node.id)\n const path = model.tree.path(row.node.id).map(node => node.label)\n const name = (props.formatOption ?? defaultOption)(row.node.label, path, row.depth + 1)\n\n return (\n <View style={{ marginStart: Math.min(row.depth, 8) * theme.spacing.sm }}>\n <LumenButton\n disabled={blocked}\n accessibilityLabel={name}\n accessibilityState={{ selected: props.value === row.node.id, disabled: blocked }}\n onPress={() => {\n if (blocked) return\n\n const next = model.selecting(row.node.id, props.value)\n\n if (next !== null && next !== props.value) props.onValueChange(next)\n\n close()\n }}\n >\n {row.node.label}\n </LumenButton>\n </View>\n )\n}\n\nexport const LumenTreeSelect = (props: LumenTreeSelectProps): ReactElement => {\n const theme = useLumenTheme()\n const [open, setOpen] = useState(false)\n const model = new LumenTreeSelectModel(props.nodes)\n const status = statusLabel(props, model)\n const selection = model.selectionLabel(props.value, props.placeholder ?? 'Select…', props.unknownSelectionLabel ?? 'Unavailable selection')\n\n return (\n <View\n testID={props.testID}\n nativeID={props.nativeID}\n accessibilityLabel={props.label}\n accessibilityHint={props.accessibilityHint}\n style={[{ gap: theme.spacing.sm }, props.style]}\n >\n <LumenText>{props.label}</LumenText>\n {status !== null ? <LumenText accessibilityLiveRegion=\"polite\">{selection}</LumenText> : null}\n {status !== null ?\n <LumenText accessibilityLiveRegion=\"polite\">{status}</LumenText> :\n (\n <>\n <LumenButton\n disabled={props.disabled}\n accessibilityLabel={`${props.label}: ${selection}`}\n accessibilityState={{ expanded: open }}\n accessibilityValue={{ text: open ? props.expandedLabel ?? 'Expanded' : props.collapsedLabel ?? 'Collapsed' }}\n onPress={() => {\n if (!props.disabled) setOpen(current => !current)\n }}\n >\n {selection}\n </LumenButton>\n {open ?\n (\n <FlatList\n nestedScrollEnabled\n style={{ maxHeight: 320 }}\n accessibilityLabel={props.label}\n data={model.rows}\n keyExtractor={row => row.node.id}\n ListEmptyComponent={<LumenText>{props.emptyLabel ?? 'No options'}</LumenText>}\n renderItem={({ item }) => (\n <TreeSelectOption\n props={props}\n model={model}\n row={item}\n close={() => {\n setOpen(false)\n }}\n />\n )}\n />\n ) :\n null}\n </>\n )}\n </View>\n )\n}\n",
60311
60530
  "packages/react-native/src/tree-select-recipes.test.ts": "import { expect, test } from 'vitest'\n\nimport { LumenTreeSelectModel } from './tree-select-recipes.js'\n\ntest('all hierarchical rows are shown and both parents and leaves can be selected', () => {\n const model = new LumenTreeSelectModel([\n { id: 'root', label: 'Root' }, { id: 'leaf', label: 'Leaf', parentId: 'root' }\n ])\n\n expect(model.rows.map(row => [row.node.id, row.depth])).toEqual([['root', 0], ['leaf', 1]])\n expect(model.selecting('root', 'missing')).toBe('root')\n expect(model.selecting('leaf', null)).toBe('leaf')\n expect(model.selectionLabel('missing', 'Choose', 'Unknown')).toBe('Unknown')\n expect(model.selectionLabel(null, 'Choose', 'Unknown')).toBe('Choose')\n})\n\ntest('disabled ancestry, excluded nodes and unknown IDs retain the host value', () => {\n const model = new LumenTreeSelectModel([\n { id: 'root', label: 'Root', disabled: true },\n { id: 'leaf', label: 'Leaf', parentId: 'root' },\n { id: 'group', label: 'Group', selectable: false }\n ])\n\n for (const id of ['root', 'leaf', 'group', 'missing']) expect(model.selecting(id, 'host-only')).toBe('host-only')\n expect(model.selectionLabel('leaf', 'Choose', 'Unknown')).toBe('Leaf')\n})\n\ntest('invalid graphs hide options and cannot rewrite an unknown controlled selection', () => {\n for (const nodes of [\n [{ id: ' ', label: '' }],\n [{ id: 'a', label: '' }, { id: 'a', label: '' }],\n [{ id: 'a', label: '', parentId: 'missing' }],\n [{ id: 'a', label: '', parentId: 'b' }, { id: 'b', label: '', parentId: 'a' }]\n ]) {\n const model = new LumenTreeSelectModel(nodes)\n\n expect(model.valid).toBe(false)\n expect(model.rows).toEqual([])\n expect(model.selecting('a', 'unknown')).toBe('unknown')\n }\n})\n\ntest('deep hierarchy traversal stays iterative', () => {\n const nodes = Array.from({ length: 10000 }, (_, index) => ({\n id: String(index), label: String(index), parentId: index === 0 ? null : String(index - 1)\n }))\n const model = new LumenTreeSelectModel(nodes)\n\n expect(model.rows).toHaveLength(10000)\n expect(model.rows.at(-1)?.depth).toBe(9999)\n})\n",
60312
60531
  "packages/react-native/src/tree-select-recipes.ts": "import { LumenTreeModel, type LumenTreeNode, type LumenTreeRow } from './tree-recipes.js'\n\n/** Single selection over the validated hierarchy; parent nodes are selectable. */\nexport class LumenTreeSelectModel {\n readonly tree: LumenTreeModel\n readonly rows: readonly LumenTreeRow[]\n readonly valid: boolean\n\n constructor(nodes: readonly LumenTreeNode[]) {\n this.tree = new LumenTreeModel(nodes)\n\n this.valid = this.tree.valid && nodes.every(node => node.id.trim().length > 0)\n\n this.rows = this.valid ? this.tree.visibleRows(new Set(nodes.map(node => node.id))) : []\n }\n\n canSelect(id: string): boolean {\n const node = this.tree.node(id)\n\n return this.valid && node !== undefined && !this.tree.isDisabled(id) && node.selectable !== false\n }\n\n selecting(id: string, current: string | null): string | null {\n return this.canSelect(id) ? id : current\n }\n\n selectionLabel(value: string | null, placeholder: string, unknownLabel: string): string {\n if (value === null) return placeholder\n\n if (!this.valid) return unknownLabel\n\n return this.tree.node(value)?.label ?? unknownLabel\n }\n}\n",
60313
- "packages/react-native/src/value-components.tsx": "import {\n type ReactElement,\n type ReactNode,\n useState\n} from 'react'\nimport {\n type AccessibilityActionEvent,\n I18nManager,\n type LayoutChangeEvent,\n type NativeSyntheticEvent,\n type NativeTouchEvent,\n Pressable,\n Text,\n type TextStyle,\n View,\n type ViewProps\n} from 'react-native'\nimport { Circle, Svg } from 'react-native-svg'\n\nimport type { LumenViewRef } from './native-ref-types.js'\nimport { LumenIcon } from './primitives.js'\nimport { resolveLumenProgressValue } from './shared-recipes.js'\nimport {\n type LumenMetricTone,\n resolveLumenMetricColor\n} from './structured-recipes.js'\nimport { useLumenTheme } from './theme-context.js'\nimport {\n resolveLumenRangeValue,\n resolveLumenSliderPosition,\n resolveLumenSliderValue,\n stepLumenSliderValue\n} from './value-recipes.js'\n\nexport interface LumenPickerOption<Value extends number | string> {\n disabled?: boolean\n label: string\n value: Value\n}\n\nexport interface LumenPickerProps<Value extends number | string> extends Omit<ViewProps, 'children'> {\n enabled?: boolean\n label: string\n onValueChange: (value: Value) => void\n options: readonly LumenPickerOption<Value>[]\n ref?: LumenViewRef\n value: Value\n}\n\nconst resolveControlOpacity = (enabled: boolean, pressed: boolean): number => {\n if (!enabled) return 0.5\n\n return pressed ? 0.82 : 1\n}\n\nconst LumenPickerControl = <Value extends number | string,>({\n enabled = true,\n label,\n onValueChange,\n options,\n ref,\n style,\n value,\n ...props\n}: LumenPickerProps<Value>): ReactElement => {\n const theme = useLumenTheme()\n const [expanded, setExpanded] = useState(false)\n const controlEnabled = enabled && options.length > 0\n const selected = options.find(option => option.value === value)\n const selectedLabel = selected?.label ?? String(value)\n const menuVisible = controlEnabled && expanded\n\n return (\n <View ref={ref} {...props} style={[{ gap: theme.spacing.xs }, 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 </Text>\n <Pressable\n accessibilityLabel={label}\n accessibilityRole=\"combobox\"\n accessibilityState={{ disabled: !controlEnabled, expanded: menuVisible }}\n accessibilityValue={{ text: selectedLabel }}\n disabled={!controlEnabled}\n onPress={() => {\n setExpanded(current => !current)\n }}\n style={({ pressed }) => ({\n alignItems: 'center',\n backgroundColor: theme.colors.surface,\n borderColor: menuVisible ? theme.colors.brand : theme.colors.line,\n borderRadius: theme.radii.md,\n borderWidth: 1,\n flexDirection: 'row',\n justifyContent: 'space-between',\n minHeight: 44,\n opacity: resolveControlOpacity(controlEnabled, pressed),\n paddingHorizontal: theme.spacing.md,\n paddingVertical: theme.spacing.sm\n })}\n >\n <Text style={{ color: theme.colors.ink, fontSize: theme.fontSizes.sm }}>{selectedLabel}</Text>\n <LumenIcon\n color={theme.colors.inkMuted}\n decorative\n name={menuVisible ? 'chevron-up' : 'chevron-down'}\n size=\"sm\"\n />\n </Pressable>\n {menuVisible ?\n (\n <View\n accessibilityLabel={`${label} options`}\n accessibilityRole=\"menu\"\n style={{\n backgroundColor: theme.colors.surface,\n borderColor: theme.colors.line,\n borderRadius: theme.radii.md,\n borderWidth: 1,\n overflow: 'hidden'\n }}\n >\n {options.map(option => {\n const optionDisabled = option.disabled ?? false\n const optionSelected = option.value === value\n\n return (\n <Pressable\n accessibilityRole=\"menuitem\"\n accessibilityState={{ disabled: optionDisabled, selected: optionSelected }}\n disabled={optionDisabled}\n key={`${typeof option.value}:${String(option.value)}`}\n onPress={() => {\n onValueChange(option.value)\n\n setExpanded(false)\n }}\n style={({ pressed }) => ({\n backgroundColor: optionSelected ? theme.colors.brandSoft : theme.colors.surface,\n justifyContent: 'center',\n minHeight: 44,\n opacity: resolveControlOpacity(!optionDisabled, pressed),\n paddingHorizontal: theme.spacing.md,\n paddingVertical: theme.spacing.sm\n })}\n >\n <Text style={{ color: theme.colors.ink, fontSize: theme.fontSizes.sm }}>\n {option.label}\n </Text>\n </Pressable>\n )\n })}\n </View>\n ) :\n null}\n </View>\n )\n}\n\nexport const LumenPicker = <Value extends number | string,>(\n props: LumenPickerProps<Value>\n): ReactElement => (\n <LumenPickerControl\n key={String((props.enabled ?? true) && props.options.length > 0)}\n {...props}\n />\n)\n\nexport interface LumenSliderProps extends Omit<ViewProps, 'children'> {\n enabled?: boolean\n label: string\n max?: number\n min?: number\n onValueChange: (value: number) => void\n ref?: LumenViewRef\n step?: number\n value: number\n valueLabel?: string\n}\n\nexport const LumenSlider = ({\n enabled = true,\n label,\n max = 100,\n min = 0,\n onValueChange,\n ref,\n step,\n style,\n value,\n valueLabel,\n ...props\n}: LumenSliderProps): ReactElement => {\n const theme = useLumenTheme()\n const [trackWidth, setTrackWidth] = useState(0)\n const resolved = resolveLumenSliderValue(value, min, max, step)\n const formattedValue = valueLabel ?? String(resolved.value)\n\n const updateFromTouch = (event: NativeSyntheticEvent<NativeTouchEvent>): void => {\n if (!enabled) return\n\n const location = I18nManager.isRTL ? trackWidth - event.nativeEvent.locationX : event.nativeEvent.locationX\n\n onValueChange(resolveLumenSliderPosition(location, trackWidth, resolved))\n }\n\n const handleAccessibilityAction = (event: AccessibilityActionEvent): void => {\n if (!enabled) return\n\n const action = event.nativeEvent.actionName\n\n if (action === 'increment' || action === 'decrement') {\n onValueChange(stepLumenSliderValue(resolved, action))\n }\n }\n\n const handleLayout = (event: LayoutChangeEvent): void => {\n setTrackWidth(event.nativeEvent.layout.width)\n }\n\n return (\n <View ref={ref} {...props} style={[{ gap: theme.spacing.sm }, style]}>\n <View style={{ alignItems: 'baseline', flexDirection: 'row', justifyContent: 'space-between' }}>\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 <Text style={{ color: theme.colors.inkMuted, fontSize: theme.fontSizes.xs }}>\n {formattedValue}\n </Text>\n </View>\n <View\n accessible\n accessibilityActions={[{ name: 'decrement' }, { name: 'increment' }]}\n accessibilityLabel={label}\n accessibilityRole=\"adjustable\"\n aria-disabled={!enabled}\n aria-valuemax={resolved.max}\n aria-valuemin={resolved.min}\n aria-valuenow={resolved.value}\n aria-valuetext={formattedValue}\n accessibilityState={{ disabled: !enabled }}\n accessibilityValue={{\n max: resolved.max,\n min: resolved.min,\n now: resolved.value,\n text: formattedValue\n }}\n onAccessibilityAction={handleAccessibilityAction}\n onLayout={handleLayout}\n onMoveShouldSetResponder={() => enabled}\n onResponderGrant={updateFromTouch}\n onResponderMove={updateFromTouch}\n onStartShouldSetResponder={() => enabled}\n style={{\n height: 44,\n justifyContent: 'center',\n opacity: enabled ? 1 : 0.5\n }}\n >\n <View\n importantForAccessibility=\"no-hide-descendants\"\n style={{\n backgroundColor: theme.colors.surfaceStrong,\n borderRadius: theme.radii.full,\n height: 6,\n overflow: 'visible'\n }}\n >\n <View\n style={{\n backgroundColor: theme.colors.brandSolid,\n borderRadius: theme.radii.full,\n height: 6,\n width: `${resolved.percentage}%`\n }}\n />\n <View\n style={{\n backgroundColor: theme.colors.brandSolid,\n borderColor: theme.colors.surface,\n borderRadius: 10,\n borderWidth: 2,\n height: 20,\n marginStart: -10,\n start: `${resolved.percentage}%`,\n position: 'absolute',\n top: -7,\n width: 20\n }}\n />\n </View>\n </View>\n </View>\n )\n}\n\nexport interface LumenGaugeProps extends Omit<ViewProps, 'children'> {\n center?: ReactNode\n label: string\n max?: number\n ref?: LumenViewRef\n tone?: LumenMetricTone\n value: number\n valueLabel: string\n}\n\nexport const LumenGauge = ({\n center,\n label,\n max = 100,\n ref,\n style,\n tone = 'brand',\n value,\n valueLabel,\n ...props\n}: LumenGaugeProps): ReactElement => {\n const theme = useLumenTheme()\n const progress = resolveLumenProgressValue(value, max)\n const circumference = 2 * Math.PI * 44\n const strokeDashoffset = circumference * (1 - progress.percentage / 100)\n\n return (\n <View\n ref={ref}\n {...props}\n accessibilityLabel={label}\n accessibilityRole=\"progressbar\"\n accessibilityValue={{ max: progress.max, min: 0, now: progress.value, text: valueLabel }}\n style={[{ alignItems: 'center', gap: theme.spacing.sm }, style]}\n >\n <View\n importantForAccessibility=\"no-hide-descendants\"\n style={{ alignItems: 'center', height: 108, justifyContent: 'center', width: 108 }}\n >\n <Svg height={108} style={{ position: 'absolute' }} viewBox=\"0 0 108 108\" width={108}>\n <Circle\n cx={54}\n cy={54}\n fill=\"none\"\n r={44}\n stroke={theme.colors.surfaceStrong}\n strokeWidth={10}\n />\n <Circle\n cx={54}\n cy={54}\n fill=\"none\"\n r={44}\n stroke={resolveLumenMetricColor(theme.colors, tone)}\n strokeDasharray={`${circumference} ${circumference}`}\n strokeDashoffset={strokeDashoffset}\n strokeLinecap=\"round\"\n strokeWidth={10}\n transform=\"rotate(-90 54 54)\"\n />\n </Svg>\n {center ?? (\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 {valueLabel}\n </Text>\n )}\n </View>\n <Text\n style={{\n color: theme.colors.inkSoft,\n fontSize: theme.fontSizes.xs,\n fontWeight: String(theme.fontWeights.semibold) as TextStyle['fontWeight']\n }}\n >\n {label}\n </Text>\n </View>\n )\n}\n\n/** Controlled interval with independent, named adjustable endpoints. */\nexport interface LumenRangeSliderProps extends Omit<ViewProps, 'children'> {\n enabled?: boolean\n endLabel?: string\n formatValue?: (value: number) => string\n label: string\n max?: number\n min?: number\n onValueChange: (value: readonly [number, number]) => void\n readOnly?: boolean\n ref?: LumenViewRef\n startLabel?: string\n step?: number\n value: readonly [number, number]\n}\n\nexport const LumenRangeSlider = ({\n enabled = true,\n endLabel = 'Maximum',\n formatValue = String,\n label,\n max = 100,\n min = 0,\n onValueChange,\n readOnly = false,\n ref,\n startLabel = 'Minimum',\n step,\n style,\n value,\n ...props\n}: LumenRangeSliderProps): ReactElement => {\n const theme = useLumenTheme()\n const [start, end] = resolveLumenRangeValue(value, min, max, step)\n const editable = enabled && !readOnly\n const sliderStep = step === undefined ? {} : { step }\n\n return (\n <View ref={ref} {...props} style={[{ gap: theme.spacing.sm }, style]}>\n <LumenSlider\n enabled={editable}\n label={`${label} · ${startLabel}`}\n max={max}\n min={min}\n onValueChange={next => {\n if (editable) onValueChange([Math.min(next, end), end])\n }}\n {...sliderStep}\n value={start}\n valueLabel={formatValue(start)}\n />\n <LumenSlider\n enabled={editable}\n label={`${label} · ${endLabel}`}\n max={max}\n min={min}\n onValueChange={next => {\n if (editable) onValueChange([start, Math.max(start, next)])\n }}\n {...sliderStep}\n value={end}\n valueLabel={formatValue(end)}\n />\n </View>\n )\n}\n",
60532
+ "packages/react-native/src/value-components.tsx": "import {\n type ReactElement,\n type ReactNode,\n useState\n} from 'react'\nimport {\n type AccessibilityActionEvent,\n type ColorValue,\n I18nManager,\n type LayoutChangeEvent,\n type NativeSyntheticEvent,\n type NativeTouchEvent,\n Pressable,\n Text,\n type TextStyle,\n View,\n type ViewProps\n} from 'react-native'\nimport { Circle, Svg } from 'react-native-svg'\n\nimport { type LumenFieldContextValue, useLumenFieldContext } from './field-context.js'\nimport type { LumenViewRef } from './native-ref-types.js'\nimport { LumenIcon } from './primitives.js'\nimport { resolveLumenAriaInvalid, resolveLumenValidationHint } from './recipes.js'\nimport { resolveLumenProgressValue } from './shared-recipes.js'\nimport {\n type LumenMetricTone,\n resolveLumenMetricColor\n} from './structured-recipes.js'\nimport { useLumenTheme } from './theme-context.js'\nimport {\n resolveLumenRangeValue,\n resolveLumenSliderPosition,\n resolveLumenSliderValue,\n stepLumenSliderValue\n} from './value-recipes.js'\n\nexport interface LumenPickerOption<Value extends number | string> {\n disabled?: boolean\n label: string\n value: Value\n}\n\nexport interface LumenPickerProps<Value extends number | string> extends Omit<ViewProps, 'children'> {\n enabled?: boolean\n label: string\n onValueChange: (value: Value) => void\n options: readonly LumenPickerOption<Value>[]\n ref?: LumenViewRef\n value: Value\n}\n\nconst resolveControlOpacity = (enabled: boolean, pressed: boolean): number => {\n if (!enabled) return 0.5\n\n return pressed ? 0.82 : 1\n}\n\nconst resolvePickerFieldValidation = (\n field: LumenFieldContextValue | null\n): { hint: string | undefined, invalid: true | undefined } => ({\n hint: resolveLumenValidationHint(field?.errorMessage, field?.description),\n invalid: resolveLumenAriaInvalid(field?.errorMessage)\n})\n\nconst resolvePickerBorderColor = (\n open: boolean,\n invalid: boolean,\n colors: { brand: ColorValue, danger: ColorValue, line: ColorValue }\n): ColorValue => {\n if (open) return colors.brand\n\n return invalid ? colors.danger : colors.line\n}\n\nconst LumenPickerControl = <Value extends number | string,>({\n enabled = true,\n label,\n onValueChange,\n options,\n ref,\n style,\n value,\n ...props\n}: LumenPickerProps<Value>): ReactElement => {\n const theme = useLumenTheme()\n // Inside a field group the trigger announces the group's error and description like other fields.\n const validation = resolvePickerFieldValidation(useLumenFieldContext())\n const [expanded, setExpanded] = useState(false)\n const controlEnabled = enabled && options.length > 0\n const selected = options.find(option => option.value === value)\n const selectedLabel = selected?.label ?? String(value)\n const menuVisible = controlEnabled && expanded\n\n return (\n <View ref={ref} {...props} style={[{ gap: theme.spacing.xs }, 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 </Text>\n <Pressable\n accessibilityHint={validation.hint}\n accessibilityLabel={label}\n accessibilityRole=\"combobox\"\n accessibilityState={{ disabled: !controlEnabled, expanded: menuVisible }}\n accessibilityValue={{ text: selectedLabel }}\n aria-invalid={validation.invalid}\n disabled={!controlEnabled}\n onPress={() => {\n setExpanded(current => !current)\n }}\n style={({ pressed }) => ({\n alignItems: 'center',\n backgroundColor: theme.colors.surface,\n borderColor: resolvePickerBorderColor(menuVisible, validation.invalid === true, theme.colors),\n borderRadius: theme.radii.md,\n borderWidth: 1,\n flexDirection: 'row',\n justifyContent: 'space-between',\n minHeight: 44,\n opacity: resolveControlOpacity(controlEnabled, pressed),\n paddingHorizontal: theme.spacing.md,\n paddingVertical: theme.spacing.sm\n })}\n >\n <Text style={{ color: theme.colors.ink, fontSize: theme.fontSizes.sm }}>{selectedLabel}</Text>\n <LumenIcon\n color={theme.colors.inkMuted}\n decorative\n name={menuVisible ? 'chevron-up' : 'chevron-down'}\n size=\"sm\"\n />\n </Pressable>\n {menuVisible ?\n (\n <View\n accessibilityLabel={`${label} options`}\n accessibilityRole=\"menu\"\n style={{\n backgroundColor: theme.colors.surface,\n borderColor: theme.colors.line,\n borderRadius: theme.radii.md,\n borderWidth: 1,\n overflow: 'hidden'\n }}\n >\n {options.map(option => {\n const optionDisabled = option.disabled ?? false\n const optionSelected = option.value === value\n\n return (\n <Pressable\n accessibilityRole=\"menuitem\"\n accessibilityState={{ disabled: optionDisabled, selected: optionSelected }}\n disabled={optionDisabled}\n key={`${typeof option.value}:${String(option.value)}`}\n onPress={() => {\n onValueChange(option.value)\n\n setExpanded(false)\n }}\n style={({ pressed }) => ({\n backgroundColor: optionSelected ? theme.colors.brandSoft : theme.colors.surface,\n justifyContent: 'center',\n minHeight: 44,\n opacity: resolveControlOpacity(!optionDisabled, pressed),\n paddingHorizontal: theme.spacing.md,\n paddingVertical: theme.spacing.sm\n })}\n >\n <Text style={{ color: theme.colors.ink, fontSize: theme.fontSizes.sm }}>\n {option.label}\n </Text>\n </Pressable>\n )\n })}\n </View>\n ) :\n null}\n </View>\n )\n}\n\nexport const LumenPicker = <Value extends number | string,>(\n props: LumenPickerProps<Value>\n): ReactElement => (\n <LumenPickerControl\n key={String((props.enabled ?? true) && props.options.length > 0)}\n {...props}\n />\n)\n\nexport interface LumenSliderProps extends Omit<ViewProps, 'children'> {\n enabled?: boolean\n label: string\n max?: number\n min?: number\n onValueChange: (value: number) => void\n ref?: LumenViewRef\n step?: number\n value: number\n valueLabel?: string\n}\n\nexport const LumenSlider = ({\n enabled = true,\n label,\n max = 100,\n min = 0,\n onValueChange,\n ref,\n step,\n style,\n value,\n valueLabel,\n ...props\n}: LumenSliderProps): ReactElement => {\n const theme = useLumenTheme()\n const [trackWidth, setTrackWidth] = useState(0)\n const resolved = resolveLumenSliderValue(value, min, max, step)\n const formattedValue = valueLabel ?? String(resolved.value)\n\n const updateFromTouch = (event: NativeSyntheticEvent<NativeTouchEvent>): void => {\n if (!enabled) return\n\n const location = I18nManager.isRTL ? trackWidth - event.nativeEvent.locationX : event.nativeEvent.locationX\n\n onValueChange(resolveLumenSliderPosition(location, trackWidth, resolved))\n }\n\n const handleAccessibilityAction = (event: AccessibilityActionEvent): void => {\n if (!enabled) return\n\n const action = event.nativeEvent.actionName\n\n if (action === 'increment' || action === 'decrement') {\n onValueChange(stepLumenSliderValue(resolved, action))\n }\n }\n\n const handleLayout = (event: LayoutChangeEvent): void => {\n setTrackWidth(event.nativeEvent.layout.width)\n }\n\n return (\n <View ref={ref} {...props} style={[{ gap: theme.spacing.sm }, style]}>\n <View style={{ alignItems: 'baseline', flexDirection: 'row', justifyContent: 'space-between' }}>\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 <Text style={{ color: theme.colors.inkMuted, fontSize: theme.fontSizes.xs }}>\n {formattedValue}\n </Text>\n </View>\n <View\n accessible\n accessibilityActions={[{ name: 'decrement' }, { name: 'increment' }]}\n accessibilityLabel={label}\n accessibilityRole=\"adjustable\"\n aria-disabled={!enabled}\n aria-valuemax={resolved.max}\n aria-valuemin={resolved.min}\n aria-valuenow={resolved.value}\n aria-valuetext={formattedValue}\n accessibilityState={{ disabled: !enabled }}\n accessibilityValue={{\n max: resolved.max,\n min: resolved.min,\n now: resolved.value,\n text: formattedValue\n }}\n onAccessibilityAction={handleAccessibilityAction}\n onLayout={handleLayout}\n onMoveShouldSetResponder={() => enabled}\n onResponderGrant={updateFromTouch}\n onResponderMove={updateFromTouch}\n onStartShouldSetResponder={() => enabled}\n style={{\n height: 44,\n justifyContent: 'center',\n opacity: enabled ? 1 : 0.5\n }}\n >\n <View\n importantForAccessibility=\"no-hide-descendants\"\n style={{\n backgroundColor: theme.colors.surfaceStrong,\n borderRadius: theme.radii.full,\n height: 6,\n overflow: 'visible'\n }}\n >\n <View\n style={{\n backgroundColor: theme.colors.brandSolid,\n borderRadius: theme.radii.full,\n height: 6,\n width: `${resolved.percentage}%`\n }}\n />\n <View\n style={{\n backgroundColor: theme.colors.brandSolid,\n borderColor: theme.colors.surface,\n borderRadius: 10,\n borderWidth: 2,\n height: 20,\n marginStart: -10,\n start: `${resolved.percentage}%`,\n position: 'absolute',\n top: -7,\n width: 20\n }}\n />\n </View>\n </View>\n </View>\n )\n}\n\nexport interface LumenGaugeProps extends Omit<ViewProps, 'children'> {\n center?: ReactNode\n label: string\n max?: number\n ref?: LumenViewRef\n tone?: LumenMetricTone\n value: number\n valueLabel: string\n}\n\nexport const LumenGauge = ({\n center,\n label,\n max = 100,\n ref,\n style,\n tone = 'brand',\n value,\n valueLabel,\n ...props\n}: LumenGaugeProps): ReactElement => {\n const theme = useLumenTheme()\n const progress = resolveLumenProgressValue(value, max)\n const circumference = 2 * Math.PI * 44\n const strokeDashoffset = circumference * (1 - progress.percentage / 100)\n\n return (\n <View\n ref={ref}\n {...props}\n accessibilityLabel={label}\n accessibilityRole=\"progressbar\"\n accessibilityValue={{ max: progress.max, min: 0, now: progress.value, text: valueLabel }}\n style={[{ alignItems: 'center', gap: theme.spacing.sm }, style]}\n >\n <View\n importantForAccessibility=\"no-hide-descendants\"\n style={{ alignItems: 'center', height: 108, justifyContent: 'center', width: 108 }}\n >\n <Svg height={108} style={{ position: 'absolute' }} viewBox=\"0 0 108 108\" width={108}>\n <Circle\n cx={54}\n cy={54}\n fill=\"none\"\n r={44}\n stroke={theme.colors.surfaceStrong}\n strokeWidth={10}\n />\n <Circle\n cx={54}\n cy={54}\n fill=\"none\"\n r={44}\n stroke={resolveLumenMetricColor(theme.colors, tone)}\n strokeDasharray={`${circumference} ${circumference}`}\n strokeDashoffset={strokeDashoffset}\n strokeLinecap=\"round\"\n strokeWidth={10}\n transform=\"rotate(-90 54 54)\"\n />\n </Svg>\n {center ?? (\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 {valueLabel}\n </Text>\n )}\n </View>\n <Text\n style={{\n color: theme.colors.inkSoft,\n fontSize: theme.fontSizes.xs,\n fontWeight: String(theme.fontWeights.semibold) as TextStyle['fontWeight']\n }}\n >\n {label}\n </Text>\n </View>\n )\n}\n\n/** Controlled interval with independent, named adjustable endpoints. */\nexport interface LumenRangeSliderProps extends Omit<ViewProps, 'children'> {\n enabled?: boolean\n endLabel?: string\n formatValue?: (value: number) => string\n label: string\n max?: number\n min?: number\n onValueChange: (value: readonly [number, number]) => void\n readOnly?: boolean\n ref?: LumenViewRef\n startLabel?: string\n step?: number\n value: readonly [number, number]\n}\n\nexport const LumenRangeSlider = ({\n enabled = true,\n endLabel = 'Maximum',\n formatValue = String,\n label,\n max = 100,\n min = 0,\n onValueChange,\n readOnly = false,\n ref,\n startLabel = 'Minimum',\n step,\n style,\n value,\n ...props\n}: LumenRangeSliderProps): ReactElement => {\n const theme = useLumenTheme()\n const [start, end] = resolveLumenRangeValue(value, min, max, step)\n const editable = enabled && !readOnly\n const sliderStep = step === undefined ? {} : { step }\n\n return (\n <View ref={ref} {...props} style={[{ gap: theme.spacing.sm }, style]}>\n <LumenSlider\n enabled={editable}\n label={`${label} · ${startLabel}`}\n max={max}\n min={min}\n onValueChange={next => {\n if (editable) onValueChange([Math.min(next, end), end])\n }}\n {...sliderStep}\n value={start}\n valueLabel={formatValue(start)}\n />\n <LumenSlider\n enabled={editable}\n label={`${label} · ${endLabel}`}\n max={max}\n min={min}\n onValueChange={next => {\n if (editable) onValueChange([start, Math.max(start, next)])\n }}\n {...sliderStep}\n value={end}\n valueLabel={formatValue(end)}\n />\n </View>\n )\n}\n",
60314
60533
  "packages/react-native/src/value-recipes.test.ts": "import { describe, expect, test } from 'vitest'\n\nimport {\n resolveLumenRangeValue,\n resolveLumenSliderPosition,\n resolveLumenSliderValue,\n stepLumenSliderValue\n} from './value-recipes.js'\n\ndescribe('Lumen React Native value recipes', () => {\n test('clamps and snaps slider values to a finite range', () => {\n expect(resolveLumenSliderValue(48, 0, 100, 10)).toEqual({\n max: 100,\n min: 0,\n percentage: 50,\n step: 10,\n value: 50\n })\n expect(resolveLumenSliderValue(Number.POSITIVE_INFINITY, 20, 10)).toMatchObject({\n max: 120,\n min: 20,\n value: 20\n })\n })\n\n test('maps pointer positions through the same step contract', () => {\n const value = resolveLumenSliderValue(0, 0, 100, 25)\n\n expect(resolveLumenSliderPosition(74, 100, value)).toBe(75)\n expect(resolveLumenSliderPosition(-20, 100, value)).toBe(0)\n expect(resolveLumenSliderPosition(140, 100, value)).toBe(100)\n })\n\n test('supports bounded assistive-technology increments', () => {\n const value = resolveLumenSliderValue(90, 0, 100, 10)\n\n expect(stepLumenSliderValue(value, 'increment')).toBe(100)\n expect(stepLumenSliderValue(value, 'decrement')).toBe(80)\n expect(stepLumenSliderValue(resolveLumenSliderValue(100, 0, 100, 10), 'increment')).toBe(100)\n })\n})\n\ndescribe('native numeric intervals', () => {\n test('normalizes unordered, out-of-range and nonfinite endpoints without mutating input', () => {\n const input = [80, 20] as const\n expect(resolveLumenRangeValue(input, 0, 100, 10)).toEqual([20, 80])\n expect(input).toEqual([80, 20])\n expect(resolveLumenRangeValue([-20, 120], 0, 100)).toEqual([0, 100])\n expect(resolveLumenRangeValue([Number.NaN, Number.POSITIVE_INFINITY], 0, 100)).toEqual([0, 100])\n expect(resolveLumenRangeValue([30, 30], 0, 100, 10)).toEqual([30, 30])\n })\n\n test('retains the domain step grid and nonzero origin', () => {\n expect(resolveLumenRangeValue([24, 76], 0, 100, 10)).toEqual([20, 80])\n expect(resolveLumenRangeValue([25, 96], 5, 96, 10)).toEqual([25, 95])\n expect(resolveLumenRangeValue([0.2, 0.8], 0, 1, 10)).toEqual([0, 1])\n })\n\n test('rejects unusable domains and steps before rendering', () => {\n const domains = [\n [1, 1],\n [2, 1],\n [Number.NaN, 1],\n [0, Number.POSITIVE_INFINITY],\n [-Number.MAX_VALUE, Number.MAX_VALUE],\n [0, Number.MIN_VALUE]\n ]\n\n for (const [min, max] of domains) {\n if (min === undefined || max === undefined) throw new Error('Missing bounds fixture')\n expect(() => resolveLumenRangeValue([0, 1], min, max)).toThrow(RangeError)\n }\n for (const step of [0, -1, Number.NaN, Number.POSITIVE_INFINITY, Number.MIN_VALUE]) {\n expect(() => resolveLumenRangeValue([0, 100], 0, 100, step)).toThrow(RangeError)\n }\n })\n})\n",
60315
60534
  "packages/react-native/src/value-recipes.ts": "export interface LumenSliderValue {\n max: number\n min: number\n percentage: number\n step: number\n value: number\n}\n\nconst finiteOr = (value: number, fallback: number): number => (\n Number.isFinite(value) ? value : fallback\n)\n\nexport const resolveLumenSliderValue = (\n value: number,\n min: number,\n max: number,\n step?: number\n): LumenSliderValue => {\n const safeMin = finiteOr(min, 0)\n const candidateMax = finiteOr(max, safeMin + 100)\n const safeMax = candidateMax > safeMin ? candidateMax : safeMin + 100\n const range = safeMax - safeMin\n const safeStep = Number.isFinite(step) && (step ?? 0) > 0 ? Math.min(step ?? 1, range) : range / 100\n const finiteValue = finiteOr(value, safeMin)\n const clampedValue = Math.min(safeMax, Math.max(safeMin, finiteValue))\n const stepIndex = Math.round((clampedValue - safeMin) / safeStep)\n const steppedValue = Math.min(safeMax, Math.max(safeMin, safeMin + stepIndex * safeStep))\n\n return {\n max: safeMax,\n min: safeMin,\n percentage: ((steppedValue - safeMin) / range) * 100,\n step: safeStep,\n value: steppedValue\n }\n}\n\nexport const resolveLumenSliderPosition = (\n position: number,\n width: number,\n value: LumenSliderValue\n): number => {\n if (!Number.isFinite(width) || width <= 0) return value.value\n\n const safePosition = Math.min(width, Math.max(0, finiteOr(position, 0)))\n const rawValue = value.min + (safePosition / width) * (value.max - value.min)\n\n return resolveLumenSliderValue(rawValue, value.min, value.max, value.step).value\n}\n\nexport const stepLumenSliderValue = (\n value: LumenSliderValue,\n direction: 'decrement' | 'increment'\n): number => resolveLumenSliderValue(\n value.value + (direction === 'increment' ? value.step : -value.step),\n value.min,\n value.max,\n value.step\n).value\n\n/** Reject unusable bounds before constructing native controls. Never mutate caller state. */\nexport const resolveLumenRangeValue = (\n value: readonly [number, number],\n min: number,\n max: number,\n step?: number\n): readonly [number, number] => {\n if (!Number.isFinite(min) || !Number.isFinite(max) || min >= max || !Number.isFinite(max - min)) {\n throw new RangeError('Range bounds must be finite, increasing, and have a finite span.')\n }\n\n if (step !== undefined && (!Number.isFinite(step) || step <= 0 || !Number.isFinite((max - min) / step))) {\n throw new RangeError('Range step must be finite, positive, and representable within the bounds.')\n }\n\n if ((max - min) / 100 === 0) throw new RangeError('Range span must support adjustment.')\n\n const start = resolveLumenSliderValue(finiteOr(value[0], min), min, max, step).value\n const end = resolveLumenSliderValue(finiteOr(value[1], max), min, max, step).value\n\n return [Math.min(start, end), Math.max(start, end)]\n}\n"
60316
60535
  },
@@ -60347,7 +60566,7 @@
60347
60566
  "packages/swift/Sources/LumenUI/KanbanBoardComponents.swift": "#if os(iOS) || os(macOS) || os(visionOS)\nimport SwiftUI\n\npublic struct LumenKanbanBoard<CardContent: View>: View {\n @Binding private var columns: [LumenKanbanColumnData]\n @Environment(\\.isEnabled) private var enabled\n private let label: String\n private let readOnly: Bool\n private let loading: Bool\n private let error: String?\n private let loadingLabel: String\n private let emptyLabel: String\n private let invalidLabel: String\n private let formatMove: (String, String, Int) -> String\n private let cardContent: (LumenKanbanCard) -> CardContent\n\n public init(_ label: String, columns: Binding<[LumenKanbanColumnData]>, readOnly: Bool = false,\n loading: Bool = false, error: String? = nil, loadingLabel: String = \"Loading\",\n emptyLabel: String = \"No cards\", invalidLabel: String = \"Invalid board data\",\n formatMove: @escaping (String, String, Int) -> String = { \"Move \\($0) to \\($1), position \\($2)\" },\n @ViewBuilder cardContent: @escaping (LumenKanbanCard) -> CardContent) {\n self.label = label; _columns = columns; self.readOnly = readOnly; self.loading = loading; self.error = error\n self.loadingLabel = loadingLabel; self.emptyLabel = emptyLabel; self.invalidLabel = invalidLabel\n self.formatMove = formatMove; self.cardContent = cardContent\n }\n private var interactive: Bool { enabled && !readOnly && !loading && error == nil }\n private var model: LumenKanbanModel { LumenKanbanModel(columns: columns) }\n private func move(_ cardId: String, to targetId: String, index: Int) -> Bool {\n guard interactive, let next = model.moving(cardId, toColumnId: targetId, toIndex: index) else { return false }\n columns = next\n return true\n }\n public var body: some View {\n VStack(alignment: .leading, spacing: LumenSpacing.md) {\n LumenText(.verbatim(label)).accessibilityAddTraits(.isHeader)\n if loading { LumenText(.verbatim(loadingLabel)) }\n else if let error { LumenText(.verbatim(error)) }\n else if !model.valid { LumenText(.verbatim(invalidLabel)) }\n else if columns.isEmpty { LumenText(.verbatim(emptyLabel)) }\n else {\n ScrollView(.horizontal) {\n HStack(alignment: .top, spacing: LumenSpacing.md) {\n ForEach(columns) { column in\n VStack(alignment: .leading, spacing: LumenSpacing.md) {\n LumenText(.verbatim(column.label)).accessibilityAddTraits(.isHeader)\n if column.cards.isEmpty { LumenText(.verbatim(emptyLabel)) }\n ForEach(Array(column.cards.enumerated()), id: \\.element.id) { index, card in\n VStack(alignment: .leading, spacing: LumenSpacing.xs) {\n if interactive && !column.disabled && !card.disabled {\n LumenCard(padding: .md, radius: .md) { cardContent(card) }.draggable(card.id)\n } else { LumenCard(padding: .md, radius: .md) { cardContent(card) } }\n ForEach(columns) { target in\n let positions = target.id == column.id ? [index - 1, index + 1].filter { column.cards.indices.contains($0) } : [target.cards.count]\n ForEach(positions, id: \\.self) { position in\n LumenButton(disabled: !interactive || model.moving(card.id, toColumnId: target.id, toIndex: position) == nil,\n action: { _ = move(card.id, to: target.id, index: position) }) {\n Text(formatMove(card.label, target.label, position + 1))\n }\n }\n }\n }.dropDestination(for: String.self) { ids, _ in\n guard ids.count == 1, let id = ids.first else { return false }\n let currentIndex = column.cards.firstIndex(where: { $0.id == id })\n return move(id, to: column.id, index: currentIndex.map { $0 < index ? index - 1 : index } ?? index)\n }\n }\n }.padding(LumenSpacing.md).frame(width: 272, alignment: .leading).frame(minHeight: 88, alignment: .topLeading)\n .dropDestination(for: String.self) { ids, _ in\n guard ids.count == 1, let id = ids.first else { return false }\n let count = column.cards.filter { $0.id != id }.count\n return move(id, to: column.id, index: count)\n }\n }\n }\n }\n }\n }.accessibilityElement(children: .contain).accessibilityLabel(Text(label))\n }\n}\npublic extension LumenKanbanBoard where CardContent == Text {\n init(_ label: String, columns: Binding<[LumenKanbanColumnData]>, readOnly: Bool = false,\n loading: Bool = false, error: String? = nil, loadingLabel: String = \"Loading\",\n emptyLabel: String = \"No cards\", invalidLabel: String = \"Invalid board data\",\n formatMove: @escaping (String, String, Int) -> String = { \"Move \\($0) to \\($1), position \\($2)\" }) {\n self.init(label, columns: columns, readOnly: readOnly, loading: loading, error: error,\n loadingLabel: loadingLabel, emptyLabel: emptyLabel, invalidLabel: invalidLabel,\n formatMove: formatMove, cardContent: { Text($0.label) })\n }\n}\n#endif\n",
60348
60567
  "packages/swift/Sources/LumenUI/KanbanColumnComponents.swift": "#if os(iOS) || os(macOS) || os(visionOS)\nimport SwiftUI\n\npublic struct LumenKanbanColumn<CardContent: View>: View {\n @Binding private var column: LumenKanbanColumnData\n @Environment(\\.isEnabled) private var enabled\n private let readOnly: Bool\n private let loading: Bool\n private let error: String?\n private let onAdd: (() -> Void)?\n private let onCardPress: ((String) -> Void)?\n private let addLabel: String\n private let loadingLabel: String\n private let emptyLabel: String\n private let invalidLabel: String\n private let formatCount: (Int, Int?) -> String\n private let formatMove: (String, Int) -> String\n private let formatOpen: (String) -> String\n private let cardContent: (LumenKanbanCard) -> CardContent\n\n public init(column: Binding<LumenKanbanColumnData>, readOnly: Bool = false,\n loading: Bool = false, error: String? = nil, onAdd: (() -> Void)? = nil,\n onCardPress: ((String) -> Void)? = nil, addLabel: String = \"Add card\",\n loadingLabel: String = \"Loading\", emptyLabel: String = \"No cards\",\n invalidLabel: String = \"Invalid column data\",\n formatCount: @escaping (Int, Int?) -> String = { count, capacity in capacity.map { \"\\(count) of \\($0) cards\" } ?? \"\\(count) cards\" },\n formatMove: @escaping (String, Int) -> String = { \"Move \\($0) to position \\($1)\" },\n formatOpen: @escaping (String) -> String = { \"Open \\($0)\" },\n @ViewBuilder cardContent: @escaping (LumenKanbanCard) -> CardContent) {\n _column = column; self.readOnly = readOnly; self.loading = loading; self.error = error\n self.onAdd = onAdd; self.onCardPress = onCardPress; self.addLabel = addLabel\n self.loadingLabel = loadingLabel; self.emptyLabel = emptyLabel; self.invalidLabel = invalidLabel\n self.formatCount = formatCount; self.formatMove = formatMove; self.formatOpen = formatOpen\n self.cardContent = cardContent\n }\n private var interactive: Bool { enabled && !readOnly && !column.disabled && !loading && error == nil }\n private var model: LumenKanbanModel { LumenKanbanModel(columns: [column]) }\n private func move(_ id: String, index: Int) -> Bool {\n guard interactive, let next = model.moving(id, toColumnId: column.id, toIndex: index)?.first else { return false }\n column = next\n return true\n }\n public var body: some View {\n VStack(alignment: .leading, spacing: LumenSpacing.md) {\n LumenText(.verbatim(column.label)).accessibilityAddTraits(.isHeader)\n if loading { LumenText(.verbatim(loadingLabel)) }\n else if let error { LumenText(.verbatim(error)) }\n else if !model.valid { LumenText(.verbatim(invalidLabel)) }\n else {\n LumenText(.verbatim(formatCount(column.cards.count, column.capacity)))\n if column.cards.isEmpty { LumenText(.verbatim(emptyLabel)) }\n ForEach(Array(column.cards.enumerated()), id: \\.element.id) { index, card in\n VStack(alignment: .leading, spacing: LumenSpacing.xs) {\n if interactive && !card.disabled {\n LumenCard(padding: .md, radius: .md) { cardContent(card) }.draggable(card.id)\n } else { LumenCard(padding: .md, radius: .md) { cardContent(card) } }\n if let onCardPress {\n LumenButton(disabled: column.disabled || card.disabled, action: {\n if enabled && !column.disabled && !card.disabled { onCardPress(card.id) }\n }) { Text(formatOpen(card.label)) }\n }\n ForEach([index - 1, index + 1].filter { column.cards.indices.contains($0) }, id: \\.self) { position in\n LumenButton(disabled: !interactive || model.moving(card.id, toColumnId: column.id, toIndex: position) == nil,\n action: { _ = move(card.id, index: position) }) {\n Text(formatMove(card.label, position + 1))\n }\n }\n }.dropDestination(for: String.self) { ids, _ in\n guard ids.count == 1, let id = ids.first else { return false }\n let current = column.cards.firstIndex(where: { $0.id == id })\n return move(id, index: current.map { $0 < index ? index - 1 : index } ?? index)\n }\n }\n if let onAdd {\n LumenButton(disabled: !interactive || column.capacity.map { column.cards.count >= $0 } == true,\n action: { if interactive && column.capacity.map({ column.cards.count < $0 }) != false { onAdd() } }) {\n Text(addLabel)\n }\n }\n }\n }.frame(minHeight: 88, alignment: .topLeading)\n .accessibilityElement(children: .contain).accessibilityLabel(Text(column.label))\n .dropDestination(for: String.self) { ids, _ in\n guard ids.count == 1, let id = ids.first else { return false }\n return move(id, index: column.cards.filter { $0.id != id }.count)\n }\n }\n}\npublic extension LumenKanbanColumn where CardContent == Text {\n init(column: Binding<LumenKanbanColumnData>, readOnly: Bool = false, loading: Bool = false,\n error: String? = nil, onAdd: (() -> Void)? = nil, onCardPress: ((String) -> Void)? = nil,\n addLabel: String = \"Add card\", loadingLabel: String = \"Loading\", emptyLabel: String = \"No cards\",\n invalidLabel: String = \"Invalid column data\",\n formatCount: @escaping (Int, Int?) -> String = { count, capacity in capacity.map { \"\\(count) of \\($0) cards\" } ?? \"\\(count) cards\" },\n formatMove: @escaping (String, Int) -> String = { \"Move \\($0) to position \\($1)\" },\n formatOpen: @escaping (String) -> String = { \"Open \\($0)\" }) {\n self.init(column: column, readOnly: readOnly, loading: loading, error: error, onAdd: onAdd,\n onCardPress: onCardPress, addLabel: addLabel, loadingLabel: loadingLabel,\n emptyLabel: emptyLabel, invalidLabel: invalidLabel, formatCount: formatCount,\n formatMove: formatMove, formatOpen: formatOpen, cardContent: { Text($0.label) })\n }\n}\n#endif\n",
60349
60568
  "packages/swift/Sources/LumenUI/KanbanModels.swift": "import Foundation\n\npublic struct LumenKanbanCard: Identifiable, Equatable, Sendable {\n public let id: String\n public var label: String\n public var disabled: Bool\n public init(id: String, label: String, disabled: Bool = false) {\n self.id = id; self.label = label; self.disabled = disabled\n }\n}\npublic struct LumenKanbanColumnData: Identifiable, Equatable, Sendable {\n public let id: String\n public var label: String\n public var cards: [LumenKanbanCard]\n public var capacity: Int?\n public var disabled: Bool\n public init(id: String, label: String, cards: [LumenKanbanCard], capacity: Int? = nil, disabled: Bool = false) {\n self.id = id; self.label = label; self.cards = cards; self.capacity = capacity; self.disabled = disabled\n }\n}\n/// Destination indices are measured after removing the moving card.\npublic struct LumenKanbanModel: Sendable {\n public let columns: [LumenKanbanColumnData]\n public let valid: Bool\n public init(columns: [LumenKanbanColumnData]) {\n self.columns = columns\n var columnIds: Set<String> = []; var cardIds: Set<String> = []\n valid = columns.allSatisfy { column in\n guard !column.id.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,\n columnIds.insert(column.id).inserted,\n column.capacity.map({ $0 >= column.cards.count }) ?? true else { return false }\n return column.cards.allSatisfy { card in\n !card.id.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && cardIds.insert(card.id).inserted\n }\n }\n }\n public func moving(_ cardId: String, toColumnId: String, toIndex: Int) -> [LumenKanbanColumnData]? {\n guard valid, let source = columns.first(where: { $0.cards.contains(where: { $0.id == cardId }) }),\n let target = columns.first(where: { $0.id == toColumnId }),\n let card = source.cards.first(where: { $0.id == cardId }), !source.disabled, !target.disabled, !card.disabled else { return nil }\n var destination = target.cards.filter { $0.id != cardId }\n guard toIndex >= 0, toIndex <= destination.count,\n target.capacity.map({ destination.count < $0 }) ?? true else { return nil }\n if source.id == target.id && source.cards.firstIndex(where: { $0.id == cardId }) == toIndex { return nil }\n destination.insert(card, at: toIndex)\n return columns.map { column in\n var next = column\n if column.id == target.id { next.cards = destination }\n else if column.id == source.id { next.cards.removeAll { $0.id == cardId } }\n return next\n }\n }\n}\n",
60350
- "packages/swift/Sources/LumenUI/LumenTheme.swift": "import SwiftUI\n\npublic enum LumenColorScheme: Sendable {\n case dark\n case light\n}\n\npublic enum LumenSurfaceMaterial: Sendable {\n case solid\n case glass\n}\n\npublic struct LumenAppearance: Sendable {\n public let radiusScale: CGFloat\n public let spacingScale: CGFloat\n public let borderWidth: CGFloat\n public let elevationScale: CGFloat\n public let material: LumenSurfaceMaterial\n\n public init(\n radiusScale: CGFloat = 1, spacingScale: CGFloat = 1,\n borderWidth: CGFloat = 1, elevationScale: CGFloat = 1,\n material: LumenSurfaceMaterial = .solid\n ) {\n precondition([radiusScale, spacingScale, borderWidth, elevationScale].allSatisfy { $0.isFinite && $0 >= 0 })\n self.radiusScale = radiusScale\n self.spacingScale = spacingScale\n self.borderWidth = borderWidth\n self.elevationScale = elevationScale\n self.material = material\n }\n}\n\npublic struct LumenTextStyles: Sendable {\n public let body: Font\n public let caption: Font\n public let label: Font\n public let title: Font\n\n public init(\n body: Font = .body, caption: Font = .caption,\n label: Font = .callout.weight(.semibold), title: Font = .title2.weight(.bold)\n ) {\n self.body = body\n self.caption = caption\n self.label = label\n self.title = title\n }\n}\n\npublic struct LumenTheme: Sendable {\n public let colors: LumenColorPalette\n public let scheme: LumenColorScheme\n public let appearance: LumenAppearance\n public let textStyles: LumenTextStyles\n\n public init(\n colors: LumenColorPalette, scheme: LumenColorScheme,\n appearance: LumenAppearance = LumenAppearance(),\n textStyles: LumenTextStyles = LumenTextStyles()\n ) {\n self.colors = colors\n self.scheme = scheme\n self.appearance = appearance\n self.textStyles = textStyles\n }\n\n public init(\n preset: LumenThemePreset, scheme: LumenColorScheme,\n appearance: LumenAppearance? = nil,\n textStyles: LumenTextStyles = LumenTextStyles()\n ) {\n self.init(colors: preset.colors(for: scheme), scheme: scheme,\n appearance: appearance ?? preset.appearance, textStyles: textStyles)\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\npublic extension LumenColorPalette {\n func overriding(\n canvas: Color? = nil,\n surface: Color? = nil,\n surfaceMuted: Color? = nil,\n surfaceStrong: Color? = nil,\n line: Color? = nil,\n ink: Color? = nil,\n inkSoft: Color? = nil,\n inkMuted: Color? = nil,\n brand: Color? = nil,\n brandSolid: Color? = nil,\n brandSoft: Color? = nil,\n onBrand: Color? = nil,\n accent: Color? = nil,\n success: Color? = nil,\n warning: Color? = nil,\n danger: Color? = nil,\n onDanger: Color? = nil\n ) -> LumenColorPalette {\n LumenColorPalette(\n canvas: canvas ?? self.canvas,\n surface: surface ?? self.surface,\n surfaceMuted: surfaceMuted ?? self.surfaceMuted,\n surfaceStrong: surfaceStrong ?? self.surfaceStrong,\n line: line ?? self.line,\n ink: ink ?? self.ink,\n inkSoft: inkSoft ?? self.inkSoft,\n inkMuted: inkMuted ?? self.inkMuted,\n brand: brand ?? self.brand,\n brandSolid: brandSolid ?? self.brandSolid,\n brandSoft: brandSoft ?? self.brandSoft,\n onBrand: onBrand ?? self.onBrand,\n accent: accent ?? self.accent,\n success: success ?? self.success,\n warning: warning ?? self.warning,\n danger: danger ?? self.danger,\n onDanger: onDanger ?? self.onDanger\n )\n }\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(\n _ theme: LumenTheme,\n enforceColorScheme: Bool = true\n ) -> some View {\n lumenTheme(theme, enforceColorScheme: enforceColorScheme, applyTint: true)\n }\n\n @ViewBuilder\n func lumenTheme(\n _ theme: LumenTheme,\n enforceColorScheme: Bool = true,\n applyTint: Bool\n ) -> some View {\n if applyTint {\n environment(\\.lumenTheme, theme)\n .tint(theme.colors.brandSolid)\n .preferredColorScheme(theme.resolvedPreferredColorScheme(enforceColorScheme: enforceColorScheme))\n } else {\n environment(\\.lumenTheme, theme)\n .preferredColorScheme(theme.resolvedPreferredColorScheme(enforceColorScheme: enforceColorScheme))\n }\n }\n}\n",
60569
+ "packages/swift/Sources/LumenUI/LumenTheme.swift": "import SwiftUI\n\npublic enum LumenColorScheme: Sendable {\n case dark\n case light\n}\n\npublic enum LumenSurfaceMaterial: Sendable {\n case solid\n case glass\n}\n\npublic struct LumenAppearance: Sendable {\n public let radiusScale: CGFloat\n public let spacingScale: CGFloat\n public let borderWidth: CGFloat\n public let elevationScale: CGFloat\n public let material: LumenSurfaceMaterial\n\n public init(\n radiusScale: CGFloat = 1, spacingScale: CGFloat = 1,\n borderWidth: CGFloat = 1, elevationScale: CGFloat = 1,\n material: LumenSurfaceMaterial = .solid\n ) {\n precondition([radiusScale, spacingScale, borderWidth, elevationScale].allSatisfy { $0.isFinite && $0 >= 0 })\n self.radiusScale = radiusScale\n self.spacingScale = spacingScale\n self.borderWidth = borderWidth\n self.elevationScale = elevationScale\n self.material = material\n }\n}\n\npublic struct LumenTextStyles: Sendable {\n public let body: Font\n public let caption: Font\n public let label: Font\n public let title: Font\n\n public init(\n body: Font = .body, caption: Font = .caption,\n label: Font = .callout.weight(.semibold), title: Font = .title2.weight(.bold)\n ) {\n self.body = body\n self.caption = caption\n self.label = label\n self.title = title\n }\n}\n\npublic struct LumenTheme: Sendable {\n public let colors: LumenColorPalette\n public let scheme: LumenColorScheme\n public let appearance: LumenAppearance\n public let textStyles: LumenTextStyles\n\n public init(\n colors: LumenColorPalette, scheme: LumenColorScheme,\n appearance: LumenAppearance = LumenAppearance(),\n textStyles: LumenTextStyles = LumenTextStyles()\n ) {\n self.colors = colors\n self.scheme = scheme\n self.appearance = appearance\n self.textStyles = textStyles\n }\n\n public init(\n preset: LumenThemePreset, scheme: LumenColorScheme,\n appearance: LumenAppearance? = nil,\n textStyles: LumenTextStyles = LumenTextStyles()\n ) {\n self.init(colors: preset.colors(for: scheme), scheme: scheme,\n appearance: appearance ?? preset.appearance, textStyles: textStyles)\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\npublic extension LumenColorPalette {\n func overriding(\n canvas: Color? = nil,\n surface: Color? = nil,\n surfaceMuted: Color? = nil,\n surfaceStrong: Color? = nil,\n line: Color? = nil,\n ink: Color? = nil,\n inkSoft: Color? = nil,\n inkMuted: Color? = nil,\n brand: Color? = nil,\n brandSolid: Color? = nil,\n brandSoft: Color? = nil,\n onBrand: Color? = nil,\n accent: Color? = nil,\n success: Color? = nil,\n warning: Color? = nil,\n danger: Color? = nil,\n onDanger: Color? = nil\n ) -> LumenColorPalette {\n LumenColorPalette(\n canvas: canvas ?? self.canvas,\n surface: surface ?? self.surface,\n surfaceMuted: surfaceMuted ?? self.surfaceMuted,\n surfaceStrong: surfaceStrong ?? self.surfaceStrong,\n line: line ?? self.line,\n ink: ink ?? self.ink,\n inkSoft: inkSoft ?? self.inkSoft,\n inkMuted: inkMuted ?? self.inkMuted,\n brand: brand ?? self.brand,\n brandSolid: brandSolid ?? self.brandSolid,\n brandSoft: brandSoft ?? self.brandSoft,\n onBrand: onBrand ?? self.onBrand,\n accent: accent ?? self.accent,\n success: success ?? self.success,\n warning: warning ?? self.warning,\n danger: danger ?? self.danger,\n onDanger: onDanger ?? self.onDanger\n )\n }\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(\n _ theme: LumenTheme,\n enforceColorScheme: Bool = true\n ) -> some View {\n lumenTheme(theme, enforceColorScheme: enforceColorScheme, applyTint: true)\n }\n\n @ViewBuilder\n func lumenTheme(\n _ theme: LumenTheme,\n enforceColorScheme: Bool = true,\n applyTint: Bool\n ) -> some View {\n let preferredScheme = theme.resolvedPreferredColorScheme(enforceColorScheme: enforceColorScheme)\n // Emit the optional window preference from a background child so changing enforcement\n // does not move application content between conditional branches and reset its state.\n let themed = environment(\\.lumenTheme, theme).background {\n if let preferredScheme {\n Color.clear.preferredColorScheme(preferredScheme)\n }\n }\n\n if applyTint {\n themed.tint(theme.colors.brandSolid)\n } else {\n themed\n }\n }\n}\n",
60351
60570
  "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 private let tint: Color?\n private let onSelection: ((String) -> Void)?\n\n public init(\n _ title: LocalizedStringKey = \"Choose a symbol\",\n selectedName: Binding<String>,\n options: [LumenSymbolOption] = LumenSymbolOption.common,\n tint: Color? = nil,\n onSelection: ((String) -> Void)? = nil\n ) {\n self.title = title\n _selectedName = selectedName\n self.options = options\n self.tint = tint\n self.onSelection = onSelection\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 func select(_ name: String) {\n selectedName = name\n onSelection?(name)\n }\n\n private func symbolButton(_ option: LumenSymbolOption) -> some View {\n let isSelected = selectedName == option.name\n\n return Button {\n select(option.name)\n } label: {\n VStack(spacing: LumenSpacing.xs) {\n LumenIcon(\n systemName: option.name,\n size: .md,\n color: isSelected ? (tint ?? 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 ? (tint ?? 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 private let tint: Color?\n private let dismissOnSelection: Bool\n private let onSelection: ((String) -> Void)?\n\n public init(\n _ label: LocalizedStringKey = \"Choose a symbol\",\n selectedName: Binding<String>,\n options: [LumenSymbolOption] = LumenSymbolOption.common,\n tint: Color? = nil,\n dismissOnSelection: Bool = true,\n onSelection: ((String) -> Void)? = nil\n ) {\n self.label = label\n _selectedName = selectedName\n self.options = options\n self.tint = tint\n self.dismissOnSelection = dismissOnSelection\n self.onSelection = onSelection\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: tint ?? 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, tint: tint) { name in\n if dismissOnSelection { isPresented = false }\n onSelection?(name)\n }\n .padding(LumenSpacing.lg)\n .frame(width: 400, height: 480)\n .lumenTheme(theme)\n }\n }\n}\n#endif\n",
60352
60571
  "packages/swift/Sources/LumenUI/MediaComponents.swift": "import SwiftUI\n\npublic enum LumenImageFit: Sendable {\n case contain\n case cover\n\n var contentMode: ContentMode {\n switch self {\n case .contain: .fit\n case .cover: .fill\n }\n }\n}\n\npublic enum LumenImageRadius: Sendable {\n case full\n case lg\n case md\n case none\n case sm\n\n var value: CGFloat {\n switch self {\n case .full: LumenRadius.full\n case .lg: LumenRadius.lg\n case .md: LumenRadius.md\n case .none: 0\n case .sm: LumenRadius.sm\n }\n }\n}\n\npublic struct LumenImage<Content: View>: View {\n private let aspectRatio: CGFloat?\n private let content: Content\n private let fit: LumenImageFit\n private let label: String?\n private let radius: LumenImageRadius\n\n public init(\n aspectRatio: CGFloat? = nil,\n fit: LumenImageFit = .cover,\n radius: LumenImageRadius = .lg,\n label: String? = nil,\n @ViewBuilder content: () -> Content\n ) {\n if let aspectRatio, aspectRatio.isFinite, aspectRatio > 0 {\n self.aspectRatio = aspectRatio\n } else {\n self.aspectRatio = nil\n }\n\n self.content = content()\n self.fit = fit\n self.label = label\n self.radius = radius\n }\n\n public var body: some View {\n content\n .aspectRatio(aspectRatio, contentMode: fit.contentMode)\n .clipShape(RoundedRectangle(cornerRadius: radius.value, style: .continuous))\n .accessibilityElement(children: .ignore)\n .accessibilityHidden(label == nil)\n .accessibilityLabel(Text(label ?? \"\"))\n }\n}\n",
60353
60572
  "packages/swift/Sources/LumenUI/MediaWorkspaceComponents.swift": "import SwiftUI\n\npublic struct LumenMediaViewportValue: Equatable, Sendable {\n public var zoom: Double\n public var x: Double\n public var y: Double\n\n public init(zoom: Double = 1, x: Double = 0, y: Double = 0) {\n self.zoom = zoom\n self.x = x\n self.y = y\n }\n\n public func normalized(maxZoom: Double = 4) -> Self {\n let limit = maxZoom.isFinite ? min(16, max(1, maxZoom)) : 4\n let scale = zoom.isFinite ? min(limit, max(1, zoom)) : 1\n return Self(zoom: scale,\n x: scale == 1 ? 0 : (x.isFinite ? min(1, max(-1, x)) : 0),\n y: scale == 1 ? 0 : (y.isFinite ? min(1, max(-1, y)) : 0))\n }\n\n public func applying(_ action: LumenMediaViewportAction, maxZoom: Double = 4) -> Self {\n var next = normalized(maxZoom: maxZoom)\n switch action {\n case .zoomIn: next.zoom += 0.25\n case .zoomOut: next.zoom -= 0.25\n case .fit: next = Self()\n case .left: next.x -= 0.25\n case .right: next.x += 0.25\n case .up: next.y -= 0.25\n case .down: next.y += 0.25\n }\n return next.normalized(maxZoom: maxZoom)\n }\n\n public func panning(dx: Double, dy: Double, width: Double, height: Double, maxZoom: Double = 4) -> Self {\n let current = normalized(maxZoom: maxZoom)\n let extent = (current.zoom - 1) / 2\n guard extent > 0, width.isFinite, height.isFinite, width > 0, height > 0 else { return current }\n return Self(zoom: current.zoom,\n x: current.x + (dx.isFinite ? dx / width / extent : 0),\n y: current.y + (dy.isFinite ? dy / height / extent : 0)).normalized(maxZoom: maxZoom)\n }\n}\n\npublic enum LumenMediaViewportAction: String, CaseIterable, Sendable {\n case zoomIn, zoomOut, fit, left, right, up, down\n}\n\npublic struct LumenMediaViewportLabels: Sendable {\n public var zoomIn: String\n public var zoomOut: String\n public var fit: String\n public var left: String\n public var right: String\n public var up: String\n public var down: String\n\n public init(zoomIn: String = \"Zoom in\", zoomOut: String = \"Zoom out\", fit: String = \"Fit to view\",\n left: String = \"Pan left\", right: String = \"Pan right\", up: String = \"Pan up\", down: String = \"Pan down\") {\n self.zoomIn = zoomIn\n self.zoomOut = zoomOut\n self.fit = fit\n self.left = left\n self.right = right\n self.up = up\n self.down = down\n }\n\n public func label(for action: LumenMediaViewportAction) -> String {\n switch action {\n case .zoomIn: zoomIn\n case .zoomOut: zoomOut\n case .fit: fit\n case .left: left\n case .right: right\n case .up: up\n case .down: down\n }\n }\n}\n\n#if os(iOS) || os(macOS) || os(visionOS)\n/// The application owns media loading and the viewport binding. Zoom is relative to fit.\npublic struct LumenMediaViewport<Content: View>: View {\n @Binding private var value: LumenMediaViewportValue\n @Environment(\\.lumenTheme) private var theme\n @Environment(\\.locale) private var locale\n @State private var dragStart: LumenMediaViewportValue?\n @State private var pinchStart: LumenMediaViewportValue?\n private let label: String\n private let maxZoom: Double\n private let ratio: CGFloat\n private let disabled: Bool\n private let labels: LumenMediaViewportLabels\n private let content: Content\n\n public init(_ label: String, value: Binding<LumenMediaViewportValue>, maxZoom: Double = 4,\n aspectRatio: CGFloat = 16 / 9, disabled: Bool = false,\n labels: LumenMediaViewportLabels = .init(), @ViewBuilder content: () -> Content) {\n self.label = label\n _value = value\n self.maxZoom = maxZoom\n ratio = aspectRatio.isFinite && (0.1...10).contains(aspectRatio) ? aspectRatio : 16 / 9\n self.disabled = disabled\n self.labels = labels\n self.content = content()\n }\n\n private var current: LumenMediaViewportValue { value.normalized(maxZoom: maxZoom) }\n private var zoomLabel: String { current.zoom.formatted(.percent.locale(locale).precision(.fractionLength(0))) }\n\n public var body: some View {\n VStack(alignment: .leading, spacing: LumenSpacing.sm) {\n GeometryReader { geometry in\n content\n .frame(width: geometry.size.width, height: geometry.size.height)\n .scaleEffect(current.zoom)\n .offset(x: current.x * (current.zoom - 1) * geometry.size.width / 2,\n y: current.y * (current.zoom - 1) * geometry.size.height / 2)\n .frame(width: geometry.size.width, height: geometry.size.height)\n .clipped()\n .contentShape(Rectangle())\n .gesture(DragGesture(minimumDistance: 2).onChanged { drag in\n guard !disabled, current.zoom > 1, pinchStart == nil else { return }\n let start = dragStart ?? current\n dragStart = start\n value = start.panning(dx: drag.translation.width, dy: drag.translation.height,\n width: geometry.size.width, height: geometry.size.height, maxZoom: maxZoom)\n }.onEnded { _ in dragStart = nil }, including: disabled || current.zoom <= 1 ? .none : .all)\n .simultaneousGesture(MagnificationGesture().onChanged { scale in\n guard !disabled else { return }\n let start = pinchStart ?? current\n pinchStart = start\n dragStart = nil\n value = LumenMediaViewportValue(zoom: start.zoom * scale, x: start.x, y: start.y).normalized(maxZoom: maxZoom)\n }.onEnded { _ in pinchStart = nil }, including: disabled ? .none : .all)\n }\n .aspectRatio(ratio, contentMode: .fit)\n .background(theme.colors.surfaceMuted)\n .clipShape(RoundedRectangle(cornerRadius: LumenRadius.lg))\n .accessibilityLabel(Text(verbatim: label))\n .accessibilityValue(Text(verbatim: zoomLabel))\n Text(verbatim: label)\n Text(verbatim: zoomLabel).monospacedDigit()\n LazyVGrid(columns: [GridItem(.adaptive(minimum: 120))], alignment: .leading, spacing: LumenSpacing.sm) {\n ForEach(LumenMediaViewportAction.allCases, id: \\.self) { action in\n LumenButton(intent: .secondary,\n disabled: disabled || current.applying(action, maxZoom: maxZoom) == current,\n action: { value = current.applying(action, maxZoom: maxZoom) }) {\n Text(verbatim: labels.label(for: action))\n }\n }\n }\n }\n }\n}\n#endif\n\npublic enum LumenMediaThumbnailState: Sendable { case ready, loading, error }\n\nfunc resolveMediaThumbnailStateLabel(_ state: LumenMediaThumbnailState, label: String) -> String {\n if !label.isEmpty { return label }\n return state == .loading ? \"Loading\" : \"Unavailable\"\n}\n\npublic struct LumenMediaThumbnail<Content: View>: View {\n @Environment(\\.lumenTheme) private var theme\n private let label: String\n private let selected: Bool\n private let order: Int?\n private let state: LumenMediaThumbnailState\n private let stateLabel: String\n private let disabled: Bool\n private let onSelectionChange: (Bool) -> Void\n private let content: Content\n\n public init(_ label: String, selected: Bool, order: Int? = nil,\n state: LumenMediaThumbnailState = .ready, stateLabel: String = \"\",\n disabled: Bool = false, onSelectionChange: @escaping (Bool) -> Void,\n @ViewBuilder content: () -> Content) {\n self.label = label\n self.selected = selected\n self.order = order.flatMap { $0 > 0 ? $0 : nil }\n self.state = state\n self.stateLabel = resolveMediaThumbnailStateLabel(state, label: stateLabel)\n self.disabled = disabled\n self.onSelectionChange = onSelectionChange\n self.content = content()\n }\n\n public var body: some View {\n LumenButton(intent: .secondary, disabled: disabled || state != .ready,\n action: { onSelectionChange(!selected) }) {\n VStack(alignment: .leading, spacing: LumenSpacing.xs) {\n content.aspectRatio(1, contentMode: .fit).clipped().accessibilityHidden(true)\n HStack(alignment: .top) {\n Text(verbatim: label)\n if selected { LumenIcon(systemName: \"checkmark\", label: nil) }\n if let order { Text(verbatim: String(order)).monospacedDigit() }\n }\n if state != .ready { Text(verbatim: stateLabel) }\n }\n .padding(LumenSpacing.xs)\n }\n .overlay {\n RoundedRectangle(cornerRadius: LumenRadius.sm)\n .stroke(selected ? theme.colors.brand : theme.colors.line, lineWidth: selected ? 2 : 1)\n }\n .accessibilityLabel(Text(verbatim: label))\n .accessibilityValue(Text(verbatim: state == .ready ? order.map(String.init) ?? \"\" : stateLabel))\n .accessibilityAddTraits(selected ? .isSelected : [])\n }\n}\n\npublic struct LumenMediaFilmstrip<Content: View>: View {\n private let label: String\n private let selectionLabel: String\n private let content: Content\n\n public init(_ label: String, selectionLabel: String, @ViewBuilder content: () -> Content) {\n self.label = label\n self.selectionLabel = selectionLabel\n self.content = content()\n }\n\n public var body: some View {\n VStack(alignment: .leading, spacing: LumenSpacing.sm) {\n LumenText(.verbatim(label), variant: .label)\n LumenText(.verbatim(selectionLabel), variant: .caption)\n ScrollView(.horizontal) {\n HStack(alignment: .top, spacing: LumenSpacing.sm) { content }\n .padding(LumenSpacing.xs)\n }\n }\n }\n}\n",
@@ -61369,17 +61588,17 @@
61369
61588
  "peerDependencies": {
61370
61589
  "astro": ">=5"
61371
61590
  },
61372
- "version": "4.0.0"
61591
+ "version": "4.1.0"
61373
61592
  },
61374
61593
  "@santi020k/lumen-core": {
61375
61594
  "peerDependencies": {
61376
61595
  "@rive-app/canvas": "^2.44.0"
61377
61596
  },
61378
- "version": "4.0.0"
61597
+ "version": "4.1.0"
61379
61598
  },
61380
61599
  "@santi020k/lumen-elements": {
61381
61600
  "peerDependencies": {},
61382
- "version": "4.0.0"
61601
+ "version": "4.2.0"
61383
61602
  },
61384
61603
  "@santi020k/lumen-icons-brand": {
61385
61604
  "peerDependencies": {},
@@ -61389,18 +61608,18 @@
61389
61608
  "peerDependencies": {
61390
61609
  "react": ">=19"
61391
61610
  },
61392
- "version": "4.0.0"
61611
+ "version": "4.2.0"
61393
61612
  },
61394
61613
  "@santi020k/lumen-mcp": {
61395
61614
  "peerDependencies": {},
61396
- "version": "4.0.0"
61615
+ "version": "4.2.0"
61397
61616
  },
61398
61617
  "@santi020k/lumen-react": {
61399
61618
  "peerDependencies": {
61400
61619
  "react": ">=19",
61401
61620
  "motion": "^14.0.0"
61402
61621
  },
61403
- "version": "4.0.0"
61622
+ "version": "4.1.0"
61404
61623
  },
61405
61624
  "@santi020k/lumen-react-hook-form": {
61406
61625
  "peerDependencies": {
@@ -61408,7 +61627,7 @@
61408
61627
  "react": ">=19",
61409
61628
  "react-hook-form": ">=7.76.0 <8"
61410
61629
  },
61411
- "version": "4.0.0"
61630
+ "version": "4.0.1"
61412
61631
  },
61413
61632
  "@santi020k/lumen-react-native": {
61414
61633
  "peerDependencies": {
@@ -61417,7 +61636,7 @@
61417
61636
  "react-native": ">=0.86.2",
61418
61637
  "react-native-svg": ">=12 <16"
61419
61638
  },
61420
- "version": "4.0.0"
61639
+ "version": "4.0.1"
61421
61640
  },
61422
61641
  "@santi020k/lumen-tokens": {
61423
61642
  "peerDependencies": {},
@@ -61428,9 +61647,9 @@
61428
61647
  "swift": {
61429
61648
  "package": "https://github.com/santi020k/lumen.git",
61430
61649
  "revision": null,
61431
- "tag": "v4.0.0"
61650
+ "tag": "v4.2.0"
61432
61651
  },
61433
- "version": "4.0.0"
61652
+ "version": "4.2.0"
61434
61653
  },
61435
61654
  "migration": {
61436
61655
  "codemod": null,
@@ -61448,7 +61667,7 @@
61448
61667
  }
61449
61668
  }
61450
61669
  },
61451
- "rules": "# Lumen UI\n\nLumen is a multi-framework primitive UI system with Astro as the reference implementation.\n\nUse these files first when generating, reviewing, or migrating apps that use Lumen:\n\n- README.md: public overview and package strategy.\n- docs/ai-usage.md: AI-focused setup, package selection, and component selection.\n- https://lumen.santi020k.com/guides/build-ui-with-ai: tested Astro and React adoption example with a reusable prompt.\n- docs/ai-efficiency.md: reproducible AI token measurement, quality checks, and limits on efficiency claims.\n- skills/lumen-ui/SKILL.md: portable Agent Skill for building, reviewing, and theming Lumen interfaces.\n- docs/feature-roadmap.md: competitor-informed gaps and implementation priorities.\n- registry/lumen.registry.json: installable component, recipe, and documentation manifest.\n- packages/core/src/components.ts: authoritative shared component catalog.\n- packages/astro/components: reference component markup and prop shapes.\n- packages/astro/styles/lumen.css: standalone CSS contract for all frameworks.\n- @santi020k/lumen-mcp: Model Context Protocol server exposing snapshot provenance, structured component discovery, framework-aware usage/source, recipes, tolerant search, tokens, rules, and lumen:// resources for AI agents.\n\nInstall the portable Lumen Agent Skill:\n\n```bash\nnpx skills add santi020k/lumen --skill lumen-ui\n```\n\nDefault usage:\n\n```astro\n---\nimport { Button, Card, Input, Label } from '@santi020k/lumen-astro'\nimport UIPrimitives from '@santi020k/lumen-astro/runtime'\nimport '@santi020k/lumen-astro/styles.css'\n---\n\n<UIPrimitives />\n<Card>\n <Label for=\"email\">Email</Label>\n <Input id=\"email\" type=\"email\" />\n <Button>Subscribe</Button>\n</Card>\n```\n\nRules for agents:\n\n- When Lumen MCP is connected, read `lumen://meta`, `lumen://diagnostics`, and `lumen://rules`, pass the target framework to `lumen_search`, and follow the framework behavior returned by `lumen_get_component`. Retain `lumen://catalog-manifest` and use `lumen_diff_catalog` after upgrades when the client supports cached context.\n- Prefer Astro unless the user explicitly targets React or Web Components.\n- In React, use the visual components with behavior hooks for Dialog, Popover, DropdownMenu, Tabs, Select, Toast, and Tooltip instead of mounting the Astro runtime.\n- In Web Components, registered `lumen-*` elements include behavior parity for Dialog, Popover, DropdownMenu, Tabs, Select, Toast, and Tooltip, including ARIA state, keyboard paths, Escape/outside dismissal, focus handling, native select form participation, and the shared toast controller.\n- For React report filters, use `DateRangeInput` (`value`, `onValueChange`, localized `labels`, bounds and presets) or inline `DateRangeCalendar`. These React-specific compositions are documented in packages/react/README.md and the reporting example at /docs/web/reporting.\n- React record tables can opt into DataTableView from @santi020k/lumen-react/components/data-table-view for search, filters, visibility, density, pagination and controlled sorting. Read docs/data-table-views.md; applications retain cells, data fetching and domain actions.\n- Keep server-paginated DataTable rows in server order with `sortMode=\"manual\"`, controlled `sort` and `onSortChange`. Apply sorting before pagination in the application backend. Use Dialog dismissal policies for pending workflows.\n- Chart X identities must be unique. Use `xLabel` for abbreviated axes, `formatCategory` for full details, and explicit null gaps for missing observations. Read docs/data-visualization.md.\n- Use ImageComparison for before/after media with matching dimensions, alternative text and a visible localized control label. See docs/image-comparison.md for adapter contracts.\n- Read docs/migrating-to-lumen.md before upgrading to v4, especially date-trigger IDs, loading buttons, hidden semantics, copy feedback and native initializer changes.\n- Do not require Tailwind configuration for Lumen components.\n- Give each outer edge one border and radius owner. Flush images inside rounded frames use Image radius=\"none\"; Image defaults to radius=\"lg\". Use one divider at shared edges, account for padding on inset curves, and check phone/desktop corners and keyboard focus in both themes. Keep artwork clipping scoped so it does not cut off focus rings or overlays. See docs/ai-usage.md for styling rules.\n- Use public tokens: canvas, surface, surface-muted, surface-strong, line, ink, ink-soft, ink-muted, brand, brand-solid, brand-soft, on-brand, accent, success, warning, danger, on-danger.\n- Mount `UIPrimitives` once for interactive Astro primitives.\n- Use `glass` for glass surfaces. The obsolete overlay `surface=\"glass\"` alias is removed.\n- Treat schedule, data collection, rich text, theme builder, and advanced fields as supported surfaces with progressively enhanced behavior.\n- Use `lumen list`, `lumen show <component>`, and `lumen add <component>` for individually addressable registry components; component names also resolve from kebab-case aliases such as `data-table`.\n- Runtime CustomEvents used by `UIPrimitives`: `ui:data-table-selection-change` from `[data-ui-datatable]` with `{ values }`; `ui:schedule-change` from `[data-ui-schedule]` with `{ eventId, slot }`; `ui:virtual-list-range` from `[data-ui-virtual-list]` with `{ startIndex, endIndex }`; `ui:tag-remove` from tag groups with `{ value }`; `ui:editor-command` from rich text editors with `{ command }`; `ui:theme-change` from theme builders with `{ hue, tokens }`; `ui:theme-export` from theme builders with `{ css }`; `ui:validate`, `ui:invalid`, and `ui:valid` from `[data-ui-form]` for native Constraint Validation API enhancement; document-level `ui:toast`, `ui:toast-update`, and `ui:toast-dismiss` for runtime toasts; `ui:toast-action` from `[data-ui-toast]` when an action button is pressed.\n- Add `data-ui-form` to forms that should reflect `required`, `pattern`, `min`, `max`, `type`, and custom `setCustomValidity()` state into Field error slots on blur and submit. Use `data-error-required`, `data-error-pattern`, `data-error-min`, `data-error-max`, or `data-error-custom` to override browser validation messages.\n- Toasts use `ToastViewport` for explicit `placement` and `maxCount` configuration. With `UIPrimitives` or registered Web Components, use `window.LumenToast.create(detail)`, `.update(id, detail)`, and `.dismiss(id?)`, import `LumenToast` from `@santi020k/lumen-elements`, or dispatch the matching document events. Runtime toast `detail` supports `title`, `description`, `variant`, `duration`, `placement`, `max`, and `action`.\n\n## Content flow\n\n- Reuse content-flow-header, content-flow-settings, content-flow-list and content-flow-actions recipes; lumen_get_recipe returns complete framework source.\n- Container gutters grow from group to section spacing. Override --ui-container-gutter at the product boundary instead of adding nested page padding.\n- Prose and Typography trim their outer child margins and separate headings from preceding text. Card allows interactive overflow; AspectRatio owns media clipping.\n\n- Read docs/content-flow.md. Stack/Grid own sibling gaps; surfaces own padding; Field owns control feedback spacing. Do not add child margins for a relationship already controlled by a parent gap.\n- Use Stack/Grid gap=\"related\" (8px), \"group\" (16px, default), or \"section\" (32px). Size gaps match the canonical scale: xs=4, sm=8, md=12, lg=16, xl=24, 2xl=32, 3xl=48. CSS variables use rem. Retrieve spacing and spacingRoles through lumen_get_tokens.\n- Use CardHeader, CardContent and CardFooter. Card owns their gap; missing, hidden and empty parts leave no spacing. Use a nested Stack inside the body and allow footer actions to wrap. Card density is compact, comfortable (default), or spacious; it does not reduce control touch targets.\n- Keep Prose/Typography text rhythm separate from sibling gaps. Check long translated text, enlarged text, validation, optional parts and phone/desktop wrapping in both themes. Use one outer Container for page gutters and one border owner per visible edge.\n\nBefore using MCP contracts, call lumen_check_compatibility with resolved installed Lumen versions.\nIf versions differ, use installed public types and package README or a matching MCP package.\nNever silently upgrade an application. For an explicit v4 upgrade, read lumen_get_migration and\npreview lumen migrate v4 --dry-run --json; review dependency updates and application workarounds.\n\n- [React form workflows](docs/powerful-forms.md): optional dependent validation, edit tracking, steps, repeatable rows, and async checks.\n\n- WorldMap: reusable dotted/solid SVG map in Astro, React and Elements. Import country geometry explicitly from @santi020k/lumen-core/world-map-data; see docs/world-map.md.\n",
61670
+ "rules": "# Lumen UI\n\nLumen is a multi-framework primitive UI system with Astro as the reference implementation.\n\nUse these files first when generating, reviewing, or migrating apps that use Lumen:\n\n- README.md: public overview and package strategy.\n- docs/ai-usage.md: AI-focused setup, package selection, and component selection.\n- https://lumen.santi020k.com/guides/build-ui-with-ai: tested Astro and React adoption example with a reusable prompt.\n- docs/ai-efficiency.md: reproducible AI token measurement, quality checks, and limits on efficiency claims.\n- skills/lumen-ui/SKILL.md: portable Agent Skill for building, reviewing, and theming Lumen interfaces.\n- docs/feature-roadmap.md: competitor-informed gaps and implementation priorities.\n- registry/lumen.registry.json: installable component, recipe, and documentation manifest.\n- packages/core/src/components.ts: authoritative shared component catalog.\n- packages/astro/components: reference component markup and prop shapes.\n- packages/astro/styles/lumen.css: standalone CSS contract for all frameworks.\n- @santi020k/lumen-mcp: Model Context Protocol server exposing snapshot provenance, structured component discovery, framework-aware usage/source, recipes, tolerant search, tokens, rules, and lumen:// resources for AI agents.\n\nInstall the portable Lumen Agent Skill:\n\n```bash\nnpx skills add santi020k/lumen --skill lumen-ui\n```\n\nDefault usage:\n\n```astro\n---\nimport { Button, Card, Input, Label } from '@santi020k/lumen-astro'\nimport UIPrimitives from '@santi020k/lumen-astro/runtime'\nimport '@santi020k/lumen-astro/styles.css'\n---\n\n<UIPrimitives />\n<Card>\n <Label for=\"email\">Email</Label>\n <Input id=\"email\" type=\"email\" />\n <Button>Subscribe</Button>\n</Card>\n```\n\nRules for agents:\n\n- When Lumen MCP is connected, read `lumen://meta`, `lumen://diagnostics`, and `lumen://rules`, pass the target framework to `lumen_search`, and follow the framework behavior returned by `lumen_get_component`. Retain `lumen://catalog-manifest` and use `lumen_diff_catalog` after upgrades when the client supports cached context.\n- Prefer Astro unless the user explicitly targets React or Web Components.\n- In React, use the visual components with behavior hooks for Dialog, Popover, DropdownMenu, Tabs, Select, Toast, and Tooltip instead of mounting the Astro runtime.\n- In Web Components, registered `lumen-*` elements include behavior parity for Dialog, Popover, DropdownMenu, Tabs, Select, Toast, and Tooltip, including ARIA state, keyboard paths, Escape/outside dismissal, focus handling, native select form participation, and the shared toast controller.\n- For React report filters, use `DateRangeInput` (`value`, `onValueChange`, localized `labels`, bounds and presets) or inline `DateRangeCalendar`. These React-specific compositions are documented in packages/react/README.md and the reporting example at /docs/web/reporting.\n- React record tables can opt into DataTableView from @santi020k/lumen-react/components/data-table-view for search, filters, visibility, density, pagination and controlled sorting. Read docs/data-table-views.md; applications retain cells, data fetching and domain actions.\n- Keep server-paginated DataTable rows in server order with `sortMode=\"manual\"`, controlled `sort` and `onSortChange`. Apply sorting before pagination in the application backend. Use Dialog dismissal policies for pending workflows.\n- Chart X identities must be unique. Use `xLabel` for abbreviated axes, `formatCategory` for full details, and explicit null gaps for missing observations. Read docs/data-visualization.md.\n- Use ImageComparison for before/after media with matching dimensions, alternative text and a visible localized control label. See docs/image-comparison.md for adapter contracts.\n- Read docs/migrating-to-lumen.md before upgrading to v4, especially date-trigger IDs, loading buttons, hidden semantics, copy feedback and native initializer changes.\n- Do not require Tailwind configuration for Lumen components.\n- Give each outer edge one border and radius owner. Flush images inside rounded frames use Image radius=\"none\"; Image defaults to radius=\"lg\". Use one divider at shared edges, account for padding on inset curves, and check phone/desktop corners and keyboard focus in both themes. Keep artwork clipping scoped so it does not cut off focus rings or overlays. See docs/ai-usage.md for styling rules.\n- Use public tokens: canvas, surface, surface-muted, surface-strong, line, ink, ink-soft, ink-muted, brand, brand-solid, brand-soft, on-brand, accent, success, warning, danger, on-danger.\n- Default to `Select` for web product option lists and `Combobox` for searchable choices. Use `NativeSelect` only for an explicit browser or platform picker requirement. Keep native fallbacks and verify the enhanced list with the matching framework behavior enabled.\n- Mount `UIPrimitives` once for interactive Astro primitives.\n- Use `glass` for glass surfaces. The obsolete overlay `surface=\"glass\"` alias is removed.\n- Treat schedule, data collection, rich text, theme builder, and advanced fields as supported surfaces with progressively enhanced behavior.\n- Use `lumen list`, `lumen show <component>`, and `lumen add <component>` for individually addressable registry components; component names also resolve from kebab-case aliases such as `data-table`.\n- Runtime CustomEvents used by `UIPrimitives`: `ui:data-table-selection-change` from `[data-ui-datatable]` with `{ values }`; `ui:schedule-change` from `[data-ui-schedule]` with `{ eventId, slot }`; `ui:virtual-list-range` from `[data-ui-virtual-list]` with `{ startIndex, endIndex }`; `ui:tag-remove` from tag groups with `{ value }`; `ui:editor-command` from rich text editors with `{ command }`; `ui:theme-change` from theme builders with `{ hue, tokens }`; `ui:theme-export` from theme builders with `{ css }`; `ui:validate`, `ui:invalid`, and `ui:valid` from `[data-ui-form]` for native Constraint Validation API enhancement; document-level `ui:toast`, `ui:toast-update`, and `ui:toast-dismiss` for runtime toasts; `ui:toast-action` from `[data-ui-toast]` when an action button is pressed.\n- Add `data-ui-form` to forms that should reflect `required`, `pattern`, `min`, `max`, `type`, and custom `setCustomValidity()` state into Field error slots on blur and submit. Use `data-error-required`, `data-error-pattern`, `data-error-min`, `data-error-max`, or `data-error-custom` to override browser validation messages.\n- Toasts use `ToastViewport` for explicit `placement` and `maxCount` configuration. With `UIPrimitives` or registered Web Components, use `window.LumenToast.create(detail)`, `.update(id, detail)`, and `.dismiss(id?)`, import `LumenToast` from `@santi020k/lumen-elements`, or dispatch the matching document events. Runtime toast `detail` supports `title`, `description`, `variant`, `duration`, `placement`, `max`, and `action`.\n\n## Content flow\n\n- Reuse content-flow-header, content-flow-settings, content-flow-list and content-flow-actions recipes; lumen_get_recipe returns complete framework source.\n- Container gutters grow from group to section spacing. Override --ui-container-gutter at the product boundary instead of adding nested page padding.\n- Prose and Typography trim their outer child margins and separate headings from preceding text. Card allows interactive overflow; AspectRatio owns media clipping.\n\n- Read docs/content-flow.md. Stack/Grid own sibling gaps; surfaces own padding; Field owns control feedback spacing. Do not add child margins for a relationship already controlled by a parent gap.\n- Use Stack/Grid gap=\"related\" (8px), \"group\" (16px, default), or \"section\" (32px). Size gaps match the canonical scale: xs=4, sm=8, md=12, lg=16, xl=24, 2xl=32, 3xl=48. CSS variables use rem. Retrieve spacing and spacingRoles through lumen_get_tokens.\n- Use CardHeader, CardContent and CardFooter. Card owns their gap; missing, hidden and empty parts leave no spacing. Use a nested Stack inside the body and allow footer actions to wrap. Card density is compact, comfortable (default), or spacious; it does not reduce control touch targets.\n- Keep Prose/Typography text rhythm separate from sibling gaps. Check long translated text, enlarged text, validation, optional parts and phone/desktop wrapping in both themes. Use one outer Container for page gutters and one border owner per visible edge.\n\nBefore using MCP contracts, call lumen_check_compatibility with resolved installed Lumen versions.\nIf versions differ, use installed public types and package README or a matching MCP package.\nNever silently upgrade an application. For an explicit v4 upgrade, read lumen_get_migration and\npreview lumen migrate v4 --dry-run --json; review dependency updates and application workarounds.\n\n- [React form workflows](docs/powerful-forms.md): optional dependent validation, edit tracking, steps, repeatable rows, and async checks.\n\n- WorldMap: reusable dotted/solid SVG map in Astro, React and Elements. Import country geometry explicitly from @santi020k/lumen-core/world-map-data; see docs/world-map.md.\n",
61452
61671
  "tokens": {
61453
61672
  "presets": {
61454
61673
  "attribute": "data-lumen-preset",