spoko-design-system 0.3.8 → 0.4.1

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.
@@ -1 +1 @@
1
- [["Map",1,2],"meta::meta",["Map",3,4,5,6],"astro-version","5.2.5","astro-config-digest","{\"root\":{},\"srcDir\":{},\"publicDir\":{},\"outDir\":{},\"cacheDir\":{},\"site\":\"https://sds.spoko.space/\",\"compressHTML\":true,\"base\":\"/\",\"trailingSlash\":\"ignore\",\"output\":\"static\",\"scopedStyleStrategy\":\"attribute\",\"build\":{\"format\":\"directory\",\"client\":{},\"server\":{},\"assets\":\"_astro\",\"serverEntry\":\"entry.mjs\",\"redirects\":true,\"inlineStylesheets\":\"auto\",\"concurrency\":1},\"server\":{\"open\":false,\"host\":false,\"port\":1234,\"streaming\":true},\"redirects\":{},\"image\":{\"endpoint\":{\"route\":\"/_image\"},\"service\":{\"entrypoint\":\"astro/assets/services/sharp\",\"config\":{}},\"domains\":[\"placehold.co\",\"api.polo.blue\",\"polo.blue\",\"media.istockphoto.com\",\"img.freepik.com\"],\"remotePatterns\":[]},\"devToolbar\":{\"enabled\":true},\"markdown\":{\"syntaxHighlight\":\"shiki\",\"shikiConfig\":{\"langs\":[],\"langAlias\":{},\"theme\":\"github-dark\",\"themes\":{},\"wrap\":false,\"transformers\":[]},\"remarkPlugins\":[],\"rehypePlugins\":[],\"remarkRehype\":{},\"gfm\":true,\"smartypants\":true},\"security\":{\"checkOrigin\":true},\"env\":{\"schema\":{},\"validateSecrets\":false},\"experimental\":{\"clientPrerender\":false,\"contentIntellisense\":false,\"responsiveImages\":false,\"serializeConfig\":false},\"legacy\":{\"collections\":false}}"]
1
+ [["Map",1,2],"meta::meta",["Map",3,4,5,6],"astro-version","5.3.0","astro-config-digest","{\"root\":{},\"srcDir\":{},\"publicDir\":{},\"outDir\":{},\"cacheDir\":{},\"site\":\"https://sds.spoko.space/\",\"compressHTML\":true,\"base\":\"/\",\"trailingSlash\":\"ignore\",\"output\":\"static\",\"scopedStyleStrategy\":\"attribute\",\"build\":{\"format\":\"directory\",\"client\":{},\"server\":{},\"assets\":\"_astro\",\"serverEntry\":\"entry.mjs\",\"redirects\":true,\"inlineStylesheets\":\"auto\",\"concurrency\":1},\"server\":{\"open\":false,\"host\":false,\"port\":1234,\"streaming\":true},\"redirects\":{},\"image\":{\"endpoint\":{\"route\":\"/_image\"},\"service\":{\"entrypoint\":\"astro/assets/services/sharp\",\"config\":{}},\"domains\":[\"placehold.co\",\"api.polo.blue\",\"polo.blue\",\"media.istockphoto.com\",\"img.freepik.com\",\"polo6r.pl\"],\"remotePatterns\":[]},\"devToolbar\":{\"enabled\":true},\"markdown\":{\"syntaxHighlight\":\"shiki\",\"shikiConfig\":{\"langs\":[],\"langAlias\":{},\"theme\":\"github-dark\",\"themes\":{},\"wrap\":false,\"transformers\":[]},\"remarkPlugins\":[],\"rehypePlugins\":[],\"remarkRehype\":{},\"gfm\":true,\"smartypants\":true},\"security\":{\"checkOrigin\":true},\"env\":{\"schema\":{},\"validateSecrets\":false},\"experimental\":{\"clientPrerender\":false,\"contentIntellisense\":false,\"responsiveImages\":false,\"serializeConfig\":false},\"legacy\":{\"collections\":false}}"]
package/astro.config.mjs CHANGED
@@ -9,7 +9,7 @@ import sitemap from "@astrojs/sitemap";
9
9
  import pagefind from "astro-pagefind";
