@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.cjs CHANGED
@@ -3,6 +3,11 @@
3
3
  var clsx = require('clsx');
4
4
  var tailwindMerge = require('tailwind-merge');
5
5
  var React = require('react');
6
+ var dialog = require('@base-ui/react/dialog');
7
+ var menu = require('@base-ui/react/menu');
8
+ var popover = require('@base-ui/react/popover');
9
+ var _switch = require('@base-ui/react/switch');
10
+ var tabs = require('@base-ui/react/tabs');
6
11
  var classVarianceAuthority = require('class-variance-authority');
7
12
  var jsxRuntime = require('react/jsx-runtime');
8
13
 
@@ -109,6 +114,175 @@ function useAppSettings() {
109
114
  );
110
115
  return { settings, isLoading, error, save, isSaving, saveError };
111
116
  }
117
+ var DEFAULT_RUN_POLL_INTERVAL_MS = 1e3;
118
+ var MAX_RUN_POLL_INTERVAL_MS = 5e3;
119
+ var RUN_POLL_BACKOFF = 1.5;
120
+ var RUNS_FOLLOW_POLL_INTERVAL_MS = 3e3;
121
+ function runProgressSignature(run) {
122
+ return `${run.progress ?? ""}|${run.message ?? ""}`;
123
+ }
124
+ function isTerminalRunError(err) {
125
+ const status = extractHttpStatus(err);
126
+ return status === 403 || status === 404;
127
+ }
128
+ function extractHttpStatus(err) {
129
+ if (typeof err !== "object" || err === null) return void 0;
130
+ const direct = err.status;
131
+ if (typeof direct === "number") return direct;
132
+ const response = err.response;
133
+ if (typeof response !== "object" || response === null) return void 0;
134
+ const nested = response.status;
135
+ return typeof nested === "number" ? nested : void 0;
136
+ }
137
+ function useAppRun(runId, opts = {}) {
138
+ const { appSlug } = useAppContext();
139
+ const api = useAppApi();
140
+ const intervalMs = opts.intervalMs ?? DEFAULT_RUN_POLL_INTERVAL_MS;
141
+ const maxIntervalMs = opts.maxIntervalMs ?? MAX_RUN_POLL_INTERVAL_MS;
142
+ const [run, setRun] = React.useState(void 0);
143
+ const [isPolling, setIsPolling] = React.useState(false);
144
+ const [error, setError] = React.useState(void 0);
145
+ const clientRef = React.useRef(api);
146
+ clientRef.current = api;
147
+ const requestIdRef = React.useRef(0);
148
+ const isMountedRef = React.useRef(true);
149
+ const path = runId !== null ? `/data/apps/${appSlug}/runs/${encodeURIComponent(runId)}` : null;
150
+ const fetchOnce = React.useCallback(async () => {
151
+ if (path === null) return { kind: "no-path" };
152
+ const requestId = ++requestIdRef.current;
153
+ try {
154
+ const loaded = await clientRef.current.get(path);
155
+ if (requestId !== requestIdRef.current || !isMountedRef.current) return { kind: "superseded" };
156
+ setRun(loaded);
157
+ setError(void 0);
158
+ return { kind: "success", run: loaded };
159
+ } catch (err) {
160
+ if (requestId !== requestIdRef.current || !isMountedRef.current) return { kind: "superseded" };
161
+ setError(err);
162
+ return { kind: "error", err };
163
+ }
164
+ }, [path]);
165
+ React.useEffect(() => {
166
+ isMountedRef.current = true;
167
+ requestIdRef.current += 1;
168
+ setRun(void 0);
169
+ setError(void 0);
170
+ if (path === null) {
171
+ setIsPolling(false);
172
+ return () => {
173
+ isMountedRef.current = false;
174
+ };
175
+ }
176
+ let timeoutHandle;
177
+ let interval = intervalMs;
178
+ let lastSignature;
179
+ let cancelled = false;
180
+ const tick = async () => {
181
+ const outcome = await fetchOnce();
182
+ if (cancelled || !isMountedRef.current) return;
183
+ if (outcome.kind === "no-path") {
184
+ setIsPolling(false);
185
+ return;
186
+ }
187
+ if (outcome.kind === "superseded") {
188
+ timeoutHandle = setTimeout(() => void tick(), interval);
189
+ return;
190
+ }
191
+ if (outcome.kind === "error") {
192
+ if (isTerminalRunError(outcome.err)) {
193
+ setIsPolling(false);
194
+ return;
195
+ }
196
+ timeoutHandle = setTimeout(() => void tick(), interval);
197
+ return;
198
+ }
199
+ const loaded = outcome.run;
200
+ const signature = runProgressSignature(loaded);
201
+ interval = signature === lastSignature ? Math.min(interval * RUN_POLL_BACKOFF, maxIntervalMs) : intervalMs;
202
+ lastSignature = signature;
203
+ if (loaded.status !== "running") {
204
+ setIsPolling(false);
205
+ return;
206
+ }
207
+ timeoutHandle = setTimeout(() => void tick(), interval);
208
+ };
209
+ setIsPolling(true);
210
+ void tick();
211
+ return () => {
212
+ cancelled = true;
213
+ isMountedRef.current = false;
214
+ if (timeoutHandle) clearTimeout(timeoutHandle);
215
+ };
216
+ }, [path, fetchOnce, intervalMs, maxIntervalMs]);
217
+ const refresh = React.useCallback(async () => {
218
+ await fetchOnce();
219
+ }, [fetchOnce]);
220
+ return { run, isPolling, error, refresh };
221
+ }
222
+ function useAppRuns(opts) {
223
+ const { appSlug } = useAppContext();
224
+ const api = useAppApi();
225
+ const follow = opts.follow ?? true;
226
+ const [runs, setRuns] = React.useState([]);
227
+ const [isLoading, setIsLoading] = React.useState(true);
228
+ const [error, setError] = React.useState(void 0);
229
+ const clientRef = React.useRef(api);
230
+ clientRef.current = api;
231
+ const requestIdRef = React.useRef(0);
232
+ const isMountedRef = React.useRef(true);
233
+ const params = {};
234
+ if (opts.kind) params["kind"] = opts.kind;
235
+ if (opts.key) params["key"] = opts.key;
236
+ if (opts.status?.length) params["status"] = opts.status.join(",");
237
+ if (opts.limit) params["limit"] = String(opts.limit);
238
+ const paramsKey = JSON.stringify(params);
239
+ const path = `/data/apps/${appSlug}/runs`;
240
+ const fetchOnce = React.useCallback(
241
+ async ({ silent } = {}) => {
242
+ const requestId = ++requestIdRef.current;
243
+ if (!silent) setIsLoading(true);
244
+ try {
245
+ const page = await clientRef.current.get(
246
+ path,
247
+ JSON.parse(paramsKey)
248
+ );
249
+ if (requestId !== requestIdRef.current || !isMountedRef.current) return { kind: "superseded" };
250
+ setRuns(page.items);
251
+ setError(void 0);
252
+ return { kind: "success", items: page.items };
253
+ } catch (err) {
254
+ if (requestId !== requestIdRef.current || !isMountedRef.current) return { kind: "superseded" };
255
+ setError(err);
256
+ return { kind: "error", err };
257
+ } finally {
258
+ if (requestId === requestIdRef.current) setIsLoading(false);
259
+ }
260
+ },
261
+ [path, paramsKey]
262
+ );
263
+ React.useEffect(() => {
264
+ isMountedRef.current = true;
265
+ let timeoutHandle;
266
+ let cancelled = false;
267
+ const tick = async (silent) => {
268
+ const outcome = await fetchOnce({ silent });
269
+ if (cancelled || !isMountedRef.current || !follow) return;
270
+ if (outcome.kind !== "success" || outcome.items.some((r) => r.status === "running")) {
271
+ timeoutHandle = setTimeout(() => void tick(true), RUNS_FOLLOW_POLL_INTERVAL_MS);
272
+ }
273
+ };
274
+ void tick(false);
275
+ return () => {
276
+ cancelled = true;
277
+ isMountedRef.current = false;
278
+ if (timeoutHandle) clearTimeout(timeoutHandle);
279
+ };
280
+ }, [fetchOnce, follow]);
281
+ const refresh = React.useCallback(async () => {
282
+ await fetchOnce({ silent: true });
283
+ }, [fetchOnce]);
284
+ return { runs, isLoading, error, refresh };
285
+ }
112
286
 
