@a4ui/core 0.38.0 → 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 +8 -4
  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 +9 -5
  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 +29 -8
  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 +9 -5
  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 +9 -5
  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
@@ -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%',
@@ -60,7 +60,7 @@ export function SideRail(props: SideRailProps): JSX.Element {
60
60
  <div
61
61
  role="tablist"
62
62
  aria-orientation="vertical"
63
- class={cn('flex w-20 flex-col items-stretch gap-1 border-r border-border bg-glass py-2', props.class)}
63
+ class={cn('flex w-20 flex-col items-stretch gap-1 border-e border-border bg-glass py-2', props.class)}
64
64
  >
65
65
  <For each={props.items}>
66
66
  {(item, index) => {
@@ -92,14 +92,14 @@ export function SideRail(props: SideRailProps): JSX.Element {
92
92
  <span
93
93
  aria-hidden="true"
94
94
  class={cn(
95
- 'absolute inset-y-1 left-0 w-0.5 rounded-full bg-primary transition-opacity',
95
+ 'absolute inset-y-1 start-0 w-0.5 rounded-full bg-primary transition-opacity',
96
96
  active() ? 'opacity-100' : 'opacity-0',
97
97
  )}
98
98
  />
99
99
  <span class="relative inline-flex">
100
100
  {item.icon}
101
101
  <Show when={item.badge}>
102
- <span class="absolute -right-2 -top-2">{item.badge}</span>
102
+ <span class="absolute -end-2 -top-2">{item.badge}</span>
103
103
  </Show>
104
104
  </span>
105
105
  <Show when={showLabels()}>
@@ -23,11 +23,20 @@ import {
23
23
  } from 'solid-js'
24
24
 
25
25
  import { cn } from '../lib/cn'
26
+ import { portalDir } from '../lib/direction'
27
+ import { useDirection } from './DirectionProvider'
26
28
  import { useMediaQuery } from '../lib/media'
27
29
  import { Drawer } from './Drawer'
28
30
  import { ScrollArea } from './ScrollArea'
29
31
 
30
- export type SidebarSide = 'left' | 'right'
32
+ /**
33
+ * Which edge the sidebar sits on. The layout is a flex row in DOM order, so the
34
+ * edge follows the reading direction: `start` (alias `left`) is the leading
35
+ * edge, `end` (alias `right`) the trailing one. Under `dir="rtl"` `start` is the
36
+ * right-hand side.
37
+ */
38
+ export type SidebarSide = 'start' | 'end' | 'left' | 'right'
39
+ const isStart = (side: SidebarSide): boolean => side === 'start' || side === 'left'
31
40
  export type SidebarVariant = 'sidebar' | 'floating' | 'inset'
32
41
  export type SidebarCollapsible = 'offcanvas' | 'icon' | 'none'
33
42
 
@@ -95,7 +104,7 @@ export function SidebarProvider(props: SidebarProviderProps): JSX.Element {
95
104
  const [internal, setInternal] = createSignal(props.defaultOpen ?? true)
96
105
  const [openMobile, setOpenMobile] = createSignal(false)
97
106
  const [config, setConfig] = createSignal<ReturnType<SidebarContextValue['config']>>({
98
- side: 'left',
107
+ side: 'start',
99
108
  variant: 'sidebar',
100
109
  collapsible: 'offcanvas',
101
110
  })
@@ -144,7 +153,7 @@ export function SidebarProvider(props: SidebarProviderProps): JSX.Element {
144
153
  }
145
154
 
146
155
  interface SidebarProps extends ParentProps {
147
- /** Which edge it sits on. @default 'left' */
156
+ /** Which edge it sits on; follows `dir` (see {@link SidebarSide}). @default 'start' */
148
157
  side?: SidebarSide
149
158
  /** `sidebar` is flush, `floating` is a detached rounded panel, `inset` has no surface of its own (pair with `SidebarInset`). @default 'sidebar' */
150
159
  variant?: SidebarVariant
@@ -169,7 +178,7 @@ interface SidebarProps extends ParentProps {
169
178
  */
170
179
  export function Sidebar(props: SidebarProps): JSX.Element {
171
180
  const ctx = useSidebarInternal()
172
- const side = () => props.side ?? 'left'
181
+ const side = () => props.side ?? 'start'
173
182
  const variant = () => props.variant ?? 'sidebar'
174
183
  const collapsible = () => props.collapsible ?? 'offcanvas'
175
184
  createEffect(() => ctx.setConfig({ side: side(), variant: variant(), collapsible: collapsible() }))
@@ -185,7 +194,7 @@ export function Sidebar(props: SidebarProps): JSX.Element {
185
194
  class={cn(
186
195
  'relative flex h-full w-full min-w-0 flex-col',
187
196
  variant() === 'sidebar' &&
188
- cn('bg-glass', side() === 'left' ? 'border-r border-border' : 'border-l border-border'),
197
+ cn('bg-glass', isStart(side()) ? 'border-e border-border' : 'border-s border-border'),
189
198
  variant() === 'floating' && 'rounded-xl border border-border bg-glass shadow-lg',
190
199
  props.class,
191
200
  )}
@@ -202,7 +211,7 @@ export function Sidebar(props: SidebarProps): JSX.Element {
202
211
  <Drawer
203
212
  open={ctx.openMobile()}
204
213
  onOpenChange={ctx.setOpenMobile}
205
- side={side()}
214
+ side={isStart(side()) ? 'start' : 'end'}
206
215
  title={label()}
207
216
  class="max-w-[var(--sidebar-w)] p-0"
208
217
  >
@@ -233,10 +242,16 @@ export function Sidebar(props: SidebarProps): JSX.Element {
233
242
  <div
234
243
  class={cn(
235
244
  'absolute inset-y-0 transition-[transform,visibility] duration-200 ease-out',
236
- side() === 'left' ? 'left-0' : 'right-0',
245
+ isStart(side()) ? 'start-0' : 'end-0',
237
246
  variant() !== 'sidebar' && 'p-2',
238
247
  collapsible() === 'icon' ? 'w-full' : 'w-[var(--sidebar-w)]',
239
- offscreen() && cn('invisible', side() === 'left' ? '-translate-x-full' : 'translate-x-full'),
248
+ offscreen() &&
249
+ cn(
250
+ 'invisible',
251
+ isStart(side())
252
+ ? '-translate-x-full rtl:translate-x-full'
253
+ : 'translate-x-full rtl:-translate-x-full',
254
+ ),
240
255
  )}
241
256
  >
242
257
  {surface()}
@@ -275,7 +290,7 @@ export function SidebarTrigger(props: SidebarTriggerProps): JSX.Element {
275
290
  props.class,
276
291
  )}
277
292
  >
278
- {props.children ?? <PanelLeft class="h-4 w-4" aria-hidden="true" />}
293
+ {props.children ?? <PanelLeft class="h-4 w-4 rtl:-scale-x-100" aria-hidden="true" />}
279
294
  </button>
280
295
  )
281
296
  }
@@ -287,7 +302,7 @@ export function SidebarTrigger(props: SidebarTriggerProps): JSX.Element {
287
302
  */
288
303
  export function SidebarRail(props: { class?: string }): JSX.Element {
289
304
  const ctx = useSidebarInternal()
290
- const left = () => ctx.config().side === 'left'
305
+ const left = () => isStart(ctx.config().side)
291
306
  return (
292
307
  <button
293
308
  type="button"
@@ -297,8 +312,10 @@ export function SidebarRail(props: { class?: string }): JSX.Element {
297
312
  title="Toggle sidebar"
298
313
  onClick={() => ctx.toggle()}
299
314
  class={cn(
300
- 'absolute inset-y-0 z-10 hidden w-3 after:absolute after:inset-y-0 after:left-1/2 after:w-px after:bg-transparent after:transition-colors hover:after:bg-border md:block',
301
- left() ? '-right-1.5 cursor-w-resize' : '-left-1.5 cursor-e-resize',
315
+ 'absolute inset-y-0 z-10 hidden w-3 after:absolute after:inset-y-0 after:start-1/2 after:w-px after:bg-transparent after:transition-colors hover:after:bg-border md:block',
316
+ left()
317
+ ? '-end-1.5 cursor-w-resize rtl:cursor-e-resize'
318
+ : '-start-1.5 cursor-e-resize rtl:cursor-w-resize',
302
319
  props.class,
303
320
  )}
304
321
  />
@@ -325,7 +342,7 @@ export function SidebarInset(props: ParentProps<{ class?: string }>): JSX.Elemen
325
342
  inset() &&
326
343
  cn(
327
344
  'm-2 rounded-xl border border-border bg-glass shadow-sm',
328
- ctx.config().side === 'left' ? 'ml-0' : 'mr-0',
345
+ isStart(ctx.config().side) ? 'ms-0' : 'me-0',
329
346
  ),
330
347
  props.class,
331
348
  )}
@@ -416,7 +433,7 @@ export function SidebarGroupAction(props: SidebarGroupActionProps): JSX.Element
416
433
  aria-label={props.label}
417
434
  onClick={(e) => props.onClick?.(e)}
418
435
  class={cn(
419
- 'absolute right-1 top-0 grid h-6 w-6 place-items-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring group-data-[icon-collapsed=true]/sidebar:hidden [&>svg]:h-4 [&>svg]:w-4',
436
+ 'absolute end-1 top-0 grid h-6 w-6 place-items-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring group-data-[icon-collapsed=true]/sidebar:hidden [&>svg]:h-4 [&>svg]:w-4',
420
437
  props.class,
421
438
  )}
422
439
  >
@@ -476,17 +493,20 @@ const SIZE: Record<MenuButtonSize, string> = {
476
493
  // child is visually hidden (sr-only) so the accessible name survives, and the
477
494
  // icon is centred.
478
495
  const ITEM_BASE =
479
- 'flex w-full min-w-0 items-center gap-2.5 rounded-lg px-2.5 text-left font-medium text-foreground/80 outline-none transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring aria-[current=page]:bg-muted aria-[current=page]:text-foreground group-has-[[data-sidebar=menu-action]]/item:pr-9 [&>svg]:h-[18px] [&>svg]:w-[18px] [&>svg]:shrink-0 [&>span]:truncate group-data-[icon-collapsed=true]/sidebar:justify-center group-data-[icon-collapsed=true]/sidebar:px-0 group-data-[icon-collapsed=true]/sidebar:[&>span]:sr-only'
496
+ 'flex w-full min-w-0 items-center gap-2.5 rounded-lg px-2.5 text-start font-medium text-foreground/80 outline-none transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring aria-[current=page]:bg-muted aria-[current=page]:text-foreground group-has-[[data-sidebar=menu-action]]/item:pe-9 [&>svg]:h-[18px] [&>svg]:w-[18px] [&>svg]:shrink-0 [&>span]:truncate group-data-[icon-collapsed=true]/sidebar:justify-center group-data-[icon-collapsed=true]/sidebar:px-0 group-data-[icon-collapsed=true]/sidebar:[&>span]:sr-only'
480
497
 
481
498
  function MenuTooltip(
482
499
  props: ParentProps<{ tooltip?: JSX.Element; as: 'a' | 'button'; [key: string]: unknown }>,
483
500
  ) {
484
501
  const iconCollapsed = useContext(IconCollapsed)
485
502
  const ctx = useSidebarInternal()
503
+ const dir = useDirection()
486
504
  const [local, rest] = splitProps(props, ['tooltip', 'as', 'children'])
505
+ // Kobalte placements are physical: the tooltip opens toward the content side.
506
+ const towardContent = () => (isStart(ctx.config().side) === (dir() === 'ltr') ? 'right' : 'left')
487
507
  return (
488
508
  <KTooltip
489
- placement={ctx.config().side === 'left' ? 'right' : 'left'}
509
+ placement={towardContent()}
490
510
  gutter={10}
491
511
  openDelay={150}
492
512
  closeDelay={0}
@@ -495,7 +515,7 @@ function MenuTooltip(
495
515
  <KTooltip.Trigger as={local.as} {...rest}>
496
516
  {local.children}
497
517
  </KTooltip.Trigger>
498
- <KTooltip.Portal>
518
+ <KTooltip.Portal ref={portalDir}>
499
519
  <KTooltip.Content class="z-50 rounded-md border border-border bg-card px-2.5 py-1.5 text-xs text-card-foreground shadow-md">
500
520
  {local.tooltip}
501
521
  </KTooltip.Content>
@@ -565,7 +585,7 @@ export function SidebarMenuAction(
565
585
  aria-label={props.label}
566
586
  onClick={(e) => props.onClick?.(e)}
567
587
  class={cn(
568
- 'absolute right-1 top-1/2 grid h-6 w-6 -translate-y-1/2 place-items-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring group-data-[icon-collapsed=true]/sidebar:hidden [&>svg]:h-4 [&>svg]:w-4',
588
+ 'absolute end-1 top-1/2 grid h-6 w-6 -translate-y-1/2 place-items-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring group-data-[icon-collapsed=true]/sidebar:hidden [&>svg]:h-4 [&>svg]:w-4',
569
589
  props.showOnHover &&
570
590
  'opacity-0 focus-visible:opacity-100 group-focus-within/item:opacity-100 group-hover/item:opacity-100',
571
591
  props.class,
@@ -582,7 +602,7 @@ export function SidebarMenuBadge(props: ParentProps<{ class?: string }>): JSX.El
582
602
  <span
583
603
  data-sidebar="menu-badge"
584
604
  class={cn(
585
- 'pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 select-none rounded-md bg-primary/15 px-1.5 text-[11px] font-semibold tabular-nums text-primary group-data-[icon-collapsed=true]/sidebar:hidden',
605
+ 'pointer-events-none absolute end-2 top-1/2 -translate-y-1/2 select-none rounded-md bg-primary/15 px-1.5 text-[11px] font-semibold tabular-nums text-foreground group-data-[icon-collapsed=true]/sidebar:hidden',
586
606
  props.class,
587
607
  )}
588
608
  >
@@ -651,8 +671,8 @@ export function SidebarMenuSub(props: SidebarMenuSubProps): JSX.Element {
651
671
  <ChevronRight
652
672
  aria-hidden="true"
653
673
  class={cn(
654
- 'ml-auto h-4 w-4 shrink-0 transition-transform duration-200 group-data-[icon-collapsed=true]/sidebar:hidden',
655
- shown() && 'rotate-90',
674
+ 'ms-auto h-4 w-4 shrink-0 transition-transform duration-200 group-data-[icon-collapsed=true]/sidebar:hidden',
675
+ shown() ? 'rotate-90' : 'rtl:-scale-x-100',
656
676
  )}
657
677
  />
658
678
  </MenuTooltip>
@@ -664,7 +684,7 @@ export function SidebarMenuSub(props: SidebarMenuSubProps): JSX.Element {
664
684
  )}
665
685
  >
666
686
  <div class="min-h-0 overflow-hidden">
667
- <ul class="ml-4 mt-1 flex list-none flex-col gap-0.5 border-l border-border py-0.5 pl-2.5">
687
+ <ul class="ms-4 mt-1 flex list-none flex-col gap-0.5 border-s border-border py-0.5 ps-2.5">
668
688
  {props.children}
669
689
  </ul>
670
690
  </div>
@@ -688,7 +708,7 @@ export function SidebarMenuSubButton(
688
708
  ): JSX.Element {
689
709
  const cls = () =>
690
710
  cn(
691
- 'flex h-8 w-full min-w-0 items-center gap-2 rounded-md px-2 text-left text-[13px] font-medium text-foreground/75 outline-none transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring aria-[current=page]:bg-muted aria-[current=page]:text-foreground [&>span]:truncate',
711
+ 'flex h-8 w-full min-w-0 items-center gap-2 rounded-md px-2 text-start text-[13px] font-medium text-foreground/75 outline-none transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring aria-[current=page]:bg-muted aria-[current=page]:text-foreground [&>span]:truncate',
692
712
  props.class,
693
713
  )
694
714
  return (
@@ -116,7 +116,7 @@ export function SlashMenu(props: SlashMenuProps): JSX.Element {
116
116
  onMouseEnter={() => setActive(i())}
117
117
  onClick={() => pick(item)}
118
118
  class={cn(
119
- 'flex cursor-pointer items-center gap-2 rounded-md px-3 py-2 text-left transition-colors',
119
+ 'flex cursor-pointer items-center gap-2 rounded-md px-3 py-2 text-start transition-colors',
120
120
  active() === i() ? 'bg-muted' : undefined,
121
121
  )}
122
122
  >
@@ -42,21 +42,24 @@ export function Snippet(props: SnippetProps): JSX.Element {
42
42
  return (
43
43
  <div class={cn('relative rounded-lg border border-border bg-muted px-3 py-2', props.class)}>
44
44
  <Show when={props.language}>
45
- <span class="absolute right-9 top-2 rounded bg-card px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground">
45
+ <span class="absolute end-9 top-2 rounded bg-card px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground">
46
46
  {props.language}
47
47
  </span>
48
48
  </Show>
49
49
  <button
50
50
  type="button"
51
51
  aria-label={copied() ? 'Copied' : 'Copy to clipboard'}
52
- class="absolute right-2 top-2 inline-flex h-5 w-5 items-center justify-center rounded text-muted-foreground hover:text-foreground"
52
+ class="absolute end-2 top-2 inline-flex h-5 w-5 items-center justify-center rounded text-muted-foreground hover:text-foreground"
53
53
  onClick={copy}
54
54
  >
55
55
  <Show when={copied()} fallback={<Copy class="h-3.5 w-3.5" />}>
56
56
  <Check class="h-3.5 w-3.5" />
57
57
  </Show>
58
58
  </button>
59
- <pre class="overflow-x-auto whitespace-pre pr-8 font-mono text-sm text-foreground">
59
+ <pre
60
+ dir="ltr"
61
+ class="overflow-x-auto whitespace-pre pr-8 rtl:pl-8 rtl:pr-0 font-mono text-sm text-foreground"
62
+ >
60
63
  <code>{props.code}</code>
61
64
  </pre>
62
65
  </div>
@@ -3,7 +3,7 @@
3
3
  // a floating clone follows the pointer through a Portal, and the grabbed
4
4
  // row's slot collapses into a dashed placeholder until it's dropped.
5
5
  import { createEffect, createSignal, For, Show, type JSX } from 'solid-js'
6
- import { Portal } from 'solid-js/web'
6
+ import { Portal } from './Portal'
7
7
  import { GripVertical } from 'lucide-solid'
8
8
 
9
9
  import { cn } from '../lib/cn'
@@ -76,7 +76,7 @@ export function SpecSheet(props: SpecSheetProps): JSX.Element {
76
76
  )}
77
77
  >
78
78
  <dt class="text-muted-foreground">{row.label}</dt>
79
- <dd class="text-right font-medium text-foreground">{row.value}</dd>
79
+ <dd class="text-end font-medium text-foreground">{row.value}</dd>
80
80
  </div>
81
81
  )}
82
82
  </For>
@@ -40,7 +40,7 @@ export function SpeedDial(props: SpeedDialProps): JSX.Element {
40
40
  const [open, setOpen] = createSignal(false)
41
41
 
42
42
  return (
43
- <div class={cn('fixed bottom-6 right-6 z-40 flex flex-col items-center gap-3', props.class)}>
43
+ <div class={cn('fixed bottom-6 end-6 z-40 flex flex-col items-center gap-3', props.class)}>
44
44
  <Show when={open()}>
45
45
  <div class="flex flex-col items-center gap-3">
46
46
  <For each={props.actions}>
@@ -2,6 +2,8 @@
2
2
  import type { JSX } from 'solid-js'
3
3
  import { createSignal } from 'solid-js'
4
4
  import { cn } from '../lib/cn'
5
+ import { readingKey } from '../lib/direction'
6
+ import { useDirection } from './DirectionProvider'
5
7
 
6
8
  export interface SplitterProps {
7
9
  /** Content of the first pane (left when horizontal, top when vertical). */
@@ -35,6 +37,7 @@ export interface SplitterProps {
35
37
  * ```
36
38
  */
37
39
  export function Splitter(props: SplitterProps): JSX.Element {
40
+ const dir = useDirection()
38
41
  const orientation = () => props.orientation ?? 'horizontal'
39
42
  const min = () => props.min ?? 15
40
43
  const max = () => props.max ?? 85
@@ -50,7 +53,7 @@ export function Splitter(props: SplitterProps): JSX.Element {
50
53
  const rect = container.getBoundingClientRect()
51
54
  const pct =
52
55
  orientation() === 'horizontal'
53
- ? ((event.clientX - rect.left) / rect.width) * 100
56
+ ? ((dir() === 'rtl' ? rect.right - event.clientX : event.clientX - rect.left) / rect.width) * 100
54
57
  : ((event.clientY - rect.top) / rect.height) * 100
55
58
  setSize(clamp(pct))
56
59
  }
@@ -72,8 +75,10 @@ export function Splitter(props: SplitterProps): JSX.Element {
72
75
  const onKeyDown = (event: KeyboardEvent) => {
73
76
  const step = 3
74
77
  let delta = 0
75
- if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') delta = -step
76
- else if (event.key === 'ArrowRight' || event.key === 'ArrowDown') delta = step
78
+ // The first pane sits on the right under rtl, so the horizontal keys swap.
79
+ const key = readingKey(event.key, dir())
80
+ if (key === 'ArrowLeft' || key === 'ArrowUp') delta = -step
81
+ else if (key === 'ArrowRight' || key === 'ArrowDown') delta = step
77
82
  else if (event.key === 'Home') return setSize(min())
78
83
  else if (event.key === 'End') return setSize(max())
79
84
  else return
@@ -246,12 +246,12 @@ export function SpreadsheetGrid(props: SpreadsheetGridProps): JSX.Element {
246
246
  >
247
247
  <div class="grid" style={{ 'grid-template-columns': `48px repeat(${props.cols}, minmax(80px, 1fr))` }}>
248
248
  <div role="row" class="contents">
249
- <div aria-hidden="true" class="border-b border-r border-border bg-muted" />
249
+ <div aria-hidden="true" class="border-b border-e border-border bg-muted" />
250
250
  <For each={headers()}>
251
251
  {(h) => (
252
252
  <div
253
253
  role="columnheader"
254
- class="border-b border-r border-border bg-muted px-2 py-1 text-center text-xs font-semibold text-muted-foreground"
254
+ class="border-b border-e border-border bg-muted px-2 py-1 text-center text-xs font-semibold text-muted-foreground"
255
255
  >
256
256
  {h}
257
257
  </div>
@@ -263,7 +263,7 @@ export function SpreadsheetGrid(props: SpreadsheetGridProps): JSX.Element {
263
263
  <div role="row" class="contents">
264
264
  <div
265
265
  role="rowheader"
266
- class="border-b border-r border-border bg-muted px-2 py-1 text-center text-xs text-muted-foreground"
266
+ class="border-b border-e border-border bg-muted px-2 py-1 text-center text-xs text-muted-foreground"
267
267
  >
268
268
  {r + 1}
269
269
  </div>
@@ -273,7 +273,7 @@ export function SpreadsheetGrid(props: SpreadsheetGridProps): JSX.Element {
273
273
  role="gridcell"
274
274
  aria-selected={isActive(r, c)}
275
275
  class={cn(
276
- 'relative h-8 border-b border-r border-border px-2 py-1 text-sm cursor-cell',
276
+ 'relative h-8 border-b border-e border-border px-2 py-1 text-sm cursor-cell',
277
277
  inSelection(r, c) && 'bg-primary/10',
278
278
  isActive(r, c) && 'ring-2 ring-inset ring-primary',
279
279
  isAnchor(r, c) && !isActive(r, c) && 'border-primary',
@@ -90,7 +90,7 @@ export function Stepper(props: StepperProps): JSX.Element {
90
90
  aria-hidden="true"
91
91
  class={cn(
92
92
  connectorFilled() ? 'bg-primary' : 'bg-border',
93
- isVertical() ? 'ml-4 -mt-1 mb-1 w-0.5 flex-1' : 'mx-2 h-0.5 flex-1',
93
+ isVertical() ? 'ms-4 -mt-1 mb-1 w-0.5 flex-1' : 'mx-2 h-0.5 flex-1',
94
94
  )}
95
95
  />
96
96
  </Show>
@@ -36,7 +36,7 @@ export function SuggestionChips(props: SuggestionChipsProps): JSX.Element {
36
36
  <div class={cn('flex flex-wrap gap-2', props.class)}>
37
37
  <For each={props.suggestions}>
38
38
  {(suggestion) => (
39
- <span class="inline-flex items-center gap-1 rounded-full border border-border pl-3 pr-1.5 py-1.5 text-sm text-foreground transition-colors hover:bg-muted">
39
+ <span class="inline-flex items-center gap-1 rounded-full border border-border ps-3 pe-1.5 py-1.5 text-sm text-foreground transition-colors hover:bg-muted">
40
40
  <button
41
41
  type="button"
42
42
  onClick={() => props.onSelect?.(suggestion)}
package/src/ui/Switch.tsx CHANGED
@@ -34,7 +34,7 @@ export function Switch(props: SwitchProps): JSX.Element {
34
34
  >
35
35
  <KSwitch.Input />
36
36
  <KSwitch.Control class="inline-flex h-5 w-9 shrink-0 items-center rounded-full bg-input px-0.5 transition-colors data-[checked]:bg-primary data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50">
37
- <KSwitch.Thumb class="h-4 w-4 rounded-full bg-white shadow-sm transition-transform data-[checked]:translate-x-4" />
37
+ <KSwitch.Thumb class="h-4 w-4 rounded-full bg-white shadow-sm transition-transform data-[checked]:translate-x-4 rtl:data-[checked]:-translate-x-4" />
38
38
  </KSwitch.Control>
39
39
  <Show when={props.label}>
40
40
  <KSwitch.Label class="text-sm text-foreground">{props.label}</KSwitch.Label>
package/src/ui/Table.tsx CHANGED
@@ -61,7 +61,7 @@ export function Table(props: TableProps): JSX.Element {
61
61
  export function TableHead(props: TableProps): JSX.Element {
62
62
  const [local, rest] = splitProps(props, ['class', 'children'])
63
63
  return (
64
- <thead class={cn('border-b border-border text-left text-muted-foreground', local.class)} {...rest}>
64
+ <thead class={cn('border-b border-border text-muted-foreground', local.class)} {...rest}>
65
65
  {local.children}
66
66
  </thead>
67
67
  )
@@ -112,7 +112,10 @@ export function TableRow(props: TableProps): JSX.Element {
112
112
  export function TableHeadCell(props: TableProps): JSX.Element {
113
113
  const [local, rest] = splitProps(props, ['class', 'children'])
114
114
  return (
115
- <th class={cn('px-3 py-2 text-xs font-semibold uppercase tracking-wide', local.class)} {...rest}>
115
+ <th
116
+ class={cn('px-3 py-2 text-start text-xs font-semibold uppercase tracking-wide', local.class)}
117
+ {...rest}
118
+ >
116
119
  {local.children}
117
120
  </th>
118
121
  )