diff --git a/app/assets/stylesheets/knobs.css b/app/assets/stylesheets/knobs.css new file mode 100644 index 000000000..77b800210 --- /dev/null +++ b/app/assets/stylesheets/knobs.css @@ -0,0 +1,135 @@ +.knob { + --knob-angle-reserve: 120deg; + --knob-angle: calc((360deg - var(--knob-angle-reserve)) / (var(--knob-options) - 1)); + --knob-button-size: 3ch; + --knob-chamfer-size: 1ch; + --knob-color: oklch(var(--lch-ink-light)); + --knob-color-accent: oklch(var(--lch-red-medium)); + --knob-tick-size: 1ch; + --knob-radius: calc(var(--knob-size) / 2); + --knob-size: 96px; + + aspect-ratio: 1; + block-size: calc(var(--knob-size) + var(--knob-button-size) * 2); + border: none; + display: grid; + font-weight: 500; + inline-size: fit-content; + place-content: center; + position: relative; +} + +.knob__slider { + appearance: none; + background-color: transparent; + block-size: var(--knob-size); + inline-size: var(--knob-size); + inset: 50% auto auto 50%; + position: absolute; + translate: -50% -50%; + z-index: 1; + + &::-webkit-slider-runnable-track { + block-size: var(--knob-size); + cursor: grab; + } + + &::-webkit-slider-thumb { + appearance: none; + background-color: transparent; + } +} + +.knob__option { + block-size: var(--knob-button-size); + border-radius: 50%; + cursor: pointer; + display: grid; + inline-size: var(--knob-button-size); + inset: 50% auto auto 50%; + place-content: center; + position: absolute; + transform: + translate(-50%, -50%) + rotate(calc(-1 * var(--knob-angle-reserve) + (var(--knob-angle) * var(--i)))) + translateY(calc(-1 * var(--knob-radius) - 50% - var(--knob-tick-size))); + z-index: 1; + + &:hover, + &:has(input:checked) { + color: var(--knob-color-accent); + } + + /* Tick marks */ + &:before { + background-color: var(--knob-color); + block-size: var(--knob-tick-size); + content: ""; + inline-size: 2px; + inset: 100% auto auto 50%; + position: absolute; + translate: -50% 0; + } + + /* The value text */ + span { + rotate: calc(var(--knob-angle-reserve) - (var(--knob-angle) * var(--i))); + } + + input { + opacity: 0; + position: absolute; + } +} + +.knob__knob { + background: linear-gradient(to top, var(--knob-color), color-mix(in oklch, var(--knob-color) 50%, var(--color-canvas) 50%)); + block-size: var(--knob-size); + border-radius: 50%; + box-shadow: + 0 0 2px 1px rgba(0,0,0,0.10), + 0 2px 4px rgba(0,0,0,0.15), + 0 2px 8px rgba(0,0,0,0.20); + inline-size: var(--knob-size); + position: relative; + + &:before, + &:after { + content: ""; + position: absolute; + } + + /* Indent */ + &:before { + background: linear-gradient(to bottom, var(--knob-color), color-mix(in oklch, var(--knob-color) 50%, var(--color-canvas) 50%)); + block-size: calc(var(--knob-size) - var(--knob-chamfer-size)); + border-radius: 50%; + box-shadow: + 0 -1px 0 rgba(255, 255, 255, 0.25), + inset 0 -1px 0 rgba(255, 255, 255, 0.25); + inline-size: calc(var(--knob-size) - var(--knob-chamfer-size)); + inset: 50% auto auto 50%; + translate: -50% -50%; + } + + /* Indicator */ + &:after { + background-color: var(--color-ink); + block-size: calc(var(--knob-radius) - var(--knob-chamfer-size) / 2); + border-radius: 50% 50% 2px 2px; + inline-size: 4px; + inset: auto auto 50% 50%; + rotate: calc(-1 * var(--knob-angle-reserve) + (var(--knob-angle) * var(--knob-index))); + transform-origin: center bottom; + transition: rotate 100ms; + translate: -50% 0; + } +} + +.knob__label { + font-weight: bold; + inset: 50% auto auto 50%; + position: absolute; + text-transform: uppercase; + transform: translate(-50%, calc(var(--knob-radius) + var(--knob-tick-size))); +} diff --git a/app/javascript/controllers/knob_controller.js b/app/javascript/controllers/knob_controller.js new file mode 100644 index 000000000..fe90fe84e --- /dev/null +++ b/app/javascript/controllers/knob_controller.js @@ -0,0 +1,27 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = [ "wrapper" ] + + optionChanged(e) { + const input = e.target + const value = input.value + const index = (input.getAttribute("data-index")) + this.wrapperTarget.style.setProperty("--knob-index", `${index}`); + } + + sliderChanged(e) { + const sliderIndex = e.target.value + } +} + + +// The slider has a range of 0-4. We can't add day values statically. +// These 0-4 values are used to set the angle via css. +// but, that if the range was simply used to set the value of the radio input. +// OK, what if we get the knob and radio inputs working as expected, then… +// When radio input changes, update the slider +// when the slider changes, update the radio input + +// knob need an index, options have the index. +// when option is changed, set i on main component diff --git a/app/views/collections/edit/_auto_close.html.erb b/app/views/collections/edit/_auto_close.html.erb index e0a05df2b..efbcb3218 100644 --- a/app/views/collections/edit/_auto_close.html.erb +++ b/app/views/collections/edit/_auto_close.html.erb @@ -4,3 +4,5 @@ <%= form_with model: collection, data: { controller: "form" } do |form| %> <%= form.select :auto_close_period, collection_auto_close_options, {}, { class: "input input--select full-width margin-block-end", data: { action: "change->form#submit" } } %> <% end %> + +<%= render "collections/edit/knob", day_options: [3, 7, 30, 90, 365], default_index: 0, label: "Days until auto-close" %> diff --git a/app/views/collections/edit/_knob.html.erb b/app/views/collections/edit/_knob.html.erb new file mode 100644 index 000000000..b036a02f0 --- /dev/null +++ b/app/views/collections/edit/_knob.html.erb @@ -0,0 +1,30 @@ +
+ + +<%# + +OK, the indicator needs to know what index position to point to +We have --knob-index on the wrapper, set to the default. +As the input changes, update that index. + +%>