poetry-extract 0.1.4 → 0.1.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.
- checksums.yaml +4 -4
- data/.mcp.json +8 -0
- data/.yard-lint.yml +25 -0
- data/.yard_coverage_all +1 -0
- data/Archspec.rb +11 -0
- data/CHANGELOG.md +8 -0
- data/lib/poetry/extract/compose.rb +2 -3
- data/lib/poetry/extract/derive_tokens.rb +163 -93
- data/lib/poetry/extract/fetch.rb +2 -2
- data/lib/poetry/extract/runner.rb +4 -0
- data/lib/poetry/extract/version.rb +1 -1
- metadata +7 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5109279ef95e4b1b84d54d211de9c092e5ef5853fb543ec13aab67c438212a3c
|
|
4
|
+
data.tar.gz: 5e5518b6a7ceed15536bb7f6998d8c53ddadc6f023cce646118ccd76701e61be
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b1678f0845188edcc2a4e3ca02cd692e2ed883dd32cdd9bdee63fe7e3c5dfbdeffeb1994f06b293036b44a3b97a613b85fe6204e90ea0d93577272251c506b41
|
|
7
|
+
data.tar.gz: 05de37f2f6d66ad3693fd3d471c3f6e5ccfee7befb538d4b973e118383c0761881458f489ab8a9f08735d9e3ebea98de90064b211283b7e57a368872632763c6
|
data/.mcp.json
ADDED
data/.yard-lint.yml
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# yard-lint: the documentation tier YARD's own gates miss - a documented
|
|
2
|
+
# method missing a @param, an @example that does not parse, option tags,
|
|
3
|
+
# tag order, invalid types. Runs as rake yard:lint (in the default task).
|
|
4
|
+
# UndocumentedObjects stays off: yard:coverage gates public objects at its
|
|
5
|
+
# recorded floor, and this validator counts the @api private internals the
|
|
6
|
+
# gems hide on purpose (--hide-api is not honoured here). MissingReturn is
|
|
7
|
+
# off until the @param backlog is cleared; revisit then.
|
|
8
|
+
AllValidators:
|
|
9
|
+
YardOptions:
|
|
10
|
+
- --no-private
|
|
11
|
+
Documentation/UndocumentedObjects:
|
|
12
|
+
Enabled: false
|
|
13
|
+
Documentation/MissingReturn:
|
|
14
|
+
Enabled: false
|
|
15
|
+
Tags/InvalidTypes:
|
|
16
|
+
Enabled: true
|
|
17
|
+
# A **options splat passed through to a control is documented as @param
|
|
18
|
+
# options [Hash], not one @option per key; and @example blocks here are
|
|
19
|
+
# ERB as often as Ruby, which this validator cannot parse.
|
|
20
|
+
Documentation/UndocumentedOptions:
|
|
21
|
+
Enabled: false
|
|
22
|
+
Tags/OptionTags:
|
|
23
|
+
Enabled: false
|
|
24
|
+
Tags/ExampleSyntax:
|
|
25
|
+
Enabled: false
|
data/.yard_coverage_all
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0
|
data/Archspec.rb
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The architecture the family enforces by review, as checks (rake arch:check).
|
|
4
|
+
# extract depends on core alone.
|
|
5
|
+
root "."
|
|
6
|
+
source "lib/**/*.rb"
|
|
7
|
+
|
|
8
|
+
component :lib, in: "lib/**/*.rb"
|
|
9
|
+
|
|
10
|
+
lib.cannot_reference_constants "Poetry::Ui", "Poetry::Charts", "Poetry::Agent", "ApplicationController",
|
|
11
|
+
because: "extract depends on core alone and never names the host"
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.5] - 2026-09-18
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- 43 methods the reference already hid with `@api private` are Ruby-private now: each was called only by its own class or template, so the runtime enforces what the tag only stated. A host that reached one gets a NoMethodError instead of an internal that may change without notice. The tag remains on the internals the family shares between its gems and on whole internal classes.
|
|
8
|
+
- Every class, module and method carries a one-sentence description, private helpers included: `rake yard:coverage:all` measures the whole tree (a tag-only docstring counts as blank) and the committed floor now stands at zero.
|
|
9
|
+
- The palette builder reads its sources, chooses its roles, builds the chart series and writes the hash in separate steps; the oracle parity suite holds every output byte for byte, and two tests pin that a primary already on the right side of its canvas is left alone in the derived mode.
|
|
10
|
+
|
|
3
11
|
## [0.1.4] - 2026-09-15
|
|
4
12
|
|
|
5
13
|
Lockstep release with the family; no changes in this gem.
|
|
@@ -110,15 +110,12 @@ module Poetry
|
|
|
110
110
|
end
|
|
111
111
|
|
|
112
112
|
# A Messages API text content block.
|
|
113
|
-
# @api private
|
|
114
113
|
def text_block(text) = { type: "text", text: text }
|
|
115
114
|
|
|
116
115
|
# A Messages API URL-source image content block.
|
|
117
|
-
# @api private
|
|
118
116
|
def image_block(url) = { type: "image", source: { type: "url", url: url } }
|
|
119
117
|
|
|
120
118
|
# Join the response's text blocks into one string.
|
|
121
|
-
# @api private
|
|
122
119
|
def extract_text(response)
|
|
123
120
|
Array(response["content"]).filter_map { |block| block["text"] if block["type"] == "text" }.join
|
|
124
121
|
end
|
|
@@ -144,6 +141,8 @@ module Poetry
|
|
|
144
141
|
|
|
145
142
|
JSON.parse(response.body)
|
|
146
143
|
end
|
|
144
|
+
|
|
145
|
+
private_class_method :text_block, :image_block, :extract_text
|
|
147
146
|
end
|
|
148
147
|
end
|
|
149
148
|
end
|
|
@@ -23,6 +23,12 @@ module Poetry
|
|
|
23
23
|
module DeriveTokens
|
|
24
24
|
WHITE = { r: 255.0, g: 255.0, b: 255.0 }.freeze
|
|
25
25
|
BLACK = { r: 0.0, g: 0.0, b: 0.0 }.freeze
|
|
26
|
+
# The dark mode canvas.
|
|
27
|
+
DARK_BACKGROUND = { r: 23.0, g: 23.0, b: 21.0 }.freeze
|
|
28
|
+
# The light mode text color.
|
|
29
|
+
LIGHT_FOREGROUND = { r: 10.0, g: 10.0, b: 10.0 }.freeze
|
|
30
|
+
# The dark mode text color.
|
|
31
|
+
DARK_FOREGROUND = { r: 245.0, g: 245.0, b: 244.0 }.freeze
|
|
26
32
|
|
|
27
33
|
SANS_FALLBACKS = %w[ui-sans-serif system-ui sans-serif].freeze
|
|
28
34
|
SERIF_FALLBACKS = %w[ui-serif Georgia serif].freeze
|
|
@@ -48,6 +54,7 @@ module Poetry
|
|
|
48
54
|
].join("\n")
|
|
49
55
|
end
|
|
50
56
|
|
|
57
|
+
# The vanilla CSS token sheet for a domain: a :root block and a .dark block from the brand and styleguide.
|
|
51
58
|
def derive_css_variables(domain, brand = nil, styleguide = nil)
|
|
52
59
|
root_body, dark_body = bodies(brand, styleguide)
|
|
53
60
|
[
|
|
@@ -57,6 +64,7 @@ module Poetry
|
|
|
57
64
|
].join("\n")
|
|
58
65
|
end
|
|
59
66
|
|
|
67
|
+
# The light and dark token bodies: each palette's lines plus the fonts, radius, shadows, tracking and spacing.
|
|
60
68
|
def bodies(brand, styleguide)
|
|
61
69
|
light = build_palette(brand, styleguide, "light")
|
|
62
70
|
dark = build_palette(brand, styleguide, "dark")
|
|
@@ -73,6 +81,7 @@ module Poetry
|
|
|
73
81
|
|
|
74
82
|
# --- Color utilities -------------------------------------------------
|
|
75
83
|
|
|
84
|
+
# An RGB hash from a hex color in three, six or eight digits; nil for anything else.
|
|
76
85
|
def parse_hex(hex)
|
|
77
86
|
return nil unless hex
|
|
78
87
|
|
|
@@ -88,17 +97,20 @@ module Poetry
|
|
|
88
97
|
end
|
|
89
98
|
end
|
|
90
99
|
|
|
100
|
+
# An RGB hash as a six-digit hex color.
|
|
91
101
|
def to_hex(color)
|
|
92
102
|
hx = ->(n) { format("%02x", n.round.clamp(0, 255)) }
|
|
93
103
|
"##{hx.call(color[:r])}#{hx.call(color[:g])}#{hx.call(color[:b])}"
|
|
94
104
|
end
|
|
95
105
|
|
|
106
|
+
# The linear mix of two RGB colors by an amount from zero to one.
|
|
96
107
|
def mix(from, to, amount)
|
|
97
108
|
{ r: from[:r] + ((to[:r] - from[:r]) * amount),
|
|
98
109
|
g: from[:g] + ((to[:g] - from[:g]) * amount),
|
|
99
110
|
b: from[:b] + ((to[:b] - from[:b]) * amount) }
|
|
100
111
|
end
|
|
101
112
|
|
|
113
|
+
# The relative luminance of an RGB color, per the WCAG formula.
|
|
102
114
|
def luminance(color)
|
|
103
115
|
f = lambda do |c|
|
|
104
116
|
s = c / 255.0
|
|
@@ -107,10 +119,13 @@ module Poetry
|
|
|
107
119
|
(0.2126 * f.call(color[:r])) + (0.7152 * f.call(color[:g])) + (0.0722 * f.call(color[:b]))
|
|
108
120
|
end
|
|
109
121
|
|
|
122
|
+
# Whether a color is dark enough to want a light foreground.
|
|
110
123
|
def dark?(color) = luminance(color) < 0.5
|
|
111
124
|
|
|
125
|
+
# The foreground that reads on a background: white on dark, near-black on light.
|
|
112
126
|
def readable(background) = dark?(background) ? "#ffffff" : "#0a0a0a"
|
|
113
127
|
|
|
128
|
+
# A number held between min and max.
|
|
114
129
|
def clamp(number, min, max) = number.clamp(min, max)
|
|
115
130
|
|
|
116
131
|
# JS parseFloat: leading number or nil - "5.9%" => 5.9, "abc" => nil.
|
|
@@ -132,6 +147,7 @@ module Poetry
|
|
|
132
147
|
js_to_fixed(number, decimals).sub(/\.?0+\z/, "")
|
|
133
148
|
end
|
|
134
149
|
|
|
150
|
+
# An RGB hash as an hsl() token with one decimal on each channel.
|
|
135
151
|
def rgb_to_hsl_token(color)
|
|
136
152
|
rr = color[:r] / 255.0
|
|
137
153
|
gg = color[:g] / 255.0
|
|
@@ -156,6 +172,7 @@ module Poetry
|
|
|
156
172
|
"hsl(#{format_number(h, 1)} #{format_number(s * 100, 1)}% #{format_number(l * 100, 1)}%)"
|
|
157
173
|
end
|
|
158
174
|
|
|
175
|
+
# A CSS color as an hsl() token: hex directly, hsl and rgb functions through their parsers; nil otherwise.
|
|
159
176
|
def normalize_css_color(raw)
|
|
160
177
|
return nil unless raw
|
|
161
178
|
|
|
@@ -166,6 +183,7 @@ module Poetry
|
|
|
166
183
|
normalize_hsl(value) || normalize_rgb(value)
|
|
167
184
|
end
|
|
168
185
|
|
|
186
|
+
# An hsl() or hsla() string as an hsl() token with its alpha, or nil when it does not parse.
|
|
169
187
|
def normalize_hsl(value)
|
|
170
188
|
m = value.match(/\Ahsla?\((.+)\)\z/i)
|
|
171
189
|
return nil unless m
|
|
@@ -182,6 +200,7 @@ module Poetry
|
|
|
182
200
|
opacity: alpha && clamp(alpha, 0, 1) }.compact
|
|
183
201
|
end
|
|
184
202
|
|
|
203
|
+
# An rgb() or rgba() string as an hsl() token with its alpha, or nil when it does not parse.
|
|
185
204
|
def normalize_rgb(value)
|
|
186
205
|
m = value.match(/\Argba?\((.+)\)\z/i)
|
|
187
206
|
return nil unless m
|
|
@@ -199,12 +218,14 @@ module Poetry
|
|
|
199
218
|
opacity: alpha && clamp(alpha, 0, 1) }.compact
|
|
200
219
|
end
|
|
201
220
|
|
|
221
|
+
# A color function's body split into its channels and the slash alpha, when one is written.
|
|
202
222
|
def split_channels(body)
|
|
203
223
|
body.include?("/") ? body.split("/", 2) : [body, nil]
|
|
204
224
|
end
|
|
205
225
|
|
|
206
226
|
# --- Length utilities ------------------------------------------------
|
|
207
227
|
|
|
228
|
+
# A length in pixels from a px, rem or em value; nil when it is not a length.
|
|
208
229
|
def to_px(value)
|
|
209
230
|
return nil unless value
|
|
210
231
|
|
|
@@ -216,18 +237,21 @@ module Poetry
|
|
|
216
237
|
%w[rem em].include?(unit) ? n * 16 : n
|
|
217
238
|
end
|
|
218
239
|
|
|
240
|
+
# The positive pixel lengths in a space-separated value.
|
|
219
241
|
def to_px_list(value)
|
|
220
242
|
return [] unless value
|
|
221
243
|
|
|
222
244
|
value.to_s.strip.split(/\s+/).filter_map { |part| to_px(part) }.select(&:positive?)
|
|
223
245
|
end
|
|
224
246
|
|
|
247
|
+
# A pixel length as a rem string with trailing zeros trimmed.
|
|
225
248
|
def px_to_rem(pixels)
|
|
226
249
|
"#{js_to_fixed(pixels / 16.0, 4).sub(/\.?0+\z/, "")}rem"
|
|
227
250
|
end
|
|
228
251
|
|
|
229
252
|
# --- Font utilities --------------------------------------------------
|
|
230
253
|
|
|
254
|
+
# A font family name quoted when it carries characters a bare CSS identifier cannot.
|
|
231
255
|
def quote_if_needed(name)
|
|
232
256
|
trimmed = name.strip.gsub(/\A["']|["']\z/, "")
|
|
233
257
|
return trimmed if trimmed.empty?
|
|
@@ -236,6 +260,7 @@ module Poetry
|
|
|
236
260
|
trimmed
|
|
237
261
|
end
|
|
238
262
|
|
|
263
|
+
# A font stack from the primary family, its fallbacks and the generic family, deduplicated case-insensitively.
|
|
239
264
|
def build_font_stack(primary, fallbacks, generic)
|
|
240
265
|
seen = {}
|
|
241
266
|
stack = []
|
|
@@ -255,6 +280,7 @@ module Poetry
|
|
|
255
280
|
stack.join(", ")
|
|
256
281
|
end
|
|
257
282
|
|
|
283
|
+
# A font family's class, sans, serif, mono or unknown, from the font links or the family name.
|
|
258
284
|
def classify_family(family, font_links)
|
|
259
285
|
return "unknown" unless family
|
|
260
286
|
|
|
@@ -274,6 +300,7 @@ module Poetry
|
|
|
274
300
|
"unknown"
|
|
275
301
|
end
|
|
276
302
|
|
|
303
|
+
# The first font link whose category reads as monospace, or nil.
|
|
277
304
|
def find_mono_family(font_links)
|
|
278
305
|
font_links&.each do |name, link|
|
|
279
306
|
return name if link["category"]&.downcase&.include?("mono")
|
|
@@ -281,6 +308,7 @@ module Poetry
|
|
|
281
308
|
nil
|
|
282
309
|
end
|
|
283
310
|
|
|
311
|
+
# The first font link whose category reads as serif, skipping the excluded names, or nil.
|
|
284
312
|
def find_serif_family(font_links, exclude)
|
|
285
313
|
font_links&.each do |name, link|
|
|
286
314
|
next if exclude.include?(name.downcase)
|
|
@@ -291,6 +319,7 @@ module Poetry
|
|
|
291
319
|
nil
|
|
292
320
|
end
|
|
293
321
|
|
|
322
|
+
# The sans, serif and mono stacks for a styleguide, from its body and heading fonts and its font links.
|
|
294
323
|
def pick_fonts(styleguide)
|
|
295
324
|
body = styleguide&.dig("typography", "p")
|
|
296
325
|
h1 = styleguide&.dig("typography", "headings", "h1")
|
|
@@ -324,6 +353,7 @@ module Poetry
|
|
|
324
353
|
|
|
325
354
|
# --- Radius / shadows / spacing --------------------------------------
|
|
326
355
|
|
|
356
|
+
# The radius token from a styleguide's card or button, clamped between two and sixteen pixels.
|
|
327
357
|
def pick_radius(styleguide)
|
|
328
358
|
card_radius = to_px(styleguide&.dig("components", "card", "borderRadius"))
|
|
329
359
|
button_radius = to_px(styleguide&.dig("components", "button", "primary", "borderRadius"))
|
|
@@ -333,11 +363,13 @@ module Poetry
|
|
|
333
363
|
px_to_rem(px.clamp(2, 16))
|
|
334
364
|
end
|
|
335
365
|
|
|
366
|
+
# A length as a px token, or the fallback when it is not a length.
|
|
336
367
|
def normalize_px_token(value, fallback)
|
|
337
368
|
px = to_px(value)
|
|
338
369
|
px.nil? ? fallback : "#{format_number(px, 2)}px"
|
|
339
370
|
end
|
|
340
371
|
|
|
372
|
+
# A box-shadow value split into its comma-separated layers, commas inside functions kept.
|
|
341
373
|
def split_shadow_layers(value)
|
|
342
374
|
layers = []
|
|
343
375
|
depth = 0
|
|
@@ -354,10 +386,12 @@ module Poetry
|
|
|
354
386
|
layers.reject(&:empty?)
|
|
355
387
|
end
|
|
356
388
|
|
|
389
|
+
# The color function or hex color inside one shadow layer, or nil.
|
|
357
390
|
def find_color_snippet(layer)
|
|
358
391
|
layer[/\b(?:rgba?|hsla?)\([^)]+\)/i] || layer[/#[0-9a-f]{3,8}\b/i]
|
|
359
392
|
end
|
|
360
393
|
|
|
394
|
+
# The first layer of a box-shadow value as offsets, blur, spread, color and opacity; nil when unusable.
|
|
361
395
|
def parse_box_shadow(value)
|
|
362
396
|
return nil if !value || value.strip.downcase == "none"
|
|
363
397
|
|
|
@@ -379,6 +413,7 @@ module Poetry
|
|
|
379
413
|
opacity: color && color[:opacity] }
|
|
380
414
|
end
|
|
381
415
|
|
|
416
|
+
# An hsl() or rgb() token with the opacity written as its alpha.
|
|
382
417
|
def color_with_opacity(color, opacity)
|
|
383
418
|
alpha = format_number(clamp(opacity, 0, 1), 4)
|
|
384
419
|
if (m = color.match(/\Ahsl\((.+)\)\z/i))
|
|
@@ -390,6 +425,7 @@ module Poetry
|
|
|
390
425
|
end
|
|
391
426
|
end
|
|
392
427
|
|
|
428
|
+
# The shadow color when the styleguide gives none: a foreground tint on light, near-black on dark.
|
|
393
429
|
def fallback_shadow_color(palette, mode)
|
|
394
430
|
bg = parse_hex(palette[:background])
|
|
395
431
|
fg = parse_hex(palette[:foreground])
|
|
@@ -399,6 +435,7 @@ module Poetry
|
|
|
399
435
|
fg ? rgb_to_hsl_token(fg) : "hsl(0 0% 5%)"
|
|
400
436
|
end
|
|
401
437
|
|
|
438
|
+
# The shadow token set from one base shadow: the base parts and the size ramp at quiet, main and loud opacities.
|
|
402
439
|
def build_shadow_tokens(base)
|
|
403
440
|
half = base[:opacity] * 0.5
|
|
404
441
|
heavy = [base[:opacity] * 2.5, 0.75].min
|
|
@@ -420,6 +457,7 @@ module Poetry
|
|
|
420
457
|
shadow2xl: "#{first} #{loud}" }
|
|
421
458
|
end
|
|
422
459
|
|
|
460
|
+
# The shadow tokens for a styleguide and palette, from the first parsable shadow among its candidates.
|
|
423
461
|
def pick_shadows(styleguide, palette, mode)
|
|
424
462
|
s = styleguide&.dig("shadows")
|
|
425
463
|
candidates = [
|
|
@@ -440,6 +478,7 @@ module Poetry
|
|
|
440
478
|
)
|
|
441
479
|
end
|
|
442
480
|
|
|
481
|
+
# The median of a list of numbers, nil for an empty one.
|
|
443
482
|
def median(values)
|
|
444
483
|
return nil if values.empty?
|
|
445
484
|
|
|
@@ -450,6 +489,7 @@ module Poetry
|
|
|
450
489
|
(sorted[mid - 1] + sorted[mid]) / 2.0
|
|
451
490
|
end
|
|
452
491
|
|
|
492
|
+
# The spacing base unit implied by a styleguide's element spacing, or nil when none reads.
|
|
453
493
|
def pick_spacing(styleguide)
|
|
454
494
|
sp = styleguide&.dig("elementSpacing")
|
|
455
495
|
divided = lambda do |value, divisor|
|
|
@@ -474,6 +514,7 @@ module Poetry
|
|
|
474
514
|
px_to_rem(safe_unit)
|
|
475
515
|
end
|
|
476
516
|
|
|
517
|
+
# The normal letter spacing as an em token, from the body or heading tracking.
|
|
477
518
|
def pick_tracking_normal(styleguide)
|
|
478
519
|
values = [styleguide&.dig("typography", "p", "letterSpacing"),
|
|
479
520
|
styleguide&.dig("typography", "headings", "h1", "letterSpacing")]
|
|
@@ -491,120 +532,134 @@ module Poetry
|
|
|
491
532
|
|
|
492
533
|
# --- Palette ---------------------------------------------------------
|
|
493
534
|
|
|
535
|
+
# The semantic palette for one mode from the brand colors and the
|
|
536
|
+
# styleguide: the source colors read once, the roles taken directly
|
|
537
|
+
# when the styleguide describes this mode and derived when it
|
|
538
|
+
# describes the other, then the palette written out as hex.
|
|
494
539
|
def build_palette(brand, styleguide, mode)
|
|
495
|
-
|
|
496
|
-
source_is_light =
|
|
540
|
+
sources = palette_sources(brand, styleguide)
|
|
541
|
+
source_is_light = styleguide&.dig("mode") != "dark"
|
|
497
542
|
direct = mode == (source_is_light ? "light" : "dark")
|
|
543
|
+
roles = direct ? direct_roles(sources, mode) : derived_roles(sources, mode)
|
|
544
|
+
palette_hash(roles, mode, chart_series(roles, sources[:brand_colors]))
|
|
545
|
+
end
|
|
498
546
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
)
|
|
505
|
-
|
|
506
|
-
sg_bg = parse_hex(styleguide&.dig("colors", "background"))
|
|
507
|
-
sg_fg = parse_hex(styleguide&.dig("colors", "text"))
|
|
508
|
-
sg_accent = parse_hex(styleguide&.dig("colors", "accent"))
|
|
509
|
-
|
|
510
|
-
btn_primary = styleguide&.dig("components", "button", "primary")
|
|
511
|
-
btn_secondary = styleguide&.dig("components", "button", "secondary")
|
|
512
|
-
btn_link = styleguide&.dig("components", "button", "link")
|
|
547
|
+
# The colors the brand and the styleguide supply, parsed, with the
|
|
548
|
+
# source accent resolved: the styleguide's, else the link button's,
|
|
549
|
+
# else a brand color named as one, else the second brand color.
|
|
550
|
+
def palette_sources(brand, styleguide)
|
|
551
|
+
colors = brand&.dig("colors") || []
|
|
552
|
+
button = ->(kind, key) { parse_hex(styleguide&.dig("components", "button", kind, key)) }
|
|
513
553
|
card = styleguide&.dig("components", "card")
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
554
|
+
named = colors.find { |c| /accent|secondary|highlight|support/i.match?(c["name"] || "") }
|
|
555
|
+
brand_colors = colors.filter_map { |c| parse_hex(c["hex"]) }
|
|
556
|
+
{ brand_colors: brand_colors,
|
|
557
|
+
bg: parse_hex(styleguide&.dig("colors", "background")), fg: parse_hex(styleguide&.dig("colors", "text")),
|
|
558
|
+
primary_bg: button.call("primary", "backgroundColor"), primary_fg: button.call("primary", "color"),
|
|
559
|
+
secondary_bg: button.call("secondary", "backgroundColor"), secondary_fg: button.call("secondary", "color"),
|
|
560
|
+
card_bg: parse_hex(card&.dig("backgroundColor")), card_fg: parse_hex(card&.dig("textColor")),
|
|
561
|
+
card_border: parse_hex(card&.dig("borderColor")),
|
|
562
|
+
button_border: button.call("secondary", "borderColor") || button.call("primary", "borderColor"),
|
|
563
|
+
accent: parse_hex(styleguide&.dig("colors", "accent")) ||
|
|
564
|
+
button.call("link", "backgroundColor") || button.call("link", "color") ||
|
|
565
|
+
parse_hex(named&.dig("hex")) || brand_colors[1] }
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
# The mode's default canvas.
|
|
569
|
+
def mode_background(mode) = mode == "light" ? WHITE : DARK_BACKGROUND
|
|
570
|
+
|
|
571
|
+
# The mode's default text color.
|
|
572
|
+
def mode_foreground(mode) = mode == "light" ? LIGHT_FOREGROUND : DARK_FOREGROUND
|
|
573
|
+
|
|
574
|
+
# The roles when the styleguide describes this very mode: what it
|
|
575
|
+
# says, the gaps filled from the brand and by mixing.
|
|
576
|
+
def direct_roles(sources, mode)
|
|
577
|
+
background = sources[:bg] || mode_background(mode)
|
|
578
|
+
foreground = sources[:fg] || mode_foreground(mode)
|
|
579
|
+
primary = sources[:primary_bg] || sources[:brand_colors][0] || sources[:accent] || foreground
|
|
580
|
+
secondary = sources[:secondary_bg] ||
|
|
581
|
+
(sources[:accent] ? mix(background, sources[:accent], 0.16) : nil) ||
|
|
582
|
+
mix(background, primary, 0.18)
|
|
583
|
+
{ background: background, foreground: foreground, primary: primary,
|
|
584
|
+
primary_foreground: sources[:primary_bg] ? sources[:primary_fg] : nil,
|
|
585
|
+
accent: sources[:accent] || primary, secondary: secondary,
|
|
586
|
+
secondary_foreground: sources[:secondary_bg] ? sources[:secondary_fg] : nil,
|
|
587
|
+
card_bg: sources[:card_bg] || mix(background, foreground, 0.03), card_fg: sources[:card_fg],
|
|
588
|
+
border: sources[:card_border] || sources[:button_border] }
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
# The roles when the styleguide describes the other mode: this mode's
|
|
592
|
+
# own canvas, the primary and accent pulled toward it, the rest mixed.
|
|
593
|
+
def derived_roles(sources, mode)
|
|
594
|
+
background = mode_background(mode)
|
|
595
|
+
foreground = mode_foreground(mode)
|
|
596
|
+
base_primary = sources[:primary_bg] || sources[:brand_colors][0] || sources[:accent] || foreground
|
|
597
|
+
primary = toward_mode(base_primary, mode, 0.3, 0.15)
|
|
598
|
+
accent = toward_mode(sources[:accent] || primary, mode, 0.25, 0.12)
|
|
599
|
+
{ background: background, foreground: foreground, primary: primary,
|
|
600
|
+
primary_foreground: sources[:primary_bg] ? sources[:primary_fg] : nil,
|
|
601
|
+
accent: accent,
|
|
602
|
+
secondary: sources[:secondary_bg] || mix(background, accent, mode == "light" ? 0.16 : 0.24),
|
|
603
|
+
secondary_foreground: sources[:secondary_bg] ? sources[:secondary_fg] : nil,
|
|
604
|
+
card_bg: mix(background, foreground, mode == "light" ? 0.03 : 0.07), card_fg: nil, border: nil }
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
# A color pulled toward the mode's canvas when it would sink into it:
|
|
608
|
+
# lightened on a dark mode when it is dark, darkened on a light mode
|
|
609
|
+
# when it is light, else as it is.
|
|
610
|
+
def toward_mode(color, mode, dark_share, light_share)
|
|
611
|
+
if mode == "dark" && dark?(color) then mix(color, WHITE, dark_share)
|
|
612
|
+
elsif mode == "light" && !dark?(color) then mix(color, BLACK, light_share)
|
|
613
|
+
else color
|
|
563
614
|
end
|
|
615
|
+
end
|
|
564
616
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
sidebar = mix(background, foreground, mode == "light" ? 0.04 : 0.05)
|
|
569
|
-
|
|
570
|
-
chart_candidates = [accent, primary, secondary] + brand_colors
|
|
617
|
+
# Five chart colors: the accent, primary, secondary and brand colors
|
|
618
|
+
# without repeats, padded with tints and shades of the primary.
|
|
619
|
+
def chart_series(roles, brand_colors)
|
|
571
620
|
seen = {}
|
|
572
|
-
|
|
621
|
+
unique = ([roles[:accent], roles[:primary], roles[:secondary]] + brand_colors).select do |color|
|
|
573
622
|
hex = to_hex(color)
|
|
574
623
|
next false if seen[hex]
|
|
575
624
|
|
|
576
625
|
seen[hex] = true
|
|
577
626
|
end
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
627
|
+
primary = roles[:primary]
|
|
628
|
+
[unique[0] || primary, unique[1] || mix(primary, WHITE, 0.3), unique[2] || mix(primary, BLACK, 0.25),
|
|
629
|
+
unique[3] || mix(primary, WHITE, 0.55), unique[4] || mix(primary, BLACK, 0.45)]
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
# The palette as hex: the roles, the muted, border and sidebar tones
|
|
633
|
+
# mixed from the canvas, and the readable foregrounds.
|
|
634
|
+
def palette_hash(roles, mode, chart)
|
|
635
|
+
background, foreground, primary, accent, secondary =
|
|
636
|
+
roles.values_at(:background, :foreground, :primary, :accent, :secondary)
|
|
637
|
+
light = mode == "light"
|
|
638
|
+
border = roles[:border] || mix(background, foreground, light ? 0.12 : 0.2)
|
|
639
|
+
card_fg = roles[:card_fg] || foreground
|
|
640
|
+
primary_fg = roles[:primary_foreground] ? to_hex(roles[:primary_foreground]) : readable(primary)
|
|
641
|
+
secondary_fg = roles[:secondary_foreground] ? to_hex(roles[:secondary_foreground]) : readable(secondary)
|
|
586
642
|
{ background: to_hex(background), foreground: to_hex(foreground),
|
|
587
|
-
card: to_hex(card_bg), card_foreground: to_hex(card_fg
|
|
588
|
-
popover: to_hex(card_bg), popover_foreground: to_hex(card_fg
|
|
589
|
-
primary: to_hex(primary),
|
|
590
|
-
primary_foreground: primary_foreground ? to_hex(primary_foreground) : readable(primary),
|
|
643
|
+
card: to_hex(roles[:card_bg]), card_foreground: to_hex(card_fg),
|
|
644
|
+
popover: to_hex(roles[:card_bg]), popover_foreground: to_hex(card_fg),
|
|
645
|
+
primary: to_hex(primary), primary_foreground: primary_fg,
|
|
591
646
|
secondary: to_hex(secondary),
|
|
592
|
-
secondary_foreground:
|
|
593
|
-
muted: to_hex(
|
|
647
|
+
secondary_foreground: secondary_fg,
|
|
648
|
+
muted: to_hex(mix(background, foreground, light ? 0.05 : 0.1)),
|
|
649
|
+
muted_foreground: to_hex(mix(foreground, background, 0.4)),
|
|
594
650
|
accent: to_hex(accent), accent_foreground: readable(accent),
|
|
595
|
-
destructive:
|
|
596
|
-
|
|
597
|
-
border: to_hex(final_border), input: to_hex(final_border), ring: to_hex(accent),
|
|
651
|
+
destructive: light ? "#dc2626" : "#ef4444", destructive_foreground: "#ffffff",
|
|
652
|
+
border: to_hex(border), input: to_hex(border), ring: to_hex(accent),
|
|
598
653
|
chart: chart.map { |color| to_hex(color) },
|
|
599
|
-
sidebar: to_hex(
|
|
600
|
-
sidebar_primary: to_hex(primary),
|
|
601
|
-
sidebar_primary_foreground: primary_foreground ? to_hex(primary_foreground) : readable(primary),
|
|
654
|
+
sidebar: to_hex(mix(background, foreground, light ? 0.04 : 0.05)), sidebar_foreground: to_hex(foreground),
|
|
655
|
+
sidebar_primary: to_hex(primary), sidebar_primary_foreground: primary_fg,
|
|
602
656
|
sidebar_accent: to_hex(accent), sidebar_accent_foreground: readable(accent),
|
|
603
|
-
sidebar_border: to_hex(
|
|
657
|
+
sidebar_border: to_hex(border), sidebar_ring: to_hex(accent) }
|
|
604
658
|
end
|
|
605
659
|
|
|
606
660
|
# --- Output formatting -----------------------------------------------
|
|
607
661
|
|
|
662
|
+
# A palette as CSS custom property declarations.
|
|
608
663
|
def palette_lines(palette)
|
|
609
664
|
["--background: #{palette[:background]};", "--foreground: #{palette[:foreground]};",
|
|
610
665
|
"--card: #{palette[:card]};", "--card-foreground: #{palette[:card_foreground]};",
|
|
@@ -625,6 +680,7 @@ module Poetry
|
|
|
625
680
|
"--sidebar-border: #{palette[:sidebar_border]};", "--sidebar-ring: #{palette[:sidebar_ring]};"]
|
|
626
681
|
end
|
|
627
682
|
|
|
683
|
+
# The font, radius, shadow, tracking and spacing declarations.
|
|
628
684
|
def non_color_lines(fonts, radius, shadows, tracking_normal = nil, spacing = nil)
|
|
629
685
|
lines = ["--font-sans: #{fonts[:sans]};", "--font-serif: #{fonts[:serif]};",
|
|
630
686
|
"--font-mono: #{fonts[:mono]};", "--radius: #{radius};",
|
|
@@ -640,10 +696,12 @@ module Poetry
|
|
|
640
696
|
lines
|
|
641
697
|
end
|
|
642
698
|
|
|
699
|
+
# The lines indented by two spaces and joined.
|
|
643
700
|
def indent(lines)
|
|
644
701
|
lines.map { |line| " #{line}" }.join("\n")
|
|
645
702
|
end
|
|
646
703
|
|
|
704
|
+
# The inline theme block mapping the color, font and radius tokens onto the framework's names.
|
|
647
705
|
def theme_inline_block
|
|
648
706
|
pairs = %w[background foreground card card-foreground popover popover-foreground
|
|
649
707
|
primary primary-foreground secondary secondary-foreground muted
|
|
@@ -663,6 +721,7 @@ module Poetry
|
|
|
663
721
|
"@theme inline {\n#{indent(lines)}\n}"
|
|
664
722
|
end
|
|
665
723
|
|
|
724
|
+
# The base layer applying the border, outline, background and foreground defaults.
|
|
666
725
|
def layer_base
|
|
667
726
|
<<~CSS.strip
|
|
668
727
|
@layer base {
|
|
@@ -675,6 +734,17 @@ module Poetry
|
|
|
675
734
|
}
|
|
676
735
|
CSS
|
|
677
736
|
end
|
|
737
|
+
|
|
738
|
+
private_class_method :bodies, :parse_hex, :to_hex, :mix, :readable, :js_float, :js_to_fixed, :format_number
|
|
739
|
+
private_class_method :rgb_to_hsl_token, :normalize_css_color, :normalize_hsl, :normalize_rgb, :split_channels
|
|
740
|
+
private_class_method :to_px, :to_px_list, :px_to_rem, :quote_if_needed, :build_font_stack, :classify_family
|
|
741
|
+
private_class_method :find_mono_family, :find_serif_family, :pick_fonts, :pick_radius, :normalize_px_token
|
|
742
|
+
private_class_method :split_shadow_layers, :find_color_snippet, :parse_box_shadow, :color_with_opacity
|
|
743
|
+
private_class_method :fallback_shadow_color, :build_shadow_tokens, :pick_shadows, :pick_spacing
|
|
744
|
+
private_class_method :pick_tracking_normal, :build_palette, :palette_lines, :non_color_lines
|
|
745
|
+
private_class_method :palette_sources, :mode_background, :mode_foreground, :direct_roles, :derived_roles
|
|
746
|
+
private_class_method :toward_mode, :chart_series, :palette_hash
|
|
747
|
+
private_class_method :theme_inline_block, :layer_base
|
|
678
748
|
end
|
|
679
749
|
end
|
|
680
750
|
end
|
data/lib/poetry/extract/fetch.rb
CHANGED
|
@@ -49,7 +49,6 @@ module Poetry
|
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
# The no-client Signals shape: homepage markdown, nothing else.
|
|
52
|
-
# @api private
|
|
53
52
|
def degraded(domain, homepage_fetcher)
|
|
54
53
|
Signals.new(styleguide: nil, brand: nil, screenshot_url: nil,
|
|
55
54
|
markdown: homepage_fetcher.call(domain).to_s)
|
|
@@ -66,7 +65,6 @@ module Poetry
|
|
|
66
65
|
|
|
67
66
|
# SDK responses expose the payload as an accessor; plain-hash fakes
|
|
68
67
|
# (tests, cassettes) work identically.
|
|
69
|
-
# @api private
|
|
70
68
|
def value_of(response, key)
|
|
71
69
|
return response[key.to_s] || response[key] if response.is_a?(Hash)
|
|
72
70
|
|
|
@@ -97,6 +95,8 @@ module Poetry
|
|
|
97
95
|
rescue StandardError
|
|
98
96
|
""
|
|
99
97
|
end
|
|
98
|
+
|
|
99
|
+
private_class_method :degraded, :value_of
|
|
100
100
|
end
|
|
101
101
|
end
|
|
102
102
|
end
|
|
@@ -53,6 +53,7 @@ module Poetry
|
|
|
53
53
|
|
|
54
54
|
# Reduce a URL or bare domain to its lowercase host.
|
|
55
55
|
#
|
|
56
|
+
# @param domain [String] a URL or a bare host
|
|
56
57
|
# @return [String]
|
|
57
58
|
def normalize_domain(domain)
|
|
58
59
|
domain.to_s.strip.downcase
|
|
@@ -62,6 +63,9 @@ module Poetry
|
|
|
62
63
|
# The composed document must at least parse as DESIGN.md; a missing
|
|
63
64
|
# frontmatter is a warning, not a failure - prose-only documents
|
|
64
65
|
# still carry the body sections, and import will say so again.
|
|
66
|
+
#
|
|
67
|
+
# @param design_md [String] the composed DESIGN.md document
|
|
68
|
+
# @param io [IO] where warnings are printed
|
|
65
69
|
def validate!(design_md, io)
|
|
66
70
|
parsed = Poetry::Core::DesignMd.parse(design_md)
|
|
67
71
|
io.puts "note: composed DESIGN.md has no frontmatter tokens" unless parsed.is_a?(Hash) && parsed.any?
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: poetry-extract
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Matt Solt
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - '='
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.1.
|
|
18
|
+
version: 0.1.5
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - '='
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.1.
|
|
25
|
+
version: 0.1.5
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: context.dev
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -47,6 +47,10 @@ executables: []
|
|
|
47
47
|
extensions: []
|
|
48
48
|
extra_rdoc_files: []
|
|
49
49
|
files:
|
|
50
|
+
- ".mcp.json"
|
|
51
|
+
- ".yard-lint.yml"
|
|
52
|
+
- ".yard_coverage_all"
|
|
53
|
+
- Archspec.rb
|
|
50
54
|
- CHANGELOG.md
|
|
51
55
|
- LICENSE.txt
|
|
52
56
|
- README.md
|