@allshift/ui 0.3.2 → 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.cjs CHANGED
@@ -33,16 +33,26 @@ var index_exports = {};
33
33
  __export(index_exports, {
34
34
  ACCENT_RGB: () => ACCENT_RGB,
35
35
  CanvasLauncher: () => CanvasLauncher,
36
+ CanvasProvider: () => CanvasProvider,
36
37
  CanvasTransition: () => CanvasTransition,
38
+ DEFAULT_H: () => DEFAULT_H,
39
+ GAP: () => GAP,
37
40
  GenerativeView: () => GenerativeView,
38
41
  HolographicShine: () => HolographicShine,
39
42
  LiveCard: () => LiveCard,
40
43
  LiveStat: () => LiveStat,
44
+ MIN_H: () => MIN_H,
45
+ MIN_W: () => MIN_W,
41
46
  PageTransition: () => PageTransition,
42
47
  ScrollGenerateCard: () => ScrollGenerateCard,
48
+ ShiftBar: () => ShiftBar,
43
49
  ShiftCard: () => ShiftCard,
44
50
  TiltCard: () => TiltCard,
45
- useGenerate: () => useGenerate
51
+ WIDTH_SPAN: () => WIDTH_SPAN,
52
+ tallH: () => tallH,
53
+ useCanvas: () => useCanvas,
54
+ useGenerate: () => useGenerate,
55
+ useHeyShift: () => useHeyShift
46
56
  });
47
57
  module.exports = __toCommonJS(index_exports);
48
58
 
@@ -822,18 +832,895 @@ function PageTransition({ children, className }) {
822
832
  pathname
823
833
  );
824
834
  }
