smoodly 0.0.61 → 0.0.62

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.
@@ -34,8 +34,8 @@ const TOKENS_LIGHT = `
34
34
  --selection-global:#C4620C;
35
35
  --hover:rgba(21,23,39,.05); --press:rgba(21,23,39,.09); --control-hover:#ECEDF1; --canvas-ground:#EDEDED;
36
36
  --btn-secondary-bg:var(--surface); --btn-secondary-hover:#ECEDF1;
37
- /* The rail sits on the frame: its own greys, the active tile a white tint. */
38
- --rail-fg:#B9BDCE; --rail-active-fg:#FFFFFF; --rail-active-bg:rgba(255,255,255,.14); --rail-hover-bg:rgba(255,255,255,.08);
37
+ /* The navigation bar sits on the frame: its own greys, the active item a white tint, its dark menus a step lighter than the frame. */
38
+ --navbar-fg:#B9BDCE; --navbar-active-fg:#FFFFFF; --navbar-active-bg:rgba(255,255,255,.14); --navbar-hover-bg:rgba(255,255,255,.08); --navbar-menu-bg:#242840;
39
39
  --bar-bg:transparent; --bar-border:0; --menu-border:0; --card-divider:0;
40
40
  --card-title-size:var(--text-md); --card-title-color:var(--text);
41
41
  /* In-card segmented (tabs, the span bar): white track, the chosen option in the tile grey. */
@@ -52,7 +52,7 @@ const TOKENS_LIGHT = `
52
52
  dark mode that washed out over the white page, 2026-09-10). */
53
53
  --plus-bg:#DDEBE4; --plus-hover:#D0E3D9; --plus-fg:#1C6E5B;
54
54
  --plus-icon-bg:#1C6E5B; --plus-icon-fg:#FFFFFF; --plus-shadow:none;
55
- --logo-color:var(--accent); --logo-outer:#0B5A49; --logo-inner:#5FC9A0; /* the rail's app icon: deep green square, mint s (2026-10-23) */
55
+ --logo-color:var(--accent); --logo-outer:#0B5A49; --logo-inner:#5FC9A0; /* the bar's app icon: deep green square, mint s (2026-10-23) */
56
56
 
57
57
  --status-published-fg:var(--green-700); --status-published-bg:var(--green-50);
58
58
  --status-changes-fg:var(--amber-700); --status-changes-bg:var(--amber-50);
@@ -78,7 +78,7 @@ const TOKENS_LIGHT = `
78
78
  `;
79
79
  /* The handoff's dark proposal (VisualEditor.md, 8c right board): the frame
80
80
  and sheet a step darker, tiles a step lighter, fields in the sheet tone;
81
- the rail and the logo unchanged. */
81
+ the bar and the logo unchanged. */
82
82
  const TOKENS_DARK = `
83
83
  --accent:#6CC4A6; --accent-hover:#82D2B8; --accent-soft:rgba(108,196,166,.18); --accent-line:#6CC4A6; --on-accent:#151727;
84
84
  --selection-global:#F09A5C;
@@ -133,10 +133,10 @@ const SCALE = `
133
133
  --control-h:40px; --control-h-sm:34px; --control-h-lg:48px; --button-h:36px; --button-h-sm:28px; --row-h:48px; --topbar-h:64px;
134
134
  --outline-row-h:36px; --seg-pad-y:7px; --tabs-pad:3px;
135
135
  --plus-h:32px; --plus-pad:0 14px 0 6px; --plus-label:inline;
136
- --nav-w:72px; --inspector-w:360px; --gutter:20px;
137
- /* The frame is 8px above, right of and below the sheet (6px in the handoff, +2px 2026-10-23); the rail fills the left edge. */
138
- --frame-pad:8px; --frame-pad-top:8px; --shell-gap:0px; --sheet-radius:20px; --sheet-pad:0 24px 24px;
139
- --rail-pad:18px 6px 16px; --rail-logo:block; --rail-logo-opacity:1; --rail-tile-w:44px; --rail-tile-h:40px; --rail-tile-radius:12px;
136
+ --inspector-w:360px; --gutter:20px;
137
+ /* The frame is 8px left of, right of and below the sheet (6px in the handoff, +2px 2026-10-23); the bar fills the top edge. */
138
+ --frame-pad:8px; --shell-gap:0px; --sheet-radius:20px; --sheet-pad:0 24px 24px;
139
+ --navbar-h:64px; --navbar-pad-x:12px; --navbar-item-h:40px; --navbar-item-radius:10px;
140
140
  --topbar-pad:0; --back-size:32px; --back-bg:var(--surface);
141
141
  --table-pad:8px 12px; --table-top:80px; --canvas-pad:28px 40px 0; --card-pad:16px 18px; --card-gap:12px; --form-pad:32px 40px;
142
142
  --border:1px solid var(--line);
@@ -182,106 +182,106 @@ html:has(.sm-admin),body:has(.sm-admin){overscroll-behavior:none}
182
182
  .sm-code{background:var(--surface);border-radius:8px;padding:10px 12px;margin:0;font:13px/1.5 var(--font-mono);overflow:auto}
183
183
 
184
184
  /* ── shell ─────────────────────────────────────────────────────────── */
185
- /* Rail | main. The shell is the frame (design_handoff_smoodly_neutral/
186
- components/AppShell.html): navy, 8px above, right of and below the main
185
+ /* Bar over main (2026-11-14; a 72px rail on the left before it). The shell is
186
+ the frame (design_handoff_smoodly_neutral/components/AppShell.html): navy,
187
+ the navigation bar across its top, 8px right of, left of and below the main
187
188
  column, which is the white sheet with a 20px radius and its own inner
188
- padding; the rail fills the frame's left edge.
189
+ padding. The shell owns the viewport and the sheet scrolls inside itself,
190
+ so the bar stays put.
189
191
  The main column stacks the top bar over one tile-shaped region: list,
190
192
  form or editor. */
191
193
  .sm-shell{
192
- display:grid;grid-template-columns:var(--nav-w) 1fr;column-gap:var(--shell-gap);
193
- padding:var(--frame-pad-top) var(--frame-pad) var(--frame-pad) 0;height:100vh;height:100dvh;overflow:hidden;background:var(--frame);
194
+ display:grid;grid-template-rows:var(--navbar-h) minmax(0,1fr);grid-template-columns:minmax(0,1fr);row-gap:var(--shell-gap);
195
+ padding:0 var(--frame-pad) var(--frame-pad);height:100vh;height:100dvh;overflow:hidden;background:var(--frame);
194
196
  }
