@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/src/themes/registry.ts
CHANGED
|
@@ -24,6 +24,7 @@ const FALLBACK_COLORS: Record<ColorToken, Color> = {
|
|
|
24
24
|
onDefault: 'default', onMuted: 'default',
|
|
25
25
|
onAccent: 'default', onPrimary: 'default', onSecondary: 'default',
|
|
26
26
|
onSuccess: 'default', onWarning: 'default', onDanger: 'default', onInfo: 'default',
|
|
27
|
+
onSelected: 'default', onActive: 'default',
|
|
27
28
|
hover: 'default', active: 'default', selected: 'default', focus: 'default',
|
|
28
29
|
disabled: 'default', scrim: 'default', cursor: 'default', shadow: 'default',
|
|
29
30
|
};
|
|
@@ -125,6 +126,7 @@ export class Themes implements ThemeRegistry {
|
|
|
125
126
|
// opts in rather than out.
|
|
126
127
|
let tableRules: TableRules = 'header';
|
|
127
128
|
let density: Density = 'normal';
|
|
129
|
+
let monochrome = false;
|
|
128
130
|
const components: Record<string, Record<string, Style>> = {};
|
|
129
131
|
let syntaxOverrides: Partial<Record<SyntaxScope, StyleColor>> = {};
|
|
130
132
|
|
|
@@ -155,11 +157,27 @@ export class Themes implements ThemeRegistry {
|
|
|
155
157
|
if (def.cursor) cursorStyle = def.cursor;
|
|
156
158
|
if (def.tableRules) tableRules = def.tableRules;
|
|
157
159
|
if (def.density) density = def.density;
|
|
160
|
+
if (def.monochrome !== undefined) monochrome = def.monochrome;
|
|
158
161
|
for (const [name, variants] of Object.entries(def.components ?? {})) {
|
|
159
162
|
components[name] = { ...components[name], ...variants };
|
|
160
163
|
}
|
|
161
164
|
}
|
|
162
165
|
|
|
166
|
+
// What is written on a filled row, derived rather than restated.
|
|
167
|
+
//
|
|
168
|
+
// A selection background is only half a pair: `selected` carries
|
|
169
|
+
// `inverted` and `active` carries `text`. Stated as two independent
|
|
170
|
+
// colours, a theme that restates one half - `paper-light` restates
|
|
171
|
+
// `inverted`, `paper` leaves `text` as the terminal's - would take the
|
|
172
|
+
// other half from the theme it extends, or from the terminal, and a row
|
|
173
|
+
// could come out unreadable with nothing in the theme saying so.
|
|
174
|
+
//
|
|
175
|
+
// So the pair follows the theme's own values unless it says otherwise. A
|
|
176
|
+
// theme that wants a different colour on a fill states it, which is what
|
|
177
|
+
// `paper` does for `active`.
|
|
178
|
+
if (colors.onSelected === 'default') colors.onSelected = colors.inverted;
|
|
179
|
+
if (colors.onActive === 'default') colors.onActive = colors.text;
|
|
180
|
+
|
|
163
181
|
// A colourless terminal gets no colour, whatever the theme says.
|
|
164
182
|
if (caps.colorDepth === 0) {
|
|
165
183
|
for (const token of Object.keys(colors) as ColorToken[]) {
|
|
@@ -190,6 +208,7 @@ export class Themes implements ThemeRegistry {
|
|
|
190
208
|
id: leaf.id,
|
|
191
209
|
name: leaf.name,
|
|
192
210
|
appearance: leaf.appearance,
|
|
211
|
+
monochrome,
|
|
193
212
|
colors,
|
|
194
213
|
spacing,
|
|
195
214
|
glyphs,
|
|
@@ -202,6 +221,10 @@ export class Themes implements ThemeRegistry {
|
|
|
202
221
|
syntax,
|
|
203
222
|
|
|
204
223
|
color(token: string): Color {
|
|
224
|
+
// A monochrome theme has no colour to give, and that has to include the
|
|
225
|
+
// literal a component states for itself - otherwise the one theme whose
|
|
226
|
+
// whole point is no colour is the one that cannot promise it.
|
|
227
|
+
if (monochrome) return 'default';
|
|
205
228
|
if (token in colors) return colors[token as ColorToken] as Color;
|
|
206
229
|
// Not a token: a literal colour passed straight through.
|
|
207
230
|
return token as Color;
|
package/src/types/i18n.ts
CHANGED
|
@@ -23,6 +23,14 @@ export interface I18n {
|
|
|
23
23
|
date(value: Date | number, options?: Intl.DateTimeFormatOptions): string;
|
|
24
24
|
relative(value: number, unit: Intl.RelativeTimeFormatUnit): string;
|
|
25
25
|
list(items: string[], options?: Intl.ListFormatOptions): string;
|
|
26
|
-
|
|
26
|
+
/**
|
|
27
|
+
* The form the locale's plural rules pick for `count`, with `{count}` and
|
|
28
|
+
* anything the sentence needs around it.
|
|
29
|
+
*
|
|
30
|
+
* `values` carries what else the form names - a limit, a glyph, the total a
|
|
31
|
+
* count is out of - because a sentence that inflects a noun is the whole
|
|
32
|
+
* sentence and not a noun with a number in front of it.
|
|
33
|
+
*/
|
|
34
|
+
plural(count: number, forms: Record<string, string>, values?: Record<string, unknown>): string;
|
|
27
35
|
onChange(fn: (locale: LocaleId) => void): Disposable;
|
|
28
36
|
}
|
package/src/types/style.ts
CHANGED
|
@@ -2,23 +2,77 @@ import type { Color } from './cells.js';
|
|
|
2
2
|
import type { EdgeSpec } from './geometry.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Semantic theme tokens
|
|
6
|
-
*
|
|
7
|
-
* a
|
|
5
|
+
* Semantic theme tokens, split by the channel each one is written in.
|
|
6
|
+
*
|
|
7
|
+
* A component names a role, never a colour - which is what lets one catalog
|
|
8
|
+
* render the same under a light theme, a dark theme and a 16-colour ssh
|
|
9
|
+
* session. The role is not enough on its own, though: a token also has to be
|
|
10
|
+
* somewhere it can actually be drawn. `canvas` is a fill, `onAccent` is
|
|
11
|
+
* writing, `borderStrong` is a rule - and one union for all three is what let
|
|
12
|
+
* `fg: 'canvas'` and `border: 'onAccent'` compile, to be found by somebody
|
|
13
|
+
* looking at a frame.
|
|
14
|
+
*
|
|
15
|
+
* So each token is listed under the channels it may appear in, and `Style`
|
|
16
|
+
* takes the matching one. A token may appear in more than one - the tones are
|
|
17
|
+
* both a fill and a colour of their own - and a literal colour is the shared
|
|
18
|
+
* escape out of all three.
|
|
19
|
+
*
|
|
20
|
+
* A token missing from a list is a token that channel cannot use. Adding one
|
|
21
|
+
* is a decision about where it reads, not a convenience.
|
|
8
22
|
*/
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
23
|
+
|
|
24
|
+
/** Colours a cell's foreground may be. */
|
|
25
|
+
export type FgColorToken =
|
|
26
|
+
// A tone is a colour in its own right as well as a fill it provides.
|
|
13
27
|
| 'accent' | 'primary' | 'secondary'
|
|
14
28
|
| 'success' | 'warning' | 'danger' | 'info'
|
|
29
|
+
// The states are readable as text too: a disabled label, a focus-coloured
|
|
30
|
+
// border rendered as a caption.
|
|
31
|
+
| 'hover' | 'active' | 'selected' | 'focus' | 'disabled'
|
|
32
|
+
| 'text' | 'muted' | 'subtle' | 'inverted'
|
|
33
|
+
// A quiet rule is also the quietest text there is.
|
|
34
|
+
| 'borderSubtle'
|
|
35
|
+
// The caret is the one token that is honestly both: an underline caret is
|
|
36
|
+
// drawn in the foreground, and a block caret is the foreground swapped, so
|
|
37
|
+
// both halves are reached by naming it here.
|
|
38
|
+
| 'cursor'
|
|
15
39
|
| 'onDefault' | 'onMuted'
|
|
16
40
|
| 'onAccent' | 'onPrimary' | 'onSecondary'
|
|
17
41
|
| 'onSuccess' | 'onWarning' | 'onDanger' | 'onInfo'
|
|
42
|
+
| 'onSelected' | 'onActive';
|
|
43
|
+
|
|
44
|
+
/** Colours a cell's background may be. */
|
|
45
|
+
export type BgColorToken =
|
|
46
|
+
| 'canvas' | 'surface' | 'surfaceAlt' | 'overlay'
|
|
18
47
|
| 'hover' | 'active' | 'selected' | 'focus' | 'disabled'
|
|
19
|
-
| '
|
|
48
|
+
| 'accent' | 'primary' | 'secondary'
|
|
49
|
+
| 'success' | 'warning' | 'danger' | 'info'
|
|
50
|
+
// What is over everything, what the caret is, and what a shadow is.
|
|
51
|
+
| 'scrim' | 'cursor' | 'shadow';
|
|
52
|
+
|
|
53
|
+
/** Colours a rule may be. */
|
|
54
|
+
export type BorderColorToken =
|
|
55
|
+
| 'border' | 'borderStrong' | 'borderSubtle' | 'divider'
|
|
56
|
+
| 'accent' | 'primary' | 'secondary'
|
|
57
|
+
| 'success' | 'warning' | 'danger' | 'info'
|
|
58
|
+
| 'hover' | 'focus'
|
|
59
|
+
// A quiet rule and a quiet label are the same two colours often enough that
|
|
60
|
+
// a frame drawn in `muted` is a line somebody means, not a mistake. What is
|
|
61
|
+
// refused here is the loud end of the foreground list: a rule in `text` or
|
|
62
|
+
// in an `on*` token is writing used as structure.
|
|
63
|
+
| 'muted' | 'subtle';
|
|
64
|
+
|
|
65
|
+
/** Every token, for the places that take a colour without saying which. */
|
|
66
|
+
export type ColorToken = FgColorToken | BgColorToken | BorderColorToken;
|
|
20
67
|
|
|
21
|
-
/**
|
|
68
|
+
/** A foreground: an `FgColorToken`, or a literal colour. */
|
|
69
|
+
export type FgColor = FgColorToken | Color;
|
|
70
|
+
/** A background: a `BgColorToken`, or a literal colour. */
|
|
71
|
+
export type BgColor = BgColorToken | Color;
|
|
72
|
+
/** A rule: a `BorderColorToken`, or a literal colour. */
|
|
73
|
+
export type BorderColor = BorderColorToken | Color;
|
|
74
|
+
|
|
75
|
+
/** Anywhere a colour is accepted, any token and any literal are accepted too. */
|
|
22
76
|
export type StyleColor = ColorToken | Color;
|
|
23
77
|
|
|
24
78
|
export type Dimension = number | `${number}%` | 'auto';
|
|
@@ -86,17 +140,17 @@ export type BorderSides = {
|
|
|
86
140
|
|
|
87
141
|
/** A colour per edge. Unnamed edges fall back to the border's `color`. */
|
|
88
142
|
export type BorderColors = {
|
|
89
|
-
top?:
|
|
90
|
-
right?:
|
|
91
|
-
bottom?:
|
|
92
|
-
left?:
|
|
143
|
+
top?: BorderColor;
|
|
144
|
+
right?: BorderColor;
|
|
145
|
+
bottom?: BorderColor;
|
|
146
|
+
left?: BorderColor;
|
|
93
147
|
};
|
|
94
148
|
|
|
95
149
|
export type BorderSpec =
|
|
96
150
|
| BorderStyle
|
|
97
151
|
| {
|
|
98
152
|
style?: BorderStyle;
|
|
99
|
-
color?:
|
|
153
|
+
color?: BorderColor;
|
|
100
154
|
/**
|
|
101
155
|
* Per-edge colour, over `color`. A corner belongs to the edge that runs
|
|
102
156
|
* through it - the top rule owns both top corners - because a cell holds
|
|
@@ -143,7 +197,7 @@ export interface Style {
|
|
|
143
197
|
* but stays legible, rather than being replaced by a rectangle of nothing.
|
|
144
198
|
* `true` uses the theme's `scrim` token; a number sets the strength.
|
|
145
199
|
*/
|
|
146
|
-
scrim?: boolean |
|
|
200
|
+
scrim?: boolean | BgColor;
|
|
147
201
|
scrimStrength?: number;
|
|
148
202
|
|
|
149
203
|
// --- box ---
|
|
@@ -196,8 +250,8 @@ export interface Style {
|
|
|
196
250
|
overflowY?: Overflow;
|
|
197
251
|
|
|
198
252
|
// --- paint ---
|
|
199
|
-
fg?:
|
|
200
|
-
bg?:
|
|
253
|
+
fg?: FgColor;
|
|
254
|
+
bg?: BgColor;
|
|
201
255
|
bold?: boolean;
|
|
202
256
|
dim?: boolean;
|
|
203
257
|
italic?: boolean;
|
|
@@ -214,16 +268,29 @@ export interface Style {
|
|
|
214
268
|
fill?: string;
|
|
215
269
|
}
|
|
216
270
|
|
|
217
|
-
/**
|
|
271
|
+
/**
|
|
272
|
+
* Styles selected by interaction state. Merged over the base in this order.
|
|
273
|
+
*
|
|
274
|
+
* `selected` is "this is the current one"; `focus` is "and the keyboard is
|
|
275
|
+
* here", so a row that has both wears `focus` and a row that has only the
|
|
276
|
+
* first wears the dimmer `selected`. They are separate names because they
|
|
277
|
+
* were the same word once - `active` meant both the unfocused selection here
|
|
278
|
+
* and the pressed state in `InteractionState` - and a token that meant two
|
|
279
|
+
* things could be filled with either and looked wrong half the time.
|
|
280
|
+
*/
|
|
218
281
|
export interface StatefulStyle {
|
|
219
282
|
base?: Style;
|
|
220
283
|
focus?: Style;
|
|
221
284
|
hover?: Style;
|
|
285
|
+
/** Pressed. Never a selection. */
|
|
222
286
|
active?: Style;
|
|
223
287
|
selected?: Style;
|
|
224
288
|
disabled?: Style;
|
|
225
289
|
}
|
|
226
290
|
|
|
291
|
+
/** The states, in the order the last one wins. The shared vocabulary. */
|
|
292
|
+
export type StateName = 'selected' | 'hover' | 'active' | 'focus' | 'disabled';
|
|
293
|
+
|
|
227
294
|
export type StyleInput = Style | StatefulStyle | (Style | StatefulStyle | undefined | false)[];
|
|
228
295
|
|
|
229
296
|
/** Global semantic variants, available to every component that opts in. */
|
package/src/types/theme.ts
CHANGED
|
@@ -77,6 +77,16 @@ export interface ThemeDefinition {
|
|
|
77
77
|
id: string;
|
|
78
78
|
name: string;
|
|
79
79
|
appearance: 'light' | 'dark';
|
|
80
|
+
/**
|
|
81
|
+
* Whether this theme has any colour to give.
|
|
82
|
+
*
|
|
83
|
+
* A theme's palette is its own business, but a component can state a literal
|
|
84
|
+
* colour - an ink, a chart, a hand-picked hex - and that used to come through
|
|
85
|
+
* untouched, so `mono` painted a rainbow the moment a banner asked for one.
|
|
86
|
+
* Stated, every colour resolves to the terminal's own: tokens and literals
|
|
87
|
+
* alike, which is what a theme that says it has no colour has to mean.
|
|
88
|
+
*/
|
|
89
|
+
monochrome?: boolean;
|
|
80
90
|
/** Extend another registered theme; only the differences need stating. */
|
|
81
91
|
extends?: string;
|
|
82
92
|
colors: Partial<Record<ColorToken, Color>>;
|
|
@@ -105,7 +115,16 @@ export interface ThemeDefinition {
|
|
|
105
115
|
*/
|
|
106
116
|
tableRules?: TableRules;
|
|
107
117
|
density?: Density;
|
|
108
|
-
/**
|
|
118
|
+
/**
|
|
119
|
+
* Per-component style overrides, keyed by component name then variant or
|
|
120
|
+
* state.
|
|
121
|
+
*
|
|
122
|
+
* A composite component has to name which of its boxes this is - a list row
|
|
123
|
+
* is a `box` node, and `List` is what a theme author knows it by - so the
|
|
124
|
+
* name is stated at the node with `styleAs` and the map is keyed by it.
|
|
125
|
+
* `base` is the component at rest; the rest are the variants it supports and
|
|
126
|
+
* the five states in `StateName`.
|
|
127
|
+
*/
|
|
109
128
|
components?: Record<string, Record<string, Style>>;
|
|
110
129
|
/**
|
|
111
130
|
* Colours for syntax scopes. Every scope has a default drawn from the
|
|
@@ -120,6 +139,8 @@ export interface ResolvedTheme {
|
|
|
120
139
|
id: string;
|
|
121
140
|
name: string;
|
|
122
141
|
appearance: 'light' | 'dark';
|
|
142
|
+
/** Whether every colour this theme gives back is the terminal's own. */
|
|
143
|
+
monochrome: boolean;
|
|
123
144
|
colors: Record<ColorToken, Color>;
|
|
124
145
|
spacing: ThemeSpacing;
|
|
125
146
|
glyphs: ThemeGlyphs;
|
|
@@ -131,11 +152,24 @@ export interface ResolvedTheme {
|
|
|
131
152
|
components: Record<string, Record<string, Style>>;
|
|
132
153
|
/** Every syntax scope, resolved to a colour. */
|
|
133
154
|
syntax: Record<SyntaxScope, Color>;
|
|
134
|
-
/** Resolve a token (or pass a literal colour through). */
|
|
155
|
+
/** Resolve a token (or pass a literal colour through, unless monochrome). */
|
|
135
156
|
color(token: string): Color;
|
|
136
157
|
borderChars(style?: BorderStyle): BorderChars;
|
|
137
158
|
dividerChars(style?: DividerStyle): DividerChars;
|
|
138
|
-
/**
|
|
159
|
+
/**
|
|
160
|
+
* Component style for a name and a list of names to merge over it, in the
|
|
161
|
+
* order given - later names win.
|
|
162
|
+
*
|
|
163
|
+
* The names are the component's `variant`, `tone` and `size` followed by the
|
|
164
|
+
* interaction states that are true: `selected`, `hover`, `active` (pressed),
|
|
165
|
+
* `focus` and `disabled`. So `components.List.selected` is the fill on the
|
|
166
|
+
* current row, and `components.List.focused` is the brighter one on the
|
|
167
|
+
* current row while the list has the keyboard.
|
|
168
|
+
*
|
|
169
|
+
* The order is the same one `flattenStyleInput` merges a `style` prop in, so
|
|
170
|
+
* a state stated at the node and the same state stated by the theme resolve
|
|
171
|
+
* to one answer rather than to whichever happened to be asked second.
|
|
172
|
+
*/
|
|
139
173
|
styleFor(component: string, variants?: string[]): Style;
|
|
140
174
|
}
|
|
141
175
|
|
package/src/util/text.ts
CHANGED
|
@@ -283,6 +283,21 @@ export function expandTabs(text: string, tabWidth = 4): string {
|
|
|
283
283
|
|
|
284
284
|
export type TruncateSide = 'end' | 'start' | 'middle';
|
|
285
285
|
|
|
286
|
+
/**
|
|
287
|
+
* Fit into `width` cells, marking the cut whether or not there was one.
|
|
288
|
+
*
|
|
289
|
+
* `truncate` marks only what it had to cut, which is right for a string that
|
|
290
|
+
* has to fit a cell. A paragraph stopped by the box rather than by its own
|
|
291
|
+
* length is the other case: its last visible row may fit exactly and still not
|
|
292
|
+
* be the end of the text, and it has to say so.
|
|
293
|
+
*/
|
|
294
|
+
export function markCut(text: string, width: number, ellipsis = '…'): string {
|
|
295
|
+
if (width <= 0) return '';
|
|
296
|
+
const ew = stringWidth(ellipsis);
|
|
297
|
+
if (width <= ew) return sliceByWidth(ellipsis, width);
|
|
298
|
+
return sliceByWidth(text, width - ew).trimEnd() + ellipsis;
|
|
299
|
+
}
|
|
300
|
+
|
|
286
301
|
/** Fit into `width` cells, marking the cut with `ellipsis`. */
|
|
287
302
|
export function truncate(
|
|
288
303
|
text: string,
|