satis 1.0.66

Sign up to get free protection for your applications and to get access to all the features.
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
+ }