@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
@@ -0,0 +1,47 @@
1
+ import { Accessor, JSX } from 'solid-js';
2
+ import { Direction } from '../lib/direction';
3
+ export type { Direction };
4
+ /** Props for {@link DirectionProvider}. */
5
+ export interface DirectionProviderProps {
6
+ /**
7
+ * Text direction. When omitted it is derived from `locale`; when both are
8
+ * omitted it is `'ltr'`.
9
+ */
10
+ dir?: Direction;
11
+ /**
12
+ * BCP 47 locale (for example `'ar'`, `'he-IL'`). Forwarded to Kobalte for
13
+ * number/date formatting and collation. Its reading direction must agree
14
+ * with `dir`; a mismatch throws.
15
+ */
16
+ locale?: string;
17
+ /** Extra classes for the wrapper element. */
18
+ class?: string;
19
+ children?: JSX.Element;
20
+ }
21
+ /**
22
+ * Sets the reading direction for everything inside it.
23
+ *
24
+ * - Renders a `display: contents` wrapper carrying the `dir` attribute, so
25
+ * Tailwind logical utilities (`ms-*`, `start-*`, `text-start`) and the
26
+ * browser's bidi handling follow it.
27
+ * - Wraps Kobalte's `I18nProvider`, so arrow keys in Tabs, Menubar, Slider,
28
+ * menus and the like reverse under `rtl`.
29
+ * - Overlays that portal out to `<body>` (Modal, Drawer, Popover, Dropdown,
30
+ * Toast, ...) copy the direction onto their own container.
31
+ *
32
+ * Without a provider nothing changes: components inherit `dir` from the page
33
+ * (set `<html dir="rtl">` yourself in that case).
34
+ *
35
+ * @example
36
+ * ```tsx
37
+ * <DirectionProvider dir="rtl" locale="ar">
38
+ * <App />
39
+ * </DirectionProvider>
40
+ * ```
41
+ */
42
+ export declare function DirectionProvider(props: DirectionProviderProps): JSX.Element;
43
+ /**
44
+ * Accessor of the current direction: the nearest {@link DirectionProvider}'s,
45
+ * or outside one the `dir` attribute of `<html>` (`'ltr'` when unset).
46
+ */
47
+ export declare function useDirection(): Accessor<Direction>;
@@ -12,10 +12,14 @@ interface DrawerProps extends ParentProps {
12
12
  /** Edge the panel is anchored to and slides in from. @default 'right' */
13
13
  side?: DrawerSide;
14
14
  }
15
- export type DrawerSide = 'right' | 'left' | 'top' | 'bottom';
15
+ /**
16
+ * `left` / `right` are physical edges. `start` / `end` follow the reading
17
+ * direction (`start` is the right edge under `dir="rtl"`).
18
+ */
19
+ export type DrawerSide = 'right' | 'left' | 'start' | 'end' | 'top' | 'bottom';
16
20
  export type { DrawerProps };
17
21
  /**
18
- * Edge-anchored slide-over panel (right by default, or `side="left" | "top" | "bottom"`) built on Kobalte's `Dialog` (focus trap, portal,
22
+ * Edge-anchored slide-over panel (right by default, or `side="left" | "start" | "end" | "top" | "bottom"`) built on Kobalte's `Dialog` (focus trap, portal,
19
23
  * and open/close presence handled for you). Use for create/edit forms or detail
20
24
  * panels that need more room than a centered `Modal`.
21
25
  *
@@ -1,5 +1,11 @@
1
1
  import { Accessor, JSX, ParentProps } from 'solid-js';
2
- export type SidebarSide = 'left' | 'right';
2
+ /**
3
+ * Which edge the sidebar sits on. The layout is a flex row in DOM order, so the
4
+ * edge follows the reading direction: `start` (alias `left`) is the leading
5
+ * edge, `end` (alias `right`) the trailing one. Under `dir="rtl"` `start` is the
6
+ * right-hand side.
7
+ */
8
+ export type SidebarSide = 'start' | 'end' | 'left' | 'right';
3
9
  export type SidebarVariant = 'sidebar' | 'floating' | 'inset';
4
10
  export type SidebarCollapsible = 'offcanvas' | 'icon' | 'none';