195
197
  .sm-shell>.sm-main{
196
198
  min-width:0;min-height:0;display:flex;flex-direction:column;overflow:hidden;
197
199
  background:var(--sheet);border-radius:var(--sheet-radius);padding:var(--sheet-pad);
198
200
  }
199
201
  .sm-wordmark{height:18px;width:auto;color:var(--logo-color);filter:var(--logo-filter);flex:none}
200
- /* The IconRail (design_handoff_smoodly_neutral/components/IconRail.html):
201
- a 72px rail on the frame with the app icon on top, 18px Lucide icons
202
- over a 10px label and a 44×40 active tile in a white tint — the rail's
203
- own colour tokens, since the frame is navy in both themes. */
204
- .sm-rail{
205
- width:var(--nav-w);height:100%;display:flex;flex-direction:column;align-items:center;gap:18px;
206
- padding:var(--rail-pad);background:var(--bar-bg);border-right:var(--bar-border);color:var(--rail-fg);
207
- font:500 10px/1 var(--font-sans);position:relative;
208
- }
202
+ /* The navigation bar: the app icon on the left, the items centred, Deploy and
203
+ the avatar on the right. The rail's colours, icons and order: 18px Lucide
204
+ icons beside a 14px label, the active item a white-tint pill. */
205
+ .sm-navbar{
206
+ display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:16px;min-width:0;
207
+ padding:0 var(--navbar-pad-x);background:var(--bar-bg);color:var(--navbar-fg);
208
+ font:500 var(--text-md)/1 var(--font-sans);position:relative;z-index:30;
209
+ }
210
+ .sm-navbar__start{display:flex;align-items:center;justify-self:start}
211
+ .sm-navbar__end{display:flex;align-items:center;gap:10px;justify-self:end}
209
212
  /* The app icon (the Smoodly menu's trigger): the deep green square with the
210
- mint s, 26px wide (2026-10-23; the lozenge before it was 20px, asked for
211
- 2026-09-26, at .8 opacity). */
212
- .sm-rail__brand{appearance:none;background:none;border:0;padding:0;margin-bottom:6px;cursor:pointer;display:block;border-radius:6px;color:inherit}
213
+ mint s (2026-10-23). */
214
+ .sm-navbar__brand{appearance:none;background:none;border:0;padding:0;cursor:pointer;display:block;border-radius:8px;color:inherit}
213
215
  /* Hover (2026-10-23): the app icon and the avatar brighten a touch. */
