@frockbot/applet-sdk 0.0.0 → 0.3.13
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/README.md +77 -1
- package/dist/cli.mjs +1007 -0
- package/package.json +51 -5
- package/src/cli/bin.ts +128 -0
- package/src/cli/build.ts +181 -0
- package/src/cli/check.ts +134 -0
- package/src/cli/dev.ts +84 -0
- package/src/cli/main.ts +17 -0
- package/src/cli/manifest.ts +58 -0
- package/src/cli/new.ts +74 -0
- package/src/cli/paths.ts +98 -0
- package/src/cli/runtime.ts +123 -0
- package/src/client/collections.ts +72 -0
- package/src/client/index.ts +203 -0
- package/src/client/transport.ts +334 -0
- package/src/kit/README.md +130 -0
- package/src/kit/index.tsx +427 -0
- package/src/kit/styles.ts +200 -0
- package/src/lint/index.ts +148 -0
- package/src/lint/rules.ts +394 -0
- package/src/protocol/index.ts +411 -0
- package/src/schema/index.ts +436 -0
- package/src/server/applet.ts +399 -0
- package/src/server/index.ts +53 -0
- package/src/server/session.ts +156 -0
- package/src/server/store.ts +398 -0
- package/template/README.md +37 -0
- package/template/applet.json +5 -0
- package/template/server.ts +46 -0
- package/template/ui.tsx +113 -0
- package/types/cloudflare-workers.d.ts +55 -0
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@frockbot/applet-sdk/kit` — the only components an Applet renders.
|
|
3
|
+
*
|
|
4
|
+
* Fourteen components on nine semantic tokens. They are versioned once here
|
|
5
|
+
* and never copied into an Applet (ADR 0022 decision 10), which is why the
|
|
6
|
+
* linter forbids raw colours and the props below are the whole vocabulary.
|
|
7
|
+
* `kit/README.md` is the reference the Bot reads.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
useEffect,
|
|
12
|
+
useRef,
|
|
13
|
+
type ButtonHTMLAttributes,
|
|
14
|
+
type InputHTMLAttributes,
|
|
15
|
+
type ReactNode,
|
|
16
|
+
type SelectHTMLAttributes,
|
|
17
|
+
type TextareaHTMLAttributes,
|
|
18
|
+
} from "react";
|
|
19
|
+
|
|
20
|
+
import { installKitStyles } from "./styles.js";
|
|
21
|
+
|
|
22
|
+
export { KIT_CSS, installKitStyles } from "./styles.js";
|
|
23
|
+
|
|
24
|
+
installKitStyles();
|
|
25
|
+
|
|
26
|
+
type Space = "none" | "small" | "medium" | "large";
|
|
27
|
+
|
|
28
|
+
const SPACE: Record<Space, string> = {
|
|
29
|
+
none: "0",
|
|
30
|
+
small: "4px",
|
|
31
|
+
medium: "8px",
|
|
32
|
+
large: "16px",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function classes(...values: Array<string | false | undefined>): string {
|
|
36
|
+
return values.filter(Boolean).join(" ");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Layout
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
export interface StackProps {
|
|
44
|
+
children?: ReactNode;
|
|
45
|
+
direction?: "row" | "column";
|
|
46
|
+
gap?: Space;
|
|
47
|
+
align?: "start" | "center" | "end" | "stretch";
|
|
48
|
+
justify?: "start" | "center" | "end" | "between";
|
|
49
|
+
wrap?: boolean;
|
|
50
|
+
/** Adds the kit's root class; put one at the top of the page. */
|
|
51
|
+
root?: boolean;
|
|
52
|
+
className?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function Stack({
|
|
56
|
+
children,
|
|
57
|
+
direction = "column",
|
|
58
|
+
gap = "medium",
|
|
59
|
+
align,
|
|
60
|
+
justify,
|
|
61
|
+
wrap,
|
|
62
|
+
root,
|
|
63
|
+
className,
|
|
64
|
+
}: StackProps) {
|
|
65
|
+
return (
|
|
66
|
+
<div
|
|
67
|
+
className={classes("fb-stack", root && "fb-root", className)}
|
|
68
|
+
data-direction={direction}
|
|
69
|
+
data-align={align}
|
|
70
|
+
data-justify={justify}
|
|
71
|
+
data-wrap={wrap ? "true" : undefined}
|
|
72
|
+
style={{ gap: SPACE[gap], ...(root ? { padding: "12px" } : null) }}
|
|
73
|
+
>
|
|
74
|
+
{children}
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface TextProps {
|
|
80
|
+
children?: ReactNode;
|
|
81
|
+
size?: "title" | "heading" | "body" | "small";
|
|
82
|
+
tone?: "default" | "muted";
|
|
83
|
+
as?: "p" | "span" | "div" | "h1" | "h2" | "h3";
|
|
84
|
+
className?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function Text({
|
|
88
|
+
children,
|
|
89
|
+
size = "body",
|
|
90
|
+
tone = "default",
|
|
91
|
+
as: Tag = "p",
|
|
92
|
+
className,
|
|
93
|
+
}: TextProps) {
|
|
94
|
+
return (
|
|
95
|
+
<Tag
|
|
96
|
+
className={classes("fb-text", className)}
|
|
97
|
+
data-size={size}
|
|
98
|
+
data-tone={tone}
|
|
99
|
+
>
|
|
100
|
+
{children}
|
|
101
|
+
</Tag>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// Controls
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
export interface ButtonProps extends Omit<
|
|
110
|
+
ButtonHTMLAttributes<HTMLButtonElement>,
|
|
111
|
+
"className" | "style"
|
|
112
|
+
> {
|
|
113
|
+
children?: ReactNode;
|
|
114
|
+
variant?: "default" | "primary" | "ghost";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function Button({
|
|
118
|
+
children,
|
|
119
|
+
variant = "default",
|
|
120
|
+
type = "button",
|
|
121
|
+
...rest
|
|
122
|
+
}: ButtonProps) {
|
|
123
|
+
return (
|
|
124
|
+
<button className="fb-button" data-variant={variant} type={type} {...rest}>
|
|
125
|
+
{children}
|
|
126
|
+
</button>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
interface FieldProps {
|
|
131
|
+
label?: string;
|
|
132
|
+
error?: string;
|
|
133
|
+
children: ReactNode;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function Field({ label, error, children }: FieldProps) {
|
|
137
|
+
return (
|
|
138
|
+
<label className="fb-field">
|
|
139
|
+
{label ? <span className="fb-label">{label}</span> : null}
|
|
140
|
+
{children}
|
|
141
|
+
{error ? <span className="fb-error">{error}</span> : null}
|
|
142
|
+
</label>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface InputProps extends Omit<
|
|
147
|
+
InputHTMLAttributes<HTMLInputElement>,
|
|
148
|
+
"className" | "style" | "onChange" | "type"
|
|
149
|
+
> {
|
|
150
|
+
label?: string;
|
|
151
|
+
error?: string;
|
|
152
|
+
/** Receives the value, not the event. */
|
|
153
|
+
onValueChange?: (value: string) => void;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function Input({ label, error, onValueChange, ...rest }: InputProps) {
|
|
157
|
+
return (
|
|
158
|
+
<Field label={label} error={error}>
|
|
159
|
+
<input
|
|
160
|
+
className="fb-control"
|
|
161
|
+
type="text"
|
|
162
|
+
onChange={(event) => onValueChange?.(event.target.value)}
|
|
163
|
+
{...rest}
|
|
164
|
+
/>
|
|
165
|
+
</Field>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface TextareaProps extends Omit<
|
|
170
|
+
TextareaHTMLAttributes<HTMLTextAreaElement>,
|
|
171
|
+
"className" | "style" | "onChange"
|
|
172
|
+
> {
|
|
173
|
+
label?: string;
|
|
174
|
+
error?: string;
|
|
175
|
+
onValueChange?: (value: string) => void;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function Textarea({
|
|
179
|
+
label,
|
|
180
|
+
error,
|
|
181
|
+
onValueChange,
|
|
182
|
+
...rest
|
|
183
|
+
}: TextareaProps) {
|
|
184
|
+
return (
|
|
185
|
+
<Field label={label} error={error}>
|
|
186
|
+
<textarea
|
|
187
|
+
className="fb-control"
|
|
188
|
+
onChange={(event) => onValueChange?.(event.target.value)}
|
|
189
|
+
{...rest}
|
|
190
|
+
/>
|
|
191
|
+
</Field>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export interface SelectOption {
|
|
196
|
+
value: string;
|
|
197
|
+
label: string;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export interface SelectProps extends Omit<
|
|
201
|
+
SelectHTMLAttributes<HTMLSelectElement>,
|
|
202
|
+
"className" | "style" | "onChange" | "children"
|
|
203
|
+
> {
|
|
204
|
+
label?: string;
|
|
205
|
+
error?: string;
|
|
206
|
+
options: SelectOption[];
|
|
207
|
+
onValueChange?: (value: string) => void;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function Select({
|
|
211
|
+
label,
|
|
212
|
+
error,
|
|
213
|
+
options,
|
|
214
|
+
onValueChange,
|
|
215
|
+
...rest
|
|
216
|
+
}: SelectProps) {
|
|
217
|
+
return (
|
|
218
|
+
<Field label={label} error={error}>
|
|
219
|
+
<select
|
|
220
|
+
className="fb-control"
|
|
221
|
+
onChange={(event) => onValueChange?.(event.target.value)}
|
|
222
|
+
{...rest}
|
|
223
|
+
>
|
|
224
|
+
{options.map((option) => (
|
|
225
|
+
<option key={option.value} value={option.value}>
|
|
226
|
+
{option.label}
|
|
227
|
+
</option>
|
|
228
|
+
))}
|
|
229
|
+
</select>
|
|
230
|
+
</Field>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export interface CheckboxProps {
|
|
235
|
+
checked: boolean;
|
|
236
|
+
onChange: (checked: boolean) => void;
|
|
237
|
+
label?: ReactNode;
|
|
238
|
+
disabled?: boolean;
|
|
239
|
+
/** Announced when there is no visible label. */
|
|
240
|
+
ariaLabel?: string;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function Checkbox({
|
|
244
|
+
checked,
|
|
245
|
+
onChange,
|
|
246
|
+
label,
|
|
247
|
+
disabled,
|
|
248
|
+
ariaLabel,
|
|
249
|
+
}: CheckboxProps) {
|
|
250
|
+
return (
|
|
251
|
+
<label
|
|
252
|
+
className="fb-checkbox"
|
|
253
|
+
data-disabled={disabled ? "true" : undefined}
|
|
254
|
+
>
|
|
255
|
+
<input
|
|
256
|
+
type="checkbox"
|
|
257
|
+
checked={checked}
|
|
258
|
+
disabled={disabled}
|
|
259
|
+
aria-label={label === undefined ? ariaLabel : undefined}
|
|
260
|
+
onChange={(event) => onChange(event.target.checked)}
|
|
261
|
+
/>
|
|
262
|
+
{label === undefined ? null : <span>{label}</span>}
|
|
263
|
+
</label>
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
// Surfaces
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
|
|
271
|
+
export interface CardProps {
|
|
272
|
+
children?: ReactNode;
|
|
273
|
+
title?: ReactNode;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function Card({ children, title }: CardProps) {
|
|
277
|
+
return (
|
|
278
|
+
<section className="fb-card">
|
|
279
|
+
{title === undefined ? null : (
|
|
280
|
+
<div className="fb-card-title">{title}</div>
|
|
281
|
+
)}
|
|
282
|
+
{children}
|
|
283
|
+
</section>
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export interface ToolbarProps {
|
|
288
|
+
children?: ReactNode;
|
|
289
|
+
/** Renders flush to the trailing edge, after a flexible spacer. */
|
|
290
|
+
end?: ReactNode;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function Toolbar({ children, end }: ToolbarProps) {
|
|
294
|
+
return (
|
|
295
|
+
<div className="fb-toolbar">
|
|
296
|
+
{children}
|
|
297
|
+
{end === undefined ? null : (
|
|
298
|
+
<>
|
|
299
|
+
<span className="fb-toolbar-spacer" />
|
|
300
|
+
{end}
|
|
301
|
+
</>
|
|
302
|
+
)}
|
|
303
|
+
</div>
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export interface ListProps {
|
|
308
|
+
children?: ReactNode;
|
|
309
|
+
/** Hairline between rows. Defaults to true. */
|
|
310
|
+
bordered?: boolean;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function List({ children, bordered = true }: ListProps) {
|
|
314
|
+
return (
|
|
315
|
+
<ul className="fb-list" data-bordered={bordered ? "true" : "false"}>
|
|
316
|
+
{children}
|
|
317
|
+
</ul>
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export interface ListItemProps {
|
|
322
|
+
children?: ReactNode;
|
|
323
|
+
/** Leading slot: a checkbox, a badge, an avatar. */
|
|
324
|
+
start?: ReactNode;
|
|
325
|
+
/** Trailing slot: actions. */
|
|
326
|
+
end?: ReactNode;
|
|
327
|
+
onClick?: () => void;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function ListItem({ children, start, end, onClick }: ListItemProps) {
|
|
331
|
+
return (
|
|
332
|
+
<li
|
|
333
|
+
className="fb-list-item"
|
|
334
|
+
data-interactive={onClick ? "true" : undefined}
|
|
335
|
+
onClick={onClick}
|
|
336
|
+
>
|
|
337
|
+
{start}
|
|
338
|
+
<div className="fb-list-item-body">{children}</div>
|
|
339
|
+
{end === undefined ? null : <div className="fb-list-item-end">{end}</div>}
|
|
340
|
+
</li>
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export interface BadgeProps {
|
|
345
|
+
children?: ReactNode;
|
|
346
|
+
tone?: "default" | "accent";
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export function Badge({ children, tone = "default" }: BadgeProps) {
|
|
350
|
+
return (
|
|
351
|
+
<span className="fb-badge" data-tone={tone}>
|
|
352
|
+
{children}
|
|
353
|
+
</span>
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export interface EmptyStateProps {
|
|
358
|
+
title: string;
|
|
359
|
+
description?: string;
|
|
360
|
+
action?: ReactNode;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export function EmptyState({ title, description, action }: EmptyStateProps) {
|
|
364
|
+
return (
|
|
365
|
+
<div className="fb-empty">
|
|
366
|
+
<div className="fb-empty-title">{title}</div>
|
|
367
|
+
{description === undefined ? null : <div>{description}</div>}
|
|
368
|
+
{action}
|
|
369
|
+
</div>
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export interface DialogProps {
|
|
374
|
+
open: boolean;
|
|
375
|
+
title?: ReactNode;
|
|
376
|
+
onClose: () => void;
|
|
377
|
+
children?: ReactNode;
|
|
378
|
+
/** Buttons for the trailing action row. */
|
|
379
|
+
actions?: ReactNode;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function Dialog({
|
|
383
|
+
open,
|
|
384
|
+
title,
|
|
385
|
+
onClose,
|
|
386
|
+
children,
|
|
387
|
+
actions,
|
|
388
|
+
}: DialogProps) {
|
|
389
|
+
const surface = useRef<HTMLDivElement>(null);
|
|
390
|
+
|
|
391
|
+
useEffect(() => {
|
|
392
|
+
if (!open) return;
|
|
393
|
+
const onKey = (event: KeyboardEvent) => {
|
|
394
|
+
if (event.key === "Escape") onClose();
|
|
395
|
+
};
|
|
396
|
+
document.addEventListener("keydown", onKey);
|
|
397
|
+
surface.current?.focus();
|
|
398
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
399
|
+
}, [open, onClose]);
|
|
400
|
+
|
|
401
|
+
if (!open) return null;
|
|
402
|
+
return (
|
|
403
|
+
<div
|
|
404
|
+
className="fb-dialog-backdrop fb-root"
|
|
405
|
+
onClick={(event) => {
|
|
406
|
+
if (event.target === event.currentTarget) onClose();
|
|
407
|
+
}}
|
|
408
|
+
>
|
|
409
|
+
<div
|
|
410
|
+
className="fb-dialog"
|
|
411
|
+
role="dialog"
|
|
412
|
+
aria-modal="true"
|
|
413
|
+
aria-label={typeof title === "string" ? title : undefined}
|
|
414
|
+
tabIndex={-1}
|
|
415
|
+
ref={surface}
|
|
416
|
+
>
|
|
417
|
+
{title === undefined ? null : (
|
|
418
|
+
<div className="fb-dialog-title">{title}</div>
|
|
419
|
+
)}
|
|
420
|
+
{children}
|
|
421
|
+
{actions === undefined ? null : (
|
|
422
|
+
<div className="fb-dialog-actions">{actions}</div>
|
|
423
|
+
)}
|
|
424
|
+
</div>
|
|
425
|
+
</div>
|
|
426
|
+
);
|
|
427
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The kit's one stylesheet, as a string so the bundle stays a single file and
|
|
3
|
+
* no build step has to know about CSS.
|
|
4
|
+
*
|
|
5
|
+
* Every colour, radius, and edge resolves through one of the nine semantic
|
|
6
|
+
* tokens the host injects as `--frockbot-*`; the fallbacks are what the page
|
|
7
|
+
* looks like before `init` arrives, or in a plain browser tab. Nothing here
|
|
8
|
+
* spells a colour that is not a fallback, which is the rule the linter enforces
|
|
9
|
+
* for Applet code too.
|
|
10
|
+
*/
|
|
11
|
+
export const KIT_CSS = `
|
|
12
|
+
.fb-root, .fb-root * { box-sizing: border-box; }
|
|
13
|
+
.fb-root {
|
|
14
|
+
--fb-surface: var(--frockbot-surface, #ffffff);
|
|
15
|
+
--fb-surface-raised: var(--frockbot-surface-raised, #ffffff);
|
|
16
|
+
--fb-surface-subtle: var(--frockbot-surface-subtle, #f3f4f6);
|
|
17
|
+
--fb-text: var(--frockbot-text, #16181d);
|
|
18
|
+
--fb-text-muted: var(--frockbot-text-muted, #5b616b);
|
|
19
|
+
--fb-border: var(--frockbot-border, #d8dbe0);
|
|
20
|
+
--fb-accent-surface: var(--frockbot-accent-surface, #2f6feb);
|
|
21
|
+
--fb-accent-text: var(--frockbot-accent-text, #ffffff);
|
|
22
|
+
--fb-radius: var(--frockbot-radius-card, 10px);
|
|
23
|
+
--fb-radius-sm: calc(var(--fb-radius) / 2);
|
|
24
|
+
--fb-gap: 8px;
|
|
25
|
+
color: var(--fb-text);
|
|
26
|
+
background: var(--fb-surface);
|
|
27
|
+
font: 400 14px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
28
|
+
min-height: 100%;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.fb-stack { display: flex; }
|
|
32
|
+
.fb-stack[data-direction="column"] { flex-direction: column; }
|
|
33
|
+
.fb-stack[data-direction="row"] { flex-direction: row; align-items: center; }
|
|
34
|
+
.fb-stack[data-wrap="true"] { flex-wrap: wrap; }
|
|
35
|
+
.fb-stack[data-align="start"] { align-items: flex-start; }
|
|
36
|
+
.fb-stack[data-align="center"] { align-items: center; }
|
|
37
|
+
.fb-stack[data-align="end"] { align-items: flex-end; }
|
|
38
|
+
.fb-stack[data-align="stretch"] { align-items: stretch; }
|
|
39
|
+
.fb-stack[data-justify="start"] { justify-content: flex-start; }
|
|
40
|
+
.fb-stack[data-justify="center"] { justify-content: center; }
|
|
41
|
+
.fb-stack[data-justify="end"] { justify-content: flex-end; }
|
|
42
|
+
.fb-stack[data-justify="between"] { justify-content: space-between; }
|
|
43
|
+
|
|
44
|
+
.fb-text { margin: 0; color: var(--fb-text); }
|
|
45
|
+
.fb-text[data-tone="muted"] { color: var(--fb-text-muted); }
|
|
46
|
+
.fb-text[data-size="title"] { font-size: 20px; font-weight: 600; line-height: 1.3; }
|
|
47
|
+
.fb-text[data-size="heading"] { font-size: 16px; font-weight: 600; line-height: 1.4; }
|
|
48
|
+
.fb-text[data-size="body"] { font-size: 14px; }
|
|
49
|
+
.fb-text[data-size="small"] { font-size: 12px; }
|
|
50
|
+
|
|
51
|
+
.fb-button {
|
|
52
|
+
appearance: none;
|
|
53
|
+
border: 1px solid var(--fb-border);
|
|
54
|
+
border-radius: var(--fb-radius-sm);
|
|
55
|
+
background: var(--fb-surface-raised);
|
|
56
|
+
color: var(--fb-text);
|
|
57
|
+
font: inherit;
|
|
58
|
+
padding: 6px 12px;
|
|
59
|
+
min-height: 32px;
|
|
60
|
+
cursor: pointer;
|
|
61
|
+
display: inline-flex;
|
|
62
|
+
align-items: center;
|
|
63
|
+
justify-content: center;
|
|
64
|
+
gap: 6px;
|
|
65
|
+
}
|
|
66
|
+
.fb-button:hover:not(:disabled) { background: var(--fb-surface-subtle); }
|
|
67
|
+
.fb-button:focus-visible { outline: 2px solid var(--fb-accent-surface); outline-offset: 1px; }
|
|
68
|
+
.fb-button:disabled { opacity: 0.5; cursor: default; }
|
|
69
|
+
.fb-button[data-variant="primary"] {
|
|
70
|
+
background: var(--fb-accent-surface);
|
|
71
|
+
border-color: var(--fb-accent-surface);
|
|
72
|
+
color: var(--fb-accent-text);
|
|
73
|
+
}
|
|
74
|
+
.fb-button[data-variant="primary"]:hover:not(:disabled) { filter: brightness(0.94); }
|
|
75
|
+
.fb-button[data-variant="ghost"] { background: transparent; border-color: transparent; }
|
|
76
|
+
.fb-button[data-variant="ghost"]:hover:not(:disabled) { background: var(--fb-surface-subtle); }
|
|
77
|
+
|
|
78
|
+
.fb-field { display: flex; flex-direction: column; gap: 4px; }
|
|
79
|
+
.fb-label { font-size: 12px; color: var(--fb-text-muted); }
|
|
80
|
+
.fb-control {
|
|
81
|
+
appearance: none;
|
|
82
|
+
border: 1px solid var(--fb-border);
|
|
83
|
+
border-radius: var(--fb-radius-sm);
|
|
84
|
+
background: var(--fb-surface-raised);
|
|
85
|
+
color: var(--fb-text);
|
|
86
|
+
font: inherit;
|
|
87
|
+
padding: 6px 10px;
|
|
88
|
+
min-height: 32px;
|
|
89
|
+
width: 100%;
|
|
90
|
+
}
|
|
91
|
+
.fb-control:focus-visible { outline: 2px solid var(--fb-accent-surface); outline-offset: -1px; }
|
|
92
|
+
.fb-control:disabled { opacity: 0.5; }
|
|
93
|
+
textarea.fb-control { min-height: 72px; resize: vertical; }
|
|
94
|
+
.fb-error { font-size: 12px; color: var(--fb-accent-surface); }
|
|
95
|
+
|
|
96
|
+
.fb-checkbox { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; }
|
|
97
|
+
.fb-checkbox input { accent-color: var(--fb-accent-surface); width: 16px; height: 16px; margin: 0; }
|
|
98
|
+
.fb-checkbox[data-disabled="true"] { opacity: 0.5; cursor: default; }
|
|
99
|
+
|
|
100
|
+
.fb-card {
|
|
101
|
+
border: 1px solid var(--fb-border);
|
|
102
|
+
border-radius: var(--fb-radius);
|
|
103
|
+
background: var(--fb-surface-raised);
|
|
104
|
+
padding: 12px;
|
|
105
|
+
display: flex;
|
|
106
|
+
flex-direction: column;
|
|
107
|
+
gap: var(--fb-gap);
|
|
108
|
+
}
|
|
109
|
+
.fb-card-title { font-size: 16px; font-weight: 600; }
|
|
110
|
+
|
|
111
|
+
.fb-toolbar {
|
|
112
|
+
display: flex;
|
|
113
|
+
align-items: center;
|
|
114
|
+
gap: var(--fb-gap);
|
|
115
|
+
padding: 8px 0;
|
|
116
|
+
border-bottom: 1px solid var(--fb-border);
|
|
117
|
+
}
|
|
118
|
+
.fb-toolbar-spacer { flex: 1 1 auto; }
|
|
119
|
+
|
|
120
|
+
.fb-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
|
|
121
|
+
.fb-list[data-bordered="true"] > .fb-list-item + .fb-list-item { border-top: 1px solid var(--fb-border); }
|
|
122
|
+
.fb-list-item {
|
|
123
|
+
display: flex;
|
|
124
|
+
align-items: center;
|
|
125
|
+
gap: var(--fb-gap);
|
|
126
|
+
padding: 8px 4px;
|
|
127
|
+
min-height: 36px;
|
|
128
|
+
}
|
|
129
|
+
.fb-list-item[data-interactive="true"] { cursor: pointer; border-radius: var(--fb-radius-sm); }
|
|
130
|
+
.fb-list-item[data-interactive="true"]:hover { background: var(--fb-surface-subtle); }
|
|
131
|
+
.fb-list-item-body { flex: 1 1 auto; min-width: 0; }
|
|
132
|
+
.fb-list-item-end { flex: 0 0 auto; display: flex; align-items: center; gap: 4px; }
|
|
133
|
+
|
|
134
|
+
.fb-badge {
|
|
135
|
+
display: inline-flex;
|
|
136
|
+
align-items: center;
|
|
137
|
+
border-radius: 999px;
|
|
138
|
+
border: 1px solid var(--fb-border);
|
|
139
|
+
background: var(--fb-surface-subtle);
|
|
140
|
+
color: var(--fb-text-muted);
|
|
141
|
+
font-size: 12px;
|
|
142
|
+
line-height: 1;
|
|
143
|
+
padding: 3px 8px;
|
|
144
|
+
}
|
|
145
|
+
.fb-badge[data-tone="accent"] {
|
|
146
|
+
background: var(--fb-accent-surface);
|
|
147
|
+
border-color: var(--fb-accent-surface);
|
|
148
|
+
color: var(--fb-accent-text);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.fb-empty {
|
|
152
|
+
display: flex;
|
|
153
|
+
flex-direction: column;
|
|
154
|
+
align-items: center;
|
|
155
|
+
gap: 6px;
|
|
156
|
+
text-align: center;
|
|
157
|
+
padding: 32px 16px;
|
|
158
|
+
color: var(--fb-text-muted);
|
|
159
|
+
border: 1px dashed var(--fb-border);
|
|
160
|
+
border-radius: var(--fb-radius);
|
|
161
|
+
}
|
|
162
|
+
.fb-empty-title { font-size: 15px; font-weight: 600; color: var(--fb-text); }
|
|
163
|
+
|
|
164
|
+
.fb-dialog-backdrop {
|
|
165
|
+
position: fixed;
|
|
166
|
+
inset: 0;
|
|
167
|
+
display: flex;
|
|
168
|
+
align-items: center;
|
|
169
|
+
justify-content: center;
|
|
170
|
+
padding: 16px;
|
|
171
|
+
background: color-mix(in srgb, var(--frockbot-text, #16181d) 45%, transparent);
|
|
172
|
+
z-index: 10;
|
|
173
|
+
}
|
|
174
|
+
.fb-dialog {
|
|
175
|
+
background: var(--fb-surface-raised);
|
|
176
|
+
border: 1px solid var(--fb-border);
|
|
177
|
+
border-radius: var(--fb-radius);
|
|
178
|
+
width: min(420px, 100%);
|
|
179
|
+
max-height: 100%;
|
|
180
|
+
overflow: auto;
|
|
181
|
+
padding: 16px;
|
|
182
|
+
display: flex;
|
|
183
|
+
flex-direction: column;
|
|
184
|
+
gap: 12px;
|
|
185
|
+
}
|
|
186
|
+
.fb-dialog-title { font-size: 16px; font-weight: 600; }
|
|
187
|
+
.fb-dialog-actions { display: flex; justify-content: flex-end; gap: var(--fb-gap); }
|
|
188
|
+
`;
|
|
189
|
+
|
|
190
|
+
const STYLE_MARKER = "data-frockbot-kit";
|
|
191
|
+
|
|
192
|
+
/** Inject the stylesheet once. Called on import; safe outside a browser. */
|
|
193
|
+
export function installKitStyles(): void {
|
|
194
|
+
if (typeof document === "undefined") return;
|
|
195
|
+
if (document.head.querySelector(`style[${STYLE_MARKER}]`)) return;
|
|
196
|
+
const style = document.createElement("style");
|
|
197
|
+
style.setAttribute(STYLE_MARKER, "1");
|
|
198
|
+
style.textContent = KIT_CSS;
|
|
199
|
+
document.head.append(style);
|
|
200
|
+
}
|