@alphacifer/slidev-addon-theme 0.0.7 → 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 (128) hide show
  1. package/README.md +186 -76
  2. package/components/{chevron-card → card/chevron-card}/ChevronCard.vue +2 -2
  3. package/components/{chevron-card → card/chevron-card}/ChevronCardBadge.vue +1 -1
  4. package/components/{chevron-card → card/chevron-card}/ChevronCardContent.vue +1 -1
  5. package/components/{chevron-card → card/chevron-card}/ChevronCardHeading.vue +1 -1
  6. package/components/{horiz-card → card/horiz-card}/HorizCard.vue +2 -2
  7. package/components/{horiz-card → card/horiz-card}/HorizCardBadge.vue +2 -2
  8. package/components/{horiz-card → card/horiz-card}/HorizCardContent.vue +2 -2
  9. package/components/{horiz-card → card/horiz-card}/HorizCardHeading.vue +2 -2
  10. package/components/{horiz-card → card/horiz-card}/HorizCardIcon.vue +2 -2
  11. package/components/{vert-card → card/vert-card}/VertCard.vue +2 -2
  12. package/components/{vert-card → card/vert-card}/VertCardBadge.vue +5 -2
  13. package/components/{vert-card → card/vert-card}/VertCardContent.vue +2 -2
  14. package/components/{vert-card → card/vert-card}/VertCardHeading.vue +2 -2
  15. package/components/{vert-card → card/vert-card}/VertCardIcon.vue +2 -2
  16. package/components/{vert-card → card/vert-card}/VertCardTitle.vue +2 -2
  17. package/components/comparisons/arc-comparison/ArcComparison.vue +654 -0
  18. package/components/comparisons/arc-comparison/ArcComparisonBadge.vue +29 -0
  19. package/components/comparisons/arc-comparison/ArcComparisonContent.vue +29 -0
  20. package/components/comparisons/arc-comparison/ArcComparisonContents.vue +17 -0
  21. package/components/comparisons/arc-comparison/ArcComparisonHeading.vue +29 -0
  22. package/components/comparisons/arc-comparison/ArcComparisonLeft.vue +268 -0
  23. package/components/comparisons/arc-comparison/ArcComparisonOrbit.vue +47 -0
  24. package/components/comparisons/arc-comparison/ArcComparisonRight.vue +271 -0
  25. package/components/comparisons/arc-comparison/ArcComparisonTitle.vue +29 -0
  26. package/components/{comparison-table/ComparisonTable.vue → comparisons/table-comparison/TableComparison.vue} +37 -27
  27. package/components/{comparison-table/ComparisonTableCell.vue → comparisons/table-comparison/TableComparisonCell.vue} +8 -8
  28. package/components/{comparison-table/ComparisonTableCol.vue → comparisons/table-comparison/TableComparisonCol.vue} +10 -10
  29. package/components/{comparison-table/ComparisonTableCols.vue → comparisons/table-comparison/TableComparisonCols.vue} +11 -11
  30. package/components/{comparison-table/ComparisonTableRow.vue → comparisons/table-comparison/TableComparisonRow.vue} +9 -9
  31. package/components/{comparison-table/ComparisonTableRows.vue → comparisons/table-comparison/TableComparisonRows.vue} +3 -3
  32. package/components/core/ReflectedTitle.vue +2 -1
  33. package/components/core/arc-orbit/ArcOrbit.vue +468 -0
  34. package/components/{arc-compare/ArcCompareBadge.vue → core/arc-orbit/ArcOrbitBadge.vue} +11 -8
  35. package/components/core/arc-orbit/ArcOrbitCallout.vue +400 -0
  36. package/components/{arc-compare/ArcCompareContent.vue → core/arc-orbit/ArcOrbitContent.vue} +8 -8
  37. package/components/{arc-compare/ArcCompareContents.vue → core/arc-orbit/ArcOrbitContents.vue} +3 -3
  38. package/components/{arc-compare/ArcCompareHeading.vue → core/arc-orbit/ArcOrbitHeading.vue} +11 -8
  39. package/components/core/arc-orbit/ArcOrbitTitle.vue +50 -0
  40. package/components/{thanks → core/thanks}/ThanksOutlineSquare.vue +1 -1
  41. package/components/{thanks → core/thanks}/ThanksSquare.vue +1 -1
  42. package/components/{quad-hub/QuadHub.vue → facets/tetrad/rect-orbit-tetrad/RectOrbitTetrad.vue} +71 -69
  43. package/components/{quad-hub/QuadHubCallout.vue → facets/tetrad/rect-orbit-tetrad/RectOrbitTetradCallout.vue} +45 -43
  44. package/components/{quad-hub/QuadHubCenter.vue → facets/tetrad/rect-orbit-tetrad/RectOrbitTetradCenter.vue} +4 -4
  45. package/components/{quad-hub/QuadHubContent.vue → facets/tetrad/rect-orbit-tetrad/RectOrbitTetradContent.vue} +6 -6
  46. package/components/{quad-hub/QuadHubHeading.vue → facets/tetrad/rect-orbit-tetrad/RectOrbitTetradHeading.vue} +6 -6
  47. package/components/{quad-hub/QuadHubIcon.vue → facets/tetrad/rect-orbit-tetrad/RectOrbitTetradIcon.vue} +6 -6
  48. package/components/{arrow-triad → facets/triad/arrow-triad}/ArrowTriad.vue +3 -3
  49. package/components/{arrow-triad → facets/triad/arrow-triad}/ArrowTriadCallout.vue +1 -1
  50. package/components/{arrow-triad → facets/triad/arrow-triad}/ArrowTriadContent.vue +1 -1
  51. package/components/{arrow-triad → facets/triad/arrow-triad}/ArrowTriadHeading.vue +1 -1
  52. package/components/facets/triad/gear-triad/GearTriad.vue +591 -0
  53. package/components/facets/triad/gear-triad/GearTriadCallout.vue +285 -0
  54. package/components/facets/triad/gear-triad/GearTriadCenter.vue +41 -0
  55. package/components/facets/triad/gear-triad/GearTriadContent.vue +58 -0
  56. package/components/facets/triad/gear-triad/GearTriadContents.vue +111 -0
  57. package/components/facets/triad/gear-triad/GearTriadDescription.vue +38 -0
  58. package/components/facets/triad/gear-triad/GearTriadHeading.vue +38 -0
  59. package/components/facets/triad/gear-triad/GearTriadIcon.vue +157 -0
  60. package/components/{hex-triad → facets/triad/hex-triad}/HexTriad.vue +2 -2
  61. package/components/{hex-triad → facets/triad/hex-triad}/HexTriadBadge.vue +1 -1
  62. package/components/{hex-triad → facets/triad/hex-triad}/HexTriadCallout.vue +1 -1
  63. package/components/{hex-triad → facets/triad/hex-triad}/HexTriadHeading.vue +1 -1
  64. package/components/{arc-arrow-process → process/arc-arrow-process}/ArcArrowProcess.vue +2 -2
  65. package/components/{arc-arrow-process → process/arc-arrow-process}/ArcArrowProcessArrow.vue +1 -1
  66. package/components/{arc-arrow-process → process/arc-arrow-process}/ArcArrowProcessCallout.vue +1 -1
  67. package/components/{arc-arrow-process → process/arc-arrow-process}/ArcArrowProcessContent.vue +1 -1
  68. package/components/{arc-arrow-process → process/arc-arrow-process}/ArcArrowProcessHeading.vue +1 -1
  69. package/components/pyramid/circular-pyramid/CircularPyramid.vue +871 -0
  70. package/components/pyramid/circular-pyramid/CircularPyramidStack.vue +375 -0
  71. package/components/pyramid/circular-pyramid/CircularPyramidStackContent.vue +208 -0
  72. package/components/pyramid/circular-pyramid/CircularPyramidStackTitle.vue +138 -0
  73. package/components/pyramid/square-pyramid/SquarePyramid.vue +876 -0
  74. package/components/pyramid/square-pyramid/SquarePyramidStack.vue +366 -0
  75. package/components/pyramid/square-pyramid/SquarePyramidStackContent.vue +204 -0
  76. package/components/pyramid/square-pyramid/SquarePyramidStackTitle.vue +134 -0
  77. package/package.json +2 -1
  78. package/utils/arcComparison/context.ts +40 -0
  79. package/utils/arcComparison/createArcComparisonGeometry.ts +175 -0
  80. package/utils/arcComparison/index.ts +3 -0
  81. package/utils/{arcCompare/resolveArcCompareItems.ts → arcComparison/resolveArcComparisonItems.ts} +24 -45
  82. package/utils/arcOrbit/context.ts +38 -0
  83. package/utils/{arcCompare/createArcCompareGeometry.ts → arcOrbit/createArcOrbitGeometry.ts} +77 -108
  84. package/utils/arcOrbit/index.ts +3 -0
  85. package/utils/arcOrbit/resolveArcOrbitItems.ts +92 -0
  86. package/utils/gearTriad/context.ts +51 -0
  87. package/utils/gearTriad/createGearTriadGeometry.ts +210 -0
  88. package/utils/gearTriad/index.ts +3 -0
  89. package/utils/gearTriad/resolveGearTriadItems.ts +159 -0
  90. package/utils/pyramid/circular-pyramid/context.ts +46 -0
  91. package/utils/pyramid/circular-pyramid/createCircularPyramidGeometry.ts +143 -0
  92. package/utils/pyramid/circular-pyramid/index.ts +3 -0
  93. package/utils/pyramid/circular-pyramid/types.ts +34 -0
  94. package/utils/pyramid/index.ts +3 -0
  95. package/utils/pyramid/shared/colors.ts +92 -0
  96. package/utils/pyramid/shared/connector.ts +111 -0
  97. package/utils/pyramid/shared/constants.ts +8 -0
  98. package/utils/pyramid/shared/index.ts +5 -0
  99. package/utils/pyramid/shared/stacking.ts +76 -0
  100. package/utils/pyramid/shared/types.ts +36 -0
  101. package/utils/pyramid/square-pyramid/context.ts +46 -0
  102. package/utils/pyramid/square-pyramid/createSquarePyramidGeometry.ts +265 -0
  103. package/utils/pyramid/square-pyramid/index.ts +3 -0
  104. package/utils/pyramid/square-pyramid/types.ts +39 -0
  105. package/utils/rectOrbitTetrad/context.ts +31 -0
  106. package/utils/{quadHub/createQuadHubGeometry.ts → rectOrbitTetrad/createRectOrbitTetradGeometry.ts} +33 -28
  107. package/utils/rectOrbitTetrad/index.ts +3 -0
  108. package/utils/{quadHub/resolveQuadHubItems.ts → rectOrbitTetrad/resolveRectOrbitTetradItems.ts} +25 -25
  109. package/utils/{comparisonTable → tableComparison}/context.ts +6 -6
  110. package/utils/useDiagramAutoScale.ts +7 -9
  111. package/components/arc-compare/ArcCompare.vue +0 -443
  112. package/components/arc-compare/ArcCompareCallout.vue +0 -386
  113. package/components/arc-compare/ArcCompareLeft.vue +0 -183
  114. package/components/arc-compare/ArcCompareRight.vue +0 -183
  115. package/components/arc-compare/ArcCompareTitle.vue +0 -47
  116. package/utils/arcCompare/context.ts +0 -36
  117. package/utils/arcCompare/index.ts +0 -3
  118. package/utils/quadHub/context.ts +0 -26
  119. package/utils/quadHub/index.ts +0 -3
  120. /package/components/{shifting-heading → core}/TransitionHeading.vue +0 -0
  121. /package/components/{thanks → core/thanks}/ThanksContent.vue +0 -0
  122. /package/components/{arrow-triad → facets/triad/arrow-triad}/ArrowTriadIcon.vue +0 -0
  123. /package/components/{hex-triad → facets/triad/hex-triad}/HexTriadCenterBadge.vue +0 -0
  124. /package/components/{hex-triad → facets/triad/hex-triad}/HexTriadContent.vue +0 -0
  125. /package/components/{hex-triad → facets/triad/hex-triad}/HexTriadLayer.vue +0 -0
  126. /package/components/{arc-arrow-process → process/arc-arrow-process}/ArcArrowProcessIcon.vue +0 -0
  127. /package/utils/{comparisonTable → tableComparison}/constants.ts +0 -0
  128. /package/utils/{comparisonTable → tableComparison}/index.ts +0 -0
