@fayz-ai/ui 0.9.0 → 0.10.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/README.md +2 -2
- package/dist/analytics/AnalyticsCard.d.ts +10 -0
- package/dist/analytics/AnalyticsCard.d.ts.map +1 -0
- package/dist/analytics/AnalyticsControls.d.ts +18 -0
- package/dist/analytics/AnalyticsControls.d.ts.map +1 -0
- package/dist/analytics/AnalyticsGrid.d.ts +16 -0
- package/dist/analytics/AnalyticsGrid.d.ts.map +1 -0
- package/dist/analytics/context.d.ts +58 -0
- package/dist/analytics/context.d.ts.map +1 -0
- package/dist/analytics/derive.d.ts +81 -0
- package/dist/analytics/derive.d.ts.map +1 -0
- package/dist/analytics/index.d.ts +13 -0
- package/dist/analytics/index.d.ts.map +1 -0
- package/dist/analytics/range.d.ts +22 -0
- package/dist/analytics/range.d.ts.map +1 -0
- package/dist/{chunk-PVIB724L.js → chunk-22EVFMS3.js} +5 -3
- package/dist/chunk-22EVFMS3.js.map +1 -0
- package/dist/{chunk-G6OKZ7WL.js → chunk-6V5MGYXH.js} +13 -9
- package/dist/chunk-6V5MGYXH.js.map +1 -0
- package/dist/{chunk-KV3MHB6N.js → chunk-H63TOSQ7.js} +196 -53
- package/dist/chunk-H63TOSQ7.js.map +1 -0
- package/dist/{chunk-OUPJDGKE.js → chunk-I6CPKAOD.js} +134 -37
- package/dist/chunk-I6CPKAOD.js.map +1 -0
- package/dist/{chunk-YRVE4FOD.js → chunk-OKZ6WQB3.js} +3 -3
- package/dist/chunk-OKZ6WQB3.js.map +1 -0
- package/dist/{chunk-S5KNM4D2.js → chunk-SEIY2F5F.js} +235 -107
- package/dist/chunk-SEIY2F5F.js.map +1 -0
- package/dist/{chunk-75EFDLST.js → chunk-VAKWUUVC.js} +157 -6
- package/dist/chunk-VAKWUUVC.js.map +1 -0
- package/dist/dashboard/OnboardingWidget.d.ts +2 -14
- package/dist/dashboard/OnboardingWidget.d.ts.map +1 -1
- package/dist/dashboard/SidebarOnboarding.d.ts +28 -0
- package/dist/dashboard/SidebarOnboarding.d.ts.map +1 -0
- package/dist/dashboard/index.d.ts +2 -0
- package/dist/dashboard/index.d.ts.map +1 -1
- package/dist/dashboard/index.js +4 -4
- package/dist/dashboard/types.d.ts +18 -0
- package/dist/dashboard/types.d.ts.map +1 -1
- package/dist/dashboard/useOnboardingProgress.d.ts +28 -0
- package/dist/dashboard/useOnboardingProgress.d.ts.map +1 -0
- package/dist/icons.js +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +960 -14
- package/dist/index.js.map +1 -1
- package/dist/layout/AppShell.d.ts +14 -1
- package/dist/layout/AppShell.d.ts.map +1 -1
- package/dist/layout/ModulePage.d.ts +11 -2
- package/dist/layout/ModulePage.d.ts.map +1 -1
- package/dist/layout/Sidebar.d.ts +8 -1
- package/dist/layout/Sidebar.d.ts.map +1 -1
- package/dist/layout/index.js +4 -4
- package/dist/lib/cn.d.ts.map +1 -0
- package/dist/preset.d.ts +3 -0
- package/dist/preset.d.ts.map +1 -0
- package/dist/primitives/data-table.d.ts +19 -4
- package/dist/primitives/data-table.d.ts.map +1 -1
- package/dist/primitives/date-picker.d.ts +4 -2
- package/dist/primitives/date-picker.d.ts.map +1 -1
- package/dist/primitives/filter-chip.d.ts +26 -0
- package/dist/primitives/filter-chip.d.ts.map +1 -0
- package/dist/primitives/header-tabs.d.ts +32 -0
- package/dist/primitives/header-tabs.d.ts.map +1 -0
- package/dist/primitives/hint.d.ts +25 -0
- package/dist/primitives/hint.d.ts.map +1 -0
- package/dist/primitives/index.d.ts +7 -0
- package/dist/primitives/index.d.ts.map +1 -1
- package/dist/primitives/index.js +4 -4
- package/dist/primitives/list-view.d.ts +14 -1
- package/dist/primitives/list-view.d.ts.map +1 -1
- package/dist/primitives/page-header.d.ts +23 -0
- package/dist/primitives/page-header.d.ts.map +1 -0
- package/dist/primitives/stat-tiles.d.ts +30 -0
- package/dist/primitives/stat-tiles.d.ts.map +1 -0
- package/dist/primitives/switch.d.ts +11 -0
- package/dist/primitives/switch.d.ts.map +1 -0
- package/dist/stores/index.d.ts +1 -0
- package/dist/stores/index.d.ts.map +1 -1
- package/dist/stores/page-header.store.d.ts +69 -0
- package/dist/stores/page-header.store.d.ts.map +1 -0
- package/package.json +6 -15
- package/dist/chunk-75EFDLST.js.map +0 -1
- package/dist/chunk-G6OKZ7WL.js.map +0 -1
- package/dist/chunk-KV3MHB6N.js.map +0 -1
- package/dist/chunk-OUPJDGKE.js.map +0 -1
- package/dist/chunk-PVIB724L.js.map +0 -1
- package/dist/chunk-S5KNM4D2.js.map +0 -1
- package/dist/chunk-YRVE4FOD.js.map +0 -1
- package/dist/utils/cn.d.ts.map +0 -1
- package/src/dashboard/ChartWidget.tsx +0 -50
- package/src/dashboard/ChartWidgetInner.tsx +0 -83
- package/src/dashboard/DashboardCanvas.tsx +0 -210
- package/src/dashboard/DashboardCustomizeMenu.tsx +0 -53
- package/src/dashboard/DashboardGrid.tsx +0 -35
- package/src/dashboard/DashboardRange.tsx +0 -62
- package/src/dashboard/GoldCard.tsx +0 -38
- package/src/dashboard/KpiCard.tsx +0 -238
- package/src/dashboard/OnboardingWidget.tsx +0 -140
- package/src/dashboard/TableWidget.tsx +0 -51
- package/src/dashboard/define.ts +0 -53
- package/src/dashboard/icon.ts +0 -13
- package/src/dashboard/index.ts +0 -38
- package/src/dashboard/layout.ts +0 -50
- package/src/dashboard/preferences.ts +0 -71
- package/src/dashboard/types.ts +0 -41
- package/src/icons.ts +0 -66
- package/src/index.ts +0 -238
- package/src/layout/AppShell.tsx +0 -987
- package/src/layout/ContentSplit.tsx +0 -153
- package/src/layout/ModulePage.tsx +0 -466
- package/src/layout/PageTransition.tsx +0 -57
- package/src/layout/SaveBar.tsx +0 -247
- package/src/layout/Sidebar.tsx +0 -535
- package/src/layout/Topbar.tsx +0 -462
- package/src/layout/index.ts +0 -39
- package/src/layout/useCrossModuleBack.ts +0 -30
- package/src/primitives/Avatar.tsx +0 -48
- package/src/primitives/SearchCombobox.tsx +0 -216
- package/src/primitives/badge.tsx +0 -68
- package/src/primitives/breadcrumb.tsx +0 -43
- package/src/primitives/button.tsx +0 -60
- package/src/primitives/card.tsx +0 -79
- package/src/primitives/checkbox.tsx +0 -53
- package/src/primitives/confirm-dialog.tsx +0 -125
- package/src/primitives/currency-input.tsx +0 -121
- package/src/primitives/data-table.tsx +0 -160
- package/src/primitives/date-picker.tsx +0 -225
- package/src/primitives/dropdown.tsx +0 -196
- package/src/primitives/index.ts +0 -87
- package/src/primitives/input.tsx +0 -50
- package/src/primitives/list-view.tsx +0 -252
- package/src/primitives/markdown-editor.tsx +0 -75
- package/src/primitives/markdown.tsx +0 -169
- package/src/primitives/modal.tsx +0 -156
- package/src/primitives/phone-link.tsx +0 -138
- package/src/primitives/popover.tsx +0 -30
- package/src/primitives/search-select.tsx +0 -314
- package/src/primitives/segmented-control.tsx +0 -66
- package/src/primitives/select.tsx +0 -157
- package/src/primitives/separator.tsx +0 -29
- package/src/primitives/sheet.tsx +0 -181
- package/src/primitives/skeleton.tsx +0 -46
- package/src/primitives/tabs.tsx +0 -53
- package/src/primitives/time-picker.tsx +0 -148
- package/src/primitives/toast.tsx +0 -46
- package/src/primitives/tooltip.tsx +0 -28
- package/src/stores/index.ts +0 -10
- package/src/stores/layout.store.ts +0 -31
- package/src/stores/theme.store.ts +0 -457
- package/src/tailwind.ts +0 -43
- package/src/theme/index.ts +0 -94
- package/src/theme/preset.ts +0 -209
- package/src/theme/presets.ts +0 -106
- package/src/utils/cn.ts +0 -6
- /package/dist/{utils → lib}/cn.d.ts +0 -0
- /package/{src → dist}/styles.css +0 -0
package/src/layout/SaveBar.tsx
DELETED
|
@@ -1,247 +0,0 @@
|
|
|
1
|
-
import * as React from 'react'
|
|
2
|
-
import { Loader2 } from 'lucide-react'
|
|
3
|
-
import { useTranslation } from '@fayz-ai/core'
|
|
4
|
-
import { cn } from '../utils/cn'
|
|
5
|
-
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
// SaveBar — the single, app-wide "Unsaved changes" floating island. A form
|
|
8
|
-
// declares its state via useSaveBar({ dirty, saving, onSave, onDiscard }); the
|
|
9
|
-
// shell renders ONE <SaveBar/> (centered, floating above the content area) that
|
|
10
|
-
// appears whenever the active form is dirty. Mirrors the BackStyleProvider /
|
|
11
|
-
// PageHeaderActions idiom: decide-once at the shell, hook-in at the page.
|
|
12
|
-
//
|
|
13
|
-
// Handlers live in a ref (not state) so per-render closure changes never cause
|
|
14
|
-
// re-render loops; only the primitive `dirty`/`saving` flags drive visibility.
|
|
15
|
-
// ---------------------------------------------------------------------------
|
|
16
|
-
|
|
17
|
-
export interface SaveBarRegistration {
|
|
18
|
-
/** Show the bar when true (form has unsaved changes). */
|
|
19
|
-
dirty: boolean
|
|
20
|
-
/** Save in progress — disables buttons and shows a spinner. */
|
|
21
|
-
saving?: boolean
|
|
22
|
-
onSave: () => void
|
|
23
|
-
onDiscard?: () => void
|
|
24
|
-
/** Override the "Unsaved changes" message. */
|
|
25
|
-
message?: string
|
|
26
|
-
/** Override the Save / Discard button labels. */
|
|
27
|
-
saveLabel?: string
|
|
28
|
-
discardLabel?: string
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
interface SaveBarState {
|
|
32
|
-
visible: boolean
|
|
33
|
-
saving: boolean
|
|
34
|
-
message?: string
|
|
35
|
-
saveLabel?: string
|
|
36
|
-
discardLabel?: string
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
interface SaveBarApi {
|
|
40
|
-
setState: React.Dispatch<React.SetStateAction<SaveBarState>>
|
|
41
|
-
handlers: React.MutableRefObject<{ onSave?: () => void; onDiscard?: () => void }>
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Two contexts on purpose: the API context is STABLE (so useSaveBar's effect
|
|
45
|
-
// deps don't change every render → no loop), the state context changes to
|
|
46
|
-
// re-render only <SaveBar/>.
|
|
47
|
-
const SaveBarApiContext = React.createContext<SaveBarApi | null>(null)
|
|
48
|
-
const SaveBarStateContext = React.createContext<SaveBarState>({ visible: false, saving: false })
|
|
49
|
-
|
|
50
|
-
// Module-level "is the SaveBar showing" signal so sibling UI that isn't a
|
|
51
|
-
// descendant of SaveBarProvider (e.g. the ToastProvider) can react to it —
|
|
52
|
-
// used to lift toasts above the bar so they stack instead of overlapping.
|
|
53
|
-
let _saveBarVisible = false
|
|
54
|
-
const _saveBarSubs = new Set<() => void>()
|
|
55
|
-
function setSaveBarVisibleSignal(v: boolean) {
|
|
56
|
-
if (v === _saveBarVisible) return
|
|
57
|
-
_saveBarVisible = v
|
|
58
|
-
_saveBarSubs.forEach((fn) => fn())
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Subscribe to whether the floating SaveBar is currently on screen. */
|
|
62
|
-
export function useSaveBarVisible(): boolean {
|
|
63
|
-
return React.useSyncExternalStore(
|
|
64
|
-
(cb) => { _saveBarSubs.add(cb); return () => { _saveBarSubs.delete(cb) } },
|
|
65
|
-
() => _saveBarVisible,
|
|
66
|
-
() => false,
|
|
67
|
-
)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function SaveBarProvider({ children }: { children: React.ReactNode }) {
|
|
71
|
-
const [state, setState] = React.useState<SaveBarState>({ visible: false, saving: false })
|
|
72
|
-
const handlers = React.useRef<{ onSave?: () => void; onDiscard?: () => void }>({})
|
|
73
|
-
const api = React.useMemo<SaveBarApi>(() => ({ setState, handlers }), [])
|
|
74
|
-
|
|
75
|
-
// Stack of registered back-navigation handlers (latest-mounted wins) so the
|
|
76
|
-
// app-wide Escape key can navigate "up" to the parent route. Lives here, next
|
|
77
|
-
// to the SaveBar handlers, so ONE keydown listener arbitrates both — Escape
|
|
78
|
-
// discards a dirty form first, and only navigates back when none is open.
|
|
79
|
-
const backStack = React.useRef<React.MutableRefObject<(() => void) | undefined>[]>([])
|
|
80
|
-
const backApi = React.useMemo<BackNavApi>(() => ({ stack: backStack }), [])
|
|
81
|
-
|
|
82
|
-
// Broadcast visibility to the module-level signal (for the ToastProvider).
|
|
83
|
-
React.useEffect(() => { setSaveBarVisibleSignal(state.visible) }, [state.visible])
|
|
84
|
-
|
|
85
|
-
// App-wide keyboard shortcuts, arbitrated in one place:
|
|
86
|
-
// • Cmd/Ctrl+Enter → save the active (dirty) form
|
|
87
|
-
// • Escape (form dirty) → discard the form
|
|
88
|
-
// • Escape (no dirty form) → navigate back to the parent route
|
|
89
|
-
React.useEffect(() => {
|
|
90
|
-
function onKey(e: KeyboardEvent) {
|
|
91
|
-
if (state.visible && !state.saving) {
|
|
92
|
-
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
|
93
|
-
e.preventDefault()
|
|
94
|
-
handlers.current.onSave?.()
|
|
95
|
-
} else if (e.key === 'Escape') {
|
|
96
|
-
e.preventDefault()
|
|
97
|
-
handlers.current.onDiscard?.()
|
|
98
|
-
}
|
|
99
|
-
return
|
|
100
|
-
}
|
|
101
|
-
// No open/dirty form — Escape walks "up" via the topmost back handler.
|
|
102
|
-
if (e.key === 'Escape' && !e.defaultPrevented && !isEditableTarget(e.target)) {
|
|
103
|
-
for (let i = backStack.current.length - 1; i >= 0; i--) {
|
|
104
|
-
const back = backStack.current[i].current
|
|
105
|
-
if (back) { e.preventDefault(); back(); return }
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
window.addEventListener('keydown', onKey)
|
|
110
|
-
return () => window.removeEventListener('keydown', onKey)
|
|
111
|
-
}, [state.visible, state.saving])
|
|
112
|
-
|
|
113
|
-
return (
|
|
114
|
-
<SaveBarApiContext.Provider value={api}>
|
|
115
|
-
<BackNavApiContext.Provider value={backApi}>
|
|
116
|
-
<SaveBarStateContext.Provider value={state}>
|
|
117
|
-
{children}
|
|
118
|
-
</SaveBarStateContext.Provider>
|
|
119
|
-
</BackNavApiContext.Provider>
|
|
120
|
-
</SaveBarApiContext.Provider>
|
|
121
|
-
)
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// ---------------------------------------------------------------------------
|
|
125
|
-
// Back-navigation registry — lets any subpage register its "go to parent" action
|
|
126
|
-
// so the shell's single Escape handler can drive it (see SaveBarProvider above).
|
|
127
|
-
// SubpageHeader already receives onBack, so wiring it there gives every plugin
|
|
128
|
-
// detail page Escape-to-parent for free, with zero per-plugin code.
|
|
129
|
-
// ---------------------------------------------------------------------------
|
|
130
|
-
|
|
131
|
-
interface BackNavApi {
|
|
132
|
-
stack: React.MutableRefObject<React.MutableRefObject<(() => void) | undefined>[]>
|
|
133
|
-
}
|
|
134
|
-
const BackNavApiContext = React.createContext<BackNavApi | null>(null)
|
|
135
|
-
|
|
136
|
-
/** True when the keyboard event originates from a field where Escape has its
|
|
137
|
-
* own meaning (clear/close), so we shouldn't hijack it for back-navigation. */
|
|
138
|
-
function isEditableTarget(target: EventTarget | null): boolean {
|
|
139
|
-
const el = target as HTMLElement | null
|
|
140
|
-
if (!el || !el.tagName) return false
|
|
141
|
-
const tag = el.tagName.toLowerCase()
|
|
142
|
-
return tag === 'input' || tag === 'textarea' || tag === 'select' || el.isContentEditable
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Register a back-navigation handler with the app-wide Escape key. Call it in a
|
|
147
|
-
* subpage (or pass through SubpageHeader's onBack); while mounted, pressing
|
|
148
|
-
* Escape with no dirty form open invokes the most-recently-registered handler.
|
|
149
|
-
* Pass `undefined` to opt out (e.g. a top-level page with nothing to go back to).
|
|
150
|
-
*/
|
|
151
|
-
export function useBackHandler(onBack?: () => void): void {
|
|
152
|
-
const api = React.useContext(BackNavApiContext)
|
|
153
|
-
// Hold the latest closure in a ref so re-renders don't churn the stack.
|
|
154
|
-
const ref = React.useRef(onBack)
|
|
155
|
-
React.useEffect(() => { ref.current = onBack })
|
|
156
|
-
React.useEffect(() => {
|
|
157
|
-
if (!api) return
|
|
158
|
-
const stack = api.stack.current
|
|
159
|
-
stack.push(ref)
|
|
160
|
-
return () => {
|
|
161
|
-
const i = stack.indexOf(ref)
|
|
162
|
-
if (i !== -1) stack.splice(i, 1)
|
|
163
|
-
}
|
|
164
|
-
}, [api])
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Register the current form with the app-wide SaveBar. Call it unconditionally
|
|
169
|
-
* in a form view; it shows the island while `dirty`, keeps handlers current, and
|
|
170
|
-
* hides the island automatically when the form unmounts (e.g. on navigation).
|
|
171
|
-
*/
|
|
172
|
-
export function useSaveBar({ dirty, saving = false, onSave, onDiscard, message, saveLabel, discardLabel }: SaveBarRegistration): void {
|
|
173
|
-
const api = React.useContext(SaveBarApiContext)
|
|
174
|
-
|
|
175
|
-
// Keep latest handlers in the shared ref (no state churn from new closures).
|
|
176
|
-
React.useEffect(() => {
|
|
177
|
-
if (api) api.handlers.current = { onSave, onDiscard }
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
// Drive visibility from primitives only; `api` is stable → no render loops.
|
|
181
|
-
React.useEffect(() => {
|
|
182
|
-
if (!api) return
|
|
183
|
-
api.setState({ visible: dirty, saving, message, saveLabel, discardLabel })
|
|
184
|
-
return () => api.setState((s) => ({ ...s, visible: false }))
|
|
185
|
-
}, [api, dirty, saving, message, saveLabel, discardLabel])
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/** The floating island. Rendered once by the shell inside the content column. */
|
|
189
|
-
export function SaveBar({ className }: { className?: string }) {
|
|
190
|
-
const state = React.useContext(SaveBarStateContext)
|
|
191
|
-
const api = React.useContext(SaveBarApiContext)
|
|
192
|
-
const t = useTranslation()
|
|
193
|
-
|
|
194
|
-
// Keep the island mounted through its exit animation: `rendered` lags behind
|
|
195
|
-
// `visible` so we can play fade-out before unmounting.
|
|
196
|
-
const [rendered, setRendered] = React.useState(state.visible)
|
|
197
|
-
const [closing, setClosing] = React.useState(false)
|
|
198
|
-
|
|
199
|
-
React.useEffect(() => {
|
|
200
|
-
if (state.visible) {
|
|
201
|
-
setRendered(true)
|
|
202
|
-
setClosing(false)
|
|
203
|
-
return
|
|
204
|
-
}
|
|
205
|
-
if (!rendered) return
|
|
206
|
-
setClosing(true)
|
|
207
|
-
const id = setTimeout(() => { setRendered(false); setClosing(false) }, 200)
|
|
208
|
-
return () => clearTimeout(id)
|
|
209
|
-
}, [state.visible, rendered])
|
|
210
|
-
|
|
211
|
-
if (!api || !rendered) return null
|
|
212
|
-
const { saving, message, saveLabel, discardLabel } = state
|
|
213
|
-
|
|
214
|
-
return (
|
|
215
|
-
<div className={cn('pointer-events-none fixed inset-x-0 bottom-5 z-40 flex justify-center px-4', className)}>
|
|
216
|
-
{/* Dark island for strong contrast against the light page (auto-inverts in dark mode). */}
|
|
217
|
-
<div className={cn(
|
|
218
|
-
'pointer-events-auto flex items-center gap-5 rounded-full bg-foreground py-2 pl-5 pr-2 text-background shadow-xl ring-1 ring-black/5 duration-200',
|
|
219
|
-
closing ? 'animate-out fade-out slide-out-to-bottom-2' : 'animate-in fade-in slide-in-from-bottom-2',
|
|
220
|
-
)}>
|
|
221
|
-
<span className="flex items-center gap-2 text-sm font-medium">
|
|
222
|
-
<span className="h-2 w-2 shrink-0 rounded-full bg-amber-400" />
|
|
223
|
-
{message ?? t('saveBar.unsaved')}
|
|
224
|
-
</span>
|
|
225
|
-
<div className="flex items-center gap-1">
|
|
226
|
-
<button
|
|
227
|
-
type="button"
|
|
228
|
-
disabled={saving}
|
|
229
|
-
onClick={() => api.handlers.current.onDiscard?.()}
|
|
230
|
-
className="rounded-full px-3 py-1.5 text-sm font-medium text-background/70 transition-colors hover:bg-background/10 hover:text-background disabled:opacity-50"
|
|
231
|
-
>
|
|
232
|
-
{discardLabel ?? t('common.discard')}
|
|
233
|
-
</button>
|
|
234
|
-
<button
|
|
235
|
-
type="button"
|
|
236
|
-
disabled={saving}
|
|
237
|
-
onClick={() => api.handlers.current.onSave?.()}
|
|
238
|
-
className="inline-flex items-center rounded-full bg-background px-4 py-1.5 text-sm font-semibold text-foreground transition-colors hover:bg-background/90 disabled:opacity-60"
|
|
239
|
-
>
|
|
240
|
-
{saving && <Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />}
|
|
241
|
-
{saveLabel ?? t('common.save')}
|
|
242
|
-
</button>
|
|
243
|
-
</div>
|
|
244
|
-
</div>
|
|
245
|
-
</div>
|
|
246
|
-
)
|
|
247
|
-
}
|