Skip to content

Commit aa80ace

Browse files
committed
feat(core): adopt attribute grammar for root and surface contracts
1 parent fc32783 commit aa80ace

File tree

17 files changed

+141
-121
lines changed

17 files changed

+141
-121
lines changed

apps/storybook/.storybook/preview.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ const preview: Preview = {
5252
${storyHtml}
5353
</div>
5454
`,
55-
surface: context.globals.surface,
55+
variant: context.globals.surface,
5656
})}
5757
`,
5858
dir: context.globals.direction,

docs/CONSTRAINTS.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ Resolver adapter details live in `packages/system/scripts/README.md`.
7171
## Components Package Constraints (Alpha)
7272

7373
- `packages/core/` is the canonical package boundary for component contracts built on token outputs.
74+
- Component markup grammar (SSR-first):
75+
- structural semantics use classes (function-focused nouns, not appearance naming).
76+
- state/variant semantics use attributes (for example `data-variant`, `data-size`, ARIA state attrs).
77+
- public renderer props should map directly to emitted DOM semantics (props -> attributes).
7478
- Component implementation model:
7579
- components that do not require runtime JS behavior should be authored as pure SSR renderers that emit native HTML.
7680
- components that do require runtime JS behavior may be authored as web-components.
@@ -183,15 +187,15 @@ Resolver adapter details live in `packages/system/scripts/README.md`.
183187
- CSS layering contract (`@layer clbr, clbr.brand, clbr.root, clbr.components;`) is normative and must be preserved in distributed bundles.
184188
- Root scoping contract:
185189
- all token usage must live under a `.clbr` scope root
186-
- select a brand via `.clbr-brand-<brand>` on the same scope root
190+
- select a brand via `data-brand="<brand>"` on the same scope root
187191
- Theme forcing contract:
188-
- `.clbr-theme-dark` and `.clbr-theme-light` force mode on the scoped brand root
189-
- without force classes, theme follows authored media query behavior
192+
- `data-theme="dark"` and `data-theme="light"` force mode on the scoped brand root
193+
- without force attributes, theme follows authored media query behavior
190194
- Surface contract:
191-
- `.clbr-surface-brand` is a descendant surface scope inside a brand root
195+
- `.surface[data-variant="brand"]` is a descendant surface scope inside a brand root
192196
- Multi-brand on one page is supported:
193-
- use isolated wrapper roots per brand (`.clbr.clbr-brand-msrd`, `.clbr.clbr-brand-wrfr`, etc.)
194-
- do not mix multiple brand classes on the same scope root
197+
- use isolated wrapper roots per brand (`.clbr[data-brand="msrd"]`, `.clbr[data-brand="wrfr"]`, etc.)
198+
- do not mix multiple brands on the same scope root
195199

196200
## Domain Conventions
197201

docs/PLANNING.md

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,15 @@ What we could working on next.
2727

2828
Everything we could attempt given sufficient time and resources.
2929

30-
### Deterministic sorting (linting)
30+
### Tokens evolution
31+
32+
#### Deterministic sorting (linting)
3133

3234
- JS import/export ordering via ESLint autofix
3335
- JSON key-order enforcement for selected token paths (including top-key conventions like `$schema` / `$type` / `$description` / `default`)
3436
- Alphabetical sorting
3537

36-
### Machine-readable intent
38+
#### Machine-readable intent
3739

3840
- Expand token/group `$description` coverage for intent guidance:
3941
- usage guidance for humans/agents
@@ -43,14 +45,14 @@ Everything we could attempt given sufficient time and resources.
4345
- stable fields for tooling/agents beyond prose descriptions
4446
- worked examples where intent is easy to misuse
4547

46-
### Design model evolution
48+
#### Design model evolution
4749

4850
- Light/dark inverse surfaces
4951
- `density` context (class-based in CSS) — current size context grid/spacing is broadly editorial/comfortable in nature, this may be fine, but may want to add a ui/compact mode
5052
- Border and Transition DTCG Composites
5153
- Give wireframe theme an actual design
5254

53-
### Style Dictionary DTCG 2025.10 gaps
55+
#### Style Dictionary DTCG 2025.10 gaps
5456

5557
[Support for DTCG v2025.10](https://github.com/style-dictionary/style-dictionary/issues/1590)
5658

@@ -59,45 +61,47 @@ Everything we could attempt given sufficient time and resources.
5961
- Revisit resolver bridge scope once Style Dictionary lands native DTCG resolver support:
6062
- reduce/remove custom resolver->SD source adaptation where SD can natively consume resolver semantics
6163

62-
### Export target evolution
64+
#### JSON export target
65+
66+
Define a stable JSON artifact contract for downstream consumers (including docs) so metadata and token data can be consumed without coupling to internal bridge/build intermediates. Likely `packages/tokens` / `@measured/calibrate-tokens`.
67+
68+
Note: pipeline is currently hard-coded to CSS; probably add optional `--formats` in `packages/system/scripts/pipeline/index.mjs` when implementing a second export target.
69+
70+
#### Export target evolution
6371

6472
1. Penpot
6573
1. Figma
6674
1. VS Code token lookup artifact
6775
1. iOS
6876
1. Android
6977

70-
Note: pipeline is currently hard-coded to CSS; probably add optional `--formats` in `packages/system/scripts/pipeline/index.mjs` when implementing a second export target.
78+
### Further evolution
7179

72-
### Automated checks
80+
#### Assets package
7381

74-
Stylelint/ESLint/axe; token-name lint rules; forbid raw hex/px; PR templates require a11y notes, screenshots, and before/after diffs.
82+
Decide whether shared fonts/images should ship as a dedicated package and define what is stable asset API vs implementation detail.
7583

76-
### JSON export target
84+
#### Shareable automated checks
7785

78-
Define a stable JSON artifact contract for downstream consumers (including docs) so metadata and token data can be consumed without coupling to internal bridge/build intermediates.
86+
Stylelint/ESLint/axe configs; token-name lint rules; forbid raw hex/px; PR templates require a11y notes, screenshots, and before/after diffs.
7987

80-
### Minimal viable publish
88+
#### Minimal viable publish
8189

8290
Define the minimum scripts, workflow, and release notes needed to publish initial alpha packages and unblock downstream adoption tasks.
8391

84-
### Documentation website (`apps/documentation`)
85-
86-
Stand up a docs site that consumes published token/component packages and serves as the canonical reference for usage, contracts, and examples.
92+
#### Documentation website (`apps/documentation`)
8793

88-
### Assets package
89-
90-
Decide whether shared fonts/images should ship as a dedicated package and define what is stable asset API vs implementation detail.
94+
Stand up a docs site that consumes published token/component packages and serves as the canonical reference for usage, contracts, and examples. Deploy to `http://calibrate.msrd.dev`, `apps/storybook` can deploy to `http://calibrate.msrd.dev/storybook/`
9195

92-
### CLI bootstrap tool
96+
#### CLI bootstrap tool (`@measured/calibrate`)
9397

9498
Scope a `calibrate` bootstrap CLI for fast project scaffolding with sensible defaults for tokens, components, and optional assets.
9599

96-
### Framework adapters
100+
#### Framework adapters (e.g. `@measured/calibrate-react`)
97101

98102
Identify the minimum adapter surface needed to consume token/component contracts ergonomically across target frameworks.
99103

100-
### MCP/API
104+
#### MCP/API
101105

102106
Evaluate whether an MCP/API distribution path adds clear value beyond package and CLI workflows for token discovery and integration.
103107

@@ -224,3 +228,8 @@ _This section is a historical completion record; some entries may describe decis
224228
- script ownership moved to `apps/storybook`, with root convenience aliases (`storybook`, `storybook:build`)
225229
- static output normalized to `apps/storybook/storybook-static`
226230
- CI expanded with dedicated Storybook build job
231+
- Class/attribute grammar applied to root + surface contracts:
232+
- structural semantics use classes; state/variant semantics use attributes
233+
- `root` now emits `.clbr` + `data-brand` (+ optional `data-theme`)
234+
- `surface` now emits `.surface` + `data-variant` (default included)
235+
- token resolver selectors and generated brand CSS updated to match attribute grammar

packages/core/src/components/root/root.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
@layer clbr.root {
22
.clbr {
3+
all: initial;
34
background-color: var(--clbr-color-background-default);
45
background-repeat: no-repeat;
56
box-sizing: border-box;
67
color: var(--clbr-color-foreground-default);
8+
display: block;
79
font-family: var(--clbr-typography-font-family-default);
810
font-feature-settings: "ss03" 1;
911
font-optical-sizing: none;
@@ -36,6 +38,12 @@
3638
}
3739

3840
:where(.clbr) {
41+
:where(:not(svg, svg *)),
42+
:where(*::before),
43+
:where(*::after) {
44+
all: revert;
45+
}
46+
3947
*,
4048
*::before,
4149
*::after {

packages/core/src/components/root/root.test.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,31 @@ import { describe, expect, it } from "vitest";
22
import { renderClbrRoot } from "./root";
33

44
describe("renderClbrRoot", () => {
5-
it("uses msrd brand by default", () => {
5+
it("renders root class and default data-brand when brand is omitted", () => {
66
const html = renderClbrRoot({ children: "<p>content</p>" });
7-
expect(html).toContain('class="clbr clbr-brand-msrd"');
7+
expect(html).toContain('class="clbr"');
8+
expect(html).toContain('data-brand="msrd"');
89
});
910

10-
it("applies explicit brand class", () => {
11+
it("applies explicit brand attribute", () => {
1112
const html = renderClbrRoot({
1213
brand: "wrfr",
1314
children: "<p>content</p>",
1415
});
15-
expect(html).toContain('class="clbr clbr-brand-wrfr"');
16+
expect(html).toContain('data-brand="wrfr"');
1617
});
1718

18-
it("renders theme class when provided", () => {
19+
it("renders theme attribute when provided", () => {
1920
const html = renderClbrRoot({
2021
children: "<p>content</p>",
2122
theme: "dark",
2223
});
23-
expect(html).toContain("clbr-theme-dark");
24+
expect(html).toContain('data-theme="dark"');
2425
});
2526

26-
it("does not render theme class when omitted", () => {
27+
it("does not render theme attribute when omitted", () => {
2728
const html = renderClbrRoot({ children: "<p>content</p>" });
28-
expect(html).not.toContain("clbr-theme-");
29+
expect(html).not.toContain("data-theme=");
2930
});
3031

3132
it("renders dir when provided", () => {

packages/core/src/components/root/root.ts

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { cx } from "../../helpers/cx";
2-
31
export type ClbrBrand = "msrd" | "wrfr";
42
export type ClbrDirection = "ltr" | "rtl";
53
export type ClbrTheme = "light" | "dark";
@@ -26,25 +24,23 @@ export interface ClbrRootProps {
2624
/**
2725
* SSR renderer for the Calibrate root wrapper.
2826
*
29-
* Emits a `<div>` with Calibrate root classes, optional `dir`/`lang`
30-
* attributes, and optional `clbr-theme-{theme}` class, then injects the
31-
* provided HTML content inside.
27+
* Emits a `<div>` with the Calibrate root class, required `data-brand`,
28+
* optional `data-theme`, and optional `dir`/`lang` attributes, then injects
29+
* the provided HTML content inside.
3230
*
3331
* @param props - Root wrapper configuration and inner HTML content.
3432
* @returns HTML string for the Calibrate root wrapper.
3533
*/
3634
export function renderClbrRoot(props: ClbrRootProps): string {
3735
const { brand = "msrd", children, dir, lang, theme } = props;
3836

39-
const classAttr = cx(
40-
"clbr",
41-
`clbr-brand-${brand}`,
42-
theme ? `clbr-theme-${theme}` : undefined,
43-
);
37+
const brandAttr = ` data-brand="${brand}"`;
38+
const classAttr = "clbr";
4439
const dirAttr = dir ? ` dir="${dir}"` : "";
4540
const langAttr = lang ? ` lang="${lang}"` : "";
41+
const themeAttr = theme ? ` data-theme="${theme}"` : "";
4642

47-
return `<div class="${classAttr}"${dirAttr}${langAttr}>${children}</div>`;
43+
return `<div class="${classAttr}"${brandAttr}${themeAttr}${langAttr}${dirAttr}>${children}</div>`;
4844
}
4945

5046
/** Declarative root contract mirror for tooling, docs, and adapters. */
@@ -82,6 +78,17 @@ export const CLBR_ROOT_SPEC = {
8278
},
8379
rules: {
8480
attributes: [
81+
{
82+
behavior: "always",
83+
target: "data-brand",
84+
value: "{brand}",
85+
},
86+
{
87+
behavior: "emit",
88+
target: "data-theme",
89+
value: "{theme}",
90+
when: "theme is provided",
91+
},
8592
{
8693
behavior: "emit",
8794
target: "dir",
@@ -98,15 +105,6 @@ export const CLBR_ROOT_SPEC = {
98105
behavior: "always",
99106
value: "clbr",
100107
},
101-
{
102-
behavior: "always",
103-
value: "clbr-brand-{brand}",
104-
},
105-
{
106-
behavior: "emit",
107-
value: "clbr-theme-{theme}",
108-
when: "theme is provided",
109-
},
110108
],
111109
},
112110
} as const;

packages/core/src/components/surface/surface.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
@layer clbr.components {
22
:where(.clbr) {
3-
.clbr-surface {
3+
.surface {
44
background-color: var(--clbr-color-background-default);
55
color: var(--clbr-color-foreground-default);
66
}

packages/core/src/components/surface/surface.stories.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const meta = {
55
children: {
66
control: false,
77
},
8-
surface: {
8+
variant: {
99
control: { type: "select" },
1010
options: ["default", "brand"],
1111
},
@@ -29,15 +29,15 @@ const exampleContent = `<div style="padding: 1.75rem 1.25rem">Example content</d
2929
export const Default = {
3030
args: {
3131
children: exampleContent,
32-
surface: "default",
32+
variant: "default",
3333
},
3434
render: (args: ClbrSurfaceProps) => renderClbrSurface(args),
3535
};
3636

3737
export const Brand = {
3838
args: {
3939
children: exampleContent,
40-
surface: "brand",
40+
variant: "brand",
4141
},
4242
render: (args: ClbrSurfaceProps) => renderClbrSurface(args),
4343
};

packages/core/src/components/surface/surface.test.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,19 @@ import { describe, expect, it } from "vitest";
22
import { renderClbrSurface } from "./surface";
33

44
describe("renderClbrSurface", () => {
5-
it("uses only base surface class by default", () => {
5+
it("uses base surface class and default variant by default", () => {
66
const html = renderClbrSurface({ children: "<p>content</p>" });
7-
expect(html).toContain('class="clbr-surface"');
8-
expect(html).not.toContain("clbr-surface-default");
7+
expect(html).toContain('class="surface"');
8+
expect(html).toContain('data-variant="default"');
99
});
1010

11-
it("renders brand surface variant class", () => {
11+
it("renders brand surface variant attribute", () => {
1212
const html = renderClbrSurface({
1313
children: "<p>content</p>",
14-
surface: "brand",
14+
variant: "brand",
1515
});
16-
expect(html).toContain('class="clbr-surface clbr-surface-brand"');
16+
expect(html).toContain('class="surface"');
17+
expect(html).toContain('data-variant="brand"');
1718
});
1819

1920
it("injects children HTML content", () => {

0 commit comments

Comments
 (0)