@floless/app 0.23.0 → 0.23.2

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.
@@ -52729,7 +52729,7 @@ function appVersion() {
52729
52729
  return resolveVersion({
52730
52730
  isSea: isSea2(),
52731
52731
  sqVersionXml: readSqVersionXml(),
52732
- define: true ? "0.23.0" : void 0,
52732
+ define: true ? "0.23.2" : void 0,
52733
52733
  pkgVersion: readPkgVersion()
52734
52734
  });
52735
52735
  }
@@ -52739,7 +52739,7 @@ function resolveChannel(s) {
52739
52739
  return "dev";
52740
52740
  }
52741
52741
  function appChannel() {
52742
- return resolveChannel({ isSea: isSea2(), define: true ? "0.23.0" : void 0 });
52742
+ return resolveChannel({ isSea: isSea2(), define: true ? "0.23.2" : void 0 });
52743
52743
  }
52744
52744
 
52745
52745
  // oauth-presets.ts
package/dist/web/app.css CHANGED
@@ -1799,6 +1799,9 @@
1799
1799
  `hidden` attribute, so it showed even when unsupported (dev/non-Windows). Restore
1800
1800
  the hide with a class+attribute selector (higher specificity than .menu-theme). */
1801
1801
  .menu-startup[hidden] { display: none; }
1802
+ /* Same footgun: .menu-item is display:flex, so JS-hiding the fullscreen row on
1803
+ browsers without the Fullscreen API needs an explicit hide to win over flex. */
1804
+ #menu-fullscreen[hidden] { display: none; }
1802
1805
  .theme-toggle {
1803
1806
  display: flex;
1804
1807
  gap: 2px;
@@ -1865,6 +1868,66 @@
1865
1868
  to { opacity: 1; transform: translateY(0); }
1866
1869
  }
1867
1870
 
1871
+ /* ========== FULLSCREEN NUDGE ==========
1872
+ One-time "need more space?" prompt, bottom-right. Deliberately shares the
1873
+ toast/whats-new notification language (surface card + 3px accent left rail)
1874
+ so it reads as a calm FloLess notice, not a marketing card — no glow, no
1875
+ gradient, no oversized shadow. */
1876
+ .fullscreen-nudge {
1877
+ position: fixed;
1878
+ right: 20px;
1879
+ bottom: 60px; /* clears the footer — matches the toast anchor */
1880
+ z-index: 210; /* above toasts (200): they share this corner */
1881
+ display: flex;
1882
+ align-items: flex-start;
1883
+ gap: 10px;
1884
+ width: 290px;
1885
+ background: var(--surface);
1886
+ border: 1px solid var(--border-strong);
1887
+ border-left: 3px solid var(--accent);
1888
+ border-radius: 6px;
1889
+ padding: 13px 12px 14px 15px;
1890
+ box-shadow: 0 12px 32px rgba(0,0,0,0.45);
1891
+ animation: fsn-in 0.16s ease-out;
1892
+ }
1893
+ /* .fullscreen-nudge is display:flex, which overrides the UA [hidden]{display:none}
1894
+ — restore the hide (same footgun the .menu-startup row hit). */
1895
+ .fullscreen-nudge[hidden] { display: none; }
1896
+ .fsn-text { flex: 1; min-width: 0; }
1897
+ .fsn-title { font-size: 13px; font-weight: 600; color: var(--text); line-height: 1.35; }
1898
+ .fsn-sub { font-size: 11.5px; color: var(--text-muted); line-height: 1.5; margin-top: 4px; }
1899
+ .fsn-go {
1900
+ margin-top: 11px;
1901
+ background: var(--accent);
1902
+ color: #fff;
1903
+ border: 1px solid var(--accent);
1904
+ border-radius: 4px;
1905
+ padding: 6px 12px;
1906
+ font-size: 11.5px;
1907
+ font-weight: 600;
1908
+ cursor: pointer;
1909
+ }
1910
+ .fsn-go:hover { background: var(--accent-bright); border-color: var(--accent-bright); box-shadow: 0 0 14px var(--accent-glow); }
1911
+ .fsn-dismiss {
1912
+ flex: none;
1913
+ background: transparent;
1914
+ border: none;
1915
+ color: var(--text-dim);
1916
+ font-size: 13px;
1917
+ line-height: 1;
1918
+ padding: 2px 4px;
1919
+ margin: -2px -2px 0 0;
1920
+ cursor: pointer;
1921
+ border-radius: 3px;
1922
+ }
1923
+ .fsn-dismiss:hover { color: var(--text); background: var(--surface-2); }
1924
+ @keyframes fsn-in {
1925
+ from { opacity: 0; transform: translateY(6px); }
1926
+ to { opacity: 1; transform: none; }
1927
+ }
1928
+ /* Calm, not flashy — honour reduced-motion by appearing without the fade/slide. */
1929
+ @media (prefers-reduced-motion: reduce) { .fullscreen-nudge { animation: none; } }
1930
+
1868
1931
  /* ========== FIND OVERLAY ========== */
