ReviewOS

also looking at this

stacks/postline

fix: MVP-readiness fixes — restore lost UI feedback, register safety, analytics error state, mobile + a11y

#31
Merged glennmichael123 wants to merge fix/readiness-batch-1-feedback-timing into main
7 files +123 -29

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

resources/views/accounts.stxmodified+7-7
Changes to resources/views/accounts.stx
@@ -37,7 +37,7 @@
3737 <span class="text-xs text-zinc-500" data-bluesky-channel-pill>Loading…</span>
3838 </header>
3939 <div class="grid gap-2.5 p-3" data-bluesky-channels>
40 <article class="flex gap-5 items-center justify-between p-3.5 bg-white border border-zinc-200 rounded-xl" data-channel="bluesky">
40 <article class="flex flex-col gap-3 justify-between sm:flex-row sm:gap-5 sm:items-center p-3.5 bg-white border border-zinc-200 rounded-xl" data-channel="bluesky">
4141 <div>
4242 <h2 class="my-0 font-bold text-[15px]">Bluesky</h2>
4343 <p class="my-1.5 text-[13px] text-slate-700" data-bluesky-channel-handle>Not connected</p>
@@ -45,7 +45,7 @@
4545 </div>
4646 <span class="px-2.5 py-1 font-bold text-[11px] text-zinc-500 whitespace-nowrap border border-zinc-200 rounded-full" data-bluesky-channel-status>Setup</span>
4747 </article>
48 <article class="flex gap-5 items-center justify-between p-3.5 bg-white border border-zinc-200 rounded-xl" data-channel="linkedin">
48 <article class="flex flex-col gap-3 justify-between sm:flex-row sm:gap-5 sm:items-center p-3.5 bg-white border border-zinc-200 rounded-xl" data-channel="linkedin">
4949 <div>
5050 <h2 class="my-0 font-bold text-[15px]">LinkedIn</h2>
5151 <p class="my-1.5 text-[13px] text-slate-700" data-linkedin-channel-handle>Not connected</p>
@@ -57,7 +57,7 @@
5757 <button class="hidden text-xs text-zinc-500 underline hover:text-zinc-700 transition-colors" type="button" data-linkedin-connect-env>Use token from .env</button>
5858 </div>
5959 </article>
60 <article class="flex gap-5 items-center justify-between p-3.5 bg-white border border-zinc-200 rounded-xl" data-channel="instagram">
60 <article class="flex flex-col gap-3 justify-between sm:flex-row sm:gap-5 sm:items-center p-3.5 bg-white border border-zinc-200 rounded-xl" data-channel="instagram">
6161 <div>
6262 <h2 class="my-0 font-bold text-[15px]">Instagram</h2>
6363 <p class="my-1.5 text-[13px] text-slate-700" data-instagram-channel-handle>Not connected</p>
@@ -69,7 +69,7 @@
6969 <button class="hidden text-xs text-zinc-500 underline hover:text-zinc-700 transition-colors" type="button" data-instagram-connect-env>Use token from .env</button>
7070 </div>
7171 </article>
72 <article class="flex gap-5 items-center justify-between p-3.5 bg-white border border-zinc-200 rounded-xl" data-channel="threads">
72 <article class="flex flex-col gap-3 justify-between sm:flex-row sm:gap-5 sm:items-center p-3.5 bg-white border border-zinc-200 rounded-xl" data-channel="threads">
7373 <div>
7474 <h2 class="my-0 font-bold text-[15px]">Threads</h2>
7575 <p class="my-1.5 text-[13px] text-slate-700" data-threads-channel-handle>Not connected</p>
@@ -91,13 +91,13 @@
9191 <span class="px-2.5 py-1 font-bold text-[11px] text-zinc-500 whitespace-nowrap border border-zinc-200 rounded-full" data-mastodon-channel-status>Setup</span>
9292 </div>
9393 <form class="grid gap-2 sm:grid-cols-[minmax(0,1fr)_minmax(0,1.4fr)_auto]" data-mastodon-connect-form>
94 <input class="px-3 h-9 text-neutral-950 text-sm bg-white border border-zinc-200 outline-none rounded-lg focus:border-blue-500 focus:ring-2 focus:ring-blue-100" name="instance" placeholder="mastodon.social" autocomplete="url" data-mastodon-instance required>
95 <input class="px-3 h-9 text-neutral-950 text-sm bg-white border border-zinc-200 outline-none rounded-lg focus:border-blue-500 focus:ring-2 focus:ring-blue-100" name="access_token" type="password" placeholder="Access token" autocomplete="off" data-mastodon-token required>
94 <input class="px-3 h-9 text-neutral-950 text-sm bg-white border border-zinc-200 outline-none rounded-lg focus:border-blue-500 focus:ring-2 focus:ring-blue-100" name="instance" placeholder="mastodon.social" autocomplete="url" aria-label="Mastodon instance" data-mastodon-instance required>
95 <input class="px-3 h-9 text-neutral-950 text-sm bg-white border border-zinc-200 outline-none rounded-lg focus:border-blue-500 focus:ring-2 focus:ring-blue-100" name="access_token" type="password" placeholder="Access token" autocomplete="off" aria-label="Mastodon access token" data-mastodon-token required>
9696 <button class="inline-flex items-center justify-center px-4 h-9 font-bold text-sm text-white bg-neutral-950 hover:bg-neutral-800 rounded-lg disabled:opacity-50 transition-colors" type="submit" data-mastodon-connect-submit>Connect</button>
9797 <p class="sm:col-span-3 m-0 text-xs text-zinc-500">Create a token in your instance's Preferences → Development → New application (needs the <code class="px-1 py-0.5 text-xs bg-zinc-100 rounded">write:statuses</code> + <code class="px-1 py-0.5 text-xs bg-zinc-100 rounded">write:media</code> scopes).</p>
9898 </form>
9999 </article>
100 <article class="flex gap-5 items-center justify-between p-3.5 bg-white border border-zinc-200 rounded-xl" data-channel="twitter">
100 <article class="flex flex-col gap-3 justify-between sm:flex-row sm:gap-5 sm:items-center p-3.5 bg-white border border-zinc-200 rounded-xl" data-channel="twitter">
101101 <div>
102102 <h2 class="my-0 font-bold text-[15px]">X (Twitter)</h2>
103103 <p class="my-1.5 text-[13px] text-slate-700" data-twitter-channel-handle>Not connected</p>
resources/views/login.stxmodified+18-0
Changes to resources/views/login.stx
@@ -51,6 +51,10 @@
5151 <span class="font-bold text-xs text-zinc-500">Password</span>
5252 <input class="px-3 h-10 text-neutral-950 text-sm bg-white border border-zinc-200 outline-none rounded-xl focus:border-blue-500 focus:ring-2 focus:ring-blue-100" name="password" type="password" autocomplete="current-password" placeholder="••••••••" required minlength="6" data-login-password>
5353 </label>
54 <label class="hidden gap-1.5" data-login-confirm-row>
55 <span class="font-bold text-xs text-zinc-500">Confirm password</span>
56 <input class="px-3 h-10 text-neutral-950 text-sm bg-white border border-zinc-200 outline-none rounded-xl focus:border-blue-500 focus:ring-2 focus:ring-blue-100" name="password_confirmation" type="password" autocomplete="new-password" placeholder="••••••••" minlength="6" data-login-confirm>
57 </label>
5458 <p class="hidden m-0 text-red-700 text-xs" data-login-error></p>
5559 <button class="inline-flex items-center justify-center px-4 h-10 font-bold text-sm text-white bg-neutral-950 hover:bg-neutral-800 rounded-xl disabled:opacity-50 transition-colors" type="submit" data-login-submit>Sign in</button>
5660 </form>
@@ -71,6 +75,8 @@
7175 const nameInput = root.querySelector('[data-login-name]')
7276 const emailInput = root.querySelector('[data-login-email]')
7377 const passwordInput = root.querySelector('[data-login-password]')
78 const confirmRow = root.querySelector('[data-login-confirm-row]')
79 const confirmInput = root.querySelector('[data-login-confirm]')
7480 const errorText = root.querySelector('[data-login-error]')
7581 const submit = root.querySelector('[data-login-submit]')
7682 const toggle = root.querySelector('[data-login-toggle]')
@@ -87,6 +93,12 @@
8793 registering = register
8894 nameRow?.classList.toggle('hidden', !register)
8995 nameRow?.classList.toggle('grid', register)
96 confirmRow?.classList.toggle('hidden', !register)
97 confirmRow?.classList.toggle('grid', register)
98 // The workspace-owning account has no recovery path, so a first-run
99 // typo must not silently create an account nobody can sign into.
100 nameInput?.toggleAttribute('required', register)
101 confirmInput?.toggleAttribute('required', register)
90102 if (heading) heading.textContent = register ? 'Create your workspace' : 'Sign in to Postline'
91103 if (subtitle) subtitle.textContent = register ? 'One account owns this Postline instance.' : 'Your personal publishing workspace.'
92104 if (submit) submit.textContent = register ? 'Create account' : 'Sign in'
@@ -105,6 +117,12 @@
105117 form?.addEventListener('submit', async (event) => {
106118 event.preventDefault()
107119 setError('')
120
121 if (registering && (passwordInput?.value || '') !== (confirmInput?.value || '')) {
122 setError('Passwords do not match.')
123 return
124 }
125
108126 submit?.setAttribute('disabled', 'disabled')
109127
110128 const body = new FormData()
resources/views/queue.stxmodified+11-4
Changes to resources/views/queue.stx
@@ -64,10 +64,14 @@
6464 </div>
6565 <strong class="text-[13px]" data-cal-title>—</strong>
6666 </header>
67 <div class="grid grid-cols-7 font-bold text-[11px] text-zinc-500 border-b border-zinc-200">
68 <div class="p-2 text-center">Sun</div><div class="p-2 text-center">Mon</div><div class="p-2 text-center">Tue</div><div class="p-2 text-center">Wed</div><div class="p-2 text-center">Thu</div><div class="p-2 text-center">Fri</div><div class="p-2 text-center">Sat</div>
67 <div class="overflow-x-auto">
68 <div class="min-w-[560px]">
69 <div class="grid grid-cols-7 font-bold text-[11px] text-zinc-500 border-b border-zinc-200">
70 <div class="p-2 text-center">Sun</div><div class="p-2 text-center">Mon</div><div class="p-2 text-center">Tue</div><div class="p-2 text-center">Wed</div><div class="p-2 text-center">Thu</div><div class="p-2 text-center">Fri</div><div class="p-2 text-center">Sat</div>
71 </div>
72 <div class="grid grid-cols-7" data-cal-grid></div>
73 </div>
6974 </div>
70 <div class="grid grid-cols-7" data-cal-grid></div>
7175 </section>
7276 </section>
7377
@@ -116,6 +120,9 @@
116120 const liveUrlFor = (uri) => {
117121 const value = String(uri || '')
118122 if (value.startsWith('/')) return value
123 // A stored permalink (Mastodon, and any provider persisting a URL) is
124 // already linkable as-is.
125 if (/^https?:\/\//i.test(value)) return value
119126 const match = value.match(/^at:\/\/([^/]+)\/app\.bsky\.feed\.post\/(.+)$/)
120127 return match ? `https://bsky.app/profile/${encodeURIComponent(match[1])}/post/${encodeURIComponent(match[2])}` : null
121128 }
@@ -144,7 +151,7 @@
144151 </header>
145152 <p class="m-0 leading-normal text-[13px] text-slate-700">${escapeHtml(item.body)}</p>
146153 ${failure ? `<p class="m-0 text-red-700 text-xs">${escapeHtml(failure)}</p>` : ''}
147 <footer class="flex gap-2 items-center justify-end">
154 <footer class="flex flex-wrap gap-2 items-center justify-end">
148155 ${liveUrl ? `<a class="inline-flex items-center justify-center px-2.5 h-7 font-bold text-neutral-950 text-xs bg-white hover:bg-zinc-50 border border-zinc-200 rounded-lg shadow-sm transition-colors" href="${escapeHtml(liveUrl)}" target="_blank" rel="noopener noreferrer">View</a>` : ''}
149156 ${actionable ? `<a class="inline-flex items-center justify-center px-2.5 h-7 font-bold text-neutral-950 text-xs bg-white hover:bg-zinc-50 border border-zinc-200 rounded-lg shadow-sm transition-colors" href="/?edit=${item.id}" data-stx-link>Edit</a>` : ''}
150157 ${actionable ? `<button class="inline-flex items-center justify-center px-2.5 h-7 font-bold text-white text-xs bg-neutral-950 hover:bg-neutral-800 rounded-lg transition-colors" type="button" data-queue-publish="${item.id}">Publish now</button>` : ''}