spina 2.18.0 → 2.19.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.

Potentially problematic release.


This version of spina might be problematic. Click here for more details.

Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/app/assets/javascripts/spina/controllers/data_binding_controller.js +1 -0
  4. data/app/assets/javascripts/spina/controllers/page_select_controller.js +2 -37
  5. data/app/assets/javascripts/spina/controllers/reveal_controller.js +1 -1
  6. data/app/assets/javascripts/spina/controllers/select_controller.js +44 -0
  7. data/app/assets/javascripts/spina/libraries/stimulus-data-bindings@1.3.2.js +234 -0
  8. data/app/assets/javascripts/spina/libraries/stimulus-reveal@1.4.2.js +424 -0
  9. data/app/controllers/spina/admin/resource_select_options_controller.rb +20 -0
  10. data/app/models/spina/navigation_item.rb +5 -1
  11. data/app/models/spina/parts/image_variant.rb +3 -3
  12. data/app/models/spina/parts/page_link.rb +3 -2
  13. data/app/models/spina/parts/resource_link.rb +13 -0
  14. data/app/views/spina/admin/page_select_options/index.html.erb +3 -4
  15. data/app/views/spina/admin/page_select_options/show.html.erb +1 -1
  16. data/app/views/spina/admin/parts/page_links/_form.html.erb +17 -14
  17. data/app/views/spina/admin/parts/resource_links/_form.html.erb +32 -0
  18. data/app/views/spina/admin/resource_select_options/index.html.erb +13 -0
  19. data/app/views/spina/admin/resource_select_options/show.html.erb +3 -0
  20. data/config/locales/da.yml +418 -0
  21. data/config/locales/en.yml +8 -5
  22. data/config/routes.rb +4 -1
  23. data/lib/spina/engine.rb +2 -1
  24. data/lib/spina/theme.rb +8 -6
  25. data/lib/spina/version.rb +1 -1
  26. data/lib/tasks/tailwind.rake +12 -4
  27. metadata +25 -10
  28. data/app/assets/javascripts/spina/libraries/stimulus-reveal@1.2.4.js +0 -388
