hyperbook 0.90.0 → 0.91.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.
@@ -5,6 +5,7 @@
5
5
  flex-direction: column;
6
6
  overflow: hidden;
7
7
  gap: 8px;
8
+ height: var(--pyide-height, calc(100dvh - 80px));
8
9
  }
9
10
 
10
11
  .directive-pyide code-input {
@@ -14,7 +15,9 @@
14
15
  .directive-pyide .container {
15
16
  width: 100%;
16
17
  overflow: hidden;
17
- height: 200px;
18
+ min-height: 120px;
19
+ min-width: 120px;
20
+ flex: 0 0 200px;
18
21
  display: flex;
19
22
  flex-direction: column;
20
23
  }
@@ -35,6 +38,57 @@
35
38
  font-family: hyperbook-monospace, monospace;
36
39
  }
37
40
 
41
+ .directive-pyide .output .error-line {
42
+ color: #b42318;
43
+ }
44
+
45
+ .directive-pyide .canvas-wrapper {
46
+ overflow: auto;
47
+ height: 100%;
48
+ border: 1px solid var(--color-spacer);
49
+ border-radius: 8px;
50
+ border-top-left-radius: 0;
51
+ border-top-right-radius: 0;
52
+ }
53
+
54
+ .directive-pyide .canvas {
55
+ display: block;
56
+ width: auto;
57
+ height: auto;
58
+ min-height: 300px;
59
+ }
60
+
61
+ .directive-pyide .canvas-header,
62
+ .directive-pyide .output-header {
63
+ border: 1px solid var(--color-spacer);
64
+ border-bottom: none;
65
+ border-top-left-radius: 8px;
66
+ border-top-right-radius: 8px;
67
+ padding: 8px 16px;
68
+ background-color: var(--color-spacer);
69
+ color: var(--color-text);
70
+ font-weight: 600;
71
+ text-align: center;
72
+ }
73
+
74
+ .directive-pyide .canvas-output-splitter {
75
+ display: none;
76
+ width: 100%;
77
+ height: 4px;
78
+ margin: 10px 0;
79
+ cursor: row-resize;
80
+ background: var(--color-spacer);
81
+ border-radius: 999px;
82
+ flex-shrink: 0;
83
+ touch-action: none;
84
+ opacity: 0.45;
85
+ transition: opacity 0.15s ease-in-out;
86
+ }
87
+
88
+ .directive-pyide .canvas-output-splitter:hover {
89
+ opacity: 0.65;
90
+ }
91
+
38
92
  .directive-pyide .hidden {
39
93
  display: none;
40
94
  }
@@ -43,7 +97,43 @@
43
97
  width: 100%;
44
98
  display: flex;
45
99
  flex-direction: column;
46
- height: 400px;
100
+ min-height: 120px;
101
+ min-width: 120px;
102
+ flex: 1 1 400px;
103
+ overflow: hidden;
104
+ }
105
+
106
+ .directive-pyide .splitter {
107
+ background: var(--color-spacer);
108
+ border-radius: 999px;
109
+ flex-shrink: 0;
110
+ touch-action: none;
111
+ opacity: 0.45;
112
+ transition: opacity 0.15s ease-in-out;
113
+ }
114
+
115
+ .directive-pyide .splitter:hover {
116
+ opacity: 0.65;
117
+ }
118
+
119
+ .directive-pyide.split-vertical .splitter {
120
+ width: 100%;
121
+ height: 4px;
122
+ cursor: row-resize;
123
+ }
124
+
125
+ .directive-pyide.split-horizontal .splitter {
126
+ width: 4px;
127
+ height: 100%;
128
+ cursor: col-resize;
129
+ }
130
+
131
+ .directive-pyide.resizing {
132
+ user-select: none;
133
+ }
134
+
135
+ .directive-pyide.resizing .splitter {
136
+ opacity: 0.75;
47
137
  }
48
138
 
49
139
  .directive-pyide .editor {
@@ -52,6 +142,20 @@
52
142
  flex: 1;
53
143
  }
54
144
 
145
+ .directive-pyide .editor.running {
146
+ pointer-events: none;
147
+ opacity: 0.7;
148
+ }
149
+
150
+ .directive-pyide.locked-by-other {
151
+ outline: 2px solid #f59e0b;
152
+ outline-offset: 2px;
153
+ }
154
+
155
+ .directive-pyide.locked-by-other .buttons {
156
+ border-color: #f59e0b;
157
+ }
158
+
55
159
  .directive-pyide .buttons {
56
160
  display: flex;
57
161
  border: 1px solid var(--color-spacer);
@@ -59,6 +163,7 @@
59
163
  border-bottom: none;
60
164
  border-bottom-left-radius: 0;
61
165
  border-bottom-right-radius: 0;
166
+ overflow: hidden;
62
167
  }
