satis 1.0.66

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.
Files changed (101) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +92 -0
  4. data/Rakefile +23 -0
  5. data/app/assets/config/satis_manifest.js +1 -0
  6. data/app/assets/stylesheets/satis/application.css +15 -0
  7. data/app/components/satis/appearance_switcher/component.html.slim +6 -0
  8. data/app/components/satis/appearance_switcher/component.rb +11 -0
  9. data/app/components/satis/appearance_switcher/component.scss +34 -0
  10. data/app/components/satis/appearance_switcher/component_controller.js +62 -0
  11. data/app/components/satis/application_component.rb +50 -0
  12. data/app/components/satis/avatar/component.html.slim +7 -0
  13. data/app/components/satis/avatar/component.rb +52 -0
  14. data/app/components/satis/breadcrumbs/component.html.slim +8 -0
  15. data/app/components/satis/breadcrumbs/component.rb +23 -0
  16. data/app/components/satis/breadcrumbs/component.scss +19 -0
  17. data/app/components/satis/breadcrumbs/crumb.slim +8 -0
  18. data/app/components/satis/card/component.html.slim +54 -0
  19. data/app/components/satis/card/component.md +14 -0
  20. data/app/components/satis/card/component.rb +41 -0
  21. data/app/components/satis/card/component.scss +15 -0
  22. data/app/components/satis/date_time_picker/component.html.slim +48 -0
  23. data/app/components/satis/date_time_picker/component.md +11 -0
  24. data/app/components/satis/date_time_picker/component.rb +48 -0
  25. data/app/components/satis/date_time_picker/component.scss +5 -0
  26. data/app/components/satis/date_time_picker/component_controller.js +499 -0
  27. data/app/components/satis/dropdown/component.html.slim +36 -0
  28. data/app/components/satis/dropdown/component.md +48 -0
  29. data/app/components/satis/dropdown/component.rb +77 -0
  30. data/app/components/satis/dropdown/component.scss +10 -0
  31. data/app/components/satis/dropdown/component_controller.js +547 -0
  32. data/app/components/satis/flash_messages/component.html.slim +3 -0
  33. data/app/components/satis/flash_messages/component.rb +31 -0
  34. data/app/components/satis/flash_messages/component.scss +18 -0
  35. data/app/components/satis/flash_messages/message.html.slim +8 -0
  36. data/app/components/satis/info/component.html.slim +4 -0
  37. data/app/components/satis/info/component.rb +22 -0
  38. data/app/components/satis/info_item/component.html.slim +7 -0
  39. data/app/components/satis/info_item/component.rb +19 -0
  40. data/app/components/satis/input/component.html.slim +11 -0
  41. data/app/components/satis/input/component.rb +38 -0
  42. data/app/components/satis/input/component.scss +50 -0
  43. data/app/components/satis/input/element.html.slim +2 -0
  44. data/app/components/satis/map/component.html.slim +2 -0
  45. data/app/components/satis/map/component.rb +17 -0
  46. data/app/components/satis/map/component.scss +9 -0
  47. data/app/components/satis/map/component_controller.js +37 -0
  48. data/app/components/satis/menu/component.html.slim +13 -0
  49. data/app/components/satis/menu/component.md +1 -0
  50. data/app/components/satis/menu/component.rb +16 -0
  51. data/app/components/satis/menu/component_controller.js +62 -0
  52. data/app/components/satis/menu_item/component.html.slim +16 -0
  53. data/app/components/satis/menu_item/component.rb +14 -0
  54. data/app/components/satis/page/component.html.slim +45 -0
  55. data/app/components/satis/page/component.rb +15 -0
  56. data/app/components/satis/page/component_controller.js +86 -0
  57. data/app/components/satis/sidebar_menu/component.html.slim +3 -0
  58. data/app/components/satis/sidebar_menu/component.rb +17 -0
  59. data/app/components/satis/sidebar_menu/component.scss +0 -0
  60. data/app/components/satis/sidebar_menu/component_controller.js +9 -0
  61. data/app/components/satis/sidebar_menu/mobile/component.html.slim +3 -0
  62. data/app/components/satis/sidebar_menu/mobile/component.rb +10 -0
  63. data/app/components/satis/sidebar_menu_item/component.html.slim +15 -0
  64. data/app/components/satis/sidebar_menu_item/component.rb +20 -0
  65. data/app/components/satis/sidebar_menu_item/component.scss +27 -0
  66. data/app/components/satis/sidebar_menu_item/component_controller.js +62 -0
  67. data/app/components/satis/sidebar_menu_item/mobile/component.html.slim +17 -0
  68. data/app/components/satis/sidebar_menu_item/mobile/component.rb +10 -0
  69. data/app/components/satis/switch/component.html.slim +14 -0
  70. data/app/components/satis/switch/component.rb +24 -0
  71. data/app/components/satis/switch/component_controller.js +49 -0
  72. data/app/components/satis/tab/component.rb +35 -0
  73. data/app/components/satis/tabs/component.html.slim +23 -0
  74. data/app/components/satis/tabs/component.md +21 -0
  75. data/app/components/satis/tabs/component.rb +16 -0
  76. data/app/components/satis/tabs/component.scss +33 -0
  77. data/app/components/satis/tabs/component_controller.js +123 -0
  78. data/app/controllers/satis/application_controller.rb +4 -0
  79. data/app/helpers/satis/application_helper.rb +15 -0
  80. data/app/jobs/satis/application_job.rb +4 -0
  81. data/app/mailers/satis/application_mailer.rb +6 -0
  82. data/app/models/satis/application_record.rb +5 -0
  83. data/app/views/shared/_fields_for.html.slim +35 -0
  84. data/config/routes.rb +5 -0
  85. data/lib/satis/action_controller_helpers.rb +29 -0
  86. data/lib/satis/configuration.rb +61 -0
  87. data/lib/satis/engine.rb +27 -0
  88. data/lib/satis/forms/builder.rb +440 -0
  89. data/lib/satis/forms/concerns/buttons.rb +49 -0
  90. data/lib/satis/forms/concerns/file.rb +35 -0
  91. data/lib/satis/forms/concerns/options.rb +44 -0
  92. data/lib/satis/forms/concerns/required.rb +68 -0
  93. data/lib/satis/forms/concerns/select.rb +95 -0
  94. data/lib/satis/helpers/container.rb +83 -0
  95. data/lib/satis/menus/builder.rb +13 -0
  96. data/lib/satis/menus/item.rb +34 -0
  97. data/lib/satis/menus/menu.rb +23 -0
  98. data/lib/satis/version.rb +3 -0
  99. data/lib/satis.rb +36 -0
  100. data/lib/tasks/satis_tasks.rake +4 -0
  101. metadata +213 -0
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Satis
4
+ module Info
5
+ class Component < Satis::ApplicationComponent
6
+ renders_many :items, lambda { |*args|
7
+ args.last.merge!(group: group)
8
+ component = Satis::InfoItem::Component.new(*args)
9
+ component.original_view_context = original_view_context
10
+ component
11
+ }
12
+
13
+ attr_reader :group, :options
14
+
15
+ def initialize(group: :main, **options)
16
+ super
17
+ @group = group
18
+ @options = options
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,7 @@
1
+ - if content.present? || options[:content].present? || options[:show_always]
2
+ div class=options[:class]
3
+ dt.text-xs.uppercase.text-gray-400 = ct(".#{name}", default: name.to_s.humanize, scope: group)
4
+ dd.text-sm.text-gray-900.dark:text-gray-300
5
+ - if options[:icon]
6
+ i.mr-1 class=options[:icon]
7
+ = content || options[:content] || @placeholder
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Satis
4
+ module InfoItem
5
+ class Component < Satis::ApplicationComponent
6
+ attr_reader :options, :name, :icon, :group
7
+
8
+ def initialize(name, *args, &block)
9
+ super
10
+ @name = name
11
+ @args = args
12
+ @options = args.extract_options!
13
+ @group = options[:group]
14
+ @icon = options[:icon]
15
+ @placeholder = options[:placeholder] || '—'
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,11 @@
1
+ .sts-input
2
+ = form.custom_label(attribute, options[:label]) unless options[:label] == false
3
+ .sts-input__input-container class=input_container_class
4
+ - prefixes.each do |prefix|
5
+ = prefix
6
+ = input || form.string_field(attribute, { class: input_class }.merge(options.fetch(:input_html, {})))
7
+ - postfixes.each do |postfix|
8
+ = postfix
9
+ - if hint
10
+ small.form-text.text-muted= hint
11
+ = form.error_text(attribute) if form.has_error?(attribute)
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Satis
4
+ module Input
5
+ class Element < ViewComponent::Base
6
+ attr_reader :classes
7
+
8
+ def initialize(classes: nil, colored: true)
9
+ @classes = classes || ''
10
+ @classes += ' colored' if colored
11
+ end
12
+ end
13
+
14
+ class Component < Satis::ApplicationComponent
15
+ attr_reader :form, :attribute, :options
16
+
17
+ renders_one :label
18
+ renders_one :input
19
+ renders_one :hint
20
+ renders_many :prefixes, Element
21
+ renders_many :postfixes, Element
22
+
23
+ def initialize(form: nil, attribute: nil, **options)
24
+ @form = form
25
+ @attribute = attribute
26
+ @options = options
27
+ end
28
+
29
+ def input_class
30
+ [@options.fetch(:input_html, {}).fetch(:class, ''), 'sts-input__input', form.has_error?(attribute) && 'is-invalid'].join(' ')
31
+ end
32
+
33
+ def input_container_class
34
+ form.has_error?(attribute) && 'is-invalid'
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,50 @@
1
+ .sts-input {
2
+ &__input-container {
3
+ @apply mt-1 flex border rounded h-12 text-gray-800 dark:text-gray-300 border-gray-300 dark:bg-gray-800 dark:border-gray-700 overflow-hidden;
4
+ }
5
+
6
+ &__label {
7
+
8
+ }
9
+
10
+ &__input-container {
11
+ &.is-invalid {
12
+ @apply border-red-300 text-red-900 placeholder-red-300 focus:outline-none focus:ring-red-500 focus:border-red-500;
13
+ }
14
+ }
15
+
16
+ &__input {
17
+ @apply flex-1 min-w-0 block w-full border-none py-2 px-3;
18
+ background: transparent;
19
+
20
+ &:focus {
21
+ box-shadow: none;
22
+ }
23
+ }
24
+
25
+ &__element {
26
+ @apply inline-flex items-center py-2 px-3;
27
+
28
+ &.colored {
29
+ background: rgba(1, 1, 1, 0.1);
30
+
31
+ .dark & {
32
+ background: rgba(255, 255, 255, 0.1);
33
+ }
34
+ }
35
+
36
+ input {
37
+ @apply -ml-3 -mr-3 -mt-2 -mb-2 w-full;
38
+
39
+ &:focus {
40
+ box-shadow: none;
41
+ }
42
+ }
43
+
44
+ * {
45
+ @apply border-none;
46
+
47
+ background-color: transparent;
48
+ }
49
+ }
50
+ }
@@ -0,0 +1,2 @@
1
+ span.sts-input__element class=classes
2
+ = content
@@ -0,0 +1,2 @@
1
+ .satis-map data-controller="satis-map" data-satis-map-urls-value=@urls data-satis-map-longitude-value=@longitude data-satis-map-latitude-value=@latitude data-satis-map-zoom-level-value=@zoom_level data-satis-map-geo-json-url-value=@geo_json_url
2
+ .sts-map__map data-satis-map-target="container"
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Satis
4
+ module Map
5
+ class Component < Satis::ApplicationComponent
6
+ attr_reader :latitude, :longitude, :zoom_level, :geo_json_url
7
+
8
+ def initialize(latitude: 52.09083, longitude: 5.12222, zoom_level: 7, geo_json_url: nil)
9
+ super
10
+ @latitude = latitude
11
+ @longitude = longitude
12
+ @zoom_level = zoom_level
13
+ @geo_json_url = geo_json_url
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,9 @@
1
+ @import "leaflet/dist/leaflet";
2
+
3
+ .sts-map {
4
+ margin-bottom: 1rem;
5
+
6
+ .sts-map__map {
7
+ height: 400px;
8
+ }
9
+ }
@@ -0,0 +1,37 @@
1
+ // map_controller.js
2
+ import ApplicationController from "../../../../frontend/controllers/application_controller"
3
+ import L from "leaflet"
4
+
5
+ export default class extends ApplicationController {
6
+ static targets = ["container"]
7
+ static values = { urls: String, latitude: Number, longitude: Number, zoomLevel: Number, geoJsonUrl: String }
8
+
9
+ connect() {
10
+ super.connect()
11
+
12
+ // Example https://leafletjs.com/examples/choropleth/
13
+ // Data https://public.opendatasoft.com/explore/?sort=modified&q=netherlands
14
+
15
+ this.map = L.map(this.containerTarget).setView([this.latitudeValue, this.longitudeValue], this.zoomLevelValue)
16
+ L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' }).addTo(this.map)
17
+
18
+ // this.map.setView() etc... as normal.
19
+
20
+ // Load layers and setup event handlers, for example:
21
+ fetch(this.geoJsonUrlValue)
22
+ .then((response) => response.json())
23
+ .then((data) => {
24
+ L.geoJSON(data, {
25
+ onEachFeature: (feature, layer) => {
26
+ layer.on("click", () => this.onClick(layer))
27
+ },
28
+ }).addTo(this.map)
29
+ })
30
+ }
31
+
32
+ disconnect() {
33
+ this.map.remove()
34
+ }
35
+
36
+ onClick(layer) {}
37
+ }
@@ -0,0 +1,13 @@
1
+ div data-controller="satis-menu" data-action="mouseover->satis-menu#show mouseleave->satis-menu#hide"
2
+ - if content
3
+ = content
4
+ - else
5
+ button.inline-flex.items-center.justify-center.h-8.w-8.rounded-full.focus:outline-none.focus:ring-2.focus:ring-offset-2.focus:ring-primary-500.dark:text-gray-500 aria-expanded="false" aria-haspopup="true" type="button"
6
+ span.font-semibold.flex-1 class="#{menu.items.present? ? '' : 'text-gray-200'}"
7
+ i class=icon
8
+
9
+ - if menu.items.present?
10
+ ul.hidden.z-10.bg-white.dark:bg-gray-400.border.dark:border-gray-800.rounded-md.shadow-lg.min-w-max data-satis-menu-target="submenu" data-satis-menu-submenu-placement="bottom"
11
+ - menu.items.each do |item|
12
+ = render(Satis::MenuItem::Component.new(item: item))
13
+
@@ -0,0 +1 @@
1
+ https://tailwindcomponents.com/component/nestable-dropdown-menu
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Satis
4
+ module Menu
5
+ class Component < Satis::ApplicationComponent
6
+ # renders_many :tabs, Tab::Component
7
+ attr_reader :menu, :icon
8
+
9
+ def initialize(menu, icon: nil)
10
+ super
11
+ @menu = menu
12
+ @icon = icon || 'fa-solid fa-ellipsis-vertical'
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,62 @@
1
+ import ApplicationController from "../../../../frontend/controllers/application_controller"
2
+ // FIXME: Is this full path really needed?
3
+ import { debounce } from "../../../../frontend/utils"
4
+ import { createPopper } from "@popperjs/core"
5
+
6
+ export default class extends ApplicationController {
7
+ static targets = ["submenu", "toggle"]
8
+
9
+ connect() {
10
+ super.connect()
11
+
12
+ if (this.hasSubmenuTarget) {
13
+ this.popperInstance = createPopper(this.element, this.submenuTarget, {
14
+ offset: [-20, 2],
15
+ placement: this.submenuTarget.getAttribute("data-satis-menu-submenu-placement") || "auto",
16
+ modifiers: [
17
+ {
18
+ name: "flip",
19
+ enabled: true,
20
+ options: {
21
+ boundary: this.element.closest(".satis-card"),
22
+ },
23
+ },
24
+ {
25
+ name: "preventOverflow",
26
+ },
27
+ ],
28
+ })
29
+ }
30
+ }
31
+
32
+ show(event) {
33
+ if (this.hasSubmenuTarget && (!this.hasToggleTarget || (this.hasToggleTarget && this.toggledOn))) {
34
+ this.submenuTarget.classList.remove("hidden")
35
+ this.submenuTarget.setAttribute("data-show", "")
36
+ this.popperInstance.update()
37
+ }
38
+ }
39
+
40
+ hide(event) {
41
+ if (this.hasSubmenuTarget) {
42
+ this.submenuTarget.classList.add("hidden")
43
+ this.submenuTarget.removeAttribute("data-show")
44
+ }
45
+ }
46
+
47
+ toggle(event) {
48
+ if (this.hasToggleTarget) {
49
+ this.toggleTarget.classList.toggle("hidden")
50
+ this.triggerEvent(this.toggleTarget, "toggle", { toggled: !this.toggleTarget.classList.contains("hidden"), id: this.toggleTarget.getAttribute("id") })
51
+ if (this.toggleTarget.classList.contains("hidden")) {
52
+ this.hide(event)
53
+ } else {
54
+ this.show(event)
55
+ }
56
+ }
57
+ }
58
+
59
+ get toggledOn() {
60
+ return !this.toggleTarget.classList.contains("hidden")
61
+ }
62
+ }
@@ -0,0 +1,16 @@
1
+ li.rounded-sm.px-3.py-1.hover:bg-gray-100.dark:hover:bg-gray-200.flex.items-center data-controller="satis-menu" data-action="mouseover->satis-menu#show mouseleave->satis-menu#hide"
2
+ a.cursor-pointer.py-1.w-full.text-left.flex.items-center.outline-none.focus:outline-none href=item.link *item.link_attributes
3
+ span.pr-1.flex-shrink-0.w-6
4
+ - if item.icon.present?
5
+ i class=item.icon
6
+ span.pr-1.flex-1 = item.label
7
+ span.pr-1.flex-shrink-0.w-6
8
+ - if item.type == :toggle
9
+ i.fal.fa-check.hidden data-satis-menu-target="toggle" id="#{item.id}"
10
+
11
+ - if item.menu
12
+ ul.hidden.bg-white.border.rounded-md.shadow-lg data-satis-menu-target="submenu" data-satis-menu-submenu-placement="left"
13
+ - item.menu.items.each do |sub_item|
14
+ = render(Satis::MenuItem::Component.new(item: sub_item))
15
+
16
+
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Satis
4
+ module MenuItem
5
+ class Component < Satis::ApplicationComponent
6
+ attr_reader :item
7
+
8
+ # renders_many :items
9
+ def initialize(**options)
10
+ @item = options[:item]
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,45 @@
1
+ doctype html
2
+ html lang="en"
3
+ head
4
+ = head
5
+ body
6
+ .h-screen.flex.overflow-hidden.bg-gray-100.dark:bg-gray-600 data-controller="satis-page"
7
+ / Off-canvas menu for mobile, show/hide based on off-canvas menu state.
8
+ .fixed.inset-0.flex.z-40.md:hidden.hidden aria-modal="true" role="dialog" data-satis-page-target="dialog"
9
+
10
+ .fixed.inset-0.bg-gray-600.bg-opacity-75.hidden aria-hidden="true" data-satis-page-target="overlay"
11
+
12
+ .relative.flex-1.flex.flex-col.max-w-xs.w-full.pt-5.pb-4.bg-white.transform.hidden data-satis-page-target="offCanvasMenu"
13
+
14
+ .absolute.top-0.right-0.-mr-12.pt-2.hidden data-satis-page-target="closeButton"
15
+ button.ml-1.flex.items-center.justify-center.h-10.w-10.rounded-full.focus:outline-none.focus:ring-2.focus:ring-inset.focus:ring-white data-action="satis-page#close"
16
+ span.sr-only Close sidebar
17
+ i.fal.fa-2x.fa-xmark.text-white aria-hidden="true"
18
+ = sidebar_mobile
19
+
20
+ .flex-shrink-0.w-14 aria-hidden="true"
21
+ / Dummy element to force sidebar to shrink to fit close icon
22
+
23
+ / SIDEBAR - for desktop
24
+ .hidden.md:flex.md:flex-shrink-0
25
+ .flex.flex-col.w-64
26
+ /! Sidebar component, swap this element with another sidebar if you like
27
+
28
+ .flex.flex-col.flex-grow.border-r.border-gray-200.pt-5.pb-4.bg-white.overflow-y-auto.dark:bg-gray-900.dark:border-gray-700
29
+ = sidebar
30
+
31
+ / TOPBAR
32
+ .flex.flex-col.w-0.flex-1.overflow-hidden
33
+ .relative.z-10.flex-shrink-0.flex.h-16.bg-white.shadow.dark:bg-gray-900.dark:border-gray-700
34
+ button.px-4.border-r.border-gray-200.text-gray-500.focus:outline-none.focus:ring-2.focus:ring-inset.focus:ring-primary-500.md:hidden data-action="satis-page#open"
35
+ span.sr-only Open sidebar
36
+ i.fal.fa-2x.fa-bars
37
+
38
+ .flex-1.px-4.flex.justify-between
39
+ = navbar
40
+
41
+ main.flex-1.relative.overflow-y-auto.focus:outline-none
42
+ .py-4
43
+ .max-w.mx-auto.px-4.sm:px-4.md:px-4
44
+ = body
45
+
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Satis
4
+ module Page
5
+ class Component < Satis::ApplicationComponent
6
+ renders_one :head
7
+ renders_one :navbar
8
+ renders_one :sidebar_mobile
9
+ renders_one :sidebar
10
+ renders_one :body
11
+
12
+ def initialize(**options); end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,86 @@
1
+ import ApplicationController from "../../../../frontend/controllers/application_controller"
2
+
3
+ export default class extends ApplicationController {
4
+ static targets = ["closeButton", "openButton", "offCanvasMenu", "overlay", "dialog"]
5
+ connect() {
6
+ super.connect()
7
+ }
8
+
9
+ close(event) {
10
+ // | Entering: "transition-opacity ease-linear duration-300"
11
+ // | From: "opacity-0"
12
+ // | To: "opacity-100"
13
+ // | Leaving: "transition-opacity ease-linear duration-300"
14
+ // | From: "opacity-100"
15
+ // | To: "opacity-0"
16
+ // overlay
17
+ this.overlayTarget.classList.add("transition-opacity", "ease-linear", "duration-300", "opacity-0")
18
+
19
+ // | Entering: "transition ease-in-out duration-300 transform"
20
+ // | From: "-translate-x-full"
21
+ // | To: "translate-x-0"
22
+ // | Leaving: "transition ease-in-out duration-300 transform"
23
+ // | From: "translate-x-0"
24
+ // | To: "-translate-x-full"
25
+ // offCanvasMenu
26
+ this.offCanvasMenuTarget.classList.add("transition", "ease-in-out", "duration-1000")
27
+
28
+ // | Entering: "ease-in-out duration-300"
29
+ // | From: "opacity-0"
30
+ // | To: "opacity-100"
31
+ // | Leaving: "ease-in-out duration-300"
32
+ // | From: "opacity-100"
33
+ // | To: "opacity-0"
34
+ // closeButton
35
+ this.closeButtonTarget.classList.add("ease-in-out", "duration-300", "opacity-0")
36
+
37
+ setTimeout(() => {
38
+ this.dialogTarget.classList.add("hidden")
39
+ this.overlayTarget.classList.add("hidden")
40
+ this.offCanvasMenuTarget.classList.add("hidden")
41
+ this.closeButtonTarget.classList.add("hidden")
42
+
43
+ this.overlayTarget.classList.remove("transition-opacity", "ease-linear", "duration-300")
44
+ this.offCanvasMenuTarget.classList.remove("transition", "ease-in-out", "duration-300")
45
+ this.closeButtonTarget.classList.remove("ease-in-out", "duration-300")
46
+ }, 100)
47
+ }
48
+
49
+ open(event) {
50
+ this.dialogTarget.classList.remove("hidden")
51
+ this.overlayTarget.classList.remove("hidden")
52
+ this.offCanvasMenuTarget.classList.remove("hidden")
53
+ this.closeButtonTarget.classList.remove("hidden")
54
+
55
+ // | Entering: "transition-opacity ease-linear duration-300"
56
+ // | From: "opacity-0"
57
+ // | To: "opacity-100"
58
+ // | Leaving: "transition-opacity ease-linear duration-300"
59
+ // | From: "opacity-100"
60
+ // | To: "opacity-0"
61
+ // overlay
62
+ this.overlayTarget.classList.remove("opacity-0")
63
+ this.overlayTarget.classList.add("transition-opacity", "ease-linear", "duration-300", "opacity-100")
64
+
65
+ // | Entering: "transition ease-in-out duration-300 transform"
66
+ // | From: "-translate-x-full"
67
+ // | To: "translate-x-0"
68
+ // | Leaving: "transition ease-in-out duration-300 transform"
69
+ // | From: "translate-x-0"
70
+ // | To: "-translate-x-full"
71
+ // offCanvasMenu
72
+ this.offCanvasMenuTarget.classList.add("transition", "ease-in-out", "duration-1000")
73
+ this.offCanvasMenuTarget.classList.remove("-translate-x-full")
74
+ this.offCanvasMenuTarget.classList.add("transition", "ease-in-out", "duration-1000", "transform", "translate-x-0")
75
+
76
+ // | Entering: "ease-in-out duration-300"
77
+ // | From: "opacity-0"
78
+ // | To: "opacity-100"
79
+ // | Leaving: "ease-in-out duration-300"
80
+ // | From: "opacity-100"
81
+ // | To: "opacity-0"
82
+ // closeButton
83
+ this.closeButtonTarget.classList.remove("opacity-0")
84
+ this.closeButtonTarget.classList.add("ease-in-out", "duration-300", "opacity-100")
85
+ }
86
+ }
@@ -0,0 +1,3 @@
1
+ nav.flex-1.px-2.space-y-1 aria-label="Sidebar"
2
+ - @menu.items.each do |item|
3
+ = render(Satis::SidebarMenuItem::Component.new(item: item, menu_options: menu_options))
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Satis
4
+ module SidebarMenu
5
+ class Component < Satis::ApplicationComponent
6
+ attr_reader :menu, :menu_options
7
+
8
+ renders_many :items
9
+
10
+ def initialize(menu, **options)
11
+ super
12
+ @menu = menu
13
+ @menu_options = options
14
+ end
15
+ end
16
+ end
17
+ end
File without changes
@@ -0,0 +1,9 @@
1
+ import ApplicationController from "../../../../frontend/controllers/application_controller"
2
+ // FIXME: Is this full path really needed?
3
+ import { debounce } from "../../../../frontend/utils"
4
+
5
+ export default class extends ApplicationController {
6
+ connect() {
7
+ super.connect()
8
+ }
9
+ }
@@ -0,0 +1,3 @@
1
+ nav.px-2.space-y-1
2
+ - @menu.items.each do |item|
3
+ = render(Satis::SidebarMenuItem::Mobile::Component.new(item: item))
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Satis
4
+ module SidebarMenu
5
+ module Mobile
6
+ class Component < ::Satis::SidebarMenu::Component
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,15 @@
1
+ .sts-sidebar-menu-item class="#{item.level >= 1 ? 'pl-4' : ''}" data-controller="satis-sidebar-menu-item"
2
+ a.sts-sidebar-menu-item__link aria-controls="sub-menu-1" aria-expanded="false" href=item.link *item.link_attributes data-satis-sidebar-menu-item-target="link" data-action=data_actions
3
+ - if item.icon
4
+ i.sts-sidebar-menu-item__icon.fa-lg class=item.icon style="width: 20px;"
5
+ - else
6
+ i.sts-sidebar-menu-item__no-icon style="width: 20px;"
7
+
8
+ span.sts-sidebar-menu-item__label= item.label
9
+ - if item.menu
10
+ i.sts-sidebar-menu-item__menu-icon.fa-solid.fa-angle-right style="width: 20px;" data-satis-sidebar-menu-item-target="indicator"
11
+
12
+ - if item.menu
13
+ div.hidden data-satis-sidebar-menu-item-target="submenu"
14
+ - item.menu.items.each do |sub_item|
15
+ = render(Satis::SidebarMenuItem::Component.new(item: sub_item))
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Satis
4
+ module SidebarMenuItem
5
+ class Component < Satis::ApplicationComponent
6
+ attr_reader :item, :menu_options
7
+
8
+ # renders_many :items
9
+ def initialize(**options)
10
+ @item = options[:item]
11
+ @menu_options = options.fetch(:menu_options, {})
12
+ @actions = item.link_attributes.delete(:'data-action')
13
+ end
14
+
15
+ def data_actions
16
+ "click->satis-sidebar-menu-item#open #{@actions}"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,27 @@
1
+ .sts-sidebar-menu-item {
2
+ @apply pt-1;
3
+
4
+ & a.focus {
5
+ background: rgba(1, 1, 1, 0.1);
6
+
7
+ .dark & {
8
+ background: rgba(255, 255, 255, 0.3);
9
+ }
10
+ }
11
+
12
+ &__link {
13
+ @apply text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 hover:text-gray-900 w-full flex items-center pl-2 pr-1 py-2 text-left text-sm font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500
14
+ }
15
+ &__icon {
16
+ @apply mr-3 flex-shrink-0 h-6 w-6 text-gray-400 group-hover:text-gray-500;
17
+ }
18
+ &__no-icon {
19
+ @apply mr-3 flex-shrink-0 h-6;
20
+ }
21
+ &__label {
22
+ @apply flex-1;
23
+ }
24
+ &__menu-icon {
25
+ @apply text-gray-300 ml-3 flex-shrink-0 h-5 w-5 transform group-hover:text-gray-400 transition-colors ease-in-out duration-150;
26
+ }
27
+ }