@frockbot/plugin-ui-theme 0.0.0 → 0.1.0

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/frockbot.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "schemaVersion": 3,
3
+ "id": "ui-theme",
4
+ "displayName": "FrockBot UI Theme",
5
+ "version": "0.0.1",
6
+ "compatibility": { "frockbot": ">=0.0.1" },
7
+ "contributions": {
8
+ "client": {
9
+ "entry": "./client",
10
+ "mounts": [],
11
+ "outlets": []
12
+ }
13
+ },
14
+ "permissions": []
15
+ }
package/package.json CHANGED
@@ -1,14 +1,37 @@
1
1
  {
2
2
  "name": "@frockbot/plugin-ui-theme",
3
- "version": "0.0.0",
4
- "description": "Placeholder reserving this name for trusted publishing. Superseded by the first release.",
5
- "license": "UNLICENSED",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./src/index.ts",
8
+ "./client": "./src/client/index.ts",
9
+ "./manifest": "./src/manifest.ts",
10
+ "./frockbot.json": "./frockbot.json",
11
+ "./package.json": "./package.json"
12
+ },
13
+ "frockbot": {
14
+ "manifest": "./frockbot.json"
15
+ },
16
+ "scripts": {
17
+ "build": "vite build",
18
+ "typecheck": "tsc --noEmit -p tsconfig.json"
19
+ },
20
+ "dependencies": {
21
+ "@frockbot/client-core": "0.1.0"
22
+ },
23
+ "devDependencies": {
24
+ "@types/bun": "1.4.0",
25
+ "@types/node": "26.2.0",
26
+ "typescript": "5.9.3",
27
+ "vite": "8.2.2"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
6
32
  "repository": {
7
33
  "type": "git",
8
34
  "url": "git+https://github.com/timoconnellaus/frockbot.git",
9
35
  "directory": "packages/plugin-ui-theme"
10
- },
11
- "publishConfig": {
12
- "access": "public"
13
36
  }
14
37
  }
