antisketch 0.0.0-stage → 1.2.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/LICENSE +21 -0
- package/README.md +103 -2
- package/app/distribution/brand.html +1 -0
- package/app/mcp.example.json +10 -0
- package/app/public/app.js +86 -0
- package/app/public/brand/favicon.svg +1 -0
- package/app/public/brand/identity.css +11 -0
- package/app/public/brand/identity.json +50 -0
- package/app/public/brand/mark.svg +1 -0
- package/app/public/fonts/Inter-LICENSE.txt +92 -0
- package/app/public/fonts/InterVariable.woff2 +0 -0
- package/app/public/fonts/PlayfairDisplay-Italic.ttf +0 -0
- package/app/public/fonts/PlayfairDisplay-LICENSE.txt +93 -0
- package/app/public/fonts/PlayfairDisplay.ttf +0 -0
- package/app/public/fonts/SpaceGrotesk-LICENSE.txt +93 -0
- package/app/public/fonts/SpaceGrotesk.ttf +0 -0
- package/app/public/index.html +15 -0
- package/app/public/setup.js +53 -0
- package/app/public/style.css +318 -0
- package/app/scripts/backup.js +8 -0
- package/app/scripts/restore.js +7 -0
- package/app/server/app.js +55 -0
- package/app/server/artwork.js +25 -0
- package/app/server/capture.js +56 -0
- package/app/server/codex-connection.js +89 -0
- package/app/server/compat.js +12 -0
- package/app/server/config.js +13 -0
- package/app/server/demo-fonts.js +14 -0
- package/app/server/files.js +9 -0
- package/app/server/generate.js +53 -0
- package/app/server/index.js +21 -0
- package/app/server/install.js +69 -0
- package/app/server/interaction-contract.js +2 -0
- package/app/server/interpret.js +108 -0
- package/app/server/library.js +44 -0
- package/app/server/mcp.js +15 -0
- package/app/server/network.js +23 -0
- package/app/server/package.js +33 -0
- package/app/server/releases.js +8 -0
- package/app/server/setup.js +6 -0
- package/app/server/store.js +25 -0
- package/app/server/validate.js +48 -0
- package/app/server/worker.js +26 -0
- package/bin/antisketch +25 -0
- package/bin/design-vault +3 -0
- package/package.json +80 -4
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Copyright 2020 The Space Grotesk Project Authors (https://github.com/floriankarsten/space-grotesk)
|
|
2
|
+
|
|
3
|
+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
4
|
+
This license is copied below, and is also available with a FAQ at:
|
|
5
|
+
http://scripts.sil.org/OFL
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
-----------------------------------------------------------
|
|
9
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
10
|
+
-----------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
PREAMBLE
|
|
13
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
14
|
+
development of collaborative font projects, to support the font creation
|
|
15
|
+
efforts of academic and linguistic communities, and to provide a free and
|
|
16
|
+
open framework in which fonts may be shared and improved in partnership
|
|
17
|
+
with others.
|
|
18
|
+
|
|
19
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
20
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
21
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
22
|
+
redistributed and/or sold with any software provided that any reserved
|
|
23
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
24
|
+
however, cannot be released under any other type of license. The
|
|
25
|
+
requirement for fonts to remain under this license does not apply
|
|
26
|
+
to any document created using the fonts or their derivatives.
|
|
27
|
+
|
|
28
|
+
DEFINITIONS
|
|
29
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
30
|
+
Holder(s) under this license and clearly marked as such. This may
|
|
31
|
+
include source files, build scripts and documentation.
|
|
32
|
+
|
|
33
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
34
|
+
copyright statement(s).
|
|
35
|
+
|
|
36
|
+
"Original Version" refers to the collection of Font Software components as
|
|
37
|
+
distributed by the Copyright Holder(s).
|
|
38
|
+
|
|
39
|
+
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
40
|
+
or substituting -- in part or in whole -- any of the components of the
|
|
41
|
+
Original Version, by changing formats or by porting the Font Software to a
|
|
42
|
+
new environment.
|
|
43
|
+
|
|
44
|
+
"Author" refers to any designer, engineer, programmer, technical
|
|
45
|
+
writer or other person who contributed to the Font Software.
|
|
46
|
+
|
|
47
|
+
PERMISSION & CONDITIONS
|
|
48
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
49
|
+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
50
|
+
redistribute, and sell modified and unmodified copies of the Font
|
|
51
|
+
Software, subject to the following conditions:
|
|
52
|
+
|
|
53
|
+
1) Neither the Font Software nor any of its individual components,
|
|
54
|
+
in Original or Modified Versions, may be sold by itself.
|
|
55
|
+
|
|
56
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
57
|
+
redistributed and/or sold with any software, provided that each copy
|
|
58
|
+
contains the above copyright notice and this license. These can be
|
|
59
|
+
included either as stand-alone text files, human-readable headers or
|
|
60
|
+
in the appropriate machine-readable metadata fields within text or
|
|
61
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
62
|
+
|
|
63
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
64
|
+
Name(s) unless explicit written permission is granted by the corresponding
|
|
65
|
+
Copyright Holder. This restriction only applies to the primary font name as
|
|
66
|
+
presented to the users.
|
|
67
|
+
|
|
68
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
69
|
+
Software shall not be used to promote, endorse or advertise any
|
|
70
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
71
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
72
|
+
permission.
|
|
73
|
+
|
|
74
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
75
|
+
must be distributed entirely under this license, and must not be
|
|
76
|
+
distributed under any other license. The requirement for fonts to
|
|
77
|
+
remain under this license does not apply to any document created
|
|
78
|
+
using the Font Software.
|
|
79
|
+
|
|
80
|
+
TERMINATION
|
|
81
|
+
This license becomes null and void if any of the above conditions are
|
|
82
|
+
not met.
|
|
83
|
+
|
|
84
|
+
DISCLAIMER
|
|
85
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
86
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
87
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
88
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
89
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
90
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
91
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
92
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
93
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
Binary file
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="theme-color" content="#f7f7f5"><title>Antisketch — A workshop for your eye</title><meta name="description" content="Turn references you love into design skills your agents can use. Capture a website, explore its design language, try new applications, and keep what works."><link rel="icon" type="image/svg+xml" href="/brand/favicon.svg"><link rel="preload" href="/fonts/InterVariable.woff2" as="font" type="font/woff2" crossorigin><link rel="preload" href="/fonts/SpaceGrotesk.ttf" as="font" type="font/ttf" crossorigin><link rel="stylesheet" href="/style.css"><script src="/app.js" defer></script><script src="/setup.js" defer></script></head><body>
|
|
3
|
+
<a class="skip" href="#main">Skip to content</a>
|
|
4
|
+
<aside class="sidebar">
|
|
5
|
+
<a class="brand" href="#inbox"><span class="brand-mark" aria-hidden="true"><img src="/brand/mark.svg" alt=""></span><span>antisketch<small>A workshop for your eye</small></span></a>
|
|
6
|
+
<nav aria-label="Main navigation"><a href="#inbox" data-nav="inbox"><span class="nav-index" aria-hidden="true">01</span>Review inbox <span id="inbox-count" class="count">0</span></a><a href="#library" data-nav="library"><span class="nav-index" aria-hidden="true">02</span>Approved skills <span id="library-count" class="count">0</span></a><a href="#archive" data-nav="archive"><span class="nav-index" aria-hidden="true">03</span>Archive</a></nav>
|
|
7
|
+
<button class="text-button" id="settings-button" aria-label="Connection & storage">Studio settings <span>↗</span></button>
|
|
8
|
+
</aside>
|
|
9
|
+
<div class="shell"><header class="topbar"><span id="breadcrumb">Your workspace / Review inbox</span><div><button id="connection" class="connection" aria-label="Manage Codex connection">Connecting…</button><button class="button small" id="new-button">+ Add a reference</button></div></header><main id="main" tabindex="-1"></main></div>
|
|
10
|
+
<div id="toast" role="status" aria-live="polite"></div>
|
|
11
|
+
<dialog id="new-dialog"><form id="new-form"><div class="dialog-top"><span class="eyebrow">START WITH AN EYE</span><button type="button" class="icon-button" data-close aria-label="Close">×</button></div><h2>A new reference.</h2><p class="muted">Bring a reference. Find its language. Take it somewhere new.</p><label for="source-url">Website address</label><input id="source-url" name="url" type="text" inputmode="url" placeholder="https://a-website-you-love.com" required maxlength="2048"><label for="source-notes">What caught your eye? <span class="optional">Optional</span></label><textarea id="source-notes" name="notes" placeholder="The typography, the tactile imagery, how the menu moves…" maxlength="4000" rows="3"></textarea><div class="capture-explainer"><span>01 <b>Capture</b></span><i>→</i><span>02 <b>Explore</b></span><i>→</i><span>03 <b>Make it yours</b></span></div><p class="fine">Public pages only. Captures and measurements are sent to your signed-in Codex account to prepare the design and demos. Nothing is approved automatically.</p><p id="new-error" class="error-text" role="alert"></p><button type="submit" class="button wide">Prepare for review <span>↗</span></button></form></dialog>
|
|
12
|
+
<dialog id="settings-dialog"><div class="dialog-top"><span class="eyebrow">YOUR SETUP</span><button class="icon-button" data-close aria-label="Close">×</button></div><h2 id="setup-title">Your workshop, connected.</h2><div id="settings-content"></div></dialog>
|
|
13
|
+
<dialog id="approve-dialog"><form id="approve-form"><div class="dialog-top"><span class="eyebrow">SAVE WHAT YOU BELIEVE IN</span><button type="button" class="icon-button" data-close aria-label="Close">×</button></div><h2>Make it a skill.</h2><p id="approve-description" class="muted"></p><div class="approval-checks"><label><input type="checkbox" name="reference" required> I reviewed the reference and the extracted design language.</label><label><input type="checkbox" name="transfer" required> I tried the selected demo, including its mobile layout, and it carries the aesthetic into a useful new task.</label><label><input type="checkbox" name="limits" required> I reviewed the coverage, limitations, and source asset restrictions.</label></div><label for="approval-note">A note for your future self <span class="optional">Optional</span></label><textarea id="approval-note" name="note" rows="2" maxlength="3000" placeholder="What makes this one worth keeping?"></textarea><p class="fine">This freezes the specification, tokens, and selected demo. Future changes create a new version.</p><p id="approve-error" class="error-text" role="alert"></p><button class="button wide" type="submit">Approve this version <span>✓</span></button></form></dialog>
|
|
14
|
+
<dialog id="install-dialog" aria-labelledby="install-title"><div class="dialog-top"><span class="eyebrow">YOUR EYE. YOUR AGENT’S NEXT SKILL.</span><button type="button" class="icon-button" data-close aria-label="Close">×</button></div><h2 id="install-title">Put it to work.</h2><p id="install-description" class="muted"></p><div id="install-content" aria-live="polite"></div></dialog>
|
|
15
|
+
</body></html>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/* Uses the same-origin API and session token owned by app.js. */
|
|
2
|
+
let setupTimer=null,setupBusy=false,setupSnapshot='',setupOpen=0;
|
|
3
|
+
async function openSetup({welcome=false}={}){
|
|
4
|
+
const dialog=$('#settings-dialog');if(!dialog.open)dialog.showModal();
|
|
5
|
+
$('#setup-title').textContent=welcome?'Give your agent an eye.':'Your workshop, connected.';
|
|
6
|
+
$('#settings-content').innerHTML='<p class="muted" role="status">Checking your setup…</p>';
|
|
7
|
+
setupSnapshot='';const generation=++setupOpen;
|
|
8
|
+
await refreshSetup(generation);clearInterval(setupTimer);
|
|
9
|
+
if(generation===setupOpen&&dialog.open)setupTimer=setInterval(()=>refreshSetup(generation),2500);
|
|
10
|
+
}
|
|
11
|
+
async function refreshSetup(generation=setupOpen,fresh=false){
|
|
12
|
+
if(setupBusy||!$('#settings-dialog').open)return;
|
|
13
|
+
setupBusy=true;
|
|
14
|
+
try{
|
|
15
|
+
const result=await api('/api/setup'+(fresh?'?refresh=1':''));if(generation!==setupOpen||!$('#settings-dialog').open)return;
|
|
16
|
+
session.provider=result.provider;renderConnection();
|
|
17
|
+
const snapshot=JSON.stringify(result);if(snapshot!==setupSnapshot){setupSnapshot=snapshot;renderSetup(result);}
|
|
18
|
+
}catch(error){if(generation===setupOpen)$('#settings-content').innerHTML=`<p class="error-text">${esc(error.message)}</p><button class="button" id="setup-retry">Check again ↗</button>`;$('#setup-retry')?.addEventListener('click',()=>refreshSetup());}
|
|
19
|
+
finally{setupBusy=false;}
|
|
20
|
+
}
|
|
21
|
+
function renderSetup({provider:p,browser,storage}){
|
|
22
|
+
const state=p.state||(p.ready?'connected':'disconnected');
|
|
23
|
+
const heading=({connected:'Connected to Codex',available:'Your Codex account is here',disconnected:'Connect your creative partner',reconnect:'Let’s reconnect',unavailable:'Codex needs attention','signing-in':'One step in your browser',limited:'A little pause',error:'Let’s try that again'})[state];
|
|
24
|
+
const account=p.account?.email|| (p.account?.type==='apiKey'?'API key connection':p.account?'Signed-in Codex account':'Sign in with your ChatGPT account');
|
|
25
|
+
let actions='';
|
|
26
|
+
if(state==='signing-in')actions=`<a class="button" href="${esc(p.login.url)}" target="_blank" rel="noopener noreferrer">Continue sign-in ↗</a><button class="link-button" data-connection-action="cancel">Cancel</button><p class="fine">This window updates when sign-in finishes. You can close the OpenAI tab afterward.</p>`;
|
|
27
|
+
else if(state==='available'||(!p.ready&&p.account&&state==='error'))actions='<button class="button" data-connection-action="use-existing">Use this Codex account <span>↗</span></button>';
|
|
28
|
+
else if(state==='connected'||state==='limited')actions='<button class="link-button" data-connection-action="disconnect">Disconnect Antisketch</button><p class="fine">Your Codex sign-in in other apps stays available. Finish or cancel preparations before disconnecting.</p>';
|
|
29
|
+
else if(state==='unavailable')actions='<p class="fine">The installer includes Codex. Reinstall Antisketch to repair it, or check your custom Codex executable setting.</p><button class="button secondary" id="setup-retry">Check again ↗</button>';
|
|
30
|
+
else actions='<button class="button" data-connection-action="login">Connect Codex <span>↗</span></button>';
|
|
31
|
+
const usage=p.usage?[p.usage.primary,p.usage.secondary].filter(Boolean).map((limit,i)=>`<p class="fine">${i?'Additional':'Current'} usage window · ${limit.usedPercent}% used${limit.resetsAt?' · resets '+esc(new Date(limit.resetsAt*1000).toLocaleString()):''}</p>`).join(''):'';
|
|
32
|
+
const focusAction=document.activeElement?.dataset?.connectionAction;
|
|
33
|
+
$('#settings-content').innerHTML=`<p class="setup-intro">Bring your eye. Connect your account. Your library belongs to you.</p><section class="setup-connection" aria-labelledby="setup-account-title"><div class="setup-step"><span>01 / YOUR AGENT</span><span class="setup-state ${p.ready?'ready':''}">${esc(state.replaceAll('-',' '))}</span></div><h3 id="setup-account-title">${esc(heading)}</h3><p class="setup-account">${esc(account)}${p.account?.plan?' <span>· '+esc(p.account.plan)+'</span>':''}</p><p class="muted">${esc(p.message)}</p>${usage}<div class="setup-actions">${actions}</div></section><section class="setup-browser"><div class="setup-step"><span>02 / YOUR BROWSER</span><span>${browser.ready?'READY':'SETUP NEEDED'}</span></div><p>${esc(browser.message)}</p>${browser.ready?'':'<a class="text-button" href="https://www.google.com/chrome/" target="_blank" rel="noopener noreferrer">Get Google Chrome ↗</a><button class="link-button" id="browser-recheck">Check again</button>'}</section><p class="fine setup-data-note">Extraction sends reference screenshots and measurements to your connected account. Its access and usage limits apply. Designs, review history, and approved skills stay on this Mac.</p><p id="setup-error" class="error-text" role="alert"></p>${p.ready&&browser.ready?'<button class="button wide" id="setup-done">Open my workshop <span>↗</span></button>':'<button class="text-button" id="setup-done">Explore my library for now ↗</button>'}<details class="setup-storage"><summary>Storage & connections</summary><p>Your private library</p><code>${esc(storage.data)}</code><p>Approved skills</p><code>${esc(storage.approved)}</code><p>Connect an MCP client with <code>antisketch mcp</code>. Back up with <code>antisketch backup</code>.</p><a href="/brand.html" target="_blank" rel="noopener">The Antisketch identity ↗</a></details>`;
|
|
34
|
+
$('#setup-done').addEventListener('click',()=>$('#settings-dialog').close());
|
|
35
|
+
$('#setup-retry')?.addEventListener('click',()=>refreshSetup());$('#browser-recheck')?.addEventListener('click',()=>{setupSnapshot='';refreshSetup(setupOpen,true);});
|
|
36
|
+
$$('[data-connection-action]').forEach(button=>button.addEventListener('click',()=>connectionAction(button.dataset.connectionAction)));
|
|
37
|
+
if(focusAction)document.querySelector(`[data-connection-action="${focusAction}"]`)?.focus();
|
|
38
|
+
}
|
|
39
|
+
async function connectionAction(action){
|
|
40
|
+
if(setupBusy)return;setupBusy=true;const generation=setupOpen;
|
|
41
|
+
$$('[data-connection-action]').forEach(button=>button.disabled=true);$('#setup-error').textContent='';
|
|
42
|
+
// Open synchronously on the user's gesture; browsers otherwise block the later OAuth tab.
|
|
43
|
+
const popup=action==='login'?window.open('about:blank','_blank'):null;
|
|
44
|
+
if(popup){popup.opener=null;popup.document.title='Connecting to Codex';popup.document.body.textContent='Preparing your secure sign-in…';}
|
|
45
|
+
try{
|
|
46
|
+
const provider=await api('/api/connection/'+action,{method:'POST',body:'{}'});session.provider=provider;renderConnection();
|
|
47
|
+
if(provider.login?.url&&popup)popup.location.replace(provider.login.url);else popup?.close();
|
|
48
|
+
setupSnapshot='';
|
|
49
|
+
}catch(error){popup?.close();if(generation===setupOpen)$('#setup-error').textContent=error.message;}
|
|
50
|
+
finally{setupBusy=false;$$('[data-connection-action]').forEach(button=>button.disabled=false);if(generation===setupOpen)await refreshSetup(generation);}
|
|
51
|
+
}
|
|
52
|
+
$('#settings-dialog').addEventListener('close',()=>{clearInterval(setupTimer);setupOpen++;setupSnapshot='';});
|
|
53
|
+
$('#connection').addEventListener('click',()=>openSetup());
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
@font-face{font-family:'Space Grotesk';font-style:normal;font-weight:300 700;font-display:swap;src:url('/fonts/SpaceGrotesk.ttf') format('truetype')}
|
|
2
|
+
@font-face{font-family:'Inter';font-style:normal;font-weight:100 900;font-display:swap;src:url('/fonts/InterVariable.woff2') format('woff2')}
|
|
3
|
+
:root{--paper:#f7f7f5;--white:#ffffff;--ink:#1c1c1a;--muted:#70706a;--line:#d5d5cf;--accent:#d83d27;--accent-soft:#faeee9;--danger:#b54738;--mono:ui-monospace,SFMono-Regular,Menlo,monospace;--display:'Space Grotesk',sans-serif;--sans:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}
|
|
4
|
+
*{box-sizing:border-box}
|
|
5
|
+
body{margin:0;background:var(--paper);font-family:var(--sans);color:var(--ink);font-size:14px;-webkit-font-smoothing:antialiased}
|
|
6
|
+
button,input,textarea,select{font:inherit}
|
|
7
|
+
button,a,input,textarea,select{-webkit-tap-highlight-color:transparent}
|
|
8
|
+
button{cursor:pointer}
|
|
9
|
+
button:disabled{cursor:not-allowed;opacity:.45}
|
|
10
|
+
a{color:inherit}
|
|
11
|
+
button:focus-visible,a:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
|
|
12
|
+
button{border:0}
|
|
13
|
+
h1,h2,h3,p{margin:0}
|
|
14
|
+
button,a{touch-action:manipulation}
|
|
15
|
+
.skip{position:fixed;top:-100px;left:20px;z-index:99;padding:12px;background:var(--accent);color:white}
|
|
16
|
+
.skip:focus{top:10px}
|
|
17
|
+
.sidebar{position:relative;display:flex;align-items:center;justify-content:space-between;gap:32px;min-height:106px;padding:20px 4.5%;border-bottom:1px solid var(--ink);background:var(--paper)}
|
|
18
|
+
.brand{display:flex;align-items:center;gap:13px;font:600 27px var(--display);letter-spacing:-1px;text-decoration:none;flex-shrink:0}
|
|
19
|
+
.brand small{font:9px var(--mono);text-transform:uppercase;letter-spacing:.7px;color:var(--muted);display:block;margin-top:6px}
|
|
20
|
+
.brand-mark{width:32px;height:38px;color:var(--accent);display:grid;place-items:center;flex-shrink:0}
|
|
21
|
+
.brand-mark span{display:none}
|
|
22
|
+
.workspace-label{font:500 10px var(--sans);letter-spacing:.8px;margin:42px 12px 12px;color:#848790;display:flex;justify-content:space-between}
|
|
23
|
+
nav{display:flex;gap:35px;align-self:stretch;align-items:stretch}
|
|
24
|
+
nav a{display:flex;align-items:center;gap:9px;position:relative;text-decoration:none;font-size:12px;color:var(--muted);padding:15px 0;white-space:nowrap}
|
|
25
|
+
nav a:hover{color:var(--ink)}
|
|
26
|
+
nav a.active{color:var(--ink);font-weight:550}
|
|
27
|
+
.nav-icon{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;flex-shrink:0}
|
|
28
|
+
.count{font:10px var(--mono);color:var(--accent);margin-left:2px}
|
|
29
|
+
.sidebar-bottom{margin-top:auto}
|
|
30
|
+
.private-badge{font-size:10px;display:flex;align-items:center;gap:7px;padding:14px 4px;color:var(--muted)}
|
|
31
|
+
.private-badge>span{width:5px;height:5px;background:#81858c;border-radius:50%}
|
|
32
|
+
.text-button{background:none;padding:10px 0;color:var(--ink);font:10px var(--mono);text-align:left;white-space:nowrap}
|
|
33
|
+
.text-button span{margin-left:8px}
|
|
34
|
+
.owner{border-top:1px solid var(--line);margin-top:20px;padding:20px 5px 0;display:flex;align-items:center;gap:9px;font-size:11px}
|
|
35
|
+
.owner small{display:block;font-size:10px;color:var(--muted);margin-top:4px}
|
|
36
|
+
.avatar{display:grid;place-items:center;background:#e9e9ed;border:1px solid #dedee3;width:29px;height:29px;border-radius:8px;font:600 12px var(--sans)}
|
|
37
|
+
.shell{margin:0}
|
|
38
|
+
.topbar{display:flex;justify-content:space-between;align-items:center;gap:20px;padding:15px 4.5%;border-bottom:1px solid var(--line);min-height:63px}
|
|
39
|
+
.topbar>span{font:9px var(--mono);letter-spacing:.6px;color:var(--muted)}
|
|
40
|
+
.topbar>div{display:flex;align-items:center;gap:24px}
|
|
41
|
+
.connection{font:9px var(--mono);color:var(--muted)}
|
|
42
|
+
.connection:before{content:'';display:inline-block;width:5px;height:5px;border-radius:50%;background:#6b6b63;margin-right:6px}
|
|
43
|
+
.connection.off:before{background:#b86b3d}
|
|
44
|
+
.button{background:var(--ink);color:var(--paper);padding:13px 18px;border:1px solid var(--ink);border-radius:0;font-size:12px;line-height:1.4;display:inline-flex;align-items:center;justify-content:center;gap:20px;text-decoration:none;font-weight:500;min-height:42px;transition:background 160ms,color 160ms,border-color 160ms}
|
|
45
|
+
.button:hover{background:var(--accent);border-color:var(--accent);color:#fff}
|
|
46
|
+
.button.small{font-size:10px;padding:9px 12px;min-height:32px}
|
|
47
|
+
.button.secondary{background:transparent;color:var(--ink);border-color:var(--line)}
|
|
48
|
+
.button.secondary:hover{border-color:var(--ink);background:#ededE7}
|
|
49
|
+
.button.subtle{border-color:var(--line);background:var(--white);color:var(--ink)}
|
|
50
|
+
.button.danger{background:#f2e6df;border-color:#dbb9a7;color:#913f29}
|
|
51
|
+
.wide{width:100%;justify-content:space-between}
|
|
52
|
+
.muted{color:var(--muted);line-height:1.7}
|
|
53
|
+
.eyebrow{font:9px/1.6 var(--mono);letter-spacing:.8px;color:var(--muted)}
|
|
54
|
+
.fine{font-size:10px;line-height:1.7;color:var(--muted)}
|
|
55
|
+
main{max-width:1640px;padding:36px 4.5% 36px;margin:auto}
|
|
56
|
+
h1{font:500 clamp(38px,5vw,72px)/1 var(--display);letter-spacing:-3px}
|
|
57
|
+
h1 em{font-style:normal;font-weight:inherit;color:inherit}
|
|
58
|
+
h2{font:500 28px/1.12 var(--display);letter-spacing:-1px}
|
|
59
|
+
h3{font:500 19px var(--display);letter-spacing:-.6px}
|
|
60
|
+
.intro-row{display:block;margin:12px 0 28px}
|
|
61
|
+
.intro-row p{max-width:620px;margin-top:12px;font-size:13px;line-height:1.8;color:var(--muted)}
|
|
62
|
+
.capture-bar{display:flex;align-items:center;gap:24px;margin:38px 0 43px;padding:14px 0;border-top:1px solid var(--ink);border-bottom:1px solid var(--ink);position:relative;transition:border-color 180ms}
|
|
63
|
+
.capture-bar>span{display:flex;padding:0 9px;color:#9299a6}
|
|
64
|
+
.capture-bar input{min-width:0;flex:1;border:0;background:transparent;padding:13px 0;color:var(--ink);font:400 17px var(--display);outline:none}
|
|
65
|
+
.section-line{display:flex;align-items:center;justify-content:space-between;gap:20px;margin-bottom:15px}
|
|
66
|
+
.section-line h2{font:10px var(--mono);letter-spacing:.7px}
|
|
67
|
+
.section-line h2 small{font:10px var(--mono);color:var(--accent);margin-left:9px}
|
|
68
|
+
.section-line input{background:transparent;border:0;border-bottom:1px solid var(--line);border-radius:0;padding:8px 0;width:190px;font:10px var(--mono);color:var(--ink)}
|
|
69
|
+
.cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(290px,1fr));gap:30px;counter-reset:studies}
|
|
70
|
+
.design-card{display:block;min-width:0;text-decoration:none;background:transparent;border:0;border-radius:0;transition:transform 180ms}
|
|
71
|
+
.design-card:hover{transform:translateY(-3px)}
|
|
72
|
+
.card-image{height:255px;position:relative;overflow:hidden;background:#e9e9e3;border:1px solid #d5d5cf;padding:12px}
|
|
73
|
+
.card-image img{width:100%;height:100%;object-fit:cover;object-position:top;display:block;transition:transform 350ms;filter:saturate(.85)}
|
|
74
|
+
.design-card:hover img{transform:scale(1.025)}
|
|
75
|
+
.card-image .badge{position:absolute;bottom:20px;left:20px;max-width:calc(100% - 40px)}
|
|
76
|
+
.card-image.placeholder{display:flex;align-items:center;justify-content:center;background:repeating-linear-gradient(135deg,#f4f4f6,#f4f4f6 8px,#ededf0 8px,#ededf0 9px)}
|
|
77
|
+
.card-monogram{font:600 60px var(--sans);color:#a0a5af}
|
|
78
|
+
.card-body{padding:17px 0 0}
|
|
79
|
+
.card-body h3{font:500 25px/1.1 var(--display);letter-spacing:-.9px;margin-bottom:9px}
|
|
80
|
+
.card-body p{font-size:11px;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
81
|
+
.card-footer{border-top:1px solid var(--line);margin-top:16px;padding-top:12px;display:flex;align-items:center;justify-content:space-between;font:9px var(--mono);color:var(--muted)}
|
|
82
|
+
.card-footer>span:last-child{font-size:9px;color:var(--ink)}
|
|
83
|
+
.badge{font:9px var(--mono);display:inline-flex;align-items:center;gap:5px;padding:6px 8px;border:1px solid #d5d5cf;background:var(--paper);color:var(--ink);border-radius:0;white-space:nowrap}
|
|
84
|
+
.badge.work{background:#f7efda;border-color:#dfcf9f;color:#8d7136}
|
|
85
|
+
.badge.fail{background:#f5e7e0;border-color:#dfc0b1;color:#954d32}
|
|
86
|
+
.badge.approved{background:#ededE7;border-color:#ccccbf;color:#52524b}
|
|
87
|
+
.badge.muted{background:#eeeee8;border-color:#dedee4;color:#6b707a}
|
|
88
|
+
.empty{grid-column:1/-1;display:grid;grid-template-columns:150px minmax(0,1fr) 260px;align-items:center;gap:40px;padding:45px 36px;border:1px solid var(--line);border-radius:0;background:transparent;position:relative;text-align:left}
|
|
89
|
+
.empty h2{font:400 clamp(26px,2.6vw,36px)/1.08 var(--display);letter-spacing:-1.3px;margin:12px 0 15px}
|
|
90
|
+
.empty p{max-width:360px;margin:0;font-size:11px;line-height:1.8;color:var(--muted)}
|
|
91
|
+
.empty-symbol{display:inline-flex;width:52px;height:52px;align-items:center;justify-content:center;border:1px solid #e7e9ee;border-radius:14px;background:linear-gradient(150deg,#fff,#f3f4f7);color:#8a94a8;font:24px var(--sans);box-shadow:0 3px 7px #20253604}
|
|
92
|
+
.library-note{margin-top:30px;padding-top:18px;border-top:1px solid var(--line);display:flex;align-items:center;gap:10px;font-size:10px;color:var(--muted)}
|
|
93
|
+
.library-note>span{font:18px var(--display);color:var(--accent)}
|
|
94
|
+
.detail-head{display:flex;justify-content:space-between;align-items:flex-start;gap:25px;margin:15px 0 24px}
|
|
95
|
+
.detail-head h1{font:500 48px/1.05 var(--display);letter-spacing:-2px;max-width:900px}
|
|
96
|
+
.detail-head p{max-width:690px;margin-top:15px;font-size:12px;line-height:1.8;color:var(--muted)}
|
|
97
|
+
.detail-actions{display:flex;gap:9px;flex-wrap:wrap;justify-content:flex-end}
|
|
98
|
+
.meta-row{display:flex;align-items:center;gap:15px;flex-wrap:wrap;font-size:10px;color:var(--muted)}
|
|
99
|
+
.meta-row a{text-underline-offset:3px}
|
|
100
|
+
.meta-row select{font-size:10px;padding:6px;background:var(--white);border:1px solid var(--line);border-radius:3px;max-width:210px}
|
|
101
|
+
.tabs{display:flex;border-bottom:1px solid var(--line);gap:27px;margin:32px 0 28px}
|
|
102
|
+
.tabs button{background:none;color:var(--muted);padding:0 0 14px;border-bottom:2px solid transparent;font-size:12px;white-space:nowrap}
|
|
103
|
+
.tabs button.active{color:var(--accent);border-color:var(--accent);font-weight:600}
|
|
104
|
+
.tabs span{font:9px var(--mono);margin-right:7px;opacity:.6}
|
|
105
|
+
.split{display:grid;grid-template-columns:minmax(0,1fr) 265px;gap:28px}
|
|
106
|
+
.panel{border:1px solid var(--line);background:transparent;border-radius:0;overflow:hidden}
|
|
107
|
+
.panel.pad{padding:24px}
|
|
108
|
+
.panel-top{padding:13px 17px;border-bottom:1px solid var(--line);display:flex;justify-content:space-between;align-items:center;gap:15px;font:9px var(--mono);color:var(--muted)}
|
|
109
|
+
.panel-top select{max-width:100%;border:0;background:transparent;font:inherit;color:var(--ink)}
|
|
110
|
+
.reference-image{display:block;width:100%;height:auto}
|
|
111
|
+
.side-note{margin-bottom:24px}
|
|
112
|
+
.side-note h3{font-size:12px;margin-bottom:11px}
|
|
113
|
+
.side-note p,.side-note li{font-size:11px;line-height:1.8;color:var(--muted)}
|
|
114
|
+
.side-note ul{padding-left:16px;margin:0}
|
|
115
|
+
.side-note .metric{font:550 26px var(--sans);color:var(--ink);display:block;margin-bottom:4px}
|
|
116
|
+
.scope-pill{display:inline-block;padding:5px 7px;background:#ebebe5;border-radius:3px;font:9px var(--mono);margin:3px 3px 3px 0}
|
|
117
|
+
.notice{border-left:3px solid #b6b6ac;background:#eeeee9;padding:15px 18px;font-size:11px;line-height:1.8;color:#5d5d55;margin-bottom:20px}
|
|
118
|
+
.notice.warn{border-color:#ba8e64;background:#f2eadc;color:#8e6741}
|
|
119
|
+
.notice.error{border-color:#bd7457;background:#f5e8df;color:#8f4c35}
|
|
120
|
+
.principles{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:18px;margin-bottom:32px}
|
|
121
|
+
.principle{padding:24px;border:1px solid var(--line);background:transparent;border-radius:0}
|
|
122
|
+
.principle h3{font:500 24px var(--display);letter-spacing:-.6px;margin:10px 0 12px}
|
|
123
|
+
.principle p{font-size:12px;line-height:1.75;color:var(--muted)}
|
|
124
|
+
.principle small{font:9px var(--mono);text-transform:uppercase;color:#777e8d}
|
|
125
|
+
.token-section{margin:28px 0}
|
|
126
|
+
.token-section h3{font:9px var(--mono);letter-spacing:1px;text-transform:uppercase;margin-bottom:15px;color:var(--muted)}
|
|
127
|
+
.swatches{display:grid;grid-template-columns:repeat(auto-fill,minmax(125px,1fr));gap:12px}
|
|
128
|
+
.swatch{background:var(--white);border:1px solid var(--line);border-radius:4px;overflow:hidden}
|
|
129
|
+
.swatch-color{height:65px;border-bottom:1px solid #0001}
|
|
130
|
+
.swatch-info{padding:11px;font:10px var(--mono)}
|
|
131
|
+
.swatch-info b{font-weight:500;display:block;margin-bottom:7px;overflow-wrap:anywhere}
|
|
132
|
+
.swatch-info span{font-size:9px;color:var(--muted);overflow-wrap:anywhere}
|
|
133
|
+
.token-table{width:100%;border-collapse:collapse;font-size:11px}
|
|
134
|
+
.token-table th{text-align:left;font:9px var(--mono);color:var(--muted);padding:11px;border-bottom:1px solid var(--line)}
|
|
135
|
+
.token-table td{padding:14px 11px;border-bottom:1px solid var(--line);vertical-align:top;line-height:1.6;overflow-wrap:anywhere}
|
|
136
|
+
.token-table td:nth-child(2){font:11px/1.6 var(--mono);max-width:250px}
|
|
137
|
+
.token-table td small{display:block;color:var(--muted);font-size:9px}
|
|
138
|
+
.token-table td:nth-child(3){color:var(--muted);max-width:400px}
|
|
139
|
+
.spec{margin-top:32px}
|
|
140
|
+
.spec summary{cursor:pointer;font-size:13px;padding:18px;background:#f3f4f6;border:1px solid var(--line);border-radius:4px}
|
|
141
|
+
.prose{font-size:13px;line-height:1.85;max-width:850px;padding:25px 10px;white-space:pre-wrap;overflow-wrap:anywhere;font-family:var(--sans);margin:0}
|
|
142
|
+
.demo-choices{display:flex;gap:12px;margin-bottom:20px}
|
|
143
|
+
.demo-choice{flex:1;text-align:left;background:transparent;border:1px solid var(--line);border-radius:0;padding:20px;color:var(--ink)}
|
|
144
|
+
.demo-choice.active{border-color:var(--ink);box-shadow:inset 0 3px 0 var(--ink);background:#eeeee8}
|
|
145
|
+
.demo-choice strong{font:500 27px var(--display);letter-spacing:-1px;display:block;margin:8px 0}
|
|
146
|
+
.demo-choice p{font-size:11px;line-height:1.7;color:var(--muted)}
|
|
147
|
+
.demo-choice small{font:9px var(--mono);color:#76766d}
|
|
148
|
+
.device-bar{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border:1px solid var(--line);background:var(--white);border-radius:5px 5px 0 0;font-size:10px;color:var(--muted);gap:12px}
|
|
149
|
+
.segmented{display:flex;gap:3px;background:#eeeee8;padding:3px;border-radius:4px}
|
|
150
|
+
.segmented button{font-size:10px;padding:7px 12px;background:none;border-radius:3px;color:#707069}
|
|
151
|
+
.segmented button.active{background:var(--white);box-shadow:0 1px 3px #2221;color:var(--accent)}
|
|
152
|
+
.demo-canvas{padding:24px;overflow:hidden;background:#e9e9e3;border:1px solid var(--line);border-top:0;border-radius:0}
|
|
153
|
+
.iframe-shell{margin:auto;background:white;box-shadow:0 8px 22px #161c2d14;position:relative;overflow:hidden}
|
|
154
|
+
.iframe-shell iframe{border:0;position:absolute;left:0;top:0;transform-origin:top left;background:white}
|
|
155
|
+
.demo-footnote{font-size:10px;color:var(--muted);line-height:1.7;margin-top:13px}
|
|
156
|
+
.review-grid{display:grid;grid-template-columns:minmax(0,1fr) 330px;gap:28px}
|
|
157
|
+
.review-grid h2{font-size:22px;margin-bottom:12px}
|
|
158
|
+
.review-grid .panel>p{font-size:12px;margin-bottom:20px}
|
|
159
|
+
.review-grid label{font-size:11px;margin-bottom:10px;display:block}
|
|
160
|
+
.review-grid select{display:block;width:100%;padding:11px;border:1px solid var(--line);background:var(--paper);border-radius:4px;margin:10px 0 18px}
|
|
161
|
+
.review-grid textarea{width:100%;min-height:135px;margin:12px 0;border:1px solid var(--line);background:var(--paper);padding:14px;border-radius:4px;resize:vertical;font-size:12px;line-height:1.65}
|
|
162
|
+
.review-grid .button{margin-top:10px}
|
|
163
|
+
.review-grid .fine{margin-top:15px}
|
|
164
|
+
.history{margin-top:28px}
|
|
165
|
+
.history h3{font-size:12px;margin-bottom:15px}
|
|
166
|
+
.history-item{border-left:1px solid #e0e3ea;padding:0 0 18px 18px;font-size:11px;line-height:1.7;color:#697180;position:relative}
|
|
167
|
+
.history-item:before{content:'';position:absolute;left:-4px;top:5px;width:7px;height:7px;border-radius:50%;background:#a2adc4}
|
|
168
|
+
.history-item small{display:block;font:9px var(--mono);color:#858b97;margin-top:4px}
|
|
169
|
+
.progress-card{padding:32px;border:1px solid var(--line);background:var(--white);border-radius:5px}
|
|
170
|
+
.progress-card h2{font-size:26px;margin:15px 0}
|
|
171
|
+
.progress-card p{font-size:12px;max-width:600px;margin-bottom:24px}
|
|
172
|
+
.steps{display:flex;gap:10px;margin:25px 0}
|
|
173
|
+
.step{flex:1;padding:14px;border-top:2px solid #e5e7ed;font:10px var(--mono);color:#8a91a0}
|
|
174
|
+
.step.current{border-color:var(--accent);color:var(--accent)}
|
|
175
|
+
.step.done{border-color:#b6b6ac;color:#68685c}
|
|
176
|
+
.spinner{width:24px;height:24px;border:2px solid #e4e8f1;border-top-color:var(--accent);border-radius:50%;animation:spin 1s linear infinite}
|
|
177
|
+
@keyframes spin{to{transform:rotate(360deg)}
|
|
178
|
+
}
|
|
179
|
+
.error-text{color:#a34e31;font-size:11px;line-height:1.7;margin:12px 0}
|
|
180
|
+
.release-list{display:grid;gap:16px}
|
|
181
|
+
.release-card{display:flex;justify-content:space-between;align-items:center;gap:20px;padding:26px 0;border-top:1px solid var(--ink);border-bottom:1px solid var(--line);border-radius:0;background:transparent}
|
|
182
|
+
.release-card h3{font:500 32px var(--display);letter-spacing:-1px;margin:15px 0}
|
|
183
|
+
.release-card small{font:9px var(--mono);color:var(--muted)}
|
|
184
|
+
.release-card>div:last-child{display:flex;gap:10px}
|
|
185
|
+
dialog{border:1px solid var(--ink);border-radius:0;padding:32px;max-width:560px;width:calc(100% - 32px);background:var(--paper);color:var(--ink);box-shadow:12px 12px 0 #1c1c1a20;max-height:90vh;overflow:auto}
|
|
186
|
+
dialog::backdrop{background:#181c2b59;backdrop-filter:blur(3px)}
|
|
187
|
+
.dialog-top{display:flex;justify-content:space-between;align-items:center;margin-bottom:19px;gap:15px}
|
|
188
|
+
.icon-button{background:none;font-size:24px;color:#7e8797;padding:0 5px;min-width:30px;min-height:30px}
|
|
189
|
+
dialog h2{font:500 40px/1 var(--display);letter-spacing:-1.7px;margin-bottom:16px}
|
|
190
|
+
dialog .muted{font-size:12px;margin-bottom:24px}
|
|
191
|
+
dialog label{display:block;font-size:11px;margin:20px 0 9px;font-weight:500}
|
|
192
|
+
.optional{color:#7e8490;font-size:10px;font-weight:400;margin-left:4px}
|
|
193
|
+
dialog input:not([type=checkbox]),dialog textarea{width:100%;padding:12px 0;background:transparent;border:0;border-bottom:1px solid #bdbdb5;border-radius:0;font-size:13px;line-height:1.6;color:var(--ink)}
|
|
194
|
+
textarea{resize:vertical}
|
|
195
|
+
.capture-explainer{display:flex;align-items:center;justify-content:space-between;gap:10px;margin:24px 0 17px;padding-top:20px;border-top:1px solid var(--line);font:9px var(--mono);color:#7a859b}
|
|
196
|
+
.capture-explainer b{display:block;font:12px var(--sans);color:var(--accent);margin-top:5px}
|
|
197
|
+
.capture-explainer i{font-style:normal}
|
|
198
|
+
.approval-checks{margin:24px 0}
|
|
199
|
+
.approval-checks label{display:flex;gap:11px;align-items:flex-start;font-size:12px;line-height:1.7;font-weight:400}
|
|
200
|
+
.approval-checks input{accent-color:var(--accent);margin-top:4px;flex-shrink:0;width:15px;height:15px}
|
|
201
|
+
#settings-content p{font-size:12px;line-height:1.8;margin:17px 0;color:var(--muted)}
|
|
202
|
+
#settings-content code{display:block;font:10px/1.7 var(--mono);background:var(--paper);padding:12px;overflow-wrap:anywhere;border-radius:4px}
|
|
203
|
+
#toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:var(--ink);color:var(--paper);padding:14px 20px;font-size:12px;max-width:80vw;z-index:100;opacity:0;pointer-events:none;transition:opacity 150ms}
|
|
204
|
+
#toast.show{opacity:1}
|
|
205
|
+
.link-button{background:none;padding:0;color:var(--accent);text-decoration:underline;text-underline-offset:3px;font-size:11px}
|
|
206
|
+
.load-error{padding:25px;background:#f5e6dc;border:1px solid #e1bba4;border-radius:4px;font-size:13px;line-height:1.8}
|
|
207
|
+
.hidden{display:none!important}
|
|
208
|
+
|
|
209
|
+
.brand-mark svg,.brand-mark img{width:36px;height:36px}
|
|
210
|
+
|
|
211
|
+
.nav-icon svg{width:18px;height:18px}
|
|
212
|
+
|
|
213
|
+
.capture-bar svg{width:20px;height:20px}
|
|
214
|
+
|
|
215
|
+
.empty-symbol svg{width:25px;height:25px}
|
|
216
|
+
|
|
217
|
+
.library-note svg{width:16px;height:16px}
|
|
218
|
+
|
|
219
|
+
.capture-bar:focus-within{border-bottom-color:var(--accent);box-shadow:0 1px 0 var(--accent)}
|
|
220
|
+
|
|
221
|
+
.capture-bar input:focus-visible{outline:none}
|
|
222
|
+
/* Text fields use a baseline focus indicator; buttons retain keyboard outlines. */
|
|
223
|
+
dialog input:not([type=checkbox]):focus,dialog textarea:focus,.section-line input:focus{outline:none;border-bottom-color:var(--accent);box-shadow:0 1px 0 var(--accent)}
|
|
224
|
+
.review-grid textarea:focus{outline:none;border-color:var(--accent);box-shadow:inset 3px 0 0 var(--accent)}
|
|
225
|
+
@media(forced-colors:active){dialog input:not([type=checkbox]):focus,dialog textarea:focus,.capture-bar:focus-within,.section-line input:focus{outline:2px solid Highlight;outline-offset:2px}}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
input::placeholder,textarea::placeholder{color:#85857c;opacity:1}
|
|
229
|
+
|
|
230
|
+
input,textarea{caret-color:var(--accent)}
|
|
231
|
+
|
|
232
|
+
#main:focus{outline:none}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
/* The collection is a working contact sheet; its contents provide the imagery. */
|
|
236
|
+
.nav-index{font:9px var(--mono);color:#88887f;align-self:flex-start;margin-top:8px}
|
|
237
|
+
nav a.active:after{content:'';position:absolute;bottom:-21px;left:0;right:0;height:3px;background:var(--accent)}
|
|
238
|
+
.collection-page{padding-top:44px}
|
|
239
|
+
.collection-intro{display:grid;grid-template-columns:minmax(0,1fr) 310px;align-items:center;gap:60px;min-height:290px}
|
|
240
|
+
.collection-intro h1{font:500 clamp(62px,6.6vw,105px)/.94 var(--display);letter-spacing:-5.5px;margin:24px 0 23px}
|
|
241
|
+
.collection-intro h1>span{color:var(--accent)}
|
|
242
|
+
.collection-intro p{font-size:12px;line-height:1.75;color:var(--muted)}
|
|
243
|
+
.index-dot{display:inline-block;width:6px;height:6px;background:var(--accent);border-radius:50%;margin-right:10px}
|
|
244
|
+
.collection-aside{position:relative;padding:10px 25px 10px 0}
|
|
245
|
+
.kept-specimen{display:block;position:relative;text-decoration:none;max-width:270px;margin:10px auto}
|
|
246
|
+
.specimen-print{padding:13px;background:#fff;border:1px solid #dadad3;transform:rotate(5deg);box-shadow:2px 5px 0 #deded640;transition:transform 250ms}
|
|
247
|
+
.kept-specimen:hover .specimen-print,.kept-specimen:focus-visible .specimen-print{transform:rotate(0deg) translateY(-3px)}
|
|
248
|
+
.specimen-print:before{content:'';position:absolute;inset:7px -8px -8px 5px;border:1px solid #d5d5cd;background:#eeeee7;z-index:-1;transform:rotate(-9deg)}
|
|
249
|
+
.specimen-top{display:flex;justify-content:space-between;font:8px var(--mono);letter-spacing:.5px;margin-bottom:14px;color:var(--muted)}
|
|
250
|
+
.specimen-print img{display:block;width:100%;height:155px;object-fit:cover;object-position:top;border:1px solid var(--line);filter:saturate(.7)}
|
|
251
|
+
.specimen-caption{font:10px var(--mono);margin-top:12px;line-height:1.5;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}
|
|
252
|
+
.specimen-stamp{display:grid;place-content:center;position:absolute;width:67px;height:67px;border-radius:50%;right:-23px;bottom:-21px;background:var(--accent);color:#fff;text-align:center;font:10px/1.6 var(--mono);transform:rotate(-12deg)}
|
|
253
|
+
.archive-mark{position:relative;height:230px;max-width:270px;margin:auto}
|
|
254
|
+
.archive-mark>span{position:absolute;width:145px;height:185px;border:1px solid #9a9a8e;left:65px;top:10px;background:var(--paper);transform:rotate(15deg)}
|
|
255
|
+
.archive-mark>span:nth-child(2){transform:rotate(-10deg);left:40px}
|
|
256
|
+
.archive-mark>span:nth-child(3){transform:rotate(0deg);left:52px;top:20px;background:repeating-linear-gradient(0deg,transparent,transparent 13px,#d7d7cf 13px,#d7d7cf 14px)}
|
|
257
|
+
.archive-mark i{position:absolute;bottom:0;left:0;right:0;text-align:center;font:8px var(--mono);letter-spacing:.5px;color:var(--muted)}
|
|
258
|
+
.capture-bar label{font:9px var(--mono);letter-spacing:.6px;flex:0 0 170px}
|
|
259
|
+
.capture-bar label span{font:11px var(--sans);letter-spacing:0;color:var(--muted);display:block;margin-top:8px}
|
|
260
|
+
.capture-bar .button{padding:13px 17px;min-height:46px}
|
|
261
|
+
.capture-bar .button>span{font-size:21px;line-height:1}
|
|
262
|
+
.card-index{display:flex;align-items:center;justify-content:space-between;border-top:1px solid var(--ink);padding:10px 0;font:9px var(--mono);color:var(--muted)}
|
|
263
|
+
.empty:before,.empty:after{content:'+';position:absolute;font:14px var(--mono);color:#8b8b81;background:var(--paper);width:14px;height:14px;text-align:center;line-height:14px}
|
|
264
|
+
.empty:before{top:-7px;left:-7px}.empty:after{bottom:-7px;right:-7px}
|
|
265
|
+
.empty-number{font:400 104px/1 var(--display);letter-spacing:-10px;color:#c9c9c0;align-self:center}
|
|
266
|
+
.empty .button{margin-top:20px}
|
|
267
|
+
.desk-process{list-style:none;margin:0;padding:4px 0 4px 35px;border-left:1px solid var(--line);display:grid;gap:22px}
|
|
268
|
+
.desk-process li{display:flex;gap:17px;font-size:11px;align-items:baseline}
|
|
269
|
+
.desk-process li>span{font:9px var(--mono);color:var(--accent)}
|
|
270
|
+
.desk-process small{display:block;font-size:10px;color:var(--muted);margin-top:6px}
|
|
271
|
+
.library-note .note-end{margin-left:auto;font:8px var(--mono);letter-spacing:.5px;color:var(--muted)}
|
|
272
|
+
.device-bar,.segmented,.segmented button,.swatch,.scope-pill,.spec summary,.progress-card,.review-grid select,.review-grid textarea,#settings-content code{border-radius:0}
|
|
273
|
+
.device-bar,.spec summary,.progress-card{background:var(--paper)}
|
|
274
|
+
.review-grid h2{font:500 28px/1.1 var(--display)}
|
|
275
|
+
.tabs button.active{color:var(--ink);border-color:var(--accent)}
|
|
276
|
+
|
|
277
|
+
#install-description{overflow-wrap:anywhere;font:11px/1.7 var(--mono)}
|
|
278
|
+
.install-local-label{margin:25px 0 12px;font-size:8px}
|
|
279
|
+
.install-agent{display:flex;align-items:center;gap:16px;border-top:1px solid var(--line);padding:21px 0}
|
|
280
|
+
.install-agent:last-child{border-bottom:1px solid var(--line)}
|
|
281
|
+
.agent-index{font:9px var(--mono);color:var(--accent);align-self:flex-start;margin-top:5px}
|
|
282
|
+
.agent-description{min-width:0;flex:1}.agent-description h3{font:500 23px var(--display);letter-spacing:-.8px}.agent-description p{font-size:10px;color:var(--muted);line-height:1.6;margin-top:6px}
|
|
283
|
+
.install-agent .button{min-width:100px;flex-shrink:0}.install-footnote{margin:15px 0 23px}
|
|
284
|
+
.install-remote{border-top:1px solid var(--line);border-bottom:1px solid var(--line)}
|
|
285
|
+
.install-remote summary{padding:17px 0;cursor:pointer;font:500 13px var(--sans)}.install-remote summary span{float:right}
|
|
286
|
+
.remote-option{padding:5px 0 23px}.remote-option h3{font:500 22px var(--display)}.remote-option p{font-size:11px;line-height:1.8;color:var(--muted);margin-top:10px}.remote-option code{font-size:10px}
|
|
287
|
+
.handoff-actions{display:flex;align-items:center;gap:18px;margin:14px 0}.install-locations{margin-top:20px}.install-locations summary{font-size:10px;color:var(--muted);cursor:pointer}.install-locations p{font-size:10px;margin:14px 0}.install-locations code{display:block;font:9px/1.7 var(--mono);overflow-wrap:anywhere;color:var(--muted);margin-top:5px}#handoff-prompt{margin-top:20px}
|
|
288
|
+
@media(max-width:540px){.install-agent{gap:10px}.agent-description h3{font-size:21px}.install-agent .button{font-size:9px;min-width:82px;padding-inline:8px;gap:8px}.handoff-actions{gap:14px;flex-wrap:wrap}.install-local-label{font-size:7px}}
|
|
289
|
+
@media(min-width:1500px){.sidebar,.topbar{padding-inline:max(4.5%,calc((100vw - 1492px)/2))}}
|
|
290
|
+
@media(max-width:1100px){
|
|
291
|
+
.sidebar{gap:20px;padding-inline:30px}.brand{font-size:21px}.brand small{font-size:8px}nav{gap:20px}nav a{font-size:11px}.sidebar .text-button{font-size:0}.text-button span{font-size:18px}
|
|
292
|
+
main{padding-inline:30px}.topbar{padding-inline:30px}.collection-intro{grid-template-columns:minmax(0,1fr) 240px;gap:30px}.collection-intro h1{font-size:76px;letter-spacing:-4px}
|
|
293
|
+
.collection-aside{padding-right:22px}.specimen-print img{height:130px}.capture-bar label{flex-basis:145px}.capture-bar input{font-size:15px}.empty{grid-template-columns:95px minmax(0,1fr) 210px;gap:28px;padding:34px 24px}.empty-number{font-size:80px;letter-spacing:-7px}.desk-process{padding-left:20px}
|
|
294
|
+
.split{grid-template-columns:minmax(0,1fr) 220px}.review-grid{grid-template-columns:minmax(0,1fr) 290px}.detail-head{display:block}.detail-actions{justify-content:flex-start;margin-top:20px}
|
|
295
|
+
}
|
|
296
|
+
@media(max-width:800px){
|
|
297
|
+
.sidebar{flex-wrap:wrap;padding:23px 24px 0;gap:20px}.brand{font-size:24px}.sidebar .text-button{order:1;margin-left:auto;font-size:10px}nav{order:2;width:100%;gap:30px}nav a{font-size:12px;padding:13px 0 18px}.nav-index{margin-top:2px}nav a.active:after{bottom:-1px}.brand small{font-size:8px}
|
|
298
|
+
.topbar{padding:12px 24px;min-height:58px}.topbar .connection{display:none}main{padding:30px 24px}.collection-intro{grid-template-columns:minmax(0,1fr) 190px;gap:30px;min-height:250px}.collection-intro h1{font-size:62px;letter-spacing:-3px}.specimen-print{padding:9px}.specimen-print img{height:100px}.specimen-caption{font-size:8px}.specimen-stamp{width:51px;height:51px;font-size:8px;right:-15px;bottom:-15px}.specimen-top{font-size:6px}.archive-mark>span{width:110px;height:155px;left:30px}.archive-mark>span:nth-child(2){left:20px}.archive-mark>span:nth-child(3){left:30px}.archive-mark{height:210px}
|
|
299
|
+
.capture-bar{gap:15px}.capture-bar label{flex-basis:125px;font-size:8px}.capture-bar label span{font-size:10px}.capture-bar input{font-size:13px}.capture-bar .button{font-size:10px;gap:14px;padding-inline:12px}.empty{grid-template-columns:80px minmax(0,1fr);gap:25px}.desk-process{grid-column:1/-1;border-left:0;border-top:1px solid var(--line);padding:23px 0 0;display:flex;justify-content:space-between;gap:20px}.empty-number{font-size:70px}.library-note .note-end{display:none}
|
|
300
|
+
.split,.review-grid{grid-template-columns:1fr}.split>aside{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:20px}.demo-choice strong{font-size:23px}.release-card{align-items:flex-start}.release-card>div:last-child{flex-direction:column}
|
|
301
|
+
}
|
|
302
|
+
@media(max-width:540px){
|
|
303
|
+
.sidebar{padding:20px 20px 0;gap:16px}.brand{font-size:22px;gap:9px}.brand-mark,.brand-mark svg,.brand-mark img{width:28px;height:32px}.brand small{font-size:7px;letter-spacing:.3px}.sidebar .text-button{font-size:0}.sidebar .text-button span{font-size:18px}nav{gap:22px;justify-content:space-between}nav a{font-size:10px;gap:5px}.nav-index{font-size:7px;margin-top:2px}.count{font-size:9px}
|
|
304
|
+
.topbar{padding:10px 20px;gap:10px}.topbar>span{font-size:7px;letter-spacing:0;max-width:145px;line-height:1.6}.topbar .button{font-size:9px;padding:8px 10px;gap:5px}.topbar>div{gap:0}
|
|
305
|
+
main{padding:30px 20px}.collection-intro{grid-template-columns:minmax(0,1fr);gap:16px;min-height:0;position:relative}.collection-intro .eyebrow{font-size:8px;letter-spacing:.4px}.collection-intro h1{font-size:clamp(54px,12.8vw,69px);letter-spacing:-3.5px;margin:23px 0 20px}.collection-intro p{font-size:11px;max-width:235px}.collection-aside{position:absolute;width:92px;right:0;bottom:8px;padding:0;pointer-events:auto}.kept-specimen{margin:0}.specimen-print{padding:5px}.specimen-top{font-size:4px;letter-spacing:0;margin-bottom:5px}.specimen-print img{height:56px}.specimen-caption{font-size:4px;margin-top:5px}.specimen-stamp{width:30px;height:30px;font-size:5px;right:-5px;bottom:-9px}.archive-mark{display:none}
|
|
306
|
+
.capture-bar{flex-wrap:wrap;gap:0 10px;margin:35px 0 30px;padding:14px 0 10px}.capture-bar label{flex-basis:100%;display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;font-size:8px}.capture-bar label span{margin:0;font-size:9px}.capture-bar input{font-size:12px;padding:12px 0}.capture-bar .button{font-size:9px;min-height:38px;padding:8px;gap:9px}.capture-bar .button>span{font-size:17px}
|
|
307
|
+
.section-line{gap:10px}.section-line h2{font-size:9px}.section-line input{width:130px;font-size:9px}.section-line>.eyebrow{display:none}.cards{grid-template-columns:1fr;gap:26px}.card-image{height:230px}.empty{grid-template-columns:55px minmax(0,1fr);gap:18px;padding:28px 18px}.empty-number{font-size:53px;letter-spacing:-5px}.empty h2{font-size:26px;letter-spacing:-1px;margin-top:10px}.empty .eyebrow{font-size:7px;letter-spacing:.5px}.empty p{font-size:10px}.desk-process{gap:12px;flex-direction:column;padding-top:20px}.desk-process li{font-size:11px;gap:16px}.desk-process small{display:inline;margin-left:10px;font-size:9px}.library-note{font-size:9px;line-height:1.6;margin-top:25px}
|
|
308
|
+
.detail-head h1{font-size:36px;letter-spacing:-1.5px}.detail-head p{font-size:11px}.detail-actions{gap:7px}.meta-row{font-size:9px;gap:9px}.tabs{gap:20px;overflow:auto;margin-top:25px}.tabs button{font-size:10px}.tabs span{display:none}.principles{grid-template-columns:1fr}.principle{padding:20px}.token-table td,.token-table th{padding:11px 5px;font-size:10px}.token-table td:nth-child(3),.token-table th:nth-child(3){display:none}.demo-choices{flex-direction:column;gap:10px}.demo-choice{padding:17px}.demo-choice strong{font-size:24px}.demo-canvas{padding:9px}.device-bar{padding:10px;flex-wrap:wrap}.demo-footnote{font-size:9px}.split>aside{display:block}.review-grid .panel.pad{padding:20px}.release-card{display:block}.release-card>div:last-child{flex-direction:row;margin-top:20px}.release-card h3{font-size:28px}.progress-card{padding:22px}.steps{gap:5px}.step{font-size:8px;padding:10px 3px}dialog{padding:24px}dialog h2{font-size:35px}.capture-explainer{font-size:8px}.capture-explainer b{font-size:10px}.approval-checks label{font-size:11px}#toast{width:max-content;font-size:11px}
|
|
309
|
+
}
|
|
310
|
+
@media(max-width:360px){.collection-aside{display:none}.collection-intro h1{font-size:54px}nav{gap:12px}.desk-process small{display:block;margin:5px 0 0}.capture-bar input{font-size:11px}.empty h2{font-size:24px}}
|
|
311
|
+
@media(prefers-reduced-motion:reduce){*,*:before,*:after{animation:none!important;transition:none!important;scroll-behavior:auto!important}}
|
|
312
|
+
/* Account onboarding follows the same paper-and-ink workshop language. */
|
|
313
|
+
button.connection{border:0;background:none;padding:6px 0;font:9px var(--mono);color:var(--muted);cursor:pointer;text-align:left}
|
|
314
|
+
#settings-dialog{max-width:620px}#settings-dialog h2{font-size:42px;line-height:1.05;letter-spacing:-1.7px}
|
|
315
|
+
.setup-intro{color:var(--muted);line-height:1.7;margin:18px 0 28px}.setup-connection{border-top:1px solid var(--ink);padding-top:20px}.setup-step{display:flex;justify-content:space-between;gap:20px;font:9px var(--mono);letter-spacing:.04em;color:var(--muted)}.setup-state{text-transform:uppercase}.setup-state.ready{color:var(--ink)}.setup-connection h3{font:500 25px var(--display);letter-spacing:-.7px;margin:22px 0 12px}.setup-account{font-size:13px;margin-bottom:12px;overflow-wrap:anywhere}.setup-account span{color:var(--muted)}.setup-connection>.muted{font-size:12px;line-height:1.7}.setup-actions{display:flex;flex-wrap:wrap;align-items:center;gap:18px;margin:24px 0}.setup-actions .fine{flex-basis:100%;margin:0}.setup-browser{border-top:1px solid var(--line);border-bottom:1px solid var(--line);padding:20px 0}.setup-browser p{font-size:12px;line-height:1.7;margin:13px 0 0}.setup-browser .text-button,.setup-browser .link-button{margin:15px 20px 0 0}.setup-data-note{margin:20px 0}.setup-storage{margin-top:24px;border-top:1px solid var(--line);padding-top:20px;font-size:12px}.setup-storage summary{cursor:pointer;font:10px var(--mono)}.setup-storage p{margin:18px 0 8px}.setup-storage code{display:block;overflow-wrap:anywhere;font-size:10px}.setup-storage a{display:inline-block;margin-top:15px}#setup-error:empty{display:none}#setup-error{margin:15px 0}
|
|
316
|
+
@media(max-width:600px){#settings-dialog h2{font-size:34px}.setup-connection h3{font-size:23px}.setup-actions .button{width:100%}.setup-step{font-size:8px;gap:10px}}
|
|
317
|
+
|
|
318
|
+
.elapsed{font:13px var(--mono,ui-monospace,monospace);color:var(--muted,#6b7280);font-variant-numeric:tabular-nums;font-weight:400;vertical-align:middle}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import {prepareLibrary} from '../server/library.js';
|
|
2
|
+
import {ROOT,DATA as libraryData,APPROVED as libraryApproved} from '../server/config.js';
|
|
3
|
+
import fs from 'node:fs';import path from 'node:path';import {DATA,APPROVED} from '../server/config.js';import {openStore} from '../server/store.js';import {inventory,atomic} from '../server/files.js';
|
|
4
|
+
await prepareLibrary({root:ROOT,data:libraryData,approved:libraryApproved});
|
|
5
|
+
fs.mkdirSync(DATA,{recursive:true});const lock=path.join(DATA,'server.lock');
|
|
6
|
+
if(fs.existsSync(lock)){let running=false;try{process.kill(Number(fs.readFileSync(lock,'utf8')),0);running=true;}catch{}if(running)throw new Error('Stop Antisketch before making a consistent backup, then run npm run backup again.');fs.rmSync(lock,{force:true});}
|
|
7
|
+
fs.writeFileSync(lock,String(process.pid),{flag:'wx'});let store;
|
|
8
|
+
try{const stamp=new Date().toISOString().replace(/[:.]/g,'-'),destination=path.resolve(process.argv[2]||path.join(DATA,'backups',stamp));for(const source of [path.join(DATA,'designs'),APPROVED])if(destination===source||destination.startsWith(source+path.sep))throw new Error('Backup destination cannot be inside the files being backed up');if(fs.existsSync(destination))throw new Error('Choose a new backup destination');fs.mkdirSync(destination,{recursive:true});store=openStore();await store.db.backup(path.join(destination,'antisketch.sqlite'));for(const [source,name] of [[path.join(DATA,'designs'),'designs'],[APPROVED,'approved']])if(fs.existsSync(source))fs.cpSync(source,path.join(destination,name),{recursive:true});atomic(path.join(destination,'backup-manifest.json'),{formatVersion:1,createdAt:new Date().toISOString(),files:inventory(destination)});console.log(`Backup complete: ${destination}`);}finally{store?.close();fs.rmSync(lock,{force:true});}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import {databasePath} from '../server/compat.js';
|
|
2
|
+
import fs from 'node:fs';import path from 'node:path';import {DATA,APPROVED} from '../server/config.js';import {readJSON,contained,hash} from '../server/files.js';
|
|
3
|
+
const source=process.argv[2]?path.resolve(process.argv[2]):null;if(!source)throw new Error('Usage: npm run restore -- /absolute/path/to/backup');
|
|
4
|
+
for(const destination of [DATA,APPROVED])if(fs.existsSync(destination)&&fs.readdirSync(destination).length)throw new Error(`Restore requires an empty destination. Stop the app and move ${destination} aside first.`);
|
|
5
|
+
const manifest=readJSON(path.join(source,'backup-manifest.json'));if(manifest.formatVersion!==1||!manifest.files?.length)throw new Error('Unsupported backup format');for(const file of manifest.files){const full=contained(source,file.path);if(fs.lstatSync(full).isSymbolicLink()||hash(full)!==file.sha256)throw new Error('Backup integrity check failed: '+file.path);}
|
|
6
|
+
const database=databasePath(source);if(!manifest.files.some(f=>f.path===path.basename(database)))throw new Error('Backup database is missing from its manifest');
|
|
7
|
+
fs.mkdirSync(DATA,{recursive:true});fs.copyFileSync(database,path.join(DATA,'antisketch.sqlite'));if(fs.existsSync(path.join(source,'designs')))fs.cpSync(path.join(source,'designs'),path.join(DATA,'designs'),{recursive:true});if(fs.existsSync(path.join(source,'approved')))fs.cpSync(path.join(source,'approved'),APPROVED,{recursive:true});console.log(`Restored working library to ${DATA}\nApproved skills to ${APPROVED}`);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {browserStatus} from './setup.js';
|
|
2
|
+
import {LEGACY} from './compat.js';
|
|
3
|
+
import express from 'express';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import crypto from 'node:crypto';
|
|
7
|
+
import {z} from 'zod';
|
|
8
|
+
import yazl from 'yazl';
|
|
9
|
+
import {ROOT,VERSION,DATA,DESIGNS,APPROVED,ACTIVE,now} from './config.js';
|
|
10
|
+
import {readJSON,inventory,hash,contained,atomic} from './files.js';
|
|
11
|
+
import {validateURL} from './network.js';
|
|
12
|
+
import {providerStatus} from './interpret.js';
|
|
13
|
+
import {revisionDir} from './worker.js';
|
|
14
|
+
import {buildRelease,DEMO_CSP} from './package.js';
|
|
15
|
+
import {installationOptions,installRelease} from './install.js';
|
|
16
|
+
export function createApp(store,worker,{designsDir=DESIGNS,approvedDir=APPROVED,validateSource=validateURL,getProvider=providerStatus,installHome,connection=null,getBrowser=browserStatus}={}){
|
|
17
|
+
const app=express();app.disable('x-powered-by');const csrf=crypto.randomBytes(32).toString('hex');
|
|
18
|
+
app.use((req,res,next)=>{const host=req.headers.host||'';if(!/^(127\.0\.0\.1|localhost)(:\d+)?$/.test(host))return res.status(403).json({error:'Antisketch is available only on this machine'});res.set({'X-Content-Type-Options':'nosniff','Referrer-Policy':'no-referrer','X-Frame-Options':'SAMEORIGIN','Cache-Control':'no-store'});const origin=req.headers.origin;if(origin&&!['http://'+host].includes(origin))return res.status(403).json({error:'Cross-origin access is not allowed'});if(!['GET','HEAD','OPTIONS'].includes(req.method)&&(req.headers['x-antisketch-token']??req.headers[LEGACY.csrf])!==csrf)return res.status(403).json({error:'Refresh Antisketch before saving changes'});next();});
|
|
19
|
+
app.use(express.json({limit:'64kb'}));
|
|
20
|
+
app.get('/api/health',(req,res)=>res.json({app:'antisketch',version:VERSION,status:'ready'}));
|
|
21
|
+
app.get('/api/session',async(req,res)=>res.json({token:csrf,provider:await getProvider(),storage:{data:DATA,approved:approvedDir},mode:'local-only'}));
|
|
22
|
+
app.get('/api/setup',async(req,res)=>res.json({provider:await getProvider(),browser:await getBrowser({fresh:req.query.refresh==='1'}),storage:{data:DATA,approved:approvedDir}}));
|
|
23
|
+
app.post('/api/connection/:action',async(req,res)=>{
|
|
24
|
+
if(!connection)return res.status(503).json({error:'Connection management is unavailable.'});
|
|
25
|
+
const action=z.enum(['login','use-existing','cancel','disconnect']).parse(req.params.action);
|
|
26
|
+
if(action==='disconnect'&&store.list().some(d=>ACTIVE.includes(d.status)))return res.status(409).json({error:'Finish or cancel queued preparations before disconnecting.'});
|
|
27
|
+
const result=await ({login:()=>connection.login(),'use-existing':()=>connection.useExisting(),cancel:()=>connection.cancel(),disconnect:()=>connection.disconnect()}[action])();res.json(result);
|
|
28
|
+
});
|
|
29
|
+
app.get('/api/designs',(req,res)=>res.json(store.list()));
|
|
30
|
+
app.post('/api/designs',async(req,res)=>{if(connection)await connection.requireReady();const body=z.object({url:z.string().min(3).max(2048),notes:z.string().max(4000).default('')}).parse(req.body);const url=await validateSource(body.url);const duplicate=store.list().find(d=>d.url===url&&!d.archived);if(duplicate)return res.status(409).json({error:'This website is already in your collection',id:duplicate.id});if(store.list().filter(d=>ACTIVE.includes(d.status)).length>=10)return res.status(429).json({error:'Your queue is full. Let a few captures finish first.'});const d=store.create(url,body.notes);worker.wake();res.status(201).json(d);});
|
|
31
|
+
app.get('/api/designs/:id',(req,res)=>{const d=store.detail(req.params.id);if(!d)return res.status(404).json({error:'Design not found'});const n=req.query.revision?Number(req.query.revision):d.current_revision;const r=store.revision(d.id,n);if(!r)return res.status(404).json({error:'Revision not found'});const dir=revisionDir(d.id,n,designsDir);const file=name=>fs.existsSync(path.join(dir,name))?readJSON(path.join(dir,name)):null;res.json({...d,selectedRevision:r,summary:file('summary.json'),capture:((c)=>c?{...c,pages:undefined}:null)(file('capture.json')),tokens:file('tokens.json'),validation:file('validation.json'),specification:fs.existsSync(path.join(dir,'specification.md'))?fs.readFileSync(path.join(dir,'specification.md'),'utf8'):null});});
|
|
32
|
+
app.post('/api/designs/:id/revise',async(req,res)=>{if(connection)await connection.requireReady();const body=z.object({feedback:z.string().max(5000).default(''),expectedRevision:z.number().int().positive()}).parse(req.body);const d=store.design(req.params.id);if(!d)return res.status(404).json({error:'Design not found'});if(d.current_revision!==body.expectedRevision)return res.status(409).json({error:'A newer revision exists. Refresh before continuing.'});const n=store.revise(d.id,body.feedback);worker.wake();res.status(201).json({revision:n});});
|
|
33
|
+
app.post('/api/designs/:id/cancel',(req,res)=>{const d=store.design(req.params.id);if(!d)return res.status(404).json({error:'Design not found'});worker.cancel(d.id,d.current_revision);res.json({ok:true});});
|
|
34
|
+
app.post('/api/designs/:id/archive',(req,res)=>{const body=z.object({archived:z.boolean()}).parse(req.body);const d=store.design(req.params.id);if(!d)return res.status(404).json({error:'Design not found'});if(ACTIVE.includes(store.revision(d.id,d.current_revision).status))return res.status(409).json({error:'Cancel preparation before archiving'});store.db.prepare('UPDATE designs SET archived=?,updated_at=? WHERE id=?').run(Number(body.archived),now(),d.id);res.json({ok:true});});
|
|
35
|
+
app.post('/api/designs/:id/approve',(req,res)=>{
|
|
36
|
+
const body=z.object({revision:z.number().int().positive(),selectedDemo:z.number().int().min(1).max(2),note:z.string().max(3000).default(''),checks:z.object({reference:z.literal(true),transfer:z.literal(true),limits:z.literal(true)})}).parse(req.body);const d=store.design(req.params.id);if(!d)return res.status(404).json({error:'Design not found'});
|
|
37
|
+
const existing=store.db.prepare('SELECT * FROM releases WHERE design_id=? AND revision=?').get(d.id,body.revision);if(existing)return res.json(existing);
|
|
38
|
+
const r=store.revision(d.id,body.revision);if(d.current_revision!==body.revision||r?.status!=='review')return res.status(409).json({error:'Only the current review-ready revision can be approved'});
|
|
39
|
+
const sourceDir=revisionDir(d.id,body.revision,designsDir);const validation=readJSON(path.join(sourceDir,'validation.json'));if(!validation.passed)return res.status(409).json({error:'Resolve the demo validation issues before approving'});
|
|
40
|
+
const version=store.db.prepare('SELECT COUNT(*) AS n FROM releases WHERE design_id=?').get(d.id).n+1;const folder=path.join(d.id,'v'+version),releaseDir=contained(approvedDir,folder);const id=crypto.randomUUID();
|
|
41
|
+
buildRelease({design:d,revision:body.revision,version,selectedDemo:body.selectedDemo,note:body.note,checks:body.checks,sourceDir,releaseDir});
|
|
42
|
+
try{store.db.transaction(()=>{store.db.prepare('INSERT INTO releases VALUES(?,?,?,?,?,?)').run(id,d.id,body.revision,version,folder,now());store.stage(d.id,body.revision,'approved',`Approved as skill version ${version}`);})();}catch(e){fs.rmSync(releaseDir,{recursive:true,force:true});throw e;}
|
|
43
|
+
res.status(201).json(store.db.prepare('SELECT * FROM releases WHERE id=?').get(id));
|
|
44
|
+
});
|
|
45
|
+
app.get('/api/releases',(req,res)=>res.json(store.db.prepare('SELECT r.*,d.name FROM releases r JOIN designs d ON d.id=r.design_id ORDER BY r.created_at DESC').all()));
|
|
46
|
+
app.get('/api/releases/:id/install',(req,res)=>{const r=store.db.prepare('SELECT * FROM releases WHERE id=?').get(req.params.id);if(!r)return res.status(404).json({error:'Approved release not found'});res.json(installationOptions(r,approvedDir,{home:installHome}));});
|
|
47
|
+
app.post('/api/releases/:id/install',(req,res)=>{const body=z.object({target:z.enum(['codex','claude'])}).strict().parse(req.body);const r=store.db.prepare('SELECT * FROM releases WHERE id=?').get(req.params.id);if(!r)return res.status(404).json({error:'Approved release not found'});res.json(installRelease(r,approvedDir,body.target,{home:installHome}));});
|
|
48
|
+
app.get('/api/releases/:id/download',(req,res)=>{const r=store.db.prepare('SELECT * FROM releases WHERE id=?').get(req.params.id);if(!r)return res.status(404).json({error:'Release not found'});const dir=contained(approvedDir,r.folder);const manifest=readJSON(path.join(dir,'manifest.json'));for(const file of manifest.files)if(hash(contained(dir,file.path))!==file.sha256)return res.status(409).json({error:'Release integrity check failed. Restore this release from a backup.'});const zip=new yazl.ZipFile();const target=z.enum(['portable','devin']).parse(req.query.target||'portable');const prefix=(target==='devin'?'.agents/skills/':'')+manifest.name;for(const f of inventory(dir))zip.addFile(contained(dir,f.path),prefix+'/'+f.path);res.type('zip');res.setHeader('Content-Disposition',`attachment; filename="${manifest.name}-v${r.version}${target==='devin'?'-devin':''}.zip"`);zip.outputStream.on('error',()=>res.destroy());zip.outputStream.pipe(res);zip.end();});
|
|
49
|
+
app.get('/files/:id/:revision/*file',(req,res)=>{const n=Number(req.params.revision);if(!store.revision(req.params.id,n))return res.status(404).end();const name=req.params.file.join('/');if(!/^(evidence\/[a-zA-Z0-9_.-]+\.(png|json)|demo-[12](?:-(1440|390)\.png|\.html)|tokens(?:\.dtcg)?\.json|specification\.md|validation\.json)$/.test(name))return res.status(404).end();const file=contained(revisionDir(req.params.id,n,designsDir),name);if(!fs.existsSync(file))return res.status(404).end();if(name.endsWith('.html'))res.setHeader('Content-Security-Policy',DEMO_CSP);res.sendFile(file,{dotfiles:'allow'});});
|
|
50
|
+
app.use((req,res,next)=>{res.setHeader('Content-Security-Policy',"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-src 'self'; frame-ancestors 'self'; base-uri 'none'; form-action 'self'");next();});
|
|
51
|
+
app.get('/brand.html',(req,res)=>res.sendFile(fs.existsSync(path.join(ROOT,'app/public/brand.html'))?path.join(ROOT,'app/public/brand.html'):path.join(ROOT,'app/distribution/brand.html')));
|
|
52
|
+
app.use(express.static(path.join(ROOT,'app','public'),{etag:true,maxAge:0}));
|
|
53
|
+
app.use((req,res)=>res.status(404).json({error:'Not found'}));
|
|
54
|
+
app.use((error,req,res,next)=>{if(res.headersSent)return next(error);const validation=error instanceof z.ZodError;res.status(validation?400:400).json({error:validation?error.issues.map(i=>`${i.path.join('.')}: ${i.message}`).join('; '):error.message||'Unable to complete this action'});});return app;
|
|
55
|
+
}
|