@a4ui/core 0.38.1 → 0.39.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.
Files changed (129) hide show
  1. package/README.md +66 -9
  2. package/dist/{Collapse-Cr5cDqAl.js → Collapse-CrKiFD8K.js} +173 -124
  3. package/dist/charts/Sparkline.d.ts +2 -0
  4. package/dist/charts.js +8 -5
  5. package/dist/commerce.js +59 -59
  6. package/dist/elements.css +248 -151
  7. package/dist/elements.iife.js +1 -1
  8. package/dist/elements.js +1025 -1015
  9. package/dist/full.css +248 -151
  10. package/dist/index.d.ts +2 -1
  11. package/dist/index.js +4723 -4656
  12. package/dist/lib/anchored.d.ts +7 -0
  13. package/dist/lib/direction.d.ts +18 -0
  14. package/dist/styles.css +27 -11
  15. package/dist/theme.css +1559 -0
  16. package/dist/ui/DirectionProvider.d.ts +47 -0
  17. package/dist/ui/Drawer.d.ts +6 -2
  18. package/dist/ui/Sidebar.d.ts +8 -2
  19. package/dist/ui/Toast.d.ts +5 -1
  20. package/package.json +5 -3
  21. package/preset.d.ts +2 -1
  22. package/preset.js +16 -5
  23. package/src/charts/BarList.tsx +1 -1
  24. package/src/charts/Sparkline.tsx +5 -1
  25. package/src/commerce/CartLine.tsx +1 -1
  26. package/src/commerce/FacetSidebar.tsx +1 -1
  27. package/src/commerce/ProductCard.tsx +1 -1
  28. package/src/index.ts +7 -1
  29. package/src/layout/NavGroup.tsx +2 -2
  30. package/src/lib/anchored.ts +13 -0
  31. package/src/lib/direction.ts +35 -0
  32. package/src/ui/Accordion.tsx +2 -2
  33. package/src/ui/ActivityFeed.tsx +2 -2
  34. package/src/ui/AlertDialog.tsx +2 -1
  35. package/src/ui/Anchor.tsx +1 -1
  36. package/src/ui/AnnouncementBar.tsx +3 -3
  37. package/src/ui/ArtifactPanel.tsx +1 -1
  38. package/src/ui/AvailabilityPicker.tsx +2 -2
  39. package/src/ui/AvatarGroup.tsx +2 -2
  40. package/src/ui/BackToTop.tsx +1 -1
  41. package/src/ui/Badge.tsx +1 -1
  42. package/src/ui/BlockEditor.tsx +1 -1
  43. package/src/ui/ButtonGroup.tsx +1 -1
  44. package/src/ui/Callout.tsx +1 -1
  45. package/src/ui/Carousel.tsx +17 -9
  46. package/src/ui/Cascader.tsx +2 -2
  47. package/src/ui/CategoryStrip.tsx +4 -1
  48. package/src/ui/CodeEditor.tsx +5 -5
  49. package/src/ui/CodeTabs.tsx +1 -1
  50. package/src/ui/Combobox.tsx +2 -1
  51. package/src/ui/Command.tsx +3 -2
  52. package/src/ui/Comment.tsx +1 -1
  53. package/src/ui/Configurator.tsx +2 -2
  54. package/src/ui/ContextMenu.tsx +2 -1
  55. package/src/ui/DateField.tsx +4 -4
  56. package/src/ui/DateRangePicker.tsx +4 -4
  57. package/src/ui/DiffViewer.tsx +4 -3
  58. package/src/ui/DirectionProvider.tsx +82 -0
  59. package/src/ui/Drawer.tsx +23 -5
  60. package/src/ui/Dropdown.tsx +2 -1
  61. package/src/ui/EmojiPicker.tsx +5 -2
  62. package/src/ui/Expandable.tsx +2 -2
  63. package/src/ui/Faq.tsx +1 -1
  64. package/src/ui/Field.tsx +1 -1
  65. package/src/ui/FloatingActionButton.tsx +2 -2
  66. package/src/ui/FollowingPointer.tsx +2 -2
  67. package/src/ui/Hero.tsx +2 -2
  68. package/src/ui/HoldToConfirm.tsx +2 -2
  69. package/src/ui/HoverCard.tsx +2 -1
  70. package/src/ui/Image.tsx +3 -2
  71. package/src/ui/InlineSelect.tsx +1 -1
  72. package/src/ui/InputGroup.tsx +2 -2
  73. package/src/ui/Item.tsx +1 -1
  74. package/src/ui/JsonViewer.tsx +2 -2
  75. package/src/ui/Kanban.tsx +1 -1
  76. package/src/ui/KpiBlock.tsx +1 -1
  77. package/src/ui/Lightbox.tsx +9 -6
  78. package/src/ui/MasterDetail.tsx +1 -1
  79. package/src/ui/Mentions.tsx +1 -1
  80. package/src/ui/Menubar.tsx +9 -8
  81. package/src/ui/Modal.tsx +3 -2
  82. package/src/ui/ModelPicker.tsx +2 -1
  83. package/src/ui/MultiSelect.tsx +5 -5
  84. package/src/ui/NativeSelect.tsx +2 -2
  85. package/src/ui/NavigationMenu.tsx +5 -4
  86. package/src/ui/NotificationCenter.tsx +1 -1
  87. package/src/ui/NotificationStack.tsx +1 -1
  88. package/src/ui/NumberInput.tsx +2 -2
  89. package/src/ui/OnboardingChecklist.tsx +2 -2
  90. package/src/ui/Pagination.tsx +2 -2
  91. package/src/ui/PillSearch.tsx +3 -3
  92. package/src/ui/PivotTable.tsx +0 -0
  93. package/src/ui/Popover.tsx +2 -1
  94. package/src/ui/Portal.tsx +3 -1
  95. package/src/ui/PresenceAvatars.tsx +4 -4
  96. package/src/ui/PricingTable.tsx +1 -1
  97. package/src/ui/Prose.tsx +3 -3
  98. package/src/ui/QueryBuilder.tsx +4 -4
  99. package/src/ui/Rating.tsx +6 -2
  100. package/src/ui/ReasoningTrace.tsx +1 -1
  101. package/src/ui/ScrollArea.tsx +1 -1
  102. package/src/ui/ScrollProgress.tsx +2 -2
  103. package/src/ui/SideRail.tsx +3 -3
  104. package/src/ui/Sidebar.tsx +43 -23
  105. package/src/ui/SlashMenu.tsx +1 -1
  106. package/src/ui/Snippet.tsx +6 -3
  107. package/src/ui/Sortable.tsx +1 -1
  108. package/src/ui/SpecSheet.tsx +1 -1
  109. package/src/ui/SpeedDial.tsx +1 -1
  110. package/src/ui/Splitter.tsx +8 -3
  111. package/src/ui/SpreadsheetGrid.tsx +4 -4
  112. package/src/ui/Stepper.tsx +1 -1
  113. package/src/ui/SuggestionChips.tsx +1 -1
  114. package/src/ui/Switch.tsx +1 -1
  115. package/src/ui/Table.tsx +5 -2
  116. package/src/ui/TimeField.tsx +8 -8
  117. package/src/ui/Timeline.tsx +2 -2
  118. package/src/ui/Toast.tsx +9 -3
  119. package/src/ui/ToolCallTimeline.tsx +2 -2
  120. package/src/ui/Tooltip.tsx +2 -1
  121. package/src/ui/Tour.tsx +1 -1
  122. package/src/ui/TransactionFeed.tsx +1 -1
  123. package/src/ui/Transfer.tsx +2 -2
  124. package/src/ui/Tree.tsx +2 -2
  125. package/src/ui/TreeSelect.tsx +2 -2
  126. package/src/ui/TreeTable.tsx +4 -4
  127. package/src/ui/UsageMeter.tsx +1 -1
  128. package/src/ui/VideoPlayerShell.tsx +2 -2
  129. package/src/ui/internal/CalendarCore.tsx +6 -6
