vome-core 0.1.82 → 0.1.83

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 (83) hide show
  1. package/dist/admin/components/ui/avatar/index.js +1 -1
  2. package/dist/admin/components/ui/badge/index.d.ts +1 -1
  3. package/dist/admin/components/ui/badge/index.js +1 -1
  4. package/dist/admin/components/ui/button/index.d.ts +1 -1
  5. package/dist/admin/components/ui/button/index.js +1 -1
  6. package/dist/admin/components/ui/checkbox/index.js +1 -1
  7. package/dist/admin/components/ui/dialog/index.js +1 -1
  8. package/dist/admin/components/ui/dropdown-menu/index.js +1 -1
  9. package/dist/admin/components/ui/input/index.js +1 -1
  10. package/dist/admin/components/ui/label/index.js +1 -1
  11. package/dist/admin/components/ui/scroll-area/index.js +1 -1
  12. package/dist/admin/components/ui/select/index.js +1 -1
  13. package/dist/admin/components/ui/separator/index.js +1 -1
  14. package/dist/admin/components/ui/sheet/index.js +1 -1
  15. package/dist/admin/components/ui/sonner/index.js +1 -1
  16. package/dist/admin/components/ui/table/index.js +1 -1
  17. package/dist/admin/components/ui/tabs/index.js +1 -1
  18. package/dist/admin/components/ui/tooltip/index.js +1 -1
  19. package/dist/admin/components/vm-role-picker.vue +10 -3
  20. package/dist/admin/crud/components/vm-richtext-extensions.js +1 -1
  21. package/dist/admin/crud/config.js +1 -1
  22. package/dist/admin/crud/confirm.js +1 -1
  23. package/dist/admin/crud/crud-page-request.js +1 -1
  24. package/dist/admin/crud/dict.js +1 -1
  25. package/dist/admin/crud/export-fetch.js +1 -1
  26. package/dist/admin/crud/key.js +1 -1
  27. package/dist/admin/crud/plugins.js +1 -1
  28. package/dist/admin/crud/span.js +1 -1
  29. package/dist/admin/crud/style.js +1 -1
  30. package/dist/admin/crud/validate.js +1 -1
  31. package/dist/admin/directives/perm.js +1 -1
  32. package/dist/admin/hooks/useUpload.js +1 -1
  33. package/dist/admin/index.d.ts +0 -5
  34. package/dist/admin/index.js +0 -4
  35. package/dist/admin/lib/browser.js +1 -1
  36. package/dist/admin/lib/cn.js +1 -1
  37. package/dist/admin/lib/dialog-float.js +1 -1
  38. package/dist/admin/lib/export-excel.js +1 -1
  39. package/dist/admin/lib/file-preview.js +6 -2
  40. package/dist/admin/lib/import-excel.js +1 -1
  41. package/dist/admin/lib/json.js +1 -1
  42. package/dist/admin/lib/menu.js +1 -1
  43. package/dist/admin/lib/micro-locale.js +1 -1
  44. package/dist/admin/lib/micro-theme.js +1 -1
  45. package/dist/admin/lib/setup-crud-defaults.js +1 -1
  46. package/dist/admin/lib/shell.js +1 -1
  47. package/dist/admin/lib/theme-color-options.js +1 -1
  48. package/dist/admin/lib/tree.js +1 -1
  49. package/dist/admin/lib/upload.js +1 -1
  50. package/dist/admin/lib/video-frame.js +1 -1
  51. package/dist/admin/router/create.js +1 -1
  52. package/dist/admin/router/guards.js +1 -1
  53. package/dist/admin/themes/dark.js +1 -1
  54. package/dist/admin/themes/index.js +1 -1
  55. package/dist/admin/themes/light.js +1 -1
  56. package/dist/ai/index.js +298 -9
  57. package/dist/client/configure.js +13 -4
  58. package/dist/client/vite-admin.d.ts +16 -1
  59. package/dist/client/vite-admin.js +33 -13
  60. package/dist/client/vite-auto-import.js +3 -3
  61. package/dist/client/vite-resolve.js +28 -4
  62. package/dist/index.js +1 -1
  63. package/dist/server/index.js +1 -1
  64. package/dist/shared/datetime-picker.js +1 -1
  65. package/dist/shared/excel.js +1 -1
  66. package/dist/shared/index.js +1 -1
  67. package/dist/shared/locale-json.js +1 -1
  68. package/dist/shared/tree.js +1 -1
  69. package/package.json +1 -1
  70. package/dist/admin/components/layout/vm-app-header.vue +0 -341
  71. package/dist/admin/components/layout/vm-app-sidebar.vue +0 -187
  72. package/dist/admin/components/layout/vm-sidebar-flyout.vue +0 -268
  73. package/dist/admin/components/layout/vm-sidebar-nav-item.vue +0 -490
  74. package/dist/admin/components/layout/vm-sidebar-nav.vue +0 -104
  75. package/dist/admin/components/layout/vm-tags-view.vue +0 -233
  76. package/dist/admin/lib/document-title.d.ts +0 -6
  77. package/dist/admin/lib/document-title.js +0 -1
  78. package/dist/admin/stores/dict.d.ts +0 -13
  79. package/dist/admin/stores/dict.js +0 -1
  80. package/dist/admin/stores/locale.d.ts +0 -3
  81. package/dist/admin/stores/locale.js +0 -1
  82. package/dist/admin/stores/theme.d.ts +0 -1
  83. package/dist/admin/stores/theme.js +0 -1