5
11
  interface SidebarContextValue {
@@ -53,7 +59,7 @@ interface SidebarProviderProps extends ParentProps {
53
59
  */
54
60
  export declare function SidebarProvider(props: SidebarProviderProps): JSX.Element;
55
61
  interface SidebarProps extends ParentProps {
56
- /** Which edge it sits on. @default 'left' */
62
+ /** Which edge it sits on; follows `dir` (see {@link SidebarSide}). @default 'start' */
57
63
  side?: SidebarSide;
58
64
  /** `sidebar` is flush, `floating` is a detached rounded panel, `inset` has no surface of its own (pair with `SidebarInset`). @default 'sidebar' */
59
65
  variant?: SidebarVariant;
@@ -17,5 +17,9 @@ export declare const toast: {
17
17
  error: (title: string, description?: string) => void;
18
18
  info: (title: string, description?: string) => void;
19
19
  };
20
- /** Mount once near the app root — hosts the toast viewport. */
20
+ /**
21
+ * Mount once near the app root — hosts the toast viewport. Mount it inside a
22
+ * `DirectionProvider` for RTL: the viewport sits in the end corner (the left
23
+ * one under RTL) and swipe-to-dismiss follows the direction.
24
+ */
21
25
  export declare function Toaster(): JSX.Element;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a4ui/core",
3
- "version": "0.38.1",
3
+ "version": "0.39.0",
4
4
  "description": "A4ui — Spatial Glass design system & component library for SolidJS (Kobalte behavior + Tailwind glass tokens + motion).",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -70,6 +70,7 @@
70
70
  },
71
71
  "./styles.css": "./dist/styles.css",
72
72
  "./full.css": "./dist/full.css",
73
+ "./theme.css": "./dist/theme.css",
73
74
  "./elements": {
74
75
  "import": "./dist/elements.js",
75
76
  "default": "./dist/elements.js"
@@ -86,8 +87,9 @@
86
87
  },
87
88
  "scripts": {
88
89
  "prebuild": "node scripts/sync-version.mjs",
89
- "build": "vite build && npm run build:elements",
90
+ "build": "vite build && npm run build:elements && npm run build:theme",
90
91
  "build:elements": "vite build --config vite.elements.config.ts && node scripts/build-elements-css.mjs",
92
+ "build:theme": "node scripts/build-theme-css.mjs",
91
93
  "dev": "vite build --watch",
92
94
  "preview": "node scripts/gen-llms.mjs && vite --config vite.preview.config.ts",
93
95
  "preview:build": "node scripts/gen-llms.mjs && vite build --config vite.preview.config.ts",
@@ -119,7 +121,7 @@
119
121
  },
120
122
  "peerDependencies": {
121
123
  "solid-js": "^1.9.16",
122
- "tailwindcss": "^3.4.0"
124
+ "tailwindcss": "^3.4.0 || ^4.0.0"
123
125
  },
124
126
  "dependencies": {
125
127
  "@kobalte/core": "^0.13.14",
package/preset.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  // Types for `@a4ui/core/preset` — the Spatial Glass Tailwind preset.
2
- // Requires Tailwind CSS v3 (`tailwindcss@^3.4.0`): v4 removed `presets:`.
2
+ // Requires Tailwind CSS v3 (`tailwindcss@^3.4.0`): v4 removed `presets:`. On v4, use
3
+ // `@import '@a4ui/core/theme.css'` instead; loading this preset there throws.
3
4
  import type { Config } from 'tailwindcss'
4
5
 
5
6
  /**
package/preset.js CHANGED
@@ -13,6 +13,9 @@
13
13
  // presets: [a4ui],
14
14
  // content: ['./src/**/*.{ts,tsx}', './node_modules/@a4ui/core/dist/**/*.js'],
15
15
  // }
16
+ //
17
+ // Tailwind 3 only. Tailwind 4 consumers use `@a4ui/core/theme.css`, which
18
+ // scripts/build-theme-css.mjs generates from this file.
16
19
 
17
20
  // Explicit .js extension so the preset resolves under strict Node ESM (used by
18
21
  // our elements-css build), not just bundlers.
@@ -21,7 +24,15 @@ import plugin from 'tailwindcss/plugin.js'
21
24
  // Frosted "space glass" surfaces. addComponents so they tree-shake like any
22
25
  // utility (emitted only when the class is found in scanned content — a4ui's own
23
26
  // components use them, so scanning ./node_modules/@a4ui/core/dist covers it).
24
- const glass = plugin(({ addComponents, addVariant }) => {
27
+ const glass = plugin(({ addComponents, addVariant, e }) => {
28
+ // Tailwind 4 reaches this only through `@config`. It would otherwise stop on a
29
+ // message about `addUtilities` that says nothing about the actual problem, and the
30
+ // components need more than this preset there anyway. `e` is a Tailwind 3 plugin API.
31
+ if (typeof e !== 'function') {
32
+ throw new Error(
33
+ "@a4ui/core/preset requires Tailwind 3. On Tailwind 4, remove the preset and add `@import '@a4ui/core/theme.css';` after `@import 'tailwindcss';` in your CSS.",
34
+ )
35
+ }
25
36
  // `light:` applies when an ancestor is in the light theme (`[data-theme='light']`),
26
37
  // mirroring how dark is the base and light is the override throughout A4ui. Used
27
38
  // for theme-aware tints (e.g. Badge tones) that need a lighter foreground on dark
@@ -56,8 +67,8 @@ const glass = plugin(({ addComponents, addVariant }) => {
56
67
  position: 'relative',
57
68
  overflow: 'hidden',
58
69
  background: 'hsl(var(--card) / 0.5)',
59
- backdropFilter: 'blur(14px) saturate(180%)',
60
70
  WebkitBackdropFilter: 'blur(14px) saturate(180%)',
71
+ backdropFilter: 'blur(14px) saturate(180%)',
61
72
  border: '1px solid hsl(var(--foreground) / 0.14)',
62
73
  borderRadius: 'var(--radius-xl, 1rem)',
63
74
  boxShadow:
@@ -76,8 +87,8 @@ const glass = plugin(({ addComponents, addVariant }) => {
76
87
  '.card': {
77
88
  position: 'relative',
78
89
  background: 'hsl(var(--card) / 0.55)',
79
- backdropFilter: 'blur(8px) saturate(150%)',
80
90
  WebkitBackdropFilter: 'blur(8px) saturate(150%)',
91
+ backdropFilter: 'blur(8px) saturate(150%)',
81
92
  border: '1px solid hsl(var(--foreground) / 0.16)',
82
93
  borderRadius: 'var(--radius-xl, 1rem)',
83
94
  boxShadow: '0 1px 2px hsl(var(--shadow) / 0.05), 0 4px 12px hsl(var(--shadow) / 0.1)',
@@ -145,8 +156,8 @@ const glass = plugin(({ addComponents, addVariant }) => {
145
156
  // ---- Floating overlay glass (menus, modals, drawers, toasts) ----
146
157
  '.bg-glass': {
147
158
  background: 'hsl(var(--card) / 0.72)',
148
- backdropFilter: 'blur(14px) saturate(160%)',
149
159
  WebkitBackdropFilter: 'blur(14px) saturate(160%)',
160
+ backdropFilter: 'blur(14px) saturate(160%)',
150
161
  border: '1px solid hsl(var(--foreground) / 0.14)',
151
162
  },
152
163
  "[data-theme='light'] .bg-glass": {
@@ -174,8 +185,8 @@ const glass = plugin(({ addComponents, addVariant }) => {
174
185
  },
175
186
  'html.calm .card, html.calm .bg-glass': {
176
187
  background: 'hsl(var(--card))',
177
- backdropFilter: 'none',
178
188
  WebkitBackdropFilter: 'none',
189
+ backdropFilter: 'none',
179
190
  },
180
191
  'html.calm .tile-glass': {
181
192
  background: 'hsl(var(--muted))',
@@ -54,7 +54,7 @@ export function BarList(props: BarListProps): JSX.Element {
54
54
  {(datum) => (
55
55
  <div class="relative isolate flex h-8 w-full min-w-0 items-center overflow-hidden rounded-md bg-muted/40">
56
56
  <div
57
- class={cn('absolute inset-y-0 left-0 rounded-md transition-all', fillClass())}
57
+ class={cn('absolute inset-y-0 start-0 rounded-md transition-all', fillClass())}
58
58
  style={{ width: `${Math.max((datum.value / max()) * 100, 0)}%` }}
59
59
  />
60
60
  <Show
@@ -17,6 +17,8 @@ export interface SparklineProps {
17
17
  tone?: 'primary' | 'accent' | 'destructive'
18
18
  /** Fill the area under the line faintly. Default false. */
19
19
  area?: boolean
20
+ /** Accessible description, e.g. 'Revenue, rising'. Without it the chart is hidden from assistive tech as decoration. */
21
+ label?: string
20
22
  class?: string
21
23
  }
22
24
 
@@ -76,7 +78,9 @@ export function Sparkline(props: SparklineProps): JSX.Element {
76
78
  height={height()}
77
79
  viewBox={`0 0 ${width()} ${height()}`}
78
80
  preserveAspectRatio="none"
79
- role="img"
81
+ role={props.label ? 'img' : undefined}
82
+ aria-label={props.label}
83
+ aria-hidden={props.label ? undefined : 'true'}
80
84
  >
81
85
  <Show when={points().length > 0}>
82
86
  <Show when={props.area}>
@@ -66,7 +66,7 @@ export function CartLine(props: CartLineProps): JSX.Element {
66
66
  onChange={(quantity) => props.onQuantityChange?.(quantity)}
67
67
  min={1}
68
68
  />
69
- <span class="min-w-[4rem] text-right font-medium tabular-nums text-foreground">
69
+ <span class="min-w-[4rem] text-end font-medium tabular-nums text-foreground">
70
70
  {format(lineTotal())}
71
71
  </span>
72
72
  <Show when={props.onRemove}>
@@ -152,7 +152,7 @@ export function FacetSidebar(props: FacetSidebarProps): JSX.Element {
152
152
  <Show when={props.onClearAll}>
153
153
  <Button
154
154
  variant="ghost"
155
- class="ml-auto h-auto px-2 py-1 text-xs"
155
+ class="ms-auto h-auto px-2 py-1 text-xs"
156
156
  onClick={() => props.onClearAll?.()}
157
157
  >
158
158
  Clear all
@@ -95,7 +95,7 @@ export function ProductCard(props: ProductCardProps): JSX.Element {
95
95
  {/* Diagonal corner ribbon (clipped by the overflow-hidden image box). */}
96
96
  <span
97
97
  class={cn(
98
- 'absolute -right-9 top-3.5 w-32 rotate-45 py-1 text-center text-[11px] font-semibold uppercase tracking-wide shadow-md',
98
+ 'absolute -end-9 top-3.5 w-32 rotate-45 py-1 text-center text-[11px] font-semibold uppercase tracking-wide shadow-md',
99
99
  RIBBON_TONE[inferTone(props.badge, props.badgeTone)],
100
100
  )}
101
101
  >
package/src/index.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  // import '@a4ui/core/styles.css'
9
9
  // import { Button, Card, Modal } from '@a4ui/core'
10
10
 
11
- export const A4UI_VERSION = '0.38.1'
11
+ export const A4UI_VERSION = '0.39.0'
12
12
 
13
13
  // Helpers (src/lib) — generic, framework-level utilities.
14
14
  export { cn } from './lib/cn'
@@ -277,6 +277,12 @@ export { ColorPicker, type ColorPickerProps } from './ui/ColorPicker'
277
277
  export { Comment, type CommentData, type CommentProps } from './ui/Comment'
278
278
  export { CalendarHeatmap, type HeatmapValue, type CalendarHeatmapProps } from './ui/CalendarHeatmap'
279
279
  export { Portal, type PortalProps } from './ui/Portal'
280
+ export {
281
+ DirectionProvider,
282
+ useDirection,
283
+ type Direction,
284
+ type DirectionProviderProps,
285
+ } from './ui/DirectionProvider'
280
286
  export { Sortable, type SortableProps } from './ui/Sortable'
281
287
  export { Clock, type ClockProps } from './ui/Clock'
282
288
 
@@ -28,7 +28,7 @@ export function NavGroup(props: NavGroupProps): JSX.Element {
28
28
  <summary class="mt-3 flex cursor-pointer list-none select-none items-center justify-between rounded-md px-3 py-1.5 text-xs font-semibold uppercase tracking-wide text-muted-foreground transition-colors hover:bg-muted hover:text-foreground [&::-webkit-details-marker]:hidden">
29
29
  <span>{props.title}</span>
30
30
  <svg
31
- class="h-3.5 w-3.5 shrink-0 transition-transform duration-200 group-[[open]]:rotate-90"
31
+ class="h-3.5 w-3.5 shrink-0 transition-transform duration-200 group-[[open]]:rotate-90 rtl:-scale-x-100 rtl:group-[[open]]:scale-x-100"
32
32
  viewBox="0 0 20 20"
33
33
  fill="none"
34
34
  stroke="currentColor"
@@ -39,7 +39,7 @@ export function NavGroup(props: NavGroupProps): JSX.Element {
39
39
  <path d="M7 5l6 5-6 5" />
40
40
  </svg>
41
41
  </summary>
42
- <div class="ml-4 mt-1 space-y-0.5 border-l border-border pl-2">{props.children}</div>
42
+ <div class="ms-4 mt-1 space-y-0.5 border-s border-border ps-2">{props.children}</div>
43
43
  </details>
44
44
  )
45
45
  }
@@ -55,3 +55,16 @@ export function createAnchorReflow(options: AnchorReflowOptions): () => void {
55
55
  else options.place()
56
56
  }
57
57
  }
58
+
59
+ /**
60
+ * Viewport `left` for a fixed panel aligned with its trigger's inline-start edge,
61
+ * kept 8px inside the viewport. Under `dir="rtl"` that is the trigger's right
62
+ * edge, so the panel extends leftwards. The direction is read from the trigger,
63
+ * which also works for a `dir` set by a provider or an ancestor element.
64
+ */
65
+ export function anchoredLeft(anchor: HTMLElement, panelWidth: number): number {
66
+ const rect = anchor.getBoundingClientRect()
67
+ const rtl = getComputedStyle(anchor).direction === 'rtl'
68
+ const start = rtl ? rect.right - panelWidth : rect.left
69
+ return Math.max(8, Math.min(start, window.innerWidth - panelWidth - 8))
70
+ }
@@ -0,0 +1,35 @@
1
+ // Shared direction context. `DirectionProvider` (src/ui/DirectionProvider.tsx)
2
+ // provides it; overlays that render through a portal read it to stamp `dir`
3
+ // on their container, because a portal mounts outside the provider's wrapper
4
+ // element and so does not inherit its `dir` attribute.
5
+ import { type Accessor, createContext, createEffect, useContext } from 'solid-js'
6
+
7
+ /** Text direction supported by {@link DirectionProvider}. */
8
+ export type Direction = 'ltr' | 'rtl'
9
+
10
+ /** Accessor of the nearest provider's direction, or `undefined` outside one. */
11
+ export const DirectionContext = createContext<Accessor<Direction>>()
12
+
13
+ /**
14
+ * Maps a physical horizontal arrow key to the key that means the same thing in
15
+ * reading order: under `rtl`, ArrowLeft moves forward and ArrowRight back, so
16
+ * handlers written for LTR can run on the swapped key. Other keys pass through.
17
+ */
18
+ export function readingKey(key: string, dir: Direction): string {
19
+ if (dir !== 'rtl') return key
20
+ if (key === 'ArrowLeft') return 'ArrowRight'
21
+ if (key === 'ArrowRight') return 'ArrowLeft'
22
+ return key
23
+ }
24
+
25
+ /**
26
+ * `ref` for a portal container (Solid `Portal` or any Kobalte `*.Portal`).
27
+ * Copies the nearest `DirectionProvider`'s direction onto the container so the
28
+ * overlay renders in the same direction as the tree that opened it. Outside a
29
+ * provider it does nothing, so the overlay inherits from `<body>` as before.
30
+ */
31
+ export function portalDir(el: Element): void {
32
+ const dir = useContext(DirectionContext)
33
+ if (!dir) return
34
+ createEffect(() => el.setAttribute('dir', dir()))
35
+ }
@@ -41,10 +41,10 @@ export function Accordion(props: AccordionProps): JSX.Element {
41
41
  {(item) => (
42
42
  <KAccordion.Item value={item.value} class="border-b border-border">
43
43
  <KAccordion.Header>
44
- <KAccordion.Trigger class="flex w-full items-center justify-between py-3 text-sm font-medium text-foreground [&[data-expanded]>svg]:rotate-90">
44
+ <KAccordion.Trigger class="flex w-full items-center justify-between py-3 text-sm font-medium text-foreground [&[data-expanded]>svg]:rotate-90 [&[data-expanded]>svg]:rtl:scale-x-100">
45
45
  {item.title}
46
46
  <svg
47
- class="h-3.5 w-3.5 shrink-0 transition-transform duration-200"
47
+ class="h-3.5 w-3.5 shrink-0 transition-transform duration-200 rtl:-scale-x-100"
48
48
  viewBox="0 0 20 20"
49
49
  fill="none"
50
50
  stroke="currentColor"
@@ -136,11 +136,11 @@ export function ActivityFeed(props: ActivityFeedProps): JSX.Element {
136
136
  <ol class="relative flex flex-col gap-5">
137
137
  <For each={group.items}>
138
138
  {(item, index) => (
139
- <li class="relative flex gap-3 pl-1">
139
+ <li class="relative flex gap-3 ps-1">
140
140
  <Show when={index() < group.items.length - 1}>
141
141
  <span
142
142
  aria-hidden="true"
143
- class="absolute left-[1.375rem] top-9 -bottom-5 w-px -translate-x-1/2 bg-border"
143
+ class="absolute start-[1.375rem] top-9 -bottom-5 w-px -translate-x-1/2 bg-border"
144
144
  />
145
145
  </Show>
146
146
  <Show
@@ -8,6 +8,7 @@ import type { JSX, ParentProps } from 'solid-js'
8
8
  import { Show } from 'solid-js'
9
9
 
10
10
  import { cn } from '../lib/cn'
11
+ import { portalDir } from '../lib/direction'
11
12
 
12
13
  interface AlertDialogProps extends ParentProps {
13
14
  /** Whether the dialog is currently visible. */
@@ -34,7 +35,7 @@ interface AlertDialogProps extends ParentProps {
34
35
  export function AlertDialog(props: AlertDialogProps): JSX.Element {
35
36
  return (
36
37
  <KAlertDialog.Root open={props.open} onOpenChange={props.onOpenChange}>
37
- <KAlertDialog.Portal>
38
+ <KAlertDialog.Portal ref={portalDir}>
38
39
  <KAlertDialog.Overlay class="modal-overlay fixed inset-0 z-40 bg-background/80 backdrop-blur-sm" />
39
40
  <div class="fixed inset-0 z-50 flex items-center justify-center p-4">
40
41
  <KAlertDialog.Content
package/src/ui/Anchor.tsx CHANGED
@@ -87,7 +87,7 @@ export function Anchor(props: AnchorProps): JSX.Element {
87
87
  class={cn(
88
88
  'block rounded px-3 py-1.5 text-sm transition-colors',
89
89
  activeId() === item.id
90
- ? 'border-l-2 border-primary font-medium text-primary'
90
+ ? 'border-s-2 border-primary font-medium text-primary'
91
91
  : 'text-muted-foreground hover:text-foreground',
92
92
  )}
93
93
  >
@@ -76,7 +76,7 @@ export function AnnouncementBar(props: AnnouncementBarProps): JSX.Element {
76
76
  class={cn(
77
77
  'relative w-full rounded-none px-4 py-2 text-center text-sm',
78
78
  TONE[props.tone ?? 'primary'],
79
- props.dismissible && 'pr-10',
79
+ props.dismissible && 'pe-10',
80
80
  props.class,
81
81
  )}
82
82
  >
@@ -91,7 +91,7 @@ export function AnnouncementBar(props: AnnouncementBarProps): JSX.Element {
91
91
 
92
92
  <Show when={props.couponCode}>
93
93
  {(code) => (
94
- <span class="ml-2 inline-flex items-center gap-1 rounded-md bg-black/10 px-2 py-0.5 font-mono">
94
+ <span class="ms-2 inline-flex items-center gap-1 rounded-md bg-black/10 px-2 py-0.5 font-mono">
95
95
  {code()}
96
96
  <button
97
97
  type="button"
@@ -113,7 +113,7 @@ export function AnnouncementBar(props: AnnouncementBarProps): JSX.Element {
113
113
  type="button"
114
114
  aria-label="Dismiss announcement"
115
115
  onClick={dismiss}
116
- class="absolute right-2 top-1/2 -translate-y-1/2 rounded-sm p-1 transition-opacity hover:opacity-80 motion-reduce:transition-none"
116
+ class="absolute end-2 top-1/2 -translate-y-1/2 rounded-sm p-1 transition-opacity hover:opacity-80 motion-reduce:transition-none"
117
117
  >
118
118
  <X class="h-4 w-4" />
119
119
  </button>
@@ -51,7 +51,7 @@ export function ArtifactPanel(props: ArtifactPanelProps): JSX.Element {
51
51
  aria-hidden={!props.open}
52
52
  class={cn(
53
53
  'card h-full shrink-0 overflow-hidden border-border bg-glass',
54
- props.open ? 'border-l' : 'border-0',
54
+ props.open ? 'border-s' : 'border-0',
55
55
  reduced() ? undefined : 'transition-[width] duration-300 ease-in-out',
56
56
  props.class,
57
57
  )}
@@ -90,7 +90,7 @@ export function AvailabilityPicker(props: AvailabilityPickerProps): JSX.Element
90
90
  return (
91
91
  <div class={cn('flex overflow-hidden rounded-xl border border-border bg-card', props.class)}>
92
92
  {/* Date list */}
93
- <div class="flex w-32 shrink-0 flex-col gap-1 overflow-y-auto border-r border-border p-2">
93
+ <div class="flex w-32 shrink-0 flex-col gap-1 overflow-y-auto border-e border-border p-2">
94
94
  <For each={dates()} fallback={<p class="p-2 text-xs text-muted-foreground">No dates</p>}>
95
95
  {(dateKey) => {
96
96
  const d = parseDateKey(dateKey)
@@ -103,7 +103,7 @@ export function AvailabilityPicker(props: AvailabilityPickerProps): JSX.Element
103
103
  disabled={count() === 0}
104
104
  onClick={() => setViewingDate(dateKey)}
105
105
  class={cn(
106
- 'flex flex-col items-start rounded-md px-2 py-1.5 text-left transition-colors',
106
+ 'flex flex-col items-start rounded-md px-2 py-1.5 text-start transition-colors',
107
107
  isActive() ? 'bg-primary text-primary-foreground' : 'text-foreground hover:bg-muted',
108
108
  count() === 0 && !isActive() && 'text-muted-foreground/50',
109
109
  )}
@@ -38,13 +38,13 @@ export function AvatarGroup(props: AvatarGroupProps): JSX.Element {
38
38
  <div class={cn('flex items-center', props.class)}>
39
39
  <For each={shown()}>
40
40
  {(avatar, index) => (
41
- <div class={cn('ring-2 ring-background rounded-full', index() > 0 && '-ml-2')}>
41
+ <div class={cn('ring-2 ring-background rounded-full', index() > 0 && '-ms-2')}>
42
42
  <Avatar src={avatar.src} alt={avatar.alt} fallback={avatar.fallback} />
43
43
  </div>
44
44
  )}
45
45
  </For>
46
46
  {overflow() > 0 && (
47
- <div class="-ml-2 ring-2 ring-background rounded-full">
47
+ <div class="-ms-2 ring-2 ring-background rounded-full">
48
48
  <span class="grid h-9 w-9 shrink-0 place-items-center rounded-full bg-muted text-xs font-medium text-muted-foreground">
49
49
  +{overflow()}
50
50
  </span>
@@ -40,7 +40,7 @@ export function BackToTop(props: BackToTopProps): JSX.Element {
40
40
  aria-label="Back to top"
41
41
  onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
42
42
  class={cn(
43
- 'fixed bottom-6 right-6 z-40 grid h-11 w-11 place-items-center rounded-full bg-primary text-primary-foreground shadow-lg',
43
+ 'fixed bottom-6 end-6 z-40 grid h-11 w-11 place-items-center rounded-full bg-primary text-primary-foreground shadow-lg',
44
44
  props.class,
45
45
  )}
46
46
  >
package/src/ui/Badge.tsx CHANGED
@@ -15,7 +15,7 @@ export type BadgeTone = 'neutral' | 'success' | 'warning' | 'danger' | 'info'
15
15
  const TONE_CLASSES: Record<BadgeTone, string> = {
16
16
  neutral: 'bg-muted text-muted-foreground ring-border',
17
17
  success: 'bg-emerald-500/15 text-emerald-300 ring-emerald-500/30 light:text-emerald-700',
18
- warning: 'bg-amber-500/15 text-amber-300 ring-amber-500/30 light:text-amber-700',
18
+ warning: 'bg-amber-500/15 text-amber-300 ring-amber-500/30 light:text-amber-800',
19
19
  danger: 'bg-rose-500/15 text-rose-300 ring-rose-500/30 light:text-rose-700',
20
20
  info: 'bg-sky-500/15 text-sky-300 ring-sky-500/30 light:text-sky-700',
21
21
  }
@@ -115,7 +115,7 @@ export function BlockEditor(props: BlockEditorProps): JSX.Element {
115
115
  onClick={() => setMenuOpen((open) => !open)}
116
116
  aria-expanded={menuOpen()}
117
117
  >
118
- <Plus class="mr-1 h-4 w-4" />
118
+ <Plus class="me-1 h-4 w-4" />
119
119
  Add block
120
120
  </Button>
121
121
 
@@ -17,7 +17,7 @@ interface ButtonGroupProps extends ParentProps {
17
17
  export type { ButtonGroupProps }
18
18
 
19
19
  const HORIZONTAL =
20
- 'flex-row [&>*:not(:first-child)]:-ml-px [&>*:not(:first-child)]:rounded-l-none [&>*:not(:last-child)]:rounded-r-none'
20
+ 'flex-row [&>*:not(:first-child)]:-ms-px [&>*:not(:first-child)]:rounded-s-none [&>*:not(:last-child)]:rounded-e-none'
21
21
  const VERTICAL =
22
22
  'flex-col [&>*]:w-full [&>*:not(:first-child)]:-mt-px [&>*:not(:first-child)]:rounded-t-none [&>*:not(:last-child)]:rounded-b-none'
23
23
 
@@ -48,7 +48,7 @@ export interface CalloutProps {
48
48
  export function Callout(props: CalloutProps): JSX.Element {
49
49
  const tone = () => TONE[props.tone ?? 'info']
50
50
  return (
51
- <div class={cn('flex gap-3 rounded-lg border-l-4 p-4 text-sm text-foreground', tone().wrap, props.class)}>
51
+ <div class={cn('flex gap-3 rounded-lg border-s-4 p-4 text-sm text-foreground', tone().wrap, props.class)}>
52
52
  <Show
53
53
  when={props.icon}
54
54
  fallback={<Dynamic component={tone().Icon} class={cn('mt-0.5 h-4 w-4 shrink-0', tone().icon)} />}
@@ -6,7 +6,9 @@ import { ChevronLeft, ChevronRight } from 'lucide-solid'
6
6
  import { For, createSignal, onCleanup, onMount, type JSX } from 'solid-js'
7
7
 
8
8
  import { cn } from '../lib/cn'
9
+ import { readingKey } from '../lib/direction'
9
10
  import { motionReduced } from '../lib/motion'
11
+ import { useDirection } from './DirectionProvider'
10
12
 
11
13
  export interface CarouselProps {
12
14
  /** Slides to page through, one visible at a time. */
@@ -37,6 +39,7 @@ export interface CarouselProps {
37
39
  * ```
38
40
  */
39
41
  export function Carousel(props: CarouselProps): JSX.Element {
42
+ const dir = useDirection()
40
43
  const [index, setIndex] = createSignal(0)
41
44
  const count = (): number => props.slides.length
42
45
 
@@ -49,10 +52,11 @@ export function Carousel(props: CarouselProps): JSX.Element {
49
52
  const next = (): void => go(index() + 1)
50
53
 
51
54
  const onKeyDown = (e: KeyboardEvent): void => {
52
- if (e.key === 'ArrowLeft') {
55
+ const key = readingKey(e.key, dir())
56
+ if (key === 'ArrowLeft') {
53
57
  e.preventDefault()
54
58
  prev()
55
- } else if (e.key === 'ArrowRight') {
59
+ } else if (key === 'ArrowRight') {
56
60
  e.preventDefault()
57
61
  next()
58
62
  }
@@ -80,8 +84,10 @@ export function Carousel(props: CarouselProps): JSX.Element {
80
84
  const width = viewport?.clientWidth ?? 1
81
85
  const dx = dragPx()
82
86
  const threshold = Math.max(48, width * 0.2)
83
- if (dx <= -threshold) next()
84
- else if (dx >= threshold) prev()
87
+ // A swipe toward the start of the line goes forward: left in LTR, right in RTL.
88
+ const forward = dir() === 'rtl' ? dx : -dx
89
+ if (forward >= threshold) next()
90
+ else if (forward <= -threshold) prev()
85
91
  setDragPx(0)
86
92
  setDragging(false)
87
93
  }
@@ -119,7 +125,9 @@ export function Carousel(props: CarouselProps): JSX.Element {
119
125
  swipeOn() && count() > 1 && 'touch-pan-y cursor-grab active:cursor-grabbing',
120
126
  !dragging() && 'transition-transform duration-500 ease-out',
121
127
  )}
122
- style={{ transform: `translateX(calc(-${index() * 100}% + ${dragPx()}px))` }}
128
+ style={{
129
+ transform: `translateX(calc(${index() * -100}% * var(--dir-x, 1) + ${dragPx()}px))`,
130
+ }}
123
131
  onPointerDown={onPointerDown}
124
132
  onPointerMove={onPointerMove}
125
133
  onPointerUp={endDrag}
@@ -132,17 +140,17 @@ export function Carousel(props: CarouselProps): JSX.Element {
132
140
  type="button"
133
141
  aria-label="Previous slide"
134
142
  onClick={prev}
135
- class="absolute left-2 top-1/2 flex h-9 w-9 -translate-y-1/2 items-center justify-center rounded-full bg-primary text-primary-foreground shadow transition-opacity hover:opacity-90"
143
+ class="absolute start-2 top-1/2 flex h-9 w-9 -translate-y-1/2 items-center justify-center rounded-full bg-primary text-primary-foreground shadow transition-opacity hover:opacity-90"
136
144
  >
137
- <ChevronLeft class="h-5 w-5" />
145
+ <ChevronLeft class="h-5 w-5 rtl:-scale-x-100" />
138
146
  </button>
139
147
  <button
140
148
  type="button"
141
149
  aria-label="Next slide"
142
150
  onClick={next}
143
- class="absolute right-2 top-1/2 flex h-9 w-9 -translate-y-1/2 items-center justify-center rounded-full bg-primary text-primary-foreground shadow transition-opacity hover:opacity-90"
151
+ class="absolute end-2 top-1/2 flex h-9 w-9 -translate-y-1/2 items-center justify-center rounded-full bg-primary text-primary-foreground shadow transition-opacity hover:opacity-90"
144
152
  >
145
- <ChevronRight class="h-5 w-5" />
153
+ <ChevronRight class="h-5 w-5 rtl:-scale-x-100" />
146
154
  </button>
147
155
  </div>
148
156