@allshift/ui 0.5.1 → 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 +160 -33
- package/dist/index.d.cts +16 -2
- package/dist/index.d.ts +16 -2
- package/dist/index.js +155 -33
- package/package.json +11 -5
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));
|
|
@@ -1004,6 +1063,20 @@ function tileItems(items, boardW, boardH) {
|
|
|
1004
1063
|
return { ...it, x, y, w, h: rowH };
|
|
1005
1064
|
});
|
|
1006
1065
|
}
|
|
1066
|
+
function fitToBoard(list, boardW, boardH) {
|
|
1067
|
+
if (list.length === 0) return list;
|
|
1068
|
+
const extent = Math.max(...list.map((b) => b.x + b.w));
|
|
1069
|
+
if (extent > boardW * 1.5) return tileItems(list, boardW, boardH);
|
|
1070
|
+
let changed = false;
|
|
1071
|
+
const next = list.map((b) => {
|
|
1072
|
+
const w = Math.min(b.w, boardW);
|
|
1073
|
+
const x = Math.max(0, Math.min(b.x, boardW - w));
|
|
1074
|
+
if (w === b.w && x === b.x) return b;
|
|
1075
|
+
changed = true;
|
|
1076
|
+
return { ...b, w, x };
|
|
1077
|
+
});
|
|
1078
|
+
return changed ? next : list;
|
|
1079
|
+
}
|
|
1007
1080
|
function overlaps(a, b) {
|
|
1008
1081
|
return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
|
|
1009
1082
|
}
|
|
@@ -1024,7 +1097,8 @@ function CanvasProvider({
|
|
|
1024
1097
|
initialLayout,
|
|
1025
1098
|
addKey = null,
|
|
1026
1099
|
persist = false,
|
|
1027
|
-
savedSession
|
|
1100
|
+
savedSession,
|
|
1101
|
+
limits
|
|
1028
1102
|
}) {
|
|
1029
1103
|
const resolveRef = (0, import_react10.useRef)(resolveBlock);
|
|
1030
1104
|
(0, import_react10.useEffect)(() => {
|
|
@@ -1032,14 +1106,22 @@ function CanvasProvider({
|
|
|
1032
1106
|
}, [resolveBlock]);
|
|
1033
1107
|
const resolve = (key) => resolveRef.current(key);
|
|
1034
1108
|
const initial = (0, import_react10.useMemo)(() => {
|
|
1035
|
-
|
|
1036
|
-
const
|
|
1037
|
-
if (addKey && resolveRef.current(addKey) && !
|
|
1038
|
-
|
|
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);
|
|
1039
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++);
|
|
1040
1118
|
return { base, nextZ: z };
|
|
1041
1119
|
}, []);
|
|
1042
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]);
|
|
1043
1125
|
const [blocks, setBlocks] = (0, import_react10.useState)(initial.base);
|
|
1044
1126
|
const [interacting, setInteracting] = (0, import_react10.useState)(false);
|
|
1045
1127
|
const [activeId, setActiveId] = (0, import_react10.useState)(null);
|
|
@@ -1047,26 +1129,39 @@ function CanvasProvider({
|
|
|
1047
1129
|
const [boardWidth, setBoardWidth] = (0, import_react10.useState)(1280);
|
|
1048
1130
|
const boardRef = (0, import_react10.useRef)(1280);
|
|
1049
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);
|
|
1050
1135
|
const persistRef = (0, import_react10.useRef)(persist);
|
|
1051
1136
|
(0, import_react10.useEffect)(() => {
|
|
1052
1137
|
persistRef.current = persist;
|
|
1053
1138
|
}, [persist]);
|
|
1139
|
+
const fitAdjustRef = (0, import_react10.useRef)(false);
|
|
1140
|
+
const hadUserChangeRef = (0, import_react10.useRef)(false);
|
|
1054
1141
|
(0, import_react10.useEffect)(() => {
|
|
1055
1142
|
const measure = () => {
|
|
1056
|
-
const
|
|
1143
|
+
const vw = Math.min(window.innerWidth, window.visualViewport?.width || Infinity);
|
|
1144
|
+
const w = Math.max(320, vw - 40);
|
|
1057
1145
|
boardRef.current = w;
|
|
1058
1146
|
boardHRef.current = Math.max(480, window.innerHeight);
|
|
1059
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);
|
|
1060
1153
|
setBlocks((prev) => {
|
|
1061
|
-
let
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
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
|
+
}
|
|
1163
|
+
if (next !== prev) fitAdjustRef.current = true;
|
|
1164
|
+
return next;
|
|
1070
1165
|
});
|
|
1071
1166
|
};
|
|
1072
1167
|
measure();
|
|
@@ -1074,22 +1169,23 @@ function CanvasProvider({
|
|
|
1074
1169
|
return () => window.removeEventListener("resize", measure);
|
|
1075
1170
|
}, []);
|
|
1076
1171
|
const layoutDefault = (0, import_react10.useCallback)(
|
|
1077
|
-
(list) => tileItems(list, boardRef.current, boardHRef.current),
|
|
1172
|
+
(list) => tileItems(list, boardRef.current, boardHRef.current, modeRef.current),
|
|
1078
1173
|
[]
|
|
1079
1174
|
);
|
|
1175
|
+
const customizedIn = (list) => modeRef.current !== "phone" && list.some((b) => b.placed);
|
|
1080
1176
|
const tiledOnce = (0, import_react10.useRef)(false);
|
|
1081
1177
|
(0, import_react10.useEffect)(() => {
|
|
1082
1178
|
if (tiledOnce.current) return;
|
|
1083
1179
|
tiledOnce.current = true;
|
|
1084
|
-
setBlocks((prev) => prev.length === 0 || prev
|
|
1180
|
+
setBlocks((prev) => prev.length === 0 || customizedIn(prev) ? prev : layoutDefault(prev));
|
|
1085
1181
|
}, [boardWidth]);
|
|
1086
1182
|
const addBlock = (0, import_react10.useCallback)((key, context, width, append = false) => {
|
|
1087
1183
|
if (!resolve(key)) return;
|
|
1088
1184
|
const w = width ?? resolve(key)?.defaultWidth;
|
|
1089
1185
|
setBlocks((prev) => {
|
|
1090
1186
|
const existing = prev.find((b) => b.key === key);
|
|
1091
|
-
const customized = prev
|
|
1092
|
-
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);
|
|
1093
1189
|
if (customized) {
|
|
1094
1190
|
if (append) {
|
|
1095
1191
|
const startY = without.length ? Math.max(...without.map((b) => b.y + b.h)) + GAP : 0;
|
|
@@ -1129,13 +1225,15 @@ function CanvasProvider({
|
|
|
1129
1225
|
const removeBlock = (0, import_react10.useCallback)((id) => {
|
|
1130
1226
|
setBlocks((prev) => {
|
|
1131
1227
|
const remaining = prev.filter((b) => b.id !== id);
|
|
1132
|
-
return remaining.length > 0 && !remaining
|
|
1228
|
+
return remaining.length > 0 && !customizedIn(remaining) ? layoutDefault(remaining) : remaining;
|
|
1133
1229
|
});
|
|
1134
1230
|
}, [layoutDefault]);
|
|
1135
1231
|
const resizeBlock = (0, import_react10.useCallback)((id, width) => {
|
|
1232
|
+
if (modeRef.current === "phone") return;
|
|
1136
1233
|
setBlocks((prev) => prev.map((b) => b.id === id ? { ...b, width, w: pxWidth(WIDTH_SPAN[width], boardRef.current), placed: true, z: zRef.current++ } : b));
|
|
1137
1234
|
}, []);
|
|
1138
1235
|
const moveTo = (0, import_react10.useCallback)((id, x, y) => {
|
|
1236
|
+
if (modeRef.current === "phone") return;
|
|
1139
1237
|
const boardW = boardRef.current;
|
|
1140
1238
|
setBlocks((prev) => prev.map((b) => {
|
|
1141
1239
|
if (b.id !== id || b.locked) return b;
|
|
@@ -1145,6 +1243,7 @@ function CanvasProvider({
|
|
|
1145
1243
|
}));
|
|
1146
1244
|
}, []);
|
|
1147
1245
|
const sizeTo = (0, import_react10.useCallback)((id, w, h) => {
|
|
1246
|
+
if (modeRef.current === "phone") return;
|
|
1148
1247
|
const boardW = boardRef.current;
|
|
1149
1248
|
setBlocks((prev) => prev.map((b) => {
|
|
1150
1249
|
if (b.id !== id || b.locked) return b;
|
|
@@ -1154,6 +1253,7 @@ function CanvasProvider({
|
|
|
1154
1253
|
}));
|
|
1155
1254
|
}, []);
|
|
1156
1255
|
const setRect = (0, import_react10.useCallback)((id, r) => {
|
|
1256
|
+
if (modeRef.current === "phone") return;
|
|
1157
1257
|
const boardW = boardRef.current;
|
|
1158
1258
|
setBlocks((prev) => prev.map((b) => {
|
|
1159
1259
|
if (b.id !== id || b.locked) return b;
|
|
@@ -1166,6 +1266,7 @@ function CanvasProvider({
|
|
|
1166
1266
|
}));
|
|
1167
1267
|
}, []);
|
|
1168
1268
|
const snapTo = (0, import_react10.useCallback)((id, layout) => {
|
|
1269
|
+
if (modeRef.current === "phone") return;
|
|
1169
1270
|
const boardW = boardRef.current;
|
|
1170
1271
|
const H = tallH(boardHRef.current);
|
|
1171
1272
|
setSnapGhost(null);
|
|
@@ -1213,6 +1314,7 @@ function CanvasProvider({
|
|
|
1213
1314
|
}, [blocks]);
|
|
1214
1315
|
const clearSnapPreview = (0, import_react10.useCallback)(() => setSnapGhost(null), []);
|
|
1215
1316
|
const resizeRow = (0, import_react10.useCallback)((id, x, w, h, origin) => {
|
|
1317
|
+
if (modeRef.current === "phone") return;
|
|
1216
1318
|
const boardW = boardRef.current;
|
|
1217
1319
|
const M0 = origin.find((b) => b.id === id);
|
|
1218
1320
|
if (!M0 || M0.locked) return;
|
|
@@ -1262,9 +1364,14 @@ function CanvasProvider({
|
|
|
1262
1364
|
}));
|
|
1263
1365
|
}, []);
|
|
1264
1366
|
const bringToFront = (0, import_react10.useCallback)((id) => {
|
|
1265
|
-
setBlocks((prev) =>
|
|
1266
|
-
|
|
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]);
|
|
1267
1373
|
const resolveCollisions = (0, import_react10.useCallback)((id) => {
|
|
1374
|
+
if (modeRef.current === "phone") return;
|
|
1268
1375
|
setBlocks((prev) => {
|
|
1269
1376
|
const moved = prev.find((b) => b.id === id);
|
|
1270
1377
|
if (!moved) return prev;
|
|
@@ -1307,8 +1414,13 @@ function CanvasProvider({
|
|
|
1307
1414
|
if (!saved || saved.length === 0) return;
|
|
1308
1415
|
restoredRef.current = true;
|
|
1309
1416
|
setCanRestore(false);
|
|
1310
|
-
const
|
|
1311
|
-
|
|
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);
|
|
1421
|
+
const fitted = fitToBoard(arranged, boardRef.current, boardHRef.current);
|
|
1422
|
+
if (fitted !== arranged) fitAdjustRef.current = true;
|
|
1423
|
+
setBlocks(fitted);
|
|
1312
1424
|
}, [savedSession, layoutDefault]);
|
|
1313
1425
|
const clear = (0, import_react10.useCallback)((keepKey) => {
|
|
1314
1426
|
setBlocks((prev) => keepKey ? prev.filter((b) => b.key === keepKey) : []);
|
|
@@ -1328,7 +1440,7 @@ function CanvasProvider({
|
|
|
1328
1440
|
const width = d.width;
|
|
1329
1441
|
setBlocks((prev) => {
|
|
1330
1442
|
const list = prev.map((b) => b.key === d.key ? { ...b, width } : b);
|
|
1331
|
-
return prev
|
|
1443
|
+
return customizedIn(prev) ? list : layoutDefault(list);
|
|
1332
1444
|
});
|
|
1333
1445
|
}
|
|
1334
1446
|
function onPlace(e) {
|
|
@@ -1342,7 +1454,7 @@ function CanvasProvider({
|
|
|
1342
1454
|
const [moved] = next.splice(srcIdx, 1);
|
|
1343
1455
|
const insertAt = next.findIndex((b) => b.key === d.afterKey) + 1;
|
|
1344
1456
|
next.splice(insertAt, 0, moved);
|
|
1345
|
-
return prev
|
|
1457
|
+
return customizedIn(prev) ? next : layoutDefault(next);
|
|
1346
1458
|
});
|
|
1347
1459
|
}
|
|
1348
1460
|
function onSwap(e) {
|
|
@@ -1350,7 +1462,7 @@ function CanvasProvider({
|
|
|
1350
1462
|
const keys = d?.keys ?? [];
|
|
1351
1463
|
const contexts = d?.contexts ?? [];
|
|
1352
1464
|
const widths = d?.widths ?? [];
|
|
1353
|
-
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) => ({
|
|
1354
1466
|
id: nextId(),
|
|
1355
1467
|
key,
|
|
1356
1468
|
context: contexts[i],
|
|
@@ -1407,12 +1519,13 @@ function CanvasProvider({
|
|
|
1407
1519
|
}, [blocks.length, canRestore]);
|
|
1408
1520
|
const firstRun = (0, import_react10.useRef)(true);
|
|
1409
1521
|
const saveTimer = (0, import_react10.useRef)(null);
|
|
1410
|
-
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("|");
|
|
1411
1523
|
const payload = (0, import_react10.useMemo)(() => layoutSignature ? {
|
|
1412
1524
|
savedDay: localDay(),
|
|
1413
1525
|
blocks: layoutSignature.split("|").map((seg) => {
|
|
1414
1526
|
const [key, width, x, y, w, h] = seg.split(":");
|
|
1415
|
-
|
|
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) };
|
|
1416
1529
|
})
|
|
1417
1530
|
} : null, [layoutSignature]);
|
|
1418
1531
|
const payloadRef = (0, import_react10.useRef)(payload);
|
|
@@ -1425,6 +1538,12 @@ function CanvasProvider({
|
|
|
1425
1538
|
firstRun.current = false;
|
|
1426
1539
|
return;
|
|
1427
1540
|
}
|
|
1541
|
+
if (fitAdjustRef.current) {
|
|
1542
|
+
fitAdjustRef.current = false;
|
|
1543
|
+
if (!hadUserChangeRef.current) return;
|
|
1544
|
+
} else {
|
|
1545
|
+
hadUserChangeRef.current = true;
|
|
1546
|
+
}
|
|
1428
1547
|
if (!payloadRef.current) return;
|
|
1429
1548
|
if (saveTimer.current) clearTimeout(saveTimer.current);
|
|
1430
1549
|
const body = JSON.stringify(payloadRef.current);
|
|
@@ -1443,6 +1562,7 @@ function CanvasProvider({
|
|
|
1443
1562
|
(0, import_react10.useEffect)(() => {
|
|
1444
1563
|
if (!persist) return;
|
|
1445
1564
|
const flush = () => {
|
|
1565
|
+
if (!hadUserChangeRef.current) return;
|
|
1446
1566
|
if (!payloadRef.current) return;
|
|
1447
1567
|
try {
|
|
1448
1568
|
void fetch("/api/canvas-layout", {
|
|
@@ -1487,8 +1607,10 @@ function CanvasProvider({
|
|
|
1487
1607
|
setInteracting,
|
|
1488
1608
|
activeId,
|
|
1489
1609
|
setActiveId,
|
|
1490
|
-
boardWidth
|
|
1491
|
-
|
|
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]);
|
|
1492
1614
|
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(CanvasContext.Provider, { value: api, children });
|
|
1493
1615
|
}
|
|
1494
1616
|
|
|
@@ -1717,11 +1839,14 @@ function ShiftBar({ state, dots = 3, size = 7, className = "" }) {
|
|
|
1717
1839
|
CanvasProvider,
|
|
1718
1840
|
CanvasTransition,
|
|
1719
1841
|
DEFAULT_H,
|
|
1842
|
+
DUO_FOCUS_SHARE,
|
|
1720
1843
|
GAP,
|
|
1721
1844
|
GenerativeView,
|
|
1722
1845
|
HolographicShine,
|
|
1723
1846
|
LiveCard,
|
|
1724
1847
|
LiveStat,
|
|
1848
|
+
MAX_WINDOWS,
|
|
1849
|
+
MAX_WINDOWS_PHONE,
|
|
1725
1850
|
MIN_H,
|
|
1726
1851
|
MIN_W,
|
|
1727
1852
|
PageTransition,
|
|
@@ -1730,6 +1855,8 @@ function ShiftBar({ state, dots = 3, size = 7, className = "" }) {
|
|
|
1730
1855
|
ShiftCard,
|
|
1731
1856
|
TiltCard,
|
|
1732
1857
|
WIDTH_SPAN,
|
|
1858
|
+
detectMode,
|
|
1859
|
+
maxWindowsFor,
|
|
1733
1860
|
tallH,
|
|
1734
1861
|
useCanvas,
|
|
1735
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));
|
|
@@ -964,6 +1018,20 @@ function tileItems(items, boardW, boardH) {
|
|
|
964
1018
|
return { ...it, x, y, w, h: rowH };
|
|
965
1019
|
});
|
|
966
1020
|
}
|
|
1021
|
+
function fitToBoard(list, boardW, boardH) {
|
|
1022
|
+
if (list.length === 0) return list;
|
|
1023
|
+
const extent = Math.max(...list.map((b) => b.x + b.w));
|
|
1024
|
+
if (extent > boardW * 1.5) return tileItems(list, boardW, boardH);
|
|
1025
|
+
let changed = false;
|
|
1026
|
+
const next = list.map((b) => {
|
|
1027
|
+
const w = Math.min(b.w, boardW);
|
|
1028
|
+
const x = Math.max(0, Math.min(b.x, boardW - w));
|
|
1029
|
+
if (w === b.w && x === b.x) return b;
|
|
1030
|
+
changed = true;
|
|
1031
|
+
return { ...b, w, x };
|
|
1032
|
+
});
|
|
1033
|
+
return changed ? next : list;
|
|
1034
|
+
}
|
|
967
1035
|
function overlaps(a, b) {
|
|
968
1036
|
return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
|
|
969
1037
|
}
|
|
@@ -984,7 +1052,8 @@ function CanvasProvider({
|
|
|
984
1052
|
initialLayout,
|
|
985
1053
|
addKey = null,
|
|
986
1054
|
persist = false,
|
|
987
|
-
savedSession
|
|
1055
|
+
savedSession,
|
|
1056
|
+
limits
|
|
988
1057
|
}) {
|
|
989
1058
|
const resolveRef = useRef6(resolveBlock);
|
|
990
1059
|
useEffect6(() => {
|
|
@@ -992,14 +1061,22 @@ function CanvasProvider({
|
|
|
992
1061
|
}, [resolveBlock]);
|
|
993
1062
|
const resolve = (key) => resolveRef.current(key);
|
|
994
1063
|
const initial = useMemo2(() => {
|
|
995
|
-
|
|
996
|
-
const
|
|
997
|
-
if (addKey && resolveRef.current(addKey) && !
|
|
998
|
-
|
|
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);
|
|
999
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++);
|
|
1000
1073
|
return { base, nextZ: z };
|
|
1001
1074
|
}, []);
|
|
1002
1075
|
const zRef = useRef6(initial.nextZ);
|
|
1076
|
+
const limitsRef = useRef6(limits);
|
|
1077
|
+
useEffect6(() => {
|
|
1078
|
+
limitsRef.current = limits;
|
|
1079
|
+
}, [limits]);
|
|
1003
1080
|
const [blocks, setBlocks] = useState6(initial.base);
|
|
1004
1081
|
const [interacting, setInteracting] = useState6(false);
|
|
1005
1082
|
const [activeId, setActiveId] = useState6(null);
|
|
@@ -1007,26 +1084,39 @@ function CanvasProvider({
|
|
|
1007
1084
|
const [boardWidth, setBoardWidth] = useState6(1280);
|
|
1008
1085
|
const boardRef = useRef6(1280);
|
|
1009
1086
|
const boardHRef = useRef6(820);
|
|
1087
|
+
const [mode, setMode] = useState6("wide");
|
|
1088
|
+
const modeRef = useRef6("wide");
|
|
1089
|
+
const maxFor = (m) => maxWindowsFor(m, limitsRef.current);
|
|
1010
1090
|
const persistRef = useRef6(persist);
|
|
1011
1091
|
useEffect6(() => {
|
|
1012
1092
|
persistRef.current = persist;
|
|
1013
1093
|
}, [persist]);
|
|
1094
|
+
const fitAdjustRef = useRef6(false);
|
|
1095
|
+
const hadUserChangeRef = useRef6(false);
|
|
1014
1096
|
useEffect6(() => {
|
|
1015
1097
|
const measure = () => {
|
|
1016
|
-
const
|
|
1098
|
+
const vw = Math.min(window.innerWidth, window.visualViewport?.width || Infinity);
|
|
1099
|
+
const w = Math.max(320, vw - 40);
|
|
1017
1100
|
boardRef.current = w;
|
|
1018
1101
|
boardHRef.current = Math.max(480, window.innerHeight);
|
|
1019
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);
|
|
1020
1108
|
setBlocks((prev) => {
|
|
1021
|
-
let
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
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
|
+
}
|
|
1118
|
+
if (next !== prev) fitAdjustRef.current = true;
|
|
1119
|
+
return next;
|
|
1030
1120
|
});
|
|
1031
1121
|
};
|
|
1032
1122
|
measure();
|
|
@@ -1034,22 +1124,23 @@ function CanvasProvider({
|
|
|
1034
1124
|
return () => window.removeEventListener("resize", measure);
|
|
1035
1125
|
}, []);
|
|
1036
1126
|
const layoutDefault = useCallback(
|
|
1037
|
-
(list) => tileItems(list, boardRef.current, boardHRef.current),
|
|
1127
|
+
(list) => tileItems(list, boardRef.current, boardHRef.current, modeRef.current),
|
|
1038
1128
|
[]
|
|
1039
1129
|
);
|
|
1130
|
+
const customizedIn = (list) => modeRef.current !== "phone" && list.some((b) => b.placed);
|
|
1040
1131
|
const tiledOnce = useRef6(false);
|
|
1041
1132
|
useEffect6(() => {
|
|
1042
1133
|
if (tiledOnce.current) return;
|
|
1043
1134
|
tiledOnce.current = true;
|
|
1044
|
-
setBlocks((prev) => prev.length === 0 || prev
|
|
1135
|
+
setBlocks((prev) => prev.length === 0 || customizedIn(prev) ? prev : layoutDefault(prev));
|
|
1045
1136
|
}, [boardWidth]);
|
|
1046
1137
|
const addBlock = useCallback((key, context, width, append = false) => {
|
|
1047
1138
|
if (!resolve(key)) return;
|
|
1048
1139
|
const w = width ?? resolve(key)?.defaultWidth;
|
|
1049
1140
|
setBlocks((prev) => {
|
|
1050
1141
|
const existing = prev.find((b) => b.key === key);
|
|
1051
|
-
const customized = prev
|
|
1052
|
-
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);
|
|
1053
1144
|
if (customized) {
|
|
1054
1145
|
if (append) {
|
|
1055
1146
|
const startY = without.length ? Math.max(...without.map((b) => b.y + b.h)) + GAP : 0;
|
|
@@ -1089,13 +1180,15 @@ function CanvasProvider({
|
|
|
1089
1180
|
const removeBlock = useCallback((id) => {
|
|
1090
1181
|
setBlocks((prev) => {
|
|
1091
1182
|
const remaining = prev.filter((b) => b.id !== id);
|
|
1092
|
-
return remaining.length > 0 && !remaining
|
|
1183
|
+
return remaining.length > 0 && !customizedIn(remaining) ? layoutDefault(remaining) : remaining;
|
|
1093
1184
|
});
|
|
1094
1185
|
}, [layoutDefault]);
|
|
1095
1186
|
const resizeBlock = useCallback((id, width) => {
|
|
1187
|
+
if (modeRef.current === "phone") return;
|
|
1096
1188
|
setBlocks((prev) => prev.map((b) => b.id === id ? { ...b, width, w: pxWidth(WIDTH_SPAN[width], boardRef.current), placed: true, z: zRef.current++ } : b));
|
|
1097
1189
|
}, []);
|
|
1098
1190
|
const moveTo = useCallback((id, x, y) => {
|
|
1191
|
+
if (modeRef.current === "phone") return;
|
|
1099
1192
|
const boardW = boardRef.current;
|
|
1100
1193
|
setBlocks((prev) => prev.map((b) => {
|
|
1101
1194
|
if (b.id !== id || b.locked) return b;
|
|
@@ -1105,6 +1198,7 @@ function CanvasProvider({
|
|
|
1105
1198
|
}));
|
|
1106
1199
|
}, []);
|
|
1107
1200
|
const sizeTo = useCallback((id, w, h) => {
|
|
1201
|
+
if (modeRef.current === "phone") return;
|
|
1108
1202
|
const boardW = boardRef.current;
|
|
1109
1203
|
setBlocks((prev) => prev.map((b) => {
|
|
1110
1204
|
if (b.id !== id || b.locked) return b;
|
|
@@ -1114,6 +1208,7 @@ function CanvasProvider({
|
|
|
1114
1208
|
}));
|
|
1115
1209
|
}, []);
|
|
1116
1210
|
const setRect = useCallback((id, r) => {
|
|
1211
|
+
if (modeRef.current === "phone") return;
|
|
1117
1212
|
const boardW = boardRef.current;
|
|
1118
1213
|
setBlocks((prev) => prev.map((b) => {
|
|
1119
1214
|
if (b.id !== id || b.locked) return b;
|
|
@@ -1126,6 +1221,7 @@ function CanvasProvider({
|
|
|
1126
1221
|
}));
|
|
1127
1222
|
}, []);
|
|
1128
1223
|
const snapTo = useCallback((id, layout) => {
|
|
1224
|
+
if (modeRef.current === "phone") return;
|
|
1129
1225
|
const boardW = boardRef.current;
|
|
1130
1226
|
const H = tallH(boardHRef.current);
|
|
1131
1227
|
setSnapGhost(null);
|
|
@@ -1173,6 +1269,7 @@ function CanvasProvider({
|
|
|
1173
1269
|
}, [blocks]);
|
|
1174
1270
|
const clearSnapPreview = useCallback(() => setSnapGhost(null), []);
|
|
1175
1271
|
const resizeRow = useCallback((id, x, w, h, origin) => {
|
|
1272
|
+
if (modeRef.current === "phone") return;
|
|
1176
1273
|
const boardW = boardRef.current;
|
|
1177
1274
|
const M0 = origin.find((b) => b.id === id);
|
|
1178
1275
|
if (!M0 || M0.locked) return;
|
|
@@ -1222,9 +1319,14 @@ function CanvasProvider({
|
|
|
1222
1319
|
}));
|
|
1223
1320
|
}, []);
|
|
1224
1321
|
const bringToFront = useCallback((id) => {
|
|
1225
|
-
setBlocks((prev) =>
|
|
1226
|
-
|
|
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]);
|
|
1227
1328
|
const resolveCollisions = useCallback((id) => {
|
|
1329
|
+
if (modeRef.current === "phone") return;
|
|
1228
1330
|
setBlocks((prev) => {
|
|
1229
1331
|
const moved = prev.find((b) => b.id === id);
|
|
1230
1332
|
if (!moved) return prev;
|
|
@@ -1267,8 +1369,13 @@ function CanvasProvider({
|
|
|
1267
1369
|
if (!saved || saved.length === 0) return;
|
|
1268
1370
|
restoredRef.current = true;
|
|
1269
1371
|
setCanRestore(false);
|
|
1270
|
-
const
|
|
1271
|
-
|
|
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);
|
|
1376
|
+
const fitted = fitToBoard(arranged, boardRef.current, boardHRef.current);
|
|
1377
|
+
if (fitted !== arranged) fitAdjustRef.current = true;
|
|
1378
|
+
setBlocks(fitted);
|
|
1272
1379
|
}, [savedSession, layoutDefault]);
|
|
1273
1380
|
const clear = useCallback((keepKey) => {
|
|
1274
1381
|
setBlocks((prev) => keepKey ? prev.filter((b) => b.key === keepKey) : []);
|
|
@@ -1288,7 +1395,7 @@ function CanvasProvider({
|
|
|
1288
1395
|
const width = d.width;
|
|
1289
1396
|
setBlocks((prev) => {
|
|
1290
1397
|
const list = prev.map((b) => b.key === d.key ? { ...b, width } : b);
|
|
1291
|
-
return prev
|
|
1398
|
+
return customizedIn(prev) ? list : layoutDefault(list);
|
|
1292
1399
|
});
|
|
1293
1400
|
}
|
|
1294
1401
|
function onPlace(e) {
|
|
@@ -1302,7 +1409,7 @@ function CanvasProvider({
|
|
|
1302
1409
|
const [moved] = next.splice(srcIdx, 1);
|
|
1303
1410
|
const insertAt = next.findIndex((b) => b.key === d.afterKey) + 1;
|
|
1304
1411
|
next.splice(insertAt, 0, moved);
|
|
1305
|
-
return prev
|
|
1412
|
+
return customizedIn(prev) ? next : layoutDefault(next);
|
|
1306
1413
|
});
|
|
1307
1414
|
}
|
|
1308
1415
|
function onSwap(e) {
|
|
@@ -1310,7 +1417,7 @@ function CanvasProvider({
|
|
|
1310
1417
|
const keys = d?.keys ?? [];
|
|
1311
1418
|
const contexts = d?.contexts ?? [];
|
|
1312
1419
|
const widths = d?.widths ?? [];
|
|
1313
|
-
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) => ({
|
|
1314
1421
|
id: nextId(),
|
|
1315
1422
|
key,
|
|
1316
1423
|
context: contexts[i],
|
|
@@ -1367,12 +1474,13 @@ function CanvasProvider({
|
|
|
1367
1474
|
}, [blocks.length, canRestore]);
|
|
1368
1475
|
const firstRun = useRef6(true);
|
|
1369
1476
|
const saveTimer = useRef6(null);
|
|
1370
|
-
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("|");
|
|
1371
1478
|
const payload = useMemo2(() => layoutSignature ? {
|
|
1372
1479
|
savedDay: localDay(),
|
|
1373
1480
|
blocks: layoutSignature.split("|").map((seg) => {
|
|
1374
1481
|
const [key, width, x, y, w, h] = seg.split(":");
|
|
1375
|
-
|
|
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) };
|
|
1376
1484
|
})
|
|
1377
1485
|
} : null, [layoutSignature]);
|
|
1378
1486
|
const payloadRef = useRef6(payload);
|
|
@@ -1385,6 +1493,12 @@ function CanvasProvider({
|
|
|
1385
1493
|
firstRun.current = false;
|
|
1386
1494
|
return;
|
|
1387
1495
|
}
|
|
1496
|
+
if (fitAdjustRef.current) {
|
|
1497
|
+
fitAdjustRef.current = false;
|
|
1498
|
+
if (!hadUserChangeRef.current) return;
|
|
1499
|
+
} else {
|
|
1500
|
+
hadUserChangeRef.current = true;
|
|
1501
|
+
}
|
|
1388
1502
|
if (!payloadRef.current) return;
|
|
1389
1503
|
if (saveTimer.current) clearTimeout(saveTimer.current);
|
|
1390
1504
|
const body = JSON.stringify(payloadRef.current);
|
|
@@ -1403,6 +1517,7 @@ function CanvasProvider({
|
|
|
1403
1517
|
useEffect6(() => {
|
|
1404
1518
|
if (!persist) return;
|
|
1405
1519
|
const flush = () => {
|
|
1520
|
+
if (!hadUserChangeRef.current) return;
|
|
1406
1521
|
if (!payloadRef.current) return;
|
|
1407
1522
|
try {
|
|
1408
1523
|
void fetch("/api/canvas-layout", {
|
|
@@ -1447,8 +1562,10 @@ function CanvasProvider({
|
|
|
1447
1562
|
setInteracting,
|
|
1448
1563
|
activeId,
|
|
1449
1564
|
setActiveId,
|
|
1450
|
-
boardWidth
|
|
1451
|
-
|
|
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]);
|
|
1452
1569
|
return /* @__PURE__ */ jsx11(CanvasContext.Provider, { value: api, children });
|
|
1453
1570
|
}
|
|
1454
1571
|
|
|
@@ -1676,11 +1793,14 @@ export {
|
|
|
1676
1793
|
CanvasProvider,
|
|
1677
1794
|
CanvasTransition,
|
|
1678
1795
|
DEFAULT_H,
|
|
1796
|
+
DUO_FOCUS_SHARE,
|
|
1679
1797
|
GAP,
|
|
1680
1798
|
GenerativeView,
|
|
1681
1799
|
HolographicShine,
|
|
1682
1800
|
LiveCard,
|
|
1683
1801
|
LiveStat,
|
|
1802
|
+
MAX_WINDOWS,
|
|
1803
|
+
MAX_WINDOWS_PHONE,
|
|
1684
1804
|
MIN_H,
|
|
1685
1805
|
MIN_W,
|
|
1686
1806
|
PageTransition,
|
|
@@ -1689,6 +1809,8 @@ export {
|
|
|
1689
1809
|
ShiftCard,
|
|
1690
1810
|
TiltCard,
|
|
1691
1811
|
WIDTH_SPAN,
|
|
1812
|
+
detectMode,
|
|
1813
|
+
maxWindowsFor,
|
|
1692
1814
|
tallH,
|
|
1693
1815
|
useCanvas,
|
|
1694
1816
|
useGenerate,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@allshift/ui",
|
|
3
|
-
"version": "0.
|
|
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": {
|
|
40
|
-
|
|
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"
|