@textui/core 0.7.0 → 0.9.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/app/app.d.ts.map +1 -1
- package/dist/app/app.js +8 -1
- package/dist/core/i18n.d.ts +1 -1
- package/dist/core/i18n.d.ts.map +1 -1
- package/dist/core/i18n.js +2 -2
- package/dist/jsx/intrinsics.d.ts +23 -0
- package/dist/jsx/intrinsics.d.ts.map +1 -1
- package/dist/runtime/paint.js +12 -2
- package/dist/runtime/style.d.ts +17 -7
- package/dist/runtime/style.d.ts.map +1 -1
- package/dist/runtime/style.js +66 -6
- package/dist/themes/builtin.d.ts +4 -13
- package/dist/themes/builtin.d.ts.map +1 -1
- package/dist/themes/builtin.js +160 -48
- package/dist/themes/registry.d.ts.map +1 -1
- package/dist/themes/registry.js +26 -0
- package/dist/types/i18n.d.ts +9 -1
- package/dist/types/i18n.d.ts.map +1 -1
- package/dist/types/style.d.ts +53 -14
- package/dist/types/style.d.ts.map +1 -1
- package/dist/types/theme.d.ts +37 -3
- package/dist/types/theme.d.ts.map +1 -1
- package/dist/util/text.d.ts +9 -0
- package/dist/util/text.d.ts.map +1 -1
- package/dist/util/text.js +16 -0
- package/package.json +1 -1
- package/src/app/app.ts +8 -1
- package/src/core/i18n.ts +2 -2
- package/src/jsx/intrinsics.ts +25 -0
- package/src/runtime/paint.ts +11 -2
- package/src/runtime/style.ts +73 -9
- package/src/themes/builtin.ts +165 -49
- package/src/themes/registry.ts +23 -0
- package/src/types/i18n.ts +9 -1
- package/src/types/style.ts +85 -18
- package/src/types/theme.ts +37 -3
- package/src/util/text.ts +15 -0
package/dist/util/text.d.ts
CHANGED
|
@@ -45,6 +45,15 @@ export declare function sliceColumns(text: string, start: number, width: number)
|
|
|
45
45
|
*/
|
|
46
46
|
export declare function expandTabs(text: string, tabWidth?: number): string;
|
|
47
47
|
export type TruncateSide = 'end' | 'start' | 'middle';
|
|
48
|
+
/**
|
|
49
|
+
* Fit into `width` cells, marking the cut whether or not there was one.
|
|
50
|
+
*
|
|
51
|
+
* `truncate` marks only what it had to cut, which is right for a string that
|
|
52
|
+
* has to fit a cell. A paragraph stopped by the box rather than by its own
|
|
53
|
+
* length is the other case: its last visible row may fit exactly and still not
|
|
54
|
+
* be the end of the text, and it has to say so.
|
|
55
|
+
*/
|
|
56
|
+
export declare function markCut(text: string, width: number, ellipsis?: string): string;
|
|
48
57
|
/** Fit into `width` cells, marking the cut with `ellipsis`. */
|
|
49
58
|
export declare function truncate(text: string, width: number, ellipsis?: string, side?: TruncateSide): string;
|
|
50
59
|
/** Pad to exactly `width` cells. Over-long input is returned unchanged. */
|
package/dist/util/text.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../../src/util/text.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAelD;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAM7C;AAED;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAShD;AA8ED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAsBrD;AAsBD,2CAA2C;AAC3C,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAgBhD;AAED,+EAA+E;AAC/E,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAYhE;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAgC/E;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,SAAI,GAAG,MAAM,CAuB7D;AAED,MAAM,MAAM,YAAY,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAC;AAEtD,+DAA+D;AAC/D,wBAAgB,QAAQ,CACtB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,QAAQ,SAAM,EACd,IAAI,GAAE,YAAoB,GACzB,MAAM,CAkCR;AAED,2EAA2E;AAC3E,wBAAgB,KAAK,CACnB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,KAAK,GAAE,MAAM,GAAG,QAAQ,GAAG,OAAgB,EAC3C,IAAI,SAAM,GACT,MAAM,CAQR;AAED,kEAAkE;AAClE,wBAAgB,KAAK,CACnB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,KAAK,GAAE,MAAM,GAAG,QAAQ,GAAG,OAAgB,EAC3C,QAAQ,SAAM,GACb,MAAM,CAER;AAED,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAEhD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,QAAQ,GAAG,SAAS,GAAG,YAAY,GAAG,SAAS,CAOnF;AAED,iFAAiF;AACjF,wBAAgB,UAAU,CAAC,IAAI,EAAE,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAE/D;AAED,4EAA4E;AAC5E,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,GAAE,QAAiB,GAAG,MAAM,EAAE,CAuDvF;AAOD,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9C;AAgBD;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAG7C;AAED,0EAA0E;AAC1E,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAIjE"}
|
|
1
|
+
{"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../../src/util/text.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAelD;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAM7C;AAED;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAShD;AA8ED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAsBrD;AAsBD,2CAA2C;AAC3C,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAgBhD;AAED,+EAA+E;AAC/E,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAYhE;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAgC/E;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,SAAI,GAAG,MAAM,CAuB7D;AAED,MAAM,MAAM,YAAY,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAC;AAEtD;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,SAAM,GAAG,MAAM,CAK3E;AAED,+DAA+D;AAC/D,wBAAgB,QAAQ,CACtB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,QAAQ,SAAM,EACd,IAAI,GAAE,YAAoB,GACzB,MAAM,CAkCR;AAED,2EAA2E;AAC3E,wBAAgB,KAAK,CACnB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,KAAK,GAAE,MAAM,GAAG,QAAQ,GAAG,OAAgB,EAC3C,IAAI,SAAM,GACT,MAAM,CAQR;AAED,kEAAkE;AAClE,wBAAgB,KAAK,CACnB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,KAAK,GAAE,MAAM,GAAG,QAAQ,GAAG,OAAgB,EAC3C,QAAQ,SAAM,GACb,MAAM,CAER;AAED,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAEhD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,QAAQ,GAAG,SAAS,GAAG,YAAY,GAAG,SAAS,CAOnF;AAED,iFAAiF;AACjF,wBAAgB,UAAU,CAAC,IAAI,EAAE,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAE/D;AAED,4EAA4E;AAC5E,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,GAAE,QAAiB,GAAG,MAAM,EAAE,CAuDvF;AAOD,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9C;AAgBD;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAG7C;AAED,0EAA0E;AAC1E,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAIjE"}
|
package/dist/util/text.js
CHANGED
|
@@ -280,6 +280,22 @@ export function expandTabs(text, tabWidth = 4) {
|
|
|
280
280
|
})
|
|
281
281
|
.join('\n');
|
|
282
282
|
}
|
|
283
|
+
/**
|
|
284
|
+
* Fit into `width` cells, marking the cut whether or not there was one.
|
|
285
|
+
*
|
|
286
|
+
* `truncate` marks only what it had to cut, which is right for a string that
|
|
287
|
+
* has to fit a cell. A paragraph stopped by the box rather than by its own
|
|
288
|
+
* length is the other case: its last visible row may fit exactly and still not
|
|
289
|
+
* be the end of the text, and it has to say so.
|
|
290
|
+
*/
|
|
291
|
+
export function markCut(text, width, ellipsis = '…') {
|
|
292
|
+
if (width <= 0)
|
|
293
|
+
return '';
|
|
294
|
+
const ew = stringWidth(ellipsis);
|
|
295
|
+
if (width <= ew)
|
|
296
|
+
return sliceByWidth(ellipsis, width);
|
|
297
|
+
return sliceByWidth(text, width - ew).trimEnd() + ellipsis;
|
|
298
|
+
}
|
|
283
299
|
/** Fit into `width` cells, marking the cut with `ellipsis`. */
|
|
284
300
|
export function truncate(text, width, ellipsis = '…', side = 'end') {
|
|
285
301
|
const total = stringWidth(text);
|
package/package.json
CHANGED
package/src/app/app.ts
CHANGED
|
@@ -752,8 +752,15 @@ export class App implements TextUIApp {
|
|
|
752
752
|
private stateOf(instance: Instance): InteractionState {
|
|
753
753
|
const id = typeof instance.props.id === 'string' ? instance.props.id : instance.id;
|
|
754
754
|
const focusedId = this.focus.focused();
|
|
755
|
+
// A node may state its own focus, and a row has to: the list holds the
|
|
756
|
+
// keyboard, the row is where it lands. Left to the id comparison every
|
|
757
|
+
// row in every list reads as unfocused, so "the current row" and "the
|
|
758
|
+
// current row you can type at" would be the same fill.
|
|
759
|
+
const stated = instance.props.focused;
|
|
755
760
|
return {
|
|
756
|
-
focused:
|
|
761
|
+
focused: typeof stated === 'boolean'
|
|
762
|
+
? stated
|
|
763
|
+
: focusedId === id || focusedId === `${instance.id}:focus`,
|
|
757
764
|
hovered: this.hovered === id || this.hoveredChain.has(instance),
|
|
758
765
|
active: false,
|
|
759
766
|
selected: instance.props.selected === true,
|
package/src/core/i18n.ts
CHANGED
|
@@ -94,10 +94,10 @@ export class I18nRegistry implements I18n {
|
|
|
94
94
|
return new Intl.ListFormat(this.locale, options).format(items);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
plural(count: number, forms: Record<string, string>): string {
|
|
97
|
+
plural(count: number, forms: Record<string, string>, values?: Record<string, unknown>): string {
|
|
98
98
|
const rule = new Intl.PluralRules(this.locale).select(count);
|
|
99
99
|
const template = forms[rule] ?? forms.other ?? '';
|
|
100
|
-
return interpolate(template, { count });
|
|
100
|
+
return interpolate(template, { ...values, count });
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
onChange(fn: (locale: LocaleId) => void): Disposable {
|
package/src/jsx/intrinsics.ts
CHANGED
|
@@ -24,6 +24,31 @@ export interface BaseProps extends Style {
|
|
|
24
24
|
disabled?: boolean;
|
|
25
25
|
selected?: boolean;
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* The states this node is in, for `style` overlays and for a theme.
|
|
29
|
+
*
|
|
30
|
+
* `focused` is a tri-state on purpose. Left out, the runtime asks the focus
|
|
31
|
+
* manager - which is right for a control and wrong for a row: a list row does
|
|
32
|
+
* not hold the keyboard, the list does. A row that has to be told so, or the
|
|
33
|
+
* distinction between "this is the current row" and "this is the current row
|
|
34
|
+
* and you can type at it" cannot be drawn at all.
|
|
35
|
+
*/
|
|
36
|
+
focused?: boolean;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Which of the component's boxes a theme styles.
|
|
40
|
+
*
|
|
41
|
+
* A composite component draws plain `box` nodes, so without this its entry
|
|
42
|
+
* in a theme's `components` map is a key nothing reads - a list row is a
|
|
43
|
+
* `box`, and `components.List.selected` is what a theme author would reach
|
|
44
|
+
* for. The name has to be stated by the component that owns the box, which
|
|
45
|
+
* is the only place that knows what the row is part of.
|
|
46
|
+
*
|
|
47
|
+
* Omitted, the node is styled under its own host name, so `components.box`
|
|
48
|
+
* and a `variant` keep working exactly as they did.
|
|
49
|
+
*/
|
|
50
|
+
styleAs?: string;
|
|
51
|
+
|
|
27
52
|
/** Participates in tab order. Implied by an interactive role. */
|
|
28
53
|
focusable?: boolean;
|
|
29
54
|
/** The focus scope this node belongs to. */
|
package/src/runtime/paint.ts
CHANGED
|
@@ -12,7 +12,7 @@ import type { Buffer } from '../render/buffer.js';
|
|
|
12
12
|
import { COLOR_DEFAULT, mix, packColor, type PackedColor } from '../render/color.js';
|
|
13
13
|
import { rectIntersect } from '../types/geometry.js';
|
|
14
14
|
import {
|
|
15
|
-
graphemes, graphemeWidth, isAscii, sanitize, stringWidth, truncate,
|
|
15
|
+
graphemes, graphemeWidth, isAscii, markCut, sanitize, stringWidth, truncate,
|
|
16
16
|
truncateSideOf, wrapModeOf, wrapText,
|
|
17
17
|
} from '../util/text.js';
|
|
18
18
|
import {
|
|
@@ -751,7 +751,16 @@ function paintText(
|
|
|
751
751
|
|
|
752
752
|
for (let i = 0; i < raw.length && i < area.height; i++) {
|
|
753
753
|
let line = raw[i] as string;
|
|
754
|
-
|
|
754
|
+
// The last line the box has room for is cut when the text goes on past
|
|
755
|
+
// it, exactly as a line too wide for the box is. Without this a wrapped
|
|
756
|
+
// paragraph with more to say stopped mid-sentence and read as the whole
|
|
757
|
+
// of it, which is the failure `truncate` exists to prevent sideways.
|
|
758
|
+
const stopped = i === area.height - 1 && raw.length > area.height;
|
|
759
|
+
if (stopped) {
|
|
760
|
+
// The last row it has room for, with more to say: always marked, since
|
|
761
|
+
// the row itself may fit to the cell.
|
|
762
|
+
line = truncateSide === false ? line : markCut(line, area.width, ellipsis);
|
|
763
|
+
} else if (stringWidth(line) > area.width) {
|
|
755
764
|
line = truncateSide === false
|
|
756
765
|
? line
|
|
757
766
|
: truncate(line, area.width, ellipsis, truncateSide ?? 'end');
|
package/src/runtime/style.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Style, StatefulStyle, StyleInput, BorderSpec, BorderStyle, StyleColor } from '../types/style.js';
|
|
1
|
+
import type { Style, StatefulStyle, StyleInput, BorderSpec, BorderColor, BorderStyle, StyleColor, StateName } from '../types/style.js';
|
|
2
2
|
import type { ResolvedTheme } from '../types/theme.js';
|
|
3
3
|
import type { Edges } from '../types/geometry.js';
|
|
4
4
|
import type { Color } from '../types/cells.js';
|
|
@@ -31,6 +31,7 @@ export const STYLE_KEYS = new Set<string>([
|
|
|
31
31
|
export interface InteractionState {
|
|
32
32
|
focused: boolean;
|
|
33
33
|
hovered: boolean;
|
|
34
|
+
/** Pressed. Not a selection - that is `selected`. */
|
|
34
35
|
active: boolean;
|
|
35
36
|
selected: boolean;
|
|
36
37
|
disabled: boolean;
|
|
@@ -40,6 +41,37 @@ export const NO_INTERACTION: InteractionState = {
|
|
|
40
41
|
focused: false, hovered: false, active: false, selected: false, disabled: false,
|
|
41
42
|
};
|
|
42
43
|
|
|
44
|
+
/**
|
|
45
|
+
* The states, least to most specific - the order the last one wins.
|
|
46
|
+
*
|
|
47
|
+
* `flattenStyleInput` merges a `style` prop in this order and so does a
|
|
48
|
+
* theme's per-component map, which is the only way the two can be made to
|
|
49
|
+
* agree: a theme states `focus` over `selected` and gets the same answer
|
|
50
|
+
* whether the state came from the node or from the theme. `hovered` is the
|
|
51
|
+
* state; `hover` is the name it wears, which is why this is the one place
|
|
52
|
+
* that has to know the difference.
|
|
53
|
+
*/
|
|
54
|
+
const STATE_ORDER: readonly (keyof InteractionState)[] = [
|
|
55
|
+
'selected', 'hovered', 'active', 'focused', 'disabled',
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const STATE_VARIANT: Record<keyof InteractionState, StateName> = {
|
|
59
|
+
selected: 'selected',
|
|
60
|
+
hovered: 'hover',
|
|
61
|
+
active: 'active',
|
|
62
|
+
focused: 'focus',
|
|
63
|
+
disabled: 'disabled',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** The names of the states that are true, in the order the last one wins. */
|
|
67
|
+
export function stateVariants(state: InteractionState): StateName[] {
|
|
68
|
+
const out: StateName[] = [];
|
|
69
|
+
for (const key of STATE_ORDER) {
|
|
70
|
+
if (state[key]) out.push(STATE_VARIANT[key]);
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
|
|
43
75
|
function isStateful(value: Style | StatefulStyle): value is StatefulStyle {
|
|
44
76
|
return (
|
|
45
77
|
'base' in value || 'focus' in value || 'hover' in value ||
|
|
@@ -68,6 +100,7 @@ export function flattenStyleInput(input: StyleInput | undefined, state: Interact
|
|
|
68
100
|
|
|
69
101
|
// Order matters: selected loses to active, active loses to focus, and
|
|
70
102
|
// disabled wins over everything - a disabled control is not focusable.
|
|
103
|
+
// The same order `stateVariants` gives a theme, so the two cannot disagree.
|
|
71
104
|
return mergeStyles(
|
|
72
105
|
input.base,
|
|
73
106
|
state.selected ? input.selected : undefined,
|
|
@@ -94,13 +127,37 @@ export function resolveStyle(
|
|
|
94
127
|
defaultStyle: Style | undefined,
|
|
95
128
|
state: InteractionState,
|
|
96
129
|
): Style {
|
|
97
|
-
const
|
|
98
|
-
if (typeof props.variant === 'string')
|
|
99
|
-
if (typeof props.tone === 'string')
|
|
100
|
-
if (typeof props.size === 'string')
|
|
130
|
+
const qualifiers: string[] = [];
|
|
131
|
+
if (typeof props.variant === 'string') qualifiers.push(props.variant);
|
|
132
|
+
if (typeof props.tone === 'string') qualifiers.push(props.tone);
|
|
133
|
+
if (typeof props.size === 'string') qualifiers.push(props.size);
|
|
134
|
+
const variants: string[] = [...qualifiers];
|
|
135
|
+
|
|
136
|
+
// The states join last, so a theme's entry for one of them wins over the
|
|
137
|
+
// same name used as a variant - `List.focused` is the fill on the row the
|
|
138
|
+
// keyboard is on, and a `focused` variant means nothing else.
|
|
139
|
+
//
|
|
140
|
+
// Each state is also offered qualified by each of the props-driven names,
|
|
141
|
+
// immediately after the flat one, for the case the flat name cannot say:
|
|
142
|
+
// whether a state paints at all is sometimes a property of a variant rather
|
|
143
|
+
// than of the state. A solid tab is filled and an underline one is not, and
|
|
144
|
+
// `Tabs.selected` has to mean the pair for both while `Tabs.solid.selected`
|
|
145
|
+
// is the one that adds the fill. The qualified name merges last and the
|
|
146
|
+
// order between the states is unchanged, so `disabled` still wins over
|
|
147
|
+
// everything and `focus` still wins over `selected`.
|
|
148
|
+
for (const name of stateVariants(state)) {
|
|
149
|
+
variants.push(name);
|
|
150
|
+
for (const qualifier of qualifiers) variants.push(`${qualifier}.${name}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// `styleAs` is how a component says which of its boxes a theme styles. A
|
|
154
|
+
// list draws its rows as plain `box` nodes, so without it `components.List`
|
|
155
|
+
// would be a key nothing ever reads - which is what every `components` entry
|
|
156
|
+
// written for a composite component was until this.
|
|
157
|
+
const owner = typeof props.styleAs === 'string' ? props.styleAs : component;
|
|
101
158
|
|
|
102
159
|
return mergeStyles(
|
|
103
|
-
theme.styleFor(
|
|
160
|
+
theme.styleFor(owner, variants),
|
|
104
161
|
defaultStyle,
|
|
105
162
|
styleFromProps(props),
|
|
106
163
|
flattenStyleInput(props.style as StyleInput | undefined, state),
|
|
@@ -109,7 +166,13 @@ export function resolveStyle(
|
|
|
109
166
|
|
|
110
167
|
// ------------------------------------------------------------------ colour
|
|
111
168
|
|
|
112
|
-
/**
|
|
169
|
+
/**
|
|
170
|
+
* A token name, a literal colour, or nothing.
|
|
171
|
+
*
|
|
172
|
+
* Takes the whole union because it is the one place a colour is turned into
|
|
173
|
+
* a cell value: the narrowing happened at the field that named it, which is
|
|
174
|
+
* where the mistake is made and where the error belongs.
|
|
175
|
+
*/
|
|
113
176
|
export function resolveColor(
|
|
114
177
|
value: StyleColor | undefined,
|
|
115
178
|
theme: ResolvedTheme,
|
|
@@ -119,6 +182,7 @@ export function resolveColor(
|
|
|
119
182
|
return theme.color(value as string);
|
|
120
183
|
}
|
|
121
184
|
|
|
185
|
+
/** Pack a colour for a cell, at the caller's own channel. */
|
|
122
186
|
export function packStyleColor(
|
|
123
187
|
value: StyleColor | undefined,
|
|
124
188
|
theme: ResolvedTheme,
|
|
@@ -144,9 +208,9 @@ export function attrsFromStyle(style: Style): number {
|
|
|
144
208
|
export interface ResolvedBorder {
|
|
145
209
|
style: BorderStyle;
|
|
146
210
|
chars: BorderChars;
|
|
147
|
-
color:
|
|
211
|
+
color: BorderColor | undefined;
|
|
148
212
|
/** Per-edge overrides. Undefined here means "use `color`". */
|
|
149
|
-
colors: { top?:
|
|
213
|
+
colors: { top?: BorderColor; right?: BorderColor; bottom?: BorderColor; left?: BorderColor };
|
|
150
214
|
dim: boolean;
|
|
151
215
|
sides: { top: boolean; right: boolean; bottom: boolean; left: boolean };
|
|
152
216
|
edges: Edges;
|
package/src/themes/builtin.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Style } from '../types/style.js';
|
|
1
2
|
import type { ThemeDefinition } from '../types/theme.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -10,6 +11,74 @@ import type { ThemeDefinition } from '../types/theme.js';
|
|
|
10
11
|
* difference lives here, in border style and density, not in the catalog.
|
|
11
12
|
*/
|
|
12
13
|
|
|
14
|
+
/**
|
|
15
|
+
* A selection, in the two states it has.
|
|
16
|
+
*
|
|
17
|
+
* `selected` is "this is the current one" and `focus` is the same row while
|
|
18
|
+
* the component holds the keyboard, so a theme states the bright fill once and
|
|
19
|
+
* the dimmer one for everything the reader has walked away from. They were one
|
|
20
|
+
* name once - `active` meant both this and pressed - and a word that means two
|
|
21
|
+
* things gets filled with either.
|
|
22
|
+
*
|
|
23
|
+
* Both name tokens rather than colours, which is the point of the whole
|
|
24
|
+
* arrangement: retint the palette and every component that has not said
|
|
25
|
+
* otherwise moves with it, while a theme that states `components.List.focused`
|
|
26
|
+
* moves the list and leaves the table where it was.
|
|
27
|
+
*/
|
|
28
|
+
const SELECTION_FOCUSED: Style = { bg: 'selected', fg: 'onSelected', bold: true };
|
|
29
|
+
const SELECTION_UNFOCUSED: Style = { bg: 'active', fg: 'onActive' };
|
|
30
|
+
const SELECTION_HOVER: Style = { bg: 'hover', fg: 'onActive' };
|
|
31
|
+
// The other way to draw a selection: reverse video, which is legible whatever
|
|
32
|
+
// a theme's own two colours are. Left beside the three above while that is
|
|
33
|
+
// decided - nothing reads them yet.
|
|
34
|
+
// const SELECTION_DEFAULT: Style = { bg: 'default', fg: 'default', inverse: true };
|
|
35
|
+
// const SELECTION_DIMMED: Style = { bg: 'default', fg: 'default', inverse: true, dim: true };
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The state colours every built-in theme starts from.
|
|
39
|
+
*
|
|
40
|
+
* Stated once and spread into `dark` and `light` because a theme that
|
|
41
|
+
* extends either inherits it whole: the entries name tokens, so `console`
|
|
42
|
+
* keeps its own `selected` and gets its own fills, without restating a single
|
|
43
|
+
* component. That is also what makes them the *defaults* - a theme states
|
|
44
|
+
* `components` to differ, and says nothing here about what it does not.
|
|
45
|
+
*/
|
|
46
|
+
const STATE_STYLES: ThemeDefinition['components'] = {
|
|
47
|
+
List: { selected: SELECTION_UNFOCUSED, focus: SELECTION_FOCUSED },
|
|
48
|
+
Tree: { selected: SELECTION_UNFOCUSED, focus: SELECTION_FOCUSED },
|
|
49
|
+
Table: { selected: SELECTION_UNFOCUSED, focus: SELECTION_FOCUSED },
|
|
50
|
+
// The field's own selection, over the text it covers. Same pair, same
|
|
51
|
+
// reason: a selection left visible in an unfocused field says what is on
|
|
52
|
+
// the clipboard, and saying it as loudly as the live one would put two
|
|
53
|
+
// selections on the screen.
|
|
54
|
+
TextArea: { selected: SELECTION_UNFOCUSED, focus: SELECTION_FOCUSED },
|
|
55
|
+
// A marked line is a selection. The caret line is that selection plus the
|
|
56
|
+
// keyboard, so it takes the next fill up rather than a colour of its own.
|
|
57
|
+
CodeViewer: { selected: SELECTION_UNFOCUSED, focus: SELECTION_HOVER },
|
|
58
|
+
Menu: { base: { fg: 'text' }, selected: SELECTION_UNFOCUSED, focus: SELECTION_FOCUSED },
|
|
59
|
+
TextInput: { base: { fg: 'text' } },
|
|
60
|
+
// A tab is open, not selected. Dimming it when the strip does not have the
|
|
61
|
+
// keyboard would say no document is open, which is a different claim and a
|
|
62
|
+
// wrong one - so it has one state and no second.
|
|
63
|
+
//
|
|
64
|
+
// Whether a tab is *filled* is a property of the variant rather than of the
|
|
65
|
+
// state: a solid tab is, an underline one is not. So the pair is stated for
|
|
66
|
+
// the state and the fill only for the variant that has one.
|
|
67
|
+
Tabs: { selected: { fg: 'onSelected' }, 'solid.selected': SELECTION_FOCUSED },
|
|
68
|
+
// The chat rows take the same fills, and for the same reason. `ToolCallRow`
|
|
69
|
+
// and `ReasoningBlock` are selections in a transcript that the caller
|
|
70
|
+
// names, so they have `selected` and no `focus`; a `ComposerChip` lights up
|
|
71
|
+
// only while it holds the keyboard, so it has `focus` and no `selected`.
|
|
72
|
+
ToolCallRow: { selected: SELECTION_FOCUSED },
|
|
73
|
+
ReasoningBlock: { selected: SELECTION_FOCUSED },
|
|
74
|
+
ComposerChip: { focus: SELECTION_FOCUSED },
|
|
75
|
+
// A range in a document is a selection, drawn in reverse video so it keeps
|
|
76
|
+
// the colours the text under it was already drawn in - the syntax of a
|
|
77
|
+
// selection is the syntax of the code it covers - and shows whether or not
|
|
78
|
+
// the theme's `active` is a fill.
|
|
79
|
+
Editor: { selected: { inverse: true } },
|
|
80
|
+
};
|
|
81
|
+
|
|
13
82
|
export const DARK: ThemeDefinition = {
|
|
14
83
|
id: 'dark',
|
|
15
84
|
name: 'Dark',
|
|
@@ -25,9 +94,9 @@ export const DARK: ThemeDefinition = {
|
|
|
25
94
|
borderStrong: '#484f58',
|
|
26
95
|
borderSubtle: '#21262d',
|
|
27
96
|
divider: '#484f58',
|
|
28
|
-
text: '#
|
|
97
|
+
text: '#f3e6f1',
|
|
29
98
|
muted: '#8b949e',
|
|
30
|
-
subtle: '#
|
|
99
|
+
subtle: '#546f94',
|
|
31
100
|
inverted: '#0d1117',
|
|
32
101
|
accent: '#58a6ff',
|
|
33
102
|
primary: '#388bfd',
|
|
@@ -46,8 +115,10 @@ export const DARK: ThemeDefinition = {
|
|
|
46
115
|
onWarning: '#0d1117',
|
|
47
116
|
onDanger: '#0d1117',
|
|
48
117
|
hover: '#1f2937',
|
|
49
|
-
active: '
|
|
50
|
-
|
|
118
|
+
active: 'default',
|
|
119
|
+
onActive: '#3191ff',
|
|
120
|
+
selected: 'default',
|
|
121
|
+
onSelected: '#3191ff',
|
|
51
122
|
focus: '#58a6ff',
|
|
52
123
|
disabled: '#484f58',
|
|
53
124
|
scrim: '#010409',
|
|
@@ -55,6 +126,7 @@ export const DARK: ThemeDefinition = {
|
|
|
55
126
|
shadow: '#010409',
|
|
56
127
|
},
|
|
57
128
|
spacing: { none: 0, xs: 0, sm: 1, md: 1, lg: 2, xl: 3 },
|
|
129
|
+
components: { ...STATE_STYLES },
|
|
58
130
|
};
|
|
59
131
|
|
|
60
132
|
export const LIGHT: ThemeDefinition = {
|
|
@@ -93,8 +165,10 @@ export const LIGHT: ThemeDefinition = {
|
|
|
93
165
|
onWarning: '#ffffff',
|
|
94
166
|
onDanger: '#ffffff',
|
|
95
167
|
hover: '#eaeef2',
|
|
96
|
-
active: '
|
|
97
|
-
|
|
168
|
+
active: 'default',
|
|
169
|
+
onActive: '#0969da',
|
|
170
|
+
selected: 'default',
|
|
171
|
+
onSelected: '#0969da',
|
|
98
172
|
focus: '#0969da',
|
|
99
173
|
disabled: '#8c959f',
|
|
100
174
|
scrim: '#8c959f',
|
|
@@ -102,6 +176,7 @@ export const LIGHT: ThemeDefinition = {
|
|
|
102
176
|
shadow: '#d0d7de',
|
|
103
177
|
},
|
|
104
178
|
spacing: { none: 0, xs: 0, sm: 1, md: 1, lg: 2, xl: 3 },
|
|
179
|
+
components: { ...STATE_STYLES },
|
|
105
180
|
};
|
|
106
181
|
|
|
107
182
|
/** Dense, bordered, high contrast. Every region is a labelled box. */
|
|
@@ -127,11 +202,16 @@ export const CONSOLE: ThemeDefinition = {
|
|
|
127
202
|
//
|
|
128
203
|
// The two selection backgrounds are picked rather than derived: `selected`
|
|
129
204
|
// carries `inverted` text so it has to be light, and `active` carries
|
|
130
|
-
// `text` so it has to be dark. Same hue, opposite ends.
|
|
205
|
+
// `text` so it has to be dark. Same hue, opposite ends. `active` is what
|
|
206
|
+
// a selection looks like when the component does not have the keyboard -
|
|
207
|
+
// it is the dim end of the pair, not a pressed control.
|
|
131
208
|
primary: '#88c0d0',
|
|
132
209
|
info: '#88c0d0',
|
|
133
210
|
selected: '#6ba3b2',
|
|
134
211
|
active: '#1e3d47',
|
|
212
|
+
// Stated, because `dark` states its own for a selection with no fill.
|
|
213
|
+
onSelected: '#0a0e14',
|
|
214
|
+
onActive: '#d8dee9',
|
|
135
215
|
onAccent: '#0a0e14',
|
|
136
216
|
onPrimary: '#0a0e14',
|
|
137
217
|
onInfo: '#0a0e14',
|
|
@@ -172,6 +252,7 @@ export const PAPER: ThemeDefinition = {
|
|
|
172
252
|
warning: '#d29922',
|
|
173
253
|
danger: '#f85149',
|
|
174
254
|
info: '#58a6ff',
|
|
255
|
+
/* ---- */
|
|
175
256
|
onAccent: '#0d1117',
|
|
176
257
|
onDefault: '#0d1117',
|
|
177
258
|
onPrimary: '#0d1117',
|
|
@@ -181,17 +262,23 @@ export const PAPER: ThemeDefinition = {
|
|
|
181
262
|
onInfo: '#0d1117',
|
|
182
263
|
onWarning: '#0d1117',
|
|
183
264
|
onDanger: '#0d1117',
|
|
265
|
+
/* ---- */
|
|
184
266
|
hover: '#1f2937',
|
|
185
|
-
active: '
|
|
186
|
-
|
|
267
|
+
active: 'default',
|
|
268
|
+
onActive: '#3191ff',
|
|
269
|
+
selected: 'default',
|
|
270
|
+
onSelected: '#58a6ff',
|
|
187
271
|
focus: '#58a6ff',
|
|
188
272
|
disabled: '#484f58',
|
|
273
|
+
|
|
274
|
+
/* ---- */
|
|
189
275
|
scrim: '#010409',
|
|
190
276
|
cursor: 'default',
|
|
191
277
|
shadow: '#010409',
|
|
192
278
|
},
|
|
193
279
|
spacing: { none: 0, xs: 1, sm: 1, md: 2, lg: 3, xl: 4 },
|
|
194
280
|
components: {
|
|
281
|
+
...STATE_STYLES,
|
|
195
282
|
Panel: { base: { border: 'none', padding: [1, 2] } },
|
|
196
283
|
Button: { base: { padding: [0, 2] } },
|
|
197
284
|
},
|
|
@@ -224,6 +311,9 @@ export const PAPER_LIGHT: ThemeDefinition = {
|
|
|
224
311
|
hover: '#f7f1e7',
|
|
225
312
|
active: '#efe4d2',
|
|
226
313
|
selected: '#eadcc6',
|
|
314
|
+
// Stated, because `light` states its own for a selection with no fill.
|
|
315
|
+
onActive: '#2b2a27',
|
|
316
|
+
onSelected: '#2b2a27',
|
|
227
317
|
inverted: '#2b2a27',
|
|
228
318
|
focus: '#b4531f',
|
|
229
319
|
},
|
|
@@ -234,6 +324,49 @@ export const PAPER_LIGHT: ThemeDefinition = {
|
|
|
234
324
|
},
|
|
235
325
|
};
|
|
236
326
|
|
|
327
|
+
/**
|
|
328
|
+
* Paper, after dark.
|
|
329
|
+
*
|
|
330
|
+
* The same airy, borderless, warm-accented character as `paper` - the point of
|
|
331
|
+
* it is the restraint, not the brightness - with the ink and the page swapped.
|
|
332
|
+
* A warm dark rather than a blue one, so the two read as one family and a
|
|
333
|
+
* reader moving between them is not moving between two different products.
|
|
334
|
+
*/
|
|
335
|
+
export const PAPER_DARK: ThemeDefinition = {
|
|
336
|
+
id: 'paper-dark',
|
|
337
|
+
name: 'Paper Dark',
|
|
338
|
+
appearance: 'dark',
|
|
339
|
+
extends: 'dark',
|
|
340
|
+
border: 'none',
|
|
341
|
+
density: 'airy',
|
|
342
|
+
colors: {
|
|
343
|
+
canvas: '#1c1a17',
|
|
344
|
+
surface: '#1c1a17',
|
|
345
|
+
surfaceAlt: '#26231f',
|
|
346
|
+
border: '#3a352e',
|
|
347
|
+
borderSubtle: '#2a2621',
|
|
348
|
+
divider: 'default',
|
|
349
|
+
subtle: '#db8c4c',
|
|
350
|
+
text: '#e8e3d9',
|
|
351
|
+
muted: '#9a9287',
|
|
352
|
+
accent: '#e0873f',
|
|
353
|
+
primary: '#e0873f',
|
|
354
|
+
// The same restraint, the other way up.
|
|
355
|
+
hover: '#26231f',
|
|
356
|
+
active: 'default',
|
|
357
|
+
onActive: '#e0873f',
|
|
358
|
+
selected: 'default',
|
|
359
|
+
onSelected: '#e0873f',
|
|
360
|
+
inverted: '#f4efe6',
|
|
361
|
+
focus: '#e0873f',
|
|
362
|
+
},
|
|
363
|
+
spacing: { none: 0, xs: 1, sm: 1, md: 2, lg: 3, xl: 4 },
|
|
364
|
+
components: {
|
|
365
|
+
Panel: { base: { border: 'none', padding: [1, 2] } },
|
|
366
|
+
Button: { base: { padding: [0, 2] } },
|
|
367
|
+
},
|
|
368
|
+
};
|
|
369
|
+
|
|
237
370
|
/** Rounded chrome around a persistent frame. */
|
|
238
371
|
export const WORKBENCH: ThemeDefinition = {
|
|
239
372
|
id: 'workbench',
|
|
@@ -263,6 +396,10 @@ export const WORKBENCH: ThemeDefinition = {
|
|
|
263
396
|
hover: '#313244',
|
|
264
397
|
active: '#45475a',
|
|
265
398
|
selected: '#585b70',
|
|
399
|
+
// The selection is a mid grey, so the light text is what reads on it; the
|
|
400
|
+
// inherited `inverted` is darker than the fill and nearly disappears.
|
|
401
|
+
onSelected: '#cdd6f4',
|
|
402
|
+
onActive: '#cdd6f4',
|
|
266
403
|
focus: '#89b4fa',
|
|
267
404
|
scrim: '#11111b',
|
|
268
405
|
},
|
|
@@ -276,7 +413,12 @@ export const MONO: ThemeDefinition = {
|
|
|
276
413
|
id: 'mono',
|
|
277
414
|
name: 'Monochrome',
|
|
278
415
|
appearance: 'dark',
|
|
416
|
+
// The theme's own colours are all `default`; this is what makes that true of
|
|
417
|
+
// the ones a component states for itself, which is the difference between a
|
|
418
|
+
// palette that happens to be grey and a theme with no colour in it.
|
|
419
|
+
monochrome: true,
|
|
279
420
|
border: 'ascii',
|
|
421
|
+
// border: 'none',
|
|
280
422
|
cursor: 'underline',
|
|
281
423
|
// Chosen, not downgraded to: this theme is ascii on a terminal that could
|
|
282
424
|
// draw anything, so the rule has to say so too.
|
|
@@ -317,47 +459,21 @@ export const MONO: ThemeDefinition = {
|
|
|
317
459
|
cursor: 'default',
|
|
318
460
|
shadow: 'default',
|
|
319
461
|
},
|
|
320
|
-
};
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* Paper, after dark.
|
|
324
|
-
*
|
|
325
|
-
* The same airy, borderless, warm-accented character as `paper` - the point of
|
|
326
|
-
* it is the restraint, not the brightness - with the ink and the page swapped.
|
|
327
|
-
* A warm dark rather than a blue one, so the two read as one family and a
|
|
328
|
-
* reader moving between them is not moving between two different products.
|
|
329
|
-
*/
|
|
330
|
-
export const PAPER_DARK: ThemeDefinition = {
|
|
331
|
-
id: 'paper-dark',
|
|
332
|
-
name: 'Paper Dark',
|
|
333
|
-
appearance: 'dark',
|
|
334
|
-
extends: 'dark',
|
|
335
|
-
border: 'none',
|
|
336
|
-
density: 'airy',
|
|
337
|
-
colors: {
|
|
338
|
-
canvas: '#1c1a17',
|
|
339
|
-
surface: '#1c1a17',
|
|
340
|
-
surfaceAlt: '#26231f',
|
|
341
|
-
border: '#3a352e',
|
|
342
|
-
borderSubtle: '#2a2621',
|
|
343
|
-
divider: 'default',
|
|
344
|
-
subtle: '#db8c4c',
|
|
345
|
-
text: '#e8e3d9',
|
|
346
|
-
muted: '#9a9287',
|
|
347
|
-
accent: '#e0873f',
|
|
348
|
-
primary: '#e0873f',
|
|
349
|
-
// The same restraint, the other way up.
|
|
350
|
-
hover: '#26231f',
|
|
351
|
-
active: '#332e27',
|
|
352
|
-
selected: '#3d362c',
|
|
353
|
-
inverted: '#f4efe6',
|
|
354
|
-
focus: '#e0873f',
|
|
355
|
-
},
|
|
356
|
-
spacing: { none: 0, xs: 1, sm: 1, md: 2, lg: 3, xl: 4 },
|
|
357
462
|
components: {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
463
|
+
List: { selected: { bold: true }, focus: { bold: true } },
|
|
464
|
+
Tree: { selected: { bold: true }, focus: { bold: true } },
|
|
465
|
+
Table: { selected: { bold: true }, focus: { bold: true } },
|
|
466
|
+
TextArea: { selected: { bold: true }, focus: { bold: true } },
|
|
467
|
+
CodeViewer: { selected: { bold: true }, focus: { bold: true } },
|
|
468
|
+
Menu: { selected: { bold: true }, focus: { bold: true } },
|
|
469
|
+
Tabs: { selected: { bold: true }, 'solid.selected': { bold: true } },
|
|
470
|
+
ToolCallRow: { selected: { bold: true } },
|
|
471
|
+
ReasoningBlock: { selected: { bold: true } },
|
|
472
|
+
ComposerChip: { focus: { bold: true } },
|
|
473
|
+
Editor: { selected: { bold: true } },
|
|
474
|
+
Panel: { base: { border: 'ascii', padding: [0, 1] } },
|
|
475
|
+
Button: { base: { padding: [0, 1] } },
|
|
476
|
+
}
|
|
361
477
|
};
|
|
362
478
|
|
|
363
479
|
export const BUILTIN_THEMES: ThemeDefinition[] = [
|