dsh-smooth-stream 0.5.0 → 0.6.1
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.en.md +4 -1
- package/README.md +5 -1
- package/lib/client.js +572 -93
- package/lib/client.js.map +1 -1
- package/lib/index.js +219 -1
- package/package.json +1 -1
- package/src/client/LogarithmicFade.module.css +160 -0
- package/src/client/SmoothStreamCard.module.css +34 -0
- package/src/client/SmoothStreamCard.tsx +42 -0
- package/src/client/TypewriterAssistantNodeView.tsx +131 -18
- package/src/client/index.ts +4 -0
- package/src/client/locales.ts +25 -0
- package/src/client/smooth-stream-card-controller.ts +10 -3
- package/src/client/smooth-stream-settings-api.ts +7 -3
- package/src/client/teleprompterGlide.ts +8 -18
- package/src/client/turn-process-face.d.ts +27 -0
- package/src/client/useLogarithmicFade.ts +293 -0
- package/src/plugin.ts +39 -4
- package/src/settings-api.ts +4 -0
- package/src/settings-channel.ts +277 -0
- package/src/settings.ts +23 -0
package/lib/index.js
CHANGED
|
@@ -136,6 +136,196 @@ function updateNpmProfilePackage(profileDir, packageName) {
|
|
|
136
136
|
});
|
|
137
137
|
}
|
|
138
138
|
//#endregion
|
|
139
|
+
//#region src/settings-channel.ts
|
|
140
|
+
/** Largest request body this channel buffers; settings payloads are tiny. */
|
|
141
|
+
const MAX_REQUEST_BYTES = 1 << 20;
|
|
142
|
+
/** Endpoint segment shape accepted by the Connection router, mirrored here. */
|
|
143
|
+
const ENDPOINT_SEGMENT = /^[A-Za-z0-9_$.-]+$/;
|
|
144
|
+
/** Envelope discriminator the browser caller sends. */
|
|
145
|
+
const CLIENT_REQUEST = "client-request";
|
|
146
|
+
/** Envelope discriminator the browser caller expects back. */
|
|
147
|
+
const SERVER_RESPONSE = "server-response";
|
|
148
|
+
/**
|
|
149
|
+
* Register the settings RPC channel on the most capable path the kernel has.
|
|
150
|
+
* The registration is an effect of `ctx`, so the caller's fiber owns the
|
|
151
|
+
* channel's lifetime on either path.
|
|
152
|
+
* @param ctx - Host context carrying `connection` and, when composed, `webServer`.
|
|
153
|
+
* @param channel - absolute channel prefix, e.g. `/smooth-stream`.
|
|
154
|
+
* @param handler - decoded endpoint handler for every endpoint on the channel.
|
|
155
|
+
* @returns Disposer that withdraws the channel.
|
|
156
|
+
*/
|
|
157
|
+
function registerSettingsChannel(ctx, channel, handler) {
|
|
158
|
+
const connection = ctx.get("connection");
|
|
159
|
+
return ctx.effect(() => {
|
|
160
|
+
const viaService = tryServiceChannel(connection, channel, handler);
|
|
161
|
+
if (viaService !== void 0) return viaService;
|
|
162
|
+
return mountDirectRoute(ctx, connection, channel, handler);
|
|
163
|
+
}, `dsh-smooth-stream: ${channel} RPC channel`);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Try the service-owned registration, which is the only path that also carries
|
|
167
|
+
* the kernel's own channel policy.
|
|
168
|
+
* @returns Disposer on success, `undefined` when the kernel cannot mount it.
|
|
169
|
+
*/
|
|
170
|
+
function tryServiceChannel(connection, channel, handler) {
|
|
171
|
+
const rpc = connection?.rpc;
|
|
172
|
+
if (rpc === void 0 || typeof rpc.handle !== "function") return void 0;
|
|
173
|
+
try {
|
|
174
|
+
const release = rpc.handle(channel, handler, { authority: "loopback" });
|
|
175
|
+
if (typeof release !== "function") return void 0;
|
|
176
|
+
return () => {
|
|
177
|
+
release();
|
|
178
|
+
};
|
|
179
|
+
} catch (error) {
|
|
180
|
+
console.warn(`[dsh-smooth-stream] connection.rpc.handle() could not mount ${channel}; falling back to a directly registered route`, error);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Mount the channel prefix on the Web server with the connection fence intact.
|
|
186
|
+
* @throws when the kernel exposes neither the fence nor a Web server, so the
|
|
187
|
+
* failure is reported instead of silently leaving the card unreachable.
|
|
188
|
+
*/
|
|
189
|
+
function mountDirectRoute(ctx, connection, channel, handler) {
|
|
190
|
+
const webServer = ctx.get("webServer");
|
|
191
|
+
const reject = connection?.requestRejection;
|
|
192
|
+
if (webServer === void 0 || typeof webServer.register !== "function") throw new Error(`dsh-smooth-stream: webServer is unavailable, so ${channel} cannot be mounted`);
|
|
193
|
+
if (typeof reject !== "function") throw new Error(`dsh-smooth-stream: connection.requestRejection is unavailable, so ${channel} cannot be mounted behind the connection trust fence`);
|
|
194
|
+
return webServer.register({
|
|
195
|
+
kind: "prefix",
|
|
196
|
+
path: channel,
|
|
197
|
+
handler: async (req, res) => {
|
|
198
|
+
const rejection = reject.call(connection, req);
|
|
199
|
+
if (rejection !== void 0) {
|
|
200
|
+
res.writeHead(rejection);
|
|
201
|
+
res.end(rejection === 401 ? "unauthorized" : "forbidden");
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
await answer(req, res, channel, handler);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Decode one request envelope, dispatch it, and write the decoded reply.
|
|
210
|
+
* Mirrors the Connection router's own carrier rules: method and endpoint must
|
|
211
|
+
* agree, the body must be JSON, and only endpoint failures are results while
|
|
212
|
+
* transport failures are statuses.
|
|
213
|
+
*/
|
|
214
|
+
async function answer(req, res, channel, handler) {
|
|
215
|
+
const endpoint = endpointOf(channel, req.url);
|
|
216
|
+
if (req.method !== "POST" || endpoint === void 0) {
|
|
217
|
+
res.writeHead(404);
|
|
218
|
+
res.end("not found");
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if ((req.headers["content-type"] ?? "").split(";", 1)[0]?.trim().toLowerCase() !== "application/json") {
|
|
222
|
+
res.writeHead(415);
|
|
223
|
+
res.end("content type must be application/json");
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const body = await readBody(req);
|
|
227
|
+
if (body.kind === "too-large") {
|
|
228
|
+
res.writeHead(413);
|
|
229
|
+
res.end("request body too large");
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (body.kind === "invalid") {
|
|
233
|
+
res.writeHead(400);
|
|
234
|
+
res.end("body is not JSON");
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const envelope = requestEnvelope(body.value);
|
|
238
|
+
if (envelope === void 0) {
|
|
239
|
+
write(res, rpcIdOf(body.value), {
|
|
240
|
+
ok: false,
|
|
241
|
+
error: {
|
|
242
|
+
code: "gateway/bad-request",
|
|
243
|
+
message: "invalid client-request message",
|
|
244
|
+
details: { issues: [] }
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (envelope.method !== endpoint) {
|
|
250
|
+
write(res, envelope.rpcId, {
|
|
251
|
+
ok: false,
|
|
252
|
+
error: {
|
|
253
|
+
code: "gateway/bad-request",
|
|
254
|
+
message: `method ${JSON.stringify(envelope.method)} does not match endpoint ${JSON.stringify(endpoint)}`,
|
|
255
|
+
details: { issues: [] }
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const controller = new AbortController();
|
|
261
|
+
res.on("close", () => {
|
|
262
|
+
controller.abort();
|
|
263
|
+
});
|
|
264
|
+
try {
|
|
265
|
+
write(res, envelope.rpcId, await handler(endpoint, envelope.payload, controller.signal));
|
|
266
|
+
} catch (error) {
|
|
267
|
+
res.writeHead(500);
|
|
268
|
+
res.end(`handler failure: ${String(error)}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Resolve the channel-relative endpoint for one request path.
|
|
273
|
+
* @returns The endpoint, or `undefined` when the path is outside the channel
|
|
274
|
+
* or carries a segment the Connection router would refuse.
|
|
275
|
+
*/
|
|
276
|
+
function endpointOf(channel, rawUrl) {
|
|
277
|
+
const pathname = new URL(rawUrl ?? "/", "http://dsh.internal").pathname;
|
|
278
|
+
if (!pathname.startsWith(`${channel}/`)) return void 0;
|
|
279
|
+
const endpoint = pathname.slice(channel.length + 1);
|
|
280
|
+
if (endpoint.split("/").some((segment) => segment === "" || segment === "." || segment === ".." || !ENDPOINT_SEGMENT.test(segment))) return;
|
|
281
|
+
return endpoint;
|
|
282
|
+
}
|
|
283
|
+
/** Structure one decoded request, rejecting anything the caller could not have sent. */
|
|
284
|
+
function requestEnvelope(value) {
|
|
285
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
286
|
+
const record = value;
|
|
287
|
+
if (record.type !== CLIENT_REQUEST) return void 0;
|
|
288
|
+
if (typeof record.rpcId !== "string" || typeof record.method !== "string") return void 0;
|
|
289
|
+
return {
|
|
290
|
+
rpcId: record.rpcId,
|
|
291
|
+
method: record.method,
|
|
292
|
+
payload: record.payload
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
/** Recover the correlation id of a malformed envelope so the caller can match it. */
|
|
296
|
+
function rpcIdOf(value) {
|
|
297
|
+
const raw = value?.rpcId;
|
|
298
|
+
return typeof raw === "string" ? raw : "invalid-request";
|
|
299
|
+
}
|
|
300
|
+
/** Write one decoded reply in the envelope the browser caller parses. */
|
|
301
|
+
function write(res, rpcId, result) {
|
|
302
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
303
|
+
res.end(JSON.stringify({
|
|
304
|
+
type: SERVER_RESPONSE,
|
|
305
|
+
rpcId,
|
|
306
|
+
result
|
|
307
|
+
}));
|
|
308
|
+
}
|
|
309
|
+
/** Buffer one request body, refusing both malformed JSON and unbounded input. */
|
|
310
|
+
async function readBody(req) {
|
|
311
|
+
const chunks = [];
|
|
312
|
+
let bytes = 0;
|
|
313
|
+
for await (const chunk of req) {
|
|
314
|
+
const buffer = chunk;
|
|
315
|
+
bytes += buffer.length;
|
|
316
|
+
if (bytes > MAX_REQUEST_BYTES) return { kind: "too-large" };
|
|
317
|
+
chunks.push(buffer);
|
|
318
|
+
}
|
|
319
|
+
try {
|
|
320
|
+
return {
|
|
321
|
+
kind: "value",
|
|
322
|
+
value: JSON.parse(Buffer.concat(chunks).toString("utf8"))
|
|
323
|
+
};
|
|
324
|
+
} catch {
|
|
325
|
+
return { kind: "invalid" };
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
//#endregion
|
|
139
329
|
//#region src/settings-api.ts
|
|
140
330
|
/** Shared wire vocabulary for the plugin-owned settings RPC channel. */
|
|
141
331
|
/** Dedicated, loopback-only RPC channel registered by the Host half. */
|
|
@@ -164,7 +354,9 @@ const STREAM_SETTINGS_NS = "smooth-stream";
|
|
|
164
354
|
const DEFAULT_STREAM_SETTINGS = {
|
|
165
355
|
enabled: true,
|
|
166
356
|
controlScroll: true,
|
|
357
|
+
motionPreference: "auto",
|
|
167
358
|
thinkAutoExpand: true,
|
|
359
|
+
logarithmicFade: true,
|
|
168
360
|
debugEnabled: false,
|
|
169
361
|
debugTuning: {
|
|
170
362
|
revealScale: 1,
|
|
@@ -200,7 +392,13 @@ const Config = Schema.object({
|
|
|
200
392
|
const StreamSettingsSchema = Schema.object({
|
|
201
393
|
enabled: Schema.boolean().default(DEFAULT_STREAM_SETTINGS.enabled),
|
|
202
394
|
controlScroll: Schema.boolean().default(DEFAULT_STREAM_SETTINGS.controlScroll),
|
|
395
|
+
motionPreference: Schema.union([
|
|
396
|
+
Schema.const("auto"),
|
|
397
|
+
Schema.const("force-smooth"),
|
|
398
|
+
Schema.const("force-reduced")
|
|
399
|
+
]).default(DEFAULT_STREAM_SETTINGS.motionPreference),
|
|
203
400
|
thinkAutoExpand: Schema.boolean().default(DEFAULT_STREAM_SETTINGS.thinkAutoExpand),
|
|
401
|
+
logarithmicFade: Schema.boolean().default(DEFAULT_STREAM_SETTINGS.logarithmicFade),
|
|
204
402
|
debugEnabled: Schema.boolean().default(DEFAULT_STREAM_SETTINGS.debugEnabled),
|
|
205
403
|
debugTuning: Schema.object({
|
|
206
404
|
revealScale: Schema.number().min(.25).max(2).default(DEFAULT_STREAM_SETTINGS.debugTuning.revealScale),
|
|
@@ -241,7 +439,9 @@ function apply(ctx, config) {
|
|
|
241
439
|
writable: connectionCtx.settings.writable,
|
|
242
440
|
enabled: settings.enabled,
|
|
243
441
|
controlScroll: settings.controlScroll,
|
|
442
|
+
motionPreference: settings.motionPreference,
|
|
244
443
|
thinkAutoExpand: settings.thinkAutoExpand,
|
|
444
|
+
logarithmicFade: settings.logarithmicFade,
|
|
245
445
|
canUpgrade: installation.kind === "npm"
|
|
246
446
|
};
|
|
247
447
|
};
|
|
@@ -281,6 +481,22 @@ function apply(ctx, config) {
|
|
|
281
481
|
};
|
|
282
482
|
try {
|
|
283
483
|
const next = payload;
|
|
484
|
+
if (next.motionPreference !== void 0 && next.motionPreference !== "auto" && next.motionPreference !== "force-smooth" && next.motionPreference !== "force-reduced") return {
|
|
485
|
+
ok: false,
|
|
486
|
+
error: {
|
|
487
|
+
code: "settings-rejected",
|
|
488
|
+
message: "motionPreference must be one of auto | force-smooth | force-reduced",
|
|
489
|
+
details: { ns: STREAM_SETTINGS_NS }
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
if (next.logarithmicFade !== void 0 && typeof next.logarithmicFade !== "boolean") return {
|
|
493
|
+
ok: false,
|
|
494
|
+
error: {
|
|
495
|
+
code: "settings-rejected",
|
|
496
|
+
message: "logarithmicFade must be a boolean",
|
|
497
|
+
details: { ns: STREAM_SETTINGS_NS }
|
|
498
|
+
}
|
|
499
|
+
};
|
|
284
500
|
const hasDebug = next.debugEnabled !== void 0 || next.debugTuning !== void 0;
|
|
285
501
|
if (hasDebug && (typeof next.debugEnabled !== "boolean" || !validDebugTuning(next.debugTuning))) return {
|
|
286
502
|
ok: false,
|
|
@@ -293,7 +509,9 @@ function apply(ctx, config) {
|
|
|
293
509
|
await scope.update({
|
|
294
510
|
enabled: next.enabled,
|
|
295
511
|
controlScroll: next.controlScroll,
|
|
512
|
+
...next.motionPreference === void 0 ? {} : { motionPreference: next.motionPreference },
|
|
296
513
|
thinkAutoExpand: next.thinkAutoExpand,
|
|
514
|
+
...next.logarithmicFade === void 0 ? {} : { logarithmicFade: next.logarithmicFade },
|
|
297
515
|
...hasDebug ? {
|
|
298
516
|
debugEnabled: next.debugEnabled,
|
|
299
517
|
debugTuning: next.debugTuning
|
|
@@ -411,7 +629,7 @@ function apply(ctx, config) {
|
|
|
411
629
|
}
|
|
412
630
|
};
|
|
413
631
|
};
|
|
414
|
-
connectionCtx
|
|
632
|
+
registerSettingsChannel(connectionCtx, STREAM_SETTINGS_RPC_CHANNEL, handle);
|
|
415
633
|
});
|
|
416
634
|
});
|
|
417
635
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-smooth-stream",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Fluid streaming rendering and silky scrolling for DeepSeek Harness replies, including Markdown, code blocks, tables, and tool results.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/* Shared buckets change paint only; original DOM, layout and colors survive. */
|
|
2
|
+
.scope::highlight(dsh-smooth-stream-log-fade-0),
|
|
3
|
+
.scope *::highlight(dsh-smooth-stream-log-fade-0) {
|
|
4
|
+
color: color-mix(in srgb, var(--dsh-smooth-stream-fade-color, currentColor) 0.00000%, transparent);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.scope::highlight(dsh-smooth-stream-log-fade-1),
|
|
8
|
+
.scope *::highlight(dsh-smooth-stream-log-fade-1) {
|
|
9
|
+
color: color-mix(in srgb, var(--dsh-smooth-stream-fade-color, currentColor) 3.22581%, transparent);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.scope::highlight(dsh-smooth-stream-log-fade-2),
|
|
13
|
+
.scope *::highlight(dsh-smooth-stream-log-fade-2) {
|
|
14
|
+
color: color-mix(in srgb, var(--dsh-smooth-stream-fade-color, currentColor) 6.45161%, transparent);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.scope::highlight(dsh-smooth-stream-log-fade-3),
|
|
18
|
+
.scope *::highlight(dsh-smooth-stream-log-fade-3) {
|
|
19
|
+
color: color-mix(in srgb, var(--dsh-smooth-stream-fade-color, currentColor) 9.67742%, transparent);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.scope::highlight(dsh-smooth-stream-log-fade-4),
|
|
23
|
+
.scope *::highlight(dsh-smooth-stream-log-fade-4) {
|
|
24
|
+
color: color-mix(in srgb, var(--dsh-smooth-stream-fade-color, currentColor) 12.90323%, transparent);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.scope::highlight(dsh-smooth-stream-log-fade-5),
|
|
28
|
+
.scope *::highlight(dsh-smooth-stream-log-fade-5) {
|
|
29
|
+
color: color-mix(in srgb, var(--dsh-smooth-stream-fade-color, currentColor) 16.12903%, transparent);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.scope::highlight(dsh-smooth-stream-log-fade-6),
|
|
33
|
+
.scope *::highlight(dsh-smooth-stream-log-fade-6) {
|
|
34
|
+
color: color-mix(in srgb, var(--dsh-smooth-stream-fade-color, currentColor) 19.35484%, transparent);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.scope::highlight(dsh-smooth-stream-log-fade-7),
|
|
38
|
+
.scope *::highlight(dsh-smooth-stream-log-fade-7) {
|
|
39
|
+
color: color-mix(in srgb, var(--dsh-smooth-stream-fade-color, currentColor) 22.58065%, transparent);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.scope::highlight(dsh-smooth-stream-log-fade-8),
|
|
43
|
+
.scope *::highlight(dsh-smooth-stream-log-fade-8) {
|
|
44
|
+
color: color-mix(in srgb, var(--dsh-smooth-stream-fade-color, currentColor) 25.80645%, transparent);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.scope::highlight(dsh-smooth-stream-log-fade-9),
|
|
48
|
+
.scope *::highlight(dsh-smooth-stream-log-fade-9) {
|
|
49
|
+
color: color-mix(in srgb, var(--dsh-smooth-stream-fade-color, currentColor) 29.03226%, transparent);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.scope::highlight(dsh-smooth-stream-log-fade-10),
|
|
53
|
+
.scope *::highlight(dsh-smooth-stream-log-fade-10) {
|
|
54
|
+
color: color-mix(in srgb, var(--dsh-smooth-stream-fade-color, currentColor) 32.25806%, transparent);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.scope::highlight(dsh-smooth-stream-log-fade-11),
|
|
58
|
+
.scope *::highlight(dsh-smooth-stream-log-fade-11) {
|
|
59
|
+
color: color-mix(in srgb, var(--dsh-smooth-stream-fade-color, currentColor) 35.48387%, transparent);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.scope::highlight(dsh-smooth-stream-log-fade-12),
|
|
63
|
+
.scope *::highlight(dsh-smooth-stream-log-fade-12) {
|
|
64
|
+
color: color-mix(in srgb, var(--dsh-smooth-stream-fade-color, currentColor) 38.70968%, transparent);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.scope::highlight(dsh-smooth-stream-log-fade-13),
|
|
68
|
+
.scope *::highlight(dsh-smooth-stream-log-fade-13) {
|
|
69
|
+
color: color-mix(in srgb, var(--dsh-smooth-stream-fade-color, currentColor) 41.93548%, transparent);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.scope::highlight(dsh-smooth-stream-log-fade-14),
|
|
73
|
+
.scope *::highlight(dsh-smooth-stream-log-fade-14) {
|
|
74
|
+
color: color-mix(in srgb, var(--dsh-smooth-stream-fade-color, currentColor) 45.16129%, transparent);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.scope::highlight(dsh-smooth-stream-log-fade-15),
|
|
78
|
+
.scope *::highlight(dsh-smooth-stream-log-fade-15) {
|
|
79
|
+
color: color-mix(in srgb, var(--dsh-smooth-stream-fade-color, currentColor) 48.38710%, transparent);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.scope::highlight(dsh-smooth-stream-log-fade-16),
|
|
83
|
+
.scope *::highlight(dsh-smooth-stream-log-fade-16) {
|
|
84
|
+
color: color-mix(in srgb, var(--dsh-smooth-stream-fade-color, currentColor) 51.61290%, transparent);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.scope::highlight(dsh-smooth-stream-log-fade-17),
|
|
88
|
+
.scope *::highlight(dsh-smooth-stream-log-fade-17) {
|
|
89
|
+
color: color-mix(in srgb, var(--dsh-smooth-stream-fade-color, currentColor) 54.83871%, transparent);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.scope::highlight(dsh-smooth-stream-log-fade-18),
|
|
93
|
+
.scope *::highlight(dsh-smooth-stream-log-fade-18) {
|
|
94
|
+
color: color-mix(in srgb, var(--dsh-smooth-stream-fade-color, currentColor) 58.06452%, transparent);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.scope::highlight(dsh-smooth-stream-log-fade-19),
|
|
98
|
+
.scope *::highlight(dsh-smooth-stream-log-fade-19) {
|
|
99
|
+
color: color-mix(in srgb, var(--dsh-smooth-stream-fade-color, currentColor) 61.29032%, transparent);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.scope::highlight(dsh-smooth-stream-log-fade-20),
|
|
103
|
+
.scope *::highlight(dsh-smooth-stream-log-fade-20) {
|
|
104
|
+
color: color-mix(in srgb, var(--dsh-smooth-stream-fade-color, currentColor) 64.51613%, transparent);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.scope::highlight(dsh-smooth-stream-log-fade-21),
|
|
108
|
+
.scope *::highlight(dsh-smooth-stream-log-fade-21) {
|
|
109
|
+
color: color-mix(in srgb, var(--dsh-smooth-stream-fade-color, currentColor) 67.74194%, transparent);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.scope::highlight(dsh-smooth-stream-log-fade-22),
|
|
113
|
+
.scope *::highlight(dsh-smooth-stream-log-fade-22) {
|
|
114
|
+
color: color-mix(in srgb, var(--dsh-smooth-stream-fade-color, currentColor) 70.96774%, transparent);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.scope::highlight(dsh-smooth-stream-log-fade-23),
|
|
118
|
+
.scope *::highlight(dsh-smooth-stream-log-fade-23) {
|
|
119
|
+
color: color-mix(in srgb, var(--dsh-smooth-stream-fade-color, currentColor) 74.19355%, transparent);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.scope::highlight(dsh-smooth-stream-log-fade-24),
|
|
123
|
+
.scope *::highlight(dsh-smooth-stream-log-fade-24) {
|
|
124
|
+
color: color-mix(in srgb, var(--dsh-smooth-stream-fade-color, currentColor) 77.41935%, transparent);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.scope::highlight(dsh-smooth-stream-log-fade-25),
|
|
128
|
+
.scope *::highlight(dsh-smooth-stream-log-fade-25) {
|
|
129
|
+
color: color-mix(in srgb, var(--dsh-smooth-stream-fade-color, currentColor) 80.64516%, transparent);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.scope::highlight(dsh-smooth-stream-log-fade-26),
|
|
133
|
+
.scope *::highlight(dsh-smooth-stream-log-fade-26) {
|
|
134
|
+
color: color-mix(in srgb, var(--dsh-smooth-stream-fade-color, currentColor) 83.87097%, transparent);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.scope::highlight(dsh-smooth-stream-log-fade-27),
|
|
138
|
+
.scope *::highlight(dsh-smooth-stream-log-fade-27) {
|
|
139
|
+
color: color-mix(in srgb, var(--dsh-smooth-stream-fade-color, currentColor) 87.09677%, transparent);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.scope::highlight(dsh-smooth-stream-log-fade-28),
|
|
143
|
+
.scope *::highlight(dsh-smooth-stream-log-fade-28) {
|
|
144
|
+
color: color-mix(in srgb, var(--dsh-smooth-stream-fade-color, currentColor) 90.32258%, transparent);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.scope::highlight(dsh-smooth-stream-log-fade-29),
|
|
148
|
+
.scope *::highlight(dsh-smooth-stream-log-fade-29) {
|
|
149
|
+
color: color-mix(in srgb, var(--dsh-smooth-stream-fade-color, currentColor) 93.54839%, transparent);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.scope::highlight(dsh-smooth-stream-log-fade-30),
|
|
153
|
+
.scope *::highlight(dsh-smooth-stream-log-fade-30) {
|
|
154
|
+
color: color-mix(in srgb, var(--dsh-smooth-stream-fade-color, currentColor) 96.77419%, transparent);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.scope::highlight(dsh-smooth-stream-log-fade-31),
|
|
158
|
+
.scope *::highlight(dsh-smooth-stream-log-fade-31) {
|
|
159
|
+
color: color-mix(in srgb, var(--dsh-smooth-stream-fade-color, currentColor) 100.00000%, transparent);
|
|
160
|
+
}
|
|
@@ -157,6 +157,40 @@
|
|
|
157
157
|
outline-offset: 2px;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
.choiceRow {
|
|
161
|
+
display: flex;
|
|
162
|
+
flex-wrap: wrap;
|
|
163
|
+
gap: 6px 14px;
|
|
164
|
+
margin: 2px 0 0;
|
|
165
|
+
padding: 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.choice {
|
|
169
|
+
display: flex;
|
|
170
|
+
align-items: center;
|
|
171
|
+
gap: 5px;
|
|
172
|
+
font-size: 12px;
|
|
173
|
+
line-height: 1.5;
|
|
174
|
+
color: var(--dsw-alias-label-secondary);
|
|
175
|
+
cursor: pointer;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.choiceInput {
|
|
179
|
+
width: 14px;
|
|
180
|
+
height: 14px;
|
|
181
|
+
accent-color: var(--dsw-alias-brand-primary);
|
|
182
|
+
cursor: pointer;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.choice:has(.choiceInput:disabled) {
|
|
186
|
+
cursor: default;
|
|
187
|
+
opacity: 0.55;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.choiceInput:disabled {
|
|
191
|
+
cursor: default;
|
|
192
|
+
}
|
|
193
|
+
|
|
160
194
|
.hint {
|
|
161
195
|
margin: 0;
|
|
162
196
|
font-size: 12px;
|
|
@@ -84,6 +84,48 @@ export function SmoothStreamCard(props: SmoothStreamCardProps) {
|
|
|
84
84
|
</span>
|
|
85
85
|
<span className={css.hint}>{t('controlScrollHint')}</span>
|
|
86
86
|
</label>
|
|
87
|
+
<label className={state.enabled ? css.field : `${css.field} ${css.fieldDisabled}`}>
|
|
88
|
+
<span className={css.fieldHead}>
|
|
89
|
+
<span className={css.label}>{t('logarithmicFade')}</span>
|
|
90
|
+
<input
|
|
91
|
+
type="checkbox"
|
|
92
|
+
className={css.toggle}
|
|
93
|
+
checked={state.logarithmicFade}
|
|
94
|
+
disabled={!state.writable || state.saving || !state.enabled}
|
|
95
|
+
onChange={(event) => { props.edit({ logarithmicFade: event.target.checked }) }}
|
|
96
|
+
/>
|
|
97
|
+
</span>
|
|
98
|
+
<span className={css.hint}>{t('logarithmicFadeHint')}</span>
|
|
99
|
+
</label>
|
|
100
|
+
{/* Motion preference is a radio group, so this row is a div:
|
|
101
|
+
nesting the choice labels inside a field <label> would be
|
|
102
|
+
illegal HTML (label within label) and browsers route the
|
|
103
|
+
inner click away, so React never sees onChange. */}
|
|
104
|
+
<div className={state.enabled ? css.field : `${css.field} ${css.fieldDisabled}`}>
|
|
105
|
+
<span className={css.fieldHead}>
|
|
106
|
+
<span className={css.label}>{t('motionPreference')}</span>
|
|
107
|
+
</span>
|
|
108
|
+
<span className={css.hint}>{t('motionPreferenceHint')}</span>
|
|
109
|
+
<span className={css.choiceRow} role="radiogroup" aria-label={t('motionPreference')}>
|
|
110
|
+
{([
|
|
111
|
+
['auto', 'motionAuto', 'motionAutoHint'],
|
|
112
|
+
['force-smooth', 'motionForceSmooth', 'motionForceSmoothHint'],
|
|
113
|
+
['force-reduced', 'motionForceReduced', 'motionForceReducedHint'],
|
|
114
|
+
] as const).map(([value, label, hint]) => (
|
|
115
|
+
<label key={value} className={css.choice} title={t(hint)}>
|
|
116
|
+
<input
|
|
117
|
+
type="radio"
|
|
118
|
+
className={css.choiceInput}
|
|
119
|
+
name="smooth-stream-motion"
|
|
120
|
+
checked={state.motionPreference === value}
|
|
121
|
+
disabled={!state.writable || state.saving || !state.enabled}
|
|
122
|
+
onChange={() => { props.edit({ motionPreference: value }) }}
|
|
123
|
+
/>
|
|
124
|
+
{t(label)}
|
|
125
|
+
</label>
|
|
126
|
+
))}
|
|
127
|
+
</span>
|
|
128
|
+
</div>
|
|
87
129
|
<label className={state.enabled ? css.field : `${css.field} ${css.fieldDisabled}`}>
|
|
88
130
|
<span className={css.fieldHead}>
|
|
89
131
|
<span className={css.label}>{t('thinkAutoExpand')}</span>
|