Esta página foi traduzida automaticamente e pode conter erros. Ler o original em inglês

Elementos

Resizable

A group redimensionável organiza painéis lado a lado ou empilhados, com uma alça em cada fronteira entre dois deles: uma barra lateral ao lado de um documento, um editor acima de seu console, uma lista ao lado de seu detalhe. O usuário arrasta uma alça para mover a fronteira, e cada painel permanece dentro dos tamanhos que o app permite. ResizablePanelGroup cuida do comportamento: o arrasto, que roda nativamente quadro a quadro, a divisão do espaço, o cursor de redimensionamento e uma área de toque maior para o dedo. A aparência é do app: uma alça é uma faixa sem linha, cor ou pegada próprias, e o que ela desenha é o indicador do app; os painéis são as views do app.

el-resizable.tsx
el-resizable.tsxLive
import { mount, signal, Text, View } from "nsh";
import { ResizablePanel, ResizablePanelGroup } from "nsh/base";
import { desktop, Log, Panel, Segmented, Split, ui, wide } from "./palette";

// The sizes are the app's: the group reports them when a drag ends, and the
// app passes them back as each panel's `size`.
function App() {
  const sidebar = signal(desktop ? 220 : 140);
  const consoleSize = signal(120);
  const showConsole = signal<"Shown" | "Hidden">("Shown");
  const events = signal<string[]>([]);
  const log = (line: string) => events.set([line, ...events()].slice(0, 4));
  const round = (sizes: number[]) => sizes.map((s) => Math.round(s)).join(" · ");

  // The handles' look: a hairline that lights up under the pointer and while dragged.
  const handle = "bg-border group-hover:bg-muted-foreground group-active:bg-primary";

  const label = (title: string, detail: string) => (
    <View style={{ gap: 4 }}>
      <Text style={{ fontSize: 13, fontWeight: "600", color: ui().text }}>{title}</Text>
      <Text style={{ fontSize: 12, color: ui().muted }}>{detail}</Text>
    </View>
  );

  return (
    <View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
      <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>resizable</Text>
      <Text style={{ color: ui().muted }}>
        {desktop ? "Drag a line between two panels" : "Drag a line between two panels with your finger"}
      </Text>

      <Split
        grow
        width={640}
        inspector={
          <>
            <Panel title="sizes">
              <Text style={{ fontSize: 13, fontVariant: ["tabular-nums"], color: ui().text }}>
                {`sidebar ${Math.round(sidebar())} · console ${Math.round(consoleSize())}`}
              </Text>
            </Panel>
            <Segmented label="console" options={["Shown", "Hidden"] as const} value={showConsole()} onChange={(v) => showConsole.set(v)} />
            <Log title="onResize" lines={events()} minHeight={96} />
          </>
        }
      >
        <View style={{ height: wide() ? 360 : 300, ...ui().card, borderRadius: 14, overflow: "hidden" }}>
          <ResizablePanelGroup
            axis="horizontal"
            handleClass={handle}
            handleAccessibilityLabel="Resize the sidebar"
            onResize={({ sizes }) => log(`row: ${round(sizes)}`)}
          >
            <ResizablePanel size={sidebar()} minSize={100} maxSize={320} onResize={({ size }) => sidebar.set(size)}>
              <View style={{ flex: 1, padding: 14, gap: 10, backgroundColor: ui().inset.backgroundColor }}>
                {label("Files", `${Math.round(sidebar())} pt, 100 to 320`)}
              </View>
            </ResizablePanel>
            <ResizablePanel minSize={120}>
              <ResizablePanelGroup
                axis="vertical"
                handleClass={handle}
                handleAccessibilityLabel="Resize the console"
                onResize={({ sizes }) => log(`column: ${round(sizes)}`)}
              >
                <ResizablePanel minSize={80}>
                  <View style={{ flex: 1, padding: 14 }}>{label("Editor", "Takes the room left")}</View>
                </ResizablePanel>
                <ResizablePanel
                  size={consoleSize()}
                  minSize={60}
                  fixed
                  visible={showConsole() === "Shown"}
                  onResize={({ size }) => consoleSize.set(size)}
                >
                  <View style={{ flex: 1, padding: 14 }}>{label("Console", "Fixed: keeps its height")}</View>
                </ResizablePanel>
              </ResizablePanelGroup>
            </ResizablePanel>
          </ResizablePanelGroup>
        </View>
      </Split>
    </View>
  );
}

mount(App);
use nsh_base::{h_resizable, resizable_panel, theme};
use nsh_core::prelude::*;

fn workspace() -> impl IntoElement {
    h_resizable()
        .handle_style(|s| s.bg(move || theme().border()))
        .handle_accessibility_label("Resize the sidebar")
        .on_resize(|sizes: &Vec<Points>| log::info!("sizes: {sizes:?}"))
        .child(
            resizable_panel()
                .size(px(240.0))
                .size_range(px(160.0)..px(480.0))
                .p_4()
                .child(text("Files").text_color(move || theme().muted_foreground())),
        )
        .child(
            resizable_panel()
                .p_4()
                .child(text("Editor").text_color(move || theme().foreground())),
        )
}

