البطاقة
تفصل مجموعة من المحتوى المترابط عن خلفيتها، ويمكن أن تكون إجراء واحدًا يفتح ما تعرضه أو رابطًا واحدًا إليه.
- الفئة
- الاحتواء
- الرموز
comp.card- المستوى
- أساسي
- Web
tanrei-cardschedule - SwiftUI
TanreiCardschedule - Jetpack Compose
TanreiCardschedule - Flutter
TanreiCardschedule - React Native
Cardschedule
مولَّد من docs/status/status.json, specs/card.spec.json
الطُرُز
تأتي البطاقة في ثلاث هيئات. البطاقة البسيطة حاوية: يحتفظ محتواها بدلالاته ويجوز أن يضم أزرارًا وروابط. والبطاقة التفاعلية زر واحد. وبطاقة الرابط، أي البطاقة التي لها href، رابط واحد. استخدم بطاقة الرابط حين يأخذ تفعيلُ البطاقة الشخصَ إلى صفحة أخرى أو عنوان آخر، والبطاقة التفاعلية حين يحدث شيء في المكان الذي هو فيه.
عيّنتا Web و React تعرضان المواصفة الحالية. أما SwiftUI و Compose و Flutter و React Native فمتأخرة: تقدّم البطاقة البسيطة والتفاعلية، ولا بطاقة رابط فيها بعد. وعيّناتها تعرض ما تقدّمه فعلًا.
<!-- بطاقة ليست إجراء: يحتفظ محتواها بدلالاته ويجوز أن يضم أزرارًا. -->
<tanrei-card>
<h3 class="tanrei-text-heading-5">التقرير الفصلي</h3>
<p class="tanrei-text-body">نمت الإيرادات 12% عن الربع السابق.</p>
<tanrei-button variant="text">مشاركة</tanrei-button>
</tanrei-card>
<!-- إجراء واحد: البطاقة كلها زر، تسمّيه label. لا شيء تفاعليًّا في داخلها. -->
<tanrei-card variant="elevated" interactive label="التقرير الفصلي">…</tanrei-card>
<!-- رابط واحد: مع href تكون البطاقة كلها عنصر anchor، تسمّيه label. لا شيء تفاعليًّا في داخلها. -->
<tanrei-card href="/reports/q3/" label="التقرير الفصلي">…</tanrei-card>
<!-- الرابط الذي يفتح تبويبًا جديدًا يقول ذلك بكلماتك. البطاقة لا ترسم علامة: ضع الرسم في محتواك. -->
<tanrei-card href="https://example.com/reports/q3.pdf" target="new" target-label="يُفتح في تبويب جديد" label="التقرير الفصلي">
<h3 class="tanrei-text-heading-5">التقرير الفصلي</h3>
<span class="tanrei-icon" aria-hidden="true">open_in_new</span>
</tanrei-card>
<!-- القيمة الافتراضية لـ inset هي true، فسمتها هي العكس: flush تدع الصورة تبلغ الحواف. -->
<tanrei-card flush>
<img src="cover.jpg" alt="" />
<div style="padding: var(--tanrei-comp-card-container-padding)">…</div>
</tanrei-card>
<script type="module">
import '@tanrei/web/components/card';
document.querySelector('[interactive]').addEventListener('click', openReport);
// بطاقة الرابط تُصدر click قبل أن يتبع المتصفح العنوان. والموجِّه من جهة العميل يلغيه.
document.querySelector('tanrei-card[href]').addEventListener('click', (event) => {
event.preventDefault();
router.push(event.currentTarget.href);
});
</script>import { Card } from '@tanrei/react';
<Card variant="filled">
<h3 className="tanrei-text-heading-5">التقرير الفصلي</h3>
<p className="tanrei-text-body">نمت الإيرادات 12% عن الربع السابق.</p>
</Card>
<Card interactive label="التقرير الفصلي" onClick={openReport}>…</Card>
<Card href="/reports/q3/" label="التقرير الفصلي">…</Card>
<Card href="https://example.com/reports/q3.pdf" target="new" targetLabel="يُفتح في تبويب جديد" label="التقرير الفصلي">…</Card>
<Card inset={false}>…</Card>import Tanrei
TanreiCard(variant: .filled) {
Text("التقرير الفصلي")
Text("نمت الإيرادات 12% عن الربع السابق.")
}
// المُهيِّئ الذي يأخذ action: هو البطاقة التفاعلية.
TanreiCard(label: Text("التقرير الفصلي"), action: openReport) { ReportSummary() }
.disabled(isLoading)import io.github.chnotchy.tanrei.components.TanreiButton
import io.github.chnotchy.tanrei.components.TanreiCard
import io.github.chnotchy.tanrei.primitives.TanreiText
import io.github.chnotchy.tanrei.tokens.TanreiCardVariant
// من دون onClick ليست البطاقة إجراء، فيجوز أن تضم أزرارًا.
TanreiCard(variant = TanreiCardVariant.Filled) {
TanreiText("التقرير الفصلي")
TanreiText("نمت الإيرادات 12% عن الربع السابق.")
TanreiButton(text = "مشاركة", onClick = ::share)
}
// الصيغة التي لها onClick إجراء واحد. و enabled = false تعطّلها.
TanreiCard(onClick = ::openReport, contentDescription = "التقرير الفصلي") {
TanreiText("التقرير الفصلي")
TanreiText("نمت الإيرادات 12% عن الربع السابق.")
}import 'package:tanrei/tanrei.dart';
TanreiCard(
variant: TanreiCardVariant.filled,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const Text('التقرير الفصلي'),
const Text('نمت الإيرادات 12% عن الربع السابق.'),
TanreiButton(onPressed: share, child: const Text('مشاركة')),
],
),
);
// القيمة غير null لـ onTap تجعل البطاقة كلها إجراء واحدًا؛ و enabled: false تعطّلها.
TanreiCard(
onTap: openReport,
semanticLabel: 'التقرير الفصلي',
child: const ReportSummary(),
);import { Text } from 'react-native';
import { Card, useContentColor } from '@tanrei/react-native';
// السلسلة النصية المجرّدة تُرسم بنمط المتن وبلون محتوى البطاقة.
<Card variant="filled">نمت الإيرادات 12% عن الربع السابق.</Card>
// مع onPress تكون البطاقة إجراء واحدًا، يسمّيه عنوانها.
<Card accessibilityLabel="التقرير الفصلي" onPress={openReport}>
<ReportSummary />
</Card>
// نصك أنت يقرأ لون محتوى البطاقة: ليس في React Native لون نص موروث.
function ReportSummary() {
const color = useContentColor();
return <Text style={{ color }}>نمت الإيرادات 12% عن الربع السابق.</Text>;
}بلا حشو، وتفاعلية، ومعطَّلة
بطاقات الرابط
البنية
البنية
outlined كما ترسمه حزمة الويب.| الجزء | الوصف |
|---|---|
1 container | The shape that holds the content, clips it to its corners and, for an interactive card, carries hover, focus and press. |
2 outline · اختياري | The border of the outlined variant. |
3 content | Whatever the product puts in the card. The card does not lay it out; it only insets it, clips it and hands it a content color. |
4 focus-ring · اختياري | Gap-then-ring indicator shown for keyboard focus of an interactive card. |
الخصائص
| الخاصية | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|
variant | elevated | filled | outlined | "outlined" | How the card separates from its background: by depth (elevated), by tone (filled) or by an edge (outlined). Pick by the background, not by importance. A design system may add values. |
interactive | boolean | false | Makes the whole card one pressable action (it emits click). Its content must then contain no other interactive element. A card whose action is to go somewhere takes href instead and is a link, not a button. |
disabled | boolean | false | Prevents interaction and removes an interactive card or a link card from the focus order. Ignored unless interactive is true or href is set: a card that is neither an action nor a link cannot be disabled. |
href | string | "" | The destination: an address as the platform's links take one (a URL; on the web a path or a fragment as well). Not empty, it makes the whole card one link — a link card — whether interactive is set or not: the link role, one tab stop, activated by pointer, touch or Enter and not by Space, opened by the platform as its own links are, and drawn exactly as an interactive card is (CRD-19, CRD-20). Its content must then contain no other interactive element, as an interactive card's. |
target | same | new | "same" | Where the destination opens. same: where the person is — the same tab on the web, the app's own handling of the address elsewhere. new: a new context — a new tab or window on the web, outside the current screen elsewhere — which the card says through targetLabel; the card draws no mark, because its content is the product's (CRD-21). Ignored without href. |
targetLabel | string | "" | What assistive technology reads after the link's name while target is new ("opens in a new tab"). Tanrei ships no words of its own, so the product gives them in its own language, as it gives a chip its removeLabel. Required while target is new; ignored otherwise. |
inset | boolean | true | Insets the content by the card's padding. Set it to false for content that reaches the card's edges (a photo on top); that content pads its own text. |
label | string | "" | The accessible name. For an interactive card or a link card, normally the text of its heading; without it the whole content becomes the name. For a card that is neither, a label makes it a named group. |
المنافذ
| المنفذ | الوصف |
|---|---|
default | The content: any layout of media, text and — only when interactive is false and href is empty — actions. |
الأحداث
| الحدث | الوصف |
|---|---|
click | An interactive card or a link card was activated by pointer, touch, keyboard or assistive technology. A link card emits it before its destination opens; a product that takes the navigation itself (a client-side router) cancels it (CRD-20). |
الحالات
السلوك
متطلبات بصيغة EARS. يُختبر تنفيذ كل منصة مقابل هذه المعرّفات. يُعرض نص المتطلبات كما ورد في ملف المواصفة، بالإنجليزية.
| المعرّف | المتطلب |
|---|---|
CRD-1 | WHEN the user activates an interactive card by pointer, touch, Enter or Space — a link card (CRD-19) by pointer, touch or Enter — THE card SHALL emit click exactly once. |
CRD-2 | WHILE disabled is true and either interactive is true or href is not empty THE card SHALL NOT emit click, SHALL NOT be focusable and SHALL expose the disabled state to assistive technology. |
CRD-3 | WHILE an interactive card is hovered, keyboard-focused or pressed THE card SHALL render its container, outline, elevation, content color and opacity from the component tokens of the highest-precedence active state, with precedence pressed over focused over hovered; focus gained by pointer or touch SHALL NOT select the focused state. |
CRD-4 | WHEN an interactive card receives keyboard focus THE card SHALL show the focus ring without animation; focus gained by pointer or touch SHALL NOT show it. |
CRD-5 | WHERE the visual size of an interactive card is below the minimum target THE card SHALL extend its hit area on both axes to sys.target.touch for touch input and sys.target.pointer for fine pointers, without changing its visual size; a platform that cannot tell the input type SHALL use sys.target.touch. |
CRD-6 | WHILE disabled is true and either interactive is true or href is not empty THE card SHALL render its container, outline, elevation, content color and opacity from the disabled component tokens of its variant. |
CRD-7 | WHEN variant names a value that the active design system does not have THE card SHALL render with the default value's tokens, and in debug builds report the unknown value. |
CRD-8 | WHEN the card is pressed by pointer, touch, Enter or Space THE card SHALL show the pressed state without delay and SHALL keep it visible for at least sys.motion.duration.press-hold, even when the press ends sooner. A press — held, or moved with the pointer down — SHALL NOT select the card's text nor start a drag of it: the card is one action, as a button is, and the platform keeps a button's text from selection. |
CRD-9 | WHILE pressed THE card SHALL answer as sys.behavior.pressed-feedback names: overlay applies the pressed tokens to the whole container at once; ripple grows the pressed state layer from the point of contact (from the center for keyboard activation); dim applies the pressed opacity to the whole control and draws no state layer; none draws no answer to the press. Under ripple and dim only the container color stays with the state under the press; content color, outline, elevation, opacity and shape still come from the pressed tokens. Under none every part stays with the state under the press. The ripple grows over comp.card.motion.feedback-duration with comp.card.motion.state-easing, fades over comp.card.motion.state-duration with comp.card.motion.state-easing once the press has ended and press-hold has passed, and appears whole where motion is reduced. |
CRD-10 | THE card SHALL ease its container corners by sys.shape.corner-smoothing (0 is a quarter circle, 0.6 the iOS continuous corner; a platform with only the OS curve draws it for any amount above 0) and show its keyboard focus as sys.behavior.focus-style names (ring or glow); a platform that cannot draw a choice SHALL fall back to the first value and state that in its README. |
CRD-11 | WHILE interactive is false and href is empty THE card SHALL NOT be focusable, SHALL NOT emit click, SHALL NOT draw a hover, focus or press answer, SHALL ignore disabled, and SHALL render from the resting tokens of its variant. |
CRD-12 | WHILE inset is true THE card SHALL place its content comp.card.container.padding inside the container on every side; WHILE inset is false the content SHALL reach the container's edges. In both cases THE card SHALL clip its content to the container's shape, draw the outline of the outlined variant (comp.card.outline.width wide) above the content, and grow with its content: the card has no height of its own. |
CRD-13 | THE card SHALL hand its content the content color of its variant and state (comp.card.outlined.content.color for outlined at rest, and so on) as the platform's inherited foreground (the web's inherited color, SwiftUI's foreground style, Compose's content color, Flutter's default text and icon style); on React Native, which has no inherited text color, it SHALL provide the color through the Tanrei context that Tanrei's text components read. Plain text given as content, where the platform must style it, SHALL take sys.typescale.body. |
CRD-14 | WHILE interactive is true THE card SHALL be exactly one focusable element with the button role (the link role WHILE href is not empty, CRD-19), named by label, over a name the product gives it on the element (aria-label or aria-labelledby), or, WHILE label is empty, by the name the product gives it on the element — aria-label, or aria-labelledby naming an element — or else by the text of its content merged in reading order, and in debug builds SHALL report, WHILE it has neither label nor a name of the product's, that it has no label; WHILE interactive is false and href is empty THE card SHALL have no role of its own and leave its content's semantics untouched, except that WHERE label is set it SHALL be a group named by label. |
CRD-15 | WHILE interactive is true or href is not empty THE card's content SHALL contain no interactive element (button, link, field, toggle or anything else focusable); a platform that can inspect its content (the web, on every slot change) SHALL report one in debug builds, and a platform that cannot SHALL state the rule in its README and docs page. |
CRD-16 | WHEN the elevation or the colors of an interactive card change between states THE card SHALL animate the change over comp.card.motion.state-duration with comp.card.motion.state-easing, and SHALL change them at once where motion is reduced. |
CRD-17 | WHEN the layout direction is right-to-left THE card SHALL apply its padding and clipping by logical start and end so that it mirrors, SHALL grow a ripple from the point of contact as in left-to-right, and SHALL NOT mirror its content itself: the content follows the layout direction. |
CRD-18 | THE card SHALL take its layout direction from where it is used — the direction of the nearest ancestor that sets one, and the platform's own where none does — and SHALL have no direction setting of its own; WHEN that direction changes while the card is shown THE card SHALL draw itself for the new direction at once, without a motion of its own, keeping what it shows and its state. |
CRD-19 | WHILE href is not empty THE card SHALL be a link card, whether interactive is true or false: exactly one focusable element with the link role and not the button role, named as CRD-14 names an interactive card, its content presentational and held to CRD-15; the platform's own link to href (the web's anchor element; SwiftUI's Link; on Compose, Flutter and React Native an element with the platform's link semantics whose address is opened by the platform's opener), so that whatever the platform gives a link — on the web its context menu, opening in a new tab or window, copying and dragging its address, and the address shown before it is followed — is the platform's, and not the card's to draw or to take over; and everything this spec says of an interactive card other than its role and its keys — its states, focus ring, minimum target, press answer, corners, motion and disabled look (CRD-3 to CRD-6, CRD-8 to CRD-10, CRD-16) — SHALL hold for a link card from the same tokens, with no look of its own for a link and none for a destination already visited. |
CRD-20 | WHEN the user activates a link card by pointer, touch or Enter THE card SHALL emit click exactly once and then open href where target names, unless the product cancelled that click, in which case it SHALL open nothing: the product's own router has taken the navigation. It SHALL show the pressed state as CRD-8 and CRD-9 say and SHALL NOT hold the navigation back for sys.motion.duration.press-hold. THE card SHALL NOT activate a link card on Space, which on the web scrolls the page as it does on any link, and SHALL NOT show the pressed state for it; WHERE the platform gives its links other keys or gestures (on the web a modified or a middle click that opens a new tab, on Apple platforms a long press that previews) THE card SHALL leave them to the platform. |
CRD-21 | WHILE the card is a link card and target is new THE card SHALL open href in a new context — on the web a new tab or window that is given no handle to the page that opened it; on the other platforms outside the screen the person is on, as the platform's opener decides. THE card SHALL draw no mark of its own for it, because its content is the product's to lay out (CRD-12): a product that shows that the card opens elsewhere puts the glyph open_in_new of the system's icon set in the content, through the icon helper. WHILE target is new THE card SHALL expose targetLabel as its description, read after its name and role, and WHILE targetLabel is empty SHALL still render and in debug builds report that a link that opens a new context does not say so. WHILE target is same THE card SHALL ignore targetLabel. |
CRD-22 | WHILE href is not empty and disabled is true THE card SHALL NOT be a link that can be followed: it SHALL render as CRD-6 says, SHALL expose the link role with the disabled state and no destination, SHALL NOT be focusable, SHALL NOT emit click, and SHALL open nothing by any means the platform gives a link (on the web the anchor carries no href, so there is no context menu entry, no drag and no new tab for it). |
CRD-23 | WHEN target names a value the card does not define THE card SHALL behave as same, and in debug builds report the unknown value. IF href is only white space, or names a script to run rather than a place to go (the web's javascript: scheme), or cannot be read as an address by the platform, THE card SHALL be the card its interactive makes it, without a destination, and in the last two cases report it in debug builds. |
CRD-24 | WHERE the platform has ARIA attributes (the web), WHEN the product sets an aria- attribute on the host, by the attribute or by the ARIA property that reflects it, THE card SHALL move it to the data-aria- attribute of the same name, so that the host is no second node with its name, description or state, and SHALL expose it on the element that carries its role — an id reference by the elements it names in the host's tree or a tree around it, an error message as part of the description while the card is invalid — except those the card owns (aria-disabled), which it SHALL move and not expose. WHEN the product removes the aria- attribute or its data-aria- twin, or sets the ARIA property that reflects it (ariaLabel, ariaDescribedByElements…) to null, THE card SHALL take it off that element. WHEN the product sets an aria- attribute the card owns THE card SHALL report it in debug builds. |
مطابقة الأسماء بين المنصات
التكافؤ لا يعني واجهات برمجية متطابقة. تعبّر كل منصة عن العقد نفسه بأسلوبها.
| المواصفة | Web | SwiftUI | Jetpack Compose | Flutter | React Native |
|---|---|---|---|---|---|
| المكوّن | tanrei-card | TanreiCard | TanreiCard | TanreiCard | Card |
variant · الخاصية | variant | variant | variant | variant | variant |
interactive · الخاصية | interactive | action | onClick | onTap | onPress |
disabled · الخاصية | disabled | disabled | enabled | enabled | disabled |
href · الخاصية | href | destination | href | href | href |
target · الخاصية | target | target | target | target | target |
targetLabel · الخاصية | targetLabel | targetLabel | targetLabel | targetSemanticLabel | targetLabel |
inset · الخاصية | inset | inset | inset | inset | inset |
label · الخاصية | label | label | contentDescription | semanticLabel | accessibilityLabel |
click · الحدث | click | action | onClick | onTap | onPress |
| ملاحظات | The content is the default slot. inset is a boolean property that defaults to true; its attribute is the inverse flush, because an HTML boolean attribute cannot default to true. click fires only while interactive is true and disabled is false. With href the shadow root holds an <a part="container"> around the slot in place of the element with role button; click then fires for a link card too. The link form: href is the href attribute; target is the target attribute with the values same and new (the HTML spellings _self and _blank are read as those two; anything else is same and reported); targetLabel is the target-label attribute. Web-only, forwarded to the anchor and to nothing else: rel (to which new adds noopener) and download. Cancelling click (preventDefault()) cancels the navigation, which is how a client-side router takes it; a modified or a middle click is the browser's. A disabled link is the anchor without href, with role="link" and aria-disabled="true", as the navigation drawer's destination already is. Space is not handed to the press controller. element.click() follows the link. | SwiftUI idiom (as Button): the initializer that takes action: makes the card interactive; the one without it is a plain container. disabled follows .disabled(_:); label sets accessibilityLabel. Content is a @ViewBuilder. Not built on this platform yet: the initializer that takes destination: URL in place of action: draws the same control as a SwiftUI Link, so the address is opened by the environment's openURL — an app that routes addresses itself provides an OpenURLAction, which is how the navigation is cancelled — and VoiceOver reads the link trait. target: TanreiLinkTarget = .same and targetLabel (the accessibility hint) are parameters of that initializer alone. | Compose idiom (as Card): the overload with a non-null onClick is interactive; enabled is the inverse flag. Content is a ColumnScope lambda and reads the content color from LocalTanreiContentColor and plain text's style from LocalTanreiTextStyle, Tanrei's own locals (the package does not depend on Material, whose LocalContentColor it would otherwise be). Not built on this platform yet: the overload that takes href: String (onClick is then optional and called first) opens the address through LocalUriHandler.current after onClick; an app that routes addresses itself provides its own UriHandler. Compose has no link role for a clickable element (only link annotations inside text), so the node drops Role.Button and carries no role, and the README says so. target: TanreiLinkTarget = TanreiLinkTarget.Same; targetLabel is appended to the node's state description. | Flutter idiom: a non-null onTap makes the card interactive. enabled: false disables it while keeping its interactive look (as ListTile), because a null onTap already means not interactive. Content is child. Not built on this platform yet: the TanreiCard.link constructor (href is a Uri) has Semantics(link: true) with the address as its linkUrl. The package has no plugin dependency, so the address is opened by the nearest TanreiLinkOpener, an inherited widget the app provides once (calling url_launcher, or its own router); without one the control opens nothing and reports it in debug builds. target is a TanreiLinkTarget; targetSemanticLabel is the semantics hint. | A non-null onPress makes the card interactive (an accessible Pressable with the button role); without it the card is a View. Content is children. Not built on this platform yet: a Card with href has accessibilityRole="link" and opens the address with Linking.openURL after onPress, unless onPress called event.preventDefault(). target is "same" | "new"; targetLabel is the accessibilityHint. |
الرموز
| الرمز | يشير إلى | القيمة المحلولة (فاتح) |
|---|---|---|
--tanrei-comp-card-container-padding | sys.space.space200 | 16px |
--tanrei-comp-card-container-shape | sys.shape.corner.medium | 8px |
--tanrei-comp-card-disabled-opacity | — | 0.38 |
--tanrei-comp-card-elevated-container-color | sys.color.surface-container-lowest | |
--tanrei-comp-card-elevated-container-elevation | sys.elevation.level2 | 0px 0px 0px 1px rgb(0 0 0 / 0.08), 0px 1px 2px 0px rgb(0 0 0 / 0.04) |
--tanrei-comp-card-elevated-content-color | sys.color.on-surface | |
--tanrei-comp-card-elevated-disabled-container-color | — | |
--tanrei-comp-card-elevated-disabled-container-elevation | — | 0px 0px 0px 1px rgb(0 0 0 / 0.08) |
--tanrei-comp-card-elevated-disabled-content-color | — | |
--tanrei-comp-card-elevated-disabled-outline-color | — | |
--tanrei-comp-card-elevated-disabled-state-layer-color | — | |
--tanrei-comp-card-elevated-focused-container-color | — | |
--tanrei-comp-card-elevated-focused-container-elevation | — | 0px 0px 0px 1px rgb(0 0 0 / 0.08), 0px 1px 2px 0px rgb(0 0 0 / 0.04) |
--tanrei-comp-card-elevated-focused-content-color | — | |
--tanrei-comp-card-elevated-focused-outline-color | — | |
--tanrei-comp-card-elevated-focused-state-layer-color | — | |
--tanrei-comp-card-elevated-hovered-container-color | — | |
--tanrei-comp-card-elevated-hovered-container-elevation | — | 0px 0px 0px 1px rgb(0 0 0 / 0.08), 0px 1px 1px 0px rgb(0 0 0 / 0.04), 0px 4px 8px -4px rgb(0 0 0 / 0.04), 0px 16px 24px -8px rgb(0 0 0 / 0.06) |
--tanrei-comp-card-elevated-hovered-content-color | — | |
--tanrei-comp-card-elevated-hovered-outline-color | — | |
--tanrei-comp-card-elevated-hovered-state-layer-color | — | |
--tanrei-comp-card-elevated-outline-color | — | |
--tanrei-comp-card-elevated-pressed-container-color | — | |
--tanrei-comp-card-elevated-pressed-container-elevation | — | 0px 0px 0px 1px rgb(0 0 0 / 0.08), 0px 1px 2px 0px rgb(0 0 0 / 0.04) |
--tanrei-comp-card-elevated-pressed-content-color | — | |
--tanrei-comp-card-elevated-pressed-outline-color | — | |
--tanrei-comp-card-elevated-pressed-state-layer-color | — | |
--tanrei-comp-card-elevated-state-layer-color | comp.card.elevated.content.color | |
--tanrei-comp-card-filled-container-color | sys.color.surface-container-highest | |
--tanrei-comp-card-filled-container-elevation | sys.elevation.level0 | |
--tanrei-comp-card-filled-content-color | sys.color.on-surface | |
--tanrei-comp-card-filled-disabled-container-color | — | |
--tanrei-comp-card-filled-disabled-container-elevation | — | |
--tanrei-comp-card-filled-disabled-content-color | — | |
--tanrei-comp-card-filled-disabled-outline-color | — | |
--tanrei-comp-card-filled-disabled-state-layer-color | — | |
--tanrei-comp-card-filled-focused-container-color | — | |
--tanrei-comp-card-filled-focused-container-elevation | — | |
--tanrei-comp-card-filled-focused-content-color | — | |
--tanrei-comp-card-filled-focused-outline-color | — | |
--tanrei-comp-card-filled-focused-state-layer-color | — | |
--tanrei-comp-card-filled-hovered-container-color | — | |
--tanrei-comp-card-filled-hovered-container-elevation | — | |
--tanrei-comp-card-filled-hovered-content-color | — | |
--tanrei-comp-card-filled-hovered-outline-color | — | |
--tanrei-comp-card-filled-hovered-state-layer-color | — | |
--tanrei-comp-card-filled-outline-color | — | |
--tanrei-comp-card-filled-pressed-container-color | — | |
--tanrei-comp-card-filled-pressed-container-elevation | — | |
--tanrei-comp-card-filled-pressed-content-color | — | |
--tanrei-comp-card-filled-pressed-outline-color | — | |
--tanrei-comp-card-filled-pressed-state-layer-color | — | |
--tanrei-comp-card-filled-state-layer-color | comp.card.filled.content.color | |
--tanrei-comp-card-focused-opacity | — | 1 |
--tanrei-comp-card-hovered-opacity | — | 1 |
--tanrei-comp-card-motion-feedback-duration | sys.motion.role.feedback | 450ms |
--tanrei-comp-card-motion-state-duration | sys.motion.role.state.large | 250ms |
--tanrei-comp-card-motion-state-easing | sys.motion.easing.state | cubic-bezier(0.2, 0, 0, 1) |
--tanrei-comp-card-outline-width | sys.border.hairline | 1px |
--tanrei-comp-card-outlined-container-color | sys.color.surface | |
--tanrei-comp-card-outlined-container-elevation | sys.elevation.level0 | |
--tanrei-comp-card-outlined-content-color | sys.color.on-surface | |
--tanrei-comp-card-outlined-disabled-container-color | — | |
--tanrei-comp-card-outlined-disabled-container-elevation | — | |
--tanrei-comp-card-outlined-disabled-content-color | — | |
--tanrei-comp-card-outlined-disabled-outline-color | — | |
--tanrei-comp-card-outlined-disabled-state-layer-color | — | |
--tanrei-comp-card-outlined-focused-container-color | — | |
--tanrei-comp-card-outlined-focused-container-elevation | — | |
--tanrei-comp-card-outlined-focused-content-color | — | |
--tanrei-comp-card-outlined-focused-outline-color | — | |
--tanrei-comp-card-outlined-focused-state-layer-color | — | |
--tanrei-comp-card-outlined-hovered-container-color | — | |
--tanrei-comp-card-outlined-hovered-container-elevation | — | |
--tanrei-comp-card-outlined-hovered-content-color | — | |
--tanrei-comp-card-outlined-hovered-outline-color | — | |
--tanrei-comp-card-outlined-hovered-state-layer-color | — | |
--tanrei-comp-card-outlined-outline-color | sys.color.outline-variant | |
--tanrei-comp-card-outlined-pressed-container-color | — | |
--tanrei-comp-card-outlined-pressed-container-elevation | — | |
--tanrei-comp-card-outlined-pressed-content-color | — | |
--tanrei-comp-card-outlined-pressed-outline-color | — | |
--tanrei-comp-card-outlined-pressed-state-layer-color | — | |
--tanrei-comp-card-outlined-state-layer-color | comp.card.outlined.content.color | |
--tanrei-comp-card-pressed-opacity | — | 1 |
نماذج الاختبار
عروض مرجعية. تلتقط كل المنصات لقطة لكل نموذج بالمعرّف نفسه حتى يمكن مقارنة النتائج.
variant: elevated (2)
elevated
variant: elevated elevated-disabled
variant: elevated interactive: true disabled: true label: Quarterly report variant: filled (2)
filled
variant: filled filled-link-new
variant: filled href: https://example.com/tanrei target: new targetLabel: opens in a new tab label: Source variant: outlined (6)
outlined
variant: outlined outlined-flush
variant: outlined inset: false outlined-interactive
variant: outlined interactive: true label: Quarterly report outlined-disabled
variant: outlined interactive: true disabled: true label: Quarterly report outlined-link
variant: outlined href: /getting-started/ label: Getting started outlined-link-disabled
variant: outlined disabled: true href: /getting-started/ label: Getting started مولَّد من specs/card.spec.json, tokens/comp/card.tokens.json, design/tokens/
استخدم البطاقة لتجمع محتوى عن موضوع واحد، كتقرير أو شخص أو منتج أو إعداد، وتفصله عمّا حوله. البطاقة لا تفعل إلا أن تحشو محتواها وتقصّه وتلوّنه. وأنت تخطّط المحتوى بأنماط النص في Tanrei (heading-5 لعنوان البطاقة) وبمكوّناته.
اجعل البطاقة كلها تفاعلية حين تكون إجراء واحدًا يفتح ما تعرضه. وإن احتاجت إلى عدة إجراءات فأبقِها غير تفاعلية وضع أزرارًا في داخلها.
عنصر التحكم الذي له وجهة رابط، ويصح ذلك على البطاقة أيضًا. حين لا يفعل تفعيل البطاقة إلا أن يأخذ الشخص إلى صفحة أخرى، أعطِها href بدل معالجة النقر على بطاقة تفاعلية. فتكون البطاقة رابطًا حقيقيًّا، وعلى الويب عنصر anchor حول محتواها. يستطيع الناس أن يفتحوها في تبويب جديد، وأن ينسخوا عنوانها، وأن يروا إلى أين تقود قبل أن يتبعوها، والتقنيات المساعدة تُعلِن رابطًا لا زرًّا. والبطاقات التي تقود من صفحة في هذا الموقع إلى أخرى بطاقات رابط.
- بطاقة الرابط تبدو تمامًا كالبطاقة التفاعلية في كل حالة. لا مظهر خاصًّا بالرابط ولا حالة «تمت زيارته».
- الخاصية
hrefتغلبinteractive: البطاقة التي لها الاثنتان رابط. - المفتاح Enter يتبع بطاقة الرابط. أما المفتاح Space فلا: يفعل ما يفعله على أي رابط، وهو على الويب تمرير الصفحة.
- يصدر
clickأولًا، ثم تُفتح الوجهة. ألغِ النقرة لتتولى التنقل بنفسك، كما يفعل الموجِّه من جهة العميل. - الخاصية
targetقيمتهاsame(الافتراضية) أوnew. الرابط الذي يفتح تبويبًا جديدًا أو نافذة جديدة يحتاج إلىtargetLabel، وهي الكلمات التي تُقرأ بعد اسمه، مثل «يُفتح في تبويب جديد». لا يقدّم Tanrei كلمات من عنده. والبطاقة لا ترسم علامة لذلك، لأن المحتوى محتواك: ضع الرسمopen_in_newفي المحتوى إن أردت أن تعرض واحدًا. - بطاقة الرابط المعطَّلة لا يمكن اتباعها بأي وسيلة، وتخرج من ترتيب التركيز.
- القيمة
hrefالمكوَّنة من مسافات بيضاء فقط ليست وجهة. والعنوان من النوعjavascript:، أو الذي لا يمكن قراءته عنوانًا، يُرفض ويُبلَّغ عنه في بنيات التنقيح (debug)، وتكون البطاقة ما كانت ستكونه من دونه.
حين تحتاج البطاقة إلى وجهة وإلى إجراءاتها الخاصة معًا، أبقِها بسيطة واجعل عنوانها رابطًا، أو ضع في داخلها Button له href.
اختر بحسب الخلفية، لا بحسب الأهمية.
| الطراز (variant) | يفصل بـ | يُستخدم لـ |
|---|---|---|
| Outlined (الافتراضي) | حافة | معظم البطاقات، والبطاقات على سطح له درجة لون البطاقة filled نفسها. |
| Filled | درجة لون | البطاقات على سطح بسيط، حيث تكفي درجة اللون وحدها لتعليم المجموعة. والبطاقة filled تختفي على سطح له الدرجة نفسها. |
| Elevated | عمق | المحتوى الذي يعلو الصفحة، كعنصر مثبَّت أو قابل للسحب. |
- سمِّ البطاقة التفاعلية أو بطاقة الرابط بالخاصية
label، وهي في العادة نص عنوانها. - استخدم طرازًا واحدًا لمجموعة البطاقات المعروضة معًا.
- استخدم القيمة false للخاصية
inset(أيflushعلى الويب) للوسائط التي تمتد إلى الحواف، واحشُ النص تحتها بنفسك برمز حشو البطاقة.
- لا تضع زرًّا أو رابطًا أو حقلًا أو عنصر تبديل أو أي شيء آخر يقبل التركيز في بطاقة تفاعلية أو بطاقة رابط. لا تستطيع التقنيات المساعدة بلوغه، والضغط عليه يفعّل البطاقة أيضًا.
- لا تتنقل من معالج النقر في بطاقة تفاعلية. البطاقة التي تذهب إلى مكان ما تأخذ
href. - لا تعطّل بطاقة بسيطة. القيمة
disabledتُتجاهل هناك، لأنه لا إجراء ولا رابط يُعطَّل. - لا تستخدم الارتفاع لترتيب البطاقات بحسب الأهمية.
- الدور
button (link while href is set)- التسمية
- The role is button while interactive is true and link while href is set; otherwise the card has no role, or is a group when label is set. An interactive card's or a link card's name is label — normally its heading's text — or, without one, its whole content read as one string (reported in debug builds). Their content is presentational: its headings and images are read as part of the name, not as separate elements. While target is new, targetLabel is a link card's description.
- الحد الأدنى للهدف
- تستوفي مساحة اللمس الحد الأدنى لحجم الهدف حتى لو كان الحجم المرئي أصغر. minimum 32px · pointer 44px · spacing 8px · touch 48px
لوحة المفاتيح
| المفاتيح | الإجراء |
|---|---|
| Tab | Moves focus to an interactive card or a link card. A card that is neither is not a tab stop; focus moves through its content. |
| Enter / Space | Activates an interactive card. A link card is activated by Enter alone: Space does what it does on any link (on the web it scrolls the page). |
ملاحظات
- Never nest interactive elements in an interactive card or a link card: assistive technology cannot reach them (the card is one element) and a press on them would also activate the card. A card that needs its own buttons or links stays plain, and its primary action or destination is one of them: a heading that is a link, or a link Button.
- Web: an interactive card draws a shadow element with role button and tabindex 0 around the slot (not a <button>, whose content model forbids block content); Enter activates on keydown and Space on keyup, as a native button does. A link card draws an anchor around the slot instead, which HTML allows around block content that holds nothing interactive. In debug builds either inspects the slotted content on every slotchange for focusable or interactive descendants and reports them.
- SwiftUI, Compose, Flutter and React Native merge the content into the one accessible element (SwiftUI's button, Compose's merged clickable semantics, Flutter's merged semantics, React Native's accessible Pressable). None of them can reliably detect a nested interactive element, so each states the rule in its README; their tests assert that the merged element is the only one exposed.
- Text size does not clip a card: it grows with its content at 200% text size.
- How a state looks is a value decision of the active design system; components read the state's tokens and never compute the effect themselves.
- A card with a destination is a link: one element with the link role, the platform's own link, Enter and not Space, so that on the web the card can be opened in a new tab and its address copied. No visited state is drawn.
- A link card that opens a new context says so through targetLabel. The card draws no mark of its own, since it does not lay out its content; a product that shows one puts the glyph open_in_new of the system's icon set in the content, where the icon helper mirrors it in a right-to-left layout.
مولَّد من specs/card.spec.json, tokens/sys/measure.tokens.json
- البطاقة غير التفاعلية لا دور لها، وتترك دلالات محتواها على حالها. ومع
labelتكون مجموعة (group) تسمّيها تلك القيمة. - البطاقة التفاعلية عنصر واحد يقبل التركيز وله دور الزر، وتعرض حالة تعطيلها. اسمها
label، أو، في غيابها، محتواها كله مقروءًا سلسلة واحدة. وتبلّغ بنيات التنقيح (debug) عن التسمية الناقصة. - بطاقة الرابط عنصر واحد يقبل التركيز وله دور الرابط، ويُسمّى بالطريقة نفسها. وما دامت
targetتساويnewتكونtargetLabelوصفه، ويُقرأ بعد اسمه ودوره. وبطاقة الرابط المعطَّلة تحتفظ بدور الرابط، وتعرض حالة التعطيل، ولا وجهة لها. - المفتاحان Enter و Space يفعّلان البطاقة التفاعلية، والمفتاح Enter وحده يفعّل بطاقة الرابط. ومساحة اللمس تستوفي الحد الأدنى للهدف، ومؤشر التركيز يظهر لتركيز لوحة المفاتيح فقط.
- تكبر مع محتواها، فلا يُقصّ النص الكبير أبدًا.
- على الويب تبلّغ بنيات التنقيح عن عنصر تفاعلي داخل بطاقة تفاعلية أو بطاقة رابط عند كل تغيّر في المنفذ (slot)، وعن بطاقة رابط تفتح تبويبًا جديدًا بلا
targetLabel.
فجوات معروفة في المنصات:
- ليس في SwiftUI و Compose و Flutter و React Native بطاقة رابط بعد. فالبطاقة التي تذهب إلى مكان ما هناك ما زالت بطاقة تفاعلية تتنقل من إجرائها.
- الويب: العنصر التفاعلي الذي يُضاف لاحقًا، في عمق عنصر موجود أصلًا في البطاقة، لا يُبلَّغ عنه.
- الويب: البطاقة غير التفاعلية لا تُنتج نقرة خاصة بها أبدًا، لكن النقرة على محتواها نقرة DOM عادية وتصعد عبر البطاقة.
- لا تستطيع SwiftUI و Compose و Flutter و React Native اكتشاف عنصر تفاعلي متداخل، فلا تفحص وجوده. والقاعدة عليك أنت أن تحفظها.
- في React Native: البطاقة غير التفاعلية التي لها تسمية مجموعة، لكن iOS لا يقابل ذلك الدور بأي صفة (trait). ولم يُتحقق على جهاز من أن VoiceOver يُعلِن اسم المجموعة.
- في Compose: التفعيل بالمفتاحين Enter و Space عبر معالجة المفاتيح في المنصة مختبَر تحت Robolectric فقط، لا على جهاز بعد.
- ليس في React Native لون نص موروث. ترسم البطاقة السلسلة النصية المجرّدة بلون محتواها؛ ونصك أنت يقرؤه بالدالة
useContentColor(). - في SwiftUI: البطاقة التفاعلية الأصغر من الحد الأدنى للهدف تحتفظ بمقاسها المرئي، لكنها تشغل مقاس الهدف في التخطيط.
- أعطِ البطاقة التفاعلية أو بطاقة الرابط
label. من دونها يُقرأ المحتوى كله اسمًا واحدًا طويلًا. - أعطِ بطاقة الرابط التي تفتح تبويبًا جديدًا أو نافذة جديدة
targetLabelبلغة منتجك. - أبقِ البطاقة التفاعلية وبطاقة الرابط خاليتين من عناصر التحكم الأخرى. حين تحتاج البطاقة إلى أزرارها أو روابطها الخاصة، أبقِها بسيطة ودع أحدها يكون إجراءها الرئيسي أو وجهتها.
- أعطِ العناوين والصور وسائر المحتوى داخل البطاقة غير التفاعلية دلالاتها الخاصة: النص البديل، ومستويات العناوين. البطاقة لا تضيف شيئًا منها.