113
287
  // src/utils.ts
114
288
  async function copyToClipboard(text) {
@@ -141,6 +315,12 @@ function downloadCsv(rows, filename) {
141
315
  const blob = new Blob([`${BOM}${toCsv(rows)}`], { type: "text/csv;charset=utf-8" });
142
316
  downloadBlob(blob, filename);
143
317
  }
318
+ function triggerProps(trigger) {
319
+ if (React__namespace.isValidElement(trigger)) {
320
+ return { render: trigger, nativeButton: trigger.type === "button" || trigger.type === Button };
321
+ }
322
+ return { render: /* @__PURE__ */ jsxRuntime.jsx("span", { children: trigger }), nativeButton: false };
323
+ }
144
324
  var buttonVariants = classVarianceAuthority.cva(
145
325
  "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",
146
326
  {
@@ -204,9 +384,18 @@ function CardHeader({ className, ...props }) {
204
384
  function CardTitle({ className, ...props }) {
205
385
  return /* @__PURE__ */ jsxRuntime.jsx("h3", { className: cn("text-lg font-semibold leading-none tracking-tight", className), ...props });
206
386
  }
387
+ function CardDescription({
388
+ className,
389
+ ...props
390
+ }) {
391
+ return /* @__PURE__ */ jsxRuntime.jsx("p", { className: cn("text-sm text-muted-foreground", className), ...props });
392
+ }
207
393
  function CardContent({ className, ...props }) {
208
394
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("p-6 pt-0", className), ...props });
209
395
  }
396
+ function CardFooter({ className, ...props }) {
397
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("flex items-center p-6 pt-0", className), ...props });
398
+ }
210
399
  var Input = React__namespace.forwardRef(({ className, type, ...props }, ref) => /* @__PURE__ */ jsxRuntime.jsx(
211
400
  "input",
212
401
  {
@@ -227,7 +416,13 @@ function TableHeader({
227
416
  className,
228
417
  ...props
229
418
  }) {
230
- return /* @__PURE__ */ jsxRuntime.jsx("thead", { className: cn("[&_tr]:border-b", className), ...props });
419
+ return /* @__PURE__ */ jsxRuntime.jsx(
420
+ "thead",
421
+ {
422
+ className: cn("[&_tr]:border-b [&_tr]:hover:bg-transparent", className),
423
+ ...props
424
+ }
425
+ );
231
426
  }
232
427
  function TableBody({ className, ...props }) {
233
428
  return /* @__PURE__ */ jsxRuntime.jsx("tbody", { className: cn("[&_tr:last-child]:border-0", className), ...props });
@@ -249,7 +444,7 @@ function TableHead({ className, ...props }) {
249
444
  "th",
250
445
  {
251
446
  className: cn(
252
- "h-10 px-4 text-left align-middle text-xs font-medium text-muted-foreground",
447
+ "h-10 px-4 text-left align-middle text-xs font-medium uppercase tracking-wide text-muted-foreground",
253
448
  className
254
449
  ),
255
450
  ...props
@@ -274,42 +469,43 @@ var Select = React__namespace.forwardRef(
274
469
  )
275
470
  );
276
471
  Select.displayName = "Select";
277
- var TabsContext = React__namespace.createContext(null);
278
- function useTabsContext() {
279
- const ctx = React__namespace.useContext(TabsContext);
280
- if (!ctx) throw new Error("Tabs sub-component must be used within <Tabs>");
281
- return ctx;
282
- }
283
472
  function Tabs({ defaultValue, value, onValueChange, className, children }) {
284
- const [internal, setInternal] = React__namespace.useState(defaultValue ?? "");
285
- const current = value ?? internal;
286
- const setValue = (v) => {
287
- setInternal(v);
288
- onValueChange?.(v);
289
- };
290
- return /* @__PURE__ */ jsxRuntime.jsx(TabsContext.Provider, { value: { value: current, setValue }, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className, children }) });
473
+ return /* @__PURE__ */ jsxRuntime.jsx(
474
+ tabs.Tabs.Root,
475
+ {
476
+ className,
477
+ defaultValue,
478
+ value,
479
+ onValueChange: (next) => onValueChange?.(String(next)),
480
+ children
481
+ }
482
+ );
291
483
  }
292
484
  function TabsList({
293
485
  className,
294
486
  children
295
487
  }) {
296
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("inline-flex items-center gap-1 rounded-lg bg-muted p-1", className), children });
488
+ return /* @__PURE__ */ jsxRuntime.jsx(
489
+ tabs.Tabs.List,
490
+ {
491
+ className: cn("inline-flex items-center gap-1 rounded-lg bg-muted p-1", className),
492
+ children
493
+ }
494
+ );
297
495
  }
298
496
  function TabsTrigger({
299
497
  value,
300
498
  className,
301
499
  children
302
500
  }) {
303
- const { value: current, setValue } = useTabsContext();
304
- const active = current === value;
305
501
  return /* @__PURE__ */ jsxRuntime.jsx(
306
- "button",
502
+ tabs.Tabs.Tab,
307
503
  {
308
- type: "button",
309
- onClick: () => setValue(value),
504
+ value,
310
505
  className: cn(
311
506
  "rounded-md px-3 py-1.5 text-sm font-medium transition-colors",
312
- active ? "bg-card text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground",
507
+ "text-muted-foreground hover:text-foreground",
508
+ "data-[active]:bg-card data-[active]:text-foreground data-[active]:shadow-sm",
313
509
  className
314
510
  ),
315
511
  children
@@ -321,38 +517,25 @@ function TabsContent({
321
517
  className,
322
518
  children
323
519
  }) {
324
- const { value: current } = useTabsContext();
325
- if (current !== value) return null;
326
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className, children });
520
+ return /* @__PURE__ */ jsxRuntime.jsx(tabs.Tabs.Panel, { value, className, children });
327
521
  }
328
522
  function Modal({ open, onClose, title, children, className }) {
329
- if (!open) return null;
330
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fixed inset-0 z-50 flex items-center justify-center p-4", children: [
331
- /* @__PURE__ */ jsxRuntime.jsx(
332
- "button",
333
- {
334
- type: "button",
335
- "aria-label": "Close",
336
- className: "absolute inset-0 cursor-default bg-foreground/40",
337
- onClick: onClose
338
- }
339
- ),
523
+ return /* @__PURE__ */ jsxRuntime.jsx(dialog.Dialog.Root, { open, onOpenChange: (next) => !next && onClose(), children: /* @__PURE__ */ jsxRuntime.jsxs(dialog.Dialog.Portal, { children: [
524
+ /* @__PURE__ */ jsxRuntime.jsx(dialog.Dialog.Backdrop, { "aria-label": "Close", className: "fixed inset-0 z-50 bg-foreground/40" }),
340
525
  /* @__PURE__ */ jsxRuntime.jsxs(
341
- "div",
526
+ dialog.Dialog.Popup,
342
527
  {
343
- role: "dialog",
344
- "aria-modal": true,
345
528
  className: cn(
346
- "relative z-10 w-full max-w-md rounded-xl border bg-card p-5 shadow-lg",
529
+ "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",
347
530
  className
348
531
  ),
349
532
  children: [
350
- title && /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "mb-3 text-base font-semibold", children: title }),
533
+ title && /* @__PURE__ */ jsxRuntime.jsx(dialog.Dialog.Title, { className: "mb-3 text-base font-semibold", children: title }),
351
534
  children
352
535
  ]
353
536
  }
354
537
  )
355
- ] });
538
+ ] }) });
356
539
  }
