From c7b02f02ad0a7e2888f2d7d3599719e59bbd1ee2 Mon Sep 17 00:00:00 2001 From: Nathan Reiner Date: Thu, 13 Nov 2025 14:56:02 +0100 Subject: frontend: design prototype --- static/widgets/editable/index.css | 36 ++++++++++ static/widgets/editable/index.js | 65 ++++++++++++++++++ static/widgets/month-select/index.css | 125 ++++++++++++++++++++++++++++++++++ static/widgets/month-select/index.js | 75 ++++++++++++++++++++ static/widgets/search/index.css | 50 ++++++++++++++ static/widgets/search/index.js | 67 ++++++++++++++++++ 6 files changed, 418 insertions(+) create mode 100644 static/widgets/editable/index.css create mode 100644 static/widgets/editable/index.js create mode 100644 static/widgets/month-select/index.css create mode 100644 static/widgets/month-select/index.js create mode 100644 static/widgets/search/index.css create mode 100644 static/widgets/search/index.js (limited to 'static/widgets') diff --git a/static/widgets/editable/index.css b/static/widgets/editable/index.css new file mode 100644 index 0000000..b474370 --- /dev/null +++ b/static/widgets/editable/index.css @@ -0,0 +1,36 @@ + +#container { + width: 100%; + background: var(--card-background); + box-shadow: var(--shadow); + border-radius: var(--border-radius); + display: grid; + grid-template-columns: min-content auto min-content; +} + +#container label { + user-select: none; + font-weight: bold; + background: var(--primary); + color: var(--fg-primary); + align-content: center; + padding: 10px; + border-top-left-radius: var(--border-radius); + border-bottom-left-radius: var(--border-radius); +} + +#container input { + background: var(--card-background); + user-select: none; + font-size: 1em; +} + +#container input:read-only { + caret-color: transparent; +} + +#edit { + width: 35px; + padding: 10px; + cursor: pointer; +} diff --git a/static/widgets/editable/index.js b/static/widgets/editable/index.js new file mode 100644 index 0000000..8a72aff --- /dev/null +++ b/static/widgets/editable/index.js @@ -0,0 +1,65 @@ +import * as sfw from 'sfw'; +const { Div, Label, Input } = sfw.element.native; + +import icons from '../../icons/index.js'; + +const css = await sfw.css(import.meta.url, './index.css'); + +export default class Editable extends sfw.element.Container { + #label + #input + #edit + + constructor() { + super({ css }); + + this.body.append( + Div.new({ + id: 'container', + children: [ + this.#label = Label.new({ htmlFor: 'input' }), + this.#input = Input.new({ + readOnly: true, + onkeydown: (e) => { + if (e.key === 'Enter') { + this.#input.readOnly = true; + this.#update() + } + }, + }), + this.#edit = Div.new({ + id: 'edit', + children: [ icons.edit ], + onclick: () => { + this.#input.readOnly = !this.#input.readOnly; + this.#update(); + } + }), + ], + }) + ); + } + + #update() { + this.#edit.innerHTML = ''; + this.#edit.append( + this.#input.readOnly ? icons.edit : icons.check + ); + + if (!this.#input.readOnly) { + this.#input.select(); + } + } + + set title(value) { + this.#label.innerText = value; + } + + set value(value) { + this.#input.value = value; + } + + set type(type) { + this.#input.type = type; + } +} diff --git a/static/widgets/month-select/index.css b/static/widgets/month-select/index.css new file mode 100644 index 0000000..87b63c0 --- /dev/null +++ b/static/widgets/month-select/index.css @@ -0,0 +1,125 @@ +:host { + position: fixed; + top: 0; + left: 0; +} + +#container { + z-index: 1000; + position: fixed; + bottom: -100%; + left: 5px; + right: 5px; + background: #fff9; + backdrop-filter: blur(10px); + border-radius: var(--border-radius); + transition: bottom 0.1s ease; + height: calc(100% - 10px); + box-shadow: var(--shadow); + padding: 10px; +} + +#container.visible { + bottom: 5px; +} + +#close-button { + position: absolute; + top: 10px; + right: 10px; + width: 25px; + height: 25px; + border-radius: 100%; + background: var(--page-background); + cursor: pointer; +} + +#title { + font-size: 1.2em; + user-select: none; + margin-bottom: 10px; +} + +#month-container { + height: calc(100% - 30px); + overflow-y: auto; + overscroll-behavior: contain; +} + +.month-item { + padding: 10px; + padding-left: 20px; + margin: 5px; + border-radius: var(--border-radius); + user-select: none; + cursor: pointer; + position: relative; +} + +.month-item:before { + content: ''; + display: block; + position: absolute; + background: var(--primary); + top: 0px; + left: 0px; + width: 4px; + height: 47px; +} + +.month-item:after { + content: ''; + display: block; + position: absolute; + background: var(--primary); + top: 50%; + transform: translate(0, -50%); + left: 0px; + width: 12px; + height: 4px; +} + +.month-item:first-child:before { + top: 50%; + height: 27px; +} + +.month-item:hover { + background: var(--primary); + color: var(--fg-primary); +} + +.year-item { + padding: 10px 20px; + font-size: 1.1em; + margin: 5px; + position: relative; + font-weight: bold; + background: #fffa; + border-radius: var(--border-radius); + box-shadow: #22322355 1px 1px 2px; +} + +.year-item:before { + content: ''; + display: block; + position: absolute; + background: var(--primary); + top: 0px; + left: 0px; + width: 4px; + height: 50px; +} + +.year-item:after { + content: ''; + display: block; + position: absolute; + background: var(--primary); + top: 50%; + transform: translate(0, -50%); + left: -4px; + width: 12px; + height: 12px; + border-radius: 100%; +} diff --git a/static/widgets/month-select/index.js b/static/widgets/month-select/index.js new file mode 100644 index 0000000..d23469d --- /dev/null +++ b/static/widgets/month-select/index.js @@ -0,0 +1,75 @@ +import * as sfw from 'sfw'; +const { Div } = sfw.element.native; + +import icons from '../../icons/index.js'; + +const css = await sfw.css(import.meta.url, './index.css'); + +export default class MonthSelect extends sfw.element.Container { + #container + #month_container + + constructor() { + super({ css }); + + this.onscroll = (e) => e.stopPropagate(); + this.onmonth = () => {}; + + this.body.append( + this.#container = Div.new({ + id: 'container', + children: [ + Div.new({ id: 'title', innerText: 'Select Month' }), + Div.new({ + id: 'close-button', + children: [ icons.close ], + onclick: () => this.hide(), + }), + this.#month_container = Div.new({ + id: 'month-container', + }), + ], + }), + ); + } + + show() { + this.#container.classList.add('visible'); + } + + hide() { + this.#container.classList.remove('visible'); + } + + set months(months) { + months.sort((a, b) => a.is_before(b)); + + this.#month_container.innerHTML = ''; + + let last = null; + + for (const month of months) { + if (last != null && !last.is_same_year(month)) { + this.#month_container.append( + Div.new({ + className: 'year-item', + innerText: last.year, + }) + ); + } + + this.#month_container.append( + Div.new({ + className: 'month-item', + innerText: month.name, + onclick: () => { + this.hide(); + this.onmonth(month); + } + }) + ); + + last = month; + } + } +} diff --git a/static/widgets/search/index.css b/static/widgets/search/index.css new file mode 100644 index 0000000..2bb940e --- /dev/null +++ b/static/widgets/search/index.css @@ -0,0 +1,50 @@ +:host { + position: fixed; + top: 0; + left: 0; +} + +#container { + position: fixed; + top: -200px; + left: 5px; + right: 5px; + height: 100px; + background: #fff9; + backdrop-filter: blur(10px); + border-radius: var(--border-radius); + transition: top 0.1s ease; + padding: 10px; + display: grid; + gap: 10px; + box-shadow: var(--shadow); +} + +#container.visible { + top: 5px; +} + +#title { + font-size: 1.2em; + align-content: center; + padding-left: 5px; + user-select: none; +} + +#search-box { + width: 100%; + display: grid; + grid-template-columns: auto 50px; +} + +input { + border-top-right-radius: 0px !important; + border-bottom-right-radius: 0px !important; +} + +button { + width: 50px; + border-top-left-radius: 0px !important; + border-bottom-left-radius: 0px !important; + height: 42px; +} diff --git a/static/widgets/search/index.js b/static/widgets/search/index.js new file mode 100644 index 0000000..324141b --- /dev/null +++ b/static/widgets/search/index.js @@ -0,0 +1,67 @@ +import * as sfw from 'sfw'; +const { Div, Input, Button } = sfw.element.native; + +import icons from '../../icons/index.js'; + +const css = await sfw.css(import.meta.url, './index.css'); + +export default class Search extends sfw.element.Container { + #container + #search + + constructor() { + super({ css }); + + this.onsubmit = () => {} + this.onhide = () => {} + + this.onclick = (e) => e.stopPropagation(); + this.hide = () => { + this.#container.classList.remove('visible'); + document.removeEventListener('click', this.hide); + this.onhide(); + }; + + this.body.append( + this.#container = Div.new({ + id: 'container', + children: [ + Div.new({ innerText: 'Search', id: 'title' }), + Div.new({ + id: 'search-box', + children: [ + this.#search = Input.new({ + type: 'search', + onsearch: () => this.submit(), + onkeydown: (event) => { + if (event.key === 'Enter') { + this.submit(); + } + } + }), + Button.new({ + children: [ icons.search ], + onclick: () => this.submit(), + }), + ] + }) + ] + }) + ); + } + + submit() { + this.onsubmit(this.#search.value); + this.#search.blur(); + this.hide(); + } + + toggle() { + this.#container.classList.toggle('visible'); + + if (this.#container.classList.contains('visible')) { + this.#search.focus() + document.addEventListener('click', this.hide) + } + } +} -- cgit v1.2.3-70-g09d2