Skip to content

Commit 1925f4e

Browse files
committed
chore: Style api docs-forward tools
1 parent d7cc26a commit 1925f4e

3 files changed

Lines changed: 80 additions & 17 deletions

File tree

src/internal/style-api/__tests__/docs.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import { extractStyleApiDocs } from '../docs';
77
const marker = (name: string, tokens: string[]) =>
88
`/* awsui:style-api-slot name=${name} tokens=${tokens.join(', ')} */`;
99

10+
// Emulates the compiled output of `@include style-api.docs-forward($name, $component, $slot)`.
11+
const forwardMarker = (name: string, component: string, slot: string) =>
12+
`/* awsui:style-api-slot name=${name} component=${component} slot=${slot} */`;
13+
1014
test('returns no slots when there are no markers', () => {
1115
const css = `
1216
.root { padding-inline: var(--awsui-style-padding-inline, 8px); }
@@ -53,3 +57,32 @@ test('tolerates whitespaces inside the marker', () => {
5357
const css = `/* \nawsui:style-api-slot name=header tokens=color-text, color-border */`;
5458
expect(extractStyleApiDocs(css).slots).toEqual([{ name: 'header', tokens: ['color-text', 'color-border'] }]);
5559
});
60+
61+
test('reads a forward slot that points to another component slot', () => {
62+
const css = `
63+
${forwardMarker('dismissButton', 'button', 'button')}
64+
.root { padding-inline: var(--awsui-style-padding-inline, 8px); }
65+
`;
66+
expect(extractStyleApiDocs(css).slots).toEqual([
67+
{ name: 'dismissButton', forwardsTo: { component: 'button', slot: 'button' } },
68+
]);
69+
});
70+
71+
test('reads token slots and forward slots together, preserving order', () => {
72+
const css = `
73+
${marker('root', ['color-text', 'color-background'])}
74+
${forwardMarker('dismissButton', 'button', 'button')}
75+
`;
76+
expect(extractStyleApiDocs(css).slots).toEqual([
77+
{ name: 'root', tokens: ['color-text', 'color-background'] },
78+
{ name: 'dismissButton', forwardsTo: { component: 'button', slot: 'button' } },
79+
]);
80+
});
81+
82+
test('throws on a duplicate slot name across token and forward markers', () => {
83+
const css = `
84+
${marker('dismissButton', ['color-text'])}
85+
${forwardMarker('dismissButton', 'button', 'button')}
86+
`;
87+
expect(() => extractStyleApiDocs(css)).toThrow(/multiple .+ annotations with the same name: "dismissButton"/);
88+
});

src/internal/style-api/docs.ts

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,49 +3,71 @@
33

44
// Extracts the Style API documentation surface from a component's *compiled* CSS.
55
//
6-
// Slots are declared explicitly by the author with the `style-api.docs($name, $tokens)` mixin, which
7-
// emits a machine-readable marker comment into the compiled CSS:
6+
// Slots are declared explicitly by the author with the style-api docs mixins, which emit a
7+
// machine-readable marker comment into the compiled CSS. Two forms exist:
88
//
9-
// /* awsui:style-api-slot name=<slot> tokens=<t1>, <t2> */
9+
// token slot — `@include style-api.docs($name, $tokens)`:
10+
// /* awsui:style-api-slot name=<slot> tokens=<t1>, <t2> */
1011
//
11-
// This module parses those markers.
12+
// forward slot — `@include style-api.docs-forward($name, $component, $slot)`:
13+
// /* awsui:style-api-slot name=<slot> component=<component> slot=<target-slot> */
14+
//
15+
// A forward slot reuses another component's slot (e.g. a nested Button) instead of owning tokens;
16+
// the docs consumer resolves it to that component's slot, so it never goes stale. This module parses
17+
// both forms.
1218

13-
const MARKER = /awsui:style-api-slot\s+name=([\w-]+)\s+tokens=([^*]*)\*\//g;
19+
const MARKER = /awsui:style-api-slot\s+name=([\w-]+)\s+(?:tokens=([^*]*)|component=([\w-]+)\s+slot=([\w-]+)\s*)\*\//g;
1420

1521
export interface StyleApiDocs {
1622
/**
17-
* The component's themeable slots (defined by classNames), each with its own set of style tokens.
23+
* The component's themeable slots (defined by classNames). Each slot either owns a set of style
24+
* tokens or forwards to another component's slot.
1825
*/
1926
slots: StyleApiSlotDocs[];
2027
}
2128

22-
export interface StyleApiSlotDocs {
29+
export type StyleApiSlotDocs = StyleApiTokenSlotDocs | StyleApiForwardSlotDocs;
30+
31+
interface StyleApiSlotDocsBase {
2332
/**
24-
* The first argument of `style-api.docs(...)` - must match the corresponding classNames slot.
33+
* The first argument of the docs mixin - must match the corresponding classNames slot.
2534
*/
2635
name: string;
36+
}
37+
38+
export interface StyleApiTokenSlotDocs extends StyleApiSlotDocsBase {
2739
/**
2840
* The public style tokens this slot supports (without "--awsui-style" prefix).
2941
*/
3042
tokens: string[];
3143
}
3244

45+
export interface StyleApiForwardSlotDocs extends StyleApiSlotDocsBase {
46+
/**
47+
* The slot this one forwards to. Its tokens are whatever the referenced component's slot documents.
48+
*/
49+
forwardsTo: { component: string; slot: string };
50+
}
51+
3352
/**
34-
* Extracts the Style API slot documentation from a component's compiled CSS by reading the
35-
* explicit slot markers emitted by `style-api.docs(...)`.
53+
* Extracts the Style API slot documentation from a component's compiled CSS by reading the explicit
54+
* slot markers emitted by the style-api docs mixins.
3655
*/
3756
export function extractStyleApiDocs(css: string): StyleApiDocs {
3857
const slots = new Array<StyleApiSlotDocs>();
3958
const usedSlots = new Set<string>();
4059

4160
for (const match of css.matchAll(MARKER)) {
42-
const name = match[1];
43-
const tokens = match[2].split(/[\s,]+/).filter(Boolean);
44-
slots.push({ name, tokens });
45-
if (!usedSlots.has(name)) {
46-
usedSlots.add(name);
61+
const [, name, tokens, component, slot] = match;
62+
if (usedSlots.has(name)) {
63+
throw new Error(`Found multiple style-api docs annotations with the same name: "${name}"`);
64+
}
65+
usedSlots.add(name);
66+
67+
if (tokens !== undefined) {
68+
slots.push({ name, tokens: tokens.split(/[\s,]+/).filter(Boolean) });
4769
} else {
48-
throw new Error(`Found multiple style-api.docs(...) annotations with the same name: "${name}"`);
70+
slots.push({ name, forwardsTo: { component, slot } });
4971
}
5072
}
5173
return { slots };

src/internal/style-api/index.scss

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,15 @@
100100
}
101101

102102
// Documents a themeable slot (emits docs only — no styling effect), from the slot map.
103-
// The `$name` must match the component's `classNames` property entry.
103+
// The `$name` must match the component's `classNames` property entry. Emits docs only.
104104
@mixin docs($name, $map) {
105105
/* awsui:style-api-slot name=#{$name} tokens=#{map.keys($map)} */
106106
}
107+
108+
// Documents a slot that forwards to another component's slot (e.g. a nested Button) instead of
109+
// owning tokens. `$name` must match the component's `classNames` property entry; `$component` and
110+
// `$slot` name the target component and its slot. The slot's tokens are whatever that target slot
111+
// documents — resolved by the docs consumer, so this stays in sync automatically. Emits docs only.
112+
@mixin docs-forward($name, $component, $slot) {
113+
/* awsui:style-api-slot name=#{$name} component=#{$component} slot=#{$slot} */
114+
}

0 commit comments

Comments
 (0)