357
540
  var spinnerSizes = { sm: "size-4", md: "size-6", lg: "size-8" };
358
541
  function Spinner({ className, size = "md" }) {
@@ -402,28 +585,19 @@ function Switch({
402
585
  ...props
403
586
  }) {
404
587
  return /* @__PURE__ */ jsxRuntime.jsx(
405
- "button",
588
+ _switch.Switch.Root,
406
589
  {
407
- type: "button",
408
- role: "switch",
409
- "aria-checked": checked,
590
+ checked,
591
+ onCheckedChange: (next) => onCheckedChange?.(next),
410
592
  disabled,
411
- onClick: () => onCheckedChange?.(!checked),
412
593
  className: cn(
413
- "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",
414
- checked ? "bg-primary" : "bg-input",
594
+ // `data-[disabled]`, not `disabled:` the root is a span, so `:disabled`
595
+ // never matches it and the dimming would silently stop happening.
596
+ "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",
415
597
  className
416
598
  ),
417
599
  ...props,
418
- children: /* @__PURE__ */ jsxRuntime.jsx(
419
- "span",
420
- {
421
- className: cn(
422
- "inline-block size-4 transform rounded-full bg-white shadow transition-transform",
423
- checked ? "translate-x-4" : "translate-x-0.5"
424
- )
425
- }
426
- )
600
+ children: /* @__PURE__ */ jsxRuntime.jsx(_switch.Switch.Thumb, { className: "inline-block size-4 translate-x-0.5 rounded-full bg-white shadow transition-transform data-[checked]:translate-x-4" })
427
601
  }
428
602
  );
429
603
  }
@@ -552,47 +726,26 @@ function SidePanel({
552
726
  children,
553
727
  className
554
728
  }) {
555
- React__namespace.useEffect(() => {
556
- if (!open) return;
557
- const onKeyDown = (e) => {
558
- if (e.key === "Escape") onClose();
559
- };
560
- window.addEventListener("keydown", onKeyDown);
561
- return () => window.removeEventListener("keydown", onKeyDown);
562
- }, [open, onClose]);
563
- if (!open) return null;
564
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fixed inset-0 z-50", children: [
565
- /* @__PURE__ */ jsxRuntime.jsx(
566
- "button",
567
- {
568
- type: "button",
569
- "aria-label": "Fermer",
570
- className: "absolute inset-0 cursor-default bg-foreground/40",
571
- onClick: onClose
572
- }
573
- ),
729
+ return /* @__PURE__ */ jsxRuntime.jsx(dialog.Dialog.Root, { open, onOpenChange: (next) => !next && onClose(), children: /* @__PURE__ */ jsxRuntime.jsxs(dialog.Dialog.Portal, { children: [
730
+ /* @__PURE__ */ jsxRuntime.jsx(dialog.Dialog.Backdrop, { "aria-label": "Fermer", className: "fixed inset-0 z-50 bg-foreground/40" }),
574
731
  /* @__PURE__ */ jsxRuntime.jsxs(
575
- "aside",
732
+ dialog.Dialog.Popup,
576
733
  {
577
- role: "dialog",
578
- "aria-modal": true,
579
734
  className: cn(
580
- "absolute inset-y-0 right-0 flex w-full max-w-lg flex-col border-l bg-card shadow-xl",
735
+ "fixed inset-y-0 right-0 z-50 flex w-full max-w-lg flex-col border-l bg-card shadow-xl",
581
736
  className
582
737
  ),
583
738
  children: [
584
739
  /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex shrink-0 items-start gap-3 border-b px-5 py-4", children: [
585
740
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
586
- title && /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-base font-semibold", children: title }),
587
- description && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-0.5 text-xs text-muted-foreground", children: description })
741
+ title && /* @__PURE__ */ jsxRuntime.jsx(dialog.Dialog.Title, { className: "text-base font-semibold", children: title }),
742
+ description && /* @__PURE__ */ jsxRuntime.jsx(dialog.Dialog.Description, { className: "mt-0.5 text-xs text-muted-foreground", children: description })
588
743
  ] }),
589
744
  /* @__PURE__ */ jsxRuntime.jsx(
590
- "button",
745
+ dialog.Dialog.Close,
591
746
  {
592
- type: "button",
593
747
  "aria-label": "Fermer",
594
748
  className: "-mr-1 shrink-0 rounded-md p-1 text-muted-foreground hover:bg-accent hover:text-foreground",
595
- onClick: onClose,
596
749
  children: /* @__PURE__ */ jsxRuntime.jsx(IconX, { className: "size-4" })
597
750
  }
598
751
  )
@@ -601,7 +754,7 @@ function SidePanel({
601
754
  ]
602
755
  }
603
756
  )
604
- ] });
757
+ ] }) });
605
758
  }
