Skip to content

Commit 726cf09

Browse files
authored
feat(tui): add dialog footer theme override and retire faint in new designs (#60003)
1 parent 66f1fa8 commit 726cf09

10 files changed

Lines changed: 69 additions & 29 deletions

File tree

‎packages/theme/src/tui/expand.ts‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type {
22
BackgroundDefinition,
3+
DialogTokensDefinition,
34
ModeDefinition,
45
StatefulColorDefinition,
56
TextDefinition,
@@ -11,7 +12,14 @@ export function expandTheme<Definition extends ModeDefinition>(definition: Defin
1112
return {
1213
...definition,
1314
...expandTokens(definition),
14-
...(definition["@dialog"] ? { "@dialog": expandTokens(definition["@dialog"]) } : {}),
15+
...(definition["@dialog"] ? { "@dialog": expandDialog(definition["@dialog"]) } : {}),
16+
}
17+
}
18+
19+
function expandDialog(definition: DialogTokensDefinition): DialogTokensDefinition {
20+
return {
21+
...expandTokens(definition),
22+
...(definition["@footer"] ? { "@footer": expandTokens(definition["@footer"]) } : {}),
1523
}
1624
}
1725

‎packages/theme/src/tui/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export {
1818
ThemeDefinition,
1919
ThemeDocument,
2020
type BackgroundDefinition,
21+
type DialogTokensDefinition,
2122
type DiffDefinition,
2223
type FormfieldColorDefinition,
2324
type HueDefinition,

‎packages/theme/src/tui/resolve.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,16 @@ function resolveExpandedTheme(definition: ThemeDefinition): ResolvedTheme {
7676
...resolveView(tokens, hue, categorical, hueSteps),
7777
surface: (name) => views[name],
7878
})
79-
views.dialog = definition["@dialog"] ? view(contextualize(base, definition["@dialog"])) : view(base)
79+
const dialog = definition["@dialog"]
80+
const dialogTokens = dialog ? contextualize(base, tokens(dialog)) : base
81+
views.dialog = view(dialogTokens)
82+
// The footer override layers on the dialog surface, so footers keep every dialog token they don't override.
83+
const footer = dialog?.["@footer"]
84+
views["dialog.footer"] = footer ? view(contextualize(dialogTokens, footer)) : views.dialog
8085
return view(base)
8186
}
8287

83-
function tokens(definition: ThemeDefinition): ThemeTokensDefinition {
88+
function tokens(definition: ThemeTokensDefinition): ThemeTokensDefinition {
8489
return {
8590
text: definition.text,
8691
background: definition.background,

‎packages/theme/src/tui/schema.ts‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export const ActionState = Schema.Literals(["disabled", "pressed", "focused", "s
1111
export type ActionState = Schema.Schema.Type<typeof ActionState>
1212
export type ActionStateKey = `$${ActionState}`
1313

14-
export const SurfaceName = Schema.Literal("dialog")
14+
export const SurfaceName = Schema.Literals(["dialog", "dialog.footer"])
1515
export type SurfaceName = Schema.Schema.Type<typeof SurfaceName>
1616

1717
export const FormfieldState = ActionState
@@ -201,6 +201,12 @@ const ThemeTokensDefinition = Schema.Struct({
201201
})
202202
export type ThemeTokensDefinition = Schema.Schema.Type<typeof ThemeTokensDefinition>
203203

204+
const DialogTokensDefinition = Schema.Struct({
205+
...ThemeTokensDefinition.fields,
206+
"@footer": Schema.optional(ThemeTokensDefinition),
207+
})
208+
export type DialogTokensDefinition = Schema.Schema.Type<typeof DialogTokensDefinition>
209+
204210
const CompleteStatefulColorDefinition = Schema.Struct({
205211
base: ColorValue,
206212
$hovered: Schema.optional(ColorValue),
@@ -264,23 +270,23 @@ const ThemeDefinitionFields = Schema.Struct({
264270
hue: HueDefinition,
265271
categorical: CategoricalDefinition,
266272
...CompleteThemeTokensDefinition.fields,
267-
"@dialog": Schema.optional(ThemeTokensDefinition),
273+
"@dialog": Schema.optional(DialogTokensDefinition),
268274
})
269275
export const ThemeDefinition = ThemeDefinitionFields
270276
export type ThemeDefinition = Schema.Schema.Type<typeof ThemeDefinition>
271277

272278
export const BaseThemeDefinition = Schema.Struct({
273279
categorical: CategoricalDefinition,
274280
...CompleteThemeTokensDefinition.fields,
275-
"@dialog": Schema.optional(ThemeTokensDefinition),
281+
"@dialog": Schema.optional(DialogTokensDefinition),
276282
})
277283
export type BaseThemeDefinition = Schema.Schema.Type<typeof BaseThemeDefinition>
278284

279285
export const ModeDefinition = Schema.Struct({
280286
hue: HueDefinition,
281287
categorical: Schema.optional(CategoricalDefinition),
282288
...ThemeTokensDefinition.fields,
283-
"@dialog": Schema.optional(ThemeTokensDefinition),
289+
"@dialog": Schema.optional(DialogTokensDefinition),
284290
})
285291
export type ModeDefinition = Schema.Schema.Type<typeof ModeDefinition>
286292

‎packages/tui/src/component/model-options.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export function ModelDetail(props: {
6868
...(props.model.variants.length > 0 ? ["Reasoning"] : []),
6969
].filter(Boolean)
7070
const list = (items: string[]) => (
71-
<text fg={theme.text.faint} wrapMode="none" flexShrink={0}>
71+
<text fg={theme.text.muted} wrapMode="none" flexShrink={0}>
7272
<For each={items}>
7373
{(fact, index) => (
7474
<>
@@ -85,7 +85,7 @@ export function ModelDetail(props: {
8585
fallback={
8686
<box flexDirection="row" justifyContent={props.price === false ? "flex-start" : "space-between"} gap={2}>
8787
<Show when={props.price !== false}>
88-
<text fg={theme.text.faint} wrapMode="none">
88+
<text fg={theme.text.muted} wrapMode="none">
8989
{price()}
9090
</text>
9191
</Show>

‎packages/tui/src/theme/assets/v2/opencode2.json‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,11 @@
1010
],
1111
"text": {
1212
"base": "$hue.neutral.100",
13-
"muted": "$hue.neutral.200",
14-
"faint": "$hue.neutral.300",
13+
"muted": "$hue.neutral.300",
1514
"action": {
1615
"primary": {
1716
"base": "$text.base",
18-
"$disabled": "$text.faint",
17+
"$disabled": "$text.muted",
1918
"$focused": "$text.base",
2019
"$selected": "$hue.interactive.400"
2120
},
@@ -26,15 +25,15 @@
2625
},
2726
"destructive": {
2827
"base": "$hue.neutral.900",
29-
"$disabled": "$text.faint"
28+
"$disabled": "$text.muted"
3029
}
3130
},
3231
"formfield": {
3332
"base": "$text.base",
3433
"$hovered": "$hue.interactive.400",
3534
"$focused": "$hue.interactive.400",
3635
"$pressed": "$hue.interactive.400",
37-
"$disabled": "$text.faint",
36+
"$disabled": "$text.muted",
3837
"$selected": "$hue.interactive.400"
3938
},
4039
"feedback": {
@@ -159,6 +158,11 @@
159158
"$hovered": "$background.raised.max"
160159
}
161160
}
161+
},
162+
"@footer": {
163+
"text": {
164+
"muted": "$hue.neutral.200"
165+
}
162166
}
163167
}
164168
},

‎packages/tui/src/ui/dialog-prompt.tsx‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useTheme } from "../context/theme"
44
import { DialogFooter, DialogHeader, DialogLayout, useDialog, type DialogSize } from "./dialog"
55
import { Show, createEffect, createSignal, onMount, type JSX } from "solid-js"
66
import { Spinner } from "../component/spinner"
7-
import { useConfig, useNewDesigns } from "../config"
7+
import { useConfig } from "../config"
88
import { useRenderer } from "@opentui/solid"
99

1010
export type DialogPromptAction = {
@@ -19,7 +19,7 @@ export type DialogPromptAction = {
1919
export type DialogPromptProps = {
2020
title: string
2121
size?: DialogSize
22-
/** Faint line below the title, under the "new-designs" experiment. */
22+
/** Muted line below the title, under the "new-designs" experiment. */
2323
subtitle?: string
2424
description?: () => JSX.Element
2525
placeholder?: string
@@ -36,7 +36,6 @@ export function DialogPrompt(props: DialogPromptProps) {
3636
const renderer = useRenderer()
3737
const theme = useTheme().surface("dialog")
3838
const config = useConfig().data
39-
const newDesigns = useNewDesigns()
4039
const [textareaTarget, setTextareaTarget] = createSignal<TextareaRenderable>()
4140
let textarea: TextareaRenderable
4241

@@ -169,7 +168,7 @@ export function DialogPrompt(props: DialogPromptProps) {
169168
}}
170169
initialValue={props.value}
171170
placeholder={props.placeholder ?? "Enter text"}
172-
placeholderColor={newDesigns() ? theme.text.faint : theme.text.muted}
171+
placeholderColor={theme.text.muted}
173172
textColor={props.busy ? theme.text.formfield.disabled : theme.text.formfield.base}
174173
focusedTextColor={props.busy ? theme.text.formfield.disabled : theme.text.formfield.base}
175174
cursorColor={props.busy ? theme.background.formfield.disabled : theme.text.base}

‎packages/tui/src/ui/dialog-select.tsx‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { moveSelection, reconcileSelection } from "./select-controller"
2626
export interface DialogSelectProps<T> {
2727
title: string
2828
titleView?: JSX.Element
29-
/** Faint line below the title, under the "new-designs" experiment. Not shown with `searchInTitle`. */
29+
/** Muted line below the title, under the "new-designs" experiment. Not shown with `searchInTitle`. */
3030
description?: string
3131
/** Blank rows above the footer under the "new-designs" experiment. Defaults to 1. */
3232
footerGap?: number
@@ -677,7 +677,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
677677
}, 1)
678678
}}
679679
placeholder={(searchInTitle() ? props.titlePlaceholder : undefined) ?? props.placeholder ?? "Search"}
680-
placeholderColor={newDesigns() ? theme.text.faint : theme.text.muted}
680+
placeholderColor={theme.text.muted}
681681
/>
682682
)
683683

@@ -829,7 +829,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
829829
</scrollbox>
830830
<Show when={ellipsis()}>
831831
<box paddingLeft={DIALOG_LIST_INSET} flexShrink={0}>
832-
<text fg={theme.text.faint}>…</text>
832+
<text fg={theme.text.muted}>…</text>
833833
</box>
834834
</Show>
835835
{/* Reserved whenever any option has a detail, so the dialog does not resize as the selection moves. */}
@@ -839,7 +839,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
839839
when={detailView()}
840840
keyed
841841
fallback={
842-
<text fg={theme.text.muted} wrapMode="word">
842+
<text fg={theme.surface("dialog.footer").text.muted} wrapMode="word">
843843
{detailText() || " "}
844844
</text>
845845
}
@@ -865,15 +865,15 @@ export function DialogSelectChoice(props: { value: string; active: boolean; open
865865
if (!props.active) return ` ${props.value} `
866866
return props.open ? ` ${props.value} ↵` : `‹ ${props.value} ›`
867867
}
868-
return <span style={{ fg: props.active ? theme.text.action.primary.focused : theme.text.faint }}>{text()}</span>
868+
return <span style={{ fg: props.active ? theme.text.action.primary.focused : theme.text.muted }}>{text()}</span>
869869
}
870870

871871
function DialogSelectCategory(props: { title: string; first: boolean }) {
872872
const theme = useTheme().surface("dialog")
873873
const newDesigns = useNewDesigns()
874874
return (
875875
<box paddingTop={props.first ? 0 : 1} paddingLeft={DIALOG_LIST_INSET - (newDesigns() ? 0 : 1)}>
876-
<text fg={newDesigns() ? theme.text.faint : theme.hue.accent[200]} attributes={TextAttributes.BOLD}>
876+
<text fg={newDesigns() ? theme.text.muted : theme.hue.accent[200]} attributes={TextAttributes.BOLD}>
877877
{props.title}
878878
</text>
879879
</box>
@@ -906,7 +906,7 @@ function DialogSelectRow(props: {
906906
// Secondary text (description and footer).
907907
const secondary = () => {
908908
if (!newDesigns()) return props.active ? text() : theme.text.muted
909-
return props.active ? theme.text.action.secondary.focused : theme.text.faint
909+
return props.active ? theme.text.action.secondary.focused : theme.text.muted
910910
}
911911

912912
const row = () => (

‎packages/tui/src/ui/dialog.tsx‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ export function DialogHeader(
9696
closeLabel?: string
9797
onClose?: () => void
9898
gap?: number
99-
/** Faint line below the title. Shown only under the "new-designs" experiment. */
99+
/** Muted line below the title. Shown only under the "new-designs" experiment. */
100100
description?: string
101101
}>,
102102
) {
@@ -112,7 +112,7 @@ export function DialogHeader(
112112
)}
113113
{props.children}
114114
<text
115-
fg={newDesigns() ? theme.text.faint : theme.text.muted}
115+
fg={theme.text.muted}
116116
flexShrink={0}
117117
onMouseUp={() => (props.onClose ?? dialog.clear)()}
118118
>
@@ -124,7 +124,7 @@ export function DialogHeader(
124124
<Show when={newDesigns() && props.description} fallback={row()}>
125125
<box>
126126
{row()}
127-
<text fg={theme.text.faint} wrapMode="word">
127+
<text fg={theme.text.muted} wrapMode="word">
128128
{props.description}
129129
</text>
130130
</box>
@@ -297,7 +297,7 @@ function DialogFooterItem(props: { action: DialogFooterAction; variant: "primary
297297
onMouseOut={() => setHovered(false)}
298298
onMouseUp={trigger}
299299
>
300-
<text>{label(theme.text.faint)}</text>
300+
<text>{label(theme.text.muted)}</text>
301301
</box>
302302
</Show>
303303
)

‎packages/tui/test/theme/v2/resolve.test.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,23 @@ test("surface overrides rewire references and reset action states from their bas
337337
expect(raised.text.action.primary.focused.toInts()).toEqual([85, 85, 85, 255])
338338
})
339339

340+
test("dialog footer overrides layer on the dialog surface", () => {
341+
const theme = resolveTheme(
342+
override(light, {
343+
"@dialog": {
344+
text: { base: "#333333" },
345+
"@footer": { text: { muted: "#666666" } },
346+
},
347+
}),
348+
)
349+
const footer = theme.surface("dialog.footer")
350+
expect(footer.text.muted.toInts()).toEqual([102, 102, 102, 255])
351+
expect(footer.text.base.toInts()).toEqual([51, 51, 51, 255])
352+
expect(theme.surface("dialog").text.muted.toInts()).not.toEqual([102, 102, 102, 255])
353+
const plain = resolveTheme(light)
354+
expect(plain.surface("dialog.footer")).toBe(plain.surface("dialog"))
355+
})
356+
340357
test("rejects missing and circular references", () => {
341358
expect(() => resolveTheme(override(light, { text: { base: "$missing" } }))).toThrow(
342359
'Theme reference "$missing" was not found',

0 commit comments

Comments
 (0)