@@ -0,0 +1,424 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ /**
4
+ * Stimulus controller to toggle element visibility
5
+ * @extends Controller
6
+ */
7
+ export default class RevealController extends Controller {
8
+ static values = {
9
+ open: Boolean,
10
+ transitioning: Boolean,
11
+ targetSelector: String,
12
+ toggleKeys: String,
13
+ showKeys: String,
14
+ hideKeys: String,
15
+ away: Boolean,
16
+ debug: Boolean,
17
+ };
18
+
19
+ connect() {
20
+ this._initCloseKeypressListener();
21
+ this._initToggleKeypressListener();
22
+ this._initShowKeypressListener();
23
+ this._awayHandler = this._awayHandler.bind(this);
24
+ }
25
+
26
+ disconnect() {
27
+ this._teardown();
28
+ }
29
+
30
+ /**
31
+ * Shows elements connected to the controller.
32
+ * @param {Event} event - an event with a currentTarget DOMElement
33
+ */
34
+ show(event) {
35
+ if (this.openValue || this.transitioningValue) return;
36
+
37
+ this._init(event, true);
38
+ }
39
+
40
+ /**
41
+ * Hides elements connected to the controller.
42
+ * @param {Event} event - an event with a currentTarget DOMElement
43
+ */
44
+ hide(event) {
45
+ if (!this.openValue || this.transitioningValue) return;
46
+
47
+ this._init(event, false);
48
+ }
49
+
50
+ /**
51
+ * Toggles elements connected to the controller.
52
+ * @param {Event} event - an event with a currentTarget DOMElement
53
+ */
54
+ toggle(event) {
55
+ if (this.transitioningValue) return;
56
+
57
+ this._init(event, !this.openValue);
58
+ }
59
+
60
+ /**
61
+ * Stops event propagation of elements connected to the controller.
62
+ * @param {Event} event - an event with a currentTarget DOMElement
63
+ */
64
+ stop(event) {
65
+ event.stopPropagation();
66
+ }
67
+
68
+ // Private methods
69
+
70
+ /**
71
+ * @private
72
+ * @param {Event} event
73
+ * @param {Event} shouldOpen
74
+ */
75
+ async _init(event, shouldOpen) {
76
+ if (event && event.currentTarget && event.currentTarget.dataset) {
77
+ if ("revealPreventDefault" in event.currentTarget.dataset) {
78
+ event.preventDefault();
79
+ }
80
+ if ("revealStopPropagation" in event.currentTarget.dataset) {
81
+ event.stopPropagation();
82
+ }
83
+ }
84
+ // start stuff
85
+ const startSelector = `${this.selector}[data-${shouldOpen ? "enter" : "leave"
86
+ }-start]`;
87
+ const startPromises = this._didInitWithPromise(startSelector, shouldOpen);
88
+ await Promise.all(startPromises);
89
+
90
+ const defaultSelector = `${this.selector}:not([data-${shouldOpen ? "enter" : "leave"
91
+ }-start]):not([data-${shouldOpen ? "enter" : "leave"}-end])`;
92
+ const defaultPromises = this._didInitWithPromise(
93
+ defaultSelector,
94
+ shouldOpen
95
+ );
96
+ await Promise.all(defaultPromises);
97
+
98
+ // end stuff
99
+ const endSelector = `${this.selector}[data-${shouldOpen ? "enter" : "leave"
100
+ }-end]`;
101
+ const endPromises = this._didInitWithPromise(endSelector, shouldOpen);
102
+ await Promise.all(endPromises);
103
+ }
104
+
105
+ /**
106
+ * @private
107
+ */
108
+ _teardown() {
109
+ if (this.hasAwayValue) {
110
+ document.removeEventListener("click", this._awayHandler);
111
+ }
112
+ }
113
+
114
+ _didInitWithPromise(selector, shouldOpen) {
115
+ this._debug("selecting", selector, this._fetchElements(selector));
116
+ return this._fetchElements(selector).map((element) => {
117
+ return this._doInitTransition(element, shouldOpen);
118
+ });
119
+ }
120
+
121
+ /**
122
+ * @private
123
+ */
124
+ _initCloseKeypressListener() {
125
+ if (this.hasHideKeysValue) {
126
+ document.addEventListener("keydown", (event) => {
127
+ if (!this.openValue) return;
128
+ if (!this.hideKeysValue.split(",").includes(event.key.toLowerCase())) {
129
+ return;
130
+ }
131
+
132
+ event.stopPropagation();
133
+ this.toggle(event);
134
+ });
135
+ }
136
+ }
137
+
138
+ /**
139
+ * @private
140
+ */
141
+ _initToggleKeypressListener() {
142
+ if (this.hasToggleKeysValue) {
143
+ document.addEventListener("keydown", (event) => {
144
+ if (
145
+ !this.toggleKeysValue.split(",").includes(event.key.toLowerCase())
146
+ ) {
147
+ return;
148
+ }
149
+
150
+ event.stopPropagation();
151
+
152
+ this.toggle(event);
153
+ });
154
+ }
155
+ }
156
+
157
+ /**
158
+ * @private
159
+ */
160
+ _initShowKeypressListener() {
161
+ if (this.hasShowKeysValue) {
162
+ document.addEventListener("keydown", (event) => {
163
+ if (this.openValue) return;
164
+ if (!this.showKeysValue.split(",").includes(event.key.toLowerCase())) {
165
+ return;
166
+ }
167
+
168
+ event.stopPropagation();
169
+
170
+ this.toggle(event);
171
+ });
172
+ }
173
+ }
174
+
175
+ /**
176
+ * @private
177
+ */
178
+ _awayHandler(event) {
179
+ if (!this.element.contains(event.target)) {
180
+ document.removeEventListener("click", this._awayHandler);
181
+ this.hide(event);
182
+ }
183
+ return true;
184
+ }
185
+
186
+ /**
187
+ * @private
188
+ * @param {DOMElement} target
189
+ * @param {boolean} openState
190
+ */
191
+ _doInitTransition(target, openState) {
192
+ this._debug("init transition", `${openState ? "open" : "closed"}`, target);
193
+ this._debug(
194
+ "dispatching event",
195
+ `reveal:${openState ? "show" : "hide"}`,
196
+ target
197
+ );
198
+ target.dispatchEvent(
199
+ new Event(`reveal:${openState ? "show" : "hide"}`, {
200
+ bubbles: true,
201
+ cancelable: false,
202
+ })
203
+ );
204
+
205
+ return new Promise((resolve, reject) => {
206
+ if (
207
+ "transition" in target.dataset &&
208
+ this.element.offsetParent !== null
209
+ ) {
210
+ requestAnimationFrame(() => {
211
+ this._transitionSetup(target, openState);
212
+ const _didEndTransition = this._didEndTransition.bind(this);
213
+
214
+ target.addEventListener(
215
+ "transitionend",
216
+ function _didEndTransitionHandler() {
217
+ _didEndTransition(target, openState);
218
+ target.removeEventListener(
219
+ "transitionend",
220
+ _didEndTransitionHandler
221
+ );
222
+ resolve();
223
+ }
224
+ );
225
+
226
+ requestAnimationFrame(() => {
227
+ this._doStartTransition(target, openState);
228
+ });
229
+ });
230
+ } else {
231
+ if (openState) {
232
+ this._debug(
233
+ "force hidden - init",
234
+ `${openState ? "open" : "closed"}`,
235
+ target
236
+ );
237
+ target.hidden = !target.hidden;
238
+ }
239
+ this._doCompleteTransition(target, openState);
240
+ resolve();
241
+ }
242
+ });
243
+ }
244
+
245
+ /**
246
+ * @private
247
+ * @param {DOMElement} target
248
+ */
249
+ _doStartTransition(target, openState) {
250
+ this._debug("start transition", `${openState ? "open" : "closed"}`, target);
251
+ this.transitioningValue = true;
252
+ if (target.dataset.useTransitionClasses === "true") {
253
+ const transitionClasses = this._transitionClasses(
254
+ target,
255
+ this.transitionType
256
+ );
257
+ target.classList.add(...transitionClasses.end.split(" "));
258
+ target.classList.remove(...transitionClasses.start.split(" "));
259
+ } else {
260
+ const transitions = this._transitionDefaults(openState);
261
+ target.style.transformOrigin = transitions.origin;
262
+ target.style.transitionProperty = "opacity transform";
263
+ target.style.transitionDuration = `${transitions.duration / 1000}s`;
264
+ target.style.transitionTimingFunction = "cubic-bezier(0.4, 0.0, 0.2, 1)";
265
+
266
+ target.style.opacity = transitions.to.opacity;
267
+ target.style.transform = `scale(${transitions.to.scale / 100})`;
268
+ }
269
+ }
270
+
271
+ /**
272
+ * @private
273
+ * @param {DOMElement} target
274
+ * @param {boolean} openState
275
+ */
276
+ _didEndTransition(target, openState) {
277
+ this._debug("end transition", `${openState ? "open" : "closed"}`, target);
278
+ if (target.dataset.useTransitionClasses === "true") {
279
+ const transitionClasses = this._transitionClasses(
280
+ target,
281
+ this.transitionType
282
+ );
283
+ target.classList.remove(...transitionClasses.before.split(" "));
284
+ } else {
285
+ target.style.opacity = target.dataset.opacityCache;
286
+ target.style.transform = target.dataset.transformCache;
287
+ target.style.transformOrigin = target.dataset.transformOriginCache;
288
+ }
289
+ this._doCompleteTransition(target, openState);
290
+ }
291
+
292
+ /**
293
+ * @private
294
+ * @param {DOMElement} target
295
+ * @param {boolean} openState
296
+ */
297
+ _doCompleteTransition(target, openState) {
298
+ this._debug(
299
+ "complete transition",
300
+ `${openState ? "open" : "closed"}`,
301
+ target
302
+ );
303
+ this.transitioningValue = false;
304
+
305
+ if (!openState) {
306
+ this._debug(
307
+ "force hidden - complete",
308
+ `${openState ? "open" : "closed"}`,
309
+ target
310
+ );
311
+ target.hidden = !target.hidden;
312
+ }
313
+ this.openValue = openState;
314
+
315
+ this._debug(
316
+ "dispatching event",
317
+ `reveal:${openState ? "shown" : "hidden"}`,
318
+ target
319
+ );
320
+ target.dispatchEvent(
321
+ new Event(`reveal:${openState ? "shown" : "hidden"}`, {
322
+ bubbles: true,
323
+ cancelable: false,
324
+ })
325
+ );
326
+
327
+ if (this.hasAwayValue && openState) {
328
+ document.addEventListener("click", this._awayHandler);
329
+ }
330
+
331
+ this._debug("dispatching event", "reveal:complete", target);
332
+ target.dispatchEvent(
333
+ new Event("reveal:complete", { bubbles: true, cancelable: false })
334
+ );
335
+ }
336
+
337
+ /**
338
+ * @private
339
+ * @param {DOMElement} target
340
+ * @param {boolean} openState
341
+ */
342
+ _transitionSetup(target, openState) {
343
+ this.transitionType = openState ? "transitionEnter" : "transitionLeave";
344
+
345
+ if (this.transitionType in target.dataset) {
346
+ target.dataset.useTransitionClasses = true;
347
+ const transitionClasses = this._transitionClasses(
348
+ target,
349
+ this.transitionType
350
+ );
351
+ target.classList.add(...transitionClasses.before.split(" "));
352
+ target.classList.add(...transitionClasses.start.split(" "));
353
+ } else {
354
+ target.dataset.useTransitionClasses = false;
355
+ const transitions = this._transitionDefaults(openState);
356
+ target.dataset.opacityCache = target.style.opacity;
357
+ target.dataset.transformCache = target.style.transform;
358
+ target.dataset.transformOriginCache = target.style.transformOrigin;
359
+
360
+ target.style.opacity = transitions.from.opacity;
361
+ target.style.transform = `scale(${transitions.from.scale / 100})`;
362
+ }
363
+ if (openState) {
364
+ this._debug("opening with transition", target);
365
+ target.hidden = !target.hidden;
366
+ }
367
+ }
368
+
369
+ /**
370
+ * @private
371
+ * @param {boolean} openState
372
+ */
373
+ _transitionDefaults(openState) {
374
+ return {
375
+ duration: openState ? 200 : 150,
376
+ origin: "center",
377
+ from: {
378
+ opacity: openState ? 0 : 1,
379
+ scale: openState ? 95 : 100,
380
+ },
381
+ to: {
382
+ opacity: openState ? 1 : 0,
383
+ scale: openState ? 100 : 95,
384
+ },
385
+ };
386
+ }
387
+
388
+ /**
389
+ * @private
390
+ * @param {DOMElement} target
391
+ * @param {string} transitionType
392
+ */
393
+ _transitionClasses(target, transitionType) {
394
+ return {
395
+ before: target.dataset[transitionType],
396
+ start: target.dataset[`${transitionType}Start`],
397
+ end: target.dataset[`${transitionType}End`],
398
+ };
399
+ }
400
+
401
+ /**
402
+ * @private
403
+ * @param {String} selector
404
+ */
405
+ _fetchElements(selector) {
406
+ return [this.element, ...this.element.querySelectorAll(selector)].filter(
407
+ (el) => el.matches(selector)
408
+ );
409
+ }
410
+
411
+ /**
412
+ * @private
413
+ * @param {Array} args
414
+ */
415
+ _debug(...args) {
416
+ if (this.debugValue) console.log(...args);
417
+ }
418
+
419
+ get selector() {
420
+ return this.hasTargetSelectorValue
421
+ ? this.targetSelectorValue
422
+ : "[data-reveal]";
423
+ }
424
+ }
@@ -0,0 +1,20 @@
1
+ module Spina
2
+ module Admin
3
+ class ResourceSelectOptionsController < AdminController
4
+
5
+ def show
6
+ @resource = Resource.find(params[:id])
7
+ end
8
+
9
+ def index
10
+ end
11
+
12
+ def search
13
+ @resources ||= Resource.all
14
+ @resources = @resources.where("name ILIKE :query OR label ILIKE :query", query: "%#{params[:search]}%").order(created_at: :desc).distinct.page(params[:page]).per(20)
15
+ render :index
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -6,7 +6,11 @@ module Spina
6
6
  # NavigationItems can be of two different kinds:
