hyperbook 0.83.0 → 0.84.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.
@@ -935,3 +935,196 @@ nav.toc li.level-3 {
935
935
  .main-grid.layout-standalone .breadcrumb {
936
936
  display: none;
937
937
  }
938
+
939
+ /* User Authentication UI */
940
+ #user-toggle {
941
+ margin-right: 8px;
942
+ line-height: 1;
943
+ background: none;
944
+ border: none;
945
+ cursor: pointer;
946
+ padding: 8px;
947
+ display: flex;
948
+ align-items: center;
949
+ justify-content: center;
950
+ }
951
+
952
+ .user-icon {
953
+ width: 24px;
954
+ height: 24px;
955
+ background-color: var(--color-brand-text);
956
+ mask-size: contain;
957
+ mask-repeat: no-repeat;
958
+ mask-position: center;
959
+ /* not-logged-in: person with question mark */
960
+ mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z"/></svg>');
961
+ opacity: 0.5;
962
+ }
963
+
964
+ /* logged-in: solid person */
965
+ .user-icon[data-state="logged-in"] {
966
+ mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z"/></svg>');
967
+ opacity: 1;
968
+ }
969
+
970
+ /* unsynced: person with exclamation */
971
+ .user-icon[data-state="unsynced"] {
972
+ mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z"/></svg>');
973
+ opacity: 1;
974
+ background-color: #f59e0b;
975
+ }
976
+
977
+ /* syncing: person with rotating animation */
978
+ .user-icon[data-state="syncing"] {
979
+ mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z"/></svg>');
980
+ opacity: 1;
981
+ animation: user-icon-pulse 1s ease-in-out infinite;
982
+ }
983
+
984
+ /* synced: person with checkmark feel */
985
+ .user-icon[data-state="synced"] {
986
+ mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z"/></svg>');
987
+ opacity: 1;
988
+ background-color: #22c55e;
989
+ }
990
+
991
+ @keyframes user-icon-pulse {
992
+ 0%, 100% { opacity: 1; }
993
+ 50% { opacity: 0.4; }
994
+ }
995
+
996
+ #user-drawer {
997
+ background: var(--color-background);
998
+ border-left: 1px solid var(--color-nav-border);
999
+ }
1000
+
1001
+ .user-drawer-content {
1002
+ padding: 20px;
1003
+ width: 100%;
1004
+ max-width: 400px;
1005
+ }
1006
+
1007
+ .user-form,
1008
+ .user-info {
1009
+ display: flex;
1010
+ flex-direction: column;
1011
+ gap: 15px;
1012
+ }
1013
+
1014
+ .user-form h3,
1015
+ .user-info h3 {
1016
+ margin: 0 0 10px 0;
1017
+ color: var(--color-text);
1018
+ }
1019
+
1020
+ .user-form input {
1021
+ padding: 10px;
1022
+ border: 1px solid var(--color-nav-border);
1023
+ border-radius: 4px;
1024
+ background: var(--color-background);
1025
+ color: var(--color-text);
1026
+ font-size: 14px;
1027
+ }
1028
+
1029
+ .user-form button,
1030
+ .user-info button {
1031
+ padding: 10px 20px;
1032
+ border: none;
1033
+ border-radius: 4px;
1034
+ cursor: pointer;
1035
+ font-size: 14px;
1036
+ background: var(--color-brand);
1037
+ color: var(--color-brand-text);
1038
+ }
1039
+
1040
+ .user-form button:hover,
1041
+ .user-info button:hover {
1042
+ opacity: 0.9;
1043
+ }
1044
+
1045
+ .user-info .logout-btn {
1046
+ background: #e74c3c;
1047
+ color: white;
1048
+ }
1049
+
1050
+ .user-info .logout-btn:hover {
1051
+ background: #c0392b;
1052
+ }
1053
+
1054
+ .user-error {
1055
+ color: #e74c3c;
1056
+ font-size: 14px;
1057
+ min-height: 20px;
1058
+ }
1059
+
1060
+ .user-info p {
1061
+ margin: 5px 0;
1062
+ color: var(--color-text);
1063
+ }
1064
+
1065
+ #user-save-status {
1066
+ font-weight: normal;
1067
+ }
1068
+
1069
+ #user-save-status.unsaved {
1070
+ color: #f39c12;
1071
+ }
1072
+
1073
+ #user-save-status.saving {
1074
+ color: #f39c12;
1075
+ }
1076
+
1077
+ #user-save-status.saved {
1078
+ color: #27ae60;
1079
+ }
1080
+
1081
+ #user-save-status.error {
1082
+ color: #e74c3c;
1083
+ }
1084
+
1085
+ #user-save-status.offline {
1086
+ color: #95a5a6;
1087
+ }
1088
+
1089
+ #user-save-status.offline-queued {
1090
+ color: #f39c12;
1091
+ }
1092
+
1093
+ #user-save-status.readonly {
1094
+ color: #e67e22;
1095
+ font-weight: bold;
1096
+ }
1097
+
1098
+ #impersonation-banner {
1099
+ position: fixed;
1100
+ top: 0;
1101
+ left: 0;
1102
+ right: 0;
1103
+ z-index: 10000;
1104
+ background: #e67e22;
1105
+ color: white;
1106
+ padding: 8px 16px;
1107
+ display: flex;
1108
+ justify-content: center;
1109
+ align-items: center;
1110
+ gap: 16px;
1111
+ font-size: 14px;
1112
+ }
1113
+
1114
+ #impersonation-banner a {
1115
+ color: white;
1116
+ font-weight: bold;
1117
+ text-decoration: underline;
1118
+ }
1119
+
1120
+ #impersonation-banner a:hover {
1121
+ opacity: 0.8;
1122
+ }
1123
+
1124
+ #impersonation-banner ~ * {
1125
+ margin-top: 36px;
1126
+ }
1127
+
1128
+ .hidden {
1129
+ display: none !important;
1130
+ }
@@ -9,7 +9,7 @@ store.version(2).stores({
9
9
  mouseX,
10
10
  mouseY,
11
11
  scrollX,
12
- scorllY,
12
+ scrollY,
13
13
  windowWidth,
14
14
  windowHeight
15
15
  `,
@@ -32,14 +32,141 @@ store.version(2).stores({
32
32
  multievent: `id,state`,
33
33
  typst: `id,code`,
34
34
  });
35
- var sqlIdeDB = new Dexie("SQL-IDE");
36
- sqlIdeDB.version(0.1).stores({
37
- scripts: `scriptId,script`,
38
- });
39
- var learnJDB = new Dexie("LearnJ");
40
- learnJDB.version(1).stores({
41
- scripts: `scriptId,script`,
42
- });
35
+
36
+ /**
37
+ * Read all data from an external IndexedDB database using the raw API.
38
+ * Returns a Dexie-export-compatible object, or null if the DB doesn't exist.
39
+ */
40
+ function exportExternalDB(dbName) {
41
+ return new Promise((resolve) => {
42
+ const request = indexedDB.open(dbName);
43
+ request.onerror = () => resolve(null);
44
+ request.onsuccess = (event) => {
45
+ const db = event.target.result;
46
+ const storeNames = Array.from(db.objectStoreNames);
47
+ if (storeNames.length === 0) {
48
+ db.close();
49
+ resolve(null);
50
+ return;
51
+ }
52
+ const result = {
53
+ formatName: "dexie",
54
+ formatVersion: 1,
55
+ data: {
56
+ databaseName: dbName,
57
+ databaseVersion: db.version,
58
+ tables: [],
59
+ },
60
+ };
61
+ const tx = db.transaction(storeNames, "readonly");
62
+ let pending = storeNames.length;
63
+ storeNames.forEach((name) => {
64
+ const objectStore = tx.objectStore(name);
65
+ const tableInfo = {
66
+ name: name,
67
+ schema: objectStore.keyPath ? `${objectStore.keyPath}` : "++id",
68
+ rowCount: 0,
69
+ rows: [],
70
+ };
71
+ const cursorReq = objectStore.openCursor();
72
+ cursorReq.onsuccess = (e) => {
73
+ const cursor = e.target.result;
74
+ if (cursor) {
75
+ tableInfo.rows.push(cursor.value);
76
+ cursor.continue();
77
+ } else {
78
+ tableInfo.rowCount = tableInfo.rows.length;
79
+ result.data.tables.push(tableInfo);
80
+ pending--;
81
+ if (pending === 0) {
82
+ db.close();
83
+ resolve(result);
84
+ }
85
+ }
86
+ };
87
+ cursorReq.onerror = () => {
88
+ pending--;
89
+ if (pending === 0) {
90
+ db.close();
91
+ resolve(result);
92
+ }
93
+ };
94
+ });
95
+ };
96
+ });
97
+ }
98
+
99
+ /**
100
+ * Import data into an external IndexedDB database using the raw API.
101
+ * Accepts a Dexie-export-compatible object. Clears existing data before importing.
102
+ */
103
+ function importExternalDB(dbName, exportData) {
104
+ return new Promise((resolve, reject) => {
105
+ const tables = exportData?.data?.tables;
106
+ if (!tables || tables.length === 0) { resolve(); return; }
107
+
108
+ // Determine the version the external tool uses (keep it in sync)
109
+ const request = indexedDB.open(dbName);
110
+ request.onerror = () => reject(request.error);
111
+
112
+ request.onupgradeneeded = (event) => {
113
+ // DB didn't exist yet — create the object stores from the export data
114
+ const db = event.target.result;
115
+ tables.forEach((table) => {
116
+ if (!db.objectStoreNames.contains(table.name)) {
117
+ const keyPath = table.schema && !table.schema.startsWith('++')
118
+ ? table.schema.split(',')[0].trim()
119
+ : null;
120
+ db.createObjectStore(table.name, keyPath ? { keyPath } : { autoIncrement: true });
121
+ }
122
+ });
123
+ };
124
+
125
+ request.onsuccess = (event) => {
126
+ const db = event.target.result;
127
+ const storeNames = tables
128
+ .map((t) => t.name)
129
+ .filter((n) => db.objectStoreNames.contains(n));
130
+ if (storeNames.length === 0) { db.close(); resolve(); return; }
131
+
132
+ const tx = db.transaction(storeNames, "readwrite");
133
+ // Clear then re-populate each store
134
+ storeNames.forEach((name) => {
135
+ const objectStore = tx.objectStore(name);
136
+ objectStore.clear();
137
+ const table = tables.find((t) => t.name === name);
138
+ if (table && table.rows) {
139
+ table.rows.forEach((row) => objectStore.put(row));
140
+ }
141
+ });
142
+ tx.oncomplete = () => { db.close(); resolve(); };
143
+ tx.onerror = () => { db.close(); reject(tx.error); };
144
+ };
145
+ });
146
+ }
147
+
148
+ /**
149
+ * Clear all tables in an external IndexedDB database using the raw API.
150
+ */
151
+ function clearExternalDB(dbName) {
152
+ return new Promise((resolve) => {
153
+ const request = indexedDB.open(dbName);
154
+ request.onerror = () => resolve();
155
+ request.onsuccess = (event) => {
156
+ const db = event.target.result;
157
+ const storeNames = Array.from(db.objectStoreNames);
158
+ if (storeNames.length === 0) {
159
+ db.close();
160
+ resolve();
161
+ return;
162
+ }
163
+ const tx = db.transaction(storeNames, "readwrite");
164
+ storeNames.forEach((name) => tx.objectStore(name).clear());
165
+ tx.oncomplete = () => { db.close(); resolve(); };
166
+ tx.onerror = () => { db.close(); resolve(); };
167
+ };
168
+ });
169
+ }
43
170
 
44
171
  const initStore = async () => {
45
172
  store.currentState.put({
@@ -67,21 +194,27 @@ const initStore = async () => {
67
194
  windowHeight: window.innerHeight,
68
195
  });
69
196
  });
197
+
198
+ // Initialize cloud integration if configured
199
+ if (window.hyperbook.cloud) {
200
+ await window.hyperbook.cloud.initializeStore(store);
201
+ }
70
202
  };
203
+
71
204
  initStore();
72
205
 
73
206
  async function hyperbookExport() {
74
207
  const hyperbook = await store.export({ prettyJson: true });
75
- const sqlIde = await sqlIdeDB.export({ prettyJson: true });
76
- const learnJ = await learnJDB.export({ prettyJson: true });
208
+ const sqlIde = await exportExternalDB('SQL-IDE');
209
+ const learnJ = await exportExternalDB('LearnJ');
77
210
 
78
211
  const data = {
79
212
  version: 1,
80
213
  origin: window.location.origin,
81
214
  data: {
82
215
  hyperbook: JSON.parse(await hyperbook.text()),
83
- sqlIde: JSON.parse(await sqlIde.text()),
84
- learnJ: JSON.parse(await learnJ.text()),
216
+ sqlIde: sqlIde || {},
217
+ learnJ: learnJ || {},
85
218
  },
86
219
  };
87
220
 
@@ -108,8 +241,8 @@ async function hyperbookReset() {
108
241
  }
109
242
 
110
243
  clearTable(store);
111
- clearTable(learnJDB);
112
- clearTable(sqlIdeDB);
244
+ await clearExternalDB('LearnJ');
245
+ await clearExternalDB('SQL-IDE');
113
246
 
114
247
  alert(i18n.get("store-reset-sucessful"));
115
248
  window.location.reload();
@@ -146,16 +279,14 @@ async function hyperbookImport() {
146
279
  const hyperbookBlob = new Blob([JSON.stringify(hyperbook)], {
147
280
  type: "application/json",
148
281
  });
149
- const sqlIdeBlob = new Blob([JSON.stringify(sqlIde)], {
150
- type: "application/json",
151
- });
152
- const learnJBlob = new Blob([JSON.stringify(learnJ)], {
153
- type: "application/json",
154
- });
155
282
 
156
283
  await store.import(hyperbookBlob, { clearTablesBeforeImport: true });
157
- await sqlIdeDB.import(sqlIdeBlob, { clearTablesBeforeImport: true });
158
- await learnJDB.import(learnJBlob, { clearTablesBeforeImport: true });
284
+ if (sqlIde) {
285
+ await importExternalDB('SQL-IDE', sqlIde);
286
+ }
287
+ if (learnJ) {
288
+ await importExternalDB('LearnJ', learnJ);
289
+ }
159
290
 
160
291
  alert(i18n.get("store-import-sucessful"));
161
292
  window.location.reload();