copy_tuner_client 1.5.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitattributes +2 -2
- data/.github/workflows/rspec.yml +1 -1
- data/CHANGELOG.md +27 -0
- data/CLAUDE.md +6 -4
- data/README.md +6 -27
- data/UPGRADING.md +201 -0
- data/app/assets/javascripts/copytuner.js +162 -250
- data/biome.json +39 -0
- data/index.html +9 -11
- data/lib/copy_tuner_client/cache.rb +0 -2
- data/lib/copy_tuner_client/configuration.rb +23 -37
- data/lib/copy_tuner_client/copyray/marker.rb +24 -0
- data/lib/copy_tuner_client/copyray/rewriter.rb +113 -0
- data/lib/copy_tuner_client/copyray.rb +11 -4
- data/lib/copy_tuner_client/copyray_middleware.rb +10 -8
- data/lib/copy_tuner_client/engine.rb +1 -1
- data/lib/copy_tuner_client/helper_extension.rb +0 -3
- data/lib/copy_tuner_client/i18n_backend.rb +5 -12
- data/lib/copy_tuner_client/version.rb +1 -1
- data/mise.toml +3 -0
- data/package.json +9 -13
- data/pnpm-lock.yaml +600 -0
- data/skills/copy-tuner/SKILL.md +37 -6
- data/skills/copy-tuner-to-locales-cleanup/SKILL.md +4 -4
- data/skills/copy-tuner-to-locales-migrate-prefix/references/local-first-regexp.md +4 -9
- data/skills/copy-tuner-to-t-migrate/SKILL.md +131 -0
- data/skills/copy-tuner-to-t-migrate/scripts/migrate_tt.rb +189 -0
- data/spec/copy_tuner_client/cache_spec.rb +2 -10
- data/spec/copy_tuner_client/client_spec.rb +1 -0
- data/spec/copy_tuner_client/configuration_spec.rb +16 -16
- data/spec/copy_tuner_client/copyray/marker_spec.rb +41 -0
- data/spec/copy_tuner_client/copyray/rewriter_spec.rb +216 -0
- data/spec/copy_tuner_client/copyray_middleware_spec.rb +88 -0
- data/spec/copy_tuner_client/copyray_spec.rb +22 -39
- data/spec/copy_tuner_client/helper_extension_spec.rb +18 -5
- data/spec/copy_tuner_client/i18n_backend_spec.rb +8 -15
- data/spec/support/client_spec_helpers.rb +0 -1
- data/src/copyray-overlay.ts +125 -0
- data/src/copytuner-bar.ts +153 -0
- data/src/main.ts +33 -26
- data/src/{copyray.css → styles.ts} +112 -136
- data/src/util.ts +9 -1
- data/tsconfig.json +5 -10
- data/vite.config.ts +0 -1
- metadata +15 -8
- data/.eslintrc.js +0 -12
- data/app/assets/stylesheets/copytuner.css +0 -1
- data/src/copyray.ts +0 -130
- data/src/copytuner_bar.ts +0 -115
- data/src/specimen.ts +0 -84
- data/yarn.lock +0 -2540
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { BAR_STYLES } from './styles'
|
|
2
|
+
import { debounce } from './util'
|
|
3
|
+
|
|
4
|
+
type OpenCallback = (key: string) => void
|
|
5
|
+
|
|
6
|
+
type InitOptions = {
|
|
7
|
+
url: string
|
|
8
|
+
data: Record<string, string>
|
|
9
|
+
keysSkipped: boolean
|
|
10
|
+
onOpen: OpenCallback
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// 画面下部のツールバー。CopyTuner / Sync ボタン、ページ内翻訳の検索・ログメニューを Shadow DOM 内に描画する。
|
|
14
|
+
export class CopytunerBar extends HTMLElement {
|
|
15
|
+
#onOpen: OpenCallback = () => {}
|
|
16
|
+
#searchBox!: HTMLInputElement
|
|
17
|
+
#logMenu!: HTMLDivElement
|
|
18
|
+
|
|
19
|
+
constructor() {
|
|
20
|
+
super()
|
|
21
|
+
this.attachShadow({ mode: 'open' })
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// custom element の constructor 内では属性・プロパティを変更できない(createElement が弾く)ため、
|
|
25
|
+
// hidden の初期化は DOM 挿入後に呼ばれる connectedCallback で行う。
|
|
26
|
+
connectedCallback() {
|
|
27
|
+
this.hidden = true
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// url/data/keysSkipped/onOpen はオブジェクトや関数を含むため属性ではなくメソッドで受け渡す。
|
|
31
|
+
init({ url, data, keysSkipped, onOpen }: InitOptions) {
|
|
32
|
+
this.#onOpen = onOpen
|
|
33
|
+
const shadow = this.shadowRoot as ShadowRoot
|
|
34
|
+
|
|
35
|
+
const style = document.createElement('style')
|
|
36
|
+
style.textContent = BAR_STYLES
|
|
37
|
+
shadow.append(style)
|
|
38
|
+
|
|
39
|
+
// 元々 Rails から出力されていたマークアップに合わせたボタン群。
|
|
40
|
+
// url は設定値だが innerHTML に直接埋めず setAttribute で渡す(XSS 面で安全側に倒す)。
|
|
41
|
+
const copyTunerButton = this.makeButton('CopyTuner', url, '_blank')
|
|
42
|
+
const syncButton = this.makeButton('Sync', '/copytuner', '_blank')
|
|
43
|
+
const openLogButton = this.makeButton('Translations in this page', 'javascript:void(0)')
|
|
44
|
+
|
|
45
|
+
this.#searchBox = document.createElement('input')
|
|
46
|
+
this.#searchBox.type = 'text'
|
|
47
|
+
this.#searchBox.classList.add('search')
|
|
48
|
+
this.#searchBox.placeholder = 'search'
|
|
49
|
+
|
|
50
|
+
shadow.append(copyTunerButton, syncButton, openLogButton, this.#searchBox)
|
|
51
|
+
|
|
52
|
+
this.#logMenu = this.makeLogMenu(data)
|
|
53
|
+
shadow.append(this.#logMenu)
|
|
54
|
+
|
|
55
|
+
// 巨大DOM/Nokogiri例外でキー付与がスキップされた場合は、オーバーレイが使えないので
|
|
56
|
+
// ツールバー(Translations in this page)から編集する旨を案内する。
|
|
57
|
+
if (keysSkipped) {
|
|
58
|
+
this.appendSkippedNotice()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
openLogButton.addEventListener('click', (event) => {
|
|
62
|
+
event.preventDefault()
|
|
63
|
+
this.toggleLogMenu()
|
|
64
|
+
})
|
|
65
|
+
this.#searchBox.addEventListener('input', debounce(this.onSearch.bind(this), 250))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
show() {
|
|
69
|
+
this.hidden = false
|
|
70
|
+
this.#searchBox.focus()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
hide() {
|
|
74
|
+
this.hidden = true
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private makeButton(label: string, href: string, target?: string): HTMLAnchorElement {
|
|
78
|
+
const button = document.createElement('a')
|
|
79
|
+
button.classList.add('button')
|
|
80
|
+
button.textContent = label
|
|
81
|
+
button.href = href
|
|
82
|
+
if (target) {
|
|
83
|
+
button.target = target
|
|
84
|
+
}
|
|
85
|
+
return button
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private appendSkippedNotice() {
|
|
89
|
+
const notice = document.createElement('span')
|
|
90
|
+
notice.classList.add('notice')
|
|
91
|
+
notice.textContent = '⚠ This page is too large for the overlay. Use "Translations in this page" to edit.'
|
|
92
|
+
;(this.shadowRoot as ShadowRoot).append(notice)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private showLogMenu() {
|
|
96
|
+
this.#logMenu.hidden = false
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private toggleLogMenu() {
|
|
100
|
+
this.#logMenu.hidden = !this.#logMenu.hidden
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private makeLogMenu(data: Record<string, string>): HTMLDivElement {
|
|
104
|
+
const div = document.createElement('div')
|
|
105
|
+
div.classList.add('log-menu')
|
|
106
|
+
div.hidden = true
|
|
107
|
+
|
|
108
|
+
const table = document.createElement('table')
|
|
109
|
+
const tbody = document.createElement('tbody')
|
|
110
|
+
|
|
111
|
+
for (const key of Object.keys(data).sort()) {
|
|
112
|
+
const value = data[key]
|
|
113
|
+
if (value === '') {
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const td1 = document.createElement('td')
|
|
118
|
+
td1.textContent = key
|
|
119
|
+
const td2 = document.createElement('td')
|
|
120
|
+
td2.textContent = value
|
|
121
|
+
const tr = document.createElement('tr')
|
|
122
|
+
tr.dataset.key = key
|
|
123
|
+
|
|
124
|
+
tr.addEventListener('click', ({ currentTarget }) => {
|
|
125
|
+
const row = currentTarget as HTMLTableRowElement
|
|
126
|
+
if (row.dataset.key) {
|
|
127
|
+
this.#onOpen(row.dataset.key)
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
tr.append(td1, td2)
|
|
132
|
+
tbody.append(tr)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
table.append(tbody)
|
|
136
|
+
div.append(table)
|
|
137
|
+
|
|
138
|
+
return div
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private onSearch() {
|
|
142
|
+
// debounce 経由で遅延実行されると Event.target は null 化されるため、検索ボックスを直接参照する
|
|
143
|
+
const keyword = this.#searchBox.value.trim()
|
|
144
|
+
this.showLogMenu()
|
|
145
|
+
|
|
146
|
+
const rows = [...this.#logMenu.querySelectorAll('tr')]
|
|
147
|
+
for (const row of rows) {
|
|
148
|
+
const isShow =
|
|
149
|
+
keyword === '' || [...row.querySelectorAll('td')].some((td) => (td.textContent ?? '').includes(keyword))
|
|
150
|
+
row.hidden = !isShow
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
data/src/main.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* eslint-disable no-console */
|
|
2
|
-
import
|
|
2
|
+
import { CopyrayOverlay } from './copyray-overlay'
|
|
3
|
+
import { CopytunerBar } from './copytuner-bar'
|
|
3
4
|
import { isMac } from './util'
|
|
4
5
|
|
|
5
6
|
declare global {
|
|
@@ -7,44 +8,50 @@ declare global {
|
|
|
7
8
|
CopyTuner: {
|
|
8
9
|
url: string
|
|
9
10
|
toggle?: () => void
|
|
10
|
-
|
|
11
|
-
data
|
|
11
|
+
data: Record<string, string>
|
|
12
|
+
// 巨大DOM/Nokogiri例外で data-copyray-key 付与をスキップしたか。
|
|
13
|
+
// true のときオーバーレイは使えないのでツールバーから編集する旨を案内する。
|
|
14
|
+
keysSkipped?: boolean
|
|
12
15
|
}
|
|
13
16
|
}
|
|
14
17
|
}
|
|
15
18
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
// NOTE: 元々railsから出力されいてたマークアップに合わせてひとまず、、
|
|
19
|
-
const appendCopyTunerBar = (url: string) => {
|
|
20
|
-
const bar = document.createElement('div')
|
|
21
|
-
bar.id = 'copy-tuner-bar'
|
|
22
|
-
bar.classList.add('copy-tuner-hidden')
|
|
23
|
-
bar.innerHTML = `
|
|
24
|
-
<a class="copy-tuner-bar-button" target="_blank" href="${url}">CopyTuner</a>
|
|
25
|
-
<a href="/copytuner" target="_blank" class="copy-tuner-bar-button">Sync</a>
|
|
26
|
-
<a href="javascript:void(0)" class="copy-tuner-bar-open-log copy-tuner-bar-button js-copy-tuner-bar-open-log">Translations in this page</a>
|
|
27
|
-
<input type="text" class="copy-tuner-bar__search js-copy-tuner-bar-search" placeholder="search">
|
|
28
|
-
`
|
|
29
|
-
document.body.append(bar)
|
|
30
|
-
}
|
|
19
|
+
customElements.define('copytuner-bar', CopytunerBar)
|
|
20
|
+
customElements.define('copyray-overlay', CopyrayOverlay)
|
|
31
21
|
|
|
32
22
|
const start = () => {
|
|
33
|
-
const { url, data } = window.CopyTuner
|
|
23
|
+
const { url, data, keysSkipped } = window.CopyTuner
|
|
24
|
+
const onOpen = (key: string) => window.open(`${url}/blurbs/${key}/edit`)
|
|
25
|
+
|
|
26
|
+
const bar = document.createElement('copytuner-bar') as CopytunerBar
|
|
27
|
+
document.body.append(bar)
|
|
28
|
+
bar.init({ url, data, keysSkipped: Boolean(keysSkipped), onOpen })
|
|
29
|
+
|
|
30
|
+
const overlay = document.createElement('copyray-overlay') as CopyrayOverlay
|
|
31
|
+
overlay.onOpen = onOpen
|
|
32
|
+
document.body.append(overlay)
|
|
33
|
+
|
|
34
|
+
const show = () => {
|
|
35
|
+
overlay.show()
|
|
36
|
+
bar.show()
|
|
37
|
+
}
|
|
38
|
+
const hide = () => {
|
|
39
|
+
overlay.hide()
|
|
40
|
+
bar.hide()
|
|
41
|
+
}
|
|
42
|
+
const toggle = () => (overlay.isShowing ? hide() : show())
|
|
34
43
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
window.CopyTuner.toggle = () => copyray.toggle()
|
|
44
|
+
overlay.onToggle = toggle
|
|
45
|
+
window.CopyTuner.toggle = toggle
|
|
38
46
|
|
|
39
47
|
document.addEventListener('keydown', (event) => {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
copyray.hide()
|
|
48
|
+
if (overlay.isShowing && ['Escape', 'Esc'].includes(event.key)) {
|
|
49
|
+
hide()
|
|
43
50
|
return
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
if (((isMac && event.metaKey) || (!isMac && event.ctrlKey)) && event.shiftKey && event.key.toLowerCase() === 'k') {
|
|
47
|
-
|
|
54
|
+
toggle()
|
|
48
55
|
}
|
|
49
56
|
})
|
|
50
57
|
|
|
@@ -1,116 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
/* selector for element and children */
|
|
4
|
-
#copyray-overlay,
|
|
5
|
-
#copyray-overlay *,
|
|
6
|
-
#copyray-overlay a:hover,
|
|
7
|
-
#copyray-overlay a:visited,
|
|
8
|
-
#copyray-overlay a:active {
|
|
9
|
-
background: none;
|
|
10
|
-
border: none;
|
|
11
|
-
bottom: auto;
|
|
12
|
-
clear: none;
|
|
13
|
-
cursor: default;
|
|
14
|
-
float: none;
|
|
15
|
-
font-family: Arial, Helvetica, sans-serif;
|
|
16
|
-
font-size: medium;
|
|
17
|
-
font-style: normal;
|
|
18
|
-
font-weight: normal;
|
|
19
|
-
height: auto;
|
|
20
|
-
left: auto;
|
|
21
|
-
letter-spacing: normal;
|
|
22
|
-
line-height: normal;
|
|
23
|
-
max-height: none;
|
|
24
|
-
max-width: none;
|
|
25
|
-
min-height: 0;
|
|
26
|
-
min-width: 0;
|
|
27
|
-
overflow: visible;
|
|
28
|
-
position: static;
|
|
29
|
-
right: auto;
|
|
30
|
-
text-align: left;
|
|
31
|
-
text-decoration: none;
|
|
32
|
-
text-indent: 0;
|
|
33
|
-
text-transform: none;
|
|
34
|
-
top: auto;
|
|
35
|
-
visibility: visible;
|
|
36
|
-
white-space: normal;
|
|
37
|
-
width: auto;
|
|
38
|
-
z-index: auto;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
#copyray-overlay {
|
|
42
|
-
position: fixed;
|
|
43
|
-
left: 0;
|
|
44
|
-
top: 0;
|
|
45
|
-
bottom: 0;
|
|
46
|
-
right: 0;
|
|
47
|
-
background-image: radial-gradient(
|
|
48
|
-
ellipse farthest-corner at center,
|
|
49
|
-
rgba(0, 0, 0, 0.4) 10%,
|
|
50
|
-
rgba(0, 0, 0, 0.8) 100%
|
|
51
|
-
);
|
|
52
|
-
z-index: 9000;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
.copyray-specimen {
|
|
56
|
-
position: absolute;
|
|
57
|
-
background: rgba(255, 255, 255, 0.15);
|
|
58
|
-
outline: 1px solid rgba(255, 255, 255, 0.8);
|
|
59
|
-
outline-offset: -1px;
|
|
60
|
-
color: #666;
|
|
61
|
-
font-family: 'Helvetica Neue', sans-serif;
|
|
62
|
-
font-size: 13px;
|
|
63
|
-
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.7);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
.copyray-specimen:hover {
|
|
67
|
-
cursor: pointer;
|
|
68
|
-
background: rgba(255, 255, 255, 0.4);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
.copyray-specimen.Specimen {
|
|
72
|
-
outline: 1px solid rgba(255, 50, 50, 0.8);
|
|
73
|
-
background: rgba(255, 50, 50, 0.1);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
.copyray-specimen.Specimen:hover {
|
|
77
|
-
background: rgba(255, 50, 50, 0.4);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
.copyray-specimen-handle {
|
|
81
|
-
float: left;
|
|
82
|
-
background: #fff;
|
|
83
|
-
padding: 0 3px;
|
|
84
|
-
color: #333;
|
|
85
|
-
font-size: 10px;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
.copyray-specimen-handle.Specimen {
|
|
89
|
-
background: rgba(255, 50, 50, 0.8);
|
|
90
|
-
color: #fff;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
a.copyray-toggle-button {
|
|
94
|
-
display: block;
|
|
95
|
-
position: fixed;
|
|
96
|
-
left: 0;
|
|
97
|
-
bottom: 0;
|
|
98
|
-
color: white;
|
|
99
|
-
background: black;
|
|
100
|
-
padding: 12px 16px;
|
|
101
|
-
border-radius: 0 10px 0 0;
|
|
102
|
-
opacity: 0;
|
|
103
|
-
transition: opacity 0.6s ease-in-out;
|
|
104
|
-
z-index: 10000;
|
|
105
|
-
font-size: 12px;
|
|
106
|
-
cursor: pointer;
|
|
107
|
-
}
|
|
1
|
+
// 各 custom element の Shadow root に <style> として注入する CSS。
|
|
2
|
+
// Shadow DOM のスタイル隔離が効くため、旧 copyray.css にあった #copyray-overlay * のグローバルリセットは不要。
|
|
108
3
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
#copy-tuner-bar {
|
|
4
|
+
// ツールバー(<copytuner-bar>)のスタイル。:host にバー本体のレイアウトを定義する。
|
|
5
|
+
export const BAR_STYLES = `
|
|
6
|
+
:host {
|
|
114
7
|
position: fixed;
|
|
115
8
|
left: 0;
|
|
116
9
|
right: 0;
|
|
@@ -124,44 +17,45 @@ a.copyray-toggle-button:hover {
|
|
|
124
17
|
z-index: 2147483647;
|
|
125
18
|
box-shadow: 0 -1px 0 rgba(255, 255, 255, 0.1), inset 0 2px 6px rgba(0, 0, 0, 0.8);
|
|
126
19
|
background-image: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.3));
|
|
20
|
+
box-sizing: border-box;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
:host([hidden]) {
|
|
24
|
+
display: none;
|
|
127
25
|
}
|
|
128
26
|
|
|
129
|
-
|
|
27
|
+
.log-menu {
|
|
130
28
|
position: fixed;
|
|
131
29
|
left: 0;
|
|
132
30
|
right: 0;
|
|
133
31
|
bottom: 40px;
|
|
134
32
|
max-height: calc(100vh - 40px);
|
|
135
33
|
background: #222;
|
|
136
|
-
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
|
137
34
|
color: #fff;
|
|
138
|
-
z-index: 2147483647;
|
|
139
35
|
overflow-y: auto;
|
|
140
36
|
}
|
|
141
37
|
|
|
142
|
-
|
|
38
|
+
.log-menu[hidden] {
|
|
39
|
+
display: none;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.log-menu tbody td {
|
|
143
43
|
padding: 2px 8px;
|
|
144
44
|
}
|
|
145
45
|
|
|
146
|
-
|
|
46
|
+
.log-menu tbody tr {
|
|
147
47
|
cursor: pointer;
|
|
148
48
|
}
|
|
149
49
|
|
|
150
|
-
|
|
50
|
+
.log-menu tbody tr:hover {
|
|
151
51
|
background: #444;
|
|
152
52
|
}
|
|
153
53
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
#copy-tuner-bar-log-menu tbody a:hover,
|
|
159
|
-
#copy-tuner-bar-log-menu tbody a:focus {
|
|
160
|
-
color: #fff;
|
|
161
|
-
text-decoration: underline;
|
|
54
|
+
.log-menu tbody tr[hidden] {
|
|
55
|
+
display: none;
|
|
162
56
|
}
|
|
163
57
|
|
|
164
|
-
.
|
|
58
|
+
.button {
|
|
165
59
|
position: relative;
|
|
166
60
|
display: inline-block;
|
|
167
61
|
color: #fff;
|
|
@@ -178,18 +72,26 @@ a.copyray-toggle-button:hover {
|
|
|
178
72
|
box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.2),
|
|
179
73
|
inset 0 0 2px rgba(255, 255, 255, 0.2);
|
|
180
74
|
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.4);
|
|
75
|
+
text-decoration: none;
|
|
181
76
|
}
|
|
182
77
|
|
|
183
|
-
.
|
|
184
|
-
.
|
|
78
|
+
.button:hover,
|
|
79
|
+
.button:focus {
|
|
185
80
|
color: #fff;
|
|
186
81
|
text-decoration: none;
|
|
187
82
|
background-color: #555;
|
|
188
83
|
}
|
|
189
84
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
85
|
+
.notice {
|
|
86
|
+
display: inline-block;
|
|
87
|
+
margin: 8px;
|
|
88
|
+
font-size: 13px;
|
|
89
|
+
line-height: 24px;
|
|
90
|
+
vertical-align: middle;
|
|
91
|
+
color: #ffd24d;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.search {
|
|
193
95
|
appearance: none;
|
|
194
96
|
border: none;
|
|
195
97
|
border-radius: 2px;
|
|
@@ -204,13 +106,87 @@ input[type='text'].copy-tuner-bar__search {
|
|
|
204
106
|
height: auto;
|
|
205
107
|
font-size: 14px;
|
|
206
108
|
}
|
|
109
|
+
`
|
|
110
|
+
|
|
111
|
+
// オーバーレイ(<copyray-overlay>)のスタイル。
|
|
112
|
+
// :host はドキュメント原点基準(position: absolute; top/left: 0)にして、
|
|
113
|
+
// 子の specimen を computeBoundingBox のページ座標で absolute 配置できるようにする。
|
|
114
|
+
// 背景の暗転(.backdrop)だけは viewport 固定(fixed)にする。
|
|
115
|
+
export const OVERLAY_STYLES = `
|
|
116
|
+
:host {
|
|
117
|
+
position: absolute;
|
|
118
|
+
top: 0;
|
|
119
|
+
left: 0;
|
|
120
|
+
width: 0;
|
|
121
|
+
height: 0;
|
|
122
|
+
}
|
|
207
123
|
|
|
208
|
-
|
|
209
|
-
display: none
|
|
124
|
+
:host([hidden]) {
|
|
125
|
+
display: none;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.backdrop {
|
|
129
|
+
position: fixed;
|
|
130
|
+
inset: 0;
|
|
131
|
+
background-image: radial-gradient(
|
|
132
|
+
ellipse farthest-corner at center,
|
|
133
|
+
rgba(0, 0, 0, 0.4) 10%,
|
|
134
|
+
rgba(0, 0, 0, 0.8) 100%
|
|
135
|
+
);
|
|
136
|
+
z-index: 9000;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.specimen {
|
|
140
|
+
position: absolute;
|
|
141
|
+
background: rgba(255, 50, 50, 0.1);
|
|
142
|
+
outline: 1px solid rgba(255, 50, 50, 0.8);
|
|
143
|
+
outline-offset: -1px;
|
|
144
|
+
color: #666;
|
|
145
|
+
font-family: 'Helvetica Neue', sans-serif;
|
|
146
|
+
font-size: 13px;
|
|
147
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.7);
|
|
148
|
+
z-index: 2000000000;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.specimen:hover {
|
|
152
|
+
cursor: pointer;
|
|
153
|
+
background: rgba(255, 50, 50, 0.4);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.specimen-handle {
|
|
157
|
+
float: left;
|
|
158
|
+
margin: 0 2px 2px 0;
|
|
159
|
+
background: rgba(255, 50, 50, 0.8);
|
|
160
|
+
padding: 0 3px;
|
|
161
|
+
color: #fff;
|
|
162
|
+
font-size: 10px;
|
|
163
|
+
cursor: pointer;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.toggle-button {
|
|
167
|
+
display: block;
|
|
168
|
+
position: fixed;
|
|
169
|
+
left: 0;
|
|
170
|
+
bottom: 0;
|
|
171
|
+
color: white;
|
|
172
|
+
background: black;
|
|
173
|
+
padding: 12px 16px;
|
|
174
|
+
border-radius: 0 10px 0 0;
|
|
175
|
+
opacity: 0;
|
|
176
|
+
transition: opacity 0.6s ease-in-out;
|
|
177
|
+
z-index: 10000;
|
|
178
|
+
font-size: 12px;
|
|
179
|
+
cursor: pointer;
|
|
180
|
+
text-decoration: none;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.toggle-button:hover {
|
|
184
|
+
opacity: 1;
|
|
210
185
|
}
|
|
211
186
|
|
|
212
187
|
@media screen and (max-width: 480px) {
|
|
213
|
-
.
|
|
214
|
-
display: none
|
|
188
|
+
.toggle-button {
|
|
189
|
+
display: none;
|
|
215
190
|
}
|
|
216
191
|
}
|
|
192
|
+
`
|
data/src/util.ts
CHANGED
|
@@ -35,4 +35,12 @@ const computeBoundingBox = (element) => {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
const debounce = <A extends unknown[]>(fn: (...args: A) => void, wait: number) => {
|
|
39
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
40
|
+
return (...args: A) => {
|
|
41
|
+
clearTimeout(timer)
|
|
42
|
+
timer = setTimeout(() => fn(...args), wait)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export { computeBoundingBox, debounce, getOffset, isMac, isVisible }
|
data/tsconfig.json
CHANGED
|
@@ -3,11 +3,9 @@
|
|
|
3
3
|
"target": "ESNext",
|
|
4
4
|
"useDefineForClassFields": true,
|
|
5
5
|
"module": "ESNext",
|
|
6
|
-
"lib": [
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
],
|
|
10
|
-
"moduleResolution": "Node",
|
|
6
|
+
"lib": ["ESNext", "DOM"],
|
|
7
|
+
"moduleResolution": "bundler",
|
|
8
|
+
"verbatimModuleSyntax": true,
|
|
11
9
|
"strict": true,
|
|
12
10
|
"sourceMap": true,
|
|
13
11
|
"resolveJsonModule": true,
|
|
@@ -17,10 +15,7 @@
|
|
|
17
15
|
"noUnusedLocals": true,
|
|
18
16
|
"noUnusedParameters": true,
|
|
19
17
|
"noImplicitReturns": true,
|
|
20
|
-
"skipLibCheck": true
|
|
18
|
+
"skipLibCheck": true
|
|
21
19
|
},
|
|
22
|
-
"include": [
|
|
23
|
-
"client",
|
|
24
|
-
"src",
|
|
25
|
-
]
|
|
20
|
+
"include": ["src"]
|
|
26
21
|
}
|
data/vite.config.ts
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: copy_tuner_client
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 2.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- SonicGarden
|
|
@@ -183,7 +183,6 @@ extensions: []
|
|
|
183
183
|
extra_rdoc_files: []
|
|
184
184
|
files:
|
|
185
185
|
- ".babelrc"
|
|
186
|
-
- ".eslintrc.js"
|
|
187
186
|
- ".gitattributes"
|
|
188
187
|
- ".github/dependabot.yml"
|
|
189
188
|
- ".github/workflows/rspec.yml"
|
|
@@ -198,8 +197,9 @@ files:
|
|
|
198
197
|
- LICENSE.txt
|
|
199
198
|
- README.md
|
|
200
199
|
- Rakefile
|
|
200
|
+
- UPGRADING.md
|
|
201
201
|
- app/assets/javascripts/copytuner.js
|
|
202
|
-
-
|
|
202
|
+
- biome.json
|
|
203
203
|
- copy_tuner_client.gemspec
|
|
204
204
|
- gemfiles/8.0.gemfile
|
|
205
205
|
- gemfiles/8.1.gemfile
|
|
@@ -210,6 +210,8 @@ files:
|
|
|
210
210
|
- lib/copy_tuner_client/client.rb
|
|
211
211
|
- lib/copy_tuner_client/configuration.rb
|
|
212
212
|
- lib/copy_tuner_client/copyray.rb
|
|
213
|
+
- lib/copy_tuner_client/copyray/marker.rb
|
|
214
|
+
- lib/copy_tuner_client/copyray/rewriter.rb
|
|
213
215
|
- lib/copy_tuner_client/copyray_middleware.rb
|
|
214
216
|
- lib/copy_tuner_client/dotted_hash.rb
|
|
215
217
|
- lib/copy_tuner_client/engine.rb
|
|
@@ -227,7 +229,9 @@ files:
|
|
|
227
229
|
- lib/copy_tuner_client/translation_log.rb
|
|
228
230
|
- lib/copy_tuner_client/version.rb
|
|
229
231
|
- lib/tasks/copy_tuner_client_tasks.rake
|
|
232
|
+
- mise.toml
|
|
230
233
|
- package.json
|
|
234
|
+
- pnpm-lock.yaml
|
|
231
235
|
- skills/copy-tuner-to-locales-cleanup/SKILL.md
|
|
232
236
|
- skills/copy-tuner-to-locales-cleanup/references/example-touchpoints.md
|
|
233
237
|
- skills/copy-tuner-to-locales-cleanup/references/verification-final.md
|
|
@@ -237,10 +241,15 @@ files:
|
|
|
237
241
|
- skills/copy-tuner-to-locales-migrate-prefix/references/local-first-regexp.md
|
|
238
242
|
- skills/copy-tuner-to-locales-migrate-prefix/references/verification-per-prefix.md
|
|
239
243
|
- skills/copy-tuner-to-locales-migrate-prefix/scripts/migrate_prefix.rb
|
|
244
|
+
- skills/copy-tuner-to-t-migrate/SKILL.md
|
|
245
|
+
- skills/copy-tuner-to-t-migrate/scripts/migrate_tt.rb
|
|
240
246
|
- skills/copy-tuner/SKILL.md
|
|
241
247
|
- spec/copy_tuner_client/cache_spec.rb
|
|
242
248
|
- spec/copy_tuner_client/client_spec.rb
|
|
243
249
|
- spec/copy_tuner_client/configuration_spec.rb
|
|
250
|
+
- spec/copy_tuner_client/copyray/marker_spec.rb
|
|
251
|
+
- spec/copy_tuner_client/copyray/rewriter_spec.rb
|
|
252
|
+
- spec/copy_tuner_client/copyray_middleware_spec.rb
|
|
244
253
|
- spec/copy_tuner_client/copyray_spec.rb
|
|
245
254
|
- spec/copy_tuner_client/dotted_hash_spec.rb
|
|
246
255
|
- spec/copy_tuner_client/helper_extension_spec.rb
|
|
@@ -262,18 +271,16 @@ files:
|
|
|
262
271
|
- spec/support/fake_unicorn.rb
|
|
263
272
|
- spec/support/middleware_stack.rb
|
|
264
273
|
- spec/support/writing_cache.rb
|
|
265
|
-
- src/copyray.
|
|
266
|
-
- src/
|
|
267
|
-
- src/copytuner_bar.ts
|
|
274
|
+
- src/copyray-overlay.ts
|
|
275
|
+
- src/copytuner-bar.ts
|
|
268
276
|
- src/main.ts
|
|
269
|
-
- src/
|
|
277
|
+
- src/styles.ts
|
|
270
278
|
- src/util.ts
|
|
271
279
|
- src/vite-env.d.ts
|
|
272
280
|
- tsconfig.json
|
|
273
281
|
- ui/views/copytuner/index.html.erb
|
|
274
282
|
- ui/views/layouts/copytuner_default.html.erb
|
|
275
283
|
- vite.config.ts
|
|
276
|
-
- yarn.lock
|
|
277
284
|
homepage: https://github.com/SonicGarden/copy-tuner-ruby-client
|
|
278
285
|
licenses: []
|
|
279
286
|
metadata:
|
data/.eslintrc.js
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
module.exports = {
|
|
2
|
-
plugins: ['@sonicgarden'],
|
|
3
|
-
extends: [
|
|
4
|
-
'plugin:@sonicgarden/browser',
|
|
5
|
-
'plugin:@sonicgarden/recommended',
|
|
6
|
-
'plugin:@sonicgarden/typescript',
|
|
7
|
-
'plugin:@sonicgarden/prettier',
|
|
8
|
-
],
|
|
9
|
-
settings: {
|
|
10
|
-
'import/internal-regex': '^@/',
|
|
11
|
-
},
|
|
12
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@charset "UTF-8";#copyray-overlay,#copyray-overlay *,#copyray-overlay a:hover,#copyray-overlay a:visited,#copyray-overlay a:active{background:none;border:none;bottom:auto;clear:none;cursor:default;float:none;font-family:Arial,Helvetica,sans-serif;font-size:medium;font-style:normal;font-weight:400;height:auto;left:auto;letter-spacing:normal;line-height:normal;max-height:none;max-width:none;min-height:0;min-width:0;overflow:visible;position:static;right:auto;text-align:left;text-decoration:none;text-indent:0;text-transform:none;top:auto;visibility:visible;white-space:normal;width:auto;z-index:auto}#copyray-overlay{position:fixed;left:0;top:0;bottom:0;right:0;background-image:radial-gradient(ellipse farthest-corner at center,rgba(0,0,0,.4) 10%,rgba(0,0,0,.8) 100%);z-index:9000}.copyray-specimen{position:absolute;background:rgba(255,255,255,.15);outline:1px solid rgba(255,255,255,.8);outline-offset:-1px;color:#666;font-family:Helvetica Neue,sans-serif;font-size:13px;box-shadow:0 1px 3px #000000b3}.copyray-specimen:hover{cursor:pointer;background:rgba(255,255,255,.4)}.copyray-specimen.Specimen{outline:1px solid rgba(255,50,50,.8);background:rgba(255,50,50,.1)}.copyray-specimen.Specimen:hover{background:rgba(255,50,50,.4)}.copyray-specimen-handle{float:left;background:#fff;padding:0 3px;color:#333;font-size:10px}.copyray-specimen-handle.Specimen{background:rgba(255,50,50,.8);color:#fff}a.copyray-toggle-button{display:block;position:fixed;left:0;bottom:0;color:#fff;background:black;padding:12px 16px;border-radius:0 10px 0 0;opacity:0;transition:opacity .6s ease-in-out;z-index:10000;font-size:12px;cursor:pointer}a.copyray-toggle-button:hover{opacity:1}#copy-tuner-bar{position:fixed;left:0;right:0;bottom:0;height:40px;padding:0 8px;background:#222;font-family:Helvetica Neue,Helvetica,Arial,sans-serif;font-weight:200;color:#fff;z-index:2147483647;box-shadow:0 -1px #ffffff1a,inset 0 2px 6px #000c;background-image:linear-gradient(rgba(0,0,0,0),rgba(0,0,0,.3))}#copy-tuner-bar-log-menu{position:fixed;left:0;right:0;bottom:40px;max-height:calc(100vh - 40px);background:#222;font-family:Helvetica Neue,Helvetica,Arial,sans-serif;color:#fff;z-index:2147483647;overflow-y:auto}#copy-tuner-bar-log-menu tbody td{padding:2px 8px}#copy-tuner-bar-log-menu tbody tr{cursor:pointer}#copy-tuner-bar-log-menu tbody tr:hover{background:#444}#copy-tuner-bar-log-menu tbody a{color:#fff}#copy-tuner-bar-log-menu tbody a:hover,#copy-tuner-bar-log-menu tbody a:focus{color:#fff;text-decoration:underline}.copy-tuner-bar-button{position:relative;display:inline-block;color:#fff;margin:8px 1px;height:24px;line-height:24px;padding:0 8px;font-size:14px;cursor:pointer;vertical-align:middle;background-color:#444;background-image:linear-gradient(rgba(0,0,0,0),rgba(0,0,0,.2));border-radius:2px;box-shadow:1px 1px 1px #00000080,inset 0 1px #fff3,inset 0 0 2px #fff3;text-shadow:0 -1px 0 rgba(0,0,0,.4)}.copy-tuner-bar-button:hover,.copy-tuner-bar-button:focus{color:#fff;text-decoration:none;background-color:#555}input[type=text].copy-tuner-bar__search{-webkit-appearance:none;-moz-appearance:none;appearance:none;border:none;border-radius:2px;background-image:linear-gradient(rgba(0,0,0,.2),rgba(0,0,0,0));box-shadow:inset 0 1px #0003,inset 0 0 2px #0003;padding:2px 8px;margin:0;line-height:20px;vertical-align:middle;color:#000;width:auto;height:auto;font-size:14px}.copy-tuner-hidden{display:none!important}@media screen and (max-width: 480px){.hidden-on-mobile{display:none!important}}
|