7
7
  # - A link to a page
8
8
  # - A link to a URL
9
- enum kind: {page: "page", url: "url"}, _suffix: true
9
+ if Rails.version >= '7.2'
10
+ enum :kind, {page: "page", url: "url"}, suffix: true
11
+ else
12
+ enum kind: {page: "page", url: "url"}, _suffix: true
13
+ end
10
14
 
11
15
  has_ancestry
12
16
 
@@ -1,6 +1,8 @@
1
1
  module Spina
2
2
  module Parts
3
3
  class ImageVariant
4
+ ImageVariation = Struct.new(:key)
5
+
4
6
  attr_reader :blob
5
7
 
6
8
  def initialize(image, options)
@@ -9,9 +11,7 @@ module Spina
9
11
  end
10
12
 
11
13
  def variation
12
- OpenStruct.new({
13
- key: ActiveStorage::Variation.encode(@options)
14
- })
14
+ ImageVariation.new(ActiveStorage::Variation.encode(@options))
15
15
  end
16
16
  end
17
17
  end
@@ -2,11 +2,12 @@ module Spina
2
2
  module Parts
3
3
  class PageLink < Base
4
4
  attr_json :page_id, :integer, default: nil
5
-
5
+ attr_json :text, :string, default: nil
6
+
6
7
  attr_accessor :options
