cleo_design_tokens 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.
- checksums.yaml +7 -0
- data/lib/cleo_design_tokens/bucket.rb +22 -0
- data/lib/cleo_design_tokens/errors.rb +5 -0
- data/lib/cleo_design_tokens/lookup_builder.rb +62 -0
- data/lib/cleo_design_tokens/semantic_bucket.rb +39 -0
- data/lib/cleo_design_tokens/semantic_entry.rb +7 -0
- data/lib/cleo_design_tokens/tree_walker.rb +28 -0
- data/lib/cleo_design_tokens/version.rb +9 -0
- data/lib/cleo_design_tokens.rb +45 -0
- data/package.json +41 -0
- data/tokens/color/primitives.json +450 -0
- data/tokens/color/semantic.json +4394 -0
- metadata +55 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 1c372b6959061b875faa56e38b0f8133d1784fb6dfdcde51506703d12502f0f8
|
|
4
|
+
data.tar.gz: b2ed96b40cbb505dee5238937ad7ae8ea515270ec3538017c0347dda2bfacb45
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 3f2761c8357487e4652bf8e590ea04966692c30cde64113e31fc4c1dabfc5c456666a35e37b454f767eaa8ca4db7f76e34290cb24afcb88eea6f389029fbf7cd
|
|
7
|
+
data.tar.gz: 0f8939327b3e5c9544f5dab0183fb253b2c66c9b972496d8668e70b2d33f935b324d1a1bdc2e0fd9f270a60d19404c7f24833257017f32216529b1afabb4b0f5
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require_relative "errors"
|
|
2
|
+
|
|
3
|
+
module CleoDesignTokens
|
|
4
|
+
# Wraps one frozen lookup with a single-argument `fetch`. Exposed so tests
|
|
5
|
+
# can build one over a fixture lookup, exactly like the module's own
|
|
6
|
+
# `colors.primitives` below.
|
|
7
|
+
class Bucket
|
|
8
|
+
def initialize(lookup)
|
|
9
|
+
@lookup = lookup
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def fetch(key)
|
|
13
|
+
value = @lookup.fetch(key) { raise UnknownTokenError, "unknown design token: #{key.inspect}" }
|
|
14
|
+
# `Hash#fetch` only guards a missing *key* — a key present with a nil
|
|
15
|
+
# value (malformed data: a primitive leaf with no `$value`) would
|
|
16
|
+
# otherwise return nil instead of raising.
|
|
17
|
+
raise UnknownTokenError, "design token #{key.inspect} has no value" if value.nil?
|
|
18
|
+
|
|
19
|
+
value
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
require_relative "tree_walker"
|
|
2
|
+
require_relative "semantic_entry"
|
|
3
|
+
require_relative "errors"
|
|
4
|
+
|
|
5
|
+
module CleoDesignTokens
|
|
6
|
+
# Flattens token trees into frozen key -> value lookups, using TreeWalker.
|
|
7
|
+
# Exposed as module functions, rather than baked into module load, so the
|
|
8
|
+
# collision case is testable against fixtures.
|
|
9
|
+
module LookupBuilder
|
|
10
|
+
# Flattens one tree of plain (theme-free) leaves — e.g. the parsed
|
|
11
|
+
# `primitives.json` — into a key -> hex lookup.
|
|
12
|
+
def self.build_lookup(tree)
|
|
13
|
+
flatten_tree(tree) { |leaf| leaf["$value"].freeze }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Flattens `semantic.json` into a key -> SemanticEntry lookup, one entry
|
|
17
|
+
# per role — the theme axis lives inside the entry, not in the key.
|
|
18
|
+
def self.build_semantic_lookup(tree)
|
|
19
|
+
flatten_tree(tree) { |leaf| semantic_entry(leaf) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Walks `tree`, turning each leaf into a lookup entry via the block —
|
|
23
|
+
# the only thing that differs between the two builders above is how a
|
|
24
|
+
# leaf becomes a value, so that's the only thing left as a parameter.
|
|
25
|
+
#
|
|
26
|
+
# Fails loudly on a collision (two paths normalising to the same key)
|
|
27
|
+
# rather than letting one silently win, shared here so neither builder
|
|
28
|
+
# can drift on that guarantee. `origins` maps key -> the *path array*
|
|
29
|
+
# that produced it, not the key itself — two different paths can
|
|
30
|
+
# normalise to the same key (e.g. a segment that itself contains a
|
|
31
|
+
# literal `.`), and storing the path (rather than re-deriving the
|
|
32
|
+
# identical string) is what lets the error below name both distinctly
|
|
33
|
+
# instead of printing the same string twice.
|
|
34
|
+
def self.flatten_tree(tree)
|
|
35
|
+
lookup = {}
|
|
36
|
+
origins = {}
|
|
37
|
+
TreeWalker.walk(tree) do |path, leaf|
|
|
38
|
+
key = path.join(".")
|
|
39
|
+
if origins.key?(key)
|
|
40
|
+
raise DuplicateTokenError,
|
|
41
|
+
"duplicate design token #{key.inspect}: defined at both " \
|
|
42
|
+
"#{origins.fetch(key).inspect} and #{path.inspect}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
origins[key] = path
|
|
46
|
+
lookup[key] = yield(leaf)
|
|
47
|
+
end
|
|
48
|
+
lookup
|
|
49
|
+
end
|
|
50
|
+
private_class_method :flatten_tree
|
|
51
|
+
|
|
52
|
+
def self.semantic_entry(leaf)
|
|
53
|
+
themes = {}
|
|
54
|
+
(leaf["$themes"] || {}).each { |theme, override| themes[theme] = override["$value"].freeze }
|
|
55
|
+
|
|
56
|
+
# `Data#new` freezes the instance itself; the Hash inside it still
|
|
57
|
+
# needs its own `.freeze` — freezing is shallow.
|
|
58
|
+
SemanticEntry.new(leaf["$value"]&.freeze, themes.freeze)
|
|
59
|
+
end
|
|
60
|
+
private_class_method :semantic_entry
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
require_relative "errors"
|
|
2
|
+
|
|
3
|
+
module CleoDesignTokens
|
|
4
|
+
# `theme:` defaults to `"base"` — `$themes` never carries a `"base"` key
|
|
5
|
+
# (Base isn't an override of itself), so the default and an explicit
|
|
6
|
+
# `theme: :base` resolve identically with no special-casing.
|
|
7
|
+
class SemanticBucket
|
|
8
|
+
THEMES = %w[base chat roast hype].freeze
|
|
9
|
+
private_constant :THEMES
|
|
10
|
+
|
|
11
|
+
def initialize(lookup)
|
|
12
|
+
@lookup = lookup
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def fetch(key, theme: "base")
|
|
16
|
+
theme = theme.to_s
|
|
17
|
+
raise UnknownThemeError, "unknown theme: #{theme.inspect}" unless THEMES.include?(theme)
|
|
18
|
+
|
|
19
|
+
entry = @lookup.fetch(key) { raise UnknownTokenError, "unknown design token: #{key.inspect}" }
|
|
20
|
+
value = resolve(entry, theme)
|
|
21
|
+
if value.nil?
|
|
22
|
+
raise UnknownTokenError,
|
|
23
|
+
"design token #{key.inspect} has no Base value and no override for theme #{theme.inspect}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
value
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# What a caller gets for (role, theme): the theme's override when it has
|
|
32
|
+
# one, otherwise the Base value — mirrors transform-core.mjs's
|
|
33
|
+
# `resolveTheme` exactly. Returns nil when neither exists, which is what
|
|
34
|
+
# makes `fetch` raise for the Base-less roles.
|
|
35
|
+
def resolve(entry, theme)
|
|
36
|
+
entry.themes[theme] || entry.value
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
module CleoDesignTokens
|
|
2
|
+
# One semantic role: its Base value (nil when the role only exists under a
|
|
3
|
+
# theme) plus whatever theme overrides genuinely differ from Base. `Data`,
|
|
4
|
+
# not a `Struct` — this is a fixed value, never mutated after it's built,
|
|
5
|
+
# and `Data` instances are frozen on construction with no extra `.freeze`.
|
|
6
|
+
SemanticEntry = Data.define(:value, :themes)
|
|
7
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module CleoDesignTokens
|
|
2
|
+
# Walks a token tree, skipping `$`-prefixed keys, yielding each leaf's path
|
|
3
|
+
# and node to the block. Keys are the JSON path, dot-joined — the files
|
|
4
|
+
# carry no `color.primitives`/`color.semantic` wrapper to strip, and
|
|
5
|
+
# segments are already normalised on disk, so there's nothing left for a
|
|
6
|
+
# caller to do to a path.
|
|
7
|
+
module TreeWalker
|
|
8
|
+
# A leaf is `{ "$type" => "color", ... }` — matches transform-core.mjs's
|
|
9
|
+
# `isLeaf` exactly. A role can carry `$themes` and no `$value` at all
|
|
10
|
+
# (defined only under a theme, missing from Base in Figma), so `$value`
|
|
11
|
+
# presence is never part of this check.
|
|
12
|
+
def self.leaf?(node)
|
|
13
|
+
node.is_a?(Hash) && node["$type"] == "color"
|
|
14
|
+
end
|
|
15
|
+
private_class_method :leaf?
|
|
16
|
+
|
|
17
|
+
def self.walk(node, path = [], &on_leaf)
|
|
18
|
+
return unless node.is_a?(Hash)
|
|
19
|
+
return on_leaf.call(path, node) if leaf?(node)
|
|
20
|
+
|
|
21
|
+
node.each do |child_key, child_value|
|
|
22
|
+
next if child_key.start_with?("$")
|
|
23
|
+
|
|
24
|
+
walk(child_value, path + [child_key], &on_leaf)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
module CleoDesignTokens
|
|
4
|
+
# `package.json`'s `version` is the single source of truth — npm requires
|
|
5
|
+
# it to be a literal, so it can't defer to us. The gemspec ships
|
|
6
|
+
# `package.json` alongside `lib/` (see `spec.files`) specifically so this
|
|
7
|
+
# read works from an installed gem too, not just this source checkout.
|
|
8
|
+
VERSION = JSON.parse(File.read(File.expand_path("../../package.json", __dir__))).fetch("version")
|
|
9
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
# Only what this file references directly. `lookup_builder` pulls in
|
|
4
|
+
# `tree_walker`/`semantic_entry`/`errors` itself, and `bucket`/
|
|
5
|
+
# `semantic_bucket` each pull in `errors` themselves — Ruby's `require` is
|
|
6
|
+
# idempotent, so re-requiring them here would just be noise.
|
|
7
|
+
require_relative "cleo_design_tokens/version"
|
|
8
|
+
require_relative "cleo_design_tokens/lookup_builder"
|
|
9
|
+
require_relative "cleo_design_tokens/bucket"
|
|
10
|
+
require_relative "cleo_design_tokens/semantic_bucket"
|
|
11
|
+
|
|
12
|
+
# Reads Cleo's canonical colour design tokens (packages/tokens/tokens/color)
|
|
13
|
+
# into two frozen lookups, namespaced by token type then layer, with the
|
|
14
|
+
# theme passed alongside the key rather than baked into it.
|
|
15
|
+
#
|
|
16
|
+
# CleoDesignTokens.colors.semantic.fetch("core.content.primary") # => "#47201C"
|
|
17
|
+
# CleoDesignTokens.colors.semantic.fetch("core.content.primary", theme: :roast) # => "#F8F6F2"
|
|
18
|
+
# CleoDesignTokens.colors.primitives.fetch("brown.800") # => "#47201C"
|
|
19
|
+
module CleoDesignTokens
|
|
20
|
+
TOKENS_DIR = File.expand_path("../tokens/color", __dir__)
|
|
21
|
+
PRIMITIVES_PATH = File.join(TOKENS_DIR, "primitives.json")
|
|
22
|
+
SEMANTIC_PATH = File.join(TOKENS_DIR, "semantic.json")
|
|
23
|
+
|
|
24
|
+
# Built at load, into constants — not `@lookup ||=`, which would race under
|
|
25
|
+
# concurrent access. Frozen from the moment the gem loads, so every reader
|
|
26
|
+
# in every thread sees the same immutable lookup.
|
|
27
|
+
PRIMITIVES_LOOKUP = LookupBuilder.build_lookup(JSON.parse(File.read(PRIMITIVES_PATH))).freeze
|
|
28
|
+
SEMANTIC_LOOKUP = LookupBuilder.build_semantic_lookup(JSON.parse(File.read(SEMANTIC_PATH))).freeze
|
|
29
|
+
|
|
30
|
+
# Namespaced by token type (`colors`), then by layer (`primitives`,
|
|
31
|
+
# `semantic`) — lowercase accessors, not `CleoDesignTokens::Colors::
|
|
32
|
+
# Semantic.fetch(...)` module nesting, so the call text stays identical to
|
|
33
|
+
# the TypeScript reader (a colour greps across both repos).
|
|
34
|
+
#
|
|
35
|
+
# `Data`, not a plain Hash: `colors.primitives` needs to be an attribute
|
|
36
|
+
# read (matching the reader's own dot-accessor contract), and a Hash has
|
|
37
|
+
# no `.primitives` method. Plural `Colors`, not `Color` — an instance
|
|
38
|
+
# holds both buckets for the `colors` token type, not a single colour.
|
|
39
|
+
Colors = Data.define(:primitives, :semantic)
|
|
40
|
+
COLORS = Colors.new(primitives: Bucket.new(PRIMITIVES_LOOKUP), semantic: SemanticBucket.new(SEMANTIC_LOOKUP))
|
|
41
|
+
|
|
42
|
+
def self.colors
|
|
43
|
+
COLORS
|
|
44
|
+
end
|
|
45
|
+
end
|
data/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@meetcleo/design-tokens",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Canonical source of truth for Cleo's design tokens (DTCG JSON), plus a TypeScript reader.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/meetcleo/cleonardo.git",
|
|
10
|
+
"directory": "packages/tokens"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/meetcleo/cleonardo/tree/main/packages/tokens",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/meetcleo/cleonardo/issues"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"registry": "https://npm.pkg.github.com"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=24"
|
|
21
|
+
},
|
|
22
|
+
"main": "dist/src/CleoDesignTokens.js",
|
|
23
|
+
"types": "dist/src/CleoDesignTokens.d.ts",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/src/CleoDesignTokens.d.ts",
|
|
27
|
+
"default": "./dist/src/CleoDesignTokens.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"README.md"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"transform": "node scripts/transform.mjs",
|
|
36
|
+
"check": "node scripts/transform.mjs --check",
|
|
37
|
+
"verify": "node scripts/verify-tokens.mjs",
|
|
38
|
+
"build": "tsc -p tsconfig.build.json",
|
|
39
|
+
"prepack": "yarn build"
|
|
40
|
+
}
|
|
41
|
+
}
|