@@ -1,490 +0,0 @@
1
- <template>
2
- <div>
3
- <!-- ===== 迷你侧栏:图标轨 + 多级浮窗 ===== -->
4
- <template v-if="collapsed">
5
- <!-- 叶子:直接跳转 -->
6
- <RouterLink
7
- v-if="isLeaf && item.router"
8
- :to="item.router"
9
- class="vm-nav-item is-collapsed"
10
- :class="{ 'is-active': shownActive }"
11
- :title="menuLabel"
12
- @click="onLeafClick"
13
- >
14
- <span class="vm-nav-item__icon-slot">
15
- <vm-ri-icon v-if="hasIcon(item)" :name="iconName(item)" class="vm-nav-item__icon" />
16
- </span>
17
- </RouterLink>
18
-
19
- <!-- 目录:悬停弹出多级浮窗(无子项仍显示图标) -->
20
- <div
21
- v-else-if="isDir"
22
- ref="miniRef"
23
- class="vm-nav-mini"
24
- @mouseenter="openMiniFlyout"
25
- @mouseleave="scheduleCloseMini"
26
- >
27
- <button
28
- type="button"
29
- class="vm-nav-item is-collapsed is-dir"
30
- :class="{ 'is-active': activeBranch, 'is-branch': activeBranch }"
31
- :title="menuLabel"
32
- >
33
- <span class="vm-nav-item__icon-slot">
34
- <vm-ri-icon v-if="hasIcon(item)" :name="iconName(item)" class="vm-nav-item__icon" />
35
- </span>
36
- </button>
37
-
38
- <Teleport to="body">
39
- <vm-sidebar-flyout
40
- v-if="miniOpen && children.length"
41
- :items="children"
42
- :parent-id="item.id"
43
- :anchor="miniAnchor"
44
- @enter="clearMiniTimer"
45
- @leave="scheduleCloseMini"
46
- @navigate="closeMiniNow"
47
- />
48
- </Teleport>
49
- </div>
50
- </template>
51
-
52
- <!-- ===== 展开侧栏 ===== -->
53
- <template v-else>
54
- <RouterLink
55
- v-if="isLeaf && item.router"
56
- :to="item.router"
57
- class="vm-nav-item"
58
- :class="{ 'is-active': shownActive, 'is-mobile': mobile }"
59
- :style="{ paddingLeft: `${pad}px` }"
60
- @click="onLeafClick"
61
- >
62
- <span
63
- v-if="!mobile"
64
- class="vm-nav-item__notch vm-nav-item__notch--top"
65
- aria-hidden="true"
66
- />
67
- <span
68
- v-if="!mobile"
69
- class="vm-nav-item__notch vm-nav-item__notch--bottom"
70
- aria-hidden="true"
71
- />
72
- <span class="vm-nav-item__icon-slot">
73
- <vm-ri-icon v-if="hasIcon(item)" :name="iconName(item)" class="vm-nav-item__icon" />
74
- </span>
75
- <span class="vm-nav-item__label">{{ menuLabel }}</span>
76
- </RouterLink>
77
-
78
- <div
79
- v-else-if="isDir"
80
- class="vm-nav-group"
81
- :class="{ 'is-deep': isDeepGroup }"
82
- >
83
- <button
84
- type="button"
85
- class="vm-nav-item is-dir"
86
- :class="{ 'is-branch': activeBranch, 'is-mobile': mobile }"
87
- :style="{ paddingLeft: `${pad}px` }"
88
- @click="toggle"
89
- >
90
- <span class="vm-nav-item__icon-slot">
91
- <vm-ri-icon v-if="hasIcon(item)" :name="iconName(item)" class="vm-nav-item__icon" />
92
- </span>
93
- <span class="vm-nav-item__label">{{ menuLabel }}</span>
94
- <i
95
- class="ri-arrow-down-s-line vm-nav-item__arrow"
96
- :class="{ 'is-open': open }"
97
- />
98
- </button>
99
- <div v-show="open" class="vm-nav-group__children">
100
- <vm-sidebar-nav-item
101
- v-for="child in children"
102
- :key="child.id"
103
- :item="child"
104
- :depth="depth + 1"
105
- :parent-id="item.id"
106
- :collapsed="false"
107
- :mobile="mobile"
108
- />
109
- </div>
110
- </div>
111
- </template>
112
- </div>
113
- </template>
114
-
115
- <script setup lang="ts">
116
- import { useLocaleStore } from '../../stores/locale'
117
-
118
- defineOptions({ name: 'vm-sidebar-nav-item' })
119
-
120
- const props = defineProps<{
121
- item: MenuTreeNode
122
- depth: number
123
- parentId?: number | null
124
- collapsed?: boolean
125
- /** 移动端抽屉:选中用内嵌白底,不用右侧开口 */
126
- mobile?: boolean
127
- }>()
128
-
129
- const locale = useLocaleStore()
130
- const menuLabel = computed(() => locale.tMenu(props.item))
131
-
132
- const route = useRoute()
133
-
134
- const OPEN_KEY = 'sidebar-open-ids'
135
- const PENDING_KEY = 'sidebar-pending-leaf'
136
- const DEEP_KEY = 'sidebar-deep-focus'
137
- const openIds = inject<Ref<Set<number>>>(OPEN_KEY)!
138
- const pendingLeafId = inject<Ref<number | null>>(PENDING_KEY)!
139
- const deepFocusId = inject<Ref<number | null>>(DEEP_KEY)!
140
- if (!openIds || !pendingLeafId || !deepFocusId) {
141
- throw new Error('[vm-sidebar-nav-item] 缺少 sidebar provide')
142
- }
143
-
144
- const miniRef = ref<HTMLElement | null>(null)
145
- const miniOpen = ref(false)
146
- const miniAnchor = ref({ top: 0, left: 0 })
147
- let miniTimer: ReturnType<typeof setTimeout> | null = null
148
-
149
- function hasIcon(item: MenuTreeNode) {
150
- return Boolean(item.icon && String(item.icon).trim())
151
- }
152
-
153
- function iconName(item: MenuTreeNode) {
154
- const raw = String(item.icon || '').trim()
155
- return raw.startsWith('ri-') ? raw : `ri-${raw}`
156
- }
157
-
158
- function visibleChildren(item: MenuTreeNode) {
159
- return (item.children ?? []).filter(
160
- (c) => c.isShow !== false && Number(c.type) !== 2,
161
- )
162
- }
163
-
164
- const children = computed(() => visibleChildren(props.item))
165
- const isDir = computed(
166
- () => Number(props.item.type) === 0 || children.value.length > 0,
167
- )
168
- const isLeaf = computed(
169
- () => Number(props.item.type) === 1 && Boolean(props.item.router),
170
- )
171
-
172
- function isActiveLeaf(item: MenuTreeNode) {
173
- if (!item.router) return false
174
- return route.path === item.router || route.path.startsWith(`${item.router}/`)
175
- }
176
-
177
- function containsActive(item: MenuTreeNode): boolean {
178
- if (isActiveLeaf(item)) return true
179
- return visibleChildren(item).some((c) => containsActive(c))
180
- }
181
-
182
- function containsId(item: MenuTreeNode, id: number): boolean {
183
- if (item.id === id) return true
184
- return visibleChildren(item).some((c) => containsId(c, id))
185
- }
186
-
187
- const activeLeaf = computed(() => isActiveLeaf(props.item))
188
- const shownActive = computed(
189
- () => activeLeaf.value || pendingLeafId.value === props.item.id,
190
- )
191
- const activeBranch = computed(() => containsActive(props.item))
192
- const open = computed(() => openIds!.value.has(props.item.id))
193
- const isDeepGroup = computed(
194
- () => !props.collapsed && open.value && deepFocusId.value === props.item.id,
195
- )
196
-
197
- function onLeafClick() {
198
- pendingLeafId.value = props.item.id
199
- deepFocusId.value = props.parentId ?? null
200
- }
201
-
202
- function ensureOpenAlongActive(item: MenuTreeNode) {
203
- if (!containsActive(item)) return
204
- if (isDir.value) {
205
- openIds!.value = new Set(openIds!.value).add(item.id)
206
- }
207
- for (const c of children.value) {
208
- if (containsActive(c) && (Number(c.type) === 0 || visibleChildren(c).length)) {
209
- openIds!.value = new Set(openIds!.value).add(c.id)
210
- }
211
- }
212
- }
213
-
214
- watch(
215
- () => route.path,
216
- () => ensureOpenAlongActive(props.item),
217
- { immediate: true },
218
- )
219
-
220
- function toggle() {
221
- const next = new Set(openIds!.value)
222
- const willOpen = !next.has(props.item.id)
223
- if (willOpen) {
224
- next.add(props.item.id)
225
- deepFocusId.value = props.item.id
226
- } else {
227
- next.delete(props.item.id)
228
- const focus = deepFocusId.value
229
- if (focus != null && containsId(props.item, focus)) {
230
- deepFocusId.value = null
231
- }
232
- }
233
- openIds!.value = next
234
- }
235
-
236
- function clearMiniTimer() {
237
- if (miniTimer) {
238
- clearTimeout(miniTimer)
239
- miniTimer = null
240
- }
241
- }
242
-
243
- function openMiniFlyout() {
244
- clearMiniTimer()
245
- const el = miniRef.value
246
- if (!el) return
247
- const btn = el.querySelector('.vm-nav-item') as HTMLElement | null
248
- const btnRect = (btn || el).getBoundingClientRect()
249
- // 按钮在侧栏内居中,必须用侧栏右边缘,否则浮窗会盖住图标
250
- const sidebar = el.closest('.vm-sidebar') as HTMLElement | null
251
- const sideRight = sidebar?.getBoundingClientRect().right ?? btnRect.right
252
- miniAnchor.value = {
253
- top: Math.round(btnRect.top),
254
- left: Math.round(sideRight + 12),
255
- }
256
- miniOpen.value = true
257
- }
258
-
259
- function scheduleCloseMini() {
260
- clearMiniTimer()
261
- miniTimer = setTimeout(() => {
262
- miniOpen.value = false
263
- }, 140)
264
- }
265
-
266
- function closeMiniNow() {
267
- clearMiniTimer()
268
- miniOpen.value = false
269
- }
270
-
271
- onBeforeUnmount(() => clearMiniTimer())
272
-
273
- const pad = computed(() => 12 + props.depth * 14)
274
- </script>
275
-
276
- <style lang="scss" scoped>
277
- $open: 14px;
278
- $gutter: 10px;
279
-
280
- .vm-nav-mini {
281
- position: relative;
282
- }
283
-
284
- .vm-nav-item {
285
- position: relative;
286
- z-index: 0;
287
- display: flex;
288
- width: 100%;
289
- height: 44px;
290
- align-items: center;
291
- gap: 10px;
292
- box-sizing: border-box;
293
- padding-right: 12px;
294
- margin: 0;
295
- border: none;
296
- border-radius: 12px;
297
- background: transparent;
298
- color: rgba(255, 255, 255, 0.95);
299
- font-size: 14px;
300
- font-weight: 700 !important;
301
- line-height: 1;
302
- text-decoration: none;
303
- cursor: pointer;
304
- transition: none;
305
-
306
- .vm-nav-item__icon-slot,
307
- .vm-nav-item__icon,
308
- .vm-nav-item__label,
309
- .vm-nav-item__arrow {
310
- position: relative;
311
- z-index: 1;
312
- }
313
-
314
- &:hover:not(.is-active)::before {
315
- content: '';
316
- position: absolute;
317
- inset: 0;
318
- z-index: 0;
319
- border-radius: 12px;
320
- background: rgba(255, 255, 255, 0.16);
321
- pointer-events: none;
322
- }
323
-
324
- &:active:not(.is-active)::before {
325
- background: rgba(255, 255, 255, 0.24);
326
- }
327
-
328
- &.is-collapsed {
329
- width: 44px;
330
- height: 44px;
331
- justify-content: center;
332
- padding: 0;
333
- margin: 0 auto;
334
- border-radius: 12px;
335
- }
336
-
337
- /* 桌面展开:右侧开口贴内容区 */
338
- &.is-active:not(.is-collapsed):not(.is-mobile) {
339
- z-index: 1;
340
- width: calc(100% + #{$gutter});
341
- margin-right: -$gutter;
342
- border-radius: $open 0 0 $open;
343
- background: var(--background);
344
- color: var(--brand);
345
-
346
- .vm-nav-item__icon,
347
- .vm-nav-item__label {
348
- color: var(--brand);
349
- }
350
-
351
- .vm-nav-item__notch {
352
- display: block;
353
- }
354
- }
355
-
356
- /* 移动端抽屉:内嵌白底圆角,避免开口溢出 Sheet */
357
- &.is-active.is-mobile:not(.is-collapsed) {
358
- z-index: 1;
359
- width: 100%;
360
- margin-right: 0;
361
- border-radius: 12px;
362
- background: #fff;
363
- color: var(--brand);
364
-
365
- .vm-nav-item__icon,
366
- .vm-nav-item__label {
367
- color: var(--brand);
368
- }
369
-
370
- .vm-nav-item__notch {
371
- display: none;
372
- }
373
- }
374
-
375
- &.is-active.is-collapsed,
376
- &.is-branch.is-collapsed {
377
- width: 44px;
378
- margin-right: auto;
379
- background: #fff;
380
- color: var(--brand);
381
- box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
382
-
383
- .vm-nav-item__notch {
384
- display: none;
385
- }
386
-
387
- .vm-nav-item__icon {
388
- color: var(--brand);
389
- }
390
- }
391
-
392
- &.is-dir.is-branch:not(.is-active):not(.is-collapsed) {
393
- color: #fff;
394
- }
395
- }
396
-
397
- .vm-nav-item__notch {
398
- display: none;
399
- position: absolute;
400
- right: 0;
401
- width: $open;
402
- height: $open;
403
- background: transparent;
404
- pointer-events: none;
405
-
406
- &--top {
407
- top: -$open;
408
- border-bottom-right-radius: $open;
409
- box-shadow: 5px 5px 0 5px var(--background);
410
- }
411
-
412
- &--bottom {
413
- bottom: -$open;
414
- border-top-right-radius: $open;
415
- box-shadow: 5px -5px 0 5px var(--background);
416
- }
417
- }
418
-
419
- .vm-nav-item__icon-slot {
420
- display: inline-flex;
421
- width: 18px;
422
- height: 18px;
423
- flex-shrink: 0;
424
- align-items: center;
425
- justify-content: center;
426
- }
427
-
428
- .vm-nav-item__icon {
429
- display: inline-flex;
430
- flex-shrink: 0;
431
- width: 18px;
432
- height: 18px;
433
- align-items: center;
434
- justify-content: center;
435
- font-size: 18px;
436
- line-height: 1;
437
- opacity: 0.95;
438
- }
439
-
440
- .vm-nav-item__label {
441
- display: inline-flex;
442
- flex: 1;
443
- min-width: 0;
444
- align-items: center;
445
- overflow: hidden;
446
- text-overflow: ellipsis;
447
- white-space: nowrap;
448
- text-align: left;
449
- font-size: 14px;
450
- font-weight: 700;
451
- line-height: 1;
452
- font-synthesis: weight;
453
- }
454
-
455
- .vm-nav-item__arrow {
456
- display: inline-flex;
457
- flex-shrink: 0;
458
- width: 18px;
459
- height: 18px;
460
- margin-right: 2px;
461
- align-items: center;
462
- justify-content: center;
463
- font-size: 16px;
464
- line-height: 1;
465
- opacity: 0.75;
466
- transition: transform 0.2s ease;
467
-
468
- &.is-open {
469
- transform: rotate(180deg);
470
- }
471
- }
472
-
473
- .vm-nav-group {
474
- display: flex;
475
- flex-direction: column;
476
- gap: 4px;
477
- padding: 0;
478
- border-radius: 12px;
479
-
480
- &.is-deep {
481
- background: rgba(0, 0, 0, 0.2);
482
- }
483
- }
484
-
485
- .vm-nav-group__children {
486
- display: flex;
487
- flex-direction: column;
488
- gap: 4px;
489
- }
490
- </style>
@@ -1,104 +0,0 @@
1
- <template>
2
- <nav class="vm-nav" :class="{ 'is-collapsed': collapsed && !mobile }">
3
- <vm-sidebar-nav-item
4
- v-for="item in visibleMenus"
5
- :key="item.id"
6
- :item="item"
7
- :depth="0"
8
- :parent-id="null"
9
- :collapsed="collapsed && !mobile"
10
- :mobile="mobile"
11
- />
12
- </nav>
13
- </template>
14
-
15
- <script setup lang="ts">
16
- defineOptions({ name: 'vm-sidebar-nav' })
17
-
18
- const props = defineProps<{
19
- menus: MenuTreeNode[]
20
- collapsed?: boolean
21
- /** 移动端抽屉:禁用右侧开口选中态 */
22
- mobile?: boolean
23
- }>()
24
-
25
- const route = useRoute()
26
- const openIds = ref(new Set<number>())
27
- /** 点击瞬间先开口,等路由落地后再清,避免闪烁 */
28
- const pendingLeafId = ref<number | null>(null)
29
- /**
30
- * 加深背景的目录 id:
31
- * - 展开目录 → 该目录
32
- * - 切到页面 → 改为该页面的直接父目录
33
- */
34
- const deepFocusId = ref<number | null>(null)
35
-
36
- provide('sidebar-open-ids', openIds)
37
- provide('sidebar-pending-leaf', pendingLeafId)
38
- provide('sidebar-deep-focus', deepFocusId)
39
-
40
- const visibleMenus = computed(() =>
41
- props.menus.filter((m) => m.isShow !== false && Number(m.type) !== 2),
42
- )
43
-
44
- function visibleChildren(item: MenuTreeNode) {
45
- return (item.children ?? []).filter(
46
- (c) => c.isShow !== false && Number(c.type) !== 2,
47
- )
48
- }
49
-
50
- function isActivePath(routerPath: string) {
51
- return route.path === routerPath || route.path.startsWith(`${routerPath}/`)
52
- }
53
-
54
- /** 当前激活叶子页的直接父目录 id */
55
- function findActiveLeafParent(
56
- items: MenuTreeNode[],
57
- parentId: number | null = null,
58
- ): number | null | undefined {
59
- for (const item of items) {
60
- const kids = visibleChildren(item)
61
- if (Number(item.type) === 1 && item.router && isActivePath(item.router)) {
62
- return parentId
63
- }
64
- if (kids.length) {
65
- const hit = findActiveLeafParent(kids, item.id)
66
- if (hit !== undefined) return hit
67
- }
68
- }
69
- return undefined
70
- }
71
-
72
- watch(
73
- () => [route.path, props.menus] as const,
74
- () => {
75
- pendingLeafId.value = null
76
- const parentId = findActiveLeafParent(visibleMenus.value)
77
- // undefined = 未找到叶子;null = 顶级叶子无父级
78
- if (parentId !== undefined) {
79
- deepFocusId.value = parentId
80
- }
81
- },
82
- { immediate: true, deep: true },
83
- )
84
- </script>
85
-
86
- <style lang="scss" scoped>
87
- $gutter: 10px;
88
-
89
- .vm-nav {
90
- display: flex;
91
- flex: 1;
92
- flex-direction: column;
93
- gap: 4px;
94
- overflow-x: hidden;
95
- overflow-y: auto;
96
- padding: 8px $gutter 24px;
97
-
98
- &.is-collapsed {
99
- align-items: center;
100
- overflow: visible;
101
- padding: 8px $gutter 24px;
102
- }
103
- }
104
- </style>