7
8
 
8
9
  def content
9
- Page.live.find_by(id: page_id)
10
+ ::Spina::Page.live.find_by(id: page_id)
10
11
  end
11
12
  end
12
13
  end
@@ -0,0 +1,13 @@
1
+ module Spina
2
+ module Parts
3
+ class ResourceLink < Base
4
+ attr_json :resource_id, :integer, default: nil
5
+
6
+ attr_accessor :options
7
+
8
+ def content
9
+ ::Spina::Resource.find_by(id: resource_id)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,12 +1,11 @@
1
1
  <%= turbo_frame_tag "page_select_options_#{params[:object_id]}" do %>
2
2
  <% if params[:search].blank? %>
3
- <button type="button" data-action="page-select#clear reveal#hide" class="block hover:bg-gray-100 w-full text-left px-5 py-2 font-medium leading-5 text-gray-700 hover:text-gray-900 text-sm border-t border-gray-150">
3
+ <button type="button" data-action="select#clear reveal#hide" class="block hover:bg-gray-100 w-full text-left px-5 py-2 font-medium leading-5 text-gray-700 hover:text-gray-900 text-sm border-t border-gray-150">
4
4
 
5
5
  </button>
6
6
  <% end %>
7
-
8
7
  <% @pages.each do |page| %>