10
10
  import AstroPWA from '@vite-pwa/astro';
11
11
  import metaTags from "astro-meta-tags";
12
- import playformInline from "@playform/inline";
12
+
13
13
  import { createSdsConfig } from './uno-config';
14
14
 
15
15
  const unoConfig = createSdsConfig();
@@ -22,68 +22,66 @@ export default defineConfig({
22
22
  },
23
23
  image: {
24
24
  service: sharpImageService(),
25
- domains: ["placehold.co", "api.polo.blue", "polo.blue", "media.istockphoto.com", "img.freepik.com"]
25
+ domains: ["placehold.co", "api.polo.blue", "polo.blue", "media.istockphoto.com", "img.freepik.com", "polo6r.pl"]
26
26
  },
27
27
  integrations: [
28
- // Enable Vue to support Vue3 components.
29
- vue(), mdx(), astroI18next(), AstroPWA({
30
- mode: 'production',
31
- base: '/',
32
- scope: '/',
33
- includeAssets: ['favicon.svg', 'safari-pinned-tab.svg', 'brands/*.svg', 'fonts/*.woff2', 'fonts/*.svg', 'vw.svg', 'polo.blue.svg', 'spoko.space.svg'],
34
- // add this to cache all the static assets in the public folder
35
- // includeAssets: [
36
- // "**/*",
37
- // ],
38
- registerType: 'autoUpdate',
39
- manifest: {
40
- name: 'Spoko Design System',
41
- short_name: 'SDS',
42
- description: 'SDS PWA app description',
43
- categories: ['multimedia'],
44
- screenshots: [{
45
- "src": "pwa-512x512.png",
46
- "sizes": "512x512",
47
- "platform": "windows",
48
- "label": "SDS"
49
- }],
50
- theme_color: '#001e50',
51
- icons: [{
52
- src: 'pwa-192x192.png',
53
- sizes: '192x192',
54
- type: 'image/png'
55
- }, {
56
- src: 'pwa-512x512.png',
57
- sizes: '512x512',
58
- type: 'image/png'
59
- }, {
60
- src: 'pwa-512x512.png',
61
- sizes: '512x512',
62
- type: 'image/png',
63
- purpose: 'any maskable'
64
- }]
65
- },
66
- workbox: {
67
- navigateFallback: '/',
68
- globPatterns: ['**/*.{css,js,html,svg,png,ico,txt}']
69
- // globPatterns: ["**/*"], // add this to cache all the imports
70
- },
71
- devOptions: {
72
- enabled: false,
73
- navigateFallbackAllowlist: [/^\//]
74
- },
75
- experimental: {
76
- directoryAndTrailingSlashHandler: true
77
- }
78
- }),
79
- UnoCSS({
80
- injectReset: true,
81
- ...unoConfig
82
- }),
83
- icon(iconConfig),
84
- metaTags(),
85
- (await import("@playform/inline")).default(),
86
- pagefind(),
87
- sitemap()
88
- ]
28
+ // Enable Vue to support Vue3 components
29
+ vue(),
30
+ mdx(),
31
+ astroI18next(),
32
+ AstroPWA({
33
+ mode: 'production',
34
+ base: '/',
35
+ scope: '/',
36
+ includeAssets: ['favicon.svg', 'safari-pinned-tab.svg', 'brands/*.svg', 'fonts/*.woff2', 'fonts/*.svg', 'vw.svg', 'polo.blue.svg', 'spoko.space.svg'],
37
+ registerType: 'autoUpdate',
38
+ manifest: {
39
+ name: 'Spoko Design System',
40
+ short_name: 'SDS',
41
+ description: 'SDS PWA app description',
42
+ categories: ['multimedia'],
43
+ screenshots: [{
44
+ "src": "pwa-512x512.png",
45
+ "sizes": "512x512",
46
+ "platform": "windows",
47
+ "label": "SDS"
48
+ }],
49
+ theme_color: '#001e50',
50
+ icons: [{
51
+ src: 'pwa-192x192.png',
52
+ sizes: '192x192',
53
+ type: 'image/png'
54
+ }, {
55
+ src: 'pwa-512x512.png',
56
+ sizes: '512x512',
57
+ type: 'image/png'
58
+ }, {
59
+ src: 'pwa-512x512.png',
60
+ sizes: '512x512',
61
+ type: 'image/png',
62
+ purpose: 'any maskable'
63
+ }]
64
+ },
65
+ workbox: {
66
+ navigateFallback: '/',
67
+ globPatterns: ['**/*.{css,js,html,svg,png,ico,txt}']
68
+ },
69
+ devOptions: {
70
+ enabled: false,
71
+ navigateFallbackAllowlist: [/^\//]
72
+ },
73
+ experimental: {
74
+ directoryAndTrailingSlashHandler: true
75
+ }
76
+ }),
77
+ UnoCSS({
78
+ injectReset: true,
79
+ ...unoConfig
80
+ }),
81
+ icon(iconConfig),
82
+ metaTags(),
83
+ (await import("@playform/inline")).default(),
84
+ pagefind(),
85
+ sitemap()
86
+ ]
89
87
  });