@@ -0,0 +1,6 @@
1
+ import type { ClientPlugin } from "@frockbot/client-core";
2
+ import "./theme.css";
3
+
4
+ export const uiThemeClientPlugin: ClientPlugin = () => undefined;
5
+
6
+ export default uiThemeClientPlugin;
@@ -0,0 +1,338 @@
1
+ * {
2
+ box-sizing: border-box;
3
+ }
4
+
5
+ :root {
6
+ color-scheme: dark;
7
+ color: #f4f2f6;
8
+ background: #1f1e24;
9
+ accent-color: #ec386b;
10
+ font-family:
11
+ Manrope,
12
+ ui-sans-serif,
13
+ -apple-system,
14
+ BlinkMacSystemFont,
15
+ "Segoe UI",
16
+ sans-serif;
17
+ font-synthesis: none;
18
+
19
+ /* Semantic color aliases. Feature plugins consume these names only. */
20
+ --frock-text: #f4f2f6;
21
+ --frock-text-muted: #aaa6b1;
22
+ --frock-text-subtle: #8d8896;
23
+ --frock-on-accent: #fff;
24
+ --frock-surface-window: #1f1e24;
25
+ --frock-surface: #211f26;
26
+ --frock-surface-sidebar: #1a191e;
27
+ --frock-surface-raised: #2c2a33;
28
+ --frock-surface-subtle: #26242c;
29
+ --frock-surface-accent: #432330;
30
+ --frock-surface-accent-soft: #2f2027;
31
+ --frock-border: #3a3742;
32
+ --frock-border-strong: #6c6874;
33
+ --frock-border-focus: #ec386b;
34
+ --frock-action-primary: #ec386b;
35
+ --frock-action-primary-hover: #d92d5f;
36
+ --frock-action-primary-pressed: #c62656;
37
+ --frock-action-secondary: #432330;
38
+ --frock-action-secondary-text: #f2a6bd;
39
+ --frock-success: #73d6aa;
40
+ --frock-success-surface: #203329;
41
+ --frock-success-border: #355f4a;
42
+ --frock-warning: #f0c36a;
43
+ --frock-warning-surface: #3a301e;
44
+ --frock-warning-border: #6d5a2e;
45
+ --frock-danger-text: #ff9aa8;
46
+ --frock-danger-surface: #3a2025;
47
+ --frock-danger-border: #70404a;
48
+ --frock-focus-ring: rgb(236 56 107 / 32%);
49
+ --frock-scrollbar: #514d5b;
50
+ --frock-overlay-tint: rgb(10 9 12 / 76%);
51
+
52
+ /* Shared typography, geometry, elevation, and motion aliases. */
53
+ --frock-font-display: "Archivo Black", Manrope, sans-serif;
54
+ --frock-font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;
55
+
56
+ /*
57
+ * Type scale. Feature styles must size text with these aliases only;
58
+ * `scripts/check-ui-styles.ts` rejects literal font sizes. The steps match
59
+ * the desktop chat apps FrockBot sits beside: 13px is the working size,
60
+ * 15px carries titles and primary rows, 11px is reserved for eyebrows.
61
+ */
62
+ --frock-text-xs: 11px;
63
+ --frock-text-sm: 12px;
64
+ --frock-text-base: 13px;
65
+ --frock-text-md: 14px;
66
+ --frock-text-lg: 15px;
67
+ --frock-text-xl: 17px;
68
+ --frock-text-2xl: 20px;
69
+ --frock-text-display: 24px;
70
+ --frock-leading-tight: 1.2;
71
+ --frock-leading-snug: 1.35;
72
+ --frock-leading-normal: 1.5;
73
+ --frock-tracking-eyebrow: 0.08em;
74
+ --frock-tracking-display: -0.02em;
75
+
76
+ /* Legacy aliases kept for older styles; prefer the scale above. */
77
+ --frock-font-body: var(--frock-text-md);
78
+ --frock-font-label: var(--frock-text-sm);
79
+ --frock-font-title: var(--frock-text-2xl);
80
+
81
+ /* Application geometry shared by the shell and the plugins it hosts. */
82
+ --frock-sidebar-width: 280px;
83
+ --frock-panel-width: 320px;
84
+
85
+ /*
86
+ * Phone geometry. A hand-held viewport has room for one column, so the
87
+ * columns a desktop shows side by side become drawers over it; these are the
88
+ * shared measurements of that layout, in the theme rather than in the shell
89
+ * because panel plugins lay themselves out against the same edges.
90
+ *
91
+ * `--frock-viewport-height` is `100dvh` on a phone: a browser whose toolbar
92
+ * collapses on scroll changes the visible height without changing the layout
93
+ * viewport, and a composer pinned to `100%` of the latter ends up under the
94
+ * toolbar. `--frock-safe-bottom` clears the home indicator, and is zero on
95
+ * every device that has none.
96
+ */
97
+ --frock-viewport-height: 100%;
98
+ --frock-drawer-width: min(300px, 84vw);
99
+ --frock-safe-bottom: env(safe-area-inset-bottom, 0px);
100
+ --frock-safe-top: env(safe-area-inset-top, 0px);
101
+
102
+ /*
103
+ * The size text is entered at. Mobile Safari zooms the page in on a focused
104
+ * field whose text is below 16px, and a zoomed page cannot be scrolled back
105
+ * out, so the composer and every plugin's inputs size through this alias
106
+ * rather than through the reading scale.
107
+ */
108
+ --frock-text-input: var(--frock-text-md);
109
+ --frock-titlebar-height: 52px;
110
+ --frock-control-sm: 28px;
111
+ --frock-control-md: 32px;
112
+ --frock-control-lg: 36px;
113
+ --frock-avatar-sm: 28px;
114
+ --frock-avatar-md: 38px;
115
+ --frock-icon-sm: 14px;
116
+ --frock-icon-md: 16px;
117
+ --frock-icon-lg: 18px;
118
+ --frock-radius-control: 10px;
119
+ --frock-radius-card: 14px;
120
+ --frock-radius-panel: 20px;
121
+ --frock-settings-width: 520px;
122
+ --frock-shadow-card: 0 8px 22px rgb(0 0 0 / 24%);
123
+ --frock-shadow-panel: 18px 0 55px rgb(0 0 0 / 42%);
124
+ --frock-shadow-floating: 0 18px 45px rgb(0 0 0 / 44%);
125
+ --frock-shadow-icon: 0 8px 18px rgb(236 56 107 / 28%);
126
+ --frock-shadow-control: 0 8px 28px rgb(0 0 0 / 34%);
127
+ --frock-shadow-composer: 0 12px 32px rgb(0 0 0 / 38%);
128
+ --frock-shadow-right-panel: -16px 0 36px rgb(0 0 0 / 38%);
129
+ --frock-motion-fast: 140ms cubic-bezier(0.2, 0, 0, 1);
130
+ --frock-motion-panel: 220ms cubic-bezier(0.2, 0, 0, 1);
131
+ --frock-motion-enter: 260ms cubic-bezier(0.16, 1, 0.3, 1);
132
+ --frock-fill-hover: rgb(244 242 246 / 7%);
133
+ --frock-fill-pressed: rgb(244 242 246 / 12%);
134
+ --frock-skeleton: rgb(244 242 246 / 8%);
135
+ --frock-skeleton-shine: rgb(255 255 255 / 12%);
136
+ --frock-layer-menu: 30;
137
+ --frock-layer-panel: 40;
138
+ --frock-layer-surface: 100;
139
+
140
+ /* Capability presentation aliases shared across first-party plugins. */
141
+ --frock-computer-accent: #67a9e8;
142
+ --frock-computer-accent-text: #9ecfff;
143
+ --frock-computer-surface: #1e3044;
144
+ --frock-computer-border: #315e87;
145
+ --frock-computer-viewer: #0c0c0c;
146
+ --frock-provider-google: #8ab4f8;
147
+ --frock-text-disabled: #77727f;
148
+ --frock-surface-disabled: #35323b;
149
+ --frock-danger-strong: #ff788c;
150
+ --frock-accent-border: #704052;
151
+ --frock-accent-text: #f2a6bd;
152
+ --frock-accent-surface: #321f28;
153
+ --frock-success-bright: #73d6aa;
154
+ --frock-overlay-border: rgb(255 255 255 / 25%);
155
+ --frock-shadow-accent: 0 10px 24px rgb(236 56 107 / 28%);
156
+ --frock-shadow-auth: 0 24px 70px rgb(0 0 0 / 42%);
157
+ --frock-shadow-avatar: 0 15px 35px rgb(0 0 0 / 34%);
158
+ --frock-shadow-dialog: 0 30px 90px rgb(0 0 0 / 62%);
159
+ --frock-thread-gradient: linear-gradient(
160
+ 180deg,
161
+ var(--frock-surface) 0%,
162
+ var(--frock-surface) 78%,
163
+ var(--frock-surface-window) 100%
164
+ );
165
+ --frock-auth-background: radial-gradient(
166
+ circle at 50% 20%,
167
+ rgb(236 56 107 / 14%),
168
+ transparent 34%
169
+ );
170
+ --frock-flock-preview: linear-gradient(
171
+ 160deg,
172
+ var(--frock-surface-accent),
173
+ var(--frock-surface-subtle)
174
+ );
175
+
176
+ --frock-computer-success-border: #355f4a;
177
+ --frock-computer-error-border: #70404a;
178
+ --frock-computer-error-text: #ff9aa8;
179
+ --frock-computer-error-surface: #3a2025;
180
+ --frock-computer-grid: rgb(244 242 246 / 5%);
181
+ --frock-computer-inset: rgb(244 242 246 / 4%);
182
+ --frock-computer-control-tint: rgb(31 30 36 / 84%);
183
+ --frock-computer-control-surface: rgb(44 42 51 / 96%);
184
+ --frock-computer-focus: rgb(103 169 232 / 34%);
185
+
186
+ --frock-viewer-overlay: rgb(3 3 3 / 94%);
187
+ --frock-viewer-text: #dedede;
188
+ --frock-viewer-text-strong: #e1e1e1;
189
+ --frock-viewer-text-muted: #727272;
190
+ --frock-viewer-text-subtle: #858585;
191
+ --frock-viewer-border: #272727;
192
+ --frock-viewer-border-hover: #3d3d3d;
193
+ --frock-viewer-surface: #141414;
194
+ --frock-viewer-surface-hover: #1b1b1b;
195
+ --frock-viewer-stage: #0c0c0c;
196
+ --frock-viewer-frame: #252525;
197
+ --frock-viewer-close: #a8a8a8;
198
+ --frock-viewer-close-surface: #171717;
199
+ --frock-viewer-blue-border: #2f6699;
200
+ --frock-viewer-blue-text: #b8dcff;
201
+ --frock-viewer-blue-surface: #17324b;
202
+ --frock-viewer-status-border: #333;
203
+ --frock-viewer-status-surface: #151515;
204
+ --frock-viewer-success-border: #24513e;
205
+ --frock-viewer-success-text: #64c99b;
206
+ --frock-viewer-success-surface: #10251c;
207
+ --frock-viewer-control-border: #2b5f92;
208
+ --frock-viewer-control-text: #87c5ff;
209
+ --frock-viewer-control-surface: #102235;
210
+ --frock-viewer-error-border: #5c3034;
211
+ --frock-viewer-error-text: #e28b92;
212
+ --frock-viewer-error-surface: #2a1719;
213
+ --frock-viewer-grid: rgb(255 255 255 / 2%);
214
+ --frock-viewer-grid-surface: #121212;
215
+ --frock-viewer-mark-text: #90c9ff;
216
+ --frock-viewer-mark-surface: #142b42;
217
+ --frock-viewer-content-text: #c8c8c8;
218
+ --frock-viewer-content-muted: #767676;
219
+ --frock-viewer-button-border: #3e3e3e;
220
+ --frock-viewer-button-text: #d8d8d8;
221
+ --frock-viewer-button-surface: #292929;
222
+ --frock-viewer-button-border-hover: #575757;
223
+ --frock-viewer-control-shield: rgb(7 7 7 / 78%);
224
+ --frock-viewer-control-panel: rgb(20 20 20 / 92%);
225
+ --frock-viewer-panel-border: #303030;
226
+ --frock-viewer-shadow-control: 0 4px 18px rgb(0 0 0 / 24%);
227
+ --frock-viewer-shadow-stage: 0 22px 80px rgb(0 0 0 / 42%);
228
+ --frock-viewer-shadow-panel: 0 8px 28px rgb(0 0 0 / 35%);
229
+ }
230
+
231
+ html,
232
+ body,
233
+ #app {
234
+ width: 100%;
235
+ height: 100%;
236
+ margin: 0;
237
+ overflow: hidden;
238
+ }
239
+
240
+ body {
241
+ background: var(--frock-surface-window);
242
+ }
243
+
244
+ button,
245
+ input,
246
+ textarea,
247
+ select {
248
+ color: inherit;
249
+ font: inherit;
250
+ }
251
+
252
+ button {
253
+ border: 0;
254
+ }
255
+
256
+ button:focus-visible,
257
+ input:focus-visible,
258
+ textarea:focus-visible,
259
+ select:focus-visible,
260
+ summary:focus-visible {
261
+ outline: 3px solid var(--frock-focus-ring);
262
+ outline-offset: 2px;
263
+ }
264
+
265
+ /* Shared motion primitives. Feature styles reference these by name. */
266
+ @keyframes frock-fade-in {
267
+ from {
268
+ opacity: 0;
269
+ }
270
+
271
+ to {
272
+ opacity: 1;
273
+ }
274
+ }
275
+
276
+ @keyframes frock-rise-in {
277
+ from {
278
+ opacity: 0;
279
+ transform: translateY(6px);
280
+ }
281
+
282
+ to {
283
+ opacity: 1;
284
+ transform: translateY(0);
285
+ }
286
+ }
287
+
288
+ @keyframes frock-scale-in {
289
+ from {
290
+ opacity: 0;
291
+ transform: scale(0.97);
292
+ }
293
+
294
+ to {
295
+ opacity: 1;
296
+ transform: scale(1);
297
+ }
298
+ }
299
+
300
+ @keyframes frock-shimmer {
301
+ from {
302
+ transform: translateX(-100%);
303
+ }
304
+
305
+ to {
306
+ transform: translateX(100%);
307
+ }
308
+ }
309
+
310
+ /*
311
+ * The phone breakpoint. `packages/plugin-shell/src/client/FrockBotApp.vue`
312
+ * matches the same width in `matchMedia`, because which layout is on screen
313
+ * decides behaviour as well as appearance — a drawer that is closed by default
314
+ * and a right panel that does not open over the conversation.
315
+ */
316
+ @media (max-width: 640px) {
317
+ :root {
318
+ --frock-viewport-height: 100dvh;
319
+ --frock-text-input: 16px;
320
+
321
+ /*
322
+ * Touch, not a cursor. Apple and Google both put the smallest comfortable
323
+ * target at around 44px, and every control in the product is sized through
324
+ * these three aliases.
325
+ */
326
+ --frock-control-sm: 34px;
327
+ --frock-control-md: 40px;
328
+ --frock-control-lg: 44px;
329
+ }
330
+ }
331
+
332
+ @media (prefers-reduced-motion: reduce) {
333
+ :root {
334
+ --frock-motion-fast: 0ms linear;
335
+ --frock-motion-panel: 0ms linear;
336
+ --frock-motion-enter: 0ms linear;
337
+ }
338
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { default, default as manifest } from "./manifest.js";
@@ -0,0 +1,3 @@
1
+ import manifest from "../frockbot.json" with { type: "json" };
2
+
3
+ export default manifest;
package/theme.test.ts ADDED
@@ -0,0 +1,56 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ const stylesheet = await Bun.file(
4
+ new URL("./src/client/theme.css", import.meta.url),
5
+ ).text();
6
+
7
+ function token(name: string): string {
8
+ const match = stylesheet.match(new RegExp(`${name}:\\s*(#[0-9a-f]{6})`, "i"));
9
+ if (!match) throw new Error(`Theme token ${name} is missing a hex value`);
10
+ return match[1].toLowerCase();
11
+ }
12
+
13
+ function luminance(color: string): number {
14
+ const channels = color
15
+ .slice(1)
16
+ .match(/.{2}/g)!
17
+ .map((channel) => Number.parseInt(channel, 16) / 255)
18
+ .map((channel) =>
19
+ channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4,
20
+ );
21
+ return 0.2126 * channels[0] + 0.7152 * channels[1] + 0.0722 * channels[2];
22
+ }
23
+
24
+ function contrast(foreground: string, background: string): number {
25
+ const brighter = Math.max(luminance(foreground), luminance(background));
26
+ const darker = Math.min(luminance(foreground), luminance(background));
27
+ return (brighter + 0.05) / (darker + 0.05);
28
+ }
29
+
30
+ describe("dark product theme", () => {
31
+ test("declares one dark native-control theme and the FrockBot accent", () => {
32
+ expect(stylesheet).toMatch(/color-scheme:\s*dark/);
33
+ expect(token("--frock-action-primary")).toBe("#ec386b");
34
+
35
+ for (const surface of [
36
+ "--frock-surface-window",
37
+ "--frock-surface",
38
+ "--frock-surface-sidebar",
39
+ "--frock-surface-raised",
40
+ "--frock-surface-subtle",
41
+ ]) {
42
+ expect(luminance(token(surface))).toBeLessThan(0.2);
43
+ }
44
+ });
45
+
46
+ test.each(["success", "warning", "danger"])(
47
+ "%s status text has at least 4.5:1 contrast on its surface",
48
+ (status) => {
49
+ const text = token(
50
+ status === "danger" ? "--frock-danger-text" : `--frock-${status}`,
51
+ );
52
+ const surface = token(`--frock-${status}-surface`);
53
+ expect(contrast(text, surface)).toBeGreaterThanOrEqual(4.5);
54
+ },
55
+ );
56
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2023",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "allowImportingTsExtensions": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "skipLibCheck": true,
10
+ "lib": ["ES2023", "DOM"],
11
+ "types": ["node", "vite/client"],
12
+ "resolveJsonModule": true
13
+ },
14
+ "include": ["src/**/*.ts", "vite.config.ts"]
15
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,22 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { defineConfig } from "vite";
3
+
4
+ export default defineConfig({
5
+ build: {
6
+ outDir: fileURLToPath(new URL("./dist", import.meta.url)),
7
+ emptyOutDir: true,
8
+ manifest: "manifest.json",
9
+ lib: {
10
+ entry: fileURLToPath(new URL("./src/client/index.ts", import.meta.url)),
11
+ formats: ["es"],
12
+ },
13
+ rollupOptions: {
14
+ external: ["@frockbot/client-core"],
15
+ output: {
16
+ entryFileNames: "assets/ui-theme-[hash].js",
17
+ chunkFileNames: "assets/chunk-[hash].js",
18
+ assetFileNames: "assets/[name]-[hash][extname]",
19
+ },
20
+ },
21
+ },
22
+ });
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # @frockbot/plugin-ui-theme
2
-
3
- Placeholder reserving this name. See https://github.com/timoconnellaus/frockbot.