9
- <button type="button" data-action="page-select#select reveal#hide" data-title="<%= page.title %>" data-id="<%= page.id %>" class="block hover:bg-gray-100 w-full text-left px-5 py-2 font-medium leading-5 text-gray-700 hover:text-gray-900 text-sm border-t border-gray-150">
8
+ <button type="button" data-action="select#select reveal#hide" data-title="<%= page.title %>" data-id="<%= page.id %>" class="block hover:bg-gray-100 w-full text-left px-5 py-2 font-medium leading-5 text-gray-700 hover:text-gray-900 text-sm border-t border-gray-150">
10
9
  <%= page.title %>
11
10
  <% if page.draft? %>
12
11
  <span class="font-normal text-gray-400">
@@ -18,4 +17,4 @@
18
17
  </div>
19
18
  </button>
20
19
  <% end %>
21
- <% end %>
20
+ <% end %>
@@ -1,3 +1,3 @@
1
1
  <%= turbo_frame_tag :page_title do %>
2
2
  <%= @page.title %>
3
- <% end %>
3
+ <% end %>
@@ -1,19 +1,23 @@
1
1
  <div class="mt-6">
2
2
  <label class="block text-sm leading-5 font-medium text-gray-700"><%= f.object.title %></label>
3
3
  <div class="text-gray-400 text-sm"><%= f.object.hint %></div>
