@apotech-os/ui-sdk 0.8.0 → 0.10.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/dist/index.js CHANGED
@@ -2,6 +2,11 @@ import { clsx } from 'clsx';
2
2
  import { twMerge } from 'tailwind-merge';
3
3
  import * as React from 'react';
4
4
  import { createContext, useContext, useState, useRef, useCallback, useEffect } from 'react';
5
+ import { Dialog } from '@base-ui/react/dialog';
6
+ import { Menu } from '@base-ui/react/menu';
7
+ import { Popover as Popover$1 } from '@base-ui/react/popover';
8
+ import { Switch as Switch$1 } from '@base-ui/react/switch';
9
+ import { Tabs as Tabs$1 } from '@base-ui/react/tabs';
5
10
  import { cva } from 'class-variance-authority';
6
11
  import { jsx, jsxs } from 'react/jsx-runtime';
7
12
 
@@ -88,6 +93,175 @@ function useAppSettings() {
88
93
  );
89
94
  return { settings, isLoading, error, save, isSaving, saveError };
90
95
  }
96
+ var DEFAULT_RUN_POLL_INTERVAL_MS = 1e3;
97
+ var MAX_RUN_POLL_INTERVAL_MS = 5e3;
98
+ var RUN_POLL_BACKOFF = 1.5;
99
+ var RUNS_FOLLOW_POLL_INTERVAL_MS = 3e3;
100
+ function runProgressSignature(run) {
101
+ return `${run.progress ?? ""}|${run.message ?? ""}`;
102
+ }
103
+ function isTerminalRunError(err) {
104
+ const status = extractHttpStatus(err);
105
+ return status === 403 || status === 404;
106
+ }
107
+ function extractHttpStatus(err) {
108
+ if (typeof err !== "object" || err === null) return void 0;
109
+ const direct = err.status;
110
+ if (typeof direct === "number") return direct;
111
+ const response = err.response;
112
+ if (typeof response !== "object" || response === null) return void 0;
113
+ const nested = response.status;
114
+ return typeof nested === "number" ? nested : void 0;
115
+ }
116
+ function useAppRun(runId, opts = {}) {
117
+ const { appSlug } = useAppContext();
118
+ const api = useAppApi();
119
+ const intervalMs = opts.intervalMs ?? DEFAULT_RUN_POLL_INTERVAL_MS;
120
+ const maxIntervalMs = opts.maxIntervalMs ?? MAX_RUN_POLL_INTERVAL_MS;
121
+ const [run, setRun] = useState(void 0);
122
+ const [isPolling, setIsPolling] = useState(false);
123
+ const [error, setError] = useState(void 0);
124
+ const clientRef = useRef(api);
125
+ clientRef.current = api;
126
+ const requestIdRef = useRef(0);
127
+ const isMountedRef = useRef(true);
128
+ const path = runId !== null ? `/data/apps/${appSlug}/runs/${encodeURIComponent(runId)}` : null;
129
+ const fetchOnce = useCallback(async () => {
130
+ if (path === null) return { kind: "no-path" };
131
+ const requestId = ++requestIdRef.current;
132
+ try {
133
+ const loaded = await clientRef.current.get(path);
134
+ if (requestId !== requestIdRef.current || !isMountedRef.current) return { kind: "superseded" };
135
+ setRun(loaded);
136
+ setError(void 0);
137
+ return { kind: "success", run: loaded };
138
+ } catch (err) {
139
+ if (requestId !== requestIdRef.current || !isMountedRef.current) return { kind: "superseded" };
140
+ setError(err);
141
+ return { kind: "error", err };
142
+ }
143
+ }, [path]);
144
+ useEffect(() => {
145
+ isMountedRef.current = true;
146
+ requestIdRef.current += 1;
147
+ setRun(void 0);
148
+ setError(void 0);
149
+ if (path === null) {
150
+ setIsPolling(false);
151
+ return () => {
152
+ isMountedRef.current = false;
153
+ };
154
+ }
155
+ let timeoutHandle;
156
+ let interval = intervalMs;
157
+ let lastSignature;
158
+ let cancelled = false;
159
+ const tick = async () => {
160
+ const outcome = await fetchOnce();
161
+ if (cancelled || !isMountedRef.current) return;
162
+ if (outcome.kind === "no-path") {
163
+ setIsPolling(false);
164
+ return;
165
+ }
166
+ if (outcome.kind === "superseded") {
167
+ timeoutHandle = setTimeout(() => void tick(), interval);
168
+ return;
169
+ }
170
+ if (outcome.kind === "error") {
171
+ if (isTerminalRunError(outcome.err)) {
172
+ setIsPolling(false);
173
+ return;
174
+ }
175
+ timeoutHandle = setTimeout(() => void tick(), interval);
176
+ return;
177
+ }
178
+ const loaded = outcome.run;
179
+ const signature = runProgressSignature(loaded);
180
+ interval = signature === lastSignature ? Math.min(interval * RUN_POLL_BACKOFF, maxIntervalMs) : intervalMs;
181
+ lastSignature = signature;
182
+ if (loaded.status !== "running") {
183
+ setIsPolling(false);
184
+ return;
185
+ }
186
+ timeoutHandle = setTimeout(() => void tick(), interval);
187
+ };
188
+ setIsPolling(true);
189
+ void tick();
190
+ return () => {
191
+ cancelled = true;
192
+ isMountedRef.current = false;
193
+ if (timeoutHandle) clearTimeout(timeoutHandle);
194
+ };
195
+ }, [path, fetchOnce, intervalMs, maxIntervalMs]);
196
+ const refresh = useCallback(async () => {
197
+ await fetchOnce();
198
+ }, [fetchOnce]);
199
+ return { run, isPolling, error, refresh };
200
+ }
201
+ function useAppRuns(opts) {
202
+ const { appSlug } = useAppContext();
203
+ const api = useAppApi();
204
+ const follow = opts.follow ?? true;
205
+ const [runs, setRuns] = useState([]);
206
+ const [isLoading, setIsLoading] = useState(true);
207
+ const [error, setError] = useState(void 0);
208
+ const clientRef = useRef(api);
209
+ clientRef.current = api;
210
+ const requestIdRef = useRef(0);
211
+ const isMountedRef = useRef(true);
212
+ const params = {};
213
+ if (opts.kind) params["kind"] = opts.kind;
214
+ if (opts.key) params["key"] = opts.key;
215
+ if (opts.status?.length) params["status"] = opts.status.join(",");
216
+ if (opts.limit) params["limit"] = String(opts.limit);
217
+ const paramsKey = JSON.stringify(params);
218
+ const path = `/data/apps/${appSlug}/runs`;
219
+ const fetchOnce = useCallback(
220
+ async ({ silent } = {}) => {
221
+ const requestId = ++requestIdRef.current;
222
+ if (!silent) setIsLoading(true);
223
+ try {
224
+ const page = await clientRef.current.get(
225
+ path,
226
+ JSON.parse(paramsKey)
227
+ );
228
+ if (requestId !== requestIdRef.current || !isMountedRef.current) return { kind: "superseded" };
229
+ setRuns(page.items);
230
+ setError(void 0);
231
+ return { kind: "success", items: page.items };
232
+ } catch (err) {
233
+ if (requestId !== requestIdRef.current || !isMountedRef.current) return { kind: "superseded" };
234
+ setError(err);
235
+ return { kind: "error", err };
236
+ } finally {
237
+ if (requestId === requestIdRef.current) setIsLoading(false);
238
+ }
239
+ },
240
+ [path, paramsKey]
241
+ );
242
+ useEffect(() => {
243
+ isMountedRef.current = true;
244
+ let timeoutHandle;
245
+ let cancelled = false;
246
+ const tick = async (silent) => {
247
+ const outcome = await fetchOnce({ silent });
248
+ if (cancelled || !isMountedRef.current || !follow) return;
249
+ if (outcome.kind !== "success" || outcome.items.some((r) => r.status === "running")) {
250
+ timeoutHandle = setTimeout(() => void tick(true), RUNS_FOLLOW_POLL_INTERVAL_MS);
251
+ }
252
+ };
253
+ void tick(false);
254
+ return () => {
255
+ cancelled = true;
256
+ isMountedRef.current = false;
257
+ if (timeoutHandle) clearTimeout(timeoutHandle);
258
+ };
259
+ }, [fetchOnce, follow]);
260
+ const refresh = useCallback(async () => {
261
+ await fetchOnce({ silent: true });
262
+ }, [fetchOnce]);
263
+ return { runs, isLoading, error, refresh };
264
+ }
91
265
 
