@allshift/ui 0.4.0 → 0.5.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.js CHANGED
@@ -793,8 +793,656 @@ function PageTransition({ children, className }) {
793
793
  );
794
794
  }
795
795
 
796
+ // src/canvas/CanvasProvider.tsx
797
+ import { createContext, useCallback, useContext, useEffect as useEffect6, useMemo as useMemo2, useRef as useRef6, useState as useState6 } from "react";
798
+ import { jsx as jsx11 } from "react/jsx-runtime";
799
+ var GAP = 20;
800
+ var DEFAULT_H = 420;
801
+ var MIN_W = 240;
802
+ var MIN_H = 200;
803
+ var WIDTH_SPAN = {
804
+ sm: 4,
805
+ // 1/3
806
+ md: 6,
807
+ // 1/2
808
+ lg: 8,
809
+ // 2/3
810
+ xl: 12
811
+ // full
812
+ };
813
+ var CanvasContext = createContext(null);
814
+ function useCanvas() {
815
+ const ctx = useContext(CanvasContext);
816
+ if (!ctx) throw new Error("useCanvas must be used inside <CanvasProvider>");
817
+ return ctx;
818
+ }
819
+ var _seq = 0;
820
+ function nextId() {
821
+ return `blk-${Date.now()}-${_seq++}`;
822
+ }
823
+ function blockFromSaved(s, z) {
824
+ const sb = typeof s === "string" ? { key: s } : s;
825
+ const hasCoords = typeof sb.x === "number" && typeof sb.y === "number" && typeof sb.w === "number";
826
+ return {
827
+ id: nextId(),
828
+ key: sb.key,
829
+ width: sb.width,
830
+ z,
831
+ placed: hasCoords,
832
+ x: sb.x ?? 0,
833
+ y: sb.y ?? 0,
834
+ w: sb.w ?? 0,
835
+ h: sb.h ?? DEFAULT_H
836
+ };
837
+ }
838
+ function colWidth(boardW) {
839
+ return (boardW - GAP * 11) / 12;
840
+ }
841
+ function pxWidth(weight, boardW) {
842
+ return Math.max(MIN_W, weight * colWidth(boardW) + (weight - 1) * GAP);
843
+ }
844
+ function tallH(boardH) {
845
+ return Math.max(460, boardH - 150);
846
+ }
847
+ function carveAround(zone, locked) {
848
+ const hit = (a, b) => a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
849
+ let r = { ...zone };
850
+ for (let i = 0; i < locked.length + 2; i++) {
851
+ const L = locked.find((l) => hit(r, l));
852
+ if (!L) break;
853
+ const ix = Math.max(r.x, L.x), iy = Math.max(r.y, L.y);
854
+ const ir = Math.min(r.x + r.w, L.x + L.w), ib = Math.min(r.y + r.h, L.y + L.h);
855
+ const slabs = [
856
+ { x: r.x, y: r.y, w: ix - r.x, h: r.h },
857
+ // left of the locked block
858
+ { x: ir, y: r.y, w: r.x + r.w - ir, h: r.h },
859
+ // right of it
860
+ { x: r.x, y: r.y, w: r.w, h: iy - r.y },
861
+ // above it
862
+ { x: r.x, y: ib, w: r.w, h: r.y + r.h - ib }
863
+ // below it
864
+ ].filter((s) => s.w > 1 && s.h > 1);
865
+ if (slabs.length === 0) break;
866
+ r = slabs.reduce((a, b) => a.w * a.h >= b.w * b.h ? a : b);
867
+ }
868
+ return r;
869
+ }
870
+ function zoneBlocked(c, locked) {
871
+ if (c.w < MIN_W || c.h < MIN_H) return true;
872
+ return locked.some((L) => c.x < L.x + L.w && c.x + c.w > L.x && c.y < L.y + L.h && c.y + c.h > L.y);
873
+ }
874
+ function snapZones(boardW, H) {
875
+ const halfW = Math.round((boardW - GAP) / 2);
876
+ const halfH = Math.round((H - GAP) / 2);
877
+ const rightX = boardW - halfW;
878
+ const bottomY = H - halfH;
879
+ return {
880
+ full: { x: 0, y: 0, w: boardW, h: H },
881
+ left: { x: 0, y: 0, w: halfW, h: H },
882
+ right: { x: rightX, y: 0, w: halfW, h: H },
883
+ top: { x: 0, y: 0, w: boardW, h: halfH },
884
+ bottom: { x: 0, y: bottomY, w: boardW, h: halfH },
885
+ tl: { x: 0, y: 0, w: halfW, h: halfH },
886
+ tr: { x: rightX, y: 0, w: halfW, h: halfH },
887
+ bl: { x: 0, y: bottomY, w: halfW, h: halfH },
888
+ br: { x: rightX, y: bottomY, w: halfW, h: halfH }
889
+ };
890
+ }
891
+ function tileRect(items, region) {
892
+ const n = items.length;
893
+ if (n === 0) return items;
894
+ const rows = Math.ceil(n / 2);
895
+ const rowH = Math.max(MIN_H, Math.round((region.h - GAP * (rows - 1)) / rows));
896
+ const halfW = Math.round((region.w - GAP) / 2);
897
+ const single = region.w < MIN_W * 2 + GAP;
898
+ return items.map((it, i) => {
899
+ if (single) {
900
+ const h = Math.max(MIN_H, Math.round((region.h - GAP * (n - 1)) / n));
901
+ return { ...it, x: region.x, y: region.y + i * (h + GAP), w: Math.max(MIN_W, region.w), h, placed: true };
902
+ }
903
+ const row = Math.floor(i / 2);
904
+ const lastOdd = row === rows - 1 && n % 2 === 1;
905
+ const y = region.y + row * (rowH + GAP);
906
+ const w = lastOdd ? region.w : halfW;
907
+ const x = lastOdd ? region.x : region.x + i % 2 * (halfW + GAP);
908
+ return { ...it, x, y, w: Math.max(MIN_W, w), h: rowH, placed: true };
909
+ });
910
+ }
911
+ function bestComplement(z, boardW, H) {
912
+ const cands = [
913
+ { x: z.x + z.w + GAP, y: 0, w: boardW - (z.x + z.w + GAP), h: H },
914
+ // right of zone
915
+ { x: 0, y: 0, w: z.x - GAP, h: H },
916
+ // left of zone
917
+ { x: 0, y: z.y + z.h + GAP, w: boardW, h: H - (z.y + z.h + GAP) },
918
+ // below zone
919
+ { x: 0, y: 0, w: boardW, h: z.y - GAP }
920
+ // above zone
921
+ ].filter((c) => c.w >= MIN_W && c.h >= MIN_H);
922
+ if (cands.length === 0) return null;
923
+ return cands.reduce((a, b) => a.w * a.h >= b.w * b.h ? a : b);
924
+ }
925
+ function computeSnap(blocks, id, layout, boardW, H) {
926
+ const target = blocks.find((b) => b.id === id);
927
+ if (!target || target.locked) return null;
928
+ const zone = snapZones(boardW, H)[layout];
929
+ const locked = blocks.filter((b) => b.id !== id && b.locked);
930
+ const tRect = carveAround(zone, locked);
931
+ if (zoneBlocked(tRect, locked)) return null;
932
+ const placements = /* @__PURE__ */ new Map();
933
+ placements.set(id, { x: Math.round(tRect.x), y: Math.round(tRect.y), w: Math.max(MIN_W, Math.round(tRect.w)), h: Math.max(MIN_H, Math.round(tRect.h)) });
934
+ const others = blocks.filter((b) => b.id !== id && !b.locked);
935
+ if (others.length > 0) {
936
+ const comp = bestComplement(zone, boardW, H);
937
+ if (comp) {
938
+ const region = carveAround(comp, locked);
939
+ if (region.w >= MIN_W && region.h >= MIN_H) {
940
+ tileRect(others, region).forEach((o) => placements.set(o.id, { x: Math.round(o.x), y: Math.round(o.y), w: Math.round(o.w), h: Math.round(o.h) }));
941
+ }
942
+ }
943
+ }
944
+ return placements;
945
+ }
946
+ function tileItems(items, boardW, boardH) {
947
+ const n = items.length;
948
+ if (n === 0) return items;
949
+ const availH = Math.max(MIN_H * 2 + GAP, boardH - 150);
950
+ const rows = Math.ceil(n / 2);
951
+ const rowH = Math.max(300, Math.round((availH - GAP * (rows - 1)) / rows));
952
+ const halfW = Math.round((boardW - GAP) / 2);
953
+ const single = boardW < 720;
954
+ return items.map((it, i) => {
955
+ if (single) {
956
+ const h = Math.max(MIN_H, Math.round(tallH(boardH) * 0.72));
957
+ return { ...it, x: 0, y: i * (h + GAP), w: boardW, h };
958
+ }
959
+ const row = Math.floor(i / 2);
960
+ const lastOdd = row === rows - 1 && n % 2 === 1;
961
+ const y = row * (rowH + GAP);
962
+ const w = lastOdd ? boardW : halfW;
963
+ const x = lastOdd ? 0 : i % 2 * (halfW + GAP);
964
+ return { ...it, x, y, w, h: rowH };
965
+ });
966
+ }
967
+ function overlaps(a, b) {
968
+ 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
+ }
970
+ function pushOut(b, m) {
971
+ const cands = [
972
+ { x: m.x - b.w - GAP, y: b.y, d: Math.abs(m.x - b.w - GAP - b.x), ok: m.x - b.w - GAP >= 0 },
973
+ { x: m.x + m.w + GAP, y: b.y, d: Math.abs(m.x + m.w + GAP - b.x), ok: true },
974
+ { x: b.x, y: m.y - b.h - GAP, d: Math.abs(m.y - b.h - GAP - b.y), ok: m.y - b.h - GAP >= 0 },
975
+ { x: b.x, y: m.y + m.h + GAP, d: Math.abs(m.y + m.h + GAP - b.y), ok: true }
976
+ ].filter((c) => c.ok).sort((p, q) => p.d - q.d);
977
+ const best = cands[0];
978
+ return { x: Math.max(0, best.x), y: Math.max(0, best.y) };
979
+ }
980
+ function CanvasProvider({
981
+ children,
982
+ resolveBlock,
983
+ initialKeys = [],
984
+ initialLayout,
985
+ addKey = null,
986
+ persist = false,
987
+ savedSession
988
+ }) {
989
+ const resolveRef = useRef6(resolveBlock);
990
+ useEffect6(() => {
991
+ resolveRef.current = resolveBlock;
992
+ }, [resolveBlock]);
993
+ const resolve = (key) => resolveRef.current(key);
994
+ const initial = useMemo2(() => {
995
+ let z = 1;
996
+ 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++));
997
+ if (addKey && resolveRef.current(addKey) && !base.some((b) => b.key === addKey)) {
998
+ base.unshift(blockFromSaved(addKey, z++));
999
+ }
1000
+ return { base, nextZ: z };
1001
+ }, []);
1002
+ const zRef = useRef6(initial.nextZ);
1003
+ const [blocks, setBlocks] = useState6(initial.base);
1004
+ const [interacting, setInteracting] = useState6(false);
1005
+ const [activeId, setActiveId] = useState6(null);
1006
+ const [snapGhost, setSnapGhost] = useState6(null);
1007
+ const [boardWidth, setBoardWidth] = useState6(1280);
1008
+ const boardRef = useRef6(1280);
1009
+ const boardHRef = useRef6(820);
1010
+ const persistRef = useRef6(persist);
1011
+ useEffect6(() => {
1012
+ persistRef.current = persist;
1013
+ }, [persist]);
1014
+ useEffect6(() => {
1015
+ const measure = () => {
1016
+ const w = Math.max(360, window.innerWidth - 40);
1017
+ boardRef.current = w;
1018
+ boardHRef.current = Math.max(480, window.innerHeight);
1019
+ setBoardWidth(w);
1020
+ };
1021
+ measure();
1022
+ window.addEventListener("resize", measure);
1023
+ return () => window.removeEventListener("resize", measure);
1024
+ }, []);
1025
+ const layoutDefault = useCallback(
1026
+ (list) => tileItems(list, boardRef.current, boardHRef.current),
1027
+ []
1028
+ );
1029
+ const tiledOnce = useRef6(false);
1030
+ useEffect6(() => {
1031
+ if (tiledOnce.current) return;
1032
+ tiledOnce.current = true;
1033
+ setBlocks((prev) => prev.length === 0 || prev.some((b) => b.placed) ? prev : layoutDefault(prev));
1034
+ }, [boardWidth]);
1035
+ const addBlock = useCallback((key, context, width, append = false) => {
1036
+ if (!resolve(key)) return;
1037
+ const w = width ?? resolve(key)?.defaultWidth;
1038
+ setBlocks((prev) => {
1039
+ const existing = prev.find((b) => b.key === key);
1040
+ const customized = prev.some((b) => b.placed);
1041
+ const without = prev.filter((b) => b.key !== key);
1042
+ if (customized) {
1043
+ if (append) {
1044
+ const startY = without.length ? Math.max(...without.map((b) => b.y + b.h)) + GAP : 0;
1045
+ const nb3 = { id: nextId(), key, context, width: w, locked: existing?.locked, z: zRef.current++, placed: true, x: 0, y: startY, w: boardRef.current, h: tallH(boardHRef.current) };
1046
+ return [...without, nb3];
1047
+ }
1048
+ if (existing?.locked) {
1049
+ return prev.map((b) => b.id === existing.id ? { ...b, z: zRef.current++ } : b);
1050
+ }
1051
+ const boardW = boardRef.current;
1052
+ const H = tallH(boardHRef.current);
1053
+ const nb2 = existing ? { ...existing, z: zRef.current++, placed: true } : { id: nextId(), key, context, width: w, z: zRef.current++, placed: true, x: 0, y: 0, w: 0, h: 0 };
1054
+ if (without.length === 0) return [{ ...nb2, x: 0, y: 0, w: boardW, h: H }];
1055
+ const zone = without.length === 1 ? "left" : "tl";
1056
+ const list2 = [nb2, ...without];
1057
+ const placements = computeSnap(list2, nb2.id, zone, boardW, H);
1058
+ if (placements) {
1059
+ return list2.map((b) => {
1060
+ const p = placements.get(b.id);
1061
+ return p ? { ...b, ...p, placed: true } : b;
1062
+ });
1063
+ }
1064
+ const lockedTop = without.filter((b) => b.locked && b.y < H + GAP);
1065
+ const newY = lockedTop.length ? Math.max(...lockedTop.map((b) => b.y + b.h)) + GAP : 0;
1066
+ const shift = newY + H + GAP;
1067
+ const moved = without.map((b) => b.locked ? b : { ...b, y: b.y + shift });
1068
+ return [{ ...nb2, x: 0, y: newY, w: boardW, h: H }, ...moved];
1069
+ }
1070
+ const nb = existing ? { ...existing, z: zRef.current++ } : { id: nextId(), key, context, width: w, z: zRef.current++, placed: false, x: 0, y: 0, w: 0, h: 0 };
1071
+ const list = append ? [...without, nb] : [nb, ...without];
1072
+ return layoutDefault(list);
1073
+ });
1074
+ if (!append && typeof window !== "undefined") {
1075
+ requestAnimationFrame(() => window.scrollTo({ top: 0, behavior: "smooth" }));
1076
+ }
1077
+ }, [layoutDefault]);
1078
+ const removeBlock = useCallback((id) => {
1079
+ setBlocks((prev) => {
1080
+ const remaining = prev.filter((b) => b.id !== id);
1081
+ return remaining.length > 0 && !remaining.some((b) => b.placed) ? layoutDefault(remaining) : remaining;
1082
+ });
1083
+ }, [layoutDefault]);
1084
+ const resizeBlock = useCallback((id, width) => {
1085
+ setBlocks((prev) => prev.map((b) => b.id === id ? { ...b, width, w: pxWidth(WIDTH_SPAN[width], boardRef.current), placed: true, z: zRef.current++ } : b));
1086
+ }, []);
1087
+ const moveTo = useCallback((id, x, y) => {
1088
+ const boardW = boardRef.current;
1089
+ setBlocks((prev) => prev.map((b) => {
1090
+ if (b.id !== id || b.locked) return b;
1091
+ const nx = Math.max(0, Math.min(Math.round(x), Math.max(0, boardW - b.w)));
1092
+ const ny = Math.max(0, Math.round(y));
1093
+ return { ...b, x: nx, y: ny, placed: true };
1094
+ }));
1095
+ }, []);
1096
+ const sizeTo = useCallback((id, w, h) => {
1097
+ const boardW = boardRef.current;
1098
+ setBlocks((prev) => prev.map((b) => {
1099
+ if (b.id !== id || b.locked) return b;
1100
+ const nw = Math.max(MIN_W, Math.min(Math.round(w), Math.max(MIN_W, boardW - b.x)));
1101
+ const nh = Math.max(MIN_H, Math.round(h));
1102
+ return { ...b, w: nw, h: nh, placed: true };
1103
+ }));
1104
+ }, []);
1105
+ const setRect = useCallback((id, r) => {
1106
+ const boardW = boardRef.current;
1107
+ setBlocks((prev) => prev.map((b) => {
1108
+ if (b.id !== id || b.locked) return b;
1109
+ const nx = Math.max(0, Math.round(r.x));
1110
+ const ny = Math.max(0, Math.round(r.y));
1111
+ let nw = Math.max(MIN_W, Math.round(r.w));
1112
+ const nh = Math.max(MIN_H, Math.round(r.h));
1113
+ if (nx + nw > boardW) nw = Math.max(MIN_W, boardW - nx);
1114
+ return { ...b, x: nx, y: ny, w: nw, h: nh, placed: true };
1115
+ }));
1116
+ }, []);
1117
+ const snapTo = useCallback((id, layout) => {
1118
+ const boardW = boardRef.current;
1119
+ const H = tallH(boardHRef.current);
1120
+ setSnapGhost(null);
1121
+ setBlocks((prev) => {
1122
+ const placements = computeSnap(prev, id, layout, boardW, H);
1123
+ if (!placements) return prev;
1124
+ return prev.map((b) => {
1125
+ const p = placements.get(b.id);
1126
+ if (!p) return b;
1127
+ return { ...b, ...p, placed: true, z: b.id === id ? zRef.current++ : b.z };
1128
+ });
1129
+ });
1130
+ }, []);
1131
+ const snapPreview = useCallback((id, layout) => {
1132
+ const boardW = boardRef.current;
1133
+ const H = tallH(boardHRef.current);
1134
+ const zone = snapZones(boardW, H)[layout];
1135
+ const locked = blocks.filter((b) => b.id !== id && b.locked);
1136
+ const c = carveAround(zone, locked);
1137
+ const box = (r) => ({
1138
+ left: r.x / boardW * 100,
1139
+ top: r.y / H * 100,
1140
+ width: r.w / boardW * 100,
1141
+ height: r.h / H * 100
1142
+ });
1143
+ const placements = computeSnap(blocks, id, layout, boardW, H);
1144
+ const others = placements ? [...placements.entries()].filter(([bid]) => bid !== id).map(([, r]) => box(r)) : [];
1145
+ return { rect: box(c), locked: locked.map((L) => box(L)), others, blocked: zoneBlocked(c, locked) };
1146
+ }, [blocks]);
1147
+ const previewSnap = useCallback((id, layout) => {
1148
+ const boardW = boardRef.current;
1149
+ const H = tallH(boardHRef.current);
1150
+ const placements = computeSnap(blocks, id, layout, boardW, H);
1151
+ if (!placements) {
1152
+ setSnapGhost(null);
1153
+ return;
1154
+ }
1155
+ const target = placements.get(id);
1156
+ if (!target) {
1157
+ setSnapGhost(null);
1158
+ return;
1159
+ }
1160
+ const others = [...placements.entries()].filter(([bid]) => bid !== id).map(([, r]) => r);
1161
+ setSnapGhost({ target, others });
1162
+ }, [blocks]);
1163
+ const clearSnapPreview = useCallback(() => setSnapGhost(null), []);
1164
+ const resizeRow = useCallback((id, x, w, h, origin) => {
1165
+ const boardW = boardRef.current;
1166
+ const M0 = origin.find((b) => b.id === id);
1167
+ if (!M0 || M0.locked) return;
1168
+ const vov = (a, b) => a.y < b.y + b.h && a.y + a.h > b.y;
1169
+ const nh = Math.max(MIN_H, Math.round(h));
1170
+ const packed = /* @__PURE__ */ new Map();
1171
+ let mx = M0.x;
1172
+ let mw;
1173
+ if (Math.round(x) === Math.round(M0.x)) {
1174
+ const rights = origin.filter((b) => b.id !== id && b.x >= M0.x && vov(b, M0)).sort((a, b) => a.x - b.x);
1175
+ const wallIdx = rights.findIndex((b) => b.locked);
1176
+ const wallX = wallIdx >= 0 ? rights[wallIdx].x : boardW + GAP;
1177
+ const pushable = wallIdx >= 0 ? rights.slice(0, wallIdx) : rights;
1178
+ const reserved = pushable.length * (MIN_W + GAP);
1179
+ const mRight = Math.max(M0.x + MIN_W, Math.min(M0.x + Math.round(w), wallX - GAP - reserved));
1180
+ mw = mRight - M0.x;
1181
+ let cursor = mRight + GAP;
1182
+ pushable.forEach((N, i) => {
1183
+ const after = (pushable.length - 1 - i) * (MIN_W + GAP);
1184
+ const nw = Math.max(MIN_W, Math.min(N.w, Math.round(wallX - GAP - cursor - after)));
1185
+ packed.set(N.id, { x: Math.round(cursor), w: nw });
1186
+ cursor += nw + GAP;
1187
+ });
1188
+ } else {
1189
+ const fixedRight = M0.x + M0.w;
1190
+ const lefts = origin.filter((b) => b.id !== id && b.x < M0.x && vov(b, M0)).sort((a, b) => b.x - a.x);
1191
+ const wallIdx = lefts.findIndex((b) => b.locked);
1192
+ const wallRight = wallIdx >= 0 ? lefts[wallIdx].x + lefts[wallIdx].w : -GAP;
1193
+ const pushable = wallIdx >= 0 ? lefts.slice(0, wallIdx) : lefts;
1194
+ const reserved = pushable.length * (MIN_W + GAP);
1195
+ const mLeft = Math.min(fixedRight - MIN_W, Math.max(Math.round(x), wallRight + GAP + reserved));
1196
+ mx = mLeft;
1197
+ mw = fixedRight - mLeft;
1198
+ let cursor = mLeft - GAP;
1199
+ pushable.forEach((N, i) => {
1200
+ const after = (pushable.length - 1 - i) * (MIN_W + GAP);
1201
+ const nw = Math.max(MIN_W, Math.min(N.w, Math.round(cursor - (wallRight + GAP) - after)));
1202
+ const nx = Math.max(wallRight + GAP, Math.round(cursor - nw));
1203
+ packed.set(N.id, { x: nx, w: nw });
1204
+ cursor = nx - GAP;
1205
+ });
1206
+ }
1207
+ setBlocks((prev) => prev.map((b) => {
1208
+ if (b.id === id) return { ...b, x: mx, w: Math.max(MIN_W, mw), h: nh, placed: true };
1209
+ const p = packed.get(b.id);
1210
+ return p ? { ...b, x: p.x, w: p.w, placed: true } : b;
1211
+ }));
1212
+ }, []);
1213
+ const bringToFront = useCallback((id) => {
1214
+ setBlocks((prev) => prev.map((b) => b.id === id ? { ...b, z: zRef.current++ } : b));
1215
+ }, []);
1216
+ const resolveCollisions = useCallback((id) => {
1217
+ setBlocks((prev) => {
1218
+ const moved = prev.find((b) => b.id === id);
1219
+ if (!moved) return prev;
1220
+ const hits = prev.filter((b) => b.id !== id && !b.locked && overlaps(b, moved));
1221
+ if (hits.length === 0) return prev;
1222
+ const cx = moved.x + moved.w / 2;
1223
+ const leftN = hits.filter((b) => b.x + b.w / 2 < cx);
1224
+ const rightN = hits.filter((b) => b.x + b.w / 2 >= cx);
1225
+ if (leftN.length && rightN.length) {
1226
+ const leftEdge = Math.max(...leftN.map((b) => b.x + b.w)) + GAP;
1227
+ const rightEdge = Math.min(...rightN.map((b) => b.x)) - GAP;
1228
+ const gapW = rightEdge - leftEdge;
1229
+ if (gapW >= MIN_W) {
1230
+ return prev.map((b) => b.id === id ? { ...b, x: Math.round(leftEdge), w: Math.round(gapW) } : b);
1231
+ }
1232
+ }
1233
+ const boardW = boardRef.current;
1234
+ let changed = false;
1235
+ const next = prev.map((b) => {
1236
+ if (b.id === id || b.locked || !overlaps(b, moved)) return b;
1237
+ const p = pushOut(b, moved);
1238
+ let nx = p.x;
1239
+ let nw = b.w;
1240
+ if (nx + nw > boardW) nw = Math.max(MIN_W, boardW - nx);
1241
+ if (nx + nw > boardW) nx = Math.max(0, boardW - nw);
1242
+ if (nx === b.x && p.y === b.y && nw === b.w) return b;
1243
+ changed = true;
1244
+ return { ...b, x: nx, y: p.y, w: nw };
1245
+ });
1246
+ return changed ? next : prev;
1247
+ });
1248
+ }, []);
1249
+ const toggleLock = useCallback((id) => {
1250
+ setBlocks((prev) => prev.map((b) => b.id === id ? { ...b, locked: !b.locked } : b));
1251
+ }, []);
1252
+ const restoredRef = useRef6(false);
1253
+ const [canRestore, setCanRestore] = useState6(false);
1254
+ const restoreSession = useCallback(() => {
1255
+ const saved = savedSession?.blocks;
1256
+ if (!saved || saved.length === 0) return;
1257
+ restoredRef.current = true;
1258
+ setCanRestore(false);
1259
+ const built = saved.filter((b) => b && typeof b.key === "string" && resolve(b.key)).map((b) => blockFromSaved(b, zRef.current++));
1260
+ setBlocks(built.some((b) => b.placed) ? built : layoutDefault(built));
1261
+ }, [savedSession, layoutDefault]);
1262
+ const clear = useCallback((keepKey) => {
1263
+ setBlocks((prev) => keepKey ? prev.filter((b) => b.key === keepKey) : []);
1264
+ }, []);
1265
+ useEffect6(() => {
1266
+ function onAdd(e) {
1267
+ const d = e.detail;
1268
+ if (d?.key) addBlock(d.key, d.context, d.width, d.append);
1269
+ }
1270
+ function onClear(e) {
1271
+ const d = e.detail;
1272
+ clear(d?.keepKey);
1273
+ }
1274
+ function onResize(e) {
1275
+ const d = e.detail;
1276
+ if (!d?.key || !d?.width) return;
1277
+ const width = d.width;
1278
+ setBlocks((prev) => {
1279
+ const list = prev.map((b) => b.key === d.key ? { ...b, width } : b);
1280
+ return prev.some((b) => b.placed) ? list : layoutDefault(list);
1281
+ });
1282
+ }
1283
+ function onPlace(e) {
1284
+ const d = e.detail;
1285
+ if (!d?.key || !d?.afterKey) return;
1286
+ setBlocks((prev) => {
1287
+ const srcIdx = prev.findIndex((b) => b.key === d.key);
1288
+ const tgtIdx = prev.findIndex((b) => b.key === d.afterKey);
1289
+ if (srcIdx === -1 || tgtIdx === -1) return prev;
1290
+ const next = [...prev];
1291
+ const [moved] = next.splice(srcIdx, 1);
1292
+ const insertAt = next.findIndex((b) => b.key === d.afterKey) + 1;
1293
+ next.splice(insertAt, 0, moved);
1294
+ return prev.some((b) => b.placed) ? next : layoutDefault(next);
1295
+ });
1296
+ }
1297
+ function onSwap(e) {
1298
+ const d = e.detail;
1299
+ const keys = d?.keys ?? [];
1300
+ const contexts = d?.contexts ?? [];
1301
+ const widths = d?.widths ?? [];
1302
+ const built = keys.filter((k) => resolve(k)).map((key, i) => ({
1303
+ id: nextId(),
1304
+ key,
1305
+ context: contexts[i],
1306
+ width: widths[i] ?? resolve(key)?.defaultWidth,
1307
+ z: zRef.current++,
1308
+ placed: false,
1309
+ x: 0,
1310
+ y: 0,
1311
+ w: 0,
1312
+ h: DEFAULT_H
1313
+ }));
1314
+ setBlocks(layoutDefault(built));
1315
+ }
1316
+ function onRestore() {
1317
+ restoreSession();
1318
+ }
1319
+ window.addEventListener("shift:canvas:add", onAdd);
1320
+ window.addEventListener("shift:canvas:clear", onClear);
1321
+ window.addEventListener("shift:canvas:resize", onResize);
1322
+ window.addEventListener("shift:canvas:place", onPlace);
1323
+ window.addEventListener("shift:canvas:swap", onSwap);
1324
+ window.addEventListener("shift:canvas:restore-session", onRestore);
1325
+ return () => {
1326
+ window.removeEventListener("shift:canvas:add", onAdd);
1327
+ window.removeEventListener("shift:canvas:clear", onClear);
1328
+ window.removeEventListener("shift:canvas:resize", onResize);
1329
+ window.removeEventListener("shift:canvas:place", onPlace);
1330
+ window.removeEventListener("shift:canvas:swap", onSwap);
1331
+ window.removeEventListener("shift:canvas:restore-session", onRestore);
1332
+ };
1333
+ }, [addBlock, clear, layoutDefault, restoreSession]);
1334
+ const localDay = () => {
1335
+ try {
1336
+ return (/* @__PURE__ */ new Date()).toLocaleDateString("en-CA");
1337
+ } catch {
1338
+ return "";
1339
+ }
1340
+ };
1341
+ useEffect6(() => {
1342
+ if (!persist || restoredRef.current) return;
1343
+ const saved = savedSession?.blocks;
1344
+ if (!saved || saved.length === 0) return;
1345
+ if (savedSession?.savedDay && savedSession.savedDay === localDay()) {
1346
+ restoreSession();
1347
+ } else {
1348
+ setCanRestore(true);
1349
+ }
1350
+ }, []);
1351
+ useEffect6(() => {
1352
+ window.dispatchEvent(new CustomEvent("shift:canvas:state", { detail: { count: blocks.length, canRestore } }));
1353
+ return () => {
1354
+ window.dispatchEvent(new CustomEvent("shift:canvas:state", { detail: { count: 0, canRestore: false } }));
1355
+ };
1356
+ }, [blocks.length, canRestore]);
1357
+ const firstRun = useRef6(true);
1358
+ const saveTimer = useRef6(null);
1359
+ 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("|");
1360
+ const payload = useMemo2(() => layoutSignature ? {
1361
+ savedDay: localDay(),
1362
+ blocks: layoutSignature.split("|").map((seg) => {
1363
+ const [key, width, x, y, w, h] = seg.split(":");
1364
+ return { key, ...width ? { width } : {}, x: Number(x), y: Number(y), w: Number(w), h: Number(h) };
1365
+ })
1366
+ } : null, [layoutSignature]);
1367
+ const payloadRef = useRef6(payload);
1368
+ useEffect6(() => {
1369
+ payloadRef.current = payload;
1370
+ }, [payload]);
1371
+ useEffect6(() => {
1372
+ if (!persist) return;
1373
+ if (firstRun.current) {
1374
+ firstRun.current = false;
1375
+ return;
1376
+ }
1377
+ if (!payloadRef.current) return;
1378
+ if (saveTimer.current) clearTimeout(saveTimer.current);
1379
+ const body = JSON.stringify(payloadRef.current);
1380
+ saveTimer.current = setTimeout(() => {
1381
+ void fetch("/api/canvas-layout", {
1382
+ method: "PUT",
1383
+ headers: { "Content-Type": "application/json" },
1384
+ body
1385
+ }).catch(() => {
1386
+ });
1387
+ }, 700);
1388
+ return () => {
1389
+ if (saveTimer.current) clearTimeout(saveTimer.current);
1390
+ };
1391
+ }, [persist, layoutSignature]);
1392
+ useEffect6(() => {
1393
+ if (!persist) return;
1394
+ const flush = () => {
1395
+ if (!payloadRef.current) return;
1396
+ try {
1397
+ void fetch("/api/canvas-layout", {
1398
+ method: "PUT",
1399
+ headers: { "Content-Type": "application/json" },
1400
+ body: JSON.stringify(payloadRef.current),
1401
+ keepalive: true
1402
+ }).catch(() => {
1403
+ });
1404
+ } catch {
1405
+ }
1406
+ };
1407
+ const onHide = () => {
1408
+ if (document.visibilityState === "hidden") flush();
1409
+ };
1410
+ window.addEventListener("pagehide", flush);
1411
+ document.addEventListener("visibilitychange", onHide);
1412
+ return () => {
1413
+ window.removeEventListener("pagehide", flush);
1414
+ document.removeEventListener("visibilitychange", onHide);
1415
+ };
1416
+ }, [persist]);
1417
+ const api = useMemo2(() => ({
1418
+ blocks,
1419
+ addBlock,
1420
+ removeBlock,
1421
+ resizeBlock,
1422
+ snapTo,
1423
+ snapPreview,
1424
+ previewSnap,
1425
+ clearSnapPreview,
1426
+ snapGhost,
1427
+ moveTo,
1428
+ sizeTo,
1429
+ setRect,
1430
+ resizeRow,
1431
+ bringToFront,
1432
+ resolveCollisions,
1433
+ toggleLock,
1434
+ clear,
1435
+ interacting,
1436
+ setInteracting,
1437
+ activeId,
1438
+ setActiveId,
1439
+ boardWidth
1440
+ }), [blocks, addBlock, removeBlock, resizeBlock, snapTo, snapPreview, previewSnap, clearSnapPreview, snapGhost, moveTo, sizeTo, setRect, resizeRow, bringToFront, resolveCollisions, toggleLock, clear, interacting, activeId, boardWidth]);
1441
+ return /* @__PURE__ */ jsx11(CanvasContext.Provider, { value: api, children });
1442
+ }
1443
+
796
1444
  // src/voice/useHeyShift.ts
