cleo_design_tokens 0.2.3 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a8ed2bf7065eaea99e6dd21e5af3658571a15e58875f66cf890fc544260d1dc0
4
- data.tar.gz: 4d27e344661369e8d8ba2ab21b4e9d6c07363bf1d8da909e366aeef54bffcd49
3
+ metadata.gz: 0a44c38f546ec1f31af8273066fa31c77538d6e9812225b0da0a763e5622afa6
4
+ data.tar.gz: 4a141717fc6df4973d260296903ce7e0f95525c6f9caee385ddf166877e7b615
5
5
  SHA512:
6
- metadata.gz: f211a9378781015279f633200656d71f3a7606c90ea13d2cb48d82d9e4ace0b54443842c30674c0cc1195b43c918fc74d0830323c6fe68ac3ff18c9f363b644a
7
- data.tar.gz: '078f30070c8c51f98ac88cfbdce950bec4f77abf66a4bdc6933127f9f666787faadd3f7be4aae624cc1b46022ae6a64b732428f3b3363058c17b61b5bab8761d'
6
+ metadata.gz: 9049839cd022f9c7cb925a3c443ccad2e0ae2145d68d463bfc19afc2cc55564e52b496f09c6c9cb93a3835a73d25cc975e3b6c8c078cdaa3afc02901abec67dc
7
+ data.tar.gz: 273d641074a554f32656ff6beabf7e723a19f7dff52506f1a57f7119b4821a3fcf6bbeb76a4d3bf7ec0c10d9edfcdd1bfd520284f1bc6bdcb2e882fde9187f87
@@ -10,7 +10,8 @@ module CleoDesignTokens
10
10
  end
11
11
 
12
12
  def fetch(key)
13
- value = @lookup.fetch(key) { raise UnknownTokenError, "unknown design token: #{key.inspect}" }
13
+ lookup = @lookup.respond_to?(:call) ? @lookup.call : @lookup
14
+ value = lookup.fetch(key) { raise UnknownTokenError, "unknown design token: #{key.inspect}" }
14
15
  # `Hash#fetch` only guards a missing *key* — a key present with a nil
15
16
  # value (malformed data: a primitive leaf with no `$value`) would
16
17
  # otherwise return nil instead of raising.
@@ -0,0 +1,58 @@
1
+ require "thread"
2
+ require_relative "lookup_builder"
3
+
4
+ module CleoDesignTokens
5
+ class Configuration
6
+ Palette = Data.define(:primitives, :semantic)
7
+ CONTEXT_KEY = :cleo_design_tokens_palette
8
+ private_constant :Palette, :CONTEXT_KEY
9
+
10
+ attr_reader :default_palette
11
+
12
+ def initialize(primitives:, semantic:)
13
+ @primitives = primitives
14
+ @semantic = semantic
15
+ @default_palette = build_palette(:current)
16
+ @refresh_palette = nil
17
+ @mutex = Mutex.new
18
+ end
19
+
20
+ def palette
21
+ # Thread#[] is fiber-local, unlike Thread#thread_variable_get.
22
+ Thread.current[CONTEXT_KEY] || default_palette
23
+ end
24
+
25
+ def configure(experience:)
26
+ selected = case experience
27
+ when :current, "current" then nil
28
+ when :refresh, "refresh"
29
+ @mutex.synchronize { @refresh_palette ||= build_palette(:refresh) }
30
+ else
31
+ raise UnknownExperienceError, "unknown experience: #{experience.inspect}; expected :current or :refresh"
32
+ end
33
+
34
+ unless block_given?
35
+ Thread.current[CONTEXT_KEY] = selected
36
+ return nil
37
+ end
38
+
39
+ previous = Thread.current[CONTEXT_KEY]
40
+ begin
41
+ Thread.current[CONTEXT_KEY] = selected
42
+ yield
43
+ ensure
44
+ Thread.current[CONTEXT_KEY] = previous
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def build_palette(experience)
51
+ primitives = LookupBuilder.build_lookup(@primitives, experience: experience).freeze
52
+ Palette.new(
53
+ primitives: primitives,
54
+ semantic: LookupBuilder.build_semantic_lookup(@semantic, experience: experience, primitives: primitives).freeze,
55
+ )
56
+ end
57
+ end
58
+ end
@@ -1,5 +1,6 @@
1
1
  module CleoDesignTokens
2
2
  class UnknownTokenError < KeyError; end
3
3
  class UnknownThemeError < ArgumentError; end
4
+ class UnknownExperienceError < ArgumentError; end
4
5
  class DuplicateTokenError < KeyError; end
