react-os-shell 0.1.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/LICENSE +21 -0
- package/README.md +242 -0
- package/dist/Calculator-BNBRNV4P.js +184 -0
- package/dist/Calculator-BNBRNV4P.js.map +1 -0
- package/dist/Calendar-5EYUVGUU.js +423 -0
- package/dist/Calendar-5EYUVGUU.js.map +1 -0
- package/dist/Checkers-MIAHIKJH.js +214 -0
- package/dist/Checkers-MIAHIKJH.js.map +1 -0
- package/dist/Chess-C5BY45NA.js +190 -0
- package/dist/Chess-C5BY45NA.js.map +1 -0
- package/dist/ConfirmDialog-ZP4AHVUD.js +3 -0
- package/dist/ConfirmDialog-ZP4AHVUD.js.map +1 -0
- package/dist/CurrencyConverter-TYPU2IRF.js +223 -0
- package/dist/CurrencyConverter-TYPU2IRF.js.map +1 -0
- package/dist/Email-JEYYJ3YV.js +1835 -0
- package/dist/Email-JEYYJ3YV.js.map +1 -0
- package/dist/Game2048-3RH3ELRD.js +191 -0
- package/dist/Game2048-3RH3ELRD.js.map +1 -0
- package/dist/GeminiChat-BXLBJFT4.js +184 -0
- package/dist/GeminiChat-BXLBJFT4.js.map +1 -0
- package/dist/Minesweeper-VQGLAZON.js +270 -0
- package/dist/Minesweeper-VQGLAZON.js.map +1 -0
- package/dist/Notepad-YTZRCAXX.js +389 -0
- package/dist/Notepad-YTZRCAXX.js.map +1 -0
- package/dist/PomodoroTimer-HARIJN4S.js +196 -0
- package/dist/PomodoroTimer-HARIJN4S.js.map +1 -0
- package/dist/Spreadsheet-IOKEDNS6.js +446 -0
- package/dist/Spreadsheet-IOKEDNS6.js.map +1 -0
- package/dist/Sudoku-XHLYCEVT.js +197 -0
- package/dist/Sudoku-XHLYCEVT.js.map +1 -0
- package/dist/Tetris-ZHCZYL24.js +243 -0
- package/dist/Tetris-ZHCZYL24.js.map +1 -0
- package/dist/Weather-ROZ7TRNW.js +310 -0
- package/dist/Weather-ROZ7TRNW.js.map +1 -0
- package/dist/apps/index.d.ts +55 -0
- package/dist/apps/index.js +48 -0
- package/dist/apps/index.js.map +1 -0
- package/dist/chunk-5O2KEISQ.js +155 -0
- package/dist/chunk-5O2KEISQ.js.map +1 -0
- package/dist/chunk-D7PYW2QS.js +265 -0
- package/dist/chunk-D7PYW2QS.js.map +1 -0
- package/dist/chunk-GP4Y3VCB.js +806 -0
- package/dist/chunk-GP4Y3VCB.js.map +1 -0
- package/dist/chunk-NSU7OHPC.js +39 -0
- package/dist/chunk-NSU7OHPC.js.map +1 -0
- package/dist/chunk-PDFQNHW7.js +24 -0
- package/dist/chunk-PDFQNHW7.js.map +1 -0
- package/dist/chunk-RFTLYCSF.js +144 -0
- package/dist/chunk-RFTLYCSF.js.map +1 -0
- package/dist/chunk-SVBID2P6.js +142 -0
- package/dist/chunk-SVBID2P6.js.map +1 -0
- package/dist/chunk-TFGOLXGD.js +41 -0
- package/dist/chunk-TFGOLXGD.js.map +1 -0
- package/dist/chunk-WIJ45SYD.js +120 -0
- package/dist/chunk-WIJ45SYD.js.map +1 -0
- package/dist/chunk-WQIS72NL.js +1470 -0
- package/dist/chunk-WQIS72NL.js.map +1 -0
- package/dist/index.d.ts +642 -0
- package/dist/index.js +3443 -0
- package/dist/index.js.map +1 -0
- package/dist/sounds-NT4DEZGD.js +3 -0
- package/dist/sounds-NT4DEZGD.js.map +1 -0
- package/dist/styles.css +174 -0
- package/dist/types-CFIZ1_xt.d.ts +67 -0
- package/package.json +76 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { loadAppearance, WidgetSettingsModal } from './chunk-SVBID2P6.js';
|
|
2
|
+
import { useWidgetSettings } from './chunk-WQIS72NL.js';
|
|
3
|
+
import './chunk-RFTLYCSF.js';
|
|
4
|
+
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
5
|
+
import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
|
|
6
|
+
|
|
7
|
+
var POMO_SETTINGS_KEY = "pomodoro_appearance";
|
|
8
|
+
var DURATIONS = { focus: 25 * 60, short: 5 * 60, long: 15 * 60 };
|
|
9
|
+
var LABELS = { focus: "Focus", short: "Short", long: "Long" };
|
|
10
|
+
var COLORS = {
|
|
11
|
+
focus: { ring: "stroke-blue-500", text: "text-blue-600" },
|
|
12
|
+
short: { ring: "stroke-emerald-500", text: "text-emerald-600" },
|
|
13
|
+
long: { ring: "stroke-emerald-600", text: "text-emerald-700" }
|
|
14
|
+
};
|
|
15
|
+
function getTodayKey() {
|
|
16
|
+
return `pomodoro-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
|
|
17
|
+
}
|
|
18
|
+
function loadCount() {
|
|
19
|
+
return parseInt(localStorage.getItem(getTodayKey()) || "0", 10);
|
|
20
|
+
}
|
|
21
|
+
function saveCount(n) {
|
|
22
|
+
localStorage.setItem(getTodayKey(), String(n));
|
|
23
|
+
}
|
|
24
|
+
function beep() {
|
|
25
|
+
try {
|
|
26
|
+
const ctx = new AudioContext();
|
|
27
|
+
const osc = ctx.createOscillator();
|
|
28
|
+
const gain = ctx.createGain();
|
|
29
|
+
osc.connect(gain);
|
|
30
|
+
gain.connect(ctx.destination);
|
|
31
|
+
osc.frequency.value = 830;
|
|
32
|
+
osc.type = "sine";
|
|
33
|
+
gain.gain.setValueAtTime(0.3, ctx.currentTime);
|
|
34
|
+
gain.gain.exponentialRampToValueAtTime(1e-3, ctx.currentTime + 0.8);
|
|
35
|
+
osc.start();
|
|
36
|
+
osc.stop(ctx.currentTime + 0.8);
|
|
37
|
+
setTimeout(() => ctx.close(), 1e3);
|
|
38
|
+
} catch {
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function PomodoroTimer() {
|
|
42
|
+
const [mode, setMode] = useState("focus");
|
|
43
|
+
const [remaining, setRemaining] = useState(DURATIONS.focus);
|
|
44
|
+
const [running, setRunning] = useState(false);
|
|
45
|
+
const [count, setCount] = useState(loadCount);
|
|
46
|
+
const [streak, setStreak] = useState(0);
|
|
47
|
+
const intervalRef = useRef();
|
|
48
|
+
const [appearance, setAppearance] = useState(() => loadAppearance(POMO_SETTINGS_KEY));
|
|
49
|
+
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
50
|
+
const [configAppearance, setConfigAppearance] = useState(appearance);
|
|
51
|
+
useWidgetSettings(useCallback(() => {
|
|
52
|
+
setConfigAppearance({ ...appearance });
|
|
53
|
+
setSettingsOpen(true);
|
|
54
|
+
}, [appearance]));
|
|
55
|
+
const total = DURATIONS[mode];
|
|
56
|
+
const progress = 1 - remaining / total;
|
|
57
|
+
const mm = String(Math.floor(remaining / 60)).padStart(2, "0");
|
|
58
|
+
const ss = String(remaining % 60).padStart(2, "0");
|
|
59
|
+
const c = COLORS[mode];
|
|
60
|
+
const R = 54, C = 2 * Math.PI * R;
|
|
61
|
+
const notify = useCallback((title) => {
|
|
62
|
+
beep();
|
|
63
|
+
if (Notification.permission === "granted") new Notification(title);
|
|
64
|
+
else if (Notification.permission !== "denied") Notification.requestPermission();
|
|
65
|
+
}, []);
|
|
66
|
+
const switchMode = useCallback((next) => {
|
|
67
|
+
setMode(next);
|
|
68
|
+
setRemaining(DURATIONS[next]);
|
|
69
|
+
setRunning(false);
|
|
70
|
+
}, []);
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (!running) {
|
|
73
|
+
clearInterval(intervalRef.current);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
intervalRef.current = setInterval(() => setRemaining((r) => {
|
|
77
|
+
if (r <= 1) {
|
|
78
|
+
clearInterval(intervalRef.current);
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
return r - 1;
|
|
82
|
+
}), 1e3);
|
|
83
|
+
return () => clearInterval(intervalRef.current);
|
|
84
|
+
}, [running]);
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (remaining > 0 || running === false) return;
|
|
87
|
+
setRunning(false);
|
|
88
|
+
if (mode === "focus") {
|
|
89
|
+
const next = streak + 1;
|
|
90
|
+
const newCount = count + 1;
|
|
91
|
+
setStreak(next);
|
|
92
|
+
setCount(newCount);
|
|
93
|
+
saveCount(newCount);
|
|
94
|
+
notify(next % 4 === 0 ? "Time for a long break!" : "Time for a short break!");
|
|
95
|
+
switchMode(next % 4 === 0 ? "long" : "short");
|
|
96
|
+
} else {
|
|
97
|
+
notify("Back to work!");
|
|
98
|
+
switchMode("focus");
|
|
99
|
+
}
|
|
100
|
+
}, [remaining, running, mode, streak, count, notify, switchMode]);
|
|
101
|
+
const reset = () => {
|
|
102
|
+
setRunning(false);
|
|
103
|
+
setRemaining(DURATIONS[mode]);
|
|
104
|
+
};
|
|
105
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
106
|
+
/* @__PURE__ */ jsxs(
|
|
107
|
+
"div",
|
|
108
|
+
{
|
|
109
|
+
className: "flex flex-col items-center justify-between h-full p-3 select-none",
|
|
110
|
+
style: { opacity: appearance.activeOpacity / 100, backdropFilter: appearance.activeBlur > 0 ? `blur(${appearance.activeBlur}px)` : void 0 },
|
|
111
|
+
children: [
|
|
112
|
+
/* @__PURE__ */ jsx("div", { className: "flex gap-0.5 rounded-lg bg-gray-100 p-0.5 text-xs font-medium w-full", children: ["focus", "short", "long"].map((m) => /* @__PURE__ */ jsx(
|
|
113
|
+
"button",
|
|
114
|
+
{
|
|
115
|
+
onClick: () => switchMode(m),
|
|
116
|
+
className: `flex-1 px-2 py-1 rounded-md transition ${mode === m ? "bg-blue-600 text-white" : "text-gray-500 hover:bg-gray-200"}`,
|
|
117
|
+
children: LABELS[m]
|
|
118
|
+
},
|
|
119
|
+
m
|
|
120
|
+
)) }),
|
|
121
|
+
/* @__PURE__ */ jsxs("div", { className: "relative", style: { width: 210, height: 210 }, children: [
|
|
122
|
+
/* @__PURE__ */ jsxs("svg", { className: "w-full h-full -rotate-90", viewBox: "0 0 120 120", children: [
|
|
123
|
+
/* @__PURE__ */ jsx("circle", { cx: "60", cy: "60", r: R, fill: "none", strokeWidth: "6", className: "stroke-gray-200" }),
|
|
124
|
+
/* @__PURE__ */ jsx(
|
|
125
|
+
"circle",
|
|
126
|
+
{
|
|
127
|
+
cx: "60",
|
|
128
|
+
cy: "60",
|
|
129
|
+
r: R,
|
|
130
|
+
fill: "none",
|
|
131
|
+
strokeWidth: "6",
|
|
132
|
+
className: `${c.ring} transition-all duration-500`,
|
|
133
|
+
strokeLinecap: "round",
|
|
134
|
+
strokeDasharray: C,
|
|
135
|
+
strokeDashoffset: C * (1 - progress)
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
] }),
|
|
139
|
+
/* @__PURE__ */ jsxs("div", { className: "absolute inset-0 flex flex-col items-center justify-center", children: [
|
|
140
|
+
/* @__PURE__ */ jsxs("span", { className: `text-3xl font-mono font-bold ${c.text}`, children: [
|
|
141
|
+
mm,
|
|
142
|
+
":",
|
|
143
|
+
ss
|
|
144
|
+
] }),
|
|
145
|
+
/* @__PURE__ */ jsx("span", { className: "text-[10px] text-gray-400 mt-0.5", children: LABELS[mode] })
|
|
146
|
+
] })
|
|
147
|
+
] }),
|
|
148
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-2 w-full", children: [
|
|
149
|
+
/* @__PURE__ */ jsx(
|
|
150
|
+
"button",
|
|
151
|
+
{
|
|
152
|
+
onClick: () => setRunning((r) => !r),
|
|
153
|
+
className: "flex-1 py-1.5 rounded-lg text-white font-medium text-xs bg-blue-600 hover:bg-blue-700 transition",
|
|
154
|
+
children: running ? "Pause" : "Start"
|
|
155
|
+
}
|
|
156
|
+
),
|
|
157
|
+
/* @__PURE__ */ jsx(
|
|
158
|
+
"button",
|
|
159
|
+
{
|
|
160
|
+
onClick: reset,
|
|
161
|
+
className: "flex-1 py-1.5 rounded-lg text-xs font-medium bg-gray-200 text-gray-700 hover:bg-gray-300 transition",
|
|
162
|
+
children: "Reset"
|
|
163
|
+
}
|
|
164
|
+
)
|
|
165
|
+
] }),
|
|
166
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 text-xs text-gray-400", children: [
|
|
167
|
+
count > 0 ? Array.from({ length: Math.min(count, 8) }, (_, i) => /* @__PURE__ */ jsx("span", { children: "\u{1F345}" }, i)) : /* @__PURE__ */ jsx("span", { children: "No sessions yet" }),
|
|
168
|
+
count > 8 && /* @__PURE__ */ jsxs("span", { children: [
|
|
169
|
+
"+",
|
|
170
|
+
count - 8
|
|
171
|
+
] })
|
|
172
|
+
] })
|
|
173
|
+
]
|
|
174
|
+
}
|
|
175
|
+
),
|
|
176
|
+
/* @__PURE__ */ jsx(
|
|
177
|
+
WidgetSettingsModal,
|
|
178
|
+
{
|
|
179
|
+
open: settingsOpen,
|
|
180
|
+
onClose: () => setSettingsOpen(false),
|
|
181
|
+
title: "Pomodoro Settings",
|
|
182
|
+
appearance: configAppearance,
|
|
183
|
+
onAppearanceChange: setConfigAppearance,
|
|
184
|
+
onSave: () => {
|
|
185
|
+
setAppearance(configAppearance);
|
|
186
|
+
localStorage.setItem(POMO_SETTINGS_KEY, JSON.stringify(configAppearance));
|
|
187
|
+
setSettingsOpen(false);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
)
|
|
191
|
+
] });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export { PomodoroTimer as default };
|
|
195
|
+
//# sourceMappingURL=PomodoroTimer-HARIJN4S.js.map
|
|
196
|
+
//# sourceMappingURL=PomodoroTimer-HARIJN4S.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/apps/PomodoroTimer.tsx"],"names":[],"mappings":";;;;;;AAKA,IAAM,iBAAA,GAAoB,qBAAA;AAC1B,IAAM,SAAA,GAAkC,EAAE,KAAA,EAAO,EAAA,GAAK,EAAA,EAAI,OAAO,CAAA,GAAI,EAAA,EAAI,IAAA,EAAM,EAAA,GAAK,EAAA,EAAG;AACvF,IAAM,SAA+B,EAAE,KAAA,EAAO,SAAS,KAAA,EAAO,OAAA,EAAS,MAAM,MAAA,EAAO;AACpF,IAAM,MAAA,GAAuD;AAAA,EAC3D,KAAA,EAAO,EAAE,IAAA,EAAM,iBAAA,EAAmB,MAAM,eAAA,EAAgB;AAAA,EACxD,KAAA,EAAO,EAAE,IAAA,EAAM,oBAAA,EAAsB,MAAM,kBAAA,EAAmB;AAAA,EAC9D,IAAA,EAAM,EAAE,IAAA,EAAM,oBAAA,EAAsB,MAAM,kBAAA;AAC5C,CAAA;AAEA,SAAS,WAAA,GAAc;AAAE,EAAA,OAAO,CAAA,SAAA,EAAA,qBAAgB,IAAA,EAAK,EAAE,aAAY,CAAE,KAAA,CAAM,CAAA,EAAG,EAAE,CAAC,CAAA,CAAA;AAAI;AACrF,SAAS,SAAA,GAAY;AAAE,EAAA,OAAO,SAAS,YAAA,CAAa,OAAA,CAAQ,aAAa,CAAA,IAAK,KAAK,EAAE,CAAA;AAAG;AACxF,SAAS,UAAU,CAAA,EAAW;AAAE,EAAA,YAAA,CAAa,OAAA,CAAQ,WAAA,EAAY,EAAG,MAAA,CAAO,CAAC,CAAC,CAAA;AAAG;AAEhF,SAAS,IAAA,GAAO;AACd,EAAA,IAAI;AACF,IAAA,MAAM,GAAA,GAAM,IAAI,YAAA,EAAa;AAC7B,IAAA,MAAM,GAAA,GAAM,IAAI,gBAAA,EAAiB;AACjC,IAAA,MAAM,IAAA,GAAO,IAAI,UAAA,EAAW;AAC5B,IAAA,GAAA,CAAI,QAAQ,IAAI,CAAA;AAAG,IAAA,IAAA,CAAK,OAAA,CAAQ,IAAI,WAAW,CAAA;AAC/C,IAAA,GAAA,CAAI,UAAU,KAAA,GAAQ,GAAA;AAAK,IAAA,GAAA,CAAI,IAAA,GAAO,MAAA;AACtC,IAAA,IAAA,CAAK,IAAA,CAAK,cAAA,CAAe,GAAA,EAAK,GAAA,CAAI,WAAW,CAAA;AAC7C,IAAA,IAAA,CAAK,IAAA,CAAK,4BAAA,CAA6B,IAAA,EAAO,GAAA,CAAI,cAAc,GAAG,CAAA;AACnE,IAAA,GAAA,CAAI,KAAA,EAAM;AAAG,IAAA,GAAA,CAAI,IAAA,CAAK,GAAA,CAAI,WAAA,GAAc,GAAG,CAAA;AAC3C,IAAA,UAAA,CAAW,MAAM,GAAA,CAAI,KAAA,EAAM,EAAG,GAAI,CAAA;AAAA,EACpC,CAAA,CAAA,MAAQ;AAAA,EAAwB;AAClC;AAEe,SAAR,aAAA,GAAiC;AACtC,EAAA,MAAM,CAAC,IAAA,EAAM,OAAO,CAAA,GAAI,SAAe,OAAO,CAAA;AAC9C,EAAA,MAAM,CAAC,SAAA,EAAW,YAAY,CAAA,GAAI,QAAA,CAAS,UAAU,KAAK,CAAA;AAC1D,EAAA,MAAM,CAAC,OAAA,EAAS,UAAU,CAAA,GAAI,SAAS,KAAK,CAAA;AAC5C,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,CAAA,GAAI,SAAS,SAAS,CAAA;AAC5C,EAAA,MAAM,CAAC,MAAA,EAAQ,SAAS,CAAA,GAAI,SAAS,CAAC,CAAA;AACtC,EAAA,MAAM,cAAc,MAAA,EAAuC;AAC3D,EAAA,MAAM,CAAC,YAAY,aAAa,CAAA,GAAI,SAAS,MAAM,cAAA,CAAe,iBAAiB,CAAC,CAAA;AACpF,EAAA,MAAM,CAAC,YAAA,EAAc,eAAe,CAAA,GAAI,SAAS,KAAK,CAAA;AACtD,EAAA,MAAM,CAAC,gBAAA,EAAkB,mBAAmB,CAAA,GAAI,SAA2B,UAAU,CAAA;AAErF,EAAA,iBAAA,CAAkB,YAAY,MAAM;AAClC,IAAA,mBAAA,CAAoB,EAAE,GAAG,UAAA,EAAY,CAAA;AACrC,IAAA,eAAA,CAAgB,IAAI,CAAA;AAAA,EACtB,CAAA,EAAG,CAAC,UAAU,CAAC,CAAC,CAAA;AAEhB,EAAA,MAAM,KAAA,GAAQ,UAAU,IAAI,CAAA;AAC5B,EAAA,MAAM,QAAA,GAAW,IAAI,SAAA,GAAY,KAAA;AACjC,EAAA,MAAM,EAAA,GAAK,MAAA,CAAO,IAAA,CAAK,KAAA,CAAM,SAAA,GAAY,EAAE,CAAC,CAAA,CAAE,QAAA,CAAS,CAAA,EAAG,GAAG,CAAA;AAC7D,EAAA,MAAM,KAAK,MAAA,CAAO,SAAA,GAAY,EAAE,CAAA,CAAE,QAAA,CAAS,GAAG,GAAG,CAAA;AACjD,EAAA,MAAM,CAAA,GAAI,OAAO,IAAI,CAAA;AACrB,EAAA,MAAM,CAAA,GAAI,EAAA,EAAI,CAAA,GAAI,CAAA,GAAI,KAAK,EAAA,GAAK,CAAA;AAEhC,EAAA,MAAM,MAAA,GAAS,WAAA,CAAY,CAAC,KAAA,KAAkB;AAC5C,IAAA,IAAA,EAAK;AACL,IAAA,IAAI,YAAA,CAAa,UAAA,KAAe,SAAA,EAAW,IAAI,aAAa,KAAK,CAAA;AAAA,SAAA,IACxD,YAAA,CAAa,UAAA,KAAe,QAAA,EAAU,YAAA,CAAa,iBAAA,EAAkB;AAAA,EAChF,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,MAAM,UAAA,GAAa,WAAA,CAAY,CAAC,IAAA,KAAe;AAC7C,IAAA,OAAA,CAAQ,IAAI,CAAA;AAAG,IAAA,YAAA,CAAa,SAAA,CAAU,IAAI,CAAC,CAAA;AAAG,IAAA,UAAA,CAAW,KAAK,CAAA;AAAA,EAChE,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,CAAC,OAAA,EAAS;AAAE,MAAA,aAAA,CAAc,YAAY,OAAO,CAAA;AAAG,MAAA;AAAA,IAAQ;AAC5D,IAAA,WAAA,CAAY,OAAA,GAAU,WAAA,CAAY,MAAM,YAAA,CAAa,CAAA,CAAA,KAAK;AACxD,MAAA,IAAI,KAAK,CAAA,EAAG;AAAE,QAAA,aAAA,CAAc,YAAY,OAAO,CAAA;AAAG,QAAA,OAAO,CAAA;AAAA,MAAG;AAC5D,MAAA,OAAO,CAAA,GAAI,CAAA;AAAA,IACb,CAAC,GAAG,GAAI,CAAA;AACR,IAAA,OAAO,MAAM,aAAA,CAAc,WAAA,CAAY,OAAO,CAAA;AAAA,EAChD,CAAA,EAAG,CAAC,OAAO,CAAC,CAAA;AAEZ,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,SAAA,GAAY,CAAA,IAAK,OAAA,KAAY,KAAA,EAAO;AACxC,IAAA,UAAA,CAAW,KAAK,CAAA;AAChB,IAAA,IAAI,SAAS,OAAA,EAAS;AACpB,MAAA,MAAM,OAAO,MAAA,GAAS,CAAA;AACtB,MAAA,MAAM,WAAW,KAAA,GAAQ,CAAA;AACzB,MAAA,SAAA,CAAU,IAAI,CAAA;AAAG,MAAA,QAAA,CAAS,QAAQ,CAAA;AAAG,MAAA,SAAA,CAAU,QAAQ,CAAA;AACvD,MAAA,MAAA,CAAO,IAAA,GAAO,CAAA,KAAM,CAAA,GAAI,wBAAA,GAA2B,yBAAyB,CAAA;AAC5E,MAAA,UAAA,CAAW,IAAA,GAAO,CAAA,KAAM,CAAA,GAAI,MAAA,GAAS,OAAO,CAAA;AAAA,IAC9C,CAAA,MAAO;AACL,MAAA,MAAA,CAAO,eAAe,CAAA;AACtB,MAAA,UAAA,CAAW,OAAO,CAAA;AAAA,IACpB;AAAA,EACF,CAAA,EAAG,CAAC,SAAA,EAAW,OAAA,EAAS,MAAM,MAAA,EAAQ,KAAA,EAAO,MAAA,EAAQ,UAAU,CAAC,CAAA;AAEhE,EAAA,MAAM,QAAQ,MAAM;AAAE,IAAA,UAAA,CAAW,KAAK,CAAA;AAAG,IAAA,YAAA,CAAa,SAAA,CAAU,IAAI,CAAC,CAAA;AAAA,EAAG,CAAA;AAExE,EAAA,uBACE,IAAA,CAAA,QAAA,EAAA,EACA,QAAA,EAAA;AAAA,oBAAA,IAAA;AAAA,MAAC,KAAA;AAAA,MAAA;AAAA,QAAI,SAAA,EAAU,mEAAA;AAAA,QACb,KAAA,EAAO,EAAE,OAAA,EAAS,UAAA,CAAW,gBAAgB,GAAA,EAAK,cAAA,EAAgB,UAAA,CAAW,UAAA,GAAa,CAAA,GAAI,CAAA,KAAA,EAAQ,UAAA,CAAW,UAAU,QAAQ,MAAA,EAAU;AAAA,QAE7I,QAAA,EAAA;AAAA,0BAAA,GAAA,CAAC,KAAA,EAAA,EAAI,WAAU,sEAAA,EACX,QAAA,EAAA,CAAC,SAAS,OAAA,EAAS,MAAM,CAAA,CAAa,GAAA,CAAI,CAAA,CAAA,qBAC1C,GAAA;AAAA,YAAC,QAAA;AAAA,YAAA;AAAA,cAAe,OAAA,EAAS,MAAM,UAAA,CAAW,CAAC,CAAA;AAAA,cACzC,SAAA,EAAW,CAAA,uCAAA,EAA0C,IAAA,KAAS,CAAA,GAAI,2BAA2B,iCAAiC,CAAA,CAAA;AAAA,cAC7H,iBAAO,CAAC;AAAA,aAAA;AAAA,YAFE;AAAA,WAId,CAAA,EACH,CAAA;AAAA,0BAGA,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,UAAA,EAAW,KAAA,EAAO,EAAE,KAAA,EAAO,GAAA,EAAK,MAAA,EAAQ,GAAA,EAAI,EACzD,QAAA,EAAA;AAAA,4BAAA,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,0BAAA,EAA2B,OAAA,EAAQ,aAAA,EAChD,QAAA,EAAA;AAAA,8BAAA,GAAA,CAAC,QAAA,EAAA,EAAO,EAAA,EAAG,IAAA,EAAK,EAAA,EAAG,IAAA,EAAK,CAAA,EAAG,CAAA,EAAG,IAAA,EAAK,MAAA,EAAO,WAAA,EAAY,GAAA,EAAI,SAAA,EAAU,iBAAA,EAAkB,CAAA;AAAA,8BACtF,GAAA;AAAA,gBAAC,QAAA;AAAA,gBAAA;AAAA,kBAAO,EAAA,EAAG,IAAA;AAAA,kBAAK,EAAA,EAAG,IAAA;AAAA,kBAAK,CAAA,EAAG,CAAA;AAAA,kBAAG,IAAA,EAAK,MAAA;AAAA,kBAAO,WAAA,EAAY,GAAA;AAAA,kBACpD,SAAA,EAAW,CAAA,EAAG,CAAA,CAAE,IAAI,CAAA,4BAAA,CAAA;AAAA,kBACpB,aAAA,EAAc,OAAA;AAAA,kBAAQ,eAAA,EAAiB,CAAA;AAAA,kBAAG,gBAAA,EAAkB,KAAK,CAAA,GAAI,QAAA;AAAA;AAAA;AAAW,aAAA,EACpF,CAAA;AAAA,4BACA,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,4DAAA,EACb,QAAA,EAAA;AAAA,8BAAA,IAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAW,CAAA,6BAAA,EAAgC,CAAA,CAAE,IAAI,CAAA,CAAA,EAAK,QAAA,EAAA;AAAA,gBAAA,EAAA;AAAA,gBAAG,GAAA;AAAA,gBAAE;AAAA,eAAA,EAAG,CAAA;AAAA,kCACnE,MAAA,EAAA,EAAK,SAAA,EAAU,kCAAA,EAAoC,QAAA,EAAA,MAAA,CAAO,IAAI,CAAA,EAAE;AAAA,aAAA,EACnE;AAAA,WAAA,EACF,CAAA;AAAA,0BAGA,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,mBAAA,EACb,QAAA,EAAA;AAAA,4BAAA,GAAA;AAAA,cAAC,QAAA;AAAA,cAAA;AAAA,gBAAO,OAAA,EAAS,MAAM,UAAA,CAAW,CAAA,CAAA,KAAK,CAAC,CAAC,CAAA;AAAA,gBACvC,SAAA,EAAU,kGAAA;AAAA,gBACT,oBAAU,OAAA,GAAU;AAAA;AAAA,aACvB;AAAA,4BACA,GAAA;AAAA,cAAC,QAAA;AAAA,cAAA;AAAA,gBAAO,OAAA,EAAS,KAAA;AAAA,gBACf,SAAA,EAAU,qGAAA;AAAA,gBAAsG,QAAA,EAAA;AAAA;AAAA;AAElH,WAAA,EACF,CAAA;AAAA,0BAGA,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,+CAAA,EACZ,QAAA,EAAA;AAAA,YAAA,KAAA,GAAQ,CAAA,GAAI,MAAM,IAAA,CAAK,EAAE,QAAQ,IAAA,CAAK,GAAA,CAAI,KAAA,EAAO,CAAC,CAAA,EAAE,EAAG,CAAC,CAAA,EAAG,CAAA,yBACzD,MAAA,EAAA,EAAa,QAAA,EAAA,WAAA,EAAA,EAAH,CAAY,CACxB,CAAA,mBAAI,GAAA,CAAC,MAAA,EAAA,EAAK,QAAA,EAAA,iBAAA,EAAe,CAAA;AAAA,YACzB,KAAA,GAAQ,CAAA,oBAAK,IAAA,CAAC,MAAA,EAAA,EAAK,QAAA,EAAA;AAAA,cAAA,GAAA;AAAA,cAAE,KAAA,GAAQ;AAAA,aAAA,EAAE;AAAA,WAAA,EAClC;AAAA;AAAA;AAAA,KACF;AAAA,oBACA,GAAA;AAAA,MAAC,mBAAA;AAAA,MAAA;AAAA,QAAoB,IAAA,EAAM,YAAA;AAAA,QAAc,OAAA,EAAS,MAAM,eAAA,CAAgB,KAAK,CAAA;AAAA,QAAG,KAAA,EAAM,mBAAA;AAAA,QACpF,UAAA,EAAY,gBAAA;AAAA,QAAkB,kBAAA,EAAoB,mBAAA;AAAA,QAClD,QAAQ,MAAM;AAAE,UAAA,aAAA,CAAc,gBAAgB,CAAA;AAAG,UAAA,YAAA,CAAa,OAAA,CAAQ,iBAAA,EAAmB,IAAA,CAAK,SAAA,CAAU,gBAAgB,CAAC,CAAA;AAAG,UAAA,eAAA,CAAgB,KAAK,CAAA;AAAA,QAAG;AAAA;AAAA;AAAG,GAAA,EACzJ,CAAA;AAEJ","file":"PomodoroTimer-HARIJN4S.js","sourcesContent":["import { useState, useEffect, useCallback, useRef } from 'react';\nimport { useWidgetSettings } from '../shell/Modal';\nimport WidgetSettingsModal, { loadAppearance, type WidgetAppearance } from '../shell/WidgetSettingsModal';\n\ntype Mode = 'focus' | 'short' | 'long';\nconst POMO_SETTINGS_KEY = 'pomodoro_appearance';\nconst DURATIONS: Record<Mode, number> = { focus: 25 * 60, short: 5 * 60, long: 15 * 60 };\nconst LABELS: Record<Mode, string> = { focus: 'Focus', short: 'Short', long: 'Long' };\nconst COLORS: Record<Mode, { ring: string; text: string }> = {\n focus: { ring: 'stroke-blue-500', text: 'text-blue-600' },\n short: { ring: 'stroke-emerald-500', text: 'text-emerald-600' },\n long: { ring: 'stroke-emerald-600', text: 'text-emerald-700' },\n};\n\nfunction getTodayKey() { return `pomodoro-${new Date().toISOString().slice(0, 10)}`; }\nfunction loadCount() { return parseInt(localStorage.getItem(getTodayKey()) || '0', 10); }\nfunction saveCount(n: number) { localStorage.setItem(getTodayKey(), String(n)); }\n\nfunction beep() {\n try {\n const ctx = new AudioContext();\n const osc = ctx.createOscillator();\n const gain = ctx.createGain();\n osc.connect(gain); gain.connect(ctx.destination);\n osc.frequency.value = 830; osc.type = 'sine';\n gain.gain.setValueAtTime(0.3, ctx.currentTime);\n gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.8);\n osc.start(); osc.stop(ctx.currentTime + 0.8);\n setTimeout(() => ctx.close(), 1000);\n } catch { /* silent fallback */ }\n}\n\nexport default function PomodoroTimer() {\n const [mode, setMode] = useState<Mode>('focus');\n const [remaining, setRemaining] = useState(DURATIONS.focus);\n const [running, setRunning] = useState(false);\n const [count, setCount] = useState(loadCount);\n const [streak, setStreak] = useState(0);\n const intervalRef = useRef<ReturnType<typeof setInterval>>();\n const [appearance, setAppearance] = useState(() => loadAppearance(POMO_SETTINGS_KEY));\n const [settingsOpen, setSettingsOpen] = useState(false);\n const [configAppearance, setConfigAppearance] = useState<WidgetAppearance>(appearance);\n\n useWidgetSettings(useCallback(() => {\n setConfigAppearance({ ...appearance });\n setSettingsOpen(true);\n }, [appearance]));\n\n const total = DURATIONS[mode];\n const progress = 1 - remaining / total;\n const mm = String(Math.floor(remaining / 60)).padStart(2, '0');\n const ss = String(remaining % 60).padStart(2, '0');\n const c = COLORS[mode];\n const R = 54, C = 2 * Math.PI * R;\n\n const notify = useCallback((title: string) => {\n beep();\n if (Notification.permission === 'granted') new Notification(title);\n else if (Notification.permission !== 'denied') Notification.requestPermission();\n }, []);\n\n const switchMode = useCallback((next: Mode) => {\n setMode(next); setRemaining(DURATIONS[next]); setRunning(false);\n }, []);\n\n useEffect(() => {\n if (!running) { clearInterval(intervalRef.current); return; }\n intervalRef.current = setInterval(() => setRemaining(r => {\n if (r <= 1) { clearInterval(intervalRef.current); return 0; }\n return r - 1;\n }), 1000);\n return () => clearInterval(intervalRef.current);\n }, [running]);\n\n useEffect(() => {\n if (remaining > 0 || running === false) return;\n setRunning(false);\n if (mode === 'focus') {\n const next = streak + 1;\n const newCount = count + 1;\n setStreak(next); setCount(newCount); saveCount(newCount);\n notify(next % 4 === 0 ? 'Time for a long break!' : 'Time for a short break!');\n switchMode(next % 4 === 0 ? 'long' : 'short');\n } else {\n notify('Back to work!');\n switchMode('focus');\n }\n }, [remaining, running, mode, streak, count, notify, switchMode]);\n\n const reset = () => { setRunning(false); setRemaining(DURATIONS[mode]); };\n\n return (\n <>\n <div className=\"flex flex-col items-center justify-between h-full p-3 select-none\"\n style={{ opacity: appearance.activeOpacity / 100, backdropFilter: appearance.activeBlur > 0 ? `blur(${appearance.activeBlur}px)` : undefined }}>\n {/* Mode tabs */}\n <div className=\"flex gap-0.5 rounded-lg bg-gray-100 p-0.5 text-xs font-medium w-full\">\n {(['focus', 'short', 'long'] as Mode[]).map(m => (\n <button key={m} onClick={() => switchMode(m)}\n className={`flex-1 px-2 py-1 rounded-md transition ${mode === m ? 'bg-blue-600 text-white' : 'text-gray-500 hover:bg-gray-200'}`}>\n {LABELS[m]}\n </button>\n ))}\n </div>\n\n {/* Circular timer */}\n <div className=\"relative\" style={{ width: 210, height: 210 }}>\n <svg className=\"w-full h-full -rotate-90\" viewBox=\"0 0 120 120\">\n <circle cx=\"60\" cy=\"60\" r={R} fill=\"none\" strokeWidth=\"6\" className=\"stroke-gray-200\" />\n <circle cx=\"60\" cy=\"60\" r={R} fill=\"none\" strokeWidth=\"6\"\n className={`${c.ring} transition-all duration-500`}\n strokeLinecap=\"round\" strokeDasharray={C} strokeDashoffset={C * (1 - progress)} />\n </svg>\n <div className=\"absolute inset-0 flex flex-col items-center justify-center\">\n <span className={`text-3xl font-mono font-bold ${c.text}`}>{mm}:{ss}</span>\n <span className=\"text-[10px] text-gray-400 mt-0.5\">{LABELS[mode]}</span>\n </div>\n </div>\n\n {/* Controls */}\n <div className=\"flex gap-2 w-full\">\n <button onClick={() => setRunning(r => !r)}\n className=\"flex-1 py-1.5 rounded-lg text-white font-medium text-xs bg-blue-600 hover:bg-blue-700 transition\">\n {running ? 'Pause' : 'Start'}\n </button>\n <button onClick={reset}\n className=\"flex-1 py-1.5 rounded-lg text-xs font-medium bg-gray-200 text-gray-700 hover:bg-gray-300 transition\">\n Reset\n </button>\n </div>\n\n {/* Session dots */}\n <div className=\"flex items-center gap-1 text-xs text-gray-400\">\n {count > 0 ? Array.from({ length: Math.min(count, 8) }, (_, i) => (\n <span key={i}>🍅</span>\n )) : <span>No sessions yet</span>}\n {count > 8 && <span>+{count - 8}</span>}\n </div>\n </div>\n <WidgetSettingsModal open={settingsOpen} onClose={() => setSettingsOpen(false)} title=\"Pomodoro Settings\"\n appearance={configAppearance} onAppearanceChange={setConfigAppearance}\n onSave={() => { setAppearance(configAppearance); localStorage.setItem(POMO_SETTINGS_KEY, JSON.stringify(configAppearance)); setSettingsOpen(false); }} />\n </>\n );\n}\n"]}
|
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
import { EditableGrid } from './chunk-GP4Y3VCB.js';
|
|
2
|
+
import { WindowTitle } from './chunk-WQIS72NL.js';
|
|
3
|
+
import './chunk-RFTLYCSF.js';
|
|
4
|
+
import { useRef, useState, useEffect, useCallback } from 'react';
|
|
5
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
6
|
+
|
|
7
|
+
var TITLE_DISPLAY_MAX = 24;
|
|
8
|
+
function truncateForTitle(s) {
|
|
9
|
+
return s.length > TITLE_DISPLAY_MAX ? `${s.slice(0, TITLE_DISPLAY_MAX - 1)}\u2026` : s;
|
|
10
|
+
}
|
|
11
|
+
var ALPHA = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
12
|
+
var DEFAULT_COLS = 10;
|
|
13
|
+
var DEFAULT_ROWS = 30;
|
|
14
|
+
function colLabel(i) {
|
|
15
|
+
if (i < 26) return ALPHA[i];
|
|
16
|
+
return ALPHA[Math.floor(i / 26) - 1] + ALPHA[i % 26];
|
|
17
|
+
}
|
|
18
|
+
function makeColumns(count) {
|
|
19
|
+
return Array.from({ length: count }, (_, i) => ({
|
|
20
|
+
key: `col_${i}`,
|
|
21
|
+
title: colLabel(i),
|
|
22
|
+
width: 100
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
function makeEmptyData(rows, cols) {
|
|
26
|
+
return Array.from({ length: rows }, () => Array(cols).fill(""));
|
|
27
|
+
}
|
|
28
|
+
function newSheet(name) {
|
|
29
|
+
return {
|
|
30
|
+
id: crypto.randomUUID(),
|
|
31
|
+
name,
|
|
32
|
+
columns: makeColumns(DEFAULT_COLS),
|
|
33
|
+
data: makeEmptyData(DEFAULT_ROWS, DEFAULT_COLS),
|
|
34
|
+
cellStyles: {}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function parseCSV(text) {
|
|
38
|
+
return text.split("\n").map((line) => {
|
|
39
|
+
if (line.includes(" ")) return line.split(" ").map((s) => s.trim());
|
|
40
|
+
const parts = [];
|
|
41
|
+
let current = "";
|
|
42
|
+
let inQuotes = false;
|
|
43
|
+
for (const ch of line) {
|
|
44
|
+
if (ch === '"') {
|
|
45
|
+
inQuotes = !inQuotes;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (ch === "," && !inQuotes) {
|
|
49
|
+
parts.push(current.trim());
|
|
50
|
+
current = "";
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
current += ch;
|
|
54
|
+
}
|
|
55
|
+
parts.push(current.trim());
|
|
56
|
+
return parts;
|
|
57
|
+
}).filter((r) => r.some((c) => c.trim()));
|
|
58
|
+
}
|
|
59
|
+
function Spreadsheet() {
|
|
60
|
+
const containerRef = useRef(null);
|
|
61
|
+
const [sheets, setSheets] = useState([newSheet("Sheet 1")]);
|
|
62
|
+
const [activeIdx, setActiveIdx] = useState(0);
|
|
63
|
+
const undoStackRef = useRef([]);
|
|
64
|
+
const lastCommittedRef = useRef(sheets);
|
|
65
|
+
const skipRecordRef = useRef(false);
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (skipRecordRef.current) {
|
|
68
|
+
skipRecordRef.current = false;
|
|
69
|
+
} else if (lastCommittedRef.current !== sheets) {
|
|
70
|
+
undoStackRef.current.push(lastCommittedRef.current);
|
|
71
|
+
if (undoStackRef.current.length > 50) undoStackRef.current.shift();
|
|
72
|
+
}
|
|
73
|
+
lastCommittedRef.current = sheets;
|
|
74
|
+
}, [sheets]);
|
|
75
|
+
const undo = useCallback(() => {
|
|
76
|
+
const prev = undoStackRef.current.pop();
|
|
77
|
+
if (!prev) return;
|
|
78
|
+
skipRecordRef.current = true;
|
|
79
|
+
setSheets(prev);
|
|
80
|
+
}, []);
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
const handler = (e) => {
|
|
83
|
+
if (!containerRef.current?.contains(document.activeElement) && !containerRef.current?.matches(":focus-within")) return;
|
|
84
|
+
const isUndo = (e.ctrlKey || e.metaKey) && !e.shiftKey && (e.key === "z" || e.key === "Z");
|
|
85
|
+
if (isUndo) {
|
|
86
|
+
e.preventDefault();
|
|
87
|
+
undo();
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
window.addEventListener("keydown", handler);
|
|
91
|
+
return () => window.removeEventListener("keydown", handler);
|
|
92
|
+
}, [undo]);
|
|
93
|
+
const [title, setTitle] = useState("Untitled");
|
|
94
|
+
const [editingTitle, setEditingTitle] = useState(false);
|
|
95
|
+
const [editingTab, setEditingTab] = useState(null);
|
|
96
|
+
const [tabName, setTabName] = useState("");
|
|
97
|
+
const fileRef = useRef(null);
|
|
98
|
+
const [addColCount, setAddColCount] = useState("1");
|
|
99
|
+
const [addRowCount, setAddRowCount] = useState("10");
|
|
100
|
+
const active = sheets[activeIdx] || sheets[0];
|
|
101
|
+
const data = active.data;
|
|
102
|
+
const columns = active.columns;
|
|
103
|
+
const cellStyles = active.cellStyles ?? {};
|
|
104
|
+
const [focusedCell, setFocusedCell] = useState(null);
|
|
105
|
+
const [selection, setSelection] = useState(null);
|
|
106
|
+
const targetCells = useCallback(() => {
|
|
107
|
+
if (selection) {
|
|
108
|
+
const r1 = Math.min(selection.anchor.row, selection.end.row);
|
|
109
|
+
const r2 = Math.max(selection.anchor.row, selection.end.row);
|
|
110
|
+
const c1 = Math.min(selection.anchor.col, selection.end.col);
|
|
111
|
+
const c2 = Math.max(selection.anchor.col, selection.end.col);
|
|
112
|
+
const cells = [];
|
|
113
|
+
for (let r = r1; r <= r2; r++) for (let c = c1; c <= c2; c++) cells.push({ row: r, col: c });
|
|
114
|
+
return cells;
|
|
115
|
+
}
|
|
116
|
+
return focusedCell ? [focusedCell] : [];
|
|
117
|
+
}, [selection, focusedCell]);
|
|
118
|
+
const toggleCellStyle = useCallback((key, value) => {
|
|
119
|
+
const cells = targetCells();
|
|
120
|
+
if (cells.length === 0) return;
|
|
121
|
+
setSheets((prev) => prev.map((s, i) => {
|
|
122
|
+
if (i !== activeIdx) return s;
|
|
123
|
+
const styles = { ...s.cellStyles ?? {} };
|
|
124
|
+
const firstKey = `${cells[0].row}:${cells[0].col}`;
|
|
125
|
+
const desired = value !== void 0 ? value : !(styles[firstKey] ?? {})[key];
|
|
126
|
+
for (const { row, col } of cells) {
|
|
127
|
+
const k = `${row}:${col}`;
|
|
128
|
+
styles[k] = { ...styles[k] ?? {}, [key]: desired };
|
|
129
|
+
}
|
|
130
|
+
return { ...s, cellStyles: styles };
|
|
131
|
+
}));
|
|
132
|
+
}, [targetCells, activeIdx]);
|
|
133
|
+
const headCell = selection ? { row: selection.anchor.row, col: selection.anchor.col } : focusedCell;
|
|
134
|
+
const focusedStyle = headCell ? cellStyles[`${headCell.row}:${headCell.col}`] ?? {} : {};
|
|
135
|
+
const hasTarget = !!headCell;
|
|
136
|
+
const updateActiveSheet = useCallback((update) => {
|
|
137
|
+
setSheets((prev) => prev.map((s, i) => i === activeIdx ? { ...s, ...update } : s));
|
|
138
|
+
}, [activeIdx]);
|
|
139
|
+
const handleChange = useCallback((newData) => {
|
|
140
|
+
const maxCols = newData.reduce((m, r) => Math.max(m, r.length), 0);
|
|
141
|
+
if (maxCols !== columns.length) {
|
|
142
|
+
updateActiveSheet({ data: newData, columns: makeColumns(maxCols) });
|
|
143
|
+
} else {
|
|
144
|
+
updateActiveSheet({ data: newData });
|
|
145
|
+
}
|
|
146
|
+
}, [updateActiveSheet, columns.length]);
|
|
147
|
+
const addSheet = () => {
|
|
148
|
+
const name = `Sheet ${sheets.length + 1}`;
|
|
149
|
+
setSheets((prev) => [...prev, newSheet(name)]);
|
|
150
|
+
setActiveIdx(sheets.length);
|
|
151
|
+
};
|
|
152
|
+
const removeSheet = (idx) => {
|
|
153
|
+
if (sheets.length <= 1) return;
|
|
154
|
+
setSheets((prev) => prev.filter((_, i) => i !== idx));
|
|
155
|
+
if (activeIdx >= idx && activeIdx > 0) setActiveIdx(activeIdx - 1);
|
|
156
|
+
};
|
|
157
|
+
const renameSheet = (idx, name) => {
|
|
158
|
+
setSheets((prev) => prev.map((s, i) => i === idx ? { ...s, name } : s));
|
|
159
|
+
setEditingTab(null);
|
|
160
|
+
};
|
|
161
|
+
const addColumns = (count) => {
|
|
162
|
+
const newColCount = columns.length + count;
|
|
163
|
+
updateActiveSheet({
|
|
164
|
+
columns: makeColumns(newColCount),
|
|
165
|
+
data: data.map((row) => [...row, ...Array(count).fill("")])
|
|
166
|
+
});
|
|
167
|
+
};
|
|
168
|
+
const addRows = (count) => {
|
|
169
|
+
updateActiveSheet({
|
|
170
|
+
data: [...data, ...Array.from({ length: count }, () => Array(columns.length).fill(""))]
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
const handleClear = () => {
|
|
174
|
+
updateActiveSheet({
|
|
175
|
+
columns: makeColumns(DEFAULT_COLS),
|
|
176
|
+
data: makeEmptyData(DEFAULT_ROWS, DEFAULT_COLS)
|
|
177
|
+
});
|
|
178
|
+
};
|
|
179
|
+
const exportCSV = () => {
|
|
180
|
+
const csv = data.filter((row) => row.some((c) => c.trim())).map((row) => row.map((cell) => {
|
|
181
|
+
if (cell.includes(",") || cell.includes('"') || cell.includes("\n"))
|
|
182
|
+
return `"${cell.replace(/"/g, '""')}"`;
|
|
183
|
+
return cell;
|
|
184
|
+
}).join(",")).join("\n");
|
|
185
|
+
const blob = new Blob([csv], { type: "text/csv" });
|
|
186
|
+
const url = URL.createObjectURL(blob);
|
|
187
|
+
const a = document.createElement("a");
|
|
188
|
+
a.href = url;
|
|
189
|
+
a.download = `${title || "spreadsheet"}.csv`;
|
|
190
|
+
a.click();
|
|
191
|
+
URL.revokeObjectURL(url);
|
|
192
|
+
};
|
|
193
|
+
const importFile = async (e) => {
|
|
194
|
+
const file = e.target.files?.[0];
|
|
195
|
+
if (!file) return;
|
|
196
|
+
const name = file.name.replace(/\.(csv|tsv|txt|xlsx|xls|ods)$/i, "");
|
|
197
|
+
if (/\.(xlsx|xls|ods)$/i.test(file.name)) {
|
|
198
|
+
const byteArr = new Uint8Array(await file.arrayBuffer());
|
|
199
|
+
const XLSX = await import('xlsx');
|
|
200
|
+
const wb = XLSX.read(byteArr, { type: "array" });
|
|
201
|
+
const newSheets = wb.SheetNames.map((sn) => {
|
|
202
|
+
const rows = XLSX.utils.sheet_to_json(wb.Sheets[sn], { header: 1, defval: "" });
|
|
203
|
+
const maxCols = Math.max(DEFAULT_COLS, rows.reduce((m, r) => Math.max(m, r.length), 0));
|
|
204
|
+
const padded = rows.map((r) => {
|
|
205
|
+
const nr = r.map((c) => String(c ?? ""));
|
|
206
|
+
while (nr.length < maxCols) nr.push("");
|
|
207
|
+
return nr;
|
|
208
|
+
});
|
|
209
|
+
while (padded.length < DEFAULT_ROWS) padded.push(Array(maxCols).fill(""));
|
|
210
|
+
return { id: crypto.randomUUID(), name: sn, columns: makeColumns(maxCols), data: padded };
|
|
211
|
+
});
|
|
212
|
+
setSheets(newSheets);
|
|
213
|
+
setActiveIdx(0);
|
|
214
|
+
setTitle(name);
|
|
215
|
+
} else {
|
|
216
|
+
const text = await file.text();
|
|
217
|
+
const parsed = parseCSV(text);
|
|
218
|
+
if (parsed.length === 0) return;
|
|
219
|
+
const maxCols = Math.max(DEFAULT_COLS, parsed.reduce((m, r) => Math.max(m, r.length), 0));
|
|
220
|
+
const padded = parsed.map((r) => {
|
|
221
|
+
while (r.length < maxCols) r.push("");
|
|
222
|
+
return r;
|
|
223
|
+
});
|
|
224
|
+
while (padded.length < DEFAULT_ROWS) padded.push(Array(maxCols).fill(""));
|
|
225
|
+
updateActiveSheet({ columns: makeColumns(maxCols), data: padded });
|
|
226
|
+
setTitle(name);
|
|
227
|
+
}
|
|
228
|
+
if (fileRef.current) fileRef.current.value = "";
|
|
229
|
+
};
|
|
230
|
+
const allNums = [];
|
|
231
|
+
data.forEach((row) => row.forEach((cell) => {
|
|
232
|
+
const v = parseFloat(cell);
|
|
233
|
+
if (!isNaN(v) && cell.trim()) allNums.push(v);
|
|
234
|
+
}));
|
|
235
|
+
const filledCount = data.reduce((c, row) => c + row.filter((cell) => cell.trim()).length, 0);
|
|
236
|
+
return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: "flex flex-col h-full", children: [
|
|
237
|
+
/* @__PURE__ */ jsx(WindowTitle, { title: `${truncateForTitle(title || "Untitled")} - Spreadsheets` }),
|
|
238
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-3 py-2 border-b border-gray-200 bg-gray-50 shrink-0", children: [
|
|
239
|
+
editingTitle ? /* @__PURE__ */ jsx(
|
|
240
|
+
"input",
|
|
241
|
+
{
|
|
242
|
+
type: "text",
|
|
243
|
+
value: title,
|
|
244
|
+
onChange: (e) => setTitle(e.target.value),
|
|
245
|
+
onBlur: () => setEditingTitle(false),
|
|
246
|
+
onKeyDown: (e) => {
|
|
247
|
+
if (e.key === "Enter") setEditingTitle(false);
|
|
248
|
+
},
|
|
249
|
+
autoFocus: true,
|
|
250
|
+
className: "text-sm font-medium text-gray-900 border border-gray-300 rounded px-2 py-0.5 w-40 focus:border-blue-500 focus:ring-blue-500"
|
|
251
|
+
}
|
|
252
|
+
) : /* @__PURE__ */ jsx("button", { onClick: () => setEditingTitle(true), className: "text-sm font-medium text-gray-900 hover:text-blue-600 truncate max-w-[200px]", title: "Click to rename", children: title || "Untitled" }),
|
|
253
|
+
/* @__PURE__ */ jsx("div", { className: "h-4 w-px bg-gray-300" }),
|
|
254
|
+
/* @__PURE__ */ jsx("input", { ref: fileRef, type: "file", accept: ".csv,.tsv,.txt,.xlsx,.xls,.ods", onChange: importFile, className: "hidden" }),
|
|
255
|
+
/* @__PURE__ */ jsx(
|
|
256
|
+
"button",
|
|
257
|
+
{
|
|
258
|
+
onClick: () => fileRef.current?.click(),
|
|
259
|
+
className: "text-xs text-gray-600 hover:text-gray-900 px-2 py-1 rounded hover:bg-gray-200 transition-colors",
|
|
260
|
+
children: "Open"
|
|
261
|
+
}
|
|
262
|
+
),
|
|
263
|
+
/* @__PURE__ */ jsx(
|
|
264
|
+
"button",
|
|
265
|
+
{
|
|
266
|
+
onClick: exportCSV,
|
|
267
|
+
className: "text-xs text-gray-600 hover:text-gray-900 px-2 py-1 rounded hover:bg-gray-200 transition-colors",
|
|
268
|
+
children: "Save CSV"
|
|
269
|
+
}
|
|
270
|
+
),
|
|
271
|
+
/* @__PURE__ */ jsx("div", { className: "h-4 w-px bg-gray-300" }),
|
|
272
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
273
|
+
/* @__PURE__ */ jsx(
|
|
274
|
+
"input",
|
|
275
|
+
{
|
|
276
|
+
type: "number",
|
|
277
|
+
min: "1",
|
|
278
|
+
max: "50",
|
|
279
|
+
value: addColCount,
|
|
280
|
+
onChange: (e) => setAddColCount(e.target.value),
|
|
281
|
+
className: "w-10 text-xs text-center border border-gray-300 rounded px-1 py-0.5 focus:border-blue-500 focus:ring-blue-500"
|
|
282
|
+
}
|
|
283
|
+
),
|
|
284
|
+
/* @__PURE__ */ jsx(
|
|
285
|
+
"button",
|
|
286
|
+
{
|
|
287
|
+
onClick: () => addColumns(parseInt(addColCount) || 1),
|
|
288
|
+
className: "text-xs text-gray-600 hover:text-gray-900 px-2 py-1 rounded hover:bg-gray-200 transition-colors",
|
|
289
|
+
children: "+ Col"
|
|
290
|
+
}
|
|
291
|
+
)
|
|
292
|
+
] }),
|
|
293
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
294
|
+
/* @__PURE__ */ jsx(
|
|
295
|
+
"input",
|
|
296
|
+
{
|
|
297
|
+
type: "number",
|
|
298
|
+
min: "1",
|
|
299
|
+
max: "500",
|
|
300
|
+
value: addRowCount,
|
|
301
|
+
onChange: (e) => setAddRowCount(e.target.value),
|
|
302
|
+
className: "w-10 text-xs text-center border border-gray-300 rounded px-1 py-0.5 focus:border-blue-500 focus:ring-blue-500"
|
|
303
|
+
}
|
|
304
|
+
),
|
|
305
|
+
/* @__PURE__ */ jsx(
|
|
306
|
+
"button",
|
|
307
|
+
{
|
|
308
|
+
onClick: () => addRows(parseInt(addRowCount) || 10),
|
|
309
|
+
className: "text-xs text-gray-600 hover:text-gray-900 px-2 py-1 rounded hover:bg-gray-200 transition-colors",
|
|
310
|
+
children: "+ Row"
|
|
311
|
+
}
|
|
312
|
+
)
|
|
313
|
+
] }),
|
|
314
|
+
/* @__PURE__ */ jsx("div", { className: "h-4 w-px bg-gray-300" }),
|
|
315
|
+
/* @__PURE__ */ jsx(
|
|
316
|
+
"button",
|
|
317
|
+
{
|
|
318
|
+
onClick: handleClear,
|
|
319
|
+
className: "text-xs text-gray-600 hover:text-gray-900 px-2 py-1 rounded hover:bg-gray-200 transition-colors",
|
|
320
|
+
children: "Clear"
|
|
321
|
+
}
|
|
322
|
+
),
|
|
323
|
+
/* @__PURE__ */ jsx("div", { className: "h-4 w-px bg-gray-300" }),
|
|
324
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5", title: hasTarget ? "" : "Click a cell first", children: [
|
|
325
|
+
/* @__PURE__ */ jsx(
|
|
326
|
+
"button",
|
|
327
|
+
{
|
|
328
|
+
onClick: () => toggleCellStyle("bold"),
|
|
329
|
+
disabled: !hasTarget,
|
|
330
|
+
className: `px-2 py-1 text-xs rounded transition-colors font-bold ${focusedStyle.bold ? "bg-blue-100 text-blue-700" : "text-gray-600 hover:bg-gray-200"} disabled:opacity-40 disabled:cursor-not-allowed`,
|
|
331
|
+
children: "B"
|
|
332
|
+
}
|
|
333
|
+
),
|
|
334
|
+
/* @__PURE__ */ jsx(
|
|
335
|
+
"button",
|
|
336
|
+
{
|
|
337
|
+
onClick: () => toggleCellStyle("italic"),
|
|
338
|
+
disabled: !hasTarget,
|
|
339
|
+
className: `px-2 py-1 text-xs rounded transition-colors italic ${focusedStyle.italic ? "bg-blue-100 text-blue-700" : "text-gray-600 hover:bg-gray-200"} disabled:opacity-40 disabled:cursor-not-allowed`,
|
|
340
|
+
children: "I"
|
|
341
|
+
}
|
|
342
|
+
),
|
|
343
|
+
/* @__PURE__ */ jsx(
|
|
344
|
+
"button",
|
|
345
|
+
{
|
|
346
|
+
onClick: () => toggleCellStyle("underline"),
|
|
347
|
+
disabled: !hasTarget,
|
|
348
|
+
className: `px-2 py-1 text-xs rounded transition-colors underline ${focusedStyle.underline ? "bg-blue-100 text-blue-700" : "text-gray-600 hover:bg-gray-200"} disabled:opacity-40 disabled:cursor-not-allowed`,
|
|
349
|
+
children: "U"
|
|
350
|
+
}
|
|
351
|
+
),
|
|
352
|
+
/* @__PURE__ */ jsxs(
|
|
353
|
+
"select",
|
|
354
|
+
{
|
|
355
|
+
value: focusedStyle.fontSize ?? "base",
|
|
356
|
+
onChange: (e) => toggleCellStyle("fontSize", e.target.value),
|
|
357
|
+
disabled: !hasTarget,
|
|
358
|
+
className: "ml-1 text-xs border border-gray-300 rounded px-1 py-0.5 bg-white disabled:opacity-40 disabled:cursor-not-allowed",
|
|
359
|
+
children: [
|
|
360
|
+
/* @__PURE__ */ jsx("option", { value: "sm", children: "XS" }),
|
|
361
|
+
/* @__PURE__ */ jsx("option", { value: "base", children: "S" }),
|
|
362
|
+
/* @__PURE__ */ jsx("option", { value: "lg", children: "M" }),
|
|
363
|
+
/* @__PURE__ */ jsx("option", { value: "xl", children: "L" })
|
|
364
|
+
]
|
|
365
|
+
}
|
|
366
|
+
)
|
|
367
|
+
] })
|
|
368
|
+
] }),
|
|
369
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1 min-h-0", children: /* @__PURE__ */ jsx(
|
|
370
|
+
EditableGrid,
|
|
371
|
+
{
|
|
372
|
+
columns,
|
|
373
|
+
data,
|
|
374
|
+
onChange: handleChange,
|
|
375
|
+
onColumnsChange: (newCols) => updateActiveSheet({ columns: newCols }),
|
|
376
|
+
cellStyles,
|
|
377
|
+
onFocusChange: setFocusedCell,
|
|
378
|
+
onSelectionChange: setSelection,
|
|
379
|
+
minRows: DEFAULT_ROWS,
|
|
380
|
+
maxHeight: "100%"
|
|
381
|
+
}
|
|
382
|
+
) }),
|
|
383
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center border-t border-gray-200 bg-gray-50 shrink-0", children: [
|
|
384
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5 px-1 py-1 overflow-x-auto flex-1 min-w-0", children: [
|
|
385
|
+
sheets.map((sheet, idx) => /* @__PURE__ */ jsx(
|
|
386
|
+
"button",
|
|
387
|
+
{
|
|
388
|
+
onClick: () => setActiveIdx(idx),
|
|
389
|
+
onDoubleClick: () => {
|
|
390
|
+
setEditingTab(idx);
|
|
391
|
+
setTabName(sheet.name);
|
|
392
|
+
},
|
|
393
|
+
onContextMenu: (e) => {
|
|
394
|
+
e.preventDefault();
|
|
395
|
+
if (sheets.length > 1) removeSheet(idx);
|
|
396
|
+
},
|
|
397
|
+
className: `px-3 py-1 text-xs font-medium rounded-b whitespace-nowrap transition-colors ${idx === activeIdx ? "bg-white text-blue-700 border border-t-0 border-gray-300 -mt-px relative z-10" : "text-gray-500 hover:text-gray-700 hover:bg-gray-100"}`,
|
|
398
|
+
children: editingTab === idx ? /* @__PURE__ */ jsx(
|
|
399
|
+
"input",
|
|
400
|
+
{
|
|
401
|
+
type: "text",
|
|
402
|
+
value: tabName,
|
|
403
|
+
onChange: (e) => setTabName(e.target.value),
|
|
404
|
+
onBlur: () => renameSheet(idx, tabName || sheet.name),
|
|
405
|
+
onKeyDown: (e) => {
|
|
406
|
+
if (e.key === "Enter") renameSheet(idx, tabName || sheet.name);
|
|
407
|
+
if (e.key === "Escape") setEditingTab(null);
|
|
408
|
+
},
|
|
409
|
+
onClick: (e) => e.stopPropagation(),
|
|
410
|
+
autoFocus: true,
|
|
411
|
+
className: "w-20 text-xs border border-blue-400 rounded px-1 py-0 focus:ring-0 focus:outline-none"
|
|
412
|
+
}
|
|
413
|
+
) : sheet.name
|
|
414
|
+
},
|
|
415
|
+
sheet.id
|
|
416
|
+
)),
|
|
417
|
+
/* @__PURE__ */ jsx("button", { onClick: addSheet, className: "px-2 py-1 text-xs text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded", title: "Add sheet", children: "+" })
|
|
418
|
+
] }),
|
|
419
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-4 px-3 py-1 text-xs text-gray-500 shrink-0 border-l border-gray-200", children: [
|
|
420
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
421
|
+
filledCount,
|
|
422
|
+
" cells"
|
|
423
|
+
] }),
|
|
424
|
+
allNums.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
425
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
426
|
+
"Sum: ",
|
|
427
|
+
allNums.reduce((s, v) => s + v, 0).toLocaleString(void 0, { maximumFractionDigits: 2 })
|
|
428
|
+
] }),
|
|
429
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
430
|
+
"Avg: ",
|
|
431
|
+
(allNums.reduce((s, v) => s + v, 0) / allNums.length).toLocaleString(void 0, { maximumFractionDigits: 2 })
|
|
432
|
+
] })
|
|
433
|
+
] }),
|
|
434
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
435
|
+
data.length,
|
|
436
|
+
" \xD7 ",
|
|
437
|
+
columns.length
|
|
438
|
+
] })
|
|
439
|
+
] })
|
|
440
|
+
] })
|
|
441
|
+
] });
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export { Spreadsheet as default };
|
|
445
|
+
//# sourceMappingURL=Spreadsheet-IOKEDNS6.js.map
|
|
446
|
+
//# sourceMappingURL=Spreadsheet-IOKEDNS6.js.map
|