package/index.ts CHANGED
@@ -38,6 +38,7 @@ export { default as Copyright } from './src/components/Copyright.astro';
38
38
  export { default as HandDrive } from './src/components/HandDrive.astro';
39
39
  export { default as Faq } from './src/components/Faq.astro';
40
40
  export { default as FaqItem } from './src/components/FaqItem.astro';
41
+ export { default as ButtonCopy } from './src/components/ButtonCopy.astro'; // Add this line
41
42
 
42
43
  export { default as ProductImage } from './src/components/Product/ProductImage.astro';
43
44
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spoko-design-system",
3
- "version": "0.3.08",
3
+ "version": "0.4.1",
4
4
  "private": false,
5
5
  "main": "./index.ts",
6
6
  "module": "./index.ts",
@@ -74,7 +74,7 @@
74
74
  "@iconify/vue": "^4.3.0",
75
75
  "@playform/compress": "^0.1.7",
76
76
  "@playform/inline": "^0.1.1",
77
- "@types/node": "^22.13.3",
77
+ "@types/node": "^22.13.4",
78
78
  "@unocss/astro": "^65.4.3",
79
79
  "@unocss/preset-attributify": "^65.4.3",
80
80
  "@unocss/preset-typography": "^65.4.3",
@@ -83,7 +83,7 @@
83
83
  "@unocss/preset-wind": "^65.4.3",
84
84
  "@unocss/reset": "^65.4.3",
85
85
  "@vite-pwa/astro": "^0.5.0",
86
- "@vueuse/core": "^12.5.0",
86
+ "@vueuse/core": "^12.6.1",
87
87
  "astro-i18next": "1.0.0-beta.21",
88
88
  "astro-icon": "^1.1.5",
89
89
  "astro-meta-tags": "^0.3.1",
@@ -92,7 +92,7 @@
92
92
  "astro-remote": "^0.3.3",
93
93
  "dotenv": "^16.4.7",
94
94
  "i18next": "^24.2.2",
95
- "i18next-browser-languagedetector": "^8.0.2",
95
+ "i18next-browser-languagedetector": "^8.0.3",
96
96
  "i18next-fs-backend": "^2.6.0",
97
97
  "i18next-http-backend": "^3.0.2",
98
98
  "i18next-vue": "^5.1.0",
