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.
Files changed (166) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +3 -0
  3. data/DESIGN.md +145 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +30 -0
  6. data/THIRD_PARTY_NOTICES.md +507 -0
  7. data/app/components/poetry/core/box/component.rb +82 -0
  8. data/app/components/poetry/core/component.rb +390 -0
  9. data/app/components/poetry/core/concerns/agent_tools.rb +418 -0
  10. data/app/components/poetry/core/concerns/declared_attributes.rb +267 -0
  11. data/app/components/poetry/core/concerns/introspection.rb +533 -0
  12. data/app/components/poetry/core/concerns/options.rb +262 -0
  13. data/app/components/poetry/core/concerns/parts.rb +148 -0
  14. data/app/components/poetry/core/concerns/stimulus.rb +363 -0
  15. data/app/components/poetry/core/concerns/styles.rb +323 -0
  16. data/app/components/poetry/core/style.rb +150 -0
  17. data/app/components/poetry/core/wrapper/component.rb +90 -0
  18. data/app/javascript/poetry/core/accordion_controller.js +165 -0
  19. data/app/javascript/poetry/core/action_bar_controller.js +109 -0
  20. data/app/javascript/poetry/core/autocomplete_controller.js +220 -0
  21. data/app/javascript/poetry/core/calendar_controller.js +406 -0
  22. data/app/javascript/poetry/core/carousel_controller.js +167 -0
  23. data/app/javascript/poetry/core/checkbox_group_controller.js +97 -0
  24. data/app/javascript/poetry/core/checked_controller.js +208 -0
  25. data/app/javascript/poetry/core/clipboard_text_controller.js +111 -0
  26. data/app/javascript/poetry/core/combobox_controller.js +1095 -0
  27. data/app/javascript/poetry/core/command_controller.js +510 -0
  28. data/app/javascript/poetry/core/context_menu_controller.js +204 -0
  29. data/app/javascript/poetry/core/date_field_controller.js +584 -0
  30. data/app/javascript/poetry/core/date_picker_controller.js +111 -0
  31. data/app/javascript/poetry/core/deferred_controller.js +111 -0
  32. data/app/javascript/poetry/core/dialog_controller.js +206 -0
  33. data/app/javascript/poetry/core/dismissable_controller.js +182 -0
  34. data/app/javascript/poetry/core/drawer_controller.js +365 -0
  35. data/app/javascript/poetry/core/file_input_controller.js +191 -0
  36. data/app/javascript/poetry/core/focus_scope_controller.js +162 -0
  37. data/app/javascript/poetry/core/helpers/announce.js +228 -0
  38. data/app/javascript/poetry/core/helpers/breakpoint.js +28 -0
  39. data/app/javascript/poetry/core/helpers/collection.js +18 -0
  40. data/app/javascript/poetry/core/helpers/direction.js +15 -0
  41. data/app/javascript/poetry/core/helpers/escape.js +38 -0
  42. data/app/javascript/poetry/core/helpers/filter_rank.js +104 -0
  43. data/app/javascript/poetry/core/helpers/focus_guards.js +46 -0
  44. data/app/javascript/poetry/core/helpers/hotkey.js +43 -0
  45. data/app/javascript/poetry/core/helpers/id_integrity.js +65 -0
  46. data/app/javascript/poetry/core/helpers/incomplete_date.js +328 -0
  47. data/app/javascript/poetry/core/helpers/mask.js +283 -0
  48. data/app/javascript/poetry/core/helpers/portal.js +217 -0
  49. data/app/javascript/poetry/core/helpers/presence.js +210 -0
  50. data/app/javascript/poetry/core/helpers/registration_guard.js +76 -0
  51. data/app/javascript/poetry/core/helpers/scroll_lock.js +67 -0
  52. data/app/javascript/poetry/core/helpers/scroller_geometry.js +422 -0
  53. data/app/javascript/poetry/core/helpers/state.js +80 -0
  54. data/app/javascript/poetry/core/helpers/tabbable.js +56 -0
  55. data/app/javascript/poetry/core/helpers/turbo_cache.js +21 -0
  56. data/app/javascript/poetry/core/helpers/typeahead.js +85 -0
  57. data/app/javascript/poetry/core/hotkey_controller.js +55 -0
  58. data/app/javascript/poetry/core/hover_card_controller.js +431 -0
  59. data/app/javascript/poetry/core/index.js +218 -0
  60. data/app/javascript/poetry/core/mask_controller.js +500 -0
  61. data/app/javascript/poetry/core/menu_controller.js +1050 -0
  62. data/app/javascript/poetry/core/menubar_controller.js +329 -0
  63. data/app/javascript/poetry/core/message_scroller_controller.js +930 -0
  64. data/app/javascript/poetry/core/navigation_menu_controller.js +458 -0
  65. data/app/javascript/poetry/core/number_field_controller.js +404 -0
  66. data/app/javascript/poetry/core/optimistic_form_controller.js +56 -0
  67. data/app/javascript/poetry/core/otp_controller.js +219 -0
  68. data/app/javascript/poetry/core/popover_controller.js +300 -0
  69. data/app/javascript/poetry/core/popper_controller.js +439 -0
  70. data/app/javascript/poetry/core/pressed_controller.js +74 -0
  71. data/app/javascript/poetry/core/questionnaire_controller.js +468 -0
  72. data/app/javascript/poetry/core/radio_group_controller.js +197 -0
  73. data/app/javascript/poetry/core/resizable_controller.js +202 -0
  74. data/app/javascript/poetry/core/roving_focus_controller.js +215 -0
  75. data/app/javascript/poetry/core/scroll_spy_controller.js +84 -0
  76. data/app/javascript/poetry/core/search_field_controller.js +95 -0
  77. data/app/javascript/poetry/core/select_controller.js +924 -0
  78. data/app/javascript/poetry/core/sensitive_input_controller.js +195 -0
  79. data/app/javascript/poetry/core/sheet_controller.js +24 -0
  80. data/app/javascript/poetry/core/sidebar_controller.js +272 -0
  81. data/app/javascript/poetry/core/slider_controller.js +419 -0
  82. data/app/javascript/poetry/core/state_controller.js +82 -0
  83. data/app/javascript/poetry/core/table_selection_controller.js +153 -0
  84. data/app/javascript/poetry/core/tabs_controller.js +151 -0
  85. data/app/javascript/poetry/core/tag_group_controller.js +134 -0
  86. data/app/javascript/poetry/core/toast_controller.js +228 -0
  87. data/app/javascript/poetry/core/toast_trigger_controller.js +26 -0
  88. data/app/javascript/poetry/core/toaster_controller.js +197 -0
  89. data/app/javascript/poetry/core/toggle_group_controller.js +206 -0
  90. data/app/javascript/poetry/core/tooltip_controller.js +541 -0
  91. data/app/javascript/poetry/core/tree_controller.js +265 -0
  92. data/app/javascript/poetry/core/vendor/floating_ui_core.js +1043 -0
  93. data/app/javascript/poetry/core/vendor/floating_ui_dom.js +794 -0
  94. data/app/javascript/poetry/core/vendor/floating_ui_utils.js +141 -0
  95. data/app/javascript/poetry/core/vendor/floating_ui_utils_dom.js +170 -0
  96. data/app/views/poetry/core/preview.html.erb +13 -0
  97. data/config/component_registry.yml +20 -0
  98. data/config/controllers_manifest.json +1528 -0
  99. data/config/importmap.rb +11 -0
  100. data/config/state_vocabulary.json +31 -0
  101. data/lib/active_model/type/list.rb +33 -0
  102. data/lib/active_model/type/symbol.rb +36 -0
  103. data/lib/poetry/core/check/stable_identity.rb +104 -0
  104. data/lib/poetry/core/check.rb +1445 -0
  105. data/lib/poetry/core/config.rb +286 -0
  106. data/lib/poetry/core/contrib/wrapped_helper.rb +68 -0
  107. data/lib/poetry/core/css/bem_merger.rb +45 -0
  108. data/lib/poetry/core/css/bem_reference.rb +87 -0
  109. data/lib/poetry/core/css/override_scan.rb +110 -0
  110. data/lib/poetry/core/css/resolver.rb +161 -0
  111. data/lib/poetry/core/css/safelist.rb +41 -0
  112. data/lib/poetry/core/css/tailwind_merger.rb +102 -0
  113. data/lib/poetry/core/css/template_classes.rb +102 -0
  114. data/lib/poetry/core/css/theme_coverage.rb +72 -0
  115. data/lib/poetry/core/css/var_coverage.rb +75 -0
  116. data/lib/poetry/core/css/verifier.rb +106 -0
  117. data/lib/poetry/core/design_lint.rb +720 -0
  118. data/lib/poetry/core/design_md/import.rb +250 -0
  119. data/lib/poetry/core/design_md.rb +461 -0
  120. data/lib/poetry/core/engine.rb +71 -0
  121. data/lib/poetry/core/errors.rb +53 -0
  122. data/lib/poetry/core/html/attributes.rb +667 -0
  123. data/lib/poetry/core/icons.rb +157 -0
  124. data/lib/poetry/core/llms_text.rb +354 -0
  125. data/lib/poetry/core/page_architectures.rb +304 -0
  126. data/lib/poetry/core/part_contract.rb +285 -0
  127. data/lib/poetry/core/preview/abstract.rb +121 -0
  128. data/lib/poetry/core/preview/base.rb +227 -0
  129. data/lib/poetry/core/preview/sidecarable.rb +136 -0
  130. data/lib/poetry/core/preview/template.rb +186 -0
  131. data/lib/poetry/core/recipe_items.rb +82 -0
  132. data/lib/poetry/core/registry.rb +363 -0
  133. data/lib/poetry/core/registry_address.rb +115 -0
  134. data/lib/poetry/core/registry_client.rb +217 -0
  135. data/lib/poetry/core/registry_installer.rb +234 -0
  136. data/lib/poetry/core/registry_items.rb +184 -0
  137. data/lib/poetry/core/skill_text.rb +288 -0
  138. data/lib/poetry/core/stable_id.rb +110 -0
  139. data/lib/poetry/core/stimulus/builder.rb +395 -0
  140. data/lib/poetry/core/stimulus/declarations.rb +444 -0
  141. data/lib/poetry/core/stimulus/manifest.rb +68 -0
  142. data/lib/poetry/core/stimulus/merger.rb +131 -0
  143. data/lib/poetry/core/stimulus_contract.rb +263 -0
  144. data/lib/poetry/core/tag_helper.rb +10 -0
  145. data/lib/poetry/core/template_compile.rb +74 -0
  146. data/lib/poetry/core/token_import.rb +317 -0
  147. data/lib/poetry/core/tokens/color.rb +215 -0
  148. data/lib/poetry/core/tokens/contrast_gate.rb +120 -0
  149. data/lib/poetry/core/tokens/generator.rb +211 -0
  150. data/lib/poetry/core/tokens.rb +117 -0
  151. data/lib/poetry/core/version.rb +9 -0
  152. data/lib/poetry/core.rb +52 -0
  153. data/lib/poetry-core.rb +5 -0
  154. data/tokens/aliases.css +145 -0
  155. data/tokens/tailwind-theme.css +47 -0
  156. data/tokens/tokens.css +78 -0
  157. data/tokens/tokens.dtcg.json +779 -0
  158. data/vendor/floating-ui/LICENSE +20 -0
  159. data/vendor/floating-ui/VENDORED_VERSIONS +15 -0
  160. data/vendor/shadcn-tailwind/LICENSE +21 -0
  161. data/vendor/shadcn-tailwind/VENDORED_COMMIT +1 -0
  162. data/vendor/shadcn-tailwind/tailwind.css +629 -0
  163. data/vendor/tw-animate-css/LICENSE +21 -0
  164. data/vendor/tw-animate-css/VENDORED_VERSION +1 -0
  165. data/vendor/tw-animate-css/tw-animate.css +1 -0
  166. metadata +267 -0
