copy_tuner_client 2.3.0 → 2.4.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/app/assets/javascripts/copytuner.js +69 -78
- data/index.html +54 -0
- data/lib/copy_tuner_client/version.rb +1 -1
- data/src/copyray-overlay.ts +54 -47
- data/src/copytuner-bar.ts +0 -11
- data/src/copytuner-root.ts +31 -0
- data/src/main.ts +22 -12
- data/src/styles.ts +62 -41
- data/src/util.ts +1 -36
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 803784553a4dbeb3916ccdffef8ebe40626644c18b1ac7fa0e42ff9158d9ae1a
|
|
4
|
+
data.tar.gz: aa985dc5ac60c2f137fe95a9fd5c3c15fd7e8667e7b4f183d49f1e89ba448aa7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 321d3cc14cfd86127ca2bbe204fa89cd76f152a9770991ed0d8939f6b7724500b1bd7c92dca63b615e4ae741b2652ef0176f60e4c6342afd0a1c718ae8503ddc
|
|
7
|
+
data.tar.gz: 3860c2738c49a2c7fd4af233ba3f062fda3fff1a6d789a851a1f2973029e4cbc201f6d61898ba05f63bf9f79fd5e19d2b8a40ddf387f58d0699e0475d2d08f8c
|
|
@@ -1,69 +1,50 @@
|
|
|
1
1
|
//#region src/styles.ts
|
|
2
|
-
var 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) => ({
|
|
2
|
+
var e = () => Array.from(document.querySelectorAll("[data-copyray-key]")).map((e) => ({
|
|
23
3
|
keys: (e.getAttribute("data-copyray-key") ?? "").split(",").filter(Boolean),
|
|
24
4
|
element: e
|
|
25
|
-
})),
|
|
5
|
+
})), t = class extends HTMLElement {
|
|
26
6
|
#e = () => {};
|
|
27
7
|
#t = () => {};
|
|
28
8
|
#n;
|
|
29
9
|
#r;
|
|
30
|
-
#i;
|
|
10
|
+
#i = /* @__PURE__ */ new Map();
|
|
11
|
+
#a = null;
|
|
31
12
|
constructor() {
|
|
32
13
|
super();
|
|
33
14
|
let e = this.attachShadow({ mode: "open" }), t = document.createElement("style");
|
|
34
|
-
t.textContent = "\n:host {\n position: absolute;\n
|
|
15
|
+
t.textContent = "\n:host {\n position: absolute;\n width: 0;\n height: 0;\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: fixed;\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[hidden] {\n display: none;\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", e.append(t), this.#n = document.createElement("div"), this.#n.classList.add("backdrop"), this.#n.addEventListener("click", () => this.#t()), this.#r = document.createElement("div"), e.append(this.#n, this.#r);
|
|
35
16
|
}
|
|
36
17
|
set onOpen(e) {
|
|
37
18
|
this.#e = e;
|
|
38
19
|
}
|
|
39
|
-
set
|
|
20
|
+
set onClose(e) {
|
|
40
21
|
this.#t = e;
|
|
41
22
|
}
|
|
42
|
-
get isShowing() {
|
|
43
|
-
return !this.#n.hidden;
|
|
44
|
-
}
|
|
45
23
|
show() {
|
|
46
|
-
this.
|
|
47
|
-
for (let { element:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
24
|
+
this.hide();
|
|
25
|
+
for (let { element: t, keys: n } of e()) this.#i.set(t, this.makeBox(n));
|
|
26
|
+
this.#s(), this.#r.append(...this.#i.values()), document.addEventListener("scroll", this.#o, {
|
|
27
|
+
capture: !0,
|
|
28
|
+
passive: !0
|
|
29
|
+
}), window.addEventListener("resize", this.#o, { passive: !0 });
|
|
51
30
|
}
|
|
52
31
|
hide() {
|
|
53
|
-
this.
|
|
32
|
+
document.removeEventListener("scroll", this.#o, { capture: !0 }), window.removeEventListener("resize", this.#o), this.#a !== null && (cancelAnimationFrame(this.#a), this.#a = null), this.#i.clear(), this.#r.replaceChildren();
|
|
54
33
|
}
|
|
55
|
-
|
|
56
|
-
this.#
|
|
34
|
+
#o = () => {
|
|
35
|
+
this.#a === null && (this.#a = requestAnimationFrame(() => {
|
|
36
|
+
this.#a = null, this.#s();
|
|
37
|
+
}));
|
|
38
|
+
};
|
|
39
|
+
#s() {
|
|
40
|
+
let e = Array.from(this.#i, ([e, t]) => [t, e.getBoundingClientRect()]);
|
|
41
|
+
for (let [t, n] of e) t.hidden = n.width === 0 && n.height === 0, !t.hidden && (t.style.left = `${n.left}px`, t.style.top = `${n.top}px`, t.style.width = `${n.width}px`, t.style.height = `${n.height}px`);
|
|
57
42
|
}
|
|
58
|
-
makeBox(e
|
|
59
|
-
let
|
|
60
|
-
|
|
61
|
-
let
|
|
62
|
-
|
|
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;
|
|
43
|
+
makeBox(e) {
|
|
44
|
+
let t = document.createElement("div");
|
|
45
|
+
t.classList.add("specimen"), t.addEventListener("click", () => this.#e(e[0]));
|
|
46
|
+
for (let n of e) t.append(this.makeLabel(n));
|
|
47
|
+
return t;
|
|
67
48
|
}
|
|
68
49
|
makeLabel(e) {
|
|
69
50
|
let t = document.createElement("div");
|
|
@@ -71,30 +52,29 @@ var e = navigator.platform.toUpperCase().includes("MAC"), t = (e) => !!(e.offset
|
|
|
71
52
|
t.stopPropagation(), this.#e(e);
|
|
72
53
|
}), t;
|
|
73
54
|
}
|
|
74
|
-
},
|
|
55
|
+
}, n = navigator.platform.toUpperCase().includes("MAC"), r = (e, t) => {
|
|
56
|
+
let n;
|
|
57
|
+
return (...r) => {
|
|
58
|
+
clearTimeout(n), n = setTimeout(() => e(...r), t);
|
|
59
|
+
};
|
|
60
|
+
}, i = class extends HTMLElement {
|
|
75
61
|
#e = () => {};
|
|
76
62
|
#t;
|
|
77
63
|
#n;
|
|
78
64
|
constructor() {
|
|
79
65
|
super(), this.attachShadow({ mode: "open" });
|
|
80
66
|
}
|
|
81
|
-
|
|
82
|
-
this
|
|
83
|
-
}
|
|
84
|
-
init({ url: e, data: t, keysSkipped: n, onOpen: r }) {
|
|
85
|
-
this.#e = r;
|
|
67
|
+
init({ url: e, data: t, keysSkipped: n, onOpen: i }) {
|
|
68
|
+
this.#e = i;
|
|
86
69
|
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
|
|
70
|
+
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.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
71
|
let s = this.makeButton("CopyTuner", e, "_blank"), c = this.makeButton("Sync", "/copytuner", "_blank"), l = this.makeButton("Translations in this page", "javascript:void(0)");
|
|
89
72
|
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
73
|
e.preventDefault(), this.toggleLogMenu();
|
|
91
|
-
}), this.#t.addEventListener("input",
|
|
74
|
+
}), this.#t.addEventListener("input", r(this.onSearch.bind(this), 250));
|
|
92
75
|
}
|
|
93
76
|
show() {
|
|
94
|
-
this
|
|
95
|
-
}
|
|
96
|
-
hide() {
|
|
97
|
-
this.hidden = !0;
|
|
77
|
+
this.#t.focus();
|
|
98
78
|
}
|
|
99
79
|
makeButton(e, t, n) {
|
|
100
80
|
let r = document.createElement("a");
|
|
@@ -135,30 +115,41 @@ var e = navigator.platform.toUpperCase().includes("MAC"), t = (e) => !!(e.offset
|
|
|
135
115
|
let t = [...this.#n.querySelectorAll("tr")];
|
|
136
116
|
for (let n of t) n.hidden = !(e === "" || [...n.querySelectorAll("td")].some((t) => (t.textContent ?? "").includes(e)));
|
|
137
117
|
}
|
|
118
|
+
}, a = class extends HTMLElement {
|
|
119
|
+
dialog;
|
|
120
|
+
#e = () => {};
|
|
121
|
+
constructor() {
|
|
122
|
+
super();
|
|
123
|
+
let e = this.attachShadow({ mode: "open" }), t = document.createElement("style");
|
|
124
|
+
t.textContent = "\n/* transform / filter / perspective / contain は置かない(理由は OVERLAY_STYLES のコメント参照) */\ndialog {\n position: fixed;\n inset: 0;\n width: 100%;\n height: 100%;\n max-width: none;\n max-height: none;\n margin: 0;\n border: none;\n padding: 0;\n background: transparent;\n color: inherit;\n}\n\ndialog::backdrop {\n background: transparent;\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.dialog = document.createElement("dialog");
|
|
125
|
+
let n = document.createElement("a");
|
|
126
|
+
n.classList.add("toggle-button"), n.textContent = "Open CopyTuner", n.addEventListener("click", () => this.#e()), e.append(this.dialog, n);
|
|
127
|
+
}
|
|
128
|
+
set onToggle(e) {
|
|
129
|
+
this.#e = e;
|
|
130
|
+
}
|
|
138
131
|
};
|
|
139
|
-
customElements.define("copytuner-bar",
|
|
140
|
-
var
|
|
141
|
-
let { url:
|
|
142
|
-
document.body.append(a)
|
|
143
|
-
|
|
144
|
-
|
|
132
|
+
customElements.define("copytuner-bar", i), customElements.define("copyray-overlay", t), customElements.define("copytuner-root", a);
|
|
133
|
+
var o = () => {
|
|
134
|
+
let { url: e, data: t, keysSkipped: r } = window.CopyTuner, i = (t) => window.open(`${e}/blurbs/${t}/edit`), a = document.createElement("copytuner-root");
|
|
135
|
+
document.body.append(a);
|
|
136
|
+
let o = document.createElement("copytuner-bar"), s = document.createElement("copyray-overlay");
|
|
137
|
+
s.onOpen = i, a.dialog.append(s, o), o.init({
|
|
138
|
+
url: e,
|
|
139
|
+
data: t,
|
|
145
140
|
keysSkipped: !!r,
|
|
146
141
|
onOpen: i
|
|
147
142
|
});
|
|
148
|
-
let
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
},
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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.`);
|
|
143
|
+
let c = () => {
|
|
144
|
+
a.dialog.showModal(), s.show(), o.show();
|
|
145
|
+
}, l = () => {
|
|
146
|
+
a.dialog.close(), s.hide();
|
|
147
|
+
}, u = () => a.dialog.open ? l() : c();
|
|
148
|
+
a.onToggle = u, s.onClose = l, window.CopyTuner.toggle = u, a.dialog.addEventListener("close", () => {
|
|
149
|
+
a.dialog.open || s.hide();
|
|
150
|
+
}), document.addEventListener("keydown", (e) => {
|
|
151
|
+
(n && e.metaKey || !n && e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === "k" && u();
|
|
152
|
+
}), console && console.log(`Ready to Copyray. Press ${n ? "cmd+shift+k" : "ctrl+shift+k"} to scan your UI.`);
|
|
162
153
|
};
|
|
163
|
-
document.readyState === "complete" || document.readyState !== "loading" ?
|
|
154
|
+
document.readyState === "complete" || document.readyState !== "loading" ? o() : document.addEventListener("DOMContentLoaded", () => o());
|
|
164
155
|
//#endregion
|
data/index.html
CHANGED
|
@@ -6,6 +6,35 @@
|
|
|
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
8
|
<title>CopyTuner Dev</title>
|
|
9
|
+
<style>
|
|
10
|
+
.fixed-banner {
|
|
11
|
+
position: fixed;
|
|
12
|
+
top: 0;
|
|
13
|
+
right: 0;
|
|
14
|
+
padding: 8px 12px;
|
|
15
|
+
background: #ffe9a8;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/* width は横スクロール時の left 経路の検証用 */
|
|
19
|
+
.spacer {
|
|
20
|
+
height: 150vh;
|
|
21
|
+
width: 150vw;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* transform を持つ祖先は fixed 要素の containing block になり、viewport 基準からずれるケースの再現用 */
|
|
25
|
+
.transform-ancestor {
|
|
26
|
+
transform: translateZ(0);
|
|
27
|
+
height: 80px;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.transformed-fixed {
|
|
31
|
+
position: fixed;
|
|
32
|
+
bottom: 0;
|
|
33
|
+
left: 0;
|
|
34
|
+
padding: 8px 12px;
|
|
35
|
+
background: #a8d8ff;
|
|
36
|
+
}
|
|
37
|
+
</style>
|
|
9
38
|
</head>
|
|
10
39
|
|
|
11
40
|
<body>
|
|
@@ -15,6 +44,23 @@
|
|
|
15
44
|
<li data-copyray-key="projects.index.blurbs">翻訳キー</li>
|
|
16
45
|
<li data-copyray-key="projects.index.members">メンバー数</li>
|
|
17
46
|
</ul>
|
|
47
|
+
|
|
48
|
+
<p class="fixed-banner" data-copyray-key="projects.index.fixed_banner">固定表示のバナー</p>
|
|
49
|
+
|
|
50
|
+
<div class="transform-ancestor">
|
|
51
|
+
<p class="transformed-fixed" data-copyray-key="projects.index.transformed_fixed">transform 祖先を持つ固定要素</p>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<button type="button" id="open-app-modal">アプリのモーダルを開く</button>
|
|
55
|
+
|
|
56
|
+
<dialog id="app-modal">
|
|
57
|
+
<p data-copyray-key="projects.index.modal_body">モーダル内の文言</p>
|
|
58
|
+
<button type="button" id="close-app-modal">閉じる</button>
|
|
59
|
+
</dialog>
|
|
60
|
+
|
|
61
|
+
<div class="spacer"></div>
|
|
62
|
+
|
|
63
|
+
<p data-copyray-key="projects.index.below_spacer">スクロール後に見える文言</p>
|
|
18
64
|
</div>
|
|
19
65
|
<script>
|
|
20
66
|
window.CopyTuner = {
|
|
@@ -23,8 +69,16 @@
|
|
|
23
69
|
'projects.index.locales': '言語',
|
|
24
70
|
'projects.index.blurbs': '翻訳キー',
|
|
25
71
|
'projects.index.members': 'メンバー数',
|
|
72
|
+
'projects.index.fixed_banner': '固定表示のバナー',
|
|
73
|
+
'projects.index.transformed_fixed': 'transform 祖先を持つ固定要素',
|
|
74
|
+
'projects.index.modal_body': 'モーダル内の文言',
|
|
75
|
+
'projects.index.below_spacer': 'スクロール後に見える文言',
|
|
26
76
|
},
|
|
27
77
|
}
|
|
78
|
+
|
|
79
|
+
const appModal = document.getElementById('app-modal')
|
|
80
|
+
document.getElementById('open-app-modal').addEventListener('click', () => appModal.showModal())
|
|
81
|
+
document.getElementById('close-app-modal').addEventListener('click', () => appModal.close())
|
|
28
82
|
</script>
|
|
29
83
|
<script type="module" src="/src/main.ts"></script>
|
|
30
84
|
</body>
|
data/src/copyray-overlay.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { OVERLAY_STYLES } from './styles'
|
|
2
|
-
import { computeBoundingBox } from './util'
|
|
3
2
|
|
|
4
3
|
type OpenCallback = (key: string) => void
|
|
5
4
|
|
|
@@ -15,13 +14,14 @@ const findBlurbs = (): Blurb[] =>
|
|
|
15
14
|
element,
|
|
16
15
|
}))
|
|
17
16
|
|
|
18
|
-
//
|
|
17
|
+
// オーバーレイ背景と翻訳要素のハイライト枠(specimen)を Shadow DOM 内に描画する。
|
|
19
18
|
export class CopyrayOverlay extends HTMLElement {
|
|
20
19
|
#onOpen: OpenCallback = () => {}
|
|
21
|
-
#
|
|
20
|
+
#onClose: () => void = () => {}
|
|
22
21
|
#backdrop: HTMLDivElement
|
|
23
22
|
#specimens: HTMLDivElement
|
|
24
|
-
#
|
|
23
|
+
#boxes = new Map<Element, HTMLDivElement>()
|
|
24
|
+
#frame: number | null = null
|
|
25
25
|
|
|
26
26
|
constructor() {
|
|
27
27
|
super()
|
|
@@ -33,74 +33,81 @@ export class CopyrayOverlay extends HTMLElement {
|
|
|
33
33
|
|
|
34
34
|
this.#backdrop = document.createElement('div')
|
|
35
35
|
this.#backdrop.classList.add('backdrop')
|
|
36
|
-
this.#backdrop.addEventListener('click', () => this
|
|
36
|
+
this.#backdrop.addEventListener('click', () => this.#onClose())
|
|
37
37
|
|
|
38
|
-
// specimen をページ座標基準で absolute 配置するコンテナ
|
|
39
38
|
this.#specimens = document.createElement('div')
|
|
40
|
-
this.#specimens.classList.add('specimens')
|
|
41
39
|
|
|
42
|
-
this.#
|
|
43
|
-
this.#toggleButton.classList.add('toggle-button')
|
|
44
|
-
this.#toggleButton.textContent = 'Open CopyTuner'
|
|
45
|
-
// 旧実装ではトグルボタンが overlay と bar の両方を表示していた。show() ではなく onToggle 経由で表示する。
|
|
46
|
-
this.#toggleButton.addEventListener('click', () => this.#onToggle())
|
|
47
|
-
|
|
48
|
-
shadow.append(this.#backdrop, this.#specimens, this.#toggleButton)
|
|
49
|
-
|
|
50
|
-
// 初期は非表示(背景と specimen を隠す)。トグルボタンは常時表示のため :host([hidden]) は使わず個別制御する。
|
|
51
|
-
this.hide()
|
|
40
|
+
shadow.append(this.#backdrop, this.#specimens)
|
|
52
41
|
}
|
|
53
42
|
|
|
54
43
|
set onOpen(callback: OpenCallback) {
|
|
55
44
|
this.#onOpen = callback
|
|
56
45
|
}
|
|
57
46
|
|
|
58
|
-
set
|
|
59
|
-
this.#
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
get isShowing(): boolean {
|
|
63
|
-
return !this.#backdrop.hidden
|
|
47
|
+
set onClose(callback: () => void) {
|
|
48
|
+
this.#onClose = callback
|
|
64
49
|
}
|
|
65
50
|
|
|
66
51
|
show() {
|
|
67
|
-
|
|
68
|
-
this
|
|
52
|
+
// 二重 show でもリスナーと boxes が重複しないよう、まず後始末を通す
|
|
53
|
+
this.hide()
|
|
69
54
|
|
|
70
55
|
for (const { element, keys } of findBlurbs()) {
|
|
71
|
-
|
|
72
|
-
if (box) {
|
|
73
|
-
this.#specimens.append(box)
|
|
74
|
-
}
|
|
56
|
+
this.#boxes.set(element, this.makeBox(keys))
|
|
75
57
|
}
|
|
58
|
+
// 初回は同期で配置する(rAF 経由だと開いた直後の 1 フレームだけ位置がずれる)
|
|
59
|
+
this.#reposition()
|
|
60
|
+
this.#specimens.append(...this.#boxes.values())
|
|
61
|
+
|
|
62
|
+
document.addEventListener('scroll', this.#requestReposition, { capture: true, passive: true })
|
|
63
|
+
window.addEventListener('resize', this.#requestReposition, { passive: true })
|
|
76
64
|
}
|
|
77
65
|
|
|
78
66
|
hide() {
|
|
79
|
-
|
|
80
|
-
this.#
|
|
81
|
-
|
|
67
|
+
// capture フラグはリスナーの同一性判定に含まれるため、省くと解除されない
|
|
68
|
+
document.removeEventListener('scroll', this.#requestReposition, { capture: true })
|
|
69
|
+
window.removeEventListener('resize', this.#requestReposition)
|
|
70
|
+
|
|
71
|
+
// 閉じた後に走っても boxes は空で no-op になるが、無駄なコールバックを残さない
|
|
72
|
+
if (this.#frame !== null) {
|
|
73
|
+
cancelAnimationFrame(this.#frame)
|
|
74
|
+
this.#frame = null
|
|
75
|
+
}
|
|
82
76
|
|
|
83
|
-
|
|
77
|
+
// detached な box を握り続けると開くたびに肥大する
|
|
78
|
+
this.#boxes.clear()
|
|
84
79
|
this.#specimens.replaceChildren()
|
|
85
80
|
}
|
|
86
81
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (
|
|
82
|
+
// scroll は慣性スクロール中に毎フレーム相当で発火するため、同一フレーム内の再計測は 1 回にまとめる
|
|
83
|
+
#requestReposition = () => {
|
|
84
|
+
if (this.#frame !== null) return
|
|
90
85
|
|
|
86
|
+
this.#frame = requestAnimationFrame(() => {
|
|
87
|
+
this.#frame = null
|
|
88
|
+
this.#reposition()
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
#reposition() {
|
|
93
|
+
// 読みと書きを分けないと、要素数分の強制同期レイアウトが走る
|
|
94
|
+
const measured = Array.from(this.#boxes, ([element, box]) => [box, element.getBoundingClientRect()] as const)
|
|
95
|
+
|
|
96
|
+
for (const [box, rect] of measured) {
|
|
97
|
+
// display: none / DOM から外れた要素は rect が全て 0 になる。原点に枠だけ残さないよう隠す
|
|
98
|
+
box.hidden = rect.width === 0 && rect.height === 0
|
|
99
|
+
if (box.hidden) continue
|
|
100
|
+
|
|
101
|
+
box.style.left = `${rect.left}px`
|
|
102
|
+
box.style.top = `${rect.top}px`
|
|
103
|
+
box.style.width = `${rect.width}px`
|
|
104
|
+
box.style.height = `${rect.height}px`
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private makeBox(keys: string[]): HTMLDivElement {
|
|
91
109
|
const box = document.createElement('div')
|
|
92
110
|
box.classList.add('specimen')
|
|
93
|
-
box.style.left = `${bounds.left}px`
|
|
94
|
-
box.style.top = `${bounds.top}px`
|
|
95
|
-
box.style.width = `${bounds.width}px`
|
|
96
|
-
box.style.height = `${bounds.height}px`
|
|
97
|
-
|
|
98
|
-
const { position, top, left } = getComputedStyle(element)
|
|
99
|
-
if (position === 'fixed') {
|
|
100
|
-
box.style.position = 'fixed'
|
|
101
|
-
box.style.top = top
|
|
102
|
-
box.style.left = left
|
|
103
|
-
}
|
|
104
111
|
|
|
105
112
|
// box 全体のクリックは先頭キーを開く(広いクリック領域を維持)。複数キー時は各ラベルから個別に開ける
|
|
106
113
|
box.addEventListener('click', () => this.#onOpen(keys[0]))
|
data/src/copytuner-bar.ts
CHANGED
|
@@ -21,12 +21,6 @@ export class CopytunerBar extends HTMLElement {
|
|
|
21
21
|
this.attachShadow({ mode: 'open' })
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
// custom element の constructor 内では属性・プロパティを変更できない(createElement が弾く)ため、
|
|
25
|
-
// hidden の初期化は DOM 挿入後に呼ばれる connectedCallback で行う。
|
|
26
|
-
connectedCallback() {
|
|
27
|
-
this.hidden = true
|
|
28
|
-
}
|
|
29
|
-
|
|
30
24
|
// url/data/keysSkipped/onOpen はオブジェクトや関数を含むため属性ではなくメソッドで受け渡す。
|
|
31
25
|
init({ url, data, keysSkipped, onOpen }: InitOptions) {
|
|
32
26
|
this.#onOpen = onOpen
|
|
@@ -66,14 +60,9 @@ export class CopytunerBar extends HTMLElement {
|
|
|
66
60
|
}
|
|
67
61
|
|
|
68
62
|
show() {
|
|
69
|
-
this.hidden = false
|
|
70
63
|
this.#searchBox.focus()
|
|
71
64
|
}
|
|
72
65
|
|
|
73
|
-
hide() {
|
|
74
|
-
this.hidden = true
|
|
75
|
-
}
|
|
76
|
-
|
|
77
66
|
private makeButton(label: string, href: string, target?: string): HTMLAnchorElement {
|
|
78
67
|
const button = document.createElement('a')
|
|
79
68
|
button.classList.add('button')
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { ROOT_STYLES } from './styles'
|
|
2
|
+
|
|
3
|
+
// dialog を showModal() で top layer の最上位に載せることで、アプリ側のモーダル dialog による
|
|
4
|
+
// inert 化を受けずに操作できる。トグルボタンだけ dialog の外に置くのは、
|
|
5
|
+
// 閉じている間 dialog のサブツリーが display: none になり、常時表示のボタンが消えてしまうため。
|
|
6
|
+
export class CopytunerRoot extends HTMLElement {
|
|
7
|
+
readonly dialog: HTMLDialogElement
|
|
8
|
+
#onToggle: () => void = () => {}
|
|
9
|
+
|
|
10
|
+
constructor() {
|
|
11
|
+
super()
|
|
12
|
+
const shadow = this.attachShadow({ mode: 'open' })
|
|
13
|
+
|
|
14
|
+
const style = document.createElement('style')
|
|
15
|
+
style.textContent = ROOT_STYLES
|
|
16
|
+
shadow.append(style)
|
|
17
|
+
|
|
18
|
+
this.dialog = document.createElement('dialog')
|
|
19
|
+
|
|
20
|
+
const toggleButton = document.createElement('a')
|
|
21
|
+
toggleButton.classList.add('toggle-button')
|
|
22
|
+
toggleButton.textContent = 'Open CopyTuner'
|
|
23
|
+
toggleButton.addEventListener('click', () => this.#onToggle())
|
|
24
|
+
|
|
25
|
+
shadow.append(this.dialog, toggleButton)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
set onToggle(callback: () => void) {
|
|
29
|
+
this.#onToggle = callback
|
|
30
|
+
}
|
|
31
|
+
}
|
data/src/main.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* eslint-disable no-console */
|
|
2
2
|
import { CopyrayOverlay } from './copyray-overlay'
|
|
3
3
|
import { CopytunerBar } from './copytuner-bar'
|
|
4
|
+
import { CopytunerRoot } from './copytuner-root'
|
|
4
5
|
import { isMac } from './util'
|
|
5
6
|
|
|
6
7
|
declare global {
|
|
@@ -18,38 +19,47 @@ declare global {
|
|
|
18
19
|
|
|
19
20
|
customElements.define('copytuner-bar', CopytunerBar)
|
|
20
21
|
customElements.define('copyray-overlay', CopyrayOverlay)
|
|
22
|
+
customElements.define('copytuner-root', CopytunerRoot)
|
|
21
23
|
|
|
22
24
|
const start = () => {
|
|
23
25
|
const { url, data, keysSkipped } = window.CopyTuner
|
|
24
26
|
const onOpen = (key: string) => window.open(`${url}/blurbs/${key}/edit`)
|
|
25
27
|
|
|
26
|
-
const
|
|
27
|
-
document.body.append(
|
|
28
|
-
bar.init({ url, data, keysSkipped: Boolean(keysSkipped), onOpen })
|
|
28
|
+
const root = document.createElement('copytuner-root') as CopytunerRoot
|
|
29
|
+
document.body.append(root)
|
|
29
30
|
|
|
31
|
+
const bar = document.createElement('copytuner-bar') as CopytunerBar
|
|
30
32
|
const overlay = document.createElement('copyray-overlay') as CopyrayOverlay
|
|
31
33
|
overlay.onOpen = onOpen
|
|
32
|
-
|
|
34
|
+
root.dialog.append(overlay, bar)
|
|
35
|
+
bar.init({ url, data, keysSkipped: Boolean(keysSkipped), onOpen })
|
|
33
36
|
|
|
34
37
|
const show = () => {
|
|
38
|
+
// display: none のままでは searchBox にフォーカスが当たらないので dialog を先に開く
|
|
39
|
+
root.dialog.showModal()
|
|
35
40
|
overlay.show()
|
|
36
41
|
bar.show()
|
|
37
42
|
}
|
|
38
43
|
const hide = () => {
|
|
44
|
+
root.dialog.close()
|
|
45
|
+
// close イベントを待たず同期で後始末する。イベントは queued task 発火で、
|
|
46
|
+
// 自動テスト環境など発火が遅れる/落ちる状況があるため、ここを唯一の頼りにしない
|
|
39
47
|
overlay.hide()
|
|
40
|
-
bar.hide()
|
|
41
48
|
}
|
|
42
|
-
const toggle = () => (
|
|
49
|
+
const toggle = () => (root.dialog.open ? hide() : show())
|
|
43
50
|
|
|
44
|
-
|
|
51
|
+
root.onToggle = toggle
|
|
52
|
+
overlay.onClose = hide
|
|
45
53
|
window.CopyTuner.toggle = toggle
|
|
46
54
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
55
|
+
// Escape のようにこちらの hide() を経由しない閉じ方の受け皿。overlay.hide() は冪等なので二重でも問題ない
|
|
56
|
+
root.dialog.addEventListener('close', () => {
|
|
57
|
+
// 閉じた直後に開き直された場合、遅れて届いたこのイベントで後始末してはいけない
|
|
58
|
+
if (root.dialog.open) return
|
|
59
|
+
overlay.hide()
|
|
60
|
+
})
|
|
52
61
|
|
|
62
|
+
document.addEventListener('keydown', (event) => {
|
|
53
63
|
if (((isMac && event.metaKey) || (!isMac && event.ctrlKey)) && event.shiftKey && event.key.toLowerCase() === 'k') {
|
|
54
64
|
toggle()
|
|
55
65
|
}
|
data/src/styles.ts
CHANGED
|
@@ -1,6 +1,57 @@
|
|
|
1
1
|
// 各 custom element の Shadow root に <style> として注入する CSS。
|
|
2
2
|
// Shadow DOM のスタイル隔離が効くため、旧 copyray.css にあった #copyray-overlay * のグローバルリセットは不要。
|
|
3
3
|
|
|
4
|
+
// コンテナ(<copytuner-root>)のスタイル。
|
|
5
|
+
// dialog の UA デフォルト(中央寄せ・白背景・枠線・padding・暗転)を全て打ち消し、
|
|
6
|
+
// viewport 全面を覆う透明なコンテナにする。暗転は overlay 側の .backdrop が担う。
|
|
7
|
+
export const ROOT_STYLES = `
|
|
8
|
+
/* transform / filter / perspective / contain は置かない(理由は OVERLAY_STYLES のコメント参照) */
|
|
9
|
+
dialog {
|
|
10
|
+
position: fixed;
|
|
11
|
+
inset: 0;
|
|
12
|
+
width: 100%;
|
|
13
|
+
height: 100%;
|
|
14
|
+
max-width: none;
|
|
15
|
+
max-height: none;
|
|
16
|
+
margin: 0;
|
|
17
|
+
border: none;
|
|
18
|
+
padding: 0;
|
|
19
|
+
background: transparent;
|
|
20
|
+
color: inherit;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
dialog::backdrop {
|
|
24
|
+
background: transparent;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.toggle-button {
|
|
28
|
+
display: block;
|
|
29
|
+
position: fixed;
|
|
30
|
+
left: 0;
|
|
31
|
+
bottom: 0;
|
|
32
|
+
color: white;
|
|
33
|
+
background: black;
|
|
34
|
+
padding: 12px 16px;
|
|
35
|
+
border-radius: 0 10px 0 0;
|
|
36
|
+
opacity: 0;
|
|
37
|
+
transition: opacity 0.6s ease-in-out;
|
|
38
|
+
z-index: 10000;
|
|
39
|
+
font-size: 12px;
|
|
40
|
+
cursor: pointer;
|
|
41
|
+
text-decoration: none;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.toggle-button:hover {
|
|
45
|
+
opacity: 1;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@media screen and (max-width: 480px) {
|
|
49
|
+
.toggle-button {
|
|
50
|
+
display: none;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
`
|
|
54
|
+
|
|
4
55
|
// ツールバー(<copytuner-bar>)のスタイル。:host にバー本体のレイアウトを定義する。
|
|
5
56
|
export const BAR_STYLES = `
|
|
6
57
|
:host {
|
|
@@ -20,10 +71,6 @@ export const BAR_STYLES = `
|
|
|
20
71
|
box-sizing: border-box;
|
|
21
72
|
}
|
|
22
73
|
|
|
23
|
-
:host([hidden]) {
|
|
24
|
-
display: none;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
74
|
.log-menu {
|
|
28
75
|
position: fixed;
|
|
29
76
|
left: 0;
|
|
@@ -109,22 +156,19 @@ export const BAR_STYLES = `
|
|
|
109
156
|
`
|
|
110
157
|
|
|
111
158
|
// オーバーレイ(<copyray-overlay>)のスタイル。
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
159
|
+
// specimen は getBoundingClientRect() の viewport 座標をそのまま使うため、
|
|
160
|
+
// dialog / :host / specimen のコンテナ div のいずれにも
|
|
161
|
+
// transform / filter / perspective / contain を置かない。
|
|
162
|
+
// 置くとそれが containing block になり、fixed な specimen が viewport 基準から外れる。
|
|
163
|
+
// :host の position: absolute と width/height: 0 は、インラインの custom element が
|
|
164
|
+
// dialog のフローに line box を作らないようにするため(fixed の containing block は作らない)。
|
|
115
165
|
export const OVERLAY_STYLES = `
|
|
116
166
|
:host {
|
|
117
167
|
position: absolute;
|
|
118
|
-
top: 0;
|
|
119
|
-
left: 0;
|
|
120
168
|
width: 0;
|
|
121
169
|
height: 0;
|
|
122
170
|
}
|
|
123
171
|
|
|
124
|
-
:host([hidden]) {
|
|
125
|
-
display: none;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
172
|
.backdrop {
|
|
129
173
|
position: fixed;
|
|
130
174
|
inset: 0;
|
|
@@ -137,7 +181,7 @@ export const OVERLAY_STYLES = `
|
|
|
137
181
|
}
|
|
138
182
|
|
|
139
183
|
.specimen {
|
|
140
|
-
position:
|
|
184
|
+
position: fixed;
|
|
141
185
|
background: rgba(255, 50, 50, 0.1);
|
|
142
186
|
outline: 1px solid rgba(255, 50, 50, 0.8);
|
|
143
187
|
outline-offset: -1px;
|
|
@@ -153,6 +197,10 @@ export const OVERLAY_STYLES = `
|
|
|
153
197
|
background: rgba(255, 50, 50, 0.4);
|
|
154
198
|
}
|
|
155
199
|
|
|
200
|
+
.specimen[hidden] {
|
|
201
|
+
display: none;
|
|
202
|
+
}
|
|
203
|
+
|
|
156
204
|
.specimen-handle {
|
|
157
205
|
float: left;
|
|
158
206
|
margin: 0 2px 2px 0;
|
|
@@ -162,31 +210,4 @@ export const OVERLAY_STYLES = `
|
|
|
162
210
|
font-size: 10px;
|
|
163
211
|
cursor: pointer;
|
|
164
212
|
}
|
|
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;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
@media screen and (max-width: 480px) {
|
|
188
|
-
.toggle-button {
|
|
189
|
-
display: none;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
213
|
`
|
data/src/util.ts
CHANGED
|
@@ -1,40 +1,5 @@
|
|
|
1
1
|
const isMac = navigator.platform.toUpperCase().includes('MAC')
|
|
2
2
|
|
|
3
|
-
// @ts-expect-error TS7006
|
|
4
|
-
const isVisible = (element) => !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length > 0)
|
|
5
|
-
|
|
6
|
-
// @ts-expect-error TS7006
|
|
7
|
-
const getOffset = (elment) => {
|
|
8
|
-
const box = elment.getBoundingClientRect()
|
|
9
|
-
|
|
10
|
-
return {
|
|
11
|
-
top: box.top + (window.pageYOffset - document.documentElement.clientTop),
|
|
12
|
-
left: box.left + (window.pageXOffset - document.documentElement.clientLeft),
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// @ts-expect-error TS7006
|
|
17
|
-
const computeBoundingBox = (element) => {
|
|
18
|
-
if (!isVisible(element)) {
|
|
19
|
-
return null
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const boxFrame = getOffset(element)
|
|
23
|
-
// @ts-expect-error TS2339
|
|
24
|
-
boxFrame.right = boxFrame.left + element.offsetWidth
|
|
25
|
-
// @ts-expect-error TS2339
|
|
26
|
-
boxFrame.bottom = boxFrame.top + element.offsetHeight
|
|
27
|
-
|
|
28
|
-
return {
|
|
29
|
-
left: boxFrame.left,
|
|
30
|
-
top: boxFrame.top,
|
|
31
|
-
// @ts-expect-error TS2339
|
|
32
|
-
width: boxFrame.right - boxFrame.left,
|
|
33
|
-
// @ts-expect-error TS2339
|
|
34
|
-
height: boxFrame.bottom - boxFrame.top,
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
3
|
const debounce = <A extends unknown[]>(fn: (...args: A) => void, wait: number) => {
|
|
39
4
|
let timer: ReturnType<typeof setTimeout> | undefined
|
|
40
5
|
return (...args: A) => {
|
|
@@ -43,4 +8,4 @@ const debounce = <A extends unknown[]>(fn: (...args: A) => void, wait: number) =
|
|
|
43
8
|
}
|
|
44
9
|
}
|
|
45
10
|
|
|
46
|
-
export {
|
|
11
|
+
export { debounce, isMac }
|
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: 2.
|
|
4
|
+
version: 2.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- SonicGarden
|
|
@@ -277,6 +277,7 @@ files:
|
|
|
277
277
|
- spec/support/writing_cache.rb
|
|
278
278
|
- src/copyray-overlay.ts
|
|
279
279
|
- src/copytuner-bar.ts
|
|
280
|
+
- src/copytuner-root.ts
|
|
280
281
|
- src/main.ts
|
|
281
282
|
- src/styles.ts
|
|
282
283
|
- src/util.ts
|
|
@@ -306,7 +307,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
306
307
|
- !ruby/object:Gem::Version
|
|
307
308
|
version: '0'
|
|
308
309
|
requirements: []
|
|
309
|
-
rubygems_version: 4.0.
|
|
310
|
+
rubygems_version: 4.0.10
|
|
310
311
|
specification_version: 4
|
|
311
312
|
summary: Client for the CopyTuner copy management service
|
|
312
313
|
test_files: []
|