@refraction-ui/astro 0.17.4 → 0.18.1
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/dist/analytics-sink-app-insights/app-insights-sink.ts +13 -3
- package/dist/analytics-sink-posthog/client-sdk-sink.ts +12 -1
- package/dist/analytics-sink-posthog/replay.ts +10 -1
- package/dist/astro-calendar/Calendar.astro +7 -2
- package/dist/astro-date-picker/DatePicker.astro +10 -2
- package/dist/astro-file-tree/FileTree.astro +86 -0
- package/dist/astro-file-tree/FileTree.stories.ts +29 -2
- package/dist/astro-file-tree/FileTreeItems.astro +77 -0
- package/dist/astro-file-tree/index.ts +2 -4
- package/dist/astro-pagination/Pagination.astro +100 -1
- package/dist/astro-pagination/Pagination.stories.ts +11 -1
- package/dist/astro-pagination/index.ts +1 -1
- package/dist/astro-skip-to-content/SkipToContent.astro +22 -1
- package/dist/astro-skip-to-content/SkipToContent.stories.ts +7 -1
- package/dist/astro-skip-to-content/index.ts +1 -1
- package/dist/astro-slider/Slider.astro +75 -0
- package/dist/astro-slider/Slider.stories.ts +24 -0
- package/dist/astro-slider/index.ts +1 -1
- package/dist/astro-steps/Step.astro +20 -0
- package/dist/astro-steps/StepContent.astro +17 -0
- package/dist/astro-steps/StepDescription.astro +17 -0
- package/dist/astro-steps/StepIndicator.astro +20 -0
- package/dist/astro-steps/StepTitle.astro +17 -0
- package/dist/astro-steps/Steps.astro +17 -1
- package/dist/astro-steps/Steps.stories.ts +7 -1
- package/dist/astro-steps/index.ts +6 -1
- package/dist/astro-toast/Toaster.astro +3 -3
- package/dist/avatar-group/avatar-group.styles.ts +5 -5
- package/dist/callout/callout.styles.ts +1 -1
- package/dist/empty-state/empty-state.styles.ts +2 -2
- package/dist/file-tree/file-tree.styles.ts +31 -0
- package/dist/file-tree/file-tree.ts +360 -0
- package/dist/file-tree/index.ts +15 -0
- package/dist/input/input.styles.ts +1 -1
- package/dist/presence-indicator/presence-indicator.styles.ts +5 -5
- package/dist/progress-display/progress-display.styles.ts +2 -2
- package/dist/status-indicator/status-indicator.styles.ts +12 -12
- package/dist/toast/toast.styles.ts +3 -6
- package/package.json +1 -1
- package/dist/astro-file-tree/astro-file-tree.astro +0 -4
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { createStepContent } from '../steps/index.ts'
|
|
3
|
+
import { cn } from '../shared/index.ts'
|
|
4
|
+
|
|
5
|
+
interface Props extends astroHTML.JSX.HTMLAttributes {}
|
|
6
|
+
|
|
7
|
+
const { class: className, ...props } = Astro.props
|
|
8
|
+
const api = createStepContent()
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<div
|
|
12
|
+
class={cn('flex-1 pb-8', className)}
|
|
13
|
+
{...api.dataAttributes}
|
|
14
|
+
{...props}
|
|
15
|
+
>
|
|
16
|
+
<slot />
|
|
17
|
+
</div>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { createStepDescription, stepDescriptionVariants } from '../steps/index.ts'
|
|
3
|
+
import { cn } from '../shared/index.ts'
|
|
4
|
+
|
|
5
|
+
interface Props extends astroHTML.JSX.HTMLAttributes {}
|
|
6
|
+
|
|
7
|
+
const { class: className, ...props } = Astro.props
|
|
8
|
+
const api = createStepDescription()
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<p
|
|
12
|
+
class={cn(stepDescriptionVariants(), className)}
|
|
13
|
+
{...api.dataAttributes}
|
|
14
|
+
{...props}
|
|
15
|
+
>
|
|
16
|
+
<slot />
|
|
17
|
+
</p>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { createStepIndicator, stepIndicatorVariants } from '../steps/index.ts'
|
|
3
|
+
import { cn } from '../shared/index.ts'
|
|
4
|
+
|
|
5
|
+
interface Props extends astroHTML.JSX.HTMLAttributes {
|
|
6
|
+
status?: 'completed' | 'active' | 'upcoming'
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { status, class: className, ...props } = Astro.props
|
|
10
|
+
const api = createStepIndicator()
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
<div
|
|
14
|
+
class={cn(stepIndicatorVariants({ status }), className)}
|
|
15
|
+
data-state={status ?? 'upcoming'}
|
|
16
|
+
{...api.dataAttributes}
|
|
17
|
+
{...props}
|
|
18
|
+
>
|
|
19
|
+
<slot />
|
|
20
|
+
</div>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { createStepTitle, stepTitleVariants } from '../steps/index.ts'
|
|
3
|
+
import { cn } from '../shared/index.ts'
|
|
4
|
+
|
|
5
|
+
interface Props extends astroHTML.JSX.HTMLAttributes {}
|
|
6
|
+
|
|
7
|
+
const { class: className, ...props } = Astro.props
|
|
8
|
+
const api = createStepTitle()
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<h4
|
|
12
|
+
class={cn(stepTitleVariants(), className)}
|
|
13
|
+
{...api.dataAttributes}
|
|
14
|
+
{...props}
|
|
15
|
+
>
|
|
16
|
+
<slot />
|
|
17
|
+
</h4>
|
|
@@ -1,3 +1,19 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { createSteps, stepsVariants } from '../steps/index.ts'
|
|
3
|
+
import { cn } from '../shared/index.ts'
|
|
4
|
+
|
|
5
|
+
interface Props extends astroHTML.JSX.HTMLAttributes {
|
|
6
|
+
orientation?: 'vertical' | 'horizontal'
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { orientation, class: className, ...props } = Astro.props
|
|
10
|
+
const api = createSteps()
|
|
2
11
|
---
|
|
3
|
-
|
|
12
|
+
|
|
13
|
+
<div
|
|
14
|
+
class={cn(stepsVariants({ orientation }), className)}
|
|
15
|
+
{...api.dataAttributes}
|
|
16
|
+
{...props}
|
|
17
|
+
>
|
|
18
|
+
<slot />
|
|
19
|
+
</div>
|
|
@@ -3,10 +3,16 @@ import Component from './Steps.astro'
|
|
|
3
3
|
const meta = {
|
|
4
4
|
title: 'Astro/Steps',
|
|
5
5
|
component: Component,
|
|
6
|
+
argTypes: {
|
|
7
|
+
orientation: { control: 'select', options: ['vertical', 'horizontal'] },
|
|
8
|
+
},
|
|
6
9
|
}
|
|
7
10
|
|
|
8
11
|
export default meta
|
|
9
12
|
|
|
10
13
|
export const Default = {
|
|
11
|
-
args: {
|
|
14
|
+
args: {
|
|
15
|
+
default: '<div data-slot="step" data-state="completed"><strong>1. Account</strong></div><div data-slot="step" data-state="active"><strong>2. Profile</strong></div><div data-slot="step" data-state="upcoming"><strong>3. Review</strong></div>',
|
|
16
|
+
orientation: 'vertical',
|
|
17
|
+
}
|
|
12
18
|
}
|
|
@@ -1 +1,6 @@
|
|
|
1
|
-
export { default as Steps } from './Steps.astro'
|
|
1
|
+
export { default as Steps } from './Steps.astro'
|
|
2
|
+
export { default as Step } from './Step.astro'
|
|
3
|
+
export { default as StepIndicator } from './StepIndicator.astro'
|
|
4
|
+
export { default as StepContent } from './StepContent.astro'
|
|
5
|
+
export { default as StepTitle } from './StepTitle.astro'
|
|
6
|
+
export { default as StepDescription } from './StepDescription.astro'
|
|
@@ -88,9 +88,9 @@ const { class: className, ...props } = Astro.props
|
|
|
88
88
|
|
|
89
89
|
const VARIANT_CLASSES: Record<string, string> = {
|
|
90
90
|
default: 'border bg-background text-foreground',
|
|
91
|
-
success: 'border-l-4 border-
|
|
92
|
-
error: 'border-l-4 border-
|
|
93
|
-
warning: 'border-l-4 border-
|
|
91
|
+
success: 'border-l-4 border-success/50 bg-success/10 text-success',
|
|
92
|
+
error: 'border-l-4 border-destructive/50 bg-destructive/10 text-destructive',
|
|
93
|
+
warning: 'border-l-4 border-warning/50 bg-warning/10 text-warning',
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
function createToastElement(options: ToastOptions): HTMLElement {
|
|
@@ -49,11 +49,11 @@ export const avatarPresenceDotVariants = cva({
|
|
|
49
49
|
xl: 'h-4 w-4',
|
|
50
50
|
},
|
|
51
51
|
status: {
|
|
52
|
-
online: 'bg-
|
|
53
|
-
offline: 'bg-
|
|
54
|
-
away: 'bg-
|
|
55
|
-
busy: 'bg-
|
|
56
|
-
dnd: 'bg-
|
|
52
|
+
online: 'bg-success',
|
|
53
|
+
offline: 'bg-neutral',
|
|
54
|
+
away: 'bg-warning',
|
|
55
|
+
busy: 'bg-destructive',
|
|
56
|
+
dnd: 'bg-destructive',
|
|
57
57
|
},
|
|
58
58
|
},
|
|
59
59
|
defaultVariants: {
|
|
@@ -8,7 +8,7 @@ export const calloutVariants = cva({
|
|
|
8
8
|
destructive: 'bg-destructive/10 border-destructive/20 text-destructive',
|
|
9
9
|
success: 'bg-success/10 border-success/20 text-success',
|
|
10
10
|
warning: 'bg-warning/10 border-warning/20 text-warning',
|
|
11
|
-
info: 'bg-
|
|
11
|
+
info: 'bg-info/10 border-info/20 text-info',
|
|
12
12
|
},
|
|
13
13
|
},
|
|
14
14
|
defaultVariants: {
|
|
@@ -26,8 +26,8 @@ export const emptyStateIconChipVariants = cva({
|
|
|
26
26
|
variants: {
|
|
27
27
|
tone: {
|
|
28
28
|
neutral: 'bg-muted text-muted-foreground',
|
|
29
|
-
success: 'bg-
|
|
30
|
-
warning: 'bg-
|
|
29
|
+
success: 'bg-success/10 text-success',
|
|
30
|
+
warning: 'bg-warning/10 text-warning',
|
|
31
31
|
danger: 'bg-destructive/10 text-destructive',
|
|
32
32
|
},
|
|
33
33
|
},
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { cva } from '../shared/index.ts'
|
|
2
|
+
|
|
3
|
+
/** Tree container. */
|
|
4
|
+
export const fileTreeVariants = cva({
|
|
5
|
+
base: 'w-full text-sm',
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A single tree row (treeitem). The `selected` variant carries the
|
|
10
|
+
* selected-row treatment so adapters never inline these classes.
|
|
11
|
+
*/
|
|
12
|
+
export const fileTreeItemVariants = cva({
|
|
13
|
+
base: [
|
|
14
|
+
'flex w-full cursor-pointer select-none items-center gap-1.5 rounded-md px-2 py-1',
|
|
15
|
+
'text-sm outline-none transition-colors',
|
|
16
|
+
'focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background',
|
|
17
|
+
'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
|
18
|
+
].join(' '),
|
|
19
|
+
variants: {
|
|
20
|
+
selected: {
|
|
21
|
+
true: 'bg-accent text-accent-foreground',
|
|
22
|
+
false: 'text-foreground hover:bg-muted',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
defaultVariants: {
|
|
26
|
+
selected: 'false',
|
|
27
|
+
},
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
/** Expand/collapse glyph or file marker preceding the label. */
|
|
31
|
+
export const fileTreeGlyphClass = 'w-4 shrink-0 text-center text-muted-foreground'
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @refraction-ui/file-tree — headless tree view.
|
|
3
|
+
*
|
|
4
|
+
* Owns the *behavior* of a hierarchical tree: the node model, expand/collapse
|
|
5
|
+
* state, single selection, roving focus, and WAI-ARIA treeview keyboard
|
|
6
|
+
* semantics. It has NO UI opinion — adapters render the nested markup and
|
|
7
|
+
* spread the ARIA objects.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** A node in the tree. Parents carry `children`; leaves do not. */
|
|
11
|
+
export interface FileTreeNode {
|
|
12
|
+
/** Unique, stable id (used for expansion/selection/focus). */
|
|
13
|
+
id: string
|
|
14
|
+
/** Display label. */
|
|
15
|
+
label: string
|
|
16
|
+
/** Child nodes — presence (even empty is not allowed: omit for leaves) marks a parent. */
|
|
17
|
+
children?: FileTreeNode[]
|
|
18
|
+
/** Disabled nodes cannot be selected, toggled, or focused via click. */
|
|
19
|
+
disabled?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Options for {@link createFileTree}. */
|
|
23
|
+
export interface FileTreeConfig {
|
|
24
|
+
/** Root-level nodes. */
|
|
25
|
+
nodes?: FileTreeNode[]
|
|
26
|
+
/** Controlled expanded node ids. Pair with `onExpandedChange`. */
|
|
27
|
+
expandedIds?: string[]
|
|
28
|
+
/** Uncontrolled initial expanded node ids. */
|
|
29
|
+
defaultExpandedIds?: string[]
|
|
30
|
+
/** Controlled selected node id (`null` = none). Pair with `onSelectionChange`. */
|
|
31
|
+
selectedId?: string | null
|
|
32
|
+
/** Uncontrolled initial selected node id. */
|
|
33
|
+
defaultSelectedId?: string | null
|
|
34
|
+
/** Called whenever the expanded set changes. */
|
|
35
|
+
onExpandedChange?: (ids: string[]) => void
|
|
36
|
+
/** Called whenever the selection changes. */
|
|
37
|
+
onSelectionChange?: (id: string | null) => void
|
|
38
|
+
/** Accessible label for the tree (default `'File tree'`). */
|
|
39
|
+
'aria-label'?: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Snapshot of the store. Returned by `getState()`; treat as immutable. */
|
|
43
|
+
export interface FileTreeState {
|
|
44
|
+
nodes: FileTreeNode[]
|
|
45
|
+
expandedIds: string[]
|
|
46
|
+
selectedId: string | null
|
|
47
|
+
/** Node holding the roving-tabindex focus (null = the first visible item). */
|
|
48
|
+
focusedId: string | null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A row of the flattened, currently-visible tree (DFS order). */
|
|
52
|
+
export interface VisibleTreeItem {
|
|
53
|
+
node: FileTreeNode
|
|
54
|
+
/** Id of the parent node, or null for root-level items. */
|
|
55
|
+
parentId: string | null
|
|
56
|
+
/** 1-based nesting depth (ARIA `aria-level`). */
|
|
57
|
+
depth: number
|
|
58
|
+
hasChildren: boolean
|
|
59
|
+
isExpanded: boolean
|
|
60
|
+
isSelected: boolean
|
|
61
|
+
isFocused: boolean
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The framework-agnostic store. React/Astro adapters wrap this. */
|
|
65
|
+
export interface FileTreeAPI {
|
|
66
|
+
/** Current immutable snapshot */
|
|
67
|
+
getState(): FileTreeState
|
|
68
|
+
/** Subscribe to changes; returns an unsubscribe fn (suits useSyncExternalStore) */
|
|
69
|
+
subscribe(listener: () => void): () => void
|
|
70
|
+
|
|
71
|
+
/** Flattened visible rows in DFS order (children of collapsed parents omitted) */
|
|
72
|
+
getVisibleItems(): VisibleTreeItem[]
|
|
73
|
+
isExpanded(id: string): boolean
|
|
74
|
+
/** Toggle a parent's expanded state (no-op for leaves/disabled nodes) */
|
|
75
|
+
toggle(id: string): void
|
|
76
|
+
expand(id: string): void
|
|
77
|
+
collapse(id: string): void
|
|
78
|
+
/** Select a node (`null` clears). No-op for unknown/disabled nodes. */
|
|
79
|
+
select(id: string | null): void
|
|
80
|
+
/** Move the roving focus to a node */
|
|
81
|
+
focus(id: string | null): void
|
|
82
|
+
/**
|
|
83
|
+
* WAI-ARIA treeview keyboard semantics, applied to the focused item:
|
|
84
|
+
* ArrowDown/Up move, ArrowRight expands or descends, ArrowLeft collapses or
|
|
85
|
+
* ascends, Home/End jump, Enter/Space selects.
|
|
86
|
+
*/
|
|
87
|
+
handleKey(key: string): void
|
|
88
|
+
|
|
89
|
+
/** Replace the node model (adapters sync the prop) */
|
|
90
|
+
setNodes(nodes: FileTreeNode[]): void
|
|
91
|
+
/** Sync a controlled expanded set */
|
|
92
|
+
setExpandedIds(ids: string[]): void
|
|
93
|
+
/** Sync a controlled selection */
|
|
94
|
+
setSelectedId(id: string | null): void
|
|
95
|
+
/** Update callbacks/options after creation */
|
|
96
|
+
setOptions(options: {
|
|
97
|
+
onExpandedChange?: (ids: string[]) => void
|
|
98
|
+
onSelectionChange?: (id: string | null) => void
|
|
99
|
+
'aria-label'?: string
|
|
100
|
+
}): void
|
|
101
|
+
|
|
102
|
+
/** ARIA attributes for the tree container (`role="tree"`). */
|
|
103
|
+
getTreeAria(): Record<string, string | number | boolean>
|
|
104
|
+
/** ARIA attributes for one visible row (`role="treeitem"`). */
|
|
105
|
+
getItemAria(item: VisibleTreeItem): Record<string, string | number | boolean>
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function arrayEquals(a: string[], b: string[]): boolean {
|
|
109
|
+
return a.length === b.length && a.every((v, i) => v === b[i])
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* createFileTree — headless tree store with expand/collapse, single
|
|
114
|
+
* selection, roving focus, and treeview keyboard semantics. Supports
|
|
115
|
+
* controlled (`expandedIds` / `selectedId`) and uncontrolled
|
|
116
|
+
* (`defaultExpandedIds` / `defaultSelectedId`) usage.
|
|
117
|
+
*/
|
|
118
|
+
export function createFileTree(config: FileTreeConfig = {}): FileTreeAPI {
|
|
119
|
+
let nodes: FileTreeNode[] = config.nodes ?? []
|
|
120
|
+
let ariaLabel = config['aria-label'] ?? 'File tree'
|
|
121
|
+
let onExpandedChange = config.onExpandedChange
|
|
122
|
+
let onSelectionChange = config.onSelectionChange
|
|
123
|
+
|
|
124
|
+
let uncontrolledExpanded: string[] = config.defaultExpandedIds ?? []
|
|
125
|
+
let controlledExpanded: string[] | undefined = config.expandedIds
|
|
126
|
+
let uncontrolledSelected: string | null = config.defaultSelectedId ?? null
|
|
127
|
+
let controlledSelected: string | null | undefined = config.selectedId
|
|
128
|
+
let focusedId: string | null = null
|
|
129
|
+
|
|
130
|
+
// Indexes rebuilt whenever the node model changes.
|
|
131
|
+
let parentById = new Map<string, string | null>()
|
|
132
|
+
let nodeById = new Map<string, FileTreeNode>()
|
|
133
|
+
|
|
134
|
+
function reindex(): void {
|
|
135
|
+
parentById = new Map()
|
|
136
|
+
nodeById = new Map()
|
|
137
|
+
const walk = (list: FileTreeNode[], parentId: string | null) => {
|
|
138
|
+
for (const node of list) {
|
|
139
|
+
nodeById.set(node.id, node)
|
|
140
|
+
parentById.set(node.id, parentId)
|
|
141
|
+
if (node.children) walk(node.children, node.id)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
walk(nodes, null)
|
|
145
|
+
}
|
|
146
|
+
reindex()
|
|
147
|
+
|
|
148
|
+
const listeners = new Set<() => void>()
|
|
149
|
+
let snapshot: FileTreeState = buildSnapshot()
|
|
150
|
+
let visibleItems: VisibleTreeItem[] = buildVisibleItems()
|
|
151
|
+
|
|
152
|
+
function expandedIds(): string[] {
|
|
153
|
+
return controlledExpanded !== undefined ? controlledExpanded : uncontrolledExpanded
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function selectedId(): string | null {
|
|
157
|
+
return controlledSelected !== undefined ? controlledSelected : uncontrolledSelected
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function buildSnapshot(): FileTreeState {
|
|
161
|
+
return {
|
|
162
|
+
nodes,
|
|
163
|
+
expandedIds: expandedIds(),
|
|
164
|
+
selectedId: selectedId(),
|
|
165
|
+
focusedId,
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function buildVisibleItems(): VisibleTreeItem[] {
|
|
170
|
+
const expanded = new Set(expandedIds())
|
|
171
|
+
const selected = selectedId()
|
|
172
|
+
const items: VisibleTreeItem[] = []
|
|
173
|
+
const walk = (list: FileTreeNode[], parentId: string | null, depth: number) => {
|
|
174
|
+
for (const node of list) {
|
|
175
|
+
const hasChildren = Boolean(node.children && node.children.length > 0)
|
|
176
|
+
const isExpanded = hasChildren && expanded.has(node.id)
|
|
177
|
+
items.push({
|
|
178
|
+
node,
|
|
179
|
+
parentId,
|
|
180
|
+
depth,
|
|
181
|
+
hasChildren,
|
|
182
|
+
isExpanded,
|
|
183
|
+
isSelected: node.id === selected,
|
|
184
|
+
isFocused: node.id === focusedId,
|
|
185
|
+
})
|
|
186
|
+
if (hasChildren && isExpanded) walk(node.children!, node.id, depth + 1)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
walk(nodes, null, 1)
|
|
190
|
+
return items
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function emit(): void {
|
|
194
|
+
snapshot = buildSnapshot()
|
|
195
|
+
visibleItems = buildVisibleItems()
|
|
196
|
+
for (const l of listeners) l()
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function setExpanded(ids: string[]): void {
|
|
200
|
+
if (controlledExpanded === undefined) {
|
|
201
|
+
if (arrayEquals(uncontrolledExpanded, ids)) return
|
|
202
|
+
uncontrolledExpanded = ids
|
|
203
|
+
}
|
|
204
|
+
onExpandedChange?.(ids)
|
|
205
|
+
emit()
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function isExpandable(id: string): boolean {
|
|
209
|
+
const node = nodeById.get(id)
|
|
210
|
+
return Boolean(node && !node.disabled && node.children && node.children.length > 0)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const api: FileTreeAPI = {
|
|
214
|
+
getState() {
|
|
215
|
+
return snapshot
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
subscribe(listener) {
|
|
219
|
+
listeners.add(listener)
|
|
220
|
+
return () => listeners.delete(listener)
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
getVisibleItems() {
|
|
224
|
+
return visibleItems
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
isExpanded(id) {
|
|
228
|
+
return expandedIds().includes(id)
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
toggle(id) {
|
|
232
|
+
if (!isExpandable(id)) return
|
|
233
|
+
const current = expandedIds()
|
|
234
|
+
setExpanded(current.includes(id) ? current.filter((v) => v !== id) : [...current, id])
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
expand(id) {
|
|
238
|
+
if (!isExpandable(id)) return
|
|
239
|
+
const current = expandedIds()
|
|
240
|
+
if (current.includes(id)) return
|
|
241
|
+
setExpanded([...current, id])
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
collapse(id) {
|
|
245
|
+
const current = expandedIds()
|
|
246
|
+
if (!current.includes(id)) return
|
|
247
|
+
setExpanded(current.filter((v) => v !== id))
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
select(id) {
|
|
251
|
+
if (id !== null) {
|
|
252
|
+
const node = nodeById.get(id)
|
|
253
|
+
if (!node || node.disabled) return
|
|
254
|
+
}
|
|
255
|
+
if (controlledSelected === undefined) {
|
|
256
|
+
if (uncontrolledSelected === id) return
|
|
257
|
+
uncontrolledSelected = id
|
|
258
|
+
}
|
|
259
|
+
onSelectionChange?.(id)
|
|
260
|
+
emit()
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
focus(id) {
|
|
264
|
+
// Focus is only meaningful on rows that are actually rendered.
|
|
265
|
+
if (id !== null && !visibleItems.some((i) => i.node.id === id)) return
|
|
266
|
+
if (focusedId === id) return
|
|
267
|
+
focusedId = id
|
|
268
|
+
emit()
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
handleKey(key) {
|
|
272
|
+
const items = visibleItems
|
|
273
|
+
if (items.length === 0) return
|
|
274
|
+
const currentIndex = focusedId ? items.findIndex((i) => i.node.id === focusedId) : -1
|
|
275
|
+
|
|
276
|
+
switch (key) {
|
|
277
|
+
case 'ArrowDown':
|
|
278
|
+
api.focus(items[Math.min(currentIndex + 1, items.length - 1)]!.node.id)
|
|
279
|
+
return
|
|
280
|
+
case 'ArrowUp':
|
|
281
|
+
api.focus(items[Math.max(currentIndex - 1, 0)]!.node.id)
|
|
282
|
+
return
|
|
283
|
+
case 'Home':
|
|
284
|
+
api.focus(items[0]!.node.id)
|
|
285
|
+
return
|
|
286
|
+
case 'End':
|
|
287
|
+
api.focus(items[items.length - 1]!.node.id)
|
|
288
|
+
return
|
|
289
|
+
case 'ArrowRight': {
|
|
290
|
+
const item = currentIndex === -1 ? undefined : items[currentIndex]
|
|
291
|
+
if (!item) {
|
|
292
|
+
api.focus(items[0]!.node.id)
|
|
293
|
+
return
|
|
294
|
+
}
|
|
295
|
+
if (!item.hasChildren) return
|
|
296
|
+
if (!item.isExpanded) api.expand(item.node.id)
|
|
297
|
+
// Expanded: descend to the first child (DFS order puts it next).
|
|
298
|
+
else api.focus(items[currentIndex + 1]?.node.id ?? item.node.id)
|
|
299
|
+
return
|
|
300
|
+
}
|
|
301
|
+
case 'ArrowLeft': {
|
|
302
|
+
const item = currentIndex === -1 ? undefined : items[currentIndex]
|
|
303
|
+
if (!item) return
|
|
304
|
+
if (item.hasChildren && item.isExpanded) api.collapse(item.node.id)
|
|
305
|
+
else if (item.parentId) api.focus(item.parentId)
|
|
306
|
+
return
|
|
307
|
+
}
|
|
308
|
+
case 'Enter':
|
|
309
|
+
case ' ': {
|
|
310
|
+
const item = currentIndex === -1 ? undefined : items[currentIndex]
|
|
311
|
+
if (item) api.select(item.node.id)
|
|
312
|
+
return
|
|
313
|
+
}
|
|
314
|
+
default:
|
|
315
|
+
return
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
|
|
319
|
+
setNodes(nextNodes) {
|
|
320
|
+
nodes = nextNodes
|
|
321
|
+
reindex()
|
|
322
|
+
emit()
|
|
323
|
+
},
|
|
324
|
+
|
|
325
|
+
setExpandedIds(ids) {
|
|
326
|
+
if (controlledExpanded !== undefined && arrayEquals(controlledExpanded, ids)) return
|
|
327
|
+
controlledExpanded = ids
|
|
328
|
+
emit()
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
setSelectedId(id) {
|
|
332
|
+
if (controlledSelected !== undefined && controlledSelected === id) return
|
|
333
|
+
controlledSelected = id
|
|
334
|
+
emit()
|
|
335
|
+
},
|
|
336
|
+
|
|
337
|
+
setOptions(options) {
|
|
338
|
+
if (options.onExpandedChange !== undefined) onExpandedChange = options.onExpandedChange
|
|
339
|
+
if (options.onSelectionChange !== undefined) onSelectionChange = options.onSelectionChange
|
|
340
|
+
if (options['aria-label'] !== undefined) ariaLabel = options['aria-label']
|
|
341
|
+
},
|
|
342
|
+
|
|
343
|
+
getTreeAria(): Record<string, string | number | boolean> {
|
|
344
|
+
return { role: 'tree', 'aria-label': ariaLabel }
|
|
345
|
+
},
|
|
346
|
+
|
|
347
|
+
getItemAria(item): Record<string, string | number | boolean> {
|
|
348
|
+
const aria: Record<string, string | number | boolean> = {
|
|
349
|
+
role: 'treeitem',
|
|
350
|
+
'aria-level': item.depth,
|
|
351
|
+
'aria-selected': item.isSelected,
|
|
352
|
+
}
|
|
353
|
+
if (item.hasChildren) aria['aria-expanded'] = item.isExpanded
|
|
354
|
+
if (item.node.disabled) aria['aria-disabled'] = true
|
|
355
|
+
return aria
|
|
356
|
+
},
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return api
|
|
360
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { createFileTree } from './file-tree.js'
|
|
2
|
+
|
|
3
|
+
export type {
|
|
4
|
+
FileTreeNode,
|
|
5
|
+
FileTreeConfig,
|
|
6
|
+
FileTreeState,
|
|
7
|
+
FileTreeAPI,
|
|
8
|
+
VisibleTreeItem,
|
|
9
|
+
} from './file-tree.js'
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
fileTreeVariants,
|
|
13
|
+
fileTreeItemVariants,
|
|
14
|
+
fileTreeGlyphClass,
|
|
15
|
+
} from './file-tree.styles.js'
|
|
@@ -9,7 +9,7 @@ export const inputVariants = cva({
|
|
|
9
9
|
lg: 'h-10 text-base',
|
|
10
10
|
},
|
|
11
11
|
validationState: {
|
|
12
|
-
valid: 'border-
|
|
12
|
+
valid: 'border-success focus-visible:ring-success',
|
|
13
13
|
invalid: 'border-destructive focus-visible:ring-destructive',
|
|
14
14
|
},
|
|
15
15
|
},
|
|
@@ -4,11 +4,11 @@ export const presenceDotVariants = cva({
|
|
|
4
4
|
base: 'inline-block rounded-full',
|
|
5
5
|
variants: {
|
|
6
6
|
status: {
|
|
7
|
-
online: 'bg-
|
|
8
|
-
offline: 'bg-
|
|
9
|
-
away: 'bg-
|
|
10
|
-
busy: 'bg-
|
|
11
|
-
dnd: 'bg-
|
|
7
|
+
online: 'bg-success',
|
|
8
|
+
offline: 'bg-neutral',
|
|
9
|
+
away: 'bg-warning',
|
|
10
|
+
busy: 'bg-destructive',
|
|
11
|
+
dnd: 'bg-destructive',
|
|
12
12
|
},
|
|
13
13
|
size: {
|
|
14
14
|
sm: 'h-2 w-2',
|
|
@@ -20,8 +20,8 @@ export const statCardVariants = cva({
|
|
|
20
20
|
color: {
|
|
21
21
|
default: 'bg-card text-card-foreground',
|
|
22
22
|
primary: 'bg-primary/10 text-primary border-primary/20',
|
|
23
|
-
success: 'bg-
|
|
24
|
-
warning: 'bg-
|
|
23
|
+
success: 'bg-success/10 text-success border-success/20',
|
|
24
|
+
warning: 'bg-warning/10 text-warning border-warning/20',
|
|
25
25
|
destructive: 'bg-destructive/10 text-destructive border-destructive/20',
|
|
26
26
|
},
|
|
27
27
|
},
|
|
@@ -7,12 +7,12 @@ export const statusDotVariants = cva({
|
|
|
7
7
|
base: 'inline-block h-2 w-2 rounded-full',
|
|
8
8
|
variants: {
|
|
9
9
|
type: {
|
|
10
|
-
success: 'bg-
|
|
11
|
-
error: 'bg-
|
|
12
|
-
warning: 'bg-
|
|
13
|
-
info: 'bg-
|
|
14
|
-
pending: 'bg-
|
|
15
|
-
neutral: 'bg-
|
|
10
|
+
success: 'bg-success',
|
|
11
|
+
error: 'bg-destructive',
|
|
12
|
+
warning: 'bg-warning',
|
|
13
|
+
info: 'bg-info',
|
|
14
|
+
pending: 'bg-pending',
|
|
15
|
+
neutral: 'bg-neutral',
|
|
16
16
|
},
|
|
17
17
|
},
|
|
18
18
|
defaultVariants: {
|
|
@@ -24,12 +24,12 @@ export const statusPulseVariants = cva({
|
|
|
24
24
|
base: 'animate-pulse inline-block h-2 w-2 rounded-full',
|
|
25
25
|
variants: {
|
|
26
26
|
type: {
|
|
27
|
-
success: 'bg-
|
|
28
|
-
error: 'bg-
|
|
29
|
-
warning: 'bg-
|
|
30
|
-
info: 'bg-
|
|
31
|
-
pending: 'bg-
|
|
32
|
-
neutral: 'bg-
|
|
27
|
+
success: 'bg-success',
|
|
28
|
+
error: 'bg-destructive',
|
|
29
|
+
warning: 'bg-warning',
|
|
30
|
+
info: 'bg-info',
|
|
31
|
+
pending: 'bg-pending',
|
|
32
|
+
neutral: 'bg-neutral',
|
|
33
33
|
},
|
|
34
34
|
},
|
|
35
35
|
defaultVariants: {
|