एलिमेंट
Collapsible
Collapsible एक ऐसा region है जिसकी सामग्री फैलती और सिमटती है: summary के नीचे details, “और दिखाएँ”, या advanced settings का कोई सेक्शन। यह अपने children को हमेशा render करता है और open होने पर उनके बाद अपना content render करता है। open का मालिक ऐप है, और उसे पलटने वाला trigger भी (जो children में से एक होता है) ऐप का ही है। इसका रूप-रंग भी ऐप तय करता है: Collapsible का अपना कोई trigger, chevron या label नहीं होता। reveal के साथ, सामग्री motion के साथ खुलती और बंद होती है: उसके region की ऊँचाई शून्य से सामग्री की अपनी ऊँचाई तक और वापस spring होती है, और collapsible के बाद आने वाली चीज़ें साथ खिसकती हैं।
import { type JSX, mount, signal, Text, View } from "nsh";
import { Collapsible, Switch } from "nsh/base";
import { desktop, focusRing, hoverFill, Log, Panel, size, Split, ui } from "./palette";
// Each section: a summary row, the app's trigger (it says whether it is
// expanded to screen readers), and details the collapsible reveals while the
// app has it open: its height springs open and shut (`reveal`), and the
// sections below move with it. The chevron turns with a transition of the
// app's. One inset per card, leading and trailing: the summary's chevron
// lane is as wide as the turned chevron, so it ends where the values below
// it end.
// The pointer's hover and the keyboard's focus ring come from the palette;
// the reveal, the chevron and the switch stay still with reduced motion.
const pad = desktop ? "px-3" : "px-4";
function Section(props: { label: string; detail: string; open: boolean; onToggle: () => void; content: JSX.Element }) {
return (
<Collapsible open={props.open} reveal content={props.content}>
<View
role="button"
accessibilityState={{ expanded: props.open }}
onPress={props.onToggle}
hoverStyle={hoverFill()}
focusStyle={focusRing()}
class={`flex-row items-center gap-3 ${pad} active:opacity-70`}
style={{ minHeight: size.row + 8 }}
>
<View style={{ flex: 1, gap: 2 }}>
<Text style={{ fontSize: size.label, fontWeight: "500", color: ui().text }}>{props.label}</Text>
<Text style={{ fontSize: size.detail, color: ui().muted }}>{props.detail}</Text>
</View>
<View class="h-5 w-2 items-center justify-center transition-transform duration-150 motion-reduce:transition-none" style={{ rotate: props.open ? "90deg" : "0deg" }}>
<Text style={{ fontSize: 18, color: ui().muted }}>›</Text>
</View>
</View>
</Collapsible>
);
}
const USAGE: [string, string][] = [
["Photos", "120 GB"],
["Apps", "54 GB"],
["System", "38 GB"],
];
function App() {
const storage = signal(true);
const advanced = signal(false);
const originals = signal(false);
const events = signal<string[]>([]);
const log = (line: string) => events.set([line, ...events()].slice(0, 4));
const toggle = (name: string, open: { (): boolean; set: (v: boolean) => void }) => () => {
open.set(!open());
log(`${name} → ${open() ? "open" : "closed"}`);
};
const line = () => <View style={{ height: 1, marginLeft: desktop ? 12 : 16, backgroundColor: ui().line }} />;
return (
<View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
<Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>collapsible</Text>
<Text style={{ color: ui().muted }}>{desktop ? "Click a summary, or Tab to it and press Space" : "Tap a summary to show its details"}</Text>
<Split
gap={14}
inspector={
<>
<Panel title="open">
<Text style={{ fontSize: 13, color: ui().text }}>
Storage {storage() ? "open" : "closed"} · Advanced {advanced() ? "open" : "closed"}
</Text>
<Text style={{ fontSize: 12, color: ui().muted }}>Keep originals {originals() ? "on" : "off"}: kept while collapsed</Text>
</Panel>
<Log title="onPress (trigger) → open" lines={events()} minHeight={92} />
</>
}
>
<View style={{ ...ui().card, borderRadius: 16, overflow: "hidden" }}>
<Section
label="Storage"
detail="212 GB of 256 GB used"
open={storage()}
onToggle={toggle("Storage", storage)}
content={
<View class={`${pad} pb-2`}>
{USAGE.map(([name, value]) => (
<View class="flex-row items-center" style={{ minHeight: size.rowSm }}>
<Text style={{ flex: 1, fontSize: size.label, color: ui().muted }}>{name}</Text>
<Text style={{ fontSize: size.label, color: ui().muted, fontVariant: ["tabular-nums"] }}>{value}</Text>
</View>
))}
</View>
}
/>
{line()}
<Section
label="Advanced"
detail="Options most people leave alone"
open={advanced()}
onToggle={toggle("Advanced", advanced)}
content={
<Switch
checked={originals()}
onChange={({ checked }) => {
originals.set(checked);
}}
class={`flex-row-reverse items-center justify-between gap-3 ${pad} pb-3 active:opacity-70`}
hoverStyle={hoverFill()}
focusStyle={focusRing()}
trackClass={`${desktop ? "w-9" : "w-12"} rounded-full p-0.5 transition-colors motion-reduce:transition-none`}
trackStyle={originals() ? ui().accentFill : ui().inset}
thumbClass={desktop ? "size-4 rounded-full transition-transform motion-reduce:transition-none checked:translate-x-4" : "size-5.5 rounded-full transition-transform motion-reduce:transition-none checked:translate-x-5"}
thumbStyle={originals() ? { backgroundColor: ui().onAccent } : ui().raised}
>
<Text style={{ flex: 1, fontSize: size.label, color: ui().text }}>Keep originals</Text>
</Switch>
}
/>
</View>
</Split>
</View>
);
}
mount(App);use nsh_base::{collapsible, theme};
use nsh_core::{AccessibilityState, prelude::*};
fn storage() -> impl IntoElement {
let open = signal(false);
collapsible()
.open(open)
.reveal(true)
.gap_2()
.child(
view()
.role(AccessibilityRole::Button)
.accessibility_state(move || AccessibilityState {
expanded: Some(open.get()),
..AccessibilityState::default()
})
.on_press(move || open.update(|o| *o = !*o))
.flex_row()
.items_center()
.justify_between()
.child(text("Storage"))
.child(text("›").transition_transform().rotate(move || {
if open.get() { deg(90.0) } else { deg(0.0) }
})),
)
.content(text("Photos 120 GB · Apps 54 GB · System 38 GB").text_color(move || theme().muted_foreground))
}import { mount, signal, Text, View } from "nsh";
import { Collapsible } from "nsh/base";
function App() {
const open = signal(false);
return (
<Collapsible
open={open()}
reveal
class="gap-2"
content={<Text class="text-muted-foreground">Photos 120 GB · Apps 54 GB · System 38 GB</Text>}
>
<View
role="button"
accessibilityState={{ expanded: open() }}
onPress={() => open.set(!open())}
class="flex-row items-center justify-between active:opacity-70"
>
<Text class="font-medium">Storage</Text>
<Text class={`transition-transform ${open() ? "rotate-90" : ""}`}>›</Text>
</View>
</Collapsible>
);
}
mount(App);Collapsible nsh/base से आता है, जो Rust में nsh-base crate है। मान ऐप का होता है: उसका trigger open को पलटता है, और collapsible वही मान दिखाता है जो ऐप उसे देता है। Rust में collapsible() हर prop के लिए एक setter (.open(..), .reveal(..), .content(..)), region के लिए typed style methods और हमेशा दिखने वाली सामग्री के लिए .child(..) के साथ एक collapsible बनाता है।
#Props
| Prop | Rust | Type | Default | विवरण |
|---|---|---|---|---|
open | .open(..) | boolean | false | फैला हुआ: content tree में है, children के बाद। मान का मालिक ऐप है। |
reveal | .reveal(..) | boolean | false | reveal के साथ खुलता और बंद होता है: सामग्री एक ऐसे region में रहती है जो उसे clip करता है, और जिसकी ऊँचाई open के चालू होने पर शून्य से सामग्री की अपनी ऊँचाई तक spring होती है और बंद होने पर वापस, critically damped (लगभग 265 ms, बिना overshoot के)। collapsible के बाद की चीज़ें उसके साथ खिसकती हैं, और region के बंद होने तक सामग्री tree में बनी रहती है। reduced motion में यह तुरंत खुलता और बंद होता है। region बंद रहने पर भी layout में रहता है, शून्य ऊँचाई के साथ, इसलिए collapsible का gap भी उससे पहले आता है। |
content | .content(..) | JSX.Element | none | जो फैलता है। यह एक बार बनता है और सिमटे रहने पर भी बना रहता है, इसलिए अंदर की चीज़ें अपनी state (input का text, scroll position, switch) एक बार खुलने से अगली बार खुलने तक बनाए रखती हैं। |
children | .child(..) | JSX.Element | none | जो दोनों स्थितियों में दिखता है: एक summary, यानी open को पलटने वाला trigger। |
class, style | style methods | string, StyleProp<ViewStyle> | none | children और सामग्री के चारों ओर का region (gap-2 उनके बीच की जगह तय करता है)। |
#Events
Collapsible अपनी ओर से कुछ report नहीं करता: trigger ऐप का है, और उसका press (role="button" वाले View का onPress) वह जगह है जहाँ ऐप open को पलटता है।
#Parts
| Part | भीतर | Styling |
|---|---|---|
| Region | class, style | |
| Children | region, सबसे पहले | उनके अपने elements |
| Reveal का region | region, children के बाद, reveal के साथ | none: यह सामग्री को clip करता है और उसकी ऊँचाई लेता है |
| सामग्री | region, children के बाद (reveal के साथ reveal के region में), open होने पर | उसके अपने elements (content) |
reveal के बिना, सामग्री तुरंत आती और जाती है, और हर प्लेटफ़ॉर्म region को तब फिर से layout करता है जब वह आती या जाती है। reveal के साथ, यह बदलाव उस commit का एक layout animation होता है जो open को बदलता है (LayoutAnimation.configureNext के रूप में, native रूप से चलता है, इसलिए motion के दौरान कोई JavaScript नहीं चलता): reveal का region शून्य से सामग्री की ऊँचाई तक बढ़ता है और वापस सिकुड़ता है, और उसी बदलाव से खिसकने वाली हर चीज़ के frames, जिनमें collapsible के बाद के views भी शामिल हैं, उसके साथ खिसकते हैं। ऐप का कोई transition साथ चलता है: trigger का chevron घूमता है (transition-transform), और सामग्री अपनी classes से fade in हो सकती है।
#एक्सेसिबिलिटी
- screen readers के लिए region अपने आप में कोई element नहीं है: उसके children और सामग्री जैसी हैं वैसी ही पढ़ी जाती हैं।
- trigger बताता है कि region फैला हुआ है या नहीं: उसे
accessibilityState={{ expanded: open }}और एक button role दें (Viewपरrole="button"), और screen readers उसके label के साथ “expanded” या “collapsed” बोलते हैं। - macOS और web पर, जब trigger presses लेता है तो वह keyboard focus का एक पड़ाव होता है: Tab उस तक पहुँचता है, Space और Return region को पलटते हैं।
labels, roles और announcements के बारे में सामान्य जानकारी के लिए एक्सेसिबिलिटी देखें।
#प्लेटफ़ॉर्म नोट्स
openहोने पर हर प्लेटफ़ॉर्म सामग्री को children के बाद दिखाता है, और अन्यथा उसे layout से बाहर रखता है।revealके साथ, हर प्लेटफ़ॉर्म region की ऊँचाई को अपने animator पर spring करता है (iOS, Android, macOS, Windows, Linux, GPUI और web), और जब तक सिस्टम reduced motion माँगता है, उसे तुरंत खोलता और बंद करता है।- iOS, Android और Mac Catalyst: trigger का press एक tap है। Desktops (macOS, Windows, Linux, GPUI) और web पर: एक click, और macOS तथा web पर focus वाले trigger पर Space या Return।