تُرجمت هذه الصفحة آليًا وقد تحتوي على أخطاء. اقرأ الأصل الإنجليزي

العناصر

Resizable

تضع المجموعة القابلة لتغيير الحجم اللوحات جنبًا إلى جنب أو فوق بعضها، مع مقبض عند كل حد بين لوحتين: شريط جانبي بجوار مستند، ومحرر فوق وحدة التحكم الخاصة به، وقائمة بجوار تفاصيلها. يسحب المستخدم المقبض لتحريك الحد، وتبقى كل لوحة ضمن الأحجام التي يسمح بها التطبيق لها. يتولى ResizablePanelGroup السلوك: السحب، الذي يعمل أصليًا إطارًا بإطار، وكيفية توزيع المساحة، ومؤشر تغيير الحجم، ومنطقة لمس أكبر للإصبع. أما المظهر فهو من مسؤولية التطبيق: المقبض شريط بلا خط أو لون أو مقبض إمساك خاص به، وما يرسمه هو مؤشر التطبيق؛ واللوحات هي واجهات التطبيق.

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 وResizablePanel من nsh/base، وهي حزمة nsh-base (crate) في Rust. اللوحات هي الأبناء المباشرون للمجموعة. في Rust، ينشئ h_resizable() وv_resizable() مجموعة على كل محور (يأخذ resizable_panel_group().axis(..) المحور كقيمة)، وينشئ resizable_panel() لوحة، مع دالة ضبط لكل خاصية، ومستمع لكل حدث، ودوال الأنماط المضبوطة الأنواع، و.child(..) لمحتواها؛ ويضبط .size_range(min..max) الحدّين معًا.

#الأحجام

size للوحة هو الحجم الذي تريده، ضمن minSize وmaxSize. اللوحات التي لا تملك حجمًا تتقاسم المتبقي بالتساوي مهما كان محتواها؛ وعندما يكون لكل لوحة حجم، تُقسَّم المساحة المتبقية بنسبتها. وأثناء سحب المستخدم لمقبض، تنقل المجموعة المساحة بين اللوحات داخل النواة، دون المرور بـ JS:

  • عند تكبير اللوحة التي قبل المقبض، تُؤخذ المساحة من اللوحات التي بعده، الأقرب أولًا، كلٌّ حتى minSize الخاص بها، مع تجاوز اللوحات fixed.
  • عند تصغيرها إلى ما دون minSize الخاص بها، تُؤخذ المساحة من اللوحات التي قبلها.
  • لا تكبر أي لوحة فوق maxSize الخاص بها، ولا يتجاوز مجموع اللوحات حجم المجموعة أبدًا.

عندما ينتهي سحب غيّر الأحجام، تبلّغ المجموعة بحجم كل لوحة مرئية، بالترتيب الذي تُرتَّب به (onResize على المجموعة)، وتبلّغ كل لوحة تغيّر حجمها بحجمها (onResize على اللوحة). تحتفظ اللوحات التي حرّكها السحب بأحجام المستخدم إلى أن يمرر التطبيق قيمة size جديدة؛ ويحتفظ التطبيق بها بعد انتهاء عمر المجموعة بتمرير الأحجام المُبلَّغ عنها مجددًا بوصفها size، بحيث يُفتح التخطيط الذي يستعيده من التخزين بالأحجام التي تركها المستخدم. لا يُبلَّغ إلا عن السحب وخطوات قارئات الشاشة: تغيير حجم النافذة أو إظهار لوحة أو إخفاؤها لا يبلّغ عن شيء.

عندما تكبر المجموعة أو تصغر (تغيير حجم النافذة)، تحتفظ اللوحات ذات الحجم بحجمها وتتحمل اللوحات التي بلا حجم الفرق. وعندما تكون المجموعة أصغر من الأحجام المطلوبة، تتقلص اللوحات، باستثناء اللوحات fixed (شريط جانبي، لوحة فحص)، وتعود إلى أحجامها عندما تعود المساحة. اللوحة التي عليها visible={false} لا تشغل أي مساحة: تتقاسم اللوحات الأخرى مساحتها، وتعود بحجمها عندما تظهر من جديد.

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