835
+
836
+ // src/canvas/CanvasProvider.tsx
837
+ var import_react10 = require("react");
838
+ var import_jsx_runtime11 = require("react/jsx-runtime");
839
+ var GAP = 20;
840
+ var DEFAULT_H = 420;
841
+ var MIN_W = 240;
842
+ var MIN_H = 200;
843
+ var WIDTH_SPAN = {
844
+ sm: 4,
845
+ // 1/3
846
+ md: 6,
847
+ // 1/2
848
+ lg: 8,
849
+ // 2/3
850
+ xl: 12
851
+ // full
852
+ };
853
+ var CanvasContext = (0, import_react10.createContext)(null);
854
+ function useCanvas() {
855
+ const ctx = (0, import_react10.useContext)(CanvasContext);
856
+ if (!ctx) throw new Error("useCanvas must be used inside <CanvasProvider>");
857
+ return ctx;
858
+ }
859
+ var _seq = 0;
860
+ function nextId() {
861
+ return `blk-${Date.now()}-${_seq++}`;
862
+ }
863
+ function blockFromSaved(s, z) {
864
+ const sb = typeof s === "string" ? { key: s } : s;
865
+ const hasCoords = typeof sb.x === "number" && typeof sb.y === "number" && typeof sb.w === "number";
866
+ return {
867
+ id: nextId(),
868
+ key: sb.key,
869
+ width: sb.width,
870
+ z,
871
+ placed: hasCoords,
872
+ x: sb.x ?? 0,
873
+ y: sb.y ?? 0,
874
+ w: sb.w ?? 0,
875
+ h: sb.h ?? DEFAULT_H
876
+ };
877
+ }
878
+ function colWidth(boardW) {
879
+ return (boardW - GAP * 11) / 12;
880
+ }
881
+ function pxWidth(weight, boardW) {
882
+ return Math.max(MIN_W, weight * colWidth(boardW) + (weight - 1) * GAP);
883
+ }
884
+ function tallH(boardH) {
885
+ return Math.max(460, boardH - 150);
886
+ }
887
+ function carveAround(zone, locked) {
888
+ 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;
889
+ let r = { ...zone };
890
+ for (let i = 0; i < locked.length + 2; i++) {
891
+ const L = locked.find((l) => hit(r, l));
892
+ if (!L) break;
893
+ const ix = Math.max(r.x, L.x), iy = Math.max(r.y, L.y);
894
+ const ir = Math.min(r.x + r.w, L.x + L.w), ib = Math.min(r.y + r.h, L.y + L.h);
895
+ const slabs = [
896
+ { x: r.x, y: r.y, w: ix - r.x, h: r.h },
897
+ // left of the locked block
898
+ { x: ir, y: r.y, w: r.x + r.w - ir, h: r.h },
899
+ // right of it
900
+ { x: r.x, y: r.y, w: r.w, h: iy - r.y },
901
+ // above it
902
+ { x: r.x, y: ib, w: r.w, h: r.y + r.h - ib }
903
+ // below it
904
+ ].filter((s) => s.w > 1 && s.h > 1);
905
+ if (slabs.length === 0) break;
906
+ r = slabs.reduce((a, b) => a.w * a.h >= b.w * b.h ? a : b);
907
+ }
908
+ return r;
909
+ }
910
+ function zoneBlocked(c, locked) {
911
+ if (c.w < MIN_W || c.h < MIN_H) return true;
912
+ 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);
913
+ }
914
+ function snapZones(boardW, H) {
915
+ const halfW = Math.round((boardW - GAP) / 2);
916
+ const halfH = Math.round((H - GAP) / 2);
917
+ const rightX = boardW - halfW;
918
+ const bottomY = H - halfH;
919
+ return {
920
+ full: { x: 0, y: 0, w: boardW, h: H },
921
+ left: { x: 0, y: 0, w: halfW, h: H },
922
+ right: { x: rightX, y: 0, w: halfW, h: H },
923
+ top: { x: 0, y: 0, w: boardW, h: halfH },
924
+ bottom: { x: 0, y: bottomY, w: boardW, h: halfH },
925
+ tl: { x: 0, y: 0, w: halfW, h: halfH },
926
+ tr: { x: rightX, y: 0, w: halfW, h: halfH },
927
+ bl: { x: 0, y: bottomY, w: halfW, h: halfH },
928
+ br: { x: rightX, y: bottomY, w: halfW, h: halfH }
929
+ };
930
+ }
931
+ function tileRect(items, region) {
932
+ const n = items.length;
933
+ if (n === 0) return items;
934
+ const rows = Math.ceil(n / 2);
935
+ const rowH = Math.max(MIN_H, Math.round((region.h - GAP * (rows - 1)) / rows));
936
+ const halfW = Math.round((region.w - GAP) / 2);
937
+ const single = region.w < MIN_W * 2 + GAP;
938
+ return items.map((it, i) => {
939
+ if (single) {
940
+ const h = Math.max(MIN_H, Math.round((region.h - GAP * (n - 1)) / n));
941
+ return { ...it, x: region.x, y: region.y + i * (h + GAP), w: Math.max(MIN_W, region.w), h, placed: true };
942
+ }
943
+ const row = Math.floor(i / 2);
944
+ const lastOdd = row === rows - 1 && n % 2 === 1;
945
+ const y = region.y + row * (rowH + GAP);
946
+ const w = lastOdd ? region.w : halfW;
947
+ const x = lastOdd ? region.x : region.x + i % 2 * (halfW + GAP);
948
+ return { ...it, x, y, w: Math.max(MIN_W, w), h: rowH, placed: true };
949
+ });
950
+ }
951
+ function bestComplement(z, boardW, H) {
952
+ const cands = [
953
+ { x: z.x + z.w + GAP, y: 0, w: boardW - (z.x + z.w + GAP), h: H },
954
+ // right of zone
955
+ { x: 0, y: 0, w: z.x - GAP, h: H },
956
+ // left of zone
957
+ { x: 0, y: z.y + z.h + GAP, w: boardW, h: H - (z.y + z.h + GAP) },
958
+ // below zone
959
+ { x: 0, y: 0, w: boardW, h: z.y - GAP }
960
+ // above zone
961
+ ].filter((c) => c.w >= MIN_W && c.h >= MIN_H);
962
+ if (cands.length === 0) return null;
963
+ return cands.reduce((a, b) => a.w * a.h >= b.w * b.h ? a : b);
964
+ }
965
+ function computeSnap(blocks, id, layout, boardW, H) {
966
+ const target = blocks.find((b) => b.id === id);
967
+ if (!target || target.locked) return null;
968
+ const zone = snapZones(boardW, H)[layout];
969
+ const locked = blocks.filter((b) => b.id !== id && b.locked);
970
+ const tRect = carveAround(zone, locked);
971
+ if (zoneBlocked(tRect, locked)) return null;
972
+ const placements = /* @__PURE__ */ new Map();
973
+ 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)) });
974
+ const others = blocks.filter((b) => b.id !== id && !b.locked);
975
+ if (others.length > 0) {
976
+ const comp = bestComplement(zone, boardW, H);
977
+ if (comp) {
978
+ const region = carveAround(comp, locked);
979
+ if (region.w >= MIN_W && region.h >= MIN_H) {
980
+ 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) }));
981
+ }
982
+ }
983
+ }
984
+ return placements;
985
+ }
986
+ function tileItems(items, boardW, boardH) {
987
+ const n = items.length;
988
+ if (n === 0) return items;
989
+ const availH = Math.max(MIN_H * 2 + GAP, boardH - 150);
990
+ const rows = Math.ceil(n / 2);
991
+ const rowH = Math.max(300, Math.round((availH - GAP * (rows - 1)) / rows));
992
+ const halfW = Math.round((boardW - GAP) / 2);
993
+ const single = boardW < 720;
994
+ return items.map((it, i) => {
995
+ if (single) {
996
+ const h = Math.max(MIN_H, Math.round(tallH(boardH) * 0.72));
997
+ return { ...it, x: 0, y: i * (h + GAP), w: boardW, h };
998
+ }
999
+ const row = Math.floor(i / 2);
1000
+ const lastOdd = row === rows - 1 && n % 2 === 1;
1001
+ const y = row * (rowH + GAP);
1002
+ const w = lastOdd ? boardW : halfW;
1003
+ const x = lastOdd ? 0 : i % 2 * (halfW + GAP);
1004
+ return { ...it, x, y, w, h: rowH };
1005
+ });
1006
+ }
1007
+ function overlaps(a, b) {
1008
+ 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
+ }
1010
+ function pushOut(b, m) {
1011
+ const cands = [
1012
+ { 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 },
1013
+ { x: m.x + m.w + GAP, y: b.y, d: Math.abs(m.x + m.w + GAP - b.x), ok: true },
1014
+ { 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 },
1015
+ { x: b.x, y: m.y + m.h + GAP, d: Math.abs(m.y + m.h + GAP - b.y), ok: true }
1016
+ ].filter((c) => c.ok).sort((p, q) => p.d - q.d);
1017
+ const best = cands[0];
1018
+ return { x: Math.max(0, best.x), y: Math.max(0, best.y) };
1019
+ }
1020
+ function CanvasProvider({
1021
+ children,
1022
+ resolveBlock,
1023
+ initialKeys = [],
1024
+ initialLayout,
1025
+ addKey = null,
1026
+ persist = false,
1027
+ savedSession
1028
+ }) {
1029
+ const resolveRef = (0, import_react10.useRef)(resolveBlock);
1030
+ (0, import_react10.useEffect)(() => {
1031
+ resolveRef.current = resolveBlock;
1032
+ }, [resolveBlock]);
1033
+ const resolve = (key) => resolveRef.current(key);
1034
+ const initial = (0, import_react10.useMemo)(() => {
1035
+ let z = 1;
1036
+ 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++));
1037
+ if (addKey && resolveRef.current(addKey) && !base.some((b) => b.key === addKey)) {
1038
+ base.unshift(blockFromSaved(addKey, z++));
1039
+ }
1040
+ return { base, nextZ: z };
1041
+ }, []);
1042
+ const zRef = (0, import_react10.useRef)(initial.nextZ);
1043
+ const [blocks, setBlocks] = (0, import_react10.useState)(initial.base);
1044
+ const [interacting, setInteracting] = (0, import_react10.useState)(false);
1045
+ const [activeId, setActiveId] = (0, import_react10.useState)(null);
1046
+ const [snapGhost, setSnapGhost] = (0, import_react10.useState)(null);
1047
+ const [boardWidth, setBoardWidth] = (0, import_react10.useState)(1280);
1048
+ const boardRef = (0, import_react10.useRef)(1280);
1049
+ const boardHRef = (0, import_react10.useRef)(820);
1050
+ const persistRef = (0, import_react10.useRef)(persist);
1051
+ (0, import_react10.useEffect)(() => {
1052
+ persistRef.current = persist;
1053
+ }, [persist]);
1054
+ (0, import_react10.useEffect)(() => {
1055
+ const measure = () => {
1056
+ const w = Math.max(360, window.innerWidth - 40);
1057
+ boardRef.current = w;
1058
+ boardHRef.current = Math.max(480, window.innerHeight);
1059
+ setBoardWidth(w);
1060
+ };
1061
+ measure();
1062
+ window.addEventListener("resize", measure);
1063
+ return () => window.removeEventListener("resize", measure);
1064
+ }, []);
1065
+ const layoutDefault = (0, import_react10.useCallback)(
1066
+ (list) => tileItems(list, boardRef.current, boardHRef.current),
1067
+ []
1068
+ );
1069
+ const tiledOnce = (0, import_react10.useRef)(false);
1070
+ (0, import_react10.useEffect)(() => {
1071
+ if (tiledOnce.current) return;
1072
+ tiledOnce.current = true;
1073
+ setBlocks((prev) => prev.length === 0 || prev.some((b) => b.placed) ? prev : layoutDefault(prev));
1074
+ }, [boardWidth]);
1075
+ const addBlock = (0, import_react10.useCallback)((key, context, width, append = false) => {
1076
+ if (!resolve(key)) return;
1077
+ const w = width ?? resolve(key)?.defaultWidth;
1078
+ setBlocks((prev) => {
1079
+ const existing = prev.find((b) => b.key === key);
1080
+ const customized = prev.some((b) => b.placed);
1081
+ const without = prev.filter((b) => b.key !== key);
1082
+ if (customized) {
1083
+ if (append) {
1084
+ const startY = without.length ? Math.max(...without.map((b) => b.y + b.h)) + GAP : 0;
1085
+ 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) };
1086
+ return [...without, nb3];
1087
+ }
1088
+ if (existing?.locked) {
1089
+ return prev.map((b) => b.id === existing.id ? { ...b, z: zRef.current++ } : b);
1090
+ }
1091
+ const boardW = boardRef.current;
1092
+ const H = tallH(boardHRef.current);
1093
+ 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 };
1094
+ if (without.length === 0) return [{ ...nb2, x: 0, y: 0, w: boardW, h: H }];
1095
+ const zone = without.length === 1 ? "left" : "tl";
1096
+ const list2 = [nb2, ...without];
1097
+ const placements = computeSnap(list2, nb2.id, zone, boardW, H);
1098
+ if (placements) {
1099
+ return list2.map((b) => {
1100
+ const p = placements.get(b.id);
1101
+ return p ? { ...b, ...p, placed: true } : b;
1102
+ });
1103
+ }
1104
+ const lockedTop = without.filter((b) => b.locked && b.y < H + GAP);
1105
+ const newY = lockedTop.length ? Math.max(...lockedTop.map((b) => b.y + b.h)) + GAP : 0;
1106
+ const shift = newY + H + GAP;
1107
+ const moved = without.map((b) => b.locked ? b : { ...b, y: b.y + shift });
1108
+ return [{ ...nb2, x: 0, y: newY, w: boardW, h: H }, ...moved];
1109
+ }
1110
+ 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 };
1111
+ const list = append ? [...without, nb] : [nb, ...without];
1112
+ return layoutDefault(list);
1113
+ });
1114
+ if (!append && typeof window !== "undefined") {
1115
+ requestAnimationFrame(() => window.scrollTo({ top: 0, behavior: "smooth" }));
1116
+ }
1117
+ }, [layoutDefault]);
1118
+ const removeBlock = (0, import_react10.useCallback)((id) => {
1119
+ setBlocks((prev) => {
1120
+ const remaining = prev.filter((b) => b.id !== id);
1121
+ return remaining.length > 0 && !remaining.some((b) => b.placed) ? layoutDefault(remaining) : remaining;
1122
+ });
1123
+ }, [layoutDefault]);
1124
+ const resizeBlock = (0, import_react10.useCallback)((id, width) => {
1125
+ setBlocks((prev) => prev.map((b) => b.id === id ? { ...b, width, w: pxWidth(WIDTH_SPAN[width], boardRef.current), placed: true, z: zRef.current++ } : b));
1126
+ }, []);
1127
+ const moveTo = (0, import_react10.useCallback)((id, x, y) => {
1128
+ const boardW = boardRef.current;
1129
+ setBlocks((prev) => prev.map((b) => {
1130
+ if (b.id !== id || b.locked) return b;
1131
+ const nx = Math.max(0, Math.min(Math.round(x), Math.max(0, boardW - b.w)));
1132
+ const ny = Math.max(0, Math.round(y));
1133
+ return { ...b, x: nx, y: ny, placed: true };
1134
+ }));
1135
+ }, []);
1136
+ const sizeTo = (0, import_react10.useCallback)((id, w, h) => {
1137
+ const boardW = boardRef.current;
1138
+ setBlocks((prev) => prev.map((b) => {
1139
+ if (b.id !== id || b.locked) return b;
1140
+ const nw = Math.max(MIN_W, Math.min(Math.round(w), Math.max(MIN_W, boardW - b.x)));
1141
+ const nh = Math.max(MIN_H, Math.round(h));
1142
+ return { ...b, w: nw, h: nh, placed: true };
1143
+ }));
1144
+ }, []);
1145
+ const setRect = (0, import_react10.useCallback)((id, r) => {
1146
+ const boardW = boardRef.current;
1147
+ setBlocks((prev) => prev.map((b) => {
1148
+ if (b.id !== id || b.locked) return b;
1149
+ const nx = Math.max(0, Math.round(r.x));
1150
+ const ny = Math.max(0, Math.round(r.y));
1151
+ let nw = Math.max(MIN_W, Math.round(r.w));
1152
+ const nh = Math.max(MIN_H, Math.round(r.h));
1153
+ if (nx + nw > boardW) nw = Math.max(MIN_W, boardW - nx);
1154
+ return { ...b, x: nx, y: ny, w: nw, h: nh, placed: true };
1155
+ }));
1156
+ }, []);
1157
+ const snapTo = (0, import_react10.useCallback)((id, layout) => {
1158
+ const boardW = boardRef.current;
1159
+ const H = tallH(boardHRef.current);
1160
+ setSnapGhost(null);
1161
+ setBlocks((prev) => {
1162
+ const placements = computeSnap(prev, id, layout, boardW, H);
1163
+ if (!placements) return prev;
1164
+ return prev.map((b) => {
1165
+ const p = placements.get(b.id);
1166
+ if (!p) return b;
1167
+ return { ...b, ...p, placed: true, z: b.id === id ? zRef.current++ : b.z };
1168
+ });
1169
+ });
1170
+ }, []);
1171
+ const snapPreview = (0, import_react10.useCallback)((id, layout) => {
1172
+ const boardW = boardRef.current;
1173
+ const H = tallH(boardHRef.current);
1174
+ const zone = snapZones(boardW, H)[layout];
1175
+ const locked = blocks.filter((b) => b.id !== id && b.locked);
1176
+ const c = carveAround(zone, locked);
1177
+ const box = (r) => ({
1178
+ left: r.x / boardW * 100,
1179
+ top: r.y / H * 100,
1180
+ width: r.w / boardW * 100,
1181
+ height: r.h / H * 100
1182
+ });
1183
+ const placements = computeSnap(blocks, id, layout, boardW, H);
1184
+ const others = placements ? [...placements.entries()].filter(([bid]) => bid !== id).map(([, r]) => box(r)) : [];
1185
+ return { rect: box(c), locked: locked.map((L) => box(L)), others, blocked: zoneBlocked(c, locked) };
1186
+ }, [blocks]);
1187
+ const previewSnap = (0, import_react10.useCallback)((id, layout) => {
1188
+ const boardW = boardRef.current;
1189
+ const H = tallH(boardHRef.current);
1190
+ const placements = computeSnap(blocks, id, layout, boardW, H);
1191
+ if (!placements) {
1192
+ setSnapGhost(null);
1193
+ return;
1194
+ }
1195
+ const target = placements.get(id);
1196
+ if (!target) {
1197
+ setSnapGhost(null);
1198
+ return;
1199
+ }
1200
+ const others = [...placements.entries()].filter(([bid]) => bid !== id).map(([, r]) => r);
1201
+ setSnapGhost({ target, others });
1202
+ }, [blocks]);
1203
+ const clearSnapPreview = (0, import_react10.useCallback)(() => setSnapGhost(null), []);
1204
+ const resizeRow = (0, import_react10.useCallback)((id, x, w, h, origin) => {
1205
+ const boardW = boardRef.current;
1206
+ const M0 = origin.find((b) => b.id === id);
1207
+ if (!M0 || M0.locked) return;
1208
+ const vov = (a, b) => a.y < b.y + b.h && a.y + a.h > b.y;
1209
+ const nh = Math.max(MIN_H, Math.round(h));
1210
+ const packed = /* @__PURE__ */ new Map();
1211
+ let mx = M0.x;
1212
+ let mw;
1213
+ if (Math.round(x) === Math.round(M0.x)) {
1214
+ const rights = origin.filter((b) => b.id !== id && b.x >= M0.x && vov(b, M0)).sort((a, b) => a.x - b.x);
1215
+ const wallIdx = rights.findIndex((b) => b.locked);
1216
+ const wallX = wallIdx >= 0 ? rights[wallIdx].x : boardW + GAP;
1217
+ const pushable = wallIdx >= 0 ? rights.slice(0, wallIdx) : rights;
1218
+ const reserved = pushable.length * (MIN_W + GAP);
1219
+ const mRight = Math.max(M0.x + MIN_W, Math.min(M0.x + Math.round(w), wallX - GAP - reserved));
1220
+ mw = mRight - M0.x;
1221
+ let cursor = mRight + GAP;
1222
+ pushable.forEach((N, i) => {
1223
+ const after = (pushable.length - 1 - i) * (MIN_W + GAP);
1224
+ const nw = Math.max(MIN_W, Math.min(N.w, Math.round(wallX - GAP - cursor - after)));
1225
+ packed.set(N.id, { x: Math.round(cursor), w: nw });
1226
+ cursor += nw + GAP;
1227
+ });
1228
+ } else {
1229
+ const fixedRight = M0.x + M0.w;
1230
+ const lefts = origin.filter((b) => b.id !== id && b.x < M0.x && vov(b, M0)).sort((a, b) => b.x - a.x);
1231
+ const wallIdx = lefts.findIndex((b) => b.locked);
1232
+ const wallRight = wallIdx >= 0 ? lefts[wallIdx].x + lefts[wallIdx].w : -GAP;
1233
+ const pushable = wallIdx >= 0 ? lefts.slice(0, wallIdx) : lefts;
1234
+ const reserved = pushable.length * (MIN_W + GAP);
1235
+ const mLeft = Math.min(fixedRight - MIN_W, Math.max(Math.round(x), wallRight + GAP + reserved));
1236
+ mx = mLeft;
1237
+ mw = fixedRight - mLeft;
1238
+ let cursor = mLeft - GAP;
1239
+ pushable.forEach((N, i) => {
1240
+ const after = (pushable.length - 1 - i) * (MIN_W + GAP);
1241
+ const nw = Math.max(MIN_W, Math.min(N.w, Math.round(cursor - (wallRight + GAP) - after)));
1242
+ const nx = Math.max(wallRight + GAP, Math.round(cursor - nw));
1243
+ packed.set(N.id, { x: nx, w: nw });
1244
+ cursor = nx - GAP;
1245
+ });
1246
+ }
1247
+ setBlocks((prev) => prev.map((b) => {
1248
+ if (b.id === id) return { ...b, x: mx, w: Math.max(MIN_W, mw), h: nh, placed: true };
1249
+ const p = packed.get(b.id);
1250
+ return p ? { ...b, x: p.x, w: p.w, placed: true } : b;
1251
+ }));
1252
+ }, []);
1253
+ const bringToFront = (0, import_react10.useCallback)((id) => {
1254
+ setBlocks((prev) => prev.map((b) => b.id === id ? { ...b, z: zRef.current++ } : b));
1255
+ }, []);
1256
+ const resolveCollisions = (0, import_react10.useCallback)((id) => {
1257
+ setBlocks((prev) => {
1258
+ const moved = prev.find((b) => b.id === id);
1259
+ if (!moved) return prev;
1260
+ const hits = prev.filter((b) => b.id !== id && !b.locked && overlaps(b, moved));
1261
+ if (hits.length === 0) return prev;
1262
+ const cx = moved.x + moved.w / 2;
1263
+ const leftN = hits.filter((b) => b.x + b.w / 2 < cx);
1264
+ const rightN = hits.filter((b) => b.x + b.w / 2 >= cx);
1265
+ if (leftN.length && rightN.length) {
1266
+ const leftEdge = Math.max(...leftN.map((b) => b.x + b.w)) + GAP;
1267
+ const rightEdge = Math.min(...rightN.map((b) => b.x)) - GAP;
1268
+ const gapW = rightEdge - leftEdge;
1269
+ if (gapW >= MIN_W) {
1270
+ return prev.map((b) => b.id === id ? { ...b, x: Math.round(leftEdge), w: Math.round(gapW) } : b);
1271
+ }
1272
+ }
1273
+ const boardW = boardRef.current;
1274
+ let changed = false;
1275
+ const next = prev.map((b) => {
1276
+ if (b.id === id || b.locked || !overlaps(b, moved)) return b;
1277
+ const p = pushOut(b, moved);
1278
+ let nx = p.x;
1279
+ let nw = b.w;
1280
+ if (nx + nw > boardW) nw = Math.max(MIN_W, boardW - nx);
1281
+ if (nx + nw > boardW) nx = Math.max(0, boardW - nw);
1282
+ if (nx === b.x && p.y === b.y && nw === b.w) return b;
1283
+ changed = true;
1284
+ return { ...b, x: nx, y: p.y, w: nw };
1285
+ });
1286
+ return changed ? next : prev;
1287
+ });
1288
+ }, []);
1289
+ const toggleLock = (0, import_react10.useCallback)((id) => {
1290
+ setBlocks((prev) => prev.map((b) => b.id === id ? { ...b, locked: !b.locked } : b));
1291
+ }, []);
1292
+ const restoredRef = (0, import_react10.useRef)(false);
1293
+ const [canRestore, setCanRestore] = (0, import_react10.useState)(false);
1294
+ const restoreSession = (0, import_react10.useCallback)(() => {
1295
+ const saved = savedSession?.blocks;
1296
+ if (!saved || saved.length === 0) return;
1297
+ restoredRef.current = true;
1298
+ setCanRestore(false);
1299
+ const built = saved.filter((b) => b && typeof b.key === "string" && resolve(b.key)).map((b) => blockFromSaved(b, zRef.current++));
1300
+ setBlocks(built.some((b) => b.placed) ? built : layoutDefault(built));
1301
+ }, [savedSession, layoutDefault]);
1302
+ const clear = (0, import_react10.useCallback)((keepKey) => {
1303
+ setBlocks((prev) => keepKey ? prev.filter((b) => b.key === keepKey) : []);
1304
+ }, []);
1305
+ (0, import_react10.useEffect)(() => {
1306
+ function onAdd(e) {
1307
+ const d = e.detail;
1308
+ if (d?.key) addBlock(d.key, d.context, d.width, d.append);
1309
+ }
1310
+ function onClear(e) {
1311
+ const d = e.detail;
1312
+ clear(d?.keepKey);
1313
+ }
1314
+ function onResize(e) {
1315
+ const d = e.detail;
1316
+ if (!d?.key || !d?.width) return;
1317
+ const width = d.width;
1318
+ setBlocks((prev) => {
1319
+ const list = prev.map((b) => b.key === d.key ? { ...b, width } : b);
1320
+ return prev.some((b) => b.placed) ? list : layoutDefault(list);
1321
+ });
1322
+ }
1323
+ function onPlace(e) {
1324
+ const d = e.detail;
1325
+ if (!d?.key || !d?.afterKey) return;
1326
+ setBlocks((prev) => {
1327
+ const srcIdx = prev.findIndex((b) => b.key === d.key);
1328
+ const tgtIdx = prev.findIndex((b) => b.key === d.afterKey);
1329
+ if (srcIdx === -1 || tgtIdx === -1) return prev;
1330
+ const next = [...prev];
1331
+ const [moved] = next.splice(srcIdx, 1);
1332
+ const insertAt = next.findIndex((b) => b.key === d.afterKey) + 1;
1333
+ next.splice(insertAt, 0, moved);
1334
+ return prev.some((b) => b.placed) ? next : layoutDefault(next);
1335
+ });
1336
+ }
1337
+ function onSwap(e) {
1338
+ const d = e.detail;
1339
+ const keys = d?.keys ?? [];
1340
+ const contexts = d?.contexts ?? [];
1341
+ const widths = d?.widths ?? [];
1342
+ const built = keys.filter((k) => resolve(k)).map((key, i) => ({
1343
+ id: nextId(),
1344
+ key,
1345
+ context: contexts[i],
1346
+ width: widths[i] ?? resolve(key)?.defaultWidth,
1347
+ z: zRef.current++,
1348
+ placed: false,
1349
+ x: 0,
1350
+ y: 0,
1351
+ w: 0,
1352
+ h: DEFAULT_H
1353
+ }));
1354
+ setBlocks(layoutDefault(built));
1355
+ }
1356
+ function onRestore() {
1357
+ restoreSession();
1358
+ }
1359
+ window.addEventListener("shift:canvas:add", onAdd);
1360
+ window.addEventListener("shift:canvas:clear", onClear);
1361
+ window.addEventListener("shift:canvas:resize", onResize);
1362
+ window.addEventListener("shift:canvas:place", onPlace);
1363
+ window.addEventListener("shift:canvas:swap", onSwap);
1364
+ window.addEventListener("shift:canvas:restore-session", onRestore);
1365
+ return () => {
1366
+ window.removeEventListener("shift:canvas:add", onAdd);
1367
+ window.removeEventListener("shift:canvas:clear", onClear);
1368
+ window.removeEventListener("shift:canvas:resize", onResize);
1369
+ window.removeEventListener("shift:canvas:place", onPlace);
1370
+ window.removeEventListener("shift:canvas:swap", onSwap);
1371
+ window.removeEventListener("shift:canvas:restore-session", onRestore);
1372
+ };
1373
+ }, [addBlock, clear, layoutDefault, restoreSession]);
1374
+ const localDay = () => {
1375
+ try {
1376
+ return (/* @__PURE__ */ new Date()).toLocaleDateString("en-CA");
1377
+ } catch {
1378
+ return "";
1379
+ }
1380
+ };
1381
+ (0, import_react10.useEffect)(() => {
1382
+ if (!persist || restoredRef.current) return;
1383
+ const saved = savedSession?.blocks;
1384
+ if (!saved || saved.length === 0) return;
1385
+ if (savedSession?.savedDay && savedSession.savedDay === localDay()) {
1386
+ restoreSession();
1387
+ } else {
1388
+ setCanRestore(true);
1389
+ }
1390
+ }, []);
1391
+ (0, import_react10.useEffect)(() => {
1392
+ window.dispatchEvent(new CustomEvent("shift:canvas:state", { detail: { count: blocks.length, canRestore } }));
1393
+ return () => {
1394
+ window.dispatchEvent(new CustomEvent("shift:canvas:state", { detail: { count: 0, canRestore: false } }));
1395
+ };
1396
+ }, [blocks.length, canRestore]);
1397
+ const firstRun = (0, import_react10.useRef)(true);
1398
+ const saveTimer = (0, import_react10.useRef)(null);
1399
+ 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("|");
1400
+ const payload = (0, import_react10.useMemo)(() => layoutSignature ? {
1401
+ savedDay: localDay(),
1402
+ blocks: layoutSignature.split("|").map((seg) => {
1403
+ const [key, width, x, y, w, h] = seg.split(":");
1404
+ return { key, ...width ? { width } : {}, x: Number(x), y: Number(y), w: Number(w), h: Number(h) };
1405
+ })
1406
+ } : null, [layoutSignature]);
1407
+ const payloadRef = (0, import_react10.useRef)(payload);
1408
+ (0, import_react10.useEffect)(() => {
1409
+ payloadRef.current = payload;
1410
+ }, [payload]);
1411
+ (0, import_react10.useEffect)(() => {
1412
+ if (!persist) return;
1413
+ if (firstRun.current) {
1414
+ firstRun.current = false;
1415
+ return;
1416
+ }
1417
+ if (!payloadRef.current) return;
1418
+ if (saveTimer.current) clearTimeout(saveTimer.current);
1419
+ const body = JSON.stringify(payloadRef.current);
1420
+ saveTimer.current = setTimeout(() => {
1421
+ void fetch("/api/canvas-layout", {
1422
+ method: "PUT",
1423
+ headers: { "Content-Type": "application/json" },
1424
+ body
1425
+ }).catch(() => {
1426
+ });
1427
+ }, 700);
1428
+ return () => {
1429
+ if (saveTimer.current) clearTimeout(saveTimer.current);
1430
+ };
1431
+ }, [persist, layoutSignature]);
1432
+ (0, import_react10.useEffect)(() => {
1433
+ if (!persist) return;
1434
+ const flush = () => {
1435
+ if (!payloadRef.current) return;
1436
+ try {
1437
+ void fetch("/api/canvas-layout", {
1438
+ method: "PUT",
1439
+ headers: { "Content-Type": "application/json" },
1440
+ body: JSON.stringify(payloadRef.current),
1441
+ keepalive: true
1442
+ }).catch(() => {
1443
+ });
1444
+ } catch {
1445
+ }
1446
+ };
1447
+ const onHide = () => {
1448
+ if (document.visibilityState === "hidden") flush();
1449
+ };
1450
+ window.addEventListener("pagehide", flush);
1451
+ document.addEventListener("visibilitychange", onHide);
1452
+ return () => {
1453
+ window.removeEventListener("pagehide", flush);
1454
+ document.removeEventListener("visibilitychange", onHide);
1455
+ };
1456
+ }, [persist]);
1457
+ const api = (0, import_react10.useMemo)(() => ({
1458
+ blocks,
1459
+ addBlock,
1460
+ removeBlock,
1461
+ resizeBlock,
1462
+ snapTo,
1463
+ snapPreview,
1464
+ previewSnap,
1465
+ clearSnapPreview,
1466
+ snapGhost,
1467
+ moveTo,
1468
+ sizeTo,
1469
+ setRect,
1470
+ resizeRow,
1471
+ bringToFront,
1472
+ resolveCollisions,
1473
+ toggleLock,
1474
+ clear,
1475
+ interacting,
1476
+ setInteracting,
1477
+ activeId,
1478
+ setActiveId,
1479
+ boardWidth
1480
+ }), [blocks, addBlock, removeBlock, resizeBlock, snapTo, snapPreview, previewSnap, clearSnapPreview, snapGhost, moveTo, sizeTo, setRect, resizeRow, bringToFront, resolveCollisions, toggleLock, clear, interacting, activeId, boardWidth]);
1481
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(CanvasContext.Provider, { value: api, children });
1482
+ }
1483
+
1484
+ // src/voice/useHeyShift.ts
1485
+ var import_react11 = require("react");
1486
+ function getSpeechRecognitionCtor() {
1487
+ const w = window;
1488
+ return w.SpeechRecognition ?? w.webkitSpeechRecognition;
1489
+ }
1490
+ function useHeyShift(config = {}) {
1491
+ const {
1492
+ wakeWord = "hey shift",
1493
+ language = "en-US",
1494
+ silenceMs = 1500,
1495
+ onCommand,
1496
+ onStateChange
1497
+ } = config;
1498
+ const [state, setState] = (0, import_react11.useState)("idle");
1499
+ const [transcript, setTranscript] = (0, import_react11.useState)("");
1500
+ const [isSupported, setIsSupported] = (0, import_react11.useState)(false);
1501
+ const recRef = (0, import_react11.useRef)(null);
1502
+ const silenceTimerRef = (0, import_react11.useRef)(null);
1503
+ const activeRef = (0, import_react11.useRef)(false);
1504
+ const accumulatedRef = (0, import_react11.useRef)("");
1505
+ const stateRef = (0, import_react11.useRef)("idle");
1506
+ const updateState = (0, import_react11.useCallback)((s) => {
1507
+ stateRef.current = s;
1508
+ setState(s);
1509
+ onStateChange?.(s);
1510
+ }, [onStateChange]);
1511
+ (0, import_react11.useEffect)(() => {
1512
+ const SR = getSpeechRecognitionCtor();
1513
+ setIsSupported(!!(SR && window.speechSynthesis));
1514
+ }, []);
1515
+ const getVoice = (0, import_react11.useCallback)(() => {
1516
+ const voices = window.speechSynthesis?.getVoices() ?? [];
1517
+ 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;
1518
+ }, []);
1519
+ const speak = (0, import_react11.useCallback)(async (text) => {
1520
+ if (!window.speechSynthesis) return;
1521
+ window.speechSynthesis.cancel();
1522
+ return new Promise((resolve) => {
1523
+ const utter = new SpeechSynthesisUtterance(text);
1524
+ const setVoiceAndSpeak = () => {
1525
+ const voice = getVoice();
1526
+ if (voice) utter.voice = voice;
1527
+ utter.rate = 1.05;
1528
+ utter.pitch = 0.95;
1529
+ updateState("speaking");
1530
+ utter.onend = () => {
1531
+ updateState("idle");
1532
+ resolve();
1533
+ };
1534
+ utter.onerror = () => {
1535
+ updateState("idle");
1536
+ resolve();
1537
+ };
1538
+ window.speechSynthesis.speak(utter);
1539
+ };
1540
+ if (window.speechSynthesis.getVoices().length > 0) {
1541
+ setVoiceAndSpeak();
1542
+ } else {
1543
+ window.speechSynthesis.onvoiceschanged = () => {
1544
+ setVoiceAndSpeak();
1545
+ };
1546
+ }
1547
+ });
1548
+ }, [getVoice, updateState]);
1549
+ const cancelSpeech = (0, import_react11.useCallback)(() => {
1550
+ window.speechSynthesis?.cancel();
1551
+ if (stateRef.current === "speaking") updateState("idle");
1552
+ }, [updateState]);
1553
+ const resetSilenceTimer = (0, import_react11.useCallback)((text) => {
1554
+ if (silenceTimerRef.current) clearTimeout(silenceTimerRef.current);
1555
+ silenceTimerRef.current = setTimeout(async () => {
1556
+ if (!activeRef.current || !text.trim()) return;
1557
+ activeRef.current = false;
1558
+ accumulatedRef.current = "";
1559
+ setTranscript("");
1560
+ updateState("thinking");
1561
+ try {
1562
+ const response = await onCommand?.(text.trim());
1563
+ if (response) {
1564
+ await speak(response);
1565
+ } else {
1566
+ updateState("idle");
1567
+ }
1568
+ } catch {
1569
+ updateState("error");
1570
+ setTimeout(() => updateState("idle"), 2e3);
1571
+ }
1572
+ }, silenceMs);
1573
+ }, [silenceMs, onCommand, speak, updateState]);
1574
+ const startListening = (0, import_react11.useCallback)(() => {
1575
+ const SR = getSpeechRecognitionCtor();
1576
+ if (!SR || recRef.current) return;
1577
+ const rec = new SR();
1578
+ rec.continuous = true;
1579
+ rec.interimResults = true;
1580
+ rec.lang = language;
1581
+ rec.onresult = (event) => {
1582
+ for (let i = event.resultIndex; i < event.results.length; i++) {
1583
+ const t = event.results[i][0].transcript.trim().toLowerCase();
1584
+ const isFinal = event.results[i].isFinal;
1585
+ if (!activeRef.current) {
1586
+ if (t.includes(wakeWord.toLowerCase())) {
1587
+ activeRef.current = true;
1588
+ accumulatedRef.current = "";
1589
+ setTranscript("");
1590
+ updateState("wake");
1591
+ setTimeout(() => {
1592
+ if (activeRef.current) updateState("listening");
1593
+ }, 300);
1594
+ }
1595
+ } else {
1596
+ const raw = event.results[i][0].transcript;
1597
+ if (isFinal) {
1598
+ accumulatedRef.current += " " + raw;
1599
+ }
1600
+ const display = (accumulatedRef.current + " " + (isFinal ? "" : raw)).trim();
1601
+ setTranscript(display);
1602
+ resetSilenceTimer(display);
1603
+ }
1604
+ }
1605
+ };
1606
+ rec.onend = () => {
1607
+ if (recRef.current) {
1608
+ setTimeout(() => {
1609
+ recRef.current?.start();
1610
+ }, 150);
1611
+ }
1612
+ };
1613
+ rec.onerror = (event) => {
1614
+ if (event.error === "no-speech") return;
1615
+ if (event.error === "not-allowed") updateState("error");
1616
+ };
1617
+ rec.start();
1618
+ recRef.current = rec;
1619
+ }, [language, wakeWord, resetSilenceTimer, updateState]);
1620
+ const stopListening = (0, import_react11.useCallback)(() => {
1621
+ if (silenceTimerRef.current) clearTimeout(silenceTimerRef.current);
1622
+ const rec = recRef.current;
1623
+ recRef.current = null;
1624
+ rec?.stop();
1625
+ activeRef.current = false;
1626
+ accumulatedRef.current = "";
1627
+ setTranscript("");
1628
+ updateState("idle");
1629
+ }, [updateState]);
1630
+ (0, import_react11.useEffect)(() => {
1631
+ return () => {
1632
+ const rec = recRef.current;
1633
+ recRef.current = null;
1634
+ rec?.stop();
1635
+ if (silenceTimerRef.current) clearTimeout(silenceTimerRef.current);
1636
+ window.speechSynthesis?.cancel();
1637
+ };
1638
+ }, []);
1639
+ return { state, transcript, startListening, stopListening, speak, cancelSpeech, isSupported };
1640
+ }
1641
+
1642
+ // src/voice/ShiftBar.tsx
1643
+ var import_jsx_runtime12 = require("react/jsx-runtime");
1644
+ var COLORS = {
1645
+ idle: "rgba(255,255,255,0.15)",
1646
+ wake: "#0ed882",
1647
+ listening: "#0ed882",
1648
+ thinking: "#22d3ee",
1649
+ speaking: "#a78bfa",
1650
+ error: "#f87171"
1651
+ };
1652
+ function ShiftBar({ state, dots = 3, size = 7, className = "" }) {
1653
+ const color = COLORS[state];
1654
+ const isAnimated = state !== "idle" && state !== "error";
1655
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_jsx_runtime12.Fragment, { children: [
1656
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("style", { children: `
1657
+ @keyframes _sb_pulse { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:.35;transform:scale(1.45)} }
1658
+ @keyframes _sb_wave { 0%,100%{transform:scaleY(1)} 50%{transform:scaleY(1.9)} }
1659
+ @keyframes _sb_spin { to{transform:rotate(360deg)} }
1660
+ ` }),
1661
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1662
+ "span",
1663
+ {
1664
+ className,
1665
+ role: "status",
1666
+ "aria-label": `Shift: ${state}`,
1667
+ style: { display: "inline-flex", alignItems: "center", gap: Math.round(size * 0.6) },
1668
+ children: Array.from({ length: dots }).map((_, i) => {
1669
+ let animation;
1670
+ if (isAnimated) {
1671
+ if (state === "thinking") {
1672
+ animation = `_sb_spin 1s linear infinite`;
1673
+ } else if (state === "speaking") {
1674
+ animation = `_sb_wave .7s ease-in-out ${i * 0.12}s infinite`;
1675
+ } else {
1676
+ animation = `_sb_pulse 1.2s ease-in-out ${i * 0.18}s infinite`;
1677
+ }
1678
+ }
1679
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1680
+ "span",
1681
+ {
1682
+ style: {
1683
+ display: "inline-block",
1684
+ width: state === "thinking" ? size * 1.6 : size,
1685
+ height: size,
1686
+ borderRadius: state === "thinking" ? "50%" : "50%",
1687
+ background: state === "thinking" ? "transparent" : color,
1688
+ border: state === "thinking" ? `2px solid ${color}` : "none",
1689
+ borderTopColor: state === "thinking" ? "transparent" : void 0,
1690
+ transition: "background .3s ease, border-color .3s ease",
1691
+ animation,
1692
+ transformOrigin: "50% 50%"
1693
+ }
1694
+ },
1695
+ i
1696
+ );
1697
+ })
1698
+ }
1699
+ )
1700
+ ] });
1701
+ }
825
1702
  // Annotate the CommonJS export names for ESM import in node:
826
1703
  0 && (module.exports = {
827
1704
  ACCENT_RGB,
828
1705
  CanvasLauncher,
1706
+ CanvasProvider,
829
1707
  CanvasTransition,
1708
+ DEFAULT_H,
1709
+ GAP,
830
1710
  GenerativeView,
831
1711
  HolographicShine,
832
1712
  LiveCard,
833
1713
  LiveStat,
1714
+ MIN_H,
1715
+ MIN_W,
834
1716
  PageTransition,
835
1717
  ScrollGenerateCard,
1718
+ ShiftBar,
836
1719
  ShiftCard,
837
1720
  TiltCard,
838
- useGenerate
1721
+ WIDTH_SPAN,
1722
+ tallH,
1723
+ useCanvas,
1724
+ useGenerate,
1725
+ useHeyShift
839
1726
  });