You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
423 lines
9.1 KiB
423 lines
9.1 KiB
<head>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css" rel="stylesheet" />
|
|
<style>
|
|
* {
|
|
box-sizing: border-box;
|
|
}
|
|
html,
|
|
body {
|
|
background: rebeccapurple;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
[role="img"] {
|
|
pointer-events: none;
|
|
}
|
|
</style>
|
|
</head>
|
|
<hello-launcher></hello-launcher>
|
|
|
|
<script type="module">
|
|
import tag from 'https://thelanding.page/tag/tag.js'
|
|
|
|
const { html, css, set, get, on } = tag('hello-launcher')
|
|
|
|
const modes = {
|
|
welcome: 'welcome',
|
|
alias: 'alias',
|
|
context: 'context',
|
|
store: 'store',
|
|
download: 'download',
|
|
settings: 'settings',
|
|
home: 'home'
|
|
}
|
|
|
|
const actions = {
|
|
goto: 'goto',
|
|
next: 'next',
|
|
back: 'back'
|
|
}
|
|
|
|
const strictModes = [modes.welcome, modes.alias, modes.home]
|
|
const paginationActions = [actions.back, actions.next]
|
|
|
|
on('click', '.next', action(actions.next))
|
|
on('click', '.back', action(actions.back))
|
|
|
|
on('click', '.welcome', goTo(modes.welcome))
|
|
on('click', '.home', goTo(modes.home))
|
|
on('click', '.store', goTo(modes.store))
|
|
on('click', '.download', goTo(modes.download))
|
|
on('click', '.settings', goTo(modes.settings))
|
|
|
|
html(target => {
|
|
const { id } = target
|
|
|
|
const launcher = launcherById(id)
|
|
|
|
const renderers = {
|
|
[modes.welcome]: () => `
|
|
<div class="card">
|
|
<h2>Welcome</h2>
|
|
<button data-id="${id}" class="next">
|
|
Continue
|
|
</button>
|
|
</div>
|
|
`,
|
|
[modes.alias]: () => `
|
|
<div class="card">
|
|
<h2>Alias</h2>
|
|
<button data-id="${id}" class="next">
|
|
Continue
|
|
</button>
|
|
<button data-id="${id}" class="back">
|
|
Go Back
|
|
</button>
|
|
</div>
|
|
`,
|
|
[modes.context]: () => `
|
|
<div class="card">
|
|
<h2>Context</h2>
|
|
<button data-id="${id}" class="next">
|
|
Continue
|
|
</button>
|
|
<button data-id="${id}" class="back">
|
|
Go Back
|
|
</button>
|
|
</div>
|
|
`,
|
|
[modes.home]: () => `
|
|
<div class="icons">
|
|
<button data-id="${id}" class="store">
|
|
<span role="img" aria-labelledby="Store">🏬</span>
|
|
</button>
|
|
<button data-id="${id}" class="download">
|
|
<span role="img" aria-labelledby="Download">📥</span>
|
|
</button>
|
|
<button data-id="${id}" class="settings">
|
|
<span role="img" aria-labelledby="Settings">⚙️</span>
|
|
</button>
|
|
</div>
|
|
`,
|
|
[modes.store]: () => `
|
|
<first-party-app>
|
|
Store
|
|
</first-party-app>
|
|
`,
|
|
[modes.download]: () => `
|
|
<first-party-app>
|
|
Download
|
|
</first-party-app>
|
|
`,
|
|
[modes.settings]: () => `
|
|
<first-party-app>
|
|
Settings<br/>
|
|
<button data-id="${id}" class="welcome">
|
|
Log out
|
|
</button>
|
|
</first-party-app>
|
|
`,
|
|
'default': () => `
|
|
<div class="card">
|
|
<h2>Error...</h2>
|
|
<button data-id="${id}" class="home">
|
|
Go Home
|
|
</button>
|
|
</div>
|
|
`
|
|
}
|
|
|
|
const { mode, nextMode } = launcher
|
|
const view = (renderers[mode] || renderers['default'])()
|
|
const fadeOut = nextMode && mode !== nextMode
|
|
|
|
return `
|
|
<div class="mode-${mode}">
|
|
<transition class="${fadeOut ? 'out' : ''}" data-id="${id}">
|
|
${view}
|
|
</transition>
|
|
</div>
|
|
<launch-home>
|
|
${showHomeButton(id, launcher)}
|
|
</launch-home>
|
|
`
|
|
})
|
|
|
|
function showHomeButton(id, launcher) {
|
|
const { mode, nextMode, emojiLabel } = launcher
|
|
const fadeOut = strictModes.includes(nextMode)
|
|
return strictModes.includes(mode)
|
|
? ''
|
|
: `<button class="launch-home home ${fadeOut ? 'out' : ''}" data-id="${id}">
|
|
${emojiLabel}
|
|
</button>`
|
|
}
|
|
|
|
function transition({target}) {
|
|
const { id } = target.dataset
|
|
const { mode, nextMode, backMode } = launcherById(id)
|
|
|
|
const currentMode = nextMode ? nextMode : mode
|
|
const previousMode = mode !== backMode ? backMode : mode
|
|
set({ mode: currentMode, backMode: previousMode }, merge(id))
|
|
target.scrollTop = '0'
|
|
document.activeElement.blur()
|
|
}
|
|
on('animationend', 'transition', transition)
|
|
|
|
css(`
|
|
& {
|
|
background: white;
|
|
display: block;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
& *:focus {
|
|
border-radius: none;
|
|
outline: 2px dashed orange;
|
|
outline-offset: .5rem;
|
|
}
|
|
|
|
& [class^="mode-"] {
|
|
display: grid;
|
|
height: 100%;
|
|
place-items: center;
|
|
width: 100%;
|
|
}
|
|
|
|
& button {
|
|
display: block;
|
|
min-height: 3rem;
|
|
margin: 1rem 0;
|
|
width: 100%;
|
|
}
|
|
|
|
& launch-home {
|
|
background: none;
|
|
border: none;
|
|
display: block;
|
|
position: absolute;
|
|
inset: auto auto auto 50%;
|
|
transform: translate(-50%, -75%);
|
|
}
|
|
|
|
& launch-home button {
|
|
animation: ease-in-out 250ms;
|
|
animation-name: &-zoom-in, &-fade-in;
|
|
background: transparent;
|
|
border: 3px solid dodgerblue;
|
|
border-radius: 100%;
|
|
color: white;
|
|
cursor: pointer;
|
|
display: grid;
|
|
font-size: 2rem;
|
|
height: 4rem;
|
|
padding: .25rem;
|
|
place-content: start center;
|
|
text-shadow: 0 0 5px dodgerblue;
|
|
transition: background 250ms ease-in-out;
|
|
width: 4rem;
|
|
}
|
|
|
|
& launch-home button:hover,
|
|
& launch-home button:focus {
|
|
background: dodgerblue;
|
|
}
|
|
|
|
& launch-home button:active {
|
|
background: orange;
|
|
}
|
|
& launch-home button.out {
|
|
animation: ease-in-out 100ms;
|
|
animation-name: &-zoom-out, &-fade-out;
|
|
}
|
|
|
|
& first-party-app {
|
|
background: rgba(0,0,0,.85);
|
|
color: #fff;
|
|
}
|
|
|
|
& third-party-app {
|
|
background: white;
|
|
}
|
|
|
|
& first-party-app,
|
|
& third-party-app {
|
|
display: block;
|
|
padding: 1rem;
|
|
height: 100%;
|
|
width: 100%;
|
|
}
|
|
|
|
& transition {
|
|
animation: &-fade-in ease-in-out 250ms;
|
|
display: grid;
|
|
height: 100%;
|
|
place-items: center;
|
|
width: 100%;
|
|
}
|
|
|
|
& transition.out {
|
|
animation: &-fade-out ease-in-out 100ms;
|
|
}
|
|
|
|
& .mode-${modes.home},
|
|
& .mode-${modes.store},
|
|
& .mode-${modes.download},
|
|
& .mode-${modes.settings} {
|
|
background: dodgerblue;
|
|
}
|
|
|
|
& .icons {
|
|
display: grid;
|
|
height: 100%;
|
|
gap: 1rem;
|
|
grid-template-columns: repeat(auto-fill, 4rem);
|
|
grid-template-rows: repeat(auto-fill, 4rem);
|
|
padding: 1rem;
|
|
width: 100%;
|
|
}
|
|
|
|
& .icons button {
|
|
margin: 0;
|
|
}
|
|
|
|
& .mode-${modes.home} transition {
|
|
animation-name: &-zoom-in, &-fade-in;
|
|
}
|
|
|
|
& .mode-${modes.home} transition.out {
|
|
animation-name: &-zoom-out, &-fade-out;
|
|
}
|
|
|
|
@keyframes &-fade-in {
|
|
0% {
|
|
opacity: 0;
|
|
}
|
|
100% {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
@keyframes &-fade-out {
|
|
0% {
|
|
opacity: 1;
|
|
}
|
|
100% {
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
@keyframes &-zoom-in {
|
|
0% {
|
|
transform: scale(.9);
|
|
}
|
|
100% {
|
|
transform: scale(1);
|
|
}
|
|
}
|
|
|
|
@keyframes &-zoom-out {
|
|
0% {
|
|
transform: scale(1);
|
|
}
|
|
100% {
|
|
transform: scale(.9);
|
|
}
|
|
}
|
|
`)
|
|
|
|
css(`
|
|
/* global styles */
|
|
.card {
|
|
background: white;
|
|
border-radius: 20px;
|
|
padding: 1em;
|
|
max-width: 100%;
|
|
min-width: 320px;
|
|
}
|
|
`)
|
|
|
|
/* controller-like logic */
|
|
const welcomePath = [
|
|
modes.welcome,
|
|
modes.alias,
|
|
modes.context,
|
|
modes.home,
|
|
]
|
|
|
|
function messageStateMachine(message) {
|
|
return ({target}) => {
|
|
const { id } = target.dataset
|
|
stateMachine(id, message)
|
|
}
|
|
}
|
|
|
|
function goTo(mode) {
|
|
return messageStateMachine({ action: actions.goto, mode })
|
|
}
|
|
|
|
function action(action) {
|
|
return messageStateMachine({ action })
|
|
}
|
|
|
|
function stateMachine(id, message) {
|
|
const { mode, backMode } = launcherById(id)
|
|
const { action } = message
|
|
|
|
function setMode(nextMode) {
|
|
set({ nextMode }, merge(id))
|
|
}
|
|
|
|
if(action === actions.goto) {
|
|
setMode(message.mode)
|
|
return
|
|
}
|
|
|
|
if(action === actions.back && backMode) {
|
|
setMode(backMode)
|
|
return
|
|
}
|
|
|
|
const onTheWelcomePath = welcomePath.includes(mode) && paginationActions.includes(action)
|
|
|
|
if(onTheWelcomePath) {
|
|
const order = action === actions.next
|
|
? welcomePath
|
|
: [...welcomePath].reverse()
|
|
|
|
const nextIndex = order.indexOf(mode) + 1
|
|
setMode(order[nextIndex])
|
|
return
|
|
}
|
|
}
|
|
|
|
/* model-like logic */
|
|
const emptyLauncher = {
|
|
applications: [],
|
|
emoji: '🏠',
|
|
emojiLabel: 'home',
|
|
mode: 'welcome',
|
|
nextMode: null,
|
|
backMode: null
|
|
}
|
|
|
|
export function launcherById(id) {
|
|
return get()[id] || emptyLauncher
|
|
}
|
|
|
|
function merge(id) {
|
|
return function middleware(state, payload) {
|
|
return {
|
|
...state,
|
|
[id]: {
|
|
...emptyLauncher,
|
|
...state[id],
|
|
...payload
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|