lesli_admin 1.1.1 → 1.1.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.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +3 -0
  3. data/app/assets/stylesheets/lesli_admin/application.css +648 -584
  4. data/app/controllers/lesli_admin/account/details_controller.rb +11 -4
  5. data/app/controllers/lesli_admin/account/locations_controller.rb +8 -4
  6. data/app/controllers/lesli_admin/accounts_controller.rb +41 -27
  7. data/app/controllers/lesli_admin/dashboards_controller.rb +21 -0
  8. data/app/models/lesli_admin/account/currency.rb +4 -2
  9. data/app/models/lesli_admin/account/detail.rb +3 -0
  10. data/app/models/lesli_admin/account/location.rb +12 -2
  11. data/app/models/lesli_admin/account/setting.rb +4 -2
  12. data/app/models/lesli_admin/account.rb +18 -1
  13. data/app/models/lesli_admin/dashboard.rb +8 -1
  14. data/app/services/lesli_admin/account_service.rb +43 -16
  15. data/app/views/lesli_admin/accounts/show.html.erb +104 -7
  16. data/app/views/lesli_admin/dashboards/_component-account-metadata.html.erb +44 -0
  17. data/app/views/lesli_admin/dashboards/_component-installed-engines.html.erb +1 -1
  18. data/app/views/lesli_admin/dashboards/_component-roles.html.erb +1 -0
  19. data/app/views/lesli_admin/dashboards/_component-users.html.erb +1 -0
  20. data/app/views/lesli_admin/settings/_configuration_value.html.erb +52 -0
  21. data/app/views/lesli_admin/settings/_defaults.html.erb +145 -58
  22. data/db/migrate/v1/0101000210_create_lesli_admin_account_details.rb +7 -1
  23. data/db/migrate/v1/0101000410_create_lesli_admin_account_locations.rb +6 -1
  24. data/lib/lesli_admin/engine.rb +0 -4
  25. data/lib/lesli_admin/version.rb +1 -1
  26. data/readme.md +8 -5
  27. metadata +10 -5
  28. data/app/assets/config/lesli_admin_manifest.js +0 -38
  29. data/app/assets/stylesheets/lesli_admin/application.tailwind.css +0 -2
@@ -1,71 +1,158 @@
1
1
  <%
