@xenide-io/the-old-ui-theme 0.5.0 → 0.5.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.
package/dist/suite.d.mts CHANGED
@@ -363,7 +363,7 @@ declare function SuiteBottomNav({ items, ariaLabel, dataTest, className, }: {
363
363
  * Slugs double as accent keys (see icons/app-accents.ts) and data-test
364
364
  * suffixes (`switch-app-${slug}`, `suite-nav-${slug}`).
365
365
  */
366
- type SuiteAppSlug = 'shellstack' | 'tides' | 'turtletime' | 'kraken';
366
+ type SuiteAppSlug = 'shellstack' | 'tides' | 'turtletime' | 'kraken' | 'nakama';
367
367
  interface SuiteAppDefinition {
368
368
  slug: SuiteAppSlug;
369
369
  name: string;
@@ -547,7 +547,7 @@ declare const SUITE_ICON_NAMES: SuiteIconName[];
547
547
  * `onTileAccent` drive `<SuiteAppIcon>` (the programmatic favicon-style
548
548
  * tile + glyph mark).
549
549
  */
550
- type SuiteAccentSlug = 'turtletime' | 'tides' | 'kraken' | 'shellstack' | 'crew';
550
+ type SuiteAccentSlug = 'turtletime' | 'tides' | 'kraken' | 'shellstack' | 'crew' | 'nakama';
551
551
  interface SuiteAppAccent {
552
552
  /** Display name (default aria label for app marks). */
553
553
  label: string;
package/dist/suite.d.ts CHANGED
@@ -363,7 +363,7 @@ declare function SuiteBottomNav({ items, ariaLabel, dataTest, className, }: {
363
363
  * Slugs double as accent keys (see icons/app-accents.ts) and data-test
364
364
  * suffixes (`switch-app-${slug}`, `suite-nav-${slug}`).
365
365
  */
366
- type SuiteAppSlug = 'shellstack' | 'tides' | 'turtletime' | 'kraken';
366
+ type SuiteAppSlug = 'shellstack' | 'tides' | 'turtletime' | 'kraken' | 'nakama';
367
367
  interface SuiteAppDefinition {
368
368
  slug: SuiteAppSlug;
369
369
  name: string;
@@ -547,7 +547,7 @@ declare const SUITE_ICON_NAMES: SuiteIconName[];
547
547
  * `onTileAccent` drive `<SuiteAppIcon>` (the programmatic favicon-style
548
548
  * tile + glyph mark).
549
549
  */
550
- type SuiteAccentSlug = 'turtletime' | 'tides' | 'kraken' | 'shellstack' | 'crew';
550
+ type SuiteAccentSlug = 'turtletime' | 'tides' | 'kraken' | 'shellstack' | 'crew' | 'nakama';
551
551
  interface SuiteAppAccent {
552
552
  /** Display name (default aria label for app marks). */
553
553
  label: string;
package/dist/suite.js CHANGED
@@ -383,6 +383,13 @@ var APP_ACCENTS = {
383
383
  tile: "#155e75",
384
384
  onTile: "#ffffff",
385
385
  onTileAccent: "#a5f3fc"
386
+ },
387
+ nakama: {
388
+ label: "Nakama",
389
+ accent: "#0891b2",
390
+ tile: "#155e75",
391
+ onTile: "#ffffff",
392
+ onTileAccent: "#a5f3fc"
386
393
  }
387
394
  };
388
395
  var APP_GLYPHS = {
@@ -390,7 +397,8 @@ var APP_GLYPHS = {
390
397
  tides: "kanban-wave",
391
398
  kraken: "squid-doc",
392
399
  shellstack: "stack-hex",
393
- crew: "crew-bot"
400
+ crew: "crew-bot",
401
+ nakama: "crew-bot"
394
402
  };
395
403
 
396
404
  // src/suite/icons/glyph-parts.tsx
@@ -1334,6 +1342,15 @@ var SUITE_APPS2 = [
1334
1342
  landing: "/today",
1335
1343
  baseUrlEnv: "NEXT_PUBLIC_KRAKEN_URL",
1336
1344
  baseUrlFallback: "http://localhost:3003"
1345
+ },
1346
+ {
1347
+ slug: "nakama",
1348
+ name: "Nakama",
1349
+ description: "AI teammates",
1350
+ icon: "/nakama-icon.svg",
1351
+ landing: "/today",
1352
+ baseUrlEnv: "NEXT_PUBLIC_NAKAMA_URL",
1353
+ baseUrlFallback: "http://localhost:3004"
1337
1354
  }
1338
1355
  ];
1339
1356
  var SUITE_APP_MAP = Object.fromEntries(SUITE_APPS2.map((app) => [app.slug, app]));
@@ -1346,6 +1363,7 @@ var SOURCE_APP_SLUG = {
1346
1363
  turtletime: "turtletime",
1347
1364
  tides: "tides",
1348
1365
  kraken: "kraken",
1366
+ nakama: "nakama",
1349
1367
  portal: "shellstack",
1350
1368
  shellstack: "shellstack"
1351
1369
  };
@@ -2389,9 +2407,7 @@ init_theme_provider();
2389
2407
  var import_react10 = require("react");
2390
2408
  var import_iconoir_react8 = require("iconoir-react");
2391
2409
  var import_jsx_runtime20 = require("react/jsx-runtime");
2392
- var SuiteAiBlockNoteMessage2 = (0, import_react10.lazy)(
2393
- () => Promise.resolve().then(() => (init_ai_message_blocknote(), ai_message_blocknote_exports))
2394
- );
2410
+ var SuiteAiBlockNoteMessage2 = (0, import_react10.lazy)(() => Promise.resolve().then(() => (init_ai_message_blocknote(), ai_message_blocknote_exports)));
2395
2411
  var SUITE_OPEN_ASK_AI_EVENT = "shellstack:open-ask-ai";
2396
2412
  function openSuiteAskAi(detail) {
2397
2413
  if (typeof window === "undefined") return;
@@ -2403,7 +2419,7 @@ function openSuiteAskAi(detail) {
2403
2419
  }
2404
2420
  var LAUNCHER_EDGE_GAP = 12;
2405
2421
  var LAUNCHER_STORAGE_KEY = "suite-ask-ai-launcher-position";
2406
- function resolveSuiteAiLauncherPosition(side, desiredY, viewportWidth, viewportHeight, launcherWidth, launcherHeight) {
2422
+ function resolveSuiteAiLauncherPosition(edge, desiredX, desiredY, viewportWidth, viewportHeight, launcherWidth, launcherHeight) {
2407
2423
  const maxX = Math.max(
2408
2424
  LAUNCHER_EDGE_GAP,
2409
2425
  viewportWidth - launcherWidth - LAUNCHER_EDGE_GAP
@@ -2412,10 +2428,36 @@ function resolveSuiteAiLauncherPosition(side, desiredY, viewportWidth, viewportH
2412
2428
  LAUNCHER_EDGE_GAP,
2413
2429
  viewportHeight - launcherHeight - LAUNCHER_EDGE_GAP
2414
2430
  );
2415
- return {
2416
- x: side === "left" ? LAUNCHER_EDGE_GAP : maxX,
2417
- y: Math.min(Math.max(desiredY, LAUNCHER_EDGE_GAP), maxY)
2418
- };
2431
+ const clampX = (x) => Math.min(Math.max(x, LAUNCHER_EDGE_GAP), maxX);
2432
+ const clampY = (y) => Math.min(Math.max(y, LAUNCHER_EDGE_GAP), maxY);
2433
+ switch (edge) {
2434
+ case "left":
2435
+ return { x: LAUNCHER_EDGE_GAP, y: clampY(desiredY) };
2436
+ case "right":
2437
+ return { x: maxX, y: clampY(desiredY) };
2438
+ case "top":
2439
+ return { x: clampX(desiredX), y: LAUNCHER_EDGE_GAP };
2440
+ case "bottom":
2441
+ return { x: clampX(desiredX), y: maxY };
2442
+ }
2443
+ }
2444
+ function resolveSuiteAiNearestEdge(x, y, width, height, viewportWidth, viewportHeight) {
2445
+ const centerX = x + width / 2;
2446
+ const centerY = y + height / 2;
2447
+ const candidates = [
2448
+ { edge: "left", distance: centerX },
2449
+ { edge: "right", distance: viewportWidth - centerX },
2450
+ { edge: "top", distance: centerY },
2451
+ { edge: "bottom", distance: viewportHeight - centerY }
2452
+ ];
2453
+ return candidates.reduce(
2454
+ (nearest, candidate) => candidate.distance < nearest.distance ? candidate : nearest
2455
+ ).edge;
2456
+ }
2457
+ function resolveSuiteAiPanelSide(edge, x, viewportWidth) {
2458
+ if (edge === "left") return "left";
2459
+ if (edge === "right") return "right";
2460
+ return x < viewportWidth / 2 ? "left" : "right";
2419
2461
  }
2420
2462
  function SuiteAiPanel({
2421
2463
  presets = [],
@@ -2436,6 +2478,7 @@ function SuiteAiPanel({
2436
2478
  const [error, setError] = (0, import_react10.useState)("");
2437
2479
  const [copiedId, setCopiedId] = (0, import_react10.useState)(null);
2438
2480
  const [launcherSide, setLauncherSide] = (0, import_react10.useState)("right");
2481
+ const [panelSide, setPanelSide] = (0, import_react10.useState)("right");
2439
2482
  const [launcherPosition, setLauncherPosition] = (0, import_react10.useState)(null);
2440
2483
  const [draggingLauncher, setDraggingLauncher] = (0, import_react10.useState)(false);
2441
2484
  const scrollerRef = (0, import_react10.useRef)(null);
@@ -2449,25 +2492,28 @@ function SuiteAiPanel({
2449
2492
  const launcher = launcherRef.current;
2450
2493
  if (!launcher) return;
2451
2494
  let side = "right";
2495
+ let desiredX = LAUNCHER_EDGE_GAP;
2452
2496
  let desiredY = window.innerHeight - launcher.offsetHeight - 20;
2453
2497
  try {
2454
2498
  const stored = JSON.parse(
2455
2499
  localStorage.getItem(LAUNCHER_STORAGE_KEY) || "null"
2456
2500
  );
2457
- if ((stored == null ? void 0 : stored.side) === "left" || (stored == null ? void 0 : stored.side) === "right")
2501
+ if ((stored == null ? void 0 : stored.side) === "top" || (stored == null ? void 0 : stored.side) === "right" || (stored == null ? void 0 : stored.side) === "bottom" || (stored == null ? void 0 : stored.side) === "left")
2458
2502
  side = stored.side;
2503
+ if (typeof (stored == null ? void 0 : stored.x) === "number") desiredX = stored.x;
2459
2504
  if (typeof (stored == null ? void 0 : stored.y) === "number") desiredY = stored.y;
2460
2505
  } catch (e) {
2461
2506
  }
2462
2507
  launcherSideRef.current = side;
2463
2508
  setLauncherSide(side);
2464
2509
  const placeLauncher = () => {
2465
- var _a, _b;
2510
+ var _a, _b, _c, _d;
2466
2511
  const element = launcherRef.current;
2467
2512
  if (!element) return;
2468
2513
  const next = resolveSuiteAiLauncherPosition(
2469
2514
  launcherSideRef.current,
2470
- (_b = (_a = launcherPositionRef.current) == null ? void 0 : _a.y) != null ? _b : desiredY,
2515
+ (_b = (_a = launcherPositionRef.current) == null ? void 0 : _a.x) != null ? _b : desiredX,
2516
+ (_d = (_c = launcherPositionRef.current) == null ? void 0 : _c.y) != null ? _d : desiredY,
2471
2517
  window.innerWidth,
2472
2518
  window.innerHeight,
2473
2519
  element.offsetWidth,
@@ -2475,6 +2521,13 @@ function SuiteAiPanel({
2475
2521
  );
2476
2522
  launcherPositionRef.current = next;
2477
2523
  setLauncherPosition(next);
2524
+ setPanelSide(
2525
+ resolveSuiteAiPanelSide(
2526
+ launcherSideRef.current,
2527
+ next.x,
2528
+ window.innerWidth
2529
+ )
2530
+ );
2478
2531
  };
2479
2532
  placeLauncher();
2480
2533
  window.addEventListener("resize", placeLauncher);
@@ -2575,31 +2628,33 @@ function SuiteAiPanel({
2575
2628
  } catch (e) {
2576
2629
  }
2577
2630
  }
2578
- function saveLauncherPosition(side, position) {
2631
+ function saveLauncherPosition(edge, position) {
2579
2632
  try {
2580
2633
  localStorage.setItem(
2581
2634
  LAUNCHER_STORAGE_KEY,
2582
- JSON.stringify({ side, y: position.y })
2635
+ JSON.stringify({ side: edge, x: position.x, y: position.y })
2583
2636
  );
2584
2637
  } catch (e) {
2585
2638
  }
2586
2639
  }
2587
- function dockLauncher(side, desiredY) {
2640
+ function dockLauncher(edge, desiredX, desiredY) {
2588
2641
  const launcher = launcherRef.current;
2589
2642
  if (!launcher) return;
2590
2643
  const next = resolveSuiteAiLauncherPosition(
2591
- side,
2644
+ edge,
2645
+ desiredX,
2592
2646
  desiredY,
2593
2647
  window.innerWidth,
2594
2648
  window.innerHeight,
2595
2649
  launcher.offsetWidth,
2596
2650
  launcher.offsetHeight
2597
2651
  );
2598
- launcherSideRef.current = side;
2652
+ launcherSideRef.current = edge;
2599
2653
  launcherPositionRef.current = next;
2600
- setLauncherSide(side);
2654
+ setLauncherSide(edge);
2601
2655
  setLauncherPosition(next);
2602
- saveLauncherPosition(side, next);
2656
+ setPanelSide(resolveSuiteAiPanelSide(edge, next.x, window.innerWidth));
2657
+ saveLauncherPosition(edge, next);
2603
2658
  }
2604
2659
  function startLauncherDrag(event) {
2605
2660
  if (event.pointerType === "mouse" && event.button !== 0) return;
@@ -2654,22 +2709,32 @@ function SuiteAiPanel({
2654
2709
  setDraggingLauncher(false);
2655
2710
  const rect = event.currentTarget.getBoundingClientRect();
2656
2711
  const currentX = (_b = (_a = launcherPositionRef.current) == null ? void 0 : _a.x) != null ? _b : rect.left;
2657
- const side = currentX + rect.width / 2 < window.innerWidth / 2 ? "left" : "right";
2658
- dockLauncher(side, (_d = (_c = launcherPositionRef.current) == null ? void 0 : _c.y) != null ? _d : rect.top);
2712
+ const currentY = (_d = (_c = launcherPositionRef.current) == null ? void 0 : _c.y) != null ? _d : rect.top;
2713
+ const edge = resolveSuiteAiNearestEdge(
2714
+ currentX,
2715
+ currentY,
2716
+ rect.width,
2717
+ rect.height,
2718
+ window.innerWidth,
2719
+ window.innerHeight
2720
+ );
2721
+ dockLauncher(edge, currentX, currentY);
2659
2722
  }
2660
2723
  function moveLauncherWithKeyboard(event) {
2661
- var _a, _b;
2662
- if (!["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(event.key))
2663
- return;
2724
+ var _a, _b, _c, _d;
2725
+ const edgeByKey = {
2726
+ ArrowLeft: "left",
2727
+ ArrowRight: "right",
2728
+ ArrowUp: "top",
2729
+ ArrowDown: "bottom"
2730
+ };
2731
+ const edge = edgeByKey[event.key];
2732
+ if (!edge) return;
2664
2733
  event.preventDefault();
2665
- const currentY = (_b = (_a = launcherPositionRef.current) == null ? void 0 : _a.y) != null ? _b : event.currentTarget.getBoundingClientRect().top;
2666
- if (event.key === "ArrowLeft") dockLauncher("left", currentY);
2667
- else if (event.key === "ArrowRight") dockLauncher("right", currentY);
2668
- else
2669
- dockLauncher(
2670
- launcherSideRef.current,
2671
- currentY + (event.key === "ArrowUp" ? -24 : 24)
2672
- );
2734
+ const rect = event.currentTarget.getBoundingClientRect();
2735
+ const currentX = (_b = (_a = launcherPositionRef.current) == null ? void 0 : _a.x) != null ? _b : rect.left;
2736
+ const currentY = (_d = (_c = launcherPositionRef.current) == null ? void 0 : _c.y) != null ? _d : rect.top;
2737
+ dockLauncher(edge, currentX, currentY);
2673
2738
  }
2674
2739
  const Icon = BrandIcon != null ? BrandIcon : import_iconoir_react8.Sparks;
2675
2740
  return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(import_jsx_runtime20.Fragment, { children: [
@@ -2717,7 +2782,7 @@ function SuiteAiPanel({
2717
2782
  /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
2718
2783
  "aside",
2719
2784
  {
2720
- className: `absolute inset-y-0 flex w-full max-w-md flex-col bg-ph-surface shadow-2xl ${launcherSide === "left" ? "left-0 border-r border-ph-border" : "right-0 border-l border-ph-border"}`,
2785
+ className: `absolute inset-y-0 flex w-full max-w-md flex-col bg-ph-surface shadow-2xl ${panelSide === "left" ? "left-0 border-r border-ph-border" : "right-0 border-l border-ph-border"}`,
2721
2786
  "data-test": "suite-ask-ai-panel",
2722
2787
  children: [
2723
2788
  /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "flex items-center justify-between border-b border-ph-border px-4 py-3", children: [
package/dist/suite.mjs CHANGED
@@ -96,6 +96,13 @@ var APP_ACCENTS = {
96
96
  tile: "#155e75",
97
97
  onTile: "#ffffff",
98
98
  onTileAccent: "#a5f3fc"
99
+ },
100
+ nakama: {
101
+ label: "Nakama",
102
+ accent: "#0891b2",
103
+ tile: "#155e75",
104
+ onTile: "#ffffff",
105
+ onTileAccent: "#a5f3fc"
99
106
  }
100
107
  };
101
108
  var APP_GLYPHS = {
@@ -103,7 +110,8 @@ var APP_GLYPHS = {
103
110
  tides: "kanban-wave",
104
111
  kraken: "squid-doc",
105
112
  shellstack: "stack-hex",
106
- crew: "crew-bot"
113
+ crew: "crew-bot",
114
+ nakama: "crew-bot"
107
115
  };
108
116
 
109
117
  // src/suite/icons/glyph-parts.tsx
@@ -1047,6 +1055,15 @@ var SUITE_APPS2 = [
1047
1055
  landing: "/today",
1048
1056
  baseUrlEnv: "NEXT_PUBLIC_KRAKEN_URL",
1049
1057
  baseUrlFallback: "http://localhost:3003"
1058
+ },
1059
+ {
1060
+ slug: "nakama",
1061
+ name: "Nakama",
1062
+ description: "AI teammates",
1063
+ icon: "/nakama-icon.svg",
1064
+ landing: "/today",
1065
+ baseUrlEnv: "NEXT_PUBLIC_NAKAMA_URL",
1066
+ baseUrlFallback: "http://localhost:3004"
1050
1067
  }
1051
1068
  ];
1052
1069
  var SUITE_APP_MAP = Object.fromEntries(SUITE_APPS2.map((app) => [app.slug, app]));
@@ -1059,6 +1076,7 @@ var SOURCE_APP_SLUG = {
1059
1076
  turtletime: "turtletime",
1060
1077
  tides: "tides",
1061
1078
  kraken: "kraken",
1079
+ nakama: "nakama",
1062
1080
  portal: "shellstack",
1063
1081
  shellstack: "shellstack"
1064
1082
  };
@@ -1994,9 +2012,7 @@ import {
1994
2012
  } from "react";
1995
2013
  import { Check as Check2, Copy, Sparks, Xmark as X2 } from "iconoir-react";
1996
2014
  import { Fragment as Fragment5, jsx as jsx17, jsxs as jsxs15 } from "react/jsx-runtime";
1997
- var SuiteAiBlockNoteMessage = lazy(
1998
- () => import("./ai-message-blocknote-4Q4U2T2W.mjs")
1999
- );
2015
+ var SuiteAiBlockNoteMessage = lazy(() => import("./ai-message-blocknote-4Q4U2T2W.mjs"));
2000
2016
  var SUITE_OPEN_ASK_AI_EVENT = "shellstack:open-ask-ai";
2001
2017
  function openSuiteAskAi(detail) {
2002
2018
  if (typeof window === "undefined") return;
@@ -2008,7 +2024,7 @@ function openSuiteAskAi(detail) {
2008
2024
  }
2009
2025
  var LAUNCHER_EDGE_GAP = 12;
2010
2026
  var LAUNCHER_STORAGE_KEY = "suite-ask-ai-launcher-position";
2011
- function resolveSuiteAiLauncherPosition(side, desiredY, viewportWidth, viewportHeight, launcherWidth, launcherHeight) {
2027
+ function resolveSuiteAiLauncherPosition(edge, desiredX, desiredY, viewportWidth, viewportHeight, launcherWidth, launcherHeight) {
2012
2028
  const maxX = Math.max(
2013
2029
  LAUNCHER_EDGE_GAP,
2014
2030
  viewportWidth - launcherWidth - LAUNCHER_EDGE_GAP
@@ -2017,10 +2033,36 @@ function resolveSuiteAiLauncherPosition(side, desiredY, viewportWidth, viewportH
2017
2033
  LAUNCHER_EDGE_GAP,
2018
2034
  viewportHeight - launcherHeight - LAUNCHER_EDGE_GAP
2019
2035
  );
2020
- return {
2021
- x: side === "left" ? LAUNCHER_EDGE_GAP : maxX,
2022
- y: Math.min(Math.max(desiredY, LAUNCHER_EDGE_GAP), maxY)
2023
- };
2036
+ const clampX = (x) => Math.min(Math.max(x, LAUNCHER_EDGE_GAP), maxX);
2037
+ const clampY = (y) => Math.min(Math.max(y, LAUNCHER_EDGE_GAP), maxY);
2038
+ switch (edge) {
2039
+ case "left":
2040
+ return { x: LAUNCHER_EDGE_GAP, y: clampY(desiredY) };
2041
+ case "right":
2042
+ return { x: maxX, y: clampY(desiredY) };
2043
+ case "top":
2044
+ return { x: clampX(desiredX), y: LAUNCHER_EDGE_GAP };
2045
+ case "bottom":
2046
+ return { x: clampX(desiredX), y: maxY };
2047
+ }
2048
+ }
2049
+ function resolveSuiteAiNearestEdge(x, y, width, height, viewportWidth, viewportHeight) {
2050
+ const centerX = x + width / 2;
2051
+ const centerY = y + height / 2;
2052
+ const candidates = [
2053
+ { edge: "left", distance: centerX },
2054
+ { edge: "right", distance: viewportWidth - centerX },
2055
+ { edge: "top", distance: centerY },
2056
+ { edge: "bottom", distance: viewportHeight - centerY }
2057
+ ];
2058
+ return candidates.reduce(
2059
+ (nearest, candidate) => candidate.distance < nearest.distance ? candidate : nearest
2060
+ ).edge;
2061
+ }
2062
+ function resolveSuiteAiPanelSide(edge, x, viewportWidth) {
2063
+ if (edge === "left") return "left";
2064
+ if (edge === "right") return "right";
2065
+ return x < viewportWidth / 2 ? "left" : "right";
2024
2066
  }
2025
2067
  function SuiteAiPanel({
2026
2068
  presets = [],
@@ -2041,6 +2083,7 @@ function SuiteAiPanel({
2041
2083
  const [error, setError] = useState7("");
2042
2084
  const [copiedId, setCopiedId] = useState7(null);
2043
2085
  const [launcherSide, setLauncherSide] = useState7("right");
2086
+ const [panelSide, setPanelSide] = useState7("right");
2044
2087
  const [launcherPosition, setLauncherPosition] = useState7(null);
2045
2088
  const [draggingLauncher, setDraggingLauncher] = useState7(false);
2046
2089
  const scrollerRef = useRef3(null);
@@ -2054,25 +2097,28 @@ function SuiteAiPanel({
2054
2097
  const launcher = launcherRef.current;
2055
2098
  if (!launcher) return;
2056
2099
  let side = "right";
2100
+ let desiredX = LAUNCHER_EDGE_GAP;
2057
2101
  let desiredY = window.innerHeight - launcher.offsetHeight - 20;
2058
2102
  try {
2059
2103
  const stored = JSON.parse(
2060
2104
  localStorage.getItem(LAUNCHER_STORAGE_KEY) || "null"
2061
2105
  );
2062
- if ((stored == null ? void 0 : stored.side) === "left" || (stored == null ? void 0 : stored.side) === "right")
2106
+ if ((stored == null ? void 0 : stored.side) === "top" || (stored == null ? void 0 : stored.side) === "right" || (stored == null ? void 0 : stored.side) === "bottom" || (stored == null ? void 0 : stored.side) === "left")
2063
2107
  side = stored.side;
2108
+ if (typeof (stored == null ? void 0 : stored.x) === "number") desiredX = stored.x;
2064
2109
  if (typeof (stored == null ? void 0 : stored.y) === "number") desiredY = stored.y;
2065
2110
  } catch (e) {
2066
2111
  }
2067
2112
  launcherSideRef.current = side;
2068
2113
  setLauncherSide(side);
2069
2114
  const placeLauncher = () => {
2070
- var _a, _b;
2115
+ var _a, _b, _c, _d;
2071
2116
  const element = launcherRef.current;
2072
2117
  if (!element) return;
2073
2118
  const next = resolveSuiteAiLauncherPosition(
2074
2119
  launcherSideRef.current,
2075
- (_b = (_a = launcherPositionRef.current) == null ? void 0 : _a.y) != null ? _b : desiredY,
2120
+ (_b = (_a = launcherPositionRef.current) == null ? void 0 : _a.x) != null ? _b : desiredX,
2121
+ (_d = (_c = launcherPositionRef.current) == null ? void 0 : _c.y) != null ? _d : desiredY,
2076
2122
  window.innerWidth,
2077
2123
  window.innerHeight,
2078
2124
  element.offsetWidth,
@@ -2080,6 +2126,13 @@ function SuiteAiPanel({
2080
2126
  );
2081
2127
  launcherPositionRef.current = next;
2082
2128
  setLauncherPosition(next);
2129
+ setPanelSide(
2130
+ resolveSuiteAiPanelSide(
2131
+ launcherSideRef.current,
2132
+ next.x,
2133
+ window.innerWidth
2134
+ )
2135
+ );
2083
2136
  };
2084
2137
  placeLauncher();
2085
2138
  window.addEventListener("resize", placeLauncher);
@@ -2180,31 +2233,33 @@ function SuiteAiPanel({
2180
2233
  } catch (e) {
2181
2234
  }
2182
2235
  }
2183
- function saveLauncherPosition(side, position) {
2236
+ function saveLauncherPosition(edge, position) {
2184
2237
  try {
2185
2238
  localStorage.setItem(
2186
2239
  LAUNCHER_STORAGE_KEY,
2187
- JSON.stringify({ side, y: position.y })
2240
+ JSON.stringify({ side: edge, x: position.x, y: position.y })
2188
2241
  );
2189
2242
  } catch (e) {
2190
2243
  }
2191
2244
  }
2192
- function dockLauncher(side, desiredY) {
2245
+ function dockLauncher(edge, desiredX, desiredY) {
2193
2246
  const launcher = launcherRef.current;
2194
2247
  if (!launcher) return;
2195
2248
  const next = resolveSuiteAiLauncherPosition(
2196
- side,
2249
+ edge,
2250
+ desiredX,
2197
2251
  desiredY,
2198
2252
  window.innerWidth,
2199
2253
  window.innerHeight,
2200
2254
  launcher.offsetWidth,
2201
2255
  launcher.offsetHeight
2202
2256
  );
2203
- launcherSideRef.current = side;
2257
+ launcherSideRef.current = edge;
2204
2258
  launcherPositionRef.current = next;
2205
- setLauncherSide(side);
2259
+ setLauncherSide(edge);
2206
2260
  setLauncherPosition(next);
2207
- saveLauncherPosition(side, next);
2261
+ setPanelSide(resolveSuiteAiPanelSide(edge, next.x, window.innerWidth));
2262
+ saveLauncherPosition(edge, next);
2208
2263
  }
2209
2264
  function startLauncherDrag(event) {
2210
2265
  if (event.pointerType === "mouse" && event.button !== 0) return;
@@ -2259,22 +2314,32 @@ function SuiteAiPanel({
2259
2314
  setDraggingLauncher(false);
2260
2315
  const rect = event.currentTarget.getBoundingClientRect();
2261
2316
  const currentX = (_b = (_a = launcherPositionRef.current) == null ? void 0 : _a.x) != null ? _b : rect.left;
2262
- const side = currentX + rect.width / 2 < window.innerWidth / 2 ? "left" : "right";
2263
- dockLauncher(side, (_d = (_c = launcherPositionRef.current) == null ? void 0 : _c.y) != null ? _d : rect.top);
2317
+ const currentY = (_d = (_c = launcherPositionRef.current) == null ? void 0 : _c.y) != null ? _d : rect.top;
2318
+ const edge = resolveSuiteAiNearestEdge(
2319
+ currentX,
2320
+ currentY,
2321
+ rect.width,
2322
+ rect.height,
2323
+ window.innerWidth,
2324
+ window.innerHeight
2325
+ );
2326
+ dockLauncher(edge, currentX, currentY);
2264
2327
  }
2265
2328
  function moveLauncherWithKeyboard(event) {
2266
- var _a, _b;
2267
- if (!["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(event.key))
2268
- return;
2329
+ var _a, _b, _c, _d;
2330
+ const edgeByKey = {
2331
+ ArrowLeft: "left",
2332
+ ArrowRight: "right",
2333
+ ArrowUp: "top",
2334
+ ArrowDown: "bottom"
2335
+ };
2336
+ const edge = edgeByKey[event.key];
2337
+ if (!edge) return;
2269
2338
  event.preventDefault();
2270
- const currentY = (_b = (_a = launcherPositionRef.current) == null ? void 0 : _a.y) != null ? _b : event.currentTarget.getBoundingClientRect().top;
2271
- if (event.key === "ArrowLeft") dockLauncher("left", currentY);
2272
- else if (event.key === "ArrowRight") dockLauncher("right", currentY);
2273
- else
2274
- dockLauncher(
2275
- launcherSideRef.current,
2276
- currentY + (event.key === "ArrowUp" ? -24 : 24)
2277
- );
2339
+ const rect = event.currentTarget.getBoundingClientRect();
2340
+ const currentX = (_b = (_a = launcherPositionRef.current) == null ? void 0 : _a.x) != null ? _b : rect.left;
2341
+ const currentY = (_d = (_c = launcherPositionRef.current) == null ? void 0 : _c.y) != null ? _d : rect.top;
2342
+ dockLauncher(edge, currentX, currentY);
2278
2343
  }
2279
2344
  const Icon = BrandIcon != null ? BrandIcon : Sparks;
2280
2345
  return /* @__PURE__ */ jsxs15(Fragment5, { children: [
@@ -2322,7 +2387,7 @@ function SuiteAiPanel({
2322
2387
  /* @__PURE__ */ jsxs15(
2323
2388
  "aside",
2324
2389
  {
2325
- className: `absolute inset-y-0 flex w-full max-w-md flex-col bg-ph-surface shadow-2xl ${launcherSide === "left" ? "left-0 border-r border-ph-border" : "right-0 border-l border-ph-border"}`,
2390
+ className: `absolute inset-y-0 flex w-full max-w-md flex-col bg-ph-surface shadow-2xl ${panelSide === "left" ? "left-0 border-r border-ph-border" : "right-0 border-l border-ph-border"}`,
2326
2391
  "data-test": "suite-ask-ai-panel",
2327
2392
  children: [
2328
2393
  /* @__PURE__ */ jsxs15("div", { className: "flex items-center justify-between border-b border-ph-border px-4 py-3", children: [
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xenide-io/the-old-ui-theme",
3
- "version": "0.5.0",
4
- "description": "Props-driven React components and theme tokens inspired by classic interface design optimized for Next.js apps and internal tools.",
3
+ "version": "0.5.2",
4
+ "description": "Props-driven React components and theme tokens inspired by classic interface design \u2014 optimized for Next.js apps and internal tools.",
5
5
  "author": "Xenide",
6
6
  "license": "MIT",
7
7
  "homepage": "https://github.com/xenide-io/the-old-ui-theme",
@@ -1764,6 +1764,154 @@
1764
1764
  --ph-select-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%23929292' stroke-width='2.25' stroke-linecap='round' stroke-linejoin='round' viewBox='0 0 24 24'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
1765
1765
  }
1766
1766
 
1767
+ /* Nakama Light — cyan-accented bright suite palette for the AI teammate app. */
1768
+ [data-theme="nakama-light"] {
1769
+ color-scheme: light;
1770
+
1771
+ --ph-canvas: #f6f8f9;
1772
+ --ph-surface: #ffffff;
1773
+ --ph-muted: #eef2f4;
1774
+ --ph-toolbar: #e6ebee;
1775
+ --ph-surface-inset: #eef2f4;
1776
+ --ph-surface-raised: #ffffff;
1777
+ --ph-surface-overlay: #ffffff;
1778
+ --ph-border: #dfe5e8;
1779
+ --ph-border-subtle: #eaeff1;
1780
+ --ph-border-strong: #a5b0b7;
1781
+
1782
+ --ph-text-primary: #17242c;
1783
+ --ph-text-secondary: #42525c;
1784
+ --ph-text-tertiary: #5f6f7a;
1785
+
1786
+ --ph-accent: #0891b2;
1787
+ --ph-accent-hover: #0e7490;
1788
+ --ph-blue: #437aff;
1789
+ --ph-violet: #8b5cf6;
1790
+ --ph-purple: #7c3aed;
1791
+
1792
+ --ph-success: #15803d;
1793
+ --ph-warning: #b26a00;
1794
+ --ph-danger: #c7463a;
1795
+ --ph-info: #1769aa;
1796
+ --ph-on-accent: #ffffff;
1797
+ --ph-on-violet: #ffffff;
1798
+ --ph-on-success: #ffffff;
1799
+ --ph-on-warning: #211606;
1800
+ --ph-on-danger: #ffffff;
1801
+ --ph-on-info: #ffffff;
1802
+ --ph-focus-ring: #0891b2;
1803
+ --ph-selected-bg: #e6f6fa;
1804
+ --ph-selected-text: #0b5569;
1805
+ --ph-selected-border: #67c6dd;
1806
+
1807
+ --ph-text-3000: #ffffff;
1808
+ --ph-primary-button-face: #0891b2;
1809
+ --ph-primary-button-border: #0b728c;
1810
+ --ph-primary-button-border-hover: #0e7490;
1811
+ --ph-primary-frame-bg: #0b5569;
1812
+ --ph-secondary-button-bg: #ffffff;
1813
+ --ph-secondary-frame-bg: #dce4e8;
1814
+ --ph-secondary-button-border: #bfcbd2;
1815
+ --ph-secondary-button-border-hover: #8797a1;
1816
+
1817
+ --ph-border-3000: #dce4e8;
1818
+ --ph-chrome-inset-face-top: inset 0 1px 0 hsl(0deg 0% 100% / 0.9);
1819
+ --ph-chrome-inset-face-bottom: inset 0 -1px 0 hsl(0deg 0% 0% / 0.06);
1820
+ --ph-accent-highlight-secondary: rgb(8 145 178 / 10%);
1821
+
1822
+ --ph-data-1: #0891b2;
1823
+ --ph-data-2: #f59e0b;
1824
+ --ph-data-3: #437aff;
1825
+ --ph-data-4: #059669;
1826
+ --ph-data-5: #b26a00;
1827
+ --ph-data-6: #c7463a;
1828
+ --ph-data-7: #7c3aed;
1829
+
1830
+ --ph-product-session-replay: #b26a00;
1831
+ --ph-product-product-analytics: #1769aa;
1832
+ --ph-product-data-pipeline: #059669;
1833
+ --ph-product-feature-flags: #f59e0b;
1834
+ --ph-product-experiments: #437aff;
1835
+ --ph-product-data-warehouse: #7b5e3b;
1836
+ --ph-product-surveys: #c7463a;
1837
+
1838
+ --ph-select-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%235f6f7a' stroke-width='2.25' stroke-linecap='round' stroke-linejoin='round' viewBox='0 0 24 24'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
1839
+ }
1840
+
1841
+ /* Nakama Dark — deep navy night palette with a bright cyan accent. */
1842
+ [data-theme="nakama-dark"] {
1843
+ color-scheme: dark;
1844
+
1845
+ --ph-canvas: #0f1420;
1846
+ --ph-surface: #161d2b;
1847
+ --ph-muted: #1c2434;
1848
+ --ph-toolbar: #232c3e;
1849
+ --ph-surface-inset: #121826;
1850
+ --ph-surface-raised: #1a2231;
1851
+ --ph-surface-overlay: #202a3c;
1852
+ --ph-border: #2b3648;
1853
+ --ph-border-subtle: #232d3f;
1854
+ --ph-border-strong: #4a5871;
1855
+
1856
+ --ph-text-primary: #e8eef4;
1857
+ --ph-text-secondary: #b9c4d1;
1858
+ --ph-text-tertiary: #8594a8;
1859
+
1860
+ --ph-accent: #22d3ee;
1861
+ --ph-accent-hover: #67e8f9;
1862
+ --ph-blue: #7da3ff;
1863
+ --ph-violet: #c4a7e7;
1864
+ --ph-purple: #b794f6;
1865
+
1866
+ --ph-success: #34d399;
1867
+ --ph-warning: #fbbf24;
1868
+ --ph-danger: #f87171;
1869
+ --ph-info: #60a5fa;
1870
+ --ph-on-accent: #062a33;
1871
+ --ph-on-violet: #1b1430;
1872
+ --ph-on-success: #052e1e;
1873
+ --ph-on-warning: #2b1e06;
1874
+ --ph-on-danger: #3b1010;
1875
+ --ph-on-info: #0a2a45;
1876
+ --ph-focus-ring: #22d3ee;
1877
+ --ph-selected-bg: #123a47;
1878
+ --ph-selected-text: #a5f3fc;
1879
+ --ph-selected-border: #2f6e80;
1880
+
1881
+ --ph-text-3000: #0f1420;
1882
+ --ph-primary-button-face: #22d3ee;
1883
+ --ph-primary-button-border: #3aa8bd;
1884
+ --ph-primary-button-border-hover: #67e8f9;
1885
+ --ph-primary-frame-bg: #0b6b80;
1886
+ --ph-secondary-button-bg: #202a3c;
1887
+ --ph-secondary-frame-bg: #2e3a50;
1888
+ --ph-secondary-button-border: #3d4b64;
1889
+ --ph-secondary-button-border-hover: #5a6b8a;
1890
+
1891
+ --ph-border-3000: #232d3f;
1892
+ --ph-chrome-inset-face-top: inset 0 1px 0 hsl(0deg 0% 100% / 0.08);
1893
+ --ph-chrome-inset-face-bottom: inset 0 -1px 0 hsl(0deg 0% 0% / 0.4);
1894
+ --ph-accent-highlight-secondary: rgb(34 211 238 / 12%);
1895
+
1896
+ --ph-data-1: #22d3ee;
1897
+ --ph-data-2: #fbbf24;
1898
+ --ph-data-3: #7da3ff;
1899
+ --ph-data-4: #34d399;
1900
+ --ph-data-5: #f59e0b;
1901
+ --ph-data-6: #f87171;
1902
+ --ph-data-7: #c4a7e7;
1903
+
1904
+ --ph-product-session-replay: #f59e0b;
1905
+ --ph-product-product-analytics: #60a5fa;
1906
+ --ph-product-data-pipeline: #34d399;
1907
+ --ph-product-feature-flags: #fbbf24;
1908
+ --ph-product-experiments: #7da3ff;
1909
+ --ph-product-data-warehouse: #d4a373;
1910
+ --ph-product-surveys: #f87171;
1911
+
1912
+ --ph-select-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%238594a8' stroke-width='2.25' stroke-linecap='round' stroke-linejoin='round' viewBox='0 0 24 24'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
1913
+ }
1914
+
1767
1915
  /* Semantic button contrast is scoped to buttons so other token consumers keep theme-native ink. */
1768
1916
  :is(
1769
1917
  [data-theme="hedgehog-dark"],
@@ -1780,7 +1928,8 @@
1780
1928
  [data-theme="rosepine-dark"],
1781
1929
  [data-theme="notion-dark"],
1782
1930
  [data-theme="deepsea-dark"],
1783
- [data-theme="kraken-dark"]
1931
+ [data-theme="kraken-dark"],
1932
+ [data-theme="nakama-dark"]
1784
1933
  ) {
1785
1934
  .ph-btn-signal {
1786
1935
  --ph-btn-face: color-mix(in oklab, var(--ph-accent) 62%, black);
@@ -1,20 +1,46 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
 
3
- import { resolveSuiteAiLauncherPosition } from './ai-panel';
3
+ import {
4
+ resolveSuiteAiLauncherPosition,
5
+ resolveSuiteAiNearestEdge,
6
+ resolveSuiteAiPanelSide,
7
+ } from './ai-panel';
4
8
 
5
9
  describe('resolveSuiteAiLauncherPosition', () => {
6
- it('docks to either edge and keeps the launcher inside the viewport', () => {
10
+ it('docks to every edge and keeps the launcher inside the viewport', () => {
7
11
  expect(
8
- resolveSuiteAiLauncherPosition('left', -20, 1000, 800, 120, 40),
9
- ).toEqual({
10
- x: 12,
11
- y: 12,
12
- });
12
+ resolveSuiteAiLauncherPosition('left', 900, -20, 1000, 800, 120, 40),
13
+ ).toEqual({ x: 12, y: 12 });
13
14
  expect(
14
- resolveSuiteAiLauncherPosition('right', 900, 1000, 800, 120, 40),
15
- ).toEqual({
16
- x: 868,
17
- y: 748,
18
- });
15
+ resolveSuiteAiLauncherPosition('right', 0, 900, 1000, 800, 120, 40),
16
+ ).toEqual({ x: 868, y: 748 });
17
+ expect(
18
+ resolveSuiteAiLauncherPosition('top', -20, 900, 1000, 800, 120, 40),
19
+ ).toEqual({ x: 12, y: 12 });
20
+ expect(
21
+ resolveSuiteAiLauncherPosition('bottom', 900, -20, 1000, 800, 120, 40),
22
+ ).toEqual({ x: 868, y: 748 });
23
+ });
24
+ });
25
+
26
+ describe('resolveSuiteAiNearestEdge', () => {
27
+ it('docks to the closest of all four edges', () => {
28
+ expect(resolveSuiteAiNearestEdge(10, 400, 120, 40, 1000, 800)).toBe('left');
29
+ expect(resolveSuiteAiNearestEdge(870, 400, 120, 40, 1000, 800)).toBe(
30
+ 'right',
31
+ );
32
+ expect(resolveSuiteAiNearestEdge(440, 10, 120, 40, 1000, 800)).toBe('top');
33
+ expect(resolveSuiteAiNearestEdge(440, 740, 120, 40, 1000, 800)).toBe(
34
+ 'bottom',
35
+ );
36
+ });
37
+ });
38
+
39
+ describe('resolveSuiteAiPanelSide', () => {
40
+ it('opens from the docked horizontal edge or the launcher half', () => {
41
+ expect(resolveSuiteAiPanelSide('left', 500, 1000)).toBe('left');
42
+ expect(resolveSuiteAiPanelSide('right', 500, 1000)).toBe('right');
43
+ expect(resolveSuiteAiPanelSide('top', 100, 1000)).toBe('left');
44
+ expect(resolveSuiteAiPanelSide('bottom', 900, 1000)).toBe('right');
19
45
  });
20
46
  });
@@ -14,9 +14,7 @@ import {
14
14
  } from 'react';
15
15
  import { Check, Copy, Sparks, Xmark as X } from 'iconoir-react';
16
16
 
17
- const SuiteAiBlockNoteMessage = lazy(
18
- () => import('./ai-message-blocknote'),
19
- );
17
+ const SuiteAiBlockNoteMessage = lazy(() => import('./ai-message-blocknote'));
20
18
 
21
19
  export const SUITE_OPEN_ASK_AI_EVENT = 'shellstack:open-ask-ai';
22
20
 
@@ -45,14 +43,16 @@ export interface SuiteAiChatMessage {
45
43
  created_at?: string;
46
44
  }
47
45
 
48
- type LauncherSide = 'left' | 'right';
46
+ export type SuiteAiLauncherEdge = 'top' | 'right' | 'bottom' | 'left';
47
+ type LauncherEdge = SuiteAiLauncherEdge;
49
48
  type LauncherPosition = { x: number; y: number };
50
49
 
51
50
  const LAUNCHER_EDGE_GAP = 12;
52
51
  const LAUNCHER_STORAGE_KEY = 'suite-ask-ai-launcher-position';
53
52
 
54
53
  export function resolveSuiteAiLauncherPosition(
55
- side: LauncherSide,
54
+ edge: LauncherEdge,
55
+ desiredX: number,
56
56
  desiredY: number,
57
57
  viewportWidth: number,
58
58
  viewportHeight: number,
@@ -67,10 +67,51 @@ export function resolveSuiteAiLauncherPosition(
67
67
  LAUNCHER_EDGE_GAP,
68
68
  viewportHeight - launcherHeight - LAUNCHER_EDGE_GAP,
69
69
  );
70
- return {
71
- x: side === 'left' ? LAUNCHER_EDGE_GAP : maxX,
72
- y: Math.min(Math.max(desiredY, LAUNCHER_EDGE_GAP), maxY),
73
- };
70
+ const clampX = (x: number) => Math.min(Math.max(x, LAUNCHER_EDGE_GAP), maxX);
71
+ const clampY = (y: number) => Math.min(Math.max(y, LAUNCHER_EDGE_GAP), maxY);
72
+ switch (edge) {
73
+ case 'left':
74
+ return { x: LAUNCHER_EDGE_GAP, y: clampY(desiredY) };
75
+ case 'right':
76
+ return { x: maxX, y: clampY(desiredY) };
77
+ case 'top':
78
+ return { x: clampX(desiredX), y: LAUNCHER_EDGE_GAP };
79
+ case 'bottom':
80
+ return { x: clampX(desiredX), y: maxY };
81
+ }
82
+ }
83
+
84
+ /** Which edge the launcher is closest to, used to dock after a drag. */
85
+ export function resolveSuiteAiNearestEdge(
86
+ x: number,
87
+ y: number,
88
+ width: number,
89
+ height: number,
90
+ viewportWidth: number,
91
+ viewportHeight: number,
92
+ ): LauncherEdge {
93
+ const centerX = x + width / 2;
94
+ const centerY = y + height / 2;
95
+ const candidates: Array<{ edge: LauncherEdge; distance: number }> = [
96
+ { edge: 'left', distance: centerX },
97
+ { edge: 'right', distance: viewportWidth - centerX },
98
+ { edge: 'top', distance: centerY },
99
+ { edge: 'bottom', distance: viewportHeight - centerY },
100
+ ];
101
+ return candidates.reduce((nearest, candidate) =>
102
+ candidate.distance < nearest.distance ? candidate : nearest,
103
+ ).edge;
104
+ }
105
+
106
+ /** Which horizontal side the slide-over should open from for a docked edge. */
107
+ export function resolveSuiteAiPanelSide(
108
+ edge: LauncherEdge,
109
+ x: number,
110
+ viewportWidth: number,
111
+ ): 'left' | 'right' {
112
+ if (edge === 'left') return 'left';
113
+ if (edge === 'right') return 'right';
114
+ return x < viewportWidth / 2 ? 'left' : 'right';
74
115
  }
75
116
 
76
117
  /**
@@ -113,13 +154,14 @@ export function SuiteAiPanel({
113
154
  const [hydrating, setHydrating] = useState(false);
114
155
  const [error, setError] = useState('');
115
156
  const [copiedId, setCopiedId] = useState<number | null>(null);
116
- const [launcherSide, setLauncherSide] = useState<LauncherSide>('right');
157
+ const [launcherSide, setLauncherSide] = useState<LauncherEdge>('right');
158
+ const [panelSide, setPanelSide] = useState<'left' | 'right'>('right');
117
159
  const [launcherPosition, setLauncherPosition] =
118
160
  useState<LauncherPosition | null>(null);
119
161
  const [draggingLauncher, setDraggingLauncher] = useState(false);
120
162
  const scrollerRef = useRef<HTMLDivElement>(null);
121
163
  const launcherRef = useRef<HTMLButtonElement>(null);
122
- const launcherSideRef = useRef<LauncherSide>('right');
164
+ const launcherSideRef = useRef<LauncherEdge>('right');
123
165
  const launcherPositionRef = useRef<LauncherPosition | null>(null);
124
166
  const launcherDragRef = useRef<{
125
167
  pointerId: number;
@@ -135,17 +177,25 @@ export function SuiteAiPanel({
135
177
  const launcher = launcherRef.current;
136
178
  if (!launcher) return;
137
179
 
138
- let side: LauncherSide = 'right';
180
+ let side: LauncherEdge = 'right';
181
+ let desiredX = LAUNCHER_EDGE_GAP;
139
182
  let desiredY = window.innerHeight - launcher.offsetHeight - 20;
140
183
  try {
141
184
  const stored = JSON.parse(
142
185
  localStorage.getItem(LAUNCHER_STORAGE_KEY) || 'null',
143
186
  ) as {
144
- side?: LauncherSide;
187
+ side?: LauncherEdge;
188
+ x?: number;
145
189
  y?: number;
146
190
  } | null;
147
- if (stored?.side === 'left' || stored?.side === 'right')
191
+ if (
192
+ stored?.side === 'top' ||
193
+ stored?.side === 'right' ||
194
+ stored?.side === 'bottom' ||
195
+ stored?.side === 'left'
196
+ )
148
197
  side = stored.side;
198
+ if (typeof stored?.x === 'number') desiredX = stored.x;
149
199
  if (typeof stored?.y === 'number') desiredY = stored.y;
150
200
  } catch {
151
201
  // Ignore invalid saved launcher positions.
@@ -159,6 +209,7 @@ export function SuiteAiPanel({
159
209
  if (!element) return;
160
210
  const next = resolveSuiteAiLauncherPosition(
161
211
  launcherSideRef.current,
212
+ launcherPositionRef.current?.x ?? desiredX,
162
213
  launcherPositionRef.current?.y ?? desiredY,
163
214
  window.innerWidth,
164
215
  window.innerHeight,
@@ -167,6 +218,13 @@ export function SuiteAiPanel({
167
218
  );
168
219
  launcherPositionRef.current = next;
169
220
  setLauncherPosition(next);
221
+ setPanelSide(
222
+ resolveSuiteAiPanelSide(
223
+ launcherSideRef.current,
224
+ next.x,
225
+ window.innerWidth,
226
+ ),
227
+ );
170
228
  };
171
229
 
172
230
  placeLauncher();
@@ -280,35 +338,41 @@ export function SuiteAiPanel({
280
338
  }
281
339
 
282
340
  function saveLauncherPosition(
283
- side: LauncherSide,
341
+ edge: LauncherEdge,
284
342
  position: LauncherPosition,
285
343
  ) {
286
344
  try {
287
345
  localStorage.setItem(
288
346
  LAUNCHER_STORAGE_KEY,
289
- JSON.stringify({ side, y: position.y }),
347
+ JSON.stringify({ side: edge, x: position.x, y: position.y }),
290
348
  );
291
349
  } catch {
292
350
  // Storage may be unavailable; dragging still works for this session.
293
351
  }
294
352
  }
295
353
 
296
- function dockLauncher(side: LauncherSide, desiredY: number) {
354
+ function dockLauncher(
355
+ edge: LauncherEdge,
356
+ desiredX: number,
357
+ desiredY: number,
358
+ ) {
297
359
  const launcher = launcherRef.current;
298
360
  if (!launcher) return;
299
361
  const next = resolveSuiteAiLauncherPosition(
300
- side,
362
+ edge,
363
+ desiredX,
301
364
  desiredY,
302
365
  window.innerWidth,
303
366
  window.innerHeight,
304
367
  launcher.offsetWidth,
305
368
  launcher.offsetHeight,
306
369
  );
307
- launcherSideRef.current = side;
370
+ launcherSideRef.current = edge;
308
371
  launcherPositionRef.current = next;
309
- setLauncherSide(side);
372
+ setLauncherSide(edge);
310
373
  setLauncherPosition(next);
311
- saveLauncherPosition(side, next);
374
+ setPanelSide(resolveSuiteAiPanelSide(edge, next.x, window.innerWidth));
375
+ saveLauncherPosition(edge, next);
312
376
  }
313
377
 
314
378
  function startLauncherDrag(event: ReactPointerEvent<HTMLButtonElement>) {
@@ -367,29 +431,34 @@ export function SuiteAiPanel({
367
431
  setDraggingLauncher(false);
368
432
  const rect = event.currentTarget.getBoundingClientRect();
369
433
  const currentX = launcherPositionRef.current?.x ?? rect.left;
370
- const side: LauncherSide =
371
- currentX + rect.width / 2 < window.innerWidth / 2 ? 'left' : 'right';
372
- dockLauncher(side, launcherPositionRef.current?.y ?? rect.top);
434
+ const currentY = launcherPositionRef.current?.y ?? rect.top;
435
+ const edge = resolveSuiteAiNearestEdge(
436
+ currentX,
437
+ currentY,
438
+ rect.width,
439
+ rect.height,
440
+ window.innerWidth,
441
+ window.innerHeight,
442
+ );
443
+ dockLauncher(edge, currentX, currentY);
373
444
  }
374
445
 
375
446
  function moveLauncherWithKeyboard(
376
447
  event: ReactKeyboardEvent<HTMLButtonElement>,
377
448
  ) {
378
- if (
379
- !['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key)
380
- )
381
- return;
449
+ const edgeByKey: Record<string, LauncherEdge> = {
450
+ ArrowLeft: 'left',
451
+ ArrowRight: 'right',
452
+ ArrowUp: 'top',
453
+ ArrowDown: 'bottom',
454
+ };
455
+ const edge = edgeByKey[event.key];
456
+ if (!edge) return;
382
457
  event.preventDefault();
383
- const currentY =
384
- launcherPositionRef.current?.y ??
385
- event.currentTarget.getBoundingClientRect().top;
386
- if (event.key === 'ArrowLeft') dockLauncher('left', currentY);
387
- else if (event.key === 'ArrowRight') dockLauncher('right', currentY);
388
- else
389
- dockLauncher(
390
- launcherSideRef.current,
391
- currentY + (event.key === 'ArrowUp' ? -24 : 24),
392
- );
458
+ const rect = event.currentTarget.getBoundingClientRect();
459
+ const currentX = launcherPositionRef.current?.x ?? rect.left;
460
+ const currentY = launcherPositionRef.current?.y ?? rect.top;
461
+ dockLauncher(edge, currentX, currentY);
393
462
  }
394
463
 
395
464
  const Icon = BrandIcon ?? Sparks;
@@ -442,7 +511,7 @@ export function SuiteAiPanel({
442
511
  aria-label="Close Ask AI"
443
512
  />
444
513
  <aside
445
- className={`absolute inset-y-0 flex w-full max-w-md flex-col bg-ph-surface shadow-2xl ${launcherSide === 'left' ? 'left-0 border-r border-ph-border' : 'right-0 border-l border-ph-border'}`}
514
+ className={`absolute inset-y-0 flex w-full max-w-md flex-col bg-ph-surface shadow-2xl ${panelSide === 'left' ? 'left-0 border-r border-ph-border' : 'right-0 border-l border-ph-border'}`}
446
515
  data-test="suite-ask-ai-panel"
447
516
  >
448
517
  <div className="flex items-center justify-between border-b border-ph-border px-4 py-3">
@@ -12,7 +12,8 @@ export type SuiteAccentSlug =
12
12
  | 'tides'
13
13
  | 'kraken'
14
14
  | 'shellstack'
15
- | 'crew';
15
+ | 'crew'
16
+ | 'nakama';
16
17
 
17
18
  export interface SuiteAppAccent {
18
19
  /** Display name (default aria label for app marks). */
@@ -63,6 +64,13 @@ export const APP_ACCENTS: Record<SuiteAccentSlug, SuiteAppAccent> = {
63
64
  onTile: '#ffffff',
64
65
  onTileAccent: '#a5f3fc',
65
66
  },
67
+ nakama: {
68
+ label: 'Nakama',
69
+ accent: '#0891b2',
70
+ tile: '#155e75',
71
+ onTile: '#ffffff',
72
+ onTileAccent: '#a5f3fc',
73
+ },
66
74
  };
67
75
 
68
76
  /** Which glyph represents each app. */
@@ -72,4 +80,5 @@ export const APP_GLYPHS: Record<SuiteAccentSlug, SuiteIconName> = {
72
80
  kraken: 'squid-doc',
73
81
  shellstack: 'stack-hex',
74
82
  crew: 'crew-bot',
83
+ nakama: 'crew-bot',
75
84
  };
@@ -9,7 +9,8 @@ export type SuiteAppSlug =
9
9
  | 'shellstack'
10
10
  | 'tides'
11
11
  | 'turtletime'
12
- | 'kraken';
12
+ | 'kraken'
13
+ | 'nakama';
13
14
 
14
15
  export interface SuiteAppDefinition {
15
16
  slug: SuiteAppSlug;
@@ -62,6 +63,15 @@ export const SUITE_APPS: readonly SuiteAppDefinition[] = [
62
63
  baseUrlEnv: 'NEXT_PUBLIC_KRAKEN_URL',
63
64
  baseUrlFallback: 'http://localhost:3003',
64
65
  },
66
+ {
67
+ slug: 'nakama',
68
+ name: 'Nakama',
69
+ description: 'AI teammates',
70
+ icon: '/nakama-icon.svg',
71
+ landing: '/today',
72
+ baseUrlEnv: 'NEXT_PUBLIC_NAKAMA_URL',
73
+ baseUrlFallback: 'http://localhost:3004',
74
+ },
65
75
  ] as const;
66
76
 
67
77
  export const SUITE_APP_MAP: Readonly<Record<SuiteAppSlug, SuiteAppDefinition>> =
@@ -82,6 +92,7 @@ const SOURCE_APP_SLUG: Record<string, SuiteAppSlug> = {
82
92
  turtletime: 'turtletime',
83
93
  tides: 'tides',
84
94
  kraken: 'kraken',
95
+ nakama: 'nakama',
85
96
  portal: 'shellstack',
86
97
  shellstack: 'shellstack',
87
98
  };