@kagayoi/support-extension 1.0.6
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.
- package/README.md +164 -0
- package/bin/kagayoi-support-sync.mjs +95 -0
- package/dist/kagayoi-support-footer.css +45 -0
- package/dist/kagayoi-support-footer.js +217 -0
- package/dist/kagayoi-support-form.css +103 -0
- package/dist/kagayoi-support-popup.css +85 -0
- package/dist/kagayoi-support-popup.js +647 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# ブラウザ拡張向けサポートフッター
|
|
2
|
+
|
|
3
|
+
拡張の設定画面へ「お問い合わせ」と「評価をお願いします!★★★★★」を置くための共通部品です。全拡張で同じ組み込み方にするため、次の5ファイルを**正本**とし、公開npmパッケージ `@kagayoi/support-extension` から拡張側へ逐語コピーして同梱します。拡張ごとに書き換えると正本の修正を配れなくなるので、変更が要るときはこのリポジトリ側を直します。
|
|
4
|
+
|
|
5
|
+
| 正本 | 拡張側の配置 | 役割 |
|
|
6
|
+
| --- | --- | --- |
|
|
7
|
+
| `public/assets/contact-form.js` | `src/shared/kagayoi-support-popup.js` | `<kagayoi-contact-popup>` と `<kagayoi-contact-form>` |
|
|
8
|
+
| `public/assets/kagayoi-support-form.css` | `src/shared/kagayoi-support-form.css` | 問い合わせフォームのShadow DOM用CSS |
|
|
9
|
+
| `public/assets/kagayoi-support-popup.css` | `src/shared/kagayoi-support-popup.css` | 問い合わせダイアログのShadow DOM用CSS |
|
|
10
|
+
| `clients/chrome-extension/support-footer.js` | `src/shared/kagayoi-support-footer.js` | `<kagayoi-support-footer>`(2 ボタンをまとめて描画) |
|
|
11
|
+
| `clients/chrome-extension/kagayoi-support-footer.css` | `src/shared/kagayoi-support-footer.css` | 共通フッターのShadow DOM用CSS |
|
|
12
|
+
|
|
13
|
+
Manifest V3 ではリモート JavaScript を実行できないため、`https://support.kagayoi.com/assets/contact-form.js` を `<script src>` で読み込む方法は Web サイト専用です。
|
|
14
|
+
|
|
15
|
+
拡張側はパッケージ版をexact固定し、build前に同期します。`postinstall`では他リポジトリを書き換えず、明示的なscriptからだけ更新します。
|
|
16
|
+
|
|
17
|
+
```powershell
|
|
18
|
+
pnpm add --save-dev --save-exact @kagayoi/support-extension
|
|
19
|
+
pnpm exec kagayoi-support-sync
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"scripts": {
|
|
25
|
+
"sync:support": "kagayoi-support-sync",
|
|
26
|
+
"prebuild": "pnpm sync:support"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
パッケージ更新時は `pnpm update --save-exact @kagayoi/support-extension` の後に通常のbuildを行います。CIでコピーのずれだけを確認する場合は `pnpm exec kagayoi-support-sync --check` を使います。
|
|
32
|
+
|
|
33
|
+
この正本リポジトリの保守時は `pnpm sync:extensions` で既存の兄弟リポジトリとの差分とHTML組み込み契約を確認できます。比較時は CRLF / LF の差を無視し、完全なフッタータグと、そのタグより後に1回ずつ置いた2本の module スクリプト(popup → footer の順)も検証します。
|
|
34
|
+
|
|
35
|
+
JavaScript 2ファイルは `innerHTML` への代入を持ちません。AMO の静的解析 `UNSAFE_VAR_ASSIGNMENT` と、拡張側リポジトリの「`innerHTML` 不使用」契約の両方を満たすためで、拡張ごとに書き換えずそのまま通せます。3つのCSSは `style-src 'self'` に適合する同梱ファイルとして各Shadow DOMから自動で読み込みます。製品ごとの配置・余白・配色はフッターを置く既存レイアウトと `--kgs-*` 変数で調整し、同梱コピーへ製品固有コードを加えません。
|
|
36
|
+
|
|
37
|
+
## 組み込み
|
|
38
|
+
|
|
39
|
+
1. `@kagayoi/support-extension` をexact固定し、`kagayoi-support-sync` でJavaScript 2ファイルとCSS 3ファイルを拡張の `src/shared/` へ同期する。
|
|
40
|
+
2. `manifest.json` に API のホスト権限を追加する。
|
|
41
|
+
3. 設定画面(`popup.html` / `options.html`)の末尾へタグとスクリプトを置く。
|
|
42
|
+
4. `product-id` を D1 の `products` に登録する(`migrations/` にシードを追加)。未登録だと API が `unknown product` を返します。
|
|
43
|
+
5. Firefox 版も出す拡張は、下の「Firefox 版」の manifest 追記を行う。
|
|
44
|
+
|
|
45
|
+
拡張の Origin は Worker がスキームで許可するので、`CLIENT_ORIGINS` へ拡張 ID を足す作業はありません。
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"host_permissions": [
|
|
50
|
+
"https://support.kagayoi.com/"
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```html
|
|
56
|
+
<kagayoi-support-footer product-id="my-extension"></kagayoi-support-footer>
|
|
57
|
+
|
|
58
|
+
<script type="module" src="../shared/kagayoi-support-popup.js"></script>
|
|
59
|
+
<script type="module" src="../shared/kagayoi-support-footer.js"></script>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
製品名・拡張バージョン・評価リンクは実行時に manifest から決まるので、拡張側で書くのは `product-id` だけです。
|
|
63
|
+
|
|
64
|
+
`type="module"` にするのは、モジュールスクリプトが既定で defer 相当になり、記述順どおりに「部品の定義 → フッターの定義」で走るためです。Vite などのバンドラを使う拡張でも、この形なら同梱ファイルとして扱われます(`defer` 付きのクラシックスクリプトだと取りこぼされます)。中身は IIFE なので、モジュールにしても動作は変わりません。
|
|
65
|
+
|
|
66
|
+
拡張ページの `content_security_policy` で `default-src` または `connect-src` を明示している場合は、`connect-src 'self' https://support.kagayoi.com` も許可します。
|
|
67
|
+
|
|
68
|
+
`app-version` は `runtime.getManifest().version` から自動で入ります。評価リンクの URL も実行時に決まるため、拡張ごとにストア ID を書く必要はありません。
|
|
69
|
+
|
|
70
|
+
| 実行環境 | 評価リンクの向き先 |
|
|
71
|
+
| --- | --- |
|
|
72
|
+
| Chrome | `https://chromewebstore.google.com/detail/<runtime.id>` |
|
|
73
|
+
| Edge(`update_url` に microsoft を含む) | `https://microsoftedge.microsoft.com/addons/detail/<runtime.id>` |
|
|
74
|
+
| Firefox | `https://addons.mozilla.org/firefox/addon/<amo-slug または gecko.id>/` |
|
|
75
|
+
|
|
76
|
+
拡張ページ以外(`popup.html` を素のブラウザで開くプレビュー等)では、フッターは自分を非表示にします。
|
|
77
|
+
|
|
78
|
+
## 設定
|
|
79
|
+
|
|
80
|
+
`<kagayoi-support-footer>` の属性です。
|
|
81
|
+
|
|
82
|
+
| 属性 | 必須 | 内容 |
|
|
83
|
+
| --- | --- | --- |
|
|
84
|
+
| `product-id` | はい | D1 の `products.id` と一致する製品 ID |
|
|
85
|
+
| `product-name` | いいえ | 省略時は manifest の `name`(`__MSG_*__` は `_locales` から解決) |
|
|
86
|
+
| `store-url` | いいえ | 評価リンクの URL を固定する場合だけ指定(自動判定より優先) |
|
|
87
|
+
| `amo-slug` | いいえ | Firefox 版の AMO スラッグ。省略時は `gecko.id` |
|
|
88
|
+
| `contact-label` / `rate-label` | いいえ | ボタンのラベル。省略時は `_locales` の `kagayoiSupportContact` / `kagayoiSupportRate`、それも無ければ組み込みの日本語 / 英語 |
|
|
89
|
+
| `dialog-title` | いいえ | ダイアログ見出し |
|
|
90
|
+
| `app-version` | いいえ | 省略時は `runtime.getManifest().version` |
|
|
91
|
+
| `os-version` / `locale` / `diagnostics` | いいえ | 問い合わせへ添付する情報 |
|
|
92
|
+
| `hide-contact` / `hide-rate` | いいえ | どちらかのボタンだけ出す場合に付ける |
|
|
93
|
+
| `firefox-data-consent` | いいえ | Firefox manifest で問い合わせ情報を任意申告する拡張だけ、送信前に許可を要求する |
|
|
94
|
+
| `api-base` | いいえ | ローカル検証などで API URL を差し替える場合だけ指定 |
|
|
95
|
+
|
|
96
|
+
配色は `--kgs-surface` / `--kgs-surface-hover` / `--kgs-border` / `--kgs-star` / `--kgs-radius` / `--kgs-gap` / `--kgs-accent` で上書きできます。既定はホストの文字色に対する中立の灰色オーバーレイなので、ライト / ダークどちらのテーマでも指定なしで馴染みます。
|
|
97
|
+
|
|
98
|
+
## 問い合わせフォームだけを使う
|
|
99
|
+
|
|
100
|
+
既存のボタンからダイアログを開く場合は `<kagayoi-contact-popup>` を直接使います。
|
|
101
|
+
|
|
102
|
+
```html
|
|
103
|
+
<button id="support-button" type="button">お問い合わせ</button>
|
|
104
|
+
<kagayoi-contact-popup
|
|
105
|
+
id="support-popup"
|
|
106
|
+
hide-trigger
|
|
107
|
+
product-id="my-extension"
|
|
108
|
+
product-name="拡張機能名"
|
|
109
|
+
></kagayoi-contact-popup>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
```js
|
|
113
|
+
document.querySelector("#support-button").addEventListener("click", () => {
|
|
114
|
+
document.querySelector("#support-popup").open()
|
|
115
|
+
})
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
ポップアップは `channel="extension"` と永続する `localStorage` セッションを自動で使います。OTP のアクセストークンは拡張ごとのオリジンに保存され、API が返した有効期限を過ぎると削除されます。
|
|
119
|
+
|
|
120
|
+
| 属性 | 必須 | 内容 |
|
|
121
|
+
| --- | --- | --- |
|
|
122
|
+
| `product-id` | はい | D1 の `products.id` と一致する製品 ID |
|
|
123
|
+
| `product-name` | いいえ | フォームに表示する製品名 |
|
|
124
|
+
| `app-version` | いいえ | 問い合わせへ添付する拡張バージョン |
|
|
125
|
+
| `os-version` | いいえ | 問い合わせへ添付する OS 情報 |
|
|
126
|
+
| `locale` | いいえ | 省略時は `navigator.language` |
|
|
127
|
+
| `diagnostics` | いいえ | 問い合わせへ添付する診断情報 |
|
|
128
|
+
| `button-label` | いいえ | 内蔵ボタンのラベル |
|
|
129
|
+
| `dialog-title` | いいえ | ダイアログ見出し |
|
|
130
|
+
| `api-base` | いいえ | ローカル検証などで API URL を差し替える場合だけ指定 |
|
|
131
|
+
|
|
132
|
+
送信完了時は、受付番号を `detail.reference` に含む `kagayoi-support-submitted` イベントが発火します。
|
|
133
|
+
|
|
134
|
+
```js
|
|
135
|
+
document.querySelector("kagayoi-contact-popup").addEventListener("kagayoi-support-submitted", (event) => {
|
|
136
|
+
console.info(event.detail.reference)
|
|
137
|
+
})
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Firefox 版
|
|
141
|
+
|
|
142
|
+
Chrome と同じく問い合わせも評価も動きます。Firefox の拡張 Origin `moz-extension://<UUID>` はインストールごとに変わって列挙できないため、Worker 側は `CLIENT_ORIGINS` の列挙ではなく**拡張スキームで許可**しています(`src/index.ts` の `isAllowedOrigin`)。拡張を公開しても `CLIENT_ORIGINS` へ ID を足す必要はありません。
|
|
143
|
+
|
|
144
|
+
Firefox 版で追加が要るのは manifest の 2 点です。
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
{
|
|
148
|
+
"host_permissions": ["https://support.kagayoi.com/*"],
|
|
149
|
+
"browser_specific_settings": {
|
|
150
|
+
"gecko": {
|
|
151
|
+
"data_collection_permissions": {
|
|
152
|
+
"required": ["personallyIdentifyingInfo", "authenticationInfo"],
|
|
153
|
+
"optional": ["personalCommunications"]
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`data_collection_permissions` は AMO の申告で、問い合わせフォームが**メールアドレス**(`personallyIdentifyingInfo`)、**確認コード**(`authenticationInfo`)、**問い合わせ本文**(`personalCommunications`)を送るためです。利用者が自分で入力した値も申告対象になります。上の例では、フォームを開く前から必要な2分類を `required`、実際に本文を送るときだけ必要な `personalCommunications` を `optional` に分けています。`"none"` は他の値と併記できないので、`["none"]` を宣言している拡張は置き換えます。
|
|
161
|
+
|
|
162
|
+
`personalCommunications` を `optional` で申告する拡張は、フッターへ `firefox-data-consent` を付けます。Firefoxだけ送信直前に `permissions.request()` でこの1分類を要求し、Chromeでは何もしません。3分類をすべて `required` で申告する拡張や、独自ボタンで許可を取得してからフォームを開く拡張には付けません。
|
|
163
|
+
|
|
164
|
+
申告を変えると AMO の再審査が要ります。プライバシーポリシーと掲載文にも問い合わせ窓口の記載を合わせてください。
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync } from "node:fs"
|
|
3
|
+
import { dirname, join, resolve } from "node:path"
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "node:url"
|
|
5
|
+
|
|
6
|
+
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..")
|
|
7
|
+
const DEFAULT_ASSET_DIRECTORY = join(PACKAGE_ROOT, "dist")
|
|
8
|
+
const TARGET_DIRECTORY = join("src", "shared")
|
|
9
|
+
const ASSETS = Object.freeze([
|
|
10
|
+
"kagayoi-support-popup.js",
|
|
11
|
+
"kagayoi-support-form.css",
|
|
12
|
+
"kagayoi-support-popup.css",
|
|
13
|
+
"kagayoi-support-footer.js",
|
|
14
|
+
"kagayoi-support-footer.css",
|
|
15
|
+
])
|
|
16
|
+
|
|
17
|
+
const normalizedText = (path) => readFileSync(path, "utf8").replace(/\r\n?/g, "\n")
|
|
18
|
+
|
|
19
|
+
export function syncExtensionAssets({
|
|
20
|
+
targetRoot = process.cwd(),
|
|
21
|
+
check = false,
|
|
22
|
+
assetDirectory = DEFAULT_ASSET_DIRECTORY,
|
|
23
|
+
} = {}) {
|
|
24
|
+
const root = resolve(targetRoot)
|
|
25
|
+
const destinationDirectory = join(root, TARGET_DIRECTORY)
|
|
26
|
+
const changed = []
|
|
27
|
+
|
|
28
|
+
if (!check) mkdirSync(destinationDirectory, { recursive: true })
|
|
29
|
+
for (const name of ASSETS) {
|
|
30
|
+
const source = join(assetDirectory, name)
|
|
31
|
+
const destination = join(destinationDirectory, name)
|
|
32
|
+
if (!existsSync(source)) {
|
|
33
|
+
throw new Error(`npm パッケージに ${name} がありません。再インストールしてください。`)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const matches = existsSync(destination) && normalizedText(destination) === normalizedText(source)
|
|
37
|
+
if (matches) continue
|
|
38
|
+
changed.push(name)
|
|
39
|
+
if (!check) copyFileSync(source, destination)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { changed, destinationDirectory }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseArguments(args) {
|
|
46
|
+
let targetRoot = process.cwd()
|
|
47
|
+
let check = false
|
|
48
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
49
|
+
const argument = args[index]
|
|
50
|
+
if (argument === "--check") {
|
|
51
|
+
check = true
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
if (argument === "--target") {
|
|
55
|
+
const value = args[index + 1]
|
|
56
|
+
if (!value) throw new Error("--target には拡張リポジトリのパスが必要です。")
|
|
57
|
+
targetRoot = value
|
|
58
|
+
index += 1
|
|
59
|
+
continue
|
|
60
|
+
}
|
|
61
|
+
if (argument === "--help" || argument === "-h") {
|
|
62
|
+
return { help: true, targetRoot, check }
|
|
63
|
+
}
|
|
64
|
+
throw new Error(`不明な引数です: ${argument}`)
|
|
65
|
+
}
|
|
66
|
+
return { help: false, targetRoot, check }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function printHelp() {
|
|
70
|
+
console.log("kagayoi-support-sync [--target <extension-root>] [--check]")
|
|
71
|
+
console.log(" 指定先の src/shared へ共通 JavaScript 2本と CSS 3本を同期します。")
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const entrypoint = process.argv[1] ? pathToFileURL(resolve(process.argv[1])).href : ""
|
|
75
|
+
if (entrypoint === import.meta.url) {
|
|
76
|
+
try {
|
|
77
|
+
const options = parseArguments(process.argv.slice(2))
|
|
78
|
+
if (options.help) {
|
|
79
|
+
printHelp()
|
|
80
|
+
} else {
|
|
81
|
+
const result = syncExtensionAssets(options)
|
|
82
|
+
if (!result.changed.length) {
|
|
83
|
+
console.log(`一致: ${result.destinationDirectory}`)
|
|
84
|
+
} else if (options.check) {
|
|
85
|
+
console.error(`差分: ${result.changed.join(", ")}`)
|
|
86
|
+
process.exitCode = 1
|
|
87
|
+
} else {
|
|
88
|
+
console.log(`同期: ${result.changed.length} ファイル → ${result.destinationDirectory}`)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error(error instanceof Error ? error.message : String(error))
|
|
93
|
+
process.exitCode = 1
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
:host {
|
|
2
|
+
--kgs-gap: 8px;
|
|
3
|
+
--kgs-radius: 10px;
|
|
4
|
+
--kgs-star: #f5b301;
|
|
5
|
+
--kgs-surface: rgba(128, 128, 128, 0.10);
|
|
6
|
+
--kgs-surface-hover: rgba(128, 128, 128, 0.18);
|
|
7
|
+
--kgs-border: rgba(128, 128, 128, 0.32);
|
|
8
|
+
display: block;
|
|
9
|
+
color: inherit;
|
|
10
|
+
font: inherit;
|
|
11
|
+
}
|
|
12
|
+
:host([hidden]) { display: none !important; }
|
|
13
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
14
|
+
[hidden] { display: none !important; }
|
|
15
|
+
.stack { display: grid; gap: var(--kgs-gap); }
|
|
16
|
+
.item {
|
|
17
|
+
display: flex;
|
|
18
|
+
align-items: center;
|
|
19
|
+
justify-content: center;
|
|
20
|
+
gap: 10px;
|
|
21
|
+
width: 100%;
|
|
22
|
+
min-height: 40px;
|
|
23
|
+
padding: 10px 14px;
|
|
24
|
+
border: 1px solid var(--kgs-border);
|
|
25
|
+
border-radius: var(--kgs-radius);
|
|
26
|
+
background: var(--kgs-surface);
|
|
27
|
+
color: inherit;
|
|
28
|
+
font: inherit;
|
|
29
|
+
font-weight: 700;
|
|
30
|
+
line-height: 1.4;
|
|
31
|
+
text-align: center;
|
|
32
|
+
text-decoration: none;
|
|
33
|
+
cursor: pointer;
|
|
34
|
+
}
|
|
35
|
+
.item:hover { background: var(--kgs-surface-hover); }
|
|
36
|
+
.item:active { transform: translateY(1px); }
|
|
37
|
+
.item:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }
|
|
38
|
+
.rate { justify-content: space-between; text-align: left; }
|
|
39
|
+
.stars { flex: none; color: var(--kgs-star); font-size: 1.15em; letter-spacing: 0.04em; }
|
|
40
|
+
/* hide-trigger 付きでも inline-block は行ボックスを作るので、フローから外して余白を出さない。
|
|
41
|
+
アクセント色は拡張側の --accent / --primary を優先し、無ければ問い合わせフォームの既定に戻す。 */
|
|
42
|
+
kagayoi-contact-popup {
|
|
43
|
+
position: absolute;
|
|
44
|
+
--ks-accent: var(--kgs-accent, var(--accent, var(--primary, #006fee)));
|
|
45
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* <kagayoi-support-footer> — Chrome 拡張の設定画面に置く共通フッター。
|
|
3
|
+
*
|
|
4
|
+
* 「お問い合わせ」と「評価をお願いします!★★★★★」の 2 ボタンをまとめて描画する。
|
|
5
|
+
* 全拡張で同じ組み込み方にするための部品で、拡張側では次の 3 行だけを書く。
|
|
6
|
+
*
|
|
7
|
+
* <kagayoi-support-footer product-id="my-extension" product-name="拡張機能名"></kagayoi-support-footer>
|
|
8
|
+
* <script type="module" src="../shared/kagayoi-support-popup.js"></script>
|
|
9
|
+
* <script type="module" src="../shared/kagayoi-support-footer.js"></script>
|
|
10
|
+
*
|
|
11
|
+
* このファイルは Kagayoi.Support が正本。拡張側へは逐語コピーで同梱し、拡張ごとに改変しない
|
|
12
|
+
* (改変すると「作り方が全拡張で同じ」が崩れ、正本の修正が配れなくなる)。
|
|
13
|
+
*
|
|
14
|
+
* 方針:
|
|
15
|
+
* - innerHTML を使わない。AMO の静的解析 UNSAFE_VAR_ASSIGNMENT 回避と、拡張側リポジトリの
|
|
16
|
+
* 「innerHTML 不使用」契約に合わせるため、DOM は createElement + textContent で組む。
|
|
17
|
+
* - 配色は host のテキスト色に対する灰色オーバーレイで作る。拡張ごとに CSS 変数名が違うので、
|
|
18
|
+
* どのテーマ(ライト / ダーク)でも破綻しない中立色を既定にし、必要なら --kgs-* で上書きする。
|
|
19
|
+
* - ストア URL は runtime.id から実行時に組み立てる。拡張ごとに ID を書かないので設定漏れが出ない。
|
|
20
|
+
*/
|
|
21
|
+
(() => {
|
|
22
|
+
"use strict"
|
|
23
|
+
|
|
24
|
+
const SUPPORT_SITE = "https://support.kagayoi.com/"
|
|
25
|
+
const DEFINITION_WAIT_MS = 1000
|
|
26
|
+
|
|
27
|
+
const LABELS = {
|
|
28
|
+
ja: { contact: "お問い合わせ", rate: "評価をお願いします!" },
|
|
29
|
+
en: { contact: "Contact support", rate: "Please rate us!" },
|
|
30
|
+
}
|
|
31
|
+
const STYLESHEET_URL = bundledStylesheetUrl("kagayoi-support-footer.css")
|
|
32
|
+
|
|
33
|
+
function bundledStylesheetUrl(fileName) {
|
|
34
|
+
const api = extensionApi()
|
|
35
|
+
if (api) return api.runtime.getURL(`src/shared/${fileName}`)
|
|
36
|
+
|
|
37
|
+
const script = Array.from(document.scripts).find(({ src }) =>
|
|
38
|
+
/(?:^|\/)kagayoi-support-footer\.js(?:[?#]|$)/.test(src),
|
|
39
|
+
)
|
|
40
|
+
return new URL(fileName, script?.src || document.baseURI).href
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function replaceShadowContent(shadowRoot, ...nodes) {
|
|
44
|
+
const stylesheet = document.createElement("link")
|
|
45
|
+
stylesheet.rel = "stylesheet"
|
|
46
|
+
stylesheet.href = STYLESHEET_URL
|
|
47
|
+
shadowRoot.replaceChildren(stylesheet, ...nodes)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Chrome は chrome.*、Firefox は browser.*。拡張ページ以外(プレビュー等)では null。 */
|
|
51
|
+
function extensionApi() {
|
|
52
|
+
const api = typeof browser !== "undefined" ? browser : typeof chrome !== "undefined" ? chrome : null
|
|
53
|
+
return api?.runtime?.getURL ? api : null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Firefox 判定は評価リンクの向き先(AMO)を決めるためだけに使う。問い合わせは
|
|
57
|
+
// Worker が拡張スキームを許可しているので、Chrome / Firefox のどちらでも送れる。
|
|
58
|
+
function isFirefox(api) {
|
|
59
|
+
return api.runtime.getURL("").startsWith("moz-extension://")
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function uiLanguage(api) {
|
|
63
|
+
const tag = api?.i18n?.getUILanguage?.() || navigator.language || "en"
|
|
64
|
+
return tag.toLowerCase().startsWith("ja") ? "ja" : "en"
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* ラベルの優先順位: 属性 → 拡張の _locales(キーがあれば) → 組み込みの ja/en。
|
|
69
|
+
* 組み込みを持つので、拡張ごとに messages.json を増やさなくても日本語表示になる。
|
|
70
|
+
*/
|
|
71
|
+
function label(host, api, attribute, messageKey, fallbackKey) {
|
|
72
|
+
const fromAttribute = host.getAttribute(attribute)?.trim()
|
|
73
|
+
if (fromAttribute) return fromAttribute
|
|
74
|
+
const fromLocale = api?.i18n?.getMessage?.(messageKey)?.trim()
|
|
75
|
+
if (fromLocale) return fromLocale
|
|
76
|
+
return LABELS[uiLanguage(api)][fallbackKey]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 製品名。既定は manifest の name で、`__MSG_appName__` 形式なら _locales から引き直す
|
|
81
|
+
* (getManifest() はプレースホルダを解決しないため)。拡張ごとに名前を書かなくて済む。
|
|
82
|
+
*/
|
|
83
|
+
function productName(host, api) {
|
|
84
|
+
const explicit = host.getAttribute("product-name")?.trim()
|
|
85
|
+
if (explicit) return explicit
|
|
86
|
+
const raw = api.runtime.getManifest?.().name?.trim() ?? ""
|
|
87
|
+
const messageKey = raw.match(/^__MSG_(.+)__$/)?.[1]
|
|
88
|
+
if (!messageKey) return raw
|
|
89
|
+
return api.i18n?.getMessage?.(messageKey)?.trim() || ""
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 評価ページの URL。Firefox は AMO、update_url に microsoft を含むものは Edge アドオン、
|
|
94
|
+
* それ以外は Chrome ウェブストア。拡張 ID は公開時にストアの ID と一致する。
|
|
95
|
+
*/
|
|
96
|
+
function storeUrl(host, api) {
|
|
97
|
+
const explicit = host.getAttribute("store-url")?.trim()
|
|
98
|
+
if (explicit?.startsWith("https://")) return explicit
|
|
99
|
+
if (!api) return ""
|
|
100
|
+
const manifest = api.runtime.getManifest?.() ?? {}
|
|
101
|
+
if (isFirefox(api)) {
|
|
102
|
+
const slug = host.getAttribute("amo-slug")?.trim() || manifest.browser_specific_settings?.gecko?.id || ""
|
|
103
|
+
return slug ? `https://addons.mozilla.org/firefox/addon/${encodeURIComponent(slug)}/` : ""
|
|
104
|
+
}
|
|
105
|
+
const id = api.runtime.id
|
|
106
|
+
if (!id) return ""
|
|
107
|
+
return String(manifest.update_url ?? "").toLowerCase().includes("microsoft")
|
|
108
|
+
? `https://microsoftedge.microsoft.com/addons/detail/${id}`
|
|
109
|
+
: `https://chromewebstore.google.com/detail/${id}`
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
class KagayoiSupportFooter extends HTMLElement {
|
|
113
|
+
constructor() {
|
|
114
|
+
super()
|
|
115
|
+
this.attachShadow({ mode: "open" })
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
connectedCallback() {
|
|
119
|
+
if (this.shadowRoot.childElementCount) return
|
|
120
|
+
const api = extensionApi()
|
|
121
|
+
// 拡張ページ以外(popup.html を素のブラウザで開いたプレビュー等)では出さない。
|
|
122
|
+
// 問い合わせ API は拡張 Origin 前提で、ストア URL も組み立てられないため。
|
|
123
|
+
if (!api) {
|
|
124
|
+
this.hidden = true
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const stack = document.createElement("div")
|
|
129
|
+
stack.className = "stack"
|
|
130
|
+
|
|
131
|
+
if (!this.hasAttribute("hide-contact")) stack.append(this.buildContact(api))
|
|
132
|
+
if (!this.hasAttribute("hide-rate")) {
|
|
133
|
+
const rate = this.buildRate(api)
|
|
134
|
+
if (rate) stack.append(rate)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
replaceShadowContent(this.shadowRoot, stack)
|
|
138
|
+
if (!stack.childElementCount) this.hidden = true
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
buildContact(api) {
|
|
142
|
+
const button = document.createElement("button")
|
|
143
|
+
button.type = "button"
|
|
144
|
+
button.className = "item contact"
|
|
145
|
+
button.setAttribute("aria-haspopup", "dialog")
|
|
146
|
+
button.textContent = label(this, api, "contact-label", "kagayoiSupportContact", "contact")
|
|
147
|
+
|
|
148
|
+
this.popup = document.createElement("kagayoi-contact-popup")
|
|
149
|
+
this.popup.setAttribute("hide-trigger", "")
|
|
150
|
+
this.popup.setAttribute("product-id", this.getAttribute("product-id")?.trim() ?? "")
|
|
151
|
+
const dialogTitle = this.getAttribute("dialog-title")?.trim() || button.textContent
|
|
152
|
+
this.popup.setAttribute("dialog-title", dialogTitle)
|
|
153
|
+
const name = productName(this, api)
|
|
154
|
+
if (name) this.popup.setAttribute("product-name", name)
|
|
155
|
+
for (const attribute of ["api-base", "os-version", "locale", "diagnostics"]) {
|
|
156
|
+
const value = this.getAttribute(attribute)?.trim()
|
|
157
|
+
if (value) this.popup.setAttribute(attribute, value)
|
|
158
|
+
}
|
|
159
|
+
if (this.hasAttribute("firefox-data-consent")) this.popup.setAttribute("firefox-data-consent", "")
|
|
160
|
+
const version = this.getAttribute("app-version")?.trim() || api.runtime.getManifest?.().version
|
|
161
|
+
if (version) this.popup.setAttribute("app-version", version)
|
|
162
|
+
|
|
163
|
+
button.addEventListener("click", async () => {
|
|
164
|
+
// バンドラが読み込み順を変えて定義が後から来ることがあるので、少しだけ待つ。
|
|
165
|
+
if (!customElements.get("kagayoi-contact-popup")) {
|
|
166
|
+
await Promise.race([
|
|
167
|
+
customElements.whenDefined("kagayoi-contact-popup"),
|
|
168
|
+
new Promise((resolve) => setTimeout(resolve, DEFINITION_WAIT_MS)),
|
|
169
|
+
])
|
|
170
|
+
}
|
|
171
|
+
// それでも来なければ(kagayoi-support-popup.js の同梱漏れ)操作不能にはせず、Web の窓口を開く。
|
|
172
|
+
if (typeof this.popup.open === "function") {
|
|
173
|
+
this.popup.open(button)
|
|
174
|
+
} else if (api.tabs?.create) {
|
|
175
|
+
api.tabs.create({ url: SUPPORT_SITE })
|
|
176
|
+
} else {
|
|
177
|
+
window.open(SUPPORT_SITE, "_blank", "noopener")
|
|
178
|
+
}
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
const wrapper = document.createDocumentFragment()
|
|
182
|
+
wrapper.append(button, this.popup)
|
|
183
|
+
return wrapper
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
buildRate(api) {
|
|
187
|
+
const href = storeUrl(this, api)
|
|
188
|
+
if (!href) return null
|
|
189
|
+
const link = document.createElement("a")
|
|
190
|
+
link.className = "item rate"
|
|
191
|
+
link.href = href
|
|
192
|
+
link.target = "_blank"
|
|
193
|
+
link.rel = "noreferrer noopener"
|
|
194
|
+
|
|
195
|
+
const text = document.createElement("span")
|
|
196
|
+
text.className = "rate-label"
|
|
197
|
+
text.textContent = label(this, api, "rate-label", "kagayoiSupportRate", "rate")
|
|
198
|
+
|
|
199
|
+
const stars = document.createElement("span")
|
|
200
|
+
stars.className = "stars"
|
|
201
|
+
stars.setAttribute("aria-hidden", "true")
|
|
202
|
+
stars.textContent = "★★★★★"
|
|
203
|
+
|
|
204
|
+
link.append(text, stars)
|
|
205
|
+
return link
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** 既存ボタンから問い合わせダイアログだけ開きたい場合の入口。 */
|
|
209
|
+
open() {
|
|
210
|
+
this.popup?.open?.()
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!customElements.get("kagayoi-support-footer")) {
|
|
215
|
+
customElements.define("kagayoi-support-footer", KagayoiSupportFooter)
|
|
216
|
+
}
|
|
217
|
+
})()
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
:host {
|
|
2
|
+
--ks-accent: var(--accent, var(--primary, #006fee));
|
|
3
|
+
--ks-surface: var(--panel, var(--surface, #ffffff));
|
|
4
|
+
--ks-surface-soft: var(--panel-2, var(--content2, rgba(127, 127, 127, 0.08)));
|
|
5
|
+
--ks-border: var(--line, var(--border, rgba(127, 127, 127, 0.28)));
|
|
6
|
+
--ks-text: var(--ink, var(--fg, inherit));
|
|
7
|
+
--ks-muted: var(--muted, #62626b);
|
|
8
|
+
display: block;
|
|
9
|
+
color-scheme: var(--kgs-color-scheme, inherit);
|
|
10
|
+
color: var(--ks-text);
|
|
11
|
+
font: inherit;
|
|
12
|
+
line-height: 1.7;
|
|
13
|
+
}
|
|
14
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
15
|
+
[hidden] { display: none !important; }
|
|
16
|
+
.panel {
|
|
17
|
+
padding: clamp(1rem, 3vw, 1.5rem);
|
|
18
|
+
border: 1px solid var(--ks-border);
|
|
19
|
+
border-radius: 16px;
|
|
20
|
+
background-color: var(--ks-surface);
|
|
21
|
+
}
|
|
22
|
+
.intro { margin: 0 0 1.25rem; color: var(--ks-muted); font-size: 0.9rem; }
|
|
23
|
+
.grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; }
|
|
24
|
+
.field { display: grid; gap: 0.4rem; min-width: 0; }
|
|
25
|
+
.field--full { grid-column: 1 / -1; }
|
|
26
|
+
label, legend { text-align: left; font-weight: 600; font-size: 0.88rem; }
|
|
27
|
+
.optional { color: var(--ks-muted); font-size: 0.76rem; font-weight: 400; }
|
|
28
|
+
input, select, textarea, button { font: inherit; }
|
|
29
|
+
input, select, textarea {
|
|
30
|
+
width: 100%;
|
|
31
|
+
min-height: 44px;
|
|
32
|
+
padding: 0.68rem 0.78rem;
|
|
33
|
+
border: 1px solid var(--ks-border);
|
|
34
|
+
border-radius: 10px;
|
|
35
|
+
background-color: var(--ks-surface);
|
|
36
|
+
color: var(--ks-text);
|
|
37
|
+
}
|
|
38
|
+
option {
|
|
39
|
+
background-color: var(--ks-option-surface, var(--ks-surface));
|
|
40
|
+
color: var(--ks-text);
|
|
41
|
+
}
|
|
42
|
+
textarea { min-height: 10rem; resize: vertical; }
|
|
43
|
+
input:focus-visible, select:focus-visible, textarea:focus-visible, button:focus-visible, a:focus-visible {
|
|
44
|
+
outline: 3px solid color-mix(in srgb, var(--ks-accent) 35%, transparent);
|
|
45
|
+
outline-offset: 2px;
|
|
46
|
+
}
|
|
47
|
+
.verification {
|
|
48
|
+
grid-column: 1 / -1;
|
|
49
|
+
display: grid;
|
|
50
|
+
grid-template-columns: minmax(0, 14rem) 1fr;
|
|
51
|
+
align-items: end;
|
|
52
|
+
gap: 0.8rem;
|
|
53
|
+
padding: 1rem;
|
|
54
|
+
border: 1px solid var(--ks-border);
|
|
55
|
+
border-radius: 12px;
|
|
56
|
+
background-color: var(--ks-surface-soft);
|
|
57
|
+
}
|
|
58
|
+
.verification p { align-self: center; margin: 0; color: var(--ks-muted); font-size: 0.82rem; }
|
|
59
|
+
.resend {
|
|
60
|
+
justify-self: start;
|
|
61
|
+
padding: 0;
|
|
62
|
+
border: 0;
|
|
63
|
+
background-color: transparent;
|
|
64
|
+
color: var(--ks-accent);
|
|
65
|
+
cursor: pointer;
|
|
66
|
+
text-decoration: underline;
|
|
67
|
+
text-underline-offset: 0.18em;
|
|
68
|
+
}
|
|
69
|
+
.actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.8rem; margin-top: 1.15rem; }
|
|
70
|
+
.submit, .again {
|
|
71
|
+
min-height: 46px;
|
|
72
|
+
padding: 0.72rem 1.15rem;
|
|
73
|
+
border: 1px solid var(--ks-accent);
|
|
74
|
+
border-radius: 999px;
|
|
75
|
+
background-color: var(--ks-accent);
|
|
76
|
+
color: #ffffff;
|
|
77
|
+
font-weight: 700;
|
|
78
|
+
cursor: pointer;
|
|
79
|
+
}
|
|
80
|
+
.submit:disabled, .again:disabled, .resend:disabled { cursor: wait; opacity: 0.62; }
|
|
81
|
+
.auth-note { margin: 0; color: var(--ks-muted); font-size: 0.8rem; }
|
|
82
|
+
.status { min-height: 1.7em; margin: 0.8rem 0 0; font-size: 0.86rem; }
|
|
83
|
+
.status[data-kind="error"] { color: #c62828; }
|
|
84
|
+
.status[data-kind="success"] { color: #19743b; }
|
|
85
|
+
.success {
|
|
86
|
+
padding: clamp(1.2rem, 4vw, 2rem);
|
|
87
|
+
border: 1px solid var(--ks-border);
|
|
88
|
+
border-radius: 16px;
|
|
89
|
+
background-color: var(--ks-surface);
|
|
90
|
+
text-align: center;
|
|
91
|
+
}
|
|
92
|
+
.success h2 { margin: 0 0 0.55rem; font-size: 1.35rem; }
|
|
93
|
+
.success p { margin: 0.45rem 0; color: var(--ks-muted); }
|
|
94
|
+
.reference { color: var(--ks-text) !important; font-weight: 700; letter-spacing: 0.05em; }
|
|
95
|
+
.success a { color: var(--ks-accent); }
|
|
96
|
+
.again { margin-top: 0.8rem; }
|
|
97
|
+
.trap { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }
|
|
98
|
+
@media (max-width: 620px) {
|
|
99
|
+
.grid, .verification { grid-template-columns: 1fr; }
|
|
100
|
+
.field--full { grid-column: auto; }
|
|
101
|
+
.submit { width: 100%; }
|
|
102
|
+
}
|
|
103
|
+
@media (prefers-reduced-motion: reduce) { * { scroll-behavior: auto !important; } }
|