@maple-design/cli-vue 0.0.0-stage → 1.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.
Files changed (228) hide show
  1. package/README.md +305 -2
  2. package/README.zh-CN.md +305 -0
  3. package/bin/cli.mjs +5 -0
  4. package/client.d.ts +1 -0
  5. package/client.js +1 -0
  6. package/lib/client/appType.d.ts +4 -0
  7. package/lib/client/appType.js +7 -0
  8. package/lib/client/index.d.ts +29 -0
  9. package/lib/client/index.js +170 -0
  10. package/lib/client/language.d.ts +11 -0
  11. package/lib/client/language.js +52 -0
  12. package/lib/client/theme-state.d.ts +3 -0
  13. package/lib/client/theme-state.js +6 -0
  14. package/lib/node/bin.d.ts +2 -0
  15. package/lib/node/bin.js +154 -0
  16. package/lib/node/commands/build.d.ts +1 -0
  17. package/lib/node/commands/build.js +15 -0
  18. package/lib/node/commands/checklist.d.ts +6 -0
  19. package/lib/node/commands/checklist.js +59 -0
  20. package/lib/node/commands/compile.d.ts +3 -0
  21. package/lib/node/commands/compile.js +41 -0
  22. package/lib/node/commands/create.d.ts +8 -0
  23. package/lib/node/commands/create.js +91 -0
  24. package/lib/node/commands/dev.d.ts +6 -0
  25. package/lib/node/commands/dev.js +58 -0
  26. package/lib/node/commands/extension.d.ts +3 -0
  27. package/lib/node/commands/extension.js +5 -0
  28. package/lib/node/commands/gen.d.ts +24 -0
  29. package/lib/node/commands/gen.js +91 -0
  30. package/lib/node/commands/icons.d.ts +4 -0
  31. package/lib/node/commands/icons.js +15 -0
  32. package/lib/node/commands/preview.d.ts +4 -0
  33. package/lib/node/commands/preview.js +18 -0
  34. package/lib/node/commands/styleVars.d.ts +1 -0
  35. package/lib/node/commands/styleVars.js +6 -0
  36. package/lib/node/commands/test.d.ts +7 -0
  37. package/lib/node/commands/test.js +16 -0
  38. package/lib/node/commands/vite.d.ts +2 -0
  39. package/lib/node/commands/vite.js +14 -0
  40. package/lib/node/compiler/compileModule.d.ts +4 -0
  41. package/lib/node/compiler/compileModule.js +78 -0
  42. package/lib/node/compiler/compileSFC.d.ts +6 -0
  43. package/lib/node/compiler/compileSFC.js +119 -0
  44. package/lib/node/compiler/compileScript.d.ts +27 -0
  45. package/lib/node/compiler/compileScript.js +221 -0
  46. package/lib/node/compiler/compileSiteEntry.d.ts +19 -0
  47. package/lib/node/compiler/compileSiteEntry.js +164 -0
  48. package/lib/node/compiler/compileStyle.d.ts +14 -0
  49. package/lib/node/compiler/compileStyle.js +55 -0
  50. package/lib/node/compiler/compileStyleVars.d.ts +3 -0
  51. package/lib/node/compiler/compileStyleVars.js +46 -0
  52. package/lib/node/compiler/compileTemplateHighlight.d.ts +18 -0
  53. package/lib/node/compiler/compileTemplateHighlight.js +133 -0
  54. package/lib/node/compiler/compileTypes.d.ts +2 -0
  55. package/lib/node/compiler/compileTypes.js +58 -0
  56. package/lib/node/compiler/generateLibraryEntry.d.ts +3 -0
  57. package/lib/node/compiler/generateLibraryEntry.js +152 -0
  58. package/lib/node/compiler/siteRoutes.d.ts +4 -0
  59. package/lib/node/compiler/siteRoutes.js +9 -0
  60. package/lib/node/config/maple.config.d.ts +215 -0
  61. package/lib/node/config/maple.config.js +32 -0
  62. package/lib/node/config/maple.default.config.d.ts +2 -0
  63. package/lib/node/config/maple.default.config.js +386 -0
  64. package/lib/node/config/site-resolver.d.ts +25 -0
  65. package/lib/node/config/site-resolver.js +138 -0
  66. package/lib/node/config/vite.config.d.ts +28 -0
  67. package/lib/node/config/vite.config.js +209 -0
  68. package/lib/node/config/vitest.config.d.ts +2 -0
  69. package/lib/node/config/vitest.config.js +54 -0
  70. package/lib/node/config/vitest.setup.d.ts +1 -0
  71. package/lib/node/config/vitest.setup.js +8 -0
  72. package/lib/node/index.d.ts +13 -0
  73. package/lib/node/index.js +13 -0
  74. package/lib/node/release/index.d.ts +20 -0
  75. package/lib/node/release/index.js +147 -0
  76. package/lib/node/shared/constant.d.ts +47 -0
  77. package/lib/node/shared/constant.js +53 -0
  78. package/lib/node/shared/fsUtils.d.ts +18 -0
  79. package/lib/node/shared/fsUtils.js +57 -0
  80. package/lib/node/shared/logger.d.ts +8 -0
  81. package/lib/node/shared/logger.js +18 -0
  82. package/lib/node/shared/serial-task.d.ts +2 -0
  83. package/lib/node/shared/serial-task.js +9 -0
  84. package/package.json +111 -3
  85. package/site/documentation-sections.spec.ts +45 -0
  86. package/site/documentation-sections.ts +48 -0
  87. package/site/index.html +89 -0
  88. package/site/mobile/App.vue +437 -0
  89. package/site/mobile/AppHome.vue +154 -0
  90. package/site/mobile/main.ts +99 -0
  91. package/site/mobile/route-sync.spec.ts +34 -0
  92. package/site/mobile/route-sync.ts +10 -0
  93. package/site/mobile.html +70 -0
  94. package/site/module.d.ts +5 -0
  95. package/site/pc/App.vue +44 -0
  96. package/site/pc/Layout.vue +641 -0
  97. package/site/pc/components/AppAd.vue +117 -0
  98. package/site/pc/components/AppHeader.vue +459 -0
  99. package/site/pc/components/AppMobile.spec.ts +204 -0
  100. package/site/pc/components/AppMobile.vue +201 -0
  101. package/site/pc/components/AppOutline.spec.ts +89 -0
  102. package/site/pc/components/AppOutline.vue +161 -0
  103. package/site/pc/components/AppSidebar.vue +144 -0
  104. package/site/pc/components/code-example/CodeExample.spec.ts +36 -0
  105. package/site/pc/components/code-example/CodeExample.vue +164 -0
  106. package/site/pc/components/code-example/codeExample.less +88 -0
  107. package/site/pc/components/code-example/index.ts +10 -0
  108. package/site/pc/components/demo-preview/DemoPreview.vue +92 -0
  109. package/site/pc/components/demo-preview/index.ts +8 -0
  110. package/site/pc/components/mobile-preview-url.ts +17 -0
  111. package/site/pc/components/preview-section.spec.ts +24 -0
  112. package/site/pc/components/preview-section.ts +23 -0
  113. package/site/pc/main.ts +108 -0
  114. package/site/pc/pages/index/index.vue +421 -0
  115. package/site/pc/pages/index/locale/en-US.ts +1 -0
  116. package/site/pc/pages/index/locale/zh-CN.ts +1 -0
  117. package/site/pc/responsive-layout.spec.ts +40 -0
  118. package/site/pc/route-state.spec.ts +34 -0
  119. package/site/pc/route-state.ts +25 -0
  120. package/site/pc/scroll-hash.spec.ts +49 -0
  121. package/site/pc/scroll-hash.ts +25 -0
  122. package/site/route-recovery.spec.ts +51 -0
  123. package/site/route-recovery.ts +38 -0
  124. package/site/tsconfig.json +16 -0
  125. package/site/types.ts +6 -0
  126. package/site/utils.ts +45 -0
  127. package/template/create/[ComponentName].tsx.ejs +16 -0
  128. package/template/create/[ComponentName].vue.ejs +21 -0
  129. package/template/create/[componentName].d.ts.ejs +13 -0
  130. package/template/create/__tests__/index.spec.js.ejs +10 -0
  131. package/template/create/docs/en-US.md +0 -0
  132. package/template/create/docs/zh-CN.md +0 -0
  133. package/template/create/example/index.vue.ejs +17 -0
  134. package/template/create/example/locale/_index.ts +17 -0
  135. package/template/create/example/locale/en-US.ts +3 -0
  136. package/template/create/example/locale/index.ts +24 -0
  137. package/template/create/example/locale/zh-CN.ts +3 -0
  138. package/template/create/index.ts.ejs +12 -0
  139. package/template/create/props.ts +1 -0
  140. package/template/create/style/[componentName].less.ejs +5 -0
  141. package/template/create/style/index.less.ejs +1 -0
  142. package/template/create/style/index.ts.ejs +1 -0
  143. package/template/generators/base/.browserslistrc +2 -0
  144. package/template/generators/base/.prettierignore +14 -0
  145. package/template/generators/base/README.md +90 -0
  146. package/template/generators/base/_gitignore +19 -0
  147. package/template/generators/base/eslint.config.js +15 -0
  148. package/template/generators/base/package.json +97 -0
  149. package/template/generators/base/prettier.config.js +3 -0
  150. package/template/generators/base/public/logo.svg +30 -0
  151. package/template/generators/base/shims/shims-md.d.ts +4 -0
  152. package/template/generators/base/shims/shims-vue.d.ts +6 -0
  153. package/template/generators/base/tsconfig.declaration.json +21 -0
  154. package/template/generators/base/tsconfig.json +28 -0
  155. package/template/generators/base/ui-build.config.mjs +16 -0
  156. package/template/generators/config/default/base/docs/home.zh-CN.md +3 -0
  157. package/template/generators/config/default/base/maple.config.mjs +49 -0
  158. package/template/generators/config/default/sfc/src/button/Button.vue +34 -0
  159. package/template/generators/config/default/sfc/src/button/__tests__/index.spec.js +8 -0
  160. package/template/generators/config/default/sfc/src/button/docs/zh-CN.md +39 -0
  161. package/template/generators/config/default/sfc/src/button/example/BasicUse.vue +7 -0
  162. package/template/generators/config/default/sfc/src/button/example/ModifyColor.vue +7 -0
  163. package/template/generators/config/default/sfc/src/button/example/index.vue +12 -0
  164. package/template/generators/config/default/sfc/src/button/index.ts +8 -0
  165. package/template/generators/config/default/sfc/src/button/style/button.less +14 -0
  166. package/template/generators/config/default/sfc/src/button/style/index.less +1 -0
  167. package/template/generators/config/default/sfc/src/button/style/index.ts +1 -0
  168. package/template/generators/config/default/sfc/src/utils/components.ts +20 -0
  169. package/template/generators/config/default/tsx/src/button/Button.tsx +30 -0
  170. package/template/generators/config/default/tsx/src/button/__tests__/index.spec.js +8 -0
  171. package/template/generators/config/default/tsx/src/button/docs/zh-CN.md +39 -0
  172. package/template/generators/config/default/tsx/src/button/example/BasicUse.vue +7 -0
  173. package/template/generators/config/default/tsx/src/button/example/ModifyColor.vue +7 -0
  174. package/template/generators/config/default/tsx/src/button/example/index.vue +12 -0
  175. package/template/generators/config/default/tsx/src/button/index.ts +8 -0
  176. package/template/generators/config/default/tsx/src/button/style/button.less +14 -0
  177. package/template/generators/config/default/tsx/src/button/style/index.less +1 -0
  178. package/template/generators/config/default/tsx/src/button/style/index.ts +1 -0
  179. package/template/generators/config/default/tsx/src/utils/components.ts +20 -0
  180. package/template/generators/config/i18n/base/docs/home.en-US.md +3 -0
  181. package/template/generators/config/i18n/base/docs/home.zh-CN.md +3 -0
  182. package/template/generators/config/i18n/base/maple.config.mjs +51 -0
  183. package/template/generators/config/i18n/sfc/src/button/Button.vue +38 -0
  184. package/template/generators/config/i18n/sfc/src/button/__tests__/index.spec.js +8 -0
  185. package/template/generators/config/i18n/sfc/src/button/docs/en-US.md +39 -0
  186. package/template/generators/config/i18n/sfc/src/button/docs/zh-CN.md +39 -0
  187. package/template/generators/config/i18n/sfc/src/button/example/BasicUse.vue +11 -0
  188. package/template/generators/config/i18n/sfc/src/button/example/ModifyColor.vue +11 -0
  189. package/template/generators/config/i18n/sfc/src/button/example/index.vue +15 -0
  190. package/template/generators/config/i18n/sfc/src/button/example/locale/en-US.ts +5 -0
  191. package/template/generators/config/i18n/sfc/src/button/example/locale/index.ts +23 -0
  192. package/template/generators/config/i18n/sfc/src/button/example/locale/zh-CN.ts +5 -0
  193. package/template/generators/config/i18n/sfc/src/button/index.ts +8 -0
  194. package/template/generators/config/i18n/sfc/src/button/style/button.less +14 -0
  195. package/template/generators/config/i18n/sfc/src/button/style/index.less +1 -0
  196. package/template/generators/config/i18n/sfc/src/button/style/index.ts +1 -0
  197. package/template/generators/config/i18n/sfc/src/locale/__tests__/index.spec.js +52 -0
  198. package/template/generators/config/i18n/sfc/src/locale/docs/en-US.md +26 -0
  199. package/template/generators/config/i18n/sfc/src/locale/docs/zh-CN.md +25 -0
  200. package/template/generators/config/i18n/sfc/src/locale/en-US.d.ts +5 -0
  201. package/template/generators/config/i18n/sfc/src/locale/en-US.ts +7 -0
  202. package/template/generators/config/i18n/sfc/src/locale/index.ts +82 -0
  203. package/template/generators/config/i18n/sfc/src/locale/zh-CN.d.ts +5 -0
  204. package/template/generators/config/i18n/sfc/src/locale/zh-CN.ts +7 -0
  205. package/template/generators/config/i18n/sfc/src/utils/components.ts +20 -0
  206. package/template/generators/config/i18n/tsx/src/button/Button.tsx +35 -0
  207. package/template/generators/config/i18n/tsx/src/button/__tests__/index.spec.js +8 -0
  208. package/template/generators/config/i18n/tsx/src/button/docs/en-US.md +39 -0
  209. package/template/generators/config/i18n/tsx/src/button/docs/zh-CN.md +39 -0
  210. package/template/generators/config/i18n/tsx/src/button/example/BasicUse.vue +11 -0
  211. package/template/generators/config/i18n/tsx/src/button/example/ModifyColor.vue +11 -0
  212. package/template/generators/config/i18n/tsx/src/button/example/index.vue +15 -0
  213. package/template/generators/config/i18n/tsx/src/button/example/locale/en-US.ts +5 -0
  214. package/template/generators/config/i18n/tsx/src/button/example/locale/index.ts +23 -0
  215. package/template/generators/config/i18n/tsx/src/button/example/locale/zh-CN.ts +5 -0
  216. package/template/generators/config/i18n/tsx/src/button/index.ts +8 -0
  217. package/template/generators/config/i18n/tsx/src/button/style/button.less +14 -0
  218. package/template/generators/config/i18n/tsx/src/button/style/index.less +1 -0
  219. package/template/generators/config/i18n/tsx/src/button/style/index.ts +1 -0
  220. package/template/generators/config/i18n/tsx/src/locale/__tests__/index.spec.js +52 -0
  221. package/template/generators/config/i18n/tsx/src/locale/docs/en-US.md +26 -0
  222. package/template/generators/config/i18n/tsx/src/locale/docs/zh-CN.md +25 -0
  223. package/template/generators/config/i18n/tsx/src/locale/en-US.d.ts +5 -0
  224. package/template/generators/config/i18n/tsx/src/locale/en-US.ts +7 -0
  225. package/template/generators/config/i18n/tsx/src/locale/index.ts +82 -0
  226. package/template/generators/config/i18n/tsx/src/locale/zh-CN.d.ts +5 -0
  227. package/template/generators/config/i18n/tsx/src/locale/zh-CN.ts +7 -0
  228. package/template/generators/config/i18n/tsx/src/utils/components.ts +20 -0
