@qijenchen/design-system 0.1.0-beta.10 → 0.1.0-beta.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +201 -0
- package/README.md +7 -15
- package/cli-init.mjs +90 -0
- package/ds-canonical/commands/README.md +26 -0
- package/ds-canonical/commands/gov-status.md +79 -0
- package/ds-canonical/hooks/README.md +145 -0
- package/ds-canonical/hooks/_log-fire.sh +44 -0
- package/ds-canonical/hooks/block_prototype_imports.py +111 -0
- package/ds-canonical/hooks/check_app_shell_primary_header_consistency.sh +68 -0
- package/ds-canonical/hooks/check_audit_post_report_validator.sh +88 -0
- package/ds-canonical/hooks/check_audit_sample_escape.sh +73 -0
- package/ds-canonical/hooks/check_benchmark_citation.sh +106 -0
- package/ds-canonical/hooks/check_canonical_propagation.sh +189 -0
- package/ds-canonical/hooks/check_chrome_header_handcraft.sh +70 -0
- package/ds-canonical/hooks/check_codex_brief_invariants.sh +83 -0
- package/ds-canonical/hooks/check_codex_collab_5step.sh +108 -0
- package/ds-canonical/hooks/check_datatable_invariants.sh +117 -0
- package/ds-canonical/hooks/check_dim_count_drift.sh +72 -0
- package/ds-canonical/hooks/check_field_controls_contracts.sh +110 -0
- package/ds-canonical/hooks/check_field_family_invariants.sh +205 -0
- package/ds-canonical/hooks/check_file_size_budget.sh +60 -0
- package/ds-canonical/hooks/check_header_with_tabs_border.sh +87 -0
- package/ds-canonical/hooks/check_main_branch_workbench.sh +93 -0
- package/ds-canonical/hooks/check_naming_and_abstraction.sh +165 -0
- package/ds-canonical/hooks/check_opacity_token_usage.sh +149 -0
- package/ds-canonical/hooks/check_pattern_invariants.sh +194 -0
- package/ds-canonical/hooks/check_peoplepicker_ssot_drift.sh +56 -0
- package/ds-canonical/hooks/check_pixel_quantified_audit.sh +53 -0
- package/ds-canonical/hooks/check_propose_plain_chinese.sh +74 -0
- package/ds-canonical/hooks/check_propose_pre_grep_verify.sh +70 -0
- package/ds-canonical/hooks/check_select_all_canonical.sh +58 -0
- package/ds-canonical/hooks/check_solo_workflow.sh +258 -0
- package/ds-canonical/hooks/check_spec_class_drift.sh +88 -0
- package/ds-canonical/hooks/check_story_invariants.sh +612 -0
- package/ds-canonical/hooks/check_substantive_edit_approval_preflight.sh +105 -0
- package/ds-canonical/hooks/check_tab_lg_chrome_header_equal.sh +66 -0
- package/ds-canonical/hooks/check_wrapper_primitive_schema_drift.sh +104 -0
- package/ds-canonical/hooks/enforce_home_charter.sh +44 -0
- package/ds-canonical/hooks/inject_pending_self_audit.sh +204 -0
- package/ds-canonical/hooks/lib/_approval_re.sh +33 -0
- package/ds-canonical/hooks/lib/_code_quality.sh +73 -0
- package/ds-canonical/hooks/lib/_cva_default_sync.sh +69 -0
- package/ds-canonical/hooks/lib/_governance_coverage_check.sh +49 -0
- package/ds-canonical/hooks/lib/_hardcoded_strings.sh +163 -0
- package/ds-canonical/hooks/lib/_layout_space_canonical.sh +56 -0
- package/ds-canonical/hooks/lib/_overlay_handcraft.sh +141 -0
- package/ds-canonical/hooks/lib/_person_data_richness.sh +42 -0
- package/ds-canonical/hooks/lib/_story_compile_drift.sh +48 -0
- package/ds-canonical/hooks/lib/_token_hygiene.sh +95 -0
- package/ds-canonical/hooks/log_governance_fires.sh +50 -0
- package/ds-canonical/hooks/log_skill_invokes.sh +41 -0
- package/ds-canonical/hooks/post_edit_dispatcher.sh +62 -0
- package/ds-canonical/hooks/retired/check_anatomy_section_numbering.sh +106 -0
- package/ds-canonical/hooks/retired/check_avatar_hovercard.sh +90 -0
- package/ds-canonical/hooks/retired/check_button_icon_literal.sh.retired-2026-04-28 +38 -0
- package/ds-canonical/hooks/retired/check_container_breathing.sh +142 -0
- package/ds-canonical/hooks/retired/check_governance_compliance.sh +61 -0
- package/ds-canonical/hooks/retired/check_icon_only_padding_formula.sh +104 -0
- package/ds-canonical/hooks/retired/check_item_content_primitive.sh +150 -0
- package/ds-canonical/hooks/retired/check_item_list_gap.sh +153 -0
- package/ds-canonical/hooks/retired/check_sideoffset_canonical.sh +65 -0
- package/ds-canonical/hooks/retired/check_spec_iteration_tag.sh +87 -0
- package/ds-canonical/hooks/retired/check_ssot_consultation.sh +88 -0
- package/ds-canonical/hooks/retired/check_sync_update.sh +20 -0
- package/ds-canonical/hooks/retired/check_third_party_dom_verified.sh +95 -0
- package/ds-canonical/hooks/retired/enforce_home_charter.sh +125 -0
- package/ds-canonical/hooks/retired/post_edit_canonical_interrogate.sh +109 -0
- package/ds-canonical/hooks/retired/pre_edit_spec_check.sh +68 -0
- package/ds-canonical/hooks/retired/pre_new_component_spec.sh +39 -0
- package/ds-canonical/hooks/retired/pre_write_subsumption_check.sh +112 -0
- package/ds-canonical/hooks/retired/stop_meta_self_audit.sh.retired-2026-05-13 +76 -0
- package/ds-canonical/hooks/retired/tests/test_check_anatomy_section_numbering.sh +14 -0
- package/ds-canonical/hooks/retired/tests/test_check_avatar_hovercard.sh +15 -0
- package/ds-canonical/hooks/retired/tests/test_check_container_breathing.sh +15 -0
- package/ds-canonical/hooks/retired/tests/test_check_governance_compliance.sh +15 -0
- package/ds-canonical/hooks/retired/tests/test_check_icon_only_padding_formula.sh +79 -0
- package/ds-canonical/hooks/retired/tests/test_check_item_content_primitive.sh +15 -0
- package/ds-canonical/hooks/retired/tests/test_check_item_list_gap.sh +163 -0
- package/ds-canonical/hooks/retired/tests/test_check_sideoffset_canonical.sh +15 -0
- package/ds-canonical/hooks/retired/tests/test_check_spec_iteration_tag.sh +15 -0
- package/ds-canonical/hooks/retired/tests/test_check_ssot_consultation.sh +15 -0
- package/ds-canonical/hooks/retired/tests/test_check_sync_update.sh +14 -0
- package/ds-canonical/hooks/retired/tests/test_check_third_party_dom_verified.sh +15 -0
- package/ds-canonical/hooks/retired/tests/test_enforce_home_charter.sh +15 -0
- package/ds-canonical/hooks/retired/tests/test_pre_edit_spec_check.sh +15 -0
- package/ds-canonical/hooks/retired/tests/test_pre_new_component_spec.sh +15 -0
- package/ds-canonical/hooks/retired/tests/test_pre_write_subsumption_check.sh +63 -0
- package/ds-canonical/hooks/session_start_governance_check.sh +263 -0
- package/ds-canonical/hooks/stop_passive_logging.sh +322 -0
- package/ds-canonical/hooks/stop_self_audit.sh +450 -0
- package/ds-canonical/hooks/tests/KNOWN-BROKEN.md +15 -0
- package/ds-canonical/hooks/tests/run-all.sh +76 -0
- package/ds-canonical/hooks/tests/test_block_prototype_imports.sh +143 -0
- package/ds-canonical/hooks/tests/test_check_app_shell_primary_header_consistency.sh +140 -0
- package/ds-canonical/hooks/tests/test_check_audit_post_report_validator.sh +115 -0
- package/ds-canonical/hooks/tests/test_check_audit_sample_escape.sh +93 -0
- package/ds-canonical/hooks/tests/test_check_benchmark_citation.sh +115 -0
- package/ds-canonical/hooks/tests/test_check_canonical_propagation.sh +133 -0
- package/ds-canonical/hooks/tests/test_check_chrome_header_handcraft.sh +123 -0
- package/ds-canonical/hooks/tests/test_check_code_quality.sh +15 -0
- package/ds-canonical/hooks/tests/test_check_codex_collab_5step.sh +96 -0
- package/ds-canonical/hooks/tests/test_check_cva_default_sync.sh +15 -0
- package/ds-canonical/hooks/tests/test_check_datatable_invariants.sh +122 -0
- package/ds-canonical/hooks/tests/test_check_dim_count_drift.sh +98 -0
- package/ds-canonical/hooks/tests/test_check_field_controls_contracts.sh +126 -0
- package/ds-canonical/hooks/tests/test_check_field_family_invariants.sh +194 -0
- package/ds-canonical/hooks/tests/test_check_file_size_budget.sh +32 -0
- package/ds-canonical/hooks/tests/test_check_hardcoded_strings.sh +14 -0
- package/ds-canonical/hooks/tests/test_check_header_with_tabs_border.sh +110 -0
- package/ds-canonical/hooks/tests/test_check_layout_space_canonical.sh +73 -0
- package/ds-canonical/hooks/tests/test_check_main_branch_workbench.sh +147 -0
- package/ds-canonical/hooks/tests/test_check_naming_and_abstraction.sh +136 -0
- package/ds-canonical/hooks/tests/test_check_opacity_token_usage.sh +110 -0
- package/ds-canonical/hooks/tests/test_check_overlay_handcraft.sh +126 -0
- package/ds-canonical/hooks/tests/test_check_pattern_invariants.sh +148 -0
- package/ds-canonical/hooks/tests/test_check_peoplepicker_ssot_drift.sh +108 -0
- package/ds-canonical/hooks/tests/test_check_person_data_richness.sh +58 -0
- package/ds-canonical/hooks/tests/test_check_pixel_quantified_audit.sh +142 -0
- package/ds-canonical/hooks/tests/test_check_propose_plain_chinese.sh +126 -0
- package/ds-canonical/hooks/tests/test_check_propose_pre_grep_verify.sh +117 -0
- package/ds-canonical/hooks/tests/test_check_select_all_canonical.sh +125 -0
- package/ds-canonical/hooks/tests/test_check_solo_workflow.sh +201 -0
- package/ds-canonical/hooks/tests/test_check_spec_class_drift.sh +135 -0
- package/ds-canonical/hooks/tests/test_check_story_anatomy.sh.broken +197 -0
- package/ds-canonical/hooks/tests/test_check_story_category.sh.broken +187 -0
- package/ds-canonical/hooks/tests/test_check_story_compile_drift.sh +15 -0
- package/ds-canonical/hooks/tests/test_check_story_invariants.sh +209 -0
- package/ds-canonical/hooks/tests/test_check_story_name_jargon.sh.broken +53 -0
- package/ds-canonical/hooks/tests/test_check_story_slot_split.sh +156 -0
- package/ds-canonical/hooks/tests/test_check_substantive_edit_approval_preflight.sh +176 -0
- package/ds-canonical/hooks/tests/test_check_tab_lg_chrome_header_equal.sh +138 -0
- package/ds-canonical/hooks/tests/test_check_token_hygiene.sh +21 -0
- package/ds-canonical/hooks/tests/test_check_wrapper_primitive_schema_drift.sh +169 -0
- package/ds-canonical/hooks/tests/test_enforce_home_charter.sh +77 -0
- package/ds-canonical/hooks/tests/test_inject_pending_self_audit.sh +125 -0
- package/ds-canonical/hooks/tests/test_log_governance_fires.sh +10 -0
- package/ds-canonical/hooks/tests/test_log_skill_invokes.sh +7 -0
- package/ds-canonical/hooks/tests/test_post_edit_dispatcher.sh +108 -0
- package/ds-canonical/hooks/tests/test_session_start_governance_check.sh +143 -0
- package/ds-canonical/hooks/tests/test_stop_capture_metrics.sh +95 -0
- package/ds-canonical/hooks/tests/test_stop_governance_drift_check.sh.broken +125 -0
- package/ds-canonical/hooks/tests/test_stop_harvest_corrections.sh +10 -0
- package/ds-canonical/hooks/tests/test_stop_passive_logging.sh +100 -0
- package/ds-canonical/hooks/tests/test_stop_self_audit.sh +76 -0
- package/ds-canonical/hooks/tests/test_stop_tsc_sanity.sh +10 -0
- package/ds-canonical/references/README.md +43 -0
- package/ds-canonical/references/audit-coverage-vs-24-checklist.md +74 -0
- package/ds-canonical/references/build-ui-canonicals.md +69 -0
- package/ds-canonical/references/cva-patterns.md +41 -0
- package/ds-canonical/references/drag-canonical.md +331 -0
- package/ds-canonical/references/item-anatomy-recipe.md +225 -0
- package/ds-canonical/references/naming-conventions.md +56 -0
- package/ds-canonical/references/principle-dim-map.json +515 -0
- package/ds-canonical/references/props-naming.md +45 -0
- package/ds-canonical/references/spec-rules.md +58 -0
- package/ds-canonical/references/ssot-consultation.md +63 -0
- package/ds-canonical/references/ssot-index.md +40 -0
- package/ds-canonical/references/story-baseline-registry.json +79 -0
- package/ds-canonical/references/structural-token-retention.md +42 -0
- package/ds-canonical/references/tailwind-gotchas.md +87 -0
- package/ds-canonical/references/ui-dev-rules.md +60 -0
- package/ds-canonical/rules/README.md +34 -0
- package/ds-canonical/rules/meta-patterns.md +87 -0
- package/ds-canonical/rules/self-verify.md +53 -0
- package/ds-canonical/rules/spec-rules.md +25 -0
- package/ds-canonical/rules/story-rules.md +56 -0
- package/ds-canonical/rules/ui-development.md +87 -0
- package/ds-canonical/skills/README.md +88 -0
- package/ds-canonical/skills/bug-fix-rhythm/SKILL.md +181 -0
- package/ds-canonical/skills/code-quality-audit/SKILL.md +63 -0
- package/ds-canonical/skills/codex-collab/SKILL.md +249 -0
- package/ds-canonical/skills/codex-collab/references/brief-template.md +48 -0
- package/ds-canonical/skills/codex-collab/references/transport.md +58 -0
- package/ds-canonical/skills/codify-corrections/SKILL.md +184 -0
- package/ds-canonical/skills/codify-principle/SKILL.md +151 -0
- package/ds-canonical/skills/component-quality-gate/SKILL.md +102 -0
- package/ds-canonical/skills/component-quality-gate/references/checklist.md +79 -0
- package/ds-canonical/skills/deep-audit-cross-codex/SKILL.md +247 -0
- package/ds-canonical/skills/deep-audit-cross-codex/references/phase-a-workflow.md +123 -0
- package/ds-canonical/skills/deep-audit-cross-codex/references/phase-b-codex-brief.md +165 -0
- package/ds-canonical/skills/deep-audit-cross-codex/references/triage-rubric.md +91 -0
- package/ds-canonical/skills/delivery-handoff/SKILL.md +229 -0
- package/ds-canonical/skills/delivery-handoff/references/flow-diagram.md +180 -0
- package/ds-canonical/skills/delivery-handoff/references/handoff-template.md +177 -0
- package/ds-canonical/skills/delivery-handoff/references/inventory-checklist.md +196 -0
- package/ds-canonical/skills/design-system-audit/SKILL.md +343 -0
- package/ds-canonical/skills/design-system-audit/references/audit-prompts.md +1260 -0
- package/ds-canonical/skills/design-system-audit/references/checkpoints.md +240 -0
- package/ds-canonical/skills/design-system-audit/references/historical-bugs.md +240 -0
- package/ds-canonical/skills/design-system-audit/references/principle-audit-protocol.md +364 -0
- package/ds-canonical/skills/design-system-audit/references/rule-placement.md +175 -0
- package/ds-canonical/skills/design-system-audit/references/spec-template.md +66 -0
- package/ds-canonical/skills/ensure-canonical/SKILL.md +196 -0
- package/ds-canonical/skills/governance-health/SKILL.md +146 -0
- package/ds-canonical/skills/knowledge-prune/SKILL.md +303 -0
- package/ds-canonical/skills/new-component/SKILL.md +170 -0
- package/ds-canonical/skills/new-component/references/new-component-checklist.md +85 -0
- package/ds-canonical/skills/performance-audit/SKILL.md +107 -0
- package/ds-canonical/skills/product-ui-audit/SKILL.md +230 -0
- package/ds-canonical/skills/product-ui-audit/references/audit-checks.md +246 -0
- package/ds-canonical/skills/product-ui-audit/references/common-misuses.md +329 -0
- package/ds-canonical/skills/product-ui-audit/references/report-template.md +159 -0
- package/ds-canonical/skills/propose-options/SKILL.md +177 -0
- package/ds-canonical/skills/prototype/SKILL.md +244 -0
- package/ds-canonical/skills/prototype/references/audit-checks.md +37 -0
- package/ds-canonical/skills/prototype/references/benchmark-sources.md +94 -0
- package/ds-canonical/skills/prototype/references/checkpoints.md +191 -0
- package/ds-canonical/skills/prototype/references/evaluation-matrix.md +141 -0
- package/ds-canonical/skills/prototype/references/ooux-template.md +198 -0
- package/ds-canonical/skills/prototype/references/proposal-template.md +229 -0
- package/ds-canonical/skills/scan-similar-bugs/SKILL.md +198 -0
- package/ds-canonical/skills/story-auto-compile-migrate/SKILL.md +159 -0
- package/ds-canonical/skills/story-writing/SKILL.md +122 -0
- package/ds-canonical/skills/story-writing/references/anatomy-standard.md +217 -0
- package/ds-canonical/skills/story-writing/references/category-templates.md +174 -0
- package/ds-canonical/skills/story-writing/references/example-selection.md +70 -0
- package/ds-canonical/skills/story-writing/references/self-check.md +20 -0
- package/ds-canonical/skills/ux-audit/SKILL.md +130 -0
- package/ds-canonical/skills/visual-audit/SKILL.md +245 -0
- package/ds-canonical/skills/visual-audit/output/.gitkeep +0 -0
- package/ds-canonical/skills/visual-audit/references/audit-architecture.md +100 -0
- package/ds-canonical/skills/visual-audit/references/visual-checklist.md +297 -0
- package/ds-canonical/skills/visual-audit/references/world-class-benchmarks.md +198 -0
- package/package.json +9 -5
- package/src/components/Accordion/accordion.spec.md +114 -0
- package/src/components/Alert/alert.spec.md +197 -0
- package/src/components/AppShell/app-shell.spec.md +331 -0
- package/src/components/AspectRatio/aspect-ratio.spec.md +134 -0
- package/src/components/Avatar/avatar.spec.md +329 -0
- package/src/components/Badge/badge.spec.md +380 -0
- package/src/components/Breadcrumb/breadcrumb.spec.md +257 -0
- package/src/components/BulkActionBar/bulk-action-bar.spec.md +210 -0
- package/src/components/Button/button.spec.md +460 -0
- package/src/components/Calendar/calendar.spec.md +242 -0
- package/src/components/Carousel/carousel.spec.md +253 -0
- package/src/components/Chart/chart.spec.md +155 -0
- package/src/components/Checkbox/checkbox.spec.md +344 -0
- package/src/components/Chip/chip.spec.md +237 -0
- package/src/components/CircularProgress/circular-progress.spec.md +268 -0
- package/src/components/Coachmark/coachmark.spec.md +230 -0
- package/src/components/Combobox/combobox.spec.md +180 -0
- package/src/components/Command/command.spec.md +171 -0
- package/src/components/DataTable/data-table.spec.md +525 -0
- package/src/components/DateGrid/date-grid.spec.md +215 -0
- package/src/components/DatePicker/date-picker.spec.md +334 -0
- package/src/components/DescriptionList/description-list.spec.md +214 -0
- package/src/components/Dialog/dialog.spec.md +202 -0
- package/src/components/DropdownMenu/dropdown-menu.spec.md +250 -0
- package/src/components/Empty/empty.spec.md +214 -0
- package/src/components/Field/field-controls.spec.md +338 -0
- package/src/components/Field/field.spec.md +438 -0
- package/src/components/Field/form-validation.spec.md +152 -0
- package/src/components/FieldControlGroup/field-control-group.spec.md +176 -0
- package/src/components/FileItem/file-item.spec.md +467 -0
- package/src/components/FileUpload/file-upload.spec.md +123 -0
- package/src/components/FileViewer/file-viewer.spec.md +373 -0
- package/src/components/HoverCard/hover-card.spec.md +157 -0
- package/src/components/Input/input.spec.md +193 -0
- package/src/components/LinkInput/link-input.spec.md +130 -0
- package/src/components/Menu/menu-item.spec.md +290 -0
- package/src/components/NameCard/name-card.spec.md +171 -0
- package/src/components/Notice/notice.spec.md +149 -0
- package/src/components/NumberInput/number-input.spec.md +126 -0
- package/src/components/OverflowIndicator/overflow-indicator.spec.md +120 -0
- package/src/components/PeoplePicker/people-picker.spec.md +263 -0
- package/src/components/Popover/popover.spec.md +198 -0
- package/src/components/ProgressBar/progress-bar.spec.md +232 -0
- package/src/components/RadioGroup/radio-group.spec.md +141 -0
- package/src/components/Rating/rating.spec.md +208 -0
- package/src/components/ScrollArea/scroll-area.spec.md +145 -0
- package/src/components/SegmentedControl/segmented-control.spec.md +295 -0
- package/src/components/Select/select.spec.md +299 -0
- package/src/components/SelectMenu/select-menu.spec.md +220 -0
- package/src/components/SelectionControl/selection-item.spec.md +128 -0
- package/src/components/Separator/separator.spec.md +109 -0
- package/src/components/Sheet/sheet.spec.md +148 -0
- package/src/components/Sidebar/sidebar.spec.md +713 -0
- package/src/components/Skeleton/skeleton.spec.md +104 -0
- package/src/components/Slider/slider.spec.md +353 -0
- package/src/components/Steps/steps.spec.md +465 -0
- package/src/components/Switch/switch.spec.md +215 -0
- package/src/components/Tabs/tabs.spec.md +314 -0
- package/src/components/Tag/tag.spec.md +282 -0
- package/src/components/Textarea/textarea.spec.md +151 -0
- package/src/components/TimePicker/time-picker.spec.md +279 -0
- package/src/components/Toast/toast.spec.md +177 -0
- package/src/components/Tooltip/tooltip.spec.md +139 -0
- package/src/components/TreeView/tree-view.spec.md +374 -0
- package/src/patterns/action-bar/action-bar.spec.md +458 -0
- package/src/patterns/element-anatomy/element-anatomy.spec.md +215 -0
- package/src/patterns/element-anatomy/inline-action.spec.md +315 -0
- package/src/patterns/element-anatomy/item-anatomy.spec.md +1042 -0
- package/src/patterns/header-canonical/header-canonical.spec.md +285 -0
- package/src/patterns/horizontal-overflow/horizontal-overflow.spec.md +191 -0
- package/src/patterns/overlay-surface/overlay-surface.spec.md +428 -0
- package/src/patterns/resize-handle/resize-handle.spec.md +109 -0
- package/src/tokens/color/color.spec.md +804 -0
- package/src/tokens/density/density.spec.md +127 -0
- package/src/tokens/elevation/elevation.spec.md +81 -0
- package/src/tokens/layoutSpace/layoutSpace.spec.md +314 -0
- package/src/tokens/motion/motion.spec.md +97 -0
- package/src/tokens/opacity/opacity.spec.md +78 -0
- package/src/tokens/orphan-tokens.spec.md +117 -0
- package/src/tokens/radius/radius.spec.md +123 -0
- package/src/tokens/typography/typography.spec.md +202 -0
- package/src/tokens/uiSize/uiSize.spec.md +438 -0
- package/src/styles/preset.css +0 -31
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
---
|
|
2
|
+
pattern: header-canonical
|
|
3
|
+
internal: true
|
|
4
|
+
scope: cross-component header SSOT (chrome + overlay families)
|
|
5
|
+
benchmark:
|
|
6
|
+
- GitHub Primer PageHeader: https://primer.style/components/page-header/react — hasBorder auto-suppress when Navigation slot contains UnderlineNav
|
|
7
|
+
- Ant Design Tabs: https://ant.design/components/tabs — "Large size tabs are usually used in page header, and small size could be used in Modal"
|
|
8
|
+
- Material UI Tabs: https://mui.com/material-ui/react-tabs/ — Box wrapper draws border, Tabs itself does not (counter-pattern)
|
|
9
|
+
- IBM Carbon Tabs: https://carbondesignsystem.com/components/tabs/usage — defining line is implementation team responsibility, not part of line tabs component
|
|
10
|
+
- Material Design v1/v2 Tabs: https://m1.material.io/components/tabs.html — 48dp default height single size, 2dp indicator
|
|
11
|
+
- Mantine Tabs: https://mantine.dev/core/tabs/ — default variant includes bottom border (self-contained)
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Header Canonical 設計原則
|
|
15
|
+
|
|
16
|
+
> **Foundational SSOT rationale**(cap 800):本 pattern 統一**所有 header 家族**(chrome page header + overlay header)的視覺契約 — border ownership / padding / 高度策略 / tabs 連動 / dismiss canonical。6 個 consumer(`SidebarHeader` / `FileViewer Toolbar+InfoPanel` / `DialogHeader` / `SheetHeader` / `PopoverHeader` / 未來 Drawer)pointer 指向本 SSOT,**禁各自 hardcode**。
|
|
17
|
+
|
|
18
|
+
## 定位
|
|
19
|
+
|
|
20
|
+
兩個 header 家族(實作 pattern 不同,**視覺契約相同**):
|
|
21
|
+
|
|
22
|
+
| Family | 實作 | Consumer | Spec home |
|
|
23
|
+
|---|---|---|---|
|
|
24
|
+
| **A. Padding-based**(overlay)| `py-[var(--layout-space-tight)]` + `data-unbounded` slot trick;高度由內容決定但對齊 `--chrome-header-height` | DialogHeader / SheetHeader / PopoverHeader / 未來 Drawer | `patterns/overlay-surface/overlay-surface.spec.md`(L1 SSOT)+ 本 spec(cross-family canonical) |
|
|
25
|
+
| **B. Fixed-height**(chrome)| `h-[var(--chrome-header-height)]` + `items-center`,剛性高度 | SidebarHeader / FileViewer Toolbar / FileViewer InfoPanel / Page top bar | `tokens/uiSize/uiSize.spec.md` L242(`Fixed-height` 段)+ 本 spec(cross-family canonical) |
|
|
26
|
+
|
|
27
|
+
**為什麼兩家族?** 因 overlay 內 slot 可能塞 multi-line content(`HoverCard` title + description)— 剛性高度會切掉。Chrome 家族 永遠 single-row(toolbar / nav)— 剛性高度給 layout 預測性。兩 pattern 並存(對齊 `tokens/uiSize/uiSize.spec.md` L240-260「Padding-based vs Fixed-h decision tree」)。
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 跨家族共同視覺契約(both A + B)
|
|
32
|
+
|
|
33
|
+
### 1. Border-bottom
|
|
34
|
+
|
|
35
|
+
- **Token**:`border-b border-divider`(1px / `--divider-color`)
|
|
36
|
+
- **Default**:有 border(分區效果)
|
|
37
|
+
- **Auto-suppress when withTabs**:見下「withTabs 連動」段
|
|
38
|
+
|
|
39
|
+
### 2. Horizontal padding
|
|
40
|
+
|
|
41
|
+
- **Token**:`px-[var(--layout-space-loose)]`(md=16px / lg=24px)
|
|
42
|
+
- **不可 override**:tabs / content / actions 所有 slot 統一靠齊此 padding
|
|
43
|
+
|
|
44
|
+
### 3. Vertical rhythm
|
|
45
|
+
|
|
46
|
+
- **A 家族**:`py-[var(--layout-space-tight)]`(md=8px / lg=12px)+ `data-unbounded` slot trick → 實際高度對齊 `--chrome-header-height`
|
|
47
|
+
- **B 家族**:`h-[var(--chrome-header-height)]`(md=48px / lg=56px)+ `items-center`
|
|
48
|
+
|
|
49
|
+
### 4. Dismiss icon button(close X)
|
|
50
|
+
|
|
51
|
+
- **Token**:`<Button iconOnly size="sm" dismiss />`(28px @ md / 32px @ lg)
|
|
52
|
+
- **永遠 size="sm"**(不論家族 / density):chrome / overlay header 內 button 一律 sm
|
|
53
|
+
- 對齊 既有 `dialog.tsx:132-153` + `sheet.tsx:122-139` + `popover.tsx:87-110`(Dialog/Sheet/Popover 已內建 close X)
|
|
54
|
+
|
|
55
|
+
### 4.5 Chrome header avatar SSOT(2026-05-21 codify per user directive)
|
|
56
|
+
|
|
57
|
+
**Canonical**:Chrome header brand mark avatar = **24px,density-fixed,row-size-fixed**。
|
|
58
|
+
|
|
59
|
+
對齊 5 家世界級共識(Linear / Notion / Figma / Slack / Polaris chrome header brand mark 皆固定 24px,不 density-scale 也不 row-size-scale)。設計理由:avatar 是品牌識別 mark,視覺穩定優於 density 緊鬆 / row size 調整(button 跟 density 走 touch target 邏輯,avatar 不該綁同邏輯)。
|
|
60
|
+
|
|
61
|
+
**Chrome header 不是 row context**:無 sm/md/lg row size lookup 需求 → chrome header 內 avatar 用 **raw `<Avatar size={24}>`**,**不用 `<ItemAvatar>`**(後者是 row primitive anatomy helper,scope 是 row context — 詳 `item-anatomy.spec.md:535` scope 限定段)。
|
|
62
|
+
|
|
63
|
+
**程式表達**(local pattern token,對齊 `data-table.css` 既有 local-token 先例):
|
|
64
|
+
- CSS:`--chrome-header-avatar-size: 1.5rem`(`header-canonical.css`)— 供 SidebarHeader 收合對齊公式消費
|
|
65
|
+
- JS 端:consumer hardcode `<Avatar size={24}>` + inline comment cite token name(per Avatar API `size: number | 'fill'` 不接 CSS var)
|
|
66
|
+
|
|
67
|
+
**Consumer rule**:
|
|
68
|
+
- 在 chrome header 內放 avatar → **必用 raw `<Avatar size={24}>`**,**禁用 `<ItemAvatar>`**(會誤啟動 row anatomy lookup)
|
|
69
|
+
- SidebarHeader 收合對齊公式必消費 `var(--chrome-header-avatar-size)`,**禁** hardcode 24
|
|
70
|
+
- WorkspaceBrand demo 即此 pattern 的 reference implementation(`_demo-helpers.tsx:WorkspaceBrand`)
|
|
71
|
+
|
|
72
|
+
**Sync invariant**:`--chrome-header-avatar-size` CSS 值 + `<Avatar size={24}>` JS literal + `AVATAR_SIZE.inline.md = 24`(`item-anatomy.tsx:93`)三者必同 24px 值。改 spec canonical 時 grep `--chrome-header-avatar-size` + `chrome header avatar canonical` keyword 找全 sync 點。
|
|
73
|
+
|
|
74
|
+
### 5. Title typography
|
|
75
|
+
|
|
76
|
+
- `text-body-lg font-medium` 或 `text-h6`(細節由 family spec 自決)
|
|
77
|
+
|
|
78
|
+
### 6. Background ownership(2026-05-20 codify per user directive,SSOT)
|
|
79
|
+
|
|
80
|
+
**核心原則**:Header 自身**不該畫 bg**。bg 永遠屬於「header 所在 surface 層」的職責。違反 = 顏色疊加 / drift / 跨 consumer 視覺不一致(user 2026-05-20 抓 aside header bg-surface 疊在 aside bg-surface 上)。
|
|
81
|
+
|
|
82
|
+
**3 場景分類**:
|
|
83
|
+
|
|
84
|
+
| 場景 | Header bg | Parent surface | Rationale |
|
|
85
|
+
|---|---|---|---|
|
|
86
|
+
| **Top-level chrome header**(直接坐 canvas 上,如 PageHeader / app top bar)| **自畫 `bg-surface`**(consumer 透過 `className="bg-surface"` 注入 ChromeHeader)| Main content 是 `bg-canvas`,header 本身就是 chrome region marker 必須區分 work area | `<ChromeHeader className="bg-surface">` |
|
|
87
|
+
| **Nested chrome header**(parent 已是 chrome region:SidebarHeader / AsideHeader / FileViewer InfoPanel)| **transparent**(不傳 bg className,繼承 parent)| Sidebar / Aside 自身已 `bg-surface`,header 再畫 = 同色疊加(理論透明)但給 dark mode 半透明變體留 drift 風險 | `<ChromeHeader>`(無 bg className)|
|
|
88
|
+
| **Overlay header**(Dialog / Sheet / Popover)| **transparent** | Overlay 自身 `bg-surface-raised`,header 透明繼承 | `<SurfaceHeader>`(SurfaceHeader 內無 bg) |
|
|
89
|
+
|
|
90
|
+
**反 pattern 錨例**(2026-05-20 修正):
|
|
91
|
+
- `app-shell.tsx:268` aside built-in header `bg-surface` 疊在 aside `bg-surface`(規則 6 nested case 違反)→ 撤回
|
|
92
|
+
- `_demo-helpers.tsx:131` PageHeader 自刻 `<header>` + `bg-surface` → migrate 消費 ChromeHeader + 保 `bg-surface`(top-level 合法)
|
|
93
|
+
- `sidebar.stories.tsx:113` Sidebar IconCollapse demo 自刻 header + `bg-surface` → 後續一併 migrate(同 PageHeader 路徑)
|
|
94
|
+
|
|
95
|
+
**Element canonical**(2026-05-20):ChromeHeader 內部用 `<header>` element(非 `<div>`)。HTML5 spec 允許 sectional content 多次 `<header>`(`role="banner"` 只 page top 一個,sectional 內 default `<header>` 不自動有 banner role)。統一 element contract 消除「何時用 header / 何時用 div」consumer drift。
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## withTabs 連動(本 SSOT 最核心契約)
|
|
100
|
+
|
|
101
|
+
當 header 內含 Tabs(`<Tabs>` 子元件)時,**6 條 lockstep 規則**:
|
|
102
|
+
|
|
103
|
+
### Rule W1:Border ownership = semantic vs paint 拆分(對齊 GitHub Primer)
|
|
104
|
+
|
|
105
|
+
**規則**:**Header 是 semantic owner**(「這 surface 需要 bottom border 分區」是 header 的概念決定),**Paint owner 隨 state 變**:
|
|
106
|
+
- `withTabs={false}` → header 自畫(`border-b border-divider`)
|
|
107
|
+
- `withTabs={true}` → header **auto-suppress 自己的 paint**,**TabsList 自身的 `border-b border-border` 接管實際 paint**(視覺同一條線)
|
|
108
|
+
|
|
109
|
+
**這不是 reject user 直覺**(「border 該由 header 自己畫」)— 而是把「誰負責這條線存在」和「誰實際 paint」拆開。User 直覺 = semantic ownership 正確;DS 實作 = withTabs 時 delegate paint 給 TabsList,因為 tabs selected underline 是「從 1px gray border 長出 2px primary」設計 idiom(`tabs.spec.md:185-187`),paint owner 不在 tabs 會讓 underline 失去 base line。
|
|
110
|
+
|
|
111
|
+
**世界級對照**:
|
|
112
|
+
- GitHub Primer PageHeader verbatim:「`hasBorder` defaults true,**but border NOT rendered if Navigation child contains UnderlineNav**;UnderlineNav itself provides bottom border」(`primer.style/components/page-header/react`)
|
|
113
|
+
- 我們既有 spec:`tabs.spec.md` 「出現在 Dialog / Sidebar / 任何 header」段「Consumer 把 Tabs 放在 header 區域內、移除 header 自己的 `border-b`,讓 Tabs 接管」
|
|
114
|
+
- 我們既有禁止:`tabs.spec.md:258`「Dialog/Sidebar header 內不得同時有 header `border-b` 和 TabsList `border-b`——會出現雙線」
|
|
115
|
+
|
|
116
|
+
**Counter-pattern(reject)**:Material UI 走「container 畫 border + tabs 不畫」(`<Box sx={{ borderBottom: 1 }}><Tabs>...</Tabs></Box>`)— 本 DS reject 此方向,理由:tabs underline 需 base line(MUI 2px indicator 浮空是另一派視覺語言,跨派遷移成本高且既有 spec 已 codify Primer 派)。
|
|
117
|
+
|
|
118
|
+
### Rule W2:Tabs 水平 padding 對齊 header(by inheritance,不重複設)
|
|
119
|
+
|
|
120
|
+
**規則**:TabsList **不設自己的左右 padding**,**繼承** parent header 的 `var(--layout-space-loose)` padding。視覺結果 = tabs 第一個 trigger 從 header 內邊起 = 跟 header content row 對齊。
|
|
121
|
+
|
|
122
|
+
**實作機制(2026-05-18 v3 — `tabsSlot` prop,user verbatim「分隔線寬度應該要填滿整個 dialog」)**:
|
|
123
|
+
|
|
124
|
+
ChromeHeader / SurfaceHeader 新增 `tabsSlot?: ReactNode` prop。提供時自動 **column mode**:
|
|
125
|
+
- Row 1:children(title + close X / actions)— 跟 single-row 模式同 padding,但 border-b 撤掉
|
|
126
|
+
- Row 2:tabsSlot 包在 `<div className="[&>[role=tablist]]:w-full [&>[role=tablist]]:px-[var(--layout-space-loose)]">`(wrapper 本身**不** inset,只用 CSS arbitrary variant 給 TabsList 注入 `w-full` + 內 padding)
|
|
127
|
+
- **TabsList 全 dialog 寬**(`w-full`)→ 自身 `border-b border-border` 延展全 dialog 寬 = W1 視覺一條線
|
|
128
|
+
- **TabsList 內 padding-x = px-loose** → triggers 從 px-loose 內邊起,對齊 header content row(W2 alignment 達成)
|
|
129
|
+
- Selected trigger 2px primary 真 overlay TabsList 1px gray border(`tabs.spec.md:187-189` canonical)
|
|
130
|
+
|
|
131
|
+
**v1/v2 反 pattern**(歷史錨):
|
|
132
|
+
- v1(2026-05-18 initial):wrapper 加 `border-b border-divider` + TabsList 自身 `border-b border-border` 共存 → 雙線(色不同 + 1px box gap)
|
|
133
|
+
- v2(2026-05-18 mid):wrapper px-loose + TabsList w-full inside → border 只跨 dialog - 2×px-loose(沒到 dialog 邊)
|
|
134
|
+
- v3(本):wrapper 不 inset,TabsList 自己 padding inset triggers + 全 dialog 寬 border ✓
|
|
135
|
+
|
|
136
|
+
**W2 spec 註**:原 W2「TabsList 不設左右 padding,繼承 parent header padding」描述假設 ChromeHeader single-row 模式;**column mode 下實作機制改為 CSS arbitrary variant 注入 TabsList 內 padding**(不是 by-inheritance 從 wrapper)。Selected primary overlay full-width gray 共識(GitHub Primer UnderlineNav / Ant Design line type / Mantine default)需要 TabsList 自畫 full-width border。
|
|
137
|
+
|
|
138
|
+
**Consumer 用法**(Dialog 範例):
|
|
139
|
+
```tsx
|
|
140
|
+
<Tabs defaultValue="general"> {/* Radix Tabs root,wrap 整 DialogContent */}
|
|
141
|
+
<DialogHeader
|
|
142
|
+
tabsSlot={
|
|
143
|
+
<TabsList>
|
|
144
|
+
<TabsTrigger value="general">一般</TabsTrigger>
|
|
145
|
+
<TabsTrigger value="members">成員</TabsTrigger>
|
|
146
|
+
</TabsList>
|
|
147
|
+
}
|
|
148
|
+
>
|
|
149
|
+
<DialogTitle>專案設定</DialogTitle>
|
|
150
|
+
</DialogHeader>
|
|
151
|
+
<DialogBody> {/* DialogBody 自管 px-loose + scroll */}
|
|
152
|
+
<TabsContent value="general">一般設定內容</TabsContent>
|
|
153
|
+
<TabsContent value="members">成員管理內容</TabsContent>
|
|
154
|
+
</DialogBody>
|
|
155
|
+
</Tabs>
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**禁忌**:
|
|
159
|
+
- ❌ 把 `<Tabs>` 放在 `<DialogContent>` 外(Radix TabsList ↔ TabsContent 必同 Tabs root)
|
|
160
|
+
- ❌ 在 TabsList 上加 `className="px-*"`(會與 wrapper padding 疊加)
|
|
161
|
+
- ❌ 在 TabsContent 上加 padding(會跟 DialogBody canonical px-loose pt-tight pb-bottom 重複)
|
|
162
|
+
- ❌ Backward-compat `withTabs={true}` 無 tabsSlot 場景:header 自己抑制 border-b,但 consumer 需自管 Tabs row layout — 不推薦(除非從 layout template 來)
|
|
163
|
+
|
|
164
|
+
**Verify**:`scripts/runtime-verify-header-canonical-tabs.mjs` playwright probe W1/W2/W4 pixel-quantified(title vs first tab left Δ ≤ 1px / flush gap ≤ 1px / row 2 border = 1px)。
|
|
165
|
+
|
|
166
|
+
**Rationale**:對齊 Carbon verbatim「first label should always align to other content in the space」(`carbondesignsystem.com/components/tabs/usage`)。「by inheritance 不重複設」是 GitHub Primer + Material UI 隱含 idiom — tabs container 不主動畫 padding,讓外層 container 統一管。
|
|
167
|
+
|
|
168
|
+
### Rule W3:Tabs size 與 header family 對應
|
|
169
|
+
|
|
170
|
+
| Header family | Tabs size | Rationale |
|
|
171
|
+
|---|---|---|
|
|
172
|
+
| **A. Overlay header(Padding-based)** | `sm`(32/40)| 對齊 Ant verbatim「small size could be used in Modal」;overlay 內 close X 也是 sm 統一 |
|
|
173
|
+
| **B. Chrome header(Fixed-h)** | `sm`(32/40)| chrome 內 buttons 全部 sm(real grep:`Sidebar / FileViewer toolbar / InfoPanel` 所有 buttons size=sm),tabs 跟齊 |
|
|
174
|
+
| **C. Standalone(tabs 取代 chrome header)** | `lg`(48/56)| Tabs 直接取代 header 用,**tab 高 = chrome-header-height 像素相等**(48/56)— Ant verbatim「Large size tabs are usually used in page header」 |
|
|
175
|
+
|
|
176
|
+
**Token alignment 強連動**:`--tab-height-lg` md=48 / lg=56 與 `--chrome-header-height` md=48 / lg=56 **像素完全相等**(`uiSize.css:36-79` + `globals.css:37-45`)— 這是「lg tabs 取代 chrome header」的 SSOT linkage 鐵證。**禁止任何一方獨自調整**(動 chrome-header-height 必同步 tab-height-lg,反之同)。
|
|
177
|
+
|
|
178
|
+
**Enforcement = assert,不 CSS alias**(per M31 codex 比稿 Step 5):`uiSize.css` 的 `--tab-height-*` 有 `data-ui-size="md"` reset selector(component-size scope),`--chrome-header-height` 是 layout/density token(app-chrome scope)— alias 會把兩個不同 scope 綁死。**機制**:保留 duplicated literals + Phase 3 hook `check_tab_lg_chrome_header_equal.sh` parse 兩 CSS file 提取 md/lg 值 assert equality → 任一方獨自改 = BLOCKER。
|
|
179
|
+
|
|
180
|
+
### Rule W4:Tabs row 與 header content row 為 flush stack(無 negative margin)
|
|
181
|
+
|
|
182
|
+
**規則**:有 tabs 的 header,header content row(title + actions)在上 / tabs row 在下,中間 **gap = 0**,直接疊。
|
|
183
|
+
|
|
184
|
+
**Rationale**:negative margin chain 會破 M25「Layered chain invariant 必整鏈 forward」— 任何 ancestor 改動 padding/height 都會讓 negative margin 視覺漂移。改用 flush stack(`<div className="flex flex-col">`)— 兩 row 各自管自己 vertical rhythm,zero coupling。
|
|
185
|
+
|
|
186
|
+
**世界級對照**:GitHub Primer PageHeader + UnderlineNav 採 flush stack(無 negative margin)— UnderlineNav 直接 append PageHeader 下方,border 由 UnderlineNav 接管。
|
|
187
|
+
|
|
188
|
+
### Rule W5:md tabs size 為 future tier,目前無 use case
|
|
189
|
+
|
|
190
|
+
**規則**:`--tab-height-md`(40/48)token **保留**,但**目前無 recommended use case**;新 consumer 想用 md 必先諮詢 DS owner 取得 use case rationale。
|
|
191
|
+
|
|
192
|
+
**Rationale**:
|
|
193
|
+
- Header 內 tabs 對應 button-sm → tabs sm(W3 已 cover)
|
|
194
|
+
- 獨立 chrome-replacement tabs → tabs lg(W3 已 cover)
|
|
195
|
+
- 中間階梯(md=40)無清楚場景 — 多家世界級 DS(Material / MUI / Primer / Polaris)只有 1 個 default size(無 sm/md/lg 階梯)= sweet spot 只需 2 tier
|
|
196
|
+
|
|
197
|
+
**Conservative path**:token 不刪(避免 breaking change 與 stories 維護成本);spec 明寫 future tier。
|
|
198
|
+
|
|
199
|
+
### Rule W6:Default tabs size 為 sm(Phase 2 visual audit gate,不 P0)
|
|
200
|
+
|
|
201
|
+
**規則**:Tabs `cva defaultVariants.size = 'sm'`(本 SSOT 提議目標 default;**Phase 2 production code 改動,經 visual audit 過 baseline diff 才 land**)。
|
|
202
|
+
|
|
203
|
+
**Rationale**:既有 default = `md`(`tabs.tsx:127` TabsList + `:377` trigger cva,grep production consumer = 0 個 `<Tabs>` 在非 stories code)。Production blast radius 低 — 但 stories / anatomy demo 大量縮高 → 必過 visual baseline diff 再 land。**禁直接 P0 改 production**;Phase 2 排序:(1) 改 cva default → (2) 跑 `npm run build-storybook` → (3) Playwright baseline diff → (4) update stories labels → (5) merge。
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## 連動機制(防漂移 SSOT)
|
|
208
|
+
|
|
209
|
+
### Layer 1 — Token shared
|
|
210
|
+
|
|
211
|
+
兩家族 consume 同組 CSS variable(機械對齊):
|
|
212
|
+
- `--chrome-header-height`(globals.css:37,45)
|
|
213
|
+
- `--layout-space-loose` / `--layout-space-tight`
|
|
214
|
+
- `--divider-color`
|
|
215
|
+
- `--tab-height-sm` / `--tab-height-lg`(uiSize.css:36-38,58-60)
|
|
216
|
+
- `--field-height-sm`(button sm 透過 button family 連動)
|
|
217
|
+
|
|
218
|
+
### Layer 2 — Spec pointer
|
|
219
|
+
|
|
220
|
+
6 consumer spec.md 在 Header 段加 pointer:
|
|
221
|
+
- `components/Sidebar/sidebar.spec.md`
|
|
222
|
+
- `components/FileViewer/file-viewer.spec.md`(Toolbar + InfoPanel 兩處 header)
|
|
223
|
+
- `components/Dialog/dialog.spec.md`
|
|
224
|
+
- `components/Sheet/sheet.spec.md`
|
|
225
|
+
- `components/Popover/popover.spec.md`
|
|
226
|
+
- `tokens/uiSize/uiSize.spec.md`「消費 --chrome-header-height」段
|
|
227
|
+
|
|
228
|
+
### Layer 3 — Primitive consumption(Phase 2,等 user 拍板)
|
|
229
|
+
|
|
230
|
+
- A 家族:`SurfaceHeader` 已是 primitive(`overlay-surface.tsx:62-118`)— add `withTabs?: boolean` prop auto-suppress border
|
|
231
|
+
- B 家族:**新建 `ChromeHeader` primitive**(`patterns/header-canonical/chrome-header.tsx`)— Sidebar / FileViewer / InfoPanel 全 migrate
|
|
232
|
+
|
|
233
|
+
**ChromeHeader API(per M31 codex 比稿 Step 5 — 比 v1 更窄,避免 M21 prop variant 風險)**:
|
|
234
|
+
```tsx
|
|
235
|
+
<ChromeHeader
|
|
236
|
+
withTabs?: boolean // false (default) | true → auto-suppress border + delegate to TabsList
|
|
237
|
+
tabsSlot?: ReactNode // Tabs row(column mode + W1-W6 lockstep,2026-05-18 ship)
|
|
238
|
+
lockDensity?: 'inherit' | 'lg' // 'inherit' (default) | 'lg' → 強制 chrome-header-height lg=56(viewer 等 fullscreen chrome)
|
|
239
|
+
leadingRail?: ReactNode // 2026-05-21 ship per AppShell primary-header globalHeader:固定 width=sidebar-width-icon 的左 rail,內 justify-center 排列(toggle/logo 跟 sidebar 收合 icon center x 對齊)。對齊 GitHub global nav 左 logo 區 / Slack thin workspace rail 慣例。
|
|
240
|
+
>
|
|
241
|
+
{children}
|
|
242
|
+
</ChromeHeader>
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
**不開**:`density?: 'md'|'lg'` 自由 prop(M21 違反 — 任意 density 等於 cva-on-pattern,該由 density context 決定)。`lockDensity="lg"` 是 escape hatch 給 fullscreen viewer chrome(FileViewer 永遠 lg-equivalent design intent),其餘 consumer 走 `inherit`(跟 page density)。**Grep evidence**(production 重複 contract,非 premature abstraction):`sidebar.tsx:433` + `file-viewer.tsx:333,459` 全是 `flex items-center gap-2 shrink-0 h-[var(--chrome-header-height)] border-b border-divider px-[var(--layout-space-loose)]` 一模一樣手刻 → 過 M21 prop variant test 3 條(`<Sidebar>` 等近親 grep / 3 家世界級 page-header primitive / value 結構不同)。
|
|
246
|
+
|
|
247
|
+
### Layer 4 — Hook mechanical
|
|
248
|
+
|
|
249
|
+
- `check_header_with_tabs_border.sh` — grep tsx 若 header 元件含 `<Tabs>` child 但未走 `withTabs` API → BLOCKER
|
|
250
|
+
- `check_chrome_header_handcraft.sh` — grep 是否仍有 hardcoded `h-[var(--chrome-header-height)] border-b` 自刻 chrome header(該消費 ChromeHeader primitive)
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## 禁止事項
|
|
255
|
+
|
|
256
|
+
- ❌ Header 內同時有 header `border-b` 和 TabsList `border-b`(雙線)— per W1
|
|
257
|
+
- ❌ Tabs underline 用 `border-b-2` 在 trigger 上實作(雙線)— per `tabs.spec.md:246`
|
|
258
|
+
- ❌ Negative margin 接 header 與 tabs row(脆弱 chain)— per W4
|
|
259
|
+
- ❌ Hardcode `h-16` / `h-14` / `h-12` 自寫 chrome header 高度 — 必 `--chrome-header-height`
|
|
260
|
+
- ❌ Header 內 close X 用 `size="md"` 或 `size="lg"`(real grep 100% sm)
|
|
261
|
+
- ❌ Tabs default 寫 `size="md"`(W5 + W6)— 等 Phase 2 改 cva default = sm
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## A11y 預設
|
|
266
|
+
|
|
267
|
+
- Header 用 `<header>` semantic element(SR landmark)
|
|
268
|
+
- Tabs 走 Radix Tabs(原生 `role="tablist" / role="tab" / aria-selected`)
|
|
269
|
+
- Dismiss button `aria-label="關閉"`(close X)
|
|
270
|
+
- 鍵盤:Tabs 左右方向鍵切換(Radix);Esc 觸發 dismiss(overlay 家族)
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## 跨元件參考
|
|
275
|
+
|
|
276
|
+
- `patterns/overlay-surface/overlay-surface.spec.md` — A 家族 SurfaceHeader / SurfaceBody / SurfaceFooter SSOT
|
|
277
|
+
- `tokens/uiSize/uiSize.spec.md` L160-289 — `--chrome-header-height` token canonical + Padding-based vs Fixed-h decision tree
|
|
278
|
+
- `components/Tabs/tabs.spec.md` L122-138(size 階梯)+ L185-247(border 視覺契約)
|
|
279
|
+
- `components/Button/button.spec.md` — size="sm" close X / inline action canonical
|
|
280
|
+
|
|
281
|
+
## 被引用(auto-maintained,Dim 3 reciprocal audit)
|
|
282
|
+
|
|
283
|
+
> 本節由 `scripts/add-reciprocal-pointers.mjs` 自動維護,列出在 SSOT 語境下指向本 spec 的其他 spec。若要手動補充,寫在本節之前。
|
|
284
|
+
|
|
285
|
+
- `app-shell.spec.md`
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
---
|
|
2
|
+
pattern: horizontal-overflow
|
|
3
|
+
internal: true
|
|
4
|
+
scope: utility primitives module (useOverflowItems hook + fade-mask + scroll-arrow helper components) — DS-internal consumer only(Tabs / ChipGroup wrap)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<!-- @benchmark-cited: D5 retrofit 2026-05-18 — body claims marked per-claim @benchmark-unverified inline; canonical source URLs in frontmatter benchmark list. -->
|
|
8
|
+
|
|
9
|
+
# Horizontal Overflow 設計原則
|
|
10
|
+
|
|
11
|
+
**Layout Family**:non-family(utility primitives module,本身不渲染 layout — 由消費端 family 1/3 提供 outer geometry)。
|
|
12
|
+
|
|
13
|
+
**水平 overflow 的 canonical primitives + helper**——給任何「一排水平 items 可能塞不下容器」的元件(Tabs、ChipGroup、未來的 Steps horizontal、SegmentedControl overflow 等)共用。
|
|
14
|
+
|
|
15
|
+
這是底層工具 module,**不是 UI 元件**。消費者組裝自己的 outer wrapper(border、gap、背景等元件特有樣式),但 overflow affordance(scroll arrows、menu trigger、fade mask)全部從本 module 取用。
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 為什麼存在
|
|
20
|
+
|
|
21
|
+
### 歷史教訓
|
|
22
|
+
|
|
23
|
+
`ScrollArrow` 元件在 `Tabs/tabs.tsx` 和 `Chip/chip.tsx` **逐行 copy-paste**,`buildFadeMask` / `ARROW_BUTTON_WIDTH` / `SCROLL_PAGE_RATIO` 常數也是複製的。任何修改都要手動改兩處,是漂移溫床——實際上已經造成了一個 bug:
|
|
24
|
+
|
|
25
|
+
**Chip 的 menu trigger 曾用 chip variant 的視覺語言(與可選 chip 同形狀),讓 menu trigger 與可選 chip 在 mental model 上無法區分——使用者預期點擊是選中,實際是打開選單。同期 Tabs 的 menu trigger 用 text button 正確表達「這是 overflow 工具」,兩個元件 overflow affordance 自相矛盾。**
|
|
26
|
+
|
|
27
|
+
### Canonical 規則
|
|
28
|
+
|
|
29
|
+
**所有 horizontal overflow 的 affordance 一律是 `<Button variant="text" size="sm" iconOnly>`。**
|
|
30
|
+
|
|
31
|
+
- 左右 scroll arrow → text button,ChevronLeft / ChevronRight
|
|
32
|
+
- Menu trigger → text button,ChevronDown
|
|
33
|
+
- **禁止**用 item 自身的視覺語言(chip 形狀、tab 底線等)來渲染 overflow trigger——overflow affordance 是「工具層」,不是「業務層」,不該跟內容爭視覺重量(對齊 `CLAUDE.md` 的「工具層必須是視覺重量最低的一層」原則)。
|
|
34
|
+
|
|
35
|
+
這條規則讓使用者看到向下 chevron 或向右 arrow 時,心智是一致的:「這是 overflow 的工具,不是可選內容」,不論這排 items 是什麼類型。
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 提供的 primitive
|
|
40
|
+
|
|
41
|
+
### 常數
|
|
42
|
+
|
|
43
|
+
| 常數 | 值 | 用途 |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| `FADE_WIDTH` | 16px | Fade mask 的漸變寬度 |
|
|
46
|
+
| `ARROW_BUTTON_WIDTH` | 32px | Scroll arrow 預留的按鈕區寬度(對齊 `field-height-sm` lg density) |
|
|
47
|
+
| `SCROLL_PAGE_RATIO` | 0.8 | 點 scroll arrow 一次滑動 80% 容器寬度 |
|
|
48
|
+
|
|
49
|
+
### Helpers
|
|
50
|
+
|
|
51
|
+
- `buildFadeMask({ canScroll, atStart, atEnd, reserveArrowWidth })`
|
|
52
|
+
回傳 `linear-gradient` 字串。`reserveArrowWidth` > 0 時 fade 會延伸到 arrow button 底下(Material 3 scrim 原理);= 0 時直接從邊緣 fade。
|
|
53
|
+
- `useScrollByPage(scrollRef)` → `(direction: 'left' | 'right') => void`
|
|
54
|
+
點一次滑動 `clientWidth × SCROLL_PAGE_RATIO`,含 `behavior: 'smooth'`。
|
|
55
|
+
|
|
56
|
+
### 元件
|
|
57
|
+
|
|
58
|
+
- `<OverflowScrollArrow direction onClick>`
|
|
59
|
+
- Root: `<Button variant="text" size="sm" iconOnly>`
|
|
60
|
+
- Icon: `ChevronLeft` / `ChevronRight`
|
|
61
|
+
- aria-label 內建繁中(「向左捲動」/「向右捲動」)
|
|
62
|
+
- **絕對定位**在容器 `left-0` / `right-0`,`pointer-events-none` 外層 + `pointer-events-auto` 內層(讓 mask 下方仍可滑動)
|
|
63
|
+
- `<OverflowMenuTriggerButton label>` (forwardRef)
|
|
64
|
+
- Root: `<Button variant="text" size="sm" iconOnly>`
|
|
65
|
+
- Icon: `ChevronDown`
|
|
66
|
+
- 接收 `label` 當 `aria-label`(由消費者決定,例如「頁籤選單(共 5 個)」)
|
|
67
|
+
- `forwardRef` + `...props` spread 讓 Radix `DropdownMenuTrigger asChild` 可以接管
|
|
68
|
+
|
|
69
|
+
### Hook re-export
|
|
70
|
+
|
|
71
|
+
為了消費者只 import 一處,本 module re-export `useScrollEdges` / `useOverflowIndices` from `hooks/use-overflow-items.ts`。
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 消費者架構
|
|
76
|
+
|
|
77
|
+
本 module 提供的是**樂高**,不是**組裝好的玩具**。消費者需要自己把外層容器、Radix primitive(TabsPrimitive / ToggleGroupPrimitive)、fade mask、arrows、menu trigger、DropdownMenu 拼起來。
|
|
78
|
+
|
|
79
|
+
### 為什麼不做成封閉的 `<HorizontalOverflowContainer>`?
|
|
80
|
+
|
|
81
|
+
- Tabs 的 underline border owner 在 **list 內部**(`TABS_LIST_BASE` 套 `border-b border-divider`),outer 不畫;Chip outer 是 pill border `rounded-full border border-border`,亦不需 outer `border-b`。Owner 升 list 是 2026-05-19 fix:outer `border-b` + inner `overflow-x-auto` → browser y auto-promote(CSS overflow-3 spec:一軸 auto 時另軸 visible compute auto)→ active underline `after:bottom:-1px` 1px clip + 1px 垂直可捲 bug
|
|
82
|
+
- Tabs 用 `TabsPrimitive.List` 作為 inner list;Chip 用 `ToggleGroupPrimitive.Root`
|
|
83
|
+
- Tabs 用 `DropdownMenuItem + selected`(單選);Chip 用 `DropdownMenuCheckboxItem`(多選)
|
|
84
|
+
- Tabs 有 `gap-[var(--layout-space-loose)]`;Chip 有 `gap-2`
|
|
85
|
+
|
|
86
|
+
把這些差異塞進一個 container prop 只會讓 API 變成 bag of flags。**讓 consumer 組裝,但強制消費 canonical primitive**,是更乾淨的做法——跟 `item-layout` module 提供 `ItemPrefix` / `ItemLabel` / `ItemIcon` 但不做「ItemRow 容器」是同一個設計哲學。
|
|
87
|
+
|
|
88
|
+
### 典型 scroll 模式組裝
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
import {
|
|
92
|
+
useScrollEdges,
|
|
93
|
+
useScrollByPage,
|
|
94
|
+
buildFadeMask,
|
|
95
|
+
ARROW_BUTTON_WIDTH,
|
|
96
|
+
OverflowScrollArrow,
|
|
97
|
+
} from '@/design-system/patterns/horizontal-overflow/horizontal-overflow'
|
|
98
|
+
|
|
99
|
+
const { scrollRef, atStart, atEnd, canScroll } = useScrollEdges<HTMLDivElement>()
|
|
100
|
+
const scrollByPage = useScrollByPage(scrollRef)
|
|
101
|
+
const maskImage = buildFadeMask({
|
|
102
|
+
canScroll, atStart, atEnd,
|
|
103
|
+
reserveArrowWidth: ARROW_BUTTON_WIDTH,
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<div className="relative"> {/* outer 不畫 border;Tabs underline owner 在 list 內 */}
|
|
108
|
+
<div
|
|
109
|
+
ref={scrollRef}
|
|
110
|
+
className="overflow-x-auto overflow-y-hidden [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
|
111
|
+
style={{ maskImage, WebkitMaskImage: maskImage }}
|
|
112
|
+
>
|
|
113
|
+
<TabsPrimitive.List className={cn(TABS_LIST_BASE, 'w-fit')}>...</TabsPrimitive.List>
|
|
114
|
+
</div>
|
|
115
|
+
{!atStart && canScroll && <OverflowScrollArrow direction="left" onClick={() => scrollByPage('left')} />}
|
|
116
|
+
{!atEnd && canScroll && <OverflowScrollArrow direction="right" onClick={() => scrollByPage('right')} />}
|
|
117
|
+
</div>
|
|
118
|
+
)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
> **Why `overflow-y-hidden`(2026-05-19 codify)**:CSS overflow-3 spec 強制「一軸 auto/scroll/hidden 時另軸 visible compute auto」。`overflow-x-auto` 單獨設 + active underline `after:bottom:-1px` 落在 box 外 → y auto-promote → 1px 垂直可捲 + underline 初始 1px clip bug。明示 `overflow-y-hidden` 阻 promote。對齊 Primer UnderlineNav `overflow-x:auto; overflow-y:hidden` 公開實作。
|
|
122
|
+
|
|
123
|
+
### 典型 menu 模式組裝
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
import {
|
|
127
|
+
useScrollEdges,
|
|
128
|
+
buildFadeMask,
|
|
129
|
+
OverflowMenuTriggerButton,
|
|
130
|
+
} from '@/design-system/patterns/horizontal-overflow/horizontal-overflow'
|
|
131
|
+
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent } from '@/design-system/components/DropdownMenu/dropdown-menu'
|
|
132
|
+
|
|
133
|
+
const { scrollRef, atStart, atEnd, canScroll } = useScrollEdges<HTMLDivElement>()
|
|
134
|
+
const maskImage = buildFadeMask({ canScroll, atStart, atEnd, reserveArrowWidth: 0 })
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<div className="flex items-stretch"> {/* items-stretch 讓 menu button 容器跟 list 共底線 */}
|
|
138
|
+
<div ref={scrollRef} className="flex-1 min-w-0 overflow-x-auto overflow-y-hidden ..." style={{ maskImage, WebkitMaskImage: maskImage }}>
|
|
139
|
+
<TabsPrimitive.List className={cn(TABS_LIST_BASE, 'w-fit')}>...</TabsPrimitive.List>
|
|
140
|
+
</div>
|
|
141
|
+
{canScroll && (
|
|
142
|
+
<DropdownMenu>
|
|
143
|
+
<DropdownMenuTrigger asChild>
|
|
144
|
+
<OverflowMenuTriggerButton label={`頁籤選單(共 ${items.length} 個)`} />
|
|
145
|
+
</DropdownMenuTrigger>
|
|
146
|
+
<DropdownMenuContent align="end">
|
|
147
|
+
{/* menu button container 自帶 border-b border-divider 對齊 list border */}
|
|
148
|
+
</DropdownMenuContent>
|
|
149
|
+
</DropdownMenu>
|
|
150
|
+
)}
|
|
151
|
+
</div>
|
|
152
|
+
)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## 禁止事項
|
|
158
|
+
|
|
159
|
+
❌ **禁止** copy `ScrollArrow` 到其他元件。要在新元件加 overflow → import 本 module 的 `<OverflowScrollArrow>`。
|
|
160
|
+
❌ **禁止** 用 item 自身的視覺語言(chip shape、tab underline、segmented border 等)渲染 overflow trigger。trigger 永遠是 text button。
|
|
161
|
+
❌ **禁止** 重新定義 `FADE_WIDTH` / `ARROW_BUTTON_WIDTH` / `SCROLL_PAGE_RATIO`。有調整需求來改這裡一處,全系統同步。
|
|
162
|
+
❌ **禁止** 在元件內部另寫 `buildFadeMask`——用本 module 的版本。
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## 消費者清單(跟 item-layout 同步維護)
|
|
167
|
+
|
|
168
|
+
- `components/Tabs/tabs.tsx` — `overflow="scroll" | "menu"`
|
|
169
|
+
- `components/Chip/chip.tsx` — `layout="scroll" | "menu"`
|
|
170
|
+
|
|
171
|
+
**Non-consumers**(2026-05-10 retire from「未來」清單 — prior prediction stale,spec 已明文不消費):
|
|
172
|
+
- ~~`components/Steps/steps.tsx`~~ — `steps.spec.md:326`「水平空間不夠塞 content 區,強塞會破壞 stepper 的掃視節奏」+ L337「步驟 ≤ 5、水平空間充足」→ 設計上不 overflow
|
|
173
|
+
- ~~`components/SegmentedControl/*`~~ — `segmented-control.spec.md:223`「**不支援 overflow / scroll**——若選項可能超出容器寬度,代表選錯元件了」+ L162「最多 5 個 item」→ 設計上拒絕 overflow
|
|
174
|
+
|
|
175
|
+
新消費者必須加到這個清單,並且**只從本 module import** overflow 相關的 primitive,不允許自己複製。
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## 反向引用
|
|
180
|
+
|
|
181
|
+
- `hooks/use-overflow-items.ts` — 底層 scroll / overflow 計算 hooks(本 module re-export)
|
|
182
|
+
- `CLAUDE.md`「選擇 / 狀態視覺」規則 — 為什麼 overflow trigger 不該用 selection-like 視覺
|
|
183
|
+
- `components/Button/button.tsx` — text variant 的 canonical 樣式
|
|
184
|
+
- `components/DropdownMenu/dropdown-menu.spec.md` — menu item 的 selection 指示器規則
|
|
185
|
+
|
|
186
|
+
## 被引用(auto-maintained,Dim 3 reciprocal audit)
|
|
187
|
+
|
|
188
|
+
> 本節由 `scripts/add-reciprocal-pointers.mjs` 自動維護,列出在 SSOT 語境下指向本 spec 的其他 spec。若要手動補充,寫在本節之前。
|
|
189
|
+
|
|
190
|
+
- `file-viewer.spec.md`
|
|
191
|
+
- `overflow-indicator.spec.md`
|