606
759
  var COPY_FEEDBACK_MS = 2e3;
607
760
  function CopyButton({
@@ -644,68 +797,110 @@ function Popover({
644
797
  align = "start",
645
798
  className
646
799
  }) {
647
- const root = React__namespace.useRef(null);
648
- React__namespace.useEffect(() => {
649
- if (!open) return;
650
- const onPointerDown = (e) => {
651
- if (!root.current?.contains(e.target)) onOpenChange(false);
652
- };
653
- const onKeyDown = (e) => {
654
- if (e.key === "Escape") onOpenChange(false);
655
- };
656
- document.addEventListener("mousedown", onPointerDown);
657
- document.addEventListener("keydown", onKeyDown);
658
- return () => {
659
- document.removeEventListener("mousedown", onPointerDown);
660
- document.removeEventListener("keydown", onKeyDown);
661
- };
662
- }, [open, onOpenChange]);
663
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: root, className: "relative inline-block", children: [
664
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "aria-expanded": open, onClick: () => onOpenChange(!open), children: trigger }),
665
- open && /* @__PURE__ */ jsxRuntime.jsx(
800
+ return (
801
+ // The callback is re-wrapped so Base UI's event-details argument stops at the
802
+ // SDK boundary: the engine is an implementation detail, not part of the API.
803
+ /* @__PURE__ */ jsxRuntime.jsxs(popover.Popover.Root, { open, onOpenChange: (next) => onOpenChange(next), children: [
804
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-block", children: /* @__PURE__ */ jsxRuntime.jsx(popover.Popover.Trigger, { ...triggerProps(trigger) }) }),
805
+ /* @__PURE__ */ jsxRuntime.jsx(popover.Popover.Portal, { children: /* @__PURE__ */ jsxRuntime.jsx(popover.Popover.Positioner, { side: "bottom", align, sideOffset: 4, className: "z-50", children: /* @__PURE__ */ jsxRuntime.jsx(
806
+ popover.Popover.Popup,
807
+ {
808
+ className: cn("min-w-48 rounded-md border bg-card p-2 shadow-lg", className),
809
+ children
810
+ }
811
+ ) }) })
812
+ ] })
813
+ );
814
+ }
815
+ function DropdownMenu({ trigger, items, align = "end", className }) {
816
+ return /* @__PURE__ */ jsxRuntime.jsxs(menu.Menu.Root, { children: [
817
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-block", children: /* @__PURE__ */ jsxRuntime.jsx(menu.Menu.Trigger, { ...triggerProps(trigger) }) }),
818
+ /* @__PURE__ */ jsxRuntime.jsx(menu.Menu.Portal, { children: /* @__PURE__ */ jsxRuntime.jsx(menu.Menu.Positioner, { side: "bottom", align, sideOffset: 4, className: "z-50", children: /* @__PURE__ */ jsxRuntime.jsx(
819
+ menu.Menu.Popup,
820
+ {
821
+ className: cn(
822
+ "flex min-w-48 flex-col rounded-md border bg-card p-1 shadow-lg",
823
+ className
824
+ ),
825
+ children: items.map((item, i) => /* @__PURE__ */ jsxRuntime.jsx(
826
+ menu.Menu.Item,
827
+ {
828
+ disabled: item.disabled,
829
+ onClick: item.onSelect,
830
+ className: cn(
831
+ "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",
832
+ item.destructive && "text-destructive"
833
+ ),
834
+ children: item.label
835
+ },
836
+ i
837
+ ))
838
+ }
839
+ ) }) })
840
+ ] });
841
+ }
842
+ var ToastContext = React.createContext(null);
843
+ function useToast() {
844
+ const toast = React.useContext(ToastContext);
845
+ if (!toast) throw new Error("useToast must be used within ToastProvider");
846
+ return toast;
847
+ }
848
+ var TOAST_TTL_MS = 4e3;
849
+ var TONE = {
850
+ success: "border-l-status-success",
851
+ error: "border-l-status-blocked",
852
+ info: "border-l-status-info"
853
+ };
854
+ function ToastProvider({ children }) {
855
+ const [toasts, setToasts] = React.useState([]);
856
+ const nextId = React.useRef(0);
857
+ const toast = React.useCallback((input) => {
858
+ const id = nextId.current++;
859
+ setToasts((prev) => [...prev, { id, ...input }]);
860
+ setTimeout(() => setToasts((prev) => prev.filter((t) => t.id !== id)), TOAST_TTL_MS);
861
+ }, []);
862
+ return /* @__PURE__ */ jsxRuntime.jsxs(ToastContext.Provider, { value: toast, children: [
863
+ children,
864
+ /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsxs(
666
865
  "div",
667
866
  {
867
+ role: "status",
868
+ className: cn(
869
+ "pointer-events-auto rounded-lg border border-l-4 bg-card px-4 py-3 text-foreground shadow-md",
870
+ TONE[t.variant ?? "success"]
871
+ ),
872
+ children: [
873
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-medium", children: t.title }),
874
+ t.description && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-0.5 break-all text-xs text-muted-foreground", children: t.description })
875
+ ]
876
+ },
877
+ t.id
878
+ )) })
879
+ ] });
880
+ }
881
+ var SIDES = {
882
+ top: "bottom-full left-1/2 -translate-x-1/2 mb-1.5",
883
+ right: "left-full top-1/2 -translate-y-1/2 ml-1.5",
884
+ bottom: "top-full left-1/2 -translate-x-1/2 mt-1.5",
885
+ left: "right-full top-1/2 -translate-y-1/2 mr-1.5"
886
+ };
887
+ function Tooltip({ label, children, side = "top", className }) {
888
+ return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "group/tt relative inline-flex", children: [
889
+ children,
890
+ /* @__PURE__ */ jsxRuntime.jsx(
891
+ "span",
892
+ {
893
+ role: "tooltip",
668
894
  className: cn(
669
- "absolute z-40 mt-1 min-w-48 rounded-md border bg-card p-2 shadow-lg",
670
- align === "end" ? "right-0" : "left-0",
895
+ "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",
896
+ SIDES[side],
671
897
  className
672
898
  ),
673
- children
899
+ children: label
674
900
  }
675
901
  )
676
902
  ] });