@@ -0,0 +1,165 @@
1
+ ---
2
+ interface Props {
3
+ productNumber: string;
4
+ tooltipClasses?: string;
5
+ texts: {
6
+ copy: string;
7
+ copied: string;
8
+ };
9
+ }
10
+
11
+ const {
12
+ productNumber,
13
+ tooltipClasses = '',
14
+ texts
15
+ } = Astro.props;
16
+
17
+ // Move SVG definition to a constant to avoid recomputing
18
+ const COPY_ICON = `url("data:image/svg+xml;utf8,${encodeURIComponent(
19
+ `<svg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'><path fill='currentColor' d='M184 66H40a6 6 0 0 0-6 6v144a6 6 0 0 0 6 6h144a6 6 0 0 0 6-6V72a6 6 0 0 0-6-6m-6 144H46V78h132Zm44-170v144a6 6 0 0 1-12 0V46H72a6 6 0 0 1 0-12h144a6 6 0 0 1 6 6'/></svg>`
20
+ )}")`;
21
+ ---
22
+
23
+ <button
24
+ aria-label={texts.copy}
25
+ class="btn-copy has-tooltip"
26
+ data-copy-text={productNumber}
27
+ >
28
+ <span
29
+ class:list={["tooltip rounded-full btn-copy-text", tooltipClasses]}
30
+ data-text={texts.copy}
31
+ data-copied-text={texts.copied}
32
+ />
33
+ <span class="copy-icon" role="img" aria-hidden="true" />
34
+ </button>
35
+
36
+ <script>
37
+ class ClipboardButton {
38
+ private static COPY_TIMEOUT = 2000;
39
+
40
+ constructor(private button: HTMLButtonElement) {
41
+ this.init();
42
+ }
43
+
44
+ private init(): void {
45
+ this.button.addEventListener('click', () => this.handleCopy());
46
+ }
47
+
48
+ private get tooltip(): HTMLSpanElement {
49
+ return this.button.querySelector('.tooltip') as HTMLSpanElement;
50
+ }
51
+
52
+ private get copyText(): string {
53
+ return this.button.dataset.copyText || '';
54
+ }
55
+
56
+ private get originalText(): string {
57
+ return this.tooltip.dataset.text || 'Copy';
58
+ }
59
+
60
+ private get copiedText(): string {
61
+ return this.tooltip.dataset.copiedText || 'Copied';
62
+ }
63
+
64
+ private async handleCopy(): Promise<void> {
65
+ try {
66
+ await this.copyToClipboard();
67
+ this.updateTooltip();
68
+ } catch (err) {
69
+ console.error('Failed to copy text:', err);
70
+ }
71
+ }
72
+
73
+ private async copyToClipboard(): Promise<void> {
74
+ if (navigator.clipboard && window.isSecureContext) {
75
+ await navigator.clipboard.writeText(this.copyText);
76
+ } else {
77
+ this.fallbackCopy();
78
+ }
79
+ }
80
+
81
+ private updateTooltip(): void {
82
+ this.tooltip.dataset.text = this.copiedText;
83
+ setTimeout(() => {
84
+ this.tooltip.dataset.text = this.originalText;
85
+ }, ClipboardButton.COPY_TIMEOUT);
86
+ }
87
+
88
+ private fallbackCopy(): void {
89
+ const textArea = document.createElement('textarea');
90
+ textArea.value = this.copyText;
91
+ textArea.style.position = 'fixed';
92
+ textArea.style.opacity = '0';
93
+
94
+ document.body.appendChild(textArea);
95
+ textArea.select();
96
+
97
+ try {
98
+ document.execCommand('copy');
99
+ } catch (err) {
100
+ console.error('Fallback copy failed:', err);
101
+ } finally {
102
+ document.body.removeChild(textArea);
103
+ }
104
+ }
105
+ }
106
+
107
+ // Use a single event listener with event delegation
108
+ document.addEventListener('astro:page-load', () => {
109
+ document.querySelectorAll('.btn-copy').forEach(button => {
110
+ if (button instanceof HTMLButtonElement) {
111
+ new ClipboardButton(button);
112
+ }
113
+ });
114
+ });
115
+ </script>
116
+
117
+ <style define:vars={{ iconUrl: COPY_ICON }}>
118
+ .btn-copy {
119
+ position: absolute;
120
+ right: -1.75rem;
121
+ margin-left: auto;
122
+ width: 1.5rem;
123
+ height: 1.5rem;
124
+ line-height: 1;
125
+ opacity: 0.1;
126
+ display: inline-flex;
127
+ align-items: center;
128
+ justify-content: center;
129
+ transition: opacity 0.2s ease-in-out;
130
+ }
131
+
132
+ .btn-copy:hover {
133
+ opacity: 1;
134
+ }
135
+
136
+ @media (min-width: 640px) {
137
+ .btn-copy {
138
+ right: -1.25rem;
139
+ }
140
+ }
141
+
142
+ .tooltip {
143
+ @apply invisible absolute -top-8 left-1/2 -translate-x-1/2 bg-neutral px-2 py-1 text-xs text-white opacity-0 transition-opacity;
144
+ }
145
+
146
+ .tooltip::before {
147
+ content: attr(data-text);
148
+ }
149
+
150
+ .has-tooltip:hover .tooltip {
151
+ @apply visible opacity-100;
152
+ }
153
+
154
+ .copy-icon {
155
+ width: 1.2em;
156
+ height: 1.2em;
157
+ display: inline-block;
158
+ vertical-align: middle;
159
+ background-color: currentColor;
160
+ -webkit-mask: var(--iconUrl) no-repeat;
161
+ mask: var(--iconUrl) no-repeat;
162
+ -webkit-mask-size: 100% 100%;
163
+ mask-size: 100% 100%;
164
+ }
165
+ </style>
@@ -4,11 +4,18 @@ import CategoryViewToggler from './CategoryViewToggler.astro';
4
4
  import { Icon } from 'astro-icon/components';
