@alphacifer/slidev-addon-theme 0.0.9 → 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.
- package/components/defense-in-depth/arc-did/ArcDiD.vue +732 -0
- package/components/defense-in-depth/arc-did/ArcDiDLayer.vue +321 -0
- package/components/defense-in-depth/arc-did/ArcDiDLayerPart.vue +449 -0
- package/components/defense-in-depth/arc-did/ArcDiDLayerParts.vue +11 -0
- package/components/defense-in-depth/arc-did/ArcDiDLayerSector.vue +148 -0
- package/components/defense-in-depth/arc-did/ArcDiDLayerTitle.vue +98 -0
- package/components/pyramid/circular-pyramid/CircularPyramid.vue +10 -23
- package/components/pyramid/circular-pyramid/CircularPyramidStack.vue +17 -50
- package/components/pyramid/circular-pyramid/CircularPyramidStackContent.vue +98 -1
- package/components/pyramid/circular-pyramid/CircularPyramidStackTitle.vue +70 -0
- package/components/pyramid/square-pyramid/SquarePyramid.vue +10 -23
- package/components/pyramid/square-pyramid/SquarePyramidStack.vue +17 -50
- package/components/pyramid/square-pyramid/SquarePyramidStackContent.vue +98 -1
- package/components/pyramid/square-pyramid/SquarePyramidStackTitle.vue +70 -0
- package/package.json +1 -1
- package/utils/defenseInDepth/arcDid/context.ts +110 -0
- package/utils/defenseInDepth/arcDid/createArcDiDGeometry.ts +570 -0
- package/utils/defenseInDepth/arcDid/index.ts +3 -0
- package/utils/defenseInDepth/arcDid/types.ts +140 -0
- package/utils/defenseInDepth/index.ts +2 -0
- package/utils/defenseInDepth/shared/colors.ts +121 -0
- package/utils/defenseInDepth/shared/constants.ts +14 -0
- package/utils/defenseInDepth/shared/index.ts +3 -0
- package/utils/defenseInDepth/shared/types.ts +12 -0
- package/utils/pyramid/shared/constants.ts +1 -0
- package/utils/useDiagramAutoScale.ts +61 -15
|
@@ -0,0 +1,732 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
type ComputedRef,
|
|
4
|
+
computed,
|
|
5
|
+
Fragment,
|
|
6
|
+
inject,
|
|
7
|
+
provide,
|
|
8
|
+
ref,
|
|
9
|
+
useSlots,
|
|
10
|
+
type VNode,
|
|
11
|
+
watch,
|
|
12
|
+
} from 'vue';
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
ARC_DID_ROOT_KEY,
|
|
16
|
+
type IArcDiDRegisteredLayerData,
|
|
17
|
+
type IArcDiDRootContext,
|
|
18
|
+
} from '../../../utils/defenseInDepth/arcDid/context';
|
|
19
|
+
import { createArcDiDGeometry } from '../../../utils/defenseInDepth/arcDid/createArcDiDGeometry';
|
|
20
|
+
import type {
|
|
21
|
+
IArcDiDLayerItem,
|
|
22
|
+
TArcDiDOrder,
|
|
23
|
+
} from '../../../utils/defenseInDepth/arcDid/types';
|
|
24
|
+
import {
|
|
25
|
+
lightenColor,
|
|
26
|
+
resolveArcDiDPalette,
|
|
27
|
+
} from '../../../utils/defenseInDepth/shared/colors';
|
|
28
|
+
import { DEFAULT_DID_COLOR } from '../../../utils/defenseInDepth/shared/constants';
|
|
29
|
+
import { useDiagramAutoScale } from '../../../utils/useDiagramAutoScale';
|
|
30
|
+
import { useMergedUnoAttrs } from '../../../utils/useMergedUnoAttrs';
|
|
31
|
+
|
|
32
|
+
defineOptions({
|
|
33
|
+
name: 'ArcDiD',
|
|
34
|
+
inheritAttrs: false,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export interface IArcDiDProps {
|
|
38
|
+
readonly order?: TArcDiDOrder;
|
|
39
|
+
readonly count?: number;
|
|
40
|
+
readonly activeIndex?: number;
|
|
41
|
+
readonly color?: string;
|
|
42
|
+
readonly colors?: readonly string[] | string[];
|
|
43
|
+
readonly layers?: readonly IArcDiDLayerItem[];
|
|
44
|
+
readonly width?: number | string;
|
|
45
|
+
readonly height?: number | string;
|
|
46
|
+
readonly scale?: number;
|
|
47
|
+
readonly autoScale?: boolean;
|
|
48
|
+
readonly maxScale?: number;
|
|
49
|
+
readonly animation?: boolean;
|
|
50
|
+
readonly startDelay?: number;
|
|
51
|
+
readonly interactive?: boolean;
|
|
52
|
+
readonly viewBoxWidth?: number;
|
|
53
|
+
readonly viewBoxHeight?: number;
|
|
54
|
+
readonly innerRadius?: number;
|
|
55
|
+
readonly outerRadius?: number;
|
|
56
|
+
readonly gap?: number;
|
|
57
|
+
readonly showDecorativeArcs?: boolean;
|
|
58
|
+
readonly titleAngleSpan?: number;
|
|
59
|
+
readonly showPartBoundaries?: boolean;
|
|
60
|
+
readonly activePart?: string | number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const props = withDefaults(defineProps<IArcDiDProps>(), {
|
|
64
|
+
order: 'outer-to-inner',
|
|
65
|
+
count: undefined,
|
|
66
|
+
activeIndex: -1,
|
|
67
|
+
activePart: undefined,
|
|
68
|
+
color: DEFAULT_DID_COLOR,
|
|
69
|
+
colors: undefined,
|
|
70
|
+
layers: undefined,
|
|
71
|
+
width: 720,
|
|
72
|
+
height: undefined,
|
|
73
|
+
scale: undefined,
|
|
74
|
+
autoScale: true,
|
|
75
|
+
maxScale: undefined,
|
|
76
|
+
animation: true,
|
|
77
|
+
startDelay: undefined,
|
|
78
|
+
interactive: true,
|
|
79
|
+
viewBoxWidth: 900,
|
|
80
|
+
viewBoxHeight: 460,
|
|
81
|
+
innerRadius: 90,
|
|
82
|
+
outerRadius: 445,
|
|
83
|
+
gap: 0,
|
|
84
|
+
showDecorativeArcs: false,
|
|
85
|
+
titleAngleSpan: undefined,
|
|
86
|
+
showPartBoundaries: true,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const emit = defineEmits<{
|
|
90
|
+
(e: 'update:activeIndex', index: number): void;
|
|
91
|
+
(e: 'update:activePart', part: string | number | undefined): void;
|
|
92
|
+
(e: 'select', index: number): void;
|
|
93
|
+
(e: 'part-click', part: IArcDiDPartGeometry): void;
|
|
94
|
+
(e: 'click', event: MouseEvent): void;
|
|
95
|
+
}>();
|
|
96
|
+
|
|
97
|
+
const slots = useSlots();
|
|
98
|
+
const rootRef = ref<HTMLElement | null>(null);
|
|
99
|
+
|
|
100
|
+
const shiftingIntro = inject<
|
|
101
|
+
| {
|
|
102
|
+
active: ComputedRef<boolean>;
|
|
103
|
+
isShifting?: ComputedRef<boolean>;
|
|
104
|
+
}
|
|
105
|
+
| undefined
|
|
106
|
+
>('SLIDEV_LAYOUT_SHIFTING_INTRO', undefined);
|
|
107
|
+
|
|
108
|
+
const isAnimated = computed(() => {
|
|
109
|
+
if (!props.animation) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
if (shiftingIntro) {
|
|
113
|
+
return shiftingIntro.active.value;
|
|
114
|
+
}
|
|
115
|
+
return true;
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const resolvedStartDelay = computed(() => {
|
|
119
|
+
if (props.startDelay !== undefined) {
|
|
120
|
+
return props.startDelay;
|
|
121
|
+
}
|
|
122
|
+
if (shiftingIntro?.isShifting?.value) {
|
|
123
|
+
return 500;
|
|
124
|
+
}
|
|
125
|
+
return 0;
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
interface INamedComponent {
|
|
129
|
+
readonly type?: {
|
|
130
|
+
readonly name?: string;
|
|
131
|
+
readonly displayName?: string;
|
|
132
|
+
readonly __name?: string;
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function hasComponentName(type: unknown, targetName: string): boolean {
|
|
137
|
+
if (typeof type === 'object' && type !== null) {
|
|
138
|
+
const comp = type as INamedComponent['type'];
|
|
139
|
+
return (
|
|
140
|
+
comp?.name === targetName ||
|
|
141
|
+
comp?.displayName === targetName ||
|
|
142
|
+
comp?.__name === targetName
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function countSlottedLayers(slotNodes?: VNode[]): number {
|
|
149
|
+
if (!slotNodes) return 0;
|
|
150
|
+
let count = 0;
|
|
151
|
+
for (const node of slotNodes) {
|
|
152
|
+
if (hasComponentName(node.type, 'ArcDiDLayer')) {
|
|
153
|
+
count++;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (node.type === Fragment && Array.isArray(node.children)) {
|
|
157
|
+
count += countSlottedLayers(node.children as VNode[]);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return count;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const slottedLayerCount = computed(() => {
|
|
164
|
+
const defaultSlotNodes = slots.default?.();
|
|
165
|
+
return countSlottedLayers(defaultSlotNodes);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const registeredLayerTokens = ref<symbol[]>([]);
|
|
169
|
+
const registeredLayerData = ref<Map<symbol, IArcDiDRegisteredLayerData>>(
|
|
170
|
+
new Map(),
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
function registerLayer(
|
|
174
|
+
token?: symbol,
|
|
175
|
+
dataRef?: ComputedRef<IArcDiDRegisteredLayerData | undefined>,
|
|
176
|
+
) {
|
|
177
|
+
const lToken = token ?? Symbol();
|
|
178
|
+
if (!registeredLayerTokens.value.includes(lToken)) {
|
|
179
|
+
registeredLayerTokens.value = [...registeredLayerTokens.value, lToken];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let stopWatch: (() => void) | undefined;
|
|
183
|
+
if (dataRef) {
|
|
184
|
+
stopWatch = watch(
|
|
185
|
+
dataRef,
|
|
186
|
+
(val) => {
|
|
187
|
+
const nextMap = new Map(registeredLayerData.value);
|
|
188
|
+
if (val) {
|
|
189
|
+
nextMap.set(lToken, val);
|
|
190
|
+
} else {
|
|
191
|
+
nextMap.delete(lToken);
|
|
192
|
+
}
|
|
193
|
+
registeredLayerData.value = nextMap;
|
|
194
|
+
},
|
|
195
|
+
{ immediate: true },
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const index = computed(() => registeredLayerTokens.value.indexOf(lToken));
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
index,
|
|
203
|
+
unregister: () => {
|
|
204
|
+
stopWatch?.();
|
|
205
|
+
registeredLayerTokens.value = registeredLayerTokens.value.filter(
|
|
206
|
+
(t) => t !== lToken,
|
|
207
|
+
);
|
|
208
|
+
const nextMap = new Map(registeredLayerData.value);
|
|
209
|
+
nextMap.delete(lToken);
|
|
210
|
+
registeredLayerData.value = nextMap;
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const resolvedCount = computed<number>(() => {
|
|
216
|
+
if (props.count !== undefined && props.count > 0) {
|
|
217
|
+
return Math.max(1, Math.min(props.count, 6));
|
|
218
|
+
}
|
|
219
|
+
if (props.layers && props.layers.length > 0) {
|
|
220
|
+
return Math.max(1, Math.min(props.layers.length, 6));
|
|
221
|
+
}
|
|
222
|
+
if (slottedLayerCount.value > 0) {
|
|
223
|
+
return Math.max(1, Math.min(slottedLayerCount.value, 6));
|
|
224
|
+
}
|
|
225
|
+
if (registeredLayerTokens.value.length > 0) {
|
|
226
|
+
return Math.max(1, Math.min(registeredLayerTokens.value.length, 6));
|
|
227
|
+
}
|
|
228
|
+
return 0;
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const resolvedLayers = computed<IArcDiDLayerItem[]>(() => {
|
|
232
|
+
const indexed: (IArcDiDLayerItem | undefined)[] = [];
|
|
233
|
+
const unindexed: IArcDiDLayerItem[] = [];
|
|
234
|
+
|
|
235
|
+
for (let i = 0; i < registeredLayerTokens.value.length; i++) {
|
|
236
|
+
const token = registeredLayerTokens.value[i];
|
|
237
|
+
if (!token) continue;
|
|
238
|
+
const data = registeredLayerData.value.get(token);
|
|
239
|
+
const item: IArcDiDLayerItem = {
|
|
240
|
+
title: data?.title ?? '',
|
|
241
|
+
color: data?.color,
|
|
242
|
+
parts: data?.parts ?? [],
|
|
243
|
+
sectors: data?.sectors ?? [],
|
|
244
|
+
titleAngleSpan: data?.titleAngleSpan,
|
|
245
|
+
titleWidth: data?.titleWidth,
|
|
246
|
+
};
|
|
247
|
+
if (data?.index !== undefined && data.index >= 0) {
|
|
248
|
+
indexed[data.index] = item;
|
|
249
|
+
} else {
|
|
250
|
+
unindexed.push(item);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
let unindexedIdx = 0;
|
|
255
|
+
const result: IArcDiDLayerItem[] = [];
|
|
256
|
+
const total = Math.max(indexed.length, registeredLayerTokens.value.length);
|
|
257
|
+
for (let i = 0; i < total; i++) {
|
|
258
|
+
const item = indexed[i];
|
|
259
|
+
if (item) {
|
|
260
|
+
result.push(item);
|
|
261
|
+
} else {
|
|
262
|
+
const fallback = unindexed[unindexedIdx++];
|
|
263
|
+
if (fallback) {
|
|
264
|
+
result.push(fallback);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return result;
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const effectiveLayers = computed<readonly IArcDiDLayerItem[]>(() => {
|
|
272
|
+
if (props.layers && props.layers.length > 0) {
|
|
273
|
+
return props.layers;
|
|
274
|
+
}
|
|
275
|
+
return resolvedLayers.value;
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const palette = computed(() =>
|
|
279
|
+
resolveArcDiDPalette(props.color, resolvedCount.value),
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
function getLayerColor(index: number): string {
|
|
283
|
+
if (index >= 0 && index < registeredLayerTokens.value.length) {
|
|
284
|
+
const token = registeredLayerTokens.value[index];
|
|
285
|
+
if (token) {
|
|
286
|
+
const custom = registeredLayerData.value.get(token)?.color;
|
|
287
|
+
if (custom) {
|
|
288
|
+
return custom;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const customColor = props.colors?.[index];
|
|
294
|
+
if (customColor) {
|
|
295
|
+
return customColor;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const defaultLayers = palette.value.layerColors;
|
|
299
|
+
return defaultLayers[index % defaultLayers.length] ?? DEFAULT_DID_COLOR;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const internalActiveIndex = ref<number>(props.activeIndex);
|
|
303
|
+
|
|
304
|
+
watch(
|
|
305
|
+
() => props.activeIndex,
|
|
306
|
+
(val) => {
|
|
307
|
+
internalActiveIndex.value = val;
|
|
308
|
+
},
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
function setActiveIndex(index: number): void {
|
|
312
|
+
internalActiveIndex.value = index;
|
|
313
|
+
emit('update:activeIndex', index);
|
|
314
|
+
emit('select', index);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const internalActivePart = ref<string | number | undefined>(props.activePart);
|
|
318
|
+
watch(
|
|
319
|
+
() => props.activePart,
|
|
320
|
+
(val) => {
|
|
321
|
+
internalActivePart.value = val;
|
|
322
|
+
},
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
function setActivePart(part: string | number | undefined): void {
|
|
326
|
+
internalActivePart.value = part;
|
|
327
|
+
emit('update:activePart', part);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function isPartActive(part: IArcDiDPartGeometry): boolean {
|
|
331
|
+
if (part.active) {
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
if (internalActivePart.value !== undefined) {
|
|
335
|
+
return (
|
|
336
|
+
internalActivePart.value === part.label ||
|
|
337
|
+
internalActivePart.value === part.index
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const geo = computed(() => {
|
|
344
|
+
return createArcDiDGeometry({
|
|
345
|
+
viewBoxWidth: props.viewBoxWidth,
|
|
346
|
+
viewBoxHeight: props.viewBoxHeight,
|
|
347
|
+
innerRadius: props.innerRadius,
|
|
348
|
+
outerRadius: props.outerRadius,
|
|
349
|
+
count: resolvedCount.value,
|
|
350
|
+
gap: props.gap,
|
|
351
|
+
order: props.order,
|
|
352
|
+
titleAngleSpan: props.titleAngleSpan,
|
|
353
|
+
layers: effectiveLayers.value,
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
const hasAnyActivePart = computed<boolean>(() => {
|
|
358
|
+
if (internalActivePart.value !== undefined) {
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
for (const layer of geo.value.layers) {
|
|
362
|
+
for (const part of layer.parts) {
|
|
363
|
+
if (part.active) {
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return false;
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
function getActivePartFill(
|
|
372
|
+
customColor?: string,
|
|
373
|
+
fallbackBaseColor?: string,
|
|
374
|
+
): string {
|
|
375
|
+
const base = customColor || fallbackBaseColor || DEFAULT_DID_COLOR;
|
|
376
|
+
return lightenColor({ hex: base, ratio: 0.5 });
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function getPartOpacity(layerIndex: number, isActive: boolean): number {
|
|
380
|
+
if (hasAnyActivePart.value) {
|
|
381
|
+
return isActive ? 1 : 0.28;
|
|
382
|
+
}
|
|
383
|
+
if (
|
|
384
|
+
internalActiveIndex.value === -1 ||
|
|
385
|
+
internalActiveIndex.value === layerIndex
|
|
386
|
+
) {
|
|
387
|
+
return 1;
|
|
388
|
+
}
|
|
389
|
+
return 0.45;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function handlePartClick(part: IArcDiDPartGeometry, layerIndex: number): void {
|
|
393
|
+
if (!props.interactive) return;
|
|
394
|
+
emit('part-click', part);
|
|
395
|
+
if (isPartActive(part)) {
|
|
396
|
+
setActivePart(undefined);
|
|
397
|
+
} else {
|
|
398
|
+
setActivePart(part.label);
|
|
399
|
+
if (
|
|
400
|
+
internalActiveIndex.value !== -1 &&
|
|
401
|
+
internalActiveIndex.value !== layerIndex
|
|
402
|
+
) {
|
|
403
|
+
setActiveIndex(layerIndex);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const rootContext: IArcDiDRootContext = {
|
|
409
|
+
geo,
|
|
410
|
+
count: resolvedCount,
|
|
411
|
+
activeIndex: internalActiveIndex,
|
|
412
|
+
setActiveIndex,
|
|
413
|
+
activePart: internalActivePart,
|
|
414
|
+
setActivePart,
|
|
415
|
+
hasAnyActivePart,
|
|
416
|
+
isPartActive: (label, index, activeProp) => {
|
|
417
|
+
if (activeProp) return true;
|
|
418
|
+
if (internalActivePart.value !== undefined) {
|
|
419
|
+
return (
|
|
420
|
+
internalActivePart.value === label || internalActivePart.value === index
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
return false;
|
|
424
|
+
},
|
|
425
|
+
color: computed(() => props.color),
|
|
426
|
+
colors: computed(() => props.colors),
|
|
427
|
+
order: computed(() => props.order),
|
|
428
|
+
animation: isAnimated,
|
|
429
|
+
interactive: computed(() => props.interactive),
|
|
430
|
+
startDelay: resolvedStartDelay,
|
|
431
|
+
getLayerColor,
|
|
432
|
+
registerLayer,
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
provide(ARC_DID_ROOT_KEY, rootContext);
|
|
436
|
+
|
|
437
|
+
const { resolvedScale, containerHeight } = useDiagramAutoScale({
|
|
438
|
+
rootRef,
|
|
439
|
+
baseWidth: props.viewBoxWidth,
|
|
440
|
+
baseHeight: props.viewBoxHeight,
|
|
441
|
+
scale: () => props.scale,
|
|
442
|
+
autoScale: () => props.autoScale,
|
|
443
|
+
maxScale: () => props.maxScale,
|
|
444
|
+
width: () => props.width,
|
|
445
|
+
height: () => props.height,
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
const stageTransform = computed(() => {
|
|
449
|
+
if (resolvedScale.value !== 1) {
|
|
450
|
+
return `scale(${resolvedScale.value})`;
|
|
451
|
+
}
|
|
452
|
+
return undefined;
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
const resolvedWidthStyle = computed(() => {
|
|
456
|
+
if (props.width !== undefined) {
|
|
457
|
+
return typeof props.width === 'number' ? `${props.width}px` : props.width;
|
|
458
|
+
}
|
|
459
|
+
return '100%';
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
const resolvedHeightStyle = computed(() => {
|
|
463
|
+
if (props.height !== undefined) {
|
|
464
|
+
return typeof props.height === 'number'
|
|
465
|
+
? `${props.height}px`
|
|
466
|
+
: props.height;
|
|
467
|
+
}
|
|
468
|
+
if (containerHeight.value > 0) {
|
|
469
|
+
return `${containerHeight.value}px`;
|
|
470
|
+
}
|
|
471
|
+
return `${props.viewBoxHeight}px`;
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
function handleLayerClick(index: number): void {
|
|
475
|
+
if (props.interactive) {
|
|
476
|
+
if (internalActiveIndex.value === index) {
|
|
477
|
+
setActiveIndex(-1);
|
|
478
|
+
} else {
|
|
479
|
+
setActiveIndex(index);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function handleContainerClick(event: MouseEvent): void {
|
|
485
|
+
emit('click', event);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const uid = Math.random().toString(36).slice(2, 8);
|
|
489
|
+
const gradIdPrefix = `alpha-arc-did-grad-${uid}`;
|
|
490
|
+
|
|
491
|
+
const { className, forwardedAttrs } = useMergedUnoAttrs(
|
|
492
|
+
'alpha-arc-did w-full flex items-center justify-center select-none relative',
|
|
493
|
+
);
|
|
494
|
+
</script>
|
|
495
|
+
|
|
496
|
+
<template>
|
|
497
|
+
<div
|
|
498
|
+
ref="rootRef"
|
|
499
|
+
v-bind="forwardedAttrs()"
|
|
500
|
+
:class="className()"
|
|
501
|
+
:data-count="resolvedCount"
|
|
502
|
+
:data-active-index="internalActiveIndex"
|
|
503
|
+
:data-order="props.order"
|
|
504
|
+
:style="{
|
|
505
|
+
width: resolvedWidthStyle,
|
|
506
|
+
height: resolvedHeightStyle,
|
|
507
|
+
'--arc-did-start-delay': `${resolvedStartDelay}ms`,
|
|
508
|
+
}"
|
|
509
|
+
@click="handleContainerClick"
|
|
510
|
+
>
|
|
511
|
+
<!-- Stage Container with diagram auto-scale -->
|
|
512
|
+
<div
|
|
513
|
+
class="alpha-arc-did__stage relative flex items-center justify-center overflow-visible flex-shrink-0"
|
|
514
|
+
:style="{
|
|
515
|
+
width: `${props.viewBoxWidth}px`,
|
|
516
|
+
height: `${props.viewBoxHeight}px`,
|
|
517
|
+
transform: stageTransform,
|
|
518
|
+
transformOrigin: 'center center',
|
|
519
|
+
}"
|
|
520
|
+
>
|
|
521
|
+
<!-- Base SVG Layer: Concentric Arcs and Decorative Flairs -->
|
|
522
|
+
<svg
|
|
523
|
+
class="alpha-arc-did__svg absolute inset-0 w-full h-full overflow-visible pointer-events-none"
|
|
524
|
+
:viewBox="`0 0 ${props.viewBoxWidth} ${props.viewBoxHeight}`"
|
|
525
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
526
|
+
>
|
|
527
|
+
<defs>
|
|
528
|
+
<filter id="alpha-arc-did-glow" x="-20%" y="-20%" width="140%" height="140%">
|
|
529
|
+
<feGaussianBlur stdDeviation="3" result="blur" />
|
|
530
|
+
<feComposite in="SourceGraphic" in2="blur" operator="over" />
|
|
531
|
+
</filter>
|
|
532
|
+
</defs>
|
|
533
|
+
|
|
534
|
+
<!-- Concentric Arc Bands (Seamless with zero spacing) -->
|
|
535
|
+
<g class="alpha-arc-did__layers">
|
|
536
|
+
<g
|
|
537
|
+
v-for="layer in geo.layers"
|
|
538
|
+
:key="layer.index"
|
|
539
|
+
class="alpha-arc-did__layer-group"
|
|
540
|
+
>
|
|
541
|
+
<!-- Full base band -->
|
|
542
|
+
<path
|
|
543
|
+
:d="layer.path"
|
|
544
|
+
:fill="getLayerColor(layer.index)"
|
|
545
|
+
:stroke="props.showPartBoundaries ? palette.strokeColor : 'none'"
|
|
546
|
+
:stroke-width="props.showPartBoundaries ? 1.5 : 0"
|
|
547
|
+
stroke-linejoin="round"
|
|
548
|
+
class="alpha-arc-did__band transition-all duration-300 ease-out"
|
|
549
|
+
:class="[
|
|
550
|
+
{ 'alpha-arc-did__band--animated': isAnimated },
|
|
551
|
+
props.interactive ? 'cursor-pointer pointer-events-auto' : 'pointer-events-none',
|
|
552
|
+
]"
|
|
553
|
+
:style="{
|
|
554
|
+
opacity: hasAnyActivePart
|
|
555
|
+
? (internalActiveIndex === layer.index ? 0.45 : 0.25)
|
|
556
|
+
: (internalActiveIndex === -1 || internalActiveIndex === layer.index ? 1 : 0.45),
|
|
557
|
+
filter:
|
|
558
|
+
internalActiveIndex === layer.index && !hasAnyActivePart
|
|
559
|
+
? 'drop-shadow(0 0 12px rgba(14, 165, 233, 0.45))'
|
|
560
|
+
: undefined,
|
|
561
|
+
'--layer-anim-delay': `${resolvedStartDelay + layer.index * 100}ms`,
|
|
562
|
+
}"
|
|
563
|
+
@click="handleLayerClick(layer.index)"
|
|
564
|
+
/>
|
|
565
|
+
|
|
566
|
+
<!-- Title Sector Boundary/Area (when layer has parts) -->
|
|
567
|
+
<path
|
|
568
|
+
v-if="props.showPartBoundaries && layer.titlePath && layer.parts.length > 0"
|
|
569
|
+
:d="layer.titlePath"
|
|
570
|
+
:fill="getLayerColor(layer.index)"
|
|
571
|
+
:stroke="palette.strokeColor"
|
|
572
|
+
stroke-width="1.5"
|
|
573
|
+
stroke-linejoin="round"
|
|
574
|
+
class="alpha-arc-did__band alpha-arc-did__sector alpha-arc-did__sector--title transition-all duration-300 ease-out hover:brightness-105"
|
|
575
|
+
:class="[
|
|
576
|
+
{ 'alpha-arc-did__band--animated': isAnimated },
|
|
577
|
+
props.interactive ? 'cursor-pointer pointer-events-auto' : 'pointer-events-none',
|
|
578
|
+
]"
|
|
579
|
+
:style="{
|
|
580
|
+
opacity: hasAnyActivePart
|
|
581
|
+
? (internalActiveIndex === layer.index ? 0.45 : 0.25)
|
|
582
|
+
: (internalActiveIndex === -1 || internalActiveIndex === layer.index ? 1 : 0.45),
|
|
583
|
+
'--layer-anim-delay': `${resolvedStartDelay + layer.index * 100}ms`,
|
|
584
|
+
}"
|
|
585
|
+
@click="handleLayerClick(layer.index)"
|
|
586
|
+
/>
|
|
587
|
+
|
|
588
|
+
<!-- Interactive Part Sector Areas on top with visible boundaries -->
|
|
589
|
+
<template v-if="layer.sectors.length > 0">
|
|
590
|
+
<template v-for="sector in layer.sectors" :key="sector.index">
|
|
591
|
+
<!-- If sector has split: true, render individual track sub-bands -->
|
|
592
|
+
<template v-if="sector.split">
|
|
593
|
+
<path
|
|
594
|
+
v-for="part in sector.parts"
|
|
595
|
+
:key="part.index"
|
|
596
|
+
:d="part.path"
|
|
597
|
+
:fill="isPartActive(part) ? getActivePartFill(part.color || sector.color, getLayerColor(layer.index)) : (part.color || sector.color || getLayerColor(layer.index))"
|
|
598
|
+
:stroke="isPartActive(part) ? '#ffffff' : (props.showPartBoundaries ? palette.strokeColor : 'none')"
|
|
599
|
+
:stroke-width="isPartActive(part) ? 3.5 : (props.showPartBoundaries ? 1.5 : 0)"
|
|
600
|
+
stroke-linejoin="round"
|
|
601
|
+
class="alpha-arc-did__band alpha-arc-did__sector alpha-arc-did__sector--part transition-all duration-300 ease-out hover:brightness-110"
|
|
602
|
+
:class="[
|
|
603
|
+
{ 'alpha-arc-did__band--animated': isAnimated },
|
|
604
|
+
{ 'alpha-arc-did__sector--active': isPartActive(part) },
|
|
605
|
+
props.interactive ? 'cursor-pointer pointer-events-auto' : 'pointer-events-none',
|
|
606
|
+
]"
|
|
607
|
+
:style="{
|
|
608
|
+
opacity: getPartOpacity(layer.index, isPartActive(part)),
|
|
609
|
+
filter: isPartActive(part)
|
|
610
|
+
? 'drop-shadow(0 0 14px rgba(255, 255, 255, 0.85)) drop-shadow(0 0 6px rgba(56, 189, 248, 0.8)) brightness(1.2)'
|
|
611
|
+
: undefined,
|
|
612
|
+
'--layer-anim-delay': `${resolvedStartDelay + layer.index * 100}ms`,
|
|
613
|
+
}"
|
|
614
|
+
@click.stop="handlePartClick(part, layer.index)"
|
|
615
|
+
/>
|
|
616
|
+
</template>
|
|
617
|
+
<!-- If sector is NOT split: render the entire sector band -->
|
|
618
|
+
<template v-else>
|
|
619
|
+
<path
|
|
620
|
+
:d="sector.path"
|
|
621
|
+
:fill="sector.parts[0] && isPartActive(sector.parts[0]) ? getActivePartFill(sector.color, getLayerColor(layer.index)) : (sector.color || getLayerColor(layer.index))"
|
|
622
|
+
:stroke="sector.parts[0] && isPartActive(sector.parts[0]) ? '#ffffff' : (props.showPartBoundaries ? palette.strokeColor : 'none')"
|
|
623
|
+
:stroke-width="sector.parts[0] && isPartActive(sector.parts[0]) ? 3.5 : (props.showPartBoundaries ? 1.5 : 0)"
|
|
624
|
+
stroke-linejoin="round"
|
|
625
|
+
class="alpha-arc-did__band alpha-arc-did__sector alpha-arc-did__sector--part transition-all duration-300 ease-out hover:brightness-110"
|
|
626
|
+
:class="[
|
|
627
|
+
{ 'alpha-arc-did__band--animated': isAnimated },
|
|
628
|
+
{ 'alpha-arc-did__sector--active': sector.parts[0] && isPartActive(sector.parts[0]) },
|
|
629
|
+
props.interactive ? 'cursor-pointer pointer-events-auto' : 'pointer-events-none',
|
|
630
|
+
]"
|
|
631
|
+
:style="{
|
|
632
|
+
opacity: getPartOpacity(layer.index, !!(sector.parts[0] && isPartActive(sector.parts[0]))),
|
|
633
|
+
filter: sector.parts[0] && isPartActive(sector.parts[0])
|
|
634
|
+
? 'drop-shadow(0 0 14px rgba(255, 255, 255, 0.85)) drop-shadow(0 0 6px rgba(56, 189, 248, 0.8)) brightness(1.2)'
|
|
635
|
+
: undefined,
|
|
636
|
+
'--layer-anim-delay': `${resolvedStartDelay + layer.index * 100}ms`,
|
|
637
|
+
}"
|
|
638
|
+
@click.stop="sector.parts[0] ? handlePartClick(sector.parts[0], layer.index) : handleLayerClick(layer.index)"
|
|
639
|
+
/>
|
|
640
|
+
</template>
|
|
641
|
+
|
|
642
|
+
<!-- Divider concentric lines when sector is split -->
|
|
643
|
+
<template v-if="props.showPartBoundaries && sector.split && sector.dividerPaths.length > 0">
|
|
644
|
+
<path
|
|
645
|
+
v-for="(dPath, dIdx) in sector.dividerPaths"
|
|
646
|
+
:key="dIdx"
|
|
647
|
+
:d="dPath"
|
|
648
|
+
fill="none"
|
|
649
|
+
:stroke="palette.strokeColor"
|
|
650
|
+
stroke-width="1.5"
|
|
651
|
+
stroke-linecap="round"
|
|
652
|
+
class="pointer-events-none"
|
|
653
|
+
/>
|
|
654
|
+
</template>
|
|
655
|
+
</template>
|
|
656
|
+
</template>
|
|
657
|
+
</g>
|
|
658
|
+
</g>
|
|
659
|
+
|
|
660
|
+
<!-- Active Sector Highlight Overlay (rendered on top of all layers and dividers so stroke & glow are never clipped) -->
|
|
661
|
+
<g v-if="hasAnyActivePart" class="alpha-arc-did__active-sectors pointer-events-none">
|
|
662
|
+
<template v-for="layer in geo.layers" :key="layer.index">
|
|
663
|
+
<template v-for="sector in layer.sectors" :key="sector.index">
|
|
664
|
+
<template v-if="sector.split">
|
|
665
|
+
<template v-for="part in sector.parts" :key="part.index">
|
|
666
|
+
<path
|
|
667
|
+
v-if="isPartActive(part) && part.path"
|
|
668
|
+
:d="part.path"
|
|
669
|
+
:fill="getActivePartFill(part.color || sector.color, getLayerColor(layer.index))"
|
|
670
|
+
stroke="#ffffff"
|
|
671
|
+
stroke-width="3.5"
|
|
672
|
+
stroke-linejoin="round"
|
|
673
|
+
class="alpha-arc-did__band alpha-arc-did__sector alpha-arc-did__sector--active pointer-events-none"
|
|
674
|
+
:style="{
|
|
675
|
+
filter: 'drop-shadow(0 0 14px rgba(255, 255, 255, 0.85)) drop-shadow(0 0 6px rgba(56, 189, 248, 0.8)) brightness(1.2)',
|
|
676
|
+
}"
|
|
677
|
+
/>
|
|
678
|
+
</template>
|
|
679
|
+
</template>
|
|
680
|
+
<template v-else>
|
|
681
|
+
<path
|
|
682
|
+
v-if="sector.parts[0] && isPartActive(sector.parts[0]) && sector.path"
|
|
683
|
+
:d="sector.path"
|
|
684
|
+
:fill="getActivePartFill(sector.color, getLayerColor(layer.index))"
|
|
685
|
+
stroke="#ffffff"
|
|
686
|
+
stroke-width="3.5"
|
|
687
|
+
stroke-linejoin="round"
|
|
688
|
+
class="alpha-arc-did__band alpha-arc-did__sector alpha-arc-did__sector--active pointer-events-none"
|
|
689
|
+
:style="{
|
|
690
|
+
filter: 'drop-shadow(0 0 14px rgba(255, 255, 255, 0.85)) drop-shadow(0 0 6px rgba(56, 189, 248, 0.8)) brightness(1.2)',
|
|
691
|
+
}"
|
|
692
|
+
/>
|
|
693
|
+
</template>
|
|
694
|
+
</template>
|
|
695
|
+
</template>
|
|
696
|
+
</g>
|
|
697
|
+
|
|
698
|
+
<!-- Center Cutout Hole (Solid White Semicircle) -->
|
|
699
|
+
<path
|
|
700
|
+
v-if="geo.innerCutoutPath"
|
|
701
|
+
:d="geo.innerCutoutPath"
|
|
702
|
+
fill="#ffffff"
|
|
703
|
+
stroke="none"
|
|
704
|
+
class="pointer-events-none"
|
|
705
|
+
/>
|
|
706
|
+
</svg>
|
|
707
|
+
|
|
708
|
+
<!-- Stage Overlays: Slotted Layers -->
|
|
709
|
+
<div class="alpha-arc-did__stage-overlay absolute inset-0 w-full h-full pointer-events-none">
|
|
710
|
+
<slot />
|
|
711
|
+
</div>
|
|
712
|
+
</div>
|
|
713
|
+
</div>
|
|
714
|
+
</template>
|
|
715
|
+
|
|
716
|
+
<style scoped>
|
|
717
|
+
.alpha-arc-did__band--animated {
|
|
718
|
+
animation: arc-did-band-in 600ms cubic-bezier(0.16, 1, 0.3, 1) var(--layer-anim-delay, 0ms) both;
|
|
719
|
+
transform-origin: 50% 90%;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
@keyframes arc-did-band-in {
|
|
723
|
+
0% {
|
|
724
|
+
opacity: 0;
|
|
725
|
+
transform: scale(0.92);
|
|
726
|
+
}
|
|
727
|
+
100% {
|
|
728
|
+
opacity: 1;
|
|
729
|
+
transform: scale(1);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
</style>
|