@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
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { JoinKey } from '@columenlabs/lumen-ui-types/domains';
|
|
2
|
+
import { type ViewLinkColor } from '@columenlabs/lumen-ui-types/views';
|
|
3
|
+
import { type ReactNode } from 'react';
|
|
4
|
+
import type { FilterField, FilterToken } from './lumen-filter.js';
|
|
5
|
+
export type LinkColor = ViewLinkColor;
|
|
6
|
+
export declare const LINK_COLORS: readonly ["teal", "amber", "pink", "lime", "blue"];
|
|
7
|
+
export declare const LINK_NAMES: Record<LinkColor, string>;
|
|
8
|
+
/** The colour as CSS (a theme token). */
|
|
9
|
+
export declare const linkVar: (c: LinkColor) => string;
|
|
10
|
+
interface Links {
|
|
11
|
+
colorOf: (id: string) => LinkColor | null;
|
|
12
|
+
setColor: (id: string, color: LinkColor | null) => void;
|
|
13
|
+
/** A card's shared chips (all of its filter's chips and its tapped row, while it has a colour). */
|
|
14
|
+
publish: (id: string, tokens: FilterToken[]) => void;
|
|
15
|
+
/** The linked chips of every other card of this card's colour. */
|
|
16
|
+
follows: (id: string) => FilterToken[];
|
|
17
|
+
}
|
|
18
|
+
export declare const useLinks: () => Links | null;
|
|
19
|
+
/**
|
|
20
|
+
* Keeps the page's card colours and linked chips. `initial` are the saved colours by card id;
|
|
21
|
+
* `kept` names the page and its code-built cards, whose picks this browser remembers.
|
|
22
|
+
*/
|
|
23
|
+
export declare function useLinkState(saved: Record<string, LinkColor | null>, kept?: {
|
|
24
|
+
page: string;
|
|
25
|
+
ids: string[];
|
|
26
|
+
}): {
|
|
27
|
+
links: Links;
|
|
28
|
+
colors: Record<string, "amber" | "blue" | "lime" | "pink" | "teal" | null>;
|
|
29
|
+
};
|
|
30
|
+
export declare const useCardId: () => string | null;
|
|
31
|
+
export declare function CardIdProvider({ id, children }: {
|
|
32
|
+
id: string;
|
|
33
|
+
children: ReactNode;
|
|
34
|
+
}): import("react").JSX.Element;
|
|
35
|
+
export declare function LinksProvider({ links, children }: {
|
|
36
|
+
links: Links;
|
|
37
|
+
children: ReactNode;
|
|
38
|
+
}): import("react").JSX.Element;
|
|
39
|
+
/** A filter box's link from the card it sits in, when its card passes none (* the square is built into every card). Its linked chips are published as that card's. */
|
|
40
|
+
export declare function useAutoFilterLink(tokens: FilterToken[], given: FilterLink | null | undefined): FilterLink | undefined;
|
|
41
|
+
/** The fields that stand for a key on any domain, so a chip on one is followed by that key. */
|
|
42
|
+
export declare const KEY_FIELDS: Record<JoinKey, string[]>;
|
|
43
|
+
/** A coloured card's chips as its group gets them: its colour, and the key a key field stands for. */
|
|
44
|
+
export declare function shared(tokens: FilterToken[], color: LinkColor | null): FilterToken[];
|
|
45
|
+
/** Publishes a coloured card's chips while it shows, and takes them back when it goes. */
|
|
46
|
+
export declare function usePublishLinks(id: string, tokens: FilterToken[]): void;
|
|
47
|
+
/** A domain's keys as filter fields (`@symbol`, `@account`), so a followed chip reads by key. */
|
|
48
|
+
export declare function keyFilterFields<T>(keys: Partial<Record<JoinKey, (row: T) => string | string[] | null>> | undefined): FilterField<T>[];
|
|
49
|
+
/**
|
|
50
|
+
* The followed chips a card can read: those on a field it has, then those on a key it carries
|
|
51
|
+
* (read on `@<key>`, see `keyFilterFields`). A chip it can read neither way is skipped.
|
|
52
|
+
*/
|
|
53
|
+
export declare function followedBy<T>(tokens: FilterToken[], fields: FilterField<T>[], keys?: Partial<Record<JoinKey, unknown>>): FilterToken[];
|
|
54
|
+
/** What a card follows, as its title says it: `IRA, Roth`. */
|
|
55
|
+
export declare function followsText(tokens: FilterToken[]): string;
|
|
56
|
+
/** The square itself: filled in its colour, or an outline for none. */
|
|
57
|
+
export declare function LinkDot({ color, className }: {
|
|
58
|
+
color: LinkColor | null;
|
|
59
|
+
className?: string;
|
|
60
|
+
}): import("react").JSX.Element;
|
|
61
|
+
/** A card's square: a tap opens the colours and None; a pick applies at once. */
|
|
62
|
+
export declare function LinkPicker({ color, onPick, label, className, }: {
|
|
63
|
+
color: LinkColor | null;
|
|
64
|
+
onPick: (c: LinkColor | null) => void;
|
|
65
|
+
label: string;
|
|
66
|
+
className?: string;
|
|
67
|
+
}): import("react").JSX.Element;
|
|
68
|
+
/** What a filter box needs to show its chips' squares. */
|
|
69
|
+
export interface FilterLink {
|
|
70
|
+
/** Its card's colour; a chip square carries it. */
|
|
71
|
+
color: LinkColor | null;
|
|
72
|
+
/** Picks the card's colour (a chip square on a card with none asks for one first). */
|
|
73
|
+
onPick: (c: LinkColor | null) => void;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Row-tap filtering (keyed): in a coloured card a row tap picks the
|
|
77
|
+
* row's value as a temporary chip of the card's colour (published with its other chips); a second
|
|
78
|
+
* tap on it, or the card losing its colour, lets go. The value is the domain's `rowLink` field, else
|
|
79
|
+
* its first key (symbol, then account), so every domain with a key links rows (a basket's orders,
|
|
80
|
+
* paper and live). `onTap` is undefined where it does not apply, so the row opens as usual.
|
|
81
|
+
*/
|
|
82
|
+
export declare function useRowPick<T>(domain: {
|
|
83
|
+
rowLink?: string;
|
|
84
|
+
fields: {
|
|
85
|
+
key: string;
|
|
86
|
+
value: (row: T) => unknown;
|
|
87
|
+
}[];
|
|
88
|
+
keys?: Partial<Record<JoinKey, (row: T) => string | string[] | null>>;
|
|
89
|
+
}, color: LinkColor | null,
|
|
90
|
+
/** The value tapped until a tap changes it (a page that opens on one row). */
|
|
91
|
+
initial?: string): {
|
|
92
|
+
token: FilterToken[];
|
|
93
|
+
onTap: ((row: T) => void) | undefined;
|
|
94
|
+
tapped: ((row: T) => boolean) | undefined;
|
|
95
|
+
};
|
|
96
|
+
export {};
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { viewLinkColors } from '@columenlabs/lumen-ui-types/views';
|
|
3
|
+
import { LinkIcon } from 'lucide-react';
|
|
4
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from 'react';
|
|
5
|
+
import { cn } from './lib/utils.js';
|
|
6
|
+
import { Button } from './ui/button.js';
|
|
7
|
+
import { Popover, PopoverContent, PopoverTrigger } from './ui/popover.js';
|
|
8
|
+
export const LINK_COLORS = viewLinkColors;
|
|
9
|
+
export const LINK_NAMES = {
|
|
10
|
+
teal: 'Teal',
|
|
11
|
+
amber: 'Amber',
|
|
12
|
+
pink: 'Pink',
|
|
13
|
+
lime: 'Lime',
|
|
14
|
+
blue: 'Blue',
|
|
15
|
+
};
|
|
16
|
+
/** The colour as CSS (a theme token). */
|
|
17
|
+
export const linkVar = (c) => `var(--link-${c})`;
|
|
18
|
+
const LinksContext = createContext(null);
|
|
19
|
+
export const useLinks = () => useContext(LinksContext);
|
|
20
|
+
// Code-built cards ("yes, save it"): their colour picks have no saved view to
|
|
21
|
+
// live in, so each browser keeps them per page route, like folds. Nothing is stored server-side; a
|
|
22
|
+
// browser that refuses storage keeps the picks for the visit only.
|
|
23
|
+
const KEY = 'edgeshell:links';
|
|
24
|
+
function readRemembered() {
|
|
25
|
+
try {
|
|
26
|
+
const raw = JSON.parse(localStorage.getItem(KEY) ?? '{}');
|
|
27
|
+
return raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function remember(page, id, color) {
|
|
34
|
+
const all = readRemembered();
|
|
35
|
+
all[page] = { ...all[page], [id]: color };
|
|
36
|
+
try {
|
|
37
|
+
localStorage.setItem(KEY, JSON.stringify(all));
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// Storage refused: the pick still holds until reload.
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const isColor = (c) => c === null || LINK_COLORS.includes(c);
|
|
44
|
+
/**
|
|
45
|
+
* Keeps the page's card colours and linked chips. `initial` are the saved colours by card id;
|
|
46
|
+
* `kept` names the page and its code-built cards, whose picks this browser remembers.
|
|
47
|
+
*/
|
|
48
|
+
export function useLinkState(saved, kept) {
|
|
49
|
+
const page = kept?.page;
|
|
50
|
+
const ids = kept?.ids.join('\n') ?? '';
|
|
51
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: keyed on the saved colours and ids.
|
|
52
|
+
const initial = useMemo(() => {
|
|
53
|
+
if (!page)
|
|
54
|
+
return saved;
|
|
55
|
+
const mine = readRemembered()[page] ?? {};
|
|
56
|
+
const own = Object.entries(mine).filter(([id, c]) => ids.split('\n').includes(id) && isColor(c));
|
|
57
|
+
return { ...saved, ...Object.fromEntries(own) };
|
|
58
|
+
}, [JSON.stringify(saved), page, ids]);
|
|
59
|
+
const [colors, setColors] = useState(initial);
|
|
60
|
+
const [sources, setSources] = useState({});
|
|
61
|
+
// A saved layout that changes (another view, a save) starts from its colours again.
|
|
62
|
+
const key = JSON.stringify(initial);
|
|
63
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: keyed on the saved colours.
|
|
64
|
+
useEffect(() => setColors(initial), [key]);
|
|
65
|
+
// Stable, so a card's effects that publish never run again only because the page re-rendered.
|
|
66
|
+
const setColor = useCallback((id, color) => {
|
|
67
|
+
if (page && ids.split('\n').includes(id))
|
|
68
|
+
remember(page, id, color);
|
|
69
|
+
setColors((c) => ({ ...c, [id]: color }));
|
|
70
|
+
}, [page, ids]);
|
|
71
|
+
const publish = useCallback((id, tokens) => setSources((s) => JSON.stringify(s[id] ?? []) === JSON.stringify(tokens) ? s : { ...s, [id]: tokens }), []);
|
|
72
|
+
const links = useMemo(() => ({
|
|
73
|
+
colorOf: (id) => colors[id] ?? null,
|
|
74
|
+
setColor,
|
|
75
|
+
publish,
|
|
76
|
+
follows: (id) => {
|
|
77
|
+
const color = colors[id];
|
|
78
|
+
if (!color)
|
|
79
|
+
return [];
|
|
80
|
+
return Object.entries(sources).flatMap(([src, ts]) => src !== id && colors[src] === color ? ts : []);
|
|
81
|
+
},
|
|
82
|
+
}), [colors, sources, setColor, publish]);
|
|
83
|
+
return { links, colors };
|
|
84
|
+
}
|
|
85
|
+
/** The id of the card a part sits in, so a filter box in any card links without being told. */
|
|
86
|
+
const CardIdContext = createContext(null);
|
|
87
|
+
export const useCardId = () => useContext(CardIdContext);
|
|
88
|
+
export function CardIdProvider({ id, children }) {
|
|
89
|
+
return _jsx(CardIdContext.Provider, { value: id, children: children });
|
|
90
|
+
}
|
|
91
|
+
export function LinksProvider({ links, children }) {
|
|
92
|
+
return _jsx(LinksContext.Provider, { value: links, children: children });
|
|
93
|
+
}
|
|
94
|
+
/** A filter box's link from the card it sits in, when its card passes none (* the square is built into every card). Its linked chips are published as that card's. */
|
|
95
|
+
export function useAutoFilterLink(tokens, given) {
|
|
96
|
+
const links = useLinks();
|
|
97
|
+
const id = useCardId();
|
|
98
|
+
// `null`: a box that never shares (a widget's own fixed filter in Edit widget).
|
|
99
|
+
const auto = given === undefined && !!links && !!id;
|
|
100
|
+
usePublishLinks(auto ? id : '', auto ? tokens : NONE);
|
|
101
|
+
if (given === null)
|
|
102
|
+
return undefined;
|
|
103
|
+
if (given || !auto || !links || !id)
|
|
104
|
+
return given;
|
|
105
|
+
return { color: links.colorOf(id), onPick: (c) => links.setColor(id, c) };
|
|
106
|
+
}
|
|
107
|
+
const NONE = [];
|
|
108
|
+
/** The fields that stand for a key on any domain, so a chip on one is followed by that key. */
|
|
109
|
+
export const KEY_FIELDS = {
|
|
110
|
+
symbol: ['symbol', 'underlying', 'ticker'],
|
|
111
|
+
account: ['account', 'account_id'],
|
|
112
|
+
contract: ['contract'],
|
|
113
|
+
};
|
|
114
|
+
const keyOfField = (key) => Object.keys(KEY_FIELDS).find((k) => KEY_FIELDS[k].includes(key));
|
|
115
|
+
/** A coloured card's chips as its group gets them: its colour, and the key a key field stands for. */
|
|
116
|
+
export function shared(tokens, color) {
|
|
117
|
+
if (!color)
|
|
118
|
+
return [];
|
|
119
|
+
return tokens.map((t) => {
|
|
120
|
+
const as = t.as ?? keyOfField(t.key);
|
|
121
|
+
return { ...t, link: color, ...(as ? { as } : {}) };
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
/** Publishes a coloured card's chips while it shows, and takes them back when it goes. */
|
|
125
|
+
export function usePublishLinks(id, tokens) {
|
|
126
|
+
const links = useLinks();
|
|
127
|
+
const color = links?.colorOf(id) ?? null;
|
|
128
|
+
const linked = useMemo(() => shared(tokens, color), [color, tokens]);
|
|
129
|
+
const publish = links?.publish;
|
|
130
|
+
useEffect(() => publish?.(id, linked), [publish, id, linked]);
|
|
131
|
+
useEffect(() => () => publish?.(id, []), [publish, id]);
|
|
132
|
+
}
|
|
133
|
+
/** A domain's keys as filter fields (`@symbol`, `@account`), so a followed chip reads by key. */
|
|
134
|
+
export function keyFilterFields(keys) {
|
|
135
|
+
return Object.entries(keys ?? {}).map(([k, read]) => ({
|
|
136
|
+
key: `@${k}`,
|
|
137
|
+
label: k === 'symbol' ? 'Symbol' : k === 'account' ? 'Account' : 'Option',
|
|
138
|
+
icon: LinkIcon,
|
|
139
|
+
values: (row) => {
|
|
140
|
+
const v = read(row);
|
|
141
|
+
return v === null ? [] : Array.isArray(v) ? v : [v];
|
|
142
|
+
},
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* The followed chips a card can read: those on a field it has, then those on a key it carries
|
|
147
|
+
* (read on `@<key>`, see `keyFilterFields`). A chip it can read neither way is skipped.
|
|
148
|
+
*/
|
|
149
|
+
export function followedBy(tokens, fields, keys) {
|
|
150
|
+
return tokens.flatMap((t) => fields.some((f) => f.key === t.key)
|
|
151
|
+
? [t]
|
|
152
|
+
: t.as && keys?.[t.as]
|
|
153
|
+
? [{ ...t, key: `@${t.as}` }]
|
|
154
|
+
: []);
|
|
155
|
+
}
|
|
156
|
+
/** What a card follows, as its title says it: `IRA, Roth`. */
|
|
157
|
+
export function followsText(tokens) {
|
|
158
|
+
return tokens
|
|
159
|
+
.map((t) => `${t.not ? 'not ' : ''}${t.op ? `${t.key} ${t.value.replace('..', '–')}` : (t.text ?? t.value)}`)
|
|
160
|
+
.join(', ');
|
|
161
|
+
}
|
|
162
|
+
/** The square itself: filled in its colour, or an outline for none. */
|
|
163
|
+
export function LinkDot({ color, className }) {
|
|
164
|
+
return (_jsx("span", { "aria-hidden": true, className: cn('inline-block size-2.5 shrink-0 rounded-[3px]', !color && 'ring-[1.5px] ring-muted-foreground/60 ring-inset', className), style: color ? { background: linkVar(color) } : undefined }));
|
|
165
|
+
}
|
|
166
|
+
/** A card's square: a tap opens the colours and None; a pick applies at once. */
|
|
167
|
+
export function LinkPicker({ color, onPick, label, className, }) {
|
|
168
|
+
const [open, setOpen] = useState(false);
|
|
169
|
+
const pick = (c) => {
|
|
170
|
+
onPick(c);
|
|
171
|
+
setOpen(false);
|
|
172
|
+
};
|
|
173
|
+
return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon-xs", className: cn('size-5 shrink-0', className), "aria-label": `${label} link colour: ${color ? LINK_NAMES[color] : 'None'}`, title: color ? `Linked: ${LINK_NAMES[color]}` : 'Link colour', children: _jsx(LinkDot, { color: color }) }) }), _jsxs(PopoverContent, { align: "start", className: "flex w-auto flex-col gap-2 p-2", children: [_jsx("p", { className: "text-xs text-muted-foreground", children: "Link colour" }), _jsxs("div", { className: "flex items-center gap-1", children: [LINK_COLORS.map((c) => (_jsx(Button, { variant: c === color ? 'secondary' : 'ghost', size: "icon-sm", "aria-label": LINK_NAMES[c], "aria-pressed": c === color, title: LINK_NAMES[c], onClick: () => pick(c), children: _jsx(LinkDot, { color: c, className: "size-3.5" }) }, c))), _jsx(Button, { variant: color === null ? 'secondary' : 'ghost', size: "sm", className: "h-8 px-2 text-xs", "aria-pressed": color === null, onClick: () => pick(null), children: "None" })] })] })] }));
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Row-tap filtering (keyed): in a coloured card a row tap picks the
|
|
177
|
+
* row's value as a temporary chip of the card's colour (published with its other chips); a second
|
|
178
|
+
* tap on it, or the card losing its colour, lets go. The value is the domain's `rowLink` field, else
|
|
179
|
+
* its first key (symbol, then account), so every domain with a key links rows (a basket's orders,
|
|
180
|
+
* paper and live). `onTap` is undefined where it does not apply, so the row opens as usual.
|
|
181
|
+
*/
|
|
182
|
+
export function useRowPick(domain, color,
|
|
183
|
+
/** The value tapped until a tap changes it (a page that opens on one row). */
|
|
184
|
+
initial) {
|
|
185
|
+
const field = domain.rowLink ? domain.fields.find((f) => f.key === domain.rowLink) : undefined;
|
|
186
|
+
const key = field ? undefined : ['symbol', 'account'].find((k) => domain.keys?.[k]);
|
|
187
|
+
const read = key ? domain.keys?.[key] : undefined;
|
|
188
|
+
// Until the first tap the page's own start holds; a tap, or the card losing its colour, ends it.
|
|
189
|
+
const [tap, setTap] = useState(null);
|
|
190
|
+
const on = !!((field || read) && color);
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
if (!on)
|
|
193
|
+
setTap(null);
|
|
194
|
+
}, [on]);
|
|
195
|
+
const value = tap ? tap.value : (initial ?? null);
|
|
196
|
+
const keyOf = useCallback((row) => {
|
|
197
|
+
const v = field ? field.value(row) : read?.(row);
|
|
198
|
+
const one = Array.isArray(v) ? v[0] : v;
|
|
199
|
+
return one === null || one === undefined ? null : String(one);
|
|
200
|
+
}, [field, read]);
|
|
201
|
+
const token = useMemo(() => {
|
|
202
|
+
if (!on || !value)
|
|
203
|
+
return [];
|
|
204
|
+
if (field)
|
|
205
|
+
return [{ key: field.key, value, link: color ?? undefined }];
|
|
206
|
+
return key ? [{ key: `@${key}`, value, as: key, link: color ?? undefined }] : [];
|
|
207
|
+
}, [on, value, field, key, color]);
|
|
208
|
+
return {
|
|
209
|
+
token,
|
|
210
|
+
onTap: on ? (row) => setTap({ value: value === keyOf(row) ? null : keyOf(row) }) : undefined,
|
|
211
|
+
tapped: on && value ? (row) => keyOf(row) === value : undefined,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { UseQueryResult } from '@tanstack/react-query';
|
|
2
|
+
import type { LucideIcon } from 'lucide-react';
|
|
3
|
+
import type { Crumb } from './page.js';
|
|
4
|
+
export declare function LumenPageState({ crumbs, title, icon, query, waiting, error, notFound, tabs, }: {
|
|
5
|
+
/** The crumbs before the page's own; the last one says …, Not found or Could not load. */
|
|
6
|
+
crumbs: Crumb[];
|
|
7
|
+
/** The placeholder card's name and icon, as the page's first card shows them. */
|
|
8
|
+
title: string;
|
|
9
|
+
icon: LucideIcon;
|
|
10
|
+
/** What the page waits on; none when the address itself is wrong. */
|
|
11
|
+
query?: Pick<UseQueryResult<unknown, Error>, 'data' | 'error' | 'isError' | 'isFetching' | 'isPending' | 'dataUpdatedAt' | 'refetch'>;
|
|
12
|
+
/** Something else the page needs is not known yet (whether the app has the assistant). */
|
|
13
|
+
waiting?: boolean;
|
|
14
|
+
/** A known reason the page cannot load, shown in place of Not found. */
|
|
15
|
+
error?: Error | null;
|
|
16
|
+
/** Said once the page is proven missing. */
|
|
17
|
+
notFound?: string;
|
|
18
|
+
/** The page has a tab row: its skeleton shows above the card while loading. */
|
|
19
|
+
tabs?: boolean;
|
|
20
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ApiError } from './api-fetch.js';
|
|
3
|
+
import { LumenLayout } from './lumen-layout.js';
|
|
4
|
+
import { useLumenLoad } from './lumen-source.js';
|
|
5
|
+
import { Skeleton } from './ui/skeleton.js';
|
|
6
|
+
export function LumenPageState({ crumbs, title, icon, query, waiting = false, error, notFound = 'Nothing here with this id. It may have been deleted.', tabs = false, }) {
|
|
7
|
+
const read = useLumenLoad(...(query ? [query] : []));
|
|
8
|
+
const failed = query?.isError ? query.error : null;
|
|
9
|
+
const loading = !error && (waiting || (!!query?.isPending && !failed));
|
|
10
|
+
const missing = !loading && !error && (!failed || (failed instanceof ApiError && failed.status === 404));
|
|
11
|
+
const load = loading
|
|
12
|
+
? { ...read, loading: true, fetching: true, error: null }
|
|
13
|
+
: { ...read, loading: false, error: missing ? new Error(notFound) : (error ?? failed) };
|
|
14
|
+
return (_jsx(LumenLayout, { crumbs: [...crumbs, { label: loading ? '…' : missing ? 'Not found' : 'Could not load' }], intro: tabs && loading ? (_jsxs("div", { "aria-busy": "true", "aria-label": "Loading tabs", className: "flex gap-2", children: [_jsx(Skeleton, { className: "h-8 w-28" }), _jsx(Skeleton, { className: "h-8 w-24" }), _jsx(Skeleton, { className: "h-8 w-20" })] })) : undefined, cards: [{ id: 'page', link: false, title, icon, kind: 'table', load, children: null }] }));
|
|
15
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { type ComponentType, type ReactNode, type Ref } from 'react';
|
|
2
|
+
type Icon = ComponentType<{
|
|
3
|
+
className?: string;
|
|
4
|
+
}>;
|
|
5
|
+
export interface LumenOption<V extends string = string> {
|
|
6
|
+
value: V;
|
|
7
|
+
label: string;
|
|
8
|
+
/** Section heading; rows with the same group sit together, groups in first-seen order. */
|
|
9
|
+
group?: string;
|
|
10
|
+
/** Second column on the right: "Screeners", "34d", "Fidelity · IRA". */
|
|
11
|
+
detail?: string;
|
|
12
|
+
icon?: Icon;
|
|
13
|
+
/** Drawn in place of the label in the row and the trigger (a model's name with its badge). */
|
|
14
|
+
content?: ReactNode;
|
|
15
|
+
/** A second line under the label, in the list only: when to pick it (the
|
|
16
|
+
* Widget picker). A row with one is taller. */
|
|
17
|
+
description?: string;
|
|
18
|
+
/** A small picture before the label, in the list only (a widget style's thumbnail). */
|
|
19
|
+
thumb?: ReactNode;
|
|
20
|
+
/** Extra words search matches. */
|
|
21
|
+
keywords?: string[];
|
|
22
|
+
/** Not pickable; a string says why (tooltip). */
|
|
23
|
+
disabled?: boolean | string;
|
|
24
|
+
/** Sits one step in, under the row before it (a workspace's tabs). */
|
|
25
|
+
indent?: boolean;
|
|
26
|
+
/** What the option stands for on screen, lit while hovered: `card:<id>` or an app path. */
|
|
27
|
+
target?: string;
|
|
28
|
+
/** The name tag on the lit target; the label by default. */
|
|
29
|
+
targetName?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface LumenOptionGroup<V extends string = string> {
|
|
32
|
+
label: string;
|
|
33
|
+
options: readonly LumenOption<V>[];
|
|
34
|
+
}
|
|
35
|
+
interface Common<V extends string> {
|
|
36
|
+
/** The options; or `groups` to fix the group order. */
|
|
37
|
+
options?: readonly LumenOption<V>[];
|
|
38
|
+
groups?: readonly LumenOptionGroup<V>[];
|
|
39
|
+
/** `field` by default. */
|
|
40
|
+
trigger?: 'field' | 'chip' | 'icon';
|
|
41
|
+
/** The icon trigger's icon, or a leading icon in a field or chip. */
|
|
42
|
+
icon?: Icon;
|
|
43
|
+
/** `auto`: a search box past 8 options. */
|
|
44
|
+
search?: 'auto' | boolean;
|
|
45
|
+
/** Turns typed text into a new value (a ticker, "ema 13", a new sector); null refuses it. */
|
|
46
|
+
create?: (query: string) => V | null;
|
|
47
|
+
/** The create row's text; `Add "…"` by default. */
|
|
48
|
+
createLabel?: (query: string) => string;
|
|
49
|
+
placeholder?: string;
|
|
50
|
+
/** Shown when nothing matches. */
|
|
51
|
+
empty?: string;
|
|
52
|
+
/** Accessible name; also the phone sheet's title. */
|
|
53
|
+
label?: string;
|
|
54
|
+
size?: 'sm' | 'default';
|
|
55
|
+
className?: string;
|
|
56
|
+
disabled?: boolean;
|
|
57
|
+
/** Above the list (a scope switch such as "for SPY / All"). */
|
|
58
|
+
header?: ReactNode;
|
|
59
|
+
align?: 'start' | 'end';
|
|
60
|
+
open?: boolean;
|
|
61
|
+
onOpenChange?: (open: boolean) => void;
|
|
62
|
+
/** A row was highlighted (hover or arrow keys): a caller reads it ahead (the assistant's Chats). */
|
|
63
|
+
onHighlight?: (value: V) => void;
|
|
64
|
+
id?: string;
|
|
65
|
+
}
|
|
66
|
+
export interface LumenSelectorSingle<V extends string> extends Common<V> {
|
|
67
|
+
multiple?: false;
|
|
68
|
+
value: V | null | undefined | '';
|
|
69
|
+
onChange: (value: V) => void;
|
|
70
|
+
}
|
|
71
|
+
export interface LumenSelectorMulti<V extends string> extends Common<V> {
|
|
72
|
+
multiple: true;
|
|
73
|
+
value: readonly V[];
|
|
74
|
+
onChange: (value: V[]) => void;
|
|
75
|
+
/** At most this many picked; the rest of the list greys out. */
|
|
76
|
+
max?: number;
|
|
77
|
+
}
|
|
78
|
+
export type LumenSelectorProps<V extends string = string> = LumenSelectorSingle<V> | LumenSelectorMulti<V>;
|
|
79
|
+
interface Shown<V extends string> {
|
|
80
|
+
label: string;
|
|
81
|
+
rows: LumenOption<V>[];
|
|
82
|
+
more: number;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* How well `q` matches `s` as a subsequence (`prc` finds Price), higher is better; null when it
|
|
86
|
+
* doesn't. Letters in a row and at word starts score more, a prefix most.
|
|
87
|
+
*/
|
|
88
|
+
export declare function fuzzyScore(q: string, s: string): number | null;
|
|
89
|
+
/**
|
|
90
|
+
* What the list shows: matching rows by group, a long group cut to 6 (plus any picked row) until
|
|
91
|
+
* it is opened or searched. Shared by every trigger so rows behave the same.
|
|
92
|
+
*/
|
|
93
|
+
export declare function useSelectorRows<V extends string>(options: readonly LumenOption<V>[] | undefined, groups: readonly LumenOptionGroup<V>[] | undefined, query: string, picked: ReadonlySet<string>,
|
|
94
|
+
/** Match letters in order, not a substring, and put the best matches first. */
|
|
95
|
+
fuzzy?: boolean): {
|
|
96
|
+
all: LumenOptionGroup<V>[];
|
|
97
|
+
shown: Shown<V>[];
|
|
98
|
+
count: number;
|
|
99
|
+
flat: LumenOption<V>[];
|
|
100
|
+
expand: (label: string) => void;
|
|
101
|
+
};
|
|
102
|
+
/** One row's inside: box or icon, label, detail, and the check when picked. */
|
|
103
|
+
export declare function SelectorRowContent({ option, selected, multiple, trailing, }: {
|
|
104
|
+
option: LumenOption;
|
|
105
|
+
selected?: boolean;
|
|
106
|
+
multiple?: boolean;
|
|
107
|
+
trailing?: ReactNode;
|
|
108
|
+
}): import("react").JSX.Element;
|
|
109
|
+
/** The one picker. See the file header for what every use gets. */
|
|
110
|
+
export declare function LumenSelector<V extends string = string>(props: LumenSelectorProps<V>): import("react").JSX.Element;
|
|
111
|
+
/** What a text box calls on keys while an `LumenSelectorList` shows. */
|
|
112
|
+
export interface LumenSelectorListHandle {
|
|
113
|
+
/** Handles arrows, Enter, Tab and Esc; true when the key was the list's. */
|
|
114
|
+
key: (e: {
|
|
115
|
+
key: string;
|
|
116
|
+
shiftKey?: boolean;
|
|
117
|
+
preventDefault: () => void;
|
|
118
|
+
}) => boolean;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* `trigger="none"`: the list anchored to a text box the page owns (the assistant @ and /). The text box
|
|
122
|
+
* passes the query and its keys (`handle.key`); Tab or Enter takes the highlighted row, the first
|
|
123
|
+
* by default.
|
|
124
|
+
*/
|
|
125
|
+
export declare function LumenSelectorList<V extends string = string>({ options, groups, query, onPick, onClose, empty, header, className, handle, label, fuzzy, }: {
|
|
126
|
+
options?: readonly LumenOption<V>[];
|
|
127
|
+
groups?: readonly LumenOptionGroup<V>[];
|
|
128
|
+
query: string;
|
|
129
|
+
onPick: (option: LumenOption<V>) => void;
|
|
130
|
+
onClose?: () => void;
|
|
131
|
+
empty?: string;
|
|
132
|
+
header?: ReactNode;
|
|
133
|
+
className?: string;
|
|
134
|
+
handle?: Ref<LumenSelectorListHandle>;
|
|
135
|
+
label?: string;
|
|
136
|
+
/** Letters in order match (`prc` finds Price), best first: autocomplete in a text box. */
|
|
137
|
+
fuzzy?: boolean;
|
|
138
|
+
}): import("react").JSX.Element;
|
|
139
|
+
export {};
|