@textui/widgets 0.3.0 → 0.4.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.
@@ -1 +1 @@
1
- {"version":3,"file":"select.d.ts","sourceRoot":"","sources":["../../src/control/select.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAiB,YAAY,EAAE,MAAM,cAAc,CAAC;AAK1E,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAY,SAAQ,QAAQ;IAC3C,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8DAA8D;IAC9D,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,oCAAoC;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;;;;;;;;;;;;;OAmBG;IACH,IAAI,CAAC,EAAE,QAAQ,GAAG,UAAU,GAAG,OAAO,CAAC;CACxC;AAED,eAAO,MAAM,MAAM,sCAuLjB,CAAC"}
1
+ {"version":3,"file":"select.d.ts","sourceRoot":"","sources":["../../src/control/select.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAiB,YAAY,EAAE,MAAM,cAAc,CAAC;AAK1E,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAY,SAAQ,QAAQ;IAC3C,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8DAA8D;IAC9D,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,oCAAoC;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;;;;;;;;;;;;;OAmBG;IACH,IAAI,CAAC,EAAE,QAAQ,GAAG,UAAU,GAAG,OAAO,CAAC;CACxC;AAED,eAAO,MAAM,MAAM,sCAiMjB,CAAC"}
@@ -146,6 +146,16 @@ export const Select = defineComponent('Select', (props) => {
146
146
  direction: 'column',
147
147
  border: { style: theme.border, color: focus.focused ? 'focus' : 'border' },
148
148
  padding: [0, 1],
149
+ /*
150
+ * The terminal cursor, parked at the start of the value.
151
+ *
152
+ * There is nothing to type here, so this is not a caret - it is the
153
+ * strongest "you are here" a terminal has, and a control that gives it up
154
+ * on focus is one whose only remaining signal is the colour of its border.
155
+ * Tabbing from a text field used to look like the focus had stayed behind
156
+ * in the field, because the cursor had.
157
+ */
158
+ cursor: focus.focused ? 0 : undefined,
149
159
  ...rest,
150
160
  }, h('box', {
151
161
  direction: 'row',
@@ -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,2DAsOf,CAAC"}
1
+ {"version":3,"file":"feed.d.ts","sourceRoot":"","sources":["../../src/data/feed.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAkB7C,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,2DAmRf,CAAC"}
package/dist/data/feed.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { chorded, defineComponent, h, useFocus, useInput, useMeasure, useRef, useScrollExtent, useState, } from '@textui/core';
2
2
  import { sizedByLayout } from '../viewport.js';
3
- import { FeedEntry, FeedScrollbar } from './shared.js';
3
+ import { FeedEntry } from './shared.js';
4
+ import { ScrollThumb } from './scroll-thumb.js';
4
5
  /**
5
6
  * A viewport over entries of any height.
6
7
  *
@@ -225,12 +226,46 @@ export const Feed = defineComponent('Feed', (props) => {
225
226
  y += height;
226
227
  }
227
228
  }
228
- const drawn = entries.map((entry, i) => h(FeedEntry, {
229
- key: i,
230
- index: i,
231
- onHeight: report.current,
232
- ...(skipped[i] ? { placeholder: true, height: heights.current[i] } : {}),
233
- }, skipped[i] ? null : entry));
229
+ /*
230
+ * A run of skipped entries is one box, not one box each.
231
+ *
232
+ * Skipping the *contents* of an entry took the wrapping away, which was the
233
+ * expensive part, and left everything else: an entry standing in for itself
234
+ * is still a component to reconcile and still a box to measure, four times a
235
+ * frame, at every level of the tree above it. Six hundred entries with
236
+ * thirty on screen was five hundred and seventy of those, and they cost the
237
+ * same whether or not anything in them had changed.
238
+ *
239
+ * The window is contiguous, so there are at most two runs - everything above
240
+ * it and everything below - and each collapses to a single box of exactly
241
+ * the height its entries had. The entries that *are* drawn keep their own
242
+ * index as their key, so scrolling moves the boundary without disturbing the
243
+ * identity of anything still on screen.
244
+ */
245
+ const drawn = [];
246
+ let gap = 0;
247
+ let gapFrom = 0;
248
+ const closeGap = () => {
249
+ if (gap <= 0)
250
+ return;
251
+ drawn.push(h('box', { key: `gap:${String(gapFrom)}`, height: gap }));
252
+ gap = 0;
253
+ };
254
+ for (let i = 0; i < count; i++) {
255
+ if (skipped[i]) {
256
+ if (gap === 0)
257
+ gapFrom = i;
258
+ gap += heights.current[i];
259
+ continue;
260
+ }
261
+ closeGap();
262
+ drawn.push(h(FeedEntry, {
263
+ key: i,
264
+ index: i,
265
+ onHeight: report.current,
266
+ }, entries[i]));
267
+ }
268
+ closeGap();
234
269
  // Content-sized: draw everything and let the box grow. Clamping to a
235
270
  // measurement here would clamp to how tall this happened to be last frame,
236
271
  // which for a box sized *by* its own content is nothing at all - the entries
@@ -256,7 +291,20 @@ export const Feed = defineComponent('Feed', (props) => {
256
291
  return true;
257
292
  },
258
293
  }, h('box', { flex: 1, direction: 'column', scrollTop: top, overflow: 'scroll' }, ...drawn),
294
+ // `ScrollThumb`, the one every other viewport in the library draws. This
295
+ // one had its own: a column filled with the border glyph, taking no offset
296
+ // and saying nothing about where in the conversation you were or how much
297
+ // of it there is. Against the panel it sits inside, it read as a second
298
+ // border - and a bar that cannot be told from a frame is not a bar.
299
+ //
259
300
  // Only when there is somewhere to scroll. A track down the side of a feed
260
301
  // that fits is chrome that states something untrue.
261
- scrollbar && limit > 0 ? h(FeedScrollbar, {}) : null);
302
+ scrollbar && limit > 0
303
+ ? h(ScrollThumb, {
304
+ total: total || measured.height,
305
+ rows: measured.height,
306
+ offset: top,
307
+ focused: focus.focused,
308
+ })
309
+ : null);
262
310
  });
@@ -13,17 +13,6 @@ import type { BoxProps } from '@textui/core';
13
13
  */
14
14
  export declare const FeedEntry: (props: BoxProps & {
15
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
16
  onHeight(index: number, height: number): void;
27
17
  }) => import("@textui/core").RenderOutput;
28
- export declare const FeedScrollbar: (props: Record<string, never>) => import("@textui/core").RenderOutput;
29
18
  //# 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;;;;;;;;;;;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"}
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;oBACG,MAAM,UAAU,MAAM,GAAG,IAAI;yCAW9C,CAAC"}
@@ -1,4 +1,4 @@
1
- import { defineComponent, h, useEffect, useMeasure, useTheme } from '@textui/core';
1
+ import { defineComponent, h, useEffect, useMeasure } from '@textui/core';
2
2
  /**
3
3
  * An entry that reports how tall it turned out to be.
4
4
  *
@@ -12,16 +12,10 @@ import { defineComponent, h, useEffect, useMeasure, useTheme } from '@textui/cor
12
12
  * along with it, however little any of them had changed.
13
13
  */
14
14
  export const FeedEntry = defineComponent('FeedEntry', (props) => {
15
- const { index, placeholder, onHeight, children, ...rest } = props;
15
+ const { index, onHeight, children, ...rest } = props;
16
16
  const measured = useMeasure();
17
17
  useEffect(() => {
18
- if (placeholder)
19
- return;
20
18
  onHeight(index, measured.height);
21
- }, [measured.height, placeholder]);
19
+ }, [measured.height]);
22
20
  return h('box', { direction: 'column', ...rest }, children);
23
21
  });