1869
1932
  .find-overlay {
1870
1933
  position: absolute;
package/dist/web/app.js CHANGED
@@ -1042,6 +1042,7 @@ function handleMenuAction(action) {
1042
1042
  case 'integrations': openIntegrations(); break;
1043
1043
  case 'routines': openRoutines(); break;
1044
1044
  case 'report-issue': openReportIssue(); break;
1045
+ case 'fullscreen': toggleFullscreen(); break;
1045
1046
  }
1046
1047
  }
1047
1048
 
@@ -1186,6 +1187,96 @@ if ($startupToggle) {
1186
1187
  // e.g. the user disabling the task in Windows Settings).
1187
1188
  refreshStartup();
1188
1189
 
1190
+ /* ============= FULLSCREEN ============= */
1191
+ // Browser Fullscreen API on the root element — gives the 3-col workspace the whole
1192
+ // screen. The ≡ menu row and a one-time "more space?" nudge both call toggleFullscreen.
1193
+ // The row's icon/label are kept honest by `fullscreenchange` (covers Esc and other
1194
+ // out-of-band exits). If the API is unsupported we hide the row and never nudge —
1195
+ // the app's "hide, never show a dead control" rule (mirrors the startup row).
1196
+
1197
+ const FS_NUDGE_KEY = 'floless-fullscreen-nudge-v1';
1198
+ const $fsRow = document.getElementById('menu-fullscreen');
1199
+ const $fsIcon = document.getElementById('menu-fullscreen-icon');
1200
+ const $fsLabel = document.getElementById('menu-fullscreen-label');
1201
+ const $fsNudge = document.getElementById('fullscreen-nudge');
1202
+
1203
+ // Lucide maximize / minimize (same icon family as the other menu rows).
1204
+ const FS_ICON_ENTER = '<svg viewBox="0 0 24 24"><path d="M8 3H5a2 2 0 0 0-2 2v3"/><path d="M21 8V5a2 2 0 0 0-2-2h-3"/><path d="M3 16v3a2 2 0 0 0 2 2h3"/><path d="M16 21h3a2 2 0 0 0 2-2v-3"/></svg>';
1205
+ const FS_ICON_EXIT = '<svg viewBox="0 0 24 24"><path d="M8 3v3a2 2 0 0 1-2 2H3"/><path d="M21 8h-3a2 2 0 0 1-2-2V3"/><path d="M3 16h3a2 2 0 0 1 2 2v3"/><path d="M16 21v-3a2 2 0 0 1 2-2h3"/></svg>';
1206
+
1207
+ function fullscreenSupported() {
1208
+ return !!(document.fullscreenEnabled && document.documentElement.requestFullscreen);
1209
+ }
1210
+ function isFullscreen() { return !!document.fullscreenElement; }
1211
+
1212
+ function syncFullscreenUi() {
1213
+ const on = isFullscreen();
1214
+ if ($fsLabel) $fsLabel.textContent = on ? 'Exit full screen' : 'Enter full screen';
1215
+ if ($fsIcon) $fsIcon.innerHTML = on ? FS_ICON_EXIT : FS_ICON_ENTER;
1216
+ // Once the user IS fullscreen (by any means), the nudge has served its purpose —
1217
+ // retire it for good so it never reappears in a later session.
1218
+ if (on) dismissFsNudge(true);
1219
+ }
1220
+
1221
+ async function toggleFullscreen() {
1222
+ const wasOn = isFullscreen();
1223
+ try {
1224
+ if (wasOn) await document.exitFullscreen();
1225
+ else await document.documentElement.requestFullscreen();
1226
+ } catch {
1227
+ // The user can't act on a blocked request — a warn toast names what failed.
1228
+ showToast(wasOn ? "Couldn't exit fullscreen" : "Couldn't enter fullscreen", 'warn');
1229
+ }
1230
+ }
1231
+
1232
+ // Retire the nudge. `remember` persists the one-time flag so it never shows again.
1233
+ function dismissFsNudge(remember) {
1234
+ if ($fsNudge && $fsNudge.isConnected) {
1235
+ $fsNudge.hidden = true;
1236
+ $fsNudge.remove(); // keep the a11y tree clean once it's gone
1237
+ }
1238
+ if (remember) { try { localStorage.setItem(FS_NUDGE_KEY, '1'); } catch {} }
1239
+ }
1240
+
1241
+ function nudgeAlreadySeen() {
1242
+ try { return localStorage.getItem(FS_NUDGE_KEY) === '1'; } catch { return false; }
1243
+ }
1244
+
1245
+ // One-time nudge: only when the window is clearly under-using the screen, the app
1246
+ // isn't already fullscreen, no what's-new strip is competing for the corner, and it
1247
+ // was never dismissed/used. Delayed ~2s so the layout settles first.
1248
+ function maybeShowFsNudge() {
1249
+ if (nudgeAlreadySeen()) return;
1250
+ setTimeout(() => {
1251
+ if (!$fsNudge || !$fsNudge.isConnected || !$fsNudge.hidden) return;
1252
+ if (nudgeAlreadySeen() || isFullscreen()) return;
1253
+ const wn = document.getElementById('whats-new-panel');
1254
+ if (wn && !wn.hidden) return; // don't stack on the post-update what's-new strip
1255
+ const h = window.innerHeight;
1256
+ const avail = (window.screen && window.screen.availHeight) || h;
1257
+ if (h < 400) return; // genuinely cramped window — a card is just noise
1258
+ if (h > avail * 0.85) return; // window already uses most of the screen
1259
+ $fsNudge.hidden = false;
1260
+ }, 2000);
1261
+ }
1262
+
1263
+ if (!fullscreenSupported()) {
1264
+ if ($fsRow) $fsRow.hidden = true; // hide the control; never nudge on an unsupported browser
1265
+ } else {
1266
+ document.addEventListener('fullscreenchange', syncFullscreenUi);
1267
+ syncFullscreenUi();
1268
+ // Query the two buttons independently — a present #fullscreen-nudge doesn't by
1269
+ // itself guarantee its children survive a future HTML refactor.
1270
+ const $fsGo = document.getElementById('fsn-go');
1271
+ const $fsDismiss = document.getElementById('fsn-dismiss');
1272
+ if ($fsGo) $fsGo.addEventListener('click', () => {
1273
+ dismissFsNudge(true);
1274
+ toggleFullscreen(); // synchronous within the click → counts as a user gesture
1275
+ });
1276
+ if ($fsDismiss) $fsDismiss.addEventListener('click', () => dismissFsNudge(true));
1277
+ if ($fsNudge) maybeShowFsNudge();
1278
+ }
1279
+
1189
1280
  /* ============= TOAST ============= */
1190
1281
 
1191
1282
  function showToast(msg, type) {
package/dist/web/aware.js CHANGED
@@ -2170,12 +2170,33 @@
2170
2170
  const $canvasMain = document.getElementById('canvas-main');
2171
2171
  const $dropTarget = document.getElementById('canvas-drop-target');
2172
2172
  const isFileDrag = (e) => !!e.dataTransfer && Array.from(e.dataTransfer.types || []).includes('Files');
2173
+ // An in-app node-card drag carries the card id as text/plain (set in the card's dragstart)
2174
+ // and no "Files" type. The canvas must accept its dragover, otherwise the browser paints the
2175
+ // default "no-drop" cursor over the topology while the card is in transit to the Templates
2176
+ // bar — the CSS `.agent-card.dragging { cursor: grabbing }` can't override it because the
2177
+ // browser owns the cursor during a native drag (#96). We mirror the card's
2178
+ // effectAllowed='copy' (and the bar's dropEffect) so it stays a steady copy affordance for
2179
+ // the whole drag instead of flickering between no-drop and copy.
2180
+ // text/plain-without-Files is a deliberate heuristic: a browser text-selection drag matches it
2181
+ // too, but over the canvas that is a harmless no-op (copy cursor in transit, swallowed drop) —
2182
+ // not worth coupling this to the card-only #fav-bar arming state to exclude.
2183
+ const isCardDrag = (e) => !!e.dataTransfer && !isFileDrag(e) && Array.from(e.dataTransfer.types || []).includes('text/plain');
2173
2184
  if ($canvasMain && $dropTarget) {
2185
+ // dragenter stays file-only on purpose: it shows the ".flo import" overlay ($dropTarget),
2186
+ // which must NOT appear for a node-card drag. dragover alone suppresses the no-drop cursor.
2174
2187
  $canvasMain.addEventListener('dragenter', (e) => { if (isFileDrag(e)) { e.preventDefault(); $dropTarget.classList.add('active'); } });
2175
- $canvasMain.addEventListener('dragover', (e) => { if (isFileDrag(e)) e.preventDefault(); }); // required to allow the drop
2188
+ $canvasMain.addEventListener('dragover', (e) => {
2189
+ if (isFileDrag(e)) { e.preventDefault(); return; } // required to allow the file drop
2190
+ if (isCardDrag(e)) { e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; } // suppress the no-drop cursor (#96)
2191
+ });
2176
2192
  $canvasMain.addEventListener('dragleave', (e) => { if (!$canvasMain.contains(e.relatedTarget)) $dropTarget.classList.remove('active'); });
2177
2193
  $canvasMain.addEventListener('drop', (e) => {
2178
- if (!isFileDrag(e)) return; // let node-card drops fall through to the Templates bar
2194
+ // A card dropped on the bare canvas is a no-op: saving as a Template only happens on the
2195
+ // Templates bar (its own drop handler, which runs first as the drop bubbles up). Now that
2196
+ // the canvas accepts the dragover we must swallow that drop so the browser performs no
2197
+ // default action; a drop landing on the bar is already handled before it reaches here.
2198
+ if (isCardDrag(e)) { e.preventDefault(); return; }
2199
+ if (!isFileDrag(e)) return; // anything else: let it fall through untouched
2179
2200
  e.preventDefault();
2180
2201
  $dropTarget.classList.remove('active');
2181
2202
  importFlo(e.dataTransfer.files && e.dataTransfer.files[0]);
@@ -168,6 +168,22 @@
168
168
  <div class="whats-new-detail"><div class="whats-new-detail-inner"></div></div>
169
169
  </div>
170
170
 
171
+ <!-- One-time "need more space?" nudge (mirrors Excel's prompt). Shown ~2s after
172
+ load ONLY when the window is clearly under-using the screen, the app isn't
173
+ already fullscreen, the post-update what's-new strip isn't already showing,
174
+ and it was never dismissed/used (localStorage). position:fixed, so it doesn't disturb the
175
+ .app grid; placed here so Tab order is main → nudge → footer. Calm
176
+ notification styling (shares the toast/whats-new accent rail) — never an ad;
177
+ one dismiss/use retires it for good. aria-live=polite (not alert): passive. -->
178
+ <aside id="fullscreen-nudge" class="fullscreen-nudge" aria-label="Fullscreen suggestion" aria-live="polite" hidden>
179
+ <div class="fsn-text">
180
+ <div class="fsn-title">Need more working space?</div>
181
+ <div class="fsn-sub">Go fullscreen to give FloLess the whole screen.</div>
182
+ <button type="button" class="fsn-go" id="fsn-go">Go fullscreen</button>
183
+ </div>
184
+ <button type="button" class="fsn-dismiss" id="fsn-dismiss" aria-label="Dismiss fullscreen suggestion">✕</button>
185
+ </aside>
186
+
171
187
  <footer>
172
188
  <div class="status">
173
189
  <span class="stat"><span class="dot"></span><span class="stat-val">runtime online</span></span>
@@ -269,6 +285,17 @@
269
285
  <span class="menu-icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/><line x1="4" x2="4" y1="22" y2="15"/></svg></span>
270
286
  <span class="menu-label">Report an issue</span>
271
287
  </button>
288
+ <!-- Browser fullscreen toggle — gives the 3-col workspace the whole screen.
289
+ An action (not a stored pref: browsers won't restore fullscreen on load),
290
+ so it's a .menu-item button, not a switch. Icon + label flip with the live
291
+ fullscreen state via `fullscreenchange`. Hidden by JS when the Fullscreen
292
+ API is unavailable — hide, never show a dead control. No F11 kbd hint: F11
293
+ is the browser's own fullscreen, which is separate from the Fullscreen API
294
+ and wouldn't keep this control's state in sync. -->
295
+ <button class="menu-item" data-action="fullscreen" id="menu-fullscreen" role="menuitem">
296
+ <span class="menu-icon" aria-hidden="true" id="menu-fullscreen-icon"><svg viewBox="0 0 24 24"><path d="M8 3H5a2 2 0 0 0-2 2v3"/><path d="M21 8V5a2 2 0 0 0-2-2h-3"/><path d="M3 16v3a2 2 0 0 0 2 2h3"/><path d="M16 21h3a2 2 0 0 0 2-2v-3"/></svg></span>
297
+ <span class="menu-label" id="menu-fullscreen-label">Enter full screen</span>
298
+ </button>
272
299
  <!-- Start-on-login + Theme are sibling machine preferences (no divider between
273
300
  them — UX review 2026-05-31). Start-on-login toggles the per-user logon
274
301
  Scheduled Task that keeps the local server alive across logins; hidden unless
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floless/app",
3
- "version": "0.23.0",
3
+ "version": "0.23.2",
4
4
  "type": "module",
5
5
  "description": "Thin localhost host for floless.app — serves web/ and shells the aware CLI. No engine, no LLM.",
6
6
  "bin": {