#المقابض

يوجد مقبض عند كل حد بين لوحتين مرئيتين: شريط سمكه 9 نقاط عبر المحور، متمركز على الحد، مع مؤشر تغيير الحجم الخاص بمحوره. على شاشات اللمس تمتد منطقة اللمس الخاصة به 10 نقاط إضافية على كل جانب، فيبقى الإمساك بخط سمكه نقطة واحدة سهلًا. المقبض هو group: وما يرسمه هو مؤشر التطبيق، ويُنسَّق باستخدام handleClass / handleStyle: نقطة واحدة عبر المحور وبكامل طول الحد، فيكون bg-border وحده خطًا رفيعًا على أي من المحورين، ويزيد w-[3px] أو h-[3px] من سمكه، وتتبع فئتاه group-hover: وgroup-active: المؤشر فوق الشريط والضغط أو السحب. وللحصول على أكثر من خط (مقبض إمساك من نقاط، أو أيقونة)، يعرض renderHandle محتوى المؤشر، مع إبلاغه بـ index وaxis وstate الخاصة بالمقبض؛ ويُرسم من جديد عندما تتغير الحالة، بضع مرات في كل سحب، وليس مع كل حركة. أما مقبض الإمساك المتحرك فهو إبهام المقبض (handleThumbClass / handleThumbStyle): واجهة واحدة متمركزة على الخط، تُرسم قائمة وتُدار ربع دورة على الخط الأفقي بحيث تخدم فئة واحدة المحورين معًا، وتعرضها وتنتقل بها فئتا group-hover: وgroup-active:؛ ويبقى المقبض مضغوطًا طوال السحب، أينما ذهب المؤشر.

<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 كل مقبض في مكانه (تخطيط مقفل).

#الخصائص

#ResizablePanelGroup