5
5
  import { t } from "i18next";
6
6
 
7
- const { category, subcategory, subtitle, subsubtitle, titleSmall, locale, showViewToggler, viewerLabels } = Astro.props;
7
+ const {
8
+ category,
9
+ subcategory,
10
+ subtitle,
11
+ subsubtitle,
12
+ titleSmall,
13
+ locale,
14
+ showViewToggler,
15
+ viewerLabels
16
+ } = Astro.props;
8
17
 
9
- // Compute base URL for localization
10
18
  const baseURL = locale === 'en' ? '' : `/${locale}`;
11
-
12
19
  ---
13
20
 
14
21
  <div
@@ -16,26 +23,26 @@ const baseURL = locale === 'en' ? '' : `/${locale}`;
16
23
  transition:name="category-details"
17
24
  transition:animate="fade"
18
25
  >
19
- <CategorySidebarToggler onclick="toggleSidebar()">
20
- <!-- Desktop expanded - initially visible -->
21
- <Icon
22
- name="ant-design:menu-fold-outlined"
23
- class="toggler-btn md:[&:not(.hidden)]:block"
24
- aria-hidden="true"
25
- />
26
- <!-- Desktop collapsed - initially hidden -->
27
- <Icon
28
- name="ant-design:menu-unfold-outlined"
29
- class="toggler-btn hidden md:[&:not(.hidden)]:block"
30
- aria-hidden="true"
31
- />
32
- <!-- Mobile icon -->
33
- <Icon
34
- name="ant-design:menu-outlined"
35
- class="toggler-btn block md:hidden"
36
- aria-hidden="true"
37
- />
38
- </CategorySidebarToggler>
26
+ <CategorySidebarToggler onclick="toggleSidebar()">
27
+ <!-- Desktop expanded -->
28
+ <Icon
29
+ name="ant-design:menu-fold-outlined"
30
+ class="toggler-btn md:[&:not(.hidden)]:block"
31
+ aria-hidden="true"
32
+ />
33
+ <!-- Desktop collapsed -->
34
+ <Icon
35
+ name="ant-design:menu-unfold-outlined"
36
+ class="toggler-btn hidden md:[&:not(.hidden)]:block"
37
+ aria-hidden="true"
38
+ />
39
+ <!-- Mobile icon -->
40
+ <Icon
41
+ name="ant-design:menu-outlined"
42
+ class="toggler-btn block md:hidden"
43
+ aria-hidden="true"
44
+ />
45
+ </CategorySidebarToggler>
39
46
 
40
47
  <div class="overflow-x-auto overflow-y-hidden flex max-w-full items-center">
