@show-docs/docusaurus-theme-copy-button 0.0.0-stage → 0.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.
- package/README.md +61 -2
- package/components/copy-button.tsx +45 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +39 -0
- package/package.json +49 -3
- package/styles/copy-button.css +67 -0
- package/theme/Footer/Copyright/index.tsx +20 -0
package/README.md
CHANGED
|
@@ -1,3 +1,62 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @show-docs/docusaurus-theme-copy-button
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
在页脚那一格加一颗复制按钮:点一下把此刻地址栏里的页面地址写进剪贴板,成败都给一条提示。
|
|
4
|
+
页脚原本的文案一个字都不改。
|
|
5
|
+
|
|
6
|
+
## 安装
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pnpm add @show-docs/docusaurus-theme-copy-button
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## 使用
|
|
13
|
+
|
|
14
|
+
### 注册插件
|
|
15
|
+
|
|
16
|
+
```js
|
|
17
|
+
// docusaurus.config.mjs
|
|
18
|
+
export default {
|
|
19
|
+
plugins: ['@show-docs/docusaurus-theme-copy-button']
|
|
20
|
+
};
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### 页脚要有内容
|
|
24
|
+
|
|
25
|
+
这一层覆盖的是页脚里的 `Footer/Copyright` 那一格,所以站点得给它东西渲染:
|
|
26
|
+
`themeConfig.footer.copyright` 写过才看得见这一格,没写的站点里这一层等于不存在。
|
|
27
|
+
|
|
28
|
+
## 层栈
|
|
29
|
+
|
|
30
|
+
本层提供 `theme/Footer/Copyright/index.tsx`:它先向下面那一层要 `Footer/Copyright` 的实现
|
|
31
|
+
(`@theme-prev/Footer/Copyright`),把文案原样交下去,只在它前面多摆一颗按钮。
|
|
32
|
+
这一层不读 frontmatter,也没有插件选项。
|
|
33
|
+
|
|
34
|
+
层栈怎么推导、谁能盖住谁,是核心解析器那一格的事,见
|
|
35
|
+
[`plugin-prev-layer-resolver` 的 README](../../plugins/plugin-prev-layer-resolver/README.md);
|
|
36
|
+
这一层自己只负责「挂在哪一格」。
|
|
37
|
+
|
|
38
|
+
## 工作方式
|
|
39
|
+
|
|
40
|
+
- 按钮是 `<button type="button">`,图标是 `styles/copy-button.css` 里纯 CSS 画的一份形状,
|
|
41
|
+
不请求图标字体也不带图片;按钮上没有文字,读屏拿到的名字是 `aria-label` 那一句。
|
|
42
|
+
- 复制走 `navigator.clipboard.writeText()`,提示条由 [`sonner`](https://sonner.emilkowal.ski/)
|
|
43
|
+
给(`<Toaster position="bottom-right" richColors />`,跟按钮一起挂在这一格里)。
|
|
44
|
+
- 写进剪贴板的是 `window.location.href` 解码之后那一份,所以地址里带 `?path=` 这类编码参数时,
|
|
45
|
+
复制出来的是看得懂的那一份。
|
|
46
|
+
- `styles/copy-button.css` 由入口的 `getClientModules()` 交给站点的样式表管线;
|
|
47
|
+
提示条自己的样式由 `sonner` 在浏览器里注入,不经过这一格。
|
|
48
|
+
- 主题层与 `components/` 是未编译的 `.tsx`,随包发布、由用它们的站点编译;
|
|
49
|
+
本包只编译 `src/index.mts` 那一份入口(`pnpm build` → `dist/`)。
|
|
50
|
+
|
|
51
|
+
## 已知限制
|
|
52
|
+
|
|
53
|
+
- 只在页脚有 `Footer/Copyright` 那一格时出现;页脚整块关掉(`themeConfig.footer: null`)的站点看不到它。
|
|
54
|
+
- `navigator.clipboard` 只在安全上下文(https 或 localhost)里存在。用 http 打开的站点上,
|
|
55
|
+
点击只会给一条「复制失败」的提示,按钮本身照常可点。
|
|
56
|
+
- 静态渲染出来的 HTML 里没有提示条:这一层是浏览器里的行为。
|
|
57
|
+
- 提示文案(成功与失败各一句)写死在 `components/copy-button.tsx`,不跟站点语言切换。
|
|
58
|
+
|
|
59
|
+
## 相关文档
|
|
60
|
+
|
|
61
|
+
- [`plugin-prev-layer-resolver` 的 README](../../plugins/plugin-prev-layer-resolver/README.md):`@theme-prev/*` 的语义与层栈。
|
|
62
|
+
- [`sonner` 文档](https://sonner.emilkowal.ski/):提示条的选项与样式。
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { toast } from 'sonner';
|
|
2
|
+
|
|
3
|
+
const ACCESSIBLE_NAME = '复制当前页地址';
|
|
4
|
+
const SUCCESS_MESSAGE = '已复制当前页地址';
|
|
5
|
+
const FAILURE_MESSAGE = '复制失败';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
把此刻地址栏里那份地址写进剪贴板,成败都给一条提示。
|
|
9
|
+
|
|
10
|
+
`navigator.clipboard` 只在这个页面是安全上下文(https,或 localhost)时才存在,
|
|
11
|
+
用 http 打开的站点拿到的就是 `undefined` —— 这一格要单独判,不能只靠 `catch`。
|
|
12
|
+
*/
|
|
13
|
+
async function copyPageUrl(): Promise<void> {
|
|
14
|
+
if (!navigator.clipboard) {
|
|
15
|
+
toast.error(FAILURE_MESSAGE);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
await navigator.clipboard.writeText(
|
|
20
|
+
decodeURIComponent(window.location.href),
|
|
21
|
+
);
|
|
22
|
+
toast.success(SUCCESS_MESSAGE);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
toast.error(FAILURE_MESSAGE);
|
|
25
|
+
console.error(error);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
页脚那一格右侧的按钮:图标是 `styles/copy-button.css` 里 `.gg-copy` 画出来的那一份,
|
|
31
|
+
按钮自己不带文字,读屏拿到的名字是 `aria-label` 那一句。用 `<button>` 而不是一个可点的
|
|
32
|
+
`<span>`,键盘才按得动;图标那一格是纯装饰,标 `aria-hidden` 让读屏跳过它。
|
|
33
|
+
*/
|
|
34
|
+
export function CopyButton() {
|
|
35
|
+
return (
|
|
36
|
+
<button
|
|
37
|
+
type="button"
|
|
38
|
+
className="footer-copy-btn"
|
|
39
|
+
aria-label={ACCESSIBLE_NAME}
|
|
40
|
+
onClick={copyPageUrl}
|
|
41
|
+
>
|
|
42
|
+
<span className="gg-copy" aria-hidden="true" />
|
|
43
|
+
</button>
|
|
44
|
+
);
|
|
45
|
+
}
|
package/dist/index.d.mts
ADDED
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { dirname, resolve } from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
// 包出口是 `dist/`,所以这里的路径都相对**产物**目录算,不是相对 `src/`。
|
|
4
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
export default function theme() {
|
|
6
|
+
return {
|
|
7
|
+
name: '@show-docs/docusaurus-theme-copy-button',
|
|
8
|
+
// 主题层与 `components/` 是**源码**,站在包根上,不经 `tsc`:用本包的 Docusaurus
|
|
9
|
+
// 项目会用自己的 babel 编译它们。这里只编译 `src/` 那一份入口。
|
|
10
|
+
getThemePath() {
|
|
11
|
+
return resolve(thisDir, '../theme');
|
|
12
|
+
},
|
|
13
|
+
// 层里放的是 `.tsx`,这条给 `docusaurus swizzle` 用
|
|
14
|
+
getTypeScriptThemePath() {
|
|
15
|
+
return resolve(thisDir, '../theme');
|
|
16
|
+
},
|
|
17
|
+
// 样式表不是模块,`tsc` 不会把它搬进 `dist/`:作为 client module 交给站点的样式表管线。
|
|
18
|
+
// 提示条自己的样式由 `sonner` 在浏览器里注入,不经过这一格。
|
|
19
|
+
getClientModules() {
|
|
20
|
+
return [resolve(thisDir, '../styles/copy-button.css')];
|
|
21
|
+
},
|
|
22
|
+
configureWebpack(_config, isServer, utils) {
|
|
23
|
+
return {
|
|
24
|
+
module: {
|
|
25
|
+
rules: [
|
|
26
|
+
{
|
|
27
|
+
// 官方那条 babel 规则排除整个 node_modules、只放行 `docusaurus*.js`,本包从
|
|
28
|
+
// npm 安装后的 `.tsx` 会报 `Module parse failed`。补一条只覆盖本包的规则
|
|
29
|
+
//(理由与代价见 theme-iframe 的 `lib/api-frame-theme.mts` 里同一条)。
|
|
30
|
+
test: /\.tsx?$/,
|
|
31
|
+
include: [resolve(thisDir, '..')],
|
|
32
|
+
use: [utils.getJSLoader({ isServer })],
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,52 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@show-docs/docusaurus-theme-copy-button",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "页脚版权那一格右边加一颗按钮,点一下把当前页地址写进剪贴板",
|
|
5
|
+
"keywords": [],
|
|
6
|
+
"homepage": "https://github.com/show-docs/docusaurus/tree/master/theme/theme-copy-button",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/show-docs/docusaurus.git",
|
|
10
|
+
"directory": "theme/theme-copy-button"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/show-docs/docusaurus/issues"
|
|
14
|
+
},
|
|
15
|
+
"types": "dist/index.d.mts",
|
|
16
|
+
"main": "dist/index.mjs",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"default": "./dist/index.mjs",
|
|
20
|
+
"types": "./dist/index.d.mts"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"components",
|
|
25
|
+
"dist",
|
|
26
|
+
"styles",
|
|
27
|
+
"theme"
|
|
28
|
+
],
|
|
29
|
+
"type": "module",
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"sonner": "^2.0.8"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"@docusaurus/types": "^3.9.2",
|
|
35
|
+
"react": "~18.3.1"
|
|
36
|
+
},
|
|
37
|
+
"peerDependenciesMeta": {
|
|
38
|
+
"@docusaurus/types": {
|
|
39
|
+
"optional": true
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": "^24.0.0"
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public",
|
|
47
|
+
"registry": "https://registry.npmjs.org/"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsc -p tsconfig.build.json"
|
|
51
|
+
}
|
|
6
52
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/* 页脚那一格右侧的复制按钮。图标 `gg-copy` 是一份纯 CSS 画的形状(不请求图标字体,
|
|
2
|
+
也不带图片):颜色取 `currentColor`,所以它跟着页脚文字自己深浅。
|
|
3
|
+
写法是摊平的(`.gg-copy::before` 而不是嵌套),站点的样式表管线不必为它补插件。 */
|
|
4
|
+
|
|
5
|
+
.gg-copy {
|
|
6
|
+
display: inline-block;
|
|
7
|
+
position: relative;
|
|
8
|
+
transform: scale(var(--ggs, 1));
|
|
9
|
+
box-sizing: border-box;
|
|
10
|
+
margin-top: -4px;
|
|
11
|
+
margin-left: -5px;
|
|
12
|
+
border: 2px solid;
|
|
13
|
+
width: 14px;
|
|
14
|
+
height: 18px;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.gg-copy::after,
|
|
18
|
+
.gg-copy::before {
|
|
19
|
+
display: block;
|
|
20
|
+
position: absolute;
|
|
21
|
+
box-sizing: border-box;
|
|
22
|
+
content: '';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.gg-copy::before {
|
|
26
|
+
right: -6px;
|
|
27
|
+
bottom: -6px;
|
|
28
|
+
box-shadow: inset -4px -4px 0 -2px;
|
|
29
|
+
background:
|
|
30
|
+
linear-gradient(to left, currentColor 5px, transparent 0) no-repeat right
|
|
31
|
+
top/5px 2px,
|
|
32
|
+
linear-gradient(to left, currentColor 5px, transparent 0) no-repeat left
|
|
33
|
+
bottom/ 2px 5px;
|
|
34
|
+
width: 14px;
|
|
35
|
+
height: 18px;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.gg-copy::after {
|
|
39
|
+
top: 2px;
|
|
40
|
+
left: 2px;
|
|
41
|
+
box-shadow:
|
|
42
|
+
0 4px 0,
|
|
43
|
+
0 8px 0;
|
|
44
|
+
background: currentColor;
|
|
45
|
+
width: 6px;
|
|
46
|
+
height: 2px;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* 这是页脚文案里的一颗按钮:去掉浏览器给 `<button>` 的那圈默认外观,让它只剩图标本身。 */
|
|
50
|
+
.footer-copy-btn {
|
|
51
|
+
float: right;
|
|
52
|
+
cursor: pointer;
|
|
53
|
+
margin-left: 2em;
|
|
54
|
+
border: 0;
|
|
55
|
+
background: none;
|
|
56
|
+
padding: 0;
|
|
57
|
+
color: inherit;
|
|
58
|
+
font: inherit;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.footer-copy-btn:hover {
|
|
62
|
+
opacity: 0.5;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.footer-copy-btn:active .gg-copy {
|
|
66
|
+
transform: scale(0.9);
|
|
67
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import Copyright from '@theme-prev/Footer/Copyright';
|
|
2
|
+
import { Toaster } from 'sonner';
|
|
3
|
+
|
|
4
|
+
import type { ReactNode } from 'react';
|
|
5
|
+
|
|
6
|
+
import { CopyButton } from '../../../components/copy-button.tsx';
|
|
7
|
+
|
|
8
|
+
type Props = Record<string, unknown>;
|
|
9
|
+
|
|
10
|
+
export default function CopyrightWrapper(props: Props): ReactNode {
|
|
11
|
+
// 这一层不改页脚那一格的内容:文案照旧交给下面那一层渲染,只在它前面多摆一颗按钮。
|
|
12
|
+
// 提示条的挂载点整站只需要一个,跟着按钮放在这里(`sonner` 把它渲染在页面末尾)。
|
|
13
|
+
return (
|
|
14
|
+
<>
|
|
15
|
+
<CopyButton />
|
|
16
|
+
<Toaster position="bottom-right" richColors />
|
|
17
|
+
<Copyright {...props} />
|
|
18
|
+
</>
|
|
19
|
+
);
|
|
20
|
+
}
|