@andespindola/brainlink 1.6.15 → 1.7.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/CHANGELOG.md +4 -0
- package/dist/application/frontend/auth-styles.js +180 -0
- package/dist/application/frontend/client/account-menu.js +21 -16
- package/dist/application/frontend/client/graph-renderer.js +20 -17
- package/dist/application/frontend/client/rendering.js +5 -5
- package/dist/application/frontend/client/scope-theme.js +23 -18
- package/dist/application/frontend/client-css.js +56 -36
- package/dist/application/frontend/login-page.js +9 -163
- package/dist/application/frontend/neon-theme.js +110 -0
- package/dist/application/frontend/settings-page.js +76 -52
- package/dist/mcp/oauth-authorize-page.js +11 -130
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.7.0
|
|
4
|
+
|
|
5
|
+
- **Unified neon graph UI.** The graph web frontend now shares one visual identity with the SaaS app so the app → SSO → graph handoff reads as a single product. Every self-contained page served by the graph server — the login and change-password pages, the settings page, the OAuth authorize/consent page, the graph explorer, and the injected account menu — is repainted onto the neon palette: `#0a0e1a` base with a faint cyan/violet cyber grid and soft ambient glow, cyan `#22d3ee` nodes, violet `#8b5cf6` edges/context, glass panels, gradient "Brainlink" wordmark, cyan focus glow, and a neon gradient primary button. Typography moves to Space Grotesk (headings) + DM Sans (body) via a Google Fonts `@import` with a robust system fallback so it degrades gracefully offline, while a monospace stack stays for graph node ids and code. All graph behavior (WebGL/worker/2D-fallback rendering, layout, zoom, pan, search, streaming, context grouping, versioning UI) and every endpoint, form field and OAuth flow are unchanged — this is a visual/identity pass only. Contrast on the new palette meets WCAG AA, focus rings are visible, and motion respects `prefers-reduced-motion`.
|
|
6
|
+
|
|
3
7
|
## 1.6.15
|
|
4
8
|
|
|
5
9
|
- **Retrieve image bytes (`brainlink_get_image`).** A new MCP tool + `get-image` use-case reads an image previously stored by `brainlink_add_image` back out of the vault as base64 bytes + mime, decrypting when the vault is encrypted. Path is validated to stay inside `.brainlink/assets` (no traversal). This closes the cross-agent loop: one agent adds an image to the shared vault, another fetches and uses it.
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// Shared glass-card styles for the branded auth-style pages (login, change
|
|
2
|
+
// password, OAuth authorize, settings). Built on the unified neon tokens from
|
|
3
|
+
// neon-theme.ts so every entry point renders the same identity: #0a0e1a base,
|
|
4
|
+
// faint cyber grid, ambient glow blobs, a frosted glass card, a gradient
|
|
5
|
+
// "Brainlink" wordmark, cyan focus glow and a neon primary button. Parameterized
|
|
6
|
+
// only by the card max-width; the class names (.auth-card, .auth-form,
|
|
7
|
+
// .auth-field, .auth-submit, .auth-message, .auth-back, .auth-brand) are shared
|
|
8
|
+
// so page-specific CSS can layer on top without redefining the base.
|
|
9
|
+
import { neonAmbientEffects, neonFontImport, neonGridBackground, neonRootTokens, neonWordmark } from './neon-theme.js';
|
|
10
|
+
export const authSharedStyles = (cardMaxWidth) => `${neonFontImport()}
|
|
11
|
+
:root {
|
|
12
|
+
${neonRootTokens()}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
* {
|
|
16
|
+
box-sizing: border-box;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
html,
|
|
20
|
+
body {
|
|
21
|
+
width: 100%;
|
|
22
|
+
min-height: 100%;
|
|
23
|
+
margin: 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
body {
|
|
27
|
+
display: flex;
|
|
28
|
+
align-items: center;
|
|
29
|
+
justify-content: center;
|
|
30
|
+
min-height: 100vh;
|
|
31
|
+
min-height: 100dvh;
|
|
32
|
+
padding: 24px;
|
|
33
|
+
color: var(--text);
|
|
34
|
+
font-family: var(--font-body);
|
|
35
|
+
${neonGridBackground()}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
${neonAmbientEffects()}
|
|
39
|
+
|
|
40
|
+
button,
|
|
41
|
+
input,
|
|
42
|
+
select,
|
|
43
|
+
textarea {
|
|
44
|
+
font: inherit;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
:focus-visible {
|
|
48
|
+
outline: 2px solid var(--accent);
|
|
49
|
+
outline-offset: 2px;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.auth-card {
|
|
53
|
+
width: 100%;
|
|
54
|
+
max-width: ${cardMaxWidth}px;
|
|
55
|
+
padding: 32px 28px;
|
|
56
|
+
border: 1px solid var(--line);
|
|
57
|
+
border-radius: 16px;
|
|
58
|
+
background: var(--panel);
|
|
59
|
+
box-shadow: 0 24px 80px rgba(2, 6, 18, 0.55);
|
|
60
|
+
backdrop-filter: blur(16px);
|
|
61
|
+
animation: neon-fade-up 260ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.auth-brand {
|
|
65
|
+
display: grid;
|
|
66
|
+
gap: 4px;
|
|
67
|
+
margin-bottom: 24px;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.auth-brand strong {
|
|
71
|
+
font-family: var(--font-heading);
|
|
72
|
+
font-size: 28px;
|
|
73
|
+
font-weight: 700;
|
|
74
|
+
letter-spacing: -0.01em;
|
|
75
|
+
${neonWordmark()}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.auth-brand span {
|
|
79
|
+
color: var(--muted);
|
|
80
|
+
font-size: 13px;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.auth-form {
|
|
84
|
+
display: grid;
|
|
85
|
+
gap: 16px;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.auth-field {
|
|
89
|
+
display: grid;
|
|
90
|
+
gap: 7px;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.auth-field label {
|
|
94
|
+
color: var(--muted);
|
|
95
|
+
font-size: 12px;
|
|
96
|
+
letter-spacing: 0.02em;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.auth-field input {
|
|
100
|
+
width: 100%;
|
|
101
|
+
height: 42px;
|
|
102
|
+
padding: 0 14px;
|
|
103
|
+
border: 1px solid var(--line);
|
|
104
|
+
border-radius: 10px;
|
|
105
|
+
outline: none;
|
|
106
|
+
background: rgba(19, 26, 42, 0.7);
|
|
107
|
+
color: var(--text);
|
|
108
|
+
transition: border-color 150ms ease, box-shadow 150ms ease;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.auth-field input:focus {
|
|
112
|
+
border-color: var(--accent);
|
|
113
|
+
box-shadow: var(--glow-cyan);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.auth-submit {
|
|
117
|
+
height: 44px;
|
|
118
|
+
margin-top: 4px;
|
|
119
|
+
border: 1px solid transparent;
|
|
120
|
+
border-radius: 10px;
|
|
121
|
+
background: var(--gradient);
|
|
122
|
+
color: #05070f;
|
|
123
|
+
font-weight: 600;
|
|
124
|
+
cursor: pointer;
|
|
125
|
+
transition: box-shadow 150ms ease, transform 120ms ease, opacity 120ms ease;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.auth-submit:hover:not(:disabled),
|
|
129
|
+
.auth-submit:focus:not(:disabled) {
|
|
130
|
+
box-shadow: 0 0 24px rgba(34, 211, 238, 0.4), 0 0 40px rgba(139, 92, 246, 0.28);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.auth-submit:active:not(:disabled) {
|
|
134
|
+
transform: scale(0.97);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.auth-submit:disabled {
|
|
138
|
+
cursor: progress;
|
|
139
|
+
opacity: 0.6;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.auth-message {
|
|
143
|
+
min-height: 18px;
|
|
144
|
+
margin: 0;
|
|
145
|
+
font-size: 12px;
|
|
146
|
+
line-height: 1.4;
|
|
147
|
+
overflow-wrap: anywhere;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.auth-message[hidden] {
|
|
151
|
+
display: none;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.auth-message.is-error {
|
|
155
|
+
color: var(--danger);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.auth-message.is-success {
|
|
159
|
+
color: var(--success);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.auth-back {
|
|
163
|
+
display: inline-block;
|
|
164
|
+
margin-top: 20px;
|
|
165
|
+
color: var(--muted);
|
|
166
|
+
font-size: 12px;
|
|
167
|
+
text-decoration: none;
|
|
168
|
+
transition: color 150ms ease;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.auth-back:hover,
|
|
172
|
+
.auth-back:focus {
|
|
173
|
+
color: var(--accent);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@media (prefers-reduced-motion: reduce) {
|
|
177
|
+
.auth-card {
|
|
178
|
+
animation: none;
|
|
179
|
+
}
|
|
180
|
+
}`;
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
// injects an account control into the graph page when server-side auth is
|
|
3
3
|
// enabled. In local (no-auth) mode there is no account, so the control is never
|
|
4
4
|
// shown. The bootstrap calls startAccountMenu(); the module only defines it.
|
|
5
|
-
// Styling mirrors the branded auth pages:
|
|
6
|
-
//
|
|
5
|
+
// Styling mirrors the branded auth pages: the unified neon identity (#0a0e1a
|
|
6
|
+
// glass surfaces, hairline borders, cyan #22d3ee accent), kept unobtrusive in
|
|
7
|
+
// the top-right corner.
|
|
7
8
|
export const createAccountMenuJs = () => `
|
|
8
9
|
const startAccountMenu = () => {
|
|
9
10
|
if (typeof fetch !== 'function' || typeof document === 'undefined' || !document.body) {
|
|
@@ -25,7 +26,7 @@ const startAccountMenu = () => {
|
|
|
25
26
|
' display: inline-flex;',
|
|
26
27
|
' align-items: center;',
|
|
27
28
|
' margin-left: 8px;',
|
|
28
|
-
' font-family: "
|
|
29
|
+
' font-family: var(--font-body, "DM Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif);',
|
|
29
30
|
'}',
|
|
30
31
|
'.bl-account--floating {',
|
|
31
32
|
' position: fixed;',
|
|
@@ -39,18 +40,20 @@ const startAccountMenu = () => {
|
|
|
39
40
|
' gap: 6px;',
|
|
40
41
|
' height: 32px;',
|
|
41
42
|
' padding: 0 12px;',
|
|
42
|
-
' border: 1px solid rgba(
|
|
43
|
-
' border-radius:
|
|
44
|
-
' background: rgba(
|
|
45
|
-
' color: #
|
|
43
|
+
' border: 1px solid rgba(255, 255, 255, 0.1);',
|
|
44
|
+
' border-radius: 10px;',
|
|
45
|
+
' background: rgba(15, 20, 32, 0.82);',
|
|
46
|
+
' color: #e7ecf6;',
|
|
46
47
|
' font: inherit;',
|
|
47
48
|
' font-size: 12px;',
|
|
48
49
|
' cursor: pointer;',
|
|
49
|
-
'
|
|
50
|
+
' backdrop-filter: blur(10px);',
|
|
51
|
+
' transition: border-color 150ms ease, box-shadow 150ms ease;',
|
|
50
52
|
'}',
|
|
51
53
|
'.bl-account-button:hover,',
|
|
52
54
|
'.bl-account-button:focus {',
|
|
53
|
-
' border-color: #
|
|
55
|
+
' border-color: #22d3ee;',
|
|
56
|
+
' box-shadow: 0 0 0 1px rgba(34, 211, 238, 0.4), 0 0 18px rgba(34, 211, 238, 0.24);',
|
|
54
57
|
' outline: none;',
|
|
55
58
|
'}',
|
|
56
59
|
'.bl-account-menu {',
|
|
@@ -60,10 +63,11 @@ const startAccountMenu = () => {
|
|
|
60
63
|
' z-index: 2147483000;',
|
|
61
64
|
' min-width: 168px;',
|
|
62
65
|
' padding: 6px;',
|
|
63
|
-
' border: 1px solid rgba(
|
|
64
|
-
' border-radius:
|
|
65
|
-
' background: rgba(
|
|
66
|
-
' box-shadow: 0 18px 60px rgba(
|
|
66
|
+
' border: 1px solid rgba(255, 255, 255, 0.1);',
|
|
67
|
+
' border-radius: 12px;',
|
|
68
|
+
' background: rgba(19, 26, 42, 0.96);',
|
|
69
|
+
' box-shadow: 0 18px 60px rgba(2, 6, 18, 0.55);',
|
|
70
|
+
' backdrop-filter: blur(14px);',
|
|
67
71
|
' display: grid;',
|
|
68
72
|
' gap: 2px;',
|
|
69
73
|
'}',
|
|
@@ -75,17 +79,18 @@ const startAccountMenu = () => {
|
|
|
75
79
|
' width: 100%;',
|
|
76
80
|
' padding: 8px 10px;',
|
|
77
81
|
' border: 0;',
|
|
78
|
-
' border-radius:
|
|
82
|
+
' border-radius: 8px;',
|
|
79
83
|
' background: transparent;',
|
|
80
|
-
' color: #
|
|
84
|
+
' color: #e7ecf6;',
|
|
81
85
|
' font: inherit;',
|
|
82
86
|
' font-size: 12px;',
|
|
83
87
|
' text-align: left;',
|
|
84
88
|
' cursor: pointer;',
|
|
89
|
+
' transition: background 150ms ease;',
|
|
85
90
|
'}',
|
|
86
91
|
'.bl-account-item:hover,',
|
|
87
92
|
'.bl-account-item:focus {',
|
|
88
|
-
' background: rgba(
|
|
93
|
+
' background: rgba(34, 211, 238, 0.16);',
|
|
89
94
|
' outline: none;',
|
|
90
95
|
'}'
|
|
91
96
|
].join('\\n')
|
|
@@ -49,26 +49,29 @@ function createGraphRenderer(host) {
|
|
|
49
49
|
let pointPositionsBuffer = new Float32Array(0)
|
|
50
50
|
let pointSizesBuffer = new Float32Array(0)
|
|
51
51
|
|
|
52
|
+
// Fallback theme when 'init' does not carry one. Mirrors the unified neon
|
|
53
|
+
// identity (cyan #22d3ee nodes, violet #8b5cf6 clusters/edges, #0a0e1a base)
|
|
54
|
+
// so the renderer stays on-brand even without a host-supplied theme.
|
|
52
55
|
const defaultTheme = {
|
|
53
|
-
node: [0.
|
|
54
|
-
nodeCluster: [0.
|
|
55
|
-
nodeHighlight: [0.
|
|
56
|
-
nodeSelected: [0.
|
|
56
|
+
node: [0.133, 0.827, 0.933, 1],
|
|
57
|
+
nodeCluster: [0.545, 0.361, 0.965, 1],
|
|
58
|
+
nodeHighlight: [0.404, 0.910, 0.976, 1],
|
|
59
|
+
nodeSelected: [0.906, 0.925, 0.965, 1],
|
|
57
60
|
nodePalette: [
|
|
58
|
-
[0.
|
|
59
|
-
[0.
|
|
60
|
-
[0.
|
|
61
|
-
[0.
|
|
62
|
-
[0.
|
|
63
|
-
[0.
|
|
64
|
-
[0.
|
|
65
|
-
[0.
|
|
66
|
-
[0.
|
|
67
|
-
[0.
|
|
61
|
+
[0.133, 0.827, 0.933, 1],
|
|
62
|
+
[0.220, 0.741, 0.973, 1],
|
|
63
|
+
[0.404, 0.910, 0.976, 1],
|
|
64
|
+
[0.506, 0.549, 0.973, 1],
|
|
65
|
+
[0.545, 0.361, 0.965, 1],
|
|
66
|
+
[0.655, 0.545, 0.980, 1],
|
|
67
|
+
[0.176, 0.831, 0.749, 1],
|
|
68
|
+
[0.753, 0.518, 0.988, 1],
|
|
69
|
+
[0.376, 0.647, 0.980, 1],
|
|
70
|
+
[0.369, 0.918, 0.831, 1]
|
|
68
71
|
],
|
|
69
|
-
edge: [0.
|
|
70
|
-
edgeHeavy: [0.
|
|
71
|
-
clear: [0.
|
|
72
|
+
edge: [0.545, 0.361, 0.965, 0.26],
|
|
73
|
+
edgeHeavy: [0.655, 0.545, 0.980, 0.5],
|
|
74
|
+
clear: [0.039, 0.055, 0.102, 1]
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
const theme = { ...defaultTheme }
|
|
@@ -13,7 +13,7 @@ const drawFallback = () => {
|
|
|
13
13
|
canvas.width = Math.floor(width * ratio)
|
|
14
14
|
canvas.height = Math.floor(height * ratio)
|
|
15
15
|
ctx2dFallback.setTransform(ratio, 0, 0, ratio, 0, 0)
|
|
16
|
-
ctx2dFallback.fillStyle = '#
|
|
16
|
+
ctx2dFallback.fillStyle = '#0a0e1a'
|
|
17
17
|
ctx2dFallback.fillRect(0, 0, width, height)
|
|
18
18
|
|
|
19
19
|
const nodes = Array.isArray(state.chunk.nodes) ? state.chunk.nodes : []
|
|
@@ -23,7 +23,7 @@ const drawFallback = () => {
|
|
|
23
23
|
nodeById.set(nodes[i][0], nodes[i])
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
ctx2dFallback.strokeStyle = 'rgba(
|
|
26
|
+
ctx2dFallback.strokeStyle = 'rgba(139,92,246,0.34)'
|
|
27
27
|
ctx2dFallback.lineWidth = 1.2
|
|
28
28
|
for (let i = 0; i < edges.length; i += 1) {
|
|
29
29
|
const edge = edges[i]
|
|
@@ -46,13 +46,13 @@ const drawFallback = () => {
|
|
|
46
46
|
const radius = Math.max(3.8, Math.min(18, 5.5 + node[7] * 0.7))
|
|
47
47
|
|
|
48
48
|
ctx2dFallback.beginPath()
|
|
49
|
-
ctx2dFallback.fillStyle = selected ? '#
|
|
49
|
+
ctx2dFallback.fillStyle = selected ? '#e7ecf6' : color
|
|
50
50
|
ctx2dFallback.arc(p.x, p.y, radius, 0, Math.PI * 2)
|
|
51
51
|
ctx2dFallback.fill()
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
ctx2dFallback.fillStyle = '#
|
|
55
|
-
ctx2dFallback.font = '12px
|
|
54
|
+
ctx2dFallback.fillStyle = '#8b94ab'
|
|
55
|
+
ctx2dFallback.font = '12px "DM Sans", system-ui, sans-serif'
|
|
56
56
|
ctx2dFallback.textAlign = 'center'
|
|
57
57
|
ctx2dFallback.fillText('Fallback canvas mode', Math.max(width, 320) / 2, 24)
|
|
58
58
|
}
|
|
@@ -46,29 +46,34 @@ const parseColor = (hex) => {
|
|
|
46
46
|
]
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
// Unified neon identity (shared with the SaaS app): cyan nodes (#22d3ee),
|
|
50
|
+
// violet edges/context (#8b5cf6), on the #0a0e1a base. The palette threads
|
|
51
|
+
// cyan→violet with a few supporting neons so context grouping stays legible
|
|
52
|
+
// while reading as one coherent, on-brand cluster field.
|
|
49
53
|
const graphTheme = {
|
|
50
|
-
node: parseColor('#
|
|
51
|
-
nodeCluster: parseColor('#
|
|
52
|
-
nodeHighlight: parseColor('#
|
|
53
|
-
nodeSelected: parseColor('#
|
|
54
|
+
node: parseColor('#22d3ee'),
|
|
55
|
+
nodeCluster: parseColor('#8b5cf6'),
|
|
56
|
+
nodeHighlight: parseColor('#67e8f9'),
|
|
57
|
+
nodeSelected: parseColor('#e7ecf6'),
|
|
54
58
|
nodePalette: [
|
|
55
|
-
parseColor('#
|
|
56
|
-
parseColor('#
|
|
57
|
-
parseColor('#
|
|
58
|
-
parseColor('#
|
|
59
|
-
parseColor('#
|
|
60
|
-
parseColor('#
|
|
61
|
-
parseColor('#
|
|
62
|
-
parseColor('#
|
|
63
|
-
parseColor('#
|
|
64
|
-
parseColor('#
|
|
59
|
+
parseColor('#22d3ee'),
|
|
60
|
+
parseColor('#38bdf8'),
|
|
61
|
+
parseColor('#67e8f9'),
|
|
62
|
+
parseColor('#818cf8'),
|
|
63
|
+
parseColor('#8b5cf6'),
|
|
64
|
+
parseColor('#a78bfa'),
|
|
65
|
+
parseColor('#2dd4bf'),
|
|
66
|
+
parseColor('#c084fc'),
|
|
67
|
+
parseColor('#60a5fa'),
|
|
68
|
+
parseColor('#5eead4')
|
|
65
69
|
],
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
70
|
+
// Edges read as violet-tinted "context" links against the cyan node field.
|
|
71
|
+
edge: [0.545, 0.361, 0.965, 0.24],
|
|
72
|
+
edgeHeavy: [0.655, 0.545, 0.98, 0.46],
|
|
73
|
+
clear: parseColor('#0a0e1a')
|
|
69
74
|
}
|
|
70
75
|
|
|
71
|
-
const segmentPalette = ['#
|
|
76
|
+
const segmentPalette = ['#22d3ee', '#38bdf8', '#67e8f9', '#818cf8', '#8b5cf6', '#a78bfa', '#2dd4bf', '#c084fc', '#60a5fa', '#5eead4']
|
|
72
77
|
|
|
73
78
|
const segmentColorIndex = (segment) => {
|
|
74
79
|
const value = String(segment || '')
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { neonFontImport, neonRootTokens } from './neon-theme.js';
|
|
3
4
|
// Crowquill Mono ships with the package under assets/fonts. Inline each face as a
|
|
4
5
|
// base64 woff2 data URI so the served CSS is self-contained (no extra route, no
|
|
5
|
-
// path resolution) and works identically for a global npm install.
|
|
6
|
-
//
|
|
6
|
+
// path resolution) and works identically for a global npm install. It is kept
|
|
7
|
+
// as the monospace face for graph node ids / code; a missing file degrades
|
|
8
|
+
// gracefully to the monospace fallback stack. Headings/body use the unified
|
|
9
|
+
// neon typography (Space Grotesk / DM Sans) via a Google Fonts @import with a
|
|
10
|
+
// robust system fallback so the UI still reads correctly offline.
|
|
7
11
|
const embedFontFace = (weight, style, file) => {
|
|
8
12
|
try {
|
|
9
13
|
const path = fileURLToPath(new URL(`../../../assets/fonts/${file}`, import.meta.url));
|
|
@@ -22,18 +26,10 @@ const crowquillFontFaces = () => [
|
|
|
22
26
|
]
|
|
23
27
|
.filter(Boolean)
|
|
24
28
|
.join('\n');
|
|
25
|
-
export const createClientCss = () => `${
|
|
29
|
+
export const createClientCss = () => `${neonFontImport()}
|
|
30
|
+
${crowquillFontFaces()}
|
|
26
31
|
:root {
|
|
27
|
-
|
|
28
|
-
--bg: #071019;
|
|
29
|
-
--panel: #0d1823;
|
|
30
|
-
--panel-strong: #112130;
|
|
31
|
-
--line: rgba(143, 172, 204, 0.18);
|
|
32
|
-
--text: #edf4ff;
|
|
33
|
-
--muted: #97a9bd;
|
|
34
|
-
--accent: #5aa8ff;
|
|
35
|
-
--accent-weak: rgba(90, 168, 255, 0.18);
|
|
36
|
-
--danger: #ff6b6b;
|
|
32
|
+
${neonRootTokens()}
|
|
37
33
|
}
|
|
38
34
|
|
|
39
35
|
* {
|
|
@@ -47,7 +43,7 @@ body {
|
|
|
47
43
|
margin: 0;
|
|
48
44
|
background: var(--bg);
|
|
49
45
|
color: var(--text);
|
|
50
|
-
font-family:
|
|
46
|
+
font-family: var(--font-body);
|
|
51
47
|
}
|
|
52
48
|
|
|
53
49
|
body {
|
|
@@ -63,6 +59,11 @@ select {
|
|
|
63
59
|
font: inherit;
|
|
64
60
|
}
|
|
65
61
|
|
|
62
|
+
:focus-visible {
|
|
63
|
+
outline: 2px solid var(--accent);
|
|
64
|
+
outline-offset: 2px;
|
|
65
|
+
}
|
|
66
|
+
|
|
66
67
|
.shell {
|
|
67
68
|
flex: 1 1 auto;
|
|
68
69
|
width: 100%;
|
|
@@ -88,9 +89,9 @@ select {
|
|
|
88
89
|
min-height: 72px;
|
|
89
90
|
padding: 10px 16px;
|
|
90
91
|
border-bottom: 1px solid var(--line);
|
|
91
|
-
background: rgba(
|
|
92
|
-
box-shadow: 0 1px
|
|
93
|
-
backdrop-filter: blur(
|
|
92
|
+
background: rgba(10, 14, 26, 0.78);
|
|
93
|
+
box-shadow: 0 1px 24px rgba(2, 6, 18, 0.5);
|
|
94
|
+
backdrop-filter: blur(14px);
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
.brand-block {
|
|
@@ -100,7 +101,15 @@ select {
|
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
.brand-block strong {
|
|
104
|
+
font-family: var(--font-heading);
|
|
103
105
|
font-size: 18px;
|
|
106
|
+
font-weight: 700;
|
|
107
|
+
letter-spacing: 0.01em;
|
|
108
|
+
background: var(--gradient);
|
|
109
|
+
-webkit-background-clip: text;
|
|
110
|
+
background-clip: text;
|
|
111
|
+
-webkit-text-fill-color: transparent;
|
|
112
|
+
color: transparent;
|
|
104
113
|
}
|
|
105
114
|
|
|
106
115
|
.graph-stage {
|
|
@@ -111,11 +120,12 @@ select {
|
|
|
111
120
|
user-select: none;
|
|
112
121
|
-webkit-user-select: none;
|
|
113
122
|
background:
|
|
114
|
-
linear-gradient(rgba(
|
|
115
|
-
linear-gradient(90deg, rgba(
|
|
116
|
-
radial-gradient(circle at
|
|
117
|
-
|
|
118
|
-
|
|
123
|
+
linear-gradient(rgba(34, 211, 238, 0.05) 1px, transparent 1px),
|
|
124
|
+
linear-gradient(90deg, rgba(139, 92, 246, 0.05) 1px, transparent 1px),
|
|
125
|
+
radial-gradient(circle at 50% -6%, rgba(34, 211, 238, 0.14), transparent 46%),
|
|
126
|
+
radial-gradient(circle at 82% 12%, rgba(139, 92, 246, 0.12), transparent 44%),
|
|
127
|
+
var(--bg);
|
|
128
|
+
background-size: 30px 30px, 30px 30px, auto, auto;
|
|
119
129
|
overflow: hidden;
|
|
120
130
|
}
|
|
121
131
|
|
|
@@ -161,21 +171,23 @@ select {
|
|
|
161
171
|
max-width: 220px;
|
|
162
172
|
transform: translate(-50%, calc(-100% - 12px));
|
|
163
173
|
padding: 4px 8px;
|
|
164
|
-
border: 1px solid
|
|
174
|
+
border: 1px solid var(--line);
|
|
165
175
|
border-radius: 6px;
|
|
166
|
-
background: rgba(
|
|
176
|
+
background: rgba(10, 14, 26, 0.9);
|
|
167
177
|
color: var(--text);
|
|
178
|
+
font-family: var(--font-mono);
|
|
168
179
|
font-size: 11px;
|
|
169
180
|
line-height: 1.25;
|
|
170
181
|
white-space: nowrap;
|
|
171
182
|
overflow: hidden;
|
|
172
183
|
text-overflow: ellipsis;
|
|
173
|
-
box-shadow: 0 12px 28px rgba(
|
|
184
|
+
box-shadow: 0 12px 28px rgba(2, 6, 18, 0.4);
|
|
174
185
|
}
|
|
175
186
|
|
|
176
187
|
.graph-label.is-focused {
|
|
177
|
-
border-color: rgba(
|
|
178
|
-
color:
|
|
188
|
+
border-color: rgba(34, 211, 238, 0.56);
|
|
189
|
+
color: var(--accent-hover);
|
|
190
|
+
box-shadow: var(--glow-cyan);
|
|
179
191
|
}
|
|
180
192
|
|
|
181
193
|
.graph-tooltip {
|
|
@@ -185,12 +197,13 @@ select {
|
|
|
185
197
|
padding: 8px 10px;
|
|
186
198
|
border: 1px solid var(--line);
|
|
187
199
|
border-radius: 6px;
|
|
188
|
-
background: rgba(
|
|
200
|
+
background: rgba(15, 20, 32, 0.96);
|
|
189
201
|
color: var(--text);
|
|
190
202
|
font-size: 12px;
|
|
191
203
|
line-height: 1.35;
|
|
192
204
|
pointer-events: none;
|
|
193
|
-
|
|
205
|
+
backdrop-filter: blur(8px);
|
|
206
|
+
box-shadow: 0 18px 44px rgba(2, 6, 18, 0.5);
|
|
194
207
|
}
|
|
195
208
|
|
|
196
209
|
.graph-tooltip strong,
|
|
@@ -212,10 +225,11 @@ select {
|
|
|
212
225
|
z-index: 3;
|
|
213
226
|
width: 180px;
|
|
214
227
|
height: 120px;
|
|
215
|
-
border: 1px solid
|
|
228
|
+
border: 1px solid var(--line);
|
|
216
229
|
border-radius: 8px;
|
|
217
|
-
background: rgba(
|
|
218
|
-
|
|
230
|
+
background: rgba(15, 20, 32, 0.82);
|
|
231
|
+
backdrop-filter: blur(10px);
|
|
232
|
+
box-shadow: 0 18px 40px rgba(2, 6, 18, 0.42);
|
|
219
233
|
}
|
|
220
234
|
|
|
221
235
|
.eyebrow {
|
|
@@ -257,6 +271,7 @@ select {
|
|
|
257
271
|
.agent-filter select:focus,
|
|
258
272
|
.context-filter select:focus {
|
|
259
273
|
border-color: var(--accent);
|
|
274
|
+
box-shadow: var(--glow-cyan);
|
|
260
275
|
}
|
|
261
276
|
|
|
262
277
|
.toolbar {
|
|
@@ -298,8 +313,11 @@ select {
|
|
|
298
313
|
}
|
|
299
314
|
|
|
300
315
|
.metric-chip strong {
|
|
316
|
+
font-family: var(--font-heading);
|
|
301
317
|
font-size: 15px;
|
|
302
318
|
line-height: 1;
|
|
319
|
+
font-variant-numeric: tabular-nums;
|
|
320
|
+
color: var(--accent);
|
|
303
321
|
}
|
|
304
322
|
|
|
305
323
|
.metric-chip small {
|
|
@@ -367,11 +385,11 @@ li small {
|
|
|
367
385
|
padding: 12px;
|
|
368
386
|
border: 1px solid var(--line);
|
|
369
387
|
border-radius: 8px;
|
|
370
|
-
background:
|
|
388
|
+
background: rgba(10, 14, 26, 0.72);
|
|
371
389
|
color: var(--text);
|
|
372
390
|
white-space: pre-wrap;
|
|
373
391
|
overflow: auto;
|
|
374
|
-
font-family:
|
|
392
|
+
font-family: var(--font-mono);
|
|
375
393
|
font-size: 12px;
|
|
376
394
|
line-height: 1.5;
|
|
377
395
|
}
|
|
@@ -565,6 +583,8 @@ li small {
|
|
|
565
583
|
|
|
566
584
|
.content-dialog h2 {
|
|
567
585
|
margin-top: 6px;
|
|
586
|
+
font-family: var(--font-heading);
|
|
587
|
+
font-weight: 600;
|
|
568
588
|
font-size: 19px;
|
|
569
589
|
line-height: 1.15;
|
|
570
590
|
overflow-wrap: anywhere;
|
|
@@ -629,7 +649,7 @@ li small {
|
|
|
629
649
|
padding: 10px;
|
|
630
650
|
border: 1px solid var(--line);
|
|
631
651
|
border-radius: 8px;
|
|
632
|
-
background:
|
|
652
|
+
background: rgba(10, 14, 26, 0.6);
|
|
633
653
|
display: grid;
|
|
634
654
|
grid-template-rows: auto minmax(0, 1fr);
|
|
635
655
|
gap: 8px;
|
|
@@ -703,7 +723,7 @@ li small {
|
|
|
703
723
|
display: flex;
|
|
704
724
|
align-items: center;
|
|
705
725
|
justify-content: center;
|
|
706
|
-
background: linear-gradient(180deg, rgba(
|
|
726
|
+
background: linear-gradient(180deg, rgba(10, 14, 26, 0), rgba(10, 14, 26, 0.84));
|
|
707
727
|
}
|
|
708
728
|
|
|
709
729
|
.app-footer small {
|
|
@@ -1,167 +1,13 @@
|
|
|
1
1
|
// Self-contained branded auth pages for the graph web server. These are served
|
|
2
|
-
// as plain strings (inline <style> + inline <script>, no external assets
|
|
3
|
-
// framework) so they work identically for a
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
--panel-strong: #112130;
|
|
12
|
-
--line: rgba(143, 172, 204, 0.18);
|
|
13
|
-
--text: #edf4ff;
|
|
14
|
-
--muted: #97a9bd;
|
|
15
|
-
--accent: #5aa8ff;
|
|
16
|
-
--accent-weak: rgba(90, 168, 255, 0.18);
|
|
17
|
-
--danger: #ff6b6b;
|
|
18
|
-
--success: #57d9a3;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
* {
|
|
22
|
-
box-sizing: border-box;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
html,
|
|
26
|
-
body {
|
|
27
|
-
width: 100%;
|
|
28
|
-
min-height: 100%;
|
|
29
|
-
margin: 0;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
body {
|
|
33
|
-
display: flex;
|
|
34
|
-
align-items: center;
|
|
35
|
-
justify-content: center;
|
|
36
|
-
min-height: 100vh;
|
|
37
|
-
min-height: 100dvh;
|
|
38
|
-
padding: 24px;
|
|
39
|
-
color: var(--text);
|
|
40
|
-
font-family: "Crowquill Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
41
|
-
background:
|
|
42
|
-
linear-gradient(rgba(164, 197, 230, 0.05) 1px, transparent 1px),
|
|
43
|
-
linear-gradient(90deg, rgba(164, 197, 230, 0.05) 1px, transparent 1px),
|
|
44
|
-
radial-gradient(circle at top, rgba(43, 93, 143, 0.22), transparent 44%),
|
|
45
|
-
#08131d;
|
|
46
|
-
background-size: 28px 28px, 28px 28px, auto;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
button,
|
|
50
|
-
input {
|
|
51
|
-
font: inherit;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
.auth-card {
|
|
55
|
-
width: 100%;
|
|
56
|
-
max-width: 380px;
|
|
57
|
-
padding: 32px 28px;
|
|
58
|
-
border: 1px solid var(--line);
|
|
59
|
-
border-radius: 14px;
|
|
60
|
-
background: rgba(13, 24, 35, 0.92);
|
|
61
|
-
box-shadow: 0 24px 80px rgba(1, 6, 13, 0.48);
|
|
62
|
-
backdrop-filter: blur(8px);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
.auth-brand {
|
|
66
|
-
display: grid;
|
|
67
|
-
gap: 4px;
|
|
68
|
-
margin-bottom: 24px;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
.auth-brand strong {
|
|
72
|
-
font-size: 26px;
|
|
73
|
-
letter-spacing: 0.01em;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
.auth-brand span {
|
|
77
|
-
color: var(--muted);
|
|
78
|
-
font-size: 13px;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
.auth-form {
|
|
82
|
-
display: grid;
|
|
83
|
-
gap: 16px;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
.auth-field {
|
|
87
|
-
display: grid;
|
|
88
|
-
gap: 7px;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
.auth-field label {
|
|
92
|
-
color: var(--muted);
|
|
93
|
-
font-size: 12px;
|
|
94
|
-
letter-spacing: 0.02em;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
.auth-field input {
|
|
98
|
-
width: 100%;
|
|
99
|
-
height: 42px;
|
|
100
|
-
padding: 0 14px;
|
|
101
|
-
border: 1px solid var(--line);
|
|
102
|
-
border-radius: 8px;
|
|
103
|
-
outline: none;
|
|
104
|
-
background: rgba(12, 24, 36, 0.94);
|
|
105
|
-
color: var(--text);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
.auth-field input:focus {
|
|
109
|
-
border-color: var(--accent);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
.auth-submit {
|
|
113
|
-
height: 44px;
|
|
114
|
-
margin-top: 4px;
|
|
115
|
-
border: 1px solid var(--accent);
|
|
116
|
-
border-radius: 8px;
|
|
117
|
-
background: var(--accent-weak);
|
|
118
|
-
color: var(--text);
|
|
119
|
-
cursor: pointer;
|
|
120
|
-
transition: background 120ms ease, opacity 120ms ease;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
.auth-submit:hover:not(:disabled),
|
|
124
|
-
.auth-submit:focus:not(:disabled) {
|
|
125
|
-
background: rgba(90, 168, 255, 0.3);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
.auth-submit:disabled {
|
|
129
|
-
cursor: progress;
|
|
130
|
-
opacity: 0.6;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
.auth-message {
|
|
134
|
-
min-height: 18px;
|
|
135
|
-
margin: 0;
|
|
136
|
-
font-size: 12px;
|
|
137
|
-
line-height: 1.4;
|
|
138
|
-
overflow-wrap: anywhere;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
.auth-message[hidden] {
|
|
142
|
-
display: none;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
.auth-message.is-error {
|
|
146
|
-
color: var(--danger);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
.auth-message.is-success {
|
|
150
|
-
color: var(--success);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
.auth-back {
|
|
154
|
-
display: inline-block;
|
|
155
|
-
margin-top: 20px;
|
|
156
|
-
color: var(--muted);
|
|
157
|
-
font-size: 12px;
|
|
158
|
-
text-decoration: none;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
.auth-back:hover,
|
|
162
|
-
.auth-back:focus {
|
|
163
|
-
color: var(--accent);
|
|
164
|
-
}`;
|
|
2
|
+
// as plain strings (inline <style> + inline <script>, no external assets beyond
|
|
3
|
+
// the shared Google Fonts @import, no framework) so they work identically for a
|
|
4
|
+
// global npm install. The visual identity is the unified neon system shared
|
|
5
|
+
// with the SaaS app (see neon-theme.ts): #0a0e1a base, cyan #22d3ee + violet
|
|
6
|
+
// #8b5cf6, Space Grotesk headings / DM Sans body, glass card + gradient
|
|
7
|
+
// wordmark + ambient glow, so the app -> SSO -> graph handoff reads as one
|
|
8
|
+
// product.
|
|
9
|
+
import { authSharedStyles } from './auth-styles.js';
|
|
10
|
+
const authStyles = () => authSharedStyles(380);
|
|
165
11
|
const documentShell = (title, body, script) => `<!DOCTYPE html>
|
|
166
12
|
<html lang="en">
|
|
167
13
|
<head>
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Single source of truth for the unified neon identity shared by every
|
|
2
|
+
// self-contained page served by the graph web server (login, settings, OAuth
|
|
3
|
+
// authorize, and the graph explorer CSS). Keeping the tokens, font import and
|
|
4
|
+
// ambient effects in one place means the app -> SSO -> graph handoff reads as a
|
|
5
|
+
// single product, and a palette change lands everywhere at once.
|
|
6
|
+
//
|
|
7
|
+
// The identity mirrors the SaaS app: #0a0e1a base, cyan #22d3ee + violet
|
|
8
|
+
// #8b5cf6 accents, Space Grotesk headings / DM Sans body via a Google Fonts
|
|
9
|
+
// @import with a robust system fallback so it degrades gracefully offline. A
|
|
10
|
+
// monospace stack stays for tokens / code / graph node ids.
|
|
11
|
+
// Google Fonts @import for Space Grotesk (headings) + DM Sans (body). Placed at
|
|
12
|
+
// the very top of the served stylesheet as @import requires. If the network is
|
|
13
|
+
// unavailable the fallback stacks in --font-heading / --font-body take over.
|
|
14
|
+
export const neonFontImport = () => "@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&family=Space+Grotesk:wght@400;500;600;700&display=swap');";
|
|
15
|
+
// Font-family stacks. Fallbacks keep the layout correct and legible offline.
|
|
16
|
+
export const neonFontStacks = () => ` --font-heading: "Space Grotesk", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
17
|
+
--font-body: "DM Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
18
|
+
--font-mono: "Crowquill Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;`;
|
|
19
|
+
// The canonical color tokens. Cyan is the primary/node accent, violet the
|
|
20
|
+
// secondary/context accent; together they form the primary gradient.
|
|
21
|
+
export const neonColorTokens = () => ` --bg: #0a0e1a;
|
|
22
|
+
--surface: #0f1420;
|
|
23
|
+
--surface-2: #131a2a;
|
|
24
|
+
--panel: rgba(15, 20, 32, 0.82);
|
|
25
|
+
--panel-strong: rgba(19, 26, 42, 0.92);
|
|
26
|
+
--line: rgba(255, 255, 255, 0.09);
|
|
27
|
+
--line-strong: rgba(255, 255, 255, 0.14);
|
|
28
|
+
--text: #e7ecf6;
|
|
29
|
+
--muted: #8b94ab;
|
|
30
|
+
--accent: #22d3ee;
|
|
31
|
+
--accent-hover: #67e8f9;
|
|
32
|
+
--accent-weak: rgba(34, 211, 238, 0.16);
|
|
33
|
+
--violet: #8b5cf6;
|
|
34
|
+
--violet-hover: #a78bfa;
|
|
35
|
+
--gradient: linear-gradient(120deg, #22d3ee, #8b5cf6);
|
|
36
|
+
--glow-cyan: 0 0 0 1px rgba(34, 211, 238, 0.4), 0 0 22px rgba(34, 211, 238, 0.28);
|
|
37
|
+
--danger: #ff6b6b;
|
|
38
|
+
--success: #57d9a3;`;
|
|
39
|
+
// Full :root token block (colors + fonts) for the branded auth-style pages.
|
|
40
|
+
export const neonRootTokens = () => ` color-scheme: dark;
|
|
41
|
+
${neonColorTokens()}
|
|
42
|
+
${neonFontStacks()}`;
|
|
43
|
+
// The faint cyber-grid + soft radial glow background used on branded screens.
|
|
44
|
+
// Layered so the grid sits above the base and the radial glow blooms from the
|
|
45
|
+
// top. Kept as a reusable string so login / authorize / settings / graph-stage
|
|
46
|
+
// all render the exact same backdrop.
|
|
47
|
+
export const neonGridBackground = () => ` background:
|
|
48
|
+
linear-gradient(rgba(34, 211, 238, 0.05) 1px, transparent 1px),
|
|
49
|
+
linear-gradient(90deg, rgba(139, 92, 246, 0.05) 1px, transparent 1px),
|
|
50
|
+
radial-gradient(circle at 50% -10%, rgba(34, 211, 238, 0.16), transparent 46%),
|
|
51
|
+
radial-gradient(circle at 82% 8%, rgba(139, 92, 246, 0.14), transparent 42%),
|
|
52
|
+
var(--bg);
|
|
53
|
+
background-size: 30px 30px, 30px 30px, auto, auto;`;
|
|
54
|
+
// A gradient wordmark: cyan -> violet text clipped to the glyphs. Used for the
|
|
55
|
+
// "Brainlink" brand lockup so every entry point shares the same signature.
|
|
56
|
+
export const neonWordmark = () => ` background: var(--gradient);
|
|
57
|
+
-webkit-background-clip: text;
|
|
58
|
+
background-clip: text;
|
|
59
|
+
-webkit-text-fill-color: transparent;
|
|
60
|
+
color: transparent;`;
|
|
61
|
+
// Slow ambient glow "blobs" behind branded cards, plus reduced-motion + focus
|
|
62
|
+
// helpers. Scoped to the branded auth-style pages via the ::before/::after on
|
|
63
|
+
// body; the caller supplies the body selector context.
|
|
64
|
+
export const neonAmbientEffects = () => `@keyframes neon-blob-a {
|
|
65
|
+
0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
|
|
66
|
+
50% { transform: translate3d(6%, 8%, 0) scale(1.12); }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@keyframes neon-blob-b {
|
|
70
|
+
0%, 100% { transform: translate3d(0, 0, 0) scale(1.05); }
|
|
71
|
+
50% { transform: translate3d(-7%, -6%, 0) scale(0.92); }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@keyframes neon-fade-up {
|
|
75
|
+
from { opacity: 0; transform: translateY(10px); }
|
|
76
|
+
to { opacity: 1; transform: translateY(0); }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
body::before,
|
|
80
|
+
body::after {
|
|
81
|
+
content: "";
|
|
82
|
+
position: fixed;
|
|
83
|
+
z-index: -1;
|
|
84
|
+
width: 46vmax;
|
|
85
|
+
height: 46vmax;
|
|
86
|
+
border-radius: 50%;
|
|
87
|
+
filter: blur(90px);
|
|
88
|
+
pointer-events: none;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
body::before {
|
|
92
|
+
top: -18vmax;
|
|
93
|
+
left: -14vmax;
|
|
94
|
+
background: radial-gradient(circle, rgba(34, 211, 238, 0.12), transparent 70%);
|
|
95
|
+
animation: neon-blob-a 22s ease-in-out infinite;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
body::after {
|
|
99
|
+
right: -16vmax;
|
|
100
|
+
bottom: -18vmax;
|
|
101
|
+
background: radial-gradient(circle, rgba(139, 92, 246, 0.12), transparent 70%);
|
|
102
|
+
animation: neon-blob-b 26s ease-in-out infinite;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@media (prefers-reduced-motion: reduce) {
|
|
106
|
+
body::before,
|
|
107
|
+
body::after {
|
|
108
|
+
animation: none;
|
|
109
|
+
}
|
|
110
|
+
}`;
|
|
@@ -1,21 +1,14 @@
|
|
|
1
1
|
// Self-contained branded Settings page for the graph web server. Served as a
|
|
2
|
-
// plain string (inline <style> + inline <script>, no external assets
|
|
3
|
-
// framework) so it works identically for a
|
|
4
|
-
// identity
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
--line: rgba(143, 172, 204, 0.18);
|
|
13
|
-
--text: #edf4ff;
|
|
14
|
-
--muted: #97a9bd;
|
|
15
|
-
--accent: #5aa8ff;
|
|
16
|
-
--accent-weak: rgba(90, 168, 255, 0.18);
|
|
17
|
-
--danger: #ff6b6b;
|
|
18
|
-
--success: #57d9a3;
|
|
2
|
+
// plain string (inline <style> + inline <script>, no external assets beyond the
|
|
3
|
+
// shared Google Fonts @import, no framework) so it works identically for a
|
|
4
|
+
// global npm install. The visual identity is the unified neon system shared
|
|
5
|
+
// with the auth pages and the graph client (see neon-theme.ts): #0a0e1a base,
|
|
6
|
+
// faint cyber grid, cyan #22d3ee + violet #8b5cf6, Space Grotesk headings / DM
|
|
7
|
+
// Sans body, glass card + gradient wordmark + cyan focus glow.
|
|
8
|
+
import { neonAmbientEffects, neonFontImport, neonGridBackground, neonRootTokens, neonWordmark } from './neon-theme.js';
|
|
9
|
+
const settingsStyles = () => `${neonFontImport()}
|
|
10
|
+
:root {
|
|
11
|
+
${neonRootTokens()}
|
|
19
12
|
}
|
|
20
13
|
|
|
21
14
|
* {
|
|
@@ -37,30 +30,40 @@ body {
|
|
|
37
30
|
min-height: 100dvh;
|
|
38
31
|
padding: 24px;
|
|
39
32
|
color: var(--text);
|
|
40
|
-
font-family:
|
|
41
|
-
|
|
42
|
-
linear-gradient(rgba(164, 197, 230, 0.05) 1px, transparent 1px),
|
|
43
|
-
linear-gradient(90deg, rgba(164, 197, 230, 0.05) 1px, transparent 1px),
|
|
44
|
-
radial-gradient(circle at top, rgba(43, 93, 143, 0.22), transparent 44%),
|
|
45
|
-
#08131d;
|
|
46
|
-
background-size: 28px 28px, 28px 28px, auto;
|
|
33
|
+
font-family: var(--font-body);
|
|
34
|
+
${neonGridBackground()}
|
|
47
35
|
}
|
|
48
36
|
|
|
37
|
+
${neonAmbientEffects()}
|
|
38
|
+
|
|
49
39
|
button,
|
|
50
40
|
input,
|
|
51
|
-
select
|
|
41
|
+
select,
|
|
42
|
+
textarea {
|
|
52
43
|
font: inherit;
|
|
53
44
|
}
|
|
54
45
|
|
|
46
|
+
:focus-visible {
|
|
47
|
+
outline: 2px solid var(--accent);
|
|
48
|
+
outline-offset: 2px;
|
|
49
|
+
}
|
|
50
|
+
|
|
55
51
|
.settings-card {
|
|
56
52
|
width: 100%;
|
|
57
53
|
max-width: 440px;
|
|
58
54
|
padding: 32px 28px;
|
|
59
55
|
border: 1px solid var(--line);
|
|
60
|
-
border-radius:
|
|
61
|
-
background:
|
|
62
|
-
box-shadow: 0 24px 80px rgba(
|
|
63
|
-
backdrop-filter: blur(
|
|
56
|
+
border-radius: 16px;
|
|
57
|
+
background: var(--panel);
|
|
58
|
+
box-shadow: 0 24px 80px rgba(2, 6, 18, 0.55);
|
|
59
|
+
backdrop-filter: blur(16px);
|
|
60
|
+
animation: neon-fade-up 260ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@media (prefers-reduced-motion: reduce) {
|
|
64
|
+
.settings-card {
|
|
65
|
+
animation: none;
|
|
66
|
+
}
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
.settings-brand {
|
|
@@ -70,8 +73,11 @@ select {
|
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
.settings-brand strong {
|
|
73
|
-
font-
|
|
74
|
-
|
|
76
|
+
font-family: var(--font-heading);
|
|
77
|
+
font-size: 28px;
|
|
78
|
+
font-weight: 700;
|
|
79
|
+
letter-spacing: -0.01em;
|
|
80
|
+
${neonWordmark()}
|
|
75
81
|
}
|
|
76
82
|
|
|
77
83
|
.settings-brand span {
|
|
@@ -101,15 +107,17 @@ select {
|
|
|
101
107
|
height: 42px;
|
|
102
108
|
padding: 0 14px;
|
|
103
109
|
border: 1px solid var(--line);
|
|
104
|
-
border-radius:
|
|
110
|
+
border-radius: 10px;
|
|
105
111
|
outline: none;
|
|
106
|
-
background: rgba(
|
|
112
|
+
background: rgba(19, 26, 42, 0.7);
|
|
107
113
|
color: var(--text);
|
|
114
|
+
transition: border-color 150ms ease, box-shadow 150ms ease;
|
|
108
115
|
}
|
|
109
116
|
|
|
110
117
|
.settings-field input:focus,
|
|
111
118
|
.settings-field select:focus {
|
|
112
119
|
border-color: var(--accent);
|
|
120
|
+
box-shadow: var(--glow-cyan);
|
|
113
121
|
}
|
|
114
122
|
|
|
115
123
|
.settings-check {
|
|
@@ -134,17 +142,22 @@ select {
|
|
|
134
142
|
.settings-submit {
|
|
135
143
|
height: 44px;
|
|
136
144
|
margin-top: 4px;
|
|
137
|
-
border: 1px solid
|
|
138
|
-
border-radius:
|
|
139
|
-
background: var(--
|
|
140
|
-
color:
|
|
145
|
+
border: 1px solid transparent;
|
|
146
|
+
border-radius: 10px;
|
|
147
|
+
background: var(--gradient);
|
|
148
|
+
color: #05070f;
|
|
149
|
+
font-weight: 600;
|
|
141
150
|
cursor: pointer;
|
|
142
|
-
transition:
|
|
151
|
+
transition: box-shadow 150ms ease, transform 120ms ease, opacity 120ms ease;
|
|
143
152
|
}
|
|
144
153
|
|
|
145
154
|
.settings-submit:hover:not(:disabled),
|
|
146
155
|
.settings-submit:focus:not(:disabled) {
|
|
147
|
-
|
|
156
|
+
box-shadow: 0 0 24px rgba(34, 211, 238, 0.4), 0 0 40px rgba(139, 92, 246, 0.28);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.settings-submit:active:not(:disabled) {
|
|
160
|
+
transform: scale(0.97);
|
|
148
161
|
}
|
|
149
162
|
|
|
150
163
|
.settings-submit:disabled {
|
|
@@ -195,6 +208,8 @@ select {
|
|
|
195
208
|
|
|
196
209
|
.settings-section h2 {
|
|
197
210
|
margin: 0;
|
|
211
|
+
font-family: var(--font-heading);
|
|
212
|
+
font-weight: 600;
|
|
198
213
|
font-size: 16px;
|
|
199
214
|
letter-spacing: 0.01em;
|
|
200
215
|
}
|
|
@@ -210,28 +225,34 @@ select {
|
|
|
210
225
|
margin: 0;
|
|
211
226
|
padding: 10px 12px;
|
|
212
227
|
border: 1px solid var(--line);
|
|
213
|
-
border-radius:
|
|
214
|
-
background: rgba(
|
|
228
|
+
border-radius: 10px;
|
|
229
|
+
background: rgba(19, 26, 42, 0.55);
|
|
215
230
|
color: var(--muted);
|
|
216
231
|
font-size: 12px;
|
|
217
232
|
line-height: 1.5;
|
|
218
233
|
overflow-wrap: anywhere;
|
|
219
234
|
}
|
|
220
235
|
|
|
236
|
+
.settings-status code {
|
|
237
|
+
font-family: var(--font-mono);
|
|
238
|
+
color: var(--accent);
|
|
239
|
+
}
|
|
240
|
+
|
|
221
241
|
.settings-secondary {
|
|
222
242
|
height: 42px;
|
|
223
243
|
border: 1px solid var(--line);
|
|
224
|
-
border-radius:
|
|
225
|
-
background: rgba(
|
|
244
|
+
border-radius: 10px;
|
|
245
|
+
background: rgba(19, 26, 42, 0.7);
|
|
226
246
|
color: var(--text);
|
|
227
247
|
cursor: pointer;
|
|
228
|
-
transition: border-color
|
|
248
|
+
transition: border-color 150ms ease, background 150ms ease, box-shadow 150ms ease, opacity 120ms ease;
|
|
229
249
|
}
|
|
230
250
|
|
|
231
251
|
.settings-secondary:hover:not(:disabled),
|
|
232
252
|
.settings-secondary:focus:not(:disabled) {
|
|
233
253
|
border-color: var(--accent);
|
|
234
|
-
background:
|
|
254
|
+
background: var(--accent-weak);
|
|
255
|
+
box-shadow: var(--glow-cyan);
|
|
235
256
|
}
|
|
236
257
|
|
|
237
258
|
.settings-secondary:disabled {
|
|
@@ -253,19 +274,21 @@ select {
|
|
|
253
274
|
min-height: 96px;
|
|
254
275
|
padding: 10px 12px;
|
|
255
276
|
border: 1px solid var(--line);
|
|
256
|
-
border-radius:
|
|
277
|
+
border-radius: 10px;
|
|
257
278
|
outline: none;
|
|
258
279
|
resize: vertical;
|
|
259
|
-
background: rgba(
|
|
280
|
+
background: rgba(19, 26, 42, 0.7);
|
|
260
281
|
color: var(--text);
|
|
261
|
-
font:
|
|
282
|
+
font-family: var(--font-mono);
|
|
262
283
|
font-size: 11px;
|
|
263
284
|
line-height: 1.4;
|
|
264
285
|
overflow-wrap: anywhere;
|
|
286
|
+
transition: border-color 150ms ease, box-shadow 150ms ease;
|
|
265
287
|
}
|
|
266
288
|
|
|
267
289
|
.settings-key textarea:focus {
|
|
268
290
|
border-color: var(--accent);
|
|
291
|
+
box-shadow: var(--glow-cyan);
|
|
269
292
|
}
|
|
270
293
|
|
|
271
294
|
.settings-key-actions {
|
|
@@ -278,18 +301,19 @@ select {
|
|
|
278
301
|
height: 32px;
|
|
279
302
|
padding: 0 14px;
|
|
280
303
|
border: 1px solid var(--line);
|
|
281
|
-
border-radius:
|
|
282
|
-
background: rgba(
|
|
304
|
+
border-radius: 10px;
|
|
305
|
+
background: rgba(19, 26, 42, 0.7);
|
|
283
306
|
color: var(--text);
|
|
284
307
|
cursor: pointer;
|
|
285
308
|
font-size: 12px;
|
|
286
|
-
transition: border-color
|
|
309
|
+
transition: border-color 150ms ease, background 150ms ease, box-shadow 150ms ease;
|
|
287
310
|
}
|
|
288
311
|
|
|
289
312
|
.settings-copy:hover,
|
|
290
313
|
.settings-copy:focus {
|
|
291
314
|
border-color: var(--accent);
|
|
292
|
-
background:
|
|
315
|
+
background: var(--accent-weak);
|
|
316
|
+
box-shadow: var(--glow-cyan);
|
|
293
317
|
}`;
|
|
294
318
|
const settingsScript = () => `(function () {
|
|
295
319
|
var form = document.getElementById('settingsForm');
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
// Self-contained branded OAuth authorization/consent page for the Brainlink MCP
|
|
2
|
-
// server. Served as a plain string (inline <style>, no external assets
|
|
3
|
-
// framework, no JavaScript) so it works
|
|
4
|
-
// The visual identity
|
|
5
|
-
//
|
|
6
|
-
//
|
|
2
|
+
// server. Served as a plain string (inline <style>, no external assets beyond
|
|
3
|
+
// the shared Google Fonts @import, no framework, no JavaScript) so it works
|
|
4
|
+
// identically for a global npm install. The visual identity is the unified neon
|
|
5
|
+
// system shared with the SaaS app and the graph client (see neon-theme.ts /
|
|
6
|
+
// auth-styles.ts): #0a0e1a base, faint cyber grid, cyan #22d3ee + violet
|
|
7
|
+
// #8b5cf6, Space Grotesk headings / DM Sans body, glass card + gradient
|
|
8
|
+
// wordmark, so the SSO consent step reads as part of one product.
|
|
9
|
+
import { authSharedStyles } from '../application/frontend/auth-styles.js';
|
|
7
10
|
// HTML-escape dynamic values. clientName arrives from an untrusted OAuth client
|
|
8
11
|
// and hidden values carry OAuth params, so both must never allow HTML injection.
|
|
9
12
|
const escapeHtml = (value) => value
|
|
@@ -12,85 +15,14 @@ const escapeHtml = (value) => value
|
|
|
12
15
|
.replace(/>/g, '>')
|
|
13
16
|
.replace(/"/g, '"')
|
|
14
17
|
.replace(/'/g, ''');
|
|
15
|
-
const authorizeStyles = () =>
|
|
16
|
-
color-scheme: dark;
|
|
17
|
-
--bg: #08131d;
|
|
18
|
-
--panel: #0d1823;
|
|
19
|
-
--panel-strong: #112130;
|
|
20
|
-
--line: rgba(143, 172, 204, 0.18);
|
|
21
|
-
--text: #edf4ff;
|
|
22
|
-
--muted: #97a9bd;
|
|
23
|
-
--accent: #5aa8ff;
|
|
24
|
-
--accent-weak: rgba(90, 168, 255, 0.18);
|
|
25
|
-
--danger: #ff6b6b;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
* {
|
|
29
|
-
box-sizing: border-box;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
html,
|
|
33
|
-
body {
|
|
34
|
-
width: 100%;
|
|
35
|
-
min-height: 100%;
|
|
36
|
-
margin: 0;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
body {
|
|
40
|
-
display: flex;
|
|
41
|
-
align-items: center;
|
|
42
|
-
justify-content: center;
|
|
43
|
-
min-height: 100vh;
|
|
44
|
-
min-height: 100dvh;
|
|
45
|
-
padding: 24px;
|
|
46
|
-
color: var(--text);
|
|
47
|
-
font-family: "Crowquill Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
48
|
-
background:
|
|
49
|
-
linear-gradient(rgba(164, 197, 230, 0.05) 1px, transparent 1px),
|
|
50
|
-
linear-gradient(90deg, rgba(164, 197, 230, 0.05) 1px, transparent 1px),
|
|
51
|
-
radial-gradient(circle at top, rgba(43, 93, 143, 0.22), transparent 44%),
|
|
52
|
-
#08131d;
|
|
53
|
-
background-size: 28px 28px, 28px 28px, auto;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
button,
|
|
57
|
-
input {
|
|
58
|
-
font: inherit;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
.auth-card {
|
|
62
|
-
width: 100%;
|
|
63
|
-
max-width: 400px;
|
|
64
|
-
padding: 32px 28px;
|
|
65
|
-
border: 1px solid var(--line);
|
|
66
|
-
border-radius: 14px;
|
|
67
|
-
background: rgba(13, 24, 35, 0.92);
|
|
68
|
-
box-shadow: 0 24px 80px rgba(1, 6, 13, 0.48);
|
|
69
|
-
backdrop-filter: blur(8px);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
.auth-brand {
|
|
73
|
-
display: grid;
|
|
74
|
-
gap: 4px;
|
|
75
|
-
margin-bottom: 20px;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
.auth-brand strong {
|
|
79
|
-
font-size: 26px;
|
|
80
|
-
letter-spacing: 0.01em;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
.auth-brand span {
|
|
84
|
-
color: var(--muted);
|
|
85
|
-
font-size: 13px;
|
|
86
|
-
}
|
|
18
|
+
const authorizeStyles = () => `${authSharedStyles(400)}
|
|
87
19
|
|
|
88
20
|
.auth-consent {
|
|
89
21
|
margin: 0 0 20px;
|
|
90
22
|
padding: 12px 14px;
|
|
91
23
|
border: 1px solid var(--line);
|
|
92
|
-
border-radius:
|
|
93
|
-
background: rgba(
|
|
24
|
+
border-radius: 12px;
|
|
25
|
+
background: rgba(19, 26, 42, 0.55);
|
|
94
26
|
color: var(--text);
|
|
95
27
|
font-size: 13px;
|
|
96
28
|
line-height: 1.5;
|
|
@@ -101,58 +33,7 @@ input {
|
|
|
101
33
|
color: var(--accent);
|
|
102
34
|
}
|
|
103
35
|
|
|
104
|
-
.auth-form {
|
|
105
|
-
display: grid;
|
|
106
|
-
gap: 16px;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
.auth-field {
|
|
110
|
-
display: grid;
|
|
111
|
-
gap: 7px;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
.auth-field label {
|
|
115
|
-
color: var(--muted);
|
|
116
|
-
font-size: 12px;
|
|
117
|
-
letter-spacing: 0.02em;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
.auth-field input {
|
|
121
|
-
width: 100%;
|
|
122
|
-
height: 42px;
|
|
123
|
-
padding: 0 14px;
|
|
124
|
-
border: 1px solid var(--line);
|
|
125
|
-
border-radius: 8px;
|
|
126
|
-
outline: none;
|
|
127
|
-
background: rgba(12, 24, 36, 0.94);
|
|
128
|
-
color: var(--text);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
.auth-field input:focus {
|
|
132
|
-
border-color: var(--accent);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
.auth-submit {
|
|
136
|
-
height: 44px;
|
|
137
|
-
margin-top: 4px;
|
|
138
|
-
border: 1px solid var(--accent);
|
|
139
|
-
border-radius: 8px;
|
|
140
|
-
background: var(--accent-weak);
|
|
141
|
-
color: var(--text);
|
|
142
|
-
cursor: pointer;
|
|
143
|
-
transition: background 120ms ease, opacity 120ms ease;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
.auth-submit:hover,
|
|
147
|
-
.auth-submit:focus {
|
|
148
|
-
background: rgba(90, 168, 255, 0.3);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
36
|
.auth-message {
|
|
152
|
-
margin: 0;
|
|
153
|
-
font-size: 12px;
|
|
154
|
-
line-height: 1.4;
|
|
155
|
-
overflow-wrap: anywhere;
|
|
156
37
|
color: var(--danger);
|
|
157
38
|
}`;
|
|
158
39
|
const consentLine = (clientName) => {
|
package/package.json
CHANGED