@@ -0,0 +1,871 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ type ComputedRef,
4
+ computed,
5
+ Fragment,
6
+ inject,
7
+ onMounted,
8
+ onUnmounted,
9
+ provide,
10
+ ref,
11
+ useSlots,
12
+ type VNode,
13
+ watch,
14
+ } from 'vue';
15
+
16
+ import {
17
+ CIRCULAR_PYRAMID_ROOT_KEY,
18
+ clampCircularCount,
19
+ createCircularPyramidGeometry,
20
+ DEFAULT_CIRCULAR_PYRAMID_COLOR,
21
+ DEFAULT_CIRCULAR_PYRAMID_STACK_COLORS,
22
+ darkenColor,
23
+ lightenColor,
24
+ resolveCircularPyramidPalette,
25
+ } from '../../../utils/pyramid/circular-pyramid';
26
+ import { resolvePyramidCardPositions } from '../../../utils/pyramid/shared';
27
+ import { useDiagramAutoScale } from '../../../utils/useDiagramAutoScale';
28
+ import { useMergedUnoAttrs } from '../../../utils/useMergedUnoAttrs';
29
+ import CircularPyramidStack from './CircularPyramidStack.vue';
30
+
31
+ defineOptions({
32
+ inheritAttrs: false,
33
+ name: 'CircularPyramid',
34
+ });
35
+
36
+ export interface ICircularPyramidProps {
37
+ /**
38
+ * Number of circular layers in the pyramid (1 to 6).
39
+ * Automatically inferred from slotted `<CircularPyramidStack>` components if omitted.
40
+ * Default: 4
41
+ */
42
+ readonly count?: number;
43
+ /**
44
+ * Index of the active circular layer (0 = top, count - 1 = bottom).
45
+ * Default: -1 (none active)
46
+ */
47
+ readonly activeIndex?: number;
48
+ /**
49
+ * Primary fallback theme color for the pyramid.
50
+ * Default: '#3b82f6'
51
+ */
52
+ readonly color?: string;
53
+ /**
54
+ * Optional custom palette for stacks/layers.
55
+ * If omitted, distinct defaults from DEFAULT_CIRCULAR_PYRAMID_STACK_COLORS are used.
56
+ */
57
+ readonly colors?: readonly string[] | string[];
58
+ /**
59
+ * Container width. Supports numeric px or CSS unit strings.
60
+ */
61
+ readonly width?: number | string;
62
+ /**
63
+ * Container height. Supports numeric px or CSS unit strings.
64
+ */
65
+ readonly height?: number | string;
66
+ /**
67
+ * Manual zoom/scale factor.
68
+ */
69
+ readonly scale?: number;
70
+ /**
71
+ * Enable responsive auto-scaling to fit container.
72
+ * Default: true
73
+ */
74
+ readonly autoScale?: boolean;
75
+ /**
76
+ * Maximum allowed auto-scale factor.
77
+ */
78
+ readonly maxScale?: number;
79
+ /**
80
+ * Whether clicking a layer or stack changes the active layer.
81
+ * Default: true
82
+ */
83
+ readonly interactive?: boolean;
84
+ /**
85
+ * Enable entrance and state transition animations.
86
+ * Default: true
87
+ */
88
+ readonly animation?: boolean;
89
+ /**
90
+ * Start delay (ms) for entrance animations.
91
+ */
92
+ readonly startDelay?: number;
93
+ /**
94
+ * Custom viewBox width. Default: 960
95
+ */
96
+ readonly viewBoxWidth?: number;
97
+ /**
98
+ * Custom viewBox height. Default: 440
99
+ */
100
+ readonly viewBoxHeight?: number;
101
+ }
102
+
103
+ const props = withDefaults(defineProps<ICircularPyramidProps>(), {
104
+ count: undefined,
105
+ activeIndex: -1,
106
+ color: DEFAULT_CIRCULAR_PYRAMID_COLOR,
107
+ colors: undefined,
108
+ width: undefined,
109
+ height: undefined,
110
+ scale: undefined,
111
+ autoScale: true,
112
+ maxScale: undefined,
113
+ interactive: true,
114
+ animation: true,
115
+ startDelay: undefined,
116
+ viewBoxWidth: 960,
117
+ viewBoxHeight: 440,
118
+ });
119
+
120
+ const emit = defineEmits<{
121
+ (e: 'update:activeIndex', index: number): void;
122
+ (e: 'select', index: number): void;
123
+ (e: 'click', event: MouseEvent): void;
124
+ }>();
125
+
126
+ const slots = useSlots();
127
+ const rootRef = ref<HTMLElement | null>(null);
128
+
129
+ const shiftingIntro = inject<
130
+ | {
131
+ active: ComputedRef<boolean>;
132
+ isShifting?: ComputedRef<boolean>;
133
+ }
134
+ | undefined
135
+ >('SLIDEV_LAYOUT_SHIFTING_INTRO', undefined);
136
+
137
+ const isAnimated = computed(() => {
138
+ if (!props.animation) {
139
+ return false;
140
+ }
141
+
142
+ if (shiftingIntro) {
143
+ return shiftingIntro.active.value;
144
+ }
145
+
146
+ return true;
147
+ });
148
+
149
+ const resolvedStartDelay = computed(() => {
150
+ if (props.startDelay !== undefined) {
151
+ return props.startDelay;
152
+ }
153
+
154
+ if (shiftingIntro) {
155
+ return shiftingIntro.isShifting?.value ? 500 : 0;
156
+ }
157
+
158
+ return 0;
159
+ });
160
+
161
+ const internalActiveIndex = ref(props.activeIndex);
162
+
163
+ watch(
164
+ () => props.activeIndex,
165
+ (newVal) => {
166
+ internalActiveIndex.value = newVal;
167
+ },
168
+ );
169
+
170
+ const currentActiveIndex = computed(() => internalActiveIndex.value);
171
+
172
+ interface INamedComponent {
173
+ readonly type?: {
174
+ readonly name?: string;
175
+ readonly displayName?: string;
176
+ };
177
+ }
178
+
179
+ function countSlottedStacks(slotNodes: VNode[] | undefined): number {
180
+ if (!slotNodes) return 0;
181
+ let count = 0;
182
+ for (const node of slotNodes) {
183
+ if (typeof node.type === 'object' && node.type !== null) {
184
+ const comp = node.type as INamedComponent['type'];
185
+ if (
186
+ comp?.name === 'CircularPyramidStack' ||
187
+ comp?.displayName === 'CircularPyramidStack'
188
+ ) {
189
+ count++;
190
+ continue;
191
+ }
192
+ }
193
+ if (node.type === Fragment && Array.isArray(node.children)) {
194
+ count += countSlottedStacks(node.children as VNode[]);
195
+ }
196
+ }
197
+ return count;
198
+ }
199
+
200
+ const slottedStackCount = computed(() => {
201
+ const defaultSlotNodes = slots.default?.();
202
+ if (!defaultSlotNodes) return 0;
203
+ let count = 0;
204
+ for (const node of defaultSlotNodes) {
205
+ if (typeof node.type === 'object' && node.type !== null) {
206
+ const comp = node.type as INamedComponent['type'];
207
+ if (
208
+ comp?.name === 'CircularPyramidStack' ||
209
+ comp?.displayName === 'CircularPyramidStack'
210
+ ) {
211
+ count++;
212
+ continue;
213
+ }
214
+ }
215
+ if (node.type === Fragment && Array.isArray(node.children)) {
216
+ count += countSlottedStacks(node.children as VNode[]);
217
+ }
218
+ }
219
+ return count;
220
+ });
221
+
222
+ const registeredStackTokens = ref<symbol[]>([]);
223
+ const registeredStackColors = ref<Map<symbol, string | undefined>>(new Map());
224
+
225
+ function registerStack(
226
+ id?: symbol,
227
+ colorRef?: ComputedRef<string | undefined>,
228
+ ) {
229
+ const token = id ?? Symbol();
230
+ if (!registeredStackTokens.value.includes(token)) {
231
+ registeredStackTokens.value.push(token);
232
+ }
233
+ let stopWatch: (() => void) | undefined;
234
+ if (colorRef) {
235
+ stopWatch = watch(
236
+ colorRef,
237
+ (newColor) => {
238
+ if (newColor) {
239
+ registeredStackColors.value.set(token, newColor);
240
+ } else {
241
+ registeredStackColors.value.delete(token);
242
+ }
243
+ },
244
+ { immediate: true },
245
+ );
246
+ }
247
+ const index = computed(() => registeredStackTokens.value.indexOf(token));
248
+ return {
249
+ index,
250
+ unregister: () => {
251
+ stopWatch?.();
252
+ registeredStackTokens.value = registeredStackTokens.value.filter(
253
+ (t) => t !== token,
254
+ );
255
+ registeredStackColors.value.delete(token);
256
+ },
257
+ };
258
+ }
259
+
260
+ function getLayerColor(index: number): string {
261
+ if (index >= 0 && index < registeredStackTokens.value.length) {
262
+ const token = registeredStackTokens.value[index];
263
+ if (token) {
264
+ const custom = registeredStackColors.value.get(token);
265
+ if (custom) {
266
+ return custom;
267
+ }
268
+ }
269
+ }
270
+
271
+ const customPaletteColor = props.colors?.[index];
272
+ if (customPaletteColor) {
273
+ return customPaletteColor;
274
+ }
275
+
276
+ const stackColor =
277
+ DEFAULT_CIRCULAR_PYRAMID_STACK_COLORS[
278
+ Math.abs(index) % DEFAULT_CIRCULAR_PYRAMID_STACK_COLORS.length
279
+ ];
280
+
281
+ return stackColor ?? props.color ?? DEFAULT_CIRCULAR_PYRAMID_COLOR;
282
+ }
283
+
284
+ const activeLayerColor = computed<string>(() => {
285
+ if (currentActiveIndex.value >= 0) {
286
+ return getLayerColor(currentActiveIndex.value);
287
+ }
288
+
289
+ return props.color;
290
+ });
291
+
292
+ const resolvedCount = computed(() => {
293
+ if (props.count !== undefined) {
294
+ return clampCircularCount(props.count);
295
+ }
296
+ if (slottedStackCount.value > 0) {
297
+ return clampCircularCount(slottedStackCount.value);
298
+ }
299
+ if (registeredStackTokens.value.length > 0) {
300
+ return clampCircularCount(registeredStackTokens.value.length);
301
+ }
302
+ return 4;
303
+ });
304
+
305
+ const geo = computed(() => {
306
+ const vbHeight =
307
+ props.viewBoxHeight !== 440
308
+ ? props.viewBoxHeight
309
+ : resolvedCount.value >= 6
310
+ ? 480
311
+ : resolvedCount.value >= 5
312
+ ? 450
313
+ : 440;
314
+
315
+ return createCircularPyramidGeometry({
316
+ count: resolvedCount.value,
317
+ viewBoxWidth: 630,
318
+ viewBoxHeight: vbHeight,
319
+ cx: 315,
320
+ bottomRadius: 180,
321
+ topRadius:
322
+ resolvedCount.value <= 2
323
+ ? 100
324
+ : Math.max(70, 95 - (resolvedCount.value - 3) * 6),
325
+ cylinderHeight: resolvedCount.value >= 5 ? 7 : 8,
326
+ ratio: 0.45,
327
+ });
328
+ });
329
+
330
+ const palette = computed(() => {
331
+ return resolveCircularPyramidPalette(props.color);
332
+ });
333
+
334
+ function setActiveIndex(index: number): void {
335
+ internalActiveIndex.value = index;
336
+ emit('update:activeIndex', index);
337
+ emit('select', index);
338
+ }
339
+
340
+ const TITLE_WIDTH_PX = 150;
341
+ const CONTENT_WIDTH_PX = 240;
342
+ // Content card extends to the right boundary limit (0px offset from right edge)
343
+ const CONTENT_RIGHT_OFFSET_PX = 0;
344
+
345
+ const titleWidthPx = computed(() => TITLE_WIDTH_PX);
346
+ const contentWidthPx = computed(() => CONTENT_WIDTH_PX);
347
+ const contentRightOffsetPx = computed(() => CONTENT_RIGHT_OFFSET_PX);
348
+
349
+ const cardHeights = ref<number[]>([]);
350
+ const cardDeltaYList = ref<number[]>([]);
351
+
352
+ const initialDeltaYList = computed(() => {
353
+ const targetY = geo.value.layers.map((l) => l.slabCenterY ?? l.cy);
354
+ const estimatedHeights = Array(targetY.length).fill(48);
355
+ const resolvedY = resolvePyramidCardPositions({
356
+ targetYList: targetY,
357
+ heights: estimatedHeights,
358
+ gap: 10,
359
+ minY: 16,
360
+ maxY: geo.value.viewBoxHeight - 16,
361
+ });
362
+ return resolvedY.map((y, i) => y - (targetY[i] ?? y));
363
+ });
364
+
365
+ function getCardDeltaY(index: number): number {
366
+ if (cardDeltaYList.value[index] !== undefined) {
367
+ return cardDeltaYList.value[index] ?? 0;
368
+ }
369
+ return initialDeltaYList.value[index] ?? 0;
370
+ }
371
+
372
+ let cardAnimationFrame: number | undefined;
373
+
374
+ function updateCardPositions(): void {
375
+ if (typeof window === 'undefined' || !rootRef.value) {
376
+ return;
377
+ }
378
+ const cardEls = rootRef.value.querySelectorAll<HTMLElement>(
379
+ '.alpha-circular-pyramid-stack-content',
380
+ );
381
+ if (cardEls.length === 0) {
382
+ return;
383
+ }
384
+
385
+ const heights: number[] = [];
386
+ cardEls.forEach((el) => {
387
+ heights.push(el.offsetHeight || 48);
388
+ });
389
+ cardHeights.value = heights;
390
+
391
+ const targetY = geo.value.layers.map((l) => l.slabCenterY ?? l.cy);
392
+ const resolvedY = resolvePyramidCardPositions({
393
+ targetYList: targetY,
394
+ heights,
395
+ gap: 10,
396
+ minY: 16,
397
+ maxY: geo.value.viewBoxHeight - 16,
398
+ });
399
+
400
+ cardDeltaYList.value = resolvedY.map((y, i) => y - (targetY[i] ?? y));
401
+ }
402
+
403
+ function scheduleCardUpdate(): void {
404
+ if (typeof window === 'undefined') {
405
+ return;
406
+ }
407
+ if (cardAnimationFrame !== undefined) {
408
+ cancelAnimationFrame(cardAnimationFrame);
409
+ }
410
+ cardAnimationFrame = requestAnimationFrame(() => {
411
+ cardAnimationFrame = undefined;
412
+ updateCardPositions();
413
+ });
414
+ }
415
+
416
+ let cardResizeObserver: ResizeObserver | null = null;
417
+ let cardMutationObserver: MutationObserver | null = null;
418
+
419
+ onMounted(() => {
420
+ scheduleCardUpdate();
421
+ if (typeof ResizeObserver !== 'undefined' && rootRef.value) {
422
+ cardResizeObserver = new ResizeObserver(scheduleCardUpdate);
423
+ cardResizeObserver.observe(rootRef.value);
424
+ }
425
+ if (typeof MutationObserver !== 'undefined' && rootRef.value) {
426
+ cardMutationObserver = new MutationObserver(scheduleCardUpdate);
427
+ cardMutationObserver.observe(rootRef.value, {
428
+ childList: true,
429
+ subtree: true,
430
+ characterData: true,
431
+ });
432
+ }
433
+ });
434
+
435
+ onUnmounted(() => {
436
+ if (cardAnimationFrame !== undefined) {
437
+ cancelAnimationFrame(cardAnimationFrame);
438
+ }
439
+ cardResizeObserver?.disconnect();
440
+ cardMutationObserver?.disconnect();
441
+ });
442
+
443
+ provide(CIRCULAR_PYRAMID_ROOT_KEY, {
444
+ count: resolvedCount,
445
+ activeIndex: currentActiveIndex,
446
+ color: computed(() => props.color),
447
+ geo,
448
+ palette,
449
+ animation: isAnimated,
450
+ interactive: computed(() => props.interactive),
451
+ titleWidthPx,
452
+ contentWidthPx,
453
+ contentRightOffsetPx,
454
+ registerStack,
455
+ getLayerColor,
456
+ setActiveIndex,
457
+ getCardDeltaY,
458
+ });
459
+
460
+ // Render from bottom layer up to top layer for natural overlapping perspective
461
+ const stackedLayers = computed(() => {
462
+ return [...geo.value.layers].reverse();
463
+ });
464
+
465
+ const lowestLayer = computed(() => {
466
+ const layers = geo.value.layers;
467
+ return layers.length > 0 ? layers[layers.length - 1] : undefined;
468
+ });
469
+
470
+ const uid = Math.random().toString(36).slice(2, 8);
471
+ const filterBlurId = `alpha-circular-pyramid-blur-${uid}`;
472
+ const filterActiveShadowId = `alpha-circular-pyramid-shadow-${uid}`;
473
+ const filterAmbientShadowId = `alpha-circular-pyramid-ambient-${uid}`;
474
+
475
+ const activeTopGradId = `alpha-circ-grad-act-top-${uid}`;
476
+ const activeSideGradId = `alpha-circ-grad-act-side-${uid}`;
477
+ const defaultTopGradId = `alpha-circ-grad-def-top-${uid}`;
478
+ const defaultSideGradId = `alpha-circ-grad-def-side-${uid}`;
479
+
480
+ function getInactiveTopGradId(index: number): string {
481
+ return `alpha-circ-grad-inact-top-${uid}-${index}`;
482
+ }
483
+
484
+ function getInactiveSideGradId(index: number): string {
485
+ return `alpha-circ-grad-inact-side-${uid}-${index}`;
486
+ }
487
+
488
+ function getActiveTopGradId(index: number): string {
489
+ return `alpha-circ-grad-act-top-${uid}-${index}`;
490
+ }
491
+
492
+ function getActiveSideGradId(index: number): string {
493
+ return `alpha-circ-grad-act-side-${uid}-${index}`;
494
+ }
495
+
496
+ function getActiveShadowId(index: number): string {
497
+ return `alpha-circ-shadow-${uid}-${index}`;
498
+ }
499
+
500
+ const { resolvedScale, containerHeight } = useDiagramAutoScale({
501
+ rootRef,
502
+ baseWidth: props.viewBoxWidth,
503
+ baseHeight: props.viewBoxHeight,
504
+ scale: () => props.scale,
505
+ autoScale: () => props.autoScale,
506
+ maxScale: () => props.maxScale,
507
+ height: () => props.height,
508
+ });
509
+
510
+ const stageTransform = computed(() => {
511
+ if (resolvedScale.value !== 1) {
512
+ return `scale(${resolvedScale.value})`;
513
+ }
514
+
515
+ return undefined;
516
+ });
517
+
518
+ const resolvedWidthStyle = computed(() => {
519
+ if (props.width !== undefined) {
520
+ return typeof props.width === 'number' ? `${props.width}px` : props.width;
521
+ }
522
+
523
+ return '100%';
524
+ });
525
+
526
+ const resolvedHeightStyle = computed(() => {
527
+ if (props.height !== undefined) {
528
+ return typeof props.height === 'number'
529
+ ? `${props.height}px`
530
+ : props.height;
531
+ }
532
+
533
+ if (resolvedCount.value >= 6) {
534
+ return '480px';
535
+ }
536
+ if (resolvedCount.value >= 5) {
537
+ return '450px';
538
+ }
539
+
540
+ return '440px';
541
+ });
542
+
543
+ function handleLayerClick(index: number, _event: MouseEvent): void {
544
+ if (props.interactive) {
545
+ if (internalActiveIndex.value === index) {
546
+ setActiveIndex(-1);
547
+ } else {
548
+ setActiveIndex(index);
549
+ }
550
+ }
551
+ }
552
+
553
+ function handleContainerClick(event: MouseEvent): void {
554
+ emit('click', event);
555
+ }
556
+
557
+ const { className, forwardedAttrs } = useMergedUnoAttrs(
558
+ 'alpha-circular-pyramid w-full flex items-center justify-center select-none relative',
559
+ );
560
+ </script>
561
+
562
+ <template>
563
+ <div
564
+ ref="rootRef"
565
+ v-bind="forwardedAttrs()"
566
+ :class="className()"
567
+ :data-count="resolvedCount"
568
+ :data-active-index="currentActiveIndex"
569
+ :style="{
570
+ width: resolvedWidthStyle,
571
+ height: resolvedHeightStyle,
572
+ '--circular-pyramid-start-delay': `${resolvedStartDelay}ms`,
573
+ '--circular-pyramid-title-width': `${titleWidthPx}px`,
574
+ '--circular-pyramid-content-width': `${contentWidthPx}px`,
575
+ }"
576
+ @click="handleContainerClick"
577
+ >
578
+ <div
579
+ class="alpha-circular-pyramid__stage relative flex items-center justify-center overflow-visible flex-shrink-0"
580
+ :style="{
581
+ width: `${props.viewBoxWidth ?? 960}px`,
582
+ height: `${geo.viewBoxHeight}px`,
583
+ transform: stageTransform,
584
+ transformOrigin: 'center center',
585
+ }"
586
+ >
587
+ <!-- Central Circular Pyramid SVG Graphic (Strictly in middle between 150px and 180px) -->
588
+ <div
589
+ class="alpha-circular-pyramid__middle absolute inset-y-0 flex items-center justify-center pointer-events-none overflow-visible"
590
+ style="left: 150px; right: 180px;"
591
+ >
592
+ <svg
593
+ class="alpha-circular-pyramid__svg w-full h-full overflow-visible pointer-events-none"
594
+ :viewBox="`0 0 ${geo.viewBoxWidth} ${geo.viewBoxHeight}`"
595
+ preserveAspectRatio="xMidYMid meet"
596
+ xmlns="http://www.w3.org/2000/svg"
597
+ >
598
+ <defs>
599
+ <!-- Per-layer Gradients: Inactive Frosted Glass & Active Luminous -->
600
+ <template v-for="layer in geo.layers" :key="layer.index">
601
+ <!-- Inactive Frosted Glass Top Face Gradient (Exact formula matching Image 1 & 2) -->
602
+ <linearGradient
603
+ :id="getInactiveTopGradId(layer.index)"
604
+ x1="90%"
605
+ y1="10%"
606
+ x2="10%"
607
+ y2="90%"
608
+ >
609
+ <stop offset="0%" stop-color="#ffffff" stop-opacity="0.9" />
610
+ <stop offset="45%" stop-color="#ffffff" stop-opacity="0.7" />
611
+ <stop
612
+ offset="80%"
613
+ :stop-color="lightenColor({ hex: getLayerColor(layer.index), ratio: 0.88 })"
614
+ stop-opacity="0.5"
615
+ />
616
+ <stop
617
+ offset="100%"
618
+ :stop-color="lightenColor({ hex: getLayerColor(layer.index), ratio: 0.78 })"
619
+ stop-opacity="0.35"
620
+ />
621
+ </linearGradient>
622
+ <!-- Inactive Frosted Glass Rim Gradient (Exact formula matching Image 1 & 2) -->
623
+ <linearGradient
624
+ :id="getInactiveSideGradId(layer.index)"
625
+ x1="0%"
626
+ y1="0%"
627
+ x2="100%"
628
+ y2="0%"
629
+ >
630
+ <stop
631
+ offset="0%"
632
+ :stop-color="lightenColor({ hex: getLayerColor(layer.index), ratio: 0.65 })"
633
+ stop-opacity="0.65"
634
+ />
635
+ <stop
636
+ offset="50%"
637
+ :stop-color="lightenColor({ hex: getLayerColor(layer.index), ratio: 0.75 })"
638
+ stop-opacity="0.5"
639
+ />
640
+ <stop
641
+ offset="100%"
642
+ :stop-color="lightenColor({ hex: getLayerColor(layer.index), ratio: 0.88 })"
643
+ stop-opacity="0.75"
644
+ />
645
+ </linearGradient>
646
+ <!-- Active Luminous Top Face Gradient -->
647
+ <linearGradient
648
+ :id="getActiveTopGradId(layer.index)"
649
+ x1="90%"
650
+ y1="10%"
651
+ x2="15%"
652
+ y2="90%"
653
+ >
654
+ <stop
655
+ offset="0%"
656
+ :stop-color="lightenColor({ hex: getLayerColor(layer.index), ratio: 0.92 })"
657
+ stop-opacity="1"
658
+ />
659
+ <stop
660
+ offset="28%"
661
+ :stop-color="lightenColor({ hex: getLayerColor(layer.index), ratio: 0.55 })"
662
+ stop-opacity="1"
663
+ />
664
+ <stop
665
+ offset="65%"
666
+ :stop-color="lightenColor({ hex: getLayerColor(layer.index), ratio: 0.15 })"
667
+ stop-opacity="1"
668
+ />
669
+ <stop
670
+ offset="100%"
671
+ :stop-color="darkenColor({ hex: getLayerColor(layer.index), ratio: 0.18 })"
672
+ stop-opacity="1"
673
+ />
674
+ </linearGradient>
675
+ <!-- Active 3D Rim Gradient -->
676
+ <linearGradient
677
+ :id="getActiveSideGradId(layer.index)"
678
+ x1="0%"
679
+ y1="0%"
680
+ x2="100%"
681
+ y2="0%"
682
+ >
683
+ <stop
684
+ offset="0%"
685
+ :stop-color="darkenColor({ hex: getLayerColor(layer.index), ratio: 0.25 })"
686
+ />
687
+ <stop
688
+ offset="50%"
689
+ :stop-color="darkenColor({ hex: getLayerColor(layer.index), ratio: 0.08 })"
690
+ />
691
+ <stop
692
+ offset="100%"
693
+ :stop-color="lightenColor({ hex: getLayerColor(layer.index), ratio: 0.45 })"
694
+ />
695
+ </linearGradient>
696
+ <!-- Active Drop Shadow / Glow Filter -->
697
+ <filter
698
+ :id="getActiveShadowId(layer.index)"
699
+ x="-40%"
700
+ y="-40%"
701
+ width="180%"
702
+ height="200%"
703
+ >
704
+ <feDropShadow
705
+ dx="0"
706
+ dy="8"
707
+ stdDeviation="10"
708
+ :flood-color="getLayerColor(layer.index)"
709
+ flood-opacity="0.35"
710
+ />
711
+ </filter>
712
+ </template>
713
+ <!-- Fallback Global Gradients for Test Spec Compatibility -->
714
+ <linearGradient :id="activeTopGradId" x1="90%" y1="10%" x2="15%" y2="90%">
715
+ <stop offset="0%" :stop-color="lightenColor({ hex: activeLayerColor, ratio: 0.92 })" stop-opacity="1" />
716
+ <stop offset="100%" :stop-color="darkenColor({ hex: activeLayerColor, ratio: 0.18 })" stop-opacity="1" />
717
+ </linearGradient>
718
+ <linearGradient :id="activeSideGradId" x1="0%" y1="0%" x2="100%" y2="0%">
719
+ <stop offset="0%" :stop-color="darkenColor({ hex: activeLayerColor, ratio: 0.25 })" />
720
+ <stop offset="100%" :stop-color="lightenColor({ hex: activeLayerColor, ratio: 0.45 })" />
721
+ </linearGradient>
722
+ <linearGradient :id="defaultTopGradId" x1="90%" y1="10%" x2="10%" y2="90%">
723
+ <stop offset="0%" stop-color="#ffffff" stop-opacity="0.9" />
724
+ <stop offset="100%" :stop-color="lightenColor({ hex: props.color, ratio: 0.78 })" stop-opacity="0.35" />
725
+ </linearGradient>
726
+ <linearGradient :id="defaultSideGradId" x1="0%" y1="0%" x2="100%" y2="0%">
727
+ <stop offset="0%" :stop-color="lightenColor({ hex: props.color, ratio: 0.65 })" stop-opacity="0.65" />
728
+ <stop offset="100%" :stop-color="lightenColor({ hex: props.color, ratio: 0.88 })" stop-opacity="0.75" />
729
+ </linearGradient>
730
+ <filter :id="filterActiveShadowId" x="-40%" y="-40%" width="180%" height="200%">
731
+ <feDropShadow dx="0" dy="8" stdDeviation="10" :flood-color="activeLayerColor" flood-opacity="0.35" />
732
+ </filter>
733
+ <!-- Ambient Bottom Surface Shadow -->
734
+ <filter
735
+ :id="filterAmbientShadowId"
736
+ x="-30%"
737
+ y="-30%"
738
+ width="160%"
739
+ height="160%"
740
+ >
741
+ <feGaussianBlur stdDeviation="14" />
742
+ </filter>
743
+ </defs>
744
+ <!-- Ambient Ground Shadow (under the lowest pyramid disc) -->
745
+ <ellipse
746
+ v-if="lowestLayer"
747
+ :cx="geo.viewBoxWidth / 2"
748
+ :cy="lowestLayer.cy + lowestLayer.cylinderHeight + 14"
749
+ :rx="lowestLayer.rx * 0.92"
750
+ :ry="lowestLayer.ry * 0.28"
751
+ fill="#0f172a"
752
+ opacity="0.1"
753
+ :filter="`url(#${filterAmbientShadowId})`"
754
+ />
755
+ <!-- Stacked Floating Circular Discs (Rendered from bottom to top) -->
756
+ <g
757
+ class="alpha-circular-pyramid__stack"
758
+ :class="{ 'alpha-circular-pyramid--animated': isAnimated }"
759
+ >
760
+ <g
761
+ v-for="layer in stackedLayers"
762
+ :key="layer.index"
763
+ class="alpha-circular-pyramid__layer transition-all duration-300 ease-out pointer-events-auto"
764
+ :class="[
765
+ layer.index === currentActiveIndex
766
+ ? 'alpha-circular-pyramid__layer--active'
767
+ : 'alpha-circular-pyramid__layer--default',
768
+ `alpha-circular-pyramid__layer--${layer.index}`,
769
+ { 'cursor-pointer': props.interactive },
770
+ ]"
771
+ :data-layer-index="layer.index"
772
+ :data-is-active="layer.index === currentActiveIndex"
773
+ :filter="
774
+ layer.index === currentActiveIndex
775
+ ? `url(#${getActiveShadowId(layer.index)})`
776
+ : undefined
777
+ "
778
+ @click.stop="handleLayerClick(layer.index, $event)"
779
+ >
780
+ <!-- 1. 3D Cylindrical Rim (Mantle) -->
781
+ <path
782
+ class="alpha-circular-pyramid__rim transition-all duration-300"
783
+ :d="layer.cylinderPath"
784
+ :fill="
785
+ layer.index === currentActiveIndex
786
+ ? `url(#${getActiveSideGradId(layer.index)})`
787
+ : `url(#${getInactiveSideGradId(layer.index)})`
788
+ "
789
+ />
790
+ <!-- 2. Disc Top Face Ellipse -->
791
+ <path
792
+ class="alpha-circular-pyramid__face transition-all duration-300"
793
+ :d="layer.topEllipsePath"
794
+ :fill="
795
+ layer.index === currentActiveIndex
796
+ ? `url(#${getActiveTopGradId(layer.index)})`
797
+ : `url(#${getInactiveTopGradId(layer.index)})`
798
+ "
799
+ :stroke="
800
+ layer.index === currentActiveIndex
801
+ ? 'none'
802
+ : 'rgba(255, 255, 255, 0.5)'
803
+ "
804
+ stroke-width="0.5"
805
+ />
806
+ </g>
807
+ </g>
808
+ </svg>
809
+ </div>
810
+ <!-- Slotted or Default Stack Items -->
811
+ <slot v-if="slots.default" />
812
+ <template v-else>
813
+ <CircularPyramidStack
814
+ v-for="idx in resolvedCount"
815
+ :key="idx - 1"
816
+ :index="idx - 1"
817
+ />
818
+ </template>
819
+ </div>
820
+ </div>
821
+ </template>
822
+
823
+ <style scoped>
824
+ .alpha-circular-pyramid {
825
+ box-sizing: border-box;
826
+ }
827
+
828
+ .alpha-circular-pyramid__layer {
829
+ transform-box: fill-box;
830
+ transform-origin: center;
831
+ }
832
+
833
+ .alpha-circular-pyramid__layer:hover {
834
+ filter: brightness(1.05);
835
+ }
836
+
837
+ .alpha-circular-pyramid--animated .alpha-circular-pyramid__layer--0 {
838
+ animation: alpha-pyramid-layer-enter 450ms cubic-bezier(0.16, 1, 0.3, 1) calc(var(--circular-pyramid-start-delay, 0ms) + 60ms) both;
839
+ }
840
+
841
+ .alpha-circular-pyramid--animated .alpha-circular-pyramid__layer--1 {
842
+ animation: alpha-pyramid-layer-enter 450ms cubic-bezier(0.16, 1, 0.3, 1) calc(var(--circular-pyramid-start-delay, 0ms) + 140ms) both;
843
+ }
844
+
845
+ .alpha-circular-pyramid--animated .alpha-circular-pyramid__layer--2 {
846
+ animation: alpha-pyramid-layer-enter 450ms cubic-bezier(0.16, 1, 0.3, 1) calc(var(--circular-pyramid-start-delay, 0ms) + 220ms) both;
847
+ }
848
+
849
+ .alpha-circular-pyramid--animated .alpha-circular-pyramid__layer--3 {
850
+ animation: alpha-pyramid-layer-enter 450ms cubic-bezier(0.16, 1, 0.3, 1) calc(var(--circular-pyramid-start-delay, 0ms) + 300ms) both;
851
+ }
852
+
853
+ .alpha-circular-pyramid--animated .alpha-circular-pyramid__layer--4 {
854
+ animation: alpha-pyramid-layer-enter 450ms cubic-bezier(0.16, 1, 0.3, 1) calc(var(--circular-pyramid-start-delay, 0ms) + 380ms) both;
855
+ }
856
+
857
+ .alpha-circular-pyramid--animated .alpha-circular-pyramid__layer--5 {
858
+ animation: alpha-pyramid-layer-enter 450ms cubic-bezier(0.16, 1, 0.3, 1) calc(var(--circular-pyramid-start-delay, 0ms) + 460ms) both;
859
+ }
860
+
861
+ @keyframes alpha-pyramid-layer-enter {
862
+ 0% {
863
+ opacity: 0;
864
+ transform: translateY(12px) scale(0.97);
865
+ }
866
+ 100% {
867
+ opacity: 1;
868
+ transform: translateY(0) scale(1);
869
+ }
870
+ }
871
+ </style>