@columenlabs/lumen-ui 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/NOTICE +12 -0
- package/README.md +4 -2
- package/dist/add-column.d.ts +23 -0
- package/dist/add-column.js +62 -0
- package/dist/api-fetch.d.ts +9 -0
- package/dist/api-fetch.js +60 -0
- package/dist/app-switcher.d.ts +21 -0
- package/dist/app-switcher.js +12 -0
- package/dist/app-views.d.ts +18 -0
- package/dist/app-views.js +41 -0
- package/dist/assistant-context.d.ts +72 -0
- package/dist/assistant-context.js +30 -0
- package/dist/assistant-panel.d.ts +58 -0
- package/dist/assistant-panel.js +244 -0
- package/dist/assistant-screen.d.ts +124 -0
- package/dist/assistant-screen.js +252 -0
- package/dist/card-switch.d.ts +16 -0
- package/dist/card-switch.js +16 -0
- package/dist/color-dot.d.ts +14 -0
- package/dist/color-dot.js +19 -0
- package/dist/confirm.d.ts +18 -0
- package/dist/confirm.js +5 -0
- package/dist/connections.d.ts +45 -0
- package/dist/connections.js +275 -0
- package/dist/coverage-calendar-map.d.ts +27 -0
- package/dist/coverage-calendar-map.js +84 -0
- package/dist/coverage-cells.d.ts +55 -0
- package/dist/coverage-cells.js +85 -0
- package/dist/coverage-map.d.ts +112 -0
- package/dist/coverage-map.js +428 -0
- package/dist/details-table.d.ts +20 -0
- package/dist/details-table.js +13 -0
- package/dist/drawer-section.d.ts +27 -0
- package/dist/drawer-section.js +23 -0
- package/dist/floating-head.d.ts +2 -0
- package/dist/floating-head.js +57 -0
- package/dist/fold.d.ts +16 -0
- package/dist/fold.js +70 -0
- package/dist/form-issues.d.ts +32 -0
- package/dist/form-issues.js +60 -0
- package/dist/formula-input.d.ts +17 -0
- package/dist/formula-input.js +104 -0
- package/dist/frame-script.d.ts +7 -0
- package/dist/frame-script.js +103 -0
- package/dist/freshness.d.ts +24 -0
- package/dist/freshness.js +99 -0
- package/dist/group-button.d.ts +33 -0
- package/dist/group-button.js +67 -0
- package/dist/growth-chart.d.ts +77 -0
- package/dist/growth-chart.js +195 -0
- package/dist/hooks/use-keyboard-fit.d.ts +15 -0
- package/dist/hooks/use-keyboard-fit.js +29 -0
- package/dist/hooks/use-mobile.d.ts +1 -0
- package/dist/hooks/use-mobile.js +15 -0
- package/dist/icon-button.d.ts +21 -0
- package/dist/icon-button.js +22 -0
- package/dist/icon-picker.d.ts +14 -0
- package/dist/icon-picker.js +36 -0
- package/dist/index.d.ts +83 -0
- package/dist/index.js +90 -0
- package/dist/integrations.d.ts +57 -0
- package/dist/integrations.js +73 -0
- package/dist/joins.d.ts +128 -0
- package/dist/joins.js +767 -0
- package/dist/kit-config.d.ts +13 -0
- package/dist/kit-config.js +15 -0
- package/dist/lib/utils.d.ts +2 -0
- package/dist/lib/utils.js +5 -0
- package/dist/lumen-boundary.d.ts +7 -0
- package/dist/lumen-boundary.js +10 -0
- package/dist/lumen-code-editor-impl.d.ts +2 -0
- package/dist/lumen-code-editor-impl.js +16 -0
- package/dist/lumen-code-editor.d.ts +11 -0
- package/dist/lumen-code-editor.js +11 -0
- package/dist/lumen-code.d.ts +22 -0
- package/dist/lumen-code.js +75 -0
- package/dist/lumen-drawer.d.ts +30 -0
- package/dist/lumen-drawer.js +27 -0
- package/dist/lumen-echart.d.ts +58 -0
- package/dist/lumen-echart.js +129 -0
- package/dist/lumen-filter.d.ts +75 -0
- package/dist/lumen-filter.js +546 -0
- package/dist/lumen-finder.d.ts +62 -0
- package/dist/lumen-finder.js +275 -0
- package/dist/lumen-form.d.ts +34 -0
- package/dist/lumen-form.js +81 -0
- package/dist/lumen-geo-map.d.ts +9 -0
- package/dist/lumen-geo-map.js +135 -0
- package/dist/lumen-grid.d.ts +49 -0
- package/dist/lumen-grid.js +203 -0
- package/dist/lumen-layout.d.ts +150 -0
- package/dist/lumen-layout.js +661 -0
- package/dist/lumen-links.d.ts +96 -0
- package/dist/lumen-links.js +213 -0
- package/dist/lumen-page-state.d.ts +20 -0
- package/dist/lumen-page-state.js +15 -0
- package/dist/lumen-selector.d.ts +139 -0
- package/dist/lumen-selector.js +308 -0
- package/dist/lumen-source.d.ts +137 -0
- package/dist/lumen-source.js +327 -0
- package/dist/lumen-tab-drawer.d.ts +27 -0
- package/dist/lumen-tab-drawer.js +26 -0
- package/dist/lumen-tabs.d.ts +21 -0
- package/dist/lumen-tabs.js +6 -0
- package/dist/lumen-toast.d.ts +11 -0
- package/dist/lumen-toast.js +16 -0
- package/dist/lumen-trend.d.ts +18 -0
- package/dist/lumen-trend.js +157 -0
- package/dist/lumen-view-card.d.ts +35 -0
- package/dist/lumen-view-card.js +50 -0
- package/dist/lumen-view.d.ts +172 -0
- package/dist/lumen-view.js +645 -0
- package/dist/lumen-widget-charts.d.ts +57 -0
- package/dist/lumen-widget-charts.js +69 -0
- package/dist/lumen-widget.d.ts +720 -0
- package/dist/lumen-widget.js +609 -0
- package/dist/market/compare-chart.d.ts +34 -0
- package/dist/market/compare-chart.js +292 -0
- package/dist/market/indicator-picker.d.ts +29 -0
- package/dist/market/indicator-picker.js +125 -0
- package/dist/market/indicators.d.ts +63 -0
- package/dist/market/indicators.js +314 -0
- package/dist/market/live.d.ts +120 -0
- package/dist/market/live.js +444 -0
- package/dist/market/lumen-chart.d.ts +88 -0
- package/dist/market/lumen-chart.js +645 -0
- package/dist/market/market-widget.d.ts +45 -0
- package/dist/market/market-widget.js +346 -0
- package/dist/market/option-chain.d.ts +19 -0
- package/dist/market/option-chain.js +53 -0
- package/dist/market/price-chart.d.ts +128 -0
- package/dist/market/price-chart.js +619 -0
- package/dist/mcp-settings.d.ts +20 -0
- package/dist/mcp-settings.js +65 -0
- package/dist/nav.d.ts +77 -0
- package/dist/nav.js +73 -0
- package/dist/note-line.d.ts +13 -0
- package/dist/note-line.js +5 -0
- package/dist/notifications.d.ts +9 -0
- package/dist/notifications.js +166 -0
- package/dist/origin-legend.d.ts +14 -0
- package/dist/origin-legend.js +38 -0
- package/dist/overview.d.ts +84 -0
- package/dist/overview.js +105 -0
- package/dist/page-inputs.d.ts +14 -0
- package/dist/page-inputs.js +16 -0
- package/dist/page.d.ts +32 -0
- package/dist/page.js +45 -0
- package/dist/pager.d.ts +10 -0
- package/dist/pager.js +14 -0
- package/dist/prefs.d.ts +71 -0
- package/dist/prefs.js +126 -0
- package/dist/query-client.d.ts +7 -0
- package/dist/query-client.js +19 -0
- package/dist/refresh.d.ts +4 -0
- package/dist/refresh.js +19 -0
- package/dist/rename-field.d.ts +11 -0
- package/dist/rename-field.js +26 -0
- package/dist/reset-button.d.ts +9 -0
- package/dist/reset-button.js +10 -0
- package/dist/route-error.d.ts +42 -0
- package/dist/route-error.js +229 -0
- package/dist/schedules.d.ts +88 -0
- package/dist/schedules.js +221 -0
- package/dist/settings.d.ts +51 -0
- package/dist/settings.js +181 -0
- package/dist/shell.d.ts +99 -0
- package/dist/shell.js +137 -0
- package/dist/source-health.d.ts +12 -0
- package/dist/source-health.js +58 -0
- package/dist/sourced-card.d.ts +10 -0
- package/dist/sourced-card.js +69 -0
- package/dist/spotlight.d.ts +23 -0
- package/dist/spotlight.js +138 -0
- package/dist/status-health.d.ts +16 -0
- package/dist/status-health.js +41 -0
- package/dist/status.d.ts +9 -0
- package/dist/status.js +12 -0
- package/dist/summary-cards.d.ts +56 -0
- package/dist/summary-cards.js +270 -0
- package/dist/symbol-sheet-context.d.ts +4 -0
- package/dist/symbol-sheet-context.js +5 -0
- package/dist/symbol-sheet.d.ts +9 -0
- package/dist/symbol-sheet.js +24 -0
- package/dist/tasks.d.ts +29 -0
- package/dist/tasks.js +278 -0
- package/dist/theme.css +606 -0
- package/dist/tile.d.ts +37 -0
- package/dist/tile.js +23 -0
- package/dist/ui/alert-dialog.d.ts +18 -0
- package/dist/ui/alert-dialog.js +41 -0
- package/dist/ui/alert.d.ts +10 -0
- package/dist/ui/alert.js +27 -0
- package/dist/ui/avatar.d.ts +11 -0
- package/dist/ui/avatar.js +22 -0
- package/dist/ui/badge.d.ts +9 -0
- package/dist/ui/badge.js +27 -0
- package/dist/ui/breadcrumb.d.ts +11 -0
- package/dist/ui/breadcrumb.js +27 -0
- package/dist/ui/button-group.d.ts +11 -0
- package/dist/ui/button-group.js +27 -0
- package/dist/ui/button.d.ts +10 -0
- package/dist/ui/button.js +38 -0
- package/dist/ui/card.d.ts +11 -0
- package/dist/ui/card.js +24 -0
- package/dist/ui/chart.d.ts +44 -0
- package/dist/ui/chart.js +117 -0
- package/dist/ui/checkbox.d.ts +4 -0
- package/dist/ui/checkbox.js +8 -0
- package/dist/ui/collapsible.d.ts +5 -0
- package/dist/ui/collapsible.js +12 -0
- package/dist/ui/combobox.d.ts +24 -0
- package/dist/ui/combobox.js +60 -0
- package/dist/ui/command.d.ts +18 -0
- package/dist/ui/command.js +34 -0
- package/dist/ui/dialog.d.ts +23 -0
- package/dist/ui/dialog.js +50 -0
- package/dist/ui/dropdown-menu.d.ts +29 -0
- package/dist/ui/dropdown-menu.js +50 -0
- package/dist/ui/emoji-picker.d.ts +6 -0
- package/dist/ui/emoji-picker.js +28 -0
- package/dist/ui/empty.d.ts +11 -0
- package/dist/ui/empty.js +33 -0
- package/dist/ui/field.d.ts +24 -0
- package/dist/ui/field.js +65 -0
- package/dist/ui/hover-card.d.ts +6 -0
- package/dist/ui/hover-card.js +13 -0
- package/dist/ui/input-group.d.ts +16 -0
- package/dist/ui/input-group.js +56 -0
- package/dist/ui/input.d.ts +3 -0
- package/dist/ui/input.js +6 -0
- package/dist/ui/item.d.ts +23 -0
- package/dist/ui/item.js +67 -0
- package/dist/ui/kbd.d.ts +3 -0
- package/dist/ui/kbd.js +9 -0
- package/dist/ui/label.d.ts +4 -0
- package/dist/ui/label.js +7 -0
- package/dist/ui/popover.d.ts +10 -0
- package/dist/ui/popover.js +25 -0
- package/dist/ui/progress.d.ts +4 -0
- package/dist/ui/progress.js +9 -0
- package/dist/ui/resizable.d.ts +7 -0
- package/dist/ui/resizable.js +13 -0
- package/dist/ui/scroll-area.d.ts +5 -0
- package/dist/ui/scroll-area.js +10 -0
- package/dist/ui/select.d.ts +15 -0
- package/dist/ui/select.js +36 -0
- package/dist/ui/separator.d.ts +4 -0
- package/dist/ui/separator.js +7 -0
- package/dist/ui/sheet.d.ts +14 -0
- package/dist/ui/sheet.js +54 -0
- package/dist/ui/sidebar.d.ts +69 -0
- package/dist/ui/sidebar.js +206 -0
- package/dist/ui/skeleton.d.ts +2 -0
- package/dist/ui/skeleton.js +6 -0
- package/dist/ui/slider.d.ts +4 -0
- package/dist/ui/slider.js +9 -0
- package/dist/ui/sonner.d.ts +3 -0
- package/dist/ui/sonner.js +24 -0
- package/dist/ui/spinner.d.ts +2 -0
- package/dist/ui/spinner.js +7 -0
- package/dist/ui/switch.d.ts +6 -0
- package/dist/ui/switch.js +7 -0
- package/dist/ui/table.d.ts +10 -0
- package/dist/ui/table.js +31 -0
- package/dist/ui/tabs.d.ts +7 -0
- package/dist/ui/tabs.js +22 -0
- package/dist/ui/textarea.d.ts +3 -0
- package/dist/ui/textarea.js +6 -0
- package/dist/ui/toggle-group.d.ts +10 -0
- package/dist/ui/toggle-group.js +22 -0
- package/dist/ui/toggle.d.ts +9 -0
- package/dist/ui/toggle.js +25 -0
- package/dist/ui/tooltip.d.ts +7 -0
- package/dist/ui/tooltip.js +16 -0
- package/dist/view-edit-cell.d.ts +49 -0
- package/dist/view-edit-cell.js +198 -0
- package/dist/view-editor.d.ts +52 -0
- package/dist/view-editor.js +1081 -0
- package/dist/view-formulas.d.ts +61 -0
- package/dist/view-formulas.js +247 -0
- package/dist/view-icons.d.ts +20 -0
- package/dist/view-icons.js +70 -0
- package/dist/view-model.d.ts +390 -0
- package/dist/view-model.js +316 -0
- package/dist/whats-new.d.ts +3 -0
- package/dist/whats-new.js +53 -0
- package/dist/widget-controls.d.ts +9 -0
- package/dist/widget-controls.js +26 -0
- package/dist/widget-data.d.ts +4 -0
- package/dist/widget-data.js +32 -0
- package/dist/widget-settings.d.ts +516 -0
- package/dist/widget-settings.js +290 -0
- package/dist/widget-thumbs.d.ts +3 -0
- package/dist/widget-thumbs.js +29 -0
- package/dist/workspace-keys.d.ts +12 -0
- package/dist/workspace-keys.js +13 -0
- package/dist/workspaces.d.ts +57 -0
- package/dist/workspaces.js +473 -0
- package/package.json +82 -4
package/dist/tasks.js
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { isActive } from '@columenlabs/lumen-ui-types/tasks';
|
|
3
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
4
|
+
import { useRouter } from '@tanstack/react-router';
|
|
5
|
+
import { ArrowUpRightIcon, BrushCleaningIcon, CheckIcon, ChevronRightIcon, CircleAlertIcon, Clock3Icon, ListChecksIcon, XIcon, } from 'lucide-react';
|
|
6
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from 'react';
|
|
7
|
+
import { toast } from 'sonner';
|
|
8
|
+
// Tasks (in Lumen for
|
|
9
|
+
// every app): long work a person started on any of the product's apps (a Sweep, a
|
|
10
|
+
// backfill, a sync, a Search) in one list. The header button sits left of the assistant: a ring while anything runs, the count, and a gold dot when something
|
|
11
|
+
// ended unseen. It opens a popover on desktop and a bottom sheet on phone with the last 7 days;
|
|
12
|
+
// opening it marks the ended ones seen. ✕ clears one ended task, the brush every ended one
|
|
13
|
+
//. A task that ends while the page is open shows a toast with
|
|
14
|
+
// Open. The list polls one cheap read every 3 s only while something runs and the tab is visible;
|
|
15
|
+
// otherwise it reads again on focus. The product mounts `TasksProvider` and `TasksButton` in the
|
|
16
|
+
// shell's slots; where the API answers 404 nothing shows.
|
|
17
|
+
import { LumenConfirm } from './confirm.js';
|
|
18
|
+
import { useIsMobile } from './hooks/use-mobile.js';
|
|
19
|
+
import { cn } from './lib/utils.js';
|
|
20
|
+
import { IconButton } from './page.js';
|
|
21
|
+
import { useShell } from './shell.js';
|
|
22
|
+
import { Badge } from './ui/badge.js';
|
|
23
|
+
import { Button } from './ui/button.js';
|
|
24
|
+
import { Popover, PopoverContent, PopoverTrigger } from './ui/popover.js';
|
|
25
|
+
import { Progress } from './ui/progress.js';
|
|
26
|
+
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger, } from './ui/sheet.js';
|
|
27
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip.js';
|
|
28
|
+
const TasksCtx = createContext(null);
|
|
29
|
+
/** The Tasks list, or null where the app has none; `refresh()` after starting one shows it at once. */
|
|
30
|
+
export function useTasks() {
|
|
31
|
+
return useContext(TasksCtx);
|
|
32
|
+
}
|
|
33
|
+
const POLL_MS = 3000;
|
|
34
|
+
export function TasksProvider({ config, children }) {
|
|
35
|
+
if (!config)
|
|
36
|
+
return children;
|
|
37
|
+
return (_jsx(TasksLive, { api: config.api, apps: config.apps, children: children }));
|
|
38
|
+
}
|
|
39
|
+
function TasksLive({ api, apps, children, }) {
|
|
40
|
+
const [list, setList] = useState(null);
|
|
41
|
+
const [gone, setGone] = useState(false);
|
|
42
|
+
const router = useRouter();
|
|
43
|
+
const qc = useQueryClient();
|
|
44
|
+
const shell = useShell();
|
|
45
|
+
// What was active at the last read, so a task that ends while the page is open gets a toast.
|
|
46
|
+
const activeIds = useRef(null);
|
|
47
|
+
const open = useCallback((t) => {
|
|
48
|
+
if (!t.link)
|
|
49
|
+
return;
|
|
50
|
+
const url = new URL(t.link, window.location.href);
|
|
51
|
+
if (url.origin !== window.location.origin) {
|
|
52
|
+
window.location.assign(url.href);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const section = url.searchParams.get('settings');
|
|
56
|
+
if (section !== null) {
|
|
57
|
+
shell.openSettings(section || undefined);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const base = (router.options.basepath ?? '/').replace(/\/$/, '');
|
|
61
|
+
const path = base && url.pathname.startsWith(base) ? url.pathname.slice(base.length) : url.pathname;
|
|
62
|
+
void router.navigate({ href: `${path || '/'}${url.search}${url.hash}` });
|
|
63
|
+
}, [router, shell]);
|
|
64
|
+
const refresh = useCallback(async () => {
|
|
65
|
+
const res = await fetch(api, { headers: { accept: 'application/json' } }).catch(() => null);
|
|
66
|
+
if (!res)
|
|
67
|
+
return;
|
|
68
|
+
if (res.status === 404) {
|
|
69
|
+
setGone(true);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (!res.ok)
|
|
73
|
+
return;
|
|
74
|
+
const next = (await res.json());
|
|
75
|
+
const before = activeIds.current;
|
|
76
|
+
if (before) {
|
|
77
|
+
const ended = next.tasks.filter((t) => before.has(t.id) && !isActive(t));
|
|
78
|
+
for (const t of ended)
|
|
79
|
+
endedToast(t, open);
|
|
80
|
+
// A task's results are new data: drop the cache once (query-client.ts).
|
|
81
|
+
if (ended.length)
|
|
82
|
+
void qc.invalidateQueries();
|
|
83
|
+
}
|
|
84
|
+
activeIds.current = new Set(next.tasks.filter(isActive).map((t) => t.id));
|
|
85
|
+
setList(next);
|
|
86
|
+
}, [api, open, qc]);
|
|
87
|
+
const post = useCallback(async (path) => {
|
|
88
|
+
await fetch(`${api}${path}`, {
|
|
89
|
+
method: 'POST',
|
|
90
|
+
headers: { 'content-type': 'application/json' },
|
|
91
|
+
body: '{}',
|
|
92
|
+
});
|
|
93
|
+
}, [api]);
|
|
94
|
+
const cancel = useCallback(async (id) => {
|
|
95
|
+
await post(`/${encodeURIComponent(id)}/cancel`);
|
|
96
|
+
await refresh();
|
|
97
|
+
}, [post, refresh]);
|
|
98
|
+
// Gone from the list at once; the read after the POST confirms it.
|
|
99
|
+
const clear = useCallback(async (id) => {
|
|
100
|
+
setList((l) => {
|
|
101
|
+
if (!l)
|
|
102
|
+
return l;
|
|
103
|
+
const tasks = l.tasks.filter((t) => (id ? t.id !== id : isActive(t)));
|
|
104
|
+
const unseen = tasks.filter((t) => t.finished_at && !t.seen_at).length;
|
|
105
|
+
return { ...l, tasks, unseen };
|
|
106
|
+
});
|
|
107
|
+
await post(id ? `/${encodeURIComponent(id)}/clear` : '/clear');
|
|
108
|
+
await refresh();
|
|
109
|
+
}, [post, refresh]);
|
|
110
|
+
// Marked seen on the server at once; the list keeps its dots until the panel closes and reads again.
|
|
111
|
+
const seen = useCallback(async () => {
|
|
112
|
+
if (list?.unseen)
|
|
113
|
+
await post('/seen');
|
|
114
|
+
}, [list?.unseen, post]);
|
|
115
|
+
// First read, then again whenever the tab comes back.
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
void refresh();
|
|
118
|
+
const again = () => {
|
|
119
|
+
if (document.visibilityState === 'visible')
|
|
120
|
+
void refresh();
|
|
121
|
+
};
|
|
122
|
+
window.addEventListener('focus', again);
|
|
123
|
+
document.addEventListener('visibilitychange', again);
|
|
124
|
+
return () => {
|
|
125
|
+
window.removeEventListener('focus', again);
|
|
126
|
+
document.removeEventListener('visibilitychange', again);
|
|
127
|
+
};
|
|
128
|
+
}, [refresh]);
|
|
129
|
+
// Every 3 s only while something is queued or running and the tab is visible.
|
|
130
|
+
const active = (list?.active ?? 0) > 0;
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
if (!active)
|
|
133
|
+
return;
|
|
134
|
+
const id = window.setInterval(() => {
|
|
135
|
+
if (document.visibilityState === 'visible')
|
|
136
|
+
void refresh();
|
|
137
|
+
}, POLL_MS);
|
|
138
|
+
return () => window.clearInterval(id);
|
|
139
|
+
}, [active, refresh]);
|
|
140
|
+
const value = useMemo(() => (gone ? null : { list, refresh, cancel, seen, clear, open, apps }), [gone, list, refresh, cancel, seen, clear, open, apps]);
|
|
141
|
+
return _jsx(TasksCtx.Provider, { value: value, children: children });
|
|
142
|
+
}
|
|
143
|
+
function endedToast(t, open) {
|
|
144
|
+
const action = t.link ? { label: 'Open', onClick: () => open(t) } : undefined;
|
|
145
|
+
const [kind, ...rest] = t.title.split(' · ');
|
|
146
|
+
const detail = rest.join(' · ');
|
|
147
|
+
if (t.state === 'done') {
|
|
148
|
+
toast.success(`${kind} finished`, {
|
|
149
|
+
description: [detail, t.summary].filter(Boolean).join(' · ') || undefined,
|
|
150
|
+
action,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
else if (t.state === 'failed') {
|
|
154
|
+
toast.error(`${kind} failed`, {
|
|
155
|
+
description: [detail, t.error].filter(Boolean).join(' · ') || undefined,
|
|
156
|
+
action,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/** The header button: a ring while anything runs, the count, a gold dot for ended unseen tasks. */
|
|
161
|
+
export function TasksButton() {
|
|
162
|
+
const tasks = useTasks();
|
|
163
|
+
const mobile = useIsMobile();
|
|
164
|
+
const [open, setOpen] = useState(false);
|
|
165
|
+
const list = tasks?.list;
|
|
166
|
+
if (!tasks || !list || list.tasks.length === 0)
|
|
167
|
+
return null;
|
|
168
|
+
const onOpen = (o) => {
|
|
169
|
+
setOpen(o);
|
|
170
|
+
if (o)
|
|
171
|
+
void tasks.seen();
|
|
172
|
+
else
|
|
173
|
+
void tasks.refresh();
|
|
174
|
+
};
|
|
175
|
+
const running = list.tasks.filter((t) => t.state === 'running' && t.total);
|
|
176
|
+
const share = running.length
|
|
177
|
+
? running.reduce((s, t) => s + (t.done ?? 0) / (t.total || 1), 0) / running.length
|
|
178
|
+
: null;
|
|
179
|
+
const label = [
|
|
180
|
+
'Tasks',
|
|
181
|
+
list.active ? `${list.active} running` : null,
|
|
182
|
+
list.unseen ? `${list.unseen} new` : null,
|
|
183
|
+
]
|
|
184
|
+
.filter(Boolean)
|
|
185
|
+
.join(', ');
|
|
186
|
+
const trigger = (_jsxs(Button, { variant: "ghost", size: list.active ? 'sm' : 'icon-sm', className: cn('relative shrink-0', list.active > 0 && 'gap-1.5 px-2'), "aria-label": label, "data-slot": "tasks-button", children: [list.active > 0 ? _jsx(Ring, { share: share }) : _jsx(ListChecksIcon, {}), list.active > 0 && _jsx("span", { className: "font-mono text-xs tabular-nums", children: list.active }), list.unseen > 0 && (_jsx("span", { "aria-hidden": true, "data-slot": "tasks-unseen", className: "absolute top-1 right-0.5 size-1.5 rounded-full bg-edge" }))] }));
|
|
187
|
+
const body = _jsx(TaskPanel, { list: list, onClose: () => setOpen(false) });
|
|
188
|
+
if (mobile) {
|
|
189
|
+
return (_jsxs(Sheet, { open: open, onOpenChange: onOpen, children: [_jsx(SheetTrigger, { asChild: true, children: trigger }), _jsxs(SheetContent, { side: "bottom", className: "max-h-[75svh] gap-0 rounded-t-2xl p-0", onOpenAutoFocus: (e) => e.preventDefault(), children: [_jsxs(SheetHeader, { className: "flex-row items-baseline gap-2 border-b px-4 py-3", children: [_jsx(SheetTitle, { children: "Tasks" }), _jsx(SheetDescription, { children: "last 7 days" }), _jsxs("span", { className: "mr-8 ml-auto flex items-center gap-2 self-center", children: [list.active > 0 && (_jsxs("span", { className: "text-xs text-muted-foreground", children: [list.active, " running"] })), _jsx(ClearAll, { list: list })] })] }), body] })] }));
|
|
190
|
+
}
|
|
191
|
+
return (_jsxs(Popover, { open: open, onOpenChange: onOpen, children: [_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(PopoverTrigger, { asChild: true, children: trigger }) }), _jsx(TooltipContent, { children: "Tasks" })] }), _jsxs(PopoverContent, { align: "end", className: "w-100 gap-0 p-0", "aria-label": "Tasks", onOpenAutoFocus: (e) => e.preventDefault(), children: [_jsxs("div", { className: "flex items-baseline gap-2 px-3.5 pt-3 pb-2", children: [_jsx("span", { className: "text-sm font-semibold", children: "Tasks" }), _jsx("span", { className: "text-xs text-muted-foreground", children: "last 7 days" }), _jsxs("span", { className: "ml-auto flex items-center gap-2 self-center", children: [list.active > 0 && (_jsxs("span", { className: "text-xs text-muted-foreground", children: [list.active, " running"] })), _jsx(ClearAll, { list: list })] })] }), body] })] }));
|
|
192
|
+
}
|
|
193
|
+
/** Clears every ended task after one confirm; shows only when there is one. */
|
|
194
|
+
function ClearAll({ list }) {
|
|
195
|
+
const tasks = useTasks();
|
|
196
|
+
const [confirm, setConfirm] = useState(false);
|
|
197
|
+
const ended = list.tasks.length - list.active;
|
|
198
|
+
if (!ended)
|
|
199
|
+
return null;
|
|
200
|
+
return (_jsxs(_Fragment, { children: [_jsx(IconButton, { icon: BrushCleaningIcon, label: "Clear finished", size: "icon-xs", className: "text-muted-foreground hover:text-destructive", onClick: () => setConfirm(true) }), _jsx(LumenConfirm, { open: confirm, onOpenChange: setConfirm, title: `Clear ${ended} finished ${ended === 1 ? 'task' : 'tasks'}?`, description: "They leave this list for good. Running tasks stay.", action: "Clear", onConfirm: () => void tasks?.clear() })] }));
|
|
201
|
+
}
|
|
202
|
+
/** A small progress ring: the share done of what runs, or a spinning arc when nothing says. */
|
|
203
|
+
function Ring({ share }) {
|
|
204
|
+
const c = 2 * Math.PI * 7.5;
|
|
205
|
+
const arc = share === null ? c / 4 : Math.max(0.04, Math.min(1, share)) * c;
|
|
206
|
+
return (_jsxs("svg", { viewBox: "0 0 20 20", role: "img", "aria-label": "Progress", className: cn('size-4.5', share === null && 'animate-spin motion-reduce:animate-none'), children: [_jsx("circle", { cx: "10", cy: "10", r: "7.5", fill: "none", className: "stroke-muted", strokeWidth: "2.5" }), _jsx("circle", { cx: "10", cy: "10", r: "7.5", fill: "none", className: "stroke-primary", strokeWidth: "2.5", strokeLinecap: "round", strokeDasharray: `${arc} ${c}`, transform: "rotate(-90 10 10)" })] }));
|
|
207
|
+
}
|
|
208
|
+
function TaskPanel({ list, onClose }) {
|
|
209
|
+
const { openSettings } = useShell();
|
|
210
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "max-h-[60svh] overflow-y-auto", children: list.tasks.map((t) => (_jsx(TaskRow, { task: t, onClose: onClose }, t.id))) }), _jsxs("div", { className: "border-t px-3.5 py-2.5 text-xs text-muted-foreground", children: ["Phone or email when a task ends:", ' ', _jsx("button", { type: "button", className: "text-primary hover:underline", onClick: () => {
|
|
211
|
+
onClose();
|
|
212
|
+
openSettings('notifications');
|
|
213
|
+
}, children: "Settings \u25B8 Notifications" })] })] }));
|
|
214
|
+
}
|
|
215
|
+
function TaskRow({ task: t, onClose }) {
|
|
216
|
+
const tasks = useTasks();
|
|
217
|
+
const apps = tasks?.apps;
|
|
218
|
+
const [confirm, setConfirm] = useState(false);
|
|
219
|
+
const unseen = !!t.finished_at && !t.seen_at;
|
|
220
|
+
const now = useNow(isActive(t));
|
|
221
|
+
const done = t.done ?? 0;
|
|
222
|
+
const pct = t.total ? Math.round((done / t.total) * 100) : null;
|
|
223
|
+
return (_jsxs("div", { "data-slot": "task-row", "data-state": t.state, className: cn('flex flex-col gap-1.5 border-t px-3.5 py-2.5', unseen && 'bg-muted/40'), children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [unseen && _jsx("span", { "aria-hidden": true, className: "size-1.5 shrink-0 rounded-full bg-edge" }), apps && (_jsx(Badge, { variant: "outline", className: "shrink-0 text-[11px]", children: apps[t.app] ?? t.app })), _jsx("span", { className: "min-w-0 flex-1 truncate text-sm font-semibold", title: t.title, children: t.title }), isActive(t) && t.cancellable ? (_jsxs(_Fragment, { children: [_jsx(IconButton, { icon: XIcon, label: "Cancel task", size: "icon-xs", className: "text-muted-foreground hover:text-destructive", onClick: () => setConfirm(true) }), _jsx(LumenConfirm, { open: confirm, onOpenChange: setConfirm, title: `Cancel ${t.title}?`, description: "It stops at its next step; what it finished stays.", action: "Cancel task", onConfirm: () => void tasks?.cancel(t.id) })] })) : t.link ? (_jsx(IconButton, { icon: isActive(t) ? ChevronRightIcon : ArrowUpRightIcon, label: "Open", size: "icon-xs", onClick: () => {
|
|
224
|
+
onClose();
|
|
225
|
+
tasks?.open(t);
|
|
226
|
+
} })) : null, !isActive(t) && (_jsx(IconButton, { icon: XIcon, label: "Clear", size: "icon-xs", className: "text-muted-foreground hover:text-destructive", onClick: () => void tasks?.clear(t.id) }))] }), t.state === 'running' && t.total ? (_jsxs(_Fragment, { children: [_jsx(Progress, { value: pct ?? 0 }), _jsxs("div", { className: "flex gap-2.5 font-mono text-[11.5px] text-muted-foreground tabular-nums", children: [_jsxs("span", { children: [done.toLocaleString('en-US'), " / ", t.total.toLocaleString('en-US'), t.unit ? ` ${t.unit}` : ''] }), _jsxs("span", { children: [pct, "%"] }), _jsx("span", { className: "ml-auto", children: timeLeft(t, now) })] })] })) : t.state === 'running' ? (_jsxs(_Fragment, { children: [_jsx(Progress, {}), _jsxs("div", { className: "font-mono text-[11.5px] text-muted-foreground tabular-nums", children: [done ? `${done.toLocaleString('en-US')}${t.unit ? ` ${t.unit}` : ''} · ` : '', "running ", took(Date.parse(t.started_at ?? t.created_at), now)] })] })) : t.state === 'queued' ? (_jsx(Line, { icon: _jsx(Clock3Icon, { className: "size-3" }), children: "Queued" })) : t.state === 'done' ? (_jsx(Line, { icon: _jsx(CheckIcon, { className: "size-3 text-gain", strokeWidth: 2.5 }), mono: true, children: [t.summary, tookLine(t), ago(t.finished_at, now)].filter(Boolean).join(' · ') })) : (_jsx(Line, { icon: _jsx(CircleAlertIcon, { className: "size-3" }), className: t.state === 'failed' ? 'text-loss' : undefined, children: [t.state === 'cancelled' ? 'Cancelled' : (t.error ?? 'Failed'), ago(t.finished_at, now)]
|
|
227
|
+
.filter(Boolean)
|
|
228
|
+
.join(' · ') }))] }));
|
|
229
|
+
}
|
|
230
|
+
function Line({ icon, mono, className, children, }) {
|
|
231
|
+
return (_jsxs("div", { className: cn('flex min-w-0 items-center gap-1.5 text-[11.5px] text-muted-foreground', mono && 'font-mono tabular-nums', className), children: [_jsx("span", { className: "shrink-0", children: icon }), _jsx("span", { className: "min-w-0 truncate", children: children })] }));
|
|
232
|
+
}
|
|
233
|
+
/** Ticks once a second while a task runs, so "left" and "running" count. */
|
|
234
|
+
function useNow(live) {
|
|
235
|
+
const [now, setNow] = useState(() => Date.now());
|
|
236
|
+
useEffect(() => {
|
|
237
|
+
if (!live)
|
|
238
|
+
return;
|
|
239
|
+
const id = window.setInterval(() => setNow(Date.now()), 1000);
|
|
240
|
+
return () => window.clearInterval(id);
|
|
241
|
+
}, [live]);
|
|
242
|
+
return now;
|
|
243
|
+
}
|
|
244
|
+
function took(from, to) {
|
|
245
|
+
const s = Math.max(0, Math.round((to - from) / 1000));
|
|
246
|
+
if (s < 60)
|
|
247
|
+
return `${s} s`;
|
|
248
|
+
const m = Math.round(s / 60);
|
|
249
|
+
if (m < 60)
|
|
250
|
+
return `${m} min`;
|
|
251
|
+
const h = Math.floor(m / 60);
|
|
252
|
+
return m % 60 ? `${h} h ${m % 60} min` : `${h} h`;
|
|
253
|
+
}
|
|
254
|
+
function tookLine(t) {
|
|
255
|
+
if (!t.finished_at)
|
|
256
|
+
return null;
|
|
257
|
+
return `took ${took(Date.parse(t.started_at ?? t.created_at), Date.parse(t.finished_at))}`;
|
|
258
|
+
}
|
|
259
|
+
function ago(at, now) {
|
|
260
|
+
if (!at)
|
|
261
|
+
return null;
|
|
262
|
+
const s = Math.max(0, Math.round((now - Date.parse(at)) / 1000));
|
|
263
|
+
if (s < 60)
|
|
264
|
+
return 'just now';
|
|
265
|
+
if (s < 3600)
|
|
266
|
+
return `${Math.round(s / 60)} min ago`;
|
|
267
|
+
if (s < 86_400)
|
|
268
|
+
return `${Math.round(s / 3600)} h ago`;
|
|
269
|
+
return `${Math.round(s / 86_400)} d ago`;
|
|
270
|
+
}
|
|
271
|
+
/** What is left at the pace so far; nothing until a first step is done. */
|
|
272
|
+
function timeLeft(t, now) {
|
|
273
|
+
if (!t.total || !t.done || t.done >= t.total || !t.started_at)
|
|
274
|
+
return '';
|
|
275
|
+
const spent = now - Date.parse(t.started_at);
|
|
276
|
+
const left = (spent / t.done) * (t.total - t.done);
|
|
277
|
+
return `~${took(0, left)} left`;
|
|
278
|
+
}
|