spoko-design-system 1.6.0 → 1.8.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,15 @@
1
+ ## [1.8.0](https://github.com/polo-blue/sds/compare/v1.7.0...v1.8.0) (2025-10-29)
2
+
3
+ ### Features
4
+
5
+ * **Headline:** add responsive design support and advanced features ([254d5d3](https://github.com/polo-blue/sds/commit/254d5d30f8eff673a48343270f0f23eb39ab7f95))
6
+
7
+ ## [1.7.0](https://github.com/polo-blue/sds/compare/v1.6.0...v1.7.0) (2025-10-29)
8
+
9
+ ### Features
10
+
11
+ * add Tippy.js tooltips with ProductEngine components and API-driven translations ([eb18a17](https://github.com/polo-blue/sds/commit/eb18a171a3e5d80889f64a645d82767898561764))
12
+
1
13
  ## [1.6.0](https://github.com/polo-blue/sds/compare/v1.5.3...v1.6.0) (2025-10-28)
2
14
 
3
15
  ### Features
package/index.ts CHANGED
@@ -19,6 +19,8 @@ export { default as Headline } from './src/components/Headline.vue';
19
19
  export { default as Quote } from './src/components/Quote.vue';
20
20
 
21
21
  export { default as ProductEngineType } from './src/components/Product/ProductEngineType.vue';
22
+ export { default as ProductEngine } from './src/components/Product/ProductEngine.vue';
23
+ export { default as ProductEngines } from './src/components/Product/ProductEngines.vue';
22
24
  export { default as ProductButton } from './src/components/Product/ProductButton.vue';
23
25
  export { default as ProductColors } from './src/components/Product/ProductColors.vue';
24
26
  export { default as ProductDetailName } from './src/components/Product/ProductDetailName.vue';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spoko-design-system",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
4
4
  "private": false,
5
5
  "main": "./index.ts",
6
6
  "module": "./index.ts",
@@ -53,12 +53,13 @@
53
53
  ],
54
54
  "dependencies": {
55
55
  "@algolia/client-search": "^5.41.0",
56
- "@astrojs/mdx": "^4.3.8",
56
+ "@astrojs/mdx": "^4.3.9",
57
57
  "@astrojs/node": "^9.5.0",
58
58
  "@astrojs/sitemap": "^3.6.0",
59
59
  "@astrojs/ts-plugin": "^1.10.5",
60
- "@astrojs/vue": "^5.1.1",
60
+ "@astrojs/vue": "^5.1.2",
61
61
  "@docsearch/css": "^4.2.0",
62
+ "@floating-ui/vue": "^1.1.9",
62
63
  "@iconify-json/ant-design": "^1.2.5",
63
64
  "@iconify-json/bi": "^1.2.6",
64
65
  "@iconify-json/bx": "^1.2.2",
@@ -107,6 +108,7 @@
107
108
  "astro-remote": "^0.3.4",
108
109
  "dotenv": "^17.2.3",
109
110
  "swiper": "^12.0.3",
111
+ "tippy.js": "^6.3.7",
110
112
  "unocss": "66.5.4",
111
113
  "vite-plugin-pwa": "^1.1.0",
112
114
  "vue": "^3.5.22"
@@ -115,14 +117,14 @@
115
117
  "@semantic-release/changelog": "^6.0.3",
116
118
  "@semantic-release/git": "^10.0.1",
117
119
  "@types/gtag.js": "^0.0.20",
118
- "@types/node": "^24.9.1",
120
+ "@types/node": "^24.9.2",
119
121
  "@typescript-eslint/eslint-plugin": "^8.46.2",
120
122
  "@typescript-eslint/parser": "^8.46.2",
121
123
  "@unocss/transformer-variant-group": "66.5.4",
122
124
  "@vitejs/plugin-vue": "^6.0.1",
123
125
  "@vue/compiler-sfc": "^3.5.22",
124
126
  "@vue/eslint-config-typescript": "^14.6.0",
125
- "astro": "^5.15.1",
127
+ "astro": "^5.15.2",
126
128
  "conventional-changelog-conventionalcommits": "^9.1.0",
127
129
  "eslint": "^9.38.0",
128
130
  "eslint-plugin-astro": "^1.4.0",
@@ -1,85 +1,90 @@
1
1
  <script setup lang="ts">
2
- import type { PropType } from 'vue';
3
-
4
- const props = defineProps({
5
- as: {
6
- type: String as PropType<'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p' | 'div' | 'span'>,
7
- default: 'span',
8
- required: true,
9
- },
10
- textSize: {
11
- type: String as PropType<
12
- | 'xs'
13
- | 'sm'
14
- | 'base'
15
- | 'lg'
16
- | 'xl'
17
- | '2xl'
18
- | '3xl'
19
- | '4xl'
20
- | '5xl'
21
- | '6xl'
22
- | '7xl'
23
- | '8xl'
24
- | '9xl'
25
- >,
26
- required: false,
27
- default: null,
28
- },
29
- fontFamily: {
30
- type: String as PropType<'head' | 'text' | 'novamono' | 'mono'>,
31
- required: false,
32
- default: 'head',
33
- },
34
- fontWeight: {
35
- type: String as PropType<'light' | 'regular' | 'bold' | 'light-bold' | 'light-thin'>,
36
- required: false,
37
- default: 'regular',
38
- },
39
- underline: {
40
- type: [Boolean, String] as PropType<boolean | 'center'>,
41
- required: false,
42
- default: false,
43
- },
2
+ import { computed, useAttrs } from 'vue';
3
+
4
+ const attrs = useAttrs();
5
+
6
+ interface HeadlineProps {
7
+ as?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p' | 'div' | 'span';
8
+ class?: string;
9
+ fontFamily?: 'head' | 'text' | 'novamono' | 'mono';
10
+ fontWeight?: 'light' | 'regular' | 'bold' | 'light-bold' | 'light-thin';
11
+ underline?: boolean | 'center';
12
+ defaultSize?: string;
13
+ noFlexLayout?: boolean;
14
+ noMargin?: boolean;
15
+ noLeading?: boolean;
16
+ }
17
+
18
+ const props = withDefaults(defineProps<HeadlineProps>(), {
19
+ as: 'span',
20
+ class: '',
21
+ fontFamily: 'head',
22
+ fontWeight: 'regular',
23
+ underline: false,
24
+ defaultSize: 'text-inherit',
25
+ noFlexLayout: false,
26
+ noMargin: false,
27
+ noLeading: false,
44
28
  });
45
29
 
46
- // Generate the typography class based on font family and weight
47
- const getTypographyClass = (): string => {
48
- const family = props.fontFamily;
49
- const weight = props.fontWeight;
30
+ const typographyClass = computed(() => {
31
+ const { fontFamily, fontWeight } = props;
50
32
 
51
- // Handle special cases for mono fonts
52
- if (family === 'novamono' || family === 'mono') {
53
- return `font-${family}`;
33
+ if (fontFamily === 'novamono' || fontFamily === 'mono') {
34
+ return `font-${fontFamily}`;
54
35
  }
55
36
 
56
- // For head family, generate specific classes
57
- if (family === 'head') {
58
- if (weight === 'light') return 'headline-light';
59
- if (weight === 'bold') return 'headline-bold';
60
- if (weight === 'light-bold') return 'headline-light-bold';
61
- if (weight === 'light-thin') return 'headline-light-thin';
62
- return 'headline'; // for regular weight
37
+ if (fontFamily === 'head') {
38
+ const weightMap = {
39
+ light: 'headline-light',
40
+ bold: 'headline-bold',
41
+ 'light-bold': 'headline-light-bold',
42
+ 'light-thin': 'headline-light-thin',
43
+ regular: 'headline',
44
+ };
45
+ return weightMap[fontWeight] || 'headline';
63
46
  }
64
47
 
65
- // For text family, generate appropriate class
66
- if (family === 'text') {
67
- return `font-text${weight}`;
48
+ if (fontFamily === 'text') {
49
+ return `font-text${fontWeight}`;
68
50
  }
69
51
 
70
- // Default fallback
71
52
  return 'headline';
72
- };
53
+ });
54
+
55
+ const underlineClass = computed(() => {
56
+ if (props.underline === true) return 'headline--underline';
57
+ if (props.underline === 'center') return 'headline--underline-center';
58
+ return '';
59
+ });
60
+
61
+ const layoutClass = computed(() => {
62
+ // Centered underline needs block + text-center
63
+ if (props.underline === 'center') return 'block text-center';
73
64
 
74
- const typographyClass = getTypographyClass();
65
+ // Default flex layout for icon alignment (unless disabled)
66
+ if (!props.noFlexLayout) {
67
+ return 'flex sm:block md:flex items-center';
68
+ }
69
+
70
+ return '';
71
+ });
72
+
73
+ const computedClasses = computed(() => {
74
+ const baseClasses = [];
75
+
76
+ // Conditionally add base classes
77
+ if (!props.noMargin) baseClasses.push('mb-2.5');
78
+ if (!props.noLeading) baseClasses.push('leading-none');
79
+
80
+ const sizeClasses = props.class || props.defaultSize;
81
+
82
+ return `${baseClasses.join(' ')} ${typographyClass.value} ${sizeClasses} ${underlineClass.value} ${layoutClass.value}`.trim();
83
+ });
75
84
  </script>
76
85
 
77
86
  <template>
78
- <component
79
- :is="props.as"
80
- class="mb-2.5 leading-none"
81
- :class="`${typographyClass} ${props.textSize ? `text-${props.textSize}` : 'text-xl'} ${props.underline === true ? 'headline--underline' : ''} ${props.underline === 'center' ? 'headline--underline-center block text-center' : 'flex sm:block md:flex items-center'}`"
82
- >
87
+ <component :is="as" :class="computedClasses" v-bind="attrs">
83
88
  <slot />
84
89
  </component>
85
90
  </template>
@@ -92,14 +97,14 @@ const typographyClass = getTypographyClass();
92
97
  @apply content-empty absolute left-0 bottom-0;
93
98
  height: 3px;
94
99
  width: 55px;
95
- background-color: var(--clr-primary-400);
100
+ background-color: var(--headline-underline-accent, var(--clr-primary-400));
96
101
  }
97
102
 
98
103
  &:before {
99
104
  @apply content-empty absolute left-0 bottom-px h-px;
100
105
  width: 95%;
101
106
  max-width: 255px;
102
- background-color: #64748b;
107
+ background-color: var(--headline-underline-base, #64748b);
103
108
  }
104
109
  }
105
110
 
@@ -110,7 +115,7 @@ const typographyClass = getTypographyClass();
110
115
  @apply content-empty absolute left-1/2 bottom-px h-px;
111
116
  width: 95%;
112
117
  max-width: 255px;
113
- background-color: #64748b;
118
+ background-color: var(--headline-underline-base, #64748b);
114
119
  transform: translateX(-50%);
115
120
  }
116
121
 
@@ -118,7 +123,7 @@ const typographyClass = getTypographyClass();
118
123
  @apply content-empty absolute bottom-0;
119
124
  height: 3px;
120
125
  width: 55px;
121
- background-color: var(--clr-primary-400);
126
+ background-color: var(--headline-underline-accent, var(--clr-primary-400));
122
127
  left: calc(50% - min(47.5%, 127.5px));
123
128
  }
124
129
  }
@@ -27,69 +27,31 @@ const props = defineProps({
27
27
  </script>
28
28
 
29
29
  <template>
30
- <span class="relative has-tooltip inline-block">
31
- <span
32
- data-pagefind-filter="PR-Code"
33
- class="btn-prcode"
34
- :class="[
35
- prcode.variant_category ? `btn-prcode--variant-${prcode.variant_category.toLowerCase()}` : '',
36
- { 'btn-prcode--pdp': isPdp }
37
- ]"
38
- >
39
- {{ prcode.code }}
40
- </span>
41
-
42
- <!-- Dynamic Tooltip with description from API -->
43
- <div v-if="props.prcode.description" class="tooltip">
44
- <div class="tooltip-content">
45
- {{ props.prcode.description }}
46
- <span v-if="props.prcode.group" class="tooltip-group">
47
- ({{ props.prcode.group }})
48
- </span>
49
- </div>
50
- </div>
30
+ <span
31
+ data-pagefind-filter="PR-Code"
32
+ class="btn-prcode"
33
+ :class="[
34
+ prcode.variant_category ? `btn-prcode--variant-${prcode.variant_category.toLowerCase()}` : '',
35
+ { 'btn-prcode--pdp': isPdp }
36
+ ]"
37
+ :data-tippy-content="prcode.description || undefined"
38
+ >
39
+ {{ prcode.code }}
51
40
  </span>
52
41
  </template>
53
42
 
54
43
  <style scoped>
55
- /* Base PrCode Button Styles */
44
+ /* Base PrCode Styles */
56
45
  .btn-prcode {
57
- @apply inline-block relative cursor-default;
46
+ @apply inline-block cursor-default;
47
+ @apply px-1.5 py-0.5;
48
+ @apply rounded;
58
49
  }
59
50
 
60
51
  .btn-prcode--pdp {
61
52
  @apply mb-1;
62
53
  }
63
54
 
64
- /* Tooltip Styles - Similar to ProductEngine */
65
- .tooltip {
66
- @apply invisible absolute left-1/2 -translate-x-1/2 bottom-full mb-2 z-50;
67
- @apply px-3 py-1.5 rounded-lg shadow-lg whitespace-nowrap;
68
- @apply bg-blue-darker text-white text-xs;
69
- @apply pointer-events-none;
70
- max-width: 300px;
71
- white-space: normal;
72
- }
73
-
74
- .has-tooltip:hover .tooltip {
75
- @apply visible;
76
- }
77
-
78
- .tooltip-content {
79
- @apply relative;
80
- }
81
-
82
- .tooltip-group {
83
- @apply ml-2 opacity-75 text-xs font-light;
84
- }
85
-
86
- /* Tooltip Arrow */
87
- .tooltip::after {
88
- content: '';
89
- @apply absolute left-1/2 -translate-x-1/2 top-full;
90
- @apply border-4 border-transparent border-t-blue-darker;
91
- }
92
-
93
55
  /* Semantic Variant Category Colors */
94
56
  /* GTI - Red */
95
57
  .btn-prcode--variant-gti {
@@ -0,0 +1,240 @@
1
+ <script lang="ts" setup>
2
+ import { ref, onMounted, onUnmounted } from 'vue';
3
+ import type { PropType } from 'vue';
4
+ import tippy, { type Instance } from 'tippy.js';
5
+ import 'tippy.js/dist/tippy.css';
6
+ import '../../styles/tippy-theme.css';
7
+
8
+ /*
9
+ VAG group (VW/Audi/Skoda/Seat/Porsche/Bentley/Lamborghini/Ducati/Cupra/Scania/MAN) manufacturer Engine Code
10
+ Displays engine code with detailed tooltip showing: name, power, displacement, dates, etc.
11
+ */
12
+
13
+ interface Engine {
14
+ id?: number;
15
+ code: string;
16
+ name?: string;
17
+ info?: string | null;
18
+ serie?: {
19
+ value: string;
20
+ label: string;
21
+ };
22
+ type?: {
23
+ value: string;
24
+ translated: string;
25
+ label: string;
26
+ };
27
+ power?: {
28
+ kw: number;
29
+ ps: number;
30
+ ps_label: string;
31
+ label: string;
32
+ };
33
+ date?: {
34
+ value: string;
35
+ label: string;
36
+ };
37
+ displacement?: {
38
+ value: number;
39
+ label: string;
40
+ };
41
+ compression_ratio?: {
42
+ value: string | null;
43
+ label: string;
44
+ };
45
+ valves?: {
46
+ value: number | null;
47
+ label: string;
48
+ };
49
+ euro?: {
50
+ value: number;
51
+ label: string;
52
+ };
53
+ pivot?: any;
54
+
55
+ // Backward compatibility - old flat structure
56
+ kw?: number;
57
+ ps?: number;
58
+ cc?: number;
59
+ c_ratio?: string | null;
60
+ }
61
+
62
+ const props = defineProps({
63
+ engine: {
64
+ type: Object as PropType<Engine>,
65
+ required: true,
66
+ },
67
+ showComma: {
68
+ type: Boolean,
69
+ default: false,
70
+ required: false,
71
+ },
72
+ translations: {
73
+ type: Object as PropType<{
74
+ power?: string;
75
+ cc?: string;
76
+ compressionRatio?: string;
77
+ valves?: string;
78
+ euro?: string;
79
+ horsepowerUnit?: string; // 'PS' for German, 'KM' for Polish, 'HP' for English
80
+ }>,
81
+ default: () => ({
82
+ power: 'Power',
83
+ cc: 'CC',
84
+ compressionRatio: 'C. Ratio',
85
+ valves: 'Valves',
86
+ euro: 'Euro',
87
+ horsepowerUnit: 'PS',
88
+ }),
89
+ required: false,
90
+ },
91
+ });
92
+
93
+ const engineRef = ref<HTMLElement | null>(null);
94
+ let tippyInstance: Instance | null = null;
95
+
96
+ // Helper to get series value (supports both old and new API)
97
+ const getSerieValue = (): string => {
98
+ if (props.engine.serie && typeof props.engine.serie === 'object') {
99
+ return props.engine.serie.value;
100
+ }
101
+ // Backward compatibility - old API
102
+ const serie = props.engine.serie as any;
103
+ if (!serie) return '';
104
+ return serie === 3 ? 'EA288' : serie === 2 ? 'EA189' : `Serie ${serie}`;
105
+ };
106
+
107
+ // Generate tooltip HTML content
108
+ const getTooltipContent = () => {
109
+ // Header section
110
+ let headerContent = `<strong>${props.engine.name || props.engine.code}</strong>`;
111
+ if (props.engine.info) {
112
+ headerContent += ` <span class="info">${props.engine.info}</span>`;
113
+ }
114
+ const serieValue = getSerieValue();
115
+ if (serieValue) {
116
+ headerContent += `<div class="series-badge">${serieValue}</div>`;
117
+ }
118
+
119
+ const header = `<div class="tooltip-header">${headerContent}</div>`;
120
+
121
+ // Specs rows
122
+ const rows = [];
123
+
124
+ // Power (supports both new and old API structure)
125
+ const power = props.engine.power;
126
+ const oldKw = props.engine.kw;
127
+ const oldPs = props.engine.ps;
128
+
129
+ if (power || oldKw || oldPs) {
130
+ const powerValues = [];
131
+ const kw = power?.kw || oldKw;
132
+ const ps = power?.ps || oldPs;
133
+ const psLabel = power?.ps_label || props.translations.horsepowerUnit;
134
+ const powerLabel = power?.label || props.translations.power;
135
+
136
+ if (kw) powerValues.push(`${kw} kW`);
137
+ if (ps) powerValues.push(`${ps} ${psLabel}`);
138
+
139
+ if (powerValues.length) {
140
+ rows.push(`<div class="tooltip-row"><span class="tooltip-label">${powerLabel}:</span><span class="tooltip-value">${powerValues.join(' / ')}</span></div>`);
141
+ }
142
+ }
143
+
144
+ // Displacement (CC)
145
+ const displacement = props.engine.displacement;
146
+ const oldCc = props.engine.cc;
147
+
148
+ if (displacement || oldCc) {
149
+ const ccValue = displacement?.value || oldCc;
150
+ const ccLabel = displacement?.label || props.translations.cc;
151
+
152
+ if (ccValue) {
153
+ rows.push(`<div class="tooltip-row"><span class="tooltip-label">${ccLabel}:</span><span class="tooltip-value">${ccValue} cm³</span></div>`);
154
+ }
155
+ }
156
+
157
+ // Euro standard
158
+ const euro = props.engine.euro;
159
+
160
+ if (euro && typeof euro === 'object') {
161
+ if (euro.value) {
162
+ rows.push(`<div class="tooltip-row"><span class="tooltip-label">${euro.label}:</span><span class="tooltip-value">Euro ${euro.value}</span></div>`);
163
+ }
164
+ } else if (euro) {
165
+ // Backward compatibility - old API
166
+ rows.push(`<div class="tooltip-row"><span class="tooltip-label">${props.translations.euro}:</span><span class="tooltip-value">Euro ${euro}</span></div>`);
167
+ }
168
+
169
+ const specsContent = rows.length
170
+ ? `<div class="tooltip-specs">${rows.join('')}</div>`
171
+ : '';
172
+
173
+ return header + specsContent;
174
+ };
175
+
176
+ onMounted(() => {
177
+ if (engineRef.value) {
178
+ tippyInstance = tippy(engineRef.value, {
179
+ content: getTooltipContent(),
180
+ allowHTML: true,
181
+ theme: 'sds',
182
+ placement: 'top',
183
+ arrow: true,
184
+ animation: 'shift-away',
185
+ duration: [200, 150],
186
+ maxWidth: 280,
187
+ });
188
+ }
189
+ });
190
+
191
+ onUnmounted(() => {
192
+ if (tippyInstance) {
193
+ tippyInstance.destroy();
194
+ }
195
+ });
196
+ </script>
197
+
198
+ <template>
199
+ <span
200
+ ref="engineRef"
201
+ class="engine-code"
202
+ :class="`engine-code-${engine.code}`"
203
+ >
204
+ {{ engine.code }}<span v-if="showComma">,</span>
205
+ </span>
206
+ </template>
207
+
208
+ <style scoped>
209
+ /* Engine Code Styles */
210
+ .engine-code {
211
+ @apply inline-block mr-1;
212
+ @apply underline decoration-dotted underline-offset-4 py-0.5;
213
+ @apply decoration-neutral-light cursor-default;
214
+ @apply transition-colors duration-200;
215
+ }
216
+
217
+ .engine-code:hover {
218
+ @apply decoration-blue-darker dark:decoration-blue-light;
219
+ }
220
+
221
+ /* Semantic Engine Code Colors */
222
+ /* GTI Engines - Red */
223
+ .engine-code-CAVE,
224
+ .engine-code-CTHE,
225
+ .engine-code-DAJA,
226
+ .engine-code-DAYB {
227
+ @apply text-red-600 dark:text-red-500;
228
+ }
229
+
230
+ /* WRC R Engine - Blue */
231
+ .engine-code-CDLJ {
232
+ @apply text-blue-600 dark:text-blue-500;
233
+ }
234
+
235
+ /* Special Blue Engines */
236
+ .engine-code-CPTA,
237
+ .engine-code-CZEA {
238
+ @apply text-blue-700 dark:text-blue-600;
239
+ }
240
+ </style>