keystone_ui 0.52.0 → 0.53.0

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: 0f67e6bd82fbcc8daa1e455f60140395bb002aee59ba2f055191130827707b0a
4
- data.tar.gz: 6335b10c5578ce27579cab7e1da5a2f05e4eeca7c99d5e92173d7278a25efd9c
3
+ metadata.gz: e7f6b6d63755b597d7b876ddba35bd4f6bbdb6039b0b16be57197cf4af01ea48
4
+ data.tar.gz: c2093a32f35bf5beb3a469cfffe14033e491f8e6435bb32003b203652574f990
5
5
  SHA512:
6
- metadata.gz: b49bb408da1f06272f711cbe7f077f8b7b438f89ae13c91d18309e3afda1d81689667ac45ff6bccb5fb94fae2abc02457caae6fc6890278635b2aae641b872ef
7
- data.tar.gz: 2e7fd2e99b1368b703f3f29ee435ad4a2695856fd2301e182d5022d84ea29122d448712c940f77e9a0ab43310d436c37e80c4298001536d137a2b68cbd24db7c
6
+ metadata.gz: ac7541139dd31e4269cbca2dc13e3201d6c44ffd9b4a8088f90a2361f15cee3e627d808277b31e7a5981c0c19d56fa453cb186350870cf48d5416d7ef0ab6a2b
7
+ data.tar.gz: bf3cb63e70d990d5b01bee266a2446d15cd8f7b710b63c767e129134312d9f6e999d6e56ef6d709614ff4a63699b303e231cbebed874001ce4c03f7c46756401
@@ -1,5 +1,6 @@
1
1
  import ColorPickerController from "keystone_ui/color_picker_controller"
2
2
  import MultiSelectController from "keystone_ui/multi_select_controller"
3
+ import SearchSelectController from "keystone_ui/search_select_controller"
3
4
  import SwipeDeckController from "keystone_ui/swipe_deck_controller"
4
5
  import ColumnPickerController from "keystone_ui/column_picker_controller"
5
6
  import FileUploadController from "keystone_ui/file_upload_controller"