ResizablePanelGroup e ResizablePanel vêm de nsh/base, o crate nsh-base em Rust. Os painéis são os filhos diretos do group. Em Rust, h_resizable() e v_resizable() criam um group em cada eixo (resizable_panel_group().axis(..) recebe o eixo como valor) e resizable_panel() cria um painel, com um setter por prop, um listener por evento, os métodos de estilo tipados e .child(..) para o conteúdo; .size_range(min..max) define os dois limites de uma vez.

#Tamanhos

O size de um painel é o tamanho que ele deseja, dentro de seu minSize e seu maxSize. Os painéis sem tamanho dividem igualmente o que sobra, seja qual for o conteúdo; quando todos os painéis têm um tamanho, o espaço excedente é dividido em proporção a eles. Enquanto o usuário arrasta uma alça, o group move o espaço entre os painéis no core, sem JS no caminho:

  • Aumentar o painel anterior à alça tira espaço dos painéis posteriores, do mais próximo ao mais distante, cada um até o seu minSize, pulando os que são fixed.
  • Reduzi-lo abaixo de seu minSize tira espaço dos painéis anteriores.
  • Nenhum painel cresce além de seu maxSize, e a soma dos painéis nunca ultrapassa o group.

Quando um arrasto termina tendo alterado os tamanhos, o group informa o tamanho de todos os painéis visíveis, na ordem em que estão dispostos (onResize no group), e cada painel cujo tamanho mudou informa o seu próprio (onResize no painel). Os painéis que um arrasto moveu mantêm os tamanhos do usuário até que o app passe um novo size; o app os preserva além da vida do group devolvendo os tamanhos informados como size, de modo que um layout restaurado do armazenamento abre com os tamanhos em que o usuário o deixou. Apenas arrastos e ajustes de leitores de tela geram notificações: uma janela redimensionada ou um painel exibido ou oculto não notifica nada.

Quando o group cresce ou diminui (a janela é redimensionada), os painéis com tamanho o mantêm e os painéis sem tamanho absorvem a diferença. Quando o group é pequeno demais para os tamanhos, os painéis encolhem, exceto os fixed (uma barra lateral, um inspetor), e voltam aos seus tamanhos quando o espaço retorna. Um painel com visible={false} não ocupa espaço: os outros dividem o seu lugar, e ele volta ao seu tamanho quando é exibido de novo.

<ResizablePanelGroup axis="vertical" handleClass="bg-border">
  <ResizablePanel>
    <Editor />
  </ResizablePanel>
  <ResizablePanel size={180} minSize={120} fixed visible={consoleOpen()}>
    <Console />
  </ResizablePanel>
</ResizablePanelGroup>

#Alças

Há uma alça em cada fronteira entre dois painéis visíveis: uma faixa de 9 pontos de espessura no sentido transversal ao eixo, centralizada na fronteira, com o cursor de redimensionamento do seu eixo. Em telas sensíveis ao toque, a área de toque se estende por mais 10 pontos para cada lado, de modo que mesmo uma linha de um ponto continua fácil de agarrar. A alça é um group: o que ela desenha é o indicador do app, estilizado com handleClass / handleStyle: 1 ponto de espessura e todo o comprimento da fronteira, então bg-border sozinho é uma linha fina em qualquer eixo, w-[3px] ou h-[3px] a engrossa, e suas classes group-hover: e group-active: acompanham o ponteiro sobre a faixa e o toque ou arrasto. Para mais do que uma linha (uma pegada de pontos, um ícone), renderHandle renderiza o conteúdo do indicador, recebendo index, axis e state da alça; ele é desenhado de novo quando o estado muda, algumas vezes por arrasto, nunca a cada movimento. Uma pegada animada é o thumb da alça (handleThumbClass / handleThumbStyle): uma view centralizada na linha, desenhada na vertical e girada um quarto de volta em uma linha horizontal, para que uma única classe sirva aos dois eixos, e que as classes group-hover: e group-active: exibem e fazem a transição; a alça permanece pressionada durante todo o arrasto, para onde quer que o ponteiro vá.

<ResizablePanelGroup
  handleClass="w-[3px] items-center justify-center bg-border"
  renderHandle={(handle) => (
    <View class={handle.state === "dragging" ? "h-8 w-1 rounded-full bg-primary" : "h-6 w-1 rounded-full bg-muted-foreground"} />
  )}
>
  {/* panels */}
</ResizablePanelGroup>

disabled mantém todas as alças onde estão (um layout travado).

#Props

#ResizablePanelGroup

