diff --git a/src/components/crowdnode-card.js b/src/components/crowdnode-card.js new file mode 100644 index 0000000..d0bceb2 --- /dev/null +++ b/src/components/crowdnode-card.js @@ -0,0 +1,221 @@ +import { + formDataEntries, + createSignal, +} from '../helpers/utils.js' + +import { + lit as html, +} from '../helpers/lit.js' + +export const CrowdNodeCard = (() => { + const initCfg = { + state: {}, + slugs: {}, + events: {}, + elements: {}, + markup: {}, + } + const initialState = { + id: 'Card', + name: 'Card', + withdrawTxt: 'Withdraw', + withdrawAlt: 'Withdraw from Crowdnode', + depositTxt: 'Deposit', + depositAlt: `Deposit to Crowdnode`, + signupTxt: 'Signup', + signupAlt: `Signup for Crowdnode`, + placement: 'center', + rendered: null, + responsive: true, + } + + const cn = function Crowdnode( + config = {} + ) { + config = { + ...initCfg, + ...config, + } + + this.appElement = document.body + + this.api = createSignal({}) + + this.state = { + ...initialState, + ...config.state, + } + + this.slugs = { + form: this.state.name?.toLowerCase().replaceAll(' ', '_'), + ...config.slugs, + } + + this.elements = { + ...config.elements, + } + + this.markup = {} + this.markup.content = () => html` +
+ + + CrowdNode + +
+ +
+ ${!this.api.value?.acceptedToS && html` + Start earning interest by staking your Dash at CrowdNode + `} + ${this.api.value?.acceptedToS && html` +

+ Balance: Ð ${this.api.value?.balance} +

+ `} + ${this.api.value?.acceptedToS && html` +

+ Earned: Ð ${this.api.value?.earned} +

+ `} +
+ + + ` + this.markup = { + ...this.markup, + ...config.markup, + } + + this.events = { + submit: event => { + event.preventDefault() + event.stopPropagation() + + this.elements.form?.removeEventListener('submit', this.events.submit) + + let fde = formDataEntries(event) + + console.log( + `${this.slugs.form} submit`, + {event, fde}, + ) + }, + ...config.events, + } + + const $d = document + + const form = $d.createElement('form') + + this.elements.form = form + + form.name = `${this.slugs.form}` + form.classList.add('flex', 'col', 'card') + form.innerHTML = this.markup.content() + + this.api.on((apiChange) => { + console.log('CN Card API Change', apiChange) + this.render?.({}) + }) + + /** + * Update the config of the CN Card + * @function + */ + this.updateConfig = (config = {}) => { + console.log('CN Card updateConfig TOP', config) + + for (let param in config) { + this[param] = { + ...this[param], + ...(config[param] || {}), + } + } + + console.log('CN Card updateConfig BOT', this) + } + + /** + * Trigger the rendering of the CN Card + * @function + */ + this.render = ({ + cfg = {}, + position = 'afterend', + el = this.appElement, + }) => { + console.log('crowdnode render', this) + + this.elements.form?.removeEventListener?.( + 'submit', + this.events.submit, + ) + + if (el !== this.appElement) { + this.appElement = el + } + + this.updateConfig(cfg) + + this.elements.form.name = this.slugs.form + this.elements.form.innerHTML = this.markup.content() + + this.elements.form.addEventListener( + 'submit', + this.events.submit, + ) + + console.log('CARD RENDER', this, cfg) + + if (!this.state.rendered) { + el.insertAdjacentElement(position, this.elements.form) + this.state.rendered = this.elements.form + } + + this.events?.render?.(this) + } + + return this + } + + return cn +})(); + +export default CrowdNodeCard diff --git a/src/components/dialog.js b/src/components/dialog.js index b133a09..4551c39 100644 --- a/src/components/dialog.js +++ b/src/components/dialog.js @@ -4,6 +4,10 @@ import { envoy, } from '../helpers/utils.js' +import { + DIALOG_STATUS, +} from '../helpers/constants.js' + let modal = envoy( { rendered: {}, @@ -26,7 +30,8 @@ const initialState = { rendered: null, responsive: true, delay: 500, - async render () {}, + status: DIALOG_STATUS.NOT_LOADING, + async render () { return this }, addListener () {}, addListeners () {}, removeAllListeners (targets) {}, @@ -210,6 +215,9 @@ const initialState = { // event.target === state.elements.dialog, // state.elements.dialog.returnValue // ) + state.status = DIALOG_STATUS.NOT_LOADING + state.elements.progress?.remove() + state.elements.progress = null if (state.elements.dialog.returnValue !== 'cancel') { resolve(state.elements.dialog.returnValue) @@ -298,30 +306,30 @@ export async function setupDialog( .replaceAll(/[^a-zA-Z _]/g, '') .replaceAll(' ', '_') - const dialog = document.createElement('dialog') - const form = document.createElement('form') - const progress = document.createElement('progress') + const dialogElement = document.createElement('dialog') + const formElement = document.createElement('form') + const progressElement = document.createElement('progress') - state.elements.dialog = dialog - state.elements.form = form - state.elements.progress = progress + state.elements.dialog = dialogElement + state.elements.form = formElement + state.elements.progress = progressElement - progress.classList.add('pending') + progressElement.classList.add('pending') - dialog.innerHTML = `` - dialog.id = state.slugs.dialog + dialogElement.innerHTML = `` + dialogElement.id = state.slugs.dialog if (state.responsive) { - dialog.classList.add('responsive') + dialogElement.classList.add('responsive') } - dialog.classList.add(state.placement) + dialogElement.classList.add(...(state.placement.split(' '))) - form.name = `${state.slugs.form}` - form.method = 'dialog' - form.innerHTML = await state.content(state) + formElement.name = `${state.slugs.form}` + formElement.method = 'dialog' + formElement.innerHTML = await state.content(state) - dialog.insertAdjacentElement( + dialogElement.insertAdjacentElement( 'afterbegin', - form + formElement ) function addListener( @@ -340,74 +348,74 @@ export async function setupDialog( ) { if (resolve && reject) { addListener( - dialog, + dialogElement, 'close', state.events.handleClose(state, resolve, reject), ) addListener( - dialog, + dialogElement, 'click', state.events.handleClick(state), ) } addListener( - form, + formElement, 'blur', state.events.handleBlur(state), ) addListener( - form, + formElement, 'focusout', state.events.handleFocusOut(state), ) addListener( - form, + formElement, 'focusin', state.events.handleFocusIn(state), ) addListener( - form, + formElement, 'change', state.events.handleChange(state), ) - // let updrop = form.querySelector('.updrop') + // let updrop = formElement.querySelector('.updrop') // state.elements.updrop = updrop // if (updrop) { addListener( - form, + formElement, 'drop', state.events.handleDrop(state), ) addListener( - form, + formElement, 'dragover', state.events.handleDragOver(state), ) addListener( - form, + formElement, 'dragend', state.events.handleDragEnd(state), ) addListener( - form, + formElement, 'dragleave', state.events.handleDragLeave(state), ) // } addListener( - form, + formElement, 'input', state.events.handleInput(state), ) addListener( - form, + formElement, 'reset', state.events.handleReset(state), ) addListener( - form, + formElement, 'submit', state.events.handleSubmit(state), ) @@ -416,7 +424,7 @@ export async function setupDialog( state.addListeners = addListeners function removeAllListeners( - targets = [dialog,form], + targets = [dialogElement,formElement], ) { if (state.elements.updrop) { targets.push(state.elements.updrop) @@ -456,29 +464,56 @@ export async function setupDialog( } } - dialog.id = state.slugs.dialog - form.name = `${state.slugs.form}` - form.innerHTML = await state.content(state) + dialogElement.id = state.slugs.dialog + formElement.name = `${state.slugs.form}` + formElement.innerHTML = await state.content(state) // console.log('DIALOG RENDER', state, position, state.slugs.dialog, modal.rendered) + if ( + state.status === DIALOG_STATUS.LOADING && + state.elements.progress + ) { + state.elements.form.insertAdjacentElement( + 'beforebegin', + state.elements.progress, + ) + + // document.body.insertAdjacentHTML( + // 'afterbegin', + // ``, + // ) + } + + if ( + state.status === DIALOG_STATUS.SUCCESS || + state.status === DIALOG_STATUS.ERROR + ) { + // document.getElementById('pageLoader')?.remove() + state.elements.progress?.remove() + } + if (!modal.rendered[state.slugs.dialog]) { - el.insertAdjacentElement(position, dialog) - modal.rendered[state.slugs.dialog] = dialog + el.insertAdjacentElement(position, dialogElement) + modal.rendered[state.slugs.dialog] = dialogElement } state.events.handleRender(state) + + return state } state.render = render return { - element: dialog, + element: dialogElement, + state, + elements: state.elements, show: (callback) => new Promise((resolve, reject) => { removeAllListeners() addListeners(resolve, reject) - // console.log('dialog show', dialog) - dialog.show() + // console.log('dialog show', dialogElement) + dialogElement.show() state.events.handleShow?.(state) callback?.() }), @@ -486,11 +521,11 @@ export async function setupDialog( removeAllListeners() addListeners(resolve, reject) // console.log('dialog showModal', dialog) - dialog.showModal() + dialogElement.showModal() state.events.handleShow?.(state) callback?.() }), - close: returnVal => dialog.close(returnVal), + close: returnVal => dialogElement.close(returnVal), render, } } diff --git a/src/components/modal.js b/src/components/modal.js new file mode 100644 index 0000000..781e7a5 --- /dev/null +++ b/src/components/modal.js @@ -0,0 +1,543 @@ +import { lit as html } from '../helpers/lit.js' +import { + formDataEntries, + createSignal, + effect, + toSlug, + addListener, + addListeners, + removeAllListeners, +} from '../helpers/utils.js' + +import { + DIALOG_STATUS, +} from '../helpers/constants.js' + +/** + * Create a new HTML Dialog + */ +export const DialogContructor = (() => { + const initCfg = { + state: {}, + slugs: {}, + events: {}, + elements: {}, + templates: {}, + } + const initialState = { + id: 'Dialog', + name: 'Dialog', + submitTxt: 'Submit', + submitAlt: 'Submit Form', + cancelTxt: 'Cancel', + cancelAlt: `Cancel Form`, + closeTxt: 'X', + closeAlt: `Close`, + placement: 'center', + rendered: null, + responsive: true, + delay: 500, + status: DIALOG_STATUS.NOT_LOADING, + } + + /** + * Create a new HTML Dialog + * + * @param {Object} config + */ + const dl = function DialogModal( + config = {} + ) { + config = { + ...initCfg, + ...config, + } + + this.eventHandlers = [] + + this.state = createSignal({ + ...initialState, + ...config.state, + }) + + this.slugs = { + dialog: toSlug(this.state.value.name, this.state.value.id), + form: toSlug(this.state.value.id, this.state.value.name), + ...config.slugs, + } + + this.appElement = document.body + + this.elements = { + ...config.elements, + } + + this.markup = {} + + effect(() => { + this.markup.header = html` +
+ ${this.state.value.name} + ${ + this.state.value.closeTxt && html`` + } +
+ ` + this.markup.footer = html` + + ` + }) + + this.markup.fields = html` + + + +

Some instructions

+ ` + + this.markup.content = () => html` + ${this.markup.header} + +
+ ${this.markup.fields} + +
+
+ + ${this.markup.footer} + ` + + this.markup = { + ...this.markup, + ...config.markup, + } + + this.events = { + input: event => { + // let { + // // @ts-ignore + // name: fieldName, form, + // } = event?.target + + // console.log('handle input', { + // fieldName, + // form, + // }) + + if ( + event?.target?.validity?.patternMismatch && + event?.target?.type !== 'checkbox' + ) { + event.preventDefault() + let label = event.target?.previousElementSibling?.textContent?.trim() + if (label) { + event.target.setCustomValidity(`Invalid ${label}`) + } + } else { + event.target.setCustomValidity('') + } + event.target.reportValidity() + }, + change: event => { + // let { + // // @ts-ignore + // name: fieldName, form, + // } = event?.target + + // console.log('handle change', { + // fieldName, + // form, + // }) + + if ( + event?.target?.validity?.patternMismatch && + event?.target?.type !== 'checkbox' + ) { + event.preventDefault() + let label = event.target?.previousElementSibling?.textContent?.trim() + if (label) { + event.target.setCustomValidity(`Invalid ${label}`) + } + } else { + event.target.setCustomValidity('') + } + event.target.reportValidity() + }, + blur: event => { + // event.preventDefault() + // console.log( + // 'handle blur', + // event, + // ) + }, + focusout: event => { + // event.preventDefault() + // console.log( + // 'handle focus out', + // event, + // ) + }, + focusin: event => { + // event.preventDefault() + // console.log( + // 'handle focus in', + // event, + // ) + }, + drop: event => { + event.preventDefault() + }, + dragover: event => { + event.preventDefault() + }, + dragend: event => { + event.preventDefault() + }, + dragleave: event => { + event.preventDefault() + }, + render: ( + state, + ) => { + // console.log( + // 'handle dialog render', + // state, + // ) + }, + show: ( + state, + ) => { + // console.log( + // 'handle dialog show', + // state, + // ) + + // focus first input + this.elements.form.querySelector( + 'input' + )?.focus() + }, + close: ( + resolve = res=>{}, + reject = res=>{}, + ) => async event => { + event.preventDefault() + removeAllListeners.bind(this) + + // console.log( + // 'handle dialog close', + // event, + // event.target === this.elements.dialog, + // this.elements.dialog.returnValue + // ) + this.state.value.status = DIALOG_STATUS.NOT_LOADING + this.elements.dialog?.querySelector('progress')?.remove() + + if (this.elements.dialog.returnValue !== 'cancel') { + resolve(this.elements.dialog.returnValue) + } else { + resolve('cancel') + } + // console.log( + // 'DIALOG handleClose', + // modal.rendered[this.slugs.dialog], + // ) + + setTimeout(t => { + // modal.rendered[this.slugs.dialog] = null + this.state.value.rendered = null + event?.target?.remove() + // console.log( + // 'DIALOG handleClose setTimeout', + // this.state.value.delay, + // // modal.rendered[this.slugs.dialog], + // modal.rendered, + // ) + }, this.state.value.delay) + }, + submit: event => { + event.preventDefault() + + let fde = formDataEntries(event) + + this.elements.dialog.returnValue = String(fde.intent) + + // console.log( + // 'handleSubmit', + // [event], + // ) + + this.elements.dialog.close(String(fde.intent)) + }, + reset: event => { + event.preventDefault() + this.elements.form?.removeEventListener( + 'close', + this.events.reset + ) + // console.log( + // 'handleReset', + // [event.target], + // ) + this.elements.dialog.close('cancel') + }, + click: event => { + if (event.target === this.elements.dialog) { + // console.log( + // 'handle dialog backdrop click', + // event, + // event.target === this.elements.dialog + // ) + this.elements.dialog.close('cancel') + } + }, + } + + const dialogElement = document.createElement('dialog') + const formElement = document.createElement('form') + const progressElement = document.createElement('progress') + + this.elements.dialog = dialogElement + this.elements.form = formElement + this.elements.progress = progressElement + + + this.element = this.elements.dialog + + progressElement.classList.add('pending') + + dialogElement.innerHTML = `` + dialogElement.id = this.slugs.dialog + if (this.state.value.responsive) { + dialogElement.classList.add('responsive') + } + dialogElement.classList.add(...(this.state.value.placement.split(' '))) + + formElement.name = `${this.slugs.form}` + formElement.method = 'dialog' + // formElement.innerHTML = this.markup.content + formElement.innerHTML = this.markup.content() + + dialogElement.insertAdjacentElement( + 'afterbegin', + formElement + ) + + /** + * Show the dialog + * @function + */ + this.show = (callback, el = this.appElement) => new Promise((resolve, reject) => { + removeAllListeners.call(this) + addListeners.call(this, resolve, reject) + console.log('modal.js dialog show', this.elements.dialog) + // if ( + // !this.state.value.rendered && + // !el.contains(this.elements.dialog) + // ) { + this.render({ + el, + // cfg: config, + position: 'afterend' + }) + // } + this.elements.dialog.show() + this.events.show?.(this) + callback?.() + }) + + /** + * Show the Modal form of the dialog + * @function + */ + this.showModal = (callback, el = this.appElement) => new Promise((resolve, reject) => { + removeAllListeners.call(this) + addListeners.call(this, resolve, reject) + console.log('modal.js dialog showModal', this, this.elements.dialog) + // if ( + // !this.state.value.rendered && + // !el.contains(this.elements.dialog) + // ) { + this.render({ + el, + // cfg: config, + position: 'afterend' + }) + // } + this.elements.dialog.showModal() + this.events.show?.(this) + callback?.() + }) + + /** + * Close the dialog + * @function + */ + this.close = returnVal => this.elements.dialog.close(returnVal) + + /** + * Update the config of the dialog + * @function + */ + this.updateConfig = (config = {}) => { + console.log('Dialog updateConfig TOP', config) + + for (let param in config) { + // if (param === 'markup') { + // this[param].value = { + // ...this[param].value, + // ...(config[param] || {}), + // } + // } else { + if ('value' in this[param]) { + this[param].value = { + ...this[param].value, + ...(config[param] || {}), + } + } else { + this[param] = { + ...this[param], + ...(config[param] || {}), + } + } + // } + } + + console.log('Dialog updateConfig BOT', this) + } + + /** + * Trigger the rendering of the dialog + * @function + */ + this.render = ({ + cfg = {}, + position = 'afterend', + el = this.appElement, + }) => { + console.log('dialog render', this, dl) + + // this.elements.form?.removeEventListener?.( + // 'submit', + // this.events.submit, + // ) + + if (el !== this.appElement) { + this.appElement = el + } + + this.updateConfig(cfg) + + console.log('DIALOG elements', this, this.elements) + + this.elements.dialog.id = this.slugs.dialog + this.elements.form.name = this.slugs.form + // this.elements.form.innerHTML = this.markup.content + this.elements.form.innerHTML = this.markup.content() + + // this.elements.form.addEventListener( + // 'submit', + // this.events.submit, + // ) + + console.log('DIALOG RENDER STATE', this.state.value, cfg) + + console.log('DIALOG RENDER', position, this.slugs.dialog) + + if ( + this.state.value.status === DIALOG_STATUS.LOADING + ) { + this.elements.form.insertAdjacentElement( + 'beforebegin', + this.elements.progress, + ) + + // document.body.insertAdjacentHTML( + // 'afterbegin', + // ``, + // ) + } + + if ( + this.state.value.status === DIALOG_STATUS.SUCCESS || + this.state.value.status === DIALOG_STATUS.ERROR + ) { + // document.getElementById('pageLoader')?.remove() + this.elements.dialog.querySelector('progress')?.remove() + } + + if (!this.state.value.rendered) { + console.log('!this.state.rendered el', el, this.appElement, this.elements.dialog) + // @ts-ignore + el?.insertAdjacentElement(position, this.elements.dialog) + this.state.value.rendered = this.elements.dialog + + this.elements.dialog.addEventListener( + 'close', + this.events.close + ) + } + + // console.log('DIALOG RENDER', state, position, state.slugs.dialog, modal.rendered) + + this.events.render(this.state) + + return this + } + + // const updateSelf = (cfg, methods = []) => { + // for (let mthd of methods) { + // this[mthd] = { + // ...this[mthd], + // ...(cfg[mthd] || {}), + // } + // } + // } + // updateSelf(cfg, [ + // 'state', 'slugs', 'events', 'elements', 'templates' + // ]) + + // state.render = render + } + + return dl +})(); + +// export function Modal(config) { +// let dialog = new DialogContructor(config) + +// console.log('Modal.js Dialog', dialog) + +// return dialog +// } + +export default DialogContructor diff --git a/src/components/svg-sprite.js b/src/components/svg-sprite.js index 3fa2055..0c7184a 100644 --- a/src/components/svg-sprite.js +++ b/src/components/svg-sprite.js @@ -7,6 +7,10 @@ const initialState = { + diff --git a/src/helpers/constants.js b/src/helpers/constants.js index 629a956..e85f271 100644 --- a/src/helpers/constants.js +++ b/src/helpers/constants.js @@ -142,3 +142,15 @@ export const USAGE = { RECEIVE, CHANGE, } + +const NOT_LOADING = 0 +const LOADING = 1 +const SUCCESS = 2 +const ERROR = 3 + +export const DIALOG_STATUS = { + NOT_LOADING, + LOADING, + SUCCESS, + ERROR +} \ No newline at end of file diff --git a/src/helpers/lit.js b/src/helpers/lit.js index f976e5a..fe7792f 100644 --- a/src/helpers/lit.js +++ b/src/helpers/lit.js @@ -1,8 +1,24 @@ /** + * Code Highlighting for String Literals. + * + * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#raw_strings MDN Reference} + * + * @example + * import { lit as html, lit as css } from './utils.js' + * let h = html`
${example}
` + * let c = css`div > span { color: #bad; }` + * + * // falsey values now default to empty string + * let falsey = html`
${doesNotExist && html``}
` + + * // falsey === '
' + * // instead of + * // falsey === '
undefined
' * * @param {TemplateStringsArray} s * @param {...any} v * * @returns {string} */ -export const lit = (s, ...v) => String.raw({ raw: s }, ...v) +export const lit = (s, ...v) => String.raw({ raw: s }, ...(v.map(x => x || ''))) +// export const lit = (s, ...v) => String.raw({ raw: s }, ...v) diff --git a/src/helpers/utils.js b/src/helpers/utils.js index 493808f..f613de1 100644 --- a/src/helpers/utils.js +++ b/src/helpers/utils.js @@ -13,6 +13,10 @@ import { SECONDS, MINUTE, HOUR, DAY, WEEK, MONTH, YEAR, } from './constants.js' +let currentSignal; +let globalDerivedValue; +let eventHandlers = [] + /** * * @param {String} [phraseOrXkey] @@ -446,14 +450,14 @@ export function envoy(obj, ...initListeners) { * document.querySelector("body").innerHTML = value; * }); * - * off(); // unsubscribe + * off(); * * @param {Object} initialValue inital value */ export function createSignal(initialValue) { let _value = initialValue; let _last = _value; - const subs = []; + let subs = []; function pub() { for (let s of subs) { @@ -461,18 +465,186 @@ export function createSignal(initialValue) { } } + function unsub(fn) { + for (let i in subs) { + if (subs[i] === fn) { + subs[i] = 0; + // break; + } + } + } + + function on(s) { + const i = subs.push(s)-1; + return () => { subs[i] = 0; }; + } + + function once(s) { + const i = subs.length + + subs.push((_value, _last) => { + s && s(_value, _last); + subs[i] = 0; + }); + } + return { - get value() { return _value; }, + get value() { + if ( + currentSignal //&& + // currentSignal !== globalDerivedValue + ) { + console.log( + 'currentSignal === derived', + {currentSignal, derived, globalDerivedValue}, + currentSignal === globalDerivedValue, + ) + on(currentSignal) + } + return _value; + }, set value(v) { _last = _value _value = v; pub(); }, - on: s => { - const i = subs.push(s)-1; - return () => { subs[i] = 0; }; + on, + once, + unsub, + } +} + +/** + * Use a reactive signal in hook fashion + * + * @example + * let [count, setCount, on] = useSignal(0) + * console.log(count()) // 0 + * setCount(2) + * console.log(count()) // 2 + * + * let off = on(value => { + * document.querySelector("body").innerHTML = value; + * }); + * + * off() + * + * @param {Object} initialValue inital value +*/ +export function useSignal(initialValue) { + let _value = initialValue; + let _last = _value; + let subs = []; + + function pub() { + for (let s of subs) { + s && s(_value, _last); + } + } + + function unsub(fn) { + for (let i in subs) { + if (subs[i] === fn) { + subs[i] = 0; + // break; + } } } + + function getValue(v) { + if ( + currentSignal //&& + // currentSignal !== globalDerivedValue + ) { + on(currentSignal) + } + return _value; + } + + function setValue(v) { + _last = _value + _value = v; + pub(); + } + + function on(s) { + const i = subs.push(s)-1; + return () => { subs[i] = 0; }; + } + + function once(s) { + const i = subs.length + + subs.push((_value, _last) => { + s && s(_value, _last); + subs[i] = 0; + }); + } + + return [ + // _value, + getValue, + setValue, + on, + once, + unsub, + ] +} + +/** + * {@link https://youtu.be/t18Kzj9S8-M?t=351 Understanding Signals} + * + * {@link https://youtu.be/1TSLEzNzGQM Learn Why JavaScript Frameworks Love Signals By Implementing Them} + * + * @example + * const [count, setCount] = useSignal(10) + * effect(() => console.log(count())) + * setCount(25) + * + * let letter = createSignal('a') + * effect(() => console.log(letter.value)) + * letter.value = 'b' + * + * @param {Function} fn + */ +export function effect(fn) { + currentSignal = fn; + + fn(); + + currentSignal = null; + + return fn +} + +/** + * {@link https://youtu.be/1TSLEzNzGQM Learn Why JavaScript Frameworks Love Signals By Implementing Them} + * + * @example + * let count = createSignal(10) + * let double = derived(() => count.value * 2) + * + * effect( + * () => console.log( + * count.value, + * double.value, + * ) + * ) + * + * count.value = 25 + * + * @param {Function} fn + */ +export function derived(fn) { + const derived = createSignal() + + globalDerivedValue = function derivedValue() { + derived.value = fn() + } + + effect(globalDerivedValue) + + return derived } export async function restate( @@ -934,6 +1106,20 @@ export function nobounce(callback, delay = 300) { } } + +/** + * @example + * await forIt(500); + * nowDoThis() + * + * @param {number} [delay] +* +* @returns {Promise} +*/ +export function forIt(delay) { + return new Promise(resolve => setTimeout(resolve, delay)); +} + export function timeago(ms, locale = TIMEAGO_LOCALE_EN) { var ago = Math.floor(ms / 1000); var part = 0; @@ -1213,3 +1399,118 @@ export function getAddressIndexFromUsage(wallet, account, usageIdx) { addressIndex, } } + +export function toSlug(...slugs) { + return slugs.join('_').toLowerCase() + .replaceAll(/[^a-zA-Z _]/g, '') + .replaceAll(' ', '_') +} + +export function addListener( + node, + event, + handler, + capture = false, + handlers = this?.eventHandlers || eventHandlers, +) { + console.log('addListener', this, { node, event, handler, capture }) + handlers.push({ node, event, handler, capture }) + node.addEventListener(event, handler, capture) +} + +export function addListeners( + resolve, + reject, +) { + if (resolve && reject) { + addListener( + this.elements.dialog, + 'close', + this.events.close(resolve, reject), + ) + + addListener( + this.elements.dialog, + 'click', + this.events.click, + ) + } + + addListener( + this.elements.form, + 'blur', + this.events.blur, + ) + addListener( + this.elements.form, + 'focusout', + this.events.focusout, + ) + addListener( + this.elements.form, + 'focusin', + this.events.focusin, + ) + addListener( + this.elements.form, + 'change', + this.events.change, + ) + // if (updrop) { + addListener( + this.elements.form, + 'drop', + this.events.drop, + ) + addListener( + this.elements.form, + 'dragover', + this.events.dragover, + ) + addListener( + this.elements.form, + 'dragend', + this.events.dragend, + ) + addListener( + this.elements.form, + 'dragleave', + this.events.dragleave, + ) + // } + addListener( + this.elements.form, + 'input', + this.events.input, + ) + addListener( + this.elements.form, + 'reset', + this.events.reset, + ) + addListener( + this.elements.form, + 'submit', + this.events.submit, + ) +} + +export function removeAllListeners( + targets = [ + this?.elements.dialog, + this?.elements.form, + ], + handlers = this?.eventHandlers || eventHandlers, +) { + if (this.elements.updrop) { + targets.push(this.elements.updrop) + } + handlers = handlers + .filter(({ node, event, handler, capture }) => { + if (targets.includes(node)) { + node.removeEventListener(event, handler, capture) + return false + } + return true + }) +} diff --git a/src/imports.js b/src/imports.js index 9057285..81fb104 100644 --- a/src/imports.js +++ b/src/imports.js @@ -11,6 +11,7 @@ * * See https://github.com/jojobyte/browser-import-rabbit-hole */ +import './secp.js'; import '../node_modules/dashtx/dashtx.js'; import '../node_modules/dashkeys/dashkeys.js'; @@ -20,7 +21,7 @@ import '../node_modules/dashsight/dashsight.js'; import '../node_modules/dashsight/dashsocket.js'; import '../node_modules/@dashincubator/base58check/base58check.js'; import '../node_modules/@dashincubator/ripemd160/ripemd160.js'; -import '../node_modules/@dashincubator/secp256k1/secp256k1.js'; +// import '../node_modules/@dashincubator/secp256k1/secp256k1.js'; import '../node_modules/crypticstorage/cryptic.js'; import '../node_modules/dashwallet/dashwallet.js'; import '../node_modules/localforage/dist/localforage.js'; @@ -56,7 +57,10 @@ export let Base58Check = window?.Base58Check || globalThis?.Base58Check; /** @type {RIPEMD160Types} */ export let RIPEMD160 = window?.RIPEMD160 || globalThis?.RIPEMD160; /** @type {Secp256k1Types} */ -export let Secp256k1 = window?.nobleSecp256k1 || globalThis?.nobleSecp256k1; +export let Secp256k1 = ( + window?.nobleSecp256k1 || globalThis?.nobleSecp256k1 || + window?.Secp256k1 || globalThis?.Secp256k1 +); /** @type {CrypticTypes} */ export let Cryptic = window?.Cryptic || globalThis?.Cryptic; diff --git a/src/index.css b/src/index.css index afa2fa0..11382a1 100644 --- a/src/index.css +++ b/src/index.css @@ -96,16 +96,35 @@ figure h4 { font-size: 2rem; } -.mh-25 { +.min-h-auto { + min-height: auto; +} +.min-h-0 { + min-height: 0; +} +.min-h-25 { + min-height: 25%; +} +.min-h-50 { + min-height: 50%; +} +.min-h-75 { + min-height: 75%; +} +.min-h-100 { + min-height: 100%; +} + +.max-h-25 { max-height: 25%; } -.mh-50 { +.max-h-50 { max-height: 50%; } -.mh-75 { +.max-h-75 { max-height: 75%; } -.mh-100 { +.max-h-100 { max-height: 100%; } @@ -167,9 +186,24 @@ figure h4 { .jc-start { justify-content: start; } +.jc-center { + justify-content: center; +} +.jc-right { + justify-content: right; +} .jc-end { justify-content: end; } +.jc-around { + justify-content: space-around; +} +.jc-between { + justify-content: space-between; +} +.jc-evenly { + justify-content: space-evenly; +} .as-start { align-self: start; } @@ -320,6 +354,28 @@ figure h4 { font-size: 1.5rem; } +.p-0 { + padding: 0; +} +.p-1 { + padding: .25rem; +} +.p-2 { + padding: .75rem; +} +.p-3 { + padding: 1rem; +} +.p-4 { + padding: 1.5rem; +} +.p-5 { + padding: 2rem; +} +.p-6 { + padding: 2.5rem; +} + .px-0 { padding-left: 0; padding-right: 0; diff --git a/src/main.js b/src/main.js index 3f06a08..f23ab8e 100644 --- a/src/main.js +++ b/src/main.js @@ -6,10 +6,16 @@ import { getStoreData, loadStoreObject, formDataEntries, + // forIt, + // useSignal, + // createSignal, + // effect, + // derived, } from './helpers/utils.js' import { DUFFS, + DIALOG_STATUS, } from './helpers/constants.js' import { @@ -79,6 +85,8 @@ import pairQrRig from './rigs/pair-qr.js' import txInfoRig from './rigs/tx-info.js' import showErrorDialog from './rigs/show-error.js' +import crowdnodeTransactionRig from './rigs/crowdnode-tx.js' + // app/data state let accounts let wallets @@ -431,6 +439,40 @@ async function showNotification({ console.log('notification', {type, title, msg, sticky}) } +async function showQrCode(state = {}) { + let initState = { + name: 'Share to receive funds', + submitTxt: `Edit Amount or Contact`, + submitAlt: `Change the currently selected contact`, + footer: state => html` + + `, + amount: 0, + wallet, + contacts: appState.contacts, + ...state, + } + + let showRequestQRRender = await appDialogs.requestQr.render( + initState, + 'afterend', + ) + + let showRequestQR = await appDialogs.requestQr.showModal() + + return showRequestQRRender +} + async function main() { appState.encryptionPassword = window.atob( sessionStorage.encryptionPassword || '' @@ -673,32 +715,9 @@ async function main() { // } // ) - await appDialogs.requestQr.render( - { - name: 'Share to receive funds', - submitTxt: `Edit Amount or Contact`, - submitAlt: `Change the currently selected contact`, - footer: state => html` - - `, - amount: 0, - wallet, - contacts: appState.contacts, - }, - 'afterend', - ) + showQrCode() - let showRequestQR = await appDialogs.requestQr.showModal() + // let showRequestQR = await appDialogs.requestQr.showModal() } } else { await appDialogs.sendOrReceive.render({ @@ -766,6 +785,7 @@ async function main() { res.push(await appTools.storedData?.decryptData?.(v) || v) }, ) + console.log('appState.contacts', appState.contacts) await contactsList.render({ userInfo, @@ -790,23 +810,301 @@ async function main() { walletFunds, }) }) + import('./components/crowdnode-card.js') + .then(async ({ CrowdNodeCard }) => { + // let crowdnodeTransaction = crowdnodeTransactionRig + console.log( + 'crowdnodeTransactionRig', + crowdnodeTransactionRig, + crowdnodeTransactionRig.markup.fields, + ) + // crowdnodeTransactionRig.markup.fields + let cfg = { + state: { + // name: '', + }, + events: { + submit: async event => { + event.preventDefault() + event.stopPropagation() - integrationsSection.insertAdjacentHTML('beforeend', html` -
-
-
Coming soon
-

Earn interest with

-
-
- - - - - - -
-
- `) + // this.elements.form?.removeEventListener('submit', this.events.submit) + + let fde = formDataEntries(event) + + console.log( + `Crowdnode Card submit`, + {event, fde}, + ) + + if (fde.intent === 'signup') { + let confAct = await appDialogs.confirmAction.render({ + name: 'Signup for Crowdnode', + actionTxt: 'Signup', + actionAlt: 'Signup for Crowdnode', + action: '', + actionType: 'infoo', + placement: 'center auto-height', + // status: DIALOG_STATUS.LOADING, + acceptedToS: false, + submitIcon: () => ``, + alert: state => html` +
+ +

This process may take a while, please be patient.

+
+ `, + fields: () => html` +
+
+ To stake your Dash and begin earning interest, read and accept the CrowdNode Terms and Conditions. + Funds are required to complete the signup process. +
+
+ `, + callback: async (state, fde) => { + state.status = DIALOG_STATUS.LOADING + + if (fde?.acceptToS === 'on') { + state.acceptedToS = true + } + + let cbConfAct = await appDialogs.confirmAction.render(state) + + console.log( + `confirm action`, + {state, fde, cbConfAct}, + ) + + let cnFunding = await showQrCode({ + name: 'CrowdNode Funding', + amount: 1.1, + status: DIALOG_STATUS.LOADING, + // wallet, + // contacts: appState.contacts, + // submitTxt: `Fund your wallet`, + // submitAlt: `Change the currently selected contact`, + generateNextAddress: state => { + // return html`` + return html` + Send 1.1 Dash or more
+ to signup & fund your CrowdNode account. + ` + }, + footer: state => html` + + `, + }) + + // await forIt(5000) + + state.status = DIALOG_STATUS.SUCCESS + + cbConfAct = await appDialogs.confirmAction.render(state) + + console.log('CN Card Funding Callback', cnCard) + + cnCard.api.value = { + acceptedToS: true, + balance: 1.234, + earned: 0.987, + } + + cnCard.render({ + cfg, + el: integrationsSection, + position: 'beforeend' + }) + + console.log( + `confirm action SUCCESS`, + {state, fde, cnFunding}, + ) + + return { state, fde } + }, + }) + + // let confAct = appDialogs.confirmAction.render({ + // el: mainApp, + // cfg: { + // appElement: mainApp, + // state: { + // name: 'Signup for Crowdnode', + // actionTxt: 'Signup', + // actionAlt: 'Signup for Crowdnode', + // action: '', + // actionType: 'infoo', + // placement: 'center auto-height', + // // status: DIALOG_STATUS.LOADING, + // }, + // markup: { + // submitIcon: ``, + // alert: html` + //
+ // + //

This process may take a while, please be patient.

+ //
+ // `, + // fields: html` + //
+ //
+ // To stake your Dash and begin earning interest, read and accept the CrowdNode Terms and Conditions. + // Funds are required to complete the signup process. + //
+ //
+ // `, + // }, + // callback: async (state, fde) => { + // state.status = DIALOG_STATUS.LOADING + + // let cbConfAct = await appDialogs.confirmAction.render(state) + + // console.log( + // `confirm action`, + // {state, fde, cbConfAct}, + // ) + + // await forIt(5000) + + // state.status = DIALOG_STATUS.SUCCESS + + // console.log( + // `confirm action SUCCESS`, + // {state, fde}, + // ) + + // return { state, fde } + // }, + // }, + // }) + + console.log('CN Card confAct Submit Event', cnCard) + console.log('confAct', confAct, appDialogs.confirmAction) + + confAct?.elements?.form?.classList.add?.('min-h-auto') + + appDialogs.confirmAction.showModal() + } + + console.log( + `Crowdnode Card submit TX`, + fde.intent, + {event, fde}, + ) + + if (fde.intent === 'deposit') { + appDialogs.sendOrReceive?.elements?.form?.classList.add?.('min-h-auto') + await appDialogs.sendOrReceive.render({ + name: 'Deposit to CrowdNode', + cashSend: () => html``, + hideAddressee: true, + action: fde.intent, + wallet, + account: appState.account, + userInfo, + contacts: appState.contacts, + to: '@crowdnode', + }) + appDialogs.sendOrReceive.showModal() + } + if (fde.intent === 'withdraw') { + crowdnodeTransactionRig.markup.fields = html` +
+ + +
+ Enter the percentage you wish to unstake. + ` + + let cnWithdraw = crowdnodeTransactionRig.render({ + el: mainApp, + cfg: { + state: { + name: 'Withdraw from CrowdNode', + submitTxt: 'Withdraw', + submitAlt: 'Withdraw funds from CrowdNode', + cancelTxt: 'Cancel', + cancelAlt: `Cancel Withdraw`, + }, + }, + }) + cnWithdraw?.elements?.form?.classList.add?.('min-h-auto') + + console.log( + `Crowdnode Card TX`, + fde.intent, + {cnWithdraw}, + ) + crowdnodeTransactionRig.showModal() + // cnWithdraw.showModal() + } + } + }, + } + + let cnCard = new CrowdNodeCard(cfg) + console.log('CN Card Outer', cnCard) + + // cfg.events.submit = + + // cnCard.updateConfig(cfg) + + cnCard.render({ + cfg, + el: integrationsSection, + position: 'beforeend' + }) + }) + + + // integrationsSection.insertAdjacentHTML('beforeend', html` + //
+ //
+ //
Coming soon
+ //

Earn interest with

+ //
+ //
+ // + // + // + // + // + // + //
+ //
+ // `) let txs = await getTxs( appState, @@ -895,7 +1193,7 @@ async function main() { action: 'lock', actionType: 'warn', alert: state => html``, - callback: () => { + callback: async () => { sessionStorage.clear() window.location.reload() }, @@ -925,7 +1223,7 @@ async function main() { This is an irreversable action which removes all wallet data from your browser, make sure to backup your data first.

WE RETAIN NO BACKUPS OF YOUR WALLET DATA.

`, - callback: () => { + callback: async () => { localStorage.clear() sessionStorage.clear() // @ts-ignore diff --git a/src/rigs/confirm-action.js b/src/rigs/confirm-action.js index 1e43ca1..4d220a1 100644 --- a/src/rigs/confirm-action.js +++ b/src/rigs/confirm-action.js @@ -3,6 +3,10 @@ import { formDataEntries, } from '../helpers/utils.js' +import { + DIALOG_STATUS, +} from '../helpers/constants.js' + export let confirmActionRig = (async function (globals) { 'use strict'; @@ -26,6 +30,7 @@ export let confirmActionRig = (async function (globals) { actionType: 'warn', actionClasses: { info: 'bg-info dark bg-info-hover', + infoo: 'outline brd-info info dark light-hover bg-info-hover', warn: 'outline brd-warn warn dark-hover bg-warn-hover', dang: 'outline brd-dang dang light-hover bg-dang-hover', }, @@ -79,16 +84,7 @@ export let confirmActionRig = (async function (globals) { `, alert: state => html``, - // alert: state => html` - //
- // - // This is an irreversable action, make sure to backup first. - // - //
- // `, - content: state => html` - ${state.header(state)} - + fields: state => html`
Are you sure you want to ${state.action} ${ @@ -96,6 +92,11 @@ export let confirmActionRig = (async function (globals) { }?
+ `, + content: state => html` + ${state.header(state)} + + ${state.fields(state)} ${state.footer(state)} `, @@ -108,8 +109,17 @@ export let confirmActionRig = (async function (globals) { if (fde?.intent === 'act') { // state.elements.dialog.returnValue = String(fde.intent) - state.callback?.(state, fde) - confirmAction.close(fde.intent) + let res = await state.callback?.(state, fde) + + if ( + res.state.status === DIALOG_STATUS.SUCCESS || + res.state.status === DIALOG_STATUS.ERROR + ) { + // state.elements.dialog?.querySelector('progress')?.remove() + state.elements.progress?.remove?.() + + confirmAction.close(fde.intent) + } } }, }, diff --git a/src/rigs/crowdnode-tx.js b/src/rigs/crowdnode-tx.js new file mode 100644 index 0000000..66b86c4 --- /dev/null +++ b/src/rigs/crowdnode-tx.js @@ -0,0 +1,135 @@ +import { lit as html } from '../helpers/lit.js' +import { + formDataEntries, +} from '../helpers/utils.js' + +import { + DIALOG_STATUS, +} from '../helpers/constants.js' + +import { DialogContructor } from '../components/modal.js' + +export const crowdnodeTransactionRig = (() => { + 'use strict'; + + // let { + // mainApp, setupDialog, + // } = globals + + let dialogConfig = { + state: { + name: 'CrowdNode Deposit / Withdraw', + actionTxt: 'Do It!', + actionAlt: 'Yeah, really do this!', + cancelTxt: 'Cancel', + cancelAlt: `Cancel`, + closeTxt: html``, + closeAlt: `Cancel & Close`, + action: '', + target: '', + targetFallback: 'this wallet', + actionType: 'warn', + actionClasses: { + info: 'bg-info dark bg-info-hover', + infoo: 'outline brd-info info dark light-hover bg-info-hover', + warn: 'outline brd-warn warn dark-hover bg-warn-hover', + dang: 'outline brd-dang dang light-hover bg-dang-hover', + }, + showCancelBtn: true, + showActBtn: true, + }, + markup: {}, + events: {}, + // appElement: mainApp, + appElement: document.body, + } + + let crowdnodeTransaction = new DialogContructor(dialogConfig) + + console.log('Modal.js Dialog', crowdnodeTransaction) + + dialogConfig.events.submit = async function (event) { + event.preventDefault() + event.stopPropagation() + + let fde = formDataEntries(event) + + if (fde?.intent === 'act') { + // state.elements.dialog.returnValue = String(fde.intent) + let res = await this.state.value.callback?.(this.state.value, fde) + + if ( + res.state.value.status === DIALOG_STATUS.SUCCESS || + res.state.value.status === DIALOG_STATUS.ERROR + ) { + // this.elements.dialog?.querySelector('progress')?.remove() + this.elements.progress?.remove?.() + + this.elements.dialog?.close(fde.intent) + } + } + } + + dialogConfig.markup.alert = html`` + dialogConfig.markup.submitIcon = `` + // dialogConfig.markup.submitIcon = html` + // + // + // + // ` + // dialogConfig.markup.cancelBtn = html` + // + // ` + // dialogConfig.markup.actionBtn = html` + // + // ` + // dialogConfig.markup.footer = html` + // + // ` + // dialogConfig.markup.fields = html` + //
+ // + // Are you sure you want to ${crowdnodeTransaction.state.value.action} ${ + // crowdnodeTransaction.state.value.target || crowdnodeTransaction.state.value.targetFallback + // }? + // + //
+ // ` + // dialogConfig.markup.content = () => html` + // ${crowdnodeTransaction.markup.header} + + // ${crowdnodeTransaction.markup.fields} + + // ${crowdnodeTransaction.markup.footer} + // ` + + crowdnodeTransaction.updateConfig(dialogConfig) + + return crowdnodeTransaction +})(); + +export default crowdnodeTransactionRig \ No newline at end of file diff --git a/src/rigs/send-or-request.js b/src/rigs/send-or-request.js index a2d94ac..ac290f2 100644 --- a/src/rigs/send-or-request.js +++ b/src/rigs/send-or-request.js @@ -38,6 +38,7 @@ export let sendOrReceiveRig = (async function (globals) { closeTxt: html``, closeAlt: `Close`, action: 'send', + hideAddressee: false, submitIcon: state => { const icon = { send: html` @@ -180,26 +181,56 @@ export let sendOrReceiveRig = (async function (globals) { ` }, + cashSend: state => html` +
+ + + +
+ `, + addressee: state => { + if (state.hideAddressee) { + return html` + + ` + } + + return html` +
+ + + ${state.qrScanBtn(state)} +
+ ` + }, content: state => html` ${state.header(state)}
-
- - - ${state.qrScanBtn(state)} -
+ ${state.addressee(state)}
-
- - - -
+ ${state.cashSend(state)}
@@ -444,6 +464,10 @@ export let sendOrReceiveRig = (async function (globals) { inWallet = Object.values(contact?.incoming)?.[0] } + console.log('send or request', { + to, contact, outWallet, inWallet + }) + if (!inWallet) { // state.wallet.addressIndex = ( // state.wallet?.addressIndex ?? -1 @@ -518,7 +542,7 @@ export let sendOrReceiveRig = (async function (globals) { } let leftoverBalance = walletFunds.balance - amount - let fullTransfer = leftoverBalance <= 0.0010_0200 + let fullTransfer = leftoverBalance > 0 && leftoverBalance <= 0.0010_0200 // let fullTransfer = leftoverBalance <= 0.0001_0200 if ( @@ -542,6 +566,7 @@ export let sendOrReceiveRig = (async function (globals) { state.wallet.accountIndex, state.wallet.addressIndex, ) + let amountNeeded = fixedDash(roundUsing(Math.floor, Math.abs( walletFunds.balance - Number(fde.amount) ))) diff --git a/src/rigs/show-error.js b/src/rigs/show-error.js index 0e590d5..b59d84f 100644 --- a/src/rigs/show-error.js +++ b/src/rigs/show-error.js @@ -60,7 +60,7 @@ export async function showErrorDialog(options) { content: state => html` ${state.header(state)} -
+
diff --git a/src/secp.js b/src/secp.js new file mode 100644 index 0000000..f49b0e8 --- /dev/null +++ b/src/secp.js @@ -0,0 +1,11 @@ +import '../node_modules/@dashincubator/secp256k1/secp256k1.js'; + +// @ts-ignore +const secp = window?.nobleSecp256k1 || globalThis?.nobleSecp256k1 || window?.Secp256k1 || globalThis?.Secp256k1 + +// @ts-ignore +window.Secp256k1 = secp + +export const Secp256k1 = secp + +export default secp diff --git a/src/styles/components.css b/src/styles/components.css index 43c7933..6751960 100644 --- a/src/styles/components.css +++ b/src/styles/components.css @@ -116,6 +116,9 @@ display: flex; } +.integration-sect { + padding: 0 1rem; +} .integration-sect > section > header { flex-direction: column; align-items: start; @@ -140,6 +143,34 @@ text-decoration: none; } +.card { + border-radius: 1rem; + overflow: auto; + flex: 1 1 auto; + max-width: 49%; + overflow: hidden; +} +.card header, +.card footer { + padding: 1rem 0; +} + +.card header a, +.card header a:active, +.card header a:focus, +.card header a:hover { + color: var(--fc); + text-decoration: none; +} +.card section { + padding: .25rem 0; +} +.card fieldset { + min-width: auto; +} +.card footer button { +} + @@ -150,6 +181,21 @@ .cols > section { padding: 0; } + + .card { + max-width: 32.5%; + padding: 0 1rem; + } + .card header, + .card footer { + padding: 1rem 0; + } + .card section { + padding: .25rem 0; + } + .integration-sect { + padding: 0; + } } @media (min-width: 980px) { .cols { diff --git a/src/styles/dialog.css b/src/styles/dialog.css index 75c3b1b..25ecf42 100644 --- a/src/styles/dialog.css +++ b/src/styles/dialog.css @@ -197,7 +197,7 @@ dialog > form > fieldset input + p { font-weight: 400; margin: .5rem 0; } -dialog > form > fieldset p { +dialog > form fieldset p { display: flex; padding: 0; margin: .8rem; @@ -217,7 +217,7 @@ dialog > form > fieldset div + p { margin: 0 0 0 1rem; text-align: left; } -dialog > form > fieldset p > span { +dialog > form fieldset p > span { padding: .25rem; border: 1px solid var(--dark-500); } diff --git a/src/styles/form.css b/src/styles/form.css index c1fc1b1..d1a4aca 100644 --- a/src/styles/form.css +++ b/src/styles/form.css @@ -494,7 +494,7 @@ label { } label > input[type=checkbox], label + input[type=checkbox] { - width: 2rem; + width: 1.25rem; } div.switch { @@ -739,6 +739,25 @@ form[name="network"] { cursor: default; } +main form header { + margin: 0 auto; + min-height: auto; + /* width: 100%; + display: flex; + justify-content: space-between; + align-items: flex-start; + flex-direction: column-reverse; */ +} + +.card footer { + gap: .25rem; +} + +.card footer button { + padding: 0.63rem .25rem; + flex: 1 1 50%; +} + @media (min-width: 650px) { main form { @@ -776,4 +795,8 @@ form[name="network"] { form > article > figure figcaption + div.big { font-size: 3.75rem; } */ + + .card footer button { + padding: 0.63rem 1rem; + } } \ No newline at end of file diff --git a/src/styles/motion.css b/src/styles/motion.css index 26aa428..85cff62 100644 --- a/src/styles/motion.css +++ b/src/styles/motion.css @@ -1,16 +1,16 @@ @-webkit-keyframes await-progress { - 0% { - background-position: -322px; + 0% { + background-position: -150%; } 100% { - background-position: 322px; + background-position: 200%; } } @keyframes await-progress { - 0% { - background-position: -322px; + 0% { + background-position: -150%; } 100% { - background-position: 322px; + background-position: 200%; } } diff --git a/src/styles/progress.css b/src/styles/progress.css index 1ad2c4f..6d30564 100644 --- a/src/styles/progress.css +++ b/src/styles/progress.css @@ -1,19 +1,3 @@ -progress:indeterminate { - background: linear-gradient( - 90deg, - #0000 0%, - #0000 50%, - var(--info) 100% - ); -} -progress.recording:indeterminate { - background: linear-gradient( - 90deg, - var(--livea) 0%, - var(--live) 50%, - var(--livea) 100% - ); -} progress.pending, progress.pending[role] { position: absolute; @@ -26,14 +10,15 @@ progress.pending[role] { padding: 0; border: none; background-color: transparent; - background-size: auto; + /* background-size: auto; */ + background-size: 50% 100%; background-repeat: no-repeat; background-position: 0 0; appearance: none; -moz-appearance: none; -webkit-appearance: none; border: 0 solid transparent; - visibility: hidden; + /* visibility: hidden; */ } progress.recording, progress.recording[role] { @@ -52,11 +37,3 @@ progress.pending:indeterminate { -webkit-animation: await-progress 2.5s ease-in-out infinite; animation: await-progress 2.5s ease-in-out infinite; } - - -/* form[name=toggle_relay]:has(input[type=checkbox]:checked) { - border-bottom: .25rem solid var(--dang); -} */ -form[name=toggle_relay]:has(input[type=checkbox]:checked) progress { - visibility: visible; -} diff --git a/src/styles/theme.css b/src/styles/theme.css index c8e5830..55d1c2e 100644 --- a/src/styles/theme.css +++ b/src/styles/theme.css @@ -19,6 +19,10 @@ --light-500: #004470; --light-600: #003e66; + --card-100: hsl(200deg 80% 60% / 15%); + --card-100: #00447066; + + --grayscale-gray-400: #9DA6A6; --c: var(--dark-900); @@ -140,11 +144,11 @@ a:hover { } svg { - fill: currentColor; vertical-align: bottom; } -svg path { +svg g:not(.lock), +svg path:not(.lock) { fill: currentColor; } @@ -316,6 +320,11 @@ th { border-radius: 6.25rem; } +.card { + background-color: var(--card-100); + /* background-color: var(--dark-600); */ +} + .cols > section > div { background-color: var(--nav-bg); }