@@ -0,0 +1,250 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Poetry
4
+ module Core
5
+ class DesignMd
6
+ # The import half of the DESIGN.md interop: a parsed document
7
+ # (DesignMd.parse - ours or foreign) becomes a token-override PLAN the
8
+ # host task renders into app/assets/tailwind/poetry/design-overrides.css.
9
+ #
10
+ # Three hard rules:
11
+ #
12
+ # 1. Contrast is enforced at the door. Every ContrastGate ledger pair
13
+ # a mapped override touches is measured on the MERGED set (defaults
14
+ # + overrides) against the WCAG AA floor; a failing pair drops its
15
+ # imported members from the plan and reports the nearest-passing
16
+ # suggestion (a deterministic OKLCH L-walk, chroma held). Only
17
+ # force: ships a failing pair.
18
+ # 2. Fonts never enter CSS. A typography family becomes a report note
19
+ # plus a snippet the host opts into by hand.
20
+ # 3. DROP-not-fabricate. Unmapped or unparseable values are listed in
21
+ # the report, never guessed into tokens.
22
+ #
23
+ # @example
24
+ # doc = Poetry::Core::DesignMd.parse(File.read("brand.DESIGN.md"))
25
+ # plan = Poetry::Core::DesignMd::Import.new.plan(doc)
26
+ # plan.overrides["light"] # => { "primary" => a Tokens::Color, ... }
27
+ #
28
+ # @api private
29
+ class Import
30
+ AA = 4.5
31
+
32
+ # Conservative cross-ecosystem role aliases - every application is
33
+ # itself reported, so nothing maps silently. Names already matching
34
+ # poetry roles pass through; an `on-<x>` name resolves to the
35
+ # mapped `<x>-foreground` token when poetry ships one.
36
+ ALIASES = {
37
+ "text" => "foreground",
38
+ "bg" => "background",
39
+ "surface" => "card",
40
+ "brand" => "primary",
41
+ "danger" => "destructive",
42
+ "error" => "destructive",
43
+ "on-background" => "foreground"
44
+ }.freeze
45
+
46
+ Application = Struct.new(:role, :from, :mode, :color, keyword_init: true)
47
+ Drop = Struct.new(:name, :value, :mode, :reason, keyword_init: true)
48
+ ContrastResult = Struct.new(:mode, :label, :roles, :ratio, :pass, :suggestion, :shipped,
49
+ keyword_init: true) do
50
+ def to_s
51
+ state = if pass
52
+ "pass"
53
+ else
54
+ (shipped ? "SHIPPED FAILING (forced)" : "FAILED - dropped")
55
+ end
56
+ line = "[#{mode}] #{label}: #{format("%.2f", ratio)}:1 (AA needs >= #{AA}) #{state}"
57
+ suggestion ? "#{line}; nearest passing: #{suggestion}" : line
58
+ end
59
+ end
60
+
61
+ # pins: dark-mode values RESTATED from the shipped defaults for every
62
+ # light-overridden token the source gave no dark value for. Without
63
+ # them a light-only import's :root block would also win in dark mode
64
+ # (:root and .dark tie on specificity; the overrides file loads
65
+ # later) - pinning keeps dark exactly as shipped, no fabrication.
66
+ Plan = Struct.new(:overrides, :pins, :radius, :applied, :dropped, :contrast, :typography_note,
67
+ keyword_init: true) do
68
+ def any_overrides?
69
+ radius || overrides.any? { |_mode, colors| colors.any? }
70
+ end
71
+ end
72
+
73
+ def initialize(tokens: Tokens.load)
74
+ @tokens = tokens
75
+ @roles = tokens.color_names("light")
76
+ end
77
+
78
+ # @param doc [Hash] DesignMd.parse output
79
+ # @param force [Boolean] ship overrides even when a touched pair fails AA
80
+ # @return [Plan]
81
+ def plan(doc, force: false)
82
+ applied = []
83
+ dropped = []
84
+ map_colors(doc, applied, dropped)
85
+ drop_unknown(doc, dropped)
86
+
87
+ overrides = applied.group_by(&:mode).transform_values do |list|
88
+ list.to_h { |application| [application.role, application.color] }
89
+ end
90
+ overrides = { "light" => {}, "dark" => {} }.merge(overrides)
91
+
92
+ contrast = enforce_contrast(overrides, force: force)
93
+ contrast.reject(&:pass).reject(&:shipped).each do |result|
94
+ drop_failing_pair(result, overrides, applied, dropped)
95
+ end
96
+
97
+ Plan.new(overrides: overrides, pins: dark_pins(overrides), radius: doc["radius"],
98
+ applied: applied, dropped: dropped, contrast: contrast,
99
+ typography_note: typography_note(doc))
100
+ end
101
+
102
+ # The deterministic nearest-AA suggestion: walk the candidate's OKLCH
103
+ # lightness away from the other member in 0.01 steps, chroma held,
104
+ # until the pair passes or L leaves [0, 1] (then there is no passing
105
+ # lightness on this hue/chroma axis).
106
+ #
107
+ # @param candidate [Tokens::Color] the color to walk
108
+ # @param against [Tokens::Color] the fixed other member of the pair
109
+ # @return [Tokens::Color, nil] the nearest passing color, or nil
110
+ def nearest_aa(candidate, against)
111
+ step = candidate.l > against.l ? 0.01 : -0.01
112
+ color = candidate
113
+ until color.contrast_ratio(against) >= AA
114
+ next_l = (color.l + step).round(3)
115
+ return nil unless next_l.between?(0.0, 1.0)
116
+
117
+ color = color.with(l: next_l)
118
+ end
119
+ color
120
+ end
121
+
122
+ private
123
+
124
+ def map_colors(doc, applied, dropped)
125
+ (doc["colors"] || {}).each do |mode, colors|
126
+ (colors || {}).each do |name, color|
127
+ role, reason = resolve_role(name)
128
+ if role
129
+ applied << Application.new(role: role, from: name, mode: mode, color: color)
130
+ else
131
+ dropped << Drop.new(name: name, value: color.css, mode: mode, reason: reason)
132
+ end
133
+ end
134
+ end
135
+ end
136
+
137
+ def resolve_role(name)
138
+ return [name, nil] if @roles.include?(name)
139
+ return [ALIASES[name], nil] if ALIASES.key?(name)
140
+
141
+ if name.start_with?("on-")
142
+ base, reason = resolve_role(name.delete_prefix("on-"))
143
+ foreground = base && "#{base}-foreground"
144
+ return [foreground, nil] if foreground && @roles.include?(foreground)
145
+
146
+ return [nil, reason || "poetry ships no #{foreground} token"]
147
+ end
148
+
149
+ [nil, "no poetry role for #{name.inspect} (roles + aliases only - never guessed)"]
150
+ end
151
+
152
+ def drop_unknown(doc, dropped)
153
+ (doc.dig("unknown", "colors") || {}).each do |name, value|
154
+ dropped << Drop.new(name: name, value: value, mode: nil,
155
+ reason: "unparseable color value (hex/rgb/oklch only)")
156
+ end
157
+ end
158
+
159
+ # Every ledger pair an override touches, measured on the merged set.
160
+ def enforce_contrast(overrides, force:)
161
+ Tokens::ContrastGate::LEDGER.flat_map do |mode, specs|
162
+ touched = overrides.fetch(mode, {}).keys
163
+ next [] if touched.empty?
164
+
165
+ specs.filter_map do |spec|
166
+ members = [spec[:fg], spec[:bg], spec[:bg_over]].compact
167
+ next unless members.intersect?(touched)
168
+
169
+ measure(mode, spec, overrides, force: force)
170
+ end
171
+ end
172
+ end
173
+
174
+ def measure(mode, spec, overrides, force:)
175
+ fg = spec[:fg] == :white ? Tokens::Color::WHITE : merged(mode, spec[:fg], overrides)
176
+ bg = merged_background(mode, spec, overrides)
177
+ ratio = fg.contrast_ratio(bg)
178
+ pass = ratio >= AA
179
+ label = "#{spec[:fg] == :white ? "white" : spec[:fg]} on #{spec[:bg]}"
180
+ label += "/#{format("%g", spec[:bg_alpha] * 100)}% over #{spec[:bg_over]}" if spec[:bg_alpha]
181
+
182
+ roles = [spec[:fg], spec[:bg], spec[:bg_over]].compact - [:white]
183
+ ContrastResult.new(mode: mode, label: label, roles: roles, ratio: ratio, pass: pass,
184
+ shipped: !pass && force,
185
+ suggestion: pass ? nil : suggestion_for(mode, spec, overrides))
186
+ end
187
+
188
+ # Walk the IMPORTED member of the failing pair (the text side when
189
+ # both were imported); composited backgrounds walk the text side only.
190
+ def suggestion_for(mode, spec, overrides)
191
+ imported = overrides.fetch(mode, {})
192
+ fg = spec[:fg] == :white ? Tokens::Color::WHITE : merged(mode, spec[:fg], overrides)
193
+ bg = merged_background(mode, spec, overrides)
194
+
195
+ if spec[:fg] != :white && imported.key?(spec[:fg])
196
+ walked = nearest_aa(fg, bg)
197
+ walked && "--#{spec[:fg]}: #{walked.css}"
198
+ elsif imported.key?(spec[:bg]) && !spec[:bg_alpha]
199
+ walked = nearest_aa(merged(mode, spec[:bg], overrides), fg)
200
+ walked && "--#{spec[:bg]}: #{walked.css}"
201
+ end
202
+ end
203
+
204
+ def drop_failing_pair(result, overrides, applied, dropped)
205
+ mode = result.mode
206
+ result.roles.each do |name|
207
+ color = overrides.fetch(mode, {}).delete(name)
208
+ next unless color
209
+
210
+ applied.reject! { |application| application.mode == mode && application.role == name }
211
+ dropped << Drop.new(name: name, value: color.css, mode: mode,
212
+ reason: "fails AA in the #{result.label} pair " \
213
+ "(#{format("%.2f", result.ratio)}:1)#{suffix(result)}")
214
+ end
215
+ end
216
+
217
+ def suffix(result)
218
+ result.suggestion ? "; nearest passing: #{result.suggestion}" : ""
219
+ end
220
+
221
+ # Dark values restated from the shipped defaults for light-overridden
222
+ # names the source left dark-silent (see the Plan docstring).
223
+ def dark_pins(overrides)
224
+ missing = overrides.fetch("light", {}).keys - overrides.fetch("dark", {}).keys
225
+ missing.to_h { |name| [name, @tokens.color("dark", name)] }
226
+ end
227
+
228
+ def merged(mode, name, overrides)
229
+ overrides.fetch(mode, {})[name] || @tokens.color(mode, name)
230
+ end
231
+
232
+ def merged_background(mode, spec, overrides)
233
+ base = merged(mode, spec[:bg], overrides)
234
+ return base unless spec[:bg_alpha]
235
+
236
+ translucent = base.with(alpha: spec[:bg_alpha])
237
+ translucent.composite_over(merged(mode, spec[:bg_over], overrides))
238
+ end
239
+
240
+ def typography_note(doc)
241
+ family = doc.dig("typography", "family")
242
+ return unless family
243
+
244
+ "typography stays out of poetry CSS - to adopt the pairing, add to your app css: " \
245
+ "body { font-family: #{family}; }"
246
+ end
247
+ end
248
+ end
249
+ end
250
+ end