attain
Advanced tools
Comparing version 0.0.182 to 0.0.183
/* globals v */ | ||
function App({ v, route, stream }){ | ||
v.css.css('body', ` | ||
margin: 0px; | ||
padding: 0px; | ||
d grid | ||
jc center | ||
ac center | ||
bc rgba(0,0,0,0.05) | ||
`) | ||
v.css.css('body', ` | ||
margin: 0px; | ||
padding: 0px; | ||
d grid | ||
jc center | ||
ac center | ||
bc rgba(0,0,0,0.05) | ||
`) | ||
// ⚠ Warning typos abound. PR's Welcome 😃 | ||
// https://gitlab.com/harth/attain | ||
// ⚠ Warning typos abound. PR's Welcome 😃 | ||
// https://gitlab.com/harth/attain | ||
route= | ||
// our state machine is a router | ||
route.subroute('TapePlayer', x => x.Stopped({ time: 0 }), { | ||
Stopped: '/stopped/:time', | ||
Playing: '/playing/:time', | ||
Paused: '/paused/:time', | ||
FF: '/ff/:after/:time', | ||
RW: '/rw/:after/:time' | ||
}, { | ||
// default to history.replaceState for this router | ||
// so our history doesn't have thousands of entry for each | ||
// new tape player state | ||
replace: true | ||
}) | ||
route= | ||
// our state machine is a router | ||
route.subroute('TapePlayer', x => x.Stopped({ time: 0 }), { | ||
Stopped: '/stopped/:time', | ||
Playing: '/playing/:time', | ||
Paused: '/paused/:time', | ||
FF: '/ff/:after/:time', | ||
RW: '/rw/:after/:time' | ||
}, { | ||
// default to history.replaceState for this router | ||
// so our history doesn't have thousands of entry for each | ||
// new tape player state | ||
replace: true | ||
}) | ||
// Creates a partial fold we can spread into route.fold | ||
// to avoid a lot of repetition. | ||
const _ = | ||
v.otherwise(['Paused', 'Stopped', 'Playing', 'RW', 'FF']) | ||
// Creates a partial fold we can spread into route.fold | ||
// to avoid a lot of repetition. | ||
const _ = | ||
v.otherwise(['Paused', 'Stopped', 'Playing', 'RW', 'FF']) | ||
const seek = | ||
v.otherwise(['FF', 'RW']) | ||
const seek = | ||
v.otherwise(['FF', 'RW']) | ||
const scrubbable = | ||
v.otherwise(['Paused', 'FF', 'RW']) | ||
const scrubbable = | ||
v.otherwise(['Paused', 'FF', 'RW']) | ||
// Whenever we transition from 1 state to the next we use this | ||
// reducer. It 's a nested fold of the route type. | ||
// It returns an Either of the next state which allows | ||
// us to reject a state with a message for debugging. | ||
// `Y(next)` means we can proceed to the next state | ||
// `N(message)` means we cannot proceed, and there's a reason | ||
// why encoded in the structure. | ||
// | ||
// `Either` is built into attain for situations exactly like this. | ||
// It's also a sum-type just like our router. | ||
// Whenever we transition from 1 state to the next we use this | ||
// reducer. It 's a nested fold of the route type. | ||
// It returns an Either of the next state which allows | ||
// us to reject a state with a message for debugging. | ||
// `Y(next)` means we can proceed to the next state | ||
// `N(message)` means we cannot proceed, and there's a reason | ||
// why encoded in the structure. | ||
// | ||
// `Either` is built into attain for situations exactly like this. | ||
// It's also a sum-type just like our router. | ||
const transition = (prev, next) => { | ||
const transition = (prev, next) => { | ||
// This is a partial fold we can mix into a larger | ||
// fold to avoid repeating logic. | ||
const scrubs = { | ||
RW: ({ time }) => time > 0 | ||
? v.Y(next) | ||
: v.N('Already at the beginning') | ||
, FF: ({ time }) => time < total() | ||
? v.Y(next) | ||
: v.N('Already at the end') | ||
} | ||
// This is a partial fold we can mix into a larger | ||
// fold to avoid repeating logic. | ||
const scrubs = { | ||
RW: ({ time }) => time > 0 | ||
? v.Y(next) | ||
: v.N('Already at the beginning') | ||
, FF: ({ time }) => time < total() | ||
? v.Y(next) | ||
: v.N('Already at the end') | ||
} | ||
const decision = | ||
v.run( | ||
prev | ||
, | ||
route.fold({ | ||
Playing: () => v.run( | ||
next | ||
, route.fold({ | ||
..._( () => v.Y(next) ) | ||
, | ||
...scrubs | ||
, Playing: () => v.N('Already playing') | ||
}) | ||
) | ||
, | ||
Paused: () => v.run( | ||
next | ||
, route.fold({ | ||
..._( () => v.Y(next) ) | ||
, | ||
...scrubs | ||
, Paused: () => v.N('Already paused') | ||
}) | ||
) | ||
, | ||
Stopped: () => v.run( | ||
next | ||
, route.fold({ | ||
..._( () => v.Y(next) ) | ||
, | ||
...scrubs | ||
, Playing({ time }){ | ||
if( Number(time) >= total() ) { | ||
return v.N('You cannot press play if you have reached the end of a track') | ||
} | ||
return v.Y(next) | ||
} | ||
, Paused: () => v.N('You cannot pause if you are stopped') | ||
, Stopped: () => v.N('Already stopped') | ||
}) | ||
) | ||
, | ||
FF: ({ time }) => v.run( | ||
time | ||
, v.tagBy( 'Reached the end of the track', x => x < total ) | ||
, v.map( () => next ) | ||
, v.chain( | ||
route.fold({ | ||
..._( () => v.Y(next) ) | ||
, | ||
...scrubs | ||
, FF: () => v.N('Already fast-forwarding.') | ||
}) | ||
) | ||
// enforce existing time | ||
, v.map( v.$.value.time(time) ) | ||
) | ||
, | ||
RW: ({ time }) => v.run( | ||
time | ||
, v.tagBy( 'Reached the end of the track', x => x > 0 ) | ||
, v.map( () => next ) | ||
, v.chain( | ||
route.fold({ | ||
..._( () => v.Y(next) ) | ||
, | ||
...scrubs | ||
, RW: () => v.N('Already rewinding.') | ||
}) | ||
) | ||
// enforce existing time | ||
, v.map( v.$.value.time(time) ) | ||
) | ||
}) | ||
) | ||
const decision = | ||
v.run( | ||
prev | ||
, | ||
route.fold({ | ||
Playing: () => v.run( | ||
next | ||
, route.fold({ | ||
..._( () => v.Y(next) ) | ||
, | ||
...scrubs | ||
, Playing: () => v.N('Already playing') | ||
}) | ||
) | ||
, | ||
Paused: () => v.run( | ||
next | ||
, route.fold({ | ||
..._( () => v.Y(next) ) | ||
, | ||
...scrubs | ||
, Paused: () => v.N('Already paused') | ||
}) | ||
) | ||
, | ||
Stopped: () => v.run( | ||
next | ||
, route.fold({ | ||
..._( () => v.Y(next) ) | ||
, | ||
...scrubs | ||
, Playing({ time }){ | ||
if( Number(time) >= total() ) { | ||
return v.N('You cannot press play if you have reached the end of a track') | ||
} | ||
return v.Y(next) | ||
} | ||
, Paused: () => v.N('You cannot pause if you are stopped') | ||
, Stopped: () => v.N('Already stopped') | ||
}) | ||
) | ||
, | ||
FF: ({ time }) => v.run( | ||
time | ||
, v.tagBy( 'Reached the end of the track', x => x < total ) | ||
, v.map( () => next ) | ||
, v.chain( | ||
route.fold({ | ||
..._( () => v.Y(next) ) | ||
, | ||
...scrubs | ||
, FF: () => v.N('Already fast-forwarding.') | ||
}) | ||
) | ||
// enforce existing time | ||
, v.map( v.$.value.time(time) ) | ||
) | ||
, | ||
RW: ({ time }) => v.run( | ||
time | ||
, v.tagBy( 'Reached the end of the track', x => x > 0 ) | ||
, v.map( () => next ) | ||
, v.chain( | ||
route.fold({ | ||
..._( () => v.Y(next) ) | ||
, | ||
...scrubs | ||
, RW: () => v.N('Already rewinding.') | ||
}) | ||
) | ||
// enforce existing time | ||
, v.map( v.$.value.time(time) ) | ||
) | ||
}) | ||
) | ||
return decision | ||
} | ||
return decision | ||
} | ||
// Not all state changes are transitions. | ||
// This loop function runs every requestAnimationFrame | ||
// and increments the time forward/backward depending | ||
// on the current state. | ||
const loop = ({ x, dt }) => { | ||
// Not all state changes are transitions. | ||
// This loop function runs every requestAnimationFrame | ||
// and increments the time forward/backward depending | ||
// on the current state. | ||
const loop = ({ x, dt }) => { | ||
const decision = v.run( | ||
x | ||
, route.fold({ | ||
Paused: route.Paused | ||
, | ||
Stopped: route.Stopped | ||
, | ||
Playing({ time }){ | ||
return ( | ||
Number(time) + dt > total() | ||
? route.Stopped({ time: total() }) | ||
: route.Playing({ time: (Number(time) + dt).toFixed(1) }) | ||
) | ||
} | ||
, | ||
FF({ time, after }){ | ||
const shift = dt * 8 | ||
return ( | ||
Number(time) > total() | ||
? route.Stopped({ time: total() }) | ||
: route.FF({ | ||
time: (Number(time) + shift).toFixed(1) | ||
, after | ||
}) | ||
) | ||
} | ||
, | ||
RW({ time, after }){ | ||
const shift = dt * 8 | ||
return ( | ||
Number(time) - dt <= 0 | ||
? route.Stopped({ time: 0 }) | ||
: route.RW({ | ||
time: | ||
Math.max(0, (Number(time) - shift)) | ||
.toFixed(1) | ||
, after | ||
}) | ||
) | ||
} | ||
}) | ||
) | ||
const decision = v.run( | ||
x | ||
, route.fold({ | ||
Paused: route.Paused | ||
, | ||
Stopped: route.Stopped | ||
, | ||
Playing({ time }){ | ||
return ( | ||
Number(time) + dt > total() | ||
? route.Stopped({ time: total() }) | ||
: route.Playing({ time: (Number(time) + dt).toFixed(1) }) | ||
) | ||
} | ||
, | ||
FF({ time, after }){ | ||
const shift = dt * 8 | ||
return ( | ||
Number(time) > total() | ||
? route.Stopped({ time: total() }) | ||
: route.FF({ | ||
time: (Number(time) + shift).toFixed(1) | ||
, after | ||
}) | ||
) | ||
} | ||
, | ||
RW({ time, after }){ | ||
const shift = dt * 8 | ||
return ( | ||
Number(time) - dt <= 0 | ||
? route.Stopped({ time: 0 }) | ||
: route.RW({ | ||
time: | ||
Math.max(0, (Number(time) - shift)) | ||
.toFixed(1) | ||
, after | ||
}) | ||
) | ||
} | ||
}) | ||
) | ||
return decision | ||
} | ||
return decision | ||
} | ||
const validTransition = (b) => v.isY( transition(route(), b)) | ||
const update = stream() | ||
const transitions = stream() | ||
// `redrawService` inspect transitions and redraws | ||
// so disabled button states refresh | ||
const redrawService = (a,b) => { | ||
if( a.value.time < total() && b.value.time >= total() ) { | ||
v.redraw() | ||
} else if (a.value.time >= 0 && b.value.time <= 0 ) { | ||
v.redraw() | ||
} | ||
} | ||
// Because there's two ways state progresses | ||
// we model that as sum type as well. | ||
// Our Update's are either requestAnimatinFrame | ||
// "game loop" style changes or Transitions | ||
// from Sum Type state to the next "FSM style". | ||
const Update = | ||
v.tags('Update', ['RAF', 'Transition']) | ||
const validTransition = (b) => v.isY( transition(route(), b)) | ||
const update = stream() | ||
const transitions = stream() | ||
// When the UI pushes into the transitions stream | ||
// we tag the data with Update.Transition and push | ||
// it into the central update stream for processing. | ||
transitions.map( | ||
next => update(Update.Transition(next)) | ||
) | ||
// Because there's two ways state progresses | ||
// we model that as sum type as well. | ||
// Our Update's are either requestAnimatinFrame | ||
// "game loop" style changes or Transitions | ||
// from Sum Type state to the next "FSM style". | ||
const Update = | ||
v.tags('Update', ['RAF', 'Transition']) | ||
// Every ~16ms we push an Update.Raf value | ||
// into the central update stream with the | ||
// delta time in ms. | ||
// Attain provides this as a primative because | ||
// it's so useful for UI programming. | ||
stream.raf().map( | ||
({ dt }) => update(Update.RAF({ dt })) | ||
) | ||
// When the UI pushes into the transitions stream | ||
// we tag the data with Update.Transition and push | ||
// it into the central update stream for processing. | ||
transitions.map( | ||
next => update(Update.Transition(next)) | ||
) | ||
// Now we process each update. | ||
// We always have access to the previous state `a` | ||
// and the next state `b` | ||
// `b` is our `Update` value, so we can fold over it | ||
// to process each case of `Update` specifically. | ||
// Every ~16ms we push an Update.Raf value | ||
// into the central update stream with the | ||
// delta time in ms. | ||
// Attain provides this as a primative because | ||
// it's so useful for UI programming. | ||
stream.raf().map( | ||
({ dt }) => update(Update.RAF({ dt })) | ||
) | ||
stream.scan( route()) ( | ||
(a,b) => { | ||
// Now we process each update. | ||
// We always have access to the previous state `a` | ||
// and the next state `b` | ||
// `b` is our `Update` value, so we can fold over it | ||
// to process each case of `Update` specifically. | ||
const f = Update.fold({ | ||
stream.scan( route()) ( | ||
(a,b) => { | ||
RAF: ({ dt }) => loop({ x: a, dt }) | ||
const f = Update.fold({ | ||
, | ||
RAF: ({ dt }) => loop({ x: a, dt }) | ||
Transition: b => | ||
v.getOr(a) (transition(a,b)) | ||
, | ||
}) | ||
Transition: b => | ||
v.getOr(a) (transition(a,b)) | ||
// Here we apply the fold | ||
// with the next update value | ||
const out = f(b) | ||
}) | ||
// And by returning the result | ||
// it is now streamed directly into the router | ||
return out | ||
} | ||
) (update) | ||
// Here we apply the fold | ||
// with the next update value | ||
const out = f(b) | ||
// this is where the router receives the new state | ||
.map( route ) | ||
// Decide when to render based on | ||
// certain boundary conditions | ||
redrawService(a, out) | ||
const button = (src, attrs={}) => | ||
v('div' | ||
+ v.css` | ||
bc #1043d8 | ||
border 0 | ||
w 3.5em | ||
h 3.5em | ||
br 0.8em | ||
transition 0.2s | ||
d grid | ||
jc center | ||
ac center | ||
` | ||
.$nest('[disabled]',` | ||
bc rgba(0,0,0,0.2) | ||
`) | ||
, attrs | ||
, v('img' | ||
+ v.css` | ||
filter: invert(100); | ||
` | ||
, { src } | ||
) | ||
) | ||
// And by returning the result | ||
// it is now streamed directly into the router | ||
return out | ||
} | ||
) (update) | ||
const transitionButton = (icon, transition, attrs=() => {}) => | ||
button(icon, { | ||
...attrs( transition ) | ||
}) | ||
// this is where the router receives the new state | ||
.map( route ) | ||
const play = () => | ||
transitionButton( | ||
'https://attain.harth.io/examples/play.svg' | ||
, () => route.Playing({ time: route().value.time }) | ||
, normalBehaviour | ||
) | ||
const button = (src, attrs={}) => | ||
v('div' | ||
+ v.css` | ||
bc #1043d8 | ||
border 0 | ||
w 3.5em | ||
h 3.5em | ||
br 0.8em | ||
transition 0.2s | ||
d grid | ||
jc center | ||
ac center | ||
` | ||
.$nest('[disabled]',` | ||
bc rgba(0,0,0,0.2) | ||
`) | ||
, attrs | ||
, v('img' | ||
+ v.css` | ||
filter: invert(100); | ||
` | ||
, { src } | ||
) | ||
) | ||
const pause = () => | ||
transitionButton( | ||
'https://attain.harth.io/examples/pause.svg' | ||
, () => route.Paused({ time: route().value.time }) | ||
, normalBehaviour | ||
) | ||
const transitionButton = (icon, transition, attrs=() => {}) => | ||
button(icon, { | ||
...attrs( transition ) | ||
}) | ||
const stop = () => | ||
transitionButton( | ||
'https://attain.harth.io/examples/square.svg' | ||
, () => route.Stopped({ time: route().value.time }) | ||
, normalBehaviour | ||
) | ||
const play = () => | ||
transitionButton( | ||
'https://attain.harth.io/examples/play.svg' | ||
, () => route.Playing({ time: route().value.time }) | ||
, normalBehaviour | ||
) | ||
const pause = () => | ||
transitionButton( | ||
'https://attain.harth.io/examples/pause.svg' | ||
, () => route.Paused({ time: route().value.time }) | ||
, normalBehaviour | ||
) | ||
const mouseUpBehaviour = x => ({ | ||
onmousedown: () => transitions(x()) | ||
, onmouseup: () => { | ||
const stop = () => | ||
transitionButton( | ||
'https://attain.harth.io/examples/square.svg' | ||
, () => route.Stopped({ time: route().value.time }) | ||
, normalBehaviour | ||
) | ||
const after = route.fold({ | ||
... _ ( () => v.N() ), | ||
... seek (({ after }) => v.Y(after)) | ||
}) | ||
const pauseAfter = | ||
v.chain( v.tagBy('NotPaused', x => x == 'Paused' ) ) | ||
const mouseUpBehaviour = x => ({ | ||
onmousedown: () => transitions(x()) | ||
, onmouseup: () => { | ||
v.run( | ||
route() | ||
, after | ||
, pauseAfter | ||
, v.map(() => transitions( | ||
route.Paused({ time: route().value.time }) | ||
)) | ||
) | ||
} | ||
, disabled: !validTransition( x() ) | ||
}) | ||
const after = route.fold({ | ||
... _ ( () => v.N() ), | ||
... seek (({ after }) => v.Y(after)) | ||
}) | ||
const normalBehaviour = x => ({ | ||
onmousedown(){ transitions(x()) } | ||
, disabled: !validTransition(x()) | ||
}) | ||
const pauseAfter = | ||
v.chain( v.tagBy('NotPaused', x => x == 'Paused' ) ) | ||
v.run( | ||
route() | ||
, after | ||
, pauseAfter | ||
, v.map(() => transitions( | ||
route.Paused({ time: route().value.time }) | ||
)) | ||
) | ||
} | ||
, disabled: !validTransition( x() ) | ||
}) | ||
const scrubbingBehaviour = | ||
route.map( | ||
route.fold({ | ||
..._ ( () => normalBehaviour ) | ||
, | ||
... scrubbable ( | ||
() => mouseUpBehaviour | ||
) | ||
}) | ||
) | ||
const normalBehaviour = x => ({ | ||
onmousedown(){ transitions(x()) } | ||
, disabled: !validTransition(x()) | ||
}) | ||
const ff = () => | ||
transitionButton( | ||
'https://attain.harth.io/examples/fast-forward.svg' | ||
, () => route.FF({ time: route().value.time, after: route().tag }) | ||
, scrubbingBehaviour() | ||
) | ||
const rw = () => | ||
transitionButton( | ||
'https://attain.harth.io/examples/rewind.svg' | ||
, () => route.RW({ time: route().value.time, after: route().tag }) | ||
, scrubbingBehaviour() | ||
) | ||
const scrubbingBehaviour = | ||
route.map( | ||
route.fold({ | ||
..._ ( () => normalBehaviour ) | ||
, | ||
... scrubbable ( | ||
() => mouseUpBehaviour | ||
) | ||
}) | ||
) | ||
const timeIsACircle = (scale) => | ||
v('.time-circle' | ||
+ v.css` | ||
border-radius: 100%; | ||
width: 8em; | ||
height: 8em; | ||
bc rgba(0,0,0,0.15) | ||
justify-self center; | ||
align-self center; | ||
d grid | ||
jc center | ||
ac center | ||
transform scale(var(--scale)) | ||
` | ||
, | ||
{ hook: ({ dom }) => | ||
scale.map( x => dom.style.setProperty('--scale', x)) | ||
} | ||
, v('.center' | ||
+ v.css(` | ||
bc rgba(0,0,0,0.5) | ||
w 1em | ||
h 1em | ||
br 100% | ||
`) | ||
) | ||
) | ||
const ff = () => | ||
transitionButton( | ||
'https://attain.harth.io/examples/fast-forward.svg' | ||
, () => route.FF({ time: route().value.time, after: route().tag }) | ||
, scrubbingBehaviour() | ||
) | ||
const time = route.map( x => x.value.time ) | ||
const rw = () => | ||
transitionButton( | ||
'https://attain.harth.io/examples/rewind.svg' | ||
, () => route.RW({ time: route().value.time, after: route().tag }) | ||
, scrubbingBehaviour() | ||
) | ||
const total = stream(30000) | ||
const timeIsACircle = (scale) => | ||
v('.time-circle' | ||
+ v.css` | ||
border-radius: 100%; | ||
width: 8em; | ||
height: 8em; | ||
bc rgba(0,0,0,0.15) | ||
justify-self center; | ||
align-self center; | ||
d grid | ||
jc center | ||
ac center | ||
transform scale(var(--scale)) | ||
` | ||
, | ||
{ hook: ({ dom }) => | ||
scale.map( x => dom.style.setProperty('--scale', x)) | ||
} | ||
, v('.center' | ||
+ v.css(` | ||
bc rgba(0,0,0,0.5) | ||
w 1em | ||
h 1em | ||
br 100% | ||
`) | ||
) | ||
) | ||
const elapsed = | ||
stream.merge([ time, total ]).map( | ||
([time,total]) => (time / total) | ||
) | ||
const time = route.map( x => x.value.time ) | ||
const remaining = elapsed.map( x => 1-x ) | ||
const total = stream(30000) | ||
return () => v('.app' | ||
+ v.css` | ||
ff Helvetica | ||
d grid | ||
jc center | ||
ac center | ||
gap 1em | ||
const elapsed = | ||
stream.merge([ time, total ]).map( | ||
([time,total]) => (time / total) | ||
) | ||
font-size: 0.9em | ||
` | ||
.desktop(` | ||
font-size: 1em; | ||
`) | ||
.$nest('*', ` | ||
user-select: none; | ||
`) | ||
, v('url' | ||
+ v.css` | ||
bc rgba(0,0,0,0.85) | ||
p 1em | ||
br 0.25em | ||
c white | ||
box-shadow 0px 0px 10px 5px rgba(0,0,0,0.1) | ||
position: relative | ||
` | ||
, v('span' | ||
, | ||
{ hook: ({ dom }) => | ||
route.map(route.toURL).map( x => dom.textContent = x ) | ||
} | ||
) | ||
) | ||
, v('.player' | ||
+ v.css(` | ||
bc white | ||
br 1em | ||
p 1em | ||
min-height: 300px; | ||
gap 1em | ||
box-shadow 0px 0px 30px 1px rgba(0,0,0,0.1) | ||
d grid | ||
gtr auto 1fr auto | ||
`) | ||
.desktop(` | ||
min-width 20em | ||
`) | ||
, v('h1' | ||
+ v.css.m(0).p(0).fw(100) | ||
, | ||
// This is a demonstration of using a hook | ||
// to write directly into a dom property | ||
// the stream will automatically be cleaned up | ||
// when the dom node unmounts | ||
{ hook: ({ dom }) => | ||
time | ||
.map( x => Math.floor(Number(x) / 1000) ) | ||
.map( x => dom.textContent = x ) | ||
} | ||
) | ||
, v('.time' | ||
+ v.css` | ||
d grid | ||
bc #EEE | ||
border-radius: 1em | ||
padding 1em | ||
display grid | ||
gtc 1fr 1fr | ||
` | ||
, timeIsACircle( remaining ) | ||
, timeIsACircle( elapsed ) | ||
) | ||
, v('.controls' | ||
+ v.css` | ||
d grid | ||
grid-auto-flow column | ||
justify-content space-between | ||
` | ||
, play() | ||
, rw() | ||
, ff() | ||
, stop() | ||
, pause() | ||
) | ||
) | ||
, v('.credits' | ||
+ v.css` | ||
fs 0.8em | ||
c gray | ||
d grid | ||
jc center | ||
` | ||
.$nest('a, a:visited', `c inherit`) | ||
, v('p' | ||
, 'Original ' | ||
, v('a', | ||
{ href: 'https://codesandbox.io/s/state-designer-counter-2nmd5'} | ||
, 'State Designer Example' | ||
) | ||
, ' by ' | ||
, v('a', | ||
{ href: 'https://twitter.com/steveruizok' } | ||
, '@steveruizok' | ||
) | ||
const remaining = elapsed.map( x => 1-x ) | ||
) | ||
) | ||
) | ||
return () => v('.app' | ||
+ v.css` | ||
ff Helvetica | ||
d grid | ||
jc center | ||
ac center | ||
gap 1em | ||
font-size: 0.9em | ||
` | ||
.desktop(` | ||
font-size: 1em; | ||
`) | ||
.$nest('*', ` | ||
user-select: none; | ||
`) | ||
, v('url' | ||
+ v.css` | ||
bc rgba(0,0,0,0.85) | ||
p 1em | ||
br 0.25em | ||
c white | ||
box-shadow 0px 0px 10px 5px rgba(0,0,0,0.1) | ||
position: relative | ||
` | ||
, v('span' | ||
, | ||
{ hook: ({ dom }) => | ||
route.map(route.toURL).map( x => dom.textContent = x ) | ||
} | ||
) | ||
) | ||
, v('.player' | ||
+ v.css(` | ||
bc white | ||
br 1em | ||
p 1em | ||
min-height: 300px; | ||
gap 1em | ||
box-shadow 0px 0px 30px 1px rgba(0,0,0,0.1) | ||
d grid | ||
gtr auto 1fr auto | ||
`) | ||
.desktop(` | ||
min-width 20em | ||
`) | ||
, v('h1' | ||
+ v.css.m(0).p(0).fw(100) | ||
, | ||
// This is a demonstration of using a hook | ||
// to write directly into a dom property | ||
// the stream will automatically be cleaned up | ||
// when the dom node unmounts | ||
{ hook: ({ dom }) => | ||
time | ||
.map( x => Math.floor(Number(x) / 1000) ) | ||
.map( x => dom.textContent = x ) | ||
} | ||
) | ||
, v('.time' | ||
+ v.css` | ||
d grid | ||
bc #EEE | ||
border-radius: 1em | ||
padding 1em | ||
display grid | ||
gtc 1fr 1fr | ||
` | ||
, timeIsACircle( remaining ) | ||
, timeIsACircle( elapsed ) | ||
) | ||
, v('.controls' | ||
+ v.css` | ||
d grid | ||
grid-auto-flow column | ||
justify-content space-between | ||
` | ||
, play() | ||
, rw() | ||
, ff() | ||
, stop() | ||
, pause() | ||
) | ||
) | ||
, v('.credits' | ||
+ v.css` | ||
fs 0.8em | ||
c gray | ||
d grid | ||
jc center | ||
` | ||
.$nest('a, a:visited', `c inherit`) | ||
, v('p' | ||
, 'Original ' | ||
, v('a', | ||
{ href: 'https://codesandbox.io/s/state-designer-counter-2nmd5'} | ||
, 'State Designer Example' | ||
) | ||
, ' by ' | ||
, v('a', | ||
{ href: 'https://twitter.com/steveruizok' } | ||
, '@steveruizok' | ||
) | ||
) | ||
) | ||
) | ||
} | ||
v(document.body, { | ||
render({ v, ...attrs }){ | ||
return v(App, { v, ...attrs }) | ||
} | ||
render({ v, ...attrs }){ | ||
return v(App, { v, ...attrs }) | ||
} | ||
}) |
{ | ||
"name": "attain", | ||
"version": "0.0.182", | ||
"version": "0.0.183", | ||
"description": "A library for modelling and accessing data.", | ||
@@ -5,0 +5,0 @@ "main": "dist/attain.min.js", |
1636444
10729