A calm place to write long-form, and publish it to the open social web.
skypress.blog/
1---
2/**
3 * Static loading scene for the `client:only` Studio + Dashboard islands.
4 *
5 * Those islands are browser-only (auth + editor) and can't render server-side,
6 * so until their JS bundle loads Astro shows this `slot="fallback"` markup.
7 * Rather than a bare, left-aligned "Loading…", render the durable page chrome —
8 * a logo-only header (matching AppBar's `status === 'loading'` state, so the
9 * swap to the live bar is seamless) plus a content skeleton — so the page has
10 * shape immediately and hydration doesn't reflow into an empty viewport.
11 *
12 * The fallback is plain server-rendered markup (it is NOT inside the island's
13 * DOM), so this component's scoped styles reach it normally. The header reuses
14 * the global `.app-bar` classes (app-bar.css is imported by both page shells);
15 * only the static logo is shown — nav/identity depend on auth state we don't
16 * have yet.
17 */
18import { skypressMark } from '../lib/brand/skypress-mark';
19
20interface Props {
21 /** Which island is loading; picks the skeleton shape + content width. */
22 variant: 'dashboard' | 'editor';
23}
24const { variant } = Astro.props;
25---
26
27<div class="loading-scene" aria-busy="true">
28 <header class="app-bar">
29 <a class="app-bar__home" href="/" aria-label="SkyPress home">
30 <span class="app-bar__mark" set:html={skypressMark( 24 )} />
31 <span class="app-bar__word">SkyPress</span>
32 </a>
33 </header>
34
35 {
36 variant === 'dashboard' ? (
37 <div class="loading-scene__body loading-scene__body--dash">
38 <div class="loading-scene__head">
39 <span class="sk sk-heading" />
40 <span class="sk sk-button" />
41 </div>
42 <ul class="loading-scene__rows">
43 {[ 0, 1, 2 ].map( () => (
44 <li class="loading-scene__row">
45 <span class="sk sk-logo" />
46 <span class="loading-scene__rowtext">
47 <span class="sk sk-line sk-line--name" />
48 <span class="sk sk-line sk-line--slug" />
49 </span>
50 </li>
51 ) )}
52 </ul>
53 </div>
54 ) : (
55 <div class="loading-scene__body loading-scene__body--editor">
56 <span class="sk sk-mode" />
57 <span class="sk sk-headline" />
58 <span class="sk sk-lede" />
59 <span class="sk sk-surface" />
60 </div>
61 )
62 }
63
64 <p class="loading-scene__sr">Loading…</p>
65</div>
66
67<style>
68 /* Skeleton block: a tinted bar with a sweeping shimmer highlight. */
69 .sk {
70 display: block;
71 border-radius: var(--radius-sm);
72 background: var(--panel);
73 position: relative;
74 overflow: hidden;
75 }
76 .sk::after {
77 content: '';
78 position: absolute;
79 inset: 0;
80 transform: translateX(-100%);
81 background: linear-gradient(
82 90deg,
83 transparent,
84 color-mix(in srgb, var(--paper-raised) 70%, transparent),
85 transparent
86 );
87 animation: loading-shimmer 1.5s ease-in-out infinite;
88 }
89 @keyframes loading-shimmer {
90 100% {
91 transform: translateX(100%);
92 }
93 }
94 @media (prefers-reduced-motion: reduce) {
95 .sk::after {
96 animation: none;
97 }
98 }
99
100 /* Visually-hidden live label — the skeleton conveys "loading" visually, but
101 screen readers get an explicit announcement (paired with aria-busy). */
102 .loading-scene__sr {
103 position: absolute;
104 width: 1px;
105 height: 1px;
106 margin: -1px;
107 padding: 0;
108 overflow: hidden;
109 clip: rect(0, 0, 0, 0);
110 white-space: nowrap;
111 border: 0;
112 }
113
114 /* Content column — mirrors the loaded layouts so the swap doesn't shift. */
115 .loading-scene__body {
116 max-width: 48rem;
117 margin: 0 auto;
118 padding: 0 1.25rem 5rem;
119 }
120 .loading-scene__body--editor {
121 max-width: var(--studio-measure, 60rem);
122 }
123
124 /* Dashboard skeleton — mirrors `.dash__section-head` + a few `.dash__pub` rows. */
125 .loading-scene__head {
126 display: flex;
127 align-items: center;
128 justify-content: space-between;
129 gap: 1rem;
130 margin: 1rem 0 1.5rem;
131 }
132 .sk-heading {
133 width: clamp(10rem, 40vw, 16rem);
134 height: clamp(1.6rem, 4vw, 2.2rem);
135 }
136 .sk-button {
137 width: 9.5rem;
138 height: 2.2rem;
139 flex: none;
140 }
141 .loading-scene__rows {
142 list-style: none;
143 margin: 0;
144 padding: 0;
145 }
146 .loading-scene__row {
147 display: flex;
148 align-items: center;
149 gap: 1rem;
150 padding: 0.9rem 0;
151 border-top: 1px solid var(--line);
152 }
153 .sk-logo {
154 width: 48px;
155 height: 48px;
156 border-radius: 10px;
157 flex: none;
158 }
159 .loading-scene__rowtext {
160 display: flex;
161 flex-direction: column;
162 gap: 0.5rem;
163 flex: 1;
164 min-width: 0;
165 }
166 .sk-line {
167 height: 0.85rem;
168 }
169 .sk-line--name {
170 width: 45%;
171 height: 1rem;
172 }
173 .sk-line--slug {
174 width: 22%;
175 height: 0.7rem;
176 }
177
178 /* Editor skeleton — mode line, title, lede, then the framed writing surface. */
179 .sk-mode {
180 width: 11rem;
181 height: 0.9rem;
182 margin: 0.5rem 0 1.25rem;
183 }
184 .sk-headline {
185 width: 70%;
186 height: clamp(1.9rem, 4vw, 2.6rem);
187 margin-bottom: 0.75rem;
188 }
189 .sk-lede {
190 width: 48%;
191 height: 1.2rem;
192 }
193 .sk-surface {
194 height: 60vh;
195 min-height: 18rem;
196 margin-top: 1.5rem;
197 border-radius: var(--radius);
198 border: 1px solid var(--line-strong);
199 }
200</style>