spoko-design-system 1.3.6 → 1.4.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/CHANGELOG.md +12 -0
- package/package.json +32 -32
- package/src/components/Category/CategoryDetails.astro +3 -9
- package/src/components/MainColors.vue +91 -9
- package/src/components/Modal.astro +132 -12
- package/src/components/SlimBanner.vue +161 -52
- package/src/pages/components/modal.mdx +178 -30
- package/src/pages/components/slimbanner.mdx +232 -7
- package/src/pages/core/colors.mdx +13 -2
- package/src/pages/core/grid.mdx +104 -0
- package/src/styles/base/grid.css +22 -0
- package/src/utils/text.ts +5 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## [1.4.0](https://github.com/polo-blue/sds/compare/v1.3.7...v1.4.0) (2025-10-20)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
* enhance components with flexible layouts and click-to-copy functionality ([3dfa718](https://github.com/polo-blue/sds/commit/3dfa718f7f4355c0beed4032e84d01a2c2c4a547))
|
|
6
|
+
|
|
7
|
+
## [1.3.7](https://github.com/polo-blue/sds/compare/v1.3.6...v1.3.7) (2025-10-08)
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* simplify CategoryDetails to use API paths directly ([28831c9](https://github.com/polo-blue/sds/commit/28831c9284c5e9ff484f7e51947cec95b2f41427))
|
|
12
|
+
|
|
1
13
|
## [1.3.6](https://github.com/polo-blue/sds/compare/v1.3.5...v1.3.6) (2025-10-07)
|
|
2
14
|
|
|
3
15
|
### Bug Fixes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spoko-design-system",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"main": "./index.ts",
|
|
6
6
|
"module": "./index.ts",
|
|
@@ -51,33 +51,33 @@
|
|
|
51
51
|
"spoko design system"
|
|
52
52
|
],
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@algolia/client-search": "^5.40.
|
|
55
|
-
"@astrojs/mdx": "^4.3.
|
|
56
|
-
"@astrojs/node": "^9.
|
|
54
|
+
"@algolia/client-search": "^5.40.1",
|
|
55
|
+
"@astrojs/mdx": "^4.3.7",
|
|
56
|
+
"@astrojs/node": "^9.5.0",
|
|
57
57
|
"@astrojs/sitemap": "^3.6.0",
|
|
58
|
-
"@astrojs/ts-plugin": "^1.10.
|
|
58
|
+
"@astrojs/ts-plugin": "^1.10.5",
|
|
59
59
|
"@astrojs/vue": "^5.1.1",
|
|
60
|
-
"@docsearch/css": "^4.
|
|
60
|
+
"@docsearch/css": "^4.2.0",
|
|
61
61
|
"@iconify-json/ant-design": "^1.2.5",
|
|
62
62
|
"@iconify-json/bi": "^1.2.6",
|
|
63
63
|
"@iconify-json/bx": "^1.2.2",
|
|
64
|
-
"@iconify-json/carbon": "^1.2.
|
|
65
|
-
"@iconify-json/circle-flags": "^1.2.
|
|
64
|
+
"@iconify-json/carbon": "^1.2.14",
|
|
65
|
+
"@iconify-json/circle-flags": "^1.2.10",
|
|
66
66
|
"@iconify-json/ei": "^1.2.2",
|
|
67
67
|
"@iconify-json/el": "^1.2.2",
|
|
68
68
|
"@iconify-json/eos-icons": "^1.2.4",
|
|
69
69
|
"@iconify-json/et": "^1.2.1",
|
|
70
70
|
"@iconify-json/flowbite": "^1.2.7",
|
|
71
|
-
"@iconify-json/fluent": "^1.2.
|
|
71
|
+
"@iconify-json/fluent": "^1.2.33",
|
|
72
72
|
"@iconify-json/fluent-emoji": "1.2.6",
|
|
73
73
|
"@iconify-json/ic": "^1.2.4",
|
|
74
74
|
"@iconify-json/icon-park-outline": "^1.2.4",
|
|
75
75
|
"@iconify-json/la": "^1.2.1",
|
|
76
|
-
"@iconify-json/lucide": "^1.2.
|
|
77
|
-
"@iconify-json/material-symbols-light": "^1.2.
|
|
76
|
+
"@iconify-json/lucide": "^1.2.70",
|
|
77
|
+
"@iconify-json/material-symbols-light": "^1.2.42",
|
|
78
78
|
"@iconify-json/mdi": "^1.2.3",
|
|
79
79
|
"@iconify-json/noto-v1": "^1.2.5",
|
|
80
|
-
"@iconify-json/octicon": "^1.2.
|
|
80
|
+
"@iconify-json/octicon": "^1.2.16",
|
|
81
81
|
"@iconify-json/ph": "^1.2.2",
|
|
82
82
|
"@iconify-json/simple-icons": "^1.2.54",
|
|
83
83
|
"@iconify-json/streamline": "^1.2.5",
|
|
@@ -85,19 +85,19 @@
|
|
|
85
85
|
"@iconify-json/streamline-freehand-color": "^1.2.2",
|
|
86
86
|
"@iconify-json/system-uicons": "^1.2.4",
|
|
87
87
|
"@iconify-json/uil": "^1.2.3",
|
|
88
|
-
"@iconify-json/vscode-icons": "^1.2.
|
|
89
|
-
"@iconify/json": "^2.2.
|
|
88
|
+
"@iconify-json/vscode-icons": "^1.2.32",
|
|
89
|
+
"@iconify/json": "^2.2.397",
|
|
90
90
|
"@iconify/vue": "^5.0.0",
|
|
91
91
|
"@playform/compress": "^0.2.0",
|
|
92
92
|
"@playform/inline": "^0.1.2",
|
|
93
|
-
"@unocss/astro": "66.5.
|
|
94
|
-
"@unocss/preset-attributify": "66.5.
|
|
95
|
-
"@unocss/preset-typography": "66.5.
|
|
96
|
-
"@unocss/preset-uno": "66.5.
|
|
97
|
-
"@unocss/preset-web-fonts": "66.5.
|
|
98
|
-
"@unocss/preset-wind": "66.5.
|
|
99
|
-
"@unocss/reset": "66.5.
|
|
100
|
-
"@vite-pwa/astro": "^1.1.
|
|
93
|
+
"@unocss/astro": "66.5.4",
|
|
94
|
+
"@unocss/preset-attributify": "66.5.4",
|
|
95
|
+
"@unocss/preset-typography": "66.5.4",
|
|
96
|
+
"@unocss/preset-uno": "66.5.4",
|
|
97
|
+
"@unocss/preset-web-fonts": "66.5.4",
|
|
98
|
+
"@unocss/preset-wind": "66.5.4",
|
|
99
|
+
"@unocss/reset": "66.5.4",
|
|
100
|
+
"@vite-pwa/astro": "^1.1.1",
|
|
101
101
|
"@vueuse/core": "^13.9.0",
|
|
102
102
|
"astro-icon": "^1.1.5",
|
|
103
103
|
"astro-meta-tags": "^0.4.0",
|
|
@@ -106,30 +106,30 @@
|
|
|
106
106
|
"astro-remote": "^0.3.4",
|
|
107
107
|
"dotenv": "^17.2.3",
|
|
108
108
|
"swiper": "^12.0.2",
|
|
109
|
-
"unocss": "66.5.
|
|
109
|
+
"unocss": "66.5.4",
|
|
110
110
|
"vue": "^3.5.22"
|
|
111
111
|
},
|
|
112
112
|
"devDependencies": {
|
|
113
113
|
"@semantic-release/changelog": "^6.0.3",
|
|
114
114
|
"@semantic-release/git": "^10.0.1",
|
|
115
115
|
"@types/gtag.js": "^0.0.20",
|
|
116
|
-
"@types/node": "^24.
|
|
117
|
-
"@typescript-eslint/eslint-plugin": "^8.46.
|
|
118
|
-
"@typescript-eslint/parser": "^8.46.
|
|
119
|
-
"@unocss/transformer-variant-group": "66.5.
|
|
116
|
+
"@types/node": "^24.8.1",
|
|
117
|
+
"@typescript-eslint/eslint-plugin": "^8.46.1",
|
|
118
|
+
"@typescript-eslint/parser": "^8.46.1",
|
|
119
|
+
"@unocss/transformer-variant-group": "66.5.4",
|
|
120
120
|
"@vitejs/plugin-vue": "^6.0.1",
|
|
121
121
|
"@vue/compiler-sfc": "^3.5.22",
|
|
122
122
|
"@vue/eslint-config-typescript": "^14.6.0",
|
|
123
|
-
"astro": "^5.14.
|
|
123
|
+
"astro": "^5.14.6",
|
|
124
124
|
"conventional-changelog-conventionalcommits": "^9.1.0",
|
|
125
|
-
"eslint": "^9.
|
|
125
|
+
"eslint": "^9.38.0",
|
|
126
126
|
"eslint-plugin-astro": "^1.3.1",
|
|
127
|
-
"eslint-plugin-vue": "^10.5.
|
|
127
|
+
"eslint-plugin-vue": "^10.5.1",
|
|
128
128
|
"prettier": "^3.6.2",
|
|
129
129
|
"prettier-plugin-astro": "^0.14.1",
|
|
130
|
-
"semantic-release": "^
|
|
130
|
+
"semantic-release": "^25.0.1",
|
|
131
131
|
"unocss": "^0.65.0",
|
|
132
|
-
"vite": "^7.1.
|
|
132
|
+
"vite": "^7.1.11"
|
|
133
133
|
},
|
|
134
134
|
"packageManager": "pnpm@10.17.1",
|
|
135
135
|
"pnpm": {
|
|
@@ -14,15 +14,9 @@ const {
|
|
|
14
14
|
locale,
|
|
15
15
|
} = Astro.props;
|
|
16
16
|
|
|
17
|
-
// Use paths directly from API - they already include locale
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
const categoryPath = category?.path
|
|
21
|
-
? ensureLeadingSlash(category.path)
|
|
22
|
-
: `/${category.slug}/`;
|
|
23
|
-
const subcategoryPath = subcategory?.path
|
|
24
|
-
? ensureLeadingSlash(subcategory.path)
|
|
25
|
-
: `/${category.slug}/${subcategory?.slug}/`;
|
|
17
|
+
// Use paths directly from API - they already include locale and proper slashes
|
|
18
|
+
const categoryPath = category?.path || `/${category.slug}/`;
|
|
19
|
+
const subcategoryPath = subcategory?.path || `/${category.slug}/${subcategory?.slug}/`;
|
|
26
20
|
---
|
|
27
21
|
|
|
28
22
|
<div
|
|
@@ -1,8 +1,30 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { ref } from 'vue';
|
|
2
3
|
import { colors } from './../../uno-config/theme/colors';
|
|
3
4
|
|
|
4
5
|
// Get color categories
|
|
5
6
|
const colorCategories = Object.entries(colors);
|
|
7
|
+
|
|
8
|
+
// Track copied state for visual feedback
|
|
9
|
+
const copiedItem = ref<string | null>(null);
|
|
10
|
+
|
|
11
|
+
const copyToClipboard = async (text: string, id: string) => {
|
|
12
|
+
try {
|
|
13
|
+
await navigator.clipboard.writeText(text);
|
|
14
|
+
copiedItem.value = id;
|
|
15
|
+
setTimeout(() => {
|
|
16
|
+
copiedItem.value = null;
|
|
17
|
+
}, 2000);
|
|
18
|
+
} catch (err) {
|
|
19
|
+
console.error('Failed to copy:', err);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const getColorClass = (category: string, name: string) => {
|
|
24
|
+
// Generate Tailwind/UnoCSS class name
|
|
25
|
+
const colorName = name === 'default' ? category : `${category}-${name}`;
|
|
26
|
+
return colorName;
|
|
27
|
+
};
|
|
6
28
|
</script>
|
|
7
29
|
|
|
8
30
|
<template>
|
|
@@ -11,22 +33,82 @@ const colorCategories = Object.entries(colors);
|
|
|
11
33
|
v-for="[category, shades] in colorCategories"
|
|
12
34
|
:key="category"
|
|
13
35
|
>
|
|
14
|
-
<h3 class="capitalize">
|
|
36
|
+
<h3 class="capitalize text-xl font-bold mb-4">
|
|
15
37
|
{{ category }}
|
|
16
38
|
</h3>
|
|
17
|
-
<div class="grid grid-cols-
|
|
39
|
+
<div class="grid grid-cols-2 md:grid-cols-4 xl:grid-cols-5 gap-x-6 gap-y-10">
|
|
18
40
|
<div
|
|
19
41
|
v-for="(value, name) in shades"
|
|
20
42
|
:key="name"
|
|
21
|
-
class="
|
|
43
|
+
class="group relative"
|
|
22
44
|
>
|
|
23
|
-
|
|
24
|
-
|
|
45
|
+
<!-- Color Swatch -->
|
|
46
|
+
<button
|
|
47
|
+
class="w-full h-10 rounded-lg shadow-md transition-all duration-200 hover:shadow-xl hover:scale-105 cursor-pointer relative overflow-hidden"
|
|
25
48
|
:style="`background: ${value}`"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
49
|
+
:title="`Click to copy hex: ${value}`"
|
|
50
|
+
@click="copyToClipboard(value, `${category}-${name}-hex`)"
|
|
51
|
+
>
|
|
52
|
+
<!-- Copied indicator -->
|
|
53
|
+
<div
|
|
54
|
+
v-if="copiedItem === `${category}-${name}-hex`"
|
|
55
|
+
class="absolute inset-0 bg-black/50 flex items-center justify-center text-white font-bold text-sm"
|
|
56
|
+
>
|
|
57
|
+
✓ Copied!
|
|
58
|
+
</div>
|
|
59
|
+
</button>
|
|
60
|
+
|
|
61
|
+
<!-- Color Info -->
|
|
62
|
+
<div class="mt-2 space-y-1">
|
|
63
|
+
<!-- Color Name -->
|
|
64
|
+
<div class="text-sm font-medium text-gray-700 text-center">
|
|
65
|
+
{{ name }}
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<!-- Copy Buttons -->
|
|
69
|
+
<div class="flex gap-1 text-xs font-mono">
|
|
70
|
+
<!-- Copy Hex -->
|
|
71
|
+
<button
|
|
72
|
+
class="flex-1 px-2 py-1 bg-gray-100 hover:bg-gray-200 rounded transition-colors text-gray-600 hover:text-gray-900 relative"
|
|
73
|
+
:title="`Copy hex: ${value}`"
|
|
74
|
+
@click="copyToClipboard(value, `${category}-${name}-hex`)"
|
|
75
|
+
>
|
|
76
|
+
<span
|
|
77
|
+
v-if="copiedItem === `${category}-${name}-hex`"
|
|
78
|
+
class="text-green-600"
|
|
79
|
+
>✓</span>
|
|
80
|
+
<span
|
|
81
|
+
v-else
|
|
82
|
+
class="uppercase text-xs"
|
|
83
|
+
>{{ value }}</span>
|
|
84
|
+
</button>
|
|
85
|
+
|
|
86
|
+
<!-- Copy Class Name (text-*) -->
|
|
87
|
+
<button
|
|
88
|
+
class="flex-1 px-2 py-1 bg-blue-100 hover:bg-blue-200 rounded transition-colors text-blue-700 hover:text-blue-900 relative"
|
|
89
|
+
:title="`Copy class: text-${getColorClass(category, name)}`"
|
|
90
|
+
@click="copyToClipboard(`text-${getColorClass(category, name)}`, `${category}-${name}-class`)"
|
|
91
|
+
>
|
|
92
|
+
<span
|
|
93
|
+
v-if="copiedItem === `${category}-${name}-class`"
|
|
94
|
+
class="text-green-600"
|
|
95
|
+
>✓</span>
|
|
96
|
+
<span v-else>text-</span>
|
|
97
|
+
</button>
|
|
98
|
+
|
|
99
|
+
<!-- Copy Class Name (bg-*) -->
|
|
100
|
+
<button
|
|
101
|
+
class="flex-1 px-2 py-1 bg-purple-100 hover:bg-purple-200 rounded transition-colors text-purple-700 hover:text-purple-900 relative"
|
|
102
|
+
:title="`Copy class: bg-${getColorClass(category, name)}`"
|
|
103
|
+
@click="copyToClipboard(`bg-${getColorClass(category, name)}`, `${category}-${name}-bg`)"
|
|
104
|
+
>
|
|
105
|
+
<span
|
|
106
|
+
v-if="copiedItem === `${category}-${name}-bg`"
|
|
107
|
+
class="text-green-600"
|
|
108
|
+
>✓</span>
|
|
109
|
+
<span v-else>bg-</span>
|
|
110
|
+
</button>
|
|
111
|
+
</div>
|
|
30
112
|
</div>
|
|
31
113
|
</div>
|
|
32
114
|
</div>
|
|
@@ -1,11 +1,40 @@
|
|
|
1
1
|
---
|
|
2
|
-
const { id, open } = Astro.props;
|
|
3
2
|
import Button from '../components/Button.vue';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
id: string;
|
|
6
|
+
open?: string;
|
|
7
|
+
title?: string;
|
|
8
|
+
maxWidth?: string;
|
|
9
|
+
showXButton?: boolean;
|
|
10
|
+
showTrigger?: boolean;
|
|
11
|
+
|
|
12
|
+
// Action buttons
|
|
13
|
+
showActions?: boolean;
|
|
14
|
+
cancelText?: string;
|
|
15
|
+
confirmText?: string;
|
|
16
|
+
confirmPrimary?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const {
|
|
20
|
+
id,
|
|
21
|
+
open = 'Open modal',
|
|
22
|
+
title,
|
|
23
|
+
maxWidth = '600px',
|
|
24
|
+
showXButton = true,
|
|
25
|
+
showTrigger = true,
|
|
26
|
+
showActions = false,
|
|
27
|
+
cancelText = 'Cancel',
|
|
28
|
+
confirmText = 'Confirm',
|
|
29
|
+
confirmPrimary = true,
|
|
30
|
+
} = Astro.props;
|
|
4
31
|
---
|
|
5
32
|
|
|
6
|
-
<style>
|
|
33
|
+
<style define:vars={{ maxWidth }}>
|
|
7
34
|
dialog {
|
|
8
|
-
@apply fixed top-0 left-0 right-0 bottom-0;
|
|
35
|
+
@apply fixed top-0 left-0 right-0 bottom-0 rounded-lg shadow-xl;
|
|
36
|
+
max-width: var(--maxWidth);
|
|
37
|
+
width: calc(100% - 2rem);
|
|
9
38
|
|
|
10
39
|
&::backdrop {
|
|
11
40
|
@apply bg-slate-medium/50 fixed;
|
|
@@ -15,20 +44,111 @@ import Button from '../components/Button.vue';
|
|
|
15
44
|
left: 0px;
|
|
16
45
|
}
|
|
17
46
|
}
|
|
47
|
+
|
|
48
|
+
.modal-header {
|
|
49
|
+
@apply relative pb-4 mb-4 border-b border-gray-300;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.modal-close-x {
|
|
53
|
+
@apply absolute top-0 right-0 w-8 h-8 flex items-center justify-center rounded-full hover:bg-gray-200 transition-colors cursor-pointer border-0 bg-transparent;
|
|
54
|
+
|
|
55
|
+
&::before,
|
|
56
|
+
&::after {
|
|
57
|
+
content: '';
|
|
58
|
+
position: absolute;
|
|
59
|
+
width: 16px;
|
|
60
|
+
height: 2px;
|
|
61
|
+
background-color: #666;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
&::before {
|
|
65
|
+
transform: rotate(45deg);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
&::after {
|
|
69
|
+
transform: rotate(-45deg);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
&:hover::before,
|
|
73
|
+
&:hover::after {
|
|
74
|
+
background-color: #000;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.modal-content {
|
|
79
|
+
@apply mb-4;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.modal-actions {
|
|
83
|
+
@apply flex gap-3 justify-end pt-4 border-t border-gray-300;
|
|
84
|
+
}
|
|
18
85
|
</style>
|
|
19
86
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
>
|
|
87
|
+
{showTrigger && (
|
|
88
|
+
<Button
|
|
89
|
+
primary
|
|
90
|
+
onclick={`window.${id}.showModal()`}
|
|
91
|
+
>{open}</Button
|
|
92
|
+
>
|
|
93
|
+
)}
|
|
25
94
|
|
|
26
95
|
<dialog
|
|
27
96
|
id={id}
|
|
28
97
|
class="p-6"
|
|
29
98
|
>
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
<slot name="
|
|
33
|
-
|
|
99
|
+
<div class="modal-header">
|
|
100
|
+
{title && <h2 class="text-2xl font-bold pr-8">{title}</h2>}
|
|
101
|
+
<slot name="header" />
|
|
102
|
+
|
|
103
|
+
{showXButton && (
|
|
104
|
+
<form method="dialog" class="inline">
|
|
105
|
+
<button
|
|
106
|
+
class="modal-close-x"
|
|
107
|
+
aria-label="Close"
|
|
108
|
+
type="submit"
|
|
109
|
+
/>
|
|
110
|
+
</form>
|
|
111
|
+
)}
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<div class="modal-content">
|
|
115
|
+
<slot name="main" />
|
|
116
|
+
<slot />
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
{showActions && (
|
|
120
|
+
<div class="modal-actions">
|
|
121
|
+
<slot name="actions">
|
|
122
|
+
<form method="dialog" class="contents">
|
|
123
|
+
<Button>{cancelText}</Button>
|
|
124
|
+
</form>
|
|
125
|
+
<Button primary={confirmPrimary} onclick={`document.getElementById('${id}').dispatchEvent(new CustomEvent('confirm', { detail: { id: '${id}' } }))`}>
|
|
126
|
+
{confirmText}
|
|
127
|
+
</Button>
|
|
128
|
+
</slot>
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
131
|
+
|
|
132
|
+
{!showActions && (
|
|
133
|
+
<form method="dialog">
|
|
134
|
+
<slot name="close" />
|
|
135
|
+
</form>
|
|
136
|
+
)}
|
|
34
137
|
</dialog>
|
|
138
|
+
|
|
139
|
+
<script define:vars={{ id }}>
|
|
140
|
+
// Close on backdrop click
|
|
141
|
+
const dialog = document.getElementById(id);
|
|
142
|
+
dialog?.addEventListener('click', (e) => {
|
|
143
|
+
if (e.target === dialog) {
|
|
144
|
+
dialog.close();
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Close on Escape key
|
|
149
|
+
dialog?.addEventListener('keydown', (e) => {
|
|
150
|
+
if (e.key === 'Escape') {
|
|
151
|
+
dialog.close();
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
</script>
|
|
@@ -1,73 +1,182 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
-
import { ref } from 'vue';
|
|
2
|
+
import { ref, onMounted } from 'vue';
|
|
3
3
|
|
|
4
4
|
const props = defineProps({
|
|
5
|
+
// Primary state (visible by default)
|
|
6
|
+
message: {
|
|
7
|
+
type: String,
|
|
8
|
+
default: 'We stand with our friends and colleagues in Ukraine. To support Ukraine in their time of need visit',
|
|
9
|
+
},
|
|
10
|
+
linkText: {
|
|
11
|
+
type: String,
|
|
12
|
+
default: 'this page',
|
|
13
|
+
},
|
|
14
|
+
linkUrl: {
|
|
15
|
+
type: String,
|
|
16
|
+
default: 'https://polo.blue/support-ukraine/',
|
|
17
|
+
},
|
|
18
|
+
linkTitle: {
|
|
19
|
+
type: String,
|
|
20
|
+
default: '',
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
// Toggle state (shows when close button is clicked)
|
|
24
|
+
toggleMessage: {
|
|
25
|
+
type: String,
|
|
26
|
+
default: '',
|
|
27
|
+
},
|
|
28
|
+
toggleBgClass: {
|
|
29
|
+
type: String,
|
|
30
|
+
default: 'bg-black',
|
|
31
|
+
},
|
|
32
|
+
toggleTextClass: {
|
|
33
|
+
type: String,
|
|
34
|
+
default: 'text-white',
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
// Close button
|
|
5
38
|
showCloseButton: {
|
|
6
39
|
type: Boolean,
|
|
7
40
|
default: true,
|
|
8
|
-
|
|
41
|
+
},
|
|
42
|
+
closeButtonAriaLabel: {
|
|
43
|
+
type: String,
|
|
44
|
+
default: 'Toggle',
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
// Show flag icon (Ukraine flag by default)
|
|
48
|
+
showIcon: {
|
|
49
|
+
type: Boolean,
|
|
50
|
+
default: false,
|
|
51
|
+
},
|
|
52
|
+
iconClass: {
|
|
53
|
+
type: String,
|
|
54
|
+
default: 'inline-block text-4xl w-6 h-3.5 min-w-[1.25rem] mr-3 bg-gradient-to-b stops-[#0057b7_50%,50%,#ffd700_100%]',
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
// LocalStorage persistence
|
|
58
|
+
persistClose: {
|
|
59
|
+
type: Boolean,
|
|
60
|
+
default: false,
|
|
61
|
+
},
|
|
62
|
+
storageKey: {
|
|
63
|
+
type: String,
|
|
64
|
+
default: 'slimbanner-closed',
|
|
65
|
+
},
|
|
66
|
+
// Expiration in days (0 = never expires)
|
|
67
|
+
expirationDays: {
|
|
68
|
+
type: Number,
|
|
69
|
+
default: 30,
|
|
9
70
|
},
|
|
10
71
|
});
|
|
72
|
+
|
|
11
73
|
const isShow = ref(true);
|
|
74
|
+
const isHidden = ref(false);
|
|
75
|
+
|
|
76
|
+
// Check localStorage on mount
|
|
77
|
+
onMounted(() => {
|
|
78
|
+
if (props.persistClose && typeof window !== 'undefined') {
|
|
79
|
+
const stored = localStorage.getItem(props.storageKey);
|
|
80
|
+
if (stored) {
|
|
81
|
+
try {
|
|
82
|
+
const data = JSON.parse(stored);
|
|
83
|
+
const now = new Date().getTime();
|
|
84
|
+
|
|
85
|
+
// Check if expired
|
|
86
|
+
if (props.expirationDays > 0 && data.expires && now > data.expires) {
|
|
87
|
+
// Expired, remove from storage
|
|
88
|
+
localStorage.removeItem(props.storageKey);
|
|
89
|
+
} else if (data.closed) {
|
|
90
|
+
// Still valid, keep hidden
|
|
91
|
+
isHidden.value = true;
|
|
92
|
+
}
|
|
93
|
+
} catch (e) {
|
|
94
|
+
// Invalid data, remove it
|
|
95
|
+
localStorage.removeItem(props.storageKey);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
12
101
|
const toggleVisibility = () => {
|
|
13
102
|
isShow.value = !isShow.value;
|
|
14
103
|
};
|
|
104
|
+
|
|
105
|
+
const closePermanently = () => {
|
|
106
|
+
isHidden.value = true;
|
|
107
|
+
|
|
108
|
+
if (props.persistClose && typeof window !== 'undefined') {
|
|
109
|
+
const data = {
|
|
110
|
+
closed: true,
|
|
111
|
+
timestamp: new Date().getTime(),
|
|
112
|
+
expires: props.expirationDays > 0
|
|
113
|
+
? new Date().getTime() + (props.expirationDays * 24 * 60 * 60 * 1000)
|
|
114
|
+
: null,
|
|
115
|
+
};
|
|
116
|
+
localStorage.setItem(props.storageKey, JSON.stringify(data));
|
|
117
|
+
}
|
|
118
|
+
};
|
|
15
119
|
</script>
|
|
16
120
|
|
|
17
121
|
<template>
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
<span
|
|
24
|
-
class="inline-block text-4xl w-6 h-3.5 min-w-[1.25rem] mr-3 bg-gradient-to-b stops-[#0057b7_50%,50%,#ffd700_100%]"
|
|
25
|
-
/>
|
|
26
|
-
<span class="leading-none"
|
|
27
|
-
><span
|
|
28
|
-
data-text="We stand with our friends and colleagues in Ukraine. To support Ukraine in their time of need visit "
|
|
29
|
-
/>
|
|
30
|
-
<a
|
|
31
|
-
href="https://polo.blue/support-ukraine/"
|
|
32
|
-
target="_blank"
|
|
33
|
-
rel="noopener"
|
|
34
|
-
title="Support Ukraine"
|
|
35
|
-
class="underline underline-offset-2 hover:text-blue-wrc"
|
|
36
|
-
>this page</a
|
|
37
|
-
>.
|
|
38
|
-
</span>
|
|
39
|
-
|
|
40
|
-
<button
|
|
41
|
-
v-if="props.showCloseButton"
|
|
42
|
-
class="btn-close text-white"
|
|
43
|
-
aria-label="Toggle"
|
|
44
|
-
@click="toggleVisibility()"
|
|
122
|
+
<template v-if="!isHidden">
|
|
123
|
+
<div
|
|
124
|
+
v-if="isShow"
|
|
125
|
+
data-pagefind-ignore
|
|
126
|
+
class="slimbanner"
|
|
45
127
|
>
|
|
46
|
-
<
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
128
|
+
<slot name="icon">
|
|
129
|
+
<span
|
|
130
|
+
v-if="props.showIcon"
|
|
131
|
+
:class="props.iconClass"
|
|
132
|
+
/>
|
|
133
|
+
</slot>
|
|
134
|
+
|
|
135
|
+
<span class="leading-none inline-flex">
|
|
136
|
+
<slot>
|
|
137
|
+
<span :data-text="props.message"></span>
|
|
138
|
+
<a
|
|
139
|
+
v-if="props.linkUrl"
|
|
140
|
+
:href="props.linkUrl"
|
|
141
|
+
target="_blank"
|
|
142
|
+
rel="noopener"
|
|
143
|
+
:title="props.linkTitle || props.linkText"
|
|
144
|
+
class="underline underline-offset-2 hover:text-blue-wrc ml-1.5"
|
|
145
|
+
:data-text="props.linkText"
|
|
146
|
+
></a>
|
|
147
|
+
<span v-if="props.linkUrl" data-text="."></span>
|
|
148
|
+
</slot>
|
|
149
|
+
</span>
|
|
150
|
+
|
|
151
|
+
<button
|
|
152
|
+
v-if="props.showCloseButton && (props.toggleMessage || props.persistClose)"
|
|
153
|
+
class="btn-close text-white"
|
|
154
|
+
:aria-label="props.closeButtonAriaLabel"
|
|
155
|
+
@click="props.persistClose ? closePermanently() : toggleVisibility()"
|
|
156
|
+
>
|
|
157
|
+
<span class="close close-dark" />
|
|
158
|
+
</button>
|
|
61
159
|
</div>
|
|
62
|
-
<
|
|
63
|
-
v-if="props.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
@click="toggleVisibility()"
|
|
160
|
+
<div
|
|
161
|
+
v-else-if="props.toggleMessage"
|
|
162
|
+
data-pagefind-ignore
|
|
163
|
+
:class="`px-4 sm:px-8 py-3 flex items-center justify-center text-xs sm:text-base leading-none relative drop-shadow-md z-2 ${props.toggleBgClass} ${props.toggleTextClass}`"
|
|
67
164
|
>
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
|
|
165
|
+
<div class="tracking-widest leading-none">
|
|
166
|
+
<slot name="toggle">
|
|
167
|
+
{{ props.toggleMessage }}
|
|
168
|
+
</slot>
|
|
169
|
+
</div>
|
|
170
|
+
<button
|
|
171
|
+
v-if="props.showCloseButton"
|
|
172
|
+
class="btn-close"
|
|
173
|
+
:aria-label="props.closeButtonAriaLabel"
|
|
174
|
+
@click="props.persistClose ? closePermanently() : toggleVisibility()"
|
|
175
|
+
>
|
|
176
|
+
<span :class="props.toggleTextClass === 'text-white' ? 'close close-light' : 'close close-dark'" />
|
|
177
|
+
</button>
|
|
178
|
+
</div>
|
|
179
|
+
</template>
|
|
71
180
|
</template>
|
|
72
181
|
|
|
73
182
|
<style>
|
|
@@ -9,56 +9,204 @@ import { Icon } from 'astro-icon/components';
|
|
|
9
9
|
|
|
10
10
|
# Modal
|
|
11
11
|
|
|
12
|
-
Native modal window based on `<dialog>` tag with `::backdrop` CSS pseudo-element
|
|
13
|
-
#
|
|
14
|
-
**Documentation:**
|
|
12
|
+
Native modal window based on `<dialog>` tag with `::backdrop` CSS pseudo-element. Features built-in X close button, action buttons, and flexible configuration.
|
|
15
13
|
|
|
14
|
+
**Documentation:**
|
|
16
15
|
- https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog
|
|
17
16
|
- https://developer.mozilla.org/en-US/docs/Web/CSS/::backdrop
|
|
18
17
|
|
|
19
|
-
## Modal
|
|
18
|
+
## Basic Modal with Title and X Button
|
|
19
|
+
|
|
20
|
+
<div class="component-preview">
|
|
21
|
+
<Modal id="dialog1" open="Open modal" title="Modal Title">
|
|
22
|
+
<p>This modal has a title and built-in X close button in the top-right corner.</p>
|
|
23
|
+
<p>You can also close by clicking the backdrop or pressing Escape.</p>
|
|
24
|
+
</Modal>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
```astro
|
|
28
|
+
<Modal id="dialog1" open="Open modal" title="Modal Title">
|
|
29
|
+
<p>This modal has a title and built-in X close button in the top-right corner.</p>
|
|
30
|
+
<p>You can also close by clicking the backdrop or pressing Escape.</p>
|
|
31
|
+
</Modal>
|
|
32
|
+
```
|
|
20
33
|
|
|
34
|
+
## Modal with Action Buttons
|
|
21
35
|
|
|
22
36
|
<div class="component-preview">
|
|
23
|
-
<Modal
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
37
|
+
<Modal
|
|
38
|
+
id="dialog2"
|
|
39
|
+
open="Delete Item"
|
|
40
|
+
title="Confirm Delete"
|
|
41
|
+
showActions
|
|
42
|
+
cancelText="Cancel"
|
|
43
|
+
confirmText="Delete"
|
|
44
|
+
>
|
|
45
|
+
<p>Are you sure you want to delete this item? This action cannot be undone.</p>
|
|
29
46
|
</Modal>
|
|
30
47
|
</div>
|
|
31
48
|
|
|
32
|
-
```
|
|
33
|
-
<Modal
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
49
|
+
```astro
|
|
50
|
+
<Modal
|
|
51
|
+
id="dialog2"
|
|
52
|
+
open="Delete Item"
|
|
53
|
+
title="Confirm Delete"
|
|
54
|
+
showActions
|
|
55
|
+
cancelText="Cancel"
|
|
56
|
+
confirmText="Delete"
|
|
57
|
+
>
|
|
58
|
+
<p>Are you sure you want to delete this item? This action cannot be undone.</p>
|
|
40
59
|
</Modal>
|
|
41
60
|
```
|
|
42
61
|
|
|
43
|
-
## Modal with
|
|
62
|
+
## Modal with Custom Actions
|
|
44
63
|
|
|
45
64
|
<div class="component-preview">
|
|
46
|
-
<Modal
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
65
|
+
<Modal
|
|
66
|
+
id="dialog3"
|
|
67
|
+
open="Save Changes"
|
|
68
|
+
title="Save Document"
|
|
69
|
+
showActions
|
|
70
|
+
>
|
|
71
|
+
<p>Would you like to save your changes before closing?</p>
|
|
72
|
+
|
|
73
|
+
<div slot="actions" class="flex gap-3 justify-end">
|
|
74
|
+
<form method="dialog" class="contents">
|
|
75
|
+
<Button>Don't Save</Button>
|
|
76
|
+
</form>
|
|
77
|
+
<form method="dialog" class="contents">
|
|
78
|
+
<Button primary>Save</Button>
|
|
79
|
+
</form>
|
|
80
|
+
</div>
|
|
81
|
+
</Modal>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
```astro
|
|
85
|
+
<Modal
|
|
86
|
+
id="dialog3"
|
|
87
|
+
open="Save Changes"
|
|
88
|
+
title="Save Document"
|
|
89
|
+
showActions
|
|
90
|
+
>
|
|
91
|
+
<p>Would you like to save your changes before closing?</p>
|
|
92
|
+
|
|
93
|
+
<div slot="actions" class="flex gap-3 justify-end">
|
|
94
|
+
<form method="dialog" class="contents">
|
|
95
|
+
<Button>Don't Save</Button>
|
|
96
|
+
</form>
|
|
97
|
+
<form method="dialog" class="contents">
|
|
98
|
+
<Button primary>Save</Button>
|
|
99
|
+
</form>
|
|
100
|
+
</div>
|
|
101
|
+
</Modal>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Modal with Custom Width
|
|
105
|
+
|
|
106
|
+
<div class="component-preview">
|
|
107
|
+
<Modal
|
|
108
|
+
id="dialog4"
|
|
109
|
+
open="Wide Modal"
|
|
110
|
+
title="Large Content"
|
|
111
|
+
maxWidth="900px"
|
|
112
|
+
>
|
|
113
|
+
<p>This modal is wider than the default. Useful for forms or detailed content.</p>
|
|
114
|
+
<div class="grid grid-cols-2 gap-4 mt-4">
|
|
115
|
+
<div class="p-4 bg-gray-100 rounded">Column 1</div>
|
|
116
|
+
<div class="p-4 bg-gray-100 rounded">Column 2</div>
|
|
117
|
+
</div>
|
|
118
|
+
</Modal>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
```astro
|
|
122
|
+
<Modal
|
|
123
|
+
id="dialog4"
|
|
124
|
+
open="Wide Modal"
|
|
125
|
+
title="Large Content"
|
|
126
|
+
maxWidth="900px"
|
|
127
|
+
>
|
|
128
|
+
<p>This modal is wider than the default. Useful for forms or detailed content.</p>
|
|
129
|
+
</Modal>
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Modal without X Button (Legacy Style)
|
|
133
|
+
|
|
134
|
+
<div class="component-preview">
|
|
135
|
+
<Modal id="dialog5" open="Open modal" title="Custom Close" showXButton={false}>
|
|
136
|
+
<p>This modal doesn't have the X button.</p>
|
|
137
|
+
<p>You need to provide your own close button.</p>
|
|
138
|
+
<Button primary slot="close" class="mt-4">Close Modal</Button>
|
|
51
139
|
</Modal>
|
|
52
140
|
</div>
|
|
53
141
|
|
|
54
|
-
```
|
|
55
|
-
<Modal id="
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
142
|
+
```astro
|
|
143
|
+
<Modal id="dialog5" open="Open modal" title="Custom Close" showXButton={false}>
|
|
144
|
+
<p>This modal doesn't have the X button.</p>
|
|
145
|
+
<Button primary slot="close" class="mt-4">Close Modal</Button>
|
|
146
|
+
</Modal>
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Modal with Custom Close Button (Icon)
|
|
150
|
+
|
|
151
|
+
<div class="component-preview">
|
|
152
|
+
<Modal id="dialog6" open="Open modal" title="Icon Close" showXButton={false}>
|
|
153
|
+
<p>Some extra content you would like here</p>
|
|
154
|
+
<Button primary slot="close" class="mt-4"><Icon name="octicon:x-24" /></Button>
|
|
155
|
+
</Modal>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
```astro
|
|
159
|
+
<Modal id="dialog6" open="Open modal" title="Icon Close" showXButton={false}>
|
|
160
|
+
<p>Some extra content you would like here</p>
|
|
60
161
|
<Button primary slot="close" class="mt-4"><Icon name="octicon:x-24" /></Button>
|
|
61
162
|
</Modal>
|
|
62
163
|
```
|
|
63
164
|
|
|
165
|
+
## Handling Confirm Events
|
|
166
|
+
|
|
167
|
+
You can listen for the `confirm` event when using action buttons:
|
|
168
|
+
|
|
169
|
+
```javascript
|
|
170
|
+
document.getElementById('dialog2').addEventListener('confirm', (e) => {
|
|
171
|
+
console.log('Confirmed!', e.detail);
|
|
172
|
+
// Perform your action here
|
|
173
|
+
document.getElementById('dialog2').close();
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Props
|
|
178
|
+
|
|
179
|
+
| Prop | Type | Default | Description |
|
|
180
|
+
|------|------|---------|-------------|
|
|
181
|
+
| `id` | string | required | Unique identifier for the modal |
|
|
182
|
+
| `open` | string | `'Open modal'` | Text for the trigger button |
|
|
183
|
+
| `title` | string | `undefined` | Modal title (shown in header) |
|
|
184
|
+
| `maxWidth` | string | `'600px'` | Maximum width of the modal |
|
|
185
|
+
| `showXButton` | boolean | `true` | Show X close button in top-right |
|
|
186
|
+
| `showTrigger` | boolean | `true` | Show the trigger button |
|
|
187
|
+
| `showActions` | boolean | `false` | Show action buttons footer |
|
|
188
|
+
| `cancelText` | string | `'Cancel'` | Text for cancel button |
|
|
189
|
+
| `confirmText` | string | `'Confirm'` | Text for confirm button |
|
|
190
|
+
| `confirmPrimary` | boolean | `true` | Make confirm button primary style |
|
|
191
|
+
|
|
192
|
+
## Slots
|
|
193
|
+
|
|
194
|
+
| Slot | Description |
|
|
195
|
+
|------|-------------|
|
|
196
|
+
| `default` | Main modal content (alias for `main` slot) |
|
|
197
|
+
| `main` | Main modal content |
|
|
198
|
+
| `header` | Custom header content (replaces title) |
|
|
199
|
+
| `actions` | Custom action buttons (when `showActions` is true) |
|
|
200
|
+
| `close` | Custom close button (when `showActions` is false) |
|
|
201
|
+
|
|
202
|
+
## Features
|
|
203
|
+
|
|
204
|
+
- ✅ Built-in X close button in top-right corner
|
|
205
|
+
- ✅ Click backdrop to close
|
|
206
|
+
- ✅ Press Escape to close
|
|
207
|
+
- ✅ Built-in action buttons (Cancel/Confirm)
|
|
208
|
+
- ✅ Customizable width
|
|
209
|
+
- ✅ TypeScript support
|
|
210
|
+
- ✅ Fully accessible with ARIA labels
|
|
211
|
+
|
|
64
212
|
|
|
@@ -6,29 +6,254 @@ import SlimBanner from '../../components/SlimBanner.vue'
|
|
|
6
6
|
|
|
7
7
|
# SlimBanner
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
A flexible banner component for announcements, alerts, or calls to action. Supports custom messages, links, icons, and toggle states.
|
|
10
|
+
|
|
11
|
+
## Basic Example (Ukraine Solidarity)
|
|
10
12
|
|
|
11
13
|
<div class="component-preview">
|
|
12
14
|
<SlimBanner
|
|
13
15
|
class="w-full drop-shadow"
|
|
14
16
|
client:load
|
|
17
|
+
message="We stand with our friends and colleagues in Ukraine. To support Ukraine in their time of need visit"
|
|
18
|
+
linkText="this page"
|
|
19
|
+
linkUrl="https://polo.blue/support-ukraine/"
|
|
20
|
+
linkTitle="Support Ukraine"
|
|
21
|
+
showIcon
|
|
15
22
|
showCloseButton
|
|
16
|
-
|
|
17
|
-
transition:persist
|
|
18
|
-
|
|
23
|
+
toggleMessage="RUSSIA IS A TERRORIST STATE"
|
|
19
24
|
/>
|
|
20
25
|
</div>
|
|
21
26
|
|
|
22
|
-
```
|
|
27
|
+
```astro
|
|
23
28
|
<SlimBanner
|
|
24
29
|
class="w-full drop-shadow"
|
|
25
30
|
client:load
|
|
31
|
+
message="We stand with our friends and colleagues in Ukraine. To support Ukraine in their time of need visit"
|
|
32
|
+
linkText="this page"
|
|
33
|
+
linkUrl="https://polo.blue/support-ukraine/"
|
|
34
|
+
linkTitle="Support Ukraine"
|
|
35
|
+
showIcon
|
|
36
|
+
showCloseButton
|
|
37
|
+
toggleMessage="RUSSIA IS A TERRORIST STATE"
|
|
38
|
+
/>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Simple Announcement
|
|
42
|
+
|
|
43
|
+
<div class="component-preview">
|
|
44
|
+
<SlimBanner
|
|
45
|
+
class="w-full drop-shadow"
|
|
46
|
+
client:load
|
|
47
|
+
message="🎉 New features are now available!"
|
|
48
|
+
linkText="Learn more"
|
|
49
|
+
linkUrl="/updates"
|
|
50
|
+
showCloseButton={false}
|
|
51
|
+
/>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
```astro
|
|
55
|
+
<SlimBanner
|
|
56
|
+
message="🎉 New features are now available!"
|
|
57
|
+
linkText="Learn more"
|
|
58
|
+
linkUrl="/updates"
|
|
59
|
+
showCloseButton={false}
|
|
60
|
+
/>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Custom Message with Link
|
|
64
|
+
|
|
65
|
+
<div class="component-preview">
|
|
66
|
+
<SlimBanner
|
|
67
|
+
class="w-full drop-shadow"
|
|
68
|
+
client:load
|
|
69
|
+
message="Limited time offer! Get 50% off all products. "
|
|
70
|
+
linkText="Shop now"
|
|
71
|
+
linkUrl="/shop"
|
|
72
|
+
linkTitle="Visit our shop"
|
|
73
|
+
showCloseButton={false}
|
|
74
|
+
/>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
```astro
|
|
78
|
+
<SlimBanner
|
|
79
|
+
message="Limited time offer! Get 50% off all products. "
|
|
80
|
+
linkText="Shop now"
|
|
81
|
+
linkUrl="/shop"
|
|
82
|
+
linkTitle="Visit our shop"
|
|
83
|
+
showCloseButton={false}
|
|
84
|
+
/>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## With Toggle State
|
|
88
|
+
|
|
89
|
+
<div class="component-preview">
|
|
90
|
+
<SlimBanner
|
|
91
|
+
class="w-full drop-shadow"
|
|
92
|
+
client:load
|
|
93
|
+
message="Join us at the annual car meet this weekend!"
|
|
94
|
+
linkText="See the lineup"
|
|
95
|
+
linkUrl="/events"
|
|
96
|
+
showCloseButton
|
|
97
|
+
toggleMessage="🏁 TORQUE IS EVERYTHING"
|
|
98
|
+
toggleBgClass="bg-red-700"
|
|
99
|
+
toggleTextClass="text-white"
|
|
100
|
+
/>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
```astro
|
|
104
|
+
<SlimBanner
|
|
105
|
+
message="Join us at the annual car meet this weekend!"
|
|
106
|
+
linkText="See the lineup"
|
|
107
|
+
linkUrl="/events"
|
|
26
108
|
showCloseButton
|
|
27
|
-
|
|
28
|
-
|
|
109
|
+
toggleMessage="🏁 TORQUE IS EVERYTHING"
|
|
110
|
+
toggleBgClass="bg-red-700"
|
|
111
|
+
toggleTextClass="text-white"
|
|
29
112
|
/>
|
|
30
113
|
```
|
|
31
114
|
|
|
115
|
+
## Message Only (No Link)
|
|
116
|
+
|
|
117
|
+
<div class="component-preview">
|
|
118
|
+
<SlimBanner
|
|
119
|
+
class="w-full drop-shadow"
|
|
120
|
+
client:load
|
|
121
|
+
message="Scheduled maintenance: The site will be down on Sunday, 3-5 AM EST."
|
|
122
|
+
linkUrl=""
|
|
123
|
+
showCloseButton={false}
|
|
124
|
+
/>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
```astro
|
|
128
|
+
<SlimBanner
|
|
129
|
+
message="Scheduled maintenance: The site will be down on Sunday, 3-5 AM EST."
|
|
130
|
+
linkUrl=""
|
|
131
|
+
showCloseButton={false}
|
|
132
|
+
/>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Persistent Banner (LocalStorage)
|
|
136
|
+
|
|
137
|
+
Close the banner permanently using localStorage. It will stay hidden even after page refresh.
|
|
138
|
+
|
|
139
|
+
<div class="component-preview">
|
|
140
|
+
<SlimBanner
|
|
141
|
+
class="w-full drop-shadow"
|
|
142
|
+
client:load
|
|
143
|
+
message="🍪 This website uses cookies to enhance your experience."
|
|
144
|
+
linkText="Learn more"
|
|
145
|
+
linkUrl="/privacy"
|
|
146
|
+
showCloseButton
|
|
147
|
+
persistClose
|
|
148
|
+
storageKey="cookie-banner-closed"
|
|
149
|
+
expirationDays={90}
|
|
150
|
+
/>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
```astro
|
|
154
|
+
<SlimBanner
|
|
155
|
+
message="🍪 This website uses cookies to enhance your experience."
|
|
156
|
+
linkText="Learn more"
|
|
157
|
+
linkUrl="/privacy"
|
|
158
|
+
showCloseButton
|
|
159
|
+
persistClose
|
|
160
|
+
storageKey="cookie-banner-closed"
|
|
161
|
+
expirationDays={90}
|
|
162
|
+
/>
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**Note:** Once you close this banner, it won't show again for 90 days (or until you clear localStorage).
|
|
166
|
+
|
|
167
|
+
## Persistent with No Expiration
|
|
168
|
+
|
|
169
|
+
<div class="component-preview">
|
|
170
|
+
<SlimBanner
|
|
171
|
+
class="w-full drop-shadow"
|
|
172
|
+
client:load
|
|
173
|
+
message="📢 Important: This is a one-time announcement."
|
|
174
|
+
linkUrl=""
|
|
175
|
+
showCloseButton
|
|
176
|
+
persistClose
|
|
177
|
+
storageKey="announcement-dismissed"
|
|
178
|
+
expirationDays={0}
|
|
179
|
+
/>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
```astro
|
|
183
|
+
<SlimBanner
|
|
184
|
+
message="📢 Important: This is a one-time announcement."
|
|
185
|
+
linkUrl=""
|
|
186
|
+
showCloseButton
|
|
187
|
+
persistClose
|
|
188
|
+
storageKey="announcement-dismissed"
|
|
189
|
+
expirationDays={0}
|
|
190
|
+
/>
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Tip:** Set `expirationDays={0}` to hide the banner forever once closed.
|
|
194
|
+
|
|
195
|
+
## Props
|
|
196
|
+
|
|
197
|
+
| Prop | Type | Default | Description |
|
|
198
|
+
|------|------|---------|-------------|
|
|
199
|
+
| `message` | String | Ukraine message | Main banner message text |
|
|
200
|
+
| `linkText` | String | `'this page'` | Text for the link |
|
|
201
|
+
| `linkUrl` | String | Ukraine URL | URL for the link (set to empty string for no link) |
|
|
202
|
+
| `linkTitle` | String | `''` | Title attribute for the link |
|
|
203
|
+
| `toggleMessage` | String | `''` | Message to show when close button is clicked (enables toggle) |
|
|
204
|
+
| `toggleBgClass` | String | `'bg-black'` | Background class for toggle state |
|
|
205
|
+
| `toggleTextClass` | String | `'text-white'` | Text color class for toggle state |
|
|
206
|
+
| `showCloseButton` | Boolean | `true` | Show close/toggle button |
|
|
207
|
+
| `closeButtonAriaLabel` | String | `'Toggle'` | Aria label for close button |
|
|
208
|
+
| `showIcon` | Boolean | `false` | Show icon (Ukraine flag by default) |
|
|
209
|
+
| `iconClass` | String | Ukraine flag CSS | Custom CSS class for icon |
|
|
210
|
+
| `persistClose` | Boolean | `false` | Save close state to localStorage |
|
|
211
|
+
| `storageKey` | String | `'slimbanner-closed'` | localStorage key for persistence |
|
|
212
|
+
| `expirationDays` | Number | `30` | Days until localStorage expires (0 = never) |
|
|
213
|
+
|
|
214
|
+
## Slots
|
|
215
|
+
|
|
216
|
+
| Slot | Description |
|
|
217
|
+
|------|-------------|
|
|
218
|
+
| `default` | Override the entire message content |
|
|
219
|
+
| `icon` | Override the icon/flag |
|
|
220
|
+
| `toggle` | Override the toggle state content |
|
|
221
|
+
|
|
222
|
+
## Using Slots
|
|
223
|
+
|
|
224
|
+
```astro
|
|
225
|
+
<SlimBanner client:load>
|
|
226
|
+
<strong slot="default">Custom content</strong> with <em>HTML markup</em>
|
|
227
|
+
</SlimBanner>
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## LocalStorage Management
|
|
231
|
+
|
|
232
|
+
When using `persistClose={true}`, the close state is saved to localStorage. Here's how to manage it:
|
|
233
|
+
|
|
234
|
+
### Clear specific banner:
|
|
235
|
+
```javascript
|
|
236
|
+
localStorage.removeItem('cookie-banner-closed');
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Clear all banners:
|
|
240
|
+
```javascript
|
|
241
|
+
// If using default key
|
|
242
|
+
localStorage.removeItem('slimbanner-closed');
|
|
243
|
+
|
|
244
|
+
// Or clear all by pattern
|
|
245
|
+
Object.keys(localStorage)
|
|
246
|
+
.filter(key => key.includes('banner'))
|
|
247
|
+
.forEach(key => localStorage.removeItem(key));
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Check if banner is closed:
|
|
251
|
+
```javascript
|
|
252
|
+
const data = JSON.parse(localStorage.getItem('cookie-banner-closed'));
|
|
253
|
+
console.log('Banner closed:', data?.closed);
|
|
254
|
+
console.log('Expires at:', new Date(data?.expires));
|
|
255
|
+
```
|
|
256
|
+
|
|
32
257
|
## uno.config.ts / shortcuts
|
|
33
258
|
```js
|
|
34
259
|
['slimbanner','px-4 sm:px-8 flex items-center justify-center text-xs sm:text-base leading-none relative bg-gray-100 z-2 px-4 py-3 sm:(text-base px-8) text-blue-darker print-hidden'],
|
|
@@ -5,6 +5,17 @@ layout: ../../layouts/MainLayout.astro
|
|
|
5
5
|
import MainColors from '../../components/MainColors.vue';
|
|
6
6
|
|
|
7
7
|
<h1>Colors</h1>
|
|
8
|
-
<p>Base colors.</p>
|
|
8
|
+
<p>Base colors for the design system.</p>
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
## How to use
|
|
11
|
+
|
|
12
|
+
Click on any element to copy values to your clipboard:
|
|
13
|
+
|
|
14
|
+
- **Color swatch** - Click the large color box to copy the hex value
|
|
15
|
+
- **Hex button** - Click to copy the hex color code (e.g., `#0040c5`)
|
|
16
|
+
- **text- button** - Click to copy the text color class (e.g., `text-brand-primary`)
|
|
17
|
+
- **bg- button** - Click to copy the background color class (e.g., `bg-brand-primary`)
|
|
18
|
+
|
|
19
|
+
A green checkmark (✓) will appear when copied successfully.
|
|
20
|
+
|
|
21
|
+
<MainColors class="mt-8" client:load />
|
package/src/pages/core/grid.mdx
CHANGED
|
@@ -87,3 +87,107 @@ Template inspired by [Kevin Powell](https://www.youtube.com/kevinpowell)
|
|
|
87
87
|
</section>
|
|
88
88
|
</main>
|
|
89
89
|
```
|
|
90
|
+
|
|
91
|
+
## Full-width variants
|
|
92
|
+
|
|
93
|
+
Additional utility classes for more flexible full-width layouts.
|
|
94
|
+
|
|
95
|
+
### Full-width with Flexbox
|
|
96
|
+
|
|
97
|
+
Use `.full-width-flex` when you need flexbox layout across the full width instead of grid.
|
|
98
|
+
|
|
99
|
+
<div class="component-preview" style="display: block">
|
|
100
|
+
<main class="flow content-grid bg-white pt-8">
|
|
101
|
+
<h2>Full-width Flexbox Example</h2>
|
|
102
|
+
<p>The section below uses flexbox for horizontal layout across the full width.</p>
|
|
103
|
+
|
|
104
|
+
<section class="full-width-flex bg-primary text-white section-padding justify-center gap-8">
|
|
105
|
+
<div class="text-center">
|
|
106
|
+
<h3 class="text-2xl font-bold">Feature 1</h3>
|
|
107
|
+
<p>Flexbox layout</p>
|
|
108
|
+
</div>
|
|
109
|
+
<div class="text-center">
|
|
110
|
+
<h3 class="text-2xl font-bold">Feature 2</h3>
|
|
111
|
+
<p>Full width span</p>
|
|
112
|
+
</div>
|
|
113
|
+
<div class="text-center">
|
|
114
|
+
<h3 class="text-2xl font-bold">Feature 3</h3>
|
|
115
|
+
<p>Easy alignment</p>
|
|
116
|
+
</div>
|
|
117
|
+
</section>
|
|
118
|
+
|
|
119
|
+
<p>Back to regular content width.</p>
|
|
120
|
+
</main>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
```html
|
|
124
|
+
<section class="full-width-flex bg-primary text-white section-padding justify-center gap-8">
|
|
125
|
+
<div class="text-center">
|
|
126
|
+
<h3 class="text-2xl font-bold">Feature 1</h3>
|
|
127
|
+
<p>Flexbox layout</p>
|
|
128
|
+
</div>
|
|
129
|
+
<div class="text-center">
|
|
130
|
+
<h3 class="text-2xl font-bold">Feature 2</h3>
|
|
131
|
+
<p>Full width span</p>
|
|
132
|
+
</div>
|
|
133
|
+
<div class="text-center">
|
|
134
|
+
<h3 class="text-2xl font-bold">Feature 3</h3>
|
|
135
|
+
<p>Easy alignment</p>
|
|
136
|
+
</div>
|
|
137
|
+
</section>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Full-width Uncontained
|
|
141
|
+
|
|
142
|
+
Use `.full-width-uncontained` when you want a full-width section where children also span the full width (not constrained to content area).
|
|
143
|
+
|
|
144
|
+
<div class="component-preview" style="display: block">
|
|
145
|
+
<main class="flow content-grid bg-white pt-8">
|
|
146
|
+
<h2>Full-width Uncontained Example</h2>
|
|
147
|
+
<p>The section below has a full-width background and its children span the full width too.</p>
|
|
148
|
+
|
|
149
|
+
<section class="full-width-uncontained bg-gray-100 section-padding">
|
|
150
|
+
<h3 class="bg-primary text-white py-4 text-center">This heading spans the entire width</h3>
|
|
151
|
+
<p class="bg-blue-100 py-4 text-center">This paragraph also spans the entire width</p>
|
|
152
|
+
</section>
|
|
153
|
+
|
|
154
|
+
<p>Back to regular content width.</p>
|
|
155
|
+
</main>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
```html
|
|
159
|
+
<section class="full-width-uncontained bg-gray-100 section-padding">
|
|
160
|
+
<h3 class="bg-primary text-white py-4 text-center">This heading spans the entire width</h3>
|
|
161
|
+
<p class="bg-blue-100 py-4 text-center">This paragraph also spans the entire width</p>
|
|
162
|
+
</section>
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Full-width Block
|
|
166
|
+
|
|
167
|
+
Use `.full-width-block` for simple block-level full-width containers without grid or flex layout.
|
|
168
|
+
|
|
169
|
+
<div class="component-preview" style="display: block">
|
|
170
|
+
<main class="flow content-grid bg-white pt-8">
|
|
171
|
+
<h2>Full-width Block Example</h2>
|
|
172
|
+
<p>The image below is wrapped in a full-width-block container.</p>
|
|
173
|
+
|
|
174
|
+
<div class="full-width-block bg-gray-200">
|
|
175
|
+
<img src="https://unsplash.it/1600/300" alt="Full width example" style="width: 100%; display: block;" />
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<p>Back to regular content width.</p>
|
|
179
|
+
</main>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
```html
|
|
183
|
+
<div class="full-width-block bg-gray-200">
|
|
184
|
+
<img src="https://unsplash.it/1600/300" alt="Full width example" style="width: 100%; display: block;" />
|
|
185
|
+
</div>
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Comparison
|
|
189
|
+
|
|
190
|
+
- **`.full-width`** - Grid layout with children constrained to content area (default behavior)
|
|
191
|
+
- **`.full-width-flex`** - Flexbox layout across full width
|
|
192
|
+
- **`.full-width-uncontained`** - Grid layout with children spanning full width
|
|
193
|
+
- **`.full-width-block`** - Simple block container at full width
|
package/src/styles/base/grid.css
CHANGED
|
@@ -29,6 +29,28 @@
|
|
|
29
29
|
display: grid;
|
|
30
30
|
grid-template-columns: inherit;
|
|
31
31
|
}
|
|
32
|
+
|
|
33
|
+
/* Variant: full-width with flexbox layout instead of grid */
|
|
34
|
+
> .full-width-flex {
|
|
35
|
+
grid-column: full-width;
|
|
36
|
+
display: flex;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* Variant: full-width container where children span the full width (not constrained to content area) */
|
|
40
|
+
> .full-width-uncontained {
|
|
41
|
+
grid-column: full-width;
|
|
42
|
+
display: grid;
|
|
43
|
+
grid-template-columns: inherit;
|
|
44
|
+
|
|
45
|
+
> * {
|
|
46
|
+
grid-column: full-width;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* Variant: full-width block (no grid/flex on container, children flow naturally at full width) */
|
|
51
|
+
> .full-width-block {
|
|
52
|
+
grid-column: full-width;
|
|
53
|
+
}
|
|
32
54
|
}
|
|
33
55
|
|
|
34
56
|
img.full-width {
|
package/src/utils/text.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
export const text2paragraphs = (text: string, firstLineBottomMargin: boolean = false) => {
|
|
2
|
-
//
|
|
2
|
+
// Normalize line endings: convert \r\n to \n, then remove any remaining \r
|
|
3
|
+
const normalizedText = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
4
|
+
|
|
3
5
|
let out =
|
|
4
6
|
'<p' +
|
|
5
|
-
(firstLineBottomMargin ? 'class="mb-3"' : '') +
|
|
7
|
+
(firstLineBottomMargin ? ' class="mb-3"' : '') +
|
|
6
8
|
'>' +
|
|
7
|
-
|
|
9
|
+
normalizedText.split('\n').join('</p><p>') +
|
|
8
10
|
'</p>';
|
|
9
11
|
return out.split('<p></p><p>').join('<p class="mt-3">');
|
|
10
12
|
};
|