├── src
├── hooks
│ ├── index.js
│ └── useNavigation.js
├── components
│ ├── index.js
│ ├── Header
│ │ ├── Header.js
│ │ └── Header.module.css
│ ├── Input
│ │ ├── Input.js
│ │ └── Input.module.css
│ ├── Todos
│ │ ├── ToDos.js
│ │ └── ToDos.module.css
│ └── Softkey
│ │ ├── Softkey.module.css
│ │ └── Softkey.js
├── index.js
├── index.css
└── App.js
├── docs
├── to-do.png
├── to-do-landscape.gif
├── to-do-on-input.png
└── to-do-portrait.gif
├── .vscode
├── settings.json
└── tasks.json
├── public
├── assets
│ └── icons
│ │ ├── kaios_56.png
│ │ └── kaios_112.png
├── index.html
└── manifest.webapp
├── .gitignore
├── package.json
└── README.md
/src/hooks/index.js:
--------------------------------------------------------------------------------
1 | export * from './useNavigation';
--------------------------------------------------------------------------------
/docs/to-do.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaiostech/sample-react/HEAD/docs/to-do.png
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "files.associations": {
3 | "*.webapp": "json",
4 | },
5 | }
--------------------------------------------------------------------------------
/docs/to-do-landscape.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaiostech/sample-react/HEAD/docs/to-do-landscape.gif
--------------------------------------------------------------------------------
/docs/to-do-on-input.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaiostech/sample-react/HEAD/docs/to-do-on-input.png
--------------------------------------------------------------------------------
/docs/to-do-portrait.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaiostech/sample-react/HEAD/docs/to-do-portrait.gif
--------------------------------------------------------------------------------
/public/assets/icons/kaios_56.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaiostech/sample-react/HEAD/public/assets/icons/kaios_56.png
--------------------------------------------------------------------------------
/public/assets/icons/kaios_112.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaiostech/sample-react/HEAD/public/assets/icons/kaios_112.png
--------------------------------------------------------------------------------
/src/components/index.js:
--------------------------------------------------------------------------------
1 | export * from './Input/Input';
2 | export * from './Header/Header';
3 | export * from './Todos/ToDos';
4 | export * from './Softkey/Softkey';
5 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import "./index.css";
4 | import App from "./App";
5 |
6 | ReactDOM.render(, document.getElementById("root"));
7 |
--------------------------------------------------------------------------------
/src/components/Header/Header.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import css from './Header.module.css';
3 |
4 | export const Header = ({ title }) => {
5 | return (
6 |
9 | )
10 | }
11 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | }
4 |
5 | html,
6 | body,
7 | #root {
8 | font-family: "Open Sans", sans-serif;
9 | margin: 0;
10 | display: flex;
11 | flex-direction: column;
12 | overflow: hidden;
13 | background-color: #E1E2E1 !important;
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/Input/Input.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import css from "./Input.module.css";
3 |
4 | export const Input = ({ label, type }) => (
5 |
6 |
7 |
8 |
9 | )
10 |
--------------------------------------------------------------------------------
/src/components/Header/Header.module.css:
--------------------------------------------------------------------------------
1 | header {
2 | height: 36px;
3 | display: flex;
4 | align-items: center;
5 | justify-content: flex-start;
6 | background-color: #9b27af;
7 | padding: 3px 10px;
8 | }
9 |
10 | header span {
11 | font-size: 16px;
12 | font-weight: 600;
13 | color: #ffffff;
14 | text-transform: uppercase;
15 | }
16 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 | My first React App KaiOS
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
25 | .idea
26 |
--------------------------------------------------------------------------------
/src/components/Todos/ToDos.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import css from './ToDos.module.css';
3 |
4 | export const ToDos = ({ toDos }) => {
5 | if (toDos === undefined || !toDos.length) return null;
6 |
7 | return (
8 |
9 | {toDos.map((toDo, index) => (
10 |
14 | {toDo.name}
15 |
16 | ))}
17 |
18 | )
19 | }
20 |
21 |
--------------------------------------------------------------------------------
/src/components/Todos/ToDos.module.css:
--------------------------------------------------------------------------------
1 | .todos {
2 | display: flex;
3 | flex-direction: column;
4 | padding: 0 10px;
5 | }
6 |
7 | .todos span {
8 | display: flex;
9 | align-items: center;
10 | width: 100%;
11 | height: 40px;
12 | box-shadow: 0 2px 6px 0 rgba(0, 0, 0, .2);
13 | border-radius: 6px;
14 | padding: 0 10px;
15 | margin-bottom: 9px;
16 | background-color: #F5F5F6;
17 | }
18 |
19 | .todos span[nav-selected="true"] {
20 | background-image: linear-gradient(255deg, #9B27AF, #9B27AF);
21 | color: #FFF;
22 | }
23 |
24 | .todos .completed {
25 | text-decoration: line-through;
26 | }
--------------------------------------------------------------------------------
/public/manifest.webapp:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.0.0",
3 | "name": "My first React App for KaiOS",
4 | "description": "Simple example of a to-do list",
5 | "type": "web",
6 | "launch_path": "/index.html",
7 | "icons": {
8 | "56": "/assets/icons/kaios_56.png",
9 | "112": "/assets/icons/kaios_112.png"
10 | },
11 | "developer": {
12 | "name": "KaiOS Team",
13 | "url": "https://www.kaiostech.com"
14 | },
15 | "locales": {
16 | "en-US": {
17 | "name": "My first React App for KaiOS",
18 | "subtitle": "Simple example of a to-do list",
19 | "description": "Simple example of a to-do list"
20 | }
21 | },
22 | "default_locale": "en-US"
23 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "my-first-react-app-kaios",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "react": "^16.8.6",
7 | "react-dom": "^16.8.6",
8 | "react-scripts": "3.0.1"
9 | },
10 | "scripts": {
11 | "start": "react-scripts start",
12 | "build": "react-scripts build",
13 | "test": "react-scripts test",
14 | "eject": "react-scripts eject"
15 | },
16 | "eslintConfig": {
17 | "extends": "react-app"
18 | },
19 | "browserslist": {
20 | "production": [
21 | ">0.2%",
22 | "not dead",
23 | "not op_mini all"
24 | ],
25 | "development": [
26 | "last 1 chrome version",
27 | "last 1 firefox version",
28 | "last 1 safari version"
29 | ]
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Sample React app for KaiOS
2 |
3 | Simple example of a to-do list, for more information see [KaiOS Developer Portal](https://developer.kaiostech.com/getting-started/build-your-first-app/sample-code#react)
4 |
5 | 
6 | 
7 |
8 | In portrait devices
9 |
10 | 
11 |
12 | In landscape devices
13 |
14 | 
15 |
16 | ## Start
17 |
18 | ```console
19 | npm run start
20 | # or
21 | yarn start
22 | ```
23 |
24 | ## Build app
25 |
26 | ```console
27 | npm run build
28 | # or
29 | yarn build
30 | ```
31 |
32 | ## Send the app to a KaiOS device
33 |
34 | follow [os-env-setup](https://developer.kaiostech.com/getting-started/env-setup/os-env-setup) and [test-your-apps](https://developer.kaiostech.com/getting-started/build-your-first-package-app/test-your-apps)
35 | install to your device.
36 |
--------------------------------------------------------------------------------
/src/components/Softkey/Softkey.module.css:
--------------------------------------------------------------------------------
1 | .softkey {
2 | height: 30px;
3 | max-height: 30px;
4 | width: 100%;
5 | max-width: 100%;
6 | background: white;
7 | border-top: 2px #cbcbcb solid;
8 | display: flex;
9 | flex-shrink: 0;
10 | white-space: nowrap;
11 | padding: 0 5px;
12 | font-weight: 700;
13 | box-sizing: border-box;
14 | line-height: 26px;
15 | margin-top: auto;
16 | position: absolute;
17 | bottom: 0;
18 | }
19 |
20 | .left,
21 | .right {
22 | font-weight: 600;
23 | font-size: 14px;
24 | color: #242424;
25 | overflow: hidden;
26 | width: 100%;
27 | letter-spacing: -0.5px;
28 | box-sizing: border-box;
29 | text-overflow: ellipsis;
30 | }
31 |
32 | .left {
33 | text-align: left;
34 | padding-right: 5px;
35 | }
36 |
37 | .center {
38 | color: #242424;
39 | text-transform: uppercase;
40 | font-size: 18px;
41 | text-align: center;
42 | max-width: 120px;
43 | overflow: hidden;
44 | text-overflow: ellipsis;
45 | width: 100%;
46 | }
47 |
48 | .right {
49 | text-align: right;
50 | padding-left: 5px;
51 | }
52 |
--------------------------------------------------------------------------------
/src/components/Softkey/Softkey.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import css from "./Softkey.module.css";
3 |
4 | export const Softkey = ({
5 | left,
6 | onKeyLeft,
7 | center,
8 | onKeyCenter,
9 | right,
10 | onKeyRight
11 | }) => {
12 | useEffect(() => {
13 | document.addEventListener("keydown", handleKeyDown);
14 |
15 | return () => document.removeEventListener("keydown", handleKeyDown);
16 | // eslint-disable-next-line react-hooks/exhaustive-deps
17 | }, []);
18 |
19 | const handleKeyDown = evt => {
20 | switch (evt.key) {
21 | case "SoftLeft":
22 | return onKeyLeft && onKeyLeft(evt);
23 | case "Enter":
24 | return onKeyCenter && onKeyCenter(evt);
25 | case "SoftRight":
26 | return onKeyRight && onKeyRight(evt);
27 | default:
28 | return;
29 | }
30 | };
31 |
32 | return (
33 |
34 |
35 |
36 |
37 |
38 | );
39 | };
40 |
--------------------------------------------------------------------------------
/src/components/Input/Input.module.css:
--------------------------------------------------------------------------------
1 | .input {
2 | display: flex;
3 | flex-direction: column;
4 | margin: 5px 10px;
5 | position: relative;
6 | padding-top: 7px;
7 | }
8 |
9 | .input input {
10 | font-family: inherit;
11 | width: 100%;
12 | border: 0;
13 | border-bottom: 1px solid #d2d2d2;
14 | outline: 0;
15 | font-size: 16px;
16 | color: transparent;
17 | padding: 7px 0;
18 | background: transparent;
19 | transition: border-color 0.2s;
20 | }
21 |
22 | .input label {
23 | position: absolute;
24 | bottom: 0;
25 | color: #9b9b9b;
26 | }
27 |
28 | .input input::placeholder {
29 | color: transparent;
30 | }
31 |
32 | .input input:placeholder-shown ~ label {
33 | font-size: 16px;
34 | cursor: text;
35 | top: 20px;
36 | }
37 |
38 | .input input[nav-selected="true"] ~ label {
39 | position: absolute;
40 | top: 0;
41 | display: block;
42 | transition: 0.2s;
43 | font-size: 12px;
44 | color: #9b9b9b;
45 | }
46 |
47 | .input input[nav-selected="true"] ~ label {
48 | color: #9b27af;
49 | }
50 |
51 | .input input[nav-selected="true"] {
52 | padding-bottom: 6px;
53 | border-bottom: 2px solid #9b27af;
54 | text-shadow: 0 0 0 rgba(0, 0, 0, 1);
55 | }
56 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "label": "Developer mode (Watch)",
6 | "type": "npm",
7 | "script": "start",
8 | "problemMatcher": [],
9 | "presentation": {
10 | "reveal": "silent",
11 | "panel": "dedicated"
12 | },
13 | "group": {
14 | "kind": "build",
15 | "isDefault": true
16 | }
17 | },
18 | {
19 | "label": "Build app",
20 | "type": "npm",
21 | "script": "build",
22 | "problemMatcher": [],
23 | "presentation": {
24 | "reveal": "silent",
25 | "panel": "dedicated"
26 | }
27 | },
28 | {
29 | "label": "Install app",
30 | "type": "npm",
31 | "script": "app:install",
32 | "problemMatcher": [],
33 | "presentation": {
34 | "reveal": "silent",
35 | "panel": "dedicated"
36 | }
37 | },
38 | {
39 | "label": "Uninstall app",
40 | "type": "npm",
41 | "script": "app:uninstall",
42 | "problemMatcher": [],
43 | "presentation": {
44 | "reveal": "silent",
45 | "panel": "dedicated"
46 | }
47 | },
48 | {
49 | "label": "Update app",
50 | "type": "npm",
51 | "script": "app:update",
52 | "problemMatcher": [],
53 | "presentation": {
54 | "reveal": "silent",
55 | "panel": "dedicated"
56 | }
57 | },
58 | {
59 | "label": "Start app",
60 | "type": "npm",
61 | "script": "app:start",
62 | "problemMatcher": [],
63 | "presentation": {
64 | "reveal": "silent",
65 | "panel": "dedicated"
66 | }
67 | },
68 | {
69 | "label": "Stop app",
70 | "type": "npm",
71 | "script": "app:stop",
72 | "problemMatcher": [],
73 | "presentation": {
74 | "reveal": "silent",
75 | "panel": "dedicated"
76 | }
77 | }
78 | ]
79 | }
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Header, Input, ToDos, Softkey } from "./components";
3 | import { useNavigation } from "./hooks";
4 |
5 | export default function App() {
6 | const [toDos, setToDo] = useState([]);
7 |
8 | const [current, setNavigation] = useNavigation();
9 |
10 | const onKeyCenter = () => {
11 | const currentElement = document.querySelector("[nav-selected=true]");
12 | const currentNavigationIndex = parseInt(currentElement.getAttribute("nav-index"), 10);
13 |
14 | const isATask = currentNavigationIndex > 0;
15 | if (isATask) {
16 | setToDo(prevState => {
17 | const current = [...prevState];
18 | current[currentNavigationIndex - 1].completed = !current[currentNavigationIndex - 1].completed;
19 | return current;
20 | });
21 | } else if (currentElement.value.length) {
22 | const toDo = { name: currentElement.value, completed: false };
23 | setToDo(prevState => [...prevState, toDo]);
24 | currentElement.value = "";
25 | }
26 | };
27 |
28 | const onKeyRight = () => {
29 | const currentIndex = parseInt(
30 | document.querySelector("[nav-selected=true]").getAttribute("nav-index"),
31 | 10
32 | );
33 | if (currentIndex > 0) {
34 | setToDo(prevState => {
35 | const current = [...prevState];
36 | current.splice(currentIndex - 1, 1);
37 | const goToPreviousElement = Boolean(current.length);
38 | setNavigation(goToPreviousElement ? currentIndex - 1 : 0);
39 | return current;
40 | });
41 | }
42 | };
43 |
44 | return (
45 | <>
46 |
47 |
48 |
49 |
50 |
51 |
57 | >
58 | );
59 | }
60 |
--------------------------------------------------------------------------------
/src/hooks/useNavigation.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 |
3 | export const useNavigation = () => {
4 | useEffect(() => {
5 | document.addEventListener('keydown', onKeyDown);
6 | setNavigation(0);
7 |
8 | return () => document.removeEventListener('keydown', onKeyDown);
9 | // eslint-disable-next-line react-hooks/exhaustive-deps
10 | }, []);
11 |
12 | const [current, setCurrent] = useState({ type: null, index: null });
13 |
14 | const getAllElements = () => document.querySelectorAll('[nav-selectable]');
15 |
16 | const getTheIndexOfTheSelectedElement = () => {
17 | const element = document.querySelector('[nav-selected=true]');
18 | return element ? parseInt(element.getAttribute('nav-index')) : 0;
19 | }
20 |
21 | const setNavigation = index => selectElement(getAllElements()[index] || document.body);
22 |
23 | const onKeyDown = evt => {
24 | if (evt.key !== 'ArrowDown' && evt.key !== 'ArrowUp') return;
25 |
26 | const allElements = getAllElements();
27 | const currentIndex = getTheIndexOfTheSelectedElement();
28 |
29 | let setIndex;
30 | switch (evt.key) {
31 | case 'ArrowDown':
32 | const goToFirstElement = currentIndex + 1 > allElements.length - 1;
33 | setIndex = goToFirstElement ? 0 : currentIndex + 1;
34 | return selectElement(allElements[setIndex] || allElements[0], setIndex);
35 | case 'ArrowUp':
36 | const goToLastElement = currentIndex === 0;
37 | setIndex = goToLastElement ? allElements.length - 1 : currentIndex - 1;
38 | return selectElement(allElements[setIndex] || allElements[0], setIndex);
39 | default:
40 | break;
41 | }
42 | }
43 |
44 | const selectElement = (selectElement, setIndex = 0) => {
45 | if (selectElement) {
46 | [].forEach.call(getAllElements(), (element, index) => {
47 | const selectThisElement = element;
48 | element.setAttribute("nav-selected", element === selectElement);
49 | element.setAttribute("nav-index", index);
50 | if (selectThisElement) {
51 | selectThisElement.scrollIntoView(true);
52 | if (element.nodeName === 'INPUT') {
53 | element.focus();
54 | } else {
55 | element.blur();
56 | }
57 | }
58 | });
59 | setCurrent({ type: selectElement.tagName, index: setIndex });
60 | } else {
61 | setNavigation(0);
62 | }
63 | }
64 |
65 | return [current, setNavigation];
66 | };
67 |
--------------------------------------------------------------------------------