api-ape 1.1.0 → 2.0.0
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/README.md +24 -11
- package/client/browser.js +7 -1
- package/client/connectSocket.js +35 -3
- package/example/Bun/README.md +74 -0
- package/example/Bun/api/message.ts +11 -0
- package/example/Bun/index.html +76 -0
- package/example/Bun/package.json +9 -0
- package/example/Bun/server.ts +59 -0
- package/example/Bun/styles.css +128 -0
- package/example/ExpressJs/README.md +5 -7
- package/example/ExpressJs/backend.js +23 -21
- package/example/NextJs/ape/client.js +3 -3
- package/example/NextJs/ape/onConnect.js +5 -5
- package/example/NextJs/package-lock.json +1353 -60
- package/example/NextJs/package.json +0 -1
- package/example/NextJs/pages/index.tsx +21 -10
- package/example/NextJs/server.js +7 -11
- package/example/README.md +51 -0
- package/example/Vite/README.md +68 -0
- package/example/Vite/ape/client.ts +66 -0
- package/example/Vite/ape/onConnect.ts +52 -0
- package/example/Vite/api/message.ts +57 -0
- package/example/Vite/index.html +16 -0
- package/example/Vite/package.json +19 -0
- package/example/Vite/server.ts +62 -0
- package/example/Vite/src/App.vue +170 -0
- package/example/Vite/src/components/Info.vue +352 -0
- package/example/Vite/src/main.ts +5 -0
- package/example/Vite/src/style.css +200 -0
- package/example/Vite/src/vite-env.d.ts +7 -0
- package/example/Vite/vite.config.ts +20 -0
- package/index.d.ts +31 -3
- package/package.json +1 -2
- package/server/index.js +10 -2
- package/server/lib/main.js +156 -61
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* 🦍 api-ape Vue Chat Example
|
|
4
|
+
*
|
|
5
|
+
* This component demonstrates how to use api-ape in a Vue application:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Client Initialization**: Connect to api-ape WebSocket server
|
|
8
|
+
* 2. **Proxy Pattern**: Use `client.sender` as a Proxy to call server functions
|
|
9
|
+
* 3. **Event Listeners**: Listen for server broadcasts using `setOnReciver`
|
|
10
|
+
* 4. **Promise-based Calls**: Server functions return Promises automatically
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { ref, onMounted } from 'vue'
|
|
14
|
+
import { getApeClient } from '../ape/client'
|
|
15
|
+
import Info from './components/Info.vue'
|
|
16
|
+
|
|
17
|
+
interface Message {
|
|
18
|
+
user: string
|
|
19
|
+
text: string
|
|
20
|
+
time: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Component state
|
|
24
|
+
const messages = ref<Message[]>([])
|
|
25
|
+
const input = ref('')
|
|
26
|
+
const username = ref('')
|
|
27
|
+
const joined = ref(false)
|
|
28
|
+
const userCount = ref(0)
|
|
29
|
+
const sending = ref(false)
|
|
30
|
+
const connectionState = ref<'disconnected' | 'connecting' | 'connected'>('connecting')
|
|
31
|
+
|
|
32
|
+
// Store the api-ape sender Proxy
|
|
33
|
+
let api: any = null
|
|
34
|
+
|
|
35
|
+
onMounted(async () => {
|
|
36
|
+
const client = await getApeClient()
|
|
37
|
+
if (!client) return
|
|
38
|
+
|
|
39
|
+
// Store the sender Proxy
|
|
40
|
+
api = client.sender
|
|
41
|
+
console.log('🦍 api-ape client connected')
|
|
42
|
+
|
|
43
|
+
// Subscribe to connection state changes
|
|
44
|
+
client.onConnectionChange((state: string) => {
|
|
45
|
+
connectionState.value = state as any
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
// Set up event listeners for server broadcasts
|
|
49
|
+
client.setOnReciver('init', ({ data }: { data: { history: Message[], users: number } }) => {
|
|
50
|
+
messages.value = data.history || []
|
|
51
|
+
userCount.value = data.users || 0
|
|
52
|
+
console.log('🦍 Initialized with', data.history?.length || 0, 'messages')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
client.setOnReciver('message', ({ data }: { data: { message: Message } }) => {
|
|
56
|
+
messages.value.push(data.message)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
client.setOnReciver('users', ({ data }: { data: { count: number } }) => {
|
|
60
|
+
userCount.value = data.count
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
async function sendMessage() {
|
|
65
|
+
if (!input.value.trim() || !api || sending.value) return
|
|
66
|
+
|
|
67
|
+
sending.value = true
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const response = await api.message({ user: username.value, text: input.value })
|
|
71
|
+
if (response?.message) {
|
|
72
|
+
messages.value.push(response.message)
|
|
73
|
+
}
|
|
74
|
+
} catch (err) {
|
|
75
|
+
console.error('Send failed:', err)
|
|
76
|
+
} finally {
|
|
77
|
+
sending.value = false
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
input.value = ''
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function handleJoin() {
|
|
84
|
+
if (username.value.trim()) {
|
|
85
|
+
joined.value = true
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function formatTime(time: string) {
|
|
90
|
+
return new Date(time).toLocaleTimeString()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function getConnectionStatus() {
|
|
94
|
+
if (connectionState.value === 'connected') {
|
|
95
|
+
if (userCount.value === 1) return '✅ Connected • Only You are online'
|
|
96
|
+
if (userCount.value > 1) return `✅ Connected • You + ${userCount.value - 1} are online`
|
|
97
|
+
return '✅ Connected'
|
|
98
|
+
}
|
|
99
|
+
if (connectionState.value === 'connecting') return '⏳ Connecting...'
|
|
100
|
+
return '❌ Disconnected'
|
|
101
|
+
}
|
|
102
|
+
</script>
|
|
103
|
+
|
|
104
|
+
<template>
|
|
105
|
+
<div class="container">
|
|
106
|
+
<main class="main">
|
|
107
|
+
<h1 class="title">
|
|
108
|
+
🦍 <span class="gradient">api-ape</span> Chat
|
|
109
|
+
</h1>
|
|
110
|
+
<p class="subtitle">{{ getConnectionStatus() }}</p>
|
|
111
|
+
|
|
112
|
+
<!-- Join Form -->
|
|
113
|
+
<form v-if="!joined" @submit.prevent="handleJoin" class="join-form">
|
|
114
|
+
<input
|
|
115
|
+
type="text"
|
|
116
|
+
placeholder="Enter your name..."
|
|
117
|
+
v-model="username"
|
|
118
|
+
class="input"
|
|
119
|
+
autofocus
|
|
120
|
+
/>
|
|
121
|
+
<button
|
|
122
|
+
type="submit"
|
|
123
|
+
class="button"
|
|
124
|
+
:disabled="connectionState !== 'connected'"
|
|
125
|
+
>
|
|
126
|
+
{{ connectionState === 'connected' ? 'Join Chat →' : 'Connecting...' }}
|
|
127
|
+
</button>
|
|
128
|
+
</form>
|
|
129
|
+
|
|
130
|
+
<!-- Chat Interface -->
|
|
131
|
+
<div v-else class="chat-container">
|
|
132
|
+
<div class="header">
|
|
133
|
+
<span>💬 {{ username }}</span>
|
|
134
|
+
<span class="user-count">🟢 {{ userCount }} online</span>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<div class="messages">
|
|
138
|
+
<p v-if="messages.length === 0" class="empty-state">
|
|
139
|
+
No messages yet. Say hi! 👋
|
|
140
|
+
</p>
|
|
141
|
+
<div
|
|
142
|
+
v-for="(msg, i) in messages"
|
|
143
|
+
:key="i"
|
|
144
|
+
:class="['message', msg.user === username ? 'my-message' : '']"
|
|
145
|
+
>
|
|
146
|
+
<strong class="username">{{ msg.user }}</strong>
|
|
147
|
+
<span>{{ msg.text }}</span>
|
|
148
|
+
<span class="time">{{ formatTime(msg.time) }}</span>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<form @submit.prevent="sendMessage" class="input-form">
|
|
153
|
+
<input
|
|
154
|
+
type="text"
|
|
155
|
+
placeholder="Type a message..."
|
|
156
|
+
v-model="input"
|
|
157
|
+
class="message-input"
|
|
158
|
+
:disabled="sending"
|
|
159
|
+
autofocus
|
|
160
|
+
/>
|
|
161
|
+
<button type="submit" class="send-button" :disabled="sending">
|
|
162
|
+
{{ sending ? '...' : 'Send' }}
|
|
163
|
+
</button>
|
|
164
|
+
</form>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<Info />
|
|
168
|
+
</main>
|
|
169
|
+
</div>
|
|
170
|
+
</template>
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Info component - explains how api-ape works
|
|
4
|
+
*/
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<template>
|
|
8
|
+
<div class="code-section">
|
|
9
|
+
<h3 class="code-title">📚 How api-ape Works</h3>
|
|
10
|
+
|
|
11
|
+
<div class="grid-container">
|
|
12
|
+
<div class="grid-layout">
|
|
13
|
+
<!-- Top Left: Key Concepts -->
|
|
14
|
+
<div>
|
|
15
|
+
<h4 class="section-heading">💡 Key Concepts</h4>
|
|
16
|
+
<pre class="code">• Proxy Pattern: api.message() → api/message.js
|
|
17
|
+
• Auto-wiring: Drop files in api/ folder, they become endpoints
|
|
18
|
+
• Promises: All calls return Promises automatically
|
|
19
|
+
• Broadcasts: Use this.broadcast() or this.broadcastOthers()
|
|
20
|
+
• Context: this.broadcast, this.hostId, this.req available in controllers
|
|
21
|
+
• Auto-reconnect: Client reconnects automatically on disconnect</pre>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<!-- Top Right: Data Flow -->
|
|
25
|
+
<div>
|
|
26
|
+
<h4 class="section-heading-large">🔄 Data Flow</h4>
|
|
27
|
+
<div class="data-flow-grid">
|
|
28
|
+
<!-- Column Headers -->
|
|
29
|
+
<div class="column-header-client">Client</div>
|
|
30
|
+
<div class="grid-cell"></div>
|
|
31
|
+
<div class="column-header-server">Server</div>
|
|
32
|
+
|
|
33
|
+
<!-- Step 1: Client sends -->
|
|
34
|
+
<div class="client-box-span3">api.message(data)</div>
|
|
35
|
+
<div class="arrow-container-row2">
|
|
36
|
+
<div class="arrow-line-send"></div>
|
|
37
|
+
<span class="arrow-label-blue">Send</span>
|
|
38
|
+
<div class="arrow-head-right"></div>
|
|
39
|
+
</div>
|
|
40
|
+
<div class="empty-grid-cell"></div>
|
|
41
|
+
|
|
42
|
+
<!-- Step 2: Server receives -->
|
|
43
|
+
<div class="empty-grid-cell-row3"></div>
|
|
44
|
+
<div class="arrow-container-row3">
|
|
45
|
+
<div class="arrow-head-left"></div>
|
|
46
|
+
<span class="arrow-label-green">Return</span>
|
|
47
|
+
<div class="arrow-line-return"></div>
|
|
48
|
+
</div>
|
|
49
|
+
<div class="server-box-span2">api/message.js</div>
|
|
50
|
+
|
|
51
|
+
<!-- Step 3: Server broadcasts -->
|
|
52
|
+
<div class="empty-grid-cell-row4"></div>
|
|
53
|
+
<div class="arrow-container-row4">
|
|
54
|
+
<div class="arrow-line-broadcast"></div>
|
|
55
|
+
<span class="arrow-label-green">Broadcast</span>
|
|
56
|
+
<div class="arrow-head-right"></div>
|
|
57
|
+
</div>
|
|
58
|
+
<div class="server-box-span3">Broadcast to others</div>
|
|
59
|
+
|
|
60
|
+
<!-- Step 4: Other clients receive -->
|
|
61
|
+
<div class="client-box-single">Other clients</div>
|
|
62
|
+
<div class="arrow-container-row5">
|
|
63
|
+
<div class="arrow-head-left-blue"></div>
|
|
64
|
+
<span class="arrow-label-blue">Broadcast</span>
|
|
65
|
+
<div class="arrow-line-broadcast-return"></div>
|
|
66
|
+
</div>
|
|
67
|
+
<div class="empty-grid-cell-row5"></div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<!-- Bottom Left: Client-Side -->
|
|
72
|
+
<div>
|
|
73
|
+
<h4 class="section-heading">🔵 Client-Side (Browser)</h4>
|
|
74
|
+
<pre class="code">// 1. Initialize api-ape client
|
|
75
|
+
const client = await getApeClient()
|
|
76
|
+
const api = client.sender // Proxy object
|
|
77
|
+
|
|
78
|
+
// 2. Call server function - property name = file path
|
|
79
|
+
// api.message() → calls api/message.js
|
|
80
|
+
api.message({ user: 'Alice', text: 'Hello!' })
|
|
81
|
+
.then(response => {
|
|
82
|
+
// Server returned: { ok: true, message: {...} }
|
|
83
|
+
console.log('Response:', response)
|
|
84
|
+
})
|
|
85
|
+
.catch(err => {
|
|
86
|
+
// Server threw an error
|
|
87
|
+
console.error('Error:', err)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// 3. Listen for server broadcasts
|
|
91
|
+
client.setOnReciver('message', ({ data }) => {
|
|
92
|
+
// Server called: this.broadcastOthers('message', data)
|
|
93
|
+
// This fires for ALL clients except the sender
|
|
94
|
+
console.log('Broadcast received:', data.message)
|
|
95
|
+
})</pre>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<!-- Bottom Right: Server-Side -->
|
|
99
|
+
<div>
|
|
100
|
+
<h4 class="section-heading">🟢 Server-Side (api/message.js)</h4>
|
|
101
|
+
<pre class="code">// File: api/message.js
|
|
102
|
+
// This function is called when client does: api.message(data)
|
|
103
|
+
|
|
104
|
+
module.exports = function message(data) {
|
|
105
|
+
const { user, text } = data
|
|
106
|
+
|
|
107
|
+
// Validate input
|
|
108
|
+
if (!user || !text) {
|
|
109
|
+
throw new Error('Missing user or text')
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const msg = {
|
|
113
|
+
user,
|
|
114
|
+
text,
|
|
115
|
+
time: new Date().toISOString()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Broadcast to ALL OTHER clients (not the sender)
|
|
119
|
+
this.broadcastOthers('message', { message: msg })
|
|
120
|
+
|
|
121
|
+
// Return response to sender (fulfills Promise)
|
|
122
|
+
return { ok: true, message: msg }
|
|
123
|
+
}</pre>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
</template>
|
|
129
|
+
|
|
130
|
+
<style scoped>
|
|
131
|
+
.code-section {
|
|
132
|
+
margin-top: 2rem;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.code-title {
|
|
136
|
+
margin-bottom: 0.5rem;
|
|
137
|
+
color: #0f0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.grid-container {
|
|
141
|
+
max-width: 1200px;
|
|
142
|
+
margin: 1.5rem auto 0;
|
|
143
|
+
padding: 0 1rem;
|
|
144
|
+
width: 100%;
|
|
145
|
+
box-sizing: border-box;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.grid-layout {
|
|
149
|
+
display: grid;
|
|
150
|
+
grid-template-columns: 1fr;
|
|
151
|
+
gap: 2rem;
|
|
152
|
+
width: 100%;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@media (min-width: 768px) {
|
|
156
|
+
.grid-layout {
|
|
157
|
+
grid-template-columns: 1fr 1fr;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.section-heading {
|
|
162
|
+
margin-bottom: 0.5rem;
|
|
163
|
+
font-size: 0.9rem;
|
|
164
|
+
font-weight: bold;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.section-heading-large {
|
|
168
|
+
margin-bottom: 1rem;
|
|
169
|
+
font-size: 0.9rem;
|
|
170
|
+
font-weight: bold;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.code {
|
|
174
|
+
background: rgba(0, 0, 0, 0.4);
|
|
175
|
+
padding: 1.5rem;
|
|
176
|
+
border-radius: 12px;
|
|
177
|
+
font-size: 0.75rem;
|
|
178
|
+
color: #0f0;
|
|
179
|
+
overflow: auto;
|
|
180
|
+
white-space: pre-wrap;
|
|
181
|
+
font-family: monospace;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.data-flow-grid {
|
|
185
|
+
display: grid;
|
|
186
|
+
grid-template-columns: 200px 1fr 200px;
|
|
187
|
+
grid-template-rows: auto auto auto auto auto;
|
|
188
|
+
gap: 1rem;
|
|
189
|
+
align-items: stretch;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.column-header-client {
|
|
193
|
+
font-size: 0.8rem;
|
|
194
|
+
font-weight: bold;
|
|
195
|
+
text-align: center;
|
|
196
|
+
grid-row: 1;
|
|
197
|
+
grid-column: 1;
|
|
198
|
+
color: #00d2ff;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.column-header-server {
|
|
202
|
+
font-size: 0.8rem;
|
|
203
|
+
font-weight: bold;
|
|
204
|
+
text-align: center;
|
|
205
|
+
grid-row: 1;
|
|
206
|
+
grid-column: 3;
|
|
207
|
+
color: #00e676;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.client-box-span3 {
|
|
211
|
+
background: linear-gradient(135deg, #3a7bd5, #00d2ff);
|
|
212
|
+
padding: 0.75rem 1rem;
|
|
213
|
+
border-radius: 8px;
|
|
214
|
+
color: #fff;
|
|
215
|
+
font-size: 0.75rem;
|
|
216
|
+
font-weight: bold;
|
|
217
|
+
box-shadow: 0 4px 12px rgba(58, 123, 213, 0.4);
|
|
218
|
+
display: flex;
|
|
219
|
+
align-items: center;
|
|
220
|
+
justify-content: center;
|
|
221
|
+
grid-column: 1;
|
|
222
|
+
grid-row: 2 / 5;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.client-box-single {
|
|
226
|
+
background: linear-gradient(135deg, #3a7bd5, #00d2ff);
|
|
227
|
+
padding: 0.75rem 1rem;
|
|
228
|
+
border-radius: 8px;
|
|
229
|
+
color: #fff;
|
|
230
|
+
font-size: 0.75rem;
|
|
231
|
+
font-weight: bold;
|
|
232
|
+
box-shadow: 0 4px 12px rgba(58, 123, 213, 0.4);
|
|
233
|
+
display: flex;
|
|
234
|
+
align-items: center;
|
|
235
|
+
justify-content: center;
|
|
236
|
+
grid-column: 1;
|
|
237
|
+
grid-row: 5;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.server-box-span2 {
|
|
241
|
+
background: linear-gradient(135deg, #00c851, #00e676);
|
|
242
|
+
padding: 0.75rem 1rem;
|
|
243
|
+
border-radius: 8px;
|
|
244
|
+
color: #fff;
|
|
245
|
+
font-size: 0.75rem;
|
|
246
|
+
font-weight: bold;
|
|
247
|
+
box-shadow: 0 4px 12px rgba(0, 200, 81, 0.4);
|
|
248
|
+
display: flex;
|
|
249
|
+
align-items: center;
|
|
250
|
+
justify-content: center;
|
|
251
|
+
grid-column: 3;
|
|
252
|
+
grid-row: 2 / 4;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.server-box-span3 {
|
|
256
|
+
background: linear-gradient(135deg, #00c851, #00e676);
|
|
257
|
+
padding: 0.75rem 1rem;
|
|
258
|
+
border-radius: 8px;
|
|
259
|
+
color: #fff;
|
|
260
|
+
font-size: 0.75rem;
|
|
261
|
+
font-weight: bold;
|
|
262
|
+
box-shadow: 0 4px 12px rgba(0, 200, 81, 0.4);
|
|
263
|
+
display: flex;
|
|
264
|
+
align-items: center;
|
|
265
|
+
justify-content: center;
|
|
266
|
+
grid-column: 3;
|
|
267
|
+
grid-row: 4 / 6;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.arrow-container-row2,
|
|
271
|
+
.arrow-container-row3,
|
|
272
|
+
.arrow-container-row4,
|
|
273
|
+
.arrow-container-row5 {
|
|
274
|
+
display: flex;
|
|
275
|
+
align-items: center;
|
|
276
|
+
justify-content: center;
|
|
277
|
+
gap: 0.5rem;
|
|
278
|
+
grid-column: 2;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.arrow-container-row2 { grid-row: 2; }
|
|
282
|
+
.arrow-container-row3 { grid-row: 3; }
|
|
283
|
+
.arrow-container-row4 { grid-row: 4; }
|
|
284
|
+
.arrow-container-row5 { grid-row: 5; }
|
|
285
|
+
|
|
286
|
+
.arrow-line-send {
|
|
287
|
+
flex: 1;
|
|
288
|
+
height: 2px;
|
|
289
|
+
background: linear-gradient(90deg, #00d2ff, #00e676);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.arrow-line-return {
|
|
293
|
+
flex: 1;
|
|
294
|
+
height: 2px;
|
|
295
|
+
background: linear-gradient(90deg, #00e676, transparent);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.arrow-line-broadcast {
|
|
299
|
+
flex: 1;
|
|
300
|
+
height: 2px;
|
|
301
|
+
background: linear-gradient(90deg, transparent, #00e676);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.arrow-line-broadcast-return {
|
|
305
|
+
flex: 1;
|
|
306
|
+
height: 2px;
|
|
307
|
+
background: linear-gradient(90deg, #00d2ff, transparent);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.arrow-label-blue {
|
|
311
|
+
font-size: 0.7rem;
|
|
312
|
+
white-space: nowrap;
|
|
313
|
+
padding: 0 0.5rem;
|
|
314
|
+
color: #00d2ff;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.arrow-label-green {
|
|
318
|
+
font-size: 0.7rem;
|
|
319
|
+
white-space: nowrap;
|
|
320
|
+
padding: 0 0.5rem;
|
|
321
|
+
color: #00e676;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.arrow-head-right {
|
|
325
|
+
width: 0;
|
|
326
|
+
height: 0;
|
|
327
|
+
border-top: 4px solid transparent;
|
|
328
|
+
border-bottom: 4px solid transparent;
|
|
329
|
+
border-left: 8px solid #00e676;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.arrow-head-left {
|
|
333
|
+
width: 0;
|
|
334
|
+
height: 0;
|
|
335
|
+
border-top: 4px solid transparent;
|
|
336
|
+
border-bottom: 4px solid transparent;
|
|
337
|
+
border-right: 8px solid #00e676;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.arrow-head-left-blue {
|
|
341
|
+
width: 0;
|
|
342
|
+
height: 0;
|
|
343
|
+
border-top: 4px solid transparent;
|
|
344
|
+
border-bottom: 4px solid transparent;
|
|
345
|
+
border-right: 8px solid #00d2ff;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.empty-grid-cell { grid-row: 2; grid-column: 3; }
|
|
349
|
+
.empty-grid-cell-row3 { grid-row: 3; grid-column: 1; }
|
|
350
|
+
.empty-grid-cell-row4 { grid-row: 4; grid-column: 1; }
|
|
351
|
+
.empty-grid-cell-row5 { grid-row: 5; grid-column: 3; }
|
|
352
|
+
</style>
|