@signal9/era-ui 1.16.0 → 1.17.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.
@@ -2,6 +2,7 @@
2
2
  import * as Pane from '../ui/pane';
3
3
  import { Button } from '../ui/button';
4
4
  import Minus from '@lucide/svelte/icons/minus';
5
+ import Square from '@lucide/svelte/icons/square';
5
6
  import X from '@lucide/svelte/icons/x';
6
7
  import { cn } from '../utils/index.js';
7
8
  import { getWindowManager, type AppWindow } from './wm.svelte.js';
@@ -11,14 +12,14 @@
11
12
  const wm = getWindowManager();
12
13
  const focused = $derived(wm.focusedId === win.id);
13
14
 
14
- // The window's single control closes on left-click and MINIMIZES on right-click
15
- // (a shell has a taskbar to restore from, so no dedicated buttons). While the
16
- // right button is held the glyph previews the minimize action; a left press
17
- // keeps the X, so holding never implies minimize when the release will close.
18
- // Like a native click, the minimize COMMITS on release, not press: right
19
- // pointerdown only arms it (contextmenu is suppressed), right pointerup over
20
- // the button fires it, and dragging off first cancels.
21
- let minimizeHint = $state(false);
15
+ // The window's single control: left-click CLOSES, right-click MINIMIZES (the
16
+ // taskbar restores it), middle-click toggles MAXIMIZE no dedicated buttons.
17
+ // While a non-primary button is held the glyph previews its action (minus /
18
+ // square); a left press keeps the ×, so holding never implies another action
19
+ // when the release will close. Like a native click, each action COMMITS on
20
+ // release, not press: the press only arms it (contextmenu / autoscroll are
21
+ // suppressed), pointerup over the button fires it, dragging off first cancels.
22
+ let armed = $state<'minimize' | 'maximize' | null>(null);
22
23
 
23
24
  // z-index is set imperatively, NOT via a style="" prop: Pane positions itself
24
25
  // by writing element.style.translate, and a whole-attribute style binding would
@@ -49,25 +50,34 @@
49
50
  {/if}
50
51
  <span class={cn('truncate text-body', focused ? 'text-bright' : 'text-muted')}>{win.title}</span
51
52
  >
52
- <!-- Close / minimize. data-pane-control so it clicks instead of dragging the pane. -->
53
+ <!-- Close / minimize / maximize. data-pane-control so it clicks instead of
54
+ dragging the pane. -->
53
55
  <div class="ml-auto shrink-0" data-pane-control>
54
56
  <Button
55
57
  icon
56
58
  size="icon"
57
59
  tone="destructive"
58
- aria-label="Close (right-click to minimize)"
60
+ aria-label="Close (right-click minimizes, middle-click maximizes)"
59
61
  onclick={() => wm.close(win.id)}
60
62
  oncontextmenu={(e: MouseEvent) => e.preventDefault()}
61
- onpointerdown={(e: PointerEvent) => (minimizeHint = e.button === 2)}
63
+ onpointerdown={(e: PointerEvent) => {
64
+ // Cancelling the middle pointerdown suppresses its compatibility
65
+ // mouse events, so the browser's autoscroll never engages.
66
+ if (e.button === 1) e.preventDefault();
67
+ armed = e.button === 2 ? 'minimize' : e.button === 1 ? 'maximize' : null;
68
+ }}
62
69
  onpointerup={(e: PointerEvent) => {
63
- if (e.button === 2 && minimizeHint) wm.minimize(win.id);
64
- minimizeHint = false;
70
+ if (e.button === 2 && armed === 'minimize') wm.minimize(win.id);
71
+ else if (e.button === 1 && armed === 'maximize') wm.toggleMaximize(win.id);
72
+ armed = null;
65
73
  }}
66
- onpointerleave={() => (minimizeHint = false)}
67
- onpointercancel={() => (minimizeHint = false)}
74
+ onpointerleave={() => (armed = null)}
75
+ onpointercancel={() => (armed = null)}
68
76
  >
69
- {#if minimizeHint}
77
+ {#if armed === 'minimize'}
70
78
  <Minus class="size-(--era-h-xs)" />
79
+ {:else if armed === 'maximize'}
80
+ <Square class="size-(--era-h-xs)" />
71
81
  {:else}
72
82
  <X class="size-(--era-h-xs)" />
73
83
  {/if}
@@ -1,6 +1,6 @@
1
1
  import { type Component } from 'svelte';
2
2
  import type { IconProps } from '@lucide/svelte';
3
- export type WindowState = 'normal' | 'minimized';
3
+ export type WindowState = 'normal' | 'minimized' | 'maximized';
4
4
  /** Snap targets: the four edges (halves) or four corners (quadrants). */
5
5
  export type SnapZone = 'left' | 'right' | 'top' | 'bottom' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
6
6
  /** Props every app body receives — its own window record and the manager, so it
@@ -88,7 +88,11 @@ export declare class WindowManager {
88
88
  minimize(id: string): void;
89
89
  /** Taskbar behaviour: focus if not focused, else minimize; restore if minimized. */
90
90
  toggleMinimize(id: string): void;
91
- /** Un-snap back to the pre-tile geometry (or just un-minimize). */
91
+ /** Fill the desktop bounds (middle-click on the window's ×). Keeps the first
92
+ * pre-maximize/pre-snap snapshot so restore returns to the true origin. */
93
+ maximize(id: string): void;
94
+ toggleMaximize(id: string): void;
95
+ /** Un-maximize / un-snap back to the pre-change geometry (or just un-minimize). */
92
96
  restore(id: string): void;
93
97
  /** Half / quadrant tiling against the desktop bounds. */
94
98
  snap(id: string, zone: SnapZone): void;
@@ -95,7 +95,29 @@ export class WindowManager {
95
95
  else
96
96
  this.focus(id);
97
97
  }
98
- /** Un-snap back to the pre-tile geometry (or just un-minimize). */
98
+ /** Fill the desktop bounds (middle-click on the window's ×). Keeps the first
99
+ * pre-maximize/pre-snap snapshot so restore returns to the true origin. */
100
+ maximize(id) {
101
+ const w = this.#find(id);
102
+ if (!w)
103
+ return;
104
+ if (!w.restore)
105
+ w.restore = { pos: { ...w.pos }, size: w.size ? { ...w.size } : null };
106
+ w.pos = { x: 0, y: 0 };
107
+ w.size = { width: this.bounds.width, height: this.bounds.height };
108
+ w.state = 'maximized';
109
+ this.focus(id);
110
+ }
111
+ toggleMaximize(id) {
112
+ const w = this.#find(id);
113
+ if (!w)
114
+ return;
115
+ if (w.state === 'maximized')
116
+ this.restore(id);
117
+ else
118
+ this.maximize(id);
119
+ }
120
+ /** Un-maximize / un-snap back to the pre-change geometry (or just un-minimize). */
99
121
  restore(id) {
100
122
  const w = this.#find(id);
101
123
  if (!w)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signal9/era-ui",
3
- "version": "1.16.0",
3
+ "version": "1.17.0",
4
4
  "scripts": {
5
5
  "dev": "vite dev --host",
6
6
  "build": "vite build && npm run prepack",