@grainulation/grainulation 1.1.0 → 1.1.2

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/lib/doctor.js CHANGED
@@ -1,5 +1,5 @@
1
- const { execSync } = require('node:child_process');
2
- const { existsSync } = require('node:fs');
1
+ const { execFileSync } = require('node:child_process');
2
+ const { existsSync, readFileSync, readdirSync } = require('node:fs');
3
3
  const path = require('node:path');
4
4
  const { getInstallable } = require('./ecosystem');
5
5
 
@@ -17,8 +17,8 @@ const { getInstallable } = require('./ecosystem');
17
17
  */
18
18
  function checkGlobal(packageName) {
19
19
  try {
20
- const out = execSync(`npm list -g ${packageName} --depth=0 2>/dev/null`, {
21
- stdio: 'pipe',
20
+ const out = execFileSync('npm', ['list', '-g', packageName, '--depth=0'], {
21
+ stdio: ['pipe', 'pipe', 'ignore'],
22
22
  encoding: 'utf-8',
23
23
  timeout: 5000,
24
24
  });
@@ -35,8 +35,8 @@ function checkGlobal(packageName) {
35
35
  */
36
36
  function checkNpxCache(packageName) {
37
37
  try {
38
- const prefix = execSync('npm config get cache', {
39
- stdio: 'pipe',
38
+ const prefix = execFileSync('npm', ['config', 'get', 'cache'], {
39
+ stdio: ['pipe', 'pipe', 'ignore'],
40
40
  encoding: 'utf-8',
41
41
  timeout: 5000,
42
42
  }).trim();
@@ -44,14 +44,13 @@ function checkNpxCache(packageName) {
44
44
  if (!existsSync(npxDir)) return null;
45
45
 
46
46
  // npx cache has hash-named directories, each with node_modules
47
- const { readdirSync } = require('node:fs');
48
47
  const entries = readdirSync(npxDir, { withFileTypes: true });
49
48
  for (const entry of entries) {
50
49
  if (!entry.isDirectory()) continue;
51
50
  const pkgJson = path.join(npxDir, entry.name, 'node_modules', packageName, 'package.json');
52
51
  if (existsSync(pkgJson)) {
53
52
  try {
54
- const pkg = JSON.parse(require('node:fs').readFileSync(pkgJson, 'utf-8'));
53
+ const pkg = JSON.parse(readFileSync(pkgJson, 'utf-8'));
55
54
  return { version: pkg.version || 'installed', method: 'npx cache' };
56
55
  } catch {
57
56
  return { version: 'installed', method: 'npx cache' };
@@ -72,7 +71,7 @@ function checkLocal(packageName) {
72
71
  try {
73
72
  const pkgJson = path.join(process.cwd(), 'node_modules', packageName, 'package.json');
74
73
  if (existsSync(pkgJson)) {
75
- const pkg = JSON.parse(require('node:fs').readFileSync(pkgJson, 'utf-8'));
74
+ const pkg = JSON.parse(readFileSync(pkgJson, 'utf-8'));
76
75
  return { version: pkg.version || 'installed', method: 'local' };
77
76
  }
78
77
  return null;
@@ -98,7 +97,7 @@ function checkSource(packageName) {
98
97
  const pkgJson = candidate.endsWith('package.json') ? candidate : path.join(candidate, 'package.json');
99
98
  if (existsSync(pkgJson)) {
100
99
  try {
101
- const pkg = JSON.parse(require('node:fs').readFileSync(pkgJson, 'utf-8'));
100
+ const pkg = JSON.parse(readFileSync(pkgJson, 'utf-8'));
102
101
  if (pkg.name === packageName) {
103
102
  return { version: pkg.version || 'installed', method: 'source' };
104
103
  }
@@ -117,8 +116,8 @@ function checkSource(packageName) {
117
116
  */
118
117
  function checkNpxNoInstall(packageName) {
119
118
  try {
120
- const out = execSync(`npx --no-install ${packageName} --version 2>/dev/null`, {
121
- stdio: 'pipe',
119
+ const out = execFileSync('npx', ['--no-install', packageName, '--version'], {
120
+ stdio: ['pipe', 'pipe', 'ignore'],
122
121
  encoding: 'utf-8',
123
122
  timeout: 5000,
124
123
  }).trim();
@@ -163,8 +162,8 @@ function getNodeVersion() {
163
162
 
164
163
  function getNpmVersion() {
165
164
  try {
166
- return execSync('npm --version', {
167
- stdio: 'pipe',
165
+ return execFileSync('npm', ['--version'], {
166
+ stdio: ['pipe', 'pipe', 'ignore'],
168
167
  encoding: 'utf-8',
169
168
  timeout: 5000,
170
169
  }).trim();
@@ -175,8 +174,8 @@ function getNpmVersion() {
175
174
 
176
175
  function getPnpmVersion() {
177
176
  try {
178
- return execSync('pnpm --version', {
179
- stdio: 'pipe',
177
+ return execFileSync('pnpm', ['--version'], {
178
+ stdio: ['pipe', 'pipe', 'ignore'],
180
179
  encoding: 'utf-8',
181
180
  timeout: 5000,
182
181
  }).trim();
@@ -187,8 +186,8 @@ function getPnpmVersion() {
187
186
 
188
187
  function getBiomeVersion() {
189
188
  try {
190
- const out = execSync('npx biome --version', {
191
- stdio: 'pipe',
189
+ const out = execFileSync('npx', ['biome', '--version'], {
190
+ stdio: ['pipe', 'pipe', 'ignore'],
192
191
  encoding: 'utf-8',
193
192
  timeout: 5000,
194
193
  }).trim();
@@ -201,8 +200,8 @@ function getBiomeVersion() {
201
200
 
202
201
  function getHooksPath() {
203
202
  try {
204
- return execSync('git config core.hooksPath', {
205
- stdio: 'pipe',
203
+ return execFileSync('git', ['config', 'core.hooksPath'], {
204
+ stdio: ['pipe', 'pipe', 'ignore'],
206
205
  encoding: 'utf-8',
207
206
  timeout: 5000,
208
207
  }).trim();
package/lib/router.js CHANGED
@@ -1,5 +1,5 @@
1
1
  const { execFileSync, spawn } = require('node:child_process');
2
- const { existsSync } = require('node:fs');
2
+ const { existsSync, readFileSync } = require('node:fs');
3
3
  const path = require('node:path');
4
4
  const { getByName, getInstallable } = require('./ecosystem');
5
5
 
@@ -92,7 +92,7 @@ function findSourceBin(tool) {
92
92
  try {
93
93
  const pkgPath = path.join(dir, 'package.json');
94
94
  if (!existsSync(pkgPath)) continue;
95
- const pkg = JSON.parse(require('node:fs').readFileSync(pkgPath, 'utf-8'));
95
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
96
96
  if (pkg.name !== tool.package) continue;
97
97
  // Find the bin entry
98
98
  if (pkg.bin) {
@@ -261,7 +261,7 @@ function statusData() {
261
261
 
262
262
  if (hasClaims) {
263
263
  try {
264
- const claimsRaw = require('node:fs').readFileSync(path.join(cwd, 'claims.json'), 'utf-8');
264
+ const claimsRaw = readFileSync(path.join(cwd, 'claims.json'), 'utf-8');
265
265
  const claims = JSON.parse(claimsRaw);
266
266
  const claimList = Array.isArray(claims) ? claims : claims.claims || [];
267
267
  const byType = {};
@@ -281,7 +281,7 @@ function statusData() {
281
281
  for (const pidFile of [farmerPidPath, farmerPidAlt]) {
282
282
  if (existsSync(pidFile)) {
283
283
  try {
284
- const pid = require('node:fs').readFileSync(pidFile, 'utf-8').trim();
284
+ const pid = readFileSync(pidFile, 'utf-8').trim();
285
285
  process.kill(Number(pid), 0);
286
286
  farmerPidValue = Number(pid);
287
287
  } catch {
@@ -339,7 +339,7 @@ function status(opts) {
339
339
 
340
340
  if (hasClaims) {
341
341
  try {
342
- const claimsRaw = require('node:fs').readFileSync(path.join(cwd, 'claims.json'), 'utf-8');
342
+ const claimsRaw = readFileSync(path.join(cwd, 'claims.json'), 'utf-8');
343
343
  const claims = JSON.parse(claimsRaw);
344
344
  const claimList = Array.isArray(claims) ? claims : claims.claims || [];
345
345
  const total = claimList.length;
@@ -377,7 +377,7 @@ function status(opts) {
377
377
  for (const pidFile of [farmerPid, farmerPidAlt]) {
378
378
  if (existsSync(pidFile)) {
379
379
  try {
380
- const pid = require('node:fs').readFileSync(pidFile, 'utf-8').trim();
380
+ const pid = readFileSync(pidFile, 'utf-8').trim();
381
381
  // Check if process is still running
382
382
  process.kill(Number(pid), 0);
383
383
  farmerRunning = true;
package/lib/server.mjs CHANGED
@@ -11,7 +11,7 @@
11
11
  * grainulation serve [--port 9098]
12
12
  */
13
13
 
14
- import { execSync } from 'node:child_process';
14
+ import { execFileSync } from 'node:child_process';
15
15
  import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
16
16
  import { createServer } from 'node:http';
17
17
  import { createRequire } from 'node:module';
@@ -176,7 +176,10 @@ function detectTool(pkg) {
176
176
 
177
177
  // 1. Global npm
178
178
  try {
179
- const out = execSync(`npm list -g ${pkg} --depth=0 2>/dev/null`, { stdio: 'pipe', encoding: 'utf-8' });
179
+ const out = execFileSync('npm', ['list', '-g', pkg, '--depth=0'], {
180
+ stdio: ['pipe', 'pipe', 'ignore'],
181
+ encoding: 'utf-8',
182
+ });
180
183
  const match = out.match(new RegExp(`${escapeRegex(pkg)}@(\\S+)`));
181
184
  if (match) return { installed: true, version: match[1], method: 'global' };
182
185
  } catch {
@@ -185,7 +188,10 @@ function detectTool(pkg) {
185
188
 
186
189
  // 2. npx cache
187
190
  try {
188
- const prefix = execSync('npm config get cache', { stdio: 'pipe', encoding: 'utf-8' }).trim();
191
+ const prefix = execFileSync('npm', ['config', 'get', 'cache'], {
192
+ stdio: ['pipe', 'pipe', 'ignore'],
193
+ encoding: 'utf-8',
194
+ }).trim();
189
195
  const npxDir = join(prefix, '_npx');
190
196
  if (existsSync(npxDir)) {
191
197
  const entries = readdirSync(npxDir, { withFileTypes: true });
@@ -242,7 +248,7 @@ function runDoctor() {
242
248
  const nodeVersion = process.version;
243
249
  let npmVersion = 'unknown';
244
250
  try {
245
- npmVersion = execSync('npm --version', { stdio: 'pipe', encoding: 'utf-8' }).trim();
251
+ npmVersion = execFileSync('npm', ['--version'], { stdio: ['pipe', 'pipe', 'ignore'], encoding: 'utf-8' }).trim();
246
252
  } catch {
247
253
  /* ignore */
248
254
  }
package/lib/setup.js CHANGED
@@ -1,5 +1,5 @@
1
1
  const readline = require('node:readline');
2
- const { execSync } = require('node:child_process');
2
+ const { execFileSync } = require('node:child_process');
3
3
  const { getInstallable, getCategories } = require('./ecosystem');
4
4
  const { getVersion } = require('./doctor');
5
5
 
@@ -108,7 +108,7 @@ async function run() {
108
108
  for (const tool of toInstall) {
109
109
  console.log(` Installing ${tool.package}...`);
110
110
  try {
111
- execSync(`npm install -g ${tool.package}`, { stdio: 'pipe' });
111
+ execFileSync('npm', ['install', '-g', tool.package], { stdio: 'pipe' });
112
112
  console.log(` \x1b[32m\u2713\x1b[0m ${tool.name} installed`);
113
113
  } catch (err) {
114
114
  console.log(` \x1b[31m\u2717\x1b[0m ${tool.name} failed: ${err.message}`);
package/package.json CHANGED
@@ -1,11 +1,8 @@
1
1
  {
2
2
  "name": "@grainulation/grainulation",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Structured research for decisions that satisfice",
5
5
  "license": "MIT",
6
- "workspaces": [
7
- "packages/*"
8
- ],
9
6
  "bin": {
10
7
  "grainulation": "bin/grainulation.js"
11
8
  },
@@ -18,15 +15,13 @@
18
15
  },
19
16
  "files": [
20
17
  "bin/",
21
- "lib/",
22
- "public/"
18
+ "lib/"
23
19
  ],
24
20
  "scripts": {
25
21
  "test": "node test/basic.test.js",
26
22
  "lint": "biome ci .",
27
23
  "format": "biome check --write .",
28
- "start": "node bin/grainulation.js",
29
- "postinstall": "git config core.hooksPath .githooks || true"
24
+ "start": "node bin/grainulation.js"
30
25
  },
31
26
  "keywords": [
32
27
  "research",
@@ -1,316 +0,0 @@
1
- /*
2
- * grainulation-tokens.css v1.0.0
3
- * Shared design tokens for the grainulation ecosystem.
4
- * Home: barn (the design-system tool)
5
- *
6
- * Usage:
7
- * <link rel="stylesheet" href="grainulation-tokens.css">
8
- * <html data-tool="wheat"> <!-- activates wheat accent scale -->
9
- *
10
- * Token architecture (three layers):
11
- * 1. Base layer -- backgrounds, foregrounds, borders, spacing, type, radii
12
- * 2. Semantic layer -- accent (maps to tool), status colors, feedback colors
13
- * 3. Tool scales -- per-tool 5-weight accent ramp (50/200/400/600/800)
14
- *
15
- * Reset layer:
16
- * Box model, scrollbar, font smoothing, shared components
17
- *
18
- * Accent activation:
19
- * Set data-tool="<name>" on <html> to load that tool's accent scale.
20
- * Default accent (no data-tool) = barn rose-red.
21
- *
22
- * Extracted from working UIs:
23
- * - barn/public/index.html (accent: #f43f5e rose — lightened from #e11d48 for WCAG AA)
24
- * - wheat/public/index.html (accent: #fbbf24 amber)
25
- * - farmer/public/index.html (accent: #22c55e emerald)
26
- * - grainulation/public/index.html (accent: #9ca3af neutral)
27
- */
28
-
29
- /* ══════════════════════════════════════════════════════════════════════════════
30
- LAYER 1 — BASE TOKENS
31
- Shared across every tool. Extracted from wheat + barn + farmer + grainulation.
32
- ══════════════════════════════════════════════════════════════════════════════ */
33
-
34
- :root {
35
- /* ── Backgrounds (dark tier) ──
36
- #0a0e1a is universal across all four UIs.
37
- bg2/bg3/bg4 from wheat + grainulation (Tailwind Slate scale). */
38
- --bg: #0a0e1a;
39
- --bg2: #111827;
40
- --bg3: #1e293b;
41
- --bg4: #334155;
42
-
43
- /* ── Foregrounds ──
44
- fg/fg2/fg3 from wheat + grainulation.
45
- Barn used --text:#e8ecf1 (close to fg); standardized to #e2e8f0. */
46
- --fg: #e2e8f0;
47
- --fg2: #94a3b8;
48
- --fg3: #64748b;
49
-
50
- /* ── Backward-compat aliases ──
51
- Barn-originated names still used in barn + farmer inline styles. */
52
- --text: var(--fg);
53
- --muted: var(--fg2);
54
- --dim: var(--fg3);
55
-
56
- /* ── Borders ──
57
- Wheat/grainulation use solid #1e293b; barn/farmer use rgba.
58
- Provide both — --border is the structural one, --border-subtle for overlays. */
59
- --border: #1e293b;
60
- --border-subtle: rgba(255, 255, 255, 0.08);
61
-
62
- /* ── Glass (farmer-originated, useful for overlays) ── */
63
- --glass: rgba(255, 255, 255, 0.08);
64
- --glass-border: rgba(255, 255, 255, 0.12);
65
-
66
- /* ── Radii ── (from wheat + barn + grainulation, converged) */
67
- --radius: 8px;
68
- --radius-sm: 4px;
69
- --radius-lg: 12px;
70
- --radius-full: 9999px;
71
-
72
- /* ── Typography ──
73
- Sans from barn/farmer; mono from wheat. Both used across UIs. */
74
- --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "Inter", sans-serif;
75
- --font-mono: "SF Mono", "Cascadia Code", "JetBrains Mono", "Fira Code", "Consolas", monospace;
76
- --text-xs: 9px;
77
- --text-sm: 10px;
78
- --text-base: 12px;
79
- --text-md: 13px;
80
- --text-lg: 15px;
81
- --text-xl: 18px;
82
- --line-height: 1.5;
83
-
84
- /* ── Spacing ── */
85
- --space-xs: 4px;
86
- --space-sm: 8px;
87
- --space-md: 12px;
88
- --space-lg: 16px;
89
- --space-xl: 24px;
90
- --space-2xl: 32px;
91
-
92
- /* ── Transitions ── */
93
- --transition-fast: 0.1s ease;
94
- --transition-base: 0.15s ease;
95
- --transition-slow: 0.3s ease;
96
- }
97
-
98
- /* ══════════════════════════════════════════════════════════════════════════════
99
- LAYER 2 — SEMANTIC TOKENS
100
- Status, feedback, and accent. Accent defaults to barn rose-red.
101
- ══════════════════════════════════════════════════════════════════════════════ */
102
-
103
- :root {
104
- /* ── Status colors ──
105
- Converged values: green from barn/farmer (#22c55e) vs wheat (#34d399).
106
- Using the brighter #22c55e for success (used for connection dots);
107
- keeping #34d399 as --success-light for softer contexts. */
108
- --success: #22c55e;
109
- --success-light: #34d399;
110
- --warning: #f59e0b;
111
- --error: #f87171;
112
- --info: #60a5fa;
113
-
114
- /* ── Semantic palette (named colors, used across UIs) ── */
115
- --green: #22c55e;
116
- --red: #f87171;
117
- --blue: #60a5fa;
118
- --purple: #a78bfa;
119
- --orange: #fb923c;
120
- --cyan: #22d3ee;
121
-
122
- /* ── Accent (default: barn rose) ──
123
- Overridden per-tool via [data-tool] selectors below.
124
- --accent-bg is the very faint tint for hover/active states.
125
- Lightened from #e11d48 to #f43f5e for WCAG AA (5.24:1 vs #0a0e1a). */
126
- --accent: #f43f5e;
127
- --accent-light: #fb7185;
128
- --accent-dim: rgba(244, 63, 94, 0.12);
129
- --accent-bg: rgba(244, 63, 94, 0.06);
130
- --accent-border: rgba(244, 63, 94, 0.25);
131
- }
132
-
133
- /* ══════════════════════════════════════════════════════════════════════════════
134
- LAYER 3 — TOOL-SPECIFIC ACCENT SCALES
135
- Each tool gets a 5-weight ramp (50/200/400/600/800) plus the four
136
- semantic accent tokens. Activated via data-tool="<name>" on <html>.
137
- ══════════════════════════════════════════════════════════════════════════════ */
138
-
139
- /* ── wheat: amber (#fbbf24) ── */
140
- [data-tool="wheat"] {
141
- --accent-50: #fffbeb;
142
- --accent-200: #fde68a;
143
- --accent-400: #fbbf24;
144
- --accent-600: #d97706;
145
- --accent-800: #92400e;
146
- --accent: #fbbf24;
147
- --accent-light: #fcd34d;
148
- --accent-dim: rgba(251, 191, 36, 0.08);
149
- --accent-bg: rgba(251, 191, 36, 0.06);
150
- --accent-border: rgba(251, 191, 36, 0.25);
151
- }
152
-
153
- /* ── barn: rose (#f43f5e) — default, repeated for explicitness ──
154
- Lightened from #e11d48 for WCAG AA compliance (5.24:1 vs #0a0e1a). */
155
- [data-tool="barn"] {
156
- --accent-50: #fff1f2;
157
- --accent-200: #fecdd3;
158
- --accent-400: #fb7185;
159
- --accent-600: #f43f5e;
160
- --accent-800: #be123c;
161
- --accent: #f43f5e;
162
- --accent-light: #fb7185;
163
- --accent-dim: rgba(244, 63, 94, 0.12);
164
- --accent-bg: rgba(244, 63, 94, 0.06);
165
- --accent-border: rgba(244, 63, 94, 0.25);
166
- }
167
-
168
- /* ── farmer: emerald (#22c55e) ── */
169
- [data-tool="farmer"] {
170
- --accent-50: #ecfdf5;
171
- --accent-200: #a7f3d0;
172
- --accent-400: #34d399;
173
- --accent-600: #16a34a;
174
- --accent-800: #166534;
175
- --accent: #22c55e;
176
- --accent-light: #4ade80;
177
- --accent-dim: rgba(34, 197, 94, 0.1);
178
- --accent-bg: rgba(34, 197, 94, 0.06);
179
- --accent-border: rgba(34, 197, 94, 0.25);
180
- }
181
-
182
- /* ── mill: blue (#3b82f6) ── */
183
- [data-tool="mill"] {
184
- --accent-50: #eff6ff;
185
- --accent-200: #bfdbfe;
186
- --accent-400: #60a5fa;
187
- --accent-600: #2563eb;
188
- --accent-800: #1e40af;
189
- --accent: #3b82f6;
190
- --accent-light: #60a5fa;
191
- --accent-dim: rgba(59, 130, 246, 0.1);
192
- --accent-bg: rgba(59, 130, 246, 0.06);
193
- --accent-border: rgba(59, 130, 246, 0.25);
194
- }
195
-
196
- /* ── silo: purple (#a78bfa) ── */
197
- [data-tool="silo"] {
198
- --accent-50: #f5f3ff;
199
- --accent-200: #ddd6fe;
200
- --accent-400: #a78bfa;
201
- --accent-600: #7c3aed;
202
- --accent-800: #5b21b6;
203
- --accent: #a78bfa;
204
- --accent-light: #c4b5fd;
205
- --accent-dim: rgba(167, 139, 250, 0.1);
206
- --accent-bg: rgba(167, 139, 250, 0.06);
207
- --accent-border: rgba(167, 139, 250, 0.25);
208
- }
209
-
210
- /* ── harvest: orange (#fb923c) ── */
211
- [data-tool="harvest"] {
212
- --accent-50: #fff7ed;
213
- --accent-200: #fed7aa;
214
- --accent-400: #fb923c;
215
- --accent-600: #ea580c;
216
- --accent-800: #9a3412;
217
- --accent: #fb923c;
218
- --accent-light: #fdba74;
219
- --accent-dim: rgba(251, 146, 60, 0.1);
220
- --accent-bg: rgba(251, 146, 60, 0.06);
221
- --accent-border: rgba(251, 146, 60, 0.25);
222
- }
223
-
224
- /* ── orchard: cyan (#22d3ee) ── */
225
- [data-tool="orchard"] {
226
- --accent-50: #ecfeff;
227
- --accent-200: #a5f3fc;
228
- --accent-400: #22d3ee;
229
- --accent-600: #0891b2;
230
- --accent-800: #155e75;
231
- --accent: #22d3ee;
232
- --accent-light: #67e8f9;
233
- --accent-dim: rgba(34, 211, 238, 0.1);
234
- --accent-bg: rgba(34, 211, 238, 0.06);
235
- --accent-border: rgba(34, 211, 238, 0.25);
236
- }
237
-
238
- /* ── grainulation: neutral/white (#9ca3af) ── */
239
- [data-tool="grainulation"],
240
- [data-tool="grainulation"] {
241
- --accent-50: #f9fafb;
242
- --accent-200: #e5e7eb;
243
- --accent-400: #9ca3af;
244
- --accent-600: #6b7280;
245
- --accent-800: #374151;
246
- --accent: #9ca3af;
247
- --accent-light: #d1d5db;
248
- --accent-dim: rgba(156, 163, 175, 0.1);
249
- --accent-bg: rgba(156, 163, 175, 0.06);
250
- --accent-border: rgba(156, 163, 175, 0.25);
251
- }
252
-
253
- /* ══════════════════════════════════════════════════════════════════════════════
254
- RESET + BASE STYLES — shared across all tool UIs
255
- ══════════════════════════════════════════════════════════════════════════════ */
256
-
257
- *,
258
- *::before,
259
- *::after {
260
- box-sizing: border-box;
261
- margin: 0;
262
- padding: 0;
263
- }
264
-
265
- html,
266
- body {
267
- height: 100%;
268
- overflow: hidden;
269
- }
270
-
271
- body {
272
- font-family: var(--font-sans);
273
- background: var(--bg);
274
- color: var(--fg);
275
- font-size: var(--text-md);
276
- line-height: var(--line-height);
277
- -webkit-font-smoothing: antialiased;
278
- -moz-osx-font-smoothing: grayscale;
279
- }
280
-
281
- /* ── Scrollbar (webkit) ── */
282
-
283
- ::-webkit-scrollbar {
284
- width: 6px;
285
- height: 6px;
286
- }
287
-
288
- ::-webkit-scrollbar-track {
289
- background: transparent;
290
- }
291
-
292
- ::-webkit-scrollbar-thumb {
293
- background: var(--bg4);
294
- border-radius: 3px;
295
- }
296
-
297
- ::-webkit-scrollbar-thumb:hover {
298
- background: var(--fg3);
299
- }
300
-
301
- /* ── Shared components ── */
302
-
303
- .conn-dot,
304
- .connection-dot {
305
- width: 8px;
306
- height: 8px;
307
- border-radius: 50%;
308
- background: var(--green);
309
- display: inline-block;
310
- flex-shrink: 0;
311
- }
312
-
313
- .conn-dot.disconnected,
314
- .connection-dot.disconnected {
315
- background: var(--red);
316
- }