|
5 | 5 | <meta charset="UTF-8"> |
6 | 6 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
7 | 7 | <link rel="icon" |
8 | | - href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect width='100' height='100' fill='%23e0e0e0' rx='16'/><text x='50%' y='45%' dominant-baseline='central' text-anchor='middle' font-size='85' font-family='Arial' font-weight='bold' fill='%231e1e1e'>白</text></svg>"> |
| 8 | + href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect width='100' height='100' fill='%23edece7' rx='16'/><text x='50%' y='47%' dominant-baseline='central' text-anchor='middle' font-size='85' font-family='Arial' font-weight='bold' fill='%2317140c'>白</text></svg>"> |
9 | 9 | <title>Shiro</title> |
10 | 10 | <meta name="description" content="A clean, distraction-free text editor for focused writing."> |
11 | 11 | <script> |
|
23 | 23 | } |
24 | 24 |
|
25 | 25 | :root { |
26 | | - --bg: #130f0c; |
27 | | - --text: #dfe4e2; |
28 | | - --placeholder: #7b807a; |
| 26 | + --bg: #17140c; |
| 27 | + --text: #edece7; |
| 28 | + --placeholder: rgb(from var(--text) r g b / 0.5); |
29 | 29 | --transition: 0.3s ease; |
30 | | - --hover-accent: #b9391b; |
| 30 | + --hover-accent: #c84733; |
| 31 | + --caret: rgba(from var(--hover-accent) r g b / 0.9); |
31 | 32 | } |
32 | 33 |
|
33 | 34 | [data-theme="light"] { |
34 | | - --bg: #dfe4e2; |
35 | | - --text: #130f0c; |
36 | | - --placeholder: #7b807a; |
| 35 | + --bg: #edece7; |
| 36 | + --text: #17140c; |
| 37 | + --placeholder: rgb(from var(--text) r g b / 0.5); |
| 38 | + --caret: var(--hover-accent); |
37 | 39 | } |
38 | 40 |
|
39 | 41 | body { |
40 | 42 | margin: 0; |
41 | | - font: 18px/1.6 "Monaco", "Courier New", monospace; |
| 43 | + font: 18px/1.6 "Menlo", "DejaVu Sans Mono", "Consolas", monospace; |
42 | 44 | background: var(--bg); |
43 | 45 | color: var(--text); |
44 | 46 | height: 100vh; |
|
52 | 54 | #editor, |
53 | 55 | .btn, |
54 | 56 | #editor::placeholder { |
55 | | - transition: background var(--transition), color var(--transition); |
| 57 | + transition: background var(--transition), color var(--transition), opacity var(--transition); |
56 | 58 | } |
57 | 59 |
|
58 | 60 | #count, |
59 | 61 | [data-action="theme"] { |
60 | | - transition: opacity var(--transition); |
| 62 | + transition: opacity var(--transition), color var(--transition); |
61 | 63 | } |
62 | 64 |
|
63 | 65 | .controls { |
64 | 66 | display: flex; |
65 | 67 | justify-content: space-between; |
66 | 68 | padding: 18px 20px; |
67 | 69 | gap: 20px; |
| 70 | + transition: opacity var(--transition); |
68 | 71 | } |
69 | 72 |
|
70 | 73 | .controls>div { |
|
84 | 87 | color: var(--hover-accent); |
85 | 88 | } |
86 | 89 |
|
87 | | - .controls.hidden .btn { |
| 90 | + .controls.hidden { |
88 | 91 | opacity: 0; |
| 92 | + pointer-events: none; |
89 | 93 | } |
90 | 94 |
|
91 | 95 | main { |
|
108 | 112 | border: none; |
109 | 113 | outline: none; |
110 | 114 | resize: none; |
111 | | - transition: opacity var(--transition); |
112 | 115 | tab-size: 4; |
113 | | - box-sizing: border-box; |
| 116 | + caret-color: var(--caret); |
114 | 117 | } |
115 | 118 |
|
116 | 119 | #editor::placeholder { |
117 | 120 | color: var(--placeholder); |
118 | 121 | } |
119 | 122 |
|
120 | 123 | ::selection { |
121 | | - background-color: rgba(185, 57, 27, 0.5); |
| 124 | + background-color: rgba(from var(--hover-accent) r g b / 0.5); |
122 | 125 | color: inherit; |
123 | 126 | } |
124 | 127 |
|
125 | 128 | .scroll-fade { |
126 | 129 | position: absolute; |
127 | | - left: 0; |
128 | | - width: 100%; |
129 | | - height: 100px; |
| 130 | + inset-inline: clamp(20px, 10vw, 20vh); |
| 131 | + height: 80px; |
130 | 132 | pointer-events: none; |
131 | | - transition: opacity 0.3s ease; |
| 133 | + transition: opacity var(--transition), background-color var(--transition); |
132 | 134 | z-index: 1; |
| 135 | + background-color: var(--bg); |
133 | 136 | } |
134 | 137 |
|
135 | 138 | .scroll-fade.top { |
136 | 139 | top: 0; |
137 | | - background: linear-gradient(to bottom, var(--bg) 50%, transparent 100%); |
| 140 | + mask-image: linear-gradient(to bottom, black 50%, transparent 100%); |
| 141 | + -webkit-mask-image: linear-gradient(to bottom, black 50%, transparent 100%); |
138 | 142 | } |
139 | 143 |
|
140 | 144 | .scroll-fade.bottom { |
141 | 145 | bottom: 0; |
142 | | - background: linear-gradient(to top, var(--bg) 50%, transparent 100%); |
| 146 | + mask-image: linear-gradient(to top, black 50%, transparent 100%); |
| 147 | + -webkit-mask-image: linear-gradient(to top, black 50%, transparent 100%); |
143 | 148 | } |
144 | 149 | </style> |
145 | 150 | </head> |
|
151 | 156 | <span class="btn" data-action="clear">Clear</span> |
152 | 157 | </div> |
153 | 158 | <div> |
154 | | - <span class="btn" data-action="fullscreen">Fullscreen</span> |
155 | 159 | <span class="btn" data-action="theme">Light</span> |
| 160 | + <span class="btn" data-action="fullscreen">Fullscreen</span> |
156 | 161 | </div> |
157 | 162 | </div> |
158 | 163 |
|
|
199 | 204 |
|
200 | 205 | init() { |
201 | 206 | this.loadText(); |
202 | | - this.setTheme(this.theme); |
| 207 | + this.setTheme(this.theme, { transition: false }); |
203 | 208 | this.bindEvents(); |
204 | 209 | this.updateCount(); |
205 | | - this.updateFade(); |
206 | 210 | this.editor.focus(); |
207 | 211 | } |
208 | 212 |
|
|
353 | 357 | this.loadCursorPosition(); |
354 | 358 | editor.style.visibility = 'visible'; |
355 | 359 | this.updateCount(); |
| 360 | + // Disable fade transitions until text is loaded to prevent flickering |
| 361 | + this.fadeTop.style.transition = 'none'; |
| 362 | + this.fadeBottom.style.transition = 'none'; |
356 | 363 | this.updateFade(); |
| 364 | + requestAnimationFrame(() => { |
| 365 | + this.fadeTop.style.transition = ''; |
| 366 | + this.fadeBottom.style.transition = ''; |
| 367 | + }); |
357 | 368 | }); |
358 | 369 | } |
359 | 370 |
|
|
431 | 442 | } |
432 | 443 | } |
433 | 444 |
|
434 | | - setTheme(theme = this.theme) { |
| 445 | + setTheme(theme = this.theme, { transition = true } = {}) { |
435 | 446 | const button = document.querySelector('[data-action="theme"]'); |
436 | | - button.style.opacity = '0'; |
437 | | - setTimeout(() => { |
| 447 | + const apply = () => { |
438 | 448 | document.documentElement.setAttribute('data-theme', theme); |
439 | 449 | button.textContent = theme === 'dark' ? 'Light' : 'Dark'; |
440 | | - button.style.opacity = '1'; |
441 | | - }, 150); |
442 | | - localStorage.setItem(SHIRO_THEME, theme); |
443 | | - this.theme = theme; |
| 450 | + localStorage.setItem(SHIRO_THEME, theme); |
| 451 | + this.theme = theme; |
| 452 | + }; |
| 453 | + |
| 454 | + if (transition) { |
| 455 | + button.style.opacity = '0'; |
| 456 | + setTimeout(() => { |
| 457 | + apply(); |
| 458 | + button.style.opacity = '1'; |
| 459 | + }, 150); |
| 460 | + } else { |
| 461 | + apply(); |
| 462 | + } |
444 | 463 | } |
445 | 464 |
|
446 | 465 | updateFade() { |
|
456 | 475 | // Actions |
457 | 476 | download() { |
458 | 477 | const timestamp = new Date().toISOString().slice(0, 16).replace(/[:T]/g, m => m === 'T' ? '_' : '-'); |
459 | | - const blob = new Blob([this.editor.value], { type: 'text/plain' }); |
460 | | - const url = URL.createObjectURL(blob); |
461 | | - |
462 | | - Object.assign(document.createElement('a'), { |
463 | | - href: url, |
464 | | - download: `shiro_${timestamp}.txt` |
465 | | - }).click(); |
466 | | - |
467 | | - URL.revokeObjectURL(url); |
| 478 | + const a = document.createElement('a'); |
| 479 | + a.href = URL.createObjectURL(new Blob([this.editor.value], { type: 'text/plain' })); |
| 480 | + a.download = `shiro_${timestamp}.txt`; |
| 481 | + a.click(); |
| 482 | + URL.revokeObjectURL(a.href); |
468 | 483 | } |
469 | 484 |
|
470 | 485 | clearWithConfirmation() { |
471 | | - if (this.editor.value.trim()) { |
472 | | - if (confirm('Are you sure you want to clear all text?')) { |
473 | | - this.clear(); |
474 | | - } |
475 | | - } else { |
476 | | - this.clear(); |
477 | | - } |
| 486 | + if (this.editor.value.trim() && !confirm('Are you sure you want to clear all text?')) return; |
| 487 | + this.clear(); |
478 | 488 | } |
479 | 489 |
|
480 | 490 | clear() { |
|
0 commit comments