canon-ds 0.1.0
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/LICENSE +21 -0
- package/README.md +153 -0
- package/THIRD_PARTY_NOTICES.md +55 -0
- package/bin/canon.js +6 -0
- package/lib/build-manifest.mjs +54 -0
- package/lib/build.mjs +69 -0
- package/lib/cli.mjs +273 -0
- package/lib/color.js +160 -0
- package/lib/components/_shared.mjs +93 -0
- package/lib/components/accordion.mjs +136 -0
- package/lib/components/account-card.mjs +166 -0
- package/lib/components/activity-feed.mjs +176 -0
- package/lib/components/activity-gauge.mjs +98 -0
- package/lib/components/agent-presence.mjs +156 -0
- package/lib/components/alert.mjs +154 -0
- package/lib/components/app-store-button.mjs +108 -0
- package/lib/components/avatar-group.mjs +128 -0
- package/lib/components/avatar.mjs +225 -0
- package/lib/components/badge-group.mjs +55 -0
- package/lib/components/badge.mjs +174 -0
- package/lib/components/banner.mjs +116 -0
- package/lib/components/breadcrumb.mjs +170 -0
- package/lib/components/button-group.mjs +114 -0
- package/lib/components/button.mjs +197 -0
- package/lib/components/card-header.mjs +115 -0
- package/lib/components/card.mjs +106 -0
- package/lib/components/carousel.mjs +182 -0
- package/lib/components/chart-frame.mjs +101 -0
- package/lib/components/checkbox.mjs +163 -0
- package/lib/components/close-button.mjs +49 -0
- package/lib/components/code.mjs +123 -0
- package/lib/components/color-picker.mjs +122 -0
- package/lib/components/combobox.mjs +210 -0
- package/lib/components/command-palette.mjs +226 -0
- package/lib/components/content-divider.mjs +104 -0
- package/lib/components/counter.mjs +86 -0
- package/lib/components/credit-card.mjs +126 -0
- package/lib/components/date-picker.mjs +202 -0
- package/lib/components/description-list.mjs +88 -0
- package/lib/components/dialog.mjs +140 -0
- package/lib/components/divider.mjs +115 -0
- package/lib/components/drawer.mjs +150 -0
- package/lib/components/empty-state.mjs +112 -0
- package/lib/components/featured-card.mjs +124 -0
- package/lib/components/featured-icon.mjs +67 -0
- package/lib/components/field.mjs +122 -0
- package/lib/components/file-dropzone.mjs +166 -0
- package/lib/components/filter-bar.mjs +179 -0
- package/lib/components/header-navigation.mjs +94 -0
- package/lib/components/icon-button.mjs +179 -0
- package/lib/components/index.mjs +52 -0
- package/lib/components/inline-cta.mjs +139 -0
- package/lib/components/input-group.mjs +164 -0
- package/lib/components/input.mjs +197 -0
- package/lib/components/kanban.mjs +179 -0
- package/lib/components/kbd.mjs +70 -0
- package/lib/components/kicker.mjs +79 -0
- package/lib/components/link.mjs +108 -0
- package/lib/components/list.mjs +117 -0
- package/lib/components/media-frame.mjs +119 -0
- package/lib/components/menu.mjs +190 -0
- package/lib/components/message.mjs +249 -0
- package/lib/components/metric-group.mjs +210 -0
- package/lib/components/mobile-header.mjs +173 -0
- package/lib/components/multi-select.mjs +218 -0
- package/lib/components/notification.mjs +175 -0
- package/lib/components/number-input.mjs +149 -0
- package/lib/components/page-header.mjs +68 -0
- package/lib/components/pagination.mjs +236 -0
- package/lib/components/pin-input.mjs +120 -0
- package/lib/components/popover.mjs +126 -0
- package/lib/components/progress-circle.mjs +115 -0
- package/lib/components/progress.mjs +93 -0
- package/lib/components/prose.mjs +96 -0
- package/lib/components/radio.mjs +160 -0
- package/lib/components/rating.mjs +93 -0
- package/lib/components/rich-text-editor.mjs +178 -0
- package/lib/components/section-header.mjs +62 -0
- package/lib/components/segmented-control.mjs +121 -0
- package/lib/components/select.mjs +163 -0
- package/lib/components/sidebar-nav.mjs +172 -0
- package/lib/components/sidebar.mjs +124 -0
- package/lib/components/skeleton.mjs +66 -0
- package/lib/components/slider.mjs +122 -0
- package/lib/components/social-button.mjs +119 -0
- package/lib/components/spinner.mjs +69 -0
- package/lib/components/stat.mjs +120 -0
- package/lib/components/stepper.mjs +228 -0
- package/lib/components/switch.mjs +159 -0
- package/lib/components/table.mjs +212 -0
- package/lib/components/tabs.mjs +207 -0
- package/lib/components/tag.mjs +196 -0
- package/lib/components/tags-input.mjs +146 -0
- package/lib/components/textarea.mjs +117 -0
- package/lib/components/timeline.mjs +135 -0
- package/lib/components/toast.mjs +115 -0
- package/lib/components/tooltip.mjs +86 -0
- package/lib/components/topbar.mjs +207 -0
- package/lib/components/tree-view.mjs +156 -0
- package/lib/components/video-player.mjs +189 -0
- package/lib/connect.mjs +110 -0
- package/lib/design-files.mjs +175 -0
- package/lib/distribution.mjs +109 -0
- package/lib/editor.js +940 -0
- package/lib/engine.js +339 -0
- package/lib/generators/agents.mjs +68 -0
- package/lib/generators/connection.md +58 -0
- package/lib/generators/css.mjs +2 -0
- package/lib/generators/designmd.mjs +324 -0
- package/lib/generators/documentation.css +82 -0
- package/lib/generators/documentation.html +81 -0
- package/lib/generators/documentation.js +96 -0
- package/lib/generators/documentation.mjs +35 -0
- package/lib/generators/dtcg.mjs +60 -0
- package/lib/generators/preview.css +279 -0
- package/lib/generators/preview.mjs +225 -0
- package/lib/generators/react.mjs +162 -0
- package/lib/generators/tailwind.mjs +114 -0
- package/lib/install.mjs +108 -0
- package/lib/lint.mjs +538 -0
- package/lib/mcp.mjs +268 -0
- package/lib/open.mjs +7 -0
- package/lib/patterns/_app.mjs +56 -0
- package/lib/patterns/app-shell.mjs +25 -0
- package/lib/patterns/audit-log.mjs +37 -0
- package/lib/patterns/auth-pages.mjs +44 -0
- package/lib/patterns/billing-page.mjs +39 -0
- package/lib/patterns/calendar-page.mjs +55 -0
- package/lib/patterns/dashboard-page.mjs +28 -0
- package/lib/patterns/files-page.mjs +30 -0
- package/lib/patterns/first-run.mjs +31 -0
- package/lib/patterns/form-layout.mjs +24 -0
- package/lib/patterns/inbox-page.mjs +58 -0
- package/lib/patterns/index.mjs +29 -0
- package/lib/patterns/list-detail-page.mjs +24 -0
- package/lib/patterns/mobile-app.mjs +46 -0
- package/lib/patterns/modal-flows.mjs +32 -0
- package/lib/patterns/onboarding-page.mjs +42 -0
- package/lib/patterns/permissions-page.mjs +38 -0
- package/lib/patterns/profile-page.mjs +32 -0
- package/lib/patterns/settings-page.mjs +24 -0
- package/lib/project.mjs +28 -0
- package/lib/serve.mjs +279 -0
- package/lib/system.mjs +210 -0
- package/lib/tokens/base.js +251 -0
- package/lib/tokens/canon-preset.mjs +67 -0
- package/lib/tokens/index.mjs +1 -0
- package/lib/tokens/legacy-canon.json +559 -0
- package/lib/tokens/presets.mjs +161 -0
- package/lib/tokens/resolve.mjs +1 -0
- package/lib/types.mjs +4 -0
- package/lib/version.mjs +2 -0
- package/package.json +54 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { ex, typeStyle } from "./_shared.mjs";
|
|
2
|
+
// the reference count chips: the 22px badge (12px medium, tinted fill with a ring one step darker)
|
|
3
|
+
// next to tabs and nav items, and the 14px red disc (10px bold white) on notification icons.
|
|
4
|
+
const ring = (color) => `inset 0 0 0 1px ${color}`;
|
|
5
|
+
const PAINT = {
|
|
6
|
+
neutral: { solid: ['{color.bg-inverse}', '{color.fg-inverse}'], soft: ['{color.bg-subtle}', '{color.fg-muted}', '{color.border-default}'] },
|
|
7
|
+
accent: { solid: ['{color.bg-accent}', '{color.fg-on-accent}'], soft: ['{color.bg-accent-subtle}', '{color.fg-accent}', '{brand.200}'] },
|
|
8
|
+
danger: { solid: ['{color.bg-danger}', '{white}'], soft: ['{color.bg-danger-subtle}', '{color.fg-danger}', '{red.200}'] },
|
|
9
|
+
action: { solid: ['{color.bg-action}', '{color.fg-on-action}'], soft: ['{brand.50}', '{brand.700}', '{brand.200}'] },
|
|
10
|
+
};
|
|
11
|
+
const compound = Object.entries(PAINT).flatMap(([tone, p]) => [
|
|
12
|
+
{ when: { tone, variant: 'solid' }, block: { root: { 'background-color': p.solid[0], color: p.solid[1], 'box-shadow': 'none' } } },
|
|
13
|
+
{ when: { tone, variant: 'soft' }, block: { root: { 'background-color': p.soft[0], color: p.soft[1], 'box-shadow': ring(p.soft[2]) } } },
|
|
14
|
+
]);
|
|
15
|
+
const c = (n, tone = 'neutral', variant = 'soft', size = 'md') => `<span class="cn-counter" data-tone="${tone}" data-variant="${variant}" data-size="${size}">${n}</span>`;
|
|
16
|
+
export const counter = {
|
|
17
|
+
name: 'Counter',
|
|
18
|
+
slug: 'counter',
|
|
19
|
+
category: 'data-display',
|
|
20
|
+
description: 'A small round pill with a number: the reference\'s 22px count badge (12px medium, tinted with a ring) after tabs and nav items, or the 14px red disc on a notification icon. Tabular figures, capped at "99+". Numbers only.',
|
|
21
|
+
usage: 'After a label that it counts: a tab, a nav item, a section title, a filter; or on the corner of an icon button for unread notifications. Not for words (Badge), not for large KPIs (Stat), never as a button.',
|
|
22
|
+
anatomy: [
|
|
23
|
+
{ part: 'root', element: 'span', description: 'The pill. Contains only the number (or "99+").' },
|
|
24
|
+
],
|
|
25
|
+
props: {
|
|
26
|
+
tone: { values: ['neutral', 'accent', 'danger', 'action'], default: 'neutral', description: 'Urgency, not decoration. neutral (gray) = totals and result counts; action (brand) = things waiting for the user, and the selected tab\'s count; danger (red) = failures, overdue items and unread notifications; accent = items produced by the assistant.' },
|
|
27
|
+
variant: { values: ['solid', 'soft'], default: 'soft', description: 'soft = tinted fill with a 1px ring one step darker (the reference badge); solid = filled, at most one per row or nav to mark the count that matters (and always for the xs notification disc).' },
|
|
28
|
+
size: { values: ['xs', 'sm', 'md'], default: 'md', description: 'md 22px tall with 12px medium text (the badge next to tabs and nav items); sm 18px inside buttons and dense rows; xs 14px with 10px bold text, the notification disc on an icon corner.' },
|
|
29
|
+
},
|
|
30
|
+
states: {},
|
|
31
|
+
base: {
|
|
32
|
+
root: {
|
|
33
|
+
display: 'inline-flex',
|
|
34
|
+
'align-items': 'center',
|
|
35
|
+
'justify-content': 'center',
|
|
36
|
+
'min-width': '22px',
|
|
37
|
+
height: '22px',
|
|
38
|
+
'padding-inline': '{space.2}',
|
|
39
|
+
'border-radius': '{radius.full}',
|
|
40
|
+
...typeStyle('label-xs'),
|
|
41
|
+
'font-variant-numeric': 'tabular-nums',
|
|
42
|
+
'flex-shrink': '0',
|
|
43
|
+
'white-space': 'nowrap',
|
|
44
|
+
'vertical-align': 'middle',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
variants: {
|
|
48
|
+
tone: { neutral: { root: {} }, accent: { root: {} }, danger: { root: {} }, action: { root: {} } },
|
|
49
|
+
variant: { solid: { root: {} }, soft: { root: {} } },
|
|
50
|
+
size: {
|
|
51
|
+
xs: { root: { 'min-width': '{space.3.5}', height: '{space.3.5}', 'padding-inline': '3px', 'font-size': '10px', 'font-weight': '{font.weight.bold}', 'line-height': '1' } },
|
|
52
|
+
sm: { root: { 'min-width': '18px', height: '18px', 'padding-inline': '{space.1.5}', 'font-size': '{font.size.2xs}', 'line-height': '1' } },
|
|
53
|
+
md: { root: { 'min-width': '22px', height: '22px', 'padding-inline': '{space.2}' } },
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
compound,
|
|
57
|
+
extraCss: `
|
|
58
|
+
.cn-counter[data-size="xs"] { box-shadow: 0 0 0 1.5px {color.bg-surface}; }
|
|
59
|
+
.cn-counter[data-anchored] { position: absolute; top: -1px; right: -1px; }`,
|
|
60
|
+
examples: [
|
|
61
|
+
ex('Soft (default)', c('12'), '22px, gray-50 with a gray-200 ring, 12px medium gray-700.'),
|
|
62
|
+
ex('Tones, soft', `${c('128', 'neutral')} ${c('7', 'accent')} ${c('2', 'danger')} ${c('3', 'action')}`, 'neutral · accent · danger · action; each with the ring one step darker than its fill.'),
|
|
63
|
+
ex('Tones, solid', `${c('128', 'neutral', 'solid')} ${c('7', 'accent', 'solid')} ${c('2', 'danger', 'solid')} ${c('3', 'action', 'solid')}`, 'One solid counter per row at most.'),
|
|
64
|
+
ex('Sizes and cap', `<div style="display:flex;align-items:center;gap:var(--cn-space-3)">${c('3', 'danger', 'solid', 'xs')} ${c('45', 'neutral', 'soft', 'sm')} ${c('99+', 'neutral', 'soft', 'md')}</div>`, 'xs 14px (the notification disc) · sm 18px · md 22px. Never more than two digits; cap at 99+.'),
|
|
65
|
+
],
|
|
66
|
+
recipes: [
|
|
67
|
+
ex('In a nav item, a tab and on an icon', `<div style="display:flex;flex-wrap:wrap;gap:var(--cn-space-3);align-items:center"><button type="button" class="cn-button" data-variant="ghost" data-size="sm"><span class="cn-button__label">Waiting for you</span>${c('3', 'action', 'soft', 'md')}</button><button type="button" class="cn-button" data-variant="ghost" data-size="sm"><span class="cn-button__label">In review</span>${c('12', 'neutral', 'soft', 'md')}</button><span style="position:relative;display:inline-flex"><button type="button" class="cn-icon-button" data-variant="ghost" data-size="sm" data-shape="square" aria-label="Notifications, 3 unread"><svg class="cn-icon-button__icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2.5 9.5h3l1 2h3l1-2h3M2.5 9.5V4a1 1 0 011-1h9a1 1 0 011 1v5.5M2.5 9.5V12a1 1 0 001 1h9a1 1 0 001-1V9.5"/></svg></button>${c('3', 'danger', 'solid', 'xs').replace('data-size="xs"', 'data-size="xs" data-anchored')}</span></div>`, 'The counter follows the label it counts; only the "needs you" count is action-tinted. data-anchored pins the xs disc to the icon corner.'),
|
|
68
|
+
],
|
|
69
|
+
rules: [
|
|
70
|
+
'Numbers only. A word ("New", "Beta", "Live") is a Badge.',
|
|
71
|
+
'Cap at "99+". Never show three or more digits.',
|
|
72
|
+
'Hide the counter at zero; "0" is noise. If the zero is the content ("0 results"), write it as text.',
|
|
73
|
+
'Tone is urgency: neutral for totals, action for what waits for the user, danger for failures and unread notifications, accent for assistant output. Never pick a tone for looks.',
|
|
74
|
+
'At most one solid counter per row, nav or tab bar; the rest are soft. The xs disc is always solid danger.',
|
|
75
|
+
'The counter comes after the label it counts ("Inbox 12"), separated by the parent\'s gap, never before it.',
|
|
76
|
+
'Stays round at one digit and widens with two; never squash it into a fixed width.',
|
|
77
|
+
'Do not animate count changes (no bounce, no flash). Update the number; announce with aria-live on the region if it matters.',
|
|
78
|
+
],
|
|
79
|
+
a11y: [
|
|
80
|
+
'The number is plain text and is read as such; give it context with the adjacent label or a visually hidden suffix ("12 unread").',
|
|
81
|
+
'On an icon button, put the count in the button\'s aria-label ("Notifications, 3 unread") and keep the disc aria-hidden.',
|
|
82
|
+
'Live counts: aria-live="polite" on the containing region (the nav item or tab), not on the counter itself.',
|
|
83
|
+
'Color is reinforcement only; the label next to the counter carries the meaning.',
|
|
84
|
+
],
|
|
85
|
+
related: ['badge', 'tag', 'stat', 'tabs', 'sidebar-nav', 'topbar'],
|
|
86
|
+
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { ex, typeStyle } from "./_shared.mjs";
|
|
2
|
+
// Credit card: a decorative 316px card at the standard 1.586 ratio with a logo,
|
|
3
|
+
// a chip, a masked number, holder name, expiry and a generic network mark. It
|
|
4
|
+
// represents a saved payment method; it never shows a full card number.
|
|
5
|
+
const CHIP = '<svg class="cn-credit-card__chip" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linejoin="round" aria-hidden="true"><rect x="1" y="1" width="30" height="22" rx="4" style="fill:currentColor;fill-opacity:.18"/><path d="M1 9h9a3 3 0 013 3v0a3 3 0 01-3 3H1M31 9h-9a3 3 0 00-3 3v0a3 3 0 003 3h9M13 1v8M19 1v8M13 15v8M19 15v8"/></svg>';
|
|
6
|
+
const MARK = '<svg class="cn-credit-card__brand" viewBox="0 0 40 24" aria-hidden="true"><circle cx="14" cy="12" r="11" style="fill:currentColor;fill-opacity:.55"/><circle cx="26" cy="12" r="11" style="fill:currentColor;fill-opacity:.35"/></svg>';
|
|
7
|
+
const LOGO = '<span class="cn-credit-card__logo"><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.4 6.1 6.1 2.4-6.1 2.4L10 18.5l-2.4-6.1L1.5 10l6.1-2.4z"/></svg>Lumen</span>';
|
|
8
|
+
const card = (variant, size, o) => `<div class="cn-credit-card" data-variant="${variant}" data-size="${size}" role="img" aria-label="Lumen card ending in ${o.last4}, ${o.name}, expires ${o.expiry}"><div class="cn-credit-card__header">${LOGO}${CHIP}</div><div class="cn-credit-card__number">•••• •••• •••• ${o.last4}</div><div class="cn-credit-card__footer"><div class="cn-credit-card__holder"><span><span class="cn-credit-card__caption">Card holder</span><span class="cn-credit-card__name">${o.name}</span></span><span><span class="cn-credit-card__caption">Expires</span><span class="cn-credit-card__expiry">${o.expiry}</span></span></div>${MARK}</div></div>`;
|
|
9
|
+
const row = (...items) => `<div style="display:flex;flex-wrap:wrap;gap:var(--cn-space-4);align-items:flex-start">${items.join('')}</div>`;
|
|
10
|
+
export const creditCard = {
|
|
11
|
+
name: 'CreditCard',
|
|
12
|
+
slug: 'credit-card',
|
|
13
|
+
category: 'media',
|
|
14
|
+
description: 'A decorative payment card at the real 1.586 ratio: logo and chip on top, a masked number in the middle, holder and expiry at the bottom next to a generic network mark. Four finishes (brand, dark, light, outline) and two sizes.',
|
|
15
|
+
usage: 'Use in billing settings, checkout summaries and the payment-method picker to show which saved card is in use. It is an illustration of a record, not a form: editing happens in a Dialog with real Inputs. Never render a full card number.',
|
|
16
|
+
anatomy: [
|
|
17
|
+
{ part: 'root', element: 'div', description: 'The card: 316px wide (256 in sm), aspect-ratio 1.586, radius 2xl, 24px padding, flex column with the number centered vertically. role="img" with an aria-label that names the card.' },
|
|
18
|
+
{ part: 'header', element: 'div', description: 'Top row: logo on the left, chip on the right.' },
|
|
19
|
+
{ part: 'logo', element: 'span', description: 'Issuer or product wordmark, label-md semibold with a 20px mark.' },
|
|
20
|
+
{ part: 'chip', element: 'svg', description: 'The 32px contact chip drawn in currentColor at reduced opacity.' },
|
|
21
|
+
{ part: 'number', element: 'div', description: 'The masked number "•••• •••• •••• 4242" in numeric-md, tabular figures and wider tracking.' },
|
|
22
|
+
{ part: 'footer', element: 'div', description: 'Bottom row: holder block on the left, network mark on the right.' },
|
|
23
|
+
{ part: 'holder', element: 'div', description: 'Name and expiry side by side, each under a tiny caption.' },
|
|
24
|
+
{ part: 'caption', element: 'span', description: 'Kicker-voice caption above name and expiry ("Card holder", "Expires").' },
|
|
25
|
+
{ part: 'name', element: 'span', description: 'Cardholder name in label-sm, wide tracking, truncated.' },
|
|
26
|
+
{ part: 'expiry', element: 'span', description: 'MM/YY in numeric-md.' },
|
|
27
|
+
{ part: 'brand', element: 'svg', description: 'A generic 40px network mark (two overlapping circles in currentColor). Replace with the real network logo when known.' },
|
|
28
|
+
],
|
|
29
|
+
props: {
|
|
30
|
+
variant: {
|
|
31
|
+
values: ['brand', 'dark', 'light', 'outline'],
|
|
32
|
+
default: 'brand',
|
|
33
|
+
description: 'brand = action-colored card with on-action text, the default card of the product. dark = inverse surface (ink in light theme, near-white in dark theme). light = surface with a hairline and shadow-xs, for lists of several cards. outline = transparent with a strong hairline, the "add a card" or placeholder look.',
|
|
34
|
+
},
|
|
35
|
+
size: {
|
|
36
|
+
values: ['sm', 'md'],
|
|
37
|
+
default: 'md',
|
|
38
|
+
description: 'md = 316px wide, 24px padding, 16px number (settings pages, checkout). sm = 256px, 16px padding, 13px number for pickers, drawers and mobile.',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
states: {
|
|
42
|
+
selected: { selector: '[aria-selected="true"], &[data-selected]', description: 'The chosen card in a picker: a 2px action-colored ring outside the card.', markup: 'aria-selected="true" (inside a listbox) or data-selected on the root' },
|
|
43
|
+
},
|
|
44
|
+
base: {
|
|
45
|
+
root: {
|
|
46
|
+
position: 'relative',
|
|
47
|
+
display: 'flex',
|
|
48
|
+
'flex-direction': 'column',
|
|
49
|
+
'justify-content': 'space-between',
|
|
50
|
+
width: '316px',
|
|
51
|
+
'max-width': '100%',
|
|
52
|
+
'aspect-ratio': '1.586',
|
|
53
|
+
padding: '{space.6}',
|
|
54
|
+
'border-radius': '{radius.2xl}',
|
|
55
|
+
overflow: 'hidden',
|
|
56
|
+
isolation: 'isolate',
|
|
57
|
+
'background-color': '{color.bg-action}',
|
|
58
|
+
color: '{color.fg-on-action}',
|
|
59
|
+
'user-select': 'none',
|
|
60
|
+
'flex-shrink': '0',
|
|
61
|
+
},
|
|
62
|
+
header: { display: 'flex', 'align-items': 'flex-start', 'justify-content': 'space-between', gap: '{space.4}' },
|
|
63
|
+
logo: { display: 'inline-flex', 'align-items': 'center', gap: '{space.2}', ...typeStyle('label-md'), 'font-weight': '{font.weight.semibold}', 'letter-spacing': '{font.letterSpacing.tight}' },
|
|
64
|
+
chip: { display: 'block', width: '{size.icon.2xl}', height: 'auto', 'flex-shrink': '0' },
|
|
65
|
+
number: { ...typeStyle('numeric-md'), 'font-size': '{font.size.lg}', 'letter-spacing': '{font.letterSpacing.wider}', 'font-variant-numeric': 'tabular-nums', 'white-space': 'nowrap' },
|
|
66
|
+
footer: { display: 'flex', 'align-items': 'flex-end', 'justify-content': 'space-between', gap: '{space.4}', 'min-width': '0' },
|
|
67
|
+
holder: { display: 'flex', 'align-items': 'flex-end', gap: '{space.6}', 'min-width': '0' },
|
|
68
|
+
caption: { display: 'block', ...typeStyle('kicker'), opacity: '{opacity.muted}', 'margin-bottom': '{space.1.5}' },
|
|
69
|
+
name: { display: 'block', ...typeStyle('label-sm'), 'letter-spacing': '{font.letterSpacing.wide}', 'white-space': 'nowrap', overflow: 'hidden', 'text-overflow': 'ellipsis', 'max-width': '160px' },
|
|
70
|
+
expiry: { display: 'block', ...typeStyle('numeric-md'), 'font-variant-numeric': 'tabular-nums' },
|
|
71
|
+
brand: { display: 'block', width: '{space.10}', height: 'auto', 'flex-shrink': '0' },
|
|
72
|
+
'@states': {
|
|
73
|
+
selected: { root: { 'box-shadow': '0 0 0 {border.width.medium} {color.border-action}' } },
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
variants: {
|
|
77
|
+
variant: {
|
|
78
|
+
brand: { root: { 'background-color': '{color.bg-action}', color: '{color.fg-on-action}' } },
|
|
79
|
+
dark: { root: { 'background-color': '{color.bg-inverse}', color: '{color.fg-inverse}' } },
|
|
80
|
+
light: { root: { 'background-color': '{color.bg-surface}', color: '{color.fg-default}', border: '{border.width.thin} solid {color.border-default}', 'box-shadow': '{shadow.xs}' } },
|
|
81
|
+
outline: { root: { 'background-color': 'transparent', color: '{color.fg-default}', border: '{border.width.thin} solid {color.border-strong}' } },
|
|
82
|
+
},
|
|
83
|
+
size: {
|
|
84
|
+
md: { root: { width: '316px', padding: '{space.6}' } },
|
|
85
|
+
sm: {
|
|
86
|
+
root: { width: '256px', padding: '{space.4}', 'border-radius': '{radius.xl}' },
|
|
87
|
+
number: { 'font-size': '{font.size.sm}', 'letter-spacing': '{font.letterSpacing.wide}' },
|
|
88
|
+
chip: { width: '{size.icon.xl}' },
|
|
89
|
+
brand: { width: '{space.8}' },
|
|
90
|
+
header: { gap: '{space.3}' },
|
|
91
|
+
holder: { gap: '{space.4}' },
|
|
92
|
+
caption: { 'margin-bottom': '{space.1}' },
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
extraCss: `
|
|
97
|
+
.cn-credit-card::before { content: ''; position: absolute; width: 240px; height: 240px; border-radius: {radius.full}; inset-inline-end: -88px; top: -128px; background-color: color-mix(in srgb, currentColor 8%, transparent); pointer-events: none; z-index: -1; }
|
|
98
|
+
.cn-credit-card[data-variant="outline"]::before { display: none; }
|
|
99
|
+
.cn-credit-card__logo > svg { width: {size.icon.lg}; height: {size.icon.lg}; flex-shrink: 0; }
|
|
100
|
+
.cn-credit-card[data-size="sm"] .cn-credit-card__logo { font-size: {font.size.sm}; }
|
|
101
|
+
.cn-credit-card[data-size="sm"] .cn-credit-card__logo > svg { width: {size.icon.md}; height: {size.icon.md}; }`,
|
|
102
|
+
examples: [
|
|
103
|
+
ex('Brand (default)', card('brand', 'md', { last4: '4242', name: 'Maya Chen', expiry: '09/29' }), 'The product card: action fill, on-action text, a quiet circle in the corner.'),
|
|
104
|
+
ex('Dark', card('dark', 'md', { last4: '8210', name: 'Daniel Costa', expiry: '03/28' }), 'Inverse surface; flips to near-white in the dark theme.'),
|
|
105
|
+
ex('Light', card('light', 'md', { last4: '1187', name: 'Sofia Almeida', expiry: '11/27' }), 'Surface with a hairline, for lists of several saved cards.'),
|
|
106
|
+
ex('Outline', card('outline', 'md', { last4: '0093', name: 'Aisha Khan', expiry: '06/30' }), 'Transparent placeholder look for "add a card" states and disabled methods.'),
|
|
107
|
+
ex('Small, selected in a picker', row(card('brand', 'sm', { last4: '4242', name: 'Maya Chen', expiry: '09/29' }).replace('role="img"', 'role="img" data-selected'), card('light', 'sm', { last4: '1187', name: 'Maya Chen', expiry: '11/27' })), '256px cards for pickers and mobile; data-selected adds the action-colored ring.'),
|
|
108
|
+
],
|
|
109
|
+
rules: [
|
|
110
|
+
'Decorative: the card is a picture of a saved payment method, never a form. Editing and adding cards happen in a Dialog with Inputs.',
|
|
111
|
+
'Always mask the number as "•••• •••• •••• 4242": the last four digits only, never more, never the CVC, never an expiry that is not needed on screen.',
|
|
112
|
+
'One brand card per screen; other saved cards use light or outline so the default method stands out.',
|
|
113
|
+
'Keep the 1.586 ratio and the 316/256 widths; never stretch the card to a column. Center it or align it with the form above.',
|
|
114
|
+
'The network mark is generic until the network is known; then use the real mark at 40px, never a colored badge.',
|
|
115
|
+
'Names are shown as stored, truncated with an ellipsis after 160px; expiry is MM/YY.',
|
|
116
|
+
'No gradients, foils or photos; the only decoration is the 8% circle in the corner.',
|
|
117
|
+
'The card never opens anything on click by itself. In a picker, wrap it in a listbox option and use data-selected for the ring.',
|
|
118
|
+
],
|
|
119
|
+
a11y: [
|
|
120
|
+
'The root has role="img" and an aria-label that says it all: "Lumen card ending in 4242, Maya Chen, expires 09/29". The inner text is then decorative.',
|
|
121
|
+
'When the card is an option in a picker, the option element carries the name and aria-selected; the card inside stays role="img".',
|
|
122
|
+
'Contrast: on-action text on the action fill and fg-inverse on bg-inverse are AA by token design; the light and outline variants use fg-default.',
|
|
123
|
+
'Do not expose the number as a table of digits; screen readers read the masked string with the last four digits.',
|
|
124
|
+
],
|
|
125
|
+
related: ['card', 'description-list', 'dialog', 'input', 'badge'],
|
|
126
|
+
};
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { RESET_BUTTON, TRANSITION_COLORS, ex, ICON, typeStyle } from "./_shared.mjs";
|
|
2
|
+
// Calendar panel: header (month + 32px ghost nav), a 7-column weekday row and a 7-column grid of
|
|
3
|
+
// 40px round day buttons (36px on sm), a footer with the date input and Cancel / Apply, and an
|
|
4
|
+
// optional presets column on the left. Panel width = 7 days + 2 × space.6 = 328px on md.
|
|
5
|
+
const DAY = { sm: '{space.9}', md: '{space.10}' };
|
|
6
|
+
const WEEKDAYS = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'];
|
|
7
|
+
const day = (n, o = {}) => `<button type="button" class="cn-date-picker__day"${o.selected ? ' aria-selected="true"' : ''}${o.range ? ` data-range="${o.range}"` : ''}${o.today ? ' data-today aria-current="date"' : ''}${o.outside ? ' data-outside' : ''}${o.disabled ? ' disabled' : ''}>${n}</button>`;
|
|
8
|
+
/** September 2026 starts on a Tuesday and has 30 days; weeks start on Monday. Today is Sep 11. */
|
|
9
|
+
function september(sel) {
|
|
10
|
+
const cells = [day(31, { outside: true })];
|
|
11
|
+
for (let n = 1; n <= 30; n++)
|
|
12
|
+
cells.push(day(n, { ...sel(n), today: n === 11 }));
|
|
13
|
+
for (let n = 1; n <= 4; n++)
|
|
14
|
+
cells.push(day(n, { outside: true }));
|
|
15
|
+
return cells.join('');
|
|
16
|
+
}
|
|
17
|
+
const single = (selected) => september((n) => ({ selected: n === selected }));
|
|
18
|
+
const range = (start, end) => september((n) => (n === start ? { selected: true, range: 'start' } : n === end ? { selected: true, range: 'end' } : n > start && n < end ? { range: 'middle' } : {}));
|
|
19
|
+
const prev = ICON.chevronRight.replace('cn-icon', 'cn-icon" style="transform:scaleX(-1)');
|
|
20
|
+
const nav = `<button type="button" class="cn-date-picker__nav" aria-label="Previous month">${prev}</button><div class="cn-date-picker__month" aria-live="polite">September 2026</div><button type="button" class="cn-date-picker__nav" aria-label="Next month">${ICON.chevronRight}</button>`;
|
|
21
|
+
const weekdays = `<div class="cn-date-picker__weekdays" aria-hidden="true">${WEEKDAYS.map((d) => `<span>${d}</span>`).join('')}</div>`;
|
|
22
|
+
const footer = (value, label) => `<div class="cn-date-picker__footer"><div class="cn-input" data-variant="default" data-size="sm">${ICON.calendar.replace('cn-icon', 'cn-input__icon')}<input class="cn-input__field" type="text" value="${value}" aria-label="${label}"></div><button type="button" class="cn-button" data-variant="outline" data-size="sm"><span class="cn-button__label">Cancel</span></button><button type="button" class="cn-button" data-variant="primary" data-size="sm"><span class="cn-button__label">Apply</span></button></div>`;
|
|
23
|
+
const calendar = (days, value, label) => `<div class="cn-date-picker__calendar"><div class="cn-date-picker__header">${nav}</div>${weekdays}<div class="cn-date-picker__grid" role="group" aria-label="September 2026">${days}</div>${footer(value, label)}</div>`;
|
|
24
|
+
const PRESETS = ['Today', 'Yesterday', 'This week', 'Last week', 'This month', 'Last month'];
|
|
25
|
+
const presets = (current) => `<div class="cn-date-picker__presets">${PRESETS.map((p) => `<button type="button" class="cn-date-picker__preset"${p === current ? ' aria-current="true"' : ''}>${p}</button>`).join('')}</div>`;
|
|
26
|
+
const panel = (attrs, inner) => `<div class="cn-date-picker" ${attrs} role="dialog" aria-label="Choose a date">${inner}</div>`;
|
|
27
|
+
export const datePicker = {
|
|
28
|
+
name: 'DatePicker',
|
|
29
|
+
slug: 'date-picker',
|
|
30
|
+
category: 'forms',
|
|
31
|
+
description: 'A calendar panel: month header with ghost prev/next buttons, a weekday row, a 7-column grid of round day buttons, and a footer with the typed date and Cancel / Apply. Selected days fill with the action color, ranges tint the days between, today carries a dot. An optional presets column offers shortcuts.',
|
|
32
|
+
usage: 'Pick one date or a start–end range: due dates, report periods, filters by period. The panel opens from a Button (calendar icon + the formatted date) or from an Input with a leading calendar icon. For a time or a date-time use an Input with the right type; for recurring rules use a form.',
|
|
33
|
+
anatomy: [
|
|
34
|
+
{ part: 'root', element: 'div', description: 'The floating panel (row): surface-raised, hairline, radius panel, shadow-lg. role="dialog" with an aria-label. Holds an optional presets column and the calendar column. Carries data-variant and data-size.' },
|
|
35
|
+
{ part: 'presets', element: 'div', description: 'Left column of shortcut buttons (Today, Yesterday, This week, Last week, This month, Last month), separated from the calendar by a hairline. Only in the with-presets variant.', optional: true },
|
|
36
|
+
{ part: 'preset', element: 'button', description: 'One shortcut row, 36px, body-sm, radius md; aria-current="true" on the active one.', optional: true },
|
|
37
|
+
{ part: 'calendar', element: 'div', description: 'The calendar column (flex column) wrapping header, weekdays, grid and footer. Width = 7 days + 2 × space.6 (328px on md, 300px on sm).' },
|
|
38
|
+
{ part: 'header', element: 'div', description: 'Row with the previous button, the month name and the next button.' },
|
|
39
|
+
{ part: 'month', element: 'div', description: 'The month and year ("September 2026"), label-md, aria-live="polite".' },
|
|
40
|
+
{ part: 'nav', element: 'button', description: '32px ghost icon button with a chevron; aria-label "Previous month" / "Next month".' },
|
|
41
|
+
{ part: 'weekdays', element: 'div', description: '7-column row of two-letter weekday names, label-sm muted, aria-hidden.' },
|
|
42
|
+
{ part: 'grid', element: 'div', description: '7-column grid of day buttons with a 4px row gap. role="group" named after the month (or a real role="grid" with rows when built with JS).' },
|
|
43
|
+
{ part: 'day', element: 'button', description: 'One day: 40×40 (36 on sm), body-sm, radius full. Attributes: aria-selected="true" (chosen), data-range="start|middle|end", data-today, data-outside (previous/next month), disabled.' },
|
|
44
|
+
{ part: 'footer', element: 'div', description: 'Top hairline; the typed date in a small Input with a calendar icon, then Cancel (outline) and Apply (primary), size sm.' },
|
|
45
|
+
],
|
|
46
|
+
props: {
|
|
47
|
+
variant: {
|
|
48
|
+
values: ['single', 'range', 'with-presets'],
|
|
49
|
+
default: 'single',
|
|
50
|
+
description: 'single = one date, one Apply; range = a start and an end day with the days between tinted, the footer input shows "Sep 8 – Sep 14, 2026"; with-presets = a range picker with a left column of shortcuts (Today … Last month) for filters by period.',
|
|
51
|
+
},
|
|
52
|
+
size: { values: ['sm', 'md'], default: 'md', description: 'md = 40px days, 328px panel (default); sm = 36px days, 300px panel for popovers inside dense toolbars.' },
|
|
53
|
+
},
|
|
54
|
+
states: {
|
|
55
|
+
dayHover: { selector: ' .cn-date-picker__day:hover:not(:disabled):not([aria-selected="true"])', description: 'On the __day: subtle fill under the pointer. Styled in extraCss because it lives on a child.', markup: 'native :hover on a day' },
|
|
56
|
+
dayFocus: { selector: ' .cn-date-picker__day:focus-visible', description: 'On the __day: the focus ring (roving tabindex, arrow keys move it). Styled in extraCss.', markup: 'native :focus-visible on a day' },
|
|
57
|
+
today: { selector: ' .cn-date-picker__day[data-today]', description: 'On the __day: semibold with a 4px action-colored dot under the number. Styled in extraCss.', markup: 'data-today and aria-current="date" on the day' },
|
|
58
|
+
selected: { selector: ' .cn-date-picker__day[aria-selected="true"]', description: 'On the __day: action fill, on-action text, medium weight. The start and end of a range are also selected. Styled in extraCss.', markup: 'aria-selected="true" on the day' },
|
|
59
|
+
range: { selector: ' .cn-date-picker__day[data-range]', description: 'On the __day: days inside a range take the subtle action tint and lose their radius; data-range="start" keeps the left radius, "end" the right one. Styled in extraCss.', markup: 'data-range="start|middle|end" on the day' },
|
|
60
|
+
outside: { selector: ' .cn-date-picker__day[data-outside]', description: 'On the __day: days of the previous/next month in fg-subtle. Styled in extraCss.', markup: 'data-outside on the day' },
|
|
61
|
+
dayDisabled: { selector: ' .cn-date-picker__day:disabled', description: 'On the __day: outside the allowed period, disabled ink, no hover. Styled in extraCss.', markup: 'disabled on the day' },
|
|
62
|
+
presetCurrent: { selector: ' .cn-date-picker__preset[aria-current="true"]', description: 'On the __preset: the active shortcut, subtle fill and medium weight. Styled in extraCss.', markup: 'aria-current="true" on the preset' },
|
|
63
|
+
},
|
|
64
|
+
base: {
|
|
65
|
+
root: {
|
|
66
|
+
display: 'inline-flex',
|
|
67
|
+
'flex-direction': 'row',
|
|
68
|
+
'align-items': 'stretch',
|
|
69
|
+
'max-width': '100%',
|
|
70
|
+
'background-color': '{color.bg-surface-raised}',
|
|
71
|
+
border: '{border.width.thin} solid {color.border-default}',
|
|
72
|
+
'border-radius': '{radius.panel}',
|
|
73
|
+
'box-shadow': '{shadow.lg}',
|
|
74
|
+
color: '{color.fg-default}',
|
|
75
|
+
overflow: 'hidden',
|
|
76
|
+
'z-index': '{z.popover}',
|
|
77
|
+
},
|
|
78
|
+
presets: {
|
|
79
|
+
display: 'flex',
|
|
80
|
+
'flex-direction': 'column',
|
|
81
|
+
gap: '{space.0.5}',
|
|
82
|
+
'flex-shrink': '0',
|
|
83
|
+
'min-width': '{space.40}',
|
|
84
|
+
padding: '{space.3}',
|
|
85
|
+
'border-inline-end': '{border.width.thin} solid {color.border-default}',
|
|
86
|
+
},
|
|
87
|
+
preset: {
|
|
88
|
+
...RESET_BUTTON,
|
|
89
|
+
display: 'flex',
|
|
90
|
+
'align-items': 'center',
|
|
91
|
+
height: '{space.9}',
|
|
92
|
+
'padding-inline': '{space.3}',
|
|
93
|
+
'border-radius': '{radius.md}',
|
|
94
|
+
...typeStyle('body-sm'),
|
|
95
|
+
'line-height': '{font.lineHeight.none}',
|
|
96
|
+
color: '{color.fg-muted}',
|
|
97
|
+
'text-align': 'start',
|
|
98
|
+
'white-space': 'nowrap',
|
|
99
|
+
...TRANSITION_COLORS,
|
|
100
|
+
},
|
|
101
|
+
calendar: { display: 'flex', 'flex-direction': 'column', 'min-width': '0', 'max-width': '100%', width: 'calc({space.10} * 7 + {space.6} * 2)' },
|
|
102
|
+
header: { display: 'flex', 'align-items': 'center', 'justify-content': 'space-between', gap: '{space.2}', padding: '{space.5} {space.6} 0' },
|
|
103
|
+
month: { ...typeStyle('label-md'), color: '{color.fg-default}' },
|
|
104
|
+
nav: {
|
|
105
|
+
...RESET_BUTTON,
|
|
106
|
+
display: 'inline-flex',
|
|
107
|
+
'align-items': 'center',
|
|
108
|
+
'justify-content': 'center',
|
|
109
|
+
'flex-shrink': '0',
|
|
110
|
+
width: '{space.8}',
|
|
111
|
+
height: '{space.8}',
|
|
112
|
+
'border-radius': '{radius.md}',
|
|
113
|
+
'font-size': '{size.icon.lg}',
|
|
114
|
+
color: '{color.fg-subtle}',
|
|
115
|
+
...TRANSITION_COLORS,
|
|
116
|
+
},
|
|
117
|
+
weekdays: { display: 'grid', 'grid-template-columns': 'repeat(7, minmax(0, 1fr))', 'margin-top': '{space.3}', 'padding-inline': '{space.6}', ...typeStyle('label-sm'), color: '{color.fg-muted}', 'text-align': 'center', 'line-height': '{space.10}' },
|
|
118
|
+
grid: { display: 'grid', 'grid-template-columns': 'repeat(7, minmax(0, 1fr))', 'row-gap': '{space.1}', padding: '0 {space.6} {space.5}' },
|
|
119
|
+
day: {
|
|
120
|
+
...RESET_BUTTON,
|
|
121
|
+
position: 'relative',
|
|
122
|
+
display: 'inline-flex',
|
|
123
|
+
'align-items': 'center',
|
|
124
|
+
'justify-content': 'center',
|
|
125
|
+
width: '100%',
|
|
126
|
+
height: '{space.10}',
|
|
127
|
+
'border-radius': '{radius.full}',
|
|
128
|
+
...typeStyle('body-sm'),
|
|
129
|
+
'line-height': '{font.lineHeight.none}',
|
|
130
|
+
color: '{color.fg-default}',
|
|
131
|
+
'font-variant-numeric': 'tabular-nums',
|
|
132
|
+
...TRANSITION_COLORS,
|
|
133
|
+
},
|
|
134
|
+
footer: { display: 'flex', 'flex-wrap': 'wrap', 'align-items': 'center', gap: '{space.3}', padding: '{space.4} {space.6}', 'border-top': '{border.width.thin} solid {color.border-default}' },
|
|
135
|
+
},
|
|
136
|
+
variants: {
|
|
137
|
+
variant: {
|
|
138
|
+
single: { root: {} },
|
|
139
|
+
range: { root: {} },
|
|
140
|
+
'with-presets': { root: {} },
|
|
141
|
+
},
|
|
142
|
+
size: {
|
|
143
|
+
sm: {
|
|
144
|
+
calendar: { width: `calc(${DAY.sm} * 7 + {space.6} * 2)` },
|
|
145
|
+
weekdays: { 'line-height': DAY.sm },
|
|
146
|
+
day: { height: DAY.sm },
|
|
147
|
+
header: { padding: '{space.4} {space.6} 0' },
|
|
148
|
+
},
|
|
149
|
+
md: { day: { height: DAY.md } },
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
extraCss: `
|
|
153
|
+
.cn-date-picker .cn-date-picker__day:hover:not(:disabled):not([aria-selected="true"]) { background-color: {color.bg-subtle}; }
|
|
154
|
+
.cn-date-picker .cn-date-picker__day:focus-visible { outline: none; box-shadow: {shadow.focus}; z-index: {z.raised}; }
|
|
155
|
+
.cn-date-picker .cn-date-picker__day[data-today] { font-weight: {font.weight.semibold}; }
|
|
156
|
+
.cn-date-picker .cn-date-picker__day[data-today]::after { content: ''; position: absolute; bottom: 4px; left: 50%; width: 4px; height: 4px; margin-left: -2px; border-radius: {radius.full}; background-color: {color.bg-action}; }
|
|
157
|
+
.cn-date-picker .cn-date-picker__day[data-range] { border-radius: 0; background-color: {color.bg-action-subtle}; font-weight: {font.weight.medium}; }
|
|
158
|
+
.cn-date-picker .cn-date-picker__day[data-range="start"] { border-radius: {radius.full} 0 0 {radius.full}; }
|
|
159
|
+
.cn-date-picker .cn-date-picker__day[data-range="end"] { border-radius: 0 {radius.full} {radius.full} 0; }
|
|
160
|
+
.cn-date-picker .cn-date-picker__day[aria-selected="true"] { background-color: {color.bg-action}; color: {color.fg-on-action}; font-weight: {font.weight.medium}; }
|
|
161
|
+
.cn-date-picker .cn-date-picker__day[aria-selected="true"]::after { background-color: {color.fg-on-action}; }
|
|
162
|
+
.cn-date-picker .cn-date-picker__day[data-outside] { color: {color.fg-subtle}; }
|
|
163
|
+
.cn-date-picker .cn-date-picker__day:disabled { color: {color.fg-disabled}; cursor: not-allowed; background-color: transparent; }
|
|
164
|
+
.cn-date-picker .cn-date-picker__nav:hover { background-color: {color.bg-subtle}; color: {color.fg-default}; }
|
|
165
|
+
.cn-date-picker .cn-date-picker__nav:focus-visible { outline: none; box-shadow: {shadow.focus}; }
|
|
166
|
+
.cn-date-picker .cn-date-picker__preset:hover { background-color: {color.bg-subtle}; color: {color.fg-default}; }
|
|
167
|
+
.cn-date-picker .cn-date-picker__preset[aria-current="true"] { background-color: {color.bg-subtle}; color: {color.fg-default}; font-weight: {font.weight.medium}; }
|
|
168
|
+
.cn-date-picker .cn-date-picker__preset:focus-visible { outline: none; box-shadow: {shadow.focus}; }
|
|
169
|
+
.cn-date-picker .cn-date-picker__footer > .cn-input { flex: 1 1 100%; min-width: 0; }
|
|
170
|
+
.cn-date-picker .cn-date-picker__footer > .cn-button { flex: 1 1 0; }
|
|
171
|
+
@media (max-width: {breakpoint.sm}) {
|
|
172
|
+
.cn-date-picker { flex-direction: column; }
|
|
173
|
+
.cn-date-picker .cn-date-picker__presets { flex-direction: row; flex-wrap: wrap; min-width: 0; max-width: calc({space.10} * 7 + {space.6} * 2); border-inline-end: 0; border-bottom: {border.width.thin} solid {color.border-default}; }
|
|
174
|
+
.cn-date-picker .cn-date-picker__header, .cn-date-picker .cn-date-picker__weekdays, .cn-date-picker .cn-date-picker__grid, .cn-date-picker .cn-date-picker__footer { padding-inline: {space.4}; }
|
|
175
|
+
}`,
|
|
176
|
+
examples: [
|
|
177
|
+
ex('Single date', panel('data-variant="single" data-size="md"', calendar(single(18), 'Sep 18, 2026', 'Selected date')), 'September 2026: the 18th is selected, today (the 11th) carries the dot, Aug 31 and Oct 1–4 are outside days.'),
|
|
178
|
+
ex('Range 8–14', panel('data-variant="range" data-size="md"', calendar(range(8, 14), 'Sep 8 – Sep 14, 2026', 'Selected range')), 'Start and end are filled; the days between take the subtle tint with no radius. Today falls inside the range and keeps its dot.'),
|
|
179
|
+
ex('With presets', panel('data-variant="with-presets" data-size="md"', presets('Last week') + calendar(range(8, 14), 'Sep 8 – Sep 14, 2026', 'Selected range')), 'A shortcut column on the left; "Last week" is the active preset and drives the range.'),
|
|
180
|
+
ex('Small', panel('data-variant="single" data-size="sm"', calendar(single(11), 'Sep 11, 2026', 'Selected date')), '36px days, 300px panel, for popovers inside dense toolbars.'),
|
|
181
|
+
ex('Date input (trigger)', `<div style="display:flex;flex-wrap:wrap;gap:var(--cn-space-3);align-items:center"><div class="cn-input" data-variant="default" data-size="md" style="max-width:220px">${ICON.calendar.replace('cn-icon', 'cn-input__icon')}<input class="cn-input__field" type="text" value="Sep 11, 2026" aria-label="Due date" aria-haspopup="dialog"></div><button type="button" class="cn-button" data-variant="outline" data-size="md" aria-haspopup="dialog">${ICON.calendar.replace('cn-icon', 'cn-button__icon')}<span class="cn-button__label">Sep 8 – Sep 14, 2026</span></button></div>`, 'The panel opens from an Input with a leading calendar icon (forms) or from an outline Button (filters).'),
|
|
182
|
+
ex('Disabled days', panel('data-variant="single" data-size="md"', calendar(september((n) => ({ selected: n === 15, disabled: n < 11 })), 'Sep 15, 2026', 'Delivery date')), 'Days before today are disabled: no hover, disabled ink, still visible so the month reads whole.'),
|
|
183
|
+
],
|
|
184
|
+
rules: [
|
|
185
|
+
'Weeks start on Monday and every month renders 6 rows at most, padded with outside days (data-outside) so the panel height does not jump between months.',
|
|
186
|
+
'Today always carries the dot (data-today + aria-current="date"), even when selected or inside a range.',
|
|
187
|
+
'A range is start + end with aria-selected="true" and data-range="start|end"; the days between get data-range="middle". The footer input shows the range as "Sep 8 – Sep 14, 2026".',
|
|
188
|
+
'The footer is mandatory: the typed date in a small Input (with a leading calendar icon), Cancel (outline) and Apply (primary), size sm. Selection is committed by Apply, not by clicking a day.',
|
|
189
|
+
'Presets are for filters by period, not for forms; the list is fixed (Today, Yesterday, This week, Last week, This month, Last month) and the active one has aria-current="true".',
|
|
190
|
+
'Dates in the input and in triggers use the "Sep 11, 2026" format; never numeric-only dates that differ by locale.',
|
|
191
|
+
'The panel is statically positioned by the component; the app anchors it under its trigger with an 8px offset (Popover rules) and closes it on Escape and outside click.',
|
|
192
|
+
'Sizes: md by default; sm only inside dense toolbars. Do not scale the panel by other means.',
|
|
193
|
+
],
|
|
194
|
+
a11y: [
|
|
195
|
+
'The panel is role="dialog" with an aria-label; focus moves into it on open (to the selected day or today) and returns to the trigger on close.',
|
|
196
|
+
'Days use a roving tabindex: one day is tabbable, Arrow keys move by day and week, Home/End jump to the row ends, PageUp/PageDown change the month.',
|
|
197
|
+
'Each day button needs a full accessible name ("Tuesday, September 8, 2026") via aria-label; the visible number alone is ambiguous.',
|
|
198
|
+
'The month name has aria-live="polite" so navigating months is announced; the nav buttons have aria-labels.',
|
|
199
|
+
'Disabled days stay in the DOM with disabled; explain the constraint near the trigger ("Deliveries start tomorrow").',
|
|
200
|
+
],
|
|
201
|
+
related: ['input', 'button', 'popover', 'field', 'input-group'],
|
|
202
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { ex, typeStyle } from "./_shared.mjs";
|
|
2
|
+
// key/value pairs in the reference's detail rhythm: 14px medium gray-600 terms on a 160px column,
|
|
3
|
+
// 14px ink values, gray-200 hairlines with 12px of padding, or stacked pairs 4px apart.
|
|
4
|
+
const HAIRLINE = '{border.width.thin} solid {color.border-default}';
|
|
5
|
+
const pair = (term, detail) => `<div class="cn-description-list__item"><dt class="cn-description-list__term">${term}</dt><dd class="cn-description-list__detail">${detail}</dd></div>`;
|
|
6
|
+
const WORKSPACE = [
|
|
7
|
+
pair('Workspace', 'Lumen'),
|
|
8
|
+
pair('Workspace ID', '<code class="cn-text-code-sm">ws_8f2a91c4</code>'),
|
|
9
|
+
pair('Owner', 'Maya Chen'),
|
|
10
|
+
pair('Plan', '<span class="cn-badge" data-tone="accent" data-variant="soft" data-size="sm">Business</span>'),
|
|
11
|
+
pair('Created', 'March 12, 2024'),
|
|
12
|
+
].join('');
|
|
13
|
+
const dl = (layout, size, divided, body, style = 'width:100%;max-width:560px') => `<dl class="cn-description-list" data-layout="${layout}" data-size="${size}" data-divided="${divided}" style="${style}">${body}</dl>`;
|
|
14
|
+
export const descriptionList = {
|
|
15
|
+
name: 'DescriptionList',
|
|
16
|
+
slug: 'description-list',
|
|
17
|
+
category: 'data-display',
|
|
18
|
+
description: 'Key/value pairs: a 14px medium muted term and its ink value, side by side on a 160px column or stacked, divided by gray-200 hairlines. The quiet way to show the facts of one record.',
|
|
19
|
+
usage: 'Details of one entity (a workspace, an invoice header, a customer profile) in drawers, cards and detail pages. For many records use Table; for a list of things use List.',
|
|
20
|
+
anatomy: [
|
|
21
|
+
{ part: 'root', element: 'dl', description: 'The <dl>. Column of items; the layout prop decides how each pair sits.' },
|
|
22
|
+
{ part: 'item', element: 'div', description: 'One pair. A <div> wrapping the dt and dd so they can be laid out as a row.' },
|
|
23
|
+
{ part: 'term', element: 'dt', description: 'The key. 14px medium fg-muted, sentence case, no colon.' },
|
|
24
|
+
{ part: 'detail', element: 'dd', description: 'The value. 14px in ink; may hold a Badge, a code value, a link or an Avatar with a name.' },
|
|
25
|
+
],
|
|
26
|
+
props: {
|
|
27
|
+
layout: {
|
|
28
|
+
values: ['horizontal', 'vertical'],
|
|
29
|
+
default: 'horizontal',
|
|
30
|
+
description: 'horizontal = term on a 160px column, value beside it (drawers, detail pages, ≥ 360px wide); vertical = term above the value, 4px apart (narrow columns, card grids, mobile).',
|
|
31
|
+
},
|
|
32
|
+
size: { values: ['sm', 'md'], default: 'md', description: 'md = 14px term and value (default); sm = 12px, for dense cards and summaries.' },
|
|
33
|
+
divided: { values: ['yes', 'no'], default: 'yes', description: 'yes = a gray-200 hairline between pairs with 12px of vertical padding (scannable lists of 4+ facts); no = pairs separated by an 8px gap only (2–3 facts inside a card).' },
|
|
34
|
+
},
|
|
35
|
+
states: {},
|
|
36
|
+
base: {
|
|
37
|
+
root: { display: 'flex', 'flex-direction': 'column', margin: '0', 'min-width': '0', color: '{color.fg-default}' },
|
|
38
|
+
item: { display: 'grid', 'min-width': '0' },
|
|
39
|
+
term: { ...typeStyle('label-sm'), color: '{color.fg-muted}', 'min-width': '0' },
|
|
40
|
+
detail: { ...typeStyle('body-md'), color: '{color.fg-default}', margin: '0', 'min-width': '0', 'overflow-wrap': 'anywhere' },
|
|
41
|
+
},
|
|
42
|
+
variants: {
|
|
43
|
+
layout: {
|
|
44
|
+
horizontal: { item: { 'grid-template-columns': '{space.40} minmax(0, 1fr)', 'column-gap': '{space.4}', 'align-items': 'baseline' } },
|
|
45
|
+
vertical: { item: { 'grid-template-columns': 'minmax(0, 1fr)', 'row-gap': '{space.1}' } },
|
|
46
|
+
},
|
|
47
|
+
size: {
|
|
48
|
+
sm: { term: { 'font-size': '{font.size.xs}' }, detail: { 'font-size': '{font.size.xs}' } },
|
|
49
|
+
md: { term: { 'font-size': '{font.size.sm}' }, detail: { 'font-size': '{font.size.sm}' } },
|
|
50
|
+
},
|
|
51
|
+
divided: {
|
|
52
|
+
yes: { root: { gap: '0' }, item: { 'padding-block': '{space.3}' } },
|
|
53
|
+
no: { root: { gap: '{space.2}' }, item: { 'padding-block': '0' } },
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
compound: [
|
|
57
|
+
{ when: { layout: 'horizontal', size: 'sm' }, block: { item: { 'grid-template-columns': '{space.32} minmax(0, 1fr)' } } },
|
|
58
|
+
{ when: { divided: 'yes', size: 'sm' }, block: { item: { 'padding-block': '{space.2}' } } },
|
|
59
|
+
{ when: { layout: 'vertical', divided: 'no' }, block: { root: { gap: '{space.4}' } } },
|
|
60
|
+
],
|
|
61
|
+
extraCss: `
|
|
62
|
+
.cn-description-list[data-divided="yes"] > .cn-description-list__item + .cn-description-list__item { border-top: ${HAIRLINE}; }
|
|
63
|
+
.cn-description-list[data-divided="yes"] > .cn-description-list__item:first-child { padding-top: 0; }
|
|
64
|
+
.cn-description-list[data-divided="yes"] > .cn-description-list__item:last-child { padding-bottom: 0; }
|
|
65
|
+
.cn-description-list__detail > .cn-avatar-label-group { display: inline-flex; }`,
|
|
66
|
+
examples: [
|
|
67
|
+
ex('Horizontal, divided', dl('horizontal', 'md', 'yes', WORKSPACE), '160px term column, 14px medium muted terms, ink values, hairlines with 12px of padding.'),
|
|
68
|
+
ex('Vertical (narrow columns)', dl('vertical', 'md', 'yes', WORKSPACE, 'width:100%;max-width:280px'), 'Term above the value, 4px apart.'),
|
|
69
|
+
ex('Compact inside a card', `<section class="cn-card" data-variant="default" data-padding="sm" style="width:100%;max-width:360px"><div class="cn-card__body">${dl('horizontal', 'sm', 'no', [pair('Plan', 'Business · $2,900 / month'), pair('Seats', '12 of 15 used'), pair('Renews', 'October 1, 2026')].join(''), '')}</div></section>`, 'size sm, no dividers: three facts as a quiet block.'),
|
|
70
|
+
ex('Values with components', dl('horizontal', 'md', 'yes', [pair('Owner', '<span class="cn-avatar-label-group" data-size="sm"><span class="cn-avatar" data-size="xs" data-shape="circle" data-tone="neutral" data-status="none" role="img" aria-label="Maya Chen"><span class="cn-avatar__fallback" aria-hidden="true">MC</span></span><span class="cn-avatar-label-group__text"><span class="cn-avatar-label-group__name">Maya Chen</span></span></span>'), pair('Status', '<span class="cn-badge" data-tone="success" data-variant="soft" data-size="sm"><span class="cn-badge__dot"></span>Active</span>'), pair('Website', '<a href="#" class="cn-link">lumen.co</a>'), pair('Notes', 'Prefers email over Slack. Decision maker is the head of product, not the CEO.')].join(''))),
|
|
71
|
+
],
|
|
72
|
+
rules: [
|
|
73
|
+
'Terms are sentence-case nouns with no trailing colon ("Workspace ID", not "Workspace ID:"), 14px medium muted.',
|
|
74
|
+
'Keep terms short (1–3 words) so they fit the 160px column; if a term needs a sentence, it is not a key/value pair.',
|
|
75
|
+
'Values are facts in ink, not paragraphs. A value longer than two lines belongs in a section of its own.',
|
|
76
|
+
'Empty values show an em dash ("—") in fg-subtle, never a blank cell and never the word "null" or "N/A".',
|
|
77
|
+
'Order pairs by how people look things up (identity first, then status, then dates), not by the database schema.',
|
|
78
|
+
'Use horizontal at ≥ 360px of width; below that, or in card grids, switch to vertical.',
|
|
79
|
+
'divided="yes" for 4 or more pairs; "no" for 2–3 pairs inside a card that already has a frame.',
|
|
80
|
+
'Values may hold one component (Badge sm, avatar label group, code, Link) but not buttons; actions live in the surface\'s header.',
|
|
81
|
+
],
|
|
82
|
+
a11y: [
|
|
83
|
+
'Use the real <dl>/<dt>/<dd> elements; the wrapping <div> per pair is valid HTML and keeps the term/detail association.',
|
|
84
|
+
'Do not put interactive controls in dd besides links; a control needs a labelled form field.',
|
|
85
|
+
'Abbreviated terms (ID, VAT) get an <abbr title> or the expansion in the value on first use.',
|
|
86
|
+
],
|
|
87
|
+
related: ['table', 'list', 'card', 'badge', 'stat', 'avatar'],
|
|
88
|
+
};
|