@flareum/mcp 0.2.3 → 0.2.5
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/README.md +5 -0
- package/dist/cli.js +13 -11
- package/dist/first-pull.d.ts +31 -0
- package/dist/first-pull.js +80 -0
- package/dist/server.js +20 -5
- package/dist/skill.js +15 -6
- package/package.json +1 -1
- package/skill/SKILL.md +15 -26
- package/skill/references/naming.md +36 -0
- package/skill/references/typography.md +55 -0
package/README.md
CHANGED
|
@@ -177,6 +177,11 @@ area — `naming.md` for how a token name is built, `typography.md` for applying
|
|
|
177
177
|
its class rather than the mixin behind it. The whole folder is rewritten every run, so do not edit
|
|
178
178
|
it, and it prints where it wrote (or why it could not) on stderr.
|
|
179
179
|
|
|
180
|
+
**`claude mcp add` alone does not write it.** An MCP server starts when a client first connects, so
|
|
181
|
+
nothing runs until you open a session — or run `claude mcp list`, which connects to report status
|
|
182
|
+
and is why the skill sometimes appears then. `flareum pull` writes it too, so one pull sets the
|
|
183
|
+
project up without waiting for a connection.
|
|
184
|
+
|
|
180
185
|
On the first connection it also **pulls the stylesheets**, once. It looks for the folder this project
|
|
181
186
|
already keeps its styles in — `src/styles`, `app/styles`, `scss`, and the rest — and writes them to
|
|
182
187
|
`<that folder>/flareum`, falling back to `styles/flareum` in a project that has none. It says on
|
package/dist/cli.js
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { join, resolve } from 'node:path';
|
|
4
5
|
import { FlareumApiError, FlareumClient } from './client.js';
|
|
5
6
|
import { CONFIG_PATH, cliErrorReport, parseArgs, resolveConfig } from './config.js';
|
|
7
|
+
import { chooseStylesDir, fileWriter } from './first-pull.js';
|
|
6
8
|
import { isRetryable, pullReport, pullStyles } from './pull.js';
|
|
9
|
+
import { installSkill, skillInstallReport } from './skill.js';
|
|
7
10
|
import { watchIntervalMs, watchPublished, watchStartedReport } from './watch.js';
|
|
8
|
-
|
|
11
|
+
// Where the styles go is decided in ONE place, so the CLI and the server never disagree about it.
|
|
12
|
+
const defaultOut = () => chooseStylesDir(path => existsSync(resolve(process.cwd(), path)));
|
|
9
13
|
// Read rather than restated: a hardcoded version is wrong the moment `npm version` runs.
|
|
10
14
|
const VERSION = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
11
15
|
const HELP = `flareum — read a Flareum project's design tokens
|
|
12
16
|
|
|
13
17
|
flareum pull [--out <dir>] [--token pk_…] [--api <url>]
|
|
14
18
|
Write the stylesheets the last push published into <dir>
|
|
15
|
-
(default: ${
|
|
19
|
+
(default: the folder this project keeps its styles in — here, ${defaultOut()}).
|
|
16
20
|
|
|
17
21
|
flareum watch [--out <dir>] [--interval <seconds>]
|
|
18
22
|
Stay running and pull again every time you Push in Flareum.
|
|
@@ -52,14 +56,12 @@ try {
|
|
|
52
56
|
process.exit(command ? 1 : 0);
|
|
53
57
|
}
|
|
54
58
|
const config = resolveConfig({ flags, env: process.env, file: await readConfigFile() });
|
|
55
|
-
const outDir = config.out ??
|
|
59
|
+
const outDir = config.out ?? defaultOut();
|
|
56
60
|
const client = new FlareumClient({ token: config.token, api: config.api });
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
await writeFile(target, contents, 'utf8');
|
|
62
|
-
};
|
|
61
|
+
const write = fileWriter(resolve(process.cwd(), outDir));
|
|
62
|
+
// The server writes the skill too, but only when a client connects — `claude mcp add` spawns
|
|
63
|
+
// nothing, so until now the rules arrived on whatever ran first, which was luck.
|
|
64
|
+
console.log(skillInstallReport(await installSkill(process.cwd())));
|
|
63
65
|
const runPull = async () => {
|
|
64
66
|
const result = await pullStyles(client, write);
|
|
65
67
|
console.log(pullReport(result, outDir));
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { FlareumClient } from './client.js';
|
|
2
|
+
import { type PullResult } from './pull.js';
|
|
3
|
+
/** Does this path exist, relative to the project root? Injected so the choice is testable. */
|
|
4
|
+
export type Probe = (path: string) => boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Where this project's Flareum stylesheets belong. ONE decision, used by the CLI's default and by
|
|
7
|
+
* the server's first pull — two answers would write the same files to two places.
|
|
8
|
+
*/
|
|
9
|
+
export declare const chooseStylesDir: (exists: Probe, configured?: string) => string;
|
|
10
|
+
export declare const autoPullEnabled: (env: Record<string, string | undefined>) => boolean;
|
|
11
|
+
/** Only ever the FIRST time: an existing directory is the developer's, and `flareum pull` owns updates. */
|
|
12
|
+
export declare const needsFirstPull: (exists: Probe, dir: string) => boolean;
|
|
13
|
+
export type FirstPull = {
|
|
14
|
+
ran: false;
|
|
15
|
+
reason: 'disabled' | 'already-pulled';
|
|
16
|
+
dir: string;
|
|
17
|
+
} | {
|
|
18
|
+
ran: true;
|
|
19
|
+
dir: string;
|
|
20
|
+
result: PullResult;
|
|
21
|
+
} | {
|
|
22
|
+
ran: false;
|
|
23
|
+
reason: 'failed';
|
|
24
|
+
dir: string;
|
|
25
|
+
error: string;
|
|
26
|
+
};
|
|
27
|
+
export declare const firstPullReport: (outcome: FirstPull) => string | null;
|
|
28
|
+
/** Writes under `root`, refusing nothing the pull already refuses (see isWritablePath). */
|
|
29
|
+
export declare const fileWriter: (root: string) => (path: string, contents: string) => Promise<void>;
|
|
30
|
+
export declare const configuredOut: (cwd: string, env: Record<string, string | undefined>) => string | undefined;
|
|
31
|
+
export declare const runFirstPull: (client: Pick<FlareumClient, "files" | "file">, cwd: string, env: Record<string, string | undefined>, configuredOut?: string) => Promise<FirstPull>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { CONFIG_PATH } from './config.js';
|
|
5
|
+
import { pullStyles } from './pull.js';
|
|
6
|
+
// Most specific first. A repo that keeps stylesheets in app/styles gets them there rather than in a
|
|
7
|
+
// src/styles nobody in that project looks at.
|
|
8
|
+
const STYLE_ROOTS = [
|
|
9
|
+
'src/styles', 'src/style', 'src/assets/styles', 'src/scss', 'src/css',
|
|
10
|
+
'app/styles', 'app/assets/styles', 'assets/styles', 'styles', 'scss', 'css',
|
|
11
|
+
];
|
|
12
|
+
const LEAF = 'flareum';
|
|
13
|
+
/**
|
|
14
|
+
* Where this project's Flareum stylesheets belong. ONE decision, used by the CLI's default and by
|
|
15
|
+
* the server's first pull — two answers would write the same files to two places.
|
|
16
|
+
*/
|
|
17
|
+
export const chooseStylesDir = (exists, configured) => {
|
|
18
|
+
if (configured)
|
|
19
|
+
return configured;
|
|
20
|
+
// An earlier pull already answered this. Re-deciding would strand those files and pull a second copy.
|
|
21
|
+
const pulled = STYLE_ROOTS.map(root => `${root}/${LEAF}`).find(exists);
|
|
22
|
+
if (pulled)
|
|
23
|
+
return pulled;
|
|
24
|
+
const root = STYLE_ROOTS.find(exists);
|
|
25
|
+
if (root)
|
|
26
|
+
return `${root}/${LEAF}`;
|
|
27
|
+
return exists('src') ? `src/styles/${LEAF}` : `styles/${LEAF}`;
|
|
28
|
+
};
|
|
29
|
+
// Opt-out, not opt-in: a project with no stylesheets is the case this exists for, and a stdio server
|
|
30
|
+
// has nobody to ask at startup. Anything but an explicit off means on.
|
|
31
|
+
export const autoPullEnabled = (env) => !['off', 'false', '0', 'no'].includes((env.FLAREUM_AUTO_PULL ?? '').trim().toLowerCase());
|
|
32
|
+
/** Only ever the FIRST time: an existing directory is the developer's, and `flareum pull` owns updates. */
|
|
33
|
+
export const needsFirstPull = (exists, dir) => !exists(dir);
|
|
34
|
+
export const firstPullReport = (outcome) => {
|
|
35
|
+
// Silence is right for a start that did nothing — this prints on every server start.
|
|
36
|
+
if (!outcome.ran && outcome.reason !== 'failed')
|
|
37
|
+
return null;
|
|
38
|
+
if (!outcome.ran)
|
|
39
|
+
return `[flareum] could NOT pull the stylesheets into ${outcome.dir} — ${outcome.error}. `
|
|
40
|
+
+ 'The token tools still work; run `npx -p @flareum/mcp flareum pull` to retry.';
|
|
41
|
+
const { written, failed } = outcome.result;
|
|
42
|
+
const head = `[flareum] no local stylesheets — pulled ${written.length} into ${outcome.dir}. `
|
|
43
|
+
+ 'Run `npx -p @flareum/mcp flareum watch` to keep them current, or set FLAREUM_AUTO_PULL=off.';
|
|
44
|
+
return failed.length ? `${head}\n[flareum] ${failed.length} could not be written: `
|
|
45
|
+
+ failed.map(({ path, reason }) => `${path} — ${reason}`).join('; ') : head;
|
|
46
|
+
};
|
|
47
|
+
/** Writes under `root`, refusing nothing the pull already refuses (see isWritablePath). */
|
|
48
|
+
export const fileWriter = (root) => async (path, contents) => {
|
|
49
|
+
const target = join(root, path);
|
|
50
|
+
await mkdir(dirname(target), { recursive: true });
|
|
51
|
+
await writeFile(target, contents, 'utf8');
|
|
52
|
+
};
|
|
53
|
+
// The same `out` the CLI honours, read the same way — otherwise the server pulls into one folder and
|
|
54
|
+
// `flareum pull` updates another.
|
|
55
|
+
export const configuredOut = (cwd, env) => {
|
|
56
|
+
if (env.FLAREUM_OUT)
|
|
57
|
+
return env.FLAREUM_OUT;
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(readFileSync(resolve(cwd, CONFIG_PATH), 'utf8')).out || undefined;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// No config file, or an unreadable one: fall through to the folder probe rather than failing.
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
export const runFirstPull = async (client, cwd, env, configuredOut) => {
|
|
67
|
+
const exists = path => existsSync(resolve(cwd, path));
|
|
68
|
+
const dir = chooseStylesDir(exists, configuredOut);
|
|
69
|
+
if (!autoPullEnabled(env))
|
|
70
|
+
return { ran: false, reason: 'disabled', dir };
|
|
71
|
+
if (!needsFirstPull(exists, dir))
|
|
72
|
+
return { ran: false, reason: 'already-pulled', dir };
|
|
73
|
+
try {
|
|
74
|
+
return { ran: true, dir, result: await pullStyles(client, fileWriter(resolve(cwd, dir))) };
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
// A project with nothing published, an expired key, no network — none of them may stop the server.
|
|
78
|
+
return { ran: false, reason: 'failed', dir, error: error instanceof Error ? error.message : String(error) };
|
|
79
|
+
}
|
|
80
|
+
};
|
package/dist/server.js
CHANGED
|
@@ -8,11 +8,21 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprot
|
|
|
8
8
|
import { FlareumApiError, FlareumClient } from './client.js';
|
|
9
9
|
import { TOOL_DESCRIPTIONS, formatError, formatSearch, formatVariable } from './tools.js';
|
|
10
10
|
import { installSkill, skillInstallReport } from './skill.js';
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
import { configuredOut, firstPullReport, runFirstPull } from './first-pull.js';
|
|
12
|
+
// A missing key threw at module load, and an editor shows that as a Node stack trace with the
|
|
13
|
+
// message buried in it. Say it in one line and exit, the way the CLI already does.
|
|
14
|
+
let client;
|
|
15
|
+
try {
|
|
16
|
+
client = new FlareumClient({
|
|
17
|
+
token: process.env.FLAREUM_TOKEN ?? '',
|
|
18
|
+
api: process.env.FLAREUM_API,
|
|
19
|
+
projectId: process.env.FLAREUM_PROJECT,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
console.error(`[flareum] ${error instanceof Error ? error.message : String(error)}`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
16
26
|
// Read, never restated: the hardcoded 0.1.0 kept reporting itself from a 0.2.1 package.
|
|
17
27
|
const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
18
28
|
const server = new Server({ name: 'flareum', version }, { capabilities: { tools: {} } });
|
|
@@ -64,4 +74,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
64
74
|
});
|
|
65
75
|
// Never silently: if the skill does not land, the agent hardcodes values and nothing says why.
|
|
66
76
|
console.error(skillInstallReport(await installSkill(process.cwd())));
|
|
77
|
+
// First connection with no stylesheets pulled yet: fetch them now, into the folder this project
|
|
78
|
+
// already keeps its styles in. Reported, never asked — a stdio server has nobody to prompt.
|
|
79
|
+
const firstPull = firstPullReport(await runFirstPull(client, process.cwd(), process.env, configuredOut(process.cwd(), process.env)));
|
|
80
|
+
if (firstPull)
|
|
81
|
+
console.error(firstPull);
|
|
67
82
|
await server.connect(new StdioServerTransport());
|
package/dist/skill.js
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
const SKILL_DIR = ['.claude', 'skills', 'flareum'];
|
|
5
|
-
//
|
|
6
|
-
//
|
|
5
|
+
// Every file under skill/, not just SKILL.md: it is a router now, and a reference it points at but
|
|
6
|
+
// never wrote is a rule the agent silently never reads. One failure fails the whole install.
|
|
7
|
+
const copyTree = async (from, to) => {
|
|
8
|
+
await mkdir(to, { recursive: true });
|
|
9
|
+
for (const entry of await readdir(from, { withFileTypes: true }))
|
|
10
|
+
if (entry.isDirectory())
|
|
11
|
+
await copyTree(join(from, entry.name), join(to, entry.name));
|
|
12
|
+
else
|
|
13
|
+
await writeFile(join(to, entry.name), await readFile(join(from, entry.name), 'utf8'), 'utf8');
|
|
14
|
+
};
|
|
15
|
+
// Written on every start, not once: a stale copy of the rules is worse than none, and the files are
|
|
16
|
+
// generated rather than hand-edited.
|
|
7
17
|
export const installSkill = async (cwd) => {
|
|
8
|
-
const source = join(dirname(fileURLToPath(import.meta.url)), '..', 'skill'
|
|
18
|
+
const source = join(dirname(fileURLToPath(import.meta.url)), '..', 'skill');
|
|
9
19
|
const path = join(cwd, ...SKILL_DIR, 'SKILL.md');
|
|
10
20
|
try {
|
|
11
|
-
await
|
|
12
|
-
await writeFile(path, await readFile(source, 'utf8'), 'utf8');
|
|
21
|
+
await copyTree(source, join(cwd, ...SKILL_DIR));
|
|
13
22
|
return { ok: true, path };
|
|
14
23
|
}
|
|
15
24
|
catch (error) {
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: flareum
|
|
3
|
-
description: Use the project's Flareum design tokens instead of literal values. Invoke whenever you are about to write a colour, size, spacing, radius, shadow or
|
|
3
|
+
description: Use the project's Flareum design tokens instead of literal values. Invoke whenever you are about to write a colour, size, spacing, radius, shadow, duration or text style in CSS/SCSS/HTML, or need to know which token a value corresponds to.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Flareum design tokens
|
|
@@ -9,6 +9,9 @@ This project's design tokens live in Flareum. The generated CSS/SCSS in the repo
|
|
|
9
9
|
of them — the tokens themselves, with their types, modes, comments and usage, are reachable through
|
|
10
10
|
the `flareum_search` and `flareum_get` tools.
|
|
11
11
|
|
|
12
|
+
Every name here is written with **`[prefix]`** where your project's own token prefix goes — the
|
|
13
|
+
string every `cssName` in a search result starts with. Substitute it; never write `[prefix]` itself.
|
|
14
|
+
|
|
12
15
|
## The one rule
|
|
13
16
|
|
|
14
17
|
**Search before you write a literal value, and before you propose a token name.**
|
|
@@ -18,6 +21,16 @@ an existing `color/border/secondary` is worse — it looks like a decision, and
|
|
|
18
21
|
reconcile the two. `flareum_search` takes a hex, an rgb(), a path fragment, a phrase from a comment,
|
|
19
22
|
or a component name.
|
|
20
23
|
|
|
24
|
+
## The rules, by topic
|
|
25
|
+
|
|
26
|
+
This page is the workflow. Open the one below that covers what you are about to write — each is a
|
|
27
|
+
page, and it is worth reading in full before the first line of code.
|
|
28
|
+
|
|
29
|
+
| Writing | Read |
|
|
30
|
+
|---|---|
|
|
31
|
+
| Any text — a heading, a label, body copy, a pseudo-element's `content` | [references/typography.md](references/typography.md) |
|
|
32
|
+
| A token name, or judging one that already exists | [references/naming.md](references/naming.md) |
|
|
33
|
+
|
|
21
34
|
## Reading a search result
|
|
22
35
|
|
|
23
36
|
The header tells you which kind of answer you got, and they mean different things:
|
|
@@ -35,33 +48,9 @@ An empty project says so explicitly. Anything else always returns candidates.
|
|
|
35
48
|
This integration is **read-only** — there is no tool that writes to Flareum, by design. So:
|
|
36
49
|
|
|
37
50
|
1. Say which token is missing and what it would be for.
|
|
38
|
-
2. Propose a name that fits the grammar
|
|
51
|
+
2. Propose a name that fits [the grammar](references/naming.md), so the designer can create it in one step.
|
|
39
52
|
3. Do not silently hardcode the value and move on.
|
|
40
53
|
|
|
41
|
-
## The naming grammar
|
|
42
|
-
|
|
43
|
-
A token name is a serialized hierarchy, general → specific, terminal value last:
|
|
44
|
-
|
|
45
|
-
```
|
|
46
|
-
--[prefix]-[category]-[layer?]-[path...]-[value]
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
- **`category` always comes first** after the prefix: `color`, `space`, `size`, `radius`, `shadow`,
|
|
50
|
-
`blur`, `duration`, `z`, `opacity`, `font`, `border`.
|
|
51
|
-
- **`layer`** is optional and names the tier. For colour: `primitive`, `global`, `semantic`,
|
|
52
|
-
`action_palette`, `action_state`, `action`. A raw scale (`--fui-space-4`) has none.
|
|
53
|
-
- **A component is a `path` segment, never a top-level layer.**
|
|
54
|
-
|
|
55
|
-
```css
|
|
56
|
-
/* ✓ */ --fui-color-semantic-button-background-primary-hover
|
|
57
|
-
/* ✗ */ --fui-button-color-background-primary-hover /* component-first */
|
|
58
|
-
/* ✗ */ --fui-semantic-color-text-primary /* layer hoisted above the category */
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
Spell each segment the way the system already spells it — `disabled` not `disable`, `background` not
|
|
62
|
-
`bg`, `accent` not `acent`. Search for the segment before coining a second spelling of a word that
|
|
63
|
-
already exists; a new word is for a genuinely new concept only.
|
|
64
|
-
|
|
65
54
|
## Blast radius before a change
|
|
66
55
|
|
|
67
56
|
`flareum_get` reports two independent things, and they answer different questions:
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Token naming
|
|
2
|
+
|
|
3
|
+
Read this before proposing a token name, or when judging whether a name that already exists is the
|
|
4
|
+
one you want.
|
|
5
|
+
|
|
6
|
+
## The grammar
|
|
7
|
+
|
|
8
|
+
A token name is a serialized hierarchy, general → specific, terminal value last:
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
--[prefix]-[category]-[layer?]-[path...]-[value]
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
- **`category` always comes first** after the prefix: `color`, `space`, `size`, `radius`, `shadow`,
|
|
15
|
+
`blur`, `duration`, `z`, `opacity`, `font`, `border`.
|
|
16
|
+
- **`layer`** is optional and names the tier. For colour: `primitive`, `global`, `semantic`,
|
|
17
|
+
`action_palette`, `action_state`, `action`. A raw scale (`--[prefix]-space-4`) has none.
|
|
18
|
+
- **A component is a `path` segment, never a top-level layer.**
|
|
19
|
+
|
|
20
|
+
```css
|
|
21
|
+
/* ✓ */ --[prefix]-color-semantic-button-background-primary-hover
|
|
22
|
+
/* ✗ */ --[prefix]-button-color-background-primary-hover /* component-first */
|
|
23
|
+
/* ✗ */ --[prefix]-semantic-color-text-primary /* layer hoisted above the category */
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Build the tree first, then serialize it — `color → semantic → input → border → error` is what makes
|
|
27
|
+
`--[prefix]-color-semantic-input-border-error` the only possible spelling of it.
|
|
28
|
+
|
|
29
|
+
## One word per concept
|
|
30
|
+
|
|
31
|
+
Spell each segment the way the system already spells it — `disabled` not `disable`, `background` not
|
|
32
|
+
`bg`, `accent` not `acent`. Two spellings of one word mean nobody can find every token for it, or
|
|
33
|
+
trust that they have.
|
|
34
|
+
|
|
35
|
+
So `flareum_search` for the segment before coining a word. A new word is for a genuinely new concept
|
|
36
|
+
only; a shorter way to write one that exists is not a new concept.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Text styles
|
|
2
|
+
|
|
3
|
+
Read this before styling any text — a heading, a label, body copy, a value in a table cell, or a
|
|
4
|
+
pseudo-element's `content`.
|
|
5
|
+
|
|
6
|
+
## The class, not the mixin
|
|
7
|
+
|
|
8
|
+
Flareum exports every text style twice — a ready-made **class** in `typography/_styles.scss`, and the
|
|
9
|
+
**mixin** it is built from in `typography/_mixins.scss`. They are not two ways of doing the same
|
|
10
|
+
thing. **The class is how a text style is applied; the mixin is a fallback for the few elements a
|
|
11
|
+
class cannot reach.**
|
|
12
|
+
|
|
13
|
+
Put the pair on the element that holds the text — the base class plus the style class:
|
|
14
|
+
|
|
15
|
+
```html
|
|
16
|
+
<h2 class="[prefix]-typography [prefix]-h5">Export settings</h2>
|
|
17
|
+
<p class="[prefix]-typography [prefix]-t2">Choose which collections ship to Figma.</p>
|
|
18
|
+
<span class="[prefix]-typography [prefix]-t2 [prefix]-bold">12 variables</span>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Both halves are required. The base class declares the shared properties (`--line-height` among them)
|
|
22
|
+
and the style class re-points them, so a style class on its own applies nothing. A variant slot —
|
|
23
|
+
`bold`, `regular`, `highlight` — is a third class beside the style, never a replacement for it.
|
|
24
|
+
|
|
25
|
+
Why the class wins: it names the style in the markup, where the next person reading the template can
|
|
26
|
+
see which style an element uses and change it without opening a stylesheet; and one rule serves every
|
|
27
|
+
element that carries it, instead of each component compiling its own copy of the same declarations.
|
|
28
|
+
|
|
29
|
+
## The exception
|
|
30
|
+
|
|
31
|
+
**Reach for the mixin only where no class can be placed** — a pseudo-element, or markup you do not
|
|
32
|
+
render (a third-party widget, generated HTML) and therefore cannot add an attribute to.
|
|
33
|
+
|
|
34
|
+
```scss
|
|
35
|
+
// ::after has no class attribute — the only way to give it the style
|
|
36
|
+
.field::after {
|
|
37
|
+
content: attr(data-hint);
|
|
38
|
+
@include [prefix]-typography-t3;
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
That is the whole list. "It was easier from SCSS" is not on it: a mixin reached for out of convenience
|
|
43
|
+
hides the style from the markup and duplicates the declarations into every component that includes it.
|
|
44
|
+
|
|
45
|
+
## When a style looks like it is not applying
|
|
46
|
+
|
|
47
|
+
Check these before overriding anything — each one silently beats the class rather than erroring:
|
|
48
|
+
|
|
49
|
+
- **A lone style class.** `[prefix]-h5` with no `[prefix]-typography` beside it applies nothing.
|
|
50
|
+
- **A `font:` shorthand on the same element.** It resets family, size, weight and line-height in one
|
|
51
|
+
go, and component styles load after the global ones. Use the longhand for the one thing you meant
|
|
52
|
+
to change.
|
|
53
|
+
- **A missing `--line-height`.** Padding computed from it resolves to an invalid `calc()`, and the
|
|
54
|
+
property falls back to its initial value with nothing in the console. The typography class has to
|
|
55
|
+
be on the element that reads the variable, or an ancestor of it.
|