@keplar-404/react-timeline-editor 1.0.6
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/components/control_area/index.d.ts +0 -0
- package/dist/components/cursor/cursor.d.ts +20 -0
- package/dist/components/cut-overlay/CutOverlay.d.ts +202 -0
- package/dist/components/edit_area/cross_row_drag.d.ts +50 -0
- package/dist/components/edit_area/drag_lines.d.ts +11 -0
- package/dist/components/edit_area/drag_preview.d.ts +14 -0
- package/dist/components/edit_area/drag_utils.d.ts +39 -0
- package/dist/components/edit_area/edit_action.d.ts +19 -0
- package/dist/components/edit_area/edit_area.d.ts +56 -0
- package/dist/components/edit_area/edit_row.d.ts +27 -0
- package/dist/components/edit_area/hooks/use_drag_line.d.ts +33 -0
- package/dist/components/edit_area/insertion_line.d.ts +12 -0
- package/dist/components/loop-zone/LoopZoneOverlay.d.ts +243 -0
- package/dist/components/row_rnd/hooks/useAutoScroll.d.ts +7 -0
- package/dist/components/row_rnd/interactable.d.ts +14 -0
- package/dist/components/row_rnd/row_rnd.d.ts +3 -0
- package/dist/components/row_rnd/row_rnd_interface.d.ts +47 -0
- package/dist/components/time_area/time_area.d.ts +17 -0
- package/dist/components/timeline.d.ts +3 -0
- package/dist/components/transport/TransportBar.d.ts +132 -0
- package/dist/components/transport/useTimelinePlayer.d.ts +164 -0
- package/dist/index.cjs.js +13 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.es.js +10102 -0
- package/dist/index.umd.js +13 -0
- package/dist/interface/common_prop.d.ts +12 -0
- package/dist/interface/const.d.ts +28 -0
- package/dist/interface/timeline.d.ts +342 -0
- package/dist/react-timeline-editor.css +1 -0
- package/dist/utils/check_props.d.ts +2 -0
- package/dist/utils/deal_class_prefix.d.ts +1 -0
- package/dist/utils/deal_data.d.ts +58 -0
- package/dist/utils/logger.d.ts +132 -0
- package/package.json +70 -0
- package/src/components/control_area/index.tsx +1 -0
- package/src/components/cursor/cursor.css +26 -0
- package/src/components/cursor/cursor.tsx +105 -0
- package/src/components/cut-overlay/CutOverlay.css +68 -0
- package/src/components/cut-overlay/CutOverlay.tsx +491 -0
- package/src/components/edit_area/cross_row_drag.tsx +174 -0
- package/src/components/edit_area/drag_lines.css +13 -0
- package/src/components/edit_area/drag_lines.tsx +31 -0
- package/src/components/edit_area/drag_preview.tsx +50 -0
- package/src/components/edit_area/drag_utils.ts +77 -0
- package/src/components/edit_area/edit_action.css +56 -0
- package/src/components/edit_area/edit_action.tsx +362 -0
- package/src/components/edit_area/edit_area.css +24 -0
- package/src/components/edit_area/edit_area.tsx +606 -0
- package/src/components/edit_area/edit_row.css +78 -0
- package/src/components/edit_area/edit_row.tsx +128 -0
- package/src/components/edit_area/hooks/use_drag_line.ts +93 -0
- package/src/components/edit_area/insertion_line.tsx +39 -0
- package/src/components/loop-zone/LoopZoneOverlay.css +65 -0
- package/src/components/loop-zone/LoopZoneOverlay.tsx +461 -0
- package/src/components/row_rnd/hooks/useAutoScroll.ts +81 -0
- package/src/components/row_rnd/interactable.tsx +55 -0
- package/src/components/row_rnd/row_rnd.tsx +365 -0
- package/src/components/row_rnd/row_rnd_interface.ts +59 -0
- package/src/components/time_area/time_area.css +35 -0
- package/src/components/time_area/time_area.tsx +93 -0
- package/src/components/timeline.css +12 -0
- package/src/components/timeline.tsx +227 -0
- package/src/components/transport/TransportBar.css +171 -0
- package/src/components/transport/TransportBar.tsx +322 -0
- package/src/components/transport/useTimelinePlayer.ts +319 -0
- package/src/index.tsx +17 -0
- package/src/interface/common_prop.ts +13 -0
- package/src/interface/const.ts +32 -0
- package/src/interface/timeline.ts +329 -0
- package/src/utils/check_props.ts +77 -0
- package/src/utils/deal_class_prefix.ts +6 -0
- package/src/utils/deal_data.ts +159 -0
- package/src/utils/logger.ts +239 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import React, { createContext, useContext, useState, useCallback, useRef } from 'react';
|
|
2
|
+
import { TimelineAction, TimelineRow } from '@keplar-404/timeline-engine';
|
|
3
|
+
|
|
4
|
+
// ─────────────────────────────────────────────
|
|
5
|
+
// Types
|
|
6
|
+
// ─────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
export interface CrossRowDragState {
|
|
9
|
+
isDragging: boolean;
|
|
10
|
+
/** The action currently being dragged across rows */
|
|
11
|
+
action: TimelineAction | null;
|
|
12
|
+
/** The source row (where the action came from) */
|
|
13
|
+
sourceRow: TimelineRow | null;
|
|
14
|
+
/** Visual width of the ghost (px) */
|
|
15
|
+
ghostWidth: number;
|
|
16
|
+
/** Visual height of the ghost (px) */
|
|
17
|
+
ghostHeight: number;
|
|
18
|
+
/** Current cursor X (client) */
|
|
19
|
+
cursorX: number;
|
|
20
|
+
/** Current cursor Y (client) */
|
|
21
|
+
cursorY: number;
|
|
22
|
+
/** Offset from the left edge of the block where the user grabbed it */
|
|
23
|
+
grabOffsetX: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface CrossRowDragAPI {
|
|
27
|
+
state: CrossRowDragState;
|
|
28
|
+
startCrossRowDrag: (params: {
|
|
29
|
+
action: TimelineAction;
|
|
30
|
+
sourceRow: TimelineRow;
|
|
31
|
+
ghostWidth: number;
|
|
32
|
+
ghostHeight: number;
|
|
33
|
+
grabOffsetX: number;
|
|
34
|
+
initialX: number;
|
|
35
|
+
initialY: number;
|
|
36
|
+
}) => void;
|
|
37
|
+
endCrossRowDrag: () => void;
|
|
38
|
+
/** Called by the EditArea when mouse is released to commit the move */
|
|
39
|
+
onCommit: (
|
|
40
|
+
action: TimelineAction,
|
|
41
|
+
sourceRow: TimelineRow,
|
|
42
|
+
targetRow: TimelineRow,
|
|
43
|
+
newStart: number,
|
|
44
|
+
newEnd: number,
|
|
45
|
+
) => void;
|
|
46
|
+
setOnCommit: (fn: CrossRowDragAPI['onCommit']) => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const initialState: CrossRowDragState = {
|
|
50
|
+
isDragging: false,
|
|
51
|
+
action: null,
|
|
52
|
+
sourceRow: null,
|
|
53
|
+
ghostWidth: 0,
|
|
54
|
+
ghostHeight: 0,
|
|
55
|
+
cursorX: 0,
|
|
56
|
+
cursorY: 0,
|
|
57
|
+
grabOffsetX: 0,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// ─────────────────────────────────────────────
|
|
61
|
+
// Context
|
|
62
|
+
// ─────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
const CrossRowDragContext = createContext<CrossRowDragAPI | null>(null);
|
|
65
|
+
|
|
66
|
+
export const useCrossRowDrag = (): CrossRowDragAPI => {
|
|
67
|
+
const ctx = useContext(CrossRowDragContext);
|
|
68
|
+
if (!ctx) throw new Error('useCrossRowDrag must be used inside CrossRowDragProvider');
|
|
69
|
+
return ctx;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// ─────────────────────────────────────────────
|
|
73
|
+
// Provider
|
|
74
|
+
// ─────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
export const CrossRowDragProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
77
|
+
const [state, setState] = useState<CrossRowDragState>(initialState);
|
|
78
|
+
const onCommitRef = useRef<CrossRowDragAPI['onCommit']>(() => {});
|
|
79
|
+
|
|
80
|
+
const startCrossRowDrag: CrossRowDragAPI['startCrossRowDrag'] = useCallback((params) => {
|
|
81
|
+
setState({
|
|
82
|
+
isDragging: true,
|
|
83
|
+
action: params.action,
|
|
84
|
+
sourceRow: params.sourceRow,
|
|
85
|
+
ghostWidth: params.ghostWidth,
|
|
86
|
+
ghostHeight: params.ghostHeight,
|
|
87
|
+
cursorX: params.initialX,
|
|
88
|
+
cursorY: params.initialY,
|
|
89
|
+
grabOffsetX: params.grabOffsetX,
|
|
90
|
+
});
|
|
91
|
+
}, []);
|
|
92
|
+
|
|
93
|
+
const endCrossRowDrag: CrossRowDragAPI['endCrossRowDrag'] = useCallback(() => {
|
|
94
|
+
setState(initialState);
|
|
95
|
+
}, []);
|
|
96
|
+
|
|
97
|
+
const setOnCommit: CrossRowDragAPI['setOnCommit'] = useCallback((fn) => {
|
|
98
|
+
onCommitRef.current = fn;
|
|
99
|
+
}, []);
|
|
100
|
+
|
|
101
|
+
const onCommit: CrossRowDragAPI['onCommit'] = useCallback(
|
|
102
|
+
(action, sourceRow, targetRow, newStart, newEnd) => {
|
|
103
|
+
onCommitRef.current(action, sourceRow, targetRow, newStart, newEnd);
|
|
104
|
+
},
|
|
105
|
+
[],
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<CrossRowDragContext.Provider
|
|
110
|
+
value={{ state, startCrossRowDrag, endCrossRowDrag, onCommit, setOnCommit }}
|
|
111
|
+
>
|
|
112
|
+
{children}
|
|
113
|
+
</CrossRowDragContext.Provider>
|
|
114
|
+
);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// ─────────────────────────────────────────────
|
|
118
|
+
// Ghost Element
|
|
119
|
+
// ─────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
interface GhostProps {
|
|
122
|
+
state: CrossRowDragState;
|
|
123
|
+
enableGhostPreview: boolean;
|
|
124
|
+
/** Optional custom render function; receives the dragged action + source row */
|
|
125
|
+
getGhostPreview?: (params: { action: TimelineAction; row: TimelineRow }) => React.ReactNode;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export const CrossRowGhost: React.FC<GhostProps> = ({
|
|
129
|
+
state,
|
|
130
|
+
enableGhostPreview,
|
|
131
|
+
getGhostPreview,
|
|
132
|
+
}) => {
|
|
133
|
+
if (!state.isDragging || !enableGhostPreview) return null;
|
|
134
|
+
if (!state.action || !state.sourceRow) return null;
|
|
135
|
+
|
|
136
|
+
const left = state.cursorX - state.grabOffsetX;
|
|
137
|
+
const top = state.cursorY - state.ghostHeight / 2;
|
|
138
|
+
|
|
139
|
+
// Render custom preview if provided, otherwise use default glowing box
|
|
140
|
+
const customContent = getGhostPreview
|
|
141
|
+
? getGhostPreview({ action: state.action, row: state.sourceRow })
|
|
142
|
+
: null;
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<div
|
|
146
|
+
style={{
|
|
147
|
+
position: 'fixed',
|
|
148
|
+
left,
|
|
149
|
+
top,
|
|
150
|
+
width: state.ghostWidth,
|
|
151
|
+
height: state.ghostHeight,
|
|
152
|
+
pointerEvents: 'none',
|
|
153
|
+
zIndex: 9999,
|
|
154
|
+
transition: 'none',
|
|
155
|
+
// Default appearance only when no custom content provided
|
|
156
|
+
...(customContent == null && {
|
|
157
|
+
background: 'rgba(99, 179, 237, 0.55)',
|
|
158
|
+
border: '2px solid rgba(99, 179, 237, 0.9)',
|
|
159
|
+
borderRadius: 4,
|
|
160
|
+
boxShadow: '0 0 16px rgba(99,179,237,0.7), 0 0 4px rgba(99,179,237,0.9)',
|
|
161
|
+
backdropFilter: 'blur(2px)',
|
|
162
|
+
}),
|
|
163
|
+
// When custom: just a transparent, sized container
|
|
164
|
+
...(customContent != null && {
|
|
165
|
+
overflow: 'hidden',
|
|
166
|
+
opacity: 0.85,
|
|
167
|
+
}),
|
|
168
|
+
}}
|
|
169
|
+
>
|
|
170
|
+
{customContent}
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
173
|
+
};
|
|
174
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React, { FC, useEffect, useState } from "react";
|
|
2
|
+
import { prefix } from "../../utils/deal_class_prefix";
|
|
3
|
+
import './drag_lines.css';
|
|
4
|
+
|
|
5
|
+
export interface DragLineData {
|
|
6
|
+
isMoving: boolean;
|
|
7
|
+
movePositions: number[];
|
|
8
|
+
assistPositions: number[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type DragLineProps = DragLineData & {scrollLeft: number};
|
|
12
|
+
|
|
13
|
+
/** Drag auxiliary lines */
|
|
14
|
+
export const DragLines: FC<DragLineProps> = ({
|
|
15
|
+
isMoving,
|
|
16
|
+
movePositions = [],
|
|
17
|
+
assistPositions = [],
|
|
18
|
+
scrollLeft,
|
|
19
|
+
}) => {
|
|
20
|
+
return(
|
|
21
|
+
<div className={prefix('drag-line-container')}>
|
|
22
|
+
{
|
|
23
|
+
isMoving && movePositions.filter(item => assistPositions.includes(item)).map(((linePos, index) => {
|
|
24
|
+
return (
|
|
25
|
+
<div key={index} className={prefix('drag-line')} style={{left: linePos - scrollLeft}} />
|
|
26
|
+
)
|
|
27
|
+
}))
|
|
28
|
+
}
|
|
29
|
+
</div>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React, { FC } from 'react';
|
|
2
|
+
import { prefix } from '../../utils/deal_class_prefix';
|
|
3
|
+
|
|
4
|
+
interface DragPreviewProps {
|
|
5
|
+
/** Top position of the preview element */
|
|
6
|
+
top: number;
|
|
7
|
+
/** Height of the preview element */
|
|
8
|
+
height: number;
|
|
9
|
+
/** Whether the preview element is visible */
|
|
10
|
+
visible: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Drag preview component - displays a preview of the row being dragged
|
|
15
|
+
*/
|
|
16
|
+
export const DragPreview: FC<DragPreviewProps> = ({ top, height, visible }) => {
|
|
17
|
+
if (!visible) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div
|
|
23
|
+
className={prefix('edit-area-drag-preview')}
|
|
24
|
+
style={{
|
|
25
|
+
position: 'absolute',
|
|
26
|
+
left: 0,
|
|
27
|
+
right: 0,
|
|
28
|
+
top,
|
|
29
|
+
height,
|
|
30
|
+
background: 'rgba(74, 144, 226, 0.3)',
|
|
31
|
+
border: '2px dashed #4a90e2',
|
|
32
|
+
borderRadius: '4px',
|
|
33
|
+
zIndex: 1001,
|
|
34
|
+
pointerEvents: 'none',
|
|
35
|
+
opacity: 0.8,
|
|
36
|
+
}}
|
|
37
|
+
>
|
|
38
|
+
<div
|
|
39
|
+
style={{
|
|
40
|
+
padding: '8px 16px',
|
|
41
|
+
color: '#4a90e2',
|
|
42
|
+
fontSize: '12px',
|
|
43
|
+
fontWeight: 'bold',
|
|
44
|
+
}}
|
|
45
|
+
>
|
|
46
|
+
Dragging...
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { TimelineRow } from '@keplar-404/timeline-engine';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Calculate the accumulated height of rows
|
|
5
|
+
* @param editorData Editor data
|
|
6
|
+
* @param rowIndex Target row index
|
|
7
|
+
* @param defaultRowHeight Default row height
|
|
8
|
+
* @returns Accumulated height
|
|
9
|
+
*/
|
|
10
|
+
export const calculateRowAccumulatedHeight = (
|
|
11
|
+
editorData: TimelineRow[],
|
|
12
|
+
rowIndex: number,
|
|
13
|
+
defaultRowHeight: number
|
|
14
|
+
): number => {
|
|
15
|
+
let accumulatedHeight = 0;
|
|
16
|
+
for (let i = 0; i < rowIndex; i++) {
|
|
17
|
+
accumulatedHeight += editorData[i]?.rowHeight || defaultRowHeight;
|
|
18
|
+
}
|
|
19
|
+
return accumulatedHeight;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Calculate total height of all rows
|
|
24
|
+
* @param editorData Editor data
|
|
25
|
+
* @param defaultRowHeight Default row height
|
|
26
|
+
* @returns Total height
|
|
27
|
+
*/
|
|
28
|
+
export const calculateTotalHeight = (
|
|
29
|
+
editorData: TimelineRow[],
|
|
30
|
+
defaultRowHeight: number
|
|
31
|
+
): number => {
|
|
32
|
+
return editorData.reduce((total, row) => total + (row?.rowHeight || defaultRowHeight), 0);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get an array of actual heights for each row
|
|
37
|
+
* @param editorData Editor data
|
|
38
|
+
* @param defaultRowHeight Default row height
|
|
39
|
+
* @returns Height array
|
|
40
|
+
*/
|
|
41
|
+
export const getRowHeights = (
|
|
42
|
+
editorData: TimelineRow[],
|
|
43
|
+
defaultRowHeight: number
|
|
44
|
+
): number[] => {
|
|
45
|
+
return editorData.map(row => row?.rowHeight || defaultRowHeight);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Calculate the position of the insertion line
|
|
50
|
+
* @param editorData Editor data
|
|
51
|
+
* @param targetIndex Target index
|
|
52
|
+
* @param defaultRowHeight Default row height
|
|
53
|
+
* @returns Insertion line top position
|
|
54
|
+
*/
|
|
55
|
+
export const calculateInsertionLineTop = (
|
|
56
|
+
editorData: TimelineRow[],
|
|
57
|
+
targetIndex: number,
|
|
58
|
+
defaultRowHeight: number
|
|
59
|
+
): number => {
|
|
60
|
+
return calculateRowAccumulatedHeight(editorData, targetIndex, defaultRowHeight);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Validate whether the drag target index is valid
|
|
65
|
+
* @param targetIndex Target index
|
|
66
|
+
* @param draggedIndex Index of the row being dragged
|
|
67
|
+
* @param totalRows Total number of rows
|
|
68
|
+
* @returns Whether it is valid
|
|
69
|
+
*/
|
|
70
|
+
export const isValidDragTarget = (
|
|
71
|
+
targetIndex: number,
|
|
72
|
+
draggedIndex: number,
|
|
73
|
+
totalRows: number
|
|
74
|
+
): boolean => {
|
|
75
|
+
// Allow dragging to the last row (targetIndex = totalRows)
|
|
76
|
+
return targetIndex >= 0 && targetIndex <= totalRows && targetIndex !== draggedIndex;
|
|
77
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
.timeline-editor-action {
|
|
2
|
+
position: absolute;
|
|
3
|
+
left: 0;
|
|
4
|
+
top: 0;
|
|
5
|
+
background-color: #2f3134;
|
|
6
|
+
cursor: default;
|
|
7
|
+
transition: box-shadow 0.15s ease;
|
|
8
|
+
/** Glow effect when the block is in an active drag state (cross-row) */
|
|
9
|
+
}
|
|
10
|
+
.timeline-editor-action.action-cross-row-dragging {
|
|
11
|
+
box-shadow: 0 0 0 2px rgba(99, 179, 237, 0.8), 0 0 16px rgba(99, 179, 237, 0.6), 0 0 32px rgba(99, 179, 237, 0.3);
|
|
12
|
+
opacity: 0.75;
|
|
13
|
+
z-index: 10;
|
|
14
|
+
}
|
|
15
|
+
.timeline-editor-action.action-movable {
|
|
16
|
+
cursor: move;
|
|
17
|
+
}
|
|
18
|
+
.timeline-editor-action .timeline-editor-action-left-stretch,
|
|
19
|
+
.timeline-editor-action .timeline-editor-action-right-stretch {
|
|
20
|
+
position: absolute;
|
|
21
|
+
top: 0;
|
|
22
|
+
width: 10px;
|
|
23
|
+
border-radius: 4px;
|
|
24
|
+
height: 100%;
|
|
25
|
+
overflow: hidden;
|
|
26
|
+
}
|
|
27
|
+
.timeline-editor-action .timeline-editor-action-left-stretch::after,
|
|
28
|
+
.timeline-editor-action .timeline-editor-action-right-stretch::after {
|
|
29
|
+
position: absolute;
|
|
30
|
+
top: 0;
|
|
31
|
+
bottom: 0;
|
|
32
|
+
margin: auto;
|
|
33
|
+
border-radius: 4px;
|
|
34
|
+
border-top: 28px solid transparent;
|
|
35
|
+
border-bottom: 28px solid transparent;
|
|
36
|
+
}
|
|
37
|
+
.timeline-editor-action .timeline-editor-action-left-stretch {
|
|
38
|
+
left: 0;
|
|
39
|
+
cursor: w-resize;
|
|
40
|
+
}
|
|
41
|
+
.timeline-editor-action .timeline-editor-action-left-stretch::after {
|
|
42
|
+
left: 0;
|
|
43
|
+
content: '';
|
|
44
|
+
border-left: 7px solid rgba(255, 255, 255, 0.1);
|
|
45
|
+
border-right: 7px solid transparent;
|
|
46
|
+
}
|
|
47
|
+
.timeline-editor-action .timeline-editor-action-right-stretch {
|
|
48
|
+
right: 0;
|
|
49
|
+
cursor: e-resize;
|
|
50
|
+
}
|
|
51
|
+
.timeline-editor-action .timeline-editor-action-right-stretch::after {
|
|
52
|
+
right: 0;
|
|
53
|
+
content: '';
|
|
54
|
+
border-right: 7px solid rgba(255, 255, 255, 0.1);
|
|
55
|
+
border-left: 7px solid transparent;
|
|
56
|
+
}
|