@gridland/demo 0.2.52 → 0.2.53

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/landing.js CHANGED
@@ -52,6 +52,33 @@ function textStyle(opts) {
52
52
 
53
53
  // ../ui/components/status-bar/status-bar.tsx
54
54
  import { jsx as jsx3 } from "react/jsx-runtime";
55
+ function StatusBar({ items, extra }) {
56
+ const theme = useTheme();
57
+ const parts = [];
58
+ if (extra !== void 0) {
59
+ parts.push(
60
+ /* @__PURE__ */ jsx3("span", { children: extra }, "extra")
61
+ );
62
+ parts.push(
63
+ /* @__PURE__ */ jsx3("span", { style: textStyle({ dim: true, fg: theme.placeholder }), children: " \u2502 " }, "pipe")
64
+ );
65
+ }
66
+ items.forEach((item, i) => {
67
+ if (i > 0) {
68
+ parts.push(/* @__PURE__ */ jsx3("span", { children: " " }, `gap-${i}`));
69
+ }
70
+ parts.push(
71
+ /* @__PURE__ */ jsx3("span", { style: textStyle({ bold: true, fg: theme.background, bg: theme.muted }), children: ` ${item.key} ` }, `key-${i}`)
72
+ );
73
+ parts.push(
74
+ /* @__PURE__ */ jsx3("span", { style: textStyle({ dim: true, fg: theme.placeholder }), children: ` ${item.label}` }, `label-${i}`)
75
+ );
76
+ });
77
+ if (parts.length === 0) {
78
+ return null;
79
+ }
80
+ return /* @__PURE__ */ jsx3("text", { children: parts });
81
+ }
55
82
 
56
83
  // ../ui/components/provider/provider.tsx
57
84
  import { createContext as createContext2, useContext as useContext2 } from "react";
@@ -185,7 +212,7 @@ import {
185
212
  createContext as createContext5,
186
213
  useContext as useContext5
187
214
  } from "react";
188
- import { Fragment as Fragment5, jsx as jsx16, jsxs as jsxs10 } from "react/jsx-runtime";
215
+ import { jsx as jsx16, jsxs as jsxs10 } from "react/jsx-runtime";
189
216
  var PromptInputControllerCtx = createContext5(null);
190
217
  var useOptionalController = () => useContext5(PromptInputControllerCtx);
191
218
  var PromptInputContext = createContext5(null);
@@ -212,10 +239,11 @@ function resolveStatusHintText(status, submittedText, streamingText, errorText,
212
239
  if (status === "error") return errorText;
213
240
  return disabledText;
214
241
  }
215
- var DIVIDER_LINE = "\u2500".repeat(500);
216
242
  function PromptInputDivider() {
217
- const { theme } = usePromptInput();
218
- return /* @__PURE__ */ jsx16("text", { wrapMode: "none", marginLeft: -1, marginRight: -1, children: /* @__PURE__ */ jsx16("span", { style: textStyle({ dim: true, fg: theme.muted }), children: DIVIDER_LINE }) });
243
+ const { dividerColor, dividerDashed, theme } = usePromptInput();
244
+ const color = dividerColor ?? theme.muted;
245
+ const char = dividerDashed ? "\u254C" : "\u2500";
246
+ return /* @__PURE__ */ jsx16("text", { wrapMode: "none", marginLeft: -1, marginRight: -1, children: /* @__PURE__ */ jsx16("span", { style: textStyle({ dim: !dividerColor, fg: color }), children: char.repeat(500) }) });
219
247
  }
220
248
  function PromptInputSuggestions() {
221
249
  const { suggestions, sugIdx, maxSuggestions, theme } = usePromptInput();
@@ -230,18 +258,25 @@ function PromptInputSuggestions() {
230
258
  ] }, sug.text);
231
259
  }) });
232
260
  }
