aeon-ui-engine 1.1.0 → 1.2.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/CHANGELOG.md +19 -0
- package/CONSUMER.md +45 -0
- package/package.json +1 -1
- package/packages/core/dist/index.d.ts +88 -8
- package/packages/core/dist/index.js +113 -0
- package/packages/core/src/anatomy.ts +38 -0
- package/packages/core/src/brand-theme.ts +172 -0
- package/packages/core/src/index.ts +1 -0
- package/packages/panda/aeon.css +1 -0
- package/packages/panda/src/theme/consumer-shell.css +129 -0
- package/packages/panda/src/theme/electron.css +30 -0
- package/packages/primitives/dist/chunk-4NOKRUSG.js +81 -0
- package/packages/primitives/dist/index.d.ts +1 -0
- package/packages/primitives/dist/index.js +16 -6
- package/packages/primitives/dist/machines/app-nav.d.ts +96 -0
- package/packages/primitives/dist/machines/app-nav.js +12 -0
- package/packages/primitives/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/packages/primitives/src/__tests__/app-nav.test.ts +30 -0
- package/packages/primitives/src/index.ts +1 -0
- package/packages/primitives/src/machines/app-nav.ts +107 -0
- package/packages/react/dist/index.d.ts +135 -3
- package/packages/react/dist/index.js +1126 -720
- package/packages/react/src/app-nav/index.tsx +249 -0
- package/packages/react/src/avatar/index.tsx +4 -1
- package/packages/react/src/hooks/use-fit-text.ts +59 -0
- package/packages/react/src/index.ts +4 -0
- package/packages/react/src/prompt/index.tsx +161 -0
- package/packages/react/src/status-banner/index.tsx +84 -0
- package/packages/ui/src/app-nav.tsx +2 -0
- package/packages/ui/src/index.ts +4 -0
- package/packages/ui/src/prompt.tsx +29 -0
- package/packages/ui/src/status-banner.tsx +1 -0
- package/scripts/vite-aliases.mjs +8 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import {
|
|
2
|
+
appNavAnatomy,
|
|
3
|
+
partAttrs,
|
|
4
|
+
scopeAttrs,
|
|
5
|
+
} from '@aeon-ui/core'
|
|
6
|
+
import {
|
|
7
|
+
appNavChild,
|
|
8
|
+
appNavMachine,
|
|
9
|
+
appNavStateAttr,
|
|
10
|
+
type AppNavChild,
|
|
11
|
+
type AppNavContext,
|
|
12
|
+
} from '@aeon-ui/primitives'
|
|
13
|
+
import {
|
|
14
|
+
createContext,
|
|
15
|
+
forwardRef,
|
|
16
|
+
useContext,
|
|
17
|
+
useMemo,
|
|
18
|
+
type ButtonHTMLAttributes,
|
|
19
|
+
type HTMLAttributes,
|
|
20
|
+
type ReactNode,
|
|
21
|
+
} from 'react'
|
|
22
|
+
import { useAeonMachine } from '../hooks/use-aeon-machine.js'
|
|
23
|
+
import { mergeProps } from '../utils/merge-props.js'
|
|
24
|
+
|
|
25
|
+
export type { AppNavChild, AppNavContext }
|
|
26
|
+
|
|
27
|
+
interface AppNavContextValue {
|
|
28
|
+
section: string
|
|
29
|
+
stack: AppNavChild[]
|
|
30
|
+
child: AppNavChild | null
|
|
31
|
+
stateAttr: string
|
|
32
|
+
send: ReturnType<typeof useAeonMachine<typeof appNavMachine>>[1]
|
|
33
|
+
setSection: (section: string) => void
|
|
34
|
+
push: (child: AppNavChild) => void
|
|
35
|
+
replace: (child: AppNavChild) => void
|
|
36
|
+
pop: () => void
|
|
37
|
+
popTo: (id: string) => void
|
|
38
|
+
clear: () => void
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const AppNavCtx = createContext<AppNavContextValue | null>(null)
|
|
42
|
+
|
|
43
|
+
export function useAppNav() {
|
|
44
|
+
const ctx = useContext(AppNavCtx)
|
|
45
|
+
if (!ctx) throw new Error('AppNav parts must be used within AppNav.Root')
|
|
46
|
+
return ctx
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Use the machine without the compound tree. */
|
|
50
|
+
export function useAppNavMachine(input?: { section?: string; stack?: AppNavChild[] }) {
|
|
51
|
+
const [snapshot, send] = useAeonMachine(appNavMachine, {
|
|
52
|
+
input: { section: input?.section ?? 'home', stack: input?.stack ?? [] },
|
|
53
|
+
})
|
|
54
|
+
const section = snapshot.context.section
|
|
55
|
+
const stack = snapshot.context.stack
|
|
56
|
+
const child = appNavChild(snapshot.context)
|
|
57
|
+
const stateAttr = appNavStateAttr(snapshot.context)
|
|
58
|
+
|
|
59
|
+
return useMemo(
|
|
60
|
+
() => ({
|
|
61
|
+
section,
|
|
62
|
+
stack,
|
|
63
|
+
child,
|
|
64
|
+
stateAttr,
|
|
65
|
+
send,
|
|
66
|
+
setSection: (next: string) => send({ type: 'SET_SECTION', section: next }),
|
|
67
|
+
push: (next: AppNavChild) => send({ type: 'PUSH', child: next }),
|
|
68
|
+
replace: (next: AppNavChild) => send({ type: 'REPLACE', child: next }),
|
|
69
|
+
pop: () => send({ type: 'POP' }),
|
|
70
|
+
popTo: (id: string) => send({ type: 'POP_TO', id }),
|
|
71
|
+
clear: () => send({ type: 'CLEAR' }),
|
|
72
|
+
reset: (next: string) => send({ type: 'RESET', section: next }),
|
|
73
|
+
}),
|
|
74
|
+
[section, stack, child, stateAttr, send],
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface AppNavRootProps extends HTMLAttributes<HTMLDivElement> {
|
|
79
|
+
defaultSection?: string
|
|
80
|
+
children?: ReactNode
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const Root = forwardRef<HTMLDivElement, AppNavRootProps>(function AppNavRoot(
|
|
84
|
+
{ defaultSection = 'home', children, ...rest },
|
|
85
|
+
ref,
|
|
86
|
+
) {
|
|
87
|
+
const nav = useAppNavMachine({ section: defaultSection })
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<AppNavCtx.Provider value={nav}>
|
|
91
|
+
<div
|
|
92
|
+
ref={ref}
|
|
93
|
+
{...mergeProps(
|
|
94
|
+
scopeAttrs(appNavAnatomy.scope, appNavAnatomy.root, { state: nav.stateAttr }),
|
|
95
|
+
rest,
|
|
96
|
+
)}
|
|
97
|
+
>
|
|
98
|
+
{children}
|
|
99
|
+
</div>
|
|
100
|
+
</AppNavCtx.Provider>
|
|
101
|
+
)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const Rail = forwardRef<HTMLElement, HTMLAttributes<HTMLElement>>(function AppNavRail(props, ref) {
|
|
105
|
+
const { stateAttr } = useAppNav()
|
|
106
|
+
return (
|
|
107
|
+
<nav
|
|
108
|
+
ref={ref as never}
|
|
109
|
+
{...mergeProps(partAttrs(appNavAnatomy.scope, appNavAnatomy.rail, { state: stateAttr }), props)}
|
|
110
|
+
/>
|
|
111
|
+
)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
export interface AppNavSectionProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
115
|
+
value: string
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const Section = forwardRef<HTMLButtonElement, AppNavSectionProps>(function AppNavSection(
|
|
119
|
+
{ value, onClick, ...rest },
|
|
120
|
+
ref,
|
|
121
|
+
) {
|
|
122
|
+
const { section, child, setSection } = useAppNav()
|
|
123
|
+
const isActive = section === value && !child
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<button
|
|
127
|
+
ref={ref}
|
|
128
|
+
type="button"
|
|
129
|
+
aria-current={isActive ? 'page' : undefined}
|
|
130
|
+
data-aeon-value={value}
|
|
131
|
+
onClick={(e) => {
|
|
132
|
+
onClick?.(e)
|
|
133
|
+
if (e.defaultPrevented) return
|
|
134
|
+
setSection(value)
|
|
135
|
+
}}
|
|
136
|
+
{...mergeProps(
|
|
137
|
+
partAttrs(appNavAnatomy.scope, appNavAnatomy.section, {
|
|
138
|
+
state: section === value ? (child ? 'section-active' : 'active') : 'inactive',
|
|
139
|
+
}),
|
|
140
|
+
rest,
|
|
141
|
+
)}
|
|
142
|
+
/>
|
|
143
|
+
)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
const Stage = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(function AppNavStage(
|
|
147
|
+
props,
|
|
148
|
+
ref,
|
|
149
|
+
) {
|
|
150
|
+
const { stateAttr } = useAppNav()
|
|
151
|
+
return (
|
|
152
|
+
<div
|
|
153
|
+
ref={ref}
|
|
154
|
+
{...mergeProps(partAttrs(appNavAnatomy.scope, appNavAnatomy.stage, { state: stateAttr }), props)}
|
|
155
|
+
/>
|
|
156
|
+
)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
export interface AppNavBreadcrumbProps extends HTMLAttributes<HTMLElement> {
|
|
160
|
+
/** Labels for section + each stack entry (length = 1 + stack.length). */
|
|
161
|
+
labels?: string[]
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const Breadcrumb = forwardRef<HTMLElement, AppNavBreadcrumbProps>(function AppNavBreadcrumb(
|
|
165
|
+
{ labels, ...rest },
|
|
166
|
+
ref,
|
|
167
|
+
) {
|
|
168
|
+
const { section, stack, setSection, popTo, clear, stateAttr } = useAppNav()
|
|
169
|
+
const crumbs = [
|
|
170
|
+
{ id: section, label: labels?.[0] ?? section, kind: 'section' as const },
|
|
171
|
+
...stack.map((entry, i) => ({
|
|
172
|
+
id: entry.id,
|
|
173
|
+
label: labels?.[i + 1] ?? entry.type,
|
|
174
|
+
kind: 'child' as const,
|
|
175
|
+
})),
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<nav
|
|
180
|
+
ref={ref as never}
|
|
181
|
+
aria-label="Breadcrumb"
|
|
182
|
+
{...mergeProps(
|
|
183
|
+
partAttrs(appNavAnatomy.scope, appNavAnatomy.breadcrumb, { state: stateAttr }),
|
|
184
|
+
rest,
|
|
185
|
+
)}
|
|
186
|
+
>
|
|
187
|
+
{crumbs.map((crumb, index) => {
|
|
188
|
+
const last = index === crumbs.length - 1
|
|
189
|
+
return (
|
|
190
|
+
<button
|
|
191
|
+
key={`${crumb.kind}:${crumb.id}`}
|
|
192
|
+
type="button"
|
|
193
|
+
disabled={last}
|
|
194
|
+
{...partAttrs(appNavAnatomy.scope, appNavAnatomy.crumb, {
|
|
195
|
+
state: last ? 'current' : 'ancestor',
|
|
196
|
+
})}
|
|
197
|
+
onClick={() => {
|
|
198
|
+
if (crumb.kind === 'section') {
|
|
199
|
+
clear()
|
|
200
|
+
setSection(crumb.id)
|
|
201
|
+
} else {
|
|
202
|
+
popTo(crumb.id)
|
|
203
|
+
}
|
|
204
|
+
}}
|
|
205
|
+
>
|
|
206
|
+
{crumb.label}
|
|
207
|
+
</button>
|
|
208
|
+
)
|
|
209
|
+
})}
|
|
210
|
+
</nav>
|
|
211
|
+
)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
const Back = forwardRef<HTMLButtonElement, ButtonHTMLAttributes<HTMLButtonElement>>(
|
|
215
|
+
function AppNavBack({ onClick, children = 'Back', ...rest }, ref) {
|
|
216
|
+
const { child, pop, stateAttr } = useAppNav()
|
|
217
|
+
if (!child) return null
|
|
218
|
+
return (
|
|
219
|
+
<button
|
|
220
|
+
ref={ref}
|
|
221
|
+
type="button"
|
|
222
|
+
onClick={(e) => {
|
|
223
|
+
onClick?.(e)
|
|
224
|
+
if (e.defaultPrevented) return
|
|
225
|
+
pop()
|
|
226
|
+
}}
|
|
227
|
+
{...mergeProps(
|
|
228
|
+
partAttrs(appNavAnatomy.scope, appNavAnatomy.back, { state: stateAttr }),
|
|
229
|
+
rest,
|
|
230
|
+
)}
|
|
231
|
+
>
|
|
232
|
+
{children}
|
|
233
|
+
</button>
|
|
234
|
+
)
|
|
235
|
+
},
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* AppNav — section rail + detail stack.
|
|
240
|
+
* Machine: SET_SECTION / PUSH / POP / CLEAR. UI = f(section, stack).
|
|
241
|
+
*/
|
|
242
|
+
export const AppNav = {
|
|
243
|
+
Root,
|
|
244
|
+
Rail,
|
|
245
|
+
Section,
|
|
246
|
+
Stage,
|
|
247
|
+
Breadcrumb,
|
|
248
|
+
Back,
|
|
249
|
+
}
|
|
@@ -47,7 +47,10 @@ const Root = forwardRef<HTMLSpanElement, AvatarRootProps>(function AvatarRoot(
|
|
|
47
47
|
ref={ref}
|
|
48
48
|
className={className}
|
|
49
49
|
{...mergeProps(
|
|
50
|
-
partAttrs(avatarAnatomy.scope, avatarAnatomy.root, {
|
|
50
|
+
partAttrs(avatarAnatomy.scope, avatarAnatomy.root, {
|
|
51
|
+
// `ready` alias keeps product CSS friendly; `loaded` stays for existing recipes.
|
|
52
|
+
state: status === 'loaded' ? 'loaded ready' : status,
|
|
53
|
+
}),
|
|
51
54
|
rest,
|
|
52
55
|
)}
|
|
53
56
|
>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useLayoutEffect, type RefObject } from 'react'
|
|
2
|
+
|
|
3
|
+
export type FitTextOptions = {
|
|
4
|
+
maxPx?: number
|
|
5
|
+
minPx?: number
|
|
6
|
+
/** Re-run when this value changes (e.g. display string). */
|
|
7
|
+
watch?: unknown
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Shrink font-size (then scale) so `contentRef` fits inside `slotRef`
|
|
12
|
+
* without expanding parent layout. Universal for balance rows, titles, tickers.
|
|
13
|
+
*/
|
|
14
|
+
export function useFitText(
|
|
15
|
+
slotRef: RefObject<HTMLElement | null>,
|
|
16
|
+
contentRef: RefObject<HTMLElement | null>,
|
|
17
|
+
opts: FitTextOptions = {},
|
|
18
|
+
) {
|
|
19
|
+
const maxPx = opts.maxPx ?? 28
|
|
20
|
+
const minPx = opts.minPx ?? 8
|
|
21
|
+
const watch = opts.watch
|
|
22
|
+
|
|
23
|
+
useLayoutEffect(() => {
|
|
24
|
+
const slot = slotRef.current
|
|
25
|
+
const content = contentRef.current
|
|
26
|
+
if (!slot || !content) return
|
|
27
|
+
|
|
28
|
+
const fit = () => {
|
|
29
|
+
const available = slot.clientWidth
|
|
30
|
+
if (available <= 0) return
|
|
31
|
+
|
|
32
|
+
content.style.transform = ''
|
|
33
|
+
content.style.transformOrigin = ''
|
|
34
|
+
content.style.fontSize = `${maxPx}px`
|
|
35
|
+
|
|
36
|
+
let size = maxPx
|
|
37
|
+
while (size > minPx && content.scrollWidth > available + 0.5) {
|
|
38
|
+
size -= 0.5
|
|
39
|
+
content.style.fontSize = `${size}px`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (content.scrollWidth > available + 0.5) {
|
|
43
|
+
const scale = Math.max(0.35, available / content.scrollWidth)
|
|
44
|
+
content.style.transformOrigin = 'center center'
|
|
45
|
+
content.style.transform = `scale(${scale})`
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
fit()
|
|
50
|
+
const ro = new ResizeObserver(fit)
|
|
51
|
+
ro.observe(slot)
|
|
52
|
+
return () => {
|
|
53
|
+
ro.disconnect()
|
|
54
|
+
content.style.fontSize = ''
|
|
55
|
+
content.style.transform = ''
|
|
56
|
+
content.style.transformOrigin = ''
|
|
57
|
+
}
|
|
58
|
+
}, [slotRef, contentRef, maxPx, minPx, watch])
|
|
59
|
+
}
|
|
@@ -2,6 +2,8 @@ export * from './button/index.js'
|
|
|
2
2
|
export * from './switch/index.js'
|
|
3
3
|
export * from './checkbox/index.js'
|
|
4
4
|
export * from './dialog/index.js'
|
|
5
|
+
export * from './prompt/index.js'
|
|
6
|
+
export * from './status-banner/index.js'
|
|
5
7
|
export * from './tabs/index.js'
|
|
6
8
|
export * from './separator/index.js'
|
|
7
9
|
export * from './progress/index.js'
|
|
@@ -31,8 +33,10 @@ export * from './profile-header/index.js'
|
|
|
31
33
|
export * from './bar/index.js'
|
|
32
34
|
export * from './panel/index.js'
|
|
33
35
|
export * from './app-shell/index.js'
|
|
36
|
+
export * from './app-nav/index.js'
|
|
34
37
|
export * from './content/index.js'
|
|
35
38
|
export * from './nav/index.js'
|
|
36
39
|
export * from './sticky-bar/index.js'
|
|
37
40
|
export { useAeonMachine } from './hooks/use-aeon-machine.js'
|
|
38
41
|
export { useLayoutMode } from './hooks/use-layout-mode.js'
|
|
42
|
+
export { useFitText } from './hooks/use-fit-text.js'
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { partAttrs, promptAnatomy, scopeAttrs } from '@aeon-ui/core'
|
|
2
|
+
import {
|
|
3
|
+
createContext,
|
|
4
|
+
forwardRef,
|
|
5
|
+
useContext,
|
|
6
|
+
useMemo,
|
|
7
|
+
type ButtonHTMLAttributes,
|
|
8
|
+
type HTMLAttributes,
|
|
9
|
+
type ReactNode,
|
|
10
|
+
} from 'react'
|
|
11
|
+
import { Dialog } from '../dialog/index.js'
|
|
12
|
+
import { mergeProps } from '../utils/merge-props.js'
|
|
13
|
+
|
|
14
|
+
export type PromptStatus = 'pending' | 'confirming' | 'dismissed' | 'confirmed'
|
|
15
|
+
|
|
16
|
+
interface PromptContextValue {
|
|
17
|
+
status: PromptStatus
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const PromptCtx = createContext<PromptContextValue | null>(null)
|
|
21
|
+
|
|
22
|
+
function usePromptCtx() {
|
|
23
|
+
const ctx = useContext(PromptCtx)
|
|
24
|
+
if (!ctx) throw new Error('Prompt parts must be used within Prompt.Root')
|
|
25
|
+
return ctx
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface PromptRootProps {
|
|
29
|
+
open?: boolean
|
|
30
|
+
defaultOpen?: boolean
|
|
31
|
+
onOpenChange?: (open: boolean) => void
|
|
32
|
+
/** Projected onto data-aeon-state (default: pending when open). */
|
|
33
|
+
status?: PromptStatus
|
|
34
|
+
children?: ReactNode
|
|
35
|
+
className?: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Prompt — confirm / permission / “restart to update” dialog.
|
|
40
|
+
* Composes Dialog (portal, focus trap, escape) with product-facing parts.
|
|
41
|
+
*/
|
|
42
|
+
const Root = forwardRef<HTMLDivElement, PromptRootProps>(function PromptRoot(
|
|
43
|
+
{ status = 'pending', children, className, ...dialogProps },
|
|
44
|
+
ref,
|
|
45
|
+
) {
|
|
46
|
+
const value = useMemo(() => ({ status }), [status])
|
|
47
|
+
return (
|
|
48
|
+
<PromptCtx.Provider value={value}>
|
|
49
|
+
<Dialog.Root {...dialogProps}>
|
|
50
|
+
<div
|
|
51
|
+
ref={ref}
|
|
52
|
+
className={className}
|
|
53
|
+
{...scopeAttrs(promptAnatomy.scope, promptAnatomy.root, { state: status })}
|
|
54
|
+
>
|
|
55
|
+
{children}
|
|
56
|
+
</div>
|
|
57
|
+
</Dialog.Root>
|
|
58
|
+
</PromptCtx.Provider>
|
|
59
|
+
)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const Eyebrow = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLParagraphElement>>(
|
|
63
|
+
function PromptEyebrow(props, ref) {
|
|
64
|
+
const { status } = usePromptCtx()
|
|
65
|
+
return (
|
|
66
|
+
<p
|
|
67
|
+
ref={ref}
|
|
68
|
+
{...mergeProps(partAttrs(promptAnatomy.scope, promptAnatomy.eyebrow, { state: status }), props)}
|
|
69
|
+
/>
|
|
70
|
+
)
|
|
71
|
+
},
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
const Meta = forwardRef<HTMLDListElement, HTMLAttributes<HTMLDListElement>>(function PromptMeta(
|
|
75
|
+
props,
|
|
76
|
+
ref,
|
|
77
|
+
) {
|
|
78
|
+
const { status } = usePromptCtx()
|
|
79
|
+
return (
|
|
80
|
+
<dl
|
|
81
|
+
ref={ref}
|
|
82
|
+
{...mergeProps(partAttrs(promptAnatomy.scope, promptAnatomy.meta, { state: status }), props)}
|
|
83
|
+
/>
|
|
84
|
+
)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const Amount = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(function PromptAmount(
|
|
88
|
+
props,
|
|
89
|
+
ref,
|
|
90
|
+
) {
|
|
91
|
+
const { status } = usePromptCtx()
|
|
92
|
+
return (
|
|
93
|
+
<div
|
|
94
|
+
ref={ref}
|
|
95
|
+
{...mergeProps(partAttrs(promptAnatomy.scope, promptAnatomy.amount, { state: status }), props)}
|
|
96
|
+
/>
|
|
97
|
+
)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
const Actions = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(function PromptActions(
|
|
101
|
+
props,
|
|
102
|
+
ref,
|
|
103
|
+
) {
|
|
104
|
+
const { status } = usePromptCtx()
|
|
105
|
+
return (
|
|
106
|
+
<div
|
|
107
|
+
ref={ref}
|
|
108
|
+
{...mergeProps(partAttrs(promptAnatomy.scope, promptAnatomy.actions, { state: status }), props)}
|
|
109
|
+
/>
|
|
110
|
+
)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
const Primary = forwardRef<HTMLButtonElement, ButtonHTMLAttributes<HTMLButtonElement>>(
|
|
114
|
+
function PromptPrimary(props, ref) {
|
|
115
|
+
const { status } = usePromptCtx()
|
|
116
|
+
return (
|
|
117
|
+
<button
|
|
118
|
+
ref={ref}
|
|
119
|
+
type="button"
|
|
120
|
+
{...mergeProps(
|
|
121
|
+
partAttrs(promptAnatomy.scope, promptAnatomy.primary, { state: status }),
|
|
122
|
+
props,
|
|
123
|
+
)}
|
|
124
|
+
/>
|
|
125
|
+
)
|
|
126
|
+
},
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
const Secondary = forwardRef<HTMLButtonElement, ButtonHTMLAttributes<HTMLButtonElement>>(
|
|
130
|
+
function PromptSecondary(props, ref) {
|
|
131
|
+
const { status } = usePromptCtx()
|
|
132
|
+
return (
|
|
133
|
+
<button
|
|
134
|
+
ref={ref}
|
|
135
|
+
type="button"
|
|
136
|
+
{...mergeProps(
|
|
137
|
+
partAttrs(promptAnatomy.scope, promptAnatomy.secondary, { state: status }),
|
|
138
|
+
props,
|
|
139
|
+
)}
|
|
140
|
+
/>
|
|
141
|
+
)
|
|
142
|
+
},
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
export const Prompt = {
|
|
146
|
+
Root,
|
|
147
|
+
Trigger: Dialog.Trigger,
|
|
148
|
+
Portal: Dialog.Portal,
|
|
149
|
+
Backdrop: Dialog.Backdrop,
|
|
150
|
+
Positioner: Dialog.Positioner,
|
|
151
|
+
Content: Dialog.Content,
|
|
152
|
+
Title: Dialog.Title,
|
|
153
|
+
Description: Dialog.Description,
|
|
154
|
+
CloseTrigger: Dialog.CloseTrigger,
|
|
155
|
+
Eyebrow,
|
|
156
|
+
Meta,
|
|
157
|
+
Amount,
|
|
158
|
+
Actions,
|
|
159
|
+
Primary,
|
|
160
|
+
Secondary,
|
|
161
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { partAttrs, scopeAttrs, statusBannerAnatomy } from '@aeon-ui/core'
|
|
2
|
+
import { forwardRef, type ButtonHTMLAttributes, type HTMLAttributes, type ReactNode } from 'react'
|
|
3
|
+
import { mergeProps } from '../utils/merge-props.js'
|
|
4
|
+
|
|
5
|
+
export type StatusBannerTone = 'info' | 'success' | 'warning' | 'danger'
|
|
6
|
+
|
|
7
|
+
export interface StatusBannerRootProps extends HTMLAttributes<HTMLDivElement> {
|
|
8
|
+
tone?: StatusBannerTone
|
|
9
|
+
/** Machine-ish status string projected to data-aeon-state (e.g. ready, downloading). */
|
|
10
|
+
status?: string
|
|
11
|
+
children?: ReactNode
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const Root = forwardRef<HTMLDivElement, StatusBannerRootProps>(function StatusBannerRoot(
|
|
15
|
+
{ tone = 'info', status, children, ...rest },
|
|
16
|
+
ref,
|
|
17
|
+
) {
|
|
18
|
+
const state = [tone, status].filter(Boolean).join(' ')
|
|
19
|
+
return (
|
|
20
|
+
<div
|
|
21
|
+
ref={ref}
|
|
22
|
+
role="status"
|
|
23
|
+
{...mergeProps(scopeAttrs(statusBannerAnatomy.scope, statusBannerAnatomy.root, { state }), rest)}
|
|
24
|
+
>
|
|
25
|
+
{children}
|
|
26
|
+
</div>
|
|
27
|
+
)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const Copy = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(function StatusBannerCopy(
|
|
31
|
+
props,
|
|
32
|
+
ref,
|
|
33
|
+
) {
|
|
34
|
+
return <div ref={ref} {...mergeProps(partAttrs(statusBannerAnatomy.scope, statusBannerAnatomy.copy), props)} />
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const Title = forwardRef<HTMLElement, HTMLAttributes<HTMLElement>>(function StatusBannerTitle(
|
|
38
|
+
props,
|
|
39
|
+
ref,
|
|
40
|
+
) {
|
|
41
|
+
return (
|
|
42
|
+
<strong
|
|
43
|
+
ref={ref as never}
|
|
44
|
+
{...mergeProps(partAttrs(statusBannerAnatomy.scope, statusBannerAnatomy.title), props)}
|
|
45
|
+
/>
|
|
46
|
+
)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const Body = forwardRef<HTMLSpanElement, HTMLAttributes<HTMLSpanElement>>(function StatusBannerBody(
|
|
50
|
+
props,
|
|
51
|
+
ref,
|
|
52
|
+
) {
|
|
53
|
+
return (
|
|
54
|
+
<span ref={ref} {...mergeProps(partAttrs(statusBannerAnatomy.scope, statusBannerAnatomy.body), props)} />
|
|
55
|
+
)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const Actions = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(function StatusBannerActions(
|
|
59
|
+
props,
|
|
60
|
+
ref,
|
|
61
|
+
) {
|
|
62
|
+
return (
|
|
63
|
+
<div
|
|
64
|
+
ref={ref}
|
|
65
|
+
{...mergeProps(partAttrs(statusBannerAnatomy.scope, statusBannerAnatomy.actions), props)}
|
|
66
|
+
/>
|
|
67
|
+
)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const Action = forwardRef<HTMLButtonElement, ButtonHTMLAttributes<HTMLButtonElement>>(
|
|
71
|
+
function StatusBannerAction(props, ref) {
|
|
72
|
+
return <button ref={ref} type="button" {...props} />
|
|
73
|
+
},
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
/** Non-blocking top-of-app notice (updates, sync, offline). */
|
|
77
|
+
export const StatusBanner = {
|
|
78
|
+
Root,
|
|
79
|
+
Copy,
|
|
80
|
+
Title,
|
|
81
|
+
Body,
|
|
82
|
+
Actions,
|
|
83
|
+
Action,
|
|
84
|
+
}
|
package/packages/ui/src/index.ts
CHANGED
|
@@ -13,6 +13,10 @@ export { Field } from './field.js'
|
|
|
13
13
|
export { Switch } from './switch.js'
|
|
14
14
|
export { Checkbox } from './checkbox.js'
|
|
15
15
|
export { Dialog } from './dialog.js'
|
|
16
|
+
export { Prompt } from './prompt.js'
|
|
17
|
+
export { StatusBanner } from './status-banner.js'
|
|
18
|
+
export { AppNav, useAppNav, useAppNavMachine } from './app-nav.js'
|
|
19
|
+
export type { AppNavChild, AppNavContext } from './app-nav.js'
|
|
16
20
|
export { Tabs } from './tabs.js'
|
|
17
21
|
export { Separator } from './separator.js'
|
|
18
22
|
export { Progress } from './progress.js'
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Prompt as Headless } from '@aeon-ui/react'
|
|
2
|
+
import type { ComponentProps } from 'react'
|
|
3
|
+
import { Dialog } from './dialog.js'
|
|
4
|
+
import { cn } from './cn.js'
|
|
5
|
+
|
|
6
|
+
/** Styled Prompt — Dialog recipes + prompt action parts. */
|
|
7
|
+
export const Prompt = {
|
|
8
|
+
Root: Headless.Root,
|
|
9
|
+
Trigger: Dialog.Trigger,
|
|
10
|
+
Portal: Dialog.Portal,
|
|
11
|
+
Backdrop: Dialog.Backdrop,
|
|
12
|
+
Positioner: Dialog.Positioner,
|
|
13
|
+
Content: Dialog.Content,
|
|
14
|
+
Title: Dialog.Title,
|
|
15
|
+
Description: Dialog.Description,
|
|
16
|
+
CloseTrigger: Dialog.CloseTrigger,
|
|
17
|
+
Eyebrow: ({ className, ...props }: ComponentProps<typeof Headless.Eyebrow>) => (
|
|
18
|
+
<Headless.Eyebrow className={cn(className)} {...props} />
|
|
19
|
+
),
|
|
20
|
+
Meta: Headless.Meta,
|
|
21
|
+
Amount: Headless.Amount,
|
|
22
|
+
Actions: Headless.Actions,
|
|
23
|
+
Primary: ({ className, ...props }: ComponentProps<typeof Headless.Primary>) => (
|
|
24
|
+
<Headless.Primary className={cn(className)} {...props} />
|
|
25
|
+
),
|
|
26
|
+
Secondary: ({ className, ...props }: ComponentProps<typeof Headless.Secondary>) => (
|
|
27
|
+
<Headless.Secondary className={cn(className)} {...props} />
|
|
28
|
+
),
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { StatusBanner } from '@aeon-ui/react'
|
package/scripts/vite-aliases.mjs
CHANGED
|
@@ -63,6 +63,14 @@ export function aeonUiViteAliases(opts = {}) {
|
|
|
63
63
|
find: '@aeon-ui/panda/chrome.css',
|
|
64
64
|
replacement: path.join(packages, 'panda/src/theme/chrome.css'),
|
|
65
65
|
},
|
|
66
|
+
{
|
|
67
|
+
find: '@aeon-ui/panda/consumer-shell.css',
|
|
68
|
+
replacement: path.join(packages, 'panda/src/theme/consumer-shell.css'),
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
find: '@aeon-ui/panda/electron.css',
|
|
72
|
+
replacement: path.join(packages, 'panda/src/theme/electron.css'),
|
|
73
|
+
},
|
|
66
74
|
{
|
|
67
75
|
find: '@aeon-ui/panda/styled-system/css',
|
|
68
76
|
replacement: path.join(packages, 'panda/styled-system/css/index.mjs'),
|