214
- .sm-rail__logo{transition:filter .15s}
215
- .sm-rail__brand:hover .sm-rail__logo{filter:brightness(1.3)}
216
- .sm-rail__user{transition:background .15s}
217
- .sm-rail__user:hover{background:color-mix(in srgb,var(--accent-soft),#fff 60%)}
218
- .sm-rail__brand:focus-visible{outline:2px solid var(--rail-active-fg);outline-offset:2px;box-shadow:none}
219
- .sm-rail__logo{width:26px;height:auto;display:var(--rail-logo);flex:none;opacity:var(--rail-logo-opacity)}
220
- .sm-rail__ext{width:13px;height:13px;flex:none;margin-left:12px;opacity:.45}
221
- .sm-rail__menu-item:hover .sm-rail__ext,.sm-rail__menu-item:focus-visible .sm-rail__ext{opacity:.8}
222
- .sm-rail__menu-version{font-size:var(--text-xs);color:var(--text-subtle);padding:7px 10px 4px;margin-top:3px;border-top:1px solid var(--line)}
223
- .sm-rail__list{display:flex;flex-direction:column;gap:8px;width:100%;margin:0;padding:0;list-style:none}
224
- .sm-rail__item{position:relative}
225
- .sm-rail__btn{
226
- appearance:none;background:none;border:0;margin:0;padding:2px 0;width:100%;display:flex;flex-direction:column;
227
- align-items:center;gap:6px;color:inherit;font:inherit;cursor:pointer;border-radius:var(--radius-md);text-decoration:none;
228
- }
229
- .sm-rail__btn:hover{color:inherit}
230
- .sm-rail__btn:focus-visible{outline:2px solid var(--rail-active-fg);outline-offset:-2px;box-shadow:none}
231
- .sm-rail__tile{position:relative;width:var(--rail-tile-w);height:var(--rail-tile-h);border-radius:var(--rail-tile-radius);display:grid;place-items:center;transition:background .12s}
232
- .sm-rail__btn:hover .sm-rail__tile{background:var(--rail-hover-bg)}
233
- .sm-rail__btn[aria-current="page"],.sm-rail__btn[aria-expanded="true"]{color:var(--rail-active-fg);font-weight:600}
234
- .sm-rail__btn[aria-current="page"]:hover,.sm-rail__btn[aria-expanded="true"]:hover{color:var(--rail-active-fg)}
235
- .sm-rail__btn[aria-current="page"] .sm-rail__tile,.sm-rail__btn[aria-expanded="true"] .sm-rail__tile{background:var(--rail-active-bg)}
236
- .sm-rail__tile svg{width:18px;height:18px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
237
- /* A collection's own title can be long: it ends in an ellipsis inside the rail (2026-11-13). */
238
- .sm-rail__label{letter-spacing:.01em;white-space:nowrap;max-width:100%;overflow:hidden;text-overflow:ellipsis}
239
- /* Not greyed: the reference renders every inactive item at --text and marks
240
- the unavailable ones only by being inert (no pointer, no hover). */
241
- .sm-rail__btn[aria-disabled="true"]{cursor:default}
242
- .sm-rail__btn[aria-disabled="true"]:hover .sm-rail__tile{background:transparent}
243
- /* The Deploy tile (spec 2026-10-09 static mode §6): above the avatar; the badge is the count waiting, the
244
- lit tile a build in flight, the warning tile a build that has not landed or a site that did not answer. */
245
- .sm-rail__deploy{margin-top:auto}
246
- .sm-rail__deploy+.sm-rail__foot{margin-top:0}
247
- /* A publish on its way to the site (plan 2026-10-14 publish through the queue): a quiet line above the Deploy tile
248
- and the avatar, the warning colour when a delivery landed late or with an error. */
249
- .sm-rail__badge{position:absolute;top:-4px;right:-6px;min-width:16px;height:16px;padding:0 4px;border-radius:var(--radius-round);background:var(--accent);color:#fff;font-size:10px;font-weight:600;line-height:16px;text-align:center}
250
- .sm-rail__tile--lit{background:var(--accent-soft);color:var(--accent);animation:sm-rail-pulse 1.6s ease-in-out infinite}
251
- .sm-rail__tile--warn{background:var(--warn-soft);color:var(--warn)}
252
- @keyframes sm-rail-pulse{0%,100%{opacity:1}50%{opacity:.55}}
253
- .sm-rail__menu-detail{font-size:var(--text-sm);color:var(--text);padding:6px 8px 4px;white-space:normal}
254
- .sm-rail__menu-note{font-size:var(--text-xs);color:var(--text-muted);padding:4px 8px 6px;white-space:normal}
255
- .sm-rail__menu-item:disabled{cursor:default;color:var(--text-muted)}
256
- .sm-rail__menu-item:disabled:hover{background:none}
257
- .sm-rail__foot{margin-top:auto;display:flex;flex-direction:column;align-items:center}
258
- .sm-rail__user{
259
- width:28px;height:28px;border-radius:var(--radius-round);background:var(--accent-soft);color:var(--accent);
260
- display:grid;place-items:center;font-size:var(--text-xs);font-weight:600;border:0;cursor:pointer;padding:0;
261
- }
262
- .sm-rail__user:focus-visible{outline:2px solid var(--rail-active-fg);outline-offset:1px;box-shadow:none}
263
- /* Flyout: 6px right of the rail, aligned with its item (the user menu aligns to the bottom).
216
+ .sm-navbar__logo{width:30px;height:auto;display:block;flex:none;transition:filter .15s}
217
+ .sm-navbar__brand:hover .sm-navbar__logo{filter:brightness(1.3)}
218
+ .sm-navbar__brand:focus-visible{outline:2px solid var(--navbar-active-fg);outline-offset:2px;box-shadow:none}
219
+ .sm-navbar__user{
220
+ width:32px;height:32px;border-radius:var(--radius-round);background:var(--accent-soft);color:var(--accent);
221
+ display:grid;place-items:center;font-size:var(--text-xs);font-weight:600;border:0;cursor:pointer;padding:0;transition:background .15s;
222
+ }
223
+ .sm-navbar__user:hover{background:color-mix(in srgb,var(--accent-soft),#fff 60%)}
224
+ .sm-navbar__user:focus-visible{outline:2px solid var(--navbar-active-fg);outline-offset:2px;box-shadow:none}
225
+ .sm-navbar__ext{width:13px;height:13px;flex:none;margin-left:12px;opacity:.45}
226
+ .sm-navbar__menu-item:hover .sm-navbar__ext,.sm-navbar__menu-item:focus-visible .sm-navbar__ext{opacity:.8}
227
+ .sm-navbar__menu-version{font-size:var(--text-xs);color:var(--text-subtle);padding:7px 10px 4px;margin-top:3px;border-top:1px solid var(--line)}
228
+ .sm-navbar__list{display:flex;align-items:center;gap:4px;margin:0;padding:0;list-style:none;min-width:0}
229
+ .sm-navbar__item{position:relative;list-style:none}
230
+ .sm-navbar__btn{
231
+ appearance:none;background:none;border:0;margin:0;height:var(--navbar-item-h);padding:0 12px;display:flex;align-items:center;gap:8px;
232
+ color:inherit;font:inherit;cursor:pointer;border-radius:var(--navbar-item-radius);text-decoration:none;white-space:nowrap;transition:background .12s,color .12s;
233
+ }
234
+ .sm-navbar__btn--icon{padding:0;width:var(--navbar-item-h);justify-content:center}
235
+ .sm-navbar__btn:hover{color:var(--navbar-active-fg);background:var(--navbar-hover-bg)}
236
+ .sm-navbar__btn:focus-visible{outline:2px solid var(--navbar-active-fg);outline-offset:-2px;box-shadow:none}
237
+ .sm-navbar__btn[aria-current="page"],.sm-navbar__btn[aria-expanded="true"]{color:var(--navbar-active-fg);background:var(--navbar-active-bg)}
238
+ .sm-navbar__btn svg{flex:none;stroke:currentColor;fill:none;stroke-linecap:round;stroke-linejoin:round}
239
+ .sm-navbar__chevron{margin-left:-2px;opacity:.8}
240
+ /* A collection's own title can be long: it ends in an ellipsis (2026-11-13). */
241
+ .sm-navbar__label{letter-spacing:.01em;white-space:nowrap;max-width:160px;overflow:hidden;text-overflow:ellipsis}
242
+ /* A narrow window keeps the icons and drops the labels; each item keeps its name for a screen reader and a tooltip. */
243
+ @media (max-width:820px){
244
+ .sm-navbar__list .sm-navbar__label,.sm-navbar__deploy .sm-navbar__label{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}
245
+ .sm-navbar__btn{padding:0 10px}
246
+ }
247
+ /* The Deploy item (spec 2026-10-09 static mode §6): next to the avatar; the badge is the count waiting, the
248
+ lit item a build in flight, the warning item a build that has not landed or a site that did not answer. */
249
+ .sm-navbar__icon{position:relative;display:grid;place-items:center}
250
+ .sm-navbar__badge{position:absolute;top:-7px;right:-9px;min-width:16px;height:16px;padding:0 4px;border-radius:var(--radius-round);background:var(--accent);color:#fff;font-size:10px;font-weight:600;line-height:16px;text-align:center}
251
+ .sm-navbar__btn--lit,.sm-navbar__btn--lit:hover{background:var(--accent-soft);color:var(--accent);animation:sm-navbar-pulse 1.6s ease-in-out infinite}
252
+ .sm-navbar__btn--warn,.sm-navbar__btn--warn:hover{background:var(--warn-soft);color:var(--warn)}
253
+ @keyframes sm-navbar-pulse{0%,100%{opacity:1}50%{opacity:.55}}
254
+ .sm-navbar__menu-detail{font-size:var(--text-sm);color:var(--text);padding:6px 8px 4px;white-space:normal}
255
+ .sm-navbar__menu-note{font-size:var(--text-xs);color:var(--text-muted);padding:4px 8px 6px;white-space:normal}
256
+ .sm-navbar__menu-item:disabled{cursor:default;color:var(--text-muted)}
257
+ .sm-navbar__menu-item:disabled:hover{background:none}
258
+ /* A menu drops 6px below its item, aligned with its left edge (the end group's with its right edge).
264
259
  Menus float on --menu-bg (the sheet white in the neutral look, so they read over the frame). */
265
- .sm-rail__menu{
266
- position:absolute;left:calc(100% + 6px);top:0;z-index:20;width:168px;padding:6px;margin:0;list-style:none;
260
+ .sm-navbar__menu{
261
+ position:absolute;left:0;top:calc(100% + 6px);z-index:20;width:200px;padding:6px;margin:0;list-style:none;
267
262
  background:var(--menu-bg);border:var(--menu-border);border-radius:var(--radius-md);box-shadow:var(--shadow-pop);
268
- display:none;flex-direction:column;gap:1px;font-size:var(--text-md);font-weight:500;color:var(--text);
269
- }
270
- .sm-rail__menu[data-open="true"]{display:flex}
271
- .sm-rail__menu--up{top:auto;bottom:0}
272
- /* The Collections flyout opens on hover, so it is roomier (the names at --text-lg) and an invisible
273
- bridge spans the 6px gap so the pointer can cross from the rail without closing it. */
274
- .sm-rail__menu--brand{width:220px}
275
- .sm-rail__menu--collections{width:208px;padding:8px;gap:2px;font-size:var(--text-lg)}
276
- .sm-rail__menu--collections::before{content:"";position:absolute;left:-8px;top:0;bottom:0;width:8px}
277
- .sm-rail__menu--collections .sm-rail__menu-item{padding:9px 12px}
278
- .sm-rail__menu-title{font-size:9px;font-weight:600;letter-spacing:var(--tracking-label);text-transform:uppercase;color:var(--text-muted);padding:6px 8px 4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
279
- .sm-rail__menu-item{
263
+ display:none;flex-direction:column;gap:1px;font-size:var(--text-md);font-weight:500;line-height:1.4;color:var(--text);
264
+ }
265
+ .sm-navbar__menu[data-open="true"]{display:flex}
266
+ .sm-navbar__menu--end{left:auto;right:0;width:240px}
267
+ .sm-navbar__menu--brand{width:240px}
268
+ /* An invisible bridge spans the 6px gap so the pointer can cross from Collections into its hover menu. */
269
+ .sm-navbar__menu--collections::before{content:"";position:absolute;left:0;right:0;top:-8px;height:8px}
270
+ .sm-navbar__menu-title{font-size:9px;font-weight:600;letter-spacing:var(--tracking-label);text-transform:uppercase;color:var(--text-muted);padding:6px 8px 4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
271
+ .sm-navbar__menu-item{
280
272
  appearance:none;background:none;border:0;width:100%;display:flex;align-items:center;justify-content:space-between;
281
273
  padding:7px 10px;border-radius:var(--radius-sm);color:inherit;font:inherit;cursor:pointer;text-align:left;text-decoration:none;
282
274
  }
283
- .sm-rail__menu-item:hover,.sm-rail__menu-item:focus-visible{background:var(--hover);color:inherit;outline:0;box-shadow:none}
284
- .sm-rail__menu-item[aria-current="page"],.sm-rail__menu-item[aria-current="page"]:hover{background:var(--nav-active-bg);color:var(--nav-active-fg);font-weight:600}
275
+ .sm-navbar__menu-item:hover,.sm-navbar__menu-item:focus-visible{background:var(--hover);color:inherit;outline:0;box-shadow:none}
276
+ .sm-navbar__menu-item[aria-current="page"],.sm-navbar__menu-item[aria-current="page"]:hover{background:var(--nav-active-bg);color:var(--nav-active-fg);font-weight:600}
277
+ .sm-navbar__menu-label{display:flex;align-items:center;gap:10px;min-width:0}
278
+ .sm-navbar__menu-label>span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
279
+ .sm-navbar__menu-label svg{flex:none;stroke:currentColor;fill:none}
280
+ /* Collections and ⋮ are dark (2026-11-14): they carry the bar's own items, so they take its colours. */
281
+ .sm-navbar__menu--dark{background:var(--navbar-menu-bg);color:var(--navbar-fg);border:0;padding:6px;gap:2px}
282
+ .sm-navbar__menu--dark .sm-navbar__menu-item{padding:9px 12px}
283
+ .sm-navbar__menu--dark .sm-navbar__menu-item:hover,.sm-navbar__menu--dark .sm-navbar__menu-item:focus-visible{background:var(--navbar-hover-bg);color:var(--navbar-active-fg)}
284
+ .sm-navbar__menu--dark .sm-navbar__menu-item[aria-current="page"],.sm-navbar__menu--dark .sm-navbar__menu-item[aria-current="page"]:hover{background:var(--navbar-active-bg);color:var(--navbar-active-fg)}
285
285
  .sm-when-light{display:var(--when-light);align-items:center;gap:8px}
286
286
  .sm-when-dark{display:var(--when-dark);align-items:center;gap:8px}
287
287
  /* .sm-nav-item survives as the inspector's link style (EditorView). */
@@ -488,6 +488,7 @@ html:has(.sm-admin),body:has(.sm-admin){overscroll-behavior:none}
488
488
  /* Inline editing in the canvas (spec 2026-09-10). The outlines are drawn in
489
489
  the overlay from the bridge's field rects; the toolbar floats above the
490
490
  selection. */
491
+ .sm-dim{background:color-mix(in srgb,var(--canvas) 62%,transparent);pointer-events:none}
491
492
  .sm-fieldhover{outline:1px solid color-mix(in srgb,var(--selection) 55%,transparent);outline-offset:2px;border-radius:2px;pointer-events:none}
492
493
  .sm-fieldedit{outline:1.5px solid var(--selection);outline-offset:3px;border-radius:2px;pointer-events:none}
493
494
  .sm-floatbar{
@@ -800,6 +801,13 @@ html:has(.sm-admin),body:has(.sm-admin){overscroll-behavior:none}
800
801
  min-width:0;min-height:0;overflow:hidden;
801
802
  }
802
803
  .sm-canvas__scroll{flex:1;min-height:0;overflow:auto;padding:var(--canvas-pad)}
804
+ /* A global item's canvas (spec 2026-11-14): a row above the frame — where the item is used, and which page shows it. */
805
+ .sm-globalcanvas{display:flex;flex-direction:column;min-width:0;min-height:0}
806
+ .sm-globalcanvas>.sm-canvas{flex:1}
807
+ .sm-globalcanvas__bar{flex:none;display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;padding:0 4px 8px;min-height:28px}
808
+ .sm-globalcanvas__bar .sm-hint{margin:0}
809
+ .sm-globalcanvas__pick{display:flex;align-items:center;gap:8px}
810
+ .sm-globalcanvas__pick .sm-select{width:auto;height:30px;padding:0 10px}
803
811
  .sm-page{
804
812
  position:relative;background:var(--page-bg);border-radius:var(--page-radius);
805
813
  box-shadow:var(--shadow-page);min-height:100%;
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const SMOODLY_VERSION = "0.0.61";
1
+ export declare const SMOODLY_VERSION = "0.0.62";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated by scripts/write-version.mjs from package.json — do not edit.
2
- export const SMOODLY_VERSION = "0.0.61";
2
+ export const SMOODLY_VERSION = "0.0.62";
@@ -26,7 +26,7 @@ export const people = smoodly.collection({
26
26
  | Name | Description |
27
27
  | --- | --- |
28
28
  | `name` | What references and queries use. Keep it stable once entries exist. global, asset and media are reserved. |
29
- | `title` | The label in the admin's rail and list. |
29
+ | `title` | The label in the admin's navigation and list. |
30
30
  | `titleField` | The field whose value names an entry in lists and pickers. Without one, or while it is empty, an entry reads Untitled. |
31
31
  | `fields` | Any builders. A field named slug gives entries a URL part; .localized() gives a field a value per language. |
32
32
  | `path` | Serves each entry at its own URL under this segment. See Entry pages. |
@@ -38,7 +38,7 @@ Register the export in the `collections` list of `smoodly/collections.ts`, which
38
38
 
39
39
  ## Entries in the admin
40
40
 
41
- The *Collections* item in the admin's rail opens a menu of every collection. A collection editors open all day can sit on the rail itself: add `admin: { showInNav: true }` to its schema and it appears after *Pages* under its title, and leaves the menu. A collection's list shows its entries with their title, status and when they changed, 20 to a page; *Previous* and *Next* under the rows move through a longer list, and *Per page* shows 100 or 500 instead, a choice the browser remembers. The box above the rows filters by the collection's `titleField`: it finds the entries whose title holds the text, in any language, and searches the whole collection, not the page on screen. A collection without a `titleField` has no filter. While a filter is on, a hand-ordered list does not drag. *+ New entry* creates an empty draft in the selected language and opens the form at once. The form autosaves, and carries the same *Publish* control as a page, so an entry is published per language like a page. *History* and *Discard changes* work the same way.
41
+ The *Collections* item in the admin's top bar opens a menu of every collection. A collection editors open all day can sit on the bar itself: add `admin: { showInNav: true }` to its schema and it appears after *Pages* under its title, and leaves the menu. A collection's list shows its entries with their title, status and when they changed, 20 to a page; *Previous* and *Next* under the rows move through a longer list, and *Per page* shows 100 or 500 instead, a choice the browser remembers. The box above the rows filters by the collection's `titleField`: it finds the entries whose title holds the text, in any language, and searches the whole collection, not the page on screen. A collection without a `titleField` has no filter. While a filter is on, a hand-ordered list does not drag. *+ New entry* creates an empty draft in the selected language and opens the form at once. The form autosaves, and carries the same *Publish* control as a page, so an entry is published per language like a page. *History* and *Discard changes* work the same way.
42
42
 
43
43
  An entry cannot be deleted while a page, a section or another entry references it. The message says how many, and the media library shows the same for assets.
44
44
 
@@ -52,7 +52,7 @@ function LandingView({ zones }: Props<typeof landingSchema>) {
52
52
  export const LandingPage = smoodly.page(landingSchema, LandingView);
53
53
  ```
54
54
 
55
- The hero zone offers a choice of two openers and takes one of them. The body takes anything. The closing zone always shows the site's one quote, edited under *Site*.
55
+ The hero zone offers a choice of two openers and takes one of them. The body takes anything. The closing zone always shows the site's one quote, edited under *Sections*.
56
56
 
57
57
  > **Note:** `sections: "*"` includes every registered section, the header and footer too. List the sections instead when some are only meant for the layout.
58
58
 
@@ -66,7 +66,7 @@ An empty zone shows as a box in the canvas so it can take a section. On the site
66
66
 
67
67
  ## Locking a zone to a global
68
68
 
69
- `z.locked()` also takes a global with a section, as `closing` does above. The zone then shows that one global on every page of the type. Editors change it once, under *Site* or right in any canvas that shows it, and every page follows. The starter's header and footer take a different route: the layout renders them directly, so they need no zone.
69
+ `z.locked()` also takes a global with a section, as `closing` does above. The zone then shows that one global on every page of the type. Editors change it once, under *Sections* or right in any canvas that shows it, and every page follows. The starter's header and footer take a different route: the layout renders them directly, so they need no zone.
70
70
 
71
71
  A global can also be placed by hand. The starter lists Quote in its body zones, so the picker's *Global* group offers the site quote there, and an editor may still add a one-off Quote from the sections list.
72
72
 
@@ -51,7 +51,7 @@ export default defineConfig({
51
51
  });
52
52
  ```
53
53
 
54
- Publishing does not change the live site. Editors press **Deploy** in the admin's rail when they are done, and the host builds. Give the site the host's deploy hook as `SMOODLY_DEPLOY_HOOK` where `astro build` runs (on Vercel: Settings › Git › Deploy Hooks): the build hands it to Smoodly's API with the site's address; without it the tile shows what is waiting and Deploy stays disabled. `SITE_URL` must be the site's own address, and the build refuses static output without it: the tile reads the build's marker, `/_smoodly/build.json`, from there. It says *Building…* until the new build answers, *N changes* while publishes wait (an unpublish or a delete counts as one too), and *The build has not landed* after ten minutes with no new build, in which case look at the host's log. A host that strips query strings from a cached file can hold the old marker for its max-age.
54
+ Publishing does not change the live site. Editors press **Deploy** in the admin's top bar, next to their avatar, when they are done, and the host builds. Give the site the host's deploy hook as `SMOODLY_DEPLOY_HOOK` where `astro build` runs (on Vercel: Settings › Git › Deploy Hooks): the build hands it to Smoodly's API with the site's address; without it the tile shows what is waiting and Deploy stays disabled. `SITE_URL` must be the site's own address, and the build refuses static output without it: the tile reads the build's marker, `/_smoodly/build.json`, from there. It says *Building…* until the new build answers, *N changes* while publishes wait (an unpublish or a delete counts as one too), and *The build has not landed* after ten minutes with no new build, in which case look at the host's log. A host that strips query strings from a cached file can hold the old marker for its max-age.
55
55
 
56
56
  The admin, its revalidate and preview routes and the preview address still run on demand, so the deployment is files plus one function; a host with no function at all cannot run the admin. A static page has no lists by query string and no draft at its real address: drafts are edited through the preview address, as in live mode. A moved page's old address becomes one of the host's redirects at build, so it is answered without a function too. The cache provider in `astro.config` is unused in static output and may stay.
57
57
 
@@ -20,7 +20,7 @@ Editors sign in at smoodly.io, never on your site: `/admin` sends them there and
20
20
 
21
21
  ## Who is editing what
22
22
 
23
- A lock after a title in Pages, in a collection or in Site means someone else has that item open in the language you are looking at. Hover it for their name. Opening the item anyway shows a line under the top bar: *aleksi is also editing this page.* Nothing is locked; it is a heads-up.
23
+ A lock after a title in Pages, in a collection or in Sections means someone else has that item open in the language you are looking at. Hover it for their name. Opening the item anyway shows a line under the top bar: *aleksi is also editing this page.* Nothing is locked; it is a heads-up.
24
24
 
25
25
  Presence runs on Smoodly's service, over one connection per open admin tab. Without it the admin works exactly the same, minus the locks and the line; a tab that loses its connection reconnects on its own.
26
26
 
@@ -45,7 +45,7 @@ Register the list in the config's `globals`. A visual global's section must be i
45
45
 
46
46
  ## In the admin
47
47
 
48
- *Site* in the rail lists every global with its kind, *Visual* or *Fields*, and its status in the selected language. Each opens a form like an entry's: the fields in schema order, and for a visual global the *Settings* tab's groups after them. The form autosaves, and carries the same *Publish* control, *History* and *Discard changes* as a page. A visual global's page also says on how many pages it is used.
48
+ *Sections*, under ⋮ in the admin's top bar, lists every global with its kind, *Visual* or *Fields*, and its status in the selected language. A *Fields* global opens a form like an entry's, the fields in schema order. A *Visual* global opens on a canvas: a page of the site that shows it, the home page for a `layout` global and otherwise the first page that places it, with the global selected and the rest of the page dimmed. Its fields sit beside the canvas under *Content* and *Settings*, text marked for inline editing can be typed in place, and when several pages place it, *Shown on* picks the page. A visual global no page shows yet opens as a form. Either way the page autosaves, and carries the same *Publish* control, *History* and *Discard changes* as a page.
49
49
 
50
50
  A global exists in every language from the start, so there is nothing to add or delete. Publishing is per language, as for pages. A field's value is shared across languages unless it chains `.localized()`: the starter's contact email is one value, its address is translated.
51
51
 
@@ -25,7 +25,7 @@ export const headerSchema = smoodly.schema.section({
25
25
 
26
26
  `styles: false` takes away the *Spacing & layout* group, because the header's spacing is the design's. The brand is shared across languages and the navigation is localized as a whole, so each language has its own labels and links. The footer follows the same pattern with a tagline, three link columns and a copyright line.
27
27
 
28
- Both are registered in `smoodly/sections.ts` and named with `smoodly.global()` in `smoodly/globals.ts`, which gives each a row under *Site* and its `sample` as starting content. No zone lists them, so the *Add section* picker never offers them. Both are `layout: true` ([Defining a global](../globals/defining.md#layout-globals)): on Astro they arrive with the page's own read, so rendering them costs no extra call.
28
+ Both are registered in `smoodly/sections.ts` and named with `smoodly.global()` in `smoodly/globals.ts`, which gives each a row under *Sections* and its `sample` as starting content. No zone lists them, so the *Add section* picker never offers them. Both are `layout: true` ([Defining a global](../globals/defining.md#layout-globals)): on Astro they arrive with the page's own read, so rendering them costs no extra call.
29
29
 
30
30
  ## Rendering them
31
31
 
@@ -112,6 +112,6 @@ On a single-language site there are no links and the header shows the brand and
112
112
 
113
113
  ## Editing them
114
114
 
115
- Editors change the header and footer under *Site*, or in any page's canvas: both are selectable there, tagged *global*, with their own form and their own *Publish* in the inspector. The header's navigation edits in place like any list with [marks](../content-modeling/marks.md). Publishing a page never publishes the footer, because a footer going live on every page is a deliberate click.
115
+ Editors change the header and footer under *Sections*, or in any page's canvas: both are selectable there, tagged *global*, with their own form and their own *Publish* in the inspector. The header's navigation edits in place like any list with [marks](../content-modeling/marks.md). Publishing a page never publishes the footer, because a footer going live on every page is a deliberate click.
116
116
 
117
117
  Next, [The media library](../media/library.md).
@@ -4,7 +4,7 @@ Every image, video and file editors upload lives in one library. A field stores
4
4
 
5
5
  ## Uploading
6
6
 
7
- Editors upload from two places. A media field takes a dropped file straight away, or opens the library to pick one. *Media* in the rail shows the whole library, with an upload button and a filter for images, video and files.
7
+ Editors upload from two places. A media field takes a dropped file straight away, or opens the library to pick one. *Media* in the top bar shows the whole library, with an upload button and a filter for images, video and files.
8
8
 
9
9
  The browser measures an image's dimensions on upload, and captures a frame of a video as its poster, stored as an image asset of its own. What the site can accept is config:
10
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smoodly",
3
- "version": "0.0.61",
3
+ "version": "0.0.62",
4
4
  "description": "Open-source, code-first CMS for Astro and Next.js: define a component once, it becomes a typed, editable building block.",
5
5
  "license": "MIT",
6
6
  "engines": {
@@ -1,109 +0,0 @@
1
- "use client";
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- // packages/smoodly/src/admin/shell/IconRail.tsx
4
- // The admin's primary navigation: the design handoff's 72px IconRail (the
5
- // cool-blue look's components/navigation/IconRail, in the tree until 2026-10-19;
6
- // the shipped tokens are design_handoff_smoodly_neutral/), which replaced the
7
- // 200px SideNav so the visual editor gets the width. Ported markup-for-markup;
8
- // the handoff's click handlers are the shell's router.
9
- //
10
- // Items are registry-derived, never separately configured: Pages, then a
11
- // rail item for each collection registered with `admin.showInNav`
12
- // (2026-11-13), then Collections — one rail item whose flyout lists every
13
- // other collection, absent when none is left — then
14
- // Media, Globals (the global content items, when the registry has any;
15
- // the route is /globals), and Settings (the Settings area, spec
16
- // 2026-09-25; the route is /settings; every member has a page there since
17
- // Redirects, 2026-09-26, so the shell passes `settings` once the role is
18
- // known). The user avatar is pinned to the bottom and opens the account
19
- // flyout: the email, the theme toggle, "My Account" (at smoodly.io) and Sign
20
- // out (spec 2026-10-16 sign-in and the dashboard §6).
21
- //
22
- // Flyouts: click toggles, Esc closes and returns focus to the trigger,
23
- // ↑/↓ move between entries, a pointer down outside closes. A child being
24
- // active highlights its parent tile and the entry inside the menu.
25
- import { useEffect, useId, useRef, useState } from "react";
26
- import { ExternalLink, Globe, Image, Layers, LibraryBig, PanelTop, RadioTower, Settings } from "lucide-react";
27
- import { tileCopy } from "./deploy-tile.js";
28
- import { ThemeToggle } from "../ui/primitives.js";
29
- import { useAdminHref } from "./base.js";
30
- import { consoleLinks } from "./console-links.js";
31
- import { SMOODLY_VERSION } from "../../version.js";
32
- import { RouteLink } from "./RouteLink.js";
33
- /** Lucide at the rail's size and stroke (the tile's CSS restates both). */
34
- const ICON = { size: 18, strokeWidth: 2, "aria-hidden": true };
35
- /** The app icon, inlined: a package can't ship a file into the host app's
36
- * /public. The wordmark's "s" on a rounded square (chosen 2026-10-23; the
37
- * same tile, green with a white s, is smoodly.io's favicon). The square takes
38
- * --logo-outer and the glyph --logo-inner, so a look decides its colours. */
39
- function AppIcon() {
40
- return (_jsxs("svg", { className: "sm-rail__logo", viewBox: "0 0 64 64", fill: "none", xmlns: "http://www.w3.org/2000/svg", role: "img", "aria-label": "Smoodly", children: [_jsx("rect", { width: "64", height: "64", rx: "14", style: { fill: "var(--logo-outer)" } }), _jsx("path", { style: { fill: "var(--logo-inner)" }, transform: "translate(14.382 -4.365) scale(0.7123)", d: "M30.5851 22.9763C36.7891 22.9763 45.6052 24.6681 49.3603 26.5237L46.2583 42.2415C41.6869 40.113 35.2653 38.4212 29.4422 38.4212C26.0137 38.4212 23.7825 39.1852 23.7825 40.9862C23.7825 42.1869 24.9797 42.8964 27.1021 43.333L31.5647 44.3153C40.8162 46.3346 45.5508 51.1373 45.5508 58.505C45.5508 73.4587 33.9592 79.1345 18.939 79.1345C11.4833 79.1345 4.24536 77.3335 0.109375 75.6963L3.42905 58.9416C8.97998 61.8341 16.4356 63.6351 21.2247 63.6351C24.6532 63.6351 26.9933 62.7619 26.9933 60.6334C26.9933 59.4327 26.1226 58.505 23.5648 58.0684L19.7553 57.3043C12.5173 55.9399 6.31336 50.9735 6.31336 43.0055C6.31336 30.6169 15.6737 23.0309 30.6939 23.0309L30.5851 22.9763Z" })] }));
41
- }
42
- /** A menu link that opens smoodly.io in a new tab, with the arrow that says so. */
43
- function ExtLink({ href, onClick, children }) {
44
- return (_jsxs("a", { role: "menuitem", className: "sm-rail__menu-item", href: href, target: "_blank", rel: "noreferrer", onClick: onClick, children: [_jsx("span", { children: children }), _jsx(ExternalLink, { className: "sm-rail__ext", size: 13, strokeWidth: 1.75, "aria-hidden": "true" })] }));
45
- }
46
- export function IconRail({ registry, active, email, settings = true, deploy, onSignOut }) {
47
- const adminHref = useAdminHref();
48
- const root = useRef(null);
49
- const uid = useId();
50
- const [open, setOpen] = useState(null);
51
- // Collections opens on hover as well (spec 2026-09-15 rail tweaks): a hover
52
- // open closes when the pointer leaves and never steals focus; a click open
53
- // is sticky and moves focus into the menu for the keyboard.
54
- const [hovered, setHovered] = useState(false);
55
- const menuId = (key) => `sm-rail-menu${uid}${key}`;
56
- const toggle = (key) => setOpen((cur) => (cur === key ? null : key));
57
- const close = () => setOpen(null);
58
- const hoverLeave = () => setHovered(false);
59
- // A click open moves focus into the menu; a pointer down outside closes it.
60
- // Outside means outside the menu and its trigger — the rest of the rail
61
- // counts too (2026-09-26); the trigger is left to its own toggle.
62
- useEffect(() => {
63
- if (!open || !root.current)
64
- return;
65
- if (!hovered)
66
- root.current.querySelector(`#${CSS.escape(menuId(open))} [role=menuitem]`)?.focus();
67
- const onPointerDown = (e) => {
68
- if (!(e.target instanceof Node))
69
- return;
70
- const menu = root.current?.querySelector(`#${CSS.escape(menuId(open))}`);
71
- const trigger = root.current?.querySelector(`[aria-controls="${menuId(open)}"]`);
72
- if (menu?.contains(e.target) || trigger?.contains(e.target))
73
- return;
74
- close();
75
- };
76
- document.addEventListener("pointerdown", onPointerDown);
77
- return () => document.removeEventListener("pointerdown", onPointerDown);
78
- // eslint-disable-next-line react-hooks/exhaustive-deps
79
- }, [open]);
80
- const onKeyDown = (e) => {
81
- if (!open || !root.current)
82
- return;
83
- if (e.key === "Escape") {
84
- close();
85
- root.current.querySelector(`[aria-controls="${menuId(open)}"]`)?.focus();
86
- }
87
- else if (e.key === "ArrowDown" || e.key === "ArrowUp") {
88
- const items = Array.from(root.current.querySelectorAll(`#${CSS.escape(menuId(open))} [role=menuitem]`));
89
- if (!items.length)
90
- return;
91
- e.preventDefault();
92
- const i = items.indexOf(document.activeElement);
93
- items[(i + (e.key === "ArrowDown" ? 1 : -1) + items.length) % items.length].focus();
94
- }
95
- };
96
- const link = (key, label, icon, href) => (_jsx("li", { className: "sm-rail__item", children: _jsxs(RouteLink, { className: "sm-rail__btn", href: href, "aria-current": key === active ? "page" : undefined, onClick: close, children: [_jsx("span", { className: "sm-rail__tile", children: icon }), _jsx("span", { className: "sm-rail__label", title: label, children: label })] }) }, key));
97
- // A collection with `admin.showInNav` is a rail item of its own; the rest stay in the Collections menu.
98
- const pinned = registry.collections.filter((c) => c.inNav);
99
- const collections = registry.collections.filter((c) => !c.inNav);
100
- const collectionActive = collections.some((c) => c.name === active);
101
- const collectionsOpen = open === "collections" || (hovered && open === null);
102
- const name = email?.split("@")[0] ?? "Editor";
103
- const initials = name.slice(0, 2).toUpperCase();
104
- return (_jsxs("nav", { className: "sm-rail", "aria-label": "Primary", ref: root, onKeyDown: onKeyDown, children: [_jsxs("div", { className: "sm-rail__item", children: [_jsx("button", { type: "button", className: "sm-rail__brand", title: "Smoodly", "aria-haspopup": "menu", "aria-expanded": open === "brand", "aria-controls": menuId("brand"), onClick: () => toggle("brand"), children: _jsx(AppIcon, {}) }), _jsxs("ul", { className: "sm-rail__menu sm-rail__menu--brand", role: "menu", id: menuId("brand"), "data-open": open === "brand", children: [_jsx("li", { role: "none", children: _jsx(ExtLink, { href: consoleLinks().project, onClick: close, children: "This site at smoodly.io" }) }), _jsx("li", { role: "none", children: _jsx(ExtLink, { href: consoleLinks().docs, onClick: close, children: "Documentation" }) }), _jsxs("li", { className: "sm-rail__menu-version", role: "presentation", children: ["Smoodly ", SMOODLY_VERSION] })] })] }), _jsxs("ul", { className: "sm-rail__list", children: [link("pages", "Pages", _jsx(PanelTop, { ...ICON }), adminHref("/pages")), pinned.map((c) => link(c.name, c.title, _jsx(Layers, { ...ICON }), adminHref(`/${c.name}`))), collections.length > 0 && (_jsxs("li", { className: "sm-rail__item", onMouseEnter: () => setHovered(true), onMouseLeave: hoverLeave, children: [_jsxs("button", { type: "button", className: "sm-rail__btn", "aria-current": collectionActive ? "page" : undefined, "aria-haspopup": "menu", "aria-expanded": collectionsOpen, "aria-controls": menuId("collections"), onClick: () => toggle("collections"), children: [_jsx("span", { className: "sm-rail__tile", children: _jsx(LibraryBig, { ...ICON }) }), _jsx("span", { className: "sm-rail__label", children: "Collections" })] }), _jsx("ul", { className: "sm-rail__menu sm-rail__menu--collections", role: "menu", id: menuId("collections"), "data-open": collectionsOpen, children: collections.map((c) => (_jsx("li", { role: "none", children: _jsx(RouteLink, { role: "menuitem", className: "sm-rail__menu-item", href: adminHref(`/${c.name}`), "aria-current": c.name === active ? "page" : undefined, onClick: () => { close(); hoverLeave(); }, children: _jsx("span", { children: c.title }) }) }, c.name))) })] })), link("media", "Media", _jsx(Image, { ...ICON }), adminHref("/media")), registry.globals.length > 0 && link("globals", "Site", _jsx(Globe, { ...ICON }), adminHref("/globals")), settings && link("settings", "Settings", _jsx(Settings, { ...ICON }), adminHref("/settings"))] }), deploy && (() => {
105
- const copy = tileCopy(deploy.status, Date.now());
106
- const id = menuId("deploy");
107
- return (_jsxs("div", { className: "sm-rail__item sm-rail__deploy", "data-state": deploy.status.state, children: [_jsxs("button", { type: "button", className: "sm-rail__btn", "aria-haspopup": "menu", "aria-expanded": open === "deploy", "aria-controls": id, onClick: () => toggle("deploy"), title: copy.detail, children: [_jsxs("span", { className: `sm-rail__tile${copy.lit ? " sm-rail__tile--lit" : ""}${copy.warn ? " sm-rail__tile--warn" : ""}`, children: [_jsx(RadioTower, { ...ICON }), copy.badge && _jsx("span", { className: "sm-rail__badge", "aria-label": `${copy.badge} changes waiting`, children: copy.badge })] }), _jsx("span", { className: "sm-rail__label", children: copy.label })] }), _jsxs("ul", { className: "sm-rail__menu sm-rail__menu--up", role: "menu", id: id, "data-open": open === "deploy", children: [_jsx("li", { className: "sm-rail__menu-detail", role: "presentation", children: copy.detail }), _jsx("li", { role: "none", children: _jsx("button", { type: "button", role: "menuitem", className: "sm-rail__menu-item", disabled: copy.action === null || deploy.busy, title: copy.disabledReason ?? undefined, onClick: () => { close(); deploy.onDeploy(); }, children: copy.action === "retry" ? "Deploy again" : "Deploy now" }) }), copy.disabledReason && _jsx("li", { className: "sm-rail__menu-note", role: "presentation", children: copy.disabledReason })] })] }));
108
- })(), _jsxs("div", { className: "sm-rail__item sm-rail__foot", children: [_jsx("button", { type: "button", className: "sm-rail__user", title: name, "aria-haspopup": "menu", "aria-expanded": open === "user", "aria-controls": menuId("user"), onClick: () => toggle("user"), children: initials }), _jsxs("ul", { className: "sm-rail__menu sm-rail__menu--up", role: "menu", id: menuId("user"), "data-open": open === "user", children: [_jsx("li", { className: "sm-rail__menu-title", role: "presentation", children: email ?? name }), _jsx("li", { role: "none", children: _jsx(ThemeToggle, {}) }), _jsx("li", { role: "none", children: _jsx(ExtLink, { href: consoleLinks().account, onClick: close, children: "My Account" }) }), _jsx("li", { role: "none", children: _jsx("button", { type: "button", role: "menuitem", className: "sm-rail__menu-item", onClick: () => { close(); onSignOut(); }, children: "Sign out" }) })] })] })] }));
109
- }