affora 0.1.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.
@@ -0,0 +1,169 @@
1
+ // shadcn — BASE UI era (the default primitive since July 2026). Empirically
2
+ // probed on base-ui.com: the SELECT trigger is button[aria-haspopup="listbox"]
3
+ // with data-placeholder when unset — NOT role="combobox" as under Radix — which
4
+ // is exactly why this is a separate variant file. Menus keep the same ARIA
5
+ // surface; portal cleanup loses the data-radix-* markers. Tabs / accordion /
6
+ // switch detection is ARIA-level and identical across eras, duplicated here so
7
+ // each variant file stands alone (a rules file is data, not a library).
8
+ ({
9
+ baseui_select: {
10
+ find: () => [...document.querySelectorAll('button[aria-haspopup="listbox"]')],
11
+ extract: (el) => {
12
+ const owned = el.getAttribute('aria-controls');
13
+ let box = owned && document.getElementById(owned);
14
+ if (!box) box = document.querySelector('[role="listbox"]');
15
+ const opts = box ? [...box.querySelectorAll('[role="option"]')]
16
+ .map(o => ({ value: o.getAttribute('data-value') || rsRendered(o),
17
+ label: rsRendered(o) })).filter(o => o.label) : [];
18
+ if (!opts.length) {
19
+ const tries = +(el.getAttribute('data-rs-probe') || 0);
20
+ if (tries >= 3) return null;
21
+ el.setAttribute('data-rs-probe', String(tries + 1));
22
+ el.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }));
23
+ el.dispatchEvent(new PointerEvent('pointerup', { bubbles: true }));
24
+ el.click();
25
+ return 'retry';
26
+ }
27
+ const isPlaceholder = el.hasAttribute('data-placeholder');
28
+ const current = rsRendered(el);
29
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
30
+ return { label: rsLabel(el), opts, current, isPlaceholder, box,
31
+ id: el.getAttribute('id') || '' };
32
+ },
33
+ render: (d) => {
34
+ const id = d.id || ('rs-' + (++window.__rsSeq));
35
+ const sel = !d.isPlaceholder && d.opts.some(o => o.label === d.current) ? d.current : '';
36
+ return `<div class="rs rs-field">
37
+ ${d.label ? `<label class="rs-label" for="${id}">${rsEsc(d.label)}</label>` : ''}
38
+ <select class="rs-select" id="${id}">
39
+ ${sel ? '' : `<option value="" selected>${rsEsc(d.current || 'Select…')}</option>`}
40
+ ${d.opts.map(o => `<option value="${rsEsc(o.value)}"${o.label === sel ? ' selected' : ''}>${rsEsc(o.label)}</option>`).join('')}
41
+ </select>
42
+ </div>`;
43
+ },
44
+ removes: (d) => d.box ? [d.box] : [],
45
+ // Base UI positions the popup in a floating wrapper without radix markers;
46
+ // removing the listbox itself is the safe, era-neutral cleanup.
47
+ after: (d) => { if (d.box && d.box.isConnected) d.box.remove(); },
48
+ },
49
+
50
+ baseui_menu: {
51
+ find: () => [...document.querySelectorAll('button[aria-haspopup="menu"]')]
52
+ // Never probe something that NAVIGATES or opens a dialog. Probing clicks,
53
+ // and on a documentation site the nav and the search both look like menu
54
+ // triggers: clicking them replaced the page and parity reported 130 severed
55
+ // navigation paths for a site that worked perfectly.
56
+ .filter(e => e.tagName !== 'A' && !e.getAttribute('href')
57
+ && e.getAttribute('aria-haspopup') !== 'dialog'
58
+ && !e.closest('[role="search"],form[role="search"],nav')),
59
+
60
+ extract: (el) => {
61
+ const owned = el.getAttribute('aria-controls');
62
+ let box = owned && document.getElementById(owned);
63
+ if (!box) box = document.querySelector('[role="menu"]');
64
+ const items = box ? [...box.querySelectorAll('[role^="menuitem"]')]
65
+ .map(o => ({ label: rsRendered(o),
66
+ // the anchor is often a child or ancestor of the menu item
67
+ href: (o.matches('a[href]') ? o : (o.querySelector('a[href]')
68
+ || o.closest('a[href]')) || {}).href || '' }))
69
+ .filter(o => o.label) : [];
70
+ if (!items.length) {
71
+ const tries = +(el.getAttribute('data-rs-probe') || 0);
72
+ if (tries >= 3) return null;
73
+ el.setAttribute('data-rs-probe', String(tries + 1));
74
+ el.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }));
75
+ el.dispatchEvent(new PointerEvent('pointerup', { bubbles: true }));
76
+ el.click();
77
+ return 'retry';
78
+ }
79
+ const label = rsRendered(el) || rsLabel(el);
80
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
81
+ return { label, items, box, orig: el };
82
+ },
83
+ render: (d) => `<div class="rs rs-field" data-rs-menu>
84
+ <span class="rs-label">${rsEsc(d.label)}</span>
85
+ <ul class="rs-pages-list">${d.items.map((i, n) =>
86
+ i.href ? `<li><a href="${rsEsc(i.href)}">${rsEsc(i.label)}</a></li>`
87
+ : `<li><button type="button" class="rs-iconbtn" data-rs-item="${n}">${rsEsc(i.label)}</button></li>`).join('')}</ul>
88
+ </div>`,
89
+ removes: (d) => d.box ? [d.box] : [],
90
+ after: (d, holder) => {
91
+ if (d.box && d.box.isConnected) d.box.remove();
92
+ d.orig.style.display = 'none';
93
+ d.orig.removeAttribute('data-rs-done');
94
+ holder.appendChild(d.orig);
95
+ holder.querySelectorAll('[data-rs-item]').forEach(btn => {
96
+ btn.addEventListener('click', () => {
97
+ const want = btn.textContent.trim();
98
+ d.orig.click();
99
+ setTimeout(() => {
100
+ const it = [...document.querySelectorAll('[role^="menuitem"]')]
101
+ .find(m => rsRendered(m) === want);
102
+ if (it) it.click();
103
+ else document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
104
+ }, 120);
105
+ });
106
+ });
107
+ },
108
+ },
109
+
110
+ // ARIA-level families shared across eras (same shapes as the radix file).
111
+ baseui_tabs: {
112
+ // flattening: the tab/section controls exist to reveal content this
113
+ // output reveals structurally, so they are subsumed, not lost
114
+ subsumes: true,
115
+ find: () => [...document.querySelectorAll('[role="tablist"]')]
116
+ .filter(t => t.querySelector('[role="tab"][aria-controls]')),
117
+ extract: (el) => {
118
+ const trigs = [...el.querySelectorAll('[role="tab"]')];
119
+ const st = el.__rsCap || (el.__rsCap = { caps: {} });
120
+ let missing = null;
121
+ trigs.forEach(t => {
122
+ const key = rsRendered(t);
123
+ const panel = document.getElementById(t.getAttribute('aria-controls') || '');
124
+ if (panel && rsRendered(panel)) st.caps[key] = { title: key, html: panel.innerHTML, panel };
125
+ else if (!(key in st.caps) && !missing) missing = t;
126
+ });
127
+ if (missing) {
128
+ const tries = +(el.getAttribute('data-rs-probe') || 0);
129
+ if (tries < trigs.length * 2 + 2) {
130
+ el.setAttribute('data-rs-probe', String(tries + 1));
131
+ missing.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
132
+ missing.focus();
133
+ missing.click();
134
+ return 'retry';
135
+ }
136
+ }
137
+ const tabs = trigs.map(t => st.caps[rsRendered(t)]).filter(Boolean);
138
+ return tabs.length >= 2 ? { tabs } : null;
139
+ },
140
+ render: (d) => `<div class="rs rs-sections">` + d.tabs.map(t =>
141
+ `<section class="rs-section"><h3 class="rs-section-title">${rsEsc(t.title)}</h3>
142
+ <div class="rs-section-body">${t.html}</div></section>`).join('') + `</div>`,
143
+ removes: (d) => d.tabs.map(t => t.panel),
144
+ after: (d) => d.tabs.forEach(t => t.panel && t.panel.isConnected && t.panel.remove()),
145
+ },
146
+
147
+ baseui_switch: {
148
+ find: () => [...document.querySelectorAll('button[role="switch"]')],
149
+ extract: (el) => ({
150
+ label: rsLabel(el) || rsRendered(el) || 'Toggle',
151
+ checked: el.getAttribute('aria-checked') === 'true',
152
+ orig: el,
153
+ }),
154
+ render: (d) => {
155
+ const id = 'rs-' + (++window.__rsSeq);
156
+ return `<div class="rs rs-field" data-rs-switch>
157
+ <label class="rs-label" for="${id}">${rsEsc(d.label)}</label>
158
+ <input type="checkbox" id="${id}"${d.checked ? ' checked' : ''}>
159
+ </div>`;
160
+ },
161
+ after: (d, holder) => {
162
+ d.orig.style.display = 'none';
163
+ d.orig.removeAttribute('data-rs-done');
164
+ holder.appendChild(d.orig);
165
+ holder.querySelector('input[type="checkbox"]')
166
+ .addEventListener('change', () => d.orig.click());
167
+ },
168
+ },
169
+ })
@@ -0,0 +1,289 @@
1
+ // shadcn — RADIX era (the default primitive until July 2026; every widget rides
2
+ // @radix-ui). Its defining hostility: a trigger
3
+ // <button role="combobox" aria-expanded="false"> whose options DO NOT EXIST in
4
+ // the DOM until the portal opens — the reason it scores in the benchmark's bottom
5
+ // tier. Passive extraction can only skip, so this rule probes: it opens the
6
+ // portal (the page's own interaction), harvests the options the page itself
7
+ // renders, closes it, and replaces trigger+portal with a native select. Every
8
+ // option label comes from the page; nothing is guessed.
9
+ ({
10
+ radix_select: {
11
+ find: () => [...document.querySelectorAll('button[role="combobox"]')],
12
+ extract: (el) => {
13
+ // Radix links trigger to the portalled listbox via aria-controls once open.
14
+ const owned = el.getAttribute('aria-controls');
15
+ const box = owned && document.getElementById(owned);
16
+ const opts = box ? [...box.querySelectorAll('[role="option"]')]
17
+ .map(o => ({ value: o.getAttribute('data-value') || rsRendered(o),
18
+ label: rsRendered(o) })).filter(o => o.label) : [];
19
+ if (!opts.length) {
20
+ // Phase 1: not open yet. Open it and wait for the mutation pass.
21
+ // Guard so a select that never yields options doesn't loop forever.
22
+ const tries = +(el.getAttribute('data-rs-probe') || 0);
23
+ if (tries >= 3) return null; // give up -> honest skip
24
+ el.setAttribute('data-rs-probe', String(tries + 1));
25
+ el.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }));
26
+ el.dispatchEvent(new PointerEvent('pointerup', { bubbles: true }));
27
+ el.click();
28
+ return 'retry';
29
+ }
30
+ // Phase 2: harvest, then close the portal before it is orphaned.
31
+ const label = rsLabel(el);
32
+ const current = rsRendered(el); // the trigger shows the value
33
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
34
+ return { label, opts, current, box, trigger: el,
35
+ id: el.getAttribute('id') || '' };
36
+ },
37
+ render: (d) => {
38
+ const id = d.id || ('rs-' + (++window.__rsSeq));
39
+ const sel = d.opts.some(o => o.label === d.current) ? d.current : '';
40
+ return `<div class="rs rs-field">
41
+ ${d.label ? `<label class="rs-label" for="${id}">${rsEsc(d.label)}</label>` : ''}
42
+ <select class="rs-select" id="${id}">
43
+ ${sel ? '' : `<option value="" selected>${rsEsc(d.current || 'Select…')}</option>`}
44
+ ${d.opts.map(o => `<option value="${rsEsc(o.value)}"${o.label === sel ? ' selected' : ''}>${rsEsc(o.label)}</option>`).join('')}
45
+ </select>
46
+ </div>`;
47
+ },
48
+ // the portalled listbox is consumed by the swap, not left floating
49
+ removes: (d) => d.box ? [d.box] : [],
50
+ after: (d, holder) => {
51
+ // Remove only the listbox, never Radix's positioning wrapper: that wrapper
52
+ // can contain more than the widget, and removing it took a site's whole
53
+ // navigation once.
54
+ if (d.box && d.box.isConnected) d.box.remove();
55
+ // SHADOW-DRIVE the trigger. Without this the replacement displays a choice
56
+ // the application never receives: React holds the state, the rebuilt <select>
57
+ // has no connection to it, and a user or agent selects an option that changes
58
+ // nothing. Parity saw the trigger as lost and was right to; effect parity
59
+ // could not see it at all, because it only exercises controls both arms name
60
+ // and a replaced trigger is by definition not one of those.
61
+ if (!d.trigger) return;
62
+ d.trigger.style.display = 'none';
63
+ d.trigger.setAttribute('data-rs-done', '');
64
+ holder.appendChild(d.trigger);
65
+ const shown = holder.querySelector('select.rs-select');
66
+ if (!shown) return;
67
+ shown.addEventListener('change', () => {
68
+ const want = shown.selectedOptions[0] && shown.selectedOptions[0].textContent.trim();
69
+ if (!want) return;
70
+ // Drive the real widget the way a person would: open it, then choose.
71
+ d.trigger.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }));
72
+ d.trigger.dispatchEvent(new PointerEvent('pointerup', { bubbles: true }));
73
+ d.trigger.click();
74
+ setTimeout(() => {
75
+ const opt = [...document.querySelectorAll('[role="option"]')]
76
+ .find(o => rsRendered(o) === want);
77
+ if (opt) opt.click();
78
+ else document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
79
+ }, 140);
80
+ });
81
+ },
82
+ },
83
+
84
+ // ── DropdownMenu: portal probing like the select, but items may be ACTIONS.
85
+ // Read items are rendered as real links; action items shadow-drive the original
86
+ // trigger (open the real menu, click the real item) so behaviour is preserved.
87
+ radix_menu: {
88
+ find: () => [...document.querySelectorAll('button[aria-haspopup="menu"]')]
89
+ // Never probe something that NAVIGATES or opens a dialog. Probing clicks,
90
+ // and on a documentation site the nav and the search both look like menu
91
+ // triggers: clicking them replaced the page and parity reported 130 severed
92
+ // navigation paths for a site that worked perfectly.
93
+ .filter(e => e.tagName !== 'A' && !e.getAttribute('href')
94
+ && e.getAttribute('aria-haspopup') !== 'dialog'
95
+ && !e.closest('[role="search"],form[role="search"],nav')),
96
+
97
+ extract: (el) => {
98
+ const owned = el.getAttribute('aria-controls');
99
+ const box = owned && document.getElementById(owned);
100
+ const items = box ? [...box.querySelectorAll('[role^="menuitem"]')]
101
+ .map(o => ({ label: rsRendered(o),
102
+ // the anchor is often a child or ancestor of the menu item
103
+ href: (o.matches('a[href]') ? o : (o.querySelector('a[href]')
104
+ || o.closest('a[href]')) || {}).href || '' }))
105
+ .filter(o => o.label) : [];
106
+ if (!items.length) {
107
+ const tries = +(el.getAttribute('data-rs-probe') || 0);
108
+ if (tries >= 3) return null;
109
+ el.setAttribute('data-rs-probe', String(tries + 1));
110
+ el.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }));
111
+ el.dispatchEvent(new PointerEvent('pointerup', { bubbles: true }));
112
+ el.click();
113
+ return 'retry';
114
+ }
115
+ const label = rsRendered(el) || rsLabel(el);
116
+ // A menu holds more than its items: a heading, an account name, an email, a
117
+ // separator caption. Harvesting only [role^=menuitem] deleted 'shadcn',
118
+ // 'm@example.com' and 'Toggle columns' from a page that shows all three.
119
+ const captions = [...box.children]
120
+ .filter(c => !c.matches('[role^="menuitem"],[role="separator"]'))
121
+ .map(c => rsRendered(c)).filter(Boolean)
122
+ .filter(t => !items.some(i => i.label === t));
123
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
124
+ return { label, items, captions, box, orig: el };
125
+ },
126
+ render: (d) => `<div class="rs rs-field" data-rs-menu>
127
+ <span class="rs-label">${rsEsc(d.label)}</span>
128
+ ${(d.captions || []).map(c => `<p class="rs-label">${rsEsc(c)}</p>`).join('')}
129
+ <ul class="rs-pages-list">${d.items.map((i, n) =>
130
+ i.href ? `<li><a href="${rsEsc(i.href)}">${rsEsc(i.label)}</a></li>`
131
+ : `<li><button type="button" class="rs-iconbtn" data-rs-item="${n}">${rsEsc(i.label)}</button></li>`).join('')}</ul>
132
+ </div>`,
133
+ removes: (d) => d.box ? [d.box] : [],
134
+ after: (d, holder) => {
135
+ // Remove ONLY the menu we harvested. Removing Radix's positioning wrapper
136
+ // took whatever else it contained: on ui.shadcn.com that was the site's main
137
+ // navigation, and every /docs/... destination vanished while the faithfulness
138
+ // check stayed green because the words were still elsewhere on the page.
139
+ // Capability is not text.
140
+ if (d.box && d.box.isConnected) d.box.remove();
141
+ // shadow-drive: the original trigger rides along hidden; an action item
142
+ // re-opens the real menu and clicks the real item, so React state is real
143
+ d.orig.style.display = 'none';
144
+ d.orig.removeAttribute('data-rs-done');
145
+ holder.appendChild(d.orig);
146
+ holder.querySelectorAll('[data-rs-item]').forEach(btn => {
147
+ btn.addEventListener('click', () => {
148
+ const want = btn.textContent.trim();
149
+ d.orig.click();
150
+ setTimeout(() => {
151
+ const it = [...document.querySelectorAll('[role^="menuitem"]')]
152
+ .find(m => rsRendered(m) === want);
153
+ if (it) it.click();
154
+ else document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
155
+ }, 120);
156
+ });
157
+ });
158
+ },
159
+ },
160
+
161
+ // ── Tabs: the inactive panel NODE exists but its content is unmounted, so a
162
+ // passive read sees an empty region. Probe: click each tab once, harvesting
163
+ // panels as they mount; when all are captured, replace with plain sections.
164
+ radix_tabs: {
165
+ // flattening: the tab/section controls exist to reveal content this
166
+ // output reveals structurally, so they are subsumed, not lost
167
+ subsumes: true,
168
+ find: () => [...document.querySelectorAll('[role="tablist"]')]
169
+ .filter(t => t.querySelector('[role="tab"][aria-controls]')),
170
+ extract: (el) => {
171
+ const trigs = [...el.querySelectorAll('[role="tab"]')];
172
+ const st = el.__rsCap || (el.__rsCap = { caps: {}, active0: null });
173
+ let missing = null;
174
+ trigs.forEach(t => {
175
+ const key = rsRendered(t);
176
+ if (t.getAttribute('data-state') === 'active' && st.active0 === null) st.active0 = key;
177
+ const panel = document.getElementById(t.getAttribute('aria-controls') || '');
178
+ if (panel && rsRendered(panel)) st.caps[key] = { title: key, html: panel.innerHTML, panel };
179
+ else if (!(key in st.caps) && !missing) missing = t;
180
+ });
181
+ if (missing) {
182
+ const tries = +(el.getAttribute('data-rs-probe') || 0);
183
+ if (tries < trigs.length * 2 + 2) {
184
+ el.setAttribute('data-rs-probe', String(tries + 1));
185
+ missing.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
186
+ missing.focus();
187
+ missing.click();
188
+ return 'retry';
189
+ }
190
+ }
191
+ const tabs = trigs.map(t => st.caps[rsRendered(t)]).filter(Boolean);
192
+ return tabs.length >= 2 ? { tabs } : null;
193
+ },
194
+ render: (d) => `<div class="rs rs-sections">` + d.tabs.map(t =>
195
+ `<section class="rs-section"><h3 class="rs-section-title">${rsEsc(t.title)}</h3>
196
+ <div class="rs-section-body">${t.html}</div></section>`).join('') + `</div>`,
197
+ removes: (d) => d.tabs.map(t => t.panel),
198
+ after: (d) => d.tabs.forEach(t => t.panel && t.panel.isConnected && t.panel.remove()),
199
+ },
200
+
201
+ // ── Accordion: same unmounted-content shape as tabs, different markup — a
202
+ // container of >= 2 button[aria-expanded][aria-controls] triggers.
203
+ radix_accordion: {
204
+ // flattening: the tab/section controls exist to reveal content this
205
+ // output reveals structurally, so they are subsumed, not lost
206
+ subsumes: true,
207
+ // Some shadcn accordions ship WITHOUT aria-controls (the arm we measure
208
+ // does), so association falls back to structure: the trigger's item wraps
209
+ // both the button and its content region.
210
+ find: () => {
211
+ const trigs = [...document.querySelectorAll('button[aria-expanded]')]
212
+ .filter(b => !b.closest('[role="tablist"]') && !b.getAttribute('aria-haspopup')
213
+ && b.getAttribute('role') !== 'combobox');
214
+ const roots = new Set();
215
+ trigs.forEach(t => {
216
+ let r = t.parentElement;
217
+ for (let i = 0; r && i < 4; i++, r = r.parentElement) {
218
+ if ([...r.querySelectorAll(':scope button[aria-expanded]')].length >= 2) {
219
+ roots.add(r); return;
220
+ }
221
+ }
222
+ });
223
+ return [...roots];
224
+ },
225
+ extract: (el) => {
226
+ const trigs = [...el.querySelectorAll('button[aria-expanded]')];
227
+ const panelOf = (t) => {
228
+ const id = t.getAttribute('aria-controls');
229
+ if (id && document.getElementById(id)) return document.getElementById(id);
230
+ // structural: the nearest wrapper that also holds a non-trigger block
231
+ let item = t.parentElement;
232
+ for (let i = 0; item && i < 3; i++, item = item.parentElement) {
233
+ const cand = [...item.children].find(c => c !== t && !c.contains(t)
234
+ && !c.querySelector('button[aria-expanded]'));
235
+ if (cand) return cand;
236
+ }
237
+ return null;
238
+ };
239
+ const st = el.__rsCap || (el.__rsCap = { caps: {} });
240
+ let missing = null;
241
+ trigs.forEach(t => {
242
+ const key = rsRendered(t);
243
+ const panel = panelOf(t);
244
+ if (panel && rsRendered(panel)) st.caps[key] = { title: key, html: panel.innerHTML, panel };
245
+ else if (!(key in st.caps) && !missing) missing = t;
246
+ });
247
+ if (missing) {
248
+ const tries = +(el.getAttribute('data-rs-probe') || 0);
249
+ if (tries < trigs.length * 2 + 2) {
250
+ el.setAttribute('data-rs-probe', String(tries + 1));
251
+ missing.click();
252
+ return 'retry';
253
+ }
254
+ }
255
+ const secs = trigs.map(t => st.caps[rsRendered(t)]).filter(Boolean);
256
+ return secs.length >= 2 ? { secs } : null;
257
+ },
258
+ render: (d) => `<div class="rs rs-sections">` + d.secs.map(t =>
259
+ `<section class="rs-section"><h3 class="rs-section-title">${rsEsc(t.title)}</h3>
260
+ <div class="rs-section-body">${t.html}</div></section>`).join('') + `</div>`,
261
+ removes: (d) => d.secs.map(t => t.panel),
262
+ after: (d) => d.secs.forEach(t => t.panel && t.panel.isConnected && t.panel.remove()),
263
+ },
264
+
265
+ // ── Switch: a button pretending to be a checkbox. Replace with a real one that
266
+ // shadow-drives the original, so React state stays the truth.
267
+ radix_switch: {
268
+ find: () => [...document.querySelectorAll('button[role="switch"]')],
269
+ extract: (el) => ({
270
+ label: rsLabel(el) || rsRendered(el) || 'Toggle',
271
+ checked: el.getAttribute('aria-checked') === 'true',
272
+ orig: el,
273
+ }),
274
+ render: (d) => {
275
+ const id = 'rs-' + (++window.__rsSeq);
276
+ return `<div class="rs rs-field" data-rs-switch>
277
+ <label class="rs-label" for="${id}">${rsEsc(d.label)}</label>
278
+ <input type="checkbox" id="${id}"${d.checked ? ' checked' : ''}>
279
+ </div>`;
280
+ },
281
+ after: (d, holder) => {
282
+ d.orig.style.display = 'none';
283
+ d.orig.removeAttribute('data-rs-done');
284
+ holder.appendChild(d.orig);
285
+ const box = holder.querySelector('input[type="checkbox"]');
286
+ box.addEventListener('change', () => d.orig.click());
287
+ },
288
+ },
289
+ })