slotlock 0.5.3 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +124 -5
- package/README.md +65 -51
- package/SECURITY.md +26 -13
- package/agent-server.d.ts +9 -0
- package/agent-server.js +29 -3
- package/application-api.js +6 -3
- package/dashboard.js +412 -207
- package/ddl.d.ts +20 -3
- package/ddl.js +164 -7
- package/event-feed.d.ts +25 -8
- package/event-feed.js +45 -14
- package/index.d.ts +1 -1
- package/openapi.json +27 -2
- package/package.json +1 -1
- package/rest-api.js +12 -0
- package/self-host.js +50 -2
- package/slotlock.manifest.json +1 -1
- package/store.d.ts +4 -1
- package/store.js +133 -21
- package/webhooks.d.ts +3 -2
- package/webhooks.js +6 -3
package/dashboard.js
CHANGED
|
@@ -380,6 +380,8 @@ const ICONS = Object.freeze({
|
|
|
380
380
|
assistant: icon('<path d="M4 5h16v11H9l-5 4z"/><path d="M8 9h8M8 12h5"/>'),
|
|
381
381
|
settings: icon('<path d="M4 7h10M18 7h2M4 17h4M12 17h8"/><circle cx="16" cy="7" r="2"/><circle cx="10" cy="17" r="2"/>'),
|
|
382
382
|
check: icon('<path d="M5 12l5 5L20 7"/>'),
|
|
383
|
+
signOut: icon('<path d="M15 4h3a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-3M10 17l-5-5 5-5M5 12h11"/>'),
|
|
384
|
+
plus: icon('<path d="M12 5v14M5 12h14"/>'),
|
|
383
385
|
github: new Html('<svg class="icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path class="fill" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>'),
|
|
384
386
|
});
|
|
385
387
|
/** How the overview names each change the feed records. */
|
|
@@ -508,6 +510,11 @@ function formatDate(date) {
|
|
|
508
510
|
return '—';
|
|
509
511
|
return `${date.toISOString().slice(0, 16).replace('T', ' ')} UTC`;
|
|
510
512
|
}
|
|
513
|
+
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
514
|
+
/** A table's date: the day (`19 Oct 2026`), with the full UTC time on hover and for machines. */
|
|
515
|
+
function dateCell(date) {
|
|
516
|
+
return html `<time datetime="${date.toISOString()}" title="${formatDate(date)}">${date.getUTCDate()} ${MONTHS[date.getUTCMonth()]} ${date.getUTCFullYear()}</time>`;
|
|
517
|
+
}
|
|
511
518
|
const WEEKDAY_NAMES = Object.freeze({
|
|
512
519
|
MO: 'Mon',
|
|
513
520
|
TU: 'Tue',
|
|
@@ -799,9 +806,10 @@ export function createSlotlockDashboard(options) {
|
|
|
799
806
|
const name = current?.login ?? session.login;
|
|
800
807
|
const badge = html `<span class="avatar" aria-hidden="true">${name.slice(0, 1).toUpperCase()}</span>`;
|
|
801
808
|
if (current && choices.length > 1) {
|
|
802
|
-
return html `<form class="workspace" method="post" action="${root}/organization">${badge}<input type="hidden" name="csrf" value="${csrfToken(session)}"><input type="hidden" name="once" value="${formNonce(session)}"><label class="visually-hidden" for="organization">Organization</label><select id="organization" name="organization">${choices.map((org) => html `<option value="${org.id}"${org.id === current.id ? html ` selected` : html ``}>${org.login}</option>`)}</select><button class="quiet" type="submit">Switch</button></form>`;
|
|
809
|
+
return html `<form class="workspace" method="post" action="${root}/organization">${badge}<input type="hidden" name="csrf" value="${csrfToken(session)}"><input type="hidden" name="once" value="${formNonce(session)}"><label class="visually-hidden" for="organization">Organization</label><select id="organization" name="organization" title="${current.login}">${choices.map((org) => html `<option value="${org.id}"${org.id === current.id ? html ` selected` : html ``}>${org.login}</option>`)}</select><button class="quiet" type="submit">Switch</button></form>`;
|
|
803
810
|
}
|
|
804
|
-
|
|
811
|
+
const shown = current ? name : `@${name}`;
|
|
812
|
+
return html `<div class="workspace">${badge}<span class="workspace-name" title="${shown}">${shown}</span></div>`;
|
|
805
813
|
}
|
|
806
814
|
function layout(title, content, session, section) {
|
|
807
815
|
const head = html `<!doctype html>
|
|
@@ -815,31 +823,39 @@ export function createSlotlockDashboard(options) {
|
|
|
815
823
|
<script src="${root}/app.js?v=${ASSET_VERSIONS.js}" defer></script>
|
|
816
824
|
</head>`;
|
|
817
825
|
const brand = html `<a class="brand" href="${root}">${ICONS.mark}<span class="name">${brandName}</span></a>`;
|
|
826
|
+
const skip = html `<a class="skip" href="#main">Skip to content</a>`;
|
|
827
|
+
// Copy buttons say what they did here, for screen readers (app.js).
|
|
828
|
+
const live = html `<p class="visually-hidden" id="live" aria-live="polite"></p>`;
|
|
818
829
|
if (!session) {
|
|
819
830
|
return html `${head}
|
|
820
831
|
<body class="bare">
|
|
832
|
+
${skip}
|
|
821
833
|
<header class="bare-bar">${brand}</header>
|
|
822
|
-
<main class="bare-main">${content}</main>
|
|
834
|
+
<main class="bare-main" id="main">${content}</main>
|
|
835
|
+
${live}
|
|
823
836
|
</body>
|
|
824
837
|
</html>`;
|
|
825
838
|
}
|
|
826
839
|
const links = nav().map((item) => html `<a href="${root}${item.path}"${item.section === section ? html ` aria-current="page"` : html ``}>${item.icon}<span>${item.label}</span></a>`);
|
|
827
840
|
return html `${head}
|
|
828
841
|
<body>
|
|
842
|
+
${skip}
|
|
829
843
|
<div class="app">
|
|
830
|
-
<
|
|
844
|
+
<header class="side">
|
|
831
845
|
<div class="side-top">${brand}${workspace(session)}</div>
|
|
832
846
|
<nav class="nav" aria-label="Dashboard">${links}</nav>
|
|
833
|
-
<div class="me"><span class="login">@${session.login}</span><form method="post" action="${root}/sign-out"><input type="hidden" name="csrf" value="${csrfToken(session)}"><button class="quiet" type="submit">Sign out</button></form></div>
|
|
834
|
-
</
|
|
835
|
-
<main class="page">${content}</main>
|
|
847
|
+
<div class="me"><span class="avatar me-avatar" aria-hidden="true">${session.login.slice(0, 1).toUpperCase()}</span><span class="login" title="@${session.login}">@${session.login}</span><form method="post" action="${root}/sign-out"><input type="hidden" name="csrf" value="${csrfToken(session)}"><button class="quiet sign-out" type="submit" title="Sign out">${ICONS.signOut}<span class="sign-out-label">Sign out</span></button></form></div>
|
|
848
|
+
</header>
|
|
849
|
+
<main class="page" id="main">${content}</main>
|
|
836
850
|
</div>
|
|
851
|
+
${live}
|
|
837
852
|
</body>
|
|
838
853
|
</html>`;
|
|
839
854
|
}
|
|
840
855
|
const noticeOf = (notice) => notice ? html `<p class="notice" role="alert">${notice}</p>` : html ``;
|
|
841
856
|
function signedOutPage(status, notice, cookies = []) {
|
|
842
857
|
const content = html `<section class="sign-in">
|
|
858
|
+
<span class="sign-in-mark">${ICONS.mark}</span>
|
|
843
859
|
<h1>Sign in to ${brandName}</h1>
|
|
844
860
|
<p>Create keys for your agents, add what they book, and watch what they do.</p>
|
|
845
861
|
${noticeOf(notice)}
|
|
@@ -861,19 +877,20 @@ ${noticeOf(notice)}
|
|
|
861
877
|
<form method="post" action="${root}/keys/revoke"><input type="hidden" name="csrf" value="${token}"><input type="hidden" name="once" value="${formNonce(session)}"><input type="hidden" name="id" value="${key.id}"><button class="danger" type="submit">Revoke key</button></form></div></details>`
|
|
862
878
|
: html ``;
|
|
863
879
|
return html `<tr>
|
|
864
|
-
<td data-label="Name" class="strong">${key.name}</td>
|
|
865
|
-
<td data-label="Key"><code>${key.prefix}…</code></td>
|
|
880
|
+
<td data-label="Name" class="strong"><span class="cell"><span class="truncate" title="${key.name}">${key.name}</span>${served ? html `<span class="meta">Created by ${key.createdBy === personOf(session) ? 'you' : key.createdBy?.startsWith('github:') ? `GitHub user ${key.createdBy.slice(7)}` : 'someone unknown'}</span>` : html ``}</span></td>
|
|
881
|
+
<td data-label="Key"><code class="chip">${key.prefix}…</code></td>
|
|
866
882
|
<td data-label="Access">${key.scopes.includes('admin') ? 'Full' : key.scopes.includes('write') ? 'Read and write' : 'Read only'}</td>
|
|
867
|
-
<td data-label="Created">${
|
|
868
|
-
|
|
869
|
-
<td data-label="
|
|
870
|
-
<td data-label="Expires">${key.expiresAt ? formatDate(key.expiresAt) : 'Never'}</td>
|
|
883
|
+
<td data-label="Created">${dateCell(key.createdAt)}</td>
|
|
884
|
+
<td data-label="Last used">${key.lastUsedAt ? dateCell(key.lastUsedAt) : html `<span class="muted">Never</span>`}</td>
|
|
885
|
+
<td data-label="Expires">${key.expiresAt ? dateCell(key.expiresAt) : html `<span class="muted">Never</span>`}</td>
|
|
871
886
|
<td data-label="Status"><span class="pill ${status.toLowerCase()}">${status}</span></td>
|
|
872
887
|
<td class="actions">${actions}</td>
|
|
873
888
|
</tr>`;
|
|
874
889
|
});
|
|
875
890
|
}
|
|
876
891
|
const pageHead = (title, action) => html `<header class="page-head"><h1>${title}</h1>${action ?? html ``}</header>`;
|
|
892
|
+
/** A list with nothing in it yet: what goes here, and the one thing to do next. */
|
|
893
|
+
const emptyState = (glyph, text, action) => html `<div class="empty-state"><span class="empty-icon">${glyph}</span><p class="empty">${text}</p>${action ? html `<a class="button" href="${action.href}">${ICONS.plus}${action.label}</a>` : html ``}</div>`;
|
|
877
894
|
/** A resource as people name it: its reference, else the start of its id. */
|
|
878
895
|
const resourceName = (resource, id) => resource?.externalRef ?? `${id.slice(0, 8)}…`;
|
|
879
896
|
function agendaPanel(entries, byId, more) {
|
|
@@ -900,7 +917,7 @@ ${served ? html `<td data-label="Created by">${key.createdBy === personOf(sessio
|
|
|
900
917
|
const pill = entry.kind === 'hold' && entry.expiresAt
|
|
901
918
|
? html `<span class="pill held">Held until ${clock(entry.expiresAt, zone)}</span>`
|
|
902
919
|
: html `<span class="pill ${entry.kind}">${entry.kind === 'event' ? 'Event' : 'Booked'}</span>`;
|
|
903
|
-
group.rows.push(html `<li class="slot"><span class="when">${clock(entry.start, zone)}<span class="until">${clock(entry.end, zone)}</span></span><span class="what"><span class="title">${title}</span>${meta.length > 0 ? html `<span class="meta">${meta.join(' · ')}</span>` : html ``}</span>${pill}</li>`);
|
|
920
|
+
group.rows.push(html `<li class="slot"><span class="when">${clock(entry.start, zone)}<span class="until">${clock(entry.end, zone)}</span></span><span class="what" title="${[title, ...meta].join(' · ')}"><span class="title">${title}</span>${meta.length > 0 ? html `<span class="meta">${meta.join(' · ')}</span>` : html ``}</span>${pill}</li>`);
|
|
904
921
|
groups.set(day.key, group);
|
|
905
922
|
}
|
|
906
923
|
return html `${[...groups.values()].map((group) => html `<h3 class="day">${group.label}</h3><ul class="slots">${group.rows}</ul>`)}${more ? html `<p class="fine">Showing the first ${String(AGENDA_SHOWN)}.</p>` : html ``}`;
|
|
@@ -929,7 +946,7 @@ ${served ? html `<td data-label="Created by">${key.createdBy === personOf(sessio
|
|
|
929
946
|
const where = change.resourceId
|
|
930
947
|
? resourceName(byId.get(change.resourceId), change.resourceId)
|
|
931
948
|
: null;
|
|
932
|
-
return html `<li class="event"><span class="dot ${label.tone}" aria-hidden="true"></span><span class="what"><span class="title">${label.text}${where ? html ` <span class="on">on</span> ${where}` : html ``}</span><span class="meta">${who} · <time datetime="${change.occurredAt.toISOString()}" title="${formatDate(change.occurredAt)}">${ago(change.occurredAt, now())}</time></span></span></li>`;
|
|
949
|
+
return html `<li class="event"><span class="dot ${label.tone}" aria-hidden="true"></span><span class="what" title="${where ? `${label.text} on ${where}` : label.text}"><span class="title">${label.text}${where ? html ` <span class="on">on</span> ${where}` : html ``}</span><span class="meta">${who} · <time datetime="${change.occurredAt.toISOString()}" title="${formatDate(change.occurredAt)}">${ago(change.occurredAt, now())}</time></span></span></li>`;
|
|
933
950
|
})}</ol>`;
|
|
934
951
|
}
|
|
935
952
|
async function overviewPage(session, status = 200, notice) {
|
|
@@ -983,23 +1000,20 @@ ${pageHead('API keys')}
|
|
|
983
1000
|
<p>Agents call ${brandName} over MCP or A2A with a key: <code>Authorization: Bearer slk_…</code></p>
|
|
984
1001
|
${endpoints()}
|
|
985
1002
|
</section>
|
|
986
|
-
<section class="panel">
|
|
1003
|
+
<section class="panel" id="new-key">
|
|
987
1004
|
<h2>New key</h2>
|
|
988
1005
|
<form class="create" method="post" action="${root}/keys">
|
|
989
1006
|
<input type="hidden" name="csrf" value="${token}">
|
|
990
1007
|
<input type="hidden" name="once" value="${formNonce(session)}">
|
|
991
|
-
<label>Name<input name="name" required maxlength="100" placeholder="Booking agent"></label>
|
|
1008
|
+
<label class="grow">Name<input id="key-name" name="name" required maxlength="100" placeholder="Booking agent"></label>
|
|
992
1009
|
<label>Access<select name="access"><option value="read_write">Read and write</option><option value="read">Read only</option><option value="full">Full, with the application API</option></select></label>
|
|
993
1010
|
<label>Expires<select name="expires"><option value="90">In 90 days</option><option value="30">In 30 days</option><option value="365">In a year</option><option value="never">Never</option></select></label>
|
|
994
1011
|
<button class="primary" type="submit">Create key</button>
|
|
995
1012
|
</form>
|
|
996
1013
|
</section>
|
|
997
1014
|
${keys.length === 0
|
|
998
|
-
?
|
|
999
|
-
: served
|
|
1000
|
-
? html `<p class="fine">Each key acts in this organization only, and every member can rotate or revoke it.</p><div class="table"><table><thead><tr><th>Name</th><th>Key</th><th>Access</th><th>Created</th><th>Created by</th><th>Last used</th><th>Expires</th><th>Status</th><th><span class="visually-hidden">Actions</span></th></tr></thead>
|
|
1001
|
-
<tbody>${keyRows(keys, token, session)}</tbody></table></div>`
|
|
1002
|
-
: html `<div class="table"><table><thead><tr><th>Name</th><th>Key</th><th>Access</th><th>Created</th><th>Last used</th><th>Expires</th><th>Status</th><th><span class="visually-hidden">Actions</span></th></tr></thead>
|
|
1015
|
+
? emptyState(ICONS.keys, 'No keys yet. Give every agent its own, with the least access it needs.', { href: '#key-name', label: 'Create a key' })
|
|
1016
|
+
: html `${served ? html `<p class="fine">Each key acts in this organization only, and every member can rotate or revoke it.</p>` : html ``}<div class="table"><table><thead><tr><th>Name</th><th>Key</th><th>Access</th><th>Created</th><th>Last used</th><th>Expires</th><th>Status</th><th><span class="visually-hidden">Actions</span></th></tr></thead>
|
|
1003
1017
|
<tbody>${keyRows(keys, token, session)}</tbody></table></div>`}`;
|
|
1004
1018
|
return respond(status, layout('API keys', content, session, 'keys'));
|
|
1005
1019
|
}
|
|
@@ -1008,20 +1022,20 @@ ${keys.length === 0
|
|
|
1008
1022
|
const token = csrfToken(session);
|
|
1009
1023
|
const content = html `${noticeOf(notice)}
|
|
1010
1024
|
${pageHead('Resources', resources.length > 0 ? html `<a class="button" href="${root}/hours">Set hours for every resource</a>` : undefined)}
|
|
1011
|
-
<section class="panel">
|
|
1025
|
+
<section class="panel" id="add-resource">
|
|
1012
1026
|
<h2>Add a resource</h2>
|
|
1013
1027
|
<form class="create" method="post" action="${root}/resources">
|
|
1014
1028
|
<input type="hidden" name="csrf" value="${token}">
|
|
1015
1029
|
<input type="hidden" name="once" value="${formNonce(session)}">
|
|
1016
|
-
<label>Reference<input name="reference" required maxlength="200" placeholder="vehicle-42"></label>
|
|
1030
|
+
<label class="grow">Reference<input id="reference" name="reference" required maxlength="200" placeholder="vehicle-42"></label>
|
|
1017
1031
|
<label>Time zone<input name="timezone" required maxlength="64" value="UTC" placeholder="Europe/London"></label>
|
|
1018
1032
|
<button class="primary" type="submit">Add resource</button>
|
|
1019
1033
|
</form>
|
|
1020
1034
|
<p class="fine">${String(resources.length)} of ${String(SLOTLOCK_DASHBOARD_MAX_RESOURCES)}. Adding a reference you have changes its time zone.</p>
|
|
1021
1035
|
</section>
|
|
1022
1036
|
${resources.length === 0
|
|
1023
|
-
?
|
|
1024
|
-
: html `<div class="table"><table><thead><tr><th>Reference</th><th>Time zone</th><th>Bookable hours</th><th>Resource id</th></tr></thead><tbody>${resources.map((resource) => html `<tr><td data-label="Reference" class="strong"><span class="cell"><a href="${root}/resources/${resource.id}">${resourceName(resource, resource.id)}</a>${resource.pool ? html `<span class="meta">Pool ${resource.pool}</span>` : html ``}</span></td><td data-label="Time zone">${resource.timezone}</td><td data-label="Bookable hours">${hoursSummary(resource.availabilityRules)}
|
|
1037
|
+
? emptyState(ICONS.resources, 'No resources yet. Agents can only book what is listed here.', { href: '#reference', label: 'Add a resource' })
|
|
1038
|
+
: html `<div class="table"><table><thead><tr><th>Reference</th><th>Time zone</th><th>Bookable hours</th><th>Resource id</th></tr></thead><tbody>${resources.map((resource) => html `<tr><td data-label="Reference" class="strong"><span class="cell"><a class="truncate" href="${root}/resources/${resource.id}" title="${resourceName(resource, resource.id)}">${resourceName(resource, resource.id)}</a>${resource.pool ? html `<span class="meta" title="Pool ${resource.pool}">Pool ${resource.pool}</span>` : html ``}</span></td><td data-label="Time zone">${resource.timezone}</td><td data-label="Bookable hours"><span class="cell"><span>${hoursSummary(resource.availabilityRules)}</span><a class="meta-link" href="${root}/hours?resource=${resource.id}">Set hours</a></span></td><td data-label="Resource id"><code class="muted truncate id" title="${resource.id}">${resource.id}</code></td></tr>`)}</tbody></table></div>`}`;
|
|
1025
1039
|
return respond(status, layout('Resources', content, session, 'resources'));
|
|
1026
1040
|
}
|
|
1027
1041
|
async function settingsPage(session, status = 200, notice) {
|
|
@@ -1212,10 +1226,10 @@ ${activity ? html `<section class="panel"><h2>Activity</h2>${activityPanel(activ
|
|
|
1212
1226
|
? `Since ${formatDate(endpoint.failingSince)}, ${String(endpoint.attempts)} attempts${endpoint.lastStatus ? `, last HTTP ${endpoint.lastStatus}` : ''}${endpoint.lastError ? `: ${endpoint.lastError.slice(0, 120)}` : ''}`
|
|
1213
1227
|
: '';
|
|
1214
1228
|
return html `<tr>
|
|
1215
|
-
<td data-label="URL" class="strong
|
|
1216
|
-
<td data-label="Events">${endpoint.eventTypes ? endpoint.eventTypes.join(', ') : 'All'}</td>
|
|
1229
|
+
<td data-label="URL" class="strong"><code class="truncate url" title="${endpoint.url}">${endpoint.url}</code></td>
|
|
1230
|
+
<td data-label="Events" class="wrap">${endpoint.eventTypes ? endpoint.eventTypes.join(', ') : 'All'}</td>
|
|
1217
1231
|
<td data-label="Status"><span class="cell"><span class="pill ${state.tone}">${state.text}</span>${problem ? html `<span class="meta wrap">${problem}</span>` : html ``}</span></td>
|
|
1218
|
-
<td data-label="Added">${
|
|
1232
|
+
<td data-label="Added">${dateCell(endpoint.createdAt)}</td>
|
|
1219
1233
|
<td class="actions">${endpoint.disabledAt
|
|
1220
1234
|
? html `<form class="inline" method="post" action="${root}/webhooks/enable"><input type="hidden" name="csrf" value="${token}"><input type="hidden" name="once" value="${formNonce(session)}"><input type="hidden" name="id" value="${endpoint.id}"><button type="submit">Enable</button></form>`
|
|
1221
1235
|
: html ``}<details class="act danger"><summary>Delete</summary><div class="pop"><p>Nothing more is sent to it. The feed keeps its changes.</p>
|
|
@@ -1229,19 +1243,19 @@ ${pageHead('Webhooks')}
|
|
|
1229
1243
|
<div class="stat"><span class="stat-label">Failing</span><span class="stat-value">${String(failing.length)}</span></div>
|
|
1230
1244
|
<div class="stat"><span class="stat-label">Disabled</span><span class="stat-value">${String(disabled.length)}</span></div>
|
|
1231
1245
|
</div>
|
|
1232
|
-
<section class="panel">
|
|
1246
|
+
<section class="panel" id="add-endpoint">
|
|
1233
1247
|
<h2>Add an endpoint</h2>
|
|
1234
1248
|
<form method="post" action="${root}/webhooks">
|
|
1235
1249
|
<input type="hidden" name="csrf" value="${token}">
|
|
1236
1250
|
<input type="hidden" name="once" value="${formNonce(session)}">
|
|
1237
|
-
<label>URL<input name="url" type="url" required maxlength="2048" placeholder="https://example.com/slotlock"></label>
|
|
1251
|
+
<label>URL<input id="webhook-url" name="url" type="url" required maxlength="2048" placeholder="https://example.com/slotlock"></label>
|
|
1238
1252
|
<details class="types"><summary>Events: all, or choose</summary><div class="checks">${SLOTLOCK_CHANGE_TYPES.map((type) => html `<label class="choice"><input type="checkbox" name="event_type" value="${type}"> <code>${type}</code></label>`)}</div></details>
|
|
1239
1253
|
<button class="primary" type="submit">Add endpoint</button>
|
|
1240
1254
|
</form>
|
|
1241
1255
|
<p class="fine">Signed as Standard Webhooks with a secret shown once. An endpoint failing for a day is disabled.</p>
|
|
1242
1256
|
</section>
|
|
1243
1257
|
${endpoints.length === 0
|
|
1244
|
-
?
|
|
1258
|
+
? emptyState(ICONS.webhooks, 'No webhooks. Add one to hear about bookings as they change.', { href: '#webhook-url', label: 'Add an endpoint' })
|
|
1245
1259
|
: html `<div class="table"><table><thead><tr><th>URL</th><th>Events</th><th>Status</th><th>Added</th><th><span class="visually-hidden">Actions</span></th></tr></thead><tbody>${rows}</tbody></table></div>`}`;
|
|
1246
1260
|
return respond(status, layout('Webhooks', content, session, 'webhooks'));
|
|
1247
1261
|
}
|
|
@@ -1312,9 +1326,9 @@ ${endpoints.length === 0
|
|
|
1312
1326
|
const tone = entry.status === 'offered' ? 'active' : entry.status === 'expired' ? 'revoked' : 'held';
|
|
1313
1327
|
return html `<tr>
|
|
1314
1328
|
<td data-label="When" class="strong">${day.label} ${clock(entry.start, zone)}–${clock(entry.end, zone)}</td>
|
|
1315
|
-
<td data-label="For">${what}</td>
|
|
1316
|
-
<td data-label="Holder"><code>${entry.holderRef}</code></td>
|
|
1317
|
-
<td data-label="Status"><span class="cell"><span class="pill ${tone}">${entry.status === 'offered' ? 'Offered' : entry.status === 'expired' ? 'Expired' : 'Waiting'}</span>${offered && entry.offeredAt ? html `<span class="meta">${offered}, ${ago(entry.offeredAt, now())}</span>` : html ``}</span></td>
|
|
1329
|
+
<td data-label="For"><span class="truncate" title="${what}">${what}</span></td>
|
|
1330
|
+
<td data-label="Holder"><code class="truncate" title="${entry.holderRef}">${entry.holderRef}</code></td>
|
|
1331
|
+
<td data-label="Status"><span class="cell"><span class="pill ${tone}">${entry.status === 'offered' ? 'Offered' : entry.status === 'expired' ? 'Expired' : 'Waiting'}</span>${offered && entry.offeredAt ? html `<span class="meta" title="${offered}">${offered}, ${ago(entry.offeredAt, now())}</span>` : html ``}</span></td>
|
|
1318
1332
|
<td data-label="Joined">${ago(entry.createdAt, now())}</td>
|
|
1319
1333
|
<td class="actions"><details class="act danger"><summary>Remove</summary><div class="pop"><p>${entry.status === 'offered' ? 'Its offered hold is released and offered to the next.' : 'It stops waiting.'}</p>
|
|
1320
1334
|
<form method="post" action="${root}/waitlist/leave"><input type="hidden" name="csrf" value="${token}"><input type="hidden" name="once" value="${formNonce(session)}"><input type="hidden" name="id" value="${entry.id}"><button class="danger" type="submit">Remove entry</button></form></div></details></td>
|
|
@@ -1329,7 +1343,7 @@ ${pageHead('Waitlist')}
|
|
|
1329
1343
|
<div class="stat"><span class="stat-label">Offered</span><span class="stat-value">${String(offered)}</span></div>
|
|
1330
1344
|
</div>
|
|
1331
1345
|
${entries.length === 0
|
|
1332
|
-
?
|
|
1346
|
+
? emptyState(ICONS.waitlist, 'Nobody is waiting. Agents add entries when the time they want is taken.')
|
|
1333
1347
|
: html `<div class="table"><table><thead><tr><th>When</th><th>For</th><th>Holder</th><th>Status</th><th>Joined</th><th><span class="visually-hidden">Actions</span></th></tr></thead><tbody>${rows}</tbody></table></div>${entries.length >= WAITLIST_SHOWN ? html `<p class="fine">Showing the first ${String(WAITLIST_SHOWN)}.</p>` : html ``}`}`;
|
|
1334
1348
|
return respond(status, layout('Waitlist', content, session, 'waitlist'));
|
|
1335
1349
|
}
|
|
@@ -2507,226 +2521,417 @@ ${org ? html `<p>For the organization <strong>${org.login}</strong>.</p>` : html
|
|
|
2507
2521
|
},
|
|
2508
2522
|
};
|
|
2509
2523
|
}
|
|
2510
|
-
//
|
|
2511
|
-
|
|
2512
|
-
|
|
2524
|
+
// Progressive enhancement only, loaded as a file so the policy needs no inline code: copy buttons
|
|
2525
|
+
// (announced in the page's live region), confirmations that close with Escape or a click outside
|
|
2526
|
+
// and put focus on their button, and, on a phone, the current page scrolled into the tab strip.
|
|
2527
|
+
const DASHBOARD_JS = `const live = document.getElementById('live');
|
|
2528
|
+
const say = (text) => {
|
|
2529
|
+
if (!live) return;
|
|
2530
|
+
live.textContent = '';
|
|
2531
|
+
setTimeout(() => { live.textContent = text; }, 50);
|
|
2532
|
+
};
|
|
2533
|
+
document.addEventListener('click', (event) => {
|
|
2534
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
2535
|
+
for (const open of document.querySelectorAll('details.act[open]')) {
|
|
2536
|
+
if (!target || !open.contains(target)) open.removeAttribute('open');
|
|
2537
|
+
}
|
|
2538
|
+
const button = target ? target.closest('[data-copy]') : null;
|
|
2513
2539
|
if (!button) return;
|
|
2514
2540
|
const field = document.getElementById(button.getAttribute('data-copy'));
|
|
2515
2541
|
if (!field) return;
|
|
2542
|
+
const label = button.textContent;
|
|
2543
|
+
const done = (text) => {
|
|
2544
|
+
button.textContent = text;
|
|
2545
|
+
say(text);
|
|
2546
|
+
setTimeout(() => { button.textContent = label; }, 2000);
|
|
2547
|
+
};
|
|
2516
2548
|
const selectIt = () => {
|
|
2517
2549
|
if (typeof field.select === 'function') field.select();
|
|
2518
|
-
|
|
2550
|
+
done('Selected: press Ctrl+C or Cmd+C');
|
|
2519
2551
|
};
|
|
2520
2552
|
if (!navigator.clipboard) return selectIt();
|
|
2521
|
-
navigator.clipboard.writeText(field.value ?? field.textContent ?? '').then(() =>
|
|
2522
|
-
|
|
2523
|
-
|
|
2553
|
+
navigator.clipboard.writeText(field.value ?? field.textContent ?? '').then(() => done('Copied'), selectIt);
|
|
2554
|
+
});
|
|
2555
|
+
document.addEventListener('keydown', (event) => {
|
|
2556
|
+
if (event.key !== 'Escape') return;
|
|
2557
|
+
for (const open of document.querySelectorAll('details.act[open]')) {
|
|
2558
|
+
open.removeAttribute('open');
|
|
2559
|
+
const summary = open.querySelector('summary');
|
|
2560
|
+
if (summary) summary.focus();
|
|
2561
|
+
}
|
|
2524
2562
|
});
|
|
2563
|
+
document.addEventListener('toggle', (event) => {
|
|
2564
|
+
const details = event.target;
|
|
2565
|
+
if (!(details instanceof HTMLDetailsElement) || !details.classList.contains('act') || !details.open) return;
|
|
2566
|
+
const confirm = details.querySelector('.pop button[type=submit]');
|
|
2567
|
+
if (confirm) confirm.focus();
|
|
2568
|
+
}, true);
|
|
2569
|
+
const current = document.querySelector('.nav a[aria-current="page"]');
|
|
2570
|
+
if (current && window.matchMedia('(max-width: 760px)').matches) {
|
|
2571
|
+
current.scrollIntoView({ block: 'nearest', inline: 'center' });
|
|
2572
|
+
}
|
|
2525
2573
|
`;
|
|
2526
2574
|
const DASHBOARD_CSS = `:root {
|
|
2527
|
-
--
|
|
2528
|
-
--
|
|
2529
|
-
--
|
|
2530
|
-
--
|
|
2531
|
-
--
|
|
2532
|
-
--
|
|
2533
|
-
|
|
2575
|
+
--font: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
2576
|
+
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
|
|
2577
|
+
--text-xs: 11px; --text-sm: 12.5px; --text-md: 13.5px; --text-lg: 15px; --text-xl: 22px; --text-2xl: 28px;
|
|
2578
|
+
--space-1: 4px; --space-2: 8px; --space-3: 12px; --space-4: 16px; --space-5: 20px; --space-6: 24px; --space-8: 32px; --space-12: 48px;
|
|
2579
|
+
--radius-sm: 6px; --radius: 8px; --radius-lg: 12px; --radius-full: 999px;
|
|
2580
|
+
--ease: cubic-bezier(0.2, 0, 0, 1); --fast: 120ms; --slow: 180ms;
|
|
2581
|
+
--canvas: #f7f7f8; --surface: #ffffff; --surface-2: #fafafa; --sunken: #f2f2f4; --hover: #f0f0f2;
|
|
2582
|
+
--line: #e6e6e9; --line-strong: #d4d4d8; --line-soft: #efeff1;
|
|
2583
|
+
--ink: #18181b; --ink-2: #52525b; --ink-3: #6b6b74;
|
|
2584
|
+
--accent: #2456e6; --accent-wash: #eef2fe; --ring: rgba(36, 86, 230, 0.28);
|
|
2585
|
+
--button: #18181b; --button-ink: #ffffff; --button-hover: #2e2e33;
|
|
2586
|
+
--good: #11764c; --good-wash: #e9f6ef; --bad: #c42b1c; --bad-ink: #ffffff; --bad-wash: #fdeeec; --bad-line: #f5c9c3;
|
|
2587
|
+
--warn: #8a5300; --warn-wash: #fff5e1; --warn-line: #f3dcae;
|
|
2588
|
+
--brand: #ff5b2e;
|
|
2589
|
+
--pop-shadow: 0 1px 2px rgba(24, 24, 27, 0.06), 0 12px 32px -8px rgba(24, 24, 27, 0.18);
|
|
2590
|
+
color-scheme: light;
|
|
2534
2591
|
}
|
|
2535
2592
|
@media (prefers-color-scheme: dark) {
|
|
2536
|
-
:root {
|
|
2537
|
-
--
|
|
2538
|
-
--
|
|
2539
|
-
--
|
|
2540
|
-
--
|
|
2541
|
-
--
|
|
2593
|
+
:root {
|
|
2594
|
+
--canvas: #0b0b0d; --surface: #111114; --surface-2: #141418; --sunken: #18181c; --hover: #1c1c21;
|
|
2595
|
+
--line: #26262c; --line-strong: #34343b; --line-soft: #1d1d22;
|
|
2596
|
+
--ink: #ededf0; --ink-2: #b4b4bc; --ink-3: #8e8e98;
|
|
2597
|
+
--accent: #7ea2ff; --accent-wash: #18203a; --ring: rgba(126, 162, 255, 0.35);
|
|
2598
|
+
--button: #ededf0; --button-ink: #0b0b0d; --button-hover: #ffffff;
|
|
2599
|
+
--good: #4fca8f; --good-wash: #0f2419; --bad: #ff7a6b; --bad-ink: #1a0806; --bad-wash: #2c1512; --bad-line: #5a2a24;
|
|
2600
|
+
--warn: #f4c25a; --warn-wash: #2a2210; --warn-line: #4d3d17;
|
|
2601
|
+
--pop-shadow: 0 0 0 1px #26262c, 0 16px 40px -8px rgba(0, 0, 0, 0.7);
|
|
2602
|
+
color-scheme: dark;
|
|
2603
|
+
}
|
|
2542
2604
|
}
|
|
2543
|
-
|
|
2544
|
-
html { -webkit-text-size-adjust: 100%; }
|
|
2545
|
-
body { margin: 0; background: var(--
|
|
2546
|
-
a { color: var(--accent); text-decoration: none; }
|
|
2605
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
2606
|
+
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
|
|
2607
|
+
body { margin: 0; background: var(--canvas); color: var(--ink); font: 400 14px/1.5 var(--font); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-rendering: optimizeLegibility; }
|
|
2608
|
+
a { color: var(--accent); text-decoration: none; text-underline-offset: 2px; }
|
|
2547
2609
|
a:hover { text-decoration: underline; }
|
|
2548
|
-
code, pre, input[readonly] { font-family:
|
|
2610
|
+
code, pre, kbd, input[readonly] { font-family: var(--mono); font-size: 12.5px; }
|
|
2611
|
+
::selection { background: var(--ring); }
|
|
2612
|
+
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
|
|
2549
2613
|
.icon { width: 16px; height: 16px; flex: none; fill: none; stroke: currentColor; stroke-width: 1.75; stroke-linecap: round; stroke-linejoin: round; }
|
|
2550
2614
|
.icon .fill { fill: currentColor; stroke: none; }
|
|
2551
2615
|
.mark { width: 20px; height: 18px; flex: none; }
|
|
2552
2616
|
.mark-ink { fill: currentColor; }
|
|
2553
|
-
.mark-slot { fill:
|
|
2554
|
-
.
|
|
2617
|
+
.mark-slot { fill: var(--brand); }
|
|
2618
|
+
.skip { position: absolute; left: var(--space-3); top: -48px; z-index: 10; padding: var(--space-2) var(--space-3); border-radius: var(--radius); background: var(--button); color: var(--button-ink); font-weight: 600; }
|
|
2619
|
+
.skip:focus { top: var(--space-3); text-decoration: none; }
|
|
2620
|
+
.visually-hidden { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
|
|
2621
|
+
|
|
2622
|
+
/* Shell */
|
|
2623
|
+
.brand { display: inline-flex; align-items: center; gap: 9px; min-width: 0; color: var(--ink); font-weight: 600; font-size: var(--text-lg); letter-spacing: -0.015em; }
|
|
2555
2624
|
.brand:hover { text-decoration: none; }
|
|
2556
|
-
.
|
|
2557
|
-
.
|
|
2558
|
-
.side
|
|
2559
|
-
.
|
|
2560
|
-
.workspace
|
|
2561
|
-
.workspace
|
|
2562
|
-
.
|
|
2563
|
-
.
|
|
2564
|
-
.
|
|
2625
|
+
.brand .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2626
|
+
.app { display: grid; grid-template-columns: 240px minmax(0, 1fr); min-height: 100vh; }
|
|
2627
|
+
.side { position: sticky; top: 0; height: 100vh; display: flex; flex-direction: column; gap: var(--space-5); padding: var(--space-4) var(--space-3); background: var(--surface-2); border-right: 1px solid var(--line); min-width: 0; }
|
|
2628
|
+
.side-top { display: grid; gap: var(--space-4); padding: var(--space-1) var(--space-2) 0; min-width: 0; }
|
|
2629
|
+
.workspace { display: flex; align-items: center; gap: var(--space-2); margin: 0; padding: 5px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); min-width: 0; }
|
|
2630
|
+
.workspace-name { font-weight: 600; font-size: var(--text-md); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2631
|
+
.workspace select { flex: 1; min-width: 0; height: 26px; border: 0; padding: 0 18px 0 2px; font-weight: 600; font-size: var(--text-md); background-color: transparent; box-shadow: none; text-overflow: ellipsis; }
|
|
2632
|
+
.workspace select:focus { box-shadow: none; }
|
|
2633
|
+
.workspace:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--ring); }
|
|
2634
|
+
.avatar { display: inline-grid; place-items: center; width: 24px; height: 24px; flex: none; border-radius: var(--radius-sm); background: var(--ink); color: var(--surface); font-size: 11px; font-weight: 700; }
|
|
2635
|
+
.nav { display: grid; gap: 1px; }
|
|
2636
|
+
.nav a { position: relative; display: flex; align-items: center; gap: 10px; height: 34px; padding: 0 10px; border-radius: var(--radius); color: var(--ink-2); font-weight: 500; font-size: var(--text-md); transition: background-color var(--fast) var(--ease), color var(--fast) var(--ease); }
|
|
2637
|
+
.nav a .icon { color: var(--ink-3); transition: color var(--fast) var(--ease); }
|
|
2565
2638
|
.nav a:hover { background: var(--hover); color: var(--ink); text-decoration: none; }
|
|
2566
|
-
.nav a
|
|
2567
|
-
.nav a[aria-current="page"]
|
|
2568
|
-
.
|
|
2569
|
-
.me
|
|
2570
|
-
.
|
|
2571
|
-
.
|
|
2572
|
-
.
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2639
|
+
.nav a:hover .icon { color: var(--ink-2); }
|
|
2640
|
+
.nav a[aria-current="page"] { background: var(--surface); color: var(--ink); font-weight: 600; box-shadow: inset 0 0 0 1px var(--line); }
|
|
2641
|
+
.nav a[aria-current="page"] .icon { color: var(--ink); }
|
|
2642
|
+
.me { margin-top: auto; display: flex; align-items: center; gap: var(--space-2); padding: var(--space-3) var(--space-2) 0; border-top: 1px solid var(--line); color: var(--ink-2); font-size: var(--text-md); min-width: 0; }
|
|
2643
|
+
.me-avatar { width: 22px; height: 22px; border-radius: var(--radius-full); background: var(--sunken); color: var(--ink-2); box-shadow: inset 0 0 0 1px var(--line); }
|
|
2644
|
+
.me form { margin: 0 0 0 auto; }
|
|
2645
|
+
.login { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
|
|
2646
|
+
.sign-out .icon { width: 15px; height: 15px; }
|
|
2647
|
+
.page { width: 100%; max-width: 1200px; padding: var(--space-8) var(--space-8) 72px; display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-5); align-content: start; min-width: 0; }
|
|
2648
|
+
.page-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); flex-wrap: wrap; min-height: 36px; margin-bottom: var(--space-1); }
|
|
2649
|
+
|
|
2650
|
+
/* Type */
|
|
2651
|
+
h1 { font-size: var(--text-xl); line-height: 1.25; margin: 0; letter-spacing: -0.022em; font-weight: 600; overflow-wrap: anywhere; }
|
|
2652
|
+
h2 { font-size: var(--text-lg); line-height: 1.35; margin: 0 0 var(--space-4); font-weight: 600; letter-spacing: -0.01em; }
|
|
2653
|
+
h3 { font: 500 var(--text-xs)/1.4 var(--mono); margin: 0; color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.06em; }
|
|
2576
2654
|
p { margin: 0 0 10px; color: var(--ink-2); }
|
|
2577
|
-
.fine { font-size:
|
|
2655
|
+
.fine { font-size: var(--text-sm); color: var(--ink-3); margin: var(--space-3) 0 0; }
|
|
2578
2656
|
.muted { color: var(--ink-3); }
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
.
|
|
2583
|
-
.
|
|
2584
|
-
.
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
.
|
|
2588
|
-
.
|
|
2589
|
-
.
|
|
2590
|
-
.
|
|
2657
|
+
strong { color: var(--ink); font-weight: 600; }
|
|
2658
|
+
|
|
2659
|
+
/* Surfaces */
|
|
2660
|
+
.panel, .card { border: 1px solid var(--line); border-radius: var(--radius-lg); padding: var(--space-5) var(--space-6); background: var(--surface); min-width: 0; }
|
|
2661
|
+
.stack { display: grid; gap: var(--space-5); align-content: start; min-width: 0; }
|
|
2662
|
+
.split { display: grid; grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); gap: var(--space-5); align-items: start; }
|
|
2663
|
+
|
|
2664
|
+
/* Stats */
|
|
2665
|
+
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 0; border: 1px solid var(--line); border-radius: var(--radius-lg); background: var(--surface); overflow: hidden; }
|
|
2666
|
+
.stat { position: relative; display: grid; gap: 6px; padding: var(--space-4) var(--space-5) 18px; color: var(--ink); box-shadow: -1px 0 0 var(--line); transition: background-color var(--fast) var(--ease); }
|
|
2667
|
+
a.stat:hover { background: var(--surface-2); text-decoration: none; }
|
|
2668
|
+
a.stat::after { content: ""; position: absolute; top: 18px; right: 18px; width: 6px; height: 6px; border-top: 1.5px solid var(--ink-3); border-right: 1.5px solid var(--ink-3); transform: rotate(45deg); opacity: 0; transition: opacity var(--fast) var(--ease); }
|
|
2669
|
+
a.stat:hover::after, a.stat:focus-visible::after { opacity: 1; }
|
|
2670
|
+
a.stat:focus-visible { outline-offset: -2px; border-radius: 0; }
|
|
2671
|
+
.stat-label { font: 500 var(--text-xs)/1.4 var(--mono); color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.06em; }
|
|
2672
|
+
.stat-value { font-size: var(--text-2xl); line-height: 1.1; font-weight: 500; letter-spacing: -0.03em; font-variant-numeric: tabular-nums; }
|
|
2673
|
+
|
|
2674
|
+
/* Setup checklist */
|
|
2675
|
+
.setup .steps { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; counter-reset: step; }
|
|
2676
|
+
.steps li { display: flex; align-items: center; gap: var(--space-3); min-height: 36px; color: var(--ink); font-weight: 500; }
|
|
2677
|
+
.steps li.done { color: var(--ink-3); font-weight: 400; text-decoration: line-through; text-decoration-color: var(--line-strong); }
|
|
2678
|
+
.check { display: inline-grid; place-items: center; width: 20px; height: 20px; flex: none; border-radius: var(--radius-full); border: 1.5px dashed var(--line-strong); color: var(--surface); }
|
|
2679
|
+
.done .check { background: var(--good); border: 0; }
|
|
2591
2680
|
.check .icon { width: 12px; height: 12px; stroke-width: 3; }
|
|
2592
|
-
|
|
2681
|
+
|
|
2682
|
+
/* Agenda and activity */
|
|
2683
|
+
.day { margin: var(--space-5) 0 var(--space-1); }
|
|
2593
2684
|
.day:first-of-type { margin-top: 0; }
|
|
2594
2685
|
.slots, .feed { list-style: none; margin: 0; padding: 0; }
|
|
2595
|
-
.slot { display: grid; grid-template-columns:
|
|
2686
|
+
.slot { display: grid; grid-template-columns: 52px minmax(0, 1fr) auto; gap: var(--space-3); align-items: center; padding: 10px 0; border-bottom: 1px solid var(--line-soft); }
|
|
2596
2687
|
.slot:last-child { border-bottom: 0; }
|
|
2597
|
-
.when { display: grid; font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
2598
|
-
.until { font-weight: 400; font-size:
|
|
2688
|
+
.when { display: grid; font-weight: 600; font-size: var(--text-md); font-variant-numeric: tabular-nums; }
|
|
2689
|
+
.until { font-weight: 400; font-size: var(--text-sm); color: var(--ink-3); }
|
|
2599
2690
|
.what { display: grid; min-width: 0; }
|
|
2600
|
-
.title { font-weight:
|
|
2691
|
+
.title { font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2601
2692
|
.on { color: var(--ink-3); font-weight: 400; }
|
|
2602
|
-
.meta { font-size:
|
|
2603
|
-
.
|
|
2604
|
-
.
|
|
2605
|
-
.
|
|
2606
|
-
.dot
|
|
2607
|
-
.
|
|
2608
|
-
.
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
.pill
|
|
2612
|
-
.
|
|
2613
|
-
.
|
|
2614
|
-
.
|
|
2615
|
-
.
|
|
2616
|
-
.
|
|
2693
|
+
.meta { font-size: var(--text-sm); color: var(--ink-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2694
|
+
.feed { position: relative; }
|
|
2695
|
+
.event { position: relative; display: grid; grid-template-columns: 12px minmax(0, 1fr); gap: var(--space-3); align-items: start; padding: 7px 0; }
|
|
2696
|
+
.event:not(:last-child)::before { content: ""; position: absolute; left: 5.5px; top: 22px; bottom: -8px; width: 1px; background: var(--line); }
|
|
2697
|
+
.dot { position: relative; width: 12px; height: 12px; margin-top: 4px; border-radius: var(--radius-full); background: var(--surface); box-shadow: inset 0 0 0 3px var(--ink-3); }
|
|
2698
|
+
.dot.good { box-shadow: inset 0 0 0 3px var(--good); }
|
|
2699
|
+
.dot.bad { box-shadow: inset 0 0 0 3px var(--bad); }
|
|
2700
|
+
|
|
2701
|
+
/* Pills */
|
|
2702
|
+
.pill { display: inline-flex; align-items: center; gap: 6px; font-size: var(--text-xs); font-weight: 600; line-height: 18px; padding: 1px 8px 1px 7px; border-radius: var(--radius-full); background: var(--sunken); color: var(--ink-2); white-space: nowrap; box-shadow: inset 0 0 0 1px var(--line); }
|
|
2703
|
+
.pill::before { content: ""; width: 6px; height: 6px; border-radius: var(--radius-full); background: currentColor; opacity: 0.9; }
|
|
2704
|
+
.pill.booking, .pill.active { background: var(--good-wash); color: var(--good); box-shadow: none; }
|
|
2705
|
+
.pill.event { background: var(--accent-wash); color: var(--accent); box-shadow: none; }
|
|
2706
|
+
.pill.held { background: var(--warn-wash); color: var(--warn); box-shadow: none; }
|
|
2707
|
+
.pill.revoked, .pill.expired { background: transparent; color: var(--ink-3); }
|
|
2708
|
+
|
|
2709
|
+
/* Definition lists */
|
|
2710
|
+
.endpoints { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: var(--space-2) var(--space-4); margin: var(--space-4) 0 0; align-items: center; }
|
|
2711
|
+
.endpoints dt { font: 500 var(--text-xs)/1 var(--mono); color: var(--ink-3); letter-spacing: 0.06em; }
|
|
2712
|
+
.endpoints dd { margin: 0; display: flex; align-items: center; gap: var(--space-2); min-width: 0; padding: 4px 4px 4px 12px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface-2); }
|
|
2713
|
+
.endpoints code { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2714
|
+
.facts { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: var(--space-2) var(--space-6); margin: 0; }
|
|
2617
2715
|
.facts dt { color: var(--ink-3); }
|
|
2618
|
-
.facts dd { margin: 0; }
|
|
2619
|
-
.
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2716
|
+
.facts dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
|
|
2717
|
+
.page > .facts { padding: var(--space-4) var(--space-6); border: 1px solid var(--line); border-radius: var(--radius-lg); background: var(--surface); }
|
|
2718
|
+
|
|
2719
|
+
/* Forms */
|
|
2720
|
+
label { display: grid; gap: 6px; font-size: var(--text-sm); font-weight: 500; color: var(--ink-2); min-width: 0; }
|
|
2721
|
+
input, select, textarea { font: inherit; font-size: 14px; color: var(--ink); background-color: var(--surface); border: 1px solid var(--line-strong); border-radius: var(--radius); min-width: 0; transition: border-color var(--fast) var(--ease), box-shadow var(--fast) var(--ease); }
|
|
2722
|
+
input, select { height: 36px; padding: 0 var(--space-3); }
|
|
2723
|
+
input::placeholder, textarea::placeholder { color: var(--ink-3); opacity: 0.8; }
|
|
2724
|
+
input:hover, select:hover, textarea:hover { border-color: var(--ink-3); }
|
|
2725
|
+
input:focus, select:focus, textarea:focus { border-color: var(--accent); outline: none; box-shadow: 0 0 0 3px var(--ring); }
|
|
2726
|
+
input[readonly] { background-color: var(--surface-2); }
|
|
2727
|
+
select { -webkit-appearance: none; appearance: none; padding-right: 32px; cursor: pointer; background-image: linear-gradient(45deg, transparent 50%, var(--ink-3) 50%), linear-gradient(135deg, var(--ink-3) 50%, transparent 50%); background-position: calc(100% - 16px) 50%, calc(100% - 11px) 50%; background-size: 5px 5px, 5px 5px; background-repeat: no-repeat; }
|
|
2728
|
+
input[type="checkbox"], input[type="radio"] { width: 16px; height: 16px; margin: 0; padding: 0; accent-color: var(--ink); flex: none; }
|
|
2729
|
+
input[type="time"] { font-variant-numeric: tabular-nums; }
|
|
2730
|
+
textarea { padding: 10px var(--space-3); resize: vertical; min-height: 72px; line-height: 1.5; }
|
|
2731
|
+
.create { display: flex; flex-wrap: wrap; align-items: end; gap: var(--space-3); margin: 0; }
|
|
2732
|
+
.create .grow { flex: 1 1 220px; }
|
|
2733
|
+
label > input, label > select, label > .unit-input { width: 100%; }
|
|
2734
|
+
label.choice > input { width: 16px; }
|
|
2735
|
+
.create label:not(.grow) { flex: 0 1 auto; }
|
|
2736
|
+
form > label { margin-bottom: var(--space-3); }
|
|
2737
|
+
.grid-form { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: var(--space-4) var(--space-3); margin-bottom: var(--space-4); align-items: end; }
|
|
2738
|
+
.unit-input { display: flex; align-items: center; gap: var(--space-2); }
|
|
2739
|
+
.unit-input input { width: 100%; }
|
|
2740
|
+
.unit-input span { color: var(--ink-3); white-space: nowrap; }
|
|
2741
|
+
.choice { display: flex; align-items: center; gap: 10px; font-size: 14px; font-weight: 400; color: var(--ink); cursor: pointer; }
|
|
2742
|
+
form .choice { margin: var(--space-1) 0 var(--space-3); }
|
|
2743
|
+
.modes { border: 0; padding: 0; margin: 0 0 var(--space-5); display: grid; gap: var(--space-2); }
|
|
2744
|
+
.modes legend { font-weight: 600; color: var(--ink); padding: 0; margin-bottom: var(--space-2); }
|
|
2745
|
+
.modes .choice { padding: 10px var(--space-3); border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); margin: 0; }
|
|
2746
|
+
.modes .choice:has(input:checked) { border-color: var(--ink); box-shadow: inset 0 0 0 1px var(--ink); }
|
|
2747
|
+
.types { margin: var(--space-1) 0 var(--space-4); }
|
|
2748
|
+
.types summary { cursor: pointer; color: var(--ink-2); font-size: var(--text-md); width: fit-content; }
|
|
2749
|
+
.checks { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: var(--space-2) var(--space-4); margin-top: var(--space-3); padding: var(--space-4); border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface-2); }
|
|
2750
|
+
.checks .choice { font-size: var(--text-md); margin: 0; }
|
|
2751
|
+
|
|
2752
|
+
/* Buttons */
|
|
2753
|
+
button, .button { font: inherit; font-size: var(--text-md); font-weight: 500; height: 36px; padding: 0 var(--space-4); border-radius: var(--radius); border: 1px solid var(--line-strong); background: var(--surface); color: var(--ink); cursor: pointer; display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2); white-space: nowrap; transition: background-color var(--fast) var(--ease), border-color var(--fast) var(--ease), color var(--fast) var(--ease), transform var(--fast) var(--ease); }
|
|
2754
|
+
button:hover, .button:hover { background: var(--hover); border-color: var(--ink-3); text-decoration: none; }
|
|
2755
|
+
button:active, .button:active { transform: translateY(0.5px); }
|
|
2756
|
+
.primary, a.primary { background: var(--button); border-color: var(--button); color: var(--button-ink); }
|
|
2757
|
+
.primary:hover, a.primary:hover { background: var(--button-hover); border-color: var(--button-hover); color: var(--button-ink); }
|
|
2758
|
+
.wide { width: 100%; height: 42px; }
|
|
2759
|
+
button.danger { background: var(--bad); border-color: var(--bad); color: var(--bad-ink); }
|
|
2760
|
+
button.danger:hover { background: var(--bad); border-color: var(--bad); filter: brightness(1.08); }
|
|
2761
|
+
button.quiet { height: 28px; padding: 0 var(--space-2); border: 0; background: none; color: var(--ink-3); font-weight: 500; font-size: var(--text-sm); }
|
|
2630
2762
|
button.quiet:hover { color: var(--ink); background: var(--hover); }
|
|
2631
|
-
|
|
2632
|
-
.
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
td
|
|
2763
|
+
.row-actions, .action-buttons { display: flex; gap: var(--space-2); align-items: center; flex-wrap: wrap; margin-top: var(--space-4); }
|
|
2764
|
+
form.inline { display: inline; margin: 0; }
|
|
2765
|
+
|
|
2766
|
+
/* Tables */
|
|
2767
|
+
.table { overflow-x: auto; border: 1px solid var(--line); border-radius: var(--radius-lg); background: var(--surface); }
|
|
2768
|
+
table { width: 100%; border-collapse: collapse; font-size: var(--text-md); }
|
|
2769
|
+
th, td { text-align: left; padding: 11px 10px; border-bottom: 1px solid var(--line-soft); vertical-align: middle; }
|
|
2770
|
+
th:first-child, td:first-child { padding-left: var(--space-4); }
|
|
2771
|
+
th:last-child, td:last-child { padding-right: var(--space-4); }
|
|
2772
|
+
th { font: 500 var(--text-xs)/1.4 var(--mono); color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.06em; background: var(--surface-2); border-bottom-color: var(--line); white-space: nowrap; padding-top: 9px; padding-bottom: 9px; }
|
|
2773
|
+
td { white-space: nowrap; color: var(--ink-2); font-variant-numeric: tabular-nums; }
|
|
2774
|
+
tbody tr { transition: background-color var(--fast) var(--ease); }
|
|
2775
|
+
tbody tr:hover { background: var(--surface-2); }
|
|
2776
|
+
td.strong { color: var(--ink); font-weight: 500; white-space: normal; min-width: 140px; }
|
|
2638
2777
|
tr:last-child td { border-bottom: 0; }
|
|
2639
|
-
.
|
|
2640
|
-
.
|
|
2641
|
-
.
|
|
2778
|
+
td.wrap { white-space: normal; min-width: 160px; }
|
|
2779
|
+
.truncate { display: block; max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2780
|
+
.truncate.url { max-width: 340px; }
|
|
2781
|
+
.truncate.id { max-width: 150px; }
|
|
2782
|
+
.chip { display: inline-block; padding: 1px 6px; border-radius: var(--radius-sm); background: var(--sunken); color: var(--ink-2); box-shadow: inset 0 0 0 1px var(--line); font-size: 12px; }
|
|
2783
|
+
.cell { display: grid; justify-items: start; gap: 2px; min-width: 0; }
|
|
2784
|
+
.cell > * { max-width: 100%; }
|
|
2785
|
+
td .meta { display: block; margin-top: 2px; }
|
|
2786
|
+
.meta-link { font-size: var(--text-sm); }
|
|
2787
|
+
.wrap, td.wrap .meta, .meta.wrap { white-space: normal; overflow-wrap: anywhere; }
|
|
2788
|
+
.actions { white-space: nowrap; text-align: right; width: 1%; }
|
|
2789
|
+
.actions button { height: 28px; padding: 0 10px; font-size: var(--text-sm); }
|
|
2790
|
+
td .meta.wrap { min-width: 200px; max-width: 340px; }
|
|
2791
|
+
.week input { width: 100%; }
|
|
2792
|
+
.week th[scope="row"] { font: 500 var(--text-md)/1.4 var(--font); text-transform: none; letter-spacing: 0; color: var(--ink); background: none; }
|
|
2793
|
+
form > .table { margin-bottom: var(--space-3); }
|
|
2794
|
+
|
|
2795
|
+
/* Confirmations */
|
|
2796
|
+
.act { display: inline-block; position: relative; margin-left: var(--space-1); text-align: left; }
|
|
2797
|
+
.act summary { display: inline-flex; align-items: center; height: 28px; padding: 0 10px; border-radius: var(--radius-sm); cursor: pointer; color: var(--ink-2); font-weight: 500; font-size: var(--text-sm); list-style: none; transition: background-color var(--fast) var(--ease), color var(--fast) var(--ease); }
|
|
2642
2798
|
.act summary::-webkit-details-marker { display: none; }
|
|
2799
|
+
.act summary:hover { background: var(--hover); color: var(--ink); }
|
|
2800
|
+
.act[open] summary { background: var(--hover); color: var(--ink); }
|
|
2643
2801
|
.act.danger summary { color: var(--bad); }
|
|
2644
|
-
.
|
|
2645
|
-
.pop
|
|
2646
|
-
.
|
|
2647
|
-
.
|
|
2802
|
+
.act.danger summary:hover, .act.danger[open] summary { background: var(--bad-wash); color: var(--bad); }
|
|
2803
|
+
.pop { position: absolute; right: 0; z-index: 5; width: 280px; margin-top: 6px; padding: var(--space-4); border: 1px solid var(--line); border-radius: var(--radius-lg); background: var(--surface); box-shadow: var(--pop-shadow); white-space: normal; animation: pop-in var(--slow) var(--ease); transform-origin: top right; }
|
|
2804
|
+
.pop p { font-size: var(--text-md); color: var(--ink); margin-bottom: var(--space-3); }
|
|
2805
|
+
.pop button { width: 100%; }
|
|
2806
|
+
.row-actions .pop { left: 0; right: auto; transform-origin: top left; }
|
|
2807
|
+
/* In a table the confirmation opens in place, growing its row. .table scrolls (overflow-x: auto,
|
|
2808
|
+
which makes it clip both ways), so a floating .pop past its last row was cut off: Revoke on a
|
|
2809
|
+
one-key account opened nothing anyone could click. */
|
|
2810
|
+
.table .pop { position: static; width: 280px; max-width: 100%; margin: var(--space-2) 0 var(--space-1) auto; box-shadow: none; text-align: left; }
|
|
2811
|
+
@keyframes pop-in { from { opacity: 0; transform: translateY(-4px) scale(0.98); } to { opacity: 1; transform: none; } }
|
|
2812
|
+
|
|
2813
|
+
/* Notices and empty states */
|
|
2814
|
+
.notice { display: block; border: 1px solid var(--line); border-radius: var(--radius); padding: var(--space-3) var(--space-4); background: var(--surface); color: var(--ink); margin: 0; font-size: var(--text-md); }
|
|
2815
|
+
.notice[role="alert"] { border-color: var(--bad-line); background: var(--bad-wash); color: var(--ink); box-shadow: inset 3px 0 0 var(--bad); }
|
|
2816
|
+
.notice.warn, .notice.warn[role="alert"] { border-color: var(--warn-line); background: var(--warn-wash); color: var(--warn); box-shadow: inset 3px 0 0 var(--warn); }
|
|
2817
|
+
.notice p { color: inherit; margin: 0 0 var(--space-2); }
|
|
2648
2818
|
.empty { color: var(--ink-3); margin: 0; }
|
|
2649
|
-
.
|
|
2650
|
-
.
|
|
2819
|
+
.empty-state { display: grid; justify-items: center; gap: var(--space-3); padding: var(--space-12) var(--space-6); border: 1px dashed var(--line-strong); border-radius: var(--radius-lg); text-align: center; }
|
|
2820
|
+
.empty-state .empty { max-width: 380px; color: var(--ink-2); }
|
|
2821
|
+
.empty-icon { display: grid; place-items: center; width: 40px; height: 40px; border-radius: var(--radius); background: var(--surface); color: var(--ink-2); box-shadow: inset 0 0 0 1px var(--line), 0 1px 2px rgba(0, 0, 0, 0.04); }
|
|
2822
|
+
.empty-icon .icon { width: 20px; height: 20px; }
|
|
2823
|
+
|
|
2824
|
+
/* Shown once */
|
|
2825
|
+
.once { max-width: 720px; padding: var(--space-8); display: grid; gap: var(--space-2); }
|
|
2826
|
+
.once h2 { margin: var(--space-4) 0 0; font-size: var(--text-md); }
|
|
2827
|
+
.once > .button { justify-self: start; margin-top: var(--space-3); }
|
|
2828
|
+
.copy { display: flex; gap: var(--space-2); margin: var(--space-2) 0 var(--space-3); }
|
|
2651
2829
|
.copy input { flex: 1; }
|
|
2652
|
-
pre { background: var(--
|
|
2830
|
+
pre { background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--radius); padding: var(--space-3) var(--space-4); margin: 0; overflow-x: auto; white-space: pre-wrap; overflow-wrap: anywhere; color: var(--ink); line-height: 1.6; }
|
|
2653
2831
|
.hours { margin: 0; padding-left: 18px; color: var(--ink-2); }
|
|
2654
|
-
.back
|
|
2655
|
-
.
|
|
2656
|
-
.
|
|
2657
|
-
.
|
|
2658
|
-
.
|
|
2659
|
-
.
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
.bare {
|
|
2663
|
-
.bare-bar { padding:
|
|
2664
|
-
.bare-main { display: grid; place-items: start center; padding:
|
|
2665
|
-
.sign-in { width: 100%; max-width: 400px; padding:
|
|
2832
|
+
.back { display: inline-flex; align-items: center; gap: 6px; color: var(--ink-3); font-size: var(--text-md); font-weight: 500; }
|
|
2833
|
+
.back::before { content: ""; width: 6px; height: 6px; border-left: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor; transform: rotate(45deg); }
|
|
2834
|
+
.back:hover { color: var(--ink); text-decoration: none; }
|
|
2835
|
+
.page > p:has(> .back) { margin: 0 0 calc(-1 * var(--space-3)); }
|
|
2836
|
+
.page > section:not([class]) { display: grid; gap: var(--space-3); }
|
|
2837
|
+
.page > section:not([class]) > h1 { margin-bottom: var(--space-1); }
|
|
2838
|
+
|
|
2839
|
+
/* Signed out */
|
|
2840
|
+
.bare { min-height: 100vh; }
|
|
2841
|
+
.bare-bar { padding: var(--space-5) var(--space-6); }
|
|
2842
|
+
.bare-main { display: grid; place-items: start center; padding: 10vh var(--space-4) var(--space-12); }
|
|
2843
|
+
.sign-in { width: 100%; max-width: 400px; padding: var(--space-8); border: 1px solid var(--line); border-radius: 16px; background: var(--surface); display: grid; gap: var(--space-3); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03), 0 24px 48px -24px rgba(0, 0, 0, 0.12); }
|
|
2844
|
+
.sign-in-mark { display: grid; place-items: center; width: 44px; height: 44px; margin-bottom: var(--space-2); border-radius: var(--radius-lg); background: var(--surface-2); box-shadow: inset 0 0 0 1px var(--line); color: var(--ink); }
|
|
2845
|
+
.sign-in-mark .mark { width: 24px; height: 22px; }
|
|
2666
2846
|
.sign-in h1 { font-size: 20px; }
|
|
2667
2847
|
.sign-in p { margin: 0; }
|
|
2668
|
-
.
|
|
2669
|
-
.
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
.
|
|
2673
|
-
.
|
|
2674
|
-
|
|
2675
|
-
.types { margin: 12px 0; }
|
|
2676
|
-
.types summary { cursor: pointer; color: var(--ink-2); font-size: 13px; }
|
|
2677
|
-
.checks { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 6px 16px; margin-top: 10px; }
|
|
2678
|
-
.checks .choice { font-size: 13px; margin: 0; }
|
|
2679
|
-
td .meta { display: block; margin-top: 2px; }
|
|
2680
|
-
td[data-label] > .pill { justify-self: start; }
|
|
2681
|
-
td.wrap, .wrap { white-space: normal; overflow-wrap: anywhere; }
|
|
2682
|
-
form.inline { display: inline; margin: 0; }
|
|
2683
|
-
form > label { margin-bottom: 4px; }
|
|
2684
|
-
.chat { display: grid; gap: 16px; }
|
|
2685
|
-
.thread { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
|
|
2686
|
-
.msg { max-width: 78%; padding: 10px 14px; border-radius: 12px; background: var(--wash); border: 1px solid var(--line); }
|
|
2848
|
+
.sign-in .button { margin-top: var(--space-3); }
|
|
2849
|
+
.bare-main > .notice { max-width: 480px; }
|
|
2850
|
+
|
|
2851
|
+
/* Assistant */
|
|
2852
|
+
.chat { display: grid; gap: var(--space-5); }
|
|
2853
|
+
.thread { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-3); }
|
|
2854
|
+
.msg { max-width: 78%; padding: 10px 14px; border-radius: 14px; background: var(--surface-2); border: 1px solid var(--line); }
|
|
2687
2855
|
.msg p { margin: 0; color: var(--ink); white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
2688
|
-
.msg.mine { justify-self: end; background: var(--
|
|
2689
|
-
.msg.
|
|
2690
|
-
.
|
|
2691
|
-
.
|
|
2692
|
-
.
|
|
2693
|
-
.action-card
|
|
2694
|
-
.action-
|
|
2695
|
-
.action-card
|
|
2856
|
+
.msg.mine { justify-self: end; background: var(--button); border-color: var(--button); border-bottom-right-radius: 4px; }
|
|
2857
|
+
.msg.mine p { color: var(--button-ink); }
|
|
2858
|
+
.msg.bot { justify-self: start; border-bottom-left-radius: 4px; }
|
|
2859
|
+
.tool-line { font: 400 var(--text-sm)/1.5 var(--mono); color: var(--ink-3); padding-left: var(--space-1); }
|
|
2860
|
+
.thread > .notice { justify-self: start; }
|
|
2861
|
+
.action-card { justify-self: start; width: min(100%, 540px); padding: var(--space-4) var(--space-5); border: 1px solid var(--line); border-radius: var(--radius-lg); background: var(--surface); box-shadow: var(--pop-shadow); display: grid; gap: var(--space-3); }
|
|
2862
|
+
.action-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); }
|
|
2863
|
+
.action-card .facts { font-size: var(--text-md); gap: 6px var(--space-4); }
|
|
2864
|
+
.action-card .facts dt::first-letter { text-transform: uppercase; }
|
|
2865
|
+
.action-card pre.wrap { white-space: pre-wrap; margin: 0; padding: var(--space-2); font-size: 12px; }
|
|
2696
2866
|
.action-summary { margin: 0; color: var(--ink); font-weight: 500; overflow-wrap: anywhere; }
|
|
2697
|
-
.action-
|
|
2698
|
-
.composer { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap:
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
.visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
|
|
2702
|
-
@media (max-width: 1000px) {
|
|
2867
|
+
.action-card .action-buttons { margin-top: var(--space-1); }
|
|
2868
|
+
.composer { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--space-3); align-items: end; margin: 0; padding-top: var(--space-4); border-top: 1px solid var(--line-soft); }
|
|
2869
|
+
|
|
2870
|
+
@media (max-width: 1080px) {
|
|
2703
2871
|
.split { grid-template-columns: minmax(0, 1fr); }
|
|
2872
|
+
.page { padding: var(--space-6) var(--space-8) 64px; }
|
|
2704
2873
|
}
|
|
2705
2874
|
@media (max-width: 760px) {
|
|
2706
|
-
.app { grid-template-columns: minmax(0, 1fr);
|
|
2707
|
-
.side { position: sticky; z-index:
|
|
2708
|
-
.side-top { display: flex; align-items: center;
|
|
2709
|
-
.
|
|
2710
|
-
.
|
|
2711
|
-
.
|
|
2712
|
-
.me {
|
|
2713
|
-
.me .login { display: none; }
|
|
2714
|
-
.
|
|
2715
|
-
.
|
|
2875
|
+
.app { grid-template-columns: minmax(0, 1fr); }
|
|
2876
|
+
.side { position: sticky; z-index: 4; top: 0; height: auto; display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 0 var(--space-2); padding: 10px var(--space-4) 0; border-right: 0; border-bottom: 1px solid var(--line); background: var(--surface-2); }
|
|
2877
|
+
.side-top { grid-column: 1; display: flex; align-items: center; gap: var(--space-3); padding: 0; min-width: 0; }
|
|
2878
|
+
.side-top .brand .name { display: none; }
|
|
2879
|
+
.workspace { flex: 1; min-width: 0; padding: 3px 4px; }
|
|
2880
|
+
.workspace .avatar { display: none; }
|
|
2881
|
+
.me { grid-column: 2; grid-row: 1; margin: 0; padding: 0; border: 0; }
|
|
2882
|
+
.me-avatar, .me .login { display: none; }
|
|
2883
|
+
.sign-out { width: 36px; height: 36px; padding: 0; }
|
|
2884
|
+
.sign-out-label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
|
|
2885
|
+
.nav { grid-column: 1 / -1; display: flex; gap: var(--space-1); overflow-x: auto; margin: var(--space-2) calc(-1 * var(--space-4)) 0; padding: 0 var(--space-4); scrollbar-width: none; scroll-padding: 0 var(--space-4); }
|
|
2886
|
+
.nav::-webkit-scrollbar { display: none; }
|
|
2887
|
+
.nav a { flex: none; height: 40px; padding: 0 10px; border-radius: 0; box-shadow: none; background: none; }
|
|
2888
|
+
.nav a:hover { background: none; }
|
|
2889
|
+
.nav a[aria-current="page"] { background: none; box-shadow: inset 0 -2px 0 var(--ink); }
|
|
2890
|
+
.page { padding: var(--space-5) var(--space-4) var(--space-12); gap: var(--space-4); }
|
|
2891
|
+
h1 { font-size: 20px; }
|
|
2892
|
+
.panel, .card { padding: var(--space-4); }
|
|
2893
|
+
.once { padding: var(--space-5); }
|
|
2716
2894
|
.stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
2717
|
-
.stat
|
|
2895
|
+
.stat { padding: 14px var(--space-4); box-shadow: -1px 0 0 var(--line), 0 -1px 0 var(--line); }
|
|
2896
|
+
.stat-value { font-size: 22px; }
|
|
2897
|
+
.create > * { flex: 1 1 100%; }
|
|
2898
|
+
.create button { width: 100%; }
|
|
2899
|
+
.endpoints { grid-template-columns: minmax(0, 1fr); gap: var(--space-1); }
|
|
2900
|
+
.endpoints dd { margin-bottom: var(--space-2); }
|
|
2901
|
+
.endpoints code { white-space: normal; overflow-wrap: anywhere; }
|
|
2902
|
+
.facts { grid-template-columns: minmax(0, 1fr); gap: 0; }
|
|
2903
|
+
.facts dd { margin-bottom: var(--space-2); }
|
|
2904
|
+
.table { border-radius: var(--radius-lg); }
|
|
2718
2905
|
thead { display: none; }
|
|
2719
2906
|
table, tbody, tr, td { display: block; }
|
|
2720
|
-
tr { border-bottom: 1px solid var(--line); padding:
|
|
2907
|
+
tr { border-bottom: 1px solid var(--line); padding: 10px 0; }
|
|
2908
|
+
tbody tr:hover { background: none; }
|
|
2721
2909
|
tr:last-child { border-bottom: 0; }
|
|
2722
|
-
td { border: 0; padding: 3px
|
|
2723
|
-
td[data-label] { display: grid; grid-template-columns:
|
|
2724
|
-
td[data-label]::before { content: attr(data-label); font
|
|
2725
|
-
.
|
|
2726
|
-
.
|
|
2727
|
-
.
|
|
2910
|
+
td { border: 0; padding: 3px var(--space-4); white-space: normal; }
|
|
2911
|
+
td[data-label] { display: grid; grid-template-columns: 92px minmax(0, 1fr); gap: var(--space-2); align-items: baseline; }
|
|
2912
|
+
td[data-label]::before { content: attr(data-label); font: 500 var(--text-xs)/1.6 var(--mono); color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.05em; }
|
|
2913
|
+
td.strong, td.wrap { min-width: 0; }
|
|
2914
|
+
td .meta.wrap { min-width: 0; max-width: none; }
|
|
2915
|
+
.truncate, .truncate.url, .truncate.id { max-width: 100%; white-space: normal; overflow-wrap: anywhere; }
|
|
2916
|
+
.week th[scope="row"] { display: block; padding: var(--space-2) var(--space-4) 0; }
|
|
2917
|
+
.actions { text-align: left; width: auto; padding-top: var(--space-2); }
|
|
2918
|
+
.act { margin: 0 var(--space-2) 0 0; }
|
|
2919
|
+
.act:first-child, form.inline + .act { margin-left: 0; }
|
|
2920
|
+
.act summary { border: 1px solid var(--line-strong); height: 32px; }
|
|
2921
|
+
.act.danger summary { border-color: var(--bad-line); }
|
|
2922
|
+
.pop { left: 0; right: auto; width: min(300px, calc(100vw - 48px)); transform-origin: top left; }
|
|
2923
|
+
.table .pop { margin-left: 0; }
|
|
2728
2924
|
.msg { max-width: 92%; }
|
|
2729
2925
|
.composer { grid-template-columns: minmax(0, 1fr); }
|
|
2926
|
+
.empty-state { padding: var(--space-8) var(--space-4); }
|
|
2927
|
+
.bare-main { padding-top: 6vh; }
|
|
2928
|
+
.sign-in { padding: var(--space-6); }
|
|
2929
|
+
}
|
|
2930
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2931
|
+
*, *::before, *::after { animation-duration: 1ms !important; animation-iteration-count: 1 !important; transition-duration: 1ms !important; scroll-behavior: auto !important; }
|
|
2932
|
+
}
|
|
2933
|
+
@media (forced-colors: active) {
|
|
2934
|
+
.pill, .chip, .nav a[aria-current="page"] { border: 1px solid CanvasText; }
|
|
2730
2935
|
}
|
|
2731
2936
|
`;
|
|
2732
2937
|
const ASSET_VERSIONS = Object.freeze({
|