fino-ui 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 54c10c469d72a79814e32a94385b0a252e5fcfba83392c29364b1f5834d51861
4
+ data.tar.gz: 040f6018d78d5ce57d13d12c3be69db3f0388c3869494de73ba6e96b1bc7721e
5
+ SHA512:
6
+ metadata.gz: ad2c824c708cd4551e396a61c1c7477da1db868c2c170efdb08728f21fed77380d68ac651534b407d63a2ee42c0fb304ab649b2ccc846f3d90ea7ceab6424fd3
7
+ data.tar.gz: 4d1436a40e58e9910c0eb45ef68437b1ba2b8a82f3aadc36d8e95edb8c48909987b9cfad76313d48b022252ab0a472af84e4a2393278e35a6b87d66711095890
data/README.md ADDED
@@ -0,0 +1,11 @@
1
+ # Fino
2
+
3
+ ```ruby
4
+ Fino.value(:retries_amount)
5
+ Fino.value(:http_read_timeout, :service_a)
6
+ ```
7
+
8
+ ## TODO
9
+
10
+ - Basic validations (presence, range, numericality)
11
+ - Enum setting type
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Fino::UI::ApplicationController < ActionController::Base
4
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Fino::UI::SettingsController < Fino::UI::ApplicationController
4
+ def index
5
+ @settings = Fino.library.all
6
+ end
7
+
8
+ def edit
9
+ setting_path = parse_setting_path(params[:key])
10
+
11
+ @setting = Fino.setting(*setting_path)
12
+ end
13
+
14
+ def update
15
+ begin
16
+ # Parse the key to create the setting path
17
+ setting_path = parse_setting_path(params[:key])
18
+
19
+ # Update the setting using the correct API
20
+ Fino.set(params[:value], *setting_path)
21
+
22
+ redirect_to root_path, notice: "Setting updated successfully"
23
+ rescue Fino::Registry::UnknownSetting
24
+ redirect_to root_path, alert: "Setting not found"
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def parse_setting_path(key)
31
+ key.split('.').map(&:to_sym).reverse
32
+ end
33
+ end
@@ -0,0 +1,182 @@
1
+ <% content_for(:title, "Edit #{@setting.name}") %>
2
+ <% content_for(:head) do %>
3
+ <style>
4
+ .form-group {
5
+ margin-bottom: 1.5rem;
6
+ }
7
+
8
+ .form-label {
9
+ display: block;
10
+ font-size: 0.875rem;
11
+ font-weight: 500;
12
+ color: #0f172a;
13
+ margin-bottom: 0.5rem;
14
+ }
15
+
16
+ .form-input {
17
+ display: block;
18
+ width: 100%;
19
+ padding: 0.5rem 0.75rem;
20
+ font-size: 0.875rem;
21
+ line-height: 1.5;
22
+ color: #0f172a;
23
+ background-color: #ffffff;
24
+ border: 1px solid #e2e8f0;
25
+ border-radius: 6px;
26
+ transition: border-color 0.15s ease-in-out;
27
+ }
28
+
29
+ .form-input:focus {
30
+ outline: none;
31
+ border-color: #0f172a;
32
+ box-shadow: 0 0 0 3px rgba(15, 23, 42, 0.1);
33
+ }
34
+
35
+ .form-select {
36
+ display: block;
37
+ width: 100%;
38
+ padding: 0.5rem 0.75rem;
39
+ font-size: 0.875rem;
40
+ line-height: 1.5;
41
+ color: #0f172a;
42
+ background-color: #ffffff;
43
+ border: 1px solid #e2e8f0;
44
+ border-radius: 6px;
45
+ transition: border-color 0.15s ease-in-out;
46
+ }
47
+
48
+ .form-select:focus {
49
+ outline: none;
50
+ border-color: #0f172a;
51
+ box-shadow: 0 0 0 3px rgba(15, 23, 42, 0.1);
52
+ }
53
+
54
+ .form-checkbox {
55
+ width: 1rem;
56
+ height: 1rem;
57
+ border: 1px solid #e2e8f0;
58
+ border-radius: 4px;
59
+ margin-right: 0.5rem;
60
+ }
61
+
62
+ .form-help {
63
+ font-size: 0.75rem;
64
+ color: #64748b;
65
+ margin-top: 0.25rem;
66
+ }
67
+
68
+ .button-group {
69
+ display: flex;
70
+ gap: 0.75rem;
71
+ margin-top: 2rem;
72
+ }
73
+
74
+ .breadcrumb {
75
+ margin-bottom: 1.5rem;
76
+ font-size: 0.875rem;
77
+ color: #64748b;
78
+ }
79
+
80
+ .breadcrumb a {
81
+ color: #0f172a;
82
+ text-decoration: none;
83
+ }
84
+
85
+ .breadcrumb a:hover {
86
+ text-decoration: underline;
87
+ }
88
+
89
+ .setting-info {
90
+ background-color: #f8fafc;
91
+ border: 1px solid #e2e8f0;
92
+ border-radius: 6px;
93
+ padding: 1rem;
94
+ margin-bottom: 1.5rem;
95
+ }
96
+
97
+ .setting-info-title {
98
+ font-weight: 600;
99
+ margin-bottom: 0.5rem;
100
+ color: #0f172a;
101
+ }
102
+
103
+ .setting-info-detail {
104
+ font-size: 0.875rem;
105
+ color: #64748b;
106
+ margin-bottom: 0.25rem;
107
+ }
108
+ </style>
109
+ <% end %>
110
+
111
+ <div class="breadcrumb">
112
+ <%= link_to "Settings", root_path %> / Edit <%= @setting.name %>
113
+ </div>
114
+
115
+ <div class="header">
116
+ <h1>Edit Setting</h1>
117
+ <p>Modify the configuration value for this setting</p>
118
+ </div>
119
+
120
+ <div class="card">
121
+ <div class="card-header">
122
+ <h2 class="card-title"><%= @setting.name %></h2>
123
+ </div>
124
+ <div class="card-content">
125
+ <div class="setting-info">
126
+ <div class="setting-info-title">Setting Information</div>
127
+ <div class="setting-info-detail"><strong>Type:</strong> <%= @setting.class.name.demodulize %></div>
128
+ <% if @setting.section_name %>
129
+ <div class="setting-info-detail"><strong>Section:</strong> <%= @setting.section_name %></div>
130
+ <% end %>
131
+ <div class="setting-info-detail"><strong>Default:</strong> <code><%= @setting.definition.default.inspect %></code></div>
132
+ <% if @setting.definition.options[:description].present? %>
133
+ <div class="setting-info-detail"><strong>Description:</strong> <%= @setting.definition.options[:description] %></div>
134
+ <% end %>
135
+ </div>
136
+
137
+ <%= form_with url: setting_path(@setting.section_name ? "#{@setting.section_name}.#{@setting.name}" : @setting.name), method: :patch, local: true do |f| %>
138
+ <div class="form-group">
139
+ <%= f.label :value, class: "form-label" do %>
140
+ Current Value
141
+ <% if @setting.definition.options[:description].present? %>
142
+ <div class="form-help"><%= @setting.definition.options[:description] %></div>
143
+ <% end %>
144
+ <% end %>
145
+
146
+ <% case @setting.class.name.demodulize.downcase %>
147
+ <% when 'booleansetting' %>
148
+ <%= f.select :value,
149
+ options_for_select([
150
+ ['True', 'true'],
151
+ ['False', 'false']
152
+ ], @setting.value.to_s),
153
+ {},
154
+ { class: "form-select" } %>
155
+ <% when 'integersetting' %>
156
+ <%= f.number_field :value,
157
+ value: @setting.value,
158
+ class: "form-input",
159
+ step: 1 %>
160
+ <% when 'floatsetting' %>
161
+ <%= f.number_field :value,
162
+ value: @setting.value,
163
+ class: "form-input",
164
+ step: 0.1 %>
165
+ <% else %>
166
+ <%= f.text_field :value,
167
+ value: @setting.value,
168
+ class: "form-input" %>
169
+ <% end %>
170
+
171
+ <div class="form-help">
172
+ Default value: <%= @setting.definition.default.inspect %>
173
+ </div>
174
+ </div>
175
+
176
+ <div class="button-group">
177
+ <%= f.submit "Update Setting", class: "btn btn-primary" %>
178
+ <%= link_to "Cancel", root_path, class: "btn btn-secondary" %>
179
+ </div>
180
+ <% end %>
181
+ </div>
182
+ </div>
@@ -0,0 +1,159 @@
1
+ <% content_for(:title, 'Application Settings') %>
2
+ <% content_for(:head) do %>
3
+ <style>
4
+ .settings-grid {
5
+ display: grid;
6
+ grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
7
+ gap: 1.5rem;
8
+ }
9
+
10
+ .setting-item {
11
+ display: block;
12
+ padding: 1rem;
13
+ border-bottom: 1px solid #e2e8f0;
14
+ text-decoration: none;
15
+ color: inherit;
16
+ transition: background-color 0.15s ease-in-out;
17
+ }
18
+
19
+ .setting-item:hover {
20
+ background-color: #f8fafc;
21
+ }
22
+
23
+ .setting-item:last-child {
24
+ border-bottom: none;
25
+ }
26
+
27
+ .setting-name {
28
+ font-weight: 600;
29
+ color: #0f172a;
30
+ font-size: 0.875rem;
31
+ margin-bottom: 0.25rem;
32
+ display: flex;
33
+ align-items: center;
34
+ gap: 0.5rem;
35
+ }
36
+
37
+ .setting-type {
38
+ display: inline-block;
39
+ padding: 0.125rem 0.5rem;
40
+ background-color: #f1f5f9;
41
+ color: #475569;
42
+ font-size: 0.75rem;
43
+ font-weight: 500;
44
+ border-radius: 4px;
45
+ text-transform: uppercase;
46
+ letter-spacing: 0.025em;
47
+ }
48
+
49
+ .setting-value {
50
+ color: #0f172a;
51
+ font-size: 0.875rem;
52
+ margin-top: 0.5rem;
53
+ font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Courier New', monospace;
54
+ background-color: #f8fafc;
55
+ padding: 0.5rem;
56
+ border-radius: 4px;
57
+ border: 1px solid #e2e8f0;
58
+ font-weight: 500;
59
+ }
60
+
61
+ .setting-default {
62
+ color: #64748b;
63
+ font-size: 0.75rem;
64
+ margin-top: 0.25rem;
65
+ font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Courier New', monospace;
66
+ }
67
+
68
+ .setting-description {
69
+ color: #64748b;
70
+ font-size: 0.875rem;
71
+ margin-top: 0.25rem;
72
+ font-style: italic;
73
+ }
74
+
75
+ .empty-state {
76
+ text-align: center;
77
+ padding: 3rem 1rem;
78
+ color: #64748b;
79
+ }
80
+
81
+ .empty-state svg {
82
+ width: 48px;
83
+ height: 48px;
84
+ margin: 0 auto 1rem;
85
+ color: #cbd5e1;
86
+ }
87
+
88
+ @media (max-width: 640px) {
89
+ .settings-grid {
90
+ grid-template-columns: 1fr;
91
+ }
92
+ }
93
+ </style>
94
+ <% end %>
95
+
96
+ <div class="header">
97
+ <h1>Application Settings</h1>
98
+ <p>Global configuration settings for your application</p>
99
+ </div>
100
+
101
+ <%
102
+ # Group settings by section
103
+ grouped_settings = @settings.group_by { |setting| setting.section_name || 'Global' }
104
+
105
+ if grouped_settings.empty?
106
+ %>
107
+ <div class="empty-state">
108
+ <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
109
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
110
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
111
+ </svg>
112
+ <h3>No settings configured</h3>
113
+ <p>Configure settings in your application to see them here.</p>
114
+ </div>
115
+ <% else %>
116
+ <div class="settings-grid">
117
+ <%
118
+ # Show Global settings first, then other sections
119
+ sections_order = grouped_settings.keys.sort_by { |section| section == 'Global' ? 0 : 1 }
120
+ sections_order.each do |section_name|
121
+ settings = grouped_settings[section_name]
122
+ is_global = section_name == 'Global'
123
+ %>
124
+ <div class="card">
125
+ <div class="card-header">
126
+ <h2 class="card-title">
127
+ <%= is_global ? 'Global Settings' : section_name.to_s.humanize %>
128
+ </h2>
129
+ </div>
130
+ <div class="card-content" style="padding: 0;">
131
+ <% settings.each do |setting| %>
132
+ <%
133
+ # Create a unique key for the setting based on section and name
134
+ setting_key = [setting.section_name, setting.name].compact.join('.')
135
+ %>
136
+ <%= link_to edit_setting_path(setting_key), class: "setting-item" do %>
137
+ <div class="setting-name">
138
+ <%= setting.name %>
139
+ <span class="setting-type"><%= setting.class.name.demodulize %></span>
140
+ </div>
141
+
142
+ <% if setting.definition.options[:description].present? %>
143
+ <div class="setting-description"><%= setting.definition.options[:description] %></div>
144
+ <% end %>
145
+
146
+ <div class="setting-value">
147
+ <%= setting.value.inspect %>
148
+ </div>
149
+
150
+ <div class="setting-default">
151
+ Default: <%= setting.default.inspect %>
152
+ </div>
153
+ <% end %>
154
+ <% end %>
155
+ </div>
156
+ </div>
157
+ <% end %>
158
+ </div>
159
+ <% end %>
@@ -0,0 +1,138 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Fino - <%= yield(:title) || 'Settings' %></title>
7
+ <style>
8
+ * {
9
+ box-sizing: border-box;
10
+ margin: 0;
11
+ padding: 0;
12
+ }
13
+
14
+ body {
15
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
16
+ line-height: 1.6;
17
+ color: #0f172a;
18
+ background-color: #ffffff;
19
+ min-height: 100vh;
20
+ }
21
+
22
+ .container {
23
+ max-width: 1200px;
24
+ margin: 0 auto;
25
+ padding: 2rem 1rem;
26
+ }
27
+
28
+ .header {
29
+ margin-bottom: 2rem;
30
+ }
31
+
32
+ .header h1 {
33
+ font-size: 2rem;
34
+ font-weight: 600;
35
+ color: #0f172a;
36
+ margin-bottom: 0.5rem;
37
+ }
38
+
39
+ .header p {
40
+ color: #64748b;
41
+ font-size: 1rem;
42
+ }
43
+
44
+ .card {
45
+ background: #ffffff;
46
+ border-radius: 8px;
47
+ border: 1px solid #e2e8f0;
48
+ }
49
+
50
+ .card-header {
51
+ padding: 1.5rem;
52
+ border-bottom: 1px solid #e2e8f0;
53
+ }
54
+
55
+ .card-title {
56
+ font-size: 1.125rem;
57
+ font-weight: 600;
58
+ color: #0f172a;
59
+ }
60
+
61
+ .card-content {
62
+ padding: 1.5rem;
63
+ }
64
+
65
+ .btn {
66
+ display: inline-flex;
67
+ align-items: center;
68
+ justify-content: center;
69
+ border-radius: 6px;
70
+ font-size: 0.875rem;
71
+ font-weight: 500;
72
+ transition: colors 0.15s ease-in-out;
73
+ cursor: pointer;
74
+ text-decoration: none;
75
+ border: 1px solid transparent;
76
+ }
77
+
78
+ .btn-primary {
79
+ background-color: #0f172a;
80
+ color: #ffffff;
81
+ padding: 0.5rem 1rem;
82
+ }
83
+
84
+ .btn-primary:hover {
85
+ background-color: #1e293b;
86
+ }
87
+
88
+ .btn-secondary {
89
+ background-color: transparent;
90
+ color: #0f172a;
91
+ border-color: #e2e8f0;
92
+ padding: 0.5rem 1rem;
93
+ }
94
+
95
+ .btn-secondary:hover {
96
+ background-color: #f8fafc;
97
+ }
98
+
99
+ .btn-ghost {
100
+ background-color: transparent;
101
+ color: #0f172a;
102
+ padding: 0.5rem;
103
+ }
104
+
105
+ .btn-ghost:hover {
106
+ background-color: #f8fafc;
107
+ }
108
+
109
+ .text-muted {
110
+ color: #64748b;
111
+ }
112
+
113
+ .text-sm {
114
+ font-size: 0.875rem;
115
+ }
116
+
117
+ .text-xs {
118
+ font-size: 0.75rem;
119
+ }
120
+
121
+ @media (max-width: 640px) {
122
+ .container {
123
+ padding: 1rem 0.5rem;
124
+ }
125
+
126
+ .header h1 {
127
+ font-size: 1.5rem;
128
+ }
129
+ }
130
+ </style>
131
+ <%= yield(:head) %>
132
+ </head>
133
+ <body>
134
+ <div class="container">
135
+ <%= yield %>
136
+ </div>
137
+ </body>
138
+ </html>
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ ActiveSupport::Inflector.inflections(:en) do |inflect|
4
+ inflect.acronym "UI"
5
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ Fino::UI::Engine.routes.draw do
4
+ root to: "settings#index"
5
+
6
+ resources :settings, only: [:index, :edit, :update], param: :key, constraints: { key: /[^\/]+/ }
7
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fino-ui"
4
+
5
+ class Fino::UI::Engine < Rails::Engine
6
+ isolate_namespace Fino::UI
7
+
8
+ paths["app"] << root.join("lib", "fino", "ui", "app")
9
+ paths["config/initializers"] << root.join("lib", "fino", "ui", "config", "initializers")
10
+
11
+ initializer "fino.ui.append_view_paths" do |_app|
12
+ ActiveSupport.on_load :action_controller do
13
+ prepend_view_path Fino::UI::Engine.root.join("lib", "fino", "ui", "app", "views")
14
+ end
15
+ end
16
+
17
+ initializer "fino.ui.load_routes", before: :add_routing_paths do |app|
18
+ custom_routes = root.join("lib", "fino", "ui", "config", "routes.rb")
19
+ app.routes_reloader.paths << custom_routes.to_s
20
+ end
21
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fino
4
+ VERSION = "1.0.0"
5
+ end
metadata ADDED
@@ -0,0 +1,65 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fino-ui
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Egor Iskrenkov
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: fino
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: 1.0.0
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: 1.0.0
26
+ email:
27
+ - egor@iskrenkov.me
28
+ executables: []
29
+ extensions: []
30
+ extra_rdoc_files: []
31
+ files:
32
+ - README.md
33
+ - lib/fino/ui/app/controllers/fino/ui/application_controller.rb
34
+ - lib/fino/ui/app/controllers/fino/ui/settings_controller.rb
35
+ - lib/fino/ui/app/views/fino/ui/settings/edit.html.erb
36
+ - lib/fino/ui/app/views/fino/ui/settings/index.html.erb
37
+ - lib/fino/ui/app/views/layouts/fino/ui/application.html.erb
38
+ - lib/fino/ui/config/initializers/inflections.rb
39
+ - lib/fino/ui/config/routes.rb
40
+ - lib/fino/ui/engine.rb
41
+ - lib/fino/version.rb
42
+ homepage: https://github.com/eiskrenkov/fino
43
+ licenses: []
44
+ metadata:
45
+ homepage_uri: https://github.com/eiskrenkov/fino
46
+ source_code_uri: https://github.com/eiskrenkov/fino
47
+ rubygems_mfa_required: 'true'
48
+ rdoc_options: []
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: 3.0.0
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ requirements: []
62
+ rubygems_version: 3.6.9
63
+ specification_version: 4
64
+ summary: UI for Fino settings engine
65
+ test_files: []