monowind 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/dist/wrap.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Greedy word-wrap for monospace text.
3
+ *
4
+ * Words are runs of non-whitespace; whitespace runs collapse to single spaces
5
+ * between fitting words. Browsers also treat a hyphen inside a word as a
6
+ * break opportunity (break after `-`, no hyphen added) — except before a
7
+ * digit, per UAX #14 (`2026-08` doesn't break) — so words are further split
8
+ * into breakable segments. A segment longer than `width` breaks at cell
9
+ * boundaries. `\n` in the input is a HARD line break — the wrap restarts on
10
+ * a new line (the source of these is `<br>` elements, converted to `\n` by
11
+ * the tree builder). A blank hard line still occupies one row.
12
+ *
13
+ * Matches how a browser wraps `white-space: normal; overflow-wrap: anywhere`
14
+ * text in a fixed-width monospace container — we set that in styles.css so
15
+ * the two agree.
16
+ */
17
+ export declare function wrapLines(text: string, width: number): string[];
18
+ /** Number of rows `text` occupies at `width` (see wrapLines). */
19
+ export declare function wrapLineCount(text: string, width: number): number;
20
+ /**
21
+ * Split a word at its internal break opportunities: after each hyphen run,
22
+ * unless the next character is a digit (UAX #14: no break between a hyphen
23
+ * and a following number). `"mx-auto"` → `["mx-", "auto"]`;
24
+ * `"2026-08"` → `["2026-08"]`. Also the unit of min-content width.
25
+ */
26
+ export declare function breakableSegments(word: string): string[];
27
+ //# sourceMappingURL=wrap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wrap.d.ts","sourceRoot":"","sources":["../src/wrap.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAG/D;AAED,iEAAiE;AACjE,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAEjE;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAcxD"}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "monowind",
3
+ "version": "0.1.0",
4
+ "description": "Text-based user interfaces (TUI) on the web, from ordinary HTML and Tailwind utility classes",
5
+ "keywords": [
6
+ "ascii",
7
+ "monospace",
8
+ "tailwind",
9
+ "terminal",
10
+ "text-based",
11
+ "tui",
12
+ "web-component"
13
+ ],
14
+ "license": "MIT",
15
+ "author": "Benoît Rouleau",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/benface/monowind.git",
19
+ "directory": "packages/core"
20
+ },
21
+ "style": "./src/styles.css",
22
+ "files": [
23
+ "dist",
24
+ "src/styles.css"
25
+ ],
26
+ "type": "module",
27
+ "sideEffects": false,
28
+ "exports": {
29
+ ".": {
30
+ "style": "./src/styles.css",
31
+ "types": "./dist/index.d.ts",
32
+ "default": "./dist/index.js"
33
+ },
34
+ "./styles.css": "./src/styles.css"
35
+ },
36
+ "devDependencies": {
37
+ "@tailwindcss/browser": "^4.3.3",
38
+ "happy-dom": "^20.11.6",
39
+ "typescript": "^7.0.2",
40
+ "vite": "^8.2.2",
41
+ "vitest": "^4.1.11"
42
+ },
43
+ "scripts": {
44
+ "build": "vite build && vite build -c vite.cdn.config.ts && tsc -p tsconfig.build.json",
45
+ "typecheck": "tsc --noEmit",
46
+ "test": "vitest run",
47
+ "test:watch": "vitest"
48
+ }
49
+ }
package/src/styles.css ADDED
@@ -0,0 +1,145 @@
1
+ /* monowind companion stylesheet — loaded into the light DOM by the app.
2
+ * The shadow root has its own baked-in styles; this file targets the light-DOM
3
+ * subtree the host owns.
4
+ *
5
+ * Layering: defaults and normalization that USERS may override live in
6
+ * `@layer base` — Tailwind's utilities layer (and any unlayered author CSS)
7
+ * then wins over them, matching Tailwind's own preflight behavior. Rules the
8
+ * ENGINE owns (geometry, typography locking, alignment/overflow enforcement)
9
+ * stay unlayered with `!important` so no utility can push content off-grid. */
10
+
11
+ @layer base {
12
+ mono-wind {
13
+ display: block;
14
+ position: relative;
15
+ /* New stacking context so ancestor z-index / blend-modes don't bleed in
16
+ * and our own absolute-positioned decorations don't leak out. */
17
+ isolation: isolate;
18
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
19
+ font-size: 14px;
20
+ line-height: 1;
21
+ letter-spacing: 0;
22
+ /* Default themable focus ring color (see form-controls rule below). */
23
+ --mw-focus-color: currentColor;
24
+ }
25
+
26
+ /* Normalize native form controls so they don't borrow browser chrome that
27
+ * would break the grid. Padding, margin, border-width, and box-sizing are
28
+ * handled by the engine-owned rules below. In `base` so utilities
29
+ * (text-*, bg-*, …) can restyle controls. */
30
+ mono-wind :is(button, input, select, textarea) {
31
+ border-radius: 0;
32
+ color: inherit;
33
+ background: transparent;
34
+ font: inherit;
35
+ line-height: inherit;
36
+ text-align: inherit;
37
+ appearance: none;
38
+ }
39
+
40
+ /* Restore a themable focus ring — `appearance: none` above kills the UA
41
+ * default, and without this keyboard users can lose track of where focus
42
+ * is. `outline-offset: -1px` keeps the ring inside the element's box so it
43
+ * doesn't overlap our decoration layer. */
44
+ mono-wind :is(button, input, select, textarea, a):focus-visible {
45
+ outline: 2px solid var(--mw-focus-color);
46
+ outline-offset: -1px;
47
+ }
48
+ }
49
+
50
+ /* Hide the host until the first layout completes to avoid the flash of raw
51
+ * browser-rendered content (flex without our decorations, elements at their
52
+ * natural sizes). `visibility: hidden` preserves layout & measurability so
53
+ * cell metrics can still be probed while invisible. */
54
+ mono-wind:not([data-mw-ready]) {
55
+ visibility: hidden !important;
56
+ }
57
+
58
+ /* Lock typography on descendants so the grid metrics stay consistent —
59
+ * `!important` because user classes (`.text-2xl` etc.) would otherwise beat
60
+ * these element-selector rules on specificity. */
61
+ mono-wind * {
62
+ font-family: inherit !important;
63
+ font-size: inherit !important;
64
+ line-height: inherit !important;
65
+ letter-spacing: inherit !important;
66
+ vertical-align: baseline !important;
67
+ }
68
+
69
+ /* Match the engine's greedy word-wrap: break at spaces, and break long
70
+ * words at cell boundaries when they overflow. Gated on `measuring` (unlike
71
+ * the font lock above) so the style reader sees the authored `white-space`
72
+ * — that's how `nowrap` is detected. */
73
+ mono-wind:not([measuring]) * {
74
+ white-space: normal !important;
75
+ overflow-wrap: anywhere !important;
76
+ }
77
+
78
+ /* Author-requested `white-space: nowrap` (or `pre`) — the engine sized this
79
+ * box for unwrapped hard lines, so the browser must not wrap either. Wins
80
+ * over the lock above on specificity. */
81
+ mono-wind:not([measuring]) [data-mw-nowrap] {
82
+ white-space: nowrap !important;
83
+ }
84
+
85
+ /* Truncation (`truncate` = nowrap + clip + text-overflow: ellipsis): use
86
+ * `hidden` rather than the normalized `clip` for this combo — some engines
87
+ * only apply `text-overflow` to scroll containers, which `clip` boxes are
88
+ * not. Wins over the `[data-mw-clip]` rule below on specificity. */
89
+ mono-wind:not([measuring]) [data-mw-nowrap][data-mw-clip] {
90
+ overflow: hidden !important;
91
+ }
92
+
93
+ /* Every CSS property the engine has taken over on laid-out elements.
94
+ * Gated on the `measuring` attribute so the style reader still sees the
95
+ * authored values during its read pass. Being unlayered already beats all
96
+ * layered CSS (e.g. Tailwind utilities); the `!important` additionally
97
+ * defends against inline `style=""` attributes (hand-written or CSS-in-JS)
98
+ * and high-specificity unlayered author CSS. Known escape hatch: Tailwind's
99
+ * `!` modifier (important-in-layer) still wins, per the cascade spec.
100
+ *
101
+ * Geometry: position/box-sizing/left/top/width/height + min/max — set from
102
+ * the engine-owned --mw-x/y/w/h custom properties.
103
+ *
104
+ * Neutralized: border-width (painted as glyphs by the decoration layer),
105
+ * margin (position:absolute + margin still nudges the element off-grid).
106
+ *
107
+ * Owned via `--mw-p{t,r,b,l}` (padding) and `--mw-b{t,r,b,l}` (border cells):
108
+ * both must land on whole cells. The browser applies the sum as native
109
+ * padding since we've zeroed native border-width — this way, content sits
110
+ * inset by exactly the (border + padding) cells the engine planned for.
111
+ *
112
+ * Left alone: flex/grid/gap/justify/align — laid-out children are all
113
+ * position:absolute (out of flow), so nothing consumes them anyway. */
114
+ mono-wind:not([measuring]) [data-mw-laid-out] {
115
+ position: absolute !important;
116
+ box-sizing: border-box !important;
117
+ left: calc(var(--mw-x, 0) * var(--mw-cw, 1px)) !important;
118
+ top: calc(var(--mw-y, 0) * var(--mw-ch, 1px)) !important;
119
+ width: calc(var(--mw-w, 0) * var(--mw-cw, 1px)) !important;
120
+ height: calc(var(--mw-h, 0) * var(--mw-ch, 1px)) !important;
121
+ min-width: 0 !important;
122
+ min-height: 0 !important;
123
+ max-width: none !important;
124
+ max-height: none !important;
125
+ border-width: 0 !important;
126
+ padding-top: calc((var(--mw-pt, 0) + var(--mw-bt, 0)) * var(--mw-ch, 1px)) !important;
127
+ padding-right: calc((var(--mw-pr, 0) + var(--mw-br, 0)) * var(--mw-cw, 1px)) !important;
128
+ padding-bottom: calc((var(--mw-pb, 0) + var(--mw-bb, 0)) * var(--mw-ch, 1px)) !important;
129
+ padding-left: calc((var(--mw-pl, 0) + var(--mw-bl, 0)) * var(--mw-cw, 1px)) !important;
130
+ margin: 0 !important;
131
+ }
132
+
133
+ /* Author-requested `overflow: hidden` or `overflow: clip` — both keep
134
+ * content inside the engine-allocated box. Uses `clip` (no scroll
135
+ * container, cheaper, more precise semantic for what we're doing). */
136
+ mono-wind [data-mw-clip] {
137
+ overflow: clip !important;
138
+ }
139
+
140
+ /* Force off-grid text alignments back to `start`. `text-left`/`right`/`start`/
141
+ * `end` all land on whole cells (offset = (W - N) × cell width); `center` and
142
+ * `justify` produce fractional per-line offsets we can't snap to the grid. */
143
+ mono-wind [data-mw-text-align-blocked] {
144
+ text-align: start !important;
145
+ }