@@ -0,0 +1,144 @@
1
+ <template>
2
+ <div class="maple-site-sidebar mp-elevation--3">
3
+ <a
4
+ v-for="(item, index) in menu"
5
+ :key="index"
6
+ class="maple-site-sidebar__link"
7
+ :href="item.type !== MenuTypes.TITLE ? `#/${language}/${item.doc}` : undefined"
8
+ @click.prevent="changeRoute(item)"
9
+ >
10
+ <mp-cell
11
+ :id="item.doc"
12
+ v-ripple="{
13
+ disabled: item.type === menuTypes.TITLE,
14
+ }"
15
+ class="maple-site-sidebar__item"
16
+ :class="{
17
+ 'maple-site-sidebar__item--active': item.doc === menuName,
18
+ 'maple-site-sidebar__cell': item.type !== menuTypes.TITLE,
19
+ 'maple-site-sidebar__title': item.type === menuTypes.TITLE,
20
+ 'maple-site-sidebar__cell--child': item.parent,
21
+ }"
22
+ >
23
+ <span class="maple-site-sidebar__indicator"></span>
24
+ <span v-if="item.type === menuTypes.TITLE" class="maple-site-sidebar__item--title">{{
25
+ item.text[language]
26
+ }}</span>
27
+ <span v-else>{{ item.text[language] }}</span>
28
+ </mp-cell>
29
+ </a>
30
+ </div>
31
+ </template>
32
+
33
+ <script setup lang="ts">
34
+ import { reactive } from 'vue'
35
+ import { MenuTypes, type Menu } from '../../utils'
36
+
37
+ const props = defineProps<{ menu: Menu[]; menuName: string; language: string }>()
38
+ const emit = defineEmits(['change'])
39
+ const menuTypes = reactive(MenuTypes)
40
+
41
+ const changeRoute = (item: Menu) => {
42
+ if (item.type === MenuTypes.TITLE || props.menuName === item.doc) {
43
+ return
44
+ }
45
+
46
+ emit('change', item)
47
+ }
48
+ </script>
49
+
50
+ <style lang="less">
51
+ @keyframes indicator-fade-in {
52
+ from {
53
+ transform: scaleY(0);
54
+ opacity: 0.3;
55
+ }
56
+
57
+ to {
58
+ transform: scaleY(1);
59
+ opacity: 1;
60
+ }
61
+ }
62
+
63
+ .maple-site-sidebar {
64
+ padding: 0 0 15px;
65
+ position: sticky;
66
+ width: 246px;
67
+ height: calc(100vh - 60px - 15px);
68
+ top: 60px;
69
+ z-index: 6;
70
+ overflow-y: scroll;
71
+ box-shadow: 0 8px 12px var(--site-config-color-shadow);
72
+ background: var(--site-config-color-bar);
73
+ scrollbar-width: none;
74
+
75
+ &::-webkit-scrollbar {
76
+ display: none;
77
+ }
78
+
79
+ &__link {
80
+ display: block;
81
+ color: initial;
82
+ text-decoration: none;
83
+ &:focus-visible {
84
+ outline: 2px solid var(--site-config-color-link);
85
+ outline-offset: -2px;
86
+ }
87
+ }
88
+
89
+ &__item {
90
+ margin: 0 !important;
91
+ user-select: none !important;
92
+ padding: 10px 28px !important;
93
+
94
+ &--title {
95
+ font-size: 16px !important;
96
+ font-weight: 600 !important;
97
+ color: var(--site-config-color-text) !important;
98
+ line-height: 28px !important;
99
+ padding: 8px 0 8px !important;
100
+ }
101
+
102
+ &--active {
103
+ position: relative;
104
+ background: var(--site-config-color-side-bar-active-background);
105
+
106
+ span {
107
+ color: var(--site-config-color-side-bar);
108
+ }
109
+
110
+ &::before {
111
+ display: block;
112
+ content: '';
113
+ background: var(--site-config-color-side-bar);
114
+ width: 4px;
115
+ height: 40px;
116
+ position: absolute;
117
+ left: 0;
118
+ animation: indicator-fade-in 0.25s;
119
+ }
120
+ }
121
+ }
122
+
123
+ &__cell {
124
+ cursor: pointer !important;
125
+ font-size: 14px !important;
126
+ color: var(--site-config-color-text) !important;
127
+ transition: color 0.2s !important;
128
+
129
+ &:hover {
130
+ color: var(--site-config-color-side-bar) !important;
131
+ }
132
+
133
+ &--child {
134
+ padding-left: 44px !important;
135
+ font-size: 13px !important;
136
+ }
137
+ }
138
+
139
+ &__title {
140
+ margin-top: 10px !important;
141
+ cursor: default;
142
+ }
143
+ }
144
+ </style>
@@ -0,0 +1,36 @@
1
+ import { flushPromises, mount } from '@vue/test-utils'
2
+ import { afterEach, describe, expect, it, vi } from 'vitest'
3
+ import CodeExample from './CodeExample.vue'
4
+
5
+ vi.mock('@config', () => ({
6
+ default: { pc: { fold: { defaultFold: true, foldHeight: 0 }, clipboard: { 'zh-CN': '已复制' } } },
7
+ }))
8
+ vi.mock('@maple-design/cli-vue/client', () => ({
9
+ getBrowserTheme: () => 'lightTheme',
10
+ getPCLocationInfo: () => ({ language: 'zh-CN' }),
11
+ }))
12
+ vi.mock('@maple-design/ui-vue', () => ({ Context: {}, Message: { success: vi.fn() } }))
13
+ vi.mock('@maple-design/icons-vue', () => ({ IconCodeJson: 'span', IconCopy: 'span', IconXml: 'span' }))
14
+ vi.mock('@maple-design/shared-vue', () => ({ doubleRaf: () => Promise.resolve() }))
15
+ vi.mock('clipboard', () => ({
16
+ default: class {
17
+ on() {}
18
+ },
19
+ }))
20
+
21
+ afterEach(() => vi.restoreAllMocks())
22
+
23
+ describe('Markdown code visibility', () => {
24
+ it('shows prose code with a stale folded site config and still allows manual folding', async () => {
25
+ vi.spyOn(HTMLElement.prototype, 'offsetHeight', 'get').mockReturnValue(160)
26
+ const wrapper = mount(CodeExample, { slots: { default: '<pre>npm install @maple-design/ui-vue</pre>' } })
27
+ const code = wrapper.get('.mp-site-code-example__code')
28
+ expect(code.attributes('aria-hidden')).toBe('false')
29
+ expect(code.attributes('style') ?? '').not.toContain('height: 0px')
30
+ await wrapper.get('button[aria-label="Hide Source"]').trigger('click')
31
+ await flushPromises()
32
+ expect(code.attributes('aria-hidden')).toBe('true')
33
+ expect(code.attributes('style')).toContain('height: 0px')
34
+ wrapper.unmount()
35
+ })
36
+ })
@@ -0,0 +1,164 @@
1
+ <template>
2
+ <div class="mp-site-code-example">
3
+ <div class="mp-site-code-example__toolbar">
4
+ <div class="mp-site-code-example__toolbar-group">
5
+ <button
6
+ v-if="playground"
7
+ type="button"
8
+ class="mp-site-code-example__toolbar-button"
9
+ aria-label="Edit In Playground"
10
+ data-tooltip="Edit In Playground"
11
+ @click="toPlayground"
12
+ >
13
+ <MIconCodeJson class="mp-site-code-example__toolbar-icon" />
14
+ </button>
15
+ </div>
16
+
17
+ <div class="mp-site-code-example__toolbar-group">
18
+ <button
19
+ v-if="clipboard"
20
+ :id="`clip-trigger-${cid}`"
21
+ :data-clipboard-target="`#clip-target-${cid}`"
22
+ type="button"
23
+ class="mp-site-code-example__toolbar-button"
24
+ aria-label="Copy Example Code"
25
+ data-tooltip="Copy Example Code"
26
+ >
27
+ <MIconCopy class="mp-site-code-example__toolbar-icon" />
28
+ </button>
29
+
30
+ <button
31
+ v-if="fold && !disabledFold"
32
+ type="button"
33
+ class="mp-site-code-example__toolbar-button"
34
+ :aria-controls="`clip-target-${cid}`"
35
+ :aria-expanded="height < 0"
36
+ :aria-label="height >= 0 ? 'Show Source' : 'Hide Source'"
37
+ :data-tooltip="height >= 0 ? 'Show Source' : 'Hide Source'"
38
+ @click="toggle"
39
+ >
40
+ <MIconXml class="mp-site-code-example__toolbar-icon" />
41
+ </button>
42
+ </div>
43
+ </div>
44
+ <div
45
+ :id="`clip-target-${cid}`"
46
+ ref="code"
47
+ class="mp-site-code-example__code"
48
+ :class="[disabledFold ? 'mp-site-code-example--scroller' : null]"
49
+ :aria-hidden="height >= 0"
50
+ :style="{
51
+ height: height >= 0 ? `${height}px` : undefined,
52
+ }"
53
+ >
54
+ <slot />
55
+ </div>
56
+ </div>
57
+ </template>
58
+
59
+ <script lang="ts">
60
+ import { defineComponent, nextTick, onMounted, ref, type Ref } from 'vue'
61
+ import config from '@config'
62
+ import { getBrowserTheme, getPCLocationInfo } from '@maple-design/cli-vue/client'
63
+ import { IconCodeJson as MIconCodeJson, IconCopy as MIconCopy, IconXml as MIconXml } from '@maple-design/icons-vue'
64
+ import { doubleRaf } from '@maple-design/shared-vue'
65
+ import { Context, Message } from '@maple-design/ui-vue'
66
+ import Clipboard from 'clipboard'
67
+ import { type SiteContext } from '../../../types'
68
+ import { utoa } from '../../../utils'
69
+
70
+ const context = Context as SiteContext
71
+
72
+ let clipId = 0
73
+ const offset = 10
74
+
75
+ export default defineComponent({
76
+ components: { MIconXml, MIconCopy, MIconCodeJson },
77
+ name: 'MpSiteCodeExample',
78
+ props: {
79
+ // Markdown is documentation content. Keep it readable even when a running
80
+ // site still has an older global fold configuration.
81
+ defaultExpanded: {
82
+ type: Boolean,
83
+ default: true,
84
+ },
85
+ playgroundIgnore: {
86
+ type: Boolean,
87
+ default: false,
88
+ },
89
+ },
90
+ setup(props) {
91
+ const code: Ref<HTMLElement | null> = ref(null)
92
+ const cid: Ref<number> = ref(clipId++)
93
+ const fold: Ref = ref(config?.pc?.fold)
94
+ const disabledFold: Ref<boolean> = ref(false)
95
+ const clipboard: Ref = ref(config?.pc?.clipboard || {})
96
+ const height: Ref<number> = ref(-1)
97
+ const playground: Ref<string | undefined> = ref(
98
+ !props.playgroundIgnore ? config?.pc?.header?.playground : undefined,
99
+ )
100
+ let timer: any = null
101
+
102
+ const toggle = async () => {
103
+ const foldHeight = fold.value?.foldHeight ?? 0
104
+
105
+ if (height.value === foldHeight) {
106
+ height.value = -1
107
+ await nextTick()
108
+ const { offsetHeight } = code.value as HTMLElement
109
+ height.value = foldHeight
110
+ await doubleRaf()
111
+ height.value = offsetHeight
112
+
113
+ timer = setTimeout(() => {
114
+ height.value = -1
115
+ }, 250)
116
+ } else {
117
+ clearTimeout(timer)
118
+ const { offsetHeight } = code.value as HTMLElement
119
+ height.value = offsetHeight
120
+ await doubleRaf()
121
+ height.value = foldHeight
122
+ }
123
+ }
124
+
125
+ const toPlayground = () => {
126
+ const codeText = code.value?.innerText ?? ''
127
+ const file = { 'App.vue': codeText }
128
+ const initialTheme = getBrowserTheme('demo')
129
+
130
+ context.showPlayground = true
131
+ context.playgroundURL = `${playground.value}/?initialTheme=${initialTheme}#${utoa(JSON.stringify(file))}`
132
+ }
133
+
134
+ onMounted(() => {
135
+ const trigger = new Clipboard(`#clip-trigger-${cid.value}`)
136
+
137
+ trigger.on('success', () => {
138
+ Message.success(clipboard.value[getPCLocationInfo().language])
139
+ })
140
+
141
+ const { offsetHeight } = code.value as HTMLElement
142
+ const foldHeight = fold.value?.foldHeight ?? 0
143
+ disabledFold.value = offsetHeight - foldHeight < offset
144
+ height.value = !props.defaultExpanded && fold.value?.defaultFold && !disabledFold.value ? foldHeight : -1
145
+ })
146
+
147
+ return {
148
+ code,
149
+ height,
150
+ cid,
151
+ fold,
152
+ disabledFold,
153
+ clipboard,
154
+ playground,
155
+ toggle,
156
+ toPlayground,
157
+ }
158
+ },
159
+ })
160
+ </script>
161
+
162
+ <style lang="less">
163
+ @import './codeExample';
164
+ </style>
@@ -0,0 +1,88 @@
1
+ .mp-site-code-example {
2
+ margin-top: 16px;
3
+ margin-bottom: 4px;
4
+ border: thin solid var(--site-config-color-hl-border);
5
+ border-radius: 4px;
6
+ overflow: hidden;
7
+ transition: border 0.25s;
8
+
9
+ &__toolbar {
10
+ display: flex;
11
+ align-items: center;
12
+ justify-content: space-between;
13
+ min-height: 40px;
14
+ padding: 0 5px;
15
+ }
16
+
17
+ &__toolbar-group {
18
+ display: flex;
19
+ align-items: center;
20
+ }
21
+
22
+ &__toolbar-button {
23
+ position: relative;
24
+ display: inline-flex;
25
+ align-items: center;
26
+ justify-content: center;
27
+ width: 32px;
28
+ height: 32px;
29
+ padding: 7px;
30
+ color: var(--site-config-color-hl-code);
31
+ background: transparent;
32
+ border: 0;
33
+ border-radius: 50%;
34
+ outline: none;
35
+ cursor: pointer;
36
+ transition: background-color 0.2s;
37
+
38
+ &:hover,
39
+ &:focus-visible {
40
+ background: color-mix(in srgb, currentColor 10%, transparent);
41
+ }
42
+
43
+ &::after {
44
+ position: absolute;
45
+ right: 50%;
46
+ bottom: calc(100% + 8px);
47
+ z-index: 2;
48
+ padding: 5px 8px;
49
+ color: #fff;
50
+ font-size: 12px;
51
+ line-height: 18px;
52
+ white-space: nowrap;
53
+ content: attr(data-tooltip);
54
+ background: rgba(0, 0, 0, 0.75);
55
+ border-radius: 4px;
56
+ opacity: 0;
57
+ pointer-events: none;
58
+ transform: translateX(50%) translateY(4px);
59
+ transition:
60
+ opacity 0.15s,
61
+ transform 0.15s;
62
+ }
63
+
64
+ &:hover::after,
65
+ &:focus-visible::after {
66
+ opacity: 1;
67
+ transform: translateX(50%) translateY(0);
68
+ }
69
+ }
70
+
71
+ &__toolbar-icon {
72
+ display: block;
73
+ width: 18px;
74
+ height: 18px;
75
+ }
76
+
77
+ &__code {
78
+ transition: all 0.25s;
79
+ overflow: hidden;
80
+ border-top: thin solid var(--site-config-color-hl-border);
81
+ }
82
+
83
+ &--scroller {
84
+ code {
85
+ white-space: nowrap;
86
+ }
87
+ }
88
+ }
@@ -0,0 +1,10 @@
1
+ import type { App } from 'vue'
2
+ import CodeExample from './CodeExample.vue'
3
+
4
+ CodeExample.install = function (app: App) {
5
+ app.component(CodeExample.name, CodeExample)
6
+ }
7
+
8
+ export const _CodeExampleComponent = CodeExample
9
+
10
+ export default CodeExample
@@ -0,0 +1,92 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import config from '@config'
4
+ import { getBrowserTheme, getPCLocationInfo } from '@maple-design/cli-vue/client'
5
+ import { Context, Message } from '@maple-design/ui-vue'
6
+ import { DocPreview, type DocPreviewSource } from '@sparknest/ui-doc-preview-vue'
7
+ import type { SiteContext } from '../../../types'
8
+ import { utoa } from '../../../utils'
9
+
10
+ const props = defineProps<{
11
+ sourceName: string
12
+ source: string
13
+ highlightCode: string
14
+ }>()
15
+
16
+ const context = Context as SiteContext
17
+ const isEnglish = getPCLocationInfo().language === 'en-US'
18
+ const sources = computed<DocPreviewSource[]>(() => [
19
+ {
20
+ name: props.sourceName,
21
+ rawCode: decodeURIComponent(props.source),
22
+ highlightCode: decodeURIComponent(props.highlightCode),
23
+ },
24
+ ])
25
+ const playground =
26
+ typeof window !== 'undefined' && window.location.pathname.endsWith('mobile.html')
27
+ ? undefined
28
+ : config?.pc?.header?.playground
29
+ const labels = isEnglish
30
+ ? {
31
+ playground: 'Edit in Playground',
32
+ copy: 'Copy source code',
33
+ copied: 'Source copied',
34
+ showSource: 'Show source',
35
+ hideSource: 'Hide source',
36
+ loadError: 'Unable to load source.',
37
+ retry: 'Retry',
38
+ }
39
+ : {
40
+ playground: '在 Playground 中编辑',
41
+ copy: '复制源码',
42
+ copied: '源码已复制',
43
+ showSource: '展开源码',
44
+ hideSource: '收起源码',
45
+ loadError: '源码加载失败。',
46
+ retry: '重试',
47
+ }
48
+
49
+ function openPlayground(source: string) {
50
+ if (!playground) {
51
+ return
52
+ }
53
+ context.showPlayground = true
54
+ context.playgroundURL = `${playground}/?initialTheme=${getBrowserTheme('demo')}#${utoa(
55
+ JSON.stringify({ 'App.vue': source }),
56
+ )}`
57
+ }
58
+
59
+ function showCopiedMessage() {
60
+ Message.success(config?.pc?.clipboard?.[getPCLocationInfo().language] ?? labels.copied)
61
+ }
62
+ </script>
63
+
64
+ <template>
65
+ <DocPreview
66
+ :sources="sources"
67
+ :show-playground="!!playground"
68
+ :labels="labels"
69
+ @copied="showCopiedMessage"
70
+ @playground="openPlayground"
71
+ >
72
+ <slot />
73
+ </DocPreview>
74
+ </template>
75
+
76
+ <style>
77
+ .maple-component-preview {
78
+ margin: 24px 0;
79
+
80
+ --loca-doc-preview-bg: var(--site-config-color-bar);
81
+ --loca-doc-preview-example-bg: var(--site-config-color-body);
82
+ --loca-doc-preview-source-bg: var(--site-config-color-hl-background);
83
+ --loca-doc-preview-text: var(--site-config-color-text);
84
+ --loca-doc-preview-border: var(--site-config-color-hl-border);
85
+ --loca-doc-preview-brand: var(--site-config-color-text);
86
+ }
87
+
88
+ .maple-component-preview--page .loca-doc-preview__example {
89
+ max-height: min(620px, 70vh);
90
+ overflow: auto;
91
+ }
92
+ </style>
@@ -0,0 +1,8 @@
1
+ import type { App } from 'vue'
2
+ import DemoPreview from './DemoPreview.vue'
3
+
4
+ DemoPreview.install = function (app: App) {
5
+ app.component('MpSiteDemoPreview', DemoPreview)
6
+ }
7
+
8
+ export default DemoPreview
@@ -0,0 +1,17 @@
1
+ export function mobilePreviewUrl(
2
+ base: string,
3
+ componentName: string,
4
+ language: string,
5
+ replace: string,
6
+ hash = '',
7
+ query: Record<string, unknown> = {},
8
+ ) {
9
+ const parameters = new URLSearchParams({ language, platform: 'pc', replace })
10
+ for (const [key, value] of Object.entries(query)) {
11
+ if (['language', 'platform', 'replace'].includes(key)) continue
12
+ for (const item of Array.isArray(value) ? value : [value]) {
13
+ if (item != null) parameters.append(key, String(item))
14
+ }
15
+ }
16
+ return `${base}#/${componentName}?${parameters}${hash ? `#${hash.replace(/^#/, '')}` : ''}`
17
+ }
@@ -0,0 +1,24 @@
1
+ import { afterEach, expect, it, vi } from 'vitest'
2
+ import { currentMobileDemo } from './preview-section'
3
+
4
+ afterEach(() => vi.restoreAllMocks())
5
+
6
+ it('maps the current heading or group to a demo and leaves API sections unmapped', () => {
7
+ const root = document.createElement('main')
8
+ root.innerHTML =
9
+ '<article class="maple-site-doc"><h2>Examples</h2><div class="card"><h3>First</h3><div data-mobile-demo="first"></div></div><div class="card"><h3>Second</h3><div data-mobile-demo="second"></div></div><h2>API</h2><div class="card"><h3>Props</h3></div></article>'
10
+ let current = 0
11
+ Array.from(root.querySelectorAll('h2,h3')).forEach((heading, index) => {
12
+ vi.spyOn(heading, 'getBoundingClientRect').mockImplementation(
13
+ () => ({ top: index <= current ? 100 : 400 }) as DOMRect,
14
+ )
15
+ })
16
+ expect(currentMobileDemo(root)).toBe('first')
17
+ current = 2
18
+ expect(currentMobileDemo(root)).toBe('second')
19
+ current = 3
20
+ expect(currentMobileDemo(root)).toBeUndefined()
21
+ current = 4
22
+ expect(currentMobileDemo(root)).toBeUndefined()
23
+ expect(currentMobileDemo(null)).toBeUndefined()
24
+ })
@@ -0,0 +1,23 @@
1
+ // Resolve from live geometry instead of the debounced URL hash, so switching
2
+ // immediately after a scroll still selects the chapter currently being read.
3
+ export function currentMobileDemo(root?: HTMLElement | null): string | undefined {
4
+ const headings = Array.from(
5
+ root?.querySelectorAll<HTMLElement>('.maple-site-doc h2, .maple-site-doc h3') ?? [],
6
+ ).filter((heading) => !heading.closest('.loca-doc-preview__example, details'))
7
+ let active = headings[0]
8
+ for (const heading of headings) {
9
+ if (heading.getBoundingClientRect().top > 120) break
10
+ active = heading
11
+ }
12
+ if (!active) return
13
+ const card = active.closest('.card')
14
+ if (card) return card.querySelector<HTMLElement>('[data-mobile-demo]')?.dataset.mobileDemo
15
+ // A group heading selects its first demo; never cross into the next group.
16
+ let sibling: Element | null = active
17
+ while (sibling) {
18
+ if (sibling !== active && sibling.matches('h2')) break
19
+ const demo = sibling.querySelector<HTMLElement>('[data-mobile-demo]')
20
+ if (demo) return demo.dataset.mobileDemo
21
+ sibling = sibling.nextElementSibling
22
+ }
23
+ }