@textui/widgets 0.2.0 → 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/dist/data/feed.d.ts.map +1 -1
- package/dist/data/feed.js +60 -2
- package/dist/data/shared.d.ts +18 -1
- package/dist/data/shared.d.ts.map +1 -1
- package/dist/data/shared.js +12 -2
- package/dist/shells/workbench-shell.d.ts.map +1 -1
- package/dist/shells/workbench-shell.js +6 -0
- package/package.json +2 -2
- package/src/data/feed.ts +58 -2
- package/src/data/shared.ts +25 -3
- package/src/shells/workbench-shell.ts +6 -0
package/dist/data/feed.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"feed.d.ts","sourceRoot":"","sources":["../../src/data/feed.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAiB7C,MAAM,WAAW,SAAU,SAAQ,QAAQ;IACzC,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,cAAc,CAAC,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;IACvC;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,UAAU,CAAC,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IAChC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,qDAAqD;IACrD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,IAAI,
|
|
1
|
+
{"version":3,"file":"feed.d.ts","sourceRoot":"","sources":["../../src/data/feed.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAiB7C,MAAM,WAAW,SAAU,SAAQ,QAAQ;IACzC,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,cAAc,CAAC,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;IACvC;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,UAAU,CAAC,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IAChC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,qDAAqD;IACrD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,IAAI,2DAsOf,CAAC"}
|
package/dist/data/feed.js
CHANGED
|
@@ -28,6 +28,20 @@ export const Feed = defineComponent('Feed', (props) => {
|
|
|
28
28
|
const measured = useMeasure();
|
|
29
29
|
const extent = useScrollExtent();
|
|
30
30
|
const heights = useRef([]);
|
|
31
|
+
/**
|
|
32
|
+
* The width those heights were measured at.
|
|
33
|
+
*
|
|
34
|
+
* A height is a statement about a width - what a paragraph wrapped to in a
|
|
35
|
+
* pane this wide - so a resize does not make them stale, it makes them
|
|
36
|
+
* about a layout that no longer exists. Kept and compared rather than
|
|
37
|
+
* ignored, because the alternative is a feed that scrolls to the wrong
|
|
38
|
+
* place for ever after the window is dragged.
|
|
39
|
+
*/
|
|
40
|
+
const heightsAt = useRef(-1);
|
|
41
|
+
/** One handler for every entry, so an entry's props do not change with it. */
|
|
42
|
+
const report = useRef((index, height) => {
|
|
43
|
+
heights.current[index] = height;
|
|
44
|
+
});
|
|
31
45
|
// The catalog's rule, and this has to follow it like every other viewport:
|
|
32
46
|
// given `flex`, a `height`, a `maxHeight` or a `basis`, the layout decided
|
|
33
47
|
// the size and the feed fills it and scrolls; given none of those, the
|
|
@@ -169,10 +183,54 @@ export const Feed = defineComponent('Feed', (props) => {
|
|
|
169
183
|
scrollTo(event.name === 'pageup' ? top - page : top + page);
|
|
170
184
|
return true;
|
|
171
185
|
}, { global: true, enabled: pageKeys === 'always' });
|
|
186
|
+
if (measured.width !== heightsAt.current) {
|
|
187
|
+
heightsAt.current = measured.width;
|
|
188
|
+
heights.current = [];
|
|
189
|
+
}
|
|
190
|
+
/*
|
|
191
|
+
* Which entries are far enough away not to be worth drawing.
|
|
192
|
+
*
|
|
193
|
+
* A feed lays out every entry it holds, and laying one out means wrapping
|
|
194
|
+
* its text - so a viewport thirty rows tall over a transcript of six
|
|
195
|
+
* hundred entries spent every frame measuring the five hundred and seventy
|
|
196
|
+
* nobody can see. That cost is paid per *frame*, not per change: a
|
|
197
|
+
* keystroke in a field elsewhere on the screen, a caret blinking, a
|
|
198
|
+
* spinner. It is why a long conversation is heavy to type into and a short
|
|
199
|
+
* one is not.
|
|
200
|
+
*
|
|
201
|
+
* An entry is skipped only once it has been measured, which is what keeps
|
|
202
|
+
* this honest: the first frame draws everything and learns every height,
|
|
203
|
+
* and from then on the ones outside the window are replaced by a box of
|
|
204
|
+
* exactly the height they had. Nothing is estimated, so the extent, the
|
|
205
|
+
* scrollbar and the position of every entry are what they would have been
|
|
206
|
+
* had all of them been drawn.
|
|
207
|
+
*
|
|
208
|
+
* A screenful either side, so scrolling and paging land on something that
|
|
209
|
+
* is already there rather than on a frame of blanks.
|
|
210
|
+
*/
|
|
211
|
+
const view = Math.max(1, measured.height);
|
|
212
|
+
const skipped = [];
|
|
213
|
+
{
|
|
214
|
+
let y = 0;
|
|
215
|
+
for (let i = 0; i < count; i++) {
|
|
216
|
+
const height = heights.current[i];
|
|
217
|
+
// Zero is not a height that has been learned, whatever wrote it: an
|
|
218
|
+
// entry standing in for itself at nothing tall is one that can never
|
|
219
|
+
// be drawn again to find out how tall it is.
|
|
220
|
+
if (height === undefined || height <= 0) {
|
|
221
|
+
skipped[i] = false;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
skipped[i] = fills && (y + height < top - view || y > top + view + view);
|
|
225
|
+
y += height;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
172
228
|
const drawn = entries.map((entry, i) => h(FeedEntry, {
|
|
173
229
|
key: i,
|
|
174
|
-
|
|
175
|
-
|
|
230
|
+
index: i,
|
|
231
|
+
onHeight: report.current,
|
|
232
|
+
...(skipped[i] ? { placeholder: true, height: heights.current[i] } : {}),
|
|
233
|
+
}, skipped[i] ? null : entry));
|
|
176
234
|
// Content-sized: draw everything and let the box grow. Clamping to a
|
|
177
235
|
// measurement here would clamp to how tall this happened to be last frame,
|
|
178
236
|
// which for a box sized *by* its own content is nothing at all - the entries
|
package/dist/data/shared.d.ts
CHANGED
|
@@ -4,9 +4,26 @@ import type { BoxProps } from '@textui/core';
|
|
|
4
4
|
*
|
|
5
5
|
* Through a ref rather than state, deliberately: a height arriving must not
|
|
6
6
|
* schedule a render, or every measurement would cause the next measurement.
|
|
7
|
+
*
|
|
8
|
+
* It reports its `index` to one shared handler rather than taking a closure
|
|
9
|
+
* that already knows it. A closure per entry is a new prop on every entry
|
|
10
|
+
* every time the feed itself re-renders, and a changed prop is a re-render -
|
|
11
|
+
* so a feed that redrew for its own reasons dragged every entry it holds
|
|
12
|
+
* along with it, however little any of them had changed.
|
|
7
13
|
*/
|
|
8
14
|
export declare const FeedEntry: (props: BoxProps & {
|
|
9
|
-
|
|
15
|
+
index: number;
|
|
16
|
+
/**
|
|
17
|
+
* Standing in for an entry that is not drawn, at the height it had.
|
|
18
|
+
*
|
|
19
|
+
* A stand-in must not answer the question it is an answer to. Its height is
|
|
20
|
+
* one the feed already knows - it is where it came from - and reporting it
|
|
21
|
+
* back is how a measurement gets to depend on itself: a round where a
|
|
22
|
+
* stand-in came out any shorter would teach the feed that it *was* shorter,
|
|
23
|
+
* and the next round would place the rest of them on top of each other.
|
|
24
|
+
*/
|
|
25
|
+
placeholder?: boolean;
|
|
26
|
+
onHeight(index: number, height: number): void;
|
|
10
27
|
}) => import("@textui/core").RenderOutput;
|
|
11
28
|
export declare const FeedScrollbar: (props: Record<string, never>) => import("@textui/core").RenderOutput;
|
|
12
29
|
//# sourceMappingURL=shared.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../src/data/shared.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAG7C
|
|
1
|
+
{"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../src/data/shared.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAG7C;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,SAAS;WACb,MAAM;IACb;;;;;;;;OAQG;kBACW,OAAO;oBACL,MAAM,UAAU,MAAM,GAAG,IAAI;yCAY9C,CAAC;AAEF,eAAO,MAAM,aAAa,uEAGxB,CAAC"}
|
package/dist/data/shared.js
CHANGED
|
@@ -4,11 +4,21 @@ import { defineComponent, h, useEffect, useMeasure, useTheme } from '@textui/cor
|
|
|
4
4
|
*
|
|
5
5
|
* Through a ref rather than state, deliberately: a height arriving must not
|
|
6
6
|
* schedule a render, or every measurement would cause the next measurement.
|
|
7
|
+
*
|
|
8
|
+
* It reports its `index` to one shared handler rather than taking a closure
|
|
9
|
+
* that already knows it. A closure per entry is a new prop on every entry
|
|
10
|
+
* every time the feed itself re-renders, and a changed prop is a re-render -
|
|
11
|
+
* so a feed that redrew for its own reasons dragged every entry it holds
|
|
12
|
+
* along with it, however little any of them had changed.
|
|
7
13
|
*/
|
|
8
14
|
export const FeedEntry = defineComponent('FeedEntry', (props) => {
|
|
9
|
-
const { onHeight, children, ...rest } = props;
|
|
15
|
+
const { index, placeholder, onHeight, children, ...rest } = props;
|
|
10
16
|
const measured = useMeasure();
|
|
11
|
-
useEffect(() => {
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (placeholder)
|
|
19
|
+
return;
|
|
20
|
+
onHeight(index, measured.height);
|
|
21
|
+
}, [measured.height, placeholder]);
|
|
12
22
|
return h('box', { direction: 'column', ...rest }, children);
|
|
13
23
|
});
|
|
14
24
|
export const FeedScrollbar = defineComponent('FeedScrollbar', () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workbench-shell.d.ts","sourceRoot":"","sources":["../../src/shells/workbench-shell.ts"],"names":[],"mappings":"AAMA;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,
|
|
1
|
+
{"version":3,"file":"workbench-shell.d.ts","sourceRoot":"","sources":["../../src/shells/workbench-shell.ts"],"names":[],"mappings":"AAMA;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,iFAmEzB,CAAC"}
|
|
@@ -30,6 +30,12 @@ export const WorkbenchShell = defineComponent('WorkbenchShell', (props) => {
|
|
|
30
30
|
}, h(SurfaceArea, { surface: 'header' }), h('box', { direction: 'row', flex: 1 }, h(SurfaceArea, { surface: 'rail' }), showSidebar
|
|
31
31
|
? h('box', {
|
|
32
32
|
width: 24,
|
|
33
|
+
// Rigid. Sideways, the layout shrinks a child with a plain `width`
|
|
34
|
+
// before it clips anything - which is how terminals have always
|
|
35
|
+
// narrowed, and wrong for chrome: an overflowing pane beside it
|
|
36
|
+
// crushed twenty-four columns of tree down to four. A sidebar is a
|
|
37
|
+
// decision about the window, not a column of the document.
|
|
38
|
+
shrink: 0,
|
|
33
39
|
border: { style: theme.border, sides: { right: true } },
|
|
34
40
|
direction: 'column',
|
|
35
41
|
padding: { left: 1 },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@textui/widgets",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "The TextUI component catalog - layout, display, controls, data, overlays and charts",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"terminal",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"test": "vitest run"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@textui/core": "^0.
|
|
49
|
+
"@textui/core": "^0.3.0"
|
|
50
50
|
},
|
|
51
51
|
"publishConfig": {
|
|
52
52
|
"access": "public"
|
package/src/data/feed.ts
CHANGED
|
@@ -85,6 +85,20 @@ export const Feed = defineComponent<FeedProps>('Feed', (props) => {
|
|
|
85
85
|
const measured = useMeasure();
|
|
86
86
|
const extent = useScrollExtent();
|
|
87
87
|
const heights = useRef<number[]>([]);
|
|
88
|
+
/**
|
|
89
|
+
* The width those heights were measured at.
|
|
90
|
+
*
|
|
91
|
+
* A height is a statement about a width - what a paragraph wrapped to in a
|
|
92
|
+
* pane this wide - so a resize does not make them stale, it makes them
|
|
93
|
+
* about a layout that no longer exists. Kept and compared rather than
|
|
94
|
+
* ignored, because the alternative is a feed that scrolls to the wrong
|
|
95
|
+
* place for ever after the window is dragged.
|
|
96
|
+
*/
|
|
97
|
+
const heightsAt = useRef(-1);
|
|
98
|
+
/** One handler for every entry, so an entry's props do not change with it. */
|
|
99
|
+
const report = useRef((index: number, height: number): void => {
|
|
100
|
+
heights.current[index] = height;
|
|
101
|
+
});
|
|
88
102
|
|
|
89
103
|
// The catalog's rule, and this has to follow it like every other viewport:
|
|
90
104
|
// given `flex`, a `height`, a `maxHeight` or a `basis`, the layout decided
|
|
@@ -209,10 +223,52 @@ export const Feed = defineComponent<FeedProps>('Feed', (props) => {
|
|
|
209
223
|
{ global: true, enabled: pageKeys === 'always' },
|
|
210
224
|
);
|
|
211
225
|
|
|
226
|
+
if (measured.width !== heightsAt.current) {
|
|
227
|
+
heightsAt.current = measured.width;
|
|
228
|
+
heights.current = [];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/*
|
|
232
|
+
* Which entries are far enough away not to be worth drawing.
|
|
233
|
+
*
|
|
234
|
+
* A feed lays out every entry it holds, and laying one out means wrapping
|
|
235
|
+
* its text - so a viewport thirty rows tall over a transcript of six
|
|
236
|
+
* hundred entries spent every frame measuring the five hundred and seventy
|
|
237
|
+
* nobody can see. That cost is paid per *frame*, not per change: a
|
|
238
|
+
* keystroke in a field elsewhere on the screen, a caret blinking, a
|
|
239
|
+
* spinner. It is why a long conversation is heavy to type into and a short
|
|
240
|
+
* one is not.
|
|
241
|
+
*
|
|
242
|
+
* An entry is skipped only once it has been measured, which is what keeps
|
|
243
|
+
* this honest: the first frame draws everything and learns every height,
|
|
244
|
+
* and from then on the ones outside the window are replaced by a box of
|
|
245
|
+
* exactly the height they had. Nothing is estimated, so the extent, the
|
|
246
|
+
* scrollbar and the position of every entry are what they would have been
|
|
247
|
+
* had all of them been drawn.
|
|
248
|
+
*
|
|
249
|
+
* A screenful either side, so scrolling and paging land on something that
|
|
250
|
+
* is already there rather than on a frame of blanks.
|
|
251
|
+
*/
|
|
252
|
+
const view = Math.max(1, measured.height);
|
|
253
|
+
const skipped: boolean[] = [];
|
|
254
|
+
{
|
|
255
|
+
let y = 0;
|
|
256
|
+
for (let i = 0; i < count; i++) {
|
|
257
|
+
const height = heights.current[i];
|
|
258
|
+
// Zero is not a height that has been learned, whatever wrote it: an
|
|
259
|
+
// entry standing in for itself at nothing tall is one that can never
|
|
260
|
+
// be drawn again to find out how tall it is.
|
|
261
|
+
if (height === undefined || height <= 0) { skipped[i] = false; continue; }
|
|
262
|
+
skipped[i] = fills && (y + height < top - view || y > top + view + view);
|
|
263
|
+
y += height;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
212
266
|
const drawn = entries.map((entry, i) => h(FeedEntry, {
|
|
213
267
|
key: i,
|
|
214
|
-
|
|
215
|
-
|
|
268
|
+
index: i,
|
|
269
|
+
onHeight: report.current,
|
|
270
|
+
...(skipped[i] ? { placeholder: true, height: heights.current[i] as number } : {}),
|
|
271
|
+
}, skipped[i] ? null : entry));
|
|
216
272
|
|
|
217
273
|
// Content-sized: draw everything and let the box grow. Clamping to a
|
|
218
274
|
// measurement here would clamp to how tall this happened to be last frame,
|
package/src/data/shared.ts
CHANGED
|
@@ -6,13 +6,35 @@ import { defineComponent, h, useEffect, useMeasure, useTheme } from '@textui/cor
|
|
|
6
6
|
*
|
|
7
7
|
* Through a ref rather than state, deliberately: a height arriving must not
|
|
8
8
|
* schedule a render, or every measurement would cause the next measurement.
|
|
9
|
+
*
|
|
10
|
+
* It reports its `index` to one shared handler rather than taking a closure
|
|
11
|
+
* that already knows it. A closure per entry is a new prop on every entry
|
|
12
|
+
* every time the feed itself re-renders, and a changed prop is a re-render -
|
|
13
|
+
* so a feed that redrew for its own reasons dragged every entry it holds
|
|
14
|
+
* along with it, however little any of them had changed.
|
|
9
15
|
*/
|
|
10
|
-
export const FeedEntry = defineComponent<BoxProps & {
|
|
16
|
+
export const FeedEntry = defineComponent<BoxProps & {
|
|
17
|
+
index: number;
|
|
18
|
+
/**
|
|
19
|
+
* Standing in for an entry that is not drawn, at the height it had.
|
|
20
|
+
*
|
|
21
|
+
* A stand-in must not answer the question it is an answer to. Its height is
|
|
22
|
+
* one the feed already knows - it is where it came from - and reporting it
|
|
23
|
+
* back is how a measurement gets to depend on itself: a round where a
|
|
24
|
+
* stand-in came out any shorter would teach the feed that it *was* shorter,
|
|
25
|
+
* and the next round would place the rest of them on top of each other.
|
|
26
|
+
*/
|
|
27
|
+
placeholder?: boolean;
|
|
28
|
+
onHeight(index: number, height: number): void;
|
|
29
|
+
}>(
|
|
11
30
|
'FeedEntry',
|
|
12
31
|
(props) => {
|
|
13
|
-
const { onHeight, children, ...rest } = props;
|
|
32
|
+
const { index, placeholder, onHeight, children, ...rest } = props;
|
|
14
33
|
const measured = useMeasure();
|
|
15
|
-
useEffect(() => {
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (placeholder) return;
|
|
36
|
+
onHeight(index, measured.height);
|
|
37
|
+
}, [measured.height, placeholder]);
|
|
16
38
|
return h('box', { direction: 'column', ...rest }, children);
|
|
17
39
|
},
|
|
18
40
|
);
|
|
@@ -39,6 +39,12 @@ export const WorkbenchShell = defineComponent<ShellProps>('WorkbenchShell', (pro
|
|
|
39
39
|
showSidebar
|
|
40
40
|
? h('box', {
|
|
41
41
|
width: 24,
|
|
42
|
+
// Rigid. Sideways, the layout shrinks a child with a plain `width`
|
|
43
|
+
// before it clips anything - which is how terminals have always
|
|
44
|
+
// narrowed, and wrong for chrome: an overflowing pane beside it
|
|
45
|
+
// crushed twenty-four columns of tree down to four. A sidebar is a
|
|
46
|
+
// decision about the window, not a column of the document.
|
|
47
|
+
shrink: 0,
|
|
42
48
|
border: { style: theme.border, sides: { right: true } },
|
|
43
49
|
direction: 'column',
|
|
44
50
|
padding: { left: 1 },
|