rastack 0.0.43 → 0.0.45
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 +14 -0
- package/admin/AdminApp.tsx +461 -0
- package/admin/adapters.ts +346 -0
- package/admin/httpEngine.ts +56 -0
- package/admin/index.tsx +140 -0
- package/admin/ui/FilterBar.tsx +231 -0
- package/admin/ui/Popover.tsx +54 -0
- package/admin/ui/Sidebar.tsx +86 -0
- package/admin/ui/icons.tsx +146 -0
- package/admin/ui/styles.ts +398 -0
- package/admin/useAdmin.ts +434 -0
- package/dist/admin.js +459 -0
- package/dist/dev/harness.d.ts +70 -0
- package/dist/dev/harness.js +189 -0
- package/dist/rastack-admin.d.ts +14 -0
- package/dist/rastack-admin.js +128 -0
- package/dist/rastack-dev.d.ts +18 -0
- package/dist/rastack-dev.js +197 -0
- package/dist/rastack-wasm-build.d.ts +9 -4
- package/dist/rastack-wasm-build.js +44 -33
- package/dist/rastack.d.ts +2 -3
- package/dist/rastack.js +13 -32
- package/dist/wasm/package.json +4 -0
- package/dist/wasm/rastack_wasm.d.ts +100 -0
- package/dist/wasm/rastack_wasm.js +745 -0
- package/dist/wasm/rastack_wasm_bg.wasm +0 -0
- package/dist/wasm/rastack_wasm_bg.wasm.d.ts +28 -0
- package/package.json +6 -2
- package/scripts/build-admin.js +48 -0
- package/scripts/copy-wasm.js +35 -0
- package/src/dev/harness.ts +174 -0
- package/src/rastack-admin.ts +112 -0
- package/src/rastack-dev.ts +197 -0
- package/src/rastack-wasm-build.ts +52 -41
- package/src/rastack.ts +14 -37
- package/test/dev.spec.ts +373 -0
- package/wasm/package.json +4 -0
- package/wasm/rastack_wasm.d.ts +100 -0
- package/wasm/rastack_wasm.js +745 -0
- package/wasm/rastack_wasm_bg.wasm +0 -0
- package/wasm/rastack_wasm_bg.wasm.d.ts +28 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Linear-style changelist filter bar: active filters render as pill "chips"
|
|
3
|
+
* (`Field is Value ×`) whose value opens a switcher popover and whose × drops
|
|
4
|
+
* the filter; a dashed "+ Filter" button opens a two-level menu (pick a field →
|
|
5
|
+
* pick a value). Search sits at the right. Every value is served from the
|
|
6
|
+
* `FilterFieldModel`s discovered by `useFilterModel` (FK target rows, distinct
|
|
7
|
+
* strings, booleans), so the bar reflects the data, and each pick drives an
|
|
8
|
+
* equality filter through the engine.
|
|
9
|
+
*/
|
|
10
|
+
import React, { useState } from "react";
|
|
11
|
+
import type { FilterOption } from "../adapters";
|
|
12
|
+
import type { FilterFieldModel } from "../useAdmin";
|
|
13
|
+
import { Popover } from "./Popover";
|
|
14
|
+
import {
|
|
15
|
+
IconChevronRight,
|
|
16
|
+
IconClose,
|
|
17
|
+
IconFilter,
|
|
18
|
+
IconLink,
|
|
19
|
+
IconPlus,
|
|
20
|
+
IconSearch,
|
|
21
|
+
} from "./icons";
|
|
22
|
+
|
|
23
|
+
function facetIcon(kind: FilterFieldModel["kind"]): React.JSX.Element {
|
|
24
|
+
return kind === "fk" ? (
|
|
25
|
+
<IconLink size={15} className="ras-ico" />
|
|
26
|
+
) : (
|
|
27
|
+
<IconFilter size={15} className="ras-ico" />
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function labelFor(facet: FilterFieldModel | undefined, value: string): string {
|
|
32
|
+
return facet?.options.find((o) => o.value === value)?.label ?? value;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A searchable list of value options (used by the add-menu and chip switcher). */
|
|
36
|
+
function OptionList(props: {
|
|
37
|
+
options: FilterOption[];
|
|
38
|
+
selected?: string;
|
|
39
|
+
onPick: (value: string) => void;
|
|
40
|
+
}): React.JSX.Element {
|
|
41
|
+
const [q, setQ] = useState("");
|
|
42
|
+
const shown = q
|
|
43
|
+
? props.options.filter((o) => o.label.toLowerCase().includes(q.toLowerCase()))
|
|
44
|
+
: props.options;
|
|
45
|
+
return (
|
|
46
|
+
<>
|
|
47
|
+
{props.options.length > 8 ? (
|
|
48
|
+
<input
|
|
49
|
+
className="ras-pop-search"
|
|
50
|
+
placeholder="Search…"
|
|
51
|
+
value={q}
|
|
52
|
+
onChange={(e) => setQ(e.target.value)}
|
|
53
|
+
autoFocus
|
|
54
|
+
/>
|
|
55
|
+
) : null}
|
|
56
|
+
<div className="ras-pop-list">
|
|
57
|
+
{shown.length === 0 ? (
|
|
58
|
+
<div className="ras-pop-empty">No matches</div>
|
|
59
|
+
) : (
|
|
60
|
+
shown.map((o) => (
|
|
61
|
+
<button
|
|
62
|
+
key={o.value}
|
|
63
|
+
type="button"
|
|
64
|
+
className={"ras-pop-item" + (o.value === props.selected ? " is-selected" : "")}
|
|
65
|
+
onClick={() => props.onPick(o.value)}
|
|
66
|
+
>
|
|
67
|
+
{o.label}
|
|
68
|
+
</button>
|
|
69
|
+
))
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
72
|
+
</>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The two-level "add filter" menu: fields → values. */
|
|
77
|
+
function AddFilterMenu(props: {
|
|
78
|
+
facets: FilterFieldModel[];
|
|
79
|
+
activeFields: string[];
|
|
80
|
+
onPick: (field: string, value: string) => void;
|
|
81
|
+
close: () => void;
|
|
82
|
+
}): React.JSX.Element {
|
|
83
|
+
const [view, setView] = useState<string | null>(null);
|
|
84
|
+
const available = props.facets.filter((f) => !props.activeFields.includes(f.name));
|
|
85
|
+
|
|
86
|
+
if (view) {
|
|
87
|
+
const facet = props.facets.find((f) => f.name === view);
|
|
88
|
+
if (!facet) return <div className="ras-pop-empty">—</div>;
|
|
89
|
+
return (
|
|
90
|
+
<>
|
|
91
|
+
<div className="ras-pop-head" onClick={() => setView(null)}>
|
|
92
|
+
<IconChevronRight size={14} style={{ transform: "rotate(180deg)" }} />
|
|
93
|
+
{facet.label}
|
|
94
|
+
</div>
|
|
95
|
+
<OptionList
|
|
96
|
+
options={facet.options}
|
|
97
|
+
onPick={(v) => {
|
|
98
|
+
props.onPick(facet.name, v);
|
|
99
|
+
props.close();
|
|
100
|
+
}}
|
|
101
|
+
/>
|
|
102
|
+
</>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<div className="ras-pop-list">
|
|
108
|
+
{available.length === 0 ? (
|
|
109
|
+
<div className="ras-pop-empty">No more filters</div>
|
|
110
|
+
) : (
|
|
111
|
+
available.map((f) => (
|
|
112
|
+
<button
|
|
113
|
+
key={f.name}
|
|
114
|
+
type="button"
|
|
115
|
+
className="ras-pop-item"
|
|
116
|
+
onClick={() => setView(f.name)}
|
|
117
|
+
>
|
|
118
|
+
{facetIcon(f.kind)}
|
|
119
|
+
{f.label}
|
|
120
|
+
<IconChevronRight size={14} className="ras-pop-item-chev" />
|
|
121
|
+
</button>
|
|
122
|
+
))
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function FilterBar(props: {
|
|
129
|
+
facets: FilterFieldModel[];
|
|
130
|
+
filters: Record<string, string>;
|
|
131
|
+
setFilter: (field: string, value: string) => void;
|
|
132
|
+
removeFilter: (field: string) => void;
|
|
133
|
+
clearFilters: () => void;
|
|
134
|
+
search: string | undefined;
|
|
135
|
+
setSearch: (value: string | undefined) => void;
|
|
136
|
+
searchable: boolean;
|
|
137
|
+
}): React.JSX.Element {
|
|
138
|
+
const active = Object.keys(props.filters);
|
|
139
|
+
const facetOf = (name: string) => props.facets.find((f) => f.name === name);
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<div className="ras-filterbar">
|
|
143
|
+
{active.map((field) => {
|
|
144
|
+
const facet = facetOf(field);
|
|
145
|
+
const value = props.filters[field];
|
|
146
|
+
return (
|
|
147
|
+
<span className="ras-chip" key={field}>
|
|
148
|
+
<span className="ras-chip-key">
|
|
149
|
+
{facet ? facetIcon(facet.kind) : null}
|
|
150
|
+
{facet?.label ?? field}
|
|
151
|
+
</span>
|
|
152
|
+
<span className="ras-chip-op">is</span>
|
|
153
|
+
{facet && facet.options.length ? (
|
|
154
|
+
<Popover
|
|
155
|
+
trigger={({ toggle }) => (
|
|
156
|
+
<span className="ras-chip-val" onClick={toggle} role="button" tabIndex={0}>
|
|
157
|
+
{labelFor(facet, value)}
|
|
158
|
+
</span>
|
|
159
|
+
)}
|
|
160
|
+
>
|
|
161
|
+
{({ close }) => (
|
|
162
|
+
<OptionList
|
|
163
|
+
options={facet.options}
|
|
164
|
+
selected={value}
|
|
165
|
+
onPick={(v) => {
|
|
166
|
+
props.setFilter(field, v);
|
|
167
|
+
close();
|
|
168
|
+
}}
|
|
169
|
+
/>
|
|
170
|
+
)}
|
|
171
|
+
</Popover>
|
|
172
|
+
) : (
|
|
173
|
+
<span className="ras-chip-val" style={{ cursor: "default" }}>
|
|
174
|
+
{labelFor(facet, value)}
|
|
175
|
+
</span>
|
|
176
|
+
)}
|
|
177
|
+
<span
|
|
178
|
+
className="ras-chip-x"
|
|
179
|
+
role="button"
|
|
180
|
+
tabIndex={0}
|
|
181
|
+
aria-label={`Remove ${facet?.label ?? field} filter`}
|
|
182
|
+
onClick={() => props.removeFilter(field)}
|
|
183
|
+
>
|
|
184
|
+
<IconClose size={13} />
|
|
185
|
+
</span>
|
|
186
|
+
</span>
|
|
187
|
+
);
|
|
188
|
+
})}
|
|
189
|
+
|
|
190
|
+
{props.facets.length ? (
|
|
191
|
+
<Popover
|
|
192
|
+
trigger={({ toggle }) => (
|
|
193
|
+
<button type="button" className="ras-addfilter" onClick={toggle}>
|
|
194
|
+
<IconPlus size={14} />
|
|
195
|
+
Filter
|
|
196
|
+
</button>
|
|
197
|
+
)}
|
|
198
|
+
>
|
|
199
|
+
{({ close }) => (
|
|
200
|
+
<AddFilterMenu
|
|
201
|
+
facets={props.facets}
|
|
202
|
+
activeFields={active}
|
|
203
|
+
onPick={props.setFilter}
|
|
204
|
+
close={close}
|
|
205
|
+
/>
|
|
206
|
+
)}
|
|
207
|
+
</Popover>
|
|
208
|
+
) : null}
|
|
209
|
+
|
|
210
|
+
{active.length > 1 ? (
|
|
211
|
+
<button type="button" className="ras-clearfilter" onClick={props.clearFilters}>
|
|
212
|
+
Clear all
|
|
213
|
+
</button>
|
|
214
|
+
) : null}
|
|
215
|
+
|
|
216
|
+
<span className="ras-grow" />
|
|
217
|
+
|
|
218
|
+
{props.searchable ? (
|
|
219
|
+
<span className="ras-search">
|
|
220
|
+
<IconSearch size={15} className="ras-search-ico" />
|
|
221
|
+
<input
|
|
222
|
+
type="search"
|
|
223
|
+
placeholder="Search…"
|
|
224
|
+
value={props.search ?? ""}
|
|
225
|
+
onChange={(e) => props.setSearch(e.target.value || undefined)}
|
|
226
|
+
/>
|
|
227
|
+
</span>
|
|
228
|
+
) : null}
|
|
229
|
+
</div>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tiny controlled popover: a trigger + a floating panel that closes on
|
|
3
|
+
* outside-click or Escape. Used by the Linear-style filter bar (the "add
|
|
4
|
+
* filter" menu and each chip's value switcher). Deliberately dependency-free —
|
|
5
|
+
* no portal, no positioning lib — the panel is absolutely positioned under its
|
|
6
|
+
* trigger and styled by `ui/styles.ts`.
|
|
7
|
+
*/
|
|
8
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
9
|
+
|
|
10
|
+
export interface PopoverRenderProps {
|
|
11
|
+
open: boolean;
|
|
12
|
+
toggle: () => void;
|
|
13
|
+
close: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function Popover(props: {
|
|
17
|
+
align?: "left" | "right";
|
|
18
|
+
trigger: (p: PopoverRenderProps) => React.ReactNode;
|
|
19
|
+
children: (p: { close: () => void }) => React.ReactNode;
|
|
20
|
+
}): React.JSX.Element {
|
|
21
|
+
const [open, setOpen] = useState(false);
|
|
22
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
23
|
+
const close = () => setOpen(false);
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (!open) return;
|
|
27
|
+
const onDown = (e: MouseEvent) => {
|
|
28
|
+
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
|
|
29
|
+
};
|
|
30
|
+
const onKey = (e: KeyboardEvent) => {
|
|
31
|
+
if (e.key === "Escape") setOpen(false);
|
|
32
|
+
};
|
|
33
|
+
document.addEventListener("mousedown", onDown);
|
|
34
|
+
document.addEventListener("keydown", onKey);
|
|
35
|
+
return () => {
|
|
36
|
+
document.removeEventListener("mousedown", onDown);
|
|
37
|
+
document.removeEventListener("keydown", onKey);
|
|
38
|
+
};
|
|
39
|
+
}, [open]);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="ras-pop-wrap" ref={ref}>
|
|
43
|
+
{props.trigger({ open, toggle: () => setOpen((o) => !o), close })}
|
|
44
|
+
{open ? (
|
|
45
|
+
<div
|
|
46
|
+
className={"ras-pop" + (props.align === "right" ? " align-right" : "")}
|
|
47
|
+
role="menu"
|
|
48
|
+
>
|
|
49
|
+
{props.children({ close })}
|
|
50
|
+
</div>
|
|
51
|
+
) : null}
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The persistent left rail — the "models on the side". Every admin-enabled
|
|
3
|
+
* resource, grouped by app, always visible so navigation between models is one
|
|
4
|
+
* click from anywhere. The active model is highlighted; a footer carries the
|
|
5
|
+
* light/dark toggle (dogfooding `rastack/theme`'s `useThemeMode`, which
|
|
6
|
+
* recascades via CSS variables with no re-render).
|
|
7
|
+
*/
|
|
8
|
+
import React, { useMemo } from "react";
|
|
9
|
+
import { useThemeMode } from "../../theme";
|
|
10
|
+
import { humanizeKey } from "../../components";
|
|
11
|
+
import type { AdminResource } from "../adapters";
|
|
12
|
+
import { adminResources } from "../adapters";
|
|
13
|
+
import { BrandMark, IconMoon, IconSun, IconTable } from "./icons";
|
|
14
|
+
|
|
15
|
+
export function Sidebar(props: {
|
|
16
|
+
resources: AdminResource[];
|
|
17
|
+
activeKey?: string;
|
|
18
|
+
}): React.JSX.Element {
|
|
19
|
+
const [mode, setMode] = useThemeMode();
|
|
20
|
+
|
|
21
|
+
const byApp = useMemo(() => {
|
|
22
|
+
const map = new Map<string, AdminResource[]>();
|
|
23
|
+
for (const r of adminResources(props.resources)) {
|
|
24
|
+
const list = map.get(r.app) ?? [];
|
|
25
|
+
list.push(r);
|
|
26
|
+
map.set(r.app, list);
|
|
27
|
+
}
|
|
28
|
+
return [...map.entries()]
|
|
29
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
30
|
+
.map(([app, models]) => [
|
|
31
|
+
app,
|
|
32
|
+
models.slice().sort((a, b) => a.model.localeCompare(b.model)),
|
|
33
|
+
] as const);
|
|
34
|
+
}, [props.resources]);
|
|
35
|
+
|
|
36
|
+
const isDark = mode === "dark";
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<aside className="ras-sidebar">
|
|
40
|
+
<a className="ras-brand" href="#/" aria-label="React API Stack admin">
|
|
41
|
+
<BrandMark size={32} />
|
|
42
|
+
<span>
|
|
43
|
+
<span className="ras-brand-name" style={{ display: "block" }}>
|
|
44
|
+
React API Stack
|
|
45
|
+
</span>
|
|
46
|
+
<span className="ras-brand-sub">Admin</span>
|
|
47
|
+
</span>
|
|
48
|
+
</a>
|
|
49
|
+
|
|
50
|
+
<nav className="ras-nav">
|
|
51
|
+
{byApp.map(([app, models]) => (
|
|
52
|
+
<div className="ras-nav-section" key={app}>
|
|
53
|
+
<div className="ras-nav-label">{humanizeKey(app)}</div>
|
|
54
|
+
{models.map((r) => {
|
|
55
|
+
const active = r.key === props.activeKey;
|
|
56
|
+
return (
|
|
57
|
+
<a
|
|
58
|
+
key={r.key}
|
|
59
|
+
href={`#/${r.app}/${r.model}/`}
|
|
60
|
+
className={"ras-nav-link" + (active ? " is-active" : "")}
|
|
61
|
+
aria-current={active ? "page" : undefined}
|
|
62
|
+
>
|
|
63
|
+
<IconTable size={16} className="ras-ico" />
|
|
64
|
+
{r.label}
|
|
65
|
+
</a>
|
|
66
|
+
);
|
|
67
|
+
})}
|
|
68
|
+
</div>
|
|
69
|
+
))}
|
|
70
|
+
</nav>
|
|
71
|
+
|
|
72
|
+
<div className="ras-sidebar-foot">
|
|
73
|
+
<span className="ras-foot-meta">Iceberg · object storage</span>
|
|
74
|
+
<button
|
|
75
|
+
type="button"
|
|
76
|
+
className="ras-icon-btn"
|
|
77
|
+
aria-label={isDark ? "Switch to light theme" : "Switch to dark theme"}
|
|
78
|
+
title={isDark ? "Light" : "Dark"}
|
|
79
|
+
onClick={() => setMode(isDark ? "light" : "dark")}
|
|
80
|
+
>
|
|
81
|
+
{isDark ? <IconSun size={16} /> : <IconMoon size={16} />}
|
|
82
|
+
</button>
|
|
83
|
+
</div>
|
|
84
|
+
</aside>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small set of inline stroke icons (currentColor, 1.6px) used across the
|
|
3
|
+
* admin chrome. Kept local to the admin — the shared `rastack/components` are
|
|
4
|
+
* intentionally icon-free — so the bundle stays self-contained and offline.
|
|
5
|
+
*/
|
|
6
|
+
import React from "react";
|
|
7
|
+
|
|
8
|
+
type SvgProps = {
|
|
9
|
+
size?: number;
|
|
10
|
+
strokeWidth?: number;
|
|
11
|
+
style?: React.CSSProperties;
|
|
12
|
+
className?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function Svg({
|
|
16
|
+
size = 16,
|
|
17
|
+
strokeWidth = 1.6,
|
|
18
|
+
children,
|
|
19
|
+
style,
|
|
20
|
+
className,
|
|
21
|
+
}: SvgProps & { children: React.ReactNode }): React.JSX.Element {
|
|
22
|
+
return (
|
|
23
|
+
<svg
|
|
24
|
+
width={size}
|
|
25
|
+
height={size}
|
|
26
|
+
viewBox="0 0 24 24"
|
|
27
|
+
fill="none"
|
|
28
|
+
stroke="currentColor"
|
|
29
|
+
strokeWidth={strokeWidth}
|
|
30
|
+
strokeLinecap="round"
|
|
31
|
+
strokeLinejoin="round"
|
|
32
|
+
style={{ display: "block", flexShrink: 0, ...style }}
|
|
33
|
+
className={className}
|
|
34
|
+
aria-hidden="true"
|
|
35
|
+
>
|
|
36
|
+
{children}
|
|
37
|
+
</svg>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const IconSearch = (p: SvgProps) => (
|
|
42
|
+
<Svg {...p}>
|
|
43
|
+
<circle cx="11" cy="11" r="7" />
|
|
44
|
+
<path d="m20 20-3.2-3.2" />
|
|
45
|
+
</Svg>
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
export const IconFilter = (p: SvgProps) => (
|
|
49
|
+
<Svg {...p}>
|
|
50
|
+
<path d="M3 5h18" />
|
|
51
|
+
<path d="M6 12h12" />
|
|
52
|
+
<path d="M10 19h4" />
|
|
53
|
+
</Svg>
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
export const IconPlus = (p: SvgProps) => (
|
|
57
|
+
<Svg {...p}>
|
|
58
|
+
<path d="M12 5v14M5 12h14" />
|
|
59
|
+
</Svg>
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
export const IconChevronRight = (p: SvgProps) => (
|
|
63
|
+
<Svg {...p}>
|
|
64
|
+
<path d="m9 6 6 6-6 6" />
|
|
65
|
+
</Svg>
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
export const IconChevronDown = (p: SvgProps) => (
|
|
69
|
+
<Svg {...p}>
|
|
70
|
+
<path d="m6 9 6 6 6-6" />
|
|
71
|
+
</Svg>
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
export const IconArrowLeft = (p: SvgProps) => (
|
|
75
|
+
<Svg {...p}>
|
|
76
|
+
<path d="M19 12H5M11 18l-6-6 6-6" />
|
|
77
|
+
</Svg>
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
export const IconClose = (p: SvgProps) => (
|
|
81
|
+
<Svg {...p}>
|
|
82
|
+
<path d="M18 6 6 18M6 6l12 12" />
|
|
83
|
+
</Svg>
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
export const IconTrash = (p: SvgProps) => (
|
|
87
|
+
<Svg {...p}>
|
|
88
|
+
<path d="M4 7h16M10 11v6M14 11v6" />
|
|
89
|
+
<path d="M5 7l1 13a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1l1-13" />
|
|
90
|
+
<path d="M9 7V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v3" />
|
|
91
|
+
</Svg>
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
export const IconSun = (p: SvgProps) => (
|
|
95
|
+
<Svg {...p}>
|
|
96
|
+
<circle cx="12" cy="12" r="4" />
|
|
97
|
+
<path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" />
|
|
98
|
+
</Svg>
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
export const IconMoon = (p: SvgProps) => (
|
|
102
|
+
<Svg {...p}>
|
|
103
|
+
<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8Z" />
|
|
104
|
+
</Svg>
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
export const IconTable = (p: SvgProps) => (
|
|
108
|
+
<Svg {...p}>
|
|
109
|
+
<rect x="3.5" y="4.5" width="17" height="15" rx="2" />
|
|
110
|
+
<path d="M3.5 9.5h17M9 9.5v10" />
|
|
111
|
+
</Svg>
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
export const IconLink = (p: SvgProps) => (
|
|
115
|
+
<Svg {...p}>
|
|
116
|
+
<path d="M10 13a5 5 0 0 0 7 0l2-2a5 5 0 0 0-7-7l-1 1" />
|
|
117
|
+
<path d="M14 11a5 5 0 0 0-7 0l-2 2a5 5 0 0 0 7 7l1-1" />
|
|
118
|
+
</Svg>
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
export const IconInbox = (p: SvgProps) => (
|
|
122
|
+
<Svg {...p}>
|
|
123
|
+
<path d="M3 13h5l1.5 3h5L21 13" />
|
|
124
|
+
<path d="M5 5h14l2 8v5a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-5Z" />
|
|
125
|
+
</Svg>
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
/** The brand mark: an isometric "stack" tile in the accent colour. */
|
|
129
|
+
export function BrandMark({ size = 30 }: { size?: number }): React.JSX.Element {
|
|
130
|
+
return (
|
|
131
|
+
<svg width={size} height={size} viewBox="0 0 32 32" aria-hidden="true" style={{ display: "block" }}>
|
|
132
|
+
<rect width="32" height="32" rx="9" fill="var(--color-accent)" />
|
|
133
|
+
<g
|
|
134
|
+
fill="none"
|
|
135
|
+
stroke="var(--color-on-accent)"
|
|
136
|
+
strokeWidth="1.9"
|
|
137
|
+
strokeLinejoin="round"
|
|
138
|
+
strokeLinecap="round"
|
|
139
|
+
>
|
|
140
|
+
<path d="M16 7 25 12 16 17 7 12 16 7Z" />
|
|
141
|
+
<path d="M7 16.5 16 21.5 25 16.5" />
|
|
142
|
+
<path d="M7 21 16 26 25 21" opacity="0.55" />
|
|
143
|
+
</g>
|
|
144
|
+
</svg>
|
|
145
|
+
);
|
|
146
|
+
}
|