atomic_view 0.4.1 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f37c405116dc18595f21f9f7b3a45223edef67c921c991a7c1523a93b594cf39
4
- data.tar.gz: 3ce7216ee4b6327f57dec679651d507ba799023f4b2eb3b865ef61bfa3781c1a
3
+ metadata.gz: d48b9aeb4fc6bd4b464ad92923bd08c91481cd07eb10784d30071e9fcc02b49f
4
+ data.tar.gz: 6f1d34f46f36a3633641b8a2b9b37aa2f29f314fb79fe59dd1d8406d7338fd5a
5
5
  SHA512:
6
- metadata.gz: a550e6749f5c77da2336b475b69d59d24d09be6e5dfb321bc9c2e73af3968d1b308e0d26962e728f8f0c5dfcd4a341e0d6e63a753ec5986943deba90db4223c7
7
- data.tar.gz: 05477662ea7b03cd022ca121c0c1b30967cdf99feae9166209aabf64f531ef1902b3a5c746e76dca92cc107e2a7caa05705a36f91f2fd9bdb3e941270eb49e07
6
+ metadata.gz: 5dec3e0b86feda6b1a8d3746ad125ff105334def14e54ac091e69d1b15c0b19ddbbcd4ecfb17ed40cb0bae3a5cf4f0b3cc30e534c6bb4e6c5e76a22bb4ec5774
7
+ data.tar.gz: 2f58dc5ba164ba0404ed902a4bef77aac1f7d159bd524eacba657439562122434118de233097e9df60382d3790f610e534c5a4bfc200368d8d379c005e0a413f
@@ -0,0 +1,30 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ // Debounced `requestSubmit()` for a filter form -- wired once on the
4
+ // `<form>` itself (see FiltersComponent), not per-field, since `input`/
5
+ // `change` events from any native form control (a text field, a select, a
6
+ // radio -- including a ChipComponent radio dropped into `with_chip`)
7
+ // bubble up to the form regardless of where they originate.
8
+ export default class extends Controller {
9
+ static values = {delay: {type: Number, default: 150}}
10
+
11
+ initialize() {
12
+ this.submit = this.submit.bind(this)
13
+ }
14
+
15
+ connect() {
16
+ if (this.delayValue > 0) this.submit = this.debounce(this.submit, this.delayValue)
17
+ }
18
+
19
+ submit() {
20
+ this.element.requestSubmit()
21
+ }
22
+
23
+ debounce(fn, delay) {
24
+ let timeout
25
+ return (...args) => {
26
+ clearTimeout(timeout)
27
+ timeout = setTimeout(() => fn.apply(this, args), delay)
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,65 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ // Progressively enhances a real, hidden <select> (see the `searchable`
4
+ // concern in select_component.rb/collection_select_component.rb) into a
5
+ // trigger + search + list combobox. Reads the select's own <option>
6
+ // elements to build the list -- there's no separate Ruby-side data source
7
+ // to duplicate or keep in sync with `selected`/`include_blank`/etc.
8
+ // Picking an item writes back into the real select's value and dispatches
9
+ // a bubbling `change`, so it participates in a real form submission (e.g.
10
+ // FiltersComponent's form-level auto-submit) exactly like a plain select.
11
+ //
12
+ // Paired with `atomic-view--dropdown` on the same element for open/close,
13
+ // positioning, and outside-click/Esc handling -- this controller only
14
+ // owns the list content and the select/label sync.
15
+ export default class extends Controller {
16
+ static targets = ["select", "input", "label", "list"]
17
+
18
+ connect() {
19
+ this.buildList()
20
+ this.syncLabel()
21
+ this.selectTarget.addEventListener("change", this.syncLabel)
22
+ }
23
+
24
+ disconnect() {
25
+ this.selectTarget.removeEventListener("change", this.syncLabel)
26
+ }
27
+
28
+ buildList() {
29
+ this.listTarget.innerHTML = ""
30
+
31
+ Array.from(this.selectTarget.options).forEach((option) => {
32
+ const button = document.createElement("button")
33
+ button.type = "button"
34
+ button.textContent = option.text
35
+ button.dataset.searchText = option.text.toLowerCase()
36
+ button.className = "flex w-full items-center gap-2.5 rounded-well px-2 py-2 text-sm text-foreground hover:bg-offset"
37
+ button.addEventListener("click", () => this.choose(option))
38
+ this.listTarget.appendChild(button)
39
+ })
40
+ }
41
+
42
+ choose(option) {
43
+ this.selectTarget.value = option.value
44
+ this.selectTarget.dispatchEvent(new Event("change", {bubbles: true}))
45
+ this.syncLabel()
46
+ this.dropdownController?.close()
47
+ }
48
+
49
+ filter() {
50
+ const query = this.inputTarget.value.trim().toLowerCase()
51
+
52
+ this.listTarget.querySelectorAll("button").forEach((button) => {
53
+ button.classList.toggle("hidden", !button.dataset.searchText.includes(query))
54
+ })
55
+ }
56
+
57
+ syncLabel = () => {
58
+ const selected = this.selectTarget.options[this.selectTarget.selectedIndex]
59
+ this.labelTarget.textContent = selected ? selected.text : ""
60
+ }
61
+
62
+ get dropdownController() {
63
+ return this.application.getControllerForElementAndIdentifier(this.element, "atomic-view--dropdown")
64
+ }
65
+ }
@@ -10,27 +10,96 @@ module AtomicView
10
10
  # Pass `dismiss_button_options` only to customize that behavior, e.g. to
11
11
  # trigger a network request instead of (or in addition to) the default
12
12
  # remove animation.
13
+ #
14
+ # A chip can also be *selectable* rather than dismissible -- for a row
15
+ # of filter options (e.g. status: All/Active/Completed) rendered as
16
+ # individual pills rather than a joined SegmentedControlComponent
17
+ # track. Two selectable modes, matching SegmentedControlComponent's own
18
+ # link/radio split:
19
+ #
20
+ # - `href:` renders an `<a>` -- plain navigation, for a page not inside
21
+ # an auto-submitting form. `selected:` drives the active styling.
22
+ # - `name:` (+ `value:`, `selected:`) renders a real
23
+ # `<input type="radio"> + <label>` pair, styled identically, so it
24
+ # participates in a real form submission -- e.g. dropped straight
25
+ # into FiltersComponent's `with_chip`, where the radio's native
26
+ # `change` event bubbles to the form's auto-submit action with no
27
+ # extra wiring.
28
+ #
29
+ # `dismissible:` only applies to the plain (non-selectable) mode -- a
30
+ # filter pill is "unselected" by picking a different one, not by its
31
+ # own close button.
32
+ #
33
+ # The selected treatment is additive (`ring-2 ring-current`), not a
34
+ # bg/text override -- a caller coloring different values differently
35
+ # (e.g. a status chip carrying `class: "text-success"`) keeps that
36
+ # color when selected, with `ring-current` picking it up automatically
37
+ # for the emphasis ring, rather than every selected chip flattening to
38
+ # the same neutral look regardless of its own color.
13
39
  class ChipComponent < AtomicView::Component
14
- attr_reader :dismissible, :dismiss_button_options, :leading
40
+ attr_reader :dismissible, :dismiss_button_options, :leading, :href, :name, :value, :selected
15
41
 
16
- def initialize(dismissible: false, dismiss_button_options: {}, leading: nil, **options)
42
+ def initialize(dismissible: false, dismiss_button_options: {}, leading: nil, href: nil, name: nil, value: nil, selected: false, **options)
17
43
  super()
18
44
  @dismissible = dismissible
19
45
  @dismiss_button_options = dismiss_button_options
20
46
  @leading = leading
47
+ @href = href
48
+ @name = name
49
+ @value = value
50
+ @selected = selected
21
51
  @options = options
22
52
  end
23
53
 
24
54
  def call
55
+ return radio_chip if name.present?
56
+ return link_chip if href.present?
57
+
58
+ span_chip
59
+ end
60
+
61
+ private
62
+
63
+ def span_chip
25
64
  tag.span(**@options.except(:class, :data), class: class_names(base_classes, @options[:class]), data: data_attributes) do
26
65
  safe_join([leading, content, dismiss_button].compact)
27
66
  end
28
67
  end
29
68
 
30
- private
69
+ def link_chip
70
+ link_to(href, **@options.except(:class), class: class_names(base_classes, selected_classes, @options[:class]), aria: {current: (selected ? "true" : nil)}) do
71
+ safe_join([leading, content].compact)
72
+ end
73
+ end
74
+
75
+ # Wrapped in a `display: contents` span -- Tailwind's `peer-checked:`
76
+ # relies on the CSS general sibling combinator (`~`), which matches
77
+ # *every* later sibling under the same parent once a peer is
78
+ # checked, not just the one meant to pair with it. Rendered as flat
79
+ # siblings (as `with_chip`'s multiple chips are), checking any one
80
+ # radio would light up every chip after it. This span isolates each
81
+ # input/label pair's sibling relationship to just the two of them --
82
+ # `display: contents` keeps it invisible to layout, same technique
83
+ # SegmentedControlComponent's own radio mode already uses.
84
+ def radio_chip
85
+ tag.span(class: "contents") do
86
+ safe_join([
87
+ tag.input(**@options.except(:class, :id), type: "radio", name: name, value: value, id: radio_id, checked: selected, class: "peer sr-only"),
88
+ tag.label(safe_join([leading, content].compact), for: radio_id, class: class_names(base_classes, "cursor-pointer peer-checked:ring-2 peer-checked:ring-current peer-checked:font-semibold", @options[:class]))
89
+ ])
90
+ end
91
+ end
92
+
93
+ def radio_id
94
+ "#{name}_#{value}"
95
+ end
96
+
97
+ def selected_classes
98
+ "ring-2 ring-current font-semibold" if selected
99
+ end
31
100
 
32
101
  def base_classes
33
- "inline-flex items-center gap-1 rounded-pill bg-offset px-2 py-0.5 text-xs font-medium text-foreground border border-border"
102
+ "inline-flex items-center gap-1 rounded-btn bg-offset px-2 py-0.5 text-xs font-medium text-foreground border border-border"
34
103
  end
35
104
 
36
105
  def data_attributes
@@ -1,4 +1,4 @@
1
- <%= content_tag :div, class: container_html_class do %>
1
+ <%= content_tag :div, class: container_html_class, data: searchable_container_data do %>
2
2
  <% if left_section? %>
3
3
  <% if left_section_addon? %>
4
4
  <span class="inline-flex items-center rounded-l-btn border border-r-0 border-ring/10 dark:border-white/10 bg-transparent dark:bg-white/5 px-3 text-primary dark:text-white sm:text-sm"><%= left_section %></span>
@@ -11,6 +11,20 @@
11
11
  <% end %>
12
12
  <% end %>
13
13
  <%= select_tag %>
14
+ <% if searchable? %>
15
+ <button type="button" data-atomic-view--dropdown-target="trigger" data-action="click->atomic-view--dropdown#toggle" class="<%= field_chrome_classes(size: :sm) %> flex items-center justify-between gap-2 text-left">
16
+ <span data-atomic-view--searchable-select-target="label" class="truncate"></span>
17
+ <%= icon "chevron-down", variant: :micro, options: {class: "text-muted-foreground shrink-0"} %>
18
+ </button>
19
+
20
+ <div data-atomic-view--dropdown-target="menu" class="hidden absolute z-50 w-full bg-surface border border-border rounded-card shadow-panel p-1">
21
+ <div class="relative px-1 pb-1.5 pt-1">
22
+ <%= icon "magnifying-glass", variant: :micro, options: {class: "pointer-events-none absolute left-3.5 top-1/2 -translate-y-1/2 text-muted-foreground"} %>
23
+ <input type="search" autocomplete="off" data-atomic-view--searchable-select-target="input" data-action="input->atomic-view--searchable-select#filter" class="w-full rounded-well border border-input bg-surface py-1.5 pl-8 pr-2 text-sm text-foreground placeholder:text-placeholder focus:border-ring focus:outline-none">
24
+ </div>
25
+ <div data-atomic-view--searchable-select-target="list" class="max-h-64 overflow-y-auto"></div>
26
+ </div>
27
+ <% end %>
14
28
  <% if right_section? %>
15
29
  <% if right_section_addon? %>
16
30
  <span class="inline-flex items-center rounded-r-btn border border-l-0 border-ring/10 dark:border-white/10 bg-transparent dark:bg-white/5 px-3 text-primary dark:text-white sm:text-sm"><%= right_section %></span>
@@ -3,6 +3,7 @@ module AtomicView
3
3
  class CollectionSelectComponent < ViewComponent::Form::CollectionSelectComponent
4
4
  include AtomicView::Components::Concerns::FieldChrome
5
5
  include AtomicView::Components::Concerns::SectionSupport
6
+ include AtomicView::Components::Concerns::Searchable
6
7
 
7
8
  def html_class
8
9
  class_names(
@@ -10,7 +11,8 @@ module AtomicView
10
11
  "pl-10" => left_section? && !(left_section_addon? || left_section_interaction?),
11
12
  "pr-10" => right_section? && !(right_section_addon? || right_section_interaction?),
12
13
  "shadow-none rounded-none rounded-r-btn ring-inset" => left_section_addon? || left_section_interaction?,
13
- "shadow-none rounded-none rounded-l-btn ring-inset" => right_section_addon? || right_section_interaction?
14
+ "shadow-none rounded-none rounded-l-btn ring-inset" => right_section_addon? || right_section_interaction?,
15
+ "sr-only" => searchable?
14
16
  )
15
17
  end
16
18
 
@@ -23,7 +25,7 @@ module AtomicView
23
25
  value_method,
24
26
  text_method,
25
27
  options,
26
- html_options
28
+ searchable_html_options
27
29
  ).render
28
30
  end
29
31
  end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AtomicView
4
+ module Components
5
+ module Concerns
6
+ # Shared by SelectComponent/CollectionSelectComponent: `searchable:
7
+ # true` progressively enhances the real, native `<select>` (kept in
8
+ # the DOM -- visually hidden, but still the actual form value) with a
9
+ # trigger + search + list combobox on top, wired to
10
+ # `atomic-view--searchable-select`. That controller reads the
11
+ # select's own `<option>` elements to build its list -- there's no
12
+ # separate Ruby-side data source to keep in sync -- and writes back
13
+ # into the select's value (dispatching `change`) when an option is
14
+ # picked, so it participates in a real form submission exactly like
15
+ # a plain select (e.g. FiltersComponent's form-level auto-submit)
16
+ # with no special-casing.
17
+ module Searchable
18
+ def searchable?
19
+ options[:searchable].present?
20
+ end
21
+
22
+ # `html_options` (the real <select>'s raw HTML attributes, kept
23
+ # separate from `options` by ViewComponent::Form::SelectComponent)
24
+ # needs the target attribute merged in only when searchable --
25
+ # every select_tag override should render with this instead of
26
+ # the raw `html_options`.
27
+ def searchable_html_options
28
+ return html_options unless searchable?
29
+
30
+ html_options.merge(data: (html_options[:data] || {}).merge("atomic-view--searchable-select-target" => "select"))
31
+ end
32
+
33
+ # The searchable UI's controllers need to live on the same
34
+ # container the real <select> is already inside of (so it's a
35
+ # valid Stimulus target scope) -- that's `container_html_class`'s
36
+ # div (from SectionSupport), already `position: relative` for
37
+ # icon-section positioning, reused here as the dropdown's anchor
38
+ # too rather than introducing another wrapping element.
39
+ def searchable_container_data
40
+ return {} unless searchable?
41
+
42
+ {controller: "atomic-view--dropdown atomic-view--searchable-select"}
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,23 @@
1
+ <%= form_with(url: url, method: method, builder: AtomicView::FormBuilder, **html_options, class: html_class, data: data_attributes) do |form| %>
2
+ <% assign_form(form) %>
3
+
4
+ <% preserve.each do |key, value| %>
5
+ <%= hidden_field_tag(key, value) %>
6
+ <% end %>
7
+
8
+ <% chips.each do |chip| %>
9
+ <div class="flex flex-wrap items-center gap-2"><%= chip %></div>
10
+ <% end %>
11
+
12
+ <% if row? %>
13
+ <div class="flex flex-col gap-2 sm:flex-row items-stretch w-full">
14
+ <% if search? %>
15
+ <div class="sm:flex-[2]"><%= search %></div>
16
+ <% end %>
17
+
18
+ <% filters.each do |filter| %>
19
+ <div class="sm:flex-1"><%= filter %></div>
20
+ <% end %>
21
+ </div>
22
+ <% end %>
23
+ <% end %>
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AtomicView
4
+ module Components
5
+ # Filters
6
+ #
7
+ # The layout/boilerplate for an index page's filter bar: one
8
+ # auto-submitting `form_with`, three fixed slots (`with_chip`,
9
+ # `with_search`, `with_filter`), each always rendered in the same spot
10
+ # rather than a freeform block callers arrange by hand.
11
+ #
12
+ # <%= render(AtomicView::Components::FiltersComponent.new(url: rentals_path(format: :turbo_stream), preserve: {view: params[:view]})) do |filters| %>
13
+ # <% filters.with_chip do %>
14
+ # <% status_options.each do |status| %>
15
+ # <%= render(AtomicView::Components::ChipComponent.new(name: "status", value: status.value, selected: params[:status] == status.value)) { status.label } %>
16
+ # <% end %>
17
+ # <% end %>
18
+ #
19
+ # <% filters.with_search do |form| %>
20
+ # <%= form.search_field :q, value: params[:q], placeholder: "Search by camper..." %>
21
+ # <% end %>
22
+ #
23
+ # <% filters.with_filter do |form| %>
24
+ # <%= form.collection_select(:park_id, Park.all, :id, :name, include_blank: "All parks", selected: params[:park_id]) %>
25
+ # <% end %>
26
+ # <% end %>
27
+ #
28
+ # - `with_chip` (repeatable) -- content-only, one full-width row per
29
+ # call, above everything else, in declaration order. Each call
30
+ # represents one logical group of options (e.g. "status") -- the
31
+ # caller loops and renders one selectable `ChipComponent` per option
32
+ # inside it; a second, independent chip group is just a second
33
+ # `with_chip` call.
34
+ # - `with_search` (single) -- yields the shared form builder; fixed
35
+ # spot at the start of the second row, given more width than each
36
+ # `with_filter`.
37
+ # - `with_filter` (repeatable) -- yields the same form builder; renders
38
+ # after search, sharing the remaining width equally.
39
+ #
40
+ # The yielded "form" in `with_search`/`with_filter` isn't literally the
41
+ # `ActionView::Helpers::FormBuilder` -- it's a thin slot wrapper that
42
+ # delegates every method to the real form builder once FiltersComponent
43
+ # builds it (`form_with` doesn't exist yet when these blocks are
44
+ # captured), so `form.search_field`/`form.select`/`form.collection_select`
45
+ # all work exactly as if it were the real builder.
46
+ #
47
+ # Auto-submit is wired once on the `<form>` itself (`atomic-view--auto-submit`,
48
+ # listening for bubbled `input`/`change`), not per-field -- any native
49
+ # form control dropped into any slot, including a `ChipComponent` radio
50
+ # in `with_chip`, submits on change with no extra wiring.
51
+ #
52
+ # `preserve:` renders one hidden field per key/value, for page state
53
+ # that isn't itself a filter but must ride along on every submit (e.g.
54
+ # a `view:` toggle owned by a different control entirely).
55
+ class FiltersComponent < AtomicView::Component
56
+ # A stand-in for the real form builder, yielded into `with_search`/
57
+ # `with_filter` blocks before the real builder exists. FiltersComponent
58
+ # assigns the real builder onto every instance of this once `form_with`
59
+ # actually yields it -- see `#assign_form`.
60
+ class FieldSlot < AtomicView::Component
61
+ attr_accessor :form
62
+
63
+ def call
64
+ content
65
+ end
66
+
67
+ def method_missing(name, ...)
68
+ return form.public_send(name, ...) if form.respond_to?(name)
69
+ super
70
+ end
71
+
72
+ def respond_to_missing?(name, include_private = false)
73
+ form.respond_to?(name) || super
74
+ end
75
+
76
+ # ViewComponent::Base already includes ActionView's generic,
77
+ # object-argument Form(Options)Helper methods for template
78
+ # rendering -- e.g. a bare `select(object, method, choices, ...)`,
79
+ # not FormBuilder's per-attribute `form.select(method, choices,
80
+ # ...)`. Those real, inherited methods share names with the form
81
+ # builder's and would otherwise win over method_missing entirely,
82
+ # getting called with the wrong arity/meaning. Undefining every
83
+ # name the real form builder responds to forces all of them
84
+ # through method_missing above instead.
85
+ (AtomicView::FormBuilder.instance_methods - Object.instance_methods).each do |name|
86
+ undef_method(name) if method_defined?(name)
87
+ end
88
+ end
89
+
90
+ renders_many :chips
91
+ renders_one :search, FieldSlot
92
+ renders_many :filters, FieldSlot
93
+
94
+ attr_reader :url, :method, :preserve
95
+
96
+ def initialize(url:, method: :get, preserve: {}, **options)
97
+ super()
98
+ @url = url
99
+ @method = method
100
+ @preserve = preserve
101
+ @options = options
102
+ end
103
+
104
+ def html_class
105
+ class_names("flex flex-col gap-3 w-full", @options[:class])
106
+ end
107
+
108
+ def html_options
109
+ @options.except(:class, :data)
110
+ end
111
+
112
+ def data_attributes
113
+ (@options[:data] || {}).merge(
114
+ controller: "atomic-view--auto-submit",
115
+ action: "input->atomic-view--auto-submit#submit change->atomic-view--auto-submit#submit"
116
+ )
117
+ end
118
+
119
+ # Called from the template once `form_with` actually yields the real
120
+ # builder -- pushes it onto every FieldSlot instance so `with_search`/
121
+ # `with_filter`'s already-captured blocks can delegate to it.
122
+ def assign_form(form)
123
+ search.form = form if search?
124
+ filters.each { |filter| filter.form = form }
125
+ end
126
+
127
+ def row?
128
+ search? || filters.any?
129
+ end
130
+ end
131
+ end
132
+ end
@@ -1,4 +1,4 @@
1
- <%= content_tag :div, class: container_html_class do %>
1
+ <%= content_tag :div, class: container_html_class, data: searchable_container_data do %>
2
2
  <% if left_section? %>
3
3
  <% if left_section_addon? %>
4
4
  <span class="inline-flex items-center rounded-l-btn border border-r-0 border-ring/10 dark:border-white/10 bg-transparent dark:bg-white/5 px-3 text-primary dark:text-white sm:text-sm"><%= left_section %></span>
@@ -11,6 +11,20 @@
11
11
  <% end %>
12
12
  <% end %>
13
13
  <%= select_tag %>
14
+ <% if searchable? %>
15
+ <button type="button" data-atomic-view--dropdown-target="trigger" data-action="click->atomic-view--dropdown#toggle" class="<%= field_chrome_classes(size: :sm) %> flex items-center justify-between gap-2 text-left">
16
+ <span data-atomic-view--searchable-select-target="label" class="truncate"></span>
17
+ <%= icon "chevron-down", variant: :micro, options: {class: "text-muted-foreground shrink-0"} %>
18
+ </button>
19
+
20
+ <div data-atomic-view--dropdown-target="menu" class="hidden absolute z-50 w-full bg-surface border border-border rounded-card shadow-panel p-1">
21
+ <div class="relative px-1 pb-1.5 pt-1">
22
+ <%= icon "magnifying-glass", variant: :micro, options: {class: "pointer-events-none absolute left-3.5 top-1/2 -translate-y-1/2 text-muted-foreground"} %>
23
+ <input type="search" autocomplete="off" data-atomic-view--searchable-select-target="input" data-action="input->atomic-view--searchable-select#filter" class="w-full rounded-well border border-input bg-surface py-1.5 pl-8 pr-2 text-sm text-foreground placeholder:text-placeholder focus:border-ring focus:outline-none">
24
+ </div>
25
+ <div data-atomic-view--searchable-select-target="list" class="max-h-64 overflow-y-auto"></div>
26
+ </div>
27
+ <% end %>
14
28
  <% if right_section? %>
15
29
  <% if right_section_addon? %>
16
30
  <span class="inline-flex items-center rounded-r-btn border border-l-0 border-ring/10 dark:border-white/10 bg-transparent dark:bg-white/5 px-3 text-primary dark:text-white sm:text-sm"><%= right_section %></span>
@@ -3,6 +3,7 @@ module AtomicView
3
3
  class SelectComponent < ViewComponent::Form::SelectComponent
4
4
  include AtomicView::Components::Concerns::FieldChrome
5
5
  include AtomicView::Components::Concerns::SectionSupport
6
+ include AtomicView::Components::Concerns::Searchable
6
7
 
7
8
  def html_class
8
9
  class_names(
@@ -11,7 +12,8 @@ module AtomicView
11
12
  "pl-10" => left_section? && !(left_section_addon? || left_section_interaction?),
12
13
  "pr-10" => right_section? && !(right_section_addon? || right_section_interaction?),
13
14
  "shadow-none rounded-none rounded-r-btn ring-inset" => left_section_addon? || left_section_interaction?,
14
- "shadow-none rounded-none rounded-l-btn ring-inset" => right_section_addon? || right_section_interaction?
15
+ "shadow-none rounded-none rounded-l-btn ring-inset" => right_section_addon? || right_section_interaction?,
16
+ "sr-only" => searchable?
15
17
  )
16
18
  end
17
19
 
@@ -22,7 +24,7 @@ module AtomicView
22
24
  @view_context,
23
25
  choices,
24
26
  options,
25
- html_options,
27
+ searchable_html_options,
26
28
  &content
27
29
  ).render
28
30
  end
@@ -62,8 +62,13 @@ module AtomicView
62
62
  "-mb-px border-b-2 pb-3 text-sm font-semibold"
63
63
  end
64
64
 
65
+ # Neutral foreground text (not `text-accent`) -- matches
66
+ # SegmentedControlComponent's active segment, which raises the
67
+ # selected option in `text-foreground` rather than an accent color.
68
+ # The accent only shows up in the underline, as the one colored
69
+ # signal of which tab is active.
65
70
  def active_classes
66
- "border-accent text-accent"
71
+ "border-accent text-foreground"
67
72
  end
68
73
 
69
74
  def inactive_classes
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AtomicView
4
- VERSION = "0.4.1"
4
+ VERSION = "0.5.1"
5
5
  end
data/lib/atomic_view.rb CHANGED
@@ -4,6 +4,7 @@ require 'atomic_view/version'
4
4
  require 'atomic_view/engine'
5
5
  require 'atomic_view/configuration'
6
6
 
7
+ require "ostruct"
7
8
  require "tailwind_merge"
8
9
  require "heroicons"
9
10
  require "local_time"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: atomic_view
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joel Warrington
@@ -140,12 +140,14 @@ files:
140
140
  - README.md
141
141
  - Rakefile
142
142
  - app/assets/config/atomic_view_manifest.js
143
+ - app/assets/javascripts/atomic_view/controllers/auto_submit_controller.js
143
144
  - app/assets/javascripts/atomic_view/controllers/chip_controller.js
144
145
  - app/assets/javascripts/atomic_view/controllers/command_palette_controller.js
145
146
  - app/assets/javascripts/atomic_view/controllers/dropdown_controller.js
146
147
  - app/assets/javascripts/atomic_view/controllers/gantt_controller.js
147
148
  - app/assets/javascripts/atomic_view/controllers/hotkey_controller.js
148
149
  - app/assets/javascripts/atomic_view/controllers/modal_controller.js
150
+ - app/assets/javascripts/atomic_view/controllers/searchable_select_controller.js
149
151
  - app/assets/javascripts/atomic_view/controllers/theme_toggle_controller.js
150
152
  - app/assets/javascripts/atomic_view/controllers/toast_controller.js
151
153
  - app/assets/javascripts/atomic_view/controllers/tooltip_controller.js
@@ -192,6 +194,7 @@ files:
192
194
  - lib/atomic_view/components/command_palette_component/section_component.rb
193
195
  - lib/atomic_view/components/concerns/button_variants.rb
194
196
  - lib/atomic_view/components/concerns/field_chrome.rb
197
+ - lib/atomic_view/components/concerns/searchable.rb
195
198
  - lib/atomic_view/components/concerns/section_support.rb
196
199
  - lib/atomic_view/components/date_field_component.rb
197
200
  - lib/atomic_view/components/date_select_component.rb
@@ -204,6 +207,8 @@ files:
204
207
  - lib/atomic_view/components/empty_state_component.rb
205
208
  - lib/atomic_view/components/field_component.html.erb
206
209
  - lib/atomic_view/components/field_component.rb
210
+ - lib/atomic_view/components/filters_component.html.erb
211
+ - lib/atomic_view/components/filters_component.rb
207
212
  - lib/atomic_view/components/gantt_component.html.erb
208
213
  - lib/atomic_view/components/gantt_component.rb
209
214
  - lib/atomic_view/components/gantt_component/date_header_component.html.erb