@marimo-team/islands 0.23.7-dev9 → 0.23.7
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/{ConnectedDataExplorerComponent-DnRhpPMJ.js → ConnectedDataExplorerComponent-2lBNiUv6.js} +13 -13
- package/dist/{ErrorBoundary-Da4UeYxT.js → ErrorBoundary-D3wrPNma.js} +1 -1
- package/dist/{any-language-editor-DDubl8YH.js → any-language-editor-VWs_7v27.js} +5 -5
- package/dist/assets/__vite-browser-external-CAdMKBac.js +1 -0
- package/dist/assets/worker-CpBbwbQo.js +73 -0
- package/dist/{button-CA5pI2YF.js → button-Dj4BTre0.js} +5 -0
- package/dist/{capabilities-6laDasij.js → capabilities-C9rrYCzf.js} +1 -1
- package/dist/{chat-ui-BmWZZ3mE.js → chat-ui-D3XBept8.js} +625 -233
- package/dist/{check-CFM2mVDr.js → check-BcUIXnUT.js} +1 -1
- package/dist/{code-visibility-CRHzv49w.js → code-visibility-sKGUbHmr.js} +11480 -1992
- package/dist/{copy-TGGAUEWp.js → copy-DLf4aN7I.js} +2 -2
- package/dist/{dist-ESg7xyoD.js → dist-D3ZI9nhS.js} +2 -2
- package/dist/{error-banner-DnBPzEWg.js → error-banner-CVkfBUT3.js} +2 -2
- package/dist/{esm-Dd1z1auZ.js → esm-CWp0KQeK.js} +1 -1
- package/dist/{extends-CzJgxo2J.js → extends-vAi97cpa.js} +4 -4
- package/dist/{formats-CgaK7Gmx.js → formats-Dsy9kkZu.js} +3 -3
- package/dist/{glide-data-editor-B-3A3G02.js → glide-data-editor-DucgdjRo.js} +9 -9
- package/dist/{html-to-image-BwZL1Pkk.js → html-to-image-CpggM7u1.js} +2667 -2408
- package/dist/{input-BAOe64zx.js → input-D4kjoQUB.js} +8 -6
- package/dist/{label-BCWi-Oqu.js → label-BLqV33b1.js} +2 -2
- package/dist/{loader-BvW0-YWZ.js → loader-Dr8Qem8p.js} +1 -1
- package/dist/main.js +1697 -10282
- package/dist/{mermaid-cXSZ1pfD.js → mermaid-DO-Daq7u.js} +5 -5
- package/dist/{process-output-lpVrk7d5.js → process-output-X8TR20AK.js} +3 -3
- package/dist/reveal-component-BBAxPTso.js +7447 -0
- package/dist/{spec-DSIuqd3f.js → spec-hVaaZsY5.js} +4 -4
- package/dist/{strings-B_FOH6eV.js → strings-BiIhGaI8.js} +4 -4
- package/dist/style.css +1 -1
- package/dist/{swiper-component-BHs0PWwp.js → swiper-component-DlD2GU2g.js} +2 -2
- package/dist/{toDate-CHtl9vts.js → toDate-CIpC_34u.js} +33 -20
- package/dist/{tooltip-B0mtKTXm.js → tooltip-DRaMBu06.js} +3 -3
- package/dist/{types-DBtDeUKD.js → types-Dzuoc3LN.js} +1 -1
- package/dist/{useAsyncData-B6hCGywC.js → useAsyncData-C56Khv_R.js} +1 -1
- package/dist/{useDateFormatter-B3mCQMP3.js → useDateFormatter-B_9k85Ex.js} +2 -2
- package/dist/{useDeepCompareMemoize-CmwDuYUH.js → useDeepCompareMemoize-Dt98v2ua.js} +1 -1
- package/dist/{useIframeCapabilities-DbdLoEDm.js → useIframeCapabilities-BkYHTrss.js} +1 -1
- package/dist/{useLifecycle-CjMjllqy.js → useLifecycle-BF6-z62y.js} +3 -3
- package/dist/{useTheme-CByZUW0p.js → useTheme-DykuNHR2.js} +2 -2
- package/dist/{vega-component-C2BYPkfd.js → vega-component-cSdqoAxe.js} +10 -10
- package/dist/{zod-BxdsqRPd.js → zod-BWkcDORu.js} +1 -1
- package/package.json +3 -3
- package/src/components/chat/chat-components.tsx +47 -0
- package/src/components/chat/chat-display.tsx +41 -7
- package/src/components/chat/chat-panel.tsx +37 -10
- package/src/components/chat/chat-utils.ts +42 -20
- package/src/components/chat/reasoning-accordion.tsx +14 -3
- package/src/components/chat/tool-call/shared.ts +13 -0
- package/src/components/chat/tool-call/tool-approval-card.tsx +62 -0
- package/src/components/chat/tool-call/tool-args.tsx +26 -0
- package/src/components/chat/tool-call/tool-call-view.tsx +99 -0
- package/src/components/chat/tool-call/tool-error-card.tsx +81 -0
- package/src/components/chat/tool-call/tool-history-row.tsx +153 -0
- package/src/components/chat/tool-call/tool-result.tsx +101 -0
- package/src/components/data-table/__tests__/column-header.test.ts +3 -1
- package/src/components/data-table/__tests__/column-header.test.tsx +308 -0
- package/src/components/data-table/__tests__/filter-by-values-picker.test.tsx +112 -0
- package/src/components/data-table/__tests__/filter-pill-editor.test.tsx +261 -0
- package/src/components/data-table/__tests__/filters.test.ts +196 -49
- package/src/components/data-table/charts/components/form-fields.tsx +1 -0
- package/src/components/data-table/column-header.tsx +349 -170
- package/src/components/data-table/date-filter-inputs.tsx +325 -0
- package/src/components/data-table/filter-by-values-picker.tsx +70 -9
- package/src/components/data-table/filter-pill-editor.tsx +410 -156
- package/src/components/data-table/filter-pills.tsx +69 -54
- package/src/components/data-table/filters.ts +218 -101
- package/src/components/data-table/header-items.tsx +8 -1
- package/src/components/data-table/operator-labels.ts +25 -0
- package/src/components/data-table/regex-input.tsx +61 -0
- package/src/components/dependency-graph/minimap-content.tsx +14 -3
- package/src/components/editor/actions/pair-with-agent-modal.tsx +140 -49
- package/src/components/editor/actions/useNotebookActions.tsx +3 -1
- package/src/components/editor/app-container.tsx +7 -1
- package/src/components/editor/chrome/panels/context-aware-panel/context-aware-panel.tsx +10 -2
- package/src/components/editor/chrome/wrapper/app-chrome.tsx +1 -0
- package/src/components/editor/chrome/wrapper/footer-items/backend-status.tsx +1 -1
- package/src/components/editor/chrome/wrapper/footer.tsx +4 -1
- package/src/components/editor/chrome/wrapper/panels.tsx +4 -1
- package/src/components/editor/chrome/wrapper/sidebar.tsx +4 -1
- package/src/components/editor/controls/Controls.tsx +11 -3
- package/src/components/editor/file-tree/file-explorer.tsx +12 -2
- package/src/components/editor/header/__tests__/status.test.tsx +108 -0
- package/src/components/editor/header/status.tsx +44 -10
- package/src/components/editor/navigation/__tests__/clipboard.test.ts +106 -0
- package/src/components/editor/navigation/__tests__/navigation.test.ts +70 -0
- package/src/components/editor/navigation/clipboard.ts +99 -25
- package/src/components/editor/navigation/navigation.ts +15 -1
- package/src/components/editor/notebook-cell.tsx +5 -0
- package/src/components/editor/output/console/ConsoleOutput.tsx +23 -5
- package/src/components/editor/output/console/__tests__/ConsoleOutput.test.tsx +114 -0
- package/src/components/editor/renderers/slides-layout/__tests__/compute-slide-cells.test.ts +5 -4
- package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +55 -15
- package/src/components/editor/renderers/slides-layout/plugin.tsx +8 -25
- package/src/components/editor/renderers/slides-layout/slides-layout.tsx +19 -6
- package/src/components/editor/renderers/slides-layout/types.ts +40 -31
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +1 -0
- package/src/components/home/components.tsx +6 -0
- package/src/components/pages/run-page.tsx +4 -1
- package/src/components/scratchpad/scratchpad.tsx +1 -0
- package/src/components/slides/__tests__/slide-notes.test.ts +131 -0
- package/src/components/slides/reveal-component.tsx +252 -147
- package/src/components/slides/slide-notes-editor.tsx +127 -0
- package/src/components/slides/slide-notes.ts +64 -0
- package/src/components/slides/slides.css +14 -0
- package/src/components/ui/combobox.tsx +24 -5
- package/src/components/ui/number-field.tsx +2 -0
- package/src/core/ai/tools/__tests__/registry.test.ts +10 -12
- package/src/core/ai/tools/registry.ts +9 -5
- package/src/core/cells/__tests__/cells.test.ts +187 -0
- package/src/core/cells/__tests__/pending-cut-service.test.tsx +123 -0
- package/src/core/cells/cells.ts +102 -17
- package/src/core/cells/document-changes.ts +6 -1
- package/src/core/cells/pending-cut-service.ts +55 -0
- package/src/core/cells/utils.ts +11 -0
- package/src/core/codemirror/cells/extensions.ts +10 -0
- package/src/core/codemirror/go-to-definition/__tests__/commands.test.ts +152 -0
- package/src/core/codemirror/go-to-definition/__tests__/utils.test.ts +99 -0
- package/src/core/codemirror/go-to-definition/commands.ts +382 -22
- package/src/core/codemirror/go-to-definition/utils.ts +23 -5
- package/src/core/edit-app.tsx +3 -2
- package/src/core/hotkeys/hotkeys.ts +5 -0
- package/src/core/islands/worker/worker.tsx +3 -2
- package/src/core/run-app.tsx +2 -1
- package/src/core/runtime/__tests__/runtime.test.ts +38 -17
- package/src/core/runtime/runtime.ts +57 -34
- package/src/core/wasm/__tests__/utils.test.ts +34 -0
- package/src/core/wasm/utils.ts +14 -0
- package/src/core/wasm/worker/bootstrap.ts +3 -2
- package/src/core/wasm/worker/worker.ts +3 -2
- package/src/core/websocket/__tests__/useMarimoKernelConnection.hook.test.tsx +156 -0
- package/src/core/websocket/__tests__/useMarimoKernelConnection.test.ts +101 -0
- package/src/core/websocket/transports/__tests__/ws.test.ts +125 -0
- package/src/core/websocket/transports/basic.ts +1 -1
- package/src/core/websocket/transports/ws.ts +96 -0
- package/src/core/websocket/useMarimoKernelConnection.tsx +133 -54
- package/src/core/websocket/useWebSocket.tsx +3 -15
- package/src/css/app/Cell.css +10 -0
- package/src/plugins/core/__test__/sanitize.test.ts +30 -0
- package/src/plugins/impl/DropdownPlugin.tsx +12 -1
- package/src/plugins/impl/MultiselectPlugin.tsx +4 -0
- package/src/plugins/impl/SearchableSelect.tsx +11 -1
- package/src/plugins/impl/TabsPlugin.tsx +35 -7
- package/src/plugins/impl/__tests__/DropdownPlugin.test.tsx +56 -0
- package/src/plugins/impl/__tests__/TabsPlugin.test.tsx +154 -0
- package/src/plugins/impl/data-frames/forms/__tests__/__snapshots__/form.test.tsx.snap +48 -36
- package/src/plugins/impl/data-frames/schema.ts +4 -1
- package/src/plugins/layout/DownloadPlugin.tsx +9 -7
- package/src/utils/__tests__/id-tree.test.ts +71 -0
- package/src/utils/download.ts +4 -2
- package/src/utils/id-tree.tsx +89 -0
- package/dist/assets/__vite-browser-external-rrUYDKRl.js +0 -1
- package/dist/assets/worker-Bfy15ViQ.js +0 -73
- package/dist/reveal-component-C97Ceb7e.js +0 -4863
- package/src/components/chat/tool-call-accordion.tsx +0 -247
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
import type {
|
|
3
|
+
CalendarDate,
|
|
4
|
+
CalendarDateTime,
|
|
5
|
+
Time,
|
|
6
|
+
} from "@internationalized/date";
|
|
7
|
+
import { parseDate, parseDateTime, parseTime } from "@internationalized/date";
|
|
8
|
+
import { MinusIcon } from "lucide-react";
|
|
9
|
+
import { useState } from "react";
|
|
10
|
+
import type { DateValue, TimeValue } from "react-aria-components";
|
|
11
|
+
import { TimeField } from "@/components/ui/date-input";
|
|
12
|
+
import { DatePicker, DateRangePicker } from "@/components/ui/date-picker";
|
|
13
|
+
import {
|
|
14
|
+
dateToISODate,
|
|
15
|
+
dateToISODateTime,
|
|
16
|
+
dateToISOTime,
|
|
17
|
+
type FilterType,
|
|
18
|
+
} from "./filters";
|
|
19
|
+
|
|
20
|
+
export type DateLikeFilterType = Extract<
|
|
21
|
+
FilterType,
|
|
22
|
+
"date" | "datetime" | "time"
|
|
23
|
+
>;
|
|
24
|
+
|
|
25
|
+
function dateToAria(
|
|
26
|
+
filterType: DateLikeFilterType,
|
|
27
|
+
d: Date,
|
|
28
|
+
): DateValue | TimeValue {
|
|
29
|
+
switch (filterType) {
|
|
30
|
+
case "date":
|
|
31
|
+
return parseDate(dateToISODate(d));
|
|
32
|
+
case "datetime":
|
|
33
|
+
return parseDateTime(dateToISODateTime(d));
|
|
34
|
+
case "time":
|
|
35
|
+
return parseTime(dateToISOTime(d));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function ariaToDate(
|
|
40
|
+
filterType: DateLikeFilterType,
|
|
41
|
+
aria: DateValue | TimeValue,
|
|
42
|
+
): Date {
|
|
43
|
+
if (filterType === "time") {
|
|
44
|
+
const t = aria as Time;
|
|
45
|
+
return new Date(1970, 0, 1, t.hour, t.minute, t.second, t.millisecond);
|
|
46
|
+
}
|
|
47
|
+
if (filterType === "date") {
|
|
48
|
+
const c = aria as CalendarDate;
|
|
49
|
+
return new Date(c.year, c.month - 1, c.day);
|
|
50
|
+
}
|
|
51
|
+
const c = aria as Partial<CalendarDateTime> & CalendarDate;
|
|
52
|
+
return new Date(
|
|
53
|
+
c.year,
|
|
54
|
+
c.month - 1,
|
|
55
|
+
c.day,
|
|
56
|
+
c.hour ?? 0,
|
|
57
|
+
c.minute ?? 0,
|
|
58
|
+
c.second ?? 0,
|
|
59
|
+
c.millisecond ?? 0,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Parses a pasted string into a Date appropriate for the filter type.
|
|
64
|
+
// Accepts ISO, US, RFC formats via the Date constructor; time-only strings
|
|
65
|
+
// (HH:MM[:SS]) are handled explicitly since `new Date("12:30")` is invalid.
|
|
66
|
+
export function parsePastedDate(
|
|
67
|
+
filterType: DateLikeFilterType,
|
|
68
|
+
text: string,
|
|
69
|
+
): Date | undefined {
|
|
70
|
+
const trimmed = text.trim();
|
|
71
|
+
if (!trimmed) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const timeMatch =
|
|
76
|
+
filterType === "time"
|
|
77
|
+
? trimmed.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?\s*(am|pm)?$/i)
|
|
78
|
+
: null;
|
|
79
|
+
if (timeMatch) {
|
|
80
|
+
const [, hStr, mStr, sStr, ampm] = timeMatch;
|
|
81
|
+
let hour = Number.parseInt(hStr, 10);
|
|
82
|
+
const minute = Number.parseInt(mStr, 10);
|
|
83
|
+
const second = sStr ? Number.parseInt(sStr, 10) : 0;
|
|
84
|
+
if (ampm) {
|
|
85
|
+
const isPm = ampm.toLowerCase() === "pm";
|
|
86
|
+
if (hour === 12) {
|
|
87
|
+
hour = isPm ? 12 : 0;
|
|
88
|
+
} else if (isPm) {
|
|
89
|
+
hour += 12;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return new Date(1970, 0, 1, hour, minute, second);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const dateOnlyMatch = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
96
|
+
if (dateOnlyMatch) {
|
|
97
|
+
const [, y, m, d] = dateOnlyMatch;
|
|
98
|
+
return new Date(
|
|
99
|
+
Number.parseInt(y, 10),
|
|
100
|
+
Number.parseInt(m, 10) - 1,
|
|
101
|
+
Number.parseInt(d, 10),
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Parse ISO datetimes as wall-clock to stay consistent with the picker's
|
|
106
|
+
// local-time basis. Trailing `Z` or offsets are stripped so that pasting
|
|
107
|
+
// `2024-01-15T08:30:00Z` displays `08:30` instead of being shifted by the
|
|
108
|
+
// viewer's timezone.
|
|
109
|
+
const isoMatch = trimmed.match(
|
|
110
|
+
/^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?$/,
|
|
111
|
+
);
|
|
112
|
+
if (isoMatch) {
|
|
113
|
+
const [, y, mo, d, h, mi, s] = isoMatch;
|
|
114
|
+
return new Date(
|
|
115
|
+
Number.parseInt(y, 10),
|
|
116
|
+
Number.parseInt(mo, 10) - 1,
|
|
117
|
+
Number.parseInt(d, 10),
|
|
118
|
+
Number.parseInt(h, 10),
|
|
119
|
+
Number.parseInt(mi, 10),
|
|
120
|
+
s ? Number.parseInt(s, 10) : 0,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const parsed = new Date(trimmed);
|
|
125
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
return parsed;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function parsePastedRange(
|
|
132
|
+
filterType: DateLikeFilterType,
|
|
133
|
+
text: string,
|
|
134
|
+
): { min: Date; max: Date } | undefined {
|
|
135
|
+
const parts = text.split(/\s+(?:-|–|—|to)\s+/i);
|
|
136
|
+
if (parts.length === 2) {
|
|
137
|
+
const min = parsePastedDate(filterType, parts[0]);
|
|
138
|
+
const max = parsePastedDate(filterType, parts[1]);
|
|
139
|
+
if (min && max) {
|
|
140
|
+
return { min, max };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const single = parsePastedDate(filterType, text);
|
|
144
|
+
if (single) {
|
|
145
|
+
return { min: single, max: single };
|
|
146
|
+
}
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
interface DateLikeInputProps {
|
|
151
|
+
filterType: DateLikeFilterType;
|
|
152
|
+
value: Date | undefined;
|
|
153
|
+
onChange: (value: Date | undefined) => void;
|
|
154
|
+
"aria-label"?: string;
|
|
155
|
+
className?: string;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export const DateLikeInput = ({
|
|
159
|
+
filterType,
|
|
160
|
+
value,
|
|
161
|
+
onChange,
|
|
162
|
+
"aria-label": ariaLabel,
|
|
163
|
+
className,
|
|
164
|
+
}: DateLikeInputProps) => {
|
|
165
|
+
const [seedKey, setSeedKey] = useState(0);
|
|
166
|
+
const [seed, setSeed] = useState(value);
|
|
167
|
+
|
|
168
|
+
const handleChange = (next: DateValue | TimeValue | null) => {
|
|
169
|
+
if (next === null) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
onChange(ariaToDate(filterType, next));
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const handlePaste = (e: React.ClipboardEvent<HTMLDivElement>) => {
|
|
176
|
+
const text = e.clipboardData.getData("text");
|
|
177
|
+
const parsed = parsePastedDate(filterType, text);
|
|
178
|
+
if (!parsed) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
e.preventDefault();
|
|
182
|
+
onChange(parsed);
|
|
183
|
+
setSeed(parsed);
|
|
184
|
+
setSeedKey((k) => k + 1);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const seedValue =
|
|
188
|
+
seed === undefined ? undefined : dateToAria(filterType, seed);
|
|
189
|
+
|
|
190
|
+
return (
|
|
191
|
+
<div onPasteCapture={handlePaste} className="contents">
|
|
192
|
+
{filterType === "time" ? (
|
|
193
|
+
<TimeField<Time>
|
|
194
|
+
key={seedKey}
|
|
195
|
+
aria-label={ariaLabel}
|
|
196
|
+
defaultValue={seedValue as Time | undefined}
|
|
197
|
+
onChange={handleChange}
|
|
198
|
+
className={className}
|
|
199
|
+
/>
|
|
200
|
+
) : filterType === "date" ? (
|
|
201
|
+
<DatePicker<CalendarDate>
|
|
202
|
+
key={seedKey}
|
|
203
|
+
aria-label={ariaLabel}
|
|
204
|
+
defaultValue={seedValue as CalendarDate | undefined}
|
|
205
|
+
onChange={handleChange}
|
|
206
|
+
className={className}
|
|
207
|
+
/>
|
|
208
|
+
) : (
|
|
209
|
+
<DatePicker<CalendarDateTime>
|
|
210
|
+
key={seedKey}
|
|
211
|
+
aria-label={ariaLabel}
|
|
212
|
+
defaultValue={seedValue as CalendarDateTime | undefined}
|
|
213
|
+
granularity="second"
|
|
214
|
+
onChange={handleChange}
|
|
215
|
+
className={className}
|
|
216
|
+
/>
|
|
217
|
+
)}
|
|
218
|
+
</div>
|
|
219
|
+
);
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
interface DateLikeRangeInputProps {
|
|
223
|
+
filterType: DateLikeFilterType;
|
|
224
|
+
min: Date | undefined;
|
|
225
|
+
max: Date | undefined;
|
|
226
|
+
onRangeChange: (min: Date | undefined, max: Date | undefined) => void;
|
|
227
|
+
className?: string;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export const DateLikeRangeInput = ({
|
|
231
|
+
filterType,
|
|
232
|
+
min,
|
|
233
|
+
max,
|
|
234
|
+
onRangeChange,
|
|
235
|
+
className,
|
|
236
|
+
}: DateLikeRangeInputProps) => {
|
|
237
|
+
const [seedKey, setSeedKey] = useState(0);
|
|
238
|
+
const [seedMin, setSeedMin] = useState(min);
|
|
239
|
+
const [seedMax, setSeedMax] = useState(max);
|
|
240
|
+
|
|
241
|
+
const handlePaste = (e: React.ClipboardEvent<HTMLDivElement>) => {
|
|
242
|
+
const text = e.clipboardData.getData("text");
|
|
243
|
+
const parsed = parsePastedRange(filterType, text);
|
|
244
|
+
if (!parsed) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
e.preventDefault();
|
|
248
|
+
e.stopPropagation();
|
|
249
|
+
onRangeChange(parsed.min, parsed.max);
|
|
250
|
+
setSeedMin(parsed.min);
|
|
251
|
+
setSeedMax(parsed.max);
|
|
252
|
+
setSeedKey((k) => k + 1);
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
if (filterType === "time") {
|
|
256
|
+
return (
|
|
257
|
+
<div onPasteCapture={handlePaste} className="flex gap-1 items-center">
|
|
258
|
+
<DateLikeInput
|
|
259
|
+
key={`min-${seedKey}`}
|
|
260
|
+
filterType="time"
|
|
261
|
+
value={seedMin}
|
|
262
|
+
onChange={(nextMin) => onRangeChange(nextMin, max)}
|
|
263
|
+
aria-label="min"
|
|
264
|
+
className={className}
|
|
265
|
+
/>
|
|
266
|
+
<MinusIcon className="h-5 w-5 text-muted-foreground" />
|
|
267
|
+
<DateLikeInput
|
|
268
|
+
key={`max-${seedKey}`}
|
|
269
|
+
filterType="time"
|
|
270
|
+
value={seedMax}
|
|
271
|
+
onChange={(nextMax) => onRangeChange(min, nextMax)}
|
|
272
|
+
aria-label="max"
|
|
273
|
+
className={className}
|
|
274
|
+
/>
|
|
275
|
+
</div>
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const handleChange = (next: { start: DateValue; end: DateValue } | null) => {
|
|
280
|
+
if (next === null) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
onRangeChange(
|
|
284
|
+
ariaToDate(filterType, next.start),
|
|
285
|
+
ariaToDate(filterType, next.end),
|
|
286
|
+
);
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const seedRange =
|
|
290
|
+
seedMin === undefined || seedMax === undefined
|
|
291
|
+
? undefined
|
|
292
|
+
: {
|
|
293
|
+
start: dateToAria(filterType, seedMin),
|
|
294
|
+
end: dateToAria(filterType, seedMax),
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
return (
|
|
298
|
+
<div onPasteCapture={handlePaste} className="contents">
|
|
299
|
+
{filterType === "date" ? (
|
|
300
|
+
<DateRangePicker<CalendarDate>
|
|
301
|
+
key={seedKey}
|
|
302
|
+
aria-label="range"
|
|
303
|
+
defaultValue={
|
|
304
|
+
seedRange as { start: CalendarDate; end: CalendarDate } | undefined
|
|
305
|
+
}
|
|
306
|
+
onChange={handleChange}
|
|
307
|
+
className={className}
|
|
308
|
+
/>
|
|
309
|
+
) : (
|
|
310
|
+
<DateRangePicker<CalendarDateTime>
|
|
311
|
+
key={seedKey}
|
|
312
|
+
aria-label="range"
|
|
313
|
+
defaultValue={
|
|
314
|
+
seedRange as
|
|
315
|
+
| { start: CalendarDateTime; end: CalendarDateTime }
|
|
316
|
+
| undefined
|
|
317
|
+
}
|
|
318
|
+
granularity="second"
|
|
319
|
+
onChange={handleChange}
|
|
320
|
+
className={className}
|
|
321
|
+
/>
|
|
322
|
+
)}
|
|
323
|
+
</div>
|
|
324
|
+
);
|
|
325
|
+
};
|
|
@@ -32,6 +32,7 @@ interface Props<TData, TValue> {
|
|
|
32
32
|
calculateTopKRows?: CalculateTopKRows;
|
|
33
33
|
chosenValues: unknown[];
|
|
34
34
|
onChange: (values: unknown[]) => void;
|
|
35
|
+
creatable?: boolean;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
export const FilterByValuesPicker = <TData, TValue>({
|
|
@@ -39,6 +40,7 @@ export const FilterByValuesPicker = <TData, TValue>({
|
|
|
39
40
|
calculateTopKRows,
|
|
40
41
|
chosenValues,
|
|
41
42
|
onChange,
|
|
43
|
+
creatable = false,
|
|
42
44
|
}: Props<TData, TValue>) => {
|
|
43
45
|
const [open, setOpen] = useState(false);
|
|
44
46
|
|
|
@@ -58,6 +60,7 @@ export const FilterByValuesPicker = <TData, TValue>({
|
|
|
58
60
|
<Popover open={open} onOpenChange={setOpen}>
|
|
59
61
|
<PopoverTrigger asChild={true}>
|
|
60
62
|
<Button
|
|
63
|
+
type="button"
|
|
61
64
|
variant="outline"
|
|
62
65
|
size="xs"
|
|
63
66
|
className="h-6 mb-1 w-full justify-between font-normal"
|
|
@@ -79,6 +82,7 @@ export const FilterByValuesPicker = <TData, TValue>({
|
|
|
79
82
|
calculateTopKRows={calculateTopKRows}
|
|
80
83
|
chosenValues={chosenValuesSet}
|
|
81
84
|
onChange={onChange}
|
|
85
|
+
creatable={creatable}
|
|
82
86
|
/>
|
|
83
87
|
</PopoverContent>
|
|
84
88
|
</Popover>
|
|
@@ -90,6 +94,7 @@ interface FilterByValuesListProps<TData, TValue> {
|
|
|
90
94
|
calculateTopKRows?: CalculateTopKRows;
|
|
91
95
|
chosenValues: Set<unknown>;
|
|
92
96
|
onChange: (values: unknown[]) => void;
|
|
97
|
+
creatable?: boolean;
|
|
93
98
|
}
|
|
94
99
|
|
|
95
100
|
/**
|
|
@@ -100,6 +105,7 @@ export const FilterByValuesList = <TData, TValue>({
|
|
|
100
105
|
calculateTopKRows,
|
|
101
106
|
chosenValues,
|
|
102
107
|
onChange,
|
|
108
|
+
creatable = false,
|
|
103
109
|
}: FilterByValuesListProps<TData, TValue>) => {
|
|
104
110
|
const [query, setQuery] = useState<string>("");
|
|
105
111
|
|
|
@@ -133,13 +139,48 @@ export const FilterByValuesList = <TData, TValue>({
|
|
|
133
139
|
}
|
|
134
140
|
}, [data, query]);
|
|
135
141
|
|
|
142
|
+
// Surface chosen values that aren't in the top-K so they stay visible/uncheckable.
|
|
143
|
+
// Count is undefined for these rows; the cell renders an em-dash.
|
|
144
|
+
const mergedData = useMemo<Array<[unknown, number | undefined]>>(() => {
|
|
145
|
+
const seen = new Set(filteredData.map(([v]) => v));
|
|
146
|
+
const extras: Array<[unknown, number | undefined]> = [];
|
|
147
|
+
for (const chosen of chosenValues) {
|
|
148
|
+
if (seen.has(chosen)) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const str = String(chosen);
|
|
152
|
+
const matches =
|
|
153
|
+
query.length === 0 ||
|
|
154
|
+
smartMatch(query, str) ||
|
|
155
|
+
str.toLowerCase().includes(query.toLowerCase());
|
|
156
|
+
if (matches) {
|
|
157
|
+
extras.push([chosen, undefined]);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return [...filteredData, ...extras];
|
|
161
|
+
}, [filteredData, chosenValues, query]);
|
|
162
|
+
|
|
136
163
|
const handleToggle = (value: unknown) => {
|
|
137
164
|
onChange([...Sets.toggle(chosenValues, value)]);
|
|
138
165
|
};
|
|
139
166
|
|
|
167
|
+
const trimmedQuery = query.trim();
|
|
168
|
+
const canCreate =
|
|
169
|
+
creatable &&
|
|
170
|
+
trimmedQuery !== "" &&
|
|
171
|
+
!mergedData.some(([v]) => String(v) === trimmedQuery);
|
|
172
|
+
|
|
173
|
+
const commitCreate = () => {
|
|
174
|
+
if (!canCreate) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
onChange([...chosenValues, trimmedQuery]);
|
|
178
|
+
setQuery("");
|
|
179
|
+
};
|
|
180
|
+
|
|
140
181
|
const allVisibleChecked =
|
|
141
|
-
|
|
142
|
-
|
|
182
|
+
mergedData.length > 0 &&
|
|
183
|
+
mergedData.every(([value]) => chosenValues.has(value));
|
|
143
184
|
|
|
144
185
|
const selectAllState: boolean | "indeterminate" = allVisibleChecked
|
|
145
186
|
? true
|
|
@@ -153,11 +194,11 @@ export const FilterByValuesList = <TData, TValue>({
|
|
|
153
194
|
}
|
|
154
195
|
const next = new Set(chosenValues);
|
|
155
196
|
if (allVisibleChecked) {
|
|
156
|
-
for (const [value] of
|
|
197
|
+
for (const [value] of mergedData) {
|
|
157
198
|
next.delete(value);
|
|
158
199
|
}
|
|
159
200
|
} else {
|
|
160
|
-
for (const [value] of
|
|
201
|
+
for (const [value] of mergedData) {
|
|
161
202
|
next.add(value);
|
|
162
203
|
}
|
|
163
204
|
}
|
|
@@ -183,13 +224,24 @@ export const FilterByValuesList = <TData, TValue>({
|
|
|
183
224
|
return (
|
|
184
225
|
<Command className="text-sm outline-hidden" shouldFilter={false}>
|
|
185
226
|
<CommandInput
|
|
186
|
-
placeholder={
|
|
227
|
+
placeholder={
|
|
228
|
+
creatable
|
|
229
|
+
? "Search or add a value…"
|
|
230
|
+
: `Search among the top ${data.length} values`
|
|
231
|
+
}
|
|
187
232
|
autoFocus={true}
|
|
188
|
-
|
|
233
|
+
value={query}
|
|
234
|
+
onValueChange={setQuery}
|
|
235
|
+
onKeyDown={(e) => {
|
|
236
|
+
if (e.key === "Enter" && canCreate) {
|
|
237
|
+
e.preventDefault();
|
|
238
|
+
commitCreate();
|
|
239
|
+
}
|
|
240
|
+
}}
|
|
189
241
|
/>
|
|
190
242
|
<CommandEmpty>No results found.</CommandEmpty>
|
|
191
243
|
<CommandList>
|
|
192
|
-
{
|
|
244
|
+
{mergedData.length > 0 && (
|
|
193
245
|
<CommandItem
|
|
194
246
|
value="__select-all__"
|
|
195
247
|
className="border-b rounded-none px-3"
|
|
@@ -204,7 +256,7 @@ export const FilterByValuesList = <TData, TValue>({
|
|
|
204
256
|
<span className="font-bold">Count</span>
|
|
205
257
|
</CommandItem>
|
|
206
258
|
)}
|
|
207
|
-
{
|
|
259
|
+
{mergedData.map(([value, count]) => {
|
|
208
260
|
const isSelected = chosenValues.has(value);
|
|
209
261
|
const valueString = stringifyUnknownValue({ value });
|
|
210
262
|
const sentinel = detectSentinel(
|
|
@@ -226,10 +278,19 @@ export const FilterByValuesList = <TData, TValue>({
|
|
|
226
278
|
<span className="flex-1 overflow-hidden max-h-20 line-clamp-3">
|
|
227
279
|
{sentinel ? <SentinelCell sentinel={sentinel} /> : valueString}
|
|
228
280
|
</span>
|
|
229
|
-
<span className="ml-3">{count}</span>
|
|
281
|
+
<span className="ml-3">{count === undefined ? "—" : count}</span>
|
|
230
282
|
</CommandItem>
|
|
231
283
|
);
|
|
232
284
|
})}
|
|
285
|
+
{canCreate && (
|
|
286
|
+
<CommandItem
|
|
287
|
+
value={`__create__:${trimmedQuery}`}
|
|
288
|
+
className="border-t rounded-none px-3 italic"
|
|
289
|
+
onSelect={commitCreate}
|
|
290
|
+
>
|
|
291
|
+
+ Add "{trimmedQuery}"
|
|
292
|
+
</CommandItem>
|
|
293
|
+
)}
|
|
233
294
|
</CommandList>
|
|
234
295
|
{data.length === TOP_K_ROWS && (
|
|
235
296
|
<span className="text-xs text-muted-foreground py-1.5 text-center">
|