opentui(keymap): adopt native @opentui/keymap for overlay close + confirm
@opentui/keymap@0.3.2 was installed but unused (the spec said we'd use it). Wire it natively: createDefaultOpenTuiKeymap(renderer) + <KeymapProvider> at the render root, and a useCloseLayer(target,onClose) helper that registers a focus-within Esc/Ctrl+C → close layer. Migrated the close handling of sessionSwitcher, picker, approvalPrompt (close-only), confirmPrompt (y/n via confirm/cancel commands), and pager + agentsDashboard (close via keymap; scroll/select stay raw — not cleanly focus-gated). Overlays gain a root ref + focus-on-mount so the focus-within layer activates. q-close re-added to pager/dashboard (footer advertises it). Composer history/refocus + masked prompt + the Ctrl+C quit machine stay raw by design (need the in-flight keystroke / careful state). Test harness gains a withKeymap() wrapper so view tests mount under a provider. 91 pass; live-verified /sessions + /tools Esc-close and composer focus recovery after.
This commit is contained in:
parent
46293f618c
commit
79c6896153
@ -19,6 +19,8 @@
|
|||||||
* The body of `run` does not change when the backend swaps — that's the point of
|
* The body of `run` does not change when the backend swaps — that's the point of
|
||||||
* the layer; only `makeAppLayer(...)` differs at the edge.
|
* the layer; only `makeAppLayer(...)` differs at the edge.
|
||||||
*/
|
*/
|
||||||
|
import { createDefaultOpenTuiKeymap } from '@opentui/keymap/opentui'
|
||||||
|
import { KeymapProvider } from '@opentui/keymap/solid'
|
||||||
import { render } from '@opentui/solid'
|
import { render } from '@opentui/solid'
|
||||||
import { Deferred, Duration, Effect } from 'effect'
|
import { Deferred, Duration, Effect } from 'effect'
|
||||||
import { writeFileSync } from 'node:fs'
|
import { writeFileSync } from 'node:fs'
|
||||||
@ -289,6 +291,11 @@ export const run = Effect.fn('Tui.run')(function* (input: TuiInput) {
|
|||||||
if (!renderer.isDestroyed) renderer.destroy()
|
if (!renderer.isDestroyed) renderer.destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Native keymap host (Phase 3): one keymap bound to this renderer, provided
|
||||||
|
// to the whole Solid tree via <KeymapProvider>. Overlays/prompts register
|
||||||
|
// close (and confirm) layers against it through useCloseLayer/useBindings.
|
||||||
|
const keymap = createDefaultOpenTuiKeymap(renderer)
|
||||||
|
|
||||||
// Submit a user turn: the service value is in hand, so `gateway.request(...)`
|
// Submit a user turn: the service value is in hand, so `gateway.request(...)`
|
||||||
// is Effect<…, never> — fire it detached with runFork; failures are logged.
|
// is Effect<…, never> — fire it detached with runFork; failures are logged.
|
||||||
const submitPrompt = (text: string) => {
|
const submitPrompt = (text: string) => {
|
||||||
@ -384,6 +391,7 @@ export const run = Effect.fn('Tui.run')(function* (input: TuiInput) {
|
|||||||
yield* Effect.promise(() =>
|
yield* Effect.promise(() =>
|
||||||
render(
|
render(
|
||||||
() => (
|
() => (
|
||||||
|
<KeymapProvider keymap={keymap}>
|
||||||
<ThemeProvider theme={() => store.state.theme}>
|
<ThemeProvider theme={() => store.state.theme}>
|
||||||
<App
|
<App
|
||||||
store={store}
|
store={store}
|
||||||
@ -397,6 +405,7 @@ export const run = Effect.fn('Tui.run')(function* (input: TuiInput) {
|
|||||||
pasteStore={pasteStore}
|
pasteStore={pasteStore}
|
||||||
/>
|
/>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
</KeymapProvider>
|
||||||
),
|
),
|
||||||
renderer
|
renderer
|
||||||
)
|
)
|
||||||
|
|||||||
27
ui-tui-opentui-v2/src/view/keymap.tsx
Normal file
27
ui-tui-opentui-v2/src/view/keymap.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* keymap.tsx — thin Solid helpers over the native `@opentui/keymap` (Phase 3).
|
||||||
|
*
|
||||||
|
* `useCloseLayer` is the shared CLOSE binding for overlays/prompts: a `close`
|
||||||
|
* command bound to Esc and Ctrl+C, scoped to the overlay's root box via a
|
||||||
|
* `focus-within` layer (the default when a `target` accessor is present). The
|
||||||
|
* box itself isn't focused — the native `<select>`/`<textarea>` inside it is —
|
||||||
|
* so `focus-within` is what makes the layer active while the overlay owns the
|
||||||
|
* screen. The keymap host is provided once at the entry by `<KeymapProvider>`.
|
||||||
|
*/
|
||||||
|
import type { BoxRenderable } from '@opentui/core'
|
||||||
|
import { useBindings } from '@opentui/keymap/solid'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bind Esc / Ctrl+C → `onClose`, scoped to the given root box (focus-within).
|
||||||
|
* Until the ref resolves the layer simply isn't registered (useBindings waits).
|
||||||
|
*/
|
||||||
|
export function useCloseLayer(target: () => BoxRenderable | undefined, onClose: () => void): void {
|
||||||
|
useBindings<BoxRenderable>(() => ({
|
||||||
|
target,
|
||||||
|
commands: [{ name: 'close', run() { onClose() } }],
|
||||||
|
bindings: [
|
||||||
|
{ key: 'escape', cmd: 'close' },
|
||||||
|
{ key: { name: 'c', ctrl: true }, cmd: 'close' }
|
||||||
|
]
|
||||||
|
}))
|
||||||
|
}
|
||||||
@ -6,13 +6,14 @@
|
|||||||
* - bottom: the SELECTED subagent's live trace (goal · status · model, latest
|
* - bottom: the SELECTED subagent's live trace (goal · status · model, latest
|
||||||
* thought, and the tool/progress/summary log) — sticky-bottom so it follows
|
* thought, and the tool/progress/summary log) — sticky-bottom so it follows
|
||||||
* live; PgUp/PgDn scroll it.
|
* live; PgUp/PgDn scroll it.
|
||||||
* Esc/q close. §8 #2 scrollbox gotchas (minHeight:0, sticky bottom).
|
* Esc/Ctrl+C close (native keymap). §8 #2 scrollbox gotchas (minHeight:0, sticky bottom).
|
||||||
*/
|
*/
|
||||||
import { type ScrollBoxRenderable } from '@opentui/core'
|
import { type BoxRenderable, type ScrollBoxRenderable } from '@opentui/core'
|
||||||
import { useKeyboard } from '@opentui/solid'
|
import { useKeyboard } from '@opentui/solid'
|
||||||
import { createSignal, For, Show } from 'solid-js'
|
import { createSignal, For, onMount, Show } from 'solid-js'
|
||||||
|
|
||||||
import type { SubagentInfo } from '../../logic/store.ts'
|
import type { SubagentInfo } from '../../logic/store.ts'
|
||||||
|
import { useCloseLayer } from '../keymap.tsx'
|
||||||
import { useTheme } from '../theme.tsx'
|
import { useTheme } from '../theme.tsx'
|
||||||
|
|
||||||
const PAGE = 8
|
const PAGE = 8
|
||||||
@ -28,17 +29,21 @@ function statusColor(status: string, theme: ReturnType<typeof useTheme>): string
|
|||||||
export function AgentsDashboard(props: { subagents: SubagentInfo[]; onClose: () => void }) {
|
export function AgentsDashboard(props: { subagents: SubagentInfo[]; onClose: () => void }) {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const [sel, setSel] = createSignal(0)
|
const [sel, setSel] = createSignal(0)
|
||||||
|
let rootRef: BoxRenderable | undefined
|
||||||
let traceBox: ScrollBoxRenderable | undefined
|
let traceBox: ScrollBoxRenderable | undefined
|
||||||
|
|
||||||
const count = () => props.subagents.length
|
const count = () => props.subagents.length
|
||||||
const selected = () => Math.min(sel(), Math.max(0, count() - 1))
|
const selected = () => Math.min(sel(), Math.max(0, count() - 1))
|
||||||
const current = () => props.subagents[selected()]
|
const current = () => props.subagents[selected()]
|
||||||
|
|
||||||
|
// Close (Esc/Ctrl+C) is the native keymap; select + scroll stay in the raw global
|
||||||
|
// handler below. Focus the root box on mount so the focus-within close layer is active.
|
||||||
|
onMount(() => rootRef?.focus())
|
||||||
|
useCloseLayer(() => rootRef, () => props.onClose())
|
||||||
|
|
||||||
useKeyboard(key => {
|
useKeyboard(key => {
|
||||||
if (key.name === 'escape' || key.name === 'q' || (key.ctrl && key.name === 'c')) {
|
// `q` closes (footer advertises "Esc/q close"); Esc/Ctrl+C close via the keymap.
|
||||||
props.onClose()
|
if (key.name === 'q') return props.onClose()
|
||||||
return
|
|
||||||
}
|
|
||||||
if (key.name === 'up') setSel(s => Math.max(0, s - 1))
|
if (key.name === 'up') setSel(s => Math.max(0, s - 1))
|
||||||
else if (key.name === 'down') setSel(s => Math.min(Math.max(0, count() - 1), s + 1))
|
else if (key.name === 'down') setSel(s => Math.min(Math.max(0, count() - 1), s + 1))
|
||||||
else if (key.name === 'pageup') traceBox?.scrollBy(-PAGE)
|
else if (key.name === 'pageup') traceBox?.scrollBy(-PAGE)
|
||||||
@ -46,7 +51,12 @@ export function AgentsDashboard(props: { subagents: SubagentInfo[]; onClose: ()
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<box style={{ borderColor: theme().color.accent, flexDirection: 'column', flexGrow: 1, minHeight: 0 }} border>
|
<box
|
||||||
|
ref={el => (rootRef = el)}
|
||||||
|
focusable
|
||||||
|
style={{ borderColor: theme().color.accent, flexDirection: 'column', flexGrow: 1, minHeight: 0 }}
|
||||||
|
border
|
||||||
|
>
|
||||||
<box style={{ flexShrink: 0, paddingLeft: 1 }}>
|
<box style={{ flexShrink: 0, paddingLeft: 1 }}>
|
||||||
<text fg={theme().color.accent}>
|
<text fg={theme().color.accent}>
|
||||||
<b>
|
<b>
|
||||||
|
|||||||
@ -4,28 +4,35 @@
|
|||||||
* /tools) at once. Replaces the transcript+composer while open (the App swaps it
|
* /tools) at once. Replaces the transcript+composer while open (the App swaps it
|
||||||
* in on `store.state.pager`).
|
* in on `store.state.pager`).
|
||||||
*
|
*
|
||||||
* Scrolling is driven explicitly via `useKeyboard` → `scrollBy`/`scrollTo` (no
|
* Scrolling is driven explicitly via a GLOBAL `useKeyboard` → `scrollBy`/`scrollTo`
|
||||||
* reliance on scrollbox auto-focus); Esc/q/Ctrl+C close. Carries the §8 #2
|
* (no reliance on focus); Esc/Ctrl+C close via the native keymap. Carries the §8 #2
|
||||||
* scrollbox gotchas (minHeight:0 wrapper+box, NO flexDirection on the box root).
|
* scrollbox gotchas (minHeight:0 wrapper+box, NO flexDirection on the box root).
|
||||||
*/
|
*/
|
||||||
import { type ScrollBoxRenderable } from '@opentui/core'
|
import { type BoxRenderable, type ScrollBoxRenderable } from '@opentui/core'
|
||||||
import { useKeyboard } from '@opentui/solid'
|
import { useKeyboard } from '@opentui/solid'
|
||||||
import { For } from 'solid-js'
|
import { For, onMount } from 'solid-js'
|
||||||
|
|
||||||
|
import { useCloseLayer } from '../keymap.tsx'
|
||||||
import { useTheme } from '../theme.tsx'
|
import { useTheme } from '../theme.tsx'
|
||||||
|
|
||||||
const PAGE = 10
|
const PAGE = 10
|
||||||
|
|
||||||
export function Pager(props: { title: string; text: string; onClose: () => void }) {
|
export function Pager(props: { title: string; text: string; onClose: () => void }) {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
let rootRef: BoxRenderable | undefined
|
||||||
let box: ScrollBoxRenderable | undefined
|
let box: ScrollBoxRenderable | undefined
|
||||||
const lines = () => props.text.split('\n')
|
const lines = () => props.text.split('\n')
|
||||||
|
|
||||||
|
// Close (Esc/Ctrl+C) is the native keymap; scroll keys stay in the raw global
|
||||||
|
// handler below. Focus the root box on mount so the focus-within close layer is
|
||||||
|
// active (the scrollbox isn't focused — scroll is global, not focus-gated).
|
||||||
|
onMount(() => rootRef?.focus())
|
||||||
|
useCloseLayer(() => rootRef, () => props.onClose())
|
||||||
|
|
||||||
useKeyboard(key => {
|
useKeyboard(key => {
|
||||||
if (key.name === 'escape' || key.name === 'q' || (key.ctrl && key.name === 'c')) {
|
// `q` closes (the footer advertises "Esc/q close"); Esc/Ctrl+C close via the
|
||||||
props.onClose()
|
// keymap layer above. Scroll stays raw (not focus-gated).
|
||||||
return
|
if (key.name === 'q') return props.onClose()
|
||||||
}
|
|
||||||
if (!box) return
|
if (!box) return
|
||||||
if (key.name === 'up') box.scrollBy(-1)
|
if (key.name === 'up') box.scrollBy(-1)
|
||||||
else if (key.name === 'down') box.scrollBy(1)
|
else if (key.name === 'down') box.scrollBy(1)
|
||||||
@ -36,7 +43,12 @@ export function Pager(props: { title: string; text: string; onClose: () => void
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<box style={{ borderColor: theme().color.accent, flexDirection: 'column', flexGrow: 1, minHeight: 0 }} border>
|
<box
|
||||||
|
ref={el => (rootRef = el)}
|
||||||
|
focusable
|
||||||
|
style={{ borderColor: theme().color.accent, flexDirection: 'column', flexGrow: 1, minHeight: 0 }}
|
||||||
|
border
|
||||||
|
>
|
||||||
<box style={{ flexShrink: 0, paddingLeft: 1 }}>
|
<box style={{ flexShrink: 0, paddingLeft: 1 }}>
|
||||||
<text fg={theme().color.accent}>
|
<text fg={theme().color.accent}>
|
||||||
<b>{props.title}</b>
|
<b>{props.title}</b>
|
||||||
|
|||||||
@ -4,10 +4,11 @@
|
|||||||
* Native select nav (↑↓/j/k/Enter); a small useKeyboard adds Esc/Ctrl+C close.
|
* Native select nav (↑↓/j/k/Enter); a small useKeyboard adds Esc/Ctrl+C close.
|
||||||
* Replaces the composer while open.
|
* Replaces the composer while open.
|
||||||
*/
|
*/
|
||||||
import { useKeyboard } from '@opentui/solid'
|
import type { BoxRenderable } from '@opentui/core'
|
||||||
import { createMemo } from 'solid-js'
|
import { createMemo } from 'solid-js'
|
||||||
|
|
||||||
import type { PickerItem } from '../../logic/store.ts'
|
import type { PickerItem } from '../../logic/store.ts'
|
||||||
|
import { useCloseLayer } from '../keymap.tsx'
|
||||||
import { useTheme } from '../theme.tsx'
|
import { useTheme } from '../theme.tsx'
|
||||||
|
|
||||||
export function Picker(props: {
|
export function Picker(props: {
|
||||||
@ -17,9 +18,9 @@ export function Picker(props: {
|
|||||||
onClose: () => void
|
onClose: () => void
|
||||||
}) {
|
}) {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
useKeyboard(key => {
|
let rootRef: BoxRenderable | undefined
|
||||||
if (key.name === 'escape' || (key.ctrl && key.name === 'c')) props.onClose()
|
// Native select handles ↑↓/j/k/Enter; the keymap owns Esc/Ctrl+C close.
|
||||||
})
|
useCloseLayer(() => rootRef, () => props.onClose())
|
||||||
|
|
||||||
const options = createMemo(() =>
|
const options = createMemo(() =>
|
||||||
props.items.map(it => ({ description: it.description ?? '', name: it.label, value: it.value }))
|
props.items.map(it => ({ description: it.description ?? '', name: it.label, value: it.value }))
|
||||||
@ -27,6 +28,7 @@ export function Picker(props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<box
|
<box
|
||||||
|
ref={el => (rootRef = el)}
|
||||||
style={{ borderColor: theme().color.border, flexDirection: 'column', flexShrink: 0, marginTop: 1, padding: 1 }}
|
style={{ borderColor: theme().color.border, flexDirection: 'column', flexShrink: 0, marginTop: 1, padding: 1 }}
|
||||||
border
|
border
|
||||||
>
|
>
|
||||||
|
|||||||
@ -4,10 +4,11 @@
|
|||||||
* Enter resumes the chosen session (the entry runs the same resume-hydrate path
|
* Enter resumes the chosen session (the entry runs the same resume-hydrate path
|
||||||
* as launch), Esc/Ctrl+C closes. Replaces the composer while open.
|
* as launch), Esc/Ctrl+C closes. Replaces the composer while open.
|
||||||
*/
|
*/
|
||||||
import { useKeyboard } from '@opentui/solid'
|
import type { BoxRenderable } from '@opentui/core'
|
||||||
import { createMemo } from 'solid-js'
|
import { createMemo } from 'solid-js'
|
||||||
|
|
||||||
import type { SessionItem } from '../../logic/store.ts'
|
import type { SessionItem } from '../../logic/store.ts'
|
||||||
|
import { useCloseLayer } from '../keymap.tsx'
|
||||||
import { useTheme } from '../theme.tsx'
|
import { useTheme } from '../theme.tsx'
|
||||||
|
|
||||||
export function SessionSwitcher(props: {
|
export function SessionSwitcher(props: {
|
||||||
@ -16,9 +17,9 @@ export function SessionSwitcher(props: {
|
|||||||
onClose: () => void
|
onClose: () => void
|
||||||
}) {
|
}) {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
useKeyboard(key => {
|
let rootRef: BoxRenderable | undefined
|
||||||
if (key.name === 'escape' || (key.ctrl && key.name === 'c')) props.onClose()
|
// Native select handles ↑↓/Enter; the keymap owns Esc/Ctrl+C close.
|
||||||
})
|
useCloseLayer(() => rootRef, () => props.onClose())
|
||||||
|
|
||||||
const options = createMemo(() =>
|
const options = createMemo(() =>
|
||||||
props.sessions.map(s => ({
|
props.sessions.map(s => ({
|
||||||
@ -30,6 +31,7 @@ export function SessionSwitcher(props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<box
|
<box
|
||||||
|
ref={el => (rootRef = el)}
|
||||||
style={{ borderColor: theme().color.border, flexDirection: 'column', flexShrink: 0, marginTop: 1, padding: 1 }}
|
style={{ borderColor: theme().color.border, flexDirection: 'column', flexShrink: 0, marginTop: 1, padding: 1 }}
|
||||||
border
|
border
|
||||||
>
|
>
|
||||||
|
|||||||
@ -4,8 +4,9 @@
|
|||||||
* adds the Esc/Ctrl+C → deny cancel path the select doesn't cover. Answered via
|
* adds the Esc/Ctrl+C → deny cancel path the select doesn't cover. Answered via
|
||||||
* `approval.respond {choice, session_id}`.
|
* `approval.respond {choice, session_id}`.
|
||||||
*/
|
*/
|
||||||
import { useKeyboard } from '@opentui/solid'
|
import type { BoxRenderable } from '@opentui/core'
|
||||||
|
|
||||||
|
import { useCloseLayer } from '../keymap.tsx'
|
||||||
import { useTheme } from '../theme.tsx'
|
import { useTheme } from '../theme.tsx'
|
||||||
|
|
||||||
const OPTIONS = [
|
const OPTIONS = [
|
||||||
@ -22,12 +23,14 @@ export function ApprovalPrompt(props: {
|
|||||||
onCancel: () => void
|
onCancel: () => void
|
||||||
}) {
|
}) {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
useKeyboard(key => {
|
let rootRef: BoxRenderable | undefined
|
||||||
if (key.name === 'escape' || (key.ctrl && key.name === 'c')) props.onCancel()
|
// Native select handles ↑↓/j/k/Enter over the options; the keymap owns the
|
||||||
})
|
// Esc/Ctrl+C → deny cancel path the select doesn't cover.
|
||||||
|
useCloseLayer(() => rootRef, () => props.onCancel())
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<box
|
<box
|
||||||
|
ref={el => (rootRef = el)}
|
||||||
style={{ borderColor: theme().color.border, flexDirection: 'column', flexShrink: 0, marginTop: 1, padding: 1 }}
|
style={{ borderColor: theme().color.border, flexDirection: 'column', flexShrink: 0, marginTop: 1, padding: 1 }}
|
||||||
border
|
border
|
||||||
>
|
>
|
||||||
|
|||||||
@ -3,19 +3,39 @@
|
|||||||
* callback, not an RPC: y/Enter → confirm, n/Esc/Ctrl+C → cancel. Used by client
|
* callback, not an RPC: y/Enter → confirm, n/Esc/Ctrl+C → cancel. Used by client
|
||||||
* slash commands like /clear and /new.
|
* slash commands like /clear and /new.
|
||||||
*/
|
*/
|
||||||
import { useKeyboard } from '@opentui/solid'
|
import type { BoxRenderable } from '@opentui/core'
|
||||||
|
import { useBindings } from '@opentui/keymap/solid'
|
||||||
|
import { onMount } from 'solid-js'
|
||||||
|
|
||||||
import { useTheme } from '../theme.tsx'
|
import { useTheme } from '../theme.tsx'
|
||||||
|
|
||||||
export function ConfirmPrompt(props: { message: string; onYes: () => void; onNo: () => void }) {
|
export function ConfirmPrompt(props: { message: string; onYes: () => void; onNo: () => void }) {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
useKeyboard(key => {
|
let rootRef: BoxRenderable | undefined
|
||||||
if (key.name === 'y' || key.name === 'return') props.onYes()
|
// No focusable child here (unlike the <select> prompts), so focus the dialog box
|
||||||
else if (key.name === 'n' || key.name === 'escape' || (key.ctrl && key.name === 'c')) props.onNo()
|
// itself on mount — that makes the focus-within keymap layer below active.
|
||||||
})
|
onMount(() => rootRef?.focus())
|
||||||
|
// Local Y/N dialog: y/Enter → confirm, n/Esc/Ctrl+C → cancel, scoped to the
|
||||||
|
// dialog box (focus-within) via the native keymap.
|
||||||
|
useBindings<BoxRenderable>(() => ({
|
||||||
|
target: () => rootRef,
|
||||||
|
commands: [
|
||||||
|
{ name: 'confirm', run() { props.onYes() } },
|
||||||
|
{ name: 'cancel', run() { props.onNo() } }
|
||||||
|
],
|
||||||
|
bindings: [
|
||||||
|
{ key: 'y', cmd: 'confirm' },
|
||||||
|
{ key: 'return', cmd: 'confirm' },
|
||||||
|
{ key: 'n', cmd: 'cancel' },
|
||||||
|
{ key: 'escape', cmd: 'cancel' },
|
||||||
|
{ key: { name: 'c', ctrl: true }, cmd: 'cancel' }
|
||||||
|
]
|
||||||
|
}))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<box
|
<box
|
||||||
|
ref={el => (rootRef = el)}
|
||||||
|
focusable
|
||||||
style={{ borderColor: theme().color.border, flexDirection: 'column', flexShrink: 0, marginTop: 1, padding: 1 }}
|
style={{ borderColor: theme().color.border, flexDirection: 'column', flexShrink: 0, marginTop: 1, padding: 1 }}
|
||||||
border
|
border
|
||||||
>
|
>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user