ReviewOS

also looking at this

stacks/postline

UI polish, real queue + scheduling, Bluesky threads, live analytics, blog target

#8
Merged glennmichael123 wants to merge feat/ui-polish into main
54 files +2036 -589

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.

components/PostlineSidebar.stxmodified+33-3
Changes to components/PostlineSidebar.stx
@@ -28,12 +28,41 @@ const settingsActive = currentPath === '/settings' || currentPath.startsWith('/s
2828</script>
2929
3030<template>
31 <aside class="flex overflow-hidden relative sticky top-0 flex-col pb-5 pt-[68px] px-5 h-screen min-w-0 bg-[#f3f3f1]/48 backdrop-blur-3xl backdrop-saturate-150 duration-200 ease-out transition-[width,padding]" aria-label="Postline" data-postline-sidebar data-window-drag-region>
31 <div class="contents">
32 <nav class="flex lg:hidden overflow-x-auto gap-1.5 items-center px-3 py-2 bg-[#f3f3f1]" aria-label="Primary" data-postline-mobile-nav>
33 <span class="grid place-items-center shrink-0 mr-1 h-8 w-8 font-bold text-[13px] text-white bg-neutral-950 rounded-lg" aria-hidden="true">P</span>
34 @foreach (navLinks as link)
35 <a
36 href=""
37 data-stx-link
38 data-stx-active-class="text-zinc-950 bg-white shadow-sm"
39 data-stx-exact-active-class="text-zinc-950 bg-white shadow-sm"
40 class="inline-flex shrink-0 gap-1.5 items-center px-3 py-1.5 text-xs font-medium whitespace-nowrap rounded-full transition-colors hover:bg-white/60 "
41 aria-current=""
42 >
43 <span class="h-4 w-4 " aria-hidden="true"></span>
44 <span>{{ link.label }}</span>
45 </a>
46 @endforeach
47 <a
48 href="/settings"
49 data-stx-link
50 data-stx-active-class="text-zinc-950 bg-white shadow-sm"
51 data-stx-exact-active-class="text-zinc-950 bg-white shadow-sm"
52 class="inline-flex shrink-0 gap-1.5 items-center px-3 py-1.5 text-xs font-medium whitespace-nowrap rounded-full transition-colors hover:bg-white/60 text-zinc-600"
53 aria-current="false"
54 >
55 <span class="h-4 w-4 i-hugeicons-settings-02" aria-hidden="true"></span>
56 <span>Settings</span>
57 </a>
58 </nav>
59
60 <aside class="hidden lg:flex overflow-hidden relative sticky top-0 flex-col pb-5 pt-[68px] px-5 h-screen min-w-0 bg-[#f3f3f1]/48 backdrop-blur-3xl backdrop-saturate-150 duration-200 ease-out transition-[width,padding]" aria-label="Postline" data-postline-sidebar data-window-drag-region>
3261 <div class="absolute inset-0 bg-[linear-gradient(112deg,rgba(255,255,255,0.20)_0%,rgba(255,255,255,0.04)_45%,rgba(226,226,222,0.16)_100%)] pointer-events-none"></div>
3362 <div class="flex absolute right-5 top-3.5 z-20 items-center min-h-7" data-shell-controls-row>
3463 <div class="flex gap-1.5 items-center">
35 <button class="inline-flex items-center justify-center h-8 w-8 text-zinc-600 hover:text-zinc-900 bg-white/60 hover:bg-white border border-zinc-200/70 rounded-xl shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400/50 transition-all duration-200" type="button" aria-label="Collapse sidebar" aria-expanded="true" data-sidebar-collapse data-no-window-drag>
36 <svg class="transition-transform duration-200" data-sidebar-collapse-icon width="18" height="18" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" d="M15.78 4.22a.75.75 0 0 1 0 1.06L10.06 11l5.72 5.72a.75.75 0 1 1-1.06 1.06l-6.25-6.25a.75.75 0 0 1 0-1.06l6.25-6.25a.75.75 0 0 1 1.06 0ZM4.75 4a.75.75 0 0 0-.75.75v10.5c0 .414.336.75.75.75h2.5a.75.75 0 0 0 0-1.5H5.5V5.5h1.75a.75.75 0 0 0 0-1.5h-2.5Z" clip-rule="evenodd"/></svg>
64 <button class="inline-flex items-center justify-center h-8 w-8 text-zinc-600 hover:text-zinc-900 bg-white/60 hover:bg-white border border-zinc-200/70 rounded-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400/50 shadow-sm duration-200 transition-all" type="button" aria-label="Collapse sidebar" aria-expanded="true" data-sidebar-collapse data-no-window-drag>
65 <svg class="duration-200 transition-transform" data-sidebar-collapse-icon width="18" height="18" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" d="M15.78 4.22a.75.75 0 0 1 0 1.06L10.06 11l5.72 5.72a.75.75 0 1 1-1.06 1.06l-6.25-6.25a.75.75 0 0 1 0-1.06l6.25-6.25a.75.75 0 0 1 1.06 0ZM4.75 4a.75.75 0 0 0-.75.75v10.5c0 .414.336.75.75.75h2.5a.75.75 0 0 0 0-1.5H5.5V5.5h1.75a.75.75 0 0 0 0-1.5h-2.5Z" clip-rule="evenodd"/></svg>
3766 </button>
3867 <button class="hidden inline-flex items-center justify-center h-7 w-7 text-zinc-500/80 hover:text-zinc-900 hover:bg-white/35 rounded-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400/50 transition-colors" type="button" aria-label="Go back" data-shell-back data-native-shell-control>
3968 <span class="h-5 w-5 i-hugeicons-arrow-left-02" aria-hidden="true"></span>
@@ -87,4 +116,5 @@ const settingsActive = currentPath === '/settings' || currentPath.startsWith('/s
87116 <span class="grid place-items-center px-1.5 h-5 min-w-5 text-[11px] text-zinc-500 bg-white/72 rounded-full shadow-sm" data-sidebar-expanded>6</span>
88117 </a>
89118 </aside>
119 </div>
90120</template>
public/favicon.svgadded+4-0
Changes to public/favicon.svg
@@ -0,0 +1,4 @@
1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
2 <rect width="64" height="64" rx="14" fill="#0a0a0a"/>
3 <text x="32" y="45" font-family="-apple-system, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif" font-size="38" font-weight="700" fill="#ffffff" text-anchor="middle">P</text>
4</svg>
resources/views/accounts.stxmodified+7-7
Changes to resources/views/accounts.stx
@@ -26,7 +26,7 @@
2626 <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="xxxx-xxxx-xxxx-xxxx" data-bluesky-password required>
2727 </label>
2828 <div class="flex items-end">
29 <button class="inline-flex items-center justify-center px-4 h-10 w-full sm:w-auto font-bold text-sm text-white bg-neutral-950 rounded-xl disabled:opacity-50" type="submit" data-bluesky-connect-submit>Connect Bluesky</button>
29 <button class="inline-flex items-center justify-center px-4 h-10 w-full sm:w-auto font-bold text-sm text-white bg-neutral-950 hover:bg-neutral-800 rounded-xl disabled:opacity-50 transition-colors" type="submit" data-bluesky-connect-submit>Connect Bluesky</button>
3030 </div>
3131 <p class="sm:col-span-3 m-0 text-xs text-zinc-500">Create an app password in Bluesky Settings → Privacy and security → App passwords.</p>
3232 </form>
@@ -53,8 +53,8 @@
5353 </div>
5454 <div class="flex flex-col gap-2 items-end">
5555 <span class="px-2.5 py-1 font-bold text-[11px] text-zinc-500 whitespace-nowrap border border-zinc-200 rounded-full" data-linkedin-channel-status>Setup</span>
56 <button class="inline-flex items-center justify-center px-3 h-9 font-bold text-sm text-white bg-[#0a66c2] rounded-xl disabled:opacity-50" type="button" data-linkedin-connect>Connect with LinkedIn</button>
57 <button class="hidden text-xs text-zinc-500 underline" type="button" data-linkedin-connect-env>Use token from .env</button>
56 <button class="inline-flex items-center justify-center px-3 h-9 font-bold text-sm text-white bg-[#0a66c2] hover:bg-[#0857a6] rounded-xl disabled:opacity-50 transition-colors" type="button" data-linkedin-connect>Connect with LinkedIn</button>
57 <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>
6060 <article class="flex gap-5 items-center justify-between p-3.5 bg-white border border-zinc-200 rounded-xl" data-channel="instagram">
@@ -65,8 +65,8 @@
6565 </div>
6666 <div class="flex flex-col gap-2 items-end">
6767 <span class="px-2.5 py-1 font-bold text-[11px] text-zinc-500 whitespace-nowrap border border-zinc-200 rounded-full" data-instagram-channel-status>Setup</span>
68 <button class="inline-flex items-center justify-center px-3 h-9 font-bold text-sm text-white bg-gradient-to-tr from-amber-500 to-purple-600 via-pink-600 rounded-xl disabled:opacity-50" type="button" data-instagram-connect>Connect with Instagram</button>
69 <button class="hidden text-xs text-zinc-500 underline" type="button" data-instagram-connect-env>Use token from .env</button>
68 <button class="inline-flex items-center justify-center px-3 h-9 font-bold text-sm text-white bg-gradient-to-tr from-amber-500 to-purple-600 via-pink-600 rounded-xl disabled:opacity-50 hover:opacity-90 transition-opacity" type="button" data-instagram-connect>Connect with Instagram</button>
69 <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>
7272 <article class="flex gap-5 items-center justify-between p-3.5 bg-white border border-zinc-200 rounded-xl" data-channel="threads">
@@ -77,8 +77,8 @@
7777 </div>
7878 <div class="flex flex-col gap-2 items-end">
7979 <span class="px-2.5 py-1 font-bold text-[11px] text-zinc-500 whitespace-nowrap border border-zinc-200 rounded-full" data-threads-channel-status>Setup</span>
80 <button class="inline-flex items-center justify-center px-3 h-9 font-bold text-sm text-white bg-neutral-950 rounded-xl disabled:opacity-50" type="button" data-threads-connect>Connect with Threads</button>
81 <button class="hidden text-xs text-zinc-500 underline" type="button" data-threads-connect-env>Use token from .env</button>
80 <button class="inline-flex items-center justify-center px-3 h-9 font-bold text-sm text-white bg-neutral-950 hover:bg-neutral-800 rounded-xl disabled:opacity-50 transition-colors" type="button" data-threads-connect>Connect with Threads</button>
81 <button class="hidden text-xs text-zinc-500 underline hover:text-zinc-700 transition-colors" type="button" data-threads-connect-env>Use token from .env</button>
8282 </div>
8383 </article>
8484 <article class="flex gap-5 items-center justify-between p-3.5 bg-zinc-50/80 border border-zinc-200 rounded-xl opacity-80">
resources/views/analytics.stxmodified+12-7
Changes to resources/views/analytics.stx
@@ -18,7 +18,15 @@ const channelSplits = [
1818</script>
1919
2020@section('content')
21 <section class="grid gap-3.5" data-postline-page="analytics" data-testid="analytics-page">
21 <section class="grid gap-5" data-postline-page="analytics" data-testid="analytics-page">
22 <header class="flex gap-4 items-center justify-between min-h-11">
23 <div>
24 <p class="mb-1.5 font-bold text-[11px] text-zinc-500 tracking-wider uppercase">Analytics</p>
25 <h1 class="m-0 font-bold text-[22px] text-neutral-950 tracking-normal">Publishing insights</h1>
26 <p class="mb-0 mt-1.5 text-[13px] text-zinc-600">How your posts performed across channels over the last 30 days.</p>
27 </div>
28 </header>
29
2230 <div class="grid gap-3.5 lg:grid-cols-3">
2331 @foreach (stats as stat)
2432 <article class="grid gap-2 p-3.5 bg-white border border-zinc-200 rounded-2xl shadow-sm">
@@ -33,13 +41,10 @@ const channelSplits = [
3341 <section class="bg-white border border-zinc-200 rounded-2xl shadow-sm">
3442 <header class="flex gap-3 items-center justify-between p-3.5 border-b border-zinc-200">
3543 <strong class="text-[13px]">Response trend</strong>
36 <button class="inline-flex gap-1.5 items-center justify-center px-3 h-8 font-bold text-xs text-white bg-neutral-950 rounded-lg" type="button">
37 <span class="h-3.5 w-3.5 i-hugeicons-download-04" aria-hidden="true"></span>
38 <span>Export</span>
39 </button>
44 <span class="text-xs text-zinc-500">Last 30 days</span>
4045 </header>
4146 <div class="p-3.5">
42 <svg class="block w-full h-auto" viewBox="0 0 640 280" role="img" aria-label="Postline response trend chart" data-analytics-chart>
47 <svg class="block h-auto w-full" viewBox="0 0 640 280" role="img" aria-label="Postline response trend chart" data-analytics-chart>
4348 <desc>Responses over the last 30 days with publishing dates on the x axis and response counts on the y axis.</desc>
4449 <rect x="0" y="0" width="640" height="280" rx="16" fill="#fafafa"></rect>
4550 <g data-chart-legend></g>
@@ -68,7 +73,7 @@ const channelSplits = [
6873 <span class="font-bold">{{ channel.label }}</span>
6974 <span class="text-zinc-500">{{ channel.value }}</span>
7075 </div>
71 <div class="h-2 overflow-hidden bg-zinc-100 rounded-full">
76 <div class="overflow-hidden h-2 bg-zinc-100 rounded-full">
7277 <div class="h-full bg-blue-600 rounded-full" style="width: "></div>
7378 </div>
7479 </div>
resources/views/index.stxmodified+17-10
Changes to resources/views/index.stx
@@ -4,11 +4,18 @@
44@section('description', 'Write, schedule, publish, and review social posts from a clean personal workspace.')
55
66@section('content')
7 <section class="grid gap-3.5" data-postline-page="composer" data-testid="composer-page">
7 <section class="grid gap-5" data-postline-page="composer" data-testid="composer-page">
88 <!-- Crosswind safelist: utilities applied dynamically from the script below
99 (provider toggles, active-post highlight, connection summary, char
1010 counter) must appear in static markup so the CSS scanner emits them. -->
1111 <div class="hidden text-amber-700 text-emerald-800 text-neutral-950 text-red-700 text-white text-zinc-400 text-zinc-500 text-zinc-600 bg-emerald-400 bg-emerald-50 bg-emerald-500 bg-neutral-950 bg-white bg-zinc-100 bg-zinc-300 bg-zinc-50 hover:bg-white border-blue-300 border-emerald-200 border-neutral-950 border-zinc-200 shadow-sm cursor-not-allowed" aria-hidden="true"></div>
12 <header class="flex gap-4 items-center justify-between min-h-11">
13 <div>
14 <p class="mb-1.5 font-bold text-[11px] text-zinc-500 tracking-wider uppercase">Composer</p>
15 <h1 class="m-0 font-bold text-[22px] text-neutral-950 tracking-normal">Write a post</h1>
16 <p class="mb-0 mt-1.5 text-[13px] text-zinc-600">Draft once, then publish to every connected account.</p>
17 </div>
18 </header>
1219 <div class="grid">
1320 <section class="grid content-start grid-rows-[auto_auto_auto] self-start bg-white border border-zinc-200 rounded-2xl shadow-sm" aria-label="Post composer">
1421 <header class="flex flex-wrap gap-3 items-center justify-between p-3 border-b border-zinc-200">
@@ -20,9 +27,9 @@
2027 </a>
2128 </div>
2229 <div class="grid grid-cols-3 p-0.5 bg-zinc-100 border border-zinc-200 rounded-xl" role="group" aria-label="Post mode">
23 <button class="px-2.5 py-1.5 min-w-19 text-neutral-950 text-xs bg-white rounded-lg shadow-sm" type="button" data-composer-mode="single">Single</button>
24 <button class="px-2.5 py-1.5 min-w-19 text-xs text-zinc-500 rounded-lg" type="button" data-composer-mode="thread">Thread</button>
25 <button class="px-2.5 py-1.5 min-w-19 text-xs text-zinc-500 rounded-lg" type="button" data-composer-mode="draft">Draft</button>
30 <button class="px-2.5 py-1.5 min-w-19 text-neutral-950 text-xs bg-white rounded-lg shadow-sm transition-colors" type="button" data-composer-mode="single">Single</button>
31 <button class="px-2.5 py-1.5 min-w-19 text-xs text-zinc-500 rounded-lg transition-colors" type="button" data-composer-mode="thread">Thread</button>
32 <button class="px-2.5 py-1.5 min-w-19 text-xs text-zinc-500 rounded-lg transition-colors" type="button" data-composer-mode="draft">Draft</button>
2633 </div>
2734 </header>
2835
@@ -41,7 +48,7 @@
4148 <span class="h-3.5 w-3.5 i-hugeicons-edit-02" aria-hidden="true"></span>
4249 <span data-post-label>Post 1</span>
4350 </button>
44 <button class="hidden items-center justify-center h-7 w-7 text-zinc-500 bg-white border border-zinc-200 rounded-lg shadow-sm" type="button" aria-label="Remove post" title="Remove post" data-remove-post>
51 <button class="hidden items-center justify-center h-7 w-7 text-zinc-500 hover:text-zinc-700 bg-white hover:bg-zinc-50 border border-zinc-200 rounded-lg shadow-sm transition-colors" type="button" aria-label="Remove post" title="Remove post" data-remove-post>
4552 <span class="h-3.5 w-3.5 i-hugeicons-cancel-01" aria-hidden="true"></span>
4653 </button>
4754 </header>
@@ -52,15 +59,15 @@
5259 </div>
5360
5461 <footer class="flex gap-3 items-center justify-between p-3 border-t border-zinc-200">
55 <span class="text-xs text-zinc-500" data-bluesky-count>118 / 300 characters</span>
62 <span class="text-xs text-zinc-500" data-bluesky-count>0 / 300 characters</span>
5663 <div class="flex gap-2 items-center">
57 <button class="inline-flex items-center justify-center h-9 w-9 text-neutral-950 bg-white border border-zinc-200 rounded-xl shadow-sm" type="button" aria-label="Add post" title="Add post" data-add-post>
64 <button class="inline-flex items-center justify-center h-9 w-9 text-neutral-950 bg-white hover:bg-zinc-50 border border-zinc-200 rounded-xl shadow-sm transition-colors" type="button" aria-label="Add post" title="Add post" data-add-post>
5865 <span class="h-4 w-4 i-hugeicons-plus-sign" aria-hidden="true"></span>
5966 </button>
60 <button class="inline-flex items-center justify-center h-9 w-9 text-neutral-950 bg-white border border-zinc-200 rounded-xl shadow-sm" type="button" aria-label="Preview post" title="Preview post" data-preview-post>
67 <button class="inline-flex items-center justify-center h-9 w-9 text-neutral-950 bg-white hover:bg-zinc-50 border border-zinc-200 rounded-xl shadow-sm transition-colors" type="button" aria-label="Preview post" title="Preview post" data-preview-post>
6168 <span class="h-4 w-4 i-hugeicons-view" aria-hidden="true"></span>
6269 </button>
63 <button class="inline-flex gap-2 items-center justify-center px-4 h-9 font-bold text-white bg-neutral-950 rounded-xl disabled:opacity-50 disabled:cursor-not-allowed" type="button" data-bluesky-publish>
70 <button class="inline-flex gap-2 items-center justify-center px-4 h-9 font-bold text-white bg-neutral-950 hover:bg-neutral-800 rounded-xl disabled:opacity-50 transition-colors disabled:cursor-not-allowed" type="button" data-bluesky-publish>
6471 <span class="h-4 w-4 i-hugeicons-sent" aria-hidden="true"></span>
6572 <span data-bluesky-publish-label>Publish</span>
6673 </button>
@@ -251,7 +258,7 @@
251258 <span class="h-3.5 w-3.5 i-hugeicons-edit-02" aria-hidden="true"></span>
252259 <span data-post-label>Post ${number}</span>
253260 </button>
254 <button class="inline-flex items-center justify-center h-7 w-7 text-zinc-500 bg-white border border-zinc-200 rounded-lg shadow-sm" type="button" aria-label="Remove post" title="Remove post" data-remove-post>
261 <button class="inline-flex items-center justify-center h-7 w-7 text-zinc-500 hover:text-zinc-700 bg-white hover:bg-zinc-50 border border-zinc-200 rounded-lg shadow-sm transition-colors" type="button" aria-label="Remove post" title="Remove post" data-remove-post>
255262 <span class="h-3.5 w-3.5 i-hugeicons-cancel-01" aria-hidden="true"></span>
256263 </button>
257264 </header>
resources/views/layouts/postline.stxmodified+33-4
Changes to resources/views/layouts/postline.stx
@@ -6,6 +6,7 @@
66 <meta name="color-scheme" content="light">
77 <meta name="description" content="@yield('description', 'Postline is a clean personal workspace for social publishing.')">
88 <title>@yield('title', 'Postline')</title>
9 <link rel="icon" href="/favicon.svg" type="image/svg+xml">
910 <script>
1011 ;(() => {
1112 document.documentElement.setAttribute('data-theme', 'light')
@@ -84,17 +85,28 @@
8485 transform: translate3d(0, 0, 0) scale(1);
8586 pointer-events: auto;
8687 }
88
89 @media (max-width: 1023px) {
90 [data-sidebar-expand-fab] {
91 display: none;
92 }
93 }
94
95 [data-postline-app] :is(a, button, [role='button']):focus-visible {
96 outline: 2px solid rgb(82 82 91 / 0.6);
97 outline-offset: 2px;
98 }
8799 </style>
88100 <link rel="stylesheet" href="/assets/styles/postline-toast.css">
89101 <link rel="stylesheet" href="/assets/styles/postline-timeline.css">
90102 </head>
91103 <body class="min-h-screen min-w-80 antialiased font-sans text-neutral-950 bg-[#f3f3f1]">
92 <div class="grid grid-cols-1 lg:grid-cols-[var(--postline-sidebar-width,286px)_minmax(0,1fr)] min-h-screen duration-200 ease-out transition-[grid-template-columns]" data-postline-app data-postline-shell>
104 <div class="grid grid-cols-1 grid-rows-[auto_minmax(0,1fr)] lg:grid-cols-[var(--postline-sidebar-width,286px)_minmax(0,1fr)] lg:grid-rows-1 min-h-screen duration-200 ease-out transition-[grid-template-columns]" data-postline-app data-postline-shell>
93105 <PostlineSidebar />
94106
95107 <div class="relative min-w-0 bg-[#f3f3f1]" data-postline-content-shell>
96108 <button
97 class="fixed top-3.5 left-3.5 z-[90] inline-flex items-center justify-center h-9 w-9 text-zinc-700 bg-white/95 border border-zinc-200/90 rounded-xl shadow-[0_8px_28px_rgba(24,31,42,0.12)] backdrop-blur-sm hover:bg-white hover:text-zinc-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400/60"
109 class="inline-flex fixed left-3.5 top-3.5 z-[90] items-center justify-center h-9 w-9 text-zinc-700 hover:text-zinc-900 bg-white/95 hover:bg-white border border-zinc-200/90 rounded-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400/60 shadow-[0_8px_28px_rgba(24,31,42,0.12)] backdrop-blur-sm"
98110 type="button"
99111 aria-label="Expand sidebar"
100112 title="Expand sidebar"
@@ -103,14 +115,14 @@
103115 >
104116 <svg viewBox="0 0 20 20" fill="currentColor" width="18" height="18" aria-hidden="true"><path fill-rule="evenodd" d="M4.75 4a.75.75 0 0 0-.75.75v10.5c0 .414.336.75.75.75h2.5a.75.75 0 0 0 0-1.5H5.5V5.5h7.69l-1.22-1.22a.75.75 0 1 1 1.06-1.06l2.5 2.5a.75.75 0 0 1 0 1.06l-2.5 2.5a.75.75 0 1 1-1.06-1.06L13.19 11H5.5v5.25h1.75a.75.75 0 0 0 0 1.5h-2.5A.75.75 0 0 0 4.75 15V4.75Z" clip-rule="evenodd"/></svg>
105117 </button>
106 <section class="overflow-hidden min-h-screen min-w-0 bg-[#f3f3f1] border-l border-t border-zinc-200/80 rounded-tl-[28px] shadow-[-10px_0_36px_rgba(24,31,42,0.08)]" aria-label="Postline workspace" data-postline-workspace data-window-drag-region>
118 <section class="overflow-hidden min-h-screen min-w-0 bg-[#f3f3f1] border-t border-zinc-200/80 lg:border-l lg:rounded-tl-[28px] lg:shadow-[-10px_0_36px_rgba(24,31,42,0.08)]" aria-label="Postline workspace" data-postline-workspace data-window-drag-region>
107119 <main class="p-3.5 sm:p-4 min-w-0" data-postline-router>
108120 @yield('content')
109121 </main>
110122 </section>
111123 </div>
112124 </div>
113 <div id="postline-toast-host" class="pointer-events-none fixed bottom-6 right-6 z-[100] flex w-[calc(100%-2rem)] max-w-[22rem] flex-col-reverse gap-2.5 sm:right-6" aria-live="polite" aria-relevant="additions"></div>
125 <div id="postline-toast-host" class="flex bottom-6 fixed right-6 z-[100] sm:right-6 flex-col-reverse gap-2.5 max-w-[22rem] w-[calc(100%-2rem)] pointer-events-none" aria-live="polite" aria-relevant="additions"></div>
114126 <script type="module" src="/assets/scripts/postline-toast.ts"></script>
115127 <script type="module" src="/assets/scripts/postline-bluesky.ts"></script>
116128 <script type="module" src="/assets/scripts/postline-linkedin.ts"></script>
@@ -302,9 +314,26 @@
302314 })
303315 }
304316
317 // Number keys 1-6 jump between pages (the sidebar shows the badges).
318 const bindKeyboardShortcuts = () => {
319 const routes = { 1: '/', 2: '/queue', 3: '/timeline', 4: '/analytics', 5: '/accounts', 6: '/settings' }
320 document.addEventListener('keydown', (event) => {
321 if (event.metaKey || event.ctrlKey || event.altKey) return
322 const target = event.target
323 if (target instanceof HTMLElement && target.closest('input, textarea, select, [contenteditable]')) return
324 const href = routes[event.key]
325 if (!href || window.location.pathname === href) return
326 event.preventDefault()
327 const link = document.querySelector(`[data-stx-link][href="${href}"]`)
328 if (link instanceof HTMLElement) link.click()
329 else window.location.assign(href)
330 })
331 }
332
305333 bindSidebarCollapse()
306334 bindShellHistory()
307335 bindWindowDragRegions()
336 bindKeyboardShortcuts()
308337 syncNativeShell()
309338 window.addEventListener('stx:load', bindShellHistory)
310339 window.addEventListener('stx:load', bindSidebarCollapse)
resources/views/queue.stxmodified+25-15
Changes to resources/views/queue.stx
@@ -4,6 +4,12 @@
44@section('description', 'Review scheduled and drafted social posts in Postline.')
55
66<script server>
7const statusStyles = {
8 Ready: 'text-emerald-700 bg-emerald-50 border border-emerald-200',
9 Draft: 'text-amber-800 bg-amber-50 border border-amber-200',
10 Planned: 'text-zinc-600 bg-zinc-100 border border-zinc-200',
11}
12
713const scheduledPosts = [
814 {
915 time: 'Today 3:30 PM',
@@ -26,24 +32,29 @@ const scheduledPosts = [
2632 text: 'Cross-posting should feel intentional, not like copy/paste chores across six tabs.',
2733 note: 'Driver support planned after Bluesky settles.',
2834 },
29]
35].map(post => ({ ...post, statusClass: statusStyles[post.status] || statusStyles.Planned }))
3036</script>
3137
3238@section('content')
33 <section class="grid gap-3.5" data-postline-page="queue" data-testid="queue-page">
39 <section class="grid gap-5" data-postline-page="queue" data-testid="queue-page">
40 <!-- Crosswind safelist: status pill classes come from server data. -->
41 <div class="hidden text-amber-800 text-emerald-700 text-zinc-600 bg-amber-50 bg-emerald-50 bg-zinc-100 border-amber-200 border-emerald-200 border-zinc-200" aria-hidden="true"></div>
42 <header class="flex gap-4 items-center justify-between min-h-11">
43 <div>
44 <p class="mb-1.5 font-bold text-[11px] text-zinc-500 tracking-wider uppercase">Queue</p>
45 <h1 class="m-0 font-bold text-[22px] text-neutral-950 tracking-normal">Scheduled posts</h1>
46 <p class="mb-0 mt-1.5 text-[13px] text-zinc-600">What's drafted, planned, and ready to publish next.</p>
47 </div>
48 <a href="/" data-stx-link class="inline-flex gap-2 items-center justify-center px-4 h-9 font-bold text-sm text-white bg-neutral-950 hover:bg-neutral-800 rounded-xl transition-colors">
49 <span class="h-4 w-4 i-hugeicons-plus-sign" aria-hidden="true"></span>
50 <span>New post</span>
51 </a>
52 </header>
53
3454 <section class="bg-white border border-zinc-200 rounded-2xl shadow-sm">
3555 <header class="flex gap-3 items-center justify-between p-3.5 border-b border-zinc-200">
36 <strong class="text-[13px]">Queue</strong>
37 <div class="flex gap-2 items-center">
38 <a href="/" data-stx-link class="inline-flex gap-1.5 items-center justify-center px-3 h-8 font-bold text-xs text-neutral-950 bg-white border border-zinc-200 rounded-lg shadow-sm">
39 <span class="h-3.5 w-3.5 i-hugeicons-plus-sign" aria-hidden="true"></span>
40 <span>New</span>
41 </a>
42 <button class="inline-flex gap-1.5 items-center justify-center px-3 h-8 font-bold text-xs text-white bg-neutral-950 rounded-lg" type="button">
43 <span class="h-3.5 w-3.5 i-hugeicons-magic-wand-01" aria-hidden="true"></span>
44 <span>Optimize</span>
45 </button>
46 </div>
56 <strong class="text-[13px]">Upcoming</strong>
57 <span class="text-xs text-zinc-500">{{ scheduledPosts.length }} posts</span>
4758 </header>
4859 <div class="grid gap-2.5 p-3">
4960 @foreach (scheduledPosts as post)
@@ -53,12 +64,11 @@ const scheduledPosts = [
5364 <span class="font-bold text-xs">{{ post.time }}</span>
5465 <span class="text-xs text-zinc-500">{{ post.channel }}</span>
5566 </div>
56 <span class="px-2 py-1 font-bold text-[11px] text-blue-700 bg-blue-50 rounded-full">{{ post.status }}</span>
67 <span class="px-2 py-1 font-bold text-[11px] rounded-full ">{{ post.status }}</span>
5768 </header>
5869 <p class="m-0 leading-normal text-[13px] text-slate-700">{{ post.text }}</p>
5970 <footer class="flex items-center justify-between text-xs text-zinc-500">
6071 <span>{{ post.note }}</span>
61 <button class="inline-flex items-center justify-center px-2.5 h-7 font-bold text-neutral-950 bg-white border border-zinc-200 rounded-lg shadow-sm" type="button">Edit</button>
6272 </footer>
6373 </article>
6474 @endforeach
resources/views/settings.stxmodified+6-6
Changes to resources/views/settings.stx
@@ -9,7 +9,7 @@
99 <div>
1010 <p class="mb-1.5 font-bold text-[11px] text-zinc-500 tracking-wider uppercase">Settings</p>
1111 <h1 class="m-0 font-bold text-[22px] text-neutral-950 tracking-normal">Publishing</h1>
12 <p class="mt-1.5 mb-0 text-[13px] text-zinc-600">Defaults for Postline. Bluesky credentials live on Accounts.</p>
12 <p class="mb-0 mt-1.5 text-[13px] text-zinc-600">Defaults for Postline. Bluesky credentials live on Accounts.</p>
1313 </div>
1414 </header>
1515
@@ -18,12 +18,12 @@
1818 <header class="flex gap-3 items-center justify-between pb-3 border-b border-zinc-200">
1919 <div>
2020 <strong class="text-[13px]">Bluesky connection</strong>
21 <p class="mt-1 mb-0 text-xs text-zinc-500" data-settings-bluesky-summary>Checking…</p>
21 <p class="mb-0 mt-1 text-xs text-zinc-500" data-settings-bluesky-summary>Checking…</p>
2222 </div>
23 <span class="rounded-full border px-2.5 py-1 text-[11px] font-bold border-zinc-200 text-zinc-500" data-settings-bluesky-pill>—</span>
23 <span class="px-2.5 py-1 font-bold text-[11px] text-zinc-500 border border-zinc-200 rounded-full" data-settings-bluesky-pill>—</span>
2424 </header>
25 <p class="m-0 text-[13px] leading-relaxed text-slate-700">Postline uses a Bluesky app password (not your main login). You can also set <code class="px-1 py-0.5 text-xs bg-zinc-100 rounded">BLUESKY_IDENTIFIER</code> and <code class="px-1 py-0.5 text-xs bg-zinc-100 rounded">BLUESKY_APP_PASSWORD</code> in <code class="px-1 py-0.5 text-xs bg-zinc-100 rounded">.env</code> for local dev.</p>
26 <a class="inline-flex items-center justify-center px-4 h-9 w-fit font-bold text-sm text-white bg-neutral-950 rounded-xl" href="/accounts" data-stx-link>Manage on Accounts</a>
25 <p class="m-0 leading-relaxed text-[13px] text-slate-700">Postline uses a Bluesky app password (not your main login). You can also set <code class="px-1 py-0.5 text-xs bg-zinc-100 rounded">BLUESKY_IDENTIFIER</code> and <code class="px-1 py-0.5 text-xs bg-zinc-100 rounded">BLUESKY_APP_PASSWORD</code> in <code class="px-1 py-0.5 text-xs bg-zinc-100 rounded">.env</code> for local dev.</p>
26 <a class="inline-flex items-center justify-center px-4 h-9 w-fit font-bold text-sm text-white bg-neutral-950 hover:bg-neutral-800 rounded-xl transition-colors" href="/accounts" data-stx-link>Manage on Accounts</a>
2727 </article>
2828
2929 <article class="grid gap-3 p-4 bg-white border border-zinc-200 rounded-2xl shadow-sm">
@@ -41,7 +41,7 @@
4141 </label>
4242 <label class="flex gap-4 items-center justify-between text-[13px] text-slate-700">
4343 <span>Auto-save drafts</span>
44 <span class="text-emerald-700 font-bold">On</span>
44 <span class="font-bold text-emerald-700">On</span>
4545 </label>
4646 </article>
4747 </section>
resources/views/timeline.stxmodified+5-5
Changes to resources/views/timeline.stx
@@ -9,11 +9,11 @@
99 <div>
1010 <p class="mb-1.5 font-bold text-[11px] text-zinc-500 tracking-wider uppercase">Timeline</p>
1111 <h1 class="m-0 font-bold text-[22px] text-neutral-950 tracking-normal">Home feed</h1>
12 <p class="mt-1.5 mb-0 text-[13px] text-zinc-600">Posts from your connected Bluesky account.</p>
12 <p class="mb-0 mt-1.5 text-[13px] text-zinc-600">Posts from your connected Bluesky account.</p>
1313 </div>
1414 <div class="flex gap-2 items-center">
15 <a class="inline-flex items-center justify-center px-4 h-9 font-bold text-sm text-neutral-950 bg-white border border-zinc-200 rounded-xl shadow-sm hover:bg-zinc-50" href="/" data-stx-link>Compose</a>
16 <button class="inline-flex gap-2 items-center justify-center px-4 h-9 font-bold text-sm text-white bg-neutral-950 rounded-xl disabled:opacity-50" type="button" data-bluesky-timeline-refresh>
15 <a class="inline-flex items-center justify-center px-4 h-9 font-bold text-neutral-950 text-sm bg-white hover:bg-zinc-50 border border-zinc-200 rounded-xl shadow-sm transition-colors" href="/" data-stx-link>Compose</a>
16 <button class="inline-flex gap-2 items-center justify-center px-4 h-9 font-bold text-sm text-white bg-neutral-950 hover:bg-neutral-800 rounded-xl disabled:opacity-50 transition-colors" type="button" data-bluesky-timeline-refresh>
1717 <svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" d="M15.312 4.818A.75.75 0 0 0 14 4H6.5a.75.75 0 0 0 0 1.5h5.992l-4.43 4.431a.75.75 0 1 0 1.06 1.06l5.784-5.784a.75.75 0 0 0 0-1.06l-5.784-5.784a.75.75 0 0 0-1.06 1.06l4.43 4.43H6a2.75 2.75 0 0 0 0 5.5h8.312a.75.75 0 0 0 .53-.22l2.25-2.25a.75.75 0 0 0-1.06-1.06l-1.72 1.72H6a1.25 1.25 0 0 1 0-2.5h7.312a.75.75 0 0 0 .53-.22l2.25-2.25a.75.75 0 0 0 0-1.06l-2.25-2.25a.75.75 0 0 0-.53-.22Z" clip-rule="evenodd"/></svg>
1818 <span data-bluesky-timeline-refresh-label>Refresh</span>
1919 </button>
@@ -23,10 +23,10 @@
2323 <div class="overflow-hidden bg-white border border-zinc-200 rounded-2xl shadow-sm postline-feed" aria-label="Bluesky timeline">
2424 <div class="postline-feed__header">
2525 <div>
26 <strong class="text-sm font-bold text-zinc-900">Following</strong>
26 <strong class="font-bold text-sm text-zinc-900">Following</strong>
2727 <p class="m-0 mt-0.5 text-xs text-zinc-500" data-bluesky-timeline-meta>Loading feed…</p>
2828 </div>
29 <span class="rounded-full border border-blue-200 bg-blue-50 px-2.5 py-1 text-[11px] font-bold text-blue-700">Bluesky</span>
29 <span class="px-2.5 py-1 font-bold text-[11px] text-blue-700 bg-blue-50 border border-blue-200 rounded-full">Bluesky</span>
3030 </div>
3131 <div class="postline-feed__list" data-bluesky-timeline-list>
3232 <p class="postline-feed__loading" data-bluesky-timeline-empty>Loading your timeline…</p>