677
903
  }
678
- function DropdownMenu({ trigger, items, align = "end", className }) {
679
- const [open, setOpen] = React__namespace.useState(false);
680
- return /* @__PURE__ */ jsxRuntime.jsx(
681
- Popover,
682
- {
683
- open,
684
- onOpenChange: setOpen,
685
- trigger,
686
- align,
687
- className: cn("p-1", className),
688
- children: /* @__PURE__ */ jsxRuntime.jsx("div", { role: "menu", className: "flex flex-col", children: items.map((item, i) => /* @__PURE__ */ jsxRuntime.jsx(
689
- "button",
690
- {
691
- type: "button",
692
- role: "menuitem",
693
- disabled: item.disabled,
694
- className: cn(
695
- "cursor-pointer rounded px-2 py-1.5 text-left text-sm hover:bg-accent disabled:pointer-events-none disabled:opacity-50",
696
- item.destructive && "text-destructive"
697
- ),
698
- onClick: () => {
699
- setOpen(false);
700
- item.onSelect();
701
- },
702
- children: item.label
703
- },
704
- i
705
- )) })
706
- }
707
- );
708
- }
709
904
 
710
905
  exports.AppApiProvider = AppApiProvider;
711
906
  exports.AppContextProvider = AppContextProvider;
@@ -714,6 +909,8 @@ exports.Badge = Badge;
714
909
  exports.Button = Button;
