Skip to content

Commit 6f09654

Browse files
DavidBabinecclaude
andauthored
feat(framework): import Core Framework defaults from onboarding (CoreBunch#91)
* feat(framework): import Core Framework defaults from onboarding Add a Core Framework default-preset importer and wire it into the dashboard onboarding "framework" step. Engine (src/core/framework/coreFrameworkPreset.ts): - buildCoreFrameworkColorSettings() maps the Core Framework default color system (6 groups / 13 tokens — Brand, Background, Text, Base, Neutral, Status) into FrameworkColorToken[]: hsla values, dark-mode pairs, transparent steps, and per-token utility kinds from each token's `gen` list. Shades/tints are generated from a count (schema-native). - buildCoreFrameworkSettings({ includeUtilities }) returns a full FrameworkSettings (colors + typography + spacing + Core Framework preferences). Typography/spacing reuse the existing CF-mirroring defaults. Two import modes: - Full framework — every generated utility class plus :root variables; tree-shaking off so the whole utility set ships in framework.css. - Variables only — the same :root variables (base, shades, tints, transparent steps, scale clamps) with color utilities off and the typography/spacing class generators dropped. Onboarding UI: - FrameworkImportModal presents the two modes as role="radio" cards in the shared Dialog, loads the site, drops the built settings onto settings.framework, and saves the shell only (pages untouched). - The dashboard onboarding "framework" step opens the modal; the panel owns the modal so DashboardPage stays at its size budget. - useOnboardingState now exposes refresh() so the step flips to done after import. Adds FrameworkSettings to the framework schema (source of truth) and a §8.14 button-primitive allowlist entry for the radio cards. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(dashboard): refine framework-import card states and typography - Card backgrounds use overlay tints: idle --overlay-5, hover --overlay-10, active --overlay-20 (the previous active state relied on a box-shadow token used as a color, which rendered nothing). - Bigger, bolder, brighter text: titles --text-xl/700, descriptions --text-m/500 in --text, weight-500 lede. - Cleaner bullet list: mint accent checkmark icons instead of grey dots. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent a125a4a commit 6f09654

10 files changed

Lines changed: 734 additions & 9 deletions

File tree

src/__tests__/architecture/button-primitive-usage.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,14 @@ const ALLOWLIST = new Set([
196196
// • the "Select all" / "Select none" bulk text links — 11.5px inline text
197197
// actions, not the token-driven Button sizes (same as §8.12).
198198
'admin/pages/data/components/ExportDialog/ExportDialog.tsx',
199+
200+
// ── §8.14 Framework import mode cards ────────────────────────────────────
201+
// FrameworkImportModal's two import modes ("Full framework" / "Variables
202+
// only") render as role="radio" cards inside a role="radiogroup" — each is a
203+
// stacked custom layout (icon + title + tick + description + bullet list)
204+
// that Button's inline-flex size tokens cannot represent. Same pattern class
205+
// as §8.7's role="option" card grids (custom ARIA role + multi-line card).
206+
'admin/pages/dashboard/components/FrameworkImportModal.tsx',
199207
])
200208

201209
// ---------------------------------------------------------------------------
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { describe, expect, it } from 'bun:test'
2+
import {
3+
buildCoreFrameworkColorSettings,
4+
buildCoreFrameworkSettings,
5+
generateFrameworkRootCss,
6+
generateFrameworkUtilityClasses,
7+
} from '@core/framework'
8+
9+
describe('Core Framework preset — color mapping', () => {
10+
it('maps the 13 default tokens across the 6 default groups', () => {
11+
const { tokens } = buildCoreFrameworkColorSettings({ includeUtilities: true })
12+
expect(tokens).toHaveLength(13)
13+
14+
const categories = [...new Set(tokens.map((t) => t.category))]
15+
expect(categories).toEqual(['Brand', 'Background', 'Text', 'Base', 'Neutral', 'Status'])
16+
17+
const slugs = tokens.map((t) => t.slug)
18+
expect(slugs).toContain('primary')
19+
expect(slugs).toContain('bg-surface')
20+
expect(slugs).toContain('shadow-primary')
21+
expect(slugs).toContain('success')
22+
})
23+
24+
it('carries the Core Framework hsla values and dark-mode pairs', () => {
25+
const { tokens } = buildCoreFrameworkColorSettings({ includeUtilities: true })
26+
const primary = tokens.find((t) => t.slug === 'primary')!
27+
expect(primary.lightValue).toBe('hsla(238, 100%, 62%, 1)')
28+
expect(primary.darkModeEnabled).toBe(false)
29+
30+
const bgBody = tokens.find((t) => t.slug === 'bg-body')!
31+
expect(bgBody.lightValue).toBe('hsla(0, 0%, 90%, 1)')
32+
expect(bgBody.darkValue).toBe('hsla(0, 0%, 5%, 1)')
33+
expect(bgBody.darkModeEnabled).toBe(true)
34+
})
35+
36+
it('enables brand shades/tints from a count and disables them elsewhere', () => {
37+
const { tokens } = buildCoreFrameworkColorSettings({ includeUtilities: true })
38+
const primary = tokens.find((t) => t.slug === 'primary')!
39+
expect(primary.generateShades).toEqual({ enabled: true, count: 4 })
40+
expect(primary.generateTints).toEqual({ enabled: true, count: 4 })
41+
expect(primary.generateTransparent).toBe(true)
42+
43+
const neutralLight = tokens.find((t) => t.slug === 'light')!
44+
expect(neutralLight.generateShades.enabled).toBe(false)
45+
expect(neutralLight.generateTints.enabled).toBe(false)
46+
expect(neutralLight.generateTransparent).toBe(true)
47+
})
48+
})
49+
50+
describe('Core Framework preset — full vs variables-only', () => {
51+
it('full import enables utility kinds from the Core Framework gen list', () => {
52+
const { tokens } = buildCoreFrameworkColorSettings({ includeUtilities: true })
53+
const primary = tokens.find((t) => t.slug === 'primary')!
54+
expect(primary.generateUtilities).toEqual({
55+
text: true,
56+
background: true,
57+
border: true,
58+
fill: false,
59+
})
60+
// shadow-primary has no `gen` — never a utility class.
61+
const shadow = tokens.find((t) => t.slug === 'shadow-primary')!
62+
expect(shadow.generateUtilities).toEqual({
63+
text: false,
64+
background: false,
65+
border: false,
66+
fill: false,
67+
})
68+
})
69+
70+
it('variables-only import turns every color utility kind off', () => {
71+
const { tokens } = buildCoreFrameworkColorSettings({ includeUtilities: false })
72+
for (const token of tokens) {
73+
expect(token.generateUtilities).toEqual({
74+
text: false,
75+
background: false,
76+
border: false,
77+
fill: false,
78+
})
79+
}
80+
})
81+
82+
it('full import ships class generators and disables tree-shaking', () => {
83+
const settings = buildCoreFrameworkSettings({ includeUtilities: true })
84+
expect(settings.typography?.classes?.length).toBeGreaterThan(0)
85+
expect(settings.spacing?.classes?.length).toBeGreaterThan(0)
86+
expect(settings.preferences?.treeShakeGeneratedFrameworkUtilities).toBe(false)
87+
})
88+
89+
it('variables-only import drops the scale class generators but keeps groups', () => {
90+
const settings = buildCoreFrameworkSettings({ includeUtilities: false })
91+
expect(settings.typography?.groups.length).toBeGreaterThan(0)
92+
expect(settings.spacing?.groups.length).toBeGreaterThan(0)
93+
expect(settings.typography?.classes).toEqual([])
94+
expect(settings.spacing?.classes).toEqual([])
95+
})
96+
})
97+
98+
describe('Core Framework preset — generated CSS', () => {
99+
it('full import emits both :root variables and utility classes', () => {
100+
const settings = buildCoreFrameworkSettings({ includeUtilities: true })
101+
const rootCss = generateFrameworkRootCss(settings)
102+
const classes = generateFrameworkUtilityClasses(settings)
103+
104+
expect(rootCss).toContain('--primary: hsla(238, 100%, 62%, 1);')
105+
// Scale variables emit too.
106+
expect(rootCss).toContain('--text-m:')
107+
expect(rootCss).toContain('--space-m:')
108+
109+
const classNames = Object.values(classes).map((rule) => rule.name)
110+
expect(classNames).toContain('bg-primary')
111+
expect(classNames).toContain('text-primary')
112+
})
113+
114+
it('variables-only import emits :root variables but no utility classes', () => {
115+
const settings = buildCoreFrameworkSettings({ includeUtilities: false })
116+
const rootCss = generateFrameworkRootCss(settings)
117+
const classes = generateFrameworkUtilityClasses(settings)
118+
119+
// Base + variant variables still present.
120+
expect(rootCss).toContain('--primary: hsla(238, 100%, 62%, 1);')
121+
expect(rootCss).toContain('--primary-d-1:')
122+
expect(rootCss).toContain('--text-m:')
123+
124+
expect(Object.keys(classes)).toHaveLength(0)
125+
})
126+
})

src/admin/pages/dashboard/DashboardPage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ export function DashboardPage() {
130130
const currentUser = useAuthenticatedAdminUser()
131131
const navigate = useAdminNavigate()
132132
const widgets = useDashboardWidgets()
133-
const facts = useOnboardingState()
133+
const { facts, refresh: refreshOnboarding } = useOnboardingState()
134134
const layoutApi = useDashboardLayout()
135135
const {
136136
layout,
@@ -518,7 +518,7 @@ export function DashboardPage() {
518518
</div>
519519

520520
{mounted && showOnboarding && (
521-
<OnboardingPanel facts={facts} onDismiss={dismissOnboarding} />
521+
<OnboardingPanel facts={facts} onDismiss={dismissOnboarding} onFrameworkImported={refreshOnboarding} />
522522
)}
523523

524524
<div className={styles.gridHeader}>
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/**
2+
* FrameworkImportModal — two selectable mode cards inside the shared Dialog.
3+
*
4+
* Cards follow the borderless tile pattern using overlay tints: idle
5+
* `--overlay-5`, hover `--overlay-10`, active `--overlay-20`, plus a mint tick
6+
* on the selected card — no ring, no recolored border. Font sizes use the
7+
* global fluid text scale; spacing uses the global space scale (admin token
8+
* policy).
9+
*/
10+
11+
.lede {
12+
margin: 0 0 var(--space-4xl);
13+
font-size: var(--text-m);
14+
font-weight: 500;
15+
line-height: 1.5;
16+
color: var(--text-muted);
17+
}
18+
19+
.options {
20+
display: grid;
21+
grid-template-columns: 1fr 1fr;
22+
gap: var(--space-xl);
23+
}
24+
25+
.option {
26+
display: flex;
27+
flex-direction: column;
28+
gap: var(--space-m);
29+
padding: var(--space-3xl);
30+
text-align: left;
31+
background: var(--overlay-5);
32+
border: 0;
33+
border-radius: var(--panel-radius);
34+
cursor: pointer;
35+
transition:
36+
background 120ms ease,
37+
box-shadow 120ms ease;
38+
}
39+
40+
.option:hover {
41+
background: var(--overlay-10);
42+
}
43+
44+
.option:disabled {
45+
cursor: default;
46+
opacity: 0.6;
47+
}
48+
49+
.optionSelected {
50+
background: var(--overlay-20);
51+
}
52+
53+
.optionSelected:hover {
54+
background: var(--overlay-20);
55+
}
56+
57+
.optionHead {
58+
display: flex;
59+
align-items: center;
60+
gap: var(--space-m);
61+
}
62+
63+
.optionIcon {
64+
display: inline-flex;
65+
align-items: center;
66+
justify-content: center;
67+
width: 28px;
68+
height: 28px;
69+
border-radius: var(--radius);
70+
background: var(--bg-surface);
71+
color: var(--text);
72+
}
73+
74+
.optionTitle {
75+
font-size: var(--text-xl);
76+
font-weight: 700;
77+
color: var(--text-bright);
78+
}
79+
80+
.optionTick {
81+
display: inline-flex;
82+
align-items: center;
83+
justify-content: center;
84+
margin-left: auto;
85+
width: 18px;
86+
height: 18px;
87+
border-radius: 50%;
88+
background: var(--accent-1);
89+
color: var(--bg-body);
90+
}
91+
92+
.optionDesc {
93+
font-size: var(--text-m);
94+
font-weight: 500;
95+
line-height: 1.5;
96+
color: var(--text);
97+
}
98+
99+
.optionBullets {
100+
margin: var(--space-2xs) 0 0;
101+
padding: 0;
102+
list-style: none;
103+
display: flex;
104+
flex-direction: column;
105+
gap: var(--space-xs);
106+
}
107+
108+
.optionBullets li {
109+
display: flex;
110+
align-items: center;
111+
gap: var(--space-s);
112+
font-size: var(--text-s);
113+
font-weight: 500;
114+
line-height: 1.4;
115+
color: var(--text);
116+
}
117+
118+
.bulletIcon {
119+
display: inline-flex;
120+
flex-shrink: 0;
121+
align-items: center;
122+
justify-content: center;
123+
color: var(--accent-1);
124+
}
125+
126+
.note {
127+
margin: var(--space-3xl) 0 0;
128+
padding: var(--space-m) var(--space-l);
129+
font-size: var(--text-s);
130+
line-height: 1.45;
131+
color: var(--info-text);
132+
background: var(--bg-surface-2);
133+
border-radius: var(--radius);
134+
}
135+
136+
.error {
137+
margin: var(--space-xl) 0 0;
138+
font-size: var(--text-s);
139+
line-height: 1.45;
140+
color: var(--danger-text);
141+
}
142+
143+
@media (max-width: 560px) {
144+
.options {
145+
grid-template-columns: 1fr;
146+
}
147+
}

0 commit comments

Comments
 (0)