agentation-vue 0.2.2 → 0.2.3
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/dist/AgentationVue.vue +416 -529
- package/dist/components/AgentationToolbar.vue +49 -76
- package/dist/components/AnnotationInput.vue +28 -38
- package/dist/components/AnnotationMarker.vue +15 -20
- package/dist/components/ComponentChain.vue +17 -30
- package/dist/components/ElementHighlight.vue +13 -16
- package/dist/components/SettingsPanel.vue +35 -43
- package/dist/components/SettingsPopover.vue +128 -159
- package/dist/components/VaButton.vue +6 -12
- package/dist/components/VaIcon.vue +5 -5
- package/dist/components/VaIconButton.vue +15 -23
- package/dist/components/VaToggle.vue +7 -8
- package/package.json +4 -3
|
@@ -1,223 +1,192 @@
|
|
|
1
|
-
<script setup
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
left
|
|
24
|
-
top:
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
let rafId: number | null = null
|
|
31
|
-
|
|
32
|
-
function getPlacementCandidates(anchorRect: DOMRect): Placement[] {
|
|
33
|
-
const isOnBottomHalf = anchorRect.top > window.innerHeight / 2
|
|
34
|
-
const isOnRightHalf = anchorRect.left > window.innerWidth / 2
|
|
35
|
-
const preferredVertical = isOnBottomHalf ? 'top' : 'bottom'
|
|
36
|
-
const secondaryVertical = isOnBottomHalf ? 'bottom' : 'top'
|
|
37
|
-
const preferredAlign = isOnRightHalf ? 'end' : 'start'
|
|
38
|
-
const secondaryAlign = isOnRightHalf ? 'start' : 'end'
|
|
39
|
-
const preferredHorizontal = isOnRightHalf ? 'left' : 'right'
|
|
40
|
-
const secondaryHorizontal = isOnRightHalf ? 'right' : 'left'
|
|
41
|
-
|
|
1
|
+
<script setup>
|
|
2
|
+
import { nextTick, onBeforeUnmount, ref, watch } from "vue-demi";
|
|
3
|
+
import { clamp } from "../utils/math";
|
|
4
|
+
import SettingsPanel from "./SettingsPanel.vue";
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
open: { type: Boolean, required: true },
|
|
7
|
+
settings: { type: Object, required: true },
|
|
8
|
+
anchorEl: { type: null, required: true }
|
|
9
|
+
});
|
|
10
|
+
const emit = defineEmits(["close", "update"]);
|
|
11
|
+
const panelEl = ref(null);
|
|
12
|
+
const placement = ref("bottom-start");
|
|
13
|
+
const style = ref({
|
|
14
|
+
left: "-9999px",
|
|
15
|
+
top: "-9999px",
|
|
16
|
+
visibility: "hidden"
|
|
17
|
+
});
|
|
18
|
+
const GAP = 8;
|
|
19
|
+
const VIEWPORT_PADDING = 8;
|
|
20
|
+
let rafId = null;
|
|
21
|
+
function getPlacementCandidates(anchorRect) {
|
|
22
|
+
const isOnBottomHalf = anchorRect.top > window.innerHeight / 2;
|
|
23
|
+
const isOnRightHalf = anchorRect.left > window.innerWidth / 2;
|
|
24
|
+
const preferredVertical = isOnBottomHalf ? "top" : "bottom";
|
|
25
|
+
const secondaryVertical = isOnBottomHalf ? "bottom" : "top";
|
|
26
|
+
const preferredAlign = isOnRightHalf ? "end" : "start";
|
|
27
|
+
const secondaryAlign = isOnRightHalf ? "start" : "end";
|
|
28
|
+
const preferredHorizontal = isOnRightHalf ? "left" : "right";
|
|
29
|
+
const secondaryHorizontal = isOnRightHalf ? "right" : "left";
|
|
42
30
|
return [
|
|
43
|
-
`${preferredVertical}-${preferredAlign}
|
|
44
|
-
`${preferredVertical}-${secondaryAlign}
|
|
45
|
-
`${secondaryVertical}-${preferredAlign}
|
|
46
|
-
`${secondaryVertical}-${secondaryAlign}
|
|
47
|
-
`${preferredHorizontal}-start
|
|
48
|
-
`${preferredHorizontal}-end
|
|
49
|
-
`${secondaryHorizontal}-start
|
|
50
|
-
`${secondaryHorizontal}-end`
|
|
51
|
-
]
|
|
31
|
+
`${preferredVertical}-${preferredAlign}`,
|
|
32
|
+
`${preferredVertical}-${secondaryAlign}`,
|
|
33
|
+
`${secondaryVertical}-${preferredAlign}`,
|
|
34
|
+
`${secondaryVertical}-${secondaryAlign}`,
|
|
35
|
+
`${preferredHorizontal}-start`,
|
|
36
|
+
`${preferredHorizontal}-end`,
|
|
37
|
+
`${secondaryHorizontal}-start`,
|
|
38
|
+
`${secondaryHorizontal}-end`
|
|
39
|
+
];
|
|
52
40
|
}
|
|
53
|
-
|
|
54
|
-
function getCoordinates(anchorRect: DOMRect, panelRect: BoundingBox, value: Placement): { x: number, y: number } {
|
|
41
|
+
function getCoordinates(anchorRect, panelRect, value) {
|
|
55
42
|
switch (value) {
|
|
56
|
-
case
|
|
57
|
-
return { x: anchorRect.left, y: anchorRect.top - GAP - panelRect.height }
|
|
58
|
-
case
|
|
59
|
-
return { x: anchorRect.right - panelRect.width, y: anchorRect.top - GAP - panelRect.height }
|
|
60
|
-
case
|
|
61
|
-
return { x: anchorRect.left, y: anchorRect.bottom + GAP }
|
|
62
|
-
case
|
|
63
|
-
return { x: anchorRect.right - panelRect.width, y: anchorRect.bottom + GAP }
|
|
64
|
-
case
|
|
65
|
-
return { x: anchorRect.left - GAP - panelRect.width, y: anchorRect.top }
|
|
66
|
-
case
|
|
67
|
-
return { x: anchorRect.left - GAP - panelRect.width, y: anchorRect.bottom - panelRect.height }
|
|
68
|
-
case
|
|
69
|
-
return { x: anchorRect.right + GAP, y: anchorRect.top }
|
|
70
|
-
case
|
|
71
|
-
return { x: anchorRect.right + GAP, y: anchorRect.bottom - panelRect.height }
|
|
43
|
+
case "top-start":
|
|
44
|
+
return { x: anchorRect.left, y: anchorRect.top - GAP - panelRect.height };
|
|
45
|
+
case "top-end":
|
|
46
|
+
return { x: anchorRect.right - panelRect.width, y: anchorRect.top - GAP - panelRect.height };
|
|
47
|
+
case "bottom-start":
|
|
48
|
+
return { x: anchorRect.left, y: anchorRect.bottom + GAP };
|
|
49
|
+
case "bottom-end":
|
|
50
|
+
return { x: anchorRect.right - panelRect.width, y: anchorRect.bottom + GAP };
|
|
51
|
+
case "left-start":
|
|
52
|
+
return { x: anchorRect.left - GAP - panelRect.width, y: anchorRect.top };
|
|
53
|
+
case "left-end":
|
|
54
|
+
return { x: anchorRect.left - GAP - panelRect.width, y: anchorRect.bottom - panelRect.height };
|
|
55
|
+
case "right-start":
|
|
56
|
+
return { x: anchorRect.right + GAP, y: anchorRect.top };
|
|
57
|
+
case "right-end":
|
|
58
|
+
return { x: anchorRect.right + GAP, y: anchorRect.bottom - panelRect.height };
|
|
72
59
|
default:
|
|
73
|
-
return { x: anchorRect.left, y: anchorRect.bottom + GAP }
|
|
60
|
+
return { x: anchorRect.left, y: anchorRect.bottom + GAP };
|
|
74
61
|
}
|
|
75
62
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
const bottomLimit = window.innerHeight - VIEWPORT_PADDING
|
|
63
|
+
function getOverflow(x, y, panelRect) {
|
|
64
|
+
const rightLimit = window.innerWidth - VIEWPORT_PADDING;
|
|
65
|
+
const bottomLimit = window.innerHeight - VIEWPORT_PADDING;
|
|
80
66
|
return {
|
|
81
67
|
left: Math.max(0, VIEWPORT_PADDING - x),
|
|
82
68
|
right: Math.max(0, x + panelRect.width - rightLimit),
|
|
83
69
|
top: Math.max(0, VIEWPORT_PADDING - y),
|
|
84
|
-
bottom: Math.max(0, y + panelRect.height - bottomLimit)
|
|
85
|
-
}
|
|
70
|
+
bottom: Math.max(0, y + panelRect.height - bottomLimit)
|
|
71
|
+
};
|
|
86
72
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
return overflow.left + overflow.right + overflow.top + overflow.bottom
|
|
73
|
+
function getOverflowScore(overflow) {
|
|
74
|
+
return overflow.left + overflow.right + overflow.top + overflow.bottom;
|
|
90
75
|
}
|
|
91
|
-
|
|
92
76
|
function applyPosition() {
|
|
93
77
|
if (!props.open)
|
|
94
|
-
return
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
const panel = panelEl.value
|
|
78
|
+
return;
|
|
79
|
+
const anchorEl = props.anchorEl;
|
|
80
|
+
const panel = panelEl.value;
|
|
98
81
|
if (!anchorEl || !panel) {
|
|
99
|
-
style.value = { left:
|
|
100
|
-
return
|
|
82
|
+
style.value = { left: "-9999px", top: "-9999px", visibility: "hidden" };
|
|
83
|
+
return;
|
|
101
84
|
}
|
|
102
|
-
|
|
103
85
|
if (!anchorEl.isConnected) {
|
|
104
|
-
emit(
|
|
105
|
-
return
|
|
86
|
+
emit("close");
|
|
87
|
+
return;
|
|
106
88
|
}
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
const panelRect: BoundingBox = {
|
|
89
|
+
const anchorRect = anchorEl.getBoundingClientRect();
|
|
90
|
+
const panelRect = {
|
|
110
91
|
x: 0,
|
|
111
92
|
y: 0,
|
|
112
93
|
width: panel.offsetWidth,
|
|
113
|
-
height: panel.offsetHeight
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
let
|
|
118
|
-
let
|
|
119
|
-
let
|
|
120
|
-
let bestScore = getOverflowScore(bestOverflow)
|
|
121
|
-
|
|
94
|
+
height: panel.offsetHeight
|
|
95
|
+
};
|
|
96
|
+
const candidates = getPlacementCandidates(anchorRect);
|
|
97
|
+
let best = candidates[0];
|
|
98
|
+
let bestCoords = getCoordinates(anchorRect, panelRect, best);
|
|
99
|
+
let bestOverflow = getOverflow(bestCoords.x, bestCoords.y, panelRect);
|
|
100
|
+
let bestScore = getOverflowScore(bestOverflow);
|
|
122
101
|
for (let i = 1; i < candidates.length; i++) {
|
|
123
|
-
const candidate = candidates[i]
|
|
124
|
-
const coords = getCoordinates(anchorRect, panelRect, candidate)
|
|
125
|
-
const overflow = getOverflow(coords.x, coords.y, panelRect)
|
|
126
|
-
const score = getOverflowScore(overflow)
|
|
102
|
+
const candidate = candidates[i];
|
|
103
|
+
const coords = getCoordinates(anchorRect, panelRect, candidate);
|
|
104
|
+
const overflow = getOverflow(coords.x, coords.y, panelRect);
|
|
105
|
+
const score = getOverflowScore(overflow);
|
|
127
106
|
if (score < bestScore) {
|
|
128
|
-
best = candidate
|
|
129
|
-
bestCoords = coords
|
|
130
|
-
bestOverflow = overflow
|
|
131
|
-
bestScore = score
|
|
107
|
+
best = candidate;
|
|
108
|
+
bestCoords = coords;
|
|
109
|
+
bestOverflow = overflow;
|
|
110
|
+
bestScore = score;
|
|
132
111
|
}
|
|
133
112
|
}
|
|
134
|
-
|
|
135
113
|
const x = clamp(
|
|
136
114
|
bestCoords.x,
|
|
137
115
|
VIEWPORT_PADDING,
|
|
138
|
-
Math.max(VIEWPORT_PADDING, window.innerWidth - VIEWPORT_PADDING - panelRect.width)
|
|
139
|
-
)
|
|
116
|
+
Math.max(VIEWPORT_PADDING, window.innerWidth - VIEWPORT_PADDING - panelRect.width)
|
|
117
|
+
);
|
|
140
118
|
const y = clamp(
|
|
141
119
|
bestCoords.y,
|
|
142
120
|
VIEWPORT_PADDING,
|
|
143
|
-
Math.max(VIEWPORT_PADDING, window.innerHeight - VIEWPORT_PADDING - panelRect.height)
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
placement.value = best
|
|
121
|
+
Math.max(VIEWPORT_PADDING, window.innerHeight - VIEWPORT_PADDING - panelRect.height)
|
|
122
|
+
);
|
|
123
|
+
placement.value = best;
|
|
147
124
|
style.value = {
|
|
148
125
|
left: `${Math.round(x)}px`,
|
|
149
126
|
top: `${Math.round(y)}px`,
|
|
150
|
-
visibility:
|
|
151
|
-
}
|
|
127
|
+
visibility: "visible"
|
|
128
|
+
};
|
|
152
129
|
}
|
|
153
|
-
|
|
154
130
|
function schedulePositionUpdate() {
|
|
155
131
|
if (rafId != null)
|
|
156
|
-
return
|
|
132
|
+
return;
|
|
157
133
|
rafId = window.requestAnimationFrame(() => {
|
|
158
|
-
rafId = null
|
|
159
|
-
applyPosition()
|
|
160
|
-
})
|
|
134
|
+
rafId = null;
|
|
135
|
+
applyPosition();
|
|
136
|
+
});
|
|
161
137
|
}
|
|
162
|
-
|
|
163
|
-
function onDocumentPointerDown(e: PointerEvent) {
|
|
138
|
+
function onDocumentPointerDown(e) {
|
|
164
139
|
if (!props.open)
|
|
165
|
-
return
|
|
166
|
-
const target = e.target
|
|
140
|
+
return;
|
|
141
|
+
const target = e.target;
|
|
167
142
|
if (!target)
|
|
168
|
-
return
|
|
143
|
+
return;
|
|
169
144
|
if (panelEl.value?.contains(target))
|
|
170
|
-
return
|
|
145
|
+
return;
|
|
171
146
|
if (props.anchorEl?.contains(target))
|
|
172
|
-
return
|
|
173
|
-
emit(
|
|
147
|
+
return;
|
|
148
|
+
emit("close");
|
|
174
149
|
}
|
|
175
|
-
|
|
176
150
|
function addGlobalListeners() {
|
|
177
|
-
window.addEventListener(
|
|
178
|
-
window.addEventListener(
|
|
179
|
-
document.addEventListener(
|
|
151
|
+
window.addEventListener("resize", schedulePositionUpdate);
|
|
152
|
+
window.addEventListener("scroll", schedulePositionUpdate, true);
|
|
153
|
+
document.addEventListener("pointerdown", onDocumentPointerDown, true);
|
|
180
154
|
}
|
|
181
|
-
|
|
182
155
|
function removeGlobalListeners() {
|
|
183
|
-
window.removeEventListener(
|
|
184
|
-
window.removeEventListener(
|
|
185
|
-
document.removeEventListener(
|
|
156
|
+
window.removeEventListener("resize", schedulePositionUpdate);
|
|
157
|
+
window.removeEventListener("scroll", schedulePositionUpdate, true);
|
|
158
|
+
document.removeEventListener("pointerdown", onDocumentPointerDown, true);
|
|
186
159
|
}
|
|
187
|
-
|
|
188
160
|
watch(
|
|
189
161
|
() => props.open,
|
|
190
162
|
async (isOpen) => {
|
|
191
163
|
if (isOpen) {
|
|
192
|
-
await nextTick()
|
|
193
|
-
applyPosition()
|
|
194
|
-
addGlobalListeners()
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
style.value = { left: '-9999px', top: '-9999px', visibility: 'hidden' }
|
|
164
|
+
await nextTick();
|
|
165
|
+
applyPosition();
|
|
166
|
+
addGlobalListeners();
|
|
167
|
+
} else {
|
|
168
|
+
removeGlobalListeners();
|
|
169
|
+
style.value = { left: "-9999px", top: "-9999px", visibility: "hidden" };
|
|
199
170
|
}
|
|
200
171
|
},
|
|
201
|
-
{ immediate: true }
|
|
202
|
-
)
|
|
203
|
-
|
|
172
|
+
{ immediate: true }
|
|
173
|
+
);
|
|
204
174
|
watch(
|
|
205
175
|
() => props.anchorEl,
|
|
206
176
|
async () => {
|
|
207
177
|
if (!props.open)
|
|
208
|
-
return
|
|
209
|
-
await nextTick()
|
|
210
|
-
applyPosition()
|
|
211
|
-
}
|
|
212
|
-
)
|
|
213
|
-
|
|
178
|
+
return;
|
|
179
|
+
await nextTick();
|
|
180
|
+
applyPosition();
|
|
181
|
+
}
|
|
182
|
+
);
|
|
214
183
|
onBeforeUnmount(() => {
|
|
215
|
-
removeGlobalListeners()
|
|
184
|
+
removeGlobalListeners();
|
|
216
185
|
if (rafId != null) {
|
|
217
|
-
window.cancelAnimationFrame(rafId)
|
|
218
|
-
rafId = null
|
|
186
|
+
window.cancelAnimationFrame(rafId);
|
|
187
|
+
rafId = null;
|
|
219
188
|
}
|
|
220
|
-
})
|
|
189
|
+
});
|
|
221
190
|
</script>
|
|
222
191
|
|
|
223
192
|
<template>
|
|
@@ -1,15 +1,9 @@
|
|
|
1
|
-
<script setup
|
|
2
|
-
|
|
3
|
-
variant
|
|
4
|
-
disabled
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
disabled: false,
|
|
8
|
-
})
|
|
9
|
-
|
|
10
|
-
defineEmits<{
|
|
11
|
-
click: [event: MouseEvent]
|
|
12
|
-
}>()
|
|
1
|
+
<script setup>
|
|
2
|
+
defineProps({
|
|
3
|
+
variant: { type: String, required: false, default: "primary" },
|
|
4
|
+
disabled: { type: Boolean, required: false, default: false }
|
|
5
|
+
});
|
|
6
|
+
defineEmits(["click"]);
|
|
13
7
|
</script>
|
|
14
8
|
|
|
15
9
|
<template>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
<script setup
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
<script setup>
|
|
2
|
+
import { icons } from "../icons";
|
|
3
|
+
defineProps({
|
|
4
|
+
name: { type: null, required: true }
|
|
5
|
+
});
|
|
6
6
|
</script>
|
|
7
7
|
|
|
8
8
|
<template>
|
|
@@ -1,31 +1,23 @@
|
|
|
1
|
-
<script setup
|
|
2
|
-
import { computed } from
|
|
3
|
-
import { vaTooltipDirective } from
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
disabled: false,
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
defineEmits<{
|
|
16
|
-
click: [event: MouseEvent]
|
|
17
|
-
}>()
|
|
18
|
-
|
|
19
|
-
const vVaTooltip = vaTooltipDirective
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from "vue-demi";
|
|
3
|
+
import { vaTooltipDirective } from "../directives/vaTooltip";
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
active: { type: Boolean, required: false, default: false },
|
|
6
|
+
disabled: { type: Boolean, required: false, default: false },
|
|
7
|
+
title: { type: String, required: false },
|
|
8
|
+
shortcut: { type: String, required: false }
|
|
9
|
+
});
|
|
10
|
+
defineEmits(["click"]);
|
|
11
|
+
const vVaTooltip = vaTooltipDirective;
|
|
20
12
|
const tooltipValue = computed(() => {
|
|
21
13
|
if (!props.title)
|
|
22
|
-
return null
|
|
14
|
+
return null;
|
|
23
15
|
return {
|
|
24
16
|
text: props.title,
|
|
25
17
|
shortcut: props.shortcut,
|
|
26
|
-
disabled: props.disabled
|
|
27
|
-
}
|
|
28
|
-
})
|
|
18
|
+
disabled: props.disabled
|
|
19
|
+
};
|
|
20
|
+
});
|
|
29
21
|
</script>
|
|
30
22
|
|
|
31
23
|
<template>
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
<script setup
|
|
2
|
-
const props = defineProps
|
|
3
|
-
modelValue:
|
|
4
|
-
ariaLabel
|
|
5
|
-
}
|
|
6
|
-
const emit = defineEmits
|
|
7
|
-
|
|
1
|
+
<script setup>
|
|
2
|
+
const props = defineProps({
|
|
3
|
+
modelValue: { type: Boolean, required: true },
|
|
4
|
+
ariaLabel: { type: String, required: false }
|
|
5
|
+
});
|
|
6
|
+
const emit = defineEmits(["update:model-value"]);
|
|
8
7
|
function toggle() {
|
|
9
|
-
emit(
|
|
8
|
+
emit("update:model-value", !props.modelValue);
|
|
10
9
|
}
|
|
11
10
|
</script>
|
|
12
11
|
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentation-vue",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Visual feedback tool for AI coding agents — Vue 2.7 & 3",
|
|
5
5
|
"author": "Dorian Becker",
|
|
6
6
|
"license": "PolyForm-Shield-1.0.0",
|
|
7
7
|
"homepage": "https://github.com/Blaked84/agentation-vue",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "https://github.com/Blaked84/agentation-vue.git",
|
|
10
|
+
"url": "git+https://github.com/Blaked84/agentation-vue.git",
|
|
11
11
|
"directory": "packages/agentation-vue"
|
|
12
12
|
},
|
|
13
13
|
"keywords": [
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"unbuild": "^3.0.0",
|
|
57
|
-
"vue": "^3.5.0"
|
|
57
|
+
"vue": "^3.5.0",
|
|
58
|
+
"vue-sfc-transformer": "^0.1.17"
|
|
58
59
|
}
|
|
59
60
|
}
|