@k2b/cloud 0.38.0 → 0.39.0

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.
Files changed (99) hide show
  1. package/package.json +6 -3
  2. package/scripts/build-canvas-workers.ts +109 -29
  3. package/scripts/build.ts +3 -5
  4. package/src/_internal/app-path.ts +25 -0
  5. package/src/_internal/app-presentation.ts +4 -0
  6. package/src/_internal/canvas-worker.ts +5 -3
  7. package/src/_internal/define-app.ts +28 -2
  8. package/src/_internal/panel-cookie.ts +39 -0
  9. package/src/_internal/registry-validation.ts +21 -26
  10. package/src/_internal/runtime-context.ts +1 -0
  11. package/src/access/PermissionEditor.browser-harness.tsx +57 -38
  12. package/src/access/PermissionEditor.tsx +30 -5
  13. package/src/access/ResourceApiKeys.tsx +5 -1
  14. package/src/ai/approval-routes.ts +5 -2
  15. package/src/ai/approvals.ts +139 -28
  16. package/src/ai/browser.ts +10 -0
  17. package/src/ai/chat/action-sentences.ts +1 -44
  18. package/src/ai/chat/turn-view.tsx +35 -3
  19. package/src/ai/chat/view-block.tsx +7 -2
  20. package/src/ai/chat-task-contracts.ts +28 -3
  21. package/src/ai/chat-task-reasons-migrate.ts +68 -0
  22. package/src/ai/chat-task-reasons.ts +92 -0
  23. package/src/ai/chat-tasks.ts +396 -66
  24. package/src/ai/index.ts +12 -0
  25. package/src/ai/migrate.ts +28 -2
  26. package/src/ai/request-context.ts +7 -1
  27. package/src/ai/routes.ts +27 -1
  28. package/src/ai/runtime.ts +12 -27
  29. package/src/ai/store.ts +5 -3
  30. package/src/ai/timeline.ts +9 -1
  31. package/src/ai/tool-wording.ts +45 -0
  32. package/src/ai/turn-abort.ts +33 -0
  33. package/src/ai/types.ts +12 -1
  34. package/src/api/admin-core-settings.ts +190 -139
  35. package/src/api/admin-outgoing-mail.ts +4 -2
  36. package/src/api/app-approval.ts +9 -2
  37. package/src/api/approvals.ts +1 -1
  38. package/src/api/index.ts +2 -1
  39. package/src/browser/CloudResourceSearch.tsx +10 -6
  40. package/src/browser/app-approval.ts +7 -2
  41. package/src/browser/composer-catalog.ts +103 -0
  42. package/src/browser/composer-insert-messages.ts +31 -0
  43. package/src/browser/composer-insert.ts +150 -0
  44. package/src/browser/panel-messages.ts +24 -0
  45. package/src/browser/panel-slot.ts +354 -0
  46. package/src/browser/panel.ts +28 -0
  47. package/src/browser/resource-picker.tsx +12 -1
  48. package/src/contracts/app-approval.ts +49 -9
  49. package/src/contracts/app.ts +14 -0
  50. package/src/contracts/registry.ts +3 -1
  51. package/src/contracts/shared.ts +2 -0
  52. package/src/server/services/access.ts +5 -1
  53. package/src/services/accounts/posix.ts +2 -1
  54. package/src/services/app-approval.ts +158 -38
  55. package/src/services/approvals/device.ts +2 -0
  56. package/src/services/index.ts +21 -3
  57. package/src/services/notifications/browser.ts +16 -5
  58. package/src/services/pdf/image-protocol.ts +135 -0
  59. package/src/services/pdf/image-variants.ts +124 -0
  60. package/src/services/pdf/image-worker.ts +31 -0
  61. package/src/services/pdf/image.ts +109 -0
  62. package/src/services/pdf/index.ts +9 -0
  63. package/src/services/settings/api.ts +11 -6
  64. package/src/services/settings/app.ts +5 -4
  65. package/src/services/settings/core-settings.ts +4 -3
  66. package/src/services/settings/index.ts +19 -17
  67. package/src/services/settings/namespace.ts +2 -1
  68. package/src/services/settings/store.ts +83 -11
  69. package/src/shared/ai-platform-prompt.ts +1 -1
  70. package/src/shared/app-presentation.ts +1 -0
  71. package/src/shared/redirect.ts +12 -0
  72. package/src/ssr/CloudShell.island.tsx +86 -9
  73. package/src/ssr/Layout.tsx +42 -9
  74. package/src/ssr/LayoutHeader.tsx +15 -9
  75. package/src/ssr/LayoutHelp.tsx +1 -1
  76. package/src/ssr/LayoutRail.tsx +2 -2
  77. package/src/ssr/LayoutTabBar.tsx +29 -0
  78. package/src/ssr/PanelLayout.tsx +27 -0
  79. package/src/ssr/PanelRuntime.island.tsx +87 -0
  80. package/src/ssr/PanelSlot.tsx +108 -0
  81. package/src/ssr/PanelTrigger.tsx +49 -0
  82. package/src/ssr/ShellRuntime.island.tsx +60 -5
  83. package/src/ssr/app-appearance.ts +3 -2
  84. package/src/ssr/app-color.ts +100 -0
  85. package/src/ssr/app-launchpad.tsx +18 -1
  86. package/src/ssr/app-navigation.ts +15 -0
  87. package/src/ssr/compact-shell.baseline.json +5270 -0
  88. package/src/ssr/compact-shell.browser-harness.ts +431 -0
  89. package/src/ssr/index.ts +3 -0
  90. package/src/ssr/panel-slot.browser-harness.tsx +110 -0
  91. package/src/ssr/platform-messages.ts +30 -0
  92. package/src/ssr/rail-navigation.ts +44 -0
  93. package/src/ssr/shell-data.tsx +4 -0
  94. package/src/ssr/shell-shortcuts.ts +13 -2
  95. package/src/styles/effects.css +3 -2
  96. package/src/styles/global.css +269 -55
  97. package/src/styles/panel.css +373 -0
  98. package/src/styles/tokens.css +23 -6
  99. package/src/styles/utilities-navigation.css +5 -21