24
- export const FeedScrollbar = defineComponent('FeedScrollbar', () => {
25
- const theme = useTheme();
26
- return h('box', { width: 1, fill: theme.borderChars().left, fg: 'borderSubtle' });
27
- });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@textui/widgets",
3
- "version": "0.3.0",
3
+ "version": "0.4.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.3.0"
49
+ "@textui/core": "^0.4.0"
50
50
  },
51
51
  "publishConfig": {
52
52
  "access": "public"
@@ -202,6 +202,16 @@ export const Select = defineComponent<SelectProps>('Select', (props) => {
202
202
  direction: 'column',
203
203
  border: { style: theme.border, color: focus.focused ? 'focus' : 'border' },
204
204
  padding: [0, 1],
205
+ /*
206
+ * The terminal cursor, parked at the start of the value.
207
+ *
208
+ * There is nothing to type here, so this is not a caret - it is the
209
+ * strongest "you are here" a terminal has, and a control that gives it up
210
+ * on focus is one whose only remaining signal is the colour of its border.
211
+ * Tabbing from a text field used to look like the focus had stayed behind
212
+ * in the field, because the cursor had.
213
+ */
214
+ cursor: focus.focused ? 0 : undefined,
205
215
  ...rest,
206
216
  },
207
217
  h('box', {
package/src/data/feed.ts CHANGED
@@ -11,7 +11,8 @@ import {
11
11
  useState,
12
12
  } from '@textui/core';
13
13
  import { sizedByLayout } from '../viewport.js';
14
- import { FeedEntry, FeedScrollbar } from './shared.js';
14
+ import { FeedEntry } from './shared.js';
15
+ import { ScrollThumb } from './scroll-thumb.js';
15
16
 
16
17
  // ---------------------------------------------------------------------- feed
17
18
 
@@ -263,12 +264,44 @@ export const Feed = defineComponent<FeedProps>('Feed', (props) => {
263
264
  y += height;
264
265
  }
265
266
  }
266
- const drawn = entries.map((entry, i) => h(FeedEntry, {
267
- key: i,
268
- index: i,
269
- onHeight: report.current,
270
- ...(skipped[i] ? { placeholder: true, height: heights.current[i] as number } : {}),
271
- }, skipped[i] ? null : entry));
267
+ /*
268
+ * A run of skipped entries is one box, not one box each.
269
+ *
270
+ * Skipping the *contents* of an entry took the wrapping away, which was the
271
+ * expensive part, and left everything else: an entry standing in for itself
272
+ * is still a component to reconcile and still a box to measure, four times a
273
+ * frame, at every level of the tree above it. Six hundred entries with
274
+ * thirty on screen was five hundred and seventy of those, and they cost the
275
+ * same whether or not anything in them had changed.
276
+ *
277
+ * The window is contiguous, so there are at most two runs - everything above
278
+ * it and everything below - and each collapses to a single box of exactly
279
+ * the height its entries had. The entries that *are* drawn keep their own
280
+ * index as their key, so scrolling moves the boundary without disturbing the
281
+ * identity of anything still on screen.
282
+ */
283
+ const drawn: unknown[] = [];
284
+ let gap = 0;
285
+ let gapFrom = 0;
286
+ const closeGap = (): void => {
287
+ if (gap <= 0) return;
288
+ drawn.push(h('box', { key: `gap:${String(gapFrom)}`, height: gap }));
289
+ gap = 0;
290
+ };
291
+ for (let i = 0; i < count; i++) {
292
+ if (skipped[i]) {
293
+ if (gap === 0) gapFrom = i;
294
+ gap += heights.current[i] as number;
295
+ continue;
296
+ }
297
+ closeGap();
298
+ drawn.push(h(FeedEntry, {
299
+ key: i,
300
+ index: i,
301
+ onHeight: report.current,
302
+ }, entries[i]));
303
+ }
304
+ closeGap();
272
305
 
273
306
  // Content-sized: draw everything and let the box grow. Clamping to a
274
307
  // measurement here would clamp to how tall this happened to be last frame,
@@ -296,8 +329,21 @@ export const Feed = defineComponent<FeedProps>('Feed', (props) => {
296
329
  },
297
330
  },
298
331
  h('box', { flex: 1, direction: 'column', scrollTop: top, overflow: 'scroll' }, ...drawn),
332
+ // `ScrollThumb`, the one every other viewport in the library draws. This
333
+ // one had its own: a column filled with the border glyph, taking no offset
334
+ // and saying nothing about where in the conversation you were or how much
335
+ // of it there is. Against the panel it sits inside, it read as a second
336
+ // border - and a bar that cannot be told from a frame is not a bar.
337
+ //
299
338
  // Only when there is somewhere to scroll. A track down the side of a feed
300
339
  // that fits is chrome that states something untrue.
301
- scrollbar && limit > 0 ? h(FeedScrollbar, {}) : null,
340
+ scrollbar && limit > 0
341
+ ? h(ScrollThumb, {
342
+ total: total || measured.height,
343
+ rows: measured.height,
344
+ offset: top,
345
+ focused: focus.focused,
346
+ })
347
+ : null,
302
348
  );
303
349
  });
@@ -1,5 +1,5 @@
1
1
  import type { BoxProps } from '@textui/core';
2
- import { defineComponent, h, useEffect, useMeasure, useTheme } from '@textui/core';
2
+ import { defineComponent, h, useEffect, useMeasure } from '@textui/core';
3
3
 
4
4
  /**
5
5
  * An entry that reports how tall it turned out to be.
@@ -15,31 +15,15 @@ import { defineComponent, h, useEffect, useMeasure, useTheme } from '@textui/cor
15
15
  */
16
16
  export const FeedEntry = defineComponent<BoxProps & {
17
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
18
  onHeight(index: number, height: number): void;
29
19
  }>(
30
20
  'FeedEntry',
31
21
  (props) => {
32
- const { index, placeholder, onHeight, children, ...rest } = props;
22
+ const { index, onHeight, children, ...rest } = props;
33
23
  const measured = useMeasure();
34
24
  useEffect(() => {
35
- if (placeholder) return;
36
25
  onHeight(index, measured.height);
37
- }, [measured.height, placeholder]);
26
+ }, [measured.height]);
38
27
  return h('box', { direction: 'column', ...rest }, children);
39
28
  },
40
29
  );
41
-
42
- export const FeedScrollbar = defineComponent<Record<string, never>>('FeedScrollbar', () => {
43
- const theme = useTheme();
44
- return h('box', { width: 1, fill: theme.borderChars().left, fg: 'borderSubtle' });
45
- });