@@ -120,7 +120,7 @@ export function JsonViewer(props: JsonViewerProps): JSX.Element {
120
120
 
121
121
  return (
122
122
  <Show when={visible()}>
123
- <li style={{ 'padding-left': `${nodeProps.depth * 1}rem` }}>
123
+ <li style={{ 'padding-inline-start': `${nodeProps.depth * 1}rem` }}>
124
124
  <Show
125
125
  when={isExpandable(value())}
126
126
  fallback={
@@ -143,7 +143,7 @@ export function JsonViewer(props: JsonViewerProps): JSX.Element {
143
143
  <ChevronRight
144
144
  class={cn(
145
145
  'h-3.5 w-3.5 shrink-0 self-center transition-transform duration-150',
146
- isOpen() && 'rotate-90',
146
+ isOpen() ? 'rotate-90' : 'rtl:-scale-x-100',
147
147
  )}
148
148
  aria-hidden="true"
149
149
  />
package/src/ui/Kanban.tsx CHANGED
@@ -6,7 +6,7 @@
6
6
  // itself (same as Sortable), so there's nothing to gate on reduced motion —
7
7
  // drops are instant either way.
8
8
  import { createEffect, createSignal, For, Show, type JSX } from 'solid-js'
9
- import { Portal } from 'solid-js/web'
9
+ import { Portal } from './Portal'
10
10
  import { GripVertical } from 'lucide-solid'
11
11
 
12
12
  import { cn } from '../lib/cn'
@@ -36,7 +36,7 @@ export function KpiBlock(props: KpiBlockProps): JSX.Element {
36
36
  <Card glass class={props.class}>
37
37
  <CardContent class="p-5">
38
38
  <p class="text-sm text-muted-foreground">{props.label}</p>
39
- <div class="mt-1 flex items-center gap-2">
39
+ <div class="mt-1 flex flex-wrap items-center gap-2">
40
40
  <span class="text-2xl font-semibold text-foreground">{props.value}</span>
41
41
  <Show when={props.delta !== undefined}>
42
42
  <Badge tone={(props.delta as number) >= 0 ? 'success' : 'danger'}>
@@ -9,7 +9,9 @@ import type { JSX } from 'solid-js'
9
9
  import { createEffect, createMemo, createSignal, For, onCleanup, Show } from 'solid-js'
10
10
 
11
11
  import { cn } from '../lib/cn'
12
+ import { readingKey } from '../lib/direction'
12
13
  import { motionReduced } from '../lib/motion'
14
+ import { useDirection } from './DirectionProvider'
13
15
  import { Portal } from './Portal'
14
16
 
15
17
  /** One image in a {@link Lightbox}. */
@@ -54,6 +56,7 @@ export interface LightboxProps {
54
56
  * ```
55
57
  */
56
58
  export function Lightbox(props: LightboxProps): JSX.Element {
59
+ const dir = useDirection()
57
60
  const [internalOpen, setInternalOpen] = createSignal(false)
58
61
  const open = createMemo(() => props.open ?? internalOpen())
59
62
 
@@ -109,10 +112,10 @@ export function Lightbox(props: LightboxProps): JSX.Element {
109
112
  if (e.key === 'Escape') {
110
113
  e.preventDefault()
111
114
  close()
112
- } else if (e.key === 'ArrowLeft') {
115
+ } else if (readingKey(e.key, dir()) === 'ArrowLeft') {
113
116
  e.preventDefault()
114
117
  prev()
115
- } else if (e.key === 'ArrowRight') {
118
+ } else if (readingKey(e.key, dir()) === 'ArrowRight') {
116
119
  e.preventDefault()
117
120
  next()
118
121
  }
@@ -175,9 +178,9 @@ export function Lightbox(props: LightboxProps): JSX.Element {
175
178
  e.stopPropagation()
176
179
  prev()
177
180
  }}
178
- class="absolute left-2 z-10 grid h-10 w-10 place-items-center rounded-full bg-card/80 text-foreground transition hover:bg-muted disabled:pointer-events-none disabled:opacity-40 sm:left-4"
181
+ class="absolute start-2 z-10 grid h-10 w-10 place-items-center rounded-full bg-card/80 text-foreground transition hover:bg-muted disabled:pointer-events-none disabled:opacity-40 sm:start-4"
179
182
  >
180
- <ChevronLeft class="h-6 w-6" />
183
+ <ChevronLeft class="h-6 w-6 rtl:-scale-x-100" />
181
184
  </button>
182
185
  </Show>
183
186
 
@@ -204,9 +207,9 @@ export function Lightbox(props: LightboxProps): JSX.Element {
204
207
  e.stopPropagation()
205
208
  next()
206
209
  }}
207
- class="absolute right-2 z-10 grid h-10 w-10 place-items-center rounded-full bg-card/80 text-foreground transition hover:bg-muted disabled:pointer-events-none disabled:opacity-40 sm:right-4"
210
+ class="absolute end-2 z-10 grid h-10 w-10 place-items-center rounded-full bg-card/80 text-foreground transition hover:bg-muted disabled:pointer-events-none disabled:opacity-40 sm:end-4"
208
211
  >
209
- <ChevronRight class="h-6 w-6" />
212
+ <ChevronRight class="h-6 w-6 rtl:-scale-x-100" />
210
213
  </button>
211
214
  </Show>
212
215
  </div>
@@ -90,7 +90,7 @@ export function MasterDetail(props: MasterDetailProps): JSX.Element {
90
90
  tabindex={-1}
91
91
  onClick={() => select(item.id)}
92
92
  class={cn(
93
- 'flex w-full items-center justify-between gap-2 rounded-lg px-3 py-2 text-left text-sm transition-colors',
93
+ 'flex w-full items-center justify-between gap-2 rounded-lg px-3 py-2 text-start text-sm transition-colors',
94
94
  isSelected() ? 'bg-muted text-foreground' : 'text-muted-foreground hover:bg-muted/60',
95
95
  )}
96
96
  >
@@ -150,7 +150,7 @@ export function Mentions(props: MentionsProps): JSX.Element {
150
150
  <button
151
151
  type="button"
152
152
  class={cn(
153
- 'w-full rounded px-2 py-1.5 text-left text-sm hover:bg-muted',
153
+ 'w-full rounded px-2 py-1.5 text-start text-sm hover:bg-muted',
154
154
  i() === highlighted() && 'bg-muted',
155
155
  )}
156
156
  onMouseDown={(ev) => {
@@ -8,12 +8,13 @@ import type { JSX, ParentProps } from 'solid-js'
8
8
  import { For, Show, splitProps } from 'solid-js'
9
9
 
10
10
  import { cn } from '../lib/cn'
11
+ import { portalDir } from '../lib/direction'
11
12
  import { Kbd } from './Kbd'
12
13
 
13
14
  const CONTENT =
14
15
  'bg-glass z-50 min-w-[12rem] overflow-hidden rounded-xl p-1 text-sm text-foreground shadow-xl outline-none'
15
16
  const ITEM =
16
- 'relative flex cursor-pointer select-none items-center gap-2 rounded-md py-1.5 pl-8 pr-2 outline-none transition-colors data-[highlighted]:bg-muted data-[expanded]:bg-muted data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus-visible:ring-2 focus-visible:ring-ring'
17
+ 'relative flex cursor-pointer select-none items-center gap-2 rounded-md py-1.5 ps-8 pe-2 outline-none transition-colors data-[highlighted]:bg-muted data-[expanded]:bg-muted data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus-visible:ring-2 focus-visible:ring-ring'
17
18
 
18
19
  interface ClassProps extends ParentProps {
19
20
  class?: string
@@ -74,7 +75,7 @@ export function MenubarMenu(props: MenubarMenuProps): JSX.Element {
74
75
  >
75
76
  {props.label}
76
77
  </KMenubar.Trigger>
77
- <KMenubar.Portal>
78
+ <KMenubar.Portal ref={portalDir}>
78
79
  <KMenubar.Content class={cn(CONTENT, props.contentClass)}>{props.children}</KMenubar.Content>
79
80
  </KMenubar.Portal>
80
81
  </KMenubar.Menu>
@@ -95,7 +96,7 @@ export interface MenubarItemProps extends ClassProps {
95
96
  function Shortcut(props: { keys?: string[] }): JSX.Element {
96
97
  return (
97
98
  <Show when={props.keys?.length}>
98
- <span class="ml-auto flex gap-0.5 pl-6" aria-hidden="true">
99
+ <span class="ms-auto flex gap-0.5 ps-6" aria-hidden="true">
99
100
  <For each={props.keys}>{(k) => <Kbd class="min-w-[1.25rem] px-1 py-0 text-[11px]">{k}</Kbd>}</For>
100
101
  </span>
101
102
  </Show>
@@ -133,7 +134,7 @@ export function MenubarCheckboxItem(props: MenubarCheckboxItemProps): JSX.Elemen
133
134
  onChange={(v) => props.onChange?.(v)}
134
135
  disabled={props.disabled}
135
136
  >
136
- <KMenubar.ItemIndicator class="absolute left-2 inline-flex items-center">
137
+ <KMenubar.ItemIndicator class="absolute start-2 inline-flex items-center">
137
138
  <Check class="h-3.5 w-3.5" />
138
139
  </KMenubar.ItemIndicator>
139
140
  {props.children}
@@ -167,7 +168,7 @@ export interface MenubarRadioItemProps extends ClassProps {
167
168
  export function MenubarRadioItem(props: MenubarRadioItemProps): JSX.Element {
168
169
  return (
169
170
  <KMenubar.RadioItem class={cn(ITEM, props.class)} value={props.value} disabled={props.disabled}>
170
- <KMenubar.ItemIndicator class="absolute left-2 inline-flex items-center">
171
+ <KMenubar.ItemIndicator class="absolute start-2 inline-flex items-center">
171
172
  <Circle class="h-2 w-2 fill-current" />
172
173
  </KMenubar.ItemIndicator>
173
174
  {props.children}
@@ -187,9 +188,9 @@ export function MenubarSub(props: MenubarSubProps): JSX.Element {
187
188
  <KMenubar.Sub overlap gutter={4} shift={-5}>
188
189
  <KMenubar.SubTrigger class={cn(ITEM, props.class)}>
189
190
  {props.label}
190
- <ChevronRight class="ml-auto h-3.5 w-3.5 text-muted-foreground" />
191
+ <ChevronRight class="ms-auto h-3.5 w-3.5 text-muted-foreground rtl:-scale-x-100" />
191
192
  </KMenubar.SubTrigger>
192
- <KMenubar.Portal>
193
+ <KMenubar.Portal ref={portalDir}>
193
194
  <KMenubar.SubContent class={CONTENT}>{props.children}</KMenubar.SubContent>
194
195
  </KMenubar.Portal>
195
196
  </KMenubar.Sub>
@@ -205,7 +206,7 @@ export function MenubarSeparator(props: { class?: string }): JSX.Element {
205
206
  export function MenubarLabel(props: ClassProps): JSX.Element {
206
207
  const [local] = splitProps(props, ['class', 'children'])
207
208
  return (
208
- <div class={cn('px-2 py-1.5 pl-8 text-xs font-semibold text-muted-foreground', local.class)}>
209
+ <div class={cn('px-2 py-1.5 ps-8 text-xs font-semibold text-muted-foreground', local.class)}>
209
210
  {local.children}
210
211
  </div>
211
212
  )
package/src/ui/Modal.tsx CHANGED
@@ -14,6 +14,7 @@ import type { JSX, ParentProps } from 'solid-js'
14
14
  import { Show } from 'solid-js'
15
15
 
16
16
  import { cn } from '../lib/cn'
17
+ import { portalDir } from '../lib/direction'
17
18
 
18
19
  export interface ModalProps extends ParentProps {
19
20
  open: boolean
@@ -79,7 +80,7 @@ export function Modal(props: ModalProps): JSX.Element {
79
80
  const isCenter = () => props.variant === 'center'
80
81
  return (
81
82
  <Dialog.Root open={props.open} onOpenChange={props.onOpenChange}>
82
- <Dialog.Portal>
83
+ <Dialog.Portal ref={portalDir}>
83
84
  <Dialog.Overlay class="modal-overlay fixed inset-0 z-40 bg-background/80 backdrop-blur-sm" />
84
85
  <Show
85
86
  when={isCenter()}
@@ -88,7 +89,7 @@ export function Modal(props: ModalProps): JSX.Element {
88
89
  <Dialog.Content
89
90
  onInteractOutside={keepForFloating}
90
91
  class={cn(
91
- 'drawer-content bg-glass flex h-full w-full flex-col overflow-y-auto border-l border-border text-card-foreground shadow-2xl',
92
+ 'drawer-content bg-glass flex h-full w-full flex-col overflow-y-auto border-s border-border text-card-foreground shadow-2xl',
92
93
  DRAWER_WIDTH[props.size ?? 'md'],
93
94
  props.class,
94
95
  )}
@@ -10,6 +10,7 @@ import type { JSX } from 'solid-js'
10
10
  import { For, Show, createSignal, untrack } from 'solid-js'
11
11
 
12
12
  import { cn } from '../lib/cn'
13
+ import { portalDir } from '../lib/direction'
13
14
 
14
15
  /** A single selectable model in a {@link ModelPicker} menu. */
15
16
  export interface ModelOption {
@@ -78,7 +79,7 @@ export function ModelPicker(props: ModelPickerProps): JSX.Element {
78
79
  <span class="truncate font-medium">{selectedModel()?.name}</span>
79
80
  <ChevronDown class="h-4 w-4 shrink-0 text-muted-foreground" />
80
81
  </DropdownMenu.Trigger>
81
- <DropdownMenu.Portal>
82
+ <DropdownMenu.Portal ref={portalDir}>
82
83
  <DropdownMenu.Content class="z-50 min-w-64 overflow-hidden rounded-md border border-border bg-card p-1 text-card-foreground shadow-sm">
83
84
  <DropdownMenu.RadioGroup value={selectedId()} onChange={handleChange}>
84
85
  <For each={props.models}>
@@ -6,9 +6,9 @@
6
6
  import { Check, ChevronDown, X } from 'lucide-solid'
7
7
  import type { JSX } from 'solid-js'
8
8
  import { For, Show, createEffect, createMemo, createSignal, onCleanup } from 'solid-js'
9
- import { Portal } from 'solid-js/web'
9
+ import { Portal } from './Portal'
10
10
 
11
- import { createAnchorReflow } from '../lib/anchored'
11
+ import { anchoredLeft, createAnchorReflow } from '../lib/anchored'
12
12
  import { cn } from '../lib/cn'
13
13
 
14
14
  /** One selectable option: a stable `value` and its display `label`. */
@@ -66,7 +66,7 @@ export function MultiSelect(props: MultiSelectProps): JSX.Element {
66
66
  if (!btnRef) return
67
67
  const r = btnRef.getBoundingClientRect()
68
68
  const width = Math.max(r.width, MIN_W)
69
- const left = Math.max(8, Math.min(r.left, window.innerWidth - width - 8))
69
+ const left = anchoredLeft(btnRef, width)
70
70
  setPos({ top: r.bottom + 4, left, width })
71
71
  }
72
72
 
@@ -151,7 +151,7 @@ export function MultiSelect(props: MultiSelectProps): JSX.Element {
151
151
  aria-haspopup="listbox"
152
152
  aria-expanded={open()}
153
153
  onClick={() => (open() ? close() : openPopover())}
154
- class="flex min-h-9 h-auto w-full flex-wrap items-center justify-between gap-2 rounded-md border border-input bg-background px-3 py-2 text-left text-sm text-foreground outline-none transition-colors a4-field disabled:cursor-not-allowed disabled:opacity-50"
154
+ class="flex min-h-9 h-auto w-full flex-wrap items-center justify-between gap-2 rounded-md border border-input bg-background px-3 py-2 text-start text-sm text-foreground outline-none transition-colors a4-field disabled:cursor-not-allowed disabled:opacity-50"
155
155
  >
156
156
  <span class="flex flex-1 flex-wrap items-center gap-1">
157
157
  <Show
@@ -217,7 +217,7 @@ export function MultiSelect(props: MultiSelectProps): JSX.Element {
217
217
  aria-pressed={isSelected()}
218
218
  onClick={() => toggleValue(opt.value)}
219
219
  class={cn(
220
- 'flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm outline-none transition-colors hover:bg-muted focus:bg-muted',
220
+ 'flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-start text-sm outline-none transition-colors hover:bg-muted focus:bg-muted',
221
221
  isSelected() && 'bg-primary/10',
222
222
  )}
223
223
  >
@@ -48,7 +48,7 @@ export function NativeSelect(props: NativeSelectProps): JSX.Element {
48
48
  ref={select}
49
49
  aria-invalid={local.invalid ? 'true' : undefined}
50
50
  class={cn(
51
- 'peer w-full appearance-none rounded-md border bg-background pl-3 pr-9 text-foreground outline-none transition-colors a4-field disabled:cursor-not-allowed disabled:opacity-50',
51
+ 'peer w-full appearance-none rounded-md border bg-background ps-3 pe-9 text-foreground outline-none transition-colors a4-field disabled:cursor-not-allowed disabled:opacity-50',
52
52
  local.invalid ? 'border-destructive' : 'border-input',
53
53
  SIZE[local.size ?? 'default'],
54
54
  local.class,
@@ -59,7 +59,7 @@ export function NativeSelect(props: NativeSelectProps): JSX.Element {
59
59
  </select>
60
60
  <ChevronDown
61
61
  aria-hidden="true"
62
- class="pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground peer-disabled:opacity-50"
62
+ class="pointer-events-none absolute end-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground peer-disabled:opacity-50"
63
63
  />
64
64
  </div>
65
65
  )
@@ -8,6 +8,7 @@ import type { JSX, ParentProps } from 'solid-js'
8
8
  import { createSignal, For, Show } from 'solid-js'
9
9
 
10
10
  import { cn } from '../lib/cn'
11
+ import { portalDir } from '../lib/direction'
11
12
  import { useMediaQuery } from '../lib/media'
12
13
  import { Accordion } from './Accordion'
13
14
  import { Button } from './Button'
@@ -91,7 +92,7 @@ function MobileNavigation(props: NavigationMenuProps): JSX.Element {
91
92
  >
92
93
  <MenuIcon class="h-4 w-4" />
93
94
  </Button>
94
- <Drawer side="left" open={open()} onOpenChange={setOpen} title={props.mobileTitle ?? 'Menu'}>
95
+ <Drawer side="start" open={open()} onOpenChange={setOpen} title={props.mobileTitle ?? 'Menu'}>
95
96
  <div
96
97
  class="p-5"
97
98
  onKeyDown={(e) => {
@@ -188,10 +189,10 @@ export function NavigationMenuContent(props: NavigationMenuContentProps): JSX.El
188
189
  else e.currentTarget.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
189
190
  }
190
191
  return (
191
- <KNav.Portal>
192
+ <KNav.Portal ref={portalDir}>
192
193
  <KNav.Content
193
194
  class={cn(
194
- 'navmenu-content absolute left-0 top-0 w-max max-w-[min(90vw,40rem)] p-3 outline-none',
195
+ 'navmenu-content absolute start-0 top-0 w-max max-w-[min(90vw,40rem)] p-3 outline-none',
195
196
  props.class,
196
197
  )}
197
198
  onKeyDown={onTab}
@@ -237,7 +238,7 @@ export function NavigationMenuPanelLink(props: NavigationMenuPanelLinkProps): JS
237
238
  <KNav.Item
238
239
  href={props.href}
239
240
  class={cn(
240
- 'flex gap-3 rounded-xl p-3 text-left no-underline outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring',
241
+ 'flex gap-3 rounded-xl p-3 text-start no-underline outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring',
241
242
  props.featured
242
243
  ? 'flex-col justify-end bg-gradient-to-br from-primary/30 to-accent/20 hover:from-primary/40'
243
244
  : 'items-start hover:bg-muted data-[highlighted]:bg-muted',
@@ -44,7 +44,7 @@ export function NotificationCenter(props: NotificationCenterProps): JSX.Element
44
44
  <span class={cn('relative inline-grid place-items-center rounded-md p-2 text-foreground', props.class)}>
45
45
  <Bell class="h-5 w-5" />
46
46
  <Show when={unread() > 0}>
47
- <span class="absolute -right-1 -top-1 grid h-4 min-w-4 place-items-center rounded-full bg-primary px-1 text-[10px] text-primary-foreground">
47
+ <span class="absolute -end-1 -top-1 grid h-4 min-w-4 place-items-center rounded-full bg-primary px-1 text-[10px] text-primary-foreground">
48
48
  {unread()}
49
49
  </span>
50
50
  </Show>
@@ -50,7 +50,7 @@ export function NotificationStack(props: NotificationStackProps): JSX.Element {
50
50
  <div
51
51
  class={cn(
52
52
  expanded()
53
- ? 'max-h-[60vh] space-y-2 overflow-y-auto pr-1'
53
+ ? 'max-h-[60vh] space-y-2 overflow-y-auto pe-1'
54
54
  : cn('relative', canExpand() && 'cursor-pointer'),
55
55
  )}
56
56
  style={
@@ -38,14 +38,14 @@ export function NumberInput(props: NumberInputProps): JSX.Element {
38
38
  maxValue={props.max}
39
39
  class={cn('inline-flex items-stretch', props.class)}
40
40
  >
41
- <KNumberField.DecrementTrigger class={cn(TRIGGER_CLASS, 'rounded-l-md')} aria-label="Decrement">
41
+ <KNumberField.DecrementTrigger class={cn(TRIGGER_CLASS, 'rounded-s-md')} aria-label="Decrement">
42
42
  −
43
43
  </KNumberField.DecrementTrigger>
44
44
  <KNumberField.Input
45
45
  aria-label={props['aria-label']}
46
46
  class="w-16 border-y border-input bg-background px-3 py-2 text-center text-sm text-foreground outline-none transition-colors a4-field"
47
47
  />
48
- <KNumberField.IncrementTrigger class={cn(TRIGGER_CLASS, 'rounded-r-md')} aria-label="Increment">
48
+ <KNumberField.IncrementTrigger class={cn(TRIGGER_CLASS, 'rounded-e-md')} aria-label="Increment">
49
49
  +
50
50
  </KNumberField.IncrementTrigger>
51
51
  </KNumberField>
@@ -118,7 +118,7 @@ export function OnboardingChecklist(props: OnboardingChecklistProps): JSX.Elemen
118
118
  </button>
119
119
  <button
120
120
  type="button"
121
- class="min-w-0 flex-1 text-left"
121
+ class="min-w-0 flex-1 text-start"
122
122
  aria-expanded={hasDetail() ? isExpanded() : undefined}
123
123
  disabled={!hasDetail()}
124
124
  onClick={() => hasDetail() && toggleExpanded(step.id)}
@@ -141,7 +141,7 @@ export function OnboardingChecklist(props: OnboardingChecklistProps): JSX.Elemen
141
141
  )}
142
142
  >
143
143
  <div class="overflow-hidden">
144
- <div class="mt-2 pl-8 text-sm text-muted-foreground">
144
+ <div class="mt-2 ps-8 text-sm text-muted-foreground">
145
145
  <Show when={step.description}>{step.description}</Show>
146
146
  <Show when={step.action}>
147
147
  {(action) => (
@@ -42,7 +42,7 @@ export function Pagination(props: PaginationProps): JSX.Element {
42
42
  aria-label={props.labels?.previous ?? 'Previous page'}
43
43
  onClick={() => props.onChange(props.page - 1)}
44
44
  >
45
- <ChevronLeft class="h-4 w-4" />
45
+ <ChevronLeft class="h-4 w-4 rtl:-scale-x-100" />
46
46
  </button>
47
47
  <span class="px-2 font-mono text-[12px] tabular-nums">
48
48
  {(props.labels?.page ?? ((p, t) => `Page ${p} of ${t}`))(props.page, props.totalPages)}
@@ -54,7 +54,7 @@ export function Pagination(props: PaginationProps): JSX.Element {
54
54
  aria-label={props.labels?.next ?? 'Next page'}
55
55
  onClick={() => props.onChange(props.page + 1)}
56
56
  >
57
- <ChevronRight class="h-4 w-4" />
57
+ <ChevronRight class="h-4 w-4 rtl:-scale-x-100" />
58
58
  </button>
59
59
  </div>
60
60
  </div>
@@ -60,8 +60,8 @@ export function PillSearch(props: PillSearchProps): JSX.Element {
60
60
  aria-label={field.label}
61
61
  onClick={() => props.onFieldClick?.(field.key)}
62
62
  class={cn(
63
- 'flex min-w-[6.5rem] shrink-0 flex-col items-start px-4 py-2 text-left transition-colors duration-150 hover:bg-muted focus:outline-none focus:ring-2 focus:ring-ring',
64
- index() === 0 ? 'rounded-l-full' : 'border-l border-border',
63
+ 'flex min-w-[6.5rem] shrink-0 flex-col items-start px-4 py-2 text-start transition-colors duration-150 hover:bg-muted focus:outline-none focus:ring-2 focus:ring-ring',
64
+ index() === 0 ? 'rounded-s-full' : 'border-s border-border',
65
65
  )}
66
66
  >
67
67
  <span class="text-xs font-medium text-muted-foreground">{field.label}</span>
@@ -71,7 +71,7 @@ export function PillSearch(props: PillSearchProps): JSX.Element {
71
71
  </button>
72
72
  )}
73
73
  </For>
74
- <div class="shrink-0 py-1.5 pl-1 pr-1.5">
74
+ <div class="shrink-0 py-1.5 ps-1 pe-1.5">
75
75
  <button
76
76
  type="button"
77
77
  aria-label="Search"
Binary file
@@ -3,6 +3,7 @@ import { Popover as KPopover } from '@kobalte/core/popover'
3
3
  import type { JSX, ParentProps } from 'solid-js'
4
4
 
5
5
  import { cn } from '../lib/cn'
6
+ import { portalDir } from '../lib/direction'
6
7
 
7
8
  interface PopoverProps extends ParentProps {
8
9
  /** Element that opens the popover on click; wrapped in an inline-flex trigger. */
@@ -26,7 +27,7 @@ export function Popover(props: PopoverProps): JSX.Element {
26
27
  return (
27
28
  <KPopover>
28
29
  <KPopover.Trigger class="inline-flex">{props.trigger}</KPopover.Trigger>
29
- <KPopover.Portal>
30
+ <KPopover.Portal ref={portalDir}>
30
31
  <KPopover.Content
31
32
  class={cn(
32
33
  'bg-glass z-50 rounded-lg border border-border p-4 text-card-foreground shadow-lg',
package/src/ui/Portal.tsx CHANGED
@@ -4,6 +4,8 @@
4
4
  import type { JSX } from 'solid-js'
5
5
  import { Portal as SolidPortal } from 'solid-js/web'
6
6
 
7
+ import { portalDir } from '../lib/direction'
8
+
7
9
  /** Props for {@link Portal}. */
8
10
  export interface PortalProps {
9
11
  /** Where to mount. Defaults to `document.body`. */
@@ -27,7 +29,7 @@ export interface PortalProps {
27
29
  */
28
30
  export function Portal(props: PortalProps): JSX.Element {
29
31
  return (
30
- <SolidPortal mount={props.mount} useShadow={props.useShadow}>
32
+ <SolidPortal mount={props.mount} useShadow={props.useShadow} ref={portalDir}>
31
33
  {props.children}
32
34
  </SolidPortal>
33
35
  )
@@ -85,19 +85,19 @@ export function PresenceAvatars(props: PresenceAvatarsProps): JSX.Element {
85
85
  <For each={shown()}>
86
86
  {(user, index) => (
87
87
  <div
88
- class={cn('relative rounded-full ring-2 ring-background', index() > 0 && '-ml-2')}
88
+ class={cn('relative rounded-full ring-2 ring-background', index() > 0 && '-ms-2')}
89
89
  title={user.name}
90
90
  >
91
91
  <Avatar src={user.avatar} alt={user.name} fallback={initials(user.name)} />
92
92
  <span
93
93
  aria-hidden="true"
94
- class="absolute bottom-0 right-0 h-2.5 w-2.5 rounded-full bg-emerald-500 ring-2 ring-background"
94
+ class="absolute bottom-0 end-0 h-2.5 w-2.5 rounded-full bg-emerald-500 ring-2 ring-background"
95
95
  />
96
96
  </div>
97
97
  )}
98
98
  </For>
99
99
  <Show when={overflow() > 0}>
100
- <div class="-ml-2 rounded-full ring-2 ring-background">
100
+ <div class="-ms-2 rounded-full ring-2 ring-background">
101
101
  <span class="grid h-9 w-9 shrink-0 place-items-center rounded-full bg-muted text-xs font-medium text-muted-foreground">
102
102
  +{overflow()}
103
103
  </span>
@@ -125,7 +125,7 @@ export function PresenceAvatars(props: PresenceAvatarsProps): JSX.Element {
125
125
  />
126
126
  </svg>
127
127
  <span
128
- class="ml-1.5 whitespace-nowrap rounded-full px-2 py-0.5 text-xs font-medium text-white shadow"
128
+ class="ms-1.5 whitespace-nowrap rounded-full px-2 py-0.5 text-xs font-medium text-white shadow"
129
129
  style={{ 'background-color': color() }}
130
130
  >
131
131
  {user()!.name}
@@ -131,7 +131,7 @@ export function PricingTable(props: PricingTableProps): JSX.Element {
131
131
  class={cn('relative flex flex-col', tier.highlighted && 'ring-2 ring-primary')}
132
132
  >
133
133
  <Show when={tier.highlighted}>
134
- <Badge tone="info" class="absolute -top-3 right-6">
134
+ <Badge tone="info" class="absolute -top-3 end-6">
135
135
  Popular
136
136
  </Badge>
137
137
  </Show>
package/src/ui/Prose.tsx CHANGED
@@ -29,14 +29,14 @@ const PROSE =
29
29
  '[&_h4]:mb-2 [&_h4]:mt-4 [&_h4]:font-semibold ' +
30
30
  '[&_p]:my-4 [&_.lead]:text-muted-foreground ' +
31
31
  '[&_a]:font-medium [&_a]:text-primary [&_a]:underline [&_a]:underline-offset-4 [&_a:hover]:opacity-80 ' +
32
- '[&_blockquote]:my-6 [&_blockquote]:border-l-2 [&_blockquote]:border-border [&_blockquote]:pl-4 [&_blockquote]:italic [&_blockquote]:text-muted-foreground ' +
33
- '[&_ul]:my-4 [&_ul]:list-disc [&_ul]:pl-6 [&_ol]:my-4 [&_ol]:list-decimal [&_ol]:pl-6 [&_li]:my-1 ' +
32
+ '[&_blockquote]:my-6 [&_blockquote]:border-s-2 [&_blockquote]:border-border [&_blockquote]:ps-4 [&_blockquote]:italic [&_blockquote]:text-muted-foreground ' +
33
+ '[&_ul]:my-4 [&_ul]:list-disc [&_ul]:ps-6 [&_ol]:my-4 [&_ol]:list-decimal [&_ol]:ps-6 [&_li]:my-1 ' +
34
34
  '[&_code]:rounded [&_code]:bg-muted [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:font-mono [&_code]:text-[0.875em] ' +
35
35
  '[&_pre]:my-4 [&_pre]:overflow-x-auto [&_pre]:rounded-lg [&_pre]:border [&_pre]:border-border [&_pre]:bg-muted [&_pre]:p-4 ' +
36
36
  '[&_pre_code]:bg-transparent [&_pre_code]:p-0 ' +
37
37
  '[&_hr]:my-8 [&_hr]:border-border ' +
38
38
  '[&_table]:my-6 [&_table]:w-full [&_table]:border-collapse [&_table]:text-sm ' +
39
- '[&_th]:border-b [&_th]:border-border [&_th]:px-3 [&_th]:py-2 [&_th]:text-left [&_th]:font-semibold ' +
39
+ '[&_th]:border-b [&_th]:border-border [&_th]:px-3 [&_th]:py-2 [&_th]:text-start [&_th]:font-semibold ' +
40
40
  '[&_td]:border-b [&_td]:border-border [&_td]:px-3 [&_td]:py-2'
41
41
 
42
42
  /**
@@ -143,7 +143,7 @@ function GroupView(props: GroupViewProps): JSX.Element {
143
143
  }
144
144
 
145
145
  return (
146
- <div class={cn('flex flex-col gap-2 rounded-md p-2', props.depth > 0 && 'border-l-2 border-border pl-3')}>
146
+ <div class={cn('flex flex-col gap-2 rounded-md p-2', props.depth > 0 && 'border-s-2 border-border ps-3')}>
147
147
  <div class="flex flex-wrap items-center gap-2">
148
148
  <div
149
149
  class="inline-flex overflow-hidden rounded-md border border-input"
@@ -169,18 +169,18 @@ function GroupView(props: GroupViewProps): JSX.Element {
169
169
  </For>
170
170
  </div>
171
171
  <Button variant="outline" onClick={addRule}>
172
- <Plus class="mr-1 h-3.5 w-3.5" aria-hidden="true" />
172
+ <Plus class="me-1 h-3.5 w-3.5" aria-hidden="true" />
173
173
  Rule
174
174
  </Button>
175
175
  <Button variant="outline" onClick={addGroup}>
176
- <Plus class="mr-1 h-3.5 w-3.5" aria-hidden="true" />
176
+ <Plus class="me-1 h-3.5 w-3.5" aria-hidden="true" />
177
177
  Group
178
178
  </Button>
179
179
  <Show when={props.onRemove}>
180
180
  {(remove) => (
181
181
  <button
182
182
  type="button"
183
- class="ml-auto rounded-md p-1.5 text-muted-foreground hover:bg-muted hover:text-foreground"
183
+ class="ms-auto rounded-md p-1.5 text-muted-foreground hover:bg-muted hover:text-foreground"
184
184
  aria-label="Remove group"
185
185
  onClick={() => remove()()}
186
186
  >
package/src/ui/Rating.tsx CHANGED
@@ -6,6 +6,8 @@ import type { JSX } from 'solid-js'
6
6
  import { createSignal, For, Show } from 'solid-js'
7
7
 
8
8
  import { cn } from '../lib/cn'
9
+ import { readingKey } from '../lib/direction'
10
+ import { useDirection } from './DirectionProvider'
9
11
 
10
12
  export interface RatingProps {
11
13
  /** Currently selected rating, from 0 to `max`. */
@@ -30,6 +32,7 @@ export interface RatingProps {
30
32
  * ```
31
33
  */
32
34
  export function Rating(props: RatingProps): JSX.Element {
35
+ const dir = useDirection()
33
36
  const [hover, setHover] = createSignal(0)
34
37
  const max = () => props.max ?? 5
35
38
  const stars = () => Array.from({ length: max() }, (_, i) => i + 1)
@@ -79,10 +82,11 @@ export function Rating(props: RatingProps): JSX.Element {
79
82
  onBlur={() => setHover(0)}
80
83
  onClick={() => set(n)}
81
84
  onKeyDown={(e) => {
82
- if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {
85
+ const key = readingKey(e.key, dir())
86
+ if (key === 'ArrowRight' || key === 'ArrowUp') {
83
87
  e.preventDefault()
84
88
  set(Math.min(props.value + 1, max()))
85
- } else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
89
+ } else if (key === 'ArrowLeft' || key === 'ArrowDown') {
86
90
  e.preventDefault()
87
91
  set(Math.max(props.value - 1, 0))
88
92
  }
@@ -46,7 +46,7 @@ export function ReasoningTrace(props: ReasoningTraceProps): JSX.Element {
46
46
  class="flex w-full items-center gap-2 rounded-xl px-3 py-2 text-sm font-medium text-muted-foreground hover:bg-muted"
47
47
  >
48
48
  <Brain class="h-4 w-4 shrink-0" aria-hidden="true" />
49
- <span class="flex-1 text-left">{props.label ?? 'Reasoning'}</span>
49
+ <span class="flex-1 text-start">{props.label ?? 'Reasoning'}</span>
50
50
  <Show when={props.streaming}>
51
51
  <span class="relative flex h-1.5 w-1.5 shrink-0" aria-hidden="true">
52
52
  <span class="absolute inline-flex h-full w-full animate-ping rounded-full bg-current opacity-75 motion-reduce:animate-none" />
@@ -59,7 +59,7 @@ export function ScrollArea(props: ScrollAreaProps): JSX.Element {
59
59
  '[scrollbar-color:hsl(var(--muted-foreground)/0.35)_transparent] [scrollbar-width:thin]',
60
60
  '[&::-webkit-scrollbar]:h-2 [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-track]:bg-transparent',
61
61
  '[&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-muted-foreground/35',
62
- 'hover:[&::-webkit-scrollbar-thumb]:bg-muted-foreground/55',
62
+ '[&::-webkit-scrollbar-thumb:hover]:bg-muted-foreground/55',
63
63
  AXIS[local.orientation ?? 'vertical'],
64
64
  local.class,
65
65
  )}
@@ -44,12 +44,12 @@ export function ScrollProgress(props: ScrollProgressProps): JSX.Element {
44
44
  return (
45
45
  <div
46
46
  aria-hidden="true"
47
- class={cn('fixed top-0 left-0 z-[9999] w-full', props.class)}
47
+ class={cn('fixed top-0 start-0 z-[9999] w-full', props.class)}
48
48
  style={{ height: `${props.height ?? 3}px` }}
49
49
  >
50
50
  <div
51
51
  ref={bar}
52
- class="origin-left will-change-transform"
52
+ class="origin-left rtl:origin-right will-change-transform"
53
53
  style={{
54
54
  height: '100%',
55
55
  width: '100%',