5
6
  end
@@ -9,14 +9,33 @@ module CleoDesignTokens
9
9
  module LookupBuilder
10
10
  # Flattens one tree of plain (theme-free) leaves — e.g. the parsed
11
11
  # `primitives.json` — into a key -> hex lookup.
12
- def self.build_lookup(tree)
13
- flatten_tree(tree) { |leaf| leaf["$value"].freeze }
12
+ def self.build_lookup(tree, experience: :current)
13
+ flatten_tree(tree) do |leaf, key|
14
+ value = experience_leaf(leaf, experience, key)["$value"]
15
+ unless value.is_a?(String)
16
+ raise UnknownExperienceError, "design token #{key.inspect} has no primitive value for experience #{experience.inspect}"
17
+ end
18
+ value.freeze
19
+ end
14
20
  end
15
21
 
16
22
  # Flattens `semantic.json` into a key -> SemanticEntry lookup, one entry
17
23
  # 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) }
24
+ def self.build_semantic_lookup(tree, experience: :current, primitives: nil)
25
+ schema = tree["$schema"]
26
+ if tree.key?("$schema") && schema != "cleo-semantic-references/1"
27
+ raise UnknownExperienceError, "unsupported semantic schema: #{schema.inspect}"
28
+ end
29
+ references = schema == "cleo-semantic-references/1"
30
+ flatten_tree(tree) do |leaf, key|
31
+ snapshots = leaf["$experiences"]
32
+ if references && leaf.key?("$experiences") && !snapshots.is_a?(Hash)
33
+ raise UnknownExperienceError, "invalid semantic experiences for #{key.inspect}"
34
+ end
35
+ shared = references && (experience.to_s == "current" || !snapshots.is_a?(Hash) || !snapshots.key?("cleoRefresh"))
36
+ snapshot = shared ? leaf : experience_leaf(leaf, experience, key)
37
+ semantic_entry(snapshot, experience, key, primitives: primitives, references: references)
38
+ end
20
39
  end
21
40
 
22
41
  # Walks `tree`, turning each leaf into a lookup entry via the block —
@@ -43,20 +62,59 @@ module CleoDesignTokens
43
62
  end
44
63
 
45
64
  origins[key] = path
46
- lookup[key] = yield(leaf)
65
+ lookup[key] = yield(leaf, key)
47
66
  end
48
67
  lookup
49
68
  end
50
69
  private_class_method :flatten_tree
51
70
 
52
- def self.semantic_entry(leaf)
71
+ def self.experience_leaf(leaf, experience, key)
72
+ return leaf if experience.to_s == "current"
73
+ unless experience.to_s == "refresh"
74
+ raise UnknownExperienceError, "unknown experience: #{experience.inspect}; expected :current or :refresh"
75
+ end
76
+
77
+ snapshots = leaf["$experiences"]
78
+ snapshot = snapshots["cleoRefresh"] if snapshots.is_a?(Hash)
79
+ unless snapshot.is_a?(Hash)
80
+ raise UnknownExperienceError, "experience :refresh is unavailable for design token #{key.inspect}; sync the Refresh colour data first"
81
+ end
82
+ snapshot
83
+ end
84
+ private_class_method :experience_leaf
85
+
86
+ def self.semantic_entry(leaf, experience, key, primitives:, references:)
53
87
  themes = {}
54
- (leaf["$themes"] || {}).each { |theme, override| themes[theme] = override["$value"].freeze }
88
+ raw_themes = leaf["$themes"] || {}
89
+ unless raw_themes.is_a?(Hash)
90
+ raise UnknownExperienceError, "design token #{key.inspect} has invalid themes for experience #{experience.inspect}"
91
+ end
92
+ raw_themes.each do |theme, override|
93
+ value = override.is_a?(Hash) ? semantic_value(override, key, primitives: primitives, references: references) : nil
94
+ unless value.is_a?(String)
95
+ raise UnknownExperienceError, "design token #{key.inspect} has no value for theme #{theme.inspect} in experience #{experience.inspect}"
96
+ end
97
+ themes[theme] = value.freeze
98
+ end
99
+ value = semantic_value(leaf, key, primitives: primitives, references: references)
100
+ if (leaf.key?("$value") && !value.is_a?(String)) || (value.nil? && themes.empty?)
101
+ raise UnknownExperienceError, "design token #{key.inspect} has no semantic values for experience #{experience.inspect}"
102
+ end
55
103
 
56
104
  # `Data#new` freezes the instance itself; the Hash inside it still
57
105
  # needs its own `.freeze` — freezing is shallow.