@@ -20,6 +21,7 @@ import { keepPageThemeInStep } from "keystone_ui/page_theme"
20
21
  export function registerControllers(application) {
21
22
  application.register("color-picker", ColorPickerController)
22
23
  application.register("multi-select", MultiSelectController)
24
+ application.register("search-select", SearchSelectController)
23
25
  application.register("swipe-deck", SwipeDeckController)
24
26
  application.register("column-picker", ColumnPickerController)
25
27
  application.register("file-upload", FileUploadController)
@@ -0,0 +1,69 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ const escaped = (text) => String(text).replace(/[&<>"']/g, (character) => `&#${character.charCodeAt(0)};`)
4
+
5
+ export default class extends Controller {
6
+ static targets = ["id", "input", "list", "newFields"]
7
+ static values = { url: String, newLabel: String }
8
+
9
+ async search() {
10
+ const response = await fetch(`${this.urlValue}?q=${encodeURIComponent(this.inputTarget.value)}`, { headers: { Accept: "application/json" } })
11
+ this.matches = await response.json()
12
+ this.highlighted = -1
13
+ this.draw()
14
+ this.listTarget.classList.remove("hidden")
15
+ }
16
+
17
+ key(event) {
18
+ if (event.key === "Enter") return this.pickHighlighted(event)
19
+ if (event.key === "Escape") return this.listTarget.classList.add("hidden")
20
+
21
+ const moves = { ArrowDown: 1, ArrowUp: -1 }
22
+ if (!(event.key in moves) || !this.matches) return
23
+
24
+ event.preventDefault()
25
+ this.highlighted = Math.min(Math.max(this.highlighted + moves[event.key], 0), this.choiceCount() - 1)
26
+ this.draw()
27
+ }
28
+
29
+ pickHighlighted(event) {
30
+ if (this.highlighted === undefined || this.highlighted < 0) return
31
+
32
+ const match = this.matches[this.highlighted]
33
+ if (match) this.choose({ params: match, preventDefault: () => event.preventDefault() })
34
+ else this.chooseNew(event)
35
+ }
36
+
37
+ draw() {
38
+ this.listTarget.innerHTML = this.matches.map((match, index) => this.option(match, index === this.highlighted)).join("") + this.newOption(this.highlighted === this.matches.length)
39
+ }
40
+
41
+ choiceCount() {
42
+ return this.matches.length + (this.newLabelValue ? 1 : 0)
43
+ }
44
+
45
+ choose(event) {
46
+ event.preventDefault()
47
+ this.idTarget.value = String(event.params.id)
48
+ this.inputTarget.value = event.params.label
49
+ if (this.hasNewFieldsTarget) this.newFieldsTarget.classList.add("hidden")
50
+ this.listTarget.classList.add("hidden")
51
+ }
52
+
53
+ chooseNew(event) {
54
+ event.preventDefault()
55
+ this.idTarget.value = ""
56
+ if (this.hasNewFieldsTarget) this.newFieldsTarget.classList.remove("hidden")
57
+ this.listTarget.classList.add("hidden")
58
+ }
59
+
60
+ newOption(highlighted = false) {
61
+ if (!this.newLabelValue) return ""
62
+
63
+ return `<li role="option" class="ks-menu-option" data-action="mousedown->search-select#chooseNew" aria-selected="${highlighted}">${escaped(this.newLabelValue)}</li>`
64
+ }
65
+
66
+ option(match, highlighted = false) {
67
+ return `<li role="option" class="ks-menu-option" data-action="mousedown->search-select#choose" data-search-select-id-param="${escaped(match.id)}" data-search-select-label-param="${escaped(match.label)}" aria-selected="${highlighted}">${escaped(match.label)}</li>`
68
+ }
69
+ }
@@ -0,0 +1,9 @@
1
+ <div class="<%= WRAPPER_CLASSES %>" data-controller="search-select" data-search-select-url-value="<%= url %>" data-search-select-new-label-value="<%= new_label %>">
2
+ <label class="<%= LABEL_CLASSES %>"><%= label %></label>
3
+ <input type="hidden" name="<%= name %>" value="<%= selected_id %>" data-search-select-target="id">
4
+ <input type="text" autocomplete="off" value="<%= selected_label %>" class="<%= Keystone::Ui::InputComponent::BASE_CLASSES %>" data-search-select-target="input" data-action="input->search-select#search keydown->search-select#key">
5
+ <ul role="listbox" class="<%= LIST_CLASSES %>" data-search-select-target="list"></ul>
6
+ <% if content? %>
7
+ <div class="hidden" data-search-select-target="newFields"><%= content %></div>
8
+ <% end %>
9
+ </div>
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Keystone
4
+ module Ui
5
+ class SearchSelectComponent < ViewComponent::Base
6
+ WRAPPER_CLASSES = "ks-form-field relative"
7
+ LABEL_CLASSES = "ks-label"
8
+ LIST_CLASSES = "ks-menu absolute z-10 w-full hidden"
9
+
10
+ attr_reader :name, :label, :url, :selected_id, :selected_label, :new_label
11
+
12
+ def initialize(name:, label:, url:, selected_id: nil, selected_label: nil, new_label: nil)
13
+ @name = name
14
+ @label = label
15
+ @url = url
16
+ @selected_id = selected_id
17
+ @selected_label = selected_label
18
+ @new_label = new_label
19
+ end
20
+ end
21
+ end
22
+ end
@@ -193,6 +193,10 @@ module KeystoneUiHelper
193
193
  render Keystone::Ui::MultiSelectComponent.new(**args)
194
194
  end
195
195
 
196
+ def ui_search_select(**args, &block)
197
+ render Keystone::Ui::SearchSelectComponent.new(**args), &block
198
+ end
199
+
196
200
  def ui_swipe_deck(**args, &block)
197
201
  render Keystone::Ui::SwipeDeckComponent.new(**args), &block
198
202
  end
data/config/importmap.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  pin "keystone_ui/index", to: "keystone_ui/index.js"
2
2
  pin "keystone_ui/color_picker_controller", to: "keystone_ui/color_picker_controller.js"
3
3
  pin "keystone_ui/multi_select_controller", to: "keystone_ui/multi_select_controller.js"
4
+ pin "keystone_ui/search_select_controller", to: "keystone_ui/search_select_controller.js"
4
5
  pin "keystone_ui/swipe_deck_controller", to: "keystone_ui/swipe_deck_controller.js"
5
6
  pin "keystone_ui/column_picker_controller", to: "keystone_ui/column_picker_controller.js"
6
7
  pin "keystone_ui/file_upload_controller", to: "keystone_ui/file_upload_controller.js"
@@ -51,6 +51,7 @@ module Keystone
51
51
  Keystone::Ui::ShowPageComponent,
52
52
  Keystone::Ui::OptionCardComponent,
53
53
  Keystone::Ui::RadioCardComponent,
54
+ Keystone::Ui::SearchSelectComponent,
54
55
  Keystone::Ui::CheckboxRowComponent,
55
56
  Keystone::Ui::ProgressComponent,
56
57
  Keystone::Ui::ColorPickerComponent,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module KeystoneUi
4
- VERSION = "0.52.0"
4
+ VERSION = "0.53.0"
5
5
  end
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: keystone_ui-develop
3
- description: Use PROACTIVELY for building or editing screens in a Rails app that has keystone_ui — page shells with Back links and breadcrumbs, forms and fields, data tables with saved columns, desktop navigation, sidebars and bottom tabs, action menus for a record's Edit and Delete, dashboards with stat cards, charts, funnels and goal buckets, amounts shown in green or red as a gain or loss, info buttons and breakdowns, marketing sections, a light/dark theme switch — MUST BE USED instead of hand-writing ERB and Tailwind for UI.
3
+ description: Use PROACTIVELY for building or editing screens in a Rails app that has keystone_ui — page shells with Back links and breadcrumbs, forms and fields, a search field that picks a record or enters a new one, data tables with saved columns, desktop navigation, sidebars and bottom tabs, action menus for a record's Edit and Delete, dashboards with stat cards, charts, funnels and goal buckets, amounts shown in green or red as a gain or loss, info buttons and breakdowns, marketing sections, a light/dark theme switch — MUST BE USED instead of hand-writing ERB and Tailwind for UI.
4
4
  tools: Bash, Read, Write, Edit, Grep
5
5
  scope: UI — pages, forms, tables, navigation, dashboards
6
6
  ---
@@ -52,6 +52,7 @@ not available in the app yet, that is `keystone_ui-install`'s job.
52
52
  - `ui_textarea` — a bare styled textarea with no label.
53
53
  - `ui_select` — a bare styled select with no label.
54
54
  - `ui_multi_select` — a dropdown of checkboxes posting every checked value.
55
+ - `ui_search_select` — a form field that searches the app as a person types and submits the id of the record they pick.
55
56
  - `ui_file_upload` — a file drop zone with selected-file feedback.
56
57
  - `ui_color_picker` — a colour swatch that writes a hex value into a hidden input.
57
58
  - `ui_radio_card` — a selectable card backed by a radio input.
@@ -302,6 +303,31 @@ two exceptions: `ui_page`'s `padding:` treats any value other than `:none` as
302
303
  `"status[]"`. `options:` is `[[label, value], ...]` and `selected:` is the
303
304
  values to check. Its button reads "All <label>" with nothing checked and
304
305
  "N selected" otherwise.
306
+ - `ui_search_select(name:, label:, url:, selected_id: nil, selected_label: nil, new_label: nil)`
307
+ is one field inside a `ui_form`, for picking one record from more than a
308
+ select can list. Each keystroke sends `GET url?q=<typed text>` with
309
+ `Accept: application/json`, and the app answers with a JSON array of
310
+ `{ "id": ..., "label": ... }` objects in the order to list them. Labels
311
+ are shown as plain text. Picking a match by click, or by the arrow keys
312
+ and Enter, puts its id in a hidden input named `name` and its label in
313
+ the field. Escape closes the list.
314
+ - The app owns the search endpoint: the route, which records it searches,
315
+ how it matches the typed text, how many matches it returns, and which
316
+ records the current user may see. Ask the developer for each, and scope
317
+ the query to the current account.
318
+ - `selected_id:` and `selected_label:` open the field with a record already
319
+ chosen, such as on an edit screen, so the form submits that id unchanged
320
+ when nothing else is picked.
321
+ - Typing without picking leaves the hidden id as it was, so the form submits
322
+ the last picked id, or `selected_id:`, whatever the field shows.
323
+ - `new_label:` adds a last choice, such as "Enter a new address", shown in
324
+ the list after each search. Picking it clears the hidden id and shows the
325
+ block passed to the helper, which holds the fields for a new record such
326
+ as `ui_form_field` calls. Picking a match afterwards hides them again.
327
+ - The block's fields are submitted whether shown or hidden, so the app's
328
+ controller treats a blank id as "create from the new fields" and a present
329
+ id as "use that record and ignore the new fields". Ask the developer
330
+ whether a person may enter a new record, and what `new_label:` says.
305
331
  - `ui_file_upload(name:, label: nil, accept: nil, multiple: false, hint: nil)`
306
332
  needs the enclosing form to be multipart.
307
333
  - `ui_color_picker(name:, value: "#000000", label: nil)` writes the chosen
@@ -516,7 +542,8 @@ two exceptions: `ui_page`'s `padding:` treats any value other than `:none` as
516
542
  and rejecting sends `swipe-deck:skip`. Both carry `detail.itemId` (the
517
543
  item's `id`, or its position) and `detail.card`, and `complete` carries
518
544
  `detail.value` from an input marked `data-swipe-deck-value`, or `null`.
519
- - For these, the pipeline's URLs and the column picker's save URL, do not
545
+ - For these, the pipeline's URLs, the column picker's save URL and the
546
+ search select's search URL, do not
520
547
  invent routes or how the outcome is stored. Ask the developer, then
521
548
  build what they pick.
522
549
 
@@ -12,7 +12,7 @@ nothing and gives no steps.
12
12
 
13
13
  keystone_ui is a Rails engine gem that supplies a host app's UI as view helpers
14
14
  built on ViewComponent. Screens are built from named pieces — page shells,
15
- sections, panels, grids, forms and fields, data tables, navigation, cards, stat
15
+ sections, panels, grids, forms and fields, search dropdowns, data tables, navigation, cards, stat
16
16
  tiles, charts, funnels, goal buckets, pipelines, breakdowns and info buttons —
17
17
  instead of hand-written ERB and Tailwind. Every class the UI renders lives in the
18
18
  gem, so two pages built from the same helpers cannot disagree about spacing,
@@ -78,6 +78,14 @@ One decision: is keystone_ui set up in the app?
78
78
  it: which hideable columns to hide and their order. With a save address, a
79
79
  "Columns" menu lets the user change it. A locked first column stays in view
80
80
  while the table scrolls sideways and is never hidden or moved.
81
+ - **Search dropdowns.** A search dropdown picks one record by its id from
82
+ matches the host serves. As the user types, it requests the host's search
83
+ address with the text as `q` and expects JSON back: a list of matches, each
84
+ with an `id` and a `label`. Picking a match submits its id under the field's
85
+ name. When the dropdown offers a choice to enter a new record, picking it
86
+ submits an empty id and shows the new record's fields the page placed inside
87
+ the dropdown, and picking a match afterwards hides them again. The arrow keys
88
+ move through the list, Enter picks, and Escape closes it.
81
89
  - **Action menus.** Each action is a label, an address and an HTTP method. A
82
90
  plain visit is a link, and any other method is a button, so an action that
83
91
  changes data is never a bare link. A delete asks for confirmation by default.
@@ -98,9 +106,9 @@ One decision: is keystone_ui set up in the app?
98
106
  - **Static Tailwind classes.** Class names are never interpolated, so Tailwind's
99
107
  scanner finds them. Sizes that depend on data, such as a progress bar's
100
108
  width, use an inline style. The host needs tailwindcss-rails v4+.
101
- - **Interactivity is Stimulus.** Dropdowns, modals, tab switchers, the column
102
- picker, info buttons, charts, copy buttons and the theme toggle ship as
109
+ - **Interactivity is Stimulus.** Dropdowns, search dropdowns, modals, tab
110
+ switchers, the column picker, info buttons, charts, copy buttons and the theme toggle ship as
103
111
  Stimulus controllers, so a host writes no JavaScript. Components that post,
104
112
  such as the column picker and the pipeline, post to endpoints the host or
105
113
  another gem owns.
106
- - Ruby >= 3.2, ViewComponent >= 2.0 and < 5, keystone_ui-styles >= 0.14.0.
114
+ - Ruby >= 3.2, ViewComponent >= 2.0 and < 5, keystone_ui-styles >= 0.25.0.
@@ -50,7 +50,7 @@ built on ViewComponent; hook it in before building any screen with those helpers
50
50
  ## How to use it
51
51
 
52
52
  1. Confirm the prerequisites: Ruby >= 3.2 and **tailwindcss-rails v4+** in the
53
- host app. The gem brings ViewComponent and keystone_ui-styles 0.14.0 or later with it.
53
+ host app. The gem brings ViewComponent and keystone_ui-styles 0.25.0 or later with it.
54
54
  Tailwind does not have to be initialized first, because the generator creates
55
55
  the stylesheet if it is missing.
56
56
 
@@ -92,7 +92,8 @@ built on ViewComponent; hook it in before building any screen with those helpers
92
92
  - `app/javascript/controllers/index.js not found` — ask the developer where
93
93
  the Stimulus application is set up and add those two lines there. Without
94
94
  them, dropdowns, modals, file uploads, the column picker, the theme toggle,
95
- the info button's popups and the other interactive components do nothing.
95
+ the info button's popups, the search select's list of matches and the other
96
+ interactive components do nothing.
96
97
  `registerControllers(application)` also copies `data-theme` and `data-look`
97
98
  from each page Turbo renders onto the `<html>` tag. Without it, a Turbo
98
99
  visit keeps the theme and look of the first page loaded.
@@ -159,7 +160,7 @@ built on ViewComponent; hook it in before building any screen with those helpers
159
160
  ```
160
161
 
161
162
  Buttons, panels, cards, alerts, badges, form fields, the modal, the mobile
162
- action menu, the action menu, the column picker, multi select, copy button, theme toggle,
163
+ action menu, the action menu, the column picker, multi select, search select, copy button, theme toggle,
163
164
  checkbox row, radio card, option card, file upload and colour picker read
164
165
  these variables. So do the data display components: stat card, chart card,
165
166
  card link, CTA banner, feature grid, hero, data table, code, accordion,
@@ -52,6 +52,7 @@ develop:
52
52
  - ui_settings_link
53
53
  - ui_color_picker
54
54
  - ui_multi_select
55
+ - ui_search_select
55
56
  - ui_swipe_deck
56
57
  - ui_column_picker
57
58
  - ui_form
@@ -137,6 +138,9 @@ sources:
137
138
  - app/components/keystone/ui/mobile_header_component.html.erb
138
139
  - app/components/keystone/ui/modal_component.rb
139
140
  - app/components/keystone/ui/multi_select_component.rb
141
+ - app/components/keystone/ui/search_select_component.rb
142
+ - app/components/keystone/ui/search_select_component.html.erb
143
+ - app/assets/javascripts/keystone_ui/search_select_controller.js
140
144
  - app/components/keystone/ui/nav_dropdown_component.rb
141
145
  - app/components/keystone/ui/nav_item_component.rb
142
146
  - app/components/keystone/ui/navbar_component.rb
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: keystone_ui
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.52.0
4
+ version: 0.53.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tyler Schneider
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: 0.25.0
18
+ version: 0.27.0
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: 0.25.0
25
+ version: 0.27.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: view_component
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -67,6 +67,7 @@ files:
67
67
  - app/assets/javascripts/keystone_ui/modal_controller.js
68
68
  - app/assets/javascripts/keystone_ui/multi_select_controller.js
69
69
  - app/assets/javascripts/keystone_ui/page_theme.js
70
+ - app/assets/javascripts/keystone_ui/search_select_controller.js
70
71
  - app/assets/javascripts/keystone_ui/stat_card_info_controller.js
71
72
  - app/assets/javascripts/keystone_ui/swipe_deck_controller.js
72
73
  - app/assets/javascripts/keystone_ui/tab_switcher_controller.js
@@ -186,6 +187,8 @@ files:
186
187
  - app/components/keystone/ui/radio_card_component.html.erb
187
188
  - app/components/keystone/ui/radio_card_component.rb
188
189
  - app/components/keystone/ui/saved_layout.rb
190
+ - app/components/keystone/ui/search_select_component.html.erb
191
+ - app/components/keystone/ui/search_select_component.rb
189
192
  - app/components/keystone/ui/section_component.html.erb
190
193
  - app/components/keystone/ui/section_component.rb
191
194
  - app/components/keystone/ui/select_component.html.erb