also looking at this
UI polish, real queue + scheduling, Bluesky threads, live analytics, blog target
#8
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.
| @@ -28,12 +28,41 @@ const settingsActive = currentPath === '/settings' || currentPath.startsWith('/s | ||
| 28 | 28 | </script> |
| 29 | 29 | |
| 30 | 30 | <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> | |
| 32 | 61 | <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> |
| 33 | 62 | <div class="flex absolute right-5 top-3.5 z-20 items-center min-h-7" data-shell-controls-row> |
| 34 | 63 | <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> | |
| 37 | 66 | </button> |
| 38 | 67 | <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> |
| 39 | 68 | <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 | ||
| 87 | 116 | <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> |
| 88 | 117 | </a> |
| 89 | 118 | </aside> |
| 119 | </div> | |
| 90 | 120 | </template> |
| @@ -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> | |
| @@ -26,7 +26,7 @@ | ||
| 26 | 26 | <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> |
| 27 | 27 | </label> |
| 28 | 28 | <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> | |
| 30 | 30 | </div> |
| 31 | 31 | <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> |
| 32 | 32 | </form> |
| @@ -53,8 +53,8 @@ | ||
| 53 | 53 | </div> |
| 54 | 54 | <div class="flex flex-col gap-2 items-end"> |
| 55 | 55 | <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> | |
| 58 | 58 | </div> |
| 59 | 59 | </article> |
| 60 | 60 | <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 @@ | ||
| 65 | 65 | </div> |
| 66 | 66 | <div class="flex flex-col gap-2 items-end"> |
| 67 | 67 | <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> | |
| 70 | 70 | </div> |
| 71 | 71 | </article> |
| 72 | 72 | <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 @@ | ||
| 77 | 77 | </div> |
| 78 | 78 | <div class="flex flex-col gap-2 items-end"> |
| 79 | 79 | <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> | |
| 82 | 82 | </div> |
| 83 | 83 | </article> |
| 84 | 84 | <article class="flex gap-5 items-center justify-between p-3.5 bg-zinc-50/80 border border-zinc-200 rounded-xl opacity-80"> |
| @@ -18,7 +18,15 @@ const channelSplits = [ | ||
| 18 | 18 | </script> |
| 19 | 19 | |
| 20 | 20 | @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 | ||
| 22 | 30 | <div class="grid gap-3.5 lg:grid-cols-3"> |
| 23 | 31 | @foreach (stats as stat) |
| 24 | 32 | <article class="grid gap-2 p-3.5 bg-white border border-zinc-200 rounded-2xl shadow-sm"> |
| @@ -33,13 +41,10 @@ const channelSplits = [ | ||
| 33 | 41 | <section class="bg-white border border-zinc-200 rounded-2xl shadow-sm"> |
| 34 | 42 | <header class="flex gap-3 items-center justify-between p-3.5 border-b border-zinc-200"> |
| 35 | 43 | <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> | |
| 40 | 45 | </header> |
| 41 | 46 | <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> | |
| 43 | 48 | <desc>Responses over the last 30 days with publishing dates on the x axis and response counts on the y axis.</desc> |
| 44 | 49 | <rect x="0" y="0" width="640" height="280" rx="16" fill="#fafafa"></rect> |
| 45 | 50 | <g data-chart-legend></g> |
| @@ -68,7 +73,7 @@ const channelSplits = [ | ||
| 68 | 73 | <span class="font-bold">{{ channel.label }}</span> |
| 69 | 74 | <span class="text-zinc-500">{{ channel.value }}</span> |
| 70 | 75 | </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"> | |
| 72 | 77 | <div class="h-full bg-blue-600 rounded-full" style="width: "></div> |
| 73 | 78 | </div> |
| 74 | 79 | </div> |
| @@ -4,11 +4,18 @@ | ||
| 4 | 4 | @section('description', 'Write, schedule, publish, and review social posts from a clean personal workspace.') |
| 5 | 5 | |
| 6 | 6 | @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"> | |
| 8 | 8 | <!-- Crosswind safelist: utilities applied dynamically from the script below |
| 9 | 9 | (provider toggles, active-post highlight, connection summary, char |
| 10 | 10 | counter) must appear in static markup so the CSS scanner emits them. --> |
| 11 | 11 | <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> | |
| 12 | 19 | <div class="grid"> |
| 13 | 20 | <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"> |
| 14 | 21 | <header class="flex flex-wrap gap-3 items-center justify-between p-3 border-b border-zinc-200"> |
| @@ -20,9 +27,9 @@ | ||
| 20 | 27 | </a> |
| 21 | 28 | </div> |
| 22 | 29 | <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> | |
| 26 | 33 | </div> |
| 27 | 34 | </header> |
| 28 | 35 | |
| @@ -41,7 +48,7 @@ | ||
| 41 | 48 | <span class="h-3.5 w-3.5 i-hugeicons-edit-02" aria-hidden="true"></span> |
| 42 | 49 | <span data-post-label>Post 1</span> |
| 43 | 50 | </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> | |
| 45 | 52 | <span class="h-3.5 w-3.5 i-hugeicons-cancel-01" aria-hidden="true"></span> |
| 46 | 53 | </button> |
| 47 | 54 | </header> |
| @@ -52,15 +59,15 @@ | ||
| 52 | 59 | </div> |
| 53 | 60 | |
| 54 | 61 | <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> | |
| 56 | 63 | <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> | |
| 58 | 65 | <span class="h-4 w-4 i-hugeicons-plus-sign" aria-hidden="true"></span> |
| 59 | 66 | </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> | |
| 61 | 68 | <span class="h-4 w-4 i-hugeicons-view" aria-hidden="true"></span> |
| 62 | 69 | </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> | |
| 64 | 71 | <span class="h-4 w-4 i-hugeicons-sent" aria-hidden="true"></span> |
| 65 | 72 | <span data-bluesky-publish-label>Publish</span> |
| 66 | 73 | </button> |
| @@ -251,7 +258,7 @@ | ||
| 251 | 258 | <span class="h-3.5 w-3.5 i-hugeicons-edit-02" aria-hidden="true"></span> |
| 252 | 259 | <span data-post-label>Post ${number}</span> |
| 253 | 260 | </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> | |
| 255 | 262 | <span class="h-3.5 w-3.5 i-hugeicons-cancel-01" aria-hidden="true"></span> |
| 256 | 263 | </button> |
| 257 | 264 | </header> |
| @@ -6,6 +6,7 @@ | ||
| 6 | 6 | <meta name="color-scheme" content="light"> |
| 7 | 7 | <meta name="description" content="@yield('description', 'Postline is a clean personal workspace for social publishing.')"> |
| 8 | 8 | <title>@yield('title', 'Postline')</title> |
| 9 | <link rel="icon" href="/favicon.svg" type="image/svg+xml"> | |
| 9 | 10 | <script> |
| 10 | 11 | ;(() => { |
| 11 | 12 | document.documentElement.setAttribute('data-theme', 'light') |
| @@ -84,17 +85,28 @@ | ||
| 84 | 85 | transform: translate3d(0, 0, 0) scale(1); |
| 85 | 86 | pointer-events: auto; |
| 86 | 87 | } |
| 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 | } | |
| 87 | 99 | </style> |
| 88 | 100 | <link rel="stylesheet" href="/assets/styles/postline-toast.css"> |
| 89 | 101 | <link rel="stylesheet" href="/assets/styles/postline-timeline.css"> |
| 90 | 102 | </head> |
| 91 | 103 | <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> | |
| 93 | 105 | <PostlineSidebar /> |
| 94 | 106 | |
| 95 | 107 | <div class="relative min-w-0 bg-[#f3f3f1]" data-postline-content-shell> |
| 96 | 108 | <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" | |
| 98 | 110 | type="button" |
| 99 | 111 | aria-label="Expand sidebar" |
| 100 | 112 | title="Expand sidebar" |
| @@ -103,14 +115,14 @@ | ||
| 103 | 115 | > |
| 104 | 116 | <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> |
| 105 | 117 | </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> | |
| 107 | 119 | <main class="p-3.5 sm:p-4 min-w-0" data-postline-router> |
| 108 | 120 | @yield('content') |
| 109 | 121 | </main> |
| 110 | 122 | </section> |
| 111 | 123 | </div> |
| 112 | 124 | </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> | |
| 114 | 126 | <script type="module" src="/assets/scripts/postline-toast.ts"></script> |
| 115 | 127 | <script type="module" src="/assets/scripts/postline-bluesky.ts"></script> |
| 116 | 128 | <script type="module" src="/assets/scripts/postline-linkedin.ts"></script> |
| @@ -302,9 +314,26 @@ | ||
| 302 | 314 | }) |
| 303 | 315 | } |
| 304 | 316 | |
| 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 | ||
| 305 | 333 | bindSidebarCollapse() |
| 306 | 334 | bindShellHistory() |
| 307 | 335 | bindWindowDragRegions() |
| 336 | bindKeyboardShortcuts() | |
| 308 | 337 | syncNativeShell() |
| 309 | 338 | window.addEventListener('stx:load', bindShellHistory) |
| 310 | 339 | window.addEventListener('stx:load', bindSidebarCollapse) |
| @@ -4,6 +4,12 @@ | ||
| 4 | 4 | @section('description', 'Review scheduled and drafted social posts in Postline.') |
| 5 | 5 | |
| 6 | 6 | <script server> |
| 7 | const 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 | ||
| 7 | 13 | const scheduledPosts = [ |
| 8 | 14 | { |
| 9 | 15 | time: 'Today 3:30 PM', |
| @@ -26,24 +32,29 @@ const scheduledPosts = [ | ||
| 26 | 32 | text: 'Cross-posting should feel intentional, not like copy/paste chores across six tabs.', |
| 27 | 33 | note: 'Driver support planned after Bluesky settles.', |
| 28 | 34 | }, |
| 29 | ] | |
| 35 | ].map(post => ({ ...post, statusClass: statusStyles[post.status] || statusStyles.Planned })) | |
| 30 | 36 | </script> |
| 31 | 37 | |
| 32 | 38 | @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 | ||
| 34 | 54 | <section class="bg-white border border-zinc-200 rounded-2xl shadow-sm"> |
| 35 | 55 | <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> | |
| 47 | 58 | </header> |
| 48 | 59 | <div class="grid gap-2.5 p-3"> |
| 49 | 60 | @foreach (scheduledPosts as post) |
| @@ -53,12 +64,11 @@ const scheduledPosts = [ | ||
| 53 | 64 | <span class="font-bold text-xs">{{ post.time }}</span> |
| 54 | 65 | <span class="text-xs text-zinc-500">{{ post.channel }}</span> |
| 55 | 66 | </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> | |
| 57 | 68 | </header> |
| 58 | 69 | <p class="m-0 leading-normal text-[13px] text-slate-700">{{ post.text }}</p> |
| 59 | 70 | <footer class="flex items-center justify-between text-xs text-zinc-500"> |
| 60 | 71 | <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> | |
| 62 | 72 | </footer> |
| 63 | 73 | </article> |
| 64 | 74 | @endforeach |
| @@ -9,7 +9,7 @@ | ||
| 9 | 9 | <div> |
| 10 | 10 | <p class="mb-1.5 font-bold text-[11px] text-zinc-500 tracking-wider uppercase">Settings</p> |
| 11 | 11 | <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> | |
| 13 | 13 | </div> |
| 14 | 14 | </header> |
| 15 | 15 | |
| @@ -18,12 +18,12 @@ | ||
| 18 | 18 | <header class="flex gap-3 items-center justify-between pb-3 border-b border-zinc-200"> |
| 19 | 19 | <div> |
| 20 | 20 | <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> | |
| 22 | 22 | </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> | |
| 24 | 24 | </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> | |
| 27 | 27 | </article> |
| 28 | 28 | |
| 29 | 29 | <article class="grid gap-3 p-4 bg-white border border-zinc-200 rounded-2xl shadow-sm"> |
| @@ -41,7 +41,7 @@ | ||
| 41 | 41 | </label> |
| 42 | 42 | <label class="flex gap-4 items-center justify-between text-[13px] text-slate-700"> |
| 43 | 43 | <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> | |
| 45 | 45 | </label> |
| 46 | 46 | </article> |
| 47 | 47 | </section> |
| @@ -9,11 +9,11 @@ | ||
| 9 | 9 | <div> |
| 10 | 10 | <p class="mb-1.5 font-bold text-[11px] text-zinc-500 tracking-wider uppercase">Timeline</p> |
| 11 | 11 | <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> | |
| 13 | 13 | </div> |
| 14 | 14 | <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> | |
| 17 | 17 | <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> |
| 18 | 18 | <span data-bluesky-timeline-refresh-label>Refresh</span> |
| 19 | 19 | </button> |
| @@ -23,10 +23,10 @@ | ||
| 23 | 23 | <div class="overflow-hidden bg-white border border-zinc-200 rounded-2xl shadow-sm postline-feed" aria-label="Bluesky timeline"> |
| 24 | 24 | <div class="postline-feed__header"> |
| 25 | 25 | <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> | |
| 27 | 27 | <p class="m-0 mt-0.5 text-xs text-zinc-500" data-bluesky-timeline-meta>Loading feed…</p> |
| 28 | 28 | </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> | |
| 30 | 30 | </div> |
| 31 | 31 | <div class="postline-feed__list" data-bluesky-timeline-list> |
| 32 | 32 | <p class="postline-feed__loading" data-bluesky-timeline-empty>Loading your timeline…</p> |