spoko-design-system 1.3.7 → 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 CHANGED
@@ -1,3 +1,9 @@
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
+
1
7
  ## [1.3.7](https://github.com/polo-blue/sds/compare/v1.3.6...v1.3.7) (2025-10-08)
2
8
 
3
9
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spoko-design-system",
3
- "version": "1.3.7",
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.0",
55
- "@astrojs/mdx": "^4.3.6",
56
- "@astrojs/node": "^9.4.4",
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.4",
58
+ "@astrojs/ts-plugin": "^1.10.5",
59
59
  "@astrojs/vue": "^5.1.1",
60
- "@docsearch/css": "^4.1.0",
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.13",
65
- "@iconify-json/circle-flags": "^1.2.8",
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.32",
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.68",
77
- "@iconify-json/material-symbols-light": "^1.2.40",
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.14",
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.30",
89
- "@iconify/json": "^2.2.392",
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.2",
94
- "@unocss/preset-attributify": "66.5.2",
95
- "@unocss/preset-typography": "66.5.2",
96
- "@unocss/preset-uno": "66.5.2",
97
- "@unocss/preset-web-fonts": "66.5.2",
98
- "@unocss/preset-wind": "66.5.2",
99
- "@unocss/reset": "66.5.2",
100
- "@vite-pwa/astro": "^1.1.0",
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.2",
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.7.0",
117
- "@typescript-eslint/eslint-plugin": "^8.46.0",
118
- "@typescript-eslint/parser": "^8.46.0",
119
- "@unocss/transformer-variant-group": "66.5.2",
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.1",
123
+ "astro": "^5.14.6",
124
124
  "conventional-changelog-conventionalcommits": "^9.1.0",
125
- "eslint": "^9.37.0",
125
+ "eslint": "^9.38.0",
126
126
  "eslint-plugin-astro": "^1.3.1",
127
- "eslint-plugin-vue": "^10.5.0",
127
+ "eslint-plugin-vue": "^10.5.1",
128
128
  "prettier": "^3.6.2",
129
129
  "prettier-plugin-astro": "^0.14.1",
130
- "semantic-release": "^24.2.9",
130
+ "semantic-release": "^25.0.1",
131
131
  "unocss": "^0.65.0",
132
- "vite": "^7.1.9"
132
+ "vite": "^7.1.11"
133
133
  },
134
134
  "packageManager": "pnpm@10.17.1",
135
135
  "pnpm": {
@@ -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-3 md:grid-cols-9">
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="mb-6"
43
+ class="group relative"
22
44
  >
23
- <div
24
- class="h-12 transition-all"
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
- <div class="text-sm flex flex-col text-center font-mono text-slate-500">
28
- <span>{{ name }}</span>
29
- <span class="uppercase text-xs">{{ value }}</span>
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
- <Button
21
- primary
22
- onclick={`window.${id}.showModal()`}
23
- >{open}</Button
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
- <slot name="main" />
31
- <form method="dialog">
32
- <slot name="close" />
33
- </form>
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
- required: false,
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
- <div
19
- v-if="isShow"
20
- data-pagefind-ignore
21
- class="slimbanner"
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
- <span class="close close-dark" />
47
- </button>
48
- </div>
49
- <div
50
- v-else
51
- data-pagefind-ignore
52
- class="px-4 sm:px-8 py-3 flex items-center justify-center text-xs sm:text-base leading-none text-white relative bg-black drop-shadow-md z-2"
53
- >
54
- <div class="tracking-widest leading-none">
55
- <span data-text="RUSSIA IS A" />
56
- <span
57
- class="underline decoration-red-600 decoration-1 underline-offset-3"
58
- data-text="TERRORIST"
59
- />
60
- <span data-text="STATE" />
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
- <button
63
- v-if="props.showCloseButton"
64
- class="btn-close"
65
- aria-label="Toggle"
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
- <span class="close close-light" />
69
- </button>
70
- </div>
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 (pure CSS3 modal window).
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 window
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 id="dialog" open="Open modal">
24
- <main slot="main">
25
- <p>Some extra content you would like here</p>
26
- <hr />
27
- </main>
28
- <Button primary slot="close" class="mt-4">Close</Button>
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
- ```js
33
- <Modal id="dialog" open="Open modal">
34
- <main slot="main">
35
- <p>Some extra content you would like here</p>
36
- <hr />
37
- <img width="60" height="80" src="/assets/logo.svg" alt="Astro logo">
38
- </main>
39
- <Button primary slot="close" class="mt-4">Close</Button>
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 different close button
62
+ ## Modal with Custom Actions
44
63
 
45
64
  <div class="component-preview">
46
- <Modal id="dialog2" open="Open modal">
47
- <main slot="main">
48
- <p>Some extra content you would like here</p>
49
- </main>
50
- <Button primary slot="close" class="mt-4"><Icon name="octicon:x-24" /></Button>
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
- ```js
55
- <Modal id="dialog2" open="Open modal">
56
- <main slot="main">
57
- <p>Some extra content you would like here</p>
58
- <img width="60" height="80" src="/assets/logo.svg" alt="Astro logo">
59
- </main>
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
- SlimBanner - solidarity with Ukraine.
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
- transition:name="slim-banner"
17
- transition:persist
18
-
23
+ toggleMessage="RUSSIA IS A TERRORIST STATE"
19
24
  />
20
25
  </div>
21
26
 
22
- ```js
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
- transition:name="slim-banner"
28
- transition:persist
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
- <MainColors class="mt-8" />
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 />
@@ -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
@@ -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
- // return '<p class="mb-2">' + text.split(/[\n\r]+/g).join('</p><p>') + '</p>'
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
- text.split('\n').join('</p><p>') +
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
  };