العناصر
Link
يأخذ الرابط المستخدم إلى مكان ما: صفحة ويب، أو تطبيقًا آخر (mailto:، tel:)، أو شاشة من التطبيق نفسه. وهو يتولى السلوك: الضغطات، ومفتاحا Return وSpace أثناء امتلاكه تركيز لوحة المفاتيح، وحالة التعطيل، ودور الرابط لقارئات الشاشة. أما المظهر فمن شأن التطبيق: فـLink لا يملك لونًا ولا خطًا سفليًا ولا تسمية خاصة به، وما يظهر هو عناصره الفرعية. href يحدد وجهة الرابط؛ فهو بيانات تُستخدم عند الفتح، وليس نصًا يظهر على الشاشة.
import { mount, signal, Text, View } from "nsh";
import { Link } from "nsh/base";
import { desktop, focusRing, Icon, Log, Panel, size, Split, tint, ui } from "./palette";
// Links of the app's look: the accent with a quiet underline that fills in
// under the pointer, violet once visited (the app remembers what it
// opened), the palette's focus ring, dimmed while disabled. The color and
// the underline move with `transition-colors`, still with reduced motion.
const visited = signal(new Set<string>());
const events = signal<string[]>([]);
const route = signal("/docs/link");
const opened = ({ href }: { href: string }) => {
visited.set(new Set(visited()).add(href));
events.set([`onOpen ${href}`, ...events()].slice(0, 4));
};
function DocLink(props: { href: string; label: string; openWith?: "system" | "app"; disabled?: boolean; current?: boolean; icon?: boolean; inline?: boolean }) {
const seen = () => visited().has(props.href);
const color = () => (props.disabled ? ui().faint : props.current ? ui().text : seen() ? ui().violet : ui().accent);
return (
<Link
href={props.href}
openWith={props.openWith ?? "app"}
disabled={props.disabled}
current={props.current}
onOpen={(e: { href: string }) => {
opened(e);
if ((props.openWith ?? "app") === "app") route.set(e.href);
}}
class={`flex-row items-center gap-1 rounded-sm transition-colors motion-reduce:transition-none active:opacity-60 ${props.inline ? "align-text-bottom" : "self-start"}`}
focusStyle={focusRing(2)}
>
<Text
class="transition-colors motion-reduce:transition-none"
style={{
fontSize: props.inline ? 15 : size.label,
fontWeight: props.current ? "600" : "500",
color: color(),
textDecorationLine: props.current || props.disabled ? "none" : "underline",
textDecorationColor: tint(color(), 0.4),
}}
hoverStyle={props.disabled || props.current ? undefined : { textDecorationColor: color() }}
>
{props.label}
</Text>
{props.icon ? <Icon name="external" size={13} tint={color()} /> : null}
</Link>
);
}
function App() {
const crumb = (href: string, label: string) => <DocLink href={href} label={label} current={route() === href} />;
const slash = () => <Text style={{ color: ui().faint, fontSize: size.label }}>/</Text>;
return (
<View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
<Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>link</Text>
<Text style={{ color: ui().muted }}>{desktop ? "Click a link, or Tab to one and press Return" : "Tap a link"}</Text>
<Split
gap={14}
inspector={
<>
<Panel title="route · visited">
<Text style={{ fontSize: 13, color: ui().text }}>
{route()} · {visited().size} visited
</Text>
</Panel>
<Log title="onOpen({ href })" lines={events()} minHeight={110} />
</>
}
>
<View style={{ ...ui().card, borderRadius: 16, padding: desktop ? 16 : 18, gap: 14 }}>
<View style={{ flexDirection: "row", alignItems: "center", flexWrap: "wrap", gap: 6 }}>
{crumb("/docs", "Docs")}
{slash()}
{crumb("/docs/elements", "Elements")}
{slash()}
{crumb("/docs/link", "Link")}
</View>
<Text style={{ fontSize: 15, lineHeight: 22, color: ui().text }}>
{"Read the "}
<DocLink href="/docs/release-notes" label="release notes" inline />
{" and the "}
<DocLink href="/docs/migration" label="migration guide" inline />
{" before you update."}
</Text>
<View style={{ height: 1, backgroundColor: ui().line }} />
<View style={{ gap: 10 }}>
<DocLink href="https://example.com/" label="example.com" openWith="system" icon />
<DocLink href="/docs/archive" label="Archived docs" disabled />
</View>
</View>
</Split>
</View>
);
}
mount(App);use nsh_base::{link, theme};
use nsh_core::prelude::*;
fn docs() -> impl IntoElement {
link()
.href("https://example.com/docs")
.text_color(move || theme().primary)
.underline()
.hover(|s| s.opacity(0.8))
.focus(|s| s.rounded_sm().outline(px(2.0)).outline_color(move || theme().ring))
.child(text("Read the docs"))
}import { mount, Text } from "nsh";
import { Link } from "nsh/base";
function App() {
return (
<Link
href="https://example.com/docs"
class="self-start text-primary underline hover:opacity-80 focus:rounded-sm focus:outline-2 focus:outline-ring"
>
<Text>Read the docs</Text>
</Link>
);
}
mount(App);يأتي Link من nsh/base، وهي الحزمة nsh-base في Rust. في Rust، ينشئ link() رابطًا بدالة ضبط لكل خاصية (.href(..)، .open_with(..))، ومستمعٍ لكل حدث (.on_open(..)، .on_press(..))، ودوال الأنماط المضبوطة الأنواع للرابط، و.child(..) لمحتواه.
#الفتح
الضغطة أو النقرة، أو Return أو Space على الرابط الذي يملك التركيز، تفتح href الخاص به:
- يُبلّغ
onOpen({ href })عن وجهة الرابط. - مع
openWith="system"(الافتراضي)، يُفتح عنوان URL لتطبيق آخر أو موقع آخر، أي عنوان له مخطط (https:،mailto:،tel:، أو مخطط سجّله تطبيق آخر)، خارج التطبيق، كما يفتحهopenUrl: يتناوله أولًا موجّه التطبيق المضيف، ثم النظام. أما أيhrefآخر، كمسار من التطبيق (/orders/42) أو#anchor، فيُبلَّغ عنه ويبقى في عهدة التطبيق. - يُبلّغ
onPress()عن الضغطة.
مع openWith="app"، يكون معالج onOpen في التطبيق هو الذي يفتح الرابط: مسار خاص به، أو صفحة في عرض ويب داخل التطبيق، أو تأكيد أولًا.
import { Link } from "nsh/base";
<Link href={`/orders/${order.id}`} openWith="app" onOpen={({ href }) => router.push(href)} class="text-primary">
<Text>Order #{order.id}</Text>
</Link>
يتطلب فتح عنوان URL خارج التطبيق صلاحية openUrl عندما يصرّح التطبيق بالصلاحيات في nsh.json ("openUrl": true، أو { "schemes": ["https", "mailto"] }).
#الخصائص
| الخاصية | Rust | النوع | الافتراضي | الوصف |
|---|---|---|---|---|
href | .href(..) | string | لا شيء | وجهة الرابط: عنوان URL، أو مسار من التطبيق. يُبلَّغ عنه عبر onOpen ولا يُعرض أبدًا. |
openWith | .open_with(LinkOpenWith::App) | "system" | "app" (LinkOpenWith) | "system" | من يفتح href: النظام يفتح عنوان URL لتطبيق آخر أو موقع آخر؛ و"app" يترك الأمر إلى onOpen. |
disabled | .disabled(..) | boolean | false | لا ضغطات، ولا استجابة بصرية للضغط، ولا تركيز لوحة مفاتيح؛ وتُطبَّق أنماط disabled:، وتذكر قارئات الشاشة أنه معتّم. |
current | .current(..) | boolean | false | الرابط إلى الصفحة التي يتصفحها المستخدم، في قائمة تنقل أو مسار تنقل: تذكر قارئات الشاشة أنها الصفحة الحالية. |
accessibilityLabel | .accessibility_label(..) | string | لا شيء | ما تنطق به قارئات الشاشة، عندما يقول المحتوى القليل جدًا (أيقونة). |
hitSlop | .hit_slop(..) | number | Partial<Edges> | لا شيء | المسافة خارج حدوده التي تظل الضغطات فيها محتسبة، بالنقاط: رقم واحد لكل الجهات، أو تسمية الجهات. كما في hitSlop الخاص بالعرض. |
children | .child(..) | JSX.Element | لا شيء | المحتوى: تسمية، أو أيقونة، أو أي شيء يعرضه التطبيق. |
class، style | دوال الأنماط | string، StyleProp<ViewStyle> | لا شيء | مظهر الرابط: لونه وخطه السفلي وحشوه؛ وتنتقل خصائص النص إلى النصوص الموجودة بداخله. |
activeStyle، hoverStyle، focusStyle، disabledStyle | .active(|s| ..)، .hover(..)، .focus(..)، .disabled_style(..) | StyleProp<ViewStyle> | لا شيء | نمط الرابط أثناء الضغط عليه أو تمرير المؤشر فوقه أو تركيزه أو تعطيله، كفئات <state>:. |
#الأحداث
| الحدث | Rust | الوصف |
|---|---|---|
onOpen | .on_open(|href: &SharedString| ..) | الرابط يفتح href: مع openWith="app" يفتحه المعالج، ومع "system" يعرف المعالج ما طُلب من النظام فتحه. ويأتي قبل onPress. |
onPress | .on_press(|| ..) | تم الضغط على الرابط: نقرة لمسية، أو نقرة بالفأرة، أو Return أو Space أثناء امتلاكه تركيز لوحة المفاتيح. |
الرابط المعطّل لا يُبلّغ عن شيء، ولا تصل الضغطة عليه إلى أي عرض قابل للضغط يحيط به.
#الحالات
يضبط التطبيق نمط كل حالة بالفئات أو بخصائص النمط:
<Link
href="https://example.com/changelog"
class="self-start text-primary underline decoration-primary/40 transition-colors hover:decoration-primary active:opacity-70 focus:outline-2 focus:outline-ring disabled:text-muted-foreground disabled:no-underline"
>
<Text>Changelog</Text>
</Link>
الرابط الذي سبقت زيارته يتولى التطبيق تذكّره: فهو يحتفظ بقيم href التي فتحها، ويضبط نمط تلك الروابط وفق حالته الخاصة.
const visited = signal(new Set<string>());
<Link
href={href}
onOpen={({ href }) => visited.set(new Set(visited()).add(href))}
class={visited().has(href) ? "text-violet-600 underline" : "text-primary underline"}
>
<Text>{label}</Text>
</Link>
#داخل النص
يُعدّ Link داخل <Text> صندوقًا مضمّنًا في الفقرة، شأنه شأن كل عرض داخل نص: فهو يقع ضمن السطر، ويحتفظ بحالاته وتركيزه ودوره، ويبقى على سطر واحد. ويضع align-text-bottom الرابط الذي يحتوي سطرًا واحدًا من النص على خط أساس الفقرة:
<Text class="text-[15px] text-foreground">
By continuing you accept the{" "}
<Link href="https://example.com/terms" class="align-text-bottom">
<Text class="text-[15px] text-primary underline">terms of service</Text>
</Link>
.
</Text>
أما الرابط الذي ينتقل مع أسطر الفقرة فهو <Text> متداخل مع role="link" وonPress (راجع Text)، كما يرسم TextView روابط المستند.
#هدف صغير مع hitSlop
قد يكون الرابط أصغر من الإصبع: أيقونة، أو كلمة قصيرة. يجعل hitSlop الضغطات التي تقع قربه محتسبة، بالنقاط خلف كل جهة، دون تغيير تخطيطه ولا مظهره: رقم واحد لكل الجهات، أو تسمية الجهات ({ top: 8, bottom: 8 })، كما يقبلها hitSlop الخاص بالعرض.
<Link href="https://example.com/help" hitSlop={12} accessibilityLabel="Help" class="size-5 items-center justify-center">
<Text>?</Text>
</Link>
#إمكانية الوصول
- الرابط عنصر واحد لقارئات الشاشة، بالدور
"link"؛ وتسميته هي نص عناصره الفرعية، أوaccessibilityLabel. ويبقىhrefبيانات فقط: فقارئات الشاشة تنطق التسمية. - يُنطق
currentعلى أنه الصفحة الحالية (aria-current="page"في ARIA)، ويُنطقdisabledكما تنطق المنصة العنصر المعتّم. - على macOS والويب يكون الرابط إحدى محطات تركيز لوحة المفاتيح: يصل إليه Tab، ويفتحه Return وSpace، وتُظهر أنماط
focus:موضع التركيز.
راجع إمكانية الوصول للاطلاع على التسميات والأدوار والإعلانات بوجه عام.
#ملاحظات المنصات
- iOS وAndroid وMac Catalyst: الضغطة هي لمسة تنتهي على الرابط.
- أنظمة سطح المكتب (macOS وWindows وLinux وGPUI) والويب: الضغطة هي نقرة. وعلى macOS والويب، يفتح Return وSpace أيضًا الرابط الذي يملك تركيز لوحة المفاتيح.
- يعرض الويب الرابط كعنصر له
role="link". - ترسم كل منصة ما يضبطه التطبيق من أنماط.