├── .gitignore ├── .vscode └── extensions.json ├── LICENSE.txt ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.png ├── fonts │ └── kai-icons.ttf ├── global.css ├── icons │ ├── icon112x112.png │ └── icon56x56.png ├── index.html ├── langs │ ├── en-US.json │ └── jp-JP.json ├── manifest.webapp └── manifest.webmanifest ├── rollup.config.js ├── src ├── App.svelte ├── components │ ├── AppBar.svelte │ ├── Button.svelte │ ├── Checkbox.svelte │ ├── DatePicker.svelte │ ├── Dialog.svelte │ ├── LinearProgress.svelte │ ├── ListView.svelte │ ├── LoadingBar.svelte │ ├── MultiSelector.svelte │ ├── OptionMenu.svelte │ ├── Radio.svelte │ ├── RangeSlider.svelte │ ├── Separator.svelte │ ├── SingleSelector.svelte │ ├── SoftwareKey.svelte │ ├── TextAreaDialog.svelte │ ├── TextAreaField.svelte │ ├── TextInputDialog.svelte │ ├── TextInputField.svelte │ ├── TimePicker.svelte │ └── index.ts ├── global.d.ts ├── main.ts ├── routes │ ├── Demo.svelte │ ├── Room.svelte │ ├── Welcome.svelte │ └── index.ts └── utils │ ├── localization.ts │ └── navigation.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /public/build/ 3 | 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["svelte.svelte-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Ahmad Malik 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # kaios-svelte-starter 4 | 5 | A simple starter template for building a KaiOS app using Svelte and TypeScript. 6 | 7 | ### Development and testing 8 | 9 | `npm run dev` builds the app in watch mode and serves the site. Great for testing your app in a desktop browser. 10 | 11 | ### Deploying to a device 12 | 13 | 1. Connect your device to your computer and make sure it appears in WebIDE. 14 | 2. `npm run build` 15 | 3. In WebIDE, load the `/public` folder as a packaged app. 16 | 4. Or, `npm run dev` then visit `localhost:port/index.html` 17 | 18 | ### Synthetic flavors: 19 | - Support D-pad navigation & Software Key listener 20 | - Support i18n 21 | 22 | ### Built-in components: 23 | 1. Dialog 24 | 2. ListView 25 | 3. Separator 26 | 4. Option Menu 27 | 5. Radio 28 | 6. Single Selector(Radio) 29 | 7. Checkbox 30 | 8. Multi Selector(Checkbox) 31 | 9. Date Picker 32 | 10. Time Picker 33 | 11. Loading Bar 34 | 11. Progress Bar 35 | 12. Range Slider 36 | 13. Button 37 | 14. InputText 38 | 15. TextArea 39 | 14. InputTextDialog 40 | 15. TextAreaDialog 41 | 16. Toaster(@zerodevx/svelte-toast) 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kaios-svelte-starter", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "rollup -c", 7 | "dev": "rollup -c -w", 8 | "start": "sirv public --no-clear", 9 | "check": "svelte-check --tsconfig ./tsconfig.json" 10 | }, 11 | "devDependencies": { 12 | "@babel/core": "^7.17.2", 13 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 14 | "@babel/plugin-transform-runtime": "^7.16.4", 15 | "@babel/preset-env": "^7.16.4", 16 | "@rollup/plugin-commonjs": "^17.0.0", 17 | "@rollup/plugin-json": "^4.1.0", 18 | "@rollup/plugin-node-resolve": "^11.0.0", 19 | "@rollup/plugin-replace": "^3.0.0", 20 | "@rollup/plugin-typescript": "^8.0.0", 21 | "@tsconfig/svelte": "^2.0.0", 22 | "rollup": "^2.3.4", 23 | "rollup-plugin-babel": "^4.4.0", 24 | "rollup-plugin-css-only": "^3.1.0", 25 | "rollup-plugin-livereload": "^2.0.0", 26 | "rollup-plugin-svelte": "^7.0.0", 27 | "rollup-plugin-terser": "^7.0.0", 28 | "svelte": "^3.0.0", 29 | "svelte-check": "^2.0.0", 30 | "svelte-preprocess": "^4.0.0", 31 | "tslib": "^2.0.0", 32 | "typescript": "^4.0.0" 33 | }, 34 | "dependencies": { 35 | "@zerodevx/svelte-toast": "^0.6.3", 36 | "sirv-cli": "^1.0.0", 37 | "sprintf-js": "^1.1.2", 38 | "svelte-navigator": "^3.1.5" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arma7x/kaios-svelte-starter/b1a957f04644b62e0871dc5a38f1e0f033aee54c/public/favicon.png -------------------------------------------------------------------------------- /public/fonts/kai-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arma7x/kaios-svelte-starter/b1a957f04644b62e0871dc5a38f1e0f033aee54c/public/fonts/kai-icons.ttf -------------------------------------------------------------------------------- /public/global.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --themeColor: #ff3e00; 3 | --themeColorLight: #ECE8F2; 4 | --themeColorTransparent: rgba(255, 62, 0, 0.1); 5 | --toastContainerTop: 26px; 6 | --toastContainerRight: 0; 7 | --toastWidth: 100vw; 8 | --toastBarHeight: 0px; 9 | --toastMinHeight: 30px; 10 | --toastBackground: #2f2f2f; 11 | --toastColor: #ffffff; 12 | --toastBorderRadius: 0; 13 | --toastBarBackground: var(--themeColor); 14 | } 15 | 16 | @font-face { 17 | font-family: 'kai-icons'; 18 | src: url('fonts/kai-icons.ttf') format('truetype'); 19 | font-weight: normal; 20 | font-style: normal; 21 | } 22 | 23 | [class^="kai-icon-"], [class*=" kai-icon-"] { 24 | /* use !important to prevent issues with browser extensions that change fonts */ 25 | font-family: 'kai-icons' !important; 26 | speak: none; 27 | font-style: normal; 28 | font-weight: normal; 29 | font-variant: normal; 30 | text-transform: none; 31 | line-height: 1; 32 | 33 | /* Better Font Rendering =========== */ 34 | -webkit-font-smoothing: antialiased; 35 | -moz-osx-font-smoothing: grayscale; 36 | } 37 | 38 | .kai-icon-arrow:before { 39 | content: "\e900"; 40 | } 41 | 42 | .kai-icon-checkbox-checked:before { 43 | content: "\e901"; 44 | } 45 | 46 | .kai-icon-checkbox-unchecked:before { 47 | content: "\e902"; 48 | } 49 | 50 | .kai-icon-radio-button-selected:before { 51 | content: "\e906"; 52 | } 53 | 54 | .kai-icon-radio-button-unselected:before { 55 | content: "\e90f"; 56 | } 57 | 58 | .kai-icon-bluetooth:before { 59 | content: "\e903"; 60 | } 61 | 62 | .kai-icon-calendar:before { 63 | content: "\e904"; 64 | } 65 | 66 | .kai-icon-camera:before { 67 | content: "\e905"; 68 | } 69 | 70 | .kai-icon-contacts:before { 71 | content: "\e907"; 72 | } 73 | 74 | .kai-icon-download:before { 75 | content: "\e908"; 76 | } 77 | 78 | .kai-icon-email:before { 79 | content: "\e909"; 80 | } 81 | 82 | .kai-icon-favorite-off:before { 83 | content: "\e90a"; 84 | } 85 | 86 | .kai-icon-favorite-on:before { 87 | content: "\e90b"; 88 | } 89 | 90 | .kai-icon-message:before { 91 | content: "\e90c"; 92 | } 93 | 94 | .kai-icon-mic:before { 95 | content: "\e90d"; 96 | } 97 | 98 | .kai-icon-phone:before { 99 | content: "\e90e"; 100 | } 101 | 102 | .kai-icon-search:before { 103 | content: "\e910"; 104 | } 105 | 106 | .kai-icon-settings:before { 107 | content: "\e911"; 108 | } 109 | 110 | .kai-icon-video:before { 111 | content: "\e912"; 112 | } 113 | 114 | .kai-icon-wifi:before { 115 | content: "\e913"; 116 | } 117 | 118 | html, 119 | body { 120 | position: relative; 121 | width: 100%; 122 | height: 100%; 123 | background-color: #ffffff; 124 | } 125 | 126 | div { 127 | padding: 0; 128 | margin: 0; 129 | box-sizing: border-box; 130 | } 131 | 132 | body { 133 | color: #333; 134 | margin: 0; 135 | padding: 0px; 136 | box-sizing: border-box; 137 | font-family: 'Open Sans'; 138 | } 139 | 140 | a { 141 | color: rgb(0, 100, 200); 142 | text-decoration: none; 143 | } 144 | 145 | a:hover { 146 | text-decoration: underline; 147 | } 148 | 149 | a:visited { 150 | color: rgb(0, 80, 160); 151 | } 152 | 153 | label { 154 | display: block; 155 | } 156 | 157 | input, 158 | button, 159 | select, 160 | textarea { 161 | font-family: inherit; 162 | font-size: inherit; 163 | -webkit-padding: 0.4em 0; 164 | padding: 0.4em; 165 | margin: 0 0 0.5em 0; 166 | box-sizing: border-box; 167 | border: 1px solid #ccc; 168 | border-radius: 2px; 169 | } 170 | 171 | input:disabled { 172 | color: #ccc; 173 | } 174 | 175 | button { 176 | color: #333; 177 | background-color: #f4f4f4; 178 | outline: none; 179 | } 180 | 181 | button:disabled { 182 | color: #999; 183 | } 184 | 185 | button:not(:disabled):active { 186 | background-color: #ddd; 187 | } 188 | 189 | button:focus { 190 | border-color: #666; 191 | } 192 | -------------------------------------------------------------------------------- /public/icons/icon112x112.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arma7x/kaios-svelte-starter/b1a957f04644b62e0871dc5a38f1e0f033aee54c/public/icons/icon112x112.png -------------------------------------------------------------------------------- /public/icons/icon56x56.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arma7x/kaios-svelte-starter/b1a957f04644b62e0871dc5a38f1e0f033aee54c/public/icons/icon56x56.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Svelte App 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/langs/en-US.json: -------------------------------------------------------------------------------- 1 | { 2 | "hello": "Hello %s", 3 | "change_locale": "Change Locale", 4 | "change_locale_subtitle": "Click to change app locale", 5 | "select_locale": "Select Locale" 6 | } 7 | -------------------------------------------------------------------------------- /public/langs/jp-JP.json: -------------------------------------------------------------------------------- 1 | { 2 | "hello": "こんにちは %s", 3 | "change_locale": "ロケールを選択", 4 | "change_locale_subtitle": "クリックしてアプリのロケールを変更します", 5 | "select_locale": "ロケールを選択" 6 | } 7 | -------------------------------------------------------------------------------- /public/manifest.webapp: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Svelte App", 3 | "description": "A starter template for Svelte and TypeScript.", 4 | "version": "1.0.0", 5 | "launch_path": "/index.html", 6 | "theme": "#ff3e00", 7 | "theme_color": "#ff3e00", 8 | "background_color": "#ff3e00", 9 | "fullscreen": "true", 10 | "chrome": { 11 | "statusbar": "overlap" 12 | }, 13 | "orientation": "default", 14 | "icons": { 15 | "56": "/icons/icon56x56.png", 16 | "112": "/icons/icon112x112.png" 17 | }, 18 | "categories": [ 19 | "utilities" 20 | ], 21 | "developer": { 22 | "name": "arma7x", 23 | "url": "https://github.com/arma7x/kaios-svelte-starter" 24 | }, 25 | "origin": "app://svelte-app.arma7x.com", 26 | "type": "privileged", 27 | "permissions": {}, 28 | "locales": { 29 | "en-US": { 30 | "name": "Svelte App", 31 | "subtitle": "A starter template for Svelte and TypeScript.", 32 | "description": "A starter template for Svelte and TypeScript." 33 | } 34 | }, 35 | "default_locale": "en" 36 | } 37 | -------------------------------------------------------------------------------- /public/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "id": "Svelte App", 3 | "name": "Svelte App", 4 | "description": "A starter template for Svelte and TypeScript.", 5 | "start_url": "/index.html", 6 | "theme": "#ff3e00", 7 | "theme_color": "#ff3e00", 8 | "background_color": "#ff3e00", 9 | "display": "fullscreen", 10 | "orientation": "default", 11 | "categories": [ 12 | "utilities" 13 | ], 14 | "icons": [ 15 | { 16 | "src": "/icons/icon56x56.png", 17 | "type": "image/png", 18 | "sizes": "56x56" 19 | }, 20 | { 21 | "src": "/icons/icon112x112.png", 22 | "type": "image/png", 23 | "sizes": "112x112" 24 | } 25 | ], 26 | "b2g_features": { 27 | "version": "3.0.0", 28 | "type": "privileged", 29 | "core": true, 30 | "default_locale": "en-US", 31 | "developer": { 32 | "name": "arma7x", 33 | "url": "https://github.com/arma7x/kaios-svelte-starter" 34 | }, 35 | "chrome": { 36 | "statusbar": "overlap" 37 | }, 38 | "origin": "svelte-app", 39 | "permissions": {}, 40 | "dependencies": {}, 41 | "locales": { 42 | "en-US": { 43 | "name": "Svelte App", 44 | "subtitle": "A starter template for Svelte and TypeScript.", 45 | "description": "A starter template for Svelte and TypeScript." 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import svelte from 'rollup-plugin-svelte'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import resolve from '@rollup/plugin-node-resolve'; 4 | import livereload from 'rollup-plugin-livereload'; 5 | import { terser } from 'rollup-plugin-terser'; 6 | import sveltePreprocess from 'svelte-preprocess'; 7 | import typescript from '@rollup/plugin-typescript'; 8 | import css from 'rollup-plugin-css-only'; 9 | import babel from 'rollup-plugin-babel'; 10 | import replace from '@rollup/plugin-replace'; 11 | import json from '@rollup/plugin-json'; 12 | 13 | const production = !process.env.ROLLUP_WATCH; 14 | 15 | function serve() { 16 | let server; 17 | 18 | function toExit() { 19 | if (server) server.kill(0); 20 | } 21 | 22 | return { 23 | writeBundle() { 24 | if (server) return; 25 | server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], { 26 | stdio: ['ignore', 'inherit', 'inherit'], 27 | shell: true, 28 | }); 29 | 30 | process.on('SIGTERM', toExit); 31 | process.on('exit', toExit); 32 | }, 33 | }; 34 | } 35 | 36 | export default { 37 | input: 'src/main.ts', 38 | output: { 39 | sourcemap: !production, 40 | format: 'iife', 41 | name: 'app', 42 | file: 'public/build/bundle.js', 43 | }, 44 | context: 'window', 45 | plugins: [ 46 | svelte({ 47 | preprocess: sveltePreprocess({ 48 | sourceMap: !production, 49 | typescript: { 50 | compilerOptions: { 51 | target: 'ES2015', 52 | module: 'ES2015', 53 | }, 54 | }, 55 | replace: [[/process\.env\.(\w+)/g, (_, prop) => JSON.stringify(process.env[prop])]], 56 | }), 57 | compilerOptions: { 58 | // enable run-time checks when not in production 59 | dev: !production, 60 | }, 61 | }), 62 | // we'll extract any component CSS out into 63 | // a separate file - better for performance 64 | css({ output: 'bundle.css' }), 65 | 66 | babel({ 67 | extensions: ['.js', '.ts', '.mjs', '.html', '.svelte'], 68 | runtimeHelpers: true, 69 | exclude: ['node_modules/@babel/**'], 70 | presets: [ 71 | [ 72 | '@babel/preset-env', 73 | { 74 | targets: { firefox: '48' }, 75 | exclude: [ 76 | '@babel/plugin-transform-regenerator' 77 | ] 78 | }, 79 | ], 80 | ], 81 | plugins: [ 82 | '@babel/plugin-syntax-dynamic-import', 83 | [ 84 | '@babel/plugin-transform-runtime', 85 | { 86 | "regenerator": false, 87 | useESModules: true, 88 | }, 89 | ], 90 | ], 91 | }), 92 | 93 | replace({ 94 | preventAssignment: true, 95 | 'process.env.NODE_ENV': !production ? "'development'" : "'production'", 96 | }), 97 | 98 | json(), 99 | 100 | // If you have external dependencies installed from 101 | // npm, you'll most likely need these plugins. In 102 | // some cases you'll need additional configuration - 103 | // consult the documentation for details: 104 | // https://github.com/rollup/plugins/tree/master/packages/commonjs 105 | resolve({ 106 | browser: true, 107 | dedupe: ['svelte'], 108 | }), 109 | commonjs(), 110 | typescript({ 111 | sourceMap: !production, 112 | inlineSources: !production, 113 | }), 114 | 115 | // In dev mode, call `npm run start` once 116 | // the bundle has been generated 117 | !production && serve(), 118 | 119 | // Watch the `public` directory and refresh the 120 | // browser on changes when not in production 121 | !production && livereload('public'), 122 | 123 | // If we're building for production (npm run build 124 | // instead of npm run dev), minify 125 | production && terser(), 126 | ], 127 | watch: { 128 | clearScreen: false, 129 | }, 130 | }; 131 | -------------------------------------------------------------------------------- /src/App.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 | 23 |
24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
39 | 40 |
41 | 42 | 63 | -------------------------------------------------------------------------------- /src/components/AppBar.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 |
{title}
22 | 23 | 39 | -------------------------------------------------------------------------------- /src/components/Button.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 | 9 | {text} 10 | 11 |
12 | 13 | 39 | -------------------------------------------------------------------------------- /src/components/Checkbox.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 15 | -------------------------------------------------------------------------------- /src/components/DatePicker.svelte: -------------------------------------------------------------------------------- 1 | 195 | 196 | 197 | 198 |
199 |
200 |
{title}
201 |
202 |
203 |
204 |
-1M
205 | 206 |
+1M
207 |
208 | 209 |
210 |
-1D
211 | 212 |
+1D
213 |
214 | 215 |
216 |
-1Y
217 | 218 |
+1Y
219 |
220 |
221 |
222 |
223 |
224 | 225 | 293 | -------------------------------------------------------------------------------- /src/components/Dialog.svelte: -------------------------------------------------------------------------------- 1 | 93 | 94 | 95 | 96 |
97 |
98 |
{title}
99 | {#if html} 100 |
{@html body}
101 | {:else} 102 |
{body}
103 | {/if} 104 |
105 |
106 | 107 | 141 | -------------------------------------------------------------------------------- /src/components/LinearProgress.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 |
20 | {#if progressType != 0 || label != null} 21 |
22 | {label == null ? '' : label} 23 | {#if progressType == 1} 24 | {progress}% 25 | {:else if progressType == 2} 26 | {value}/{max} 27 | {/if} 28 |
29 | {/if} 30 |
31 |
32 |
33 |
34 |
35 |
36 | 37 | 94 | -------------------------------------------------------------------------------- /src/components/ListView.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 | {#if $$slots.leftWidget} 11 | 12 | {/if} 13 | 14 |
15 |

{title}

16 | {#if subtitle}{subtitle}{/if} 17 |
18 |
19 | 20 |
21 | 22 | 81 | -------------------------------------------------------------------------------- /src/components/LoadingBar.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 |
17 |
18 |
19 |
20 | 21 | 273 | -------------------------------------------------------------------------------- /src/components/MultiSelector.svelte: -------------------------------------------------------------------------------- 1 | 104 | 105 | 106 | 107 |
108 |
109 |
{title}
110 |
111 | {#each options as option, i} 112 | 113 | 114 | 115 | {/each} 116 |
117 |
118 |
119 | 120 | 155 | -------------------------------------------------------------------------------- /src/components/OptionMenu.svelte: -------------------------------------------------------------------------------- 1 | 75 | 76 | 77 | 78 |
79 |
80 |
{title}
81 |
82 | {#each options as option} 83 | 84 | {@html (option.icon ? option.icon : '')} 85 | 86 | 87 | {/each} 88 |
89 |
90 |
91 | 92 | 127 | -------------------------------------------------------------------------------- /src/components/Radio.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 15 | -------------------------------------------------------------------------------- /src/components/RangeSlider.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 |
20 | {#if progressType != 0 || label != null} 21 |
22 | {label == null ? '' : label} 23 | {#if progressType == 1} 24 | {progress}% 25 | {:else if progressType == 2} 26 | {value}/{max} 27 | {/if} 28 |
29 | {/if} 30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | 38 | 111 | -------------------------------------------------------------------------------- /src/components/Separator.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
{title}
6 | 7 | 21 | -------------------------------------------------------------------------------- /src/components/SingleSelector.svelte: -------------------------------------------------------------------------------- 1 | 93 | 94 | 95 | 96 |
97 |
98 |
{title}
99 |
100 | {#each options as option, i} 101 | 102 | 103 | 104 | {/each} 105 |
106 |
107 |
108 | 109 | 144 | -------------------------------------------------------------------------------- /src/components/SoftwareKey.svelte: -------------------------------------------------------------------------------- 1 | 37 | 38 |
39 |
{leftText}
40 |
{centerText}
41 |
{rightText}
42 |
43 | 44 | 78 | -------------------------------------------------------------------------------- /src/components/TextAreaDialog.svelte: -------------------------------------------------------------------------------- 1 | 104 | 105 | 106 | 107 |
108 |
109 |
{title}
110 |
111 | 112 |
113 |
114 |
115 | 116 | 146 | -------------------------------------------------------------------------------- /src/components/TextAreaField.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 | {#if label} 15 | 16 | {/if} 17 | 18 |
19 | 20 | 48 | -------------------------------------------------------------------------------- /src/components/TextInputDialog.svelte: -------------------------------------------------------------------------------- 1 | 105 | 106 | 107 | 108 |
109 |
110 |
{title}
111 |
112 | 113 |
114 |
115 |
116 | 117 | 147 | -------------------------------------------------------------------------------- /src/components/TextInputField.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 |
13 | {#if label} 14 | 15 | {/if} 16 | 17 |
18 | 19 | 48 | -------------------------------------------------------------------------------- /src/components/TimePicker.svelte: -------------------------------------------------------------------------------- 1 | 225 | 226 | 227 | 228 |
229 |
230 |
{title}
231 |
232 |
233 |
234 |
-HH
235 | 236 |
+HH
237 |
238 |
239 |
240 |
-MM
241 | 242 |
+MM
243 |
244 | {#if is12HourSystem } 245 |
246 |
247 |
-DD
248 | 249 |
+DD
250 |
251 | {/if} 252 |
253 |
254 |
255 |
256 | 257 | 325 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | import AppBar from "./AppBar.svelte"; 2 | import SoftwareKey from "./SoftwareKey.svelte"; 3 | import Dialog from "./Dialog.svelte";; 4 | import OptionMenu from "./OptionMenu.svelte"; 5 | import SingleSelector from "./SingleSelector.svelte"; 6 | import MultiSelector from "./MultiSelector.svelte"; 7 | import ListView from "./ListView.svelte"; 8 | import Separator from "./Separator.svelte"; 9 | import Radio from "./Radio.svelte"; 10 | import Checkbox from "./Checkbox.svelte"; 11 | import LoadingBar from "./LoadingBar.svelte"; 12 | import LinearProgress from "./LinearProgress.svelte"; 13 | import Button from "./Button.svelte"; 14 | import RangeSlider from "./RangeSlider.svelte"; 15 | import TextInputField from "./TextInputField.svelte"; 16 | import TextAreaField from "./TextAreaField.svelte"; 17 | import TextInputDialog from "./TextInputDialog.svelte"; 18 | import TextAreaDialog from "./TextAreaDialog.svelte"; 19 | import DatePicker from "./DatePicker.svelte"; 20 | import TimePicker from "./TimePicker.svelte"; 21 | import { SvelteToast, toast } from '@zerodevx/svelte-toast' 22 | 23 | export { 24 | AppBar, 25 | SoftwareKey, 26 | Dialog, 27 | OptionMenu, 28 | SingleSelector, 29 | MultiSelector, 30 | ListView, 31 | Separator, 32 | Radio, 33 | Checkbox, 34 | LoadingBar, 35 | LinearProgress, 36 | Button, 37 | RangeSlider, 38 | TextInputField, 39 | TextAreaField, 40 | TextInputDialog, 41 | TextAreaDialog, 42 | DatePicker, 43 | TimePicker, 44 | SvelteToast as Toast, 45 | toast as Toaster 46 | } 47 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | /// -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import App from './App.svelte'; 2 | 3 | const app = new App({ 4 | target: document.body, 5 | props: {} 6 | }); 7 | 8 | export default app; 9 | -------------------------------------------------------------------------------- /src/routes/Demo.svelte: -------------------------------------------------------------------------------- 1 | 552 | 553 |
554 | onClickHandler('room')}/> 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 599 |
600 | 601 | 607 | -------------------------------------------------------------------------------- /src/routes/Room.svelte: -------------------------------------------------------------------------------- 1 | 47 | 48 |
49 |

Hello {name}!

50 |
51 |
Vertical 1
52 |
Vertical 2
53 |
54 |
55 |
Horizontal 1
56 |
Horizontal 2
57 |
58 |
59 | 60 | 85 | -------------------------------------------------------------------------------- /src/routes/Welcome.svelte: -------------------------------------------------------------------------------- 1 | 46 | 47 |
48 |

Hello {name}!

49 |
50 |
Vertical 1
51 |
Vertical 2
52 |
53 |
54 |
Horizontal 1
55 |
Horizontal 2
56 |
57 |
58 | 59 | 84 | -------------------------------------------------------------------------------- /src/routes/index.ts: -------------------------------------------------------------------------------- 1 | import Welcome from "./Welcome.svelte"; 2 | import Demo from "./Demo.svelte"; 3 | import Room from "./Room.svelte"; 4 | 5 | export { 6 | Welcome, 7 | Demo, 8 | Room 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/localization.ts: -------------------------------------------------------------------------------- 1 | import { sprintf } from 'sprintf-js'; 2 | 3 | interface Translation { 4 | [key: string]: string; 5 | } 6 | 7 | interface Locale { 8 | [locale: string]: Translation; 9 | } 10 | 11 | class Localization { 12 | 13 | locales: Locale = {}; 14 | namespace: string; 15 | defaultLocale: string; 16 | 17 | constructor(locale: string, namespace: string) { 18 | this.defaultLocale = locale; 19 | this.namespace = namespace; 20 | this.loadLocale(this.defaultLocale); 21 | } 22 | 23 | loadLocale(locale: string, cache: boolean = true, origin: string = document.location.origin): boolean { 24 | if (cache && this.locales[locale] != null) { 25 | this.defaultLocale = locale; 26 | return true; 27 | } else { 28 | const url = []; 29 | url.push(origin); 30 | if (this.namespace !== '' && origin === document.location.origin) 31 | url.push(this.namespace); 32 | url.push(`${locale}.json`); 33 | const request = new XMLHttpRequest(); 34 | request.open('GET', url.join('/'), false); 35 | request.send(null); 36 | if (request.readyState === 4 && request.status >= 200 && request.status <= 399) { 37 | this.defaultLocale = locale; 38 | this.locales[this.defaultLocale] = JSON.parse(request.responseText); 39 | return true; 40 | } else if (request.readyState === 4) { 41 | return false; 42 | } 43 | } 44 | } 45 | 46 | lang(key: string, ...args: any): string | boolean { 47 | const line = this.locales[this.defaultLocale][key]; 48 | if (line == null) 49 | return false; 50 | return sprintf(line, ...args); 51 | } 52 | 53 | langByLocale(key: string, locale: string, ...args: any): string | boolean { 54 | if (this.locales[locale] == null) 55 | return false; 56 | const line = this.locales[locale][key]; 57 | if (line == null) 58 | return false; 59 | return sprintf(line, ...args); 60 | } 61 | 62 | getLocaleTranslation(locale: string): Translation { 63 | return this.locales[locale]; 64 | } 65 | 66 | } 67 | 68 | export { 69 | Localization 70 | } 71 | -------------------------------------------------------------------------------- /src/utils/navigation.ts: -------------------------------------------------------------------------------- 1 | function keydownEventHandler(evt, scope) { 2 | switch (evt.key) { 3 | case 'Backspace': 4 | case 'EndCall': 5 | if (['INPUT', 'TEXTAREA'].indexOf(evt.target.tagName) > -1 && evt.target.value == '') { 6 | evt.preventDefault(); 7 | evt.stopPropagation(); 8 | } else { 9 | scope.backspaceListener(evt); 10 | } 11 | break; 12 | case 'SoftLeft': 13 | case 'PageUp': 14 | case 'Shift': 15 | scope.softkeyLeftListener(evt); 16 | evt.preventDefault(); 17 | break; 18 | case 'SoftRight': 19 | case 'PageDown': 20 | case 'Control': 21 | scope.softkeyRightListener(evt); 22 | evt.preventDefault(); 23 | break; 24 | case 'Enter': 25 | scope.enterListener(evt); 26 | break; 27 | case 'ArrowUp': 28 | scope.arrowUpListener(evt); 29 | break; 30 | case 'ArrowDown': 31 | scope.arrowDownListener(evt); 32 | break; 33 | case 'ArrowLeft': 34 | scope.arrowLeftListener(evt); 35 | break; 36 | case 'ArrowRight': 37 | scope.arrowRightListener(evt); 38 | break; 39 | } 40 | } 41 | 42 | function isElementInViewport(el, marginTop = 0, marginBottom = 0) { 43 | if (el.parentElement.getAttribute("data-pad-top")) 44 | marginTop = parseFloat(el.parentElement.getAttribute("data-pad-top")); 45 | if (el.parentElement.getAttribute("data-pad-bottom")) 46 | marginBottom = parseFloat(el.parentElement.getAttribute("data-pad-bottom")); 47 | const rect = el.getBoundingClientRect(); 48 | return ( 49 | rect.top >= 0 + marginTop && 50 | rect.left >= 0 && 51 | rect.bottom <= ((window.innerHeight || document.documentElement.clientHeight) - marginBottom) && /* or $(window).height() */ 52 | rect.right <= (window.innerWidth || document.documentElement.clientWidth) /* or $(window).width() */ 53 | ); 54 | } 55 | 56 | function dispatchScroll(target, newScrollTop) { 57 | target.scroll({ top: newScrollTop, behavior: 'smooth' }); 58 | } 59 | 60 | function dispatchFocus(target, newScrollTop) { 61 | target.scrollTop = newScrollTop; 62 | const e = document.createEvent("UIEvents"); 63 | e.initUIEvent("scroll", true, true, window, 1); 64 | target.dispatchEvent(e); 65 | } 66 | 67 | class KaiNavigator { 68 | private init: boolean = false; 69 | private eventHandler: any; // actual is EventListenerObject, any to suppress error 70 | target: string; 71 | verticalNavIndex: number = -1; 72 | verticalNavClass: string; 73 | horizontalNavIndex: number = -1; 74 | horizontalNavClass: string; 75 | arrowUpListener: Function = (evt) => { 76 | if (this.verticalNavClass) { 77 | evt.preventDefault(); 78 | this.navigateListNav(-1); 79 | } 80 | }; 81 | arrowDownListener: Function = (evt) => { 82 | if (this.verticalNavClass) { 83 | evt.preventDefault(); 84 | this.navigateListNav(1); 85 | } 86 | }; 87 | arrowLeftListener: Function = (evt) => { 88 | if (this.horizontalNavClass) { 89 | evt.preventDefault(); 90 | this.navigateTabNav(-1); 91 | } 92 | }; 93 | arrowRightListener: Function = (evt) => { 94 | if (this.horizontalNavClass) { 95 | evt.preventDefault(); 96 | this.navigateTabNav(1); 97 | } 98 | }; 99 | softkeyLeftListener: Function = (evt) => {}; 100 | softkeyRightListener: Function = (evt) => {}; 101 | enterListener: Function = (evt) => {}; 102 | backspaceListener: Function = (evt) => {}; 103 | 104 | constructor(opts = {}) { 105 | for(const x in opts) { 106 | if (typeof opts[x] === 'function') 107 | typeof opts[x]; 108 | this[x] = opts[x]; 109 | } 110 | this.eventHandler = (evt: any) => { 111 | keydownEventHandler(evt, this); 112 | } 113 | } 114 | 115 | navigateListNav(next) { 116 | return this.nav(next, 'verticalNavIndex', 'verticalNavClass'); 117 | } 118 | 119 | navigateTabNav(next) { 120 | return this.nav(next, 'horizontalNavIndex', 'horizontalNavClass'); 121 | } 122 | 123 | nav(next, navIndex, navClass) { 124 | const currentIndex = this[navIndex]; 125 | const nav = document.getElementsByClassName(this[navClass]); 126 | if (nav.length === 0) { 127 | return; 128 | } 129 | var move = currentIndex + next; 130 | var cursor:any = nav[move]; 131 | if (cursor != undefined) { 132 | cursor.focus(); 133 | this[navIndex] = move; 134 | } else { 135 | if (move < 0) { 136 | move = nav.length - 1; 137 | } else if (move >= nav.length) { 138 | move = 0; 139 | } 140 | cursor = nav[move]; 141 | cursor.focus(); 142 | this[navIndex] = move; 143 | } 144 | cursor.classList.add('focus'); 145 | if (currentIndex > -1 && nav.length > 1) { 146 | nav[currentIndex].classList.remove('focus'); 147 | } 148 | if (!isElementInViewport(cursor)) { 149 | var marginTop = 0, marginBottom = 0; 150 | if (cursor.parentElement.getAttribute("data-pad-top")) 151 | marginTop = parseFloat(cursor.parentElement.getAttribute("data-pad-top")); 152 | if (cursor.parentElement.getAttribute("data-pad-bottom")) 153 | marginBottom = parseFloat(cursor.parentElement.getAttribute("data-pad-bottom")); 154 | let offsetTop = cursor.offsetTop - ((cursor.parentElement.clientHeight - marginTop - marginBottom) / 2); 155 | if ((cursor.clientHeight / cursor.parentElement.clientHeight) >= 0.7) 156 | offsetTop = cursor.offsetTop; 157 | dispatchScroll(cursor.parentElement, offsetTop); 158 | setTimeout(() => { 159 | if (!isElementInViewport(cursor)) { 160 | dispatchFocus(cursor.parentElement, offsetTop); 161 | } 162 | }, 150); 163 | } 164 | if (['INPUT', 'TEXTAREA'].indexOf(document.activeElement.tagName) > -1) { 165 | if (document.activeElement instanceof HTMLElement) { 166 | document.activeElement.blur(); 167 | } 168 | } 169 | const keys = Object.keys(cursor.children); 170 | for (var k in keys) { 171 | if (['INPUT', 'TEXTAREA'].indexOf(cursor.children[k].tagName) > -1) { 172 | setTimeout(() => { 173 | cursor.children[k].focus(); 174 | cursor.children[k].selectionStart = cursor.children[k].selectionEnd = (cursor.children[k].value.length || cursor.children[k].value.length); 175 | }, 100); 176 | break; 177 | } 178 | } 179 | } 180 | 181 | attachListener(next:number = 1) { 182 | document.addEventListener('keydown', this.eventHandler); 183 | if (!this.init) 184 | this.init = true; 185 | else 186 | return; 187 | setTimeout(() => { 188 | if (this.verticalNavClass != null) 189 | this.navigateListNav(next); 190 | else if (this.horizontalNavClass != null) 191 | this.navigateTabNav(next); 192 | }, 100); 193 | } 194 | 195 | detachListener() { 196 | document.removeEventListener('keydown', this.eventHandler); 197 | } 198 | } 199 | 200 | const createKaiNavigator = (opts = {}) => { 201 | return new KaiNavigator(opts); 202 | } 203 | 204 | export { 205 | createKaiNavigator, 206 | KaiNavigator 207 | } 208 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "target": "es5", 3 | "extends": "@tsconfig/svelte/tsconfig.json", 4 | "compilerOptions": { "resolveJsonModule": true }, 5 | "include": ["src/**/*"], 6 | "exclude": ["node_modules/*", "__sapper__/*", "public/*"] 7 | } 8 | --------------------------------------------------------------------------------