@@ -1,8 +1,19 @@
1
1
  import type { platformMessages } from "./platform-messages";
2
2
 
3
- export const SHELL_SHORTCUT_ORDER = ["cloud.search", "cloud.help"] as const;
3
+ export const SHELL_SHORTCUT_ORDER = ["cloud.search", "cloud.help", "cloud.panel"] as const;
4
4
  /** The shell's always-present shortcuts, without their actions; ShellRuntime adds the actions, Help renders them before the browser reports commands. */
5
- export const shellShortcuts = (t: ReturnType<typeof platformMessages.resolve>["t"]) => [
5
+ export const shellShortcuts = (t: ReturnType<typeof platformMessages.resolve>["t"], options: { panel?: boolean } = {}) => [
6
6
  { id: "cloud.search", title: t.openGlobalSearch, description: t.globalSearchDescription, icon: "ti ti-search", shortcut: "mod+k" },
7
7
  { id: "cloud.help", title: t.openShortcutHelp, description: t.shortcutHelpDescription, icon: "ti ti-help-circle", shortcut: "shift+/" },
8
+ ...(options.panel
9
+ ? [
10
+ {
11
+ id: "cloud.panel",
12
+ title: t.togglePanel,
13
+ description: t.togglePanelDescription,
14
+ icon: "ti ti-layout-sidebar-right",
15
+ shortcut: "mod+j",
16
+ },
17
+ ]
18
+ : []),
8
19
  ];
@@ -880,14 +880,15 @@
880
880
  flex-direction: column;
881
881
  gap: 0.5rem;
882
882
  border-radius: var(--ui-radius-surface);
883
- background: color-mix(in srgb, var(--app-accent, var(--color-blue-500)) 9%, var(--ui-surface-subtle));
883
+ /* Both fills are computed with the app ink, so its label stays at AA. */
884
+ background: var(--ui-app-selected);
884
885
  padding: 1.25rem 1.25rem 1.375rem;
885
886
  text-decoration: none;
886
887
  transition: background-color 140ms ease;
887
888
  }
888
889
 
889
890
  .help-article-next:hover {
890
- background: color-mix(in srgb, var(--app-accent, var(--color-blue-500)) 15%, var(--ui-surface-subtle));
891
+ background: var(--ui-app-selected-nav);
891
892
  }
