copy_tuner_client 2.0.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/CLAUDE.md +4 -1
- data/README.md +6 -2
- data/app/assets/javascripts/copytuner.js +162 -255
- data/biome.json +39 -0
- data/index.html +9 -11
- data/lib/copy_tuner_client/copyray_middleware.rb +0 -6
- data/lib/copy_tuner_client/engine.rb +1 -1
- 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/spec/copy_tuner_client/copyray_middleware_spec.rb +1 -2
- data/src/copyray-overlay.ts +125 -0
- data/src/copytuner-bar.ts +153 -0
- data/src/main.ts +29 -25
- data/src/{copyray.css → styles.ts} +104 -139
- data/src/util.ts +9 -1
- data/tsconfig.json +5 -10
- data/vite.config.ts +0 -1
- metadata +7 -8
- data/.eslintrc.js +0 -12
- data/app/assets/stylesheets/copytuner.css +0 -1
- data/src/copyray.ts +0 -111
- data/src/copytuner_bar.ts +0 -129
- data/src/specimen.ts +0 -94
- data/yarn.lock +0 -2540
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a3c816e1f12e5715861194284864c00ab5d18428d3c56b6a3140cfe9db9cd360
|
|
4
|
+
data.tar.gz: d9a8cd76caf12860c0a981a840655254a5aab64665745d16f4524ded77821771
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1538da853220ee8f4bfc4942023e1afcfd55dc4f719a89b3c965d8719acb57ec5bd418d8821e72eee8adbb5b59ca2f23b462d8894b9239734ff68a30b8a0a074
|
|
7
|
+
data.tar.gz: '091eee7aae91c30f88fb294716bd9a139d55d7ac90d982602e14305d2c43203360dc81b98092e17dd931dcce1885940af27a47f0031e374e9724eaf4898934d5'
|
data/CLAUDE.md
CHANGED
|
@@ -6,9 +6,12 @@ CopyTuner の Ruby クライアント gem。Rails アプリの I18n を CopyTune
|
|
|
6
6
|
- テスト: `bundle exec rspec`(単一ファイル: `bundle exec rspec spec/copy_tuner_client/cache_spec.rb`)
|
|
7
7
|
- 特定の Rails バージョンでテスト: `BUNDLE_GEMFILE=gemfiles/8.0.gemfile bundle exec rspec`
|
|
8
8
|
- Lint: `bundle exec rubocop`(`sgcop` を継承)
|
|
9
|
-
- フロントエンドビルド: `
|
|
9
|
+
- フロントエンドビルド: `pnpm build`(開発: `pnpm dev`)
|
|
10
|
+
- フロントエンド Lint/Format: `pnpm check`(Biome)
|
|
10
11
|
- gem リリース: `bundle exec rake build|install|release`
|
|
11
12
|
|
|
13
|
+
Node.js / pnpm は mise(mise.toml)で管理。フロントエンドのツールチェーンは pnpm + Biome + Vite + TypeScript。
|
|
14
|
+
|
|
12
15
|
## アーキテクチャ
|
|
13
16
|
`Configuration#apply`(lib/copy_tuner_client/configuration.rb)が全コンポーネントを組み立てる起点:
|
|
14
17
|
- `Client` — CopyTuner サーバ / S3 との HTTP 通信
|
data/README.md
CHANGED
|
@@ -84,9 +84,13 @@ Development
|
|
|
84
84
|
`src`以下を編集してください。
|
|
85
85
|
`app/assets/*`を直接編集したらダメよ!
|
|
86
86
|
|
|
87
|
+
Node.js と pnpm は [mise](https://mise.jdx.dev/) で管理しています(`mise install` でセットアップ)。
|
|
88
|
+
|
|
87
89
|
```
|
|
88
|
-
$
|
|
89
|
-
$
|
|
90
|
+
$ pnpm install # 依存インストール
|
|
91
|
+
$ pnpm dev # 開発時
|
|
92
|
+
$ pnpm build # ビルド
|
|
93
|
+
$ pnpm check # Lint + Format(Biome)
|
|
90
94
|
```
|
|
91
95
|
|
|
92
96
|
|
|
@@ -1,257 +1,164 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
//#region src/styles.ts
|
|
2
|
+
var e = navigator.platform.toUpperCase().includes("MAC"), t = (e) => !!(e.offsetWidth || e.offsetHeight || e.getClientRects().length > 0), n = (e) => {
|
|
3
|
+
let t = e.getBoundingClientRect();
|
|
4
|
+
return {
|
|
5
|
+
top: t.top + (window.pageYOffset - document.documentElement.clientTop),
|
|
6
|
+
left: t.left + (window.pageXOffset - document.documentElement.clientLeft)
|
|
7
|
+
};
|
|
8
|
+
}, r = (e) => {
|
|
9
|
+
if (!t(e)) return null;
|
|
10
|
+
let r = n(e);
|
|
11
|
+
return r.right = r.left + e.offsetWidth, r.bottom = r.top + e.offsetHeight, {
|
|
12
|
+
left: r.left,
|
|
13
|
+
top: r.top,
|
|
14
|
+
width: r.right - r.left,
|
|
15
|
+
height: r.bottom - r.top
|
|
16
|
+
};
|
|
17
|
+
}, i = (e, t) => {
|
|
18
|
+
let n;
|
|
19
|
+
return (...r) => {
|
|
20
|
+
clearTimeout(n), n = setTimeout(() => e(...r), t);
|
|
21
|
+
};
|
|
22
|
+
}, a = () => Array.from(document.querySelectorAll("[data-copyray-key]")).map((e) => ({
|
|
23
|
+
keys: (e.getAttribute("data-copyray-key") ?? "").split(",").filter(Boolean),
|
|
24
|
+
element: e
|
|
25
|
+
})), o = class extends HTMLElement {
|
|
26
|
+
#e = () => {};
|
|
27
|
+
#t = () => {};
|
|
28
|
+
#n;
|
|
29
|
+
#r;
|
|
30
|
+
#i;
|
|
31
|
+
constructor() {
|
|
32
|
+
super();
|
|
33
|
+
let e = this.attachShadow({ mode: "open" }), t = document.createElement("style");
|
|
34
|
+
t.textContent = "\n:host {\n position: absolute;\n top: 0;\n left: 0;\n width: 0;\n height: 0;\n}\n\n:host([hidden]) {\n display: none;\n}\n\n.backdrop {\n position: fixed;\n inset: 0;\n background-image: radial-gradient(\n ellipse farthest-corner at center,\n rgba(0, 0, 0, 0.4) 10%,\n rgba(0, 0, 0, 0.8) 100%\n );\n z-index: 9000;\n}\n\n.specimen {\n position: absolute;\n background: rgba(255, 50, 50, 0.1);\n outline: 1px solid rgba(255, 50, 50, 0.8);\n outline-offset: -1px;\n color: #666;\n font-family: 'Helvetica Neue', sans-serif;\n font-size: 13px;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.7);\n z-index: 2000000000;\n}\n\n.specimen:hover {\n cursor: pointer;\n background: rgba(255, 50, 50, 0.4);\n}\n\n.specimen-handle {\n float: left;\n margin: 0 2px 2px 0;\n background: rgba(255, 50, 50, 0.8);\n padding: 0 3px;\n color: #fff;\n font-size: 10px;\n cursor: pointer;\n}\n\n.toggle-button {\n display: block;\n position: fixed;\n left: 0;\n bottom: 0;\n color: white;\n background: black;\n padding: 12px 16px;\n border-radius: 0 10px 0 0;\n opacity: 0;\n transition: opacity 0.6s ease-in-out;\n z-index: 10000;\n font-size: 12px;\n cursor: pointer;\n text-decoration: none;\n}\n\n.toggle-button:hover {\n opacity: 1;\n}\n\n@media screen and (max-width: 480px) {\n .toggle-button {\n display: none;\n }\n}\n", e.append(t), this.#n = document.createElement("div"), this.#n.classList.add("backdrop"), this.#n.addEventListener("click", () => this.hide()), this.#r = document.createElement("div"), this.#r.classList.add("specimens"), this.#i = document.createElement("a"), this.#i.classList.add("toggle-button"), this.#i.textContent = "Open CopyTuner", this.#i.addEventListener("click", () => this.#t()), e.append(this.#n, this.#r, this.#i), this.hide();
|
|
35
|
+
}
|
|
36
|
+
set onOpen(e) {
|
|
37
|
+
this.#e = e;
|
|
38
|
+
}
|
|
39
|
+
set onToggle(e) {
|
|
40
|
+
this.#t = e;
|
|
41
|
+
}
|
|
42
|
+
get isShowing() {
|
|
43
|
+
return !this.#n.hidden;
|
|
44
|
+
}
|
|
45
|
+
show() {
|
|
46
|
+
this.reset(), this.#n.hidden = !1;
|
|
47
|
+
for (let { element: e, keys: t } of a()) {
|
|
48
|
+
let n = this.makeBox(e, t);
|
|
49
|
+
n && this.#r.append(n);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
hide() {
|
|
53
|
+
this.reset(), this.#n.hidden = !0;
|
|
54
|
+
}
|
|
55
|
+
reset() {
|
|
56
|
+
this.#r.replaceChildren();
|
|
57
|
+
}
|
|
58
|
+
makeBox(e, t) {
|
|
59
|
+
let n = r(e);
|
|
60
|
+
if (n === null) return null;
|
|
61
|
+
let i = document.createElement("div");
|
|
62
|
+
i.classList.add("specimen"), i.style.left = `${n.left}px`, i.style.top = `${n.top}px`, i.style.width = `${n.width}px`, i.style.height = `${n.height}px`;
|
|
63
|
+
let { position: a, top: o, left: s } = getComputedStyle(e);
|
|
64
|
+
a === "fixed" && (i.style.position = "fixed", i.style.top = o, i.style.left = s), i.addEventListener("click", () => this.#e(t[0]));
|
|
65
|
+
for (let e of t) i.append(this.makeLabel(e));
|
|
66
|
+
return i;
|
|
67
|
+
}
|
|
68
|
+
makeLabel(e) {
|
|
69
|
+
let t = document.createElement("div");
|
|
70
|
+
return t.classList.add("specimen-handle"), t.textContent = e, t.addEventListener("click", (t) => {
|
|
71
|
+
t.stopPropagation(), this.#e(e);
|
|
72
|
+
}), t;
|
|
73
|
+
}
|
|
74
|
+
}, s = class extends HTMLElement {
|
|
75
|
+
#e = () => {};
|
|
76
|
+
#t;
|
|
77
|
+
#n;
|
|
78
|
+
constructor() {
|
|
79
|
+
super(), this.attachShadow({ mode: "open" });
|
|
80
|
+
}
|
|
81
|
+
connectedCallback() {
|
|
82
|
+
this.hidden = !0;
|
|
83
|
+
}
|
|
84
|
+
init({ url: e, data: t, keysSkipped: n, onOpen: r }) {
|
|
85
|
+
this.#e = r;
|
|
86
|
+
let a = this.shadowRoot, o = document.createElement("style");
|
|
87
|
+
o.textContent = "\n:host {\n position: fixed;\n left: 0;\n right: 0;\n bottom: 0;\n height: 40px;\n padding: 0 8px;\n background: #222;\n font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;\n font-weight: 200;\n color: #fff;\n z-index: 2147483647;\n box-shadow: 0 -1px 0 rgba(255, 255, 255, 0.1), inset 0 2px 6px rgba(0, 0, 0, 0.8);\n background-image: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.3));\n box-sizing: border-box;\n}\n\n:host([hidden]) {\n display: none;\n}\n\n.log-menu {\n position: fixed;\n left: 0;\n right: 0;\n bottom: 40px;\n max-height: calc(100vh - 40px);\n background: #222;\n color: #fff;\n overflow-y: auto;\n}\n\n.log-menu[hidden] {\n display: none;\n}\n\n.log-menu tbody td {\n padding: 2px 8px;\n}\n\n.log-menu tbody tr {\n cursor: pointer;\n}\n\n.log-menu tbody tr:hover {\n background: #444;\n}\n\n.log-menu tbody tr[hidden] {\n display: none;\n}\n\n.button {\n position: relative;\n display: inline-block;\n color: #fff;\n margin: 8px 1px;\n height: 24px;\n line-height: 24px;\n padding: 0 8px;\n font-size: 14px;\n cursor: pointer;\n vertical-align: middle;\n background-color: #444;\n background-image: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.2));\n border-radius: 2px;\n box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.2),\n inset 0 0 2px rgba(255, 255, 255, 0.2);\n text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.4);\n text-decoration: none;\n}\n\n.button:hover,\n.button:focus {\n color: #fff;\n text-decoration: none;\n background-color: #555;\n}\n\n.notice {\n display: inline-block;\n margin: 8px;\n font-size: 13px;\n line-height: 24px;\n vertical-align: middle;\n color: #ffd24d;\n}\n\n.search {\n appearance: none;\n border: none;\n border-radius: 2px;\n background-image: linear-gradient(rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0));\n box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.2), inset 0 0 2px rgba(0, 0, 0, 0.2);\n padding: 2px 8px;\n margin: 0;\n line-height: 20px;\n vertical-align: middle;\n color: black;\n width: auto;\n height: auto;\n font-size: 14px;\n}\n", a.append(o);
|
|
88
|
+
let s = this.makeButton("CopyTuner", e, "_blank"), c = this.makeButton("Sync", "/copytuner", "_blank"), l = this.makeButton("Translations in this page", "javascript:void(0)");
|
|
89
|
+
this.#t = document.createElement("input"), this.#t.type = "text", this.#t.classList.add("search"), this.#t.placeholder = "search", a.append(s, c, l, this.#t), this.#n = this.makeLogMenu(t), a.append(this.#n), n && this.appendSkippedNotice(), l.addEventListener("click", (e) => {
|
|
90
|
+
e.preventDefault(), this.toggleLogMenu();
|
|
91
|
+
}), this.#t.addEventListener("input", i(this.onSearch.bind(this), 250));
|
|
92
|
+
}
|
|
93
|
+
show() {
|
|
94
|
+
this.hidden = !1, this.#t.focus();
|
|
95
|
+
}
|
|
96
|
+
hide() {
|
|
97
|
+
this.hidden = !0;
|
|
98
|
+
}
|
|
99
|
+
makeButton(e, t, n) {
|
|
100
|
+
let r = document.createElement("a");
|
|
101
|
+
return r.classList.add("button"), r.textContent = e, r.href = t, n && (r.target = n), r;
|
|
102
|
+
}
|
|
103
|
+
appendSkippedNotice() {
|
|
104
|
+
let e = document.createElement("span");
|
|
105
|
+
e.classList.add("notice"), e.textContent = "⚠ This page is too large for the overlay. Use \"Translations in this page\" to edit.", this.shadowRoot.append(e);
|
|
106
|
+
}
|
|
107
|
+
showLogMenu() {
|
|
108
|
+
this.#n.hidden = !1;
|
|
109
|
+
}
|
|
110
|
+
toggleLogMenu() {
|
|
111
|
+
this.#n.hidden = !this.#n.hidden;
|
|
112
|
+
}
|
|
113
|
+
makeLogMenu(e) {
|
|
114
|
+
let t = document.createElement("div");
|
|
115
|
+
t.classList.add("log-menu"), t.hidden = !0;
|
|
116
|
+
let n = document.createElement("table"), r = document.createElement("tbody");
|
|
117
|
+
for (let t of Object.keys(e).sort()) {
|
|
118
|
+
let n = e[t];
|
|
119
|
+
if (n === "") continue;
|
|
120
|
+
let i = document.createElement("td");
|
|
121
|
+
i.textContent = t;
|
|
122
|
+
let a = document.createElement("td");
|
|
123
|
+
a.textContent = n;
|
|
124
|
+
let o = document.createElement("tr");
|
|
125
|
+
o.dataset.key = t, o.addEventListener("click", ({ currentTarget: e }) => {
|
|
126
|
+
let t = e;
|
|
127
|
+
t.dataset.key && this.#e(t.dataset.key);
|
|
128
|
+
}), o.append(i, a), r.append(o);
|
|
129
|
+
}
|
|
130
|
+
return n.append(r), t.append(n), t;
|
|
131
|
+
}
|
|
132
|
+
onSearch() {
|
|
133
|
+
let e = this.#t.value.trim();
|
|
134
|
+
this.showLogMenu();
|
|
135
|
+
let t = [...this.#n.querySelectorAll("tr")];
|
|
136
|
+
for (let n of t) n.hidden = !(e === "" || [...n.querySelectorAll("td")].some((t) => (t.textContent ?? "").includes(e)));
|
|
137
|
+
}
|
|
3
138
|
};
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return S(i);
|
|
28
|
-
a = setTimeout(m, M(i));
|
|
29
|
-
}
|
|
30
|
-
function S(i) {
|
|
31
|
-
return a = void 0, b && o ? g(i) : (o = s = void 0, r);
|
|
32
|
-
}
|
|
33
|
-
function I() {
|
|
34
|
-
a !== void 0 && clearTimeout(a), u = 0, o = c = s = a = void 0;
|
|
35
|
-
}
|
|
36
|
-
function A() {
|
|
37
|
-
return a === void 0 ? r : S(v());
|
|
38
|
-
}
|
|
39
|
-
function k() {
|
|
40
|
-
var i = v(), d = w(i);
|
|
41
|
-
if (o = arguments, s = this, c = i, d) {
|
|
42
|
-
if (a === void 0)
|
|
43
|
-
return j(c);
|
|
44
|
-
if (h)
|
|
45
|
-
return a = setTimeout(m, e), g(c);
|
|
46
|
-
}
|
|
47
|
-
return a === void 0 && (a = setTimeout(m, e)), r;
|
|
48
|
-
}
|
|
49
|
-
return k.cancel = I, k.flush = A, k;
|
|
50
|
-
}
|
|
51
|
-
function x(t) {
|
|
52
|
-
var e = typeof t;
|
|
53
|
-
return !!t && (e == "object" || e == "function");
|
|
54
|
-
}
|
|
55
|
-
function V(t) {
|
|
56
|
-
return !!t && typeof t == "object";
|
|
57
|
-
}
|
|
58
|
-
function Y(t) {
|
|
59
|
-
return typeof t == "symbol" || V(t) && P.call(t) == $;
|
|
60
|
-
}
|
|
61
|
-
function B(t) {
|
|
62
|
-
if (typeof t == "number")
|
|
63
|
-
return t;
|
|
64
|
-
if (Y(t))
|
|
65
|
-
return C;
|
|
66
|
-
if (x(t)) {
|
|
67
|
-
var e = typeof t.valueOf == "function" ? t.valueOf() : t;
|
|
68
|
-
t = x(e) ? e + "" : e;
|
|
69
|
-
}
|
|
70
|
-
if (typeof t != "string")
|
|
71
|
-
return t === 0 ? t : +t;
|
|
72
|
-
t = t.replace(N, "");
|
|
73
|
-
var n = q.test(t);
|
|
74
|
-
return n || D.test(t) ? R(t.slice(2), n ? 2 : 8) : H.test(t) ? C : +t;
|
|
75
|
-
}
|
|
76
|
-
var Z = G;
|
|
77
|
-
const p = "copy-tuner-hidden";
|
|
78
|
-
class J {
|
|
79
|
-
// @ts-expect-error TS7006
|
|
80
|
-
constructor(e, n, o, s = !1) {
|
|
81
|
-
this.element = e, this.data = n, this.callback = o, this.searchBoxElement = e.querySelector(".js-copy-tuner-bar-search"), this.logMenuElement = this.makeLogMenu(), this.element.append(this.logMenuElement), s && this.appendSkippedNotice(), this.addHandler();
|
|
82
|
-
}
|
|
83
|
-
appendSkippedNotice() {
|
|
84
|
-
const e = document.createElement("span");
|
|
85
|
-
e.classList.add("copy-tuner-bar__notice"), e.textContent = '⚠ This page is too large for the overlay. Use "Translations in this page" to edit.', this.element.append(e);
|
|
86
|
-
}
|
|
87
|
-
addHandler() {
|
|
88
|
-
this.element.querySelector(".js-copy-tuner-bar-open-log").addEventListener("click", (n) => {
|
|
89
|
-
n.preventDefault(), this.toggleLogMenu();
|
|
90
|
-
}), this.searchBoxElement.addEventListener("input", Z(this.onKeyup.bind(this), 250));
|
|
91
|
-
}
|
|
92
|
-
show() {
|
|
93
|
-
this.element.classList.remove(p), this.searchBoxElement.focus();
|
|
94
|
-
}
|
|
95
|
-
hide() {
|
|
96
|
-
this.element.classList.add(p);
|
|
97
|
-
}
|
|
98
|
-
showLogMenu() {
|
|
99
|
-
this.logMenuElement.classList.remove(p);
|
|
100
|
-
}
|
|
101
|
-
toggleLogMenu() {
|
|
102
|
-
this.logMenuElement.classList.toggle(p);
|
|
103
|
-
}
|
|
104
|
-
makeLogMenu() {
|
|
105
|
-
const e = document.createElement("div");
|
|
106
|
-
e.setAttribute("id", "copy-tuner-bar-log-menu"), e.classList.add(p);
|
|
107
|
-
const n = document.createElement("table"), o = document.createElement("tbody");
|
|
108
|
-
o.classList.remove("is-not-initialized");
|
|
109
|
-
for (const s of Object.keys(this.data).sort()) {
|
|
110
|
-
const l = this.data[s];
|
|
111
|
-
if (l === "")
|
|
112
|
-
continue;
|
|
113
|
-
const r = document.createElement("td");
|
|
114
|
-
r.textContent = s;
|
|
115
|
-
const a = document.createElement("td");
|
|
116
|
-
a.textContent = l;
|
|
117
|
-
const c = document.createElement("tr");
|
|
118
|
-
c.classList.add("copy-tuner-bar-log-menu__row"), c.dataset.key = s, c.addEventListener("click", ({ currentTarget: u }) => {
|
|
119
|
-
this.callback(u.dataset.key);
|
|
120
|
-
}), c.append(r), c.append(a), o.append(c);
|
|
121
|
-
}
|
|
122
|
-
return n.append(o), e.append(n), e;
|
|
123
|
-
}
|
|
124
|
-
// @ts-expect-error TS7031
|
|
125
|
-
onKeyup({ target: e }) {
|
|
126
|
-
const n = e.value.trim();
|
|
127
|
-
this.showLogMenu();
|
|
128
|
-
const o = [...this.logMenuElement.querySelectorAll("tr")];
|
|
129
|
-
for (const s of o) {
|
|
130
|
-
const l = n === "" || [...s.querySelectorAll("td")].some((r) => r.textContent.includes(n));
|
|
131
|
-
s.classList.toggle(p, !l);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
const L = navigator.platform.toUpperCase().includes("MAC"), Q = (t) => !!(t.offsetWidth || t.offsetHeight || t.getClientRects().length > 0), ee = (t) => {
|
|
136
|
-
const e = t.getBoundingClientRect();
|
|
137
|
-
return {
|
|
138
|
-
top: e.top + (window.pageYOffset - document.documentElement.clientTop),
|
|
139
|
-
left: e.left + (window.pageXOffset - document.documentElement.clientLeft)
|
|
140
|
-
};
|
|
141
|
-
}, te = (t) => {
|
|
142
|
-
if (!Q(t))
|
|
143
|
-
return null;
|
|
144
|
-
const e = ee(t);
|
|
145
|
-
return e.right = e.left + t.offsetWidth, e.bottom = e.top + t.offsetHeight, {
|
|
146
|
-
left: e.left,
|
|
147
|
-
top: e.top,
|
|
148
|
-
// @ts-expect-error TS2339
|
|
149
|
-
width: e.right - e.left,
|
|
150
|
-
// @ts-expect-error TS2339
|
|
151
|
-
height: e.bottom - e.top
|
|
152
|
-
};
|
|
153
|
-
}, ne = 2e9;
|
|
154
|
-
class oe {
|
|
155
|
-
// @ts-expect-error TS7006
|
|
156
|
-
constructor(e, n, o) {
|
|
157
|
-
this.element = e, this.keys = n, this.callback = o;
|
|
158
|
-
}
|
|
159
|
-
show() {
|
|
160
|
-
this.box = this.makeBox(), this.box !== null && (this.box.addEventListener("click", () => {
|
|
161
|
-
this.callback(this.keys[0]);
|
|
162
|
-
}), document.body.append(this.box));
|
|
163
|
-
}
|
|
164
|
-
remove() {
|
|
165
|
-
this.box && (this.box.remove(), this.box = null);
|
|
166
|
-
}
|
|
167
|
-
makeBox() {
|
|
168
|
-
const e = document.createElement("div");
|
|
169
|
-
e.classList.add("copyray-specimen"), e.classList.add("Specimen");
|
|
170
|
-
const n = te(this.element);
|
|
171
|
-
if (n === null)
|
|
172
|
-
return null;
|
|
173
|
-
for (const r of Object.keys(n)) {
|
|
174
|
-
const a = n[r];
|
|
175
|
-
e.style[r] = `${a}px`;
|
|
176
|
-
}
|
|
177
|
-
e.style.zIndex = ne;
|
|
178
|
-
const { position: o, top: s, left: l } = getComputedStyle(this.element);
|
|
179
|
-
o === "fixed" && (this.box.style.position = "fixed", this.box.style.top = `${s}px`, this.box.style.left = `${l}px`);
|
|
180
|
-
for (const r of this.keys)
|
|
181
|
-
e.append(this.makeLabel(r));
|
|
182
|
-
return e;
|
|
183
|
-
}
|
|
184
|
-
// @ts-expect-error TS7006
|
|
185
|
-
makeLabel(e) {
|
|
186
|
-
const n = document.createElement("div");
|
|
187
|
-
return n.classList.add("copyray-specimen-handle"), n.classList.add("Specimen"), n.textContent = e, n.addEventListener("click", (o) => {
|
|
188
|
-
o.stopPropagation(), this.callback(e);
|
|
189
|
-
}), n;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
const se = () => Array.from(document.querySelectorAll("[data-copyray-key]")).map((t) => ({
|
|
193
|
-
// 1 要素に複数キーがカンマ区切りで入りうる(同一テキストノードに複数訳文が連結された場合)
|
|
194
|
-
keys: (t.getAttribute("data-copyray-key") ?? "").split(",").filter(Boolean),
|
|
195
|
-
element: t
|
|
196
|
-
}));
|
|
197
|
-
class ie {
|
|
198
|
-
// @ts-expect-error TS7006
|
|
199
|
-
constructor(e, n, o = !1) {
|
|
200
|
-
this.baseUrl = e, this.data = n, this.isShowing = !1, this.specimens = [], this.overlay = this.makeOverlay(), this.toggleButton = this.makeToggleButton(), this.boundOpen = this.open.bind(this), this.copyTunerBar = new J(document.querySelector("#copy-tuner-bar"), this.data, this.boundOpen, o);
|
|
201
|
-
}
|
|
202
|
-
show() {
|
|
203
|
-
this.reset(), document.body.append(this.overlay), this.makeSpecimens();
|
|
204
|
-
for (const e of this.specimens)
|
|
205
|
-
e.show();
|
|
206
|
-
this.copyTunerBar.show(), this.isShowing = !0;
|
|
207
|
-
}
|
|
208
|
-
hide() {
|
|
209
|
-
this.overlay.remove(), this.reset(), this.copyTunerBar.hide(), this.isShowing = !1;
|
|
210
|
-
}
|
|
211
|
-
toggle() {
|
|
212
|
-
this.isShowing ? this.hide() : this.show();
|
|
213
|
-
}
|
|
214
|
-
// @ts-expect-error TS7006
|
|
215
|
-
open(e) {
|
|
216
|
-
window.open(`${this.baseUrl}/blurbs/${e}/edit`);
|
|
217
|
-
}
|
|
218
|
-
makeSpecimens() {
|
|
219
|
-
for (const { element: e, keys: n } of se())
|
|
220
|
-
this.specimens.push(new oe(e, n, this.boundOpen));
|
|
221
|
-
}
|
|
222
|
-
makeToggleButton() {
|
|
223
|
-
const e = document.createElement("a");
|
|
224
|
-
return e.addEventListener("click", () => {
|
|
225
|
-
this.show();
|
|
226
|
-
}), e.classList.add("copyray-toggle-button"), e.classList.add("hidden-on-mobile"), e.textContent = "Open CopyTuner", document.body.append(e), e;
|
|
227
|
-
}
|
|
228
|
-
makeOverlay() {
|
|
229
|
-
const e = document.createElement("div");
|
|
230
|
-
return e.setAttribute("id", "copyray-overlay"), e.addEventListener("click", () => this.hide()), e;
|
|
231
|
-
}
|
|
232
|
-
reset() {
|
|
233
|
-
for (const e of this.specimens)
|
|
234
|
-
e.remove();
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
const re = (t) => {
|
|
238
|
-
const e = document.createElement("div");
|
|
239
|
-
e.id = "copy-tuner-bar", e.classList.add("copy-tuner-hidden"), e.innerHTML = `
|
|
240
|
-
<a class="copy-tuner-bar-button" target="_blank" href="${t}">CopyTuner</a>
|
|
241
|
-
<a href="/copytuner" target="_blank" class="copy-tuner-bar-button">Sync</a>
|
|
242
|
-
<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>
|
|
243
|
-
<input type="text" class="copy-tuner-bar__search js-copy-tuner-bar-search" placeholder="search">
|
|
244
|
-
`, document.body.append(e);
|
|
245
|
-
}, O = () => {
|
|
246
|
-
const { url: t, data: e, keysSkipped: n } = window.CopyTuner;
|
|
247
|
-
re(t);
|
|
248
|
-
const o = new ie(t, e, !!n);
|
|
249
|
-
window.CopyTuner.toggle = () => o.toggle(), document.addEventListener("keydown", (s) => {
|
|
250
|
-
if (o.isShowing && ["Escape", "Esc"].includes(s.key)) {
|
|
251
|
-
o.hide();
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
(L && s.metaKey || !L && s.ctrlKey) && s.shiftKey && s.key.toLowerCase() === "k" && o.toggle();
|
|
255
|
-
}), console && console.log(`Ready to Copyray. Press ${L ? "cmd+shift+k" : "ctrl+shift+k"} to scan your UI.`);
|
|
139
|
+
customElements.define("copytuner-bar", s), customElements.define("copyray-overlay", o);
|
|
140
|
+
var c = () => {
|
|
141
|
+
let { url: t, data: n, keysSkipped: r } = window.CopyTuner, i = (e) => window.open(`${t}/blurbs/${e}/edit`), a = document.createElement("copytuner-bar");
|
|
142
|
+
document.body.append(a), a.init({
|
|
143
|
+
url: t,
|
|
144
|
+
data: n,
|
|
145
|
+
keysSkipped: !!r,
|
|
146
|
+
onOpen: i
|
|
147
|
+
});
|
|
148
|
+
let o = document.createElement("copyray-overlay");
|
|
149
|
+
o.onOpen = i, document.body.append(o);
|
|
150
|
+
let s = () => {
|
|
151
|
+
o.show(), a.show();
|
|
152
|
+
}, c = () => {
|
|
153
|
+
o.hide(), a.hide();
|
|
154
|
+
}, l = () => o.isShowing ? c() : s();
|
|
155
|
+
o.onToggle = l, window.CopyTuner.toggle = l, document.addEventListener("keydown", (t) => {
|
|
156
|
+
if (o.isShowing && ["Escape", "Esc"].includes(t.key)) {
|
|
157
|
+
c();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
(e && t.metaKey || !e && t.ctrlKey) && t.shiftKey && t.key.toLowerCase() === "k" && l();
|
|
161
|
+
}), console && console.log(`Ready to Copyray. Press ${e ? "cmd+shift+k" : "ctrl+shift+k"} to scan your UI.`);
|
|
256
162
|
};
|
|
257
|
-
document.readyState === "complete" || document.readyState !== "loading" ?
|
|
163
|
+
document.readyState === "complete" || document.readyState !== "loading" ? c() : document.addEventListener("DOMContentLoaded", () => c());
|
|
164
|
+
//#endregion
|
data/biome.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/2.5.0/schema.json",
|
|
3
|
+
"vcs": {
|
|
4
|
+
"enabled": true,
|
|
5
|
+
"clientKind": "git",
|
|
6
|
+
"useIgnoreFile": true
|
|
7
|
+
},
|
|
8
|
+
"files": {
|
|
9
|
+
"ignoreUnknown": false,
|
|
10
|
+
"includes": ["src/**", "*.ts", "*.json"]
|
|
11
|
+
},
|
|
12
|
+
"formatter": {
|
|
13
|
+
"enabled": true,
|
|
14
|
+
"indentStyle": "space",
|
|
15
|
+
"indentWidth": 2,
|
|
16
|
+
"lineWidth": 120
|
|
17
|
+
},
|
|
18
|
+
"linter": {
|
|
19
|
+
"enabled": true,
|
|
20
|
+
"rules": {
|
|
21
|
+
"preset": "recommended"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"javascript": {
|
|
25
|
+
"formatter": {
|
|
26
|
+
"quoteStyle": "single",
|
|
27
|
+
"semicolons": "asNeeded",
|
|
28
|
+
"trailingCommas": "all"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"assist": {
|
|
32
|
+
"enabled": true,
|
|
33
|
+
"actions": {
|
|
34
|
+
"source": {
|
|
35
|
+
"organizeImports": "on"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
data/index.html
CHANGED
|
@@ -5,27 +5,25 @@
|
|
|
5
5
|
<meta charset="UTF-8" />
|
|
6
6
|
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
|
|
7
7
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
8
|
-
<title>
|
|
8
|
+
<title>CopyTuner Dev</title>
|
|
9
9
|
</head>
|
|
10
10
|
|
|
11
11
|
<body>
|
|
12
12
|
<div id="app">
|
|
13
13
|
<ul>
|
|
14
|
-
<li>
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
<li>
|
|
18
|
-
<!--COPYRAY projects.index.blurbs-->翻訳キー
|
|
19
|
-
</li>
|
|
20
|
-
<li>
|
|
21
|
-
<!--COPYRAY projects.index.members-->メンバー数
|
|
22
|
-
</li>
|
|
14
|
+
<li data-copyray-key="projects.index.locales">言語</li>
|
|
15
|
+
<li data-copyray-key="projects.index.blurbs">翻訳キー</li>
|
|
16
|
+
<li data-copyray-key="projects.index.members">メンバー数</li>
|
|
23
17
|
</ul>
|
|
24
18
|
</div>
|
|
25
19
|
<script>
|
|
26
20
|
window.CopyTuner = {
|
|
27
21
|
url: 'https://copy-tuner.herokuapp.com/projects/xxx',
|
|
28
|
-
data: {
|
|
22
|
+
data: {
|
|
23
|
+
'projects.index.locales': '言語',
|
|
24
|
+
'projects.index.blurbs': '翻訳キー',
|
|
25
|
+
'projects.index.members': 'メンバー数',
|
|
26
|
+
},
|
|
29
27
|
}
|
|
30
28
|
</script>
|
|
31
29
|
<script type="module" src="/src/main.ts"></script>
|
|
@@ -18,7 +18,6 @@ module CopyTunerClient
|
|
|
18
18
|
# NOTE: skipped は data-copyray-key を付与できなかったこと(巨大DOM/Nokogiri例外)を表す。
|
|
19
19
|
# JS にこれを伝え、オーバーレイ非対応である旨をツールバーで案内させる。
|
|
20
20
|
body, skipped = CopyTunerClient::Copyray::Rewriter.rewrite(body)
|
|
21
|
-
body = append_css(body, csp_nonce)
|
|
22
21
|
body = append_js(body, csp_nonce, skipped: skipped)
|
|
23
22
|
content_length = body.bytesize.to_s
|
|
24
23
|
headers['Content-Length'] = content_length
|
|
@@ -39,11 +38,6 @@ module CopyTunerClient
|
|
|
39
38
|
ActionController::Base.helpers
|
|
40
39
|
end
|
|
41
40
|
|
|
42
|
-
def append_css(html, csp_nonce)
|
|
43
|
-
css_tag = helpers.stylesheet_link_tag 'copytuner', media: :all, nonce: csp_nonce
|
|
44
|
-
append_to_html_body(html, css_tag)
|
|
45
|
-
end
|
|
46
|
-
|
|
47
41
|
def append_js(html, csp_nonce, skipped: false)
|
|
48
42
|
json =
|
|
49
43
|
if CopyTunerClient::TranslationLog.initialized?
|
data/mise.toml
ADDED
data/package.json
CHANGED
|
@@ -4,26 +4,22 @@
|
|
|
4
4
|
"repository": "https://github.com/SonicGarden/copy-tuner-ruby-client",
|
|
5
5
|
"author": "SonicGarden",
|
|
6
6
|
"license": "MIT",
|
|
7
|
+
"packageManager": "pnpm@11.9.0",
|
|
7
8
|
"engines": {
|
|
8
|
-
"node": "
|
|
9
|
+
"node": ">=24"
|
|
9
10
|
},
|
|
10
11
|
"scripts": {
|
|
11
12
|
"dev": "vite",
|
|
12
13
|
"build": "tsc && vite build",
|
|
13
|
-
"preview": "vite preview"
|
|
14
|
+
"preview": "vite preview",
|
|
15
|
+
"lint": "biome lint .",
|
|
16
|
+
"format": "biome format --write .",
|
|
17
|
+
"check": "biome check --write ."
|
|
14
18
|
},
|
|
15
19
|
"dependencies": {},
|
|
16
20
|
"devDependencies": {
|
|
17
|
-
"@
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"lodash.debounce": "^4.0.8",
|
|
21
|
-
"typescript": "^5.0.2",
|
|
22
|
-
"vite": "^4.2.1"
|
|
23
|
-
},
|
|
24
|
-
"prettier": "@sonicgarden/prettier-config",
|
|
25
|
-
"volta": {
|
|
26
|
-
"node": "16.15.0",
|
|
27
|
-
"yarn": "1.22.18"
|
|
21
|
+
"@biomejs/biome": "2.5.0",
|
|
22
|
+
"typescript": "^6.0.0",
|
|
23
|
+
"vite": "^8.0.0"
|
|
28
24
|
}
|
|
29
25
|
}
|