diff --git a/app/assets/stylesheets/knobs.css b/app/assets/stylesheets/knobs.css new file mode 100644 index 000000000..573ef12a7 --- /dev/null +++ b/app/assets/stylesheets/knobs.css @@ -0,0 +1,142 @@ +@layer components { + .knob { + --knob-angle-reserve: 120deg; + --knob-option-angle: calc((360deg - var(--knob-angle-reserve)) / (var(--knob-options) - 1)); + --knob-option-size: 3ch; + --knob-chamfer-size: 1ch; + --knob-color: oklch(var(--lch-ink-light)); + --knob-color-accent: oklch(var(--lch-blue-medium)); + --knob-tick-size: 1ch; + --knob-radius: calc(var(--knob-size) / 2); + --knob-size: 96px; + + border: none; + display: block; + font-weight: 500; + padding: var(--knob-option-size) 0 0; + position: relative; + text-align: center; + } + + .knob__slider { + --hover-size: 0; + + 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; + height: 1px; + width: 1px; + } + } + + .knob__option { + block-size: var(--knob-option-size); + border-radius: 50%; + cursor: pointer; + display: grid; + inline-size: var(--knob-option-size); + inset: 50% auto auto 50%; + place-content: center; + position: absolute; + transform: + translate(-50%, -50%) + rotate(calc(-1 * ((360deg - var(--knob-angle-reserve)) / 2) + (var(--knob-option-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); + } + + &:has(:focus-visible) { + box-shadow: 0 0 0 2px var(--knob-color); + } + + /* 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(((360deg - var(--knob-angle-reserve)) / 2) - (var(--knob-option-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); + margin-inline: auto; + 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 * ((360deg - var(--knob-angle-reserve)) / 2) + (var(--knob-option-angle) * var(--knob-index))); + transform-origin: center bottom; + transition: rotate 100ms; + translate: -50% 0; + } + } + + .knob__label { + font-weight: bold; + margin-block-start: 1ch; + text-transform: uppercase; + } +} \ No newline at end of file diff --git a/app/helpers/collections_helper.rb b/app/helpers/collections_helper.rb index 2155b4ae5..6d050be9a 100644 --- a/app/helpers/collections_helper.rb +++ b/app/helpers/collections_helper.rb @@ -1,11 +1,12 @@ module CollectionsHelper def collection_auto_close_options [ - [ "3 days", 3.days ], - [ "7 days", 7.days ], - [ "30 days", 30.days ], - [ "90 days", 90.days ], - [ "365 days", 365.days ] + [ "3", 3.days ], + [ "7", 7.days ], + [ "11", 11.days ], + [ "30", 30.days ], + [ "90", 90.days ], + [ "365", 365.days ] ] end diff --git a/app/javascript/controllers/knob_controller.js b/app/javascript/controllers/knob_controller.js new file mode 100644 index 000000000..7603f5ed0 --- /dev/null +++ b/app/javascript/controllers/knob_controller.js @@ -0,0 +1,28 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = [ "field", "option", "slider" ] + + optionChanged(e) { + this.#setIndex(e.target.getAttribute("data-index")) + } + + sliderChanged(e) { + const index = e.target.value + this.#setIndex(index) + this.#setValue(index) + } + + #setIndex(index) { + this.fieldTarget.style.setProperty("--knob-index", `${index}`); + this.sliderTarget.value = index; + } + + #setValue(index) { + const option = this.optionTargets.find(option => { + return option.dataset.index === index; + }); + + option.checked = true; + } +} \ No newline at end of file diff --git a/app/views/collections/edit.html.erb b/app/views/collections/edit.html.erb index 119e926d9..aa592630b 100644 --- a/app/views/collections/edit.html.erb +++ b/app/views/collections/edit.html.erb @@ -32,10 +32,9 @@
<%= render "collections/edit/workflows", collection: @collection %> - <%= render "collections/edit/auto_close", collection: @collection %> - + <%= form_with model: @collection, class: "txt-align-center", method: :delete do |form| %> <%= form.button class: "btn txt-negative borderless txt-small", data: { turbo_confirm: "Are you sure you want to permanently delete this Collection?" } do %> diff --git a/app/views/entropy/_auto_close.html.erb b/app/views/entropy/_auto_close.html.erb index 1afa046e9..db12171b7 100644 --- a/app/views/entropy/_auto_close.html.erb +++ b/app/views/entropy/_auto_close.html.erb @@ -3,17 +3,25 @@
Cards in Considering -

Auto-close after…

- <%= form_with model: model, url: url, data: { controller: "form" } do |form| %> - <%= form.select :auto_reconsider_period, collection_auto_close_options, {}, { class: "input input--select full-width margin-block-half txt-small fill-white txt-align-center", data: { action: "change->form#submit" } } %> +

Auto-close after…

+ <%= form_with model: model, url: url || collection_path(model), data: { controller: "form" } do |form| %> + <%= render "entropy/knob", + knob_options: collection_auto_close_options.map(&:first).map(&:to_i), + label: "Days until auto-close", + period: model.auto_close_period, + field_name: url ? "entropy_configuration[auto_close_period]" : "collection[auto_close_period]" %> <% end %>
Cards in Doing -

Fall back to Considering after…

- <%= form_with model: model, url: url, data: { controller: "form" } do |form| %> - <%= form.select :auto_close_period, collection_auto_close_options, {}, { class: "input input--select full-width margin-block-half txt-small fill-white txt-align-center", data: { action: "change->form#submit" } } %> +

Fall back to Considering after…

+ <%= form_with model: model, url: url || collection_path(model), data: { controller: "form" } do |form| %> + <%= render "entropy/knob", + knob_options: collection_auto_close_options.map(&:first).map(&:to_i), + label: "Days until auto-close", + period: model.auto_reconsider_period, + field_name: url ? "entropy_configuration[auto_reconsider_period]" : "collection[auto_reconsider_period]" %> <% end %>
diff --git a/app/views/entropy/_knob.html.erb b/app/views/entropy/_knob.html.erb new file mode 100644 index 000000000..ed0a19eca --- /dev/null +++ b/app/views/entropy/_knob.html.erb @@ -0,0 +1,42 @@ +<% + name = label.parameterize + default_index = if period + knob_options.index(period.to_i / 1.day) + else + knob_options.index(Entropy::Configuration.default.auto_close_period.to_i / 1.day) + end +%> + +
+ +
+ <% knob_options.each_with_index do |value, i| %> + + <% end %> + + +
+
+ +
Days
+
\ No newline at end of file