@alphacifer/slidev-addon-theme 0.0.8 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/README.md +80 -0
  2. package/components/pyramid/circular-pyramid/CircularPyramid.vue +871 -0
  3. package/components/pyramid/circular-pyramid/CircularPyramidStack.vue +375 -0
  4. package/components/pyramid/circular-pyramid/CircularPyramidStackContent.vue +208 -0
  5. package/components/pyramid/circular-pyramid/CircularPyramidStackTitle.vue +138 -0
  6. package/components/pyramid/square-pyramid/SquarePyramid.vue +876 -0
  7. package/components/pyramid/square-pyramid/SquarePyramidStack.vue +366 -0
  8. package/components/pyramid/square-pyramid/SquarePyramidStackContent.vue +204 -0
  9. package/components/pyramid/square-pyramid/SquarePyramidStackTitle.vue +134 -0
  10. package/package.json +2 -1
  11. package/utils/pyramid/circular-pyramid/context.ts +46 -0
  12. package/utils/pyramid/circular-pyramid/createCircularPyramidGeometry.ts +143 -0
  13. package/utils/pyramid/circular-pyramid/index.ts +3 -0
  14. package/utils/pyramid/circular-pyramid/types.ts +34 -0
  15. package/utils/pyramid/index.ts +3 -0
  16. package/utils/pyramid/shared/colors.ts +92 -0
  17. package/utils/pyramid/shared/connector.ts +111 -0
  18. package/utils/pyramid/shared/constants.ts +8 -0
  19. package/utils/pyramid/shared/index.ts +5 -0
  20. package/utils/pyramid/shared/stacking.ts +76 -0
  21. package/utils/pyramid/shared/types.ts +36 -0
  22. package/utils/pyramid/square-pyramid/context.ts +46 -0
  23. package/utils/pyramid/square-pyramid/createSquarePyramidGeometry.ts +265 -0
  24. package/utils/pyramid/square-pyramid/index.ts +3 -0
  25. package/utils/pyramid/square-pyramid/types.ts +39 -0
  26. package/utils/useDiagramAutoScale.ts +6 -8
