poetry-core 0.1.11 → 0.1.13
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/CHANGELOG.md +13 -0
- data/app/javascript/poetry/core/behaviors/clipboard.js +61 -0
- data/app/javascript/poetry/core/behaviors/in_view.js +36 -0
- data/app/javascript/poetry/core/behaviors/interval.js +39 -0
- data/app/javascript/poetry/core/behaviors/measure.js +28 -0
- data/app/javascript/poetry/core/behaviors/mutation.js +20 -0
- data/lib/poetry/core/design_md/import.rb +52 -4
- data/lib/poetry/core/version.rb +1 -1
- metadata +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d3a645a256745edb1c315853e3ce88d918362af24218c198a405e66dd2e224f0
|
|
4
|
+
data.tar.gz: 799ffd218002cdbd8f6ed71745f0a152b559e833c0feb7105fa4afdf3907b689
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7bded4405ebe9a3fdb3b09fac843525522cab4ccb163911889421fe65f94427430d4426d1c5b904a38594b5a863edb6a2f66b543cb5a56c8c3bbaf8aee4b09f3
|
|
7
|
+
data.tar.gz: fb9a1925cacace5469d961f86007fe490da19b8692a69c8061a781885261fb404a705080de32180e8b475064f997953594805b4f08b3251790e88e34b8f7a61b
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.13] - 2026-10-13
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Effect behaviors over the per-connect scope, for controllers of your own and the components ported from other registries: `useInterval` and `useTimeout` (timers cleared at teardown), `useInView` (IntersectionObserver, with `once:`; visible at once where the API is missing), `useMeasure` (ResizeObserver, reporting the size now and on change), `useMutation` (MutationObserver, children in the subtree by default) and `useClipboard` (the Clipboard API with a textarea fallback and a copied state that resets). Import them from `@poetry/controllers/behaviors/<name>`; each is undone by `teardown(this)`.
|
|
8
|
+
- `poetry:design:import` warns when a palette makes border or input too faint to see: in each mode the import touches, both are measured against the background, card and popover, and a pair below 1.15:1 is reported with the nearest visible lightness. The colors still ship, since a faint edge is a design choice and not an accessibility floor. A dark palette that gave border, input and card one value hid every bordered control on cards. The JSON report carries the warnings as `edges`.
|
|
9
|
+
|
|
10
|
+
## [0.1.12] - 2026-10-07
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Version bump with the family; no changes in this gem.
|
|
15
|
+
|
|
3
16
|
## [0.1.11] - 2026-10-05
|
|
4
17
|
|
|
5
18
|
### Changed
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// useClipboard: copying text for one connect. The async Clipboard API
|
|
2
|
+
// where the page may use it, else a hidden textarea and execCommand; the
|
|
3
|
+
// copied flag resets after `resetAfter` and its timer clears at teardown.
|
|
4
|
+
import { scopeOf } from "@poetry/controllers/helpers/scope"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A copier for the controller's current connect.
|
|
8
|
+
*
|
|
9
|
+
* @param {Object} controller - the Stimulus controller instance
|
|
10
|
+
* @param {{ resetAfter?: number, onChange?: (copied: boolean) => void }} [options]
|
|
11
|
+
* how long the copied state lasts, in milliseconds, and who hears it change
|
|
12
|
+
* @returns {{ copy: (text: string) => Promise<boolean> }} copy resolves true when the text reached the clipboard
|
|
13
|
+
*/
|
|
14
|
+
export function useClipboard(controller, { resetAfter = 2000, onChange = () => {} } = {}) {
|
|
15
|
+
const scope = scopeOf(controller)
|
|
16
|
+
let timer = null
|
|
17
|
+
scope.defer(() => clearTimeout(timer))
|
|
18
|
+
|
|
19
|
+
const copy = async (text) => {
|
|
20
|
+
const copied = await write(text)
|
|
21
|
+
if (!copied) return false
|
|
22
|
+
|
|
23
|
+
clearTimeout(timer)
|
|
24
|
+
onChange(true)
|
|
25
|
+
timer = setTimeout(() => onChange(false), resetAfter)
|
|
26
|
+
return true
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return { copy }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const write = async (text) => {
|
|
33
|
+
try {
|
|
34
|
+
if (navigator.clipboard?.writeText) {
|
|
35
|
+
await navigator.clipboard.writeText(text)
|
|
36
|
+
return true
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
// Denied or unavailable: fall through to the textarea.
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return fallback(text)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const fallback = (text) => {
|
|
46
|
+
const area = document.createElement("textarea")
|
|
47
|
+
area.value = text
|
|
48
|
+
area.setAttribute("readonly", "")
|
|
49
|
+
area.style.position = "fixed"
|
|
50
|
+
area.style.opacity = "0"
|
|
51
|
+
document.body.append(area)
|
|
52
|
+
area.select()
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
return document.execCommand?.("copy") === true
|
|
56
|
+
} catch {
|
|
57
|
+
return false
|
|
58
|
+
} finally {
|
|
59
|
+
area.remove()
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// useInView: whether an element is on screen, for one connect, over
|
|
2
|
+
// IntersectionObserver. Reveal-on-scroll and lazy starts read it; the
|
|
3
|
+
// observer disconnects at teardown. Without IntersectionObserver the
|
|
4
|
+
// element counts as visible at once, so content never stays hidden.
|
|
5
|
+
import { scopeOf } from "@poetry/controllers/helpers/scope"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Reports `element` entering and leaving the viewport (or `root`) until
|
|
9
|
+
* teardown(controller). With `once`, it reports the first entry and stops.
|
|
10
|
+
*
|
|
11
|
+
* @param {Object} controller - the Stimulus controller instance
|
|
12
|
+
* @param {Element} element - the element to watch
|
|
13
|
+
* @param {(inView: boolean, entry?: IntersectionObserverEntry) => void} onChange - the handler
|
|
14
|
+
* @param {{ root?: Element, rootMargin?: string, threshold?: number | number[], once?: boolean }} [options]
|
|
15
|
+
* @returns {() => void} stops watching early; teardown stops it otherwise
|
|
16
|
+
*/
|
|
17
|
+
export function useInView(controller, element, onChange, options = {}) {
|
|
18
|
+
const { once = false, ...observe } = options
|
|
19
|
+
|
|
20
|
+
if (typeof IntersectionObserver === "undefined") {
|
|
21
|
+
onChange(true)
|
|
22
|
+
return () => {}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let stop = () => {}
|
|
26
|
+
const observer = new IntersectionObserver((entries) => {
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
onChange(entry.isIntersecting, entry)
|
|
29
|
+
if (once && entry.isIntersecting) stop()
|
|
30
|
+
}
|
|
31
|
+
}, observe)
|
|
32
|
+
observer.observe(element)
|
|
33
|
+
stop = scopeOf(controller).defer(() => observer.disconnect())
|
|
34
|
+
|
|
35
|
+
return stop
|
|
36
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// useInterval and useTimeout: timers that live for one connect. The
|
|
2
|
+
// clears are registered for you, so a ticker or an autoplay never fires
|
|
3
|
+
// into a disconnected controller.
|
|
4
|
+
import { scopeOf } from "@poetry/controllers/helpers/scope"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Calls `callback` every `milliseconds` until teardown(controller).
|
|
8
|
+
*
|
|
9
|
+
* @param {Object} controller - the Stimulus controller instance
|
|
10
|
+
* @param {() => void} callback - the tick
|
|
11
|
+
* @param {number} milliseconds - the period
|
|
12
|
+
* @returns {() => void} stops the interval early; teardown stops it otherwise
|
|
13
|
+
*/
|
|
14
|
+
export function useInterval(controller, callback, milliseconds) {
|
|
15
|
+
const id = setInterval(callback, milliseconds)
|
|
16
|
+
|
|
17
|
+
return scopeOf(controller).defer(() => clearInterval(id))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Calls `callback` once after `milliseconds`, unless teardown(controller)
|
|
22
|
+
* comes first.
|
|
23
|
+
*
|
|
24
|
+
* @param {Object} controller - the Stimulus controller instance
|
|
25
|
+
* @param {() => void} callback - the delayed call
|
|
26
|
+
* @param {number} milliseconds - the delay
|
|
27
|
+
* @returns {() => void} cancels early; teardown cancels otherwise
|
|
28
|
+
*/
|
|
29
|
+
export function useTimeout(controller, callback, milliseconds) {
|
|
30
|
+
const scope = scopeOf(controller)
|
|
31
|
+
let cancel = () => {}
|
|
32
|
+
const id = setTimeout(() => {
|
|
33
|
+
cancel()
|
|
34
|
+
callback()
|
|
35
|
+
}, milliseconds)
|
|
36
|
+
cancel = scope.defer(() => clearTimeout(id))
|
|
37
|
+
|
|
38
|
+
return cancel
|
|
39
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// useMeasure: an element's size for one connect, over ResizeObserver.
|
|
2
|
+
// The handler runs now with the current size and again on every change;
|
|
3
|
+
// the observer disconnects at teardown. Without ResizeObserver it runs
|
|
4
|
+
// once.
|
|
5
|
+
import { scopeOf } from "@poetry/controllers/helpers/scope"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Reports `element`'s content box until teardown(controller).
|
|
9
|
+
*
|
|
10
|
+
* @param {Object} controller - the Stimulus controller instance
|
|
11
|
+
* @param {Element} element - the element to measure
|
|
12
|
+
* @param {(size: { width: number, height: number }) => void} onResize - the handler
|
|
13
|
+
* @returns {() => void} stops measuring early; teardown stops it otherwise
|
|
14
|
+
*/
|
|
15
|
+
export function useMeasure(controller, element, onResize) {
|
|
16
|
+
const rect = element.getBoundingClientRect()
|
|
17
|
+
onResize({ width: rect.width, height: rect.height })
|
|
18
|
+
|
|
19
|
+
if (typeof ResizeObserver === "undefined") return () => {}
|
|
20
|
+
|
|
21
|
+
const observer = new ResizeObserver((entries) => {
|
|
22
|
+
const box = entries[entries.length - 1]?.contentRect
|
|
23
|
+
if (box) onResize({ width: box.width, height: box.height })
|
|
24
|
+
})
|
|
25
|
+
observer.observe(element)
|
|
26
|
+
|
|
27
|
+
return scopeOf(controller).defer(() => observer.disconnect())
|
|
28
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// useMutation: changes under an element for one connect, over
|
|
2
|
+
// MutationObserver; the observer disconnects at teardown. The default
|
|
3
|
+
// watches children added and removed anywhere below.
|
|
4
|
+
import { scopeOf } from "@poetry/controllers/helpers/scope"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Reports mutations under `element` until teardown(controller).
|
|
8
|
+
*
|
|
9
|
+
* @param {Object} controller - the Stimulus controller instance
|
|
10
|
+
* @param {Node} element - the node to observe
|
|
11
|
+
* @param {(records: MutationRecord[]) => void} onMutate - the handler
|
|
12
|
+
* @param {MutationObserverInit} [options] - what to watch; children in the subtree by default
|
|
13
|
+
* @returns {() => void} stops observing early; teardown stops it otherwise
|
|
14
|
+
*/
|
|
15
|
+
export function useMutation(controller, element, onMutate, options = { childList: true, subtree: true }) {
|
|
16
|
+
const observer = new MutationObserver(onMutate)
|
|
17
|
+
observer.observe(element, options)
|
|
18
|
+
|
|
19
|
+
return scopeOf(controller).defer(() => observer.disconnect())
|
|
20
|
+
}
|
|
@@ -28,6 +28,13 @@ module Poetry
|
|
|
28
28
|
# @api private
|
|
29
29
|
class Import
|
|
30
30
|
AA = 4.5
|
|
31
|
+
# The contrast a border or input line needs against the surfaces it
|
|
32
|
+
# sits on to be seen. Below it every bordered control on that
|
|
33
|
+
# surface disappears; the import warns and ships anyway, since a
|
|
34
|
+
# faint edge is a design choice, never an accessibility floor.
|
|
35
|
+
EDGE = 1.15
|
|
36
|
+
EDGES = %w[border input].freeze
|
|
37
|
+
SURFACES = %w[background card popover].freeze
|
|
31
38
|
|
|
32
39
|
# Conservative cross-ecosystem role aliases - every application is
|
|
33
40
|
# itself reported, so nothing maps silently. Names already matching
|
|
@@ -62,12 +69,22 @@ module Poetry
|
|
|
62
69
|
end
|
|
63
70
|
end
|
|
64
71
|
|
|
72
|
+
# One line too faint against a surface it sits on, with the nearest
|
|
73
|
+
# lightness that clears EDGE.
|
|
74
|
+
Edge = Struct.new(:mode, :edge, :surface, :ratio, :suggestion, keyword_init: true) do
|
|
75
|
+
# The warning as one line: mode, the pair, its ratio and the nearest visible value.
|
|
76
|
+
def to_s
|
|
77
|
+
line = "[#{mode}] #{edge} on #{surface}: #{format("%.2f", ratio)}:1 (visible needs >= #{EDGE})"
|
|
78
|
+
suggestion ? "#{line}; nearest visible: #{suggestion}" : line
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
65
82
|
# pins: dark-mode values RESTATED from the shipped defaults for every
|
|
66
83
|
# light-overridden token the source gave no dark value for. Without
|
|
67
84
|
# them a light-only import's :root block would also win in dark mode
|
|
68
85
|
# (:root and .dark tie on specificity; the overrides file loads
|
|
69
86
|
# later) - pinning keeps dark exactly as shipped, no fabrication.
|
|
70
|
-
Plan = Struct.new(:overrides, :pins, :radius, :applied, :dropped, :contrast, :typography_note,
|
|
87
|
+
Plan = Struct.new(:overrides, :pins, :radius, :applied, :dropped, :contrast, :edges, :typography_note,
|
|
71
88
|
keyword_init: true) do
|
|
72
89
|
# Whether the plan changes a radius or any color.
|
|
73
90
|
def any_overrides?
|
|
@@ -102,7 +119,7 @@ module Poetry
|
|
|
102
119
|
end
|
|
103
120
|
|
|
104
121
|
Plan.new(overrides: overrides, pins: dark_pins(overrides), radius: doc["radius"],
|
|
105
|
-
applied: applied, dropped: dropped, contrast: contrast,
|
|
122
|
+
applied: applied, dropped: dropped, contrast: contrast, edges: faint_edges(overrides),
|
|
106
123
|
typography_note: typography_note(doc))
|
|
107
124
|
end
|
|
108
125
|
|
|
@@ -114,10 +131,19 @@ module Poetry
|
|
|
114
131
|
# @param candidate [Tokens::Color] the color to walk
|
|
115
132
|
# @param against [Tokens::Color] the fixed other member of the pair
|
|
116
133
|
# @return [Tokens::Color, nil] the nearest passing color, or nil
|
|
117
|
-
def nearest_aa(candidate, against)
|
|
134
|
+
def nearest_aa(candidate, against) = nearest_contrast(candidate, against, AA)
|
|
135
|
+
|
|
136
|
+
# The same walk to any contrast floor: AA for text, EDGE for lines.
|
|
137
|
+
#
|
|
138
|
+
# @param candidate [Tokens::Color] the color to walk
|
|
139
|
+
# @param against [Tokens::Color] the fixed other member of the pair
|
|
140
|
+
# @param floor [Float] the ratio the pair must reach
|
|
141
|
+
# @return [Tokens::Color, nil] the nearest passing color, or nil
|
|
142
|
+
def nearest_contrast(candidate, against, floor)
|
|
118
143
|
step = candidate.l > against.l ? 0.01 : -0.01
|
|
144
|
+
step = 0.01 if candidate.l == against.l && against.l < 0.5
|
|
119
145
|
color = candidate
|
|
120
|
-
until color.contrast_ratio(against) >=
|
|
146
|
+
until color.contrast_ratio(against) >= floor
|
|
121
147
|
next_l = (color.l + step).round(3)
|
|
122
148
|
return nil unless next_l.between?(0.0, 1.0)
|
|
123
149
|
|
|
@@ -166,6 +192,28 @@ module Poetry
|
|
|
166
192
|
end
|
|
167
193
|
end
|
|
168
194
|
|
|
195
|
+
# Border and input against every surface, in each mode the import
|
|
196
|
+
# touches one of them: the lines too faint to see (the case of a dark
|
|
197
|
+
# palette giving border, input and card one value).
|
|
198
|
+
def faint_edges(overrides)
|
|
199
|
+
overrides.flat_map do |mode, colors|
|
|
200
|
+
next [] unless colors.keys.intersect?(EDGES + SURFACES)
|
|
201
|
+
|
|
202
|
+
EDGES.product(SURFACES).filter_map do |edge, surface|
|
|
203
|
+
line = merged(mode, edge, overrides)
|
|
204
|
+
fill = merged(mode, surface, overrides)
|
|
205
|
+
next unless line && fill
|
|
206
|
+
|
|
207
|
+
ratio = line.contrast_ratio(fill)
|
|
208
|
+
next if ratio >= EDGE
|
|
209
|
+
|
|
210
|
+
walked = nearest_contrast(line, fill, EDGE)
|
|
211
|
+
Edge.new(mode: mode, edge: edge, surface: surface, ratio: ratio,
|
|
212
|
+
suggestion: walked && "--#{edge}: #{walked.css}")
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
169
217
|
# Every ledger pair an override touches, measured on the merged set.
|
|
170
218
|
def enforce_contrast(overrides, force:)
|
|
171
219
|
Tokens::ContrastGate::LEDGER.flat_map do |mode, specs|
|
data/lib/poetry/core/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: poetry-core
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.13
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Matt Solt
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-10-
|
|
10
|
+
date: 2026-10-13 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: rails
|
|
@@ -101,10 +101,15 @@ files:
|
|
|
101
101
|
- app/javascript/poetry/core/autocomplete_controller.js
|
|
102
102
|
- app/javascript/poetry/core/behaviors/announce.js
|
|
103
103
|
- app/javascript/poetry/core/behaviors/before_cache.js
|
|
104
|
+
- app/javascript/poetry/core/behaviors/clipboard.js
|
|
104
105
|
- app/javascript/poetry/core/behaviors/escape.js
|
|
106
|
+
- app/javascript/poetry/core/behaviors/in_view.js
|
|
107
|
+
- app/javascript/poetry/core/behaviors/interval.js
|
|
105
108
|
- app/javascript/poetry/core/behaviors/layers.js
|
|
106
109
|
- app/javascript/poetry/core/behaviors/listen.js
|
|
110
|
+
- app/javascript/poetry/core/behaviors/measure.js
|
|
107
111
|
- app/javascript/poetry/core/behaviors/mobile.js
|
|
112
|
+
- app/javascript/poetry/core/behaviors/mutation.js
|
|
108
113
|
- app/javascript/poetry/core/behaviors/overlay.js
|
|
109
114
|
- app/javascript/poetry/core/behaviors/portal.js
|
|
110
115
|
- app/javascript/poetry/core/behaviors/presence.js
|