58
- SemanticEntry.new(leaf["$value"]&.freeze, themes.freeze)
106
+ SemanticEntry.new(value&.freeze, themes.freeze)
59
107
  end
60
108
  private_class_method :semantic_entry
109
+
110
+ def self.semantic_value(binding, key, primitives:, references:)
111
+ return binding["$value"] unless references && binding.key?("$ref")
112
+ ref = binding["$ref"]
113
+ if !ref.is_a?(String) || !primitives&.key?(ref) || binding.key?("$value")
114
+ raise UnknownExperienceError, "invalid primitive reference #{ref.inspect} for #{key.inspect}"
115
+ end
116
+ primitives.fetch(ref)
117
+ end
118
+ private_class_method :semantic_value
61
119
  end
62
120
  end
@@ -16,7 +16,8 @@ module CleoDesignTokens
16
16
  theme = theme.to_s
17
17
  raise UnknownThemeError, "unknown theme: #{theme.inspect}" unless THEMES.include?(theme)
18
18
 
19
- entry = @lookup.fetch(key) { raise UnknownTokenError, "unknown design token: #{key.inspect}" }
19
+ lookup = @lookup.respond_to?(:call) ? @lookup.call : @lookup
20
+ entry = lookup.fetch(key) { raise UnknownTokenError, "unknown design token: #{key.inspect}" }
20
21
  value = resolve(entry, theme)
21
22
  if value.nil?
22
23
  raise UnknownTokenError,
@@ -8,6 +8,7 @@ require_relative "cleo_design_tokens/version"
8
8
  require_relative "cleo_design_tokens/lookup_builder"
9
9
  require_relative "cleo_design_tokens/bucket"
10
10
  require_relative "cleo_design_tokens/semantic_bucket"
11
+ require_relative "cleo_design_tokens/configuration"
11
12
 
12
13
  # Reads Cleo's canonical colour design tokens (packages/tokens/tokens/color)
13
14
  # into two frozen lookups, namespaced by token type then layer, with the
@@ -27,8 +28,13 @@ module CleoDesignTokens
27
28
  # Built at load, into constants — not `@lookup ||=`, which would race under
28
29
  # concurrent access. Frozen from the moment the gem loads, so every reader
29
30
  # in every thread sees the same immutable lookup.
30
- PRIMITIVES_LOOKUP = LookupBuilder.build_lookup(JSON.parse(File.read(PRIMITIVES_PATH))).freeze
31
- SEMANTIC_LOOKUP = LookupBuilder.build_semantic_lookup(JSON.parse(File.read(SEMANTIC_PATH))).freeze
31
+ CONFIGURATION = Configuration.new(
32
+ primitives: JSON.parse(File.read(PRIMITIVES_PATH)),
33
+ semantic: JSON.parse(File.read(SEMANTIC_PATH)),
34
+ )
35
+ private_constant :CONFIGURATION
36
+ PRIMITIVES_LOOKUP = CONFIGURATION.default_palette.primitives
37
+ SEMANTIC_LOOKUP = CONFIGURATION.default_palette.semantic
32
38
 
33
39
  # Namespaced by token type (`colors`), then by layer (`primitives`,
34
40
  # `semantic`) — lowercase accessors, not `CleoDesignTokens::Colors::
@@ -40,12 +46,19 @@ module CleoDesignTokens
40
46
  # no `.primitives` method. Plural `Colors`, not `Color` — an instance
41
47
  # holds both buckets for the `colors` token type, not a single colour.
42
48
  Colors = Data.define(:primitives, :semantic)
43
- COLORS = Colors.new(primitives: Bucket.new(PRIMITIVES_LOOKUP), semantic: SemanticBucket.new(SEMANTIC_LOOKUP))
49
+ COLORS = Colors.new(
50
+ primitives: Bucket.new(-> { CONFIGURATION.palette.primitives }),
51
+ semantic: SemanticBucket.new(-> { CONFIGURATION.palette.semantic }),
52
+ )
44
53
 
45
54
  def self.colors
46
55
  COLORS
47
56
  end
48
57
 
58
+ def self.configure(experience:, &block)
59
+ CONFIGURATION.configure(experience: experience, &block)
60
+ end
61
+
49
62
  # Spacing is code-owned, not generated from Figma. Without a unit it is a
50
63
  # numeric React Native-ready pixel value; CSS callers opt into a unit.
51
64
  def self.spacing(multiplier, unit: nil)
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meetcleo/design-tokens",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "description": "Canonical source of truth for Cleo's design tokens (DTCG JSON), plus a TypeScript reader.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",