@@ -0,0 +1,366 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ computed,
4
+ Fragment,
5
+ inject,
6
+ onUnmounted,
7
+ provide,
8
+ useSlots,
9
+ type VNode,
10
+ } from 'vue';
11
+
12
+ import {
13
+ DEFAULT_SQUARE_PYRAMID_COLOR,
14
+ DEFAULT_SQUARE_PYRAMID_STACK_COLORS,
15
+ SQUARE_PYRAMID_ROOT_KEY,
16
+ SQUARE_PYRAMID_STACK_KEY,
17
+ } from '../../../utils/pyramid/square-pyramid';
18
+ import SquarePyramidStackContent from './SquarePyramidStackContent.vue';
19
+ import SquarePyramidStackTitle from './SquarePyramidStackTitle.vue';
20
+
21
+ defineOptions({
22
+ name: 'SquarePyramidStack',
23
+ });
24
+
25
+ export interface ISquarePyramidStackProps {
26
+ readonly index?: number;
27
+ readonly step?: string | number;
28
+ readonly title?: string;
29
+ readonly content?: string;
30
+ readonly color?: string;
31
+ readonly active?: boolean;
32
+ readonly connectorWidth?: number;
33
+ }
34
+
35
+ const props = withDefaults(defineProps<ISquarePyramidStackProps>(), {
36
+ index: undefined,
37
+ step: undefined,
38
+ title: undefined,
39
+ content: undefined,
40
+ color: undefined,
41
+ active: undefined,
42
+ connectorWidth: undefined,
43
+ });
44
+
45
+ const slots = useSlots();
46
+ const rootContext = inject(SQUARE_PYRAMID_ROOT_KEY, undefined);
47
+
48
+ interface INamedComponent {
49
+ readonly type?: {
50
+ readonly name?: string;
51
+ readonly displayName?: string;
52
+ };
53
+ }
54
+
55
+ function hasComponentName(type: unknown, targetName: string): boolean {
56
+ if (typeof type === 'object' && type !== null) {
57
+ const comp = type as INamedComponent['type'];
58
+ return comp?.name === targetName || comp?.displayName === targetName;
59
+ }
60
+ return false;
61
+ }
62
+
63
+ function flattenVNodes(nodes: VNode[]): VNode[] {
64
+ const result: VNode[] = [];
65
+ for (const node of nodes) {
66
+ if (node.type === Fragment && Array.isArray(node.children)) {
67
+ result.push(...flattenVNodes(node.children as VNode[]));
68
+ } else {
69
+ result.push(node);
70
+ }
71
+ }
72
+ return result;
73
+ }
74
+
75
+ const defaultNodes = computed(() => {
76
+ if (!slots.default) {
77
+ return [];
78
+ }
79
+ return flattenVNodes(slots.default());
80
+ });
81
+
82
+ const slottedTitle = computed(() => {
83
+ return defaultNodes.value.find((node) =>
84
+ hasComponentName(node.type, 'SquarePyramidStackTitle'),
85
+ );
86
+ });
87
+
88
+ const slottedContent = computed(() => {
89
+ return defaultNodes.value.find((node) =>
90
+ hasComponentName(node.type, 'SquarePyramidStackContent'),
91
+ );
92
+ });
93
+
94
+ const stackToken = Symbol();
95
+ const effectiveColor = computed(() => {
96
+ return (
97
+ props.color || (slottedTitle.value?.props?.color as string | undefined)
98
+ );
99
+ });
100
+ const registration = rootContext?.registerStack(stackToken, effectiveColor);
101
+
102
+ onUnmounted(() => {
103
+ registration?.unregister();
104
+ });
105
+
106
+ const resolvedIndex = computed<number>(() => {
107
+ if (props.index !== undefined) {
108
+ return Number(props.index);
109
+ }
110
+ return registration?.index.value ?? 0;
111
+ });
112
+
113
+ const isActive = computed<boolean>(() => {
114
+ if (props.active !== undefined) {
115
+ return props.active;
116
+ }
117
+ if (!rootContext) {
118
+ return false;
119
+ }
120
+ return rootContext.activeIndex.value === resolvedIndex.value;
121
+ });
122
+
123
+ const resolvedStep = computed(() => {
124
+ if (props.step !== undefined) {
125
+ return props.step;
126
+ }
127
+ return String(resolvedIndex.value + 1).padStart(2, '0');
128
+ });
129
+
130
+ const layerGeometry = computed(() => {
131
+ if (!rootContext) {
132
+ return undefined;
133
+ }
134
+ return rootContext.geo.value.layers[resolvedIndex.value];
135
+ });
136
+
137
+ const posY = computed(() => {
138
+ if (!layerGeometry.value) {
139
+ return 0;
140
+ }
141
+ const geo = layerGeometry.value;
142
+ return (
143
+ geo.slabCenterY ?? Number((geo.cy + (geo.slabHeight ?? 0) / 2).toFixed(2))
144
+ );
145
+ });
146
+
147
+ const DOT_POSITION_X = 210;
148
+
149
+ const leftWidth = computed(() => {
150
+ if (layerGeometry.value) {
151
+ const rx = layerGeometry.value.rx;
152
+ const cx = 465;
153
+ return Math.max(180, Math.round(cx - rx - 40));
154
+ }
155
+ return DOT_POSITION_X;
156
+ });
157
+
158
+ const rightWidth = computed(() => rootContext?.contentWidthPx.value ?? 240);
159
+
160
+ const rightWingWidth = computed(() => {
161
+ if (layerGeometry.value) {
162
+ const rx = layerGeometry.value.rx;
163
+ const cx = 465;
164
+ const dotRightX = cx + rx + 40;
165
+ const containerW = 960;
166
+ return Math.max(rightWidth.value + 20, Math.round(containerW - dotRightX));
167
+ }
168
+ return rightWidth.value + 40;
169
+ });
170
+
171
+ const rightOffset = computed(
172
+ () => rootContext?.contentRightOffsetPx.value ?? 0,
173
+ );
174
+
175
+ const resolvedColor = computed<string>(() => {
176
+ if (effectiveColor.value) {
177
+ return effectiveColor.value;
178
+ }
179
+ return (
180
+ rootContext?.getLayerColor(resolvedIndex.value) ??
181
+ DEFAULT_SQUARE_PYRAMID_STACK_COLORS[
182
+ Math.abs(resolvedIndex.value) % DEFAULT_SQUARE_PYRAMID_STACK_COLORS.length
183
+ ] ??
184
+ DEFAULT_SQUARE_PYRAMID_COLOR
185
+ );
186
+ });
187
+
188
+ const hasActiveStack = computed(() => {
189
+ return (rootContext?.activeIndex.value ?? -1) >= 0;
190
+ });
191
+
192
+ function handleStackClick(): void {
193
+ if (rootContext?.interactive.value) {
194
+ if (rootContext.activeIndex.value === resolvedIndex.value) {
195
+ rootContext.setActiveIndex(-1);
196
+ } else {
197
+ rootContext.setActiveIndex(resolvedIndex.value);
198
+ }
199
+ }
200
+ }
201
+
202
+ const deltaY = computed(() => {
203
+ return rootContext?.getCardDeltaY?.(resolvedIndex.value) ?? 0;
204
+ });
205
+
206
+ const connectorWidth = computed(() => {
207
+ return Math.max(20, rightWingWidth.value - rightWidth.value);
208
+ });
209
+
210
+ provide(SQUARE_PYRAMID_STACK_KEY, {
211
+ index: resolvedIndex,
212
+ isActive,
213
+ step: resolvedStep,
214
+ layerGeo: layerGeometry,
215
+ color: resolvedColor,
216
+ deltaY,
217
+ connectorWidth,
218
+ setActive: handleStackClick,
219
+ });
220
+ </script>
221
+
222
+ <template>
223
+ <div
224
+ class="alpha-square-pyramid-stack absolute inset-x-0 pointer-events-none select-none flex items-center justify-between"
225
+ :class="[
226
+ isActive
227
+ ? 'alpha-square-pyramid-stack--active'
228
+ : 'alpha-square-pyramid-stack--inactive',
229
+ `alpha-square-pyramid-stack--${resolvedIndex}`,
230
+ ]"
231
+ :data-index="resolvedIndex"
232
+ :data-active="isActive"
233
+ :style="{
234
+ top: `${posY}px`,
235
+ transform: 'translateY(-50%)',
236
+ }"
237
+ >
238
+ <!-- Left Wing: Title Pill (150px) -->
239
+ <div
240
+ class="alpha-square-pyramid-stack__left flex-shrink-0 flex items-center justify-start pointer-events-auto cursor-pointer"
241
+ :class="[
242
+ `alpha-square-pyramid-stack__left--${resolvedIndex}`,
243
+ { 'alpha-square-pyramid-stack--animated': rootContext?.animation.value },
244
+ ]"
245
+ :style="{
246
+ width: `${leftWidth}px`,
247
+ minWidth: `${leftWidth}px`,
248
+ maxWidth: `${leftWidth}px`,
249
+ opacity: !hasActiveStack || isActive ? 1 : 0.65,
250
+ }"
251
+ @click="handleStackClick"
252
+ >
253
+ <component :is="slottedTitle" v-if="slottedTitle" />
254
+ <SquarePyramidStackTitle
255
+ v-else
256
+ :step="resolvedStep"
257
+ :title="props.title || `Stage ${resolvedStep}`"
258
+ :connector-width="props.connectorWidth"
259
+ />
260
+ </div>
261
+ <!-- Right Wing: Dot + Connector + Content Card (40px from slab) -->
262
+ <div
263
+ class="alpha-square-pyramid-stack__right flex-shrink-0 flex items-center justify-end pointer-events-auto cursor-pointer"
264
+ :class="[
265
+ `alpha-square-pyramid-stack__right--${resolvedIndex}`,
266
+ { 'alpha-square-pyramid-stack--animated': rootContext?.animation.value },
267
+ ]"
268
+ :style="{
269
+ width: `${rightWingWidth}px`,
270
+ minWidth: `${rightWidth}px`,
271
+ maxWidth: `${rightWingWidth}px`,
272
+ marginRight: rightOffset ? `${rightOffset}px` : undefined,
273
+ opacity: !hasActiveStack || isActive ? 1 : 0.65,
274
+ }"
275
+ @click="handleStackClick"
276
+ >
277
+ <component :is="slottedContent" v-if="slottedContent" />
278
+ <SquarePyramidStackContent
279
+ v-else
280
+ :content="props.content || 'Layer details and operational description.'"
281
+ />
282
+ </div>
283
+ </div>
284
+ </template>
285
+
286
+ <style scoped>
287
+ .alpha-square-pyramid-stack {
288
+ height: 0;
289
+ overflow: visible;
290
+ }
291
+
292
+ .alpha-square-pyramid-stack__left,
293
+ .alpha-square-pyramid-stack__right {
294
+ transition: opacity 0.3s ease, transform 0.3s ease;
295
+ }
296
+
297
+ .alpha-square-pyramid-stack--animated.alpha-square-pyramid-stack__left--0 {
298
+ animation: alpha-pyramid-left-in 450ms cubic-bezier(0.16, 1, 0.3, 1) calc(var(--square-pyramid-start-delay, 0ms) + 100ms) both;
299
+ }
300
+
301
+ .alpha-square-pyramid-stack--animated.alpha-square-pyramid-stack__left--1 {
302
+ animation: alpha-pyramid-left-in 450ms cubic-bezier(0.16, 1, 0.3, 1) calc(var(--square-pyramid-start-delay, 0ms) + 180ms) both;
303
+ }
304
+
305
+ .alpha-square-pyramid-stack--animated.alpha-square-pyramid-stack__left--2 {
306
+ animation: alpha-pyramid-left-in 450ms cubic-bezier(0.16, 1, 0.3, 1) calc(var(--square-pyramid-start-delay, 0ms) + 260ms) both;
307
+ }
308
+
309
+ .alpha-square-pyramid-stack--animated.alpha-square-pyramid-stack__left--3 {
310
+ animation: alpha-pyramid-left-in 450ms cubic-bezier(0.16, 1, 0.3, 1) calc(var(--square-pyramid-start-delay, 0ms) + 340ms) both;
311
+ }
312
+
313
+ .alpha-square-pyramid-stack--animated.alpha-square-pyramid-stack__left--4 {
314
+ animation: alpha-pyramid-left-in 450ms cubic-bezier(0.16, 1, 0.3, 1) calc(var(--square-pyramid-start-delay, 0ms) + 420ms) both;
315
+ }
316
+
317
+ .alpha-square-pyramid-stack--animated.alpha-square-pyramid-stack__left--5 {
318
+ animation: alpha-pyramid-left-in 450ms cubic-bezier(0.16, 1, 0.3, 1) calc(var(--square-pyramid-start-delay, 0ms) + 500ms) both;
319
+ }
320
+
321
+ @keyframes alpha-pyramid-left-in {
322
+ 0% {
323
+ opacity: 0;
324
+ transform: translateX(-12px);
325
+ }
326
+ 100% {
327
+ opacity: 1;
328
+ transform: translateX(0);
329
+ }
330
+ }
331
+
332
+ .alpha-square-pyramid-stack--animated.alpha-square-pyramid-stack__right--0 {
333
+ animation: alpha-pyramid-right-in 450ms cubic-bezier(0.16, 1, 0.3, 1) calc(var(--square-pyramid-start-delay, 0ms) + 120ms) both;
334
+ }
335
+
336
+ .alpha-square-pyramid-stack--animated.alpha-square-pyramid-stack__right--1 {
337
+ animation: alpha-pyramid-right-in 450ms cubic-bezier(0.16, 1, 0.3, 1) calc(var(--square-pyramid-start-delay, 0ms) + 200ms) both;
338
+ }
339
+
340
+ .alpha-square-pyramid-stack--animated.alpha-square-pyramid-stack__right--2 {
341
+ animation: alpha-pyramid-right-in 450ms cubic-bezier(0.16, 1, 0.3, 1) calc(var(--square-pyramid-start-delay, 0ms) + 280ms) both;
342
+ }
343
+
344
+ .alpha-square-pyramid-stack--animated.alpha-square-pyramid-stack__right--3 {
345
+ animation: alpha-pyramid-right-in 450ms cubic-bezier(0.16, 1, 0.3, 1) calc(var(--square-pyramid-start-delay, 0ms) + 360ms) both;
346
+ }
347
+
348
+ .alpha-square-pyramid-stack--animated.alpha-square-pyramid-stack__right--4 {
349
+ animation: alpha-pyramid-right-in 450ms cubic-bezier(0.16, 1, 0.3, 1) calc(var(--square-pyramid-start-delay, 0ms) + 440ms) both;
350
+ }
351
+
352
+ .alpha-square-pyramid-stack--animated.alpha-square-pyramid-stack__right--5 {
353
+ animation: alpha-pyramid-right-in 450ms cubic-bezier(0.16, 1, 0.3, 1) calc(var(--square-pyramid-start-delay, 0ms) + 520ms) both;
354
+ }
355
+
356
+ @keyframes alpha-pyramid-right-in {
357
+ 0% {
358
+ opacity: 0;
359
+ transform: translateX(12px);
360
+ }
361
+ 100% {
362
+ opacity: 1;
363
+ transform: translateX(0);
364
+ }
365
+ }
366
+ </style>
@@ -0,0 +1,204 @@
1
+ <script setup lang="ts">
2
+ import { computed, inject, onMounted, onUnmounted, ref } from 'vue';
3
+
4
+ import { createPyramidConnectorPath } from '../../../utils/pyramid/shared';
5
+ import {
6
+ SQUARE_PYRAMID_ROOT_KEY,
7
+ SQUARE_PYRAMID_STACK_KEY,
8
+ } from '../../../utils/pyramid/square-pyramid';
9
+
10
+ defineOptions({
11
+ name: 'SquarePyramidStackContent',
12
+ });
13
+
14
+ export interface ISquarePyramidStackContentProps {
15
+ readonly content?: string;
16
+ readonly color?: string;
17
+ readonly connectorWidth?: number;
18
+ }
19
+
20
+ const props = withDefaults(defineProps<ISquarePyramidStackContentProps>(), {
21
+ content: undefined,
22
+ color: undefined,
23
+ connectorWidth: undefined,
24
+ });
25
+
26
+ const rootContext = inject(SQUARE_PYRAMID_ROOT_KEY, undefined);
27
+ const stackContext = inject(SQUARE_PYRAMID_STACK_KEY, undefined);
28
+
29
+ const connectorRef = ref<HTMLElement | null>(null);
30
+ const measuredConnectorWidth = ref<number>(0);
31
+
32
+ let resizeObserver: ResizeObserver | null = null;
33
+
34
+ function updateWidth(): void {
35
+ if (connectorRef.value && connectorRef.value.clientWidth > 0) {
36
+ measuredConnectorWidth.value = connectorRef.value.clientWidth;
37
+ }
38
+ }
39
+
40
+ onMounted(() => {
41
+ updateWidth();
42
+ if (typeof ResizeObserver !== 'undefined' && connectorRef.value) {
43
+ resizeObserver = new ResizeObserver(updateWidth);
44
+ resizeObserver.observe(connectorRef.value);
45
+ }
46
+ });
47
+
48
+ onUnmounted(() => {
49
+ resizeObserver?.disconnect();
50
+ });
51
+
52
+ const isActive = computed(() => {
53
+ return stackContext?.isActive.value ?? false;
54
+ });
55
+
56
+ const contentWidth = computed(() => rootContext?.contentWidthPx.value ?? 240);
57
+
58
+ const deltaY = computed(() => {
59
+ if (stackContext?.deltaY?.value !== undefined) {
60
+ return stackContext.deltaY.value;
61
+ }
62
+ const idx = stackContext?.index.value ?? 0;
63
+ return rootContext?.getCardDeltaY?.(idx) ?? 0;
64
+ });
65
+
66
+ const effectiveDeltaY = computed(() => deltaY.value ?? 0);
67
+
68
+ const connectorWidth = computed(() => {
69
+ if (props.connectorWidth !== undefined) {
70
+ return props.connectorWidth;
71
+ }
72
+ if (stackContext?.connectorWidth?.value !== undefined) {
73
+ return stackContext.connectorWidth.value;
74
+ }
75
+ return measuredConnectorWidth.value || 40;
76
+ });
77
+
78
+ const svgMargin = 4;
79
+ const svgHeight = computed(() => {
80
+ return Math.max(8, Math.abs(effectiveDeltaY.value) + svgMargin * 2);
81
+ });
82
+
83
+ const svgMinY = computed(() => {
84
+ return Math.min(0, effectiveDeltaY.value) - svgMargin;
85
+ });
86
+
87
+ const svgTop = computed(() => {
88
+ return Math.min(0, effectiveDeltaY.value) - svgMargin;
89
+ });
90
+
91
+ const connectorPath = computed(() => {
92
+ const w = connectorWidth.value;
93
+ return createPyramidConnectorPath(
94
+ { x: 0, y: 0 },
95
+ { x: w, y: effectiveDeltaY.value },
96
+ );
97
+ });
98
+
99
+ const stackColor = computed(() => {
100
+ return (
101
+ props.color ??
102
+ stackContext?.color?.value ??
103
+ rootContext?.color.value ??
104
+ '#3b82f6'
105
+ );
106
+ });
107
+ </script>
108
+
109
+ <template>
110
+ <div class="alpha-square-pyramid-stack-content-wrapper relative w-full flex items-center select-none overflow-visible">
111
+ <!-- Scalable Line between Dot and Content Card (always at least 20px) -->
112
+ <div
113
+ ref="connectorRef"
114
+ class="alpha-square-pyramid-stack-content__connector flex-1 flex items-center min-w-[20px] pointer-events-none relative overflow-visible h-0"
115
+ >
116
+ <div
117
+ class="alpha-square-pyramid-stack-content__dot absolute left-0 top-0 -translate-x-1/2 -translate-y-1/2 w-1.5 h-1.5 rounded-full flex-shrink-0 transition-all duration-300 pointer-events-auto"
118
+ :style="{
119
+ backgroundColor: stackColor,
120
+ boxShadow: isActive
121
+ ? `0 0 6px ${stackColor}`
122
+ : `0 0 2px ${stackColor}80`,
123
+ }"
124
+ />
125
+ <!-- Straight horizontal line when deltaY is 0 -->
126
+ <div
127
+ v-if="!effectiveDeltaY"
128
+ class="alpha-square-pyramid-stack-content__line absolute left-0 top-0 w-full h-[1px] -translate-y-1/2 pointer-events-none transition-colors duration-300"
129
+ :style="{
130
+ backgroundColor: stackColor,
131
+ opacity: isActive ? 1 : 0.65,
132
+ }"
133
+ />
134
+ <!-- Zigzag SVG connector when deltaY !== 0 -->
135
+ <svg
136
+ v-else
137
+ class="alpha-square-pyramid-stack-content__line-svg absolute left-0 pointer-events-none overflow-visible"
138
+ :width="connectorWidth"
139
+ :height="svgHeight"
140
+ :viewBox="`0 ${svgMinY} ${connectorWidth} ${svgHeight}`"
141
+ :style="{
142
+ top: `${svgTop}px`,
143
+ width: `${connectorWidth}px`,
144
+ height: `${svgHeight}px`,
145
+ }"
146
+ >
147
+ <path
148
+ class="alpha-square-pyramid-stack-content__line transition-all duration-300"
149
+ :d="connectorPath"
150
+ fill="none"
151
+ :stroke="stackColor"
152
+ stroke-width="1.25"
153
+ stroke-linecap="round"
154
+ stroke-linejoin="round"
155
+ :style="{
156
+ opacity: isActive ? 1 : 0.65,
157
+ }"
158
+ />
159
+ </svg>
160
+ </div>
161
+ <!-- Content Card -->
162
+ <div
163
+ class="alpha-square-pyramid-stack-content rounded-xl p-2 px-2.5 transition-all duration-300 border select-none box-border flex-shrink-0"
164
+ :class="[
165
+ isActive
166
+ ? 'alpha-square-pyramid-stack-content--active'
167
+ : 'alpha-square-pyramid-stack-content--default',
168
+ ]"
169
+ :style="{
170
+ width: `${contentWidth}px`,
171
+ minWidth: `${contentWidth}px`,
172
+ maxWidth: `${contentWidth}px`,
173
+ boxSizing: 'border-box',
174
+ borderColor: stackColor,
175
+ backgroundColor: isActive
176
+ ? 'rgba(255, 255, 255, 0.95)'
177
+ : 'rgba(255, 255, 255, 0.85)',
178
+ boxShadow: isActive
179
+ ? `0 4px 14px ${stackColor}26`
180
+ : 'none',
181
+ transform: deltaY ? `translateY(${deltaY}px)` : undefined,
182
+ }"
183
+ >
184
+ <div
185
+ class="text-[10px] leading-snug transition-colors duration-300 break-words line-clamp-3"
186
+ :style="{
187
+ color: isActive ? '#0f172a' : '#475569',
188
+ }"
189
+ >
190
+ <slot>{{ props.content }}</slot>
191
+ </div>
192
+ </div>
193
+ </div>
194
+ </template>
195
+
196
+ <style scoped>
197
+ .alpha-square-pyramid-stack-content {
198
+ box-sizing: border-box;
199
+ }
200
+
201
+ .alpha-square-pyramid-stack-content--active {
202
+ backdrop-filter: blur(8px);
203
+ }
204
+ </style>
@@ -0,0 +1,134 @@
1
+ <script setup lang="ts">
2
+ import { computed, inject } from 'vue';
3
+
4
+ import {
5
+ SQUARE_PYRAMID_ROOT_KEY,
6
+ SQUARE_PYRAMID_STACK_KEY,
7
+ } from '../../../utils/pyramid/square-pyramid';
8
+
9
+ defineOptions({
10
+ name: 'SquarePyramidStackTitle',
11
+ });
12
+
13
+ export interface ISquarePyramidStackTitleProps {
14
+ readonly step?: string | number;
15
+ readonly title?: string;
16
+ readonly color?: string;
17
+ readonly minWidth?: number;
18
+ }
19
+
20
+ const props = withDefaults(defineProps<ISquarePyramidStackTitleProps>(), {
21
+ step: undefined,
22
+ title: undefined,
23
+ color: undefined,
24
+ minWidth: 150,
25
+ });
26
+
27
+ const rootContext = inject(SQUARE_PYRAMID_ROOT_KEY, undefined);
28
+ const stackContext = inject(SQUARE_PYRAMID_STACK_KEY, undefined);
29
+
30
+ const isActive = computed(() => {
31
+ return stackContext?.isActive.value ?? false;
32
+ });
33
+
34
+ const resolvedStep = computed(() => {
35
+ if (props.step !== undefined) {
36
+ return props.step;
37
+ }
38
+ if (stackContext?.step.value !== undefined) {
39
+ return stackContext.step.value;
40
+ }
41
+ return '01';
42
+ });
43
+
44
+ const minPillWidth = computed(() => props.minWidth ?? 150);
45
+
46
+ const stackColor = computed(() => {
47
+ return (
48
+ props.color ??
49
+ stackContext?.color?.value ??
50
+ rootContext?.color.value ??
51
+ '#3b82f6'
52
+ );
53
+ });
54
+ </script>
55
+
56
+ <template>
57
+ <div
58
+ class="alpha-square-pyramid-stack-title relative w-full flex items-center select-none"
59
+ :class="[
60
+ isActive
61
+ ? 'alpha-square-pyramid-stack-title--active'
62
+ : 'alpha-square-pyramid-stack-title--default',
63
+ ]"
64
+ >
65
+ <!-- Pill Badge Container -->
66
+ <div
67
+ class="alpha-square-pyramid-stack-title__pill flex items-center gap-2 px-2.5 py-1 rounded-full transition-all duration-300 border flex-shrink-0"
68
+ :style="{
69
+ minWidth: `${minPillWidth}px`,
70
+ maxWidth: 'calc(100% - 30px)',
71
+ width: 'fit-content',
72
+ boxSizing: 'border-box',
73
+ borderColor: stackColor,
74
+ backgroundColor: isActive
75
+ ? 'rgba(255, 255, 255, 0.95)'
76
+ : 'rgba(255, 255, 255, 0.85)',
77
+ boxShadow: isActive
78
+ ? `0 4px 14px ${stackColor}26`
79
+ : 'none',
80
+ }"
81
+ >
82
+ <!-- Circular Step Indicator -->
83
+ <div
84
+ class="w-5.5 h-5.5 rounded-full flex items-center justify-center text-[10px] font-bold tracking-tight transition-all duration-300 flex-shrink-0"
85
+ :style="{
86
+ backgroundColor: stackColor,
87
+ color: '#ffffff',
88
+ }"
89
+ >
90
+ <slot name="step">{{ resolvedStep }}</slot>
91
+ </div>
92
+ <!-- Title Text -->
93
+ <span
94
+ class="text-xs font-semibold tracking-tight transition-colors duration-300 pr-1 truncate flex-1 leading-tight"
95
+ :style="{
96
+ color: isActive ? '#0f172a' : '#334155',
97
+ }"
98
+ >
99
+ <slot>{{ props.title }}</slot>
100
+ </span>
101
+ </div>
102
+ <!-- Scalable Line between Title and Dot -->
103
+ <div
104
+ class="alpha-square-pyramid-stack-title__connector flex-1 flex items-center min-w-[30px] pointer-events-none"
105
+ >
106
+ <div
107
+ class="alpha-square-pyramid-stack-title__line flex-1 h-[1px] transition-colors duration-300"
108
+ :style="{
109
+ backgroundColor: stackColor,
110
+ opacity: isActive ? 1 : 0.65,
111
+ }"
112
+ />
113
+ <div
114
+ class="alpha-square-pyramid-stack-title__dot w-1.5 h-1.5 rounded-full flex-shrink-0 -ml-1 transition-all duration-300"
115
+ :style="{
116
+ backgroundColor: stackColor,
117
+ boxShadow: isActive
118
+ ? `0 0 6px ${stackColor}`
119
+ : `0 0 2px ${stackColor}80`,
120
+ }"
121
+ />
122
+ </div>
123
+ </div>
124
+ </template>
125
+
126
+ <style scoped>
127
+ .alpha-square-pyramid-stack-title {
128
+ box-sizing: border-box;
129
+ }
130
+
131
+ .alpha-square-pyramid-stack-title--active .alpha-square-pyramid-stack-title__pill {
132
+ backdrop-filter: blur(8px);
133
+ }
134
+ </style>