@alphacifer/slidev-addon-theme 0.0.8 → 0.0.10

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 (41) hide show
  1. package/README.md +80 -0
  2. package/components/defense-in-depth/arc-did/ArcDiD.vue +732 -0
  3. package/components/defense-in-depth/arc-did/ArcDiDLayer.vue +321 -0
  4. package/components/defense-in-depth/arc-did/ArcDiDLayerPart.vue +449 -0
  5. package/components/defense-in-depth/arc-did/ArcDiDLayerParts.vue +11 -0
  6. package/components/defense-in-depth/arc-did/ArcDiDLayerSector.vue +148 -0
  7. package/components/defense-in-depth/arc-did/ArcDiDLayerTitle.vue +98 -0
  8. package/components/pyramid/circular-pyramid/CircularPyramid.vue +858 -0
  9. package/components/pyramid/circular-pyramid/CircularPyramidStack.vue +342 -0
  10. package/components/pyramid/circular-pyramid/CircularPyramidStackContent.vue +305 -0
  11. package/components/pyramid/circular-pyramid/CircularPyramidStackTitle.vue +208 -0
  12. package/components/pyramid/square-pyramid/SquarePyramid.vue +863 -0
  13. package/components/pyramid/square-pyramid/SquarePyramidStack.vue +333 -0
  14. package/components/pyramid/square-pyramid/SquarePyramidStackContent.vue +301 -0
  15. package/components/pyramid/square-pyramid/SquarePyramidStackTitle.vue +204 -0
  16. package/package.json +2 -1
  17. package/utils/defenseInDepth/arcDid/context.ts +110 -0
  18. package/utils/defenseInDepth/arcDid/createArcDiDGeometry.ts +570 -0
  19. package/utils/defenseInDepth/arcDid/index.ts +3 -0
  20. package/utils/defenseInDepth/arcDid/types.ts +140 -0
  21. package/utils/defenseInDepth/index.ts +2 -0
  22. package/utils/defenseInDepth/shared/colors.ts +121 -0
  23. package/utils/defenseInDepth/shared/constants.ts +14 -0
  24. package/utils/defenseInDepth/shared/index.ts +3 -0
  25. package/utils/defenseInDepth/shared/types.ts +12 -0
  26. package/utils/pyramid/circular-pyramid/context.ts +46 -0
  27. package/utils/pyramid/circular-pyramid/createCircularPyramidGeometry.ts +143 -0
  28. package/utils/pyramid/circular-pyramid/index.ts +3 -0
  29. package/utils/pyramid/circular-pyramid/types.ts +34 -0
  30. package/utils/pyramid/index.ts +3 -0
  31. package/utils/pyramid/shared/colors.ts +92 -0
  32. package/utils/pyramid/shared/connector.ts +111 -0
  33. package/utils/pyramid/shared/constants.ts +9 -0
  34. package/utils/pyramid/shared/index.ts +5 -0
  35. package/utils/pyramid/shared/stacking.ts +76 -0
  36. package/utils/pyramid/shared/types.ts +36 -0
  37. package/utils/pyramid/square-pyramid/context.ts +46 -0
  38. package/utils/pyramid/square-pyramid/createSquarePyramidGeometry.ts +265 -0
  39. package/utils/pyramid/square-pyramid/index.ts +3 -0
  40. package/utils/pyramid/square-pyramid/types.ts +39 -0
  41. package/utils/useDiagramAutoScale.ts +64 -20