41
48
  {subtitle ? (
@@ -51,7 +58,7 @@ const baseURL = locale === 'en' ? '' : `/${locale}`;
51
58
  </h1>
52
59
  ) : (
53
60
  <>
54
- <div class="text-lg py-2.5 sm:py-0 whitespace-nowrap ">
61
+ <div class="text-lg py-2.5 sm:py-0 whitespace-nowrap">
55
62
  <a href={`${baseURL}/${category.slug}/${subcategory.slug}/`}>
56
63
  {subtitle}
57
64
  </a>
@@ -66,36 +73,41 @@ const baseURL = locale === 'en' ? '' : `/${locale}`;
66
73
  ) : (
67
74
  <h1 class="text-lg py-2.5 sm:py-0 whitespace-nowrap">
68
75
  {category.name}
69
- {titleSmall && (<small>{titleSmall}</small>)}
76
+ {titleSmall && <small>{titleSmall}</small>}
70
77
  <span class="sr-only"> {t('catalog.extra-short')}</span>
71
78
  </h1>
72
79
  )}
73
80
  </div>
74
81
 
75
- { showViewToggler && ( <CategoryViewToggler {...viewerLabels} class="hidden lg:flex items-center gap-2 ml-auto" /> ) }
76
-
82
+ {showViewToggler && (
83
+ <CategoryViewToggler
84
+ {...viewerLabels}
85
+ class="hidden lg:flex items-center gap-2 ml-auto"
86
+ />
87
+ )}
77
88
  </div>
78
89
 
79
- <script is:inline>
90
+ <script>
80
91
  function initializeSidebar() {
81
92
  const sidebar = document.getElementById('sidebar');
82
93
  const savedState = localStorage.getItem('sidebarState') || 'open';
83
94
 
84
95
  if (sidebar) {
85
96
  sidebar.classList.toggle('collapsed', savedState === 'closed');
86
-
87
- const togglers = document.querySelectorAll('.toggler-btn');
88
- if (togglers.length >= 2) {
89
- const isCollapsed = savedState === 'closed';
90
- togglers[0].classList.toggle('hidden', isCollapsed);
91
- togglers[1].classList.toggle('hidden', !isCollapsed);
92
- }
97
+ updateTogglers(savedState === 'closed');
98
+ }
99
+ }
100
+
101
+ function updateTogglers(isCollapsed) {
102
+ const togglers = document.querySelectorAll('.toggler-btn');
103
+ if (togglers.length >= 2) {
104
+ togglers[0].classList.toggle('hidden', isCollapsed);
105
+ togglers[1].classList.toggle('hidden', !isCollapsed);
93
106
  }
94
107
  }
95
108
 
96
109
  function toggleSidebar() {
97
110
  const sidebar = document.getElementById('sidebar');
98
- const togglers = document.querySelectorAll('.toggler-btn');
99
111
  const isMobile = window.matchMedia("(max-width: 768px)").matches;
100
112
 
101
113
  if (sidebar) {
@@ -106,21 +118,34 @@ const baseURL = locale === 'en' ? '' : `/${locale}`;
106
118
  const isCollapsed = sidebar.classList.toggle('collapsed');
107
119
  document.body.classList.remove('overflow-hidden');
108
120
  localStorage.setItem('sidebarState', isCollapsed ? 'closed' : 'open');
109
-
110
- if (togglers.length >= 2) {
111
- togglers[0].classList.toggle('hidden', isCollapsed);
112
- togglers[1].classList.toggle('hidden', !isCollapsed);
113
- }
121
+ updateTogglers(isCollapsed);
114
122
  }
115
123
  }
116
124
  }
117
-
118
- document.addEventListener('DOMContentLoaded', initializeSidebar);
119
- document.addEventListener('astro:page-load', initializeSidebar, { once: true });
120
- </script>
121
125
 
