this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: move to new civility app permissions

+609 -159
+4 -4
deno.json
··· 1 1 { 2 - "version": "4.1.2", 2 + "version": "4.2.0", 3 3 "workspace": ["./data"], 4 4 "compilerOptions": { 5 5 "lib": [ ··· 42 42 "@civility/store": "jsr:@civility/store@^1.0.0-beta.10", 43 43 "@civility/store/idb": "jsr:@civility/store@^1.0.0-beta.10/idb", 44 44 "@civility/store/memory": "jsr:@civility/store@^1.0.0-beta.10/memory", 45 - "@civility/sync": "jsr:@civility/sync@^1.0.0-beta.13", 46 - "@civility/ui": "jsr:@civility/ui@^1.0.0-beta.5", 45 + "@civility/sync": "jsr:@civility/sync@^1.0.0-beta.14", 46 + "@civility/ui": "jsr:@civility/ui@^1.0.0-beta.10", 47 47 "@civility/workers": "jsr:@civility/workers@^0.2.7", 48 48 "@flashcard/core": "jsr:@flashcard/core@^0.1.0", 49 49 "@flashcard/schedulers": "jsr:@flashcard/schedulers@^0.1.0", ··· 52 52 "@leeoniya/ufuzzy": "npm:@leeoniya/ufuzzy@^1.0.19", 53 53 "@std/assert": "jsr:@std/assert@^1.0.19", 54 54 "@std/async": "jsr:@std/async@^1.3.0", 55 - "@zod/zod": "jsr:@zod/zod@^4.4.1", 55 + "@zod/zod": "jsr:@zod/zod@^4.4.2", 56 56 "fake-indexeddb": "npm:fake-indexeddb@6.2.5", 57 57 "howler": "npm:howler@^2.2.4", 58 58 "lit": "npm:lit@^3.3.2",
+19 -19
deno.lock
··· 2 2 "version": "5", 3 3 "specifiers": { 4 4 "jsr:@civility/blobs@^1.0.0-beta.4": "1.0.0-beta.4", 5 - "jsr:@civility/errors@^1.0.0-beta.1": "1.0.0-beta.1", 5 + "jsr:@civility/errors@^1.0.0-beta.2": "1.0.0-beta.2", 6 6 "jsr:@civility/store@^1.0.0-beta.10": "1.0.0-beta.10", 7 7 "jsr:@civility/store@~0.3.1": "0.3.1", 8 - "jsr:@civility/sync@^1.0.0-beta.13": "1.0.0-beta.13", 9 - "jsr:@civility/ui@^1.0.0-beta.5": "1.0.0-beta.5", 8 + "jsr:@civility/sync@^1.0.0-beta.14": "1.0.0-beta.14", 9 + "jsr:@civility/ui@^1.0.0-beta.10": "1.0.0-beta.10", 10 10 "jsr:@civility/workers@~0.2.7": "0.2.7", 11 11 "jsr:@cliffy/ansi@1": "1.0.1", 12 12 "jsr:@cliffy/ansi@1.0.1": "1.0.1", ··· 34 34 "jsr:@std/encoding@^1.0.10": "1.0.10", 35 35 "jsr:@std/fmt@^1.0.9": "1.0.10", 36 36 "jsr:@std/fs@^1.0.23": "1.0.23", 37 - "jsr:@std/html@^1.0.5": "1.0.6", 38 - "jsr:@std/internal@^1.0.12": "1.0.12", 37 + "jsr:@std/html@^1.0.6": "1.0.6", 38 + "jsr:@std/internal@^1.0.12": "1.0.13", 39 39 "jsr:@std/io@~0.225.3": "0.225.3", 40 40 "jsr:@std/path@^1.1.4": "1.1.4", 41 41 "jsr:@std/semver@^1.0.8": "1.0.8", 42 42 "jsr:@std/streams@^1.0.9": "1.0.17", 43 43 "jsr:@std/text@^1.0.17": "1.0.18", 44 44 "jsr:@std/ulid@1": "1.0.0", 45 - "jsr:@zod/zod@^4.4.1": "4.4.1", 45 + "jsr:@zod/zod@^4.4.2": "4.4.2", 46 46 "npm:@byojs/storage@~0.12.1": "0.12.1", 47 47 "npm:@leeoniya/ufuzzy@^1.0.19": "1.0.19", 48 48 "npm:cc-cedict@^1.1.1": "1.1.1", ··· 64 64 "@civility/blobs@1.0.0-beta.4": { 65 65 "integrity": "6806eb2a5b02e9e611385107b539abe0b2fe8e17066cfc42eaf467e301a6afa0" 66 66 }, 67 - "@civility/errors@1.0.0-beta.1": { 68 - "integrity": "2b28c161162aa855498ba7a7c9fe95b43cc149cc8bfb1b70bfa2f895c1cc186d" 67 + "@civility/errors@1.0.0-beta.2": { 68 + "integrity": "b89beaec634400edf2efbd467dd3596aca4e4bb906ead115f17457cecb5bc4ea" 69 69 }, 70 70 "@civility/store@0.3.1": { 71 71 "integrity": "0438f2cdb16145a61a97f5be509cd0b34e7cbd9f71dc657feffe2a4dd7dd0ec3", ··· 83 83 "npm:fast-json-patch" 84 84 ] 85 85 }, 86 - "@civility/sync@1.0.0-beta.13": { 87 - "integrity": "ca1b957583be279176fa3c32ef672bdeac418fe5980178094c9aa651a6405f7b", 86 + "@civility/sync@1.0.0-beta.14": { 87 + "integrity": "e149f52bb1404701dae9c593df4370b237421667df872d26cc6473b0dd1755b4", 88 88 "dependencies": [ 89 89 "jsr:@civility/blobs", 90 90 "jsr:@civility/errors", ··· 92 92 "jsr:@paulmillr/qr" 93 93 ] 94 94 }, 95 - "@civility/ui@1.0.0-beta.5": { 96 - "integrity": "fefa2b541adcf9bbda3fc0dfea07dcab99612497d647856ba76d857c6d98a8b7", 95 + "@civility/ui@1.0.0-beta.10": { 96 + "integrity": "16ff7103eb57a7974ba1f471c158a452c77eae47fb98b3505274164c9d971141", 97 97 "dependencies": [ 98 98 "jsr:@std/html", 99 99 "npm:lit" ··· 229 229 "@std/html@1.0.6": { 230 230 "integrity": "eaf759c8141e0733ca30eb49e4c08d8e6ca442b85c4d51f9894a56f502993e08" 231 231 }, 232 - "@std/internal@1.0.12": { 233 - "integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027" 232 + "@std/internal@1.0.13": { 233 + "integrity": "2f9546691d4ac2d32859c82dff284aaeac980ddeca38430d07941e7e288725c0" 234 234 }, 235 235 "@std/io@0.225.3": { 236 236 "integrity": "27b07b591384d12d7b568f39e61dff966b8230559122df1e9fd11cc068f7ddd1", ··· 256 256 "@std/ulid@1.0.0": { 257 257 "integrity": "d41c3d27a907714413649fee864b7cde8d42ee68437d22b79d5de4f81d808780" 258 258 }, 259 - "@zod/zod@4.4.1": { 260 - "integrity": "48fa5c11f9747ae936dc893253354c2044a60a94c8f208701393cd3ab4b56833" 259 + "@zod/zod@4.4.2": { 260 + "integrity": "7be10562a99ca7a0ce194764f0c0c2abee8013f43e368594690a381f44036de4" 261 261 } 262 262 }, 263 263 "npm": { ··· 458 458 "workspace": { 459 459 "dependencies": [ 460 460 "jsr:@civility/store@^1.0.0-beta.10", 461 - "jsr:@civility/sync@^1.0.0-beta.13", 462 - "jsr:@civility/ui@^1.0.0-beta.5", 461 + "jsr:@civility/sync@^1.0.0-beta.14", 462 + "jsr:@civility/ui@^1.0.0-beta.10", 463 463 "jsr:@civility/workers@~0.2.7", 464 464 "jsr:@flashcard/core@0.1", 465 465 "jsr:@flashcard/schedulers@0.1", 466 466 "jsr:@std/assert@^1.0.19", 467 467 "jsr:@std/async@^1.3.0", 468 - "jsr:@zod/zod@^4.4.1", 468 + "jsr:@zod/zod@^4.4.2", 469 469 "npm:@byojs/storage@~0.12.1", 470 470 "npm:@leeoniya/ufuzzy@^1.0.19", 471 471 "npm:fake-indexeddb@6.2.5",
+4 -2
www/models/app.ts
··· 426 426 } 427 427 428 428 /** Import store data from file picker */ 429 - importStore(): Promise<{ 429 + importStore(options?: { 430 + mode?: 'replace' | 'extend' 431 + }): Promise<{ 430 432 success: boolean 431 433 path: string 432 434 error?: string ··· 447 449 this.#suppressed = true 448 450 try { 449 451 const data = JSON.parse(await file.text()) 450 - await this.#store.import(data as StoreImportData) 452 + await this.#store.import(data as StoreImportData, options) 451 453 await Promise.all([ 452 454 this.#settingsDoc.preload(), 453 455 this.#flagsDoc.preload(),
+95 -99
www/routes/settings.ts
··· 1 1 import { html, LitElement, TemplateResult } from 'lit' 2 - import type { UiDataActionMethods } from '@civility/ui' 2 + import type { StoreImportMethods } from '@civility/ui' 3 3 import getString from '$/utils/get_string.ts' 4 4 import app from '$/models/app.ts' 5 5 import { CardSortMethod } from '@flashcard/core' ··· 116 116 117 117 override render() { 118 118 return html` 119 - <h2>${getString('Version')}</h2> 120 - <ui-pwa-version></ui-pwa-version> 121 - <ui-pwa-install></ui-pwa-install> 122 - 123 - <a href="#!/settings/about" class="ma3"> 124 - ${getString('about')} 125 - </a> 119 + <section> 120 + <h2>${getString('Version')}</h2> 121 + <p>${getString('version_hint') || 'App version and update information.'}</p> 122 + <ui-pwa-version></ui-pwa-version> 123 + <ui-pwa-install></ui-pwa-install> 124 + <a href="#!/settings/about" class="settings-nav-link"> 125 + <span>${getString('about')}</span> 126 + </a> 127 + </section> 126 128 127 - <hr /> 129 + <section> 130 + <h2>${getString('General')}</h2> 131 + <p>${getString('general_hint') || 'Configure language, audio, and display preferences.'}</p> 132 + <div class="item"> 133 + <label for="userLang">${getString('user_language')}</label> 134 + <h-user-lang-select user-lang="${app.userLang}"></h-user-lang-select> 135 + </div> 136 + <div class="item"> 137 + <label for="locale">${getString('locale')}</label> 138 + <h-locale-select locale="${app.locale}"></h-locale-select> 139 + </div> 140 + <div class="item"> 141 + <label for="transliteration">${getString('transliteration')}</label> 142 + <h-transliteration-select 143 + locale="${app.locale}" 144 + transliteration="${app.transliteration}" 145 + ></h-transliteration-select> 146 + </div> 147 + <div class="item"> 148 + <label for="playAudio">${getString('sound')}</label> 149 + <input 150 + class="br2 blue-shadow" 151 + name="playAudio" 152 + type="checkbox" 153 + .checked="${!!app.settings?.playAudio}" 154 + @change="${async (e: InputEvent) => { 155 + await app.updateSettings({ 156 + playAudio: (e.target as HTMLInputElement).checked, 157 + }) 158 + }}" 159 + /> 160 + </div> 161 + <a href="#!/settings/downloads" class="settings-nav-link"> 162 + <span>${getString('download_audio')}</span> 163 + </a> 164 + <a href="#!/settings/assignments" class="settings-nav-link"> 165 + <span>${getString('mark_known')}</span> 166 + </a> 167 + <a href="#!/settings/custom-sets" class="settings-nav-link"> 168 + <span>${getString('custom_sets')}</span> 169 + </a> 170 + <a href="javascript:void(0)" class="settings-nav-link" @click="${async (e: MouseEvent) => { 171 + e.preventDefault() 172 + await app.updateFlags({ 173 + isFirstLoad: true, 174 + isFirstPinyin: true, 175 + isFirstPinyinVocab: true, 176 + isFirstZhuyin: true, 177 + isFirstHiragana: true, 178 + }) 179 + }}"> 180 + <span>${getString('reset_help_dialogs')}</span> 181 + </a> 182 + </section> 128 183 129 - <h2>${getString('General')}</h2> 130 - <div class="item"> 131 - <label for="userLang">${getString('user_language')}</label> 132 - <h-user-lang-select user-lang="${app.userLang}"></h-user-lang-select> 133 - </div> 134 - <div class="item"> 135 - <label for="locale">${getString('locale')}</label> 136 - <h-locale-select locale="${app.locale}"></h-locale-select> 137 - </div> 138 - <div class="item"> 139 - <label for="transliteration">${getString('transliteration')}</label> 140 - <h-transliteration-select 141 - locale="${app.locale}" 142 - transliteration="${app.transliteration}" 143 - ></h-transliteration-select> 144 - </div> 145 - <div class="item"> 146 - <label for="playAudio">${getString('sound')}</label> 147 - <input 148 - class="br2 blue-shadow" 149 - name="playAudio" 150 - type="checkbox" 151 - style="height: 2.5em; width: 2.5em;" 152 - .checked="${!!app.settings?.playAudio}" 153 - @change="${async (e: InputEvent) => { 154 - await app.updateSettings({ 155 - playAudio: (e.target as HTMLInputElement).checked, 156 - }) 157 - }}" 158 - /> 159 - </div> 160 - <div class="item half-button"> 161 - <a href="#!/settings/downloads"><button>${getString( 162 - 'download_audio', 163 - )}</button></a> 164 - </div> 165 - <div class="item half-button"> 166 - <a href="#!/settings/assignments"><button>${getString( 167 - 'mark_known', 168 - )}</button></a> 169 - </div> 170 - <div class="item half-button"> 171 - <a href="#!/settings/custom-sets"><button>${getString( 172 - 'custom_sets', 173 - )}</button></a> 174 - </div> 184 + <section> 185 + <h2>Study</h2> 186 + <p>${getString('study_hint') || 'Configure study session limits and group sizes.'}</p> 187 + ${this.#renderNumInput('learnLimit', 'daily_limit', 'daily_limit_hint')} 188 + ${this.#renderNumInput('learnSessionSize', 'learn_group_size', 'learn_group_size_hint')} 189 + ${this.#renderNumInput('reviewSessionSize', 'study_group_size', 'study_group_size_hint')} 190 + </section> 175 191 176 - <h2>Study</h2> 177 - ${this.#renderNumInput( 178 - 'learnLimit', 179 - 'daily_limit', 180 - 'daily_limit_hint', 181 - )} ${this.#renderNumInput( 182 - 'learnSessionSize', 183 - 'learn_group_size', 184 - 'learn_group_size_hint', 185 - )} ${this.#renderNumInput( 186 - 'reviewSessionSize', 187 - 'study_group_size', 188 - 'study_group_size_hint', 189 - )} 190 - <h2>${getString('card_order')}</h2> 191 - ${this.#renderCardSortSelect()} 192 + <section> 193 + <h2>${getString('card_order')}</h2> 194 + <p>${getString('card_order_hint') || 'Choose how flashcards are ordered during study.'}</p> 195 + ${this.#renderCardSortSelect()} 196 + </section> 192 197 193 - <h2>${getString('sync')}</h2> 194 - <ui-sync 195 - storage-key="hanzi-sync" 196 - .synced="${app.synced}" 197 - ></ui-sync> 198 + <section> 199 + <h2>${getString('sync')}</h2> 200 + <p>${getString('sync_hint') || 'Sync your progress across devices.'}</p> 201 + <ui-sync-input 202 + storage-key="hanzi-sync" 203 + .synced="${app.synced}" 204 + ></ui-sync-input> 205 + </section> 198 206 199 - <h2>${getString('progress')}</h2> 200 - <ui-data-actions 201 - .methods="${{ 202 - exportData: (filename?: string) => app.exportStore(filename), 203 - importData: () => app.importStore(), 204 - deleteAllData: () => app.deleteAllData(), 205 - } as UiDataActionMethods}" 206 - ></ui-data-actions> 207 - <div class="full-button"> 208 - <button 209 - @click="${async () => { 210 - await app.updateFlags({ 211 - isFirstLoad: true, 212 - isFirstPinyin: true, 213 - isFirstPinyinVocab: true, 214 - isFirstZhuyin: true, 215 - isFirstHiragana: true, 216 - }) 217 - }}" 218 - > 219 - ${getString('reset_help_dialogs')} 220 - </button> 221 - </div> 207 + <section> 208 + <h2>${getString('progress')}</h2> 209 + <p>${getString('progress_hint') || 'Manage your learning progress data.'}</p> 210 + <ui-store-import 211 + .methods="${{ 212 + export: (filename?: string) => app.exportStore(filename), 213 + import: (opts) => app.importStore(opts), 214 + deleteAll: () => app.deleteAllData(), 215 + } as StoreImportMethods}" 216 + ></ui-store-import> 217 + </section> 222 218 ` 223 219 } 224 220 }
+21 -26
www/routes/settings/about.ts
··· 26 26 <article> 27 27 <p>version ${globalThis.__APP_VERSION__}</p> 28 28 <p> 29 - <a href="https://codeberg.org/bpev/hanzi-app">${getString( 29 + <a href="https://tangled.org/bpev.me/hanzi">${getString( 30 30 'open_source', 31 31 )}</a> 32 32 </p> 33 - <section style="text-align: center; padding-top: var(--s4);"> 33 + <section class="made-by"> 34 34 <p> 35 35 <a 36 - href="https://apps.bpev.me" 36 + href="https://bpev.me/apps" 37 37 rel="noopener noreferrer" 38 38 target="_blank" 39 - style="font-size: var(--f3); color: black;" 40 39 >Made by Ben</a> 41 40 </p> 42 41 43 42 <a 43 + class="ko-fi" 44 44 href="https://ko-fi.com/O5O21ETSMZ" 45 45 target="_blank" 46 - style="cursor: pointer;" 47 46 > 48 47 <img 49 48 height="36" 50 - style="border:0px;height:36px;" 51 49 src="https://storage.ko-fi.com/cdn/kofi5.png?v=6" 52 - border="0" 53 50 alt="Buy Me a Coffee at ko-fi.com" 54 51 ></a> 55 52 </section> 56 53 <section> 57 54 <h2>Acknowledgements</h2> 55 + <p> 56 + Definitions and Translations were mostly sourced from 57 + <a href="https://cc-cedict.org">cc-cedict</a> 58 + </p> 59 + <p> 60 + Example sentences were mostly sourced from 61 + <a href="https://tatoeba.org">Tatoeba</a> 62 + </p> 63 + <p> 64 + Audio is generated by 65 + <a 66 + href="https://learn.microsoft.com/en-us/azure/ai-services/speech-service/text-to-speech" 67 + > 68 + Azure Text-To-Speech 69 + </a> 70 + </p> 58 71 <p>Word lists were compiled from:</p> 59 72 <ul> 60 73 <li> ··· 78 91 </a> 79 92 </li> 80 93 </ul> 81 - <p> 82 - Definitions and Translations were mostly sourced from 83 - <a href="https://cc-cedict.org">cc-cedict</a> 84 - </p> 85 - <p> 86 - Example sentences were mostly sourced from 87 - <a href="https://tatoeba.org">Tatoeba</a> 88 - </p> 89 - <p> 90 - Audio is generated by 91 - <a 92 - href="https://learn.microsoft.com/en-us/azure/ai-services/speech-service/text-to-speech" 93 - > 94 - Azure Text-To-Speech 95 - </a> 96 - </p> 97 94 </section> 98 95 <h2>Licenses</h2> 99 96 ${licenses.map((license) => { 100 97 const text = license.text 101 98 const isHTML = /<\/?[a-z][\s\S]*>/i.test(text) 102 - const style = 103 - 'text-wrap: wrap; overflow: scroll; background-color: whitesmoke; max-height: 400px;' 104 99 return html` 105 100 <details name="licenses"> 106 101 <summary>${license.name}</summary> ··· 109 104 globalThis.open(license.href)}">open</button> 110 105 ${!isHTML 111 106 ? html` 112 - <pre style="${style}">${text}</pre> 107 + <pre>${text}</pre> 113 108 ` 114 109 : ''} 115 110 </section>
www/static/brand/title.png

This is a binary file and will not be displayed.

+6
www/static/strings/en.json
··· 5 5 "back": "Back", 6 6 "cancel": "Cancel", 7 7 "card_order": "Card Order", 8 + "card_order_hint": "Choose how flashcards are ordered during study.", 8 9 "card_sort_hint": "How the different meaning and reading flashcards are ordered in a study session.", 9 10 "card_sort_method": "Sort Method", 10 11 "character": "Character", ··· 27 28 "furigana": "Furigana", 28 29 "games": "Games", 29 30 "general": "General", 31 + "general_hint": "Configure language, audio, and display preferences.", 30 32 "got_it": "Got it!", 31 33 "hant": "Traditional Chinese", 32 34 "hanzi_app": "HanziApp", ··· 77 79 "play_audio": "Play Audio", 78 80 "practice_type": "Practice Type", 79 81 "progress": "Progress", 82 + "progress_hint": "Manage your learning progress data.", 80 83 "radical": "Radical", 81 84 "radicals": "Radicals", 82 85 "random": "Random", ··· 103 106 "strokes": "Strokes", 104 107 "studied": "Studied", 105 108 "study": "Study", 109 + "study_hint": "Configure study session limits and group sizes.", 106 110 "study_group_size": "Study Group Size", 107 111 "study_group_size_hint": "The maximum number of items to study at a time.", 108 112 "sync": "Sync", 113 + "sync_hint": "Sync your progress across devices.", 109 114 "sync_url": "Synclink Url", 110 115 "synonyms": "Synonyms", 111 116 "test_connection": "Connect", ··· 116 121 "typing": "Typing", 117 122 "use_app_for_audio": "Get the mobile app to download audio for offline use!", 118 123 "user_language": "Language", 124 + "version_hint": "App version and update information.", 119 125 "vocabulary": "Vocabulary", 120 126 "welcome": "Welcome to HanziApp!", 121 127 "welcome_desc": "This is an app for learning Chinese characters!",
+460 -9
www/static/styles/theme.css
··· 240 240 241 241 r-search { 242 242 padding: var(--s3); 243 + width: 100%; 243 244 } 244 245 245 246 main.study, ··· 423 424 /* Settings page layout */ 424 425 ui-settings-item { 425 426 align-items: center; 426 - border-bottom: 1px solid currentColor; 427 + border-bottom: 1px dotted var(--primary-dull); 427 428 display: flex; 428 429 flex-wrap: wrap; 429 430 font-size: var(--f6); ··· 818 819 819 820 /* Settings page */ 820 821 main.settings, 822 + r-settings-about, 821 823 r-about, 822 824 r-assignments, 823 825 r-downloads, 824 826 r-settings { 825 - align-items: start; 826 827 display: flex; 827 828 flex-direction: column; 828 829 margin: auto; ··· 830 831 padding: var(--s3); 831 832 } 832 833 834 + main.settings section, 835 + r-settings-about section, 836 + r-settings section, 837 + r-downloads section, 838 + r-assignments section { 839 + margin-bottom: var(--s5); 840 + } 841 + 842 + main.settings section > p, 843 + r-settings-about section > p, 844 + r-settings section > p, 845 + r-downloads section > p, 846 + r-assignments section > p { 847 + opacity: 0.6; 848 + margin-bottom: var(--s3); 849 + font-size: var(--f6); 850 + } 851 + 833 852 main.settings .item, 834 853 r-settings .item, 835 854 r-downloads .item, 836 855 r-assignments .item { 837 856 display: flex; 857 + align-items: center; 838 858 padding: var(--s3); 839 859 width: 100%; 840 - flex-wrap: wrap; 841 860 justify-content: space-between; 861 + border-bottom: 1px dotted var(--primary-dull); 862 + } 863 + 864 + main.settings .item:last-child, 865 + r-settings .item:last-child, 866 + r-downloads .item:last-child, 867 + r-assignments .item:last-child { 868 + border-bottom: none; 842 869 } 843 870 844 871 main.settings .item label, ··· 848 875 font-weight: bold; 849 876 padding: var(--s0) var(--s2); 850 877 display: block; 878 + flex-shrink: 0; 851 879 } 852 880 853 881 main.settings .item input, ··· 865 893 padding: var(--s2); 866 894 border-radius: var(--br2); 867 895 display: block; 896 + width: 5em; 897 + } 898 + 899 + main.settings .item input[type='checkbox'], 900 + r-settings .item input[type='checkbox'], 901 + r-downloads .item input[type='checkbox'], 902 + r-assignments .item input[type='checkbox'] { 903 + height: 2.5em; 904 + width: 2.5em; 905 + accent-color: var(--blue); 906 + cursor: pointer; 868 907 } 869 908 870 909 main.settings .full-button, ··· 884 923 width: 100%; 885 924 max-width: var(--main-max-width); 886 925 height: 3em; 926 + border: 1px solid currentColor; 927 + background: transparent; 928 + cursor: pointer; 929 + font-weight: bold; 930 + transition: all var(--transition-fast); 887 931 } 888 932 889 933 main.settings select, 890 - main.settings input, 891 934 r-settings select, 892 - r-settings input, 893 935 r-downloads select, 894 - r-downloads input, 895 - r-assignments select, 896 - r-assignments input { 897 - width: unset; 936 + r-assignments select { 937 + border: 1px solid currentColor; 938 + padding: var(--s2); 939 + border-radius: var(--br2); 940 + background: transparent; 941 + color: inherit; 942 + font-size: var(--f5); 943 + cursor: pointer; 944 + box-shadow: var(--blue-shadow); 945 + } 946 + 947 + main.settings .item select, 948 + r-settings .item select, 949 + r-downloads .item select, 950 + r-assignments .item select { 951 + min-width: 12em; 898 952 } 899 953 900 954 main.settings .half-button, ··· 904 958 justify-content: end; 905 959 } 906 960 961 + /* Settings navigation link (like ClimbApp) */ 962 + .settings-nav-link { 963 + display: flex; 964 + align-items: center; 965 + justify-content: space-between; 966 + padding: var(--s3); 967 + border: 1px solid currentColor; 968 + border-radius: var(--br2); 969 + text-decoration: none; 970 + color: inherit; 971 + font-weight: bold; 972 + transition: opacity var(--transition-fast); 973 + margin-top: var(--s3); 974 + box-shadow: var(--blue-shadow); 975 + } 976 + 977 + .settings-nav-link:hover { 978 + opacity: 0.8; 979 + } 980 + 981 + .settings-nav-link span { 982 + font-size: var(--f5); 983 + } 984 + 985 + /* ui-sync component styling in settings */ 986 + main.settings ui-sync, 987 + r-settings ui-sync { 988 + display: block; 989 + padding: var(--s2) 0; 990 + } 991 + 992 + main.settings ui-sync form, 993 + r-settings ui-sync form { 994 + display: flex; 995 + flex-direction: column; 996 + gap: var(--s2); 997 + } 998 + 999 + main.settings ui-sync label, 1000 + r-settings ui-sync label { 1001 + display: flex; 1002 + flex-direction: column; 1003 + gap: var(--s1); 1004 + cursor: default; 1005 + border: none; 1006 + padding: 0; 1007 + font-weight: normal; 1008 + } 1009 + 1010 + main.settings ui-sync label span, 1011 + r-settings ui-sync label span { 1012 + font-size: var(--f6); 1013 + opacity: 0.7; 1014 + } 1015 + 1016 + main.settings ui-sync input[type='text'], 1017 + r-settings ui-sync input[type='text'] { 1018 + width: 100%; 1019 + box-sizing: border-box; 1020 + border: 1px solid currentColor; 1021 + padding: var(--s2); 1022 + border-radius: var(--br2); 1023 + background: transparent; 1024 + color: inherit; 1025 + } 1026 + 1027 + main.settings ui-sync .ui-sync__status, 1028 + r-settings ui-sync .ui-sync__status { 1029 + display: flex; 1030 + align-items: baseline; 1031 + gap: var(--s3); 1032 + font-size: var(--f6); 1033 + } 1034 + 1035 + main.settings ui-sync .ui-sync__status small, 1036 + r-settings ui-sync .ui-sync__status small { 1037 + opacity: 0.6; 1038 + } 1039 + 1040 + main.settings ui-sync .ui-sync__actions, 1041 + r-settings ui-sync .ui-sync__actions { 1042 + display: flex; 1043 + align-items: center; 1044 + gap: var(--s2); 1045 + flex-wrap: wrap; 1046 + } 1047 + 1048 + main.settings ui-sync .ui-sync__actions button, 1049 + r-settings ui-sync .ui-sync__actions button { 1050 + border: 1px solid currentColor; 1051 + background: transparent; 1052 + padding: var(--s2) var(--s3); 1053 + cursor: pointer; 1054 + font-weight: bold; 1055 + border-radius: var(--br2); 1056 + font-size: var(--f6); 1057 + } 1058 + 1059 + /* ui-data-actions component styling in settings */ 1060 + main.settings ui-data-actions, 1061 + r-settings ui-data-actions { 1062 + display: flex; 1063 + gap: var(--s2); 1064 + flex-wrap: wrap; 1065 + padding: var(--s2) 0; 1066 + } 1067 + 1068 + main.settings ui-data-actions button, 1069 + r-settings ui-data-actions button { 1070 + border: 1px solid currentColor; 1071 + background: transparent; 1072 + padding: var(--s2) var(--s3); 1073 + cursor: pointer; 1074 + font-weight: bold; 1075 + border-radius: var(--br2); 1076 + font-size: var(--f6); 1077 + } 1078 + 907 1079 .answer-input { 908 1080 border: none; 909 1081 text-align: center; ··· 955 1127 .search-item:hover { 956 1128 background-color: rgba(231, 222, 237, 0.3); 957 1129 } 1130 + 1131 + /* About page */ 1132 + r-settings-about article { 1133 + padding: var(--s3); 1134 + } 1135 + 1136 + r-settings-about article > p { 1137 + font-size: var(--f6); 1138 + opacity: 0.6; 1139 + margin-bottom: var(--s2); 1140 + } 1141 + 1142 + r-settings-about .made-by { 1143 + text-align: center; 1144 + padding-top: var(--s4); 1145 + } 1146 + 1147 + r-settings-about .made-by a { 1148 + font-size: var(--f3); 1149 + color: var(--body); 1150 + text-decoration: none; 1151 + font-weight: var(--fw-bold); 1152 + } 1153 + 1154 + r-settings-about .ko-fi { 1155 + cursor: pointer; 1156 + border: none; 1157 + height: 36px; 1158 + } 1159 + 1160 + r-settings-about section { 1161 + margin-bottom: var(--s4); 1162 + } 1163 + 1164 + r-settings-about h2 { 1165 + font-size: var(--f4); 1166 + font-weight: var(--fw-bold); 1167 + margin-bottom: var(--s3); 1168 + padding-bottom: var(--s2); 1169 + border-bottom: 1px dotted var(--primary-dull); 1170 + } 1171 + 1172 + r-settings-about ul { 1173 + list-style: none; 1174 + padding: 0; 1175 + margin: var(--s3) 0; 1176 + } 1177 + 1178 + r-settings-about li { 1179 + padding: var(--s2) var(--s3); 1180 + border-bottom: 1px dotted var(--primary-dull); 1181 + } 1182 + 1183 + r-settings-about li:last-child { 1184 + border-bottom: none; 1185 + } 1186 + 1187 + r-settings-about a { 1188 + color: var(--blue); 1189 + text-decoration: none; 1190 + } 1191 + 1192 + r-settings-about a:hover { 1193 + opacity: 0.8; 1194 + } 1195 + 1196 + r-settings-about details { 1197 + margin-bottom: var(--s2); 1198 + } 1199 + 1200 + r-settings-about details summary { 1201 + cursor: pointer; 1202 + font-weight: var(--fw-bold); 1203 + padding: var(--s2) var(--s3); 1204 + border-bottom: 1px dotted var(--primary-dull); 1205 + } 1206 + 1207 + r-settings-about details section { 1208 + margin-bottom: var(--s2); 1209 + } 1210 + 1211 + r-settings-about details button { 1212 + border: 1px solid currentColor; 1213 + background: transparent; 1214 + padding: var(--s1) var(--s3); 1215 + cursor: pointer; 1216 + font-weight: bold; 1217 + border-radius: var(--br2); 1218 + font-size: var(--f6); 1219 + margin-bottom: var(--s2); 1220 + } 1221 + 1222 + r-settings-about pre { 1223 + text-wrap: wrap; 1224 + overflow: scroll; 1225 + background-color: var(--purple-bg); 1226 + max-height: 400px; 1227 + padding: var(--s3); 1228 + border-radius: var(--br2); 1229 + font-size: var(--f6); 1230 + margin: var(--s2) 0; 1231 + } 1232 + 1233 + .header-back { 1234 + box-shadow: none; 1235 + } 1236 + 1237 + 1238 + /* ── ui-sync-input component ─────────────────────────────────────────────── */ 1239 + 1240 + ui-sync-input form{ 1241 + display: flex; 1242 + flex-direction: column; 1243 + gap: var(--s3); 1244 + } 1245 + 1246 + ui-sync-input label { 1247 + display: flex; 1248 + flex-direction: column; 1249 + gap: var(--s1); 1250 + cursor: default; 1251 + border: none; 1252 + padding: 0; 1253 + font-weight: var(--fw-normal); 1254 + border-radius: 0; 1255 + background: none; 1256 + } 1257 + 1258 + ui-sync-input label span { 1259 + font-size: var(--f6); 1260 + opacity: 0.7; 1261 + } 1262 + 1263 + ui-sync-input label input { 1264 + width: 100%; 1265 + box-sizing: border-box; 1266 + } 1267 + 1268 + ui-sync-input .ui-sync-input__error { 1269 + font-size: var(--f6); 1270 + color: hsl(var(--errorH), var(--errorS), var(--errorL)); 1271 + margin: 0; 1272 + } 1273 + 1274 + ui-sync-input .ui-sync-input__error code { 1275 + font-family: var(--font-family-mono); 1276 + font-size: var(--f7); 1277 + opacity: 0.8; 1278 + } 1279 + 1280 + ui-sync-input .ui-sync-input__status { 1281 + display: flex; 1282 + align-items: baseline; 1283 + gap: var(--s3); 1284 + margin-bottom: var(--s3); 1285 + } 1286 + 1287 + ui-sync-input .ui-sync-input__status small { 1288 + opacity: 0.6; 1289 + } 1290 + 1291 + ui-sync-input .ui-sync-input__indicator { 1292 + font-weight: var(--fw-medium); 1293 + } 1294 + 1295 + ui-sync-input .ui-sync-input__indicator--ok { 1296 + color: hsl(var(--successH), var(--successS), var(--successL)); 1297 + } 1298 + 1299 + ui-sync-input .ui-sync-input__indicator--syncing { 1300 + opacity: 0.7; 1301 + } 1302 + 1303 + ui-sync-input .ui-sync-input__actions { 1304 + display: flex; 1305 + align-items: center; 1306 + gap: var(--s3); 1307 + flex-wrap: wrap; 1308 + } 1309 + 1310 + ui-sync-input .ui-sync-input__actions .action { 1311 + width: auto; 1312 + padding: var(--s2) var(--s4); 1313 + } 1314 + 1315 + /* ── ui-sync-state component ─────────────────────────────────────────────── */ 1316 + ui-store-import button.action { 1317 + width: 100%; 1318 + } 1319 + 1320 + ui-store-import div { 1321 + display: flex; 1322 + gap: var(--s3); 1323 + margin-bottom: var(--s3); 1324 + } 1325 + 1326 + /* Shared dot shape — used by both badge and inline/detailed inner dot */ 1327 + .ui-sync-state--badge, 1328 + .ui-sync-state__dot { 1329 + display: inline-block; 1330 + width: 8px; 1331 + height: 8px; 1332 + border-radius: 50%; 1333 + flex-shrink: 0; 1334 + } 1335 + 1336 + /* Badge: the root span IS the dot */ 1337 + .ui-sync-state--badge { 1338 + background: currentColor; 1339 + opacity: 0.3; 1340 + } 1341 + 1342 + /* Status colors — applied via modifier class on the wrapper */ 1343 + .ui-sync-state--ok .ui-sync-state__dot, 1344 + .ui-sync-state--badge.ui-sync-state--ok { 1345 + background: hsl(var(--successH), var(--successS), var(--successL)); 1346 + opacity: 1; 1347 + } 1348 + 1349 + .ui-sync-state--syncing .ui-sync-state__dot, 1350 + .ui-sync-state--badge.ui-sync-state--syncing { 1351 + background: hsl(var(--primaryH), var(--primaryS), var(--primaryL)); 1352 + opacity: 0.7; 1353 + } 1354 + 1355 + .ui-sync-state--error .ui-sync-state__dot, 1356 + .ui-sync-state--badge.ui-sync-state--error { 1357 + background: hsl(var(--errorH), var(--errorS), var(--errorL)); 1358 + opacity: 1; 1359 + } 1360 + 1361 + .ui-sync-state--disconnected .ui-sync-state__dot, 1362 + .ui-sync-state--badge.ui-sync-state--disconnected { 1363 + background: currentColor; 1364 + opacity: 0.3; 1365 + } 1366 + 1367 + /* Inline variant */ 1368 + .ui-sync-state--inline { 1369 + display: inline-flex; 1370 + align-items: center; 1371 + gap: var(--s2); 1372 + font-size: var(--f6); 1373 + } 1374 + 1375 + /* Detailed variant */ 1376 + .ui-sync-state--detailed { 1377 + display: flex; 1378 + flex-direction: column; 1379 + gap: var(--s1); 1380 + } 1381 + 1382 + .ui-sync-state--detailed .ui-sync-state__row { 1383 + display: flex; 1384 + align-items: center; 1385 + gap: var(--s2); 1386 + font-weight: var(--fw-medium); 1387 + } 1388 + 1389 + .ui-sync-state--detailed .ui-sync-state__meta { 1390 + font-size: var(--f6); 1391 + opacity: 0.6; 1392 + } 1393 + 1394 + .ui-sync-state--detailed .ui-sync-state__error { 1395 + font-size: var(--f6); 1396 + color: hsl(var(--errorH), var(--errorS), var(--errorL)); 1397 + margin: 0; 1398 + } 1399 + 1400 + .ui-sync-state--detailed .ui-sync-state__error code { 1401 + font-family: var(--font-family-mono); 1402 + font-size: var(--f7); 1403 + } 1404 + 1405 + .action--danger { 1406 + color: var(--error); 1407 + border-color: var(--error); 1408 + }