@pantoken/swatches 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/README.md +70 -0
- package/dist/index.d.mts +177 -0
- package/dist/index.mjs +340 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# @pantoken/swatches
|
|
2
|
+
|
|
3
|
+
Export Instructure colours as designer swatch palettes — files a designer imports directly into
|
|
4
|
+
their tool — plus a viewable SVG specimen sheet. These are terminal outputs, not an interchange
|
|
5
|
+
format: for token interchange use `@pantoken/dtcg` or `@pantoken/figma`.
|
|
6
|
+
|
|
7
|
+
| Format | Tools |
|
|
8
|
+
| ---------------------------------------- | ------------------------------------------------------------------ |
|
|
9
|
+
| **ASE** (`toAse`) | Photoshop, Illustrator, InDesign (and Affinity, which imports ASE) |
|
|
10
|
+
| **`.gpl`** (`toGpl`) | GIMP, Inkscape, Krita, Blender, and Aseprite |
|
|
11
|
+
| **`.sketchpalette`** (`toSketchPalette`) | Sketch (Sketch Palettes plugin) and Lunacy |
|
|
12
|
+
| **`.svg`** (`toSvg`) | A grouped specimen sheet for a README, docs page, PR, or Figma |
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npm i @pantoken/swatches
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Also available as `pantoken/swatches`.
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { swatches, toAse, toGpl, toSketchPalette, toSvg } from "@pantoken/swatches";
|
|
26
|
+
|
|
27
|
+
writeFileSync("instructure.ase", toAse(swatches));
|
|
28
|
+
writeFileSync("instructure.gpl", toGpl(swatches));
|
|
29
|
+
writeFileSync("instructure.sketchpalette", JSON.stringify(toSketchPalette(swatches)));
|
|
30
|
+
writeFileSync("instructure.svg", toSvg(swatches));
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or via the CLI:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
pantoken generate swatches --format ase --out instructure.ase
|
|
37
|
+
pantoken generate swatches --format gpl --out instructure.gpl
|
|
38
|
+
pantoken generate swatches --format sketch --out instructure.sketchpalette
|
|
39
|
+
pantoken generate swatches --format svg --out instructure.svg
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Swatches are colours only — a flat reduction of the token set. Build the list from any IR with
|
|
43
|
+
`toSwatches(tokens, mode)`.
|
|
44
|
+
|
|
45
|
+
## API
|
|
46
|
+
|
|
47
|
+
- **`swatches: Swatch[]`** — the `rebrand` colour swatches (the default palette).
|
|
48
|
+
- **`toSwatches(tokens, mode?): Swatch[]`** — reduce a token IR to a flat list of colour swatches:
|
|
49
|
+
resolve references, pick a `mode` (`"light"` default or `"dark"`), and keep only tokens whose value
|
|
50
|
+
is a hex colour. Names drop the `--instui-` prefix.
|
|
51
|
+
- **`toAse(swatches): Uint8Array`** — encode swatches as Adobe Swatch Exchange (ASE) bytes.
|
|
52
|
+
- **`toGpl(swatches, options?): string`** — encode swatches as a GIMP `.gpl` palette string.
|
|
53
|
+
`options.name` sets the palette name (default `"Instructure"`).
|
|
54
|
+
- **`toSketchPalette(swatches): SketchPalette`** — encode swatches as a Sketch palette object
|
|
55
|
+
(serialize with `JSON.stringify`).
|
|
56
|
+
- **`toSvg(swatches, options?): string`** — render a grouped SVG specimen sheet. Swatches are grouped
|
|
57
|
+
by name minus the leaf segment; `options.title` sets the heading and `options.columns` the chips
|
|
58
|
+
per row (default `6`).
|
|
59
|
+
- **`hexToRgb(hex): Rgb | undefined`** — parse a hex colour to 0–255 channels, or `undefined`.
|
|
60
|
+
- **`Swatch`, `Rgb`, `Mode`, `ToGplOptions`, `SketchPalette`, `ToSvgOptions`** — the model and option
|
|
61
|
+
types.
|
|
62
|
+
|
|
63
|
+
## Related
|
|
64
|
+
|
|
65
|
+
- Pairs with `@pantoken/tokens` for the token IR to reduce.
|
|
66
|
+
- For token interchange (not terminal palettes), see `@pantoken/dtcg` and `@pantoken/figma`.
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
|
|
70
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { Token } from "@pantoken/model";
|
|
2
|
+
|
|
3
|
+
//#region src/model.d.ts
|
|
4
|
+
/** A single palette entry. */
|
|
5
|
+
interface Swatch {
|
|
6
|
+
name: string;
|
|
7
|
+
hex: string;
|
|
8
|
+
}
|
|
9
|
+
/** RGB channels, 0–255. */
|
|
10
|
+
interface Rgb {
|
|
11
|
+
r: number;
|
|
12
|
+
g: number;
|
|
13
|
+
b: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Parse `#rgb`/`#rrggbb`/`#rrggbbaa` to 0–255 channels, or `undefined` if not a hex colour.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* import { hexToRgb } from "@pantoken/swatches";
|
|
21
|
+
*
|
|
22
|
+
* hexToRgb("#ff8800"); // { r: 255, g: 136, b: 0 }
|
|
23
|
+
* hexToRgb("nope"); // undefined
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
declare function hexToRgb(hex: string): Rgb | undefined;
|
|
27
|
+
/** The colour mode to resolve when flattening `light-dark()` values. */
|
|
28
|
+
type Mode = "light" | "dark";
|
|
29
|
+
/**
|
|
30
|
+
* Reduce a token IR to a flat list of colour swatches: resolve references, pick a mode, keep only
|
|
31
|
+
* tokens whose value is a hex colour (icons and non-colour tokens are dropped).
|
|
32
|
+
*
|
|
33
|
+
* @param tokens - The IR (e.g. from `@pantoken/tokens`).
|
|
34
|
+
* @param mode - Which colour mode to resolve (default `"light"`).
|
|
35
|
+
* @returns The swatch list, named by token (without the `--instui-` prefix).
|
|
36
|
+
*
|
|
37
|
+
* @example Reduce the token IR to light-mode swatches
|
|
38
|
+
* ```ts
|
|
39
|
+
* import { toSwatches } from "@pantoken/swatches";
|
|
40
|
+
* import { tokens } from "@pantoken/tokens";
|
|
41
|
+
*
|
|
42
|
+
* const swatches = toSwatches(tokens); // [{ name: "color-background-brand", hex: "#…" }, …]
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* @example Dark mode
|
|
46
|
+
* ```ts
|
|
47
|
+
* import { toSwatches } from "@pantoken/swatches";
|
|
48
|
+
* import { byTheme } from "@pantoken/tokens";
|
|
49
|
+
*
|
|
50
|
+
* const swatches = toSwatches(byTheme("canvas"), "dark");
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
declare function toSwatches(tokens: readonly Token[], mode?: Mode): Swatch[];
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region src/ase.d.ts
|
|
56
|
+
/**
|
|
57
|
+
* Encode swatches as ASE bytes. Non-hex swatches are skipped.
|
|
58
|
+
*
|
|
59
|
+
* @param swatches - The palette.
|
|
60
|
+
* @returns The ASE file as bytes.
|
|
61
|
+
*
|
|
62
|
+
* @example Write an Adobe .ase palette
|
|
63
|
+
* ```ts
|
|
64
|
+
* import { writeFileSync } from "node:fs";
|
|
65
|
+
* import { swatches, toAse } from "@pantoken/swatches";
|
|
66
|
+
*
|
|
67
|
+
* writeFileSync("instructure.ase", toAse(swatches));
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
declare function toAse(swatches: readonly Swatch[]): Uint8Array;
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region src/gpl.d.ts
|
|
73
|
+
/** Options for {@link toGpl}. */
|
|
74
|
+
interface ToGplOptions {
|
|
75
|
+
/** The palette name (default `"Instructure"`). */
|
|
76
|
+
name?: string;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Encode swatches as a GIMP `.gpl` palette string.
|
|
80
|
+
*
|
|
81
|
+
* @param swatches - The palette.
|
|
82
|
+
* @param options - {@link ToGplOptions}.
|
|
83
|
+
*
|
|
84
|
+
* @example Write a GIMP .gpl palette
|
|
85
|
+
* ```ts
|
|
86
|
+
* import { writeFileSync } from "node:fs";
|
|
87
|
+
* import { swatches, toGpl } from "@pantoken/swatches";
|
|
88
|
+
*
|
|
89
|
+
* writeFileSync("instructure.gpl", toGpl(swatches));
|
|
90
|
+
* ```
|
|
91
|
+
*
|
|
92
|
+
* @example With a custom palette name
|
|
93
|
+
* ```ts
|
|
94
|
+
* import { swatches, toGpl } from "@pantoken/swatches";
|
|
95
|
+
*
|
|
96
|
+
* const gpl = toGpl(swatches, { name: "Instructure Rebrand" });
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
declare function toGpl(swatches: readonly Swatch[], options?: ToGplOptions): string;
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region src/sketch.d.ts
|
|
102
|
+
/** A `.sketchpalette` document. */
|
|
103
|
+
interface SketchPalette {
|
|
104
|
+
compatibleVersion: string;
|
|
105
|
+
pluginVersion: string;
|
|
106
|
+
colors: {
|
|
107
|
+
name: string;
|
|
108
|
+
red: number;
|
|
109
|
+
green: number;
|
|
110
|
+
blue: number;
|
|
111
|
+
alpha: number;
|
|
112
|
+
}[];
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Encode swatches as a Sketch palette object (serialize with `JSON.stringify`).
|
|
116
|
+
*
|
|
117
|
+
* @param swatches - The palette.
|
|
118
|
+
*
|
|
119
|
+
* @example Write a Sketch .sketchpalette
|
|
120
|
+
* ```ts
|
|
121
|
+
* import { writeFileSync } from "node:fs";
|
|
122
|
+
* import { swatches, toSketchPalette } from "@pantoken/swatches";
|
|
123
|
+
*
|
|
124
|
+
* writeFileSync("instructure.sketchpalette", JSON.stringify(toSketchPalette(swatches)));
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
declare function toSketchPalette(swatches: readonly Swatch[]): SketchPalette;
|
|
128
|
+
//#endregion
|
|
129
|
+
//#region src/svg.d.ts
|
|
130
|
+
/** Options for {@link toSvg}. */
|
|
131
|
+
interface ToSvgOptions {
|
|
132
|
+
/** The sheet title (default `"Instructure color tokens"`). */
|
|
133
|
+
title?: string;
|
|
134
|
+
/** Chips per row within a section (default `6`). */
|
|
135
|
+
columns?: number;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Render swatches as a grouped SVG specimen sheet.
|
|
139
|
+
*
|
|
140
|
+
* @param swatches - The palette (e.g. from `toSwatches`).
|
|
141
|
+
* @param options - {@link ToSvgOptions}.
|
|
142
|
+
* @returns The SVG document as a string.
|
|
143
|
+
*
|
|
144
|
+
* @example Render a specimen sheet for a README
|
|
145
|
+
* ```ts
|
|
146
|
+
* import { writeFileSync } from "node:fs";
|
|
147
|
+
* import { swatches, toSvg } from "@pantoken/swatches";
|
|
148
|
+
*
|
|
149
|
+
* writeFileSync("palette.svg", toSvg(swatches));
|
|
150
|
+
* ```
|
|
151
|
+
*
|
|
152
|
+
* @example Specimen only the core palette, with a title and wider grid
|
|
153
|
+
* ```ts
|
|
154
|
+
* import { swatches, toSvg } from "@pantoken/swatches";
|
|
155
|
+
*
|
|
156
|
+
* const core = swatches.filter((s) => !s.name.startsWith("primitive-"));
|
|
157
|
+
* const svg = toSvg(core, { title: "Instructure core colors", columns: 8 });
|
|
158
|
+
* ```
|
|
159
|
+
*/
|
|
160
|
+
declare function toSvg(swatches: readonly Swatch[], options?: ToSvgOptions): string;
|
|
161
|
+
//#endregion
|
|
162
|
+
//#region src/index.d.ts
|
|
163
|
+
/**
|
|
164
|
+
* The `rebrand` colour swatches (the default palette).
|
|
165
|
+
*
|
|
166
|
+
* @example Encode the ready-made palette to any format
|
|
167
|
+
* ```ts
|
|
168
|
+
* import { writeFileSync } from "node:fs";
|
|
169
|
+
* import { swatches, toAse, toGpl } from "@pantoken/swatches";
|
|
170
|
+
*
|
|
171
|
+
* writeFileSync("instructure.ase", toAse(swatches));
|
|
172
|
+
* writeFileSync("instructure.gpl", toGpl(swatches));
|
|
173
|
+
* ```
|
|
174
|
+
*/
|
|
175
|
+
declare const swatches: Swatch[];
|
|
176
|
+
//#endregion
|
|
177
|
+
export { type Mode, type Rgb, type SketchPalette, type Swatch, type ToGplOptions, type ToSvgOptions, hexToRgb, swatches, toAse, toGpl, toSketchPalette, toSvg, toSwatches };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { tokens } from "@pantoken/tokens";
|
|
2
|
+
import { makeResolver, parseHexColor } from "@pantoken/utils";
|
|
3
|
+
//#region src/model.ts
|
|
4
|
+
/**
|
|
5
|
+
* The shared swatch model and colour helpers. A {@link Swatch} is just a name + hex colour — the
|
|
6
|
+
* flat reduction of the token set that every designer swatch format consumes.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Parse `#rgb`/`#rrggbb`/`#rrggbbaa` to 0–255 channels, or `undefined` if not a hex colour.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { hexToRgb } from "@pantoken/swatches";
|
|
16
|
+
*
|
|
17
|
+
* hexToRgb("#ff8800"); // { r: 255, g: 136, b: 0 }
|
|
18
|
+
* hexToRgb("nope"); // undefined
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
function hexToRgb(hex) {
|
|
22
|
+
const c = parseHexColor(hex);
|
|
23
|
+
return c ? {
|
|
24
|
+
r: c.r,
|
|
25
|
+
g: c.g,
|
|
26
|
+
b: c.b
|
|
27
|
+
} : void 0;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Reduce a token IR to a flat list of colour swatches: resolve references, pick a mode, keep only
|
|
31
|
+
* tokens whose value is a hex colour (icons and non-colour tokens are dropped).
|
|
32
|
+
*
|
|
33
|
+
* @param tokens - The IR (e.g. from `@pantoken/tokens`).
|
|
34
|
+
* @param mode - Which colour mode to resolve (default `"light"`).
|
|
35
|
+
* @returns The swatch list, named by token (without the `--instui-` prefix).
|
|
36
|
+
*
|
|
37
|
+
* @example Reduce the token IR to light-mode swatches
|
|
38
|
+
* ```ts
|
|
39
|
+
* import { toSwatches } from "@pantoken/swatches";
|
|
40
|
+
* import { tokens } from "@pantoken/tokens";
|
|
41
|
+
*
|
|
42
|
+
* const swatches = toSwatches(tokens); // [{ name: "color-background-brand", hex: "#…" }, …]
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* @example Dark mode
|
|
46
|
+
* ```ts
|
|
47
|
+
* import { toSwatches } from "@pantoken/swatches";
|
|
48
|
+
* import { byTheme } from "@pantoken/tokens";
|
|
49
|
+
*
|
|
50
|
+
* const swatches = toSwatches(byTheme("canvas"), "dark");
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
function toSwatches(tokens, mode = "light") {
|
|
54
|
+
const resolve = makeResolver(tokens, { mode });
|
|
55
|
+
const out = [];
|
|
56
|
+
for (const token of tokens) {
|
|
57
|
+
if (token.meta?.kind === "icon") continue;
|
|
58
|
+
const hex = resolve(token.value).trim();
|
|
59
|
+
if (hexToRgb(hex)) out.push({
|
|
60
|
+
name: token.name.replace(/^--instui-/, ""),
|
|
61
|
+
hex
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/ase.ts
|
|
68
|
+
/**
|
|
69
|
+
* Encode swatches as an Adobe Swatch Exchange (ASE) file — the palette format Photoshop,
|
|
70
|
+
* Illustrator, and InDesign import. Binary layout (big-endian):
|
|
71
|
+
*
|
|
72
|
+
* ```
|
|
73
|
+
* "ASEF" u16 major=1 u16 minor=0 u32 blockCount
|
|
74
|
+
* per colour block:
|
|
75
|
+
* u16 type=0x0001 u32 dataLength
|
|
76
|
+
* u16 nameLen(incl. NUL) UTF-16BE name + NUL "RGB " f32 r,g,b (0–1) u16 colorType=2
|
|
77
|
+
* ```
|
|
78
|
+
*
|
|
79
|
+
* @module
|
|
80
|
+
*/
|
|
81
|
+
/**
|
|
82
|
+
* Encode swatches as ASE bytes. Non-hex swatches are skipped.
|
|
83
|
+
*
|
|
84
|
+
* @param swatches - The palette.
|
|
85
|
+
* @returns The ASE file as bytes.
|
|
86
|
+
*
|
|
87
|
+
* @example Write an Adobe .ase palette
|
|
88
|
+
* ```ts
|
|
89
|
+
* import { writeFileSync } from "node:fs";
|
|
90
|
+
* import { swatches, toAse } from "@pantoken/swatches";
|
|
91
|
+
*
|
|
92
|
+
* writeFileSync("instructure.ase", toAse(swatches));
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
function toAse(swatches) {
|
|
96
|
+
const entries = swatches.map((s) => ({
|
|
97
|
+
name: s.name,
|
|
98
|
+
rgb: hexToRgb(s.hex)
|
|
99
|
+
})).filter((e) => Boolean(e.rgb));
|
|
100
|
+
const header = Buffer.alloc(12);
|
|
101
|
+
header.write("ASEF", 0, "ascii");
|
|
102
|
+
header.writeUInt16BE(1, 4);
|
|
103
|
+
header.writeUInt16BE(0, 6);
|
|
104
|
+
header.writeUInt32BE(entries.length, 8);
|
|
105
|
+
const chunks = [header];
|
|
106
|
+
for (const { name, rgb } of entries) {
|
|
107
|
+
const nameLen = name.length + 1;
|
|
108
|
+
const nameBuf = Buffer.alloc(nameLen * 2);
|
|
109
|
+
for (let i = 0; i < name.length; i++) nameBuf.writeUInt16BE(name.charCodeAt(i), i * 2);
|
|
110
|
+
const color = Buffer.alloc(12);
|
|
111
|
+
color.writeFloatBE(rgb.r / 255, 0);
|
|
112
|
+
color.writeFloatBE(rgb.g / 255, 4);
|
|
113
|
+
color.writeFloatBE(rgb.b / 255, 8);
|
|
114
|
+
const nameLenBuf = Buffer.alloc(2);
|
|
115
|
+
nameLenBuf.writeUInt16BE(nameLen, 0);
|
|
116
|
+
const colorType = Buffer.alloc(2);
|
|
117
|
+
colorType.writeUInt16BE(2, 0);
|
|
118
|
+
const data = Buffer.concat([
|
|
119
|
+
nameLenBuf,
|
|
120
|
+
nameBuf,
|
|
121
|
+
Buffer.from("RGB ", "ascii"),
|
|
122
|
+
color,
|
|
123
|
+
colorType
|
|
124
|
+
]);
|
|
125
|
+
const blockHeader = Buffer.alloc(6);
|
|
126
|
+
blockHeader.writeUInt16BE(1, 0);
|
|
127
|
+
blockHeader.writeUInt32BE(data.length, 2);
|
|
128
|
+
chunks.push(blockHeader, data);
|
|
129
|
+
}
|
|
130
|
+
return new Uint8Array(Buffer.concat(chunks));
|
|
131
|
+
}
|
|
132
|
+
//#endregion
|
|
133
|
+
//#region src/gpl.ts
|
|
134
|
+
/**
|
|
135
|
+
* Encode swatches as a GIMP palette (`.gpl`) — imported by GIMP, Inkscape, Krita, Blender, and
|
|
136
|
+
* Aseprite. Plain text: a header, then `R G B name` lines (0–255).
|
|
137
|
+
*
|
|
138
|
+
* @module
|
|
139
|
+
*/
|
|
140
|
+
/**
|
|
141
|
+
* Encode swatches as a GIMP `.gpl` palette string.
|
|
142
|
+
*
|
|
143
|
+
* @param swatches - The palette.
|
|
144
|
+
* @param options - {@link ToGplOptions}.
|
|
145
|
+
*
|
|
146
|
+
* @example Write a GIMP .gpl palette
|
|
147
|
+
* ```ts
|
|
148
|
+
* import { writeFileSync } from "node:fs";
|
|
149
|
+
* import { swatches, toGpl } from "@pantoken/swatches";
|
|
150
|
+
*
|
|
151
|
+
* writeFileSync("instructure.gpl", toGpl(swatches));
|
|
152
|
+
* ```
|
|
153
|
+
*
|
|
154
|
+
* @example With a custom palette name
|
|
155
|
+
* ```ts
|
|
156
|
+
* import { swatches, toGpl } from "@pantoken/swatches";
|
|
157
|
+
*
|
|
158
|
+
* const gpl = toGpl(swatches, { name: "Instructure Rebrand" });
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
function toGpl(swatches, options = {}) {
|
|
162
|
+
const lines = [
|
|
163
|
+
"GIMP Palette",
|
|
164
|
+
`Name: ${options.name ?? "Instructure"}`,
|
|
165
|
+
"Columns: 0",
|
|
166
|
+
"#"
|
|
167
|
+
];
|
|
168
|
+
for (const swatch of swatches) {
|
|
169
|
+
const rgb = hexToRgb(swatch.hex);
|
|
170
|
+
if (!rgb) continue;
|
|
171
|
+
const col = (n) => String(n).padStart(3, " ");
|
|
172
|
+
lines.push(`${col(rgb.r)} ${col(rgb.g)} ${col(rgb.b)}\t${swatch.name}`);
|
|
173
|
+
}
|
|
174
|
+
return `${lines.join("\n")}\n`;
|
|
175
|
+
}
|
|
176
|
+
//#endregion
|
|
177
|
+
//#region src/sketch.ts
|
|
178
|
+
/**
|
|
179
|
+
* Encode swatches as a Sketch `.sketchpalette` document (the Sketch Palettes plugin format). JSON
|
|
180
|
+
* with colours as 0–1 RGBA.
|
|
181
|
+
*
|
|
182
|
+
* @module
|
|
183
|
+
*/
|
|
184
|
+
/**
|
|
185
|
+
* Encode swatches as a Sketch palette object (serialize with `JSON.stringify`).
|
|
186
|
+
*
|
|
187
|
+
* @param swatches - The palette.
|
|
188
|
+
*
|
|
189
|
+
* @example Write a Sketch .sketchpalette
|
|
190
|
+
* ```ts
|
|
191
|
+
* import { writeFileSync } from "node:fs";
|
|
192
|
+
* import { swatches, toSketchPalette } from "@pantoken/swatches";
|
|
193
|
+
*
|
|
194
|
+
* writeFileSync("instructure.sketchpalette", JSON.stringify(toSketchPalette(swatches)));
|
|
195
|
+
* ```
|
|
196
|
+
*/
|
|
197
|
+
function toSketchPalette(swatches) {
|
|
198
|
+
const colors = [];
|
|
199
|
+
for (const swatch of swatches) {
|
|
200
|
+
const rgb = hexToRgb(swatch.hex);
|
|
201
|
+
if (!rgb) continue;
|
|
202
|
+
colors.push({
|
|
203
|
+
name: swatch.name,
|
|
204
|
+
red: rgb.r / 255,
|
|
205
|
+
green: rgb.g / 255,
|
|
206
|
+
blue: rgb.b / 255,
|
|
207
|
+
alpha: 1
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
compatibleVersion: "2.0",
|
|
212
|
+
pluginVersion: "2.22",
|
|
213
|
+
colors
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
//#endregion
|
|
217
|
+
//#region src/svg.ts
|
|
218
|
+
const MARGIN = 32;
|
|
219
|
+
const GROUP_HEAD_H = 26;
|
|
220
|
+
const GROUP_GAP = 26;
|
|
221
|
+
const CELL_W = 150;
|
|
222
|
+
const SWATCH_H = 46;
|
|
223
|
+
const CELL_H = 78;
|
|
224
|
+
const GAP_X = 14;
|
|
225
|
+
const GAP_Y = 16;
|
|
226
|
+
const MAX_LABEL = 22;
|
|
227
|
+
/** Escape the five XML text characters. */
|
|
228
|
+
function esc(text) {
|
|
229
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
230
|
+
}
|
|
231
|
+
/** The category a swatch belongs to: first two name segments, or three for a primitive family. */
|
|
232
|
+
function categoryOf(name) {
|
|
233
|
+
const parts = name.split("-");
|
|
234
|
+
const depth = parts[0] === "primitive" ? 3 : 2;
|
|
235
|
+
return parts.slice(0, depth).join("-");
|
|
236
|
+
}
|
|
237
|
+
/** Group swatches by category, preserving first-appearance order. */
|
|
238
|
+
function group(swatches) {
|
|
239
|
+
const groups = /* @__PURE__ */ new Map();
|
|
240
|
+
for (const swatch of swatches) {
|
|
241
|
+
const key = categoryOf(swatch.name);
|
|
242
|
+
const rest = swatch.name.length > key.length ? swatch.name.slice(key.length + 1) : swatch.name;
|
|
243
|
+
const label = rest.length > MAX_LABEL ? `${rest.slice(0, MAX_LABEL - 1)}…` : rest;
|
|
244
|
+
(groups.get(key) ?? groups.set(key, []).get(key)).push({
|
|
245
|
+
label,
|
|
246
|
+
swatch
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
return [...groups].map(([name, items]) => ({
|
|
250
|
+
name,
|
|
251
|
+
items
|
|
252
|
+
}));
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Render swatches as a grouped SVG specimen sheet.
|
|
256
|
+
*
|
|
257
|
+
* @param swatches - The palette (e.g. from `toSwatches`).
|
|
258
|
+
* @param options - {@link ToSvgOptions}.
|
|
259
|
+
* @returns The SVG document as a string.
|
|
260
|
+
*
|
|
261
|
+
* @example Render a specimen sheet for a README
|
|
262
|
+
* ```ts
|
|
263
|
+
* import { writeFileSync } from "node:fs";
|
|
264
|
+
* import { swatches, toSvg } from "@pantoken/swatches";
|
|
265
|
+
*
|
|
266
|
+
* writeFileSync("palette.svg", toSvg(swatches));
|
|
267
|
+
* ```
|
|
268
|
+
*
|
|
269
|
+
* @example Specimen only the core palette, with a title and wider grid
|
|
270
|
+
* ```ts
|
|
271
|
+
* import { swatches, toSvg } from "@pantoken/swatches";
|
|
272
|
+
*
|
|
273
|
+
* const core = swatches.filter((s) => !s.name.startsWith("primitive-"));
|
|
274
|
+
* const svg = toSvg(core, { title: "Instructure core colors", columns: 8 });
|
|
275
|
+
* ```
|
|
276
|
+
*/
|
|
277
|
+
function toSvg(swatches, options = {}) {
|
|
278
|
+
const title = options.title ?? "Instructure color tokens";
|
|
279
|
+
const columns = Math.max(1, options.columns ?? 6);
|
|
280
|
+
const width = MARGIN * 2 + columns * CELL_W + (columns - 1) * GAP_X;
|
|
281
|
+
const groups = group(swatches);
|
|
282
|
+
const body = [];
|
|
283
|
+
let y = 78;
|
|
284
|
+
for (const g of groups) {
|
|
285
|
+
body.push(`<text x="${MARGIN}" y="${y}" class="group">${esc(g.name)}</text>`);
|
|
286
|
+
const top = y + GROUP_HEAD_H;
|
|
287
|
+
const rows = Math.ceil(g.items.length / columns);
|
|
288
|
+
g.items.forEach(({ label, swatch }, i) => {
|
|
289
|
+
const x = MARGIN + i % columns * 164;
|
|
290
|
+
const cy = top + Math.floor(i / columns) * 94;
|
|
291
|
+
body.push(`<g transform="translate(${x},${cy})">`, `<rect width="${CELL_W}" height="${SWATCH_H}" rx="6" fill="${esc(swatch.hex)}" class="chip"/>`, `<text x="0" y="62" class="leaf">${esc(label)}</text>`, `<text x="0" y="75" class="hex">${esc(swatch.hex.toUpperCase())}</text>`, `</g>`);
|
|
292
|
+
});
|
|
293
|
+
y = top + rows * CELL_H + (rows - 1) * GAP_Y + GROUP_GAP;
|
|
294
|
+
}
|
|
295
|
+
const height = y + MARGIN - GROUP_GAP;
|
|
296
|
+
return [
|
|
297
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" font-family="system-ui, sans-serif">`,
|
|
298
|
+
`<style>`,
|
|
299
|
+
`.bg { fill: #ffffff; }`,
|
|
300
|
+
`.title { fill: #111827; font-size: 20px; font-weight: 700; }`,
|
|
301
|
+
`.group { fill: #374151; font-size: 14px; font-weight: 600; }`,
|
|
302
|
+
`.chip { stroke: rgba(0,0,0,0.15); stroke-width: 1; }`,
|
|
303
|
+
`.leaf { fill: #374151; font-size: 11px; }`,
|
|
304
|
+
`.hex { fill: #9ca3af; font-size: 10px; font-family: ui-monospace, monospace; }`,
|
|
305
|
+
`</style>`,
|
|
306
|
+
`<rect class="bg" width="${width}" height="${height}"/>`,
|
|
307
|
+
`<text x="${MARGIN}" y="52" class="title">${esc(title)}</text>`,
|
|
308
|
+
...body,
|
|
309
|
+
`</svg>`,
|
|
310
|
+
``
|
|
311
|
+
].join("\n");
|
|
312
|
+
}
|
|
313
|
+
//#endregion
|
|
314
|
+
//#region src/index.ts
|
|
315
|
+
/**
|
|
316
|
+
* `@pantoken/swatches` — export Instructure colours as designer swatch palettes.
|
|
317
|
+
*
|
|
318
|
+
* Swatches are a flat reduction of the tokens to named colours, delivered *directly to a designer*
|
|
319
|
+
* to import into their tool. This is not an interchange format (use `@pantoken/dtcg` or
|
|
320
|
+
* `@pantoken/figma` for that) — it's a terminal palette file: Adobe ASE, GIMP `.gpl`, or Sketch
|
|
321
|
+
* `.sketchpalette`.
|
|
322
|
+
*
|
|
323
|
+
* @module
|
|
324
|
+
* @experimental
|
|
325
|
+
*/
|
|
326
|
+
/**
|
|
327
|
+
* The `rebrand` colour swatches (the default palette).
|
|
328
|
+
*
|
|
329
|
+
* @example Encode the ready-made palette to any format
|
|
330
|
+
* ```ts
|
|
331
|
+
* import { writeFileSync } from "node:fs";
|
|
332
|
+
* import { swatches, toAse, toGpl } from "@pantoken/swatches";
|
|
333
|
+
*
|
|
334
|
+
* writeFileSync("instructure.ase", toAse(swatches));
|
|
335
|
+
* writeFileSync("instructure.gpl", toGpl(swatches));
|
|
336
|
+
* ```
|
|
337
|
+
*/
|
|
338
|
+
const swatches = toSwatches(tokens);
|
|
339
|
+
//#endregion
|
|
340
|
+
export { hexToRgb, swatches, toAse, toGpl, toSketchPalette, toSvg, toSwatches };
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pantoken/swatches",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Export Instructure colours as designer swatch palettes: Adobe ASE, GIMP .gpl, and Sketch .sketchpalette.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist"
|
|
8
|
+
],
|
|
9
|
+
"type": "module",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./dist/index.mjs",
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@pantoken/model": "0.1.0",
|
|
19
|
+
"@pantoken/tokens": "0.1.0",
|
|
20
|
+
"@pantoken/utils": "0.1.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^24.13.3",
|
|
24
|
+
"typescript": "^6.0.3",
|
|
25
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@0.2.4",
|
|
26
|
+
"vite-plus": "0.2.4"
|
|
27
|
+
},
|
|
28
|
+
"pantoken": {
|
|
29
|
+
"key": "swatches",
|
|
30
|
+
"kind": "subpath"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "vp pack",
|
|
34
|
+
"dev": "vp pack --watch",
|
|
35
|
+
"test": "vp test",
|
|
36
|
+
"check": "vp check"
|
|
37
|
+
}
|
|
38
|
+
}
|