715
910
  exports.Card = Card;
716
911
  exports.CardContent = CardContent;
912
+ exports.CardDescription = CardDescription;
913
+ exports.CardFooter = CardFooter;
717
914
  exports.CardHeader = CardHeader;
718
915
  exports.CardTitle = CardTitle;
719
916
  exports.Checkbox = Checkbox;
@@ -739,6 +936,10 @@ exports.TabsContent = TabsContent;
739
936
  exports.TabsList = TabsList;
740
937
  exports.TabsTrigger = TabsTrigger;
741
938
  exports.Textarea = Textarea;
939
+ exports.ToastProvider = ToastProvider;
940
+ exports.Tooltip = Tooltip;
941
+ exports.badgeVariants = badgeVariants;
942
+ exports.buttonVariants = buttonVariants;
742
943
  exports.cn = cn;
743
944
  exports.copyToClipboard = copyToClipboard;
744
945
  exports.downloadBlob = downloadBlob;
@@ -746,6 +947,9 @@ exports.downloadCsv = downloadCsv;
746
947
  exports.toCsv = toCsv;
747
948
  exports.useAppApi = useAppApi;
748
949
  exports.useAppContext = useAppContext;
950
+ exports.useAppRun = useAppRun;
951
+ exports.useAppRuns = useAppRuns;
749
952
  exports.useAppSettings = useAppSettings;
953
+ exports.useToast = useToast;
750
954
  //# sourceMappingURL=index.cjs.map
751
955
  //# sourceMappingURL=index.cjs.map