@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.
Files changed (26) hide show
  1. package/components/defense-in-depth/arc-did/ArcDiD.vue +732 -0
  2. package/components/defense-in-depth/arc-did/ArcDiDLayer.vue +321 -0
  3. package/components/defense-in-depth/arc-did/ArcDiDLayerPart.vue +449 -0
  4. package/components/defense-in-depth/arc-did/ArcDiDLayerParts.vue +11 -0
  5. package/components/defense-in-depth/arc-did/ArcDiDLayerSector.vue +148 -0
  6. package/components/defense-in-depth/arc-did/ArcDiDLayerTitle.vue +98 -0
  7. package/components/pyramid/circular-pyramid/CircularPyramid.vue +10 -23
  8. package/components/pyramid/circular-pyramid/CircularPyramidStack.vue +17 -50
  9. package/components/pyramid/circular-pyramid/CircularPyramidStackContent.vue +98 -1
  10. package/components/pyramid/circular-pyramid/CircularPyramidStackTitle.vue +70 -0
  11. package/components/pyramid/square-pyramid/SquarePyramid.vue +10 -23
  12. package/components/pyramid/square-pyramid/SquarePyramidStack.vue +17 -50
  13. package/components/pyramid/square-pyramid/SquarePyramidStackContent.vue +98 -1
  14. package/components/pyramid/square-pyramid/SquarePyramidStackTitle.vue +70 -0
  15. package/package.json +1 -1
  16. package/utils/defenseInDepth/arcDid/context.ts +110 -0
  17. package/utils/defenseInDepth/arcDid/createArcDiDGeometry.ts +570 -0
  18. package/utils/defenseInDepth/arcDid/index.ts +3 -0
  19. package/utils/defenseInDepth/arcDid/types.ts +140 -0
  20. package/utils/defenseInDepth/index.ts +2 -0
  21. package/utils/defenseInDepth/shared/colors.ts +121 -0
  22. package/utils/defenseInDepth/shared/constants.ts +14 -0
  23. package/utils/defenseInDepth/shared/index.ts +3 -0
  24. package/utils/defenseInDepth/shared/types.ts +12 -0
  25. package/utils/pyramid/shared/constants.ts +1 -0
  26. package/utils/useDiagramAutoScale.ts +61 -15