2
- sections = {
3
- "Instance Info" => {
4
- demo: Lesli.config.demo,
5
- instance: Lesli.config.instance
2
+ sections = [
3
+ {
4
+ title: "Instance",
5
+ icon: "dns",
6
+ description: "Runtime identity and operating mode for this installation.",
7
+ config: {
8
+ demo: Lesli.config.demo,
9
+ instance: Lesli.config.instance
10
+ }
6
11
  },
7
- "Company Info" => Lesli.config.company,
8
- "Available Locales" => Lesli.config.babel[:locales],
9
- "Datetime Settings" => Lesli.config.datetime,
10
- "Security Settings" => Lesli.config.security,
11
- "Theme" => Lesli.config.theme,
12
- "Layout" => Lesli.config.layout,
13
- "Mailer" => Lesli.config.mailer,
14
- "Paths" => {
15
- path_after_login: Lesli.config.shield[:path_after_login]
12
+ {
13
+ title: "Company",
14
+ icon: "apartment",
15
+ description: "Organization details displayed throughout the platform.",
16
+ config: Lesli.config.company
17
+ },
18
+ {
19
+ title: "Locales",
20
+ icon: "translate",
21
+ description: "Languages currently available to users.",
22
+ config: Lesli.config.babel&.fetch(:locales, {})
23
+ },
24
+ {
25
+ title: "Date and time",
26
+ icon: "schedule",
27
+ description: "Time zone, week boundaries, and display formats.",
28
+ config: Lesli.config.datetime
29
+ },
30
+ {
31
+ title: "Security",
32
+ icon: "shield_lock",
33
+ description: "Authentication and account-access behavior.",
34
+ config: Lesli.config.security
35
+ },
36
+ {
37
+ title: "Theme",
38
+ icon: "palette",
39
+ description: "Colors used by the shared Lesli interface.",
40
+ config: Lesli.config.theme
41
+ },
42
+ {
43
+ title: "Layout",
44
+ icon: "dashboard_customize",
45
+ description: "Global interface features and visibility controls.",
46
+ config: Lesli.config.layout
47
+ },
48
+ {
49
+ title: "Mailer",
50
+ icon: "mail",
51
+ description: "Email delivery and template configuration.",
52
+ config: Lesli.config.mailer
53
+ },
54
+ {
55
+ title: "Paths",
56
+ icon: "route",
57
+ description: "Application destinations used by navigation and authentication.",
58
+ config: {
59
+ path_after_login: Lesli.config.shield&.fetch(:path_after_login, nil)
60
+ }
16
61
  }
17
- }
62
+ ].map { |section| section.merge(config: section[:config] || {}) }
63
+
64
+ leaf_count = lambda do |value|
65
+ case value
66
+ when Hash
67
+ value.values.sum { |nested_value| leaf_count.call(nested_value) }
68
+ when Array
69
+ value.sum { |nested_value| leaf_count.call(nested_value) }
70
+ else
71
+ 1
72
+ end
73
+ end
18
74
 
19
- grid_classes = {
20
- "Instance Info" => "sm:grid-cols-2 lg:grid-cols-4",
21
- "Datetime Settings" => "sm:grid-cols-2",
22
- "Security Settings" => "sm:grid-cols-2 lg:grid-cols-4",
23
- "Theme" => "sm:grid-cols-2 lg:grid-cols-3"
24
- }
75
+ total_settings = sections.sum { |section| leaf_count.call(section[:config]) }
25
76
  %>
26
77
 
27
78
  <section aria-labelledby="lesli-configuration-title">
28
- <h4
29
- id="lesli-configuration-title"
30
- class="mb-5 text-xl font-semibold tracking-tight text-slate-900">
31
- Lesli Configuration
32
- </h4>
33
79
 
34
- <div class="space-y-5">
35
- <% sections.each do |title, config| %>
36
- <article class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm sm:p-6">
37
- <h5 class="mb-4 text-base font-semibold text-slate-900"><%= title %></h5>
80
+ <div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm sm:p-6">
81
+ <div class="flex items-center gap-3">
82
+ <span class="material-symbols-outlined rounded-xl bg-sky-50 p-2.5 text-2xl text-sky-700" aria-hidden="true">settings</span>
83
+ <div>
84
+ <h2 id="lesli-configuration-title" class="text-xl font-semibold tracking-tight text-slate-900">
85
+ Lesli configuration
86
+ </h2>
87
+ <p class="mt-1 text-sm leading-6 text-slate-500">
88
+ Effective read-only values loaded by this application instance.
89
+ </p>
90
+ </div>
91
+ </div>
92
+
93
+ <dl class="mt-6 grid grid-cols-2 gap-3 lg:grid-cols-4">
94
+ <div class="rounded-xl bg-slate-50 px-4 py-3">
95
+ <dt class="text-xs font-medium uppercase tracking-wide text-slate-500">Environment</dt>
96
+ <dd class="mt-1 text-sm font-semibold text-slate-900"><%= Rails.env.to_s.humanize %></dd>
97
+ </div>
98
+ <div class="rounded-xl bg-slate-50 px-4 py-3">
99
+ <dt class="text-xs font-medium uppercase tracking-wide text-slate-500">Sections</dt>
100
+ <dd class="mt-1 text-sm font-semibold text-slate-900"><%= sections.size %></dd>
101
+ </div>
102
+ <div class="rounded-xl bg-slate-50 px-4 py-3">
103
+ <dt class="text-xs font-medium uppercase tracking-wide text-slate-500">Values</dt>
104
+ <dd class="mt-1 text-sm font-semibold text-slate-900"><%= total_settings %></dd>
105
+ </div>
106
+ <div class="rounded-xl bg-slate-50 px-4 py-3">
107
+ <dt class="text-xs font-medium uppercase tracking-wide text-slate-500">Demo mode</dt>
108
+ <dd class="mt-1">
109
+ <span class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-semibold <%= Lesli.config.demo ? "bg-amber-100 text-amber-700" : "bg-emerald-100 text-emerald-700" %>">
110
+ <%= Lesli.config.demo ? "Enabled" : "Disabled" %>
111
+ </span>
112
+ </dd>
113
+ </div>
114
+ </dl>
115
+ </div>
116
+
117
+ <div class="mt-5 space-y-4">
118
+ <% sections.each_with_index do |section, index| %>
119
+ <details
120
+ class="group rounded-2xl border border-slate-200 bg-white shadow-sm"
121
+ <%= "open" if index < 2 %>>
122
+ <summary class="flex cursor-pointer list-none items-center gap-4 px-5 py-4 marker:hidden sm:px-6">
123
+ <span class="material-symbols-outlined rounded-xl bg-slate-100 p-2.5 text-2xl text-slate-600" aria-hidden="true"><%= section[:icon] %></span>
124
+ <span class="min-w-0 flex-1">
125
+ <span class="block text-base font-semibold text-slate-900"><%= section[:title] %></span>
126
+ <span class="mt-0.5 block text-sm leading-5 text-slate-500"><%= section[:description] %></span>
127
+ </span>
128
+ <span class="hidden rounded-full bg-slate-100 px-2.5 py-1 text-xs font-semibold text-slate-600 sm:inline-flex">
129
+ <%= pluralize(section[:config].size, "group") %>
130
+ </span>
131
+ <span class="material-symbols-outlined text-slate-400 transition-transform group-open:rotate-180" aria-hidden="true">expand_more</span>
132
+ </summary>
38
133
 
39
- <dl class="grid grid-cols-1 gap-x-6 gap-y-5 <%= grid_classes.fetch(title, "sm:grid-cols-2 lg:grid-cols-4") %>">
40
- <% config.each do |key, value| %>
41
- <div class="min-w-0">
42
- <dt class="text-sm font-medium text-slate-600"><%= key.to_s.humanize %></dt>
43
- <dd class="mt-2 text-sm text-slate-900">
44
- <% if value.is_a?(Hash) %>
45
- <ul class="space-y-2 border-l-2 border-slate-200 pl-3">
46
- <% value.each do |nested_key, nested_value| %>
47
- <li class="min-w-0">
48
- <span class="font-medium text-slate-700"><%= nested_key.to_s.humanize %>:</span>
49
- <code class="ml-1 break-all rounded bg-slate-100 px-1.5 py-0.5 text-xs text-slate-700"><%= nested_value %></code>
50
- </li>
51
- <% end %>
52
- </ul>
53
- <% elsif value == true %>
54
- <span class="inline-flex items-center rounded-full bg-emerald-100 px-2.5 py-1 text-xs font-semibold text-emerald-700">
55
- Enabled
56
- </span>
57
- <% elsif value == false %>
58
- <span class="inline-flex items-center rounded-full bg-red-100 px-2.5 py-1 text-xs font-semibold text-red-700">
59
- Disabled
60
- </span>
61
- <% else %>
62
- <code class="break-all rounded bg-slate-100 px-1.5 py-1 text-xs text-slate-700"><%= value.to_s %></code>
134
+ <div class="border-t border-slate-100 px-5 py-5 sm:px-6">
135
+ <% if section[:config].empty? %>
136
+ <p class="text-sm text-slate-500">No values are configured for this section.</p>
137
+ <% else %>
138
+ <dl class="grid grid-cols-1 gap-4 lg:grid-cols-2">
139
+ <% section[:config].each do |key, value| %>
140
+ <div class="min-w-0 rounded-xl border border-slate-100 bg-slate-50/60 p-4">
141
+ <dt class="text-xs font-semibold uppercase tracking-wide text-slate-500"><%= key.to_s.humanize %></dt>
142
+ <dd class="mt-2 text-sm text-slate-900">
143
+ <%= render("configuration_value", setting_key: key, value: value) %>
144
+ </dd>
145
+ </div>
63
146
  <% end %>
64
- </dd>
65
- </div>
66
- <% end %>
67
- </dl>
68
- </article>
147
+ </dl>
148
+ <% end %>
149
+ </div>
150
+ </details>
69
151
  <% end %>
70
152
  </div>
153
+
154
+ <p class="mt-5 flex items-start gap-2 text-xs leading-5 text-slate-500">
155
+ <span class="material-symbols-outlined text-base" aria-hidden="true">lock</span>
156
+ Sensitive values are filtered on the server and are never included in this page.
157
+ </p>
71
158
  </section>
@@ -65,6 +65,12 @@ class CreateLesliAdminAccountDetails < ActiveRecord::Migration[6.0]
65
65
  t.datetime :deleted_at, index: true
66
66
  t.timestamps
67
67
  end
68
- add_reference(:lesli_admin_account_details, :account, foreign_key: { to_table: :lesli_accounts })
68
+ add_reference(
69
+ :lesli_admin_account_details,
70
+ :account,
71
+ null: false,
72
+ index: { unique: true },
73
+ foreign_key: { to_table: :lesli_admin_accounts }
74
+ )
69
75
  end
70
76
  end
@@ -57,7 +57,12 @@ class CreateLesliAdminAccountLocations < ActiveRecord::Migration[6.0]
57
57
  end
58
58
 
59
59
  add_reference(:lesli_admin_account_locations, :parent, foreign_key: { to_table: :lesli_admin_account_locations })
60
- add_reference(:lesli_admin_account_locations, :account, foreign_key: { to_table: :lesli_accounts })
60
+ add_reference(
61
+ :lesli_admin_account_locations,
62
+ :account,
63
+ null: false,
64
+ foreign_key: { to_table: :lesli_admin_accounts }
65
+ )
61
66
  add_index(:lesli_admin_account_locations, %i[account_id name level parent_id], unique: true, name: "location_uniqueness_index")
62
67
  end
63
68
  end
@@ -35,10 +35,6 @@ module LesliAdmin
35
35
  isolate_namespace LesliAdmin
36
36
 
37
37
  initializer :lesli_admin do |app|
38
-
39
- # register assets manifest
40
- config.assets.precompile += %w[lesli_admin_manifest.js]
41
-
42
38
  # register engine migrations path
43
39
  unless app.root.to_s.match root.to_s
44
40
  config.paths["db/migrate"].expanded.each do |expanded_path|
@@ -1,4 +1,4 @@
1
1
  module LesliAdmin
2
- VERSION = "1.1.1"
2
+ VERSION = "1.1.2"
3
3
  BUILD = "1791607001"
4
4
  end
data/readme.md CHANGED
@@ -8,8 +8,8 @@
8
8
  <br />
9
9
 
10
10
  <div align="center">
11
- <a target="_blank" href="https://github.com/LesliTech/LesliAdmin/actions/workflows/lesli-ci.yaml">
12
- <img alt="LesliAdmin test status" src="https://img.shields.io/github/actions/workflow/status/LesliTech/LesliAdmin/lesli-ci.yaml?branch=master&style=for-the-badge&logo=github&label=tests">
11
+ <a target="_blank" href="https://github.com/LesliTech/LesliAdmin/actions/workflows/lesli-ci-engine.yaml">
12
+ <img alt="LesliAdmin test status" src="https://img.shields.io/github/actions/workflow/status/LesliTech/LesliAdmin/lesli-ci-engine.yaml?branch=master&style=for-the-badge&logo=github&label=tests">
13
13
  </a>
14
14
  <a target="_blank" href="https://rubygems.org/gems/lesli_admin">
15
15
  <img alt="Gem Version" src="https://img.shields.io/gem/v/lesli_admin?style=for-the-badge&logo=ruby">
@@ -45,8 +45,10 @@ It gives account owners and administrators a central interface for maintaining t
45
45
 
46
46
  ## Features
47
47
 
48
- - Core account name and email management
49
- - Framework configuration overview
48
+ - Core account identity, status, and data-region management
49
+ - Company profile, contact, address, and social-link management
50
+ - Read-only framework configuration viewer with server-side secret filtering
51
+ - Account dashboard with user, role, installed-engine, and lifecycle metrics
50
52
  - Installed-engine inventory and metadata
51
53
  - Account-scoped data structures for details, locations, settings, and currencies
52
54
  - Optional dashboard components through LesliDashboard
@@ -103,7 +105,7 @@ bin/rails routes -g admin
103
105
  bin/rails server
104
106
  ```
105
107
 
106
- Install [LesliDashboard](https://rubygems.org/gems/lesli_dashboard) when the application should render LesliAdmin's shared dashboard and installed-engines component.
108
+ Install [LesliDashboard](https://rubygems.org/gems/lesli_dashboard) when the application should persist and render LesliAdmin's account metrics and metadata components.
107
109
 
108
110
  <br />
109
111
 
@@ -149,6 +151,7 @@ bin/rails test
149
151
  - [Installation guide](https://www.lesli.dev/engines/admin/about/installation)
150
152
  - [Account management](https://www.lesli.dev/engines/admin/account/)
151
153
  - [Dashboard components](https://www.lesli.dev/engines/admin/about/dashboards)
154
+ - [Settings and configuration](https://www.lesli.dev/engines/admin/about/settings)
152
155
  - [Translations](https://www.lesli.dev/engines/admin/about/translations)
153
156
  - [Database](https://www.lesli.dev/engines/admin/about/database)
154
157
  - [Release notes](https://github.com/LesliTech/LesliAdmin/releases)
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lesli_admin
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.1
4
+ version: 1.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - The Lesli Development Team
8
+ autorequire:
8
9
  bindir: bin
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-10-23 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: lesli
@@ -31,11 +32,9 @@ extensions: []
31
32
  extra_rdoc_files: []
32
33
  files:
33
34
  - Rakefile
34
- - app/assets/config/lesli_admin_manifest.js
35
35
  - app/assets/images/lesli_admin/admin-logo.svg
36
36
  - app/assets/javascripts/lesli_admin/application.js
37
37
  - app/assets/stylesheets/lesli_admin/application.css
38
- - app/assets/stylesheets/lesli_admin/application.tailwind.css
39
38
  - app/controllers/lesli_admin/account/currencies_controller.rb
40
39
  - app/controllers/lesli_admin/account/details_controller.rb
41
40
  - app/controllers/lesli_admin/account/locations_controller.rb
@@ -87,12 +86,16 @@ files:
87
86
  - app/views/lesli_admin/account/settings/new.html.erb
88
87
  - app/views/lesli_admin/account/settings/show.html.erb
89
88
  - app/views/lesli_admin/accounts/show.html.erb
89
+ - app/views/lesli_admin/dashboards/_component-account-metadata.html.erb
90
90
  - app/views/lesli_admin/dashboards/_component-installed-engines.html.erb
91
+ - app/views/lesli_admin/dashboards/_component-roles.html.erb
92
+ - app/views/lesli_admin/dashboards/_component-users.html.erb
91
93
  - app/views/lesli_admin/dashboards/edit.html.erb
92
94
  - app/views/lesli_admin/dashboards/index.html.erb
93
95
  - app/views/lesli_admin/dashboards/new.html.erb
94
96
  - app/views/lesli_admin/dashboards/show.html.erb
95
97
  - app/views/lesli_admin/partials/_navigation.html.erb
98
+ - app/views/lesli_admin/settings/_configuration_value.html.erb
96
99
  - app/views/lesli_admin/settings/_defaults.html.erb
97
100
  - app/views/lesli_admin/settings/_form.html.erb
98
101
  - app/views/lesli_admin/settings/_modules.html.erb
@@ -134,6 +137,7 @@ metadata:
134
137
  source_code_uri: https://github.com/LesliTech/LesliAdmin
135
138
  bug_tracker_uri: https://github.com/LesliTech/LesliAdmin/issues
136
139
  documentation_uri: https://www.lesli.dev/engines/admin
140
+ post_install_message:
137
141
  rdoc_options: []
138
142
  require_paths:
139
143
  - lib
@@ -148,7 +152,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
148
152
  - !ruby/object:Gem::Version
149
153
  version: '0'
150
154
  requirements: []
151
- rubygems_version: 3.7.1
155
+ rubygems_version: 3.5.16
156
+ signing_key:
152
157
  specification_version: 4
153
158
  summary: Administration area for the Lesli Framework
154
159
  test_files: []
@@ -1,38 +0,0 @@
1
- /*
2
-
3
- Lesli
4
-
5
- Copyright (c) 2023, Lesli Technologies, S. A.
6
-
7
- This program is free software: you can redistribute it and/or modify
8
- it under the terms of the GNU General Public License as published by
9
- the Free Software Foundation, either version 3 of the License, or
10
- (at your option) any later version.
11
-
12
- This program is distributed in the hope that it will be useful,
13
- but WITHOUT ANY WARRANTY; without even the implied warranty of
14
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
- GNU General Public License for more details.
16
-
17
- You should have received a copy of the GNU General Public License
18
- along with this program. If not, see http://www.gnu.org/licenses/.
19
-
20
- Lesli · Ruby on Rails SaaS development platform.
21
-
22
- Made with ♥ by https://www.lesli.tech
23
- Building a better future, one line of code at a time.
24
-
25
- @contact hello@lesli.tech
26
- @website https://www.lesli.tech
27
- @license GPLv3 http://www.gnu.org/licenses/gpl-3.0.en.html
28
-
29
- // · ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~
30
- // ·
31
- */
32
-
33
- //= link_tree ../images/lesli_admin .png
34
- //= link_tree ../images/lesli_admin .jpg
35
- //= link_tree ../images/lesli_admin .svg
36
-
37
- //= link lesli_admin/application.css
38
- //= link lesli_admin/application.js
@@ -1,2 +0,0 @@
1
- /*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
2
- @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-leading:initial}}}@layer theme{:root,:host{--color-red-100:oklch(93.6% .032 17.717);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-emerald-100:oklch(95% .052 163.051);--color-emerald-500:oklch(69.6% .17 162.48);--color-emerald-600:oklch(59.6% .145 163.225);--color-emerald-700:oklch(50.8% .118 165.612);--color-slate-100:oklch(96.8% .007 247.896);--color-slate-200:oklch(92.9% .013 255.508);--color-slate-600:oklch(44.6% .043 257.281);--color-slate-700:oklch(37.2% .044 257.287);--color-slate-900:oklch(20.8% .042 265.755);--color-white:#fff;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--radius-lg:.5rem;--radius-2xl:1rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1)}}@layer components{.module-engine-card .partial-engine-spec>div{margin-bottom:calc(var(--spacing) * 4);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-slate-200);padding-bottom:calc(var(--spacing) * 4)}.module-engine-card .partial-engine-spec h5{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-slate-900)}.module-engine-card .partial-engine-spec p{margin-top:var(--spacing);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6);color:var(--color-slate-600)}.module-engine-card .partial-engine-spec ul{column-gap:calc(var(--spacing) * 4);row-gap:calc(var(--spacing) * 2);flex-wrap:wrap;display:flex}.module-engine-card .partial-engine-spec li{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--color-slate-600);margin:0}.module-engine-card .partial-engine-spec li:last-child,.module-engine-card .partial-engine-spec .material-symbols{display:none}}@layer utilities{.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-5{margin-top:calc(var(--spacing) * 5)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-5{margin-bottom:calc(var(--spacing) * 5)}.ml-1{margin-left:var(--spacing)}.flex{display:flex}.grid{display:grid}.inline-flex{display:inline-flex}.w-fit{width:fit-content}.min-w-0{min-width:0}.flex-1{flex:1}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-center{justify-content:center}.gap-5{gap:calc(var(--spacing) * 5)}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-6{column-gap:calc(var(--spacing) * 6)}.gap-y-5{row-gap:calc(var(--spacing) * 5)}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.border{border-style:var(--tw-border-style);border-width:1px}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-slate-200{border-color:var(--color-slate-200)}.bg-emerald-100{background-color:var(--color-emerald-100)}.bg-emerald-600{background-color:var(--color-emerald-600)}.bg-red-100{background-color:var(--color-red-100)}.bg-red-600{background-color:var(--color-red-600)}.bg-slate-100{background-color:var(--color-slate-100)}.bg-white{background-color:var(--color-white)}.p-5{padding:calc(var(--spacing) * 5)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:var(--spacing)}.py-2{padding-block:calc(var(--spacing) * 2)}.pl-3{padding-left:calc(var(--spacing) * 3)}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.break-all{word-break:break-all}.text-emerald-700{color:var(--color-emerald-700)}.text-red-700{color:var(--color-red-700)}.text-slate-600{color:var(--color-slate-600)}.text-slate-700{color:var(--color-slate-700)}.text-slate-900{color:var(--color-slate-900)}.text-white{color:var(--color-white)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media (hover:hover){.hover\:bg-emerald-700:hover{background-color:var(--color-emerald-700)}.hover\:bg-red-700:hover{background-color:var(--color-red-700)}}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus-visible\:ring-emerald-500\/30:focus-visible{--tw-ring-color:#00bb7f4d}@supports (color:color-mix(in lab, red, red)){.focus-visible\:ring-emerald-500\/30:focus-visible{--tw-ring-color:color-mix(in oklab, var(--color-emerald-500) 30%, transparent)}}.focus-visible\:ring-red-500\/30:focus-visible{--tw-ring-color:#fb2c364d}@supports (color:color-mix(in lab, red, red)){.focus-visible\:ring-red-500\/30:focus-visible{--tw-ring-color:color-mix(in oklab, var(--color-red-500) 30%, transparent)}}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.focus-visible\:outline-none:focus-visible{--tw-outline-style:none;outline-style:none}@media (min-width:40rem){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:p-6{padding:calc(var(--spacing) * 6)}}@media (min-width:64rem){.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media (min-width:80rem){.xl\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-leading{syntax:"*";inherits:false}