stenotype 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,357 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Stenotype · Sign In</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,400;0,9..144,600;1,9..144,400&family=Geist:wght@300;400;500&display=swap" rel="stylesheet" />
10
+ <link rel="stylesheet" href="/styles.css" />
11
+ <style>
12
+ :root {
13
+ --bg: #FAFAF8;
14
+ --accent: #C9CB24;
15
+ --accent-hover: #b3b51e;
16
+ --text: #1A1A18;
17
+ --muted: #6B6B66;
18
+ --border: #E0DED9;
19
+ --input-bg: #F4F3EF;
20
+ --shadow-sm: 0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04);
21
+ --shadow-md: 0 4px 16px rgba(0,0,0,0.08), 0 1px 4px rgba(0,0,0,0.04);
22
+ }
23
+
24
+ * { box-sizing: border-box; margin: 0; padding: 0; }
25
+
26
+ body {
27
+ font-family: 'Geist', -apple-system, sans-serif;
28
+ background-color: var(--bg);
29
+ color: var(--text);
30
+ min-height: 100vh;
31
+ display: flex;
32
+ align-items: center;
33
+ justify-content: center;
34
+ position: relative;
35
+ overflow: hidden;
36
+ }
37
+
38
+ /* Subtle noise texture overlay */
39
+ body::before {
40
+ content: '';
41
+ position: fixed;
42
+ inset: 0;
43
+ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.75' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='0.04'/%3E%3C/svg%3E");
44
+ pointer-events: none;
45
+ opacity: 0.5;
46
+ z-index: 0;
47
+ }
48
+
49
+ /* Decorative background blobs */
50
+ .bg-blob {
51
+ position: fixed;
52
+ border-radius: 50%;
53
+ filter: blur(80px);
54
+ opacity: 0.12;
55
+ pointer-events: none;
56
+ z-index: 0;
57
+ }
58
+ .bg-blob-1 {
59
+ width: 400px; height: 400px;
60
+ background: var(--accent);
61
+ top: -100px; right: -100px;
62
+ }
63
+ .bg-blob-2 {
64
+ width: 300px; height: 300px;
65
+ background: #8B7CF6;
66
+ bottom: -80px; left: -80px;
67
+ }
68
+
69
+ .login-container {
70
+ position: relative;
71
+ z-index: 1;
72
+ width: 100%;
73
+ max-width: 420px;
74
+ padding: 0 24px;
75
+ }
76
+
77
+ .login-card {
78
+ background: rgba(255,255,255,0.8);
79
+ backdrop-filter: blur(20px);
80
+ border: 1px solid rgba(255,255,255,0.9);
81
+ border-radius: 20px;
82
+ padding: 48px 40px 40px;
83
+ box-shadow: var(--shadow-md), 0 0 0 1px rgba(0,0,0,0.04);
84
+ }
85
+
86
+ .brand {
87
+ text-align: center;
88
+ margin-bottom: 32px;
89
+ }
90
+
91
+ .brand-wordmark {
92
+ font-family: 'Fraunces', Georgia, serif;
93
+ font-size: 72px;
94
+ font-weight: 400;
95
+ letter-spacing: -0.03em;
96
+ line-height: 1;
97
+ color: var(--text);
98
+ display: block;
99
+ }
100
+
101
+ .brand-subtitle {
102
+ font-family: 'Geist', sans-serif;
103
+ font-size: 13px;
104
+ font-weight: 400;
105
+ color: var(--muted);
106
+ letter-spacing: 0.08em;
107
+ text-transform: uppercase;
108
+ margin-top: 8px;
109
+ }
110
+
111
+ .form-group {
112
+ margin-bottom: 16px;
113
+ }
114
+
115
+ .form-label {
116
+ display: block;
117
+ font-size: 11px;
118
+ font-weight: 500;
119
+ letter-spacing: 0.06em;
120
+ text-transform: uppercase;
121
+ color: var(--muted);
122
+ margin-bottom: 8px;
123
+ }
124
+
125
+ .form-input {
126
+ width: 100%;
127
+ padding: 12px 16px;
128
+ font-family: 'Geist', sans-serif;
129
+ font-size: 15px;
130
+ color: var(--text);
131
+ background: var(--input-bg);
132
+ border: 1.5px solid transparent;
133
+ border-radius: 10px;
134
+ outline: none;
135
+ transition: border-color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease;
136
+ -webkit-appearance: none;
137
+ }
138
+
139
+ .form-input:focus {
140
+ background: #fff;
141
+ border-color: var(--accent);
142
+ box-shadow: 0 0 0 3px rgba(201,203,36,0.15);
143
+ }
144
+
145
+ .form-input::placeholder {
146
+ color: #B0AFA9;
147
+ }
148
+
149
+ .error-msg {
150
+ font-size: 13px;
151
+ color: #E05454;
152
+ text-align: center;
153
+ margin-top: 12px;
154
+ min-height: 20px;
155
+ opacity: 0;
156
+ transition: opacity 0.2s ease;
157
+ }
158
+ .error-msg.visible {
159
+ opacity: 1;
160
+ }
161
+
162
+ .btn-signin {
163
+ width: 100%;
164
+ margin-top: 24px;
165
+ padding: 14px 24px;
166
+ font-family: 'Geist', sans-serif;
167
+ font-size: 15px;
168
+ font-weight: 500;
169
+ color: var(--text);
170
+ background: var(--accent);
171
+ border: none;
172
+ border-radius: 100px;
173
+ cursor: pointer;
174
+ transition: background 0.15s ease, transform 0.1s ease, box-shadow 0.15s ease;
175
+ box-shadow: 0 2px 8px rgba(201,203,36,0.4);
176
+ position: relative;
177
+ overflow: hidden;
178
+ }
179
+
180
+ .btn-signin:hover {
181
+ background: var(--accent-hover);
182
+ transform: translateY(-1px);
183
+ box-shadow: 0 4px 16px rgba(201,203,36,0.45);
184
+ }
185
+
186
+ .btn-signin:active {
187
+ transform: translateY(0);
188
+ box-shadow: 0 1px 4px rgba(201,203,36,0.3);
189
+ }
190
+
191
+ .btn-signin:disabled {
192
+ opacity: 0.7;
193
+ cursor: not-allowed;
194
+ transform: none;
195
+ }
196
+
197
+ .btn-signin .spinner {
198
+ display: none;
199
+ width: 18px;
200
+ height: 18px;
201
+ border: 2px solid rgba(26,26,24,0.3);
202
+ border-top-color: var(--text);
203
+ border-radius: 50%;
204
+ animation: spin 0.6s linear infinite;
205
+ margin: 0 auto;
206
+ }
207
+
208
+ .btn-signin.loading .btn-text { display: none; }
209
+ .btn-signin.loading .spinner { display: block; }
210
+
211
+ @keyframes spin {
212
+ to { transform: rotate(360deg); }
213
+ }
214
+
215
+ .login-footer {
216
+ text-align: center;
217
+ margin-top: 24px;
218
+ font-size: 12px;
219
+ color: var(--muted);
220
+ letter-spacing: 0.02em;
221
+ }
222
+
223
+ .login-footer span {
224
+ display: inline-block;
225
+ width: 3px;
226
+ height: 3px;
227
+ background: var(--muted);
228
+ border-radius: 50%;
229
+ margin: 0 6px;
230
+ vertical-align: middle;
231
+ opacity: 0.5;
232
+ }
233
+ </style>
234
+ </head>
235
+ <body>
236
+ <div class="bg-blob bg-blob-1"></div>
237
+ <div class="bg-blob bg-blob-2"></div>
238
+
239
+ <div class="login-container">
240
+ <div class="login-card">
241
+ <div class="brand">
242
+ <span class="brand-wordmark">Stenotype</span>
243
+ <div class="brand-subtitle">Memory Dashboard</div>
244
+ </div>
245
+
246
+ <form id="login-form" autocomplete="off">
247
+ <div class="form-group">
248
+ <label class="form-label" for="username">Username</label>
249
+ <input
250
+ class="form-input"
251
+ type="text"
252
+ id="username"
253
+ name="username"
254
+ placeholder="Enter username"
255
+ autocomplete="username"
256
+ required
257
+ />
258
+ </div>
259
+ <div class="form-group">
260
+ <label class="form-label" for="password">Password</label>
261
+ <input
262
+ class="form-input"
263
+ type="password"
264
+ id="password"
265
+ name="password"
266
+ placeholder="Enter password"
267
+ autocomplete="current-password"
268
+ required
269
+ />
270
+ </div>
271
+
272
+ <div class="error-msg" id="error-msg"></div>
273
+
274
+ <button type="submit" class="btn-signin" id="signin-btn">
275
+ <span class="btn-text">Sign In</span>
276
+ <span class="spinner"></span>
277
+ </button>
278
+ </form>
279
+ </div>
280
+
281
+ <div class="login-footer">
282
+ Internal use only
283
+ <span></span>
284
+ Tailscale required
285
+ </div>
286
+ </div>
287
+
288
+ <script>
289
+ // Check if already logged in
290
+ (async () => {
291
+ try {
292
+ const res = await fetch('/auth/me');
293
+ const data = await res.json();
294
+ if (data.user) {
295
+ window.location.href = '/dashboard.html';
296
+ }
297
+ } catch (e) {
298
+ // ignore
299
+ }
300
+ })();
301
+
302
+ const form = document.getElementById('login-form');
303
+ const errorMsg = document.getElementById('error-msg');
304
+ const signinBtn = document.getElementById('signin-btn');
305
+
306
+ function showError(msg) {
307
+ errorMsg.textContent = msg;
308
+ errorMsg.classList.add('visible');
309
+ }
310
+
311
+ function clearError() {
312
+ errorMsg.classList.remove('visible');
313
+ }
314
+
315
+ form.addEventListener('submit', async (e) => {
316
+ e.preventDefault();
317
+ clearError();
318
+
319
+ const username = document.getElementById('username').value.trim();
320
+ const password = document.getElementById('password').value;
321
+
322
+ if (!username || !password) {
323
+ showError('Please enter username and password.');
324
+ return;
325
+ }
326
+
327
+ signinBtn.classList.add('loading');
328
+ signinBtn.disabled = true;
329
+
330
+ try {
331
+ const res = await fetch('/auth/login', {
332
+ method: 'POST',
333
+ headers: { 'Content-Type': 'application/json' },
334
+ body: JSON.stringify({ username, password }),
335
+ });
336
+ const data = await res.json();
337
+
338
+ if (res.ok && data.ok) {
339
+ window.location.href = '/dashboard.html';
340
+ } else {
341
+ showError(data.error || 'Invalid credentials. Try again.');
342
+ signinBtn.classList.remove('loading');
343
+ signinBtn.disabled = false;
344
+ }
345
+ } catch (err) {
346
+ showError('Network error. Please try again.');
347
+ signinBtn.classList.remove('loading');
348
+ signinBtn.disabled = false;
349
+ }
350
+ });
351
+
352
+ // Clear error on input
353
+ document.getElementById('username').addEventListener('input', clearError);
354
+ document.getElementById('password').addEventListener('input', clearError);
355
+ </script>
356
+ </body>
357
+ </html>
@@ -0,0 +1,2 @@
1
+ /*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */
2
+ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--radius-sm:.25rem;--shadow-sm:0 1px 3px 0 #0000001a, 0 1px 2px -1px #0000001a;--shadow-md:0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.visible{visibility:visible}.static{position:static}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.table{display:table}.flex-shrink{flex-shrink:1}.border-collapse{border-collapse:collapse}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.flex-wrap{flex-wrap:wrap}.border{border-style:var(--tw-border-style);border-width:1px}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-filter{-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}