rastack 0.0.44 → 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.
@@ -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
+ }
@@ -0,0 +1,398 @@
1
+ /**
2
+ * The admin's chrome stylesheet — injected once. Every value is a design-token
3
+ * CSS variable (`var(--color-*)`, `var(--space-*)`, `var(--radius-*)`, emitted
4
+ * by `<ThemeProvider>` with no prefix) or an admin-local `--ras-*` derived from
5
+ * them, so the whole surface re-themes (light/dark included) with the tokens.
6
+ * The shared `rastack/components` paint themselves the same way — this only
7
+ * styles the structural chrome the component kit doesn't ship (sidebar,
8
+ * top bar, filter popovers, chips).
9
+ */
10
+ import React from "react";
11
+
12
+ const CSS = String.raw`
13
+ :root {
14
+ --ras-sidebar-w: 274px;
15
+ --ras-shadow-sm: 0 1px 2px rgba(11, 16, 32, 0.05), 0 1px 3px rgba(11, 16, 32, 0.04);
16
+ --ras-shadow-md: 0 6px 16px rgba(11, 16, 32, 0.08), 0 18px 40px rgba(11, 16, 32, 0.10);
17
+ --ras-shadow-pop: 0 10px 30px rgba(11, 16, 32, 0.16), 0 2px 6px rgba(11, 16, 32, 0.08);
18
+ --ras-hover: color-mix(in srgb, var(--color-text) 6%, transparent);
19
+ --ras-active: color-mix(in srgb, var(--color-accent) 12%, transparent);
20
+ --ras-active-line: var(--color-accent);
21
+ --ras-ring: 0 0 0 3px color-mix(in srgb, var(--color-accent) 22%, transparent);
22
+ --ras-ease: cubic-bezier(0.22, 0.7, 0.24, 1);
23
+ }
24
+ [data-theme="dark"] {
25
+ --ras-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.5);
26
+ --ras-shadow-md: 0 8px 24px rgba(0, 0, 0, 0.5), 0 2px 8px rgba(0, 0, 0, 0.4);
27
+ --ras-shadow-pop: 0 14px 40px rgba(0, 0, 0, 0.6), 0 2px 8px rgba(0, 0, 0, 0.5);
28
+ --ras-hover: color-mix(in srgb, var(--color-text) 9%, transparent);
29
+ --ras-active: color-mix(in srgb, var(--color-accent) 20%, transparent);
30
+ }
31
+
32
+ * { box-sizing: border-box; }
33
+ html, body { margin: 0; height: 100%; }
34
+ body {
35
+ background: var(--color-bg);
36
+ color: var(--color-text);
37
+ font-family: var(--font-family-sans, "Inter", system-ui, sans-serif);
38
+ font-size: 14px;
39
+ line-height: 1.5;
40
+ -webkit-font-smoothing: antialiased;
41
+ text-rendering: optimizeLegibility;
42
+ }
43
+ .ras-boot,
44
+ .rastack-boot {
45
+ min-height: 100vh;
46
+ display: flex;
47
+ align-items: center;
48
+ justify-content: center;
49
+ gap: 10px;
50
+ color: var(--color-text-muted);
51
+ font-family: var(--font-family-sans, system-ui, sans-serif);
52
+ }
53
+ a { color: inherit; text-decoration: none; }
54
+ :focus-visible { outline: none; box-shadow: var(--ras-ring); border-radius: var(--radius-sm); }
55
+
56
+ /* ---- layout -------------------------------------------------------------- */
57
+ .ras-root {
58
+ display: grid;
59
+ grid-template-columns: var(--ras-sidebar-w) minmax(0, 1fr);
60
+ min-height: 100vh;
61
+ }
62
+ .ras-main { min-width: 0; display: flex; flex-direction: column; }
63
+
64
+ /* ---- sidebar ------------------------------------------------------------- */
65
+ .ras-sidebar {
66
+ position: sticky;
67
+ top: 0;
68
+ align-self: start;
69
+ height: 100vh;
70
+ display: flex;
71
+ flex-direction: column;
72
+ gap: 4px;
73
+ padding: 20px 14px 14px;
74
+ background: var(--color-surface-alt);
75
+ border-right: 1px solid var(--color-border);
76
+ overflow-y: auto;
77
+ }
78
+ .ras-brand {
79
+ display: flex;
80
+ align-items: center;
81
+ gap: 11px;
82
+ padding: 6px 8px 18px;
83
+ }
84
+ .ras-brand-name {
85
+ font-weight: 700;
86
+ font-size: 15px;
87
+ letter-spacing: -0.01em;
88
+ line-height: 1.1;
89
+ }
90
+ .ras-brand-sub {
91
+ font-size: 11px;
92
+ color: var(--color-text-muted);
93
+ letter-spacing: 0.04em;
94
+ text-transform: uppercase;
95
+ margin-top: 1px;
96
+ }
97
+ .ras-nav { display: flex; flex-direction: column; gap: 18px; padding-top: 2px; }
98
+ .ras-nav-section { display: flex; flex-direction: column; gap: 2px; }
99
+ .ras-nav-label {
100
+ font-size: 11px;
101
+ font-weight: 600;
102
+ letter-spacing: 0.07em;
103
+ text-transform: uppercase;
104
+ color: var(--color-text-muted);
105
+ padding: 4px 10px;
106
+ }
107
+ .ras-nav-link {
108
+ position: relative;
109
+ display: flex;
110
+ align-items: center;
111
+ gap: 10px;
112
+ padding: 8px 10px;
113
+ border-radius: var(--radius-md);
114
+ color: var(--color-text-muted);
115
+ font-weight: 500;
116
+ cursor: pointer;
117
+ transition: background 0.14s var(--ras-ease), color 0.14s var(--ras-ease);
118
+ }
119
+ .ras-nav-link:hover { background: var(--ras-hover); color: var(--color-text); }
120
+ .ras-nav-link .ras-ico { color: var(--color-text-muted); transition: color 0.14s; }
121
+ .ras-nav-link.is-active {
122
+ background: var(--ras-active);
123
+ color: var(--color-accent-strong);
124
+ font-weight: 600;
125
+ }
126
+ .ras-nav-link.is-active .ras-ico { color: var(--color-accent); }
127
+ .ras-nav-link.is-active::before {
128
+ content: "";
129
+ position: absolute;
130
+ left: -14px;
131
+ top: 8px;
132
+ bottom: 8px;
133
+ width: 3px;
134
+ border-radius: 0 3px 3px 0;
135
+ background: var(--ras-active-line);
136
+ }
137
+ .ras-sidebar-foot {
138
+ margin-top: auto;
139
+ padding: 12px 8px 2px;
140
+ display: flex;
141
+ align-items: center;
142
+ justify-content: space-between;
143
+ gap: 8px;
144
+ border-top: 1px solid var(--color-border);
145
+ }
146
+ .ras-foot-meta { font-size: 11px; color: var(--color-text-muted); letter-spacing: 0.02em; }
147
+ .ras-icon-btn {
148
+ display: inline-flex;
149
+ align-items: center;
150
+ justify-content: center;
151
+ width: 32px;
152
+ height: 32px;
153
+ border-radius: var(--radius-md);
154
+ border: 1px solid var(--color-border);
155
+ background: var(--color-surface);
156
+ color: var(--color-text-muted);
157
+ cursor: pointer;
158
+ transition: background 0.14s, color 0.14s, border-color 0.14s;
159
+ }
160
+ .ras-icon-btn:hover { color: var(--color-text); border-color: color-mix(in srgb, var(--color-text) 22%, transparent); }
161
+
162
+ /* ---- top bar ------------------------------------------------------------- */
163
+ .ras-topbar {
164
+ position: sticky;
165
+ top: 0;
166
+ z-index: 20;
167
+ display: flex;
168
+ align-items: center;
169
+ gap: 8px;
170
+ min-height: 56px;
171
+ padding: 10px 28px;
172
+ background: color-mix(in srgb, var(--color-bg) 82%, transparent);
173
+ border-bottom: 1px solid var(--color-border);
174
+ backdrop-filter: saturate(1.4) blur(10px);
175
+ }
176
+ .ras-crumbs { display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--color-text-muted); flex-wrap: wrap; }
177
+ .ras-crumbs a { cursor: pointer; }
178
+ .ras-crumbs a:hover { color: var(--color-text); }
179
+ .ras-crumb-sep { color: color-mix(in srgb, var(--color-text-muted) 55%, transparent); }
180
+ .ras-crumb-current { color: var(--color-text); font-weight: 600; }
181
+
182
+ .ras-content { padding: 26px 28px 64px; max-width: 1240px; width: 100%; }
183
+
184
+ /* ---- page head ----------------------------------------------------------- */
185
+ .ras-pagehead {
186
+ display: flex;
187
+ align-items: flex-end;
188
+ justify-content: space-between;
189
+ gap: 16px;
190
+ margin-bottom: 20px;
191
+ }
192
+ .ras-title { font-size: 26px; font-weight: 700; letter-spacing: -0.02em; margin: 0; line-height: 1.1; }
193
+ .ras-subtitle { color: var(--color-text-muted); font-size: 13px; margin-top: 5px; }
194
+ .ras-eyebrow {
195
+ font-size: 11px; font-weight: 600; letter-spacing: 0.09em; text-transform: uppercase;
196
+ color: var(--color-accent); margin-bottom: 8px;
197
+ }
198
+
199
+ /* ---- filter bar (Linear-style) ------------------------------------------- */
200
+ .ras-filterbar {
201
+ display: flex;
202
+ align-items: center;
203
+ gap: 8px;
204
+ flex-wrap: wrap;
205
+ margin-bottom: 16px;
206
+ }
207
+ .ras-filterbar .ras-grow { flex: 1 1 auto; }
208
+ .ras-chip {
209
+ display: inline-flex;
210
+ align-items: stretch;
211
+ height: 30px;
212
+ border: 1px solid var(--color-border);
213
+ border-radius: var(--radius-pill);
214
+ background: var(--color-surface);
215
+ box-shadow: var(--ras-shadow-sm);
216
+ overflow: hidden;
217
+ font-size: 12.5px;
218
+ }
219
+ .ras-chip-key { display: inline-flex; align-items: center; gap: 6px; padding: 0 4px 0 11px; color: var(--color-text-muted); }
220
+ .ras-chip-op { color: color-mix(in srgb, var(--color-text-muted) 75%, transparent); padding: 0 2px; }
221
+ .ras-chip-val {
222
+ display: inline-flex; align-items: center; gap: 5px; padding: 0 9px;
223
+ font-weight: 600; color: var(--color-text); cursor: pointer;
224
+ border-left: 1px solid transparent; transition: background 0.14s;
225
+ }
226
+ .ras-chip-val:hover { background: var(--ras-hover); }
227
+ .ras-chip-x {
228
+ display: inline-flex; align-items: center; justify-content: center;
229
+ padding: 0 9px; color: var(--color-text-muted); cursor: pointer;
230
+ border-left: 1px solid var(--color-border); transition: background 0.14s, color 0.14s;
231
+ }
232
+ .ras-chip-x:hover { background: color-mix(in srgb, var(--color-danger) 14%, transparent); color: var(--color-danger); }
233
+ .ras-addfilter {
234
+ display: inline-flex; align-items: center; gap: 6px; height: 30px; padding: 0 12px;
235
+ border: 1px dashed color-mix(in srgb, var(--color-text-muted) 45%, transparent);
236
+ border-radius: var(--radius-pill); background: transparent;
237
+ color: var(--color-text-muted); font-weight: 500; font-size: 12.5px; cursor: pointer;
238
+ transition: background 0.14s, color 0.14s, border-color 0.14s;
239
+ }
240
+ .ras-addfilter:hover { color: var(--color-text); border-color: var(--color-accent); background: var(--ras-hover); }
241
+ .ras-clearfilter {
242
+ background: none; border: none; color: var(--color-text-muted); cursor: pointer;
243
+ font-size: 12.5px; padding: 0 8px; height: 30px; border-radius: var(--radius-sm);
244
+ }
245
+ .ras-clearfilter:hover { color: var(--color-danger); }
246
+
247
+ /* ---- search -------------------------------------------------------------- */
248
+ .ras-search { position: relative; display: inline-flex; align-items: center; min-width: 220px; }
249
+ .ras-search .ras-search-ico { position: absolute; left: 10px; color: var(--color-text-muted); pointer-events: none; }
250
+ .ras-search input {
251
+ width: 100%; height: 32px; padding: 0 12px 0 32px;
252
+ border: 1px solid var(--color-border); border-radius: var(--radius-md);
253
+ background: var(--color-surface); color: var(--color-text);
254
+ font: inherit; font-size: 13px; transition: border-color 0.14s, box-shadow 0.14s;
255
+ }
256
+ .ras-search input::placeholder { color: var(--color-text-muted); }
257
+ .ras-search input:focus { outline: none; border-color: var(--color-accent); box-shadow: var(--ras-ring); }
258
+
259
+ /* ---- popover ------------------------------------------------------------- */
260
+ .ras-pop-wrap { position: relative; display: inline-flex; }
261
+ .ras-pop {
262
+ position: absolute;
263
+ top: calc(100% + 6px);
264
+ left: 0;
265
+ z-index: 50;
266
+ min-width: 236px;
267
+ max-width: 300px;
268
+ padding: 6px;
269
+ background: var(--color-surface);
270
+ border: 1px solid var(--color-border);
271
+ border-radius: var(--radius-lg);
272
+ box-shadow: var(--ras-shadow-pop);
273
+ animation: ras-pop-in 0.13s var(--ras-ease);
274
+ }
275
+ .ras-pop.align-right { left: auto; right: 0; }
276
+ @keyframes ras-pop-in { from { opacity: 0; transform: translateY(-4px) scale(0.98); } to { opacity: 1; transform: none; } }
277
+ .ras-pop-search {
278
+ width: 100%; height: 32px; padding: 0 10px; margin-bottom: 4px;
279
+ border: none; border-bottom: 1px solid var(--color-border); border-radius: 0;
280
+ background: transparent; color: var(--color-text); font: inherit; font-size: 13px;
281
+ }
282
+ .ras-pop-search:focus { outline: none; box-shadow: none; border-bottom-color: var(--color-accent); }
283
+ .ras-pop-head {
284
+ display: flex; align-items: center; gap: 8px; padding: 6px 8px; margin-bottom: 2px;
285
+ color: var(--color-text-muted); font-weight: 600; font-size: 12px; cursor: pointer;
286
+ border-radius: var(--radius-sm);
287
+ }
288
+ .ras-pop-head:hover { background: var(--ras-hover); color: var(--color-text); }
289
+ .ras-pop-list { max-height: 280px; overflow-y: auto; display: flex; flex-direction: column; gap: 1px; }
290
+ .ras-pop-item {
291
+ display: flex; align-items: center; gap: 9px;
292
+ padding: 7px 9px; border-radius: var(--radius-sm);
293
+ color: var(--color-text); font-size: 13px; cursor: pointer; text-align: left;
294
+ border: none; background: none; width: 100%;
295
+ transition: background 0.1s;
296
+ }
297
+ .ras-pop-item:hover { background: var(--ras-hover); }
298
+ .ras-pop-item .ras-ico { color: var(--color-text-muted); }
299
+ .ras-pop-item .ras-pop-item-chev { margin-left: auto; color: var(--color-text-muted); }
300
+ .ras-pop-item.is-selected { color: var(--color-accent-strong); font-weight: 600; }
301
+ .ras-pop-empty { padding: 12px 10px; color: var(--color-text-muted); font-size: 12.5px; text-align: center; }
302
+
303
+ /* ---- table wrapper ------------------------------------------------------- */
304
+ .ras-tablecard { overflow: hidden; }
305
+ .ras-tablecard table { font-size: 13.5px; }
306
+ .ras-tablecard thead th {
307
+ text-transform: uppercase;
308
+ letter-spacing: 0.05em;
309
+ font-size: 11px !important;
310
+ color: var(--color-text-muted) !important;
311
+ background: var(--color-surface-alt) !important;
312
+ font-weight: 600 !important;
313
+ }
314
+ .ras-tablecard tbody tr { transition: background 0.1s; }
315
+ .ras-id { font-family: var(--font-family-mono, ui-monospace, monospace); font-size: 12.5px; color: var(--color-text-muted); }
316
+ .ras-cell-link {
317
+ display: inline-flex; align-items: center; gap: 6px;
318
+ color: var(--color-accent-strong); font-weight: 500; cursor: pointer;
319
+ }
320
+ .ras-cell-link:hover { text-decoration: underline; }
321
+ .ras-fk {
322
+ display: inline-flex; align-items: center; gap: 6px;
323
+ padding: 2px 9px 2px 8px; border-radius: var(--radius-pill);
324
+ border: 1px solid var(--color-border);
325
+ background: color-mix(in srgb, var(--color-accent) 8%, transparent);
326
+ color: var(--color-accent-strong); font-weight: 500; font-size: 12px; cursor: pointer;
327
+ }
328
+ .ras-fk:hover { border-color: var(--color-accent); }
329
+ .ras-muted { color: var(--color-text-muted); }
330
+ .ras-bool {
331
+ display: inline-flex; align-items: center; gap: 6px; font-size: 12.5px; font-weight: 500;
332
+ }
333
+ .ras-dot { width: 7px; height: 7px; border-radius: 999px; }
334
+
335
+ /* ---- empty state --------------------------------------------------------- */
336
+ .ras-empty {
337
+ display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px;
338
+ padding: 64px 24px; text-align: center; color: var(--color-text-muted);
339
+ }
340
+ .ras-empty .ras-empty-ico {
341
+ display: inline-flex; align-items: center; justify-content: center;
342
+ width: 52px; height: 52px; border-radius: var(--radius-lg);
343
+ background: var(--color-surface-alt); color: var(--color-text-muted);
344
+ }
345
+ .ras-empty h3 { margin: 4px 0 0; color: var(--color-text); font-size: 16px; font-weight: 600; }
346
+
347
+ /* ---- forms --------------------------------------------------------------- */
348
+ .ras-formgrid { max-width: 620px; }
349
+ .ras-danger-zone {
350
+ display: flex; align-items: center; justify-content: space-between; gap: 16px;
351
+ margin-top: 22px; padding: 16px 18px;
352
+ border: 1px solid color-mix(in srgb, var(--color-danger) 30%, var(--color-border));
353
+ border-radius: var(--radius-lg); background: color-mix(in srgb, var(--color-danger) 5%, transparent);
354
+ }
355
+ .ras-danger-zone .ras-dz-title { font-weight: 600; color: var(--color-text); }
356
+ .ras-danger-zone .ras-dz-sub { font-size: 12.5px; color: var(--color-text-muted); margin-top: 2px; }
357
+
358
+ /* ---- index / overview ---------------------------------------------------- */
359
+ .ras-cardgrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 14px; }
360
+ .ras-modelcard {
361
+ display: flex; flex-direction: column; gap: 10px; padding: 18px;
362
+ cursor: pointer; transition: transform 0.14s var(--ras-ease), box-shadow 0.14s, border-color 0.14s;
363
+ }
364
+ .ras-modelcard:hover { transform: translateY(-2px); box-shadow: var(--ras-shadow-md); border-color: color-mix(in srgb, var(--color-accent) 40%, var(--color-border)); }
365
+ .ras-modelcard .ras-mc-top { display: flex; align-items: center; justify-content: space-between; }
366
+ .ras-modelcard .ras-mc-name { font-weight: 600; font-size: 15px; }
367
+ .ras-modelcard .ras-mc-go { color: var(--color-accent); display: inline-flex; align-items: center; gap: 4px; font-size: 12.5px; font-weight: 500; }
368
+ .ras-mc-icon { display:inline-flex; align-items:center; justify-content:center; width:36px; height:36px; border-radius: var(--radius-md); background: var(--color-surface-alt); color: var(--color-accent); }
369
+
370
+ /* ---- scrollbars ---------------------------------------------------------- */
371
+ * { scrollbar-width: thin; scrollbar-color: color-mix(in srgb, var(--color-text) 20%, transparent) transparent; }
372
+ *::-webkit-scrollbar { width: 10px; height: 10px; }
373
+ *::-webkit-scrollbar-thumb { background: color-mix(in srgb, var(--color-text) 18%, transparent); border-radius: 999px; border: 3px solid transparent; background-clip: content-box; }
374
+ *::-webkit-scrollbar-thumb:hover { background: color-mix(in srgb, var(--color-text) 30%, transparent); background-clip: content-box; }
375
+
376
+ /* ---- responsive ---------------------------------------------------------- */
377
+ @media (max-width: 860px) {
378
+ .ras-root { grid-template-columns: 1fr; }
379
+ .ras-sidebar {
380
+ position: static; height: auto; flex-direction: row; align-items: center;
381
+ gap: 10px; overflow-x: auto; border-right: none; border-bottom: 1px solid var(--color-border);
382
+ padding: 10px 14px;
383
+ }
384
+ .ras-brand { padding: 0 8px 0 0; }
385
+ .ras-nav { flex-direction: row; gap: 8px; }
386
+ .ras-nav-label, .ras-sidebar-foot, .ras-brand-sub { display: none; }
387
+ .ras-nav-link.is-active::before { display: none; }
388
+ .ras-topbar, .ras-content { padding-left: 16px; padding-right: 16px; }
389
+ }
390
+ `;
391
+
392
+ /** Inject the admin chrome stylesheet once. */
393
+ export function AdminStyles(): React.JSX.Element {
394
+ return React.createElement("style", {
395
+ "data-rastack-admin": "",
396
+ dangerouslySetInnerHTML: { __html: CSS },
397
+ });
398
+ }