milkdown_engine 0.1.1 → 0.1.2
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 +4 -4
- data/app/assets/stylesheets/milkdown_engine/application.css +390 -0
- data/app/controllers/milkdown_engine/application_controller.rb +1 -1
- data/app/controllers/milkdown_engine/notes_controller.rb +62 -0
- data/app/helpers/milkdown_engine/application_helper.rb +19 -8
- data/app/javascript/milkdown_engine/controllers/auto_submit_controller.js +10 -0
- data/app/javascript/milkdown_engine/controllers/editor_controller.js +1 -1
- data/app/javascript/milkdown_engine/controllers/item_list_controller.js +29 -0
- data/app/models/milkdown_engine/{md_document.rb → document.rb} +17 -5
- data/app/views/milkdown_engine/notes/_content_editor.html.ruby +3 -0
- data/app/views/milkdown_engine/notes/_content_form.html.ruby +3 -0
- data/app/views/milkdown_engine/notes/_form.html.ruby +27 -0
- data/app/views/milkdown_engine/notes/_item_list_panel.html.ruby +22 -0
- data/app/views/milkdown_engine/notes/_navigation_rail.html.ruby +30 -0
- data/app/views/milkdown_engine/notes/_note_card.html.ruby +9 -0
- data/app/views/milkdown_engine/notes/_title_form.html.ruby +3 -0
- data/app/views/milkdown_engine/notes/edit.html.ruby +11 -0
- data/app/views/milkdown_engine/notes/index.html.ruby +11 -0
- data/app/views/milkdown_engine/notes/new.html.ruby +11 -0
- data/app/views/milkdown_engine/notes/show.html.ruby +11 -0
- data/config/importmap.rb +3 -0
- data/config/routes.rb +1 -0
- data/db/migrate/20260327000000_create_acts_as_taggable_on_tables.rb +40 -0
- data/db/migrate/20260327100000_rename_md_documents_to_documents.rb +7 -0
- data/lib/milkdown_engine/engine.rb +8 -0
- data/lib/milkdown_engine/version.rb +1 -1
- metadata +46 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cae76a4c5e5b3f031abb244592b8409c33d85e3cd590536ce62017d707d38314
|
|
4
|
+
data.tar.gz: f50de7f3bb331329dcbb8642d51e7464d97f6379416cd9db1b0b7dd9d15bfd1c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f1070fe2f7f90718c6bdda8362f940d6a1e7ee9efcdf9b444172a06b044970cc0da7bbfa2040b4a4ec9628d43a51f7e94b81d8398d69c2239a56291993f30b42
|
|
7
|
+
data.tar.gz: 982f0725cdddc3bacd8cfeecdb202f666f8661dc23590c0231bacda39d30a780100ddb0f731f2c912e1db22b024cf43984ea46579a9f352522a98e0b328900fe
|
|
@@ -10,6 +10,14 @@
|
|
|
10
10
|
.milkdown-editor.ui.segment {
|
|
11
11
|
padding: 0;
|
|
12
12
|
position: relative;
|
|
13
|
+
display: flex;
|
|
14
|
+
flex-direction: column;
|
|
15
|
+
flex: 1;
|
|
16
|
+
overflow: hidden;
|
|
17
|
+
margin: 0;
|
|
18
|
+
border: none;
|
|
19
|
+
box-shadow: none;
|
|
20
|
+
border-radius: 0;
|
|
13
21
|
}
|
|
14
22
|
|
|
15
23
|
.milkdown-editor-label {
|
|
@@ -18,11 +26,52 @@
|
|
|
18
26
|
font-weight: 700;
|
|
19
27
|
font-size: 0.92857143em;
|
|
20
28
|
color: rgba(0, 0, 0, 0.87);
|
|
29
|
+
flex-shrink: 0;
|
|
21
30
|
}
|
|
22
31
|
|
|
23
32
|
/* The inner surface where Crepe mounts the ProseMirror editor. */
|
|
24
33
|
.milkdown-editor-surface {
|
|
25
34
|
min-height: 12rem;
|
|
35
|
+
flex: 1;
|
|
36
|
+
overflow-y: auto;
|
|
37
|
+
display: flex;
|
|
38
|
+
flex-direction: column;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.milkdown-editor-surface .milkdown {
|
|
42
|
+
display: flex;
|
|
43
|
+
flex-direction: column;
|
|
44
|
+
flex: 1;
|
|
45
|
+
min-height: 100%;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.milkdown-editor-surface .milkdown .ProseMirror {
|
|
49
|
+
padding: 15px 15px 15px 40px;
|
|
50
|
+
flex: 1;
|
|
51
|
+
min-height: 100%;
|
|
52
|
+
cursor: text;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* Title bar: fixed at top, no grow */
|
|
56
|
+
#content-editor .editor-title-bar {
|
|
57
|
+
flex-shrink: 0;
|
|
58
|
+
border-bottom: 1px solid #dfe1e4;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* Content form: fills remaining space, scrolls via milkdown surface */
|
|
62
|
+
#content-editor .editor-content {
|
|
63
|
+
display: flex;
|
|
64
|
+
flex-direction: column;
|
|
65
|
+
flex: 1;
|
|
66
|
+
overflow: hidden;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#content-editor .editor-content .ui.form {
|
|
70
|
+
display: flex;
|
|
71
|
+
flex-direction: column;
|
|
72
|
+
flex: 1;
|
|
73
|
+
overflow: hidden;
|
|
74
|
+
margin: 0;
|
|
26
75
|
}
|
|
27
76
|
|
|
28
77
|
/* When used inside a Fomantic form field, drop the segment border so it
|
|
@@ -38,3 +87,344 @@
|
|
|
38
87
|
opacity: 0.75;
|
|
39
88
|
pointer-events: none;
|
|
40
89
|
}
|
|
90
|
+
|
|
91
|
+
/* ═══════════════════════════════════════════════════════════════════════
|
|
92
|
+
Three-panel editor layout
|
|
93
|
+
═══════════════════════════════════════════════════════════════════════ */
|
|
94
|
+
|
|
95
|
+
/* Prevent parent from scrolling when the editor layout is active */
|
|
96
|
+
#site-content-scroll:has(#md-app) {
|
|
97
|
+
overflow-y: hidden;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* ── App shell grid ──────────────────────────────────── */
|
|
101
|
+
#md-app {
|
|
102
|
+
display: grid;
|
|
103
|
+
grid-template-columns: 360px 1fr;
|
|
104
|
+
grid-template-rows: 1fr;
|
|
105
|
+
grid-template-areas:
|
|
106
|
+
"item-list-panel content-editor";
|
|
107
|
+
height: 100%;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* ── Zone borders / backgrounds ──────────────────────── */
|
|
111
|
+
#navigation-rail { grid-area: navigation-rail; background: #f0ede8; border-right: 1px solid #dddbd7; display: none; }
|
|
112
|
+
#item-list-panel { grid-area: item-list-panel; background: #faf9f7; border-right: 1px solid #dddbd7; }
|
|
113
|
+
#content-editor { grid-area: content-editor; background: #ffffff; }
|
|
114
|
+
#system-bar { grid-area: system-bar; background: #f0ede8; border-top: 1px solid #dddbd7; }
|
|
115
|
+
|
|
116
|
+
/* ── Navigation rail ────────────────────────────────── */
|
|
117
|
+
#navigation-rail {
|
|
118
|
+
display: flex;
|
|
119
|
+
flex-direction: column;
|
|
120
|
+
overflow: hidden;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#navigation-rail-search {
|
|
124
|
+
padding: 10px 10px 6px;
|
|
125
|
+
flex-shrink: 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
#navigation-rail-search .ui.input input {
|
|
129
|
+
background: rgba(255,255,255,0.6);
|
|
130
|
+
border-color: #dddbd7;
|
|
131
|
+
border-radius: 20px;
|
|
132
|
+
font-size: 0.82rem;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
#navigation-rail-views,
|
|
136
|
+
#navigation-rail-tags {
|
|
137
|
+
flex-shrink: 0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
#navigation-rail-tags {
|
|
141
|
+
flex: 1;
|
|
142
|
+
overflow-y: auto;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/* Rail menus — strip Fomantic box shadows / borders */
|
|
146
|
+
#navigation-rail .ui.vertical.menu {
|
|
147
|
+
width: 100%;
|
|
148
|
+
box-shadow: none;
|
|
149
|
+
border: none;
|
|
150
|
+
border-radius: 0;
|
|
151
|
+
background: transparent;
|
|
152
|
+
margin: 0;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
#navigation-rail .ui.vertical.menu .item {
|
|
156
|
+
padding: 7px 14px;
|
|
157
|
+
font-size: 0.83rem;
|
|
158
|
+
color: #555;
|
|
159
|
+
border-radius: 0;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
#navigation-rail .ui.vertical.menu .item.active {
|
|
163
|
+
background: rgba(255,255,255,0.55);
|
|
164
|
+
color: #1a1a1a;
|
|
165
|
+
font-weight: 600;
|
|
166
|
+
border-left: 3px solid #4a90d9;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
#navigation-rail .ui.vertical.menu .item .icon {
|
|
170
|
+
margin-right: 8px;
|
|
171
|
+
color: #888;
|
|
172
|
+
width: 1em;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
#navigation-rail .ui.vertical.menu .item.active .icon {
|
|
176
|
+
color: #4a90d9;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.rail-section-label {
|
|
180
|
+
padding: 14px 14px 4px;
|
|
181
|
+
font-size: 0.7rem;
|
|
182
|
+
font-weight: 700;
|
|
183
|
+
letter-spacing: 0.08em;
|
|
184
|
+
text-transform: uppercase;
|
|
185
|
+
color: #999;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* ── Item list panel ────────────────────────────────── */
|
|
189
|
+
#item-list-panel {
|
|
190
|
+
display: flex;
|
|
191
|
+
flex-direction: column;
|
|
192
|
+
overflow: hidden;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
#item-list-header {
|
|
196
|
+
padding: 12px 14px 8px;
|
|
197
|
+
flex-shrink: 0;
|
|
198
|
+
border-bottom: 1px solid #eae8e4;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
#item-list-header .ui.header {
|
|
202
|
+
margin: 0;
|
|
203
|
+
font-size: 1rem;
|
|
204
|
+
font-weight: 600;
|
|
205
|
+
color: #1a1a1a;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
#item-list-search {
|
|
209
|
+
flex-shrink: 0;
|
|
210
|
+
padding: 0 10px 8px;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
#item-list-search .ui.input input {
|
|
214
|
+
background: #efecea;
|
|
215
|
+
border-color: transparent;
|
|
216
|
+
border-radius: 20px;
|
|
217
|
+
font-size: 0.82rem;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
#item-list-banner {
|
|
221
|
+
flex-shrink: 0;
|
|
222
|
+
margin: 0 10px 6px;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
#item-list-banner .ui.message {
|
|
226
|
+
font-size: 0.78rem;
|
|
227
|
+
padding: 8px 12px;
|
|
228
|
+
border-radius: 8px;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
#item-list-scroll {
|
|
232
|
+
flex: 1;
|
|
233
|
+
overflow-y: auto;
|
|
234
|
+
padding: 4px 0;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/* Note cards */
|
|
238
|
+
.note-card {
|
|
239
|
+
padding: 10px 14px;
|
|
240
|
+
border-bottom: 1px solid #eae8e4;
|
|
241
|
+
cursor: pointer;
|
|
242
|
+
border-left: 3px solid transparent;
|
|
243
|
+
transition: background 0.12s;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.note-card:hover { background: #f3f1ee; }
|
|
247
|
+
|
|
248
|
+
.note-card.active {
|
|
249
|
+
background: #eaf0f9;
|
|
250
|
+
border-left-color: #4a90d9;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.note-card-title {
|
|
254
|
+
font-size: 0.88rem;
|
|
255
|
+
font-weight: 600;
|
|
256
|
+
color: #1a1a1a;
|
|
257
|
+
margin-bottom: 3px;
|
|
258
|
+
white-space: nowrap;
|
|
259
|
+
overflow: hidden;
|
|
260
|
+
text-overflow: ellipsis;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.note-card-preview {
|
|
264
|
+
font-size: 0.78rem;
|
|
265
|
+
color: #777;
|
|
266
|
+
white-space: nowrap;
|
|
267
|
+
overflow: hidden;
|
|
268
|
+
text-overflow: ellipsis;
|
|
269
|
+
margin-bottom: 4px;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.note-card-meta {
|
|
273
|
+
font-size: 0.72rem;
|
|
274
|
+
color: #aaa;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.note-card-tag {
|
|
278
|
+
display: inline-block;
|
|
279
|
+
background: #eae8e4;
|
|
280
|
+
color: #666;
|
|
281
|
+
border-radius: 4px;
|
|
282
|
+
padding: 1px 6px;
|
|
283
|
+
font-size: 0.7rem;
|
|
284
|
+
margin-top: 4px;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/* ── Content editor ─────────────────────────────────── */
|
|
288
|
+
#content-editor {
|
|
289
|
+
display: flex;
|
|
290
|
+
flex-direction: column;
|
|
291
|
+
flex-grow: 1;
|
|
292
|
+
overflow-y: hidden;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/* Toolbar row */
|
|
296
|
+
#editor-toolbar-row.ui.menu {
|
|
297
|
+
border-radius: 0;
|
|
298
|
+
border: none;
|
|
299
|
+
border-bottom: none !important;
|
|
300
|
+
box-shadow: none;
|
|
301
|
+
background: #fff;
|
|
302
|
+
margin: 0;
|
|
303
|
+
min-height: 46px;
|
|
304
|
+
padding: 0 6px;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
#editor-toolbar-row .ui.transparent.input input {
|
|
308
|
+
font-size: 0.98rem;
|
|
309
|
+
font-weight: 600;
|
|
310
|
+
color: #1a1a1a;
|
|
311
|
+
padding-left: 4px;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
#editor-toolbar-row .item {
|
|
315
|
+
padding: 6px 8px;
|
|
316
|
+
color: #888;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
#editor-toolbar-row .item:hover { color: #333; }
|
|
320
|
+
|
|
321
|
+
/* Context strip */
|
|
322
|
+
#editor-context-strip.ui.menu {
|
|
323
|
+
border-radius: 0;
|
|
324
|
+
border: none;
|
|
325
|
+
border-bottom: 1px solid #eae8e4 !important;
|
|
326
|
+
box-shadow: none;
|
|
327
|
+
background: #fafafa;
|
|
328
|
+
margin: 0;
|
|
329
|
+
min-height: 36px;
|
|
330
|
+
padding: 0 10px;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
#editor-context-strip .item {
|
|
334
|
+
padding: 4px 6px;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
#editor-context-strip .ui.label {
|
|
338
|
+
font-size: 0.73rem;
|
|
339
|
+
border-radius: 20px;
|
|
340
|
+
padding: 3px 10px;
|
|
341
|
+
background: #eae8e4;
|
|
342
|
+
color: #555;
|
|
343
|
+
border: none;
|
|
344
|
+
margin-right: 4px;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
#editor-context-strip .ui.transparent.input input {
|
|
348
|
+
font-size: 0.78rem;
|
|
349
|
+
color: #aaa;
|
|
350
|
+
padding: 0;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/* Formatting toolbar */
|
|
354
|
+
#editor-format-bar.ui.menu {
|
|
355
|
+
border-radius: 0;
|
|
356
|
+
border: none;
|
|
357
|
+
border-bottom: 1px solid #eae8e4 !important;
|
|
358
|
+
box-shadow: none;
|
|
359
|
+
background: #f8f7f5;
|
|
360
|
+
margin: 0;
|
|
361
|
+
min-height: 34px;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
#editor-format-bar .item {
|
|
365
|
+
padding: 5px 10px;
|
|
366
|
+
color: #777;
|
|
367
|
+
font-size: 0.82rem;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
#editor-format-bar .item:hover { background: #eee; color: #222; }
|
|
371
|
+
#editor-format-bar .item.active { color: #4a90d9; }
|
|
372
|
+
|
|
373
|
+
#editor-format-bar .divider {
|
|
374
|
+
width: 1px;
|
|
375
|
+
background: #ddd;
|
|
376
|
+
margin: 6px 2px;
|
|
377
|
+
align-self: stretch;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/* Body canvas */
|
|
381
|
+
#editor-body-canvas {
|
|
382
|
+
flex: 1;
|
|
383
|
+
overflow-y: auto;
|
|
384
|
+
padding: 28px 40px;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
#editor-body-canvas textarea {
|
|
388
|
+
width: 100%;
|
|
389
|
+
height: 100%;
|
|
390
|
+
border: none;
|
|
391
|
+
outline: none;
|
|
392
|
+
resize: none;
|
|
393
|
+
font-size: 0.95rem;
|
|
394
|
+
line-height: 1.75;
|
|
395
|
+
color: #2a2a2a;
|
|
396
|
+
font-family: 'Georgia', serif;
|
|
397
|
+
background: transparent;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/* ── System bar ─────────────────────────────────────── */
|
|
401
|
+
#system-bar {
|
|
402
|
+
display: flex;
|
|
403
|
+
align-items: center;
|
|
404
|
+
justify-content: space-between;
|
|
405
|
+
padding: 0 12px;
|
|
406
|
+
font-size: 0.72rem;
|
|
407
|
+
color: #888;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
#system-bar-left,
|
|
411
|
+
#system-bar-right {
|
|
412
|
+
display: flex;
|
|
413
|
+
align-items: center;
|
|
414
|
+
gap: 12px;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
#system-bar .sync-dot {
|
|
418
|
+
width: 6px;
|
|
419
|
+
height: 6px;
|
|
420
|
+
border-radius: 50%;
|
|
421
|
+
background: #5cb85c;
|
|
422
|
+
display: inline-block;
|
|
423
|
+
margin-right: 4px;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
#system-bar a {
|
|
427
|
+
color: #4a90d9;
|
|
428
|
+
text-decoration: none;
|
|
429
|
+
font-weight: 500;
|
|
430
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MilkdownEngine
|
|
4
|
+
class NotesController < ApplicationController
|
|
5
|
+
before_action :set_notes
|
|
6
|
+
before_action :set_note, only: %i[show edit update destroy]
|
|
7
|
+
|
|
8
|
+
def index
|
|
9
|
+
@note = @notes.first || Document.new
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def show; end
|
|
13
|
+
|
|
14
|
+
def new
|
|
15
|
+
@note = Document.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def create
|
|
19
|
+
@note = Document.new(note_params)
|
|
20
|
+
if @note.save
|
|
21
|
+
redirect_to note_path(@note), notice: "Note was successfully created."
|
|
22
|
+
else
|
|
23
|
+
render :new, status: :unprocessable_entity
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def edit; end
|
|
28
|
+
|
|
29
|
+
def update
|
|
30
|
+
if @note.update(note_params)
|
|
31
|
+
respond_to do |format|
|
|
32
|
+
format.turbo_stream { head :ok }
|
|
33
|
+
format.html { redirect_to note_path(@note), notice: "Note was successfully updated." }
|
|
34
|
+
end
|
|
35
|
+
else
|
|
36
|
+
render :edit, status: :unprocessable_entity
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def destroy
|
|
41
|
+
@note.destroy!
|
|
42
|
+
redirect_to notes_path, notice: "Note was successfully deleted."
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def set_notes
|
|
48
|
+
@q = Document.ransack(params[:q])
|
|
49
|
+
@notes = @q.result.order(updated_at: :desc)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def set_note
|
|
53
|
+
@note = Document.find(params[:id])
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def note_params
|
|
57
|
+
params.require(:document).permit(:title, :content).tap do |p|
|
|
58
|
+
p[:content] = JSON.parse(p[:content]) if p[:content].is_a?(String)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module MilkdownEngine
|
|
4
4
|
module ApplicationHelper
|
|
5
5
|
CONTROLLER_ID = "milkdown-engine--editor"
|
|
6
|
-
EMPTY_DOC = { type: "doc", content: [{ type: "paragraph" }] }.to_json.freeze
|
|
6
|
+
EMPTY_DOC = { type: "doc", content: [ { type: "paragraph" } ] }.to_json.freeze
|
|
7
7
|
|
|
8
8
|
# Renders a Milkdown editor wired to a hidden <input> for form submission.
|
|
9
9
|
#
|
|
@@ -24,12 +24,12 @@ module MilkdownEngine
|
|
|
24
24
|
# ==== Examples
|
|
25
25
|
#
|
|
26
26
|
# # Standalone (no form builder)
|
|
27
|
-
# <%= milkdown_editor "
|
|
27
|
+
# <%= milkdown_editor "document[content]", @document.content %>
|
|
28
28
|
#
|
|
29
29
|
# # Inside a Fomantic-UI form
|
|
30
30
|
# <% Form { %>
|
|
31
31
|
# <div class="field">
|
|
32
|
-
# <%= milkdown_editor "
|
|
32
|
+
# <%= milkdown_editor "document[content]", @document.content, label: "Content" %>
|
|
33
33
|
# </div>
|
|
34
34
|
# <% Button(variant: :primary, type: :submit) { text "Save" } %>
|
|
35
35
|
# <% } %>
|
|
@@ -43,11 +43,11 @@ module MilkdownEngine
|
|
|
43
43
|
editor_html = html_options.delete(:editor_html) || {}
|
|
44
44
|
|
|
45
45
|
json_value = case value
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
when String then value.presence || EMPTY_DOC
|
|
47
|
+
when Hash then value.present? ? value.to_json : EMPTY_DOC
|
|
48
|
+
when nil then EMPTY_DOC
|
|
49
|
+
else value.to_json
|
|
50
|
+
end
|
|
51
51
|
|
|
52
52
|
extra_class = html_options.delete(:class)
|
|
53
53
|
wrapper_classes = class_names("ui segment milkdown-editor", extra_class)
|
|
@@ -84,5 +84,16 @@ module MilkdownEngine
|
|
|
84
84
|
|
|
85
85
|
milkdown_editor(name, value, **options)
|
|
86
86
|
end
|
|
87
|
+
|
|
88
|
+
# PascalCase helper for use inside Form() blocks.
|
|
89
|
+
# Uses @_form_builder set by FormComponent automatically.
|
|
90
|
+
#
|
|
91
|
+
# Form(model: @note, url: note_path(@note)) {
|
|
92
|
+
# MilkdownEditor(:content)
|
|
93
|
+
# }
|
|
94
|
+
#
|
|
95
|
+
def MilkdownEditor(method, **options)
|
|
96
|
+
output_buffer << milkdown_editor_field(@_form_builder, method, **options)
|
|
97
|
+
end
|
|
87
98
|
end
|
|
88
99
|
end
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
// data-milkdown-engine--editor-readonly-value="false">
|
|
14
14
|
// <input type="hidden"
|
|
15
15
|
// data-milkdown-engine--editor-target="input"
|
|
16
|
-
// name="
|
|
16
|
+
// name="document[content]"
|
|
17
17
|
// value='{"type":"doc","content":[]}'>
|
|
18
18
|
// <div data-milkdown-engine--editor-target="editor"></div>
|
|
19
19
|
// </div>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["item"]
|
|
5
|
+
static values = { selectedId: { type: Number, default: 0 } }
|
|
6
|
+
|
|
7
|
+
connect() {
|
|
8
|
+
this.#applySelected()
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
select(event) {
|
|
12
|
+
const card = event.currentTarget.querySelector("[data-note-id]")
|
|
13
|
+
|| event.target.closest("[data-note-id]")
|
|
14
|
+
if (card) {
|
|
15
|
+
this.selectedIdValue = parseInt(card.dataset.noteId, 10) || 0
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
selectedIdValueChanged() {
|
|
20
|
+
this.#applySelected()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
#applySelected() {
|
|
24
|
+
this.itemTargets.forEach((item) => {
|
|
25
|
+
const id = parseInt(item.dataset.noteId, 10) || 0
|
|
26
|
+
item.classList.toggle("active", id === this.selectedIdValue)
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module MilkdownEngine
|
|
4
|
-
class
|
|
4
|
+
class Document < ApplicationRecord
|
|
5
5
|
ChecklistItem = Data.define(:text, :checked)
|
|
6
6
|
Heading = Data.define(:level, :id, :text)
|
|
7
7
|
|
|
8
|
+
acts_as_taggable_on :tags
|
|
9
|
+
|
|
8
10
|
validates :content, presence: true
|
|
9
11
|
|
|
12
|
+
def self.ransackable_attributes(auth_object = nil)
|
|
13
|
+
%w[title created_at updated_at]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.ransackable_associations(auth_object = nil)
|
|
17
|
+
%w[tags taggings]
|
|
18
|
+
end
|
|
19
|
+
|
|
10
20
|
before_save :set_title_from_content, if: -> { title.blank? && content_changed? }
|
|
11
21
|
|
|
12
22
|
# ── JSON query helpers (PostgreSQL jsonb_path_query) ─────────────────
|
|
@@ -24,7 +34,7 @@ module MilkdownEngine
|
|
|
24
34
|
(item #>> '{attrs,checked}')::boolean AS checked,
|
|
25
35
|
string_agg(text_node #>> '{text}', '' ORDER BY text_ord) AS item_text
|
|
26
36
|
FROM jsonb_path_query(
|
|
27
|
-
(SELECT content FROM
|
|
37
|
+
(SELECT content FROM milkdown_engine_documents WHERE id = :id),
|
|
28
38
|
'strict $.** ? (@.type == "list_item" && @.attrs.checked != null)'
|
|
29
39
|
) WITH ORDINALITY AS items(item, item_ord),
|
|
30
40
|
jsonb_path_query(item, 'strict $.** ? (@.type == "text")') WITH ORDINALITY AS texts(text_node, text_ord)
|
|
@@ -46,7 +56,7 @@ module MilkdownEngine
|
|
|
46
56
|
heading #>> '{attrs,id}' AS id,
|
|
47
57
|
string_agg(text_node #>> '{text}', '' ORDER BY text_ord) AS heading_text
|
|
48
58
|
FROM jsonb_path_query(
|
|
49
|
-
(SELECT content FROM
|
|
59
|
+
(SELECT content FROM milkdown_engine_documents WHERE id = :id),
|
|
50
60
|
'strict $.** ? (@.type == "heading")'
|
|
51
61
|
) WITH ORDINALITY AS headings(heading, heading_ord),
|
|
52
62
|
jsonb_path_query(heading, 'strict $.** ? (@.type == "text")') WITH ORDINALITY AS texts(text_node, text_ord)
|
|
@@ -69,8 +79,10 @@ module MilkdownEngine
|
|
|
69
79
|
#
|
|
70
80
|
# Instead we interpolate the quoted id via +:id+ and +connection.quote+.
|
|
71
81
|
def exec_jsonb_query(sql)
|
|
72
|
-
|
|
73
|
-
|
|
82
|
+
self.class.with_connection do |conn|
|
|
83
|
+
quoted = sql.gsub(":id", conn.quote(id))
|
|
84
|
+
conn.select_all(quoted)
|
|
85
|
+
end
|
|
74
86
|
end
|
|
75
87
|
|
|
76
88
|
# Walk the ProseMirror JSON tree in Ruby to find the first heading's text.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
output_buffer << form_with(model: note, url: note.persisted? ? note_path(note) : notes_path, html: { class: "ui form" }) { |f|
|
|
2
|
+
if note.errors.any?
|
|
3
|
+
Message(type: :error) { |c|
|
|
4
|
+
c.header { text pluralize(note.errors.count, "error") + " prohibited this note from being saved" }
|
|
5
|
+
note.errors.full_messages.each { |msg| output_buffer << tag.p(msg) }
|
|
6
|
+
}
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
output_buffer << tag.div(class: "field") {
|
|
10
|
+
safe_join([
|
|
11
|
+
f.label(:title),
|
|
12
|
+
tag.input(type: "text", name: "document[title]", value: note.title, placeholder: "Auto-filled from first heading if blank")
|
|
13
|
+
])
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
output_buffer << tag.div(class: "field") {
|
|
17
|
+
safe_join([
|
|
18
|
+
tag.label("Content"),
|
|
19
|
+
milkdown_editor_field(f, :content)
|
|
20
|
+
])
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
Divider(hidden: true)
|
|
24
|
+
Button(variant: :primary, type: :submit, icon: "save") {
|
|
25
|
+
text note.persisted? ? "Update note" : "Create note"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Wrapper(id: "item-list-panel", data: { turbo_permanent: true, controller: "milkdown-engine--item-list", "milkdown-engine--item-list-selected-id-value": (defined?(@note) && @note&.persisted? ? @note.id : 0) }) {
|
|
2
|
+
Wrapper(id: "item-list-header") {
|
|
3
|
+
HStack(spacing: 8, justify: "between", align: "center") {
|
|
4
|
+
Header(size: :h3) { text "Notes" }
|
|
5
|
+
Button(href: new_note_path, size: :mini, icon: "plus", variant: :primary)
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
Wrapper(id: "item-list-search") {
|
|
10
|
+
output_buffer << search_form_for(@q, url: notes_path, html: { class: "ui form", data: { turbo_frame: "notes-list", controller: "milkdown-engine--auto-submit", action: "input->milkdown-engine--auto-submit#submit" } }) { |f|
|
|
11
|
+
Input(fluid: true, icon: "search", placeholder: "Search…", name: "q[title_cont]", value: params.dig(:q, :title_cont))
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
output_buffer << turbo_frame_tag("notes-list") {
|
|
16
|
+
Wrapper(id: "item-list-scroll") {
|
|
17
|
+
@notes.each do |note|
|
|
18
|
+
Partial("note_card", note: note)
|
|
19
|
+
end
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
Wrapper(id: "navigation-rail") {
|
|
2
|
+
Wrapper(id: "navigation-rail-search") {
|
|
3
|
+
Input(fluid: true, icon: "search", placeholder: "Search tags…")
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
Wrapper(html_class: "rail-section-label") { text "Views" }
|
|
7
|
+
|
|
8
|
+
Wrapper(id: "navigation-rail-views") {
|
|
9
|
+
Menu(vertical: true) {
|
|
10
|
+
MenuItem(active: true, icon: "list") { text "Notes" }
|
|
11
|
+
MenuItem(icon: "folder") { text "Files" }
|
|
12
|
+
MenuItem(icon: "star") { text "Starred" }
|
|
13
|
+
MenuItem(icon: "archive") { text "Archived" }
|
|
14
|
+
MenuItem(icon: "trash") { text "Trash" }
|
|
15
|
+
MenuItem(icon: "tag") { text "Untagged" }
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
Wrapper(html_class: "rail-section-label") { text "Tags" }
|
|
20
|
+
|
|
21
|
+
Wrapper(id: "navigation-rail-tags") {
|
|
22
|
+
Menu(vertical: true) {
|
|
23
|
+
MenuItem(icon: "hashtag") { text "construction" }
|
|
24
|
+
MenuItem(icon: "hashtag") { text "clients" }
|
|
25
|
+
MenuItem(icon: "hashtag") { text "invoices" }
|
|
26
|
+
MenuItem(icon: "hashtag") { text "site-visits" }
|
|
27
|
+
MenuItem(icon: "hashtag") { text "materials" }
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Link(href: edit_note_path(note), data: { turbo_frame: "_top", action: "click->milkdown-engine--item-list#select" }) {
|
|
2
|
+
Wrapper(html_class: "note-card", data: { "milkdown-engine--item-list-target": "item", note_id: note.id }) {
|
|
3
|
+
Wrapper(html_class: "note-card-title") { text note.title.presence || "Untitled" }
|
|
4
|
+
Wrapper(html_class: "note-card-meta") { text note.updated_at.strftime("%A, %d %b %Y") }
|
|
5
|
+
note.tag_list.each do |t|
|
|
6
|
+
output_buffer << tag.span(t, class: "note-card-tag")
|
|
7
|
+
end
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
Form(model: note, url: note.persisted? ? note_path(note) : notes_path, data: { controller: "milkdown-engine--auto-submit", action: "input->milkdown-engine--auto-submit#submit" }) {
|
|
2
|
+
Input(transparent: true, fluid: true, name: "document[title]", value: note.title, placeholder: "Untitled")
|
|
3
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Wrapper(id: "md-app") {
|
|
2
|
+
Partial("milkdown_engine/notes/item_list_panel")
|
|
3
|
+
Partial("milkdown_engine/notes/content_editor") {
|
|
4
|
+
Wrapper(html_class: "editor-title-bar") {
|
|
5
|
+
Partial("title_form", note: @note)
|
|
6
|
+
}
|
|
7
|
+
Wrapper(html_class: "editor-content") {
|
|
8
|
+
Partial("content_form", note: @note)
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Wrapper(id: "md-app") {
|
|
2
|
+
Partial("milkdown_engine/notes/item_list_panel")
|
|
3
|
+
Partial("milkdown_engine/notes/content_editor") {
|
|
4
|
+
Wrapper(html_class: "editor-title-bar") {
|
|
5
|
+
Partial("title_form", note: @note)
|
|
6
|
+
}
|
|
7
|
+
Wrapper(html_class: "editor-content") {
|
|
8
|
+
Partial("content_form", note: @note)
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Wrapper(id: "md-app") {
|
|
2
|
+
Partial("milkdown_engine/notes/item_list_panel")
|
|
3
|
+
Partial("milkdown_engine/notes/content_editor") {
|
|
4
|
+
Wrapper(html_class: "editor-title-bar") {
|
|
5
|
+
Partial("title_form", note: @note)
|
|
6
|
+
}
|
|
7
|
+
Wrapper(html_class: "editor-content") {
|
|
8
|
+
Partial("content_form", note: @note)
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Wrapper(id: "md-app") {
|
|
2
|
+
Partial("milkdown_engine/notes/item_list_panel")
|
|
3
|
+
Partial("milkdown_engine/notes/content_editor") {
|
|
4
|
+
Wrapper(html_class: "editor-title-bar") {
|
|
5
|
+
Partial("title_form", note: @note)
|
|
6
|
+
}
|
|
7
|
+
Wrapper(html_class: "editor-content") {
|
|
8
|
+
Partial("content_form", note: @note)
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
data/config/importmap.rb
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
# Vendored Milkdown bundle (built by esbuild from src/)
|
|
4
4
|
pin "milkdown_engine/milkdown", to: "milkdown_engine/milkdown.min.js"
|
|
5
5
|
|
|
6
|
+
# Vendored dependencies
|
|
7
|
+
pin "@stimulus-components/auto-submit", to: "@stimulus-components--auto-submit.js"
|
|
8
|
+
|
|
6
9
|
# MilkdownEngine Stimulus controllers
|
|
7
10
|
pin_all_from MilkdownEngine::Engine.root.join("app/javascript/milkdown_engine/controllers"),
|
|
8
11
|
under: "controllers/milkdown_engine", to: "milkdown_engine/controllers"
|
data/config/routes.rb
CHANGED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Consolidated migration for acts-as-taggable-on (v13).
|
|
4
|
+
# Creates both `tags` and `taggings` tables with all required indexes.
|
|
5
|
+
|
|
6
|
+
class CreateActsAsTaggableOnTables < ActiveRecord::Migration[8.1]
|
|
7
|
+
def change
|
|
8
|
+
create_table :tags do |t|
|
|
9
|
+
t.string :name, null: false
|
|
10
|
+
t.integer :taggings_count, default: 0
|
|
11
|
+
t.timestamps
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
add_index :tags, :name, unique: true
|
|
15
|
+
|
|
16
|
+
create_table :taggings do |t|
|
|
17
|
+
t.references :tag, null: false, foreign_key: true
|
|
18
|
+
t.references :taggable, polymorphic: true
|
|
19
|
+
t.references :tagger, polymorphic: true
|
|
20
|
+
t.string :context, limit: 128
|
|
21
|
+
t.datetime :created_at
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
add_index :taggings,
|
|
25
|
+
%i[tag_id taggable_id taggable_type context tagger_id tagger_type],
|
|
26
|
+
unique: true, name: "taggings_idx"
|
|
27
|
+
|
|
28
|
+
add_index :taggings,
|
|
29
|
+
%i[taggable_id taggable_type context],
|
|
30
|
+
name: "taggings_taggable_context_idx"
|
|
31
|
+
|
|
32
|
+
add_index :taggings, %i[tagger_id tagger_type]
|
|
33
|
+
|
|
34
|
+
add_index :taggings,
|
|
35
|
+
%i[taggable_id taggable_type tagger_id context],
|
|
36
|
+
name: "taggings_idy"
|
|
37
|
+
|
|
38
|
+
add_index :taggings, :context
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -6,6 +6,11 @@ module MilkdownEngine
|
|
|
6
6
|
class Engine < ::Rails::Engine
|
|
7
7
|
isolate_namespace MilkdownEngine
|
|
8
8
|
|
|
9
|
+
initializer "milkdown_engine.dependencies" do
|
|
10
|
+
require "acts-as-taggable-on"
|
|
11
|
+
require "ransack"
|
|
12
|
+
end
|
|
13
|
+
|
|
9
14
|
# Register importmap pins for Stimulus controllers
|
|
10
15
|
initializer "milkdown_engine.importmap", before: "importmap" do |app|
|
|
11
16
|
if app.config.respond_to?(:importmap)
|
|
@@ -23,6 +28,9 @@ module MilkdownEngine
|
|
|
23
28
|
# importmap pin_all_from URLs resolve through the asset pipeline.
|
|
24
29
|
app.config.assets.paths << Engine.root.join("app/javascript")
|
|
25
30
|
|
|
31
|
+
# Vendored JS packages
|
|
32
|
+
app.config.assets.paths << Engine.root.join("vendor/javascript")
|
|
33
|
+
|
|
26
34
|
# rails-active-ui ships stylesheets.css directly in app/assets/
|
|
27
35
|
app.config.assets.paths << Ui::Engine.root.join("app/assets")
|
|
28
36
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: milkdown_engine
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nathan Kidd
|
|
@@ -65,6 +65,34 @@ dependencies:
|
|
|
65
65
|
- - ">="
|
|
66
66
|
- !ruby/object:Gem::Version
|
|
67
67
|
version: '0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: acts-as-taggable-on
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '13.0'
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '13.0'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: ransack
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '0'
|
|
89
|
+
type: :runtime
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '0'
|
|
68
96
|
description: Provides a Milkdown-based rich markdown editor as a Rails engine with
|
|
69
97
|
Stimulus controllers, importmap integration, and rails-active-ui components.
|
|
70
98
|
email:
|
|
@@ -140,13 +168,29 @@ files:
|
|
|
140
168
|
- app/assets/stylesheets/milkdown_engine/fonts/KaTeX_Typewriter-Regular.woff2
|
|
141
169
|
- app/assets/stylesheets/milkdown_engine/milkdown.css
|
|
142
170
|
- app/controllers/milkdown_engine/application_controller.rb
|
|
171
|
+
- app/controllers/milkdown_engine/notes_controller.rb
|
|
143
172
|
- app/helpers/milkdown_engine/application_helper.rb
|
|
173
|
+
- app/javascript/milkdown_engine/controllers/auto_submit_controller.js
|
|
144
174
|
- app/javascript/milkdown_engine/controllers/editor_controller.js
|
|
175
|
+
- app/javascript/milkdown_engine/controllers/item_list_controller.js
|
|
145
176
|
- app/models/milkdown_engine/application_record.rb
|
|
146
|
-
- app/models/milkdown_engine/
|
|
177
|
+
- app/models/milkdown_engine/document.rb
|
|
178
|
+
- app/views/milkdown_engine/notes/_content_editor.html.ruby
|
|
179
|
+
- app/views/milkdown_engine/notes/_content_form.html.ruby
|
|
180
|
+
- app/views/milkdown_engine/notes/_form.html.ruby
|
|
181
|
+
- app/views/milkdown_engine/notes/_item_list_panel.html.ruby
|
|
182
|
+
- app/views/milkdown_engine/notes/_navigation_rail.html.ruby
|
|
183
|
+
- app/views/milkdown_engine/notes/_note_card.html.ruby
|
|
184
|
+
- app/views/milkdown_engine/notes/_title_form.html.ruby
|
|
185
|
+
- app/views/milkdown_engine/notes/edit.html.ruby
|
|
186
|
+
- app/views/milkdown_engine/notes/index.html.ruby
|
|
187
|
+
- app/views/milkdown_engine/notes/new.html.ruby
|
|
188
|
+
- app/views/milkdown_engine/notes/show.html.ruby
|
|
147
189
|
- config/importmap.rb
|
|
148
190
|
- config/routes.rb
|
|
149
191
|
- db/migrate/20260325000000_create_milkdown_engine_md_documents.rb
|
|
192
|
+
- db/migrate/20260327000000_create_acts_as_taggable_on_tables.rb
|
|
193
|
+
- db/migrate/20260327100000_rename_md_documents_to_documents.rb
|
|
150
194
|
- lib/milkdown_engine.rb
|
|
151
195
|
- lib/milkdown_engine/engine.rb
|
|
152
196
|
- lib/milkdown_engine/version.rb
|