233
- var CURSOR_CHAR = "\u258D";
234
261
  function PromptInputTextarea() {
235
- const { value, disabled, statusHintText, placeholder, prompt, promptColor, theme } = usePromptInput();
236
- return /* @__PURE__ */ jsxs10("text", { children: [
237
- /* @__PURE__ */ jsx16("span", { style: textStyle({ fg: promptColor }), children: prompt }),
238
- value.length === 0 ? /* @__PURE__ */ jsxs10(Fragment5, { children: [
239
- !disabled && /* @__PURE__ */ jsx16("span", { style: textStyle({ fg: theme.muted }), children: CURSOR_CHAR }),
240
- /* @__PURE__ */ jsx16("span", { style: textStyle({ dim: true, fg: theme.placeholder }), children: disabled ? statusHintText : " " + placeholder })
241
- ] }) : /* @__PURE__ */ jsxs10(Fragment5, { children: [
242
- /* @__PURE__ */ jsx16("span", { style: textStyle({ fg: theme.foreground }), children: value }),
243
- !disabled && /* @__PURE__ */ jsx16("span", { style: textStyle({ fg: theme.muted }), children: CURSOR_CHAR })
244
- ] })
262
+ const { value, isFocused, disabled, statusHintText, placeholder, prompt, promptColor, theme, handleInput, handleInputSubmit, handleInputKeyDown } = usePromptInput();
263
+ return /* @__PURE__ */ jsxs10("box", { flexDirection: "row", children: [
264
+ /* @__PURE__ */ jsx16("text", { children: /* @__PURE__ */ jsx16("span", { style: textStyle({ fg: promptColor }), children: prompt }) }),
265
+ isFocused ? /* @__PURE__ */ jsx16(
266
+ "input",
267
+ {
268
+ value,
269
+ placeholder,
270
+ focused: true,
271
+ onInput: handleInput,
272
+ onSubmit: handleInputSubmit,
273
+ onKeyDown: handleInputKeyDown,
274
+ cursorColor: theme.muted,
275
+ cursorStyle: { style: "line", blinking: !value },
276
+ placeholderColor: theme.placeholder,
277
+ textColor: theme.foreground
278
+ }
279
+ ) : disabled && value.length === 0 ? /* @__PURE__ */ jsx16("text", { children: /* @__PURE__ */ jsx16("span", { style: textStyle({ dim: true, fg: theme.placeholder }), children: statusHintText }) }) : /* @__PURE__ */ jsx16("text", { children: /* @__PURE__ */ jsx16("span", { style: textStyle({ fg: value ? theme.foreground : theme.placeholder, dim: !value }), children: value || placeholder }) })
245
280
  ] });
246
281
  }
247
282
  function PromptInputSubmit(props) {
@@ -288,13 +323,16 @@ function PromptInput({
288
323
  maxSuggestions = 5,
289
324
  enableHistory = true,
290
325
  model,
326
+ focus = true,
291
327
  showDividers = true,
292
328
  autoFocus = false,
329
+ dividerColor,
330
+ dividerDashed,
293
331
  useKeyboard: useKeyboardProp,
294
332
  children
295
333
  }) {
296
334
  const theme = useTheme();
297
- const useKeyboard = useKeyboardContext(useKeyboardProp);
335
+ const useKeyboard3 = useKeyboardContext(useKeyboardProp);
298
336
  useEffect2(() => {
299
337
  if (!autoFocus) return;
300
338
  if (typeof document === "undefined") return;
@@ -305,6 +343,7 @@ function PromptInput({
305
343
  }, [autoFocus]);
306
344
  const resolvedPromptColor = promptColor ?? theme.muted;
307
345
  const disabled = status ? status === "submitted" || status === "streaming" : disabledProp;
346
+ const isFocused = focus && !disabled;
308
347
  const statusHintText = resolveStatusHintText(status, submittedText, streamingLabel, errorText, disabledText);
309
348
  const controller = useOptionalController();
310
349
  const usingProvider = !!controller;
@@ -395,18 +434,86 @@ function PromptInput({
395
434
  clearInput();
396
435
  }
397
436
  }, [onSubmit, clearInput]);
398
- useKeyboard?.((event) => {
437
+ const handleInputSubmit = (text) => {
438
+ const trimmed = text.trim();
439
+ if (!trimmed) return;
440
+ if (enableHistory) {
441
+ setHist([trimmed, ...historyRef.current]);
442
+ }
443
+ updateValue("");
444
+ setHistI(-1);
445
+ handleSubmit(trimmed);
446
+ };
447
+ const handleInputKeyDown = (key) => {
448
+ if (key.name === "return" && suggestionsRef.current.length > 0) {
449
+ const sel = suggestionsRef.current[sugIdxRef.current];
450
+ if (sel) {
451
+ if (valueRef.current.startsWith("/")) {
452
+ updateValue("");
453
+ if (enableHistory) {
454
+ setHist([sel.text, ...historyRef.current]);
455
+ }
456
+ setHistI(-1);
457
+ handleSubmit(sel.text);
458
+ } else {
459
+ const base = valueRef.current.slice(0, valueRef.current.lastIndexOf("@"));
460
+ updateValue(base + sel.text + " ");
461
+ setSug([]);
462
+ }
463
+ }
464
+ key.preventDefault();
465
+ return;
466
+ }
467
+ if (key.name === "tab" && suggestionsRef.current.length > 0) {
468
+ setSugI((sugIdxRef.current + 1) % suggestionsRef.current.length);
469
+ key.preventDefault();
470
+ return;
471
+ }
472
+ if (key.name === "up") {
473
+ if (suggestionsRef.current.length > 0) {
474
+ setSugI(Math.max(0, sugIdxRef.current - 1));
475
+ } else if (enableHistory && historyRef.current.length > 0) {
476
+ const idx = Math.min(historyRef.current.length - 1, histIdxRef.current + 1);
477
+ setHistI(idx);
478
+ updateValue(historyRef.current[idx]);
479
+ }
480
+ key.preventDefault();
481
+ return;
482
+ }
483
+ if (key.name === "down") {
484
+ if (suggestionsRef.current.length > 0) {
485
+ setSugI(Math.min(suggestionsRef.current.length - 1, sugIdxRef.current + 1));
486
+ } else if (enableHistory && histIdxRef.current > 0) {
487
+ const nextIdx = histIdxRef.current - 1;
488
+ setHistI(nextIdx);
489
+ updateValue(historyRef.current[nextIdx]);
490
+ } else if (enableHistory && histIdxRef.current === 0) {
491
+ setHistI(-1);
492
+ updateValue("");
493
+ }
494
+ key.preventDefault();
495
+ return;
496
+ }
497
+ if (key.name === "escape") {
498
+ if (suggestionsRef.current.length > 0) {
499
+ setSug([]);
500
+ key.preventDefault();
501
+ }
502
+ return;
503
+ }
504
+ };
505
+ useKeyboard3?.((event) => {
399
506
  if (event.name === "escape" && (status === "streaming" || status === "submitted") && onStop) {
400
507
  onStop();
401
508
  return;
402
509
  }
510
+ if (isFocused) return;
403
511
  if (disabled) return;
404
512
  if (event.name === "return") {
405
513
  if (suggestionsRef.current.length > 0) {
406
514
  const sel = suggestionsRef.current[sugIdxRef.current];
407
515
  if (sel) {
408
516
  if (valueRef.current.startsWith("/")) {
409
- setSug([]);
410
517
  updateValue("");
411
518
  if (enableHistory) {
412
519
  setHist([sel.text, ...historyRef.current]);
@@ -427,7 +534,6 @@ function PromptInput({
427
534
  }
428
535
  updateValue("");
429
536
  setHistI(-1);
430
- setSug([]);
431
537
  handleSubmit(trimmed);
432
538
  }
433
539
  return;
@@ -481,6 +587,7 @@ function PromptInput({
481
587
  const visibleSuggestions = suggestions.slice(0, maxSuggestions);
482
588
  const ctxValue = {
483
589
  value,
590
+ isFocused,
484
591
  disabled,
485
592
  status,
486
593
  onStop,
@@ -493,7 +600,12 @@ function PromptInput({
493
600
  maxSuggestions,
494
601
  errorText,
495
602
  model,
496
- theme
603
+ dividerColor,
604
+ dividerDashed,
605
+ theme,
606
+ handleInput: updateValue,
607
+ handleInputSubmit,
608
+ handleInputKeyDown
497
609
  };
498
610
  if (children) {
499
611
  return /* @__PURE__ */ jsx16(PromptInputContext.Provider, { value: ctxValue, children: /* @__PURE__ */ jsx16("box", { flexDirection: "column", flexShrink: 0, children }) });
@@ -521,7 +633,7 @@ import { jsx as jsx17, jsxs as jsxs11 } from "react/jsx-runtime";
521
633
 
522
634
  // ../ui/components/chain-of-thought/chain-of-thought.tsx
523
635
  import { createContext as createContext6, memo, useContext as useContext6, useEffect as useEffect3, useMemo as useMemo4, useState as useState7 } from "react";
524
- import { Fragment as Fragment6, jsx as jsx18, jsxs as jsxs12 } from "react/jsx-runtime";
636
+ import { Fragment as Fragment5, jsx as jsx18, jsxs as jsxs12 } from "react/jsx-runtime";
525
637
  var DOTS = ["\u25CB", "\u25D4", "\u25D1", "\u25D5", "\u25CF"];
526
638
  var SPINNER_INTERVAL = 150;
527
639
  var ChainOfThoughtContext = createContext6(null);
@@ -580,7 +692,7 @@ var ChainOfThoughtHeader = memo(({
580
692
  var ChainOfThoughtContent = memo(({ children }) => {
581
693
  const { isOpen } = useChainOfThought();
582
694
  if (!isOpen) return null;
583
- return /* @__PURE__ */ jsx18(Fragment6, { children });
695
+ return /* @__PURE__ */ jsx18(Fragment5, { children });
584
696
  });
585
697
  var ChainOfThoughtStep = memo(({
586
698
  label,
@@ -793,7 +905,7 @@ function useBreakpoints() {
793
905
  }
794
906
 
795
907
  // src/landing/landing-app.tsx
796
- import { useMemo as useMemo7 } from "react";
908
+ import { useMemo as useMemo8, useRef as useRef9, useState as useState12 } from "react";
797
909
 
798
910
  // src/landing/install-box.tsx
799
911
  import { jsx as jsx21, jsxs as jsxs15 } from "react/jsx-runtime";
@@ -818,10 +930,12 @@ function InstallBox() {
818
930
  }
819
931
 
820
932
  // src/landing/links-box.tsx
933
+ import { isBrowser } from "@gridland/utils";
821
934
  import { jsx as jsx22, jsxs as jsxs16 } from "react/jsx-runtime";
822
935
  var UNDERLINE3 = 1 << 3;
823
936
  function LinksBox() {
824
937
  const theme = useTheme();
938
+ const docsHref = isBrowser() ? `${window.location.origin}/docs` : "https://gridland.io/docs";
825
939
  return /* @__PURE__ */ jsx22(
826
940
  "box",
827
941
  {
@@ -836,7 +950,7 @@ function LinksBox() {
836
950
  /* @__PURE__ */ jsx22("a", { href: "https://github.com/thoughtfulllc/gridland", style: { attributes: UNDERLINE3, fg: theme.accent }, children: " GitHub" }),
837
951
  /* @__PURE__ */ jsx22("span", { children: " " }),
838
952
  /* @__PURE__ */ jsx22("span", { children: "\u{1F4D6}" }),
839
- /* @__PURE__ */ jsx22("a", { href: "https://gridland.io/docs", style: { attributes: UNDERLINE3, fg: theme.accent }, children: " Docs" })
953
+ /* @__PURE__ */ jsx22("a", { href: docsHref, style: { attributes: UNDERLINE3, fg: theme.accent }, children: " Docs" })
840
954
  ] })
841
955
  }
842
956
  );
@@ -845,22 +959,22 @@ function LinksBox() {
845
959
  // src/landing/logo.tsx
846
960
  import { useState as useState8, useEffect as useEffect4, useRef as useRef5, useMemo as useMemo5 } from "react";
847
961
  import figlet from "figlet";
848
- import ansiShadow from "figlet/importable-fonts/ANSI Shadow.js";
849
- import { Fragment as Fragment7, jsx as jsx23, jsxs as jsxs17 } from "react/jsx-runtime";
850
- figlet.parseFont("ANSI Shadow", ansiShadow);
851
- function makeArt(text) {
852
- return figlet.textSync(text, { font: "ANSI Shadow" }).split("\n").filter((l) => l.trimEnd().length > 0).join("\n");
962
+ import blockFont from "figlet/importable-fonts/Block.js";
963
+ import { Fragment as Fragment6, jsx as jsx23, jsxs as jsxs17 } from "react/jsx-runtime";
964
+ figlet.parseFont("Block", blockFont);
965
+ function makeArt(text, font = "Block") {
966
+ return figlet.textSync(text, { font }).split("\n").filter((l) => l.trimEnd().length > 0).join("\n");
853
967
  }
854
- var fullArt = makeArt("gridland");
855
- var gridArt = makeArt("grid");
856
- var landArt = makeArt("land");
857
- var ART_HEIGHT = 6;
968
+ var fullArt = makeArt("gridland", "Block");
969
+ var gridArt = makeArt("grid", "Block");
970
+ var landArt = makeArt("land", "Block");
971
+ var ART_HEIGHT = fullArt.split("\n").length;
858
972
  function useAnimation(duration = 1e3) {
859
- const isBrowser = typeof document !== "undefined";
860
- const [progress, setProgress] = useState8(isBrowser ? 0 : 1);
973
+ const isBrowser2 = typeof document !== "undefined";
974
+ const [progress, setProgress] = useState8(isBrowser2 ? 0 : 1);
861
975
  const startTime = useRef5(null);
862
976
  useEffect4(() => {
863
- if (!isBrowser) return;
977
+ if (!isBrowser2) return;
864
978
  let raf;
865
979
  const tick = (time) => {
866
980
  if (startTime.current === null) startTime.current = time;
@@ -875,12 +989,19 @@ function useAnimation(duration = 1e3) {
875
989
  }, []);
876
990
  return progress;
877
991
  }
992
+ function darkenHex(hex, factor = 0.4) {
993
+ const r = Math.round(parseInt(hex.slice(1, 3), 16) * factor);
994
+ const g = Math.round(parseInt(hex.slice(3, 5), 16) * factor);
995
+ const b = Math.round(parseInt(hex.slice(5, 7), 16) * factor);
996
+ return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
997
+ }
878
998
  function RevealGradient({ children, revealCol }) {
879
999
  const gradientColors = GRADIENTS.instagram;
880
1000
  const lines = children.split("\n");
881
1001
  const maxLength = Math.max(...lines.map((l) => l.length));
882
1002
  if (maxLength === 0) return /* @__PURE__ */ jsx23("text", { children });
883
1003
  const hexColors = useMemo5(() => generateGradient(gradientColors, maxLength), [maxLength]);
1004
+ const bgColors = useMemo5(() => hexColors.map((c) => darkenHex(c)), [hexColors]);
884
1005
  return /* @__PURE__ */ jsx23("box", { position: "relative", width: maxLength, height: lines.length, shouldFill: false, children: lines.map((line, lineIndex) => {
885
1006
  const runs = [];
886
1007
  let current = null;
@@ -911,7 +1032,7 @@ function RevealGradient({ children, revealCol }) {
911
1032
  children: /* @__PURE__ */ jsx23("text", { shouldFill: false, children: run.chars.map((char, ci) => /* @__PURE__ */ jsx23(
912
1033
  "span",
913
1034
  {
914
- style: { fg: hexColors[run.start + ci] },
1035
+ style: { fg: hexColors[run.start + ci], bg: bgColors[run.start + ci] },
915
1036
  children: char
916
1037
  },
917
1038
  ci
@@ -922,15 +1043,15 @@ function RevealGradient({ children, revealCol }) {
922
1043
  }) });
923
1044
  }
924
1045
  function Logo({ compact, narrow, mobile }) {
925
- const isBrowser = typeof document !== "undefined";
1046
+ const isBrowser2 = typeof document !== "undefined";
926
1047
  const progress = useAnimation(900);
927
- const artHeight = compact ? 1 : narrow ? ART_HEIGHT * 2 : ART_HEIGHT;
1048
+ const artHeight = compact ? 1 : narrow && !mobile ? ART_HEIGHT * 2 : ART_HEIGHT;
928
1049
  const dropOffset = Math.round((1 - progress) * -artHeight);
929
1050
  const revealProgress = Math.max(0, Math.min(1, (progress - 0.1) / 0.7));
930
- const maxWidth = compact ? 8 : narrow ? 40 : 62;
1051
+ const maxWidth = compact ? 8 : narrow ? 35 : 69;
931
1052
  const revealCol = Math.round(revealProgress * (maxWidth + 4)) - 2;
932
1053
  const taglineOpacity = Math.max(0, Math.min(1, (progress - 0.7) / 0.3));
933
- const subtitle = /* @__PURE__ */ jsxs17(Fragment7, { children: [
1054
+ const subtitle = /* @__PURE__ */ jsxs17(Fragment6, { children: [
934
1055
  /* @__PURE__ */ jsx23("text", { children: " " }),
935
1056
  /* @__PURE__ */ jsx23("box", { flexDirection: "column", alignItems: "center", width: "100%", shouldFill: false, children: /* @__PURE__ */ jsxs17("text", { style: textStyle({ fg: "#d4b0e8" }), opacity: taglineOpacity, wrapMode: "word", textAlign: "center", width: "100%", shouldFill: false, children: [
936
1057
  "A framework for building terminal apps, built on ",
@@ -938,8 +1059,8 @@ function Logo({ compact, narrow, mobile }) {
938
1059
  " + React." + (mobile ? " " : "\n") + "(Gridland apps, like this website, work in the browser and terminal.)"
939
1060
  ] }) })
940
1061
  ] });
941
- if (!isBrowser) {
942
- const art = compact ? "gridland" : narrow ? gridArt + "\n" + landArt : fullArt;
1062
+ if (!isBrowser2) {
1063
+ const art = compact ? "gridland" : narrow && !mobile ? gridArt + "\n" + landArt : fullArt;
943
1064
  return /* @__PURE__ */ jsxs17("box", { flexDirection: "column", flexShrink: 0, width: "100%", alignItems: "center", shouldFill: false, children: [
944
1065
  /* @__PURE__ */ jsx23(Gradient, { name: "instagram", children: art }),
945
1066
  /* @__PURE__ */ jsx23("text", { children: " " }),
@@ -956,7 +1077,7 @@ function Logo({ compact, narrow, mobile }) {
956
1077
  subtitle
957
1078
  ] });
958
1079
  }
959
- if (narrow) {
1080
+ if (narrow && !mobile) {
960
1081
  return /* @__PURE__ */ jsxs17("box", { flexDirection: "column", flexShrink: 0, width: "100%", shouldFill: false, children: [
961
1082
  /* @__PURE__ */ jsx23("box", { height: artHeight, overflow: "hidden", position: "relative", width: "100%", flexShrink: 0, shouldFill: false, children: /* @__PURE__ */ jsxs17("box", { position: "absolute", top: dropOffset, width: "100%", flexDirection: "column", alignItems: "center", shouldFill: false, children: [
962
1083
  /* @__PURE__ */ jsx23(RevealGradient, { revealCol, children: gridArt }),
@@ -975,7 +1096,7 @@ function Logo({ compact, narrow, mobile }) {
975
1096
  import { useMemo as useMemo6 } from "react";
976
1097
 
977
1098
  // src/landing/use-matrix.ts
978
- import { useState as useState9, useEffect as useEffect5, useRef as useRef6 } from "react";
1099
+ import { useState as useState9, useEffect as useEffect5, useLayoutEffect, useRef as useRef6 } from "react";
979
1100
  var CHARS = "abcdefghijklmnopqrstuvwxyz0123456789@#$%^&*(){}[]|;:<>,.?/~`";
980
1101
  function randomChar() {
981
1102
  return CHARS[Math.floor(Math.random() * CHARS.length)];
@@ -989,7 +1110,11 @@ function createDrop(height, seeded = false) {
989
1110
  chars: Array.from({ length }, randomChar)
990
1111
  };
991
1112
  }
992
- function buildGrid(columns, width, height) {
1113
+ var PULL_RADIUS = 18;
1114
+ var PULL_STRENGTH = 7;
1115
+ var RIPPLE_DURATION_MS = 3200;
1116
+ var RIPPLE_SPEED = 8e-3;
1117
+ function buildGrid(columns, width, height, mousePos, ripples, now = Date.now()) {
993
1118
  const grid = Array.from({ length: height }, () => Array(width).fill(" "));
994
1119
  const brightness = Array.from({ length: height }, () => Array(width).fill(0));
995
1120
  for (let x = 0; x < width; x++) {
@@ -998,17 +1123,53 @@ function buildGrid(columns, width, height) {
998
1123
  for (let i = 0; i < drop.length; i++) {
999
1124
  const row = Math.floor(drop.y) - i;
1000
1125
  if (row < 0 || row >= height) continue;
1001
- grid[row][x] = drop.chars[i];
1002
- if (i === 0) {
1003
- brightness[row][x] = 1;
1004
- } else {
1005
- brightness[row][x] = Math.max(0.15, 1 - i / drop.length);
1126
+ let renderX = x;
1127
+ if (mousePos) {
1128
+ const dx = mousePos.x - x;
1129
+ const dy = mousePos.y - row;
1130
+ const dist = Math.sqrt(dx * dx + dy * dy);
1131
+ if (dist < PULL_RADIUS && dist > 0.5) {
1132
+ const t = 1 - dist / PULL_RADIUS;
1133
+ const strength = t * t * PULL_STRENGTH;
1134
+ renderX = Math.round(x + dx / dist * strength);
1135
+ renderX = Math.max(0, Math.min(width - 1, renderX));
1136
+ }
1137
+ }
1138
+ const b = i === 0 ? 1 : Math.max(0.15, 1 - i / drop.length);
1139
+ if (brightness[row][renderX] < b) {
1140
+ grid[row][renderX] = drop.chars[i];
1141
+ brightness[row][renderX] = b;
1142
+ }
1143
+ }
1144
+ }
1145
+ for (const ripple of ripples) {
1146
+ const elapsed = now - ripple.createdAt;
1147
+ if (elapsed > RIPPLE_DURATION_MS || elapsed < 0) continue;
1148
+ const radius = elapsed * RIPPLE_SPEED;
1149
+ const fade = 1 - elapsed / RIPPLE_DURATION_MS;
1150
+ const maxR = Math.ceil(radius) + 2;
1151
+ const rx = Math.round(ripple.x);
1152
+ const ry = Math.round(ripple.y);
1153
+ for (let dy = -maxR; dy <= maxR; dy++) {
1154
+ for (let dx = -maxR; dx <= maxR; dx++) {
1155
+ const cy = ry + dy;
1156
+ const cx = rx + dx;
1157
+ if (cy < 0 || cy >= height || cx < 0 || cx >= width) continue;
1158
+ const dist = Math.sqrt(dx * dx + dy * dy);
1159
+ const ringDist = Math.abs(dist - radius);
1160
+ if (ringDist < 2) {
1161
+ const boost = (1 - ringDist / 2) * fade * 0.7;
1162
+ brightness[cy][cx] = Math.min(1, brightness[cy][cx] + boost);
1163
+ if (grid[cy][cx] === " " && boost > 0.2) {
1164
+ grid[cy][cx] = randomChar();
1165
+ }
1166
+ }
1006
1167
  }
1007
1168
  }
1008
1169
  }
1009
1170
  return { grid, brightness };
1010
1171
  }
1011
- function useMatrix(width, height) {
1172
+ function useMatrix(width, height, mousePosRef, ripplesRef) {
1012
1173
  const columnsRef = useRef6([]);
1013
1174
  const [state, setState] = useState9(() => {
1014
1175
  const columns = Array.from(
@@ -1016,12 +1177,13 @@ function useMatrix(width, height) {
1016
1177
  () => Math.random() < 0.5 ? createDrop(height, true) : null
1017
1178
  );
1018
1179
  columnsRef.current = columns;
1019
- return buildGrid(columns, width, height);
1180
+ return buildGrid(columns, width, height, null, []);
1020
1181
  });
1021
1182
  useEffect5(() => {
1022
1183
  if (width < 2 || height < 2) return;
1023
1184
  const id = setInterval(() => {
1024
1185
  const columns = columnsRef.current;
1186
+ const now = Date.now();
1025
1187
  for (let x = 0; x < width; x++) {
1026
1188
  if (columns[x] === null || columns[x] === void 0) {
1027
1189
  if (Math.random() < 0.03) {
@@ -1039,16 +1201,23 @@ function useMatrix(width, height) {
1039
1201
  columns[x] = null;
1040
1202
  }
1041
1203
  }
1042
- setState(buildGrid(columns, width, height));
1204
+ if (ripplesRef?.current) {
1205
+ ripplesRef.current = ripplesRef.current.filter(
1206
+ (r) => now - r.createdAt < RIPPLE_DURATION_MS
1207
+ );
1208
+ }
1209
+ const mousePos = mousePosRef?.current ?? null;
1210
+ const ripples = ripplesRef?.current ?? [];
1211
+ setState(buildGrid(columns, width, height, mousePos, ripples, now));
1043
1212
  }, 80);
1044
1213
  return () => clearInterval(id);
1045
1214
  }, [width, height]);
1046
- useEffect5(() => {
1215
+ useLayoutEffect(() => {
1047
1216
  columnsRef.current = Array.from(
1048
1217
  { length: width },
1049
1218
  () => Math.random() < 0.5 ? createDrop(height, true) : null
1050
1219
  );
1051
- setState(buildGrid(columnsRef.current, width, height));
1220
+ setState(buildGrid(columnsRef.current, width, height, null, []));
1052
1221
  }, [width, height]);
1053
1222
  return state;
1054
1223
  }
@@ -1070,8 +1239,8 @@ function colorForCell(mutedColors, b) {
1070
1239
  const idx = Math.min(Math.floor(b * (MUTE_LEVELS.length - 1)), MUTE_LEVELS.length - 2);
1071
1240
  return mutedColors[idx];
1072
1241
  }
1073
- function MatrixBackground({ width, height, clearRect, clearRects }) {
1074
- const { grid, brightness } = useMatrix(width, height);
1242
+ function MatrixBackground({ width, height, clearRect, clearRects, mousePosRef, ripplesRef }) {
1243
+ const { grid, brightness } = useMatrix(width, height, mousePosRef, ripplesRef);
1075
1244
  const theme = useTheme();
1076
1245
  const columnColors = useMemo6(
1077
1246
  () => width > 0 ? generateGradient([theme.accent, theme.secondary, theme.primary], width) : [],
@@ -1102,48 +1271,495 @@ function MatrixBackground({ width, height, clearRect, clearRects }) {
1102
1271
  }) }, y)) });
1103
1272
  }
1104
1273
 
1105
- // src/landing/landing-app.tsx
1274
+ // demos/ripple.tsx
1275
+ import { useState as useState10, useEffect as useEffect6, useRef as useRef7, useCallback as useCallback3 } from "react";
1276
+ import { useKeyboard } from "@gridland/utils";
1106
1277
  import { jsx as jsx25, jsxs as jsxs18 } from "react/jsx-runtime";
1107
- function LandingApp({ useKeyboard }) {
1278
+ var DEFAULT_COLS = 40;
1279
+ var DEFAULT_ROWS = 10;
1280
+ var CHARS2 = ["\xB7", "\u2591", "\u2592", "\u2593", "\u2588"];
1281
+ function hexToRgb2(hex) {
1282
+ const h = hex.replace("#", "");
1283
+ return [
1284
+ parseInt(h.slice(0, 2), 16),
1285
+ parseInt(h.slice(2, 4), 16),
1286
+ parseInt(h.slice(4, 6), 16)
1287
+ ];
1288
+ }
1289
+ function rgbToHex2(r, g, b) {
1290
+ const clamp = (v) => Math.max(0, Math.min(255, Math.round(v)));
1291
+ return "#" + clamp(r).toString(16).padStart(2, "0") + clamp(g).toString(16).padStart(2, "0") + clamp(b).toString(16).padStart(2, "0");
1292
+ }
1293
+ function lerp2(a, b, t) {
1294
+ return a + (b - a) * t;
1295
+ }
1296
+ function RippleApp({ mouseOffset = { x: 0, y: 0 }, containerWidth, containerHeight } = {}) {
1297
+ const theme = useTheme();
1298
+ const [, setTick] = useState10(0);
1299
+ const COLS2 = containerWidth ? containerWidth - 2 : DEFAULT_COLS;
1300
+ const ROWS2 = containerHeight ? Math.max(3, containerHeight - 2 - 2) : DEFAULT_ROWS;
1301
+ const cursorRef = useRef7({ x: Math.floor(COLS2 / 2), y: Math.floor(ROWS2 / 2) });
1302
+ const ripplesRef = useRef7([]);
1303
+ const frameRef = useRef7(0);
1304
+ const mousePosRef = useRef7(null);
1305
+ const accentRgb = hexToRgb2(theme.accent);
1306
+ const dimRgb = [40, 40, 50];
1307
+ const baseRgb = [60, 60, 70];
1308
+ const addRipple = useCallback3((x, y) => {
1309
+ ripplesRef.current = [
1310
+ ...ripplesRef.current,
1311
+ { x, y, time: frameRef.current }
1312
+ ];
1313
+ }, []);
1314
+ useEffect6(() => {
1315
+ const interval = setInterval(() => {
1316
+ frameRef.current++;
1317
+ ripplesRef.current = ripplesRef.current.filter(
1318
+ (r) => frameRef.current - r.time < 30
1319
+ );
1320
+ setTick((t) => t + 1);
1321
+ }, 60);
1322
+ return () => clearInterval(interval);
1323
+ }, []);
1324
+ useKeyboard((event) => {
1325
+ const cursor2 = cursorRef.current;
1326
+ if (event.name === "up") {
1327
+ cursorRef.current = { ...cursor2, y: Math.max(0, cursor2.y - 1) };
1328
+ } else if (event.name === "down") {
1329
+ cursorRef.current = { ...cursor2, y: Math.min(ROWS2 - 1, cursor2.y + 1) };
1330
+ } else if (event.name === "left") {
1331
+ cursorRef.current = { ...cursor2, x: Math.max(0, cursor2.x - 1) };
1332
+ } else if (event.name === "right") {
1333
+ cursorRef.current = { ...cursor2, x: Math.min(COLS2 - 1, cursor2.x + 1) };
1334
+ } else if (event.name === "return") {
1335
+ addRipple(cursorRef.current.x, cursorRef.current.y);
1336
+ }
1337
+ event.preventDefault();
1338
+ });
1339
+ const cursor = cursorRef.current;
1340
+ const frame = frameRef.current;
1341
+ const ripples = ripplesRef.current;
1342
+ const grid = Array.from({ length: ROWS2 }, (_, row) => /* @__PURE__ */ jsx25("text", { children: Array.from({ length: COLS2 }, (_2, col) => {
1343
+ const isCursor = col === cursor.x && row === cursor.y;
1344
+ const baseWave = Math.sin(frame * 0.08 + col * 0.3 + row * 0.5) * 0.5 + 0.5;
1345
+ let intensity = baseWave * 0.15;
1346
+ for (const ripple of ripples) {
1347
+ const dx = col - ripple.x;
1348
+ const dy = row - ripple.y;
1349
+ const dist = Math.sqrt(dx * dx + dy * dy);
1350
+ const age = frame - ripple.time;
1351
+ const radius = age * 0.5;
1352
+ const fade = 1 - age / 30;
1353
+ const ringDist = Math.abs(dist - radius);
1354
+ if (ringDist < 1.5) {
1355
+ const ringIntensity = (1 - ringDist / 1.5) * fade;
1356
+ intensity = Math.max(intensity, ringIntensity);
1357
+ } else if (dist < radius) {
1358
+ const innerIntensity = fade * 0.3 * (1 - dist / radius);
1359
+ intensity = Math.max(intensity, innerIntensity);
1360
+ }
1361
+ }
1362
+ intensity = Math.max(0, Math.min(1, intensity));
1363
+ const charIndex = Math.min(
1364
+ CHARS2.length - 1,
1365
+ Math.floor(intensity * CHARS2.length)
1366
+ );
1367
+ const char = isCursor ? "\u25C6" : CHARS2[charIndex];
1368
+ let fg;
1369
+ if (isCursor) {
1370
+ fg = theme.primary;
1371
+ } else {
1372
+ const r = lerp2(dimRgb[0], accentRgb[0], intensity);
1373
+ const g = lerp2(dimRgb[1], accentRgb[1], intensity);
1374
+ const b = lerp2(dimRgb[2], accentRgb[2], intensity);
1375
+ fg = rgbToHex2(r, g, b);
1376
+ }
1377
+ return /* @__PURE__ */ jsx25("span", { style: { fg, bold: isCursor || intensity > 0.7 }, children: char }, col);
1378
+ }) }, row));
1379
+ return /* @__PURE__ */ jsxs18("box", { flexDirection: "column", flexGrow: 1, children: [
1380
+ /* @__PURE__ */ jsxs18(
1381
+ "box",
1382
+ {
1383
+ flexDirection: "column",
1384
+ flexGrow: 1,
1385
+ paddingX: 1,
1386
+ onMouseMove: (e) => {
1387
+ const gx = e.x - mouseOffset.x - 1;
1388
+ const gy = e.y - mouseOffset.y - 2;
1389
+ if (gx >= 0 && gx < COLS2 && gy >= 0 && gy < ROWS2) {
1390
+ mousePosRef.current = { x: gx, y: gy };
1391
+ cursorRef.current = { x: gx, y: gy };
1392
+ }
1393
+ },
1394
+ onMouseDown: (e) => {
1395
+ const gx = e.x - mouseOffset.x - 1;
1396
+ const gy = e.y - mouseOffset.y - 2;
1397
+ if (gx >= 0 && gx < COLS2 && gy >= 0 && gy < ROWS2) {
1398
+ addRipple(gx, gy);
1399
+ }
1400
+ },
1401
+ children: [
1402
+ /* @__PURE__ */ jsx25("text", { style: { dim: true, fg: theme.muted }, children: "Click or press Enter to create ripples" }),
1403
+ /* @__PURE__ */ jsx25("box", { flexDirection: "column", children: grid })
1404
+ ]
1405
+ }
1406
+ ),
1407
+ /* @__PURE__ */ jsx25("box", { flexGrow: 1 }),
1408
+ /* @__PURE__ */ jsx25("box", { paddingX: 1, children: /* @__PURE__ */ jsx25(
1409
+ StatusBar,
1410
+ {
1411
+ items: [
1412
+ { key: "\u2191\u2193\u2190\u2192", label: "move" },
1413
+ { key: "enter/click", label: "ripple" }
1414
+ ]
1415
+ }
1416
+ ) })
1417
+ ] });
1418
+ }
1419
+
1420
+ // demos/puzzle.tsx
1421
+ import { useState as useState11, useEffect as useEffect7, useRef as useRef8, useMemo as useMemo7 } from "react";
1422
+ import { useKeyboard as useKeyboard2 } from "@gridland/utils";
1423
+ import { jsx as jsx26, jsxs as jsxs19 } from "react/jsx-runtime";
1424
+ var COLS = 4;
1425
+ var ROWS = 3;
1426
+ var TILE_COUNT = COLS * ROWS;
1427
+ var DEFAULT_TILE_WIDTH = 8;
1428
+ var DEFAULT_TILE_HEIGHT = 3;
1429
+ var SOLVED = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0];
1430
+ var tileColors = [
1431
+ "#ef4444",
1432
+ "#f97316",
1433
+ "#eab308",
1434
+ "#22c55e",
1435
+ "#3b82f6",
1436
+ "#8b5cf6",
1437
+ "#ec4899",
1438
+ "#14b8a6",
1439
+ "#f43f5e",
1440
+ "#6366f1",
1441
+ "#84cc16"
1442
+ ];
1443
+ function isSolved(board) {
1444
+ for (let i = 0; i < TILE_COUNT; i++) {
1445
+ if (board[i] !== SOLVED[i]) return false;
1446
+ }
1447
+ return true;
1448
+ }
1449
+ function getEmptyIndex(board) {
1450
+ return board.indexOf(0);
1451
+ }
1452
+ function getNeighbor(emptyIdx, direction) {
1453
+ const row = Math.floor(emptyIdx / COLS);
1454
+ const col = emptyIdx % COLS;
1455
+ switch (direction) {
1456
+ case "up":
1457
+ return row > 0 ? emptyIdx - COLS : null;
1458
+ case "down":
1459
+ return row < ROWS - 1 ? emptyIdx + COLS : null;
1460
+ case "left":
1461
+ return col > 0 ? emptyIdx - 1 : null;
1462
+ case "right":
1463
+ return col < COLS - 1 ? emptyIdx + 1 : null;
1464
+ default:
1465
+ return null;
1466
+ }
1467
+ }
1468
+ function swap(board, a, b) {
1469
+ const next = [...board];
1470
+ next[a] = board[b];
1471
+ next[b] = board[a];
1472
+ return next;
1473
+ }
1474
+ function shuffle(board, count) {
1475
+ let current = [...board];
1476
+ const directions = ["up", "down", "left", "right"];
1477
+ let lastDir = "";
1478
+ for (let i = 0; i < count; i++) {
1479
+ const emptyIdx = getEmptyIndex(current);
1480
+ const validMoves = directions.filter((d) => {
1481
+ if (d === lastDir) return false;
1482
+ return getNeighbor(emptyIdx, d) !== null;
1483
+ });
1484
+ const dir = validMoves[Math.floor(Math.random() * validMoves.length)];
1485
+ const neighbor = getNeighbor(emptyIdx, dir);
1486
+ current = swap(current, emptyIdx, neighbor);
1487
+ const opposites = { up: "down", down: "up", left: "right", right: "left" };
1488
+ lastDir = opposites[dir];
1489
+ }
1490
+ return current;
1491
+ }
1492
+ function isAdjacentToEmpty(board, tileIdx) {
1493
+ const emptyIdx = getEmptyIndex(board);
1494
+ const eRow = Math.floor(emptyIdx / COLS);
1495
+ const eCol = emptyIdx % COLS;
1496
+ const tRow = Math.floor(tileIdx / COLS);
1497
+ const tCol = tileIdx % COLS;
1498
+ return Math.abs(eRow - tRow) === 1 && eCol === tCol || Math.abs(eCol - tCol) === 1 && eRow === tRow;
1499
+ }
1500
+ function PuzzleApp({ containerWidth, containerHeight } = {}) {
1501
+ const theme = useTheme();
1502
+ const TILE_WIDTH = containerWidth ? Math.floor((containerWidth - 2) / COLS) : DEFAULT_TILE_WIDTH;
1503
+ const TILE_HEIGHT = containerHeight ? Math.max(3, Math.floor((containerHeight - 2 - 4) / ROWS)) : DEFAULT_TILE_HEIGHT;
1504
+ const [board, setBoard] = useState11(SOLVED);
1505
+ const [moves, setMoves] = useState11(0);
1506
+ const [solved, setSolved] = useState11(false);
1507
+ const boardRef = useRef8(board);
1508
+ const movesRef = useRef8(moves);
1509
+ boardRef.current = board;
1510
+ movesRef.current = moves;
1511
+ const doMove = (direction) => {
1512
+ const current = boardRef.current;
1513
+ if (isSolved(current) && movesRef.current > 0) return;
1514
+ const emptyIdx = getEmptyIndex(current);
1515
+ const neighbor = getNeighbor(emptyIdx, direction);
1516
+ if (neighbor === null) return;
1517
+ const next = swap(current, emptyIdx, neighbor);
1518
+ boardRef.current = next;
1519
+ movesRef.current++;
1520
+ setBoard(next);
1521
+ setMoves(movesRef.current);
1522
+ setSolved(isSolved(next));
1523
+ };
1524
+ const doShuffle = () => {
1525
+ const shuffled = shuffle(SOLVED, 30);
1526
+ boardRef.current = shuffled;
1527
+ movesRef.current = 0;
1528
+ setBoard(shuffled);
1529
+ setMoves(0);
1530
+ setSolved(false);
1531
+ };
1532
+ useEffect7(() => {
1533
+ doShuffle();
1534
+ }, []);
1535
+ useKeyboard2((event) => {
1536
+ if (event.name === "up") {
1537
+ doMove("down");
1538
+ event.preventDefault();
1539
+ } else if (event.name === "down") {
1540
+ doMove("up");
1541
+ event.preventDefault();
1542
+ } else if (event.name === "left") {
1543
+ doMove("right");
1544
+ event.preventDefault();
1545
+ } else if (event.name === "right") {
1546
+ doMove("left");
1547
+ event.preventDefault();
1548
+ } else if (event.key === "r") {
1549
+ doShuffle();
1550
+ event.preventDefault();
1551
+ }
1552
+ });
1553
+ const rows = useMemo7(() => {
1554
+ const result = [];
1555
+ for (let r = 0; r < ROWS; r++) {
1556
+ result.push(board.slice(r * COLS, r * COLS + COLS));
1557
+ }
1558
+ return result;
1559
+ }, [board]);
1560
+ return /* @__PURE__ */ jsxs19("box", { flexDirection: "column", flexGrow: 1, paddingX: 1, children: [
1561
+ /* @__PURE__ */ jsx26("text", { style: textStyle({ dim: true, fg: theme.muted }), children: "Slide tiles to solve the puzzle" }),
1562
+ /* @__PURE__ */ jsx26("box", { flexDirection: "column", children: rows.map((row, rowIdx) => /* @__PURE__ */ jsx26("box", { flexDirection: "row", children: row.map((tile, colIdx) => {
1563
+ const idx = rowIdx * COLS + colIdx;
1564
+ if (tile === 0) {
1565
+ return /* @__PURE__ */ jsx26("box", { width: TILE_WIDTH, height: TILE_HEIGHT }, idx);
1566
+ }
1567
+ const color = tileColors[tile - 1];
1568
+ return /* @__PURE__ */ jsx26(
1569
+ "box",
1570
+ {
1571
+ width: TILE_WIDTH,
1572
+ height: TILE_HEIGHT,
1573
+ border: true,
1574
+ borderStyle: "rounded",
1575
+ borderColor: solved ? theme.success : color,
1576
+ onMouseDown: () => {
1577
+ if (isAdjacentToEmpty(boardRef.current, idx)) {
1578
+ const emptyIdx = getEmptyIndex(boardRef.current);
1579
+ const next = swap(boardRef.current, emptyIdx, idx);
1580
+ boardRef.current = next;
1581
+ movesRef.current++;
1582
+ setBoard(next);
1583
+ setMoves(movesRef.current);
1584
+ setSolved(isSolved(next));
1585
+ }
1586
+ },
1587
+ children: /* @__PURE__ */ jsx26("text", { style: textStyle({
1588
+ fg: solved ? theme.success : color,
1589
+ bold: true
1590
+ }), children: String(tile).padStart(2) })
1591
+ },
1592
+ idx
1593
+ );
1594
+ }) }, rowIdx)) }),
1595
+ /* @__PURE__ */ jsx26("box", { height: 1 }),
1596
+ /* @__PURE__ */ jsxs19("box", { flexDirection: "row", gap: 2, children: [
1597
+ /* @__PURE__ */ jsxs19("text", { style: textStyle({ dim: true, fg: theme.muted }), children: [
1598
+ "Moves: ",
1599
+ moves
1600
+ ] }),
1601
+ solved && moves > 0 && /* @__PURE__ */ jsx26("text", { style: textStyle({ fg: theme.success, bold: true }), children: "Solved!" })
1602
+ ] }),
1603
+ /* @__PURE__ */ jsx26("box", { flexGrow: 1 }),
1604
+ /* @__PURE__ */ jsx26("box", { paddingX: 1, children: /* @__PURE__ */ jsx26(StatusBar, { items: [
1605
+ { key: "\u2191\u2193\u2190\u2192", label: "slide" },
1606
+ { key: "click", label: "slide tile" },
1607
+ { key: "r", label: "shuffle" }
1608
+ ] }) })
1609
+ ] });
1610
+ }
1611
+
1612
+ // src/landing/landing-app.tsx
1613
+ import { Fragment as Fragment7, jsx as jsx27, jsxs as jsxs20 } from "react/jsx-runtime";
1614
+ var DEMOS = ["ripple", "puzzle"];
1615
+ var TAB_HEIGHT = 2;
1616
+ var TAB_WIDTHS = DEMOS.map((n) => n.length + 4);
1617
+ var TAB_POSITIONS = [];
1618
+ var _pos = 0;
1619
+ for (const w of TAB_WIDTHS) {
1620
+ TAB_POSITIONS.push(_pos);
1621
+ _pos += w;
1622
+ }
1623
+ function LandingApp({ useKeyboard: useKeyboard3 }) {
1108
1624
  const theme = useTheme();
1109
1625
  const { width, height, isNarrow, isTiny, isMobile } = useBreakpoints();
1110
- const isBrowser = typeof document !== "undefined";
1111
- const { clearRect, installLinksClearRect } = useMemo7(() => {
1112
- const logoHeight = isTiny ? 2 : isNarrow ? 13 : 7;
1113
- const logoExtra = isBrowser ? 1 : 0;
1626
+ const [activeIndex, setActiveIndex] = useState12(0);
1627
+ const mousePosRef = useRef9(null);
1628
+ const matrixRipplesRef = useRef9([]);
1629
+ useKeyboard3((event) => {
1630
+ if (event.name === "tab") {
1631
+ setActiveIndex((prev) => (prev + 1) % DEMOS.length);
1632
+ event.preventDefault();
1633
+ }
1634
+ });
1635
+ const isBrowser2 = typeof document !== "undefined";
1636
+ const { clearRect, installLinksClearRect, boxTop } = useMemo8(() => {
1637
+ const logoHeight = isTiny ? 2 : isMobile ? 7 : isNarrow ? 13 : 7;
1638
+ const logoExtra = isBrowser2 ? 1 : 0;
1114
1639
  const gap = isMobile ? 0 : 1;
1115
- const installLinksTop = 3 + logoHeight + logoExtra + gap;
1640
+ const paddingTop = isMobile ? 1 : 3;
1641
+ const installLinksTop = paddingTop + logoHeight + logoExtra + gap;
1116
1642
  const installLinksHeight = 3;
1117
- const boxTop = installLinksTop + installLinksHeight + gap + 1;
1118
- const bh = height - boxTop - 1;
1643
+ const boxTop2 = installLinksTop + installLinksHeight + gap + 1;
1644
+ const bh = height - boxTop2 - 1;
1645
+ const bw = Math.min(82, width - 2);
1646
+ const bl = Math.floor((width - bw) / 2);
1119
1647
  return {
1120
- clearRect: { top: boxTop, left: 1, width: width - 2, height: bh },
1121
- installLinksClearRect: { top: installLinksTop, left: 1, width: width - 2, height: installLinksHeight }
1648
+ clearRect: { top: boxTop2, left: bl, width: bw, height: bh },
1649
+ installLinksClearRect: { top: installLinksTop, left: 1, width: width - 2, height: installLinksHeight },
1650
+ boxTop: boxTop2
1122
1651
  };
1123
- }, [width, height, isTiny, isNarrow, isMobile, isBrowser]);
1124
- return /* @__PURE__ */ jsxs18("box", { width: "100%", height: "100%", position: "relative", children: [
1125
- /* @__PURE__ */ jsx25(MatrixBackground, { width, height, clearRect, clearRects: isBrowser ? void 0 : [installLinksClearRect] }),
1126
- /* @__PURE__ */ jsx25("box", { position: "absolute", top: 0, left: 0, width, height, zIndex: 1, flexDirection: "column", shouldFill: false, children: /* @__PURE__ */ jsxs18("box", { flexGrow: 1, flexDirection: "column", paddingTop: 3, paddingLeft: 1, paddingRight: 1, paddingBottom: 1, gap: isMobile ? 0 : 1, shouldFill: false, children: [
1127
- /* @__PURE__ */ jsx25("box", { flexShrink: 0, shouldFill: false, children: /* @__PURE__ */ jsx25(Logo, { compact: isTiny, narrow: isNarrow, mobile: isMobile }) }),
1128
- /* @__PURE__ */ jsxs18("box", { flexDirection: "row", flexWrap: "wrap", justifyContent: "center", gap: isMobile ? 0 : 1, flexShrink: 0, shouldFill: false, children: [
1129
- /* @__PURE__ */ jsx25("box", { border: true, borderStyle: "rounded", borderColor: theme.border, paddingX: 1, flexDirection: "column", flexShrink: 0, children: /* @__PURE__ */ jsxs18("text", { children: [
1130
- /* @__PURE__ */ jsx25("span", { style: textStyle({ dim: true }), children: "$ " }),
1131
- /* @__PURE__ */ jsx25("span", { style: textStyle({ bold: true }), children: "bunx " }),
1132
- /* @__PURE__ */ jsx25("span", { style: textStyle({ fg: theme.accent }), children: "@gridland/demo landing" })
1133
- ] }) }),
1134
- /* @__PURE__ */ jsx25(InstallBox, {}),
1135
- /* @__PURE__ */ jsx25(LinksBox, {})
1136
- ] }),
1137
- /* @__PURE__ */ jsx25("box", { flexGrow: 1, border: true, borderStyle: "rounded", borderColor: theme.border, flexDirection: "column", overflow: "hidden" })
1138
- ] }) })
1652
+ }, [width, height, isTiny, isNarrow, isMobile, isBrowser2]);
1653
+ const MAX_BOX_WIDTH = 82;
1654
+ const availableWidth = width - 2;
1655
+ const boxWidth = Math.min(MAX_BOX_WIDTH, availableWidth);
1656
+ const boxLeft = Math.floor((width - boxWidth) / 2);
1657
+ const mouseOffset = useMemo8(() => ({
1658
+ x: boxLeft + 1,
1659
+ // box left edge + border(1)
1660
+ y: boxTop + TAB_HEIGHT + 1
1661
+ // boxTop + tab rows + box top border(1)
1662
+ }), [boxTop, boxLeft]);
1663
+ const containerWidth = boxWidth - 2;
1664
+ const maxBoxHeight = 20;
1665
+ const containerHeight = Math.min(height - boxTop - 1 - TAB_HEIGHT, maxBoxHeight - TAB_HEIGHT);
1666
+ const activeStart = TAB_POSITIONS[activeIndex];
1667
+ const activeWidth = TAB_WIDTHS[activeIndex];
1668
+ const activeEnd = activeStart + activeWidth;
1669
+ const connectParts = [];
1670
+ if (activeStart === 0) {
1671
+ connectParts.push(/* @__PURE__ */ jsx27("span", { style: textStyle({ fg: theme.border }), children: "\u2502" }, "cl"));
1672
+ connectParts.push(/* @__PURE__ */ jsx27("span", { style: textStyle({ fg: theme.border }), children: " ".repeat(activeWidth - 2) }, "gap"));
1673
+ connectParts.push(/* @__PURE__ */ jsx27("span", { style: textStyle({ fg: theme.border }), children: "\u2570" }, "cr"));
1674
+ } else {
1675
+ connectParts.push(
1676
+ /* @__PURE__ */ jsx27("span", { style: textStyle({ fg: theme.border }), children: "\u256D" + "\u2500".repeat(activeStart - 1) }, "left")
1677
+ );
1678
+ connectParts.push(/* @__PURE__ */ jsx27("span", { style: textStyle({ fg: theme.border }), children: "\u256F" }, "cl"));
1679
+ connectParts.push(/* @__PURE__ */ jsx27("span", { style: textStyle({ fg: theme.border }), children: " ".repeat(activeWidth - 2) }, "gap"));
1680
+ connectParts.push(/* @__PURE__ */ jsx27("span", { style: textStyle({ fg: theme.border }), children: "\u2570" }, "cr"));
1681
+ }
1682
+ const rightFill = boxWidth - activeEnd - 1;
1683
+ if (rightFill > 0) {
1684
+ connectParts.push(
1685
+ /* @__PURE__ */ jsx27("span", { style: textStyle({ fg: theme.border }), children: "\u2500".repeat(rightFill) }, "right")
1686
+ );
1687
+ }
1688
+ connectParts.push(/* @__PURE__ */ jsx27("span", { style: textStyle({ fg: theme.border }), children: "\u256E" }, "corner-r"));
1689
+ return /* @__PURE__ */ jsxs20("box", { width: "100%", height: "100%", position: "relative", children: [
1690
+ /* @__PURE__ */ jsx27(MatrixBackground, { width, height, clearRect, clearRects: isBrowser2 ? void 0 : [installLinksClearRect], mousePosRef, ripplesRef: matrixRipplesRef }),
1691
+ /* @__PURE__ */ jsx27(
1692
+ "box",
1693
+ {
1694
+ position: "absolute",
1695
+ top: 0,
1696
+ left: 0,
1697
+ width,
1698
+ height,
1699
+ zIndex: 1,
1700
+ flexDirection: "column",
1701
+ shouldFill: false,
1702
+ onMouseMove: (e) => {
1703
+ mousePosRef.current = { x: e.x, y: e.y };
1704
+ },
1705
+ onMouseDown: (e) => {
1706
+ matrixRipplesRef.current = [
1707
+ ...matrixRipplesRef.current,
1708
+ { x: e.x, y: e.y, createdAt: Date.now() }
1709
+ ];
1710
+ },
1711
+ children: /* @__PURE__ */ jsxs20("box", { flexGrow: 1, flexDirection: "column", paddingTop: isMobile ? 1 : 3, paddingLeft: 1, paddingRight: 1, paddingBottom: 1, gap: isMobile ? 0 : 1, shouldFill: false, children: [
1712
+ /* @__PURE__ */ jsx27("box", { flexShrink: 0, shouldFill: false, children: /* @__PURE__ */ jsx27(Logo, { compact: isTiny, narrow: isNarrow, mobile: isMobile }) }),
1713
+ /* @__PURE__ */ jsxs20("box", { flexDirection: "row", flexWrap: "wrap", justifyContent: "center", gap: isMobile ? 0 : 1, flexShrink: 0, shouldFill: false, children: [
1714
+ !isMobile && /* @__PURE__ */ jsx27("box", { border: true, borderStyle: "rounded", borderColor: theme.border, paddingX: 1, flexDirection: "column", flexShrink: 0, children: /* @__PURE__ */ jsxs20("text", { children: [
1715
+ /* @__PURE__ */ jsx27("span", { style: textStyle({ dim: true }), children: "$ " }),
1716
+ /* @__PURE__ */ jsx27("span", { style: textStyle({ bold: true }), children: "bunx " }),
1717
+ /* @__PURE__ */ jsx27("span", { style: textStyle({ fg: theme.accent }), children: "@gridland/demo landing" })
1718
+ ] }) }),
1719
+ !isMobile && /* @__PURE__ */ jsx27(InstallBox, {}),
1720
+ /* @__PURE__ */ jsx27(LinksBox, {})
1721
+ ] }),
1722
+ /* @__PURE__ */ jsxs20("box", { flexDirection: "column", width: boxWidth, maxWidth: MAX_BOX_WIDTH, maxHeight: 20, alignSelf: "center", flexGrow: 1, children: [
1723
+ /* @__PURE__ */ jsx27("box", { height: 1, flexShrink: 0, flexDirection: "row", shouldFill: false, children: DEMOS.map((name, i) => {
1724
+ const isActive = i === activeIndex;
1725
+ const w = TAB_WIDTHS[i];
1726
+ return /* @__PURE__ */ jsx27("box", { width: w, onMouseDown: () => setActiveIndex(i), children: /* @__PURE__ */ jsx27("text", { style: textStyle({ fg: theme.border }), children: isActive ? "\u256D" + "\u2500".repeat(w - 2) + "\u256E" : " ".repeat(w) }) }, name);
1727
+ }) }),
1728
+ /* @__PURE__ */ jsx27("box", { height: 1, flexShrink: 0, flexDirection: "row", shouldFill: false, children: DEMOS.map((name, i) => {
1729
+ const isActive = i === activeIndex;
1730
+ const w = TAB_WIDTHS[i];
1731
+ return /* @__PURE__ */ jsx27("box", { width: w, onMouseDown: () => setActiveIndex(i), children: /* @__PURE__ */ jsx27("text", { children: isActive ? /* @__PURE__ */ jsxs20(Fragment7, { children: [
1732
+ /* @__PURE__ */ jsx27("span", { style: textStyle({ fg: theme.border }), children: "\u2502" }),
1733
+ /* @__PURE__ */ jsx27("span", { style: textStyle({ bold: true, fg: theme.foreground }), children: ` ${name} ` }),
1734
+ /* @__PURE__ */ jsx27("span", { style: textStyle({ fg: theme.border }), children: "\u2502" })
1735
+ ] }) : /* @__PURE__ */ jsx27("span", { style: textStyle({ fg: theme.muted }), children: ` ${name} ` }) }) }, name);
1736
+ }) }),
1737
+ /* @__PURE__ */ jsxs20("box", { position: "relative", flexGrow: 1, children: [
1738
+ /* @__PURE__ */ jsx27("box", { position: "absolute", top: 0, left: 0, width: boxWidth, height: 1, zIndex: 2, children: /* @__PURE__ */ jsx27("text", { children: connectParts }) }),
1739
+ /* @__PURE__ */ jsxs20("box", { border: true, borderStyle: "rounded", borderColor: theme.border, flexGrow: 1, flexDirection: "column", overflow: "hidden", children: [
1740
+ activeIndex === 0 && /* @__PURE__ */ jsx27(RippleApp, { mouseOffset, containerWidth, containerHeight }),
1741
+ activeIndex === 1 && /* @__PURE__ */ jsx27(PuzzleApp, { containerWidth, containerHeight })
1742
+ ] })
1743
+ ] }),
1744
+ /* @__PURE__ */ jsx27("box", { height: 1 }),
1745
+ /* @__PURE__ */ jsx27("box", { width: "100%", alignItems: "center", flexDirection: "column", shouldFill: false, children: /* @__PURE__ */ jsxs20("text", { style: textStyle({ dim: true, fg: theme.muted }), children: [
1746
+ "Made with \u2764\uFE0F by ",
1747
+ /* @__PURE__ */ jsx27("a", { href: "https://cjroth.com", style: { attributes: 1 << 3, fg: theme.muted }, children: "Chris Roth" }),
1748
+ " + ",
1749
+ /* @__PURE__ */ jsx27("a", { href: "https://jessicacheng.studio", style: { attributes: 1 << 3, fg: theme.muted }, children: "Jessica Cheng" })
1750
+ ] }) })
1751
+ ] })
1752
+ ] })
1753
+ }
1754
+ )
1139
1755
  ] });
1140
1756
  }
1141
1757
 
1142
1758
  // src/landing/matrix-rain.tsx
1143
- import { jsx as jsx26 } from "react/jsx-runtime";
1759
+ import { jsx as jsx28 } from "react/jsx-runtime";
1144
1760
 
1145
1761
  // src/landing/about-modal.tsx
1146
- import { jsx as jsx27, jsxs as jsxs19 } from "react/jsx-runtime";
1762
+ import { jsx as jsx29, jsxs as jsxs21 } from "react/jsx-runtime";
1147
1763
  export {
1148
1764
  LandingApp
1149
1765
  };