797
- import { useCallback, useEffect as useEffect6, useRef as useRef6, useState as useState6 } from "react";
1445
+ import { useCallback as useCallback2, useEffect as useEffect7, useRef as useRef7, useState as useState7 } from "react";
798
1446
  function getSpeechRecognitionCtor() {
799
1447
  const w = window;
800
1448
  return w.SpeechRecognition ?? w.webkitSpeechRecognition;
@@ -807,28 +1455,28 @@ function useHeyShift(config = {}) {
807
1455
  onCommand,
808
1456
  onStateChange
809
1457
  } = config;
810
- const [state, setState] = useState6("idle");
811
- const [transcript, setTranscript] = useState6("");
812
- const [isSupported, setIsSupported] = useState6(false);
813
- const recRef = useRef6(null);
814
- const silenceTimerRef = useRef6(null);
815
- const activeRef = useRef6(false);
816
- const accumulatedRef = useRef6("");
817
- const stateRef = useRef6("idle");
818
- const updateState = useCallback((s) => {
1458
+ const [state, setState] = useState7("idle");
1459
+ const [transcript, setTranscript] = useState7("");
1460
+ const [isSupported, setIsSupported] = useState7(false);
1461
+ const recRef = useRef7(null);
1462
+ const silenceTimerRef = useRef7(null);
1463
+ const activeRef = useRef7(false);
1464
+ const accumulatedRef = useRef7("");
1465
+ const stateRef = useRef7("idle");
1466
+ const updateState = useCallback2((s) => {
819
1467
  stateRef.current = s;
820
1468
  setState(s);
821
1469
  onStateChange?.(s);
822
1470
  }, [onStateChange]);
823
- useEffect6(() => {
1471
+ useEffect7(() => {
824
1472
  const SR = getSpeechRecognitionCtor();
825
1473
  setIsSupported(!!(SR && window.speechSynthesis));
826
1474
  }, []);
827
- const getVoice = useCallback(() => {
1475
+ const getVoice = useCallback2(() => {
828
1476
  const voices = window.speechSynthesis?.getVoices() ?? [];
829
1477
  return voices.find((v) => v.name.toLowerCase().includes("will")) ?? voices.find((v) => v.name.toLowerCase().includes("daniel")) ?? voices.find((v) => v.lang.startsWith("en") && v.name.toLowerCase().includes("male")) ?? voices.find((v) => v.lang.startsWith("en")) ?? voices[0] ?? null;
830
1478
  }, []);
831
- const speak = useCallback(async (text) => {
1479
+ const speak = useCallback2(async (text) => {
832
1480
  if (!window.speechSynthesis) return;
833
1481
  window.speechSynthesis.cancel();
834
1482
  return new Promise((resolve) => {
@@ -858,11 +1506,11 @@ function useHeyShift(config = {}) {
858
1506
  }
859
1507
  });
860
1508
  }, [getVoice, updateState]);
861
- const cancelSpeech = useCallback(() => {
1509
+ const cancelSpeech = useCallback2(() => {
862
1510
  window.speechSynthesis?.cancel();
863
1511
  if (stateRef.current === "speaking") updateState("idle");
864
1512
  }, [updateState]);
865
- const resetSilenceTimer = useCallback((text) => {
1513
+ const resetSilenceTimer = useCallback2((text) => {
866
1514
  if (silenceTimerRef.current) clearTimeout(silenceTimerRef.current);
867
1515
  silenceTimerRef.current = setTimeout(async () => {
868
1516
  if (!activeRef.current || !text.trim()) return;
@@ -883,7 +1531,7 @@ function useHeyShift(config = {}) {
883
1531
  }
884
1532
  }, silenceMs);
885
1533
  }, [silenceMs, onCommand, speak, updateState]);
886
- const startListening = useCallback(() => {
1534
+ const startListening = useCallback2(() => {
887
1535
  const SR = getSpeechRecognitionCtor();
888
1536
  if (!SR || recRef.current) return;
889
1537
  const rec = new SR();
@@ -929,7 +1577,7 @@ function useHeyShift(config = {}) {
929
1577
  rec.start();
930
1578
  recRef.current = rec;
931
1579
  }, [language, wakeWord, resetSilenceTimer, updateState]);
932
- const stopListening = useCallback(() => {
1580
+ const stopListening = useCallback2(() => {
933
1581
  if (silenceTimerRef.current) clearTimeout(silenceTimerRef.current);
934
1582
  const rec = recRef.current;
935
1583
  recRef.current = null;
@@ -939,7 +1587,7 @@ function useHeyShift(config = {}) {
939
1587
  setTranscript("");
940
1588
  updateState("idle");
941
1589
  }, [updateState]);
942
- useEffect6(() => {
1590
+ useEffect7(() => {
943
1591
  return () => {
944
1592
  const rec = recRef.current;
945
1593
  recRef.current = null;
@@ -952,7 +1600,7 @@ function useHeyShift(config = {}) {
952
1600
  }
953
1601
 
954
1602
  // src/voice/ShiftBar.tsx
955
- import { Fragment as Fragment3, jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
1603
+ import { Fragment as Fragment3, jsx as jsx12, jsxs as jsxs7 } from "react/jsx-runtime";
956
1604
  var COLORS = {
957
1605
  idle: "rgba(255,255,255,0.15)",
958
1606
  wake: "#0ed882",
@@ -965,12 +1613,12 @@ function ShiftBar({ state, dots = 3, size = 7, className = "" }) {
965
1613
  const color = COLORS[state];
966
1614
  const isAnimated = state !== "idle" && state !== "error";
967
1615
  return /* @__PURE__ */ jsxs7(Fragment3, { children: [
968
- /* @__PURE__ */ jsx11("style", { children: `
1616
+ /* @__PURE__ */ jsx12("style", { children: `
969
1617
  @keyframes _sb_pulse { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:.35;transform:scale(1.45)} }
970
1618
  @keyframes _sb_wave { 0%,100%{transform:scaleY(1)} 50%{transform:scaleY(1.9)} }
971
1619
  @keyframes _sb_spin { to{transform:rotate(360deg)} }
972
1620
  ` }),
973
- /* @__PURE__ */ jsx11(
1621
+ /* @__PURE__ */ jsx12(
974
1622
  "span",
975
1623
  {
976
1624
  className,
@@ -988,7 +1636,7 @@ function ShiftBar({ state, dots = 3, size = 7, className = "" }) {
988
1636
  animation = `_sb_pulse 1.2s ease-in-out ${i * 0.18}s infinite`;
989
1637
  }
990
1638
  }
991
- return /* @__PURE__ */ jsx11(
1639
+ return /* @__PURE__ */ jsx12(
992
1640
  "span",
993
1641
  {
994
1642
  style: {
@@ -1014,16 +1662,24 @@ function ShiftBar({ state, dots = 3, size = 7, className = "" }) {
1014
1662
  export {
1015
1663
  ACCENT_RGB,
1016
1664
  CanvasLauncher,
1665
+ CanvasProvider,
1017
1666
  CanvasTransition,
1667
+ DEFAULT_H,
1668
+ GAP,
1018
1669
  GenerativeView,
1019
1670
  HolographicShine,
1020
1671
  LiveCard,
1021
1672
  LiveStat,
1673
+ MIN_H,
1674
+ MIN_W,
1022
1675
  PageTransition,
1023
1676
  ScrollGenerateCard,
1024
1677
  ShiftBar,
1025
1678
  ShiftCard,
1026
1679
  TiltCard,
1680
+ WIDTH_SPAN,
1681
+ tallH,
1682
+ useCanvas,
1027
1683
  useGenerate,
1028
1684
  useHeyShift
1029
1685
  };