pinokiod 3.132.0 → 3.134.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.
@@ -47,7 +47,6 @@ class Backend {
47
47
  let authPath = this.kernel.path(`connect/${this.name}/auth.json`)
48
48
  this.auth = (await this.kernel.loader.load(authPath)).resolved
49
49
  if (!this.auth) {
50
- console.log("not authenticated")
51
50
  return null
52
51
  }
53
52
  if (!this.auth.refresh_token) {
@@ -38,7 +38,6 @@ class Huggingface {
38
38
  let authPath = this.kernel.path('connect/huggingface.json')
39
39
  this.auth = (await this.kernel.loader.load(authPath)).resolved
40
40
  if (!this.auth) {
41
- console.log("not authenticated")
42
41
  return null
43
42
  }
44
43
  if (!this.auth.refresh_token) {
package/kernel/index.js CHANGED
@@ -488,7 +488,7 @@ class Kernel {
488
488
  let changed
489
489
  let new_config = JSON.stringify(await this.peer.current_host())
490
490
  if (this.old_config !== new_config) {
491
- console.log("Proc config has changed. update router.")
491
+ // console.log("Proc config has changed. update router.")
492
492
  changed = true
493
493
  } else {
494
494
  // console.log("Proc config is the same")
@@ -1,3 +1,4 @@
1
+ const fs = require('fs')
1
2
  const path = require('path')
2
3
  const Common = require('./common')
3
4
  const Rewriter = require('./rewriter')
@@ -23,6 +24,19 @@ class LocalhostStaticRouter extends Processor {
23
24
  }
24
25
  }
25
26
  for(let { api_name, config } of configs) {
27
+ const apiRoot = this.router.kernel.path('api', api_name)
28
+ const indexPath = path.join(apiRoot, 'index.html')
29
+ const hasMenu = Boolean(config.menu)
30
+ if (hasMenu) {
31
+ delete this.router.rewrite_mapping[api_name]
32
+ continue
33
+ }
34
+ const hasIndex = fs.existsSync(indexPath)
35
+ const fileServerOptions = {}
36
+ if (hasIndex) {
37
+ fileServerOptions.index_names = ["index.html"]
38
+ }
39
+ const effectiveFileServerOptions = Object.keys(fileServerOptions).length ? { ...fileServerOptions } : undefined
26
40
  for(let domain in config.dns) {
27
41
  let localhost_match
28
42
  let peer_match
@@ -45,11 +59,13 @@ class LocalhostStaticRouter extends Processor {
45
59
  route: rewrite,
46
60
  match: [localhost_match],
47
61
  dial: local_dial,
62
+ fileServerOptions: effectiveFileServerOptions,
48
63
  })
49
64
  this.rewriter.handle({
50
65
  route: rewrite,
51
66
  match: [peer_match],
52
67
  dial: peer_dial,
68
+ fileServerOptions: effectiveFileServerOptions,
53
69
  })
54
70
 
55
71
  // this.router.add_rewrite({ route: new_path, match, peer, dial })
@@ -64,7 +80,7 @@ class LocalhostStaticRouter extends Processor {
64
80
 
65
81
 
66
82
 
67
- this.router.rewrite_mapping[api_name] = {
83
+ const rewriteEntry = {
68
84
  name: api_name,
69
85
  internal_router: [
70
86
  `${local_dial}${rewrite}`,
@@ -75,6 +91,10 @@ class LocalhostStaticRouter extends Processor {
75
91
  peer_match
76
92
  ]
77
93
  }
94
+ if (effectiveFileServerOptions) {
95
+ rewriteEntry.file_server_options = effectiveFileServerOptions
96
+ }
97
+ this.router.rewrite_mapping[api_name] = rewriteEntry
78
98
  // this.connector.handle({
79
99
  // match: peer_match,
80
100
  // connector: {
@@ -33,10 +33,14 @@ class PeerStaticRouter extends Processor {
33
33
  let url = new URL("http://" + rewrite_mapping.external_ip)
34
34
  let dial = url.host
35
35
  let rewrite = url.pathname
36
+ const fileServerOptions = rewrite_mapping.file_server_options
37
+ ? { ...rewrite_mapping.file_server_options }
38
+ : undefined
36
39
  this.rewriter.handle({
37
40
  route: url.pathname,
38
41
  match: rewrite_mapping.external_router,
39
42
  dial: url.host,
43
+ fileServerOptions,
40
44
  })
41
45
  }
42
46
  }
@@ -4,7 +4,7 @@ class Rewriter extends Processor {
4
4
  super()
5
5
  this.router = router
6
6
  }
7
- handle({ match, dial, route }) {
7
+ handle({ match, dial, route, fileServerOptions }) {
8
8
  //let rewrite = `${route}/{path}`
9
9
  //let rewrite = `${route}{http.request.uri}`
10
10
  /*
@@ -35,24 +35,32 @@ class Rewriter extends Processor {
35
35
 
36
36
  // stript the leading /asset/ => (/asset/api/test => /api/test)
37
37
  const asset_path = this.router.kernel.path(route.replace(/\/asset\//, ''))
38
- let handler = [{
38
+
39
+ const fileServerHandler = {
39
40
  "handler": "file_server",
40
41
  "root": asset_path,
41
- "browse": { },
42
- "index_names": ["index.html"]
43
- }]
42
+ ...fileServerOptions
43
+ }
44
+
45
+ const handlers = [fileServerHandler]
44
46
 
45
47
  // if the dial port has been overridden by router.custom_routers, use that instead
46
48
  let parsed_dial = this.parse_ip(dial)
47
49
  let override_handler = this.router.custom_routers[String(parsed_dial.port)]
48
50
  if (override_handler) {
49
- handler = override_handler
51
+ this.router.config.apps.http.servers.main.routes.push({
52
+ "match": [{
53
+ "host": match ,
54
+ }],
55
+ "handle": override_handler
56
+ })
57
+ return
50
58
  }
51
59
  this.router.config.apps.http.servers.main.routes.push({
52
60
  "match": [{
53
61
  "host": match ,
54
62
  }],
55
- "handle": handler
63
+ "handle": handlers
56
64
  })
57
65
  }
58
66
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "3.132.0",
3
+ "version": "3.134.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/server/index.js CHANGED
@@ -460,9 +460,16 @@ class Server {
460
460
  }
461
461
  async renderIndex(name, cfg) {
462
462
  let p = this.kernel.path("api", name)
463
- let html_path = path.resolve(p, "index.html")
464
- let html_exists = await this.kernel.exists(html_path)
465
- if (html_exists) {
463
+ let index_path = path.resolve(p, "index.html")
464
+ let index_exists = await this.kernel.exists(index_path)
465
+ let c = cfg
466
+ let menu = []
467
+ if (cfg.menu) {
468
+ ({ menu, ...c } = cfg)
469
+ }
470
+ console.log("Menu", menu)
471
+ console.log("c", c)
472
+ if (index_exists) {
466
473
  return Object.assign({
467
474
  title: name,
468
475
  menu: [{
@@ -470,8 +477,8 @@ class Server {
470
477
  icon: "fa-solid fa-link",
471
478
  text: "index.html",
472
479
  href: `/asset/api/${name}/index.html`,
473
- }]
474
- }, cfg)
480
+ }].concat(menu)
481
+ }, c)
475
482
  } else {
476
483
  return Object.assign({
477
484
  title: name,
@@ -480,8 +487,8 @@ class Server {
480
487
  icon: "fa-solid fa-link",
481
488
  text: "Project Files",
482
489
  href: `/files/api/${name}`,
483
- }]
484
- }, cfg)
490
+ }].concat(menu)
491
+ }, c)
485
492
  }
486
493
  }
487
494
  async getGit(ref, filepath) {
@@ -688,7 +695,7 @@ class Server {
688
695
  if (config && config.version) {
689
696
  let coerced = semver.coerce(config.version)
690
697
  if (semver.satisfies(coerced, this.kernel.schema)) {
691
- console.log("semver satisfied", config.version, this.kernel.schema)
698
+ // console.log("semver satisfied", config.version, this.kernel.schema)
692
699
  } else {
693
700
  console.log("semver NOT satisfied", config.version, this.kernel.schema)
694
701
  err = `Please update to the latest Pinokio (current script version: ${config.version}, supported: ${this.kernel.schema})`
@@ -3712,7 +3719,11 @@ class Server {
3712
3719
  this.app.use(express.json());
3713
3720
  this.app.use(express.urlencoded({ extended: true }));
3714
3721
  this.app.use(cookieParser());
3715
- this.app.use(session({secret: "secret" }))
3722
+ this.app.use(session({
3723
+ secret: "secret",
3724
+ resave: false,
3725
+ saveUninitialized: false
3726
+ }))
3716
3727
  this.app.use((req, res, next) => {
3717
3728
  const originalRedirect = res.redirect;
3718
3729
  res.redirect = function (url) {
@@ -1,12 +1,309 @@
1
1
  document.addEventListener("DOMContentLoaded", () => {
2
- if (document.querySelector("#new-window")) {
3
- document.querySelector("#new-window").addEventListener("click", (e) => {
4
- let agent = document.body.getAttribute("data-agent")
2
+ const newWindowButton = document.querySelector("#new-window");
3
+ if (newWindowButton) {
4
+ newWindowButton.addEventListener("click", (event) => {
5
+ const agent = document.body.getAttribute("data-agent");
5
6
  if (agent === "electron") {
6
- window.open("/", "_blank", "pinokio")
7
+ window.open("/", "_blank", "pinokio");
7
8
  } else {
8
- window.open("/", "_blank")
9
+ window.open("/", "_blank");
9
10
  }
10
- })
11
+ });
11
12
  }
12
- })
13
+
14
+ const header = document.querySelector("header.navheader");
15
+ const minimizeButton = document.querySelector("#minimize-header");
16
+ const homeLink = header ? header.querySelector(".home") : null;
17
+ if (!header || !minimizeButton || !homeLink) {
18
+ return;
19
+ }
20
+
21
+ const dispatchHeaderState = (minimized, detail = {}) => {
22
+ if (typeof window === "undefined" || typeof window.CustomEvent !== "function") {
23
+ return;
24
+ }
25
+ const payload = { minimized, ...detail };
26
+ document.dispatchEvent(new CustomEvent("pinokio:header-state", { detail: payload }));
27
+ const aliasEvent = minimized ? "pinokio:header-minimized" : "pinokio:header-restored";
28
+ document.dispatchEvent(new CustomEvent(aliasEvent, { detail: { ...payload } }));
29
+ };
30
+
31
+ const headerTitle = header.querySelector("h1") || header;
32
+ let dragHandle = headerTitle.querySelector(".header-drag-handle");
33
+ if (!dragHandle) {
34
+ dragHandle = document.createElement("div");
35
+ dragHandle.className = "header-drag-handle";
36
+ dragHandle.setAttribute("aria-hidden", "true");
37
+ dragHandle.setAttribute("title", "Drag minimized header");
38
+ headerTitle.insertBefore(dragHandle, homeLink ? homeLink.nextSibling : headerTitle.firstChild);
39
+ }
40
+
41
+ const state = {
42
+ minimized: header.classList.contains("minimized"),
43
+ pointerId: null,
44
+ offsetX: 0,
45
+ offsetY: 0,
46
+ lastLeft: parseFloat(header.style.left) || 0,
47
+ lastTop: parseFloat(header.style.top) || 0,
48
+ hasCustomPosition: false,
49
+ originalPosition: {
50
+ top: header.style.top || "",
51
+ left: header.style.left || "",
52
+ right: header.style.right || "",
53
+ bottom: header.style.bottom || "",
54
+ },
55
+ transitionHandler: null,
56
+ };
57
+
58
+ dispatchHeaderState(state.minimized, { phase: "init" });
59
+
60
+ const MIN_MARGIN = 8;
61
+
62
+ const clampPosition = (left, top) => {
63
+ const rect = header.getBoundingClientRect();
64
+ const maxLeft = Math.max(0, window.innerWidth - rect.width);
65
+ const maxTop = Math.max(0, window.innerHeight - rect.height);
66
+ return {
67
+ left: Math.min(Math.max(0, left), maxLeft),
68
+ top: Math.min(Math.max(0, top), maxTop),
69
+ };
70
+ };
71
+
72
+ const applyPosition = (left, top) => {
73
+ header.style.left = `${left}px`;
74
+ header.style.top = `${top}px`;
75
+ header.style.right = "auto";
76
+ header.style.bottom = "auto";
77
+ };
78
+
79
+ const rememberOriginalPosition = () => {
80
+ state.originalPosition = {
81
+ top: header.style.top || "",
82
+ left: header.style.left || "",
83
+ right: header.style.right || "",
84
+ bottom: header.style.bottom || "",
85
+ };
86
+ };
87
+
88
+ const measureRect = (configureClone) => {
89
+ const clone = header.cloneNode(true);
90
+ clone.querySelectorAll("[id]").forEach((node) => node.removeAttribute("id"));
91
+ Object.assign(clone.style, {
92
+ transition: "none",
93
+ transform: "none",
94
+ position: "fixed",
95
+ visibility: "hidden",
96
+ pointerEvents: "none",
97
+ margin: "0",
98
+ left: "0",
99
+ top: "0",
100
+ right: "auto",
101
+ bottom: "auto",
102
+ width: "auto",
103
+ height: "auto",
104
+ });
105
+ document.body.appendChild(clone);
106
+ if (typeof configureClone === "function") {
107
+ configureClone(clone);
108
+ }
109
+ clone.style.right = "auto";
110
+ clone.style.bottom = "auto";
111
+ const rect = clone.getBoundingClientRect();
112
+ clone.remove();
113
+ return rect;
114
+ };
115
+
116
+ const stopTransition = () => {
117
+ if (state.transitionHandler) {
118
+ header.removeEventListener("transitionend", state.transitionHandler);
119
+ state.transitionHandler = null;
120
+ }
121
+ header.classList.remove("transitioning");
122
+ header.style.transition = "";
123
+ header.style.transform = "";
124
+ header.style.transformOrigin = "";
125
+ header.style.opacity = "";
126
+ header.style.willChange = "";
127
+ };
128
+
129
+ const minimize = () => {
130
+ if (state.minimized || header.classList.contains("transitioning")) {
131
+ return;
132
+ }
133
+
134
+ rememberOriginalPosition();
135
+
136
+ const firstRect = header.getBoundingClientRect();
137
+ const minimizedSize = measureRect((clone) => {
138
+ clone.classList.add("minimized");
139
+ });
140
+
141
+ const defaultLeft = Math.max(MIN_MARGIN, window.innerWidth - minimizedSize.width - MIN_MARGIN);
142
+ const defaultTop = Math.max(MIN_MARGIN, window.innerHeight - minimizedSize.height - MIN_MARGIN);
143
+ const targetLeft = state.hasCustomPosition ? state.lastLeft : defaultLeft;
144
+ const targetTop = state.hasCustomPosition ? state.lastTop : defaultTop;
145
+
146
+ state.lastLeft = targetLeft;
147
+ state.lastTop = targetTop;
148
+
149
+ stopTransition();
150
+
151
+ header.classList.add("minimized");
152
+ applyPosition(targetLeft, targetTop);
153
+
154
+ dispatchHeaderState(true, { phase: "start" });
155
+
156
+ const lastRect = header.getBoundingClientRect();
157
+ const deltaX = firstRect.left - lastRect.left;
158
+ const deltaY = firstRect.top - lastRect.top;
159
+ const scaleX = firstRect.width / lastRect.width;
160
+ const scaleY = firstRect.height / lastRect.height;
161
+
162
+ header.style.transition = "none";
163
+ header.style.transformOrigin = "top left";
164
+ header.style.transform = `translate(${deltaX}px, ${deltaY}px) scale(${scaleX}, ${scaleY})`;
165
+ header.style.willChange = "transform";
166
+
167
+ header.offsetWidth;
168
+
169
+ header.classList.add("transitioning");
170
+ header.style.transition = "transform 520ms cubic-bezier(0.22, 1, 0.36, 1)";
171
+ header.style.transform = "";
172
+
173
+ state.transitionHandler = (event) => {
174
+ if (event.propertyName !== "transform") {
175
+ return;
176
+ }
177
+ header.removeEventListener("transitionend", state.transitionHandler);
178
+ state.transitionHandler = null;
179
+ stopTransition();
180
+ state.minimized = true;
181
+ dispatchHeaderState(true, { phase: "settled" });
182
+ };
183
+
184
+ header.addEventListener("transitionend", state.transitionHandler);
185
+ };
186
+
187
+ const restore = () => {
188
+ if (!state.minimized || header.classList.contains("transitioning")) {
189
+ return;
190
+ }
191
+
192
+ const firstRect = header.getBoundingClientRect();
193
+
194
+ stopTransition();
195
+
196
+ header.classList.add("transitioning");
197
+ header.style.willChange = "transform";
198
+ header.style.transition = "none";
199
+ header.style.transformOrigin = "top left";
200
+
201
+ header.classList.remove("minimized");
202
+ header.style.left = state.originalPosition.left;
203
+ header.style.top = state.originalPosition.top;
204
+ header.style.right = state.originalPosition.right;
205
+ header.style.bottom = state.originalPosition.bottom;
206
+
207
+ dispatchHeaderState(false, { phase: "start" });
208
+
209
+ const lastRect = header.getBoundingClientRect();
210
+ const deltaX = firstRect.left - lastRect.left;
211
+ const deltaY = firstRect.top - lastRect.top;
212
+ const scaleX = firstRect.width / lastRect.width;
213
+ const scaleY = firstRect.height / lastRect.height;
214
+
215
+ header.style.transform = `translate(${deltaX}px, ${deltaY}px) scale(${scaleX}, ${scaleY})`;
216
+
217
+ header.offsetWidth;
218
+
219
+ header.style.transition = "transform 560ms cubic-bezier(0.18, 0.85, 0.4, 1)";
220
+ header.style.transform = "";
221
+
222
+ state.transitionHandler = (event) => {
223
+ if (event.propertyName !== "transform") {
224
+ return;
225
+ }
226
+ header.removeEventListener("transitionend", state.transitionHandler);
227
+ state.transitionHandler = null;
228
+ stopTransition();
229
+ state.minimized = false;
230
+ state.hasCustomPosition = false;
231
+ state.lastLeft = parseFloat(header.style.left) || 0;
232
+ state.lastTop = parseFloat(header.style.top) || 0;
233
+ dispatchHeaderState(false, { phase: "settled" });
234
+ };
235
+
236
+ header.addEventListener("transitionend", state.transitionHandler);
237
+ };
238
+
239
+ minimizeButton.addEventListener("click", (event) => {
240
+ event.preventDefault();
241
+ minimize();
242
+ });
243
+
244
+ homeLink.addEventListener("click", (event) => {
245
+ if (!state.minimized) {
246
+ return;
247
+ }
248
+ event.preventDefault();
249
+ restore();
250
+ });
251
+
252
+ const onPointerDown = (event) => {
253
+ if (!state.minimized || header.classList.contains("transitioning")) {
254
+ return;
255
+ }
256
+ state.pointerId = event.pointerId;
257
+ const rect = header.getBoundingClientRect();
258
+ state.offsetX = event.clientX - rect.left;
259
+ state.offsetY = event.clientY - rect.top;
260
+ if (typeof dragHandle.setPointerCapture === "function") {
261
+ try {
262
+ dragHandle.setPointerCapture(event.pointerId);
263
+ } catch (error) {}
264
+ }
265
+ dragHandle.classList.add("dragging");
266
+ event.preventDefault();
267
+ };
268
+
269
+ const onPointerMove = (event) => {
270
+ if (!state.minimized || state.pointerId !== event.pointerId) {
271
+ return;
272
+ }
273
+ const left = event.clientX - state.offsetX;
274
+ const top = event.clientY - state.offsetY;
275
+ const clamped = clampPosition(left, top);
276
+ state.lastLeft = clamped.left;
277
+ state.lastTop = clamped.top;
278
+ state.hasCustomPosition = true;
279
+ applyPosition(clamped.left, clamped.top);
280
+ };
281
+
282
+ const onPointerEnd = (event) => {
283
+ if (state.pointerId !== event.pointerId) {
284
+ return;
285
+ }
286
+ if (typeof dragHandle.releasePointerCapture === "function") {
287
+ try {
288
+ dragHandle.releasePointerCapture(event.pointerId);
289
+ } catch (error) {}
290
+ }
291
+ dragHandle.classList.remove("dragging");
292
+ state.pointerId = null;
293
+ };
294
+
295
+ dragHandle.addEventListener("pointerdown", onPointerDown);
296
+ dragHandle.addEventListener("pointermove", onPointerMove);
297
+ dragHandle.addEventListener("pointerup", onPointerEnd);
298
+ dragHandle.addEventListener("pointercancel", onPointerEnd);
299
+
300
+ window.addEventListener("resize", () => {
301
+ if (!state.minimized || header.classList.contains("transitioning")) {
302
+ return;
303
+ }
304
+ const { left, top } = clampPosition(state.lastLeft, state.lastTop);
305
+ state.lastLeft = left;
306
+ state.lastTop = top;
307
+ applyPosition(left, top);
308
+ });
309
+ });
@@ -2482,3 +2482,85 @@ body.dark #dropdown-portal .dropdown-content .btn2 {
2482
2482
  border-radius: 10px;
2483
2483
  background: white;
2484
2484
  }
2485
+
2486
+
2487
+ header.navheader.minimized {
2488
+ position: fixed;
2489
+ right: auto;
2490
+ bottom: auto;
2491
+ width: auto;
2492
+ height: auto;
2493
+ max-height: none;
2494
+ padding: 4px 8px;
2495
+ border-radius: 12px;
2496
+ background: var(--light-nav-bg);
2497
+ box-shadow: 0 8px 18px rgba(0, 0, 0, 0.14), 0 2px 6px rgba(0, 0, 0, 0.08);
2498
+ display: inline-flex;
2499
+ align-items: center;
2500
+ overflow: visible;
2501
+ z-index: 1000000;
2502
+ }
2503
+ body.dark header.navheader.minimized {
2504
+ background: var(--dark-nav-bg);
2505
+ box-shadow: 0 12px 26px rgba(0, 0, 0, 0.55), 0 0 1px rgba(255, 255, 255, 0.15);
2506
+ }
2507
+ header.navheader.minimized h1 {
2508
+ display: flex;
2509
+ align-items: center;
2510
+ gap: 8px;
2511
+ margin: 0;
2512
+ padding: 0;
2513
+ height: auto;
2514
+ overflow: visible;
2515
+ }
2516
+ header.navheader.minimized h1 > *:not(.home):not(.header-drag-handle) {
2517
+ display: none !important;
2518
+ }
2519
+ header.navheader .header-drag-handle {
2520
+ display: none;
2521
+ position: relative;
2522
+ cursor: grab;
2523
+ user-select: none;
2524
+ touch-action: none;
2525
+ }
2526
+ header.navheader .header-drag-handle::before {
2527
+ content: "";
2528
+ display: block;
2529
+ width: 6px;
2530
+ height: 18px;
2531
+ border-radius: 3px;
2532
+ opacity: 0.45;
2533
+ background-image: repeating-linear-gradient(to bottom, rgba(0, 0, 0, 0.5) 0 1px, transparent 1px 4px);
2534
+ transition: opacity 0.2s ease;
2535
+ }
2536
+ body.dark header.navheader .header-drag-handle::before {
2537
+ background-image: repeating-linear-gradient(to bottom, rgba(255, 255, 255, 0.55) 0 1px, transparent 1px 4px);
2538
+ opacity: 0.35;
2539
+ }
2540
+ header.navheader.minimized .header-drag-handle {
2541
+ display: block;
2542
+ }
2543
+ header.navheader.minimized:hover .header-drag-handle::before {
2544
+ opacity: 0.7;
2545
+ }
2546
+ header.navheader.minimized .header-drag-handle.dragging {
2547
+ cursor: grabbing;
2548
+ }
2549
+ header.navheader.minimized .header-drag-handle.dragging::before {
2550
+ opacity: 0.85;
2551
+ }
2552
+ header.navheader.minimized .home {
2553
+ display: flex;
2554
+ align-items: center;
2555
+ position: static;
2556
+ padding: 0;
2557
+ }
2558
+ header.navheader.minimized .home .icon {
2559
+ width: 24px;
2560
+ height: 24px;
2561
+ }
2562
+
2563
+
2564
+ header.navheader.transitioning {
2565
+ pointer-events: none;
2566
+ }
@@ -180,6 +180,20 @@ body.dark .appcanvas_filler {
180
180
  --sidebar-tab-outline: var(--pinokio-sidebar-tabbar-border);
181
181
  }
182
182
 
183
+ .appcanvas > aside.appcanvas-aside-animating {
184
+ will-change: height, opacity;
185
+ }
186
+
187
+ .appcanvas > aside.appcanvas-aside-collapsed {
188
+ height: 0 !important;
189
+ opacity: 0;
190
+ pointer-events: none;
191
+ padding-top: 0;
192
+ padding-bottom: 0;
193
+ margin-top: 0;
194
+ margin-bottom: 0;
195
+ }
196
+
183
197
  /*
184
198
  body.dark .appcanvas > aside {
185
199
  background: var(--pinokio-sidebar-tabbar-bg);
@@ -404,6 +418,9 @@ body.dark .appcanvas > aside .header-item.selected {
404
418
  border-bottom: none;
405
419
  z-index: 1;
406
420
  }
421
+ header.navheader.minimized + .appcanvas > aside .menu-container {
422
+ padding: 8px 0 0;
423
+ }
407
424
 
408
425
  main {
409
426
  flex-grow: 1;
@@ -2723,6 +2740,9 @@ body.dark {
2723
2740
  .appcanvas {
2724
2741
  margin-left: 55px;
2725
2742
  }
2743
+ header.navheader.minimized + .appcanvas {
2744
+ margin-left: 0;
2745
+ }
2726
2746
  main, iframe.mainframe {
2727
2747
  padding-left: 0;
2728
2748
  }
@@ -2822,6 +2842,9 @@ body.dark {
2822
2842
  <a class='btn2' href="/rows" data-tippy-content="split into 2 rows">
2823
2843
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
2824
2844
  </a>
2845
+ <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
2846
+ <div><i class="fa-solid fa-expand"></i></div>
2847
+ </button>
2825
2848
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
2826
2849
  <div><i class="fa-solid fa-plus"></i></div>
2827
2850
  </button>
@@ -8545,6 +8568,153 @@ body.dark {
8545
8568
 
8546
8569
  </script>
8547
8570
  <% } %>
8571
+ <script>
8572
+ (() => {
8573
+ const aside = document.querySelector(".appcanvas > aside")
8574
+ if (!aside) {
8575
+ return
8576
+ }
8577
+
8578
+ const collapsedClass = "appcanvas-aside-collapsed"
8579
+ const animatingClass = "appcanvas-aside-animating"
8580
+ const collapseDuration = 420
8581
+ const collapseEasing = "cubic-bezier(0.22, 1, 0.36, 1)"
8582
+ const expandEasing = "cubic-bezier(0.18, 0.85, 0.4, 1)"
8583
+
8584
+ let transitionHandler = null
8585
+ let queuedFrame = null
8586
+
8587
+ const clearAnimation = () => {
8588
+ if (transitionHandler) {
8589
+ aside.removeEventListener("transitionend", transitionHandler)
8590
+ transitionHandler = null
8591
+ }
8592
+ if (queuedFrame !== null) {
8593
+ cancelAnimationFrame(queuedFrame)
8594
+ queuedFrame = null
8595
+ }
8596
+ aside.classList.remove(animatingClass)
8597
+ aside.style.transition = ""
8598
+ aside.style.height = ""
8599
+ aside.style.opacity = ""
8600
+ }
8601
+
8602
+ const finish = (minimized) => {
8603
+ aside.classList.toggle(collapsedClass, minimized)
8604
+ aside.classList.remove(animatingClass)
8605
+ aside.style.transition = ""
8606
+ aside.style.height = ""
8607
+ aside.style.opacity = ""
8608
+ aside.dataset.asideState = minimized ? "collapsed" : "expanded"
8609
+ }
8610
+
8611
+ const collapse = (immediate) => {
8612
+ if (aside.dataset.asideState === "collapsed" && !immediate) {
8613
+ return
8614
+ }
8615
+ clearAnimation()
8616
+ if (immediate) {
8617
+ finish(true)
8618
+ return
8619
+ }
8620
+ const startHeight = aside.getBoundingClientRect().height
8621
+ if (startHeight <= 0.5) {
8622
+ finish(true)
8623
+ return
8624
+ }
8625
+
8626
+ aside.classList.add(animatingClass)
8627
+ aside.style.height = `${startHeight}px`
8628
+ aside.style.opacity = getComputedStyle(aside).opacity || "1"
8629
+ aside.dataset.asideState = "animating"
8630
+
8631
+ queuedFrame = requestAnimationFrame(() => {
8632
+ queuedFrame = null
8633
+ aside.style.transition = `height ${collapseDuration}ms ${collapseEasing}, opacity ${collapseDuration - 120}ms ease`
8634
+ aside.style.height = "0px"
8635
+ aside.style.opacity = "0"
8636
+ })
8637
+
8638
+ transitionHandler = (event) => {
8639
+ if (event.target !== aside || event.propertyName !== "height") {
8640
+ return
8641
+ }
8642
+ aside.removeEventListener("transitionend", transitionHandler)
8643
+ transitionHandler = null
8644
+ finish(true)
8645
+ }
8646
+ aside.addEventListener("transitionend", transitionHandler)
8647
+ }
8648
+
8649
+ const expand = (immediate) => {
8650
+ if (aside.dataset.asideState === "expanded" && !immediate) {
8651
+ return
8652
+ }
8653
+ clearAnimation()
8654
+ if (immediate) {
8655
+ finish(false)
8656
+ return
8657
+ }
8658
+
8659
+ aside.classList.remove(collapsedClass)
8660
+ const targetHeight = aside.scrollHeight
8661
+ aside.classList.add(animatingClass)
8662
+ aside.style.height = "0px"
8663
+ aside.style.opacity = "0"
8664
+ aside.dataset.asideState = "animating"
8665
+
8666
+ aside.offsetHeight
8667
+
8668
+ queuedFrame = requestAnimationFrame(() => {
8669
+ queuedFrame = null
8670
+ aside.style.transition = `height ${collapseDuration}ms ${expandEasing}, opacity ${collapseDuration - 140}ms ease`
8671
+ aside.style.height = `${targetHeight}px`
8672
+ aside.style.opacity = ""
8673
+ })
8674
+
8675
+ transitionHandler = (event) => {
8676
+ if (event.target !== aside || event.propertyName !== "height") {
8677
+ return
8678
+ }
8679
+ aside.removeEventListener("transitionend", transitionHandler)
8680
+ transitionHandler = null
8681
+ finish(false)
8682
+ }
8683
+ aside.addEventListener("transitionend", transitionHandler)
8684
+ }
8685
+
8686
+ const setAside = (minimized, immediate) => {
8687
+ if (minimized) {
8688
+ collapse(immediate)
8689
+ } else {
8690
+ expand(immediate)
8691
+ }
8692
+ }
8693
+
8694
+ const header = document.querySelector("header.navheader")
8695
+ const initialMinimized = !!(header && header.classList.contains("minimized"))
8696
+ setAside(initialMinimized, true)
8697
+
8698
+ document.addEventListener("pinokio:header-state", (event) => {
8699
+ if (!event || !event.detail) {
8700
+ return
8701
+ }
8702
+ const { minimized, phase } = event.detail
8703
+ if (phase === "init") {
8704
+ setAside(!!minimized, true)
8705
+ return
8706
+ }
8707
+ if (phase === "start") {
8708
+ setAside(!!minimized, false)
8709
+ return
8710
+ }
8711
+ if (phase === "settled") {
8712
+ clearAnimation()
8713
+ finish(!!minimized)
8714
+ }
8715
+ })
8716
+ })()
8717
+ </script>
8548
8718
  <script src="/tab-idle-notifier.js"></script>
8549
8719
  </body>
8550
8720
  </html>
@@ -205,6 +205,9 @@ pre {
205
205
  <a class='btn2' href="/rows" data-tippy-content="split into 2 rows">
206
206
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
207
207
  </a>
208
+ <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
209
+ <div><i class="fa-solid fa-expand"></i></div>
210
+ </button>
208
211
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
209
212
  <div><i class="fa-solid fa-plus"></i></div>
210
213
  </button>
@@ -830,6 +830,9 @@ document.addEventListener('DOMContentLoaded', function() {
830
830
  <a class='btn2' href="/rows" data-tippy-content="split into 2 rows">
831
831
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
832
832
  </a>
833
+ <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
834
+ <div><i class="fa-solid fa-expand"></i></div>
835
+ </button>
833
836
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
834
837
  <div><i class="fa-solid fa-plus"></i></div>
835
838
  </button>
@@ -344,6 +344,9 @@ iframe {
344
344
  <a class='btn2' href="/rows" data-tippy-content="split into 2 rows">
345
345
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
346
346
  </a>
347
+ <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
348
+ <div><i class="fa-solid fa-expand"></i></div>
349
+ </button>
347
350
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
348
351
  <div><i class="fa-solid fa-plus"></i></div>
349
352
  </button>
@@ -133,6 +133,9 @@ body.frozen {
133
133
  <a class='btn2' href="/rows" data-tippy-content="split into 2 rows">
134
134
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
135
135
  </a>
136
+ <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
137
+ <div><i class="fa-solid fa-expand"></i></div>
138
+ </button>
136
139
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
137
140
  <div><i class="fa-solid fa-plus"></i></div>
138
141
  </button>
@@ -134,6 +134,9 @@ body main iframe {
134
134
  <a class='btn2' href="/rows" data-tippy-content="split into 2 rows">
135
135
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
136
136
  </a>
137
+ <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
138
+ <div><i class="fa-solid fa-expand"></i></div>
139
+ </button>
137
140
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
138
141
  <div><i class="fa-solid fa-plus"></i></div>
139
142
  </button>
@@ -167,6 +167,9 @@ document.addEventListener("DOMContentLoaded", async () => {
167
167
  <a class='btn2' href="/rows" data-tippy-content="split into 2 rows">
168
168
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
169
169
  </a>
170
+ <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
171
+ <div><i class="fa-solid fa-expand"></i></div>
172
+ </button>
170
173
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
171
174
  <div><i class="fa-solid fa-plus"></i></div>
172
175
  </button>
@@ -63,6 +63,9 @@ main iframe {
63
63
  <a class='btn2' href="/rows" data-tippy-content="split into 2 rows">
64
64
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
65
65
  </a>
66
+ <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
67
+ <div><i class="fa-solid fa-expand"></i></div>
68
+ </button>
66
69
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
67
70
  <div><i class="fa-solid fa-plus"></i></div>
68
71
  </button>
@@ -246,6 +246,9 @@ ol {
246
246
  <a class='btn2' href="/rows" data-tippy-content="split into 2 rows">
247
247
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
248
248
  </a>
249
+ <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
250
+ <div><i class="fa-solid fa-expand"></i></div>
251
+ </button>
249
252
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
250
253
  <div><i class="fa-solid fa-plus"></i></div>
251
254
  </button>
@@ -267,6 +267,9 @@ body.dark .item .tile .badge {
267
267
  <a class='btn2' href="/rows" data-tippy-content="split into 2 rows">
268
268
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
269
269
  </a>
270
+ <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
271
+ <div><i class="fa-solid fa-expand"></i></div>
272
+ </button>
270
273
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
271
274
  <div><i class="fa-solid fa-plus"></i></div>
272
275
  </button>
@@ -435,6 +435,9 @@ body.dark aside .current.selected {
435
435
  <a class='btn2' href="/rows" data-tippy-content="split into 2 rows">
436
436
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
437
437
  </a>
438
+ <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
439
+ <div><i class="fa-solid fa-expand"></i></div>
440
+ </button>
438
441
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
439
442
  <div><i class="fa-solid fa-plus"></i></div>
440
443
  </button>
@@ -272,6 +272,9 @@ body.dark .open-menu, body.dark .browse {
272
272
  <button class='btn2' id='genlog'><div><i class="fa-solid fa-laptop-code"></i></div><div>Logs</div></button>
273
273
  <a id='downloadlogs' download class='hidden btn2' href="/pinokio/logs.zip"><div><i class="fa-solid fa-download"></i></div><div>Download logs</div></a>
274
274
  <a class='btn2' href="/home?mode=settings"><div><i class="fa-solid fa-gear"></i></div><div>Settings</div></a>
275
+ <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
276
+ <div><i class="fa-solid fa-expand"></i></div>
277
+ </button>
275
278
  <button id='new-window' title='open a new window' class='btn2' data-agent="<%=agent%>"><div><i class="fa-solid fa-plus"></i></div><div>Window</div></button>
276
279
  </div>
277
280
  <% } %>
@@ -1532,6 +1532,9 @@ body.dark .ace-editor {
1532
1532
  <a class='btn2' href="/rows" data-tippy-content="split into 2 rows">
1533
1533
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
1534
1534
  </a>
1535
+ <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
1536
+ <div><i class="fa-solid fa-expand"></i></div>
1537
+ </button>
1535
1538
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
1536
1539
  <div><i class="fa-solid fa-plus"></i></div>
1537
1540
  </button>
@@ -767,6 +767,9 @@ body.dark .appcanvas {
767
767
  <a class='btn2' href="/rows" data-tippy-content="split into 2 rows">
768
768
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
769
769
  </a>
770
+ <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
771
+ <div><i class="fa-solid fa-expand"></i></div>
772
+ </button>
770
773
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
771
774
  <div><i class="fa-solid fa-plus"></i></div>
772
775
  </button>
@@ -553,6 +553,9 @@ document.addEventListener('DOMContentLoaded', function() {
553
553
  <a class='btn2' href="/rows" data-tippy-content="split into 2 rows">
554
554
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
555
555
  </a>
556
+ <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
557
+ <div><i class="fa-solid fa-expand"></i></div>
558
+ </button>
556
559
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
557
560
  <div><i class="fa-solid fa-plus"></i></div>
558
561
  </button>
@@ -1066,6 +1066,9 @@ document.addEventListener('DOMContentLoaded', function() {
1066
1066
  <a class='btn2' href="/rows" data-tippy-content="split into 2 rows">
1067
1067
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
1068
1068
  </a>
1069
+ <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
1070
+ <div><i class="fa-solid fa-expand"></i></div>
1071
+ </button>
1069
1072
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
1070
1073
  <div><i class="fa-solid fa-plus"></i></div>
1071
1074
  </button>
@@ -428,6 +428,9 @@ table h3 {
428
428
  <button class='btn2' id='genlog'><div><i class="fa-solid fa-laptop-code"></i></div><div>Logs</div></button>
429
429
  <a id='downloadlogs' download class='hidden btn2' href="/pinokio/logs.zip"><div><i class="fa-solid fa-download"></i></div><div>Download logs</div></a>
430
430
  <a class='btn2' href="/home?mode=settings"><div><i class="fa-solid fa-gear"></i></div><div>Settings</div></a>
431
+ <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
432
+ <div><i class="fa-solid fa-expand"></i></div>
433
+ </button>
431
434
  <button id='new-window' title='open a new window' class='btn2' data-agent="<%=agent%>"><div><i class="fa-solid fa-plus"></i></div><div>Window</div></button>
432
435
  </div>
433
436
  </h1>
@@ -414,6 +414,9 @@ input:checked + .slider:before {
414
414
  <button class='btn2' id='genlog'><div><i class="fa-solid fa-laptop-code"></i></div><div>Logs</div></button>
415
415
  <a id='downloadlogs' download class='hidden btn2' href="/pinokio/logs.zip"><div><i class="fa-solid fa-download"></i></div><div>Download logs</div></a>
416
416
  <a class='btn2' href="/home?mode=settings"><div><i class="fa-solid fa-gear"></i></div><div>Settings</div></a>
417
+ <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
418
+ <div><i class="fa-solid fa-expand"></i></div>
419
+ </button>
417
420
  <button id='new-window' title='open a new window' class='btn2' data-agent="<%=agent%>"><div><i class="fa-solid fa-plus"></i></div><div>Window</div></button>
418
421
  </div>
419
422
  </h1>
@@ -1014,6 +1014,9 @@ body.dark .appcanvas {
1014
1014
  <a class='btn2' href="/rows" data-tippy-content="split into 2 rows">
1015
1015
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
1016
1016
  </a>
1017
+ <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
1018
+ <div><i class="fa-solid fa-expand"></i></div>
1019
+ </button>
1017
1020
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
1018
1021
  <div><i class="fa-solid fa-plus"></i></div>
1019
1022
  </button>
@@ -954,6 +954,9 @@ body.dark .top-menu .btn2.selected {
954
954
  <a class='btn2' href="/rows" data-tippy-content="split into 2 rows">
955
955
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
956
956
  </a>
957
+ <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
958
+ <div><i class="fa-solid fa-expand"></i></div>
959
+ </button>
957
960
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
958
961
  <div><i class="fa-solid fa-plus"></i></div>
959
962
  </button>
@@ -726,6 +726,9 @@ document.addEventListener('DOMContentLoaded', function() {
726
726
  <a class='btn2' href="/rows" data-tippy-content="split into 2 rows">
727
727
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
728
728
  </a>
729
+ <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
730
+ <div><i class="fa-solid fa-expand"></i></div>
731
+ </button>
729
732
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
730
733
  <div><i class="fa-solid fa-plus"></i></div>
731
734
  </button>
@@ -398,6 +398,9 @@ document.addEventListener('DOMContentLoaded', function() {
398
398
  <a class='btn2' href="/rows" data-tippy-content="split into 2 rows">
399
399
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
400
400
  </a>
401
+ <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
402
+ <div><i class="fa-solid fa-expand"></i></div>
403
+ </button>
401
404
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
402
405
  <div><i class="fa-solid fa-plus"></i></div>
403
406
  </button>
@@ -144,6 +144,9 @@ body {
144
144
  <a class='btn2' href="/rows" data-tippy-content="split into 2 rows">
145
145
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
146
146
  </a>
147
+ <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
148
+ <div><i class="fa-solid fa-expand"></i></div>
149
+ </button>
147
150
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
148
151
  <div><i class="fa-solid fa-plus"></i></div>
149
152
  </button>
@@ -160,6 +160,9 @@ body.dark .card {
160
160
  <a class='btn2' href="/rows" data-tippy-content="split into 2 rows">
161
161
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
162
162
  </a>
163
+ <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
164
+ <div><i class="fa-solid fa-expand"></i></div>
165
+ </button>
163
166
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
164
167
  <div><i class="fa-solid fa-plus"></i></div>
165
168
  </button>
@@ -381,6 +381,9 @@ body.dark .plugin-option:hover {
381
381
  <a class='btn2' href="/rows" data-tippy-content="split into 2 rows">
382
382
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
383
383
  </a>
384
+ <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
385
+ <div><i class="fa-solid fa-expand"></i></div>
386
+ </button>
384
387
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
385
388
  <div><i class="fa-solid fa-plus"></i></div>
386
389
  </button>
@@ -1116,6 +1116,9 @@ document.addEventListener('DOMContentLoaded', function() {
1116
1116
  <a class='btn2' href="/rows" data-tippy-content="split into 2 rows">
1117
1117
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
1118
1118
  </a>
1119
+ <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
1120
+ <div><i class="fa-solid fa-expand"></i></div>
1121
+ </button>
1119
1122
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
1120
1123
  <div><i class="fa-solid fa-plus"></i></div>
1121
1124
  </button>