satis 2.2.1 → 2.2.2

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.
@@ -1,5 +1,8 @@
1
+ /* ── Base menu item ── */
1
2
  .sts-sidebar-menu-item {
2
3
  @apply pt-1;
4
+ position: relative;
5
+
3
6
  & a.focus {
4
7
  background: rgba(1, 1, 1, 0.1);
5
8
 
@@ -15,9 +18,8 @@
15
18
  @apply rotate-90;
16
19
  }
17
20
 
18
-
19
21
  &__link {
20
- @apply text-gray-800 dark:text-gray-300 hover:bg-gray-50 dark:text-gray-100 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
22
+ @apply text-gray-800 dark:text-gray-300 hover:bg-gray-50 dark:text-gray-100 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;
21
23
  }
22
24
 
23
25
  &__icon {
@@ -34,166 +36,143 @@
34
36
  }
35
37
  }
36
38
 
37
-
38
- .sidebar.close .sts-sidebar-menu-item__label {
39
+ /* submenu-label always hidden in expanded sidebar */
40
+ .sidebar .sts-sidebar-menu-item .submenu-label {
39
41
  display: none;
40
42
  }
41
43
 
42
- .sidebar.close .sts-sidebar-menu-item:hover > .sts-sidebar-menu-item__link {
43
- width: 40px;
44
- }
45
-
44
+ /* ===================================
45
+ COLLAPSED SIDEBAR (.sidebar.close)
46
+ =================================== */
46
47
 
47
- .sidebar.close .sts-sidebar-menu-item [data-satis-sidebar-menu-item-target="submenu"] .sts-sidebar-menu-item{
48
- display: none;
49
- visibility: hidden;
50
- opacity: 0;
48
+ /* Center icons in collapsed sidebar */
49
+ .sidebar.close .icon-link > .sts-sidebar-menu-item {
50
+ margin-left: 6px;
51
51
  }
52
52
 
53
- .sidebar.close .sts-sidebar-menu-item:hover > [data-satis-sidebar-menu-item-target="submenu"] .sts-sidebar-menu-item > [data-satis-sidebar-menu-item-target="submenu"]{
54
- opacity: 0;
53
+ /* Hide labels and chevrons */
54
+ .sidebar.close .sts-sidebar-menu-item__label {
55
55
  display: none;
56
- visibility: hidden;
57
56
  }
58
57
 
59
-
60
- .sidebar.close .sts-sidebar-menu-item .sts-sidebar-menu-item__link .sts-sidebar-menu-item__menu-icon{
58
+ .sidebar.close .sts-sidebar-menu-item__menu-icon {
61
59
  display: none;
62
- visibility: hidden;
63
- opacity: 0;
64
- }
65
-
66
- .sidebar.close .icon-link > .sts-sidebar-menu-item:hover:not(:has([data-satis-sidebar-menu-item-target="submenu"])) .sts-sidebar-menu-item__label {
67
- @apply rounded-md bg-gray-50 dark:bg-gray-900 shadow-md py-2;
68
- text-decoration-thickness: 2px;
69
- display: block;
70
- position: absolute;
71
- padding-right: 20px;
72
- padding-left: 20px;
73
- margin-left: 40px;
74
60
  }
75
61
 
76
- .sidebar.close .icon-link > .sts-sidebar-menu-item:has([data-satis-sidebar-menu-item-target="submenu"]):hover > .sts-sidebar-menu-item__link > .sts-sidebar-menu-item__label {
77
- @apply text-white dark:bg-gray-600;
78
- display: block;
62
+ /* ── Flyout panel (shown via JS .flyout-visible) ── */
63
+ .sidebar.close .sts-sidebar-menu-item > [data-satis-sidebar-menu-item-target="submenu"].flyout-visible {
64
+ @apply bg-white dark:bg-gray-800 shadow-lg ring-1 ring-black/5 dark:ring-white/10;
65
+ display: block !important;
79
66
  position: absolute;
80
- background-color: rgba(0, 0, 0, 1);
81
- padding: 5px 10px;
82
- border-radius: 4px;
67
+ left: 54px;
83
68
  width: 220px;
84
- margin-left: 22px;
85
- margin-bottom: 80px;
69
+ padding: 4px 0;
70
+ z-index: 1100;
71
+ border-radius: 0 0.5rem 0.5rem 0.5rem;
86
72
  }
87
73
 
88
-
89
- .sidebar.close .icon-link > .sts-sidebar-menu-item:has([data-satis-sidebar-menu-item-target="submenu"]):last-child:hover > .sts-sidebar-menu-item__link > .sts-sidebar-menu-item__label {
90
- margin-top: -760px;
91
- margin-left: 20px;
74
+ /* Invisible bridge: prevents hover gap between sidebar icon and flyout */
75
+ .sidebar.close .sts-sidebar-menu-item > [data-satis-sidebar-menu-item-target="submenu"].flyout-visible::before {
76
+ content: '';
77
+ position: absolute;
78
+ top: -10px;
79
+ left: -20px;
80
+ width: 20px;
81
+ bottom: 0;
92
82
  }
93
83
 
94
- .fontawesome-i2svg-active .h-screen.flex .sidebar.close .icon-link .sts-sidebar-menu-item:hover > [data-satis-sidebar-menu-item-target="submenu"] {
95
- @apply rounded-md bg-white dark:bg-gray-900 py-0 px-0 shadow-lg;
96
- display: block;
97
- visibility: visible;
98
- opacity: 1;
84
+ /* Nested flyout panels (level 3+) */
85
+ .sidebar.close [data-satis-sidebar-menu-item-target="submenu"].flyout-visible [data-satis-sidebar-menu-item-target="submenu"].flyout-visible {
86
+ @apply bg-white dark:bg-gray-800 shadow-lg ring-1 ring-black/5 dark:ring-white/10 rounded-lg;
87
+ display: block !important;
99
88
  position: absolute;
100
- margin-left: 30px;
101
- margin-top: -45px;
102
-
89
+ left: 100%;
103
90
  width: 220px;
104
-
91
+ padding: 4px 0;
92
+ z-index: 1100;
105
93
  }
106
94
 
107
- .fontawesome-i2svg-active .h-screen.flex .sidebar.close .icon-link .sts-sidebar-menu-item:hover > [data-satis-sidebar-menu-item-target="submenu"] .sts-sidebar-menu-item:only-child {
108
- @apply rounded-md bg-white dark:bg-gray-900 py-0 px-0 shadow-lg;
109
- display: block;
110
- visibility: visible;
111
- opacity: 1;
112
- position: fixed;
113
- width: 220px;
114
- margin-left: 0;
115
-
95
+ /* Nested bridge */
96
+ .sidebar.close [data-satis-sidebar-menu-item-target="submenu"].flyout-visible [data-satis-sidebar-menu-item-target="submenu"].flyout-visible::before {
97
+ content: '';
98
+ position: absolute;
99
+ top: 0;
100
+ left: -8px;
101
+ width: 8px;
102
+ bottom: 0;
116
103
  }
117
104
 
118
- .fontawesome-i2svg-active .h-screen.flex .sidebar.close .icon-link .sts-sidebar-menu-item:hover > [data-satis-sidebar-menu-item-target="submenu"] .sts-sidebar-menu-item {
105
+ /* Items inside a visible flyout */
106
+ .sidebar.close [data-satis-sidebar-menu-item-target="submenu"].flyout-visible > .sts-sidebar-menu-item {
119
107
  @apply py-0 px-0;
108
+ display: block;
120
109
  margin-left: 0;
121
110
  }
122
111
 
123
- .h-screen.flex .sidebar.close .icon-link .sts-sidebar-menu-item:hover > [data-satis-sidebar-menu-item-target="submenu"] .sts-sidebar-menu-item a.focus{
124
- @apply bg-white dark:bg-gray-900;
112
+ /* Flyout item links: Tailwind UI dropdown style */
113
+ .sidebar.close [data-satis-sidebar-menu-item-target="submenu"].flyout-visible .sts-sidebar-menu-item__link {
114
+ @apply rounded-none px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-white;
125
115
  }
126
116
 
127
- .h-screen.flex .sidebar.close .icon-link .sts-sidebar-menu-item a.focus{
128
- @apply bg-white dark:bg-gray-900;
129
- }
130
-
131
- .h-screen.flex .sidebar.close .icon-link .sts-sidebar-menu-item:hover > [data-satis-sidebar-menu-item-target="submenu"] .sts-sidebar-menu-item.pl-4 {
132
- @apply py-0 px-0 ;
117
+ /* Show regular labels inside flyout (not submenu-label) */
118
+ .sidebar.close [data-satis-sidebar-menu-item-target="submenu"].flyout-visible .sts-sidebar-menu-item__label:not(.submenu-label) {
133
119
  display: block;
134
- visibility: visible;
135
- opacity: 100;
136
-
137
- }
138
-
139
- .sidebar.close .sts-sidebar-menu-item:hover > [data-satis-sidebar-menu-item-target="submenu"] .sts-sidebar-menu-item .sts-sidebar-menu-item__label {
140
120
  background: none;
141
- display:block;
142
- overflow: visible;
143
121
  padding: 0;
144
122
  }
145
123
 
146
-
147
- .h-screen.flex .sidebar.close .icon-link .sts-sidebar-menu-item:last-child:hover > [data-satis-sidebar-menu-item-target="submenu"] {
148
- margin-top: -425px;
149
- }
150
-
151
- .h-screen.flex .sidebar.close .sts-sidebar-menu-item:hover > [data-satis-sidebar-menu-item-target="submenu"] .sts-sidebar-menu-item:hover > [data-satis-sidebar-menu-item-target="submenu"] {
152
- position: fixed;
153
- margin-left: 185px;
154
- margin-top: -80px;
124
+ /* Reset focus style inside flyout */
125
+ .sidebar.close .icon-link .sts-sidebar-menu-item a.focus {
126
+ @apply bg-white dark:bg-gray-800;
155
127
  }
156
128
 
157
-
158
- .sidebar.close .sts-sidebar-menu-item:hover > [data-satis-sidebar-menu-item-target="submenu"]::-webkit-scrollbar {
159
- display: none;
129
+ .sidebar.close [data-satis-sidebar-menu-item-target="submenu"].flyout-visible .sts-sidebar-menu-item a.focus {
130
+ @apply bg-gray-100 dark:bg-gray-700;
160
131
  }
161
132
 
162
- .h-screen.flex .sidebar .icon-link .sts-sidebar-menu-item .submenu-label {
163
- display: none;
133
+ /* ── Tooltip label for items without submenus (shown via JS .tooltip-visible) ── */
134
+ .sidebar.close .sts-sidebar-menu-item__label.tooltip-visible {
135
+ @apply bg-gray-900 dark:bg-gray-700 text-white text-xs font-medium shadow-lg ring-1 ring-black/5 rounded-md;
136
+ display: block !important;
137
+ position: absolute;
138
+ left: 54px;
139
+ padding: 6px 12px;
140
+ white-space: nowrap;
141
+ z-index: 1100;
164
142
  }
165
143
 
166
- .h-screen.flex .sidebar.close .icon-link .sts-sidebar-menu-item .submenu-label {
167
- display: none;
144
+ /* Invisible bridge for tooltip */
145
+ .sidebar.close .sts-sidebar-menu-item__label.tooltip-visible::before {
146
+ content: '';
147
+ position: absolute;
148
+ top: 0;
149
+ left: -20px;
150
+ width: 20px;
151
+ bottom: 0;
168
152
  }
169
153
 
170
- .h-screen.flex .sidebar.close .icon-link .sts-sidebar-menu-item:hover > [data-satis-sidebar-menu-item-target="submenu"] .sts-sidebar-menu-item:hover .submenu-label {
171
- @apply text-white dark:bg-gray-600;
172
- display: block;
154
+ /* ── Submenu-label header (shown via JS .tooltip-visible) — flush with flyout ── */
155
+ .sidebar.close .submenu-label.tooltip-visible {
156
+ @apply bg-gray-900 dark:bg-gray-700 text-white text-xs font-semibold;
157
+ display: block !important;
173
158
  position: absolute;
174
- background-color: rgba(0, 0, 0, 1);
175
- padding: 5px 10px;
176
- border-radius: 4px;
177
- z-index: 10;
159
+ left: 54px;
160
+ padding: 6px 12px;
161
+ white-space: nowrap;
162
+ z-index: 1100;
178
163
  width: 220px;
179
- margin-left: 177px;
180
- margin-top: 0;
181
- margin-bottom: 150px;
164
+ border-radius: 0.5rem 0.5rem 0 0;
182
165
  }
183
166
 
184
- .h-screen.flex .sidebar.close .icon-link .sts-sidebar-menu-item:hover > [data-satis-sidebar-menu-item-target="submenu"] .sts-sidebar-menu-item:hover > [data-satis-sidebar-menu-item-target="submenu"] .sts-sidebar-menu-item .submenu-label {
185
- display: none;
167
+ /* Nested submenu-label tooltips position off the flyout edge */
168
+ .sidebar.close [data-satis-sidebar-menu-item-target="submenu"].flyout-visible .submenu-label.tooltip-visible {
169
+ @apply rounded-lg;
170
+ left: 100%;
171
+ width: max-content;
172
+ max-width: 220px;
186
173
  }
187
174
 
188
- .fontawesome-i2svg-active .h-screen.flex .sidebar.close .icon-link .sts-sidebar-menu-item:hover > [data-satis-sidebar-menu-item-target="submenu"] .sts-sidebar-menu-item:hover > [data-satis-sidebar-menu-item-target="submenu"] .sts-sidebar-menu-item:hover .submenu-label {
189
- @apply text-white dark:bg-gray-600;
190
- display: block;
191
- position: absolute;
192
- background-color: rgba(0, 0, 0, 1);
193
- padding: 5px 10px;
194
- border-radius: 4px;
195
- z-index: 10;
196
- width: 220px;
197
- margin-left: 177px;
198
- margin-bottom: 150px;
175
+ /* Nested tooltip labels shouldn't show (already visible in flyout) */
176
+ .sidebar.close [data-satis-sidebar-menu-item-target="submenu"].flyout-visible .sts-sidebar-menu-item__label.tooltip-visible {
177
+ display: none !important;
199
178
  }
@@ -15,18 +15,131 @@ export default class SidebarMenuItemComponentController extends ApplicationContr
15
15
 
16
16
  this.boundUpdateFocus = this.updateFocus.bind(this)
17
17
  this.boundOpenListener = this.openListener.bind(this)
18
+ this.boundShowFlyout = this.showFlyout.bind(this)
19
+ this.boundHideFlyout = this.hideFlyout.bind(this)
20
+ this.hideTimer = null
18
21
 
19
22
  this.updateFocus(true)
20
23
  this.element.addEventListener('sts-sidebar-menu-item:open', this.boundOpenListener)
24
+ this.element.addEventListener('mouseenter', this.boundShowFlyout)
25
+ this.element.addEventListener('mouseleave', this.boundHideFlyout)
21
26
  window.addEventListener('popstate', debounce(this.boundUpdateFocus, 200))
22
27
  }
23
28
 
24
29
  disconnect() {
25
30
  super.disconnect()
31
+ clearTimeout(this.hideTimer)
26
32
  this.element.removeEventListener('sts-sidebar-menu-item:open', this.boundOpenListener)
33
+ this.element.removeEventListener('mouseenter', this.boundShowFlyout)
34
+ this.element.removeEventListener('mouseleave', this.boundHideFlyout)
27
35
  window.removeEventListener('popstate', debounce(this.boundUpdateFocus, 200))
28
36
  }
29
37
 
38
+ // ── Collapsed sidebar flyout logic ──
39
+
40
+ get isSidebarClosed() {
41
+ const sidebar = this.element.closest('.sidebar')
42
+ return sidebar?.classList.contains('close')
43
+ }
44
+
45
+ get isInsideFlyout() {
46
+ return !!this.element.closest('[data-satis-sidebar-menu-item-target="submenu"].flyout-visible')
47
+ }
48
+
49
+ showFlyout() {
50
+ if (!this.isSidebarClosed) return
51
+
52
+ // Cancel any pending hide
53
+ clearTimeout(this.hideTimer)
54
+
55
+ if (this.hasSubmenuTarget) {
56
+ const submenuLabel = this.element.querySelector(':scope > .sts-sidebar-menu-item__link > .submenu-label')
57
+
58
+ this.submenuTarget.classList.add('flyout-visible')
59
+ if (submenuLabel) submenuLabel.classList.add('tooltip-visible')
60
+
61
+ this.positionFlyout(this.submenuTarget, submenuLabel)
62
+ } else if (!this.isInsideFlyout) {
63
+ const label = this.element.querySelector(':scope > .sts-sidebar-menu-item__link > .sts-sidebar-menu-item__label:not(.submenu-label)')
64
+ if (label) {
65
+ label.classList.add('tooltip-visible')
66
+ this.positionWithinViewport(label)
67
+ }
68
+ }
69
+ }
70
+
71
+ hideFlyout() {
72
+ if (!this.isSidebarClosed) return
73
+
74
+ // Delay hiding so the user can move to the flyout without it disappearing
75
+ this.hideTimer = setTimeout(() => {
76
+ if (this.hasSubmenuTarget) {
77
+ this.submenuTarget.classList.remove('flyout-visible')
78
+ this.submenuTarget.style.top = ''
79
+ }
80
+
81
+ this.element.querySelectorAll('.tooltip-visible').forEach(el => {
82
+ el.classList.remove('tooltip-visible')
83
+ el.style.top = ''
84
+ })
85
+ }, 150)
86
+ }
87
+
88
+ positionFlyout(flyout, label) {
89
+ requestAnimationFrame(() => {
90
+ const flyoutRect = flyout.getBoundingClientRect()
91
+ if (flyoutRect.height === 0) return
92
+
93
+ const viewportHeight = window.innerHeight
94
+ const margin = 8
95
+ const labelHeight = label ? label.getBoundingClientRect().height : 0
96
+
97
+ let flyoutTop = parseFloat(getComputedStyle(flyout).top) || 0
98
+
99
+ // Check if flyout overflows bottom
100
+ if (flyoutRect.bottom > viewportHeight - margin) {
101
+ const overflow = flyoutRect.bottom - viewportHeight + margin
102
+ flyoutTop -= overflow
103
+ }
104
+
105
+ // Check if label above flyout would overflow top
106
+ const parentRect = flyout.offsetParent?.getBoundingClientRect()
107
+ const parentTop = parentRect?.top || 0
108
+ if (parentTop + flyoutTop - labelHeight < margin) {
109
+ flyoutTop = margin - parentTop + labelHeight
110
+ }
111
+
112
+ flyout.style.top = `${flyoutTop}px`
113
+
114
+ // Position label flush against the top of the flyout panel (no gap)
115
+ if (label) {
116
+ label.style.top = `${flyoutTop - labelHeight}px`
117
+ }
118
+ })
119
+ }
120
+
121
+ positionWithinViewport(el) {
122
+ requestAnimationFrame(() => {
123
+ const rect = el.getBoundingClientRect()
124
+ if (rect.height === 0) return
125
+
126
+ const viewportHeight = window.innerHeight
127
+ const margin = 8
128
+
129
+ if (rect.bottom > viewportHeight - margin) {
130
+ const overflow = rect.bottom - viewportHeight + margin
131
+ const currentTop = parseFloat(getComputedStyle(el).top) || 0
132
+ el.style.top = `${currentTop - overflow}px`
133
+ }
134
+
135
+ if (rect.top < margin) {
136
+ el.style.top = `${margin - el.parentElement.getBoundingClientRect().top}px`
137
+ }
138
+ })
139
+ }
140
+
141
+ // ── Submenu expand/collapse (expanded sidebar) ──
142
+
30
143
  open(event) {
31
144
  if (this.hasSubmenuTarget) {
32
145
  const sidebar = this.element.closest('.sidebar')
@@ -43,8 +156,6 @@ export default class SidebarMenuItemComponentController extends ApplicationContr
43
156
  event.preventDefault()
44
157
  }
45
158
  }
46
- // This breaks turbo, so we need to keep the propagation.
47
- // event.stopPropagation();
48
159
  }
49
160
 
50
161
  openListener(event) {
@@ -54,7 +165,6 @@ export default class SidebarMenuItemComponentController extends ApplicationContr
54
165
  }
55
166
  }
56
167
 
57
- // This method is used to show the submenu
58
168
  showSubmenu() {
59
169
  if (!this.hasSubmenuTarget || this.isSubmenuVisible) return
60
170
 
@@ -62,7 +172,6 @@ export default class SidebarMenuItemComponentController extends ApplicationContr
62
172
  this.element.classList.toggle("active", true)
63
173
  }
64
174
 
65
- // This method is used to hide the submenu
66
175
  hideSubmenu() {
67
176
  if (!this.hasSubmenuTarget || !this.isSubmenuVisible) return
68
177
 
@@ -110,12 +219,7 @@ export default class SidebarMenuItemComponentController extends ApplicationContr
110
219
  return this.openSubmenus.length > 0
111
220
  }
112
221
 
113
- /**
114
- * Get a list of all open submenus
115
- * @returns {NodeListOf<Element>}
116
- */
117
222
  get openSubmenus() {
118
- // scope to first match. check if there are any submenus that are not hidden
119
223
  return this.element.querySelectorAll('[data-satis-sidebar-menu-item-target="submenu"]:not([class*="hidden"])')
120
224
  }
121
225
 
@@ -132,7 +132,7 @@ export default class FieldsForController extends ApplicationController {
132
132
  }
133
133
 
134
134
  monitorChanges(event) {
135
- if (event?.detail?.src == "satis-dropdown") {
135
+ if (event?.detail?.src == "satis-dropdown" || event?.detail?.src == "prepopulate" ) {
136
136
  // Skip events caused by the initial load of a satis-dropdown
137
137
  return
138
138
  }
@@ -6,6 +6,9 @@ application.register("satis-appearance-switcher", AppearanceSwitcherComponentCon
6
6
  import DateTimePickerComponentController from "satis/components/date_time_picker/component_controller";
7
7
  application.register("satis-date-time-picker", DateTimePickerComponentController);
8
8
 
9
+ import CardComponentController from "satis/components/card/component_controller";
10
+ application.register("satis-card", CardComponentController);
11
+
9
12
  import DropdownComponentController from "satis/components/dropdown/component_controller";
10
13
  application.register("satis-dropdown", DropdownComponentController);
11
14
 
@@ -20,7 +20,7 @@ export default class extends ApplicationController {
20
20
  this.boundUpdate = this.update.bind(this)
21
21
 
22
22
  this.watchOn = "input"
23
- if (this.inputTarget.type == "hidden") {
23
+ if (this.inputTarget.type == "hidden" || this.inputTarget.type == "select-one") {
24
24
  this.watchOn = "change"
25
25
  }
26
26
 
data/lib/satis/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Satis
2
- VERSION = "2.2.1"
2
+ VERSION = "2.2.2"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: satis
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.1
4
+ version: 2.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tom de Grunt
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-09-18 00:00:00.000000000 Z
11
+ date: 2026-05-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: browser
@@ -196,10 +196,7 @@ files:
196
196
  - Rakefile
197
197
  - TODO.md
198
198
  - app/assets/config/satis_manifest.js
199
- - app/assets/fontawesome/brands.js
200
- - app/assets/fontawesome/fontawesome.js
201
199
  - app/assets/fontawesome/regular.js
202
- - app/assets/fontawesome/solid.js
203
200
  - app/assets/images/satis/.keep
204
201
  - app/assets/images/satis/flags/1x1/ac.svg
205
202
  - app/assets/images/satis/flags/1x1/ad.svg
@@ -770,6 +767,7 @@ files:
770
767
  - app/components/satis/card/component.css
771
768
  - app/components/satis/card/component.html.slim
772
769
  - app/components/satis/card/component.rb
770
+ - app/components/satis/card/component_controller.js
773
771
  - app/components/satis/color_picker/component.css
774
772
  - app/components/satis/color_picker/component.rb
775
773
  - app/components/satis/color_picker/component.slim