feat: enhance typography and accessibility in design system
- Added new UI heading utility with specific styles in typography.css. - Updated typography tests to include new UI heading role and ensure proper size and styling. - Documented design system audit findings, focusing on accessibility improvements and component consistency. - Introduced CalendarExamples component to demonstrate calendar functionality with habit tracking. - Created a shared Field component for consistent labeling and hinting of form controls. - Implemented comprehensive accessibility tests for calendar and editor components, ensuring proper focus management and keyboard navigation. - Added structure tests for Field and Card components to verify correct ID generation and heading levels.
This commit is contained in:
@@ -17,18 +17,19 @@
|
||||
--ds-ink: #111;
|
||||
--ds-secondary: #666;
|
||||
--ds-rule: #dedede;
|
||||
--ds-control-border: #767676;
|
||||
--ds-paper: #fff;
|
||||
--ds-surface: #f5f5f5;
|
||||
--ds-empty: #eee;
|
||||
--ds-serif: "Instrument Serif", Georgia, serif;
|
||||
--ds-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
position: relative;
|
||||
margin: -8px;
|
||||
min-height: 100vh;
|
||||
color: var(--ds-ink);
|
||||
background: var(--ds-paper);
|
||||
@apply type-body;
|
||||
font-family: -apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
sans-serif;
|
||||
font-family: var(--ds-sans);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
.ds-root *,
|
||||
@@ -83,7 +84,7 @@
|
||||
}
|
||||
.ds-eyebrow {
|
||||
@apply type-eyebrow;
|
||||
color: #737373;
|
||||
color: var(--ds-secondary);
|
||||
}
|
||||
.ds-header {
|
||||
max-width: 1280px;
|
||||
@@ -137,10 +138,10 @@
|
||||
.ds-library-note { max-width: 180px; text-align: right; }
|
||||
.ds-tab-bar {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 4px;
|
||||
padding: 6px;
|
||||
background: #f5f5f5;
|
||||
background: var(--ds-surface);
|
||||
}
|
||||
.ds-tab-bar [role="tab"] {
|
||||
@apply type-body;
|
||||
@@ -149,7 +150,7 @@
|
||||
border: 1px solid transparent;
|
||||
padding: 10px 8px;
|
||||
}
|
||||
.ds-tab-bar [role="tab"]:hover { background: #ebebeb; color: var(--ds-ink); }
|
||||
.ds-tab-bar [role="tab"]:hover { background: var(--ds-surface); color: var(--ds-ink); }
|
||||
.ds-tab-bar [aria-selected="true"] {
|
||||
background: var(--ds-paper);
|
||||
color: var(--ds-ink);
|
||||
@@ -326,7 +327,7 @@
|
||||
color: var(--ds-paper);
|
||||
}
|
||||
.ds-button:hover:not(:disabled) {
|
||||
background: #333;
|
||||
background: var(--ds-secondary);
|
||||
}
|
||||
.ds-button-icon { width: 1.25rem; height: 1.25rem; flex-shrink: 0; }
|
||||
.ds-button--secondary {
|
||||
@@ -337,21 +338,21 @@
|
||||
background: transparent;
|
||||
color: var(--ds-ink);
|
||||
border-color: transparent;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
.ds-button--secondary:hover:not(:disabled),
|
||||
.ds-button--text:hover:not(:disabled) {
|
||||
background: #f3f3f3;
|
||||
background: var(--ds-surface);
|
||||
}
|
||||
.ds-counter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ds-counter .ds-button {
|
||||
width: 36px;
|
||||
min-height: 40px;
|
||||
width: 44px;
|
||||
min-width: 44px;
|
||||
min-height: 44px;
|
||||
@apply type-control;
|
||||
font-weight: 300;
|
||||
padding: 0;
|
||||
@@ -365,7 +366,7 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 11px;
|
||||
min-height: 36px;
|
||||
min-height: 44px;
|
||||
cursor: pointer;
|
||||
@apply type-small;
|
||||
}
|
||||
@@ -375,8 +376,8 @@
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
flex: 0 0 17px;
|
||||
border: 1px solid #777;
|
||||
background: #fff;
|
||||
border: 1px solid var(--ds-control-border);
|
||||
background: var(--ds-paper);
|
||||
display: grid;
|
||||
place-content: center;
|
||||
cursor: pointer;
|
||||
@@ -389,8 +390,8 @@
|
||||
content: "";
|
||||
width: 8px;
|
||||
height: 4px;
|
||||
border-left: 1px solid #fff;
|
||||
border-bottom: 1px solid #fff;
|
||||
border-left: 1px solid var(--ds-paper);
|
||||
border-bottom: 1px solid var(--ds-paper);
|
||||
transform: rotate(-45deg) translateY(-1px);
|
||||
}
|
||||
.ds-checkbox input:disabled {
|
||||
@@ -399,6 +400,8 @@
|
||||
}
|
||||
.ds-checkbox--icon {
|
||||
padding: 10px;
|
||||
min-width: 44px;
|
||||
justify-content: center;
|
||||
}
|
||||
.ds-checkbox--icon span {
|
||||
position: absolute;
|
||||
@@ -449,12 +452,12 @@
|
||||
box-shadow: none;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.ds-card--soft { background: #f5f5f5; }
|
||||
.ds-card--soft { background: var(--ds-surface); }
|
||||
.ds-card--outlined {
|
||||
background: var(--ds-paper);
|
||||
border-color: var(--ds-rule);
|
||||
}
|
||||
.ds-card-header h3 { @apply type-title; }
|
||||
.ds-card-title { @apply type-title; }
|
||||
.ds-card-header .ds-eyebrow { margin-bottom: 12px; }
|
||||
.ds-card-body { display: grid; gap: 20px; }
|
||||
.ds-card-body > p { color: var(--ds-secondary); }
|
||||
@@ -505,7 +508,7 @@
|
||||
}
|
||||
.ds-spec-heading h3,
|
||||
.ds-spec-heading label {
|
||||
@apply type-small;
|
||||
@apply type-ui-heading;
|
||||
}
|
||||
.ds-spec-heading--spaced {
|
||||
margin-top: 36px;
|
||||
@@ -533,7 +536,7 @@
|
||||
}
|
||||
.ds-palette {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 18px;
|
||||
}
|
||||
.ds-swatch {
|
||||
@@ -550,9 +553,11 @@
|
||||
display: block;
|
||||
@apply type-small;
|
||||
margin-top: 3px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.ds-spacing {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
gap: 30px;
|
||||
}
|
||||
@@ -579,13 +584,14 @@
|
||||
border-top: 1px solid var(--ds-rule);
|
||||
padding: 24px 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
}
|
||||
.ds-component-row h3 {
|
||||
@apply type-small;
|
||||
margin-bottom: 3px;
|
||||
@apply type-ui-heading;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.ds-example-form {
|
||||
border-top: 1px solid var(--ds-rule);
|
||||
@@ -593,24 +599,19 @@
|
||||
}
|
||||
.ds-input-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 14px;
|
||||
}
|
||||
.ds-input-row input {
|
||||
flex: 1;
|
||||
flex: 1 1 12rem;
|
||||
min-width: 0;
|
||||
border: 0;
|
||||
border-bottom: 1px solid #888;
|
||||
padding: 10px 0;
|
||||
background: var(--ds-paper);
|
||||
color: var(--ds-ink);
|
||||
outline-offset: 4px;
|
||||
}
|
||||
.ds-input-row input::placeholder {
|
||||
color: var(--ds-secondary);
|
||||
}
|
||||
.ds-shade-scale {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(9, 1fr);
|
||||
grid-template-columns: repeat(9, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
.ds-shade-scale > div {
|
||||
@@ -627,6 +628,8 @@
|
||||
}
|
||||
.ds-scale-caption {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 16px;
|
||||
justify-content: space-between;
|
||||
@apply type-small;
|
||||
color: var(--ds-secondary);
|
||||
@@ -645,12 +648,12 @@
|
||||
gap: 13px;
|
||||
}
|
||||
.ds-neutral-states h4 {
|
||||
@apply type-small;
|
||||
@apply type-ui-heading;
|
||||
}
|
||||
.ds-neutral-states p {
|
||||
@apply type-small;
|
||||
color: var(--ds-secondary);
|
||||
margin-top: 3px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.ds-state-square {
|
||||
flex: 0 0 22px;
|
||||
@@ -658,7 +661,7 @@
|
||||
height: 22px;
|
||||
}
|
||||
.ds-state-square--future {
|
||||
background: #eeeeee;
|
||||
background: var(--ds-empty);
|
||||
}
|
||||
.ds-reference-grid {
|
||||
display: grid;
|
||||
@@ -704,6 +707,8 @@
|
||||
}
|
||||
.ds-detail-count {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px 24px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-top: 1px solid var(--ds-rule);
|
||||
@@ -723,10 +728,28 @@
|
||||
gap: 26px;
|
||||
padding: 4px 0 22px;
|
||||
}
|
||||
.ds-calendar-scroll {
|
||||
overflow-x: auto;
|
||||
padding: 5px;
|
||||
margin: -5px;
|
||||
.ds-calendar {
|
||||
min-width: 0;
|
||||
container-type: inline-size;
|
||||
--calendar-gap: 4px;
|
||||
}
|
||||
/* Shared axes and consecutive weeks; landscape fits without a scroll container. */
|
||||
.ds-calendar-viewport {
|
||||
padding: 4px;
|
||||
}
|
||||
.ds-calendar-timeline {
|
||||
display: grid;
|
||||
grid-template-columns: 36px minmax(0, 1fr);
|
||||
gap: 12px 10px;
|
||||
width: 100%;
|
||||
max-width: calc(46px + var(--calendar-weeks) * 24px + (var(--calendar-weeks) - 1) * var(--calendar-gap));
|
||||
}
|
||||
.ds-calendar-months {
|
||||
display: grid;
|
||||
grid-column: 2;
|
||||
grid-template-columns: repeat(var(--calendar-weeks), minmax(0, 1fr));
|
||||
gap: var(--calendar-gap);
|
||||
min-width: 0;
|
||||
}
|
||||
.ds-calendar-range-toolbar {
|
||||
display: flex;
|
||||
@@ -742,11 +765,29 @@
|
||||
}
|
||||
.ds-month-views {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
.ds-calendar-custom-range {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: end;
|
||||
gap: 12px;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
.ds-calendar-custom-range .ds-field {
|
||||
flex: 1 1 160px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.ds-calendar-custom-range input { width: 100%; }
|
||||
.ds-calendar-range-error {
|
||||
flex-basis: 100%;
|
||||
@apply type-small;
|
||||
color: var(--ds-ink);
|
||||
}
|
||||
.ds-month-views button {
|
||||
min-height: 32px;
|
||||
min-width: 32px;
|
||||
min-height: 44px;
|
||||
min-width: 44px;
|
||||
padding: 5px 7px;
|
||||
color: var(--ds-secondary);
|
||||
background: transparent;
|
||||
@@ -756,32 +797,25 @@
|
||||
}
|
||||
.ds-month-views button:hover {
|
||||
color: var(--ds-ink);
|
||||
background: #f3f3f3;
|
||||
background: var(--ds-surface);
|
||||
}
|
||||
.ds-month-views button[aria-pressed="true"] {
|
||||
color: var(--ds-ink);
|
||||
border-bottom-color: var(--ds-ink);
|
||||
}
|
||||
.ds-calendar-inner {
|
||||
min-width: 600px;
|
||||
}
|
||||
.ds-calendar-months {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(26, 1fr);
|
||||
margin-left: 38px;
|
||||
.ds-calendar-month-label {
|
||||
min-width: 0;
|
||||
@apply type-small;
|
||||
color: var(--ds-secondary);
|
||||
height: 28px;
|
||||
}
|
||||
.ds-calendar-body {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
.ds-calendar-weekdays {
|
||||
display: grid;
|
||||
grid-template-rows: repeat(7, 1fr);
|
||||
width: 36px;
|
||||
flex-shrink: 0;
|
||||
grid-column: 1;
|
||||
grid-template-rows: repeat(7, minmax(0, 1fr));
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
contain: size;
|
||||
gap: var(--calendar-gap);
|
||||
@apply type-small;
|
||||
color: var(--ds-secondary);
|
||||
align-items: center;
|
||||
@@ -797,18 +831,29 @@
|
||||
}
|
||||
.ds-calendar-grid {
|
||||
display: grid;
|
||||
grid-column: 2;
|
||||
grid-template-columns: repeat(var(--calendar-weeks), minmax(0, 1fr));
|
||||
grid-template-rows: repeat(7, 1fr);
|
||||
grid-auto-flow: column;
|
||||
gap: 6px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
gap: var(--calendar-gap);
|
||||
}
|
||||
.ds-day {
|
||||
padding: 0;
|
||||
aspect-ratio: 1;
|
||||
border: 1px solid transparent;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
/* Keep a full-size date target, but reserve filled squares for tracked days. */
|
||||
/* Only narrow portrait screens scroll, preserving comfortable date targets. */
|
||||
@media (max-width: 600px) and (orientation: portrait) {
|
||||
.ds-calendar-viewport { overflow-x: auto; }
|
||||
.ds-day { min-width: 24px; min-height: 24px; }
|
||||
.ds-calendar-timeline {
|
||||
min-width: calc(46px + var(--calendar-weeks) * 24px + (var(--calendar-weeks) - 1) * var(--calendar-gap));
|
||||
}
|
||||
}
|
||||
/* Reserve filled squares for tracked days. */
|
||||
.ds-day--not-due,
|
||||
.ds-legend-swatch--not-due,
|
||||
.ds-state-square--not-due {
|
||||
@@ -825,7 +870,7 @@
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
border-radius: 50%;
|
||||
background: #888;
|
||||
background: var(--ds-secondary);
|
||||
transform: translate(-50%, -50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -890,7 +935,7 @@
|
||||
top: 0;
|
||||
left: 24px;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
background: var(--ds-paper);
|
||||
transform: translateY(-110%);
|
||||
z-index: 2;
|
||||
}
|
||||
@@ -957,7 +1002,7 @@
|
||||
gap: 30px;
|
||||
}
|
||||
.ds-section-heading p br {
|
||||
display: none;
|
||||
display: initial;
|
||||
}
|
||||
.ds-view-switch {
|
||||
gap: 18px;
|
||||
@@ -980,7 +1025,7 @@
|
||||
gap: 2px;
|
||||
}
|
||||
.ds-counter .ds-button {
|
||||
width: 32px;
|
||||
width: 44px;
|
||||
}
|
||||
.ds-task-count {
|
||||
min-width: 106px;
|
||||
@@ -992,6 +1037,7 @@
|
||||
gap: 3px 20px;
|
||||
}
|
||||
.ds-palette {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 13px;
|
||||
}
|
||||
.ds-swatch {
|
||||
@@ -1049,14 +1095,15 @@
|
||||
padding-top: 20px;
|
||||
}
|
||||
.ds-habit-chart:nth-child(n + 3),
|
||||
.ds-habit-chart:has(.ds-calendar[data-months="12"]) + .ds-habit-chart {
|
||||
.ds-habit-chart:has(.ds-calendar:is([data-months="12"], [data-months="custom"])) + .ds-habit-chart {
|
||||
border-top: 1px solid var(--ds-rule);
|
||||
}
|
||||
.ds-habit-chart:has(.ds-calendar[data-months="12"]) {
|
||||
.ds-habit-chart:has(.ds-calendar:is([data-months="12"], [data-months="custom"])) {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.ds-habit-chart-heading {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
@@ -1086,7 +1133,7 @@
|
||||
gap: 4px;
|
||||
}
|
||||
.ds-habit-chart-heading .ds-counter .ds-button {
|
||||
width: 28px;
|
||||
width: 44px;
|
||||
}
|
||||
.ds-habit-chart-progress {
|
||||
display: flex;
|
||||
@@ -1099,12 +1146,6 @@
|
||||
grid-column: 1 / -1;
|
||||
margin-top: -12px !important;
|
||||
}
|
||||
.ds-calendar--compact .ds-calendar-inner {
|
||||
min-width: 380px;
|
||||
}
|
||||
.ds-calendar--compact .ds-calendar-grid {
|
||||
gap: 5px;
|
||||
}
|
||||
.ds-calendar--compact .ds-date-inspector {
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
@@ -1168,6 +1209,10 @@
|
||||
height: 10px;
|
||||
}
|
||||
@media (max-width: 760px) {
|
||||
.ds-split-section {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 30px;
|
||||
}
|
||||
.ds-habit-chart + .ds-habit-chart {
|
||||
border-top: 1px solid var(--ds-rule);
|
||||
}
|
||||
@@ -1175,43 +1220,30 @@
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 32px;
|
||||
}
|
||||
.ds-calendar--compact .ds-calendar-inner {
|
||||
max-width: 520px;
|
||||
}
|
||||
.ds-habit-chart-heading .ds-counter {
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* A full year gets a full row; fit every week instead of clipping or scrolling. */
|
||||
.ds-calendar[data-months="12"] .ds-calendar-inner {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
.ds-calendar[data-months="12"] .ds-calendar-grid {
|
||||
min-width: 0;
|
||||
gap: clamp(2px, 0.4vw, 6px);
|
||||
}
|
||||
|
||||
.ds-legend-scale { flex-wrap: wrap; }
|
||||
|
||||
/* Editing patterns use the same open layout, rules, and neutral controls. */
|
||||
.ds-color-picker { border: 0; border-top: 1px solid var(--ds-rule); padding: 20px 0 0; margin: 26px 0; min-width: 0; }
|
||||
.ds-color-picker legend { padding: 0 12px 0 0; @apply type-small; }
|
||||
.ds-color-picker legend { padding: 0 12px 0 0; @apply type-ui-heading; }
|
||||
.ds-color-picker > .ds-footnote { margin-top: 0 !important; }
|
||||
.ds-color-palettes { display: flex; flex-wrap: wrap; gap: 20px 28px; margin: 20px 0; }
|
||||
.ds-color-palette > span { @apply type-small; color: var(--ds-secondary); }
|
||||
.ds-color-swatches { display: flex; gap: 4px; margin-top: 8px; }
|
||||
.ds-color-swatches button { width: 38px; min-height: 44px; padding: 4px; border: 1px solid transparent; background: transparent; }
|
||||
.ds-color-swatches button { display: grid; place-items: center; width: 44px; min-width: 44px; min-height: 44px; padding: 4px; border: 1px solid transparent; background: transparent; }
|
||||
.ds-color-swatches button[aria-pressed="true"] { border-color: var(--ds-ink); }
|
||||
.ds-color-swatches button > span { display: grid; place-content: center; height: 28px; width: 28px; color: white; @apply type-body; }
|
||||
.ds-custom-color { display: flex; gap: 16px; align-items: start; }
|
||||
.ds-color-swatches button > span { display: grid; place-content: center; height: 28px; width: 28px; color: var(--ds-paper); @apply type-body; }
|
||||
.ds-custom-color { display: flex; flex-wrap: wrap; gap: 16px; align-items: start; }
|
||||
.ds-custom-color .ds-field { margin-bottom: 0; }
|
||||
.ds-custom-color .ds-field:last-child { width: 180px; }
|
||||
.ds-custom-color input[type="color"] { width: 80px; height: 44px; padding: 5px; cursor: pointer; }
|
||||
.ds-color-live-preview { display: flex; flex-wrap: wrap; align-items: center; gap: 12px 16px; margin-top: 22px; @apply type-small; }
|
||||
.ds-color-preview-shades { display: flex; gap: 4px; }
|
||||
.ds-color-preview-shades > span { width: 18px; height: 18px; border: 1px solid #dedede; }
|
||||
.ds-color-preview-shades > span { width: 18px; height: 18px; border: 1px solid var(--ds-rule); }
|
||||
.ds-edit-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 280px minmax(0, 1fr);
|
||||
@@ -1229,12 +1261,12 @@
|
||||
.ds-edit-meta { display: flex; align-items: center; gap: 10px; margin-top: 24px; overflow-wrap: anywhere; }
|
||||
.ds-habit-dot { width: 8px; height: 8px; flex: 0 0 8px; }
|
||||
.ds-field { display: grid; min-width: 0; gap: 8px; margin-bottom: 22px; }
|
||||
.ds-field label, .ds-weekday-field legend { @apply type-small; }
|
||||
.ds-field label, .ds-weekday-field legend { @apply type-ui-heading; }
|
||||
.ds-field :is(input, select) {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 44px;
|
||||
border: 1px solid #999;
|
||||
border: 1px solid var(--ds-control-border);
|
||||
padding: 10px 12px;
|
||||
font: inherit;
|
||||
@apply type-body;
|
||||
@@ -1249,13 +1281,14 @@
|
||||
.ds-edit-content .ds-checkbox { @apply type-small; }
|
||||
.ds-weekday-field, .ds-task-editor-fieldset { border: 0; padding: 0; margin: 0 0 20px; min-width: 0; }
|
||||
.ds-weekdays { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 12px; }
|
||||
.ds-weekdays button { min-height: 44px; min-width: 44px; padding: 8px; border: 1px solid var(--ds-rule); background: white; color: var(--ds-secondary); }
|
||||
.ds-weekdays button[aria-pressed="true"] { color: white; background: var(--ds-ink); border-color: var(--ds-ink); }
|
||||
.ds-weekdays button { min-height: 44px; min-width: 44px; padding: 8px; border: 1px solid var(--ds-control-border); background: var(--ds-paper); color: var(--ds-secondary); }
|
||||
.ds-weekdays button[aria-pressed="true"] { color: var(--ds-paper); background: var(--ds-ink); border-color: var(--ds-ink); }
|
||||
.ds-form-actions { display: flex; flex-wrap: wrap; gap: 24px; align-items: center; }
|
||||
.ds-form-feedback { min-height: 24px; margin: 20px 0 12px !important; color: var(--ds-secondary); @apply type-small; overflow-wrap: anywhere; }
|
||||
.ds-form-feedback[role="alert"] { border-left: 2px solid var(--ds-ink); padding-left: 12px; color: var(--ds-ink); }
|
||||
.ds-save-notice { margin-bottom: 0 !important; }
|
||||
.ds-edit-task { border-top: 1px solid var(--ds-rule); padding: 16px 0 10px; }
|
||||
.ds-edit-task { min-width: 0; margin: 0; border: 0; border-top: 1px solid var(--ds-rule); padding: 16px 0 10px; }
|
||||
.ds-edit-task > legend { padding: 0 12px 0 0; @apply type-ui-heading; }
|
||||
.ds-edit-task .ds-spec-heading { margin-bottom: 16px; }
|
||||
.ds-empty-state { padding: 24px 0; color: var(--ds-secondary); }
|
||||
.ds-history-requirement { display: flex; justify-content: space-between; gap: 16px; border-block: 1px solid var(--ds-rule); padding: 18px 0; margin-bottom: 24px; }
|
||||
@@ -1284,4 +1317,20 @@
|
||||
@media (max-width: 640px) {
|
||||
.ds-header,
|
||||
.ds-preview-toolbar { flex-wrap: wrap; }
|
||||
.ds-view-switch { flex-wrap: wrap; gap: 0 18px; }
|
||||
}
|
||||
|
||||
@media (forced-colors: active) {
|
||||
.ds-checkbox input {
|
||||
appearance: auto;
|
||||
background: Canvas;
|
||||
border-color: ButtonText;
|
||||
}
|
||||
.ds-checkbox input:checked::after { content: none; }
|
||||
.ds-root :is(button, input):disabled { color: GrayText; opacity: 1; }
|
||||
.ds-day { border-color: ButtonText; }
|
||||
.ds-day[aria-pressed="true"] { outline-color: Highlight; }
|
||||
.ds-tab-bar [aria-selected="true"],
|
||||
.ds-month-views [aria-pressed="true"],
|
||||
.ds-weekdays [aria-pressed="true"] { border-color: Highlight; }
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
--text-copy--line-height: 1.5;
|
||||
--text-caption: 0.875rem;
|
||||
--text-caption--line-height: 1.5;
|
||||
--text-ui-heading: 1.125rem;
|
||||
--text-ui-heading--line-height: 1.5;
|
||||
--text-lead: 1.25rem;
|
||||
--text-lead--line-height: 1.5;
|
||||
--text-title: 2rem;
|
||||
@@ -17,6 +19,12 @@
|
||||
|
||||
@utility type-body { @apply text-copy; }
|
||||
@utility type-small { @apply text-caption; }
|
||||
@utility type-ui-heading {
|
||||
@apply text-ui-heading;
|
||||
font-family: var(--ds-sans);
|
||||
font-weight: 500;
|
||||
letter-spacing: normal;
|
||||
}
|
||||
@utility type-label {
|
||||
@apply text-caption;
|
||||
font-weight: 500;
|
||||
|
||||
@@ -3,6 +3,16 @@ import { readFileSync } from "node:fs";
|
||||
|
||||
const css = (name: string) => readFileSync(new URL(name, import.meta.url), "utf8");
|
||||
|
||||
function tokenLuminance(source: string, token: string) {
|
||||
let hex = source.match(new RegExp(`--ds-${token}: #([\\da-f]+);`))![1]!;
|
||||
if (hex.length === 3) hex = [...hex].map((c) => c + c).join("");
|
||||
const channels = [0, 2, 4].map((start) => {
|
||||
const value = parseInt(hex.slice(start, start + 2), 16) / 255;
|
||||
return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
|
||||
});
|
||||
return channels[0]! * 0.2126 + channels[1]! * 0.7152 + channels[2]! * 0.0722;
|
||||
}
|
||||
|
||||
describe("shared typography contract", () => {
|
||||
test("all component and page styles use the fixed shared type utilities", () => {
|
||||
for (const file of ["design-system.css"]) {
|
||||
@@ -15,24 +25,50 @@ describe("shared typography contract", () => {
|
||||
expect(typography).not.toMatch(/clamp\(|\d(?:vw|vh|px)\b/);
|
||||
expect(typography).toContain("--text-copy: 1rem");
|
||||
expect(typography).toContain("--text-caption: 0.875rem");
|
||||
for (const role of ["body", "small", "label", "lead", "title", "section", "display", "control"]) {
|
||||
for (const role of ["body", "small", "label", "ui-heading", "eyebrow", "lead", "title", "section", "display", "control"]) {
|
||||
expect(typography).toContain(`@utility type-${role}`);
|
||||
}
|
||||
});
|
||||
|
||||
test("text colors meet WCAG AA normal-text contrast on paper", () => {
|
||||
const source = css("design-system.css");
|
||||
const luminance = (token: string) => {
|
||||
let hex = source.match(new RegExp(`--ds-${token}: #([\\da-f]+);`))![1]!;
|
||||
if (hex.length === 3) hex = [...hex].map((c) => c + c).join("");
|
||||
const channels = [0, 2, 4].map((start) => {
|
||||
const value = parseInt(hex.slice(start, start + 2), 16) / 255;
|
||||
return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
|
||||
});
|
||||
return channels[0]! * 0.2126 + channels[1]! * 0.7152 + channels[2]! * 0.0722;
|
||||
};
|
||||
for (const color of ["ink", "secondary"]) {
|
||||
expect((luminance("paper") + 0.05) / (luminance(color) + 0.05)).toBeGreaterThanOrEqual(4.5);
|
||||
expect((tokenLuminance(source, "paper") + 0.05) / (tokenLuminance(source, color) + 0.05)).toBeGreaterThanOrEqual(4.5);
|
||||
}
|
||||
});
|
||||
|
||||
test("UI headings are larger than supporting text and reset editorial typography", () => {
|
||||
const rule = css("typography.css").match(/@utility type-ui-heading\s*\{([^}]+)\}/)![1]!;
|
||||
expect(rule).toContain("@apply text-ui-heading");
|
||||
const source = css("typography.css");
|
||||
const size = (role: string) => Number(source.match(new RegExp(`--text-${role}: ([\\d.]+)rem`))![1]);
|
||||
expect(size("ui-heading")).toBe(1.125);
|
||||
expect(size("ui-heading")).toBeGreaterThan(size("caption"));
|
||||
expect(rule).toContain("font-family: var(--ds-sans)");
|
||||
expect(rule).toContain("font-weight: 500");
|
||||
expect(rule).toContain("letter-spacing: normal");
|
||||
expect(css("design-system.css")).toMatch(/\.ds-spec-heading label\s*\{\s*@apply type-ui-heading/);
|
||||
});
|
||||
|
||||
test("control boundaries contrast with paper and use a shared token", () => {
|
||||
const source = css("design-system.css");
|
||||
const ratio = (tokenLuminance(source, "paper") + 0.05) / (tokenLuminance(source, "control-border") + 0.05);
|
||||
expect(ratio).toBeGreaterThanOrEqual(3);
|
||||
const fieldRule = source.match(/\.ds-field :is\(input, select\)\s*\{([^}]+)\}/)![1]!;
|
||||
expect(fieldRule).toContain("border: 1px solid var(--ds-control-border)");
|
||||
});
|
||||
|
||||
test("shared controls retain usable targets and native forced-colors checkboxes", () => {
|
||||
const source = css("design-system.css");
|
||||
const counterRule = source.match(/\.ds-counter \.ds-button\s*\{([^}]+)\}/)![1]!;
|
||||
expect(counterRule).toContain("min-width: 44px");
|
||||
expect(counterRule).toContain("min-height: 44px");
|
||||
const dayRule = source.match(/\.ds-day\s*\{([^}]+)\}/)![1]!;
|
||||
expect(dayRule).toContain("min-width: 0");
|
||||
expect(dayRule).toContain("aspect-ratio: 1");
|
||||
expect(source).toContain("--calendar-gap: 4px;");
|
||||
expect(source).toContain("@media (max-width: 600px) and (orientation: portrait)");
|
||||
expect(source).toContain(".ds-day { min-width: 24px; min-height: 24px; }");
|
||||
expect(source).toMatch(/@media \(forced-colors: active\)\s*\{\s*\.ds-checkbox input\s*\{\s*appearance: auto/);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user