92
266
  // src/utils.ts
93
267
  async function copyToClipboard(text) {
@@ -120,6 +294,12 @@ function downloadCsv(rows, filename) {
120
294
  const blob = new Blob([`${BOM}${toCsv(rows)}`], { type: "text/csv;charset=utf-8" });
121
295
  downloadBlob(blob, filename);
122
296
  }
297
+ function triggerProps(trigger) {
298
+ if (React.isValidElement(trigger)) {
299
+ return { render: trigger, nativeButton: trigger.type === "button" || trigger.type === Button };
300
+ }
301
+ return { render: /* @__PURE__ */ jsx("span", { children: trigger }), nativeButton: false };
302
+ }
123
303
  var buttonVariants = cva(
124
304
  "inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
125
305
  {
@@ -183,9 +363,18 @@ function CardHeader({ className, ...props }) {
183
363
  function CardTitle({ className, ...props }) {
184
364
  return /* @__PURE__ */ jsx("h3", { className: cn("text-lg font-semibold leading-none tracking-tight", className), ...props });
185
365
  }
366
+ function CardDescription({
367
+ className,
368
+ ...props
369
+ }) {
370
+ return /* @__PURE__ */ jsx("p", { className: cn("text-sm text-muted-foreground", className), ...props });
371
+ }
186
372
  function CardContent({ className, ...props }) {
187
373
  return /* @__PURE__ */ jsx("div", { className: cn("p-6 pt-0", className), ...props });
188
374
  }
375
+ function CardFooter({ className, ...props }) {
376
+ return /* @__PURE__ */ jsx("div", { className: cn("flex items-center p-6 pt-0", className), ...props });
377
+ }
189
378
  var Input = React.forwardRef(({ className, type, ...props }, ref) => /* @__PURE__ */ jsx(
190
379
  "input",
191
380
  {
@@ -206,7 +395,13 @@ function TableHeader({
206
395
  className,
207
396
  ...props
208
397
  }) {
209
- return /* @__PURE__ */ jsx("thead", { className: cn("[&_tr]:border-b", className), ...props });
398
+ return /* @__PURE__ */ jsx(
399
+ "thead",
400
+ {
401
+ className: cn("[&_tr]:border-b [&_tr]:hover:bg-transparent", className),
402
+ ...props
403
+ }
404
+ );
210
405
  }
211
406
  function TableBody({ className, ...props }) {
212
407
  return /* @__PURE__ */ jsx("tbody", { className: cn("[&_tr:last-child]:border-0", className), ...props });
@@ -228,7 +423,7 @@ function TableHead({ className, ...props }) {
228
423
  "th",
229
424
  {
230
425
  className: cn(
231
- "h-10 px-4 text-left align-middle text-xs font-medium text-muted-foreground",
426
+ "h-10 px-4 text-left align-middle text-xs font-medium uppercase tracking-wide text-muted-foreground",
232
427
  className
233
428
  ),
234
429
  ...props
@@ -253,42 +448,43 @@ var Select = React.forwardRef(
253
448
  )
254
449
  );
255
450
  Select.displayName = "Select";
256
- var TabsContext = React.createContext(null);
257
- function useTabsContext() {
258
- const ctx = React.useContext(TabsContext);
259
- if (!ctx) throw new Error("Tabs sub-component must be used within <Tabs>");
260
- return ctx;
261
- }
262
451
  function Tabs({ defaultValue, value, onValueChange, className, children }) {
263
- const [internal, setInternal] = React.useState(defaultValue ?? "");
264
- const current = value ?? internal;
265
- const setValue = (v) => {
266
- setInternal(v);
267
- onValueChange?.(v);
268
- };
269
- return /* @__PURE__ */ jsx(TabsContext.Provider, { value: { value: current, setValue }, children: /* @__PURE__ */ jsx("div", { className, children }) });
452
+ return /* @__PURE__ */ jsx(
453
+ Tabs$1.Root,
454
+ {
455
+ className,
456
+ defaultValue,
457
+ value,
458
+ onValueChange: (next) => onValueChange?.(String(next)),
459
+ children
460
+ }
461
+ );
270
462
  }
271
463
  function TabsList({
272
464
  className,
273
465
  children
274
466
  }) {
275
- return /* @__PURE__ */ jsx("div", { className: cn("inline-flex items-center gap-1 rounded-lg bg-muted p-1", className), children });
467
+ return /* @__PURE__ */ jsx(
468
+ Tabs$1.List,
469
+ {
470
+ className: cn("inline-flex items-center gap-1 rounded-lg bg-muted p-1", className),
471
+ children
472
+ }
473
+ );
276
474
  }
277
475
  function TabsTrigger({
278
476
  value,
279
477
  className,
280
478
  children
281
479
  }) {
282
- const { value: current, setValue } = useTabsContext();
283
- const active = current === value;
284
480
  return /* @__PURE__ */ jsx(
285
- "button",
481
+ Tabs$1.Tab,
286
482
  {
287
- type: "button",
288
- onClick: () => setValue(value),
483
+ value,
289
484
  className: cn(
290
485
  "rounded-md px-3 py-1.5 text-sm font-medium transition-colors",
291
- active ? "bg-card text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground",
486
+ "text-muted-foreground hover:text-foreground",
487
+ "data-[active]:bg-card data-[active]:text-foreground data-[active]:shadow-sm",
292
488
  className
293
489
  ),
294
490
  children
@@ -300,38 +496,25 @@ function TabsContent({
300
496
  className,
301
497
  children
302
498
  }) {
303
- const { value: current } = useTabsContext();
304
- if (current !== value) return null;
305
- return /* @__PURE__ */ jsx("div", { className, children });
499
+ return /* @__PURE__ */ jsx(Tabs$1.Panel, { value, className, children });
306
500
  }
307
501
  function Modal({ open, onClose, title, children, className }) {
308
- if (!open) return null;
309
- return /* @__PURE__ */ jsxs("div", { className: "fixed inset-0 z-50 flex items-center justify-center p-4", children: [
310
- /* @__PURE__ */ jsx(
311
- "button",
312
- {
313
- type: "button",
314
- "aria-label": "Close",
315
- className: "absolute inset-0 cursor-default bg-foreground/40",
316
- onClick: onClose
317
- }
318
- ),
502
+ return /* @__PURE__ */ jsx(Dialog.Root, { open, onOpenChange: (next) => !next && onClose(), children: /* @__PURE__ */ jsxs(Dialog.Portal, { children: [
503
+ /* @__PURE__ */ jsx(Dialog.Backdrop, { "aria-label": "Close", className: "fixed inset-0 z-50 bg-foreground/40" }),
319
504
  /* @__PURE__ */ jsxs(
320
- "div",
505
+ Dialog.Popup,
321
506
  {
322
- role: "dialog",
323
- "aria-modal": true,
324
507
  className: cn(
325
- "relative z-10 w-full max-w-md rounded-xl border bg-card p-5 shadow-lg",
508
+ "fixed left-1/2 top-1/2 z-50 w-[calc(100%-2rem)] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-xl border bg-card p-5 shadow-lg",
326
509
  className
327
510
  ),
328
511
  children: [
329
- title && /* @__PURE__ */ jsx("h2", { className: "mb-3 text-base font-semibold", children: title }),
512
+ title && /* @__PURE__ */ jsx(Dialog.Title, { className: "mb-3 text-base font-semibold", children: title }),
330
513
  children
331
514
  ]
332
515
  }
333
516
  )
334
- ] });
517
+ ] }) });
335
518
  }
336
519
  var spinnerSizes = { sm: "size-4", md: "size-6", lg: "size-8" };
337
520
  function Spinner({ className, size = "md" }) {
@@ -381,28 +564,19 @@ function Switch({
381
564
  ...props
382
565
  }) {
383
566
  return /* @__PURE__ */ jsx(
384
- "button",
567
+ Switch$1.Root,
385
568
  {
386
- type: "button",
387
- role: "switch",
388
- "aria-checked": checked,
569
+ checked,
570
+ onCheckedChange: (next) => onCheckedChange?.(next),
389
571
  disabled,
390
- onClick: () => onCheckedChange?.(!checked),
391
572
  className: cn(
392
- "relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
393
- checked ? "bg-primary" : "bg-input",
573
+ // `data-[disabled]`, not `disabled:` the root is a span, so `:disabled`
574
+ // never matches it and the dimming would silently stop happening.
575
+ "relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full bg-input transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring data-[checked]:bg-primary data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50",
394
576
  className
395
577
  ),
396
578
  ...props,
397
- children: /* @__PURE__ */ jsx(
398
- "span",
399
- {
400
- className: cn(
401
- "inline-block size-4 transform rounded-full bg-white shadow transition-transform",
402
- checked ? "translate-x-4" : "translate-x-0.5"
403
- )
404
- }
405
- )
579
+ children: /* @__PURE__ */ jsx(Switch$1.Thumb, { className: "inline-block size-4 translate-x-0.5 rounded-full bg-white shadow transition-transform data-[checked]:translate-x-4" })
406
580
  }
407
581
  );
408
582
  }
@@ -531,47 +705,26 @@ function SidePanel({
531
705
  children,
532
706
  className
533
707
  }) {
534
- React.useEffect(() => {
535
- if (!open) return;
536
- const onKeyDown = (e) => {
537
- if (e.key === "Escape") onClose();
538
- };
539
- window.addEventListener("keydown", onKeyDown);
540
- return () => window.removeEventListener("keydown", onKeyDown);
541
- }, [open, onClose]);
542
- if (!open) return null;
543
- return /* @__PURE__ */ jsxs("div", { className: "fixed inset-0 z-50", children: [
544
- /* @__PURE__ */ jsx(
545
- "button",
546
- {
547
- type: "button",
548
- "aria-label": "Fermer",
549
- className: "absolute inset-0 cursor-default bg-foreground/40",
550
- onClick: onClose
551
- }
552
- ),
708
+ return /* @__PURE__ */ jsx(Dialog.Root, { open, onOpenChange: (next) => !next && onClose(), children: /* @__PURE__ */ jsxs(Dialog.Portal, { children: [
709
+ /* @__PURE__ */ jsx(Dialog.Backdrop, { "aria-label": "Fermer", className: "fixed inset-0 z-50 bg-foreground/40" }),
553
710
  /* @__PURE__ */ jsxs(
554
- "aside",
711
+ Dialog.Popup,
555
712
  {
556
- role: "dialog",
557
- "aria-modal": true,
558
713
  className: cn(
559
- "absolute inset-y-0 right-0 flex w-full max-w-lg flex-col border-l bg-card shadow-xl",
714
+ "fixed inset-y-0 right-0 z-50 flex w-full max-w-lg flex-col border-l bg-card shadow-xl",
560
715
  className
561
716
  ),
562
717
  children: [
563
718
  /* @__PURE__ */ jsxs("header", { className: "flex shrink-0 items-start gap-3 border-b px-5 py-4", children: [
564
719
  /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
565
- title && /* @__PURE__ */ jsx("h2", { className: "text-base font-semibold", children: title }),
566
- description && /* @__PURE__ */ jsx("p", { className: "mt-0.5 text-xs text-muted-foreground", children: description })
720
+ title && /* @__PURE__ */ jsx(Dialog.Title, { className: "text-base font-semibold", children: title }),
721
+ description && /* @__PURE__ */ jsx(Dialog.Description, { className: "mt-0.5 text-xs text-muted-foreground", children: description })
567
722
  ] }),
568
723
  /* @__PURE__ */ jsx(
569
- "button",
724
+ Dialog.Close,
570
725
  {
571
- type: "button",
572
726
  "aria-label": "Fermer",
573
727
  className: "-mr-1 shrink-0 rounded-md p-1 text-muted-foreground hover:bg-accent hover:text-foreground",
574
- onClick: onClose,
575
728
  children: /* @__PURE__ */ jsx(IconX, { className: "size-4" })
576
729
  }
577
730
  )
@@ -580,7 +733,7 @@ function SidePanel({
580
733
  ]
581
734
  }
582
735
  )
583
- ] });
736
+ ] }) });
584
737
  }
585
738
  var COPY_FEEDBACK_MS = 2e3;
586
739
  function CopyButton({
@@ -623,69 +776,111 @@ function Popover({
623
776
  align = "start",
624
777
  className
625
778
  }) {
626
- const root = React.useRef(null);
627
- React.useEffect(() => {
628
- if (!open) return;
629
- const onPointerDown = (e) => {
630
- if (!root.current?.contains(e.target)) onOpenChange(false);
631
- };
632
- const onKeyDown = (e) => {
633
- if (e.key === "Escape") onOpenChange(false);
634
- };
635
- document.addEventListener("mousedown", onPointerDown);
636
- document.addEventListener("keydown", onKeyDown);
637
- return () => {
638
- document.removeEventListener("mousedown", onPointerDown);
639
- document.removeEventListener("keydown", onKeyDown);
640
- };
641
- }, [open, onOpenChange]);
642
- return /* @__PURE__ */ jsxs("div", { ref: root, className: "relative inline-block", children: [
643
- /* @__PURE__ */ jsx("button", { type: "button", "aria-expanded": open, onClick: () => onOpenChange(!open), children: trigger }),
644
- open && /* @__PURE__ */ jsx(
779
+ return (
780
+ // The callback is re-wrapped so Base UI's event-details argument stops at the
781
+ // SDK boundary: the engine is an implementation detail, not part of the API.
782
+ /* @__PURE__ */ jsxs(Popover$1.Root, { open, onOpenChange: (next) => onOpenChange(next), children: [
783
+ /* @__PURE__ */ jsx("span", { className: "inline-block", children: /* @__PURE__ */ jsx(Popover$1.Trigger, { ...triggerProps(trigger) }) }),
784
+ /* @__PURE__ */ jsx(Popover$1.Portal, { children: /* @__PURE__ */ jsx(Popover$1.Positioner, { side: "bottom", align, sideOffset: 4, className: "z-50", children: /* @__PURE__ */ jsx(
785
+ Popover$1.Popup,
786
+ {
787
+ className: cn("min-w-48 rounded-md border bg-card p-2 shadow-lg", className),
788
+ children
789
+ }
790
+ ) }) })
791
+ ] })
792
+ );
793
+ }
794
+ function DropdownMenu({ trigger, items, align = "end", className }) {
795
+ return /* @__PURE__ */ jsxs(Menu.Root, { children: [
796
+ /* @__PURE__ */ jsx("span", { className: "inline-block", children: /* @__PURE__ */ jsx(Menu.Trigger, { ...triggerProps(trigger) }) }),
797
+ /* @__PURE__ */ jsx(Menu.Portal, { children: /* @__PURE__ */ jsx(Menu.Positioner, { side: "bottom", align, sideOffset: 4, className: "z-50", children: /* @__PURE__ */ jsx(
798
+ Menu.Popup,
799
+ {
800
+ className: cn(
801
+ "flex min-w-48 flex-col rounded-md border bg-card p-1 shadow-lg",
802
+ className
803
+ ),
804
+ children: items.map((item, i) => /* @__PURE__ */ jsx(
805
+ Menu.Item,
806
+ {
807
+ disabled: item.disabled,
808
+ onClick: item.onSelect,
809
+ className: cn(
810
+ "cursor-pointer rounded px-2 py-1.5 text-left text-sm outline-none data-[highlighted]:bg-accent data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
811
+ item.destructive && "text-destructive"
812
+ ),
813
+ children: item.label
814
+ },
815
+ i
816
+ ))
817
+ }
818
+ ) }) })
819
+ ] });
820
+ }
821
+ var ToastContext = createContext(null);
822
+ function useToast() {
823
+ const toast = useContext(ToastContext);
824
+ if (!toast) throw new Error("useToast must be used within ToastProvider");
825
+ return toast;
826
+ }
827
+ var TOAST_TTL_MS = 4e3;
828
+ var TONE = {
829
+ success: "border-l-status-success",
830
+ error: "border-l-status-blocked",
831
+ info: "border-l-status-info"
832
+ };
833
+ function ToastProvider({ children }) {
834
+ const [toasts, setToasts] = useState([]);
835
+ const nextId = useRef(0);
836
+ const toast = useCallback((input) => {
837
+ const id = nextId.current++;
838
+ setToasts((prev) => [...prev, { id, ...input }]);
839
+ setTimeout(() => setToasts((prev) => prev.filter((t) => t.id !== id)), TOAST_TTL_MS);
840
+ }, []);
841
+ return /* @__PURE__ */ jsxs(ToastContext.Provider, { value: toast, children: [
842
+ children,
843
+ /* @__PURE__ */ jsx("div", { className: "pointer-events-none fixed bottom-4 right-4 z-[60] flex w-80 flex-col gap-2", children: toasts.map((t) => /* @__PURE__ */ jsxs(
645
844
  "div",
646
845
  {
846
+ role: "status",
847
+ className: cn(
848
+ "pointer-events-auto rounded-lg border border-l-4 bg-card px-4 py-3 text-foreground shadow-md",
849
+ TONE[t.variant ?? "success"]
850
+ ),
851
+ children: [
852
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: t.title }),
853
+ t.description && /* @__PURE__ */ jsx("p", { className: "mt-0.5 break-all text-xs text-muted-foreground", children: t.description })
854
+ ]
855
+ },
856
+ t.id
857
+ )) })
858
+ ] });
859
+ }
860
+ var SIDES = {
861
+ top: "bottom-full left-1/2 -translate-x-1/2 mb-1.5",
862
+ right: "left-full top-1/2 -translate-y-1/2 ml-1.5",
863
+ bottom: "top-full left-1/2 -translate-x-1/2 mt-1.5",
864
+ left: "right-full top-1/2 -translate-y-1/2 mr-1.5"
865
+ };
866
+ function Tooltip({ label, children, side = "top", className }) {
867
+ return /* @__PURE__ */ jsxs("span", { className: "group/tt relative inline-flex", children: [
868
+ children,
869
+ /* @__PURE__ */ jsx(
870
+ "span",
871
+ {
872
+ role: "tooltip",
647
873
  className: cn(
648
- "absolute z-40 mt-1 min-w-48 rounded-md border bg-card p-2 shadow-lg",
649
- align === "end" ? "right-0" : "left-0",
874
+ "pointer-events-none absolute z-50 whitespace-nowrap rounded-md bg-foreground px-2 py-1 text-xs font-medium text-background opacity-0 shadow-md transition-opacity group-hover/tt:opacity-100",
875
+ SIDES[side],
650
876
  className
651
877
  ),
652
- children
878
+ children: label
653
879
  }
654
880
  )
655
881
  ] });
656
882
  }
657
- function DropdownMenu({ trigger, items, align = "end", className }) {
658
- const [open, setOpen] = React.useState(false);
659
- return /* @__PURE__ */ jsx(
660
- Popover,
661
- {
662
- open,
663
- onOpenChange: setOpen,
664
- trigger,
665
- align,
666
- className: cn("p-1", className),
667
- children: /* @__PURE__ */ jsx("div", { role: "menu", className: "flex flex-col", children: items.map((item, i) => /* @__PURE__ */ jsx(
668
- "button",
669
- {
670
- type: "button",
671
- role: "menuitem",
672
- disabled: item.disabled,
673
- className: cn(
674
- "cursor-pointer rounded px-2 py-1.5 text-left text-sm hover:bg-accent disabled:pointer-events-none disabled:opacity-50",
675
- item.destructive && "text-destructive"
676
- ),
677
- onClick: () => {
678
- setOpen(false);
679
- item.onSelect();
680
- },
681
- children: item.label
682
- },
683
- i
684
- )) })
685
- }
686
- );
687
- }
688
883
 
689
- export { AppApiProvider, AppContextProvider, AppInvokeError, Badge, Button, Card, CardContent, CardHeader, CardTitle, Checkbox, CopyButton, DateRangePicker, DropdownMenu, Input, Link, Modal, Popover, Select, SidePanel, Spinner, Switch, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Textarea, cn, copyToClipboard, downloadBlob, downloadCsv, toCsv, useAppApi, useAppContext, useAppSettings };
884
+ export { AppApiProvider, AppContextProvider, AppInvokeError, Badge, Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, CopyButton, DateRangePicker, DropdownMenu, Input, Link, Modal, Popover, Select, SidePanel, Spinner, Switch, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Textarea, ToastProvider, Tooltip, badgeVariants, buttonVariants, cn, copyToClipboard, downloadBlob, downloadCsv, toCsv, useAppApi, useAppContext, useAppRun, useAppRuns, useAppSettings, useToast };
690
885
  //# sourceMappingURL=index.js.map
691
886
  //# sourceMappingURL=index.js.map