valaxy 0.6.0 → 0.6.1

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.
@@ -3,6 +3,7 @@ import type { Post } from 'valaxy'
3
3
  import { onMounted, ref } from 'vue'
4
4
  import { useI18n } from 'vue-i18n'
5
5
  import { useAplayer, useCodePen } from '~/composables'
6
+ import { useCopyCode } from '~/composables/copy-code'
6
7
  import { wrapTable } from '~/utils'
7
8
 
8
9
  const props = defineProps<{
@@ -27,41 +28,47 @@ if (props.frontmatter.aplayer)
27
28
 
28
29
  if (props.frontmatter.codepen)
29
30
  useCodePen()
31
+
32
+ useCopyCode()
30
33
  </script>
31
34
 
32
35
  <template>
33
- <article v-if="$slots.default" :class="frontmatter.markdown !== false && 'markdown-body'">
34
- <slot ref="content" @vnode-updated="updateDom" />
35
-
36
- <div text="center">
37
- <a
38
- v-if="frontmatter.url"
39
- :href="frontmatter.url"
40
- class="post-link-btn shadow hover:shadow-md"
41
- rounded
42
- target="_blank"
43
- m="b-4"
44
- >
45
- {{ t('post.view_link') }}
46
- </a>
47
- </div>
36
+ <Transition appear>
37
+ <article v-if="$slots.default" :class="frontmatter.markdown !== false && 'markdown-body'">
38
+ <slot ref="content" @vnode-updated="updateDom" />
48
39
 
49
- <slot v-if="typeof frontmatter.end !== 'undefined'" name="end">
50
- <div m="y-4" class="end flex justify-center items-center">
51
- <hr class="line inline-flex" w="full" m="!y-2">
52
- <span p="x-4" font="serif bold" class="whitespace-nowrap">
53
- {{ frontmatter.end ? 'Q.E.D.' : 'To Be Continued.' }}
54
- </span>
55
- <hr class="line inline-flex" w="full" m="!y-2">
40
+ <div text="center">
41
+ <a
42
+ v-if="frontmatter.url"
43
+ :href="frontmatter.url"
44
+ class="post-link-btn shadow hover:shadow-md"
45
+ rounded
46
+ target="_blank"
47
+ m="b-4"
48
+ >
49
+ {{ t('post.view_link') }}
50
+ </a>
56
51
  </div>
57
- </slot>
58
- </article>
52
+
53
+ <slot v-if="frontmatter.end !== undefined" name="end">
54
+ <div m="y-4" class="end flex justify-center items-center">
55
+ <hr class="line inline-flex" w="full" m="!y-2">
56
+ <span p="x-4" font="serif bold" class="whitespace-nowrap">
57
+ {{ frontmatter.end ? 'Q.E.D.' : 'To Be Continued.' }}
58
+ </span>
59
+ <hr class="line inline-flex" w="full" m="!y-2">
60
+ </div>
61
+ </slot>
62
+ </article>
63
+ </Transition>
59
64
  </template>
60
65
 
61
66
  <style lang="scss">
62
- .end {
63
- .line {
64
- height: 1px;
67
+ .markdown-body {
68
+ .end {
69
+ .line {
70
+ height: 1px;
71
+ }
65
72
  }
66
73
  }
67
74
  </style>
@@ -0,0 +1,92 @@
1
+ import { nextTick, watch } from 'vue'
2
+ import { isClient } from '@vueuse/core'
3
+ import { useRoute } from 'vue-router'
4
+
5
+ export function useCopyCode() {
6
+ const route = useRoute()
7
+
8
+ if (isClient) {
9
+ watch(
10
+ () => route.path,
11
+ () => {
12
+ nextTick(() => {
13
+ document
14
+ .querySelectorAll<HTMLSpanElement>(
15
+ '.markdown-body div[class*="language-"]>span.copy',
16
+ )
17
+ .forEach(handleElement)
18
+ })
19
+ },
20
+ { immediate: true, flush: 'post' },
21
+ )
22
+ }
23
+ }
24
+
25
+ async function copyToClipboard(text: string) {
26
+ try {
27
+ return navigator.clipboard.writeText(text)
28
+ }
29
+ catch {
30
+ const element = document.createElement('textarea')
31
+ const previouslyFocusedElement = document.activeElement
32
+
33
+ element.value = text
34
+
35
+ // Prevent keyboard from showing on mobile
36
+ element.setAttribute('readonly', '')
37
+
38
+ element.style.contain = 'strict'
39
+ element.style.position = 'absolute'
40
+ element.style.left = '-9999px'
41
+ element.style.fontSize = '12pt' // Prevent zooming on iOS
42
+
43
+ const selection = document.getSelection()
44
+ const originalRange = selection
45
+ ? selection.rangeCount > 0 && selection.getRangeAt(0)
46
+ : null
47
+
48
+ document.body.appendChild(element)
49
+ element.select()
50
+
51
+ // Explicit selection workaround for iOS
52
+ element.selectionStart = 0
53
+ element.selectionEnd = text.length
54
+
55
+ document.execCommand('copy')
56
+ document.body.removeChild(element)
57
+
58
+ if (originalRange) {
59
+ selection!.removeAllRanges() // originalRange can't be truthy when selection is falsy
60
+ selection!.addRange(originalRange)
61
+ }
62
+
63
+ // Get the focus back on the previously focused element, if any
64
+ if (previouslyFocusedElement)
65
+ (previouslyFocusedElement as HTMLElement).focus()
66
+ }
67
+ }
68
+
69
+ function handleElement(el: HTMLElement) {
70
+ el.onclick = () => {
71
+ const parent = el.parentElement
72
+
73
+ if (!parent)
74
+ return
75
+
76
+ const isShell
77
+ = parent.classList.contains('language-sh')
78
+ || parent.classList.contains('language-bash')
79
+
80
+ let { innerText: text = '' } = parent
81
+
82
+ if (isShell)
83
+ text = text.replace(/^ *\$ /gm, '')
84
+
85
+ copyToClipboard(text).then(() => {
86
+ el.classList.add('copied')
87
+ setTimeout(() => {
88
+ el.classList.remove('copied')
89
+ }, 3000)
90
+ })
91
+ }
92
+ }
@@ -1,6 +1,5 @@
1
1
  import type { Ref } from 'vue'
2
- import { computed, onMounted, onUnmounted, onUpdated } from 'vue'
3
- import { useRoute } from 'vue-router'
2
+ import { onMounted, onUnmounted, onUpdated } from 'vue'
4
3
  import type { Header } from '../../types'
5
4
  import { throttleAndDebounce } from '~/utils'
6
5
 
@@ -52,18 +51,6 @@ function mapHeaders(
52
51
  // magic number to avoid repeated retrieval
53
52
  const PAGE_OFFSET = 56
54
53
 
55
- export function useOutline() {
56
- const route = useRoute()
57
-
58
- const hasOutline = computed(() => {
59
- return route.meta?.headers?.length > 0
60
- })
61
-
62
- return {
63
- hasOutline,
64
- }
65
- }
66
-
67
54
  export function useActiveAnchor(
68
55
  container: Ref<HTMLElement>,
69
56
  marker: Ref<HTMLElement>,
@@ -158,15 +158,3 @@ function throttleAndDebounce(fn: () => void, delay: number): () => void {
158
158
  }
159
159
  }
160
160
  }
161
-
162
- export function useOutline() {
163
- const route = useRoute()
164
-
165
- const hasOutline = computed(() => {
166
- return route.meta.headers.length > 0
167
- })
168
-
169
- return {
170
- hasOutline,
171
- }
172
- }
@@ -12,7 +12,7 @@ import { useStorage } from '@vueuse/core'
12
12
 
13
13
  import type { Router } from 'vue-router'
14
14
  import type { PageDataPayload } from '../../types'
15
- import { initConfig, initContext, valaxyConfigSymbol } from '../config'
15
+ import { initConfig, valaxyConfigSymbol } from '../config'
16
16
  import { ensureSuffix } from '@antfu/utils'
17
17
 
18
18
  import type { UserModule } from '~/types'
@@ -45,7 +45,6 @@ function shouldHotReload(payload: PageDataPayload): boolean {
45
45
 
46
46
  export const install: UserModule = ({ app, router }) => {
47
47
  // inject valaxy config before modules
48
- const ctx = initContext()
49
48
  const config = initConfig()
50
49
  app.provide(valaxyConfigSymbol, config)
51
50
 
@@ -62,18 +61,6 @@ export const install: UserModule = ({ app, router }) => {
62
61
  router.isReady().then(() => {
63
62
  handleHMR(router)
64
63
  })
65
-
66
- function pathToUrl(path: string): string {
67
- return `${location.origin}/@fs${ctx.value.userRoot}/pages${path}.md?import`
68
- }
69
-
70
- router.beforeEach(async (to) => {
71
- try {
72
- const { __pageData } = await import(/* @vite-ignore */pathToUrl(to.path))
73
- to.meta = Object.assign(to.meta, __pageData)
74
- }
75
- catch {}
76
- })
77
64
  }
78
65
 
79
66
  function handleHMR(router: Router): void {
@@ -1,8 +1,5 @@
1
1
  @use "~/styles/mixins" as *;
2
2
 
3
- /* https://github.com/antfu/prism-theme-vars */
4
- @use "prism-theme-vars/base.css" as *;
5
-
6
3
  @include mobile {
7
4
  .markdown-body {
8
5
  div[class*="language-"] {
@@ -11,17 +8,40 @@
11
8
  }
12
9
  }
13
10
 
11
+ @media (min-width: 640px) {
12
+ .markdown-body div[class*="language-"] {
13
+ border-radius: 6px;
14
+ margin: 16px 0;
15
+ }
16
+ }
17
+
18
+ @media (max-width: 639px) {
19
+ .markdown-body li div[class*="language-"] {
20
+ border-radius: 6px 0 0 6px;
21
+ }
22
+ }
23
+
14
24
  .markdown-body {
15
25
  div[class*="language-"] {
16
26
  position: relative;
17
- padding: 0;
18
- background-color: var(--smc-code-bg-color);
27
+ background-color: var(--va-code-block-bg);
28
+
29
+ code {
30
+ padding: 0 24px;
31
+ line-height: var(--va-code-line-height);
32
+ font-size: var(--va-code-font-size);
33
+ color: var(--va-code-block-color);
34
+ transition: color 0.5s;
35
+ width: fit-content;
36
+ }
19
37
 
20
38
  pre {
21
39
  position: relative;
22
- padding: 1rem;
23
40
  z-index: 1;
24
- background: none;
41
+ margin: 0;
42
+ padding: 1rem 0;
43
+ background: transparent;
44
+ overflow-x: auto;
25
45
 
26
46
  // expand
27
47
  code {
@@ -31,177 +51,181 @@
31
51
  }
32
52
  }
33
53
 
34
- /* Line highlighting */
35
-
36
- .highlight-lines {
37
- position: absolute;
38
- top: 0;
39
- bottom: 0;
40
- left: 0;
41
- padding: 1rem 0;
42
- width: 100%;
43
- line-height: var(--prism-line-height);
44
- user-select: none;
45
- overflow: hidden;
46
-
47
- .highlighted {
48
- background-color: rgba(0, 0, 0, 0.08);
49
- }
50
- }
51
-
52
- .dark {
54
+ // marker
55
+ .markdown-body {
53
56
  .highlight-lines {
57
+ position: absolute;
58
+ top: 0;
59
+ bottom: 0;
60
+ left: 0;
61
+ padding: 16px 0;
62
+ width: 100%;
63
+ line-height: var(--va-code-line-height);
64
+ font-family: var(--va-font-mono);
65
+ font-size: var(--va-code-font-size);
66
+ user-select: none;
67
+ overflow: hidden;
68
+
54
69
  .highlighted {
55
- background-color: rgba(0, 0, 0, 0.4);
70
+ background-color: var(--va-code-line-highlight-color);
71
+ transition: background-color 0.5s;
56
72
  }
57
73
  }
58
- }
59
74
 
60
- .markdown-body {
61
- code {
62
- color: var(--prism-foreground) !important;
75
+ [class*="language-"] > span.copy {
76
+ position: absolute;
77
+ top: 8px;
78
+ right: 8px;
79
+ z-index: 2;
80
+ display: block;
81
+ justify-content: center;
82
+ align-items: center;
83
+ border-radius: 4px;
84
+ width: 40px;
85
+ height: 40px;
86
+ background-color: var(--va-code-block-bg);
87
+ opacity: 0;
88
+ cursor: pointer;
89
+ background-image: var(--va-icon-copy);
90
+ background-position: 50%;
91
+ background-size: 20px;
92
+ background-repeat: no-repeat;
93
+ transition: opacity 0.25s;
63
94
  }
64
- }
65
-
66
- // prism
67
- html:not(.dark) {
68
- // text
69
- --prism-foreground: #224466;
70
-
71
- --prism-background: #f8f8f8;
72
- --prism-comment: #758575;
73
- --prism-namespace: #444444;
74
- --prism-string: #bc8671;
75
- --prism-punctuation: #80817d;
76
- --prism-literal: #36acaa;
77
- --prism-keyword: #248459;
78
- --prism-function: #0088bb;
79
- --prism-deleted: #9a050f;
80
- --prism-class: #2b91af;
81
- --prism-builtin: #800000;
82
- --prism-property: #ce9178;
83
- --prism-regex: #ad502b;
84
- }
85
-
86
- html.dark {
87
- --prism-foreground: #a6accd;
88
-
89
- --prism-background: #242424;
90
- --prism-namespace: #aaaaaa;
91
- --prism-comment: #758575;
92
- --prism-namespace: #444444;
93
- --prism-string: #c3e88d;
94
- --prism-punctuation: #a6accd;
95
- --prism-literal: #36acaa;
96
- --prism-keyword: #89ddff;
97
- --prism-function: #82aaff;
98
- --prism-deleted: #9a050f;
99
- --prism-class: #4ec9b0;
100
- --prism-builtin: #d16969;
101
- --prism-property: #c792ea;
102
- --prism-regex: #ad502b;
103
- --prism-selector: #c3e88d;
104
- }
105
-
106
- /* Language marker */
107
- // @use 'prism-theme-vars/marker.css' as *; * not div
108
- div[class*="language-"]:before {
109
- position: absolute;
110
- top: 0.6em;
111
- right: 1em;
112
- z-index: 2;
113
- font-size: 0.8rem;
114
- color: #888;
115
- }
116
-
117
- div[class~="language-html"]:before,
118
- div[class~="language-markup"]:before {
119
- content: "html";
120
- }
121
-
122
- div[class~="language-md"]:before,
123
- div[class~="language-markdown"]:before {
124
- content: "md";
125
- }
126
-
127
- div[class~="language-css"]:before {
128
- content: "css";
129
- }
130
-
131
- div[class~="language-sass"]:before {
132
- content: "sass";
133
- }
134
-
135
- div[class~="language-scss"]:before {
136
- content: "scss";
137
- }
138
-
139
- div[class~="language-less"]:before {
140
- content: "less";
141
- }
142
-
143
- div[class~="language-stylus"]:before {
144
- content: "styl";
145
- }
146
-
147
- div[class~="language-js"]:before,
148
- div[class~="language-javascript"]:before {
149
- content: "js";
150
- }
151
-
152
- div[class~="language-ts"]:before,
153
- div[class~="language-typescript"]:before {
154
- content: "ts";
155
- }
156
-
157
- div[class~="language-json"]:before {
158
- content: "json";
159
- }
160
-
161
- div[class~="language-rb"]:before,
162
- div[class~="language-ruby"]:before {
163
- content: "rb";
164
- }
165
-
166
- div[class~="language-py"]:before,
167
- div[class~="language-python"]:before {
168
- content: "py";
169
- }
170
-
171
- div[class~="language-sh"]:before,
172
- div[class~="language-bash"]:before {
173
- content: "sh";
174
- }
175
95
 
176
- div[class~="language-php"]:before {
177
- content: "php";
178
- }
179
-
180
- div[class~="language-go"]:before {
181
- content: "go";
182
- }
96
+ [class*="language-"]:hover > span.copy {
97
+ opacity: 1;
98
+ }
183
99
 
184
- div[class~="language-rust"]:before {
185
- content: "rust";
186
- }
100
+ [class*="language-"] > span.copy:hover {
101
+ background-color: var(--va-code-copy-code-hover-bg);
102
+ }
187
103
 
188
- div[class~="language-java"]:before {
189
- content: "java";
190
- }
104
+ [class*="language-"] > span.copy.copied,
105
+ [class*="language-"] > span.copy:hover.copied {
106
+ border-radius: 0 4px 4px 0;
107
+ background-color: var(--va-code-copy-code-hover-bg);
108
+ background-image: var(--va-icon-copied);
109
+ }
191
110
 
192
- div[class~="language-c"]:before {
193
- content: "c";
194
- }
111
+ [class*="language-"] > span.copy.copied::before,
112
+ [class*="language-"] > span.copy:hover.copied::before {
113
+ position: relative;
114
+ left: -65px;
115
+ display: block;
116
+ border-radius: 4px 0 0 4px;
117
+ padding-top: 8px;
118
+ width: 64px;
119
+ height: 40px;
120
+ text-align: center;
121
+ font-size: 12px;
122
+ font-weight: 500;
123
+ color: var(--va-code-copy-code-active-text);
124
+ background-color: var(--va-code-copy-code-hover-bg);
125
+ white-space: nowrap;
126
+ content: "Copied";
127
+ }
195
128
 
196
- div[class~="language-yml"]:before,
197
- div[class~="language-yaml"]:before {
198
- content: "yaml";
199
- }
129
+ [class*="language-"]:before {
130
+ position: absolute;
131
+ top: 6px;
132
+ right: 12px;
133
+ z-index: 2;
134
+ font-size: 12px;
135
+ font-weight: 500;
136
+ color: var(--va-c-text-dark-3);
137
+ transition: color 0.5s, opacity 0.5s;
138
+ }
200
139
 
201
- div[class~="language-dockerfile"]:before {
202
- content: "dockerfile";
203
- }
140
+ [class*="language-"]:hover:before {
141
+ opacity: 0;
142
+ }
204
143
 
205
- div[class~="language-vue"]:before {
206
- content: "vue";
144
+ [class~="language-c"]:before {
145
+ content: "c";
146
+ }
147
+ [class~="language-css"]:before {
148
+ content: "css";
149
+ }
150
+ [class~="language-go"]:before {
151
+ content: "go";
152
+ }
153
+ [class~="language-html"]:before {
154
+ content: "html";
155
+ }
156
+ [class~="language-java"]:before {
157
+ content: "java";
158
+ }
159
+ [class~="language-javascript"]:before {
160
+ content: "js";
161
+ }
162
+ [class~="language-js"]:before {
163
+ content: "js";
164
+ }
165
+ [class~="language-json"]:before {
166
+ content: "json";
167
+ }
168
+ [class~="language-jsx"]:before {
169
+ content: "jsx";
170
+ }
171
+ [class~="language-less"]:before {
172
+ content: "less";
173
+ }
174
+ [class~="language-markdown"]:before {
175
+ content: "md";
176
+ }
177
+ [class~="language-md"]:before {
178
+ content: "md";
179
+ }
180
+ [class~="language-php"]:before {
181
+ content: "php";
182
+ }
183
+ [class~="language-python"]:before {
184
+ content: "py";
185
+ }
186
+ [class~="language-py"]:before {
187
+ content: "py";
188
+ }
189
+ [class~="language-rb"]:before {
190
+ content: "rb";
191
+ }
192
+ [class~="language-ruby"]:before {
193
+ content: "rb";
194
+ }
195
+ [class~="language-rust"]:before {
196
+ content: "rust";
197
+ }
198
+ [class~="language-sass"]:before {
199
+ content: "sass";
200
+ }
201
+ [class~="language-scss"]:before {
202
+ content: "scss";
203
+ }
204
+ [class~="language-sh"]:before {
205
+ content: "sh";
206
+ }
207
+ [class~="language-bash"]:before {
208
+ content: "sh";
209
+ }
210
+ [class~="language-stylus"]:before {
211
+ content: "styl";
212
+ }
213
+ [class~="language-vue-html"]:before {
214
+ content: "template";
215
+ }
216
+ [class~="language-typescript"]:before {
217
+ content: "ts";
218
+ }
219
+ [class~="language-ts"]:before {
220
+ content: "ts";
221
+ }
222
+ [class~="language-tsx"]:before {
223
+ content: "tsx";
224
+ }
225
+ [class~="language-vue"]:before {
226
+ content: "vue";
227
+ }
228
+ [class~="language-yaml"]:before {
229
+ content: "yaml";
230
+ }
207
231
  }
@@ -1,8 +1,6 @@
1
1
  @use "sass:map";
2
2
 
3
3
  .markdown-body {
4
- --prism-font-family: var(--va-font-mono);
5
-
6
4
  --smc-font-family: var(--va-font-sans);
7
5
 
8
6
  video {