@allshift/ui 0.4.0 → 0.5.1
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 +698 -23
- package/dist/index.d.cts +108 -1
- package/dist/index.d.ts +108 -1
- package/dist/index.js +689 -22
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -793,8 +793,667 @@ 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
|
+
setBlocks((prev) => {
|
|
1021
|
+
let changed = false;
|
|
1022
|
+
const next = prev.map((b) => {
|
|
1023
|
+
const bw = Math.min(b.w, w);
|
|
1024
|
+
const bx = Math.max(0, Math.min(b.x, w - bw));
|
|
1025
|
+
if (bw === b.w && bx === b.x) return b;
|
|
1026
|
+
changed = true;
|
|
1027
|
+
return { ...b, w: bw, x: bx };
|
|
1028
|
+
});
|
|
1029
|
+
return changed ? next : prev;
|
|
1030
|
+
});
|
|
1031
|
+
};
|
|
1032
|
+
measure();
|
|
1033
|
+
window.addEventListener("resize", measure);
|
|
1034
|
+
return () => window.removeEventListener("resize", measure);
|
|
1035
|
+
}, []);
|
|
1036
|
+
const layoutDefault = useCallback(
|
|
1037
|
+
(list) => tileItems(list, boardRef.current, boardHRef.current),
|
|
1038
|
+
[]
|
|
1039
|
+
);
|
|
1040
|
+
const tiledOnce = useRef6(false);
|
|
1041
|
+
useEffect6(() => {
|
|
1042
|
+
if (tiledOnce.current) return;
|
|
1043
|
+
tiledOnce.current = true;
|
|
1044
|
+
setBlocks((prev) => prev.length === 0 || prev.some((b) => b.placed) ? prev : layoutDefault(prev));
|
|
1045
|
+
}, [boardWidth]);
|
|
1046
|
+
const addBlock = useCallback((key, context, width, append = false) => {
|
|
1047
|
+
if (!resolve(key)) return;
|
|
1048
|
+
const w = width ?? resolve(key)?.defaultWidth;
|
|
1049
|
+
setBlocks((prev) => {
|
|
1050
|
+
const existing = prev.find((b) => b.key === key);
|
|
1051
|
+
const customized = prev.some((b) => b.placed);
|
|
1052
|
+
const without = prev.filter((b) => b.key !== key);
|
|
1053
|
+
if (customized) {
|
|
1054
|
+
if (append) {
|
|
1055
|
+
const startY = without.length ? Math.max(...without.map((b) => b.y + b.h)) + GAP : 0;
|
|
1056
|
+
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) };
|
|
1057
|
+
return [...without, nb3];
|
|
1058
|
+
}
|
|
1059
|
+
if (existing?.locked) {
|
|
1060
|
+
return prev.map((b) => b.id === existing.id ? { ...b, z: zRef.current++ } : b);
|
|
1061
|
+
}
|
|
1062
|
+
const boardW = boardRef.current;
|
|
1063
|
+
const H = tallH(boardHRef.current);
|
|
1064
|
+
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 };
|
|
1065
|
+
if (without.length === 0) return [{ ...nb2, x: 0, y: 0, w: boardW, h: H }];
|
|
1066
|
+
const zone = without.length === 1 ? "left" : "tl";
|
|
1067
|
+
const list2 = [nb2, ...without];
|
|
1068
|
+
const placements = computeSnap(list2, nb2.id, zone, boardW, H);
|
|
1069
|
+
if (placements) {
|
|
1070
|
+
return list2.map((b) => {
|
|
1071
|
+
const p = placements.get(b.id);
|
|
1072
|
+
return p ? { ...b, ...p, placed: true } : b;
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
const lockedTop = without.filter((b) => b.locked && b.y < H + GAP);
|
|
1076
|
+
const newY = lockedTop.length ? Math.max(...lockedTop.map((b) => b.y + b.h)) + GAP : 0;
|
|
1077
|
+
const shift = newY + H + GAP;
|
|
1078
|
+
const moved = without.map((b) => b.locked ? b : { ...b, y: b.y + shift });
|
|
1079
|
+
return [{ ...nb2, x: 0, y: newY, w: boardW, h: H }, ...moved];
|
|
1080
|
+
}
|
|
1081
|
+
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 };
|
|
1082
|
+
const list = append ? [...without, nb] : [nb, ...without];
|
|
1083
|
+
return layoutDefault(list);
|
|
1084
|
+
});
|
|
1085
|
+
if (!append && typeof window !== "undefined") {
|
|
1086
|
+
requestAnimationFrame(() => window.scrollTo({ top: 0, behavior: "smooth" }));
|
|
1087
|
+
}
|
|
1088
|
+
}, [layoutDefault]);
|
|
1089
|
+
const removeBlock = useCallback((id) => {
|
|
1090
|
+
setBlocks((prev) => {
|
|
1091
|
+
const remaining = prev.filter((b) => b.id !== id);
|
|
1092
|
+
return remaining.length > 0 && !remaining.some((b) => b.placed) ? layoutDefault(remaining) : remaining;
|
|
1093
|
+
});
|
|
1094
|
+
}, [layoutDefault]);
|
|
1095
|
+
const resizeBlock = useCallback((id, width) => {
|
|
1096
|
+
setBlocks((prev) => prev.map((b) => b.id === id ? { ...b, width, w: pxWidth(WIDTH_SPAN[width], boardRef.current), placed: true, z: zRef.current++ } : b));
|
|
1097
|
+
}, []);
|
|
1098
|
+
const moveTo = useCallback((id, x, y) => {
|
|
1099
|
+
const boardW = boardRef.current;
|
|
1100
|
+
setBlocks((prev) => prev.map((b) => {
|
|
1101
|
+
if (b.id !== id || b.locked) return b;
|
|
1102
|
+
const nx = Math.max(0, Math.min(Math.round(x), Math.max(0, boardW - b.w)));
|
|
1103
|
+
const ny = Math.max(0, Math.round(y));
|
|
1104
|
+
return { ...b, x: nx, y: ny, placed: true };
|
|
1105
|
+
}));
|
|
1106
|
+
}, []);
|
|
1107
|
+
const sizeTo = useCallback((id, w, h) => {
|
|
1108
|
+
const boardW = boardRef.current;
|
|
1109
|
+
setBlocks((prev) => prev.map((b) => {
|
|
1110
|
+
if (b.id !== id || b.locked) return b;
|
|
1111
|
+
const nw = Math.max(MIN_W, Math.min(Math.round(w), Math.max(MIN_W, boardW - b.x)));
|
|
1112
|
+
const nh = Math.max(MIN_H, Math.round(h));
|
|
1113
|
+
return { ...b, w: nw, h: nh, placed: true };
|
|
1114
|
+
}));
|
|
1115
|
+
}, []);
|
|
1116
|
+
const setRect = useCallback((id, r) => {
|
|
1117
|
+
const boardW = boardRef.current;
|
|
1118
|
+
setBlocks((prev) => prev.map((b) => {
|
|
1119
|
+
if (b.id !== id || b.locked) return b;
|
|
1120
|
+
const nx = Math.max(0, Math.round(r.x));
|
|
1121
|
+
const ny = Math.max(0, Math.round(r.y));
|
|
1122
|
+
let nw = Math.max(MIN_W, Math.round(r.w));
|
|
1123
|
+
const nh = Math.max(MIN_H, Math.round(r.h));
|
|
1124
|
+
if (nx + nw > boardW) nw = Math.max(MIN_W, boardW - nx);
|
|
1125
|
+
return { ...b, x: nx, y: ny, w: nw, h: nh, placed: true };
|
|
1126
|
+
}));
|
|
1127
|
+
}, []);
|
|
1128
|
+
const snapTo = useCallback((id, layout) => {
|
|
1129
|
+
const boardW = boardRef.current;
|
|
1130
|
+
const H = tallH(boardHRef.current);
|
|
1131
|
+
setSnapGhost(null);
|
|
1132
|
+
setBlocks((prev) => {
|
|
1133
|
+
const placements = computeSnap(prev, id, layout, boardW, H);
|
|
1134
|
+
if (!placements) return prev;
|
|
1135
|
+
return prev.map((b) => {
|
|
1136
|
+
const p = placements.get(b.id);
|
|
1137
|
+
if (!p) return b;
|
|
1138
|
+
return { ...b, ...p, placed: true, z: b.id === id ? zRef.current++ : b.z };
|
|
1139
|
+
});
|
|
1140
|
+
});
|
|
1141
|
+
}, []);
|
|
1142
|
+
const snapPreview = useCallback((id, layout) => {
|
|
1143
|
+
const boardW = boardRef.current;
|
|
1144
|
+
const H = tallH(boardHRef.current);
|
|
1145
|
+
const zone = snapZones(boardW, H)[layout];
|
|
1146
|
+
const locked = blocks.filter((b) => b.id !== id && b.locked);
|
|
1147
|
+
const c = carveAround(zone, locked);
|
|
1148
|
+
const box = (r) => ({
|
|
1149
|
+
left: r.x / boardW * 100,
|
|
1150
|
+
top: r.y / H * 100,
|
|
1151
|
+
width: r.w / boardW * 100,
|
|
1152
|
+
height: r.h / H * 100
|
|
1153
|
+
});
|
|
1154
|
+
const placements = computeSnap(blocks, id, layout, boardW, H);
|
|
1155
|
+
const others = placements ? [...placements.entries()].filter(([bid]) => bid !== id).map(([, r]) => box(r)) : [];
|
|
1156
|
+
return { rect: box(c), locked: locked.map((L) => box(L)), others, blocked: zoneBlocked(c, locked) };
|
|
1157
|
+
}, [blocks]);
|
|
1158
|
+
const previewSnap = useCallback((id, layout) => {
|
|
1159
|
+
const boardW = boardRef.current;
|
|
1160
|
+
const H = tallH(boardHRef.current);
|
|
1161
|
+
const placements = computeSnap(blocks, id, layout, boardW, H);
|
|
1162
|
+
if (!placements) {
|
|
1163
|
+
setSnapGhost(null);
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
const target = placements.get(id);
|
|
1167
|
+
if (!target) {
|
|
1168
|
+
setSnapGhost(null);
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
const others = [...placements.entries()].filter(([bid]) => bid !== id).map(([, r]) => r);
|
|
1172
|
+
setSnapGhost({ target, others });
|
|
1173
|
+
}, [blocks]);
|
|
1174
|
+
const clearSnapPreview = useCallback(() => setSnapGhost(null), []);
|
|
1175
|
+
const resizeRow = useCallback((id, x, w, h, origin) => {
|
|
1176
|
+
const boardW = boardRef.current;
|
|
1177
|
+
const M0 = origin.find((b) => b.id === id);
|
|
1178
|
+
if (!M0 || M0.locked) return;
|
|
1179
|
+
const vov = (a, b) => a.y < b.y + b.h && a.y + a.h > b.y;
|
|
1180
|
+
const nh = Math.max(MIN_H, Math.round(h));
|
|
1181
|
+
const packed = /* @__PURE__ */ new Map();
|
|
1182
|
+
let mx = M0.x;
|
|
1183
|
+
let mw;
|
|
1184
|
+
if (Math.round(x) === Math.round(M0.x)) {
|
|
1185
|
+
const rights = origin.filter((b) => b.id !== id && b.x >= M0.x && vov(b, M0)).sort((a, b) => a.x - b.x);
|
|
1186
|
+
const wallIdx = rights.findIndex((b) => b.locked);
|
|
1187
|
+
const wallX = wallIdx >= 0 ? rights[wallIdx].x : boardW + GAP;
|
|
1188
|
+
const pushable = wallIdx >= 0 ? rights.slice(0, wallIdx) : rights;
|
|
1189
|
+
const reserved = pushable.length * (MIN_W + GAP);
|
|
1190
|
+
const mRight = Math.max(M0.x + MIN_W, Math.min(M0.x + Math.round(w), wallX - GAP - reserved));
|
|
1191
|
+
mw = mRight - M0.x;
|
|
1192
|
+
let cursor = mRight + GAP;
|
|
1193
|
+
pushable.forEach((N, i) => {
|
|
1194
|
+
const after = (pushable.length - 1 - i) * (MIN_W + GAP);
|
|
1195
|
+
const nw = Math.max(MIN_W, Math.min(N.w, Math.round(wallX - GAP - cursor - after)));
|
|
1196
|
+
packed.set(N.id, { x: Math.round(cursor), w: nw });
|
|
1197
|
+
cursor += nw + GAP;
|
|
1198
|
+
});
|
|
1199
|
+
} else {
|
|
1200
|
+
const fixedRight = M0.x + M0.w;
|
|
1201
|
+
const lefts = origin.filter((b) => b.id !== id && b.x < M0.x && vov(b, M0)).sort((a, b) => b.x - a.x);
|
|
1202
|
+
const wallIdx = lefts.findIndex((b) => b.locked);
|
|
1203
|
+
const wallRight = wallIdx >= 0 ? lefts[wallIdx].x + lefts[wallIdx].w : -GAP;
|
|
1204
|
+
const pushable = wallIdx >= 0 ? lefts.slice(0, wallIdx) : lefts;
|
|
1205
|
+
const reserved = pushable.length * (MIN_W + GAP);
|
|
1206
|
+
const mLeft = Math.min(fixedRight - MIN_W, Math.max(Math.round(x), wallRight + GAP + reserved));
|
|
1207
|
+
mx = mLeft;
|
|
1208
|
+
mw = fixedRight - mLeft;
|
|
1209
|
+
let cursor = mLeft - GAP;
|
|
1210
|
+
pushable.forEach((N, i) => {
|
|
1211
|
+
const after = (pushable.length - 1 - i) * (MIN_W + GAP);
|
|
1212
|
+
const nw = Math.max(MIN_W, Math.min(N.w, Math.round(cursor - (wallRight + GAP) - after)));
|
|
1213
|
+
const nx = Math.max(wallRight + GAP, Math.round(cursor - nw));
|
|
1214
|
+
packed.set(N.id, { x: nx, w: nw });
|
|
1215
|
+
cursor = nx - GAP;
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
setBlocks((prev) => prev.map((b) => {
|
|
1219
|
+
if (b.id === id) return { ...b, x: mx, w: Math.max(MIN_W, mw), h: nh, placed: true };
|
|
1220
|
+
const p = packed.get(b.id);
|
|
1221
|
+
return p ? { ...b, x: p.x, w: p.w, placed: true } : b;
|
|
1222
|
+
}));
|
|
1223
|
+
}, []);
|
|
1224
|
+
const bringToFront = useCallback((id) => {
|
|
1225
|
+
setBlocks((prev) => prev.map((b) => b.id === id ? { ...b, z: zRef.current++ } : b));
|
|
1226
|
+
}, []);
|
|
1227
|
+
const resolveCollisions = useCallback((id) => {
|
|
1228
|
+
setBlocks((prev) => {
|
|
1229
|
+
const moved = prev.find((b) => b.id === id);
|
|
1230
|
+
if (!moved) return prev;
|
|
1231
|
+
const hits = prev.filter((b) => b.id !== id && !b.locked && overlaps(b, moved));
|
|
1232
|
+
if (hits.length === 0) return prev;
|
|
1233
|
+
const cx = moved.x + moved.w / 2;
|
|
1234
|
+
const leftN = hits.filter((b) => b.x + b.w / 2 < cx);
|
|
1235
|
+
const rightN = hits.filter((b) => b.x + b.w / 2 >= cx);
|
|
1236
|
+
if (leftN.length && rightN.length) {
|
|
1237
|
+
const leftEdge = Math.max(...leftN.map((b) => b.x + b.w)) + GAP;
|
|
1238
|
+
const rightEdge = Math.min(...rightN.map((b) => b.x)) - GAP;
|
|
1239
|
+
const gapW = rightEdge - leftEdge;
|
|
1240
|
+
if (gapW >= MIN_W) {
|
|
1241
|
+
return prev.map((b) => b.id === id ? { ...b, x: Math.round(leftEdge), w: Math.round(gapW) } : b);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
const boardW = boardRef.current;
|
|
1245
|
+
let changed = false;
|
|
1246
|
+
const next = prev.map((b) => {
|
|
1247
|
+
if (b.id === id || b.locked || !overlaps(b, moved)) return b;
|
|
1248
|
+
const p = pushOut(b, moved);
|
|
1249
|
+
let nx = p.x;
|
|
1250
|
+
let nw = b.w;
|
|
1251
|
+
if (nx + nw > boardW) nw = Math.max(MIN_W, boardW - nx);
|
|
1252
|
+
if (nx + nw > boardW) nx = Math.max(0, boardW - nw);
|
|
1253
|
+
if (nx === b.x && p.y === b.y && nw === b.w) return b;
|
|
1254
|
+
changed = true;
|
|
1255
|
+
return { ...b, x: nx, y: p.y, w: nw };
|
|
1256
|
+
});
|
|
1257
|
+
return changed ? next : prev;
|
|
1258
|
+
});
|
|
1259
|
+
}, []);
|
|
1260
|
+
const toggleLock = useCallback((id) => {
|
|
1261
|
+
setBlocks((prev) => prev.map((b) => b.id === id ? { ...b, locked: !b.locked } : b));
|
|
1262
|
+
}, []);
|
|
1263
|
+
const restoredRef = useRef6(false);
|
|
1264
|
+
const [canRestore, setCanRestore] = useState6(false);
|
|
1265
|
+
const restoreSession = useCallback(() => {
|
|
1266
|
+
const saved = savedSession?.blocks;
|
|
1267
|
+
if (!saved || saved.length === 0) return;
|
|
1268
|
+
restoredRef.current = true;
|
|
1269
|
+
setCanRestore(false);
|
|
1270
|
+
const built = saved.filter((b) => b && typeof b.key === "string" && resolve(b.key)).map((b) => blockFromSaved(b, zRef.current++));
|
|
1271
|
+
setBlocks(built.some((b) => b.placed) ? built : layoutDefault(built));
|
|
1272
|
+
}, [savedSession, layoutDefault]);
|
|
1273
|
+
const clear = useCallback((keepKey) => {
|
|
1274
|
+
setBlocks((prev) => keepKey ? prev.filter((b) => b.key === keepKey) : []);
|
|
1275
|
+
}, []);
|
|
1276
|
+
useEffect6(() => {
|
|
1277
|
+
function onAdd(e) {
|
|
1278
|
+
const d = e.detail;
|
|
1279
|
+
if (d?.key) addBlock(d.key, d.context, d.width, d.append);
|
|
1280
|
+
}
|
|
1281
|
+
function onClear(e) {
|
|
1282
|
+
const d = e.detail;
|
|
1283
|
+
clear(d?.keepKey);
|
|
1284
|
+
}
|
|
1285
|
+
function onResize(e) {
|
|
1286
|
+
const d = e.detail;
|
|
1287
|
+
if (!d?.key || !d?.width) return;
|
|
1288
|
+
const width = d.width;
|
|
1289
|
+
setBlocks((prev) => {
|
|
1290
|
+
const list = prev.map((b) => b.key === d.key ? { ...b, width } : b);
|
|
1291
|
+
return prev.some((b) => b.placed) ? list : layoutDefault(list);
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
function onPlace(e) {
|
|
1295
|
+
const d = e.detail;
|
|
1296
|
+
if (!d?.key || !d?.afterKey) return;
|
|
1297
|
+
setBlocks((prev) => {
|
|
1298
|
+
const srcIdx = prev.findIndex((b) => b.key === d.key);
|
|
1299
|
+
const tgtIdx = prev.findIndex((b) => b.key === d.afterKey);
|
|
1300
|
+
if (srcIdx === -1 || tgtIdx === -1) return prev;
|
|
1301
|
+
const next = [...prev];
|
|
1302
|
+
const [moved] = next.splice(srcIdx, 1);
|
|
1303
|
+
const insertAt = next.findIndex((b) => b.key === d.afterKey) + 1;
|
|
1304
|
+
next.splice(insertAt, 0, moved);
|
|
1305
|
+
return prev.some((b) => b.placed) ? next : layoutDefault(next);
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
function onSwap(e) {
|
|
1309
|
+
const d = e.detail;
|
|
1310
|
+
const keys = d?.keys ?? [];
|
|
1311
|
+
const contexts = d?.contexts ?? [];
|
|
1312
|
+
const widths = d?.widths ?? [];
|
|
1313
|
+
const built = keys.filter((k) => resolve(k)).map((key, i) => ({
|
|
1314
|
+
id: nextId(),
|
|
1315
|
+
key,
|
|
1316
|
+
context: contexts[i],
|
|
1317
|
+
width: widths[i] ?? resolve(key)?.defaultWidth,
|
|
1318
|
+
z: zRef.current++,
|
|
1319
|
+
placed: false,
|
|
1320
|
+
x: 0,
|
|
1321
|
+
y: 0,
|
|
1322
|
+
w: 0,
|
|
1323
|
+
h: DEFAULT_H
|
|
1324
|
+
}));
|
|
1325
|
+
setBlocks(layoutDefault(built));
|
|
1326
|
+
}
|
|
1327
|
+
function onRestore() {
|
|
1328
|
+
restoreSession();
|
|
1329
|
+
}
|
|
1330
|
+
window.addEventListener("shift:canvas:add", onAdd);
|
|
1331
|
+
window.addEventListener("shift:canvas:clear", onClear);
|
|
1332
|
+
window.addEventListener("shift:canvas:resize", onResize);
|
|
1333
|
+
window.addEventListener("shift:canvas:place", onPlace);
|
|
1334
|
+
window.addEventListener("shift:canvas:swap", onSwap);
|
|
1335
|
+
window.addEventListener("shift:canvas:restore-session", onRestore);
|
|
1336
|
+
return () => {
|
|
1337
|
+
window.removeEventListener("shift:canvas:add", onAdd);
|
|
1338
|
+
window.removeEventListener("shift:canvas:clear", onClear);
|
|
1339
|
+
window.removeEventListener("shift:canvas:resize", onResize);
|
|
1340
|
+
window.removeEventListener("shift:canvas:place", onPlace);
|
|
1341
|
+
window.removeEventListener("shift:canvas:swap", onSwap);
|
|
1342
|
+
window.removeEventListener("shift:canvas:restore-session", onRestore);
|
|
1343
|
+
};
|
|
1344
|
+
}, [addBlock, clear, layoutDefault, restoreSession]);
|
|
1345
|
+
const localDay = () => {
|
|
1346
|
+
try {
|
|
1347
|
+
return (/* @__PURE__ */ new Date()).toLocaleDateString("en-CA");
|
|
1348
|
+
} catch {
|
|
1349
|
+
return "";
|
|
1350
|
+
}
|
|
1351
|
+
};
|
|
1352
|
+
useEffect6(() => {
|
|
1353
|
+
if (!persist || restoredRef.current) return;
|
|
1354
|
+
const saved = savedSession?.blocks;
|
|
1355
|
+
if (!saved || saved.length === 0) return;
|
|
1356
|
+
if (savedSession?.savedDay && savedSession.savedDay === localDay()) {
|
|
1357
|
+
restoreSession();
|
|
1358
|
+
} else {
|
|
1359
|
+
setCanRestore(true);
|
|
1360
|
+
}
|
|
1361
|
+
}, []);
|
|
1362
|
+
useEffect6(() => {
|
|
1363
|
+
window.dispatchEvent(new CustomEvent("shift:canvas:state", { detail: { count: blocks.length, canRestore } }));
|
|
1364
|
+
return () => {
|
|
1365
|
+
window.dispatchEvent(new CustomEvent("shift:canvas:state", { detail: { count: 0, canRestore: false } }));
|
|
1366
|
+
};
|
|
1367
|
+
}, [blocks.length, canRestore]);
|
|
1368
|
+
const firstRun = useRef6(true);
|
|
1369
|
+
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("|");
|
|
1371
|
+
const payload = useMemo2(() => layoutSignature ? {
|
|
1372
|
+
savedDay: localDay(),
|
|
1373
|
+
blocks: layoutSignature.split("|").map((seg) => {
|
|
1374
|
+
const [key, width, x, y, w, h] = seg.split(":");
|
|
1375
|
+
return { key, ...width ? { width } : {}, x: Number(x), y: Number(y), w: Number(w), h: Number(h) };
|
|
1376
|
+
})
|
|
1377
|
+
} : null, [layoutSignature]);
|
|
1378
|
+
const payloadRef = useRef6(payload);
|
|
1379
|
+
useEffect6(() => {
|
|
1380
|
+
payloadRef.current = payload;
|
|
1381
|
+
}, [payload]);
|
|
1382
|
+
useEffect6(() => {
|
|
1383
|
+
if (!persist) return;
|
|
1384
|
+
if (firstRun.current) {
|
|
1385
|
+
firstRun.current = false;
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
if (!payloadRef.current) return;
|
|
1389
|
+
if (saveTimer.current) clearTimeout(saveTimer.current);
|
|
1390
|
+
const body = JSON.stringify(payloadRef.current);
|
|
1391
|
+
saveTimer.current = setTimeout(() => {
|
|
1392
|
+
void fetch("/api/canvas-layout", {
|
|
1393
|
+
method: "PUT",
|
|
1394
|
+
headers: { "Content-Type": "application/json" },
|
|
1395
|
+
body
|
|
1396
|
+
}).catch(() => {
|
|
1397
|
+
});
|
|
1398
|
+
}, 700);
|
|
1399
|
+
return () => {
|
|
1400
|
+
if (saveTimer.current) clearTimeout(saveTimer.current);
|
|
1401
|
+
};
|
|
1402
|
+
}, [persist, layoutSignature]);
|
|
1403
|
+
useEffect6(() => {
|
|
1404
|
+
if (!persist) return;
|
|
1405
|
+
const flush = () => {
|
|
1406
|
+
if (!payloadRef.current) return;
|
|
1407
|
+
try {
|
|
1408
|
+
void fetch("/api/canvas-layout", {
|
|
1409
|
+
method: "PUT",
|
|
1410
|
+
headers: { "Content-Type": "application/json" },
|
|
1411
|
+
body: JSON.stringify(payloadRef.current),
|
|
1412
|
+
keepalive: true
|
|
1413
|
+
}).catch(() => {
|
|
1414
|
+
});
|
|
1415
|
+
} catch {
|
|
1416
|
+
}
|
|
1417
|
+
};
|
|
1418
|
+
const onHide = () => {
|
|
1419
|
+
if (document.visibilityState === "hidden") flush();
|
|
1420
|
+
};
|
|
1421
|
+
window.addEventListener("pagehide", flush);
|
|
1422
|
+
document.addEventListener("visibilitychange", onHide);
|
|
1423
|
+
return () => {
|
|
1424
|
+
window.removeEventListener("pagehide", flush);
|
|
1425
|
+
document.removeEventListener("visibilitychange", onHide);
|
|
1426
|
+
};
|
|
1427
|
+
}, [persist]);
|
|
1428
|
+
const api = useMemo2(() => ({
|
|
1429
|
+
blocks,
|
|
1430
|
+
addBlock,
|
|
1431
|
+
removeBlock,
|
|
1432
|
+
resizeBlock,
|
|
1433
|
+
snapTo,
|
|
1434
|
+
snapPreview,
|
|
1435
|
+
previewSnap,
|
|
1436
|
+
clearSnapPreview,
|
|
1437
|
+
snapGhost,
|
|
1438
|
+
moveTo,
|
|
1439
|
+
sizeTo,
|
|
1440
|
+
setRect,
|
|
1441
|
+
resizeRow,
|
|
1442
|
+
bringToFront,
|
|
1443
|
+
resolveCollisions,
|
|
1444
|
+
toggleLock,
|
|
1445
|
+
clear,
|
|
1446
|
+
interacting,
|
|
1447
|
+
setInteracting,
|
|
1448
|
+
activeId,
|
|
1449
|
+
setActiveId,
|
|
1450
|
+
boardWidth
|
|
1451
|
+
}), [blocks, addBlock, removeBlock, resizeBlock, snapTo, snapPreview, previewSnap, clearSnapPreview, snapGhost, moveTo, sizeTo, setRect, resizeRow, bringToFront, resolveCollisions, toggleLock, clear, interacting, activeId, boardWidth]);
|
|
1452
|
+
return /* @__PURE__ */ jsx11(CanvasContext.Provider, { value: api, children });
|
|
1453
|
+
}
|
|
1454
|
+
|
|
796
1455
|
// src/voice/useHeyShift.ts
|
|
797
|
-
import { useCallback, useEffect as
|
|
1456
|
+
import { useCallback as useCallback2, useEffect as useEffect7, useRef as useRef7, useState as useState7 } from "react";
|
|
798
1457
|
function getSpeechRecognitionCtor() {
|
|
799
1458
|
const w = window;
|
|
800
1459
|
return w.SpeechRecognition ?? w.webkitSpeechRecognition;
|
|
@@ -807,28 +1466,28 @@ function useHeyShift(config = {}) {
|
|
|
807
1466
|
onCommand,
|
|
808
1467
|
onStateChange
|
|
809
1468
|
} = config;
|
|
810
|
-
const [state, setState] =
|
|
811
|
-
const [transcript, setTranscript] =
|
|
812
|
-
const [isSupported, setIsSupported] =
|
|
813
|
-
const recRef =
|
|
814
|
-
const silenceTimerRef =
|
|
815
|
-
const activeRef =
|
|
816
|
-
const accumulatedRef =
|
|
817
|
-
const stateRef =
|
|
818
|
-
const updateState =
|
|
1469
|
+
const [state, setState] = useState7("idle");
|
|
1470
|
+
const [transcript, setTranscript] = useState7("");
|
|
1471
|
+
const [isSupported, setIsSupported] = useState7(false);
|
|
1472
|
+
const recRef = useRef7(null);
|
|
1473
|
+
const silenceTimerRef = useRef7(null);
|
|
1474
|
+
const activeRef = useRef7(false);
|
|
1475
|
+
const accumulatedRef = useRef7("");
|
|
1476
|
+
const stateRef = useRef7("idle");
|
|
1477
|
+
const updateState = useCallback2((s) => {
|
|
819
1478
|
stateRef.current = s;
|
|
820
1479
|
setState(s);
|
|
821
1480
|
onStateChange?.(s);
|
|
822
1481
|
}, [onStateChange]);
|
|
823
|
-
|
|
1482
|
+
useEffect7(() => {
|
|
824
1483
|
const SR = getSpeechRecognitionCtor();
|
|
825
1484
|
setIsSupported(!!(SR && window.speechSynthesis));
|
|
826
1485
|
}, []);
|
|
827
|
-
const getVoice =
|
|
1486
|
+
const getVoice = useCallback2(() => {
|
|
828
1487
|
const voices = window.speechSynthesis?.getVoices() ?? [];
|
|
829
1488
|
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
1489
|
}, []);
|
|
831
|
-
const speak =
|
|
1490
|
+
const speak = useCallback2(async (text) => {
|
|
832
1491
|
if (!window.speechSynthesis) return;
|
|
833
1492
|
window.speechSynthesis.cancel();
|
|
834
1493
|
return new Promise((resolve) => {
|
|
@@ -858,11 +1517,11 @@ function useHeyShift(config = {}) {
|
|
|
858
1517
|
}
|
|
859
1518
|
});
|
|
860
1519
|
}, [getVoice, updateState]);
|
|
861
|
-
const cancelSpeech =
|
|
1520
|
+
const cancelSpeech = useCallback2(() => {
|
|
862
1521
|
window.speechSynthesis?.cancel();
|
|
863
1522
|
if (stateRef.current === "speaking") updateState("idle");
|
|
864
1523
|
}, [updateState]);
|
|
865
|
-
const resetSilenceTimer =
|
|
1524
|
+
const resetSilenceTimer = useCallback2((text) => {
|
|
866
1525
|
if (silenceTimerRef.current) clearTimeout(silenceTimerRef.current);
|
|
867
1526
|
silenceTimerRef.current = setTimeout(async () => {
|
|
868
1527
|
if (!activeRef.current || !text.trim()) return;
|
|
@@ -883,7 +1542,7 @@ function useHeyShift(config = {}) {
|
|
|
883
1542
|
}
|
|
884
1543
|
}, silenceMs);
|
|
885
1544
|
}, [silenceMs, onCommand, speak, updateState]);
|
|
886
|
-
const startListening =
|
|
1545
|
+
const startListening = useCallback2(() => {
|
|
887
1546
|
const SR = getSpeechRecognitionCtor();
|
|
888
1547
|
if (!SR || recRef.current) return;
|
|
889
1548
|
const rec = new SR();
|
|
@@ -929,7 +1588,7 @@ function useHeyShift(config = {}) {
|
|
|
929
1588
|
rec.start();
|
|
930
1589
|
recRef.current = rec;
|
|
931
1590
|
}, [language, wakeWord, resetSilenceTimer, updateState]);
|
|
932
|
-
const stopListening =
|
|
1591
|
+
const stopListening = useCallback2(() => {
|
|
933
1592
|
if (silenceTimerRef.current) clearTimeout(silenceTimerRef.current);
|
|
934
1593
|
const rec = recRef.current;
|
|
935
1594
|
recRef.current = null;
|
|
@@ -939,7 +1598,7 @@ function useHeyShift(config = {}) {
|
|
|
939
1598
|
setTranscript("");
|
|
940
1599
|
updateState("idle");
|
|
941
1600
|
}, [updateState]);
|
|
942
|
-
|
|
1601
|
+
useEffect7(() => {
|
|
943
1602
|
return () => {
|
|
944
1603
|
const rec = recRef.current;
|
|
945
1604
|
recRef.current = null;
|
|
@@ -952,7 +1611,7 @@ function useHeyShift(config = {}) {
|
|
|
952
1611
|
}
|
|
953
1612
|
|
|
954
1613
|
// src/voice/ShiftBar.tsx
|
|
955
|
-
import { Fragment as Fragment3, jsx as
|
|
1614
|
+
import { Fragment as Fragment3, jsx as jsx12, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
956
1615
|
var COLORS = {
|
|
957
1616
|
idle: "rgba(255,255,255,0.15)",
|
|
958
1617
|
wake: "#0ed882",
|
|
@@ -965,12 +1624,12 @@ function ShiftBar({ state, dots = 3, size = 7, className = "" }) {
|
|
|
965
1624
|
const color = COLORS[state];
|
|
966
1625
|
const isAnimated = state !== "idle" && state !== "error";
|
|
967
1626
|
return /* @__PURE__ */ jsxs7(Fragment3, { children: [
|
|
968
|
-
/* @__PURE__ */
|
|
1627
|
+
/* @__PURE__ */ jsx12("style", { children: `
|
|
969
1628
|
@keyframes _sb_pulse { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:.35;transform:scale(1.45)} }
|
|
970
1629
|
@keyframes _sb_wave { 0%,100%{transform:scaleY(1)} 50%{transform:scaleY(1.9)} }
|
|
971
1630
|
@keyframes _sb_spin { to{transform:rotate(360deg)} }
|
|
972
1631
|
` }),
|
|
973
|
-
/* @__PURE__ */
|
|
1632
|
+
/* @__PURE__ */ jsx12(
|
|
974
1633
|
"span",
|
|
975
1634
|
{
|
|
976
1635
|
className,
|
|
@@ -988,7 +1647,7 @@ function ShiftBar({ state, dots = 3, size = 7, className = "" }) {
|
|
|
988
1647
|
animation = `_sb_pulse 1.2s ease-in-out ${i * 0.18}s infinite`;
|
|
989
1648
|
}
|
|
990
1649
|
}
|
|
991
|
-
return /* @__PURE__ */
|
|
1650
|
+
return /* @__PURE__ */ jsx12(
|
|
992
1651
|
"span",
|
|
993
1652
|
{
|
|
994
1653
|
style: {
|
|
@@ -1014,16 +1673,24 @@ function ShiftBar({ state, dots = 3, size = 7, className = "" }) {
|
|
|
1014
1673
|
export {
|
|
1015
1674
|
ACCENT_RGB,
|
|
1016
1675
|
CanvasLauncher,
|
|
1676
|
+
CanvasProvider,
|
|
1017
1677
|
CanvasTransition,
|
|
1678
|
+
DEFAULT_H,
|
|
1679
|
+
GAP,
|
|
1018
1680
|
GenerativeView,
|
|
1019
1681
|
HolographicShine,
|
|
1020
1682
|
LiveCard,
|
|
1021
1683
|
LiveStat,
|
|
1684
|
+
MIN_H,
|
|
1685
|
+
MIN_W,
|
|
1022
1686
|
PageTransition,
|
|
1023
1687
|
ScrollGenerateCard,
|
|
1024
1688
|
ShiftBar,
|
|
1025
1689
|
ShiftCard,
|
|
1026
1690
|
TiltCard,
|
|
1691
|
+
WIDTH_SPAN,
|
|
1692
|
+
tallH,
|
|
1693
|
+
useCanvas,
|
|
1027
1694
|
useGenerate,
|
|
1028
1695
|
useHeyShift
|
|
1029
1696
|
};
|