4
-
5
- <div data-controller="page-select reveal" data-placeholder="<%=t "spina.pages.select_page" %>" class="relative mt-1" data-reveal-away-value>
6
- <%= f.hidden_field :page_id, data: {page_select_target: "input"} %>
7
-
8
- <button type="button" class="btn btn-default px-3 inline-flex items-center text-sm font-medium" data-action="reveal#toggle page-select#autofocus">
9
- <%= heroicon("link", style: :mini, class: "w-4 h-4 mr-1 text-gray-600") %>
10
- <div data-page-select-target="label">
11
- <% if f.object.page_id.present? %>
12
- <%= turbo_frame_tag :page_title, src: spina.admin_page_select_option_path(f.object.page_id) %>
13
- <% end %>
4
+ <div data-controller="select reveal data-binding" data-placeholder="<%=t "spina.pages.select_page" %>" class="relative mt-1" data-reveal-away-value>
5
+ <%= f.hidden_field :page_id, data: { select_target: "input", action: 'change->data-binding#update', binding_target: 'label', binding_condition: "$source.value == ''", binding_attribute: 'hidden' } %>
6
+ <div class="flex">
7
+ <div class="rounded-md border text-gray-700 bg-white shadow-sm border-gray-300">
8
+ <button type="button" class="w-full py-1.5 px-3 inline-flex items-center text-sm font-medium" data-action="reveal#toggle select#autofocus">
9
+ <%= heroicon("link", style: :mini, class: "w-4 h-4 mr-1 text-gray-600") %>
10
+ <div data-select-target="label">
11
+ <% if f.object.page_id.present? %>
12
+ <%= turbo_frame_tag :page_title, src: spina.admin_page_select_option_path(f.object.page_id) %>
13
+ <% end %>
14
+ </div>
15
+ </button>
16
+ <fieldset class="border-t border-gray-300 border-dashed" data-binding-ref="label">
17
+ <%= f.text_field :text, placeholder: t("spina.page_links.text_placeholder"), class: "w-full min-w-64 py-1.5 border-0 rounded-b-md text-sm text-gray-700 focus:border-spina-light focus:ring focus:ring-spina-light" %>
18
+ </fieldset>
14
19
  </div>
15
- </button>
16
-
20
+ </div>
17
21
  <div class="relative mt-1">
18
22
  <div data-reveal data-transition hidden class="absolute shadow-lg border border-gray-200 origin-top-right rounded-md z-10 top-0">
19
23
  <div class="rounded-md bg-white shadow-xs">
@@ -21,10 +25,9 @@
21
25
  <%= ff.hidden_field :object_id, value: f.object.object_id %>
22
26
  <%= ff.hidden_field :resource, value: f.object.options&.dig(:resource) %>
23
27
  <div class="p-2">
