canvas_erd 0.1.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +9 -0
- data/LICENSE +21 -0
- data/README.md +86 -0
- data/exe/canvas_erd +9 -0
- data/lib/canvas_erd/application_loader.rb +37 -0
- data/lib/canvas_erd/browser.rb +40 -0
- data/lib/canvas_erd/cli.rb +125 -0
- data/lib/canvas_erd/diagram.rb +130 -0
- data/lib/canvas_erd/diagram_store.rb +70 -0
- data/lib/canvas_erd/png_document.rb +80 -0
- data/lib/canvas_erd/schema_provider.rb +15 -0
- data/lib/canvas_erd/server.rb +210 -0
- data/lib/canvas_erd/version.rb +6 -0
- data/lib/canvas_erd/web/app.js +1394 -0
- data/lib/canvas_erd/web/document.js +473 -0
- data/lib/canvas_erd/web/index.html +119 -0
- data/lib/canvas_erd/web/layout.js +74 -0
- data/lib/canvas_erd/web/png.js +146 -0
- data/lib/canvas_erd/web/styles.css +439 -0
- data/lib/canvas_erd/web/vendor/elk-api.js +227 -0
- data/lib/canvas_erd/web/vendor/elk-worker.min.js +6317 -0
- data/lib/canvas_erd/web/vendor/fabric.min.js +473 -0
- data/lib/canvas_erd.rb +14 -0
- data/licenses/ELKJS-LICENSE.md +263 -0
- data/licenses/FABRIC-JS-LICENSE.txt +22 -0
- data/licenses/ICONOIR-LICENSE +21 -0
- metadata +155 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
(function (root, factory) {
|
|
2
|
+
const api = factory();
|
|
3
|
+
if (typeof module === "object" && module.exports) module.exports = api;
|
|
4
|
+
else root.CanvasERDPng = api;
|
|
5
|
+
})(typeof globalThis === "object" ? globalThis : this, function () {
|
|
6
|
+
"use strict";
|
|
7
|
+
|
|
8
|
+
const SIGNATURE = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
9
|
+
const KEYWORD = "CanvasERD";
|
|
10
|
+
const MAX_EXPORT_SIDE = 8192;
|
|
11
|
+
const MAX_EXPORT_PIXELS = 16 * 1024 * 1024;
|
|
12
|
+
const encoder = new TextEncoder();
|
|
13
|
+
const decoder = new TextDecoder();
|
|
14
|
+
const crcTable = new Uint32Array(256);
|
|
15
|
+
|
|
16
|
+
for (let number = 0; number < 256; number += 1) {
|
|
17
|
+
let value = number;
|
|
18
|
+
for (let bit = 0; bit < 8; bit += 1) {
|
|
19
|
+
value = (value & 1) ? (0xedb88320 ^ (value >>> 1)) : (value >>> 1);
|
|
20
|
+
}
|
|
21
|
+
crcTable[number] = value >>> 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function concatenate(parts) {
|
|
25
|
+
const result = new Uint8Array(parts.reduce((total, part) => total + part.length, 0));
|
|
26
|
+
let offset = 0;
|
|
27
|
+
parts.forEach((part) => {
|
|
28
|
+
result.set(part, offset);
|
|
29
|
+
offset += part.length;
|
|
30
|
+
});
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readUint32(bytes, offset) {
|
|
35
|
+
return (
|
|
36
|
+
bytes[offset] * 0x1000000 +
|
|
37
|
+
(bytes[offset + 1] << 16) +
|
|
38
|
+
(bytes[offset + 2] << 8) +
|
|
39
|
+
bytes[offset + 3]
|
|
40
|
+
) >>> 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function writeUint32(value) {
|
|
44
|
+
return new Uint8Array([
|
|
45
|
+
(value >>> 24) & 0xff,
|
|
46
|
+
(value >>> 16) & 0xff,
|
|
47
|
+
(value >>> 8) & 0xff,
|
|
48
|
+
value & 0xff
|
|
49
|
+
]);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function crc32(bytes) {
|
|
53
|
+
let checksum = 0xffffffff;
|
|
54
|
+
bytes.forEach((byte) => {
|
|
55
|
+
checksum = crcTable[(checksum ^ byte) & 0xff] ^ (checksum >>> 8);
|
|
56
|
+
});
|
|
57
|
+
return (checksum ^ 0xffffffff) >>> 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function chunks(bytes) {
|
|
61
|
+
if (SIGNATURE.some((byte, index) => bytes[index] !== byte)) throw new Error("Not a PNG file");
|
|
62
|
+
|
|
63
|
+
const result = [];
|
|
64
|
+
let offset = SIGNATURE.length;
|
|
65
|
+
while (offset < bytes.length) {
|
|
66
|
+
if (offset + 12 > bytes.length) throw new Error("PNG chunk is truncated");
|
|
67
|
+
const length = readUint32(bytes, offset);
|
|
68
|
+
const end = offset + 12 + length;
|
|
69
|
+
if (end > bytes.length) throw new Error("PNG chunk is truncated");
|
|
70
|
+
|
|
71
|
+
const typeBytes = bytes.slice(offset + 4, offset + 8);
|
|
72
|
+
const data = bytes.slice(offset + 8, offset + 8 + length);
|
|
73
|
+
const checksum = readUint32(bytes, offset + 8 + length);
|
|
74
|
+
if (crc32(concatenate([typeBytes, data])) !== checksum) throw new Error("PNG chunk checksum is invalid");
|
|
75
|
+
|
|
76
|
+
const type = String.fromCharCode(...typeBytes);
|
|
77
|
+
result.push({ type, data, offset, end });
|
|
78
|
+
offset = end;
|
|
79
|
+
if (type === "IEND") return result;
|
|
80
|
+
}
|
|
81
|
+
throw new Error("PNG ended before its IEND chunk");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function createChunk(type, data) {
|
|
85
|
+
const typeBytes = encoder.encode(type);
|
|
86
|
+
return concatenate([
|
|
87
|
+
writeUint32(data.length),
|
|
88
|
+
typeBytes,
|
|
89
|
+
data,
|
|
90
|
+
writeUint32(crc32(concatenate([typeBytes, data])))
|
|
91
|
+
]);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function metadataData(documentData) {
|
|
95
|
+
return concatenate([
|
|
96
|
+
encoder.encode(KEYWORD),
|
|
97
|
+
new Uint8Array(5),
|
|
98
|
+
encoder.encode(JSON.stringify(documentData))
|
|
99
|
+
]);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function embedDocument(pngBytes, documentData) {
|
|
103
|
+
const parsedChunks = chunks(pngBytes);
|
|
104
|
+
const endChunk = parsedChunks.find((chunk) => chunk.type === "IEND");
|
|
105
|
+
const metadataChunk = createChunk("iTXt", metadataData(documentData));
|
|
106
|
+
return concatenate([
|
|
107
|
+
pngBytes.slice(0, endChunk.offset),
|
|
108
|
+
metadataChunk,
|
|
109
|
+
pngBytes.slice(endChunk.offset)
|
|
110
|
+
]);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function extractDocument(pngBytes) {
|
|
114
|
+
const prefix = metadataData({}).slice(0, encoder.encode(KEYWORD).length + 5);
|
|
115
|
+
let json;
|
|
116
|
+
chunks(pngBytes).forEach((chunk) => {
|
|
117
|
+
const matches = chunk.type === "iTXt" && prefix.every((byte, index) => chunk.data[index] === byte);
|
|
118
|
+
if (matches) json = decoder.decode(chunk.data.slice(prefix.length));
|
|
119
|
+
});
|
|
120
|
+
if (!json) throw new Error("PNG does not contain an editable CanvasERD diagram");
|
|
121
|
+
return JSON.parse(json);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function bytesFromDataUrl(dataUrl) {
|
|
125
|
+
const marker = "data:image/png;base64,";
|
|
126
|
+
if (!dataUrl.startsWith(marker)) throw new Error("Expected a PNG data URL");
|
|
127
|
+
const binary = atob(dataUrl.slice(marker.length));
|
|
128
|
+
return Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function exportMultiplier(width, height) {
|
|
132
|
+
return Math.min(
|
|
133
|
+
1,
|
|
134
|
+
MAX_EXPORT_SIDE / width,
|
|
135
|
+
MAX_EXPORT_SIDE / height,
|
|
136
|
+
Math.sqrt(MAX_EXPORT_PIXELS / (width * height))
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
embedDocument,
|
|
142
|
+
extractDocument,
|
|
143
|
+
bytesFromDataUrl,
|
|
144
|
+
exportMultiplier
|
|
145
|
+
};
|
|
146
|
+
});
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color: #1f2937;
|
|
3
|
+
background: #f3f5f8;
|
|
4
|
+
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
5
|
+
font-synthesis: none;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
* {
|
|
9
|
+
box-sizing: border-box;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
[hidden] {
|
|
13
|
+
display: none !important;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
html,
|
|
17
|
+
body,
|
|
18
|
+
.app-shell {
|
|
19
|
+
height: 100%;
|
|
20
|
+
margin: 0;
|
|
21
|
+
overflow: hidden;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
button,
|
|
25
|
+
input,
|
|
26
|
+
select {
|
|
27
|
+
font: inherit;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
button {
|
|
31
|
+
min-height: 2.25rem;
|
|
32
|
+
border: 1px solid #cbd3df;
|
|
33
|
+
border-radius: 0.45rem;
|
|
34
|
+
color: #263447;
|
|
35
|
+
background: #ffffff;
|
|
36
|
+
cursor: pointer;
|
|
37
|
+
padding: 0.35rem 0.75rem;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
button:hover {
|
|
41
|
+
border-color: #91a1b8;
|
|
42
|
+
background: #f8fafc;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.icon-button[aria-pressed="true"] {
|
|
46
|
+
border-color: #2563eb;
|
|
47
|
+
color: #ffffff;
|
|
48
|
+
background: #2563eb;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
button:disabled {
|
|
52
|
+
cursor: default;
|
|
53
|
+
opacity: 0.65;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
button:focus-visible,
|
|
57
|
+
input:focus-visible,
|
|
58
|
+
select:focus-visible {
|
|
59
|
+
outline: 2px solid #2563eb;
|
|
60
|
+
outline-offset: 2px;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#diagram-name,
|
|
64
|
+
#saved-diagrams {
|
|
65
|
+
min-height: 2.25rem;
|
|
66
|
+
border: 1px solid #cbd3df;
|
|
67
|
+
border-radius: 0.45rem;
|
|
68
|
+
color: #263447;
|
|
69
|
+
background: #ffffff;
|
|
70
|
+
padding: 0.35rem 0.5rem;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
#diagram-name {
|
|
74
|
+
width: 18rem;
|
|
75
|
+
max-width: 40vw;
|
|
76
|
+
font-size: 1.15rem;
|
|
77
|
+
font-weight: 700;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
#saved-diagrams {
|
|
81
|
+
width: 12rem;
|
|
82
|
+
max-width: 20vw;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.app-shell {
|
|
86
|
+
display: grid;
|
|
87
|
+
grid-template-rows: 1fr;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.diagram-toolbar {
|
|
91
|
+
position: absolute;
|
|
92
|
+
top: 0.75rem;
|
|
93
|
+
left: 0.75rem;
|
|
94
|
+
z-index: 2;
|
|
95
|
+
display: flex;
|
|
96
|
+
align-items: center;
|
|
97
|
+
gap: 1rem;
|
|
98
|
+
max-width: calc(100% - 5rem);
|
|
99
|
+
border: 1px solid rgba(203, 213, 225, 0.9);
|
|
100
|
+
border-radius: 0.75rem;
|
|
101
|
+
background: rgba(255, 255, 255, 0.94);
|
|
102
|
+
padding: 0.55rem 0.65rem;
|
|
103
|
+
box-shadow: 0 5px 18px rgba(15, 23, 42, 0.12);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.diagram-identity {
|
|
107
|
+
min-width: 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.diagram-name-row {
|
|
111
|
+
display: flex;
|
|
112
|
+
align-items: center;
|
|
113
|
+
gap: 0.25rem;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.diagram-name-row h1 {
|
|
117
|
+
margin: 0;
|
|
118
|
+
overflow: hidden;
|
|
119
|
+
color: #172554;
|
|
120
|
+
font-size: 1.15rem;
|
|
121
|
+
letter-spacing: -0.01em;
|
|
122
|
+
text-overflow: ellipsis;
|
|
123
|
+
white-space: nowrap;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.diagram-identity p {
|
|
127
|
+
margin: 0.2rem 0 0;
|
|
128
|
+
overflow: hidden;
|
|
129
|
+
color: #66758a;
|
|
130
|
+
font-size: 0.78rem;
|
|
131
|
+
text-overflow: ellipsis;
|
|
132
|
+
white-space: nowrap;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.toolbar-actions,
|
|
136
|
+
.button-group,
|
|
137
|
+
.selection-actions,
|
|
138
|
+
.sidebar-heading,
|
|
139
|
+
.sidebar-actions,
|
|
140
|
+
.canvas-footer,
|
|
141
|
+
.canvas-controls {
|
|
142
|
+
display: flex;
|
|
143
|
+
align-items: center;
|
|
144
|
+
gap: 0.4rem;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.icon-button {
|
|
148
|
+
width: 2.35rem;
|
|
149
|
+
flex: 0 0 auto;
|
|
150
|
+
padding: 0;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.icon-button svg {
|
|
154
|
+
width: 1.15rem;
|
|
155
|
+
height: 1.15rem;
|
|
156
|
+
fill: none;
|
|
157
|
+
stroke: currentColor;
|
|
158
|
+
stroke-linecap: round;
|
|
159
|
+
stroke-linejoin: round;
|
|
160
|
+
stroke-width: 1.8;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.icon-button.compact,
|
|
164
|
+
.icon-button.name-edit {
|
|
165
|
+
width: 1.9rem;
|
|
166
|
+
min-height: 1.9rem;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.icon-button.name-edit {
|
|
170
|
+
border-color: transparent;
|
|
171
|
+
background: transparent;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.button-group {
|
|
175
|
+
gap: 0;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.button-group button {
|
|
179
|
+
width: 2.35rem;
|
|
180
|
+
border-radius: 0;
|
|
181
|
+
padding: 0;
|
|
182
|
+
font-size: 1.25rem;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.button-group button:first-child {
|
|
186
|
+
border-radius: 0.45rem 0 0 0.45rem;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.button-group button:last-child {
|
|
190
|
+
margin-left: -1px;
|
|
191
|
+
border-radius: 0 0.45rem 0.45rem 0;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.workspace {
|
|
195
|
+
position: relative;
|
|
196
|
+
height: 100%;
|
|
197
|
+
min-height: 0;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.sidebar {
|
|
201
|
+
position: absolute;
|
|
202
|
+
top: 6.25rem;
|
|
203
|
+
bottom: 3.75rem;
|
|
204
|
+
left: 0.75rem;
|
|
205
|
+
z-index: 2;
|
|
206
|
+
display: flex;
|
|
207
|
+
width: 17rem;
|
|
208
|
+
min-height: 0;
|
|
209
|
+
flex-direction: column;
|
|
210
|
+
border: 1px solid rgba(203, 213, 225, 0.9);
|
|
211
|
+
border-radius: 0.75rem;
|
|
212
|
+
background: rgba(255, 255, 255, 0.94);
|
|
213
|
+
padding: 1rem;
|
|
214
|
+
box-shadow: 0 5px 18px rgba(15, 23, 42, 0.12);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.sidebar-heading {
|
|
218
|
+
justify-content: space-between;
|
|
219
|
+
margin-bottom: 0.45rem;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.sidebar-content {
|
|
223
|
+
display: flex;
|
|
224
|
+
min-height: 0;
|
|
225
|
+
flex: 1;
|
|
226
|
+
flex-direction: column;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.sidebar.collapsed {
|
|
230
|
+
display: none;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.search-label {
|
|
234
|
+
color: #334155;
|
|
235
|
+
font-size: 0.75rem;
|
|
236
|
+
font-weight: 700;
|
|
237
|
+
letter-spacing: 0.06em;
|
|
238
|
+
text-transform: uppercase;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
#table-search {
|
|
242
|
+
width: 100%;
|
|
243
|
+
min-height: 2.4rem;
|
|
244
|
+
border: 1px solid #cbd3df;
|
|
245
|
+
border-radius: 0.45rem;
|
|
246
|
+
color: #1f2937;
|
|
247
|
+
background: #ffffff;
|
|
248
|
+
padding: 0.45rem 0.65rem;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.selection-actions {
|
|
252
|
+
margin: 0.55rem 0 0.65rem;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.selection-actions button {
|
|
256
|
+
min-height: 1.9rem;
|
|
257
|
+
padding: 0.2rem 0.55rem;
|
|
258
|
+
font-size: 0.75rem;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.table-list {
|
|
262
|
+
min-height: 0;
|
|
263
|
+
flex: 1;
|
|
264
|
+
overflow-y: auto;
|
|
265
|
+
scrollbar-gutter: stable;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.table-option {
|
|
269
|
+
display: flex;
|
|
270
|
+
align-items: center;
|
|
271
|
+
gap: 0.55rem;
|
|
272
|
+
min-height: 2rem;
|
|
273
|
+
border-radius: 0.35rem;
|
|
274
|
+
cursor: pointer;
|
|
275
|
+
padding: 0.25rem 0.35rem;
|
|
276
|
+
font-size: 0.82rem;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.table-option:hover {
|
|
280
|
+
background: #f1f5f9;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.table-option span {
|
|
284
|
+
overflow: hidden;
|
|
285
|
+
text-overflow: ellipsis;
|
|
286
|
+
white-space: nowrap;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.table-option input {
|
|
290
|
+
width: 1rem;
|
|
291
|
+
height: 1rem;
|
|
292
|
+
flex: 0 0 auto;
|
|
293
|
+
accent-color: #2563eb;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.canvas-panel {
|
|
297
|
+
position: relative;
|
|
298
|
+
width: 100%;
|
|
299
|
+
height: 100%;
|
|
300
|
+
min-width: 0;
|
|
301
|
+
min-height: 0;
|
|
302
|
+
overflow: hidden;
|
|
303
|
+
background-color: #f7f8fb;
|
|
304
|
+
background-image: radial-gradient(#cbd4e1 0.8px, transparent 0.8px);
|
|
305
|
+
background-size: 20px 20px;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.canvas-tools {
|
|
309
|
+
position: absolute;
|
|
310
|
+
bottom: 0.75rem;
|
|
311
|
+
left: 50%;
|
|
312
|
+
z-index: 2;
|
|
313
|
+
display: flex;
|
|
314
|
+
align-items: center;
|
|
315
|
+
gap: 0.25rem;
|
|
316
|
+
border: 1px solid rgba(203, 213, 225, 0.9);
|
|
317
|
+
border-radius: 0.75rem;
|
|
318
|
+
background: rgba(255, 255, 255, 0.94);
|
|
319
|
+
padding: 0.25rem;
|
|
320
|
+
transform: translateX(-50%);
|
|
321
|
+
box-shadow: 0 5px 18px rgba(15, 23, 42, 0.12);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.canvas-panel .canvas-container {
|
|
325
|
+
position: absolute !important;
|
|
326
|
+
inset: 0;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.canvas-footer {
|
|
330
|
+
position: absolute;
|
|
331
|
+
left: 0.75rem;
|
|
332
|
+
right: 0.75rem;
|
|
333
|
+
bottom: 0.75rem;
|
|
334
|
+
z-index: 1;
|
|
335
|
+
justify-content: space-between;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.canvas-brand {
|
|
339
|
+
color: #64748b;
|
|
340
|
+
font-size: 0.78rem;
|
|
341
|
+
font-weight: 700;
|
|
342
|
+
text-decoration: none;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.canvas-brand:hover {
|
|
346
|
+
color: #243b63;
|
|
347
|
+
text-decoration: underline;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.shortcut-panel {
|
|
351
|
+
position: absolute;
|
|
352
|
+
right: 0.75rem;
|
|
353
|
+
bottom: 3.75rem;
|
|
354
|
+
z-index: 2;
|
|
355
|
+
width: 18rem;
|
|
356
|
+
border: 1px solid rgba(203, 213, 225, 0.9);
|
|
357
|
+
border-radius: 0.75rem;
|
|
358
|
+
background: rgba(255, 255, 255, 0.96);
|
|
359
|
+
padding: 0.8rem;
|
|
360
|
+
box-shadow: 0 5px 18px rgba(15, 23, 42, 0.12);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.shortcut-panel h2 {
|
|
364
|
+
margin: 0 0 0.65rem;
|
|
365
|
+
color: #172554;
|
|
366
|
+
font-size: 0.9rem;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.shortcut-panel dl {
|
|
370
|
+
margin: 0;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.shortcut-panel dl div {
|
|
374
|
+
display: flex;
|
|
375
|
+
align-items: center;
|
|
376
|
+
justify-content: space-between;
|
|
377
|
+
gap: 1rem;
|
|
378
|
+
min-height: 1.8rem;
|
|
379
|
+
font-size: 0.76rem;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.shortcut-panel dt {
|
|
383
|
+
color: #526277;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.shortcut-panel dd {
|
|
387
|
+
margin: 0;
|
|
388
|
+
white-space: nowrap;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
kbd {
|
|
392
|
+
border: 1px solid #cbd3df;
|
|
393
|
+
border-radius: 0.25rem;
|
|
394
|
+
color: #263447;
|
|
395
|
+
background: #f8fafc;
|
|
396
|
+
padding: 0.08rem 0.3rem;
|
|
397
|
+
font: 0.7rem ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
398
|
+
box-shadow: 0 1px 0 #cbd3df;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
.notice {
|
|
402
|
+
position: absolute;
|
|
403
|
+
top: 0.75rem;
|
|
404
|
+
left: 50%;
|
|
405
|
+
z-index: 1;
|
|
406
|
+
border: 1px solid #bfdbfe;
|
|
407
|
+
border-radius: 0.45rem;
|
|
408
|
+
color: #1e3a5f;
|
|
409
|
+
background: #eff6ff;
|
|
410
|
+
padding: 0.5rem 0.7rem;
|
|
411
|
+
font-size: 0.78rem;
|
|
412
|
+
transform: translateX(-50%);
|
|
413
|
+
box-shadow: 0 4px 14px rgba(15, 23, 42, 0.1);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.notice.error {
|
|
417
|
+
color: #991b1b;
|
|
418
|
+
border-color: #fecaca;
|
|
419
|
+
background: #fff7f7;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
.loading {
|
|
423
|
+
position: absolute;
|
|
424
|
+
top: 50%;
|
|
425
|
+
left: 50%;
|
|
426
|
+
border: 1px solid #d6deea;
|
|
427
|
+
border-radius: 0.6rem;
|
|
428
|
+
color: #526277;
|
|
429
|
+
background: #ffffff;
|
|
430
|
+
padding: 0.8rem 1rem;
|
|
431
|
+
transform: translate(-50%, -50%);
|
|
432
|
+
box-shadow: 0 8px 30px rgba(15, 23, 42, 0.12);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
.loading.error {
|
|
436
|
+
color: #991b1b;
|
|
437
|
+
border-color: #fecaca;
|
|
438
|
+
background: #fff7f7;
|
|
439
|
+
}
|