hookehuyr

chore: 更新依赖版本并添加uuid库

更新了esbuild、rollup、vite及其相关插件的版本,并在package.json中添加了uuid库。同时,新增了多个API视图文件和相关逻辑处理文件。
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
20 "lodash-es": "^4.17.21", 20 "lodash-es": "^4.17.21",
21 "postcss": "^8.5.3", 21 "postcss": "^8.5.3",
22 "tailwindcss": "^4.0.12", 22 "tailwindcss": "^4.0.12",
23 + "uuid": "^11.1.0",
23 "vue": "^3.5.13", 24 "vue": "^3.5.13",
24 "vue-router": "^4.5.0" 25 "vue-router": "^4.5.0"
25 }, 26 },
......
1 /* 1 /*
2 * @Date: 2025-03-10 13:07:05 2 * @Date: 2025-03-10 13:07:05
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-03-12 23:35:54 4 + * @LastEditTime: 2025-03-17 15:19:41
5 * @FilePath: /logic-flow2/src/main.js 5 * @FilePath: /logic-flow2/src/main.js
6 * @Description: 文件描述 6 * @Description: 文件描述
7 */ 7 */
...@@ -16,13 +16,14 @@ import ElementPlus from 'element-plus' ...@@ -16,13 +16,14 @@ import ElementPlus from 'element-plus'
16 import 'element-plus/dist/index.css' 16 import 'element-plus/dist/index.css'
17 17
18 import LogicFlow from '@logicflow/core'; 18 import LogicFlow from '@logicflow/core';
19 -import { Menu, DndPanel, SelectionSelect, Control, InsertNodeInPolyline, Highlight } from "@logicflow/extension"; 19 +import { Menu, DndPanel, SelectionSelect, Control, InsertNodeInPolyline, Highlight, Label } from "@logicflow/extension";
20 LogicFlow.use(Menu) // 右键菜单 20 LogicFlow.use(Menu) // 右键菜单
21 LogicFlow.use(DndPanel) // 拖拽面板 21 LogicFlow.use(DndPanel) // 拖拽面板
22 LogicFlow.use(SelectionSelect) // 选中元素 22 LogicFlow.use(SelectionSelect) // 选中元素
23 // LogicFlow.use(Control) // 控制面板 23 // LogicFlow.use(Control) // 控制面板
24 LogicFlow.use(InsertNodeInPolyline) // 边上插入节点 24 LogicFlow.use(InsertNodeInPolyline) // 边上插入节点
25 // LogicFlow.use(Highlight) // 高亮 25 // LogicFlow.use(Highlight) // 高亮
26 +// LogicFlow.use(Label) // Label标签
26 27
27 const app = createApp(App) 28 const app = createApp(App)
28 app.use(ElementPlus) 29 app.use(ElementPlus)
......
1 /* 1 /*
2 * @Date: 2025-03-13 18:34:16 2 * @Date: 2025-03-13 18:34:16
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-03-16 00:21:53 4 + * @LastEditTime: 2025-03-19 00:34:12
5 * @FilePath: /logic-flow2/src/router/index.js 5 * @FilePath: /logic-flow2/src/router/index.js
6 * @Description: 文件描述 6 * @Description: 文件描述
7 */ 7 */
...@@ -125,6 +125,26 @@ const router = createRouter({ ...@@ -125,6 +125,26 @@ const router = createRouter({
125 name: 'api-graphModel', 125 name: 'api-graphModel',
126 component: () => import('../views/api/graphModel.vue') 126 component: () => import('../views/api/graphModel.vue')
127 }, 127 },
128 + {
129 + path: '/api-nodeModel',
130 + name: 'api-nodeModel',
131 + component: () => import('../views/api/nodeModel.vue')
132 + },
133 + {
134 + path: '/api-scalable-node',
135 + name: 'api-scalable-node',
136 + component: () => import('../views/api/scalable-node.vue')
137 + },
138 + {
139 + path: '/api-edge-model',
140 + name: 'api-edge-model',
141 + component: () => import('../views/api/edgeModel.vue')
142 + },
143 + {
144 + path: '/api-transform-model',
145 + name: 'api-transform-model',
146 + component: () => import('../views/api/transformModel.vue')
147 + },
128 ] 148 ]
129 }) 149 })
130 150
......
1 +/*
2 + * @Date: 2025-03-17 15:40:00
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-03-18 17:17:47
5 + * @FilePath: /logic-flow2/src/views/api/draggable-text-node.js
6 + * @Description: 可拖动文本的矩形节点
7 + */
8 +import { RectNode, RectNodeModel } from "@logicflow/core";
9 +import { v4 as uuidv4 } from "uuid";
10 +
11 +const useNodeBehavior = () => {
12 + const isTextDraggable = ref(true);
13 + const isTextEditable = ref(false);
14 +
15 + const behavior = {
16 + nodeStyle: ref({
17 + stroke: '#1E90FF',
18 + fill: '#F0F8FF',
19 + strokeWidth: 1
20 + }),
21 + selectedStyle: ref({
22 + stroke: '#ff7f0e',
23 + strokeWidth: 2
24 + }),
25 + textStyle: ref({
26 + cursor: 'move'
27 + })
28 + };
29 +
30 + return {
31 + isTextDraggable,
32 + isTextEditable,
33 + behavior
34 + };
35 +};
36 +
37 +class DraggableTextNodeModel extends RectNodeModel {
38 + initNodeData(data) {
39 + // 确保 data.x 和 data.y 有默认值
40 + const nodeX = data.x || 0;
41 + const nodeY = data.y || 0;
42 +
43 + // 处理文本数据
44 + if (!data.text || typeof data.text === "string") {
45 + data.text = {
46 + value: data.text || "",
47 + x: nodeX,
48 + y: nodeY + 80,
49 + };
50 + }
51 +
52 + super.initNodeData(data);
53 +
54 + const { behavior } = useNodeBehavior();
55 + this.behavior = behavior;
56 +
57 + this.text.draggable = true;
58 + this.text.editable = false;
59 + }
60 +
61 + getNodeStyle() {
62 + /**
63 + * - 合并基础样式
64 + * - 根据选中状态添加额外样式
65 + */
66 + return {
67 + ...this.behavior.nodeStyle.value,
68 + ...(this.isSelected ? this.behavior.selectedStyle.value : {})
69 + };
70 + }
71 +
72 + getTextStyle() {
73 + /**
74 + * - 扩展原有文本样式
75 + * - 添加自定义样式(如移动光标)
76 + */
77 + const style = super.getTextStyle();
78 + return {
79 + ...style,
80 + ...this.behavior.textStyle.value
81 + };
82 + }
83 +
84 + getAnchorStyle() {
85 + const style = super.getAnchorStyle();
86 + style.stroke = "rgb(24, 125, 255)";
87 + style.r = 3;
88 + style.hover.r = 8;
89 + style.hover.fill = "rgb(24, 125, 255)";
90 + style.hover.stroke = "rgb(24, 125, 255)";
91 + return style;
92 + }
93 +
94 + getAnchorLineStyle() {
95 + const style = super.getAnchorLineStyle();
96 + style.stroke = "rgb(24, 125, 255)";
97 + return style;
98 + }
99 +
100 + getOutlineStyle() {
101 + const style = super.getOutlineStyle();
102 + style.stroke = "red";
103 + style.hover.stroke = "yellow";
104 + return style;
105 + }
106 +
107 + createId() {
108 + return `custom-rect-${uuidv4()}`;
109 + }
110 +
111 + // 定义节点只有左右两个锚点. 锚点位置通过中心点和宽度算出来。
112 + getDefaultAnchor() {
113 + const { width, height, x, y, id } = this;
114 + return [
115 + {
116 + x: x - width / 2,
117 + y,
118 + name: 'left',
119 + id: `${id}_0`
120 + },
121 + {
122 + x: x + width / 2,
123 + y,
124 + name: 'right',
125 + id: `${id}_1`,
126 + // edgeAddable: false
127 + },
128 + ]
129 + }
130 +
131 + // getConnectedSourceRules(){
132 + // const rules = super.getConnectedSourceRules();
133 + // const getWayOnlyAsTarget = {
134 + // message: "结束节点只能连入,不能连出!",
135 + // validate: ( source, target, sourceAnchor, targetAnchor ) => {
136 + // let isValid = true;
137 + // if (source) {
138 + // isValid = false;
139 + // }
140 + // return isValid;
141 + // },
142 + // };
143 +
144 + // rules.push(getWayOnlyAsTarget);
145 + // return rules;
146 + // }
147 +
148 + // getConnectedTargetRules() {
149 + // const rules = super.getConnectedTargetRules();
150 + // const notAsTarget = {
151 + // message: "起始节点不能作为边的终点",
152 + // validate: () => false,
153 + // };
154 +
155 + // rules.push(notAsTarget);
156 + // return rules;
157 + // }
158 +
159 + isAllowMoveNode(deltaX, deltaY) {
160 + let newX = this.x + deltaX
161 + let newY = this.y + deltaY
162 + let isAllowMoveX = true
163 + let isAllowMoveY = true
164 + // 处理
165 + return {
166 + x: isAllowMoveX,
167 + y: isAllowMoveY,
168 + }
169 + }
170 +
171 + // isAllowConnectedAsSource(target, sourceAnchor, targetAnchor) {
172 + // // 根据节点类型判断是否允许连线
173 + // if (this.properties.nodeType === 'source') {
174 + // if (target.properties.nodeType === 'target') {
175 + // return false
176 + // }
177 + // return true
178 + // }
179 + // }
180 +
181 + // isAllowConnectedAsTarget(source, sourceAnchor, targetAnchor) {
182 + // // 根据节点类型判断是否允许连线
183 + // if (this.properties.nodeType ==='target') {
184 + // if (source.properties.nodeType === 'source') {
185 + // return false
186 + // }
187 + // return true
188 + // }
189 + // }
190 +}
191 +
192 +class DraggableTextNode extends RectNode {}
193 +
194 +export default {
195 + type: "custom-rect",
196 + view: DraggableTextNode,
197 + model: DraggableTextNodeModel,
198 +};
1 +<!--
2 + * @Date: 2025-03-10 16:52:35
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-03-18 23:51:14
5 + * @FilePath: /logic-flow2/src/views/api/edgeModel.vue
6 + * @Description: 拖拽面板
7 +-->
8 +<template>
9 + <div class="container">
10 + <div ref="container" class="flow-container"></div>
11 + </div>
12 +</template>
13 +
14 +<script setup>
15 +import LogicFlow from '@logicflow/core';
16 +import { PolylineEdge, PolylineEdgeModel } from '@logicflow/core';
17 +
18 +class CustomEdgeModel extends PolylineEdgeModel {
19 + getEdgeStyle() {
20 + const style = super.getEdgeStyle();
21 + style.stroke = "blue";
22 + style.strokeDasharray = "3 3";
23 + return style;
24 + }
25 +}
26 +
27 +const container = ref(null);
28 +let lf = null;
29 +
30 +onMounted(() => {
31 + lf = new LogicFlow({
32 + container: container.value,
33 + grid: true,
34 + });
35 +
36 + // 注册自定义边
37 + lf.register({
38 + type: 'custom-edge',
39 + view: PolylineEdge,
40 + model: CustomEdgeModel
41 + });
42 +
43 + lf.setDefaultEdgeType('custom-edge');
44 +
45 + lf.render({
46 + nodes: [
47 + { id: 'node1', type: 'rect', x: 200, y: 100 },
48 + { id: 'node2', type: 'circle', x: 400, y: 100 },
49 + ],
50 + edges: [{
51 + id: 'edge1',
52 + type: 'custom-edge',
53 + sourceNodeId: 'node1',
54 + targetNodeId: 'node2'
55 + }],
56 + });
57 +});
58 +</script>
59 +
60 +
61 +<style scoped>
62 +.container {
63 + width: 100vw;
64 + height: 100vh;
65 + display: flex;
66 + flex-direction: column;
67 +}
68 +
69 +.flow-container {
70 + flex: 1;
71 + width: 100%;
72 + height: 100%;
73 +}
74 +</style>
1 +<!--
2 + * @Date: 2025-03-10 16:52:35
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-03-18 17:18:57
5 + * @FilePath: /logic-flow2/src/views/api/nodeModel.vue
6 + * @Description: 拖拽面板
7 +-->
8 +<template>
9 + <div class="container">
10 + <div ref="container" class="flow-container"></div>
11 + </div>
12 +</template>
13 +
14 +<script setup>
15 +import LogicFlow from "@logicflow/core";
16 +import "@logicflow/extension/lib/style/index.css";
17 +import CustomNode from "./draggable-text-node";
18 +
19 +const container = ref(null);
20 +let lf = null;
21 +
22 +/**
23 + * 先必须初始化的时把配置项打开,之后再进行单独的数据设置
24 + * 比如你如果要文本移动,那么你必须先打开文本移动的配置项,然后再进行单独的数据设置
25 + */
26 +
27 +onMounted(() => {
28 + lf = new LogicFlow({
29 + container: container.value,
30 + grid: true,
31 + // plugins: [Label], // 引入 Label 插件
32 + nodeTextDraggable: true, // 开启节点文本拖拽
33 + });
34 +
35 + // 注册自定义节点
36 + lf.register(CustomNode);
37 +
38 + // 设置拖拽面板配置
39 + lf.setPatternItems([
40 + {
41 + type: "custom-rect",
42 + text: "自定义节点",
43 + label: "拖拽生成节点",
44 + className: "custom-node",
45 + },
46 + ]);
47 +
48 + // 监听节点创建事件,自定义ID规则
49 + lf.on("node:dnd-add", ({ data }) => {
50 + const prefix = "custom_node_";
51 + const timestamp = Date.now();
52 + data.id = `${prefix}${timestamp}`;
53 + console.log("新创建的节点ID:", data.id);
54 + });
55 +
56 + // 添加连线事件监听
57 + lf.on('edge:connect', ({ data }) => {
58 + console.log('连线成功', data);
59 + });
60 +
61 + lf.on('connection:not-allowed', (data) => {
62 + console.log('连线被阻止', data);
63 + });
64 +
65 + lf.render({
66 + nodes: [
67 + {
68 + id: "node1",
69 + type: "custom-rect",
70 + x: 200,
71 + y: 100,
72 + // text: {
73 + // x: 250,
74 + // y: 150,
75 + // value: 'Node 1',
76 + // },
77 + text: "Node 1",
78 + },
79 + {
80 + id: "node2",
81 + type: "circle",
82 + x: 400,
83 + y: 100,
84 + text: {
85 + x: 450,
86 + y: 150,
87 + value: "Node 2",
88 + draggable: false,
89 + },
90 + },
91 + ],
92 + edges: [{ id: "edge1", sourceNodeId: "node1", targetNodeId: "node2" }],
93 + });
94 +
95 + lf.translateCenter();
96 +
97 + lf.on("node:click", ({data}) => {
98 + lf.getNodeModelById(data.id).setProperties({
99 + disabled: !data.properties.disabled,
100 + scale: 2,
101 + });
102 + console.warn(lf.getGraphData());
103 +
104 + });
105 +
106 + const nodeModel = lf.getNodeModelById("node1");
107 + // const { anchors } = nodeModel;
108 + // nodeModel.setIsShowAnchor(true)
109 + console.warn("nodeModel", nodeModel.getConnectedTargetRules());
110 +
111 +
112 +});
113 +</script>
114 +
115 +<style scoped>
116 +.container {
117 + width: 100vw;
118 + height: 100vh;
119 + display: flex;
120 + flex-direction: column;
121 +}
122 +
123 +.flow-container {
124 + flex: 1;
125 + width: 100%;
126 + height: 100%;
127 +}
128 +</style>
1 +<!--
2 + * @Date: 2025-03-10 16:52:35
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-03-17 23:33:06
5 + * @FilePath: /logic-flow2/src/views/api/scalable-node.vue
6 + * @Description: 可缩放节点示例
7 +-->
8 +<template>
9 + <div class="container">
10 + <div ref="container" class="flow-container"></div>
11 + </div>
12 +</template>
13 +
14 +<script setup>
15 +import LogicFlow from '@logicflow/core';
16 +import ScalableRectNode from './scalable-rect-node';
17 +
18 +const container = ref(null);
19 +let lf = null;
20 +
21 +onMounted(() => {
22 + lf = new LogicFlow({
23 + container: container.value,
24 + grid: true,
25 + });
26 +
27 + // 注册自定义节点
28 + lf.register(ScalableRectNode);
29 +
30 + lf.render({
31 + nodes: [
32 + {
33 + id: 'node1',
34 + type: 'scalable-rect',
35 + x: 300,
36 + y: 200,
37 + text: '可缩放节点',
38 + properties: {
39 + scale: 1, // 初始缩放比例
40 + },
41 + },
42 + ],
43 + });
44 +
45 + lf.translateCenter();
46 +});
47 +</script>
48 +
49 +<style scoped>
50 +.container {
51 + width: 100vw;
52 + height: 100vh;
53 + display: flex;
54 + flex-direction: column;
55 +}
56 +
57 +.flow-container {
58 + flex: 1;
59 + width: 100%;
60 + height: 100%;
61 +}
62 +</style>
1 +/*
2 + * @Date: 2025-03-17 23:23:33
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-03-17 23:35:02
5 + * @FilePath: /logic-flow2/src/views/api/scalable-rect-node.js
6 + * @Description: 文件描述
7 + */
8 +import { RectResize } from "@logicflow/extension";
9 +
10 +class CustomNode extends RectResize.view {
11 +}
12 +
13 +class ScalableRectNode extends RectResize.model {
14 +
15 + initNodeData(data) {
16 + super.initNodeData(data);
17 + this.width = 80;
18 + this.height = 40;
19 + this.maxWidth = 300;
20 + this.maxHeight = 300;
21 + this.text.draggable = true;
22 + }
23 +
24 + // setAttributes() {
25 + // const size = this.properties.scale || 1;
26 + // this.width = 100 * size;
27 + // this.height = 80 * size;
28 + // }
29 +
30 + getDefaultAnchor() {
31 + const { width, height, x, y, id } = this;
32 + return [
33 + { x: x - width / 2, y, id: `${id}_0` },
34 + { x: x + width / 2, y, id: `${id}_1` },
35 + { x, y: y - height / 2, id: `${id}_2` },
36 + { x, y: y + height / 2, id: `${id}_3` },
37 + ];
38 + }
39 +}
40 +
41 +export default {
42 + type: 'scalable-rect',
43 + view: CustomNode,
44 + model: ScalableRectNode,
45 +};
1 +<!--
2 + * @Date: 2025-03-10 16:52:35
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-03-19 00:57:55
5 + * @FilePath: /logic-flow2/src/views/api/transformModel.vue
6 + * @Description: 拖拽面板
7 +-->
8 +<template>
9 + <div class="container">
10 + <div ref="container" class="flow-container"></div>
11 + <div class="control-panel">
12 + <button @click="zoomIn">放大</button>
13 + <button @click="zoomOut">缩小</button>
14 + <button @click="moveLeft">左移</button>
15 + <button @click="moveRight">右移</button>
16 + <button @click="centerView">居中</button>
17 + </div>
18 + </div>
19 +</template>
20 +
21 +<script setup>
22 +import LogicFlow from "@logicflow/core";
23 +
24 +const container = ref(null);
25 +let lf = null;
26 +
27 +// 缩放画布
28 +const zoomIn = () => {
29 + const { transformModel } = lf.graphModel;
30 + const currentZoom = transformModel.ZOOM;
31 + transformModel.zoom(currentZoom + 0.1);
32 +};
33 +
34 +const zoomOut = () => {
35 + const { transformModel } = lf.graphModel;
36 + const currentZoom = transformModel.ZOOM;
37 + transformModel.zoom(currentZoom - 0.1);
38 +};
39 +
40 +// 平移画布
41 +const moveLeft = () => {
42 + const { transformModel } = lf.graphModel;
43 + const [x, y] = transformModel.getTranslate();
44 + transformModel.translate(x - 50, y);
45 +};
46 +
47 +const moveRight = () => {
48 + const { transformModel } = lf.graphModel;
49 + const [x, y] = transformModel.getTranslate();
50 + transformModel.translate(x + 50, y);
51 +};
52 +
53 +// 居中显示
54 +const centerView = () => {
55 + const { transformModel } = lf;
56 + // 直接从 lf 实例获取节点
57 +
58 + const nodes = lf.graphModel.nodes;
59 + if (nodes.length === 0) return;
60 +
61 + // 计算所有节点的边界框
62 + const bounds = nodes.reduce((acc, node) => {
63 + const { x, y } = node;
64 + acc.minX = Math.min(acc.minX, x);
65 + acc.maxX = Math.max(acc.maxX, x);
66 + acc.minY = Math.min(acc.minY, y);
67 + acc.maxY = Math.max(acc.maxY, y);
68 + return acc;
69 + }, { minX: Infinity, maxX: -Infinity, minY: Infinity, maxY: -Infinity });
70 +
71 + // 计算中心点和范围
72 + const centerX = (bounds.minX + bounds.maxX) / 2;
73 + const centerY = (bounds.minY + bounds.maxY) / 2;
74 + const width = bounds.maxX - bounds.minX + 200; // 添加边距
75 + const height = bounds.maxY - bounds.minY + 100;
76 +
77 + // 居中显示
78 + transformModel.focusOn(centerX, centerY, width, height);
79 +};
80 +
81 +onMounted(() => {
82 + lf = new LogicFlow({
83 + container: container.value,
84 + grid: true,
85 + });
86 +
87 + // 监听点击画布功能
88 + lf.on("canvas:click", (e) => {
89 + console.log("Canvas clicked at:", e.x, e.y);
90 + });
91 +
92 + lf.on('node:click', (e) => {
93 + console.log(e);
94 + // 示例:HTML坐标转换为画布坐标
95 + console.warn(e);
96 +
97 +
98 + const htmlPoint = { x: 100, y: 100 };
99 + const { transformModel } = lf.graphModel;
100 + const canvasPoint = transformModel.HtmlPointToCanvasPoint(htmlPoint);
101 + console.log("Canvas coordinates:", canvasPoint);
102 + });
103 +
104 + lf.render({
105 + nodes: [
106 + { id: "node1", type: "rect", x: 200, y: 100 },
107 + { id: "node2", type: "circle", x: 400, y: 100 },
108 + ],
109 + edges: [{ id: "edge1", sourceNodeId: "node1", targetNodeId: "node2" }],
110 + });
111 +
112 + // 初始化时居中显示
113 + setTimeout(() => {
114 + const { transformModel } = lf;
115 + // 这里也需要修改
116 + const nodes = lf.graphModel.nodes;
117 + if (nodes.length === 0) return;
118 +
119 + const bounds = nodes.reduce((acc, node) => {
120 + const { x, y } = node;
121 + acc.minX = Math.min(acc.minX, x);
122 + acc.maxX = Math.max(acc.maxX, x);
123 + acc.minY = Math.min(acc.minY, y);
124 + acc.maxY = Math.max(acc.maxY, y);
125 + return acc;
126 + }, { minX: Infinity, maxX: -Infinity, minY: Infinity, maxY: -Infinity });
127 +
128 + const centerX = (bounds.minX + bounds.maxX) / 2;
129 + const centerY = (bounds.minY + bounds.maxY) / 2;
130 + const width = bounds.maxX - bounds.minX + 200;
131 + const height = bounds.maxY - bounds.minY + 100;
132 +
133 + // transformModel.focusOn(centerX, centerY, width, height);
134 + }, 0);
135 +
136 +
137 +});
138 +</script>
139 +
140 +<style scoped>
141 +.container {
142 + width: 100vw;
143 + height: 100vh;
144 + display: flex;
145 + flex-direction: column;
146 +}
147 +
148 +.flow-container {
149 + flex: 1;
150 + width: 100%;
151 + height: 100%;
152 +}
153 +
154 +.control-panel {
155 + position: fixed;
156 + top: 20px;
157 + left: 20px;
158 + display: flex;
159 + gap: 10px;
160 +}
161 +
162 +button {
163 + padding: 8px 16px;
164 + border: none;
165 + border-radius: 4px;
166 + background-color: #4a90e2;
167 + color: white;
168 + cursor: pointer;
169 +}
170 +
171 +button:hover {
172 + background-color: #357abd;
173 +}
174 +</style>
This diff is collapsed. Click to expand it.