Prop Rust Tipo Padrão Descrição
axis h_resizable(), v_resizable(), .axis(..) "horizontal" | "vertical" "horizontal" Lado a lado (as alças são arrastadas para a esquerda e a direita) ou empilhados (para cima e para baixo).
disabled .disabled(..) boolean false Sem arrastos: as alças permanecem onde estão.
handleClass, handleStyle .handle_style(|s| ..) string, StyleProp<ViewStyle> nenhum O indicador de cada alça, na fronteira, dentro da faixa da alça: 1 ponto de espessura e todo o comprimento da fronteira, a menos que se diga o contrário (w-[3px], h-[3px]); group-hover: e group-active: acompanham a faixa.
handleThumbClass, handleThumbStyle .handle_thumb_class(..), .handle_thumb_style(|s| ..) string, StyleProp<ViewStyle> nenhum O thumb de cada alça: uma pegada centralizada na linha, desenhada na vertical como em uma linha vertical e girada um quarto de volta em uma horizontal, para que uma única classe sirva aos dois eixos. Ele continua sendo a mesma view enquanto a alça recebe hover, é pressionada e arrastada, então as classes group-hover: e group-active: fazem a transição dele (h-11 w-[3px] rounded-full bg-muted-foreground opacity-0 scale-y-0 transition group-hover:opacity-40 group-hover:scale-y-[0.45] group-active:opacity-90 group-active:scale-y-100).
renderHandle .render_handle(|handle| ..) (handle: ResizeHandleContext) => JSX.Element nenhum O que há dentro de cada indicador, desenhado de novo quando o state da alça muda.
handleAccessibilityLabel .handle_accessibility_label(..) string nenhum O que os leitores de tela dizem para cada alça: o que ela redimensiona. Eles a ajustam de 10 em 10 pontos.
children .child(..) JSX.Element nenhum Os ResizablePanels, seus filhos diretos.
class, style métodos de estilo string, StyleProp<ViewStyle> size-full O próprio group; ele preenche o pai, a menos que sua classe diga o contrário.

#ResizablePanel

Prop Rust Tipo Padrão Descrição
size .size(..) number nenhum O comprimento desejado ao longo do eixo do group, em pontos, dentro de minSize e maxSize; um novo valor o redimensiona. Sem um valor, ele divide o que sobra.
minSize .min_size(..), .size_range(..) number 100 O menor tamanho que ele assume.
maxSize .max_size(..), .size_range(..) number sem limite O maior tamanho que ele assume.
fixed .fixed(..) boolean false Mantém seu tamanho quando o group é pequeno demais para os painéis; arrastar a própria alça ainda o redimensiona. Recebe um size.
visible .visible(..) boolean true Oculto, ele não ocupa espaço e os outros dividem o seu lugar.
children .child(..) JSX.Element nenhum Seu conteúdo.
class, style métodos de estilo string, StyleProp<ViewStyle> nenhum A superfície do próprio painel. Seu tamanho ao longo do eixo é definido pelo group.

#ResizeHandleContext

Campo Tipo Descrição
index number A fronteira após o indexº painel visível.
axis "horizontal" | "vertical" O eixo ao longo do qual ela redimensiona.
state "idle" | "hovered" | "pressed" | "dragging" A distância que o ponteiro ou o dedo percorreu com ela.

#Eventos

Evento Rust Descrição
onResize (ResizablePanelGroup) .on_resize(|sizes: &Vec<Points>| ..) Um arrasto terminou e moveu uma alça, ou um leitor de tela ajustou uma: sizes, o tamanho de cada painel visível, na ordem em que estão dispostos (da esquerda para a direita, de cima para baixo).
onResize (ResizablePanel) .on_resize(|size: &Points| ..) Um arrasto terminou e alterou o tamanho deste painel: size, o novo tamanho, para devolver como seu size.

#Partes

Parte Dentro de Estilizada com Estados
O group ResizablePanelGroup de class, style
Um painel o group ResizablePanel de class, style
O indicador de uma alça a faixa da alça, em uma fronteira handleClass, handleStyle, renderHandle group-hover: sob um ponteiro, group-active: enquanto pressionada ou arrastada; state em renderHandle
O thumb de uma alça centralizado na linha de uma alça handleThumbClass, handleThumbStyle group-hover:, group-active: (pressionado até o fim do arrasto)

#Acessibilidade

  • Cada alça é um elemento ajustável para leitores de tela, com handleAccessibilityLabel como rótulo. Seu valor é o tamanho do painel anterior, dentro de minSize e maxSize desse painel.
  • Deslizar para cima ou para baixo sobre uma alça com o VoiceOver, ou usar as teclas de volume do TalkBack, a move 10 pontos, como um arrasto faria; o group notifica o movimento com onResize. As alças de um group disabled permanecem onde estão.
  • O conteúdo dos painéis é do app e é lido como em qualquer outro lugar.

Veja Acessibilidade para rótulos, papéis e anúncios em geral.

#Notas de plataforma

  • Desktops (macOS, Windows, Linux, GPUI) e web: o ponteiro sobre uma alça mostra o cursor de redimensionamento de coluna ou de linha, e um arrasto com o mouse ou o touchpad move a fronteira.
  • iOS, Android e Mac Catalyst: um dedo arrasta a alça, dentro de uma área de toque 10 pontos mais larga de cada lado do que a faixa.
  • Em layouts da direita para a esquerda, os painéis e seus tamanhos seguem a ordem do layout.
  • Todas as plataformas desenham o que o app estiliza: os indicadores e os painéis.
  • As divisões do Dock são groups redimensionáveis: elas são redimensionadas da mesma forma.