@@ -0,0 +1,863 @@
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 { resolvePyramidCardPositions } from '../../../utils/pyramid/shared';
17
+ import {
18
+ clampSquareCount,
19
+ createSquarePyramidGeometry,
20
+ DEFAULT_SQUARE_PYRAMID_COLOR,
21
+ DEFAULT_SQUARE_PYRAMID_STACK_COLORS,
22
+ darkenColor,
23
+ lightenColor,
24
+ resolveSquarePyramidPalette,
25
+ SQUARE_PYRAMID_ROOT_KEY,
26
+ } from '../../../utils/pyramid/square-pyramid';
27
+ import { useDiagramAutoScale } from '../../../utils/useDiagramAutoScale';
28
+ import { useMergedUnoAttrs } from '../../../utils/useMergedUnoAttrs';
29
+ import SquarePyramidStack from './SquarePyramidStack.vue';
30
+
31
+ defineOptions({
32
+ inheritAttrs: false,
33
+ name: 'SquarePyramid',
34
+ });
35
+
36
+ export interface ISquarePyramidProps {
37
+ /**
38
+ * Number of square layers in the pyramid (1 to 6).
39
+ * Automatically inferred from slotted `<SquarePyramidStack>` components if omitted.
40
+ * Default: 4
41
+ */
42
+ readonly count?: number;
43
+ /**
44
+ * Index of the active square 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_SQUARE_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<ISquarePyramidProps>(), {
104
+ count: undefined,
105
+ activeIndex: -1,
106
+ color: DEFAULT_SQUARE_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 === 'SquarePyramidStack' ||
187
+ comp?.displayName === 'SquarePyramidStack'
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 === 'SquarePyramidStack' ||
209
+ comp?.displayName === 'SquarePyramidStack'
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_SQUARE_PYRAMID_STACK_COLORS[
278
+ Math.abs(index) % DEFAULT_SQUARE_PYRAMID_STACK_COLORS.length
279
+ ];
280
+
281
+ return stackColor ?? props.color ?? DEFAULT_SQUARE_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 clampSquareCount(props.count);
295
+ }
296
+ if (slottedStackCount.value > 0) {
297
+ return clampSquareCount(slottedStackCount.value);
298
+ }
299
+ if (registeredStackTokens.value.length > 0) {
300
+ return clampSquareCount(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 createSquarePyramidGeometry({
316
+ count: resolvedCount.value,
317
+ viewBoxWidth: 630,
318
+ viewBoxHeight: vbHeight,
319
+ cx: 315,
320
+ bottomSize: 180,
321
+ topSize:
322
+ resolvedCount.value <= 2
323
+ ? 100
324
+ : Math.max(70, 95 - (resolvedCount.value - 3) * 6),
325
+ slabHeight: resolvedCount.value >= 5 ? 8 : 10,
326
+ ratio: 0.46,
327
+ });
328
+ });
329
+
330
+ const palette = computed(() => {
331
+ return resolveSquarePyramidPalette(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
+ const CONTENT_RIGHT_OFFSET_PX = 0;
343
+
344
+ const titleWidthPx = computed(() => TITLE_WIDTH_PX);
345
+ const contentWidthPx = computed(() => CONTENT_WIDTH_PX);
346
+ const contentRightOffsetPx = computed(() => CONTENT_RIGHT_OFFSET_PX);
347
+
348
+ const cardHeights = ref<number[]>([]);
349
+ const cardDeltaYList = ref<number[]>([]);
350
+
351
+ const initialDeltaYList = computed(() => {
352
+ const targetY = geo.value.layers.map((l) => l.slabCenterY ?? l.cy);
353
+ const estimatedHeights = Array(targetY.length).fill(48);
354
+ const resolvedY = resolvePyramidCardPositions({
355
+ targetYList: targetY,
356
+ heights: estimatedHeights,
357
+ gap: 10,
358
+ minY: 16,
359
+ maxY: geo.value.viewBoxHeight - 16,
360
+ });
361
+ return resolvedY.map((y, i) => y - (targetY[i] ?? y));
362
+ });
363
+
364
+ function getCardDeltaY(index: number): number {
365
+ if (cardDeltaYList.value[index] !== undefined) {
366
+ return cardDeltaYList.value[index] ?? 0;
367
+ }
368
+ return initialDeltaYList.value[index] ?? 0;
369
+ }
370
+
371
+ let cardAnimationFrame: number | undefined;
372
+
373
+ function updateCardPositions(): void {
374
+ if (typeof window === 'undefined' || !rootRef.value) {
375
+ return;
376
+ }
377
+ const cardEls = rootRef.value.querySelectorAll<HTMLElement>(
378
+ '.alpha-square-pyramid-stack-content',
379
+ );
380
+ if (cardEls.length === 0) {
381
+ return;
382
+ }
383
+
384
+ const heights: number[] = [];
385
+ cardEls.forEach((el) => {
386
+ heights.push(el.offsetHeight || 48);
387
+ });
388
+ cardHeights.value = heights;
389
+
390
+ const targetY = geo.value.layers.map((l) => l.slabCenterY ?? l.cy);
391
+ const resolvedY = resolvePyramidCardPositions({
392
+ targetYList: targetY,
393
+ heights,
394
+ gap: 10,
395
+ minY: 16,
396
+ maxY: geo.value.viewBoxHeight - 16,
397
+ });
398
+
399
+ cardDeltaYList.value = resolvedY.map((y, i) => y - (targetY[i] ?? y));
400
+ }
401
+
402
+ function scheduleCardUpdate(): void {
403
+ if (typeof window === 'undefined') {
404
+ return;
405
+ }
406
+ if (cardAnimationFrame !== undefined) {
407
+ cancelAnimationFrame(cardAnimationFrame);
408
+ }
409
+ cardAnimationFrame = requestAnimationFrame(() => {
410
+ cardAnimationFrame = undefined;
411
+ updateCardPositions();
412
+ });
413
+ }
414
+
415
+ let cardResizeObserver: ResizeObserver | null = null;
416
+ let cardMutationObserver: MutationObserver | null = null;
417
+
418
+ onMounted(() => {
419
+ scheduleCardUpdate();
420
+ if (typeof ResizeObserver !== 'undefined' && rootRef.value) {
421
+ cardResizeObserver = new ResizeObserver(scheduleCardUpdate);
422
+ cardResizeObserver.observe(rootRef.value);
423
+ }
424
+ if (typeof MutationObserver !== 'undefined' && rootRef.value) {
425
+ cardMutationObserver = new MutationObserver(scheduleCardUpdate);
426
+ cardMutationObserver.observe(rootRef.value, {
427
+ childList: true,
428
+ subtree: true,
429
+ characterData: true,
430
+ });
431
+ }
432
+ });
433
+
434
+ onUnmounted(() => {
435
+ if (cardAnimationFrame !== undefined) {
436
+ cancelAnimationFrame(cardAnimationFrame);
437
+ }
438
+ cardResizeObserver?.disconnect();
439
+ cardMutationObserver?.disconnect();
440
+ });
441
+
442
+ provide(SQUARE_PYRAMID_ROOT_KEY, {
443
+ count: resolvedCount,
444
+ activeIndex: currentActiveIndex,
445
+ color: computed(() => props.color),
446
+ geo,
447
+ palette,
448
+ animation: isAnimated,
449
+ interactive: computed(() => props.interactive),
450
+ titleWidthPx,
451
+ contentWidthPx,
452
+ contentRightOffsetPx,
453
+ registerStack,
454
+ getLayerColor,
455
+ setActiveIndex,
456
+ getCardDeltaY,
457
+ });
458
+
459
+ // Render from bottom layer up to top layer for natural overlapping perspective
460
+ const stackedLayers = computed(() => {
461
+ return [...geo.value.layers].reverse();
462
+ });
463
+
464
+ const lowestLayer = computed(() => {
465
+ const layers = geo.value.layers;
466
+ return layers.length > 0 ? layers[layers.length - 1] : undefined;
467
+ });
468
+
469
+ const uid = Math.random().toString(36).slice(2, 8);
470
+ const filterActiveShadowId = `alpha-square-pyramid-shadow-${uid}`;
471
+ const filterAmbientShadowId = `alpha-square-pyramid-ambient-${uid}`;
472
+
473
+ const activeTopGradId = `alpha-sq-grad-act-top-${uid}`;
474
+ const activeSideGradId = `alpha-sq-grad-act-side-${uid}`;
475
+ const defaultTopGradId = `alpha-sq-grad-def-top-${uid}`;
476
+ const defaultSideGradId = `alpha-sq-grad-def-side-${uid}`;
477
+
478
+ function getInactiveTopGradId(index: number): string {
479
+ return `alpha-sq-grad-inact-top-${uid}-${index}`;
480
+ }
481
+
482
+ function getInactiveSideGradId(index: number): string {
483
+ return `alpha-sq-grad-inact-side-${uid}-${index}`;
484
+ }
485
+
486
+ function getActiveTopGradId(index: number): string {
487
+ return `alpha-sq-grad-act-top-${uid}-${index}`;
488
+ }
489
+
490
+ function getActiveSideGradId(index: number): string {
491
+ return `alpha-sq-grad-act-side-${uid}-${index}`;
492
+ }
493
+
494
+ function getActiveShadowId(index: number): string {
495
+ return `alpha-sq-shadow-${uid}-${index}`;
496
+ }
497
+
498
+ const { resolvedScale } = useDiagramAutoScale({
499
+ rootRef,
500
+ baseWidth: props.viewBoxWidth,
501
+ baseHeight: props.viewBoxHeight,
502
+ scale: () => props.scale,
503
+ autoScale: () => props.autoScale,
504
+ maxScale: () => props.maxScale,
505
+ height: () => props.height,
506
+ });
507
+
508
+ const stageTransform = computed(() => {
509
+ if (resolvedScale.value !== 1) {
510
+ return `scale(${resolvedScale.value})`;
511
+ }
512
+
513
+ return undefined;
514
+ });
515
+
516
+ const resolvedWidthStyle = computed(() => {
517
+ if (props.width !== undefined) {
518
+ return typeof props.width === 'number' ? `${props.width}px` : props.width;
519
+ }
520
+
521
+ return '100%';
522
+ });
523
+
524
+ const resolvedHeightStyle = computed(() => {
525
+ if (props.height !== undefined) {
526
+ return typeof props.height === 'number'
527
+ ? `${props.height}px`
528
+ : props.height;
529
+ }
530
+
531
+ if (resolvedCount.value >= 6) {
532
+ return '480px';
533
+ }
534
+ if (resolvedCount.value >= 5) {
535
+ return '450px';
536
+ }
537
+
538
+ return '440px';
539
+ });
540
+
541
+ function handleLayerClick(index: number, _event: MouseEvent): void {
542
+ if (props.interactive) {
543
+ if (internalActiveIndex.value === index) {
544
+ setActiveIndex(-1);
545
+ } else {
546
+ setActiveIndex(index);
547
+ }
548
+ }
549
+ }
550
+
551
+ function handleContainerClick(event: MouseEvent): void {
552
+ emit('click', event);
553
+ }
554
+
555
+ const { className, forwardedAttrs } = useMergedUnoAttrs(
556
+ 'alpha-square-pyramid w-full flex items-center justify-center select-none relative',
557
+ );
558
+ </script>
559
+
560
+ <template>
561
+ <div
562
+ ref="rootRef"
563
+ v-bind="forwardedAttrs()"
564
+ :class="className()"
565
+ :data-count="resolvedCount"
566
+ :data-active-index="currentActiveIndex"
567
+ :style="{
568
+ width: resolvedWidthStyle,
569
+ height: resolvedHeightStyle,
570
+ '--square-pyramid-start-delay': `${resolvedStartDelay}ms`,
571
+ '--square-pyramid-title-width': `${titleWidthPx}px`,
572
+ '--square-pyramid-content-width': `${contentWidthPx}px`,
573
+ }"
574
+ @click="handleContainerClick"
575
+ >
576
+ <div
577
+ class="alpha-square-pyramid__stage relative flex items-center justify-center overflow-visible flex-shrink-0"
578
+ :style="{
579
+ width: `${props.viewBoxWidth ?? 960}px`,
580
+ height: `${geo.viewBoxHeight}px`,
581
+ transform: stageTransform,
582
+ transformOrigin: 'center center',
583
+ }"
584
+ >
585
+ <!-- Central Square Pyramid SVG Graphic -->
586
+ <div
587
+ class="alpha-square-pyramid__middle absolute inset-y-0 flex items-center justify-center pointer-events-none overflow-visible"
588
+ style="left: 150px; right: 180px;"
589
+ >
590
+ <svg
591
+ class="alpha-square-pyramid__svg w-full h-full overflow-visible pointer-events-none"
592
+ :viewBox="`0 0 ${geo.viewBoxWidth} ${geo.viewBoxHeight}`"
593
+ preserveAspectRatio="xMidYMid meet"
594
+ xmlns="http://www.w3.org/2000/svg"
595
+ >
596
+ <defs>
597
+ <!-- Per-layer Gradients: Inactive Frosted Glass & Active Luminous -->
598
+ <template v-for="layer in geo.layers" :key="layer.index">
599
+ <!-- Inactive Frosted Glass Top Face Gradient -->
600
+ <linearGradient
601
+ :id="getInactiveTopGradId(layer.index)"
602
+ x1="90%"
603
+ y1="10%"
604
+ x2="10%"
605
+ y2="90%"
606
+ >
607
+ <stop offset="0%" stop-color="#ffffff" stop-opacity="0.9" />
608
+ <stop offset="45%" stop-color="#ffffff" stop-opacity="0.7" />
609
+ <stop
610
+ offset="80%"
611
+ :stop-color="lightenColor({ hex: getLayerColor(layer.index), ratio: 0.88 })"
612
+ stop-opacity="0.5"
613
+ />
614
+ <stop
615
+ offset="100%"
616
+ :stop-color="lightenColor({ hex: getLayerColor(layer.index), ratio: 0.78 })"
617
+ stop-opacity="0.35"
618
+ />
619
+ </linearGradient>
620
+ <!-- Inactive Frosted Glass Rim Gradient -->
621
+ <linearGradient
622
+ :id="getInactiveSideGradId(layer.index)"
623
+ x1="0%"
624
+ y1="0%"
625
+ x2="100%"
626
+ y2="0%"
627
+ >
628
+ <stop
629
+ offset="0%"
630
+ :stop-color="lightenColor({ hex: getLayerColor(layer.index), ratio: 0.65 })"
631
+ stop-opacity="0.65"
632
+ />
633
+ <stop
634
+ offset="50%"
635
+ :stop-color="lightenColor({ hex: getLayerColor(layer.index), ratio: 0.75 })"
636
+ stop-opacity="0.5"
637
+ />
638
+ <stop
639
+ offset="100%"
640
+ :stop-color="lightenColor({ hex: getLayerColor(layer.index), ratio: 0.88 })"
641
+ stop-opacity="0.75"
642
+ />
643
+ </linearGradient>
644
+ <!-- Active Luminous Top Face Gradient -->
645
+ <linearGradient
646
+ :id="getActiveTopGradId(layer.index)"
647
+ x1="90%"
648
+ y1="10%"
649
+ x2="15%"
650
+ y2="90%"
651
+ >
652
+ <stop
653
+ offset="0%"
654
+ :stop-color="lightenColor({ hex: getLayerColor(layer.index), ratio: 0.92 })"
655
+ stop-opacity="1"
656
+ />
657
+ <stop
658
+ offset="28%"
659
+ :stop-color="lightenColor({ hex: getLayerColor(layer.index), ratio: 0.55 })"
660
+ stop-opacity="1"
661
+ />
662
+ <stop
663
+ offset="65%"
664
+ :stop-color="lightenColor({ hex: getLayerColor(layer.index), ratio: 0.15 })"
665
+ stop-opacity="1"
666
+ />
667
+ <stop
668
+ offset="100%"
669
+ :stop-color="darkenColor({ hex: getLayerColor(layer.index), ratio: 0.18 })"
670
+ stop-opacity="1"
671
+ />
672
+ </linearGradient>
673
+ <!-- Active 3D Rim Gradient -->
674
+ <linearGradient
675
+ :id="getActiveSideGradId(layer.index)"
676
+ x1="0%"
677
+ y1="0%"
678
+ x2="100%"
679
+ y2="0%"
680
+ >
681
+ <stop
682
+ offset="0%"
683
+ :stop-color="darkenColor({ hex: getLayerColor(layer.index), ratio: 0.25 })"
684
+ />
685
+ <stop
686
+ offset="50%"
687
+ :stop-color="darkenColor({ hex: getLayerColor(layer.index), ratio: 0.08 })"
688
+ />
689
+ <stop
690
+ offset="100%"
691
+ :stop-color="lightenColor({ hex: getLayerColor(layer.index), ratio: 0.45 })"
692
+ />
693
+ </linearGradient>
694
+ <!-- Active Drop Shadow / Glow Filter -->
695
+ <filter
696
+ :id="getActiveShadowId(layer.index)"
697
+ x="-40%"
698
+ y="-40%"
699
+ width="180%"
700
+ height="200%"
701
+ >
702
+ <feDropShadow
703
+ dx="0"
704
+ dy="8"
705
+ stdDeviation="10"
706
+ :flood-color="getLayerColor(layer.index)"
707
+ flood-opacity="0.35"
708
+ />
709
+ </filter>
710
+ </template>
711
+ <!-- Fallback Global Gradients for Test Spec Compatibility -->
712
+ <linearGradient :id="activeTopGradId" x1="90%" y1="10%" x2="15%" y2="90%">
713
+ <stop offset="0%" :stop-color="lightenColor({ hex: activeLayerColor, ratio: 0.92 })" stop-opacity="1" />
714
+ <stop offset="100%" :stop-color="darkenColor({ hex: activeLayerColor, ratio: 0.18 })" stop-opacity="1" />
715
+ </linearGradient>
716
+ <linearGradient :id="activeSideGradId" x1="0%" y1="0%" x2="100%" y2="0%">
717
+ <stop offset="0%" :stop-color="darkenColor({ hex: activeLayerColor, ratio: 0.25 })" />
718
+ <stop offset="100%" :stop-color="lightenColor({ hex: activeLayerColor, ratio: 0.45 })" />
719
+ </linearGradient>
720
+ <linearGradient :id="defaultTopGradId" x1="90%" y1="10%" x2="10%" y2="90%">
721
+ <stop offset="0%" stop-color="#ffffff" stop-opacity="0.9" />
722
+ <stop offset="100%" :stop-color="lightenColor({ hex: props.color, ratio: 0.78 })" stop-opacity="0.35" />
723
+ </linearGradient>
724
+ <linearGradient :id="defaultSideGradId" x1="0%" y1="0%" x2="100%" y2="0%">
725
+ <stop offset="0%" :stop-color="lightenColor({ hex: props.color, ratio: 0.65 })" stop-opacity="0.65" />
726
+ <stop offset="100%" :stop-color="lightenColor({ hex: props.color, ratio: 0.88 })" stop-opacity="0.75" />
727
+ </linearGradient>
728
+ <filter :id="filterActiveShadowId" x="-40%" y="-40%" width="180%" height="200%">
729
+ <feDropShadow dx="0" dy="8" stdDeviation="10" :flood-color="activeLayerColor" flood-opacity="0.35" />
730
+ </filter>
731
+ <!-- Ambient Bottom Surface Shadow -->
732
+ <filter
733
+ :id="filterAmbientShadowId"
734
+ x="-30%"
735
+ y="-30%"
736
+ width="160%"
737
+ height="160%"
738
+ >
739
+ <feGaussianBlur stdDeviation="14" />
740
+ </filter>
741
+ </defs>
742
+ <!-- Ambient Ground Shadow (under the lowest pyramid slab) -->
743
+ <ellipse
744
+ v-if="lowestLayer"
745
+ :cx="geo.viewBoxWidth / 2"
746
+ :cy="lowestLayer.cy + lowestLayer.slabHeight + 14"
747
+ :rx="lowestLayer.rx * 0.92"
748
+ :ry="lowestLayer.ry * 0.3"
749
+ fill="#0f172a"
750
+ opacity="0.1"
751
+ :filter="`url(#${filterAmbientShadowId})`"
752
+ />
753
+ <!-- Stacked Floating Square Slabs (Rendered from bottom to top) -->
754
+ <g
755
+ class="alpha-square-pyramid__stack"
756
+ :class="{ 'alpha-square-pyramid--animated': isAnimated }"
757
+ >
758
+ <g
759
+ v-for="layer in stackedLayers"
760
+ :key="layer.index"
761
+ class="alpha-square-pyramid__layer transition-all duration-300 ease-out pointer-events-auto"
762
+ :class="[
763
+ layer.index === currentActiveIndex
764
+ ? 'alpha-square-pyramid__layer--active'
765
+ : 'alpha-square-pyramid__layer--default',
766
+ `alpha-square-pyramid__layer--${layer.index}`,
767
+ { 'cursor-pointer': props.interactive },
768
+ ]"
769
+ :data-layer-index="layer.index"
770
+ :data-is-active="layer.index === currentActiveIndex"
771
+ :filter="
772
+ layer.index === currentActiveIndex
773
+ ? `url(#${getActiveShadowId(layer.index)})`
774
+ : undefined
775
+ "
776
+ :style="{
777
+ '--layer-order': layer.index,
778
+ '--layer-delay': `calc(var(--square-pyramid-start-delay, 0ms) + ${layer.index * 260 + 10}ms)`,
779
+ }"
780
+ @click.stop="handleLayerClick(layer.index, $event)"
781
+ >
782
+ <!-- 1. 3D Square Slab Thickness Rim (Mantle) -->
783
+ <path
784
+ class="alpha-square-pyramid__rim transition-all duration-300"
785
+ :d="layer.sidePath"
786
+ :fill="
787
+ layer.index === currentActiveIndex
788
+ ? `url(#${getActiveSideGradId(layer.index)})`
789
+ : `url(#${getInactiveSideGradId(layer.index)})`
790
+ "
791
+ />
792
+ <!-- 2. Slab Top Face Isometric Rounded Diamond -->
793
+ <path
794
+ class="alpha-square-pyramid__face transition-all duration-300"
795
+ :d="layer.topFacePath"
796
+ :fill="
797
+ layer.index === currentActiveIndex
798
+ ? `url(#${getActiveTopGradId(layer.index)})`
799
+ : `url(#${getInactiveTopGradId(layer.index)})`
800
+ "
801
+ :stroke="
802
+ layer.index === currentActiveIndex
803
+ ? 'none'
804
+ : 'rgba(255, 255, 255, 0.5)'
805
+ "
806
+ stroke-width="0.5"
807
+ />
808
+ <!-- 3. Inner Inset Highlight (matches translucent glassmorphic plate) -->
809
+ <path
810
+ class="alpha-square-pyramid__inner transition-all duration-300 pointer-events-none"
811
+ :d="layer.innerFacePath"
812
+ fill="#ffffff"
813
+ :fill-opacity="layer.index === currentActiveIndex ? 0.35 : 0.2"
814
+ />
815
+ </g>
816
+ </g>
817
+ </svg>
818
+ </div>
819
+ <!-- Slotted or Default Stack Items -->
820
+ <slot v-if="slots.default" />
821
+ <template v-else>
822
+ <SquarePyramidStack
823
+ v-for="idx in resolvedCount"
824
+ :key="idx - 1"
825
+ :index="idx - 1"
826
+ />
827
+ </template>
828
+ </div>
829
+ </div>
830
+ </template>
831
+
832
+ <style scoped>
833
+ .alpha-square-pyramid {
834
+ box-sizing: border-box;
835
+ }
836
+
837
+ .alpha-square-pyramid__layer {
838
+ transform-box: fill-box;
839
+ transform-origin: center;
840
+ }
841
+
842
+ .alpha-square-pyramid__layer:hover {
843
+ filter: brightness(1.05);
844
+ }
845
+
846
+ .alpha-square-pyramid--animated .alpha-square-pyramid__layer {
847
+ animation: alpha-pyramid-layer-enter 450ms cubic-bezier(0.16, 1, 0.3, 1) var(--layer-delay, calc(var(--square-pyramid-start-delay, 0ms) + 10ms)) both;
848
+ }
849
+
850
+ @keyframes alpha-pyramid-layer-enter {
851
+ 0% {
852
+ opacity: 0;
853
+ transform: translateY(80px) scale(0.92);
854
+ }
855
+ 25% {
856
+ opacity: 1;
857
+ }
858
+ 100% {
859
+ opacity: 1;
860
+ transform: translateY(0) scale(1);
861
+ }
862
+ }
863
+ </style>