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 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
@@ -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
+ }