pi-openui 0.1.2 → 0.3.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/CHANGELOG.md +96 -0
- package/README.md +49 -6
- package/package.json +3 -2
- package/src/blocks/choice.ts +152 -0
- package/src/blocks/code.ts +16 -3
- package/src/blocks/core.ts +150 -55
- package/src/blocks/diagrams.ts +8 -26
- package/src/blocks/fallback.ts +25 -0
- package/src/blocks/gesture.ts +112 -0
- package/src/blocks/interact.ts +85 -11
- package/src/blocks/markdown.ts +13 -3
- package/src/blocks/pan.ts +139 -0
- package/src/blocks/panblock.ts +44 -0
- package/src/blocks/render.ts +34 -21
- package/src/blocks/sequence.ts +4 -4
- package/src/blocks/table.ts +122 -14
- package/src/blocks/types.ts +31 -4
- package/src/blocks/util.ts +5 -0
- package/src/diagram/canvas.ts +11 -4
- package/src/diagram/dag.ts +4 -3
- package/src/diagram/erd.ts +4 -2
- package/src/diagram/infra.ts +3 -2
- package/src/diagram/palette.ts +1 -1
- package/src/diagram/wide.ts +4 -5
- package/src/diagram/widefix.ts +56 -0
- package/src/lang/docs.ts +81 -25
- package/src/lang/library.ts +154 -22
- package/src/lang/samples.ts +144 -1
- package/src/pi/config.ts +185 -0
- package/src/pi/events.ts +150 -0
- package/src/pi/extension.ts +82 -20
- package/src/pi/widget.ts +189 -18
- package/src/pi/widths.ts +16 -0
package/src/blocks/table.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { textWidth, truncateText, type Cell, type Rect } from "../diagram/canvas.ts";
|
|
2
2
|
import type { Block } from "./types.ts";
|
|
3
|
+
import { frozenFor, pinBudget } from "./pan.ts";
|
|
3
4
|
import { cell, fromRows, padRow, textCells } from "./util.ts";
|
|
4
5
|
|
|
5
6
|
export interface TableAnchors {
|
|
@@ -10,23 +11,81 @@ export interface TableAnchors {
|
|
|
10
11
|
|
|
11
12
|
const NUMERIC = /^[-+]?[\d.,]+\s*(%|ms|s|kb|mb|gb|tb|x)?$/i;
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
/**
|
|
15
|
+
* `opts.minCol` is the user's minimum column width. With it, a table that does not fit shrinks its widest columns only down to
|
|
16
|
+
* min(natural, minCol); if even that does not fit, every column takes its natural width and the block is wider than the pane (the
|
|
17
|
+
* caller scrolls it): a table that scrolls cuts nothing, whatever a cell holds. Without it (a markdown table, which has nowhere to scroll) the old rule stands: columns shrink
|
|
18
|
+
* to three characters and no further. `edges` are the left borders of the columns, so a scroll step can land on one.
|
|
19
|
+
*
|
|
20
|
+
* `opts.pin` lists the columns (indexes) that stay in place while the table scrolls; left out, nothing is pinned and the whole table
|
|
21
|
+
* scrolls. Only a table that is wider than the pane has pinned columns, and then they are drawn first, in the order they have in the
|
|
22
|
+
* table (`order` says which column is where). They take at most half the pane (`pinBudget`): they are considered in the order
|
|
23
|
+
* listed, and one that does not fit is skipped. It is never squeezed: a pinned column that lost text could not be scrolled to.
|
|
24
|
+
* `frozen` is their width with their borders, 0 when nothing is pinned.
|
|
25
|
+
*/
|
|
26
|
+
export interface TableBlock {
|
|
27
|
+
block: Block;
|
|
28
|
+
anchors: TableAnchors;
|
|
29
|
+
edges: number[];
|
|
30
|
+
frozen: number;
|
|
31
|
+
order: number[];
|
|
32
|
+
/** per body row and drawn column: the cell was cut to fit (it ends in an ellipsis). Only a table that fits by squeezing cuts anything. */
|
|
33
|
+
cut: boolean[][];
|
|
34
|
+
/** per drawn column: the header was cut */
|
|
35
|
+
headCut: boolean[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function tableBlock(columns: string[], rows: Array<Array<string | number>>, width: number, opts: { minCol?: number; pin?: number[] } = {}): TableBlock {
|
|
14
39
|
const n = Math.max(1, columns.length);
|
|
15
40
|
const str = (v: string | number) => String(v ?? "").replace(/\s+/g, " ").trim();
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const nat =
|
|
41
|
+
const data0 = rows.map((r) => columns.map((_, i) => str(r[i] ?? "")));
|
|
42
|
+
const heads0 = columns.map(str);
|
|
43
|
+
const numeric0 = heads0.map((_, i) => data0.length > 0 && data0.every((r) => r[i] === "" || NUMERIC.test(r[i])));
|
|
44
|
+
const nat = heads0.map((h, i) => Math.max(textWidth(h), ...data0.map((r) => textWidth(r[i])), 1));
|
|
20
45
|
const avail = Math.max(n, width - (3 * n + 1));
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
sum
|
|
46
|
+
const sum = (a: number[]) => a.reduce((x, y) => x + y, 0);
|
|
47
|
+
const minCol = opts.minCol ?? 3;
|
|
48
|
+
const floors = nat.map((x) => Math.min(x, minCol));
|
|
49
|
+
let w = nat.slice();
|
|
50
|
+
let order = heads0.map((_, i) => i);
|
|
51
|
+
let frozen = 0;
|
|
52
|
+
if (sum(w) > avail) {
|
|
53
|
+
if (sum(floors) <= avail || opts.minCol === undefined) {
|
|
54
|
+
// shrink the widest column, one character at a time, never below its floor
|
|
55
|
+
let total = sum(w);
|
|
56
|
+
while (total > avail) {
|
|
57
|
+
let k = -1;
|
|
58
|
+
for (let i = 0; i < n; i++) if (w[i] > floors[i] && (k < 0 || w[i] > w[k])) k = i;
|
|
59
|
+
if (k < 0) break;
|
|
60
|
+
w[k]--;
|
|
61
|
+
total--;
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
// the floors do not fit either: every column at its natural width, the table is wider than the pane and scrolls, and what
|
|
65
|
+
// the model asked to pin is pinned if there is room for it
|
|
66
|
+
w = nat.slice();
|
|
67
|
+
const budget = pinBudget(width);
|
|
68
|
+
const chosen: number[] = [];
|
|
69
|
+
let used = 1; // the left border
|
|
70
|
+
for (const i of opts.pin ?? []) {
|
|
71
|
+
if (!Number.isInteger(i) || i < 0 || i >= n || chosen.includes(i)) continue;
|
|
72
|
+
if (used + w[i] + 3 > budget) continue; // not squeezed: text that is cut here could never be scrolled to
|
|
73
|
+
used += w[i] + 3;
|
|
74
|
+
chosen.push(i);
|
|
75
|
+
}
|
|
76
|
+
if (chosen.length && chosen.length < n) {
|
|
77
|
+
chosen.sort((a, b) => a - b);
|
|
78
|
+
order = [...chosen, ...order.filter((i) => !chosen.includes(i))];
|
|
79
|
+
frozen = used;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
29
82
|
}
|
|
83
|
+
// from here on the columns are in the order they are drawn
|
|
84
|
+
const by = <T>(a: T[]): T[] => order.map((i) => a[i]);
|
|
85
|
+
const heads = by(heads0);
|
|
86
|
+
const data = data0.map(by);
|
|
87
|
+
const numeric = by(numeric0);
|
|
88
|
+
w = by(w);
|
|
30
89
|
const total = w.reduce((a, b) => a + b, 0) + 3 * n + 1;
|
|
31
90
|
const line = (l: string, m: string, r: string): Cell[] => {
|
|
32
91
|
const out: Cell[] = [cell(l, "table.border")];
|
|
@@ -66,5 +125,54 @@ export function tableBlock(columns: string[], rows: Array<Array<string | number>
|
|
|
66
125
|
rows: data.map((_, ri) => ({ x: 0, y: 3 + ri, w: total, h: 1 })),
|
|
67
126
|
cells: data.map((_, ri) => w.map((cw, i) => ({ x: colX[i], y: 3 + ri, w: cw, h: 1 }))),
|
|
68
127
|
};
|
|
69
|
-
return {
|
|
128
|
+
return {
|
|
129
|
+
block: fromRows(total, out.map((r) => padRow(r, total))),
|
|
130
|
+
anchors,
|
|
131
|
+
edges: colX.map((x) => x - 2),
|
|
132
|
+
frozen,
|
|
133
|
+
order,
|
|
134
|
+
cut: data.map((r) => r.map((v, i) => textWidth(v) > w[i])),
|
|
135
|
+
headCut: heads.map((h, i) => textWidth(h) > w[i]),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* How `panBlock` should scroll a table. With the first column frozen the window starts F cells after the offset, so an offset that
|
|
141
|
+
* puts a column border there is the border's position minus F (and the start, 0, stays a place to stop). Without that a step lands
|
|
142
|
+
* in the middle of a column.
|
|
143
|
+
*/
|
|
144
|
+
export function tablePan(t: { edges: number[]; frozen: number }, width: number): { edges: number[]; frozen: number } {
|
|
145
|
+
const F = frozenFor(t.frozen, width);
|
|
146
|
+
return { edges: F ? [0, ...t.edges.map((e) => e - F).filter((e) => e > 0)] : t.edges, frozen: t.frozen };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The columns a Table's `pin` names, as indexes: by header, the way the table draws it (spacing collapsed). Undefined when the
|
|
151
|
+
* model did not say (the first column is pinned then); a name that matches nothing is left out.
|
|
152
|
+
*/
|
|
153
|
+
export function pinIndexes(pin: unknown, columns: string[]): number[] | undefined {
|
|
154
|
+
if (!Array.isArray(pin)) return undefined;
|
|
155
|
+
const norm = (v: unknown) => String(v ?? "").replace(/\s+/g, " ").trim();
|
|
156
|
+
const heads = columns.map(norm);
|
|
157
|
+
return pin.map((name) => heads.indexOf(norm(name))).filter((i) => i >= 0);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The cells a table cut to fit, each with what it takes to read it whole: the anchor key a tooltip would use (`cell:3.Notes`,
|
|
162
|
+
* `col:Notes`), where it is drawn, its full text as it was written, and a short name for the hover line. For a Table and a
|
|
163
|
+
* Markdown table alike.
|
|
164
|
+
*/
|
|
165
|
+
export function cutCells(t: TableBlock, columns: string[], rows: Array<Array<string | number>>): Array<{ key: string; rect: Rect; full: string; label: string }> {
|
|
166
|
+
const out: Array<{ key: string; rect: Rect; full: string; label: string }> = [];
|
|
167
|
+
const head = columns.map((c) => String(c ?? ""));
|
|
168
|
+
t.headCut.forEach((cut, ci) => {
|
|
169
|
+
if (cut) out.push({ key: `col:${head[t.order[ci]]}`, rect: t.anchors.cols[ci], full: head[t.order[ci]].trim(), label: `column ${head[t.order[ci]].replace(/\s+/g, " ").trim()}` });
|
|
170
|
+
});
|
|
171
|
+
t.cut.forEach((row, ri) =>
|
|
172
|
+
row.forEach((cut, ci) => {
|
|
173
|
+
const col = t.order[ci];
|
|
174
|
+
if (cut) out.push({ key: `cell:${ri + 1}.${head[col]}`, rect: t.anchors.cells[ri][ci], full: String(rows[ri]?.[col] ?? "").trim(), label: `${head[col].replace(/\s+/g, " ").trim()}, row ${ri + 1}` });
|
|
175
|
+
}),
|
|
176
|
+
);
|
|
177
|
+
return out;
|
|
70
178
|
}
|
package/src/blocks/types.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Cell, Rect } from "../diagram/canvas.ts";
|
|
2
2
|
import type { El } from "../lang/library.ts";
|
|
3
|
+
import type { Answers, Choice } from "./choice.ts";
|
|
3
4
|
|
|
4
5
|
export interface Tip {
|
|
5
6
|
hint?: string;
|
|
@@ -9,12 +10,19 @@ export interface Tip {
|
|
|
9
10
|
tone?: string;
|
|
10
11
|
link?: string;
|
|
11
12
|
linkLabel?: string;
|
|
13
|
+
/** the whole text of a cell that was cut to fit the pane: the panel shows it as plain text, under whatever else the tip says */
|
|
14
|
+
full?: string;
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
export type TargetKind =
|
|
15
18
|
| "tab"
|
|
16
19
|
| "button"
|
|
20
|
+
/** one option of a Choice, or its Other row */
|
|
17
21
|
| "choice"
|
|
22
|
+
/** the Send answers button under a widget that has Choices */
|
|
23
|
+
| "send"
|
|
24
|
+
/** the Clear button next to it */
|
|
25
|
+
| "clear"
|
|
18
26
|
| "faq"
|
|
19
27
|
| "node"
|
|
20
28
|
| "copy"
|
|
@@ -27,7 +35,11 @@ export type TargetKind =
|
|
|
27
35
|
| "noteadd"
|
|
28
36
|
| "noteclear"
|
|
29
37
|
/** the footer chip that turns notes mode on and off */
|
|
30
|
-
| "notes"
|
|
38
|
+
| "notes"
|
|
39
|
+
/** a part of the pan bar under a block that is wider than its pane: the left arrow, the right arrow or the track */
|
|
40
|
+
| "pan"
|
|
41
|
+
/** the area of a clipped block, body and bar. Never reachable: the widget asks which one is under a wheel or a drag */
|
|
42
|
+
| "panarea";
|
|
31
43
|
|
|
32
44
|
/** What a note says about the element it is attached to. All words are already cleaned for a message the user will send. */
|
|
33
45
|
export interface ItemInfo {
|
|
@@ -68,6 +80,10 @@ export interface Target {
|
|
|
68
80
|
send?: string;
|
|
69
81
|
openUrl?: string;
|
|
70
82
|
copy?: string;
|
|
83
|
+
/** for kind "send": what it sends. The message is built from the answers as they are when it is pressed, never from a picture that may be a frame old. */
|
|
84
|
+
bar?: { choices: Choice[]; title: string };
|
|
85
|
+
/** for kind "choice": the question and which row of it this is (an option index, or its Other row) */
|
|
86
|
+
choice?: { of: Choice; index: number | "other" };
|
|
71
87
|
/** extra cells (block coordinates) that belong to the target besides its rect, e.g. the wedge of a pie slice */
|
|
72
88
|
cells?: Array<[number, number]>;
|
|
73
89
|
/** for kind "item": what a note on it is about */
|
|
@@ -77,7 +93,11 @@ export interface Target {
|
|
|
77
93
|
/** for kind "image": where the picture goes and how big it is, in cells */
|
|
78
94
|
image?: { key: string; cols: number; rows: number };
|
|
79
95
|
/** a diagram that may be wider than its view: focusing this target should reveal it */
|
|
80
|
-
|
|
96
|
+
/** `min` is where the scrolling part begins when the first columns are frozen: what is left of it is behind them */
|
|
97
|
+
pan?: { key: string; x: number; w: number; view: number; total: number; min?: number };
|
|
98
|
+
/** for kind "pan": which part of which bar this is, and what it needs to move the offset */
|
|
99
|
+
/** `left`/`right` are the fine arrows (`step` cells), `pageleft`/`pageright` the page arrows (three quarters of the pane, to a column edge) */
|
|
100
|
+
scroll?: { key: string; part: "left" | "right" | "pageleft" | "pageright" | "track"; total: number; view: number; edges?: number[]; step?: number };
|
|
81
101
|
}
|
|
82
102
|
|
|
83
103
|
export interface Block {
|
|
@@ -89,7 +109,12 @@ export interface Block {
|
|
|
89
109
|
export interface WState {
|
|
90
110
|
tab: Record<string, number>;
|
|
91
111
|
faq: Record<string, boolean>;
|
|
92
|
-
|
|
112
|
+
/** what the user has staged for each Choice, by question key. Nothing is sent until Send answers. */
|
|
113
|
+
answers: Answers;
|
|
114
|
+
/** the answers have been sent: every Choice is locked */
|
|
115
|
+
sent: boolean;
|
|
116
|
+
/** sent, but pi has not said yet that it took the message (the lock is held; the bar says Sending) */
|
|
117
|
+
sending: boolean;
|
|
93
118
|
pan: Record<string, number>;
|
|
94
119
|
tip: string | null;
|
|
95
120
|
focus: string | null;
|
|
@@ -102,7 +127,7 @@ export interface WState {
|
|
|
102
127
|
noteSeq: number;
|
|
103
128
|
}
|
|
104
129
|
|
|
105
|
-
export const newState = (): WState => ({ tab: {}, faq: {},
|
|
130
|
+
export const newState = (): WState => ({ tab: {}, faq: {}, answers: {}, sent: false, sending: false, pan: {}, tip: null, focus: null, hover: null, copied: null, notes: [], noteMode: false, noteSeq: 0 });
|
|
106
131
|
|
|
107
132
|
/** Supplied by the host when it can draw bitmaps. Without it, an Image is drawn as a labelled frame. */
|
|
108
133
|
export interface ImageService {
|
|
@@ -114,6 +139,8 @@ export interface ImageService {
|
|
|
114
139
|
|
|
115
140
|
export interface Services {
|
|
116
141
|
images?: ImageService;
|
|
142
|
+
/** the user's settings (src/pi/config.ts); the defaults when absent */
|
|
143
|
+
settings?: { tableMinCol: number; scrollStep: number };
|
|
117
144
|
}
|
|
118
145
|
|
|
119
146
|
export interface Ctx {
|
package/src/blocks/util.ts
CHANGED
|
@@ -133,6 +133,11 @@ export interface BoxOpts {
|
|
|
133
133
|
|
|
134
134
|
/** Wrap a block in a rounded box. `inner` is placed with padX columns of padding. Returns the boxed block. */
|
|
135
135
|
export function boxed(inner: Block, opts: BoxOpts): Block {
|
|
136
|
+
// A title that does not fit its border is not cut: it moves inside as a wrapped heading, above the content.
|
|
137
|
+
if (opts.title && textWidth(opts.title) > Math.max(1, inner.w + 2 * (opts.padX ?? 1) - 4 - (opts.tag ? textWidth(opts.tag) + 2 : 0))) {
|
|
138
|
+
const head = wrapText(opts.title, Math.max(1, inner.w), opts.titleRole ?? opts.role).map((r) => padRow(r, inner.w));
|
|
139
|
+
return boxed({ w: inner.w, rows: [...head, ...inner.rows], targets: shiftTargets(inner.targets, 0, head.length) }, { ...opts, title: undefined });
|
|
140
|
+
}
|
|
136
141
|
const padX = opts.padX ?? 1;
|
|
137
142
|
const w = inner.w + 2 + 2 * padX;
|
|
138
143
|
const role = opts.role;
|
package/src/diagram/canvas.ts
CHANGED
|
@@ -8,14 +8,21 @@ export function graphemes(s: string): string[] {
|
|
|
8
8
|
return seg ? Array.from(seg.segment(s), (x) => x.segment) : Array.from(s);
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
// The code points pi draws two cells wide: the generated table, which src/pi/widths.ts corrects against the running pi when the
|
|
12
|
+
// extension loads (the blocks where Unicode adds emoji). Flat [first, last] pairs, ascending.
|
|
13
|
+
let table: readonly number[] = WIDE;
|
|
14
|
+
export function setWide(t: readonly number[]): void {
|
|
15
|
+
table = t;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Whether `cp` is in the table of code points pi draws two cells wide (binary search over [first, last] pairs). */
|
|
12
19
|
function isWide(cp: number): boolean {
|
|
13
20
|
let lo = 0;
|
|
14
|
-
let hi =
|
|
21
|
+
let hi = table.length / 2 - 1;
|
|
15
22
|
while (lo <= hi) {
|
|
16
23
|
const mid = (lo + hi) >> 1;
|
|
17
|
-
if (cp <
|
|
18
|
-
else if (cp >
|
|
24
|
+
if (cp < table[mid * 2]) hi = mid - 1;
|
|
25
|
+
else if (cp > table[mid * 2 + 1]) lo = mid + 1;
|
|
19
26
|
else return true;
|
|
20
27
|
}
|
|
21
28
|
return false;
|
package/src/diagram/dag.ts
CHANGED
|
@@ -129,7 +129,8 @@ function attempt(norm: Norm, dir: "LR" | "TB", cap: number): DiagramResult {
|
|
|
129
129
|
return layeredDiagram("dag", { nodes, edges, dashReversed: true }, dir);
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
|
|
132
|
+
/** How far labels shrink to fit the pane. Below 14 characters a label is no longer a name, so past this the graph scrolls with whole labels. */
|
|
133
|
+
const CAPS = [28, 20, 14];
|
|
133
134
|
|
|
134
135
|
export function dag(nodesIn: unknown, edgesIn: unknown, opts: DagOptions = {}): DiagramResult {
|
|
135
136
|
return budgeted(() => dagLayout(nodesIn, edgesIn, opts));
|
|
@@ -159,8 +160,8 @@ function dagLayout(nodesIn: unknown, edgesIn: unknown, opts: DagOptions): Diagra
|
|
|
159
160
|
if (longest <= cap) break; // shrinking more changes nothing
|
|
160
161
|
}
|
|
161
162
|
if (!chosen) {
|
|
162
|
-
// nothing fits even with short labels: the view will
|
|
163
|
-
const cap =
|
|
163
|
+
// nothing fits even with short labels: the view will scroll, so scroll to whole labels instead of cutting them as well
|
|
164
|
+
const cap = maxLabel;
|
|
164
165
|
const tries = dirs.map((d) => attempt(norm, d, cap));
|
|
165
166
|
best = tries.reduce((a, b) => (b.width < a.width ? b : a));
|
|
166
167
|
}
|
package/src/diagram/erd.ts
CHANGED
|
@@ -146,7 +146,8 @@ function tableSpec(e: NormErd["entities"][number], fieldCap: number, selfRef: bo
|
|
|
146
146
|
};
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
|
|
149
|
+
/** How far an entity's lines shrink to fit the pane. Below 30 characters the types and flags are dropped, so past this the diagram scrolls. */
|
|
150
|
+
const CAPS = [60, 40, 30];
|
|
150
151
|
|
|
151
152
|
export function erd(entitiesIn: unknown, relationsIn: unknown, opts: ErdOptions = {}): DiagramResult {
|
|
152
153
|
return budgeted(() => erdLayout(entitiesIn, relationsIn, opts));
|
|
@@ -178,7 +179,8 @@ function erdLayout(entitiesIn: unknown, relationsIn: unknown, opts: ErdOptions):
|
|
|
178
179
|
if (chosen) break;
|
|
179
180
|
}
|
|
180
181
|
if (!chosen) {
|
|
181
|
-
|
|
182
|
+
// it will scroll whatever the fields: keep them whole (the widest cap) rather than cut them as well
|
|
183
|
+
const nodes = norm.entities.map((e, i) => tableSpec(e, CAPS[0], norm.selfRefs.has(i)));
|
|
182
184
|
const tries = dirs.map((d) => layeredDiagram("erd", { nodes, edges, portSpacing: 4 }, d));
|
|
183
185
|
best = tries.reduce((a, b) => (b.width < a.width ? b : a));
|
|
184
186
|
}
|
package/src/diagram/infra.ts
CHANGED
|
@@ -402,11 +402,11 @@ function build(norm: NormInfra, dir: "LR" | "TB", caps: Caps): DiagramResult {
|
|
|
402
402
|
};
|
|
403
403
|
}
|
|
404
404
|
|
|
405
|
+
/** How far labels shrink to fit the pane. Past the last row the map scrolls with whole labels, not 8-character ones. */
|
|
405
406
|
const CAPS: Caps[] = [
|
|
406
407
|
{ label: 22, detail: 26, link: 18 },
|
|
407
408
|
{ label: 16, detail: 18, link: 12 },
|
|
408
409
|
{ label: 12, detail: 12, link: 9 },
|
|
409
|
-
{ label: 8, detail: 8, link: 6 },
|
|
410
410
|
];
|
|
411
411
|
|
|
412
412
|
export function infra(zonesIn: unknown, linksIn: unknown, opts: InfraOptions = {}): DiagramResult {
|
|
@@ -434,7 +434,8 @@ function infraLayout(zonesIn: unknown, linksIn: unknown, opts: InfraOptions): Di
|
|
|
434
434
|
if (chosen) break;
|
|
435
435
|
}
|
|
436
436
|
if (!chosen) {
|
|
437
|
-
|
|
437
|
+
// it will scroll whatever the labels: use the whole ones (the first, widest caps) rather than cut them as well
|
|
438
|
+
const tries = dirs.map((d) => build(norm, d, CAPS[0]));
|
|
438
439
|
best = tries.reduce((a, b) => (b.width < a.width ? b : a));
|
|
439
440
|
}
|
|
440
441
|
const res = chosen ?? best!;
|
package/src/diagram/palette.ts
CHANGED
|
@@ -112,7 +112,7 @@ export function createPalette(t: PaletteTokens): Palette {
|
|
|
112
112
|
f = compose(t.accent, t.bold);
|
|
113
113
|
break;
|
|
114
114
|
case "choice":
|
|
115
|
-
f = a === "q" ? t.bold : a === "picked" ? compose(t.accent, t.bold) : undefined;
|
|
115
|
+
f = a === "q" ? t.bold : a === "picked" ? compose(t.accent, t.bold) : a === "tag" || a === "other" ? t.dim : a === "locked" ? t.dim : a === "sent" ? t.good : undefined;
|
|
116
116
|
break;
|
|
117
117
|
case "tab":
|
|
118
118
|
f = a === "on" ? t.accent : a === "label" ? compose(t.accent, t.bold) : a === "off" ? t.dim : a === "rule" ? t.muted : undefined;
|
package/src/diagram/wide.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Generated by scripts/width-table.ts from pi's own width function: do not edit. Run `npm run width`
|
|
1
|
+
// Generated by scripts/width-table.ts from pi's own width function: do not edit. Run `npm run width` to refresh it (the extension corrects its emoji blocks from the running pi when it loads).
|
|
2
2
|
// The code points pi draws two cells wide, as inclusive [first, last] pairs in ascending order.
|
|
3
3
|
export const WIDE: readonly number[] = [
|
|
4
4
|
0x1100, 0x115e, 0x231a, 0x231b, 0x2329, 0x232a, 0x23e9, 0x23ec,
|
|
@@ -27,10 +27,9 @@ export const WIDE: readonly number[] = [
|
|
|
27
27
|
0x1f3e0, 0x1f3f0, 0x1f3f4, 0x1f3f4, 0x1f3f8, 0x1f43e, 0x1f440, 0x1f440,
|
|
28
28
|
0x1f442, 0x1f4fc, 0x1f4ff, 0x1f53d, 0x1f54b, 0x1f54e, 0x1f550, 0x1f567,
|
|
29
29
|
0x1f57a, 0x1f57a, 0x1f595, 0x1f596, 0x1f5a4, 0x1f5a4, 0x1f5fb, 0x1f64f,
|
|
30
|
-
0x1f680, 0x1f6c5, 0x1f6cc, 0x1f6cc, 0x1f6d0, 0x1f6d2, 0x1f6d5,
|
|
30
|
+
0x1f680, 0x1f6c5, 0x1f6cc, 0x1f6cc, 0x1f6d0, 0x1f6d2, 0x1f6d5, 0x1f6d9,
|
|
31
31
|
0x1f6dc, 0x1f6df, 0x1f6eb, 0x1f6ec, 0x1f6f4, 0x1f6fc, 0x1f7e0, 0x1f7eb,
|
|
32
32
|
0x1f7f0, 0x1f7f0, 0x1f90c, 0x1f93a, 0x1f93c, 0x1f945, 0x1f947, 0x1f9ff,
|
|
33
|
-
0x1fa70, 0x1fa7c, 0x1fa80,
|
|
34
|
-
|
|
35
|
-
0x30000, 0x3fffd,
|
|
33
|
+
0x1fa70, 0x1fa7c, 0x1fa80, 0x1fac6, 0x1fac8, 0x1fac8, 0x1facc, 0x1fadd,
|
|
34
|
+
0x1fadf, 0x1faeb, 0x1faef, 0x1fafa, 0x20000, 0x2fffd, 0x30000, 0x3fffd,
|
|
36
35
|
];
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Keep the committed table of two-cell characters (wide.ts), but let the running pi decide the blocks where Unicode keeps adding
|
|
2
|
+
// emoji. Pure: the probe is any function from a code point to its width in columns, so nothing here needs pi.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Where new two-cell characters appear with each Unicode release: the arrows, symbols and dingbats that got an emoji form
|
|
6
|
+
* (U+2190-2BFF) and the emoji blocks (U+1F000-1FFFF). Everything else the table covers (CJK, Hangul, fullwidth forms) is stable.
|
|
7
|
+
* About 6,800 code points: cheap to ask pi about when the extension loads.
|
|
8
|
+
*/
|
|
9
|
+
export const PROBE_SPANS: ReadonlyArray<readonly [number, number]> = [
|
|
10
|
+
[0x2190, 0x2bff],
|
|
11
|
+
[0x1f000, 0x1ffff],
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A table (flat [first, last] pairs, ascending, merged) that is `base` outside `spans` and what `probe` says inside them.
|
|
16
|
+
* `probe` is only ever asked about code points inside the spans.
|
|
17
|
+
*/
|
|
18
|
+
export function healWide(base: readonly number[], probe: (cp: number) => number, spans: ReadonlyArray<readonly [number, number]> = PROBE_SPANS): number[] {
|
|
19
|
+
// the base ranges with the spans cut out of them
|
|
20
|
+
const keep: Array<[number, number]> = [];
|
|
21
|
+
for (let i = 0; i < base.length; i += 2) {
|
|
22
|
+
let pieces: Array<[number, number]> = [[base[i], base[i + 1]]];
|
|
23
|
+
for (const [s, e] of spans) {
|
|
24
|
+
const next: Array<[number, number]> = [];
|
|
25
|
+
for (const [a, b] of pieces) {
|
|
26
|
+
if (b < s || a > e) next.push([a, b]);
|
|
27
|
+
else {
|
|
28
|
+
if (a < s) next.push([a, s - 1]);
|
|
29
|
+
if (b > e) next.push([e + 1, b]);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
pieces = next;
|
|
33
|
+
}
|
|
34
|
+
keep.push(...pieces);
|
|
35
|
+
}
|
|
36
|
+
// what the probe says about the spans
|
|
37
|
+
const asked: Array<[number, number]> = [];
|
|
38
|
+
for (const [s, e] of spans) {
|
|
39
|
+
let start = -1;
|
|
40
|
+
for (let cp = s; cp <= e; cp++) {
|
|
41
|
+
const wide = probe(cp) === 2;
|
|
42
|
+
if (wide && start < 0) start = cp;
|
|
43
|
+
else if (!wide && start >= 0) {
|
|
44
|
+
asked.push([start, cp - 1]);
|
|
45
|
+
start = -1;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (start >= 0) asked.push([start, e]);
|
|
49
|
+
}
|
|
50
|
+
const out: number[] = [];
|
|
51
|
+
for (const [a, b] of [...keep, ...asked].sort((x, y) => x[0] - y[0])) {
|
|
52
|
+
if (out.length && a <= out[out.length - 1] + 1) out[out.length - 1] = Math.max(out[out.length - 1], b);
|
|
53
|
+
else out.push(a, b);
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|