الخاصيةRustالنوعالافتراضيالوصف
axish_resizable(), v_resizable(), .axis(..)"horizontal" | "vertical""horizontal"جنبًا إلى جنب (تُسحب المقابض يمينًا ويسارًا) أو فوق بعضها (أعلى وأسفل).
disabled.disabled(..)booleanfalseلا سحب: تبقى المقابض في أماكنها.
handleClass, handleStyle.handle_style(|s| ..)string, StyleProp<ViewStyle>لا شيءمؤشر كل مقبض، على الحد داخل شريط المقبض: نقطة واحدة عبر المحور وبكامل طول الحد ما لم يُحدَّد غير ذلك (w-[3px], h-[3px])؛ ويتبع group-hover: وgroup-active: الشريط.
handleThumbClass, handleThumbStyle.handle_thumb_class(..), .handle_thumb_style(|s| ..)string, StyleProp<ViewStyle>لا شيءإبهام كل مقبض: مقبض إمساك متمركز على الخط، يُرسم قائمًا كما على خط عمودي ويُدار ربع دورة على الخط الأفقي، بحيث تخدم فئة واحدة المحورين. يبقى الواجهة نفسها أثناء تمرير المؤشر فوق المقبض وضغطه وسحبه، ولذلك تنتقل به فئتا group-hover: وgroup-active: (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لا شيءما بداخل كل مؤشر، ويُرسم من جديد عندما يتغير state الخاص بالمقبض.
handleAccessibilityLabel.handle_accessibility_label(..)stringلا شيءما تقوله قارئات الشاشة عن كل مقبض: ما الذي يغيّر حجمه. تضبطه بمقدار 10 نقاط في كل مرة.
children.child(..)JSX.Elementلا شيءعناصر ResizablePanel، وهي أبناؤها المباشرون.
class, styleدوال الأنماطstring, StyleProp<ViewStyle>size-fullالمجموعة نفسها؛ تملأ الأصل الخاص بها ما لم تقل فئتها غير ذلك.

#ResizablePanel

الخاصيةRustالنوعالافتراضيالوصف
size.size(..)numberلا شيءالطول الذي تريده على محور المجموعة، بالنقاط، ضمن minSize وmaxSize؛ وتؤدي القيمة الجديدة إلى تغيير حجمها. وبدونها تتقاسم المتبقي.
minSize.min_size(..), .size_range(..)number100أصغر حجم تبلغه.
maxSize.max_size(..), .size_range(..)numberبلا حدأكبر حجم تبلغه.
fixed.fixed(..)booleanfalseتحتفظ بحجمها عندما تكون المجموعة أصغر من اللوحات؛ ويظل سحب مقبضها يغيّر حجمها. تأخذ size.
visible.visible(..)booleantrueعند إخفائها لا تشغل أي مساحة وتتقاسم اللوحات الأخرى مساحتها.
children.child(..)JSX.Elementلا شيءمحتواها.
class, styleدوال الأنماطstring, StyleProp<ViewStyle>لا شيءسطح اللوحة الخاص بها. حجمها على المحور تحدده المجموعة.

#ResizeHandleContext

الحقلالنوعالوصف
indexnumberالحد الذي يلي اللوحة المرئية رقم index.
axis"horizontal" | "vertical"المحور الذي يغيّر حجمه.
state"idle" | "hovered" | "pressed" | "dragging"المسافة التي قطعها المؤشر أو الإصبع معه.

#الأحداث

الحدثRustالوصف
onResize (ResizablePanelGroup).on_resize(|sizes: &Vec<Points>| ..)انتهى سحب وحرّك مقبضًا، أو ضبط قارئ شاشة أحد المقابض: sizes، أي حجم كل لوحة مرئية، بالترتيب الذي تُرتَّب به (من اليسار إلى اليمين، ومن الأعلى إلى الأسفل).
onResize (ResizablePanel).on_resize(|size: &Points| ..)انتهى سحب وغيّر حجم هذه اللوحة: size، أي الحجم الجديد، ليُمرَّر مجددًا بوصفه size الخاص بها.

#الأجزاء

الجزءداخليُنسَّق باستخدامالحالات
المجموعةclass، style الخاصة بـ ResizablePanelGroup
لوحةالمجموعةclass، style الخاصة بـ ResizablePanel
مؤشر المقبضشريط المقبض، على حدhandleClass, handleStyle, renderHandlegroup-hover: تحت المؤشر، group-active: أثناء الضغط أو السحب؛ state في renderHandle
إبهام المقبضمتمركز على خط المقبضhandleThumbClass, handleThumbStylegroup-hover:, group-active: (مضغوط حتى ينتهي السحب)

#إمكانية الوصول

  • كل مقبض عنصر قابل للضبط بالنسبة إلى قارئات الشاشة، وتسميته handleAccessibilityLabel. وقيمته هي حجم اللوحة التي قبله، ضمن minSize وmaxSize الخاصين بتلك اللوحة.
  • يؤدي التمرير لأعلى أو لأسفل على المقبض باستخدام VoiceOver، أو مفاتيح الصوت في TalkBack، إلى تحريكه 10 نقاط، كما يفعل السحب؛ وتبلّغ المجموعة بذلك عبر onResize. وتبقى مقابض المجموعة disabled في أماكنها.
  • محتوى اللوحات من مسؤولية التطبيق، ويُقرأ كما يُقرأ في أي مكان آخر.

راجع إمكانية الوصول للاطلاع على التسميات والأدوار والإعلانات بوجه عام.

#ملاحظات حول المنصات

  • أنظمة سطح المكتب (macOS وWindows وLinux وGPUI) والويب: يُظهر المؤشر فوق المقبض مؤشر تغيير حجم العمود أو الصف، ويحرّك السحب بالفأرة أو لوحة اللمس الحد.
  • iOS وAndroid وMac Catalyst: يسحب الإصبع المقبض، ضمن منطقة لمس أعرض بمقدار 10 نقاط على كل جانب من الشريط.
  • في التخطيطات من اليمين إلى اليسار، تتبع اللوحات وأحجامها ترتيب التخطيط.
  • تعرض كل منصة ما ينسّقه التطبيق: المؤشرات واللوحات.
  • انقسامات Dock هي مجموعات قابلة لتغيير الحجم: ويُغيَّر حجمها بالطريقة نفسها.