@octanejs/radix 0.1.2

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.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +100 -0
  3. package/package.json +45 -0
  4. package/src/AccessibleIcon.ts +26 -0
  5. package/src/Accordion.ts +282 -0
  6. package/src/AlertDialog.ts +110 -0
  7. package/src/Arrow.ts +21 -0
  8. package/src/AspectRatio.ts +34 -0
  9. package/src/Avatar.ts +152 -0
  10. package/src/Checkbox.ts +325 -0
  11. package/src/Collapsible.ts +170 -0
  12. package/src/ContextMenu.ts +303 -0
  13. package/src/Dialog.ts +308 -0
  14. package/src/DismissableLayer.ts +413 -0
  15. package/src/DropdownMenu.ts +275 -0
  16. package/src/FocusScope.ts +266 -0
  17. package/src/Form.ts +621 -0
  18. package/src/HoverCard.ts +318 -0
  19. package/src/Label.ts +25 -0
  20. package/src/Menu.ts +1057 -0
  21. package/src/Menubar.ts +572 -0
  22. package/src/NavigationMenu.ts +1236 -0
  23. package/src/OneTimePasswordField.ts +977 -0
  24. package/src/PasswordToggleField.ts +495 -0
  25. package/src/Popover.ts +354 -0
  26. package/src/Popper.ts +401 -0
  27. package/src/Portal.ts +19 -0
  28. package/src/Presence.ts +225 -0
  29. package/src/Primitive.ts +53 -0
  30. package/src/Progress.ts +92 -0
  31. package/src/Radio.ts +262 -0
  32. package/src/RadioGroup.ts +212 -0
  33. package/src/RovingFocusGroup.ts +259 -0
  34. package/src/ScrollArea.ts +966 -0
  35. package/src/Select.ts +1901 -0
  36. package/src/Separator.ts +36 -0
  37. package/src/Slider.ts +745 -0
  38. package/src/Slot.ts +105 -0
  39. package/src/Switch.ts +278 -0
  40. package/src/Tabs.ts +177 -0
  41. package/src/Toast.ts +923 -0
  42. package/src/Toggle.ts +31 -0
  43. package/src/ToggleGroup.ts +157 -0
  44. package/src/Toolbar.ts +130 -0
  45. package/src/Tooltip.ts +669 -0
  46. package/src/VisuallyHidden.ts +29 -0
  47. package/src/collection.ts +97 -0
  48. package/src/compose-event-handlers.ts +15 -0
  49. package/src/compose-refs.ts +53 -0
  50. package/src/context.ts +109 -0
  51. package/src/direction.ts +19 -0
  52. package/src/focus-guards.ts +56 -0
  53. package/src/index.ts +54 -0
  54. package/src/internal.ts +42 -0
  55. package/src/scroll-lock.ts +57 -0
  56. package/src/use-callback-ref.ts +28 -0
  57. package/src/use-effect-event.ts +36 -0
  58. package/src/use-is-hydrated.ts +23 -0
  59. package/src/use-previous.ts +28 -0
  60. package/src/use-size.ts +57 -0
  61. package/src/useControllableState.ts +78 -0
  62. package/src/useId.ts +15 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dominic Gannaway
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,100 @@
1
+ # @octanejs/radix
2
+
3
+ [Radix UI Primitives](https://www.radix-ui.com/primitives) for the
4
+ [octane](https://github.com/octanejs/octane) renderer — a port of
5
+ [`@radix-ui/react`](https://www.npmjs.com/org/radix-ui) (headless, accessible UI
6
+ primitives) on octane's hooks. It mirrors the unified `radix-ui` package's shape: the
7
+ low-level composition utilities are exported directly, and each component is a namespace
8
+ (`Separator.Root`, `Label.Root`).
9
+
10
+ ```tsx
11
+ import { Separator, Label } from '@octanejs/radix';
12
+
13
+ function Field() {
14
+ return (
15
+ <div>
16
+ <Label.Root class="label">Email</Label.Root>
17
+ <Separator.Root orientation="horizontal" />
18
+ </div>
19
+ );
20
+ }
21
+ ```
22
+
23
+ ## How it works
24
+
25
+ The React binding layer maps onto octane cleanly: `forwardRef` → octane's ref-as-prop,
26
+ `composeRefs` → `composeRefs`/`useComposedRefs`, and Radix's `Slot`/`asChild`
27
+ (`Children.only` + `cloneElement` + prop/ref merge) → octane's runtime `Children` /
28
+ `cloneElement` / `isValidElement`. Popper/focus/dismiss behavior reuses the already-ported
29
+ [`@octanejs/floating-ui`](../floating-ui). See
30
+ [`docs/radix-migration-plan.md`](../../docs/radix-migration-plan.md) for the full plan.
31
+
32
+ ### `asChild` takes a descriptor
33
+
34
+ `Slot`/`asChild` operate on element **descriptors**. In `.tsrx`/`.tsx`, prop-position JSX
35
+ (`el={<button/>}`), `createElement`, and `.map()` returns are descriptors, but
36
+ children-position JSX compiles to a render function. So an octane `asChild` consumer passes
37
+ the child element at a value/prop position (or via `createElement`) rather than React's
38
+ children-position `<Trigger asChild><button/></Trigger>`.
39
+
40
+ ## Status
41
+
42
+ **Complete against the unified `radix-ui@1.6.1` component surface.** Landed:
43
+
44
+ - Composition foundation — `Slot`, `Slottable`, `Primitive.<tag>` (`asChild`), `mergeProps`
45
+ (event chaining, `style` merge, clsx-style `class` composition), `composeRefs` /
46
+ `useComposedRefs`, `composeEventHandlers`.
47
+ - State foundation — `useControllableState` (controlled/uncontrolled), `Presence` (keeps a
48
+ child mounted through its CSS exit animation), the full **`createContextScope`**
49
+ (`createScope` + `composeContextScopes`) so composed primitives can't collide,
50
+ `createCollection` (the Collection primitive: `data-radix-collection-item` stamping +
51
+ DOM-ordered item registry), the `radix-`-prefixed `useId`, `useCallbackRef`,
52
+ `Direction` (`useDirection`/`Provider`), and `useSize`.
53
+ - Components — `Separator`, `Label`, `Collapsible`, `Accordion` (single + multiple, full
54
+ keyboard nav), **`Dialog`** (modal + non-modal), **`AlertDialog`**, **`Toggle`**,
55
+ **`ToggleGroup`**, **`Tabs`**, **`Toolbar`** (roving focus + embedded ToggleGroup),
56
+ `AspectRatio`, `VisuallyHidden`, `Avatar` (image loading state machine), `Progress`,
57
+ `Arrow` — the roving-focus family on the full **`RovingFocusGroup`** port.
58
+ - **The Popper overlay family** — **`Popper`** (anchor/content/arrow on
59
+ `@octanejs/floating-ui`'s positioning core: offset/shift/flip/size/arrow/hide +
60
+ transform-origin middleware, `--radix-popper-*` CSS vars, virtual anchors),
61
+ **`Tooltip`** (provider delays, skip-delay, grace-area convex hull, hidden a11y copy),
62
+ **`Popover`** (modal + non-modal, custom Anchor), **`HoverCard`** (open/close delays,
63
+ selection containment), the shared **`Menu`** primitive (typeahead, checkbox/radio
64
+ items + indicators, submenus with pointer-grace polygons), **`DropdownMenu`**, and
65
+ **`ContextMenu`** (right-click / long-press virtual anchor).
66
+ - **`ScrollArea`** — custom scrollbars over a native scroll viewport: all four
67
+ visibility strategies (`hover` / `scroll` state machine / `auto` overflow
68
+ measurement / `always`), thumb drag + wheel scroll geometry, corner, and the
69
+ scroll-linked-effect-avoiding rAF thumb loop.
70
+ - **The form batch** — **`Checkbox`** (indeterminate), **`Switch`**,
71
+ **`RadioGroup`** (roving-focus arrow keys check items), **`Slider`**
72
+ (multi-thumb, keyboard + pointer-capture sliding), and **`Form`** (native
73
+ Constraint Validation: built-in + custom sync/async matchers, messages wired
74
+ into `aria-describedby`). Each control renders Radix's hidden native "bubble
75
+ input" inside forms, so FormData, form reset, and `<form onChange>` reflect
76
+ state natively (see the per-file headers for the documented octane
77
+ adaptations around the uncontrolled-input model).
78
+ - **The final six** — **`Menubar`** (menu family over the shared Menu primitive),
79
+ **`Select`** (item-aligned + popper positioning, typeahead, hidden native bubble
80
+ `<select>`), **`NavigationMenu`** (viewport/indicator machinery, motion attributes,
81
+ delayed open/close), **`Toast`** (viewport hotkey, pausable timers, swipe machinery,
82
+ announce regions), **`OneTimePasswordField`** (per-char cells, paste distribution,
83
+ roving focus), and **`PasswordToggleField`** — plus `AccessibleIcon`,
84
+ `useEffectEvent`, and `useIsHydrated`.
85
+ - Overlay infra — `Portal`, `DismissableLayer`, `FocusScope`, `useFocusGuards`,
86
+ `useScrollLock` (a focused `react-remove-scroll` replacement — see `scroll-lock.ts`),
87
+ with the framework-agnostic `aria-hidden` package reused as-is.
88
+
89
+ **Verified against real Radix**: the differential suite
90
+ (`tests/differential/parity.test.ts`) runs the SAME fixture through `@octanejs/radix` and
91
+ the real `radix-ui` package on React, asserting byte-identical DOM after every interaction
92
+ step; portal'd overlays (which the rig can't see) carry dedicated focus/keyboard/dismiss
93
+ unit suites. Ports come from the pinned radix-ui/primitives source checkout
94
+ (`.radix-primitives/`); every file header cites its source path.
95
+
96
+ Remaining (documented in
97
+ [`docs/radix-migration-plan.md`](../../docs/radix-migration-plan.md)): SSR/hydration
98
+ coverage for the overlay/portal components and Phase-5 polish. The port surfaced —
99
+ and fixed, in octane itself — fourteen runtime/compiler parity bugs along the way;
100
+ each is pinned by an octane regression test and a changeset.
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@octanejs/radix",
3
+ "version": "0.1.2",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "description": "Radix UI Primitives for the octane renderer — a port of @radix-ui/react (headless, accessible UI primitives) on octane's hooks, built on the same @octanejs/floating-ui substrate.",
7
+ "author": {
8
+ "name": "Dominic Gannaway",
9
+ "email": "dg@domgan.com"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/octanejs/octane.git",
17
+ "directory": "packages/radix"
18
+ },
19
+ "main": "src/index.ts",
20
+ "module": "src/index.ts",
21
+ "types": "src/index.ts",
22
+ "files": [
23
+ "src",
24
+ "README.md"
25
+ ],
26
+ "exports": {
27
+ ".": "./src/index.ts"
28
+ },
29
+ "dependencies": {
30
+ "aria-hidden": "^1.2.6",
31
+ "@octanejs/floating-ui": "0.1.2",
32
+ "octane": "0.1.3"
33
+ },
34
+ "devDependencies": {
35
+ "@tsrx/react": "^0.2.33",
36
+ "esbuild": "^0.28.1",
37
+ "radix-ui": "^1.4.3",
38
+ "react": "^19.2.0",
39
+ "react-dom": "^19.2.0",
40
+ "vitest": "^4.1.9"
41
+ },
42
+ "scripts": {
43
+ "test": "vitest run"
44
+ }
45
+ }
@@ -0,0 +1,26 @@
1
+ // Ported from @radix-ui/react-accessible-icon (source:
2
+ // .radix-primitives/packages/react/accessible-icon/src/accessible-icon.tsx). Hides the
3
+ // icon child from ATs (aria-hidden + unfocusable SVG) and announces `label` through a
4
+ // VisuallyHidden copy instead — `alt` text for icons. octane note: like Slot/asChild,
5
+ // the icon child must be an element DESCRIPTOR (prop/value-position JSX or
6
+ // createElement); a single-element array unwraps, matching the Slot convention.
7
+ import { Children, cloneElement, createElement } from 'octane';
8
+
9
+ import * as VisuallyHiddenPrimitive from './VisuallyHidden';
10
+
11
+ export function Root(props: any): any {
12
+ const { children, label } = props ?? {};
13
+ const array = Children.toArray(children);
14
+ const child = array.length === 1 ? array[0] : Children.only(children);
15
+ return [
16
+ cloneElement(child as any, {
17
+ // accessibility
18
+ 'aria-hidden': 'true',
19
+ focusable: 'false', // See: https://allyjs.io/tutorials/focusing-in-svg.html#making-svg-elements-focusable
20
+ key: 'icon',
21
+ }),
22
+ createElement(VisuallyHiddenPrimitive.Root, { key: 'label', children: label }),
23
+ ];
24
+ }
25
+
26
+ export { Root as AccessibleIcon };
@@ -0,0 +1,282 @@
1
+ // Ported from @radix-ui/react-accordion. A set of collapsible sections. Each Item is a
2
+ // Collapsible whose `open` is derived from the Accordion's controllable `value` (a single
3
+ // string for `type="single"`, or a string[] for `type="multiple"`). `.ts` components via
4
+ // createElement. Uses Radix's scoped context: `createAccordionScope` composes
5
+ // `createCollapsibleScope`, so each Item threads its own isolated Collapsible scope
6
+ // (`__scopeCollapsible`) — a user's separate Collapsible won't collide with an Accordion's.
7
+ //
8
+ // DEFERRED: arrow-key roving focus between triggers (Radix wraps triggers in
9
+ // RovingFocusGroup). Expand/collapse + ARIA + data-state are complete; the roving-focus
10
+ // primitive is a separate, reusable follow-up (also used by Tabs/Toolbar/RadioGroup).
11
+ import { createElement, useCallback } from 'octane';
12
+
13
+ import * as Collapsible from './Collapsible';
14
+ import { createCollapsibleScope } from './Collapsible';
15
+ import { createCollection } from './collection';
16
+ import { composeEventHandlers } from './compose-event-handlers';
17
+ import { createContextScope } from './context';
18
+ import { S, subSlot } from './internal';
19
+ import { Primitive } from './Primitive';
20
+ import { useControllableState } from './useControllableState';
21
+ import { useId } from './useId';
22
+
23
+ const ACCORDION_KEYS = ['Home', 'End', 'ArrowDown', 'ArrowUp', 'ArrowLeft', 'ArrowRight'];
24
+ const [Collection, useCollection] = createCollection('Accordion');
25
+
26
+ interface ValueContextValue {
27
+ value: string[];
28
+ onItemOpen: (value: string) => void;
29
+ onItemClose: (value: string) => void;
30
+ }
31
+ interface ImplContextValue {
32
+ disabled?: boolean;
33
+ orientation: 'horizontal' | 'vertical';
34
+ collapsible: boolean;
35
+ isMultiple: boolean;
36
+ }
37
+ interface ItemContextValue {
38
+ open: boolean;
39
+ disabled?: boolean;
40
+ triggerId: string;
41
+ }
42
+
43
+ const [createAccordionContext, createAccordionScope] = createContextScope('Accordion', [
44
+ createCollapsibleScope,
45
+ ]);
46
+ export { createAccordionScope };
47
+ const useCollapsibleScope = createCollapsibleScope();
48
+
49
+ const [AccordionValueProvider, useAccordionValueContext] =
50
+ createAccordionContext<ValueContextValue>('Accordion');
51
+ const [AccordionImplProvider, useAccordionImplContext] =
52
+ createAccordionContext<ImplContextValue>('Accordion');
53
+ const [AccordionItemProvider, useAccordionItemContext] =
54
+ createAccordionContext<ItemContextValue>('AccordionItem');
55
+
56
+ function getState(open?: boolean): 'open' | 'closed' {
57
+ return open ? 'open' : 'closed';
58
+ }
59
+
60
+ export function Root(props: any): any {
61
+ const slot = S('Accordion.Root');
62
+ const {
63
+ __scopeAccordion,
64
+ type,
65
+ value: valueProp,
66
+ defaultValue,
67
+ onValueChange,
68
+ collapsible = false,
69
+ disabled,
70
+ dir,
71
+ orientation = 'vertical',
72
+ ...rest
73
+ } = props ?? {};
74
+ const isMultiple = type === 'multiple';
75
+
76
+ const [value, setValue] = useControllableState<any>(
77
+ {
78
+ prop: valueProp,
79
+ defaultProp: defaultValue ?? (isMultiple ? [] : undefined),
80
+ onChange: onValueChange,
81
+ },
82
+ subSlot(slot, 'value'),
83
+ );
84
+ const valueArray: string[] = isMultiple
85
+ ? (value ?? [])
86
+ : value != null && value !== ''
87
+ ? [value]
88
+ : [];
89
+
90
+ const onItemOpen = useCallback(
91
+ (itemValue: string) => {
92
+ if (isMultiple) setValue((prev: string[]) => [...(prev ?? []), itemValue]);
93
+ else setValue(itemValue);
94
+ },
95
+ [isMultiple],
96
+ subSlot(slot, 'open'),
97
+ );
98
+ const onItemClose = useCallback(
99
+ (itemValue: string) => {
100
+ if (isMultiple) setValue((prev: string[]) => (prev ?? []).filter((v) => v !== itemValue));
101
+ else if (collapsible) setValue('');
102
+ },
103
+ [isMultiple, collapsible],
104
+ subSlot(slot, 'close'),
105
+ );
106
+
107
+ return createElement(Collection.Provider, {
108
+ scope: __scopeAccordion,
109
+ children: createElement(AccordionValueProvider, {
110
+ scope: __scopeAccordion,
111
+ value: valueArray,
112
+ onItemOpen,
113
+ onItemClose,
114
+ children: createElement(AccordionImplProvider, {
115
+ scope: __scopeAccordion,
116
+ disabled,
117
+ orientation,
118
+ collapsible,
119
+ isMultiple,
120
+ children: createElement(AccordionImpl, {
121
+ __scopeAccordion,
122
+ disabled,
123
+ dir,
124
+ orientation,
125
+ ...rest,
126
+ }),
127
+ }),
128
+ }),
129
+ });
130
+ }
131
+
132
+ // The keyboard-navigable root element (Radix's AccordionImpl): Home/End jump, arrow keys
133
+ // move between triggers per orientation/direction, wrapping — driven by the Collection.
134
+ function AccordionImpl(props: any): any {
135
+ const slot = S('Accordion.Impl');
136
+ const { __scopeAccordion, disabled, dir, orientation, ...accordionProps } = props;
137
+ const getItems = useCollection(__scopeAccordion, subSlot(slot, 'items'));
138
+ const isDirectionLTR = (dir ?? 'ltr') === 'ltr';
139
+
140
+ const handleKeyDown = composeEventHandlers(accordionProps.onKeyDown, (event: KeyboardEvent) => {
141
+ if (!ACCORDION_KEYS.includes(event.key)) return;
142
+ const target = event.target as HTMLElement;
143
+ const triggerCollection = getItems().filter((item: any) => !item.ref.current?.disabled);
144
+ const triggerIndex = triggerCollection.findIndex((item: any) => item.ref.current === target);
145
+ const triggerCount = triggerCollection.length;
146
+ if (triggerIndex === -1) return;
147
+ event.preventDefault();
148
+ let nextIndex = triggerIndex;
149
+ const homeIndex = 0;
150
+ const endIndex = triggerCount - 1;
151
+ const moveNext = (): void => {
152
+ nextIndex = triggerIndex + 1;
153
+ if (nextIndex > endIndex) nextIndex = homeIndex;
154
+ };
155
+ const movePrev = (): void => {
156
+ nextIndex = triggerIndex - 1;
157
+ if (nextIndex < homeIndex) nextIndex = endIndex;
158
+ };
159
+ switch (event.key) {
160
+ case 'Home':
161
+ nextIndex = homeIndex;
162
+ break;
163
+ case 'End':
164
+ nextIndex = endIndex;
165
+ break;
166
+ case 'ArrowRight':
167
+ if (orientation === 'horizontal') {
168
+ if (isDirectionLTR) moveNext();
169
+ else movePrev();
170
+ }
171
+ break;
172
+ case 'ArrowDown':
173
+ if (orientation === 'vertical') moveNext();
174
+ break;
175
+ case 'ArrowLeft':
176
+ if (orientation === 'horizontal') {
177
+ if (isDirectionLTR) movePrev();
178
+ else moveNext();
179
+ }
180
+ break;
181
+ case 'ArrowUp':
182
+ if (orientation === 'vertical') movePrev();
183
+ break;
184
+ }
185
+ triggerCollection[nextIndex % triggerCount].ref.current?.focus();
186
+ });
187
+
188
+ return createElement(Collection.Slot, {
189
+ scope: __scopeAccordion,
190
+ children: createElement(Primitive.div, {
191
+ ...accordionProps,
192
+ 'data-orientation': orientation,
193
+ onKeyDown: disabled ? undefined : handleKeyDown,
194
+ }),
195
+ });
196
+ }
197
+
198
+ export function Item(props: any): any {
199
+ const slot = S('Accordion.Item');
200
+ const { __scopeAccordion, value: itemValue, disabled: itemDisabled, ...rest } = props ?? {};
201
+ const impl = useAccordionImplContext('AccordionItem', __scopeAccordion);
202
+ const valueCtx = useAccordionValueContext('AccordionItem', __scopeAccordion);
203
+ const collapsibleScope = useCollapsibleScope(__scopeAccordion, subSlot(slot, 'cscope'));
204
+ const triggerId = useId(subSlot(slot, 'id'));
205
+ const open = (itemValue != null && valueCtx.value.includes(itemValue)) || false;
206
+ const disabled = impl.disabled || itemDisabled || false;
207
+
208
+ return createElement(AccordionItemProvider, {
209
+ scope: __scopeAccordion,
210
+ open,
211
+ disabled,
212
+ triggerId,
213
+ children: createElement(Collapsible.Root, {
214
+ 'data-orientation': impl.orientation,
215
+ 'data-state': getState(open),
216
+ ...collapsibleScope,
217
+ ...rest,
218
+ disabled,
219
+ open,
220
+ onOpenChange: (isOpen: boolean) => {
221
+ if (isOpen) valueCtx.onItemOpen(itemValue);
222
+ else valueCtx.onItemClose(itemValue);
223
+ },
224
+ }),
225
+ });
226
+ }
227
+
228
+ export function Header(props: any): any {
229
+ const { __scopeAccordion, ...rest } = props ?? {};
230
+ const impl = useAccordionImplContext('AccordionHeader', __scopeAccordion);
231
+ const item = useAccordionItemContext('AccordionHeader', __scopeAccordion);
232
+ return createElement(Primitive.h3, {
233
+ 'data-orientation': impl.orientation,
234
+ 'data-state': getState(item.open),
235
+ 'data-disabled': item.disabled ? '' : undefined,
236
+ ...rest,
237
+ });
238
+ }
239
+
240
+ export function Trigger(props: any): any {
241
+ const slot = S('Accordion.Trigger');
242
+ const { __scopeAccordion, ...rest } = props ?? {};
243
+ const impl = useAccordionImplContext('AccordionTrigger', __scopeAccordion);
244
+ const item = useAccordionItemContext('AccordionTrigger', __scopeAccordion);
245
+ const collapsibleScope = useCollapsibleScope(__scopeAccordion, subSlot(slot, 'cscope'));
246
+ // A `type="single"` non-collapsible open item can't be closed → aria-disabled.
247
+ const ariaDisabled = (item.open && !impl.isMultiple && !impl.collapsible) || undefined;
248
+ // Registered in the Collection (stamps `data-radix-collection-item` + a ref onto the
249
+ // underlying button) so the root's keyboard nav can enumerate triggers in DOM order.
250
+ return createElement(Collection.ItemSlot, {
251
+ scope: __scopeAccordion,
252
+ children: createElement(Collapsible.Trigger, {
253
+ 'aria-disabled': ariaDisabled,
254
+ 'data-orientation': impl.orientation,
255
+ id: item.triggerId,
256
+ ...collapsibleScope,
257
+ ...rest,
258
+ }),
259
+ });
260
+ }
261
+
262
+ export function Content(props: any): any {
263
+ const slot = S('Accordion.Content');
264
+ const { __scopeAccordion, style, ...rest } = props ?? {};
265
+ const impl = useAccordionImplContext('AccordionContent', __scopeAccordion);
266
+ const item = useAccordionItemContext('AccordionContent', __scopeAccordion);
267
+ const collapsibleScope = useCollapsibleScope(__scopeAccordion, subSlot(slot, 'cscope'));
268
+ return createElement(Collapsible.Content, {
269
+ role: 'region',
270
+ 'aria-labelledby': item.triggerId,
271
+ 'data-orientation': impl.orientation,
272
+ ...collapsibleScope,
273
+ ...rest,
274
+ style: {
275
+ '--radix-accordion-content-height': 'var(--radix-collapsible-content-height)',
276
+ '--radix-accordion-content-width': 'var(--radix-collapsible-content-width)',
277
+ ...style,
278
+ },
279
+ });
280
+ }
281
+
282
+ export { Root as Accordion };
@@ -0,0 +1,110 @@
1
+ // Ported from @radix-ui/react-alert-dialog. A modal confirmation dialog built as a thin
2
+ // wrapper over Dialog: always `modal`, `role="alertdialog"`, outside interactions never
3
+ // dismiss (pointer-down/interact-outside prevented), and opening autofocuses the CANCEL
4
+ // action (the safe choice) instead of the first tabbable.
5
+ import { createElement, useRef } from 'octane';
6
+
7
+ import { composeEventHandlers } from './compose-event-handlers';
8
+ import { useComposedRefs } from './compose-refs';
9
+ import { createContextScope } from './context';
10
+ import * as DialogPrimitive from './Dialog';
11
+ import { createDialogScope } from './Dialog';
12
+ import { S, subSlot } from './internal';
13
+
14
+ const [createAlertDialogContext, createAlertDialogScope] = createContextScope('AlertDialog', [
15
+ createDialogScope,
16
+ ]);
17
+ export { createAlertDialogScope };
18
+ const useDialogScope = createDialogScope();
19
+
20
+ const [AlertDialogContentProvider, useAlertDialogContentContext] = createAlertDialogContext<{
21
+ cancelRef: { current: HTMLElement | null };
22
+ }>('AlertDialogContent');
23
+
24
+ export function Root(props: any): any {
25
+ const slot = S('AlertDialog.Root');
26
+ const { __scopeAlertDialog, ...alertDialogProps } = props ?? {};
27
+ const dialogScope = useDialogScope(__scopeAlertDialog, subSlot(slot, 'scope'));
28
+ return createElement(DialogPrimitive.Root, { ...dialogScope, ...alertDialogProps, modal: true });
29
+ }
30
+
31
+ export function Trigger(props: any): any {
32
+ const slot = S('AlertDialog.Trigger');
33
+ const { __scopeAlertDialog, ...triggerProps } = props ?? {};
34
+ const dialogScope = useDialogScope(__scopeAlertDialog, subSlot(slot, 'scope'));
35
+ return createElement(DialogPrimitive.Trigger, { ...dialogScope, ...triggerProps });
36
+ }
37
+
38
+ export function Portal(props: any): any {
39
+ const slot = S('AlertDialog.Portal');
40
+ const { __scopeAlertDialog, ...portalProps } = props ?? {};
41
+ const dialogScope = useDialogScope(__scopeAlertDialog, subSlot(slot, 'scope'));
42
+ return createElement(DialogPrimitive.Portal, { ...dialogScope, ...portalProps });
43
+ }
44
+
45
+ export function Overlay(props: any): any {
46
+ const slot = S('AlertDialog.Overlay');
47
+ const { __scopeAlertDialog, ...overlayProps } = props ?? {};
48
+ const dialogScope = useDialogScope(__scopeAlertDialog, subSlot(slot, 'scope'));
49
+ return createElement(DialogPrimitive.Overlay, { ...dialogScope, ...overlayProps });
50
+ }
51
+
52
+ export function Content(props: any): any {
53
+ const slot = S('AlertDialog.Content');
54
+ const { __scopeAlertDialog, children, ref: forwardedRef, ...contentProps } = props ?? {};
55
+ const dialogScope = useDialogScope(__scopeAlertDialog, subSlot(slot, 'scope'));
56
+ const contentRef = useRef<HTMLElement | null>(null, subSlot(slot, 'content'));
57
+ const composedRefs = useComposedRefs(forwardedRef, contentRef, subSlot(slot, 'refs'));
58
+ const cancelRef = useRef<HTMLElement | null>(null, subSlot(slot, 'cancel'));
59
+ return createElement(AlertDialogContentProvider, {
60
+ scope: __scopeAlertDialog,
61
+ cancelRef,
62
+ children: createElement(DialogPrimitive.Content, {
63
+ role: 'alertdialog',
64
+ ...dialogScope,
65
+ ...contentProps,
66
+ ref: composedRefs,
67
+ onOpenAutoFocus: composeEventHandlers(contentProps.onOpenAutoFocus, (event: Event) => {
68
+ event.preventDefault();
69
+ cancelRef.current?.focus({ preventScroll: true });
70
+ }),
71
+ onPointerDownOutside: (event: Event) => event.preventDefault(),
72
+ onInteractOutside: (event: Event) => event.preventDefault(),
73
+ children,
74
+ }),
75
+ });
76
+ }
77
+
78
+ export function Title(props: any): any {
79
+ const slot = S('AlertDialog.Title');
80
+ const { __scopeAlertDialog, ...titleProps } = props ?? {};
81
+ const dialogScope = useDialogScope(__scopeAlertDialog, subSlot(slot, 'scope'));
82
+ return createElement(DialogPrimitive.Title, { ...dialogScope, ...titleProps });
83
+ }
84
+
85
+ export function Description(props: any): any {
86
+ const slot = S('AlertDialog.Description');
87
+ const { __scopeAlertDialog, ...descriptionProps } = props ?? {};
88
+ const dialogScope = useDialogScope(__scopeAlertDialog, subSlot(slot, 'scope'));
89
+ return createElement(DialogPrimitive.Description, { ...dialogScope, ...descriptionProps });
90
+ }
91
+
92
+ /** Confirms the action — closes the dialog (a `Dialog.Close`). */
93
+ export function Action(props: any): any {
94
+ const slot = S('AlertDialog.Action');
95
+ const { __scopeAlertDialog, ...actionProps } = props ?? {};
96
+ const dialogScope = useDialogScope(__scopeAlertDialog, subSlot(slot, 'scope'));
97
+ return createElement(DialogPrimitive.Close, { ...dialogScope, ...actionProps });
98
+ }
99
+
100
+ /** Cancels — closes the dialog and is the element autofocused on open. */
101
+ export function Cancel(props: any): any {
102
+ const slot = S('AlertDialog.Cancel');
103
+ const { __scopeAlertDialog, ref: forwardedRef, ...cancelProps } = props ?? {};
104
+ const { cancelRef } = useAlertDialogContentContext('AlertDialogCancel', __scopeAlertDialog);
105
+ const dialogScope = useDialogScope(__scopeAlertDialog, subSlot(slot, 'scope'));
106
+ const ref = useComposedRefs(forwardedRef, cancelRef, subSlot(slot, 'refs'));
107
+ return createElement(DialogPrimitive.Close, { ...dialogScope, ...cancelProps, ref });
108
+ }
109
+
110
+ export { Root as AlertDialog };
package/src/Arrow.ts ADDED
@@ -0,0 +1,21 @@
1
+ // Ported from @radix-ui/react-arrow (source:
2
+ // .radix-primitives/packages/react/arrow/src/arrow.tsx). The default popper arrow: a
3
+ // 30×10 viewBox svg polygon, stretchable via width/height.
4
+ import { createElement } from 'octane';
5
+
6
+ import { Primitive } from './Primitive';
7
+
8
+ export function Arrow(props: any): any {
9
+ const { children, width = 10, height = 5, ...arrowProps } = props ?? {};
10
+ return createElement(Primitive.svg, {
11
+ ...arrowProps,
12
+ width,
13
+ height,
14
+ viewBox: '0 0 30 10',
15
+ preserveAspectRatio: 'none',
16
+ // We use their children if they're slotting to replace the whole svg.
17
+ children: props?.asChild ? children : createElement('polygon', { points: '0,0 30,0 15,10' }),
18
+ });
19
+ }
20
+
21
+ export { Arrow as Root };
@@ -0,0 +1,34 @@
1
+ // Ported from @radix-ui/react-aspect-ratio (source:
2
+ // .radix-primitives/packages/react/aspect-ratio/src/aspect-ratio.tsx). Constrains content
3
+ // to a ratio via the padding-bottom trick on a wrapper div.
4
+ import { createElement } from 'octane';
5
+
6
+ import { Primitive } from './Primitive';
7
+
8
+ export function AspectRatio(props: any): any {
9
+ const { ratio = 1 / 1, style, ...aspectRatioProps } = props ?? {};
10
+ return createElement('div', {
11
+ style: {
12
+ // ensures inner element is contained
13
+ position: 'relative',
14
+ // ensures padding bottom trick maths works
15
+ width: '100%',
16
+ paddingBottom: `${100 / ratio}%`,
17
+ },
18
+ 'data-radix-aspect-ratio-wrapper': '',
19
+ children: createElement(Primitive.div, {
20
+ ...aspectRatioProps,
21
+ style: {
22
+ ...style,
23
+ // ensures children expand in ratio
24
+ position: 'absolute',
25
+ top: 0,
26
+ right: 0,
27
+ bottom: 0,
28
+ left: 0,
29
+ },
30
+ }),
31
+ });
32
+ }
33
+
34
+ export { AspectRatio as Root };