@xenide-io/the-old-ui-theme 0.4.6 → 0.4.8
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.js +187 -15
- package/dist/suite.mjs +187 -15
- package/package.json +1 -1
- package/src/suite/components/ai-panel.test.ts +20 -0
- package/src/suite/components/ai-panel.tsx +236 -11
package/dist/suite.js
CHANGED
|
@@ -2318,6 +2318,22 @@ function openSuiteAskAi(detail) {
|
|
|
2318
2318
|
})
|
|
2319
2319
|
);
|
|
2320
2320
|
}
|
|
2321
|
+
var LAUNCHER_EDGE_GAP = 12;
|
|
2322
|
+
var LAUNCHER_STORAGE_KEY = "suite-ask-ai-launcher-position";
|
|
2323
|
+
function resolveSuiteAiLauncherPosition(side, desiredY, viewportWidth, viewportHeight, launcherWidth, launcherHeight) {
|
|
2324
|
+
const maxX = Math.max(
|
|
2325
|
+
LAUNCHER_EDGE_GAP,
|
|
2326
|
+
viewportWidth - launcherWidth - LAUNCHER_EDGE_GAP
|
|
2327
|
+
);
|
|
2328
|
+
const maxY = Math.max(
|
|
2329
|
+
LAUNCHER_EDGE_GAP,
|
|
2330
|
+
viewportHeight - launcherHeight - LAUNCHER_EDGE_GAP
|
|
2331
|
+
);
|
|
2332
|
+
return {
|
|
2333
|
+
x: side === "left" ? LAUNCHER_EDGE_GAP : maxX,
|
|
2334
|
+
y: Math.min(Math.max(desiredY, LAUNCHER_EDGE_GAP), maxY)
|
|
2335
|
+
};
|
|
2336
|
+
}
|
|
2321
2337
|
function SuiteAiPanel({
|
|
2322
2338
|
presets = [],
|
|
2323
2339
|
emptyState = "Ask anything about your workspace, or look something up online.",
|
|
@@ -2336,8 +2352,51 @@ function SuiteAiPanel({
|
|
|
2336
2352
|
const [hydrating, setHydrating] = (0, import_react8.useState)(false);
|
|
2337
2353
|
const [error, setError] = (0, import_react8.useState)("");
|
|
2338
2354
|
const [copiedId, setCopiedId] = (0, import_react8.useState)(null);
|
|
2355
|
+
const [launcherSide, setLauncherSide] = (0, import_react8.useState)("right");
|
|
2356
|
+
const [launcherPosition, setLauncherPosition] = (0, import_react8.useState)(null);
|
|
2357
|
+
const [draggingLauncher, setDraggingLauncher] = (0, import_react8.useState)(false);
|
|
2339
2358
|
const scrollerRef = (0, import_react8.useRef)(null);
|
|
2359
|
+
const launcherRef = (0, import_react8.useRef)(null);
|
|
2360
|
+
const launcherSideRef = (0, import_react8.useRef)("right");
|
|
2361
|
+
const launcherPositionRef = (0, import_react8.useRef)(null);
|
|
2362
|
+
const launcherDragRef = (0, import_react8.useRef)(null);
|
|
2363
|
+
const suppressLauncherClickRef = (0, import_react8.useRef)(false);
|
|
2340
2364
|
const pendingPromptRef = (0, import_react8.useRef)(null);
|
|
2365
|
+
(0, import_react8.useEffect)(() => {
|
|
2366
|
+
const launcher = launcherRef.current;
|
|
2367
|
+
if (!launcher) return;
|
|
2368
|
+
let side = "right";
|
|
2369
|
+
let desiredY = window.innerHeight - launcher.offsetHeight - 20;
|
|
2370
|
+
try {
|
|
2371
|
+
const stored = JSON.parse(
|
|
2372
|
+
localStorage.getItem(LAUNCHER_STORAGE_KEY) || "null"
|
|
2373
|
+
);
|
|
2374
|
+
if ((stored == null ? void 0 : stored.side) === "left" || (stored == null ? void 0 : stored.side) === "right")
|
|
2375
|
+
side = stored.side;
|
|
2376
|
+
if (typeof (stored == null ? void 0 : stored.y) === "number") desiredY = stored.y;
|
|
2377
|
+
} catch (e) {
|
|
2378
|
+
}
|
|
2379
|
+
launcherSideRef.current = side;
|
|
2380
|
+
setLauncherSide(side);
|
|
2381
|
+
const placeLauncher = () => {
|
|
2382
|
+
var _a, _b;
|
|
2383
|
+
const element = launcherRef.current;
|
|
2384
|
+
if (!element) return;
|
|
2385
|
+
const next = resolveSuiteAiLauncherPosition(
|
|
2386
|
+
launcherSideRef.current,
|
|
2387
|
+
(_b = (_a = launcherPositionRef.current) == null ? void 0 : _a.y) != null ? _b : desiredY,
|
|
2388
|
+
window.innerWidth,
|
|
2389
|
+
window.innerHeight,
|
|
2390
|
+
element.offsetWidth,
|
|
2391
|
+
element.offsetHeight
|
|
2392
|
+
);
|
|
2393
|
+
launcherPositionRef.current = next;
|
|
2394
|
+
setLauncherPosition(next);
|
|
2395
|
+
};
|
|
2396
|
+
placeLauncher();
|
|
2397
|
+
window.addEventListener("resize", placeLauncher);
|
|
2398
|
+
return () => window.removeEventListener("resize", placeLauncher);
|
|
2399
|
+
}, []);
|
|
2341
2400
|
const scrollToBottom = (0, import_react8.useCallback)(() => {
|
|
2342
2401
|
const el = scrollerRef.current;
|
|
2343
2402
|
if (!el) return;
|
|
@@ -2433,22 +2492,135 @@ function SuiteAiPanel({
|
|
|
2433
2492
|
} catch (e) {
|
|
2434
2493
|
}
|
|
2435
2494
|
}
|
|
2495
|
+
function saveLauncherPosition(side, position) {
|
|
2496
|
+
try {
|
|
2497
|
+
localStorage.setItem(
|
|
2498
|
+
LAUNCHER_STORAGE_KEY,
|
|
2499
|
+
JSON.stringify({ side, y: position.y })
|
|
2500
|
+
);
|
|
2501
|
+
} catch (e) {
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
function dockLauncher(side, desiredY) {
|
|
2505
|
+
const launcher = launcherRef.current;
|
|
2506
|
+
if (!launcher) return;
|
|
2507
|
+
const next = resolveSuiteAiLauncherPosition(
|
|
2508
|
+
side,
|
|
2509
|
+
desiredY,
|
|
2510
|
+
window.innerWidth,
|
|
2511
|
+
window.innerHeight,
|
|
2512
|
+
launcher.offsetWidth,
|
|
2513
|
+
launcher.offsetHeight
|
|
2514
|
+
);
|
|
2515
|
+
launcherSideRef.current = side;
|
|
2516
|
+
launcherPositionRef.current = next;
|
|
2517
|
+
setLauncherSide(side);
|
|
2518
|
+
setLauncherPosition(next);
|
|
2519
|
+
saveLauncherPosition(side, next);
|
|
2520
|
+
}
|
|
2521
|
+
function startLauncherDrag(event) {
|
|
2522
|
+
if (event.pointerType === "mouse" && event.button !== 0) return;
|
|
2523
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
2524
|
+
launcherDragRef.current = {
|
|
2525
|
+
pointerId: event.pointerId,
|
|
2526
|
+
offsetX: event.clientX - rect.left,
|
|
2527
|
+
offsetY: event.clientY - rect.top,
|
|
2528
|
+
startX: event.clientX,
|
|
2529
|
+
startY: event.clientY
|
|
2530
|
+
};
|
|
2531
|
+
suppressLauncherClickRef.current = false;
|
|
2532
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
2533
|
+
setDraggingLauncher(true);
|
|
2534
|
+
}
|
|
2535
|
+
function moveLauncher(event) {
|
|
2536
|
+
const drag = launcherDragRef.current;
|
|
2537
|
+
if (!drag || drag.pointerId !== event.pointerId) return;
|
|
2538
|
+
const launcher = event.currentTarget;
|
|
2539
|
+
const maxX = Math.max(
|
|
2540
|
+
LAUNCHER_EDGE_GAP,
|
|
2541
|
+
window.innerWidth - launcher.offsetWidth - LAUNCHER_EDGE_GAP
|
|
2542
|
+
);
|
|
2543
|
+
const maxY = Math.max(
|
|
2544
|
+
LAUNCHER_EDGE_GAP,
|
|
2545
|
+
window.innerHeight - launcher.offsetHeight - LAUNCHER_EDGE_GAP
|
|
2546
|
+
);
|
|
2547
|
+
const next = {
|
|
2548
|
+
x: Math.min(
|
|
2549
|
+
Math.max(event.clientX - drag.offsetX, LAUNCHER_EDGE_GAP),
|
|
2550
|
+
maxX
|
|
2551
|
+
),
|
|
2552
|
+
y: Math.min(
|
|
2553
|
+
Math.max(event.clientY - drag.offsetY, LAUNCHER_EDGE_GAP),
|
|
2554
|
+
maxY
|
|
2555
|
+
)
|
|
2556
|
+
};
|
|
2557
|
+
if (Math.hypot(event.clientX - drag.startX, event.clientY - drag.startY) > 4) {
|
|
2558
|
+
suppressLauncherClickRef.current = true;
|
|
2559
|
+
}
|
|
2560
|
+
launcherPositionRef.current = next;
|
|
2561
|
+
setLauncherPosition(next);
|
|
2562
|
+
}
|
|
2563
|
+
function finishLauncherDrag(event) {
|
|
2564
|
+
var _a, _b, _c, _d;
|
|
2565
|
+
const drag = launcherDragRef.current;
|
|
2566
|
+
if (!drag || drag.pointerId !== event.pointerId) return;
|
|
2567
|
+
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
|
2568
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
2569
|
+
}
|
|
2570
|
+
launcherDragRef.current = null;
|
|
2571
|
+
setDraggingLauncher(false);
|
|
2572
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
2573
|
+
const currentX = (_b = (_a = launcherPositionRef.current) == null ? void 0 : _a.x) != null ? _b : rect.left;
|
|
2574
|
+
const side = currentX + rect.width / 2 < window.innerWidth / 2 ? "left" : "right";
|
|
2575
|
+
dockLauncher(side, (_d = (_c = launcherPositionRef.current) == null ? void 0 : _c.y) != null ? _d : rect.top);
|
|
2576
|
+
}
|
|
2577
|
+
function moveLauncherWithKeyboard(event) {
|
|
2578
|
+
var _a, _b;
|
|
2579
|
+
if (!["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(event.key))
|
|
2580
|
+
return;
|
|
2581
|
+
event.preventDefault();
|
|
2582
|
+
const currentY = (_b = (_a = launcherPositionRef.current) == null ? void 0 : _a.y) != null ? _b : event.currentTarget.getBoundingClientRect().top;
|
|
2583
|
+
if (event.key === "ArrowLeft") dockLauncher("left", currentY);
|
|
2584
|
+
else if (event.key === "ArrowRight") dockLauncher("right", currentY);
|
|
2585
|
+
else
|
|
2586
|
+
dockLauncher(
|
|
2587
|
+
launcherSideRef.current,
|
|
2588
|
+
currentY + (event.key === "ArrowUp" ? -24 : 24)
|
|
2589
|
+
);
|
|
2590
|
+
}
|
|
2436
2591
|
const Icon = BrandIcon != null ? BrandIcon : import_iconoir_react8.Sparks;
|
|
2437
2592
|
return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(import_jsx_runtime19.Fragment, { children: [
|
|
2438
|
-
!hideLauncher ? /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2593
|
+
!hideLauncher ? /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(import_jsx_runtime19.Fragment, { children: [
|
|
2594
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
|
|
2595
|
+
"button",
|
|
2596
|
+
{
|
|
2597
|
+
ref: launcherRef,
|
|
2598
|
+
type: "button",
|
|
2599
|
+
"data-test": "ai-panel-launcher",
|
|
2600
|
+
onClick: () => {
|
|
2601
|
+
if (suppressLauncherClickRef.current) {
|
|
2602
|
+
suppressLauncherClickRef.current = false;
|
|
2603
|
+
return;
|
|
2604
|
+
}
|
|
2605
|
+
setOpen(true);
|
|
2606
|
+
},
|
|
2607
|
+
onPointerDown: startLauncherDrag,
|
|
2608
|
+
onPointerMove: moveLauncher,
|
|
2609
|
+
onPointerUp: finishLauncherDrag,
|
|
2610
|
+
onPointerCancel: finishLauncherDrag,
|
|
2611
|
+
onKeyDown: moveLauncherWithKeyboard,
|
|
2612
|
+
className: `fixed z-30 flex touch-none select-none items-center gap-2 rounded-full border border-ph-border bg-ph-surface px-4 py-2.5 text-sm font-medium text-ph-ink shadow-ph hover:bg-ph-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ph-brand ${draggingLauncher ? "cursor-grabbing" : "cursor-grab"}`,
|
|
2613
|
+
style: launcherPosition ? { left: launcherPosition.x, top: launcherPosition.y } : { bottom: 20, right: 20 },
|
|
2614
|
+
"aria-label": "Open Ask AI",
|
|
2615
|
+
"aria-describedby": "suite-ask-ai-launcher-help",
|
|
2616
|
+
children: [
|
|
2617
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)(Icon, { className: "h-4 w-4 text-ph-brand" }),
|
|
2618
|
+
"Ask AI"
|
|
2619
|
+
]
|
|
2620
|
+
}
|
|
2621
|
+
),
|
|
2622
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { id: "suite-ask-ai-launcher-help", className: "sr-only", children: "Drag to reposition, or use arrow keys. The launcher docks to the nearest screen edge." })
|
|
2623
|
+
] }) : null,
|
|
2452
2624
|
open ? /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "fixed inset-0 z-40", role: "dialog", "aria-modal": "true", children: [
|
|
2453
2625
|
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
2454
2626
|
"button",
|
|
@@ -2462,7 +2634,7 @@ function SuiteAiPanel({
|
|
|
2462
2634
|
/* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
|
|
2463
2635
|
"aside",
|
|
2464
2636
|
{
|
|
2465
|
-
className:
|
|
2637
|
+
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"}`,
|
|
2466
2638
|
"data-test": "suite-ask-ai-panel",
|
|
2467
2639
|
children: [
|
|
2468
2640
|
/* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "flex items-center justify-between border-b border-ph-border px-4 py-3", children: [
|
package/dist/suite.mjs
CHANGED
|
@@ -2092,6 +2092,22 @@ function openSuiteAskAi(detail) {
|
|
|
2092
2092
|
})
|
|
2093
2093
|
);
|
|
2094
2094
|
}
|
|
2095
|
+
var LAUNCHER_EDGE_GAP = 12;
|
|
2096
|
+
var LAUNCHER_STORAGE_KEY = "suite-ask-ai-launcher-position";
|
|
2097
|
+
function resolveSuiteAiLauncherPosition(side, desiredY, viewportWidth, viewportHeight, launcherWidth, launcherHeight) {
|
|
2098
|
+
const maxX = Math.max(
|
|
2099
|
+
LAUNCHER_EDGE_GAP,
|
|
2100
|
+
viewportWidth - launcherWidth - LAUNCHER_EDGE_GAP
|
|
2101
|
+
);
|
|
2102
|
+
const maxY = Math.max(
|
|
2103
|
+
LAUNCHER_EDGE_GAP,
|
|
2104
|
+
viewportHeight - launcherHeight - LAUNCHER_EDGE_GAP
|
|
2105
|
+
);
|
|
2106
|
+
return {
|
|
2107
|
+
x: side === "left" ? LAUNCHER_EDGE_GAP : maxX,
|
|
2108
|
+
y: Math.min(Math.max(desiredY, LAUNCHER_EDGE_GAP), maxY)
|
|
2109
|
+
};
|
|
2110
|
+
}
|
|
2095
2111
|
function SuiteAiPanel({
|
|
2096
2112
|
presets = [],
|
|
2097
2113
|
emptyState = "Ask anything about your workspace, or look something up online.",
|
|
@@ -2110,8 +2126,51 @@ function SuiteAiPanel({
|
|
|
2110
2126
|
const [hydrating, setHydrating] = useState8(false);
|
|
2111
2127
|
const [error, setError] = useState8("");
|
|
2112
2128
|
const [copiedId, setCopiedId] = useState8(null);
|
|
2129
|
+
const [launcherSide, setLauncherSide] = useState8("right");
|
|
2130
|
+
const [launcherPosition, setLauncherPosition] = useState8(null);
|
|
2131
|
+
const [draggingLauncher, setDraggingLauncher] = useState8(false);
|
|
2113
2132
|
const scrollerRef = useRef3(null);
|
|
2133
|
+
const launcherRef = useRef3(null);
|
|
2134
|
+
const launcherSideRef = useRef3("right");
|
|
2135
|
+
const launcherPositionRef = useRef3(null);
|
|
2136
|
+
const launcherDragRef = useRef3(null);
|
|
2137
|
+
const suppressLauncherClickRef = useRef3(false);
|
|
2114
2138
|
const pendingPromptRef = useRef3(null);
|
|
2139
|
+
useEffect7(() => {
|
|
2140
|
+
const launcher = launcherRef.current;
|
|
2141
|
+
if (!launcher) return;
|
|
2142
|
+
let side = "right";
|
|
2143
|
+
let desiredY = window.innerHeight - launcher.offsetHeight - 20;
|
|
2144
|
+
try {
|
|
2145
|
+
const stored = JSON.parse(
|
|
2146
|
+
localStorage.getItem(LAUNCHER_STORAGE_KEY) || "null"
|
|
2147
|
+
);
|
|
2148
|
+
if ((stored == null ? void 0 : stored.side) === "left" || (stored == null ? void 0 : stored.side) === "right")
|
|
2149
|
+
side = stored.side;
|
|
2150
|
+
if (typeof (stored == null ? void 0 : stored.y) === "number") desiredY = stored.y;
|
|
2151
|
+
} catch (e) {
|
|
2152
|
+
}
|
|
2153
|
+
launcherSideRef.current = side;
|
|
2154
|
+
setLauncherSide(side);
|
|
2155
|
+
const placeLauncher = () => {
|
|
2156
|
+
var _a, _b;
|
|
2157
|
+
const element = launcherRef.current;
|
|
2158
|
+
if (!element) return;
|
|
2159
|
+
const next = resolveSuiteAiLauncherPosition(
|
|
2160
|
+
launcherSideRef.current,
|
|
2161
|
+
(_b = (_a = launcherPositionRef.current) == null ? void 0 : _a.y) != null ? _b : desiredY,
|
|
2162
|
+
window.innerWidth,
|
|
2163
|
+
window.innerHeight,
|
|
2164
|
+
element.offsetWidth,
|
|
2165
|
+
element.offsetHeight
|
|
2166
|
+
);
|
|
2167
|
+
launcherPositionRef.current = next;
|
|
2168
|
+
setLauncherPosition(next);
|
|
2169
|
+
};
|
|
2170
|
+
placeLauncher();
|
|
2171
|
+
window.addEventListener("resize", placeLauncher);
|
|
2172
|
+
return () => window.removeEventListener("resize", placeLauncher);
|
|
2173
|
+
}, []);
|
|
2115
2174
|
const scrollToBottom = useCallback3(() => {
|
|
2116
2175
|
const el = scrollerRef.current;
|
|
2117
2176
|
if (!el) return;
|
|
@@ -2207,22 +2266,135 @@ function SuiteAiPanel({
|
|
|
2207
2266
|
} catch (e) {
|
|
2208
2267
|
}
|
|
2209
2268
|
}
|
|
2269
|
+
function saveLauncherPosition(side, position) {
|
|
2270
|
+
try {
|
|
2271
|
+
localStorage.setItem(
|
|
2272
|
+
LAUNCHER_STORAGE_KEY,
|
|
2273
|
+
JSON.stringify({ side, y: position.y })
|
|
2274
|
+
);
|
|
2275
|
+
} catch (e) {
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
function dockLauncher(side, desiredY) {
|
|
2279
|
+
const launcher = launcherRef.current;
|
|
2280
|
+
if (!launcher) return;
|
|
2281
|
+
const next = resolveSuiteAiLauncherPosition(
|
|
2282
|
+
side,
|
|
2283
|
+
desiredY,
|
|
2284
|
+
window.innerWidth,
|
|
2285
|
+
window.innerHeight,
|
|
2286
|
+
launcher.offsetWidth,
|
|
2287
|
+
launcher.offsetHeight
|
|
2288
|
+
);
|
|
2289
|
+
launcherSideRef.current = side;
|
|
2290
|
+
launcherPositionRef.current = next;
|
|
2291
|
+
setLauncherSide(side);
|
|
2292
|
+
setLauncherPosition(next);
|
|
2293
|
+
saveLauncherPosition(side, next);
|
|
2294
|
+
}
|
|
2295
|
+
function startLauncherDrag(event) {
|
|
2296
|
+
if (event.pointerType === "mouse" && event.button !== 0) return;
|
|
2297
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
2298
|
+
launcherDragRef.current = {
|
|
2299
|
+
pointerId: event.pointerId,
|
|
2300
|
+
offsetX: event.clientX - rect.left,
|
|
2301
|
+
offsetY: event.clientY - rect.top,
|
|
2302
|
+
startX: event.clientX,
|
|
2303
|
+
startY: event.clientY
|
|
2304
|
+
};
|
|
2305
|
+
suppressLauncherClickRef.current = false;
|
|
2306
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
2307
|
+
setDraggingLauncher(true);
|
|
2308
|
+
}
|
|
2309
|
+
function moveLauncher(event) {
|
|
2310
|
+
const drag = launcherDragRef.current;
|
|
2311
|
+
if (!drag || drag.pointerId !== event.pointerId) return;
|
|
2312
|
+
const launcher = event.currentTarget;
|
|
2313
|
+
const maxX = Math.max(
|
|
2314
|
+
LAUNCHER_EDGE_GAP,
|
|
2315
|
+
window.innerWidth - launcher.offsetWidth - LAUNCHER_EDGE_GAP
|
|
2316
|
+
);
|
|
2317
|
+
const maxY = Math.max(
|
|
2318
|
+
LAUNCHER_EDGE_GAP,
|
|
2319
|
+
window.innerHeight - launcher.offsetHeight - LAUNCHER_EDGE_GAP
|
|
2320
|
+
);
|
|
2321
|
+
const next = {
|
|
2322
|
+
x: Math.min(
|
|
2323
|
+
Math.max(event.clientX - drag.offsetX, LAUNCHER_EDGE_GAP),
|
|
2324
|
+
maxX
|
|
2325
|
+
),
|
|
2326
|
+
y: Math.min(
|
|
2327
|
+
Math.max(event.clientY - drag.offsetY, LAUNCHER_EDGE_GAP),
|
|
2328
|
+
maxY
|
|
2329
|
+
)
|
|
2330
|
+
};
|
|
2331
|
+
if (Math.hypot(event.clientX - drag.startX, event.clientY - drag.startY) > 4) {
|
|
2332
|
+
suppressLauncherClickRef.current = true;
|
|
2333
|
+
}
|
|
2334
|
+
launcherPositionRef.current = next;
|
|
2335
|
+
setLauncherPosition(next);
|
|
2336
|
+
}
|
|
2337
|
+
function finishLauncherDrag(event) {
|
|
2338
|
+
var _a, _b, _c, _d;
|
|
2339
|
+
const drag = launcherDragRef.current;
|
|
2340
|
+
if (!drag || drag.pointerId !== event.pointerId) return;
|
|
2341
|
+
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
|
2342
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
2343
|
+
}
|
|
2344
|
+
launcherDragRef.current = null;
|
|
2345
|
+
setDraggingLauncher(false);
|
|
2346
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
2347
|
+
const currentX = (_b = (_a = launcherPositionRef.current) == null ? void 0 : _a.x) != null ? _b : rect.left;
|
|
2348
|
+
const side = currentX + rect.width / 2 < window.innerWidth / 2 ? "left" : "right";
|
|
2349
|
+
dockLauncher(side, (_d = (_c = launcherPositionRef.current) == null ? void 0 : _c.y) != null ? _d : rect.top);
|
|
2350
|
+
}
|
|
2351
|
+
function moveLauncherWithKeyboard(event) {
|
|
2352
|
+
var _a, _b;
|
|
2353
|
+
if (!["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(event.key))
|
|
2354
|
+
return;
|
|
2355
|
+
event.preventDefault();
|
|
2356
|
+
const currentY = (_b = (_a = launcherPositionRef.current) == null ? void 0 : _a.y) != null ? _b : event.currentTarget.getBoundingClientRect().top;
|
|
2357
|
+
if (event.key === "ArrowLeft") dockLauncher("left", currentY);
|
|
2358
|
+
else if (event.key === "ArrowRight") dockLauncher("right", currentY);
|
|
2359
|
+
else
|
|
2360
|
+
dockLauncher(
|
|
2361
|
+
launcherSideRef.current,
|
|
2362
|
+
currentY + (event.key === "ArrowUp" ? -24 : 24)
|
|
2363
|
+
);
|
|
2364
|
+
}
|
|
2210
2365
|
const Icon = BrandIcon != null ? BrandIcon : Sparks;
|
|
2211
2366
|
return /* @__PURE__ */ jsxs15(Fragment5, { children: [
|
|
2212
|
-
!hideLauncher ? /* @__PURE__ */ jsxs15(
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2367
|
+
!hideLauncher ? /* @__PURE__ */ jsxs15(Fragment5, { children: [
|
|
2368
|
+
/* @__PURE__ */ jsxs15(
|
|
2369
|
+
"button",
|
|
2370
|
+
{
|
|
2371
|
+
ref: launcherRef,
|
|
2372
|
+
type: "button",
|
|
2373
|
+
"data-test": "ai-panel-launcher",
|
|
2374
|
+
onClick: () => {
|
|
2375
|
+
if (suppressLauncherClickRef.current) {
|
|
2376
|
+
suppressLauncherClickRef.current = false;
|
|
2377
|
+
return;
|
|
2378
|
+
}
|
|
2379
|
+
setOpen(true);
|
|
2380
|
+
},
|
|
2381
|
+
onPointerDown: startLauncherDrag,
|
|
2382
|
+
onPointerMove: moveLauncher,
|
|
2383
|
+
onPointerUp: finishLauncherDrag,
|
|
2384
|
+
onPointerCancel: finishLauncherDrag,
|
|
2385
|
+
onKeyDown: moveLauncherWithKeyboard,
|
|
2386
|
+
className: `fixed z-30 flex touch-none select-none items-center gap-2 rounded-full border border-ph-border bg-ph-surface px-4 py-2.5 text-sm font-medium text-ph-ink shadow-ph hover:bg-ph-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ph-brand ${draggingLauncher ? "cursor-grabbing" : "cursor-grab"}`,
|
|
2387
|
+
style: launcherPosition ? { left: launcherPosition.x, top: launcherPosition.y } : { bottom: 20, right: 20 },
|
|
2388
|
+
"aria-label": "Open Ask AI",
|
|
2389
|
+
"aria-describedby": "suite-ask-ai-launcher-help",
|
|
2390
|
+
children: [
|
|
2391
|
+
/* @__PURE__ */ jsx18(Icon, { className: "h-4 w-4 text-ph-brand" }),
|
|
2392
|
+
"Ask AI"
|
|
2393
|
+
]
|
|
2394
|
+
}
|
|
2395
|
+
),
|
|
2396
|
+
/* @__PURE__ */ jsx18("span", { id: "suite-ask-ai-launcher-help", className: "sr-only", children: "Drag to reposition, or use arrow keys. The launcher docks to the nearest screen edge." })
|
|
2397
|
+
] }) : null,
|
|
2226
2398
|
open ? /* @__PURE__ */ jsxs15("div", { className: "fixed inset-0 z-40", role: "dialog", "aria-modal": "true", children: [
|
|
2227
2399
|
/* @__PURE__ */ jsx18(
|
|
2228
2400
|
"button",
|
|
@@ -2236,7 +2408,7 @@ function SuiteAiPanel({
|
|
|
2236
2408
|
/* @__PURE__ */ jsxs15(
|
|
2237
2409
|
"aside",
|
|
2238
2410
|
{
|
|
2239
|
-
className:
|
|
2411
|
+
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"}`,
|
|
2240
2412
|
"data-test": "suite-ask-ai-panel",
|
|
2241
2413
|
children: [
|
|
2242
2414
|
/* @__PURE__ */ jsxs15("div", { className: "flex items-center justify-between border-b border-ph-border px-4 py-3", children: [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xenide-io/the-old-ui-theme",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.8",
|
|
4
4
|
"description": "Props-driven React components and theme tokens inspired by classic interface design — optimized for Next.js apps and internal tools.",
|
|
5
5
|
"author": "Xenide",
|
|
6
6
|
"license": "MIT",
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { resolveSuiteAiLauncherPosition } from './ai-panel';
|
|
4
|
+
|
|
5
|
+
describe('resolveSuiteAiLauncherPosition', () => {
|
|
6
|
+
it('docks to either edge and keeps the launcher inside the viewport', () => {
|
|
7
|
+
expect(
|
|
8
|
+
resolveSuiteAiLauncherPosition('left', -20, 1000, 800, 120, 40),
|
|
9
|
+
).toEqual({
|
|
10
|
+
x: 12,
|
|
11
|
+
y: 12,
|
|
12
|
+
});
|
|
13
|
+
expect(
|
|
14
|
+
resolveSuiteAiLauncherPosition('right', 900, 1000, 800, 120, 40),
|
|
15
|
+
).toEqual({
|
|
16
|
+
x: 868,
|
|
17
|
+
y: 748,
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
useRef,
|
|
7
7
|
useState,
|
|
8
8
|
type ComponentType,
|
|
9
|
+
type KeyboardEvent as ReactKeyboardEvent,
|
|
10
|
+
type PointerEvent as ReactPointerEvent,
|
|
9
11
|
type ReactNode,
|
|
10
12
|
} from 'react';
|
|
11
13
|
import { Check, Copy, Sparks, Xmark as X } from 'iconoir-react';
|
|
@@ -37,6 +39,34 @@ export interface SuiteAiChatMessage {
|
|
|
37
39
|
created_at?: string;
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
type LauncherSide = 'left' | 'right';
|
|
43
|
+
type LauncherPosition = { x: number; y: number };
|
|
44
|
+
|
|
45
|
+
const LAUNCHER_EDGE_GAP = 12;
|
|
46
|
+
const LAUNCHER_STORAGE_KEY = 'suite-ask-ai-launcher-position';
|
|
47
|
+
|
|
48
|
+
export function resolveSuiteAiLauncherPosition(
|
|
49
|
+
side: LauncherSide,
|
|
50
|
+
desiredY: number,
|
|
51
|
+
viewportWidth: number,
|
|
52
|
+
viewportHeight: number,
|
|
53
|
+
launcherWidth: number,
|
|
54
|
+
launcherHeight: number,
|
|
55
|
+
): LauncherPosition {
|
|
56
|
+
const maxX = Math.max(
|
|
57
|
+
LAUNCHER_EDGE_GAP,
|
|
58
|
+
viewportWidth - launcherWidth - LAUNCHER_EDGE_GAP,
|
|
59
|
+
);
|
|
60
|
+
const maxY = Math.max(
|
|
61
|
+
LAUNCHER_EDGE_GAP,
|
|
62
|
+
viewportHeight - launcherHeight - LAUNCHER_EDGE_GAP,
|
|
63
|
+
);
|
|
64
|
+
return {
|
|
65
|
+
x: side === 'left' ? LAUNCHER_EDGE_GAP : maxX,
|
|
66
|
+
y: Math.min(Math.max(desiredY, LAUNCHER_EDGE_GAP), maxY),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
40
70
|
/**
|
|
41
71
|
* Suite-wide Ask AI slide-over — Notion-style continuous chat.
|
|
42
72
|
*
|
|
@@ -77,9 +107,67 @@ export function SuiteAiPanel({
|
|
|
77
107
|
const [hydrating, setHydrating] = useState(false);
|
|
78
108
|
const [error, setError] = useState('');
|
|
79
109
|
const [copiedId, setCopiedId] = useState<number | null>(null);
|
|
110
|
+
const [launcherSide, setLauncherSide] = useState<LauncherSide>('right');
|
|
111
|
+
const [launcherPosition, setLauncherPosition] =
|
|
112
|
+
useState<LauncherPosition | null>(null);
|
|
113
|
+
const [draggingLauncher, setDraggingLauncher] = useState(false);
|
|
80
114
|
const scrollerRef = useRef<HTMLDivElement>(null);
|
|
115
|
+
const launcherRef = useRef<HTMLButtonElement>(null);
|
|
116
|
+
const launcherSideRef = useRef<LauncherSide>('right');
|
|
117
|
+
const launcherPositionRef = useRef<LauncherPosition | null>(null);
|
|
118
|
+
const launcherDragRef = useRef<{
|
|
119
|
+
pointerId: number;
|
|
120
|
+
offsetX: number;
|
|
121
|
+
offsetY: number;
|
|
122
|
+
startX: number;
|
|
123
|
+
startY: number;
|
|
124
|
+
} | null>(null);
|
|
125
|
+
const suppressLauncherClickRef = useRef(false);
|
|
81
126
|
const pendingPromptRef = useRef<string | null>(null);
|
|
82
127
|
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
const launcher = launcherRef.current;
|
|
130
|
+
if (!launcher) return;
|
|
131
|
+
|
|
132
|
+
let side: LauncherSide = 'right';
|
|
133
|
+
let desiredY = window.innerHeight - launcher.offsetHeight - 20;
|
|
134
|
+
try {
|
|
135
|
+
const stored = JSON.parse(
|
|
136
|
+
localStorage.getItem(LAUNCHER_STORAGE_KEY) || 'null',
|
|
137
|
+
) as {
|
|
138
|
+
side?: LauncherSide;
|
|
139
|
+
y?: number;
|
|
140
|
+
} | null;
|
|
141
|
+
if (stored?.side === 'left' || stored?.side === 'right')
|
|
142
|
+
side = stored.side;
|
|
143
|
+
if (typeof stored?.y === 'number') desiredY = stored.y;
|
|
144
|
+
} catch {
|
|
145
|
+
// Ignore invalid saved launcher positions.
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
launcherSideRef.current = side;
|
|
149
|
+
setLauncherSide(side);
|
|
150
|
+
|
|
151
|
+
const placeLauncher = () => {
|
|
152
|
+
const element = launcherRef.current;
|
|
153
|
+
if (!element) return;
|
|
154
|
+
const next = resolveSuiteAiLauncherPosition(
|
|
155
|
+
launcherSideRef.current,
|
|
156
|
+
launcherPositionRef.current?.y ?? desiredY,
|
|
157
|
+
window.innerWidth,
|
|
158
|
+
window.innerHeight,
|
|
159
|
+
element.offsetWidth,
|
|
160
|
+
element.offsetHeight,
|
|
161
|
+
);
|
|
162
|
+
launcherPositionRef.current = next;
|
|
163
|
+
setLauncherPosition(next);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
placeLauncher();
|
|
167
|
+
window.addEventListener('resize', placeLauncher);
|
|
168
|
+
return () => window.removeEventListener('resize', placeLauncher);
|
|
169
|
+
}, []);
|
|
170
|
+
|
|
83
171
|
const scrollToBottom = useCallback(() => {
|
|
84
172
|
const el = scrollerRef.current;
|
|
85
173
|
if (!el) return;
|
|
@@ -185,21 +273,158 @@ export function SuiteAiPanel({
|
|
|
185
273
|
}
|
|
186
274
|
}
|
|
187
275
|
|
|
276
|
+
function saveLauncherPosition(
|
|
277
|
+
side: LauncherSide,
|
|
278
|
+
position: LauncherPosition,
|
|
279
|
+
) {
|
|
280
|
+
try {
|
|
281
|
+
localStorage.setItem(
|
|
282
|
+
LAUNCHER_STORAGE_KEY,
|
|
283
|
+
JSON.stringify({ side, y: position.y }),
|
|
284
|
+
);
|
|
285
|
+
} catch {
|
|
286
|
+
// Storage may be unavailable; dragging still works for this session.
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function dockLauncher(side: LauncherSide, desiredY: number) {
|
|
291
|
+
const launcher = launcherRef.current;
|
|
292
|
+
if (!launcher) return;
|
|
293
|
+
const next = resolveSuiteAiLauncherPosition(
|
|
294
|
+
side,
|
|
295
|
+
desiredY,
|
|
296
|
+
window.innerWidth,
|
|
297
|
+
window.innerHeight,
|
|
298
|
+
launcher.offsetWidth,
|
|
299
|
+
launcher.offsetHeight,
|
|
300
|
+
);
|
|
301
|
+
launcherSideRef.current = side;
|
|
302
|
+
launcherPositionRef.current = next;
|
|
303
|
+
setLauncherSide(side);
|
|
304
|
+
setLauncherPosition(next);
|
|
305
|
+
saveLauncherPosition(side, next);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function startLauncherDrag(event: ReactPointerEvent<HTMLButtonElement>) {
|
|
309
|
+
if (event.pointerType === 'mouse' && event.button !== 0) return;
|
|
310
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
311
|
+
launcherDragRef.current = {
|
|
312
|
+
pointerId: event.pointerId,
|
|
313
|
+
offsetX: event.clientX - rect.left,
|
|
314
|
+
offsetY: event.clientY - rect.top,
|
|
315
|
+
startX: event.clientX,
|
|
316
|
+
startY: event.clientY,
|
|
317
|
+
};
|
|
318
|
+
suppressLauncherClickRef.current = false;
|
|
319
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
320
|
+
setDraggingLauncher(true);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function moveLauncher(event: ReactPointerEvent<HTMLButtonElement>) {
|
|
324
|
+
const drag = launcherDragRef.current;
|
|
325
|
+
if (!drag || drag.pointerId !== event.pointerId) return;
|
|
326
|
+
const launcher = event.currentTarget;
|
|
327
|
+
const maxX = Math.max(
|
|
328
|
+
LAUNCHER_EDGE_GAP,
|
|
329
|
+
window.innerWidth - launcher.offsetWidth - LAUNCHER_EDGE_GAP,
|
|
330
|
+
);
|
|
331
|
+
const maxY = Math.max(
|
|
332
|
+
LAUNCHER_EDGE_GAP,
|
|
333
|
+
window.innerHeight - launcher.offsetHeight - LAUNCHER_EDGE_GAP,
|
|
334
|
+
);
|
|
335
|
+
const next = {
|
|
336
|
+
x: Math.min(
|
|
337
|
+
Math.max(event.clientX - drag.offsetX, LAUNCHER_EDGE_GAP),
|
|
338
|
+
maxX,
|
|
339
|
+
),
|
|
340
|
+
y: Math.min(
|
|
341
|
+
Math.max(event.clientY - drag.offsetY, LAUNCHER_EDGE_GAP),
|
|
342
|
+
maxY,
|
|
343
|
+
),
|
|
344
|
+
};
|
|
345
|
+
if (
|
|
346
|
+
Math.hypot(event.clientX - drag.startX, event.clientY - drag.startY) > 4
|
|
347
|
+
) {
|
|
348
|
+
suppressLauncherClickRef.current = true;
|
|
349
|
+
}
|
|
350
|
+
launcherPositionRef.current = next;
|
|
351
|
+
setLauncherPosition(next);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function finishLauncherDrag(event: ReactPointerEvent<HTMLButtonElement>) {
|
|
355
|
+
const drag = launcherDragRef.current;
|
|
356
|
+
if (!drag || drag.pointerId !== event.pointerId) return;
|
|
357
|
+
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
|
358
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
359
|
+
}
|
|
360
|
+
launcherDragRef.current = null;
|
|
361
|
+
setDraggingLauncher(false);
|
|
362
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
363
|
+
const currentX = launcherPositionRef.current?.x ?? rect.left;
|
|
364
|
+
const side: LauncherSide =
|
|
365
|
+
currentX + rect.width / 2 < window.innerWidth / 2 ? 'left' : 'right';
|
|
366
|
+
dockLauncher(side, launcherPositionRef.current?.y ?? rect.top);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function moveLauncherWithKeyboard(
|
|
370
|
+
event: ReactKeyboardEvent<HTMLButtonElement>,
|
|
371
|
+
) {
|
|
372
|
+
if (
|
|
373
|
+
!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key)
|
|
374
|
+
)
|
|
375
|
+
return;
|
|
376
|
+
event.preventDefault();
|
|
377
|
+
const currentY =
|
|
378
|
+
launcherPositionRef.current?.y ??
|
|
379
|
+
event.currentTarget.getBoundingClientRect().top;
|
|
380
|
+
if (event.key === 'ArrowLeft') dockLauncher('left', currentY);
|
|
381
|
+
else if (event.key === 'ArrowRight') dockLauncher('right', currentY);
|
|
382
|
+
else
|
|
383
|
+
dockLauncher(
|
|
384
|
+
launcherSideRef.current,
|
|
385
|
+
currentY + (event.key === 'ArrowUp' ? -24 : 24),
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
188
389
|
const Icon = BrandIcon ?? Sparks;
|
|
189
390
|
|
|
190
391
|
return (
|
|
191
392
|
<>
|
|
192
393
|
{!hideLauncher ? (
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
394
|
+
<>
|
|
395
|
+
<button
|
|
396
|
+
ref={launcherRef}
|
|
397
|
+
type="button"
|
|
398
|
+
data-test="ai-panel-launcher"
|
|
399
|
+
onClick={() => {
|
|
400
|
+
if (suppressLauncherClickRef.current) {
|
|
401
|
+
suppressLauncherClickRef.current = false;
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
setOpen(true);
|
|
405
|
+
}}
|
|
406
|
+
onPointerDown={startLauncherDrag}
|
|
407
|
+
onPointerMove={moveLauncher}
|
|
408
|
+
onPointerUp={finishLauncherDrag}
|
|
409
|
+
onPointerCancel={finishLauncherDrag}
|
|
410
|
+
onKeyDown={moveLauncherWithKeyboard}
|
|
411
|
+
className={`fixed z-30 flex touch-none select-none items-center gap-2 rounded-full border border-ph-border bg-ph-surface px-4 py-2.5 text-sm font-medium text-ph-ink shadow-ph hover:bg-ph-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ph-brand ${draggingLauncher ? 'cursor-grabbing' : 'cursor-grab'}`}
|
|
412
|
+
style={
|
|
413
|
+
launcherPosition
|
|
414
|
+
? { left: launcherPosition.x, top: launcherPosition.y }
|
|
415
|
+
: { bottom: 20, right: 20 }
|
|
416
|
+
}
|
|
417
|
+
aria-label="Open Ask AI"
|
|
418
|
+
aria-describedby="suite-ask-ai-launcher-help"
|
|
419
|
+
>
|
|
420
|
+
<Icon className="h-4 w-4 text-ph-brand" />
|
|
421
|
+
Ask AI
|
|
422
|
+
</button>
|
|
423
|
+
<span id="suite-ask-ai-launcher-help" className="sr-only">
|
|
424
|
+
Drag to reposition, or use arrow keys. The launcher docks to the
|
|
425
|
+
nearest screen edge.
|
|
426
|
+
</span>
|
|
427
|
+
</>
|
|
203
428
|
) : null}
|
|
204
429
|
|
|
205
430
|
{open ? (
|
|
@@ -211,7 +436,7 @@ export function SuiteAiPanel({
|
|
|
211
436
|
aria-label="Close Ask AI"
|
|
212
437
|
/>
|
|
213
438
|
<aside
|
|
214
|
-
className=
|
|
439
|
+
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'}`}
|
|
215
440
|
data-test="suite-ask-ai-panel"
|
|
216
441
|
>
|
|
217
442
|
<div className="flex items-center justify-between border-b border-ph-border px-4 py-3">
|