@rackbops/plugin-tracker 0.2.0 → 0.3.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/README.md +72 -7
- package/dist/plugin.js +1950 -106
- package/package.json +12 -4
package/dist/plugin.js
CHANGED
|
@@ -2063,6 +2063,40 @@ function resumedNotice(r) {
|
|
|
2063
2063
|
return `I could not DM you ${PAUSE_AFTER} times in a row, so your reminders were paused. ${what} ` + "Check that you accept DMs from members of a server we share, or they will pause again.";
|
|
2064
2064
|
}
|
|
2065
2065
|
|
|
2066
|
+
// plugins/tracker/src/discord-common.ts
|
|
2067
|
+
import { MessageFlags } from "discord.js";
|
|
2068
|
+
var STARTING = "The tracker is starting up; try again in a minute.";
|
|
2069
|
+
var FAILED = "Something went wrong on my side; try again in a minute.";
|
|
2070
|
+
var EPHEMERAL = { flags: MessageFlags.Ephemeral };
|
|
2071
|
+
var NO_MENTIONS = { allowedMentions: { parse: [] } };
|
|
2072
|
+
var UNKNOWN_MEMBER = 10007;
|
|
2073
|
+
var UNKNOWN_USER = 10013;
|
|
2074
|
+
async function lookupMembership(interaction, guildId, discordId, log) {
|
|
2075
|
+
if (guildId === null)
|
|
2076
|
+
return "not-checked";
|
|
2077
|
+
if (interaction.guildId === guildId && interaction.user.id === discordId)
|
|
2078
|
+
return "member";
|
|
2079
|
+
try {
|
|
2080
|
+
const guild = await interaction.client.guilds.fetch(guildId);
|
|
2081
|
+
await guild.members.fetch({ user: discordId, force: true });
|
|
2082
|
+
return "member";
|
|
2083
|
+
} catch (err) {
|
|
2084
|
+
const code = err.code;
|
|
2085
|
+
if (code === UNKNOWN_MEMBER || code === UNKNOWN_USER)
|
|
2086
|
+
return "not-member";
|
|
2087
|
+
log.warn(`could not check membership of ${guildId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2088
|
+
return "unknown";
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
function serial() {
|
|
2092
|
+
let chain = Promise.resolve();
|
|
2093
|
+
return (fn) => {
|
|
2094
|
+
const next = chain.then(fn, fn);
|
|
2095
|
+
chain = next.catch(() => {});
|
|
2096
|
+
return next;
|
|
2097
|
+
};
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2066
2100
|
// plugins/tracker/src/people.ts
|
|
2067
2101
|
var DEFAULT_TIME_ZONE = "America/New_York";
|
|
2068
2102
|
var DEFAULT_PREFERRED_HOUR = 9;
|
|
@@ -2190,6 +2224,20 @@ async function registerPerson(d, user, input) {
|
|
|
2190
2224
|
await rescheduleOwned(d, updated, zoneChanged);
|
|
2191
2225
|
return registeredText(updated, first);
|
|
2192
2226
|
}
|
|
2227
|
+
async function saveSettings(d, user, input) {
|
|
2228
|
+
let updated;
|
|
2229
|
+
try {
|
|
2230
|
+
updated = await setPreferences(d.store, user.id, { preferredHour: input.hour, timeZone: input.zone });
|
|
2231
|
+
} catch (err) {
|
|
2232
|
+
if (err instanceof PeopleError)
|
|
2233
|
+
return err.message;
|
|
2234
|
+
throw err;
|
|
2235
|
+
}
|
|
2236
|
+
const zoneChanged = updated.timeZone !== user.timeZone;
|
|
2237
|
+
if (zoneChanged || updated.preferredHour !== user.preferredHour)
|
|
2238
|
+
await rescheduleOwned(d, updated, zoneChanged);
|
|
2239
|
+
return null;
|
|
2240
|
+
}
|
|
2193
2241
|
async function setHour(d, user, hour) {
|
|
2194
2242
|
if (hour === user.preferredHour)
|
|
2195
2243
|
return `Your preferred hour is already ${String(hour).padStart(2, "0")}:00, ${user.timeZone} time.`;
|
|
@@ -2269,19 +2317,25 @@ async function remind(d, user, input) {
|
|
|
2269
2317
|
return clip(`Reminder \`${task.id}\` set: ${text}
|
|
2270
2318
|
Next: ${at}${cadence}.`);
|
|
2271
2319
|
}
|
|
2272
|
-
async function
|
|
2273
|
-
const
|
|
2274
|
-
const
|
|
2320
|
+
async function loadTaskList(d, user) {
|
|
2321
|
+
const entries = await taskList(d.store, { userId: user.id, admin: user.admin });
|
|
2322
|
+
const paused = [];
|
|
2275
2323
|
for (const task of await d.store.listTasks({ ownerId: user.id, status: "paused" })) {
|
|
2276
|
-
const held =
|
|
2277
|
-
|
|
2324
|
+
const held = [];
|
|
2325
|
+
for (const id of d.health.pausesFor(task.id).filter((u) => u !== user.id))
|
|
2326
|
+
held.push((await d.store.getUser(id))?.displayName ?? id);
|
|
2327
|
+
paused.push({ task, held });
|
|
2328
|
+
}
|
|
2329
|
+
return { entries, paused };
|
|
2330
|
+
}
|
|
2331
|
+
async function listTasks(d, user) {
|
|
2332
|
+
const { entries, paused } = await loadTaskList(d, user);
|
|
2333
|
+
const lines = [formatTaskList(entries, user, d.clock.now())];
|
|
2334
|
+
for (const { task, held } of paused) {
|
|
2335
|
+
if (held.length === 0)
|
|
2278
2336
|
lines.push(`Paused: \`${task.id}\` ${task.title}`);
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
const who = [];
|
|
2282
|
-
for (const id of held)
|
|
2283
|
-
who.push((await d.store.getUser(id))?.displayName ?? id);
|
|
2284
|
-
lines.push(`Paused: \`${task.id}\` ${task.title} -- I could not DM ${who.join(", ")}; \`/task resume ${task.id}\` goes on without them.`);
|
|
2337
|
+
else
|
|
2338
|
+
lines.push(`Paused: \`${task.id}\` ${task.title} -- I could not DM ${held.join(", ")}; \`/task resume ${task.id}\` goes on without them.`);
|
|
2285
2339
|
}
|
|
2286
2340
|
return clip(lines.join(`
|
|
2287
2341
|
`));
|
|
@@ -2338,40 +2392,6 @@ async function resumeTask(d, user, taskId) {
|
|
|
2338
2392
|
return `Resumed \`${task.id}\`${without}.`;
|
|
2339
2393
|
}
|
|
2340
2394
|
|
|
2341
|
-
// plugins/tracker/src/discord-common.ts
|
|
2342
|
-
import { MessageFlags } from "discord.js";
|
|
2343
|
-
var STARTING = "The tracker is starting up; try again in a minute.";
|
|
2344
|
-
var FAILED = "Something went wrong on my side; try again in a minute.";
|
|
2345
|
-
var EPHEMERAL = { flags: MessageFlags.Ephemeral };
|
|
2346
|
-
var NO_MENTIONS = { allowedMentions: { parse: [] } };
|
|
2347
|
-
var UNKNOWN_MEMBER = 10007;
|
|
2348
|
-
var UNKNOWN_USER = 10013;
|
|
2349
|
-
async function lookupMembership(interaction, guildId, discordId, log) {
|
|
2350
|
-
if (guildId === null)
|
|
2351
|
-
return "not-checked";
|
|
2352
|
-
if (interaction.guildId === guildId && interaction.user.id === discordId)
|
|
2353
|
-
return "member";
|
|
2354
|
-
try {
|
|
2355
|
-
const guild = await interaction.client.guilds.fetch(guildId);
|
|
2356
|
-
await guild.members.fetch({ user: discordId });
|
|
2357
|
-
return "member";
|
|
2358
|
-
} catch (err) {
|
|
2359
|
-
const code = err.code;
|
|
2360
|
-
if (code === UNKNOWN_MEMBER || code === UNKNOWN_USER)
|
|
2361
|
-
return "not-member";
|
|
2362
|
-
log.warn(`could not check membership of ${guildId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2363
|
-
return "unknown";
|
|
2364
|
-
}
|
|
2365
|
-
}
|
|
2366
|
-
function serial() {
|
|
2367
|
-
let chain = Promise.resolve();
|
|
2368
|
-
return (fn) => {
|
|
2369
|
-
const next = chain.then(fn, fn);
|
|
2370
|
-
chain = next.catch(() => {});
|
|
2371
|
-
return next;
|
|
2372
|
-
};
|
|
2373
|
-
}
|
|
2374
|
-
|
|
2375
2395
|
// plugins/tracker/src/history.ts
|
|
2376
2396
|
var HISTORY_RUNS = 10;
|
|
2377
2397
|
var HISTORY_CHANGES = 5;
|
|
@@ -2384,23 +2404,36 @@ function replyText(r) {
|
|
|
2384
2404
|
return `chose ${String(r.payload?.choice ?? "?")}`;
|
|
2385
2405
|
return r.kind === "opt_out" ? "opted out" : r.kind;
|
|
2386
2406
|
}
|
|
2387
|
-
function
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2407
|
+
function runView(o, replies, people, viewer, now) {
|
|
2408
|
+
return {
|
|
2409
|
+
due: formatInstant(o.dueAt, viewer.timeZone, now),
|
|
2410
|
+
status: o.status,
|
|
2411
|
+
late: o.late,
|
|
2412
|
+
error: o.status === "failed" && o.error ? clip(o.error, 120) : null,
|
|
2413
|
+
answers: replies.filter((r) => r.occurrenceId === o.id).map((r) => `${people.get(r.userId)?.displayName ?? r.userId} ${replyText(r)}`)
|
|
2414
|
+
};
|
|
2415
|
+
}
|
|
2416
|
+
function changeView(e, viewer, now) {
|
|
2417
|
+
return {
|
|
2418
|
+
at: formatInstant(e.at, viewer.timeZone, now),
|
|
2419
|
+
kind: e.kind.replaceAll("_", " "),
|
|
2420
|
+
detail: e.kind === "paused" || e.kind === "resumed" ? e.detail : null
|
|
2421
|
+
};
|
|
2393
2422
|
}
|
|
2394
|
-
function
|
|
2395
|
-
const
|
|
2396
|
-
|
|
2423
|
+
function runLine(v) {
|
|
2424
|
+
const late = v.late ? " (late)" : "";
|
|
2425
|
+
const error = v.error ? `: ${v.error}` : "";
|
|
2426
|
+
const answered = v.answers.length > 0 ? `; ${v.answers.join(", ")}` : "";
|
|
2427
|
+
return `- ${v.due} -- ${v.status}${late}${error}${answered}`;
|
|
2397
2428
|
}
|
|
2398
|
-
|
|
2429
|
+
function changeLine(v) {
|
|
2430
|
+
return `- ${v.at} ${v.kind}${v.detail !== null ? `: ${v.detail}` : ""}`;
|
|
2431
|
+
}
|
|
2432
|
+
async function loadHistory(d, user, taskId) {
|
|
2399
2433
|
const actor = { userId: user.id, admin: user.admin };
|
|
2400
|
-
const
|
|
2401
|
-
const task = await visibleTask(d.store, actor, id);
|
|
2434
|
+
const task = await visibleTask(d.store, actor, taskId.trim());
|
|
2402
2435
|
if (!task)
|
|
2403
|
-
return
|
|
2436
|
+
return null;
|
|
2404
2437
|
const now = d.clock.now();
|
|
2405
2438
|
const all = await visibleOccurrences(d.store, actor, task.id) ?? [];
|
|
2406
2439
|
const upcoming = (o) => o.status === "queued" && Date.parse(o.dueAt) > now.getTime();
|
|
@@ -2417,19 +2450,33 @@ async function taskHistory(d, user, taskId) {
|
|
|
2417
2450
|
}
|
|
2418
2451
|
}
|
|
2419
2452
|
const owner = task.ownerId === user.id ? user : await d.store.getUser(task.ownerId);
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2453
|
+
return {
|
|
2454
|
+
task,
|
|
2455
|
+
cadence: task.schedule && owner ? describeSchedule(task.schedule, owner, user.timeZone, now) : "no schedule",
|
|
2456
|
+
next: next ? formatInstant(next.dueAt, user.timeZone, now) : null,
|
|
2457
|
+
runs: runs.slice(-HISTORY_RUNS).map((o) => runView(o, replies, people, user, now)),
|
|
2458
|
+
earlierRuns: Math.max(0, runs.length - HISTORY_RUNS),
|
|
2459
|
+
changes: changes.slice(-HISTORY_CHANGES).map((e) => changeView(e, user, now)),
|
|
2460
|
+
earlierChanges: Math.max(0, changes.length - HISTORY_CHANGES)
|
|
2461
|
+
};
|
|
2462
|
+
}
|
|
2463
|
+
async function taskHistory(d, user, taskId) {
|
|
2464
|
+
const h = await loadHistory(d, user, taskId);
|
|
2465
|
+
if (!h)
|
|
2466
|
+
return NO_SUCH_TASK;
|
|
2467
|
+
const { task } = h;
|
|
2468
|
+
const lines = [`\`${task.id}\` ${task.title} -- ${task.type}, ${task.status}, ${h.cadence}`];
|
|
2469
|
+
lines.push(h.next ? `Next: ${h.next}` : "Next: nothing scheduled");
|
|
2470
|
+
lines.push(h.runs.length === 0 ? "Runs: none yet" : "Runs:");
|
|
2471
|
+
if (h.earlierRuns > 0)
|
|
2472
|
+
lines.push(`- (${h.earlierRuns} earlier)`);
|
|
2473
|
+
for (const v of h.runs)
|
|
2474
|
+
lines.push(runLine(v));
|
|
2428
2475
|
lines.push("Changes:");
|
|
2429
|
-
if (
|
|
2430
|
-
lines.push(`- (${
|
|
2431
|
-
for (const
|
|
2432
|
-
lines.push(
|
|
2476
|
+
if (h.earlierChanges > 0)
|
|
2477
|
+
lines.push(`- (${h.earlierChanges} earlier)`);
|
|
2478
|
+
for (const c of h.changes)
|
|
2479
|
+
lines.push(changeLine(c));
|
|
2433
2480
|
return clip(lines.join(`
|
|
2434
2481
|
`));
|
|
2435
2482
|
}
|
|
@@ -2815,6 +2862,68 @@ ${body}` : body;
|
|
|
2815
2862
|
};
|
|
2816
2863
|
}
|
|
2817
2864
|
|
|
2865
|
+
// plugins/tracker/src/web/secrets.ts
|
|
2866
|
+
import { createHash, randomBytes, timingSafeEqual } from "crypto";
|
|
2867
|
+
function randomToken() {
|
|
2868
|
+
return randomBytes(32).toString("base64url");
|
|
2869
|
+
}
|
|
2870
|
+
function sha256(value) {
|
|
2871
|
+
return createHash("sha256").update(value).digest("hex");
|
|
2872
|
+
}
|
|
2873
|
+
function safeEqual(a, b) {
|
|
2874
|
+
const x = Buffer.from(a);
|
|
2875
|
+
const y = Buffer.from(b);
|
|
2876
|
+
return x.length === y.length && timingSafeEqual(x, y);
|
|
2877
|
+
}
|
|
2878
|
+
function isTokenShape(value) {
|
|
2879
|
+
return /^[A-Za-z0-9_-]{43}$/.test(value);
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
// plugins/tracker/src/web/signin-link.ts
|
|
2883
|
+
var LINK_TTL_MS = 10 * 60 * 1000;
|
|
2884
|
+
|
|
2885
|
+
class LoginLinks {
|
|
2886
|
+
db;
|
|
2887
|
+
constructor(db) {
|
|
2888
|
+
this.db = db;
|
|
2889
|
+
}
|
|
2890
|
+
issue(userId, now, memberCheckedAt = null) {
|
|
2891
|
+
const token = randomToken();
|
|
2892
|
+
const at = now.toISOString();
|
|
2893
|
+
this.db.query("DELETE FROM web_login_tokens WHERE expires_at <= ?").run(at);
|
|
2894
|
+
this.db.query("INSERT INTO web_login_tokens (token_hash, user_id, created_at, expires_at, member_checked_at) VALUES (?, ?, ?, ?, ?)").run(sha256(token), userId, at, new Date(now.getTime() + LINK_TTL_MS).toISOString(), memberCheckedAt);
|
|
2895
|
+
return token;
|
|
2896
|
+
}
|
|
2897
|
+
peek(token, now) {
|
|
2898
|
+
if (!isTokenShape(token))
|
|
2899
|
+
return false;
|
|
2900
|
+
const row = this.db.query("SELECT 1 AS ok FROM web_login_tokens WHERE token_hash = ? AND used_at IS NULL AND expires_at > ?").get(sha256(token), now.toISOString());
|
|
2901
|
+
return row !== null;
|
|
2902
|
+
}
|
|
2903
|
+
consume(token, now) {
|
|
2904
|
+
if (!isTokenShape(token))
|
|
2905
|
+
return null;
|
|
2906
|
+
const at = now.toISOString();
|
|
2907
|
+
const row = this.db.query("UPDATE web_login_tokens SET used_at = ? WHERE token_hash = ? AND used_at IS NULL AND expires_at > ? RETURNING user_id, member_checked_at").get(at, sha256(token), at);
|
|
2908
|
+
return row ? { userId: row.user_id, memberCheckedAt: row.member_checked_at } : null;
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
|
|
2912
|
+
// plugins/tracker/src/web/command.ts
|
|
2913
|
+
var WEB_NOT_CONFIGURED = "The web area is not set up on this bot.";
|
|
2914
|
+
function webLink(d, user, web) {
|
|
2915
|
+
if (!web)
|
|
2916
|
+
return WEB_NOT_CONFIGURED;
|
|
2917
|
+
const now = d.clock.now();
|
|
2918
|
+
const token = d.logins.issue(user.id, now, web.gated ? now.toISOString() : null);
|
|
2919
|
+
const minutes = LINK_TTL_MS / 60000;
|
|
2920
|
+
return [
|
|
2921
|
+
`Your sign-in link for the tracker's web area. It works once, within ${minutes} minutes; do not share it.`,
|
|
2922
|
+
`<${web.origin}/${web.name}/login?t=${token}>`
|
|
2923
|
+
].join(`
|
|
2924
|
+
`);
|
|
2925
|
+
}
|
|
2926
|
+
|
|
2818
2927
|
// plugins/tracker/src/discord.ts
|
|
2819
2928
|
function displayName(user) {
|
|
2820
2929
|
return user.globalName ?? user.username;
|
|
@@ -2822,7 +2931,7 @@ function displayName(user) {
|
|
|
2822
2931
|
function createSurface(w) {
|
|
2823
2932
|
const ctx = {
|
|
2824
2933
|
deps: w.deps,
|
|
2825
|
-
queue: serial(),
|
|
2934
|
+
queue: w.queue ?? serial(),
|
|
2826
2935
|
membershipOf: (interaction, discordId) => w.membership ? w.membership(interaction, discordId) : lookupMembership(interaction, w.guildId, discordId, w.log),
|
|
2827
2936
|
log: w.log
|
|
2828
2937
|
};
|
|
@@ -2937,6 +3046,11 @@ ${body}` : body);
|
|
|
2937
3046
|
name: "settings",
|
|
2938
3047
|
build: (b) => b.setDescription("Your tracker settings").addSubcommand((s) => s.setName("hour").setDescription("The hour (0-23) a reminder without a time reaches you").addIntegerOption((o) => o.setName("hour").setDescription("0-23, in your time zone").setRequired(true).setMinValue(0).setMaxValue(23))),
|
|
2939
3048
|
handle: (interaction) => run(interaction, "registered", (d, user) => setHour(d, user, interaction.options.getInteger("hour", true)))
|
|
3049
|
+
},
|
|
3050
|
+
{
|
|
3051
|
+
name: "web",
|
|
3052
|
+
build: (b) => b.setDescription("Get a one-time link to sign in to the tracker's web area"),
|
|
3053
|
+
handle: (interaction) => run(interaction, "registered", async (d, user) => webLink(d, user, w.web))
|
|
2940
3054
|
}
|
|
2941
3055
|
];
|
|
2942
3056
|
return { commands, interactions: createInteractionHandler(ctx) };
|
|
@@ -3150,6 +3264,28 @@ var MIGRATIONS = [
|
|
|
3150
3264
|
at TEXT NOT NULL,
|
|
3151
3265
|
PRIMARY KEY (task_id, user_id)
|
|
3152
3266
|
);
|
|
3267
|
+
`,
|
|
3268
|
+
`
|
|
3269
|
+
CREATE TABLE web_login_tokens (
|
|
3270
|
+
token_hash TEXT PRIMARY KEY,
|
|
3271
|
+
user_id TEXT NOT NULL,
|
|
3272
|
+
created_at TEXT NOT NULL,
|
|
3273
|
+
expires_at TEXT NOT NULL,
|
|
3274
|
+
used_at TEXT,
|
|
3275
|
+
member_checked_at TEXT
|
|
3276
|
+
);
|
|
3277
|
+
CREATE INDEX web_login_tokens_expiry ON web_login_tokens (expires_at);
|
|
3278
|
+
|
|
3279
|
+
CREATE TABLE web_sessions (
|
|
3280
|
+
id_hash TEXT PRIMARY KEY,
|
|
3281
|
+
user_id TEXT NOT NULL,
|
|
3282
|
+
csrf TEXT NOT NULL,
|
|
3283
|
+
created_at TEXT NOT NULL,
|
|
3284
|
+
expires_at TEXT NOT NULL,
|
|
3285
|
+
member_checked_at TEXT
|
|
3286
|
+
);
|
|
3287
|
+
CREATE INDEX web_sessions_user ON web_sessions (user_id);
|
|
3288
|
+
CREATE INDEX web_sessions_expiry ON web_sessions (expires_at);
|
|
3153
3289
|
`
|
|
3154
3290
|
];
|
|
3155
3291
|
function migrate(db) {
|
|
@@ -3493,40 +3629,1746 @@ class SqliteStore {
|
|
|
3493
3629
|
}
|
|
3494
3630
|
}
|
|
3495
3631
|
|
|
3496
|
-
//
|
|
3497
|
-
var
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3632
|
+
// node_modules/@rackbops/styles/rackbops-noir/bundle.css
|
|
3633
|
+
var bundle_default = `:where([data-rb-style], [data-rb-style] *), :where([data-rb-style], [data-rb-style] *):before, :where([data-rb-style], [data-rb-style] *):after {
|
|
3634
|
+
box-sizing: border-box;
|
|
3635
|
+
}
|
|
3636
|
+
|
|
3637
|
+
:where(html[data-rb-style]) body {
|
|
3638
|
+
min-height: 100vh;
|
|
3639
|
+
margin: 0;
|
|
3640
|
+
}
|
|
3641
|
+
|
|
3642
|
+
:where([data-rb-style="rackbops-noir"]) {
|
|
3643
|
+
color-scheme: dark;
|
|
3644
|
+
--rb-bg: #0d141d;
|
|
3645
|
+
--rb-surface: #141f2a;
|
|
3646
|
+
--rb-surface-2: #19242f;
|
|
3647
|
+
--rb-surface-sunken: #0f1720;
|
|
3648
|
+
--rb-text: #e9eff4;
|
|
3649
|
+
--rb-text-soft: #a4b4c3;
|
|
3650
|
+
--rb-text-faint: #697a8c;
|
|
3651
|
+
--rb-border: #243140;
|
|
3652
|
+
--rb-border-strong: #31404f;
|
|
3653
|
+
--rb-accent: #ff5d3d;
|
|
3654
|
+
--rb-accent-fg: #0d141d;
|
|
3655
|
+
--rb-accent-wash: #ff5d3d21;
|
|
3656
|
+
--rb-accent-grad: linear-gradient(180deg, #ff5d3d, #ff7a5f);
|
|
3657
|
+
--rb-info: #4c9df0;
|
|
3658
|
+
--rb-success: #43c97e;
|
|
3659
|
+
--rb-warning: #f2a93b;
|
|
3660
|
+
--rb-danger: #f0616d;
|
|
3661
|
+
--rb-font-display: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
3662
|
+
--rb-font-body: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
3663
|
+
--rb-font-mono: ui-monospace, "SF Mono", "Cascadia Code", "JetBrains Mono", Menlo, Consolas, monospace;
|
|
3664
|
+
--rb-font-weight: 400;
|
|
3665
|
+
--rb-font-weight-medium: 600;
|
|
3666
|
+
--rb-font-weight-bold: 800;
|
|
3667
|
+
--rb-heading-tracking: -.02em;
|
|
3668
|
+
--rb-label-tracking: .18em;
|
|
3669
|
+
--rb-text-sm: 13.5px;
|
|
3670
|
+
--rb-radius: .6875rem;
|
|
3671
|
+
--rb-radius-lg: .9375rem;
|
|
3672
|
+
--rb-radius-pill: 99px;
|
|
3673
|
+
--rb-shadow-sm: 0 1px 2px #00000059;
|
|
3674
|
+
--rb-shadow-lg: 0 2px 6px #0006, 0 24px 50px -24px #000000b3;
|
|
3675
|
+
--rb-blur: 10px;
|
|
3676
|
+
--rb-transition: .15s;
|
|
3677
|
+
--rb-focus-ring: 2.5px solid var(--rb-accent);
|
|
3678
|
+
--rb-ease: ease;
|
|
3679
|
+
--rb-space-1: .5rem;
|
|
3680
|
+
--rb-space-2: .75rem;
|
|
3681
|
+
--rb-space-3: 1rem;
|
|
3682
|
+
--rb-space-4: 1.5rem;
|
|
3683
|
+
--rb-space-5: 2.25rem;
|
|
3684
|
+
}
|
|
3685
|
+
|
|
3686
|
+
@media (prefers-reduced-motion: reduce) {
|
|
3687
|
+
:where([data-rb-style="rackbops-noir"]) {
|
|
3688
|
+
--rb-transition: 0s;
|
|
3506
3689
|
}
|
|
3507
|
-
}
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3690
|
+
}
|
|
3691
|
+
|
|
3692
|
+
:where([data-rb-style="rackbops-noir"]) {
|
|
3693
|
+
background-color: var(--rb-bg);
|
|
3694
|
+
color: var(--rb-text);
|
|
3695
|
+
font-family: var(--rb-font-body);
|
|
3696
|
+
font-weight: var(--rb-font-weight);
|
|
3697
|
+
font-size: 17px;
|
|
3698
|
+
line-height: 1.6;
|
|
3699
|
+
}
|
|
3700
|
+
|
|
3701
|
+
:where(html[data-rb-style="rackbops-noir"]) body {
|
|
3702
|
+
background-color: var(--rb-bg);
|
|
3703
|
+
color: var(--rb-text);
|
|
3704
|
+
font-family: var(--rb-font-body);
|
|
3705
|
+
font-weight: var(--rb-font-weight);
|
|
3706
|
+
}
|
|
3707
|
+
|
|
3708
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *):where(h1, h2, h3, h4, h5, h6) {
|
|
3709
|
+
margin: 0 0 var(--rb-space-3);
|
|
3710
|
+
font-family: var(--rb-font-display);
|
|
3711
|
+
font-weight: var(--rb-font-weight-bold);
|
|
3712
|
+
letter-spacing: var(--rb-heading-tracking);
|
|
3713
|
+
text-wrap: balance;
|
|
3714
|
+
line-height: 1.08;
|
|
3715
|
+
}
|
|
3716
|
+
|
|
3717
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *):where(p) {
|
|
3718
|
+
margin: 0 0 var(--rb-space-3);
|
|
3719
|
+
}
|
|
3720
|
+
|
|
3721
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *):where(ul, ol) {
|
|
3722
|
+
margin: 0 0 var(--rb-space-3);
|
|
3723
|
+
padding-inline-start: var(--rb-space-4);
|
|
3724
|
+
}
|
|
3725
|
+
|
|
3726
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *):where(a) {
|
|
3727
|
+
color: var(--rb-accent);
|
|
3728
|
+
}
|
|
3729
|
+
|
|
3730
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *):where(:focus-visible) {
|
|
3731
|
+
outline: var(--rb-focus-ring);
|
|
3732
|
+
outline-offset: 3px;
|
|
3733
|
+
border-radius: 3px;
|
|
3734
|
+
}
|
|
3735
|
+
|
|
3736
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *)::selection {
|
|
3737
|
+
background: var(--rb-accent);
|
|
3738
|
+
color: #fff;
|
|
3739
|
+
}
|
|
3740
|
+
|
|
3741
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-btn {
|
|
3742
|
+
align-items: center;
|
|
3743
|
+
gap: var(--rb-space-2);
|
|
3744
|
+
border-radius: var(--rb-radius);
|
|
3745
|
+
font-weight: var(--rb-font-weight-medium);
|
|
3746
|
+
cursor: pointer;
|
|
3747
|
+
color: var(--rb-text);
|
|
3748
|
+
transition: transform var(--rb-transition) var(--rb-ease),
|
|
3749
|
+
background var(--rb-transition) var(--rb-ease),
|
|
3750
|
+
border-color var(--rb-transition) var(--rb-ease),
|
|
3751
|
+
color var(--rb-transition) var(--rb-ease);
|
|
3752
|
+
background: none;
|
|
3753
|
+
border: 1px solid #0000;
|
|
3754
|
+
padding: 13px 22px;
|
|
3755
|
+
font-size: 16px;
|
|
3756
|
+
text-decoration: none;
|
|
3757
|
+
display: inline-flex;
|
|
3758
|
+
}
|
|
3759
|
+
|
|
3760
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-btn--sm {
|
|
3761
|
+
font-size: var(--rb-text-sm);
|
|
3762
|
+
padding: 7px 14px;
|
|
3763
|
+
}
|
|
3764
|
+
|
|
3765
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-icon-btn {
|
|
3766
|
+
padding: var(--rb-space-2);
|
|
3767
|
+
justify-content: center;
|
|
3768
|
+
gap: 0;
|
|
3769
|
+
min-width: 2rem;
|
|
3770
|
+
min-height: 2rem;
|
|
3771
|
+
line-height: 1;
|
|
3772
|
+
}
|
|
3773
|
+
|
|
3774
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-btn:active {
|
|
3775
|
+
transform: translateY(1px);
|
|
3776
|
+
}
|
|
3777
|
+
|
|
3778
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-btn--primary {
|
|
3779
|
+
background: var(--rb-text);
|
|
3780
|
+
color: var(--rb-bg);
|
|
3781
|
+
}
|
|
3782
|
+
|
|
3783
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-btn--primary:not(:disabled):hover, :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-btn--primary:focus-visible, :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-btn--accent {
|
|
3784
|
+
background: var(--rb-accent);
|
|
3785
|
+
color: var(--rb-accent-fg);
|
|
3786
|
+
}
|
|
3787
|
+
|
|
3788
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-btn--accent:not(:disabled):hover, :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-btn--accent:focus-visible {
|
|
3789
|
+
background: color-mix(in srgb, var(--rb-accent), var(--rb-text) 15%);
|
|
3790
|
+
}
|
|
3791
|
+
|
|
3792
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-btn--ghost {
|
|
3793
|
+
color: var(--rb-text);
|
|
3794
|
+
border-color: var(--rb-border-strong);
|
|
3795
|
+
background: none;
|
|
3796
|
+
}
|
|
3797
|
+
|
|
3798
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-btn--ghost:not(:disabled):hover, :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-btn--ghost:focus-visible {
|
|
3799
|
+
border-color: var(--rb-text);
|
|
3800
|
+
background: var(--rb-surface-2);
|
|
3801
|
+
}
|
|
3802
|
+
|
|
3803
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-btn--danger {
|
|
3804
|
+
color: var(--rb-danger);
|
|
3805
|
+
border-color: color-mix(in srgb, var(--rb-danger) 45%, var(--rb-border));
|
|
3806
|
+
background: none;
|
|
3807
|
+
}
|
|
3808
|
+
|
|
3809
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-btn--danger:not(:disabled):hover, :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-btn--danger:focus-visible {
|
|
3810
|
+
border-color: var(--rb-danger);
|
|
3811
|
+
}
|
|
3812
|
+
|
|
3813
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-btn:disabled {
|
|
3814
|
+
opacity: .5;
|
|
3815
|
+
cursor: not-allowed;
|
|
3816
|
+
}
|
|
3817
|
+
|
|
3818
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-btn__arrow {
|
|
3819
|
+
transition: transform var(--rb-transition) var(--rb-ease);
|
|
3820
|
+
}
|
|
3821
|
+
|
|
3822
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-btn:not(:disabled):hover .rb-btn__arrow {
|
|
3823
|
+
transform: translateX(3px);
|
|
3824
|
+
}
|
|
3825
|
+
|
|
3826
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-card {
|
|
3827
|
+
background: var(--rb-surface);
|
|
3828
|
+
border: 1px solid var(--rb-border);
|
|
3829
|
+
border-radius: var(--rb-radius-lg);
|
|
3830
|
+
box-shadow: var(--rb-shadow-sm);
|
|
3831
|
+
transition: transform var(--rb-transition) var(--rb-ease),
|
|
3832
|
+
border-color var(--rb-transition) var(--rb-ease),
|
|
3833
|
+
box-shadow var(--rb-transition) var(--rb-ease);
|
|
3834
|
+
flex-direction: column;
|
|
3835
|
+
padding: 26px 24px 28px;
|
|
3836
|
+
display: flex;
|
|
3837
|
+
}
|
|
3838
|
+
|
|
3839
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-card:hover, :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-card--raised {
|
|
3840
|
+
border-color: var(--rb-border-strong);
|
|
3841
|
+
box-shadow: var(--rb-shadow-lg);
|
|
3842
|
+
transform: translateY(-4px);
|
|
3843
|
+
}
|
|
3844
|
+
|
|
3845
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-card--raised:hover {
|
|
3846
|
+
transform: translateY(-6px);
|
|
3847
|
+
}
|
|
3848
|
+
|
|
3849
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-card__tag {
|
|
3850
|
+
font-family: var(--rb-font-mono);
|
|
3851
|
+
letter-spacing: .12em;
|
|
3852
|
+
text-transform: uppercase;
|
|
3853
|
+
color: var(--rb-accent);
|
|
3854
|
+
justify-content: space-between;
|
|
3855
|
+
align-items: center;
|
|
3856
|
+
font-size: 12px;
|
|
3857
|
+
display: flex;
|
|
3858
|
+
}
|
|
3859
|
+
|
|
3860
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-link {
|
|
3861
|
+
align-items: center;
|
|
3862
|
+
gap: var(--rb-space-1);
|
|
3863
|
+
font-family: var(--rb-font-mono);
|
|
3864
|
+
font-size: var(--rb-text-sm);
|
|
3865
|
+
color: var(--rb-text-soft);
|
|
3866
|
+
cursor: pointer;
|
|
3867
|
+
transition: color var(--rb-transition) var(--rb-ease),
|
|
3868
|
+
background var(--rb-transition) var(--rb-ease);
|
|
3869
|
+
border-radius: 8px;
|
|
3870
|
+
padding: 8px 12px;
|
|
3871
|
+
text-decoration: none;
|
|
3872
|
+
display: inline-flex;
|
|
3873
|
+
}
|
|
3874
|
+
|
|
3875
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-link:hover {
|
|
3876
|
+
color: var(--rb-text);
|
|
3877
|
+
background: var(--rb-surface-2);
|
|
3878
|
+
}
|
|
3879
|
+
|
|
3880
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-link.rb-link--active, :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-link[aria-current="page"] {
|
|
3881
|
+
color: var(--rb-accent);
|
|
3882
|
+
}
|
|
3883
|
+
|
|
3884
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-nav-rail {
|
|
3885
|
+
gap: var(--rb-space-1);
|
|
3886
|
+
width: 14rem;
|
|
3887
|
+
padding: var(--rb-space-3);
|
|
3888
|
+
background: var(--rb-surface);
|
|
3889
|
+
border-right: 1px solid var(--rb-border);
|
|
3890
|
+
flex-direction: column;
|
|
3891
|
+
flex-shrink: 0;
|
|
3892
|
+
display: flex;
|
|
3893
|
+
}
|
|
3894
|
+
|
|
3895
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-field {
|
|
3896
|
+
gap: var(--rb-space-1);
|
|
3897
|
+
flex-direction: column;
|
|
3898
|
+
display: flex;
|
|
3899
|
+
}
|
|
3900
|
+
|
|
3901
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-label {
|
|
3902
|
+
font-size: var(--rb-text-sm);
|
|
3903
|
+
font-weight: var(--rb-font-weight-medium);
|
|
3904
|
+
color: var(--rb-text-soft);
|
|
3905
|
+
}
|
|
3906
|
+
|
|
3907
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-label__required {
|
|
3908
|
+
color: var(--rb-danger);
|
|
3909
|
+
margin-left: var(--rb-space-1);
|
|
3910
|
+
}
|
|
3911
|
+
|
|
3912
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-field__help {
|
|
3913
|
+
color: var(--rb-text-soft);
|
|
3914
|
+
font-size: var(--rb-text-sm);
|
|
3915
|
+
margin-top: var(--rb-space-1);
|
|
3916
|
+
}
|
|
3917
|
+
|
|
3918
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-field__error {
|
|
3919
|
+
color: var(--rb-text);
|
|
3920
|
+
font-size: var(--rb-text-sm);
|
|
3921
|
+
padding-left: var(--rb-space-2);
|
|
3922
|
+
border-left: 3px solid var(--rb-danger);
|
|
3923
|
+
margin-top: var(--rb-space-1);
|
|
3924
|
+
}
|
|
3925
|
+
|
|
3926
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-input, :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-textarea, :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-select {
|
|
3927
|
+
background: var(--rb-surface);
|
|
3928
|
+
border: 1px solid var(--rb-border-strong);
|
|
3929
|
+
border-radius: var(--rb-radius);
|
|
3930
|
+
width: 100%;
|
|
3931
|
+
color: var(--rb-text);
|
|
3932
|
+
font-family: var(--rb-font-body);
|
|
3933
|
+
transition: border-color var(--rb-transition) var(--rb-ease),
|
|
3934
|
+
box-shadow var(--rb-transition) var(--rb-ease);
|
|
3935
|
+
padding: 12px 14px;
|
|
3936
|
+
font-size: 16px;
|
|
3937
|
+
}
|
|
3938
|
+
|
|
3939
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-textarea {
|
|
3940
|
+
resize: vertical;
|
|
3941
|
+
min-height: 6rem;
|
|
3942
|
+
}
|
|
3943
|
+
|
|
3944
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-input:focus-visible, :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-textarea:focus-visible, :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-select:focus-visible {
|
|
3945
|
+
border-color: var(--rb-accent);
|
|
3946
|
+
box-shadow: 0 0 0 3px var(--rb-accent-wash);
|
|
3947
|
+
outline: none;
|
|
3948
|
+
}
|
|
3949
|
+
|
|
3950
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-input[aria-invalid="true"], :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-textarea[aria-invalid="true"], :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-select[aria-invalid="true"] {
|
|
3951
|
+
border-color: var(--rb-danger);
|
|
3952
|
+
}
|
|
3953
|
+
|
|
3954
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-input::placeholder, :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-textarea::placeholder {
|
|
3955
|
+
color: var(--rb-text-faint);
|
|
3956
|
+
}
|
|
3957
|
+
|
|
3958
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-choice {
|
|
3959
|
+
align-items: center;
|
|
3960
|
+
gap: var(--rb-space-1);
|
|
3961
|
+
color: var(--rb-text);
|
|
3962
|
+
cursor: pointer;
|
|
3963
|
+
font-size: 16px;
|
|
3964
|
+
display: inline-flex;
|
|
3965
|
+
}
|
|
3966
|
+
|
|
3967
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-checkbox, :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-radio {
|
|
3968
|
+
accent-color: var(--rb-accent);
|
|
3969
|
+
cursor: pointer;
|
|
3970
|
+
width: 1.1rem;
|
|
3971
|
+
height: 1.1rem;
|
|
3972
|
+
}
|
|
3973
|
+
|
|
3974
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-switch {
|
|
3975
|
+
appearance: none;
|
|
3976
|
+
vertical-align: text-bottom;
|
|
3977
|
+
cursor: pointer;
|
|
3978
|
+
background: var(--rb-surface-sunken);
|
|
3979
|
+
border: 1px solid var(--rb-border-strong);
|
|
3980
|
+
border-radius: var(--rb-radius-pill);
|
|
3981
|
+
width: 2.25rem;
|
|
3982
|
+
height: 1.25rem;
|
|
3983
|
+
transition: background var(--rb-transition) var(--rb-ease), border-color var(--rb-transition) var(--rb-ease);
|
|
3984
|
+
margin: 0;
|
|
3985
|
+
position: relative;
|
|
3986
|
+
}
|
|
3987
|
+
|
|
3988
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-switch:after {
|
|
3989
|
+
content: "";
|
|
3990
|
+
background: var(--rb-text-soft);
|
|
3991
|
+
width: calc(1.25rem - 6px);
|
|
3992
|
+
height: calc(1.25rem - 6px);
|
|
3993
|
+
transition: transform var(--rb-transition) var(--rb-ease), background var(--rb-transition) var(--rb-ease);
|
|
3994
|
+
border-radius: 50%;
|
|
3995
|
+
position: absolute;
|
|
3996
|
+
top: 2px;
|
|
3997
|
+
left: 2px;
|
|
3998
|
+
}
|
|
3999
|
+
|
|
4000
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-switch:checked {
|
|
4001
|
+
background: var(--rb-accent);
|
|
4002
|
+
border-color: var(--rb-accent);
|
|
4003
|
+
}
|
|
4004
|
+
|
|
4005
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-switch:checked:after {
|
|
4006
|
+
background: var(--rb-accent-fg);
|
|
4007
|
+
transform: translateX(1rem);
|
|
4008
|
+
}
|
|
4009
|
+
|
|
4010
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-switch:disabled {
|
|
4011
|
+
cursor: not-allowed;
|
|
4012
|
+
opacity: .5;
|
|
4013
|
+
}
|
|
4014
|
+
|
|
4015
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-badge {
|
|
4016
|
+
align-items: center;
|
|
4017
|
+
gap: var(--rb-space-1);
|
|
4018
|
+
border: 1px solid var(--rb-border);
|
|
4019
|
+
border-radius: var(--rb-radius-pill);
|
|
4020
|
+
font-family: var(--rb-font-mono);
|
|
4021
|
+
letter-spacing: .02em;
|
|
4022
|
+
color: var(--rb-text-soft);
|
|
4023
|
+
background: var(--rb-surface);
|
|
4024
|
+
padding: 4px 10px;
|
|
4025
|
+
font-size: 11.5px;
|
|
4026
|
+
display: inline-flex;
|
|
4027
|
+
}
|
|
4028
|
+
|
|
4029
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-badge--md {
|
|
4030
|
+
font-size: var(--rb-text-sm);
|
|
4031
|
+
padding: var(--rb-space-1) var(--rb-space-3);
|
|
4032
|
+
}
|
|
4033
|
+
|
|
4034
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-badge--info {
|
|
4035
|
+
background: color-mix(in srgb, var(--rb-info) 14%, var(--rb-surface));
|
|
4036
|
+
border-color: color-mix(in srgb, var(--rb-info) 45%, var(--rb-border));
|
|
4037
|
+
color: var(--rb-text);
|
|
4038
|
+
}
|
|
4039
|
+
|
|
4040
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-badge--success {
|
|
4041
|
+
background: color-mix(in srgb, var(--rb-success) 14%, var(--rb-surface));
|
|
4042
|
+
border-color: color-mix(in srgb, var(--rb-success) 45%, var(--rb-border));
|
|
4043
|
+
color: var(--rb-text);
|
|
4044
|
+
}
|
|
4045
|
+
|
|
4046
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-badge--warning {
|
|
4047
|
+
background: color-mix(in srgb, var(--rb-warning) 16%, var(--rb-surface));
|
|
4048
|
+
border-color: color-mix(in srgb, var(--rb-warning) 45%, var(--rb-border));
|
|
4049
|
+
color: var(--rb-text);
|
|
4050
|
+
}
|
|
4051
|
+
|
|
4052
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-badge--danger {
|
|
4053
|
+
background: color-mix(in srgb, var(--rb-danger) 14%, var(--rb-surface));
|
|
4054
|
+
border-color: color-mix(in srgb, var(--rb-danger) 45%, var(--rb-border));
|
|
4055
|
+
color: var(--rb-text);
|
|
4056
|
+
}
|
|
4057
|
+
|
|
4058
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-alert {
|
|
4059
|
+
gap: var(--rb-space-1);
|
|
4060
|
+
padding: var(--rb-space-3) var(--rb-space-4);
|
|
4061
|
+
background: var(--rb-surface);
|
|
4062
|
+
border: 1px solid var(--rb-border);
|
|
4063
|
+
border-left: 4px solid var(--rb-accent);
|
|
4064
|
+
border-radius: var(--rb-radius);
|
|
4065
|
+
box-shadow: var(--rb-shadow-sm);
|
|
4066
|
+
color: var(--rb-text);
|
|
4067
|
+
flex-direction: column;
|
|
4068
|
+
display: flex;
|
|
4069
|
+
}
|
|
4070
|
+
|
|
4071
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-alert__title {
|
|
4072
|
+
font-weight: var(--rb-font-weight-medium);
|
|
4073
|
+
}
|
|
4074
|
+
|
|
4075
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-alert--info {
|
|
4076
|
+
border-left-color: var(--rb-info);
|
|
4077
|
+
}
|
|
4078
|
+
|
|
4079
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-alert--success {
|
|
4080
|
+
border-left-color: var(--rb-success);
|
|
4081
|
+
}
|
|
4082
|
+
|
|
4083
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-alert--warning {
|
|
4084
|
+
border-left-color: var(--rb-warning);
|
|
4085
|
+
}
|
|
4086
|
+
|
|
4087
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-alert--danger {
|
|
4088
|
+
border-left-color: var(--rb-danger);
|
|
4089
|
+
}
|
|
4090
|
+
|
|
4091
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-toast-region {
|
|
4092
|
+
top: var(--rb-space-4);
|
|
4093
|
+
right: var(--rb-space-4);
|
|
4094
|
+
z-index: 1000;
|
|
4095
|
+
gap: var(--rb-space-2);
|
|
4096
|
+
width: fit-content;
|
|
4097
|
+
max-width: min(28rem, calc(100vw - 2 * var(--rb-space-4)));
|
|
4098
|
+
pointer-events: none;
|
|
4099
|
+
flex-direction: column;
|
|
4100
|
+
display: flex;
|
|
4101
|
+
position: fixed;
|
|
4102
|
+
}
|
|
4103
|
+
|
|
4104
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-toast {
|
|
4105
|
+
pointer-events: auto;
|
|
4106
|
+
align-items: flex-start;
|
|
4107
|
+
gap: var(--rb-space-3);
|
|
4108
|
+
padding: var(--rb-space-3) var(--rb-space-4);
|
|
4109
|
+
background: var(--rb-surface);
|
|
4110
|
+
border: 1px solid var(--rb-border);
|
|
4111
|
+
border-left: 4px solid var(--rb-accent);
|
|
4112
|
+
border-radius: var(--rb-radius);
|
|
4113
|
+
box-shadow: var(--rb-shadow-lg);
|
|
4114
|
+
color: var(--rb-text);
|
|
4115
|
+
transition: opacity var(--rb-transition) var(--rb-ease),
|
|
4116
|
+
transform var(--rb-transition) var(--rb-ease);
|
|
4117
|
+
display: flex;
|
|
4118
|
+
}
|
|
4119
|
+
|
|
4120
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-toast[data-rb-enter] {
|
|
4121
|
+
opacity: 0;
|
|
4122
|
+
transform: translateY(calc(-1 * var(--rb-space-2)));
|
|
4123
|
+
}
|
|
4124
|
+
|
|
4125
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-toast--info {
|
|
4126
|
+
border-left-color: var(--rb-info);
|
|
4127
|
+
}
|
|
4128
|
+
|
|
4129
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-toast--success {
|
|
4130
|
+
border-left-color: var(--rb-success);
|
|
4131
|
+
}
|
|
4132
|
+
|
|
4133
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-toast--warning {
|
|
4134
|
+
border-left-color: var(--rb-warning);
|
|
4135
|
+
}
|
|
4136
|
+
|
|
4137
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-toast--danger {
|
|
4138
|
+
border-left-color: var(--rb-danger);
|
|
4139
|
+
}
|
|
4140
|
+
|
|
4141
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-toast__close {
|
|
4142
|
+
border-radius: var(--rb-radius);
|
|
4143
|
+
width: 1.25rem;
|
|
4144
|
+
height: 1.25rem;
|
|
4145
|
+
color: var(--rb-text-soft);
|
|
4146
|
+
cursor: pointer;
|
|
4147
|
+
transition: color var(--rb-transition) var(--rb-ease);
|
|
4148
|
+
background: none;
|
|
4149
|
+
border: none;
|
|
4150
|
+
flex: none;
|
|
4151
|
+
justify-content: center;
|
|
4152
|
+
align-items: center;
|
|
4153
|
+
margin-inline-start: auto;
|
|
4154
|
+
padding: 0;
|
|
4155
|
+
font-size: 1.125rem;
|
|
4156
|
+
line-height: 1;
|
|
4157
|
+
display: inline-flex;
|
|
4158
|
+
}
|
|
4159
|
+
|
|
4160
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-toast__close:hover {
|
|
4161
|
+
color: var(--rb-text);
|
|
4162
|
+
}
|
|
4163
|
+
|
|
4164
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-toast__close:focus-visible {
|
|
4165
|
+
outline: var(--rb-focus-ring);
|
|
4166
|
+
outline-offset: 2px;
|
|
4167
|
+
}
|
|
4168
|
+
|
|
4169
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-dialog {
|
|
4170
|
+
border: 1px solid var(--rb-border);
|
|
4171
|
+
border-radius: var(--rb-radius-lg);
|
|
4172
|
+
background: var(--rb-surface);
|
|
4173
|
+
color: var(--rb-text);
|
|
4174
|
+
box-shadow: var(--rb-shadow-lg);
|
|
4175
|
+
max-width: min(92vw, 34rem);
|
|
4176
|
+
padding: 0;
|
|
4177
|
+
}
|
|
4178
|
+
|
|
4179
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-dialog::backdrop {
|
|
4180
|
+
-webkit-backdrop-filter: blur(var(--rb-blur));
|
|
4181
|
+
background: #131e2966;
|
|
4182
|
+
}
|
|
4183
|
+
|
|
4184
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-dialog__body {
|
|
4185
|
+
padding: var(--rb-space-5);
|
|
4186
|
+
}
|
|
4187
|
+
|
|
4188
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-dialog__title {
|
|
4189
|
+
margin: 0 0 var(--rb-space-3);
|
|
4190
|
+
font-family: var(--rb-font-display);
|
|
4191
|
+
font-weight: var(--rb-font-weight-bold);
|
|
4192
|
+
letter-spacing: var(--rb-heading-tracking);
|
|
4193
|
+
font-size: 1.4rem;
|
|
4194
|
+
}
|
|
4195
|
+
|
|
4196
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-dialog__actions {
|
|
4197
|
+
justify-content: flex-end;
|
|
4198
|
+
gap: var(--rb-space-2);
|
|
4199
|
+
padding: var(--rb-space-3) var(--rb-space-5);
|
|
4200
|
+
border-top: 1px solid var(--rb-border);
|
|
4201
|
+
display: flex;
|
|
4202
|
+
}
|
|
4203
|
+
|
|
4204
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-tabs {
|
|
4205
|
+
gap: var(--rb-space-4);
|
|
4206
|
+
border-bottom: 1px solid var(--rb-border);
|
|
4207
|
+
display: flex;
|
|
4208
|
+
}
|
|
4209
|
+
|
|
4210
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-tab {
|
|
4211
|
+
padding: var(--rb-space-2) 0;
|
|
4212
|
+
color: var(--rb-text-soft);
|
|
4213
|
+
font-family: var(--rb-font-body);
|
|
4214
|
+
font-weight: var(--rb-font-weight-medium);
|
|
4215
|
+
cursor: pointer;
|
|
4216
|
+
transition: color var(--rb-transition) var(--rb-ease);
|
|
4217
|
+
background: none;
|
|
4218
|
+
border: 0;
|
|
4219
|
+
font-size: 15px;
|
|
4220
|
+
position: relative;
|
|
4221
|
+
}
|
|
4222
|
+
|
|
4223
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-tab:not(:disabled):hover {
|
|
4224
|
+
color: var(--rb-text);
|
|
4225
|
+
}
|
|
4226
|
+
|
|
4227
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-tab:disabled {
|
|
4228
|
+
opacity: .5;
|
|
4229
|
+
cursor: not-allowed;
|
|
4230
|
+
}
|
|
4231
|
+
|
|
4232
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-tab.rb-tab--active, :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-tab[aria-selected="true"] {
|
|
4233
|
+
color: var(--rb-text);
|
|
4234
|
+
}
|
|
4235
|
+
|
|
4236
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-tab.rb-tab--active:after, :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-tab[aria-selected="true"]:after {
|
|
4237
|
+
content: "";
|
|
4238
|
+
background: var(--rb-accent);
|
|
4239
|
+
border-radius: 2px 2px 0 0;
|
|
4240
|
+
height: 2px;
|
|
4241
|
+
position: absolute;
|
|
4242
|
+
bottom: -1px;
|
|
4243
|
+
left: 0;
|
|
4244
|
+
right: 0;
|
|
4245
|
+
}
|
|
4246
|
+
|
|
4247
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-tabpanel {
|
|
4248
|
+
padding: var(--rb-space-4) 0;
|
|
4249
|
+
}
|
|
4250
|
+
|
|
4251
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-tabstrip {
|
|
4252
|
+
gap: var(--rb-space-2);
|
|
4253
|
+
padding: var(--rb-space-2) 0;
|
|
4254
|
+
display: flex;
|
|
4255
|
+
}
|
|
4256
|
+
|
|
4257
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-tabstrip__tab {
|
|
4258
|
+
padding: var(--rb-space-2) var(--rb-space-4);
|
|
4259
|
+
background: var(--rb-surface-2);
|
|
4260
|
+
border: 1px solid var(--rb-border);
|
|
4261
|
+
border-radius: var(--rb-radius);
|
|
4262
|
+
color: var(--rb-text);
|
|
4263
|
+
font-size: var(--rb-text-sm);
|
|
4264
|
+
font-weight: var(--rb-font-weight-medium);
|
|
4265
|
+
cursor: pointer;
|
|
4266
|
+
transition: border-color var(--rb-transition) var(--rb-ease);
|
|
4267
|
+
position: relative;
|
|
4268
|
+
}
|
|
4269
|
+
|
|
4270
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-tabstrip__tab:not(:disabled):hover {
|
|
4271
|
+
border-color: var(--rb-accent);
|
|
4272
|
+
}
|
|
4273
|
+
|
|
4274
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-tabstrip__tab:disabled {
|
|
4275
|
+
opacity: .5;
|
|
4276
|
+
cursor: not-allowed;
|
|
4277
|
+
}
|
|
4278
|
+
|
|
4279
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-tabstrip__tab.rb-tabstrip__tab--active {
|
|
4280
|
+
color: var(--rb-accent);
|
|
4281
|
+
border-color: color-mix(in srgb, var(--rb-accent) 40%, var(--rb-border));
|
|
4282
|
+
background: var(--rb-accent-wash);
|
|
4283
|
+
}
|
|
4284
|
+
|
|
4285
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-tabstrip__badge {
|
|
4286
|
+
color: var(--rb-text-faint);
|
|
4287
|
+
font-size: var(--rb-text-sm);
|
|
4288
|
+
font-variant-numeric: tabular-nums;
|
|
4289
|
+
}
|
|
4290
|
+
|
|
4291
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-wordmark {
|
|
4292
|
+
width: fit-content;
|
|
4293
|
+
font-family: var(--rb-font-display);
|
|
4294
|
+
font-weight: var(--rb-font-weight-medium);
|
|
4295
|
+
letter-spacing: var(--rb-heading-tracking);
|
|
4296
|
+
color: var(--rb-text);
|
|
4297
|
+
align-items: baseline;
|
|
4298
|
+
gap: .4em;
|
|
4299
|
+
margin: 0;
|
|
4300
|
+
font-size: 1rem;
|
|
4301
|
+
display: inline-flex;
|
|
4302
|
+
}
|
|
4303
|
+
|
|
4304
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-wordmark__spark {
|
|
4305
|
+
color: var(--rb-accent);
|
|
4306
|
+
}
|
|
4307
|
+
|
|
4308
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-table {
|
|
4309
|
+
border-collapse: collapse;
|
|
4310
|
+
width: 100%;
|
|
4311
|
+
font-size: 15px;
|
|
4312
|
+
}
|
|
4313
|
+
|
|
4314
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-table th {
|
|
4315
|
+
text-align: left;
|
|
4316
|
+
padding: var(--rb-space-2) var(--rb-space-3);
|
|
4317
|
+
border-bottom: 2px solid var(--rb-border-strong);
|
|
4318
|
+
color: var(--rb-text-soft);
|
|
4319
|
+
font-family: var(--rb-font-mono);
|
|
4320
|
+
letter-spacing: .06em;
|
|
4321
|
+
text-transform: uppercase;
|
|
4322
|
+
font-size: 12px;
|
|
4323
|
+
font-weight: var(--rb-font-weight-medium);
|
|
4324
|
+
}
|
|
4325
|
+
|
|
4326
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-table td {
|
|
4327
|
+
padding: var(--rb-space-2) var(--rb-space-3);
|
|
4328
|
+
border-bottom: 1px solid var(--rb-border);
|
|
4329
|
+
color: var(--rb-text);
|
|
4330
|
+
}
|
|
4331
|
+
|
|
4332
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-table tbody tr:hover td {
|
|
4333
|
+
background: var(--rb-surface-2);
|
|
4334
|
+
}
|
|
4335
|
+
|
|
4336
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-table .rb-num {
|
|
4337
|
+
text-align: right;
|
|
4338
|
+
font-variant-numeric: tabular-nums;
|
|
4339
|
+
}
|
|
4340
|
+
|
|
4341
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-table__group-row td {
|
|
4342
|
+
padding-top: var(--rb-space-3);
|
|
4343
|
+
padding-bottom: var(--rb-space-1);
|
|
4344
|
+
color: var(--rb-text-faint);
|
|
4345
|
+
font-size: var(--rb-text-sm);
|
|
4346
|
+
font-weight: var(--rb-font-weight-medium);
|
|
4347
|
+
text-transform: uppercase;
|
|
4348
|
+
letter-spacing: .04em;
|
|
4349
|
+
border-bottom: none;
|
|
4350
|
+
}
|
|
4351
|
+
|
|
4352
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-table__sort {
|
|
4353
|
+
align-items: center;
|
|
4354
|
+
gap: var(--rb-space-1);
|
|
4355
|
+
font: inherit;
|
|
4356
|
+
color: inherit;
|
|
4357
|
+
cursor: pointer;
|
|
4358
|
+
background: none;
|
|
4359
|
+
border: none;
|
|
4360
|
+
margin: 0;
|
|
4361
|
+
padding: 0;
|
|
4362
|
+
display: inline-flex;
|
|
4363
|
+
}
|
|
4364
|
+
|
|
4365
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-table__sort:hover {
|
|
4366
|
+
color: var(--rb-text);
|
|
4367
|
+
}
|
|
4368
|
+
|
|
4369
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-table__sort-icon {
|
|
4370
|
+
color: var(--rb-text-faint);
|
|
4371
|
+
}
|
|
4372
|
+
|
|
4373
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-table__sort:hover .rb-table__sort-icon {
|
|
4374
|
+
color: var(--rb-text);
|
|
4375
|
+
}
|
|
4376
|
+
|
|
4377
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-table-scroll {
|
|
4378
|
+
max-height: 70vh;
|
|
4379
|
+
overflow: auto;
|
|
4380
|
+
}
|
|
4381
|
+
|
|
4382
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-table-scroll thead th {
|
|
4383
|
+
background: var(--rb-surface);
|
|
4384
|
+
z-index: 1;
|
|
4385
|
+
position: sticky;
|
|
4386
|
+
top: 0;
|
|
4387
|
+
}
|
|
4388
|
+
|
|
4389
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-table--interactive tbody tr {
|
|
4390
|
+
cursor: pointer;
|
|
4391
|
+
}
|
|
4392
|
+
|
|
4393
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-table--interactive tbody tr:focus-visible, :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-table--interactive tbody tr:has(:focus-visible) {
|
|
4394
|
+
outline: var(--rb-focus-ring);
|
|
4395
|
+
outline-offset: -3px;
|
|
4396
|
+
border-radius: 0;
|
|
4397
|
+
}
|
|
4398
|
+
|
|
4399
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-table--interactive tbody tr:focus-visible td, :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-table--interactive tbody tr:has(:focus-visible) td {
|
|
4400
|
+
background: var(--rb-surface-2);
|
|
4401
|
+
}
|
|
4402
|
+
|
|
4403
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-progress {
|
|
4404
|
+
appearance: none;
|
|
4405
|
+
border-radius: var(--rb-radius-pill);
|
|
4406
|
+
background: var(--rb-surface-sunken);
|
|
4407
|
+
border: 0;
|
|
4408
|
+
width: 100%;
|
|
4409
|
+
height: 8px;
|
|
4410
|
+
overflow: hidden;
|
|
4411
|
+
}
|
|
4412
|
+
|
|
4413
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-progress::-webkit-progress-bar {
|
|
4414
|
+
background: var(--rb-surface-sunken);
|
|
4415
|
+
}
|
|
4416
|
+
|
|
4417
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-progress::-webkit-progress-value {
|
|
4418
|
+
background: var(--rb-accent);
|
|
4419
|
+
}
|
|
4420
|
+
|
|
4421
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-progress::-moz-progress-bar {
|
|
4422
|
+
background: var(--rb-accent);
|
|
4423
|
+
}
|
|
4424
|
+
|
|
4425
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-progress:indeterminate::-webkit-progress-bar {
|
|
4426
|
+
background-color: var(--rb-surface-sunken);
|
|
4427
|
+
background-image: linear-gradient(90deg, transparent 0%, var(--rb-accent) 50%, transparent 100%);
|
|
4428
|
+
background-repeat: no-repeat;
|
|
4429
|
+
background-size: 50% 100%;
|
|
4430
|
+
animation: 1.4s ease-in-out infinite rb-noir-indeterminate;
|
|
4431
|
+
}
|
|
4432
|
+
|
|
4433
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-progress:indeterminate::-moz-progress-bar {
|
|
4434
|
+
background-color: var(--rb-surface-sunken);
|
|
4435
|
+
background-image: linear-gradient(90deg, transparent 0%, var(--rb-accent) 50%, transparent 100%);
|
|
4436
|
+
background-repeat: no-repeat;
|
|
4437
|
+
background-size: 50% 100%;
|
|
4438
|
+
animation: 1.4s ease-in-out infinite rb-noir-indeterminate;
|
|
4439
|
+
}
|
|
4440
|
+
|
|
4441
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-spinner {
|
|
4442
|
+
border: 2px solid var(--rb-border-strong);
|
|
4443
|
+
border-top-color: var(--rb-accent);
|
|
4444
|
+
border-radius: 50%;
|
|
4445
|
+
width: 1.25rem;
|
|
4446
|
+
height: 1.25rem;
|
|
4447
|
+
animation: .7s linear infinite rb-noir-spin;
|
|
4448
|
+
display: inline-block;
|
|
4449
|
+
}
|
|
4450
|
+
|
|
4451
|
+
@keyframes rb-noir-spin {
|
|
4452
|
+
to {
|
|
4453
|
+
transform: rotate(360deg);
|
|
4454
|
+
}
|
|
4455
|
+
}
|
|
4456
|
+
|
|
4457
|
+
@keyframes rb-noir-indeterminate {
|
|
4458
|
+
0% {
|
|
4459
|
+
background-position: -50% 0;
|
|
4460
|
+
}
|
|
4461
|
+
|
|
4462
|
+
100% {
|
|
4463
|
+
background-position: 150% 0;
|
|
4464
|
+
}
|
|
4465
|
+
}
|
|
4466
|
+
|
|
4467
|
+
@media (prefers-reduced-motion: reduce) {
|
|
4468
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-progress:indeterminate::-webkit-progress-bar {
|
|
4469
|
+
animation-duration: 2s;
|
|
4470
|
+
}
|
|
4471
|
+
|
|
4472
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-progress:indeterminate::-moz-progress-bar {
|
|
4473
|
+
animation-duration: 2s;
|
|
4474
|
+
}
|
|
4475
|
+
|
|
4476
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-spinner {
|
|
4477
|
+
animation-duration: 2s;
|
|
4478
|
+
}
|
|
4479
|
+
}
|
|
4480
|
+
|
|
4481
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-stepper {
|
|
4482
|
+
isolation: isolate;
|
|
4483
|
+
margin: 0;
|
|
4484
|
+
padding: 0;
|
|
4485
|
+
list-style: none;
|
|
4486
|
+
display: flex;
|
|
4487
|
+
}
|
|
4488
|
+
|
|
4489
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-stepper__step {
|
|
4490
|
+
align-items: center;
|
|
4491
|
+
gap: var(--rb-space-2);
|
|
4492
|
+
text-align: center;
|
|
4493
|
+
flex-direction: column;
|
|
4494
|
+
flex: 1;
|
|
4495
|
+
min-width: 0;
|
|
4496
|
+
display: flex;
|
|
4497
|
+
position: relative;
|
|
4498
|
+
}
|
|
4499
|
+
|
|
4500
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-stepper__step:not(:first-child):before {
|
|
4501
|
+
content: "";
|
|
4502
|
+
background: var(--rb-surface-sunken);
|
|
4503
|
+
z-index: -1;
|
|
4504
|
+
width: 100%;
|
|
4505
|
+
height: 2px;
|
|
4506
|
+
transition: background-color var(--rb-transition) var(--rb-ease);
|
|
4507
|
+
position: absolute;
|
|
4508
|
+
top: 1rem;
|
|
4509
|
+
right: 50%;
|
|
4510
|
+
}
|
|
4511
|
+
|
|
4512
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-stepper__step:not(:first-child).rb-stepper--complete:before, :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-stepper__step:not(:first-child).rb-stepper--current:before, :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-stepper__step[aria-current="step"]:before {
|
|
4513
|
+
background: var(--rb-accent);
|
|
4514
|
+
}
|
|
4515
|
+
|
|
4516
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-stepper__node {
|
|
4517
|
+
border-radius: var(--rb-radius-pill);
|
|
4518
|
+
background: var(--rb-surface-2);
|
|
4519
|
+
width: 2rem;
|
|
4520
|
+
height: 2rem;
|
|
4521
|
+
color: var(--rb-text-faint);
|
|
4522
|
+
font-size: var(--rb-text-sm);
|
|
4523
|
+
font-weight: var(--rb-font-weight-medium);
|
|
4524
|
+
border: 2px solid #0000;
|
|
4525
|
+
justify-content: center;
|
|
4526
|
+
align-items: center;
|
|
4527
|
+
display: flex;
|
|
4528
|
+
}
|
|
4529
|
+
|
|
4530
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-stepper__label {
|
|
4531
|
+
font-size: var(--rb-text-sm);
|
|
4532
|
+
color: var(--rb-text-faint);
|
|
4533
|
+
}
|
|
4534
|
+
|
|
4535
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-stepper--complete .rb-stepper__node {
|
|
4536
|
+
background: var(--rb-accent);
|
|
4537
|
+
color: var(--rb-accent-fg);
|
|
4538
|
+
}
|
|
4539
|
+
|
|
4540
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-stepper--complete .rb-stepper__label {
|
|
4541
|
+
color: var(--rb-text);
|
|
4542
|
+
}
|
|
4543
|
+
|
|
4544
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-stepper--current .rb-stepper__node, :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-stepper__step[aria-current="step"] .rb-stepper__node {
|
|
4545
|
+
background: var(--rb-surface);
|
|
4546
|
+
border-color: var(--rb-accent);
|
|
4547
|
+
color: var(--rb-accent);
|
|
4548
|
+
box-shadow: var(--rb-shadow-lg);
|
|
4549
|
+
}
|
|
4550
|
+
|
|
4551
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-stepper--current .rb-stepper__label, :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-stepper__step[aria-current="step"] .rb-stepper__label {
|
|
4552
|
+
color: var(--rb-accent);
|
|
4553
|
+
font-weight: var(--rb-font-weight-medium);
|
|
4554
|
+
}
|
|
4555
|
+
|
|
4556
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-muted {
|
|
4557
|
+
color: var(--rb-text-faint);
|
|
4558
|
+
font-style: italic;
|
|
4559
|
+
}
|
|
4560
|
+
|
|
4561
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-pre {
|
|
4562
|
+
padding: var(--rb-space-3) var(--rb-space-4);
|
|
4563
|
+
background: var(--rb-surface-sunken);
|
|
4564
|
+
border: 1px solid var(--rb-border);
|
|
4565
|
+
border-radius: var(--rb-radius);
|
|
4566
|
+
font-family: var(--rb-font-mono);
|
|
4567
|
+
font-size: var(--rb-text-sm);
|
|
4568
|
+
margin: 0;
|
|
4569
|
+
overflow-x: auto;
|
|
4570
|
+
}
|
|
4571
|
+
|
|
4572
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-log {
|
|
4573
|
+
white-space: pre-wrap;
|
|
4574
|
+
overflow-wrap: anywhere;
|
|
4575
|
+
max-height: 16rem;
|
|
4576
|
+
line-height: 1.5;
|
|
4577
|
+
overflow-y: auto;
|
|
4578
|
+
}
|
|
4579
|
+
|
|
4580
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-pre.rb-log {
|
|
4581
|
+
overflow-x: hidden;
|
|
4582
|
+
}
|
|
4583
|
+
|
|
4584
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-eyebrow {
|
|
4585
|
+
font-family: var(--rb-font-mono);
|
|
4586
|
+
font-size: 12.5px;
|
|
4587
|
+
font-weight: var(--rb-font-weight-medium);
|
|
4588
|
+
letter-spacing: var(--rb-label-tracking);
|
|
4589
|
+
text-transform: uppercase;
|
|
4590
|
+
color: var(--rb-accent);
|
|
4591
|
+
align-items: center;
|
|
4592
|
+
gap: 10px;
|
|
4593
|
+
display: inline-flex;
|
|
4594
|
+
}
|
|
4595
|
+
|
|
4596
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-eyebrow:before {
|
|
4597
|
+
content: "";
|
|
4598
|
+
background: var(--rb-accent);
|
|
4599
|
+
width: 22px;
|
|
4600
|
+
height: 1.5px;
|
|
4601
|
+
display: inline-block;
|
|
4602
|
+
}
|
|
4603
|
+
|
|
4604
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-rack {
|
|
4605
|
+
--rb-rack-panel: #0a121a;
|
|
4606
|
+
--rb-rack-ink: #e7eef4;
|
|
4607
|
+
--rb-rack-faint: #7d90a2;
|
|
4608
|
+
--rb-rack-line: #1e2b37;
|
|
4609
|
+
background: var(--rb-rack-panel);
|
|
4610
|
+
border: 1px solid var(--rb-rack-line);
|
|
4611
|
+
box-shadow: var(--rb-shadow-lg);
|
|
4612
|
+
color: var(--rb-rack-ink);
|
|
4613
|
+
border-radius: 16px;
|
|
4614
|
+
padding: 24px;
|
|
4615
|
+
overflow: hidden;
|
|
4616
|
+
}
|
|
4617
|
+
|
|
4618
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-rack__top {
|
|
4619
|
+
font-family: var(--rb-font-mono);
|
|
4620
|
+
letter-spacing: .14em;
|
|
4621
|
+
text-transform: uppercase;
|
|
4622
|
+
color: var(--rb-rack-faint);
|
|
4623
|
+
justify-content: space-between;
|
|
4624
|
+
align-items: center;
|
|
4625
|
+
font-size: 11.5px;
|
|
4626
|
+
display: flex;
|
|
4627
|
+
}
|
|
4628
|
+
|
|
4629
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-rack__live {
|
|
4630
|
+
color: var(--rb-rack-ink);
|
|
4631
|
+
align-items: center;
|
|
4632
|
+
gap: 7px;
|
|
4633
|
+
display: inline-flex;
|
|
4634
|
+
}
|
|
4635
|
+
|
|
4636
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-rack__live:before {
|
|
4637
|
+
content: "";
|
|
4638
|
+
background: var(--rb-accent);
|
|
4639
|
+
border-radius: 50%;
|
|
4640
|
+
width: 7px;
|
|
4641
|
+
height: 7px;
|
|
4642
|
+
animation: 1.8s ease-in-out infinite rb-noir-pulse;
|
|
4643
|
+
}
|
|
4644
|
+
|
|
4645
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-rack__bars {
|
|
4646
|
+
align-items: flex-end;
|
|
4647
|
+
gap: 7px;
|
|
4648
|
+
height: 132px;
|
|
4649
|
+
margin: 22px 2px 16px;
|
|
4650
|
+
display: flex;
|
|
4651
|
+
}
|
|
4652
|
+
|
|
4653
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-rack__bars > i {
|
|
4654
|
+
background: var(--rb-accent-grad);
|
|
4655
|
+
transform-origin: bottom;
|
|
4656
|
+
opacity: .92;
|
|
4657
|
+
border-radius: 4px 4px 2px 2px;
|
|
4658
|
+
flex: 1;
|
|
4659
|
+
animation: 1.4s ease-in-out infinite rb-noir-bop;
|
|
4660
|
+
}
|
|
4661
|
+
|
|
4662
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-rack__bars > i:first-child {
|
|
4663
|
+
height: 52%;
|
|
4664
|
+
animation-delay: -.2s;
|
|
4665
|
+
}
|
|
4666
|
+
|
|
4667
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-rack__bars > i:nth-child(2) {
|
|
4668
|
+
height: 88%;
|
|
4669
|
+
animation-delay: -1.1s;
|
|
4670
|
+
}
|
|
4671
|
+
|
|
4672
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-rack__bars > i:nth-child(3) {
|
|
4673
|
+
height: 34%;
|
|
4674
|
+
animation-delay: -.6s;
|
|
4675
|
+
}
|
|
4676
|
+
|
|
4677
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-rack__bars > i:nth-child(4) {
|
|
4678
|
+
height: 70%;
|
|
4679
|
+
animation-delay: -.9s;
|
|
4680
|
+
}
|
|
4681
|
+
|
|
4682
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-rack__bars > i:nth-child(5) {
|
|
4683
|
+
height: 100%;
|
|
4684
|
+
animation-delay: -.35s;
|
|
4685
|
+
}
|
|
4686
|
+
|
|
4687
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-rack__bars > i:nth-child(6) {
|
|
4688
|
+
height: 46%;
|
|
4689
|
+
animation-delay: -1.3s;
|
|
4690
|
+
}
|
|
4691
|
+
|
|
4692
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-rack__foot {
|
|
4693
|
+
border-top: 1px solid var(--rb-rack-line);
|
|
4694
|
+
font-family: var(--rb-font-mono);
|
|
4695
|
+
color: var(--rb-rack-faint);
|
|
4696
|
+
letter-spacing: .04em;
|
|
4697
|
+
justify-content: space-between;
|
|
4698
|
+
gap: 12px;
|
|
4699
|
+
padding-top: 14px;
|
|
4700
|
+
font-size: 11.5px;
|
|
4701
|
+
display: flex;
|
|
4702
|
+
}
|
|
4703
|
+
|
|
4704
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-rack__foot b {
|
|
4705
|
+
color: var(--rb-rack-ink);
|
|
4706
|
+
font-weight: var(--rb-font-weight-medium);
|
|
4707
|
+
}
|
|
4708
|
+
|
|
4709
|
+
@keyframes rb-noir-bop {
|
|
4710
|
+
0%, 100% {
|
|
4711
|
+
transform: scaleY(.5);
|
|
4712
|
+
}
|
|
4713
|
+
|
|
4714
|
+
50% {
|
|
4715
|
+
transform: scaleY(1);
|
|
4716
|
+
}
|
|
4717
|
+
}
|
|
4718
|
+
|
|
4719
|
+
@keyframes rb-noir-pulse {
|
|
4720
|
+
0%, 100% {
|
|
4721
|
+
opacity: 1;
|
|
4722
|
+
}
|
|
4723
|
+
|
|
4724
|
+
50% {
|
|
4725
|
+
opacity: .35;
|
|
4726
|
+
}
|
|
4727
|
+
}
|
|
4728
|
+
|
|
4729
|
+
@media (prefers-reduced-motion: reduce) {
|
|
4730
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-rack__bars > i, :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-rack__live:before {
|
|
4731
|
+
animation: none;
|
|
4732
|
+
}
|
|
4733
|
+
}
|
|
4734
|
+
|
|
4735
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-principles {
|
|
4736
|
+
flex-direction: column;
|
|
4737
|
+
display: flex;
|
|
4738
|
+
}
|
|
4739
|
+
|
|
4740
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-principle {
|
|
4741
|
+
border-top: 1px solid var(--rb-border);
|
|
4742
|
+
grid-template-columns: 56px 1fr;
|
|
4743
|
+
gap: 18px;
|
|
4744
|
+
padding: 22px 0;
|
|
4745
|
+
display: grid;
|
|
4746
|
+
}
|
|
4747
|
+
|
|
4748
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-principles > .rb-principle:first-child {
|
|
4749
|
+
border-top: 0;
|
|
4750
|
+
}
|
|
4751
|
+
|
|
4752
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-principle__n {
|
|
4753
|
+
font-family: var(--rb-font-mono);
|
|
4754
|
+
font-size: 13px;
|
|
4755
|
+
font-weight: var(--rb-font-weight-medium);
|
|
4756
|
+
color: var(--rb-accent);
|
|
4757
|
+
padding-top: 3px;
|
|
4758
|
+
}
|
|
4759
|
+
|
|
4760
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-principle__body h3 {
|
|
4761
|
+
margin: 0;
|
|
4762
|
+
font-size: 1.16rem;
|
|
4763
|
+
}
|
|
4764
|
+
|
|
4765
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-principle__body p {
|
|
4766
|
+
color: var(--rb-text-soft);
|
|
4767
|
+
margin: 7px 0 0;
|
|
4768
|
+
}
|
|
4769
|
+
|
|
4770
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-tags {
|
|
4771
|
+
flex-wrap: wrap;
|
|
4772
|
+
gap: 8px;
|
|
4773
|
+
margin: 0;
|
|
4774
|
+
padding: 0;
|
|
4775
|
+
list-style: none;
|
|
4776
|
+
display: flex;
|
|
4777
|
+
}
|
|
4778
|
+
|
|
4779
|
+
:where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-tag {
|
|
4780
|
+
font-family: var(--rb-font-mono);
|
|
4781
|
+
color: var(--rb-text-soft);
|
|
4782
|
+
border: 1px solid var(--rb-border);
|
|
4783
|
+
border-radius: var(--rb-radius-pill);
|
|
4784
|
+
letter-spacing: .02em;
|
|
4785
|
+
padding: 4px 10px;
|
|
4786
|
+
font-size: 11.5px;
|
|
4787
|
+
}
|
|
4788
|
+
`;
|
|
4789
|
+
|
|
4790
|
+
// plugins/tracker/src/web/theme.ts
|
|
4791
|
+
var THEME_CSS = bundle_default;
|
|
4792
|
+
var LAYOUT_CSS = `
|
|
4793
|
+
/* tracker web area layout (not part of the theme) */
|
|
4794
|
+
.tr-top { display: flex; flex-wrap: wrap; align-items: center; gap: var(--rb-space-3); padding: var(--rb-space-3) var(--rb-space-4); border-bottom: 1px solid var(--rb-border); }
|
|
4795
|
+
.tr-top nav { display: flex; flex-wrap: wrap; align-items: center; gap: var(--rb-space-2); margin-left: auto; }
|
|
4796
|
+
.tr-top form { margin: 0; }
|
|
4797
|
+
.tr-main { max-width: 60rem; margin: 0 auto; padding: var(--rb-space-4); }
|
|
4798
|
+
.tr-main > section { margin-bottom: var(--rb-space-5); }
|
|
4799
|
+
.tr-stack { display: grid; gap: var(--rb-space-3); max-width: 28rem; }
|
|
4800
|
+
.tr-foot { margin-top: var(--rb-space-5); }
|
|
4801
|
+
`;
|
|
4802
|
+
var STYLESHEET = `${THEME_CSS}
|
|
4803
|
+
${LAYOUT_CSS}`;
|
|
4804
|
+
var STYLESHEET_PATH = `/assets/theme.${sha256(STYLESHEET).slice(0, 16)}.css`;
|
|
4805
|
+
|
|
4806
|
+
// plugins/tracker/src/web/html.ts
|
|
4807
|
+
function esc(value) {
|
|
4808
|
+
return String(value).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
4809
|
+
}
|
|
4810
|
+
|
|
4811
|
+
class Html {
|
|
4812
|
+
value;
|
|
4813
|
+
constructor(value) {
|
|
4814
|
+
this.value = value;
|
|
4815
|
+
}
|
|
4816
|
+
toString() {
|
|
4817
|
+
return this.value;
|
|
4818
|
+
}
|
|
4819
|
+
}
|
|
4820
|
+
function render(part) {
|
|
4821
|
+
if (part === null || part === undefined || part === false)
|
|
4822
|
+
return "";
|
|
4823
|
+
if (part instanceof Html)
|
|
4824
|
+
return part.value;
|
|
4825
|
+
if (Array.isArray(part))
|
|
4826
|
+
return part.map(render).join("");
|
|
4827
|
+
return esc(part);
|
|
4828
|
+
}
|
|
4829
|
+
function html(strings, ...values) {
|
|
4830
|
+
let out = strings[0] ?? "";
|
|
4831
|
+
values.forEach((v, i) => {
|
|
4832
|
+
out += render(v) + (strings[i + 1] ?? "");
|
|
4833
|
+
});
|
|
4834
|
+
return new Html(out);
|
|
4835
|
+
}
|
|
4836
|
+
var CSP = "default-src 'none'; style-src 'self'; img-src 'none'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'";
|
|
4837
|
+
var REFERRER_POLICY = "same-origin";
|
|
4838
|
+
function pageHeaders(extra = {}) {
|
|
4839
|
+
const h = new Headers({
|
|
4840
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
4841
|
+
"Content-Security-Policy": CSP,
|
|
4842
|
+
"X-Content-Type-Options": "nosniff",
|
|
4843
|
+
"X-Frame-Options": "DENY",
|
|
4844
|
+
"Referrer-Policy": REFERRER_POLICY,
|
|
4845
|
+
"Cache-Control": "no-store"
|
|
4846
|
+
});
|
|
4847
|
+
for (const [k, v] of Object.entries(extra))
|
|
4848
|
+
h.set(k, v);
|
|
4849
|
+
return h;
|
|
4850
|
+
}
|
|
4851
|
+
function page(f) {
|
|
4852
|
+
const nav = f.signedIn ? html`<nav aria-label="Tracker">
|
|
4853
|
+
<span class="rb-muted">${f.signedIn.name}</span>
|
|
4854
|
+
<a class="rb-link" href="${f.base}/">My tasks</a>
|
|
4855
|
+
<a class="rb-link" href="${f.base}/settings">Settings</a>
|
|
4856
|
+
<form method="post" action="${f.base}/logout"><input type="hidden" name="csrf" value="${f.signedIn.csrf}"><button class="rb-btn rb-btn--ghost rb-btn--sm" type="submit">Sign out</button></form>
|
|
4857
|
+
</nav>` : null;
|
|
4858
|
+
return html`<!doctype html>
|
|
4859
|
+
<html lang="en" data-rb-style="rackbops-noir">
|
|
4860
|
+
<head>
|
|
4861
|
+
<meta charset="utf-8">
|
|
4862
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
4863
|
+
<meta name="referrer" content="${REFERRER_POLICY}">
|
|
4864
|
+
<title>${f.title} -- Tracker</title>
|
|
4865
|
+
<link rel="stylesheet" href="${f.base}${STYLESHEET_PATH}">
|
|
4866
|
+
</head>
|
|
4867
|
+
<body>
|
|
4868
|
+
<header class="tr-top"><span class="rb-wordmark">Tracker<span class="rb-wordmark__spark">.</span></span>${nav}</header>
|
|
4869
|
+
<main class="tr-main">
|
|
4870
|
+
${f.body}
|
|
4871
|
+
</main>
|
|
4872
|
+
</body>
|
|
4873
|
+
</html>
|
|
4874
|
+
`.value;
|
|
4875
|
+
}
|
|
4876
|
+
function htmlResponse(body, status = 200, extra = {}) {
|
|
4877
|
+
return new Response(body, { status, headers: pageHeaders(extra) });
|
|
4878
|
+
}
|
|
4879
|
+
function redirect(location, cookies = []) {
|
|
4880
|
+
const h = new Headers({ Location: location, "Cache-Control": "no-store", "Referrer-Policy": REFERRER_POLICY });
|
|
4881
|
+
for (const c of cookies)
|
|
4882
|
+
h.append("Set-Cookie", c);
|
|
4883
|
+
return new Response(null, { status: 303, headers: h });
|
|
4884
|
+
}
|
|
4885
|
+
function readCookie(request, name) {
|
|
4886
|
+
const header = request.headers.get("cookie");
|
|
4887
|
+
if (!header)
|
|
4888
|
+
return null;
|
|
4889
|
+
for (const part of header.split(";")) {
|
|
4890
|
+
const eq = part.indexOf("=");
|
|
4891
|
+
if (eq < 0)
|
|
4892
|
+
continue;
|
|
4893
|
+
if (part.slice(0, eq).trim() === name)
|
|
4894
|
+
return part.slice(eq + 1).trim();
|
|
4895
|
+
}
|
|
4896
|
+
return null;
|
|
4897
|
+
}
|
|
4898
|
+
function cookie(name, value, o) {
|
|
4899
|
+
return `${name}=${value}; Path=${o.base}/; Max-Age=${o.maxAgeSeconds}; HttpOnly; Secure; SameSite=${o.sameSite}`;
|
|
4900
|
+
}
|
|
4901
|
+
|
|
4902
|
+
// plugins/tracker/src/web/pages.ts
|
|
4903
|
+
function name3(u) {
|
|
4904
|
+
return u.displayName ?? "you";
|
|
4905
|
+
}
|
|
4906
|
+
function framed(v, title, body) {
|
|
4907
|
+
return page({ base: v.base, title, signedIn: { name: name3(v.user), csrf: v.csrf }, body });
|
|
4908
|
+
}
|
|
4909
|
+
function signInHelpPage(base, note) {
|
|
4910
|
+
return page({
|
|
4911
|
+
base,
|
|
4912
|
+
title: "Sign in",
|
|
4913
|
+
body: html`<section class="rb-card">
|
|
4914
|
+
<h1>Sign in</h1>
|
|
4915
|
+
${note ? html`<p class="rb-muted">${note}</p>` : null}
|
|
4916
|
+
<p>Run <code>/web</code> in Discord: the bot answers you alone with a sign-in link, good for ten minutes and one use.</p>
|
|
4917
|
+
</section>`
|
|
4918
|
+
});
|
|
4919
|
+
}
|
|
4920
|
+
function loginPage(base, input) {
|
|
4921
|
+
if (!input.valid)
|
|
4922
|
+
return signInHelpPage(base, "That sign-in link has expired or was already used.");
|
|
4923
|
+
return page({
|
|
4924
|
+
base,
|
|
4925
|
+
title: "Sign in",
|
|
4926
|
+
body: html`<section class="rb-card">
|
|
4927
|
+
<h1>Sign in to the tracker</h1>
|
|
4928
|
+
<p>This link works once. Press the button to sign in on this browser.</p>
|
|
4929
|
+
<form method="post" action="${base}/login">
|
|
4930
|
+
<input type="hidden" name="t" value="${input.token}">
|
|
4931
|
+
<input type="hidden" name="csrf" value="${input.loginCsrf}">
|
|
4932
|
+
<button class="rb-btn rb-btn--primary" type="submit">Sign in</button>
|
|
4933
|
+
</form>
|
|
4934
|
+
</section>`
|
|
4935
|
+
});
|
|
4936
|
+
}
|
|
4937
|
+
function when2(entry, viewer, now) {
|
|
4938
|
+
return entry.next ? formatInstant(entry.next.dueAt, viewer.timeZone, now) : "nothing due";
|
|
4939
|
+
}
|
|
4940
|
+
function cadence(entry, viewer, now) {
|
|
4941
|
+
const s = entry.task.schedule;
|
|
4942
|
+
if (!s || s.kind === "once")
|
|
4943
|
+
return "once";
|
|
4944
|
+
return describeSchedule(s, entry.from ?? viewer, viewer.timeZone, now);
|
|
4945
|
+
}
|
|
4946
|
+
function tasksPage(v, data, now) {
|
|
4947
|
+
const rows = data.entries.map((e) => html`<tr>
|
|
4948
|
+
<td><a class="rb-link" href="${v.base}/tasks/${e.task.id}">${e.task.title}</a></td>
|
|
4949
|
+
<td>${e.from ? name3(e.from) : "you"}</td>
|
|
4950
|
+
<td>${when2(e, v.user, now)}</td>
|
|
4951
|
+
<td>${cadence(e, v.user, now)}</td>
|
|
4952
|
+
</tr>`);
|
|
4953
|
+
const active = data.entries.length === 0 ? html`<p class="rb-muted">You have no active tasks. <code>/remind</code> in Discord sets one.</p>` : html`<div class="rb-table-scroll"><table class="rb-table">
|
|
4954
|
+
<thead><tr><th scope="col">Task</th><th scope="col">From</th><th scope="col">Next</th><th scope="col">Repeats</th></tr></thead>
|
|
4955
|
+
<tbody>${rows}</tbody>
|
|
4956
|
+
</table></div>`;
|
|
4957
|
+
const paused = data.paused.length === 0 ? null : html`<section>
|
|
4958
|
+
<h2>Paused</h2>
|
|
4959
|
+
<ul>${data.paused.map((p) => html`<li><a class="rb-link" href="${v.base}/tasks/${p.task.id}">${p.task.title}</a>${p.held.length > 0 ? html` -- I could not DM ${p.held.join(", ")}; <code>/task resume ${p.task.id}</code> goes on without them.` : null}</li>`)}</ul>
|
|
4960
|
+
</section>`;
|
|
4961
|
+
return framed(v, "My tasks", html`<section>
|
|
4962
|
+
<h1>My tasks</h1>
|
|
4963
|
+
<p class="rb-muted">Times are in ${v.user.timeZone}.</p>
|
|
4964
|
+
${active}
|
|
4965
|
+
</section>
|
|
4966
|
+
${paused}
|
|
4967
|
+
<p class="rb-muted tr-foot">${ADMIN_DISCLOSURE}</p>`);
|
|
4968
|
+
}
|
|
4969
|
+
function historyPage(v, h) {
|
|
4970
|
+
const runs = h.runs.length === 0 ? html`<p class="rb-muted">No runs yet.</p>` : html`<div class="rb-table-scroll"><table class="rb-table">
|
|
4971
|
+
<thead><tr><th scope="col">Due</th><th scope="col">Status</th><th scope="col">Answers</th></tr></thead>
|
|
4972
|
+
<tbody>${h.runs.map((r) => html`<tr>
|
|
4973
|
+
<td>${r.due}</td>
|
|
4974
|
+
<td>${r.status}${r.late ? " (late)" : ""}${r.error ? html`<br><span class="rb-muted">${r.error}</span>` : null}</td>
|
|
4975
|
+
<td>${r.answers.join("; ")}</td>
|
|
4976
|
+
</tr>`)}</tbody>
|
|
4977
|
+
</table></div>`;
|
|
4978
|
+
return framed(v, h.task.title, html`<section>
|
|
4979
|
+
<p><a class="rb-link" href="${v.base}/">My tasks</a></p>
|
|
4980
|
+
<h1>${h.task.title}</h1>
|
|
4981
|
+
<p>${h.task.type}, ${h.task.status}, ${h.cadence}. Next: ${h.next ?? "nothing scheduled"}.</p>
|
|
4982
|
+
</section>
|
|
4983
|
+
<section>
|
|
4984
|
+
<h2>Runs</h2>
|
|
4985
|
+
${h.earlierRuns > 0 ? html`<p class="rb-muted">${h.earlierRuns} earlier run(s) not shown.</p>` : null}
|
|
4986
|
+
${runs}
|
|
4987
|
+
</section>
|
|
4988
|
+
<section>
|
|
4989
|
+
<h2>Changes</h2>
|
|
4990
|
+
${h.earlierChanges > 0 ? html`<p class="rb-muted">${h.earlierChanges} earlier change(s) not shown.</p>` : null}
|
|
4991
|
+
<ul>${h.changes.map((c) => html`<li>${c.at} ${c.kind}${c.detail !== null ? `: ${c.detail}` : ""}</li>`)}</ul>
|
|
4992
|
+
</section>`);
|
|
4993
|
+
}
|
|
4994
|
+
function settingsPage(v, form) {
|
|
4995
|
+
const hours = Array.from({ length: 24 }, (_, h) => h);
|
|
4996
|
+
return framed(v, "Settings", html`<section>
|
|
4997
|
+
<h1>Settings</h1>
|
|
4998
|
+
${form.saved ? html`<div class="rb-alert rb-alert--success" role="status"><p class="rb-alert__title">Saved.</p></div>` : null}
|
|
4999
|
+
${form.error ? html`<div class="rb-alert rb-alert--danger" role="alert"><p class="rb-alert__title">Not saved</p><p>${form.error}</p></div>` : null}
|
|
5000
|
+
<form method="post" action="${v.base}/settings" class="tr-stack">
|
|
5001
|
+
<input type="hidden" name="csrf" value="${v.csrf}">
|
|
5002
|
+
<div class="rb-field">
|
|
5003
|
+
<label class="rb-label" for="hour">Preferred hour</label>
|
|
5004
|
+
<select class="rb-select" id="hour" name="hour">${hours.map((h) => html`<option value="${h}"${String(h) === form.hour ? html` selected` : null}>${String(h).padStart(2, "0")}:00</option>`)}</select>
|
|
5005
|
+
<p class="rb-field__help">A reminder without a time of day arrives at this hour.</p>
|
|
5006
|
+
</div>
|
|
5007
|
+
<div class="rb-field">
|
|
5008
|
+
<label class="rb-label" for="zone">Time zone</label>
|
|
5009
|
+
<input class="rb-input" id="zone" name="zone" value="${form.zone}" maxlength="64" required${form.error ? html` aria-invalid="true"` : null}>
|
|
5010
|
+
<p class="rb-field__help">An IANA name, such as America/New_York or Europe/London.</p>
|
|
5011
|
+
</div>
|
|
5012
|
+
<div><button class="rb-btn rb-btn--primary" type="submit">Save</button></div>
|
|
5013
|
+
</form>
|
|
5014
|
+
</section>`);
|
|
5015
|
+
}
|
|
5016
|
+
function notFoundPage(base, v) {
|
|
5017
|
+
const body = html`<section><h1>Not found</h1><p>There is nothing here, or nothing you can see.</p><p><a class="rb-link" href="${base}/">My tasks</a></p></section>`;
|
|
5018
|
+
return v ? framed(v, "Not found", body) : page({ base, title: "Not found", body });
|
|
5019
|
+
}
|
|
5020
|
+
function errorPage(base, title, message) {
|
|
5021
|
+
return page({ base, title, body: html`<section class="rb-card"><h1>${title}</h1><p>${message}</p></section>` });
|
|
5022
|
+
}
|
|
5023
|
+
|
|
5024
|
+
// plugins/tracker/src/web/sessions.ts
|
|
5025
|
+
var SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
5026
|
+
|
|
5027
|
+
class Sessions {
|
|
5028
|
+
db;
|
|
5029
|
+
constructor(db) {
|
|
5030
|
+
this.db = db;
|
|
5031
|
+
}
|
|
5032
|
+
create(userId, now, memberCheckedAt = null) {
|
|
5033
|
+
const id = randomToken();
|
|
5034
|
+
const at = now.toISOString();
|
|
5035
|
+
const session = { userId, csrf: randomToken(), expiresAt: new Date(now.getTime() + SESSION_TTL_MS).toISOString(), memberCheckedAt };
|
|
5036
|
+
this.db.query("DELETE FROM web_sessions WHERE expires_at <= ?").run(at);
|
|
5037
|
+
this.db.query("INSERT INTO web_sessions (id_hash, user_id, csrf, created_at, expires_at, member_checked_at) VALUES (?, ?, ?, ?, ?, ?)").run(sha256(id), userId, session.csrf, at, session.expiresAt, memberCheckedAt);
|
|
5038
|
+
return { id, session };
|
|
5039
|
+
}
|
|
5040
|
+
find(id, now) {
|
|
5041
|
+
if (!isTokenShape(id))
|
|
5042
|
+
return null;
|
|
5043
|
+
const row = this.db.query("SELECT user_id, csrf, expires_at, member_checked_at FROM web_sessions WHERE id_hash = ? AND expires_at > ?").get(sha256(id), now.toISOString());
|
|
5044
|
+
return row ? { userId: row.user_id, csrf: row.csrf, expiresAt: row.expires_at, memberCheckedAt: row.member_checked_at } : null;
|
|
5045
|
+
}
|
|
5046
|
+
delete(id) {
|
|
5047
|
+
if (isTokenShape(id))
|
|
5048
|
+
this.db.query("DELETE FROM web_sessions WHERE id_hash = ?").run(sha256(id));
|
|
5049
|
+
}
|
|
5050
|
+
confirmMember(userId, at) {
|
|
5051
|
+
this.db.query("UPDATE web_sessions SET member_checked_at = ? WHERE user_id = ?").run(at, userId);
|
|
5052
|
+
}
|
|
5053
|
+
deleteForUser(userId) {
|
|
5054
|
+
this.db.query("DELETE FROM web_sessions WHERE user_id = ?").run(userId);
|
|
5055
|
+
}
|
|
5056
|
+
}
|
|
5057
|
+
|
|
5058
|
+
// plugins/tracker/src/web/app.ts
|
|
5059
|
+
var MEMBER_RECHECK_MS = 15 * 60 * 1000;
|
|
5060
|
+
var MEMBER_GRACE_MS = 24 * 60 * 60 * 1000;
|
|
5061
|
+
var MEMBER_CHECK_TIMEOUT_MS = 3000;
|
|
5062
|
+
var MEMBER_RETRY_MS = 60 * 1000;
|
|
5063
|
+
var LEFT_SERVER = `You are signed out. ${NOT_MEMBER}`;
|
|
5064
|
+
var RECHECK_FAILED = "You are signed out: I could not check that you are still a member of this tracker's server.";
|
|
5065
|
+
var MAX_FORM_BYTES = 8 * 1024;
|
|
5066
|
+
var TASK_PATH = /^\/tasks\/([^/]+)$/;
|
|
5067
|
+
function plain(status, text, extra = {}) {
|
|
5068
|
+
return new Response(text, {
|
|
5069
|
+
status,
|
|
5070
|
+
headers: { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff", ...extra }
|
|
5071
|
+
});
|
|
5072
|
+
}
|
|
5073
|
+
async function lookupWithin(lookup, ms) {
|
|
5074
|
+
let timer;
|
|
5075
|
+
const late = new Promise((resolve) => {
|
|
5076
|
+
timer = setTimeout(() => resolve("unknown"), ms);
|
|
5077
|
+
});
|
|
5078
|
+
try {
|
|
5079
|
+
return await Promise.race([lookup().catch(() => "unknown"), late]);
|
|
5080
|
+
} finally {
|
|
5081
|
+
clearTimeout(timer);
|
|
5082
|
+
}
|
|
5083
|
+
}
|
|
5084
|
+
function createWebHandler(w) {
|
|
5085
|
+
const base = `/${w.name}`;
|
|
5086
|
+
const SESSION_COOKIE = `__Secure-${w.name}-session`;
|
|
5087
|
+
const LOGIN_COOKIE = `__Secure-${w.name}-login`;
|
|
5088
|
+
const clear = (name) => cookie(name, "", { base, maxAgeSeconds: 0, sameSite: "Lax" });
|
|
5089
|
+
const inFlight = new Map;
|
|
5090
|
+
const failedAt = new Map;
|
|
5091
|
+
async function authenticate(d, request) {
|
|
5092
|
+
const id = readCookie(request, SESSION_COOKIE);
|
|
5093
|
+
if (!id)
|
|
5094
|
+
return { kind: "none" };
|
|
5095
|
+
const session = d.sessions.find(id, d.clock.now());
|
|
5096
|
+
if (!session)
|
|
5097
|
+
return { kind: "stale" };
|
|
5098
|
+
const user = await d.store.getUser(session.userId);
|
|
5099
|
+
if (!user || !d.admissions.isRegistered(user.id)) {
|
|
5100
|
+
d.sessions.deleteForUser(session.userId);
|
|
5101
|
+
return { kind: "stale" };
|
|
5102
|
+
}
|
|
5103
|
+
const refused = await recheckMembership(d, session, user);
|
|
5104
|
+
if (refused)
|
|
5105
|
+
return refused;
|
|
5106
|
+
const current = d.sessions.find(id, d.clock.now());
|
|
5107
|
+
if (!current)
|
|
5108
|
+
return { kind: "stale" };
|
|
5109
|
+
return { kind: "ok", id, session: current, user };
|
|
5110
|
+
}
|
|
5111
|
+
function lookupShared(userId, discordId) {
|
|
5112
|
+
let pending = inFlight.get(userId);
|
|
5113
|
+
if (!pending) {
|
|
5114
|
+
const started = w.membership(discordId).catch(() => "unknown");
|
|
5115
|
+
pending = started;
|
|
5116
|
+
inFlight.set(userId, started);
|
|
5117
|
+
started.finally(() => {
|
|
5118
|
+
if (inFlight.get(userId) === started)
|
|
5119
|
+
inFlight.delete(userId);
|
|
5120
|
+
});
|
|
5121
|
+
}
|
|
5122
|
+
const shared = pending;
|
|
5123
|
+
return lookupWithin(() => shared, MEMBER_CHECK_TIMEOUT_MS);
|
|
5124
|
+
}
|
|
5125
|
+
async function recheckMembership(d, session, user) {
|
|
5126
|
+
if (w.guildId === null)
|
|
5127
|
+
return null;
|
|
5128
|
+
const now = d.clock.now().getTime();
|
|
5129
|
+
const age = session.memberCheckedAt ? now - Date.parse(session.memberCheckedAt) : Number.POSITIVE_INFINITY;
|
|
5130
|
+
const fresh = (limit) => age >= 0 && age < limit;
|
|
5131
|
+
if (fresh(MEMBER_RECHECK_MS))
|
|
5132
|
+
return null;
|
|
5133
|
+
const lastFailed = failedAt.get(user.id);
|
|
5134
|
+
const backingOff = lastFailed !== undefined && now - lastFailed >= 0 && now - lastFailed < MEMBER_RETRY_MS;
|
|
5135
|
+
let found = "unknown";
|
|
5136
|
+
if (!backingOff && user.discordId) {
|
|
5137
|
+
found = await lookupShared(user.id, user.discordId);
|
|
5138
|
+
if (found === "member" || found === "not-member")
|
|
5139
|
+
failedAt.delete(user.id);
|
|
5140
|
+
else
|
|
5141
|
+
failedAt.set(user.id, d.clock.now().getTime());
|
|
5142
|
+
}
|
|
5143
|
+
if (found === "not-member") {
|
|
5144
|
+
d.sessions.deleteForUser(user.id);
|
|
5145
|
+
return { kind: "signed-out", note: LEFT_SERVER };
|
|
5146
|
+
}
|
|
5147
|
+
if (found === "member") {
|
|
5148
|
+
d.sessions.confirmMember(user.id, d.clock.now().toISOString());
|
|
5149
|
+
return null;
|
|
5150
|
+
}
|
|
5151
|
+
if (fresh(MEMBER_GRACE_MS))
|
|
5152
|
+
return null;
|
|
5153
|
+
d.sessions.deleteForUser(user.id);
|
|
5154
|
+
return { kind: "signed-out", note: RECHECK_FAILED };
|
|
5155
|
+
}
|
|
5156
|
+
async function readForm(request) {
|
|
5157
|
+
const type = request.headers.get("content-type") ?? "";
|
|
5158
|
+
if (!type.toLowerCase().startsWith("application/x-www-form-urlencoded"))
|
|
5159
|
+
return null;
|
|
5160
|
+
const body = await request.text();
|
|
5161
|
+
if (Buffer.byteLength(body) > MAX_FORM_BYTES)
|
|
5162
|
+
return null;
|
|
5163
|
+
return new URLSearchParams(body);
|
|
5164
|
+
}
|
|
5165
|
+
function forbidden() {
|
|
5166
|
+
return htmlResponse(errorPage(base, "Refused", "That form was stale or came from somewhere else. Go back, reload the page and try again."), 403);
|
|
5167
|
+
}
|
|
5168
|
+
async function loginGet(d, url) {
|
|
5169
|
+
const token = url.searchParams.get("t") ?? "";
|
|
5170
|
+
const valid = d.logins.peek(token, d.clock.now());
|
|
5171
|
+
if (!valid)
|
|
5172
|
+
return htmlResponse(loginPage(base, { token: "", loginCsrf: "", valid: false }));
|
|
5173
|
+
const loginCsrf = randomToken();
|
|
5174
|
+
return htmlResponse(loginPage(base, { token, loginCsrf, valid: true }), 200, {
|
|
5175
|
+
"Set-Cookie": cookie(LOGIN_COOKIE, loginCsrf, { base, maxAgeSeconds: LINK_TTL_MS / 1000, sameSite: "Strict" })
|
|
5176
|
+
});
|
|
5177
|
+
}
|
|
5178
|
+
async function loginPost(d, request) {
|
|
5179
|
+
const form = await readForm(request);
|
|
5180
|
+
if (!form)
|
|
5181
|
+
return plain(400, "Bad request");
|
|
5182
|
+
const expected = readCookie(request, LOGIN_COOKIE);
|
|
5183
|
+
const sent = form.get("csrf") ?? "";
|
|
5184
|
+
if (!expected || !safeEqual(expected, sent))
|
|
5185
|
+
return forbidden();
|
|
5186
|
+
const now = d.clock.now();
|
|
5187
|
+
const link = d.logins.consume(form.get("t") ?? "", now);
|
|
5188
|
+
const user = link ? await d.store.getUser(link.userId) : null;
|
|
5189
|
+
if (!user || !d.admissions.isRegistered(user.id)) {
|
|
5190
|
+
return htmlResponse(signInHelpPage(base, "That sign-in link has expired or was already used."), 400, { "Set-Cookie": clear(LOGIN_COOKIE) });
|
|
5191
|
+
}
|
|
5192
|
+
const { id } = d.sessions.create(user.id, now, link?.memberCheckedAt ?? null);
|
|
5193
|
+
return redirect(`${base}/`, [cookie(SESSION_COOKIE, id, { base, maxAgeSeconds: SESSION_TTL_MS / 1000, sameSite: "Lax" }), clear(LOGIN_COOKIE)]);
|
|
5194
|
+
}
|
|
5195
|
+
async function settingsPost(d, v, form) {
|
|
5196
|
+
const hour = (form.get("hour") ?? "").trim();
|
|
5197
|
+
const zone = (form.get("zone") ?? "").trim();
|
|
5198
|
+
let error = null;
|
|
5199
|
+
if (!/^([0-9]|1[0-9]|2[0-3])$/.test(hour))
|
|
5200
|
+
error = "The preferred hour must be a whole hour from 0 to 23.";
|
|
5201
|
+
else if (zone === "" || zone.length > 64)
|
|
5202
|
+
error = "Give a time zone, such as America/New_York.";
|
|
5203
|
+
else {
|
|
5204
|
+
error = await w.queue(async () => {
|
|
5205
|
+
const fresh = await d.store.getUser(v.user.id);
|
|
5206
|
+
return fresh ? saveSettings(d, fresh, { hour: Number(hour), zone }) : "You are no longer on this tracker's list.";
|
|
5207
|
+
});
|
|
5208
|
+
}
|
|
5209
|
+
if (error !== null)
|
|
5210
|
+
return htmlResponse(settingsPage(v, { hour, zone, error }), 400);
|
|
5211
|
+
return redirect(`${base}/settings?saved=1`);
|
|
5212
|
+
}
|
|
5213
|
+
return async (request, info) => {
|
|
5214
|
+
const method = request.method;
|
|
5215
|
+
if (method !== "GET" && method !== "POST")
|
|
5216
|
+
return plain(405, "Method not allowed", { Allow: "GET, POST" });
|
|
5217
|
+
if (w.origin === null)
|
|
5218
|
+
return plain(404, "Not found");
|
|
5219
|
+
const d = w.deps();
|
|
5220
|
+
if (!d)
|
|
5221
|
+
return plain(503, "The tracker is starting up; try again in a minute.");
|
|
5222
|
+
if (method === "POST") {
|
|
5223
|
+
const origin = request.headers.get("origin");
|
|
5224
|
+
if (origin !== null && origin !== w.origin)
|
|
5225
|
+
return forbidden();
|
|
5226
|
+
}
|
|
5227
|
+
const path = info.path;
|
|
5228
|
+
const url = new URL(request.url);
|
|
5229
|
+
const only = (allowed) => method === allowed ? null : plain(405, "Method not allowed", { Allow: allowed });
|
|
5230
|
+
if (path === STYLESHEET_PATH) {
|
|
5231
|
+
return only("GET") ?? new Response(STYLESHEET, {
|
|
5232
|
+
headers: { "Content-Type": "text/css; charset=utf-8", "Cache-Control": "public, max-age=31536000, immutable", "X-Content-Type-Options": "nosniff" }
|
|
5233
|
+
});
|
|
5234
|
+
}
|
|
5235
|
+
if (path === "/signin")
|
|
5236
|
+
return only("GET") ?? htmlResponse(signInHelpPage(base, url.searchParams.get("out") === "1" ? "You are signed out." : undefined));
|
|
5237
|
+
if (path === "/login")
|
|
5238
|
+
return method === "GET" ? loginGet(d, url) : loginPost(d, request);
|
|
5239
|
+
const task = TASK_PATH.exec(path);
|
|
5240
|
+
const route = path === "/" ? "tasks" : path === "/settings" ? "settings" : path === "/logout" ? "logout" : task ? "history" : null;
|
|
5241
|
+
if (route === null)
|
|
5242
|
+
return htmlResponse(notFoundPage(base), 404);
|
|
5243
|
+
const wrong = route === "logout" ? only("POST") : route === "settings" ? null : only("GET");
|
|
5244
|
+
if (wrong)
|
|
5245
|
+
return wrong;
|
|
5246
|
+
const auth = await authenticate(d, request);
|
|
5247
|
+
if (auth.kind === "signed-out")
|
|
5248
|
+
return htmlResponse(signInHelpPage(base, auth.note), 403, { "Set-Cookie": clear(SESSION_COOKIE) });
|
|
5249
|
+
if (auth.kind !== "ok")
|
|
5250
|
+
return redirect(`${base}/signin`, auth.kind === "stale" ? [clear(SESSION_COOKIE)] : []);
|
|
5251
|
+
const v = { base, user: auth.user, csrf: auth.session.csrf };
|
|
5252
|
+
if (method === "POST") {
|
|
5253
|
+
const form = await readForm(request);
|
|
5254
|
+
if (!form)
|
|
5255
|
+
return plain(400, "Bad request");
|
|
5256
|
+
if (!safeEqual(auth.session.csrf, form.get("csrf") ?? ""))
|
|
5257
|
+
return forbidden();
|
|
5258
|
+
if (route === "logout") {
|
|
5259
|
+
d.sessions.delete(auth.id);
|
|
5260
|
+
return redirect(`${base}/signin?out=1`, [clear(SESSION_COOKIE)]);
|
|
5261
|
+
}
|
|
5262
|
+
return settingsPost(d, v, form);
|
|
5263
|
+
}
|
|
5264
|
+
const now = d.clock.now();
|
|
5265
|
+
switch (route) {
|
|
5266
|
+
case "tasks":
|
|
5267
|
+
return htmlResponse(tasksPage(v, await loadTaskList(d, auth.user), now));
|
|
5268
|
+
case "history": {
|
|
5269
|
+
let id;
|
|
5270
|
+
try {
|
|
5271
|
+
id = decodeURIComponent(task?.[1] ?? "");
|
|
5272
|
+
} catch {
|
|
5273
|
+
return htmlResponse(notFoundPage(base, v), 404);
|
|
5274
|
+
}
|
|
5275
|
+
const view = await loadHistory(d, auth.user, id);
|
|
5276
|
+
return view ? htmlResponse(historyPage(v, view)) : htmlResponse(notFoundPage(base, v), 404);
|
|
5277
|
+
}
|
|
5278
|
+
default:
|
|
5279
|
+
return htmlResponse(settingsPage(v, { hour: String(auth.user.preferredHour), zone: auth.user.timeZone, saved: url.searchParams.get("saved") === "1" }));
|
|
5280
|
+
}
|
|
5281
|
+
};
|
|
5282
|
+
}
|
|
5283
|
+
|
|
5284
|
+
// plugins/tracker/src/web/config.ts
|
|
5285
|
+
var WEB_URL_FORMAT = "^https://[A-Za-z0-9.-]+(:[0-9]{1,5})?/?$";
|
|
5286
|
+
function parseWebUrl(raw) {
|
|
5287
|
+
if (raw === undefined || raw.trim() === "")
|
|
5288
|
+
return null;
|
|
5289
|
+
const value = raw.trim();
|
|
5290
|
+
if (!new RegExp(WEB_URL_FORMAT).test(value)) {
|
|
5291
|
+
throw new Error(`TRACKER_WEB_URL: "${value}" is not an https origin such as https://clerk.example.com (no path, query or credentials)`);
|
|
5292
|
+
}
|
|
5293
|
+
let url;
|
|
5294
|
+
try {
|
|
5295
|
+
url = new URL(value);
|
|
5296
|
+
} catch {
|
|
5297
|
+
throw new Error(`TRACKER_WEB_URL: "${value}" is not a URL`);
|
|
5298
|
+
}
|
|
5299
|
+
if (url.protocol !== "https:" || url.username !== "" || url.password !== "" || url.pathname !== "/" || url.search !== "" || url.hash !== "") {
|
|
5300
|
+
throw new Error(`TRACKER_WEB_URL: "${value}" must be a bare https origin`);
|
|
5301
|
+
}
|
|
5302
|
+
return url.origin;
|
|
5303
|
+
}
|
|
5304
|
+
|
|
5305
|
+
// plugins/tracker/src/index.ts
|
|
5306
|
+
var DB_DIR = "tracker";
|
|
5307
|
+
var DB_FILE = "tracker.sqlite";
|
|
5308
|
+
var TRACKER_TYPES = Object.freeze({
|
|
5309
|
+
reminder,
|
|
5310
|
+
renewal
|
|
5311
|
+
});
|
|
5312
|
+
var NO_DM = {
|
|
5313
|
+
async sendDm() {
|
|
5314
|
+
throw new Error("this bot has no host.dm (rackbops-discord-bot#736)");
|
|
5315
|
+
}
|
|
5316
|
+
};
|
|
5317
|
+
function createPlugin(host, options = {}) {
|
|
5318
|
+
const adminIds = parseAdminIds(host.env.TRACKER_ADMIN_DISCORD_IDS);
|
|
5319
|
+
const guildId = parseGuildId(host.env.TRACKER_GUILD_ID);
|
|
5320
|
+
const webOrigin = parseWebUrl(host.env.TRACKER_WEB_URL);
|
|
5321
|
+
const clock = options.clock ?? { now: () => new Date };
|
|
5322
|
+
const types = options.types ?? TRACKER_TYPES;
|
|
5323
|
+
const health = { activatedAt: null, lastTickAt: null, blocked: null };
|
|
5324
|
+
let db = null;
|
|
5325
|
+
let store = null;
|
|
5326
|
+
let claims = null;
|
|
5327
|
+
let delivery = null;
|
|
5328
|
+
let deps = null;
|
|
5329
|
+
let warnedNoDm = false;
|
|
5330
|
+
const dm = host.dm?.bind(host);
|
|
5331
|
+
const queue = serial();
|
|
5332
|
+
const surface = createSurface({
|
|
5333
|
+
deps: () => deps,
|
|
5334
|
+
guildId,
|
|
5335
|
+
log: host.log,
|
|
5336
|
+
queue,
|
|
5337
|
+
web: webOrigin ? { origin: webOrigin, name: host.name, gated: guildId !== null } : null,
|
|
5338
|
+
...options.membership ? { membership: options.membership } : {}
|
|
5339
|
+
});
|
|
5340
|
+
let discordClient = null;
|
|
5341
|
+
const capture = (interaction) => {
|
|
5342
|
+
const client = interaction.client;
|
|
5343
|
+
if (client)
|
|
5344
|
+
discordClient = client;
|
|
5345
|
+
};
|
|
5346
|
+
const clientMembership = async (discordId) => {
|
|
5347
|
+
if (!discordClient)
|
|
5348
|
+
return null;
|
|
5349
|
+
return lookupMembership({ guildId: null, user: { id: discordId }, client: discordClient }, guildId, discordId, host.log);
|
|
5350
|
+
};
|
|
5351
|
+
const web = createWebHandler({
|
|
5352
|
+
name: host.name,
|
|
5353
|
+
origin: webOrigin,
|
|
5354
|
+
deps: () => deps,
|
|
5355
|
+
queue,
|
|
5356
|
+
guildId,
|
|
5357
|
+
membership: options.webMembership ?? clientMembership
|
|
5358
|
+
});
|
|
5359
|
+
const interactions = surface.interactions;
|
|
5360
|
+
return {
|
|
5361
|
+
commands: surface.commands.map((c) => ({
|
|
5362
|
+
...c,
|
|
5363
|
+
handle: (interaction) => {
|
|
5364
|
+
capture(interaction);
|
|
5365
|
+
return c.handle(interaction);
|
|
5366
|
+
}
|
|
5367
|
+
})),
|
|
5368
|
+
interactions: (interaction) => {
|
|
5369
|
+
capture(interaction);
|
|
5370
|
+
return interactions(interaction);
|
|
5371
|
+
},
|
|
3530
5372
|
async activate() {
|
|
3531
5373
|
let path = options.dbPath;
|
|
3532
5374
|
if (path === undefined) {
|
|
@@ -3576,7 +5418,9 @@ function createPlugin(host, options = {}) {
|
|
|
3576
5418
|
types,
|
|
3577
5419
|
dm,
|
|
3578
5420
|
log: host.log,
|
|
3579
|
-
notifier: dm ? createDmNotifier({ store: openedStore, claims: openedClaims, dm, clock, log: host.log, health: delivery }) : NO_DM
|
|
5421
|
+
notifier: dm ? createDmNotifier({ store: openedStore, claims: openedClaims, dm, clock, log: host.log, health: delivery }) : NO_DM,
|
|
5422
|
+
logins: new LoginLinks(opened),
|
|
5423
|
+
sessions: new Sessions(opened)
|
|
3580
5424
|
};
|
|
3581
5425
|
if (guildId === null)
|
|
3582
5426
|
host.log.warn("TRACKER_GUILD_ID is unset: no membership gate, only the admission list");
|
|
@@ -3619,7 +5463,7 @@ function createPlugin(host, options = {}) {
|
|
|
3619
5463
|
if (info.path === "/healthz" && (request.method === "GET" || request.method === "HEAD")) {
|
|
3620
5464
|
return healthResponse(decideHealth(health, clock.now()));
|
|
3621
5465
|
}
|
|
3622
|
-
return
|
|
5466
|
+
return web(request, info);
|
|
3623
5467
|
}
|
|
3624
5468
|
};
|
|
3625
5469
|
}
|