hyperbook 0.83.0 → 0.84.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.
@@ -935,3 +935,197 @@ 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
+ font-family: hyperbook-body, sans-serif;
1100
+ position: fixed;
1101
+ top: 0;
1102
+ left: 0;
1103
+ right: 0;
1104
+ z-index: 10000;
1105
+ background: #e67e22;
1106
+ color: white;
1107
+ padding: 8px 16px;
1108
+ display: flex;
1109
+ justify-content: center;
1110
+ align-items: center;
1111
+ gap: 16px;
1112
+ font-size: 1rem;
1113
+ }
1114
+
1115
+ #impersonation-banner a {
1116
+ color: white;
1117
+ font-weight: bold;
1118
+ text-decoration: underline;
1119
+ }
1120
+
1121
+ #impersonation-banner a:hover {
1122
+ opacity: 0.8;
1123
+ }
1124
+
1125
+ #impersonation-banner ~ * {
1126
+ margin-top: 36px;
1127
+ }
1128
+
1129
+ .hidden {
1130
+ display: none !important;
1131
+ }
@@ -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({
@@ -68,20 +195,21 @@ const initStore = async () => {
68
195
  });
69
196
  });
70
197
  };
198
+
71
199
  initStore();
72
200
 
73
201
  async function hyperbookExport() {
74
202
  const hyperbook = await store.export({ prettyJson: true });
75
- const sqlIde = await sqlIdeDB.export({ prettyJson: true });
76
- const learnJ = await learnJDB.export({ prettyJson: true });
203
+ const sqlIde = await exportExternalDB('SQL-IDE');
204
+ const learnJ = await exportExternalDB('LearnJ');
77
205
 
78
206
  const data = {
79
207
  version: 1,
80
208
  origin: window.location.origin,
81
209
  data: {
82
210
  hyperbook: JSON.parse(await hyperbook.text()),
83
- sqlIde: JSON.parse(await sqlIde.text()),
84
- learnJ: JSON.parse(await learnJ.text()),
211
+ sqlIde: sqlIde || {},
212
+ learnJ: learnJ || {},
85
213
  },
86
214
  };
87
215
 
@@ -108,8 +236,8 @@ async function hyperbookReset() {
108
236
  }
109
237
 
110
238
  clearTable(store);
111
- clearTable(learnJDB);
112
- clearTable(sqlIdeDB);
239
+ await clearExternalDB('LearnJ');
240
+ await clearExternalDB('SQL-IDE');
113
241
 
114
242
  alert(i18n.get("store-reset-sucessful"));
115
243
  window.location.reload();
@@ -146,16 +274,14 @@ async function hyperbookImport() {
146
274
  const hyperbookBlob = new Blob([JSON.stringify(hyperbook)], {
147
275
  type: "application/json",
148
276
  });
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
277
 
156
278
  await store.import(hyperbookBlob, { clearTablesBeforeImport: true });
157
- await sqlIdeDB.import(sqlIdeBlob, { clearTablesBeforeImport: true });
158
- await learnJDB.import(learnJBlob, { clearTablesBeforeImport: true });
279
+ if (sqlIde) {
280
+ await importExternalDB('SQL-IDE', sqlIde);
281
+ }
282
+ if (learnJ) {
283
+ await importExternalDB('LearnJ', learnJ);
284
+ }
159
285
 
160
286
  alert(i18n.get("store-import-sucessful"));
161
287
  window.location.reload();