122
- <style>
123
- .toggler-btn {
124
- @apply md:-mt-0.5;
126
+ // Initialize on astro page load
127
+ document.addEventListener('astro:page-load', initializeSidebar);
128
+
129
+ // Preserve state during view transitions
130
+ document.addEventListener('astro:before-swap', () => {
131
+ const sidebarState = localStorage.getItem('sidebarState');
132
+ if (sidebarState) {
133
+ sessionStorage.setItem('tempSidebarState', sidebarState);
134
+ }
135
+ });
136
+
137
+ document.addEventListener('astro:after-swap', () => {
138
+ const tempState = sessionStorage.getItem('tempSidebarState');
139
+ if (tempState) {
140
+ localStorage.setItem('sidebarState', tempState);
141
+ sessionStorage.removeItem('tempSidebarState');
142
+ initializeSidebar();
125
143
  }
126
- </style>
144
+ });
145
+ </script>
146
+
147
+ <style>
148
+ .toggler-btn {
149
+ @apply md:-mt-0.5;
150
+ }
151
+ </style>
@@ -42,35 +42,41 @@ const {
42
42
  </div>
43
43
  </div>
44
44
 
45
+ <script>
46
+ function initializeView() {
47
+ const savedView = localStorage.getItem('categoryView') || 'list';
48
+ updateUI(savedView);
49
+
50
+ // Clean up existing listeners to prevent duplicates
51
+ document.querySelectorAll('.view-toggle').forEach(btn => {
52
+ btn.removeEventListener('click', handleViewToggle);
53
+ btn.addEventListener('click', handleViewToggle);
54
+ });
55
+ }
45
56
 
46
- <script >
47
57
  function handleViewToggle(e) {
48
- const button = e.currentTarget;
49
- if (!(button instanceof HTMLElement)) return;
50
-
51
- const view = button.dataset.view;
58
+ if (!(e.currentTarget instanceof HTMLElement)) return;
59
+ const view = e.currentTarget.dataset.view;
52
60
  if (!view) return;
53
-
61
+
54
62
  localStorage.setItem('categoryView', view);
55
63
  updateUI(view);
56
64
  }
57
-
65
+
58
66
  function updateUI(view) {
59
- document.querySelectorAll('.view-toggle')
60
- .forEach(btn => btn.classList.toggle('bg-neutral-lightest',
61
- btn.dataset.view === view));
62
-
67
+ // Update toggle buttons
68
+ document.querySelectorAll('.view-toggle').forEach(btn => {
69
+ btn.classList.toggle('bg-neutral-lightest', btn.dataset.view === view);
70
+ });
71
+
72
+ // Update products container
63
73
  const productsContainer = document.querySelector('.products-container');
64
74
  if (productsContainer) {
65
75
  productsContainer.classList.remove('view-grid', 'view-list');
66
76
  productsContainer.classList.add(`view-${view}`);
67
77
  }
68
78
  }
69
-
70
- document.addEventListener('astro:page-load', () => {
71
-
72
- updateUI(localStorage.getItem('categoryView') || 'list');
73
- document.querySelectorAll('.view-toggle')
74
- .forEach(btn => btn.addEventListener('click', handleViewToggle));
75
- }, { once: true });
76
- </script>
79
+
80
+ // Initialize on page load and view transitions
81
+ document.addEventListener('astro:page-load', initializeView);
82
+ </script>
@@ -96,7 +96,7 @@ const commonProps = {
96
96
  />
97
97
  )}
98
98
 
99
- <style is:global>
99
+ <style>
100
100
  .bg-vw {
101
101
  background: radial-gradient(circle at 50% 85%, #00437a 0%, #001e50 100%);
102
102
  }
@@ -1,5 +1,5 @@
1
1
  ---
2
- import ButtonCopy from '../ButtonCopy.vue';
2
+ import ButtonCopy from '../ButtonCopy.astro';
3
3
  import useFormatProductNumber from '../../utils/product/useFormatProductNumber';
4
4
 
5
5
  const {
@@ -41,7 +41,6 @@ const FormattedWrapper = isPdp ? 'h3' : 'div';
41
41
  productNumber={productNumber}
42
42
  texts={buttonTexts}
43
43
  tooltipClasses=""
44
- client:only="vue"
45
44
  />
46
45
  )}
47
46
  </div>
@@ -25,7 +25,7 @@ export const jumbotronShortcuts = [
25
25
 
26
26
  // Metadata and categories
27
27
  ['jumbotron-meta', 'order-3 flex items-center text-gray-100'],
28
- ['jumbotron-categories', 'order-1'],
28
+ ['jumbotron-categories', 'order-1 mt-4'],
29
29
 
30
30
  // Content styles
31
31
  ['jumbotron-description', 'mb-1 line-clamp-3 text-base sm:text-lg leading-none mt-4'],