finch-markdown-editor 0.1.9 → 0.2.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/dist/panel.css ADDED
@@ -0,0 +1,207 @@
1
+ :root{
2
+ --bg:var(--finch-bg-root,#f7f7f8);
3
+ --card:var(--finch-bg-elevated,#fff);
4
+ --text:var(--finch-text-primary,#1d1d20);
5
+ --muted:var(--finch-text-secondary,#777);
6
+ --border:var(--finch-border,#ddd);
7
+ --accent:var(--finch-accent,#3f6b56);
8
+ }
9
+ @media (prefers-color-scheme: dark) {
10
+ :root{
11
+ --bg:var(--finch-bg-root,#17171a);
12
+ --card:var(--finch-bg-elevated,#242429);
13
+ --text:var(--finch-text-primary,#eee);
14
+ --muted:var(--finch-text-secondary,#aaa);
15
+ --border:var(--finch-border,#3c3c43);
16
+ --accent:var(--finch-accent,#70a889);
17
+ }
18
+ }
19
+ *{box-sizing:border-box}
20
+ html,body{height:100%}
21
+ body{margin:0;background:var(--bg);color:var(--text);font:13px var(--finch-font-body,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif)}
22
+ .app{width:100%;height:100vh;display:flex;flex-direction:column}
23
+ .stage{position:relative;flex:1;min-height:0}
24
+ .pane{position:absolute;inset:0;display:none}
25
+ .pane.active{display:block}
26
+ .editor-wrap{position:relative;width:100%;height:100%;overflow:hidden;background:var(--card)}
27
+ #editor{width:100%;height:100%;overflow:hidden}
28
+ .fake-sel{position:fixed;z-index:15;border-radius:2px;background:rgba(63,107,86,.35);background:color-mix(in srgb,var(--accent) 34%,transparent);pointer-events:none}
29
+ .preview-shell{position:absolute;inset:0;padding:32px 20px 20px;background:var(--bg);overflow:hidden}
30
+ #article{display:block;width:100%;height:100%;border:0;border-radius:10px;background:var(--card)}
31
+ /* Home / empty state: a launch page rather than a placeholder — headline plus
32
+ the Markdown files this editor recently touched in the current working
33
+ directory, newest-modified first, so a session can be resumed in one click. */
34
+ .empty{position:absolute;z-index:5;inset:0;background:var(--card);color:var(--muted);display:block;overflow:auto;padding:30px 26px 34px;font-size:14px;line-height:1.7}
35
+ .empty[hidden]{display:none}
36
+ .home{max-width:860px;margin:0 auto}
37
+ .home-head{display:flex;align-items:flex-end;justify-content:space-between;gap:18px;margin-bottom:22px}
38
+ .home-head h1{margin:0 0 3px;font-size:19px;font-weight:650;line-height:1.3;color:var(--text);letter-spacing:-.01em}
39
+ .home-head p{margin:0;font-size:12.5px;line-height:1.5;color:var(--muted)}
40
+ .home-cta{display:flex;gap:8px;flex:none}
41
+ .home-btn{appearance:none;display:inline-flex;align-items:center;gap:5px;border:1px solid var(--accent);border-radius:8px;background:var(--accent);color:#fff;padding:7px 12px;font:inherit;font-size:13px;font-weight:600;cursor:pointer;white-space:nowrap}
42
+ .home-btn:hover{filter:brightness(1.06)}
43
+ .home-btn.ghost{background:transparent;border-color:var(--border);color:var(--text);font-weight:500}
44
+ .home-btn.ghost:hover{filter:none;background:var(--bg)}
45
+ .home-btn svg{width:14px;height:14px;flex:none}
46
+ .home-section{display:flex;align-items:baseline;justify-content:space-between;gap:8px;margin:0 0 10px;font-size:12px;color:var(--muted)}
47
+ .home-section b{font-size:12.5px;font-weight:600;color:var(--text);flex:none}
48
+ /* flex:0 1 auto (not flex:1) so the button hugs its own text — the blank
49
+ space next to a short path stays unclickable. Front-truncation (the "…"
50
+ prefix) happens in JS, not via a direction:rtl CSS trick: mixing a plain
51
+ "~" with rtl reordered the whole string visually ("finch-tools/~" instead
52
+ of "~/finch-tools"), so this box now renders left-to-right like normal. */
53
+ .home-section .home-cwd{flex:0 1 auto;max-width:100%;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;opacity:.75;cursor:pointer;background:none;border:0;padding:0;font:inherit;color:inherit}
54
+ .home-section .home-cwd:hover{opacity:1;color:var(--accent);text-decoration:underline}
55
+ .home-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(196px,1fr));gap:12px}
56
+ .doc-card{position:relative;display:flex;flex-direction:column;height:138px;padding:0;text-align:left;border:1px solid var(--border);border-radius:11px;background:var(--bg);color:var(--text);font:inherit;cursor:pointer;overflow:hidden;transition:border-color .15s ease,box-shadow .15s ease,transform .1s ease}
57
+ .doc-card:hover{border-color:var(--accent);box-shadow:0 5px 16px rgba(0,0,0,.1)}
58
+ .doc-card:active{transform:scale(.99)}
59
+ .doc-preview{position:relative;flex:1;min-height:0;padding:11px 12px 0;font-size:11.5px;line-height:1.6;color:var(--muted);overflow:hidden;word-break:break-word}
60
+ .doc-preview::after{content:'';position:absolute;left:0;right:0;bottom:0;height:34px;background:linear-gradient(to bottom,transparent,var(--bg))}
61
+ .doc-foot{flex:none;padding:8px 12px 10px;border-top:1px solid var(--border);background:var(--card)}
62
+ .doc-title{font-size:13px;font-weight:600;line-height:1.35;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
63
+ .doc-sub{margin-top:2px;display:flex;gap:6px;font-size:11px;line-height:1.4;color:var(--muted)}
64
+ /* Front-truncated in JS (see elideFrontAll) rather than via CSS
65
+ direction:rtl trick: that trick reorders per Unicode bidi rules on a
66
+ per-run basis, and a plain LTR path (Latin text + weakly-directional
67
+ `/` separators) doesn't reliably get its ellipsis pinned to the front —
68
+ it was observed still cutting off the filename at the end instead.
69
+ Doing it in JS with real measured width is reliable regardless of
70
+ directionality. */
71
+ .doc-sub .doc-path{flex:1;min-width:0;overflow:hidden;white-space:nowrap}
72
+ .doc-sub .doc-time{flex:none;opacity:.8}
73
+ .doc-reveal{position:absolute;top:6px;right:6px;z-index:1;display:flex;align-items:center;justify-content:center;width:24px;height:24px;padding:0;border:none;border-radius:7px;background:color-mix(in srgb, var(--bg) 72%, transparent);color:var(--muted);cursor:pointer;opacity:0;transition:opacity .12s ease,background .12s ease,color .12s ease}
74
+ .doc-reveal svg{width:14px;height:14px}
75
+ .doc-card:hover .doc-reveal,.doc-reveal:focus-visible{opacity:1}
76
+ .doc-reveal:hover{background:var(--card);color:var(--text)}
77
+ .home-hint{margin:0;padding:22px 0;text-align:center;font-size:12.5px;color:var(--muted)}
78
+ .home-hint[hidden],.home-recent[hidden]{display:none}
79
+ .status{font-size:12px;color:var(--muted);padding:5px 10px;text-align:center;border-top:1px solid var(--border);background:var(--bg)}
80
+ .status a{color:var(--accent);text-decoration:underline;text-underline-offset:2px;cursor:pointer}
81
+ .status.error{color:#c54232}
82
+ .status:empty{padding:0;border-top:0}
83
+ /* Changed glyphs briefly take the accent color, then settle back to normal
84
+ text. Nested syntax spans receive the same animation; no background flash. */
85
+ .cm-line.cm-ai-changed,.cm-line.cm-ai-changed *{animation:ai-edit-text-flash 1.9s cubic-bezier(.33,0,.2,1) both}
86
+ @keyframes ai-edit-text-flash{0%,26%{color:var(--accent)}100%{color:var(--text)}}
87
+ @media (prefers-reduced-motion:reduce){.cm-line.cm-ai-changed,.cm-line.cm-ai-changed *{animation:none}}
88
+ .popup{position:fixed;z-index:20;display:flex;align-items:center;gap:6px;background:var(--card);border:1px solid var(--border);border-radius:9px;box-shadow:0 8px 22px rgba(0,0,0,.22);padding:5px;transform-origin:center bottom;animation:popup-in .18s cubic-bezier(.2,.8,.2,1)}
89
+ .popup.hidden{display:none}
90
+ @keyframes popup-in{from{opacity:0;transform:translateY(5px) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}}
91
+ @media (prefers-reduced-motion:reduce){.popup{animation:none}}
92
+ .popup input{width:147px;border:0;outline:0;background:transparent;color:var(--text);font:inherit;padding:4px 6px}
93
+ .popup button{font:inherit;border:0;background:var(--accent);color:#fff;border-radius:6px;padding:2px 6px;cursor:pointer;white-space:nowrap}
94
+ .popup button:hover{opacity:.9}
95
+ /* The slot stays sized by the shortcut even after focus: hiding with
96
+ visibility (rather than display:none) preserves both the popup's width
97
+ and height. Send is absolutely centered over that reserved space, so it
98
+ never coexists visually with the hint. */
99
+ .popup-action-slot{position:relative;display:flex;align-items:center;justify-content:center;min-height:28px}
100
+ .popup-shortcut{display:flex;align-items:center;gap:3px;padding:0 6px;color:var(--muted);white-space:nowrap;cursor:default;font-size:11px}
101
+ .popup-shortcut kbd{font:inherit;font-size:11px}
102
+ .popup.focused .popup-shortcut{visibility:hidden}
103
+ .popup:not(.focused) .popup-send{visibility:hidden;pointer-events:none}
104
+ /* Round send button, icon-only, centered inside the permanently reserved
105
+ shortcut slot. */
106
+ .popup-send{position:absolute;inset:0;margin:auto;margin-right:1px;width:28px;height:28px;padding:0;border-radius:999px;display:flex;align-items:center;justify-content:center}
107
+ .popup-send svg{width:18px;height:18px}
108
+ /* Inline prompt bar: the blank line the writer pressed space on turns into
109
+ a full-width input. Width comes from JS (the editor's text column), so the
110
+ input just needs to fill whatever it is given. */
111
+ .popup.popup-bar{border-radius:8px;padding:5px 6px;box-shadow:0 4px 16px rgba(0,0,0,.18)}
112
+ .popup.popup-bar input{flex:1;width:auto;white-space:nowrap;text-overflow:ellipsis}
113
+ /* Unsaved-changes confirm, deliberately a plain in-page overlay rather than
114
+ `window.finch.ui.confirm()`: the only entry point that can trigger it is
115
+ the toolbar's "home" button, which reaches this page as a `finch:menu`
116
+ postMessage — not a real user gesture — so the same Bridge action that
117
+ requires one (see `.actions` comment above) could not be trusted to fire
118
+ reliably from there. A plain DOM overlay has no such gesture requirement. */
119
+ .confirm-overlay{position:fixed;inset:0;z-index:30;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.32);animation:confirm-fade .12s ease}
120
+ .confirm-overlay[hidden]{display:none}
121
+ @keyframes confirm-fade{from{opacity:0}to{opacity:1}}
122
+ .confirm-card{width:300px;max-width:calc(100% - 40px);background:var(--card);border:1px solid var(--border);border-radius:12px;box-shadow:0 16px 40px rgba(0,0,0,.3);padding:16px;animation:confirm-in .16s cubic-bezier(.2,.8,.2,1)}
123
+ @keyframes confirm-in{from{opacity:0;transform:translateY(6px) scale(.97)}to{opacity:1;transform:translateY(0) scale(1)}}
124
+ @media (prefers-reduced-motion:reduce){.confirm-overlay,.confirm-card{animation:none}}
125
+ .confirm-card p{margin:0 0 16px;font-size:13px;line-height:1.6;color:var(--text)}
126
+ .confirm-actions{display:flex;justify-content:flex-end;gap:8px}
127
+ .confirm-actions button{font:inherit;font-size:13px;border:1px solid var(--border);background:transparent;color:var(--text);border-radius:7px;padding:6px 12px;cursor:pointer}
128
+ .confirm-actions button:hover{background:var(--bg)}
129
+ .confirm-actions button.primary{border-color:var(--accent);background:var(--accent);color:#fff}
130
+ .confirm-actions button.primary:hover{filter:brightness(1.06)}
131
+ .confirm-actions button.ghost{border-color:transparent}
132
+ /* Floating copy/export bar, deliberately living INSIDE this page rather than
133
+ in the host toolbar: a host toolbar click arrives here as a postMessage,
134
+ which is not a user activation for this document, so the async Clipboard
135
+ API and execCommand('copy') both refuse to run until the user happens to
136
+ click the page first. A real in-page click is a genuine user gesture, so
137
+ every copy/export below works on the first try. */
138
+ .actions{position:absolute;right:32px;bottom:32px;z-index:8;display:flex;align-items:center;gap:3px;padding:5px;border-radius:11px;background:var(--card);border:1px solid var(--border);box-shadow:0 6px 20px rgba(0,0,0,.18);transition:gap .15s ease}
139
+ .actions[hidden]{display:none}
140
+ .actions button{position:relative;display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;padding:0;border:0;border-radius:7px;background:transparent;color:var(--text);font:inherit;cursor:pointer;white-space:nowrap}
141
+ .actions button:hover:not(:disabled){background:var(--finch-bg-hover,var(--bg))}
142
+ .actions button[data-tooltip]::after{content:attr(data-tooltip);position:absolute;left:50%;bottom:calc(100% + 8px);z-index:20;padding:6px 8px;border:1px solid var(--border);border-radius:7px;background:linear-gradient(135deg,color-mix(in srgb,var(--card) 96%,#fff),var(--bg));box-shadow:0 5px 16px rgba(0,0,0,.2);color:var(--text);font-size:12px;line-height:1;white-space:nowrap;pointer-events:none;opacity:0;transform:translate(-50%,3px);transition:opacity .16s ease,transform .16s ease}
143
+ .actions button[data-tooltip]:hover::after{opacity:1;transform:translate(-50%,0)}
144
+ /* Center tooltips over ordinary buttons; at either end of the floating bar,
145
+ grow them inward instead of letting a long label escape the panel. */
146
+ .actions button:first-child[data-tooltip]::after{left:0;transform:translateY(3px)}
147
+ .actions button:first-child[data-tooltip]:hover::after{transform:translateY(0)}
148
+ .actions button:last-child[data-tooltip]::after{left:auto;right:0;transform:translateY(3px)}
149
+ .actions button:last-child[data-tooltip]:hover::after{transform:translateY(0)}
150
+ .actions button:disabled{opacity:.45;cursor:default}
151
+ .actions .sep{width:1px;height:18px;background:var(--border);margin:0 3px}
152
+ .actions svg{width:16px;height:16px;flex:none}
153
+ .actions .act-toggle{padding:0 6px;color:var(--muted)}
154
+ .actions .act-toggle svg{transition:transform .15s ease;transform:rotate(180deg)}
155
+ /* Collapsed = only the WeChat-copy action (icon only, label hidden) plus the
156
+ toggle remain visible; every other button/separator is a one-off export
157
+ action that most sessions never touch, so hiding them by default keeps
158
+ the floating bar from covering preview content. */
159
+ .actions.collapsed [data-collapsible]{display:none}
160
+ .actions.collapsed #actCopyWx{padding:0 7px}
161
+ .actions.collapsed #actCopyWx .act-label{display:none}
162
+ .actions.collapsed .act-toggle svg{transform:rotate(0deg)}
163
+
164
+ /* Full-screen App View shell. The same document remains a compact, host-toolbar-
165
+ driven Panel App until finch:env explicitly identifies the appView surface. */
166
+ .app-toolbar{height:48px;flex:none;align-items:center;gap:12px;padding:7px 12px;border-bottom:1px solid var(--border);background:var(--card)}
167
+ body.app-view .app-toolbar{display:flex!important}
168
+ .app-toolbar-group{display:flex;align-items:center;gap:6px;flex:none}
169
+ .app-toolbar-end{margin-left:auto}
170
+ .app-tool{height:32px;border:1px solid transparent;border-radius:8px;background:transparent;color:var(--text);font:12px var(--finch-font-body,system-ui);cursor:pointer;display:inline-flex;align-items:center;justify-content:center;min-width:32px;padding:0 8px;gap:6px}
171
+ .app-tool svg{width:19px;height:19px;stroke-width:1.8}.app-tool:hover:not(:disabled){background:var(--finch-bg-hover,var(--bg))}
172
+ .app-tool:disabled{opacity:.4;cursor:default}.app-tool.checked{color:var(--text);background:var(--finch-bg-active,color-mix(in srgb,var(--text) 10%,transparent))}.app-tool.dirty{color:var(--accent);background:transparent}
173
+ .app-tool-label{min-width:auto;padding:0 9px}.app-menu-wrap{position:relative}.app-menu{position:absolute;z-index:32;top:calc(100% + 7px);right:0;min-width:174px;padding:5px;border:1px solid var(--border);border-radius:9px;background:var(--card);box-shadow:0 2px 8px rgba(0,0,0,.12)}
174
+ .app-menu button{display:flex;width:100%;align-items:center;justify-content:space-between;gap:12px;padding:7px 9px;border:0;border-radius:6px;background:transparent;color:var(--text);font:12px var(--finch-font-body,system-ui);text-align:left;cursor:pointer}.app-menu button:hover{background:var(--finch-bg-hover,var(--bg))}.app-menu button.checked{color:var(--text);background:var(--finch-bg-active,color-mix(in srgb,var(--text) 10%,transparent));font-weight:600}.app-menu hr{margin:5px 3px;border:0;border-top:1px solid var(--border)}
175
+ .app-document-meta{min-width:0;display:flex;flex-direction:column;gap:1px}.app-document-meta strong,.app-document-meta span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.app-document-meta strong{font-size:13px}.app-document-meta span{max-width:34vw;color:var(--muted);font-size:11px}
176
+ body.app-view .stage{display:grid;grid-template-columns:minmax(0,1fr) 6px var(--preview-width,480px);background:var(--bg)}
177
+ body.app-view .pane{position:relative;inset:auto;min-width:0;min-height:0}
178
+ body.app-view .pane.active{display:block}
179
+ body.app-view.has-document #editPane,body.app-view.has-document #previewPane{display:block}
180
+ body.app-view #editPane{border-right:1px solid var(--border)}
181
+ body.app-view .preview-shell{padding:20px;display:flex;justify-content:center;align-items:stretch}
182
+ body.app-view #article{width:100%;border:1px solid var(--border)}
183
+ body.app-view.preview-hidden .stage{grid-template-columns:minmax(0,1fr)}body.app-view.preview-hidden #previewPane,body.app-view.preview-hidden .preview-resizer{display:none!important}
184
+ @keyframes cm-ai-spin{to{transform:rotate(360deg)}}
185
+ .preview-resizer{position:relative;z-index:10;width:6px;cursor:col-resize;background:var(--border);opacity:0;transition:opacity .15s ease}.preview-resizer:hover,.preview-resizer.active{opacity:1}
186
+ .app-tool{position:relative}.app-tool[data-tooltip]::after{content:attr(data-tooltip);position:absolute;left:50%;top:calc(100% + 8px);z-index:40;padding:6px 8px;border:1px solid var(--border);border-radius:7px;background:var(--card);box-shadow:0 5px 16px rgba(0,0,0,.2);color:var(--text);font-size:12px;line-height:1;white-space:nowrap;pointer-events:none;opacity:0;transform:translate(-50%,-3px);transition:opacity .16s ease,transform .16s ease}.app-tool[data-tooltip]:hover::after{opacity:1;transform:translate(-50%,0)}
187
+ body.app-view .empty{grid-column:1/-1;background:var(--bg);padding:54px 38px}
188
+ body.app-view .home{max-width:1040px}body.app-view #emptyNew{display:none}
189
+ body.app-view .actions{right:24px;bottom:24px}
190
+ .library-backdrop{position:fixed;z-index:39;inset:0;background:rgba(0,0,0,.24)}
191
+ .library-drawer{position:fixed;z-index:40;top:0;bottom:0;left:0;width:min(390px,88vw);padding:0;background:var(--card);border-right:1px solid var(--border);transform:translateX(-102%);transition:transform .18s ease;display:flex;flex-direction:column}
192
+ .library-drawer.open{transform:translateX(0)}.library-drawer>header{height:58px;display:flex;align-items:center;justify-content:space-between;padding:10px 14px;border-bottom:1px solid var(--border)}
193
+ .library-drawer>header div{display:flex;flex-direction:column}.library-drawer>header span{font-size:11px;color:var(--muted)}.library-drawer>header button{border:0;background:transparent;color:var(--muted);font-size:24px;cursor:pointer}
194
+ /* Directory-like library: every source group is a native collapsible
195
+ details/summary, while rows separate a readable title from the quieter
196
+ path + time metadata instead of cramming all three into one text run. */
197
+ .library-groups{flex:1;overflow:auto;padding:10px 12px 20px}
198
+ .library-group{margin:0 0 8px;border:0;border-radius:9px}.library-group[open]{background:color-mix(in srgb,var(--text) 2%,transparent)}
199
+ .library-group summary{display:flex;align-items:center;gap:7px;min-height:32px;padding:0 9px;list-style:none;color:var(--muted);font-size:12px;font-weight:600;cursor:pointer;user-select:none;touch-action:none}.library-group summary::-webkit-details-marker{display:none}.library-group summary::before{content:'›';color:var(--muted);font-size:17px;font-weight:400;line-height:1;transform:rotate(0deg);transition:transform .14s ease}.library-group[open] summary::before{transform:rotate(90deg)}.library-group summary:hover{background:var(--finch-bg-hover,var(--bg));border-radius:8px}.library-group[data-draggable="true"] summary{cursor:grab}.library-group.dragging{opacity:.32}
200
+ .library-group-title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--muted)}.library-group-count{margin-left:auto;color:var(--muted);font-size:11px;font-weight:400}.library-items{padding:0 4px 4px}
201
+ /* Custom drag ghost: a floating clone that follows the pointer, replacing
202
+ the browser's native (unstylable) HTML5 drag screenshot. Collapsed
203
+ (.library-items hidden) since drags always start from a collapsed row. */
204
+ .library-drag-ghost{position:fixed;left:0;top:0;z-index:9999;pointer-events:none;margin:0;border-radius:10px;background:var(--card,var(--bg));box-shadow:0 10px 28px color-mix(in srgb,#000 22%,transparent),0 2px 6px color-mix(in srgb,#000 12%,transparent);transform:scale(1.02) rotate(-1deg);opacity:.96;overflow:hidden}
205
+ .library-drag-ghost .library-items{display:none}.library-drag-ghost summary{cursor:grabbing}
206
+ .library-row{display:block;width:100%;padding:9px 9px 8px;border:0;border-radius:7px;background:transparent;color:var(--text);text-align:left;cursor:pointer}.library-row:hover{background:var(--finch-bg-hover,var(--bg))}.library-row-main,.library-row strong,.library-row-meta,.library-row-path{display:block;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.library-row strong{font-size:13px;line-height:1.35;font-weight:600}.library-row-meta{display:flex;align-items:center;gap:7px;margin-top:3px;color:var(--muted);font-size:11px;line-height:1.3}.library-row-path,.library-row time{color:var(--muted)}.library-row-path{flex:1}.library-row time{flex:none;opacity:.86}
207
+ @media(max-width:850px){body.app-view .stage{grid-template-columns:minmax(0,1fr) 6px min(var(--preview-width,480px),40vw)}.app-document-meta span{display:none}.app-toolbar{gap:6px}.app-tool-label{padding:0 8px}}