@wot-ui/vitepress-theme 2.0.0-alpha.10

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 (57) hide show
  1. package/README.md +153 -0
  2. package/dist/config.js +118 -0
  3. package/dist/index.js +68 -0
  4. package/dist/locales/md-component-links.js +15 -0
  5. package/dist/plugins/md-component-links.js +34 -0
  6. package/dist/plugins/md-scss-vars.js +73 -0
  7. package/dist/plugins/md-version-badge.js +51 -0
  8. package/dist/plugins/virtual-version-data.js +83 -0
  9. package/dist/src/config.d.ts +3 -0
  10. package/dist/src/index.d.ts +21 -0
  11. package/dist/src/locales/md-component-links.d.ts +13 -0
  12. package/dist/src/plugins/md-component-links.d.ts +3 -0
  13. package/dist/src/plugins/md-scss-vars.d.ts +3 -0
  14. package/dist/src/plugins/md-version-badge.d.ts +6 -0
  15. package/dist/src/plugins/virtual-version-data.d.ts +3 -0
  16. package/dist/src/theme/composables/adSponsor.d.ts +30 -0
  17. package/dist/src/theme/composables/adsData.d.ts +16 -0
  18. package/dist/src/theme/composables/banner.d.ts +16 -0
  19. package/dist/src/theme/composables/cases.d.ts +16 -0
  20. package/dist/src/theme/composables/friendly.d.ts +19 -0
  21. package/dist/src/theme/composables/sponsor.d.ts +3 -0
  22. package/dist/src/theme/composables/team.d.ts +28 -0
  23. package/dist/src/theme/options.d.ts +3 -0
  24. package/dist/src/types.d.ts +86 -0
  25. package/dist/theme/Layout.vue +45 -0
  26. package/dist/theme/components/AsideSponsors.vue +105 -0
  27. package/dist/theme/components/Banner.vue +377 -0
  28. package/dist/theme/components/CustomFooter.vue +123 -0
  29. package/dist/theme/components/ExternalLink.vue +36 -0
  30. package/dist/theme/components/HomeCases.vue +122 -0
  31. package/dist/theme/components/HomeFriendly.vue +96 -0
  32. package/dist/theme/components/HomeTeam.vue +250 -0
  33. package/dist/theme/components/QrCode.vue +45 -0
  34. package/dist/theme/components/SidebarAds.vue +86 -0
  35. package/dist/theme/components/SpecialSponsor.vue +115 -0
  36. package/dist/theme/components/SvgImage.vue +22 -0
  37. package/dist/theme/components/VPContent.vue +92 -0
  38. package/dist/theme/components/VPDoc.vue +222 -0
  39. package/dist/theme/components/VPFeature.vue +150 -0
  40. package/dist/theme/components/VPIframe.vue +445 -0
  41. package/dist/theme/components/VPLocalNav.vue +151 -0
  42. package/dist/theme/components/VPNavBar.vue +253 -0
  43. package/dist/theme/components/VPSidebar.vue +120 -0
  44. package/dist/theme/components/VPSidebarItem.vue +271 -0
  45. package/dist/theme/components/WwAds.vue +74 -0
  46. package/dist/theme/composables/adSponsor.js +29 -0
  47. package/dist/theme/composables/adsData.js +35 -0
  48. package/dist/theme/composables/banner.js +35 -0
  49. package/dist/theme/composables/cases.js +43 -0
  50. package/dist/theme/composables/friendly.js +35 -0
  51. package/dist/theme/composables/sponsor.js +35 -0
  52. package/dist/theme/composables/team.js +48 -0
  53. package/dist/theme/options.js +4 -0
  54. package/dist/theme/styles/custom.css +206 -0
  55. package/dist/theme/styles/vars.css +136 -0
  56. package/dist/types.js +0 -0
  57. package/package.json +41 -0
