@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.
- package/README.md +66 -9
- package/dist/{Collapse-Cr5cDqAl.js → Collapse-CrKiFD8K.js} +173 -124
- package/dist/charts/Sparkline.d.ts +2 -0
- package/dist/charts.js +8 -5
- package/dist/commerce.js +59 -59
- package/dist/elements.css +248 -151
- package/dist/elements.iife.js +1 -1
- package/dist/elements.js +1025 -1015
- package/dist/full.css +248 -151
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4723 -4656
- package/dist/lib/anchored.d.ts +7 -0
- package/dist/lib/direction.d.ts +18 -0
- package/dist/styles.css +27 -11
- package/dist/theme.css +1559 -0
- package/dist/ui/DirectionProvider.d.ts +47 -0
- package/dist/ui/Drawer.d.ts +6 -2
- package/dist/ui/Sidebar.d.ts +8 -2
- package/dist/ui/Toast.d.ts +5 -1
- package/package.json +5 -3
- package/preset.d.ts +2 -1
- package/preset.js +16 -5
- package/src/charts/BarList.tsx +1 -1
- package/src/charts/Sparkline.tsx +5 -1
- package/src/commerce/CartLine.tsx +1 -1
- package/src/commerce/FacetSidebar.tsx +1 -1
- package/src/commerce/ProductCard.tsx +1 -1
- package/src/index.ts +7 -1
- package/src/layout/NavGroup.tsx +2 -2
- package/src/lib/anchored.ts +13 -0
- package/src/lib/direction.ts +35 -0
- package/src/ui/Accordion.tsx +2 -2
- package/src/ui/ActivityFeed.tsx +2 -2
- package/src/ui/AlertDialog.tsx +8 -4
- package/src/ui/Anchor.tsx +1 -1
- package/src/ui/AnnouncementBar.tsx +3 -3
- package/src/ui/ArtifactPanel.tsx +1 -1
- package/src/ui/AvailabilityPicker.tsx +2 -2
- package/src/ui/AvatarGroup.tsx +2 -2
- package/src/ui/BackToTop.tsx +1 -1
- package/src/ui/Badge.tsx +1 -1
- package/src/ui/BlockEditor.tsx +1 -1
- package/src/ui/ButtonGroup.tsx +1 -1
- package/src/ui/Callout.tsx +1 -1
- package/src/ui/Carousel.tsx +17 -9
- package/src/ui/Cascader.tsx +2 -2
- package/src/ui/CategoryStrip.tsx +4 -1
- package/src/ui/CodeEditor.tsx +5 -5
- package/src/ui/CodeTabs.tsx +1 -1
- package/src/ui/Combobox.tsx +2 -1
- package/src/ui/Command.tsx +9 -5
- package/src/ui/Comment.tsx +1 -1
- package/src/ui/Configurator.tsx +2 -2
- package/src/ui/ContextMenu.tsx +2 -1
- package/src/ui/DateField.tsx +4 -4
- package/src/ui/DateRangePicker.tsx +4 -4
- package/src/ui/DiffViewer.tsx +4 -3
- package/src/ui/DirectionProvider.tsx +82 -0
- package/src/ui/Drawer.tsx +29 -8
- package/src/ui/Dropdown.tsx +2 -1
- package/src/ui/EmojiPicker.tsx +5 -2
- package/src/ui/Expandable.tsx +2 -2
- package/src/ui/Faq.tsx +1 -1
- package/src/ui/Field.tsx +1 -1
- package/src/ui/FloatingActionButton.tsx +2 -2
- package/src/ui/FollowingPointer.tsx +2 -2
- package/src/ui/Hero.tsx +2 -2
- package/src/ui/HoldToConfirm.tsx +2 -2
- package/src/ui/HoverCard.tsx +2 -1
- package/src/ui/Image.tsx +9 -5
- package/src/ui/InlineSelect.tsx +1 -1
- package/src/ui/InputGroup.tsx +2 -2
- package/src/ui/Item.tsx +1 -1
- package/src/ui/JsonViewer.tsx +2 -2
- package/src/ui/Kanban.tsx +1 -1
- package/src/ui/KpiBlock.tsx +1 -1
- package/src/ui/Lightbox.tsx +9 -6
- package/src/ui/MasterDetail.tsx +1 -1
- package/src/ui/Mentions.tsx +1 -1
- package/src/ui/Menubar.tsx +9 -8
- package/src/ui/Modal.tsx +9 -5
- package/src/ui/ModelPicker.tsx +2 -1
- package/src/ui/MultiSelect.tsx +5 -5
- package/src/ui/NativeSelect.tsx +2 -2
- package/src/ui/NavigationMenu.tsx +5 -4
- package/src/ui/NotificationCenter.tsx +1 -1
- package/src/ui/NotificationStack.tsx +1 -1
- package/src/ui/NumberInput.tsx +2 -2
- package/src/ui/OnboardingChecklist.tsx +2 -2
- package/src/ui/Pagination.tsx +2 -2
- package/src/ui/PillSearch.tsx +3 -3
- package/src/ui/PivotTable.tsx +0 -0
- package/src/ui/Popover.tsx +2 -1
- package/src/ui/Portal.tsx +3 -1
- package/src/ui/PresenceAvatars.tsx +4 -4
- package/src/ui/PricingTable.tsx +1 -1
- package/src/ui/Prose.tsx +3 -3
- package/src/ui/QueryBuilder.tsx +4 -4
- package/src/ui/Rating.tsx +6 -2
- package/src/ui/ReasoningTrace.tsx +1 -1
- package/src/ui/ScrollArea.tsx +1 -1
- package/src/ui/ScrollProgress.tsx +2 -2
- package/src/ui/SideRail.tsx +3 -3
- package/src/ui/Sidebar.tsx +43 -23
- package/src/ui/SlashMenu.tsx +1 -1
- package/src/ui/Snippet.tsx +6 -3
- package/src/ui/Sortable.tsx +1 -1
- package/src/ui/SpecSheet.tsx +1 -1
- package/src/ui/SpeedDial.tsx +1 -1
- package/src/ui/Splitter.tsx +8 -3
- package/src/ui/SpreadsheetGrid.tsx +4 -4
- package/src/ui/Stepper.tsx +1 -1
- package/src/ui/SuggestionChips.tsx +1 -1
- package/src/ui/Switch.tsx +1 -1
- package/src/ui/Table.tsx +5 -2
- package/src/ui/TimeField.tsx +8 -8
- package/src/ui/Timeline.tsx +2 -2
- package/src/ui/Toast.tsx +9 -3
- package/src/ui/ToolCallTimeline.tsx +2 -2
- package/src/ui/Tooltip.tsx +2 -1
- package/src/ui/Tour.tsx +1 -1
- package/src/ui/TransactionFeed.tsx +1 -1
- package/src/ui/Transfer.tsx +2 -2
- package/src/ui/Tree.tsx +2 -2
- package/src/ui/TreeSelect.tsx +2 -2
- package/src/ui/TreeTable.tsx +4 -4
- package/src/ui/UsageMeter.tsx +1 -1
- package/src/ui/VideoPlayerShell.tsx +2 -2
- package/src/ui/internal/CalendarCore.tsx +6 -6
package/src/ui/Carousel.tsx
CHANGED
|
@@ -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
|
-
|
|
55
|
+
const key = readingKey(e.key, dir())
|
|
56
|
+
if (key === 'ArrowLeft') {
|
|
53
57
|
e.preventDefault()
|
|
54
58
|
prev()
|
|
55
|
-
} else if (
|
|
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
|
-
|
|
84
|
-
|
|
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={{
|
|
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
|
|
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
|
|
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
|
|
package/src/ui/Cascader.tsx
CHANGED
|
@@ -126,7 +126,7 @@ export function Cascader(props: CascaderProps): JSX.Element {
|
|
|
126
126
|
<div class="absolute z-50 mt-1 flex overflow-hidden rounded-md border border-border bg-card text-card-foreground shadow-md">
|
|
127
127
|
<For each={columns()}>
|
|
128
128
|
{(column, colIndex) => (
|
|
129
|
-
<ul class="min-w-[10rem] max-h-64 overflow-y-auto border-
|
|
129
|
+
<ul class="min-w-[10rem] max-h-64 overflow-y-auto border-e border-border p-1 last:border-e-0">
|
|
130
130
|
<For each={column}>
|
|
131
131
|
{(opt) => {
|
|
132
132
|
const hasChildren = () => !!opt.children && opt.children.length > 0
|
|
@@ -142,7 +142,7 @@ export function Cascader(props: CascaderProps): JSX.Element {
|
|
|
142
142
|
>
|
|
143
143
|
<span class="truncate">{opt.label}</span>
|
|
144
144
|
<Show when={hasChildren()}>
|
|
145
|
-
<ChevronRight class="h-4 w-4 shrink-0 opacity-60" />
|
|
145
|
+
<ChevronRight class="h-4 w-4 shrink-0 opacity-60 rtl:-scale-x-100" />
|
|
146
146
|
</Show>
|
|
147
147
|
</li>
|
|
148
148
|
)
|
package/src/ui/CategoryStrip.tsx
CHANGED
|
@@ -5,6 +5,8 @@ import type { JSX } from 'solid-js'
|
|
|
5
5
|
import { For } from 'solid-js'
|
|
6
6
|
|
|
7
7
|
import { cn } from '../lib/cn'
|
|
8
|
+
import { readingKey } from '../lib/direction'
|
|
9
|
+
import { useDirection } from './DirectionProvider'
|
|
8
10
|
|
|
9
11
|
/** A single selectable category: value, label, and optional leading icon. */
|
|
10
12
|
export interface CategoryItem {
|
|
@@ -31,6 +33,7 @@ export interface CategoryStripProps {
|
|
|
31
33
|
* ```
|
|
32
34
|
*/
|
|
33
35
|
export function CategoryStrip(props: CategoryStripProps): JSX.Element {
|
|
36
|
+
const dir = useDirection()
|
|
34
37
|
const activeIndex = () => props.items.findIndex((item) => item.value === props.value)
|
|
35
38
|
|
|
36
39
|
const onKeyDown = (event: KeyboardEvent) => {
|
|
@@ -39,7 +42,7 @@ export function CategoryStrip(props: CategoryStripProps): JSX.Element {
|
|
|
39
42
|
const count = props.items.length
|
|
40
43
|
if (count === 0) return
|
|
41
44
|
const current = activeIndex()
|
|
42
|
-
const delta = event.key === 'ArrowRight' ? 1 : -1
|
|
45
|
+
const delta = readingKey(event.key, dir()) === 'ArrowRight' ? 1 : -1
|
|
43
46
|
const next = ((current === -1 ? 0 : current) + delta + count) % count
|
|
44
47
|
props.onChange(props.items[next].value)
|
|
45
48
|
}
|
package/src/ui/CodeEditor.tsx
CHANGED
|
@@ -233,13 +233,13 @@ export function CodeEditor(props: CodeEditorProps): JSX.Element {
|
|
|
233
233
|
}
|
|
234
234
|
|
|
235
235
|
return (
|
|
236
|
-
<div
|
|
236
|
+
<div
|
|
237
|
+
dir="ltr"
|
|
238
|
+
class={cn('relative flex overflow-hidden rounded-md border border-border bg-card', local.class)}
|
|
239
|
+
>
|
|
237
240
|
<div
|
|
238
241
|
aria-hidden="true"
|
|
239
|
-
class={cn(
|
|
240
|
-
SHARED_TEXT,
|
|
241
|
-
'select-none border-r border-border bg-muted text-right text-muted-foreground',
|
|
242
|
-
)}
|
|
242
|
+
class={cn(SHARED_TEXT, 'select-none border-e border-border bg-muted text-end text-muted-foreground')}
|
|
243
243
|
>
|
|
244
244
|
<For each={lines()}>{(_line, index) => <div>{index() + 1}</div>}</For>
|
|
245
245
|
</div>
|
package/src/ui/CodeTabs.tsx
CHANGED
|
@@ -52,7 +52,7 @@ export function CodeTabs(props: CodeTabsProps): JSX.Element {
|
|
|
52
52
|
|
|
53
53
|
return (
|
|
54
54
|
<div class={cn('overflow-hidden rounded-lg border border-border bg-card', props.class)}>
|
|
55
|
-
<div class="flex items-center justify-between border-b border-border
|
|
55
|
+
<div class="flex items-center justify-between border-b border-border pe-2">
|
|
56
56
|
<div class="flex" role="tablist">
|
|
57
57
|
<For each={props.tabs}>
|
|
58
58
|
{(tab, index) => (
|
package/src/ui/Combobox.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import { Check, ChevronsUpDown } from 'lucide-solid'
|
|
|
4
4
|
import type { JSX } from 'solid-js'
|
|
5
5
|
|
|
6
6
|
import { cn } from '../lib/cn'
|
|
7
|
+
import { portalDir } from '../lib/direction'
|
|
7
8
|
|
|
8
9
|
interface ComboboxProps {
|
|
9
10
|
/** Full list of selectable string options (filtered as the user types). */
|
|
@@ -57,7 +58,7 @@ export function Combobox(props: ComboboxProps): JSX.Element {
|
|
|
57
58
|
</KCombobox.Icon>
|
|
58
59
|
</KCombobox.Trigger>
|
|
59
60
|
</KCombobox.Control>
|
|
60
|
-
<KCombobox.Portal>
|
|
61
|
+
<KCombobox.Portal ref={portalDir}>
|
|
61
62
|
<KCombobox.Content class="z-50 overflow-hidden rounded-md border border-border bg-card p-1 text-card-foreground shadow-sm">
|
|
62
63
|
<KCombobox.Listbox class="max-h-60 overflow-y-auto" />
|
|
63
64
|
</KCombobox.Content>
|
package/src/ui/Command.tsx
CHANGED
|
@@ -3,10 +3,14 @@
|
|
|
3
3
|
// handles fuzzy filtering (label + hint + group), keyboard navigation
|
|
4
4
|
// (↑/↓/Enter), optional grouping, and query reset on close. Theme-agnostic:
|
|
5
5
|
// every color comes from a semantic token, never a literal.
|
|
6
|
-
import
|
|
6
|
+
// Namespace import on purpose: Kobalte's `AlertDialog` is built with Object.assign on the
|
|
7
|
+
// same `DialogRoot` function as `Dialog`, so `Dialog.Content` depends on module load order (#118).
|
|
8
|
+
// The named exports (`Root`, `Content`, ...) are not affected.
|
|
9
|
+
import * as Dialog from '@kobalte/core/dialog'
|
|
7
10
|
import { createEffect, createMemo, createSignal, For, Show, type JSX } from 'solid-js'
|
|
8
11
|
|
|
9
12
|
import { cn } from '../lib/cn'
|
|
13
|
+
import { portalDir } from '../lib/direction'
|
|
10
14
|
|
|
11
15
|
/** A single selectable row in the palette. */
|
|
12
16
|
export interface CommandItem {
|
|
@@ -93,8 +97,8 @@ export function Command(props: CommandProps): JSX.Element {
|
|
|
93
97
|
}
|
|
94
98
|
|
|
95
99
|
return (
|
|
96
|
-
<Dialog open={props.open} onOpenChange={props.onOpenChange}>
|
|
97
|
-
<Dialog.Portal>
|
|
100
|
+
<Dialog.Root open={props.open} onOpenChange={props.onOpenChange}>
|
|
101
|
+
<Dialog.Portal ref={portalDir}>
|
|
98
102
|
<Dialog.Overlay class="modal-overlay fixed inset-0 z-40 bg-background/80 backdrop-blur-sm" />
|
|
99
103
|
<div class="fixed inset-x-0 top-[14vh] z-50 mx-auto w-full max-w-lg px-4">
|
|
100
104
|
<Dialog.Content
|
|
@@ -129,7 +133,7 @@ export function Command(props: CommandProps): JSX.Element {
|
|
|
129
133
|
onMouseEnter={() => setActive(i())}
|
|
130
134
|
onClick={() => pick(item)}
|
|
131
135
|
class={cn(
|
|
132
|
-
'flex w-full items-center justify-between gap-3 rounded-md px-3 py-2 text-
|
|
136
|
+
'flex w-full items-center justify-between gap-3 rounded-md px-3 py-2 text-start transition-colors',
|
|
133
137
|
active() === i() ? 'bg-primary/15 text-foreground' : 'text-muted-foreground',
|
|
134
138
|
)}
|
|
135
139
|
>
|
|
@@ -153,6 +157,6 @@ export function Command(props: CommandProps): JSX.Element {
|
|
|
153
157
|
</Dialog.Content>
|
|
154
158
|
</div>
|
|
155
159
|
</Dialog.Portal>
|
|
156
|
-
</Dialog>
|
|
160
|
+
</Dialog.Root>
|
|
157
161
|
)
|
|
158
162
|
}
|
package/src/ui/Comment.tsx
CHANGED
|
@@ -80,7 +80,7 @@ export function Comment(props: CommentProps): JSX.Element {
|
|
|
80
80
|
</div>
|
|
81
81
|
</div>
|
|
82
82
|
<Show when={props.comment.replies?.length}>
|
|
83
|
-
<div class="mt-4 space-y-4 border-
|
|
83
|
+
<div class="mt-4 space-y-4 border-s border-border ps-4">
|
|
84
84
|
<For each={props.comment.replies}>
|
|
85
85
|
{(reply) => <Comment comment={reply} actions={props.actions} />}
|
|
86
86
|
</For>
|
package/src/ui/Configurator.tsx
CHANGED
|
@@ -125,7 +125,7 @@ export function Configurator(props: ConfiguratorProps): JSX.Element {
|
|
|
125
125
|
>
|
|
126
126
|
{o.label}
|
|
127
127
|
<Show when={o.price}>
|
|
128
|
-
<span class="
|
|
128
|
+
<span class="ms-1 opacity-70">· {fmt(o.price as number)}</span>
|
|
129
129
|
</Show>
|
|
130
130
|
</button>
|
|
131
131
|
)}
|
|
@@ -144,7 +144,7 @@ export function Configurator(props: ConfiguratorProps): JSX.Element {
|
|
|
144
144
|
<span class="text-sm text-foreground">
|
|
145
145
|
{it.label}
|
|
146
146
|
<Show when={it.price}>
|
|
147
|
-
<span class="
|
|
147
|
+
<span class="ms-1 text-muted-foreground">· {fmt(it.price as number)}</span>
|
|
148
148
|
</Show>
|
|
149
149
|
</span>
|
|
150
150
|
<div class="flex items-center gap-2">
|
package/src/ui/ContextMenu.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import type { JSX, ParentProps } from 'solid-js'
|
|
|
4
4
|
import { For } from 'solid-js'
|
|
5
5
|
|
|
6
6
|
import { cn } from '../lib/cn'
|
|
7
|
+
import { portalDir } from '../lib/direction'
|
|
7
8
|
|
|
8
9
|
/** A single actionable row rendered by {@link ContextMenu}. */
|
|
9
10
|
export interface ContextMenuItem {
|
|
@@ -35,7 +36,7 @@ export function ContextMenu(props: ContextMenuProps): JSX.Element {
|
|
|
35
36
|
return (
|
|
36
37
|
<KContextMenu>
|
|
37
38
|
<KContextMenu.Trigger class={cn('inline-flex', props.class)}>{props.children}</KContextMenu.Trigger>
|
|
38
|
-
<KContextMenu.Portal>
|
|
39
|
+
<KContextMenu.Portal ref={portalDir}>
|
|
39
40
|
<KContextMenu.Content class="z-50 min-w-[10rem] overflow-hidden rounded-md border border-border bg-card p-1 text-card-foreground shadow-sm">
|
|
40
41
|
<For each={props.items}>
|
|
41
42
|
{(item) => (
|
package/src/ui/DateField.tsx
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
import { CalendarDays } from 'lucide-solid'
|
|
8
8
|
import type { JSX } from 'solid-js'
|
|
9
9
|
import { Show, createEffect, createMemo, createSignal, onCleanup } from 'solid-js'
|
|
10
|
-
import { Portal } from '
|
|
10
|
+
import { Portal } from './Portal'
|
|
11
11
|
|
|
12
|
-
import { createAnchorReflow } from '../lib/anchored'
|
|
12
|
+
import { anchoredLeft, createAnchorReflow } from '../lib/anchored'
|
|
13
13
|
import { cn } from '../lib/cn'
|
|
14
14
|
import { CalendarCore } from './internal/CalendarCore'
|
|
15
15
|
|
|
@@ -93,7 +93,7 @@ export function DateField(props: DateFieldProps): JSX.Element {
|
|
|
93
93
|
const place = () => {
|
|
94
94
|
if (!btnRef) return
|
|
95
95
|
const r = btnRef.getBoundingClientRect()
|
|
96
|
-
const left =
|
|
96
|
+
const left = anchoredLeft(btnRef, POP_W)
|
|
97
97
|
setPos({ top: r.bottom + 4, left })
|
|
98
98
|
}
|
|
99
99
|
|
|
@@ -155,7 +155,7 @@ export function DateField(props: DateFieldProps): JSX.Element {
|
|
|
155
155
|
type="button"
|
|
156
156
|
disabled={props.disabled}
|
|
157
157
|
onClick={() => (open() ? close() : openPopover())}
|
|
158
|
-
class="flex w-full items-center justify-between gap-2 rounded-md border border-input bg-background px-3 py-2 text-
|
|
158
|
+
class="flex w-full 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"
|
|
159
159
|
>
|
|
160
160
|
<span class={displayLabel(props.value, props.months ?? MONTHS) ? '' : 'text-muted-foreground'}>
|
|
161
161
|
{displayLabel(props.value, props.months ?? MONTHS) ?? props.label ?? 'Select date'}
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
import { CalendarDays } from 'lucide-solid'
|
|
8
8
|
import type { JSX } from 'solid-js'
|
|
9
9
|
import { createEffect, createMemo, createSignal, onCleanup, Show } from 'solid-js'
|
|
10
|
-
import { Portal } from '
|
|
10
|
+
import { Portal } from './Portal'
|
|
11
11
|
|
|
12
|
-
import { createAnchorReflow } from '../lib/anchored'
|
|
12
|
+
import { anchoredLeft, createAnchorReflow } from '../lib/anchored'
|
|
13
13
|
import { cn } from '../lib/cn'
|
|
14
14
|
import { CalendarCore } from './internal/CalendarCore'
|
|
15
15
|
|
|
@@ -94,7 +94,7 @@ export function DateRangePicker(props: DateRangePickerProps): JSX.Element {
|
|
|
94
94
|
const place = () => {
|
|
95
95
|
if (!btnRef) return
|
|
96
96
|
const r = btnRef.getBoundingClientRect()
|
|
97
|
-
const left =
|
|
97
|
+
const left = anchoredLeft(btnRef, POP_W)
|
|
98
98
|
setPos({ top: r.bottom + 4, left })
|
|
99
99
|
}
|
|
100
100
|
|
|
@@ -181,7 +181,7 @@ export function DateRangePicker(props: DateRangePickerProps): JSX.Element {
|
|
|
181
181
|
type="button"
|
|
182
182
|
disabled={props.disabled}
|
|
183
183
|
onClick={() => (open() ? close() : openPopover())}
|
|
184
|
-
class="flex w-full items-center justify-between gap-2 rounded-md border border-input bg-background px-3 py-2 text-
|
|
184
|
+
class="flex w-full 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"
|
|
185
185
|
>
|
|
186
186
|
<span class={triggerLabel() ? '' : 'text-muted-foreground'}>
|
|
187
187
|
{triggerLabel() ?? props.label ?? 'Select range'}
|
package/src/ui/DiffViewer.tsx
CHANGED
|
@@ -131,6 +131,7 @@ export function DiffViewer(props: DiffViewerProps): JSX.Element {
|
|
|
131
131
|
return (
|
|
132
132
|
<div
|
|
133
133
|
role="figure"
|
|
134
|
+
dir="ltr"
|
|
134
135
|
aria-label={ariaLabel()}
|
|
135
136
|
class={cn('overflow-hidden rounded-lg border border-border bg-card', props.class)}
|
|
136
137
|
>
|
|
@@ -144,16 +145,16 @@ export function DiffViewer(props: DiffViewerProps): JSX.Element {
|
|
|
144
145
|
<For each={rows()}>
|
|
145
146
|
{(line) => (
|
|
146
147
|
<div class={cn('flex', ROW_BG[line.type])}>
|
|
147
|
-
<span class="w-12 shrink-0 select-none py-0.5
|
|
148
|
+
<span class="w-12 shrink-0 select-none py-0.5 pe-2 text-end tabular-nums text-xs text-muted-foreground">
|
|
148
149
|
{line.oldNum ?? ''}
|
|
149
150
|
</span>
|
|
150
|
-
<span class="w-12 shrink-0 select-none py-0.5
|
|
151
|
+
<span class="w-12 shrink-0 select-none py-0.5 pe-2 text-end tabular-nums text-xs text-muted-foreground">
|
|
151
152
|
{line.newNum ?? ''}
|
|
152
153
|
</span>
|
|
153
154
|
<span class={cn('w-5 shrink-0 select-none py-0.5 text-center', MARKER_TONE[line.type])}>
|
|
154
155
|
{MARKER[line.type]}
|
|
155
156
|
</span>
|
|
156
|
-
<span class={cn('flex-1 whitespace-pre py-0.5
|
|
157
|
+
<span class={cn('flex-1 whitespace-pre py-0.5 pe-4', CONTENT_TONE[line.type])}>
|
|
157
158
|
{line.content}
|
|
158
159
|
</span>
|
|
159
160
|
</div>
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Right-to-left support: one provider that sets `dir` on the DOM and tells
|
|
2
|
+
// Kobalte which direction to use for keyboard navigation.
|
|
3
|
+
import { I18nProvider, getReadingDirection } from '@kobalte/core/i18n'
|
|
4
|
+
import { type Accessor, type JSX, createMemo, splitProps, useContext } from 'solid-js'
|
|
5
|
+
|
|
6
|
+
import { type Direction, DirectionContext } from '../lib/direction'
|
|
7
|
+
import { cn } from '../lib/cn'
|
|
8
|
+
|
|
9
|
+
export type { Direction }
|
|
10
|
+
|
|
11
|
+
/** Props for {@link DirectionProvider}. */
|
|
12
|
+
export interface DirectionProviderProps {
|
|
13
|
+
/**
|
|
14
|
+
* Text direction. When omitted it is derived from `locale`; when both are
|
|
15
|
+
* omitted it is `'ltr'`.
|
|
16
|
+
*/
|
|
17
|
+
dir?: Direction
|
|
18
|
+
/**
|
|
19
|
+
* BCP 47 locale (for example `'ar'`, `'he-IL'`). Forwarded to Kobalte for
|
|
20
|
+
* number/date formatting and collation. Its reading direction must agree
|
|
21
|
+
* with `dir`; a mismatch throws.
|
|
22
|
+
*/
|
|
23
|
+
locale?: string
|
|
24
|
+
/** Extra classes for the wrapper element. */
|
|
25
|
+
class?: string
|
|
26
|
+
children?: JSX.Element
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Sets the reading direction for everything inside it.
|
|
31
|
+
*
|
|
32
|
+
* - Renders a `display: contents` wrapper carrying the `dir` attribute, so
|
|
33
|
+
* Tailwind logical utilities (`ms-*`, `start-*`, `text-start`) and the
|
|
34
|
+
* browser's bidi handling follow it.
|
|
35
|
+
* - Wraps Kobalte's `I18nProvider`, so arrow keys in Tabs, Menubar, Slider,
|
|
36
|
+
* menus and the like reverse under `rtl`.
|
|
37
|
+
* - Overlays that portal out to `<body>` (Modal, Drawer, Popover, Dropdown,
|
|
38
|
+
* Toast, ...) copy the direction onto their own container.
|
|
39
|
+
*
|
|
40
|
+
* Without a provider nothing changes: components inherit `dir` from the page
|
|
41
|
+
* (set `<html dir="rtl">` yourself in that case).
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```tsx
|
|
45
|
+
* <DirectionProvider dir="rtl" locale="ar">
|
|
46
|
+
* <App />
|
|
47
|
+
* </DirectionProvider>
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export function DirectionProvider(props: DirectionProviderProps): JSX.Element {
|
|
51
|
+
const [local] = splitProps(props, ['dir', 'locale', 'class', 'children'])
|
|
52
|
+
const resolved = createMemo<Direction>(() => {
|
|
53
|
+
const fromLocale = local.locale ? getReadingDirection(local.locale) : undefined
|
|
54
|
+
if (local.dir && fromLocale && local.dir !== fromLocale) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`DirectionProvider: dir="${local.dir}" conflicts with locale "${local.locale}" (reading direction "${fromLocale}").`,
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
return local.dir ?? fromLocale ?? 'ltr'
|
|
60
|
+
})
|
|
61
|
+
const locale = createMemo(() => local.locale ?? (resolved() === 'rtl' ? 'ar' : 'en-US'))
|
|
62
|
+
return (
|
|
63
|
+
<DirectionContext.Provider value={resolved}>
|
|
64
|
+
<I18nProvider locale={locale()}>
|
|
65
|
+
<div dir={resolved()} lang={local.locale} class={cn('contents', local.class)}>
|
|
66
|
+
{local.children}
|
|
67
|
+
</div>
|
|
68
|
+
</I18nProvider>
|
|
69
|
+
</DirectionContext.Provider>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Accessor of the current direction: the nearest {@link DirectionProvider}'s,
|
|
75
|
+
* or outside one the `dir` attribute of `<html>` (`'ltr'` when unset).
|
|
76
|
+
*/
|
|
77
|
+
export function useDirection(): Accessor<Direction> {
|
|
78
|
+
const ctx = useContext(DirectionContext)
|
|
79
|
+
if (ctx) return ctx
|
|
80
|
+
return () =>
|
|
81
|
+
typeof document !== 'undefined' && document.documentElement.getAttribute('dir') === 'rtl' ? 'rtl' : 'ltr'
|
|
82
|
+
}
|
package/src/ui/Drawer.tsx
CHANGED
|
@@ -3,12 +3,16 @@
|
|
|
3
3
|
// slide-in/out keyframes in styles.css off Kobalte's data-expanded/data-closed.
|
|
4
4
|
// Pass a `title` for the sticky glass header, or omit it and render your own
|
|
5
5
|
// inside `children`.
|
|
6
|
-
import
|
|
6
|
+
// Namespace import on purpose: Kobalte's `AlertDialog` is built with Object.assign on the
|
|
7
|
+
// same `DialogRoot` function as `Dialog`, so `Dialog.Content` depends on module load order (#118).
|
|
8
|
+
// The named exports (`Root`, `Content`, ...) are not affected.
|
|
9
|
+
import * as Dialog from '@kobalte/core/dialog'
|
|
7
10
|
import { X } from 'lucide-solid'
|
|
8
11
|
import type { JSX, ParentProps } from 'solid-js'
|
|
9
12
|
import { Show } from 'solid-js'
|
|
10
13
|
|
|
11
14
|
import { cn } from '../lib/cn'
|
|
15
|
+
import { portalDir } from '../lib/direction'
|
|
12
16
|
|
|
13
17
|
interface DrawerProps extends ParentProps {
|
|
14
18
|
open: boolean
|
|
@@ -24,19 +28,36 @@ interface DrawerProps extends ParentProps {
|
|
|
24
28
|
side?: DrawerSide
|
|
25
29
|
}
|
|
26
30
|
|
|
27
|
-
|
|
31
|
+
/**
|
|
32
|
+
* `left` / `right` are physical edges. `start` / `end` follow the reading
|
|
33
|
+
* direction (`start` is the right edge under `dir="rtl"`).
|
|
34
|
+
*/
|
|
35
|
+
export type DrawerSide = 'right' | 'left' | 'start' | 'end' | 'top' | 'bottom'
|
|
28
36
|
|
|
29
37
|
const SIDE: Record<DrawerSide, { wrap: string; panel: string; offscreen: string }> = {
|
|
38
|
+
// `right` / `left` are physical and never flip; `end` / `start` follow the
|
|
39
|
+
// reading direction (flex `justify-*` and `border-s/e` are logical, while the
|
|
40
|
+
// slide offset uses `--dir-x`, which `[dir=rtl]` sets to -1 in tokens.css).
|
|
30
41
|
right: {
|
|
31
|
-
wrap: 'justify-end',
|
|
42
|
+
wrap: 'justify-end rtl:justify-start',
|
|
32
43
|
panel: 'h-full w-full max-w-[520px] border-l',
|
|
33
44
|
offscreen: 'translateX(100%)',
|
|
34
45
|
},
|
|
35
46
|
left: {
|
|
36
|
-
wrap: 'justify-start',
|
|
47
|
+
wrap: 'justify-start rtl:justify-end',
|
|
37
48
|
panel: 'h-full w-full max-w-[520px] border-r',
|
|
38
49
|
offscreen: 'translateX(-100%)',
|
|
39
50
|
},
|
|
51
|
+
end: {
|
|
52
|
+
wrap: 'justify-end',
|
|
53
|
+
panel: 'h-full w-full max-w-[520px] border-s',
|
|
54
|
+
offscreen: 'translateX(calc(100% * var(--dir-x, 1)))',
|
|
55
|
+
},
|
|
56
|
+
start: {
|
|
57
|
+
wrap: 'justify-start',
|
|
58
|
+
panel: 'h-full w-full max-w-[520px] border-e',
|
|
59
|
+
offscreen: 'translateX(calc(-100% * var(--dir-x, 1)))',
|
|
60
|
+
},
|
|
40
61
|
top: {
|
|
41
62
|
wrap: 'flex-col justify-start',
|
|
42
63
|
panel: 'max-h-[85vh] w-full rounded-b-2xl border-b',
|
|
@@ -52,7 +73,7 @@ const SIDE: Record<DrawerSide, { wrap: string; panel: string; offscreen: string
|
|
|
52
73
|
export type { DrawerProps }
|
|
53
74
|
|
|
54
75
|
/**
|
|
55
|
-
* Edge-anchored slide-over panel (right by default, or `side="left" | "top" | "bottom"`) built on Kobalte's `Dialog` (focus trap, portal,
|
|
76
|
+
* Edge-anchored slide-over panel (right by default, or `side="left" | "start" | "end" | "top" | "bottom"`) built on Kobalte's `Dialog` (focus trap, portal,
|
|
56
77
|
* and open/close presence handled for you). Use for create/edit forms or detail
|
|
57
78
|
* panels that need more room than a centered `Modal`.
|
|
58
79
|
*
|
|
@@ -67,8 +88,8 @@ export type { DrawerProps }
|
|
|
67
88
|
export function Drawer(props: DrawerProps): JSX.Element {
|
|
68
89
|
const side = (): (typeof SIDE)[DrawerSide] => SIDE[props.side ?? 'right']
|
|
69
90
|
return (
|
|
70
|
-
<Dialog open={props.open} onOpenChange={props.onOpenChange}>
|
|
71
|
-
<Dialog.Portal>
|
|
91
|
+
<Dialog.Root open={props.open} onOpenChange={props.onOpenChange}>
|
|
92
|
+
<Dialog.Portal ref={portalDir}>
|
|
72
93
|
<Dialog.Overlay class="modal-overlay fixed inset-0 z-40 bg-black/50 backdrop-blur-[2px]" />
|
|
73
94
|
<div class={cn('fixed inset-0 z-50 flex', side().wrap)}>
|
|
74
95
|
<Dialog.Content
|
|
@@ -100,6 +121,6 @@ export function Drawer(props: DrawerProps): JSX.Element {
|
|
|
100
121
|
</Dialog.Content>
|
|
101
122
|
</div>
|
|
102
123
|
</Dialog.Portal>
|
|
103
|
-
</Dialog>
|
|
124
|
+
</Dialog.Root>
|
|
104
125
|
)
|
|
105
126
|
}
|
package/src/ui/Dropdown.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import type { JSX, ParentProps } from 'solid-js'
|
|
|
4
4
|
import { For } from 'solid-js'
|
|
5
5
|
|
|
6
6
|
import { cn } from '../lib/cn'
|
|
7
|
+
import { portalDir } from '../lib/direction'
|
|
7
8
|
|
|
8
9
|
/** A single selectable row in a {@link Dropdown} menu. */
|
|
9
10
|
export interface DropdownItem {
|
|
@@ -48,7 +49,7 @@ export function Dropdown(props: DropdownProps): JSX.Element {
|
|
|
48
49
|
<DropdownMenu.Trigger class={cn('inline-flex', props.class)} aria-label={props.label}>
|
|
49
50
|
{props.children}
|
|
50
51
|
</DropdownMenu.Trigger>
|
|
51
|
-
<DropdownMenu.Portal>
|
|
52
|
+
<DropdownMenu.Portal ref={portalDir}>
|
|
52
53
|
<DropdownMenu.Content class="z-50 min-w-[10rem] overflow-hidden rounded-md border border-border bg-card p-1 text-card-foreground shadow-sm">
|
|
53
54
|
<For each={props.items}>
|
|
54
55
|
{(item) => (
|
package/src/ui/EmojiPicker.tsx
CHANGED
|
@@ -8,6 +8,8 @@ import { Dumbbell, Hash, Lightbulb, Leaf, Plane, Search, Smile, Users, Utensils
|
|
|
8
8
|
import { createMemo, createSignal, For, Show, type Component, type JSX } from 'solid-js'
|
|
9
9
|
|
|
10
10
|
import { cn } from '../lib/cn'
|
|
11
|
+
import { readingKey } from '../lib/direction'
|
|
12
|
+
import { useDirection } from './DirectionProvider'
|
|
11
13
|
|
|
12
14
|
type Category = 'Smileys' | 'People' | 'Nature' | 'Food' | 'Activity' | 'Travel' | 'Objects' | 'Symbols'
|
|
13
15
|
|
|
@@ -276,6 +278,7 @@ export interface EmojiPickerProps {
|
|
|
276
278
|
* ```
|
|
277
279
|
*/
|
|
278
280
|
export function EmojiPicker(props: EmojiPickerProps): JSX.Element {
|
|
281
|
+
const dir = useDirection()
|
|
279
282
|
const [query, setQuery] = createSignal('')
|
|
280
283
|
const [recents, setRecents] = createSignal<string[]>([])
|
|
281
284
|
|
|
@@ -320,9 +323,9 @@ export function EmojiPicker(props: EmojiPickerProps): JSX.Element {
|
|
|
320
323
|
if (index === -1) return
|
|
321
324
|
const columns = 8
|
|
322
325
|
const delta =
|
|
323
|
-
event.key === 'ArrowRight'
|
|
326
|
+
readingKey(event.key, dir()) === 'ArrowRight'
|
|
324
327
|
? 1
|
|
325
|
-
: event.key === 'ArrowLeft'
|
|
328
|
+
: readingKey(event.key, dir()) === 'ArrowLeft'
|
|
326
329
|
? -1
|
|
327
330
|
: event.key === 'ArrowDown'
|
|
328
331
|
? columns
|
package/src/ui/Expandable.tsx
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// scale, so its content never stretches. Covers the motion.dev "family dialog"
|
|
5
5
|
// (size="dialog") and "app store layout" (size="full") patterns with one API.
|
|
6
6
|
import { createSignal, Show, onCleanup, onMount, type JSX } from 'solid-js'
|
|
7
|
-
import { Portal } from '
|
|
7
|
+
import { Portal } from './Portal'
|
|
8
8
|
|
|
9
9
|
import { cn } from '../lib/cn'
|
|
10
10
|
import { animate, motionReduced } from '../lib/motion'
|
|
@@ -206,7 +206,7 @@ export function Expandable(props: ExpandableProps): JSX.Element {
|
|
|
206
206
|
type="button"
|
|
207
207
|
aria-label="Close"
|
|
208
208
|
onClick={doClose}
|
|
209
|
-
class="absolute
|
|
209
|
+
class="absolute end-3 top-3 z-10 grid h-8 w-8 place-items-center rounded-full bg-muted/80 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
|
210
210
|
>
|
|
211
211
|
<svg
|
|
212
212
|
viewBox="0 0 24 24"
|
package/src/ui/Faq.tsx
CHANGED
|
@@ -41,7 +41,7 @@ export function Faq(props: FaqProps): JSX.Element {
|
|
|
41
41
|
class={cn('mx-auto flex w-full max-w-3xl flex-col gap-10 px-5 py-20', props.class)}
|
|
42
42
|
>
|
|
43
43
|
<SectionHeading title={props.title} subtitle={props.subtitle} />
|
|
44
|
-
<div class="card rounded-xl px-5 sm:px-6 [&_button]:py-4 [&_button]:text-
|
|
44
|
+
<div class="card rounded-xl px-5 sm:px-6 [&_button]:py-4 [&_button]:text-start [&_button]:text-base">
|
|
45
45
|
<Accordion
|
|
46
46
|
multiple={props.multiple}
|
|
47
47
|
class="[&>*:last-child]:border-b-0"
|
package/src/ui/Field.tsx
CHANGED
|
@@ -364,7 +364,7 @@ export function FieldError(props: FieldErrorProps): JSX.Element {
|
|
|
364
364
|
<Show when={visible()}>
|
|
365
365
|
<div id={id} role="alert" data-slot="field-error" class={cn('text-xs text-destructive', props.class)}>
|
|
366
366
|
<Show when={messages().length > 1} fallback={<>{messages()[0] ?? props.children}</>}>
|
|
367
|
-
<ul class="
|
|
367
|
+
<ul class="ms-4 list-disc space-y-0.5">
|
|
368
368
|
<For each={messages()}>{(m) => <li>{m}</li>}</For>
|
|
369
369
|
</ul>
|
|
370
370
|
</Show>
|
|
@@ -7,8 +7,8 @@ import { spawnRipple } from './Ripple'
|
|
|
7
7
|
export type FloatingActionButtonPosition = 'bottom-right' | 'bottom-left'
|
|
8
8
|
|
|
9
9
|
const POSITION_CLASSES: Record<FloatingActionButtonPosition, string> = {
|
|
10
|
-
'bottom-right': 'bottom-6
|
|
11
|
-
'bottom-left': 'bottom-6
|
|
10
|
+
'bottom-right': 'bottom-6 end-6',
|
|
11
|
+
'bottom-left': 'bottom-6 start-6',
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
// brightness-110 on hover keeps the tint theme-agnostic — it lifts whatever
|
|
@@ -75,7 +75,7 @@ export function FollowingPointer(props: FollowingPointerProps): JSX.Element {
|
|
|
75
75
|
<div
|
|
76
76
|
ref={pointerEl}
|
|
77
77
|
aria-hidden="true"
|
|
78
|
-
class="pointer-events-none absolute
|
|
78
|
+
class="pointer-events-none absolute start-0 top-0 z-50 flex items-center opacity-0 transition-opacity duration-150 will-change-transform"
|
|
79
79
|
>
|
|
80
80
|
<svg
|
|
81
81
|
width="20"
|
|
@@ -96,7 +96,7 @@ export function FollowingPointer(props: FollowingPointerProps): JSX.Element {
|
|
|
96
96
|
<Show when={props.label}>
|
|
97
97
|
<div
|
|
98
98
|
class={cn(
|
|
99
|
-
'
|
|
99
|
+
'ms-2 whitespace-nowrap rounded-full px-2.5 py-1 text-xs font-medium shadow-lg',
|
|
100
100
|
tone() === 'accent'
|
|
101
101
|
? 'bg-accent text-accent-foreground'
|
|
102
102
|
: 'bg-primary text-primary-foreground',
|