Composites
Composites
Tầng 2 là phần của gói thật sự nói về Comitor: 48 mẫu mà mọi app trong hệ sinh thái đều lặp lại — một màn hình danh sách, một chip trạng thái, một hộp xác nhận, một khoảng ngày — ráp từ các primitive Tầng 1 và phát hành từ entry @comitor/ui, cùng entry với các primitive. Có bốn ngoại lệ kéo theo một peer tuỳ chọn nên nằm sau entry riêng: Upload, Data Grid, hai trình soạn thảo văn bản — Markdown Editor và Rich Text Editor — và Chat, dựng trên hai trình soạn thảo đó.
Không cài thêm gì, không import thêm gì: import { Button, DataTable } from '@comitor/ui' là một dòng với tới cả hai tầng.
Mười tám mẫu trong số này đến ở 2.1.0, chuyển từ Dice UI — Kanban, Stepper, Timeline, Time Picker, Tags Input, Mention, Action Bar, Phone Input, Mask Input, Rating, Circular Progress, Banner, Responsive Dialog, Tour, Key Value, Cropper, Scroll Spy và Badge Overflow. Gói xếp chúng vào Tầng 1, cạnh các primitive; site này để chúng ở đây vì mỗi cái là một tổ hợp nhiều phần đã ra sẵn quyết định — đúng việc của tầng này. Cùng entry, cùng dòng import, không peer mới phải cài. Thêm ba cái ở 3.15.0 — Color Picker, Color Swatch và Compare Slider. Mỗi bản chuyển đã sửa gì khi vào gói thì xem ở nhật ký thay đổi.
Thứ gì thuộc tầng này
Một primitive render một control. Một composite đã ra sẵn một quyết định — và giá trị nằm ở quyết định, không ở markup.
Header trang thụt vào bao nhiêu, bảng trống nói gì, chữ trạng thái chạy ở bậc tương phản nào, một con số tăng là tin tốt hay xấu. Tự dựng lại một composite nghĩa là quyết định lại tất cả, mỗi app một kiểu.
Không composite nào import next/*. Chỗ nào cần link của router thì nhận prop LinkComponent thay vào. Nhờ vậy entry chính chạy được dưới Vite, Storybook và test thuần — framework nằm ở Tầng 3.
Mọi tông ở tầng này là token vai trò, không bao giờ là mã hex hay thang thô, nên đi theo bộ màu đang dùng. Chữ và icon đứng riêng dùng bậc -ink; bậc trơn chỉ để tô nền.
Một màn hình danh sách, ráp sẵn
Chạy thật, và chỉ dùng Tầng 2: ô số liệu, thanh tìm kiếm có debounce với chip lọc, hàng có avatar và status pill, và empty state hiện ra khi bộ lọc loại hết mọi thứ. Gõ duc hoặc ha để thấy matchesSearch tìm ra tên viết có dấu. Header trang là mảnh duy nhất bị bỏ ra — trang tài liệu này đã có h1 của nó, còn PageHeader render thêm một cái nữa.
Open
2
Blocked
1
+1vs last week
Done
1
- Nguyễn Đức ThànhOpen
Draft the Q3 handover note
Nguyễn Đức Thành · updated
- Trần Minh AnhBlocked
Reconcile August invoices
Trần Minh Anh · updated
- Lê Thu HàDone
Ship the payroll export
Lê Thu Hà · updated
- Phạm Quốc BảoOpen
Review the vendor contract
Phạm Quốc Bảo · updated
Khung trang
Đường viền của một màn hình. Page Header và Route Tabs là tĩnh, nên Server Component render được, và cả hai nhận prop LinkComponent thay vì import next/link — nhờ đó entry chính dùng được ngoài Next. Scroll Spy là mục lục cho màn hình dài quá một lần nhìn.
Breadcrumb, tiêu đề, mô tả và slot actions; giữ h1 của trang. PageContainer là khung nội dung đi kèm, bốn độ rộng.
PageHeader · PageContainer
Dải tab mà mỗi mục là một route thật: một <nav> gồm các link mang aria-current, không phải tablist của Radix. Trông y hệt Tabs vì đọc cùng bảng class.
RouteTabs
Mục lục trong trang đi theo người đọc. Link mang aria-current, và nó theo dõi đúng vùng cuộn được đưa vào — kể cả <main> của AppShell, không chỉ window.
ScrollSpy · ScrollSpyNav · ScrollSpyLink · ScrollSpySection
Dữ liệu
Màn hình danh sách, tách thành các phần dùng riêng được. Sắp xếp, phân trang và lọc đều là controlled — composite render state và báo cho app biết nó đã đổi; việc tải dữ liệu vẫn là của app.
Sắp xếp, chọn hàng, ghim cột, skeleton khi tải và empty state có sẵn. Mọi chuỗi, kể cả aria-label, đều ghi đè được.
DataTable · DataTableColumnToggle
Nửa còn lại của cùng bài toán: bảng tính cho màn hình người dùng GÕ vào. Hàng ảo hoá, chín loại ô, chọn theo vùng và clipboard theo kiểu Excel. Composite duy nhất có @tanstack/react-table làm peer, nên nằm sau @comitor/ui/data-grid.
DataGrid · useDataGrid · DataGridFilterMenu · …
Thanh dưới bảng. Nó tự tính số trang từ total và pageSize, nên app và control không bao giờ lệch nhau một trang.
TablePagination · DEFAULT_PAGE_SIZE_OPTIONS
Ô tìm kiếm có debounce, một slot cho control lọc, badge đếm bộ lọc đang bật và số kết quả. FilterChips là hàng toggle thường nằm trong slot đó.
SearchFilterBar · FilterChips
Thanh công cụ nổi cho thao tác hàng loạt khi đang chọn hàng. Một phím Escape đã được menu phía trên dùng — hay nhấn giữa chừng khi đang gõ Telex — không còn đóng nó và xoá vùng chọn.
ActionBar · ActionBarSelection · ActionBarItem · …
Bảng và pipeline kéo thả bằng chuột, cảm ứng và bàn phím — và thả bằng bàn phím thật sự đổi dữ liệu, điều bản gốc chưa bao giờ làm. Escape đưa thẻ về chỗ cũ.
Kanban · KanbanBoard · KanbanColumn · KanbanItem · KanbanOverlay
Dòng hoạt động và lịch sử bản ghi, dọc hoặc ngang, mỗi mục là đã xong, đang làm hoặc chờ.
Timeline · TimelineItem · TimelineDot · TimelineContent · …
Gantt công việc và roadmap dự án trong một: thanh theo ngày, bốn loại mũi tên phụ thuộc kèm phát hiện vi phạm, kéo để dời hoặc kéo dài, đường găng do app tính, năm thang từ ngày tới năm — ảo hoá, và điều khiển được bằng bàn phím.
Gantt · GANTT_SCALES · GanttItem · GanttDependency · …
Cây đề cương cho bảng phân rã công việc: Tab và Shift+Tab để thụt vào và lùi ra, kéo thả để đổi cha, đánh số 1.2.3 và cột cộng dồn. Một onMove cho mọi thay đổi, và moveTreeItems để áp dụng nó — cả trên server.
TreeView · moveTreeItems · computeTreeRollup · …
Ô KPI với delta tuỳ chọn. Delta tô màu theo hướng nhân với cực tính, vì số ticket quá hạn tăng lên không phải tin tốt.
StatCard · statCardIconVariants
Chip trạng thái ba variant, tô màu từ bảng trạng thái của chính app. Giá trị lạ vẫn được hiện chứ không bị nuốt, nên enum cũ vẫn nhìn thấy được.
StatusPill
Hàng chip gập thành “+N” khi hết chỗ — và nói cho trình đọc màn hình biết còn bao nhiêu, thay vì đọc “cộng N”.
BadgeOverflow
Hiện một màu đã lưu — ô vuông có tên trình đọc màn hình đọc được, nền bàn cờ dưới kênh alpha, ô gạch chéo khi không có màu. Dành cho màu là dữ liệu, không phải màu giao diện.
ColorSwatch
Trước và sau, ảnh này chồng lên ảnh kia, chia bằng một tay nắm để kéo, bấm hoặc di bằng bàn phím — ngang hoặc dọc, vẫn cuộn được trên điện thoại.
CompareSlider · CompareSliderBefore · CompareSliderAfter · CompareSliderHandle · CompareSliderLabel
Form
Bố cục, khung của form nhiều bước, và các ô nhập quá lớn để làm primitive — bộ chọn, tag, mention, hai trình soạn thảo văn bản và ô soạn chat dựng trên chúng, số điện thoại, số tiền có mask, đánh giá sao, hàng khoá–giá trị, công cụ cắt ảnh, bộ chọn màu. Chúng ghép được với lớp nối react-hook-form ở @comitor/ui/form, nhưng không bắt buộc.
Lưới 12 cột với các phần được nhóm, tuỳ chọn thu gọn được. FormField trao cho control id và phần nối aria để không phải gõ lại.
FormSection · FormField
Khung của form nhiều bước: các bước, trạng thái của chúng, Trước và Tiếp — có ngay từ lần render trên server khi các bước được viết ra trong JSX.
Stepper · StepperItem · StepperContent · StepperNext · useStepper · …
Một hàng cho avatar hay logo, một vùng thả cao cho tài liệu, và hook headless bên dưới khi cần hình thứ ba. Composite duy nhất có entry riêng — @comitor/ui/uploader, với @uppy/core và @uppy/xhr-upload là peer tuỳ chọn; route của app nhận một POST multipart bình thường.
ImageUploadField · FileUpload · useFileUpload
Cắt avatar hay logo trước khi tải lên — bằng con trỏ, con lăn, cảm ứng hoặc bàn phím, với + và − để phóng to thu nhỏ.
Cropper · CropperImage · CropperArea · useCropper
Nhập một màu như dữ liệu — màu thương hiệu trên hồ sơ khách hàng — bằng hex, RGB, HSL hoặc HSB, với vùng chọn hai trục dùng được bằng bàn phím. Không dùng để chọn màu giao diện.
ColorPicker · ColorPickerTrigger · ColorPickerContent · ColorPickerArea · …
Select tìm kiếm được, chọn một hoặc nhiều, khớp tiếng Việt có dấu hay không dấu. Chế độ async trả truy vấn về để app tự tải.
Combobox · MultiCombobox · SELECT_EMPTY_VALUE
Giá trị tự do thành chip gỡ được — nhãn, danh sách người nhận. Mỗi nút xoá mang tên của tag nó gỡ.
TagsInput · TagsInputItem · TagsInputInput · …
@-mention trong ô soạn hay ô bình luận, với danh sách gợi ý có tên, điều khiển bằng bàn phím.
Mention · MentionInput · MentionContent · MentionItem
Trình soạn thảo kiểu Linear lưu Markdown: viết trên văn bản đã định dạng, / để chèn khối, @ để mention, bong bóng định dạng khi bôi chọn, ảnh, bảng, code tô màu. Nằm sau entry riêng — @comitor/ui/editor, với Tiptap là peer tuỳ chọn.
MarkdownEditor · MarkdownEditorHandle
Cùng lõi đó nhưng lưu HTML, cho nội dung xuất bản: thanh công cụ ghim ở trên, gạch chân, ô bảng chứa được danh sách và ảnh, và schema theo allow-list, loại bỏ script và event handler ngay khi nhận vào.
RichTextEditor · RichTextEditorHandle
Khung hội thoại cho trợ lý AI: luồng tin dính đáy, tin nhắn vẽ bằng Markdown Editor chỉ đọc kèm nhãn “AI tạo”, các bước của agent, nguồn đánh số, và ô soạn mà Enter là gửi. Nằm sau entry riêng — @comitor/ui/chat, với các peer của trình soạn thảo.
ChatThread · ChatMessage · ChatSteps · ChatSources · ChatComposer
Lịch cộng ô nhập gõ tay cộng preset. Một locale date-fns điều khiển cả lịch lẫn thứ tự các trường, nên en-US gõ MM/dd/yyyy mà không cần cấu hình thêm.
DatePicker · DateRangePicker · dateFormatForLocale
Các đoạn giờ gõ tay cộng bộ chọn cuộn. Đồng hồ theo locale — 2:30 PM bằng tiếng Anh, 14:30 bằng tiếng Việt — còn giá trị luôn là 24 giờ.
TimePicker · TimePickerInput · TimePickerContent · …
Bộ chọn quốc gia và ô số xuất ra E.164 — số trong nước 0912 345 678 thành +84912345678 khi đã chọn quốc gia. Không có quốc gia mặc định: truyền quốc gia của workspace vào.
PhoneInput · PhoneInputCountrySelect · PhoneInputField · usePhoneInput
Tiền tệ, phần trăm và mẫu cố định được định dạng ngay khi gõ, kèm giá trị thô bên cạnh. Đọc đúng số chữ số thập phân thật của từng tiền tệ — VND không có — và mặc định là USD, nên hãy truyền tiền tệ của app.
MaskInput · MASK_PATTERNS · applyMask · …
Sao, kể cả nửa sao, là một radio group: đúng một sao aria-checked, sao nào cũng có tên, và giá trị ban đầu được gửi cùng form.
Rating · RatingItem · useRating
Hàng khoá–giá trị sửa được cho trường tuỳ biến, header và metadata; focus rơi vào hàng mới và không bao giờ tụt về <body>.
KeyValue · KeyValueItem · useKeyValueStore · …
Phản hồi
Những gì app nói lại, và bề mặt nó dùng để nói. Mỗi cái là một quyết định đã chốt về cách xác nhận, trạng thái trống, tiến độ và tin bất đồng bộ trông ra sao trong cả hệ sinh thái — và một hộp thoại cư xử thế nào trên điện thoại.
Icon, tiêu đề, mô tả và một hành động. Hình dạng chung của mọi "chưa có gì ở đây", để Tasks, CRM và HR thống nhất trạng thái trống trông thế nào.
EmptyState · IconComponent
Thông báo toàn workspace đóng được, theo các tông của Alert, đứng một mình hoặc xếp chồng trong hàng đợi ưu tiên — onDismiss của nó không còn bắn mỗi lần cha re-render.
Banner · Banners · useBanners · …
Tiến độ dạng vòng, có giá trị hoặc không, bên cạnh Progress dạng thanh. Nhãn nằm dưới vòng thay vì dưới con số phần trăm.
CircularProgress · CircularProgressIndicator · …
Alert dialog quanh một promise. Trả promise từ onConfirm thì hộp thoại vẫn mở và bận cho tới khi xong — đóng trước là cách sinh ra xoá hai lần.
ConfirmDialog
Dialog trên desktop và Drawer trên điện thoại từ cùng một bộ phần, focus được đưa vào trong ở cả hai — và không có khung hình nào chớp nhầm cái kia.
ResponsiveDialog · ResponsiveDialogContent · …
Bề mặt Cmd/Ctrl+K: lệnh theo nhóm, nhóm gần đây, gợi ý phím tắt từng mục, và chế độ async tắt lọc cục bộ.
CommandPalette
Hướng dẫn lần đầu chiếu sáng phần tử thật, cuộn bên trong vùng cuộn của chính app, và chỉ giữ focus khi đang mở.
Tour · TourStep · TourSpotlight · …
Thông báo xếp chồng với icon trạng thái của Comitor. Tài liệu đặt cùng các primitive — SonnerToaster ở entry shell là bản biết next-themes.
Toaster · toast · toastPromise · dismissToast
Tiện ích
Component nhỏ xuất hiện trên gần như mọi màn hình. Nhỏ không có nghĩa là mỏng: mỗi cái mang sẵn một phương án dự phòng, một locale hay một quy tắc hydrate mà không có nó thì app phải tự khám phá lại.
Chữ viết tắt lấy theo kiểu Việt (chữ đầu của họ cộng chữ đầu của tên) trên một tông suy ra từ chính cái tên.
LetterAvatar · AvatarGroup · getAvatarToneClasses
Icon của app hay workspace trong ô nhận diện có màu — anh em của LetterAvatar. Tông suy ra từ key, nên sản phẩm mới có màu mà không cần cấu hình, và tone ghi đè bằng tay.
IconAvatar · AVATAR_TONE_NAMES
Chép vào clipboard với trạng thái đã xác nhận, và phương án dự phòng bằng textarea cho các ngữ cảnh không hề có navigator.clipboard.
CopyButton
Phần tử <time> tự cập nhật. Truyền locale cho mọi ngôn ngữ trừ tiếng Việt, và now để cố định giá trị cho fixture và snapshot.
RelativeTime
Bấm vào chữ, hiện ô nhập, Enter để lưu, Escape để huỷ. onSave async giữ ô tại chỗ cho tới khi xong thay vì chớp lại giá trị cũ.
InlineEdit
Hiện "mod+k" thành ⌘K hoặc Ctrl+K. useIsMac cố ý trả false ở lần render đầu — server không thể biết, và đoán sai là mất hydrate. Cùng useKeySequences và bảng ?, KeyboardShortcutsDialog.
KeyboardHint · useIsMac
Module thuần
Không phải component: hằng và hàm thuần, không import React chút nào, nên chạy được trong route handler, trong test hay trong Server Component cũng như trên trình duyệt.
Hợp đồng trạng thái chung. App tự khai các trạng thái của mình; gói cung cấp kiểu và một bảng tông gồm các class token. Tài liệu đặt cùng StatusPill.
STATUS_TONES · getStatusConfig · getStatusClasses · getStatusLabel · sortByStatusPriority
Một quy tắc chuẩn hoá cho cả gói: bỏ dấu tiếng Việt, chữ thường, cắt khoảng trắng. Mọi ô tìm kiếm trong hệ sinh thái khớp theo cùng một cách.
removeVietnameseTones · normalizeVi · matchesSearch · getVietnameseInitials · truncate
Chữ đi theo một provider, và chuỗi nào cũng đổi được
Mọi chuỗi ở tầng này đến từ một bảng cho mỗi component, kể cả những chuỗi chỉ trình đọc màn hình nghe thấy — aria-label của checkbox trên một hàng bảng, bốn nhãn sr-only trên các mũi tên phân trang, tên của nút mở lịch, câu được đọc lên khi một thẻ đang được kéo qua bảng. Không có provider thì các bảng đó là tiếng Anh; một ComitorLocaleProvider ở gốc đổi tất cả cùng lúc, và đó là cách duy nhất chạm tới những chuỗi không ảnh chụp màn hình nào thấy. Bước ngôn ngữ của hướng dẫn cài đặt nói về chính provider; dưới đây là cách các bảng cư xử khi đã có nó.
Tiếng Việt
trang này: provider ở gốc của site
English
<ComitorLocaleProvider locale="en">
labelslà mộtPartial<>. Component trải nó lên bảng của provider, nên ghi đè một khoá thì các khoá còn lại vẫn ở ngôn ngữ đang dùng. Các hằngDEFAULT_*_LABELSvẫn được export — là các bảng tiếng Anh, trong module không có'use client', đọc được từ code server — còn bảng tiếng Việt của bất kỳ component nào làcomitorMessagesVi.<namespace>.- Khoá có chèn giá trị là hàm.
range(start, end, total, unitLabel),pageStatus(page, pageCount),filterCount(count),pageSizeSelect(unitLabel)— không bao giờ là template có placeholder, vì thứ tự từ không giống nhau giữa các ngôn ngữ.resultSummaryđi xa hơn và trả về mộtReactNode: con số cần phần tử riêng để giữtabular-nums, cho nó khỏi rung khi số đếm đổi. Không truyền được hàm từ Server Component, nên khai các khoá này trong một Client Component. - Prop riêng thắng
labels.unitLabel,labelvàsearchPlaceholdervẫn là prop riêng và được ưu tiên. Chúng đổi theo nội dung — “contacts”, “invoices” — còn các bảng đổi theo ngôn ngữ. Một provider cho mỗi app, một prop cho mỗi bảng. - Prop chuỗi đơn cũng theo provider.
CopyButton,ConfirmDialog,InlineEdit,CommandPalettevàAvatarGroupnhận từng prop chuỗi thay vì một túi — bỏ trống một prop thì nó đọc cùng bảng đó: “Copy” và “Confirm” bằng tiếng Anh, “Sao chép” và “Xác nhận” dướilocale="vi".FilterChipslấy chữ trên chip từ các item được truyền vào; hai chuỗi item không mang được —label, tên của nhóm cho trình đọc màn hình (“Quick filters”), vàallLabel, chip đặt lại màshowAllthêm vào (“All”) — lấy từ bảng. - Ngày tháng theo cùng provider.
DatePicker,RelativeTimevàCalendarđịnh dạng bằng locale date-fns của provider —vicho tiếng Việt,enUScho mọi tag khác trừ khi provider được truyềndateFnsLocale— và proplocalecủa riêng chúng vẫn ghi đè ngày tháng của một instance. Định dạng gõ tay cũng theo:MM/dd/yyyybằng tiếng Anh,dd/MM/yyyybằng tiếng Việt.
Icon là component, không phải element
Mọi prop icon ở tầng này — EmptyState, PageHeader, StatCard, FormSection, ConfirmDialog, Combobox, FilterChips, CommandPalette — có kiểu IconComponent và nhận chính component. Icon Lucide là một object forwardRef chứ không phải hàm, nên phép kiểm typeof mà một component hay dùng không phân biệt được nó với một element đã render — cả hai đều trả 'object'. Đoán thì sẽ sai mà không báo gì, nên kiểu dữ liệu từ chối việc đoán. Khi cần nội dung tuỳ ý thì có slot riêng — media trên EmptyState, sparkline trên StatCard.
import { Inbox } from 'lucide-react'
import { EmptyState } from '@comitor/ui'
// ✓ the component itself — the composite renders it and sets aria-hidden
<EmptyState icon={Inbox} title="No invoices yet" />
// ✗ an already-rendered element — a type error, on purpose
<EmptyState icon={<Inbox />} title="No invoices yet" />Server hay client
Tám composite không có 'use client' của riêng mình và render thẳng từ Server Component; mọi thứ khác trên trang này, kể cả mười tám mẫu thêm ở 2.1.0, là Client Component mà trang server vẫn render được — chỉ là không truyền hàm vào được. Đây là quyết định có chủ ý về chi phí, không phải tình cờ: một danh sách hai trăm hàng render bằng Avatar của Radix sẽ là hai trăm đảo client, nên LetterAvatar được viết từ đầu.
PageHeaderPageContainerRouteTabsStatCardStatusPillEmptyStateLetterAvatarIconAvatarCác hằng là nửa còn lại của chuyện này. Nhãn mặc định, preset ngày, STATUS_TONES, SELECT_EMPTY_VALUE và các helper xử lý chữ đều nằm trong module cố ý không có chỉ thị 'use client', dù component bên cạnh có. Mọi thứ export từ một module client tới Server Component dưới dạng proxy tham chiếu client, đọc ra undefined mà không throw, không cảnh báo — và hằng chính là thứ code server hay với tới, để trải lên một giá trị mặc định hay dựng một danh sách. Để chúng ngoài ranh giới là điều khiến { ...DEFAULT_DATA_TABLE_LABELS } an toàn trên trang server.
Hai cái tên cần phân biệt
FormFieldtừ@comitor/uilà composite ở tầng này: một ô lưới có nhãn, mô tả và lỗi, trao cho control id và các thuộc tính aria.FormFieldtừ@comitor/ui/formlà một component khác trùng tên — wrapper shadcn quanhControllercủa react-hook-form. Dùng chung được, nhưng không bao giờ import chung.Toastervàtoast()từ@comitor/uilà cặp dùng hằng ngày, có icon trạng thái của Comitor và không phụ thuộc next-themes.SonnerToastertrong@comitor/ui/shelllà bản mỏng biết theme, đặt trong root layout. Cả hai có tài liệu ở trang Toast.
Trợ năng trên toàn tầng
- Role mô tả đúng điều thực sự xảy ra.
RouteTabslà một<nav>gồm các link đánh dấuaria-current="page"chứ không phải tablist, vì trên trang không có tabpanel nào đểaria-controlstrỏ vào, và một tablist roving sẽ nuốt các phím mũi tên mà một danh sách link nên để yên.FilterChipsdùngaria-pressedtrên button thật cũng vì lý do đó: nó bật tắt một điều kiện, không đổi panel. - Màu không bao giờ là tín hiệu duy nhất, và thứ được tô màu được đo với chính thứ nằm sau nó. Chữ trạng thái chạy ở bậc
-inkvì bậc fill đặc chỉ cho 2,69:1 với success và 3,44:1 với info trên tint của chính chúng — cái đầu dưới cả ngưỡng 3:1 cho phần không phải chữ, cả hai đều thấp xa so với 4,5:1 mà chữ của chip phải đạt; bậc ink vượt 4,5:1 ở cả hai theme, hẹp nhất 4,86:1. Viền chip lấy màu ink đậm hết mức, vì ở variant outline viền là đường biên duy nhất của chip. - Icon là trang trí cho tới khi chứng minh được điều ngược lại.
IconComponentnhậnaria-hiddentừ composite render nó, còn ý nghĩa do chữ bên cạnh mang. Chỗ nào control chỉ có icon — mũi tên phân trang, nút xoá trong ô tìm kiếm, nút mở lịch — tên nằm trong một spansr-onlymà túilabelsdịch được. LetterAvatarluôn xuất tên đầy đủ trong một spansr-only. Bản thân chữ viết tắt làaria-hidden— đọc to “NT” chỉ là tạp âm — và nhánh có ảnh dùngalt=""để tên không bị đọc hai lần.- Trạng thái cần thời gian thì nói ra.
ConfirmDialogvàInlineEditgiữ nguyên mở và bận trong lúc promise trả về chưa xong, nên control không bị bấm hai lần.SearchFilterBarđặt số kết quả trong một vùngrole="status"có chiều cao dành sẵn, nên khi lọc thì số mới được đọc lên mà bố cục không nhảy dưới con trỏ. RelativeTimerender một<time>thật vớidateTimemáy đọc được và mốc thời gian tuyệt đối trongtitle— và lùi về span thường khi ngày không hợp lệ, thay vì công bốInvalid Datenhư dữ liệu có cấu trúc.useIsMactrảfalseở lần render đầu, kể cả trên Mac. Server không biết nền tảng, và lệch nhau thì React bỏ cả cây con; hiện “Ctrl” một khung hình trước khi thành “⌘” là cái giá rẻ hơn.
Tầng tiếp theo
Composite lấp đầy một trang. Khung quanh nó — sidebar, header, bộ chuyển workspace, app launcher, và bốn trục hiển thị độc lập — là Tầng 3, cần Next và next-themes nên nằm sau entry riêng.
Shell@comitor/ui/shell