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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +92 -0
- data/Rakefile +23 -0
- data/app/assets/config/satis_manifest.js +1 -0
- data/app/assets/stylesheets/satis/application.css +15 -0
- data/app/components/satis/appearance_switcher/component.html.slim +6 -0
- data/app/components/satis/appearance_switcher/component.rb +11 -0
- data/app/components/satis/appearance_switcher/component.scss +34 -0
- data/app/components/satis/appearance_switcher/component_controller.js +62 -0
- data/app/components/satis/application_component.rb +50 -0
- data/app/components/satis/avatar/component.html.slim +7 -0
- data/app/components/satis/avatar/component.rb +52 -0
- data/app/components/satis/breadcrumbs/component.html.slim +8 -0
- data/app/components/satis/breadcrumbs/component.rb +23 -0
- data/app/components/satis/breadcrumbs/component.scss +19 -0
- data/app/components/satis/breadcrumbs/crumb.slim +8 -0
- data/app/components/satis/card/component.html.slim +54 -0
- data/app/components/satis/card/component.md +14 -0
- data/app/components/satis/card/component.rb +41 -0
- data/app/components/satis/card/component.scss +15 -0
- data/app/components/satis/date_time_picker/component.html.slim +48 -0
- data/app/components/satis/date_time_picker/component.md +11 -0
- data/app/components/satis/date_time_picker/component.rb +48 -0
- data/app/components/satis/date_time_picker/component.scss +5 -0
- data/app/components/satis/date_time_picker/component_controller.js +499 -0
- data/app/components/satis/dropdown/component.html.slim +36 -0
- data/app/components/satis/dropdown/component.md +48 -0
- data/app/components/satis/dropdown/component.rb +77 -0
- data/app/components/satis/dropdown/component.scss +10 -0
- data/app/components/satis/dropdown/component_controller.js +547 -0
- data/app/components/satis/flash_messages/component.html.slim +3 -0
- data/app/components/satis/flash_messages/component.rb +31 -0
- data/app/components/satis/flash_messages/component.scss +18 -0
- data/app/components/satis/flash_messages/message.html.slim +8 -0
- data/app/components/satis/info/component.html.slim +4 -0
- data/app/components/satis/info/component.rb +22 -0
- data/app/components/satis/info_item/component.html.slim +7 -0
- data/app/components/satis/info_item/component.rb +19 -0
- data/app/components/satis/input/component.html.slim +11 -0
- data/app/components/satis/input/component.rb +38 -0
- data/app/components/satis/input/component.scss +50 -0
- data/app/components/satis/input/element.html.slim +2 -0
- data/app/components/satis/map/component.html.slim +2 -0
- data/app/components/satis/map/component.rb +17 -0
- data/app/components/satis/map/component.scss +9 -0
- data/app/components/satis/map/component_controller.js +37 -0
- data/app/components/satis/menu/component.html.slim +13 -0
- data/app/components/satis/menu/component.md +1 -0
- data/app/components/satis/menu/component.rb +16 -0
- data/app/components/satis/menu/component_controller.js +62 -0
- data/app/components/satis/menu_item/component.html.slim +16 -0
- data/app/components/satis/menu_item/component.rb +14 -0
- data/app/components/satis/page/component.html.slim +45 -0
- data/app/components/satis/page/component.rb +15 -0
- data/app/components/satis/page/component_controller.js +86 -0
- data/app/components/satis/sidebar_menu/component.html.slim +3 -0
- data/app/components/satis/sidebar_menu/component.rb +17 -0
- data/app/components/satis/sidebar_menu/component.scss +0 -0
- data/app/components/satis/sidebar_menu/component_controller.js +9 -0
- data/app/components/satis/sidebar_menu/mobile/component.html.slim +3 -0
- data/app/components/satis/sidebar_menu/mobile/component.rb +10 -0
- data/app/components/satis/sidebar_menu_item/component.html.slim +15 -0
- data/app/components/satis/sidebar_menu_item/component.rb +20 -0
- data/app/components/satis/sidebar_menu_item/component.scss +27 -0
- data/app/components/satis/sidebar_menu_item/component_controller.js +62 -0
- data/app/components/satis/sidebar_menu_item/mobile/component.html.slim +17 -0
- data/app/components/satis/sidebar_menu_item/mobile/component.rb +10 -0
- data/app/components/satis/switch/component.html.slim +14 -0
- data/app/components/satis/switch/component.rb +24 -0
- data/app/components/satis/switch/component_controller.js +49 -0
- data/app/components/satis/tab/component.rb +35 -0
- data/app/components/satis/tabs/component.html.slim +23 -0
- data/app/components/satis/tabs/component.md +21 -0
- data/app/components/satis/tabs/component.rb +16 -0
- data/app/components/satis/tabs/component.scss +33 -0
- data/app/components/satis/tabs/component_controller.js +123 -0
- data/app/controllers/satis/application_controller.rb +4 -0
- data/app/helpers/satis/application_helper.rb +15 -0
- data/app/jobs/satis/application_job.rb +4 -0
- data/app/mailers/satis/application_mailer.rb +6 -0
- data/app/models/satis/application_record.rb +5 -0
- data/app/views/shared/_fields_for.html.slim +35 -0
- data/config/routes.rb +5 -0
- data/lib/satis/action_controller_helpers.rb +29 -0
- data/lib/satis/configuration.rb +61 -0
- data/lib/satis/engine.rb +27 -0
- data/lib/satis/forms/builder.rb +440 -0
- data/lib/satis/forms/concerns/buttons.rb +49 -0
- data/lib/satis/forms/concerns/file.rb +35 -0
- data/lib/satis/forms/concerns/options.rb +44 -0
- data/lib/satis/forms/concerns/required.rb +68 -0
- data/lib/satis/forms/concerns/select.rb +95 -0
- data/lib/satis/helpers/container.rb +83 -0
- data/lib/satis/menus/builder.rb +13 -0
- data/lib/satis/menus/item.rb +34 -0
- data/lib/satis/menus/menu.rb +23 -0
- data/lib/satis/version.rb +3 -0
- data/lib/satis.rb +36 -0
- data/lib/tasks/satis_tasks.rake +4 -0
- metadata +213 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 3dffc5ab90aa3d26440c35d5ca251870e5f5c6863706108380fd117629133f2e
|
|
4
|
+
data.tar.gz: 72a53d8079ca58762c1e7de4b5a91f8d7be3898fbf79d1f2f4749772ea0d9aab
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 6bc1e23b0a4197f01e9b976ab27e63e07da645db958b0e466a87efe4a10bb4c830a8f5b4d18a573959a046039a17a7544aec76f1a31856e4247b0b649066a45c
|
|
7
|
+
data.tar.gz: 9532fbe01e4f4b2a8ba8222276a9afdd87209c0ac660a7f3b2a5efae80a43db1c17978dc4b615a83a74c300f7f1096732e72d6ed610def3753b37db828caa699
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright 2021 Tom de Grunt
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Satis
|
|
2
|
+
|
|
3
|
+
Tailwind CSS based UI framework for Rails.
|
|
4
|
+
We use:
|
|
5
|
+
|
|
6
|
+
- [TailwindCSS](https://tailwindcss.com)
|
|
7
|
+
- [TailwindUI](https://tailwindui.com)
|
|
8
|
+
- [FontAwesome 6](https://fontawesome.com/v6.0/)
|
|
9
|
+
- [ViewComponent](https://viewcomponent.org)
|
|
10
|
+
- [HotWired](https://hotwired.dev)
|
|
11
|
+
- [BEM](https://cssguidelin.es/#bem-like-naming)
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
You can use satis helpers in your own helpers:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
def mycard(&block)
|
|
19
|
+
sts.card(icon: 'fad fa-user', title: "Profile", &block)
|
|
20
|
+
end
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
and then in your template:
|
|
24
|
+
|
|
25
|
+
```slim
|
|
26
|
+
= mycard do |card|
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Components
|
|
30
|
+
|
|
31
|
+
Each component has it's own documentation in the component folder.
|
|
32
|
+
Other engines can add components to Satis too:
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
Satis.add_helper :name, ViewComponent::Class
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Forms
|
|
39
|
+
|
|
40
|
+
```slim
|
|
41
|
+
= sts.form_with model: @user, url: profile_url, class: 'mt-2' do |f|
|
|
42
|
+
= f.input :id, as: :hidden
|
|
43
|
+
= f.input :first_name
|
|
44
|
+
= f.input :last_name
|
|
45
|
+
= f.association :account, collection: policy_scope(Account).with(@user.account_id), as: :dropdown
|
|
46
|
+
= f.input :location_id, url: select_locations_url(format: :html), as: :dropdown, hint: "The user's main location"
|
|
47
|
+
|
|
48
|
+
= f.button
|
|
49
|
+
= f.submit
|
|
50
|
+
= f.reset
|
|
51
|
+
= f.continue
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Browser detection
|
|
55
|
+
|
|
56
|
+
Satis now includes browser detection using the browser gem, you can use it in controllers and in your views:
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
sts.browser.chrome?
|
|
60
|
+
sts.browser.mobile?
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
For more information see the [browser gem](https://github.com/fnando/browser)
|
|
64
|
+
|
|
65
|
+
## Dark
|
|
66
|
+
|
|
67
|
+
bg-gray-800 - hoofd achtergrond card/sidebar
|
|
68
|
+
bg-gray-700 - highlight card/sidebar / hover
|
|
69
|
+
text-gray-300 - tekst kleur
|
|
70
|
+
bg-gray-600 - body achtergrond kleur
|
|
71
|
+
|
|
72
|
+
## Known issues
|
|
73
|
+
|
|
74
|
+
- dropdown results will not overlap the card, they should, just like date-time picker
|
|
75
|
+
- dropdown triggers on-change upon initial population (for attributes), which is different from select's
|
|
76
|
+
- dropdown hoogte van results is niet altijd goed
|
|
77
|
+
- table state is not saved
|
|
78
|
+
- table columns removing is weird, you really need to be on the left part of the screen to drag
|
|
79
|
+
- table filters initially passed should not be editable
|
|
80
|
+
- sidebar has no small / collapsed version
|
|
81
|
+
|
|
82
|
+
## Installation
|
|
83
|
+
|
|
84
|
+
Add this line to your application's Gemfile:
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
gem 'satis'
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## License
|
|
91
|
+
|
|
92
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
require "bundler/setup"
|
|
2
|
+
|
|
3
|
+
APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
|
|
4
|
+
load "rails/tasks/engine.rake"
|
|
5
|
+
|
|
6
|
+
load "rails/tasks/statistics.rake"
|
|
7
|
+
|
|
8
|
+
require "bundler/gem_tasks"
|
|
9
|
+
|
|
10
|
+
require "rake/testtask"
|
|
11
|
+
|
|
12
|
+
Rake::TestTask.new(:test) do |t|
|
|
13
|
+
t.libs << "test"
|
|
14
|
+
t.pattern = "test/**/*_test.rb"
|
|
15
|
+
t.verbose = false
|
|
16
|
+
t.warning = false
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
task default: :test
|
|
20
|
+
|
|
21
|
+
# Adds the Auxilium semver task
|
|
22
|
+
spec = Gem::Specification.find_by_name "auxilium"
|
|
23
|
+
load "#{spec.gem_dir}/lib/tasks/semver.rake"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//= link_directory ../stylesheets/satis .css
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
|
3
|
+
* listed below.
|
|
4
|
+
*
|
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
|
7
|
+
*
|
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
|
10
|
+
* files in this directory. Styles in this file should be added after the last require_* statement.
|
|
11
|
+
* It is generally better to create a new file per style scope.
|
|
12
|
+
*
|
|
13
|
+
*= require_tree .
|
|
14
|
+
*= require_self
|
|
15
|
+
*/
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
.sts-appearance-switcher data-controller="satis-appearance-switcher" data-action='click->satis-appearance-switcher#switch'
|
|
2
|
+
.sts-appearance-switcher__theme.sts-appearance-switcher__theme_light data-satis-appearance-switcher-target="light"
|
|
3
|
+
i.fa-solid.fa-sun.sts-appearance-switcher__icon.sts-appearance-switcher__icon_light
|
|
4
|
+
.sts-appearance-switcher__theme.sts-appearance-switcher__theme_dark data-satis-appearance-switcher-target="dark"
|
|
5
|
+
i.fa-solid.fa-moon-stars.sts-appearance-switcher__icon.sts-appearance-switcher__icon_dark
|
|
6
|
+
.sts-appearance-switcher__label Switch theme
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
.sts-appearance-switcher {
|
|
2
|
+
@apply w-8 h-8 bg-white rounded-full text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 dark:bg-gray-900 dark:border-gray-700 overflow-hidden relative;
|
|
3
|
+
text-align: center;
|
|
4
|
+
|
|
5
|
+
&__theme {
|
|
6
|
+
@apply absolute;
|
|
7
|
+
|
|
8
|
+
&_light {
|
|
9
|
+
top: 5px;
|
|
10
|
+
left: 7px;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
&_dark {
|
|
14
|
+
top: 4px;
|
|
15
|
+
left: 8px;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
&__icon {
|
|
20
|
+
font-size: larger;
|
|
21
|
+
|
|
22
|
+
&_light {
|
|
23
|
+
@apply text-yellow-500;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
&_dark {
|
|
27
|
+
@apply text-indigo-600;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
&__label {
|
|
32
|
+
@apply sr-only;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import ApplicationController from "../../../../frontend/controllers/application_controller"
|
|
2
|
+
import { getInitialTheme } from "../../../../frontend/utils"
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Theme controller
|
|
6
|
+
*
|
|
7
|
+
* div data-controller="satis-appearance-switcher" data-action='click->satis-appearance-switcher#switch'
|
|
8
|
+
* i.fal.fa-sun data-satis-appearance-switcher-target="light"
|
|
9
|
+
* i.fal.fa-moon-stars data-satis-appearance-switcher-target="dark"
|
|
10
|
+
*
|
|
11
|
+
*/
|
|
12
|
+
export default class extends ApplicationController {
|
|
13
|
+
static targets = ["light", "dark"]
|
|
14
|
+
|
|
15
|
+
connect() {
|
|
16
|
+
super.connect()
|
|
17
|
+
|
|
18
|
+
const theme = getInitialTheme()
|
|
19
|
+
this.rawSetTheme(theme, false)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
switch() {
|
|
23
|
+
const theme = getInitialTheme()
|
|
24
|
+
|
|
25
|
+
if (theme == "dark") {
|
|
26
|
+
this.rawSetTheme("light", true)
|
|
27
|
+
} else {
|
|
28
|
+
this.rawSetTheme("dark", true)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
rawSetTheme(rawTheme, delay) {
|
|
33
|
+
const root = window.document.documentElement
|
|
34
|
+
const eventLight = new CustomEvent('theme-change', { detail: { theme: 'light' } });
|
|
35
|
+
const eventDark = new CustomEvent('theme-change', { detail: { theme: 'dark' } });
|
|
36
|
+
const isDark = rawTheme === "dark"
|
|
37
|
+
|
|
38
|
+
if (delay == true) {
|
|
39
|
+
this.lightTarget.classList.add("transition", "ease-in-out", "duration-1000")
|
|
40
|
+
this.darkTarget.classList.add("transition", "ease-in-out", "duration-1000")
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (isDark) {
|
|
44
|
+
window.dispatchEvent(eventDark);
|
|
45
|
+
this.lightTarget.classList.add("transform", "translate-y-7")
|
|
46
|
+
this.darkTarget.classList.remove("transform", "-translate-y-7")
|
|
47
|
+
} else {
|
|
48
|
+
window.dispatchEvent(eventLight);
|
|
49
|
+
this.lightTarget.classList.remove("transform", "translate-y-7")
|
|
50
|
+
this.darkTarget.classList.add("transform", "-translate-y-7")
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
localStorage.setItem("color-theme", rawTheme)
|
|
54
|
+
setTimeout(
|
|
55
|
+
() => {
|
|
56
|
+
root.classList.remove(isDark ? "light" : "dark")
|
|
57
|
+
root.classList.add(rawTheme)
|
|
58
|
+
},
|
|
59
|
+
delay ? 500 : 0
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Satis
|
|
4
|
+
class ApplicationComponent < ViewComponent::Base
|
|
5
|
+
include ViewComponent::SlotableV2
|
|
6
|
+
include ActionView::Helpers::TranslationHelper
|
|
7
|
+
|
|
8
|
+
attr_accessor :original_view_context
|
|
9
|
+
|
|
10
|
+
#
|
|
11
|
+
# This provides us with a translation helper which scopes into the original view
|
|
12
|
+
# and thereby conveniently scopes the translations.
|
|
13
|
+
#
|
|
14
|
+
# In your component.html.slim you can use:
|
|
15
|
+
# ```slim
|
|
16
|
+
# = ct(".#{tab.name}", scope: [group.to_sym])
|
|
17
|
+
# ````
|
|
18
|
+
#
|
|
19
|
+
# It'll then try and find a translation with scope: en.admin.spaces.edit.tabs.main.admin_versions
|
|
20
|
+
#
|
|
21
|
+
def ct(key = nil, **options)
|
|
22
|
+
scope = Array.wrap(options.delete(:scope))
|
|
23
|
+
|
|
24
|
+
scope = if scope
|
|
25
|
+
scope.unshift(i18n_scope)
|
|
26
|
+
else
|
|
27
|
+
[i18n_scope]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
scope = original_i18n_scope.concat(scope)
|
|
31
|
+
|
|
32
|
+
key = key&.to_s unless key.is_a?(String)
|
|
33
|
+
key = "#{scope.join('.')}#{key}" if key.start_with?('.')
|
|
34
|
+
|
|
35
|
+
original_view_context.t(key, **options)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def original_virtual_path
|
|
39
|
+
original_view_context.instance_variable_get(:@virtual_path)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def original_i18n_scope
|
|
43
|
+
original_virtual_path.sub(%r{^/}, '').gsub(%r{/_?}, '.').split('.')
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def i18n_scope
|
|
47
|
+
self.class.name.split('::').second.underscore.to_sym
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
button.inline-flex.items-center.justify-center.rounded-full.bg-gray-500.focus:outline-none.focus:ring-2.focus:ring-offset-2.focus:ring-primary-500 aria-expanded="false" aria-haspopup="true" type="button" class=options[:class]
|
|
2
|
+
- if photo&.attached?
|
|
3
|
+
img class="rounded-full" src=photo_url alt=name class=options[:class]
|
|
4
|
+
- elsif gravatar?
|
|
5
|
+
img class="rounded-full" src=gravatar_url alt=name class=options[:class]
|
|
6
|
+
- else
|
|
7
|
+
span.text-sm.font-medium.leading-none.text-white = initials
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Satis
|
|
4
|
+
module Avatar
|
|
5
|
+
class Component < Satis::ApplicationComponent
|
|
6
|
+
attr_reader :name, :photo, :email, :options
|
|
7
|
+
|
|
8
|
+
def initialize(name: nil, email: nil, photo: nil, **options)
|
|
9
|
+
super
|
|
10
|
+
@name = name
|
|
11
|
+
@photo = photo
|
|
12
|
+
@options = options
|
|
13
|
+
@options[:class] ||= 'w-8 h-8'
|
|
14
|
+
@email = email
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def initials
|
|
18
|
+
if name.present? && !name.index('@')
|
|
19
|
+
name.scan(/[A-Z]/)[0..1].join
|
|
20
|
+
else
|
|
21
|
+
(name || email).split('@').map(&:capitalize).join('@').scan(/[A-Z]/)[0..1].join
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def photo_url
|
|
26
|
+
return unless photo&.attached?
|
|
27
|
+
|
|
28
|
+
helpers.main_app.url_for(photo)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def gravatar?
|
|
32
|
+
return false if email.blank?
|
|
33
|
+
|
|
34
|
+
url = "https://www.gravatar.com/avatar/#{Digest::MD5.hexdigest(email).downcase}?d=404"
|
|
35
|
+
|
|
36
|
+
uri = URI.parse(url)
|
|
37
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
38
|
+
http.use_ssl = true if uri.scheme == 'https'
|
|
39
|
+
|
|
40
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
|
41
|
+
request.add_field('User-Agent', controller.request.user_agent)
|
|
42
|
+
response = http.request(request)
|
|
43
|
+
|
|
44
|
+
response.code.to_i != 404
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def gravatar_url
|
|
48
|
+
"https://www.gravatar.com/avatar/#{Digest::MD5.hexdigest(email).downcase}?d=404" if gravatar?
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Satis
|
|
4
|
+
module Breadcrumbs
|
|
5
|
+
class Crumb < ViewComponent::Base
|
|
6
|
+
attr_reader :path, :title, :icon
|
|
7
|
+
|
|
8
|
+
def initialize(path:, title: nil, icon: nil)
|
|
9
|
+
@path = path
|
|
10
|
+
@title = title
|
|
11
|
+
@icon = icon
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class Component < Satis::ApplicationComponent
|
|
16
|
+
|
|
17
|
+
renders_many :crumbs, Crumb
|
|
18
|
+
def initialize
|
|
19
|
+
super
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
.sts-breadcrumbs {
|
|
2
|
+
@apply bg-white border-b border-gray-200 flex mb-4 sm:rounded-lg sm:shadow dark:text-gray-300 dark:bg-gray-800 dark:border-gray-700;
|
|
3
|
+
|
|
4
|
+
height: 40px;
|
|
5
|
+
|
|
6
|
+
&__list {
|
|
7
|
+
@apply max-w-screen-xl w-full mx-auto px-4 flex space-x-4 sm:px-6 lg:px-8;
|
|
8
|
+
|
|
9
|
+
margin-left: 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
&__link {
|
|
13
|
+
@apply ml-4 text-sm font-normal text-gray-400 hover:text-gray-500 dark:text-gray-400 dark:hover:text-gray-300;
|
|
14
|
+
|
|
15
|
+
svg {
|
|
16
|
+
margin-right: 0.5rem;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
li.flex
|
|
2
|
+
.flex.items-center
|
|
3
|
+
svg.flex-shrink-0.w-6.h-full.text-gray-200.dark:text-gray-700 aria-hidden="true" fill="currentColor" preserveaspectratio="none" viewbox=("0 0 24 44") xmlns="http://www.w3.org/2000/svg"
|
|
4
|
+
path d=("M.293 0l22 22-22 22h1.414l22-22-22-22H.293z") /
|
|
5
|
+
a.sts-breadcrumbs__link href=path
|
|
6
|
+
- if icon
|
|
7
|
+
i class=icon
|
|
8
|
+
= title
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
.sts-card.sts-tabs data-controller="satis-tabs" data-satis-tabs-persist-value="false"
|
|
2
|
+
- if header?
|
|
3
|
+
.sts-card__header class="#{tabs? ? '' : 'border-b border-gray-200'} #{header_background_color[:light]} dark:#{header_background_color[:dark]}"
|
|
4
|
+
.-ml-4.-mt-4.flex.justify-between.items-center.flex-wrap.sm:flex-nowrap
|
|
5
|
+
- if icon
|
|
6
|
+
.ml-4.mt-4.flex-shrink-0.text-primary-600.dark:text-gray-300
|
|
7
|
+
i class=icon
|
|
8
|
+
.ml-4.mt-4.flex-1
|
|
9
|
+
h3.text-lg.leading-6.font-medium.text-gray-900.dark:text-gray-300
|
|
10
|
+
== title
|
|
11
|
+
- if description.present?
|
|
12
|
+
p.mt-1.text-sm.text-gray-500.dark:text-gray-500
|
|
13
|
+
== description
|
|
14
|
+
|
|
15
|
+
- if actions.present? || initial_actions.present?
|
|
16
|
+
.ml-4.mt-4.flex-shrink-0
|
|
17
|
+
.grid.grid-flow-row.gap-1.sm:grid-flow-col
|
|
18
|
+
- for action in initial_actions
|
|
19
|
+
= action
|
|
20
|
+
- for action in actions
|
|
21
|
+
= action
|
|
22
|
+
|
|
23
|
+
- if menu
|
|
24
|
+
.ml-4.mt-4.flex-shrink-0
|
|
25
|
+
= render(Satis::Menu::Component.new(menu))
|
|
26
|
+
|
|
27
|
+
- if tabs?
|
|
28
|
+
.sts-card__tabs
|
|
29
|
+
.sm:hidden
|
|
30
|
+
label.sr-only for="tabs" Select a tab
|
|
31
|
+
select#tabs.block.w-full.pl-3.pr-10.py-2.text-base.border-gray-300.focus:outline-none.focus:ring-primary-500.focus:border-primary-500.sm:text-sm.rounded-md name="tabs" data-action="change->satis-tabs#select" data-satis-tabs-target="select"
|
|
32
|
+
- tabs.each do |tab|
|
|
33
|
+
option selected=tab.selected? = t(tab.name, scope: [:tabs])
|
|
34
|
+
.hidden.sm:block
|
|
35
|
+
nav.-mb-px.flex.space-x-8.overflow-x-auto aria-label="Tabs"
|
|
36
|
+
- tabs.each do |tab|
|
|
37
|
+
a.tab id="#{tab.name}" href="#" aria-current="#{tab.selected? ? "page" : ''}" data-satis-tabs-target="tab" data-action="click->satis-tabs#select"
|
|
38
|
+
- if tab.icon
|
|
39
|
+
i.mr-2 class=tab.icon
|
|
40
|
+
= t(tab.name, scope: [:tabs], default: tab.title || tab.name)
|
|
41
|
+
i.fal.fa-triangle-exclamation.ml-2.hidden
|
|
42
|
+
- if tab.badge
|
|
43
|
+
span.badge
|
|
44
|
+
= tab.badge
|
|
45
|
+
|
|
46
|
+
- tabs.each do |tab|
|
|
47
|
+
div id="#{tab.name}-content" class="tab-content #{tab.options[:padding] == true ? 'px-6 py-6' : ''}" data-satis-tabs-target="content"
|
|
48
|
+
= tab.to_s
|
|
49
|
+
- else
|
|
50
|
+
div class="#{content_padding ? 'px-6 py-6' : ''}"
|
|
51
|
+
= content
|
|
52
|
+
|
|
53
|
+
- if footer
|
|
54
|
+
= footer
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Card
|
|
2
|
+
|
|
3
|
+
## UI
|
|
4
|
+
|
|
5
|
+
https://tailwindui.com/components/application-ui/headings/card-headings
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
```slim
|
|
10
|
+
= sts.card title: 'Your profile', description: 'Edit your profile information' do |c|
|
|
11
|
+
- c.action
|
|
12
|
+
button Save
|
|
13
|
+
| Content here
|
|
14
|
+
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Satis
|
|
4
|
+
module Card
|
|
5
|
+
class Component < Satis::ApplicationComponent
|
|
6
|
+
renders_many :actions
|
|
7
|
+
renders_many :tabs, Tab::Component
|
|
8
|
+
renders_one :footer
|
|
9
|
+
|
|
10
|
+
attr_reader :icon, :title, :description, :menu, :content_padding, :header_background_color, :initial_actions
|
|
11
|
+
|
|
12
|
+
def initialize(icon: nil,
|
|
13
|
+
title: nil,
|
|
14
|
+
description: nil,
|
|
15
|
+
menu: nil,
|
|
16
|
+
content_padding: true,
|
|
17
|
+
header_background_color: {
|
|
18
|
+
dark: 'bg-gray-800', light: 'bg-white'
|
|
19
|
+
},
|
|
20
|
+
actions: [])
|
|
21
|
+
super
|
|
22
|
+
@title = title
|
|
23
|
+
@title = @title.reject(&:blank?).compact.join(' ') if @title.is_a?(Array)
|
|
24
|
+
@description = description
|
|
25
|
+
@icon = icon
|
|
26
|
+
@menu = menu
|
|
27
|
+
@content_padding = content_padding
|
|
28
|
+
@header_background_color = header_background_color
|
|
29
|
+
@initial_actions = actions
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def tabs?
|
|
33
|
+
tabs.present?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def header?
|
|
37
|
+
icon.present? || title.present? || description.present? || menu
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
.sts-card {
|
|
2
|
+
@apply bg-white sm:rounded-lg sm:shadow dark:bg-gray-800 overflow-hidden;
|
|
3
|
+
|
|
4
|
+
&__header {
|
|
5
|
+
@apply px-4 py-5 sm:px-6 dark:border-gray-700
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
&__tabs {
|
|
9
|
+
@apply bg-white px-4 border-b border-gray-200 sm:px-5 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-300
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
a.tab:not(.selected) {
|
|
13
|
+
@apply dark:text-gray-300;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
div.satis-date-time-picker data-controller="satis-date-time-picker" data-satis-date-time-picker-time-picker-value=time_picker.to_s data-satis-date-time-picker-clearable-value=clearable.to_s data-satis-date-time-picker-locale-value="" data-satis-date-time-picker-range-value=range.to_s data-satis-date-time-picker-multiple-value=multiple.to_s data-satis-date-time-picker-week-start-value=week_start data-satis-date-time-picker-inline-value=inline.to_s data-satis-date-time-picker-format-value=JSON.dump(format)
|
|
2
|
+
.relative.flex.items-center
|
|
3
|
+
= form.text_field attribute, options[:input_html].merge(class: 'hidden')
|
|
4
|
+
input.form-control data-action="focus->satis-date-time-picker#showCalendar input->satis-date-time-picker#dateTimeEntered" data-satis-date-time-picker-target="input"
|
|
5
|
+
.absolute.inset-y-0.right-0.flex.py-1.5.pr-1.5
|
|
6
|
+
button.cursor-pointer.w-6.h-full.flex.items-center.text-gray-400.outline-none.focus:outline-none data-satis-date-time-picker-target="clearButton" data-action="click->satis-date-time-picker#clear"
|
|
7
|
+
i.fas.fa-xmark
|
|
8
|
+
|
|
9
|
+
.container.z-10.shadow.bg-white.border.border-gray-300.dark:bg-gray-800.dark:border-gray-700.rounded.p-4.w-72 class="#{inline ? 'inline-block' : 'hidden'}" data-satis-date-time-picker-target="calendarView"
|
|
10
|
+
.flex.justify-between.items-center.mb-2
|
|
11
|
+
div
|
|
12
|
+
span.text-lg.font-bold.text-gray-800.dark:text-gray-200 data-satis-date-time-picker-target="month"
|
|
13
|
+
span.ml-1.text-lg.text-gray-600.dark:text-gray-200.font-normal data-satis-date-time-picker-target="year"
|
|
14
|
+
div
|
|
15
|
+
button type="button" class="transition ease-in-out duration-100 inline-flex cursor-pointer hover:bg-gray-200 p-2 rounded-full" data-action="satis-date-time-picker#previousMonth"
|
|
16
|
+
i.fal.fa-angle-left.text-gray-500.inline-flex.px-2.py-1
|
|
17
|
+
button type="button" class="transition ease-in-out duration-100 inline-flex cursor-pointer hover:bg-gray-200 p-1 rounded-full" data-action="satis-date-time-picker#nextMonth"
|
|
18
|
+
i.fal.fa-angle-right.text-gray-500.inline-flex.px-2.py-1
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
.grid.grid-cols-7 data-satis-date-time-picker-target="weekDays"
|
|
22
|
+
template data-satis-date-time-picker-target="weekDayTemplate"
|
|
23
|
+
div.px-2.w-8
|
|
24
|
+
div.text-gray-800.dark:text-gray-200.font-medium.text-center.text-xs ${name}
|
|
25
|
+
|
|
26
|
+
.grid.grid-cols-7 data-satis-date-time-picker-target="days"
|
|
27
|
+
|
|
28
|
+
- if time_picker
|
|
29
|
+
.flex.items-center.py-2.space-x-2
|
|
30
|
+
label.flex-grow.text-sm.text-gray-500 Time
|
|
31
|
+
.flex.items-center.space-x-2
|
|
32
|
+
.bg-gray-100.rounded-md.w-full.text-right.flex.items-center.border.border-gray-100.focus:border-primary-500.focus:ring-2.focus:ring-primary-500.focus:outline-none.focus:ring-opacity-50 style=("caret-color: transparent;")
|
|
33
|
+
input.text-center.w-8.border-transparent.bg-transparent.p-0.h-6.text-sm.transition.duration-100.ease-in-out.border.border-transparent.focus:border-primary-500.focus:ring-2.focus:ring-primary-500.focus:outline-none.focus:ring-opacity-50.rounded inputmode="numeric" type="text" data-satis-date-time-picker-target="hours" data-action="satis-date-time-picker#changeHours keypress->satis-date-time-picker#keyPress"
|
|
34
|
+
span contenteditable="false"
|
|
35
|
+
| :
|
|
36
|
+
input.text-center.w-8.border-transparent.bg-transparent.p-0.h-6.text-sm.transition.duration-100.ease-in-out.border.border-transparent.focus:border-primary-500.focus:ring-2.focus:ring-primary-500.focus:outline-none.focus:ring-opacity-50.rounded inputmode="numeric" type="text" data-satis-date-time-picker-target="minutes" data-action="satis-date-time-picker#changeMinutes keypress->satis-date-time-picker#keyPress"
|
|
37
|
+
/span.relative.inline-flex.flex-shrink-0.transition.duration-200.ease-in-out.bg-gray-100.border.border-transparent.rounded.cursor-pointer.focus:border-primary-500.focus:ring-2.focus:ring-primary-500.focus:outline-none.focus:ring-opacity-50 aria-checked="false" role="checkbox" tabindex="0"
|
|
38
|
+
input type="hidden" value="AM" /
|
|
39
|
+
span.flex.items-center.justify-center.w-6.h-6.text-xs.text-gray-500.rounded-sm aria-hidden="true" AM
|
|
40
|
+
span.flex.items-center.justify-center.w-6.h-6.text-xs.text-gray-500.rounded-sm aria-hidden="true" PM
|
|
41
|
+
span.absolute.flex.items-center.justify-center.w-6.h-6.text-xs.text-gray-800.transition.duration-200.ease-in-out.transform.translate-x-0.bg-white.rounded.shadow aria-hidden="true" AM
|
|
42
|
+
/a.text-primary-600.text-sm.uppercase.font-semibold.transition.duration-100.ease-in-out.border.border-transparent.focus:border-primary-500.focus:ring-2.focus:ring-primary-500.focus:outline-none.focus:ring-opacity-50.rounded.cursor-pointer href="#" Ok
|
|
43
|
+
|
|
44
|
+
template data-satis-date-time-picker-target="emtpyTemplate"
|
|
45
|
+
.text-center.border.p-1.border-transparent.text-sm
|
|
46
|
+
template data-satis-date-time-picker-target="dayTemplate"
|
|
47
|
+
div
|
|
48
|
+
a.block.w-full.h-9.cursor-pointer.text-center.pt-2px.text-sm.rounded-l-full.rounded-r-full.leading-loose.transition.ease-in-out.duration-100.hover:bg-primary-200 data-action="satis-date-time-picker#selectDay" ${day}
|