@cosmoops/connect-sdk 0.7.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +662 -0
- package/README.md +376 -124
- package/dist/cjs/cli/doctor.js +3 -3
- package/dist/cjs/cli/init.js +3 -3
- package/dist/cjs/client/components/access-gate.js +59 -5
- package/dist/cjs/client/components/ai-format.js +135 -0
- package/dist/cjs/client/components/ai-widgets.js +232 -0
- package/dist/cjs/client/components/app-shell.js +80 -45
- package/dist/cjs/client/components/apps-grid.js +3 -2
- package/dist/cjs/client/components/ask-ai.js +353 -0
- package/dist/cjs/client/components/confirm-dialog.js +40 -0
- package/dist/cjs/client/components/cursor-pagination.js +22 -0
- package/dist/cjs/client/components/filter-tabs.js +24 -0
- package/dist/cjs/client/components/grievance-notice.js +30 -0
- package/dist/cjs/client/components/link-button.js +17 -10
- package/dist/cjs/client/components/logo.js +4 -3
- package/dist/cjs/client/components/nav.js +42 -1
- package/dist/cjs/client/components/plan-pricing.js +27 -0
- package/dist/cjs/client/components/plans.js +1 -1
- package/dist/cjs/client/components/platform-gate.js +48 -0
- package/dist/cjs/client/components/shell-context.js +149 -0
- package/dist/cjs/client/components/side-nav.js +50 -24
- package/dist/cjs/client/components/sidebar.js +127 -0
- package/dist/cjs/client/components/subscribe-dialog.js +2 -2
- package/dist/cjs/client/components/subscription.js +1 -1
- package/dist/cjs/client/components/user-menu.js +122 -40
- package/dist/cjs/client/components/workspace-switcher.js +100 -0
- package/dist/cjs/client/components/workspace-type-gate.js +48 -0
- package/dist/cjs/client/index.js +98 -3
- package/dist/cjs/client/provider.js +100 -2
- package/dist/cjs/client/use-checkout.js +9 -5
- package/dist/cjs/errors.js +1 -1
- package/dist/cjs/index.js +11 -1
- package/dist/cjs/next/index.js +2 -2
- package/dist/cjs/server/client.js +25 -2
- package/dist/cjs/server/handler.js +237 -16
- package/dist/cjs/server/http.js +52 -26
- package/dist/cjs/server/resources/acting-as.js +16 -0
- package/dist/cjs/server/resources/ai.js +15 -2
- package/dist/cjs/server/resources/data.js +374 -0
- package/dist/cjs/server/resources/entitlements.js +41 -0
- package/dist/cjs/server/resources/identity.js +9 -3
- package/dist/cjs/server/resources/kv.js +84 -0
- package/dist/cjs/server/resources/platform.js +43 -0
- package/dist/cjs/server/resources/sessions.js +1 -1
- package/dist/cjs/server/resources/workspaces.js +74 -0
- package/dist/cjs/store.js +152 -0
- package/dist/cjs/ui/empty-state.js +28 -0
- package/dist/cjs/ui/index.js +44 -0
- package/dist/cjs/ui/page-header.js +27 -0
- package/dist/cjs/ui/page-shell.js +44 -0
- package/dist/cjs/ui/section.js +20 -0
- package/dist/cjs/ui/setting-row.js +14 -0
- package/dist/cjs/ui/status-chip.js +17 -0
- package/dist/esm/cli/doctor.js +3 -3
- package/dist/esm/cli/init.js +3 -3
- package/dist/esm/client/components/access-gate.js +60 -6
- package/dist/esm/client/components/ai-format.js +129 -0
- package/dist/esm/client/components/ai-widgets.js +223 -0
- package/dist/esm/client/components/app-shell.js +85 -50
- package/dist/esm/client/components/apps-grid.js +3 -2
- package/dist/esm/client/components/ask-ai.js +350 -0
- package/dist/esm/client/components/confirm-dialog.js +37 -0
- package/dist/esm/client/components/cursor-pagination.js +19 -0
- package/dist/esm/client/components/filter-tabs.js +21 -0
- package/dist/esm/client/components/grievance-notice.js +27 -0
- package/dist/esm/client/components/link-button.js +17 -10
- package/dist/esm/client/components/logo.js +4 -3
- package/dist/esm/client/components/nav.js +41 -1
- package/dist/esm/client/components/plan-pricing.js +25 -0
- package/dist/esm/client/components/plans.js +2 -2
- package/dist/esm/client/components/platform-gate.js +45 -0
- package/dist/esm/client/components/shell-context.js +141 -0
- package/dist/esm/client/components/side-nav.js +49 -22
- package/dist/esm/client/components/sidebar.js +120 -0
- package/dist/esm/client/components/subscribe-dialog.js +3 -3
- package/dist/esm/client/components/subscription.js +2 -2
- package/dist/esm/client/components/user-menu.js +121 -41
- package/dist/esm/client/components/workspace-switcher.js +97 -0
- package/dist/esm/client/components/workspace-type-gate.js +45 -0
- package/dist/esm/client/index.js +58 -5
- package/dist/esm/client/provider.js +94 -2
- package/dist/esm/client/use-checkout.js +9 -5
- package/dist/esm/errors.js +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/next/index.js +2 -2
- package/dist/esm/server/client.js +25 -2
- package/dist/esm/server/handler.js +237 -16
- package/dist/esm/server/http.js +52 -26
- package/dist/esm/server/resources/acting-as.js +13 -0
- package/dist/esm/server/resources/ai.js +15 -2
- package/dist/esm/server/resources/data.js +369 -0
- package/dist/esm/server/resources/entitlements.js +41 -0
- package/dist/esm/server/resources/identity.js +9 -3
- package/dist/esm/server/resources/kv.js +80 -0
- package/dist/esm/server/resources/platform.js +39 -0
- package/dist/esm/server/resources/sessions.js +1 -1
- package/dist/esm/server/resources/workspaces.js +70 -0
- package/dist/esm/store.js +141 -0
- package/dist/esm/ui/empty-state.js +25 -0
- package/dist/esm/ui/index.js +34 -0
- package/dist/esm/ui/page-header.js +24 -0
- package/dist/esm/ui/page-shell.js +41 -0
- package/dist/esm/ui/section.js +17 -0
- package/dist/esm/ui/setting-row.js +11 -0
- package/dist/esm/ui/status-chip.js +14 -0
- package/dist/theme.css +87 -0
- package/dist/types/client/components/access-gate.d.ts +17 -3
- package/dist/types/client/components/ai-format.d.ts +24 -0
- package/dist/types/client/components/ai-widgets.d.ts +42 -0
- package/dist/types/client/components/app-shell.d.ts +85 -31
- package/dist/types/client/components/apps-grid.d.ts +11 -2
- package/dist/types/client/components/ask-ai.d.ts +73 -0
- package/dist/types/client/components/confirm-dialog.d.ts +34 -0
- package/dist/types/client/components/cursor-pagination.d.ts +27 -0
- package/dist/types/client/components/filter-tabs.d.ts +26 -0
- package/dist/types/client/components/grievance-notice.d.ts +31 -0
- package/dist/types/client/components/link-button.d.ts +32 -9
- package/dist/types/client/components/logo.d.ts +18 -2
- package/dist/types/client/components/nav.d.ts +27 -3
- package/dist/types/client/components/plan-pricing.d.ts +10 -0
- package/dist/types/client/components/platform-gate.d.ts +53 -0
- package/dist/types/client/components/shell-context.d.ts +116 -0
- package/dist/types/client/components/side-nav.d.ts +28 -14
- package/dist/types/client/components/sidebar.d.ts +95 -0
- package/dist/types/client/components/user-menu.d.ts +70 -23
- package/dist/types/client/components/workspace-switcher.d.ts +49 -0
- package/dist/types/client/components/workspace-type-gate.d.ts +48 -0
- package/dist/types/client/index.d.ts +36 -7
- package/dist/types/client/provider.d.ts +149 -3
- package/dist/types/client/use-checkout.d.ts +8 -4
- package/dist/types/errors.d.ts +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/next/index.d.ts +1 -1
- package/dist/types/server/client.d.ts +25 -2
- package/dist/types/server/http.d.ts +14 -3
- package/dist/types/server/index.d.ts +4 -0
- package/dist/types/server/resources/acting-as.d.ts +11 -0
- package/dist/types/server/resources/ai.d.ts +9 -1
- package/dist/types/server/resources/data.d.ts +258 -0
- package/dist/types/server/resources/entitlements.d.ts +33 -1
- package/dist/types/server/resources/kv.d.ts +63 -0
- package/dist/types/server/resources/platform.d.ts +32 -0
- package/dist/types/server/resources/workspaces.d.ts +55 -0
- package/dist/types/store.d.ts +96 -0
- package/dist/types/types.d.ts +491 -13
- package/dist/types/ui/empty-state.d.ts +34 -0
- package/dist/types/ui/index.d.ts +40 -0
- package/dist/types/ui/page-header.d.ts +34 -0
- package/dist/types/ui/page-shell.d.ts +39 -0
- package/dist/types/ui/section.d.ts +26 -0
- package/dist/types/ui/setting-row.d.ts +18 -0
- package/dist/types/ui/status-chip.d.ts +36 -0
- package/package.json +13 -3
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useMemo, useState } from "react";
|
|
4
|
+
import { Button } from "@heroui/react";
|
|
5
|
+
import { ArrowDown, ArrowUp, ChevronsUpDown, Download, ExternalLink, FileText, Minus, TrendingDown, TrendingUp } from "lucide-react";
|
|
6
|
+
import { AI_EMPTY_VALUE, AI_TONE_CHIP, AI_TONE_DOT, aiStatusTone, formatAiValue, isNumericAiFormat, } from "./ai-format.js";
|
|
7
|
+
/**
|
|
8
|
+
* What Ask AI draws.
|
|
9
|
+
*
|
|
10
|
+
* Seven widgets, one per data shape, rendered from the model's own tool call -
|
|
11
|
+
* see `AiToolName`. Everything here is HeroUI semantic tokens and inline SVG:
|
|
12
|
+
* **no charting library, no table library, no new dependency**, because this
|
|
13
|
+
* package ships to partners and a chart library is not a reasonable thing to
|
|
14
|
+
* make every application platform install for one panel.
|
|
15
|
+
*
|
|
16
|
+
* The hand-rolled chart is the deliberate part of that. A 400px side panel does
|
|
17
|
+
* not need pan, zoom, brushes or animation; it needs axis labels a human can
|
|
18
|
+
* read and a line that goes where the numbers do. That is ~80 lines of SVG.
|
|
19
|
+
*/
|
|
20
|
+
/** The four small panels share one frame, so an answer with two of them reads as one object. */
|
|
21
|
+
function Panel({ title, children }) {
|
|
22
|
+
return (_jsxs("figure", { className: "my-3 rounded-lg border border-border p-3", children: [title && _jsx("figcaption", { className: "mb-2 text-xs font-semibold", children: title }), children] }));
|
|
23
|
+
}
|
|
24
|
+
const TONE_TEXT = {
|
|
25
|
+
neutral: "text-muted",
|
|
26
|
+
positive: "text-success",
|
|
27
|
+
warning: "text-warning",
|
|
28
|
+
danger: "text-danger",
|
|
29
|
+
};
|
|
30
|
+
function StatusChip({ value }) {
|
|
31
|
+
return (_jsx("span", { className: `inline-block rounded-full px-1.5 py-0.5 text-[10px] font-medium ${AI_TONE_CHIP[aiStatusTone(value)]}`, children: String(value) }));
|
|
32
|
+
}
|
|
33
|
+
/* ------------------------------------------------------------------ table */
|
|
34
|
+
/**
|
|
35
|
+
* `showTable`. Sorting and CSV are what a table is *for* - once an answer is
|
|
36
|
+
* tabular the next thing anyone wants is to reorder it or take it elsewhere,
|
|
37
|
+
* and a markdown table can do neither. Both work off the raw values, so
|
|
38
|
+
* "₹12,500" sorts as 12500 and the export carries numbers a spreadsheet can add.
|
|
39
|
+
*/
|
|
40
|
+
export function AiTable({ title, columns, rows, caption }) {
|
|
41
|
+
// By column *index*: rows are positional (see `AiTableInput`), so the index
|
|
42
|
+
// is the only identity a column has.
|
|
43
|
+
const [sort, setSort] = useState(null);
|
|
44
|
+
const sorted = useMemo(() => {
|
|
45
|
+
if (!sort)
|
|
46
|
+
return rows;
|
|
47
|
+
const direction = sort.direction === "asc" ? 1 : -1;
|
|
48
|
+
return [...rows].sort((a, b) => {
|
|
49
|
+
const left = a[sort.column];
|
|
50
|
+
const right = b[sort.column];
|
|
51
|
+
// Missing values sink in both directions. The alternative is empty rows
|
|
52
|
+
// migrating to the top when you reverse a column, which reads as data
|
|
53
|
+
// appearing out of nowhere.
|
|
54
|
+
if (left === null || left === undefined)
|
|
55
|
+
return 1;
|
|
56
|
+
if (right === null || right === undefined)
|
|
57
|
+
return -1;
|
|
58
|
+
if (typeof left === "number" && typeof right === "number")
|
|
59
|
+
return (left - right) * direction;
|
|
60
|
+
return String(left).localeCompare(String(right)) * direction;
|
|
61
|
+
});
|
|
62
|
+
}, [rows, sort]);
|
|
63
|
+
const downloadCsv = () => {
|
|
64
|
+
const escape = (value) => `"${String(value ?? "").replaceAll('"', '""')}"`;
|
|
65
|
+
const csv = [
|
|
66
|
+
columns.map((column) => escape(column.label)).join(","),
|
|
67
|
+
...sorted.map((row) => columns.map((_column, index) => escape(row[index])).join(",")),
|
|
68
|
+
].join("\n");
|
|
69
|
+
const url = URL.createObjectURL(new Blob([csv], { type: "text/csv;charset=utf-8" }));
|
|
70
|
+
const link = document.createElement("a");
|
|
71
|
+
link.href = url;
|
|
72
|
+
link.download = `${(title ?? "table").toLowerCase().replaceAll(/[^a-z0-9]+/g, "-")}.csv`;
|
|
73
|
+
link.click();
|
|
74
|
+
URL.revokeObjectURL(url);
|
|
75
|
+
};
|
|
76
|
+
const toggleSort = (column) => setSort((current) => current?.column === column
|
|
77
|
+
? current.direction === "asc"
|
|
78
|
+
? { column, direction: "desc" }
|
|
79
|
+
: null
|
|
80
|
+
: { column, direction: "asc" });
|
|
81
|
+
return (_jsxs("figure", { className: "my-3 overflow-hidden rounded-lg border border-border", children: [_jsxs("div", { className: "flex items-center gap-2 border-b border-border bg-surface-secondary px-3 py-2", children: [_jsx("figcaption", { className: "flex-1 truncate text-xs font-semibold", children: title ?? "Results" }), _jsxs("span", { className: "text-[11px] text-muted", children: [rows.length, " rows"] }), _jsx(Button, { isIconOnly: true, size: "sm", variant: "tertiary", "aria-label": "Download as CSV", onPress: downloadCsv, children: _jsx(Download, { className: "size-3.5" }) })] }), _jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-xs", children: [_jsx("thead", { children: _jsx("tr", { className: "border-b border-border", children: columns.map((column, columnIndex) => {
|
|
82
|
+
const isSorted = sort?.column === columnIndex;
|
|
83
|
+
const Icon = !isSorted ? ChevronsUpDown : sort.direction === "asc" ? ArrowUp : ArrowDown;
|
|
84
|
+
return (_jsx("th", { scope: "col", className: "p-0", children: _jsxs("button", { type: "button", onClick: () => toggleSort(columnIndex), "aria-label": `Sort by ${column.label}`, className: `flex w-full items-center gap-1 px-2.5 py-1.5 font-medium whitespace-nowrap hover:bg-surface-secondary ${isNumericAiFormat(column.format) ? "justify-end" : "justify-start"}`, children: [column.label, _jsx(Icon, { className: `size-3 shrink-0 ${isSorted ? "text-foreground" : "text-muted/60"}` })] }) }, columnIndex));
|
|
85
|
+
}) }) }), _jsxs("tbody", { children: [sorted.map((row, index) => (_jsx("tr", { className: "border-b border-border/60 last:border-b-0", children: columns.map((column, columnIndex) => {
|
|
86
|
+
const value = row[columnIndex] ?? null;
|
|
87
|
+
return (_jsx("td", { className: `px-2.5 py-1.5 align-top ${isNumericAiFormat(column.format) ? "text-end tabular-nums" : "text-start"}`, children: column.format === "status" && value !== null ? (_jsx(StatusChip, { value: value })) : (_jsx("span", { className: value === null ? "text-muted" : undefined, children: formatAiValue(value, column.format) })) }, columnIndex));
|
|
88
|
+
}) }, index))), sorted.length === 0 && (_jsx("tr", { children: _jsx("td", { colSpan: columns.length, className: "px-2.5 py-3 text-center text-muted", children: AI_EMPTY_VALUE }) }))] })] }) }), caption && _jsx("p", { className: "border-t border-border px-3 py-1.5 text-[11px] text-muted", children: caption })] }));
|
|
89
|
+
}
|
|
90
|
+
/* ------------------------------------------------------------------ chart */
|
|
91
|
+
/**
|
|
92
|
+
* The theme's categorical ramp. Series identity is carried by the legend as
|
|
93
|
+
* well as the hue, never by hue alone.
|
|
94
|
+
*/
|
|
95
|
+
const SERIES_COLORS = [
|
|
96
|
+
"var(--chart-cat-1, var(--accent))",
|
|
97
|
+
"var(--chart-cat-2, var(--warning))",
|
|
98
|
+
"var(--chart-cat-3, var(--success))",
|
|
99
|
+
"var(--chart-cat-4, var(--danger))",
|
|
100
|
+
"var(--chart-cat-5, var(--muted))",
|
|
101
|
+
];
|
|
102
|
+
const VIEW_W = 320;
|
|
103
|
+
const VIEW_H = 140;
|
|
104
|
+
const PAD = { top: 8, right: 8, bottom: 20, left: 44 };
|
|
105
|
+
/**
|
|
106
|
+
* `showChart`. Cartesian kinds are drawn as SVG against a fixed viewBox and
|
|
107
|
+
* scaled by the browser, which is why there is no resize observer here: the
|
|
108
|
+
* chart is resolution-independent, so "responsive" costs nothing at runtime.
|
|
109
|
+
*
|
|
110
|
+
* `pie` is deliberately *not* a pie. In a 400px column a pie plus its legend
|
|
111
|
+
* is smaller than the labels it needs, and reading angles is worse than
|
|
112
|
+
* reading lengths at any size - so proportions render as a labelled bar list,
|
|
113
|
+
* which answers the same question and stays legible.
|
|
114
|
+
*/
|
|
115
|
+
export function AiChart({ title, kind, xLabel, yLabel, valueFormat, series, caption }) {
|
|
116
|
+
const format = (value) => formatAiValue(value, valueFormat);
|
|
117
|
+
const axis = series[0]?.points.map((point) => point.x) ?? [];
|
|
118
|
+
// Which x the pointer is nearest. An SVG chart is an interactive chart:
|
|
119
|
+
// without this the exact values are unreadable, because a 140px-tall plot
|
|
120
|
+
// can only carry three axis labels.
|
|
121
|
+
const [hovered, setHovered] = useState(null);
|
|
122
|
+
if (kind === "pie") {
|
|
123
|
+
const slices = series[0]?.points ?? [];
|
|
124
|
+
// Guards a divide-by-zero on an all-zero set, which would otherwise make
|
|
125
|
+
// every bar `NaN%` wide.
|
|
126
|
+
const total = slices.reduce((sum, point) => sum + (point.y ?? 0), 0) || 1;
|
|
127
|
+
return (_jsxs(Panel, { title: title, children: [_jsx("ul", { className: "space-y-1.5", children: slices.map((slice, index) => (_jsxs("li", { className: "text-xs", children: [_jsxs("div", { className: "flex items-baseline justify-between gap-2", children: [_jsx("span", { className: "truncate", children: slice.x }), _jsxs("span", { className: "shrink-0 tabular-nums text-muted", children: [format(slice.y ?? 0), " \u00B7 ", Math.round(((slice.y ?? 0) / total) * 100), "%"] })] }), _jsx("div", { className: "mt-0.5 h-1.5 overflow-hidden rounded-full bg-surface-tertiary", children: _jsx("div", { className: "h-full rounded-full", style: {
|
|
128
|
+
width: `${((slice.y ?? 0) / total) * 100}%`,
|
|
129
|
+
backgroundColor: SERIES_COLORS[index % SERIES_COLORS.length],
|
|
130
|
+
} }) })] }, slice.x))) }), caption && _jsx("p", { className: "mt-2 text-[11px] text-muted", children: caption })] }));
|
|
131
|
+
}
|
|
132
|
+
const values = series.flatMap((entry) => entry.points.map((point) => point.y)).filter((y) => y !== null);
|
|
133
|
+
// A flat series would give a zero-height range and divide by zero; padding
|
|
134
|
+
// the top by 1 draws it as a line across the middle instead of NaN.
|
|
135
|
+
const max = values.length ? Math.max(...values, 0) : 1;
|
|
136
|
+
const min = values.length ? Math.min(...values, 0) : 0;
|
|
137
|
+
const range = max - min || 1;
|
|
138
|
+
const plotW = VIEW_W - PAD.left - PAD.right;
|
|
139
|
+
const plotH = VIEW_H - PAD.top - PAD.bottom;
|
|
140
|
+
const xAt = (index) => PAD.left + (axis.length <= 1 ? plotW / 2 : (index / (axis.length - 1)) * plotW);
|
|
141
|
+
const yAt = (value) => PAD.top + plotH - ((value - min) / range) * plotH;
|
|
142
|
+
/** Ticks at the extremes and the midpoint - three labels is all a 140px-tall plot can carry. */
|
|
143
|
+
const yTicks = [min, min + range / 2, max];
|
|
144
|
+
// First, middle and last x label only, for the same reason.
|
|
145
|
+
const xTicks = axis.length <= 3 ? axis.map((_, i) => i) : [0, Math.floor((axis.length - 1) / 2), axis.length - 1];
|
|
146
|
+
return (_jsxs(Panel, { title: title, children: [_jsxs("div", { className: "relative", children: [hovered !== null && (
|
|
147
|
+
// Positioned as a percentage of the plot so it tracks the mark at any
|
|
148
|
+
// rendered width - the SVG scales, and this has to scale with it.
|
|
149
|
+
_jsxs("div", { className: "pointer-events-none absolute top-0 z-10 -translate-x-1/2 rounded-md border border-border bg-overlay px-2 py-1 text-[10px] shadow-lg", style: { left: `${(xAt(hovered) / VIEW_W) * 100}%` }, children: [_jsx("p", { className: "font-medium", children: axis[hovered] }), series.map((entry, index) => {
|
|
150
|
+
const value = entry.points[hovered]?.y;
|
|
151
|
+
return (_jsxs("p", { className: "flex items-center gap-1 text-muted", children: [_jsx("span", { className: "size-1.5 shrink-0 rounded-full", style: { backgroundColor: SERIES_COLORS[index % SERIES_COLORS.length] }, "aria-hidden": true }), _jsx("span", { className: "tabular-nums", children: value === null || value === undefined ? AI_EMPTY_VALUE : format(value) })] }, entry.name));
|
|
152
|
+
})] })), _jsxs("svg", { viewBox: `0 0 ${VIEW_W} ${VIEW_H}`, className: "w-full", role: "img", "aria-label": title ?? "Chart", onMouseLeave: () => setHovered(null), children: [yTicks.map((tick) => (_jsxs("g", { children: [_jsx("line", { x1: PAD.left, x2: VIEW_W - PAD.right, y1: yAt(tick), y2: yAt(tick), stroke: "var(--separator)", strokeDasharray: "3 3" }), _jsx("text", { x: PAD.left - 4, y: yAt(tick) + 3, textAnchor: "end", fontSize: "7", fill: "var(--muted)", children: format(tick) })] }, tick))), xTicks.map((index) => (_jsx("text", { x: xAt(index), y: VIEW_H - 6, textAnchor: "middle", fontSize: "7", fill: "var(--muted)", children: axis[index] }, index))), series.map((entry, seriesIndex) => {
|
|
153
|
+
const color = SERIES_COLORS[seriesIndex % SERIES_COLORS.length];
|
|
154
|
+
if (kind === "bar") {
|
|
155
|
+
const slot = plotW / Math.max(axis.length, 1);
|
|
156
|
+
// A 2px surface gap between adjacent series, so two bars in one
|
|
157
|
+
// slot read as two marks rather than one two-toned block.
|
|
158
|
+
const width = Math.max(2, (slot * 0.7) / series.length - (series.length > 1 ? 2 : 0));
|
|
159
|
+
return entry.points.map((point, index) => point.y === null ? null : (_jsx("rect", { x: xAt(index) - (width * series.length) / 2 + seriesIndex * width, y: Math.min(yAt(point.y), yAt(0)), width: width, height: Math.max(1, Math.abs(yAt(point.y) - yAt(0))), fill: color, rx: "3" }, `${entry.name}-${point.x}`)));
|
|
160
|
+
}
|
|
161
|
+
// `null` breaks the path into separate segments rather than drawing
|
|
162
|
+
// a straight line through a gap that has no data in it.
|
|
163
|
+
const segments = [];
|
|
164
|
+
let current = [];
|
|
165
|
+
entry.points.forEach((point, index) => {
|
|
166
|
+
if (point.y === null) {
|
|
167
|
+
if (current.length)
|
|
168
|
+
segments.push(current.join(" "));
|
|
169
|
+
current = [];
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
current.push(`${current.length ? "L" : "M"}${xAt(index)},${yAt(point.y)}`);
|
|
173
|
+
});
|
|
174
|
+
if (current.length)
|
|
175
|
+
segments.push(current.join(" "));
|
|
176
|
+
return (_jsxs("g", { children: [kind === "area" && segments.length === 1 && (_jsx("path", { d: `${segments[0]} L${xAt(entry.points.length - 1)},${yAt(min)} L${xAt(0)},${yAt(min)} Z`, fill: color, fillOpacity: "0.15" })), segments.map((segment) => (_jsx("path", { d: segment, fill: "none", stroke: color, strokeWidth: "1.5", strokeLinejoin: "round" }, segment)))] }, entry.name));
|
|
177
|
+
}), axis.map((label, index) => (_jsx("rect", { x: xAt(index) - plotW / Math.max(axis.length, 1) / 2, y: PAD.top, width: plotW / Math.max(axis.length, 1), height: plotH, fill: "transparent", onMouseEnter: () => setHovered(index) }, label))), hovered !== null && (_jsx("line", { x1: xAt(hovered), x2: xAt(hovered), y1: PAD.top, y2: PAD.top + plotH, stroke: "var(--border)", strokeWidth: "1" }))] })] }), (series.length > 1 || xLabel || yLabel) && (_jsxs("div", { className: "mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-[10px] text-muted", children: [series.length > 1 &&
|
|
178
|
+
series.map((entry, index) => (_jsxs("span", { className: "inline-flex items-center gap-1", children: [_jsx("span", { className: "size-2 rounded-full", style: { backgroundColor: SERIES_COLORS[index % SERIES_COLORS.length] }, "aria-hidden": true }), entry.name] }, entry.name))), yLabel && _jsxs("span", { children: ["y: ", yLabel] }), xLabel && _jsxs("span", { children: ["x: ", xLabel] })] })), caption && _jsx("p", { className: "mt-2 text-[11px] text-muted", children: caption })] }));
|
|
179
|
+
}
|
|
180
|
+
/* ----------------------------------------------------------------- panels */
|
|
181
|
+
/**
|
|
182
|
+
* `showMetrics`. A delta is coloured by `deltaMeaning`, not by its sign -
|
|
183
|
+
* churn rising is bad and revenue rising is good, and painting both green
|
|
184
|
+
* because the arrow points up is exactly the confidently-wrong signal a
|
|
185
|
+
* dashboard must never send. The arrow still shows direction, so meaning and
|
|
186
|
+
* direction stay two separate channels.
|
|
187
|
+
*/
|
|
188
|
+
export function AiMetrics({ title, metrics }) {
|
|
189
|
+
return (_jsx(Panel, { title: title, children: _jsx("div", { className: "grid grid-cols-2 gap-2", children: metrics.map((metric) => {
|
|
190
|
+
const delta = metric.delta ?? null;
|
|
191
|
+
const rising = delta !== null && delta > 0;
|
|
192
|
+
const Arrow = delta === null || delta === 0 ? Minus : rising ? TrendingUp : TrendingDown;
|
|
193
|
+
const good = metric.deltaMeaning === "neutral" || delta === null || delta === 0
|
|
194
|
+
? null
|
|
195
|
+
: metric.deltaMeaning === "up-is-good"
|
|
196
|
+
? rising
|
|
197
|
+
: !rising;
|
|
198
|
+
return (_jsxs("div", { className: "rounded-md bg-surface-secondary p-2.5", children: [_jsx("p", { className: "truncate text-[11px] text-muted", children: metric.label }), _jsx("p", { className: "mt-0.5 text-base font-semibold tabular-nums", children: formatAiValue(metric.value, metric.format) }), delta !== null && (_jsxs("p", { className: `mt-0.5 flex items-center gap-0.5 text-[11px] tabular-nums ${good === null ? "text-muted" : good ? "text-success" : "text-danger"}`, children: [_jsx(Arrow, { className: "size-3", "aria-hidden": true }), formatAiValue(Math.abs(delta), "percent")] })), metric.hint && _jsx("p", { className: "mt-0.5 text-[10px] text-muted", children: metric.hint })] }, metric.label));
|
|
199
|
+
}) }) }));
|
|
200
|
+
}
|
|
201
|
+
/** `showTimeline`: dated events, oldest first, on a single rail. */
|
|
202
|
+
export function AiTimeline({ title, events }) {
|
|
203
|
+
return (_jsx(Panel, { title: title, children: _jsxs("ol", { className: "relative space-y-3 ps-4", children: [_jsx("span", { className: "absolute inset-y-1 start-1 w-px bg-border", "aria-hidden": true }), events.map((event, index) => (_jsxs("li", { className: "relative", children: [_jsx("span", { className: `absolute -start-4 top-1 size-2 rounded-full ring-2 ring-surface ${AI_TONE_DOT[event.tone]}`, "aria-hidden": true }), _jsx("p", { className: "text-[11px] text-muted", children: formatAiValue(event.at, "datetime") }), _jsx("p", { className: "text-xs font-medium", children: event.title }), event.detail && _jsx("p", { className: "text-xs text-muted", children: event.detail })] }, `${event.at}-${index}`)))] }) }));
|
|
204
|
+
}
|
|
205
|
+
/** `showDetails`: one entity's fields, as a definition list rather than a one-row table. */
|
|
206
|
+
export function AiDetails({ title, fields }) {
|
|
207
|
+
return (_jsx(Panel, { title: title, children: _jsx("dl", { className: "divide-y divide-border/60", children: fields.map((field) => (_jsxs("div", { className: "flex gap-3 py-1.5 first:pt-0 last:pb-0", children: [_jsx("dt", { className: "w-2/5 shrink-0 text-xs text-muted", children: field.label }), _jsx("dd", { className: `min-w-0 flex-1 text-xs break-words ${TONE_TEXT[field.tone]}`, children: field.format === "status" && field.value !== null ? (_jsx(StatusChip, { value: field.value })) : (formatAiValue(field.value, field.format)) })] }, field.label))) }) }));
|
|
208
|
+
}
|
|
209
|
+
/** `showSteps`: an ordered procedure. `renderDetail` is how the panel feeds each step back through its markdown renderer. */
|
|
210
|
+
export function AiSteps({ title, steps, renderDetail, }) {
|
|
211
|
+
return (_jsx(Panel, { title: title, children: _jsx("ol", { className: "space-y-2.5", children: steps.map((step, index) => (_jsxs("li", { className: "flex gap-2.5", children: [_jsx("span", { className: "mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full bg-accent/12 text-[10px] font-semibold text-accent", children: index + 1 }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("p", { className: "text-xs font-medium", children: step.title }), step.detail && (_jsx("div", { className: "mt-0.5 text-xs text-muted", children: renderDetail ? renderDetail(step.detail) : _jsx("span", { className: "whitespace-pre-wrap", children: step.detail }) }))] })] }, step.title))) }) }));
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* `showSources`: what a grounded answer actually rested on. The point of a
|
|
215
|
+
* citation is that it can be opened, so a source with a `uri` is a link and one
|
|
216
|
+
* without is still a named row - never a marker with nothing behind it.
|
|
217
|
+
*/
|
|
218
|
+
export function AiSources({ sources }) {
|
|
219
|
+
return (_jsxs("figure", { className: "my-3 rounded-lg border border-border bg-surface-secondary p-2.5", children: [_jsx("figcaption", { className: "mb-1.5 text-[11px] font-semibold tracking-wide text-muted uppercase", children: "Sources" }), _jsx("ul", { className: "space-y-1.5", children: sources.map((source, index) => {
|
|
220
|
+
const label = (_jsxs(_Fragment, { children: [_jsx(FileText, { className: "mt-0.5 size-3 shrink-0 text-muted", "aria-hidden": true }), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block truncate text-xs font-medium", children: source.title }), source.snippet && _jsxs("span", { className: "mt-0.5 block text-[11px] text-muted italic", children: ["\u201C", source.snippet, "\u201D"] })] })] }));
|
|
221
|
+
return (_jsx("li", { children: source.uri ? (_jsxs("a", { href: source.uri, target: "_blank", rel: "noopener noreferrer", className: "flex gap-1.5 rounded-sm hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus", children: [label, _jsx(ExternalLink, { className: "mt-0.5 size-3 shrink-0 text-muted", "aria-hidden": true })] })) : (_jsx("span", { className: "flex gap-1.5", children: label })) }, `${source.uri ?? source.title}-${index}`));
|
|
222
|
+
}) })] }));
|
|
223
|
+
}
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
-
import { useCallback, useState, useSyncExternalStore } from "react";
|
|
4
|
-
import { Button, Drawer
|
|
5
|
-
import { Menu
|
|
3
|
+
import { useCallback, useMemo, useState, useSyncExternalStore } from "react";
|
|
4
|
+
import { Button, Drawer } from "@heroui/react";
|
|
5
|
+
import { Menu } from "lucide-react";
|
|
6
6
|
import { useConnect } from "../provider.js";
|
|
7
7
|
import { ConnectAccessGate } from "./access-gate.js";
|
|
8
8
|
import { ConnectAppsGrid } from "./apps-grid.js";
|
|
9
|
+
import { ConnectAskAi } from "./ask-ai.js";
|
|
9
10
|
import { ConnectLoginButton, ConnectRegisterButton } from "./auth-buttons.js";
|
|
10
11
|
import { ConnectLogo } from "./logo.js";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
12
|
+
import { MotionProvider } from "./motion.js";
|
|
13
|
+
import { ConnectSidebar, ConnectSidebarBody, ConnectSidebarFooter, ConnectSidebarHeader, } from "./sidebar.js";
|
|
14
|
+
import { ConnectSidebarNav } from "./side-nav.js";
|
|
15
|
+
import { ConnectShellProvider, defaultRenderLink } from "./shell-context.js";
|
|
16
|
+
import { ConnectWorkspaceSwitcher } from "./workspace-switcher.js";
|
|
17
|
+
import { ConnectUserButton } from "./user-menu.js";
|
|
18
|
+
import { filterNavForAudience, gateExemptPaths, normalizePath, } from "./nav.js";
|
|
14
19
|
import { useActivePath } from "../use-active-path.js";
|
|
15
20
|
const COLLAPSED_KEY = "connect_sidenav_collapsed";
|
|
16
21
|
const COLLAPSED_EVENT = "connect:sidenav-collapsed";
|
|
@@ -33,7 +38,7 @@ function collapsedSnapshot() {
|
|
|
33
38
|
/**
|
|
34
39
|
* No preference on the server, which is the point: `useSyncExternalStore` uses
|
|
35
40
|
* this for the hydrating render and only then swaps in the real reading, so a
|
|
36
|
-
* returning user's
|
|
41
|
+
* returning user's rail state can never make the server's HTML and the
|
|
37
42
|
* client's first pass disagree. Reading storage during render instead is a
|
|
38
43
|
* hydration mismatch waiting for its first user.
|
|
39
44
|
*/
|
|
@@ -51,65 +56,73 @@ function subscribeCollapsed(onStoreChange) {
|
|
|
51
56
|
};
|
|
52
57
|
}
|
|
53
58
|
/**
|
|
54
|
-
* The
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return renderLink({
|
|
58
|
-
href: homeHref,
|
|
59
|
-
className: `flex items-center rounded-lg focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus ${collapsed ? "justify-center" : ""}`,
|
|
60
|
-
// `ConnectLogo` names itself, but it does it with `role="img"` on a
|
|
61
|
-
// `<span>`, which the `<a>` wrapping it does not inherit as its own name.
|
|
62
|
-
"aria-label": `${name} home`,
|
|
63
|
-
children: _jsx(ConnectLogo, { name: name, icon: icon, collapsed: collapsed }),
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* The application frame every CosmoOps application platform runs inside: a
|
|
68
|
-
* collapsible sidebar, a top bar carrying the app launcher and the account
|
|
69
|
-
* menu, and your pages in the middle.
|
|
59
|
+
* The application frame every CosmoOps application platform runs inside: one
|
|
60
|
+
* rail down the side holding the brand, the workspace, the navigation and the
|
|
61
|
+
* account, and your pages in the rest.
|
|
70
62
|
*
|
|
71
63
|
* This is the piece each partner was otherwise writing by hand, and writing
|
|
72
|
-
* slightly differently - which is how a family of applications stops looking
|
|
73
|
-
* one product. Everything that should be identical across platforms
|
|
74
|
-
*
|
|
75
|
-
* account menu, the access gate) lives here; everything that genuinely
|
|
76
|
-
* differs is a prop:
|
|
64
|
+
* slightly differently - which is how a family of applications stops looking
|
|
65
|
+
* like one product. Everything that should be identical across platforms
|
|
66
|
+
* lives here; everything that genuinely differs is a prop:
|
|
77
67
|
*
|
|
78
68
|
* ```tsx
|
|
79
69
|
* "use client";
|
|
80
70
|
* import Link from "next/link";
|
|
81
71
|
* import { usePathname } from "next/navigation";
|
|
82
|
-
* import {
|
|
83
|
-
* import { LayoutDashboard, Truck } from "lucide-react";
|
|
72
|
+
* import { AppShell } from "@cosmoops/connect-sdk/client";
|
|
73
|
+
* import { LayoutDashboard, Settings, Truck } from "lucide-react";
|
|
84
74
|
*
|
|
85
75
|
* export function Shell({ children }: { children: React.ReactNode }) {
|
|
86
76
|
* return (
|
|
87
|
-
* <
|
|
77
|
+
* <AppShell
|
|
88
78
|
* name="Dispatch"
|
|
89
79
|
* icon={<Truck />}
|
|
90
80
|
* activePath={usePathname()}
|
|
91
81
|
* renderLink={(props) => <Link {...props} />}
|
|
92
82
|
* nav={[
|
|
93
83
|
* { label: "Overview", href: "/", icon: <LayoutDashboard className="size-5" /> },
|
|
94
|
-
* { label: "
|
|
84
|
+
* { label: "Shipments", href: "/shipments", icon: <Truck className="size-5" /> },
|
|
85
|
+
* ]}
|
|
86
|
+
* secondaryNav={[
|
|
87
|
+
* { label: "Settings", href: "/settings", icon: <Settings className="size-5" /> },
|
|
95
88
|
* ]}
|
|
96
89
|
* >
|
|
97
90
|
* {children}
|
|
98
|
-
* </
|
|
91
|
+
* </AppShell>
|
|
99
92
|
* );
|
|
100
93
|
* }
|
|
101
94
|
* ```
|
|
102
95
|
*
|
|
96
|
+
* **There is no top bar.** There used to be one, carrying a page title, the
|
|
97
|
+
* launcher and the account menu - and it was a second, half-width strip of
|
|
98
|
+
* chrome saying things the rail was better placed to say. The launcher and
|
|
99
|
+
* the account row moved into the rail's footer, where they are one press from
|
|
100
|
+
* wherever the pointer already is; the page title moved into the page, where
|
|
101
|
+
* `ConnectPageShell` renders it next to that page's own actions and
|
|
102
|
+
* breadcrumbs. What is left is a single column of chrome and a full-height
|
|
103
|
+
* content pane, which is what every application this shell is modelled on
|
|
104
|
+
* settled at.
|
|
105
|
+
*
|
|
106
|
+
* The one exception is below `md`, where a slim bar carries the drawer
|
|
107
|
+
* trigger. A phone has no rail to put it in, and a navigation you cannot open
|
|
108
|
+
* is not a smaller navigation.
|
|
109
|
+
*
|
|
110
|
+
* **The rail starts collapsed.** `defaultCollapsed` is `true`: the icon rail
|
|
111
|
+
* is the resting state, and expanding is the deliberate act. That is the right
|
|
112
|
+
* default because the rail's job is to be *there*, not to be read - a person
|
|
113
|
+
* learns five glyphs in a day and then wants the 200px back for their data.
|
|
114
|
+
* A stored preference from the user's last visit always wins over it.
|
|
115
|
+
*
|
|
103
116
|
* There is no `isSignedIn` branch anywhere in here, and there should be none
|
|
104
|
-
* in yours: the auth buttons render nothing once a session exists, the
|
|
105
|
-
*
|
|
117
|
+
* in yours: the auth buttons render nothing once a session exists, the account
|
|
118
|
+
* row renders nothing without one, and the launcher renders nothing when
|
|
106
119
|
* signed out. The swap happens inside the components.
|
|
107
120
|
*
|
|
108
121
|
* It renders no provider of its own - mount `ConnectProvider` above it, once,
|
|
109
122
|
* so a page with a shell, a plan table and a usage meter still makes one
|
|
110
123
|
* session read.
|
|
111
124
|
*/
|
|
112
|
-
export function
|
|
125
|
+
export function AppShell({ name, icon, homeHref = "/", nav, secondaryNav, activePath, renderLink = defaultRenderLink, sidebarFooter, workspaceSwitcher, footer, defaultCollapsed = true, collapsed: controlledCollapsed, onCollapsedChange, collapsible = true, showAppsGrid = true, showAuthButtons = true, theme, onThemeChange, gate = true, gateExemptRoutes, askAi = true, className, contentClassName, children, }) {
|
|
113
126
|
const { session, isLoading } = useConnect();
|
|
114
127
|
const pathname = useActivePath(activePath);
|
|
115
128
|
const [mobileOpen, setMobileOpen] = useState(false);
|
|
@@ -119,8 +132,7 @@ export function ConnectAppShell({ name, icon, homeHref = "/", nav, activePath, r
|
|
|
119
132
|
const stored = useSyncExternalStore(subscribeCollapsed, collapsedSnapshot, serverCollapsedSnapshot);
|
|
120
133
|
const isControlled = controlledCollapsed !== undefined;
|
|
121
134
|
const collapsed = collapsible ? (isControlled ? controlledCollapsed : (stored ?? defaultCollapsed)) : false;
|
|
122
|
-
const
|
|
123
|
-
const next = !collapsed;
|
|
135
|
+
const setCollapsed = useCallback((next) => {
|
|
124
136
|
onCollapsedChange?.(next);
|
|
125
137
|
if (isControlled)
|
|
126
138
|
return;
|
|
@@ -131,24 +143,47 @@ export function ConnectAppShell({ name, icon, homeHref = "/", nav, activePath, r
|
|
|
131
143
|
// Not persisted - but the event below still moves the rail for this session.
|
|
132
144
|
}
|
|
133
145
|
window.dispatchEvent(new Event(COLLAPSED_EVENT));
|
|
134
|
-
}, [
|
|
146
|
+
}, [isControlled, onCollapsedChange]);
|
|
135
147
|
const closeMobile = useCallback(() => setMobileOpen(false), []);
|
|
148
|
+
// Connect platform staff see a different set of routes - see
|
|
149
|
+
// `ConnectNavItem.audience`. Derived from the session rather than passed in,
|
|
150
|
+
// so a host app never has to thread "is this an operator" through its own
|
|
151
|
+
// layout to get the right rail.
|
|
152
|
+
const isStaff = session?.staff === true;
|
|
153
|
+
const visibleNav = useMemo(() => filterNavForAudience(nav, isStaff), [nav, isStaff]);
|
|
154
|
+
const visibleSecondaryNav = useMemo(() => (secondaryNav ? filterNavForAudience(secondaryNav, isStaff) : undefined), [secondaryNav, isStaff]);
|
|
136
155
|
const currentPath = normalizePath(pathname);
|
|
137
|
-
|
|
156
|
+
// Computed from the *unfiltered* nav on purpose: `gateExempt` is a fact
|
|
157
|
+
// about a route, not about who can see it, and reading it from the filtered
|
|
158
|
+
// list would quietly re-gate a route the moment it became staff-only. Both
|
|
159
|
+
// navs contribute - a `gateExempt` "Plans" route is just as often in the
|
|
160
|
+
// secondary group as the primary one.
|
|
161
|
+
const exempt = [
|
|
162
|
+
...gateExemptPaths(nav),
|
|
163
|
+
...(secondaryNav ? gateExemptPaths(secondaryNav) : []),
|
|
164
|
+
...(gateExemptRoutes ?? []).map(normalizePath),
|
|
165
|
+
];
|
|
138
166
|
const isExempt = exempt.some((path) => currentPath === path || currentPath.startsWith(`${path}/`));
|
|
139
167
|
// A signed-out visitor has no readiness to check and nothing to be blocked
|
|
140
168
|
// by; `ConnectAccessGate` passes them straight through, but mounting it at
|
|
141
169
|
// all costs an entitlements read that cannot succeed.
|
|
142
170
|
const signedOut = !isLoading && !session?.user;
|
|
143
171
|
const gated = gate && !isExempt && !signedOut;
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
172
|
+
// Two shell contexts, one set of children. The drawer's is always expanded
|
|
173
|
+
// and closes itself on navigation; the rail's carries the real collapse
|
|
174
|
+
// state. Building them here rather than inside the sidebar is what lets the
|
|
175
|
+
// same `railContent` render in both places with no `mobile` prop threaded
|
|
176
|
+
// through every part.
|
|
177
|
+
const railShell = useMemo(() => ({ collapsed, collapsible, setCollapsed, renderLink, activePath }), [collapsed, collapsible, setCollapsed, renderLink, activePath]);
|
|
178
|
+
const drawerShell = useMemo(() => ({
|
|
179
|
+
collapsed: false,
|
|
180
|
+
collapsible: false,
|
|
181
|
+
setCollapsed: () => { },
|
|
182
|
+
renderLink,
|
|
183
|
+
activePath,
|
|
184
|
+
onNavigate: closeMobile,
|
|
185
|
+
}), [renderLink, activePath, closeMobile]);
|
|
186
|
+
const accountRow = signedOut ? (showAuthButtons && (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(ConnectLoginButton, { size: "sm", fullWidth: true }), _jsx(ConnectRegisterButton, { size: "sm", fullWidth: true })] }))) : (_jsx(ConnectUserButton, { variant: "row", theme: theme, onThemeChange: onThemeChange }));
|
|
187
|
+
const railContent = (_jsxs(_Fragment, { children: [_jsx(ConnectSidebarHeader, { name: name, icon: icon, homeHref: homeHref }), workspaceSwitcher !== false && (_jsx("div", { className: "shrink-0 px-3 pb-1", children: _jsx(ConnectWorkspaceSwitcher, {}) })), _jsx(ConnectSidebarBody, { children: _jsx(ConnectSidebarNav, { nav: visibleNav }) }), (visibleSecondaryNav || sidebarFooter || showAppsGrid) && (_jsxs(ConnectSidebarFooter, { separated: false, children: [visibleSecondaryNav && _jsx(ConnectSidebarNav, { nav: visibleSecondaryNav }), sidebarFooter, showAppsGrid && _jsx(ConnectAppsGrid, { variant: "row" })] })), accountRow && _jsx(ConnectSidebarFooter, { children: accountRow })] }));
|
|
188
|
+
return (_jsx(MotionProvider, { children: _jsxs("div", { className: `flex h-screen bg-background ${className ?? ""}`, children: [_jsx(ConnectShellProvider, { value: railShell, children: _jsx(ConnectSidebar, { children: railContent }) }), _jsx(Drawer.Backdrop, { isOpen: mobileOpen, onOpenChange: setMobileOpen, children: _jsx(Drawer.Content, { placement: "left", className: "w-[min(20rem,85vw)]", children: _jsx(Drawer.Dialog, { "aria-label": "Navigation menu", className: "p-0", children: _jsx(ConnectShellProvider, { value: drawerShell, children: _jsx("div", { className: "flex h-full flex-col", children: railContent }) }) }) }) }), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col", children: [_jsxs("header", { className: "flex h-14 shrink-0 items-center gap-2 border-b border-border bg-surface px-3 md:hidden", children: [_jsx(Button, { isIconOnly: true, variant: "tertiary", size: "sm", "aria-label": "Open navigation menu", onPress: () => setMobileOpen(true), children: _jsx(Menu, { className: "size-5" }) }), _jsx(ConnectLogo, { name: name, icon: icon })] }), _jsx("main", { className: "flex-1 overflow-y-auto", children: _jsxs("div", { className: contentClassName ?? "mx-auto w-full max-w-6xl px-4 py-6 md:px-8 md:py-8", children: [gated ? _jsx(ConnectAccessGate, { children: children }) : children, footer] }) })] }), askAi && _jsx(ConnectAskAi, { ...(askAi === true ? {} : askAi) })] }) }));
|
|
154
189
|
}
|
|
@@ -4,6 +4,7 @@ import { Button, Modal, Skeleton } from "@heroui/react";
|
|
|
4
4
|
import { AlertCircle, ExternalLink, LayoutGrid } from "lucide-react";
|
|
5
5
|
import { useConnectApps, useConnect } from "../provider.js";
|
|
6
6
|
import { ENTER, m, MotionProvider } from "./motion.js";
|
|
7
|
+
import { SidebarLabel, sidebarRowClass } from "./shell-context.js";
|
|
7
8
|
function AppTile({ app }) {
|
|
8
9
|
const disabled = app.launchUrl === null;
|
|
9
10
|
const body = (_jsxs(_Fragment, { children: [_jsxs("span", { className: "relative flex size-12 items-center justify-center overflow-hidden rounded-xl bg-surface-secondary", children: [app.iconUrl ? (
|
|
@@ -30,13 +31,13 @@ function AppTile({ app }) {
|
|
|
30
31
|
* Renders nothing when signed out: a launcher with nothing to launch is just a
|
|
31
32
|
* button that opens an empty box.
|
|
32
33
|
*/
|
|
33
|
-
export function ConnectAppsGrid({ className, label = "Switch app" }) {
|
|
34
|
+
export function ConnectAppsGrid({ className, label = "Switch app", variant = "row" }) {
|
|
34
35
|
const { session, isLoading: sessionLoading } = useConnect();
|
|
35
36
|
const { data, isLoading, error } = useConnectApps();
|
|
36
37
|
if (sessionLoading || !session?.user)
|
|
37
38
|
return null;
|
|
38
39
|
const apps = data?.apps ?? [];
|
|
39
|
-
return (_jsx(MotionProvider, { children: _jsxs(Modal, { children: [_jsx(Button, { isIconOnly: true, variant: "primary", "aria-label": label, className: className, children: _jsx(LayoutGrid, { className: "size-5" }) }), _jsx(Modal.Backdrop, { children: _jsx(Modal.Container, { children: _jsxs(Modal.Dialog, { className: "sm:max-w-md", children: [_jsx(Modal.CloseTrigger, {}), _jsx(Modal.Header, { children: _jsx(Modal.Heading, { children: "Your apps" }) }), _jsxs(Modal.Body, { children: [isLoading && (_jsx("div", { className: "grid grid-cols-3 gap-2", children: Array.from({ length: 6 }, (_, index) => (_jsxs("div", { className: "flex flex-col items-center gap-2 p-3", children: [_jsx(Skeleton, { className: "size-12 rounded-xl" }), _jsx(Skeleton, { className: "h-3 w-14" })] }, index))) })), !isLoading && error && (_jsx("p", { className: "py-6 text-center text-sm text-danger", children: "Could not load your apps." })), !isLoading && !error && apps.length === 0 && (_jsx("p", { className: "py-6 text-center text-sm text-muted", children: "No other apps yet. Anything this workspace subscribes to shows up here." })), !isLoading && !error && apps.length > 0 && (_jsx(m.div, { className: "grid grid-cols-3 gap-2", variants: { show: { transition: { staggerChildren: 0.04 } } }, initial: "hidden", animate: "show", children: apps.map((app) => (_jsx(m.div, { variants: { hidden: { opacity: 0, y: 8 }, show: { opacity: 1, y: 0, transition: ENTER } }, children: _jsx(AppTile, { app: app }) }, app.applicationId))) }))] }), _jsx(Modal.Footer, { children: _jsx(ConnectBrowseAllLink, {}) })] }) }) })] }) }));
|
|
40
|
+
return (_jsx(MotionProvider, { children: _jsxs(Modal, { children: [variant === "row" ? (_jsxs(Button, { variant: "tertiary", "aria-label": label, className: sidebarRowClass({ className }), children: [_jsx("span", { className: "flex size-5 shrink-0 items-center justify-center", children: _jsx(LayoutGrid, { className: "size-5" }) }), _jsx(SidebarLabel, { children: label })] })) : (_jsx(Button, { isIconOnly: true, variant: "primary", "aria-label": label, className: className, children: _jsx(LayoutGrid, { className: "size-5" }) })), _jsx(Modal.Backdrop, { children: _jsx(Modal.Container, { children: _jsxs(Modal.Dialog, { className: "sm:max-w-md", children: [_jsx(Modal.CloseTrigger, {}), _jsx(Modal.Header, { children: _jsx(Modal.Heading, { children: "Your apps" }) }), _jsxs(Modal.Body, { children: [isLoading && (_jsx("div", { className: "grid grid-cols-3 gap-2", children: Array.from({ length: 6 }, (_, index) => (_jsxs("div", { className: "flex flex-col items-center gap-2 p-3", children: [_jsx(Skeleton, { className: "size-12 rounded-xl" }), _jsx(Skeleton, { className: "h-3 w-14" })] }, index))) })), !isLoading && error && (_jsx("p", { className: "py-6 text-center text-sm text-danger", children: "Could not load your apps." })), !isLoading && !error && apps.length === 0 && (_jsx("p", { className: "py-6 text-center text-sm text-muted", children: "No other apps yet. Anything this workspace subscribes to shows up here." })), !isLoading && !error && apps.length > 0 && (_jsx(m.div, { className: "grid grid-cols-3 gap-2", variants: { show: { transition: { staggerChildren: 0.04 } } }, initial: "hidden", animate: "show", children: apps.map((app) => (_jsx(m.div, { variants: { hidden: { opacity: 0, y: 8 }, show: { opacity: 1, y: 0, transition: ENTER } }, children: _jsx(AppTile, { app: app }) }, app.applicationId))) }))] }), _jsx(Modal.Footer, { children: _jsx(ConnectBrowseAllLink, {}) })] }) }) })] }) }));
|
|
40
41
|
}
|
|
41
42
|
function ConnectBrowseAllLink() {
|
|
42
43
|
const { appUrl } = useConnect();
|