poetry-core 0.0.2
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/CHANGELOG.md +3 -0
- data/DESIGN.md +145 -0
- data/LICENSE.txt +21 -0
- data/README.md +30 -0
- data/THIRD_PARTY_NOTICES.md +507 -0
- data/app/components/poetry/core/box/component.rb +82 -0
- data/app/components/poetry/core/component.rb +390 -0
- data/app/components/poetry/core/concerns/agent_tools.rb +418 -0
- data/app/components/poetry/core/concerns/declared_attributes.rb +267 -0
- data/app/components/poetry/core/concerns/introspection.rb +533 -0
- data/app/components/poetry/core/concerns/options.rb +262 -0
- data/app/components/poetry/core/concerns/parts.rb +148 -0
- data/app/components/poetry/core/concerns/stimulus.rb +363 -0
- data/app/components/poetry/core/concerns/styles.rb +323 -0
- data/app/components/poetry/core/style.rb +150 -0
- data/app/components/poetry/core/wrapper/component.rb +90 -0
- data/app/javascript/poetry/core/accordion_controller.js +165 -0
- data/app/javascript/poetry/core/action_bar_controller.js +109 -0
- data/app/javascript/poetry/core/autocomplete_controller.js +220 -0
- data/app/javascript/poetry/core/calendar_controller.js +406 -0
- data/app/javascript/poetry/core/carousel_controller.js +167 -0
- data/app/javascript/poetry/core/checkbox_group_controller.js +97 -0
- data/app/javascript/poetry/core/checked_controller.js +208 -0
- data/app/javascript/poetry/core/clipboard_text_controller.js +111 -0
- data/app/javascript/poetry/core/combobox_controller.js +1095 -0
- data/app/javascript/poetry/core/command_controller.js +510 -0
- data/app/javascript/poetry/core/context_menu_controller.js +204 -0
- data/app/javascript/poetry/core/date_field_controller.js +584 -0
- data/app/javascript/poetry/core/date_picker_controller.js +111 -0
- data/app/javascript/poetry/core/deferred_controller.js +111 -0
- data/app/javascript/poetry/core/dialog_controller.js +206 -0
- data/app/javascript/poetry/core/dismissable_controller.js +182 -0
- data/app/javascript/poetry/core/drawer_controller.js +365 -0
- data/app/javascript/poetry/core/file_input_controller.js +191 -0
- data/app/javascript/poetry/core/focus_scope_controller.js +162 -0
- data/app/javascript/poetry/core/helpers/announce.js +228 -0
- data/app/javascript/poetry/core/helpers/breakpoint.js +28 -0
- data/app/javascript/poetry/core/helpers/collection.js +18 -0
- data/app/javascript/poetry/core/helpers/direction.js +15 -0
- data/app/javascript/poetry/core/helpers/escape.js +38 -0
- data/app/javascript/poetry/core/helpers/filter_rank.js +104 -0
- data/app/javascript/poetry/core/helpers/focus_guards.js +46 -0
- data/app/javascript/poetry/core/helpers/hotkey.js +43 -0
- data/app/javascript/poetry/core/helpers/id_integrity.js +65 -0
- data/app/javascript/poetry/core/helpers/incomplete_date.js +328 -0
- data/app/javascript/poetry/core/helpers/mask.js +283 -0
- data/app/javascript/poetry/core/helpers/portal.js +217 -0
- data/app/javascript/poetry/core/helpers/presence.js +210 -0
- data/app/javascript/poetry/core/helpers/registration_guard.js +76 -0
- data/app/javascript/poetry/core/helpers/scroll_lock.js +67 -0
- data/app/javascript/poetry/core/helpers/scroller_geometry.js +422 -0
- data/app/javascript/poetry/core/helpers/state.js +80 -0
- data/app/javascript/poetry/core/helpers/tabbable.js +56 -0
- data/app/javascript/poetry/core/helpers/turbo_cache.js +21 -0
- data/app/javascript/poetry/core/helpers/typeahead.js +85 -0
- data/app/javascript/poetry/core/hotkey_controller.js +55 -0
- data/app/javascript/poetry/core/hover_card_controller.js +431 -0
- data/app/javascript/poetry/core/index.js +218 -0
- data/app/javascript/poetry/core/mask_controller.js +500 -0
- data/app/javascript/poetry/core/menu_controller.js +1050 -0
- data/app/javascript/poetry/core/menubar_controller.js +329 -0
- data/app/javascript/poetry/core/message_scroller_controller.js +930 -0
- data/app/javascript/poetry/core/navigation_menu_controller.js +458 -0
- data/app/javascript/poetry/core/number_field_controller.js +404 -0
- data/app/javascript/poetry/core/optimistic_form_controller.js +56 -0
- data/app/javascript/poetry/core/otp_controller.js +219 -0
- data/app/javascript/poetry/core/popover_controller.js +300 -0
- data/app/javascript/poetry/core/popper_controller.js +439 -0
- data/app/javascript/poetry/core/pressed_controller.js +74 -0
- data/app/javascript/poetry/core/questionnaire_controller.js +468 -0
- data/app/javascript/poetry/core/radio_group_controller.js +197 -0
- data/app/javascript/poetry/core/resizable_controller.js +202 -0
- data/app/javascript/poetry/core/roving_focus_controller.js +215 -0
- data/app/javascript/poetry/core/scroll_spy_controller.js +84 -0
- data/app/javascript/poetry/core/search_field_controller.js +95 -0
- data/app/javascript/poetry/core/select_controller.js +924 -0
- data/app/javascript/poetry/core/sensitive_input_controller.js +195 -0
- data/app/javascript/poetry/core/sheet_controller.js +24 -0
- data/app/javascript/poetry/core/sidebar_controller.js +272 -0
- data/app/javascript/poetry/core/slider_controller.js +419 -0
- data/app/javascript/poetry/core/state_controller.js +82 -0
- data/app/javascript/poetry/core/table_selection_controller.js +153 -0
- data/app/javascript/poetry/core/tabs_controller.js +151 -0
- data/app/javascript/poetry/core/tag_group_controller.js +134 -0
- data/app/javascript/poetry/core/toast_controller.js +228 -0
- data/app/javascript/poetry/core/toast_trigger_controller.js +26 -0
- data/app/javascript/poetry/core/toaster_controller.js +197 -0
- data/app/javascript/poetry/core/toggle_group_controller.js +206 -0
- data/app/javascript/poetry/core/tooltip_controller.js +541 -0
- data/app/javascript/poetry/core/tree_controller.js +265 -0
- data/app/javascript/poetry/core/vendor/floating_ui_core.js +1043 -0
- data/app/javascript/poetry/core/vendor/floating_ui_dom.js +794 -0
- data/app/javascript/poetry/core/vendor/floating_ui_utils.js +141 -0
- data/app/javascript/poetry/core/vendor/floating_ui_utils_dom.js +170 -0
- data/app/views/poetry/core/preview.html.erb +13 -0
- data/config/component_registry.yml +20 -0
- data/config/controllers_manifest.json +1528 -0
- data/config/importmap.rb +11 -0
- data/config/state_vocabulary.json +31 -0
- data/lib/active_model/type/list.rb +33 -0
- data/lib/active_model/type/symbol.rb +36 -0
- data/lib/poetry/core/check/stable_identity.rb +104 -0
- data/lib/poetry/core/check.rb +1445 -0
- data/lib/poetry/core/config.rb +286 -0
- data/lib/poetry/core/contrib/wrapped_helper.rb +68 -0
- data/lib/poetry/core/css/bem_merger.rb +45 -0
- data/lib/poetry/core/css/bem_reference.rb +87 -0
- data/lib/poetry/core/css/override_scan.rb +110 -0
- data/lib/poetry/core/css/resolver.rb +161 -0
- data/lib/poetry/core/css/safelist.rb +41 -0
- data/lib/poetry/core/css/tailwind_merger.rb +102 -0
- data/lib/poetry/core/css/template_classes.rb +102 -0
- data/lib/poetry/core/css/theme_coverage.rb +72 -0
- data/lib/poetry/core/css/var_coverage.rb +75 -0
- data/lib/poetry/core/css/verifier.rb +106 -0
- data/lib/poetry/core/design_lint.rb +720 -0
- data/lib/poetry/core/design_md/import.rb +250 -0
- data/lib/poetry/core/design_md.rb +461 -0
- data/lib/poetry/core/engine.rb +71 -0
- data/lib/poetry/core/errors.rb +53 -0
- data/lib/poetry/core/html/attributes.rb +667 -0
- data/lib/poetry/core/icons.rb +157 -0
- data/lib/poetry/core/llms_text.rb +354 -0
- data/lib/poetry/core/page_architectures.rb +304 -0
- data/lib/poetry/core/part_contract.rb +285 -0
- data/lib/poetry/core/preview/abstract.rb +121 -0
- data/lib/poetry/core/preview/base.rb +227 -0
- data/lib/poetry/core/preview/sidecarable.rb +136 -0
- data/lib/poetry/core/preview/template.rb +186 -0
- data/lib/poetry/core/recipe_items.rb +82 -0
- data/lib/poetry/core/registry.rb +363 -0
- data/lib/poetry/core/registry_address.rb +115 -0
- data/lib/poetry/core/registry_client.rb +217 -0
- data/lib/poetry/core/registry_installer.rb +234 -0
- data/lib/poetry/core/registry_items.rb +184 -0
- data/lib/poetry/core/skill_text.rb +288 -0
- data/lib/poetry/core/stable_id.rb +110 -0
- data/lib/poetry/core/stimulus/builder.rb +395 -0
- data/lib/poetry/core/stimulus/declarations.rb +444 -0
- data/lib/poetry/core/stimulus/manifest.rb +68 -0
- data/lib/poetry/core/stimulus/merger.rb +131 -0
- data/lib/poetry/core/stimulus_contract.rb +263 -0
- data/lib/poetry/core/tag_helper.rb +10 -0
- data/lib/poetry/core/template_compile.rb +74 -0
- data/lib/poetry/core/token_import.rb +317 -0
- data/lib/poetry/core/tokens/color.rb +215 -0
- data/lib/poetry/core/tokens/contrast_gate.rb +120 -0
- data/lib/poetry/core/tokens/generator.rb +211 -0
- data/lib/poetry/core/tokens.rb +117 -0
- data/lib/poetry/core/version.rb +9 -0
- data/lib/poetry/core.rb +52 -0
- data/lib/poetry-core.rb +5 -0
- data/tokens/aliases.css +145 -0
- data/tokens/tailwind-theme.css +47 -0
- data/tokens/tokens.css +78 -0
- data/tokens/tokens.dtcg.json +779 -0
- data/vendor/floating-ui/LICENSE +20 -0
- data/vendor/floating-ui/VENDORED_VERSIONS +15 -0
- data/vendor/shadcn-tailwind/LICENSE +21 -0
- data/vendor/shadcn-tailwind/VENDORED_COMMIT +1 -0
- data/vendor/shadcn-tailwind/tailwind.css +629 -0
- data/vendor/tw-animate-css/LICENSE +21 -0
- data/vendor/tw-animate-css/VENDORED_VERSION +1 -0
- data/vendor/tw-animate-css/tw-animate.css +1 -0
- metadata +267 -0
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Poetry
|
|
6
|
+
module Core
|
|
7
|
+
# DESIGN.md interop: poetry WRITES and READS the design-skill
|
|
8
|
+
# ecosystem's shared artifact - the DESIGN.md file that design tools
|
|
9
|
+
# emit, author, and read, formalized by an open spec
|
|
10
|
+
# (github.com/google-labs-code/design.md, alpha).
|
|
11
|
+
#
|
|
12
|
+
# Serialized files carry both interop surfaces at once:
|
|
13
|
+
#
|
|
14
|
+
# - YAML front matter in the spec's shape (`version`/`name`/flat
|
|
15
|
+
# `colors:`/`typography:`/`rounded:`), plus namespaced extensions the
|
|
16
|
+
# spec tolerates: `modes.dark` (poetry ships BOTH modes) and
|
|
17
|
+
# a `poetry:` block (source-of-truth pointers, contrast policy, theme
|
|
18
|
+
# treatment) that makes parse -> serialize lossless;
|
|
19
|
+
# - Markdown sections in the spec's canonical order (its linter checks
|
|
20
|
+
# section-order), readable by humans and section-walking skills.
|
|
21
|
+
#
|
|
22
|
+
# `parse` is deliberately more tolerant than `serialize`: poetry-authored
|
|
23
|
+
# files rebuild exactly from the front matter (round-trip guarantee:
|
|
24
|
+
# serialize(parse(md)) is byte-identical for files serialize emitted);
|
|
25
|
+
# foreign files fall back to the section walker (heading variants across
|
|
26
|
+
# the ecosystems, colors in hex/rgb/oklch). What cannot be parsed lands
|
|
27
|
+
# in doc["unknown"] - the import pipeline DROPS it with a report,
|
|
28
|
+
# never guesses.
|
|
29
|
+
#
|
|
30
|
+
# @example
|
|
31
|
+
# doc = Poetry::Core::DesignMd.parse(File.read("DESIGN.md"))
|
|
32
|
+
# doc.dig("colors", "light", "primary") # => a Tokens::Color
|
|
33
|
+
#
|
|
34
|
+
# @api private
|
|
35
|
+
class DesignMd
|
|
36
|
+
# The spec's canonical section order (its own linter warns on
|
|
37
|
+
# out-of-order sections).
|
|
38
|
+
SECTIONS = ["Overview", "Colors", "Typography", "Layout", "Elevation & Depth",
|
|
39
|
+
"Shapes", "Components", "Do's and Don'ts", "Intentional deviations"].freeze
|
|
40
|
+
|
|
41
|
+
COMPONENTS_POINTER = "/poetry/llms.txt"
|
|
42
|
+
|
|
43
|
+
# Heading classifier for the tolerant walker - the spellings emitting
|
|
44
|
+
# tools actually use (including British "colour").
|
|
45
|
+
HEADING_KINDS = {
|
|
46
|
+
colors: /\b(?:colou?rs?|palette)\b/i,
|
|
47
|
+
typography: /\b(?:typography|type(?:\s+pairing)?|fonts?)\b/i,
|
|
48
|
+
shapes: /\b(?:shapes?|radius|radii|corners?|rounded)\b/i
|
|
49
|
+
}.freeze
|
|
50
|
+
|
|
51
|
+
# A color-looking CSS value anywhere in a line.
|
|
52
|
+
COLOR_VALUE = /(#\h{3,8}\b|rgba?\([^)]*\)|oklch\([^)]*\))/i
|
|
53
|
+
# `name: value` in bullets, definition lines, or `| name | value |` rows.
|
|
54
|
+
NAME_VALUE_LINE = %r{\A\s*(?:[-*]\s+)?(?:\*\*)?([A-Za-z][\w /-]*?)(?:\*\*)?\s*[:=]\s*(.+)\z}
|
|
55
|
+
|
|
56
|
+
class << self
|
|
57
|
+
# Build a document hash from the live token model. `details` carries
|
|
58
|
+
# the per-theme presentation metadata: "typography" (pairing/family -
|
|
59
|
+
# metadata only, no poetry theme moves a font token),
|
|
60
|
+
# "treatment", "components_count", optional "description".
|
|
61
|
+
# `deviations`: the host's declared .cn-* overrides from
|
|
62
|
+
# config/poetry_components.yml - design intent, not drift, so the
|
|
63
|
+
# export carries them (the poetry: front matter round-trips them).
|
|
64
|
+
def build(tokens:, theme:, details:, deviations: [])
|
|
65
|
+
gate = Tokens::ContrastGate.new(tokens)
|
|
66
|
+
{
|
|
67
|
+
"name" => "poetry #{theme}",
|
|
68
|
+
"theme" => theme,
|
|
69
|
+
"description" => details["description"] ||
|
|
70
|
+
"poetry #{theme} - semantic role tokens (shared across every poetry theme) " \
|
|
71
|
+
"under the #{theme} component treatment.",
|
|
72
|
+
"colors" => tokens.modes.to_h do |mode|
|
|
73
|
+
[mode, tokens.color_names(mode).to_h { |name| [name, tokens.color(mode, name)] }]
|
|
74
|
+
end,
|
|
75
|
+
"typography" => details.fetch("typography"),
|
|
76
|
+
"radius" => tokens.radius_css,
|
|
77
|
+
"radius_scale" => Tokens::Generator::RADIUS_SCALE.dup,
|
|
78
|
+
"contrast" => {
|
|
79
|
+
"floor" => "WCAG 2.2 AA (4.5:1) - locked, every gated pair",
|
|
80
|
+
"target" => "AAA (7:1) wherever achievable at lock time",
|
|
81
|
+
"gate" => "Poetry::Core::Tokens::ContrastGate",
|
|
82
|
+
"aa_exceptions" => gate.results.select { |result| result.lock == :aa }.map(&:to_s)
|
|
83
|
+
},
|
|
84
|
+
"treatment" => details.fetch("treatment"),
|
|
85
|
+
"components" => { "count" => details.fetch("components_count"), "pointer" => COMPONENTS_POINTER },
|
|
86
|
+
"generator" => details["generator"],
|
|
87
|
+
"deviations" => (deviations unless Array(deviations).empty?)
|
|
88
|
+
}.compact
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def serialize(doc)
|
|
92
|
+
"#{front_matter(doc)}#{body(doc)}"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def parse(markdown)
|
|
96
|
+
fm, prose = split_front_matter(markdown.to_s)
|
|
97
|
+
return from_poetry_front_matter(fm) if fm&.key?("poetry")
|
|
98
|
+
|
|
99
|
+
from_foreign(fm, prose)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# --- serialization -------------------------------------------------
|
|
103
|
+
|
|
104
|
+
def front_matter(doc)
|
|
105
|
+
light = doc.dig("colors", "light") || {}
|
|
106
|
+
dark = doc.dig("colors", "dark") || {}
|
|
107
|
+
data = {
|
|
108
|
+
"version" => "alpha",
|
|
109
|
+
"name" => doc["name"],
|
|
110
|
+
"description" => doc["description"],
|
|
111
|
+
"colors" => light.transform_values(&:css),
|
|
112
|
+
"modes" => { "dark" => dark.transform_values(&:css) },
|
|
113
|
+
"typography" => { "body" => { "fontFamily" => doc.dig("typography", "family") } },
|
|
114
|
+
"rounded" => resolved_rounded(doc),
|
|
115
|
+
"poetry" => {
|
|
116
|
+
"theme" => doc["theme"],
|
|
117
|
+
"source" => Tokens::DEFAULT_RELATIVE_PATH,
|
|
118
|
+
"generator" => doc["generator"],
|
|
119
|
+
"dark_mode" => "class .dark",
|
|
120
|
+
"radius" => doc["radius"],
|
|
121
|
+
"radius_scale" => doc["radius_scale"],
|
|
122
|
+
"typography_pairing" => doc.dig("typography", "pairing"),
|
|
123
|
+
"treatment" => doc["treatment"],
|
|
124
|
+
"components_count" => doc.dig("components", "count"),
|
|
125
|
+
"components_pointer" => doc.dig("components", "pointer"),
|
|
126
|
+
"contrast_policy" => doc["contrast"],
|
|
127
|
+
"deviations" => doc["deviations"]
|
|
128
|
+
}.compact
|
|
129
|
+
}
|
|
130
|
+
"#{YAML.dump(data)}---\n"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# The radius scale resolved to concrete px (rem at 16px/em) - the
|
|
134
|
+
# spec's `rounded:` wants plain dimensions foreign tools can consume;
|
|
135
|
+
# the calc() truth stays under poetry.radius_scale.
|
|
136
|
+
def resolved_rounded(doc)
|
|
137
|
+
base_px = radius_px(doc["radius"])
|
|
138
|
+
return {} unless base_px
|
|
139
|
+
|
|
140
|
+
(doc["radius_scale"] || {}).to_h do |step, value|
|
|
141
|
+
factor = value[/\*\s*([\d.]+)\s*\)/, 1]&.to_f || 1.0
|
|
142
|
+
[step, "#{format("%g", base_px * factor)}px"]
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def radius_px(radius)
|
|
147
|
+
number = radius.to_s[/[\d.]+/]&.to_f
|
|
148
|
+
return unless number
|
|
149
|
+
|
|
150
|
+
radius.to_s.include?("rem") ? number * 16 : number
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def body(doc)
|
|
154
|
+
[overview_section(doc), colors_section(doc), typography_section(doc),
|
|
155
|
+
layout_section, elevation_section(doc), shapes_section(doc),
|
|
156
|
+
components_section(doc), dos_and_donts_section,
|
|
157
|
+
deviations_section(doc)].compact.join("\n")
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def overview_section(doc)
|
|
161
|
+
<<~MD
|
|
162
|
+
# DESIGN.md - #{doc["name"]}
|
|
163
|
+
|
|
164
|
+
#{doc["description"]}
|
|
165
|
+
|
|
166
|
+
## Overview
|
|
167
|
+
|
|
168
|
+
- design system: poetry - server-rendered Rails components (ViewComponent + Stimulus + Tailwind v4)
|
|
169
|
+
- theme: #{doc["theme"]} - #{doc["treatment"]}
|
|
170
|
+
- token source: one DTCG file shared by every poetry theme (`#{Tokens::DEFAULT_RELATIVE_PATH}`);
|
|
171
|
+
themes are component-treatment layers, not palettes
|
|
172
|
+
- dark mode: the `.dark` class - both modes ship in this file
|
|
173
|
+
MD
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def colors_section(doc)
|
|
177
|
+
light = doc.dig("colors", "light") || {}
|
|
178
|
+
dark = doc.dig("colors", "dark") || {}
|
|
179
|
+
rows = light.keys.map { |name| "| #{name} | #{light[name].css} | #{dark[name]&.css} |" }
|
|
180
|
+
<<~MD
|
|
181
|
+
## Colors
|
|
182
|
+
|
|
183
|
+
Semantic roles only - components never reference raw palette values
|
|
184
|
+
(the class verifier and `poetry check` enforce this).
|
|
185
|
+
|
|
186
|
+
| role | light | dark |
|
|
187
|
+
|---|---|---|
|
|
188
|
+
#{rows.join("\n")}
|
|
189
|
+
MD
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def typography_section(doc)
|
|
193
|
+
<<~MD
|
|
194
|
+
## Typography
|
|
195
|
+
|
|
196
|
+
- pairing: #{doc.dig("typography", "pairing")} (app-level metadata - no poetry theme
|
|
197
|
+
moves a font token)
|
|
198
|
+
- family: #{doc.dig("typography", "family")}
|
|
199
|
+
MD
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def layout_section
|
|
203
|
+
<<~MD
|
|
204
|
+
## Layout
|
|
205
|
+
|
|
206
|
+
- spacing: the Tailwind v4 default scale (0.25rem base unit) - poetry adds no spacing tokens
|
|
207
|
+
- structure: components size to their container; page layout stays host-owned
|
|
208
|
+
MD
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def elevation_section(doc)
|
|
212
|
+
<<~MD
|
|
213
|
+
## Elevation & Depth
|
|
214
|
+
|
|
215
|
+
- shadows and overlay scrims are theme treatment (the `cn-*` layer), not tokens -
|
|
216
|
+
#{doc["theme"]} ships its own elevation story
|
|
217
|
+
MD
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def shapes_section(doc)
|
|
221
|
+
scale = (doc["radius_scale"] || {}).map { |step, value| "#{step} #{value}" }.join(" - ")
|
|
222
|
+
<<~MD
|
|
223
|
+
## Shapes
|
|
224
|
+
|
|
225
|
+
- radius: #{doc["radius"]} (`--radius`)
|
|
226
|
+
- scale: #{scale}
|
|
227
|
+
MD
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def components_section(doc)
|
|
231
|
+
<<~MD
|
|
232
|
+
## Components
|
|
233
|
+
|
|
234
|
+
- catalog: #{doc.dig("components", "count")} components - the registry is the truth; read
|
|
235
|
+
`#{doc.dig("components", "pointer")}` (or `llms-full.txt` with events and variants)
|
|
236
|
+
instead of duplicating it here
|
|
237
|
+
- consume via `poetry_*` helpers; styling rides the theme's `cn-*` classes,
|
|
238
|
+
never per-instance CSS
|
|
239
|
+
MD
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def dos_and_donts_section
|
|
243
|
+
<<~MD
|
|
244
|
+
## Do's and Don'ts
|
|
245
|
+
|
|
246
|
+
- Do pick variants by intent - one `primary` action per view; `destructive`
|
|
247
|
+
only for irreversible actions.
|
|
248
|
+
- Do keep both modes honest - every surface must hold in light and dark.
|
|
249
|
+
- Don't hand-write hex/oklch in markup - use role tokens (`bg-primary`,
|
|
250
|
+
`text-destructive`, ...); `poetry check` flags raw color classes.
|
|
251
|
+
- Don't paint white text on solid `destructive` in dark mode - dark
|
|
252
|
+
destructive renders at 60% over the background.
|
|
253
|
+
- Don't introduce new colors, shadows, or radii without adding tokens first.
|
|
254
|
+
MD
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Declared .cn-* overrides: rendered only when the host has
|
|
258
|
+
# any - the nine gem exports stay byte-stable.
|
|
259
|
+
def deviations_section(doc)
|
|
260
|
+
deviations = Array(doc["deviations"])
|
|
261
|
+
return nil if deviations.empty?
|
|
262
|
+
|
|
263
|
+
rows = deviations.map do |entry|
|
|
264
|
+
files = Array(entry["files"]).join(", ")
|
|
265
|
+
"| `#{entry["cn"]}` | #{files.empty? ? "-" : "`#{files}`"} | #{entry["reason"]} | #{entry["created"]} |"
|
|
266
|
+
end
|
|
267
|
+
<<~MD
|
|
268
|
+
## Intentional deviations
|
|
269
|
+
|
|
270
|
+
Declared `.cn-*` overrides from `config/poetry_components.yml` - design
|
|
271
|
+
intent, reviewed and reasoned; anything else touching `cn-*` from host
|
|
272
|
+
CSS is drift (`bin/rails poetry:design:overrides`).
|
|
273
|
+
|
|
274
|
+
| cn | files | reason | created |
|
|
275
|
+
|----|-------|--------|---------|
|
|
276
|
+
#{rows.join("\n")}
|
|
277
|
+
MD
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# --- parsing ---------------------------------------------------------
|
|
281
|
+
|
|
282
|
+
def split_front_matter(markdown)
|
|
283
|
+
match = markdown.match(/\A---\n(.*?)\n---\n?(.*)\z/m)
|
|
284
|
+
return [nil, markdown] unless match
|
|
285
|
+
|
|
286
|
+
[YAML.safe_load(match[1], permitted_classes: [], aliases: true), match[2]]
|
|
287
|
+
rescue Psych::SyntaxError
|
|
288
|
+
[nil, markdown]
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# A file serialize emitted: everything rebuilds from the front matter,
|
|
292
|
+
# so serialize(parse(md)) == md.
|
|
293
|
+
def from_poetry_front_matter(front)
|
|
294
|
+
meta = front["poetry"]
|
|
295
|
+
light, light_unknown = parse_color_map(front["colors"])
|
|
296
|
+
dark, dark_unknown = parse_color_map(front.dig("modes", "dark"))
|
|
297
|
+
{
|
|
298
|
+
"name" => front["name"],
|
|
299
|
+
"theme" => meta["theme"],
|
|
300
|
+
"description" => front["description"],
|
|
301
|
+
"colors" => { "light" => light, "dark" => dark },
|
|
302
|
+
"typography" => { "pairing" => meta["typography_pairing"],
|
|
303
|
+
"family" => front.dig("typography", "body", "fontFamily") },
|
|
304
|
+
"radius" => meta["radius"],
|
|
305
|
+
"radius_scale" => meta["radius_scale"],
|
|
306
|
+
"contrast" => meta["contrast_policy"],
|
|
307
|
+
"treatment" => meta["treatment"],
|
|
308
|
+
"components" => { "count" => meta["components_count"], "pointer" => meta["components_pointer"] },
|
|
309
|
+
"generator" => meta["generator"],
|
|
310
|
+
"deviations" => meta["deviations"],
|
|
311
|
+
"unknown" => { "colors" => light_unknown.merge(dark_unknown), "sections" => [] }
|
|
312
|
+
}
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# A foreign DESIGN.md (another tool's export, or hand-authored):
|
|
316
|
+
# front matter where present, then the section walker over the prose.
|
|
317
|
+
def from_foreign(front, prose)
|
|
318
|
+
front ||= {}
|
|
319
|
+
light, unknown_colors = parse_color_map(front["colors"])
|
|
320
|
+
walked = walk_sections(prose)
|
|
321
|
+
|
|
322
|
+
walked[:colors].each do |name, value|
|
|
323
|
+
next if light.key?(name)
|
|
324
|
+
|
|
325
|
+
color = Tokens::Color.parse(value)
|
|
326
|
+
color ? light[name] = color : unknown_colors[name] = value
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
{
|
|
330
|
+
"name" => front["name"],
|
|
331
|
+
"theme" => nil,
|
|
332
|
+
"description" => front["description"],
|
|
333
|
+
"colors" => { "light" => light, "dark" => walked[:dark_colors] },
|
|
334
|
+
"typography" => { "pairing" => nil,
|
|
335
|
+
"family" => front.dig("typography", "body", "fontFamily") || walked[:family] },
|
|
336
|
+
"radius" => foreign_radius(front) || walked[:radius],
|
|
337
|
+
"radius_scale" => nil,
|
|
338
|
+
"contrast" => nil,
|
|
339
|
+
"treatment" => nil,
|
|
340
|
+
"components" => nil,
|
|
341
|
+
"unknown" => { "colors" => unknown_colors.merge(walked[:unknown_colors]),
|
|
342
|
+
"sections" => walked[:unknown_sections] }
|
|
343
|
+
}
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Flat name => css-string map -> [{name => Color}, {name => raw}].
|
|
347
|
+
def parse_color_map(map)
|
|
348
|
+
parsed = {}
|
|
349
|
+
unknown = {}
|
|
350
|
+
(map || {}).each do |name, value|
|
|
351
|
+
color = Tokens::Color.parse(value)
|
|
352
|
+
color ? parsed[normalize_name(name)] = color : unknown[normalize_name(name)] = value
|
|
353
|
+
end
|
|
354
|
+
[parsed, unknown]
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def foreign_radius(front)
|
|
358
|
+
rounded = front["rounded"]
|
|
359
|
+
return rounded.values.first&.to_s if rounded.is_a?(Hash) && rounded.any?
|
|
360
|
+
|
|
361
|
+
rounded&.to_s
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# The tolerant section walker: headings classified by HEADING_KINDS,
|
|
365
|
+
# `name: value` lines mined inside each. Returns raw findings; the
|
|
366
|
+
# importer decides what maps onto poetry roles.
|
|
367
|
+
def walk_sections(prose)
|
|
368
|
+
result = { colors: {}, dark_colors: {}, unknown_colors: {}, unknown_sections: [],
|
|
369
|
+
family: nil, radius: nil }
|
|
370
|
+
each_section(prose) do |heading, lines|
|
|
371
|
+
kind = HEADING_KINDS.find { |_kind, pattern| heading.match?(pattern) }&.first
|
|
372
|
+
case kind
|
|
373
|
+
when :colors then walk_colors(lines, result)
|
|
374
|
+
when :typography then walk_typography(lines, result)
|
|
375
|
+
when :shapes then walk_shapes(lines, result)
|
|
376
|
+
else result[:unknown_sections] << heading
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
result
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def each_section(prose)
|
|
383
|
+
heading = nil
|
|
384
|
+
lines = []
|
|
385
|
+
prose.each_line do |line|
|
|
386
|
+
if (match = line.match(/\A\#{1,6}\s+(.+?)\s*\z/))
|
|
387
|
+
yield(heading, lines) if heading
|
|
388
|
+
heading = match[1]
|
|
389
|
+
lines = []
|
|
390
|
+
else
|
|
391
|
+
lines << line.chomp
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
yield(heading, lines) if heading
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def walk_colors(lines, result)
|
|
398
|
+
table_modes = nil
|
|
399
|
+
lines.each do |line|
|
|
400
|
+
if line.strip.start_with?("|")
|
|
401
|
+
table_modes = color_table_row(line, table_modes, result)
|
|
402
|
+
elsif (match = line.match(NAME_VALUE_LINE))
|
|
403
|
+
assign_color(result, match[1], match[2])
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# Handles `| role | light | dark |` and `| name | value |` tables.
|
|
409
|
+
# Returns the sniffed header layout so data rows know their columns.
|
|
410
|
+
def color_table_row(line, table_modes, result)
|
|
411
|
+
cells = line.split("|").map(&:strip).reject(&:empty?)
|
|
412
|
+
return table_modes if cells.all? { |cell| cell.match?(/\A:?-+:?\z/) } # separator row
|
|
413
|
+
|
|
414
|
+
if table_modes.nil?
|
|
415
|
+
header = cells.map(&:downcase)
|
|
416
|
+
return { light: header.index("light"), dark: header.index("dark") } if header.include?("light")
|
|
417
|
+
|
|
418
|
+
return { light: 1, dark: nil } # value column follows the name
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
name = cells[0]
|
|
422
|
+
assign_color(result, name, cells[table_modes[:light]].to_s)
|
|
423
|
+
if table_modes[:dark] && (dark = Tokens::Color.parse(cells[table_modes[:dark]].to_s))
|
|
424
|
+
result[:dark_colors][normalize_name(name)] = dark
|
|
425
|
+
end
|
|
426
|
+
table_modes
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def assign_color(result, name, raw)
|
|
430
|
+
value = raw[COLOR_VALUE, 1] || raw.strip
|
|
431
|
+
color = Tokens::Color.parse(value)
|
|
432
|
+
key = normalize_name(name)
|
|
433
|
+
color ? result[:colors][key] = value : result[:unknown_colors][key] = raw.strip
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def walk_typography(lines, result)
|
|
437
|
+
lines.each do |line|
|
|
438
|
+
match = line.match(NAME_VALUE_LINE)
|
|
439
|
+
next unless match
|
|
440
|
+
next unless match[1].match?(/\A(?:font[\s-]?family|family|body|pairing|type)\z/i)
|
|
441
|
+
|
|
442
|
+
result[:family] ||= match[2].strip
|
|
443
|
+
end
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def walk_shapes(lines, result)
|
|
447
|
+
lines.each do |line|
|
|
448
|
+
next unless line.match?(/radius|rounded/i)
|
|
449
|
+
|
|
450
|
+
dimension = line[/([\d.]+(?:px|rem|em))/, 1]
|
|
451
|
+
result[:radius] ||= dimension if dimension
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def normalize_name(name)
|
|
456
|
+
name.to_s.strip.downcase.gsub(/\s+/, "-")
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/engine"
|
|
4
|
+
# Rails::Engine::Configuration references ActionDispatch::Routing::RouteSet as
|
|
5
|
+
# soon as `config` is touched (below), so action_dispatch must be loaded for the
|
|
6
|
+
# engine to be requirable standalone (e.g. via `require "poetry/core"`), not only
|
|
7
|
+
# inside an already-booted Rails app.
|
|
8
|
+
require "action_dispatch"
|
|
9
|
+
|
|
10
|
+
module Poetry
|
|
11
|
+
module Core
|
|
12
|
+
# The Rails engine: wires the component autoload paths, previews,
|
|
13
|
+
# importmap pins, asset paths, and the StableId / TagHelper mixins into
|
|
14
|
+
# a host app at boot.
|
|
15
|
+
class Engine < ::Rails::Engine
|
|
16
|
+
# isolate_namespace Poetry::Core
|
|
17
|
+
|
|
18
|
+
config.poetry_core = Poetry::Core::Config.current
|
|
19
|
+
|
|
20
|
+
# Add components to autoload paths for Zeitwerk
|
|
21
|
+
config.autoload_paths << "#{Poetry::Core.root}/app/components"
|
|
22
|
+
config.eager_load_paths << "#{Poetry::Core.root}/app/components"
|
|
23
|
+
|
|
24
|
+
initializer "poetry_core.stable_id" do
|
|
25
|
+
ActiveSupport.on_load(:action_controller) do
|
|
26
|
+
include Poetry::Core::StableId::Controller
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
initializer "poetry_core.tag_helper" do
|
|
31
|
+
ActiveSupport.on_load(:action_view) do
|
|
32
|
+
include Poetry::Core::TagHelper
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
initializer "poetry_core.previews" do
|
|
37
|
+
ActiveSupport.on_load(:view_component) do
|
|
38
|
+
ViewComponent::Preview.extend Poetry::Core::Preview::Sidecarable
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
initializer "poetry_core.view_component" do |app|
|
|
43
|
+
app.config.view_component.previews.paths << "#{Poetry::Core.root}/app/components"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Lookbook is a dev-only dependency; guard so the engine never crashes a
|
|
47
|
+
# production (or lean test) host that does not load it.
|
|
48
|
+
initializer "poetry_core.setup_lookbook" do |app|
|
|
49
|
+
app.config.lookbook.preview_paths << "#{Poetry::Core.root}/app/components" if defined?(Lookbook)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
initializer "poetry_core.assets", before: "propshaft.set_manifest" do |_app|
|
|
53
|
+
# Add JavaScript to asset paths for Propshaft
|
|
54
|
+
if Rails.application.config.respond_to?(:assets)
|
|
55
|
+
Rails.application.config.assets.paths << Poetry::Core.root.join("app/javascript")
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The importmap-first JS channel: merge poetry's pins into the
|
|
60
|
+
# host's importmap so `import ... from "@poetry/controllers"` works
|
|
61
|
+
# with zero build. Guarded - bundler hosts (esbuild/Vite) use the npm
|
|
62
|
+
# channel instead and never load importmap-rails.
|
|
63
|
+
initializer "poetry_core.importmap", before: "importmap" do |app|
|
|
64
|
+
if app.config.respond_to?(:importmap)
|
|
65
|
+
app.config.importmap.paths << Poetry::Core.root.join("config/importmap.rb")
|
|
66
|
+
app.config.importmap.cache_sweepers << Poetry::Core.root.join("app/javascript")
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Poetry::Core module provides extensions and enhancements for Rails views and components.
|
|
4
|
+
module Poetry
|
|
5
|
+
module Core
|
|
6
|
+
# Base error class for all Poetry::Core-related errors.
|
|
7
|
+
#
|
|
8
|
+
# All custom errors in the Poetry::Core module inherit from this class, which in turn
|
|
9
|
+
# inherits from Ruby's StandardError. This provides a common ancestor for
|
|
10
|
+
# rescuing all Poetry::Core-specific exceptions.
|
|
11
|
+
#
|
|
12
|
+
# @example Rescuing all Poetry::Core errors
|
|
13
|
+
# begin
|
|
14
|
+
# # Poetry::Core operations
|
|
15
|
+
# rescue Poetry::Core::Error => e
|
|
16
|
+
# Rails.logger.error("Poetry::Core error: #{e.message}")
|
|
17
|
+
# end
|
|
18
|
+
class Error < StandardError; end
|
|
19
|
+
|
|
20
|
+
# Raised by an icon set's #fetch for a name it cannot serve -
|
|
21
|
+
# malformed, or simply not in the set. This is the public rescue
|
|
22
|
+
# point of the icon system: the Icon component's missing-icon policy
|
|
23
|
+
# rescues exactly this class, so a custom set registered via
|
|
24
|
+
# {Poetry::Core::Icons.register} must raise it too (that is the set
|
|
25
|
+
# contract).
|
|
26
|
+
#
|
|
27
|
+
# @example Custom handling around a dynamic icon name
|
|
28
|
+
# begin
|
|
29
|
+
# Poetry::Core::Icons.set.fetch(user_preference)
|
|
30
|
+
# rescue Poetry::Core::IconNotFound => e
|
|
31
|
+
# logger.info("missing icon #{e.name.inspect}") # e.suggestion may name the fix
|
|
32
|
+
# end
|
|
33
|
+
class IconNotFound < Error
|
|
34
|
+
# @return [Symbol, String] the requested icon name, as given
|
|
35
|
+
attr_reader :name
|
|
36
|
+
|
|
37
|
+
# @return [String, nil] the closest valid name, when one exists
|
|
38
|
+
attr_reader :suggestion
|
|
39
|
+
|
|
40
|
+
# Carries the requested name (and the did-you-mean fix, when one
|
|
41
|
+
# exists) alongside the message.
|
|
42
|
+
#
|
|
43
|
+
# @param message [String] the full error message
|
|
44
|
+
# @param name [Symbol, String] the requested icon name
|
|
45
|
+
# @param suggestion [String, nil] the closest valid name, when known
|
|
46
|
+
def initialize(message, name:, suggestion: nil)
|
|
47
|
+
super(message)
|
|
48
|
+
@name = name
|
|
49
|
+
@suggestion = suggestion
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|