@@ -0,0 +1,449 @@
1
+ <script setup lang="ts">
2
+ import { computed, inject, onUnmounted, useSlots, type VNode } from 'vue';
3
+
4
+ import {
5
+ ARC_DID_LAYER_KEY,
6
+ ARC_DID_ROOT_KEY,
7
+ ARC_DID_SECTOR_KEY,
8
+ } from '../../../utils/defenseInDepth/arcDid/context';
9
+ import {
10
+ calculatePartAngles,
11
+ polarToCartesian,
12
+ } from '../../../utils/defenseInDepth/arcDid/createArcDiDGeometry';
13
+ import type {
14
+ IArcDiDPartItem,
15
+ TArcDiDPartPosition,
16
+ } from '../../../utils/defenseInDepth/arcDid/types';
17
+ import { DEFAULT_DID_DOT_COLOR } from '../../../utils/defenseInDepth/shared/constants';
18
+
19
+ defineOptions({
20
+ name: 'ArcDiDLayerPart',
21
+ });
22
+
23
+ export interface IArcDiDLayerPartProps {
24
+ readonly label?: string;
25
+ readonly index?: number;
26
+ readonly angle?: number;
27
+ readonly radius?: number;
28
+ readonly radialOffset?: number;
29
+ readonly color?: string;
30
+ readonly icon?: string;
31
+ readonly showDot?: boolean;
32
+ readonly span?: number;
33
+ readonly position?: TArcDiDPartPosition;
34
+ readonly maxWidth?: number | string;
35
+ readonly rotate?: boolean | number | 'tangent' | 'auto';
36
+ readonly skew?: boolean | number | string;
37
+ readonly active?: boolean;
38
+ }
39
+
40
+ const props = withDefaults(defineProps<IArcDiDLayerPartProps>(), {
41
+ label: undefined,
42
+ index: undefined,
43
+ angle: undefined,
44
+ radius: undefined,
45
+ radialOffset: undefined,
46
+ color: undefined,
47
+ icon: undefined,
48
+ showDot: false,
49
+ span: undefined,
50
+ position: undefined,
51
+ maxWidth: undefined,
52
+ rotate: undefined,
53
+ skew: undefined,
54
+ active: undefined,
55
+ });
56
+
57
+ const slots = useSlots();
58
+ const rootContext = inject(ARC_DID_ROOT_KEY, undefined);
59
+ const layerContext = inject(ARC_DID_LAYER_KEY, undefined);
60
+ const sectorContext = inject(ARC_DID_SECTOR_KEY, undefined);
61
+
62
+ function extractTextFromVNodes(nodes: VNode[]): string {
63
+ let text = '';
64
+ for (const node of nodes) {
65
+ if (typeof node.type === 'string' && node.type === 'br') {
66
+ text += '\n';
67
+ } else if (typeof node.children === 'string') {
68
+ text += node.children;
69
+ } else if (Array.isArray(node.children)) {
70
+ text += extractTextFromVNodes(node.children as VNode[]);
71
+ }
72
+ }
73
+ return text.trim();
74
+ }
75
+
76
+ const slottedLabel = computed<string>(() => {
77
+ if (props.label) {
78
+ return props.label;
79
+ }
80
+ if (slots.default) {
81
+ const vnodes = slots.default();
82
+ const textNode = vnodes[0]?.children;
83
+ if (typeof textNode === 'string') {
84
+ return textNode.trim();
85
+ }
86
+ return extractTextFromVNodes(slots.default());
87
+ }
88
+ return '';
89
+ });
90
+
91
+ const partToken = Symbol();
92
+ const registrationData = computed<IArcDiDPartItem>(() => ({
93
+ label: slottedLabel.value,
94
+ angle: props.angle,
95
+ radius: props.radius,
96
+ radialOffset: props.radialOffset,
97
+ color: props.color,
98
+ icon: props.icon,
99
+ span: props.span,
100
+ position: props.position,
101
+ maxWidth: props.maxWidth,
102
+ rotate: props.rotate,
103
+ skew: props.skew,
104
+ active: props.active,
105
+ }));
106
+
107
+ const registration = sectorContext
108
+ ? sectorContext.registerPart(partToken, registrationData)
109
+ : layerContext?.registerPart(partToken, registrationData);
110
+
111
+ onUnmounted(() => {
112
+ registration?.unregister();
113
+ });
114
+
115
+ const resolvedIndex = computed<number>(() => {
116
+ if (props.index !== undefined) {
117
+ return Number(props.index);
118
+ }
119
+ return registration?.index.value ?? 0;
120
+ });
121
+
122
+ const calculatedPosition = computed(() => {
123
+ const geo = layerContext?.layerGeo.value;
124
+ const rootGeo = rootContext?.geo.value;
125
+
126
+ if (props.angle !== undefined && rootGeo && geo) {
127
+ const r = props.radius ?? geo.midRadius + (props.radialOffset ?? 0);
128
+ return polarToCartesian(rootGeo.cx, rootGeo.cy, r, props.angle);
129
+ }
130
+
131
+ if (sectorContext) {
132
+ const partGeo = sectorContext.sectorGeo.value?.parts[resolvedIndex.value];
133
+ if (partGeo) {
134
+ return { x: partGeo.x, y: partGeo.y };
135
+ }
136
+ } else {
137
+ const standalonePartGeo =
138
+ layerContext?.layerGeo.value?.sectors[resolvedIndex.value]?.parts[0];
139
+ if (standalonePartGeo) {
140
+ return { x: standalonePartGeo.x, y: standalonePartGeo.y };
141
+ }
142
+ }
143
+
144
+ const partGeo = geo?.parts[resolvedIndex.value];
145
+ if (partGeo) {
146
+ return { x: partGeo.x, y: partGeo.y };
147
+ }
148
+
149
+ if (rootGeo && geo) {
150
+ const totalParts = layerContext?.partsCount?.value ?? 1;
151
+ const angles = calculatePartAngles(totalParts);
152
+ const fallbackAngle = angles[resolvedIndex.value] ?? 90;
153
+ const r = props.radius ?? geo.midRadius + (props.radialOffset ?? 0);
154
+ return polarToCartesian(rootGeo.cx, rootGeo.cy, r, fallbackAngle);
155
+ }
156
+
157
+ return { x: 0, y: 0 };
158
+ });
159
+
160
+ const isAnimated = computed(() => rootContext?.animation.value ?? true);
161
+ const rootStartDelay = computed(() => rootContext?.startDelay.value ?? 0);
162
+
163
+ const effectiveDotColor = computed(() => {
164
+ return props.color ?? layerContext?.color.value ?? DEFAULT_DID_DOT_COLOR;
165
+ });
166
+
167
+ const animationDelay = computed(() => {
168
+ const layerIdx = layerContext?.layerIndex.value ?? 0;
169
+ const sectorIdx = sectorContext?.sectorIndex.value ?? 0;
170
+ return (
171
+ rootStartDelay.value +
172
+ layerIdx * 120 +
173
+ sectorIdx * 60 +
174
+ resolvedIndex.value * 40 +
175
+ 200
176
+ );
177
+ });
178
+
179
+ const maxContentWidth = computed(() => {
180
+ if (props.maxWidth !== undefined) {
181
+ return typeof props.maxWidth === 'number'
182
+ ? `${props.maxWidth}px`
183
+ : props.maxWidth;
184
+ }
185
+ if (sectorContext) {
186
+ const partGeo = sectorContext.sectorGeo.value?.parts[resolvedIndex.value];
187
+ if (partGeo?.maxContentWidth) {
188
+ return `${partGeo.maxContentWidth}px`;
189
+ }
190
+ } else {
191
+ const standalonePartGeo =
192
+ layerContext?.layerGeo.value?.sectors[resolvedIndex.value]?.parts[0];
193
+ if (standalonePartGeo?.maxContentWidth) {
194
+ return `${standalonePartGeo.maxContentWidth}px`;
195
+ }
196
+ }
197
+ const geo = layerContext?.layerGeo.value;
198
+ if (!geo) return '110px';
199
+ const partGeo = geo.parts[resolvedIndex.value];
200
+ if (partGeo?.maxContentWidth) {
201
+ return `${partGeo.maxContentWidth}px`;
202
+ }
203
+ const span =
204
+ partGeo?.startAngle !== undefined && partGeo?.endAngle !== undefined
205
+ ? Math.abs(partGeo.startAngle - partGeo.endAngle)
206
+ : 35;
207
+ const arcLength = geo.midRadius * (span * (Math.PI / 180));
208
+ return `${Math.max(100, Math.min(180, Math.round(arcLength * 0.92)))}px`;
209
+ });
210
+
211
+ const isSplitSector = computed(() => {
212
+ if (
213
+ props.position === 'top' ||
214
+ props.position === 'bottom' ||
215
+ props.position === 'middle'
216
+ ) {
217
+ return true;
218
+ }
219
+ if (sectorContext?.sectorGeo.value?.split) {
220
+ return true;
221
+ }
222
+ if ((sectorContext?.sectorGeo.value?.parts.length ?? 0) > 1) {
223
+ return true;
224
+ }
225
+ return false;
226
+ });
227
+
228
+ const resolvedAngle = computed<number>(() => {
229
+ if (props.angle !== undefined) {
230
+ return props.angle;
231
+ }
232
+ if (sectorContext) {
233
+ const partGeo = sectorContext.sectorGeo.value?.parts[resolvedIndex.value];
234
+ if (partGeo?.angle !== undefined) {
235
+ return partGeo.angle;
236
+ }
237
+ if (sectorContext.sectorGeo.value?.midAngle !== undefined) {
238
+ return sectorContext.sectorGeo.value.midAngle;
239
+ }
240
+ }
241
+ const geo = layerContext?.layerGeo.value;
242
+ const partGeo = geo?.parts[resolvedIndex.value];
243
+ if (partGeo?.angle !== undefined) {
244
+ return partGeo.angle;
245
+ }
246
+ return 90;
247
+ });
248
+
249
+ const effectiveRotate = computed(() => {
250
+ return props.rotate ?? sectorContext?.rotate?.value;
251
+ });
252
+
253
+ const effectiveSkew = computed(() => {
254
+ return props.skew ?? sectorContext?.skew?.value;
255
+ });
256
+
257
+ const resolvedRotation = computed<string | undefined>(() => {
258
+ const rot = effectiveRotate.value;
259
+ if (rot === undefined || rot === false) {
260
+ const sk = effectiveSkew.value;
261
+ if (sk === true || sk === 'tangent' || sk === 'auto') {
262
+ const tangent = Math.round(90 - resolvedAngle.value);
263
+ return `rotate(${tangent}deg)`;
264
+ }
265
+ return undefined;
266
+ }
267
+ if (typeof rot === 'number') {
268
+ return `rotate(${rot}deg)`;
269
+ }
270
+ if (rot === true || rot === 'tangent' || rot === 'auto') {
271
+ const tangent = Math.round(90 - resolvedAngle.value);
272
+ return `rotate(${tangent}deg)`;
273
+ }
274
+ if (typeof rot === 'string') {
275
+ return rot.includes('rotate')
276
+ ? rot
277
+ : rot.includes('deg')
278
+ ? `rotate(${rot})`
279
+ : `rotate(${rot}deg)`;
280
+ }
281
+ return undefined;
282
+ });
283
+
284
+ const resolvedSkew = computed<string | undefined>(() => {
285
+ const sk = effectiveSkew.value;
286
+ if (sk === undefined || sk === false) {
287
+ return undefined;
288
+ }
289
+ if (sk === true || sk === 'tangent' || sk === 'auto') {
290
+ return undefined;
291
+ }
292
+ if (typeof sk === 'number') {
293
+ return `skewX(${sk}deg)`;
294
+ }
295
+ if (typeof sk === 'string') {
296
+ return sk.includes('skew') || sk.includes('rotate')
297
+ ? sk
298
+ : sk.includes('deg')
299
+ ? `skewX(${sk})`
300
+ : `skewX(${sk}deg)`;
301
+ }
302
+ return undefined;
303
+ });
304
+
305
+ const partLabelTransform = computed<string | undefined>(() => {
306
+ const transforms: string[] = [];
307
+ if (resolvedRotation.value) {
308
+ transforms.push(resolvedRotation.value);
309
+ }
310
+ if (resolvedSkew.value) {
311
+ transforms.push(resolvedSkew.value);
312
+ }
313
+ return transforms.length > 0 ? transforms.join(' ') : undefined;
314
+ });
315
+
316
+ const isSelfActive = computed<boolean>(() => {
317
+ if (props.active !== undefined) {
318
+ return props.active;
319
+ }
320
+ if (rootContext?.isPartActive) {
321
+ return rootContext.isPartActive(
322
+ slottedLabel.value,
323
+ resolvedIndex.value,
324
+ props.active,
325
+ );
326
+ }
327
+ if (rootContext?.activePart?.value !== undefined) {
328
+ const ap = rootContext.activePart.value;
329
+ return ap === slottedLabel.value || ap === resolvedIndex.value;
330
+ }
331
+ return false;
332
+ });
333
+
334
+ const hasAnyActivePart = computed<boolean>(() => {
335
+ return rootContext?.hasAnyActivePart?.value ?? false;
336
+ });
337
+
338
+ function handlePartAreaClick(event: MouseEvent): void {
339
+ if (rootContext?.interactive?.value && rootContext.setActivePart) {
340
+ event.stopPropagation();
341
+ if (isSelfActive.value) {
342
+ rootContext.setActivePart(undefined);
343
+ } else {
344
+ rootContext.setActivePart(slottedLabel.value);
345
+ }
346
+ }
347
+ }
348
+ </script>
349
+
350
+ <template>
351
+ <div
352
+ class="alpha-arc-did__part alpha-arc-did__part-area absolute flex flex-col items-center justify-center select-none text-center transition-all duration-300 ease-out"
353
+ :class="[
354
+ { 'alpha-arc-did__part--animated': isAnimated },
355
+ { 'alpha-arc-did__part--active': isSelfActive },
356
+ hasAnyActivePart && !isSelfActive ? 'opacity-30' : 'opacity-100',
357
+ rootContext?.interactive?.value ? 'cursor-pointer pointer-events-auto' : 'pointer-events-none',
358
+ ]"
359
+ :data-active="isSelfActive"
360
+ :style="{
361
+ left: `${calculatedPosition.x}px`,
362
+ top: `${calculatedPosition.y}px`,
363
+ transform: 'translate(-50%, -50%)',
364
+ zIndex: isSelfActive ? 30 : 10,
365
+ '--part-delay': `${animationDelay}ms`,
366
+ }"
367
+ @click="handlePartAreaClick"
368
+ >
369
+ <!-- Optional Dot Indicator or Icon -->
370
+ <div
371
+ v-if="props.showDot || props.icon"
372
+ class="relative flex items-center justify-center cursor-default group mb-1"
373
+ >
374
+ <span
375
+ v-if="props.icon"
376
+ class="text-xs text-white"
377
+ :class="props.icon"
378
+ />
379
+ <div
380
+ v-else-if="props.showDot"
381
+ class="w-4 h-4 rounded-full transition-all duration-200 flex items-center justify-center"
382
+ :class="[
383
+ isSelfActive ? 'scale-125 ring-2 ring-white shadow-[0_0_12px_#38bdf8]' : 'group-hover:scale-125',
384
+ ]"
385
+ :style="{
386
+ background: isSelfActive
387
+ ? 'radial-gradient(circle at 35% 32%, #ffffff 25%, #38bdf8 70%, #0369a1 100%)'
388
+ : props.color
389
+ ? `radial-gradient(circle at 35% 32%, #ffffff 15%, ${effectiveDotColor} 65%, #0f172a 100%)`
390
+ : 'radial-gradient(circle at 35% 32%, #ffffff 15%, #cbd5e1 55%, #94a3b8 100%)',
391
+ boxShadow: isSelfActive
392
+ ? '0 0 12px rgba(56, 189, 248, 0.9), inset 0 1px 1px rgba(255, 255, 255, 0.9)'
393
+ : '0 2px 5px rgba(0, 0, 0, 0.28), inset 0 1px 1px rgba(255, 255, 255, 0.8)',
394
+ }"
395
+ />
396
+ </div>
397
+
398
+ <!-- Part Area Content -->
399
+ <div
400
+ class="alpha-arc-did__part-label-wrapper"
401
+ :style="{
402
+ transform: partLabelTransform,
403
+ }"
404
+ >
405
+ <div
406
+ class="alpha-arc-did__part-label alpha-arc-did__part-content text-center font-medium leading-tight text-white drop-shadow-sm whitespace-pre-line transition-all duration-200"
407
+ :class="[
408
+ isSplitSector ? 'text-[11px] md:text-xs' : 'text-xs md:text-sm',
409
+ isSelfActive
410
+ ? 'font-bold !text-white scale-110'
411
+ : 'hover:scale-105',
412
+ ]"
413
+ :style="{
414
+ maxWidth: maxContentWidth,
415
+ color: isSelfActive ? '#ffffff' : props.color,
416
+ textShadow: isSelfActive
417
+ ? '0 0 12px rgba(255, 255, 255, 0.95), 0 2px 6px rgba(0, 0, 0, 0.9)'
418
+ : undefined,
419
+ }"
420
+ >
421
+ <slot>{{ slottedLabel }}</slot>
422
+ </div>
423
+ </div>
424
+ </div>
425
+ </template>
426
+
427
+ <style scoped>
428
+ .alpha-arc-did__part-label {
429
+ word-break: normal;
430
+ overflow-wrap: normal;
431
+ hyphens: none;
432
+ white-space: pre-line;
433
+ }
434
+
435
+ .alpha-arc-did__part--animated {
436
+ animation: arc-did-part-pop 450ms cubic-bezier(0.16, 1, 0.3, 1) var(--part-delay, 0ms) both;
437
+ }
438
+
439
+ @keyframes arc-did-part-pop {
440
+ 0% {
441
+ opacity: 0;
442
+ transform: translate(-50%, -50%) scale(0.6);
443
+ }
444
+ 100% {
445
+ opacity: 1;
446
+ transform: translate(-50%, -50%) scale(1);
447
+ }
448
+ }
449
+ </style>
@@ -0,0 +1,11 @@
1
+ <script setup lang="ts">
2
+ defineOptions({
3
+ name: 'ArcDiDLayerParts',
4
+ });
5
+ </script>
6
+
7
+ <template>
8
+ <div class="alpha-arc-did__layer-parts contents pointer-events-none">
9
+ <slot />
10
+ </div>
11
+ </template>
@@ -0,0 +1,148 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ type ComputedRef,
4
+ computed,
5
+ inject,
6
+ onUnmounted,
7
+ provide,
8
+ ref,
9
+ watch,
10
+ } from 'vue';
11
+
12
+ import {
13
+ ARC_DID_LAYER_KEY,
14
+ ARC_DID_SECTOR_KEY,
15
+ type IArcDiDRegisteredSectorData,
16
+ type IArcDiDSectorContext,
17
+ } from '../../../utils/defenseInDepth/arcDid/context';
18
+ import type { IArcDiDPartItem } from '../../../utils/defenseInDepth/arcDid/types';
19
+
20
+ defineOptions({
21
+ name: 'ArcDiDLayerSector',
22
+ });
23
+
24
+ export interface IArcDiDLayerSectorProps {
25
+ readonly split?: boolean;
26
+ readonly angle?: number;
27
+ readonly span?: number;
28
+ readonly color?: string;
29
+ readonly index?: number;
30
+ readonly rotate?: boolean | number | 'tangent' | 'auto';
31
+ readonly skew?: boolean | number | string;
32
+ }
33
+
34
+ const props = withDefaults(defineProps<IArcDiDLayerSectorProps>(), {
35
+ split: true,
36
+ angle: undefined,
37
+ span: undefined,
38
+ color: undefined,
39
+ index: undefined,
40
+ rotate: undefined,
41
+ skew: undefined,
42
+ });
43
+
44
+ const layerContext = inject(ARC_DID_LAYER_KEY, undefined);
45
+
46
+ const registeredPartTokens = ref<symbol[]>([]);
47
+ const registeredPartData = ref<Map<symbol, IArcDiDPartItem>>(new Map());
48
+
49
+ function registerPart(
50
+ token?: symbol,
51
+ partDataRef?: ComputedRef<IArcDiDPartItem | undefined>,
52
+ ) {
53
+ const pToken = token ?? Symbol();
54
+ if (!registeredPartTokens.value.includes(pToken)) {
55
+ registeredPartTokens.value = [...registeredPartTokens.value, pToken];
56
+ }
57
+
58
+ let stopWatch: (() => void) | undefined;
59
+ if (partDataRef) {
60
+ stopWatch = watch(
61
+ partDataRef,
62
+ (val) => {
63
+ const nextMap = new Map(registeredPartData.value);
64
+ if (val) {
65
+ nextMap.set(pToken, val);
66
+ } else {
67
+ nextMap.delete(pToken);
68
+ }
69
+ registeredPartData.value = nextMap;
70
+ },
71
+ { immediate: true },
72
+ );
73
+ }
74
+
75
+ const index = computed(() => registeredPartTokens.value.indexOf(pToken));
76
+
77
+ return {
78
+ index,
79
+ unregister: () => {
80
+ stopWatch?.();
81
+ registeredPartTokens.value = registeredPartTokens.value.filter(
82
+ (t) => t !== pToken,
83
+ );
84
+ const nextMap = new Map(registeredPartData.value);
85
+ nextMap.delete(pToken);
86
+ registeredPartData.value = nextMap;
87
+ },
88
+ };
89
+ }
90
+
91
+ const registeredPartsList = computed<IArcDiDPartItem[]>(() => {
92
+ return registeredPartTokens.value
93
+ .map((token) => registeredPartData.value.get(token))
94
+ .filter((item): item is IArcDiDPartItem => item !== undefined);
95
+ });
96
+
97
+ const sectorToken = Symbol();
98
+ const sectorRegistrationData = computed<IArcDiDRegisteredSectorData>(() => ({
99
+ parts: registeredPartsList.value,
100
+ split: props.split,
101
+ angle: props.angle,
102
+ span: props.span,
103
+ color: props.color,
104
+ index: props.index !== undefined ? Number(props.index) : undefined,
105
+ rotate: props.rotate,
106
+ skew: props.skew,
107
+ }));
108
+
109
+ const registration = layerContext?.registerSector(
110
+ sectorToken,
111
+ sectorRegistrationData,
112
+ );
113
+
114
+ onUnmounted(() => {
115
+ registration?.unregister();
116
+ });
117
+
118
+ const resolvedIndex = computed<number>(() => {
119
+ if (props.index !== undefined) {
120
+ return Number(props.index);
121
+ }
122
+ return registration?.index.value ?? 0;
123
+ });
124
+
125
+ const sectorGeo = computed(() => {
126
+ return layerContext?.layerGeo.value?.sectors[resolvedIndex.value];
127
+ });
128
+
129
+ const sectorContext: IArcDiDSectorContext = {
130
+ sectorIndex: resolvedIndex,
131
+ sectorGeo,
132
+ rotate: computed(() => props.rotate),
133
+ skew: computed(() => props.skew),
134
+ registerPart,
135
+ };
136
+
137
+ provide(ARC_DID_SECTOR_KEY, sectorContext);
138
+ </script>
139
+
140
+ <template>
141
+ <div
142
+ class="alpha-arc-did__layer-sector contents pointer-events-none"
143
+ :data-sector-index="resolvedIndex"
144
+ :data-split="props.split"
145
+ >
146
+ <slot />
147
+ </div>
148
+ </template>
@@ -0,0 +1,98 @@
1
+ <script setup lang="ts">
2
+ import { computed, inject, ref, useSlots, type VNode, watch } from 'vue';
3
+
4
+ import { ARC_DID_LAYER_KEY } from '../../../utils/defenseInDepth/arcDid/context';
5
+
6
+ defineOptions({
7
+ name: 'ArcDiDLayerTitle',
8
+ });
9
+
10
+ export interface IArcDiDLayerTitleProps {
11
+ readonly title?: string;
12
+ readonly color?: string;
13
+ }
14
+
15
+ const props = withDefaults(defineProps<IArcDiDLayerTitleProps>(), {
16
+ title: undefined,
17
+ color: undefined,
18
+ });
19
+
20
+ const slots = useSlots();
21
+ const layerContext = inject(ARC_DID_LAYER_KEY, undefined);
22
+
23
+ function extractTextFromVNodes(nodes: VNode[]): string {
24
+ let text = '';
25
+ for (const node of nodes) {
26
+ if (typeof node.type === 'string' && node.type === 'br') {
27
+ text += '\n';
28
+ } else if (typeof node.children === 'string') {
29
+ text += node.children;
30
+ } else if (Array.isArray(node.children)) {
31
+ text += extractTextFromVNodes(node.children as VNode[]);
32
+ }
33
+ }
34
+ return text.trim();
35
+ }
36
+
37
+ const rawTitle = computed(() => {
38
+ if (props.title) {
39
+ return props.title;
40
+ }
41
+ if (slots.default) {
42
+ return extractTextFromVNodes(slots.default());
43
+ }
44
+ return '';
45
+ });
46
+
47
+ watch(
48
+ rawTitle,
49
+ (val) => {
50
+ if (val && layerContext?.setTitle) {
51
+ layerContext.setTitle(val);
52
+ }
53
+ },
54
+ { immediate: true },
55
+ );
56
+
57
+ const maxTitleWidth = computed(() => {
58
+ const geo = layerContext?.layerGeo.value;
59
+ if (!geo) {
60
+ return 130;
61
+ }
62
+ const arcLen = geo.midRadius * (40 * (Math.PI / 180));
63
+ return Math.max(75, Math.min(130, Math.round(arcLen * 0.82)));
64
+ });
65
+
66
+ const positionStyle = computed(() => {
67
+ const geo = layerContext?.layerGeo.value;
68
+ if (!geo) {
69
+ return {};
70
+ }
71
+ return {
72
+ left: `${geo.titlePosition.x}px`,
73
+ top: `${geo.titlePosition.y}px`,
74
+ transform: 'translate(-50%, -50%)',
75
+ maxWidth: `${maxTitleWidth.value}px`,
76
+ };
77
+ });
78
+ </script>
79
+
80
+ <template>
81
+ <div
82
+ class="alpha-arc-did__layer-title absolute z-20 pointer-events-auto select-none text-center"
83
+ :style="positionStyle"
84
+ >
85
+ <div
86
+ class="font-semibold text-slate-800 dark:text-slate-100 tracking-tight leading-snug whitespace-pre-line text-sm md:text-base transition-colors duration-200"
87
+ :style="{ color: props.color ?? layerContext?.textColor?.value }"
88
+ >
89
+ <slot>{{ props.title }}</slot>
90
+ </div>
91
+ </div>
92
+ </template>
93
+
94
+ <style scoped>
95
+ .alpha-arc-did__layer-title {
96
+ max-width: 140px;
97
+ }
98
+ </style>