24
- <%= ff.search_field :search, placeholder: t("spina.search"), class: "form-input sticky top-0 text-sm w-80", data: {action: "input->form#submit focus->form#submit", page_select_target: "search"} %>
28
+ <%= ff.search_field :search, placeholder: t("spina.search"), class: "form-input sticky top-0 text-sm w-80", data: {action: "input->form#submit focus->form#submit", select_target: "search"} %>
25
29
  </div>
26
30
  <% end %>
27
-
28
31
  <div class="overflow-scroll max-h-80">
29
32
  <%= turbo_frame_tag "page_select_options_#{f.object.object_id}" do %>
30
33
  <% end %>
@@ -0,0 +1,32 @@
1
+ <div class="mt-6">
2
+ <label class="block text-sm leading-5 font-medium text-gray-700"><%= f.object.title %></label>
3
+ <div class="text-gray-400 text-sm"><%= f.object.hint %></div>
4
+ <div data-controller="select reveal" data-placeholder="<%=t "spina.resources.select_resource" %>" class="relative mt-1" data-reveal-away-value>
5
+ <%= f.hidden_field :resource_id, data: {select_target: "input"} %>
6
+ <button type="button" class="btn btn-default px-3 inline-flex items-center text-sm font-medium" data-action="reveal#toggle select#autofocus">
7
+ <%= heroicon("link", style: :mini, class: "w-4 h-4 mr-1 text-gray-600") %>
8
+ <div data-select-target="label">
9
+ <% if f.object.resource_id.present? %>
10
+ <%= turbo_frame_tag :resource_title, src: spina.admin_resource_select_option_path(f.object.resource_id) %>
11
+ <% end %>
12
+ </div>
13
+ </button>
14
+ <div class="relative mt-1">
15
+ <div data-reveal data-transition hidden class="absolute shadow-lg border border-gray-200 origin-top-right rounded-md z-10 top-0">
16
+ <div class="rounded-md bg-white shadow-xs">
17
+ <%= form_with url: spina.search_admin_resource_select_options_path, data: {turbo_frame: "resource_select_options_#{f.object.object_id}", controller: "form", debounce_time: 100} do |ff| %>
18
+ <%= ff.hidden_field :object_id, value: f.object.object_id %>
19
+ <%= ff.hidden_field :resource, value: f.object.options&.dig(:resource) %>
20
+ <div class="p-2">
21
+ <%= ff.search_field :search, placeholder: t("spina.search"), class: "form-input sticky top-0 text-sm w-80", data: {action: "input->form#submit focus->form#submit", select_target: "search"} %>
22
+ </div>
23
+ <% end %>
24
+ <div class="overflow-scroll max-h-80">
25
+ <%= turbo_frame_tag "resource_select_options_#{f.object.object_id}" do %>
26
+ <% end %>
27
+ </div>
28
+ </div>
29
+ </div>
30
+ </div>
31
+ </div>
32
+ </div>
@@ -0,0 +1,13 @@
1
+ <%= turbo_frame_tag "resource_select_options_#{params[:object_id]}" do %>
2
+ <% if params[:search].blank? %>
3
+ <button type="button" data-action="select#clear reveal#hide" class="block hover:bg-gray-100 w-full text-left px-5 py-2 font-medium leading-5 text-gray-700 hover:text-gray-900 text-sm border-t border-gray-150">
4
+
5
+ </button>
6
+ <% end %>
7
+
8
+ <% @resources.each do |resource| %>
9
+ <button type="button" data-action="select#select reveal#hide" data-title="<%= resource.label %>" data-id="<%= resource.id %>" class="block hover:bg-gray-100 w-full text-left px-5 py-2 font-medium leading-5 text-gray-700 hover:text-gray-900 text-sm border-t border-gray-150">
10
+ <%= resource.label %>
11
+ </button>
12
+ <% end %>
13
+ <% end %>
@@ -0,0 +1,3 @@
1
+ <%= turbo_frame_tag :resource_title do %>
2
+ <%= @resource.label %>
3
+ <% end %>