minterm 0.1.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.
- package/LICENSE +24 -0
- package/README.md +194 -0
- package/css/minterm.css +400 -0
- package/index.js +27 -0
- package/package.json +40 -0
- package/src/adapters/base-adapter.js +59 -0
- package/src/adapters/manual-adapter.js +14 -0
- package/src/adapters/nats-adapter.js +42 -0
- package/src/adapters/redis-adapter.js +83 -0
- package/src/adapters/rest-poller.js +42 -0
- package/src/adapters/websocket-adapter.js +59 -0
- package/src/arrow-overlay.js +250 -0
- package/src/emitter.js +30 -0
- package/src/formatters.js +41 -0
- package/src/modal-manager.js +70 -0
- package/src/themes.js +149 -0
- package/src/widgets/activity-bars.js +55 -0
- package/src/widgets/base-widget.js +49 -0
- package/src/widgets/message-log.js +81 -0
- package/src/widgets/mini-chart.js +144 -0
- package/src/widgets/range-bar.js +94 -0
- package/src/widgets/ticker.js +53 -0
- package/src/window-manager.js +425 -0
- package/src/z-index.js +35 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
This is free and unencumbered software released into the public domain.
|
|
2
|
+
|
|
3
|
+
Anyone is free to copy, modify, publish, use, compile, sell, or
|
|
4
|
+
distribute this software, either in source code form or as a compiled
|
|
5
|
+
binary, for any purpose, commercial or non-commercial, and by any
|
|
6
|
+
means.
|
|
7
|
+
|
|
8
|
+
In jurisdictions that recognize copyright laws, the author or authors
|
|
9
|
+
of this software dedicate any and all copyright interest in the
|
|
10
|
+
software to the public domain. We make this dedication for the benefit
|
|
11
|
+
of the public at large and to the detriment of our heirs and
|
|
12
|
+
successors. We intend this dedication to be an overt act of
|
|
13
|
+
relinquishment in perpetuity of all present and future rights to this
|
|
14
|
+
software under copyright law.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
19
|
+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
20
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
21
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
|
23
|
+
|
|
24
|
+
For more information, please refer to <https://unlicense.org>
|
package/README.md
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# minterm
|
|
2
|
+
|
|
3
|
+
Zero-dependency DOS-style windowing UI for the browser. Draggable, resizable windows with rAF-batched widgets and pluggable data adapters.
|
|
4
|
+
|
|
5
|
+
`npm install minterm`
|
|
6
|
+
|
|
7
|
+
## Quick start
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<link rel="stylesheet" href="minterm/css/minterm.css">
|
|
11
|
+
```
|
|
12
|
+
```js
|
|
13
|
+
import { WindowManager, MiniChart } from 'minterm';
|
|
14
|
+
const wm = new WindowManager();
|
|
15
|
+
const chart = wm.addWidget('chart', MiniChart,
|
|
16
|
+
{ width: 50, height: 10 },
|
|
17
|
+
{ x: 20, y: 20, width: 500, height: 300, title: 'Sparkline', closable: true }
|
|
18
|
+
);
|
|
19
|
+
chart.push(42.5); // safe to call 1000x/sec — rAF batched
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Architecture
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
DataSource -> Adapter.emit('data', payload) -> Widget.update(payload) -> rAF batched DOM render
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Everything extends `Emitter` (on/off/emit/once/destroy). Data flows one way — sources into adapters, adapters emit events, widgets render. Skip adapters and call `.push()` directly if preferred.
|
|
29
|
+
|
|
30
|
+
## WindowManager
|
|
31
|
+
|
|
32
|
+
Creates draggable, resizable, focusable windows with pinning, layout snapshots, and keyboard shortcuts.
|
|
33
|
+
|
|
34
|
+
**Constructor options:** `container` (HTMLElement, default: body), `classPrefix` (string, default: 'mt'), `lockable` (bool), `layoutBar` (bool), `layoutSlots` (number, default: 5), `escapeClose` (bool, default: true).
|
|
35
|
+
|
|
36
|
+
**Window options:** `x`, `y`, `width`, `height`, `title`, `closable`, `lockable`, `minWidth` (default: 200), `minHeight` (default: 60).
|
|
37
|
+
|
|
38
|
+
### API
|
|
39
|
+
|
|
40
|
+
| Method | Returns | Description |
|
|
41
|
+
|--------|---------|-------------|
|
|
42
|
+
| `createWindow(id, opts)` | HTMLElement | Create a window |
|
|
43
|
+
| `addWidget(id, WidgetClass, widgetOpts, windowOpts)` | Widget | Create window + mount widget |
|
|
44
|
+
| `closeWindow(id)` | void | Remove a window |
|
|
45
|
+
| `closeAll()` | void | Close all unlocked closable windows |
|
|
46
|
+
| `setContent(id, title, html)` | void | Replace title and body HTML |
|
|
47
|
+
| `getBody(id)` | HTMLElement | The `.mt-win-body` div |
|
|
48
|
+
| `getWindow(id)` | HTMLElement | The whole `.mt-win` div |
|
|
49
|
+
| `has(id)` | boolean | Check if window exists |
|
|
50
|
+
| `getIds()` | string[] | All window IDs |
|
|
51
|
+
| `focus(id)` | void | Bring to front |
|
|
52
|
+
| `lockWindow(id)` / `unlockWindow(id)` / `toggleLock(id)` | void | Pin/unpin (prevents drag/resize) |
|
|
53
|
+
| `isLocked(id)` | boolean | Check lock state |
|
|
54
|
+
| `showLockButtons()` / `hideLockButtons()` | void | Toggle pin button visibility |
|
|
55
|
+
| `saveLayout(slot)` / `restoreLayout(slot)` | void | Save/restore positions, sizes, lock states |
|
|
56
|
+
| `hasLayout(slot)` | boolean | Check if slot has a snapshot |
|
|
57
|
+
|
|
58
|
+
### Keyboard shortcuts
|
|
59
|
+
|
|
60
|
+
`Ctrl+1`–`Ctrl+5` save layout, `1`–`5` restore (not in inputs), `Escape` closes all.
|
|
61
|
+
|
|
62
|
+
### Events
|
|
63
|
+
|
|
64
|
+
`window:create` `{id, el}`, `window:close` `{id}`, `window:focus` `{id, el}`, `window:move` `{id, el}`, `window:resize` `{id, el}`, `window:lock` `{id, locked}`, `layout:save` `{slot}`, `layout:restore` `{slot}`.
|
|
65
|
+
|
|
66
|
+
## Widgets
|
|
67
|
+
|
|
68
|
+
Shared API: `update(data)`, `push(value)`, `bind(adapter, 'key')`, `destroy()`. All renders are rAF-batched.
|
|
69
|
+
|
|
70
|
+
### Ticker — scrolling tape, CSS animated
|
|
71
|
+
|
|
72
|
+
Options: `baseDuration` (default: 8), `durationPerItem` (default: 2).
|
|
73
|
+
Items: `{ label, value, className }`.
|
|
74
|
+
|
|
75
|
+
### MiniChart — ASCII sparkline, auto-scaling
|
|
76
|
+
|
|
77
|
+
Options: `width` (cols), `height` (rows), `maxPoints` (default: 500), `formatLabel`, `upClass` (default: 'mt-green'), `downClass` (default: 'mt-red').
|
|
78
|
+
Data: array of numbers via `update([...])` or `push(num)`.
|
|
79
|
+
|
|
80
|
+
### MessageLog — scrolling color-coded log
|
|
81
|
+
|
|
82
|
+
Options: `maxMessages` (default: 50), `typeClasses` (map of type -> CSS class, defaults: bad=mt-red, good=mt-green, trade=mt-yellow, info='', error=mt-red, warn=mt-yellow).
|
|
83
|
+
Items: `{ text, type, timestamp }` or plain string.
|
|
84
|
+
|
|
85
|
+
### RangeBar — horizontal bar with markers and zones
|
|
86
|
+
|
|
87
|
+
Options: `formatLabel` (value formatter).
|
|
88
|
+
Data: `{ lo, hi, markers: [{ position, label, type?, className? }], zones: [{ from, to, className }] }`.
|
|
89
|
+
Events: `hover` `{ value, pct }`.
|
|
90
|
+
|
|
91
|
+
### ActivityBars — animated pulsing bars, pure CSS
|
|
92
|
+
|
|
93
|
+
Options: `count` (default: 8).
|
|
94
|
+
Data: `[{ height, color?, opacity? }]` or call `randomize()`.
|
|
95
|
+
|
|
96
|
+
### Custom widgets
|
|
97
|
+
|
|
98
|
+
Extend `BaseWidget`, implement `_paint()`, call `this._schedulePaint()` from `update()`/`push()`. Gets free rAF batching, adapter binding, and cleanup.
|
|
99
|
+
|
|
100
|
+
## Data Adapters
|
|
101
|
+
|
|
102
|
+
All emit `'data'`. All support `throttle` (ms) and `transform` (fn).
|
|
103
|
+
|
|
104
|
+
| Adapter | Constructor args | Description |
|
|
105
|
+
|---------|-----------------|-------------|
|
|
106
|
+
| `ManualAdapter` | `{ throttle?, transform? }` | Push data manually via `.push(data)` |
|
|
107
|
+
| `WebSocketAdapter` | `url, { reconnect?, reconnectDelay?, throttle?, transform? }` | Auto-reconnecting WS, JSON parsed. Methods: `connect()`, `disconnect()`, `send(data)` |
|
|
108
|
+
| `RestPoller` | `url, { interval?, fetchOpts?, transform? }` | Polls endpoint on interval. Methods: `connect()`, `disconnect()` |
|
|
109
|
+
| `RedisAdapter` | `{ client, channels?, patterns?, transform? }` | Redis Pub/Sub. Inject ioredis client (no hard dep) |
|
|
110
|
+
| `NatsAdapter` | `{ natsConnect, servers, subject, transform? }` | NATS messaging. Inject `connect` from nats.ws |
|
|
111
|
+
|
|
112
|
+
**Binding:** `widget.bind(adapter, 'key')` — auto-updates widget when adapter emits, extracting `d[key]`.
|
|
113
|
+
|
|
114
|
+
## ArrowOverlay
|
|
115
|
+
|
|
116
|
+
SVG arrows between windows. Auto-updates on drag/resize.
|
|
117
|
+
|
|
118
|
+
```js
|
|
119
|
+
const arrows = new ArrowOverlay(wm, { defaultColor: '#0ff' });
|
|
120
|
+
arrows.setArrow('id', { fromWindow, toWindow, progress?, label?, color? });
|
|
121
|
+
arrows.removeArrow('id');
|
|
122
|
+
arrows.clearArrows();
|
|
123
|
+
arrows.destroy();
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## ModalManager
|
|
127
|
+
|
|
128
|
+
```js
|
|
129
|
+
const modal = new ModalManager(wm);
|
|
130
|
+
modal.show(html, { title, width });
|
|
131
|
+
modal.hide();
|
|
132
|
+
const ok = await modal.confirm(message, { title, okText, cancelText }); // -> boolean
|
|
133
|
+
const val = await modal.prompt(message, { title, defaultValue }); // -> string|null
|
|
134
|
+
modal.isOpen; // boolean
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Themes
|
|
138
|
+
|
|
139
|
+
6 built-in themes. Each sets colors, font, and font size. Apply by name or custom object.
|
|
140
|
+
|
|
141
|
+
```js
|
|
142
|
+
applyTheme('phosphor');
|
|
143
|
+
applyTheme({ '--mt-accent': '#f80', '--mt-font': "'Fira Code', monospace" });
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
| Theme | Font | Description |
|
|
147
|
+
|-------|------|-------------|
|
|
148
|
+
| `cyber` | Courier New 15px | Vivid cyan/teal (default) |
|
|
149
|
+
| `amber` | VT323 16px | Warm gold retro CRT |
|
|
150
|
+
| `phosphor` | IBM Plex Mono 14px | Soft green terminal |
|
|
151
|
+
| `hotline` | Share Tech Mono 15px | Desaturated pink/mauve |
|
|
152
|
+
| `ice` | Fira Code 14px | Muted blue-grey |
|
|
153
|
+
| `slate` | JetBrains Mono 14px | Neutral grey |
|
|
154
|
+
|
|
155
|
+
### CSS custom properties
|
|
156
|
+
|
|
157
|
+
- **Colors:** `--mt-accent`, `--mt-accent-bright`, `--mt-bg`, `--mt-border`, `--mt-green`, `--mt-red`, `--mt-yellow`, `--mt-cyan`, `--mt-magenta`, `--mt-orange`, `--mt-white`, `--mt-dim`, `--mt-text`, `--mt-titlebar-bg`, `--mt-titlebar-fg`, `--mt-glow`, `--mt-glow-strong`
|
|
158
|
+
- **Typography:** `--mt-font`, `--mt-font-size`
|
|
159
|
+
- **Sizing:** `--mt-win-min-w`, `--mt-win-min-h`, `--mt-win-padding`, `--mt-bar-width`, `--mt-bar-height`, `--mt-resize-handle`, `--mt-pb-height`, `--mt-scrollbar-width`
|
|
160
|
+
- **Animation:** `--mt-ticker-speed`, `--mt-bar-speed`, `--mt-arrow-dash-speed`, `--mt-arrow-dot-speed`
|
|
161
|
+
|
|
162
|
+
## Formatters
|
|
163
|
+
|
|
164
|
+
`formatNum(n)` — adaptive: 1.50bil / 12.3mil / 50.0k / 1,234.56 / 0.0500. `formatQty(n)` — comma-separated or 4dp for small. `formatPct(n)` — signed percentage with 1dp.
|
|
165
|
+
|
|
166
|
+
## File structure
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
index.js # barrel export
|
|
170
|
+
css/minterm.css # all styles + CSS custom properties
|
|
171
|
+
src/
|
|
172
|
+
emitter.js # Emitter base class
|
|
173
|
+
z-index.js # z-index management
|
|
174
|
+
window-manager.js # WindowManager
|
|
175
|
+
modal-manager.js # ModalManager
|
|
176
|
+
arrow-overlay.js # ArrowOverlay
|
|
177
|
+
formatters.js # formatNum, formatQty, formatPct
|
|
178
|
+
themes.js # built-in themes + applyTheme
|
|
179
|
+
widgets/{base-widget,ticker,mini-chart,message-log,range-bar,activity-bars}.js
|
|
180
|
+
adapters/{base-adapter,manual-adapter,websocket-adapter,rest-poller,nats-adapter,redis-adapter}.js
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Every file is independently importable: `import { MiniChart } from 'minterm/src/widgets/mini-chart.js'`.
|
|
184
|
+
|
|
185
|
+
## Performance
|
|
186
|
+
|
|
187
|
+
- **rAF batching** — 1000 `.push()` calls/sec = 60 renders/sec
|
|
188
|
+
- **Incremental DOM** — MessageLog appends nodes, doesn't rebuild innerHTML
|
|
189
|
+
- **SVG reuse** — ArrowOverlay caches elements, updates attributes only
|
|
190
|
+
- **CSS-only animation** — ticker, bars, arrows use `@keyframes`, no JS animation loop
|
|
191
|
+
- **GPU-composited drag** — windows use `transform: translate()` during drag, baked to `left/top` on drop
|
|
192
|
+
- **CSS containment** — `.mt-win` uses `contain: layout style` to isolate style recalc scope
|
|
193
|
+
- **Flat grid** — MiniChart uses `Array(H*W)` with indexed access, no nested arrays
|
|
194
|
+
- **Adapter throttle** — `{ throttle: 16 }` caps at ~60 emissions/sec, drops intermediates, keeps latest value
|
package/css/minterm.css
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
/* minterm — DOS-style windowing CSS
|
|
2
|
+
All classes prefixed mt-. Retheme via CSS custom properties. */
|
|
3
|
+
|
|
4
|
+
:root {
|
|
5
|
+
/* ── Colors ── */
|
|
6
|
+
--mt-accent: #0aa;
|
|
7
|
+
--mt-accent-bright: #0ff;
|
|
8
|
+
--mt-bg: #0a0a0a;
|
|
9
|
+
--mt-border: #0aa;
|
|
10
|
+
--mt-green: #0f0;
|
|
11
|
+
--mt-red: #f44;
|
|
12
|
+
--mt-yellow: #ff0;
|
|
13
|
+
--mt-cyan: #0ff;
|
|
14
|
+
--mt-magenta: #f0f;
|
|
15
|
+
--mt-orange: #f80;
|
|
16
|
+
--mt-white: #fff;
|
|
17
|
+
--mt-dim: #666;
|
|
18
|
+
--mt-text: #ccc;
|
|
19
|
+
|
|
20
|
+
/* ── Typography ── */
|
|
21
|
+
--mt-font: 'Courier New', 'Consolas', monospace;
|
|
22
|
+
--mt-font-size: 15px;
|
|
23
|
+
|
|
24
|
+
/* ── Titlebar ── */
|
|
25
|
+
--mt-titlebar-bg: var(--mt-accent);
|
|
26
|
+
--mt-titlebar-fg: #000;
|
|
27
|
+
--mt-titlebar-padding: 3px 8px;
|
|
28
|
+
|
|
29
|
+
/* ── Window chrome ── */
|
|
30
|
+
--mt-win-min-w: 200px;
|
|
31
|
+
--mt-win-min-h: 60px;
|
|
32
|
+
--mt-win-padding: 6px 8px;
|
|
33
|
+
--mt-glow: rgba(0, 170, 170, 0.12);
|
|
34
|
+
--mt-glow-strong: rgba(0, 170, 170, 0.3);
|
|
35
|
+
|
|
36
|
+
/* ── Scrollbar ── */
|
|
37
|
+
--mt-scrollbar-track: var(--mt-bg);
|
|
38
|
+
--mt-scrollbar-thumb: var(--mt-accent);
|
|
39
|
+
--mt-scrollbar-width: 6px;
|
|
40
|
+
|
|
41
|
+
/* ── Animation speeds ── */
|
|
42
|
+
--mt-ticker-speed: 8s; /* base ticker scroll duration */
|
|
43
|
+
--mt-bar-speed: 0.8s; /* activity bar pulse cycle */
|
|
44
|
+
--mt-arrow-dash-speed: 1.2s; /* arrow dash animation */
|
|
45
|
+
--mt-arrow-dot-speed: 1.5s; /* arrow traveling dot pulse */
|
|
46
|
+
|
|
47
|
+
/* ── Sizing ── */
|
|
48
|
+
--mt-bar-width: 3px; /* activity bar column width */
|
|
49
|
+
--mt-bar-height: 28px; /* activity bars container height */
|
|
50
|
+
--mt-resize-handle: 16px; /* resize grip size */
|
|
51
|
+
--mt-pb-height: 32px; /* price bar height */
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* ─── Color utilities ────────────────────────────────────────── */
|
|
55
|
+
.mt-cyan { color: var(--mt-cyan); }
|
|
56
|
+
.mt-yellow { color: var(--mt-yellow); }
|
|
57
|
+
.mt-white { color: var(--mt-white); }
|
|
58
|
+
.mt-green { color: var(--mt-green); }
|
|
59
|
+
.mt-red { color: var(--mt-red); }
|
|
60
|
+
.mt-magenta { color: var(--mt-magenta); }
|
|
61
|
+
.mt-dim { color: var(--mt-dim); }
|
|
62
|
+
.mt-orange { color: var(--mt-orange); }
|
|
63
|
+
|
|
64
|
+
/* ─── Window ─────────────────────────────────────────────────── */
|
|
65
|
+
.mt-win {
|
|
66
|
+
position: fixed;
|
|
67
|
+
background: var(--mt-bg);
|
|
68
|
+
border: 1px solid var(--mt-border);
|
|
69
|
+
font-family: var(--mt-font);
|
|
70
|
+
font-size: var(--mt-font-size);
|
|
71
|
+
box-shadow: 0 0 12px var(--mt-glow), 0 0 2px var(--mt-glow-strong);
|
|
72
|
+
display: flex;
|
|
73
|
+
flex-direction: column;
|
|
74
|
+
min-width: var(--mt-win-min-w);
|
|
75
|
+
min-height: var(--mt-win-min-h);
|
|
76
|
+
overflow: hidden;
|
|
77
|
+
color: var(--mt-text);
|
|
78
|
+
contain: layout style;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.mt-win-dragging { will-change: transform; }
|
|
82
|
+
|
|
83
|
+
.mt-win-titlebar {
|
|
84
|
+
background: var(--mt-titlebar-bg);
|
|
85
|
+
color: var(--mt-titlebar-fg);
|
|
86
|
+
padding: var(--mt-titlebar-padding);
|
|
87
|
+
cursor: grab;
|
|
88
|
+
font-weight: bold;
|
|
89
|
+
font-size: var(--mt-font-size);
|
|
90
|
+
user-select: none;
|
|
91
|
+
flex-shrink: 0;
|
|
92
|
+
display: flex;
|
|
93
|
+
align-items: center;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.mt-win-title { pointer-events: none; flex: 1; }
|
|
97
|
+
|
|
98
|
+
.mt-win-lock {
|
|
99
|
+
cursor: pointer;
|
|
100
|
+
padding: 0 4px;
|
|
101
|
+
margin-left: auto;
|
|
102
|
+
color: inherit;
|
|
103
|
+
opacity: 0.35;
|
|
104
|
+
transition: opacity 0.15s, color 0.15s;
|
|
105
|
+
display: inline-flex;
|
|
106
|
+
align-items: center;
|
|
107
|
+
}
|
|
108
|
+
.mt-win-lock svg { display: block; transition: transform 0.15s; }
|
|
109
|
+
.mt-win-lock-hidden { display: none; }
|
|
110
|
+
.mt-win-lock:hover { opacity: 0.8; }
|
|
111
|
+
.mt-win-locked .mt-win-lock { opacity: 1; color: var(--mt-accent-bright); filter: drop-shadow(0 0 3px var(--mt-accent)); }
|
|
112
|
+
.mt-win-locked .mt-win-lock svg { transform: rotate(-45deg); }
|
|
113
|
+
.mt-win-locked .mt-win-titlebar { cursor: default; }
|
|
114
|
+
.mt-win-locked .mt-win-resize { display: none; }
|
|
115
|
+
|
|
116
|
+
.mt-win-close {
|
|
117
|
+
cursor: pointer;
|
|
118
|
+
padding: 0 4px;
|
|
119
|
+
font-size: 14px;
|
|
120
|
+
}
|
|
121
|
+
.mt-win-close:hover { background: var(--mt-red); color: var(--mt-white); }
|
|
122
|
+
|
|
123
|
+
.mt-win-body {
|
|
124
|
+
padding: var(--mt-win-padding);
|
|
125
|
+
overflow: auto;
|
|
126
|
+
flex: 1;
|
|
127
|
+
scrollbar-width: thin;
|
|
128
|
+
scrollbar-color: var(--mt-scrollbar-thumb) var(--mt-scrollbar-track);
|
|
129
|
+
}
|
|
130
|
+
.mt-win-body::-webkit-scrollbar { width: var(--mt-scrollbar-width); }
|
|
131
|
+
.mt-win-body::-webkit-scrollbar-track { background: var(--mt-scrollbar-track); }
|
|
132
|
+
.mt-win-body::-webkit-scrollbar-thumb { background: var(--mt-scrollbar-thumb); border-radius: 3px; }
|
|
133
|
+
.mt-win-body::-webkit-scrollbar-thumb:hover { background: var(--mt-accent-bright); }
|
|
134
|
+
|
|
135
|
+
.mt-win-resize {
|
|
136
|
+
position: absolute;
|
|
137
|
+
right: 0; bottom: 0;
|
|
138
|
+
width: var(--mt-resize-handle); height: var(--mt-resize-handle);
|
|
139
|
+
cursor: nwse-resize;
|
|
140
|
+
background:
|
|
141
|
+
linear-gradient(135deg,
|
|
142
|
+
transparent 50%, var(--mt-accent) 50%, var(--mt-accent) 58%, transparent 58%,
|
|
143
|
+
transparent 66%, var(--mt-accent) 66%, var(--mt-accent) 74%, transparent 74%,
|
|
144
|
+
transparent 82%, var(--mt-accent) 82%, var(--mt-accent) 90%, transparent 90%);
|
|
145
|
+
opacity: 0.5;
|
|
146
|
+
}
|
|
147
|
+
.mt-win-resize:hover { opacity: 1; }
|
|
148
|
+
|
|
149
|
+
/* ─── Ticker ─────────────────────────────────────────────────── */
|
|
150
|
+
.mt-ticker-wrap {
|
|
151
|
+
overflow: hidden;
|
|
152
|
+
white-space: nowrap;
|
|
153
|
+
border-bottom: 1px solid color-mix(in srgb, var(--mt-accent) 25%, transparent);
|
|
154
|
+
padding: 3px 0;
|
|
155
|
+
margin-bottom: 2px;
|
|
156
|
+
font-size: 11px;
|
|
157
|
+
position: relative;
|
|
158
|
+
mask-image: linear-gradient(to right, transparent, #000 8%, #000 92%, transparent);
|
|
159
|
+
-webkit-mask-image: linear-gradient(to right, transparent, #000 8%, #000 92%, transparent);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.mt-ticker-track {
|
|
163
|
+
display: inline-block;
|
|
164
|
+
animation: mt-tickerScroll var(--ticker-duration, var(--mt-ticker-speed)) linear infinite;
|
|
165
|
+
will-change: transform;
|
|
166
|
+
}
|
|
167
|
+
.mt-ticker-track:hover { animation-play-state: paused; }
|
|
168
|
+
|
|
169
|
+
.mt-ticker-item { display: inline-block; margin-right: 16px; }
|
|
170
|
+
.mt-ticker-label { color: var(--mt-white); }
|
|
171
|
+
|
|
172
|
+
@keyframes mt-tickerScroll {
|
|
173
|
+
0% { transform: translateX(0); }
|
|
174
|
+
100% { transform: translateX(-50%); }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* ─── Activity Bars ──────────────────────────────────────────── */
|
|
178
|
+
.mt-bars {
|
|
179
|
+
display: flex;
|
|
180
|
+
align-items: flex-end;
|
|
181
|
+
gap: 1px;
|
|
182
|
+
height: var(--mt-bar-height);
|
|
183
|
+
flex-shrink: 0;
|
|
184
|
+
padding: 0 2px;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.mt-bar {
|
|
188
|
+
width: var(--mt-bar-width);
|
|
189
|
+
border-radius: 1px 1px 0 0;
|
|
190
|
+
animation: mt-barPulse var(--mt-bar-speed) ease-in-out infinite alternate;
|
|
191
|
+
transform-origin: bottom;
|
|
192
|
+
will-change: transform;
|
|
193
|
+
}
|
|
194
|
+
.mt-bar:nth-child(2) { animation-duration: 0.65s; animation-delay: 0.08s; }
|
|
195
|
+
.mt-bar:nth-child(3) { animation-duration: 0.9s; animation-delay: 0.18s; }
|
|
196
|
+
.mt-bar:nth-child(4) { animation-duration: 0.55s; animation-delay: 0.25s; }
|
|
197
|
+
.mt-bar:nth-child(5) { animation-duration: 0.75s; animation-delay: 0.12s; }
|
|
198
|
+
.mt-bar:nth-child(6) { animation-duration: 0.6s; animation-delay: 0.32s; }
|
|
199
|
+
.mt-bar:nth-child(7) { animation-duration: 0.85s; animation-delay: 0.05s; }
|
|
200
|
+
.mt-bar:nth-child(8) { animation-duration: 0.7s; animation-delay: 0.2s; }
|
|
201
|
+
|
|
202
|
+
@keyframes mt-barPulse {
|
|
203
|
+
0% { transform: scaleY(1); }
|
|
204
|
+
25% { transform: scaleY(0.4); }
|
|
205
|
+
50% { transform: scaleY(0.85); }
|
|
206
|
+
75% { transform: scaleY(0.3); }
|
|
207
|
+
100% { transform: scaleY(1); }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/* ─── Price Bar ──────────────────────────────────────────────── */
|
|
211
|
+
.mt-pb {
|
|
212
|
+
height: var(--mt-pb-height);
|
|
213
|
+
position: relative;
|
|
214
|
+
margin: 2px 0;
|
|
215
|
+
cursor: crosshair;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.mt-pb-track {
|
|
219
|
+
position: absolute;
|
|
220
|
+
top: 0; left: 0; right: 0; bottom: 0;
|
|
221
|
+
background: var(--mt-bg);
|
|
222
|
+
border: 1px solid #222;
|
|
223
|
+
overflow: hidden;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.mt-pb-fill {
|
|
227
|
+
position: absolute;
|
|
228
|
+
top: 0; bottom: 0;
|
|
229
|
+
pointer-events: none;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.mt-pb-marker {
|
|
233
|
+
position: absolute;
|
|
234
|
+
top: 0; bottom: 0;
|
|
235
|
+
width: 1px;
|
|
236
|
+
z-index: 2;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.mt-pb-line {
|
|
240
|
+
position: absolute;
|
|
241
|
+
top: 0; bottom: 0;
|
|
242
|
+
left: 0; width: 2px;
|
|
243
|
+
background: var(--mt-accent);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.mt-pb-label {
|
|
247
|
+
position: absolute;
|
|
248
|
+
top: -1px; left: 4px;
|
|
249
|
+
font-size: 9px;
|
|
250
|
+
white-space: nowrap;
|
|
251
|
+
line-height: 32px;
|
|
252
|
+
color: var(--mt-accent);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.mt-pb-current .mt-pb-line { background: var(--mt-white); width: 3px; }
|
|
256
|
+
.mt-pb-current .mt-pb-label { color: var(--mt-white); font-weight: bold; font-size: 10px; }
|
|
257
|
+
|
|
258
|
+
.mt-pb-danger .mt-pb-line { background: var(--mt-red); }
|
|
259
|
+
.mt-pb-danger .mt-pb-label { color: var(--mt-red); }
|
|
260
|
+
|
|
261
|
+
.mt-pb-ghost {
|
|
262
|
+
position: absolute;
|
|
263
|
+
top: 0; bottom: 0;
|
|
264
|
+
width: 1px;
|
|
265
|
+
z-index: 5;
|
|
266
|
+
pointer-events: none;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.mt-pb-ghost-line {
|
|
270
|
+
position: absolute;
|
|
271
|
+
top: 0; bottom: 0;
|
|
272
|
+
left: 0; width: 1px;
|
|
273
|
+
border-left: 1px dashed var(--mt-dim);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.mt-pb-ghost-label {
|
|
277
|
+
position: absolute;
|
|
278
|
+
top: 1px; left: 6px;
|
|
279
|
+
font-size: 11px;
|
|
280
|
+
font-weight: bold;
|
|
281
|
+
white-space: nowrap;
|
|
282
|
+
line-height: 14px;
|
|
283
|
+
text-shadow: 0 0 4px #000, 0 0 8px #000;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/* ─── Chart Canvas ───────────────────────────────────────────── */
|
|
287
|
+
.mt-chart-canvas {
|
|
288
|
+
font-family: var(--mt-font);
|
|
289
|
+
font-size: 11px;
|
|
290
|
+
line-height: 1.1;
|
|
291
|
+
letter-spacing: 0;
|
|
292
|
+
margin: 0; padding: 0;
|
|
293
|
+
color: var(--mt-dim);
|
|
294
|
+
overflow: hidden;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/* ─── Message Log ────────────────────────────────────────────── */
|
|
298
|
+
.mt-message { padding: 1px 0; color: #aaa; }
|
|
299
|
+
|
|
300
|
+
/* ─── Arrow Overlay ──────────────────────────────────────────── */
|
|
301
|
+
.mt-arrow-svg {
|
|
302
|
+
position: fixed;
|
|
303
|
+
top: 0; left: 0;
|
|
304
|
+
width: 100vw; height: 100vh;
|
|
305
|
+
pointer-events: none;
|
|
306
|
+
z-index: 1;
|
|
307
|
+
overflow: visible;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.mt-arrow-line {
|
|
311
|
+
animation: mt-arrowDash var(--mt-arrow-dash-speed) linear infinite;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
@keyframes mt-arrowDash {
|
|
315
|
+
0% { stroke-dashoffset: 0; }
|
|
316
|
+
100% { stroke-dashoffset: -20; }
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.mt-arrow-dot {
|
|
320
|
+
animation: mt-arrowDotPulse var(--mt-arrow-dot-speed) ease-in-out infinite;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
@keyframes mt-arrowDotPulse {
|
|
324
|
+
0%, 100% { opacity: 1; r: 3; }
|
|
325
|
+
50% { opacity: 0.5; r: 5; }
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.mt-arrow-label {
|
|
329
|
+
font-family: var(--mt-font);
|
|
330
|
+
font-size: 10px;
|
|
331
|
+
text-anchor: middle;
|
|
332
|
+
paint-order: stroke;
|
|
333
|
+
stroke: #000;
|
|
334
|
+
stroke-width: 3px;
|
|
335
|
+
stroke-linecap: round;
|
|
336
|
+
stroke-linejoin: round;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/* ─── Modal ──────────────────────────────────────────────────── */
|
|
340
|
+
.mt-modal-body { margin: 6px 0; }
|
|
341
|
+
.mt-modal-footer { display: flex; gap: 8px; margin-top: 8px; justify-content: center; }
|
|
342
|
+
|
|
343
|
+
.mt-btn {
|
|
344
|
+
font-family: var(--mt-font);
|
|
345
|
+
font-size: 12px;
|
|
346
|
+
background: var(--mt-bg);
|
|
347
|
+
color: var(--mt-accent-bright);
|
|
348
|
+
border: 1px solid var(--mt-accent);
|
|
349
|
+
padding: 4px 12px;
|
|
350
|
+
cursor: pointer;
|
|
351
|
+
}
|
|
352
|
+
.mt-btn:hover { background: var(--mt-accent); color: var(--mt-titlebar-fg); }
|
|
353
|
+
|
|
354
|
+
.mt-input {
|
|
355
|
+
font-family: var(--mt-font);
|
|
356
|
+
font-size: 12px;
|
|
357
|
+
background: var(--mt-bg);
|
|
358
|
+
color: var(--mt-accent-bright);
|
|
359
|
+
border: 1px solid var(--mt-accent);
|
|
360
|
+
padding: 4px 8px;
|
|
361
|
+
width: 100%;
|
|
362
|
+
box-sizing: border-box;
|
|
363
|
+
margin-top: 4px;
|
|
364
|
+
}
|
|
365
|
+
.mt-input:focus { outline: none; border-color: var(--mt-accent-bright); }
|
|
366
|
+
|
|
367
|
+
/* ─── Layout Bar ─────────────────────────────────────────────── */
|
|
368
|
+
.mt-layout-bar {
|
|
369
|
+
position: fixed;
|
|
370
|
+
top: 6px; left: 40px;
|
|
371
|
+
z-index: 999999;
|
|
372
|
+
display: flex; gap: 4px;
|
|
373
|
+
font-family: var(--mt-font);
|
|
374
|
+
font-size: 11px;
|
|
375
|
+
user-select: none;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.mt-layout-slot {
|
|
379
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
380
|
+
width: 20px; height: 20px;
|
|
381
|
+
border: 1px solid var(--mt-dim);
|
|
382
|
+
color: var(--mt-dim);
|
|
383
|
+
background: var(--mt-bg);
|
|
384
|
+
cursor: pointer;
|
|
385
|
+
transition: all 0.15s;
|
|
386
|
+
}
|
|
387
|
+
.mt-layout-slot:hover { border-color: var(--mt-accent); color: var(--mt-accent); }
|
|
388
|
+
|
|
389
|
+
.mt-layout-saved {
|
|
390
|
+
border-color: var(--mt-accent);
|
|
391
|
+
color: var(--mt-accent-bright);
|
|
392
|
+
text-shadow: 0 0 4px var(--mt-accent);
|
|
393
|
+
}
|
|
394
|
+
.mt-layout-saved:hover { background: var(--mt-accent); color: var(--mt-titlebar-fg); text-shadow: none; }
|
|
395
|
+
|
|
396
|
+
@keyframes mt-layoutFlash {
|
|
397
|
+
0%, 100% { background: var(--mt-bg); }
|
|
398
|
+
50% { background: var(--mt-accent); color: var(--mt-titlebar-fg); }
|
|
399
|
+
}
|
|
400
|
+
.mt-layout-flash { animation: mt-layoutFlash 0.3s ease 2; }
|
package/index.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// minterm — modular DOS-style windowing UI
|
|
2
|
+
// Import only what you need, or use this barrel export.
|
|
3
|
+
|
|
4
|
+
// Core
|
|
5
|
+
export { Emitter } from './src/emitter.js';
|
|
6
|
+
export { getNextZ, bringToFront, getAllWindows, cycleWindow } from './src/z-index.js';
|
|
7
|
+
export { WindowManager } from './src/window-manager.js';
|
|
8
|
+
export { ModalManager } from './src/modal-manager.js';
|
|
9
|
+
export { ArrowOverlay } from './src/arrow-overlay.js';
|
|
10
|
+
export { formatNum, formatPrice, formatQty, formatPct } from './src/formatters.js';
|
|
11
|
+
export { themes, applyTheme } from './src/themes.js';
|
|
12
|
+
|
|
13
|
+
// Widgets
|
|
14
|
+
export { BaseWidget } from './src/widgets/base-widget.js';
|
|
15
|
+
export { Ticker } from './src/widgets/ticker.js';
|
|
16
|
+
export { MiniChart } from './src/widgets/mini-chart.js';
|
|
17
|
+
export { MessageLog } from './src/widgets/message-log.js';
|
|
18
|
+
export { RangeBar } from './src/widgets/range-bar.js';
|
|
19
|
+
export { ActivityBars } from './src/widgets/activity-bars.js';
|
|
20
|
+
|
|
21
|
+
// Adapters
|
|
22
|
+
export { BaseAdapter } from './src/adapters/base-adapter.js';
|
|
23
|
+
export { ManualAdapter } from './src/adapters/manual-adapter.js';
|
|
24
|
+
export { WebSocketAdapter } from './src/adapters/websocket-adapter.js';
|
|
25
|
+
export { RestPoller } from './src/adapters/rest-poller.js';
|
|
26
|
+
export { NatsAdapter } from './src/adapters/nats-adapter.js';
|
|
27
|
+
export { RedisAdapter } from './src/adapters/redis-adapter.js';
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "minterm",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Minimal DOS-style windowing UI — zero deps, rAF-batched widgets, data adapters",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.js",
|
|
9
|
+
"./css": "./css/minterm.css"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"index.js",
|
|
13
|
+
"src/",
|
|
14
|
+
"css/",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"sideEffects": ["./css/minterm.css"],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"lint": "eslint src/ index.js",
|
|
20
|
+
"lint:fix": "eslint src/ index.js --fix",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"test:watch": "vitest"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"eslint": "^9.0.0",
|
|
26
|
+
"jsdom": "^25.0.0",
|
|
27
|
+
"vitest": "^3.0.0"
|
|
28
|
+
},
|
|
29
|
+
"keywords": ["terminal", "retro", "ui", "windowing", "dos", "widgets", "minterm"],
|
|
30
|
+
"author": "haiyanghe",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/haiyanghe/minterm.git"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/haiyanghe/minterm#readme",
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/haiyanghe/minterm/issues"
|
|
38
|
+
},
|
|
39
|
+
"license": "Unlicense"
|
|
40
|
+
}
|