@@ -0,0 +1,377 @@
1
+ <script setup lang="ts">
2
+ import { ref, watch, computed, onMounted } from 'vue'
3
+ import { useBanner } from '../composables/banner'
4
+
5
+ const open = ref(false) // 默认不显示,避免闪烁
6
+ const BANNER_STORAGE_KEY = 'wot-banner-dismissed-time'
7
+ const TWENTY_FOUR_HOURS = 24 * 60 * 60 * 1000 // 24小时的毫秒数
8
+
9
+ // 使用 banner composable 获取远程数据
10
+ const { data: bannerData } = useBanner()
11
+
12
+ // 计算当前要显示的 banner 信息(取第一个)
13
+ const currentBanner = computed(() => {
14
+ return bannerData.value && bannerData.value.length > 0 ? bannerData.value[0] : null
15
+ })
16
+
17
+ /**
18
+ * 检查是否应该显示横幅
19
+ */
20
+ function checkShouldShowBanner() {
21
+ if (typeof window === 'undefined') return true
22
+
23
+ const dismissedTime = localStorage.getItem(BANNER_STORAGE_KEY)
24
+ if (!dismissedTime) {
25
+ // 首次访问,添加 banner-show class 以显示横幅
26
+ document.documentElement.classList.add('banner-show')
27
+ return true
28
+ }
29
+
30
+ const dismissedTimestamp = parseInt(dismissedTime, 10)
31
+ const currentTime = Date.now()
32
+
33
+ // 如果超过24小时,清除记录并显示横幅
34
+ if (currentTime - dismissedTimestamp > TWENTY_FOUR_HOURS) {
35
+ localStorage.removeItem(BANNER_STORAGE_KEY)
36
+ document.documentElement.classList.add('banner-show')
37
+ return true
38
+ }
39
+
40
+ // 未超过24小时,确保不显示横幅
41
+ document.documentElement.classList.remove('banner-show')
42
+ return false
43
+ }
44
+
45
+ function dismiss() {
46
+ open.value = false
47
+ document.documentElement.classList.remove('banner-show')
48
+
49
+ // 存储当前时间戳到 localStorage
50
+ if (typeof window !== 'undefined') {
51
+ localStorage.setItem(BANNER_STORAGE_KEY, Date.now().toString())
52
+ }
53
+ }
54
+
55
+ // 监听 banner 数据变化,只有当有数据时才进行展示逻辑校验
56
+ watch(
57
+ currentBanner,
58
+ (newBanner) => {
59
+ if (newBanner) {
60
+ // 有 banner 数据时,检查是否应该显示
61
+ const shouldShow = checkShouldShowBanner()
62
+ open.value = shouldShow
63
+ } else {
64
+ // 没有 banner 数据时,不显示横幅
65
+ open.value = false
66
+ }
67
+ },
68
+ { immediate: true }
69
+ )
70
+ </script>
71
+
72
+ <template>
73
+ <div class="banner" v-if="open && currentBanner">
74
+ <div class="vt-banner-text">
75
+ <p class="vt-banner-title">{{ currentBanner.title }}</p>
76
+ <a target="_blank" class="vt-primary-action" :href="currentBanner.link">
77
+ {{ currentBanner.action }}
78
+ </a>
79
+ </div>
80
+ <button aria-label="close" @click="dismiss">
81
+ <svg class="close" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewBox="0 0 24 24">
82
+ <path
83
+ d="M18.9,10.9h-6v-6c0-0.6-0.4-1-1-1s-1,0.4-1,1v6h-6c-0.6,0-1,0.4-1,1s0.4,1,1,1h6v6c0,0.6,0.4,1,1,1s1-0.4,1-1v-6h6c0.6,0,1-0.4,1-1S19.5,10.9,18.9,10.9z"
84
+ />
85
+ </svg>
86
+ </button>
87
+ <div class="glow glow--purple"></div>
88
+ <div class="glow glow--blue"></div>
89
+ </div>
90
+ </template>
91
+
92
+ <style>
93
+ html.banner-show {
94
+ --vp-layout-top-height: 64px;
95
+ }
96
+
97
+ /* 移动端优化高度 */
98
+ @media (max-width: 768px) {
99
+ html.banner-show {
100
+ --vp-layout-top-height: 56px;
101
+ }
102
+ }
103
+
104
+ @media (max-width: 480px) {
105
+ html.banner-show {
106
+ --vp-layout-top-height: 48px;
107
+ }
108
+ }
109
+ </style>
110
+
111
+ <style scoped>
112
+ .banner {
113
+ position: fixed;
114
+ z-index: 100;
115
+ box-sizing: border-box;
116
+ top: 0;
117
+ left: 0;
118
+ right: 0;
119
+ height: var(--vp-layout-top-height, 64px);
120
+ padding: 0 48px 0 12px;
121
+ text-align: center;
122
+ font-size: 18px;
123
+ font-weight: 600;
124
+ color: var(--vp-c-white);
125
+ background: var(--vp-c-bg-alt);
126
+ display: none;
127
+ justify-content: center;
128
+ align-items: center;
129
+ overflow: hidden;
130
+ }
131
+
132
+ html.banner-show .banner {
133
+ display: flex;
134
+ }
135
+
136
+ .glow.glow--purple {
137
+ position: absolute;
138
+ bottom: -15%;
139
+ left: -75%;
140
+ width: 80%;
141
+ aspect-ratio: 1.5;
142
+ pointer-events: none;
143
+ border-radius: 100%;
144
+ background: linear-gradient(270deg, var(--vt-c-accent-purple), var(--vt-c-brand-2) 60% 80%, transparent);
145
+ filter: blur(15vw);
146
+ transform: none;
147
+ opacity: 0.6;
148
+ }
149
+
150
+ .glow.glow--blue {
151
+ position: absolute;
152
+ bottom: -15%;
153
+ right: -40%;
154
+ width: 80%;
155
+ aspect-ratio: 1.5;
156
+ pointer-events: none;
157
+ border-radius: 100%;
158
+ background: linear-gradient(180deg, var(--vt-c-accent-cyan), transparent);
159
+ filter: blur(15vw);
160
+ transform: none;
161
+ opacity: 0.3;
162
+ }
163
+
164
+ button {
165
+ position: absolute;
166
+ right: 8px;
167
+ top: 50%;
168
+ transform: translateY(-50%);
169
+ padding: 4px;
170
+ background: transparent;
171
+ border: none;
172
+ cursor: pointer;
173
+ display: flex;
174
+ align-items: center;
175
+ justify-content: center;
176
+ transition: opacity 0.2s ease;
177
+ }
178
+
179
+ button:hover {
180
+ opacity: 0.8;
181
+ }
182
+
183
+ button:active {
184
+ opacity: 0.6;
185
+ }
186
+
187
+ .close {
188
+ width: 28px;
189
+ height: 28px;
190
+ fill: var(--vp-c-white);
191
+ transform: rotate(45deg);
192
+ transition: transform 0.2s ease;
193
+ }
194
+
195
+ button:hover .close {
196
+ transform: rotate(45deg) scale(1.1);
197
+ }
198
+
199
+ .vt-banner-text {
200
+ display: flex;
201
+ align-items: center;
202
+ justify-content: center;
203
+ flex-wrap: wrap;
204
+ gap: 8px;
205
+ color: var(--vp-c-white);
206
+ font-size: 18px;
207
+ line-height: 1.4;
208
+ padding: 8px 0;
209
+ }
210
+
211
+ .vt-banner-title {
212
+ display: inline-block;
213
+ background: linear-gradient(90deg, var(--vt-c-accent-purple) 0%, var(--vt-c-accent-cyan) 100%);
214
+ background-clip: text;
215
+ -webkit-background-clip: text;
216
+ -webkit-text-fill-color: transparent;
217
+ color: transparent;
218
+ text-align: center;
219
+ font-size: 18px;
220
+ font-style: normal;
221
+ font-weight: 700;
222
+ line-height: 1.4;
223
+ white-space: nowrap;
224
+ }
225
+
226
+ .vt-primary-action {
227
+ display: inline-block;
228
+ background: radial-gradient(140.35% 140.35% at 175% 94.74%, var(--vt-c-accent-mint), transparent),
229
+ radial-gradient(89.94% 89.94% at 18.42% 15.79%, var(--vt-c-brand-1), transparent);
230
+ color: var(--vp-c-white);
231
+ padding: 6px 12px;
232
+ border-radius: 6px;
233
+ font-size: 16px;
234
+ font-weight: 600;
235
+ text-decoration: none;
236
+ transition: all 0.2s ease-in-out;
237
+ white-space: nowrap;
238
+ }
239
+
240
+ .vt-primary-action:hover {
241
+ transform: translateY(-1px);
242
+ box-shadow: 0 8px 24px rgba(59, 130, 246, 0.28);
243
+ }
244
+
245
+ .vt-primary-action:active {
246
+ transform: translateY(0);
247
+ }
248
+
249
+ /* 桌面端优化 */
250
+ @media (min-width: 769px) {
251
+ .banner {
252
+ padding: 0 60px 0 20px;
253
+ }
254
+
255
+ .glow.glow--blue {
256
+ top: -15%;
257
+ right: -40%;
258
+ width: 80%;
259
+ }
260
+
261
+ .glow.glow--purple {
262
+ bottom: -15%;
263
+ left: -40%;
264
+ width: 80%;
265
+ }
266
+
267
+ .vt-banner-text {
268
+ gap: 12px;
269
+ }
270
+ }
271
+
272
+ /* 平板端优化 */
273
+ @media (max-width: 768px) {
274
+ .banner {
275
+ padding: 0 40px 0 10px;
276
+ }
277
+
278
+ button {
279
+ right: 6px;
280
+ }
281
+
282
+ .close {
283
+ width: 24px;
284
+ height: 24px;
285
+ }
286
+
287
+ .vt-banner-text {
288
+ font-size: 16px;
289
+ gap: 6px;
290
+ }
291
+
292
+ .vt-banner-title {
293
+ font-size: 16px;
294
+ }
295
+
296
+ .vt-primary-action {
297
+ font-size: 14px;
298
+ padding: 5px 10px;
299
+ }
300
+ }
301
+
302
+ /* 手机端优化 */
303
+ @media (max-width: 640px) {
304
+ .banner {
305
+ padding: 0 36px 0 8px;
306
+ }
307
+
308
+ .vt-banner-text {
309
+ font-size: 14px;
310
+ gap: 6px;
311
+ }
312
+
313
+ .vt-banner-title {
314
+ font-size: 14px;
315
+ }
316
+
317
+ .vt-primary-action {
318
+ font-size: 13px;
319
+ padding: 4px 8px;
320
+ }
321
+ }
322
+
323
+ /* 小屏手机优化 */
324
+ @media (max-width: 480px) {
325
+ .banner {
326
+ padding: 0 32px 0 6px;
327
+ }
328
+
329
+ button {
330
+ right: 4px;
331
+ padding: 2px;
332
+ }
333
+
334
+ .close {
335
+ width: 20px;
336
+ height: 20px;
337
+ }
338
+
339
+ .vt-banner-text {
340
+ font-size: 12px;
341
+ gap: 4px;
342
+ flex-direction: column;
343
+ line-height: 1.3;
344
+ }
345
+
346
+ .vt-banner-title {
347
+ font-size: 12px;
348
+ }
349
+
350
+ .vt-primary-action {
351
+ font-size: 12px;
352
+ padding: 3px 8px;
353
+ border-radius: 4px;
354
+ }
355
+ }
356
+
357
+ /* 超小屏优化 */
358
+ @media (max-width: 375px) {
359
+ .banner {
360
+ padding: 0 28px 0 4px;
361
+ }
362
+
363
+ .vt-banner-text {
364
+ font-size: 11px;
365
+ gap: 3px;
366
+ }
367
+
368
+ .vt-banner-title {
369
+ font-size: 11px;
370
+ }
371
+
372
+ .vt-primary-action {
373
+ font-size: 11px;
374
+ padding: 2px 6px;
375
+ }
376
+ }
377
+ </style>
@@ -0,0 +1,123 @@
1
+ <script setup lang="ts">
2
+ import { computed, onMounted, ref } from 'vue'
3
+ import { useData } from 'vitepress'
4
+ import { useLayout } from 'vitepress/theme'
5
+
6
+ const { theme }: any = useData()
7
+ const { hasSidebar } = useLayout()
8
+
9
+ const isNetlify = ref<boolean>(false)
10
+ const isWotUiDomain = ref<boolean>(false)
11
+
12
+ const copyright = computed(() => {
13
+ let copyrightText = theme.value.footer.copyright
14
+
15
+ if (isWotUiDomain.value) {
16
+ copyrightText += ' | <a href="https://beian.miit.gov.cn/" target="_blank" style="text-decoration: none;">沪ICP备2024070925号-4</a>'
17
+ }
18
+
19
+ if (isNetlify.value) {
20
+ copyrightText += ' | <a style="text-decoration: none;" href="https://www.netlify.com">This site is powered by Netlify</a>'
21
+ }
22
+
23
+ return copyrightText
24
+ })
25
+
26
+ onMounted(() => {
27
+ if (typeof window !== 'undefined') {
28
+ isNetlify.value = window.location.href.includes('netlify')
29
+ isWotUiDomain.value = window.location.hostname.includes('wot-ui.cn')
30
+ }
31
+ })
32
+ </script>
33
+
34
+ <template>
35
+ <footer v-if="theme.footer" class="VPFooter" :class="{ 'has-sidebar': hasSidebar }">
36
+ <div class="container">
37
+ <p v-if="theme.footer.message" class="message" v-html="theme.footer.message"></p>
38
+ <p v-if="copyright" class="copyright" v-html="copyright"></p>
39
+ </div>
40
+ </footer>
41
+ </template>
42
+
43
+ <style scoped>
44
+ .VPFooter {
45
+ position: relative;
46
+ z-index: var(--vp-z-index-footer);
47
+ border-top: 1px solid var(--vp-c-gutter);
48
+ padding: 40px 24px 32px;
49
+ background: linear-gradient(135deg, var(--vp-c-bg) 0%, var(--vp-c-bg-soft) 100%);
50
+ box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.04);
51
+ transition: all 0.3s ease;
52
+ }
53
+
54
+ .VPFooter.has-sidebar {
55
+ display: none;
56
+ }
57
+
58
+ .VPFooter :deep(a) {
59
+ color: var(--vp-c-brand-1);
60
+ text-decoration: none;
61
+ padding: 2px 6px;
62
+ border-radius: 4px;
63
+ transition: all 0.3s ease;
64
+ position: relative;
65
+ font-weight: 500;
66
+ }
67
+
68
+ .VPFooter :deep(a:hover) {
69
+ color: var(--vp-c-brand-2);
70
+ background-color: var(--vp-c-brand-soft);
71
+ transform: translateY(-1px);
72
+ }
73
+
74
+ .VPFooter :deep(a:active) {
75
+ transform: translateY(0);
76
+ }
77
+
78
+ @media (min-width: 768px) {
79
+ .VPFooter {
80
+ padding: 48px 32px 40px;
81
+ }
82
+ }
83
+
84
+ @media (max-width: 767px) {
85
+ .VPFooter {
86
+ padding: 32px 20px 24px;
87
+ }
88
+
89
+ .message,
90
+ .copyright {
91
+ font-size: 13px;
92
+ line-height: 20px;
93
+ }
94
+ }
95
+
96
+ .container {
97
+ margin: 0 auto;
98
+ max-width: var(--vp-layout-max-width);
99
+ text-align: center;
100
+ }
101
+
102
+ .message {
103
+ line-height: 28px;
104
+ font-size: 15px;
105
+ font-weight: 600;
106
+ color: var(--vp-c-text-1);
107
+ margin-bottom: 16px;
108
+ opacity: 0.9;
109
+ }
110
+
111
+ .copyright {
112
+ line-height: 26px;
113
+ font-size: 14px;
114
+ font-weight: 400;
115
+ color: var(--vp-c-text-2);
116
+ opacity: 0.8;
117
+ transition: opacity 0.3s ease;
118
+ }
119
+
120
+ .copyright:hover {
121
+ opacity: 1;
122
+ }
123
+ </style>
@@ -0,0 +1,36 @@
1
+ <script setup lang="ts">
2
+ import { defineComponent, h } from 'vue'
3
+ defineProps({
4
+ href: {
5
+ type: String,
6
+ required: true
7
+ }
8
+ })
9
+
10
+ const ExternalLinkIconComponent = defineComponent({
11
+ name: 'ExternalLinkIcon',
12
+ render() {
13
+ const ExternalLinkIcon = {
14
+ template: `
15
+ <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
16
+ <g fill="none">
17
+ <path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z" />
18
+ <path fill="currentColor" d="M11 6a1 1 0 1 1 0 2H5v11h11v-6a1 1 0 1 1 2 0v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2zm9-3a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0V6.414l-8.293 8.293a1 1 0 0 1-1.414-1.414L17.586 5H15a1 1 0 1 1 0-2Z" />
19
+ </g>
20
+ </svg>
21
+ `
22
+ }
23
+
24
+ return h('div', {
25
+ innerHTML: ExternalLinkIcon.template
26
+ })
27
+ }
28
+ })
29
+ </script>
30
+
31
+ <template>
32
+ <el-link :href="href" target="_blank" style="text-decoration: none; font-size: 1.2em" :underline="false">
33
+ <slot />
34
+ <ExternalLinkIconComponent style="margin-left: 0.25em"></ExternalLinkIconComponent>
35
+ </el-link>
36
+ </template>
@@ -0,0 +1,122 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { useCaseData } from '../composables/cases'
4
+ import { useData } from 'vitepress'
5
+ import VPFeature from './VPFeature.vue'
6
+
7
+ const { data } = useCaseData()
8
+ const { lang } = useData()
9
+
10
+ const cases = computed(() => {
11
+ return lang.value === 'en-US' ? [] : data.value.length ? data.value : []
12
+ })
13
+
14
+ const grid = computed(() => {
15
+ const length = cases.value.length || 0
16
+ if (!length) {
17
+ return
18
+ } else {
19
+ return 'grid-4'
20
+ }
21
+ })
22
+ </script>
23
+
24
+ <template>
25
+ <div v-if="cases && cases.length" class="VPFeatures">
26
+ <div class="container">
27
+ <h1 class="cases-title">优秀案例</h1>
28
+
29
+ <div class="items">
30
+ <div v-for="(item, index) in cases" :key="index" class="item" :class="[grid]">
31
+ <VPFeature
32
+ :icon="{ src: item.image, height: '48px', width: 'auto' }"
33
+ :title="item.name"
34
+ :details="item.description"
35
+ :image-preview="true"
36
+ />
37
+ </div>
38
+ </div>
39
+ </div>
40
+ </div>
41
+ </template>
42
+
43
+ <style scoped>
44
+ .VPFeatures {
45
+ position: relative;
46
+ padding: 0 24px;
47
+ }
48
+
49
+ :deep(.VPImage) {
50
+ display: flex;
51
+ justify-content: center;
52
+ align-items: center;
53
+ margin-bottom: 20px;
54
+ border-radius: 6px;
55
+ /* background-color: var(--vp-c-default-soft); */
56
+ width: 48px;
57
+ height: 48px;
58
+ font-size: 24px;
59
+ transition: background-color 0.25s;
60
+ }
61
+
62
+ @media (min-width: 640px) {
63
+ .VPFeatures {
64
+ padding: 0 48px;
65
+ }
66
+ }
67
+
68
+ @media (min-width: 960px) {
69
+ .VPFeatures {
70
+ padding: 0 64px;
71
+ }
72
+ }
73
+
74
+ .container {
75
+ margin: 0 auto;
76
+ max-width: 1152px;
77
+ }
78
+
79
+ .cases-title {
80
+ text-align: center;
81
+ margin-bottom: 50px;
82
+ margin-top: 50px;
83
+ font-size: 24px;
84
+ }
85
+
86
+ .items {
87
+ display: flex;
88
+ flex-wrap: wrap;
89
+ margin: -8px;
90
+ }
91
+
92
+ .item {
93
+ padding: 8px;
94
+ width: 100%;
95
+ }
96
+
97
+ @media (min-width: 640px) {
98
+ .item.grid-2,
99
+ .item.grid-4,
100
+ .item.grid-6 {
101
+ width: calc(100% / 2);
102
+ }
103
+ }
104
+
105
+ @media (min-width: 768px) {
106
+ .item.grid-2 {
107
+ width: calc(100% / 2);
108
+ }
109
+
110
+ .item.grid-3,
111
+ .item.grid-4,
112
+ .item.grid-6 {
113
+ width: calc(100% / 3);
114
+ }
115
+ }
116
+
117
+ @media (min-width: 960px) {
118
+ .item.grid-4 {
119
+ width: calc(100% / 4);
120
+ }
121
+ }
122
+ </style>