@allshift/ui 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -36,11 +36,14 @@ __export(index_exports, {
36
36
  CanvasProvider: () => CanvasProvider,
37
37
  CanvasTransition: () => CanvasTransition,
38
38
  DEFAULT_H: () => DEFAULT_H,
39
+ DUO_FOCUS_SHARE: () => DUO_FOCUS_SHARE,
39
40
  GAP: () => GAP,
40
41
  GenerativeView: () => GenerativeView,
41
42
  HolographicShine: () => HolographicShine,
42
43
  LiveCard: () => LiveCard,
43
44
  LiveStat: () => LiveStat,
45
+ MAX_WINDOWS: () => MAX_WINDOWS,
46
+ MAX_WINDOWS_PHONE: () => MAX_WINDOWS_PHONE,
44
47
  MIN_H: () => MIN_H,
45
48
  MIN_W: () => MIN_W,
46
49
  PageTransition: () => PageTransition,
@@ -49,6 +52,8 @@ __export(index_exports, {
49
52
  ShiftCard: () => ShiftCard,
50
53
  TiltCard: () => TiltCard,
51
54
  WIDTH_SPAN: () => WIDTH_SPAN,
55
+ detectMode: () => detectMode,
56
+ maxWindowsFor: () => maxWindowsFor,
52
57
  tallH: () => tallH,
53
58
  useCanvas: () => useCanvas,
54
59
  useGenerate: () => useGenerate,
@@ -824,7 +829,7 @@ function PageTransition({ children, className }) {
824
829
  import_framer_motion11.motion.div,
825
830
  {
826
831
  initial: { opacity: 0, x: dir * 22, filter: "blur(3px)" },
827
- animate: { opacity: 1, x: 0, filter: "blur(0px)" },
832
+ animate: { opacity: 1, x: 0, filter: "blur(0px)", transitionEnd: { filter: "none" } },
828
833
  transition: { type: "spring", stiffness: 400, damping: 34 },
829
834
  className: className ?? "flex flex-col flex-1 min-h-0",
830
835
  children
@@ -835,6 +840,59 @@ function PageTransition({ children, className }) {
835
840
 
836
841
  // src/canvas/CanvasProvider.tsx
837
842
  var import_react10 = require("react");
843
+
844
+ // src/canvas/duo.ts
845
+ var MAX_WINDOWS = 4;
846
+ var MAX_WINDOWS_PHONE = 2;
847
+ var DUO_FOCUS_SHARE = 0.62;
848
+ var PHONE_CHROME = 220;
849
+ var DUO_GAP = 12;
850
+ var DUO_MIN_H = 160;
851
+ function detectMode(innerWidth, innerHeight, coarsePointer) {
852
+ if (innerWidth < 720) return "phone";
853
+ if (coarsePointer && Math.min(innerWidth, innerHeight) < 600) return "phone";
854
+ return "wide";
855
+ }
856
+ function maxWindowsFor(mode, limits) {
857
+ const n = mode === "phone" ? limits?.phone ?? MAX_WINDOWS_PHONE : limits?.wide ?? MAX_WINDOWS;
858
+ return Math.max(1, Math.floor(n));
859
+ }
860
+ function capList(list, max, keepId) {
861
+ let out = list;
862
+ while (out.length > max) {
863
+ const victim = out.filter((b) => !b.locked && b.id !== keepId).sort((a, b) => a.z - b.z)[0];
864
+ if (!victim) break;
865
+ out = out.filter((b) => b.id !== victim.id);
866
+ }
867
+ return out;
868
+ }
869
+ function tileDuo(items, boardW, boardH) {
870
+ const n = items.length;
871
+ if (n === 0) return items;
872
+ const sorted = [...items].sort((a, b) => b.z - a.z);
873
+ const availH = Math.max(DUO_MIN_H * 2 + DUO_GAP, boardH - PHONE_CHROME);
874
+ if (n === 1) return [{ ...sorted[0], x: 0, y: 0, w: boardW, h: availH, placed: false }];
875
+ const minRest = (DUO_MIN_H + DUO_GAP) * (n - 1);
876
+ const focusH = Math.max(DUO_MIN_H, Math.min(Math.round(availH * DUO_FOCUS_SHARE), availH - minRest));
877
+ const restH = Math.max(DUO_MIN_H, Math.round((availH - focusH - DUO_GAP * (n - 1)) / (n - 1)));
878
+ let y = 0;
879
+ return sorted.map((it, i) => {
880
+ const h = i === 0 ? focusH : restH;
881
+ const out = { ...it, x: 0, y, w: boardW, h, placed: false };
882
+ y += h + DUO_GAP;
883
+ return out;
884
+ });
885
+ }
886
+ function sameGeometry(a, b) {
887
+ if (a.length !== b.length) return false;
888
+ const byId = new Map(b.map((x) => [x.id, x]));
889
+ return a.every((x) => {
890
+ const y = byId.get(x.id);
891
+ return !!y && x.x === y.x && x.y === y.y && x.w === y.w && x.h === y.h;
892
+ });
893
+ }
894
+
895
+ // src/canvas/CanvasProvider.tsx
838
896
  var import_jsx_runtime11 = require("react/jsx-runtime");
839
897
  var GAP = 20;
840
898
  var DEFAULT_H = 420;
@@ -983,9 +1041,10 @@ function computeSnap(blocks, id, layout, boardW, H) {
983
1041
  }
984
1042
  return placements;
985
1043
  }
986
- function tileItems(items, boardW, boardH) {
1044
+ function tileItems(items, boardW, boardH, mode = "wide") {
987
1045
  const n = items.length;
988
1046
  if (n === 0) return items;
1047
+ if (mode === "phone") return tileDuo(items, boardW, boardH);
989
1048
  const availH = Math.max(MIN_H * 2 + GAP, boardH - 150);
990
1049
  const rows = Math.ceil(n / 2);
991
1050
  const rowH = Math.max(300, Math.round((availH - GAP * (rows - 1)) / rows));
@@ -1038,7 +1097,8 @@ function CanvasProvider({
1038
1097
  initialLayout,
1039
1098
  addKey = null,
1040
1099
  persist = false,
1041
- savedSession
1100
+ savedSession,
1101
+ limits
1042
1102
  }) {
1043
1103
  const resolveRef = (0, import_react10.useRef)(resolveBlock);
1044
1104
  (0, import_react10.useEffect)(() => {
@@ -1046,14 +1106,22 @@ function CanvasProvider({
1046
1106
  }, [resolveBlock]);
1047
1107
  const resolve = (key) => resolveRef.current(key);
1048
1108
  const initial = (0, import_react10.useMemo)(() => {
1049
- let z = 1;
1050
- const base = initialLayout ? initialLayout.filter((b) => b && typeof b.key === "string" && resolveRef.current(b.key)).map((b) => blockFromSaved(b, z++)) : initialKeys.filter((k) => resolveRef.current(k)).map((k) => blockFromSaved(k, z++));
1051
- if (addKey && resolveRef.current(addKey) && !base.some((b) => b.key === addKey)) {
1052
- base.unshift(blockFromSaved(addKey, z++));
1109
+ const max = maxWindowsFor("wide", limits);
1110
+ const entries = initialLayout ? initialLayout.filter((b) => b && typeof b.key === "string" && resolveRef.current(b.key)) : initialKeys.filter((k) => resolveRef.current(k));
1111
+ if (addKey && resolveRef.current(addKey) && !entries.some((b) => (typeof b === "string" ? b : b.key) === addKey)) {
1112
+ entries.unshift(addKey);
1053
1113
  }
1114
+ const kept = entries.slice(0, max);
1115
+ let z = 1;
1116
+ const base = kept.map(() => null);
1117
+ for (let i = kept.length - 1; i >= 0; i--) base[i] = blockFromSaved(kept[i], z++);
1054
1118
  return { base, nextZ: z };
1055
1119
  }, []);
1056
1120
  const zRef = (0, import_react10.useRef)(initial.nextZ);
1121
+ const limitsRef = (0, import_react10.useRef)(limits);
1122
+ (0, import_react10.useEffect)(() => {
1123
+ limitsRef.current = limits;
1124
+ }, [limits]);
1057
1125
  const [blocks, setBlocks] = (0, import_react10.useState)(initial.base);
1058
1126
  const [interacting, setInteracting] = (0, import_react10.useState)(false);
1059
1127
  const [activeId, setActiveId] = (0, import_react10.useState)(null);
@@ -1061,6 +1129,9 @@ function CanvasProvider({
1061
1129
  const [boardWidth, setBoardWidth] = (0, import_react10.useState)(1280);
1062
1130
  const boardRef = (0, import_react10.useRef)(1280);
1063
1131
  const boardHRef = (0, import_react10.useRef)(820);
1132
+ const [mode, setMode] = (0, import_react10.useState)("wide");
1133
+ const modeRef = (0, import_react10.useRef)("wide");
1134
+ const maxFor = (m) => maxWindowsFor(m, limitsRef.current);
1064
1135
  const persistRef = (0, import_react10.useRef)(persist);
1065
1136
  (0, import_react10.useEffect)(() => {
1066
1137
  persistRef.current = persist;
@@ -1069,12 +1140,26 @@ function CanvasProvider({
1069
1140
  const hadUserChangeRef = (0, import_react10.useRef)(false);
1070
1141
  (0, import_react10.useEffect)(() => {
1071
1142
  const measure = () => {
1072
- const w = Math.max(360, window.innerWidth - 40);
1143
+ const vw = Math.min(window.innerWidth, window.visualViewport?.width || Infinity);
1144
+ const w = Math.max(320, vw - 40);
1073
1145
  boardRef.current = w;
1074
1146
  boardHRef.current = Math.max(480, window.innerHeight);
1075
1147
  setBoardWidth(w);
1148
+ const coarse = !!window.matchMedia?.("(pointer: coarse)").matches;
1149
+ const m = detectMode(vw, window.innerHeight, coarse);
1150
+ const modeChanged = m !== modeRef.current;
1151
+ modeRef.current = m;
1152
+ setMode(m);
1076
1153
  setBlocks((prev) => {
1077
- const next = fitToBoard(prev, w, boardHRef.current);
1154
+ let next;
1155
+ if (m === "phone") {
1156
+ next = tileDuo(capList(prev, maxFor("phone")), w, boardHRef.current);
1157
+ if (sameGeometry(prev, next)) return prev;
1158
+ } else if (modeChanged) {
1159
+ next = tileItems(capList(prev, maxFor("wide")), w, boardHRef.current);
1160
+ } else {
1161
+ next = fitToBoard(prev, w, boardHRef.current);
1162
+ }
1078
1163
  if (next !== prev) fitAdjustRef.current = true;
1079
1164
  return next;
1080
1165
  });
@@ -1084,22 +1169,23 @@ function CanvasProvider({
1084
1169
  return () => window.removeEventListener("resize", measure);
1085
1170
  }, []);
1086
1171
  const layoutDefault = (0, import_react10.useCallback)(
1087
- (list) => tileItems(list, boardRef.current, boardHRef.current),
1172
+ (list) => tileItems(list, boardRef.current, boardHRef.current, modeRef.current),
1088
1173
  []
1089
1174
  );
1175
+ const customizedIn = (list) => modeRef.current !== "phone" && list.some((b) => b.placed);
1090
1176
  const tiledOnce = (0, import_react10.useRef)(false);
1091
1177
  (0, import_react10.useEffect)(() => {
1092
1178
  if (tiledOnce.current) return;
1093
1179
  tiledOnce.current = true;
1094
- setBlocks((prev) => prev.length === 0 || prev.some((b) => b.placed) ? prev : layoutDefault(prev));
1180
+ setBlocks((prev) => prev.length === 0 || customizedIn(prev) ? prev : layoutDefault(prev));
1095
1181
  }, [boardWidth]);
1096
1182
  const addBlock = (0, import_react10.useCallback)((key, context, width, append = false) => {
1097
1183
  if (!resolve(key)) return;
1098
1184
  const w = width ?? resolve(key)?.defaultWidth;
1099
1185
  setBlocks((prev) => {
1100
1186
  const existing = prev.find((b) => b.key === key);
1101
- const customized = prev.some((b) => b.placed);
1102
- const without = prev.filter((b) => b.key !== key);
1187
+ const customized = customizedIn(prev);
1188
+ const without = capList(prev.filter((b) => b.key !== key), maxFor(modeRef.current) - 1);
1103
1189
  if (customized) {
1104
1190
  if (append) {
1105
1191
  const startY = without.length ? Math.max(...without.map((b) => b.y + b.h)) + GAP : 0;
@@ -1139,13 +1225,15 @@ function CanvasProvider({
1139
1225
  const removeBlock = (0, import_react10.useCallback)((id) => {
1140
1226
  setBlocks((prev) => {
1141
1227
  const remaining = prev.filter((b) => b.id !== id);
1142
- return remaining.length > 0 && !remaining.some((b) => b.placed) ? layoutDefault(remaining) : remaining;
1228
+ return remaining.length > 0 && !customizedIn(remaining) ? layoutDefault(remaining) : remaining;
1143
1229
  });
1144
1230
  }, [layoutDefault]);
1145
1231
  const resizeBlock = (0, import_react10.useCallback)((id, width) => {
1232
+ if (modeRef.current === "phone") return;
1146
1233
  setBlocks((prev) => prev.map((b) => b.id === id ? { ...b, width, w: pxWidth(WIDTH_SPAN[width], boardRef.current), placed: true, z: zRef.current++ } : b));
1147
1234
  }, []);
1148
1235
  const moveTo = (0, import_react10.useCallback)((id, x, y) => {
1236
+ if (modeRef.current === "phone") return;
1149
1237
  const boardW = boardRef.current;
1150
1238
  setBlocks((prev) => prev.map((b) => {
1151
1239
  if (b.id !== id || b.locked) return b;
@@ -1155,6 +1243,7 @@ function CanvasProvider({
1155
1243
  }));
1156
1244
  }, []);
1157
1245
  const sizeTo = (0, import_react10.useCallback)((id, w, h) => {
1246
+ if (modeRef.current === "phone") return;
1158
1247
  const boardW = boardRef.current;
1159
1248
  setBlocks((prev) => prev.map((b) => {
1160
1249
  if (b.id !== id || b.locked) return b;
@@ -1164,6 +1253,7 @@ function CanvasProvider({
1164
1253
  }));
1165
1254
  }, []);
1166
1255
  const setRect = (0, import_react10.useCallback)((id, r) => {
1256
+ if (modeRef.current === "phone") return;
1167
1257
  const boardW = boardRef.current;
1168
1258
  setBlocks((prev) => prev.map((b) => {
1169
1259
  if (b.id !== id || b.locked) return b;
@@ -1176,6 +1266,7 @@ function CanvasProvider({
1176
1266
  }));
1177
1267
  }, []);
1178
1268
  const snapTo = (0, import_react10.useCallback)((id, layout) => {
1269
+ if (modeRef.current === "phone") return;
1179
1270
  const boardW = boardRef.current;
1180
1271
  const H = tallH(boardHRef.current);
1181
1272
  setSnapGhost(null);
@@ -1223,6 +1314,7 @@ function CanvasProvider({
1223
1314
  }, [blocks]);
1224
1315
  const clearSnapPreview = (0, import_react10.useCallback)(() => setSnapGhost(null), []);
1225
1316
  const resizeRow = (0, import_react10.useCallback)((id, x, w, h, origin) => {
1317
+ if (modeRef.current === "phone") return;
1226
1318
  const boardW = boardRef.current;
1227
1319
  const M0 = origin.find((b) => b.id === id);
1228
1320
  if (!M0 || M0.locked) return;
@@ -1272,9 +1364,14 @@ function CanvasProvider({
1272
1364
  }));
1273
1365
  }, []);
1274
1366
  const bringToFront = (0, import_react10.useCallback)((id) => {
1275
- setBlocks((prev) => prev.map((b) => b.id === id ? { ...b, z: zRef.current++ } : b));
1276
- }, []);
1367
+ setBlocks((prev) => {
1368
+ if (prev.find((b) => b.id === id)?.z === zRef.current - 1) return prev;
1369
+ const next = prev.map((b) => b.id === id ? { ...b, z: zRef.current++ } : b);
1370
+ return modeRef.current === "phone" ? layoutDefault(next) : next;
1371
+ });
1372
+ }, [layoutDefault]);
1277
1373
  const resolveCollisions = (0, import_react10.useCallback)((id) => {
1374
+ if (modeRef.current === "phone") return;
1278
1375
  setBlocks((prev) => {
1279
1376
  const moved = prev.find((b) => b.id === id);
1280
1377
  if (!moved) return prev;
@@ -1317,8 +1414,10 @@ function CanvasProvider({
1317
1414
  if (!saved || saved.length === 0) return;
1318
1415
  restoredRef.current = true;
1319
1416
  setCanRestore(false);
1320
- const built = saved.filter((b) => b && typeof b.key === "string" && resolve(b.key)).map((b) => blockFromSaved(b, zRef.current++));
1321
- const arranged = built.some((b) => b.placed) ? built : layoutDefault(built);
1417
+ const kept = saved.filter((b) => b && typeof b.key === "string" && resolve(b.key)).slice(0, maxFor(modeRef.current));
1418
+ const built = kept.map(() => null);
1419
+ for (let i = kept.length - 1; i >= 0; i--) built[i] = blockFromSaved(kept[i], zRef.current++);
1420
+ const arranged = customizedIn(built) ? built : layoutDefault(built);
1322
1421
  const fitted = fitToBoard(arranged, boardRef.current, boardHRef.current);
1323
1422
  if (fitted !== arranged) fitAdjustRef.current = true;
1324
1423
  setBlocks(fitted);
@@ -1341,7 +1440,7 @@ function CanvasProvider({
1341
1440
  const width = d.width;
1342
1441
  setBlocks((prev) => {
1343
1442
  const list = prev.map((b) => b.key === d.key ? { ...b, width } : b);
1344
- return prev.some((b) => b.placed) ? list : layoutDefault(list);
1443
+ return customizedIn(prev) ? list : layoutDefault(list);
1345
1444
  });
1346
1445
  }
1347
1446
  function onPlace(e) {
@@ -1355,7 +1454,7 @@ function CanvasProvider({
1355
1454
  const [moved] = next.splice(srcIdx, 1);
1356
1455
  const insertAt = next.findIndex((b) => b.key === d.afterKey) + 1;
1357
1456
  next.splice(insertAt, 0, moved);
1358
- return prev.some((b) => b.placed) ? next : layoutDefault(next);
1457
+ return customizedIn(prev) ? next : layoutDefault(next);
1359
1458
  });
1360
1459
  }
1361
1460
  function onSwap(e) {
@@ -1363,7 +1462,7 @@ function CanvasProvider({
1363
1462
  const keys = d?.keys ?? [];
1364
1463
  const contexts = d?.contexts ?? [];
1365
1464
  const widths = d?.widths ?? [];
1366
- const built = keys.filter((k) => resolve(k)).map((key, i) => ({
1465
+ const built = keys.filter((k) => resolve(k)).slice(0, maxFor(modeRef.current)).map((key, i) => ({
1367
1466
  id: nextId(),
1368
1467
  key,
1369
1468
  context: contexts[i],
@@ -1420,12 +1519,13 @@ function CanvasProvider({
1420
1519
  }, [blocks.length, canRestore]);
1421
1520
  const firstRun = (0, import_react10.useRef)(true);
1422
1521
  const saveTimer = (0, import_react10.useRef)(null);
1423
- const layoutSignature = blocks.map((b) => `${b.key}:${b.width ?? ""}:${Math.round(b.x)}:${Math.round(b.y)}:${Math.round(b.w)}:${Math.round(b.h)}`).join("|");
1522
+ const layoutSignature = blocks.map((b) => mode === "phone" ? `${b.key}:${b.width ?? ""}` : `${b.key}:${b.width ?? ""}:${Math.round(b.x)}:${Math.round(b.y)}:${Math.round(b.w)}:${Math.round(b.h)}`).join("|");
1424
1523
  const payload = (0, import_react10.useMemo)(() => layoutSignature ? {
1425
1524
  savedDay: localDay(),
1426
1525
  blocks: layoutSignature.split("|").map((seg) => {
1427
1526
  const [key, width, x, y, w, h] = seg.split(":");
1428
- return { key, ...width ? { width } : {}, x: Number(x), y: Number(y), w: Number(w), h: Number(h) };
1527
+ const base = { key, ...width ? { width } : {} };
1528
+ return x === void 0 ? base : { ...base, x: Number(x), y: Number(y), w: Number(w), h: Number(h) };
1429
1529
  })
1430
1530
  } : null, [layoutSignature]);
1431
1531
  const payloadRef = (0, import_react10.useRef)(payload);
@@ -1507,8 +1607,10 @@ function CanvasProvider({
1507
1607
  setInteracting,
1508
1608
  activeId,
1509
1609
  setActiveId,
1510
- boardWidth
1511
- }), [blocks, addBlock, removeBlock, resizeBlock, snapTo, snapPreview, previewSnap, clearSnapPreview, snapGhost, moveTo, sizeTo, setRect, resizeRow, bringToFront, resolveCollisions, toggleLock, clear, interacting, activeId, boardWidth]);
1610
+ boardWidth,
1611
+ mode,
1612
+ maxBlocks: maxWindowsFor(mode, limits)
1613
+ }), [blocks, addBlock, removeBlock, resizeBlock, snapTo, snapPreview, previewSnap, clearSnapPreview, snapGhost, moveTo, sizeTo, setRect, resizeRow, bringToFront, resolveCollisions, toggleLock, clear, interacting, activeId, boardWidth, mode, limits]);
1512
1614
  return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(CanvasContext.Provider, { value: api, children });
1513
1615
  }
1514
1616
 
@@ -1737,11 +1839,14 @@ function ShiftBar({ state, dots = 3, size = 7, className = "" }) {
1737
1839
  CanvasProvider,
1738
1840
  CanvasTransition,
1739
1841
  DEFAULT_H,
1842
+ DUO_FOCUS_SHARE,
1740
1843
  GAP,
1741
1844
  GenerativeView,
1742
1845
  HolographicShine,
1743
1846
  LiveCard,
1744
1847
  LiveStat,
1848
+ MAX_WINDOWS,
1849
+ MAX_WINDOWS_PHONE,
1745
1850
  MIN_H,
1746
1851
  MIN_W,
1747
1852
  PageTransition,
@@ -1750,6 +1855,8 @@ function ShiftBar({ state, dots = 3, size = 7, className = "" }) {
1750
1855
  ShiftCard,
1751
1856
  TiltCard,
1752
1857
  WIDTH_SPAN,
1858
+ detectMode,
1859
+ maxWindowsFor,
1753
1860
  tallH,
1754
1861
  useCanvas,
1755
1862
  useGenerate,
package/dist/index.d.cts CHANGED
@@ -263,6 +263,17 @@ interface PageTransitionProps {
263
263
  */
264
264
  declare function PageTransition({ children, className }: PageTransitionProps): react.JSX.Element;
265
265
 
266
+ declare const MAX_WINDOWS = 4;
267
+ declare const MAX_WINDOWS_PHONE = 2;
268
+ type CanvasMode = "wide" | "phone";
269
+ type CanvasLimits = {
270
+ wide?: number;
271
+ phone?: number;
272
+ };
273
+ declare const DUO_FOCUS_SHARE = 0.62;
274
+ declare function detectMode(innerWidth: number, innerHeight: number, coarsePointer: boolean): CanvasMode;
275
+ declare function maxWindowsFor(mode: CanvasMode, limits?: CanvasLimits): number;
276
+
266
277
  declare const GAP = 20;
267
278
  declare const DEFAULT_H = 420;
268
279
  declare const MIN_W = 240;
@@ -354,16 +365,19 @@ interface CanvasApi {
354
365
  activeId: string | null;
355
366
  setActiveId: (id: string | null) => void;
356
367
  boardWidth: number;
368
+ mode: CanvasMode;
369
+ maxBlocks: number;
357
370
  }
358
371
  declare function useCanvas(): CanvasApi;
359
372
  declare function tallH(boardH: number): number;
360
- declare function CanvasProvider({ children, resolveBlock, initialKeys, initialLayout, addKey, persist, savedSession, }: {
373
+ declare function CanvasProvider({ children, resolveBlock, initialKeys, initialLayout, addKey, persist, savedSession, limits, }: {
361
374
  children: ReactNode;
362
375
  resolveBlock: ResolveBlock;
363
376
  initialKeys?: string[];
364
377
  initialLayout?: SavedBlock[];
365
378
  addKey?: string | null;
366
379
  persist?: boolean;
380
+ limits?: CanvasLimits;
367
381
  savedSession?: {
368
382
  blocks?: SavedBlock[];
369
383
  savedDay?: string | null;
@@ -399,4 +413,4 @@ interface ShiftBarProps {
399
413
  }
400
414
  declare function ShiftBar({ state, dots, size, className }: ShiftBarProps): react.JSX.Element;
401
415
 
402
- export { ACCENT_RGB, type Accent, type BlockWidth, type CanvasBlock, CanvasLauncher, type CanvasLauncherProps, CanvasProvider, CanvasTransition, type CanvasTransitionProps, DEFAULT_H, GAP, type GenPhase, GenerativeView, type GenerativeViewProps, type HeyShiftConfig, type HeyShiftReturn, HolographicShine, type HolographicShineProps, type LauncherGroup, type LauncherItem, LiveCard, type LiveCardProps, LiveStat, type LiveStatProps, MIN_H, MIN_W, PageTransition, type PageTransitionProps, type ResolveBlock, type SavedBlock, ScrollGenerateCard, type ScrollGenerateCardProps, ShiftBar, type ShiftBarProps, type ShiftBarState, ShiftCard, type ShiftCardProps, type SnapBox, type SnapGhost, type SnapLayout, type SnapPreview, TiltCard, type TiltCardProps, type UseGenerateOptions, WIDTH_SPAN, tallH, useCanvas, useGenerate, useHeyShift };
416
+ export { ACCENT_RGB, type Accent, type BlockWidth, type CanvasBlock, CanvasLauncher, type CanvasLauncherProps, type CanvasLimits, type CanvasMode, CanvasProvider, CanvasTransition, type CanvasTransitionProps, DEFAULT_H, DUO_FOCUS_SHARE, GAP, type GenPhase, GenerativeView, type GenerativeViewProps, type HeyShiftConfig, type HeyShiftReturn, HolographicShine, type HolographicShineProps, type LauncherGroup, type LauncherItem, LiveCard, type LiveCardProps, LiveStat, type LiveStatProps, MAX_WINDOWS, MAX_WINDOWS_PHONE, MIN_H, MIN_W, PageTransition, type PageTransitionProps, type ResolveBlock, type SavedBlock, ScrollGenerateCard, type ScrollGenerateCardProps, ShiftBar, type ShiftBarProps, type ShiftBarState, ShiftCard, type ShiftCardProps, type SnapBox, type SnapGhost, type SnapLayout, type SnapPreview, TiltCard, type TiltCardProps, type UseGenerateOptions, WIDTH_SPAN, detectMode, maxWindowsFor, tallH, useCanvas, useGenerate, useHeyShift };
package/dist/index.d.ts CHANGED
@@ -263,6 +263,17 @@ interface PageTransitionProps {
263
263
  */
264
264
  declare function PageTransition({ children, className }: PageTransitionProps): react.JSX.Element;
265
265
 
266
+ declare const MAX_WINDOWS = 4;
267
+ declare const MAX_WINDOWS_PHONE = 2;
268
+ type CanvasMode = "wide" | "phone";
269
+ type CanvasLimits = {
270
+ wide?: number;
271
+ phone?: number;
272
+ };
273
+ declare const DUO_FOCUS_SHARE = 0.62;
274
+ declare function detectMode(innerWidth: number, innerHeight: number, coarsePointer: boolean): CanvasMode;
275
+ declare function maxWindowsFor(mode: CanvasMode, limits?: CanvasLimits): number;
276
+
266
277
  declare const GAP = 20;
267
278
  declare const DEFAULT_H = 420;
268
279
  declare const MIN_W = 240;
@@ -354,16 +365,19 @@ interface CanvasApi {
354
365
  activeId: string | null;
355
366
  setActiveId: (id: string | null) => void;
356
367
  boardWidth: number;
368
+ mode: CanvasMode;
369
+ maxBlocks: number;
357
370
  }
358
371
  declare function useCanvas(): CanvasApi;
359
372
  declare function tallH(boardH: number): number;
360
- declare function CanvasProvider({ children, resolveBlock, initialKeys, initialLayout, addKey, persist, savedSession, }: {
373
+ declare function CanvasProvider({ children, resolveBlock, initialKeys, initialLayout, addKey, persist, savedSession, limits, }: {
361
374
  children: ReactNode;
362
375
  resolveBlock: ResolveBlock;
363
376
  initialKeys?: string[];
364
377
  initialLayout?: SavedBlock[];
365
378
  addKey?: string | null;
366
379
  persist?: boolean;
380
+ limits?: CanvasLimits;
367
381
  savedSession?: {
368
382
  blocks?: SavedBlock[];
369
383
  savedDay?: string | null;
@@ -399,4 +413,4 @@ interface ShiftBarProps {
399
413
  }
400
414
  declare function ShiftBar({ state, dots, size, className }: ShiftBarProps): react.JSX.Element;
401
415
 
402
- export { ACCENT_RGB, type Accent, type BlockWidth, type CanvasBlock, CanvasLauncher, type CanvasLauncherProps, CanvasProvider, CanvasTransition, type CanvasTransitionProps, DEFAULT_H, GAP, type GenPhase, GenerativeView, type GenerativeViewProps, type HeyShiftConfig, type HeyShiftReturn, HolographicShine, type HolographicShineProps, type LauncherGroup, type LauncherItem, LiveCard, type LiveCardProps, LiveStat, type LiveStatProps, MIN_H, MIN_W, PageTransition, type PageTransitionProps, type ResolveBlock, type SavedBlock, ScrollGenerateCard, type ScrollGenerateCardProps, ShiftBar, type ShiftBarProps, type ShiftBarState, ShiftCard, type ShiftCardProps, type SnapBox, type SnapGhost, type SnapLayout, type SnapPreview, TiltCard, type TiltCardProps, type UseGenerateOptions, WIDTH_SPAN, tallH, useCanvas, useGenerate, useHeyShift };
416
+ export { ACCENT_RGB, type Accent, type BlockWidth, type CanvasBlock, CanvasLauncher, type CanvasLauncherProps, type CanvasLimits, type CanvasMode, CanvasProvider, CanvasTransition, type CanvasTransitionProps, DEFAULT_H, DUO_FOCUS_SHARE, GAP, type GenPhase, GenerativeView, type GenerativeViewProps, type HeyShiftConfig, type HeyShiftReturn, HolographicShine, type HolographicShineProps, type LauncherGroup, type LauncherItem, LiveCard, type LiveCardProps, LiveStat, type LiveStatProps, MAX_WINDOWS, MAX_WINDOWS_PHONE, MIN_H, MIN_W, PageTransition, type PageTransitionProps, type ResolveBlock, type SavedBlock, ScrollGenerateCard, type ScrollGenerateCardProps, ShiftBar, type ShiftBarProps, type ShiftBarState, ShiftCard, type ShiftCardProps, type SnapBox, type SnapGhost, type SnapLayout, type SnapPreview, TiltCard, type TiltCardProps, type UseGenerateOptions, WIDTH_SPAN, detectMode, maxWindowsFor, tallH, useCanvas, useGenerate, useHeyShift };
package/dist/index.js CHANGED
@@ -784,7 +784,7 @@ function PageTransition({ children, className }) {
784
784
  motion10.div,
785
785
  {
786
786
  initial: { opacity: 0, x: dir * 22, filter: "blur(3px)" },
787
- animate: { opacity: 1, x: 0, filter: "blur(0px)" },
787
+ animate: { opacity: 1, x: 0, filter: "blur(0px)", transitionEnd: { filter: "none" } },
788
788
  transition: { type: "spring", stiffness: 400, damping: 34 },
789
789
  className: className ?? "flex flex-col flex-1 min-h-0",
790
790
  children
@@ -795,6 +795,59 @@ function PageTransition({ children, className }) {
795
795
 
796
796
  // src/canvas/CanvasProvider.tsx
797
797
  import { createContext, useCallback, useContext, useEffect as useEffect6, useMemo as useMemo2, useRef as useRef6, useState as useState6 } from "react";
798
+
799
+ // src/canvas/duo.ts
800
+ var MAX_WINDOWS = 4;
801
+ var MAX_WINDOWS_PHONE = 2;
802
+ var DUO_FOCUS_SHARE = 0.62;
803
+ var PHONE_CHROME = 220;
804
+ var DUO_GAP = 12;
805
+ var DUO_MIN_H = 160;
806
+ function detectMode(innerWidth, innerHeight, coarsePointer) {
807
+ if (innerWidth < 720) return "phone";
808
+ if (coarsePointer && Math.min(innerWidth, innerHeight) < 600) return "phone";
809
+ return "wide";
810
+ }
811
+ function maxWindowsFor(mode, limits) {
812
+ const n = mode === "phone" ? limits?.phone ?? MAX_WINDOWS_PHONE : limits?.wide ?? MAX_WINDOWS;
813
+ return Math.max(1, Math.floor(n));
814
+ }
815
+ function capList(list, max, keepId) {
816
+ let out = list;
817
+ while (out.length > max) {
818
+ const victim = out.filter((b) => !b.locked && b.id !== keepId).sort((a, b) => a.z - b.z)[0];
819
+ if (!victim) break;
820
+ out = out.filter((b) => b.id !== victim.id);
821
+ }
822
+ return out;
823
+ }
824
+ function tileDuo(items, boardW, boardH) {
825
+ const n = items.length;
826
+ if (n === 0) return items;
827
+ const sorted = [...items].sort((a, b) => b.z - a.z);
828
+ const availH = Math.max(DUO_MIN_H * 2 + DUO_GAP, boardH - PHONE_CHROME);
829
+ if (n === 1) return [{ ...sorted[0], x: 0, y: 0, w: boardW, h: availH, placed: false }];
830
+ const minRest = (DUO_MIN_H + DUO_GAP) * (n - 1);
831
+ const focusH = Math.max(DUO_MIN_H, Math.min(Math.round(availH * DUO_FOCUS_SHARE), availH - minRest));
832
+ const restH = Math.max(DUO_MIN_H, Math.round((availH - focusH - DUO_GAP * (n - 1)) / (n - 1)));
833
+ let y = 0;
834
+ return sorted.map((it, i) => {
835
+ const h = i === 0 ? focusH : restH;
836
+ const out = { ...it, x: 0, y, w: boardW, h, placed: false };
837
+ y += h + DUO_GAP;
838
+ return out;
839
+ });
840
+ }
841
+ function sameGeometry(a, b) {
842
+ if (a.length !== b.length) return false;
843
+ const byId = new Map(b.map((x) => [x.id, x]));
844
+ return a.every((x) => {
845
+ const y = byId.get(x.id);
846
+ return !!y && x.x === y.x && x.y === y.y && x.w === y.w && x.h === y.h;
847
+ });
848
+ }
849
+
850
+ // src/canvas/CanvasProvider.tsx
798
851
  import { jsx as jsx11 } from "react/jsx-runtime";
799
852
  var GAP = 20;
800
853
  var DEFAULT_H = 420;
@@ -943,9 +996,10 @@ function computeSnap(blocks, id, layout, boardW, H) {
943
996
  }
944
997
  return placements;
945
998
  }
946
- function tileItems(items, boardW, boardH) {
999
+ function tileItems(items, boardW, boardH, mode = "wide") {
947
1000
  const n = items.length;
948
1001
  if (n === 0) return items;
1002
+ if (mode === "phone") return tileDuo(items, boardW, boardH);
949
1003
  const availH = Math.max(MIN_H * 2 + GAP, boardH - 150);
950
1004
  const rows = Math.ceil(n / 2);
951
1005
  const rowH = Math.max(300, Math.round((availH - GAP * (rows - 1)) / rows));
@@ -998,7 +1052,8 @@ function CanvasProvider({
998
1052
  initialLayout,
999
1053
  addKey = null,
1000
1054
  persist = false,
1001
- savedSession
1055
+ savedSession,
1056
+ limits
1002
1057
  }) {
1003
1058
  const resolveRef = useRef6(resolveBlock);
1004
1059
  useEffect6(() => {
@@ -1006,14 +1061,22 @@ function CanvasProvider({
1006
1061
  }, [resolveBlock]);
1007
1062
  const resolve = (key) => resolveRef.current(key);
1008
1063
  const initial = useMemo2(() => {
1009
- let z = 1;
1010
- const base = initialLayout ? initialLayout.filter((b) => b && typeof b.key === "string" && resolveRef.current(b.key)).map((b) => blockFromSaved(b, z++)) : initialKeys.filter((k) => resolveRef.current(k)).map((k) => blockFromSaved(k, z++));
1011
- if (addKey && resolveRef.current(addKey) && !base.some((b) => b.key === addKey)) {
1012
- base.unshift(blockFromSaved(addKey, z++));
1064
+ const max = maxWindowsFor("wide", limits);
1065
+ const entries = initialLayout ? initialLayout.filter((b) => b && typeof b.key === "string" && resolveRef.current(b.key)) : initialKeys.filter((k) => resolveRef.current(k));
1066
+ if (addKey && resolveRef.current(addKey) && !entries.some((b) => (typeof b === "string" ? b : b.key) === addKey)) {
1067
+ entries.unshift(addKey);
1013
1068
  }
1069
+ const kept = entries.slice(0, max);
1070
+ let z = 1;
1071
+ const base = kept.map(() => null);
1072
+ for (let i = kept.length - 1; i >= 0; i--) base[i] = blockFromSaved(kept[i], z++);
1014
1073
  return { base, nextZ: z };
1015
1074
  }, []);
1016
1075
  const zRef = useRef6(initial.nextZ);
1076
+ const limitsRef = useRef6(limits);
1077
+ useEffect6(() => {
1078
+ limitsRef.current = limits;
1079
+ }, [limits]);
1017
1080
  const [blocks, setBlocks] = useState6(initial.base);
1018
1081
  const [interacting, setInteracting] = useState6(false);
1019
1082
  const [activeId, setActiveId] = useState6(null);
@@ -1021,6 +1084,9 @@ function CanvasProvider({
1021
1084
  const [boardWidth, setBoardWidth] = useState6(1280);
1022
1085
  const boardRef = useRef6(1280);
1023
1086
  const boardHRef = useRef6(820);
1087
+ const [mode, setMode] = useState6("wide");
1088
+ const modeRef = useRef6("wide");
1089
+ const maxFor = (m) => maxWindowsFor(m, limitsRef.current);
1024
1090
  const persistRef = useRef6(persist);
1025
1091
  useEffect6(() => {
1026
1092
  persistRef.current = persist;
@@ -1029,12 +1095,26 @@ function CanvasProvider({
1029
1095
  const hadUserChangeRef = useRef6(false);
1030
1096
  useEffect6(() => {
1031
1097
  const measure = () => {
1032
- const w = Math.max(360, window.innerWidth - 40);
1098
+ const vw = Math.min(window.innerWidth, window.visualViewport?.width || Infinity);
1099
+ const w = Math.max(320, vw - 40);
1033
1100
  boardRef.current = w;
1034
1101
  boardHRef.current = Math.max(480, window.innerHeight);
1035
1102
  setBoardWidth(w);
1103
+ const coarse = !!window.matchMedia?.("(pointer: coarse)").matches;
1104
+ const m = detectMode(vw, window.innerHeight, coarse);
1105
+ const modeChanged = m !== modeRef.current;
1106
+ modeRef.current = m;
1107
+ setMode(m);
1036
1108
  setBlocks((prev) => {
1037
- const next = fitToBoard(prev, w, boardHRef.current);
1109
+ let next;
1110
+ if (m === "phone") {
1111
+ next = tileDuo(capList(prev, maxFor("phone")), w, boardHRef.current);
1112
+ if (sameGeometry(prev, next)) return prev;
1113
+ } else if (modeChanged) {
1114
+ next = tileItems(capList(prev, maxFor("wide")), w, boardHRef.current);
1115
+ } else {
1116
+ next = fitToBoard(prev, w, boardHRef.current);
1117
+ }
1038
1118
  if (next !== prev) fitAdjustRef.current = true;
1039
1119
  return next;
1040
1120
  });
@@ -1044,22 +1124,23 @@ function CanvasProvider({
1044
1124
  return () => window.removeEventListener("resize", measure);
1045
1125
  }, []);
1046
1126
  const layoutDefault = useCallback(
1047
- (list) => tileItems(list, boardRef.current, boardHRef.current),
1127
+ (list) => tileItems(list, boardRef.current, boardHRef.current, modeRef.current),
1048
1128
  []
1049
1129
  );
1130
+ const customizedIn = (list) => modeRef.current !== "phone" && list.some((b) => b.placed);
1050
1131
  const tiledOnce = useRef6(false);
1051
1132
  useEffect6(() => {
1052
1133
  if (tiledOnce.current) return;
1053
1134
  tiledOnce.current = true;
1054
- setBlocks((prev) => prev.length === 0 || prev.some((b) => b.placed) ? prev : layoutDefault(prev));
1135
+ setBlocks((prev) => prev.length === 0 || customizedIn(prev) ? prev : layoutDefault(prev));
1055
1136
  }, [boardWidth]);
1056
1137
  const addBlock = useCallback((key, context, width, append = false) => {
1057
1138
  if (!resolve(key)) return;
1058
1139
  const w = width ?? resolve(key)?.defaultWidth;
1059
1140
  setBlocks((prev) => {
1060
1141
  const existing = prev.find((b) => b.key === key);
1061
- const customized = prev.some((b) => b.placed);
1062
- const without = prev.filter((b) => b.key !== key);
1142
+ const customized = customizedIn(prev);
1143
+ const without = capList(prev.filter((b) => b.key !== key), maxFor(modeRef.current) - 1);
1063
1144
  if (customized) {
1064
1145
  if (append) {
1065
1146
  const startY = without.length ? Math.max(...without.map((b) => b.y + b.h)) + GAP : 0;
@@ -1099,13 +1180,15 @@ function CanvasProvider({
1099
1180
  const removeBlock = useCallback((id) => {
1100
1181
  setBlocks((prev) => {
1101
1182
  const remaining = prev.filter((b) => b.id !== id);
1102
- return remaining.length > 0 && !remaining.some((b) => b.placed) ? layoutDefault(remaining) : remaining;
1183
+ return remaining.length > 0 && !customizedIn(remaining) ? layoutDefault(remaining) : remaining;
1103
1184
  });
1104
1185
  }, [layoutDefault]);
1105
1186
  const resizeBlock = useCallback((id, width) => {
1187
+ if (modeRef.current === "phone") return;
1106
1188
  setBlocks((prev) => prev.map((b) => b.id === id ? { ...b, width, w: pxWidth(WIDTH_SPAN[width], boardRef.current), placed: true, z: zRef.current++ } : b));
1107
1189
  }, []);
1108
1190
  const moveTo = useCallback((id, x, y) => {
1191
+ if (modeRef.current === "phone") return;
1109
1192
  const boardW = boardRef.current;
1110
1193
  setBlocks((prev) => prev.map((b) => {
1111
1194
  if (b.id !== id || b.locked) return b;
@@ -1115,6 +1198,7 @@ function CanvasProvider({
1115
1198
  }));
1116
1199
  }, []);
1117
1200
  const sizeTo = useCallback((id, w, h) => {
1201
+ if (modeRef.current === "phone") return;
1118
1202
  const boardW = boardRef.current;
1119
1203
  setBlocks((prev) => prev.map((b) => {
1120
1204
  if (b.id !== id || b.locked) return b;
@@ -1124,6 +1208,7 @@ function CanvasProvider({
1124
1208
  }));
1125
1209
  }, []);
1126
1210
  const setRect = useCallback((id, r) => {
1211
+ if (modeRef.current === "phone") return;
1127
1212
  const boardW = boardRef.current;
1128
1213
  setBlocks((prev) => prev.map((b) => {
1129
1214
  if (b.id !== id || b.locked) return b;
@@ -1136,6 +1221,7 @@ function CanvasProvider({
1136
1221
  }));
1137
1222
  }, []);
1138
1223
  const snapTo = useCallback((id, layout) => {
1224
+ if (modeRef.current === "phone") return;
1139
1225
  const boardW = boardRef.current;
1140
1226
  const H = tallH(boardHRef.current);
1141
1227
  setSnapGhost(null);
@@ -1183,6 +1269,7 @@ function CanvasProvider({
1183
1269
  }, [blocks]);
1184
1270
  const clearSnapPreview = useCallback(() => setSnapGhost(null), []);
1185
1271
  const resizeRow = useCallback((id, x, w, h, origin) => {
1272
+ if (modeRef.current === "phone") return;
1186
1273
  const boardW = boardRef.current;
1187
1274
  const M0 = origin.find((b) => b.id === id);
1188
1275
  if (!M0 || M0.locked) return;
@@ -1232,9 +1319,14 @@ function CanvasProvider({
1232
1319
  }));
1233
1320
  }, []);
1234
1321
  const bringToFront = useCallback((id) => {
1235
- setBlocks((prev) => prev.map((b) => b.id === id ? { ...b, z: zRef.current++ } : b));
1236
- }, []);
1322
+ setBlocks((prev) => {
1323
+ if (prev.find((b) => b.id === id)?.z === zRef.current - 1) return prev;
1324
+ const next = prev.map((b) => b.id === id ? { ...b, z: zRef.current++ } : b);
1325
+ return modeRef.current === "phone" ? layoutDefault(next) : next;
1326
+ });
1327
+ }, [layoutDefault]);
1237
1328
  const resolveCollisions = useCallback((id) => {
1329
+ if (modeRef.current === "phone") return;
1238
1330
  setBlocks((prev) => {
1239
1331
  const moved = prev.find((b) => b.id === id);
1240
1332
  if (!moved) return prev;
@@ -1277,8 +1369,10 @@ function CanvasProvider({
1277
1369
  if (!saved || saved.length === 0) return;
1278
1370
  restoredRef.current = true;
1279
1371
  setCanRestore(false);
1280
- const built = saved.filter((b) => b && typeof b.key === "string" && resolve(b.key)).map((b) => blockFromSaved(b, zRef.current++));
1281
- const arranged = built.some((b) => b.placed) ? built : layoutDefault(built);
1372
+ const kept = saved.filter((b) => b && typeof b.key === "string" && resolve(b.key)).slice(0, maxFor(modeRef.current));
1373
+ const built = kept.map(() => null);
1374
+ for (let i = kept.length - 1; i >= 0; i--) built[i] = blockFromSaved(kept[i], zRef.current++);
1375
+ const arranged = customizedIn(built) ? built : layoutDefault(built);
1282
1376
  const fitted = fitToBoard(arranged, boardRef.current, boardHRef.current);
1283
1377
  if (fitted !== arranged) fitAdjustRef.current = true;
1284
1378
  setBlocks(fitted);
@@ -1301,7 +1395,7 @@ function CanvasProvider({
1301
1395
  const width = d.width;
1302
1396
  setBlocks((prev) => {
1303
1397
  const list = prev.map((b) => b.key === d.key ? { ...b, width } : b);
1304
- return prev.some((b) => b.placed) ? list : layoutDefault(list);
1398
+ return customizedIn(prev) ? list : layoutDefault(list);
1305
1399
  });
1306
1400
  }
1307
1401
  function onPlace(e) {
@@ -1315,7 +1409,7 @@ function CanvasProvider({
1315
1409
  const [moved] = next.splice(srcIdx, 1);
1316
1410
  const insertAt = next.findIndex((b) => b.key === d.afterKey) + 1;
1317
1411
  next.splice(insertAt, 0, moved);
1318
- return prev.some((b) => b.placed) ? next : layoutDefault(next);
1412
+ return customizedIn(prev) ? next : layoutDefault(next);
1319
1413
  });
1320
1414
  }
1321
1415
  function onSwap(e) {
@@ -1323,7 +1417,7 @@ function CanvasProvider({
1323
1417
  const keys = d?.keys ?? [];
1324
1418
  const contexts = d?.contexts ?? [];
1325
1419
  const widths = d?.widths ?? [];
1326
- const built = keys.filter((k) => resolve(k)).map((key, i) => ({
1420
+ const built = keys.filter((k) => resolve(k)).slice(0, maxFor(modeRef.current)).map((key, i) => ({
1327
1421
  id: nextId(),
1328
1422
  key,
1329
1423
  context: contexts[i],
@@ -1380,12 +1474,13 @@ function CanvasProvider({
1380
1474
  }, [blocks.length, canRestore]);
1381
1475
  const firstRun = useRef6(true);
1382
1476
  const saveTimer = useRef6(null);
1383
- const layoutSignature = blocks.map((b) => `${b.key}:${b.width ?? ""}:${Math.round(b.x)}:${Math.round(b.y)}:${Math.round(b.w)}:${Math.round(b.h)}`).join("|");
1477
+ const layoutSignature = blocks.map((b) => mode === "phone" ? `${b.key}:${b.width ?? ""}` : `${b.key}:${b.width ?? ""}:${Math.round(b.x)}:${Math.round(b.y)}:${Math.round(b.w)}:${Math.round(b.h)}`).join("|");
1384
1478
  const payload = useMemo2(() => layoutSignature ? {
1385
1479
  savedDay: localDay(),
1386
1480
  blocks: layoutSignature.split("|").map((seg) => {
1387
1481
  const [key, width, x, y, w, h] = seg.split(":");
1388
- return { key, ...width ? { width } : {}, x: Number(x), y: Number(y), w: Number(w), h: Number(h) };
1482
+ const base = { key, ...width ? { width } : {} };
1483
+ return x === void 0 ? base : { ...base, x: Number(x), y: Number(y), w: Number(w), h: Number(h) };
1389
1484
  })
1390
1485
  } : null, [layoutSignature]);
1391
1486
  const payloadRef = useRef6(payload);
@@ -1467,8 +1562,10 @@ function CanvasProvider({
1467
1562
  setInteracting,
1468
1563
  activeId,
1469
1564
  setActiveId,
1470
- boardWidth
1471
- }), [blocks, addBlock, removeBlock, resizeBlock, snapTo, snapPreview, previewSnap, clearSnapPreview, snapGhost, moveTo, sizeTo, setRect, resizeRow, bringToFront, resolveCollisions, toggleLock, clear, interacting, activeId, boardWidth]);
1565
+ boardWidth,
1566
+ mode,
1567
+ maxBlocks: maxWindowsFor(mode, limits)
1568
+ }), [blocks, addBlock, removeBlock, resizeBlock, snapTo, snapPreview, previewSnap, clearSnapPreview, snapGhost, moveTo, sizeTo, setRect, resizeRow, bringToFront, resolveCollisions, toggleLock, clear, interacting, activeId, boardWidth, mode, limits]);
1472
1569
  return /* @__PURE__ */ jsx11(CanvasContext.Provider, { value: api, children });
1473
1570
  }
1474
1571
 
@@ -1696,11 +1793,14 @@ export {
1696
1793
  CanvasProvider,
1697
1794
  CanvasTransition,
1698
1795
  DEFAULT_H,
1796
+ DUO_FOCUS_SHARE,
1699
1797
  GAP,
1700
1798
  GenerativeView,
1701
1799
  HolographicShine,
1702
1800
  LiveCard,
1703
1801
  LiveStat,
1802
+ MAX_WINDOWS,
1803
+ MAX_WINDOWS_PHONE,
1704
1804
  MIN_H,
1705
1805
  MIN_W,
1706
1806
  PageTransition,
@@ -1709,6 +1809,8 @@ export {
1709
1809
  ShiftCard,
1710
1810
  TiltCard,
1711
1811
  WIDTH_SPAN,
1812
+ detectMode,
1813
+ maxWindowsFor,
1712
1814
  tallH,
1713
1815
  useCanvas,
1714
1816
  useGenerate,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@allshift/ui",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "description": "Shift family shared FEEL layer — glass morphism, 3D tilt physics, holographic shine, scroll-triggered generation, and the living-canvas engine (LiveCard, LiveStat, CanvasTransition, CanvasLauncher). Brand-neutral by design.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -27,7 +27,8 @@
27
27
  "scripts": {
28
28
  "build": "tsup",
29
29
  "dev": "tsup --watch",
30
- "type-check": "tsc --noEmit"
30
+ "type-check": "tsc --noEmit",
31
+ "test": "vitest run"
31
32
  },
32
33
  "peerDependencies": {
33
34
  "framer-motion": ">=10",
@@ -36,8 +37,12 @@
36
37
  "next": ">=14"
37
38
  },
38
39
  "peerDependenciesMeta": {
39
- "lucide-react": { "optional": true },
40
- "next": { "optional": true }
40
+ "lucide-react": {
41
+ "optional": true
42
+ },
43
+ "next": {
44
+ "optional": true
45
+ }
41
46
  },
42
47
  "devDependencies": {
43
48
  "@types/react": "^19.2.17",
@@ -46,7 +51,8 @@
46
51
  "next": "^16.2.7",
47
52
  "react": "^19.2.7",
48
53
  "tsup": "^8.5.1",
49
- "typescript": "^6.0.3"
54
+ "typescript": "^6.0.3",
55
+ "vitest": "^4.1.11"
50
56
  },
51
57
  "publishConfig": {
52
58
  "access": "public"