892
893
 
893
894
  .help-article-next-label {
@@ -32,6 +32,7 @@
32
32
  @import "./input.css";
33
33
  @import "./resource-search.css";
34
34
  @import "./file-chooser.css";
35
+ @import "./panel.css";
35
36
 
36
37
  @theme {
37
38
  --color-dark: rgb(9 9 11);
@@ -52,17 +53,19 @@
52
53
  }
53
54
 
54
55
  .cloud-app-canvas {
56
+ --cloud-canvas-strength: var(--app-canvas-strength, 0%);
57
+ --cloud-canvas-dark-strength: var(--app-canvas-dark-strength, 0%);
55
58
  padding: var(--ui-space-shell);
56
59
  background: linear-gradient(
57
60
  var(--app-canvas-angle, 135deg),
58
- color-mix(in srgb, var(--app-canvas-from, var(--color-zinc-50)) var(--app-canvas-strength, 0%), var(--color-zinc-50)),
61
+ color-mix(in srgb, var(--app-canvas-from, var(--color-zinc-50)) var(--cloud-canvas-strength), var(--color-zinc-50)),
59
62
  color-mix(
60
63
  in srgb,
61
- var(--app-canvas-via, var(--app-canvas-from, var(--color-zinc-50))) var(--app-canvas-strength, 0%),
64
+ var(--app-canvas-via, var(--app-canvas-from, var(--color-zinc-50))) var(--cloud-canvas-strength),
62
65
  var(--color-zinc-50)
63
66
  )
64
67
  50%,
65
- color-mix(in srgb, var(--app-canvas-to, var(--color-zinc-50)) var(--app-canvas-strength, 0%), var(--color-zinc-50))
68
+ color-mix(in srgb, var(--app-canvas-to, var(--color-zinc-50)) var(--cloud-canvas-strength), var(--color-zinc-50))
66
69
  );
67
70
  }
68
71
 
@@ -88,12 +91,46 @@ html:has(.cloud-app-canvas[data-layout-full-page="true"]) {
88
91
  .dark .cloud-app-canvas {
89
92
  background: linear-gradient(
90
93
  var(--app-canvas-angle, 135deg),
91
- color-mix(in srgb, var(--app-canvas-from, var(--ui-canvas)) var(--app-canvas-dark-strength, 0%), var(--ui-canvas)),
92
- color-mix(in srgb, var(--app-canvas-via-dark, var(--ui-canvas)) var(--app-canvas-dark-strength, 0%), var(--ui-canvas)) 50%,
93
- color-mix(in srgb, var(--app-canvas-to, var(--ui-canvas)) var(--app-canvas-dark-strength, 0%), var(--ui-canvas))
94
+ color-mix(in srgb, var(--app-canvas-from, var(--ui-canvas)) var(--cloud-canvas-dark-strength), var(--ui-canvas)),
95
+ color-mix(in srgb, var(--app-canvas-via-dark, var(--ui-canvas)) var(--cloud-canvas-dark-strength), var(--ui-canvas)) 50%,
96
+ color-mix(in srgb, var(--app-canvas-to, var(--ui-canvas)) var(--cloud-canvas-dark-strength), var(--ui-canvas))
94
97
  );
95
98
  }
96
99
 
100
+ /* The shell's own colored surfaces, the tablet rail's container and the
101
+ phone's bottom bar, carry the app color alone in a narrow shape. A white
102
+ middle, the canvas's default `via`, would make their edge vanish halfway,
103
+ so they mix the middle from the app's two colors and take a stronger tint:
104
+ 34 % light and 22 % dark, with a 16 % and 12 % middle. A page without an app
105
+ color gets a neutral tint. The app declaration stays the same. */
106
+ .cloud-app-canvas {
107
+ --cloud-shell-strength: 34%;
108
+ --cloud-shell-via-strength: 16%;
109
+ --cloud-shell-gradient: linear-gradient(
110
+ 160deg,
111
+ color-mix(in srgb, var(--app-canvas-from, var(--color-zinc-400)) var(--cloud-shell-strength), var(--ui-canvas)),
112
+ color-mix(
113
+ in srgb,
114
+ color-mix(in srgb, var(--app-canvas-from, var(--color-zinc-400)) 45%, var(--app-canvas-to, var(--color-zinc-400)))
115
+ var(--cloud-shell-via-strength),
116
+ var(--ui-canvas)
117
+ )
118
+ 55%,
119
+ color-mix(in srgb, var(--app-canvas-to, var(--color-zinc-400)) var(--cloud-shell-strength), var(--ui-canvas))
120
+ );
121
+ /* On color the open item is the surface; on the surface it is tinted. Its
122
+ label is the app ink, which app-color.ts keeps at AA on the surface and on
123
+ the app's selection fills. */
124
+ --cloud-shell-selected: var(--k2b-surface);
125
+ --cloud-shell-selected-text: var(--ui-app-accent-text);
126
+ }
127
+
128
+ .dark .cloud-app-canvas {
129
+ --cloud-shell-strength: 22%;
130
+ --cloud-shell-via-strength: 12%;
131
+ --cloud-shell-selected: var(--ui-app-selected-nav);
132
+ }
133
+
97
134
  :root {
98
135
  color-scheme: light;
99
136
  }
@@ -243,6 +280,21 @@ body {
243
280
  color: rgb(244 244 245); /* zinc-100 */
244
281
  }
245
282
 
283
+ /* The root paints the screen's page color as well. @k2b/ui makes it the
284
+ default `k2b-app-area` container, and containment stops a body background
285
+ from reaching the canvas that overscroll and short pages show. Zero
286
+ specificity, so the PWA canvas style and syncThemeColor still win; screen
287
+ only, so paper stays white. */
288
+ @media screen {
289
+ :where(html) {
290
+ background-color: rgb(250 250 250); /* zinc-50 */
291
+ }
292
+
293
+ :where(html.dark) {
294
+ background-color: rgb(9 9 11); /* zinc-950 */
295
+ }
296
+ }
297
+
246
298
  /* Pages of the mobile app paint the shell canvas, which their theme-color and
247
299
  html background repeat for the status bar. */
248
300
  html:has(.k2b-mobile-shell) body {
@@ -295,31 +347,37 @@ solid-island {
295
347
 
296
348
  /* Cloud has three shell modes. Phones, below md, get the header and no rail.
297
349
  From md, signed-in people get the 56 px rail instead of the header: up to
298
- 1023 px the app area meets the window's top, right and bottom edges, and
299
- from 1024 px it is a card beside the rail. Anonymous pages keep the header
300
- and the card from md. Each query is the exact opposite of the next one, so
301
- every width, also a fractional one under browser zoom, falls into exactly
302
- one mode. */
350
+ 1023 px the rail sits in a rounded container beside the app as a rounded
351
+ card, and from 1024 px the app is a card beside the rail. Anonymous pages
352
+ keep the header and the card from md. Each query is the exact opposite of
353
+ the next one, so every width, also a fractional one under browser zoom,
354
+ falls into exactly one mode. */
303
355
  @media not all and (min-width: 768px) {
304
356
  .cloud-app-canvas {
305
357
  padding: env(safe-area-inset-top, 0px) env(safe-area-inset-right, 0px) env(safe-area-inset-bottom, 0px) env(safe-area-inset-left, 0px);
306
358
  }
307
- /* The flat surface replaces the app gradient in both themes. The dark
308
- selector matches the specificity of the dark gradient above. */
309
- .cloud-app-canvas,
310
- .dark .cloud-app-canvas {
359
+ /* Anonymous pages: the flat surface replaces the app gradient in both
360
+ themes, and the document is the page scroller. The dark selector matches
361
+ the specificity of the dark gradient above. */
362
+ .cloud-app-canvas:not([data-layout-authenticated]),
363
+ .dark .cloud-app-canvas:not([data-layout-authenticated]) {
311
364
  background: var(--ui-surface);
312
365
  }
313
- /* Below lg the document is the page scroller; reserve its scrollbar gutter
314
- so short and long pages keep the same width. Full-page surfaces own their
315
- inner scrolling. */
316
- html:has(.cloud-app-canvas:not([data-layout-full-page])) { scrollbar-gutter: stable; }
366
+ /* Reserve the page scroller's gutter so short and long pages keep the same
367
+ width: the document's, or the card's for signed-in people. Full-page
368
+ surfaces, workspaces, and scroll areas own their inner scrolling, so the
369
+ card reserves no second gutter beside theirs. */
370
+ html:has(.cloud-app-canvas:not([data-layout-full-page], [data-layout-authenticated])) { scrollbar-gutter: stable; }
371
+ .cloud-app-canvas[data-layout-authenticated]:not([data-layout-full-page])
372
+ .layout-content-main:not(:has(> .k2b-scroll-area, > .k2b-app-workspace, > :is(solid-island, solid-client) > .k2b-app-workspace)) {
373
+ scrollbar-gutter: stable;
374
+ }
317
375
  /* A page that scrolls its content in a ScrollArea grows with it here, so
318
376
  the area does not scroll and reserves no second gutter beside the
319
377
  document's. From md, `main` bounds its height and it scrolls. */
320
- .cloud-app-canvas:not([data-layout-full-page]) .layout-content-main > .k2b-scroll-area { scrollbar-gutter: auto; }
378
+ .cloud-app-canvas:not([data-layout-full-page], [data-layout-authenticated]) .layout-content-main > .k2b-scroll-area { scrollbar-gutter: auto; }
321
379
  .cloud-app-canvas > .layout-shell-content { gap: 0; }
322
- .cloud-app-canvas .layout-header {
380
+ .cloud-app-canvas:not([data-layout-authenticated]) .layout-header {
323
381
  border-radius: 0; border: 0; border-bottom: 1px solid var(--ui-border);
324
382
  box-shadow: none !important;
325
383
  }
@@ -329,6 +387,145 @@ solid-island {
329
387
  --k2b-toast-offset-top: 2.875rem;
330
388
  }
331
389
 
390
+ /* Signed-in phones: the app gradient is the page. The title row sits on it
391
+ without a line, and the content is one soft card with 28 px top corners
392
+ that scrolls itself, so its rounded edge stays while it scrolls. The bar
393
+ with the person's apps floats over the card's end; the card keeps that
394
+ space free at its end and in its scroll padding, so the bar never covers
395
+ the last row or a focused control. The window never scrolls. */
396
+ .cloud-app-canvas[data-layout-authenticated] {
397
+ /* Text sits on the gradient here, so its tint stays at most 30 % light
398
+ and 15 % dark: then the title and controls read at 4.5:1 on any app
399
+ color. Built-in apps declare less. */
400
+ --cloud-canvas-strength: min(var(--app-canvas-strength, 0%), 30%);
401
+ --cloud-canvas-dark-strength: min(var(--app-canvas-dark-strength, 0%), 15%);
402
+ height: 100dvh;
403
+ overflow: hidden;
404
+ padding-bottom: 0;
405
+ }
406
+ .cloud-app-canvas[data-layout-authenticated] .layout-header {
407
+ border: 0;
408
+ border-radius: 0;
409
+ background: transparent;
410
+ box-shadow: none !important;
411
+ }
412
+ .cloud-app-canvas[data-layout-authenticated] .layout-header .font-semibold {
413
+ font-size: 1.125rem;
414
+ }
415
+ /* Content wider than the card scrolls sideways in it, as from md; the
416
+ rounded corners still clip. */
417
+ .cloud-app-canvas[data-layout-authenticated] .layout-content-main {
418
+ overflow: auto;
419
+ border-top: 1px solid transparent;
420
+ border-radius: 1.75rem 1.75rem 0 0;
421
+ background: var(--k2b-surface);
422
+ box-shadow: 0 -10px 30px -24px rgb(15 23 19 / 0.3);
423
+ }
424
+ /* Dark: a faint top edge keeps the card apart from the dark gradient. */
425
+ .dark .cloud-app-canvas[data-layout-authenticated] .layout-content-main {
426
+ border-top-color: var(--k2b-border);
427
+ }
428
+ }
429
+
430
+ /* The bar and the room it takes switch with `md`, as the header and the rail
431
+ do, so Home stays reachable also where a larger default font size moves
432
+ that switch above 768 px. */
433
+ .cloud-app-canvas[data-layout-authenticated] {
434
+ --cloud-tab-bar-height: 3.875rem;
435
+ --cloud-tab-bar-end: calc(env(safe-area-inset-bottom, 0px) + 0.5rem);
436
+ --cloud-tab-bar-space: calc(var(--cloud-tab-bar-end) + var(--cloud-tab-bar-height) + 0.75rem);
437
+ }
438
+
439
+ /* A page whose app has its own navigation (a mailbox, a Space, a book) makes
440
+ its title the menu "Name ⌄" on phones. It opens the same app menu as the
441
+ menu button beside it. The navigation's server-rendered snapshot decides,
442
+ so the first frame already shows the right title. */
443
+ .layout-header-title-menu {
444
+ display: none;
445
+ min-width: 0;
446
+ min-height: 2rem;
447
+ align-items: center;
448
+ gap: 0.25rem;
449
+ margin-inline-start: -0.5rem;
450
+ padding: 0 0.5rem;
451
+ border-radius: var(--k2b-radius-control);
452
+ color: inherit;
453
+ background: transparent;
454
+ cursor: pointer;
455
+ }
456
+ .layout-header-title-menu:hover {
457
+ background: color-mix(in srgb, currentColor 8%, transparent);
458
+ }
459
+ .layout-header-title-menu:focus-visible {
460
+ outline: var(--k2b-focus-width) solid var(--k2b-focus-ring);
461
+ outline-offset: 1px;
462
+ }
463
+ .layout-header-title-menu > i {
464
+ flex: none;
465
+ font-size: 1rem;
466
+ opacity: 0.7;
467
+ }
468
+
469
+ @media not all and (min-width: 48rem) {
470
+ :root:has(script[data-cloud-workspace-navigation]) .layout-header-title {
471
+ display: none;
472
+ }
473
+ :root:has(script[data-cloud-workspace-navigation]) .layout-header-title-menu {
474
+ display: inline-flex;
475
+ }
476
+ .cloud-app-canvas[data-layout-authenticated] .layout-content-main {
477
+ padding-bottom: var(--cloud-tab-bar-space);
478
+ scroll-padding-bottom: var(--cloud-tab-bar-space);
479
+ }
480
+ .cloud-app-canvas[data-layout-authenticated] > .cloud-tab-bar {
481
+ position: fixed;
482
+ right: 0;
483
+ bottom: 0;
484
+ left: 0;
485
+ z-index: 10;
486
+ display: flex;
487
+ justify-content: center;
488
+ align-items: flex-end;
489
+ height: var(--cloud-tab-bar-space);
490
+ padding: 0 calc(env(safe-area-inset-right, 0px) + 0.75rem) var(--cloud-tab-bar-end) calc(env(safe-area-inset-left, 0px) + 0.75rem);
491
+ /* Content that scrolls past fades flat into the card above the bar and is
492
+ gone where the bar begins: no glass, and no text beside or under it. */
493
+ background: linear-gradient(to bottom, color-mix(in srgb, var(--k2b-surface) 0%, transparent), var(--k2b-surface) 0.75rem);
494
+ pointer-events: none;
495
+ }
496
+ }
497
+
498
+ /* The bar exists only on signed-in phones. Its row keeps the place of the
499
+ app's primary action free at its end. */
500
+ .cloud-tab-bar {
501
+ display: none;
502
+ }
503
+
504
+ .cloud-tab-bar__row {
505
+ display: flex;
506
+ width: 100%;
507
+ max-width: 33.75rem;
508
+ padding-inline-end: calc(var(--cloud-tab-bar-height) + 0.625rem);
509
+ }
510
+
511
+ /* The shell's live bar replaces the server's once the bar's content changes
512
+ in the browser. */
513
+ .cloud-tab-bar__row:has(.cloud-tab-bar__live) > .k2b-tab-bar {
514
+ display: none;
515
+ }
516
+
517
+ .cloud-tab-bar__row > div:has(> .cloud-tab-bar__live) {
518
+ display: contents;
519
+ }
520
+
521
+ .cloud-tab-bar .k2b-tab-bar {
522
+ flex: 1;
523
+ min-width: 0;
524
+ pointer-events: auto;
525
+ --k2b-tab-bar-background: var(--cloud-shell-gradient);
526
+ --k2b-tab-bar-color: var(--k2b-text);
527
+ --k2b-tab-bar-selected-background: var(--cloud-shell-selected);
528
+ --k2b-tab-bar-selected-color: var(--cloud-shell-selected-text);
332
529
  }
333
530
 
334
531
  /* Card: the outermost workspace is the app card, so it gets the card's radius
@@ -337,9 +534,7 @@ solid-island {
337
534
  child fills `main` and scrolls inside itself, so `main` neither scrolls nor
338
535
  clips: it keeps no scrollbar gutter beside the card, the card keeps the
339
536
  shell spacing on every side, and its frame shadow shows in that spacing. A
340
- workspace nested in other page content leaves `main` the page scroller. For
341
- signed-in people, the flush range below removes the frame again; it comes
342
- later at equal specificity. */
537
+ workspace nested in other page content leaves `main` the page scroller. */
343
538
  @media (min-width: 768px) {
344
539
  .cloud-app-canvas .layout-content-main:has(> .k2b-app-workspace, > :is(solid-island, solid-client) > .k2b-app-workspace) {
345
540
  overflow: visible;
@@ -354,42 +549,60 @@ solid-island {
354
549
  }
355
550
  }
356
551
 
357
- /* Flush: the app area is one opaque surface from the rail to the window's
358
- edges, without padding, radius, shadow, or border. The rail keeps the app
359
- canvas, so the change of surface alone separates the two; no line does. The
360
- outermost workspace and the notices above it lose their frame, so nothing
361
- draws a box inside the app area.
362
- The rail and the app area reach the window's edges also where the installed
363
- app reports safe areas, such as the status bar and the home indicator. Their
364
- own surfaces fill the insets, and their content stays inside them. */
552
+ /* Tablet (I5): the rail sits in a rounded container filled with the app
553
+ color, and the app is a rounded card beside it, both 20 px round with 8 px
554
+ of neutral canvas on every side. The installed app's safe areas, such as
555
+ the status bar and the home indicator, add to that spacing, so nothing sits
556
+ under them. The outermost workspace is the card, as from 1024 px; a page
557
+ without a workspace shows its content on the canvas. On color the open app
558
+ is the surface color, and no separator divides the rail. */
365
559
  @media (min-width: 768px) {
366
560
  @media not all and (min-width: 1024px) {
367
561
  .cloud-app-canvas[data-layout-authenticated] {
368
- padding: 0;
562
+ padding: calc(env(safe-area-inset-top, 0px) + 0.5rem) calc(env(safe-area-inset-right, 0px) + 0.5rem)
563
+ calc(env(safe-area-inset-bottom, 0px) + 0.5rem) calc(env(safe-area-inset-left, 0px) + 0.5rem);
564
+ background: var(--ui-canvas);
369
565
  }
370
566
  .cloud-app-canvas[data-layout-authenticated] > .layout-rail {
371
- padding-top: env(safe-area-inset-top, 0px);
372
- padding-bottom: calc(var(--ui-space-shell) + env(safe-area-inset-bottom, 0px));
567
+ width: 3.75rem;
568
+ margin-inline-end: 0.5rem;
569
+ padding: 0 0 0.5rem;
570
+ border-radius: 1.25rem;
571
+ background-image: var(--cloud-shell-gradient) !important;
373
572
  }
374
- .cloud-app-canvas[data-layout-authenticated] > .layout-shell-content {
375
- gap: 0;
376
- padding: env(safe-area-inset-top, 0px) env(safe-area-inset-right, 0px) 0 0;
377
- background: var(--k2b-surface);
573
+ .dark .cloud-app-canvas[data-layout-authenticated] > .layout-rail {
574
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--app-accent, var(--k2b-border)) 22%, var(--k2b-border));
378
575
  }
379
- /* A regular page scrolls `main`, so its end clears the home indicator. A
380
- workspace reaches the bottom edge and clears it inside its own surfaces. */
381
- .cloud-app-canvas[data-layout-authenticated] .layout-content-main:not(:has(.k2b-app-workspace)) {
382
- padding-bottom: env(safe-area-inset-bottom, 0px);
576
+ .cloud-app-canvas[data-layout-authenticated] > .layout-rail .rail-item {
577
+ color: var(--k2b-text-secondary);
383
578
  }
384
- .cloud-app-canvas[data-layout-authenticated] .layout-content-main .k2b-app-workspace:not(.k2b-app-workspace *) {
385
- --k2b-workspace-radius: 0px;
386
- --k2b-workspace-inset-bottom: env(safe-area-inset-bottom, 0px);
387
- border: 0;
388
- box-shadow: none;
579
+ .cloud-app-canvas[data-layout-authenticated] > .layout-rail .rail-item:hover {
580
+ background: color-mix(in srgb, var(--k2b-surface) 45%, transparent);
581
+ }
582
+ .cloud-app-canvas[data-layout-authenticated] > .layout-rail .rail-item-active {
583
+ box-shadow: 0 1px 2px rgb(9 9 11 / 0.06);
584
+ }
585
+ .cloud-app-canvas[data-layout-authenticated] > .layout-rail .rail-item-active,
586
+ .cloud-app-canvas[data-layout-authenticated] > .layout-rail .rail-item-active:hover {
587
+ border-color: transparent;
588
+ background: var(--cloud-shell-selected);
589
+ color: var(--cloud-shell-selected-text);
590
+ }
591
+ .cloud-app-canvas[data-layout-authenticated] > .layout-rail [role="separator"] {
592
+ display: none;
389
593
  }
390
- .cloud-app-canvas[data-layout-authenticated] > .layout-shell-content > :not(.layout-content-main) .k2b-notice-card {
391
- border: 0;
392
- border-radius: 0;
594
+ /* The apps scroll in their own area; room above and below the first and
595
+ last app keeps a focus ring whole, also WebKit's wider one, without
596
+ moving an app. */
597
+ .cloud-app-canvas[data-layout-authenticated] > .layout-rail [data-cloud-rail-apps] {
598
+ margin-block: -0.375rem;
599
+ padding-block: 0.375rem;
600
+ }
601
+ .cloud-app-canvas[data-layout-authenticated] > .layout-shell-content {
602
+ gap: 0.5rem;
603
+ }
604
+ .cloud-app-canvas[data-layout-authenticated] .layout-content-main .k2b-app-workspace:not(.k2b-app-workspace *) {
605
+ --k2b-workspace-radius: 1.25rem;
393
606
  }
394
607
  }
395
608
  }
@@ -401,7 +614,7 @@ solid-island {
401
614
  rail starts at the window's left edge and moves down with the card. In the
402
615
  installed app the card and the rail stay below the status bar; the card keeps
403
616
  its spacing at the bottom, and its surfaces keep their content above the
404
- home indicator, as in the flush range. */
617
+ home indicator. */
405
618
  @media (min-width: 1024px) {
406
619
  .cloud-app-canvas[data-layout-authenticated] {
407
620
  --cloud-card-inset: round(clamp(0.25rem, calc(0.25rem + (100vw - 64rem) / 104), 0.5rem), 1px);
@@ -429,7 +642,7 @@ solid-island {
429
642
  (Cmd/Ctrl+\ or the sidebar buttons). The canvas carries the state from the
430
643
  `cloud_shell_focus` cookie, so the server renders the same first frame.
431
644
  Phones ignore it. The app area takes the app bar's place and starts at the
432
- card spacing, or at the window's edge where it is flush. The button that
645
+ card spacing. The button that
433
646
  shows them again floats at the app area's bottom start corner; on touch
434
647
  screens a drag from the 18 px strip at the left edge does the same. From
435
648
  768 to 1023 px the navigation is already an overlay behind the
@@ -470,8 +683,9 @@ solid-island {
470
683
 
471
684
  @media (min-width: 768px) {
472
685
  @media not all and (min-width: 1024px) {
473
- .cloud-app-canvas[data-layout-authenticated][data-shell-focus] > .layout-shell-content {
474
- padding-left: env(safe-area-inset-left, 0px);
686
+ .cloud-app-canvas[data-layout-authenticated][data-shell-focus] .cloud-shell-show {
687
+ left: calc(env(safe-area-inset-left, 0px) + 1.25rem);
688
+ bottom: calc(env(safe-area-inset-bottom, 0px) + 1.25rem);
475
689
  }
476
690
  }
477
691
  }