63
168
 
64
169
  .directive-pyide .buttons.bottom {
@@ -90,10 +195,26 @@
90
195
  cursor: pointer;
91
196
  }
92
197
 
93
- .directive-pyide .buttons:last-child {
198
+ .directive-pyide .buttons button:last-child {
94
199
  border-right: none;
95
200
  }
96
201
 
202
+ .directive-pyide .buttons:not(.bottom) button:first-child {
203
+ border-top-left-radius: 8px;
204
+ }
205
+
206
+ .directive-pyide .buttons:not(.bottom) button:last-child {
207
+ border-top-right-radius: 8px;
208
+ }
209
+
210
+ .directive-pyide .buttons.bottom button:first-child {
211
+ border-bottom-left-radius: 8px;
212
+ }
213
+
214
+ .directive-pyide .buttons.bottom button:last-child {
215
+ border-bottom-right-radius: 8px;
216
+ }
217
+
97
218
  .directive-pyide button.active {
98
219
  background-color: var(--color-spacer);
99
220
  }
@@ -108,24 +229,80 @@
108
229
  opacity: 0.5;
109
230
  }
110
231
 
232
+ .directive-pyide button.stopping {
233
+ pointer-events: auto;
234
+ cursor: progress;
235
+ opacity: 0.75;
236
+ }
237
+
238
+ .directive-pyide button.locked {
239
+ color: #b45309;
240
+ font-weight: 600;
241
+ }
242
+
243
+ .directive-pyide button.fullscreen {
244
+ flex: 0 0 auto;
245
+ min-width: 42px;
246
+ width: 42px;
247
+ padding: 8px 0;
248
+ }
249
+
250
+ .directive-pyide button.stop {
251
+ flex: 0 0 auto;
252
+ min-width: 96px;
253
+ color: #b42318;
254
+ }
255
+
256
+ .directive-pyide button.stop:disabled {
257
+ color: var(--color-text);
258
+ opacity: 0.5;
259
+ cursor: not-allowed;
260
+ }
261
+
262
+ .directive-pyide:fullscreen {
263
+ width: 100vw;
264
+ height: 100dvh !important;
265
+ padding: 12px;
266
+ box-sizing: border-box;
267
+ background-color: var(--color-background, var(--color--background, #fff));
268
+ }
269
+
270
+ .directive-pyide:fullscreen::backdrop {
271
+ background-color: var(--color-background, var(--color--background, #fff));
272
+ }
273
+
111
274
  @media screen and (min-width: 1024px) {
112
275
  .directive-pyide {
113
276
  flex-direction: row;
114
- height: calc(100dvh - 128px);
115
277
 
116
278
  .output {
117
279
  height: 100%;
118
280
  }
119
281
 
120
282
  .container {
121
- flex: 1;
122
283
  height: 100% !important;
123
284
  }
124
285
 
125
286
  .editor-container {
126
- flex: 3;
127
287
  height: 100%;
128
288
  overflow: hidden;
129
289
  }
130
290
  }
291
+
292
+ .directive-pyide[data-canvas="true"].canvas-split-mode .canvas-wrapper,
293
+ .directive-pyide[data-canvas="true"].canvas-split-mode .output {
294
+ flex: 1 1 0;
295
+ min-height: 120px;
296
+ border-radius: 8px;
297
+ }
298
+
299
+ .directive-pyide[data-canvas="true"].canvas-split-mode .canvas-wrapper,
300
+ .directive-pyide[data-canvas="true"].canvas-split-mode .output {
301
+ border-top-left-radius: 0;
302
+ border-top-right-radius: 0;
303
+ }
304
+
305
+ .directive-pyide[data-canvas="true"].canvas-split-mode .canvas-output-splitter {
306
+ display: block;
307
+ }
131
308
  }
@@ -3,6 +3,9 @@
3
3
  border-radius: 8px;
4
4
  background: #1e1e1e;
5
5
  overflow: hidden;
6
+ display: flex;
7
+ flex-direction: column;
8
+ height: var(--sqlide-height, calc(100dvh - 80px));
6
9
 
7
10
  .jo_iconButton.img_whole-window-dark {
8
11
  display: none;
@@ -19,6 +22,11 @@
19
22
  }
20
23
  }
21
24
 
25
+ .directive-sqlide .sql-online {
26
+ flex: 1;
27
+ min-height: 0;
28
+ }
29
+
22
30
  .directive-sqlide .menu {
23
31
  display: flex;
24
32
  border-top: 1px solid var(--color-spacer);
@@ -1148,6 +1148,92 @@ hyperbook.typst = (function () {
1148
1148
  // TYPST EDITOR
1149
1149
  // ============================================================================
1150
1150
 
1151
+ function setupSplitter(elem, container, editorContainer, splitter) {
1152
+ if (!container || !editorContainer || !splitter) return;
1153
+
1154
+ const minPanelSize = 120;
1155
+
1156
+ const getIsHorizontal = () =>
1157
+ getComputedStyle(elem).flexDirection.startsWith('row');
1158
+
1159
+ const applySplitSize = (rawSize, isHorizontal) => {
1160
+ const total = isHorizontal ? elem.clientWidth : elem.clientHeight;
1161
+ const splitterSize = isHorizontal ? splitter.offsetWidth : splitter.offsetHeight;
1162
+ const maxSize = Math.max(minPanelSize, total - splitterSize - minPanelSize);
1163
+ const clamped = Math.max(minPanelSize, Math.min(rawSize, maxSize));
1164
+ container.style.flex = `0 0 ${clamped}px`;
1165
+ return clamped;
1166
+ };
1167
+
1168
+ const applyStoredSplitSize = () => {
1169
+ const isHorizontal = getIsHorizontal();
1170
+ elem.classList.toggle('split-horizontal', isHorizontal);
1171
+ elem.classList.toggle('split-vertical', !isHorizontal);
1172
+ const key = isHorizontal ? 'splitHorizontal' : 'splitVertical';
1173
+ const rawStored = Number(elem.dataset[key]);
1174
+ if (!Number.isFinite(rawStored) || rawStored <= 0) {
1175
+ container.style.flex = '';
1176
+ return;
1177
+ }
1178
+ applySplitSize(rawStored, isHorizontal);
1179
+ };
1180
+
1181
+ applyStoredSplitSize();
1182
+
1183
+ splitter.addEventListener('pointerdown', (event) => {
1184
+ event.preventDefault();
1185
+ splitter.setPointerCapture(event.pointerId);
1186
+
1187
+ const isHorizontal = getIsHorizontal();
1188
+ const key = isHorizontal ? 'splitHorizontal' : 'splitVertical';
1189
+ const startPointer = isHorizontal ? event.clientX : event.clientY;
1190
+ const startSize = isHorizontal
1191
+ ? container.getBoundingClientRect().width
1192
+ : container.getBoundingClientRect().height;
1193
+
1194
+ elem.classList.add('resizing');
1195
+
1196
+ const onPointerMove = (moveEvent) => {
1197
+ const pointer = isHorizontal ? moveEvent.clientX : moveEvent.clientY;
1198
+ const delta = pointer - startPointer;
1199
+ const size = applySplitSize(startSize + delta, isHorizontal);
1200
+ elem.dataset[key] = String(Math.round(size));
1201
+ };
1202
+
1203
+ const onPointerUp = () => {
1204
+ elem.classList.remove('resizing');
1205
+ splitter.removeEventListener('pointermove', onPointerMove);
1206
+ splitter.removeEventListener('pointerup', onPointerUp);
1207
+ splitter.removeEventListener('pointercancel', onPointerUp);
1208
+ };
1209
+
1210
+ splitter.addEventListener('pointermove', onPointerMove);
1211
+ splitter.addEventListener('pointerup', onPointerUp);
1212
+ splitter.addEventListener('pointercancel', onPointerUp);
1213
+ });
1214
+
1215
+ window.addEventListener('resize', applyStoredSplitSize);
1216
+ }
1217
+
1218
+ const updateFullscreenButtonState = (elem, button) => {
1219
+ if (!elem || !button) return;
1220
+ const isFullscreen = document.fullscreenElement === elem;
1221
+ const label = i18nGet('ide-fullscreen-enter', 'Fullscreen');
1222
+ button.textContent = '⛶';
1223
+ button.title = label;
1224
+ button.setAttribute('aria-label', label);
1225
+ button.classList.toggle('active', isFullscreen);
1226
+ };
1227
+
1228
+ const toggleFullscreen = async (elem) => {
1229
+ if (!elem) return;
1230
+ if (document.fullscreenElement === elem) {
1231
+ await document.exitFullscreen();
1232
+ return;
1233
+ }
1234
+ await elem.requestFullscreen();
1235
+ };
1236
+
1151
1237
  class TypstEditor {
1152
1238
  constructor({
1153
1239
  elem,
@@ -1178,13 +1264,24 @@ hyperbook.typst = (function () {
1178
1264
  this.preview = elem.querySelector('.typst-preview');
1179
1265
  this.loadingIndicator = elem.querySelector('.typst-loading');
1180
1266
  this.editor = elem.querySelector('.editor.typst');
1267
+ this.editorContainer = elem.querySelector('.editor-container');
1268
+ this.splitter = elem.querySelector('.splitter');
1181
1269
  this.sourceTextarea = elem.querySelector('.typst-source');
1270
+ this.fullscreenBtn = elem.querySelector('.fullscreen');
1271
+ this.editorInitialized = false;
1272
+
1273
+ setupSplitter(this.elem, this.previewContainer, this.editorContainer, this.splitter);
1182
1274
 
1183
1275
  // Setup UI callbacks
1184
1276
  this.setupUICallbacks();
1185
1277
 
1186
1278
  // Setup event handlers
1187
1279
  this.setupEventHandlers();
1280
+ this.handleFullscreenChange = () => {
1281
+ updateFullscreenButtonState(this.elem, this.fullscreenBtn);
1282
+ };
1283
+ document.addEventListener('fullscreenchange', this.handleFullscreenChange);
1284
+ updateFullscreenButtonState(this.elem, this.fullscreenBtn);
1188
1285
 
1189
1286
  // Initialize
1190
1287
  this.initialize();
@@ -1208,12 +1305,14 @@ hyperbook.typst = (function () {
1208
1305
  const resetBtn = this.elem.querySelector('.reset');
1209
1306
  const addSourceFileBtn = this.elem.querySelector('.add-source-file');
1210
1307
  const addBinaryFileBtn = this.elem.querySelector('.add-binary-file');
1308
+ const fullscreenBtn = this.elem.querySelector('.fullscreen');
1211
1309
 
1212
1310
  downloadBtn?.addEventListener('click', () => this.handleExportPdf());
1213
1311
  downloadProjectBtn?.addEventListener('click', () => this.handleExportProject());
1214
1312
  resetBtn?.addEventListener('click', () => this.handleReset());
1215
1313
  addSourceFileBtn?.addEventListener('click', () => this.handleAddSourceFile());
1216
1314
  addBinaryFileBtn?.addEventListener('click', (e) => this.handleAddBinaryFile(e));
1315
+ fullscreenBtn?.addEventListener('click', () => this.handleFullscreenToggle());
1217
1316
  }
1218
1317
 
1219
1318
  /**
@@ -1221,8 +1320,9 @@ hyperbook.typst = (function () {
1221
1320
  */
1222
1321
  async initialize() {
1223
1322
  if (this.editor) {
1224
- // Edit mode - wait for code-input to load
1225
- this.editor.addEventListener('code-input_load', async () => {
1323
+ const initializeEditor = async () => {
1324
+ if (this.editorInitialized) return;
1325
+ this.editorInitialized = true;
1226
1326
  await this.restoreState();
1227
1327
  this.uiManager.updateTabs();
1228
1328
  this.uiManager.updateBinaryFilesList();
@@ -1235,7 +1335,13 @@ hyperbook.typst = (function () {
1235
1335
  this.saveState();
1236
1336
  debouncedRerender();
1237
1337
  });
1238
- });
1338
+ };
1339
+
1340
+ // Edit mode - wait for code-input to load (or initialize immediately if already ready)
1341
+ this.editor.addEventListener('code-input_load', initializeEditor);
1342
+ if (this.editor.querySelector('textarea')) {
1343
+ await initializeEditor();
1344
+ }
1239
1345
  } else if (this.sourceTextarea) {
1240
1346
  // Preview mode
1241
1347
  const initialCode = this.sourceTextarea.value;
@@ -1261,7 +1367,7 @@ hyperbook.typst = (function () {
1261
1367
  const result = await window.store?.typst?.get(this.id);
1262
1368
 
1263
1369
  if (result) {
1264
- this.editor.value = result.code;
1370
+ this.setEditorValue(result.code || this.fileManager.getCurrentContent());
1265
1371
 
1266
1372
  if (result.sourceFiles) {
1267
1373
  this.fileManager.sourceFiles = result.sourceFiles;
@@ -1280,11 +1386,11 @@ hyperbook.typst = (function () {
1280
1386
  );
1281
1387
  if (file) {
1282
1388
  this.fileManager.currentFile = file;
1283
- this.editor.value = this.fileManager.getCurrentContent();
1389
+ this.setEditorValue(this.fileManager.getCurrentContent());
1284
1390
  }
1285
1391
  }
1286
1392
  } else {
1287
- this.editor.value = this.fileManager.getCurrentContent();
1393
+ this.setEditorValue(this.fileManager.getCurrentContent());
1288
1394
  }
1289
1395
  }
1290
1396
 
@@ -1294,11 +1400,11 @@ hyperbook.typst = (function () {
1294
1400
  async saveState() {
1295
1401
  if (!this.editor) return;
1296
1402
 
1297
- this.fileManager.updateCurrentContent(this.editor.value);
1403
+ this.fileManager.updateCurrentContent(this.getEditorValue());
1298
1404
 
1299
1405
  await window.store?.typst?.put({
1300
1406
  id: this.id,
1301
- code: this.editor.value,
1407
+ code: this.getEditorValue(),
1302
1408
  sourceFiles: this.fileManager.getSourceFiles(),
1303
1409
  binaryFiles: this.binaryFiles,
1304
1410
  currentFile: this.fileManager.currentFile.filename,
@@ -1311,7 +1417,7 @@ hyperbook.typst = (function () {
1311
1417
  rerender() {
1312
1418
  if (!this.editor) return;
1313
1419
 
1314
- this.fileManager.updateCurrentContent(this.editor.value);
1420
+ this.fileManager.updateCurrentContent(this.getEditorValue());
1315
1421
 
1316
1422
  const mainFile = this.fileManager.findMainFile();
1317
1423
  const mainCode = mainFile
@@ -1338,9 +1444,9 @@ hyperbook.typst = (function () {
1338
1444
  */
1339
1445
  handleFileSwitch(filename) {
1340
1446
  if (this.editor) {
1341
- this.fileManager.updateCurrentContent(this.editor.value);
1447
+ this.fileManager.updateCurrentContent(this.getEditorValue());
1342
1448
  const content = this.fileManager.switchTo(filename);
1343
- this.editor.value = content;
1449
+ this.setEditorValue(content);
1344
1450
  this.uiManager.updateTabs();
1345
1451
  this.saveState();
1346
1452
  }
@@ -1351,7 +1457,7 @@ hyperbook.typst = (function () {
1351
1457
  */
1352
1458
  handleFilesChange() {
1353
1459
  if (this.editor) {
1354
- this.editor.value = this.fileManager.getCurrentContent();
1460
+ this.setEditorValue(this.fileManager.getCurrentContent());
1355
1461
  }
1356
1462
  this.saveState();
1357
1463
  this.rerender();
@@ -1389,7 +1495,7 @@ hyperbook.typst = (function () {
1389
1495
  }
1390
1496
 
1391
1497
  if (this.editor) {
1392
- this.editor.value = this.fileManager.getCurrentContent();
1498
+ this.setEditorValue(this.fileManager.getCurrentContent());
1393
1499
  }
1394
1500
 
1395
1501
  this.uiManager.updateTabs();
@@ -1463,7 +1569,7 @@ hyperbook.typst = (function () {
1463
1569
  */
1464
1570
  async handleExportProject() {
1465
1571
  const mainFile = this.fileManager.findMainFile();
1466
- const code = mainFile ? mainFile.content : (this.editor ? this.editor.value : '');
1572
+ const code = mainFile ? mainFile.content : this.getEditorValue();
1467
1573
 
1468
1574
  await this.exporter.export({
1469
1575
  code,
@@ -1489,6 +1595,40 @@ hyperbook.typst = (function () {
1489
1595
  window.location.reload();
1490
1596
  }
1491
1597
  }
1598
+
1599
+ async handleFullscreenToggle() {
1600
+ try {
1601
+ await toggleFullscreen(this.elem);
1602
+ } catch (error) {
1603
+ console.error(error.message);
1604
+ }
1605
+ }
1606
+
1607
+ getEditorValue() {
1608
+ if (!this.editor) return '';
1609
+ const textarea = this.editor.querySelector('textarea');
1610
+ if (textarea) return textarea.value;
1611
+ try {
1612
+ if (typeof this.editor.value === 'string') {
1613
+ return this.editor.value;
1614
+ }
1615
+ } catch (e) {}
1616
+ return this.editor.textContent || '';
1617
+ }
1618
+
1619
+ setEditorValue(value) {
1620
+ if (!this.editor) return;
1621
+ const normalizedValue = value ?? '';
1622
+ const textarea = this.editor.querySelector('textarea');
1623
+ if (textarea) {
1624
+ textarea.value = normalizedValue;
1625
+ }
1626
+ try {
1627
+ this.editor.value = normalizedValue;
1628
+ } catch (e) {
1629
+ this.editor.textContent = normalizedValue;
1630
+ }
1631
+ }
1492
1632
  }
1493
1633
 
1494
1634
  // ============================================================================
@@ -5,7 +5,7 @@
5
5
  flex-direction: column;
6
6
  overflow: hidden;
7
7
  gap: 8px;
8
- height: calc(100dvh - 128px);
8
+ height: var(--typst-height, calc(100dvh - 80px));
9
9
  }
10
10
 
11
11
  code-input {
@@ -14,11 +14,14 @@ code-input {
14
14
 
15
15
  .directive-typst .preview-container {
16
16
  width: 100%;
17
+ min-height: 120px;
18
+ min-width: 120px;
17
19
  border: 1px solid var(--color-spacer);
18
20
  border-radius: 8px;
19
21
  overflow: auto;
20
22
  background-color: var(--color--background);
21
23
  position: relative;
24
+ flex: 1 1 0;
22
25
  }
23
26
 
24
27
  .directive-typst .typst-preview {
@@ -167,7 +170,42 @@ code-input {
167
170
  width: 100%;
168
171
  display: flex;
169
172
  flex-direction: column;
170
- height: 400px;
173
+ min-height: 120px;
174
+ min-width: 120px;
175
+ flex: 1 1 0;
176
+ }
177
+
178
+ .directive-typst .splitter {
179
+ background: var(--color-spacer);
180
+ border-radius: 999px;
181
+ flex-shrink: 0;
182
+ touch-action: none;
183
+ opacity: 0.45;
184
+ transition: opacity 0.15s ease-in-out;
185
+ }
186
+
187
+ .directive-typst .splitter:hover {
188
+ opacity: 0.65;
189
+ }
190
+
191
+ .directive-typst.split-vertical .splitter {
192
+ width: 100%;
193
+ height: 4px;
194
+ cursor: row-resize;
195
+ }
196
+
197
+ .directive-typst.split-horizontal .splitter {
198
+ width: 4px;
199
+ height: 100%;
200
+ cursor: col-resize;
201
+ }
202
+
203
+ .directive-typst.resizing {
204
+ user-select: none;
205
+ }
206
+
207
+ .directive-typst.resizing .splitter {
208
+ opacity: 0.75;
171
209
  }
172
210
 
173
211
  .directive-typst .file-tabs {
@@ -398,6 +436,7 @@ code-input {
398
436
  border-bottom: none;
399
437
  border-bottom-left-radius: 0;
400
438
  border-bottom-right-radius: 0;
439
+ overflow: hidden;
401
440
  }
402
441
 
403
442
  .directive-typst .buttons.bottom {
@@ -428,10 +467,17 @@ code-input {
428
467
  opacity: 0.6;
429
468
  }
430
469
 
431
- .directive-typst .buttons:last-child {
470
+ .directive-typst .buttons button:last-child {
432
471
  border-right: none;
433
472
  }
434
473
 
474
+ .directive-typst button.fullscreen {
475
+ flex: 0 0 auto;
476
+ min-width: 42px;
477
+ width: 42px;
478
+ padding: 8px 0;
479
+ }
480
+
435
481
  .directive-typst button:hover {
436
482
  background-color: var(--color-spacer);
437
483
  }
@@ -447,10 +493,25 @@ code-input {
447
493
  display: none !important;
448
494
  }
449
495
 
496
+ .directive-typst:fullscreen {
497
+ width: 100vw;
498
+ height: 100dvh !important;
499
+ padding: 12px;
500
+ box-sizing: border-box;
501
+ background-color: var(--color-background, var(--color--background, #fff));
502
+ }
503
+
504
+ .directive-typst:fullscreen::backdrop {
505
+ background-color: var(--color-background, var(--color--background, #fff));
506
+ }
507
+
508
+ .directive-typst:fullscreen.preview-only {
509
+ height: 100dvh !important;
510
+ }
511
+
450
512
  @media screen and (min-width: 1024px) {
451
513
  .directive-typst:not(.preview-only) {
452
514
  flex-direction: row;
453
- height: calc(100dvh - 128px);
454
515
 
455
516
  .preview-container {
456
517
  flex: 1;